Skip to content

Commit fcb8fda

Browse files
authored
Display node join command during initial install (#4815)
1 parent 982cb68 commit fcb8fda

File tree

4 files changed

+246
-130
lines changed

4 files changed

+246
-130
lines changed

web/src/components/apps/EmbeddedClusterManagement.tsx

Lines changed: 148 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
import { useQuery } from "@tanstack/react-query";
22
import classNames from "classnames";
33
import MaterialReactTable, { MRT_ColumnDef } from "material-react-table";
4-
import { ChangeEvent, useEffect, useMemo, useReducer, useState } from "react";
5-
import Modal from "react-modal";
4+
import { useEffect, useMemo, useReducer, useState } from "react";
65
import { Link, useNavigate, useParams } from "react-router-dom";
76

87
import { KotsPageTitle } from "@components/Head";
98
import { useApps } from "@features/App";
109
import { rbacRoles } from "../../constants/rbac";
1110
import { Utilities } from "../../utilities/utilities";
12-
import Icon from "../Icon";
1311
import CodeSnippet from "../shared/CodeSnippet";
1412

1513
import "@src/scss/components/apps/EmbeddedClusterManagement.scss";
1614
import { isEqual } from "lodash";
15+
import AddANodeModal from "@components/modals/AddANodeModal";
16+
import Icon from "@components/Icon";
1717

1818
const testData = {
1919
nodes: undefined,
@@ -241,24 +241,23 @@ const EmbeddedClusterManagement = ({
241241
if (nodeTypes.length === 1) {
242242
// if there's only one node type, select it by default
243243
setSelectedNodeTypes(nodeTypes);
244-
} else {
245-
setSelectedNodeTypes([]);
244+
} else if (nodeTypes.length > 1) {
245+
setSelectedNodeTypes([nodeTypes[0]]);
246246
}
247247
}, [rolesData]);
248248

249249
const determineDisabledState = () => {
250250
return false;
251251
};
252252

253-
const handleSelectNodeType = (e: ChangeEvent<HTMLInputElement>) => {
254-
let nodeType = e.currentTarget.value;
255-
let types = selectedNodeTypes;
256-
257-
if (selectedNodeTypes.includes(nodeType)) {
258-
setSelectedNodeTypes(types.filter((type) => type !== nodeType));
259-
} else {
260-
setSelectedNodeTypes([...types, nodeType]);
261-
}
253+
const handleSelectNodeType = (nodeType) => {
254+
setSelectedNodeTypes((prevSelectedNodeTypes) => {
255+
if (prevSelectedNodeTypes.includes(nodeType)) {
256+
return prevSelectedNodeTypes.filter((type) => type !== nodeType);
257+
} else {
258+
return [...prevSelectedNodeTypes, nodeType];
259+
}
260+
});
262261
};
263262
// #endregion
264263

@@ -413,6 +412,111 @@ const EmbeddedClusterManagement = ({
413412
}
414413
};
415414

415+
const AddNodeInstructions = () => {
416+
return (
417+
<div className="tw-mb-2 tw-text-base">
418+
<p>
419+
Optionally add nodes to the cluster. Click{" "}
420+
<span className="tw-font-semibold">Continue </span>
421+
to proceed with a single node.
422+
</p>
423+
<p>
424+
{rolesData?.roles &&
425+
rolesData.roles.length > 1 &&
426+
"Select one or more roles to assign to the new node."}{" "}
427+
Copy the join command and run it on the machine you'd like to join to
428+
the cluster.
429+
</p>
430+
</div>
431+
);
432+
};
433+
434+
const AddNodeCommands = () => {
435+
return (
436+
<>
437+
{rolesLoading && (
438+
<p className="tw-text-base tw-w-full tw-text-center tw-py-4 tw-text-gray-500 tw-font-semibold">
439+
Loading roles...
440+
</p>
441+
)}
442+
{!rolesData && rolesError && (
443+
<p className="tw-text-base tw-w-full tw-text-center tw-py-4 tw-text-pink-500 tw-font-semibold">
444+
{rolesError?.message || "Unable to fetch roles"}
445+
</p>
446+
)}
447+
448+
{rolesData?.roles && rolesData.roles.length > 1 && (
449+
<div className="tw-flex tw-gap-2 tw-items-center tw-mt-2">
450+
<p className="tw-text-gray-600 tw-font-semibold">Roles: </p>
451+
{rolesData.roles.map((nodeType) => (
452+
<div
453+
key={nodeType}
454+
className={classNames(
455+
"tw-border-[1px] tw-border-solid tw-border-[#326DE6] tw-rounded tw-px-2 tw-py-2 tw-flex tw-items-center tw-cursor-pointer",
456+
{
457+
"tw-text-white tw-bg-[#326DE6]":
458+
selectedNodeTypes.includes(nodeType),
459+
"is-disabled": determineDisabledState(),
460+
"tw-text-[#326DE6] tw-bg-white tw-hover:tw-bg-[#f8fafe]":
461+
!selectedNodeTypes.includes(nodeType),
462+
}
463+
)}
464+
onClick={() => {
465+
handleSelectNodeType(nodeType);
466+
}}
467+
>
468+
<label
469+
htmlFor={`${nodeType}NodeType`}
470+
className=" u-userSelect--none tw-text-gray-600 u-fontSize--normal u-fontWeight--medium tw-text-center tw-flex tw-items-center"
471+
>
472+
{selectedNodeTypes.includes(nodeType) && (
473+
<Icon icon="check" size={12} className="tw-mr-2" />
474+
)}
475+
<input
476+
id={`${nodeType}NodeType`}
477+
className="u-cursor--pointer tw-mr-2 hidden-input"
478+
type="checkbox"
479+
name={`${nodeType}NodeType`}
480+
value={nodeType}
481+
disabled={determineDisabledState()}
482+
checked={selectedNodeTypes.includes(nodeType)}
483+
/>
484+
</label>
485+
{nodeType}
486+
</div>
487+
))}
488+
</div>
489+
)}
490+
<div className="tw-max-w-[700px]">
491+
{selectedNodeTypes.length > 0 && generateAddNodeCommandLoading && (
492+
<p className="tw-text-base tw-w-full tw-text-center tw-py-4 tw-text-gray-500 tw-font-semibold">
493+
Generating command...
494+
</p>
495+
)}
496+
{!generateAddNodeCommand && generateAddNodeCommandError && (
497+
<p className="tw-text-base tw-w-full tw-text-center tw-py-4 tw-text-pink-500 tw-font-semibold">
498+
{generateAddNodeCommandError?.message}
499+
</p>
500+
)}
501+
{!generateAddNodeCommandLoading && generateAddNodeCommand?.command && (
502+
<>
503+
<CodeSnippet
504+
key={selectedNodeTypes.toString()}
505+
language="bash"
506+
canCopy={true}
507+
onCopyText={
508+
<span className="u-textColor--success">Copied!</span>
509+
}
510+
>
511+
{generateAddNodeCommand?.command}
512+
</CodeSnippet>
513+
</>
514+
)}
515+
</div>
516+
</>
517+
);
518+
};
519+
416520
return (
417521
<div className="EmbeddedClusterManagement--wrapper container u-overflow--auto u-paddingTop--50 tw-font-sans">
418522
<KotsPageTitle pageName="Cluster Management" />
@@ -421,15 +525,32 @@ const EmbeddedClusterManagement = ({
421525
Nodes
422526
</p>
423527
<div className="tw-flex tw-gap-6 tw-items-center">
424-
{Utilities.sessionRolesHasOneOf([rbacRoles.CLUSTER_ADMIN]) && (
425-
<button
426-
className="btn primary tw-ml-auto tw-w-fit tw-h-fit"
427-
onClick={onAddNodeClick}
428-
>
429-
Add node
430-
</button>
528+
{" "}
529+
{!Utilities.isInitialAppInstall(app) && (
530+
<div className="tw-flex tw-gap-6">
531+
<p>
532+
View the nodes in your cluster, generate commands to add nodes
533+
to the cluster, and view workloads running on each node.
534+
</p>
535+
</div>
431536
)}
537+
{Utilities.sessionRolesHasOneOf([rbacRoles.CLUSTER_ADMIN]) &&
538+
!Utilities.isInitialAppInstall(app) && (
539+
<button
540+
className="btn primary tw-ml-auto tw-w-fit tw-h-fit"
541+
onClick={onAddNodeClick}
542+
>
543+
Add node
544+
</button>
545+
)}
432546
</div>
547+
{Utilities.isInitialAppInstall(app) && (
548+
<div className="tw-mt-4 tw-flex tw-flex-col">
549+
<AddNodeInstructions />
550+
<AddNodeCommands />
551+
</div>
552+
)}
553+
433554
<div className="flex1 u-overflow--auto card-item">
434555
{nodesLoading && (
435556
<p className="tw-text-base tw-w-full tw-text-center tw-py-4 tw-text-gray-500 tw-font-semibold">
@@ -498,109 +619,13 @@ const EmbeddedClusterManagement = ({
498619
)}
499620
</div>
500621
{/* MODALS */}
501-
<Modal
502-
isOpen={state.displayAddNode}
503-
onRequestClose={() => setState({ displayAddNode: false })}
504-
contentLabel="Add Node"
505-
className="Modal"
506-
ariaHideApp={false}
622+
<AddANodeModal
623+
displayAddNode={state.displayAddNode}
624+
toggleDisplayAddNode={() => setState({ displayAddNode: false })}
625+
rolesData={rolesData}
507626
>
508-
<div className="Modal-body tw-flex tw-flex-col tw-gap-4 tw-font-sans">
509-
<div className="tw-flex">
510-
<h1 className="u-fontSize--largest u-fontWeight--bold u-textColor--primary u-lineHeight--normal u-marginBottom--more">
511-
Add a Node
512-
</h1>
513-
<Icon
514-
icon="close"
515-
size={14}
516-
className="tw-ml-auto gray-color clickable close-icon"
517-
onClick={() => setState({ displayAddNode: false })}
518-
/>
519-
</div>
520-
<p className="tw-text-base tw-text-gray-600">
521-
{rolesData?.roles &&
522-
rolesData.roles.length > 1 &&
523-
"Select one or more roles to assign to the new node. "}
524-
Copy the join command and run it on the machine you'd like to join
525-
to the cluster.
526-
</p>
527-
{rolesLoading && (
528-
<p className="tw-text-base tw-w-full tw-text-center tw-py-4 tw-text-gray-500 tw-font-semibold">
529-
Loading roles...
530-
</p>
531-
)}
532-
{!rolesData && rolesError && (
533-
<p className="tw-text-base tw-w-full tw-text-center tw-py-4 tw-text-pink-500 tw-font-semibold">
534-
{rolesError?.message || "Unable to fetch roles"}
535-
</p>
536-
)}
537-
{rolesData?.roles && rolesData.roles.length > 1 && (
538-
<div className="tw-grid tw-gap-2 tw-grid-cols-4 tw-auto-rows-auto">
539-
{rolesData.roles.map((nodeType) => (
540-
<div
541-
key={nodeType}
542-
className={classNames("BoxedCheckbox", {
543-
"is-active": selectedNodeTypes.includes(nodeType),
544-
"is-disabled": determineDisabledState(),
545-
})}
546-
>
547-
<input
548-
id={`${nodeType}NodeType`}
549-
className="u-cursor--pointer hidden-input"
550-
type="checkbox"
551-
name={`${nodeType}NodeType`}
552-
value={nodeType}
553-
disabled={determineDisabledState()}
554-
checked={selectedNodeTypes.includes(nodeType)}
555-
onChange={handleSelectNodeType}
556-
/>
557-
<label
558-
htmlFor={`${nodeType}NodeType`}
559-
className="tw-block u-cursor--pointer u-userSelect--none u-textColor--primary u-fontSize--normal u-fontWeight--medium tw-text-center"
560-
>
561-
{nodeType}
562-
</label>
563-
</div>
564-
))}
565-
</div>
566-
)}
567-
<div>
568-
{selectedNodeTypes.length > 0 && generateAddNodeCommandLoading && (
569-
<p className="tw-text-base tw-w-full tw-text-center tw-py-4 tw-text-gray-500 tw-font-semibold">
570-
Generating command...
571-
</p>
572-
)}
573-
{!generateAddNodeCommand && generateAddNodeCommandError && (
574-
<p className="tw-text-base tw-w-full tw-text-center tw-py-4 tw-text-pink-500 tw-font-semibold">
575-
{generateAddNodeCommandError?.message}
576-
</p>
577-
)}
578-
{!generateAddNodeCommandLoading && generateAddNodeCommand?.command && (
579-
<>
580-
<CodeSnippet
581-
key={selectedNodeTypes.toString()}
582-
language="bash"
583-
canCopy={true}
584-
onCopyText={
585-
<span className="u-textColor--success">Copied!</span>
586-
}
587-
>
588-
{generateAddNodeCommand?.command}
589-
</CodeSnippet>
590-
</>
591-
)}
592-
</div>
593-
{/* buttons */}
594-
<div className="tw-w-full tw-flex tw-justify-end tw-gap-2">
595-
<button
596-
className="btn secondary large"
597-
onClick={() => setState({ displayAddNode: false })}
598-
>
599-
Close
600-
</button>
601-
</div>
602-
</div>
603-
</Modal>
627+
<AddNodeCommands />
628+
</AddANodeModal>
604629
</div>
605630
);
606631
};

web/src/components/apps/EmbeddedClusterViewNode.jsx

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -120,15 +120,14 @@ const EmbeddedClusterViewNode = () => {
120120
)}
121121
{!nodeLoading && node && (
122122
<>
123-
{/* Node Info */}
124-
<div className="tw-p-3">
125-
<p className="tw-font-semibold tw-text-xl tw-text-gray-800">
126-
{node?.name}
127-
</p>
128-
</div>
129123
{/* Pods table */}
130124
<div className="card-bg tw-p-3 tw-flex tw-flex-col tw-gap-2">
131-
<p className="tw-font-semibold tw-text-xl tw-text-gray-800">Pods</p>
125+
<div>
126+
<p className="tw-font-semibold tw-text-xl tw-text-gray-800">
127+
Workloads
128+
</p>
129+
<p>View the workloads running on this node.</p>
130+
</div>
132131
<div className="card-item">
133132
<MaterialReactTable
134133
columns={columns}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import Modal from "react-modal";
2+
import Icon from "../Icon";
3+
4+
const AddANodeModal = ({
5+
displayAddNode,
6+
toggleDisplayAddNode,
7+
rolesData,
8+
children,
9+
}) => {
10+
return (
11+
<Modal
12+
isOpen={displayAddNode}
13+
onRequestClose={() => toggleDisplayAddNode()}
14+
contentLabel="Add Node"
15+
className="Modal"
16+
ariaHideApp={false}
17+
>
18+
<div className="Modal-body tw-flex tw-flex-col tw-gap-4 tw-font-sans">
19+
<div className="tw-flex">
20+
<h1 className="u-fontSize--largest u-fontWeight--bold u-textColor--primary u-lineHeight--normal u-marginBottom--more">
21+
Add a Node
22+
</h1>
23+
<Icon
24+
icon="close"
25+
size={14}
26+
className="tw-ml-auto gray-color clickable close-icon"
27+
onClick={() => toggleDisplayAddNode()}
28+
/>
29+
</div>
30+
<p className="tw-text-base tw-text-gray-600">
31+
{rolesData?.roles &&
32+
rolesData.roles.length > 1 &&
33+
"Select one or more roles to assign to the new node. "}
34+
Copy the join command and run it on the machine you'd like to join to
35+
the cluster.
36+
</p>
37+
{children}
38+
<div className="tw-w-full tw-flex tw-justify-end tw-gap-2">
39+
<button
40+
className="btn secondary large"
41+
onClick={() => toggleDisplayAddNode()}
42+
>
43+
Close
44+
</button>
45+
</div>
46+
</div>
47+
</Modal>
48+
);
49+
};
50+
51+
export default AddANodeModal;

0 commit comments

Comments
 (0)