UNPKG

@iobroker/adapter-react-v5

Version:

React components to develop ioBroker interfaces with react.

1,449 lines 236 kB
/** * Copyright 2020-2025, Denis Haev <dogafox@gmail.com> * * MIT License * */ import React, { Component, createRef } from 'react'; import { Badge, Box, Button, Checkbox, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Fab, FormControlLabel, Grid2, IconButton, List, ListItem, ListItemButton, ListItemIcon, ListItemText, Menu, MenuItem, Paper, Snackbar, Switch, TextField, Tooltip, } from '@mui/material'; // Icons import { Add as AddIcon, ArrowRight as ArrowRightIcon, BedroomParent, BorderColor, Build as BuildIcon, CalendarToday as IconSchedule, Check as IconCheck, Close as IconClose, Code as IconScript, Construction, CreateNewFolder as IconFolder, Delete as IconDelete, Description as IconMeta, Edit as IconEdit, Error as IconError, FindInPage, FormatItalic as IconValueEdit, Link as IconLink, ListAlt as IconEnum, LooksOne as LooksOneIcon, PersonOutlined as IconUser, Publish as PublishIcon, Refresh as RefreshIcon, Router as IconHost, Settings as IconConfig, ShowChart as IconChart, SupervisedUserCircle as IconGroup, TextFields as TextFieldsIcon, ViewColumn as IconColumns, Wifi as IconConnection, WifiOff as IconDisconnected, DriveFileRenameOutline, ContentPaste, UploadFile, } from '@mui/icons-material'; import { IconExpert } from '../icons/IconExpert'; import { IconAdapter } from '../icons/IconAdapter'; import { IconChannel } from '../icons/IconChannel'; import { IconCopy } from '../icons/IconCopy'; import { IconDevice } from '../icons/IconDevice'; import { IconDocument } from '../icons/IconDocument'; import { IconDocumentReadOnly } from '../icons/IconDocumentReadOnly'; import { IconInstance } from '../icons/IconInstance'; import { IconState } from '../icons/IconState'; import { IconClosed } from '../icons/IconClosed'; import { IconOpen } from '../icons/IconOpen'; import { IconClearFilter } from '../icons/IconClearFilter'; import { Connection } from '../Connection'; import { Icon } from './Icon'; import { withWidth } from './withWidth'; import { Utils } from './Utils'; // @iobroker/adapter-react-v5/Components/Utils import { TabContainer } from './TabContainer'; import { TabContent } from './TabContent'; import { TabHeader } from './TabHeader'; import { CustomFilterInput, CustomFilterSelect, ButtonIcon, applyFilter, binarySearch, buildTree, findEnumsForObjectAsIds, findFunctionsForObject, findNode, findRoomsForObject, formatValue, generateFile, getCustomValue, getIdFieldTooltip, getName, getObjectTooltip, getSelectIdIconFromObjects, getValueStyle, getVisibleItems, isNonExpertId, setCustomValue, prepareSparkData, COLOR_NAME_USERDATA, COLOR_NAME_ALIAS, COLOR_NAME_JAVASCRIPT, COLOR_NAME_SYSTEM, COLOR_NAME_SYSTEM_ADAPTER, ICON_SIZE, ROW_HEIGHT, styles as utilStyles, } from './objectBrowserUtils'; export { getSelectIdIconFromObjects }; const ITEM_LEVEL = 16; const SMALL_BUTTON_SIZE = 20; const COLOR_NAME_ERROR_DARK = '#ff413c'; const COLOR_NAME_ERROR_LIGHT = '#86211f'; const COLOR_NAME_CONNECTED_DARK = '#57ff45'; const COLOR_NAME_CONNECTED_LIGHT = '#098c04'; const COLOR_NAME_DISCONNECTED_DARK = '#f3ad11'; const COLOR_NAME_DISCONNECTED_LIGHT = '#6c5008'; const styles = { toolbar: { minHeight: 38, // Theme.toolbar.height, // boxShadow: '0px 2px 4px -1px rgba(0, 0, 0, 0.2), 0px 4px 5px 0px rgba(0, 0, 0, 0.14), 0px 1px 10px 0px rgba(0, 0, 0, 0.12)' }, toolbarButtons: { padding: 4, marginLeft: 4, }, switchColumnAuto: { marginLeft: 16, }, dialogColumns: { transition: 'opacity 1s', }, dialogColumnsLabel: { fontSize: 12, paddingTop: 8, }, columnCustom: { width: '100%', display: 'inline-block', }, columnCustomEditable: { cursor: 'text', }, columnCustom_center: { textAlign: 'center', }, columnCustom_left: { textAlign: 'left', }, columnCustom_right: { textAlign: 'right', }, width100: { width: '100%', }, transparent_10: { opacity: 0.1, }, transparent_20: { opacity: 0.2, }, transparent_30: { opacity: 0.3, }, transparent_40: { opacity: 0.4, }, transparent_50: { opacity: 0.5, }, transparent_60: { opacity: 0.6, }, transparent_70: { opacity: 0.7, }, transparent_80: { opacity: 0.8, }, transparent_90: { opacity: 0.9, }, transparent_100: { opacity: 1, }, headerRow: { paddingLeft: 8, height: 38, whiteSpace: 'nowrap', userSelect: 'none', }, buttonClearFilter: { position: 'relative', float: 'right', padding: 0, }, buttonClearFilterIcon: { zIndex: 2, position: 'absolute', top: 0, left: 0, color: '#FF0000', opacity: 0.7, }, tableDiv: { paddingTop: 0, paddingLeft: 0, width: 'calc(100% - 8px)', height: 'calc(100% - 38px)', overflow: 'auto', }, tableRow: (theme) => ({ pl: 1, height: ROW_HEIGHT, lineHeight: `${ROW_HEIGHT}px`, verticalAlign: 'top', userSelect: 'none', position: 'relative', width: '100%', '&:hover': { background: `${theme.palette.mode === 'dark' ? theme.palette.primary.dark : theme.palette.primary.light} !important`, color: Utils.invertColor(theme.palette.primary.main, true), }, whiteSpace: 'nowrap', flexWrap: 'nowrap', }), tableRowLines: (theme) => ({ borderBottom: `1px solid ${theme.palette.mode === 'dark' ? '#8888882e' : '#8888882e'}`, '& > div': { borderRight: `1px solid ${theme.palette.mode === 'dark' ? '#8888882e' : '#8888882e'}`, }, }), tableRowNoDragging: { cursor: 'pointer', }, tableRowAlias: { height: ROW_HEIGHT + 10, }, tableRowAliasReadWrite: { height: ROW_HEIGHT + 22, }, tableRowFocused: (theme) => ({ '&:after': { content: '""', position: 'absolute', top: 1, left: 1, right: 1, bottom: 1, border: theme.palette.mode ? '1px dotted #000' : '1px dotted #FFF', }, }), checkBox: { padding: 0, }, cellId: { position: 'relative', fontSize: '1rem', overflow: 'hidden', textOverflow: 'ellipsis', // verticalAlign: 'top', // position: 'relative', '& .copyButton': { display: 'none', }, '&:hover .copyButton': { display: 'block', }, '& .iconOwn': { display: 'block', width: ROW_HEIGHT - 4, height: ROW_HEIGHT - 4, mt: '2px', float: 'right', }, '&:hover .iconOwn': { display: 'none', }, '& *': { width: 'initial', }, }, cellIdSpan: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', // display: 'inline-block', // verticalAlign: 'top', }, // This style is used for simple div. Do not migrate it to "secondary.main" cellIdIconFolder: (theme) => ({ marginRight: 8, width: ROW_HEIGHT - 4, height: ROW_HEIGHT - 4, cursor: 'pointer', color: theme.palette.secondary.main || '#fbff7d', verticalAlign: 'top', }), cellIdIconDocument: { verticalAlign: 'middle', marginLeft: (ROW_HEIGHT - SMALL_BUTTON_SIZE) / 2, marginRight: 8, width: SMALL_BUTTON_SIZE, height: SMALL_BUTTON_SIZE, }, cellIdIconOwn: {}, cellCopyButton: { width: SMALL_BUTTON_SIZE, height: SMALL_BUTTON_SIZE, top: (ROW_HEIGHT - SMALL_BUTTON_SIZE) / 2, opacity: 0.8, position: 'absolute', right: 3, }, cellCopyButtonInDetails: { width: SMALL_BUTTON_SIZE, height: SMALL_BUTTON_SIZE, top: (ROW_HEIGHT - SMALL_BUTTON_SIZE) / 2, opacity: 0.8, }, cellEditButton: { width: SMALL_BUTTON_SIZE, height: SMALL_BUTTON_SIZE, color: 'white', position: 'absolute', top: (ROW_HEIGHT - SMALL_BUTTON_SIZE) / 2, right: SMALL_BUTTON_SIZE + 3, opacity: 0.7, '&:hover': { opacity: 1, }, }, cellName: { display: 'inline-block', verticalAlign: 'top', fontSize: 14, ml: '5px', overflow: 'hidden', textOverflow: 'ellipsis', position: 'relative', '& .copyButton': { display: 'none', }, '&:hover .copyButton': { display: 'block', }, }, cellNameWithDesc: { lineHeight: 'normal', }, cellNameDivDiv: {}, cellDescription: { fontSize: 10, opacity: 0.5, fontStyle: 'italic', }, cellIdAlias: (theme) => ({ fontStyle: 'italic', fontSize: 12, opacity: 0.7, '&:hover': { color: theme.palette.mode === 'dark' ? '#009900' : '#007700', }, }), cellIdAliasReadWriteDiv: { height: 24, marginTop: -5, }, cellIdAliasAlone: { lineHeight: 0, }, cellIdAliasReadWrite: { lineHeight: '12px', }, cellType: { display: 'inline-block', verticalAlign: 'top', '& .itemIcon': { verticalAlign: 'middle', width: ICON_SIZE, height: ICON_SIZE, display: 'inline-block', }, '& .itemIconFolder': { marginLeft: 3, }, }, cellRole: { display: 'inline-block', verticalAlign: 'top', textOverflow: 'ellipsis', overflow: 'hidden', }, cellRoom: { display: 'inline-block', verticalAlign: 'top', textOverflow: 'ellipsis', overflow: 'hidden', }, cellEnumParent: { opacity: 0.4, }, cellFunc: { display: 'inline-block', verticalAlign: 'top', textOverflow: 'ellipsis', overflow: 'hidden', }, cellValue: { display: 'inline-block', verticalAlign: 'top', textOverflow: 'ellipsis', overflow: 'hidden', }, cellValueButton: { marginTop: 5, }, cellValueButtonFalse: { opacity: 0.3, }, cellAdapter: { display: 'inline-block', verticalAlign: 'top', }, cellValueTooltip: { fontSize: 12, }, cellValueText: { width: '100%', height: ROW_HEIGHT, fontSize: 16, display: 'flex', overflow: 'hidden', textOverflow: 'ellipsis', position: 'relative', verticalAlign: 'top', '& .copyButton': { display: 'none', }, '&:hover .copyButton': { display: 'block', }, }, cellValueFile: { color: '#2837b9', }, cellValueTooltipTitle: { fontStyle: 'italic', width: 100, display: 'inline-block', }, cellValueTooltipValue: { width: 120, display: 'inline-block', // overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis', }, cellValueTooltipImage: { width: 100, height: 'auto', }, cellValueTooltipBoth: { width: 220, display: 'inline-block', whiteSpace: 'nowrap', }, cellValueTooltipBox: { width: 250, overflow: 'hidden', pointerEvents: 'none', }, tooltip: { pointerEvents: 'none', }, cellValueTextUnit: { marginLeft: 4, opacity: 0.8, display: 'inline-block', }, cellValueTextState: { opacity: 0.7, }, cellValueTooltipCopy: { position: 'absolute', bottom: 3, right: 3, }, cellValueTooltipEdit: { position: 'absolute', bottom: 3, right: 15, }, cellButtons: { display: 'inline-block', verticalAlign: 'top', }, cellButtonsButton: { display: 'inline-block', opacity: 0.5, width: SMALL_BUTTON_SIZE + 4, height: SMALL_BUTTON_SIZE + 4, '&:hover': { opacity: 1, }, p: 0, mt: '-2px', }, cellButtonsEmptyButton: { fontSize: 12, }, cellButtonMinWidth: { minWidth: 40, }, cellButtonsButtonAlone: { ml: `${SMALL_BUTTON_SIZE + 6}px`, pt: 0, mt: '-2px', }, cellButtonsButtonWithCustoms: (theme) => ({ color: theme.palette.mode === 'dark' ? theme.palette.primary.main : theme.palette.secondary.main, }), cellButtonsButtonWithoutCustoms: { opacity: 0.2, }, cellButtonsValueButton: (theme) => ({ position: 'absolute', top: SMALL_BUTTON_SIZE / 2 - 2, opacity: 0.7, width: SMALL_BUTTON_SIZE - 2, height: SMALL_BUTTON_SIZE - 2, color: theme.palette.action.active, '&:hover': { opacity: 1, }, }), cellButtonsValueButtonCopy: { right: 8, cursor: 'pointer', }, cellButtonsValueButtonEdit: { right: SMALL_BUTTON_SIZE / 2 + 16, }, cellDetailsLine: { display: 'flex', alignItems: 'center', width: '100%', height: 32, fontSize: 16, }, cellDetailsName: { fontWeight: 'bold', marginRight: 8, minWidth: 80, }, filteredOut: { opacity: 0.5, }, filteredParentOut: { opacity: 0.3, }, filterInput: { mt: 0, mb: 0, }, selectIcon: { width: 24, height: 24, marginRight: 4, }, itemSelected: (theme) => ({ background: `${theme.palette.primary.main} !important`, color: `${Utils.invertColor(theme.palette.primary.main, true)} !important`, }), header: { width: '100%', }, headerCell: { display: 'inline-block', verticalAlign: 'top', }, headerCellValue: { paddingTop: 4, // paddingLeft: 5, fontSize: 16, }, visibleButtons: { color: '#2196f3', opacity: 0.7, }, grow: { flexGrow: 1, }, enumIconDiv: { marginRight: 8, width: 32, height: 32, borderRadius: 8, background: '#FFFFFF', }, enumIcon: { marginTop: 4, marginLeft: 4, width: 24, height: 24, }, enumDialog: { overflow: 'hidden', }, enumList: { minWidth: 250, height: 'calc(100% - 50px)', overflow: 'auto', }, enumCheckbox: { minWidth: 0, }, buttonDiv: { display: 'flex', height: '100%', alignItems: 'center', }, aclText: { fontSize: 13, marginTop: 6, }, rightsObject: { color: '#55ff55', paddingLeft: 3, }, rightsState: { color: '#86b6ff', paddingLeft: 3, }, textCenter: { padding: 12, textAlign: 'center', }, tooltipAccessControl: { display: 'flex', flexDirection: 'column', }, fontSizeTitle: { '@media screen and (max-width: 465px)': { '& *': { fontSize: 12, }, }, }, draggable: { cursor: 'copy', }, nonDraggable: { cursor: 'no-drop', }, iconDeviceConnected: (theme) => ({ color: theme.palette.mode === 'dark' ? COLOR_NAME_CONNECTED_DARK : COLOR_NAME_CONNECTED_LIGHT, opacity: 0.8, position: 'absolute', top: 4, right: 32, width: 20, }), iconDeviceDisconnected: (theme) => ({ color: theme.palette.mode === 'dark' ? COLOR_NAME_DISCONNECTED_DARK : COLOR_NAME_DISCONNECTED_LIGHT, opacity: 0.8, position: 'absolute', top: 4, right: 32, width: 20, }), iconDeviceError: (theme) => ({ color: theme.palette.mode === 'dark' ? COLOR_NAME_ERROR_DARK : COLOR_NAME_ERROR_LIGHT, opacity: 0.8, position: 'absolute', top: 4, right: 50, width: 20, }), resizeHandle: { display: 'block', position: 'absolute', cursor: 'col-resize', width: 7, top: 2, bottom: 2, zIndex: 1, }, resizeHandleRight: { right: 3, borderRight: '2px dotted #888', '&:hover': { borderColor: '#ccc', borderRightStyle: 'solid', }, '&.active': { borderColor: '#517ea5', borderRightStyle: 'solid', }, }, invertedBackground: (theme) => ({ backgroundColor: theme.palette.mode === 'dark' ? '#9a9a9a' : '#565656', padding: '0 3px', borderRadius: '2px 0 0 2px', }), invertedBackgroundFlex: (theme) => ({ backgroundColor: theme.palette.mode === 'dark' ? '#9a9a9a' : '#565656', borderRadius: '0 2px 2px 0', }), contextMenuEdit: (theme) => ({ color: theme.palette.mode === 'dark' ? '#ffee48' : '#cbb801', }), contextMenuEditValue: (theme) => ({ color: theme.palette.mode === 'dark' ? '#5dff45' : '#1cd301', }), contextMenuView: (theme) => ({ color: theme.palette.mode === 'dark' ? '#FFF' : '#000', }), contextMenuCustom: (theme) => ({ color: theme.palette.mode === 'dark' ? '#42eaff' : '#01bbc2', }), contextMenuACL: (theme) => ({ color: theme.palette.mode === 'dark' ? '#e079ff' : '#500070', }), contextMenuRoom: (theme) => ({ color: theme.palette.mode === 'dark' ? '#ff9a33' : '#642a00', }), contextMenuRole: (theme) => ({ color: theme.palette.mode === 'dark' ? '#ffdb43' : '#562d00', }), contextMenuDelete: (theme) => ({ color: theme.palette.mode === 'dark' ? '#ff4f4f' : '#cf0000', }), contextMenuKeys: { marginLeft: 8, opacity: 0.7, fontSize: 'smaller', }, contextMenuWithSubMenu: { display: 'flex', }, ...utilStyles, }; export const ITEM_IMAGES = { state: (React.createElement(IconState, { className: "itemIcon", style: { verticalAlign: 'middle' } })), channel: (React.createElement(IconChannel, { className: "itemIcon", style: { verticalAlign: 'middle' } })), device: (React.createElement(IconDevice, { className: "itemIcon", style: { verticalAlign: 'middle' } })), adapter: (React.createElement(IconAdapter, { className: "itemIcon", style: { verticalAlign: 'middle' } })), meta: (React.createElement(IconMeta, { className: "itemIcon", style: { verticalAlign: 'middle' } })), instance: (React.createElement(IconInstance, { className: "itemIcon", style: { color: '#7da7ff', verticalAlign: 'middle' } })), enum: (React.createElement(IconEnum, { className: "itemIcon", style: { verticalAlign: 'middle' } })), chart: (React.createElement(IconChart, { className: "itemIcon", style: { verticalAlign: 'middle' } })), config: (React.createElement(IconConfig, { className: "itemIcon", style: { verticalAlign: 'middle' } })), group: (React.createElement(IconGroup, { className: "itemIcon", style: { verticalAlign: 'middle' } })), user: (React.createElement(IconUser, { className: "itemIcon", style: { verticalAlign: 'middle' } })), host: (React.createElement(IconHost, { className: "itemIcon", style: { verticalAlign: 'middle' } })), schedule: (React.createElement(IconSchedule, { className: "itemIcon", style: { verticalAlign: 'middle' } })), script: (React.createElement(IconScript, { className: "itemIcon", style: { verticalAlign: 'middle' } })), folder: (React.createElement(IconClosed, { className: "itemIcon itemIconFolder", style: { verticalAlign: 'middle' } })), }; const SCREEN_WIDTHS = { // extra-small: 0px xs: { idWidth: '100%', fields: [], widths: {} }, // small: 600px sm: { idWidth: 300, fields: ['room', 'val'], widths: { room: 100, val: 200 } }, // medium: 960px md: { idWidth: 300, fields: ['room', 'func', 'val', 'buttons'], widths: { name: 200, room: 150, func: 150, val: 120, buttons: 120, }, }, // large: 1280px lg: { idWidth: 300, fields: [ 'name', 'type', 'role', 'room', 'func', 'val', 'buttons', 'changedFrom', 'qualityCode', 'timestamp', 'lastChange', ], widths: { name: 300, type: 80, role: 120, room: 180, func: 180, val: 140, buttons: 120, changedFrom: 120, qualityCode: 100, timestamp: 165, lastChange: 165, }, }, // ///////////// // extra-large: 1920px xl: { idWidth: 550, fields: [ 'name', 'type', 'role', 'room', 'func', 'val', 'buttons', 'changedFrom', 'qualityCode', 'timestamp', 'lastChange', ], widths: { name: 400, type: 80, role: 120, room: 180, func: 180, val: 140, buttons: 120, changedFrom: 120, qualityCode: 100, timestamp: 170, lastChange: 170, }, }, }; let objectsAlreadyLoaded = false; const DEFAULT_FILTER = { id: '', name: '', room: [], func: [], role: [], type: [], custom: [], expertMode: false, }; export class ObjectBrowserClass extends Component { // do not define the type as null to save the performance, so we must check it every time info = { funcEnums: [], roomEnums: [], roles: [], ids: [], types: [], objects: {}, customs: [], enums: [], hasSomeCustoms: false, aliasesMap: {}, }; localStorage = window._localStorage || window.localStorage; tableRef; pausedSubscribes = false; selectFirst; root = null; states = {}; subscribes = []; unsubscribeTimer = null; statesUpdateTimer = null; objectsUpdateTimer = null; visibleCols; texts; possibleCols; imagePrefix; adapterColumns = []; styleTheme = ''; edit = { id: '', val: '', q: 0, ack: false, }; levelPadding; customWidth = false; resizeTimeout = null; resizerNextName = null; resizerActiveName = null; resizerCurrentWidths = {}; resizeLeft = false; resizerOldWidth = 0; resizerMin = 0; resizerNextMin = 0; resizerOldWidthNext = 0; resizerPosition = 0; resizerActiveDiv = null; resizerNextDiv = null; storedWidths = null; systemConfig; objects; defaultHistory = ''; ctrlPressed = false; columnsVisibility = {}; changedIds = null; contextMenu = null; recordStates = []; styles = {}; expertMode = false; customColumnDialog = null; constructor(props) { super(props); const lastSelectedItemStr = this.localStorage.getItem(`${props.dialogName || 'App'}.objectSelected`) || ''; this.selectFirst = ''; this.expertMode = !!this.props.expertMode; if (lastSelectedItemStr.startsWith('[')) { try { const lastSelectedItems = JSON.parse(lastSelectedItemStr); this.selectFirst = lastSelectedItems[0] || ''; } catch { // ignore } } else { this.selectFirst = lastSelectedItemStr; } let expanded; const expandedStr = this.localStorage.getItem(`${props.dialogName || 'App'}.objectExpanded`) || '[]'; try { expanded = JSON.parse(expandedStr); } catch { expanded = []; } let filter; const filterStr = props.defaultFilters ? '' : this.localStorage.getItem(`${props.dialogName || 'App'}.objectFilter`) || ''; if (filterStr) { try { filter = JSON.parse(filterStr); } catch { filter = { ...DEFAULT_FILTER }; } } else if (props.defaultFilters && typeof props.defaultFilters === 'object') { filter = { ...props.defaultFilters }; } else { filter = { ...DEFAULT_FILTER }; } // Migrate old filters to new one if (typeof filter.room === 'string' && filter.room) { filter.room = [filter.room].filter(s => s); if (!filter.room.length) { delete filter.room; } } if (typeof filter.func === 'string' && filter.func) { filter.func = [filter.func].filter(s => s); if (!filter.func.length) { delete filter.func; } } if (typeof filter.role === 'string' && filter.role) { filter.role = [filter.role].filter(s => s); if (!filter.role.length) { delete filter.role; } } if (typeof filter.type === 'string') { filter.type = [filter.type].filter(s => s); if (!filter.type.length) { delete filter.type; } } if (typeof filter.custom === 'string') { filter.custom = [filter.custom].filter(s => s); if (!filter.custom.length) { delete filter.custom; } } filter.expertMode = props.expertMode !== undefined ? props.expertMode : (window._sessionStorage || window.sessionStorage).getItem('App.expertMode') === 'true'; this.tableRef = createRef(); this.visibleCols = props.columns || SCREEN_WIDTHS[props.width || 'lg'].fields; // remove type column if only one type must be selected if (props.types && props.types.length === 1) { const pos = this.visibleCols.indexOf('type'); if (pos !== -1) { this.visibleCols.splice(pos, 1); } } this.possibleCols = SCREEN_WIDTHS.xl.fields; let customDialog = null; if (props.router) { const location = props.router.getLocation(); if (location.id && location.dialog === 'customs') { customDialog = [location.id]; this.pauseSubscribe(true); } } let selected; if (!Array.isArray(props.selected)) { selected = [props.selected || '']; } else { selected = props.selected; } selected = selected.map(id => id.replace(/["']/g, '')).filter(id => id); this.selectFirst = selected.length && selected[0] ? selected[0] : this.selectFirst; const columnsStr = this.localStorage.getItem(`${props.dialogName || 'App'}.columns`); let columns; try { columns = columnsStr ? JSON.parse(columnsStr) : null; } catch { columns = null; } let columnsWidths = null; // this.localStorage.getItem(`${props.dialogName || 'App'}.columnsWidths`); try { columnsWidths = columnsWidths ? JSON.parse(columnsWidths) : {}; } catch { columnsWidths = {}; } this.imagePrefix = props.imagePrefix || '.'; let foldersFirst; const foldersFirstStr = this.localStorage.getItem(`${props.dialogName || 'App'}.foldersFirst`); if (foldersFirstStr === 'false') { foldersFirst = false; } else if (foldersFirstStr === 'true') { foldersFirst = true; } else { foldersFirst = props.foldersFirst === undefined ? true : props.foldersFirst; } let statesView = false; try { statesView = this.props.objectStatesView ? JSON.parse(this.localStorage.getItem(`${props.dialogName || 'App'}.objectStatesView`) || '') || false : false; } catch { // ignore } this.state = { aliasMenu: '', beautifyJsonExport: true, columns, columnsAuto: this.localStorage.getItem(`${props.dialogName || 'App'}.columnsAuto`) !== 'false', columnsDialogTransparent: 100, columnsEditCustomDialog: null, columnsForAdmin: null, columnsSelectorShow: false, columnsWidths, customColumnDialogValueChanged: false, customDialog, depth: 0, editObjectAlias: false, // open the edit object dialog on alias tab editObjectDialog: '', enumDialog: null, excludeSystemRepositoriesFromExport: true, excludeTranslations: false, expandAllVisible: false, expanded, filter, filterKey: 0, focused: this.localStorage.getItem(`${props.dialogName || 'App'}.focused`) || '', foldersFirst, linesEnabled: this.localStorage.getItem(`${props.dialogName || 'App'}.lines`) === 'true', loaded: false, noStatesByExportImport: false, roleDialog: null, scrollBarWidth: 16, selected, selectedNonObject: this.localStorage.getItem(`${props.dialogName || 'App'}.selectedNonObject`) || '', showAliasEditor: '', showAllExportOptions: false, showContextMenu: null, showDescription: this.localStorage.getItem(`${props.dialogName || 'App'}.desc`) !== 'false', showExportDialog: false, showImportDialog: false, showImportMenu: null, showRenameDialog: null, statesView, toast: '', tooltipInfo: null, viewFileDialog: '', }; this.texts = { name: props.t('ra_Name'), categories: props.t('ra_Categories'), value: props.t('ra_tooltip_value'), ack: props.t('ra_tooltip_ack'), ts: props.t('ra_tooltip_ts'), lc: props.t('ra_tooltip_lc'), from: props.t('ra_tooltip_from'), user: props.t('ra_tooltip_user'), c: props.t('ra_tooltip_comment'), quality: props.t('ra_tooltip_quality'), editObject: props.t('ra_tooltip_editObject'), deleteObject: props.t('ra_tooltip_deleteObject'), customConfig: props.t('ra_tooltip_customConfig'), copyState: props.t('ra_tooltip_copyState'), editState: props.t('ra_tooltip_editState'), ctrlForLink: props.t('ra_tooltip_ctrlForLink'), close: props.t('ra_Close'), filter_id: props.t('ra_filter_id'), filter_name: props.t('ra_filter_name'), filter_type: props.t('ra_filter_type'), filter_role: props.t('ra_filter_role'), filter_room: props.t('ra_filter_room'), filter_func: props.t('ra_filter_func'), filter_custom: props.t('ra_filter_customs'), // filterCustomsWithout: props.t('ra_filter_customs_without'), // objectChangedByUser: props.t('ra_object_changed_by_user'), // Object last changed at objectChangedBy: props.t('ra_object_changed_by'), // Object changed by objectChangedFrom: props.t('ra_state_changed_from'), // Object changed from stateChangedBy: props.t('ra_state_changed_by'), // State changed by stateChangedFrom: props.t('ra_state_changed_from'), // State changed from ownerGroup: props.t('ra_Owner group'), ownerUser: props.t('ra_Owner user'), showAll: props.t('ra_show_all'), deviceError: props.t('ra_Error'), deviceDisconnected: props.t('ra_Disconnected'), deviceConnected: props.t('ra_Connected'), aclOwner_read_object: props.t('ra_aclOwner_read_object'), aclOwner_read_state: props.t('ra_aclOwner_read_state'), aclOwner_write_object: props.t('ra_aclOwner_write_object'), aclOwner_write_state: props.t('ra_aclOwner_write_state'), aclGroup_read_object: props.t('ra_aclGroup_read_object'), aclGroup_read_state: props.t('ra_aclGroup_read_state'), aclGroup_write_object: props.t('ra_aclGroup_write_object'), aclGroup_write_state: props.t('ra_aclGroup_write_state'), aclEveryone_read_object: props.t('ra_aclEveryone_read_object'), aclEveryone_read_state: props.t('ra_aclEveryone_read_state'), aclEveryone_write_object: props.t('ra_aclEveryone_write_object'), aclEveryone_write_state: props.t('ra_aclEveryone_write_state'), create: props.t('ra_Create'), createBooleanState: props.t('ra_create_boolean_state'), createNumberState: props.t('ra_create_number_state'), createStringState: props.t('ra_create_string_state'), createState: props.t('ra_create_state'), createChannel: props.t('ra_create_channel'), createDevice: props.t('ra_create_device'), createFolder: props.t('ra_Create folder'), }; this.levelPadding = props.levelPadding || ITEM_LEVEL; const resizerCurrentWidthsStr = this.localStorage.getItem(`${this.props.dialogName || 'App'}.table`); if (resizerCurrentWidthsStr) { try { const resizerCurrentWidths = JSON.parse(resizerCurrentWidthsStr); const width = this.props.width || 'lg'; this.storedWidths = JSON.parse(JSON.stringify(SCREEN_WIDTHS[width])); Object.keys(resizerCurrentWidths).forEach(id => { if (id === 'id') { SCREEN_WIDTHS[width].idWidth = resizerCurrentWidths.id; } else if (id === 'nameHeader') { SCREEN_WIDTHS[width].widths.name = resizerCurrentWidths[id]; } else if (SCREEN_WIDTHS[width].widths[id] !== undefined) { SCREEN_WIDTHS[width].widths[id] = resizerCurrentWidths[id]; } }); this.customWidth = true; } catch { // ignore } } this.calculateColumnsVisibility(); } async loadAllObjects(update) { const props = this.props; try { await new Promise(resolve => { this.setState({ updating: true }, () => resolve()); }); const objects = (props.objectsWorker ? await props.objectsWorker.getObjects(update) : await props.socket.getObjects(update, true)) || {}; if (props.types && Connection.isWeb()) { for (let i = 0; i < props.types.length; i++) { // admin has ALL objects // web has only state, channel, device, enum, and system.config if (props.types[i] === 'state' || props.types[i] === 'channel' || props.types[i] === 'device' || props.types[i] === 'enum') { continue; } const moreObjects = await props.socket.getObjectViewSystem(props.types[i]); Object.assign(objects || {}, moreObjects); } } this.systemConfig = this.systemConfig || objects?.['system.config'] || (await props.socket.getObject('system.config')); this.systemConfig.common = this.systemConfig.common || {}; this.systemConfig.common.defaultNewAcl = this.systemConfig.common.defaultNewAcl || { object: 0, state: 0, file: 0, owner: 'system.user.admin', ownerGroup: 'system.group.administrator', }; this.systemConfig.common.defaultNewAcl.owner = this.systemConfig.common.defaultNewAcl.owner || 'system.user.admin'; this.systemConfig.common.defaultNewAcl.ownerGroup = this.systemConfig.common.defaultNewAcl.ownerGroup || 'system.group.administrator'; if (typeof this.systemConfig.common.defaultNewAcl.state !== 'number') { // TODO: may be convert here from string this.systemConfig.common.defaultNewAcl.state = 0x664; } if (typeof this.systemConfig.common.defaultNewAcl.object !== 'number') { // TODO: may be convert here from string this.systemConfig.common.defaultNewAcl.state = 0x664; } if (typeof props.filterFunc === 'function') { this.objects = {}; const filterFunc = props.filterFunc; Object.keys(objects).forEach(id => { try { if (filterFunc(objects[id])) { this.objects[id] = objects[id]; } else { const type = objects[id] && objects[id].type; // include "folder" types too for icons and names of nodes if (type && (type === 'channel' || type === 'device' || type === 'folder' || type === 'adapter' || type === 'instance')) { this.objects[id] = objects[id]; } } } catch (e) { console.log(`Error by filtering of "${id}": ${e}`); } }); } else if (props.types) { this.objects = {}; const propsTypes = props.types; Object.keys(objects).forEach(id => { const type = objects[id]?.type; // include "folder" types too if (type && (type === 'channel' || type === 'device' || type === 'enum' || type === 'folder' || type === 'adapter' || type === 'instance' || propsTypes.includes(type))) { this.objects[id] = objects[id]; } }); } else { this.objects = objects; } if (props.setObjectsReference) { props.setObjectsReference(this.objects); } // read default history this.defaultHistory = this.systemConfig.common.defaultHistory; if (this.defaultHistory) { props.socket .getState(`system.adapter.${this.defaultHistory}.alive`) .then(state => { if (!state?.val) { this.defaultHistory = ''; } }) .catch(e => window.alert(`Cannot get state: ${e}`)); } const columnsForAdmin = await this.getAdditionalColumns(); this.calculateColumnsVisibility(null, null, columnsForAdmin); const { info, root } = buildTree(this.objects, { imagePrefix: props.imagePrefix, root: props.root, lang: props.lang, themeType: props.themeType, }); this.root = root; this.info = info; // Show first selected item const node = this.state.selected?.length && findNode(this.root, this.state.selected[0]); // If the selected ID is not visible, reset filter if (node && !applyFilter(node, this.state.filter, props.lang, this.objects, undefined, undefined, props.customFilter, props.types)) { // reset filter this.setState({ filter: { ...DEFAULT_FILTER }, columnsForAdmin }, () => { this.doFilter(); this.setState({ loaded: true, updating: false }, () => this.expandAllSelected(() => this.onAfterSelect())); }); } else { this.doFilter(); this.setState({ loaded: true, updating: false, columnsForAdmin }, () => this.expandAllSelected(() => this.onAfterSelect())); } } catch (error) { this.showError(error); } } expandAllSelected(cb) { const expanded = [...this.state.expanded]; let changed = false; this.state.selected.forEach(id => { const parts = id.split('.'); const path = []; for (let i = 0; i < parts.length - 1; i++) { path.push(parts[i]); if (!expanded.includes(path.join('.'))) { expanded.push(path.join('.')); changed = true; } } }); if (changed) { expanded.sort(); this.localStorage.setItem(`${this.props.dialogName || 'App'}.objectExpanded`, JSON.stringify(expanded)); this.setState({ expanded }, cb); } else if (cb) { cb(); } } /** * @param isDouble is double click */ onAfterSelect(isDouble) { if (this.state.selected?.length && this.state.selected[0]) { this.localStorage.setItem(`${this.props.dialogName || 'App'}.objectSelected`, this.state.selected[0]); // remove a task to select the pre-selected item if now we want to see another object if (this.selectFirst && this.selectFirst !== this.state.selected[0]) { this.selectFirst = ''; } // If the this.state.selected[0] filtered out, disable the filter const item = this.findItem(this.state.selected[0]); if (item?.data && !item.data.visible && !item.data.hasVisibleChildren) { // If the selected ID is not visible, reset filter this.clearFilter(); } if (this.state.selected.length === 1 && this.objects[this.state.selected[0]]) { const name = Utils.getObjectName(this.objects, this.state.selected[0], null, { language: this.props.lang, }); this.props.onSelect?.(this.state.selected, name, isDouble); } else if (this.state.selected.length === 1 && this.props.allowNonObjects) { this.props.onSelect?.(this.state.selected, null, isDouble); } else { // we have more than one state // Check if all IDs are objects if (!this.props.allowNonObjects || !this.state.selected.find(id => !this.objects[id])) { this.props.onSelect?.(this.state.selected, null, isDouble); } } } else { this.localStorage.removeItem(`${this.props.dialogName || 'App'}.objectSelected`); if (this.state.selected.length) { this.setState({ selected: [] }, () => { if (this.props.onSelect) { if (this.state.focused && this.props.allowNonObjects) { // remove a task to select the pre-selected item if now we want to see another object if (this.selectFirst && this.selectFirst !== this.state.selected[0]) { this.selectFirst = ''; } this.props.onSelect([this.state.focused], null, isDouble); } else { this.props.onSelect([], ''); } } }); } else if (this.props.onSelect) { if (this.state.focused && this.props.allowNonObjects) { // remove a task to select the pre-selected item if now we want to see another object if (this.selectFirst && this.selectFirst !== this.state.selected[0]) { this.selectFirst = ''; } this.props.onSelect([this.state.focused], null, isDouble); } else { this.props.onSelect([], ''); } } } } // This function is used static getDerivedStateFromProps(props, state) { const newState = {}; let changed = false; if (props.expertMode !== undefined && props.expertMode !== state.filter.expertMode) { changed = true; newState.filter = { ...state.filter }; newState.filter.expertMode = props.expertMode; } return changed ? newState : null; } /** * Called when component is mounted. */ async componentDidMount() { await this.loadAllObjects(!objectsAlreadyLoaded); if (this.props.objectsWorker) { this.props.objectsWorker.registerHandler(this.onObjectChangeFromWorker); } else { await this.props.socket.subscribeObject('*', this.onObjectChange); } objectsAlreadyLoaded = true; window.addEventListener('contextmenu', this.onContextMenu, true); window.addEventListener('keydown', this.onKeyPress, true); window.addEventListener('keyup', this.onKeyPress, true); // Inform dialog that all objects are loaded if (this.props.onAllLoaded) { setTimeout(() => { this.props.onAllLoaded?.(); }, 100); } } onKeyPress = (event) => { if (event.type === 'keydown' && event.ctrlKey && !this.ctrlPressed) { this.ctrlPressed = true; if (this.tableRef.current) { this.tableRef.current.className = 'highlight-link'; } } else if (event.type === 'keyup' && !event.ctrlKey && this.ctrlPressed) { this.ctrlPressed = false; if (this.tableRef.current) { this.tableRef.current.className = ''; } } }; /** * Called when component is unmounted. */ componentWillUnmount() { window.removeEventListener('contextmenu', this.onContextMenu, true); window.removeEventListener('keydown', this.onKeyPress, true); window.removeEventListener('keyup', this.onKeyPress, true); if (this.props.objectsWorker) { this.props.objectsWorker.unregisterHandler(this.onObjectChangeFromWorker, true); } else { void this.props.socket .unsubscribeObject('*', this.onObjectChange) .catch(e => console.error(`Cannot unsubscribe *: ${e}`)); } // remove all subscribes this.subscribes.forEach(pattern => { // console.log(`- unsubscribe ${pattern}`); this.props.socket.unsubscribeState(pattern, this.onStateChange); }); this.subscribes = []; this.objects = {}; } /** * Show the deletion dialog for a given object */ showDeleteDialog(options) { const { id, obj, item } = options; // calculate the number of children const keys = Object.keys(this.objects); keys.sort(); let count = 0; const start = `${id}.`; for (let i = 0; i < keys.length; i++) { if (keys[i].startsWith(start)) { count++; } else if (keys[i] > start) { break; } } this.props.onObjectDelete?.(id, !!item.children?.length, !obj.common?.dontDelete, count + 1); } /** * Context menu handler. */ onContextMenu = (e) => { // console.log(`CONTEXT MENU: ${this.contextMenu ? Date.now() - this.contextMenu.ts : 'false'}`); if (this.contextMenu && Date.now() - this.contextMenu.ts < 2000) { e.preventDefault(); this.setState({ showContextMenu: { item: this.contextMenu.item, position: { left: e.clientX + 2, top: e.clientY - 6 }, }, }); } else if (this.state.showContextMenu) { e.preventDefault(); this.setState({ showContextMenu: null }); } this.contextMenu = null; }; /** * Called when component is mounted. */ refreshComponent() { // remove all subscribes this.subscribes.forEach(pattern => { // console.log(`- unsubscribe ${pattern}`); th