@iobroker/dm-gui-components 10.0.23 → 10.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/build/AnimatedIcons.d.ts +37 -0
- package/build/AnimatedIcons.js +108 -0
- package/build/AnimatedIcons.js.map +1 -0
- package/build/Communication.d.ts +7 -0
- package/build/Communication.js +43 -11
- package/build/Communication.js.map +1 -1
- package/build/DeviceCard.d.ts +25 -13
- package/build/DeviceCard.js +177 -282
- package/build/DeviceCard.js.map +1 -1
- package/build/DeviceControl.js +38 -26
- package/build/DeviceControl.js.map +1 -1
- package/build/DeviceImageUpload.d.ts +5 -1
- package/build/DeviceImageUpload.js +79 -62
- package/build/DeviceImageUpload.js.map +1 -1
- package/build/DeviceList.d.ts +8 -0
- package/build/DeviceList.js +74 -59
- package/build/DeviceList.js.map +1 -1
- package/build/DeviceStatus.d.ts +20 -4
- package/build/DeviceStatus.js +146 -191
- package/build/DeviceStatus.js.map +1 -1
- package/build/StatusIndicator.js +9 -37
- package/build/StatusIndicator.js.map +1 -1
- package/build/StatusPill.d.ts +18 -0
- package/build/StatusPill.js +30 -0
- package/build/StatusPill.js.map +1 -0
- package/build/Switch.d.ts +3 -0
- package/build/Switch.js +36 -26
- package/build/Switch.js.map +1 -1
- package/build/Utils.js +92 -60
- package/build/Utils.js.map +1 -1
- package/build/i18n/de.json +3 -0
- package/build/i18n/en.json +3 -0
- package/build/i18n/es.json +3 -0
- package/build/i18n/fr.json +3 -0
- package/build/i18n/it.json +3 -0
- package/build/i18n/nl.json +3 -0
- package/build/i18n/pl.json +3 -0
- package/build/i18n/pt.json +3 -0
- package/build/i18n/ru.json +3 -0
- package/build/i18n/uk.json +3 -0
- package/build/i18n/zh-cn.json +3 -0
- package/build/styles.d.ts +17 -0
- package/build/styles.js +226 -0
- package/build/styles.js.map +1 -0
- package/package.json +10 -10
package/build/DeviceCard.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
import React, { PureComponent } from 'react';
|
|
2
|
-
import { Close as CloseIcon, VideogameAsset as ControlIcon, MoreVert as MoreVertIcon
|
|
3
|
-
import {
|
|
4
|
-
import { DeviceTypeIcon, Utils, Icon, } from '@iobroker/gui-components';
|
|
1
|
+
import React, { PureComponent, useState } from 'react';
|
|
2
|
+
import { Close as CloseIcon, VideogameAsset as ControlIcon, MoreVert as MoreVertIcon } from '@mui/icons-material';
|
|
3
|
+
import { Button, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, Skeleton, Tooltip, Typography, Accordion, AccordionSummary, AccordionDetails, } from '@mui/material';
|
|
4
|
+
import { DeviceTypeIcon, Utils, Icon, IconCopyToClipboard, IconFolder, } from '@iobroker/gui-components';
|
|
5
5
|
import DeviceActionButton from './DeviceActionButton';
|
|
6
6
|
import DeviceControlComponent from './DeviceControl';
|
|
7
7
|
import DeviceImageUpload from './DeviceImageUpload';
|
|
8
|
-
import DeviceStatusComponent from './DeviceStatus';
|
|
8
|
+
import DeviceStatusComponent, { DeviceLed } from './DeviceStatus';
|
|
9
9
|
import JsonConfig from './JsonConfig';
|
|
10
10
|
import { StatusIndicators } from './StatusIndicator';
|
|
11
|
+
import Switch from './Switch';
|
|
11
12
|
import { getText, getTranslation } from './Utils';
|
|
13
|
+
import { ensureDmStyles, getThemeVars } from './styles';
|
|
12
14
|
/** Reserved action names (this is copied from https://github.com/ioBroker/dm-utils/blob/main/src/types/base.ts as we can only have type references to dm-utils) */
|
|
13
15
|
const ACTIONS = {
|
|
14
16
|
/** This action will be called when the user clicks on the connection icon */
|
|
@@ -25,10 +27,10 @@ const ACTIONS = {
|
|
|
25
27
|
* the same size, so that neither the layout nor the length of the scrollbar changes.
|
|
26
28
|
*/
|
|
27
29
|
export const CARD_WIDTH = 300;
|
|
28
|
-
export const CARD_MIN_HEIGHT =
|
|
29
|
-
export const CARD_MARGIN =
|
|
30
|
-
export const SMALL_CARD_WIDTH =
|
|
31
|
-
export const SMALL_CARD_MIN_HEIGHT =
|
|
30
|
+
export const CARD_MIN_HEIGHT = 250;
|
|
31
|
+
export const CARD_MARGIN = 8;
|
|
32
|
+
export const SMALL_CARD_WIDTH = 210;
|
|
33
|
+
export const SMALL_CARD_MIN_HEIGHT = 190;
|
|
32
34
|
export const SMALL_CARD_MARGIN = 5;
|
|
33
35
|
const cardSize = { width: CARD_WIDTH, minHeight: CARD_MIN_HEIGHT, margin: CARD_MARGIN };
|
|
34
36
|
const smallCardSize = {
|
|
@@ -38,6 +40,7 @@ const smallCardSize = {
|
|
|
38
40
|
};
|
|
39
41
|
/** A card that sits in a container which already has the footprint of a card */
|
|
40
42
|
const filledCardSize = { width: '100%', margin: 0 };
|
|
43
|
+
const tooltipSlotProps = { popper: { sx: { pointerEvents: 'none' } } };
|
|
41
44
|
/**
|
|
42
45
|
* Icons read from the file storage of the adapter, keyed by `<instance>/<file>`.
|
|
43
46
|
*
|
|
@@ -45,69 +48,44 @@ const filledCardSize = { width: '100%', margin: 0 };
|
|
|
45
48
|
* would repeat the `readFile` round trip for an icon that is very often not there at all.
|
|
46
49
|
*/
|
|
47
50
|
const fileIconCache = new Map();
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
},
|
|
78
|
-
titleStyle: {
|
|
79
|
-
fontSize: 16,
|
|
80
|
-
fontWeight: 'bold',
|
|
81
|
-
// whiteSpace: 'nowrap',
|
|
82
|
-
overflow: 'hidden',
|
|
83
|
-
textOverflow: 'ellipsis',
|
|
84
|
-
},
|
|
85
|
-
detailsButtonStyle: {
|
|
86
|
-
right: 20,
|
|
87
|
-
bottom: -20,
|
|
88
|
-
position: 'absolute',
|
|
89
|
-
},
|
|
90
|
-
bodyStyle: {
|
|
91
|
-
flex: 1,
|
|
92
|
-
display: 'flex',
|
|
93
|
-
flexDirection: 'column',
|
|
94
|
-
},
|
|
95
|
-
deviceInfoStyle: {
|
|
96
|
-
padding: '20px 16px 0 16px',
|
|
97
|
-
},
|
|
98
|
-
statusStyle: {
|
|
99
|
-
padding: '15px 25px 0 15px',
|
|
100
|
-
// The line may grow if the device provides custom indicators, so only the minimum is fixed
|
|
101
|
-
minHeight: 41,
|
|
102
|
-
},
|
|
103
|
-
};
|
|
104
|
-
function NoImageIcon(props) {
|
|
105
|
-
return (React.createElement("svg", { viewBox: "0 0 24 24", width: "24", height: "24", style: props.style, className: props.className },
|
|
106
|
-
React.createElement("path", { fill: "currentColor", d: "M21.9,21.9l-8.49-8.49l0,0L3.59,3.59l0,0L2.1,2.1L0.69,3.51L3,5.83V19c0,1.1,0.9,2,2,2h13.17l2.31,2.31L21.9,21.9z M5,18 l3.5-4.5l2.5,3.01L12.17,15l3,3H5z M21,18.17L5.83,3H19c1.1,0,2,0.9,2,2V18.17z" })));
|
|
51
|
+
/**
|
|
52
|
+
* True if the icon is a picture (a photo of the device, an uploaded image). A picture keeps its own
|
|
53
|
+
* colors and sits on a neutral tile; a device type or a symbol takes the color of the device.
|
|
54
|
+
*/
|
|
55
|
+
function isPicture(src) {
|
|
56
|
+
if (src.startsWith('data:image/svg')) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
return src.startsWith('data:image') || src.includes('/') || /\.(png|jpe?g|gif|webp|ico|bmp)$/i.test(src);
|
|
60
|
+
}
|
|
61
|
+
/** The status entries of a device as a list */
|
|
62
|
+
function getStatusList(device) {
|
|
63
|
+
if (!device.status) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
return Array.isArray(device.status) ? device.status : [device.status];
|
|
67
|
+
}
|
|
68
|
+
/** A group of controls in the control dialog: the folder opens with the group */
|
|
69
|
+
function ControlGroup(props) {
|
|
70
|
+
const [expanded, setExpanded] = useState(false);
|
|
71
|
+
const { control } = props;
|
|
72
|
+
return (React.createElement(Accordion, { expanded: expanded, onChange: (_e, isExpanded) => setExpanded(isExpanded), disableGutters: true },
|
|
73
|
+
React.createElement(AccordionSummary, { expandIcon: React.createElement(IconFolder, { open: expanded }),
|
|
74
|
+
// The folder shows the state itself, it must not be turned around like the arrow
|
|
75
|
+
sx: { '& .MuiAccordionSummary-expandIconWrapper.Mui-expanded': { transform: 'none' } } },
|
|
76
|
+
React.createElement(Typography, { component: "span", style: { display: 'flex', alignItems: 'center', gap: 8, color: control.color } },
|
|
77
|
+
control.icon ? React.createElement(Icon, { src: control.icon }) : null,
|
|
78
|
+
getTranslation(control.label))),
|
|
79
|
+
React.createElement(AccordionDetails, { style: { display: 'flex', flexDirection: 'column' } }, props.children)));
|
|
107
80
|
}
|
|
108
81
|
/**
|
|
109
82
|
* Device Card Component.
|
|
110
83
|
*
|
|
84
|
+
* The card is laid out like the type plate of a device: its symbol (in the color of the device, with
|
|
85
|
+
* an LED for the connection) and its name at the top, the status line below, then the facts and at
|
|
86
|
+
* the bottom the actions. The status values are neutral while all is well and tinted when they need
|
|
87
|
+
* attention, so that in a long list the devices with a problem stand out.
|
|
88
|
+
*
|
|
111
89
|
* A `PureComponent`: the list re-renders on every loading step, on every filter change and on every
|
|
112
90
|
* resolved device value. Without the shallow property comparison all cards would be re-rendered
|
|
113
91
|
* every time, so all properties the list passes down are kept stable there.
|
|
@@ -117,6 +95,7 @@ export default class DeviceCard extends PureComponent {
|
|
|
117
95
|
mounted = false;
|
|
118
96
|
constructor(props) {
|
|
119
97
|
super(props);
|
|
98
|
+
ensureDmStyles();
|
|
120
99
|
const cacheKey = DeviceCard.iconCacheKey(props);
|
|
121
100
|
this.state = {
|
|
122
101
|
open: false,
|
|
@@ -161,17 +140,6 @@ export default class DeviceCard extends PureComponent {
|
|
|
161
140
|
return;
|
|
162
141
|
}
|
|
163
142
|
this.setState({ localIcon });
|
|
164
|
-
// const response = await fetch(url);
|
|
165
|
-
// if (response.ok) {
|
|
166
|
-
// const blob = await response.blob();
|
|
167
|
-
// const reader = new FileReader();
|
|
168
|
-
// reader.onloadend = () => {
|
|
169
|
-
// setIcon(reader.result);
|
|
170
|
-
// };
|
|
171
|
-
// reader.readAsDataURL(blob);
|
|
172
|
-
// } else {
|
|
173
|
-
// throw new Error('Response not ok');
|
|
174
|
-
// }
|
|
175
143
|
}
|
|
176
144
|
catch {
|
|
177
145
|
fileIconCache.set(cacheKey, '');
|
|
@@ -207,15 +175,13 @@ export default class DeviceCard extends PureComponent {
|
|
|
207
175
|
this.setState({ details, data: details?.data || {} });
|
|
208
176
|
}
|
|
209
177
|
/**
|
|
210
|
-
* Copy the device ID to the clipboard
|
|
178
|
+
* Copy the device ID to the clipboard. The copy symbol shows a check mark as confirmation.
|
|
211
179
|
*/
|
|
212
180
|
copyToClipboard = () => {
|
|
213
181
|
const textToCopy = this.props.fields.identifier;
|
|
214
|
-
if (
|
|
215
|
-
|
|
182
|
+
if (textToCopy) {
|
|
183
|
+
Utils.copyToClipboard(textToCopy);
|
|
216
184
|
}
|
|
217
|
-
Utils.copyToClipboard(textToCopy);
|
|
218
|
-
alert(`${getTranslation('copied')} ${textToCopy} ${getTranslation('toClipboard')}!`);
|
|
219
185
|
};
|
|
220
186
|
renderDialog() {
|
|
221
187
|
if (!this.state.open || !this.state.details) {
|
|
@@ -233,12 +199,7 @@ export default class DeviceCard extends PureComponent {
|
|
|
233
199
|
.map(control => {
|
|
234
200
|
if (control.type === 'group') {
|
|
235
201
|
const children = allControls.filter(ctrl => ctrl.group === control.id);
|
|
236
|
-
return (React.createElement(
|
|
237
|
-
React.createElement(AccordionSummary, { expandIcon: React.createElement(ExpandMore, null) },
|
|
238
|
-
React.createElement(Typography, { component: "span", style: { display: 'flex', gap: 8, color: control.color } },
|
|
239
|
-
control.icon ? React.createElement(Icon, { src: control.icon }) : null,
|
|
240
|
-
getTranslation(control.label))),
|
|
241
|
-
React.createElement(AccordionDetails, { style: { display: 'flex', flexDirection: 'column' } }, this.renderControlItems(children, allControls, colors, control.id))));
|
|
202
|
+
return (React.createElement(ControlGroup, { key: control.id, control: control }, this.renderControlItems(children, allControls, colors, control.id)));
|
|
242
203
|
}
|
|
243
204
|
return (React.createElement(DeviceControlComponent, { disabled: false, key: control.id, control: control, socket: this.props.socket, colors: colors, deviceId: this.props.device.id, controlHandler: this.props.controlHandler, controlStateHandler: this.props.controlStateHandler }));
|
|
244
205
|
});
|
|
@@ -268,8 +229,8 @@ export default class DeviceCard extends PureComponent {
|
|
|
268
229
|
}
|
|
269
230
|
if (this.props.device.controls?.length) {
|
|
270
231
|
// place a button and show a control dialog
|
|
271
|
-
return (React.createElement(
|
|
272
|
-
React.createElement(ControlIcon,
|
|
232
|
+
return (React.createElement(IconButton, { size: "small", color: "primary", disabled: !this.props.alive, onClick: () => this.setState({ showControlDialog: true }) },
|
|
233
|
+
React.createElement(ControlIcon, { className: "dm-ani dm-ani-tick" })));
|
|
273
234
|
}
|
|
274
235
|
return null;
|
|
275
236
|
}
|
|
@@ -313,7 +274,7 @@ export default class DeviceCard extends PureComponent {
|
|
|
313
274
|
};
|
|
314
275
|
};
|
|
315
276
|
/** Custom indicators and the actions that requested to be shown in the status line */
|
|
316
|
-
renderIndicators(
|
|
277
|
+
renderIndicators() {
|
|
317
278
|
const reserved = [ACTIONS.STATUS, ACTIONS.ENABLE_DISABLE, ACTIONS.UPDATE, ACTIONS.BATTERY];
|
|
318
279
|
const statusActions = this.props.device.actions?.filter(a => a.placement === 'status' && !reserved.includes(a.id)) || [];
|
|
319
280
|
// An indicator switched off by the user is not shown at all. An action referenced by it stays
|
|
@@ -324,7 +285,9 @@ export default class DeviceCard extends PureComponent {
|
|
|
324
285
|
if (!indicators?.length && !statusActions.length) {
|
|
325
286
|
return null;
|
|
326
287
|
}
|
|
327
|
-
return (React.createElement(StatusIndicators, { indicators: indicators, theme: this.props.theme, stateOrObjectHandler: this.props.stateOrObjectHandler, disabled: !this.props.alive, resolveAction: this.resolveIndicatorAction,
|
|
288
|
+
return (React.createElement(StatusIndicators, { indicators: indicators, theme: this.props.theme, stateOrObjectHandler: this.props.stateOrObjectHandler, disabled: !this.props.alive, resolveAction: this.resolveIndicatorAction,
|
|
289
|
+
// The indicators flow in the status line together with the built-in values
|
|
290
|
+
style: { display: 'contents' } }, statusActions.map(action => (React.createElement(DeviceActionButton, { disabled: !this.props.alive, key: action.id, deviceId: this.props.device.id, action: action, deviceHandler: this.props.deviceHandler })))));
|
|
328
291
|
}
|
|
329
292
|
renderActions() {
|
|
330
293
|
const statusActionIds = this.getStatusActionIds();
|
|
@@ -333,67 +296,8 @@ export default class DeviceCard extends PureComponent {
|
|
|
333
296
|
? actions.map(a => (React.createElement(DeviceActionButton, { disabled: !this.props.alive, key: a.id, deviceId: this.props.device.id, action: a, deviceHandler: this.props.deviceHandler })))
|
|
334
297
|
: null;
|
|
335
298
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
? []
|
|
339
|
-
: Array.isArray(this.props.device.status)
|
|
340
|
-
? this.props.device.status
|
|
341
|
-
: [this.props.device.status];
|
|
342
|
-
const iconSrc = this.state.localIcon || this.props.fields.icon;
|
|
343
|
-
const icon = iconSrc ? (React.createElement(DeviceTypeIcon, { src: iconSrc, style: styles.imgStyle })) : (React.createElement(NoImageIcon, { style: styles.imgStyle }));
|
|
344
|
-
const headerStyle = this.getCardHeaderStyle(this.props.theme);
|
|
345
|
-
const title = this.state.details?.data?.name || this.props.device.name || '';
|
|
346
|
-
return (React.createElement(Paper, { style: this.props.fillContainer ? filledCardSize : smallCardSize, sx: styles.cardStyle },
|
|
347
|
-
React.createElement(Box, { sx: headerStyle, style: { ...styles.headerStyle, minHeight: 48 } },
|
|
348
|
-
React.createElement("div", { style: { ...styles.imgAreaStyle, height: 32, width: 32 } },
|
|
349
|
-
this.props.uploadImagesToInstance ? (React.createElement(DeviceImageUpload, { uploadImagesToInstance: this.props.uploadImagesToInstance, deviceId: this.props.device.id, manufacturer: this.props.fields.manufacturer, model: this.props.fields.model, onImageSelect: (imageData) => {
|
|
350
|
-
if (imageData) {
|
|
351
|
-
this.setState({ localIcon: imageData });
|
|
352
|
-
}
|
|
353
|
-
}, socket: this.props.socket })) : null,
|
|
354
|
-
icon),
|
|
355
|
-
React.createElement(Box, { style: { ...styles.titleStyle, fontSize: 14 }, title: title.length > 15 ? title : undefined, sx: theme => ({ color: headerStyle.color || theme.palette.secondary.contrastText }) }, this.state.details?.data?.name || this.props.fields.name),
|
|
356
|
-
this.props.fields.hasDetails ? (React.createElement(Fab, { disabled: !this.props.alive, size: "small", style: styles.detailsButtonStyle, onClick: () => {
|
|
357
|
-
if (!this.state.open) {
|
|
358
|
-
this.loadDetails().catch(console.error);
|
|
359
|
-
this.setState({ open: true });
|
|
360
|
-
}
|
|
361
|
-
}, color: "primary" },
|
|
362
|
-
React.createElement(MoreVertIcon, null))) : null),
|
|
363
|
-
React.createElement("div", { style: { ...styles.statusStyle, height: 'auto', padding: '8px 15px 0 15px' } },
|
|
364
|
-
status.map((s, i) => (React.createElement(DeviceStatusComponent, { key: i, socket: this.props.socket, deviceId: this.props.device.id, connectionType: this.props.fields.connectionType, status: s, enabled: this.props.fields.enabled, statusAction: this.props.device.actions?.find(a => a.id === ACTIONS.STATUS), disableEnableAction: this.props.device.actions?.find(a => a.id === ACTIONS.ENABLE_DISABLE), update: i === 0 ? this.props.device.update : undefined, updateAction: i === 0 ? this.props.device.actions?.find(a => a.id === ACTIONS.UPDATE) : undefined, batteryAction: this.props.device.actions?.find(a => a.id === ACTIONS.BATTERY), deviceHandler: this.props.deviceHandler, theme: this.props.theme, stateOrObjectHandler: this.props.stateOrObjectHandler }))),
|
|
365
|
-
this.renderIndicators(true)),
|
|
366
|
-
React.createElement("div", { style: styles.bodyStyle },
|
|
367
|
-
React.createElement(Typography, { variant: "body2", style: { ...styles.deviceInfoStyle, padding: '10px 10px 0 10px' } },
|
|
368
|
-
this.props.fields.identifier ? (React.createElement("div", { onClick: this.copyToClipboard, style: { textOverflow: 'ellipsis', overflow: 'hidden' } },
|
|
369
|
-
React.createElement("b", null,
|
|
370
|
-
getText(this.props.identifierLabel),
|
|
371
|
-
":"),
|
|
372
|
-
React.createElement("span", { style: { marginLeft: 4 } }, this.props.fields.identifier))) : null,
|
|
373
|
-
this.props.fields.manufacturer ? (React.createElement(Tooltip, { title: getTranslation('manufacturer'), slotProps: { popper: { sx: { pointerEvents: 'none' } } } },
|
|
374
|
-
React.createElement("div", null, this.props.fields.manufacturer))) : null,
|
|
375
|
-
this.props.fields.model ? (React.createElement(Tooltip, { title: getTranslation('model'), slotProps: { popper: { sx: { pointerEvents: 'none' } } } },
|
|
376
|
-
React.createElement("div", null, this.props.fields.model))) : null),
|
|
377
|
-
this.props.device.customInfo ? (React.createElement("div", { style: { padding: '0 10px 10px' } },
|
|
378
|
-
React.createElement(JsonConfig, { instanceId: this.props.instanceId, socket: this.props.socket, schema: this.props.device.customInfo.schema, data: this.props.device.customInfo.data || {}, onChange: (_data) => {
|
|
379
|
-
/* ignore */
|
|
380
|
-
}, themeName: this.props.themeName, themeType: this.props.themeType, theme: this.props.theme, isFloatComma: this.props.isFloatComma, dateFormat: this.props.dateFormat }))) : null,
|
|
381
|
-
!!(this.hasFooterActions() || this.props.device.controls?.length) && (React.createElement("div", { style: {
|
|
382
|
-
marginTop: 'auto',
|
|
383
|
-
display: 'flex',
|
|
384
|
-
gap: 4,
|
|
385
|
-
paddingBottom: 5,
|
|
386
|
-
minHeight: 34,
|
|
387
|
-
paddingLeft: 8,
|
|
388
|
-
paddingRight: 8,
|
|
389
|
-
} },
|
|
390
|
-
this.renderActions(),
|
|
391
|
-
React.createElement("div", { style: { flexGrow: 1 } }),
|
|
392
|
-
this.renderControls()))),
|
|
393
|
-
this.renderDialog(),
|
|
394
|
-
this.renderControlDialog()));
|
|
395
|
-
}
|
|
396
|
-
getCardHeaderStyle(theme, maxWidth) {
|
|
299
|
+
/** Color of the device symbol and of the glyph on it */
|
|
300
|
+
getAccent(theme) {
|
|
397
301
|
const backgroundColor = this.props.fields.backgroundColor === 'primary'
|
|
398
302
|
? theme.palette.primary.main
|
|
399
303
|
: this.props.fields.backgroundColor === 'secondary'
|
|
@@ -424,142 +328,133 @@ export default class DeviceCard extends PureComponent {
|
|
|
424
328
|
color = Utils.invertColor(backgroundColor, true);
|
|
425
329
|
}
|
|
426
330
|
}
|
|
427
|
-
return {
|
|
428
|
-
backgroundColor,
|
|
429
|
-
color,
|
|
430
|
-
maxWidth,
|
|
431
|
-
};
|
|
331
|
+
return { background: backgroundColor, color };
|
|
432
332
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
? []
|
|
436
|
-
: Array.isArray(this.props.device.status)
|
|
437
|
-
? this.props.device.status
|
|
438
|
-
: [this.props.device.status];
|
|
333
|
+
/** The device symbol with the LED and, if allowed, the image upload */
|
|
334
|
+
renderAvatar(name, small) {
|
|
439
335
|
const iconSrc = this.state.localIcon || this.props.fields.icon;
|
|
440
|
-
const
|
|
441
|
-
const
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
":"),
|
|
469
|
-
this.props.fields.identifier)) : null,
|
|
470
|
-
this.props.fields.manufacturer ? (React.createElement("div", null,
|
|
471
|
-
React.createElement("b", { style: { marginRight: 4 } },
|
|
472
|
-
getTranslation('manufacturer'),
|
|
473
|
-
":"),
|
|
474
|
-
this.props.fields.manufacturer)) : null,
|
|
475
|
-
this.props.fields.model ? (React.createElement("div", null,
|
|
476
|
-
React.createElement("b", { style: { marginRight: 4 } },
|
|
477
|
-
getTranslation('model'),
|
|
478
|
-
":"),
|
|
479
|
-
this.props.fields.model)) : null),
|
|
480
|
-
this.props.device.customInfo ? (React.createElement("div", { style: { padding: '0 16px 16px' } },
|
|
481
|
-
React.createElement(JsonConfig, { instanceId: this.props.instanceId, socket: this.props.socket, schema: this.props.device.customInfo.schema, data: this.props.device.customInfo.data || {}, onChange: (_data) => {
|
|
482
|
-
/* ignore */
|
|
483
|
-
}, themeName: this.props.themeName, themeType: this.props.themeType, theme: this.props.theme, isFloatComma: this.props.isFloatComma, dateFormat: this.props.dateFormat }))) : null,
|
|
484
|
-
!!(this.hasFooterActions() || this.props.device.controls?.length) && (React.createElement("div", { style: {
|
|
485
|
-
marginTop: 'auto',
|
|
486
|
-
display: 'flex',
|
|
487
|
-
gap: 8,
|
|
488
|
-
paddingBottom: 5,
|
|
489
|
-
minHeight: 34,
|
|
490
|
-
paddingLeft: 10,
|
|
491
|
-
paddingRight: 10,
|
|
492
|
-
} },
|
|
493
|
-
this.renderActions(),
|
|
494
|
-
React.createElement("div", { style: { flexGrow: 1 } }),
|
|
495
|
-
this.renderControls()))),
|
|
496
|
-
this.renderDialog(),
|
|
497
|
-
this.renderControlDialog()));
|
|
336
|
+
const picture = !!iconSrc && isPicture(iconSrc);
|
|
337
|
+
const accent = this.getAccent(this.props.theme);
|
|
338
|
+
const firstStatus = getStatusList(this.props.device)[0];
|
|
339
|
+
let glyph;
|
|
340
|
+
if (iconSrc) {
|
|
341
|
+
// The size is given inline, because `DeviceTypeIcon` sets an inline size of its own
|
|
342
|
+
const glyphSize = small ? 20 : 26;
|
|
343
|
+
glyph = (React.createElement(DeviceTypeIcon, { src: iconSrc, className: "dm-avatar-glyph", style: picture
|
|
344
|
+
? { width: '100%', height: '100%', objectFit: 'contain' }
|
|
345
|
+
: { width: glyphSize, height: glyphSize, color: accent.color } }));
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
// Without an icon, the initial of the name gives each device its own symbol
|
|
349
|
+
glyph = React.createElement("span", { className: "dm-avatar-letter" }, Array.from(name.trim())[0] || '?');
|
|
350
|
+
}
|
|
351
|
+
return (React.createElement("div", { className: picture ? 'dm-avatar dm-avatar-img' : 'dm-avatar', style: picture
|
|
352
|
+
? undefined
|
|
353
|
+
: {
|
|
354
|
+
'--dm-accent': accent.background,
|
|
355
|
+
'--dm-accent-text': accent.color,
|
|
356
|
+
} },
|
|
357
|
+
glyph,
|
|
358
|
+
this.props.uploadImagesToInstance ? (React.createElement(DeviceImageUpload, { uploadImagesToInstance: this.props.uploadImagesToInstance, deviceId: this.props.device.id, manufacturer: this.props.fields.manufacturer, model: this.props.fields.model, onImageSelect: (imageData) => {
|
|
359
|
+
if (imageData) {
|
|
360
|
+
this.setState({ localIcon: imageData });
|
|
361
|
+
}
|
|
362
|
+
}, socket: this.props.socket })) : null,
|
|
363
|
+
firstStatus ? (React.createElement(DeviceLed, { status: firstStatus, stateOrObjectHandler: this.props.stateOrObjectHandler, alive: this.props.alive, enabled: this.props.fields.enabled })) : null));
|
|
498
364
|
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
365
|
+
/** The switch that enables or disables the device */
|
|
366
|
+
renderEnabled(small) {
|
|
367
|
+
if (typeof this.props.fields.enabled !== 'boolean') {
|
|
368
|
+
return null;
|
|
502
369
|
}
|
|
503
|
-
|
|
370
|
+
const action = this.props.device.actions?.find(a => a.id === ACTIONS.ENABLE_DISABLE);
|
|
371
|
+
const tooltip = this.props.fields.enabled
|
|
372
|
+
? getTranslation('disableIconTooltip')
|
|
373
|
+
: getTranslation('enableIconTooltip');
|
|
374
|
+
return (React.createElement("div", { className: "dm-status-end" },
|
|
375
|
+
React.createElement(Tooltip, { title: tooltip, slotProps: tooltipSlotProps },
|
|
376
|
+
React.createElement("div", null,
|
|
377
|
+
React.createElement(Switch, { size: small ? 'small' : undefined, checked: this.props.fields.enabled, disabled: !action || !this.props.alive, ariaLabel: tooltip, onChange: () => action && this.props.deviceHandler(this.props.device.id, action)(), theme: this.props.theme })))));
|
|
504
378
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
if (
|
|
509
|
-
return
|
|
379
|
+
/** Identifier, manufacturer and model as a table of labels and values */
|
|
380
|
+
renderFacts() {
|
|
381
|
+
const { identifier, manufacturer, model } = this.props.fields;
|
|
382
|
+
if (!identifier && !manufacturer && !model) {
|
|
383
|
+
return null;
|
|
510
384
|
}
|
|
511
|
-
return
|
|
385
|
+
return (React.createElement("div", { className: "dm-facts" },
|
|
386
|
+
identifier ? (React.createElement(React.Fragment, null,
|
|
387
|
+
React.createElement("span", { className: "dm-fact-label" }, getText(this.props.identifierLabel)),
|
|
388
|
+
React.createElement("span", { className: "dm-fact-copy" },
|
|
389
|
+
React.createElement("span", { title: identifier }, identifier),
|
|
390
|
+
React.createElement(Tooltip, { title: getTranslation('ctcButtonText'), slotProps: tooltipSlotProps },
|
|
391
|
+
React.createElement(IconButton, { size: "small", className: "dm-copy-btn", "aria-label": getTranslation('ctcButtonText'), onClick: this.copyToClipboard },
|
|
392
|
+
React.createElement(IconCopyToClipboard, null)))))) : null,
|
|
393
|
+
manufacturer ? (React.createElement(React.Fragment, null,
|
|
394
|
+
React.createElement("span", { className: "dm-fact-label" }, getTranslation('manufacturer')),
|
|
395
|
+
React.createElement("span", { className: "dm-fact-value", title: manufacturer }, manufacturer))) : null,
|
|
396
|
+
model ? (React.createElement(React.Fragment, null,
|
|
397
|
+
React.createElement("span", { className: "dm-fact-label" }, getTranslation('model')),
|
|
398
|
+
React.createElement("span", { className: "dm-fact-value", title: model }, model))) : null));
|
|
512
399
|
}
|
|
513
|
-
|
|
514
|
-
const
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
React.createElement("div", {
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
React.createElement("
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
400
|
+
render() {
|
|
401
|
+
const small = !!this.props.smallCards;
|
|
402
|
+
const status = getStatusList(this.props.device);
|
|
403
|
+
const name = this.state.details?.data?.name || this.props.fields.name || '';
|
|
404
|
+
const off = this.props.fields.enabled === false;
|
|
405
|
+
let className = small ? 'dm-card dm-small' : 'dm-card';
|
|
406
|
+
if (off) {
|
|
407
|
+
className += ' dm-off';
|
|
408
|
+
}
|
|
409
|
+
const size = this.props.fillContainer ? filledCardSize : small ? smallCardSize : cardSize;
|
|
410
|
+
return (React.createElement("div", { className: className, style: { ...getThemeVars(this.props.theme), ...size } },
|
|
411
|
+
React.createElement("div", { className: "dm-head" },
|
|
412
|
+
this.renderAvatar(name, small),
|
|
413
|
+
React.createElement("div", { className: "dm-name", title: name.length > (small ? 15 : 20) ? name : undefined }, name),
|
|
414
|
+
this.props.fields.hasDetails ? (React.createElement("div", { className: "dm-head-end" },
|
|
415
|
+
React.createElement(IconButton, { size: "small", disabled: !this.props.alive, "aria-label": getTranslation('moreInformation'), onClick: () => {
|
|
416
|
+
if (!this.state.open) {
|
|
417
|
+
this.loadDetails().catch(console.error);
|
|
418
|
+
this.setState({ open: true });
|
|
419
|
+
}
|
|
420
|
+
} },
|
|
421
|
+
React.createElement(MoreVertIcon, { className: "dm-ani dm-ani-turn" })))) : null),
|
|
422
|
+
React.createElement("div", { className: "dm-status" },
|
|
423
|
+
status.map((s, i) => (React.createElement(DeviceStatusComponent, { key: i, socket: this.props.socket, deviceId: this.props.device.id, connectionType: this.props.fields.connectionType, status: s, statusAction: this.props.device.actions?.find(a => a.id === ACTIONS.STATUS), update: i === 0 ? this.props.device.update : undefined, updateAction: i === 0 ? this.props.device.actions?.find(a => a.id === ACTIONS.UPDATE) : undefined, batteryAction: this.props.device.actions?.find(a => a.id === ACTIONS.BATTERY), deviceHandler: this.props.deviceHandler, theme: this.props.theme, stateOrObjectHandler: this.props.stateOrObjectHandler }))),
|
|
424
|
+
this.renderIndicators(),
|
|
425
|
+
this.renderEnabled(small)),
|
|
426
|
+
this.renderFacts(),
|
|
427
|
+
this.props.device.customInfo ? (React.createElement("div", { className: "dm-custom" },
|
|
428
|
+
React.createElement(JsonConfig, { instanceId: this.props.instanceId, socket: this.props.socket, schema: this.props.device.customInfo.schema, data: this.props.device.customInfo.data || {}, onChange: (_data) => {
|
|
429
|
+
/* ignore */
|
|
430
|
+
}, themeName: this.props.themeName, themeType: this.props.themeType, theme: this.props.theme, isFloatComma: this.props.isFloatComma, dateFormat: this.props.dateFormat }))) : null,
|
|
431
|
+
this.hasFooterActions() || this.props.device.controls?.length ? (React.createElement("div", { className: "dm-foot" },
|
|
432
|
+
this.renderActions(),
|
|
433
|
+
React.createElement("div", { className: "dm-foot-end" }, this.renderControls()))) : null,
|
|
434
|
+
this.renderDialog(),
|
|
435
|
+
this.renderControlDialog()));
|
|
532
436
|
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
React.createElement(Box, { style: styles.titleStyle, sx: theme => ({
|
|
540
|
-
color: headerStyle.color || theme.palette.secondary.contrastText,
|
|
541
|
-
minWidth: '50%',
|
|
542
|
-
}) },
|
|
543
|
-
React.createElement(Skeleton, null))),
|
|
544
|
-
React.createElement("div", { style: styles.statusStyle }),
|
|
545
|
-
React.createElement("div", { style: styles.bodyStyle },
|
|
546
|
-
React.createElement(Typography, { variant: "body1", style: styles.deviceInfoStyle },
|
|
547
|
-
React.createElement("div", null,
|
|
548
|
-
React.createElement(Skeleton, null)),
|
|
549
|
-
React.createElement("div", null,
|
|
550
|
-
React.createElement(Skeleton, null)),
|
|
551
|
-
React.createElement("div", null,
|
|
552
|
-
React.createElement(Skeleton, null))))));
|
|
437
|
+
}
|
|
438
|
+
/** Placeholder of a device that is not loaded yet, with the layout of the card */
|
|
439
|
+
export class DeviceCardSkeleton extends PureComponent {
|
|
440
|
+
constructor(props) {
|
|
441
|
+
super(props);
|
|
442
|
+
ensureDmStyles();
|
|
553
443
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
444
|
+
render() {
|
|
445
|
+
const small = !!this.props.smallCards;
|
|
446
|
+
return (React.createElement("div", { className: small ? 'dm-card dm-small dm-skeleton' : 'dm-card dm-skeleton', style: { ...getThemeVars(this.props.theme), ...(small ? smallCardSize : cardSize) } },
|
|
447
|
+
React.createElement("div", { className: "dm-head" },
|
|
448
|
+
React.createElement("div", { className: "dm-avatar" }),
|
|
449
|
+
React.createElement("div", { className: "dm-name" },
|
|
450
|
+
React.createElement(Skeleton, { width: "70%" }))),
|
|
451
|
+
React.createElement("div", { className: "dm-status" },
|
|
452
|
+
React.createElement(Skeleton, { variant: "rounded", width: small ? 120 : 150, height: 20 })),
|
|
453
|
+
React.createElement("div", { className: "dm-facts" }, [0, 1, 2].map(i => (React.createElement(React.Fragment, { key: i },
|
|
454
|
+
React.createElement(Skeleton, { width: small ? 42 : 56 }),
|
|
455
|
+
React.createElement(Skeleton, { width: `${70 - i * 12}%` }))))),
|
|
456
|
+
React.createElement("div", { className: "dm-foot" },
|
|
457
|
+
React.createElement(Skeleton, { variant: "rounded", width: small ? 110 : 150, height: 20 }))));
|
|
563
458
|
}
|
|
564
459
|
}
|
|
565
460
|
//# sourceMappingURL=DeviceCard.js.map
|