From 46a3be7b2e988e01a002e8b924b505e7d295a038 Mon Sep 17 00:00:00 2001 From: Charles Date: Mon, 31 Oct 2022 09:35:44 +0000 Subject: [PATCH] [WIFI-11273] Added captive to services configuration Signed-off-by: Charles --- public/locales/de/translation.json | 1 + public/locales/en/translation.json | 1 + public/locales/es/translation.json | 1 + public/locales/fr/translation.json | 1 + public/locales/pt/translation.json | 1 + .../Buttons/FileInputButton/index.tsx | 30 ++- .../FileInputFieldModal/FileInputModal.jsx | 14 +- .../FormFields/FileInputFieldModal/index.jsx | 13 ++ .../ServicesSection/Captive/index.tsx | 182 ++++++++++++++++++ .../ServicesSection/index.jsx | 3 + .../ServicesSection/servicesConstants.js | 122 ++++++++++++ 11 files changed, 365 insertions(+), 4 deletions(-) create mode 100644 src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/ServicesSection/Captive/index.tsx diff --git a/public/locales/de/translation.json b/public/locales/de/translation.json index 7dc02b5..7adfa9a 100644 --- a/public/locales/de/translation.json +++ b/public/locales/de/translation.json @@ -670,6 +670,7 @@ "version": "Ausführung" }, "form": { + "captive_web_root_explanation": "Bitte verwenden Sie nur .tar-Dateien (keine komprimierten Dateien wie z. B. .targz)", "certificate_file_explanation": "Bitte verwenden Sie eine .pem-Datei, die mit „-----BEGIN CERTIFICATE-----“ beginnt und mit „-----END CERTIFICATE-----“ endet.", "invalid_cidr": "Ungültige CIDR-IPv4-Adresse. Beispiel: 192.168.0.1/12", "invalid_email": "Ungültige E-Mail", diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 62d3364..74826f0 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -670,6 +670,7 @@ "version": "Version" }, "form": { + "captive_web_root_explanation": "Please use .tar files only (no compressed files like .targz, for example)", "certificate_file_explanation": "Please use a .pem file that starts with \"-----BEGIN CERTIFICATE-----\" and ends with \"-----END CERTIFICATE-----\"", "invalid_cidr": "Invalid CIDR IPv4 address. Example: 192.168.0.1/12", "invalid_email": "Invalid Email", diff --git a/public/locales/es/translation.json b/public/locales/es/translation.json index 64ef20a..09656f0 100644 --- a/public/locales/es/translation.json +++ b/public/locales/es/translation.json @@ -670,6 +670,7 @@ "version": "Versión" }, "form": { + "captive_web_root_explanation": "Utilice únicamente archivos .tar (no archivos comprimidos como .targz, por ejemplo)", "certificate_file_explanation": "Utilice un archivo .pem que comience con \"-----BEGIN CERTIFICATE-----\" y termine con \"-----END CERTIFICATE-----\"", "invalid_cidr": "Dirección IPv4 CIDR no válida. Ejemplo: 192.168.0.1/12", "invalid_email": "Email inválido", diff --git a/public/locales/fr/translation.json b/public/locales/fr/translation.json index 47d3317..8961f52 100644 --- a/public/locales/fr/translation.json +++ b/public/locales/fr/translation.json @@ -670,6 +670,7 @@ "version": "Version" }, "form": { + "captive_web_root_explanation": "Veuillez utiliser uniquement des fichiers .tar (pas de fichiers compressés comme .targz, par exemple)", "certificate_file_explanation": "Veuillez utiliser un fichier .pem qui commence par \"-----BEGIN CERTIFICATE-----\" et se termine par \"-----END CERTIFICATE-----\"", "invalid_cidr": "Adresse IPv4 CIDR non valide. Exemple : 192.168.0.1/12", "invalid_email": "Email Invalide", diff --git a/public/locales/pt/translation.json b/public/locales/pt/translation.json index f33ccf7..743e1a8 100644 --- a/public/locales/pt/translation.json +++ b/public/locales/pt/translation.json @@ -670,6 +670,7 @@ "version": "Versão" }, "form": { + "captive_web_root_explanation": "Por favor, use apenas arquivos .tar (sem arquivos compactados como .targz, por exemplo)", "certificate_file_explanation": "Use um arquivo .pem que comece com \"-----BEGIN CERTIFICATE-----\" e termine com \"-----END CERTIFICATE-----\"", "invalid_cidr": "Endereço CIDR IPv4 inválido. Exemplo: 192.168.0.1/12", "invalid_email": "E-mail inválido", diff --git a/src/components/Buttons/FileInputButton/index.tsx b/src/components/Buttons/FileInputButton/index.tsx index a60934e..436354c 100644 --- a/src/components/Buttons/FileInputButton/index.tsx +++ b/src/components/Buttons/FileInputButton/index.tsx @@ -10,6 +10,7 @@ interface Props { accept: string; isHidden?: boolean; isStringFile?: boolean; + wantBase64?: boolean; } const defaultProps = { @@ -18,7 +19,16 @@ const defaultProps = { isStringFile: false, }; -const FileInputButton = ({ value, setValue, setFileName, refreshId, accept, isHidden, isStringFile }: Props) => { +const FileInputButton = ({ + value, + setValue, + setFileName, + refreshId, + accept, + isHidden, + isStringFile, + wantBase64, +}: Props) => { const [fileKey, setFileKey] = useState(uuid()); let fileReader: FileReader | undefined; @@ -31,11 +41,27 @@ const FileInputButton = ({ value, setValue, setFileName, refreshId, accept, isHi } }; + const handleBase64FileRead = () => { + if (fileReader) { + const content = fileReader.result; + if (content && typeof content === 'string') { + const split = content.split('base64,'); + if (split[1]) { + setValue(split[1] as string); + } + } + } + }; + const changeFile = (e: React.ChangeEvent) => { const file = e.target.files ? e.target.files[0] : undefined; if (file) { const newVal = URL.createObjectURL(file); - if (!isStringFile) { + if (wantBase64) { + fileReader = new FileReader(); + fileReader.onloadend = handleBase64FileRead; + fileReader.readAsDataURL(file); + } else if (!isStringFile) { setValue(newVal, file); if (setFileName) setFileName(file.name ?? ''); } else { diff --git a/src/components/FormFields/FileInputFieldModal/FileInputModal.jsx b/src/components/FormFields/FileInputFieldModal/FileInputModal.jsx index cc21759..d772adb 100644 --- a/src/components/FormFields/FileInputFieldModal/FileInputModal.jsx +++ b/src/components/FormFields/FileInputFieldModal/FileInputModal.jsx @@ -21,6 +21,7 @@ import PropTypes from 'prop-types'; import { useTranslation } from 'react-i18next'; import { v4 as uuid } from 'uuid'; import ConfigurationFieldExplanation from '../ConfigurationFieldExplanation'; +import DeleteButton from 'components/Buttons/DeleteButton'; import FileInputButton from 'components/Buttons/FileInputButton'; import SaveButton from 'components/Buttons/SaveButton'; import ModalHeader from 'components/Modals/ModalHeader'; @@ -39,6 +40,9 @@ const propTypes = { error: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), touched: PropTypes.bool, definitionKey: PropTypes.string, + canDelete: PropTypes.bool.isRequired, + onDelete: PropTypes.func.isRequired, + wantBase64: PropTypes.bool, }; const defaultProps = { @@ -51,6 +55,7 @@ const defaultProps = { error: false, touched: false, definitionKey: null, + wantBase64: false, }; const FileInputModal = ({ @@ -67,6 +72,9 @@ const FileInputModal = ({ isDisabled, isHidden, definitionKey, + canDelete, + onDelete, + wantBase64, }) => { const { t } = useTranslation(); const { isOpen, onOpen, onClose } = useDisclosure(); @@ -108,9 +116,10 @@ const FileInputModal = ({ onClick={onOpen} icon={} isDisabled={isDisabled} - ml={2} + mx={2} /> + {value !== undefined && canDelete && } {error} @@ -135,7 +144,8 @@ const FileInputModal = ({ setFileName={setTempFilename} refreshId={refreshId} accept={acceptedFileTypes} - isStringFile + isStringFile={!wantBase64} + wantBase64={wantBase64} /> diff --git a/src/components/FormFields/FileInputFieldModal/index.jsx b/src/components/FormFields/FileInputFieldModal/index.jsx index 6aaf464..0212b1b 100644 --- a/src/components/FormFields/FileInputFieldModal/index.jsx +++ b/src/components/FormFields/FileInputFieldModal/index.jsx @@ -14,6 +14,8 @@ const propTypes = { isRequired: PropTypes.bool, isHidden: PropTypes.bool, definitionKey: PropTypes.string, + canDelete: PropTypes.bool, + wantBase64: PropTypes.bool, }; const defaultProps = { @@ -22,6 +24,8 @@ const defaultProps = { isDisabled: false, isHidden: false, definitionKey: null, + canDelete: false, + wantBase64: false, }; const FileInputFieldModal = ({ @@ -35,10 +39,16 @@ const FileInputFieldModal = ({ isRequired, isHidden, definitionKey, + canDelete, + wantBase64, }) => { const [{ value }, { touched, error }, { setValue }] = useField(name); const [{ value: fileNameValue }, , { setValue: setFile }] = useField(fileName); + const onDelete = useCallback(() => { + setValue(undefined); + setFile(undefined); + }, []); const onChange = useCallback((newValue, newFilename) => { setValue(newValue); setFile(newFilename); @@ -59,6 +69,9 @@ const FileInputFieldModal = ({ isDisabled={isDisabled} isHidden={isHidden} definitionKey={definitionKey} + canDelete={canDelete} + onDelete={onDelete} + wantBase64={wantBase64} /> ); }; diff --git a/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/ServicesSection/Captive/index.tsx b/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/ServicesSection/Captive/index.tsx new file mode 100644 index 0000000..8dc49a8 --- /dev/null +++ b/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/ServicesSection/Captive/index.tsx @@ -0,0 +1,182 @@ +import * as React from 'react'; +import { Box, Heading, Select, SimpleGrid, Spacer, VStack } from '@chakra-ui/react'; +import { useTranslation } from 'react-i18next'; +import { object, string } from 'yup'; +import Card from 'components/Card'; +import CardBody from 'components/Card/CardBody'; +import CardHeader from 'components/Card/CardHeader'; +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'; +import useFastField from 'hooks/useFastField'; + +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); +}; +const namePrefix = 'configuration.captive'; + +const CaptiveConfiguration = ({ editing }: { editing: boolean }) => { + const { t } = useTranslation(); + const name = React.useCallback((suffix: string) => `${namePrefix}.${suffix}`, []); + const { value: captive, onChange } = useFastField({ + name: namePrefix, + }); + + const handleAuthModeChange = (e: React.ChangeEvent) => { + if (e.target.value === 'radius') { + onChange({ + 'walled-garden-fqdn': [], + '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 { + onChange({ 'walled-garden-fqdn': [], 'idle-timeout': 600, 'auth-mode': e.target.value }); + } + }; + + const fieldProps = (suffix: string) => ({ + name: name(suffix), + label: suffix, + definitionKey: `interface.ssid.pass-point.${suffix}`, + isDisabled: !editing, + }); + + const mode = captive?.['auth-mode'] as string | undefined; + + const credFields = React.useMemo( + () => ( + + + + + ), + [], + ); + const credCols = React.useMemo( + () => [ + { + id: 'username', + Header: 'username', + Footer: '', + accessor: 'username', + }, + { + id: 'password', + Header: 'password', + Footer: '', + accessor: 'password', + }, + ], + [], + ); + + return ( + + + + Captive + + + + + + + + { + // Basic Fields + } + + + true} + acceptedFileTypes=".tar" + isDisabled={!editing} + canDelete + isRequired + wantBase64 + /> + + + {mode === 'credentials' && ( + + )} + + {mode === 'uam' && ( + + + + + + + + + + )} + {(mode === 'radius' || mode === 'uam') && ( + + + + + + + + + + )} + + + ); +}; + +export default React.memo(CaptiveConfiguration); diff --git a/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/ServicesSection/index.jsx b/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/ServicesSection/index.jsx index 77be4dd..a791d30 100644 --- a/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/ServicesSection/index.jsx +++ b/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/ServicesSection/index.jsx @@ -9,6 +9,7 @@ import InternalFormAccess from '../common/InternalFormAccess'; import SectionGeneralCard from '../common/SectionGeneralCard'; import SubSectionPicker from '../common/SubSectionPicker'; import AirtimePolicies from './AirtimePolicies'; +import Captive from './Captive'; import DataPlane from './DataPlane'; import FacebookWifi from './FacebookWifi'; import Http from './Http'; @@ -121,6 +122,7 @@ const ServicesSection = ({ editing, setSection, sectionInformation, removeSub }) editing={editing} subsections={[ 'airtime-policies', + 'captive', 'data-plane', 'facebook-wifi', 'http', @@ -159,6 +161,7 @@ const ServicesSection = ({ editing, setSection, sectionInformation, removeSub }) {isSubSectionActive('data-plane') && } {isSubSectionActive('ieee8021x') && } {isSubSectionActive('radius-proxy') && } + {isSubSectionActive('captive') && } )} diff --git a/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/ServicesSection/servicesConstants.js b/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/ServicesSection/servicesConstants.js index b8c0686..195ffca 100644 --- a/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/ServicesSection/servicesConstants.js +++ b/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/ServicesSection/servicesConstants.js @@ -1,6 +1,125 @@ import { object, number, string, array, bool } from 'yup'; import { testFqdnHostname, testIpv4, testLength, testUcMac } from 'constants/formTests'; +export const SERVICES_CAPTIVE_SCHEMA = (t, useDefault = false) => { + const shape = object() + .shape({ + 'auto-mode': string().required(t('form.required')).default('click'), + 'walled-garden-fqdn': array().of(string()).min(1, t('form.required')).default([]), + '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: array() + .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: string().required(t('form.required')).default(''), + }) + .default(undefined), + 'auth-secret': string() + .when('auth-mode', { + is: (authMode) => authMode === 'radius' || authMode === 'uam', + then: string().required(t('form.required')).default(''), + else: string().default(undefined), + }) + .default(undefined), + 'auth-port': number() + .when('auth-mode', { + is: (authMode) => authMode === 'radius' || authMode === 'uam', + then: number().required(t('form.required')).moreThan(1023).lessThan(65535).integer().default(1812), + else: number().default(undefined), + }) + .default(undefined), + 'acct-server': string() + .when('auth-mode', { + is: (authMode) => authMode === 'radius' || authMode === 'uam', + then: string().default(undefined), + else: string().default(undefined), + }) + .default(undefined), + 'acct-secret': string() + .when('auth-mode', { + is: (authMode) => authMode === 'radius' || authMode === 'uam', + then: string().default(undefined), + else: string().default(undefined), + }) + .default(undefined), + 'acct-port': number() + .when('auth-mode', { + is: (authMode) => authMode === 'radius' || authMode === 'uam', + then: number().moreThan(1023).lessThan(65535).integer().default(undefined), + else: number().default(undefined), + }) + .default(undefined), + 'acct-interval': number() + .when('auth-mode', { + is: (authMode) => authMode === 'radius' || authMode === 'uam', + then: number().positive().lessThan(65535).integer().default(undefined), + }) + .default(undefined), + // Only UAM fields + 'uam-server': string() + .when('auth-mode', { + is: (authMode) => authMode === 'uam', + then: string().required(t('form.required')).default(''), + }) + .default(undefined), + 'uam-secret': string() + .when('auth-mode', { + is: (authMode) => authMode === 'uam', + then: string().required(t('form.required')).default(''), + }) + .default(undefined), + 'uam-port': number() + .when('auth-mode', { + is: (authMode) => authMode === 'uam', + then: number().required(t('form.required')).moreThan(1023).lessThan(65535).integer().default(3990), + }) + .default(undefined), + ssid: string() + .when('auth-mode', { + is: (authMode) => authMode === 'uam', + then: string().default(undefined), + }) + .default(undefined), + 'mac-format': string() + .when('auth-mode', { + is: (authMode) => authMode === 'uam', + then: string().required(t('form.required')).default('aabbccddeeff'), + }) + .default(undefined), + nasid: string() + .when('auth-mode', { + is: (authMode) => authMode === 'uam', + then: string().required(t('form.required')).default(''), + }) + .default(undefined), + nasmac: string() + .when('auth-mode', { + is: (authMode) => authMode === 'uam', + then: string().default(undefined), + }) + .default(undefined), + }) + .default({}); + + return useDefault ? shape : shape.nullable().default(undefined); +}; + export const SERVICES_CLASSIFIER_DNS_SCHEMA = (t, useDefault = false) => { const shape = object().shape({ fqdn: string().default(''), @@ -295,6 +414,7 @@ export const SERVICES_SCHEMA = (t, useDefault = false) => 'data-plane': SERVICES_DATA_PLANE_SCHEMA(t, useDefault), 'radius-proxy': SERVICES_RADIUS_PROXY_SCHEMA(t, useDefault), ieee8021x: SERVICES_IEEE8021X_SCHEMA(t, useDefault), + captive: SERVICES_CAPTIVE_SCHEMA(t, useDefault), }), }); @@ -334,6 +454,8 @@ export const getSubSectionDefaults = (t, sub) => { return SERVICES_IEEE8021X_SCHEMA(t, true).cast(); case 'radius-proxy': return SERVICES_RADIUS_PROXY_SCHEMA(t, true).cast(); + case 'captive': + return SERVICES_CAPTIVE_SCHEMA(t, true).cast(); default: return null; }