@iobroker/json-config 6.17.13 → 6.17.14
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/package.json +27 -27
- package/src/JsonConfig.tsx +0 -710
- package/src/JsonConfigComponent/ChipInput.tsx +0 -752
- package/src/JsonConfigComponent/ConfigAccordion.tsx +0 -278
- package/src/JsonConfigComponent/ConfigAlive.tsx +0 -74
- package/src/JsonConfigComponent/ConfigAutocomplete.tsx +0 -108
- package/src/JsonConfigComponent/ConfigAutocompleteSendTo.tsx +0 -183
- package/src/JsonConfigComponent/ConfigCRON.jsx +0 -101
- package/src/JsonConfigComponent/ConfigCertCollection.tsx +0 -102
- package/src/JsonConfigComponent/ConfigCertificateSelect.tsx +0 -92
- package/src/JsonConfigComponent/ConfigCertificates.tsx +0 -202
- package/src/JsonConfigComponent/ConfigCheckLicense.jsx +0 -662
- package/src/JsonConfigComponent/ConfigCheckbox.tsx +0 -67
- package/src/JsonConfigComponent/ConfigChip.jsx +0 -81
- package/src/JsonConfigComponent/ConfigColor.tsx +0 -86
- package/src/JsonConfigComponent/ConfigCoordinates.tsx +0 -234
- package/src/JsonConfigComponent/ConfigCustom.tsx +0 -246
- package/src/JsonConfigComponent/ConfigDatePicker.tsx +0 -48
- package/src/JsonConfigComponent/ConfigDeviceManager.tsx +0 -33
- package/src/JsonConfigComponent/ConfigFile.jsx +0 -181
- package/src/JsonConfigComponent/ConfigFileSelector.jsx +0 -520
- package/src/JsonConfigComponent/ConfigFunc.jsx +0 -90
- package/src/JsonConfigComponent/ConfigGeneric.tsx +0 -1027
- package/src/JsonConfigComponent/ConfigIP.jsx +0 -96
- package/src/JsonConfigComponent/ConfigImageSendTo.jsx +0 -79
- package/src/JsonConfigComponent/ConfigImageUpload.jsx +0 -114
- package/src/JsonConfigComponent/ConfigInstanceSelect.jsx +0 -172
- package/src/JsonConfigComponent/ConfigInterface.jsx +0 -112
- package/src/JsonConfigComponent/ConfigJsonEditor.jsx +0 -103
- package/src/JsonConfigComponent/ConfigLanguage.tsx +0 -153
- package/src/JsonConfigComponent/ConfigLicense.jsx +0 -148
- package/src/JsonConfigComponent/ConfigNumber.tsx +0 -207
- package/src/JsonConfigComponent/ConfigObjectId.jsx +0 -113
- package/src/JsonConfigComponent/ConfigPanel.tsx +0 -360
- package/src/JsonConfigComponent/ConfigPassword.jsx +0 -160
- package/src/JsonConfigComponent/ConfigPattern.jsx +0 -50
- package/src/JsonConfigComponent/ConfigPort.tsx +0 -232
- package/src/JsonConfigComponent/ConfigRoom.jsx +0 -90
- package/src/JsonConfigComponent/ConfigSelect.jsx +0 -124
- package/src/JsonConfigComponent/ConfigSelectSendTo.tsx +0 -251
- package/src/JsonConfigComponent/ConfigSendto.tsx +0 -340
- package/src/JsonConfigComponent/ConfigSetState.jsx +0 -116
- package/src/JsonConfigComponent/ConfigSlider.jsx +0 -97
- package/src/JsonConfigComponent/ConfigStaticDivider.jsx +0 -51
- package/src/JsonConfigComponent/ConfigStaticHeader.jsx +0 -63
- package/src/JsonConfigComponent/ConfigStaticImage.jsx +0 -48
- package/src/JsonConfigComponent/ConfigStaticText.jsx +0 -72
- package/src/JsonConfigComponent/ConfigTable.tsx +0 -1040
- package/src/JsonConfigComponent/ConfigTabs.tsx +0 -150
- package/src/JsonConfigComponent/ConfigText.tsx +0 -188
- package/src/JsonConfigComponent/ConfigTextSendTo.tsx +0 -102
- package/src/JsonConfigComponent/ConfigTimePicker.tsx +0 -63
- package/src/JsonConfigComponent/ConfigTopic.jsx +0 -78
- package/src/JsonConfigComponent/ConfigUUID.tsx +0 -55
- package/src/JsonConfigComponent/ConfigUser.jsx +0 -104
- package/src/JsonConfigComponent/index.tsx +0 -435
- package/src/JsonConfigComponent/wrapper/Components/CustomModal.jsx +0 -145
- package/src/JsonConfigComponent/wrapper/Components/Editor.jsx +0 -65
- package/src/Utils.jsx +0 -1683
- package/src/index.tsx +0 -14
- package/src/types.d.ts +0 -372
package/src/JsonConfig.tsx
DELETED
|
@@ -1,710 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { type Styles, withStyles } from '@mui/styles';
|
|
3
|
-
import JSON5 from 'json5';
|
|
4
|
-
import MD5 from 'crypto-js/md5';
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
Fab,
|
|
8
|
-
Tooltip,
|
|
9
|
-
LinearProgress,
|
|
10
|
-
} from '@mui/material';
|
|
11
|
-
import { Publish as PublishIcon } from '@mui/icons-material';
|
|
12
|
-
|
|
13
|
-
import {
|
|
14
|
-
I18n,
|
|
15
|
-
Router,
|
|
16
|
-
SaveCloseButtons,
|
|
17
|
-
Theme as theme,
|
|
18
|
-
Confirm as ConfirmDialog,
|
|
19
|
-
AdminConnection,
|
|
20
|
-
} from '@iobroker/adapter-react-v5';
|
|
21
|
-
|
|
22
|
-
import type { Theme, ThemeName, ThemeType } from '@iobroker/adapter-react-v5/types';
|
|
23
|
-
import type { ConfigItemAny, ConfigItemPanel, ConfigItemTabs } from '#JC/types';
|
|
24
|
-
import Utils from '#JC/Utils';
|
|
25
|
-
import ConfigGeneric from './JsonConfigComponent/ConfigGeneric';
|
|
26
|
-
import JsonConfigComponent from './JsonConfigComponent';
|
|
27
|
-
|
|
28
|
-
const styles = {
|
|
29
|
-
root: {
|
|
30
|
-
width: '100%',
|
|
31
|
-
height: '100%',
|
|
32
|
-
overflow: 'hidden',
|
|
33
|
-
position: 'relative',
|
|
34
|
-
},
|
|
35
|
-
scroll: {
|
|
36
|
-
height: 'calc(100% - 48px)',
|
|
37
|
-
overflowY: 'auto',
|
|
38
|
-
},
|
|
39
|
-
exportImportButtons: {
|
|
40
|
-
position: 'absolute',
|
|
41
|
-
top: 5,
|
|
42
|
-
right: 0,
|
|
43
|
-
zIndex: 3,
|
|
44
|
-
},
|
|
45
|
-
button: {
|
|
46
|
-
marginRight: 5,
|
|
47
|
-
},
|
|
48
|
-
} satisfies Styles<any, any>;
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Decrypt the password/value with given key
|
|
52
|
-
* @param key - Secret key
|
|
53
|
-
* @param value - value to decrypt
|
|
54
|
-
*/
|
|
55
|
-
function decryptLegacy(key: string, value: string): string {
|
|
56
|
-
let result = '';
|
|
57
|
-
for (let i = 0; i < value.length; i++) {
|
|
58
|
-
// eslint-disable-next-line no-bitwise
|
|
59
|
-
result += String.fromCharCode(key[i % key.length].charCodeAt(0) ^ value.charCodeAt(i));
|
|
60
|
-
}
|
|
61
|
-
return result;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Encrypt the password/value with given key
|
|
66
|
-
* @param key - Secret key
|
|
67
|
-
* @param value - value to encrypt
|
|
68
|
-
*/
|
|
69
|
-
function encryptLegacy(key: string, value: string): string {
|
|
70
|
-
let result = '';
|
|
71
|
-
for (let i = 0; i < value.length; i++) {
|
|
72
|
-
// eslint-disable-next-line no-bitwise
|
|
73
|
-
result += String.fromCharCode(key[i % key.length].charCodeAt(0) ^ value.charCodeAt(i));
|
|
74
|
-
}
|
|
75
|
-
return result;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Decrypt the password/value with given key
|
|
80
|
-
* Usage:
|
|
81
|
-
* ```js
|
|
82
|
-
* function load(settings, onChange) {
|
|
83
|
-
* if (settings.password) {
|
|
84
|
-
* settings.password = decrypt(systemSecret, settings.password);
|
|
85
|
-
* // same as
|
|
86
|
-
* settings.password = decrypt(settings.password);
|
|
87
|
-
* }
|
|
88
|
-
* // ...
|
|
89
|
-
* }
|
|
90
|
-
* ```
|
|
91
|
-
* @param key - Secret key
|
|
92
|
-
* @param value - value to decrypt
|
|
93
|
-
*/
|
|
94
|
-
function decrypt(key: string, value: string): string {
|
|
95
|
-
if (typeof value !== 'string') {
|
|
96
|
-
return value;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// if not encrypted as aes-192 or key not a valid 48-digit hex -> fallback
|
|
100
|
-
if (!value.startsWith('$/aes-192-cbc:') || !/^[0-9a-f]{48}$/.test(key)) {
|
|
101
|
-
return decryptLegacy(key, value);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// algorithm:iv:encryptedValue
|
|
105
|
-
const textParts = value.split(':', 3);
|
|
106
|
-
|
|
107
|
-
const _key = window.CryptoJS.enc.Hex.parse(key);
|
|
108
|
-
const iv = window.CryptoJS.enc.Hex.parse(textParts[1]);
|
|
109
|
-
|
|
110
|
-
const cipherParams = window.CryptoJS.lib.CipherParams.create({ ciphertext: window.CryptoJS.enc.Hex.parse(textParts[2]) });
|
|
111
|
-
|
|
112
|
-
const decryptedBinary = window.CryptoJS.AES.decrypt(cipherParams, _key, { iv });
|
|
113
|
-
|
|
114
|
-
return window.CryptoJS.enc.Utf8.stringify(decryptedBinary);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Encrypt the password/value with given key
|
|
119
|
-
* Usage:
|
|
120
|
-
* ```
|
|
121
|
-
* function save(callback) {
|
|
122
|
-
* ...
|
|
123
|
-
* if (obj.password) {
|
|
124
|
-
* obj.password = encrypt(systemSecret, obj.password);
|
|
125
|
-
* // same as
|
|
126
|
-
* obj.password = decrypt(obj.password);
|
|
127
|
-
* }
|
|
128
|
-
* ...
|
|
129
|
-
* }
|
|
130
|
-
* ```
|
|
131
|
-
* @param key - Secret key
|
|
132
|
-
* @param value - value to encrypt
|
|
133
|
-
* @param _iv - optional initial vector for tests
|
|
134
|
-
*/
|
|
135
|
-
function encrypt(key: string, value: string, _iv?: string): string {
|
|
136
|
-
if (typeof value !== 'string') {
|
|
137
|
-
return value;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (!/^[0-9a-f]{48}$/.test(key)) {
|
|
141
|
-
// key length is not matching for AES-192-CBC or key is no valid hex - fallback to old encryption
|
|
142
|
-
return encryptLegacy(key, value);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
let iv;
|
|
146
|
-
if (_iv) {
|
|
147
|
-
iv = window.CryptoJS.enc.Hex.parse(_iv);
|
|
148
|
-
} else {
|
|
149
|
-
iv = window.CryptoJS.lib.WordArray.random(128 / 8);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const _key = window.CryptoJS.enc.Hex.parse(key);
|
|
153
|
-
const encrypted = window.CryptoJS.AES.encrypt(value, _key, { iv }).ciphertext;
|
|
154
|
-
|
|
155
|
-
return `$/aes-192-cbc:${window.CryptoJS.enc.Hex.stringify(iv)}:${encrypted}`;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function loadScript(src: string, id: string) {
|
|
159
|
-
if (!id || !document.getElementById(id)) {
|
|
160
|
-
return new Promise(resolve => {
|
|
161
|
-
const script = document.createElement('script');
|
|
162
|
-
script.setAttribute('id', id);
|
|
163
|
-
script.onload = resolve;
|
|
164
|
-
script.src = src;
|
|
165
|
-
document.getElementsByTagName('head')[0].appendChild(script);
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
return document.getElementById(id)?.onload;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
interface BufferObject {
|
|
172
|
-
type: 'Buffer';
|
|
173
|
-
data: Buffer;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
interface JsonConfigProps {
|
|
177
|
-
menuPadding: number;
|
|
178
|
-
adapterName: string;
|
|
179
|
-
instance: number;
|
|
180
|
-
isFloatComma: boolean;
|
|
181
|
-
dateFormat: string;
|
|
182
|
-
secret: string;
|
|
183
|
-
socket: AdminConnection;
|
|
184
|
-
theme: Record<string, any>;
|
|
185
|
-
themeName: ThemeName;
|
|
186
|
-
themeType: ThemeType;
|
|
187
|
-
/** CSS classes */
|
|
188
|
-
classes: Record<string, any>;
|
|
189
|
-
/** Translate method */
|
|
190
|
-
t: typeof I18n.t;
|
|
191
|
-
configStored: (notChanged: boolean) => void;
|
|
192
|
-
width: 'xs' | 'sm' | 'md';
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
interface JsonConfigState {
|
|
196
|
-
schema?: ConfigItemPanel | ConfigItemTabs;
|
|
197
|
-
data?: Record<string, unknown>;
|
|
198
|
-
originalData?: Record<string, unknown>;
|
|
199
|
-
updateData: number;
|
|
200
|
-
common?: ioBroker.InstanceCommon;
|
|
201
|
-
changed: boolean;
|
|
202
|
-
confirmDialog: boolean;
|
|
203
|
-
theme: Theme;
|
|
204
|
-
saveConfigDialog: boolean;
|
|
205
|
-
hash: string;
|
|
206
|
-
error?: boolean;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
class JsonConfig extends Router<JsonConfigProps, JsonConfigState> {
|
|
210
|
-
private fileSubscribed: string[] = [];
|
|
211
|
-
|
|
212
|
-
private fileLangSubscribed = '';
|
|
213
|
-
|
|
214
|
-
private secret = '';
|
|
215
|
-
|
|
216
|
-
constructor(props: JsonConfigProps) {
|
|
217
|
-
super(props);
|
|
218
|
-
|
|
219
|
-
this.state = {
|
|
220
|
-
updateData: 0,
|
|
221
|
-
changed: false,
|
|
222
|
-
confirmDialog: false,
|
|
223
|
-
theme: theme(props.themeName), // buttons require special theme
|
|
224
|
-
saveConfigDialog: false,
|
|
225
|
-
hash: '_',
|
|
226
|
-
};
|
|
227
|
-
|
|
228
|
-
this.getInstanceObject()
|
|
229
|
-
.then(obj => this.getConfigFile()
|
|
230
|
-
.then(schema =>
|
|
231
|
-
// load language
|
|
232
|
-
// @ts-expect-error it has the static method
|
|
233
|
-
JsonConfigComponent.loadI18n(this.props.socket, schema?.i18n, this.props.adapterName)
|
|
234
|
-
.then((langFileName: string) => {
|
|
235
|
-
if (langFileName) {
|
|
236
|
-
// subscribe on changes
|
|
237
|
-
if (!this.fileLangSubscribed) {
|
|
238
|
-
this.fileLangSubscribed = langFileName;
|
|
239
|
-
this.props.socket.subscribeFiles(`${this.props.adapterName}.admin`, this.fileLangSubscribed, this.onFileChange);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
if (obj) {
|
|
244
|
-
this.setState({
|
|
245
|
-
schema,
|
|
246
|
-
data: obj.native,
|
|
247
|
-
common: obj.common,
|
|
248
|
-
// @ts-expect-error really no string?
|
|
249
|
-
hash: MD5(JSON.stringify(schema)),
|
|
250
|
-
});
|
|
251
|
-
} else {
|
|
252
|
-
window.alert(`Instance system.adapter.${this.props.adapterName}.${this.props.instance} not found!`);
|
|
253
|
-
}
|
|
254
|
-
})));
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
async componentWillUnmount(): Promise<void> {
|
|
258
|
-
super.componentWillUnmount();
|
|
259
|
-
if (this.fileSubscribed.length) {
|
|
260
|
-
this.props.socket.unsubscribeFiles(`${this.props.adapterName}.admin`, this.fileSubscribed, this.onFileChange);
|
|
261
|
-
this.fileSubscribed = [];
|
|
262
|
-
}
|
|
263
|
-
if (this.fileLangSubscribed) {
|
|
264
|
-
this.props.socket.unsubscribeFiles(`${this.props.adapterName}.admin`, this.fileLangSubscribed, this.onFileChange);
|
|
265
|
-
this.fileLangSubscribed = '';
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* @private
|
|
271
|
-
* @param evt
|
|
272
|
-
*/
|
|
273
|
-
handleFileSelect = (evt: Record<string, any>): void => {
|
|
274
|
-
const f = evt.target.files[0];
|
|
275
|
-
if (f) {
|
|
276
|
-
const r = new FileReader();
|
|
277
|
-
r.onload = async e => {
|
|
278
|
-
if (!e.target) {
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
const contents = e.target.result as string;
|
|
283
|
-
try {
|
|
284
|
-
const data = JSON.parse(contents);
|
|
285
|
-
this.setState({ data, changed: JSON.stringify(data) !== JSON.stringify(this.state.originalData) });
|
|
286
|
-
} catch (err) {
|
|
287
|
-
window.alert(I18n.t('[JsonConfig] Failed to parse JSON file'));
|
|
288
|
-
}
|
|
289
|
-
};
|
|
290
|
-
r.readAsText(f);
|
|
291
|
-
} else {
|
|
292
|
-
window.alert(I18n.t('[JsonConfig] Failed to open JSON File'));
|
|
293
|
-
}
|
|
294
|
-
};
|
|
295
|
-
|
|
296
|
-
getExportImportButtons(): React.JSX.Element {
|
|
297
|
-
return <div className={this.props.classes.exportImportButtons}>
|
|
298
|
-
<Tooltip title={this.props.t('Import settings from JSON file')}>
|
|
299
|
-
<Fab
|
|
300
|
-
size="small"
|
|
301
|
-
classes={{ root: this.props.classes.button }}
|
|
302
|
-
onClick={() => {
|
|
303
|
-
const input = document.createElement('input');
|
|
304
|
-
input.setAttribute('type', 'file');
|
|
305
|
-
input.setAttribute('id', 'files');
|
|
306
|
-
// @ts-expect-error check
|
|
307
|
-
input.setAttribute('opacity', 0);
|
|
308
|
-
input.addEventListener('change', e => this.handleFileSelect(e), false);
|
|
309
|
-
input.click();
|
|
310
|
-
}}
|
|
311
|
-
>
|
|
312
|
-
<PublishIcon />
|
|
313
|
-
</Fab>
|
|
314
|
-
</Tooltip>
|
|
315
|
-
<Tooltip title={this.props.t('Export setting to JSON file')}>
|
|
316
|
-
<Fab
|
|
317
|
-
size="small"
|
|
318
|
-
classes={{ root: this.props.classes.button }}
|
|
319
|
-
onClick={() => {
|
|
320
|
-
if (!this.state.data) {
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
Utils.generateFile(`${this.props.adapterName}.${this.props.instance}.json`, this.state.data);
|
|
325
|
-
}}
|
|
326
|
-
>
|
|
327
|
-
<PublishIcon style={{ transform: 'rotate(180deg)' }} />
|
|
328
|
-
</Fab>
|
|
329
|
-
</Tooltip>
|
|
330
|
-
</div>;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
onFileChange = async (id: string, fileName: string, size: number): Promise<void> => {
|
|
334
|
-
if (id === `${this.props.adapterName}.admin` && size) {
|
|
335
|
-
if (fileName === this.fileLangSubscribed) {
|
|
336
|
-
try {
|
|
337
|
-
// @ts-expect-error needs types
|
|
338
|
-
await JsonConfigComponent.loadI18n(this.props.socket, this.state.schema?.i18n, this.props.adapterName);
|
|
339
|
-
this.setState({ hash: `${this.state.hash}1` });
|
|
340
|
-
} catch {
|
|
341
|
-
// ignore errors
|
|
342
|
-
}
|
|
343
|
-
} else if (this.fileSubscribed.includes(fileName)) {
|
|
344
|
-
try {
|
|
345
|
-
const schema = await this.getConfigFile(this.fileSubscribed[0]);
|
|
346
|
-
// @ts-expect-error really no string?
|
|
347
|
-
this.setState({ schema, hash: MD5(JSON.stringify(schema)) });
|
|
348
|
-
} catch {
|
|
349
|
-
// ignore errors
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
};
|
|
354
|
-
|
|
355
|
-
async getInstanceObject(): Promise<ioBroker.InstanceObject | null> {
|
|
356
|
-
try {
|
|
357
|
-
const obj = await this.props.socket.getObject(`system.adapter.${this.props.adapterName}.${this.props.instance}`);
|
|
358
|
-
// decode all native attributes listed in obj.encryptedNative
|
|
359
|
-
if (Array.isArray(obj.encryptedNative)) {
|
|
360
|
-
const systemConfig = await this.props.socket.getSystemConfig();
|
|
361
|
-
await loadScript('../../lib/js/crypto-js/crypto-js.js', 'crypto-js');
|
|
362
|
-
this.secret = systemConfig.native.secret;
|
|
363
|
-
obj.encryptedNative?.forEach(attr => {
|
|
364
|
-
if (obj.native[attr]) {
|
|
365
|
-
obj.native[attr] = decrypt(this.secret, obj.native[attr]);
|
|
366
|
-
}
|
|
367
|
-
});
|
|
368
|
-
return obj;
|
|
369
|
-
}
|
|
370
|
-
return obj;
|
|
371
|
-
} catch (e) {
|
|
372
|
-
window.alert(`[JsonConfig] Cannot read instance object: ${e}`);
|
|
373
|
-
}
|
|
374
|
-
return null;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
renderConfirmDialog(): React.JSX.Element | null {
|
|
378
|
-
if (!this.state.confirmDialog) {
|
|
379
|
-
return null;
|
|
380
|
-
}
|
|
381
|
-
return <ConfirmDialog
|
|
382
|
-
title={I18n.t('ra_Please confirm')}
|
|
383
|
-
text={I18n.t('ra_Some data are not stored. Discard?')}
|
|
384
|
-
ok={I18n.t('ra_Discard')}
|
|
385
|
-
cancel={I18n.t('ra_Cancel')}
|
|
386
|
-
onClose={isYes =>
|
|
387
|
-
this.setState({ confirmDialog: false }, () => isYes && Router.doNavigate(null))}
|
|
388
|
-
/>;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
async scanForInclude(json: Record<string, any>, filePaths: string[]): Promise<Record<string, any>> {
|
|
392
|
-
if (typeof json['#include'] === 'string') {
|
|
393
|
-
// load file
|
|
394
|
-
const data = await this._getConfigFile(json['#include'], [...filePaths]);
|
|
395
|
-
delete json['#include'];
|
|
396
|
-
if (data) {
|
|
397
|
-
// merge data
|
|
398
|
-
json = { ...json, ...data };
|
|
399
|
-
}
|
|
400
|
-
return json;
|
|
401
|
-
}
|
|
402
|
-
const keys = Object.keys(json);
|
|
403
|
-
for (let k = 0; k < keys.length; k++) {
|
|
404
|
-
if (typeof json[keys[k]] === 'object') {
|
|
405
|
-
json[keys[k]] = await this.scanForInclude(json[keys[k]], filePaths);
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
return json;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
async getConfigFile(fileName?: string): Promise<ConfigItemPanel | ConfigItemTabs> {
|
|
412
|
-
return this._getConfigFile(fileName);
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
async _getConfigFile(fileName?: string, _filePaths?: string[]): Promise<ConfigItemPanel | ConfigItemTabs> {
|
|
416
|
-
fileName = fileName || 'jsonConfig.json5';
|
|
417
|
-
_filePaths = _filePaths || [];
|
|
418
|
-
|
|
419
|
-
if (_filePaths.includes(fileName)) {
|
|
420
|
-
window.alert(`[JsonConfig] Circular reference in file: ${fileName} => ${_filePaths.join(' => ')}`);
|
|
421
|
-
return null;
|
|
422
|
-
}
|
|
423
|
-
_filePaths.push(fileName);
|
|
424
|
-
|
|
425
|
-
try {
|
|
426
|
-
const exist = await this.props.socket.fileExists(`${this.props.adapterName}.admin`, fileName);
|
|
427
|
-
if (!exist) {
|
|
428
|
-
fileName = 'jsonConfig.json';
|
|
429
|
-
}
|
|
430
|
-
const data: {
|
|
431
|
-
file: string;
|
|
432
|
-
mimeType: string;
|
|
433
|
-
} = await this.props.socket.readFile(`${this.props.adapterName}.admin`, fileName);
|
|
434
|
-
let content = '';
|
|
435
|
-
let file: string | BufferObject = '';
|
|
436
|
-
|
|
437
|
-
if (data.file !== undefined) {
|
|
438
|
-
file = data.file;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
if (typeof file === 'string') {
|
|
442
|
-
content = file;
|
|
443
|
-
// @ts-expect-error revisit
|
|
444
|
-
} else if (file.type === 'Buffer') {
|
|
445
|
-
let binary = '';
|
|
446
|
-
// @ts-expect-error revisit
|
|
447
|
-
const bytes = new Uint8Array(file.data);
|
|
448
|
-
const len = bytes.byteLength;
|
|
449
|
-
for (let i = 0; i < len; i++) {
|
|
450
|
-
binary += String.fromCharCode(bytes[i]);
|
|
451
|
-
}
|
|
452
|
-
content = binary;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
// subscribe on changes
|
|
456
|
-
if (!this.fileSubscribed.includes(fileName)) {
|
|
457
|
-
this.fileSubscribed.push(fileName);
|
|
458
|
-
await this.props.socket.subscribeFiles(`${this.props.adapterName}.admin`, fileName, this.onFileChange);
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
try {
|
|
462
|
-
// detect #include attr
|
|
463
|
-
return (await this.scanForInclude(JSON5.parse(content), _filePaths)) as (ConfigItemPanel | ConfigItemTabs);
|
|
464
|
-
} catch (e) {
|
|
465
|
-
window.alert('[JsonConfig] Cannot parse json5 config!');
|
|
466
|
-
console.log(e);
|
|
467
|
-
}
|
|
468
|
-
} catch (e1) {
|
|
469
|
-
!this.state.schema && window.alert(`[JsonConfig] Cannot read file "${fileName}: ${e1}`);
|
|
470
|
-
}
|
|
471
|
-
return null;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
renderSaveConfigDialog(): React.JSX.Element | null {
|
|
475
|
-
if (!this.state.saveConfigDialog) {
|
|
476
|
-
return null;
|
|
477
|
-
}
|
|
478
|
-
return <ConfirmDialog
|
|
479
|
-
title={I18n.t('ra_Please confirm')}
|
|
480
|
-
text={I18n.t('Save configuration?')}
|
|
481
|
-
ok={I18n.t('ra_Save')}
|
|
482
|
-
cancel={I18n.t('ra_Cancel')}
|
|
483
|
-
onClose={isYes =>
|
|
484
|
-
this.setState({ saveConfigDialog: false }, () => isYes && this.onSave(true))}
|
|
485
|
-
/>;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
findAttr(attr: string, schema?: ConfigItemPanel | ConfigItemTabs): ConfigItemAny | null {
|
|
489
|
-
schema = schema || this.state.schema;
|
|
490
|
-
if (schema?.items) {
|
|
491
|
-
if (attr in schema.items) {
|
|
492
|
-
return schema.items[attr] as ConfigItemAny;
|
|
493
|
-
}
|
|
494
|
-
for (const _item of Object.values(schema.items)) {
|
|
495
|
-
const item = this.findAttr(attr, _item as ConfigItemPanel | ConfigItemTabs);
|
|
496
|
-
if (item) {
|
|
497
|
-
return item;
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
return null;
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
// this function is called recursively and trims all text fields, that must be trimmed
|
|
506
|
-
postProcessing(data: Record<string, unknown>, attr: string, schema: ConfigItemAny): void {
|
|
507
|
-
schema = schema || this.state.schema;
|
|
508
|
-
if (!data) {
|
|
509
|
-
// should not happen
|
|
510
|
-
console.error(`Data is empty in postProcessing: ${attr}, ${JSON.stringify(schema)}`);
|
|
511
|
-
return;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
const dataAttr = data[attr];
|
|
515
|
-
|
|
516
|
-
if ((schema as ConfigItemTabs).items) {
|
|
517
|
-
if (schema.type === 'table') {
|
|
518
|
-
const table = dataAttr;
|
|
519
|
-
|
|
520
|
-
if (!Array.isArray(table)) {
|
|
521
|
-
return;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
for (const entry of table) {
|
|
525
|
-
for (const tItem of schema.items) {
|
|
526
|
-
this.postProcessing(entry, tItem.attr as string, tItem as ConfigItemAny);
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
} else {
|
|
530
|
-
for (const [_attr, item] of Object.entries((schema as ConfigItemTabs).items)) {
|
|
531
|
-
if ((item as any).type === 'panel' || (item as any).type === 'tabs' || (item as any).type === 'accordion') {
|
|
532
|
-
return;
|
|
533
|
-
}
|
|
534
|
-
this.postProcessing(data, _attr, item);
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
} else if (attr && typeof dataAttr === 'string') {
|
|
538
|
-
// postprocessing
|
|
539
|
-
if (schema.type === 'text') {
|
|
540
|
-
if (schema.trim !== false) {
|
|
541
|
-
data[attr] = dataAttr.trim();
|
|
542
|
-
}
|
|
543
|
-
} else if (schema.type === 'ip') {
|
|
544
|
-
// should not happen
|
|
545
|
-
data[attr] = dataAttr.trim();
|
|
546
|
-
} else if (schema.type === 'number') {
|
|
547
|
-
const dataVal = parseFloat(dataAttr.toString().replace(',', '.'));
|
|
548
|
-
|
|
549
|
-
if (schema.min !== undefined && dataVal < schema.min) {
|
|
550
|
-
data[attr] = schema.min;
|
|
551
|
-
} else if (schema.max !== undefined && dataVal > schema.max) {
|
|
552
|
-
data[attr] = schema.max;
|
|
553
|
-
} else {
|
|
554
|
-
data[attr] = dataVal;
|
|
555
|
-
}
|
|
556
|
-
} else if (schema.type === 'port') {
|
|
557
|
-
const dataVal = parseInt(dataAttr.toString(), 10);
|
|
558
|
-
if (schema.min !== undefined && dataVal < schema.min) {
|
|
559
|
-
data[attr] = schema.min;
|
|
560
|
-
} else if (schema.max !== undefined && dataVal > schema.max) {
|
|
561
|
-
data[attr] = schema.max;
|
|
562
|
-
}
|
|
563
|
-
if (data[attr] !== 0 && dataVal < 20) {
|
|
564
|
-
data[attr] = 20;
|
|
565
|
-
} else if (dataVal > 0xFFFF) {
|
|
566
|
-
data[attr] = 0xFFFF;
|
|
567
|
-
} else {
|
|
568
|
-
data[attr] = dataVal;
|
|
569
|
-
}
|
|
570
|
-
} else if (schema.type === 'checkbox') {
|
|
571
|
-
// should not happen
|
|
572
|
-
data[attr] = data[attr] === true || data[attr] === 'true' || data[attr] === 'on' || data[attr] === 1 || data[attr] === '1';
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
async onSave(doSave: boolean, close?: boolean): Promise<void> {
|
|
578
|
-
if (doSave) {
|
|
579
|
-
const obj = await this.getInstanceObject();
|
|
580
|
-
|
|
581
|
-
if (!obj) {
|
|
582
|
-
console.error('Something went wrong: may be no connection?');
|
|
583
|
-
window.alert('Something went wrong: may be no connection?');
|
|
584
|
-
return;
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
if (!this.state.data || !this.state.schema) {
|
|
588
|
-
return;
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
const doNotSaveAttributes: Record<string, any> = {};
|
|
592
|
-
|
|
593
|
-
for (const attr of Object.keys(this.state.data)) {
|
|
594
|
-
const item = this.findAttr(attr);
|
|
595
|
-
if ((!item || !item.doNotSave) && !attr.startsWith('_')) {
|
|
596
|
-
ConfigGeneric.setValue(obj.native, attr, this.state.data[attr]);
|
|
597
|
-
} else {
|
|
598
|
-
ConfigGeneric.setValue(obj.native, attr, null);
|
|
599
|
-
doNotSaveAttributes[attr] = this.state.data[attr];
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
try {
|
|
604
|
-
const encryptedObj = JSON.parse(JSON.stringify(obj));
|
|
605
|
-
// encode all native attributes listed in obj.encryptedNative
|
|
606
|
-
if (Array.isArray(encryptedObj.encryptedNative)) {
|
|
607
|
-
await loadScript('../../lib/js/crypto-js/crypto-js.js', 'crypto-js');
|
|
608
|
-
|
|
609
|
-
for (const attr of encryptedObj.encryptedNative) {
|
|
610
|
-
if (encryptedObj.native[attr]) {
|
|
611
|
-
encryptedObj.native[attr] = encrypt(this.secret, encryptedObj.native[attr]);
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
await this.props.socket.setObject(encryptedObj._id, encryptedObj);
|
|
617
|
-
} catch (e) {
|
|
618
|
-
window.alert(`[JsonConfig] Cannot set object: ${e}`);
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
/** We want to preserve the doNotSaveAttributes too, just not save it */
|
|
622
|
-
const nativeWithNonSaved = { ...obj.native, ...doNotSaveAttributes };
|
|
623
|
-
console.log(nativeWithNonSaved);
|
|
624
|
-
|
|
625
|
-
this.setState({
|
|
626
|
-
changed: false,
|
|
627
|
-
data: nativeWithNonSaved,
|
|
628
|
-
updateData: this.state.updateData + 1,
|
|
629
|
-
originalData: nativeWithNonSaved,
|
|
630
|
-
}, () =>
|
|
631
|
-
close && Router.doNavigate(null));
|
|
632
|
-
} else if (this.state.changed) {
|
|
633
|
-
this.setState({ confirmDialog: true });
|
|
634
|
-
} else {
|
|
635
|
-
Router.doNavigate(null);
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
componentDidUpdate(_prevProps: JsonConfigProps, prevState: JsonConfigState): void {
|
|
640
|
-
if (prevState.changed !== this.state.changed) {
|
|
641
|
-
this.props.configStored(!this.state.changed);
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
/**
|
|
646
|
-
* Validate the JSON config once on mount
|
|
647
|
-
*/
|
|
648
|
-
async componentDidMount() {
|
|
649
|
-
const link = `${window.location.protocol}//${window.location.host}${window.location.pathname}validate_config/${this.props.adapterName}`;
|
|
650
|
-
console.log(`fetch ${link}`);
|
|
651
|
-
await fetch(link);
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
render(): React.JSX.Element {
|
|
655
|
-
const { classes } = this.props;
|
|
656
|
-
if (!this.state.data || !this.state.schema) {
|
|
657
|
-
return <LinearProgress />;
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
return <div className={this.props.classes.root}>
|
|
661
|
-
{this.renderConfirmDialog()}
|
|
662
|
-
{this.getExportImportButtons()}
|
|
663
|
-
{this.renderSaveConfigDialog()}
|
|
664
|
-
<JsonConfigComponent
|
|
665
|
-
key={this.state.hash as string}
|
|
666
|
-
className={classes.scroll}
|
|
667
|
-
socket={this.props.socket}
|
|
668
|
-
themeName={this.props.themeName}
|
|
669
|
-
themeType={this.props.themeType}
|
|
670
|
-
adapterName={this.props.adapterName}
|
|
671
|
-
instance={this.props.instance}
|
|
672
|
-
isFloatComma={this.props.isFloatComma}
|
|
673
|
-
dateFormat={this.props.dateFormat}
|
|
674
|
-
schema={this.state.schema}
|
|
675
|
-
common={this.state.common}
|
|
676
|
-
data={this.state.data}
|
|
677
|
-
updateData={this.state.updateData}
|
|
678
|
-
onError={error => this.setState({ error })}
|
|
679
|
-
onChange={(data, changed, saveConfigDialog) => {
|
|
680
|
-
if (saveConfigDialog && this.state.error) {
|
|
681
|
-
window.alert(I18n.t('Cannot save configuration because of error in configuration'));
|
|
682
|
-
saveConfigDialog = false;
|
|
683
|
-
}
|
|
684
|
-
if (saveConfigDialog && !this.state.changed && !changed) {
|
|
685
|
-
saveConfigDialog = false;
|
|
686
|
-
}
|
|
687
|
-
if (data) {
|
|
688
|
-
this.setState({ data, changed, saveConfigDialog });
|
|
689
|
-
} else if (saveConfigDialog !== undefined) {
|
|
690
|
-
this.setState({ saveConfigDialog });
|
|
691
|
-
}
|
|
692
|
-
}}
|
|
693
|
-
/>
|
|
694
|
-
<SaveCloseButtons
|
|
695
|
-
isIFrame={false}
|
|
696
|
-
dense
|
|
697
|
-
paddingLeft={0}
|
|
698
|
-
newReact
|
|
699
|
-
theme={this.state.theme}
|
|
700
|
-
noTextOnButtons={this.props.width === 'xs' || this.props.width === 'sm' || this.props.width === 'md'}
|
|
701
|
-
changed={!!(this.state.error || this.state.changed)}
|
|
702
|
-
error={!!this.state.error}
|
|
703
|
-
onSave={(close: any) => this.onSave(true, close)}
|
|
704
|
-
onClose={() => this.onSave(false)}
|
|
705
|
-
/>
|
|
706
|
-
</div>;
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
export default withStyles(styles)(JsonConfig);
|