diff --git a/src/components/Core/Select/UnifiedSelect.tsx b/src/components/Core/Select/UnifiedSelect.tsx index 4e3b2e1e..0d084811 100644 --- a/src/components/Core/Select/UnifiedSelect.tsx +++ b/src/components/Core/Select/UnifiedSelect.tsx @@ -221,6 +221,16 @@ export const UnifiedSelect = component$((props) => { return selectedOption ? selectedOption.label : ""; }); + const selectedSingleOption = useComputed$(() => { + const val = currentValue.value; + + if (!val || Array.isArray(val)) { + return undefined; + } + + return options.find((opt) => opt.value === val); + }); + // Check if mobile device and handle orientation changes useVisibleTask$(() => { const checkMobile = () => { @@ -510,10 +520,17 @@ export const UnifiedSelect = component$((props) => { // Helper function to render option content const renderOption = (option: SelectOption, isOptionSelected: boolean) => { - // We'll simplify to use only the default rendering for now - // Custom renderers can be implemented with proper Qwik patterns later return ( - {option.label} + + {option.icon && ( + + {option.icon} + + )} + + {option.label} + + ); }; @@ -744,8 +761,17 @@ export const UnifiedSelect = component$((props) => { : undefined } > - - {loading ? loadingText : displayValue.value || placeholder} + + {!loading && selectedSingleOption.value?.icon && ( + + {selectedSingleOption.value.icon} + + )} + + {loading ? loadingText : displayValue.value || placeholder} + {/* Loading indicator for button */} @@ -1137,7 +1163,10 @@ export const UnifiedSelect = component$((props) => { )} - {option.label} + {renderOption( + option, + isSelected(option.value), + )} ), ); diff --git a/src/components/Core/Select/UnifiedSelect.types.ts b/src/components/Core/Select/UnifiedSelect.types.ts index cea01758..f0ca5df2 100644 --- a/src/components/Core/Select/UnifiedSelect.types.ts +++ b/src/components/Core/Select/UnifiedSelect.types.ts @@ -2,7 +2,7 @@ * Type definitions for the unified Select component */ -import { type QRL } from "@builder.io/qwik"; +import { type JSXNode, type JSXOutput, type QRL } from "@builder.io/qwik"; /** * Option item for the Select component @@ -18,6 +18,11 @@ export interface SelectOption { */ label: string; + /** + * Optional icon rendered before the label in custom mode + */ + icon?: JSXNode | JSXOutput; + /** * Whether the option is disabled */ diff --git a/src/components/Core/common/AdvancedSummaryBanner.tsx b/src/components/Core/common/AdvancedSummaryBanner.tsx new file mode 100644 index 00000000..d6ec3f85 --- /dev/null +++ b/src/components/Core/common/AdvancedSummaryBanner.tsx @@ -0,0 +1,38 @@ +import { component$ } from "@builder.io/qwik"; + +export interface AdvancedSummaryBannerProps { + title: string; + description?: string; + class?: string; +} + +export const AdvancedSummaryBanner = component$( + ({ title, description, class: className }) => { + const classes = [ + "relative overflow-hidden rounded-xl bg-gradient-to-br from-primary-600 to-primary-800 px-6 py-5 text-white md:px-7 md:py-6", + className, + ] + .filter(Boolean) + .join(" "); + + return ( +
+
+

+ {title} +

