diff --git a/package-lock.json b/package-lock.json index bb84509..1cf077e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "wlan-cloud-owprov-ui", - "version": "2.7.0(20)", + "version": "2.7.1(1)", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "wlan-cloud-owprov-ui", - "version": "2.7.0(20)", + "version": "2.7.1(1)", "license": "ISC", "dependencies": { "@chakra-ui/icons": "^1.1.1", diff --git a/package.json b/package.json index 018d16c..53f3e80 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wlan-cloud-owprov-ui", - "version": "2.7.0(20)", + "version": "2.7.1(1)", "description": "", "main": "index.tsx", "scripts": { diff --git a/public/locales/de/translation.json b/public/locales/de/translation.json index e758aa3..363585b 100644 --- a/public/locales/de/translation.json +++ b/public/locales/de/translation.json @@ -510,6 +510,7 @@ "invalid_file_content": "Ungültiger Dateiinhalt, bitte bestätigen Sie, dass es sich um ein gültiges Format handelt", "invalid_fqdn_host": "Ungültiger FQDN-Hostname", "invalid_hostname": "Ungültiger Hostname: Er darf nur aus alphanumerischen Zeichen und Bindestrichen bestehen", + "invalid_icon_lang": "Ungültige Sprache, sollte aus 3 Buchstaben bestehen (eng, fre, ger, ita usw.)", "invalid_ieee": "Für dieses Verschlüsselungsprotokoll muss ieee80211w entweder „optional“ oder „erforderlich“ sein.", "invalid_interfaces": "Ungültige Schnittstellen-JSON-Zeichenfolge. Bitte bestätigen Sie, dass Ihr Wert gültiges JSON ist und Schnittstellen als einzigen Schlüssel hat und dass der Schnittstellenwert ein Array ist. Beispiel: {\"interfaces\": []}", "invalid_ipv4": "Ungültige IPv4-Adresse (Bsp.: 192.168.0.1 oder 192.168.0.1/16", diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index b87c6fb..0730308 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -510,6 +510,7 @@ "invalid_file_content": "Invalid file content, please confirm that it is of the valid format", "invalid_fqdn_host": "Invalid FQDN hostname", "invalid_hostname": "Invalid hostname: it needs to be composed of alphanumeric characters and dashes only", + "invalid_icon_lang": "Invalid language, it should be in a 3-letter format (eng, fre, ger, ita, etc.)", "invalid_ieee": "For this encryption protocol, ieee80211w needs to be either 'optional' or 'required'", "invalid_interfaces": "Invalid Interfaces JSON string. Please confirm that your value is: valid JSON and has interfaces as its only key and that the interfaces value is an array. Example: {\"interfaces\": []}", "invalid_ipv4": "Invalid IPv4 address (ex.: 192.168.0.1 or 192.168.0.1/16", diff --git a/public/locales/es/translation.json b/public/locales/es/translation.json index c95daab..990eb23 100644 --- a/public/locales/es/translation.json +++ b/public/locales/es/translation.json @@ -510,6 +510,7 @@ "invalid_file_content": "Contenido de archivo no válido, confirme que tiene un formato válido", "invalid_fqdn_host": "Nombre de host FQDN no válido", "invalid_hostname": "Nombre de host no válido: debe estar compuesto solo de caracteres alfanuméricos y guiones", + "invalid_icon_lang": "Idioma no válido, debe estar en un formato de 3 letras (eng, fre, ger, ita, etc.)", "invalid_ieee": "Para este protocolo de encriptación, ieee80211w debe ser 'opcional' u 'requerido'", "invalid_interfaces": "Cadena JSON de interfaces no válida. Confirme que su valor es: JSON válido y tiene interfaces como su única clave y que el valor de las interfaces es una matriz. Ejemplo: {\"interfaces\": []}", "invalid_ipv4": "Dirección IPv4 no válida (ej.: 192.168.0.1 o 192.168.0.1/16", diff --git a/public/locales/fr/translation.json b/public/locales/fr/translation.json index 90d0734..73031eb 100644 --- a/public/locales/fr/translation.json +++ b/public/locales/fr/translation.json @@ -510,6 +510,7 @@ "invalid_file_content": "Contenu de fichier non valide, veuillez confirmer qu'il est au format valide", "invalid_fqdn_host": "Nom d'hôte FQDN non valide", "invalid_hostname": "Nom d'hôte non valide : il doit être composé uniquement de caractères alphanumériques et de tirets", + "invalid_icon_lang": "Langue non valide, elle doit être dans un format à 3 lettres (eng, fre, ger, ita, etc.)", "invalid_ieee": "Pour ce protocole de cryptage, ieee80211w doit être soit 'facultatif' soit 'obligatoire'", "invalid_interfaces": "Chaîne JSON d'interfaces non valide. Veuillez confirmer que votre valeur est : JSON valide et a des interfaces comme seule clé et que la valeur des interfaces est un tableau. Exemple : {\"interfaces\": []}", "invalid_ipv4": "Adresse IPv4 invalide (ex. : 192.168.0.1 ou 192.168.0.1/16", diff --git a/public/locales/pt/translation.json b/public/locales/pt/translation.json index 38280f4..ff0c07a 100644 --- a/public/locales/pt/translation.json +++ b/public/locales/pt/translation.json @@ -510,6 +510,7 @@ "invalid_file_content": "Conteúdo de arquivo inválido. Confirme se está no formato válido", "invalid_fqdn_host": "Nome de host FQDN inválido", "invalid_hostname": "Nome de host inválido: precisa ser composto apenas de caracteres alfanuméricos e traços", + "invalid_icon_lang": "Idioma inválido, deve estar em formato de 3 letras (eng, fre, ger, ita, etc.)", "invalid_ieee": "Para este protocolo de criptografia, ieee80211w precisa ser 'opcional' ou 'obrigatório'", "invalid_interfaces": "Sequência JSON de interfaces inválida. Confirme se seu valor é: JSON válido e tem interfaces como sua única chave e que o valor de interfaces é uma matriz. Exemplo: {\"interfaces\": []}", "invalid_ipv4": "Endereço IPv4 inválido (ex.: 192.168.0.1 ou 192.168.0.1/16", diff --git a/src/components/CustomFields/NotesTable/index.js b/src/components/CustomFields/NotesTable/index.js index 97ffea7..e5f247b 100644 --- a/src/components/CustomFields/NotesTable/index.js +++ b/src/components/CustomFields/NotesTable/index.js @@ -87,7 +87,7 @@ const NotesTable = ({ name, isDisabled }) => { b.created - a.created)} + data={notes?.sort((a, b) => b.created - a.created)} obj={t('common.notes')} minHeight="200px" /> diff --git a/src/components/FormFields/ImageField/Input.tsx b/src/components/FormFields/ImageField/Input.tsx new file mode 100644 index 0000000..11282eb --- /dev/null +++ b/src/components/FormFields/ImageField/Input.tsx @@ -0,0 +1,80 @@ +/* eslint-disable react/no-unused-prop-types */ +/* eslint-disable react/destructuring-assignment */ +import * as React from 'react'; +import { Box, FormControl, FormLabel, Image, Input } from '@chakra-ui/react'; +import { v4 as uuid } from 'uuid'; + +type Props = { + name: string; + height?: number; + width?: number; + typeName: string; + typeValue: string; + label?: string; + isDisabled?: boolean; + displayImage?: boolean; + isRequired?: boolean; + emptyIsUndefined?: boolean; + isHidden?: boolean; + definitionKey?: string; + hideLabel?: boolean; + value?: string; + onChange: (value: string) => void; + onTypeChange: (fileType: string) => void; +}; +const ImageFieldInput = (props: Props) => { + const [fileKey, setFileKey] = React.useState(uuid()); + + let fileReader: FileReader | undefined; + + const handleStringFileRead = () => { + if (fileReader) { + const content = fileReader.result; + if (content && typeof content === 'string') { + const split = content.split('base64,'); + if (split[1]) { + props.onChange(split[1] as string); + } + } + } + }; + + const changeFile = (e: React.ChangeEvent) => { + const file = e.target.files ? e.target.files[0] : undefined; + if (file) { + props.onTypeChange(file.type); + fileReader = new FileReader(); + fileReader.onloadend = handleStringFileRead; + fileReader.readAsDataURL(file); + } + }; + + React.useEffect(() => { + if (props.value === '') setFileKey(uuid()); + }, [props.value]); + + return ( + <> + + + + + {props.value && ( + New Image + )} + + + ); +}; + +export default React.memo(ImageFieldInput); diff --git a/src/components/FormFields/ImageField/index.tsx b/src/components/FormFields/ImageField/index.tsx new file mode 100644 index 0000000..8d3a40d --- /dev/null +++ b/src/components/FormFields/ImageField/index.tsx @@ -0,0 +1,56 @@ +/* eslint-disable react/destructuring-assignment */ +import useFastField from 'hooks/useFastField'; +import * as React from 'react'; +import ImageFieldInput from './Input'; + +type Props = { + name: string; + typeName: string; + heightName: string; + widthName: string; + label?: string; + isDisabled?: boolean; + displayImage?: boolean; + isRequired?: boolean; + emptyIsUndefined?: boolean; + isHidden?: boolean; + definitionKey?: string; + hideLabel?: boolean; +}; +const ImageField = (props: Props) => { + const image = useFastField({ + name: props.name, + }); + const imageType = useFastField({ + name: props.typeName, + }); + const height = useFastField({ + name: props.heightName, + }); + const width = useFastField({ + name: props.widthName, + }); + + const onChange = (value: string) => { + image.onChange(value); + }; + const onTypeChange = (fileType: string) => { + if (props.typeName) { + imageType.onChange(fileType); + } + }; + + return ( + + ); +}; + +export default React.memo(ImageField); diff --git a/src/components/FormFields/NotesField/index.tsx b/src/components/FormFields/NotesField/index.tsx index 39eeded..443326d 100644 --- a/src/components/FormFields/NotesField/index.tsx +++ b/src/components/FormFields/NotesField/index.tsx @@ -122,7 +122,7 @@ const _NotesField: React.FC = ({ name = 'notes', isDisabled, ha b.created - a.created)} + data={notes?.sort((a: Note, b: Note) => b.created - a.created)} obj={hasDeleteButton ? undefined : t('common.notes')} minHeight="200px" /> diff --git a/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/SingleInterface/SsidList/LockedPasspoint.jsx b/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/SingleInterface/SsidList/LockedPasspoint.jsx index 948802c..c406f7a 100644 --- a/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/SingleInterface/SsidList/LockedPasspoint.jsx +++ b/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/SingleInterface/SsidList/LockedPasspoint.jsx @@ -1,11 +1,16 @@ import React from 'react'; -import { Flex, Heading, SimpleGrid } from '@chakra-ui/react'; +import { Flex, Heading, Image, NumberInputField, SimpleGrid } from '@chakra-ui/react'; import PropTypes from 'prop-types'; import DisplayNumberField from 'components/DisplayFields/DisplayNumberField'; +import DisplayObjectArrayField from 'components/DisplayFields/DisplayObjectArrayField'; import DisplaySelectField from 'components/DisplayFields/DisplaySelectField'; import DisplayStringField from 'components/DisplayFields/DisplayStringField'; import DisplayToggleField from 'components/DisplayFields/DisplayToggleField'; import FastCreatableSelectInput from 'components/FormFields/CreatableSelectField/FastCreatableSelectInput'; +import ImageField from 'components/FormFields/ImageField'; +import StringField from 'components/FormFields/StringField'; +import NumberField from 'components/FormFields/NumberField'; +import { INTERFACE_PASSPOINT_ICONS_SCHEMA } from '../../interfacesConstants'; const propTypes = { data: PropTypes.instanceOf(Object).isRequired, @@ -20,6 +25,66 @@ const LockedPasspoint = ({ data }) => { isDisabled: true, }); + const iconCell = React.useCallback( + (src, fileType) => ( + New Image + ), + [], + ); + const iconFields = React.useMemo( + () => ( + <> + + + + + + + + ), + [], + ); + const iconCols = React.useMemo( + () => [ + { + id: 'icon', + Header: 'icon', + Footer: '', + Cell: ({ cell }) => iconCell(cell.row.original.icon, cell.row.original.type), + accessor: 'icon', + }, + { + id: 'type', + Header: 'type', + Footer: '', + accessor: 'type', + customWidth: '100px', + }, + { + id: 'width', + Header: 'width', + Footer: '', + accessor: 'width', + customWidth: '150px', + }, + { + id: 'height', + Header: 'height', + Footer: '', + accessor: 'height', + customWidth: '100px', + }, + { + id: 'language', + Header: 'language', + Footer: '', + accessor: 'language', + customWidth: '100px', + }, + ], + [], + ); + if (!data) return null; return ( @@ -89,6 +154,15 @@ const LockedPasspoint = ({ data }) => { emptyIsUndefined /> + )} diff --git a/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/SingleInterface/SsidList/PassPoint/Form.tsx b/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/SingleInterface/SsidList/PassPoint/Form.tsx index 36ced20..87026b5 100644 --- a/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/SingleInterface/SsidList/PassPoint/Form.tsx +++ b/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/SingleInterface/SsidList/PassPoint/Form.tsx @@ -1,10 +1,13 @@ import React from 'react'; -import { Heading, SimpleGrid, Switch, Text } from '@chakra-ui/react'; +import { Heading, Image, SimpleGrid, Switch, Text } from '@chakra-ui/react'; import ToggleField from 'components/FormFields/ToggleField'; import CreatableSelectField from 'components/FormFields/CreatableSelectField'; import NumberField from 'components/FormFields/NumberField'; import SelectField from 'components/FormFields/SelectField'; import StringField from 'components/FormFields/StringField'; +import ObjectArrayFieldModal from 'components/FormFields/ObjectArrayFieldModal'; +import ImageField from 'components/FormFields/ImageField'; +import { INTERFACE_PASSPOINT_ICONS_SCHEMA } from '../../../interfacesConstants'; interface Props { isDisabled?: boolean; @@ -23,6 +26,77 @@ const PassPointForm: React.FC = ({ isDisabled, namePrefix, isEnabled, onT isDisabled, }); + const iconCell = React.useCallback( + (src: string, fileType: string) => ( + New Image + ), + [], + ); + const iconFields = React.useMemo( + () => ( + <> + + + + + + + + ), + [], + ); + const iconCols = React.useMemo( + () => [ + { + id: 'icon', + Header: 'icon', + Footer: '', + Cell: ({ + cell, + }: { + cell: { + row: { + original: { + icon: string; + type: string; + }; + }; + }; + }) => iconCell(cell.row.original.icon, cell.row.original.type), + accessor: 'icon', + }, + { + id: 'type', + Header: 'type', + Footer: '', + accessor: 'type', + customWidth: '100px', + }, + { + id: 'width', + Header: 'width', + Footer: '', + accessor: 'width', + customWidth: '150px', + }, + { + id: 'height', + Header: 'height', + Footer: '', + accessor: 'height', + customWidth: '100px', + }, + { + id: 'language', + Header: 'language', + Footer: '', + accessor: 'language', + customWidth: '100px', + }, + ], + [], + ); + return ( <> @@ -106,6 +180,14 @@ const PassPointForm: React.FC = ({ isDisabled, namePrefix, isEnabled, onT emptyIsUndefined /> + )} diff --git a/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/interfacesConstants.js b/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/interfacesConstants.js index 55ff76b..e5bace1 100644 --- a/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/interfacesConstants.js +++ b/src/pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/interfacesConstants.js @@ -5,6 +5,7 @@ import { testIpv6, testLeaseTime, testLength, + testRegex, testSelectPorts, testUcMac, } from 'constants/formTests'; @@ -74,6 +75,26 @@ export const CREATE_INTERFACE_SCHEMA = (t) => role: string().required(t('form.required')).default('upstream'), }); +export const INTERFACE_PASSPOINT_ICONS_SCHEMA = (t, useDefault = false) => { + const shape = object() + .shape({ + width: number().required(t('form.required')).moreThan(-1).lessThan(65535).integer().default(64), + height: number().required(t('form.required')).moreThan(-1).lessThan(65535).integer().default(64), + icon: string().required(t('form.required')).default(''), + language: string() + .required(t('form.required')) + .test('test-passpoint-icon-lang', t('form.invalid_icon_lang'), (v) => testRegex(v, '^[a-z][a-z][a-z]$')) + .default('eng'), + }) + .default({ + width: 64, + height: 64, + language: 'eng', + }); + + return useDefault ? shape : shape.nullable().default(undefined); +}; + export const INTERFACE_SSID_PASS_POINT_SCHEMA = (t, useDefault = false) => { const shape = object() .shape({ diff --git a/src/pages/ConfigurationPage/ConfigurationCard/Form.js b/src/pages/ConfigurationPage/ConfigurationCard/Form.js index 81b13f4..0945dd1 100644 --- a/src/pages/ConfigurationPage/ConfigurationCard/Form.js +++ b/src/pages/ConfigurationPage/ConfigurationCard/Form.js @@ -49,8 +49,8 @@ const EditConfigurationForm = ({ editing, configuration, formRef }) => { }); const getEntity = () => { - if (configuration.entity !== '') return `ent:${configuration.entity}`; - if (configuration.venue !== '') return `ven:${configuration.venue}`; + if (configuration?.entity !== '') return `ent:${configuration?.entity}`; + if (configuration?.venue !== '') return `ven:${configuration?.venue}`; return `ent:0000-0000-0000`; };