@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 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.1.0",
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, binarySearch, buildTree, findNode, formatValue, generateFile, getName, colVar, widthFromContainer, growVar, getSelectIdIconFromObjects, setCustomValue, } from './utils';
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 (let i = this.subscribes.length - 1; i >= 0; i--) {
841
- if (!this.recordStates.includes(this.subscribes[i])) {
842
- this.unsubscribe(this.subscribes[i]);
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.objectsUpdateTimer && this.objects) {
999
- this.objectsUpdateTimer = setTimeout(() => {
1000
- this.objectsUpdateTimer = null;
1001
- const { info, root } = buildTree(this.objects, {
1002
- imagePrefix: this.props.imagePrefix,
1003
- root: this.props.root,
1004
- lang: this.props.lang,
1005
- themeType: this.props.themeType,
1006
- });
1007
- this.root = root;
1008
- this.info = info;
1009
- if (!this.pausedSubscribes) {
1010
- this.doFilter();
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.includes(id)) {
1101
- this.subscribes.push(id);
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
- const pos = this.subscribes.indexOf(id);
1112
- if (pos !== -1) {
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 (!binarySearch(expanded, item.data.id)) {
1216
+ if (!expandedSet.has(item.data.id)) {
1180
1217
  expanded.push(item.data.id);
1181
- expanded.sort();
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
  }