+ {description && ( +

+ {description} +

+ )} +
+ +
+
+
+
+
+ ); + }, +); diff --git a/src/components/Core/common/SummaryItemCard.tsx b/src/components/Core/common/SummaryItemCard.tsx new file mode 100644 index 00000000..13d868be --- /dev/null +++ b/src/components/Core/common/SummaryItemCard.tsx @@ -0,0 +1,46 @@ +import { component$, Slot } from "@builder.io/qwik"; + +export interface SummaryItemCardProps { + statusColorClass: string; + class?: string; + contentClass?: string; + trailingClass?: string; +} + +export const SummaryItemCard = component$( + ({ statusColorClass, class: className, contentClass, trailingClass }) => { + const wrapperClasses = [ + "rounded-lg border bg-gradient-to-r from-white to-gray-50 p-4 dark:from-gray-800 dark:to-gray-900", + statusColorClass, + className, + ] + .filter(Boolean) + .join(" "); + + const detailsClasses = ["min-w-0", contentClass].filter(Boolean).join(" "); + const statusClasses = [ + "flex shrink-0 flex-col items-end gap-1.5 pt-0.5", + trailingClass, + ] + .filter(Boolean) + .join(" "); + + return ( +
+
+
+ + +
+ +
+
+ +
+ +
+
+
+ ); + }, +); diff --git a/src/components/Core/common/index.ts b/src/components/Core/common/index.ts index 2e4297c2..6cfc257c 100644 --- a/src/components/Core/common/index.ts +++ b/src/components/Core/common/index.ts @@ -9,6 +9,8 @@ export * from "./types"; export * from "./utils"; // Components +export * from "./AdvancedSummaryBanner"; +export * from "./SummaryItemCard"; export * from "./VisuallyHidden"; // ==================== diff --git a/src/components/Core/index.ts b/src/components/Core/index.ts index fec0a1c0..4f9acc20 100644 --- a/src/components/Core/index.ts +++ b/src/components/Core/index.ts @@ -20,6 +20,9 @@ export { CoreUtils }; import { VisuallyHidden } from "./common/VisuallyHidden"; export { VisuallyHidden }; +import { AdvancedSummaryBanner } from "./common/AdvancedSummaryBanner"; +import { SummaryItemCard } from "./common/SummaryItemCard"; + //------------------------------- // Typography Components //------------------------------- @@ -428,4 +431,14 @@ export { * /> */ Newsletter, + + /** + * Compact summary banner for advanced review steps. + */ + AdvancedSummaryBanner, + + /** + * Shared compact summary item shell for review cards. + */ + SummaryItemCard, }; diff --git a/src/components/Star/WAN/VPNClient/VPNClientAdvanced/AdvancedVPNClient.tsx b/src/components/Star/WAN/VPNClient/VPNClientAdvanced/AdvancedVPNClient.tsx index 462b1a92..1ea591cd 100644 --- a/src/components/Star/WAN/VPNClient/VPNClientAdvanced/AdvancedVPNClient.tsx +++ b/src/components/Star/WAN/VPNClient/VPNClientAdvanced/AdvancedVPNClient.tsx @@ -303,7 +303,6 @@ export const VPNClientAdvanced = component$( type: "L2TP" as const, enabled: true, priority: 1, // Give it highest priority - weight: 50, config: { Name: "NasNetConnect", Server: { diff --git a/src/components/Star/WAN/VPNClient/VPNClientAdvanced/hooks/useVPNClientAdvanced.ts b/src/components/Star/WAN/VPNClient/VPNClientAdvanced/hooks/useVPNClientAdvanced.ts index f9370408..7b557d2a 100644 --- a/src/components/Star/WAN/VPNClient/VPNClientAdvanced/hooks/useVPNClientAdvanced.ts +++ b/src/components/Star/WAN/VPNClient/VPNClientAdvanced/hooks/useVPNClientAdvanced.ts @@ -150,7 +150,6 @@ export const useVPNClientAdvanced = (): UseVPNClientAdvancedReturn => { type, enabled: true, priority: state.vpnConfigs.length + 1, - weight: 50, assignedLink: undefined as string | undefined, }; diff --git a/src/components/Star/WAN/VPNClient/VPNClientAdvanced/steps/Step3_Summary.tsx b/src/components/Star/WAN/VPNClient/VPNClientAdvanced/steps/Step3_Summary.tsx index 27d6748f..8bf9fb33 100644 --- a/src/components/Star/WAN/VPNClient/VPNClientAdvanced/steps/Step3_Summary.tsx +++ b/src/components/Star/WAN/VPNClient/VPNClientAdvanced/steps/Step3_Summary.tsx @@ -1,5 +1,10 @@ import { component$, $, type QRL } from "@builder.io/qwik"; -import { Card, Alert } from "~/components/Core"; +import { + Card, + Alert, + AdvancedSummaryBanner, + SummaryItemCard, +} from "~/components/Core"; import type { VPNClientAdvancedState } from "../types/VPNClientAdvancedTypes"; import type { UseVPNClientAdvancedReturn } from "../hooks/useVPNClientAdvanced"; import { semanticMessages, useMessageLocale } from "~/i18n/semantic"; @@ -14,6 +19,10 @@ export interface Step3SummaryProps { export const Step3_Summary = component$( ({ wizardState, onEdit$, onValidate$: _onValidate$ }) => { const locale = useMessageLocale(); + const showsWeightBadges = + wizardState.multiVPNStrategy?.strategy === "LoadBalance" || + wizardState.multiVPNStrategy?.strategy === "Both"; + // Check validation status const hasValidationErrors = Object.keys(wizardState.validationErrors).length > 0; @@ -69,32 +78,20 @@ export const Step3_Summary = component$( return (
- {/* Modern Header with Gradient */} -
-
-

- {semanticMessages.vpn_client_advanced_summary_title( - {}, - { - locale, - }, - )} -

-

- {semanticMessages.vpn_client_advanced_summary_description( - {}, - { - locale, - }, - )} -

