Version 1.0.26

This commit is contained in:
Charles
2021-11-10 09:36:12 -05:00
parent fc082dcc84
commit 0ad902376b
37 changed files with 2212 additions and 386 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "ucentral-libs",
"version": "1.0.1",
"version": "1.0.26",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "ucentral-libs",
"version": "1.0.1",
"version": "1.0.26",
"license": "BSD-3-Clause",
"dependencies": {
"@coreui/coreui": "^3.4.0",

View File

@@ -1,6 +1,6 @@
{
"name": "ucentral-libs",
"version": "1.0.1",
"version": "1.0.26",
"main": "dist/index.js",
"source": "src/index.js",
"engines": {

View File

@@ -256,7 +256,7 @@ const AddContactForm = ({ t, disable, fields, updateField, updateFieldWithKey, e
<CInvalidFeedback>{t('common.required')}</CInvalidFeedback>
</CCol>
<CLabel className="mb-2" sm="2" col htmlFor="phones">
Landlines
{t('location.phones')}
</CLabel>
<CCol sm="4">
<CreatableSelect
@@ -267,11 +267,11 @@ const AddContactForm = ({ t, disable, fields, updateField, updateFieldWithKey, e
components={{ NoOptionsMessage }}
options={[]}
value={fields.phones.value.map((opt) => ({ value: opt, label: opt }))}
placeholder={t('common.type_for_options')}
placeholder="+1(202)555-0103"
/>
</CCol>
<CLabel className="mb-2" sm="2" col htmlFor="phones">
Mobiles
{t('location.mobiles')}
</CLabel>
<CCol sm="4">
<CreatableSelect
@@ -282,7 +282,7 @@ const AddContactForm = ({ t, disable, fields, updateField, updateFieldWithKey, e
components={{ NoOptionsMessage }}
options={[]}
value={fields.mobiles.value.map((opt) => ({ value: opt, label: opt }))}
placeholder={t('common.type_for_options')}
placeholder="+1(202)555-0103"
/>
</CCol>
<CLabel sm="2" col htmlFor="description">

View File

@@ -130,7 +130,7 @@ const AddLocationForm = ({
</div>
</CCol>
<CLabel className="mb-2" sm="2" col htmlFor="phones">
Landlines
{t('location.phones')}
</CLabel>
<CCol sm="4">
<CreatableSelect
@@ -141,11 +141,11 @@ const AddLocationForm = ({
components={{ NoOptionsMessage }}
options={[]}
value={fields.phones.value.map((opt) => ({ value: opt, label: opt }))}
placeholder={t('common.type_for_options')}
placeholder="+1(202)555-0103"
/>
</CCol>
<CLabel className="mb-2" sm="2" col htmlFor="phones">
Mobiles
{t('location.mobiles')}
</CLabel>
<CCol sm="4">
<CreatableSelect
@@ -156,7 +156,7 @@ const AddLocationForm = ({
components={{ NoOptionsMessage }}
options={[]}
value={fields.mobiles.value.map((opt) => ({ value: opt, label: opt }))}
placeholder={t('common.type_for_options')}
placeholder="+1(202)555-0103"
/>
</CCol>
<CCol className="mb-3" sm="12">

View File

@@ -0,0 +1,542 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import {
CForm,
CInput,
CLabel,
CCol,
CFormText,
CRow,
CInputFile,
CInvalidFeedback,
CSelect,
} from '@coreui/react';
import RequiredAsterisk from '../RequiredAsterisk';
const validatePem = (value) =>
(value.includes('---BEGIN CERTIFICATE---') && value.includes('---END CERTIFICATE---')) ||
(value.includes('---BEGIN PRIVATE KEY---') && value.includes('---END PRIVATE KEY---'));
const AddSimulationForm = ({ t, show, disable, fields, updateField, updateFieldWithKey }) => {
const [certKey, setCertKey] = useState(0);
const [keyKey, setKeyKey] = useState(0);
let fileReader;
const handleCertFileRead = () => {
updateFieldWithKey('certificate', { error: false });
const content = fileReader.result;
if (content && validatePem(content)) {
updateFieldWithKey('certificate', { value: content });
} else {
updateFieldWithKey('certificate', { error: true });
}
};
const handleCertFile = (file) => {
fileReader = new FileReader();
fileReader.onloadend = handleCertFileRead;
fileReader.readAsText(file);
};
const handleKeyFileRead = () => {
updateFieldWithKey('key', { error: false });
const content = fileReader.result;
if (content && validatePem(content)) {
updateFieldWithKey('key', { value: content });
} else {
updateFieldWithKey('key', { error: true });
}
};
const handleKeyFile = (file) => {
fileReader = new FileReader();
fileReader.onloadend = handleKeyFileRead;
fileReader.readAsText(file);
};
useEffect(() => {
if (show) {
setCertKey(certKey + 1);
setKeyKey(keyKey + 1);
}
}, [show]);
return (
<CForm>
<CRow>
<CLabel className="mb-2" sm="2" col htmlFor="name">
{t('user.name')}
<RequiredAsterisk />
</CLabel>
<CCol sm="4">
<CInput
id="name"
type="text"
required
value={fields.name.value}
onChange={updateField}
invalid={fields.name.error}
disabled={disable}
maxLength="50"
/>
<CFormText hidden={!fields.name.error} color={fields.name.error ? 'danger' : ''}>
{t('common.required')}
</CFormText>
</CCol>
<CLabel className="mb-2" sm="2" col htmlFor="gateway">
{t('simulation.gateway')}
<RequiredAsterisk />
</CLabel>
<CCol sm="4">
<CInput
id="gateway"
type="text"
required
value={fields.gateway.value}
onChange={updateField}
invalid={fields.gateway.error}
disabled={disable}
maxLength="50"
/>
<CFormText hidden={!fields.gateway.error} color={fields.gateway.error ? 'danger' : ''}>
{t('common.required')}
</CFormText>
</CCol>
<CLabel className="mb-2" sm="2" col htmlFor="certificate">
{t('common.certificate')}
<RequiredAsterisk />
</CLabel>
<CCol sm="4">
<CInputFile
className="mt-1"
key={certKey}
id="file-input"
name="file-input"
accept=".pem"
onChange={(e) => handleCertFile(e.target.files[0])}
/>
<CFormText
hidden={!fields.certificate.error}
color={fields.certificate.error ? 'danger' : ''}
>
{t('common.required')}
</CFormText>
</CCol>
<CLabel className="mb-2" sm="2" col htmlFor="key">
{t('common.key')}
<RequiredAsterisk />
</CLabel>
<CCol sm="4">
<CInputFile
className="mt-1"
key={keyKey}
id="file-input"
name="file-input"
accept=".pem"
onChange={(e) => handleKeyFile(e.target.files[0])}
/>
<CFormText hidden={!fields.key.error} color={fields.key.error ? 'danger' : ''}>
{t('common.required')}
</CFormText>
</CCol>
<CLabel className="mb-2" sm="2" col htmlFor="macPrefix">
{t('simulation.mac_prefix')}
<RequiredAsterisk />
</CLabel>
<CCol sm="4">
<CInput
id="macPrefix"
type="text"
required
value={fields.macPrefix.value}
onChange={updateField}
invalid={fields.macPrefix.error}
disabled={disable}
maxLength="50"
/>
<CFormText
hidden={!fields.macPrefix.error}
color={fields.macPrefix.error ? 'danger' : ''}
>
{t('simulation.prefix_length')}
</CFormText>
</CCol>
<CLabel className="mb-2" sm="2" col htmlFor="devices">
{t('common.devices')}
<RequiredAsterisk />
</CLabel>
<CCol sm="4">
<CInput
id="devices"
type="number"
required
value={fields.devices.value}
onChange={updateField}
invalid={
fields.devices.value < fields.devices.min || fields.devices.value > fields.devices.max
}
disabled={disable}
pattern="[0-9]*"
style={{ width: '100px' }}
/>
<CInvalidFeedback>
{t('common.min_max', {
min: fields.devices.min,
max: fields.devices.max,
})}
</CInvalidFeedback>
</CCol>
<CLabel className="mb-2" sm="2" col htmlFor="healthCheckInterval">
{t('simulation.healtcheck_interval')}
<RequiredAsterisk />
</CLabel>
<CCol sm="4">
<CInput
id="healthCheckInterval"
type="number"
required
value={fields.healthCheckInterval.value}
onChange={updateField}
invalid={
fields.healthCheckInterval.value < fields.healthCheckInterval.min ||
fields.healthCheckInterval.value > fields.healthCheckInterval.max
}
disabled={disable}
pattern="[0-9]*"
style={{ width: '100px' }}
/>
<CInvalidFeedback>
{t('common.min_max', {
min: fields.healthCheckInterval.min,
max: fields.healthCheckInterval.max,
})}
</CInvalidFeedback>
</CCol>
<CLabel className="mb-2" sm="2" col htmlFor="stateInterval">
{t('simulation.state_interval')}
<RequiredAsterisk />
</CLabel>
<CCol sm="4">
<CInput
id="stateInterval"
type="number"
required
value={fields.stateInterval.value}
onChange={updateField}
invalid={
fields.stateInterval.value < fields.stateInterval.min ||
fields.stateInterval.value > fields.stateInterval.max
}
disabled={disable}
pattern="[0-9]*"
style={{ width: '100px' }}
/>
<CInvalidFeedback>
{t('common.min_max', {
min: fields.stateInterval.min,
max: fields.stateInterval.max,
})}
</CInvalidFeedback>
</CCol>
<CLabel className="mb-2" sm="2" col htmlFor="clientInterval">
{t('simulation.client_interval')}
<RequiredAsterisk />
</CLabel>
<CCol sm="4">
<CInput
id="clientInterval"
type="number"
required
value={fields.clientInterval.value}
onChange={updateField}
invalid={
fields.clientInterval.value < fields.clientInterval.min ||
fields.clientInterval.value > fields.clientInterval.max
}
disabled={disable}
pattern="[0-9]*"
style={{ width: '100px' }}
/>
<CInvalidFeedback>
{t('common.min_max', {
min: fields.clientInterval.min,
max: fields.clientInterval.max,
})}
</CInvalidFeedback>
</CCol>
<CLabel className="mb-2" sm="2" col htmlFor="reconnectInterval">
{t('simulation.reconnect_interval')}
<RequiredAsterisk />
</CLabel>
<CCol sm="4">
<CInput
id="reconnectInterval"
type="number"
required
value={fields.reconnectInterval.value}
onChange={updateField}
invalid={
fields.reconnectInterval.value < fields.reconnectInterval.min ||
fields.reconnectInterval.value > fields.reconnectInterval.max
}
disabled={disable}
pattern="[0-9]*"
style={{ width: '100px' }}
/>
<CInvalidFeedback>
{t('common.min_max', {
min: fields.reconnectInterval.min,
max: fields.reconnectInterval.max,
})}
</CInvalidFeedback>
</CCol>
<CLabel className="mb-2" sm="2" col htmlFor="minAssociations">
{t('simulation.min_associations')}
<RequiredAsterisk />
</CLabel>
<CCol sm="4">
<CInput
id="minAssociations"
type="number"
required
value={fields.minAssociations.value}
onChange={updateField}
invalid={
fields.minAssociations.value < fields.minAssociations.min ||
fields.minAssociations.value > fields.minAssociations.max
}
disabled={disable}
pattern="[0-9]*"
style={{ width: '100px' }}
/>
<CInvalidFeedback>
{t('common.min_max', {
min: fields.minAssociations.min,
max: fields.minAssociations.max,
})}
</CInvalidFeedback>
</CCol>
<CLabel className="mb-2" sm="2" col htmlFor="maxAssociations">
{t('simulation.max_associations')}
<RequiredAsterisk />
</CLabel>
<CCol sm="4">
<CInput
id="maxAssociations"
type="number"
required
value={fields.maxAssociations.value}
onChange={updateField}
invalid={
fields.maxAssociations.value < fields.maxAssociations.min ||
fields.maxAssociations.value > fields.maxAssociations.max
}
disabled={disable}
pattern="[0-9]*"
style={{ width: '100px' }}
/>
<CInvalidFeedback>
{t('common.min_max', {
min: fields.maxAssociations.min,
max: fields.maxAssociations.max,
})}
</CInvalidFeedback>
</CCol>
<CLabel className="mb-2" sm="2" col htmlFor="minClients">
{t('simulation.min_clients')}
<RequiredAsterisk />
</CLabel>
<CCol sm="4">
<CInput
id="minClients"
type="number"
required
value={fields.minClients.value}
onChange={updateField}
invalid={
fields.minClients.value < fields.minClients.min ||
fields.minClients.value > fields.minClients.max
}
disabled={disable}
pattern="[0-9]*"
style={{ width: '100px' }}
/>
<CInvalidFeedback>
{t('common.min_max', {
min: fields.minClients.min,
max: fields.minClients.max,
})}
</CInvalidFeedback>
</CCol>
<CLabel className="mb-2" sm="2" col htmlFor="maxClients">
{t('simulation.max_clients')}
<RequiredAsterisk />
</CLabel>
<CCol sm="4">
<CInput
id="maxClients"
type="number"
required
value={fields.maxClients.value}
onChange={updateField}
invalid={
fields.maxClients.value < fields.maxClients.min ||
fields.maxClients.value > fields.maxClients.max
}
disabled={disable}
pattern="[0-9]*"
style={{ width: '100px' }}
/>
<CInvalidFeedback>
{t('common.min_max', {
min: fields.maxClients.min,
max: fields.maxClients.max,
})}
</CInvalidFeedback>
</CCol>
<CLabel className="mb-2" sm="2" col htmlFor="simulationLength">
{t('simulation.length')}
<RequiredAsterisk />
</CLabel>
<CCol sm="4">
<CInput
id="simulationLength"
type="number"
required
value={fields.simulationLength.value}
onChange={updateField}
invalid={
fields.simulationLength.value < fields.simulationLength.min ||
fields.simulationLength.value > fields.simulationLength.max
}
disabled={disable}
pattern="[0-9]*"
style={{ width: '100px' }}
/>
<CInvalidFeedback>
{t('common.min_max', {
min: fields.simulationLength.min,
max: fields.simulationLength.max,
})}
</CInvalidFeedback>
</CCol>
<CLabel className="mb-2" sm="2" col htmlFor="threads">
{t('simulation.threads')}
<RequiredAsterisk />
</CLabel>
<CCol sm="4">
<CInput
id="threads"
type="number"
required
value={fields.threads.value}
onChange={updateField}
invalid={
fields.threads.value < fields.threads.min || fields.threads.value > fields.threads.max
}
disabled={disable}
pattern="[0-9]*"
style={{ width: '100px' }}
/>
<CInvalidFeedback>
{t('common.min_max', {
min: fields.threads.min,
max: fields.threads.max,
})}
</CInvalidFeedback>
</CCol>
<CLabel className="mb-2" sm="2" col htmlFor="keepAlive">
{t('simulation.keep_alive')}
<RequiredAsterisk />
</CLabel>
<CCol sm="4">
<CInput
id="keepAlive"
type="number"
required
value={fields.keepAlive.value}
onChange={updateField}
invalid={
fields.keepAlive.value < fields.keepAlive.min ||
fields.keepAlive.value > fields.keepAlive.max
}
disabled={disable}
pattern="[0-9]*"
style={{ width: '100px' }}
/>
<CInvalidFeedback>
{t('common.min_max', {
min: fields.keepAlive.min,
max: fields.keepAlive.max,
})}
</CInvalidFeedback>
</CCol>
<CLabel className="mb-2" sm="2" col htmlFor="deviceType">
{t('configuration.device_type')}
<RequiredAsterisk />
</CLabel>
<CCol sm="4">
<CSelect
custom
id="deviceType"
type="text"
required
value={fields.deviceType.value}
onChange={updateField}
invalid={fields.deviceType.error}
disabled={disable}
maxLength="50"
>
<option value="cig_wf160d">cig_wf160d</option>
<option value="cig_wf188">cig_wf188</option>
<option value="cig_wf188n">cig_wf188n</option>
<option value="cig_wf194c">cig_wf194c</option>
<option value="cig_wf194c4">cig_wf194c4</option>
<option value="edgecore_eap101">edgecore_eap101</option>
<option value="edgecore_eap102">edgecore_eap102</option>
<option value="edgecore_ecs4100-12ph">edgecore_ecs4100-12ph</option>
<option value="edgecore_ecw5211">edgecore_ecw5211</option>
<option value="edgecore_ecw5410">edgecore_ecw5410</option>
<option value="edgecore_oap100">edgecore_oap100</option>
<option value="edgecore_spw2ac1200">edgecore_spw2ac1200</option>
<option value="edgecore_spw2ac1200-lan-poe">edgecore_spw2ac1200-lan-poe</option>
<option value="edgecore_ssw2ac2600">edgecore_ssw2ac2600</option>
<option value="hfcl_ion4.yml">hfcl_ion4.yml</option>
<option value="indio_um-305ac">indio_um-305ac</option>
<option value="linksys_e8450-ubi">linksys_e8450-ubi</option>
<option value="linksys_ea6350">linksys_ea6350</option>
<option value="linksys_ea6350-v4">linksys_ea6350-v4</option>
<option value="linksys_ea8300">linksys_ea8300</option>
<option value="mikrotik_nand">mikrotik_nand</option>
<option value="tp-link_ec420-g1">tp-link_ec420-g1</option>
<option value="tplink_cpe210_v3">tplink_cpe210_v3</option>
<option value="tplink_cpe510_v3">tplink_cpe510_v3</option>
<option value="tplink_eap225_outdoor_v1">tplink_eap225_outdoor_v1</option>
<option value="tplink_ec420">tplink_ec420</option>
<option value="tplink_ex227">tplink_ex227</option>
<option value="tplink_ex228">tplink_ex228</option>
<option value="tplink_ex447">tplink_ex447</option>
<option value="wallys_dr40x9">wallys_dr40x9</option>
</CSelect>
<CFormText
hidden={!fields.deviceType.error}
color={fields.deviceType.error ? 'danger' : ''}
>
{t('common.required')}
</CFormText>
</CCol>
</CRow>
</CForm>
);
};
AddSimulationForm.propTypes = {
t: PropTypes.func.isRequired,
show: PropTypes.bool.isRequired,
disable: PropTypes.bool.isRequired,
fields: PropTypes.instanceOf(Object).isRequired,
updateField: PropTypes.func.isRequired,
updateFieldWithKey: PropTypes.func.isRequired,
};
export default AddSimulationForm;

View File

@@ -6,7 +6,7 @@ import _ from 'lodash';
import formatGoogleAddress from 'utils/formatGoogleAddress';
import selectStyles from 'utils/selectStyles';
const AddressEditor = ({ t, currentToken, endpoint, setAddress, show }) => {
const AddressEditor = ({ t, currentToken, endpoint, setAddress, show, disabled }) => {
const [tempValue, setTempValue] = useState('');
const [socket, setSocket] = useState(null);
const [results, setResults] = useState([]);
@@ -25,7 +25,7 @@ const AddressEditor = ({ t, currentToken, endpoint, setAddress, show }) => {
const onChange = useCallback(
(v) => {
setWaitingSearch(v);
if (v.length >= 4) setWaitingSearch(v);
},
[setWaitingSearch],
);
@@ -115,6 +115,7 @@ const AddressEditor = ({ t, currentToken, endpoint, setAddress, show }) => {
placeholder={t('location.search')}
onInputChange={handleTyping}
onChange={(property) => changeAddress(property)}
isDisabled={disabled}
/>
);
};
@@ -125,6 +126,11 @@ AddressEditor.propTypes = {
endpoint: PropTypes.string.isRequired,
setAddress: PropTypes.func.isRequired,
show: PropTypes.bool.isRequired,
disabled: PropTypes.bool,
};
AddressEditor.defaultProps = {
disabled: false,
};
export default AddressEditor;

View File

@@ -32,6 +32,7 @@ const CustomMultiModal = ({
noTable,
toggleAdd,
reset,
disabled,
}) => {
const [show, toggle] = useToggle();
@@ -82,12 +83,19 @@ const CustomMultiModal = ({
variant="outline"
className="ml-2"
onClick={toggleAdd}
disabled={disabled}
>
<CIcon content={cilPlus} />
</CButton>
</CPopover>
<CPopover content={t('common.save')}>
<CButton color="primary" variant="outline" className="ml-2" onClick={closeAndSave}>
<CButton
color="primary"
variant="outline"
className="ml-2"
onClick={closeAndSave}
disabled={disabled}
>
<CIcon content={cilSave} />
</CButton>
</CPopover>
@@ -145,6 +153,7 @@ CustomMultiModal.propTypes = {
noTable: PropTypes.bool,
toggleAdd: PropTypes.func,
reset: PropTypes.func,
disabled: PropTypes.bool.isRequired,
};
CustomMultiModal.defaultProps = {

View File

@@ -2,14 +2,14 @@ import React from 'react';
import PropTypes from 'prop-types';
import { CButtonClose } from '@coreui/react';
const SectionToggler = ({ id, label, field, updateField }) => {
const SectionToggler = ({ id, label, field, updateField, disabled }) => {
const toggle = () => updateField(id, { enabled: !field.enabled });
return (
<div className="py-1 pb-0 mb-0">
<h6 className="mt-1 float-left">{label}</h6>
<div className="text-right">
<CButtonClose onClick={toggle} style={{ color: 'white' }} />
<CButtonClose onClick={toggle} style={{ color: 'white' }} disabled={disabled} />
</div>
</div>
);
@@ -20,6 +20,7 @@ SectionToggler.propTypes = {
field: PropTypes.instanceOf(Object).isRequired,
label: PropTypes.string.isRequired,
updateField: PropTypes.func.isRequired,
disabled: PropTypes.bool.isRequired,
};
export default SectionToggler;

View File

@@ -104,7 +104,7 @@ const ConfigurationTable = ({
deleteConfig={deleteConfig}
hideTooltips={hideTooltips}
/>
<CPopover content={t('configuration.view_config')}>
<CPopover content={t('common.details')}>
<CButton
color="primary"
variant="outline"

View File

@@ -0,0 +1,64 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
CAlert,
CButton,
CModal,
CModalBody,
CModalHeader,
CModalTitle,
CPopover,
} from '@coreui/react';
import CIcon from '@coreui/icons-react';
import { cilX } from '@coreui/icons';
import useToggle from '../../hooks/useToggle';
const ConfirmStopEditingButton = ({ t, stopEditing, disabled }) => {
const [show, toggleShow] = useToggle();
const toggleAndStop = () => {
toggleShow();
stopEditing();
};
return (
<>
<CPopover content={t('common.stop_editing')}>
<CButton disabled={disabled} color="dark" onClick={toggleShow} className="ml-2">
<CIcon name="cil-x" content={cilX} />
</CButton>
</CPopover>
<CModal show={show} onClose={toggleShow}>
<CModalHeader className="p-1">
<CModalTitle className="pl-1 pt-1 text-dark">{t('common.stop_editing')}</CModalTitle>
<div className="text-right">
<CPopover content={t('common.close')}>
<CButton color="primary" variant="outline" className="ml-2" onClick={toggleShow}>
<CIcon content={cilX} />
</CButton>
</CPopover>
</div>
</CModalHeader>
<CModalBody>
<CAlert color="danger">{t('common.confirm_stop_editing')}</CAlert>
<div className="text-center">
<CButton className="mr-2" color="danger" onClick={toggleAndStop}>
{t('common.stop_editing')}
</CButton>
<CButton className="ml-2" color="light" onClick={toggleShow}>
{t('common.go_back')}
</CButton>
</div>
</CModalBody>
</CModal>
</>
);
};
ConfirmStopEditingButton.propTypes = {
t: PropTypes.func.isRequired,
stopEditing: PropTypes.func.isRequired,
disabled: PropTypes.bool.isRequired,
};
export default ConfirmStopEditingButton;

View File

@@ -1,7 +1,7 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { CButton, CDataTable, CLink, CPopover, CButtonToolbar } from '@coreui/react';
import { cilPencil, cilPlus } from '@coreui/icons';
import { cilMagnifyingGlass, cilPlus } from '@coreui/icons';
import CIcon from '@coreui/icons-react';
import ReactTooltip from 'react-tooltip';
import DeleteButton from './DeleteButton';
@@ -118,7 +118,7 @@ const ContactTable = ({
deleteContact={deleteContact}
hideTooltips={hideTooltips}
/>
<CPopover content={t('common.edit')}>
<CPopover content={t('common.details')}>
<CButton
color="primary"
variant="outline"
@@ -128,7 +128,7 @@ const ContactTable = ({
onClick={() => toggleEditModal(item.id)}
style={{ width: '33px', height: '30px' }}
>
<CIcon name="cil-pencil" content={cilPencil} size="sm" />
<CIcon name="cil-magnifying-glass" content={cilMagnifyingGlass} size="sm" />
</CButton>
</CPopover>
</CButtonToolbar>

View File

@@ -14,7 +14,7 @@ const CopyToClipboardButton = ({ t, content, size }) => {
return (
<CPopover content={t('common.copy_to_clipboard')}>
<CButton onClick={copyToClipboard} size={size}>
<CButton onClick={copyToClipboard} size={size} className="py-0">
<CIcon content={cilClone} />
{' '}
{result || ''}

View File

@@ -77,10 +77,11 @@ const DeviceDetails = ({ t, loading, getData, status, deviceConfig, lastStats })
</div>
<CRow>
<CCol lg="2" xl="1" xxl="1">
<CLabel>{t('common.serial_number')}: </CLabel>
<CLabel>{t('common.serial_num')}: </CLabel>
</CCol>
<CCol className="border-right" lg="2" xl="3" xxl="3">
{deviceConfig?.serialNumber}
{' '}
<CopyToClipboardButton t={t} size="sm" content={deviceConfig?.serialNumber} />
</CCol>
<CCol lg="2" xl="1" xxl="1">
@@ -88,6 +89,7 @@ const DeviceDetails = ({ t, loading, getData, status, deviceConfig, lastStats })
</CCol>
<CCol lg="2" xl="3" xxl="3">
{getPassword()}
{' '}
<HideTextButton t={t} toggle={toggleShowPassword} show={showPassword} />
<CopyToClipboardButton
t={t}

View File

@@ -52,10 +52,10 @@ const DeviceListTable = ({
deleteStatus,
}) => {
const columns = [
{ key: 'deviceType', label: '', filter: false, sorter: false, _style: { width: '3%' } },
{ key: 'deviceType', label: '', filter: false, sorter: false, _style: { width: '1%' } },
{ key: 'serialNumber', label: t('common.serial_number'), _style: { width: '6%' } },
{ key: 'firmware', label: t('firmware.revision') },
{ key: 'firmware_button', label: '', filter: false, _style: { width: '5%' } },
{ key: 'firmware_button', label: '', filter: false, _style: { width: '1%' } },
{ key: 'compatible', label: t('common.type'), filter: false, _style: { width: '13%' } },
{ key: 'txBytes', label: 'Tx', filter: false, _style: { width: '14%' } },
{ key: 'rxBytes', label: 'Rx', filter: false, _style: { width: '14%' } },
@@ -92,14 +92,14 @@ const DeviceListTable = ({
const tooltipId = createUuid();
let text = t('firmware.unknown_firmware_status');
let upgradeText = t('firmware.upgrade_to_latest');
let icon = <CIcon size="md" name="cil-arrow-circle-top" content={cilArrowCircleTop} />;
let icon = <CIcon name="cil-arrow-circle-top" content={cilArrowCircleTop} />;
let color = 'secondary';
if (latest !== undefined) {
text = t('firmware.newer_firmware_available');
color = 'warning';
if (latest) {
icon = <CIcon size="md" name="cil-check-circle" content={cilCheckCircle} />;
icon = <CIcon name="cil-check-circle" content={cilCheckCircle} />;
text = t('firmware.latest_version_installed');
upgradeText = t('firmware.reinstall_latest');
color = 'success';
@@ -147,7 +147,9 @@ const DeviceListTable = ({
isLoading={upgradeStatus.loading}
action={() => upgradeToLatest(device)}
block
disabled={upgradeStatus.loading}
disabled={
upgradeStatus.loading && upgradeStatus.serialNumber === device.serialNumber
}
/>
</CCol>
<CCol>
@@ -185,75 +187,82 @@ const DeviceListTable = ({
const tooltipId = createUuid();
return (
<CPopover content={t('common.delete_device')}>
<div>
<CButton
color="primary"
variant="outline"
shape="square"
size="sm"
className="mx-1"
data-tip
data-for={tooltipId}
data-event="click"
style={{ width: '33px', height: '30px' }}
>
<CIcon name="cil-trash" content={cilTrash} size="sm" />
</CButton>
<ReactTooltip
id={tooltipId}
place="top"
effect="solid"
globalEventOff="click"
clickable
className={[styles.deleteTooltip, 'tooltipRight'].join(' ')}
border
borderColor="#321fdb"
arrowColor="white"
overridePosition={({ left, top }) => {
const element = document.getElementById(tooltipId);
const tooltipWidth = element ? element.offsetWidth : 0;
const newLeft = left - tooltipWidth * 0.25;
return { top, left: newLeft };
}}
>
<CCardHeader color="primary" className={styles.tooltipHeader}>
{t('common.device_delete', { serialNumber })}
<CButtonClose
style={{ color: 'white' }}
onClick={(e) => {
e.target.parentNode.parentNode.classList.remove('show');
hideTooltips();
}}
/>
</CCardHeader>
<CCardBody>
<CRow>
<CCol>
<LoadingButton
data-toggle="dropdown"
variant="outline"
color="danger"
label={t('common.confirm')}
isLoadingLabel={t('user.deleting')}
isLoading={deleteStatus.loading}
action={() => deleteDevice(serialNumber)}
block
disabled={deleteStatus.loading}
/>
</CCol>
</CRow>
</CCardBody>
</ReactTooltip>
</div>
</CPopover>
<div>
<CPopover content={t('common.delete_device')}>
<div>
<CButton
color="primary"
variant="outline"
shape="square"
size="sm"
className="mx-1"
data-tip
data-for={tooltipId}
data-event="click"
style={{ width: '33px', height: '30px' }}
>
<CIcon name="cil-trash" content={cilTrash} size="sm" />
</CButton>
</div>
</CPopover>
<ReactTooltip
id={tooltipId}
place="top"
effect="solid"
globalEventOff="click"
clickable
className={[styles.deleteTooltip, 'tooltipRight'].join(' ')}
border
borderColor="#321fdb"
arrowColor="white"
overridePosition={({ left, top }) => {
const element = document.getElementById(tooltipId);
const tooltipWidth = element ? element.offsetWidth : 0;
const newLeft = left - tooltipWidth * 0.25;
return { top, left: newLeft };
}}
>
<CCardHeader color="primary" className={styles.tooltipHeader}>
{t('common.device_delete', { serialNumber })}
<CButtonClose
className="p-0 mb-1"
style={{ color: 'white' }}
onClick={(e) => {
e.target.parentNode.parentNode.classList.remove('show');
hideTooltips();
}}
/>
</CCardHeader>
<CCardBody>
<CRow>
<CCol>
<LoadingButton
data-toggle="dropdown"
variant="outline"
color="danger"
label={t('common.confirm')}
isLoadingLabel={t('user.deleting')}
isLoading={deleteStatus.loading}
action={(e) => {
e.target.parentNode.parentNode.parentNode.parentNode.classList.remove('show');
hideTooltips();
deleteDevice(serialNumber);
}}
block
disabled={deleteStatus.loading}
/>
</CCol>
</CRow>
</CCardBody>
</ReactTooltip>
</div>
);
};
return (
<>
<CCard className="m-0 p-0">
<CCardHeader className="dark-header">
<CCardHeader className="p-0">
<div className="float-left" style={{ width: '400px' }}>
{searchBar}
</div>
@@ -335,7 +344,7 @@ const DeviceListTable = ({
<CButtonToolbar
role="group"
className="justify-content-center"
style={{ width: '170px' }}
style={{ width: '180px' }}
>
<CPopover content={t('actions.connect')}>
<CButton

View File

@@ -19,10 +19,12 @@
font-size: 0.875rem !important;
font-weight: 400 !important;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2) !important;
width: 300px;
width: 200px;
}
.tooltipHeader {
padding-left: 5px;
padding-right: 10px;
border-top-left-radius: 1rem !important;
border-top-right-radius: 1rem !important;
}

View File

@@ -2,7 +2,7 @@ import React, { useState } from 'react';
import PropTypes from 'prop-types';
import Select, { components } from 'react-select';
const DeviceSearchBar = ({ t, search, results, history }) => {
const DeviceSearchBar = ({ t, search, results, history, action }) => {
const [selected, setSelected] = useState('');
const NoOptionsMessage = (props) => (
<components.NoOptionsMessage {...props}>
@@ -25,7 +25,9 @@ const DeviceSearchBar = ({ t, search, results, history }) => {
inputValue={selected}
placeholder={t('common.search')}
onInputChange={onInputChange}
onChange={(property) => history.push(`/devices/${property.value}`)}
onChange={(property) =>
action === null ? history.push(`/devices/${property.value}`) : action(property.value)
}
/>
);
};
@@ -35,6 +37,11 @@ DeviceSearchBar.propTypes = {
search: PropTypes.func.isRequired,
results: PropTypes.instanceOf(Array).isRequired,
history: PropTypes.instanceOf(Object).isRequired,
action: PropTypes.func,
};
DeviceSearchBar.defaultProps = {
action: null,
};
export default DeviceSearchBar;

View File

@@ -119,11 +119,18 @@ const DeviceStatusCard = ({
errorField(t)
) : (
<div>
{lastStats?.unit?.load[0] ? (lastStats?.unit?.load[0] * 100).toFixed(2) : '-'}%
{' / '}
{lastStats?.unit?.load[1] ? (lastStats?.unit?.load[1] * 100).toFixed(2) : '-'}%
{' / '}
{lastStats?.unit?.load[2] ? (lastStats?.unit?.load[2] * 100).toFixed(2) : '-'}%
{lastStats?.unit?.load[0] !== undefined
? (lastStats?.unit?.load[0] * 100).toFixed(2)
: '-'}
%{' / '}
{lastStats?.unit?.load[1] !== undefined
? (lastStats?.unit?.load[1] * 100).toFixed(2)
: '-'}
%{' / '}
{lastStats?.unit?.load[2] !== undefined
? (lastStats?.unit?.load[2] * 100).toFixed(2)
: '-'}
%
</div>
)}
</CCol>

View File

@@ -191,6 +191,14 @@ const EditConfigurationForm = ({
/>
</div>
</CCol>
<CLabel className="mb-2" md="5" lg="2" xl="2" xxl="1" col htmlFor="name">
<div>{t('common.created')}:</div>
</CLabel>
<CCol md="7" lg="4" xl="4" xxl="5">
<p className="mt-2 mb-0">
<FormattedDate date={fields.created.value} />
</p>
</CCol>
<CLabel col className="mb-2" md="5" lg="2" xl="2" xxl="1" htmlFor="firmwareRCOnly">
Only Release Candidates
</CLabel>
@@ -206,14 +214,6 @@ const EditConfigurationForm = ({
disabled={!editing || fields.firmwareUpgrade.value === 'no'}
/>
</CCol>
<CLabel className="mb-2" md="5" lg="2" xl="2" xxl="1" col htmlFor="name">
<div>{t('common.created')}:</div>
</CLabel>
<CCol md="7" lg="4" xl="4" xxl="5">
<p className="mt-2 mb-0">
<FormattedDate date={fields.created.value} />
</p>
</CCol>
<CLabel className="mb-2" md="5" lg="2" xl="2" xxl="1" col htmlFor="name">
<div>{t('common.modified')}:</div>
</CLabel>

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import Select from 'react-select';
import CreatableSelect from 'react-select/creatable';
@@ -14,10 +14,12 @@ import {
CLink,
CPopover,
CButton,
CCollapse,
} from '@coreui/react';
import FormattedDate from '../FormattedDate';
import RequiredAsterisk from '../RequiredAsterisk';
import selectStyles from '../../utils/selectStyles';
import useToggle from '../../hooks/useToggle';
const EditContactForm = ({
t,
@@ -30,6 +32,7 @@ const EditContactForm = ({
hideEntities,
editing,
}) => {
const [showDropdown, toggleDropdown, setShowDropdown] = useToggle(false);
const [filter, setFilter] = useState('');
const onPhonesChange = (v) => updateFieldWithKey('phones', { value: v.map((obj) => obj.value) });
@@ -52,8 +55,13 @@ const EditContactForm = ({
{ id: 'entity', value: id },
{ id: 'entityName', value: name },
]);
toggleDropdown();
};
useEffect(() => {
if (!editing) setShowDropdown(false);
}, [editing]);
return (
<CForm>
<CRow>
@@ -344,7 +352,7 @@ const EditContactForm = ({
)}
</CCol>
<CLabel className="mb-2" sm="2" col htmlFor="phones">
Landlines
{t('location.phones')}
</CLabel>
<CCol sm="4">
<CreatableSelect
@@ -355,11 +363,11 @@ const EditContactForm = ({
components={{ NoOptionsMessage }}
options={[]}
value={fields.phones.value.map((opt) => ({ value: opt, label: opt }))}
placeholder={t('common.type_for_options')}
placeholder="+1(202)555-0103"
/>
</CCol>
<CLabel className="mb-2" sm="2" col htmlFor="phones">
Mobiles
{t('location.mobiles')}
</CLabel>
<CCol sm="4">
<CreatableSelect
@@ -370,70 +378,72 @@ const EditContactForm = ({
components={{ NoOptionsMessage }}
options={[]}
value={fields.mobiles.value.map((opt) => ({ value: opt, label: opt }))}
placeholder={t('common.type_for_options')}
placeholder="+1(202)555-0103"
/>
</CCol>
</CRow>
<CFormGroup row className="pb-1">
<CLabel sm="2" col htmlFor="title">
{t('entity.selected_entity')}
{t('entity.entity')}
</CLabel>
<CCol sm="4" className="pt-2">
<h6>
<CCol sm="4">
<CButton className="pl-0" color="link" onClick={toggleDropdown} disabled={!editing}>
{fields.entity.value === '' ? t('entity.need_select_entity') : fields.entityName.value}
</h6>
</CButton>
</CCol>
</CFormGroup>
{hideEntities ? null : (
<div className="overflow-auto border mb-3" style={{ height: '200px' }}>
<CInput
className="w-50 mb-2"
type="text"
placeholder="Search"
value={filter}
onChange={(e) => setFilter(e.target.value)}
/>
<CDataTable
items={entities}
fields={columns}
hover
tableFilterValue={filter}
border
scopedSlots={{
name: (item) => (
<td className="align-middle p-1">
<CLink
className="c-subheader-nav-link"
aria-current="page"
to={() => `/entity/${item.id}`}
>
{item.name}
</CLink>
</td>
),
created: (item) => (
<td className="align-middle p-1">
<FormattedDate date={item.created} />
</td>
),
actions: (item) => (
<td className="align-middle p-1">
<CPopover content={t('entity.select_entity')}>
<CButton
size="sm"
color="primary"
variant="outline"
onClick={() => selectEntity(item)}
disabled={!editing}
<CCollapse show={showDropdown}>
<div className="overflow-auto border mb-3" style={{ height: '200px' }}>
<CInput
className="w-50 mb-2"
type="text"
placeholder="Search"
value={filter}
onChange={(e) => setFilter(e.target.value)}
/>
<CDataTable
items={entities}
fields={columns}
hover
tableFilterValue={filter}
border
scopedSlots={{
name: (item) => (
<td className="align-middle p-1">
<CLink
className="c-subheader-nav-link"
aria-current="page"
to={() => `/entity/${item.id}`}
>
{t('common.select')}
</CButton>
</CPopover>
</td>
),
}}
/>
</div>
{item.name}
</CLink>
</td>
),
created: (item) => (
<td className="align-middle p-1">
<FormattedDate date={item.created} />
</td>
),
actions: (item) => (
<td className="align-middle p-1">
<CPopover content={t('entity.select_entity')}>
<CButton
size="sm"
color="primary"
variant="outline"
onClick={() => selectEntity(item)}
disabled={!editing}
>
{t('common.select')}
</CButton>
</CPopover>
</td>
),
}}
/>
</div>
</CCollapse>
)}
</CForm>
);

View File

@@ -80,6 +80,14 @@ const EditEntityForm = ({
/>
</div>
</CCol>
<CLabel className="mb-2" md="5" lg="2" xl="2" xxl="1" col htmlFor="name">
<div>{t('common.created')}:</div>
</CLabel>
<CCol md="7" lg="4" xl="4" xxl="5">
<div className="mt-2 mb-0">
<FormattedDate date={fields.created.value} />
</div>
</CCol>
<CLabel className="mb-2" md="5" lg="2" xl="2" xxl="1" col htmlFor="sourceIp">
<div>{t('entity.ip_detection')}:</div>
</CLabel>
@@ -100,14 +108,6 @@ const EditEntityForm = ({
</div>
)}
</CCol>
<CLabel className="mb-2" md="5" lg="2" xl="2" xxl="1" col htmlFor="name">
<div>{t('common.created')}:</div>
</CLabel>
<CCol md="7" lg="4" xl="4" xxl="5">
<div className="mt-2 mb-0">
<FormattedDate date={fields.created.value} />
</div>
</CCol>
<CLabel className="mb-2" md="5" lg="2" xl="2" xxl="1" col htmlFor="name">
<div>{t('common.modified')}:</div>
</CLabel>

View File

@@ -1,9 +1,24 @@
import React from 'react';
import { CForm, CInput, CLabel, CCol, CFormGroup, CFormText, CRow } from '@coreui/react';
import React, { useEffect, useState } from 'react';
import {
CForm,
CDataTable,
CLink,
CInput,
CLabel,
CCol,
CFormGroup,
CFormText,
CRow,
CButton,
CCollapse,
CPopover,
} from '@coreui/react';
import Select from 'react-select';
import PropTypes from 'prop-types';
import FormattedDate from '../FormattedDate';
import RequiredAsterisk from '../RequiredAsterisk';
import selectStyles from '../../utils/selectStyles';
import useToggle from '../../hooks/useToggle';
const EditInventoryTagForm = ({
t,
@@ -11,116 +26,281 @@ const EditInventoryTagForm = ({
fields,
updateField,
updateFieldDirectly,
entities,
venues,
deviceTypes,
editing,
}) => (
<CForm>
<CFormGroup row className="mb-1">
<CCol>
<CLabel htmlFor="serialNumber">{t('common.serial_number')}</CLabel>
</CCol>
<CCol sm="8">{fields.serialNumber.value}</CCol>
</CFormGroup>
<CFormGroup row className="mb-1">
<CLabel col htmlFor="name">
{t('user.name')}
<RequiredAsterisk />
</CLabel>
<CCol sm="8">
{editing ? (
<div>
<CInput
id="name"
type="text"
required
value={fields.name.value}
onChange={updateField}
invalid={fields.name.error}
disabled={disable}
maxLength="50"
/>
<CFormText hidden={!fields.name.error} color={fields.name.error ? 'danger' : ''}>
{t('common.required')}
</CFormText>
</div>
) : (
<p className="mt-2 mb-0">{fields.name.value}</p>
)}
</CCol>
</CFormGroup>
<CFormGroup row className="mb-1">
<CLabel col htmlFor="description">
{t('user.description')}
</CLabel>
<CCol sm="8">
{editing ? (
<div>
<CInput
id="description"
type="text"
required
value={fields.description.value}
onChange={updateField}
disabled={disable}
maxLength="50"
hideEntities,
batchSetField,
}) => {
const [showDropdown, toggleDropdown, setShowDropdown] = useToggle(false);
const [entityFilter, setEntityFilter] = useState('');
const [venueFilter, setVenueFilter] = useState('');
const selectEntity = ({ id, name }) => {
batchSetField([
{ id: 'entity', value: id },
{ id: 'entityName', value: name },
{ id: 'venue', value: '' },
{ id: 'venueName', value: '' },
]);
toggleDropdown();
};
const selectVenue = ({ id, name }) => {
batchSetField([
{ id: 'venue', value: id },
{ id: 'venueName', value: name },
{ id: 'entity', value: '' },
{ id: 'entityName', value: '' },
]);
toggleDropdown();
};
const columns = [
{ key: 'created', label: t('common.created'), _style: { width: '20%' }, filter: false },
{ key: 'name', label: t('user.name'), _style: { width: '25%' }, filter: false },
{ key: 'description', label: t('user.description'), _style: { width: '50%' } },
{ key: 'actions', label: '', _style: { width: '15%' }, filter: false },
];
const getEntityLabel = () => {
if (fields.entity.value !== '') return t('entity.entity');
if (fields.venue.value !== '') return t('inventory.venue');
return `${t('entity.entity')}/${t('inventory.venue')}`;
};
const getEntityValue = () => {
if (fields.entity.value !== '') return fields.entityName.value;
if (fields.venue.value !== '') return fields.venueName.value;
return t('entity.need_select_entity');
};
useEffect(() => {
if (!editing) setShowDropdown(false);
}, [editing]);
return (
<CForm>
<CFormGroup row className="mb-1">
<CCol>
<CLabel htmlFor="serialNumber">{t('common.serial_number')}</CLabel>
</CCol>
<CCol sm="8">{fields.serialNumber.value}</CCol>
</CFormGroup>
<CFormGroup row className="mb-1">
<CLabel col htmlFor="name">
{t('user.name')}
<RequiredAsterisk />
</CLabel>
<CCol sm="8">
{editing ? (
<div>
<CInput
id="name"
type="text"
required
value={fields.name.value}
onChange={updateField}
invalid={fields.name.error}
disabled={disable}
maxLength="50"
/>
<CFormText hidden={!fields.name.error} color={fields.name.error ? 'danger' : ''}>
{t('common.required')}
</CFormText>
</div>
) : (
<p className="mt-2 mb-0">{fields.name.value}</p>
)}
</CCol>
</CFormGroup>
<CFormGroup row className="mb-1">
<CLabel col htmlFor="description">
{t('user.description')}
</CLabel>
<CCol sm="8">
{editing ? (
<div>
<CInput
id="description"
type="text"
required
value={fields.description.value}
onChange={updateField}
disabled={disable}
maxLength="50"
/>
</div>
) : (
<p className="mt-2 mb-0">{fields.description.value}</p>
)}
</CCol>
</CFormGroup>
<CFormGroup row className="mb-1">
<CLabel col htmlFor="deviceType">
{t('firmware.device_type')}
<RequiredAsterisk />
</CLabel>
<CCol sm="8">
<div style={{ width: '250px' }}>
<Select
styles={selectStyles}
id="deviceType"
value={{ value: fields.deviceType.value, label: fields.deviceType.value }}
onChange={(v) => updateFieldDirectly('deviceType', { value: v.value })}
options={deviceTypes.map((v) => ({ value: v, label: v }))}
isDisabled={disable || !editing}
/>
</div>
) : (
<p className="mt-2 mb-0">{fields.description.value}</p>
)}
</CCol>
</CFormGroup>
<CFormGroup row className="mb-1">
<CLabel col htmlFor="deviceType">
{t('firmware.device_type')}
<RequiredAsterisk />
</CLabel>
<CCol sm="8">
<div style={{ width: '250px' }}>
<Select
styles={selectStyles}
id="deviceType"
value={{ value: fields.deviceType.value, label: fields.deviceType.value }}
onChange={(v) => updateFieldDirectly('deviceType', { value: v.value })}
options={deviceTypes.map((v) => ({ value: v, label: v }))}
isDisabled={disable || !editing}
/>
</div>
<CFormText
hidden={!fields.deviceType.error}
color={fields.deviceType.error ? 'danger' : ''}
>
{t('common.required')}
</CFormText>
</CCol>
</CFormGroup>
<CRow className="mb-1">
<CLabel sm="4" col htmlFor="rrm">
RRM
<RequiredAsterisk />
</CLabel>
<CCol sm="8">
<div style={{ width: '120px' }}>
<Select
id="rrm"
styles={selectStyles}
value={{ value: fields.rrm.value, label: fields.rrm.value }}
onChange={(v) => updateFieldDirectly('rrm', { value: v.value, error: false })}
options={[
{ label: 'on', value: 'on' },
{ label: 'off', value: 'off' },
{ label: 'inherit', value: 'inherit' },
]}
isDisabled={disable || !editing}
/>
</div>
<CFormText hidden={!fields.rrm.error} color={fields.rrm.error ? 'danger' : ''}>
{t('common.required')}
</CFormText>
</CCol>
</CRow>
</CForm>
);
<CFormText
hidden={!fields.deviceType.error}
color={fields.deviceType.error ? 'danger' : ''}
>
{t('common.required')}
</CFormText>
</CCol>
</CFormGroup>
<CRow className="mb-1">
<CLabel sm="4" col htmlFor="rrm">
RRM
<RequiredAsterisk />
</CLabel>
<CCol sm="8">
<div style={{ width: '120px' }}>
<Select
id="rrm"
styles={selectStyles}
value={{ value: fields.rrm.value, label: fields.rrm.value }}
onChange={(v) => updateFieldDirectly('rrm', { value: v.value, error: false })}
options={[
{ label: 'on', value: 'on' },
{ label: 'off', value: 'off' },
{ label: 'inherit', value: 'inherit' },
]}
isDisabled={disable || !editing}
/>
</div>
<CFormText hidden={!fields.rrm.error} color={fields.rrm.error ? 'danger' : ''}>
{t('common.required')}
</CFormText>
</CCol>
</CRow>
<CFormGroup row className="pb-1">
<CLabel sm="4" col htmlFor="title">
{getEntityLabel()}
</CLabel>
<CCol sm="8">
<CButton className="pl-0" color="link" onClick={toggleDropdown} disabled={!editing}>
{getEntityValue()}
</CButton>
</CCol>
</CFormGroup>
{hideEntities ? null : (
<CCollapse show={showDropdown}>
<div className="overflow-auto border mb-3" style={{ height: '200px' }}>
<h5>{t('entity.entities')}</h5>
<CInput
className="w-50 mb-2"
type="text"
placeholder="Search"
value={entityFilter}
onChange={(e) => setEntityFilter(e.target.value)}
/>
<CDataTable
items={entities}
fields={columns}
hover
tableFilterValue={entityFilter}
border
scopedSlots={{
name: (item) => (
<td className="align-middle p-1">
<CLink
className="c-subheader-nav-link"
aria-current="page"
to={() => `/entity/${item.id}`}
>
{item.name}
</CLink>
</td>
),
created: (item) => (
<td className="align-middle p-1">
<FormattedDate date={item.created} />
</td>
),
actions: (item) => (
<td className="align-middle p-1">
<CPopover content={t('entity.select_entity')}>
<CButton
size="sm"
color="primary"
variant="outline"
onClick={() => selectEntity(item)}
disabled={!editing}
>
{t('common.select')}
</CButton>
</CPopover>
</td>
),
}}
/>
<h5>{t('entity.venues')}</h5>
<CInput
className="w-50 mb-2"
type="text"
placeholder="Search"
value={venueFilter}
onChange={(e) => setVenueFilter(e.target.value)}
/>
<CDataTable
items={venues}
fields={columns}
hover
tableFilterValue={venueFilter}
border
scopedSlots={{
name: (item) => (
<td className="align-middle p-1">
<CLink
className="c-subheader-nav-link"
aria-current="page"
to={() => `/entity/${item.id}`}
>
{item.name}
</CLink>
</td>
),
created: (item) => (
<td className="align-middle p-1">
<FormattedDate date={item.created} />
</td>
),
actions: (item) => (
<td className="align-middle p-1">
<CPopover content={t('entity.select_entity')}>
<CButton
size="sm"
color="primary"
variant="outline"
onClick={() => selectVenue(item)}
disabled={!editing}
>
{t('common.select')}
</CButton>
</CPopover>
</td>
),
}}
/>
</div>
</CCollapse>
)}
</CForm>
);
};
EditInventoryTagForm.propTypes = {
t: PropTypes.func.isRequired,
@@ -130,6 +310,14 @@ EditInventoryTagForm.propTypes = {
updateFieldDirectly: PropTypes.func.isRequired,
deviceTypes: PropTypes.instanceOf(Array).isRequired,
editing: PropTypes.bool.isRequired,
entities: PropTypes.instanceOf(Array).isRequired,
venues: PropTypes.instanceOf(Array).isRequired,
hideEntities: PropTypes.bool,
batchSetField: PropTypes.func.isRequired,
};
EditInventoryTagForm.defaultProps = {
hideEntities: false,
};
export default EditInventoryTagForm;

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import Select from 'react-select';
import CreatableSelect from 'react-select/creatable';
@@ -14,10 +14,12 @@ import {
CLink,
CPopover,
CButton,
CCollapse,
} from '@coreui/react';
import countryList from 'utils/countryList';
import FormattedDate from '../FormattedDate';
import RequiredAsterisk from '../RequiredAsterisk';
import useToggle from '../../hooks/useToggle';
const EditLocationForm = ({
t,
@@ -30,6 +32,7 @@ const EditLocationForm = ({
batchSetField,
editing,
}) => {
const [showDropdown, toggleDropdown, setShowDropdown] = useToggle(false);
const [filter, setFilter] = useState('');
const onPhonesChange = (v) => updateFieldWithKey('phones', { value: v.map((obj) => obj.value) });
@@ -52,8 +55,13 @@ const EditLocationForm = ({
{ id: 'entity', value: id },
{ id: 'entityName', value: name },
]);
toggleDropdown();
};
useEffect(() => {
if (!editing) setShowDropdown(false);
}, [editing]);
return (
<CForm>
<CRow>
@@ -153,7 +161,7 @@ const EditLocationForm = ({
)}
</CCol>
<CLabel className="mb-3" sm="2" col htmlFor="phones">
Landlines
{t('location.phones')}
</CLabel>
<CCol sm="4">
<CreatableSelect
@@ -164,11 +172,11 @@ const EditLocationForm = ({
components={{ NoOptionsMessage }}
options={[]}
value={fields.phones.value.map((opt) => ({ value: opt, label: opt }))}
placeholder={t('common.type_for_options')}
placeholder="+1(202)555-0103"
/>
</CCol>
<CLabel className="mb-3" sm="2" col htmlFor="phones">
Mobiles
<CLabel className="mb-3" sm="2" col htmlFor="mobiles">
{t('location.mobiles')}
</CLabel>
<CCol sm="4">
<CreatableSelect
@@ -179,7 +187,7 @@ const EditLocationForm = ({
components={{ NoOptionsMessage }}
options={[]}
value={fields.mobiles.value.map((opt) => ({ value: opt, label: opt }))}
placeholder={t('common.type_for_options')}
placeholder="+1(202)555-0103"
/>
</CCol>
<CCol className="mb-3" sm="12">
@@ -339,63 +347,65 @@ const EditLocationForm = ({
</CRow>
<CFormGroup row className="pt-2 pb-1">
<CLabel sm="2" col htmlFor="title">
{t('entity.selected_entity')}
{t('entity.entity')}
</CLabel>
<CCol sm="4" className="pt-2">
<h6>
<CCol sm="4">
<CButton className="pl-0" color="link" onClick={toggleDropdown} disabled={!editing}>
{fields.entity.value === '' ? t('entity.need_select_entity') : fields.entityName.value}
</h6>
</CButton>
</CCol>
</CFormGroup>
<div className="overflow-auto border mb-1" style={{ height: '200px' }}>
<CInput
className="w-50 mb-2"
type="text"
placeholder="Search"
value={filter}
onChange={(e) => setFilter(e.target.value)}
/>
<CDataTable
items={entities}
fields={columns}
hover
tableFilterValue={filter}
border
scopedSlots={{
name: (item) => (
<td className="align-middle p-1">
<CLink
className="c-subheader-nav-link"
aria-current="page"
to={() => `/configuration/${item.id}`}
>
{item.name}
</CLink>
</td>
),
created: (item) => (
<td className="align-middle p-1">
<FormattedDate date={item.created} />
</td>
),
actions: (item) => (
<td className="align-middle p-1">
<CPopover content={t('entity.select_entity')}>
<CButton
disabled={!editing}
size="sm"
color="primary"
variant="outline"
onClick={() => selectEntity(item)}
<CCollapse show={showDropdown}>
<div className="overflow-auto border mb-1" style={{ height: '200px' }}>
<CInput
className="w-50 mb-2"
type="text"
placeholder="Search"
value={filter}
onChange={(e) => setFilter(e.target.value)}
/>
<CDataTable
items={entities}
fields={columns}
hover
tableFilterValue={filter}
border
scopedSlots={{
name: (item) => (
<td className="align-middle p-1">
<CLink
className="c-subheader-nav-link"
aria-current="page"
to={() => `/configuration/${item.id}`}
>
{t('common.select')}
</CButton>
</CPopover>
</td>
),
}}
/>
</div>
{item.name}
</CLink>
</td>
),
created: (item) => (
<td className="align-middle p-1">
<FormattedDate date={item.created} />
</td>
),
actions: (item) => (
<td className="align-middle p-1">
<CPopover content={t('entity.select_entity')}>
<CButton
disabled={!editing}
size="sm"
color="primary"
variant="outline"
onClick={() => selectEntity(item)}
>
{t('common.select')}
</CButton>
</CPopover>
</td>
),
}}
/>
</div>
</CCollapse>
</CForm>
);
};

View File

@@ -0,0 +1,554 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import {
CForm,
CInput,
CLabel,
CCol,
CFormText,
CRow,
CInputFile,
CInvalidFeedback,
CSelect,
} from '@coreui/react';
import RequiredAsterisk from '../RequiredAsterisk';
const validatePem = (value) =>
(value.includes('---BEGIN CERTIFICATE---') && value.includes('---END CERTIFICATE---')) ||
(value.includes('---BEGIN PRIVATE KEY---') && value.includes('---END PRIVATE KEY---'));
const EditSimulationForm = ({
t,
show,
disable,
fields,
updateField,
updateFieldWithKey,
editing,
}) => {
const [certKey, setCertKey] = useState(0);
const [keyKey, setKeyKey] = useState(0);
let fileReader;
const handleCertFileRead = () => {
const content = fileReader.result;
if (content && validatePem(content)) {
updateFieldWithKey('certificate', { value: content, error: false });
} else {
updateFieldWithKey('certificate', { error: true });
}
};
const handleCertFile = (file) => {
fileReader = new FileReader();
fileReader.onloadend = handleCertFileRead;
fileReader.readAsText(file);
};
const handleKeyFileRead = () => {
updateFieldWithKey('key', { error: false });
const content = fileReader.result;
if (content && validatePem(content)) {
updateFieldWithKey('key', { value: content, error: false });
} else {
updateFieldWithKey('key', { error: true });
}
};
const handleKeyFile = (file) => {
fileReader = new FileReader();
fileReader.onloadend = handleKeyFileRead;
fileReader.readAsText(file);
};
useEffect(() => {
if (show) {
setCertKey(certKey + 1);
setKeyKey(keyKey + 1);
}
}, [show]);
return (
<CForm>
<CRow>
<CLabel className="mb-2" sm="2" col htmlFor="name">
{t('user.name')}
<RequiredAsterisk />
</CLabel>
<CCol sm="4">
<CInput
id="name"
type="text"
required
value={fields.name.value}
onChange={updateField}
invalid={fields.name.error}
disabled={disable || !editing}
maxLength="50"
/>
<CFormText hidden={!fields.name.error} color={fields.name.error ? 'danger' : ''}>
{t('common.required')}
</CFormText>
</CCol>
<CLabel className="mb-2" sm="2" col htmlFor="gateway">
{t('simulation.gateway')}
<RequiredAsterisk />
</CLabel>
<CCol sm="4">
<CInput
id="gateway"
type="text"
required
value={fields.gateway.value}
onChange={updateField}
invalid={fields.gateway.error}
disabled={disable || !editing}
maxLength="50"
/>
<CFormText hidden={!fields.gateway.error} color={fields.gateway.error ? 'danger' : ''}>
{t('common.required')}
</CFormText>
</CCol>
<CLabel className="mb-2" sm="2" col htmlFor="certificate">
{t('common.certificate')}
<RequiredAsterisk />
</CLabel>
<CCol sm="4">
{editing ? (
<div>
<CInputFile
className="mt-1"
key={certKey}
id="file-input"
name="file-input"
accept=".pem"
onChange={(e) => handleCertFile(e.target.files[0])}
/>
<CFormText
hidden={!fields.certificate.error}
color={fields.certificate.error ? 'danger' : ''}
>
{t('common.required')}
</CFormText>
</div>
) : (
<div className="pt-1 mt-1">{t('simulation.valid_cert')}</div>
)}
</CCol>
<CLabel className="mb-2" sm="2" col htmlFor="key">
{t('common.key')}
<RequiredAsterisk />
</CLabel>
<CCol sm="4">
{editing ? (
<div>
<CInputFile
className="mt-1"
key={keyKey}
id="file-input"
name="file-input"
accept=".pem"
onChange={(e) => handleKeyFile(e.target.files[0])}
/>
<CFormText hidden={!fields.key.error} color={fields.key.error ? 'danger' : ''}>
{t('common.required')}
</CFormText>
</div>
) : (
<div className="pt-1 mt-1">{t('simulation.valid_key')}</div>
)}
</CCol>
<CLabel className="mb-2" sm="2" col htmlFor="macPrefix">
{t('simulation.mac_prefix')}
<RequiredAsterisk />
</CLabel>
<CCol sm="4">
<CInput
id="macPrefix"
type="text"
required
value={fields.macPrefix.value}
onChange={updateField}
invalid={fields.macPrefix.error}
disabled={disable || !editing}
maxLength="50"
/>
<CFormText
hidden={!fields.macPrefix.error}
color={fields.macPrefix.error ? 'danger' : ''}
>
{t('simulation.prefix_length')}
</CFormText>
</CCol>
<CLabel className="mb-2" sm="2" col htmlFor="devices">
{t('common.devices')}
<RequiredAsterisk />
</CLabel>
<CCol sm="4">
<CInput
id="devices"
type="number"
required
value={fields.devices.value}
onChange={updateField}
invalid={
fields.devices.value < fields.devices.min || fields.devices.value > fields.devices.max
}
disabled={disable || !editing}
pattern="[0-9]*"
style={{ width: '100px' }}
/>
<CFormText hidden={!fields.devices.error} color={fields.devices.error ? 'danger' : ''}>
{t('common.required')}
</CFormText>
</CCol>
<CLabel className="mb-2" sm="2" col htmlFor="healthCheckInterval">
{t('simulation.healtcheck_interval')}
<RequiredAsterisk />
</CLabel>
<CCol sm="4">
<CInput
id="healthCheckInterval"
type="number"
required
value={fields.healthCheckInterval.value}
onChange={updateField}
invalid={
fields.healthCheckInterval.value < fields.healthCheckInterval.min ||
fields.healthCheckInterval.value > fields.healthCheckInterval.max
}
disabled={disable || !editing}
pattern="[0-9]*"
style={{ width: '100px' }}
/>
<CInvalidFeedback>
{t('common.min_max', {
min: fields.healthCheckInterval.min,
max: fields.healthCheckInterval.max,
})}
</CInvalidFeedback>
</CCol>
<CLabel className="mb-2" sm="2" col htmlFor="stateInterval">
{t('simulation.state_interval')}
<RequiredAsterisk />
</CLabel>
<CCol sm="4">
<CInput
id="stateInterval"
type="number"
required
value={fields.stateInterval.value}
onChange={updateField}
invalid={
fields.stateInterval.value < fields.stateInterval.min ||
fields.stateInterval.value > fields.stateInterval.max
}
disabled={disable || !editing}
pattern="[0-9]*"
style={{ width: '100px' }}
/>
<CInvalidFeedback>
{t('common.min_max', {
min: fields.stateInterval.min,
max: fields.stateInterval.max,
})}
</CInvalidFeedback>
</CCol>
<CLabel className="mb-2" sm="2" col htmlFor="clientInterval">
{t('simulation.client_interval')}
<RequiredAsterisk />
</CLabel>
<CCol sm="4">
<CInput
id="clientInterval"
type="number"
required
value={fields.clientInterval.value}
onChange={updateField}
invalid={
fields.clientInterval.value < fields.clientInterval.min ||
fields.clientInterval.value > fields.clientInterval.max
}
disabled={disable || !editing}
pattern="[0-9]*"
style={{ width: '100px' }}
/>
<CInvalidFeedback>
{t('common.min_max', {
min: fields.clientInterval.min,
max: fields.clientInterval.max,
})}
</CInvalidFeedback>
</CCol>
<CLabel className="mb-2" sm="2" col htmlFor="reconnectInterval">
{t('simulation.reconnect_interval')}
<RequiredAsterisk />
</CLabel>
<CCol sm="4">
<CInput
id="reconnectInterval"
type="number"
required
value={fields.reconnectInterval.value}
onChange={updateField}
invalid={
fields.reconnectInterval.value < fields.reconnectInterval.min ||
fields.reconnectInterval.value > fields.reconnectInterval.max
}
disabled={disable || !editing}
pattern="[0-9]*"
style={{ width: '100px' }}
/>
<CInvalidFeedback>
{t('common.min_max', {
min: fields.reconnectInterval.min,
max: fields.reconnectInterval.max,
})}
</CInvalidFeedback>
</CCol>
<CLabel className="mb-2" sm="2" col htmlFor="minAssociations">
{t('simulation.min_associations')}
<RequiredAsterisk />
</CLabel>
<CCol sm="4">
<CInput
id="minAssociations"
type="number"
required
value={fields.minAssociations.value}
onChange={updateField}
invalid={
fields.minAssociations.value < fields.minAssociations.min ||
fields.minAssociations.value > fields.minAssociations.max
}
disabled={disable || !editing}
pattern="[0-9]*"
style={{ width: '100px' }}
/>
<CInvalidFeedback>
{t('common.min_max', {
min: fields.minAssociations.min,
max: fields.minAssociations.max,
})}
</CInvalidFeedback>
</CCol>
<CLabel className="mb-2" sm="2" col htmlFor="maxAssociations">
{t('simulation.max_associations')}
<RequiredAsterisk />
</CLabel>
<CCol sm="4">
<CInput
id="maxAssociations"
type="number"
required
value={fields.maxAssociations.value}
onChange={updateField}
invalid={
fields.maxAssociations.value < fields.maxAssociations.min ||
fields.maxAssociations.value > fields.maxAssociations.max
}
disabled={disable || !editing}
pattern="[0-9]*"
style={{ width: '100px' }}
/>
<CInvalidFeedback>
{t('common.min_max', {
min: fields.maxAssociations.min,
max: fields.maxAssociations.max,
})}
</CInvalidFeedback>
</CCol>
<CLabel className="mb-2" sm="2" col htmlFor="minClients">
{t('simulation.min_clients')}
<RequiredAsterisk />
</CLabel>
<CCol sm="4">
<CInput
id="minClients"
type="number"
required
value={fields.minClients.value}
onChange={updateField}
invalid={
fields.minClients.value < fields.minClients.min ||
fields.minClients.value > fields.minClients.max
}
disabled={disable || !editing}
pattern="[0-9]*"
style={{ width: '100px' }}
/>
<CInvalidFeedback>
{t('common.min_max', {
min: fields.minClients.min,
max: fields.minClients.max,
})}
</CInvalidFeedback>
</CCol>
<CLabel className="mb-2" sm="2" col htmlFor="maxClients">
{t('simulation.max_clients')}
<RequiredAsterisk />
</CLabel>
<CCol sm="4">
<CInput
id="maxClients"
type="number"
required
value={fields.maxClients.value}
onChange={updateField}
invalid={
fields.maxClients.value < fields.maxClients.min ||
fields.maxClients.value > fields.maxClients.max
}
disabled={disable || !editing}
pattern="[0-9]*"
style={{ width: '100px' }}
/>
<CInvalidFeedback>
{t('common.min_max', {
min: fields.maxClients.min,
max: fields.maxClients.max,
})}
</CInvalidFeedback>
</CCol>
<CLabel className="mb-2" sm="2" col htmlFor="simulationLength">
{t('simulation.length')}
<RequiredAsterisk />
</CLabel>
<CCol sm="4">
<CInput
id="simulationLength"
type="number"
required
value={fields.simulationLength.value}
onChange={updateField}
invalid={
fields.simulationLength.value < fields.simulationLength.min ||
fields.simulationLength.value > fields.simulationLength.max
}
disabled={disable || !editing}
pattern="[0-9]*"
style={{ width: '100px' }}
/>
<CInvalidFeedback>
{t('common.min_max', {
min: fields.simulationLength.min,
max: fields.simulationLength.max,
})}
</CInvalidFeedback>
</CCol>
<CLabel className="mb-2" sm="2" col htmlFor="threads">
{t('simulation.threads')}
<RequiredAsterisk />
</CLabel>
<CCol sm="4">
<CInput
id="threads"
type="number"
required
value={fields.threads.value}
onChange={updateField}
invalid={
fields.threads.value < fields.threads.min || fields.threads.value > fields.threads.max
}
disabled={disable || !editing}
pattern="[0-9]*"
style={{ width: '100px' }}
/>
<CInvalidFeedback>
{t('common.min_max', {
min: fields.threads.min,
max: fields.threads.max,
})}
</CInvalidFeedback>
</CCol>
<CLabel className="mb-2" sm="2" col htmlFor="keepAlive">
{t('simulation.keep_alive')}
<RequiredAsterisk />
</CLabel>
<CCol sm="4">
<CInput
id="keepAlive"
type="number"
required
value={fields.keepAlive.value}
onChange={updateField}
invalid={
fields.keepAlive.value < fields.keepAlive.min ||
fields.keepAlive.value > fields.keepAlive.max
}
disabled={disable || !editing}
pattern="[0-9]*"
style={{ width: '100px' }}
/>
<CInvalidFeedback>
{t('common.min_max', {
min: fields.keepAlive.min,
max: fields.keepAlive.max,
})}
</CInvalidFeedback>
</CCol>
<CLabel className="mb-2" sm="2" col htmlFor="deviceType">
{t('configuration.device_type')}
<RequiredAsterisk />
</CLabel>
<CCol sm="4">
<CSelect
custom
id="deviceType"
type="text"
required
value={fields.deviceType.value}
onChange={updateField}
invalid={fields.deviceType.error}
disabled={disable || !editing}
maxLength="50"
>
<option value="cig_wf160d">cig_wf160d</option>
<option value="cig_wf188">cig_wf188</option>
<option value="cig_wf188n">cig_wf188n</option>
<option value="cig_wf194c">cig_wf194c</option>
<option value="cig_wf194c4">cig_wf194c4</option>
<option value="edgecore_eap101">edgecore_eap101</option>
<option value="edgecore_eap102">edgecore_eap102</option>
<option value="edgecore_ecs4100-12ph">edgecore_ecs4100-12ph</option>
<option value="edgecore_ecw5211">edgecore_ecw5211</option>
<option value="edgecore_ecw5410">edgecore_ecw5410</option>
<option value="edgecore_oap100">edgecore_oap100</option>
<option value="edgecore_spw2ac1200">edgecore_spw2ac1200</option>
<option value="edgecore_spw2ac1200-lan-poe">edgecore_spw2ac1200-lan-poe</option>
<option value="edgecore_ssw2ac2600">edgecore_ssw2ac2600</option>
<option value="hfcl_ion4.yml">hfcl_ion4.yml</option>
<option value="indio_um-305ac">indio_um-305ac</option>
<option value="linksys_e8450-ubi">linksys_e8450-ubi</option>
<option value="linksys_ea6350">linksys_ea6350</option>
<option value="linksys_ea6350-v4">linksys_ea6350-v4</option>
<option value="linksys_ea8300">linksys_ea8300</option>
<option value="mikrotik_nand">mikrotik_nand</option>
<option value="tp-link_ec420-g1">tp-link_ec420-g1</option>
<option value="tplink_cpe210_v3">tplink_cpe210_v3</option>
<option value="tplink_cpe510_v3">tplink_cpe510_v3</option>
<option value="tplink_eap225_outdoor_v1">tplink_eap225_outdoor_v1</option>
<option value="tplink_ec420">tplink_ec420</option>
<option value="tplink_ex227">tplink_ex227</option>
<option value="tplink_ex228">tplink_ex228</option>
<option value="tplink_ex447">tplink_ex447</option>
<option value="wallys_dr40x9">wallys_dr40x9</option>
</CSelect>
</CCol>
</CRow>
</CForm>
);
};
EditSimulationForm.propTypes = {
t: PropTypes.func.isRequired,
show: PropTypes.bool.isRequired,
disable: PropTypes.bool.isRequired,
fields: PropTypes.instanceOf(Object).isRequired,
updateField: PropTypes.func.isRequired,
updateFieldWithKey: PropTypes.func.isRequired,
editing: PropTypes.bool.isRequired,
};
export default EditSimulationForm;

View File

@@ -0,0 +1,81 @@
import React from 'react';
import PropTypes from 'prop-types';
import ReactFlow, { removeElements, MiniMap, Controls, Background } from 'react-flow-renderer';
const EntityTree = ({ elements, setElements, history, toggle, setReactFlowInstance }) => {
const onElementsRemove = (elementsToRemove) => {
setElements((els) => removeElements(elementsToRemove, els));
};
const onClick = (e, el) => {
const split = el.id.split('/');
const type = split[0];
if (type === 'entity' || type === 'world') {
toggle();
history.push(`/entity/${split[1]}`);
} else if (type === 'venue') {
toggle();
history.push(`/venue/${split[1]}`);
}
};
const onLoad = (instance) => {
setReactFlowInstance(instance);
};
return (
<div style={{ height: '80vh', width: '100%' }}>
<ReactFlow
elements={elements}
onElementsRemove={onElementsRemove}
onElementClick={onClick}
deleteKeyCode={null}
onLoad={onLoad}
snapToGrid
snapGrid={[10, 10]}
>
<div className="float-left">
<div
className="align-middle text-center mx-auto"
style={{ backgroundColor: '#0F0A0A', color: 'white', width: '150px' }}
>
<h4 className="align-middle mb-0 font-weight-bold">Root</h4>
</div>
<div
className="align-middle text-center mx-auto"
style={{ backgroundColor: '#2292A4', color: 'white', width: '150px' }}
>
<h4 className="align-middle mb-0 font-weight-bold">Entity</h4>
</div>
<div
className="align-middle text-center mx-auto"
style={{ backgroundColor: '#F5EFED', width: '150px' }}
>
<h4 className="align-middle mb-0 font-weight-bold">Venue</h4>
</div>
</div>
<MiniMap
nodeColor={(n) => {
if (n.style?.background) return n.style.background;
return '#fff';
}}
nodeBorderRadius={5}
/>
<Controls />
<Background color="#aaa" gap={20} />
</ReactFlow>
</div>
);
};
EntityTree.propTypes = {
elements: PropTypes.instanceOf(Array).isRequired,
setElements: PropTypes.func.isRequired,
history: PropTypes.instanceOf(Object).isRequired,
toggle: PropTypes.func.isRequired,
setReactFlowInstance: PropTypes.func.isRequired,
};
export default React.memo(EntityTree);

View File

@@ -18,7 +18,15 @@ const validatePem = (value) =>
(value.includes('---BEGIN CERTIFICATE---') && value.includes('---END CERTIFICATE---')) ||
(value.includes('---BEGIN PRIVATE KEY---') && value.includes('---END PRIVATE KEY---'));
const FileToStringButton = ({ t, save, title, explanations, acceptedFileTypes, size }) => {
const FileToStringButton = ({
t,
save,
title,
explanations,
acceptedFileTypes,
size,
disabled,
}) => {
const [show, toggle] = useToggle(false);
const [value, setValue] = useState('');
const [fileName, setFileName] = useState('');
@@ -74,6 +82,7 @@ const FileToStringButton = ({ t, save, title, explanations, acceptedFileTypes, s
variant="outline"
style={{ height: '35px', width: '35px' }}
size={size}
disabled={disabled}
>
<CIcon content={cilCloudUpload} />
</CButton>
@@ -125,6 +134,7 @@ FileToStringButton.propTypes = {
explanations: PropTypes.string.isRequired,
acceptedFileTypes: PropTypes.string.isRequired,
size: PropTypes.string,
disabled: PropTypes.bool.isRequired,
};
FileToStringButton.defaultProps = {

View File

@@ -2,37 +2,32 @@ import React from 'react';
import PropTypes from 'prop-types';
import { CCardBody, CCol, CInput, CRow } from '@coreui/react';
import { prettyDate, cleanBytesString } from '../../utils/formatting';
import NotesTable from '../NotesTable';
const FirmwareDetailsForm = ({ t, fields, updateFieldsWithId, addNote, editing }) => (
const FirmwareDetailsForm = ({ t, fields, updateFieldsWithId, editing }) => (
<CCardBody className="p-1">
<CRow>
<CCol sm="2">Created</CCol>
<CCol sm="4">{prettyDate(fields.created.value)}</CCol>
<CCol sm="2">Release</CCol>
<CCol sm="2">{t('firmware.release')}</CCol>
<CCol sm="4">{fields.release.value}</CCol>
<CCol sm="2">{t('common.created')}</CCol>
<CCol sm="4">{prettyDate(fields.created.value)}</CCol>
</CRow>
<CRow className="my-3">
<CCol sm="2">Image</CCol>
<CCol sm="4">{fields.image.value}</CCol>
<CCol sm="2">Image Date</CCol>
<CCol sm="2">{t('firmware.image_date')}</CCol>
<CCol sm="4">{prettyDate(fields.imageDate.value)}</CCol>
<CCol sm="2">{t('firmware.size')}</CCol>
<CCol sm="4">{cleanBytesString(fields.size.value)}</CCol>
</CRow>
<CRow className="my-3">
<CCol sm="2">Revision</CCol>
<CCol sm="2">{t('firmware.image')}</CCol>
<CCol sm="4">{fields.image.value}</CCol>
<CCol sm="2">{t('firmware.revision')}</CCol>
<CCol sm="4">{fields.revision.value}</CCol>
<CCol sm="2">Size</CCol>
<CCol sm="4">{cleanBytesString(fields.size.value)}</CCol>
</CRow>
<CRow className="my-3">
<CCol sm="2">URI</CCol>
<CCol sm="4">{fields.uri.value}</CCol>
<CCol sm="2">Owner</CCol>
<CCol sm="4">{fields.owner.value === '' ? t('common.unknown') : fields.owner.value}</CCol>
</CRow>
<CRow className="my-3">
<CCol sm="2" className="mt-2">
Description
{t('user.description')}
</CCol>
<CCol sm="4">
{editing ? (
@@ -46,9 +41,6 @@ const FirmwareDetailsForm = ({ t, fields, updateFieldsWithId, addNote, editing }
<p className="mt-2 mb-0">{fields.description.value}</p>
)}
</CCol>
<CCol>
<NotesTable t={t} notes={fields.notes.value} addNote={addNote} editable={editing} />
</CCol>
</CRow>
</CCardBody>
);
@@ -57,7 +49,6 @@ FirmwareDetailsForm.propTypes = {
t: PropTypes.func.isRequired,
fields: PropTypes.instanceOf(Object).isRequired,
updateFieldsWithId: PropTypes.func.isRequired,
addNote: PropTypes.func.isRequired,
editing: PropTypes.bool.isRequired,
};
export default FirmwareDetailsForm;

View File

@@ -38,7 +38,7 @@ const FirmwareList = ({
{ key: 'size', label: t('firmware.size'), _style: { width: '1%' } },
{ key: 'revision', label: t('firmware.revision'), _style: { width: '1%' } },
{ key: 'uri', label: 'URI' },
{ key: 'show_details', label: '', _style: { width: '5%' } },
{ key: 'show_details', label: '', _style: { width: '1%' } },
];
const getShortRevision = (revision) => {
@@ -54,7 +54,7 @@ const FirmwareList = ({
return (
<CCard className="m-0">
<CCardHeader className="dark-header">
<CCardHeader className="p-1">
<div className="d-flex flex-row-reverse">
<div className="px-3">
<CSwitch
@@ -113,7 +113,7 @@ const FirmwareList = ({
),
uri: (item) => (
<td className="align-middle">
<div style={{ width: 'calc(45vw)' }}>
<div style={{ width: 'calc(50vw)' }}>
<div className="text-truncate align-middle">
<CopyToClipboardButton key={item.uri} t={t} size="sm" content={item.uri} />
<CPopover content={item.uri}>
@@ -125,14 +125,16 @@ const FirmwareList = ({
),
show_details: (item) => (
<td className="text-center align-middle">
<CButton
size="sm"
color="primary"
variant="outline"
onClick={() => toggleEditModal(item.id)}
>
<CIcon content={cilSearch} />
</CButton>
<CPopover content={t('common.details')}>
<CButton
size="sm"
color="primary"
variant="outline"
onClick={() => toggleEditModal(item.id)}
>
<CIcon content={cilSearch} />
</CButton>
</CPopover>
</td>
),
}}

View File

@@ -4,7 +4,7 @@ import { CPopover } from '@coreui/react';
import { formatDaysAgo, prettyDate } from 'utils/formatting';
const FormattedDate = ({ date }) => (
<CPopover content={prettyDate(date)}>
<CPopover content={prettyDate(date)} advancedOptions={{ animation: false }}>
<span className="d-inline-block">{date === 0 ? '-' : formatDaysAgo(date)}</span>
</CPopover>
);

View File

@@ -5,7 +5,7 @@ import { CButton, CPopover } from '@coreui/react';
const HideTextButton = ({ t, toggle, show, size }) => (
<CPopover content={t('user.show_hide_password')}>
<CButton onClick={toggle} size={size} className="pt-0">
<CButton onClick={toggle} size={size} className="py-0">
<CIcon name={show ? 'cil-envelope-open' : 'cil-envelope-closed'} />
</CButton>
</CPopover>

View File

@@ -203,7 +203,7 @@ const InventoryTable = ({
<CIcon name="cil-spreadsheet" content={cilSpreadsheet} size="sm" />
</CButton>
</CPopover>
<CPopover content="View Tag">
<CPopover content={t('common.details')}>
<CButton
color="primary"
variant="outline"

View File

@@ -1,7 +1,7 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { CButton, CDataTable, CLink, CPopover, CButtonToolbar } from '@coreui/react';
import { cilPencil, cilPlus } from '@coreui/icons';
import { cilMagnifyingGlass, cilPlus } from '@coreui/icons';
import CIcon from '@coreui/icons-react';
import ReactTooltip from 'react-tooltip';
import DeleteButton from './DeleteButton';
@@ -118,7 +118,7 @@ const LocationTable = ({
deleteLocation={deleteLocation}
hideTooltips={hideTooltips}
/>
<CPopover content={t('common.edit')}>
<CPopover content={t('common.details')}>
<CButton
color="primary"
variant="outline"
@@ -128,7 +128,7 @@ const LocationTable = ({
onClick={() => toggleEditModal(item.id)}
style={{ width: '33px', height: '30px' }}
>
<CIcon name="cil-pencil" content={cilPencil} size="sm" />
<CIcon name="cil-magnifying-glass" content={cilMagnifyingGlass} size="sm" />
</CButton>
</CPopover>
</CButtonToolbar>

View File

@@ -0,0 +1,273 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import ReactPaginate from 'react-paginate';
import {
CCardBody,
CDataTable,
CButton,
CCard,
CCardHeader,
CRow,
CCol,
CPopover,
CSelect,
CButtonToolbar,
CButtonClose,
} from '@coreui/react';
import { cilTrash, cilSearch, cilMediaPlay } from '@coreui/icons';
import CIcon from '@coreui/icons-react';
import ReactTooltip from 'react-tooltip';
import { v4 as createUuid } from 'uuid';
import styles from './index.module.scss';
const SimulationTable = ({
currentPage,
simulations,
simulationsPerPage,
loading,
updateSimulationsPerPage,
pageCount,
updatePage,
t,
deleteSimulation,
toggleEdit,
startSim,
}) => {
const columns = [
{ key: 'name', label: t('user.name'), filter: false, sorter: false, _style: { width: '10%' } },
{
key: 'gateway',
label: t('simulation.gateway'),
filter: false,
sorter: false,
_style: { width: '10%' },
},
{ key: 'deviceType', label: t('firmware.device_type'), filter: false, sorter: false },
{ key: 'devices', label: t('common.devices'), filter: false, sorter: false },
{ key: 'macPrefix', label: t('simulation.mac_prefix'), filter: false, sorter: false },
{ key: 'stateInterval', label: t('simulation.state_interval'), filter: false, sorter: false },
{
key: 'minAssociations',
label: t('simulation.min_associations'),
filter: false,
sorter: false,
},
{
key: 'maxAssociations',
label: t('simulation.max_associations'),
filter: false,
sorter: false,
},
{ key: 'minClients', label: t('simulation.min_clients'), filter: false, sorter: false },
{ key: 'maxClients', label: t('simulation.max_clients'), filter: false, sorter: false },
{ key: 'simulationLength', label: t('simulation.length'), filter: false, sorter: false },
{ key: 'actions', label: '', filter: false, sorter: false, _style: { width: '1%' } },
];
const hideTooltips = () => ReactTooltip.hide();
const escFunction = (event) => {
if (event.keyCode === 27) {
hideTooltips();
}
};
useEffect(() => {
document.addEventListener('keydown', escFunction, false);
return () => {
document.removeEventListener('keydown', escFunction, false);
};
}, []);
const deleteButton = ({ name, id }) => {
const tooltipId = createUuid();
return (
<CPopover content={t('common.delete')}>
<div>
<CButton
color="primary"
variant="outline"
shape="square"
size="sm"
className="mx-1"
data-tip
data-for={tooltipId}
data-event="click"
style={{ width: '33px', height: '30px' }}
>
<CIcon name="cil-trash" content={cilTrash} size="sm" />
</CButton>
<ReactTooltip
id={tooltipId}
place="top"
effect="solid"
globalEventOff="click"
clickable
className={[styles.deleteTooltip, 'tooltipRight'].join(' ')}
border
borderColor="#321fdb"
arrowColor="white"
overridePosition={({ left, top }) => {
const element = document.getElementById(tooltipId);
const tooltipWidth = element ? element.offsetWidth : 0;
const newLeft = left - tooltipWidth * 0.25;
return { top, left: newLeft };
}}
>
<CCardHeader color="primary" className={styles.tooltipHeader}>
{t('simulation.delete_simulation', { name })}
<CButtonClose
style={{ color: 'white' }}
onClick={(e) => {
e.target.parentNode.parentNode.classList.remove('show');
hideTooltips();
}}
/>
</CCardHeader>
<CCardBody>
<CRow>
<CCol>
<CButton
data-toggle="dropdown"
variant="outline"
color="danger"
onClick={() => deleteSimulation(id)}
block
>
{t('common.confirm')}
</CButton>
</CCol>
</CRow>
</CCardBody>
</ReactTooltip>
</div>
</CPopover>
);
};
return (
<>
<CCard className="m-0 p-0">
<CCardBody className="p-0">
<CDataTable
addTableClasses="ignore-overflow table-sm"
items={simulations ?? []}
fields={columns}
hover
border
loading={loading}
scopedSlots={{
name: (item) => <td className="align-middle">{item.name}</td>,
gateway: (item) => <td className="align-middle">{item.gateway}</td>,
deviceType: (item) => <td className="align-middle">{item.deviceType}</td>,
devices: (item) => <td className="align-middle">{item.devices}</td>,
macPrefix: (item) => <td className="align-middle">{item.macPrefix}</td>,
healthCheckInterval: (item) => (
<td className="align-middle">{item.healthCheckInterval}</td>
),
stateInterval: (item) => <td className="align-middle">{item.stateInterval}</td>,
minAssociations: (item) => <td className="align-middle">{item.minAssociations}</td>,
maxAssociations: (item) => <td className="align-middle">{item.maxAssociations}</td>,
minClients: (item) => <td className="align-middle">{item.minClients}</td>,
maxClients: (item) => <td className="align-middle">{item.maxClients}</td>,
simulationLength: (item) => <td className="align-middle">{item.simulationLength}</td>,
actions: (item) => (
<td className="text-center align-middle">
<CButtonToolbar
role="group"
className="justify-content-center"
style={{ width: '150px' }}
>
<CPopover content={t('simulation.run_simulation')}>
<CButton
color="primary"
variant="outline"
shape="square"
size="sm"
className="mx-1"
style={{ width: '33px', height: '30px' }}
onClick={() => startSim(item.id)}
>
<CIcon name="cil-media-play" content={cilMediaPlay} size="sm" />
</CButton>
</CPopover>
{deleteButton(item)}
<CPopover content={t('configuration.details')}>
<CButton
color="primary"
variant="outline"
shape="square"
size="sm"
className="mx-1"
style={{ width: '33px', height: '30px' }}
onClick={() => toggleEdit(item.id)}
>
<CIcon name="cil-search" content={cilSearch} size="sm" />
</CButton>
</CPopover>
</CButtonToolbar>
</td>
),
}}
/>
<div className="d-flex flex-row pl-3">
<div className="pr-3">
<ReactPaginate
previousLabel="← Previous"
nextLabel="Next →"
pageCount={pageCount}
onPageChange={updatePage}
forcePage={Number(currentPage)}
breakClassName="page-item"
breakLinkClassName="page-link"
containerClassName="pagination"
pageClassName="page-item"
pageLinkClassName="page-link"
previousClassName="page-item"
previousLinkClassName="page-link"
nextClassName="page-item"
nextLinkClassName="page-link"
activeClassName="active"
/>
</div>
<p className="pr-2 mt-1">{t('common.items_per_page')}</p>
<div style={{ width: '100px' }} className="px-2">
<CSelect
custom
defaultValue={simulationsPerPage}
onChange={(e) => updateSimulationsPerPage(e.target.value)}
disabled={loading}
>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
</CSelect>
</div>
</div>
</CCardBody>
</CCard>
</>
);
};
SimulationTable.propTypes = {
startSim: PropTypes.func.isRequired,
currentPage: PropTypes.string,
simulations: PropTypes.instanceOf(Array).isRequired,
updateSimulationsPerPage: PropTypes.func.isRequired,
pageCount: PropTypes.number.isRequired,
updatePage: PropTypes.func.isRequired,
simulationsPerPage: PropTypes.string.isRequired,
t: PropTypes.func.isRequired,
loading: PropTypes.bool.isRequired,
toggleEdit: PropTypes.func.isRequired,
deleteSimulation: PropTypes.func.isRequired,
};
SimulationTable.defaultProps = {
currentPage: '0',
};
export default React.memo(SimulationTable);

View File

@@ -0,0 +1,16 @@
.deleteTooltip {
opacity: 1 !important;
padding: 0px 0px 0px 0px !important;
border-radius: 1rem !important;
background-color: #fff !important;
border-color: #321fdb !important;
font-size: 0.875rem !important;
font-weight: 400 !important;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2) !important;
width: 300px;
}
.tooltipHeader {
border-top-left-radius: 1rem !important;
border-top-right-radius: 1rem !important;
}

View File

@@ -169,6 +169,7 @@ const UserListTable = ({
onClick={() => handleDeleteClick(item.Id)}
color="primary"
variant="outline"
shape="square"
size="sm"
>
<CIcon content={cilTrash} size="sm" />

View File

@@ -14,6 +14,7 @@ export { default as AddEntityForm } from './components/AddEntityForm';
export { default as AddInventoryTagForm } from './components/AddInventoryTagForm';
export { default as AddLocationForm } from './components/AddLocationForm';
export { default as AddressEditor } from './components/AddressEditor';
export { default as AddSimulationForm } from './components/AddSimulationForm';
export { default as ApiStatusCard } from './components/ApiStatusCard';
export { default as Avatar } from './components/Avatar';
export { default as CompactNotesTable } from './components/CompactNotesTable';
@@ -29,6 +30,7 @@ export { default as ConfigurationSelect } from './components/Configuration/Selec
export { default as ConfigurationStringField } from './components/Configuration/StringField';
export { default as ConfigurationToggle } from './components/Configuration/Toggle';
export { default as ConfirmFooter } from './components/ConfirmFooter';
export { default as ConfirmStopEditingButton } from './components/ConfirmStopEditingButton';
export { default as ContactTable } from './components/ContactTable';
export { default as CopyToClipboardButton } from './components/CopyToClipboardButton';
export { default as CreateUserForm } from './components/CreateUserForm';
@@ -45,8 +47,10 @@ export { default as EditEntityForm } from './components/EditEntityForm';
export { default as EditInventoryTagForm } from './components/EditInventoryTagForm';
export { default as EditLocationForm } from './components/EditLocationForm';
export { default as EditMyProfile } from './components/EditMyProfile';
export { default as EditSimulationForm } from './components/EditSimulationForm';
export { default as EditUserForm } from './components/EditUserForm';
export { default as EditUserModal } from './components/EditUserModal';
export { default as EntityTree } from './components/EntityTree';
export { default as EventQueueModal } from './components/EventQueueModal';
export { default as InventoryTable } from './components/InventoryTable';
export { default as FileToStringButton } from './components/FileToStringButton';
@@ -62,6 +66,7 @@ export { default as LoadingButton } from './components/LoadingButton';
export { default as NetworkDiagram } from './components/NetworkDiagram';
export { default as NotesTable } from './components/NotesTable';
export { default as RadioAnalysisTable } from './components/RadioAnalysisTable';
export { default as SimulationTable } from './components/SimulationTable';
export { default as UserListTable } from './components/UserListTable';
export { default as VenueTable } from './components/VenueTable';
export { default as WifiAnalysisTable } from './components/WifiAnalysisTable';

View File

@@ -30,6 +30,8 @@ const Header = ({
user,
avatar,
hideBreadcrumb,
extraButton,
hideSidebarButton,
}) => {
const [translatedRoutes, setTranslatedRoutes] = useState(routes);
@@ -49,14 +51,25 @@ const Header = ({
return (
<CHeader withSubheader>
<CToggler inHeader className="ml-md-3 d-lg-none" onClick={toggleSidebarMobile} />
<CToggler inHeader className="ml-3 d-md-down-none" onClick={toggleSidebar} />
{hideSidebarButton ? null : (
<>
<CToggler inHeader className="ml-md-3 d-lg-none" onClick={toggleSidebarMobile} />
<CToggler inHeader className="ml-3 d-md-down-none" onClick={toggleSidebar} />
</>
)}
<CHeaderBrand className="mx-auto d-lg-none" to="/">
<img src={logo} alt="OpenWifi" />
<img
src={logo}
alt="OpenWifi"
className="c-sidebar-brand-full"
style={{ height: '75px', width: '175px' }}
/>
</CHeaderBrand>
<CHeaderNav className="d-md-down-none mr-auto" />
<CHeaderNav>{extraButton}</CHeaderNav>
<CHeaderNav className="px-3">
<LanguageSwitcher i18n={i18n} />
</CHeaderNav>
@@ -78,12 +91,14 @@ const Header = ({
</CDropdown>
</CHeaderNav>
<CSubheader hidden={hideBreadcrumb} className="px-3 justify-content-between">
<CBreadcrumbRouter
className="border-0 c-subheader-nav m-0 px-0 px-md-3"
routes={translatedRoutes}
/>
</CSubheader>
{hideBreadcrumb ? null : (
<CSubheader hidden={hideBreadcrumb} className="px-3 justify-content-between">
<CBreadcrumbRouter
className="border-0 c-subheader-nav m-0 px-0 px-md-3"
routes={translatedRoutes}
/>
</CSubheader>
)}
</CHeader>
);
};
@@ -101,10 +116,14 @@ Header.propTypes = {
user: PropTypes.instanceOf(Object).isRequired,
avatar: PropTypes.string.isRequired,
hideBreadcrumb: PropTypes.bool,
extraButton: PropTypes.node,
hideSidebarButton: PropTypes.bool,
};
Header.defaultProps = {
extraButton: null,
hideBreadcrumb: false,
hideSidebarButton: false,
};
export default React.memo(Header);

View File

@@ -13,16 +13,26 @@ import {
import PropTypes from 'prop-types';
import styles from './index.module.scss';
const Sidebar = ({ showSidebar, setShowSidebar, logo, options, redirectTo }) => (
const Sidebar = ({
showSidebar,
setShowSidebar,
logo,
options,
redirectTo,
logoHeight,
logoWidth,
}) => (
<CSidebar show={showSidebar} onShowChange={(val) => setShowSidebar(val)}>
<CSidebarBrand className="d-md-down-none" to={redirectTo}>
<img
className={[styles.sidebarImgFull, 'c-sidebar-brand-full'].join(' ')}
style={{ height: logoHeight ?? undefined, width: logoWidth ?? undefined }}
src={logo}
alt="OpenWifi"
/>
<img
className={[styles.sidebarImgMinimized, 'c-sidebar-brand-minimized'].join(' ')}
style={{ height: logoHeight ?? undefined, width: logoWidth ?? undefined }}
src={logo}
alt="OpenWifi"
/>
@@ -48,6 +58,13 @@ Sidebar.propTypes = {
logo: PropTypes.string.isRequired,
options: PropTypes.arrayOf(Object).isRequired,
redirectTo: PropTypes.string.isRequired,
logoHeight: PropTypes.string,
logoWidth: PropTypes.string,
};
Sidebar.defaultProps = {
logoHeight: null,
logoWidth: null,
};
export default React.memo(Sidebar);