From 571064d06ff53dabb79928288e3da5a33d7946e0 Mon Sep 17 00:00:00 2001
From: Star Richardson <67430892+alicenstar@users.noreply.github.com>
Date: Mon, 16 Oct 2023 16:05:54 -0600
Subject: [PATCH] add new add node modal

---
 .../apps/HelmVMClusterManagement.jsx          | 321 +++++++++---------
 web/src/components/apps/HelmVMNodeRow.jsx     |   2 +-
 2 files changed, 156 insertions(+), 167 deletions(-)

diff --git a/web/src/components/apps/HelmVMClusterManagement.jsx b/web/src/components/apps/HelmVMClusterManagement.jsx
index 5d2ef854af..28d2c56860 100644
--- a/web/src/components/apps/HelmVMClusterManagement.jsx
+++ b/web/src/components/apps/HelmVMClusterManagement.jsx
@@ -1,6 +1,6 @@
 import classNames from "classnames";
 import dayjs from "dayjs";
-import React, { useEffect, useReducer } from "react";
+import React, { useEffect, useReducer, useState } from "react";
 import Modal from "react-modal";
 import { useQuery } from "react-query";
 
@@ -111,6 +111,8 @@ const HelmVMClusterManagement = () => {
       drainNodeSuccessful: false,
     }
   );
