From 65f48321dc8a0a79b8b05029dc2f73b3a86282c6 Mon Sep 17 00:00:00 2001 From: Charles Date: Thu, 21 Mar 2024 13:36:07 +0100 Subject: [PATCH] [WIFI-13534] Captive portal configuration schema fixes Signed-off-by: Charles --- package-lock.json | 4 +- package.json | 2 +- .../Sections/CaptivePortal/index.tsx | 4 +- .../SingleInterface/Captive/Captive.tsx | 102 ------------ .../SingleInterface/Captive/index.tsx | 42 ----- .../SsidList/AdvancedSettings.tsx | 2 + .../SsidList/Captive/Captive.tsx | 152 ++++++++++++++++++ .../{ => SsidList}/Captive/LockedCaptive.tsx | 0 .../SsidList/Captive/index.tsx | 54 +++++++ .../SingleInterface/index.tsx | 2 - .../InterfaceSection/interfacesConstants.js | 142 ++++++++++++++-- 11 files changed, 340 insertions(+), 166 deletions(-) delete mode 100644 src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/SingleInterface/Captive/Captive.tsx delete mode 100644 src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/SingleInterface/Captive/index.tsx create mode 100644 src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/SingleInterface/SsidList/Captive/Captive.tsx rename src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/SingleInterface/{ => SsidList}/Captive/LockedCaptive.tsx (100%) create mode 100644 src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/SingleInterface/SsidList/Captive/index.tsx diff --git a/package-lock.json b/package-lock.json index 9e645ba3..ce53593b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "wlan-cloud-owprov-ui", - "version": "3.0.2(1)", + "version": "3.0.2(2)", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "wlan-cloud-owprov-ui", - "version": "3.0.2(1)", + "version": "3.0.2(2)", "license": "ISC", "dependencies": { "@chakra-ui/anatomy": "^2.1.1", diff --git a/package.json b/package.json index 23c49326..4a8200f1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wlan-cloud-owprov-ui", - "version": "3.0.2(1)", + "version": "3.0.2(2)", "description": "", "main": "index.tsx", "scripts": { diff --git a/src/components/Modals/Resources/Sections/CaptivePortal/index.tsx b/src/components/Modals/Resources/Sections/CaptivePortal/index.tsx index b648cf6a..a9080c6d 100644 --- a/src/components/Modals/Resources/Sections/CaptivePortal/index.tsx +++ b/src/components/Modals/Resources/Sections/CaptivePortal/index.tsx @@ -11,7 +11,7 @@ import { useCreateResource, useUpdateResource } from 'hooks/Network/Resources'; import { Note } from 'models/Note'; import { Resource } from 'models/Resource'; import { INTERFACE_CAPTIVE_SCHEMA } from 'pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/interfacesConstants'; -import Captive from 'pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/SingleInterface/Captive/Captive'; +import Captive from 'pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/SingleInterface/SsidList/Captive/Captive'; export const EDIT_SCHEMA = (t: (str: string) => string) => object().shape({ @@ -211,7 +211,7 @@ const InterfaceCaptiveResource = ({ - + diff --git a/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/SingleInterface/Captive/Captive.tsx b/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/SingleInterface/Captive/Captive.tsx deleted file mode 100644 index dcf53d03..00000000 --- a/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/SingleInterface/Captive/Captive.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import React from 'react'; -import { Heading, SimpleGrid, Switch, Text } from '@chakra-ui/react'; -import { INTERFACE_CAPTIVE_SCHEMA } from '../../interfacesConstants'; -import LockedCaptive from './LockedCaptive'; -import ConfigurationResourcePicker from 'components/CustomFields/ConfigurationResourcePicker'; -import NumberField from 'components/FormFields/NumberField'; -import StringField from 'components/FormFields/StringField'; - -interface Props { - isDisabled?: boolean; - isActive: boolean; - namePrefix: string; - onToggle?: (e: React.ChangeEvent) => void; - variableBlockId?: string; -} - -const CaptiveForm = ( - { - isDisabled, - namePrefix, - isActive, - onToggle, - variableBlockId - }: Props -) => (<> - - Captive Portal - {onToggle !== undefined && ( - - )} - {onToggle !== undefined && isActive && ( - - )} - - {variableBlockId !== undefined && } - {variableBlockId === undefined && isActive && ( - - - - - - - - - - )} -); - -export default React.memo(CaptiveForm); diff --git a/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/SingleInterface/Captive/index.tsx b/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/SingleInterface/Captive/index.tsx deleted file mode 100644 index 01a1a3aa..00000000 --- a/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/SingleInterface/Captive/index.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import React, { useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { INTERFACE_CAPTIVE_SCHEMA } from '../../interfacesConstants'; -import CaptiveForm from './Captive'; -import useFastField from 'hooks/useFastField'; - -const Captive = ({ editing, index }: { editing: boolean; index: number }) => { - const { t } = useTranslation(); - const { value, onChange } = useFastField({ name: `configuration[${index}].captive` }); - - const { isActive, variableBlock } = useMemo( - () => ({ - isActive: value !== undefined, - isUsingCustom: value !== undefined && value.__variableBlock === undefined, - variableBlock: value?.__variableBlock, - }), - [value], - ); - - const onToggle = useCallback( - (e: React.ChangeEvent) => { - if (!e.target.checked) { - onChange(undefined); - } else { - onChange(INTERFACE_CAPTIVE_SCHEMA(t, true).cast()); - } - }, - [onChange], - ); - - return ( - - ); -}; - -export default React.memo(Captive); diff --git a/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/SingleInterface/SsidList/AdvancedSettings.tsx b/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/SingleInterface/SsidList/AdvancedSettings.tsx index 3a9b983e..b51311d2 100644 --- a/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/SingleInterface/SsidList/AdvancedSettings.tsx +++ b/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/SingleInterface/SsidList/AdvancedSettings.tsx @@ -3,6 +3,7 @@ import { Flex, Heading, SimpleGrid } from '@chakra-ui/react'; import { useTranslation } from 'react-i18next'; import { INTERFACE_SSID_MULTIPSK_SCHEMA, NO_MULTI_PROTOS } from '../../interfacesConstants'; import AccessControlList from './AccessControlList'; +import Captive from './Captive'; import RateLimit from './RateLimit'; import Roaming from './Roaming'; import Rrm from './Rrm'; @@ -187,6 +188,7 @@ const AdvancedSettings: React.FC<{ editing: boolean; namePrefix: string }> = ({ + diff --git a/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/SingleInterface/SsidList/Captive/Captive.tsx b/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/SingleInterface/SsidList/Captive/Captive.tsx new file mode 100644 index 00000000..a0939a42 --- /dev/null +++ b/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/SingleInterface/SsidList/Captive/Captive.tsx @@ -0,0 +1,152 @@ +import React from 'react'; +import { Heading, Select, SimpleGrid, Text } from '@chakra-ui/react'; +import { useTranslation } from 'react-i18next'; +import { object, string } from 'yup'; +import CreatableSelectField from 'components/FormFields/CreatableSelectField'; +import FileInputFieldModal from 'components/FormFields/FileInputFieldModal'; +import NumberField from 'components/FormFields/NumberField'; +import ObjectArrayFieldModal from 'components/FormFields/ObjectArrayFieldModal'; +import SelectField from 'components/FormFields/SelectField'; +import StringField from 'components/FormFields/StringField'; + +const CREDENTIALS_SCHEMA = (t: (str: string) => string, useDefault = false) => { + const shape = object().shape({ + username: string().required(t('form.required')).default(''), + password: string().required(t('form.required')).default(''), + }); + + return useDefault ? shape : shape.nullable().default(undefined); +}; + +interface Props { + isDisabled?: boolean; + namePrefix: string; + onAuthModeChange?: (e: React.ChangeEvent) => void; + authMode?: string; +} + +const CaptiveForm = ({ isDisabled, namePrefix, onAuthModeChange, authMode }: Props) => { + const fieldProps = (suffix: string) => ({ + name: `${namePrefix}.${suffix}`, + label: suffix, + definitionKey: `interface.ssid.pass-point.${suffix}`, + isDisabled, + }); + const { t } = useTranslation(); + + const credFields = React.useMemo( + () => ( + + + + + ), + [], + ); + const credCols = React.useMemo( + () => [ + { + id: 'username', + Header: 'username', + Footer: '', + accessor: 'username', + }, + { + id: 'password', + Header: 'password', + Footer: '', + accessor: 'password', + }, + ], + [], + ); + + return ( + <> + + Captive Portal + + + {authMode !== undefined && ( + + + + true} + acceptedFileTypes=".tar" + isDisabled={isDisabled} + canDelete + isRequired + wantBase64 + /> + + + {authMode === 'credentials' && ( + + )} + {authMode === 'uam' && ( + <> + + + + + + + + + )} + {(authMode === 'radius' || authMode === 'uam') && ( + <> + + + + + + + + + )} + + )} + + ); +}; + +export default React.memo(CaptiveForm); diff --git a/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/SingleInterface/Captive/LockedCaptive.tsx b/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/SingleInterface/SsidList/Captive/LockedCaptive.tsx similarity index 100% rename from src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/SingleInterface/Captive/LockedCaptive.tsx rename to src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/SingleInterface/SsidList/Captive/LockedCaptive.tsx diff --git a/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/SingleInterface/SsidList/Captive/index.tsx b/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/SingleInterface/SsidList/Captive/index.tsx new file mode 100644 index 00000000..e81d72c2 --- /dev/null +++ b/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/SingleInterface/SsidList/Captive/index.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import CaptiveForm from './Captive'; +import useFastField from 'hooks/useFastField'; + +type Props = { + editing: boolean; + namePrefix: string; +}; + +const Captive = ({ editing, namePrefix }: Props) => { + const { value, onChange } = useFastField({ name: namePrefix }); + + const handleAuthModeChange = (e: React.ChangeEvent) => { + if (e.target.value === 'radius') { + onChange({ + 'idle-timeout': 600, + 'auth-mode': e.target.value, + 'auth-server': '192.168.1.10', + 'auth-secret': 'secret', + 'aut-port': 1812, + }); + } else if (e.target.value === 'uam') { + onChange({ + 'walled-garden-fqdn': [], + 'idle-timeout': 600, + 'auth-mode': e.target.value, + 'auth-server': '192.168.1.10', + 'auth-secret': 'secret', + 'aut-port': 1812, + 'uam-port': 3990, + 'uam-secret': 'secret', + 'uam-server': 'https://YOUR-LOGIN-ADDRESS.YOURS', + nasid: 'TestLab', + }); + } else if (e.target.value === 'none') { + onChange(undefined); + } else { + onChange({ 'idle-timeout': 600, 'auth-mode': e.target.value }); + } + }; + + const mode = value?.['auth-mode'] as string | undefined; + + return ( + + ); +}; + +export default React.memo(Captive); diff --git a/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/SingleInterface/index.tsx b/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/SingleInterface/index.tsx index a0945576..9281f57f 100644 --- a/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/SingleInterface/index.tsx +++ b/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/SingleInterface/index.tsx @@ -2,7 +2,6 @@ import React, { useMemo } from 'react'; import { Box, Flex, Heading, SimpleGrid, Spacer } from '@chakra-ui/react'; import { FieldArray } from 'formik'; import { useTranslation } from 'react-i18next'; -import Captive from './Captive'; import InterfaceSelectPortsField from './InterfaceSelectPortsField'; import IpV4 from './IpV4'; import IpV6 from './IpV6'; @@ -140,7 +139,6 @@ const SingleInterface: React.FC = ({ editing, index, remove }) => { - {(arrayHelpers) => ( diff --git a/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/interfacesConstants.js b/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/interfacesConstants.js index b814b53f..8aa0bf4d 100644 --- a/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/interfacesConstants.js +++ b/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/interfacesConstants.js @@ -392,6 +392,132 @@ export const INTERFACE_SSID_RRM_SCHEMA = (t, useDefault = false) => { return useDefault ? shape : shape.nullable().default(undefined); }; +export const INTERFACE_CAPTIVE_SCHEMA = (t, useDefault = false) => { + const shape = object() + .shape({ + 'auto-mode': string().required(t('form.required')).default('click'), + 'walled-garden-fqdn': array() + .when('auth-mode', { + is: 'uam', + then: (schema) => schema.of(string()).min(1, t('form.required')), + }) + .default(undefined), + 'walled-garden-ipaddr': array().of(string()).default(undefined), + 'web-root': string().default(undefined), + 'idle-timeout': number().required(t('form.required')).positive().lessThan(65535).integer().default(600), + 'session-timeout': number().positive().lessThan(65535).integer().default(undefined), + // Only if auto-mode is "credentials" + credentials: array() + .when('auth-mode', { + is: 'credentials', + then: (schema) => + schema + .of( + object().shape({ + username: string().required(t('form.required')).default(''), + password: string().required(t('form.required')).default(''), + }), + ) + .min(1, t('form.required')), + }) + .default(undefined), + // Radius && UAM values + 'auth-server': string() + .when('auth-mode', { + is: (authMode) => authMode === 'radius' || authMode === 'uam', + then: (schema) => schema.required(t('form.required')).default(''), + }) + .default(undefined), + 'auth-secret': string() + .when('auth-mode', { + is: (authMode) => authMode === 'radius' || authMode === 'uam', + then: (schema) => schema.required(t('form.required')).default(''), + else: (schema) => schema.default(undefined), + }) + .default(undefined), + 'auth-port': number() + .when('auth-mode', { + is: (authMode) => authMode === 'radius' || authMode === 'uam', + then: (schema) => schema.required(t('form.required')).moreThan(1023).lessThan(65535).integer().default(1812), + else: (schema) => schema.default(undefined), + }) + .default(undefined), + 'acct-server': string() + .when('auth-mode', { + is: (authMode) => authMode === 'radius' || authMode === 'uam', + then: (schema) => schema.default(undefined), + else: (schema) => schema.default(undefined), + }) + .default(undefined), + 'acct-secret': string() + .when('auth-mode', { + is: (authMode) => authMode === 'radius' || authMode === 'uam', + then: (schema) => schema.default(undefined), + else: (schema) => schema.default(undefined), + }) + .default(undefined), + 'acct-port': number() + .when('auth-mode', { + is: (authMode) => authMode === 'radius' || authMode === 'uam', + then: (schema) => schema.moreThan(1023).lessThan(65535).integer().default(undefined), + else: (schema) => schema.default(undefined), + }) + .default(undefined), + 'acct-interval': number() + .when('auth-mode', { + is: (authMode) => authMode === 'radius' || authMode === 'uam', + then: (schema) => schema.positive().lessThan(65535).integer().default(undefined), + }) + .default(undefined), + // Only UAM fields + 'uam-server': string() + .when('auth-mode', { + is: (authMode) => authMode === 'uam', + then: (schema) => schema.required(t('form.required')).default(''), + }) + .default(undefined), + 'uam-secret': string() + .when('auth-mode', { + is: (authMode) => authMode === 'uam', + then: (schema) => schema.required(t('form.required')).default(''), + }) + .default(undefined), + 'uam-port': number() + .when('auth-mode', { + is: (authMode) => authMode === 'uam', + then: (schema) => schema.required(t('form.required')).moreThan(1023).lessThan(65535).integer().default(3990), + }) + .default(undefined), + ssid: string() + .when('auth-mode', { + is: (authMode) => authMode === 'uam', + then: (schema) => schema.default(undefined), + }) + .default(undefined), + 'mac-format': string() + .when('auth-mode', { + is: (authMode) => authMode === 'uam', + then: (schema) => schema.required(t('form.required')).default('aabbccddeeff'), + }) + .default(undefined), + nasid: string() + .when('auth-mode', { + is: (authMode) => authMode === 'uam', + then: (schema) => schema.required(t('form.required')).default(''), + }) + .default(undefined), + nasmac: string() + .when('auth-mode', { + is: (authMode) => authMode === 'uam', + then: (schema) => schema.default(undefined), + }) + .default(undefined), + }) + .default({}); + + return useDefault ? shape : shape.nullable().default(undefined); +}; + export const INTERFACE_SSID_SCHEMA = (t, useDefault = false) => { const shape = object().shape({ name: string() @@ -434,6 +560,7 @@ export const INTERFACE_SSID_SCHEMA = (t, useDefault = false) => { encryption: INTERFACE_SSID_ENCRYPTION_SCHEMA(t, useDefault), 'rate-limit': INTERFACE_SSID_RATE_LIMIT_SCHEMA(t), rrm: INTERFACE_SSID_RRM_SCHEMA(t), + captive: useDefault ? object().shape().nullable().default(undefined) : INTERFACE_CAPTIVE_SCHEMA(t, useDefault), 'access-control-list': INTERFACE_SSID_ACCESS_CONTROL_LIST_SCHEMA(t), roaming: INTERFACE_SSID_ROAMING_SCHEMA(t), radius: INTERFACE_SSID_RADIUS_SCHEMA(t), @@ -488,20 +615,6 @@ export const INTERFACE_IPV4_DHCP_LEASE_SCHEMA = (t, useDefault = false) => { return useDefault ? shape : shape.nullable().default(undefined); }; -export const INTERFACE_CAPTIVE_SCHEMA = (t, useDefault = false) => { - const shape = object().shape({ - 'gateway-name': string().default('uCentral - Captive Portal'), - 'gateway-fqdn': string().default('ucentral.splash'), - 'max-clients': number().moreThan(0).lessThan(65535).integer().default(32), - 'upload-rate': number().moreThan(-1).lessThan(65535).integer().default(0), - 'download-rate': number().moreThan(-1).lessThan(65535).integer().default(0), - 'upload-quota': number().moreThan(-1).lessThan(65535).integer().default(0), - 'download-quota': number().moreThan(-1).lessThan(65535).integer().default(0), - }); - - return useDefault ? shape : shape.nullable().default(undefined); -}; - export const INTERFACE_BRIDGE_SCHEMA = (t, useDefault = false) => { const shape = object() .shape({ @@ -756,7 +869,6 @@ export const SINGLE_INTERFACE_SCHEMA = ( : object().shape({ id: number().required(t('form.required')).moreThan(0).lessThan(4051).default(1080), }), - captive: initialCreation ? object().shape().nullable().default(undefined) : INTERFACE_CAPTIVE_SCHEMA(t, useDefault), ipv4: initialCreation ? object() .shape({ addressing: string().required(t('form.required')) })