From dcde1964b4199a43e69ecedf415249d451f3150c Mon Sep 17 00:00:00 2001 From: foxriver76 Date: Fri, 1 Sep 2023 09:07:54 +0200 Subject: [PATCH 1/7] fixed problem with discovery dialog - closes #2089 --- README.md | 3 + src/.eslintrc.js | 19 + src/src/dialogs/DiscoveryDialog.jsx | 1040 -------------- src/src/dialogs/DiscoveryDialog.tsx | 1228 +++++++++++++++++ ...nputsModal.jsx => GenerateInputsModal.jsx} | 0 src/tsconfig.json | 17 + 6 files changed, 1267 insertions(+), 1040 deletions(-) delete mode 100644 src/src/dialogs/DiscoveryDialog.jsx create mode 100644 src/src/dialogs/DiscoveryDialog.tsx rename src/src/dialogs/{GenereteInputsModal.jsx => GenerateInputsModal.jsx} (100%) create mode 100644 src/tsconfig.json diff --git a/README.md b/README.md index 3be697f59..a4465130d 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,9 @@ The icons may not be reused in other projects without the proper flaticon licens --> ## Changelog +### **WORK IN PROGRESS** +* (foxriver76) fixed problem with discovery dialog + ### 6.9.2 (2023-09-01) * (foxriver76) show info, if server time differs from client time * (foxriver76) remove confusion with different names for state (datapoint and state) diff --git a/src/.eslintrc.js b/src/.eslintrc.js index 83e6eb36a..9cc99e714 100644 --- a/src/.eslintrc.js +++ b/src/.eslintrc.js @@ -75,4 +75,23 @@ module.exports = { 'no-alert': 'off', 'class-methods-use-this': 'off', }, + overrides: [ + { + files: [ + '*.tsx', + ], + extends: [ + 'plugin:@typescript-eslint/recommended', + 'plugin:prettier/recommended', + ], + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + }, + rules: { + '@typescript-eslint/no-explicit-any': 'off', + }, + }, + ], }; diff --git a/src/src/dialogs/DiscoveryDialog.jsx b/src/src/dialogs/DiscoveryDialog.jsx deleted file mode 100644 index 3dfdae9ce..000000000 --- a/src/src/dialogs/DiscoveryDialog.jsx +++ /dev/null @@ -1,1040 +0,0 @@ -import React, { useEffect, useState } from 'react'; - -import Button from '@mui/material/Button'; -import Dialog from '@mui/material/Dialog'; -import DialogActions from '@mui/material/DialogActions'; -import DialogContent from '@mui/material/DialogContent'; -import { - Tooltip, - AppBar, Avatar, Box, - Checkbox, CircularProgress, LinearProgress, - Paper, Step, StepLabel, Stepper, Switch, - Table, TableBody, TableCell, - TableContainer, TableHead, TableRow, - TableSortLabel, Typography, -} from '@mui/material'; -import { ThemeProvider } from '@mui/material/styles'; -import { makeStyles } from '@mui/styles'; - -import VisibilityIcon from '@mui/icons-material/Visibility'; -import NavigateNextIcon from '@mui/icons-material/NavigateNext'; -import NavigateBeforeIcon from '@mui/icons-material/NavigateBefore'; -import SearchIcon from '@mui/icons-material/Search'; -import CloseIcon from '@mui/icons-material/Close'; -import LibraryAddIcon from '@mui/icons-material/LibraryAdd'; -import AssignmentTurnedInIcon from '@mui/icons-material/AssignmentTurnedIn'; -import ReportProblemIcon from '@mui/icons-material/ReportProblem'; - -import { I18n, Utils, SelectWithIcon } from '@iobroker/adapter-react-v5'; - -import Command from '../components/Command'; -import LicenseDialog from './LicenseDialog'; -import GenerateInputsModal from './GenereteInputsModal'; -import useStateLocal from '../helpers/hooks/useStateLocal'; - -const useStyles = makeStyles(theme => ({ - root: { - // backgroundColor: theme.palette.background.paper, - width: '100%', - height: 'auto', - display: 'flex', - borderRadius: 4, - flexDirection: 'column', - }, - paper: { - maxWidth: 1000, - width: '100%', - maxHeight: 800, - height: 'calc(100% - 32px)', - - }, - flex: { - display: 'flex', - }, - overflowHidden: { - overflow: 'hidden', - }, - overflowAuto: { - overflowY: 'auto', - }, - pre: { - overflow: 'auto', - margin: 20, - '& p': { - fontSize: 18, - }, - }, - blockInfo: { - right: 20, - top: 10, - position: 'absolute', - display: 'flex', - alignItems: 'center', - color: 'silver', - }, - img: { - marginLeft: 10, - width: 45, - height: 45, - margin: 'auto 0', - position: 'relative', - '&:after': { - content: '""', - position: 'absolute', - zIndex: 2, - top: 0, - left: 0, - width: '100%', - height: '100%', - background: 'url("img/no-image.png") 100% 100% no-repeat', - backgroundSize: 'cover', - backgroundColor: '#fff', - }, - }, - message: { - justifyContent: 'space-between', - display: 'flex', - width: '100%', - alignItems: 'center', - }, - column: { - flexDirection: 'column', - }, - headerText: { - fontWeight: 'bold', - fontSize: 15, - }, - descriptionHeaderText: { - margin: '10px 0', - }, - silver: { - color: 'silver', - }, - button: { - paddingTop: 18, - paddingBottom: 5, - position: 'sticky', - bottom: 0, - background: 'white', - zIndex: 3, - }, - terminal: { - fontFamily: 'monospace', - fontSize: 14, - marginLeft: 20, - }, - img2: { - width: 25, - height: 25, - marginRight: 10, - margin: 'auto 0', - position: 'relative', - '&:after': { - content: '""', - position: 'absolute', - zIndex: 2, - top: 0, - left: 0, - width: '100%', - height: '100%', - background: 'url("img/no-image.png") 100% 100% no-repeat', - backgroundSize: 'cover', - backgroundColor: '#fff', - }, - }, - heading: { - display: 'flex', - alignItems: 'center', - }, - headerBlock: { - backgroundColor: '#272727', - padding: 13, - fontSize: 16, - }, - headerBlockDisplay: { - backgroundColor: '#272727', - padding: 13, - fontSize: 16, - display: 'flex', - }, - headerBlockDisplayItem: { - padding: 5, - fontSize: 16, - display: 'flex', - margin: 2, - border: '1px solid #c0c0c045', - borderRadius: 4, - alignItems: 'center', - transition: 'background .5s, color .5s', - }, - activeBlock: { - background: '#c0c0c021', - border: '1px solid #4dabf5', - }, - pointer: { - cursor: 'pointer', - }, - hover: { - '&:hover': { - background: '#c0c0c021', - }, - }, - installSuccess: { - opacity: 0.7, - color: '#5ef05e', - }, - installError: { - opacity: 0.7, - color: '#ffc14f', - }, - width200: { - width: 200, - }, - table: { - // '& *': { - // color: 'black' - // } - }, - paperTable: { - width: '100%', - marginBottom: theme.spacing(2), - }, - wrapperSwitch: { - display: 'flex', - margin: 10, - marginTop: 0, - }, - divSwitch: { - display: 'flex', - // margin: 10, - alignItems: 'center', - fontSize: 10, - marginLeft: 0, - color: 'silver', - }, - marginLeft: { - marginLeft: 40, - }, - stepper: { - padding: 0, - background: 'inherit', - }, - instanceIcon: { - width: 30, - height: 30, - margin: 3, - }, - instanceId: { - marginLeft: 10, - }, - instanceWrapper: { - display: 'flex', - alignItems: 'center', - }, -})); - -const TabPanel = ({ - classes, children, value, index, title, custom, boxHeight, black, ...props -}) => { - if (custom) { - return
- {value === index && children} -
; - } - if (value === index) { - return
- -
- {title} -
-
- - - {children} - - -
; - } - return null; -}; - -const headCells = [ - { - id: 'instance', numeric: false, disablePadding: true, label: 'Instance', - }, - { - id: 'host', numeric: false, disablePadding: false, label: 'Host', - }, - { - id: 'description', numeric: false, disablePadding: false, label: 'Description', - }, - { - id: 'ignore', numeric: true, disablePadding: false, label: 'Ignore', - }, -]; - -function EnhancedTableHead(props) { - const { numSelected, rowCount, onSelectAllClick } = props; - - return - - - 0 && numSelected < rowCount} - checked={rowCount > 0 && numSelected === rowCount} - onChange={onSelectAllClick} - inputProps={{ 'aria-label': 'select all desserts' }} - /> - - {headCells.map(headCell => ( - - - {headCell.label} - - - ))} - - ; -} - -const buildComment = (comment, t) => { - if (!comment) { - return 'new'; - } - if (typeof comment === 'string') { - return comment; - } - let text = ''; - - if (comment.add) { - text += t('new'); - if (Array.isArray(comment.add) && comment.add.length) { - text += ': '; - if (comment.add.length <= 5) { - text += comment.add.join(', '); - } else { - text += t('%s devices', comment.add.length); - } - } else if (typeof comment.add === 'string' || typeof comment.add === 'number') { - text += ': '; - text += comment.add; - } - } - - if (comment.changed) { - text += (text ? ', ' : '') + t('changed'); - if (Array.isArray(comment.changed === 'object') && comment.changed.length) { - text += ': '; - if (comment.changed.length <= 5) { - text += comment.changed.join(', '); - } else { - text += t('%s devices', comment.changed.length); - } - } else if (typeof comment.changed === 'string' || typeof comment.changed === 'number') { - text += ': '; - text += comment.changed; - } - } - - if (comment.extended) { - text += (text ? ', ' : '') + t('extended'); - if (Array.isArray(comment.extended) && comment.extended.length) { - text += ': '; - if (comment.extended.length <= 5) { - text += comment.extended.join(', '); - } else { - text += t('%s devices', comment.extended.length); - } - } else if (typeof comment.extended === 'string' || typeof comment.extended === 'number') { - text += ': '; - text += comment.extended; - } - } - - if (comment.text) { - text += (text ? ', ' : '') + comment.text; - } - return text; -}; - -const DiscoveryDialog = ({ - themeType, themeName, socket, dateFormat, currentHost, defaultLogLevel, repository, hosts, onClose, theme, -}) => { - const classes = useStyles(); - - const [step, setStep] = useState(0); - const [listMethods, setListMethods] = useState({}); - const [checkboxChecked, setCheckboxChecked] = useState({}); - const [disableScanner, setDisableScanner] = useState(false); - const [discoveryData, setDiscoveryData] = useState({}); - - useEffect(() => { - async function fetchData() { - const resultList = await socket.sendTo('system.adapter.discovery.0', 'listMethods', null); - const listChecked = {}; - let lastSelection = (window._localStorage || window.localStorage).getItem('App.discoveryLastSelection') || null; - if (lastSelection) { - try { - lastSelection = JSON.parse(lastSelection); - } catch (e) { - lastSelection = null; - } - } - - Object.keys(resultList).forEach(key => { - if (lastSelection) { - listChecked[key] = lastSelection[key]; - } else { - listChecked[key] = key !== 'serial'; - } - }); - - setCheckboxChecked(listChecked); - setListMethods(resultList); - } - - fetchData(); - }, [socket]); - - useEffect(() => { - async function readOldData() { - const dataDiscovery = await socket.getObject('system.discovery'); - dataDiscovery !== undefined && setDiscoveryData(dataDiscovery); - } - - readOldData(); - }, [socket]); - - const [aliveHosts, setAliveHosts] = useState({}); - const [checkSelectHosts, setCheckSelectHosts] = useState(false); - const [hostInstances, setHostInstances] = useState({}); - - useEffect(() => { - hosts.forEach(async ({ _id }) => { - const aliveValue = await socket.getState(`${_id}.alive`); - setAliveHosts(prev => ({ ...prev, [_id]: !aliveValue || aliveValue.val === null ? false : !!aliveValue.val })); - }); - - if (Object.keys(aliveHosts).filter(key => aliveHosts[key]).length > 1) { - setCheckSelectHosts(true); - } - }, [hosts, socket]); - - const [devicesFound, setDevicesFound] = useState(0); - const [devicesProgress, setDevicesProgress] = useState(0); - const [instancesFound, setInstancesFound] = useState(0); - const [scanRunning, setScanRunning] = useState(false); - const [servicesProgress, setServicesProgress] = useState(0); - const [selected, setSelected] = useState([]); - const [installProgress, setInstallProgress] = useState(false); - const [currentInstall, setCurrentInstall] = useState(1); - const [installStatus, setInstallStatus] = useState({}); - const [cmdName, setCmdName] = useState('install'); - const [suggested, setSuggested] = useStateLocal(true, 'discovery.suggested'); - const [showAll, setShowAll] = useStateLocal(true, 'discovery.showAll'); - const [showLicenseDialog, setShowLicenseDialog] = useState(false); - const [showInputsDialog, setShowInputsDialog] = useState(false); - - const black = themeType === 'dark'; - - const [instancesInputsParams, setInstancesInputsParams] = useState({}); - const steps = ['Select methods', 'Create instances', 'Installation process']; - const [logs, setLogs] = useState({}); - const [finishInstall, setFinishInstall] = useState(false); - const [selectLogsIndex, setSelectLogsIndex] = useState(1); - - const handlerInstall = (name, value) => { - if (!value) { - return; - } - switch (name) { - case 'discovery.0.devicesFound': - setDevicesFound(value.val); - break; - case 'discovery.0.devicesProgress': - setDevicesProgress(value.val); - break; - case 'discovery.0.instancesFound': - setInstancesFound(value.val); - break; - case 'discovery.0.scanRunning': - setScanRunning(value.val); - break; - case 'discovery.0.servicesProgress': - setServicesProgress(value.val); - break; - case 'system.discovery': - setDiscoveryData(value); - break; - default: - } - }; - - useEffect(() => { - socket.subscribeObject('system.discovery', handlerInstall); - socket.subscribeState('discovery.0.devicesFound', handlerInstall); - socket.subscribeState('discovery.0.devicesProgress', handlerInstall); - socket.subscribeState('discovery.0.instancesFound', handlerInstall); - socket.subscribeState('discovery.0.scanRunning', handlerInstall); - socket.subscribeState('discovery.0.servicesProgress', handlerInstall); - - return () => { - socket.unsubscribeObject('system.discovery', handlerInstall); - socket.unsubscribeState('discovery.0.devicesFound', handlerInstall); - socket.unsubscribeState('discovery.0.devicesProgress', handlerInstall); - socket.unsubscribeState('discovery.0.instancesFound', handlerInstall); - socket.unsubscribeState('discovery.0.scanRunning', handlerInstall); - socket.unsubscribeState('discovery.0.servicesProgress', handlerInstall); - }; - }, [socket]); - - const stepUp = () => setStep(step + 1); - - const stepDown = () => setStep(step - 1); - - const extendObject = (id, data) => - socket.extendObject(id, data) - .catch(error => window.alert(error)); - - const discoverScanner = async () => { - setDisableScanner(true); - const dataArray = Object.keys(checkboxChecked).filter(key => checkboxChecked[key]); - const resultList = await socket.sendTo('system.adapter.discovery.0', 'browse', dataArray); - setDisableScanner(false); - if (resultList.error) { - window.alert(resultList.error); - } else { - setStep(1); - } - }; - - const handleSelectAllClick = event => { - if (event.target.checked) { - const newSelected = discoveryData?.native?.newInstances?.map(n => n._id); - setSelected(newSelected); - return; - } - setSelected([]); - }; - - const isSelected = (name, arr) => arr.includes(name); - - const handleClick = (event, name, arr, func) => { - const selectedIndex = arr.indexOf(name); - let newSelected = []; - - if (selectedIndex === -1) { - newSelected = newSelected.concat(arr, name); - } else if (selectedIndex === 0) { - newSelected = newSelected.concat(arr.slice(1)); - } else if (selectedIndex === arr.length - 1) { - newSelected = newSelected.concat(arr.slice(0, -1)); - } else if (selectedIndex > 0) { - newSelected = newSelected.concat( - arr.slice(0, selectedIndex), - arr.slice(selectedIndex + 1), - ); - } - - func(newSelected); - }; - - const checkLicenseAndInputs = (objName, cb) => { - const obj = JSON.parse(JSON.stringify(discoveryData?.native?.newInstances.find(ob => ob._id === objName))); - let license = true; - if (obj?.comment?.license && obj.comment.license !== 'MIT') { - license = false; - if (!obj.common.licenseUrl) { - obj.common.licenseUrl = `https://raw.githubusercontent.com/ioBroker/ioBroker.${obj.common.name}/master/LICENSE`; - } - if (typeof obj.common.licenseUrl === 'object') { - obj.common.licenseUrl = obj.common.licenseUrl[I18n.getLanguage()] || obj.common.licenseUrl.en; - } - if (obj.common.licenseUrl.includes('github.com')) { - obj.common.licenseUrl = obj.common.licenseUrl.replace('github.com', 'raw.githubusercontent.com').replace('/blob/', '/'); - } - } - - if (license) { - if (obj.comment?.inputs) { - setShowInputsDialog({ cb, obj }); - } else { - cb(); - } - } else { - setShowLicenseDialog({ cb, obj }); - } - }; - - const goToNextInstance = (id, reason) => { - const index = selected.indexOf(id) + 1; - setInstallStatus(status => ({ ...status, [index]: 'error' })); - - if (reason) { - setLogs(logsEl => ({ ...logsEl, [selected[index - 1]]: [I18n.t(reason)] })); - } - - if (selected.length > index) { - setTimeout(() => - checkLicenseAndInputs(selected[index], () => { - setCurrentInstall(index + 1); - setCmdName('install'); - setInstallProgress(true); - }), 100); - } else { - setFinishInstall(true); - } - }; - - const inputsDialog = showInputsDialog ? { - const cb = showInputsDialog.cb; - const obj = showInputsDialog.obj; - setShowInputsDialog(false); - - if (params) { - setInstancesInputsParams(params); - cb(); - } else { - goToNextInstance(obj._id, 'Error: configuration dialog canceled'); - } - }} - /> : null; - - const resetStateBack = () => { - setSelected([]); - setInstallProgress(false); - setFinishInstall(false); - setCurrentInstall(1); - setCmdName('install'); - setInstallStatus({}); - }; - - const checkInstall = () => { - checkLicenseAndInputs(selected[0], () => { - setCurrentInstall(1); - setInstallProgress(true); - }); - }; - - const licenseDialog = showLicenseDialog ? { - const cb = showLicenseDialog.cb; - const obj = showLicenseDialog.obj; - setShowLicenseDialog(false); - if (!result) { - // license isn't accepted, go to the next instance - goToNextInstance(obj._id, 'Error: license not accepted'); - } else if (obj.comment?.inputs) { - setShowInputsDialog({ cb, obj }); - } else { - cb(); - } - }} - /> : null; - - return - {licenseDialog} - {inputsDialog} - { - if (reason !== 'backdropClick' && reason !== 'escapeKeyDown') { - onClose(); - } - }} - open={!0} - classes={{ paper: classes.paper }} - > -

- - {I18n.t('Find devices and services')} -

- - {steps.map(label => ( - - {I18n.t(label)} - - ))} - - -
- - {!disableScanner ? <> - {' '} -
- {I18n.t('press_discover')} -
- {discoveryData?.native?.lastScan &&
- {I18n.t('Last scan on %s', Utils.formatDate(new Date(discoveryData.native.lastScan), dateFormat))} -
} -
- {I18n.t('Use following methods:')} - -
- {Object.keys(listMethods).map(key =>
- { - const newCheckboxChecked = JSON.parse(JSON.stringify(checkboxChecked)); - newCheckboxChecked[key] = value; - (window._localStorage || window.localStorage).setItem('App.discoveryLastSelection', JSON.stringify(newCheckboxChecked)); - setCheckboxChecked(newCheckboxChecked); - }} - /> - {key} -
)} - - : (scanRunning &&
- {devicesProgress >= 99 ? `Lookup services - ${servicesProgress}%` : `Lookup devices - ${devicesProgress}%`} - {disableScanner && = 99 ? servicesProgress : devicesProgress} />} - {devicesProgress >= 99 ? `${instancesFound} service(s) found` : `${devicesFound} device(s) found`} -
)} -
- -
-
-
{I18n.t('hide ignored')}
- setShowAll(e.target.checked)} - color="primary" - /> -
{I18n.t('show ignored')}
-
-
-
{I18n.t('hide suggested')}
- setSuggested(e.target.checked)} - color="primary" - /> -
{I18n.t('show suggested')}
-
-
- - - - - - {discoveryData?.native?.newInstances?.filter(el => { - if (!suggested) { - return !el.comment?.advice; - } - if (!showAll) { - return !el?.comment?.ack; - } - return true; - }).map((obj, idx) => - - handleClick(e, obj._id, selected, setSelected)} - /> - - -
- -
- {obj._id.replace('system.adapter.', '')} -
-
-
- - {checkSelectHosts ? - - setHostInstances({ ...hostInstances, [obj._id]: val })} - /> - : - '_'} - - {buildComment(obj.comment, I18n.t)} - - { - const newInstances = JSON.parse(JSON.stringify(discoveryData?.native.newInstances)); - newInstances[idx].comment = { ...newInstances[idx].comment, ack: !newInstances[idx].comment.ack }; - extendObject('system.discovery', { native: { newInstances } }); - }} - /> - -
)} -
-
-
-
-
- -
-
- {selected.map((el, idx) =>
setSelectLogsIndex(idx) : null} - className={Utils.clsx( - classes.headerBlockDisplayItem, - finishInstall && classes.pointer, - finishInstall && classes.hover, - finishInstall && selectLogsIndex === idx && classes.activeBlock, - )} - > -
-
- -
- {el.replace('system.adapter.', '')} -
-
-
- {currentInstall === idx + 1 && !installStatus[idx + 1] && } - {installStatus[idx + 1] === 'error' ? : installStatus[idx + 1] === 'success' ? : null} -
)} -
- {currentInstall && (installProgress || finishInstall) &&
- { - let data = JSON.parse(JSON.stringify(discoveryData?.native.newInstances.find(obj => - obj._id === selected[currentInstall - 1]))); - delete data.comment; - - let adapterId = data._id.split('.'); - adapterId.pop(); - adapterId = adapterId.join('.'); - socket.getObject(adapterId) - .then(obj => { - data = { ...obj, ...data }; - data.common = Object.assign(obj.common, data.common); - data.native = Object.assign(obj.native, data.native); - data.type = 'instance'; - - // set log level - if (defaultLogLevel) { - data.common.logLevel = defaultLogLevel; - } - data.common.logLevel = data.common.logLevel || 'info'; - - if (instancesInputsParams.native && Object.keys(instancesInputsParams.native).length) { - Object.assign(data.native, instancesInputsParams.native); - setInstancesInputsParams({}); - } - if (checkSelectHosts && hostInstances[data._id]) { - data.common.host = hostInstances[data._id]; - } - - // write created instance - extendObject(data._id, data) - .then(() => { - if (currentInstall < selected.length) { - // install next - checkLicenseAndInputs(selected[currentInstall], () => { - setCurrentInstall(currentInstall + 1); - setCmdName('install'); - }); - setLogs({ ...logs, [selected[currentInstall - 1]]: logsSuccess }); - setInstallStatus({ ...installStatus, [currentInstall]: 'success' }); - } else { - setLogs({ ...logs, [selected[currentInstall - 1]]: logsSuccess }); - setInstallStatus({ ...installStatus, [currentInstall]: 'success' }); - setSelectLogsIndex(currentInstall - 1); - const dataDiscovery = JSON.parse(JSON.stringify(discoveryData)); - if (dataDiscovery) { - dataDiscovery.native.newInstances = dataDiscovery.native.newInstances.filter(({ _id }) => { - const find = selected.find(el => el === _id); - if (!find) { - return true; - } - return installStatus[selected.indexOf(find) + 1] !== 'success'; - }); - socket.setObject('system.discovery', dataDiscovery); - } - setFinishInstall(true); - window.alert(I18n.t('Finished')); - } - }); - }); - }} - errorFunc={(el, logsError) => { - if (el === 51 && cmdName === 'install') { - setCmdName('upload'); - return; - } - if (selected.length > currentInstall && cmdName === 'upload') { - checkLicenseAndInputs(selected[currentInstall], () => - setCurrentInstall(currentInstall + 1)); - setInstallStatus({ ...installStatus, [currentInstall]: 'error' }); - } else { - if (selected.length > currentInstall) { - setInstallStatus({ ...installStatus, [currentInstall]: 'error' }); - checkLicenseAndInputs(selected[currentInstall], () => - setCurrentInstall(currentInstall + 1)); - setCmdName('install'); - setLogs({ ...logs, [selected[currentInstall - 1]]: logsError }); - } else { - setInstallStatus({ ...installStatus, [currentInstall]: 'error' }); - setLogs({ ...logs, [selected[currentInstall - 1]]: logsError }); - setFinishInstall(true); - setSelectLogsIndex(currentInstall - 1); - const dataDiscovery = JSON.parse(JSON.stringify(discoveryData)); - if (dataDiscovery) { - dataDiscovery.native.newInstances = dataDiscovery.native.newInstances.filter(({ _id }) => { - const find = selected.find(ele => ele === _id); - if (!find) { - return true; - } - return installStatus[selected.indexOf(find) + 1] !== 'success'; - }); - socket.setObject('system.discovery', dataDiscovery); - } - } - window.alert(`error ${selected[currentInstall - 1]}`); - } - }} - /> -
} -
-
-
-
- - {step > 0 && step !== 4 && } - {step === 0 && } - {step !== 2 && step !== 4 && - - - - - } - - -
-
; -}; - -export default DiscoveryDialog; diff --git a/src/src/dialogs/DiscoveryDialog.tsx b/src/src/dialogs/DiscoveryDialog.tsx new file mode 100644 index 000000000..89cdbfa0c --- /dev/null +++ b/src/src/dialogs/DiscoveryDialog.tsx @@ -0,0 +1,1228 @@ +import React, { useEffect, useState } from 'react'; + +import Button from '@mui/material/Button'; +import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import { + Tooltip, + AppBar, + Avatar, + Box, + Checkbox, + CircularProgress, + LinearProgress, + Paper, + Step, + StepLabel, + Stepper, + Switch, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TableSortLabel, + Typography, +} from '@mui/material'; +import { ThemeProvider } from '@mui/material/styles'; +import { makeStyles } from '@mui/styles'; + +import VisibilityIcon from '@mui/icons-material/Visibility'; +import NavigateNextIcon from '@mui/icons-material/NavigateNext'; +import NavigateBeforeIcon from '@mui/icons-material/NavigateBefore'; +import SearchIcon from '@mui/icons-material/Search'; +import CloseIcon from '@mui/icons-material/Close'; +import LibraryAddIcon from '@mui/icons-material/LibraryAdd'; +import AssignmentTurnedInIcon from '@mui/icons-material/AssignmentTurnedIn'; +import ReportProblemIcon from '@mui/icons-material/ReportProblem'; + +import { I18n, Utils, SelectWithIcon } from '@iobroker/adapter-react-v5'; + +import Command from '../components/Command'; +import LicenseDialog from './LicenseDialog'; +import GenerateInputsModal from './GenerateInputsModal'; +import useStateLocal from '../helpers/hooks/useStateLocal'; + +const useStyles = makeStyles(theme => ({ + root: { + // backgroundColor: theme.palette.background.paper, + width: '100%', + height: 'auto', + display: 'flex', + borderRadius: 4, + flexDirection: 'column', + }, + paper: { + maxWidth: 1000, + width: '100%', + maxHeight: 800, + height: 'calc(100% - 32px)', + }, + flex: { + display: 'flex', + }, + overflowHidden: { + overflow: 'hidden', + }, + overflowAuto: { + overflowY: 'auto', + }, + pre: { + overflow: 'auto', + margin: 20, + '& p': { + fontSize: 18, + }, + }, + blockInfo: { + right: 20, + top: 10, + position: 'absolute', + display: 'flex', + alignItems: 'center', + color: 'silver', + }, + img: { + marginLeft: 10, + width: 45, + height: 45, + margin: 'auto 0', + position: 'relative', + '&:after': { + content: '""', + position: 'absolute', + zIndex: 2, + top: 0, + left: 0, + width: '100%', + height: '100%', + background: 'url("img/no-image.png") 100% 100% no-repeat', + backgroundSize: 'cover', + backgroundColor: '#fff', + }, + }, + message: { + justifyContent: 'space-between', + display: 'flex', + width: '100%', + alignItems: 'center', + }, + column: { + flexDirection: 'column', + }, + headerText: { + fontWeight: 'bold', + fontSize: 15, + }, + descriptionHeaderText: { + margin: '10px 0', + }, + silver: { + color: 'silver', + }, + button: { + paddingTop: 18, + paddingBottom: 5, + position: 'sticky', + bottom: 0, + background: 'white', + zIndex: 3, + }, + terminal: { + fontFamily: 'monospace', + fontSize: 14, + marginLeft: 20, + }, + img2: { + width: 25, + height: 25, + marginRight: 10, + margin: 'auto 0', + position: 'relative', + '&:after': { + content: '""', + position: 'absolute', + zIndex: 2, + top: 0, + left: 0, + width: '100%', + height: '100%', + background: 'url("img/no-image.png") 100% 100% no-repeat', + backgroundSize: 'cover', + backgroundColor: '#fff', + }, + }, + heading: { + display: 'flex', + alignItems: 'center', + }, + headerBlock: { + backgroundColor: '#272727', + padding: 13, + fontSize: 16, + }, + headerBlockDisplay: { + backgroundColor: '#272727', + padding: 13, + fontSize: 16, + display: 'flex', + }, + headerBlockDisplayItem: { + padding: 5, + fontSize: 16, + display: 'flex', + margin: 2, + border: '1px solid #c0c0c045', + borderRadius: 4, + alignItems: 'center', + transition: 'background .5s, color .5s', + }, + activeBlock: { + background: '#c0c0c021', + border: '1px solid #4dabf5', + }, + pointer: { + cursor: 'pointer', + }, + hover: { + '&:hover': { + background: '#c0c0c021', + }, + }, + installSuccess: { + opacity: 0.7, + color: '#5ef05e', + }, + installError: { + opacity: 0.7, + color: '#ffc14f', + }, + width200: { + width: 200, + }, + table: { + // '& *': { + // color: 'black' + // } + }, + paperTable: { + width: '100%', + marginBottom: (theme as any).spacing(2), + }, + wrapperSwitch: { + display: 'flex', + margin: 10, + marginTop: 0, + }, + divSwitch: { + display: 'flex', + // margin: 10, + alignItems: 'center', + fontSize: 10, + marginLeft: 0, + color: 'silver', + }, + marginLeft: { + marginLeft: 40, + }, + stepper: { + padding: 0, + background: 'inherit', + }, + instanceIcon: { + width: 30, + height: 30, + margin: 3, + }, + instanceId: { + marginLeft: 10, + }, + instanceWrapper: { + display: 'flex', + alignItems: 'center', + }, +})); + +interface TabPanelProps { + classes: Record; + children: any; + value: any; + index: any; + title: string; + custom?: boolean; + boxHeight?: boolean; + black?: boolean; + [other: string]: unknown; +} + +const TabPanel = ({ classes, children, value, index, title, custom, boxHeight, black, ...props }: TabPanelProps) => { + if (custom) { + return
{value === index && children}
; + } + if (value === index) { + return ( +
+ +
+ {title} +
+
+ + + {children} + + +
+ ); + } + return null; +}; + +const headCells = [ + { + id: 'instance', + numeric: false, + disablePadding: true, + label: 'Instance', + }, + { + id: 'host', + numeric: false, + disablePadding: false, + label: 'Host', + }, + { + id: 'description', + numeric: false, + disablePadding: false, + label: 'Description', + }, + { + id: 'ignore', + numeric: true, + disablePadding: false, + label: 'Ignore', + }, +]; + +function EnhancedTableHead(props) { + const { numSelected, rowCount, onSelectAllClick } = props; + + return ( + + + + 0 && numSelected < rowCount} + checked={rowCount > 0 && numSelected === rowCount} + onChange={onSelectAllClick} + inputProps={{ 'aria-label': 'select all desserts' }} + /> + + {headCells.map(headCell => ( + + {headCell.label} + + ))} + + + ); +} + +const buildComment = (comment, t) => { + if (!comment) { + return 'new'; + } + if (typeof comment === 'string') { + return comment; + } + let text = ''; + + if (comment.add) { + text += t('new'); + if (Array.isArray(comment.add) && comment.add.length) { + text += ': '; + if (comment.add.length <= 5) { + text += comment.add.join(', '); + } else { + text += t('%s devices', comment.add.length); + } + } else if (typeof comment.add === 'string' || typeof comment.add === 'number') { + text += ': '; + text += comment.add; + } + } + + if (comment.changed) { + text += (text ? ', ' : '') + t('changed'); + if (Array.isArray(comment.changed === 'object') && comment.changed.length) { + text += ': '; + if (comment.changed.length <= 5) { + text += comment.changed.join(', '); + } else { + text += t('%s devices', comment.changed.length); + } + } else if (typeof comment.changed === 'string' || typeof comment.changed === 'number') { + text += ': '; + text += comment.changed; + } + } + + if (comment.extended) { + text += (text ? ', ' : '') + t('extended'); + if (Array.isArray(comment.extended) && comment.extended.length) { + text += ': '; + if (comment.extended.length <= 5) { + text += comment.extended.join(', '); + } else { + text += t('%s devices', comment.extended.length); + } + } else if (typeof comment.extended === 'string' || typeof comment.extended === 'number') { + text += ': '; + text += comment.extended; + } + } + + if (comment.text) { + text += (text ? ', ' : '') + comment.text; + } + return text; +}; + +const DiscoveryDialog = ({ + themeType, + themeName, + socket, + dateFormat, + currentHost, + defaultLogLevel, + repository, + hosts, + onClose, + theme, +}) => { + const classes = useStyles(); + + const [step, setStep] = useState(0); + const [listMethods, setListMethods] = useState>({}); + const [checkboxChecked, setCheckboxChecked] = useState>({}); + const [disableScanner, setDisableScanner] = useState(false); + const [discoveryData, setDiscoveryData] = useState>({}); + + useEffect(() => { + async function fetchData() { + const resultList = await socket.sendTo('system.adapter.discovery.0', 'listMethods', null); + const listChecked = {}; + let lastSelection = + ((window as any)._localStorage || window.localStorage).getItem('App.discoveryLastSelection') || null; + if (lastSelection) { + try { + lastSelection = JSON.parse(lastSelection); + } catch (e) { + lastSelection = null; + } + } + + Object.keys(resultList).forEach(key => { + if (lastSelection) { + listChecked[key] = lastSelection[key]; + } else { + listChecked[key] = key !== 'serial'; + } + }); + + setCheckboxChecked(listChecked); + setListMethods(resultList); + } + + fetchData(); + }, [socket]); + + useEffect(() => { + async function readOldData() { + const dataDiscovery = await socket.getObject('system.discovery'); + dataDiscovery !== undefined && setDiscoveryData(dataDiscovery); + } + + readOldData(); + }, [socket]); + + const [aliveHosts, setAliveHosts] = useState({}); + const [checkSelectHosts, setCheckSelectHosts] = useState(false); + const [hostInstances, setHostInstances] = useState({}); + + useEffect(() => { + hosts.forEach(async ({ _id }) => { + const aliveValue = await socket.getState(`${_id}.alive`); + setAliveHosts(prev => ({ + ...prev, + [_id]: !aliveValue || aliveValue.val === null ? false : !!aliveValue.val, + })); + }); + + if (Object.keys(aliveHosts).filter(key => aliveHosts[key]).length > 1) { + setCheckSelectHosts(true); + } + }, [hosts, socket]); + + const [devicesFound, setDevicesFound] = useState(0); + const [devicesProgress, setDevicesProgress] = useState(0); + const [instancesFound, setInstancesFound] = useState(0); + const [scanRunning, setScanRunning] = useState(false); + const [servicesProgress, setServicesProgress] = useState(0); + const [selected, setSelected] = useState([]); + const [installProgress, setInstallProgress] = useState(false); + const [currentInstall, setCurrentInstall] = useState(1); + const [installStatus, setInstallStatus] = useState({}); + const [cmdName, setCmdName] = useState('install'); + const [suggested, setSuggested] = useStateLocal(true, 'discovery.suggested'); + const [showAll, setShowAll] = useStateLocal(true, 'discovery.showAll'); + const [showLicenseDialog, setShowLicenseDialog] = useState(false); + const [showInputsDialog, setShowInputsDialog] = useState(false); + + const black = themeType === 'dark'; + + const [instancesInputsParams, setInstancesInputsParams] = useState>({}); + const steps = ['Select methods', 'Create instances', 'Installation process']; + const [logs, setLogs] = useState({}); + const [finishInstall, setFinishInstall] = useState(false); + const [selectLogsIndex, setSelectLogsIndex] = useState(1); + + const handlerInstall = (name, value) => { + if (!value) { + return; + } + switch (name) { + case 'discovery.0.devicesFound': + setDevicesFound(value.val); + break; + case 'discovery.0.devicesProgress': + setDevicesProgress(value.val); + break; + case 'discovery.0.instancesFound': + setInstancesFound(value.val); + break; + case 'discovery.0.scanRunning': + setScanRunning(value.val); + break; + case 'discovery.0.servicesProgress': + setServicesProgress(value.val); + break; + case 'system.discovery': + setDiscoveryData(value); + break; + default: + } + }; + + useEffect(() => { + socket.subscribeObject('system.discovery', handlerInstall); + socket.subscribeState('discovery.0.devicesFound', handlerInstall); + socket.subscribeState('discovery.0.devicesProgress', handlerInstall); + socket.subscribeState('discovery.0.instancesFound', handlerInstall); + socket.subscribeState('discovery.0.scanRunning', handlerInstall); + socket.subscribeState('discovery.0.servicesProgress', handlerInstall); + + return () => { + socket.unsubscribeObject('system.discovery', handlerInstall); + socket.unsubscribeState('discovery.0.devicesFound', handlerInstall); + socket.unsubscribeState('discovery.0.devicesProgress', handlerInstall); + socket.unsubscribeState('discovery.0.instancesFound', handlerInstall); + socket.unsubscribeState('discovery.0.scanRunning', handlerInstall); + socket.unsubscribeState('discovery.0.servicesProgress', handlerInstall); + }; + }, [socket]); + + const stepUp = () => setStep(step + 1); + + const stepDown = () => setStep(step - 1); + + const extendObject = (id, data) => socket.extendObject(id, data).catch(error => window.alert(error)); + + const discoverScanner = async () => { + setDisableScanner(true); + const dataArray = Object.keys(checkboxChecked).filter(key => checkboxChecked[key]); + const resultList = await socket.sendTo('system.adapter.discovery.0', 'browse', dataArray); + setDisableScanner(false); + if (resultList.error) { + window.alert(resultList.error); + } else { + setStep(1); + } + }; + + const handleSelectAllClick = event => { + if (event.target.checked) { + const newSelected = discoveryData?.native?.newInstances?.map(n => n._id); + setSelected(newSelected); + return; + } + setSelected([]); + }; + + const isSelected = (name, arr) => arr.includes(name); + + const handleClick = (event, name, arr, func) => { + const selectedIndex = arr.indexOf(name); + let newSelected = []; + + if (selectedIndex === -1) { + newSelected = newSelected.concat(arr, name); + } else if (selectedIndex === 0) { + newSelected = newSelected.concat(arr.slice(1)); + } else if (selectedIndex === arr.length - 1) { + newSelected = newSelected.concat(arr.slice(0, -1)); + } else if (selectedIndex > 0) { + newSelected = newSelected.concat(arr.slice(0, selectedIndex), arr.slice(selectedIndex + 1)); + } + + func(newSelected); + }; + + const checkLicenseAndInputs = (objName, cb) => { + const obj = JSON.parse(JSON.stringify(discoveryData?.native?.newInstances.find(ob => ob._id === objName))); + let license = true; + if (obj?.comment?.license && obj.comment.license !== 'MIT') { + license = false; + if (!obj.common.licenseUrl) { + obj.common.licenseUrl = `https://raw.githubusercontent.com/ioBroker/ioBroker.${obj.common.name}/master/LICENSE`; + } + if (typeof obj.common.licenseUrl === 'object') { + obj.common.licenseUrl = obj.common.licenseUrl[I18n.getLanguage()] || obj.common.licenseUrl.en; + } + if (obj.common.licenseUrl.includes('github.com')) { + obj.common.licenseUrl = obj.common.licenseUrl + .replace('github.com', 'raw.githubusercontent.com') + .replace('/blob/', '/'); + } + } + + if (license) { + if (obj.comment?.inputs) { + setShowInputsDialog({ cb, obj }); + } else { + cb(); + } + } else { + setShowLicenseDialog({ cb, obj }); + } + }; + + const goToNextInstance = (id, reason) => { + const index = selected.indexOf(id) + 1; + setInstallStatus(status => ({ ...status, [index]: 'error' })); + + if (reason) { + setLogs(logsEl => ({ ...logsEl, [selected[index - 1]]: [I18n.t(reason)] })); + } + + if (selected.length > index) { + setTimeout( + () => + checkLicenseAndInputs(selected[index], () => { + setCurrentInstall(index + 1); + setCmdName('install'); + setInstallProgress(true); + }), + 100 + ); + } else { + setFinishInstall(true); + } + }; + + const inputsDialog = showInputsDialog ? ( + { + const cb = showInputsDialog.cb; + const obj = showInputsDialog.obj; + setShowInputsDialog(false); + + if (params) { + setInstancesInputsParams(params); + cb(); + } else { + goToNextInstance(obj._id, 'Error: configuration dialog canceled'); + } + }} + /> + ) : null; + + const resetStateBack = () => { + setSelected([]); + setInstallProgress(false); + setFinishInstall(false); + setCurrentInstall(1); + setCmdName('install'); + setInstallStatus({}); + }; + + const checkInstall = () => { + checkLicenseAndInputs(selected[0], () => { + setCurrentInstall(1); + setInstallProgress(true); + }); + }; + + const licenseDialog = showLicenseDialog ? ( + { + const cb = showLicenseDialog.cb; + const obj = showLicenseDialog.obj; + setShowLicenseDialog(false); + if (!result) { + // license isn't accepted, go to the next instance + goToNextInstance(obj._id, 'Error: license not accepted'); + } else if (obj.comment?.inputs) { + setShowInputsDialog({ cb, obj }); + } else { + cb(); + } + }} + /> + ) : null; + + return ( + + {licenseDialog} + {inputsDialog} + { + if (reason !== 'backdropClick' && reason !== 'escapeKeyDown') { + onClose(); + } + }} + open={!0} + classes={{ paper: classes.paper }} + > +

+ + {I18n.t('Find devices and services')} +

+ + {steps.map(label => ( + + {I18n.t(label)} + + ))} + + +
+ + {!disableScanner ? ( + <> + {' '} +
{I18n.t('press_discover')}
+ {discoveryData?.native?.lastScan && ( +
+ {I18n.t( + 'Last scan on %s', + Utils.formatDate(new Date(discoveryData.native.lastScan), dateFormat) + )} +
+ )} +
+ {I18n.t('Use following methods:')} +
+ {Object.keys(listMethods).map(key => ( +
+ { + const newCheckboxChecked = JSON.parse( + JSON.stringify(checkboxChecked) + ); + newCheckboxChecked[key] = value; + ((window as any)._localStorage || window.localStorage).setItem( + 'App.discoveryLastSelection', + JSON.stringify(newCheckboxChecked) + ); + setCheckboxChecked(newCheckboxChecked); + }} + /> + {key} +
+ ))} + + ) : ( + scanRunning && ( +
+ {devicesProgress >= 99 + ? `Lookup services - ${servicesProgress}%` + : `Lookup devices - ${devicesProgress}%`} + {disableScanner && ( + = 99 ? servicesProgress : devicesProgress} + /> + )} + {devicesProgress >= 99 + ? `${instancesFound} service(s) found` + : `${devicesFound} device(s) found`} +
+ ) + )} +
+ +
+
+
+ {I18n.t('hide ignored')} +
+ setShowAll(e.target.checked)} + color="primary" + /> +
{I18n.t('show ignored')}
+
+
+
+ {I18n.t('hide suggested')} +
+ setSuggested(e.target.checked)} + color="primary" + /> +
+ {I18n.t('show suggested')} +
+
+
+ + + + + + {discoveryData?.native?.newInstances + ?.filter(el => { + if (!suggested) { + return !el.comment?.advice; + } + if (!showAll) { + return !el?.comment?.ack; + } + return true; + }) + .map((obj, idx) => ( + + + + handleClick(e, obj._id, selected, setSelected) + } + /> + + +
+ +
+ {obj._id.replace('system.adapter.', '')} +
+
+
+ + {checkSelectHosts ? ( + + setHostInstances({ + ...hostInstances, + [obj._id]: val, + }) + } + /> + ) : ( + '_' + )} + + + {buildComment(obj.comment, I18n.t)} + + + { + const newInstances = JSON.parse( + JSON.stringify( + discoveryData?.native.newInstances + ) + ); + newInstances[idx].comment = { + ...newInstances[idx].comment, + ack: !newInstances[idx].comment.ack, + }; + extendObject('system.discovery', { + native: { newInstances }, + }); + }} + /> + +
+ ))} +
+
+
+
+
+ +
+
+ {selected.map((el, idx) => ( +
setSelectLogsIndex(idx) : undefined} + className={Utils.clsx( + classes.headerBlockDisplayItem, + finishInstall && classes.pointer, + finishInstall && classes.hover, + finishInstall && selectLogsIndex === idx && classes.activeBlock + )} + > +
+
+ +
+ {el.replace('system.adapter.', '')} +
+
+
+ {currentInstall === idx + 1 && !installStatus[idx + 1] && ( + + )} + {installStatus[idx + 1] === 'error' ? ( + + ) : installStatus[idx + 1] === 'success' ? ( + + ) : null} +
+ ))} +
+ {currentInstall && (installProgress || finishInstall) && ( +
+ { + let data = JSON.parse( + JSON.stringify( + discoveryData?.native.newInstances.find( + obj => obj._id === selected[currentInstall - 1] + ) + ) + ); + delete data.comment; + + let adapterId = data._id.split('.'); + adapterId.pop(); + adapterId = adapterId.join('.'); + socket.getObject(adapterId).then(obj => { + data = { ...obj, ...data }; + data.common = Object.assign(obj.common, data.common); + data.native = Object.assign(obj.native, data.native); + data.type = 'instance'; + + // set log level + if (defaultLogLevel) { + data.common.logLevel = defaultLogLevel; + } + data.common.logLevel = data.common.logLevel || 'info'; + + if ( + instancesInputsParams.native && + Object.keys(instancesInputsParams.native).length + ) { + Object.assign(data.native, instancesInputsParams.native); + setInstancesInputsParams({}); + } + if (checkSelectHosts && hostInstances[data._id]) { + data.common.host = hostInstances[data._id]; + } + + // write created instance + extendObject(data._id, data).then(() => { + if (currentInstall < selected.length) { + // install next + checkLicenseAndInputs(selected[currentInstall], () => { + setCurrentInstall(currentInstall + 1); + setCmdName('install'); + }); + setLogs({ + ...logs, + [selected[currentInstall - 1]]: logsSuccess, + }); + setInstallStatus({ + ...installStatus, + [currentInstall]: 'success', + }); + } else { + setLogs({ + ...logs, + [selected[currentInstall - 1]]: logsSuccess, + }); + setInstallStatus({ + ...installStatus, + [currentInstall]: 'success', + }); + setSelectLogsIndex(currentInstall - 1); + const dataDiscovery = JSON.parse( + JSON.stringify(discoveryData) + ); + if (dataDiscovery) { + dataDiscovery.native.newInstances = + dataDiscovery.native.newInstances.filter( + ({ _id }) => { + const find = selected.find( + el => el === _id + ); + if (!find) { + return true; + } + return ( + installStatus[ + selected.indexOf(find) + 1 + ] !== 'success' + ); + } + ); + socket.setObject('system.discovery', dataDiscovery); + } + setFinishInstall(true); + window.alert(I18n.t('Finished')); + } + }); + }); + }} + errorFunc={(el, logsError) => { + if (el === 51 && cmdName === 'install') { + setCmdName('upload'); + return; + } + if (selected.length > currentInstall && cmdName === 'upload') { + checkLicenseAndInputs(selected[currentInstall], () => + setCurrentInstall(currentInstall + 1) + ); + setInstallStatus({ ...installStatus, [currentInstall]: 'error' }); + } else { + if (selected.length > currentInstall) { + setInstallStatus({ + ...installStatus, + [currentInstall]: 'error', + }); + checkLicenseAndInputs(selected[currentInstall], () => + setCurrentInstall(currentInstall + 1) + ); + setCmdName('install'); + setLogs({ ...logs, [selected[currentInstall - 1]]: logsError }); + } else { + setInstallStatus({ + ...installStatus, + [currentInstall]: 'error', + }); + setLogs({ ...logs, [selected[currentInstall - 1]]: logsError }); + setFinishInstall(true); + setSelectLogsIndex(currentInstall - 1); + const dataDiscovery = JSON.parse(JSON.stringify(discoveryData)); + if (dataDiscovery) { + dataDiscovery.native.newInstances = + dataDiscovery.native.newInstances.filter(({ _id }) => { + const find = selected.find(ele => ele === _id); + if (!find) { + return true; + } + return ( + installStatus[selected.indexOf(find) + 1] !== + 'success' + ); + }); + socket.setObject('system.discovery', dataDiscovery); + } + } + window.alert(`error ${selected[currentInstall - 1]}`); + } + }} + /> +
+ )} +
+
+
+
+ + {step > 0 && step !== 4 && ( + + )} + {step === 0 && ( + + )} + {step !== 2 && step !== 4 && ( + + + + + + )} + + +
+
+ ); +}; + +export default DiscoveryDialog; diff --git a/src/src/dialogs/GenereteInputsModal.jsx b/src/src/dialogs/GenerateInputsModal.jsx similarity index 100% rename from src/src/dialogs/GenereteInputsModal.jsx rename to src/src/dialogs/GenerateInputsModal.jsx diff --git a/src/tsconfig.json b/src/tsconfig.json new file mode 100644 index 000000000..d3cdbb030 --- /dev/null +++ b/src/tsconfig.json @@ -0,0 +1,17 @@ +// Specialized tsconfig for the admin directory, +// includes DOM typings and configures the admin build +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "checkJs": true, + "noEmit": false, + "outDir": "./build", + "sourceMap": true, + "sourceRoot": "./src", + "noImplicitAny": false, + "lib": ["es2018", "DOM"], + "jsx": "react" + }, + "include": ["./**/*.ts", "./**/*.tsx", "../src/lib/adapter-config.d.ts"], + "exclude": ["./**/*.test.ts", "./**/*.test.tsx"] +} \ No newline at end of file From c4a2a9f0799f4be9daaac2246ef945c1a806ace6 Mon Sep 17 00:00:00 2001 From: foxriver76 Date: Fri, 1 Sep 2023 09:10:13 +0200 Subject: [PATCH 2/7] suppress unnecessary import warning --- src/src/App.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/src/App.jsx b/src/src/App.jsx index 8f9bc8f9f..6ddf58211 100644 --- a/src/src/App.jsx +++ b/src/src/App.jsx @@ -70,6 +70,7 @@ import InstancesWorker from './Workers/InstancesWorker'; import HostsWorker from './Workers/HostsWorker'; import AdaptersWorker from './Workers/AdaptersWorker'; import ObjectsWorker from './Workers/ObjectsWorker'; +// eslint-disable-next-line import/no-unresolved import DiscoveryDialog from './dialogs/DiscoveryDialog'; import SlowConnectionWarningDialog from './dialogs/SlowConnectionWarningDialog'; import IsVisible from './components/IsVisible'; From e6cd094fcc56ffcffd00a891517fba0bb7a91bd6 Mon Sep 17 00:00:00 2001 From: foxriver76 Date: Fri, 1 Sep 2023 09:58:55 +0200 Subject: [PATCH 3/7] setting a number value now renders a real number input - with min/max/step support - closes #2083 --- .../components/Object/ObjectBrowserValue.jsx | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/src/components/Object/ObjectBrowserValue.jsx b/src/src/components/Object/ObjectBrowserValue.jsx index dc00b4804..373c8002c 100644 --- a/src/src/components/Object/ObjectBrowserValue.jsx +++ b/src/src/components/Object/ObjectBrowserValue.jsx @@ -188,7 +188,7 @@ class ObjectBrowserValue extends Component { this.inputRef = React.createRef(); - this.chartFrom = Date.now() - 3600000 * 2; + this.chartFrom = Date.now() - 3_600_000 * 2; } componentDidMount() { @@ -203,10 +203,21 @@ class ObjectBrowserValue extends Component { } setTimeout(() => { - if (this.inputRef && this.inputRef.current) { + if (this.inputRef?.current) { const el = this.inputRef.current; const value = el.value || ''; + const origType = el.type; + + // type number cannot be selected, so we perform a short workaround + if (el.type === 'number') { + el.type = 'text'; + } + el.setSelectionRange(0, value.length); + + if (origType === 'number') { + el.type = origType; + } } }, 200); } @@ -534,11 +545,13 @@ class ObjectBrowserValue extends Component { variant="standard" classes={{ root: this.props.classes.textInput }} autoFocus + type="number" + inputProps={{ step: this.props.object.common.step, min: this.props.object.common.min, max: this.props.object.common.max }} inputRef={this.inputRef} helperText={this.props.t( 'Press ENTER to write the value, when focused', )} - value={parseFloat(this.state.targetValue) || 0} + value={this.state.targetValue || 0} label={this.props.t('Value')} onKeyUp={e => e.keyCode === 13 && this.onUpdate(e)} onChange={e => this.setState({ targetValue: e.target.value })} From f2c5a7574c70f21cae68c9a00ecc28674fa674d2 Mon Sep 17 00:00:00 2001 From: foxriver76 Date: Sat, 2 Sep 2023 12:07:35 +0200 Subject: [PATCH 4/7] also perform validation --- .../components/Object/ObjectBrowserValue.jsx | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/src/src/components/Object/ObjectBrowserValue.jsx b/src/src/components/Object/ObjectBrowserValue.jsx index 373c8002c..409a7eec1 100644 --- a/src/src/components/Object/ObjectBrowserValue.jsx +++ b/src/src/components/Object/ObjectBrowserValue.jsx @@ -180,6 +180,8 @@ class ObjectBrowserValue extends Component { chartEnabled: (window._localStorage || window.localStorage).getItem('App.chartSetValue') !== 'false', fullScreen: (window._localStorage || window.localStorage).getItem('App.fullScreen') === 'true', targetValue: value, + /** IF input is invalid, set value button is disabled */ + valid: true, }; this.ack = false; @@ -261,6 +263,33 @@ class ObjectBrowserValue extends Component { }); } + /** @typedef {{ value: unknown, common: ioBroker.StateCommon }} NumberValidationOptions */ + + /** + * Check if a number value is valid according to the objects common properties + * @param {NumberValidationOptions} options value and common information + * + * @returns {boolean} + */ + isNumberValid(options) { + const { common } = options; + let { value } = options; + + if (typeof value !== 'number') { + value = Number(value); + } + + if (typeof common.min === 'number' && value < common.min) { + return false; + } + + if (typeof common.max === 'number' && value > common.max) { + return false; + } + + return true; + } + /** * Render time picker component for date type * @returns {React.JSX.Element} @@ -545,6 +574,7 @@ class ObjectBrowserValue extends Component { variant="standard" classes={{ root: this.props.classes.textInput }} autoFocus + error={!this.state.valid} type="number" inputProps={{ step: this.props.object.common.step, min: this.props.object.common.min, max: this.props.object.common.max }} inputRef={this.inputRef} @@ -553,8 +583,10 @@ class ObjectBrowserValue extends Component { )} value={this.state.targetValue || 0} label={this.props.t('Value')} - onKeyUp={e => e.keyCode === 13 && this.onUpdate(e)} - onChange={e => this.setState({ targetValue: e.target.value })} + onKeyUp={e => e.keyCode === 13 && this.state.valid && this.onUpdate(e)} + onChange={e => { + this.setState({ targetValue: e.target.value, valid: this.isNumberValid({ value: e.target.value, common: this.props.object.common }) }); + }} /> : (this.state.type === 'json' ? this.renderJsonEditor() : (this.state.type === 'states' ? @@ -643,6 +675,7 @@ class ObjectBrowserValue extends Component { {!this.props.expertMode ?
: null}