@iobroker/adapter-react-v5
Version:
React components to develop ioBroker interfaces with react.
1,449 lines • 236 kB
JavaScript
/**
* 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