From 6dde1db74043a0f7ef9548ed4bc781056b3e74a7 Mon Sep 17 00:00:00 2001 From: Emilien Macchi Date: Fri, 21 Mar 2025 20:28:50 -0400 Subject: [PATCH] Refactor --- src/App.jsx | 975 ++++++++++++++++++++++++++-------------------------- 1 file changed, 493 insertions(+), 482 deletions(-) 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 && ( -
- {tooltip} +
+
+ + updatePort(index, "networkId", e.target.value)} + className="w-full p-2 border rounded" + required + />
- )} +
+ + updatePort(index, "vnicType", e.target.value)} + className="w-full p-2 border rounded" + /> +
+
+ + updatePort(index, "addressPairs", e.target.value)} + className="w-full p-2 border rounded" + placeholder="ip_address=mac_address,ip_address=mac_address" + /> +
+
+ +
+
); }; +// 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} - -
-
-
- - { - try { - const ports = JSON.parse(form.additionalPorts); - ports[index].networkId = e.target.value; - setForm({ - ...form, - additionalPorts: JSON.stringify(ports) - }); - } catch (e) { - console.error("Error updating port networkId:", e); - } - }} - className="w-full p-2 border rounded" - required - /> -
-
- - { - try { - const ports = JSON.parse(form.additionalPorts); - ports[index].vnicType = e.target.value; - setForm({ - ...form, - additionalPorts: JSON.stringify(ports) - }); - } catch (e) { - console.error("Error updating port vnicType:", e); - } - }} - className="w-full p-2 border rounded" - /> -
-
- - { - try { - const ports = JSON.parse(form.additionalPorts); - ports[index].addressPairs = e.target.value; - setForm({ - ...form, - additionalPorts: JSON.stringify(ports) - }); - } catch (e) { - console.error("Error updating port addressPairs:", e); - } - }} - className="w-full p-2 border rounded" - placeholder="ip_address=mac_address,ip_address=mac_address" - /> -
-
- -
-
-
- ))} - -
- - ); - 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]}