From e995f28a073a03fdc31bfc43be1ec48413872be0 Mon Sep 17 00:00:00 2001 From: burgerni10 Date: Wed, 7 Dec 2022 16:34:52 +0100 Subject: [PATCH] feat(config): add nanoid for scan modes, proxies and points and retrieve specific south/north from api call --- src/engine/base-engine.js | 20 + src/frontend/components/oib-form/index.js | 2 + .../components/oib-form/oib-select.jsx | 2 +- src/frontend/components/points-component.jsx | 202 +- src/frontend/components/table/table-rows.jsx | 2 +- src/frontend/context/config-context.spec.jsx | 4 +- src/frontend/home/engine-menu.jsx | 24 +- src/frontend/home/new-north.jsx | 28 +- src/frontend/home/new-north.spec.jsx | 10 +- src/frontend/home/new-south.jsx | 28 +- src/frontend/home/new-south.spec.jsx | 10 +- src/frontend/index.jsx | 4 +- src/frontend/north/form/caching.jsx | 145 + src/frontend/service/apis.js | 19 +- src/frontend/service/apis.spec.js | 12 +- .../configure-points.spec.jsx.snap | 9304 ----------------- src/frontend/south/configure-points.jsx | 105 +- src/frontend/south/configure-points.spec.jsx | 396 - src/migration/migration-rules.js | 16 + src/migration/migration.service.js | 2 +- .../controllers/engine.controller.js | 30 + src/web-server/routes/index.js | 7 +- 22 files changed, 418 insertions(+), 9954 deletions(-) create mode 100644 src/frontend/north/form/caching.jsx delete mode 100644 src/frontend/south/__snapshots__/configure-points.spec.jsx.snap delete mode 100644 src/frontend/south/configure-points.spec.jsx diff --git a/src/engine/base-engine.js b/src/engine/base-engine.js index 6f2efbe662..e973e86957 100644 --- a/src/engine/base-engine.js +++ b/src/engine/base-engine.js @@ -175,6 +175,16 @@ export default class BaseEngine { })) } + /** + * Retrieve a south connector from the config + * @param {String} id - The South ID + * @returns {Object} - The connector + */ + getSouth(id) { + const { southConfig } = this.configService.getConfig() + return southConfig.find((southConnector) => southConnector.id === id) + } + /** * Return the North connector * @param {Object} configuration - The North connector configuration @@ -205,4 +215,14 @@ export default class BaseEngine { category, })) } + + /** + * Retrieve a north connector from the config + * @param {String} id - The North ID + * @returns {Object} - The connector + */ + getNorth(id) { + const { northConfig } = this.configService.getConfig() + return northConfig.find((northConnector) => northConnector.id === id) + } } diff --git a/src/frontend/components/oib-form/index.js b/src/frontend/components/oib-form/index.js index 2187bbc4ac..8ea3a0d376 100644 --- a/src/frontend/components/oib-form/index.js +++ b/src/frontend/components/oib-form/index.js @@ -11,6 +11,7 @@ import OibTitle from './oib-title.jsx' import OibAuthentication from './oib-authentication.jsx' import OibTimezone from './oib-timezone.jsx' import OibCron from './oib-cron.jsx' +import OibDate from './oib-date.jsx' export { OibCheckbox, @@ -25,5 +26,6 @@ export { OibTitle, OibAuthentication, OibTimezone, + OibDate, OibCron, } diff --git a/src/frontend/components/oib-form/oib-select.jsx b/src/frontend/components/oib-form/oib-select.jsx index 5ed2889d5a..c791838539 100644 --- a/src/frontend/components/oib-form/oib-select.jsx +++ b/src/frontend/components/oib-form/oib-select.jsx @@ -33,7 +33,7 @@ const OibSelect = ({ label, help, valid, value, options, optionsLabel, name, onC invalid={validCheck !== null} > {options.map((o, index) => ( - ))} diff --git a/src/frontend/components/points-component.jsx b/src/frontend/components/points-component.jsx index 1ccdd26648..9758d4992d 100644 --- a/src/frontend/components/points-component.jsx +++ b/src/frontend/components/points-component.jsx @@ -1,59 +1,42 @@ -import React from 'react' +import React, { useEffect } from 'react' import { Button, Input } from 'reactstrap' import PropTypes from 'prop-types' +import objectPath from 'object-path' +import { nanoid } from 'nanoid' import Table from './table/table.jsx' import TablePagination from './table/table-pagination.jsx' import Modal from './modal.jsx' -import SouthSchemas from '../south/south-types.jsx' import * as Controls from './oib-form/index.js' import utils from '../helpers/utils.js' -import validation from '../south/form/south.validation.js' + +// Max points on one page +const MAX_ON_PAGE = 10 +// This value will be used to calculate the amount of max pagination displayed +const MAX_PAGINATION_DISPLAY = 11 const PointsComponent = ({ - southId, - points: pointsOrdered, - southType, - handleDeleteAllPoint, - handleAdd, - handleDelete, - handleImportPoints, - onUpdate, + prefix, + schema, + points, + onChange, }) => { const [filterText, setFilterText] = React.useState('') // used to limit the list of points const [selectedPage, setSelectedPage] = React.useState(1) - // max points on one page - const MAX_ON_PAGE = 10 - // this value will be used to calculate the amount of max pagination displayed - const MAX_PAGINATION_DISPLAY = 11 - const pageOffset = selectedPage * MAX_ON_PAGE - MAX_ON_PAGE - - // add virtualIndex for each point for helping the filter - const points = pointsOrdered.slice().map((item, index) => ({ virtualIndex: index, ...item })).reverse() - - // filter - const filteredPoints = filterText - ? points.filter( - (point) => { - // remove the virtualIndex from filterable attributes - const filterableAttributes = { ...point } - delete filterableAttributes.virtualIndex - return Object.values(filterableAttributes).findIndex((element) => element - .toString() - .toLowerCase() - .includes(filterText.toLowerCase())) >= 0 - }, - ) : points + const [allPoints, setAllPoints] = React.useState(points) + const [filteredPoints, setFilteredPoints] = React.useState(points) + const [tableRows, setTableRows] = React.useState([]) - /** - * @param {number} index the index of a point in the table - * @returns {number} the index in the config file of the chosen point - */ - const findIndexBasedOnVirtualIndex = (index) => { - const paginatedIndex = MAX_ON_PAGE * (selectedPage - 1) + index - const pointToOperate = filteredPoints[paginatedIndex] - return pointsOrdered.findIndex((point) => point.pointId === pointToOperate.pointId) - } + // configure help if exists + const pointsWithHelp = Object.entries(schema.points).filter(([name, value]) => name && value.help) + const tableHelps = pointsWithHelp.length > 0 && pointsWithHelp.map(([name, value]) => ( +
+ {`${value.label || name}: `} + {value.help} +
+ )) + // configure table header and rows + const tableHeaders = Object.entries(schema.points).map(([_name, value]) => value.label || _name) /** * Sets the filter text @@ -66,29 +49,39 @@ const PointsComponent = ({ } /** - * add point + * Add a point to the list * @returns {void} */ const handleAddPoint = () => { + const newPoint = Object.entries(schema.points).map(([name]) => name).reduce((previousValue, currentValue) => { + objectPath.set(previousValue, currentValue, '') + return previousValue + }, {}) + newPoint.id = nanoid() + const newAllPoints = [...allPoints] + newAllPoints.unshift(newPoint) + setAllPoints(newAllPoints) setSelectedPage(1) // jump to first page, to see new row - handleAdd(Object.entries(SouthSchemas[southType].points).map(([name]) => name)) + onChange(prefix, newAllPoints) + } + + const handleChange = (idAndName, value) => { + const [id, name] = idAndName.split('.') + const newAllPoints = [...allPoints] + const pointToUpdate = newAllPoints.find((point) => point.id === id) + pointToUpdate[name] = value + setAllPoints(newAllPoints) } /** - * Delete point - * @param {number} index the index of point + * Delete point by its ID + * @param {string} id the ID of point * @returns {void} */ - const handleDeletePoint = (index) => { - const indexInConfig = findIndexBasedOnVirtualIndex(index) - handleDelete(indexInConfig) - } - - const onChange = (name, value, validity) => { - // add pageOffset before dispatch the update to update the correct point (pagination) - const index = Number(name.match(/[0-9]+/g)) - const pathWithPageOffset = name.replace(/[0-9]+/g, `${findIndexBasedOnVirtualIndex(index)}`) - onUpdate(pathWithPageOffset, value, validity) + const handleDeletePoint = (id) => { + const newAllPoints = allPoints.filter((point) => point.id !== id) + setAllPoints(newAllPoints) + onChange(prefix, newAllPoints) } /** @@ -96,7 +89,10 @@ const PointsComponent = ({ * @returns {void} */ const handleExportPoints = () => { - const csvString = utils.createCSV(pointsOrdered) + const csvString = utils.createCSV(allPoints.map((point) => { + delete point.id + return point + })) const element = document.createElement('a') const file = new Blob([csvString], { type: 'text/csv' }) element.href = URL.createObjectURL(file) @@ -106,39 +102,51 @@ const PointsComponent = ({ document.body.removeChild(element) } - const southSchema = SouthSchemas[southType] - // configure help if exists - const pointsWithHelp = Object.entries(southSchema.points).filter(([name, value]) => name && value.help) - const tableHelps = pointsWithHelp.length > 0 && pointsWithHelp.map(([name, value]) => ( -
- {`${value.label || name}: `} - {value.help} -
- )) - // configure table header and rows - const tableHeaders = Object.entries(southSchema.points).map(([_name, value]) => value.label || _name) - - // paging - const pagedPoints = filteredPoints.filter((_, index) => index >= pageOffset && index < selectedPage * MAX_ON_PAGE) - const tableRows = pagedPoints.map((point, index) => Object.entries(southSchema.points).map(([key, value]) => { - const { type, ...rest } = value - const Control = Controls[type] - rest.value = point[key] - rest.label = null // remove field title in table rows - rest.help = null // remove help in table rows - // check if the key must be unique and extend already existing validation with isUnique check - if (southSchema.points[key].unique) { - const indexOffset = (selectedPage - 1) * MAX_ON_PAGE - const pointIds = points.filter((_point) => _point.virtualIndex !== filteredPoints[indexOffset + index].virtualIndex).map((p) => p[key]) - const oldValid = rest.valid.bind({}) - rest.valid = (val) => oldValid(val) || validation.points.isUnique(val, pointIds) || validation.points.noUnintendedTrailingSpaces(val) + /** + * Send the imported file content to the backend + * @param {Object} file the file returned by input + * @returns {void} + */ + const handleImportPoints = async (file) => { + try { + const text = await utils.readFileContent(file) + const newAllPoints = await utils.parseCSV(text) + setAllPoints(newAllPoints.map((point) => ({ ...point, id: nanoid() }))) + onChange(prefix, newAllPoints) + } catch (error) { + console.error(error) } - const name = `points.${index}.${key}` - return ( - /* eslint-disable-next-line react/jsx-props-no-spreading */ - { name, value: } - ) - })) + } + + const handleDeleteAllPoints = () => { + setAllPoints([]) + onChange(prefix, []) + } + + useEffect(() => { + const pageOffset = selectedPage * MAX_ON_PAGE - MAX_ON_PAGE + const newFilteredPoints = filterText !== '' ? allPoints.filter((point) => Object.values(point).findIndex((element) => element + .toString() + .toLowerCase() + .includes(filterText.toLowerCase())) >= 0) : allPoints + + const newTableRows = newFilteredPoints.filter((_, index) => index >= pageOffset && index < selectedPage * MAX_ON_PAGE) + .map((point) => Object.entries(schema.points).map(([key, value]) => { + const { type, ...rest } = value + const Control = Controls[type] + rest.value = point[key] + rest.label = null // remove field title in table rows + rest.help = null // remove help in table rows + return ( + // id is used to remove the point from its id with handle delete + /* eslint-disable-next-line react/jsx-props-no-spreading */ + { id: point.id, name: `${point.id}.${key}`, value: } + ) + })) + + setFilteredPoints(newFilteredPoints) + setTableRows(newTableRows) + }, [filterText, selectedPage, allPoints]) return (
@@ -151,7 +159,7 @@ const PointsComponent = ({ onChange={(_name, val) => updateFilterText(val)} /> - {filteredPoints.length && ( + {filteredPoints.length > 0 && ( {(confirm) => (
-
@@ -193,14 +201,10 @@ const PointsComponent = ({ } PointsComponent.propTypes = { - southId: PropTypes.string.isRequired, points: PropTypes.arrayOf(PropTypes.object).isRequired, - southType: PropTypes.string.isRequired, - handleDeleteAllPoint: PropTypes.func.isRequired, - handleAdd: PropTypes.func.isRequired, - handleDelete: PropTypes.func.isRequired, - handleImportPoints: PropTypes.func.isRequired, - onUpdate: PropTypes.func.isRequired, + prefix: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, + schema: PropTypes.object.isRequired, } export default PointsComponent diff --git a/src/frontend/components/table/table-rows.jsx b/src/frontend/components/table/table-rows.jsx index 08dfca60ff..5073aa4f3f 100644 --- a/src/frontend/components/table/table-rows.jsx +++ b/src/frontend/components/table/table-rows.jsx @@ -105,7 +105,7 @@ const TableRows = ({ { - handleDelete(index) + handleDelete(row[0].id || index) })} /> )} diff --git a/src/frontend/context/config-context.spec.jsx b/src/frontend/context/config-context.spec.jsx index c9870db15a..ce2e89c90b 100644 --- a/src/frontend/context/config-context.spec.jsx +++ b/src/frontend/context/config-context.spec.jsx @@ -13,10 +13,10 @@ import { ConfigProvider, reducer } from './config-context.jsx' global.fetch = jest.fn().mockImplementation((uri) => { let jsonString switch (uri) { - case '/config/schemas/north': + case '/api/installed-north': jsonString = JSON.stringify(['a', 'b', 'c']) break - case '/config/schemas/south': + case '/api/installed-south': jsonString = JSON.stringify(['d', 'e', 'f']) break case '/config': diff --git a/src/frontend/home/engine-menu.jsx b/src/frontend/home/engine-menu.jsx index 220bf007a8..4c550e5cf4 100644 --- a/src/frontend/home/engine-menu.jsx +++ b/src/frontend/home/engine-menu.jsx @@ -3,11 +3,15 @@ import PropTypes from 'prop-types' import { FaEllipsisV } from 'react-icons/fa' import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap' import { useNavigate } from 'react-router-dom' +import { nanoid } from 'nanoid' import ConfirmationModal from '../components/confirmation-modal.jsx' import NewSouth from './new-south.jsx' import NewNorth from './new-north.jsx' +import { ConfigContext } from '../context/config-context.jsx' const EngineMenu = ({ onRestart, onShutdown }) => { + const { dispatchNewConfig } = React.useContext(ConfigContext) + const [southModal, setSouthModal] = useState(false) const [northModal, setNorthModal] = useState(false) const [restartShow, setRestartShow] = useState(false) @@ -16,6 +20,22 @@ const EngineMenu = ({ onRestart, onShutdown }) => { const toggle = () => setDropdownOpen((prevState) => !prevState) const navigate = useNavigate() + const callback = (connector, type, name) => { + const myNewId = nanoid() + + dispatchNewConfig({ + type: 'addRow', + name: connector, + value: { + id: myNewId, + name, + type, + enabled: false, + }, + }) + navigate(`/${connector}/${myNewId}`) + } + return ( <> { - setSouthModal(false)} /> - setNorthModal(false)} /> + setSouthModal(false)} callback={callback} /> + setNorthModal(false)} callback={callback} /> ) } diff --git a/src/frontend/home/new-north.jsx b/src/frontend/home/new-north.jsx index 8016bdbe75..433e7a18eb 100644 --- a/src/frontend/home/new-north.jsx +++ b/src/frontend/home/new-north.jsx @@ -1,20 +1,18 @@ import React, { useState } from 'react' -import { nanoid } from 'nanoid' import PropTypes from 'prop-types' import { Button, Container, Row, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap' -import { useNavigate } from 'react-router-dom' import { ConfigContext } from '../context/config-context.jsx' import validationNorth from '../north/form/north.validation.js' import { OibText } from '../components/oib-form/index.js' import imageCategories from './image-categories.js' const NewNorth = ({ - modal, + displayModal, toggle, + callback, }) => { const { newConfig, - dispatchNewConfig, northTypes, } = React.useContext(ConfigContext) const [name, setName] = React.useState('') @@ -23,7 +21,6 @@ const NewNorth = ({ const [nameError, setNameError] = React.useState(null) const [northType, setNorthType] = React.useState(null) const northConnectors = newConfig?.north ?? [] - const navigate = useNavigate() const northCategoryList = northTypes ? [...new Set(northTypes.map((e) => e.category))] : [] @@ -39,31 +36,18 @@ const NewNorth = ({ } if (!validationNorth.north.isValidName(name, northConnectors.map((north) => north.name)) && name !== '' && northType !== null) { - const myNewId = nanoid() - - dispatchNewConfig({ - type: 'addRow', - name: 'north', - value: { - id: myNewId, - name, - type: northType, - enabled: false, - }, - }) - toggle() setNorthType(null) setName('') setNorthTypeError(null) setNameError(null) - navigate(`/north/${myNewId}`) + callback('north', northType, name) } } return ( { test('display NewNorth page based on config', async () => { act(() => { root.render( - false} />, + false} callback={() => null} />, ) }) expect(container).toMatchSnapshot() @@ -96,7 +96,7 @@ describe('NewNorth', () => { act(() => { root.render( - false} />, + false} callback={() => null} />, ) }) expect(container).toMatchSnapshot() @@ -130,7 +130,7 @@ describe('NewNorth', () => { act(() => { root.render( - false} />, + false} callback={() => null} />, ) }) expect(container).toMatchSnapshot() @@ -163,7 +163,7 @@ describe('NewNorth', () => { }) act(() => { - root.render( false} />) + root.render( false} callback={() => null} />) }) expect(container).toMatchSnapshot() @@ -196,7 +196,7 @@ describe('NewNorth', () => { act(() => { root.render( - false} />, + false} callback={() => null} />, ) }) expect(container).toMatchSnapshot() diff --git a/src/frontend/home/new-south.jsx b/src/frontend/home/new-south.jsx index 978ae3ea7a..4f18229469 100644 --- a/src/frontend/home/new-south.jsx +++ b/src/frontend/home/new-south.jsx @@ -1,20 +1,18 @@ import React, { useState } from 'react' -import { nanoid } from 'nanoid' import PropTypes from 'prop-types' import { Row, Container, Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap' -import { useNavigate } from 'react-router-dom' import { ConfigContext } from '../context/config-context.jsx' import validationSouth from '../south/form/south.validation.js' import { OibText } from '../components/oib-form/index.js' import imageCategories from './image-categories.js' const NewSouth = ({ - modal, + displayModal, toggle, + callback, }) => { const { newConfig, - dispatchNewConfig, southTypes, } = React.useContext(ConfigContext) const [name, setName] = React.useState('') @@ -23,11 +21,10 @@ const NewSouth = ({ const [nameError, setNameError] = React.useState(null) const [southType, setSouthType] = React.useState(null) const southConnectors = newConfig?.south ?? [] - const navigate = useNavigate() const southCategoryList = southTypes ? [...new Set(southTypes.map((e) => e.category))] : [] - const addSouth = () => { + const addSouth = async () => { if (southType === null && name !== '') { setSouthTypeError('A South type must be selected') } @@ -38,29 +35,18 @@ const NewSouth = ({ setSouthTypeError('A name must be specified and a South type must be selected') } if (!validationSouth.south.isValidName(name, southConnectors.map((south) => south.name)) && name !== '' && southType !== null) { - const myNewId = nanoid() - dispatchNewConfig({ - type: 'addRow', - name: 'south', - value: { - id: myNewId, - name, - type: southType, - enabled: false, - }, - }) toggle() setSouthType(null) setName('') setSouthTypeError(null) setNameError(null) - navigate(`/south/${myNewId}`) + callback('south', southType, name) } } return ( { test('display NewSouth page based on config', async () => { act(() => { root.render( - false} />, + false} callback={() => null} />, ) }) expect(container).toMatchSnapshot() @@ -97,7 +97,7 @@ describe('NewSouth', () => { act(() => { root.render( - false} />, + false} callback={() => null} />, ) }) expect(container).toMatchSnapshot() @@ -131,7 +131,7 @@ describe('NewSouth', () => { act(() => { root.render( - false} />, + false} callback={() => null} />, ) }) expect(container).toMatchSnapshot() @@ -165,7 +165,7 @@ describe('NewSouth', () => { act(() => { root.render( - false} />, + false} callback={() => null} />, ) }) expect(container).toMatchSnapshot() @@ -199,7 +199,7 @@ describe('NewSouth', () => { act(() => { root.render( - false} />, + false} callback={() => null} />, ) }) expect(container).toMatchSnapshot() diff --git a/src/frontend/index.jsx b/src/frontend/index.jsx index b1b5dcb42f..9eec341447 100644 --- a/src/frontend/index.jsx +++ b/src/frontend/index.jsx @@ -22,6 +22,7 @@ import AlertContainer from './components/alert-container.jsx' import { AlertProvider } from './context/alert-context.jsx' import ConfigProviders from './context/config-providers.jsx' import ConfigureHistoryQuery from './history-query/configure-history-query.jsx' +import HistoryQueryForm from './history-query/form/history-query-form.jsx' const Main = () => ( @@ -34,7 +35,8 @@ const Main = () => ( } /> } /> } /> - } /> + } /> + } /> } /> } /> } /> diff --git a/src/frontend/north/form/caching.jsx b/src/frontend/north/form/caching.jsx new file mode 100644 index 0000000000..8ae11271d8 --- /dev/null +++ b/src/frontend/north/form/caching.jsx @@ -0,0 +1,145 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Col, Row } from 'reactstrap' +import { OibCheckbox, OibInteger, OibTitle } from '../../components/oib-form/index.js' +import validation from './north.validation.js' + +const Caching = ({ onChange, prefix, value }) => ( + <> + + <> +

+ The cache is a local file storage to allow OIBus to store values + or files when the communication with the north connector is + interrupted. The more space is allocated to the cache, the + longer the interruption can be. The parameters below are + important to understand. +

+
    +
  • + sendInterval: the cache will try to group a maximum of values + in a buffer and to send them in a single transaction. However, + if the sendInterval (in ms) is reached, the transaction will + be sent even if the buffer is not full. +
  • +
  • + Retry Interval: If the communication is broken, OIBus will try + to resend the buffer after this interval of time (in ms) until + the communication is restored. +
  • +
  • + Group Count: OIBus will try to group the number of values + specified here when the communication is normal. Please note + that one value is an JSON object with a size that can be + between 10 to 100 bytes. +
  • +
  • + Max Group Count: In normal operations, the group count above + is used but if the cache has grown because of a communication + issue, it will try to group the largest possible transaction + but limited to this count. This is to avoid a too large + transaction. +
  • +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + Move to archive folder or delete files when they are sent} + onChange={onChange} + /> + + {value.archive.enabled && ( + + Retention period of archived files (in hours)} + onChange={onChange} + /> + + )} + + + +) + +Caching.propTypes = { + onChange: PropTypes.func.isRequired, + value: PropTypes.object.isRequired, + prefix: PropTypes.string.isRequired, +} + +export default Caching diff --git a/src/frontend/service/apis.js b/src/frontend/service/apis.js index f8260c7518..b94b8b5321 100644 --- a/src/frontend/service/apis.js +++ b/src/frontend/service/apis.js @@ -73,8 +73,6 @@ const putRequest = async (uri, body) => { } } -const getSouthProtocols = () => getRequest('/config/schemas/south') -const getNorthApis = () => getRequest('/config/schemas/north') const getConfig = () => getRequest('/config') const updateConfig = (body) => putRequest('/config', body) const activateConfig = () => putRequest('/config/activate') @@ -90,21 +88,24 @@ const getLastCompletedForHistoryQuery = async (id) => getRequest(`/history-queri const getLogs = (fromDate, toDate, verbosity) => getRequest(`/logs?fromDate=${fromDate || ''}&toDate=${toDate || ''}&verbosity=[${verbosity}]`) const getOIBusInfo = () => getRequest('/info') -const getSouthStatus = (id) => getRequest(`/status/south/${id}`) -const getNorthStatus = (id) => getRequest(`/status/north/${id}`) - +const getSouthTypes = () => getRequest('/api/installed-south') +const getNorthTypes = () => getRequest('/api/installed-north') +const getSouth = async (id) => getRequest(`/api/south/${id}`) +const getNorth = async (id) => getRequest(`/api/north/${id}`) +const getSouthStatus = (id) => getRequest(`/south/${id}/status`) +const getNorthStatus = (id) => getRequest(`/north/${id}/status`) const reload = () => getRequest('/reload') const shutdown = () => getRequest('/shutdown') export default { - getSouthTypes: getSouthProtocols, - getNorthTypes: getNorthApis, + getSouthTypes, + getNorthTypes, getConfig, activateConfig, updateConfig, - createHistoryQuery, getHistoryQueries, getHistoryQueryById, + createHistoryQuery, updateHistoryQuery, enableHistoryQuery, orderHistoryQuery, @@ -113,7 +114,9 @@ export default { getLogs, getOIBusInfo, getSouthStatus, + getSouth, getNorthStatus, + getNorth, reload, shutdown, } diff --git a/src/frontend/service/apis.spec.js b/src/frontend/service/apis.spec.js index 28fb6dd29b..114f014833 100644 --- a/src/frontend/service/apis.spec.js +++ b/src/frontend/service/apis.spec.js @@ -12,10 +12,10 @@ const logSample = { global.fetch = jest.fn().mockImplementation((uri) => { let jsonString switch (uri) { - case '/config/schemas/north': + case '/api/installed-north': jsonString = JSON.stringify(['a', 'b', 'c']) break - case '/config/schemas/south': + case '/api/installed-south': jsonString = JSON.stringify(['d', 'e', 'f']) break case '/config': @@ -37,12 +37,12 @@ global.fetch = jest.fn().mockImplementation((uri) => { }) describe('apis', () => { - it('check getSouthProtocols', async () => { + it('check getSouthTypes', async () => { const result = await apis.getSouthTypes() expect(result).toEqual(['d', 'e', 'f']) }) - it('check getSouthProtocols with status 500', async () => { + it('check getSouthTypes with status 500', async () => { const originalError = console.error console.error = jest.fn() const originalGlobalFetchMock = global.fetch @@ -56,7 +56,7 @@ describe('apis', () => { console.error = originalError }) - it('check getSouthProtocols with catch error', async () => { + it('check getSouthTypes with catch error', async () => { const originalError = console.error console.error = jest.fn() const originalGlobalFetchMock = global.fetch @@ -73,7 +73,7 @@ describe('apis', () => { console.error = originalError }) - it('check getNorthApis', async () => { + it('check getNorthTypes', async () => { const result = await apis.getNorthTypes() expect(result).toEqual(['a', 'b', 'c']) }) diff --git a/src/frontend/south/__snapshots__/configure-points.spec.jsx.snap b/src/frontend/south/__snapshots__/configure-points.spec.jsx.snap deleted file mode 100644 index ba2d170b55..0000000000 --- a/src/frontend/south/__snapshots__/configure-points.spec.jsx.snap +++ /dev/null @@ -1,9304 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ConfigurePoints check ConfigurePoints 1`] = ` -
-
-
- - - | - - - TestOPCUA - -
-
- -
-
-
-
-
-
- Points -
-
-
-
- - -
- -
- Type any points related data -
-
-
-
-
- Table help - -
-
-
-
-
-
-
- - Point ID: - -
- The pointId to used to send the data -
-
-
- - Node ID: - -
- The nodeId referenced in the OPCUA server -
-
-
- - Scan Group: - -
-
    -
  • - Only scan modes set in scan groups are displayed -
  • -
  • - A new scan group must be added to be able to select other scan modes -
  • -
-
-
-
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - -
- - - - - Point ID - - - Node ID - - Scan Group - -
-
- -
-
-
-
- -
-
-
-
- -
-
-
- - - -
-
- -
-
-
-
- -
-
-
-
- -
-
-
- - - -
- -
- - - -
- -
-
-
- - -`; - -exports[`ConfigurePoints check add new point 1`] = ` -
-
-
- - - | - - - TestOPCUA - -
-
- -
-
-
-
-
-
- Points -
-
-
-
- - -
- -
- Type any points related data -
-
-
-
-
- Table help - -
-
-
-
-
-
-
- - Point ID: - -
- The pointId to used to send the data -
-
-
- - Node ID: - -
- The nodeId referenced in the OPCUA server -
-
-
- - Scan Group: - -
-
    -
  • - Only scan modes set in scan groups are displayed -
  • -
  • - A new scan group must be added to be able to select other scan modes -
  • -
-
-
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - - -
- - - - - Point ID - - - Node ID - - Scan Group - -
-
- -
-
-
-
- -
-
-
-
- -
-
-
- - - -
-
- -
-
-
-
- -
-
-
-
- -
-
-
- - - -
- -
- - - -
- -
-
-
-
-
-`; - -exports[`ConfigurePoints check confirm on delete all points 1`] = ` -
-
-
- - - | - - - TestOPCUA - -
-
- -
-
-
-
-
-
- Points -
-
-
-
- - -
- -
- Type any points related data -
-
-
-
-
- Table help - -
-
-
-
-
-
-
- - Point ID: - -
- The pointId to used to send the data -
-
-
- - Node ID: - -
- The nodeId referenced in the OPCUA server -
-
-
- - Scan Group: - -
-
    -
  • - Only scan modes set in scan groups are displayed -
  • -
  • - A new scan group must be added to be able to select other scan modes -
  • -
-
-
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - - -
- - - - - Point ID - - - Node ID - - Scan Group - -
-
- -
-
-
-
- -
-
-
-
- -
-
-
- - - -
-
- -
-
-
-
- -
-
-
-
- -
-
-
- - - -
- -
- - - -
- -
-
-
-
-
-`; - -exports[`ConfigurePoints check delete all points 1`] = ` -
-
-
- - - | - - - TestOPCUA - -
-
- -
-
-
-
-
-
- Points -
-
-
-
- - -
- -
- Type any points related data -
-
-
-
-
- Table help - -
-
-
-
-
-
-
- - Point ID: - -
- The pointId to used to send the data -
-
-
- - Node ID: - -
- The nodeId referenced in the OPCUA server -
-
-
- - Scan Group: - -
-
    -
  • - Only scan modes set in scan groups are displayed -
  • -
  • - A new scan group must be added to be able to select other scan modes -
  • -
-
-
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - - -
- - - - - Point ID - - - Node ID - - Scan Group - -
-
- -
-
-
-
- -
-
-
-
- -
-
-
- - - -
-
- -
-
-
-
- -
-
-
-
- -
-
-
- - - -
- -
- - - -
- -
-
-
-
-
-`; - -exports[`ConfigurePoints check delete first point 1`] = ` -
-
-
- - - | - - - TestOPCUA - -
-
- -
-
-
-
-
-
- Points -
-
-
-
- - -
- -
- Type any points related data -
-
-
-
-
- Table help - -
-
-
-
-
-
-
- - Point ID: - -
- The pointId to used to send the data -
-
-
- - Node ID: - -
- The nodeId referenced in the OPCUA server -
-
-
- - Scan Group: - -
-
    -
  • - Only scan modes set in scan groups are displayed -
  • -
  • - A new scan group must be added to be able to select other scan modes -
  • -
-
-
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - - -
- - - - - Point ID - - - Node ID - - Scan Group - -
-
- -
-
-
-
- -
-
-
-
- -
-
-
- - - -
-
- -
-
-
-
- -
-
-
-
- -
-
-
- - - -
- -
- - - -
- -
-
-
-
-
-`; - -exports[`ConfigurePoints check edit filter input 1`] = ` -
-
-
- - - | - - - TestOPCUA - -
-
- -
-
-
-
-
-
- Points -
-
-
-
- - -
- -
- Type any points related data -
-
-
-
-
- Table help - -
-
-
-
-
-
-
- - Point ID: - -
- The pointId to used to send the data -
-
-
- - Node ID: - -
- The nodeId referenced in the OPCUA server -
-
-
- - Scan Group: - -
-
    -
  • - Only scan modes set in scan groups are displayed -
  • -
  • - A new scan group must be added to be able to select other scan modes -
  • -
-
-
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - - -
- - - - - Point ID - - - Node ID - - Scan Group - -
-
- -
-
-
-
- -
-
-
-
- -
-
-
- - - -
-
- -
-
-
-
- -
-
-
-
- -
-
-
- - - -
- -
- - - -
- -
-
-
-
-
-`; - -exports[`ConfigurePoints check edit first pointId 1`] = ` -
-
-
- - - | - - - TestOPCUA - -
-
- -
-
-
-
-
-
- Points -
-
-
-
- - -
- -
- Type any points related data -
-
-
-
-
- Table help - -
-
-
-
-
-
-
- - Point ID: - -
- The pointId to used to send the data -
-
-
- - Node ID: - -
- The nodeId referenced in the OPCUA server -
-
-
- - Scan Group: - -
-
    -
  • - Only scan modes set in scan groups are displayed -
  • -
  • - A new scan group must be added to be able to select other scan modes -
  • -
-
-
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - - -
- - - - - Point ID - - - Node ID - - Scan Group - -
-
- -
-
-
-
- -
-
-
-
- -
-
-
- - - -
-
- -
-
-
-
- -
-
-
-
- -
-
-
- - - -
- -
- - - -
- -
-
-
-
-
-`; - -exports[`ConfigurePoints check edit first pointId 2`] = ` -
-
-
- - - | - - - TestOPCUA - -
-
- -
-
-
-
-
-
- Points -
-
-
-
- - -
- -
- Type any points related data -
-
-
-
-
- Table help - -
-
-
-
-
-
-
- - Point ID: - -
- The pointId to used to send the data -
-
-
- - Node ID: - -
- The nodeId referenced in the OPCUA server -
-
-
- - Scan Group: - -
-
    -
  • - Only scan modes set in scan groups are displayed -
  • -
  • - A new scan group must be added to be able to select other scan modes -
  • -
-
-
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - - -
- - - - - Point ID - - - Node ID - - Scan Group - -
-
- -
-
-
-
- -
-
-
-
- -
-
-
- - - -
-
- -
-
-
-
- -
-
-
-
- -
-
-
- - - -
- -
- - - -
- -
-
-
-
-
-`; - -exports[`ConfigurePoints check edit first scanMode 1`] = ` -
-
-
- - - | - - - TestOPCUA - -
-
- -
-
-
-
-
-
- Points -
-
-
-
- - -
- -
- Type any points related data -
-
-
-
-
- Table help - -
-
-
-
-
-
-
- - Point ID: - -
- The pointId to used to send the data -
-
-
- - Node ID: - -
- The nodeId referenced in the OPCUA server -
-
-
- - Scan Group: - -
-
    -
  • - Only scan modes set in scan groups are displayed -
  • -
  • - A new scan group must be added to be able to select other scan modes -
  • -
-
-
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - - -
- - - - - Point ID - - - Node ID - - Scan Group - -
-
- -
-
-
-
- -
-
-
-
- -
-
-
- - - -
-
- -
-
-
-
- -
-
-
-
- -
-
-
- - - -
- -
- - - -
- -
-
-
-
-
-`; - -exports[`ConfigurePoints check edit first scanMode 2`] = ` -
-
-
- - - | - - - TestOPCUA - -
-
- -
-
-
-
-
-
- Points -
-
-
-
- - -
- -
- Type any points related data -
-
-
-
-
- Table help - -
-
-
-
-
-
-
- - Point ID: - -
- The pointId to used to send the data -
-
-
- - Node ID: - -
- The nodeId referenced in the OPCUA server -
-
-
- - Scan Group: - -
-
    -
  • - Only scan modes set in scan groups are displayed -
  • -
  • - A new scan group must be added to be able to select other scan modes -
  • -
-
-
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - - -
- - - - - Point ID - - - Node ID - - Scan Group - -
-
- -
-
-
-
- -
-
-
-
- -
-
-
- - - -
-
- -
-
-
-
- -
-
-
-
- -
-
-
- - - -
- -
- - - -
- -
-
-
-
-
-`; - -exports[`ConfigurePoints check export points 1`] = ` -
-
-
- - - | - - - TestOPCUA - -
-
- -
-
-
-
-
-
- Points -
-
-
-
- - -
- -
- Type any points related data -
-
-
-
-
- Table help - -
-
-
-
-
-
-
- - Point ID: - -
- The pointId to used to send the data -
-
-
- - Node ID: - -
- The nodeId referenced in the OPCUA server -
-
-
- - Scan Group: - -
-
    -
  • - Only scan modes set in scan groups are displayed -
  • -
  • - A new scan group must be added to be able to select other scan modes -
  • -
-
-
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - - -
- - - - - Point ID - - - Node ID - - Scan Group - -
-
- -
-
-
-
- -
-
-
-
- -
-
-
- - - -
-
- -
-
-
-
- -
-
-
-
- -
-
-
- - - -
- -
- - - -
- -
-
-
-
-
-`; - -exports[`ConfigurePoints check import points file input 1`] = ` -
-
-
- - - | - - - TestOPCUA - -
-
- -
-
-
-
-
-
- Points -
-
-
-
- - -
- -
- Type any points related data -
-
-
-
-
- Table help - -
-
-
-
-
-
-
- - Point ID: - -
- The pointId to used to send the data -
-
-
- - Node ID: - -
- The nodeId referenced in the OPCUA server -
-
-
- - Scan Group: - -
-
    -
  • - Only scan modes set in scan groups are displayed -
  • -
  • - A new scan group must be added to be able to select other scan modes -
  • -
-
-
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - - -
- - - - - Point ID - - - Node ID - - Scan Group - -
-
- -
-
-
-
- -
-
-
-
- -
-
-
- - - -
-
- -
-
-
-
- -
-
-
-
- -
-
-
- - - -
- -
- - - -
- -
-
-
-
-
-`; - -exports[`ConfigurePoints check import points parseCSV fail 1`] = ` -
-
-
- - - | - - - TestOPCUA - -
-
- -
-
-
-
-
-
- Points -
-
-
-
- - -
- -
- Type any points related data -
-
-
-
-
- Table help - -
-
-
-
-
-
-
- - Point ID: - -
- The pointId to used to send the data -
-
-
- - Node ID: - -
- The nodeId referenced in the OPCUA server -
-
-
- - Scan Group: - -
-
    -
  • - Only scan modes set in scan groups are displayed -
  • -
  • - A new scan group must be added to be able to select other scan modes -
  • -
-
-
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - - -
- - - - - Point ID - - - Node ID - - Scan Group - -
-
- -
-
-
-
- -
-
-
-
- -
-
-
- - - -
-
- -
-
-
-
- -
-
-
-
- -
-
-
- - - -
- -
- - - -
- -
-
-
-
-
-`; - -exports[`ConfigurePoints check import points press 1`] = ` -
-
-
- - - | - - - TestOPCUA - -
-
- -
-
-
-
-
-
- Points -
-
-
-
- - -
- -
- Type any points related data -
-
-
-
-
- Table help - -
-
-
-
-
-
-
- - Point ID: - -
- The pointId to used to send the data -
-
-
- - Node ID: - -
- The nodeId referenced in the OPCUA server -
-
-
- - Scan Group: - -
-
    -
  • - Only scan modes set in scan groups are displayed -
  • -
  • - A new scan group must be added to be able to select other scan modes -
  • -
-
-
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - - -
- - - - - Point ID - - - Node ID - - Scan Group - -
-
- -
-
-
-
- -
-
-
-
- -
-
-
- - - -
-
- -
-
-
-
- -
-
-
-
- -
-
-
- - - -
- -
- - - -
- -
-
-
-
-
-`; - -exports[`ConfigurePoints check import points readFileContent fail 1`] = ` -
-
-
- - - | - - - TestOPCUA - -
-
- -
-
-
-
-
-
- Points -
-
-
-
- - -
- -
- Type any points related data -
-
-
-
-
- Table help - -
-
-
-
-
-
-
- - Point ID: - -
- The pointId to used to send the data -
-
-
- - Node ID: - -
- The nodeId referenced in the OPCUA server -
-
-
- - Scan Group: - -
-
    -
  • - Only scan modes set in scan groups are displayed -
  • -
  • - A new scan group must be added to be able to select other scan modes -
  • -
-
-
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - - -
- - - - - Point ID - - - Node ID - - Scan Group - -
-
- -
-
-
-
- -
-
-
-
- -
-
-
- - - -
-
- -
-
-
-
- -
-
-
-
- -
-
-
- - - -
- -
- - - -
- -
-
-
-
-
-`; - -exports[`ConfigurePoints check import points success 1`] = ` -
-
-
- - - | - - - TestOPCUA - -
-
- -
-
-
-
-
-
- Points -
-
-
-
- - -
- -
- Type any points related data -
-
-
-
-
- Table help - -
-
-
-
-
-
-
- - Point ID: - -
- The pointId to used to send the data -
-
-
- - Node ID: - -
- The nodeId referenced in the OPCUA server -
-
-
- - Scan Group: - -
-
    -
  • - Only scan modes set in scan groups are displayed -
  • -
  • - A new scan group must be added to be able to select other scan modes -
  • -
-
-
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - - -
- - - - - Point ID - - - Node ID - - Scan Group - -
-
- -
-
-
-
- -
-
-
-
- -
-
-
- - - -
-
- -
-
-
-
- -
-
-
-
- -
-
-
- - - -
- -
- - - -
- -
-
-
-
-
-`; - -exports[`ConfigurePoints check no config 1`] = ` -
-
-
- - Loading... - -
- ...loading points from OIBus server... -
-
-`; - -exports[`ConfigurePoints check no points 1`] = ` -
-
-
- - - | - - - TestOPCUA - -
-
- -
-
-
-
-
-
- Points -
-
-
-
- - -
- -
- Type any points related data -
-
-
-
-
- Table help - -
-
-
-
-
-
-
- - Point ID: - -
- The pointId to used to send the data -
-
-
- - Node ID: - -
- The nodeId referenced in the OPCUA server -
-
-
- - Scan Group: - -
-
    -
  • - Only scan modes set in scan groups are displayed -
  • -
  • - A new scan group must be added to be able to select other scan modes -
  • -
-
-
-
-
-
-
- - - - - - - - - -
- - - - - Point ID - - - Node ID - - Scan Group - -
- 0 -
- - - -
- -
-
-
-
-
-`; - -exports[`ConfigurePoints check pagination 1`] = ` -
-
-
- - - | - - - TestOPCUA - -
-
- -
-
-
-
-
-
- Points -
-
-
-
- - -
- -
- Type any points related data -
-
-
-
-
- Table help - -
-
-
-
-
-
-
- - Point ID: - -
- The pointId to used to send the data -
-
-
- - Node ID: - -
- The nodeId referenced in the OPCUA server -
-
-
- - Scan Group: - -
-
    -
  • - Only scan modes set in scan groups are displayed -
  • -
  • - A new scan group must be added to be able to select other scan modes -
  • -
-
-
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - - -
- - - - - Point ID - - - Node ID - - Scan Group - -
-
- -
-
-
-
- -
-
-
-
- -
-
-
- - - -
-
- -
-
-
-
- -
-
-
-
- -
-
-
- - - -
- -
- - - -
- -
-
-
-
-
-`; - -exports[`ConfigurePoints check render with filterText 1`] = ` -
-
-
- - - | - - - TestOPCUA - -
-
- -
-
-
-
-
-
- Points -
-
-
-
- - -
- -
- Type any points related data -
-
-
-
-
- Table help - -
-
-
-
-
-
-
- - Point ID: - -
- The pointId to used to send the data -
-
-
- - Node ID: - -
- The nodeId referenced in the OPCUA server -
-
-
- - Scan Group: - -
-
    -
  • - Only scan modes set in scan groups are displayed -
  • -
  • - A new scan group must be added to be able to select other scan modes -
  • -
-
-
-
-
-
-
- - - - - - - - - -
- - - - - Point ID - - - Node ID - - Scan Group - -
- 0 -
- - - -
- -
-
-
-
-
-`; diff --git a/src/frontend/south/configure-points.jsx b/src/frontend/south/configure-points.jsx index 6b2c2bdef0..bf3aa3da2e 100644 --- a/src/frontend/south/configure-points.jsx +++ b/src/frontend/south/configure-points.jsx @@ -1,97 +1,41 @@ -import React from 'react' +import React, { useContext, useEffect, useState } from 'react' import { useNavigate, useParams } from 'react-router-dom' import { Button, Container, Spinner } from 'reactstrap' import { FaArrowLeft } from 'react-icons/fa' -import { AlertContext } from '../context/alert-context.jsx' import { ConfigContext } from '../context/config-context.jsx' -import utils from '../helpers/utils.js' import PointsComponent from '../components/points-component.jsx' import StatusButton from '../components/status-button.jsx' +import SouthSchemas from './south-types.jsx' const ConfigurePoints = () => { - const { newConfig, dispatchNewConfig } = React.useContext(ConfigContext) - const { setAlert } = React.useContext(AlertContext) - const navigate = useNavigate() + const { newConfig, dispatchNewConfig } = useContext(ConfigContext) const { id } = useParams() + const navigate = useNavigate() - if (!newConfig?.south) { - return ( -
- - ...loading points from OIBus server... -
- ) - } - const southIndex = newConfig.south.findIndex( - (south) => south.id === id, - ) - const south = newConfig.south[southIndex] - - const { points: pointsOrdered = [], type } = south - - /** - * add point - * @returns {void} - */ - const handleAdd = () => { - dispatchNewConfig({ type: 'addRow', name: `south.${southIndex}.points`, value: {} }) - } - - /** - * Delete point - * @param {string} index the index of point - * @returns {void} - */ - const handleDelete = (index) => { - dispatchNewConfig({ type: 'deleteRow', name: `south.${southIndex}.points.${index}` }) - } - - /** - * Delete all points - * @returns {void} - */ - const handleDeleteAllPoint = () => { - dispatchNewConfig({ type: 'deleteAllRows', name: `south.${southIndex}.points` }) - } + const [south, setSouth] = useState(null) + const [southIndex, setSouthIndex] = useState(null) - /** - * Send the imported file content to the backend - * @param {Object} file the file returned by input - * @returns {void} - */ - const handleImportPoints = async (file) => { - try { - const text = await utils.readFileContent(file) - utils - .parseCSV(text) - .then((newPoints) => { - dispatchNewConfig({ - type: 'importPoints', - name: `south.${southIndex}.points`, - value: newPoints, - }) - }) - .catch((error) => { - console.error(error) - setAlert({ text: error.message, type: 'danger' }) - }) - } catch (error) { - console.error(error) - setAlert({ text: error.message, type: 'danger' }) + useEffect(() => { + if (newConfig?.south && south?.id !== id) { + const index = newConfig.south.findIndex( + (connector) => connector.id === id, + ) + setSouthIndex(index) + setSouth(newConfig.south[index]) } - } + }, [newConfig]) const onChange = (name, value, validity) => { dispatchNewConfig({ type: 'update', - name: `south.${southIndex}.${name}`, + name, value, validity, }) } - return ( + return south ? ( <>
@@ -119,17 +63,18 @@ const ConfigurePoints = () => {
+ ) : ( +
+ + ...loading points from OIBus server... +
) } diff --git a/src/frontend/south/configure-points.spec.jsx b/src/frontend/south/configure-points.spec.jsx deleted file mode 100644 index a04533ec92..0000000000 --- a/src/frontend/south/configure-points.spec.jsx +++ /dev/null @@ -1,396 +0,0 @@ -/** - * @jest-environment jsdom - */ -import React from 'react' -import { act, Simulate } from 'react-dom/test-utils' - -import * as ReactDOMClient from 'react-dom/client' -import { defaultConfig } from '../../../tests/test-config.js' -import ConfigurePoints from './configure-points.jsx' -import utils from '../helpers/utils.js' - -// mock context -const dispatchNewConfig = jest.fn() -const setAlert = jest.fn() -React.useContext = jest.fn().mockReturnValue({ - newConfig: { - ...defaultConfig, - south: [{ - id: 'south-uuid-1', - name: 'TestOPCUA', - type: 'OPCUA_HA', - enabled: false, - OPCUA_HA: { - scanGroups: [ - { aggregate: 'Raw', resampling: 'None', scanMode: 'everySecond' }, - { aggregate: 'Raw', resampling: 'None', scanMode: 'every10Seconds' }, - ], - }, - points: [ - { pointId: '111.temperature', nodeId: '111.temperature', scanMode: 'everySecond' }, - { pointId: '222.temperature', nodeId: '222.temperature', scanMode: 'everySecond' }, - ], - }], - }, - dispatchNewConfig, - setAlert, -}) -jest.mock('react-router-dom', () => ( - { - useParams: jest.fn().mockReturnValue({ id: 'south-uuid-1' }), - useNavigate: jest.fn(), - } -)) - -window.URL.createObjectURL = () => { } -// mock states -const originalUseState = React.useState -let filterText = '' -const setFilterText = jest.fn() -const setSelectedPage = jest.fn() -const setState = jest.fn() -const useStateMock = jest.fn().mockImplementation((init) => { - if (init === '') { - return [filterText, setFilterText] - } - if (init === 1) { - return [init, setSelectedPage] - } - return [init, setState] -}) -React.useState = useStateMock - -// mock createCSV -let resolve -let reject -utils.createCSV = () => new Promise((_resolve, _reject) => { - resolve = _resolve - reject = _reject -}) -// mock parseCSV -utils.parseCSV = () => new Promise((_resolve, _reject) => { - resolve = _resolve - reject = _reject -}) - -utils.readFileContent = () => new Promise((_resolve, _reject) => { - resolve = _resolve - reject = _reject -}) - -const mockMath = Object.create(global.Math) -mockMath.random = () => 1 -global.Math = mockMath - -let container -let root -// eslint-disable-next-line no-undef -globalThis.IS_REACT_ACT_ENVIRONMENT = true -beforeEach(() => { - container = document.createElement('div') - root = ReactDOMClient.createRoot(container) - document.body.appendChild(container) -}) - -afterEach(() => { - document.body.removeChild(container) - container = null - root = null -}) - -describe('ConfigurePoints', () => { - test('check ConfigurePoints', () => { - act(() => { - root.render() - }) - expect(container).toMatchSnapshot() - }) - test('check edit filter input', () => { - act(() => { - root.render() - }) - act(() => { - Simulate.change(document.getElementById('filterText'), { target: { value: 'A13518/AI1/PV.CV' } }) - }) - - expect(setFilterText).toBeCalledWith('A13518/AI1/PV.CV') - expect(container).toMatchSnapshot() - }) - test('check render with filterText', () => { - filterText = 'filter' - act(() => { - root.render() - }) - expect(container).toMatchSnapshot() - filterText = '' - }) - test('check add new point', () => { - act(() => { - root.render() - }) - act(() => { - Simulate.click(document.querySelector('th path')) - }) - - expect(dispatchNewConfig).toBeCalledWith({ - type: 'addRow', - name: 'south.0.points', - value: {}, - }) - expect(container).toMatchSnapshot() - }) - test('check edit first pointId', () => { - act(() => { - root.render() - }) - act(() => { - Simulate.change(document.getElementById('points.0.pointId'), { target: { value: 'new_point_id' } }) - }) - - expect(dispatchNewConfig).toBeCalledWith({ - type: 'update', - name: 'south.0.points.1.pointId', - value: 'new_point_id', - validity: null, - }) - expect(container).toMatchSnapshot() - }) - test('check edit first scanMode', () => { - act(() => { - root.render() - }) - act(() => { - Simulate.change(document.getElementById('points.0.scanMode'), { target: { value: 'everySecond' } }) - }) - - expect(dispatchNewConfig).toBeCalledWith({ - type: 'update', - name: 'south.0.points.1.scanMode', - value: 'everySecond', - validity: null, - }) - expect(container).toMatchSnapshot() - }) - test('check edit first pointId', () => { - act(() => { - root.render() - }) - act(() => { - Simulate.change(document.getElementById('points.0.pointId'), { target: { value: 'new_value' } }) - }) - - expect(dispatchNewConfig).toBeCalledWith({ - type: 'update', - name: 'south.0.points.1.pointId', - value: 'new_value', - validity: null, - }) - expect(container).toMatchSnapshot() - }) - test('check edit first scanMode', () => { - act(() => { - root.render() - }) - act(() => { - Simulate.change(document.getElementById('points.0.scanMode'), { target: { value: 'every1Min' } }) - }) - - expect(dispatchNewConfig).toBeCalledWith({ - type: 'update', - name: 'south.0.points.1.scanMode', - value: 'every1Min', - validity: null, - }) - expect(container).toMatchSnapshot() - }) - test('check import points press', () => { - act(() => { - root.render() - }) - act(() => { - Simulate.click(document.getElementsByClassName('inline-button btn btn-primary')[0]) - }) - - expect(container).toMatchSnapshot() - }) - test('check import points file input', () => { - console.error = jest.fn() - act(() => { - root.render() - }) - act(() => { - Simulate.change(document.getElementById('importFile'), { target: { files: ['new_file'] } }) - }) - - expect(container).toMatchSnapshot() - }) - test('check import points readFileContent fail', async () => { - console.error = jest.fn() - - act(() => { - root.render() - }) - act(() => { - Simulate.change(document.getElementById('importFile'), { target: { files: ['new_file'] } }) - }) - - expect(container).toMatchSnapshot() - // await utils.readFileContent - await act(async () => { - reject('error') - }) - }) - test('check import points parseCSV fail', async () => { - console.error = jest.fn() - - act(() => { - root.render() - }) - act(() => { - Simulate.change(document.getElementById('importFile'), { target: { files: ['new_file'] } }) - }) - expect(container).toMatchSnapshot() - // await utils.readFileContent - await act(async () => { - resolve('') - }) - // await utils.parseCSV to fail - await act(async () => { - reject('error') - }) - }) - test('check import points success', async () => { - console.error = jest.fn() - - act(() => { - root.render() - }) - act(() => { - Simulate.change(document.getElementById('importFile'), { target: { files: ['new_file'] } }) - }) - expect(container).toMatchSnapshot() - // await utils.readFileContent - await act(async () => { - resolve('') - }) - - const newPoints = [ - { pointId: '111.temperature', nodeId: '111.temperature', scanMode: 'everySecond' }, - { pointId: '222.temperature', nodeId: '222.temperature', scanMode: 'everySecond' }, - ] - - // await utils.parseCSV to success - await act(async () => { - resolve(newPoints) - }) - expect(dispatchNewConfig).toBeCalledWith({ - type: 'importPoints', - name: 'south.0.points', - value: newPoints, - }) - }) - test('check export points', async () => { - console.error = jest.fn() - const originalUrlCreateObjectURL = URL.createObjectURL - URL.createObjectURL = jest.fn() - act(() => { - root.render() - }) - act(() => { - Simulate.click(document.getElementsByClassName('inline-button btn btn-primary')[1]) - }) - - expect(container).toMatchSnapshot() - await act(async () => { - resolve('test,csv') - }) - URL.createObjectURL = originalUrlCreateObjectURL - }) - test('check pagination', () => { - act(() => { - root.render() - }) - const pagination = document.getElementsByClassName('pagination')[0] - const items = pagination.getElementsByClassName('page-item') - act(() => { - Simulate.click(items[0].querySelector('button')) - }) - - expect(setSelectedPage).toBeCalledWith(1) - expect(container).toMatchSnapshot() - }) - test('check delete all points', () => { - act(() => { - root.render() - }) - act(() => { - Simulate.click(document.getElementsByClassName('inline-button btn btn-danger')[0]) - }) - - expect(container).toMatchSnapshot() - }) - test('check confirm on delete all points', () => { - React.useState = originalUseState - - act(() => { - root.render() - }) - act(() => { - Simulate.click(document.getElementsByClassName('inline-button btn btn-danger')[0]) - }) - act(() => { - Simulate.click(document.getElementsByClassName('btn btn-primary')[2]) - }) - expect(dispatchNewConfig).toBeCalledWith({ - type: 'deleteAllRows', - name: 'south.0.points', - }) - expect(container).toMatchSnapshot() - }) - test('check delete first point', () => { - act(() => { - root.render() - }) - act(() => { - Simulate.click(document.querySelector('td path')) // click on delete icon - }) - act(() => { - Simulate.click(document.getElementsByClassName('btn btn-primary')[3]) - }) - expect(dispatchNewConfig).toBeCalledWith({ - type: 'deleteRow', - name: 'south.0.points.1', - }) - expect(container).toMatchSnapshot() - }) - test('check no config', () => { - React.useContext = jest.fn().mockReturnValue({ newConfig: null, dispatchNewConfig, setAlert }) - act(() => { - root.render() - }) - expect(container).toMatchSnapshot() - }) - test('check no points', () => { - const configNoPoints = { - ...defaultConfig, - south: [{ - id: 'south-uuid-1', - name: 'TestOPCUA', - type: 'OPCUA_HA', - enabled: false, - OPCUA_HA: { - scanGroups: [ - { Aggregate: 'Raw', resampling: 'None', scanMode: 'everySecond' }, - { Aggregate: 'Raw', resampling: 'None', scanMode: 'everySecond' }, - ], - }, - points: undefined, - }], - } - React.useContext = jest.fn().mockReturnValue({ newConfig: configNoPoints, dispatchNewConfig, setAlert }) - act(() => { - root.render() - }) - expect(container).toMatchSnapshot() - }) -}) diff --git a/src/migration/migration-rules.js b/src/migration/migration-rules.js index 0594257237..09b85c72e2 100644 --- a/src/migration/migration-rules.js +++ b/src/migration/migration-rules.js @@ -1762,4 +1762,20 @@ export default { config.engine.logParameters.sqliteLog.maxNumberOfLogs = 100000 } }, + 30: async (config, logger) => { + for (const south of config.south) { + logger.info(`Create UID for points for South connector ${south.name} (${south.id}).`) + south.points = south.points.map((point) => ({ ...point, id: nanoid() })) + if (south.settings.scanGroups) { + logger.info(`Create UID for scan groups for South connector ${south.name} (${south.id}).`) + south.settings.scanGroups = south.settings.scanGroups.map((scanGroup) => ({ ...scanGroup, id: nanoid() })) + } + } + + logger.info('Create UID for scan modes.') + config.engine.scanModes = config.engine.scanModes.map((scanMode) => ({ ...scanMode, id: nanoid() })) + + logger.info('Create UID for proxies.') + config.engine.proxies = config.engine.proxies.map((proxy) => ({ ...proxy, id: nanoid() })) + }, } diff --git a/src/migration/migration.service.js b/src/migration/migration.service.js index 20118c16de..d66e18b870 100644 --- a/src/migration/migration.service.js +++ b/src/migration/migration.service.js @@ -3,7 +3,7 @@ import path from 'node:path' import migrationRules from './migration-rules.js' -const REQUIRED_SCHEMA_VERSION = 29 +const REQUIRED_SCHEMA_VERSION = 30 const DEFAULT_VERSION = 1 /** diff --git a/src/web-server/controllers/engine.controller.js b/src/web-server/controllers/engine.controller.js index 37b33962b1..0919c0cbdc 100644 --- a/src/web-server/controllers/engine.controller.js +++ b/src/web-server/controllers/engine.controller.js @@ -17,6 +17,20 @@ const getNorthList = (ctx) => { ctx.ok(ctx.app.engine.getNorthList()) } +/** + * Retrieve a specific North. + * @param {Object} ctx - The KOA context + * @return {void} + */ +const getNorth = (ctx) => { + const connector = ctx.app.engine.getNorth(ctx.params.id) + if (connector) { + ctx.ok(connector) + } else { + throw ctx(404, `North connector ${ctx.params.id} not found`) + } +} + /** * Get South list. * @param {Object} ctx - The KOA context @@ -26,6 +40,20 @@ const getSouthList = (ctx) => { ctx.ok(ctx.app.engine.getSouthList()) } +/** + * Retrieve a specific North. + * @param {Object} ctx - The KOA context + * @return {void} + */ +const getSouth = (ctx) => { + const connector = ctx.app.engine.getSouth(ctx.params.id) + if (connector) { + ctx.ok(connector) + } else { + throw ctx(404, `South connector ${ctx.params.id} not found`) + } +} + /** * Add Values to the Engine * @param {Object} ctx - The KOA context @@ -87,7 +115,9 @@ const aliveSignal = async (ctx) => { export default { getOIBusInfo, getNorthList, + getNorth, getSouthList, + getSouth, addValues, addFile, aliveSignal, diff --git a/src/web-server/routes/index.js b/src/web-server/routes/index.js index 4cdfdfaab9..0f1d34f4f1 100644 --- a/src/web-server/routes/index.js +++ b/src/web-server/routes/index.js @@ -20,8 +20,6 @@ const upload = multer({ storage }) router.get('/config', configController.getActiveConfiguration) router.put('/config', configController.updateConfig) router.put('/config/activate', configController.activateConfiguration) -router.get('/config/schemas/north', engineController.getNorthList) -router.get('/config/schemas/south', engineController.getSouthList) router.post('/engine/addValues', engineController.addValues) router.post('/engine/addFile', upload.single('file'), engineController.addFile) router.post('/engine/aliveSignal', engineController.aliveSignal) @@ -31,6 +29,11 @@ router.get('/shutdown', oibusController.shutdown) router.get('/logs', logController.getLogsEndpoint) router.post('/logs', logController.addLogs) +router.get('/api/installed-north', engineController.getNorthList) +router.get('/api/installed-south', engineController.getSouthList) +router.get('/api/north/:id', engineController.getNorth) +router.get('/api/south/:id', engineController.getSouth) + router.post('/history-queries', historyQueryController.createHistoryQuery) router.get('/history-queries', historyQueryController.getHistoryQueries) router.get('/history-queries/:id', historyQueryController.getHistoryQueryById)