@iobroker/gui-components 10.1.0 → 10.2.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 +12 -1
- package/build/Components/ObjectBrowser/ObjectBrowserClass.d.ts +6 -2
- package/build/Components/ObjectBrowser/ObjectBrowserClass.js +73 -35
- package/build/Components/ObjectBrowser/ObjectBrowserClass.js.map +1 -1
- package/build/Components/ObjectBrowser/dialogs.js +1 -1
- package/build/Components/ObjectBrowser/dialogs.js.map +1 -1
- package/build/Components/ObjectBrowser/renderLeaf.js +5 -11
- package/build/Components/ObjectBrowser/renderLeaf.js.map +1 -1
- package/build/Components/ObjectBrowser/styles.js +9 -0
- package/build/Components/ObjectBrowser/styles.js.map +1 -1
- package/build/Components/ObjectBrowser/utils.js +5 -12
- package/build/Components/ObjectBrowser/utils.js.map +1 -1
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ npm create vite@latest src -- --template react-ts
|
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@emotion/react": "^11.14.0",
|
|
38
38
|
"@emotion/styled": "^11.14.1",
|
|
39
|
-
"@iobroker/gui-components": "^10.
|
|
39
|
+
"@iobroker/gui-components": "^10.2.0",
|
|
40
40
|
"@mui/icons-material": "^9.0.1",
|
|
41
41
|
"@mui/material": "^9.0.1",
|
|
42
42
|
"react": "^19.2.5",
|
|
@@ -799,6 +799,11 @@ shared is delivered with this package:
|
|
|
799
799
|
const { moduleFederationShared } = require('@iobroker/gui-components/modulefederation.admin.config');
|
|
800
800
|
```
|
|
801
801
|
|
|
802
|
+
Custom components of the JSON config must additionally be built with `manifest: true` and ship the
|
|
803
|
+
generated `mf-manifest.json` next to `customComponents.js`, as the admin reads from it against which
|
|
804
|
+
component library the build was made. See
|
|
805
|
+
[the migration instructions](MIGRATION_8_10.md#6-custom-admin-components) for the details.
|
|
806
|
+
|
|
802
807
|
## Test GUI
|
|
803
808
|
|
|
804
809
|
The `test-gui` directory contains a vite application to develop and test the components visually.
|
|
@@ -872,6 +877,12 @@ You can find the migration instructions:
|
|
|
872
877
|
-->
|
|
873
878
|
|
|
874
879
|
## Changelog
|
|
880
|
+
### 10.2.0 (2026-08-26)
|
|
881
|
+
|
|
882
|
+
- (@krobipd) Corrected: the object browser stayed empty after closing the object customization dialog if any object was changed while the dialog was open (ioBroker/ioBroker.admin#3391)
|
|
883
|
+
- (@krobipd) Changed: `ObjectBrowserClass.subscribes` and `.recordStates` are `Set`s instead of arrays now
|
|
884
|
+
- (@krobipd) Improved: object browser performance on large installations — bursts of object changes cause one tree rebuild instead of several, state-change echoes no longer trigger redraws, subscription bookkeeping is no longer quadratic, and rows outside the viewport skip layout and paint
|
|
885
|
+
|
|
875
886
|
### 10.1.0 (2026-08-13)
|
|
876
887
|
|
|
877
888
|
- (@GermanBluefox) Updated `@iobroker/type-detector` to 6.0.0
|
|
@@ -33,6 +33,10 @@ export declare class ObjectBrowserClass extends Component<ObjectBrowserProps, Ob
|
|
|
33
33
|
/** Width class for which the columns were calculated the last time */
|
|
34
34
|
private lastCalculatedWidth;
|
|
35
35
|
private pausedSubscribes;
|
|
36
|
+
/** The tree was rebuilt while subscribes were paused, so the filter must be re-applied on resume */
|
|
37
|
+
private treeRebuiltWhilePaused;
|
|
38
|
+
/** Time of the first object change since the last tree rebuild (upper bound for the rebuild delay) */
|
|
39
|
+
private objectsUpdateFirstTs;
|
|
36
40
|
private selectFirst;
|
|
37
41
|
/** Last navigation that was applied from `navigateTo` or reported via `onNavigateTo` (loop guard). */
|
|
38
42
|
private lastNav;
|
|
@@ -40,7 +44,7 @@ export declare class ObjectBrowserClass extends Component<ObjectBrowserProps, Ob
|
|
|
40
44
|
private applyingNav;
|
|
41
45
|
root: TreeItem | null;
|
|
42
46
|
readonly states: Record<string, ioBroker.State>;
|
|
43
|
-
subscribes: string
|
|
47
|
+
subscribes: Set<string>;
|
|
44
48
|
private unsubscribeTimer;
|
|
45
49
|
private statesUpdateTimer;
|
|
46
50
|
private objectsUpdateTimer;
|
|
@@ -94,7 +98,7 @@ export declare class ObjectBrowserClass extends Component<ObjectBrowserProps, Ob
|
|
|
94
98
|
item: any;
|
|
95
99
|
ts: number;
|
|
96
100
|
};
|
|
97
|
-
recordStates: string
|
|
101
|
+
recordStates: Set<string>;
|
|
98
102
|
styles: {
|
|
99
103
|
cellIdIconFolder?: React.CSSProperties;
|
|
100
104
|
cellIdIconDocument?: React.CSSProperties;
|
|
@@ -17,7 +17,7 @@ import { Utils } from '../Utils'; // @iobroker/gui-components/Components/Utils
|
|
|
17
17
|
import { TabContainer } from '../TabContainer';
|
|
18
18
|
import { TabContent } from '../TabContent';
|
|
19
19
|
import { TabHeader } from '../TabHeader';
|
|
20
|
-
import { applyFilter,
|
|
20
|
+
import { applyFilter, buildTree, findNode, formatValue, generateFile, getName, colVar, widthFromContainer, growVar, getSelectIdIconFromObjects, setCustomValue, } from './utils';
|
|
21
21
|
import { HistoryChart } from './HistoryChart';
|
|
22
22
|
import { styles } from './styles';
|
|
23
23
|
import * as dialogs from './dialogs';
|
|
@@ -54,6 +54,10 @@ export class ObjectBrowserClass extends Component {
|
|
|
54
54
|
/** Width class for which the columns were calculated the last time */
|
|
55
55
|
lastCalculatedWidth = null;
|
|
56
56
|
pausedSubscribes = false;
|
|
57
|
+
/** The tree was rebuilt while subscribes were paused, so the filter must be re-applied on resume */
|
|
58
|
+
treeRebuiltWhilePaused = false;
|
|
59
|
+
/** Time of the first object change since the last tree rebuild (upper bound for the rebuild delay) */
|
|
60
|
+
objectsUpdateFirstTs = 0;
|
|
57
61
|
selectFirst;
|
|
58
62
|
/** Last navigation that was applied from `navigateTo` or reported via `onNavigateTo` (loop guard). */
|
|
59
63
|
lastNav = null;
|
|
@@ -61,7 +65,7 @@ export class ObjectBrowserClass extends Component {
|
|
|
61
65
|
applyingNav = false;
|
|
62
66
|
root = null;
|
|
63
67
|
states = {};
|
|
64
|
-
subscribes =
|
|
68
|
+
subscribes = new Set();
|
|
65
69
|
unsubscribeTimer = null;
|
|
66
70
|
statesUpdateTimer = null;
|
|
67
71
|
objectsUpdateTimer = null;
|
|
@@ -99,7 +103,7 @@ export class ObjectBrowserClass extends Component {
|
|
|
99
103
|
columnsVisibility = {};
|
|
100
104
|
changedIds = null;
|
|
101
105
|
contextMenu = null;
|
|
102
|
-
recordStates =
|
|
106
|
+
recordStates = new Set();
|
|
103
107
|
styles = {};
|
|
104
108
|
expertMode = false;
|
|
105
109
|
customColumnDialog = null;
|
|
@@ -688,7 +692,7 @@ export class ObjectBrowserClass extends Component {
|
|
|
688
692
|
// console.log(`- unsubscribe ${pattern}`);
|
|
689
693
|
this.props.socket.unsubscribeState(pattern, this.onStateChange);
|
|
690
694
|
});
|
|
691
|
-
this.subscribes
|
|
695
|
+
this.subscribes.clear();
|
|
692
696
|
this.objects = {};
|
|
693
697
|
}
|
|
694
698
|
/**
|
|
@@ -740,7 +744,7 @@ export class ObjectBrowserClass extends Component {
|
|
|
740
744
|
// console.log(`- unsubscribe ${pattern}`);
|
|
741
745
|
this.props.socket.unsubscribeState(pattern, this.onStateChange);
|
|
742
746
|
});
|
|
743
|
-
this.subscribes
|
|
747
|
+
this.subscribes.clear();
|
|
744
748
|
this.loadAllObjects(true)
|
|
745
749
|
.then(() => console.log('updated!'))
|
|
746
750
|
.catch(e => this.showError(e));
|
|
@@ -837,12 +841,12 @@ export class ObjectBrowserClass extends Component {
|
|
|
837
841
|
}
|
|
838
842
|
checkUnsubscribes() {
|
|
839
843
|
// Remove unused subscriptions
|
|
840
|
-
for (
|
|
841
|
-
if (!this.recordStates.
|
|
842
|
-
this.unsubscribe(
|
|
844
|
+
for (const id of [...this.subscribes]) {
|
|
845
|
+
if (!this.recordStates.has(id)) {
|
|
846
|
+
this.unsubscribe(id);
|
|
843
847
|
}
|
|
844
848
|
}
|
|
845
|
-
this.recordStates
|
|
849
|
+
this.recordStates.clear();
|
|
846
850
|
}
|
|
847
851
|
/**
|
|
848
852
|
* Find an item.
|
|
@@ -875,6 +879,19 @@ export class ObjectBrowserClass extends Component {
|
|
|
875
879
|
*/
|
|
876
880
|
onStateChange = (id, state) => {
|
|
877
881
|
// console.log(`> stateChange ${id}`);
|
|
882
|
+
const oldState = this.states[id];
|
|
883
|
+
if (state &&
|
|
884
|
+
oldState &&
|
|
885
|
+
state.val === oldState.val &&
|
|
886
|
+
state.ack === oldState.ack &&
|
|
887
|
+
state.q === oldState.q &&
|
|
888
|
+
state.ts === oldState.ts &&
|
|
889
|
+
state.lc === oldState.lc &&
|
|
890
|
+
state.from === oldState.from) {
|
|
891
|
+
// echo of the already known state: nothing that is displayed can have changed
|
|
892
|
+
// (`from` is compared too, because it is shown in the `changedFrom` column)
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
878
895
|
if (this.states[id]) {
|
|
879
896
|
const item = this.findItem(id);
|
|
880
897
|
if (item?.data.state) {
|
|
@@ -995,23 +1012,38 @@ export class ObjectBrowserClass extends Component {
|
|
|
995
1012
|
this.afterObjectUpdated();
|
|
996
1013
|
};
|
|
997
1014
|
afterObjectUpdated() {
|
|
998
|
-
if (!this.
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
}
|
|
1012
|
-
// else it will be re-rendered when the dialog will be closed
|
|
1013
|
-
}, 500);
|
|
1015
|
+
if (!this.objects) {
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
if (this.objectsUpdateTimer) {
|
|
1019
|
+
// collect a burst of object changes into one rebuild: wait for a quiet period,
|
|
1020
|
+
// but do not postpone the rebuild forever while changes keep arriving
|
|
1021
|
+
if (Date.now() - this.objectsUpdateFirstTs >= 2000) {
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
clearTimeout(this.objectsUpdateTimer);
|
|
1025
|
+
}
|
|
1026
|
+
else {
|
|
1027
|
+
this.objectsUpdateFirstTs = Date.now();
|
|
1014
1028
|
}
|
|
1029
|
+
this.objectsUpdateTimer = setTimeout(() => {
|
|
1030
|
+
this.objectsUpdateTimer = null;
|
|
1031
|
+
const { info, root } = buildTree(this.objects, {
|
|
1032
|
+
imagePrefix: this.props.imagePrefix,
|
|
1033
|
+
root: this.props.root,
|
|
1034
|
+
lang: this.props.lang,
|
|
1035
|
+
themeType: this.props.themeType,
|
|
1036
|
+
});
|
|
1037
|
+
this.root = root;
|
|
1038
|
+
this.info = info;
|
|
1039
|
+
if (!this.pausedSubscribes) {
|
|
1040
|
+
this.doFilter(true);
|
|
1041
|
+
}
|
|
1042
|
+
else {
|
|
1043
|
+
// the new tree has no visibility flags yet; re-apply the filter when the dialog is closed
|
|
1044
|
+
this.treeRebuiltWhilePaused = true;
|
|
1045
|
+
}
|
|
1046
|
+
}, 500);
|
|
1015
1047
|
}
|
|
1016
1048
|
// This function is called when the user changes the alias of an object.
|
|
1017
1049
|
// It updates the aliasMap and returns true if the aliasMap has changed.
|
|
@@ -1097,8 +1129,8 @@ export class ObjectBrowserClass extends Component {
|
|
|
1097
1129
|
return { newInnerState, filtered: false };
|
|
1098
1130
|
}
|
|
1099
1131
|
subscribe(id) {
|
|
1100
|
-
if (!this.subscribes.
|
|
1101
|
-
this.subscribes.
|
|
1132
|
+
if (!this.subscribes.has(id)) {
|
|
1133
|
+
this.subscribes.add(id);
|
|
1102
1134
|
// console.log(`+ subscribe ${id}`);
|
|
1103
1135
|
if (!this.pausedSubscribes) {
|
|
1104
1136
|
this.props.socket
|
|
@@ -1108,9 +1140,8 @@ export class ObjectBrowserClass extends Component {
|
|
|
1108
1140
|
}
|
|
1109
1141
|
}
|
|
1110
1142
|
unsubscribe(id) {
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
this.subscribes.splice(pos, 1);
|
|
1143
|
+
if (this.subscribes.has(id)) {
|
|
1144
|
+
this.subscribes.delete(id);
|
|
1114
1145
|
if (this.states[id]) {
|
|
1115
1146
|
delete this.states[id];
|
|
1116
1147
|
}
|
|
@@ -1129,6 +1160,11 @@ export class ObjectBrowserClass extends Component {
|
|
|
1129
1160
|
else if (this.pausedSubscribes && !isPause) {
|
|
1130
1161
|
this.pausedSubscribes = false;
|
|
1131
1162
|
this.subscribes.forEach(id => this.props.socket.subscribeState(id, this.onStateChange));
|
|
1163
|
+
if (this.treeRebuiltWhilePaused) {
|
|
1164
|
+
// without this filter run, no node of the rebuilt tree is marked visible and the browser stays empty
|
|
1165
|
+
this.treeRebuiltWhilePaused = false;
|
|
1166
|
+
this.doFilter(true);
|
|
1167
|
+
}
|
|
1132
1168
|
}
|
|
1133
1169
|
}
|
|
1134
1170
|
clearFilter() {
|
|
@@ -1168,20 +1204,21 @@ export class ObjectBrowserClass extends Component {
|
|
|
1168
1204
|
this.localStorage.setItem(`${this.props.dialogName || 'App'}.objectSelected`, '[]');
|
|
1169
1205
|
this.setState({ expanded: [], depth: 0, selected: [] }, () => this.onAfterSelect());
|
|
1170
1206
|
}
|
|
1171
|
-
expandDepth(root, depth, expanded) {
|
|
1207
|
+
expandDepth(root, depth, expanded, expandedSet) {
|
|
1172
1208
|
if (!this.root) {
|
|
1173
1209
|
throw new Error('No root');
|
|
1174
1210
|
}
|
|
1175
1211
|
root ||= this.root;
|
|
1212
|
+
expandedSet ||= new Set(expanded);
|
|
1176
1213
|
if (depth > 0) {
|
|
1177
1214
|
root.children?.forEach(item => {
|
|
1178
1215
|
if (item.data.sumVisibility) {
|
|
1179
|
-
if (!
|
|
1216
|
+
if (!expandedSet.has(item.data.id)) {
|
|
1180
1217
|
expanded.push(item.data.id);
|
|
1181
|
-
|
|
1218
|
+
expandedSet.add(item.data.id);
|
|
1182
1219
|
}
|
|
1183
1220
|
if (depth - 1 > 0) {
|
|
1184
|
-
this.expandDepth(item, depth - 1, expanded);
|
|
1221
|
+
this.expandDepth(item, depth - 1, expanded, expandedSet);
|
|
1185
1222
|
}
|
|
1186
1223
|
}
|
|
1187
1224
|
});
|
|
@@ -1196,6 +1233,7 @@ export class ObjectBrowserClass extends Component {
|
|
|
1196
1233
|
const expanded = [...this.state.expanded];
|
|
1197
1234
|
if (this.root) {
|
|
1198
1235
|
this.expandDepth(this.root, depth, expanded);
|
|
1236
|
+
expanded.sort();
|
|
1199
1237
|
}
|
|
1200
1238
|
this.localStorage.setItem(`${this.props.dialogName || 'App'}.objectExpanded`, JSON.stringify(expanded));
|
|
1201
1239
|
this.setState({ depth, expanded });
|
|
@@ -2303,7 +2341,7 @@ export class ObjectBrowserClass extends Component {
|
|
|
2303
2341
|
* The rendering method of this component.
|
|
2304
2342
|
*/
|
|
2305
2343
|
render() {
|
|
2306
|
-
this.recordStates
|
|
2344
|
+
this.recordStates.clear();
|
|
2307
2345
|
if (this.unsubscribeTimer) {
|
|
2308
2346
|
clearTimeout(this.unsubscribeTimer);
|
|
2309
2347
|
}
|