Merge pull request #137 from stephb9959/main

[WIFI-10916] Added icons field to passpoint config
This commit is contained in:
Charles Bourque
2022-10-11 10:23:28 +01:00
committed by GitHub
15 changed files with 327 additions and 9 deletions

4
package-lock.json generated
View File

@@ -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",

View File

@@ -1,6 +1,6 @@
{
"name": "wlan-cloud-owprov-ui",
"version": "2.7.0(20)",
"version": "2.7.1(1)",
"description": "",
"main": "index.tsx",
"scripts": {

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -87,7 +87,7 @@ const NotesTable = ({ name, isDisabled }) => {
</InputGroup>
<DataTable
columns={columns}
data={notes.sort((a, b) => b.created - a.created)}
data={notes?.sort((a, b) => b.created - a.created)}
obj={t('common.notes')}
minHeight="200px"
/>

View File

@@ -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<HTMLInputElement>) => {
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 (
<>
<Box mb={2}>
<FormControl hidden={props.isHidden} isDisabled={props.isDisabled} w="50%">
<FormLabel>{props.label ?? props.name}</FormLabel>
<Input borderRadius="15px" pt={1} fontSize="sm" type="file" onChange={changeFile} key={fileKey} />
</FormControl>
</Box>
<Box mb={2}>
{props.value && (
<Image
height={props.height !== undefined ? `${props.height}px` : 200}
width={props.width !== undefined ? `${props.width}px` : 200}
ml="auto"
mr="auto"
src={`data:${props.typeValue ?? 'image/png'};base64,${props.value}`}
alt="New Image"
/>
)}
</Box>
</>
);
};
export default React.memo(ImageFieldInput);

View File

@@ -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<string | undefined>({
name: props.name,
});
const imageType = useFastField<string | undefined>({
name: props.typeName,
});
const height = useFastField<number | undefined>({
name: props.heightName,
});
const width = useFastField<string | undefined>({
name: props.widthName,
});
const onChange = (value: string) => {
image.onChange(value);
};
const onTypeChange = (fileType: string) => {
if (props.typeName) {
imageType.onChange(fileType);
}
};
return (
<ImageFieldInput
{...props}
value={image.value}
onChange={onChange}
onTypeChange={onTypeChange}
typeValue={imageType.value}
height={height.value}
width={width.value}
/>
);
};
export default React.memo(ImageField);

View File

@@ -122,7 +122,7 @@ const _NotesField: React.FC<NotesFieldProps> = ({ name = 'notes', isDisabled, ha
</InputGroup>
<DataTable
columns={columns}
data={notes.sort((a: Note, b: Note) => b.created - a.created)}
data={notes?.sort((a: Note, b: Note) => b.created - a.created)}
obj={hasDeleteButton ? undefined : t('common.notes')}
minHeight="200px"
/>

View File

@@ -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) => (
<Image boxSize={100} mx="auto" my="auto" src={`data:${fileType ?? 'image/png'};base64,${src}`} alt="New Image" />
),
[],
);
const iconFields = React.useMemo(
() => (
<>
<SimpleGrid minChildWidth="180px" gap={4} mb={4}>
<NumberInputField name="width" label="width" w="140px" emptyIsUndefined isRequired unit="px" />
<NumberField name="height" label="height" w="140px" isRequired unit="px" />
<StringField name="language" label="language" w="100px" isRequired />
</SimpleGrid>
<ImageField name="icon" heightName="height" widthName="width" typeName="type" />
</>
),
[],
);
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
/>
<FastCreatableSelectInput {...fieldProps('connection-capability')} />
<DisplayObjectArrayField
{...fieldProps('icons')}
fields={iconFields}
columns={iconCols}
schema={INTERFACE_PASSPOINT_ICONS_SCHEMA}
isDisabled
emptyIsUndefined
isRequired
/>
</SimpleGrid>
)}
</>

View File

@@ -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<Props> = ({ isDisabled, namePrefix, isEnabled, onT
isDisabled,
});
const iconCell = React.useCallback(
(src: string, fileType: string) => (
<Image boxSize={100} mx="auto" my="auto" src={`data:${fileType ?? 'image/png'};base64,${src}`} alt="New Image" />
),
[],
);
const iconFields = React.useMemo(
() => (
<>
<SimpleGrid minChildWidth="180px" gap={4} mb={4}>
<NumberField name="width" label="width" w="140px" emptyIsUndefined isRequired unit="px" />
<NumberField name="height" label="height" w="140px" isRequired unit="px" />
<StringField name="language" label="language" isRequired />
</SimpleGrid>
<ImageField name="icon" heightName="height" widthName="width" typeName="type" />
</>
),
[],
);
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 (
<>
<Heading size="md" display="flex">
@@ -106,6 +180,14 @@ const PassPointForm: React.FC<Props> = ({ isDisabled, namePrefix, isEnabled, onT
emptyIsUndefined
/>
<CreatableSelectField {...fieldProps('connection-capability')} emptyIsUndefined placeholder="17:5060:0" />
<ObjectArrayFieldModal
{...fieldProps('icons')}
fields={iconFields}
// @ts-ignore
columns={iconCols}
schema={INTERFACE_PASSPOINT_ICONS_SCHEMA}
emptyIsUndefined
/>
</SimpleGrid>
)}
</>

View File

@@ -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({

View File

@@ -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`;
};