-
- {/* Background Pattern */} -
-
-
-
-
+ {/* Status Alert with Modern Style */} {hasValidationErrors && ( @@ -168,7 +165,7 @@ export const Step3_Summary = component$(
-
+
{[...wizardState.vpnConfigs] .sort((a, b) => (a.priority || 0) - (b.priority || 0)) .map((vpn, index) => { @@ -181,133 +178,88 @@ export const Step3_Summary = component$( : "border-yellow-300 dark:border-yellow-600"; return ( -
-
-
- {/* Priority Badge */} -
- - {semanticMessages.wan_advanced_priority( - {}, - { locale }, - )} - -
- {index + 1} -
-
- - {/* Protocol Icon with Gradient Background */} -
- - - -
- - {/* VPN Details */} -
-

- {vpn.name} -

-
- - - - - {vpn.type || - semanticMessages.vpn_client_advanced_no_protocol_selected( - {}, - { locale }, - )} - - {vpn.assignedLink && ( - <> - - - - - - {vpn.assignedLink} - - - )} -
-
+ +
+ + {semanticMessages.wan_advanced_priority({}, { locale })} + +
+ {index + 1}
+
- {/* Status Indicators */} -
- {!vpn.enabled ? ( - - - {semanticMessages.shared_disabled({}, { locale })} - - ) : isConfigured ? ( - - - {semanticMessages.shared_ready({}, { locale })} - - ) : ( - - - {semanticMessages.vpn_client_advanced_not_configured_status( - {}, - { locale }, - )} - - )} +
+ + + +
- {/* Weight Badge for Load Balancing */} - {vpn.weight && ( - - {vpn.weight}%{" "} - {semanticMessages.vpn_client_advanced_weight( - {}, - { locale }, - )} +

+ {vpn.name} +

+
+ + {vpn.type || + semanticMessages.vpn_client_advanced_no_protocol_selected( + {}, + { locale }, + )} + + {vpn.assignedLink && ( + <> + + + {vpn.assignedLink} - )} -
+ + )}
- {/* Hover Effect Line */} -
-
+
+ {!vpn.enabled ? ( + + + {semanticMessages.shared_disabled({}, { locale })} + + ) : isConfigured ? ( + + + {semanticMessages.shared_ready({}, { locale })} + + ) : ( + + + {semanticMessages.vpn_client_advanced_not_configured_status( + {}, + { locale }, + )} + + )} + + {showsWeightBadges && vpn.weight !== undefined && ( + + {vpn.weight}%{" "} + {semanticMessages.vpn_client_advanced_weight( + {}, + { locale }, + )} + + )} +
+ ); })}
diff --git a/src/components/Star/WAN/WANInterface/WANInterfaceAdvanced/WANAdvanced.tsx b/src/components/Star/WAN/WANInterface/WANInterfaceAdvanced/WANAdvanced.tsx index 4b3538bd..6d7a439d 100644 --- a/src/components/Star/WAN/WANInterface/WANInterfaceAdvanced/WANAdvanced.tsx +++ b/src/components/Star/WAN/WANInterface/WANInterfaceAdvanced/WANAdvanced.tsx @@ -11,7 +11,6 @@ import type { CStepMeta } from "../../../../Core/Stepper/CStepper/types"; import { StarContext } from "../../../StarContext/StarContext"; import type { StarContextType } from "../../../StarContext/StarContext"; import { Step1_LinkInterface } from "./steps/Step1_LinkInterface"; -import { Step2_Connection } from "./steps/Step2_Connection"; import { Step3_MultiLink } from "./steps/Step3_MultiLink"; import { Step4_Summary } from "./steps/Step4_Summary"; import { useWANAdvanced } from "./hooks/useWANAdvanced"; @@ -41,8 +40,8 @@ export const WANAdvanced = component$( const stepsInitialized = useSignal(false); // Track step completion status - const step1Complete = useSignal(false); // Link & Interface - const step2Complete = useSignal(false); // Connection + const step1Complete = useSignal(false); // Interface + connection + const step2Complete = useSignal(false); // Connection readiness for downstream gating // Note: Removed automatic step completion tracking to avoid potential render loops @@ -191,36 +190,6 @@ export const WANAdvanced = component$( ), isComplete: step1Complete.value, }, - { - id: 2, - title: - mode === "Foreign" - ? semanticMessages.wan_advanced_step_connection_foreign( - {}, - { locale }, - ) - : semanticMessages.wan_advanced_step_connection_domestic( - {}, - { locale }, - ), - description: - mode === "Foreign" - ? semanticMessages.wan_advanced_step_connection_foreign_desc( - {}, - { locale }, - ) - : semanticMessages.wan_advanced_step_connection_domestic_desc( - {}, - { locale }, - ), - component: ( - - ), - isComplete: step2Complete.value, - }, ]; // Add multi-link strategy step only if user has multiple links @@ -241,7 +210,7 @@ export const WANAdvanced = component$( isComplete: true, // Allow navigation between steps skippable: true, isOptional: true, - isDisabled: !step1Complete.value || !step2Complete.value, + isDisabled: !step1Complete.value, }); } @@ -263,7 +232,7 @@ export const WANAdvanced = component$( isComplete: true, // Allow navigation between steps skippable: true, isOptional: true, - isDisabled: !step1Complete.value || !step2Complete.value, + isDisabled: !step1Complete.value, }); return steps; @@ -294,31 +263,31 @@ export const WANAdvanced = component$( // Don't track weight/priority changes as they don't affect step structure // Weight and priority tracking removed to prevent infinite loops - // Check step 1 completion: All links have interfaces selected + // Step 1 is complete only when links are fully configured on the merged screen. const allLinksHaveInterfaces = advancedHooks.state.links.length > 0 && advancedHooks.state.links.every((link) => link.interfaceName); + const allLinksHaveConnectionConfirmed = + advancedHooks.state.links.length > 0 && + advancedHooks.state.links.every( + (link) => link.connectionType && link.connectionConfirmed, + ); + const firstStepComplete = + allLinksHaveInterfaces && allLinksHaveConnectionConfirmed; // Save to StarContext when step 1 is completed for the first time const prevStep1Complete = step1Complete.value; - step1Complete.value = allLinksHaveInterfaces; - if (!prevStep1Complete && allLinksHaveInterfaces) { + step1Complete.value = firstStepComplete; + if (!prevStep1Complete && firstStepComplete) { await advancedHooks.syncWithStarContext$(); } // Immediately update step 1's isComplete property for responsive UI if (steps.value[0]) { - steps.value[0].isComplete = allLinksHaveInterfaces; + steps.value[0].isComplete = firstStepComplete; steps.value = [...steps.value]; // Trigger reactivity } - // Check step 2 completion: All links have connection type selected and confirmed - const allLinksHaveConnectionConfirmed = - advancedHooks.state.links.length > 0 && - advancedHooks.state.links.every( - (link) => link.connectionType && link.connectionConfirmed, - ); - // Save to StarContext when step 2 is completed for the first time const prevStep2Complete = step2Complete.value; step2Complete.value = allLinksHaveConnectionConfirmed; @@ -326,25 +295,15 @@ export const WANAdvanced = component$( await advancedHooks.syncWithStarContext$(); } - // Immediately update step 2's isComplete property for responsive UI - if (steps.value[1]) { - steps.value[1].isComplete = allLinksHaveConnectionConfirmed; - } - - // Update optional Step 3 (MultiLink) if it exists - always allow navigation - if (steps.value[2] && advancedHooks.state.links.length > 1) { - steps.value[2].isDisabled = - !allLinksHaveInterfaces || !allLinksHaveConnectionConfirmed; + // Update optional MultiLink step if it exists. + if (steps.value[1] && advancedHooks.state.links.length > 1) { + steps.value[1].isDisabled = !firstStepComplete; } // Update Review step (last step) if it exists const lastStepIndex = steps.value.length - 1; - if ( - steps.value[lastStepIndex] && - steps.value[lastStepIndex].title.includes("Review") - ) { - steps.value[lastStepIndex].isDisabled = - !allLinksHaveInterfaces || !allLinksHaveConnectionConfirmed; + if (steps.value[lastStepIndex] && lastStepIndex > 0) { + steps.value[lastStepIndex].isDisabled = !firstStepComplete; } // Trigger reactivity after all updates @@ -357,21 +316,15 @@ export const WANAdvanced = component$( if (stepsInitialized.value) { // Check if we need to add/remove the multi-link step const hasMultipleLinks = advancedHooks.state.links.length > 1; - const currentHasMultiLink = steps.value.some((s) => - s.title.includes("LoadBalance"), - ); + const expectedStepCount = hasMultipleLinks ? 3 : 2; // Only recreate steps if structure changes (multi-link step added/removed) - if (hasMultipleLinks !== currentHasMultiLink) { + if (steps.value.length !== expectedStepCount) { const newSteps = await createSteps(); steps.value = newSteps; - // If we're currently on step 3 but it no longer exists (single link), go to step 2 - if ( - activeStep.value >= 2 && - advancedHooks.state.links.length === 1 - ) { - activeStep.value = 1; // Go to step 2 (0-indexed) + if (activeStep.value >= newSteps.length) { + activeStep.value = Math.max(0, newSteps.length - 1); } } else { // Only update completion status without recreating components @@ -379,7 +332,12 @@ export const WANAdvanced = component$( const newCompletions = { step1: advancedHooks.state.links.length > 0 && - advancedHooks.state.links.every((link) => link.interfaceName), + advancedHooks.state.links.every( + (link) => + link.interfaceName && + link.connectionType && + link.connectionConfirmed, + ), step2: advancedHooks.state.links.length > 0 && advancedHooks.state.links.every( @@ -392,16 +350,10 @@ export const WANAdvanced = component$( steps.value[0].isComplete = newCompletions.step1; } - // Update step 2 completion - if (steps.value[1]) { - steps.value[1].isComplete = newCompletions.step2; - } - // Update optional steps' disabled state - for (let i = 2; i < steps.value.length; i++) { + for (let i = 1; i < steps.value.length; i++) { if (steps.value[i]) { - steps.value[i].isDisabled = - !newCompletions.step1 || !newCompletions.step2; + steps.value[i].isDisabled = !newCompletions.step1; } } diff --git a/src/components/Star/WAN/WANInterface/WANInterfaceAdvanced/components/fields/ConnectionTypeSelector.tsx b/src/components/Star/WAN/WANInterface/WANInterfaceAdvanced/components/fields/ConnectionTypeSelector.tsx index d6edab26..d22700e8 100644 --- a/src/components/Star/WAN/WANInterface/WANInterfaceAdvanced/components/fields/ConnectionTypeSelector.tsx +++ b/src/components/Star/WAN/WANInterface/WANInterfaceAdvanced/components/fields/ConnectionTypeSelector.tsx @@ -26,21 +26,6 @@ export const ConnectionTypeSelector = component$( {}, { locale }, ), - icon: ( - - - - ), }, { value: "PPPoE" as ConnectionType, @@ -55,21 +40,6 @@ export const ConnectionTypeSelector = component$( {}, { locale }, ), - icon: ( - - - - ), }, { value: "Static" as ConnectionType, @@ -84,21 +54,6 @@ export const ConnectionTypeSelector = component$( {}, { locale }, ), - icon: ( - - - - ), }, ]; @@ -173,12 +128,6 @@ export const ConnectionTypeSelector = component$( : "border-gray-200 bg-white hover:border-gray-300 dark:border-gray-700 dark:bg-gray-800 dark:hover:border-gray-600" }`; - const iconClass = `flex h-12 w-12 items-center justify-center rounded-lg transition-colors ${ - isSelected - ? "bg-primary-500 text-white" - : "bg-gray-100 text-gray-600 group-hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-400 dark:group-hover:bg-gray-600" - }`; - return ( - ))} -
-
- - {/* Interface Selection */} @@ -270,13 +150,15 @@ export const InterfaceSelector = component$( onChange$={(value: string | string[]) => { const selectedValue = Array.isArray(value) ? value[0] : value; const previousInterface = link.interfaceName; + const selectedInterface = currentInterfaces.find( + ({ interfaceName }) => interfaceName === selectedValue, + ); - // Update the interface selection onUpdate$({ interfaceName: selectedValue, + interfaceType: selectedInterface?.interfaceType, }); - // Update occupied interfaces in context const updatedModels = starContext.state.Choose.RouterModels.map( (model) => { if (!model.isMaster) return model; @@ -315,7 +197,9 @@ export const InterfaceSelector = component$( { locale }, )} options={getSelectOptions()} - key={`${link.id}-${link.interfaceType}`} + data-testid={`wan-advanced-interface-select-${link.id}`} + clearable={false} + key={link.id} />
diff --git a/src/components/Star/WAN/WANInterface/WANInterfaceAdvanced/components/fields/VLANMACFields.tsx b/src/components/Star/WAN/WANInterface/WANInterfaceAdvanced/components/fields/VLANMACFields.tsx index 17247872..a9efb9ce 100644 --- a/src/components/Star/WAN/WANInterface/WANInterfaceAdvanced/components/fields/VLANMACFields.tsx +++ b/src/components/Star/WAN/WANInterface/WANInterfaceAdvanced/components/fields/VLANMACFields.tsx @@ -19,188 +19,150 @@ export const VLANMACFields = component$( const locale = useMessageLocale(); return ( -
- {/* Background decoration */} -
+
+ {/* VLAN Configuration Card */} +
+
-
- {/* Header with icon */} -
-
- - - - -
-

- {semanticMessages.wan_advanced_advanced_network_settings( - {}, - { locale }, - )} -

-
- -
- {/* VLAN Configuration Card */} -
-
- -
- {/* Toggle with modern design */} - - - {/* VLAN ID Input */} - {vlanConfig?.enabled === true && ( - - { - const numValue = parseInt(value) || 1; - onUpdateVLAN$({ enabled: true, id: numValue }); - }} - placeholder="Enter VLAN ID" +
+ {/* Toggle with modern design */} +
- {/* MAC Address Configuration Card */} -
-
+ { + if (checked) { + onUpdateVLAN$({ + enabled: true, + id: vlanConfig?.id || 1, + }); + } else { + onUpdateVLAN$(undefined); + } + })} + size="sm" + /> + -
- {/* Toggle with modern design */} -
+
- { - if (checked) { - onUpdateMAC$({ - enabled: true, - address: macAddress?.address || "", - }); - } else { - onUpdateMAC$(undefined); - } - })} - size="sm" - /> - + {/* MAC Address Configuration Card */} +
+
- {/* MAC Address Input */} - {macAddress?.enabled === true && ( - - { - onUpdateMAC$({ enabled: true, address: value }); - }} - placeholder="00:00:00:00:00:00" - class="font-mono" +
+ {/* Toggle with modern design */} +
+ + { + if (checked) { + onUpdateMAC$({ + enabled: true, + address: macAddress?.address || "", + }); + } else { + onUpdateMAC$(undefined); + } + })} + size="sm" + /> + + + {/* MAC Address Input */} + {macAddress?.enabled === true && ( + + { + onUpdateMAC$({ enabled: true, address: value }); + }} + placeholder="00:00:00:00:00:00" + class="font-mono" + /> + + )}
diff --git a/src/components/Star/WAN/WANInterface/WANInterfaceAdvanced/hooks/useWANAdvanced.tsx b/src/components/Star/WAN/WANInterface/WANInterfaceAdvanced/hooks/useWANAdvanced.tsx index 3d28c58d..d269f30a 100644 --- a/src/components/Star/WAN/WANInterface/WANInterfaceAdvanced/hooks/useWANAdvanced.tsx +++ b/src/components/Star/WAN/WANInterface/WANInterfaceAdvanced/hooks/useWANAdvanced.tsx @@ -259,20 +259,22 @@ export function useWANAdvanced( updates.interfaceType && updates.interfaceType !== currentLink.interfaceType ) { - // Release the old interface when type changes - if (updatedLink.interfaceName) { + if (updates.interfaceName === undefined && currentLink.interfaceName) { await interfaceManagement.releaseInterface$( - updatedLink.interfaceName as InterfaceType, + currentLink.interfaceName as InterfaceType, ); + updatedLink.interfaceName = ""; } - updatedLink.interfaceName = ""; updatedLink.wirelessCredentials = undefined; updatedLink.lteSettings = undefined; - // Reset connection type for LTE if (updates.interfaceType === "LTE") { updatedLink.connectionType = "LTE"; + updatedLink.connectionConfig = undefined; + } else if (currentLink.connectionType === "LTE") { + updatedLink.connectionType = "DHCP"; + updatedLink.connectionConfig = { isDHCP: true }; } } @@ -320,13 +322,18 @@ export function useWANAdvanced( linkUpdates.interfaceType && linkUpdates.interfaceType !== currentLink.interfaceType ) { - updatedLink.interfaceName = ""; + if (linkUpdates.interfaceName === undefined) { + updatedLink.interfaceName = ""; + } updatedLink.wirelessCredentials = undefined; updatedLink.lteSettings = undefined; - // Reset connection type for LTE if (linkUpdates.interfaceType === "LTE") { updatedLink.connectionType = "LTE"; + updatedLink.connectionConfig = undefined; + } else if (currentLink.connectionType === "LTE") { + updatedLink.connectionType = "DHCP"; + updatedLink.connectionConfig = { isDHCP: true }; } } diff --git a/src/components/Star/WAN/WANInterface/WANInterfaceAdvanced/hooks/useWANValidation.tsx b/src/components/Star/WAN/WANInterface/WANInterfaceAdvanced/hooks/useWANValidation.tsx index 85d6ecae..2691966c 100644 --- a/src/components/Star/WAN/WANInterface/WANInterfaceAdvanced/hooks/useWANValidation.tsx +++ b/src/components/Star/WAN/WANInterface/WANInterfaceAdvanced/hooks/useWANValidation.tsx @@ -146,7 +146,7 @@ export function useWANValidation(): UseWANValidationReturn { let allErrors: Record = {}; switch (step) { - case 0: // Step 1: Link & Interface Configuration + case 0: // Step 1: Link, interface, and connection configuration for (const link of state.links) { const validation = await validateLink$( link, @@ -169,26 +169,27 @@ export function useWANValidation(): UseWANValidationReturn { if (link.interfaceType === "LTE" && !link.lteSettings) { allErrors[`link-${link.id}-lte`] = ["LTE settings required"]; } - } - break; - case 1: // Step 2: Connection Configuration - for (const link of state.links) { - // Validate connection configuration + if (!link.connectionType) { + allErrors[`link-${link.id}-connection-type`] = [ + "Connection type is required", + ]; + } + if ( + link.connectionType && link.connectionType !== "DHCP" && - link.connectionType !== "LTE" + link.connectionType !== "LTE" && + !link.connectionConfig ) { - if (!link.connectionConfig) { - allErrors[`link-${link.id}-connection`] = [ - "Connection configuration required", - ]; - } + allErrors[`link-${link.id}-connection`] = [ + "Connection configuration required", + ]; } } break; - case 2: // Step 3: Multi-Link Strategy + case 1: // Step 2: Multi-Link Strategy if (state.links.length > 1 && !state.multiLinkStrategy) { allErrors["multi-link"] = [ "Multi-link strategy must be configured", @@ -212,7 +213,7 @@ export function useWANValidation(): UseWANValidationReturn { } break; - case 3: // Step 4: Summary + case 2: // Step 3: Summary/Review // Full validation const fullValidation = await validateAdvanced$(state); if (!fullValidation.isValid) { diff --git a/src/components/Star/WAN/WANInterface/WANInterfaceAdvanced/steps/Step1_LinkInterface.tsx b/src/components/Star/WAN/WANInterface/WANInterfaceAdvanced/steps/Step1_LinkInterface.tsx index c4ef7886..3688b5d1 100644 --- a/src/components/Star/WAN/WANInterface/WANInterfaceAdvanced/steps/Step1_LinkInterface.tsx +++ b/src/components/Star/WAN/WANInterface/WANInterfaceAdvanced/steps/Step1_LinkInterface.tsx @@ -4,6 +4,9 @@ import { InterfaceSelector } from "../components/fields/InterfaceSelector"; import { WirelessFields } from "../components/fields/WirelessFields"; import { LTEFields } from "../components/fields/LTEFields"; import { VLANMACFields } from "../components/fields/VLANMACFields"; +import { ConnectionTypeSelector } from "../components/fields/ConnectionTypeSelector"; +import { PPPoEFields } from "../components/fields/PPPoEFields"; +import { StaticIPFields } from "../components/fields/StaticIPFields"; import type { UseWANAdvancedReturn } from "../hooks/useWANAdvanced"; import { Input } from "~/components/Core"; import { SearchBar } from "../components/common/SearchBar"; @@ -15,7 +18,11 @@ import { filterLinks, getLinkStatistics, } from "../utils/linkHelpers"; -import { getLinkErrors, getFieldErrors } from "../utils/validationUtils"; +import { + getLinkErrors, + getFieldErrors, + isLinkConfigurationComplete, +} from "../utils/validationUtils"; import { semanticMessages, useMessageLocale } from "~/i18n/semantic"; export interface Step1Props { @@ -40,9 +47,138 @@ export const Step1_LinkInterface = component$( // Helper to handle interface changes const handleInterfaceUpdate = $(async (linkId: string, updates: any) => { - await wizardActions.updateLink$(linkId, updates); + const link = wizardState.links.find((item) => item.id === linkId); + if (!link) return; + + const updatedLink = { ...link, ...updates }; + + if (updates.interfaceType === "LTE") { + updatedLink.connectionType = "LTE"; + } else if (updates.interfaceType && link.connectionType === "LTE") { + updatedLink.connectionType = "DHCP"; + updatedLink.connectionConfig = { isDHCP: true }; + } + + await wizardActions.updateLink$(linkId, { + ...updates, + connectionConfirmed: isLinkConfigurationComplete(updatedLink), + }); + }); + + const handleConnectionUpdate = $(async (linkId: string, updates: any) => { + const link = wizardState.links.find((item) => item.id === linkId); + if (!link) return; + + const updatedLink = { ...link, ...updates }; + + await wizardActions.updateLink$(linkId, { + ...updates, + connectionConfirmed: isLinkConfigurationComplete(updatedLink), + }); }); + const renderConnectionFields = (link: WANWizardState["links"][0]) => { + if (!link.interfaceName) { + return null; + } + + if (link.interfaceType === "LTE") { + return null; + } + + return ( +
+
+

+ {semanticMessages.wan_advanced_configure_connection_type( + {}, + { locale }, + )} +

+
+ + + handleConnectionUpdate(link.id, { + connectionType: type, + }), + )} + mode={wizardState.mode} + /> + + {link.connectionType === "PPPoE" && ( + + handleConnectionUpdate(link.id, { + connectionConfig: { + ...link.connectionConfig, + pppoe: config, + }, + }), + )} + errors={{ + username: getFieldErrors( + link.id, + "pppoe-username", + wizardState.validationErrors, + ), + password: getFieldErrors( + link.id, + "pppoe-password", + wizardState.validationErrors, + ), + }} + /> + )} + + {link.connectionType === "Static" && ( + + handleConnectionUpdate(link.id, { + connectionConfig: { + ...link.connectionConfig, + static: config, + }, + }), + )} + errors={{ + ipAddress: getFieldErrors( + link.id, + "static-ip", + wizardState.validationErrors, + ), + subnet: getFieldErrors( + link.id, + "static-subnet", + wizardState.validationErrors, + ), + gateway: getFieldErrors( + link.id, + "static-gateway", + wizardState.validationErrors, + ), + DNS: getFieldErrors( + link.id, + "static-dns1", + wizardState.validationErrors, + ), + secondaryDns: getFieldErrors( + link.id, + "static-dns2", + wizardState.validationErrors, + ), + }} + /> + )} +
+ ); + }; + // Get link statistics const stats = getLinkStatistics( wizardState.links, @@ -98,21 +234,6 @@ export const Step1_LinkInterface = component$( {/* Empty State Message */}
-
- - - -

