diff --git a/web/src/Root.tsx b/web/src/Root.tsx
index 2e7930a438..684fe64170 100644
--- a/web/src/Root.tsx
+++ b/web/src/Root.tsx
@@ -58,6 +58,7 @@ import SnapshotDetails from "@components/snapshots/SnapshotDetails";
import SnapshotRestore from "@components/snapshots/SnapshotRestore";
import AppSnapshots from "@components/snapshots/AppSnapshots";
import AppSnapshotRestore from "@components/snapshots/AppSnapshotRestore";
+import HelmVMViewNode from "@components/apps/HelmVMViewNode";
// react-query client
const queryClient = new QueryClient();
@@ -579,10 +580,11 @@ const Root = () => {
state.adminConsoleMetadata?.isKurl ? (
) : (
-
+
)
}
/>
+ } />
}
diff --git a/web/src/components/apps/HelmVMClusterManagement.jsx b/web/src/components/apps/HelmVMClusterManagement.jsx
index 4d1712f258..5d2ef854af 100644
--- a/web/src/components/apps/HelmVMClusterManagement.jsx
+++ b/web/src/components/apps/HelmVMClusterManagement.jsx
@@ -1,96 +1,167 @@
-import React, { Component, Fragment } from "react";
import classNames from "classnames";
import dayjs from "dayjs";
+import React, { useEffect, useReducer } from "react";
+import Modal from "react-modal";
+import { useQuery } from "react-query";
+
import { KotsPageTitle } from "@components/Head";
-import CodeSnippet from "../shared/CodeSnippet";
-import HelmVMNodeRow from "./HelmVMNodeRow";
-import Loader from "../shared/Loader";
import { rbacRoles } from "../../constants/rbac";
-import { Utilities } from "../../utilities/utilities";
import { Repeater } from "../../utilities/repeater";
+import { Utilities } from "../../utilities/utilities";
+import Icon from "../Icon";
import ErrorModal from "../modals/ErrorModal";
-import Modal from "react-modal";
+import CodeSnippet from "../shared/CodeSnippet";
+import Loader from "../shared/Loader";
+import HelmVMNodeRow from "./HelmVMNodeRow";
import "@src/scss/components/apps/HelmVMClusterManagement.scss";
-import Icon from "../Icon";
-export class HelmVMClusterManagement extends Component {
- state = {
- generating: false,
- command: "",
- expiry: null,
- displayAddNode: false,
- selectedNodeType: "primary",
- generateCommandErrMsg: "",
- helmvm: null,
- getNodeStatusJob: new Repeater(),
- deletNodeError: "",
- confirmDeleteNode: "",
- showConfirmDrainModal: false,
- nodeNameToDrain: "",
- drainingNodeName: null,
- drainNodeSuccessful: false,
- };
-
- componentDidMount() {
- this.getNodeStatus();
- this.state.getNodeStatusJob.start(this.getNodeStatus, 1000);
- }
+const testData = {
+ isHelmVMEnabled: true,
+ ha: false,
+ nodes: [
+ {
+ name: "test-helmvm-node",
+ isConnected: true,
+ isReady: true,
+ isPrimaryNode: true,
+ canDelete: false,
+ kubeletVersion: "v1.28.2",
+ cpu: {
+ capacity: 8,
+ available: 7.466876775,
+ },
+ memory: {
+ capacity: 31.33294677734375,
+ available: 24.23790740966797,
+ },
+ pods: {
+ capacity: 110,
+ available: 77,
+ },
+ labels: [
+ "beta.kubernetes.io/arch:amd64",
+ "beta.kubernetes.io/os:linux",
+ "node-role.kubernetes.io/master:",
+ "node.kubernetes.io/exclude-from-external-load-balancers:",
+ "kubernetes.io/arch:amd64",
+ "kubernetes.io/hostname:laverya-kurl",
+ "kubernetes.io/os:linux",
+ "node-role.kubernetes.io/control-plane:",
+ ],
+ conditions: {
+ memoryPressure: false,
+ diskPressure: false,
+ pidPressure: false,
+ ready: true,
+ },
+ },
+ {
+ name: "test-helmvm-worker",
+ isConnected: true,
+ isReady: true,
+ isPrimaryNode: false,
+ canDelete: false,
+ kubeletVersion: "v1.28.2",
+ cpu: {
+ capacity: 4,
+ available: 3.761070507,
+ },
+ memory: {
+ capacity: 15.50936508178711,
+ available: 11.742542266845703,
+ },
+ pods: {
+ capacity: 110,
+ available: 94,
+ },
+ labels: [
+ "beta.kubernetes.io/arch:amd64",
+ "beta.kubernetes.io/os:linux",
+ "kubernetes.io/arch:amd64",
+ "kubernetes.io/os:linux",
+ "kurl.sh/cluster:true",
+ ],
+ conditions: {
+ memoryPressure: false,
+ diskPressure: false,
+ pidPressure: false,
+ ready: true,
+ },
+ },
+ ],
+};
- componentWillUnmount() {
- this.state.getNodeStatusJob.stop();
- }
+const HelmVMClusterManagement = () => {
+ const [state, setState] = useReducer(
+ (state, newState) => ({ ...state, ...newState }),
+ {
+ generating: false,
+ command: "",
+ expiry: null,
+ displayAddNode: false,
+ selectedNodeType: "primary",
+ generateCommandErrMsg: "",
+ helmvm: null,
+ deletNodeError: "",
+ confirmDeleteNode: "",
+ showConfirmDrainModal: false,
+ nodeNameToDrain: "",
+ drainingNodeName: null,
+ drainNodeSuccessful: false,
+ }
+ );
- getNodeStatus = async () => {
- try {
- const res = await fetch(`${process.env.API_ENDPOINT}/helmvm/nodes`, {
- headers: {
- Accept: "application/json",
- },
- credentials: "include",
- method: "GET",
- });
- if (!res.ok) {
- if (res.status === 401) {
- Utilities.logoutUser();
- return;
- }
- console.log(
- "failed to get node status list, unexpected status code",
- res.status
- );
+ const { data: nodes, isLoading: nodesLoading } = useQuery({
+ queryKey: "helmVmNodes",
+ queryFn: async () => {
+ return (
+ await fetch(`${process.env.API_ENDPOINT}/helmvm/nodes`, {
+ headers: {
+ Accept: "application/json",
+ },
+ credentials: "include",
+ method: "GET",
+ })
+ ).json();
+ },
+ onError: (err) => {
+ if (err.status === 401) {
+ Utilities.logoutUser();
return;
}
- const response = await res.json();
- this.setState({
- helmvm: response,
+ console.log(
+ "failed to get node status list, unexpected status code",
+ err.status
+ );
+ },
+ onSuccess: (data) => {
+ setState({
// if cluster doesn't support ha, then primary will be disabled. Force into secondary
- selectedNodeType: !response.ha
- ? "secondary"
- : this.state.selectedNodeType,
+ selectedNodeType: !data.ha ? "secondary" : state.selectedNodeType,
});
- return response;
- } catch (err) {
- console.log(err);
- throw err;
- }
- };
+ },
+ config: {
+ refetchInterval: 1000,
+ retry: false,
+ },
+ });
- deleteNode = (name) => {
- this.setState({
+ const deleteNode = (name) => {
+ setState({
confirmDeleteNode: name,
});
};
- cancelDeleteNode = () => {
- this.setState({
+ const cancelDeleteNode = () => {
+ setState({
confirmDeleteNode: "",
});
};
- reallyDeleteNode = () => {
- const name = this.state.confirmDeleteNode;
- this.cancelDeleteNode();
+ const reallyDeleteNode = () => {
+ const name = state.confirmDeleteNode;
+ cancelDeleteNode();
fetch(`${process.env.API_ENDPOINT}/helmvm/nodes/${name}`, {
headers: {
@@ -106,7 +177,7 @@ export class HelmVMClusterManagement extends Component {
Utilities.logoutUser();
return;
}
- this.setState({
+ setState({
deleteNodeError: `Delete failed with status ${res.status}`,
});
}
@@ -116,8 +187,8 @@ export class HelmVMClusterManagement extends Component {
});
};
- generateWorkerAddNodeCommand = async () => {
- this.setState({
+ const generateWorkerAddNodeCommand = async () => {
+ setState({
generating: true,
command: "",
expiry: null,
@@ -137,13 +208,13 @@ export class HelmVMClusterManagement extends Component {
)
.then(async (res) => {
if (!res.ok) {
- this.setState({
+ setState({
generating: false,
generateCommandErrMsg: `Failed to generate command with status ${res.status}`,
});
} else {
const data = await res.json();
- this.setState({
+ setState({
generating: false,
command: data.command,
expiry: data.expiry,
@@ -152,22 +223,22 @@ export class HelmVMClusterManagement extends Component {
})
.catch((err) => {
console.log(err);
- this.setState({
+ setState({
generating: false,
generateCommandErrMsg: err ? err.message : "Something went wrong",
});
});
};
- onDrainNodeClick = (name) => {
- this.setState({
+ const onDrainNodeClick = (name) => {
+ setState({
showConfirmDrainModal: true,
nodeNameToDrain: name,
});
};
- drainNode = async (name) => {
- this.setState({ showConfirmDrainModal: false, drainingNodeName: name });
+ const drainNode = async (name) => {
+ setState({ showConfirmDrainModal: false, drainingNodeName: name });
fetch(`${process.env.API_ENDPOINT}/helmvm/nodes/${name}/drain`, {
headers: {
"Content-Type": "application/json",
@@ -177,9 +248,9 @@ export class HelmVMClusterManagement extends Component {
method: "POST",
})
.then(async (res) => {
- this.setState({ drainNodeSuccessful: true });
+ setState({ drainNodeSuccessful: true });
setTimeout(() => {
- this.setState({
+ setState({
drainingNodeName: null,
drainNodeSuccessful: false,
});
@@ -187,15 +258,15 @@ export class HelmVMClusterManagement extends Component {
})
.catch((err) => {
console.log(err);
- this.setState({
+ setState({
drainingNodeName: null,
drainNodeSuccessful: false,
});
});
};
- generatePrimaryAddNodeCommand = async () => {
- this.setState({
+ const generatePrimaryAddNodeCommand = async () => {
+ setState({
generating: true,
command: "",
expiry: null,
@@ -215,13 +286,13 @@ export class HelmVMClusterManagement extends Component {
)
.then(async (res) => {
if (!res.ok) {
- this.setState({
+ setState({
generating: false,
generateCommandErrMsg: `Failed to generate command with status ${res.status}`,
});
} else {
const data = await res.json();
- this.setState({
+ setState({
generating: false,
command: data.command,
expiry: data.expiry,
@@ -230,257 +301,299 @@ export class HelmVMClusterManagement extends Component {
})
.catch((err) => {
console.log(err);
- this.setState({
+ setState({
generating: false,
generateCommandErrMsg: err ? err.message : "Something went wrong",
});
});
};
- onAddNodeClick = () => {
- this.setState(
+ const onAddNodeClick = () => {
+ setState(
{
displayAddNode: true,
},
async () => {
- await this.generateWorkerAddNodeCommand();
+ await generateWorkerAddNodeCommand();
}
);
};
- onSelectNodeType = (event) => {
+ const onSelectNodeType = (event) => {
const value = event.currentTarget.value;
- this.setState(
+ setState(
{
selectedNodeType: value,
},
async () => {
- if (this.state.selectedNodeType === "secondary") {
- await this.generateWorkerAddNodeCommand();
+ if (state.selectedNodeType === "secondary") {
+ await generateWorkerAddNodeCommand();
} else {
- await this.generatePrimaryAddNodeCommand();
+ await generatePrimaryAddNodeCommand();
}
}
);
};
- ackDeleteNodeError = () => {
- this.setState({ deleteNodeError: "" });
+ const ackDeleteNodeError = () => {
+ setState({ deleteNodeError: "" });
};
- render() {
- const { helmvm } = this.state;
- const { displayAddNode, generateCommandErrMsg } = this.state;
-
- if (!helmvm) {
- return (
-
-
-
- );
- }
+ const { displayAddNode, generateCommandErrMsg } = state;
+ if (nodesLoading) {
return (
-
-
-
-
-
-
- Your nodes
-
-
- {helmvm?.nodes &&
- helmvm?.nodes.map((node, i) => (
-
- ))}
-
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+ Cluster Nodes
+
+
+ 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.
+
+
+ {(nodes?.nodes || testData?.nodes) &&
+ (nodes?.nodes || testData?.nodes).map((node, i) => (
+
+ ))}
- {helmvm?.isHelmVMEnabled &&
- Utilities.sessionRolesHasOneOf([rbacRoles.CLUSTER_ADMIN]) ? (
- !displayAddNode ? (
-
-
+
+ {(nodes?.isHelmVMEnabled || testData.isHelmVMEnabled) &&
+ Utilities.sessionRolesHasOneOf([rbacRoles.CLUSTER_ADMIN]) ? (
+ !displayAddNode ? (
+
+
+ Add a node
+
+
+ ) : (
+
+
- ) : (
-
-
-
-
+
+
+
-
-
-
-
-
-
-
- Primary Node
-
-
- Provides high availability
-
-
-
-
-
+
+
+
+
+ Primary Node
+
+
+ Provides high availability
+
+
+
+
+
+
+
-
-
-
-
-
-
-
- Secondary Node
-
-
- Optimal for running application workloads
-
-
-
-
+
+
+
+
+
+ Secondary Node
+
+
+ Optimal for running application workloads
+
+
+
- {this.state.generating && (
-
-
-
- )}
- {!this.state.generating && this.state.command?.length > 0 ? (
-
-
- Run this command on the node you wish to join the
- cluster
-
-
- Command has been copied to your clipboard
-
- }
- >
- {[this.state.command.join(" \\\n ")]}
-
- {this.state.expiry && (
-
- {`Expires on ${dayjs(this.state.expiry).format(
- "MMM Do YYYY, h:mm:ss a z"
- )} UTC${(-1 * new Date().getTimezoneOffset()) / 60}`}
-
- )}
-
- ) : (
-
- {generateCommandErrMsg && (
-
-
- {generateCommandErrMsg}
-
-
- )}
-
- )}
- )
- ) : null}
+ {state.generating && (
+
+
+
+ )}
+ {!state.generating && state.command?.length > 0 ? (
+ <>
+
+ Run this command on the node you wish to join the cluster
+
+
+ Command has been copied to your clipboard
+
+ }
+ >
+ {[state.command.join(" \\\n ")]}
+
+ {state.expiry && (
+
+ {`Expires on ${dayjs(state.expiry).format(
+ "MMM Do YYYY, h:mm:ss a z"
+ )} UTC${(-1 * new Date().getTimezoneOffset()) / 60}`}
+
+ )}
+ >
+ ) : (
+ <>
+ {generateCommandErrMsg && (
+
+
+ {generateCommandErrMsg}
+
+
+ )}
+ >
+ )}
+
+ )
+ ) : null}
+
+
+ {state.deleteNodeError && (
+
+ )}
+
+
+
+ Deleting this node may cause data loss. Are you sure you want to
+ proceed?
+
+
+
+ Delete {state.confirmDeleteNode}
+
+
+ Cancel
+
- {this.state.deleteNodeError && (
-
- )}
+
+ {state.showConfirmDrainModal && (
+ setState({
+ showConfirmDrainModal: false,
+ nodeNameToDrain: "",
+ })
+ }
shouldReturnFocusAfterClose={false}
- contentLabel="Confirm Delete Node"
+ contentLabel="Confirm Drain Node"
ariaHideApp={false}
- className="Modal"
+ className="Modal MediumSize"
>
+
+ Are you sure you want to drain {state.nodeNameToDrain}?
+
- Deleting this node may cause data loss. Are you sure you want to
- proceed?
+ Draining this node may cause data loss. If you want to delete{" "}
+ {state.nodeNameToDrain} you must disconnect it after it has been
+ drained.
drainNode(state.nodeNameToDrain)}
type="button"
className="btn red primary"
>
- Delete {this.state.confirmDeleteNode}
+ Drain {state.nodeNameToDrain}
+ setState({
+ showConfirmDrainModal: false,
+ nodeNameToDrain: "",
+ })
+ }
type="button"
className="btn secondary u-marginLeft--20"
>
@@ -489,56 +602,9 @@ export class HelmVMClusterManagement extends Component {
- {this.state.showConfirmDrainModal && (
-
- this.setState({
- showConfirmDrainModal: false,
- nodeNameToDrain: "",
- })
- }
- shouldReturnFocusAfterClose={false}
- contentLabel="Confirm Drain Node"
- ariaHideApp={false}
- className="Modal MediumSize"
- >
-
-
- Are you sure you want to drain {this.state.nodeNameToDrain}?
-
-
- Draining this node may cause data loss. If you want to delete{" "}
- {this.state.nodeNameToDrain} you must disconnect it after it has
- been drained.
-
-
- this.drainNode(this.state.nodeNameToDrain)}
- type="button"
- className="btn red primary"
- >
- Drain {this.state.nodeNameToDrain}
-
-
- this.setState({
- showConfirmDrainModal: false,
- nodeNameToDrain: "",
- })
- }
- type="button"
- className="btn secondary u-marginLeft--20"
- >
- Cancel
-
-
-
-
- )}
-
- );
- }
-}
+ )}
+
+ );
+};
export default HelmVMClusterManagement;
diff --git a/web/src/components/apps/HelmVMNodeRow.jsx b/web/src/components/apps/HelmVMNodeRow.jsx
index 93f2c5489c..e7dac9025d 100644
--- a/web/src/components/apps/HelmVMNodeRow.jsx
+++ b/web/src/components/apps/HelmVMNodeRow.jsx
@@ -1,15 +1,20 @@
-import React from "react";
import classNames from "classnames";
-import Loader from "../shared/Loader";
+import React from "react";
+import { Link } from "react-router-dom";
+
import { rbacRoles } from "../../constants/rbac";
import { getPercentageStatus, Utilities } from "../../utilities/utilities";
import Icon from "../Icon";
+import Loader from "../shared/Loader";
-export default function HelmVMNodeRow(props) {
- const { node } = props;
-
+export default function HelmVMNodeRow({
+ node,
+ drainNode,
+ drainNodeSuccessful,
+ drainingNodeName,
+ deleteNode,
+}) {
const DrainDeleteNode = () => {
- const { drainNode, drainNodeSuccessful, drainingNodeName } = props;
if (drainNode && Utilities.sessionRolesHasOneOf(rbacRoles.DRAIN_NODE)) {
if (
!drainNodeSuccessful &&
@@ -44,9 +49,7 @@ export default function HelmVMNodeRow(props) {
- node?.canDelete
- ? props.deleteNode(node?.name)
- : props.drainNode(node?.name)
+ node?.canDelete ? deleteNode(node?.name) : drainNode(node?.name)
}
className="btn secondary red"
>
@@ -62,9 +65,12 @@ export default function HelmVMNodeRow(props) {
-
+
{node?.name}
-
+
{node?.isPrimaryNode && (
Primary node
diff --git a/web/src/components/apps/HelmVMViewNode.jsx b/web/src/components/apps/HelmVMViewNode.jsx
new file mode 100644
index 0000000000..f9e7106938
--- /dev/null
+++ b/web/src/components/apps/HelmVMViewNode.jsx
@@ -0,0 +1,158 @@
+import React, { useMemo } from "react";
+import { useQuery } from "react-query";
+import { Link } from "react-router-dom";
+
+const HelmVMViewNode = () => {
+ const { data: nodes } = useQuery({
+ queryKey: "helmVmNodes",
+ queryFn: async () => {
+ return (
+ await fetch(`${process.env.API_ENDPOINT}/helmvm/nodes`, {
+ headers: {
+ Accept: "application/json",
+ },
+ credentials: "include",
+ method: "GET",
+ })
+ ).json();
+ },
+ onError: (err) => {
+ if (err.status === 401) {
+ Utilities.logoutUser();
+ return;
+ }
+ console.log(
+ "failed to get node status list, unexpected status code",
+ err.status
+ );
+ },
+ onSuccess: (data) => {
+ setState({
+ // if cluster doesn't support ha, then primary will be disabled. Force into secondary
+ selectedNodeType: !data.ha ? "secondary" : state.selectedNodeType,
+ });
+ },
+ config: {
+ retry: false,
+ },
+ });
+
+ const node = nodes.nodes[0];
+
+ const columns = useMemo(
+ () => [
+ {
+ accessorKey: "name",
+ header: "Name",
+ enableHiding: false,
+ enableColumnDragging: false,
+ size: 150,
+ },
+ {
+ accessorKey: "isConnected",
+ header: "Connection",
+ size: 150,
+ },
+ {
+ accessorKey: "kubeletVersion",
+ header: "Kubelet Version",
+ size: 170,
+ },
+ {
+ accessorKey: "cpu",
+ header: "CPU",
+ size: 150,
+ },
+ {
+ accessorKey: "memory",
+ header: "Memory",
+ size: 150,
+ },
+ {
+ accessorKey: "pods",
+ header: "Pods",
+ size: 150,
+ },
+ {
+ accessorKey: "canDelete",
+ header: "Delete Node",
+ size: 150,
+ },
+ ],
+ []
+ );
+
+ return (
+
+ {/* Breadcrumbs */}
+
+
+ Cluster Nodes
+ {" "}
+ / {node?.name}
+
+ {/* Node Info */}
+
+ {/* Pods table */}
+
+
Pods
+
+
+
+ {columns.map((col) => {
+ return (
+
+
+ {col.header}
+
+
+ );
+ })}
+
+
+ Some pods here
+
+
+ {/* Troubleshooting */}
+
+ {/* Danger Zone */}
+
+
+ Danger Zone
+
+
+ Prepare node for delete
+
+
+
+ );
+};
+
+export default HelmVMViewNode;
diff --git a/web/tailwind.config.js b/web/tailwind.config.js
index 1ea5c980de..d89f3af2a5 100644
--- a/web/tailwind.config.js
+++ b/web/tailwind.config.js
@@ -13,17 +13,60 @@ module.exports = {
"teal-muted-dark": "#577981",
"teal-medium": "#097992",
gray: {
- 100: "#dfdfdf",
+ 100: "#dedede",
200: "#c4c8ca",
300: "#b3b3b3",
+ 410: "#9b9b9b",
400: "#959595",
500: "#717171",
600: "#585858",
700: "#4f4f4f",
- 800: "#323232"
+ 800: "#323232",
+ 900: "#2c2c2c",
+ },
+ blue: {
+ 50: "#ecf4fe",
+ 75: "#b3d2fc",
+ 200: "#65a4f8",
+ 300: "#4591f7",
+ 400: "#3066ad",
+ },
+ green: {
+ 50: "#e7f7f3",
+ 75: "#9cdfcf",
+ 100: "#73d2bb",
+ 200: "#37bf9e",
+ 300: "#0eb28a",
+ 400: "#0a7d61",
+ 500: "#096d54",
+ },
+ indigo: {
+ 100: "#f0f1ff",
+ 200: "#c2c7fd",
+ 300: "#a9b0fd",
+ 400: "#838efc",
+ 500: "#6a77fb",
+ 600: "#4a53b0",
+ 700: "#414999",
},
neutral: {
- 700: "#4A4A4A"
+ 700: "#4A4A4A",
+ },
+ teal: {
+ 300: "#4db9c0",
+ 400: "#38a3a8",
+ },
+ pink: {
+ 50: "#fff0f3",
+ 100: "#ffc1cf",
+ 200: "#fea7bc",
+ 300: "#fe819f",
+ 400: "#fe678b",
+ 500: "#b24861",
+ 600: "#9b3f55",
+ },
+ purple: {
+ 400: "#7242b0",
},
error: "#bc4752",
"error-xlight": "#fbedeb",
@@ -34,26 +77,26 @@ module.exports = {
"warning-bright": "#ec8f39",
"info-bright": "#76bbca",
"disabled-teal": "#76a6cf",
- "dark-neon-green": "#38cc97"
+ "dark-neon-green": "#38cc97",
},
extend: {
borderRadius: {
xs: "0.125rem",
sm: "0.187rem",
- variants: ["first", "last"]
+ variants: ["first", "last"],
},
fontFamily: {
- sans: ["Open Sans", ...defaultTheme.fontFamily.sans]
- }
- }
+ sans: ["Open Sans", ...defaultTheme.fontFamily.sans],
+ },
+ },
},
corePlugins: {
- preflight: false
+ preflight: false,
},
plugins: [
plugin(function ({ addVariant }) {
addVariant("is-enabled", "&:not([disabled])");
addVariant("is-disabled", "&[disabled]");
- })
- ]
+ }),
+ ],
};