+  const [selectedNodeTypes, setSelectedNodeTypes] = useState([]);
+  const [useStaticToken, setUseStaticToken] = useState(false);
 
   const { data: nodes, isLoading: nodesLoading } = useQuery({
     queryKey: "helmVmNodes",
@@ -309,37 +311,57 @@ const HelmVMClusterManagement = () => {
   };
 
   const onAddNodeClick = () => {
-    setState(
-      {
-        displayAddNode: true,
-      },
-      async () => {
-        await generateWorkerAddNodeCommand();
-      }
-    );
-  };
-
-  const onSelectNodeType = (event) => {
-    const value = event.currentTarget.value;
-    setState(
-      {
-        selectedNodeType: value,
-      },
-      async () => {
-        if (state.selectedNodeType === "secondary") {
-          await generateWorkerAddNodeCommand();
-        } else {
-          await generatePrimaryAddNodeCommand();
-        }
-      }
-    );
+    setState({
+      displayAddNode: true,
+    });
   };
 
   const ackDeleteNodeError = () => {
     setState({ deleteNodeError: "" });
   };
 
-  const { displayAddNode, generateCommandErrMsg } = state;
+  const NODE_TYPES = [
+    "controlplane",
+    "db",
+    "app",
+    "search",
+    "webserver",
+    "jobs",
+  ];
+
+  const determineDisabledState = (nodeType, selectedNodeTypes) => {
+    if (nodeType === "controlplane") {
+      const numControlPlanes = testData.nodes.reduce((acc, node) => {
+        if (node.labels.includes("controlplane")) {
+          acc++;
+        }
+        return acc;
+      });
+      return numControlPlanes === 3;
+    }
+    if (
+      (nodeType === "db" || nodeType === "search") &&
+      selectedNodeTypes.includes("webserver")
+    ) {
+      return true;
+    }
+    return false;
+  };
+
+  const handleSelectNodeType = (e) => {
+    const nodeType = e.currentTarget.value;
+    let types = selectedNodeTypes;
+
+    if (nodeType === "webserver") {
+      types = types.filter((type) => type !== "db" && type !== "search");
+    }
+
+    if (selectedNodeTypes.includes(nodeType)) {
+      setSelectedNodeTypes(types.filter((type) => type !== nodeType));
+    } else {
+      setSelectedNodeTypes([...types, nodeType]);
+    }
+  };
 
   if (nodesLoading) {
     return (
@@ -350,7 +372,7 @@ const HelmVMClusterManagement = () => {
   }
 
   return (
-    <div className="HelmVMClusterManagement--wrapper container flex-column flex1 u-overflow--auto u-paddingTop--50">
+    <div className="HelmVMClusterManagement--wrapper container flex-column flex1 u-overflow--auto u-paddingTop--50 tw-font-sans">
       <KotsPageTitle pageName="Cluster Management" />
       <div className="flex-column flex1 alignItems--center u-paddingBottom--50">
         <div className="flex1 flex-column centered-container">
@@ -358,7 +380,7 @@ const HelmVMClusterManagement = () => {
             <p className="flex-auto u-fontSize--larger u-fontWeight--bold u-textColor--primary u-paddingBottom--10">
               Cluster Nodes
             </p>
-            <p className="u-paddingBottom--10">
+            <p className="u-paddingBottom--10 tw-text-base">
               This section lists the nodes that are configured and shows the
               status/health of each. To add additional nodes to this cluster,
               click the "Add node" button at the bottom of this page.
@@ -377,146 +399,113 @@ const HelmVMClusterManagement = () => {
                 ))}
             </div>
           </div>
-          {(nodes?.isHelmVMEnabled || testData.isHelmVMEnabled) &&
-          Utilities.sessionRolesHasOneOf([rbacRoles.CLUSTER_ADMIN]) ? (
-            !displayAddNode ? (
-              <div className="flex justifyContent--center alignItems--center">
-                <button className="btn primary" onClick={onAddNodeClick}>
-                  Add a node
-                </button>
-              </div>
-            ) : (
-              <div className="flex-column">
-                <div>
-                  <p className="u-width--full u-fontSize--larger u-textColor--primary u-fontWeight--bold u-lineHeight--normal u-borderBottom--gray u-paddingBottom--10">
-                    Add a node
-                  </p>
-                </div>
-                <div className="flex justifyContent--center alignItems--center u-marginTop--15">
-                  <div
-                    className={classNames(
-                      "BoxedCheckbox flex-auto flex u-marginRight--20",
-                      {
-                        "is-active": state.selectedNodeType === "primary",
-                        "is-disabled": nodes ? !nodes?.ha : !testData?.ha,
-                      }
-                    )}
-                  >
-                    <input
-                      id="primaryNode"
-                      className="u-cursor--pointer hidden-input"
-                      type="radio"
-                      name="nodeType"
-                      value="primary"
-                      disabled={nodes ? !nodes?.ha : !testData?.ha}
-                      checked={state.selectedNodeType === "primary"}
-                      onChange={onSelectNodeType}
-                    />
-                    <label
-                      htmlFor="primaryNode"
-                      className="flex1 flex u-width--full u-position--relative u-cursor--pointer u-userSelect--none"
-                    >
-                      <div className="flex-auto">
-                        <Icon
-                          icon="commit"
-                          size={32}
-                          className="clickable u-marginRight--10"
-                        />
-                      </div>
-                      <div className="flex1">
-                        <p className="u-textColor--primary u-fontSize--normal u-fontWeight--medium">
-                          Primary Node
-                        </p>
-                        <p className="u-textColor--bodyCopy u-lineHeight--normal u-fontSize--small u-fontWeight--medium u-marginTop--5">
-                          Provides high availability
-                        </p>
-                      </div>
-                    </label>
-                  </div>
-                  <div
-                    className={classNames(
-                      "BoxedCheckbox flex-auto flex u-marginRight--20",
-                      {
-                        "is-active": state.selectedNodeType === "secondary",
-                      }
-                    )}
-                  >
-                    <input
-                      id="secondaryNode"
-                      className="u-cursor--pointer hidden-input"
-                      type="radio"
-                      name="nodeType"
-                      value="secondary"
-                      checked={state.selectedNodeType === "secondary"}
-                      onChange={onSelectNodeType}
-                    />
-                    <label
-                      htmlFor="secondaryNode"
-                      className="flex1 flex u-width--full u-position--relative u-cursor--pointer u-userSelect--none"
-                    >
-                      <div className="flex-auto">
-                        <Icon
-                          icon="commit"
-                          size={32}
-                          className="clickable u-marginRight--10"
-                        />
-                      </div>
-                      <div className="flex1">
-                        <p className="u-textColor--primary u-fontSize--normal u-fontWeight--medium">
-                          Secondary Node
-                        </p>
-                        <p className="u-textColor--bodyCopy u-lineHeight--normal u-fontSize--small u-fontWeight--medium u-marginTop--5">
-                          Optimal for running application workloads
-                        </p>
-                      </div>
-                    </label>
-                  </div>
-                </div>
-                {state.generating && (
-                  <div className="flex u-width--full justifyContent--center">
-                    <Loader size={60} />
-                  </div>
-                )}
-                {!state.generating && state.command?.length > 0 ? (
-                  <>
-                    <p className="u-fontSize--normal u-textColor--bodyCopy u-fontWeight--medium u-lineHeight--normal u-marginBottom--5 u-marginTop--15">
-                      Run this command on the node you wish to join the cluster
-                    </p>
-                    <CodeSnippet
-                      language="bash"
-                      canCopy={true}
-                      onCopyText={
-                        <span className="u-textColor--success">
-                          Command has been copied to your clipboard
-                        </span>
-                      }
-                    >
-                      {[state.command.join(" \\\n  ")]}
-                    </CodeSnippet>
-                    {state.expiry && (
-                      <span className="timestamp u-marginTop--15 u-width--full u-textAlign--right u-fontSize--small u-fontWeight--bold u-textColor--primary">
-                        {`Expires on ${dayjs(state.expiry).format(
-                          "MMM Do YYYY, h:mm:ss a z"
-                        )} UTC${(-1 * new Date().getTimezoneOffset()) / 60}`}
-                      </span>
-                    )}
-                  </>
-                ) : (
-                  <>
-                    {generateCommandErrMsg && (
-                      <div className="alignSelf--center u-marginTop--15">
-                        <span className="u-textColor--error">
-                          {generateCommandErrMsg}
-                        </span>
-                      </div>
-                    )}
-                  </>
-                )}
-              </div>
-            )
-          ) : null}
+          {Utilities.sessionRolesHasOneOf([rbacRoles.CLUSTER_ADMIN]) && (
+            <div className="flex justifyContent--center alignItems--center">
+              <button className="btn primary" onClick={onAddNodeClick}>
+                Add a node
+              </button>
+            </div>
+          )}
         </div>
       </div>
+      <Modal
+        isOpen={state.displayAddNode}
+        onRequestClose={() => setState({ displayAddNode: false })}
+        contentLabel="Add Node"
+        className="Modal"
+        ariaHideApp={false}
+      >
+        <div className="Modal-body tw-flex tw-flex-col tw-gap-4">
+          <div className="tw-flex">
+            <h1 className="u-fontSize--largest u-fontWeight--bold u-textColor--primary u-lineHeight--normal u-marginBottom--more">
+              Add A Node
+            </h1>
+            <Icon
+              icon="close"
+              size={14}
+              className="tw-ml-auto gray-color clickable close-icon"
+              onClick={() => setState({ displayAddNode: false })}
+            />
+          </div>
+          <p className="tw-text-base">
+            To add a node to this cluster, select the type of node you are
+            adding, and then select an installation method below. This screen
+            will automatically show the status when the node successfully joins
+            the cluster.
+          </p>
+          <div className="tw-grid tw-gap-2 tw-grid-cols-4 tw-auto-rows-auto">
+            {NODE_TYPES.map((nodeType) => (
+              <div
+                className={classNames("BoxedCheckbox", {
+                  "is-active": selectedNodeTypes.includes(nodeType),
+                  "is-disabled": determineDisabledState(
+                    nodeType,
+                    selectedNodeTypes
+                  ),
+                })}
+              >
+                <input
+                  id={`${nodeType}NodeType`}
+                  className="u-cursor--pointer hidden-input"
+                  type="checkbox"
+                  name={`${nodeType}NodeType`}
+                  value={nodeType}
+                  disabled={determineDisabledState(nodeType, selectedNodeTypes)}
+                  checked={selectedNodeTypes.includes(nodeType)}
+                  onChange={handleSelectNodeType}
+                />
+                <label
+                  htmlFor={`${nodeType}NodeType`}
+                  className="tw-block u-cursor--pointer u-userSelect--none u-textColor--primary u-fontSize--normal u-fontWeight--medium tw-text-center"
+                >
+                  {nodeType}
+                </label>
+              </div>
+            ))}
+          </div>
+          <div>
+            <CodeSnippet
+              language="bash"
+              canCopy={true}
+              onCopyText={<span className="u-textColor--success">Copied!</span>}
+            >
+              {`curl http://node.something/join?token=abc&labels=${selectedNodeTypes.join(
+                ","
+              )}`}
+            </CodeSnippet>
+          </div>
+          <div className="tw-flex tw-items-center tw-gap-1.5">
+            <input
+              id="useStaticToken"
+              type="checkbox"
+              checked={useStaticToken}
+              onChange={(e) => setUseStaticToken(e.target.checked)}
+            />
+            <label
+              htmlFor="useStaticToken"
+              className="tw-text-base tw-text-gray-700"
+            >
+              Use a static token (useful for ASGs and scripts)
+            </label>
+          </div>
+          {/* buttons */}
+          <div className="tw-w-full tw-flex tw-justify-end tw-gap-2">
+            <button
+              className="btn secondary large"
+              onClick={() => setState({ displayAddNode: false })}
+            >
+              Close
+            </button>
+            <button
+              className="btn primary large"
+              disabled={selectedNodeTypes.length === 0}
+              onClick={() => setState({ displayAddNode: false })}
+            >
+              Add node
+            </button>
+          </div>
+        </div>
+      </Modal>
       {state.deleteNodeError && (
         <ErrorModal
           errorModal={true}
diff --git a/web/src/components/apps/HelmVMNodeRow.jsx b/web/src/components/apps/HelmVMNodeRow.jsx
index e7dac9025d..1b084af8ff 100644
--- a/web/src/components/apps/HelmVMNodeRow.jsx
+++ b/web/src/components/apps/HelmVMNodeRow.jsx
@@ -67,7 +67,7 @@ export default function HelmVMNodeRow({
         <div className="flex flex-auto alignItems--center u-fontWeight--bold u-textColor--primary">
           <Link
             to={`/cluster/${node?.name}`}
-            className="u-fontSize--normal u-fontWeight--bold u-textColor--primary"
+            className="u-fontSize--normal u-fontWeight--bold tw-color-blue-300 hover:tw-underline"
           >
             {node?.name}
           </Link>