@iobroker/gui-components 10.3.2 → 10.3.4
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 -1
- package/build/Components/ObjectBrowser/ObjectBrowserClass.d.ts +118 -1
- package/build/Components/ObjectBrowser/ObjectBrowserClass.js +314 -23
- package/build/Components/ObjectBrowser/ObjectBrowserClass.js.map +1 -1
- package/build/Components/ObjectBrowser/renderLeaf.d.ts +34 -2
- package/build/Components/ObjectBrowser/renderLeaf.js +107 -28
- package/build/Components/ObjectBrowser/renderLeaf.js.map +1 -1
- package/build/Components/Schedule.d.ts +2 -0
- package/build/Components/Schedule.js +12 -4
- package/build/Components/Schedule.js.map +1 -1
- package/package.json +2 -2
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.3.
|
|
39
|
+
"@iobroker/gui-components": "^10.3.4",
|
|
40
40
|
"@mui/icons-material": "^9.0.1",
|
|
41
41
|
"@mui/material": "^9.0.1",
|
|
42
42
|
"react": "^19.2.5",
|
|
@@ -877,6 +877,15 @@ You can find the migration instructions:
|
|
|
877
877
|
-->
|
|
878
878
|
|
|
879
879
|
## Changelog
|
|
880
|
+
### 10.3.4 (2026-09-23)
|
|
881
|
+
|
|
882
|
+
- (@GermanBluefox) Fixed: the schedule wizard threw the time of day away. A schedule with an exact time was saved as `{"time":{"exactTime":true}}` without the time itself, so it ran at 00:00 and the dialog showed `--:--` and "every day at undefined" when it was opened again (ioBroker.javascript#2386). A saved schedule that lacks a setting now also falls back to the default of that setting instead of `undefined`
|
|
883
|
+
|
|
884
|
+
### 10.3.3 (2026-09-23)
|
|
885
|
+
|
|
886
|
+
- (@krobipd) Improved: the object browser renders the rows around the visible area instead of every open row of the tree. Selecting a row, applying a filter or folding a node rebuilt every open row, and that cost grows with the size of the tree: measured with 4003 rows open, one click on a row blocked the main thread for 3.9 seconds and a filter for 2.6 seconds - with 603 rows it was 0.4 seconds each. Now it is 0 ms in both cases, and the table holds 38 rows instead of 4003 (1985 instead of 164652 DOM nodes). Scrolling, scrolling to a row and the keyboard navigation reach every row as before
|
|
887
|
+
- (@krobipd) Improved: a value that changes no longer rebuilds every open row of the object browser. Until now one state answered with a render of the whole table, and with a few hundred rows open that blocked the main thread for about a fifth of a second per value - on an installation whose adapters report all the time the table never stood still. A row is kept as it is unless its own state changed or something happened that concerns every row (filter, columns, theme, selection, a rebuilt tree). Measured in the admin with 542 open rows and ten state changes: 1937 ms of blocked main thread before, none after, with the new values shown in every case
|
|
888
|
+
|
|
880
889
|
### 10.3.2 (2026-09-22)
|
|
881
890
|
|
|
882
891
|
- (@GermanBluefox) Updated packages
|
|
@@ -39,6 +39,65 @@ export declare class ObjectBrowserClass extends Component<ObjectBrowserProps, Ob
|
|
|
39
39
|
private treeRebuiltWhilePaused;
|
|
40
40
|
/** Time of the first object change since the last tree rebuild (upper bound for the rebuild delay) */
|
|
41
41
|
private objectsUpdateFirstTs;
|
|
42
|
+
/**
|
|
43
|
+
* Counts up on every render that can change ANY row - a new filter, other columns, another
|
|
44
|
+
* theme, a different selection, a rebuilt tree. A state change does NOT count it up: it only
|
|
45
|
+
* concerns the rows whose value changed. Together with `rowStateVersion` this is the memo key
|
|
46
|
+
* of a row (see `renderRow`), so a value that ticks every second no longer rebuilds the whole
|
|
47
|
+
* table.
|
|
48
|
+
*
|
|
49
|
+
* Whether a render concerns every row is decided in `render` and not by a flag that the caller
|
|
50
|
+
* sets: React may batch the render of a state change with a `setState` (a click) or with new
|
|
51
|
+
* props, and a flag would then keep every row as it was, although the selection has changed.
|
|
52
|
+
*/
|
|
53
|
+
renderEpoch: number;
|
|
54
|
+
/**
|
|
55
|
+
* Set by every `forceUpdate()`: something changed that is not visible in `state` or `props` (a
|
|
56
|
+
* rebuilt tree, other column widths), so every row has to be built again. Only
|
|
57
|
+
* `updateRowsOnly` renders without it
|
|
58
|
+
*/
|
|
59
|
+
private rowsInvalid;
|
|
60
|
+
/** `state` and `props` of the last render: a render with the same two cannot concern every row */
|
|
61
|
+
private lastRenderedState;
|
|
62
|
+
private lastRenderedProps;
|
|
63
|
+
/**
|
|
64
|
+
* row id -> the states the row showed when it was built last: its own value and the status
|
|
65
|
+
* states of a device (online, offline, error). The subscriptions that are still needed are
|
|
66
|
+
* collected from the rows of the window (see `checkUnsubscribes`) - a row that is served from
|
|
67
|
+
* the memo is not built again, but its entry here is still valid
|
|
68
|
+
*/
|
|
69
|
+
readonly rowStates: Record<string, Set<string>>;
|
|
70
|
+
/**
|
|
71
|
+
* state id -> the rows that show this state although it is not their own: a device shows the
|
|
72
|
+
* status of its `statusStates`. A change of such a state has to render those rows as well
|
|
73
|
+
*/
|
|
74
|
+
private readonly stateDependents;
|
|
75
|
+
/**
|
|
76
|
+
* Rows that are rendered above and below the visible area, so that scrolling does not run into
|
|
77
|
+
* a gap before the next render
|
|
78
|
+
*/
|
|
79
|
+
private static readonly OVERSCAN_ROWS;
|
|
80
|
+
/** Every row of the table in the order in which it is shown (see `flattenItems`) */
|
|
81
|
+
private flatRows;
|
|
82
|
+
/**
|
|
83
|
+
* Where every row of `flatRows` begins, in pixels. One entry longer than `flatRows`: the last
|
|
84
|
+
* one is the height of the whole table
|
|
85
|
+
*/
|
|
86
|
+
private rowOffsets;
|
|
87
|
+
/** A row measured another height than `rowOffsets` expected: calculate them again */
|
|
88
|
+
private rowOffsetsInvalid;
|
|
89
|
+
/** id -> the height the row really had. Only rows that are not `ROW_HEIGHT` high end up here */
|
|
90
|
+
private readonly measuredHeights;
|
|
91
|
+
/** Scroll position and height of the table container, the two values the window is cut from */
|
|
92
|
+
private scrollTop;
|
|
93
|
+
private viewportHeight;
|
|
94
|
+
private scrollFrame;
|
|
95
|
+
/** First and last row of the last rendered window, to notice that scrolling moved it */
|
|
96
|
+
private windowFirst;
|
|
97
|
+
private windowLast;
|
|
98
|
+
/** id -> how often the state of this row has changed. Part of the memo key of the row */
|
|
99
|
+
readonly rowStateVersion: Record<string, number>;
|
|
100
|
+
private stateVersionCounter;
|
|
42
101
|
private selectFirst;
|
|
43
102
|
/** Last navigation that was applied from `navigateTo` or reported via `onNavigateTo` (loop guard). */
|
|
44
103
|
private lastNav;
|
|
@@ -105,7 +164,6 @@ export declare class ObjectBrowserClass extends Component<ObjectBrowserProps, Ob
|
|
|
105
164
|
item: any;
|
|
106
165
|
ts: number;
|
|
107
166
|
};
|
|
108
|
-
recordStates: Set<string>;
|
|
109
167
|
styles: {
|
|
110
168
|
cellIdIconFolder?: React.CSSProperties;
|
|
111
169
|
cellIdIconDocument?: React.CSSProperties;
|
|
@@ -193,6 +251,13 @@ export declare class ObjectBrowserClass extends Component<ObjectBrowserProps, Ob
|
|
|
193
251
|
*/
|
|
194
252
|
renderColumnsSelectorDialog(): JSX.Element | null;
|
|
195
253
|
private getAdditionalColumns;
|
|
254
|
+
/**
|
|
255
|
+
* Remember that a row shows a state. Called while the row is built (see `renderLeaf`)
|
|
256
|
+
*
|
|
257
|
+
* @param rowId the row
|
|
258
|
+
* @param stateId the state it shows: its own or one of its status states
|
|
259
|
+
*/
|
|
260
|
+
recordState(rowId: string, stateId: string): void;
|
|
196
261
|
private checkUnsubscribes;
|
|
197
262
|
/**
|
|
198
263
|
* Find an item.
|
|
@@ -301,8 +366,43 @@ export declare class ObjectBrowserClass extends Component<ObjectBrowserProps, Ob
|
|
|
301
366
|
get width(): Width;
|
|
302
367
|
/** Watch the width of the table container to detect the width class (see `width`) */
|
|
303
368
|
observeContainerWidth(): void;
|
|
369
|
+
/** Store the height of the container: it decides how many rows the window holds */
|
|
370
|
+
setViewportHeight(viewportHeight: number): void;
|
|
304
371
|
/** Store the width class of the container if it changed */
|
|
305
372
|
setContainerWidth(containerWidth: number): void;
|
|
373
|
+
/**
|
|
374
|
+
* The height a row needs.
|
|
375
|
+
*
|
|
376
|
+
* A row with an alias is higher than the others, and what a row really measured beats the
|
|
377
|
+
* estimate - the table has to know the height of rows it does not render at the moment,
|
|
378
|
+
* otherwise the scrollbar does not fit the content.
|
|
379
|
+
*/
|
|
380
|
+
private rowHeight;
|
|
381
|
+
/** All rows in the order in which they are shown, and where each of them begins */
|
|
382
|
+
private buildFlatRows;
|
|
383
|
+
/** Where every row begins - again after a row measured another height than was expected */
|
|
384
|
+
private calculateRowOffsets;
|
|
385
|
+
/** The row that covers this position (binary search over `rowOffsets`) */
|
|
386
|
+
private findRowAt;
|
|
387
|
+
/**
|
|
388
|
+
* The rows that are rendered: the visible ones plus `OVERSCAN_ROWS` above and below.
|
|
389
|
+
*
|
|
390
|
+
* A tree with a few thousand open rows costs a fifth of a second of blocked main thread for
|
|
391
|
+
* every change that concerns all rows - selecting a row, a filter, folding a node - because
|
|
392
|
+
* every row is built anew. Only the rows around the visible area are rendered now, so that
|
|
393
|
+
* work no longer grows with the size of the tree.
|
|
394
|
+
*/
|
|
395
|
+
private getWindow;
|
|
396
|
+
/** The table was scrolled: move the window of rendered rows, at most once per frame */
|
|
397
|
+
private readonly onTableScroll;
|
|
398
|
+
/**
|
|
399
|
+
* Remember how high the rendered rows really are.
|
|
400
|
+
*
|
|
401
|
+
* Rows are not all `ROW_HEIGHT` high (an alias is higher, the focused row of the narrow view
|
|
402
|
+
* has no fixed height at all), and a row outside the window cannot be measured - so every row
|
|
403
|
+
* that was rendered once keeps its measured height for the calculation of the window.
|
|
404
|
+
*/
|
|
405
|
+
private measureRenderedRows;
|
|
306
406
|
/**
|
|
307
407
|
* All column widths as CSS variables of the table container. The header and every row read the
|
|
308
408
|
* widths from here (see `colWidth`), so they can never get out of sync, and the resizer can
|
|
@@ -343,6 +443,13 @@ export declare class ObjectBrowserClass extends Component<ObjectBrowserProps, Ob
|
|
|
343
443
|
* Called when component is updated.
|
|
344
444
|
*/
|
|
345
445
|
componentDidUpdate(prevProps: ObjectBrowserProps): void;
|
|
446
|
+
/**
|
|
447
|
+
* Scrolls to a row.
|
|
448
|
+
*
|
|
449
|
+
* The row does not have to be rendered: only the rows around the visible area are (see
|
|
450
|
+
* `getWindow`), so the position is taken from the flat list of all rows and the row is brought
|
|
451
|
+
* into the middle of the table by scrolling there.
|
|
452
|
+
*/
|
|
346
453
|
scrollToItem(id: string): void;
|
|
347
454
|
onUpdate(valAck: {
|
|
348
455
|
val: ioBroker.StateValue;
|
|
@@ -353,6 +460,16 @@ export declare class ObjectBrowserClass extends Component<ObjectBrowserProps, Ob
|
|
|
353
460
|
showAddDataPointDialog(id: string, initialType: ioBroker.ObjectType, initialStateType?: ioBroker.CommonType): void;
|
|
354
461
|
/** Renders the aliases list for one state (if more than 2) */
|
|
355
462
|
doFilter(doNotStore?: boolean): void;
|
|
463
|
+
/**
|
|
464
|
+
* Renders the browser and every row again: whoever calls it has changed something that is not
|
|
465
|
+
* visible in `state` or `props`, so the rows cannot keep their output
|
|
466
|
+
*/
|
|
467
|
+
forceUpdate(callback?: () => void): void;
|
|
468
|
+
/**
|
|
469
|
+
* Renders the browser, but keeps the output of every row that did not change: a state changed
|
|
470
|
+
* (see `rowStateVersion`), the table was scrolled or a row measured another height
|
|
471
|
+
*/
|
|
472
|
+
private updateRowsOnly;
|
|
356
473
|
/**
|
|
357
474
|
* The rendering method of this component.
|
|
358
475
|
*/
|
|
@@ -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, buildTree, findNode, formatValue, generateFile, getName, colVar, widthFromContainer, growVar, getSelectIdIconFromObjects, setCustomValue, } from './utils';
|
|
20
|
+
import { ROW_HEIGHT, 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';
|
|
@@ -60,6 +60,65 @@ export class ObjectBrowserClass extends Component {
|
|
|
60
60
|
treeRebuiltWhilePaused = false;
|
|
61
61
|
/** Time of the first object change since the last tree rebuild (upper bound for the rebuild delay) */
|
|
62
62
|
objectsUpdateFirstTs = 0;
|
|
63
|
+
/**
|
|
64
|
+
* Counts up on every render that can change ANY row - a new filter, other columns, another
|
|
65
|
+
* theme, a different selection, a rebuilt tree. A state change does NOT count it up: it only
|
|
66
|
+
* concerns the rows whose value changed. Together with `rowStateVersion` this is the memo key
|
|
67
|
+
* of a row (see `renderRow`), so a value that ticks every second no longer rebuilds the whole
|
|
68
|
+
* table.
|
|
69
|
+
*
|
|
70
|
+
* Whether a render concerns every row is decided in `render` and not by a flag that the caller
|
|
71
|
+
* sets: React may batch the render of a state change with a `setState` (a click) or with new
|
|
72
|
+
* props, and a flag would then keep every row as it was, although the selection has changed.
|
|
73
|
+
*/
|
|
74
|
+
renderEpoch = 0;
|
|
75
|
+
/**
|
|
76
|
+
* Set by every `forceUpdate()`: something changed that is not visible in `state` or `props` (a
|
|
77
|
+
* rebuilt tree, other column widths), so every row has to be built again. Only
|
|
78
|
+
* `updateRowsOnly` renders without it
|
|
79
|
+
*/
|
|
80
|
+
rowsInvalid = true;
|
|
81
|
+
/** `state` and `props` of the last render: a render with the same two cannot concern every row */
|
|
82
|
+
lastRenderedState = null;
|
|
83
|
+
lastRenderedProps = null;
|
|
84
|
+
/**
|
|
85
|
+
* row id -> the states the row showed when it was built last: its own value and the status
|
|
86
|
+
* states of a device (online, offline, error). The subscriptions that are still needed are
|
|
87
|
+
* collected from the rows of the window (see `checkUnsubscribes`) - a row that is served from
|
|
88
|
+
* the memo is not built again, but its entry here is still valid
|
|
89
|
+
*/
|
|
90
|
+
rowStates = {};
|
|
91
|
+
/**
|
|
92
|
+
* state id -> the rows that show this state although it is not their own: a device shows the
|
|
93
|
+
* status of its `statusStates`. A change of such a state has to render those rows as well
|
|
94
|
+
*/
|
|
95
|
+
stateDependents = {};
|
|
96
|
+
/**
|
|
97
|
+
* Rows that are rendered above and below the visible area, so that scrolling does not run into
|
|
98
|
+
* a gap before the next render
|
|
99
|
+
*/
|
|
100
|
+
static OVERSCAN_ROWS = 12;
|
|
101
|
+
/** Every row of the table in the order in which it is shown (see `flattenItems`) */
|
|
102
|
+
flatRows = [];
|
|
103
|
+
/**
|
|
104
|
+
* Where every row of `flatRows` begins, in pixels. One entry longer than `flatRows`: the last
|
|
105
|
+
* one is the height of the whole table
|
|
106
|
+
*/
|
|
107
|
+
rowOffsets = [0];
|
|
108
|
+
/** A row measured another height than `rowOffsets` expected: calculate them again */
|
|
109
|
+
rowOffsetsInvalid = false;
|
|
110
|
+
/** id -> the height the row really had. Only rows that are not `ROW_HEIGHT` high end up here */
|
|
111
|
+
measuredHeights = {};
|
|
112
|
+
/** Scroll position and height of the table container, the two values the window is cut from */
|
|
113
|
+
scrollTop = 0;
|
|
114
|
+
viewportHeight = 0;
|
|
115
|
+
scrollFrame = null;
|
|
116
|
+
/** First and last row of the last rendered window, to notice that scrolling moved it */
|
|
117
|
+
windowFirst = 0;
|
|
118
|
+
windowLast = 0;
|
|
119
|
+
/** id -> how often the state of this row has changed. Part of the memo key of the row */
|
|
120
|
+
rowStateVersion = {};
|
|
121
|
+
stateVersionCounter = 0;
|
|
63
122
|
selectFirst;
|
|
64
123
|
/** Last navigation that was applied from `navigateTo` or reported via `onNavigateTo` (loop guard). */
|
|
65
124
|
lastNav = null;
|
|
@@ -110,7 +169,6 @@ export class ObjectBrowserClass extends Component {
|
|
|
110
169
|
columnsVisibility = {};
|
|
111
170
|
changedIds = null;
|
|
112
171
|
contextMenu = null;
|
|
113
|
-
recordStates = new Set();
|
|
114
172
|
styles = {};
|
|
115
173
|
expertMode = false;
|
|
116
174
|
customColumnDialog = null;
|
|
@@ -664,6 +722,7 @@ export class ObjectBrowserClass extends Component {
|
|
|
664
722
|
window.addEventListener('keydown', this.onKeyPress, true);
|
|
665
723
|
window.addEventListener('keyup', this.onKeyPress, true);
|
|
666
724
|
this.observeContainerWidth();
|
|
725
|
+
this.measureRenderedRows();
|
|
667
726
|
// Inform dialog that all objects are loaded
|
|
668
727
|
if (this.props.onAllLoaded) {
|
|
669
728
|
setTimeout(() => {
|
|
@@ -695,6 +754,10 @@ export class ObjectBrowserClass extends Component {
|
|
|
695
754
|
this.resizeObserver?.disconnect();
|
|
696
755
|
this.resizeObserver = null;
|
|
697
756
|
this.observedContainer = null;
|
|
757
|
+
if (this.scrollFrame !== null) {
|
|
758
|
+
window.cancelAnimationFrame(this.scrollFrame);
|
|
759
|
+
this.scrollFrame = null;
|
|
760
|
+
}
|
|
698
761
|
// the timers must not fire after the component was destroyed
|
|
699
762
|
if (this.unsubscribeTimer) {
|
|
700
763
|
clearTimeout(this.unsubscribeTimer);
|
|
@@ -868,14 +931,33 @@ export class ObjectBrowserClass extends Component {
|
|
|
868
931
|
return null;
|
|
869
932
|
}
|
|
870
933
|
}
|
|
934
|
+
/**
|
|
935
|
+
* Remember that a row shows a state. Called while the row is built (see `renderLeaf`)
|
|
936
|
+
*
|
|
937
|
+
* @param rowId the row
|
|
938
|
+
* @param stateId the state it shows: its own or one of its status states
|
|
939
|
+
*/
|
|
940
|
+
recordState(rowId, stateId) {
|
|
941
|
+
(this.rowStates[rowId] ||= new Set()).add(stateId);
|
|
942
|
+
if (stateId !== rowId) {
|
|
943
|
+
(this.stateDependents[stateId] ||= new Set()).add(rowId);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
871
946
|
checkUnsubscribes() {
|
|
947
|
+
// Only the states of the rows in the window are needed. Rows that scrolled away are not
|
|
948
|
+
// rendered anymore, so their states are unsubscribed as well
|
|
949
|
+
const needed = new Set();
|
|
950
|
+
for (let i = this.windowFirst; i < this.windowLast; i++) {
|
|
951
|
+
const rowId = this.flatRows[i]?.item.data.id;
|
|
952
|
+
this.rowStates[rowId]?.forEach(id => needed.add(id));
|
|
953
|
+
}
|
|
872
954
|
// Remove unused subscriptions
|
|
873
955
|
for (const id of [...this.subscribes]) {
|
|
874
|
-
if (!
|
|
956
|
+
if (!needed.has(id)) {
|
|
875
957
|
this.unsubscribe(id);
|
|
958
|
+
delete this.stateDependents[id];
|
|
876
959
|
}
|
|
877
960
|
}
|
|
878
|
-
this.recordStates.clear();
|
|
879
961
|
}
|
|
880
962
|
/**
|
|
881
963
|
* Find an item.
|
|
@@ -933,11 +1015,15 @@ export class ObjectBrowserClass extends Component {
|
|
|
933
1015
|
else {
|
|
934
1016
|
delete this.states[id];
|
|
935
1017
|
}
|
|
1018
|
+
// only this row has to be rendered again - and the rows that show this state as their status
|
|
1019
|
+
const version = ++this.stateVersionCounter;
|
|
1020
|
+
this.rowStateVersion[id] = version;
|
|
1021
|
+
this.stateDependents[id]?.forEach(rowId => (this.rowStateVersion[rowId] = version));
|
|
936
1022
|
if (!this.pausedSubscribes) {
|
|
937
1023
|
if (!this.statesUpdateTimer) {
|
|
938
1024
|
this.statesUpdateTimer = setTimeout(() => {
|
|
939
1025
|
this.statesUpdateTimer = null;
|
|
940
|
-
this.
|
|
1026
|
+
this.updateRowsOnly();
|
|
941
1027
|
}, 300);
|
|
942
1028
|
}
|
|
943
1029
|
}
|
|
@@ -2023,11 +2109,27 @@ export class ObjectBrowserClass extends Component {
|
|
|
2023
2109
|
}
|
|
2024
2110
|
this.resizeObserver?.disconnect();
|
|
2025
2111
|
this.observedContainer = container;
|
|
2026
|
-
this.resizeObserver = new ResizeObserver(entries =>
|
|
2112
|
+
this.resizeObserver = new ResizeObserver(entries => {
|
|
2113
|
+
// the height decides how many rows are rendered (see `getWindow`)
|
|
2114
|
+
this.setViewportHeight(entries[0].contentRect.height);
|
|
2115
|
+
this.setContainerWidth(entries[0].contentRect.width);
|
|
2116
|
+
});
|
|
2027
2117
|
this.resizeObserver.observe(container);
|
|
2028
2118
|
// measure immediately, so the first render after mounting shows the right columns
|
|
2119
|
+
this.setViewportHeight(container.clientHeight);
|
|
2029
2120
|
this.setContainerWidth(container.clientWidth);
|
|
2030
2121
|
}
|
|
2122
|
+
/** Store the height of the container: it decides how many rows the window holds */
|
|
2123
|
+
setViewportHeight(viewportHeight) {
|
|
2124
|
+
if (Math.abs(this.viewportHeight - viewportHeight) < 1) {
|
|
2125
|
+
return;
|
|
2126
|
+
}
|
|
2127
|
+
this.viewportHeight = viewportHeight;
|
|
2128
|
+
const { first, last } = this.getWindow();
|
|
2129
|
+
if (first !== this.windowFirst || last !== this.windowLast) {
|
|
2130
|
+
this.updateRowsOnly();
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2031
2133
|
/** Store the width class of the container if it changed */
|
|
2032
2134
|
setContainerWidth(containerWidth) {
|
|
2033
2135
|
const width = widthFromContainer(containerWidth, this.props.theme);
|
|
@@ -2036,6 +2138,139 @@ export class ObjectBrowserClass extends Component {
|
|
|
2036
2138
|
this.setState({ containerWidth: width });
|
|
2037
2139
|
}
|
|
2038
2140
|
}
|
|
2141
|
+
/**
|
|
2142
|
+
* The height a row needs.
|
|
2143
|
+
*
|
|
2144
|
+
* A row with an alias is higher than the others, and what a row really measured beats the
|
|
2145
|
+
* estimate - the table has to know the height of rows it does not render at the moment,
|
|
2146
|
+
* otherwise the scrollbar does not fit the content.
|
|
2147
|
+
*/
|
|
2148
|
+
rowHeight(item) {
|
|
2149
|
+
const id = item.data.id;
|
|
2150
|
+
// the root itself has no row, only its children have one
|
|
2151
|
+
if (!id) {
|
|
2152
|
+
return 0;
|
|
2153
|
+
}
|
|
2154
|
+
const measured = this.measuredHeights[id];
|
|
2155
|
+
if (measured) {
|
|
2156
|
+
return measured;
|
|
2157
|
+
}
|
|
2158
|
+
const common = this.objects[id]?.common;
|
|
2159
|
+
if (id.startsWith('alias.') && common?.alias?.id) {
|
|
2160
|
+
// see `tableRowAlias` and `tableRowAliasReadWrite` in styles.ts
|
|
2161
|
+
return typeof common.alias.id === 'object' ? ROW_HEIGHT + 22 : ROW_HEIGHT + 10;
|
|
2162
|
+
}
|
|
2163
|
+
return ROW_HEIGHT;
|
|
2164
|
+
}
|
|
2165
|
+
/** All rows in the order in which they are shown, and where each of them begins */
|
|
2166
|
+
buildFlatRows() {
|
|
2167
|
+
this.flatRows = this.root ? leaf.flattenItems(this, this.root) : [];
|
|
2168
|
+
this.calculateRowOffsets();
|
|
2169
|
+
}
|
|
2170
|
+
/** Where every row begins - again after a row measured another height than was expected */
|
|
2171
|
+
calculateRowOffsets() {
|
|
2172
|
+
this.rowOffsetsInvalid = false;
|
|
2173
|
+
const offsets = new Array(this.flatRows.length + 1);
|
|
2174
|
+
offsets[0] = 0;
|
|
2175
|
+
for (let i = 0; i < this.flatRows.length; i++) {
|
|
2176
|
+
offsets[i + 1] = offsets[i] + this.rowHeight(this.flatRows[i].item);
|
|
2177
|
+
}
|
|
2178
|
+
this.rowOffsets = offsets;
|
|
2179
|
+
}
|
|
2180
|
+
/** The row that covers this position (binary search over `rowOffsets`) */
|
|
2181
|
+
findRowAt(position) {
|
|
2182
|
+
let low = 0;
|
|
2183
|
+
let high = this.flatRows.length - 1;
|
|
2184
|
+
while (low < high) {
|
|
2185
|
+
const middle = (low + high + 1) >> 1;
|
|
2186
|
+
if (this.rowOffsets[middle] <= position) {
|
|
2187
|
+
low = middle;
|
|
2188
|
+
}
|
|
2189
|
+
else {
|
|
2190
|
+
high = middle - 1;
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
return low;
|
|
2194
|
+
}
|
|
2195
|
+
/**
|
|
2196
|
+
* The rows that are rendered: the visible ones plus `OVERSCAN_ROWS` above and below.
|
|
2197
|
+
*
|
|
2198
|
+
* A tree with a few thousand open rows costs a fifth of a second of blocked main thread for
|
|
2199
|
+
* every change that concerns all rows - selecting a row, a filter, folding a node - because
|
|
2200
|
+
* every row is built anew. Only the rows around the visible area are rendered now, so that
|
|
2201
|
+
* work no longer grows with the size of the tree.
|
|
2202
|
+
*/
|
|
2203
|
+
getWindow() {
|
|
2204
|
+
const count = this.flatRows.length;
|
|
2205
|
+
if (!count) {
|
|
2206
|
+
return { first: 0, last: 0 };
|
|
2207
|
+
}
|
|
2208
|
+
// until the container was measured (the very first render) render a screen full of rows
|
|
2209
|
+
const height = this.viewportHeight || 1_000;
|
|
2210
|
+
const first = Math.max(0, this.findRowAt(this.scrollTop) - ObjectBrowserClass.OVERSCAN_ROWS);
|
|
2211
|
+
const last = Math.min(count, this.findRowAt(this.scrollTop + height) + 1 + ObjectBrowserClass.OVERSCAN_ROWS);
|
|
2212
|
+
return { first, last };
|
|
2213
|
+
}
|
|
2214
|
+
/** The table was scrolled: move the window of rendered rows, at most once per frame */
|
|
2215
|
+
onTableScroll = () => {
|
|
2216
|
+
if (this.scrollFrame !== null) {
|
|
2217
|
+
return;
|
|
2218
|
+
}
|
|
2219
|
+
this.scrollFrame = window.requestAnimationFrame(() => {
|
|
2220
|
+
this.scrollFrame = null;
|
|
2221
|
+
const container = this.tableRef.current;
|
|
2222
|
+
if (!container) {
|
|
2223
|
+
return;
|
|
2224
|
+
}
|
|
2225
|
+
this.scrollTop = container.scrollTop;
|
|
2226
|
+
const { first, last } = this.getWindow();
|
|
2227
|
+
if (first !== this.windowFirst || last !== this.windowLast) {
|
|
2228
|
+
// scrolling changes no row, so the rows that stay in the window keep their output
|
|
2229
|
+
this.updateRowsOnly();
|
|
2230
|
+
}
|
|
2231
|
+
});
|
|
2232
|
+
};
|
|
2233
|
+
/**
|
|
2234
|
+
* Remember how high the rendered rows really are.
|
|
2235
|
+
*
|
|
2236
|
+
* Rows are not all `ROW_HEIGHT` high (an alias is higher, the focused row of the narrow view
|
|
2237
|
+
* has no fixed height at all), and a row outside the window cannot be measured - so every row
|
|
2238
|
+
* that was rendered once keeps its measured height for the calculation of the window.
|
|
2239
|
+
*/
|
|
2240
|
+
measureRenderedRows() {
|
|
2241
|
+
const container = this.tableRef.current;
|
|
2242
|
+
if (!container) {
|
|
2243
|
+
return;
|
|
2244
|
+
}
|
|
2245
|
+
const heights = {};
|
|
2246
|
+
for (const child of Array.from(container.children)) {
|
|
2247
|
+
// the details panel of the narrow view follows its row and belongs to its height
|
|
2248
|
+
const detailsOf = child.dataset.detailsOf;
|
|
2249
|
+
if (detailsOf) {
|
|
2250
|
+
if (heights[detailsOf]) {
|
|
2251
|
+
heights[detailsOf] += child.offsetHeight;
|
|
2252
|
+
}
|
|
2253
|
+
continue;
|
|
2254
|
+
}
|
|
2255
|
+
// with drag and drop enabled the row sits inside a wrapper
|
|
2256
|
+
const row = (child.id ? child : child.firstElementChild);
|
|
2257
|
+
if (row?.id && row.offsetHeight) {
|
|
2258
|
+
heights[row.id] = row.offsetHeight;
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
let changed = false;
|
|
2262
|
+
for (const id of Object.keys(heights)) {
|
|
2263
|
+
if (Math.abs((this.measuredHeights[id] || ROW_HEIGHT) - heights[id]) > 0.5) {
|
|
2264
|
+
this.measuredHeights[id] = heights[id];
|
|
2265
|
+
changed = true;
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
if (changed) {
|
|
2269
|
+
// the rows kept their content, only the calculated positions move
|
|
2270
|
+
this.rowOffsetsInvalid = true;
|
|
2271
|
+
this.updateRowsOnly();
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2039
2274
|
/**
|
|
2040
2275
|
* All column widths as CSS variables of the table container. The header and every row read the
|
|
2041
2276
|
* widths from here (see `colWidth`), so they can never get out of sync, and the resizer can
|
|
@@ -2162,14 +2397,8 @@ export class ObjectBrowserClass extends Component {
|
|
|
2162
2397
|
}
|
|
2163
2398
|
if (event.code === 'ArrowUp' || event.code === 'ArrowDown') {
|
|
2164
2399
|
event.preventDefault();
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
this.tableRef.current?.childNodes.forEach((node) => {
|
|
2168
|
-
const nodeId = node.id;
|
|
2169
|
-
if (nodeId) {
|
|
2170
|
-
ids.push(nodeId);
|
|
2171
|
-
}
|
|
2172
|
-
});
|
|
2400
|
+
// every row of the table, not only the rendered ones (see `getWindow`)
|
|
2401
|
+
const ids = this.flatRows.map(row => row.item.data.id).filter(id => !!id);
|
|
2173
2402
|
const idx = ids.indexOf(selectedId);
|
|
2174
2403
|
const newIdx = event.code === 'ArrowDown' ? idx + 1 : idx - 1;
|
|
2175
2404
|
const newId = ids[newIdx] || selectedId;
|
|
@@ -2342,19 +2571,38 @@ export class ObjectBrowserClass extends Component {
|
|
|
2342
2571
|
componentDidUpdate(prevProps) {
|
|
2343
2572
|
// the table is rendered only after the objects are loaded
|
|
2344
2573
|
this.observeContainerWidth();
|
|
2574
|
+
this.measureRenderedRows();
|
|
2345
2575
|
if (this.tableRef.current && this.selectFirst) {
|
|
2346
2576
|
this.scrollToItem(this.selectFirst);
|
|
2347
2577
|
}
|
|
2348
2578
|
this.reconcileNavigation(prevProps);
|
|
2349
2579
|
}
|
|
2580
|
+
/**
|
|
2581
|
+
* Scrolls to a row.
|
|
2582
|
+
*
|
|
2583
|
+
* The row does not have to be rendered: only the rows around the visible area are (see
|
|
2584
|
+
* `getWindow`), so the position is taken from the flat list of all rows and the row is brought
|
|
2585
|
+
* into the middle of the table by scrolling there.
|
|
2586
|
+
*/
|
|
2350
2587
|
scrollToItem(id) {
|
|
2351
2588
|
this.selectFirst = '';
|
|
2352
2589
|
const node = window.document.getElementById(id);
|
|
2353
|
-
node
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2590
|
+
if (node) {
|
|
2591
|
+
node.scrollIntoView({
|
|
2592
|
+
behavior: 'auto',
|
|
2593
|
+
block: 'center',
|
|
2594
|
+
inline: 'center',
|
|
2595
|
+
});
|
|
2596
|
+
return;
|
|
2597
|
+
}
|
|
2598
|
+
const container = this.tableRef.current;
|
|
2599
|
+
const index = this.flatRows.findIndex(row => row.item.data.id === id);
|
|
2600
|
+
if (!container || index === -1) {
|
|
2601
|
+
return;
|
|
2602
|
+
}
|
|
2603
|
+
const middle = this.rowOffsets[index] - Math.max(0, (this.viewportHeight - ROW_HEIGHT) / 2);
|
|
2604
|
+
container.scrollTop = Math.max(0, middle);
|
|
2605
|
+
// the scroll event moves the window, and the row is rendered with the next frame
|
|
2358
2606
|
}
|
|
2359
2607
|
onUpdate(valAck) {
|
|
2360
2608
|
this.props.socket
|
|
@@ -2396,11 +2644,35 @@ export class ObjectBrowserClass extends Component {
|
|
|
2396
2644
|
this.forceUpdate();
|
|
2397
2645
|
}
|
|
2398
2646
|
}
|
|
2647
|
+
/**
|
|
2648
|
+
* Renders the browser and every row again: whoever calls it has changed something that is not
|
|
2649
|
+
* visible in `state` or `props`, so the rows cannot keep their output
|
|
2650
|
+
*/
|
|
2651
|
+
forceUpdate(callback) {
|
|
2652
|
+
this.rowsInvalid = true;
|
|
2653
|
+
super.forceUpdate(callback);
|
|
2654
|
+
}
|
|
2655
|
+
/**
|
|
2656
|
+
* Renders the browser, but keeps the output of every row that did not change: a state changed
|
|
2657
|
+
* (see `rowStateVersion`), the table was scrolled or a row measured another height
|
|
2658
|
+
*/
|
|
2659
|
+
updateRowsOnly() {
|
|
2660
|
+
super.forceUpdate();
|
|
2661
|
+
}
|
|
2399
2662
|
/**
|
|
2400
2663
|
* The rendering method of this component.
|
|
2401
2664
|
*/
|
|
2402
2665
|
render() {
|
|
2403
|
-
|
|
2666
|
+
// Every row is built again, if anything changed that concerns all of them: `state`, `props`
|
|
2667
|
+
// or whatever a `forceUpdate()` stands for. Otherwise the render only moved the window or
|
|
2668
|
+
// showed new values, and the rows keep their output (see `renderRow`)
|
|
2669
|
+
const fullRender = this.rowsInvalid || this.state !== this.lastRenderedState || this.props !== this.lastRenderedProps;
|
|
2670
|
+
this.rowsInvalid = false;
|
|
2671
|
+
this.lastRenderedState = this.state;
|
|
2672
|
+
this.lastRenderedProps = this.props;
|
|
2673
|
+
if (fullRender) {
|
|
2674
|
+
this.renderEpoch++;
|
|
2675
|
+
}
|
|
2404
2676
|
if (this.unsubscribeTimer) {
|
|
2405
2677
|
clearTimeout(this.unsubscribeTimer);
|
|
2406
2678
|
}
|
|
@@ -2449,7 +2721,24 @@ export class ObjectBrowserClass extends Component {
|
|
|
2449
2721
|
this.applyStoredWidths();
|
|
2450
2722
|
this.calculateColumnsVisibility();
|
|
2451
2723
|
}
|
|
2452
|
-
|
|
2724
|
+
// Only the rows around the visible area are rendered - see `getWindow`. The rows that are
|
|
2725
|
+
// not rendered are replaced by two spacers, so the scrollbar keeps the size of the table
|
|
2726
|
+
if (fullRender || !this.flatRows.length) {
|
|
2727
|
+
// a state change cannot add or remove a row, and scrolling does not either
|
|
2728
|
+
this.buildFlatRows();
|
|
2729
|
+
}
|
|
2730
|
+
else if (this.rowOffsetsInvalid) {
|
|
2731
|
+
this.calculateRowOffsets();
|
|
2732
|
+
}
|
|
2733
|
+
const { first, last } = this.getWindow();
|
|
2734
|
+
this.windowFirst = first;
|
|
2735
|
+
this.windowLast = last;
|
|
2736
|
+
const items = [];
|
|
2737
|
+
for (let i = first; i < last; i++) {
|
|
2738
|
+
items.push(leaf.renderRow(this, this.flatRows[i].item, this.flatRows[i].isExpanded));
|
|
2739
|
+
}
|
|
2740
|
+
const spaceAbove = this.rowOffsets[first];
|
|
2741
|
+
const spaceBelow = this.rowOffsets[this.flatRows.length] - this.rowOffsets[last];
|
|
2453
2742
|
return (React.createElement(TabContainer, { key: this.props.dialogName, styles: { root: this.props.style } },
|
|
2454
2743
|
React.createElement("style", null, `
|
|
2455
2744
|
@keyframes newValueAnimation-light {
|
|
@@ -2487,9 +2776,11 @@ export class ObjectBrowserClass extends Component {
|
|
|
2487
2776
|
`),
|
|
2488
2777
|
React.createElement(TabHeader, null, this.getToolbar()),
|
|
2489
2778
|
React.createElement(TabContent, null,
|
|
2490
|
-
React.createElement(Box, { style: { ...styles.tableDiv, ...this.getColumnWidthVariables() }, ref: this.tableRef, onKeyDown: event => this.navigateKeyPress(event) },
|
|
2779
|
+
React.createElement(Box, { style: { ...styles.tableDiv, ...this.getColumnWidthVariables() }, ref: this.tableRef, onKeyDown: event => this.navigateKeyPress(event), onScroll: this.onTableScroll },
|
|
2491
2780
|
toolbar.renderHeader(this),
|
|
2492
|
-
|
|
2781
|
+
spaceAbove ? React.createElement("div", { style: { height: spaceAbove } }) : null,
|
|
2782
|
+
items,
|
|
2783
|
+
spaceBelow ? React.createElement("div", { style: { height: spaceBelow } }) : null)),
|
|
2493
2784
|
contextMenu.renderContextMenu(this),
|
|
2494
2785
|
dialogs.renderAliasMenu(this),
|
|
2495
2786
|
dialogs.renderToast(this),
|