diff --git a/src/App.jsx b/src/App.jsx
index 5cf46e1..ffcea5d 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1,5 +1,30 @@
import './App.css'
-import React, { useState, useEffect } from "react";
+import React, { useState, useEffect, useCallback, useMemo } from "react";
+
+// Tooltip Icon component to reduce duplication
+const TooltipIcon = () => (
+
+);
+
+// Tooltip wrapper component
+const Tooltip = ({ children, content, show, setShow }) => (
+
+
setShow(true)}
+ onMouseLeave={() => setShow(false)}
+ >
+ {children}
+
+ {show && (
+
+ {content}
+
+ )}
+
+);
// Reusable input with tooltip component
const InputWithTooltip = ({ type, name, placeholder, value, onChange, required, tooltip, className = "" }) => {
@@ -17,21 +42,10 @@ const InputWithTooltip = ({ type, name, placeholder, value, onChange, required,
className={`w-full p-2 border rounded ${className}`}
required={required}
/>
- setShowTooltip(true)}
- onMouseLeave={() => setShowTooltip(false)}
- >
-
-
+
+
+
- {showTooltip && (
-
- {tooltip}
-
- )}
);
};
@@ -57,36 +71,107 @@ const SelectWithTooltip = ({ name, value, onChange, options, required, tooltip }
))}
- setShowTooltip(true)}
- onMouseLeave={() => setShowTooltip(false)}
+
+
+
+
+
+ );
+};
+
+// Port configuration component
+const PortConfig = ({ port, index, updatePort, removePort }) => {
+ return (
+
+
+
Port {index + 1}
+
+ Remove
+
- {showTooltip && (
-
);
};
+// Progress bar component
+const ProgressBar = ({ steps, currentStep }) => (
+
+
+ {steps.map((stepName, idx) => (
+
+ {stepName}
+
+ ))}
+
+
+
+);
+
export default function HcpCliAssistant() {
// Platform config
- const platforms = [
+ const platforms = useMemo(() => [
{ value: "openstack", label: "OpenStack" },
// Future platforms can be added here
// { value: "aws", label: "AWS" },
// { value: "azure", label: "Azure" },
- ];
+ ], []);
// Step configurations for each platform
- const platformSteps = {
+ const platformSteps = useMemo(() => ({
openstack: [
"OpenStack Authentication",
"OpenStack Networking",
@@ -94,7 +179,7 @@ export default function HcpCliAssistant() {
"Review & Generate Command"
],
// Add more platform steps as they become available
- };
+ }), []);
// Common initial steps
const [step, setStep] = useState(0);
@@ -119,22 +204,11 @@ export default function HcpCliAssistant() {
nodeAZ: "",
nodeImageName: "",
dnsNameservers: "",
- additionalPorts: JSON.stringify([]),
+ additionalPorts: "[]", // Initialize as string to avoid JSON parsing issues
});
- // Add this to your component
- useEffect(() => {
- // If we're on the platform selection step and there's only one platform available, select it automatically
- if (step === 1 && platforms.length === 1 && !form.platform) {
- setForm(prev => ({
- ...prev,
- platform: platforms[0].value
- }));
- }
- }, [step, platforms.length, form.platform]);
-
- // Get steps based on selected platform
- const getSteps = () => {
+ // Get steps based on selected platform - memoize to prevent recalculation
+ const steps = useMemo(() => {
const commonSteps = ["Cluster Details", "Platform Selection"];
if (!form.platform) return commonSteps;
@@ -145,62 +219,125 @@ export default function HcpCliAssistant() {
// Fallback if platform not found
return [...commonSteps, "Platform Not Supported"];
- };
-
- const steps = getSteps();
+ }, [form.platform, platformSteps]);
- const handleChange = (e) => {
+ // Auto-select platform if only one available
+ useEffect(() => {
+ if (step === 1 && platforms.length === 1 && !form.platform) {
+ setForm(prev => ({
+ ...prev,
+ platform: platforms[0].value
+ }));
+ }
+ }, [step, platforms, form.platform]);
+
+ const handleChange = useCallback((e) => {
const { name, value, type, checked } = e.target;
- setForm({
- ...form,
+ setForm(prev => ({
+ ...prev,
[name]: type === "checkbox" ? checked : value,
- });
- };
+ }));
+ }, []);
- const isStepValid = () => {
- switch (step) {
- case 0: // Cluster Details
- return form.name.trim() !== "" &&
- form.baseDomain.trim() !== "" &&
- form.nodePoolReplicas.trim() !== "" &&
- form.pullSecret.trim() !== "" &&
- form.sshKey.trim() !== "";
- case 1: // Platform Selection
- return form.platform && form.platform.trim() !== "" && platformSteps[form.platform] !== undefined;
- default:
- // Platform specific validations - only if the platform is supported
- if (form.platform === "openstack") {
- const platformStep = step - 2; // Adjust for common steps
-
- switch (platformStep) {
- case 0: // OpenStack Authentication
- return form.osCloudSet || form.openstackCredentialsFile.trim() !== "";
- case 1: // OpenStack Networking
- if (form.additionalPorts) {
- try {
- const ports = JSON.parse(form.additionalPorts);
- if (ports.length === 0) {
- return true;
- }
- // Every port must have a networkId
- return ports.every(port => port.networkId && port.networkId.trim() !== "");
- } catch (e) {
- console.error("Error parsing additionalPorts:", e);
- return false;
- }
- }
- return true;
- case 2: // OpenStack Node Configuration
- return form.nodeFlavor.trim() !== "";
- default:
- return true;
+ // Safely parse ports from JSON string
+ const parsePorts = useCallback(() => {
+ try {
+ return JSON.parse(form.additionalPorts);
+ } catch (e) {
+ console.error("Error parsing additional ports:", e);
+ return [];
+ }
+ }, [form.additionalPorts]);
+
+ // Handle additional ports updates
+ const updatePort = useCallback((index, field, value) => {
+ try {
+ const ports = parsePorts();
+ ports[index] = { ...ports[index], [field]: value };
+ setForm(prev => ({
+ ...prev,
+ additionalPorts: JSON.stringify(ports)
+ }));
+ } catch (e) {
+ console.error(`Error updating port ${field}:`, e);
+ }
+ }, [parsePorts]);
+
+ const removePort = useCallback((index) => {
+ try {
+ const ports = parsePorts();
+ ports.splice(index, 1);
+ setForm(prev => ({
+ ...prev,
+ additionalPorts: JSON.stringify(ports)
+ }));
+ } catch (e) {
+ console.error("Error removing port:", e);
+ }
+ }, [parsePorts]);
+
+ const addPort = useCallback(() => {
+ try {
+ const ports = parsePorts();
+ ports.push({ networkId: "" });
+ setForm(prev => ({
+ ...prev,
+ additionalPorts: JSON.stringify(ports)
+ }));
+ } catch (e) {
+ console.error("Error adding new port:", e);
+ // Initialize with empty array if parsing fails
+ setForm(prev => ({
+ ...prev,
+ additionalPorts: JSON.stringify([{ networkId: "" }])
+ }));
+ }
+ }, [parsePorts]);
+
+ // Step validation logic
+ const isStepValid = useCallback(() => {
+ if (step === 0) { // Cluster Details
+ return Boolean(
+ form.name.trim() &&
+ form.baseDomain.trim() &&
+ form.nodePoolReplicas.trim() &&
+ form.pullSecret.trim() &&
+ form.sshKey.trim()
+ );
+ }
+
+ if (step === 1) { // Platform Selection
+ return form.platform && platformSteps[form.platform] !== undefined;
+ }
+
+ // Platform specific validations
+ if (form.platform === "openstack") {
+ const platformStep = step - 2; // Adjust for common steps
+
+ switch (platformStep) {
+ case 0: // OpenStack Authentication
+ return form.osCloudSet || form.openstackCredentialsFile.trim() !== "";
+ case 1: // OpenStack Networking
+ try {
+ const ports = parsePorts();
+ if (ports.length === 0) return true;
+ // Every port must have a networkId
+ return ports.every(port => port.networkId && port.networkId.trim() !== "");
+ } catch (e) {
+ return false;
}
- }
- return false; // If the platform is not handled, validation fails
+ case 2: // OpenStack Node Configuration
+ return form.nodeFlavor.trim() !== "";
+ default:
+ return true;
+ }
}
- };
+
+ return false;
+ }, [step, form, platformSteps, parsePorts]);
- const generateCommand = () => {
+ // Command generation logic - memoize to prevent recalculation
+ const generateCommand = useCallback(() => {
if (form.platform === "openstack") {
let cmd = `hcp create cluster openstack \
--name ${form.name} \
@@ -247,32 +384,30 @@ export default function HcpCliAssistant() {
--openstack-node-image-name ${form.nodeImageName}`;
}
- if (form.additionalPorts) {
- try {
- const ports = JSON.parse(form.additionalPorts);
- ports.forEach((port) => {
- if (port.networkId && port.networkId.trim() !== "") {
- let portConfig = `--openstack-node-additional-port=network-id:${port.networkId}`;
-
- if (port.vnicType) {
- portConfig += `,vnic-type:${port.vnicType}`;
- }
-
- if (port.addressPairs) {
- portConfig += `,address-pairs:${port.addressPairs}`;
- }
-
- if (port.disablePortSecurity) {
- portConfig += `,disable-port-security:true`;
- }
-
- cmd += ` \
- ${portConfig}`;
+ try {
+ const ports = parsePorts();
+ ports.forEach((port) => {
+ if (port.networkId && port.networkId.trim() !== "") {
+ let portConfig = `--openstack-node-additional-port=network-id:${port.networkId}`;
+
+ if (port.vnicType) {
+ portConfig += `,vnic-type:${port.vnicType}`;
+ }
+
+ if (port.addressPairs) {
+ portConfig += `,address-pairs:${port.addressPairs}`;
}
- });
- } catch (e) {
- console.error("Error parsing additionalPorts during command generation:", e);
- }
+
+ if (port.disablePortSecurity) {
+ portConfig += `,disable-port-security:true`;
+ }
+
+ cmd += ` \
+ ${portConfig}`;
+ }
+ });
+ } catch (e) {
+ console.error("Error parsing additionalPorts during command generation:", e);
}
cmd = cmd.replace(/\s+/g, ' ').trim();
@@ -280,371 +415,262 @@ export default function HcpCliAssistant() {
}
return "Platform command generation not implemented";
- };
+ }, [form, parsePorts]);
- const handleCopy = () => {
+ const handleCopy = useCallback(() => {
navigator.clipboard.writeText(generateCommand());
setCopied(true);
setTimeout(() => setCopied(false), 2000);
- };
+ }, [generateCommand]);
- const nextStep = () => {
+ const nextStep = useCallback(() => {
if (isStepValid()) {
setStep((prev) => Math.min(prev + 1, steps.length - 1));
}
- };
+ }, [isStepValid, steps.length]);
- const prevStep = () => setStep((prev) => Math.max(prev - 1, 0));
+ const prevStep = useCallback(() => setStep((prev) => Math.max(prev - 1, 0)), []);
+
+ // Extract form step rendering to separate components
+ const renderClusterDetailsStep = () => (
+ <>
+
+
+
+
+
+ >
+ );
+
+ const renderPlatformSelectionStep = () => (
+ <>
+ Select the platform where you want to deploy your HyperShift cluster:
+
+
+ {form.platform && (
+
+
+ Selected platform: {platforms.find(p => p.value === form.platform)?.label || form.platform}
+
+
+ )}
+
+ {platforms.length === 1 && (
+
+
+ Note: Currently only OpenStack is supported. More platforms will be added in future updates.
+
+
+ )}
+ >
+ );
+
+ const renderOpenstackAuthStep = () => (
+ <>
+
+
+ {!form.osCloudSet && (
+
+ )}
+
+
+
+
+ >
+ );
+
+ const renderOpenstackNetworkingStep = () => {
+ // Use the parsePorts function to safely get the ports array
+ const ports = parsePorts();
+
+ return (
+ <>
+
+
+
+
+
+
+ {/* Additional ports section */}
+
+
+ Additional Nodepool Ports (optional)
+
+
+
+
+ Attach additional ports to nodes. Params: Neutron Network ID, VNIC type, Port Security and Allowed Address Pairs.
+
+
+
+
+
+ {ports.map((port, index) => (
+
+ ))}
+
+
+
+ >
+ );
+ };
+
+ const renderOpenstackNodeConfigStep = () => (
+ <>
+
+
+
+
+
+ >
+ );
+
+ const renderCommandReviewStep = () => (
+
+
Generated Command:
+
{generateCommand()}
+
+
+ );
// Render current step content
const renderStepContent = () => {
if (step === 0) {
- // Cluster Details (Common for all platforms)
- return (
- <>
-
-
-
-
-
- >
- );
+ return renderClusterDetailsStep();
} else if (step === 1) {
- // Platform Selection
- return (
- <>
- Select the platform where you want to deploy your HyperShift cluster:
-
-
- {form.platform && (
-
-
- Selected platform: {platforms.find(p => p.value === form.platform)?.label || form.platform}
-
-
- )}
-
- {platforms.length === 1 && (
-
-
- Note: Currently only OpenStack is supported. More platforms will be added in future updates.
-
-
- )}
- >
- );
+ return renderPlatformSelectionStep();
} else if (form.platform === "openstack") {
- // OpenStack specific steps
const platformStep = step - 2; // Adjust for common steps
switch (platformStep) {
- case 0: // OpenStack Authentication
- return (
- <>
-
-
- {!form.osCloudSet && (
-
- )}
-
-
-
-
- >
- );
- case 1: // OpenStack Networking
- return (
- <>
-
-
-
-
-
-
- {/* Additional ports section */}
-
-
- Additional Nodepool Ports (optional)
- e.target.querySelector('div').style.display = 'block'}
- onMouseLeave={(e) => e.target.querySelector('div').style.display = 'none'}
- >
-
-
- Attach additional ports to nodes. Params: Neutron Network ID, VNIC type, Port Security and Allowed Address Pairs.
-
-
-
-
- {form.additionalPorts && JSON.parse(form.additionalPorts).map((port, index) => (
-
-
- Port {index + 1}
-
-
-
-
- ))}
-
-
- >
- );
- case 2: // OpenStack Node Configuration
- return (
- <>
-
-
-
-
-
- >
- );
- case 3: // Review & Generate Command (final step for OpenStack)
- return (
-
-
Generated Command:
-
{generateCommand()}
-
-
- );
+ case 0: return renderOpenstackAuthStep();
+ case 1: return renderOpenstackNetworkingStep();
+ case 2: return renderOpenstackNodeConfigStep();
+ case 3: return renderCommandReviewStep();
default:
return (
@@ -672,22 +698,7 @@ export default function HcpCliAssistant() {
Hypershift CLI Assistant
{/* Progress bar */}
-
-
- {steps.map((stepName, idx) => (
-
- {stepName}
-
- ))}
-
-
-
+
Step {step + 1}: {steps[step]}