mirror of
https://github.com/Telecominfraproject/wlan-cloud-owprov-ui.git
synced 2025-10-30 18:17:46 +00:00
Merge pull request #137 from stephb9959/main
[WIFI-10916] Added icons field to passpoint config
This commit is contained in:
4
package-lock.json
generated
4
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wlan-cloud-owprov-ui",
|
||||
"version": "2.7.0(20)",
|
||||
"version": "2.7.1(1)",
|
||||
"description": "",
|
||||
"main": "index.tsx",
|
||||
"scripts": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
80
src/components/FormFields/ImageField/Input.tsx
Normal file
80
src/components/FormFields/ImageField/Input.tsx
Normal 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);
|
||||
56
src/components/FormFields/ImageField/index.tsx
Normal file
56
src/components/FormFields/ImageField/index.tsx
Normal 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);
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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`;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user