{semanticMessages.wan_advanced_no_links({}, { locale })}

@@ -321,6 +442,10 @@ export const Step1_LinkInterface = component$( />
)} + + {singleLink.interfaceName && ( +
{renderConnectionFields(singleLink)}
+ )}
) : ( @@ -548,6 +673,8 @@ export const Step1_LinkInterface = component$( }} /> )} + + {link.interfaceName && renderConnectionFields(link)} ); })} diff --git a/src/components/Star/WAN/WANInterface/WANInterfaceAdvanced/steps/Step4_Summary.tsx b/src/components/Star/WAN/WANInterface/WANInterfaceAdvanced/steps/Step4_Summary.tsx index 50190c32..493a57fc 100644 --- a/src/components/Star/WAN/WANInterface/WANInterfaceAdvanced/steps/Step4_Summary.tsx +++ b/src/components/Star/WAN/WANInterface/WANInterfaceAdvanced/steps/Step4_Summary.tsx @@ -1,7 +1,8 @@ import { component$, type QRL, useComputed$, $ } from "@builder.io/qwik"; import type { WANWizardState } from "../types"; -import { Alert, Card } from "~/components/Core"; +import { Alert, Card, AdvancedSummaryBanner, SummaryItemCard } from "~/components/Core"; import { semanticMessages, useMessageLocale } from "~/i18n/semantic"; +import { renderInterfaceTypeIcon } from "../utils/interfaceTypeIcons"; export interface Step4Props { wizardState: WANWizardState; @@ -39,38 +40,6 @@ export const Step4_Summary = component$( return strategies[strategy || ""] || ""; }; - const getInterfaceIcon = (type: string) => { - switch (type) { - case "Ethernet": - return "M8 12h8m-8 0a8 8 0 1 0 16 0 8 8 0 1 0-16 0"; - case "Wireless": - return "M8.111 16.404a5.5 5.5 0 0 1 7.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0"; - case "LTE": - return "M3 7v10a2 2 0 002 2h14a2 2 0 002-2V7M5 7h14M8 7V5a2 2 0 012-2h4a2 2 0 012 2v2"; - case "SFP": - return "M12 6v6m0 0v6m0-6h6m-6 0H6"; - default: - return "M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"; - } - }; - - const getConnectionIcon = (type?: string) => { - if (!type) - return "M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"; - switch (type) { - case "DHCP": - return "M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01"; - case "PPPoE": - return "M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"; - case "Static": - return "M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"; - case "LTE": - return "M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z"; - default: - return "M13 10V3L4 14h7v7l9-11h-7z"; - } - }; - const getConnectionTypeColor = (type?: string) => { switch (type) { case "DHCP": @@ -116,28 +85,16 @@ export const Step4_Summary = component$( return (
- {/* Modern Header with Gradient */} -
-
-

- {semanticMessages.wan_advanced_summary_title({}, { locale })} -

-

- {semanticMessages.wan_advanced_review_before_deploy( - {}, - { locale }, - )} -

-
- {/* Background Pattern */} -
-
-
-
-
+ {/* Status Alert with Modern Style */} - {validationErrors.value.hasErrors ? ( + {validationErrors.value.hasErrors && (
(
- ) : ( - -
- - - -
-

- {semanticMessages.wan_advanced_configuration_ready( - {}, - { locale }, - )} -

-

- {semanticMessages.wan_advanced_validated_successfully( - {}, - { locale }, - )} -

-
-
-
)} {/* WAN Links Overview with Modern Cards */} @@ -235,7 +162,7 @@ export const Step4_Summary = component$(
-
+
{sortedLinksByPriority.value.map((link, index) => { const isConfigured = link.connectionType && link.connectionConfirmed; @@ -244,53 +171,89 @@ export const Step4_Summary = component$( : "border-green-300 dark:border-green-600"; return ( -
-
-
- {/* Priority Badge */} -
- - {semanticMessages.wan_advanced_priority( - {}, - { locale }, - )} + +
+ + {semanticMessages.wan_advanced_priority({}, { locale })} + +
+ {index + 1} +
+
+ +
+ {renderInterfaceTypeIcon( + link.interfaceType || "Ethernet", + "h-5 w-5", + )} +
+ +

+ {link.name} +

+
+ + {link.interfaceType || + semanticMessages.wan_advanced_no_interface_selected( + {}, + { locale }, + )}{" "} + •{" "} + {link.interfaceName || + semanticMessages.wan_advanced_not_selected( + {}, + { locale }, + )} + + {link.connectionType && link.connectionType !== "LTE" && ( + <> + + + {getConnectionTypeDisplay(link.connectionType)} -
- {index + 1} -
-
+ + )} +
- {/* Interface Icon with Gradient Background */} -
- - - -
+ {(link.wirelessCredentials || link.vlanConfig?.enabled) && ( +
+ {link.wirelessCredentials && ( + + + + + {link.wirelessCredentials.SSID} + + )} + + {link.vlanConfig?.enabled && ( + + VLAN {link.vlanConfig.id} + + )} +
+ )} - {/* Link Details */} -
-

- {link.name} -

-
- + {link.connectionType === "PPPoE" && + link.connectionConfig?.pppoe && ( +
+
+
( stroke-linecap="round" stroke-linejoin="round" stroke-width="2" - d="M8 12h8m-8 0a8 8 0 1 0 16 0 8 8 0 1 0-16 0" + d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" /> - {link.interfaceType || - semanticMessages.wan_advanced_no_interface_selected( - {}, - { locale }, - )}{" "} - •{" "} - {link.interfaceName || - semanticMessages.wan_advanced_not_selected( +
+
+

+ {semanticMessages.wan_advanced_username( {}, { locale }, )} - - {link.connectionType && ( - <> - - - - - - {getConnectionTypeDisplay(link.connectionType)} + :{" "} + + {link.connectionConfig.pppoe.username} - - )} +

+
+
+ )} - {/* Additional Configuration Details */} -
- {link.wirelessCredentials && ( - - - - - {link.wirelessCredentials.SSID} + {link.connectionType === "Static" && + link.connectionConfig?.static && ( +
+
+
+ + + + + IP:{" "} + + {link.connectionConfig.static.ipAddress}/ + {link.connectionConfig.static.subnet} + - )} - - {link.vlanConfig?.enabled && ( - - VLAN {link.vlanConfig.id} +
+
+ + + + + Gateway:{" "} + + {link.connectionConfig.static.gateway} + - )} - - {wizardState.links.length > 1 && - link.weight !== undefined && - wizardState.multiLinkStrategy?.strategy !== - "Failover" && ( - - {link.weight}%{" "} - {semanticMessages.wan_advanced_weight( - {}, - { locale }, - )} +
+
+ + + + + DNS:{" "} + + {link.connectionConfig.static.DNS} - )} + +
- - {/* Connection Configuration Details */} - {link.connectionType === "PPPoE" && - link.connectionConfig?.pppoe && ( -
-
-
- - - -
-
-

- {semanticMessages.wan_advanced_username( - {}, - { locale }, - )} - :{" "} - - {link.connectionConfig.pppoe.username} - -

-
-
-
- )} - - {link.connectionType === "Static" && - link.connectionConfig?.static && ( -
-
-
- - - - - IP:{" "} - - {link.connectionConfig.static.ipAddress}/ - {link.connectionConfig.static.subnet} - - -
-
- - - - - Gateway:{" "} - - {link.connectionConfig.static.gateway} - - -
-
- - - - - DNS:{" "} - - {link.connectionConfig.static.DNS} - - -
-
-
- )}
-
+ )} - {/* Status Indicators */} -
- {!isConfigured ? ( - - - {semanticMessages.wan_advanced_not_configured( - {}, - { locale }, - )} - - ) : ( - - - {semanticMessages.wan_advanced_link_ready( +
+ {!isConfigured ? ( + + + {semanticMessages.wan_advanced_not_configured( + {}, + { locale }, + )} + + ) : ( + + + {semanticMessages.wan_advanced_link_ready( + {}, + { locale }, + )} + + )} + + {wizardState.links.length > 1 && + link.weight !== undefined && + wizardState.multiLinkStrategy?.strategy !== "Failover" && ( + + {link.weight}%{" "} + {semanticMessages.wan_advanced_weight( {}, { locale }, )} )} -
- - {/* Hover Effect Line */} -
-
+ ); })}
@@ -534,7 +405,7 @@ export const Step4_Summary = component$( )}