@iobroker/json-config 9.0.23 → 9.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 +48 -3
- package/build/JsonConfigComponent/ConfigGeneric.d.ts +67 -5
- package/build/JsonConfigComponent/ConfigGeneric.js +164 -20
- package/build/JsonConfigComponent/ConfigGeneric.js.map +1 -1
- package/build/JsonConfigComponent/ConfigState.js +16 -1
- package/build/JsonConfigComponent/ConfigState.js.map +1 -1
- package/build/JsonConfigComponent/ConfigTabs.d.ts +9 -0
- package/build/JsonConfigComponent/ConfigTabs.js +37 -5
- package/build/JsonConfigComponent/ConfigTabs.js.map +1 -1
- package/build/JsonConfigComponent/index.d.ts +3 -0
- package/build/JsonConfigComponent/index.js +11 -0
- package/build/JsonConfigComponent/index.js.map +1 -1
- package/build/JsonConfigComponent/statePool.d.ts +60 -0
- package/build/JsonConfigComponent/statePool.js +172 -0
- package/build/JsonConfigComponent/statePool.js.map +1 -0
- package/build/types.d.ts +27 -0
- package/package.json +3 -3
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pool of the ioBroker states, on which the configuration elements depend (see the `dependsOnStates` attribute).
|
|
3
|
+
*
|
|
4
|
+
* Every state is subscribed only once, independent of how many elements use it (a table with 100 rows,
|
|
5
|
+
* that all depend on `adapter.0.info.connection`, produces exactly one subscription), and the value is
|
|
6
|
+
* cached, so an element that is mounted later gets it without an additional request.
|
|
7
|
+
*/
|
|
8
|
+
export class StatePool {
|
|
9
|
+
socket;
|
|
10
|
+
/** All currently subscribed state IDs */
|
|
11
|
+
states = new Map();
|
|
12
|
+
/** State IDs and the change handler of every owner (= configuration element) */
|
|
13
|
+
owners = new Map();
|
|
14
|
+
destroyed = false;
|
|
15
|
+
constructor(socket) {
|
|
16
|
+
this.socket = socket;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Subscribe an owner to the given state IDs. It replaces the IDs, that were requested by this owner before,
|
|
20
|
+
* because an ID can be built with a `${data.xxx}` pattern and so it can change with the data.
|
|
21
|
+
*
|
|
22
|
+
* The promise is resolved as soon as the values of all given states are known, so that the first
|
|
23
|
+
* calculation of `hidden`/`disabled` does not run with unknown values (and a button does not flicker
|
|
24
|
+
* from enabled to disabled).
|
|
25
|
+
*
|
|
26
|
+
* @param owner the configuration element, that requires the states
|
|
27
|
+
* @param ids resolved state IDs
|
|
28
|
+
* @param onChange called if one of the states changed
|
|
29
|
+
*/
|
|
30
|
+
subscribe = async (owner, ids, onChange) => {
|
|
31
|
+
if (this.destroyed) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const wanted = [...new Set(ids.filter(id => id))];
|
|
35
|
+
const registered = this.owners.get(owner);
|
|
36
|
+
// Nothing to subscribe if this owner is already subscribed to exactly these states
|
|
37
|
+
if (registered && registered.ids.length === wanted.length && registered.ids.every(id => wanted.includes(id))) {
|
|
38
|
+
registered.onChange = onChange;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
this.owners.set(owner, { ids: wanted, onChange });
|
|
42
|
+
// Release the IDs, that are not required by this owner anymore
|
|
43
|
+
registered?.ids.filter(id => !wanted.includes(id)).forEach(id => this.releaseId(owner, id));
|
|
44
|
+
const toSubscribe = [];
|
|
45
|
+
wanted.forEach(id => {
|
|
46
|
+
const entry = this.states.get(id);
|
|
47
|
+
if (entry) {
|
|
48
|
+
entry.owners.add(owner);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
this.states.set(id, { owners: new Set([owner]), value: null, loaded: false });
|
|
52
|
+
toSubscribe.push(id);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
if (toSubscribe.length) {
|
|
56
|
+
const request = this.requestStates(toSubscribe);
|
|
57
|
+
toSubscribe.forEach(id => {
|
|
58
|
+
const entry = this.states.get(id);
|
|
59
|
+
if (entry) {
|
|
60
|
+
entry.pending = request;
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Wait for all running requests, also for those, that were started by another element
|
|
66
|
+
const pending = wanted.map(id => this.states.get(id)?.pending).filter(p => !!p);
|
|
67
|
+
if (pending.length) {
|
|
68
|
+
await Promise.all(pending);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
/** Unsubscribe an owner from all its states */
|
|
72
|
+
unsubscribe = (owner) => {
|
|
73
|
+
const registered = this.owners.get(owner);
|
|
74
|
+
if (!registered) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
this.owners.delete(owner);
|
|
78
|
+
registered.ids.forEach(id => this.releaseId(owner, id));
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Get the last known value of a state.
|
|
82
|
+
*
|
|
83
|
+
* @param id resolved state ID
|
|
84
|
+
* @returns `undefined` if the state was not read yet, `null` if it does not exist
|
|
85
|
+
*/
|
|
86
|
+
getValue = (id) => {
|
|
87
|
+
const entry = this.states.get(id);
|
|
88
|
+
if (!entry?.loaded) {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
return entry.value;
|
|
92
|
+
};
|
|
93
|
+
/** Release all subscriptions. The pool cannot be used afterwards */
|
|
94
|
+
destroy() {
|
|
95
|
+
this.destroyed = true;
|
|
96
|
+
const ids = [...this.states.keys()];
|
|
97
|
+
this.states.clear();
|
|
98
|
+
this.owners.clear();
|
|
99
|
+
if (ids.length) {
|
|
100
|
+
try {
|
|
101
|
+
this.socket.unsubscribeState(ids, this.onStateChanged);
|
|
102
|
+
}
|
|
103
|
+
catch (e) {
|
|
104
|
+
console.error(`[JsonConfigComponent] Cannot unsubscribe from ${ids.join(', ')}: ${e}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Subscribe on the given IDs. `subscribeState` reports the current values of all existing states
|
|
110
|
+
* before it resolves, so no additional `getState` request is required. States, that were not reported,
|
|
111
|
+
* do not exist and stay `null`.
|
|
112
|
+
*/
|
|
113
|
+
async requestStates(ids) {
|
|
114
|
+
try {
|
|
115
|
+
await this.socket.subscribeState(ids, this.onStateChanged);
|
|
116
|
+
}
|
|
117
|
+
catch (e) {
|
|
118
|
+
console.error(`[JsonConfigComponent] Cannot subscribe on ${ids.join(', ')}: ${e}`);
|
|
119
|
+
}
|
|
120
|
+
ids.forEach(id => {
|
|
121
|
+
const entry = this.states.get(id);
|
|
122
|
+
if (entry) {
|
|
123
|
+
entry.pending = undefined;
|
|
124
|
+
entry.loaded = true;
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
releaseId(owner, id) {
|
|
129
|
+
const entry = this.states.get(id);
|
|
130
|
+
if (!entry) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
entry.owners.delete(owner);
|
|
134
|
+
if (!entry.owners.size) {
|
|
135
|
+
this.states.delete(id);
|
|
136
|
+
try {
|
|
137
|
+
this.socket.unsubscribeState(id, this.onStateChanged);
|
|
138
|
+
}
|
|
139
|
+
catch (e) {
|
|
140
|
+
console.error(`[JsonConfigComponent] Cannot unsubscribe from ${id}: ${e}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* The new value is always stored, but the owners are only informed if `val`, `ack` or `q` changed.
|
|
146
|
+
* A repeated write of the same value (only `ts` changes) does not trigger a recalculation of the GUI.
|
|
147
|
+
*/
|
|
148
|
+
onStateChanged = (id, state) => {
|
|
149
|
+
const entry = this.states.get(id);
|
|
150
|
+
if (!entry) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const value = state ?? null;
|
|
154
|
+
const changed = !entry.loaded ||
|
|
155
|
+
entry.value?.val !== value?.val ||
|
|
156
|
+
entry.value?.ack !== value?.ack ||
|
|
157
|
+
entry.value?.q !== value?.q;
|
|
158
|
+
entry.value = value;
|
|
159
|
+
entry.loaded = true;
|
|
160
|
+
if (changed) {
|
|
161
|
+
entry.owners.forEach(owner => {
|
|
162
|
+
try {
|
|
163
|
+
this.owners.get(owner)?.onChange();
|
|
164
|
+
}
|
|
165
|
+
catch (e) {
|
|
166
|
+
console.error(`[JsonConfigComponent] Cannot process the change of ${id}: ${e}`);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=statePool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"statePool.js","sourceRoot":"src/","sources":["JsonConfigComponent/statePool.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AdminConnection } from '@iobroker/gui-components';\n\n/**\n * Value of a state, that was requested with the `dependsOnStates` attribute:\n * - `undefined` - the state was not read yet,\n * - `null` - the state does not exist,\n * - else the state object.\n */\nexport type SubscribedState = ioBroker.State | null | undefined;\n\ninterface StatePoolEntry {\n /** Components, that requested this state. The subscription is released with the last one */\n owners: Set<object>;\n /** Last known value. `null` if the state does not exist */\n value: ioBroker.State | null;\n /** True as soon as the first value (or the information, that the state does not exist) was received */\n loaded: boolean;\n /** Running subscription request. Every owner of this ID awaits it, so no element is calculated with an unknown value */\n pending?: Promise<void>;\n}\n\n/**\n * Pool of the ioBroker states, on which the configuration elements depend (see the `dependsOnStates` attribute).\n *\n * Every state is subscribed only once, independent of how many elements use it (a table with 100 rows,\n * that all depend on `adapter.0.info.connection`, produces exactly one subscription), and the value is\n * cached, so an element that is mounted later gets it without an additional request.\n */\nexport class StatePool {\n private readonly socket: AdminConnection;\n /** All currently subscribed state IDs */\n private readonly states = new Map<string, StatePoolEntry>();\n /** State IDs and the change handler of every owner (= configuration element) */\n private readonly owners = new Map<object, { ids: string[]; onChange: () => void }>();\n private destroyed = false;\n\n constructor(socket: AdminConnection) {\n this.socket = socket;\n }\n\n /**\n * Subscribe an owner to the given state IDs. It replaces the IDs, that were requested by this owner before,\n * because an ID can be built with a `${data.xxx}` pattern and so it can change with the data.\n *\n * The promise is resolved as soon as the values of all given states are known, so that the first\n * calculation of `hidden`/`disabled` does not run with unknown values (and a button does not flicker\n * from enabled to disabled).\n *\n * @param owner the configuration element, that requires the states\n * @param ids resolved state IDs\n * @param onChange called if one of the states changed\n */\n subscribe = async (owner: object, ids: string[], onChange: () => void): Promise<void> => {\n if (this.destroyed) {\n return;\n }\n const wanted = [...new Set(ids.filter(id => id))];\n const registered = this.owners.get(owner);\n\n // Nothing to subscribe if this owner is already subscribed to exactly these states\n if (registered && registered.ids.length === wanted.length && registered.ids.every(id => wanted.includes(id))) {\n registered.onChange = onChange;\n } else {\n this.owners.set(owner, { ids: wanted, onChange });\n\n // Release the IDs, that are not required by this owner anymore\n registered?.ids.filter(id => !wanted.includes(id)).forEach(id => this.releaseId(owner, id));\n\n const toSubscribe: string[] = [];\n wanted.forEach(id => {\n const entry = this.states.get(id);\n if (entry) {\n entry.owners.add(owner);\n } else {\n this.states.set(id, { owners: new Set([owner]), value: null, loaded: false });\n toSubscribe.push(id);\n }\n });\n\n if (toSubscribe.length) {\n const request = this.requestStates(toSubscribe);\n toSubscribe.forEach(id => {\n const entry = this.states.get(id);\n if (entry) {\n entry.pending = request;\n }\n });\n }\n }\n\n // Wait for all running requests, also for those, that were started by another element\n const pending = wanted.map(id => this.states.get(id)?.pending).filter(p => !!p);\n if (pending.length) {\n await Promise.all(pending);\n }\n };\n\n /** Unsubscribe an owner from all its states */\n unsubscribe = (owner: object): void => {\n const registered = this.owners.get(owner);\n if (!registered) {\n return;\n }\n this.owners.delete(owner);\n registered.ids.forEach(id => this.releaseId(owner, id));\n };\n\n /**\n * Get the last known value of a state.\n *\n * @param id resolved state ID\n * @returns `undefined` if the state was not read yet, `null` if it does not exist\n */\n getValue = (id: string): SubscribedState => {\n const entry = this.states.get(id);\n if (!entry?.loaded) {\n return undefined;\n }\n return entry.value;\n };\n\n /** Release all subscriptions. The pool cannot be used afterwards */\n destroy(): void {\n this.destroyed = true;\n const ids = [...this.states.keys()];\n this.states.clear();\n this.owners.clear();\n if (ids.length) {\n try {\n this.socket.unsubscribeState(ids, this.onStateChanged);\n } catch (e) {\n console.error(`[JsonConfigComponent] Cannot unsubscribe from ${ids.join(', ')}: ${e as Error}`);\n }\n }\n }\n\n /**\n * Subscribe on the given IDs. `subscribeState` reports the current values of all existing states\n * before it resolves, so no additional `getState` request is required. States, that were not reported,\n * do not exist and stay `null`.\n */\n private async requestStates(ids: string[]): Promise<void> {\n try {\n await this.socket.subscribeState(ids, this.onStateChanged);\n } catch (e) {\n console.error(`[JsonConfigComponent] Cannot subscribe on ${ids.join(', ')}: ${e as Error}`);\n }\n ids.forEach(id => {\n const entry = this.states.get(id);\n if (entry) {\n entry.pending = undefined;\n entry.loaded = true;\n }\n });\n }\n\n private releaseId(owner: object, id: string): void {\n const entry = this.states.get(id);\n if (!entry) {\n return;\n }\n entry.owners.delete(owner);\n if (!entry.owners.size) {\n this.states.delete(id);\n try {\n this.socket.unsubscribeState(id, this.onStateChanged);\n } catch (e) {\n console.error(`[JsonConfigComponent] Cannot unsubscribe from ${id}: ${e as Error}`);\n }\n }\n }\n\n /**\n * The new value is always stored, but the owners are only informed if `val`, `ack` or `q` changed.\n * A repeated write of the same value (only `ts` changes) does not trigger a recalculation of the GUI.\n */\n private onStateChanged = (id: string, state: ioBroker.State | null | undefined): void => {\n const entry = this.states.get(id);\n if (!entry) {\n return;\n }\n const value = state ?? null;\n const changed =\n !entry.loaded ||\n entry.value?.val !== value?.val ||\n entry.value?.ack !== value?.ack ||\n entry.value?.q !== value?.q;\n\n entry.value = value;\n entry.loaded = true;\n\n if (changed) {\n entry.owners.forEach(owner => {\n try {\n this.owners.get(owner)?.onChange();\n } catch (e) {\n console.error(`[JsonConfigComponent] Cannot process the change of ${id}: ${e as Error}`);\n }\n });\n }\n };\n}\n"]}
|
package/build/types.d.ts
CHANGED
|
@@ -216,6 +216,23 @@ export interface ConfigItem {
|
|
|
216
216
|
docker?: boolean;
|
|
217
217
|
/** JS function to calculate if the control is disabled. You can write "true" too */
|
|
218
218
|
disabled?: string | boolean;
|
|
219
|
+
/**
|
|
220
|
+
* ioBroker states, on which this element depends: `{ "<alias>": "<state ID>" }`.
|
|
221
|
+
*
|
|
222
|
+
* The states are subscribed, and if one of them changes, `hidden`, `disabled`, `label`, `help`, `validator`
|
|
223
|
+
* and `defaultFunc` will be calculated anew. The values are available in all JS functions and in all
|
|
224
|
+
* `${...}` patterns as `_states.<alias>`, and they contain the whole state object (`_states.running?.val`,
|
|
225
|
+
* `_states.running?.ts`, ...). `_states.<alias>` is `null` if the state does not exist.
|
|
226
|
+
*
|
|
227
|
+
* A state ID, that starts with a dot, addresses the own instance: `.info.connection` => `adapter.0.info.connection`.
|
|
228
|
+
* Every other ID is used as it is, so states of other adapters can be used too. `${data.xxx}` patterns
|
|
229
|
+
* are allowed in the ID, wildcards are not.
|
|
230
|
+
*
|
|
231
|
+
* The short array form `["adapter.0.info.connection"]` uses the ID itself as an alias.
|
|
232
|
+
*
|
|
233
|
+
* @example { "running": ".info.running" } together with `disabled: "_states.running?.val === true"`
|
|
234
|
+
*/
|
|
235
|
+
dependsOnStates?: Record<string, string> | string[];
|
|
219
236
|
/** Help text of the control */
|
|
220
237
|
help?: ioBroker.StringOrTranslated;
|
|
221
238
|
/** Link that will be opened by clicking on the help text */
|
|
@@ -1312,6 +1329,16 @@ export type JsonConfigContext = {
|
|
|
1312
1329
|
onValueChange?: (attr: string, value: any, saveConfig: boolean) => void;
|
|
1313
1330
|
registerOnForceUpdate?: (attr: string, cb?: (data: any) => void) => void;
|
|
1314
1331
|
getCachedObject?: (id: string) => Promise<ioBroker.Object | null>;
|
|
1332
|
+
/**
|
|
1333
|
+
* Subscribe an element (`owner`) on the given state IDs (see the `dependsOnStates` attribute).
|
|
1334
|
+
* It replaces the IDs, this element was subscribed to before, and it is resolved as soon as the
|
|
1335
|
+
* values of all these states are known.
|
|
1336
|
+
*/
|
|
1337
|
+
subscribeStates?: (owner: object, ids: string[], onChange: () => void) => Promise<void>;
|
|
1338
|
+
/** Unsubscribe an element from all states, it was subscribed to */
|
|
1339
|
+
unsubscribeStates?: (owner: object) => void;
|
|
1340
|
+
/** Last known value of a subscribed state. `undefined` - not read yet, `null` - the state does not exist */
|
|
1341
|
+
getStateValue?: (id: string) => ioBroker.State | null | undefined;
|
|
1315
1342
|
/** Information about the host, on which the configured instance runs */
|
|
1316
1343
|
hostInfo?: JsonConfigHostInfo;
|
|
1317
1344
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iobroker/json-config",
|
|
3
3
|
"description": "This package contains the ioBroker JSON config UI components",
|
|
4
|
-
"version": "9.0
|
|
4
|
+
"version": "9.1.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "bluefox",
|
|
7
7
|
"email": "dogafox@gmail.com"
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
"@emotion/styled": "^11.14.1",
|
|
40
40
|
"@iobroker/gui-components": "^10.2.0",
|
|
41
41
|
"@module-federation/runtime": "^2.9.0",
|
|
42
|
-
"@mui/icons-material": "^9.
|
|
43
|
-
"@mui/material": "^9.
|
|
42
|
+
"@mui/icons-material": "^9.4.0",
|
|
43
|
+
"@mui/material": "^9.4.0",
|
|
44
44
|
"@mui/x-date-pickers": "^9.12.0",
|
|
45
45
|
"crypto-js": "^4.2.0",
|
|
46
46
|
"json5": "^2.2.3",
|