@iobroker/dm-gui-components 10.0.20 → 10.0.21
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 +7 -0
- package/build/DeviceCard.d.ts +46 -45
- package/build/DeviceCard.js +130 -163
- package/build/DeviceCard.js.map +1 -1
- package/build/DeviceFields.d.ts +75 -0
- package/build/DeviceFields.js +190 -0
- package/build/DeviceFields.js.map +1 -0
- package/build/DeviceList.d.ts +59 -7
- package/build/DeviceList.js +231 -75
- package/build/DeviceList.js.map +1 -1
- package/build/LazyRender.d.ts +69 -0
- package/build/LazyRender.js +128 -0
- package/build/LazyRender.js.map +1 -0
- package/build/StateOrObjectHandler.d.ts +3 -0
- package/build/StateOrObjectHandler.js +57 -7
- package/build/StateOrObjectHandler.js.map +1 -1
- package/build/Utils.d.ts +7 -0
- package/build/Utils.js +13 -0
- package/build/Utils.js.map +1 -1
- package/build/hooks.js +22 -3
- package/build/hooks.js.map +1 -1
- package/package.json +2 -2
package/build/DeviceList.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { IconButton, InputAdornment, TextField, Toolbar, Tooltip, LinearProgress, Select, MenuItem,
|
|
2
|
+
import { IconButton, InputAdornment, TextField, Toolbar, Tooltip, LinearProgress, Select, MenuItem, Card, CardActionArea, CardContent, Typography, Menu, Checkbox, ListSubheader, } from '@mui/material';
|
|
3
3
|
import { ArrowBack, Clear, QuestionMark, Refresh, FilterAlt, FilterAltOff, SystemUpdateAlt, BatteryAlert, Tune, } from '@mui/icons-material';
|
|
4
4
|
import { I18n, DeviceTypeIcon, Icon, InfoBox } from '@iobroker/gui-components';
|
|
5
|
-
import DeviceCard, { DeviceCardSkeleton } from './DeviceCard';
|
|
5
|
+
import DeviceCard, { DeviceCardSkeleton, CARD_WIDTH, CARD_MIN_HEIGHT, CARD_MARGIN, SMALL_CARD_WIDTH, SMALL_CARD_MIN_HEIGHT, SMALL_CARD_MARGIN, } from './DeviceCard';
|
|
6
6
|
import { getTranslation, renderIcon } from './Utils';
|
|
7
7
|
import Communication from './Communication';
|
|
8
8
|
import InstanceActionButton from './InstanceActionButton';
|
|
9
9
|
import { StatusIndicators } from './StatusIndicator';
|
|
10
10
|
import { StateOrObjectHandler } from './StateOrObjectHandler';
|
|
11
|
+
import { DeviceFieldsResolver } from './DeviceFields';
|
|
12
|
+
import { LazyRender, LazyRenderObserver } from './LazyRender';
|
|
11
13
|
import de from './i18n/de.json';
|
|
12
14
|
import en from './i18n/en.json';
|
|
13
15
|
import ru from './i18n/ru.json';
|
|
@@ -19,6 +21,25 @@ import es from './i18n/es.json';
|
|
|
19
21
|
import pl from './i18n/pl.json';
|
|
20
22
|
import uk from './i18n/uk.json';
|
|
21
23
|
import zhCn from './i18n/zh-cn.json';
|
|
24
|
+
/**
|
|
25
|
+
* The key of a device. It is used as React key, for the resolved fields and for the filter, so it is
|
|
26
|
+
* computed on every render for every device. `JSON.stringify` adds up noticeably with a few hundred
|
|
27
|
+
* devices, therefore the result is cached per device object.
|
|
28
|
+
*/
|
|
29
|
+
const deviceKeys = new WeakMap();
|
|
30
|
+
function deviceKey(device) {
|
|
31
|
+
let key = deviceKeys.get(device);
|
|
32
|
+
if (key === undefined) {
|
|
33
|
+
key = JSON.stringify(device.id);
|
|
34
|
+
deviceKeys.set(device, key);
|
|
35
|
+
}
|
|
36
|
+
return key;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* How many skeletons are shown at most while loading. They stand for devices that are not there yet,
|
|
40
|
+
* and a few hundred pulsing placeholders cost more than the cards they are waiting for.
|
|
41
|
+
*/
|
|
42
|
+
const MAX_SKELETONS = 12;
|
|
22
43
|
/** Returns true if any of the device status objects carries a battery value */
|
|
23
44
|
function hasBatteryStatus(status) {
|
|
24
45
|
if (!status || typeof status === 'string') {
|
|
@@ -43,14 +64,32 @@ export default class DeviceList extends Communication {
|
|
|
43
64
|
alive = null;
|
|
44
65
|
lastTriggerLoad = 0;
|
|
45
66
|
filterTimeout = null;
|
|
46
|
-
/** Resolved model value per device (stringified id -> model), reported by the cards to build the model dropdown */
|
|
47
|
-
modelValues = new Map();
|
|
48
67
|
language = I18n.getLanguage();
|
|
49
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* One handler for the toolbar indicators and for all cards, so that several devices referring to
|
|
70
|
+
* the same object or state share a single subscription on the socket.
|
|
71
|
+
*/
|
|
50
72
|
stateOrObjectHandler;
|
|
73
|
+
/** Resolves the device fields that may be bound to a state or an object (name, model, ...) */
|
|
74
|
+
fieldsResolver;
|
|
75
|
+
/** One `IntersectionObserver` for all cards: only what is near the viewport is really rendered */
|
|
76
|
+
lazyObserver = new LazyRenderObserver();
|
|
77
|
+
/** The scrolling container of the card list; it is the root of `lazyObserver` */
|
|
78
|
+
containerRef = React.createRef();
|
|
79
|
+
/** A load is running. A second one must not tear down the same list in parallel */
|
|
80
|
+
loadingDevices = false;
|
|
81
|
+
/** A load was requested while another one was still running */
|
|
82
|
+
reloadRequested = false;
|
|
83
|
+
/** Devices of the running initial load that are not published to the state yet */
|
|
84
|
+
pendingDevices = null;
|
|
85
|
+
pendingTotal;
|
|
86
|
+
flushTimer = null;
|
|
87
|
+
/** Memoized result of `getHiddenIndicators`, so that all cards keep getting the same array */
|
|
88
|
+
hiddenIndicatorsCache = null;
|
|
51
89
|
constructor(props) {
|
|
52
90
|
super(props);
|
|
53
91
|
this.stateOrObjectHandler = new StateOrObjectHandler(this.props.socket);
|
|
92
|
+
this.fieldsResolver = new DeviceFieldsResolver(this.stateOrObjectHandler, this.onFieldsChanged);
|
|
54
93
|
if (!DeviceList.i18nInitialized) {
|
|
55
94
|
DeviceList.i18nInitialized = true;
|
|
56
95
|
I18n.extendTranslations({
|
|
@@ -84,6 +123,7 @@ export default class DeviceList extends Communication {
|
|
|
84
123
|
modelOptions: [],
|
|
85
124
|
indicatorVisibility: {},
|
|
86
125
|
indicatorsAnchor: null,
|
|
126
|
+
fieldsVersion: 0,
|
|
87
127
|
};
|
|
88
128
|
if (this.props.selectedInstance === undefined) {
|
|
89
129
|
// Start with the root page that shows all instances as cards
|
|
@@ -153,9 +193,8 @@ export default class DeviceList extends Communication {
|
|
|
153
193
|
window.localStorage.removeItem('dmSelectedInstance');
|
|
154
194
|
this.props.onInstanceChanged?.('');
|
|
155
195
|
this.alive = null;
|
|
156
|
-
this.
|
|
196
|
+
this.applyDevices([], {
|
|
157
197
|
selectedInstance: '',
|
|
158
|
-
devices: [],
|
|
159
198
|
totalDevices: undefined,
|
|
160
199
|
instanceInfo: null,
|
|
161
200
|
alive: null,
|
|
@@ -169,6 +208,7 @@ export default class DeviceList extends Communication {
|
|
|
169
208
|
this.setState({ dmInstances: null }, () => this.loadAdapters().catch(console.error));
|
|
170
209
|
}
|
|
171
210
|
async componentDidMount() {
|
|
211
|
+
this.lazyObserver.setRoot(this.containerRef.current);
|
|
172
212
|
let alive = false;
|
|
173
213
|
// If an instance selector must be shown
|
|
174
214
|
if (this.props.selectedInstance === undefined) {
|
|
@@ -207,10 +247,98 @@ export default class DeviceList extends Communication {
|
|
|
207
247
|
}
|
|
208
248
|
}
|
|
209
249
|
componentWillUnmount() {
|
|
250
|
+
super.componentWillUnmount();
|
|
251
|
+
this.cancelDeviceFlush();
|
|
252
|
+
this.fieldsResolver.destroy();
|
|
253
|
+
this.stateOrObjectHandler.destroy();
|
|
254
|
+
this.lazyObserver.destroy();
|
|
255
|
+
if (this.filterTimeout) {
|
|
256
|
+
clearTimeout(this.filterTimeout);
|
|
257
|
+
this.filterTimeout = null;
|
|
258
|
+
}
|
|
210
259
|
if (this.state.selectedInstance) {
|
|
211
260
|
this.props.socket.unsubscribeState(`system.adapter.${this.state.selectedInstance}.alive`, this.aliveHandler);
|
|
212
261
|
}
|
|
213
262
|
}
|
|
263
|
+
/**
|
|
264
|
+
* The side effects that used to be triggered from `render()`. A render must not have any, and
|
|
265
|
+
* the `setTimeout` there was executed on every single render.
|
|
266
|
+
*/
|
|
267
|
+
componentDidUpdate() {
|
|
268
|
+
// The scrolling container only exists after the first render
|
|
269
|
+
this.lazyObserver.setRoot(this.containerRef.current);
|
|
270
|
+
if ((this.props.triggerLoad || 0) !== this.lastTriggerLoad) {
|
|
271
|
+
this.lastTriggerLoad = this.props.triggerLoad || 0;
|
|
272
|
+
this.loadDeviceList();
|
|
273
|
+
}
|
|
274
|
+
// if instance changed
|
|
275
|
+
if (this.lastInstance !== this.state.selectedInstance) {
|
|
276
|
+
this.lastInstance = this.state.selectedInstance;
|
|
277
|
+
// The indicator visibility is stored per instance
|
|
278
|
+
this.setState({
|
|
279
|
+
indicatorVisibility: DeviceList.loadIndicatorVisibility(this.state.selectedInstance),
|
|
280
|
+
indicatorsAnchor: null,
|
|
281
|
+
});
|
|
282
|
+
if (this.state.selectedInstance) {
|
|
283
|
+
this.loadAllData().catch(error => console.error(error));
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
this.loadDeviceList();
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (this.props.selectedInstance && this.props.selectedInstance !== this.state.selectedInstance) {
|
|
290
|
+
this.setState({ selectedInstance: this.props.selectedInstance });
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* A value bound to a state or an object arrived. The cards are `PureComponent`s and receive the
|
|
295
|
+
* values through their `fields` property, so the list only has to trigger a re-render.
|
|
296
|
+
*/
|
|
297
|
+
onFieldsChanged = () => {
|
|
298
|
+
const modelOptions = this.fieldsResolver.getModels();
|
|
299
|
+
const modelsChanged = modelOptions.length !== this.state.modelOptions.length ||
|
|
300
|
+
modelOptions.some((model, i) => model !== this.state.modelOptions[i]);
|
|
301
|
+
this.setState({
|
|
302
|
+
fieldsVersion: this.state.fieldsVersion + 1,
|
|
303
|
+
modelOptions: modelsChanged ? modelOptions : this.state.modelOptions,
|
|
304
|
+
});
|
|
305
|
+
};
|
|
306
|
+
/**
|
|
307
|
+
* Publish a new device list.
|
|
308
|
+
*
|
|
309
|
+
* The resolver is updated *before* the state, because it resolves literal values synchronously -
|
|
310
|
+
* the cards and the filter therefore already have their values on the very first render.
|
|
311
|
+
*/
|
|
312
|
+
applyDevices(devices, state) {
|
|
313
|
+
this.fieldsResolver.setDevices(devices.map(device => ({ key: deviceKey(device), device })));
|
|
314
|
+
this.setState({ ...state, devices });
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Publish the devices loaded so far, but at most every 100 ms: a fast backend delivers a dozen
|
|
318
|
+
* batches in a row, and every single one of them would otherwise re-render the complete list.
|
|
319
|
+
*/
|
|
320
|
+
scheduleDeviceFlush(devices, total) {
|
|
321
|
+
this.pendingDevices = devices;
|
|
322
|
+
this.pendingTotal = total;
|
|
323
|
+
if (this.flushTimer) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
this.flushTimer = setTimeout(() => {
|
|
327
|
+
this.flushTimer = null;
|
|
328
|
+
const pending = this.pendingDevices;
|
|
329
|
+
if (pending) {
|
|
330
|
+
this.pendingDevices = null;
|
|
331
|
+
this.applyDevices(pending, { loading: true, totalDevices: this.pendingTotal });
|
|
332
|
+
}
|
|
333
|
+
}, 100);
|
|
334
|
+
}
|
|
335
|
+
cancelDeviceFlush() {
|
|
336
|
+
if (this.flushTimer) {
|
|
337
|
+
clearTimeout(this.flushTimer);
|
|
338
|
+
this.flushTimer = null;
|
|
339
|
+
}
|
|
340
|
+
this.pendingDevices = null;
|
|
341
|
+
}
|
|
214
342
|
aliveHandler = (id, state) => {
|
|
215
343
|
if (this.state.selectedInstance && id === `system.adapter.${this.state.selectedInstance}.alive`) {
|
|
216
344
|
const alive = !!state?.val;
|
|
@@ -237,9 +365,20 @@ export default class DeviceList extends Communication {
|
|
|
237
365
|
return new Promise(resolve => this.setState({ instanceInfo, apiVersionError: !['v1', 'v2', 'v3'].includes(instanceInfo.apiVersion) }, () => resolve(instanceInfo)));
|
|
238
366
|
}
|
|
239
367
|
/**
|
|
240
|
-
* Load devices
|
|
368
|
+
* Load devices.
|
|
369
|
+
*
|
|
370
|
+
* The list is deliberately *not* emptied first: as long as the keys stay the same React keeps the
|
|
371
|
+
* existing cards, so a reload - the matter adapter triggers one on every `updateController`
|
|
372
|
+
* message - no longer unmounts and remounts a few hundred cards with all their subscriptions.
|
|
241
373
|
*/
|
|
242
374
|
loadDeviceList() {
|
|
375
|
+
if (this.loadingDevices) {
|
|
376
|
+
// Remember the request and repeat the load once the running one has finished, instead of
|
|
377
|
+
// running two loads over the same list at the same time.
|
|
378
|
+
this.reloadRequested = true;
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
this.loadingDevices = true;
|
|
243
382
|
this.setState({ loading: true }, async () => {
|
|
244
383
|
console.log(`Loading devices for ${this.state.selectedInstance}...`);
|
|
245
384
|
let alive = this.alive;
|
|
@@ -268,14 +407,20 @@ export default class DeviceList extends Communication {
|
|
|
268
407
|
alive = false;
|
|
269
408
|
}
|
|
270
409
|
}
|
|
410
|
+
// Only the very first load shows the devices step by step (with skeletons for the ones
|
|
411
|
+
// that are still missing). A reload keeps the current list until the new one is complete,
|
|
412
|
+
// so the already rendered cards survive.
|
|
413
|
+
const incremental = !this.state.devices.length;
|
|
271
414
|
let devices = [];
|
|
272
415
|
try {
|
|
273
416
|
this.alive = alive;
|
|
274
|
-
this.setState({
|
|
417
|
+
this.setState({ loading: !!alive, alive });
|
|
275
418
|
if (alive) {
|
|
276
419
|
await this.loadDevices((batch, total) => {
|
|
277
420
|
devices = devices.concat(batch);
|
|
278
|
-
|
|
421
|
+
if (incremental) {
|
|
422
|
+
this.scheduleDeviceFlush(devices, total);
|
|
423
|
+
}
|
|
279
424
|
console.log(`Loaded ${devices.length} of ${total} devices...`);
|
|
280
425
|
});
|
|
281
426
|
}
|
|
@@ -284,21 +429,27 @@ export default class DeviceList extends Communication {
|
|
|
284
429
|
console.error(error);
|
|
285
430
|
devices = [];
|
|
286
431
|
}
|
|
287
|
-
this.
|
|
432
|
+
this.cancelDeviceFlush();
|
|
433
|
+
this.applyDevices(devices, { loading: false, totalDevices: devices.length });
|
|
288
434
|
console.log(`Loaded ${devices.length} devices for ${this.state.selectedInstance}`);
|
|
435
|
+
this.loadingDevices = false;
|
|
436
|
+
if (this.reloadRequested) {
|
|
437
|
+
this.reloadRequested = false;
|
|
438
|
+
setTimeout(() => this.loadDeviceList(), 0);
|
|
439
|
+
}
|
|
289
440
|
});
|
|
290
441
|
}
|
|
291
442
|
updateDevice(update) {
|
|
292
443
|
const updateId = JSON.stringify(update.id);
|
|
293
|
-
this.
|
|
444
|
+
this.applyDevices(this.state.devices.map(d => (deviceKey(d) === updateId ? update : d)), {});
|
|
294
445
|
}
|
|
295
446
|
deleteDevice(deviceId) {
|
|
296
447
|
const deleteId = JSON.stringify(deviceId);
|
|
297
|
-
const devices = this.state.devices.filter(d =>
|
|
448
|
+
const devices = this.state.devices.filter(d => deviceKey(d) !== deleteId);
|
|
298
449
|
const totalDevices = this.state.totalDevices && devices.length < this.state.devices.length
|
|
299
450
|
? this.state.totalDevices - 1
|
|
300
451
|
: undefined;
|
|
301
|
-
this.
|
|
452
|
+
this.applyDevices(devices, { totalDevices });
|
|
302
453
|
}
|
|
303
454
|
getText(text) {
|
|
304
455
|
if (typeof text === 'object') {
|
|
@@ -388,27 +539,38 @@ export default class DeviceList extends Communication {
|
|
|
388
539
|
info.title ? (React.createElement(Typography, { variant: "body2", color: "textSecondary", style: { textAlign: 'center' } }, info.title)) : null))));
|
|
389
540
|
});
|
|
390
541
|
}
|
|
391
|
-
/**
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
542
|
+
/**
|
|
543
|
+
* Apply the text filter and the "only updatable" / "only battery problem" filters.
|
|
544
|
+
*
|
|
545
|
+
* This used to be done by every card for itself. A filtered out card stayed mounted with all its
|
|
546
|
+
* subscriptions and only rendered nothing, so filtering did not reduce the load at all - and a
|
|
547
|
+
* card that is not rendered cannot filter itself in the first place.
|
|
548
|
+
*/
|
|
549
|
+
filterDevices(devices) {
|
|
550
|
+
const filter = (this.props.embedded ? this.props.filter : this.state.filter)?.toLowerCase();
|
|
551
|
+
const field = this.props.embedded ? 'name' : this.getEffectiveFilterField();
|
|
552
|
+
const onlyUpdatable = !this.props.embedded && this.state.onlyUpdatable;
|
|
553
|
+
const onlyBatteryProblem = !this.props.embedded && this.state.onlyBatteryProblem;
|
|
554
|
+
if (!filter && !onlyUpdatable && !onlyBatteryProblem) {
|
|
555
|
+
return devices;
|
|
556
|
+
}
|
|
557
|
+
return devices.filter(device => {
|
|
558
|
+
const fields = this.fieldsResolver.getValues(deviceKey(device));
|
|
559
|
+
if (filter &&
|
|
560
|
+
!String(fields[field] ?? '')
|
|
561
|
+
.toLowerCase()
|
|
562
|
+
.includes(filter)) {
|
|
563
|
+
return false;
|
|
397
564
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
return;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
if (modelOptions.length !== this.state.modelOptions.length ||
|
|
408
|
-
modelOptions.some((model, i) => model !== this.state.modelOptions[i])) {
|
|
409
|
-
this.setState({ modelOptions });
|
|
410
|
-
}
|
|
411
|
-
};
|
|
565
|
+
if (onlyUpdatable && !fields.updateAvailable) {
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
if (onlyBatteryProblem && !fields.batteryProblem) {
|
|
569
|
+
return false;
|
|
570
|
+
}
|
|
571
|
+
return true;
|
|
572
|
+
});
|
|
573
|
+
}
|
|
412
574
|
/** The selected filter field, falling back to `name` if the stored field is not available (e.g. no models found) */
|
|
413
575
|
getEffectiveFilterField() {
|
|
414
576
|
const field = this.state.filterField;
|
|
@@ -529,9 +691,16 @@ export default class DeviceList extends Communication {
|
|
|
529
691
|
}
|
|
530
692
|
/** IDs of the configurable indicators the user has switched off */
|
|
531
693
|
getHiddenIndicators() {
|
|
532
|
-
|
|
694
|
+
const hidden = this.getConfigurableIndicators()
|
|
533
695
|
.filter(indicator => !this.isIndicatorVisible(indicator))
|
|
534
696
|
.map(indicator => indicator.id);
|
|
697
|
+
// The array is handed to every card. A new array on every render would defeat the shallow
|
|
698
|
+
// property comparison of `DeviceCard`.
|
|
699
|
+
const key = hidden.join(',');
|
|
700
|
+
if (!this.hiddenIndicatorsCache || this.hiddenIndicatorsCache.key !== key) {
|
|
701
|
+
this.hiddenIndicatorsCache = { key, value: hidden };
|
|
702
|
+
}
|
|
703
|
+
return this.hiddenIndicatorsCache.value;
|
|
535
704
|
}
|
|
536
705
|
toggleIndicator(indicator) {
|
|
537
706
|
const indicatorVisibility = {
|
|
@@ -585,35 +754,6 @@ export default class DeviceList extends Communication {
|
|
|
585
754
|
const emptyStyle = {
|
|
586
755
|
padding: 25,
|
|
587
756
|
};
|
|
588
|
-
if ((this.props.triggerLoad || 0) !== this.lastTriggerLoad) {
|
|
589
|
-
this.lastTriggerLoad = this.props.triggerLoad || 0;
|
|
590
|
-
setTimeout(() => this.loadDeviceList(), 50);
|
|
591
|
-
}
|
|
592
|
-
// if instance changed
|
|
593
|
-
if (this.lastInstance !== this.state.selectedInstance) {
|
|
594
|
-
this.lastInstance = this.state.selectedInstance;
|
|
595
|
-
setTimeout(async () => {
|
|
596
|
-
// The indicator visibility is stored per instance
|
|
597
|
-
this.setState({
|
|
598
|
-
indicatorVisibility: DeviceList.loadIndicatorVisibility(this.state.selectedInstance),
|
|
599
|
-
indicatorsAnchor: null,
|
|
600
|
-
});
|
|
601
|
-
if (this.state.selectedInstance) {
|
|
602
|
-
try {
|
|
603
|
-
await this.loadAllData();
|
|
604
|
-
}
|
|
605
|
-
catch (error) {
|
|
606
|
-
console.error(error);
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
else {
|
|
610
|
-
this.loadDeviceList();
|
|
611
|
-
}
|
|
612
|
-
}, 50);
|
|
613
|
-
}
|
|
614
|
-
if (this.props.selectedInstance && this.props.selectedInstance !== this.state.selectedInstance) {
|
|
615
|
-
setTimeout(() => this.setState({ selectedInstance: this.props.selectedInstance }), 50);
|
|
616
|
-
}
|
|
617
757
|
const deviceGroups = [];
|
|
618
758
|
const showRootPage = !this.props.embedded && this.props.selectedInstance === undefined && !this.state.selectedInstance;
|
|
619
759
|
let list;
|
|
@@ -680,21 +820,37 @@ export default class DeviceList extends Communication {
|
|
|
680
820
|
}
|
|
681
821
|
}
|
|
682
822
|
if (this.state.selectedInstance) {
|
|
823
|
+
const selectedInstance = this.state.selectedInstance;
|
|
683
824
|
const hiddenIndicators = this.getHiddenIndicators();
|
|
684
|
-
|
|
825
|
+
const smallCards = this.props.smallCards ?? this.state.instanceInfo?.smallCards;
|
|
826
|
+
const identifierLabel = this.state.instanceInfo?.identifierLabel ?? 'ID';
|
|
827
|
+
const cardWidth = smallCards ? SMALL_CARD_WIDTH : CARD_WIDTH;
|
|
828
|
+
const cardMinHeight = smallCards ? SMALL_CARD_MIN_HEIGHT : CARD_MIN_HEIGHT;
|
|
829
|
+
const cardMargin = smallCards ? SMALL_CARD_MARGIN : CARD_MARGIN;
|
|
830
|
+
const visibleDevices = this.filterDevices(filteredDevices);
|
|
831
|
+
// Every card is wrapped in a `LazyRender`: the placeholder always occupies the full
|
|
832
|
+
// footprint of a card, but the card itself is only built while it is near the visible
|
|
833
|
+
// area. Without that, 80 devices mean thousands of DOM nodes and several hundred MUI
|
|
834
|
+
// tooltips and buttons at once - which is what makes Safari (and especially iPadOS)
|
|
835
|
+
// hang and finally kill the tab.
|
|
836
|
+
list = visibleDevices.map(device => {
|
|
837
|
+
const key = deviceKey(device);
|
|
838
|
+
return (React.createElement(LazyRender, { key: key, observer: this.lazyObserver, width: cardWidth, minHeight: cardMinHeight, margin: cardMargin }, () => (React.createElement(DeviceCard, { fillContainer: true, smallCards: smallCards, alive: !!this.state.alive, id: device.id, identifierLabel: identifierLabel, device: device, fields: this.fieldsResolver.getValues(key), stateOrObjectHandler: this.stateOrObjectHandler, instanceId: selectedInstance, uploadImagesToInstance: this.props.uploadImagesToInstance, deviceHandler: this.deviceHandler, controlHandler: this.controlHandler, controlStateHandler: this.controlStateHandler, socket: this.props.socket, themeName: this.props.themeName, themeType: this.props.themeType, theme: this.props.theme, isFloatComma: this.props.isFloatComma, dateFormat: this.props.dateFormat, hiddenIndicators: hiddenIndicators }))));
|
|
839
|
+
});
|
|
685
840
|
if (this.state.loading) {
|
|
686
|
-
|
|
841
|
+
// How many devices are still missing - deliberately not derived from `list`,
|
|
842
|
+
// which only contains the devices that pass the filter
|
|
843
|
+
const loaded = this.state.devices.length;
|
|
844
|
+
const missing = (this.state.totalDevices ?? loaded + 1) - loaded;
|
|
845
|
+
const skeletons = Math.min(missing, MAX_SKELETONS);
|
|
687
846
|
for (let i = 0; i < skeletons; i++) {
|
|
688
|
-
list.push(React.createElement(DeviceCardSkeleton, { key: `skeleton-${i}`, smallCards:
|
|
847
|
+
list.push(React.createElement(DeviceCardSkeleton, { key: `skeleton-${i}`, smallCards: smallCards, theme: this.props.theme }));
|
|
689
848
|
}
|
|
690
849
|
}
|
|
691
|
-
else if (this.state.devices.length > 0) {
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
display: 'none',
|
|
696
|
-
},
|
|
697
|
-
} },
|
|
850
|
+
else if (!visibleDevices.length && this.state.devices.length > 0) {
|
|
851
|
+
// The filter is applied here now, so the list knows that nothing is left instead
|
|
852
|
+
// of having to hide the message with a CSS rule
|
|
853
|
+
list.push(React.createElement("div", { style: emptyStyle, key: "filtered" },
|
|
698
854
|
React.createElement("span", null, getTranslation('allDevicesFilteredOut'))));
|
|
699
855
|
}
|
|
700
856
|
}
|
|
@@ -769,7 +925,7 @@ export default class DeviceList extends Communication {
|
|
|
769
925
|
whiteSpace: 'nowrap',
|
|
770
926
|
color: '#fff',
|
|
771
927
|
} }, "Config-Manager")),
|
|
772
|
-
React.createElement("div", { style: {
|
|
928
|
+
React.createElement("div", { ref: this.containerRef, style: {
|
|
773
929
|
width: '100%',
|
|
774
930
|
flex: 1,
|
|
775
931
|
minHeight: 0,
|