@iobroker/adapter-react-v5
Version:
React components to develop ioBroker interfaces with react.
1,257 lines (1,256 loc) • 54.5 kB
JavaScript
import React, { Component } from 'react';
import { Box, FormControl, IconButton, Input, MenuItem, Select } from '@mui/material';
import SVG from 'react-inlinesvg';
import { Description as IconMeta, PersonOutlined as IconUser, Router as IconHost, SupervisedUserCircle as IconGroup, SettingsApplications as IconSystem, DataObject as IconData, Info as IconInfo, Link as IconLink, Close as IconClose, Wifi as IconConnection, } from '@mui/icons-material';
import { Utils } from './Utils';
export const ICON_SIZE = 24;
export const ROW_HEIGHT = 32;
export const COLOR_NAME_USERDATA = (themeType) => (themeType === 'dark' ? '#62ff25' : '#37c400');
export const COLOR_NAME_ALIAS = (themeType) => (themeType === 'dark' ? '#ee56ff' : '#a204b4');
export const COLOR_NAME_JAVASCRIPT = (themeType) => (themeType === 'dark' ? '#fff46e' : '#b89101');
export const COLOR_NAME_SYSTEM = (themeType) => (themeType === 'dark' ? '#ff6d69' : '#ff6d69');
export const COLOR_NAME_SYSTEM_ADAPTER = (themeType) => themeType === 'dark' ? '#5773ff' : '#5773ff';
/** Namespaces which are allowed to be edited by non-expert users */
const NON_EXPERT_NAMESPACES = ['0_userdata.0.', 'alias.0.'];
export const styles = {
headerCellInput: {
width: 'calc(100% - 5px)',
height: ROW_HEIGHT,
pt: 0,
'& .itemIcon': {
verticalAlign: 'middle',
width: ICON_SIZE,
height: ICON_SIZE,
display: 'inline-block',
},
},
headerCellSelectItem: {
'& .itemIcon': {
width: ICON_SIZE,
height: ICON_SIZE,
mr: '5px',
display: 'inline-block',
},
},
selectNone: {
opacity: 0.5,
},
selectClearButton: {
position: 'absolute',
top: 0,
right: 0,
borderRadius: 5,
backgroundColor: 'background.default',
},
cellIdTooltipLink: {
color: '#7ec2fd',
'&:hover': {
color: '#7ec2fd',
},
'&:visited': {
color: '#7ec2fd',
},
},
cellIdTooltip: {
fontSize: 14,
},
};
export function ButtonIcon(props) {
return (React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 436 436", style: props?.style, width: "24", height: "24", className: "admin-button" },
React.createElement("g", { fill: "currentColor" },
React.createElement("path", { d: "m195.23077,24.30769c-36,3 -67,12 -96,26c-49,24 -82,61 -93,104l-3,11l-1,50c0,46 0,49 2,59l5,20c21,58 84,103 165,116c16,3 53,4 70,2c60,-6 111,-28 147,-64c21,-21 36,-49 40,-74a866,866 0 0 0 1,-104c-3,-18 -6,-28 -13,-43c-26,-52 -87,-90 -162,-101c-16,-2 -48,-3 -63,-2l1,0zm60,23c36,5 70,18 95,35c31,20 51,47 59,77c2,7 2,11 2,25c1,15 0,18 -2,26c-19,69 -104,117 -200,114c-47,-2 -90,-15 -124,-38c-31,-20 -51,-47 -59,-77c-3,-11 -4,-32 -2,-43c8,-42 41,-78 91,-101a260,260 0 0 1 140,-19l0,1zm-221,222c21,26 57,49 95,62c81,27 174,14 239,-32c14,-10 31,-27 41,-41c2,-2 2,-2 2,7c-1,23 -16,50 -38,72c-78,74 -233,74 -311,-1a121,121 0 0 1 -39,-76l0,-6l3,4l8,11z" }),
React.createElement("path", { d: "m201.23077,47.30769c-40,3 -79,19 -104,44c-55,55 -38,133 37,171c52,26 122,24 172,-5c30,-17 51,-42 58,-71c3,-11 3,-34 0,-45c-6,-23 -21,-44 -40,-60l-27,-16a184,184 0 0 0 -96,-18zm30,21c56,5 100,35 112,75c4,11 4,30 0,41c-8,25 -26,45 -54,59a166,166 0 0 1 -160,-8a98,98 0 0 1 -41,-53c-5,-18 -2,-39 8,-57c23,-39 79,-62 135,-57z" }))));
}
/** Converts ioB pattern into regex */
export function pattern2RegEx(pattern) {
pattern = (pattern || '').toString();
const startsWithWildcard = pattern[0] === '*';
const endsWithWildcard = pattern[pattern.length - 1] === '*';
pattern = pattern.replace(/[-/\\^$+?.()|[\]{}]/g, '\\$&').replace(/\*/g, '.*');
return (startsWithWildcard ? '' : '^') + pattern + (endsWithWildcard ? '' : '$');
}
/**
* Function that walks through all keys of an object or array and applies a function to each key.
*/
export function walkThroughArray(object, iteratee) {
const copiedObject = [];
for (let index = 0; index < object.length; index++) {
iteratee(copiedObject, object[index], index);
}
return copiedObject;
}
/**
* Function that walks through all keys of an object or array and applies a function to each key.
*/
export function walkThroughObject(object, iteratee) {
const copiedObject = {};
for (const key in object) {
if (Object.prototype.hasOwnProperty.call(object, key)) {
iteratee(copiedObject, object[key], key);
}
}
return copiedObject;
}
/**
* Function to reduce an object primarily by a given list of keys
*/
export function filterObject(
/** The objects which should be filtered */
obj,
/** The keys which should be excluded */
filterKeys,
/** Whether translations should be reduced to only the english value */
excludeTranslations) {
if (Array.isArray(obj)) {
return walkThroughArray(obj, (result, value, key) => {
if (value === undefined || value === null) {
return;
}
// if the key is an object, run it through the inner function - omitFromObject
const isObject = typeof value === 'object';
if (excludeTranslations && isObject) {
if (typeof value.en === 'string' && typeof value.de === 'string') {
result[key] = value.en;
return;
}
}
result[key] = isObject ? filterObject(value, filterKeys, excludeTranslations) : value;
});
}
return walkThroughObject(obj, (result, value, key) => {
if (value === undefined || value === null) {
return;
}
if (filterKeys.includes(key)) {
return;
}
// if the key is an object, run it through the inner function - omitFromObject
const isObject = typeof value === 'object';
if (excludeTranslations && isObject) {
if (typeof value.en === 'string' && typeof value.de === 'string') {
result[key] = value.en;
return;
}
}
result[key] = isObject ? filterObject(value, filterKeys, excludeTranslations) : value;
});
}
// It is an export function and used somewhere else
export function filterRoles(roleArray, type, defaultRoles) {
const bigRoleArray = [];
roleArray.forEach(role => (role.type === 'mixed' || role.type) === type &&
!bigRoleArray.includes(role.role) &&
bigRoleArray.push(role.role));
defaultRoles?.forEach(role => (role.type === 'mixed' || role.type) === type &&
!bigRoleArray.includes(role.role) &&
bigRoleArray.push(role.role));
bigRoleArray.sort();
return bigRoleArray;
}
/**
* Function to generate a json-file for an object and trigger download it
*/
export function generateFile(
/** The desired filename */
fileName,
/** The objects which should be downloaded */
obj,
/** Options to filter/reduce the output */
options) {
const el = document.createElement('a');
const filterKeys = [];
if (options.excludeSystemRepositories) {
filterKeys.push('system.repositories');
}
const filteredObject = filterKeys.length > 0 || options.excludeTranslations
? filterObject(obj, filterKeys, options.excludeTranslations)
: obj;
const data = options.beautify ? JSON.stringify(filteredObject, null, 2) : JSON.stringify(filteredObject);
el.setAttribute('href', `data:application/json;charset=utf-8,${encodeURIComponent(data)}`);
el.setAttribute('download', fileName);
el.style.display = 'none';
document.body.appendChild(el);
el.click();
document.body.removeChild(el);
}
export class CustomFilterSelect extends Component {
hasIcons;
timer = null;
constructor(props) {
super(props);
this.state = {
value: props.initialValue || [],
};
this.hasIcons = !!props.values?.find(item => item.icon);
}
componentWillUnmount() {
if (this.timer) {
clearTimeout(this.timer);
}
}
render() {
return (React.createElement("div", { style: { position: 'relative' } },
React.createElement(Select, { variant: "standard", key: this.props.name, sx: styles.headerCellInput, className: "no-underline", multiple: true, renderValue: value => {
if (!value?.length) {
return this.props.name === 'custom'
? this.props.texts.showAll
: this.props.texts[`filter_${this.props.name}`];
}
return value.map(val => {
const item = this.props.values.find(i => typeof i === 'object' ? i.value === val : i === val);
let id;
let _name;
let icon;
if (typeof item === 'object') {
id = item.value;
_name = item.name;
icon = item.icon;
}
else {
id = item;
_name = item;
}
return (React.createElement(Box, { component: "span", sx: styles.headerCellSelectItem, key: id },
icon || (this.hasIcons ? React.createElement("div", { className: "itemIcon" }) : null),
_name));
});
}, value: this.state.value, onChange: event => {
let selectedValues = event.target.value;
// '_' may be selected only alone
if (this.state.value[0] === '_' && selectedValues.includes('_') && selectedValues.length > 1) {
const pos = selectedValues.indexOf('_');
if (pos !== -1) {
selectedValues.splice(pos, 1);
}
}
else if (this.state.value[0] !== '_' && selectedValues.includes('_')) {
selectedValues = ['_'];
}
// '_' may be selected only alone
if (selectedValues.includes('')) {
selectedValues = [];
}
this.setState({ value: selectedValues }, () => {
if (this.timer) {
clearTimeout(this.timer);
}
this.timer = setTimeout(() => {
this.timer = null;
this.props.onChange(this.props.name, selectedValues);
}, 400);
});
}, onClose: () => {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
this.props.onChange(this.props.name, this.state.value);
}
}, inputProps: { name: this.props.name, id: this.props.name }, displayEmpty: true },
React.createElement(MenuItem, { key: "empty", value: "" },
React.createElement("span", { style: styles.selectNone }, this.props.name === 'custom'
? this.props.texts.showAll
: this.props.texts[`filter_${this.props.name}`])),
this.props.values?.map(item => {
let id;
let _name;
let icon;
if (typeof item === 'object') {
id = item.value;
_name = item.name;
icon = item.icon;
}
else {
id = item;
_name = item;
}
return (React.createElement(MenuItem, { sx: styles.headerCellSelectItem, key: id, value: id },
icon || (this.hasIcons ? React.createElement("div", { className: "itemIcon" }) : null),
_name));
})),
this.state.value.length ? (React.createElement(Box, { component: "div", sx: styles.selectClearButton },
React.createElement(IconButton, { size: "small", onClick: () => {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
this.setState({ value: [] }, () => this.props.onChange(this.props.name, undefined));
} },
React.createElement(IconClose, null)))) : null));
}
}
export class CustomFilterInput extends Component {
timer = null;
constructor(props) {
super(props);
this.state = {
value: props.initialValue || '',
};
}
componentWillUnmount() {
if (this.timer) {
clearTimeout(this.timer);
}
}
render() {
return (React.createElement(FormControl, { sx: this.props.styles, key: this.props.name, title: this.props.t('ra_You can use * as wildcard'), margin: "dense" },
React.createElement(Input, { classes: { underline: 'no-underline' }, id: this.props.name, placeholder: this.props.texts[`filter_${this.props.name}`], value: this.state.value, onChange: event => {
const selectedValues = event.target.value;
this.setState({ value: selectedValues }, () => {
if (this.timer) {
clearTimeout(this.timer);
}
this.timer = setTimeout(() => {
this.timer = null;
this.props.onChange(this.props.name, selectedValues);
}, 400);
});
}, onBlur: () => {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
this.props.onChange(this.props.name, this.state.value);
}
}, autoComplete: "off" }),
this.state.value ? (React.createElement("div", { style: {
position: 'absolute',
right: 0,
} },
React.createElement(IconButton, { size: "small", onClick: () => {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
this.setState({ value: '' }, () => this.props.onChange(this.props.name, undefined));
} },
React.createElement(IconClose, null)))) : null));
}
}
// d=data, t=target, s=start, e=end, m=middle
export function binarySearch(list, find, _start, _end) {
_start ||= 0;
if (_end === undefined) {
_end = list.length - 1;
if (!_end) {
return list[0] === find;
}
}
const middle = Math.floor((_start + _end) / 2);
if (find === list[middle]) {
return true;
}
if (_end - 1 === _start) {
return list[_start] === find || list[_end] === find;
}
if (find > list[middle]) {
return binarySearch(list, find, middle, _end);
}
if (find < list[middle]) {
return binarySearch(list, find, _start, middle);
}
return false;
}
export function getName(name, lang) {
if (typeof name === 'object') {
if (!name) {
return '';
}
return (name[lang] || name.en || '').toString();
}
return name ? name.toString() : '';
}
export function getSelectIdIconFromObjects(objects, id, lang, imagePrefix) {
// `admin` has prefix '.' and `web` has '../..'
imagePrefix ||= '.'; // http://localhost:8081';
let src = '';
const _id_ = `system.adapter.${id}`;
const aIcon = id && objects[_id_]?.common?.icon;
if (aIcon) {
// if not BASE64
if (!aIcon.startsWith('data:image/')) {
if (aIcon.includes('.')) {
const name = objects[_id_].common.name;
if (typeof name === 'object') {
src = `${imagePrefix}/adapter/${name[lang] || name.en}/${aIcon}`;
}
else {
src = `${imagePrefix}/adapter/${name}/${aIcon}`;
}
}
else if (aIcon && aIcon.length < 3) {
return aIcon; // utf-8
}
else {
return null; // '<i class="material-icons iob-list-icon">' + objects[_id_].common.icon + '</i>';
}
}
else if (aIcon.startsWith('data:image/svg')) {
src = (React.createElement(SVG, { className: "iconOwn", src: aIcon, width: 28, height: 28 }));
}
else {
src = aIcon;
}
}
else {
const common = objects[id] && objects[id].common;
if (common) {
const cIcon = common.icon;
if (cIcon) {
if (!cIcon.startsWith('data:image/')) {
if (cIcon.includes('.')) {
let instance;
if (objects[id].type === 'instance' || objects[id].type === 'adapter') {
if (typeof common.name === 'object') {
src = `${imagePrefix}/adapter/${common.name[lang] || common.name.en}/${cIcon}`;
}
else {
src = `${imagePrefix}/adapter/${common.name}/${cIcon}`;
}
}
else if (id && id.startsWith('system.adapter.')) {
instance = id.split('.', 3);
if (cIcon[0] === '/') {
instance[2] += cIcon;
}
else {
instance[2] += `/${cIcon}`;
}
src = `${imagePrefix}/adapter/${instance[2]}`;
}
else {
instance = id.split('.', 2);
if (cIcon[0] === '/') {
instance[0] += cIcon;
}
else {
instance[0] += `/${cIcon}`;
}
src = `${imagePrefix}/adapter/${instance[0]}`;
}
}
else if (aIcon && aIcon.length < 3) {
return aIcon; // utf-8
}
else {
return null;
}
}
else if (cIcon.startsWith('data:image/svg')) {
// if base 64 image
src = (React.createElement(SVG, { className: "iconOwn", src: cIcon, width: 28, height: 28 }));
}
else {
src = cIcon;
}
}
}
}
return src || null;
}
export function applyFilter(item, filters, lang, objects, context, counter, customFilter, selectedTypes, _depth) {
_depth ||= 0;
let filteredOut = false;
if (!context) {
context = {};
if (filters.id) {
const id = filters.id.toLowerCase();
if (id.includes('*')) {
context.idRx = new RegExp(pattern2RegEx(filters.id), 'i');
}
else {
context.id = id;
}
}
if (filters.name) {
const name = filters.name.toLowerCase();
if (name.includes('*')) {
context.nameRx = new RegExp(pattern2RegEx(name), 'i');
}
else {
context.name = name;
}
}
if (filters.type?.length) {
context.type = filters.type.map(f => f.toLowerCase());
}
if (filters.custom?.length) {
context.custom = filters.custom.map(c => c.toLowerCase());
}
if (filters.role?.length) {
context.role = filters.role.map(r => r.toLowerCase());
}
if (filters.room?.length) {
context.room = [];
filters.room.forEach(room => {
context.room = context.room.concat(objects[room]?.common?.members || []);
});
}
if (filters.func?.length) {
context.func = [];
filters.func.forEach(func => {
context.func = context.func.concat(objects[func]?.common?.members || []);
});
}
}
const data = item.data;
if (data?.id) {
const common = data.obj?.common;
if (customFilter) {
if (customFilter.type) {
if (typeof customFilter.type === 'string') {
if (!data.obj || customFilter.type !== data.obj.type) {
filteredOut = true;
}
}
else if (Array.isArray(customFilter.type)) {
if (!data.obj || !customFilter.type.includes(data.obj.type)) {
filteredOut = true;
}
}
}
if (!filteredOut && customFilter.common?.type) {
if (!common?.type) {
filteredOut = true;
}
else if (typeof customFilter.common.type === 'string') {
if (customFilter.common.type !== common.type) {
filteredOut = true;
}
}
else if (Array.isArray(customFilter.common.type)) {
if (!customFilter.common.type.includes(common.type)) {
filteredOut = true;
}
}
}
if (!filteredOut && customFilter.common?.role) {
if (!common?.role) {
filteredOut = true;
}
else if (typeof customFilter.common.role === 'string') {
if (common.role.startsWith(customFilter.common.role)) {
filteredOut = true;
}
}
else if (Array.isArray(customFilter.common.role)) {
if (!customFilter.common.role.find(role => common.role.startsWith(role))) {
filteredOut = true;
}
}
}
if (!filteredOut && customFilter.common?.custom === '_' && common?.custom) {
filteredOut = true;
}
else if (!filteredOut && customFilter.common?.custom && customFilter.common?.custom !== '_') {
const filterOfCustom = customFilter.common.custom;
if (!common?.custom) {
filteredOut = true;
}
else if (filterOfCustom === '_dataSources') {
// TODO: make it configurable
if (!Object.keys(common.custom).find(id => id.startsWith('history.') || id.startsWith('sql.') || id.startsWith('influxdb.'))) {
filteredOut = true;
}
}
else if (Array.isArray(filterOfCustom)) {
// here are ['influxdb.', 'telegram.']
const customs = Object.keys(common.custom); // here are ['influxdb.0', 'telegram.2']
if (filterOfCustom.find(cst => customs.find(id => id.startsWith(cst)))) {
filteredOut = true;
}
}
else if (filterOfCustom !== true &&
!Object.keys(common.custom).find(id => id.startsWith(filterOfCustom))) {
filteredOut = true;
}
}
}
if (!filteredOut && !filters.expertMode) {
filteredOut =
data.id === 'system' ||
data.id === 'enum' ||
// (data.obj && data.obj.type === 'meta') ||
data.id.startsWith('system.') ||
data.id.startsWith('enum.') ||
data.id.startsWith('_design/') ||
data.id.endsWith('.admin') ||
!!common?.expert;
}
if (!filteredOut && context.id) {
if (data.fID === undefined) {
data.fID = data.id.toLowerCase();
}
filteredOut = !data.fID.includes(context.id);
}
if (!filteredOut && context.idRx) {
filteredOut = !context.idRx.test(data.id);
}
if (!filteredOut && context.name) {
if (common) {
if (data.fName === undefined) {
data.fName = getName(common.name, lang) || '';
data.fName = data.fName.toLowerCase();
}
filteredOut = !data.fName.includes(context.name);
}
else {
filteredOut = true;
}
}
if (!filteredOut && context.nameRx) {
if (common) {
if (data.fName === undefined) {
data.fName = getName(common.name, lang) || '';
data.fName = data.fName.toLowerCase();
}
filteredOut = !context.nameRx.test(data.fName);
}
}
if (!filteredOut && filters.role?.length && common) {
filteredOut = !(typeof common.role === 'string' && context.role.find(role => common.role.startsWith(role)));
}
if (!filteredOut && context.room?.length) {
filteredOut = !context.room.find(id => id === data.id || data.id.startsWith(`${id}.`));
}
if (!filteredOut && context.func?.length) {
filteredOut = !context.func.find(id => id === data.id || data.id.startsWith(`${id}.`));
}
if (!filteredOut && context.type?.length) {
filteredOut = !(data.obj?.type && context.type.includes(data.obj.type));
}
if (!filteredOut && selectedTypes) {
filteredOut = !(data.obj?.type && selectedTypes.includes(data.obj.type));
}
if (!filteredOut && context.custom?.length) {
if (common) {
if (context.custom[0] === '_') {
filteredOut = !!common.custom;
}
else if (common.custom) {
filteredOut = !context.custom.find(custom => common.custom[custom]);
}
else {
filteredOut = true;
}
}
else {
filteredOut = context.custom[0] !== '_';
}
}
}
data.visible = !filteredOut;
data.hasVisibleChildren = false;
if (item.children && _depth < 20) {
item.children.forEach(_item => {
const visible = applyFilter(_item, filters, lang, objects, context, counter, customFilter, selectedTypes, _depth + 1);
if (visible) {
data.hasVisibleChildren = true;
}
});
}
// const visible = data.visible || data.hasVisibleChildren;
data.sumVisibility = data.visible || data.hasVisibleChildren; // || data.hasVisibleParent;
if (counter && data.sumVisibility) {
counter.count++;
}
// show all children of visible object with opacity 0.5
if (data.id && data.sumVisibility && item.children) {
item.children.forEach(_item => (_item.data.hasVisibleParent = true));
}
return data.visible || data.hasVisibleChildren;
}
export function getVisibleItems(item, type, objects, _result) {
_result ||= [];
const data = item.data;
if (data.sumVisibility) {
if (data.id && objects[data.id] && (!type || objects[data.id].type === type)) {
_result.push(data.id);
}
item.children?.forEach(_item => getVisibleItems(_item, type, objects, _result));
}
return _result;
}
function getSystemIcon(objects, id, level, themeType, lang, imagePrefix) {
let icon;
// system or design has special icons
if (id === 'alias' || id === 'alias.0') {
icon = (React.createElement(IconLink, { className: "iconOwn", style: { color: COLOR_NAME_ALIAS(themeType) } }));
}
else if (id === '0_userdata' || id === '0_userdata.0') {
icon = (React.createElement(IconData, { className: "iconOwn", style: { color: COLOR_NAME_USERDATA(themeType) } }));
}
else if (id.startsWith('_design/') || id === 'system') {
icon = (React.createElement(IconSystem, { className: "iconOwn", style: { color: COLOR_NAME_SYSTEM(themeType) } }));
}
else if (id === 'system.adapter') {
icon = (React.createElement(IconSystem, { className: "iconOwn", style: { color: COLOR_NAME_SYSTEM_ADAPTER(themeType) } }));
}
else if (id === 'system.group') {
icon = React.createElement(IconGroup, { className: "iconOwn" });
}
else if (id === 'system.user') {
icon = React.createElement(IconUser, { className: "iconOwn" });
}
else if (id === 'system.host') {
icon = React.createElement(IconHost, { className: "iconOwn" });
}
else if (id.endsWith('.connection') || id.endsWith('.connected')) {
icon = React.createElement(IconConnection, { className: "iconOwn" });
}
else if (id.endsWith('.info')) {
icon = React.createElement(IconInfo, { className: "iconOwn" });
}
else if (objects[id] && objects[id].type === 'meta') {
icon = React.createElement(IconMeta, { className: "iconOwn" });
}
else if (level < 2) {
// detect "cloud.0"
if (objects[`system.adapter.${id}`]) {
icon = getSelectIdIconFromObjects(objects, `system.adapter.${id}`, lang, imagePrefix);
}
}
return icon || null;
}
export function getObjectTooltip(data, lang) {
if (data?.obj?.common?.desc) {
return getName(data.obj.common.desc, lang) || null;
}
return null;
}
export function getIdFieldTooltip(data, lang) {
const tooltip = getObjectTooltip(data, lang);
if (tooltip?.startsWith('http')) {
return (React.createElement(Box, { component: "a", sx: styles.cellIdTooltipLink, href: tooltip, target: "_blank", rel: "noreferrer" }, tooltip));
}
return React.createElement("span", { style: styles.cellIdTooltip }, tooltip || data.id || '');
}
export function buildTree(objects, options) {
const imagePrefix = options.imagePrefix || '.';
let ids = Object.keys(objects);
ids.sort((a, b) => {
if (a === b) {
return 0;
}
a = a.replace(/\./g, '!!!');
b = b.replace(/\./g, '!!!');
if (a > b) {
return 1;
}
return -1;
});
if (options.root) {
ids = ids.filter(id => id === options.root || id.startsWith(`${options.root}.`));
}
// find empty nodes and create names for it
let currentPathArr = [];
let currentPath = '';
let currentPathLen = 0;
const root = {
data: {
name: '',
id: '',
},
children: [],
};
const info = {
funcEnums: [],
roomEnums: [],
roles: [],
ids: [],
types: [],
objects,
customs: ['_'],
enums: [],
hasSomeCustoms: false,
aliasesMap: {},
};
let cRoot = root;
for (let i = 0; i < ids.length; i++) {
const id = ids[i];
if (!id) {
continue;
}
const obj = objects[id];
const parts = id.split('.');
if (obj.type && !info.types.includes(obj.type)) {
info.types.push(obj.type);
}
if (obj) {
const common = obj.common;
const role = common?.role;
if (role && !info.roles.find(it => it.role === role)) {
if (typeof role !== 'string') {
console.warn(`Invalid role type "${typeof role}" in "${obj._id}"`);
}
else {
info.roles.push({ role, type: common.type });
}
}
else if (id.startsWith('enum.rooms.')) {
info.roomEnums.push(id);
info.enums.push(id);
}
else if (id.startsWith('enum.functions.')) {
info.funcEnums.push(id);
info.enums.push(id);
}
else if (obj.type === 'enum') {
info.enums.push(id);
}
else if (obj.type === 'instance' && common && (common.supportCustoms || common.adminUI?.custom)) {
info.hasSomeCustoms = true;
info.customs.push(id.substring('system.adapter.'.length));
}
// Build a map of aliases
if (id.startsWith('alias.') && obj.common.alias?.id) {
if (typeof obj.common.alias.id === 'string') {
const usedId = obj.common.alias.id;
if (!info.aliasesMap[usedId]) {
info.aliasesMap[usedId] = [id];
}
else if (!info.aliasesMap[usedId].includes(id)) {
info.aliasesMap[usedId].push(id);
}
}
else {
const readId = obj.common.alias.id.read;
if (readId) {
if (!info.aliasesMap[readId]) {
info.aliasesMap[readId] = [id];
}
else if (!info.aliasesMap[readId].includes(id)) {
info.aliasesMap[readId].push(id);
}
}
const writeId = obj.common.alias.id.write;
if (writeId) {
if (!info.aliasesMap[writeId]) {
info.aliasesMap[writeId] = [id];
}
else if (!info.aliasesMap[writeId].includes(id)) {
info.aliasesMap[writeId].push(id);
}
}
}
}
}
info.ids.push(id);
let repeat;
// if next level
do {
repeat = false;
// If the current level is still OK, and we can add ID to children
if (!currentPath || id.startsWith(`${currentPath}.`)) {
// if more than one level added
if (parts.length - currentPathLen > 1) {
let curPath = currentPath;
// generate missing levels
for (let k = currentPathLen; k < parts.length - 1; k++) {
curPath += (curPath ? '.' : '') + parts[k];
// level does not exist
if (!binarySearch(info.ids, curPath)) {
const _cRoot = {
data: {
name: parts[k],
parent: cRoot,
id: curPath,
obj: objects[curPath],
level: k,
icon: getSystemIcon(objects, curPath, k, options.themeType, options.lang, imagePrefix),
generated: true,
},
};
cRoot.children ||= [];
cRoot.children.push(_cRoot);
cRoot = _cRoot;
info.ids.push(curPath); // IDs will be added by alphabet
}
else if (cRoot.children) {
cRoot = cRoot.children.find(item => item.data.name === parts[k]);
}
}
}
const _cRoot = {
data: {
name: parts[parts.length - 1],
title: getName(obj?.common?.name, options.lang),
obj,
parent: cRoot,
icon: getSelectIdIconFromObjects(objects, id, options.lang, imagePrefix) ||
getSystemIcon(objects, id, 0, options.themeType, options.lang, imagePrefix),
id,
hasCustoms: !!(obj.common?.custom && Object.keys(obj.common.custom).length),
level: parts.length - 1,
generated: false,
button: obj.type === 'state' &&
!!obj.common?.role &&
typeof obj.common.role === 'string' &&
obj.common.role.startsWith('button') &&
obj.common?.write !== false,
switch: obj.type === 'state' &&
obj.common?.type === 'boolean' &&
obj.common?.write !== false &&
obj.common?.read !== false,
url: !!obj.common?.role &&
typeof obj.common.role === 'string' &&
obj.common.role.startsWith('url'),
},
};
cRoot.children ||= [];
cRoot.children.push(_cRoot);
cRoot = _cRoot;
currentPathLen = parts.length;
currentPathArr = parts;
currentPath = id;
}
else {
let u = 0;
while (currentPathArr[u] === parts[u]) {
u++;
}
if (u > 0) {
let move = currentPathArr.length;
currentPathArr = currentPathArr.splice(0, u);
currentPathLen = u;
currentPath = currentPathArr.join('.');
while (move > u) {
if (cRoot.data.parent) {
cRoot = cRoot.data.parent;
}
else {
console.error(`Parent is null for ${id} ${currentPath} ${currentPathArr.join('.')}`);
}
move--;
}
}
else {
cRoot = root;
currentPathArr = [];
currentPath = '';
currentPathLen = 0;
}
repeat = true;
}
} while (repeat);
}
info.roomEnums.sort((a, b) => {
const aName = getName(objects[a]?.common?.name, options.lang) || a.split('.').pop();
const bName = getName(objects[b]?.common?.name, options.lang) || b.split('.').pop();
if (aName > bName) {
return 1;
}
if (aName < bName) {
return -1;
}
return 0;
});
info.funcEnums.sort((a, b) => {
const aName = getName(objects[a]?.common?.name, options.lang) || a.split('.').pop();
const bName = getName(objects[b]?.common?.name, options.lang) || b.split('.').pop();
if (aName > bName) {
return 1;
}
if (aName < bName) {
return -1;
}
return 0;
});
info.roles.sort((a, b) => a.role.localeCompare(b.role));
info.types.sort();
return { info, root };
}
export function findNode(root, id, _parts, _path, _level) {
if (root.data.id === id) {
return root;
}
if (!_parts) {
_parts = id.split('.');
_level = 0;
_path = _parts[_level];
}
if (!root.children && root.data.id !== id) {
return null;
}
let found;
if (root.children) {
for (let i = 0; i < root.children.length; i++) {
const _id = root.children[i].data.id;
if (_id === _path) {
found = root.children[i];
break;
}
else if (_id > _path) {
break;
}
}
}
if (found) {
_level ||= 0;
return findNode(found, id, _parts, `${_path}.${_parts[_level + 1]}`, _level + 1);
}
return null;
}
export function findRoomsForObject(info, id, lang, rooms) {
if (!id) {
return { rooms: [], per: false };
}
rooms ||= [];
for (const room of info.roomEnums) {
const common = info.objects[room]?.common;
if (!common) {
continue;
}
const name = getName(common.name, lang);
if (common.members?.includes(id) && !rooms.includes(name)) {
rooms.push(name);
}
}
let ownEnums;
// Check parent
const parts = id.split('.');
parts.pop();
id = parts.join('.');
if (info.objects[id]) {
ownEnums = rooms.length;
findRoomsForObject(info, id, lang, rooms);
}
return { rooms, per: !ownEnums }; // per is if the enums are from parent
}
export function findEnumsForObjectAsIds(info, id, enumName, funcs) {
if (!id) {
return [];
}
funcs ||= [];
for (let i = 0; i < info[enumName].length; i++) {
const common = info.objects[info[enumName][i]]?.common;
if (common?.members?.includes(id) && !funcs.includes(info[enumName][i])) {
funcs.push(info[enumName][i]);
}
}
funcs.sort();
return funcs;
}
export function findFunctionsForObject(info, id, lang, funcs) {
if (!id) {
return { funcs: [], pef: false };
}
funcs ||= [];
for (let i = 0; i < info.funcEnums.length; i++) {
const common = info.objects[info.funcEnums[i]]?.common;
if (!common) {
continue;
}
const name = getName(common.name, lang);
if (common.members?.includes(id) && !funcs.includes(name)) {
funcs.push(name);
}
}
let ownEnums;
// Check parent
const parts = id.split('.');
parts.pop();
id = parts.join('.');
if (info.objects[id]) {
ownEnums = funcs.length;
findFunctionsForObject(info, id, lang, funcs);
}
return { funcs, pef: !ownEnums };
}
/*
function quality2text(q) {
if (!q) {
return 'ok';
}
const custom = q & 0xFFFF0000;
let text = '';
if (q & 0x40) text += 'device';
if (q & 0x80) text += 'sensor';
if (q & 0x01) text += ' bad';
if (q & 0x02) text += ' not connected';
if (q & 0x04) text += ' error';
return text + (custom ? '|0x' + (custom >> 16).toString(16).toUpperCase() : '') + ' [0x' + q.toString(16).toUpperCase() + ']';
}
*/
/**
* Format a state value for visualization
*/
export function formatValue(options) {
const { dateFormat, state, isFloatComma, texts, obj } = options;
const states = Utils.getStates(obj);
const isCommon = obj.common;
let fileViewer;
let v =
// @ts-expect-error deprecated from js-controller 6
isCommon?.type === 'file'
? '[file]'
: !state || state.val === null
? '(null)'
: state.val === undefined
? '[undef]'
: state.val;
const type = typeof v;
if (isCommon?.role && typeof isCommon.role === 'string' && isCommon.role.match(/^value\.time|^date/)) {
if (v && typeof v === 'string') {
if (Utils.isStringInteger(v)) {
// we assume a unix ts
v = new Date(parseInt(v, 10)).toString();
}
else {
// check if parsable by new date
try {
const parsedDate = new Date(v);
if (Utils.isValidDate(parsedDate)) {
v = parsedDate.toString();
}
}
catch {
// ignore
}
}
}
else {
if (v > 946681200 && v < 946681200000) {
// '2000-01-01T00:00:00' => 946681200000
v *= 1_000; // maybe the time is in seconds (UNIX time)
}
// "null" and undefined could not be here. See `let v = (isCommon && isCommon.type === 'file') ....` above
v = v ? new Date(v).toString() : v;
}
}
else if (isCommon?.role && typeof isCommon.role === 'string' && isCommon.role.match(/^value\.duration/)) {
// Format duration values in HH:mm:ss format
if (typeof v === 'number' && v >= 0) {
const hours = Math.floor(v / 3600);
const minutes = Math.floor((v % 3600) / 60);
const seconds = Math.floor(v % 60);
v = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
}
else if (typeof v === 'string' && Utils.isStringInteger(v)) {
const numValue = parseInt(v, 10);
if (numValue >= 0) {
const hours = Math.floor(numValue / 3600);
const minutes = Math.floor((numValue % 3600) / 60);
const seconds = Math.floor(numValue % 60);
v = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
}
}
}
else {
if (type === 'number') {
if (!Number.isInteger(v)) {
v = Math.round(v * 100_000_000) / 100_000_000; // remove 4.00000000000000001
if (isFloatComma) {
v = v.toString().replace('.', ',');
}
}
}
else if (type === 'object') {
v = JSON.stringify(v);
}
else if (type !== 'string') {
v = v.toString();
}
else if (v.startsWith('data:image/')) {
fileViewer = 'image';
}
if (typeof v !== 'string') {
v = v.toString();
}
}
const valText = { v: v };
// try to replace number with "common.states"
if (states && states[v] !== undefined) {
if (v !== states[v]) {
valText.s = v;
v = states[v];
valText.v = v;
}
}
if (valText.v?.length > 40) {
valText.c = valText.v;
valText.v = `${valText.v.substring(0, 40)}...`;
}
if (isCommon?.unit) {
valText.u = isCommon.unit;
}
let valFull;
if (options.full) {
if (typeof v === 'string' && v.length > 100) {
valFull = [{ t: texts.value, v: `${v.substring(0, 100)}...` }];
}
else {
valFull = [{ t: texts.value, v }];
}
if (state) {
if (state.ack !== undefined && state.ack !== null) {
valFull.push({ t: texts.ack, v: state.ack.toString() });
}
if (state.ts) {
valFull.push({ t: texts.ts, v: state.ts ? Utils.formatDate(new Date(state.ts), dateFormat) : '' });
}
if (state.lc) {
valFull.push({ t: texts.lc, v: state.lc ? Utils.formatDate(new Date(state.lc), dateFormat) : '' });
}
if (state.from) {
let from = state.from.toString();
if (from.startsWith('system.adapter.')) {
from = from.substring(15);
}
valFull.push({ t: texts.from, v: from });
}
if (state.user) {
let user = state.user.toString();
if (user.startsWith('system.user.')) {
user = user.substring(12);
}
valFull.push({ t: texts.user, v: user });
}
if (state.c) {
valFull.push({ t: texts.c, v: state.c });
}
valFull.push({ t: texts.quality, v: Utils.quality2text(state.q || 0).join(', '), nbr: true });
}
}
return {
valText,
valFull,
fileViewer,
};
}
/**
* Get CSS style for given state value
*/
export function getValueStyle(options) {
const { state /* , isExpertMode, isButton */ } = options;
const color = state?.ack ? (state.q ? '#ffa500' : '') : '#ff2222c9';
// do not show the color of the button in non-expert mode
// if (!isExpertMode && isButton) {
// color = '';
// }
return { color };
}
export function prepareSparkData(values, from) {
// set one point every hour
let time = from;
let i = 1;
const v = [];
while (i < values.length && time < from + 25 * 3600000) {
// find the interval
while (values[i - 1].ts < time && time <= values[i].ts && i < values.length) {
i++;
}
if (i === 1 && values[i - 1].ts >= time) {
// assume the value was always null
v.push(0);
}
else if (i < values.length) {
if (typeof values[i].val === 'boolean' || typeof values[i - 1].val === 'boolean') {
v.push(values[i].val ? 1 : 0);
}
else {
// remove nulls
values[i - 1].val ||= 0;
values[i].val ||= 0;
// interpolate
const nm1 = values[i - 1].val;
const n = values[i].val;
const val