@iobroker/adapter-react-v5 6.1.9 → 7.0.1

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.
Files changed (210) hide show
  1. package/Components/404.js +13 -13
  2. package/Components/FileBrowser.js +24 -19
  3. package/Components/FileViewer.js +14 -5
  4. package/Components/Loader.js +223 -223
  5. package/Components/Loaders/PT.css +108 -108
  6. package/Components/Loaders/PT.js +103 -103
  7. package/Components/Loaders/Vendor.css +13 -13
  8. package/Components/Loaders/Vendor.js +7 -7
  9. package/Components/ObjectBrowser.d.ts +2 -0
  10. package/Components/ObjectBrowser.js +214 -115
  11. package/Components/UploadImage.js +305 -305
  12. package/Components/loader.css +221 -221
  13. package/Components/types.d.ts +82 -82
  14. package/GenericApp.js +49 -49
  15. package/LICENSE +22 -22
  16. package/Prompt.js +7 -7
  17. package/README.md +1004 -1008
  18. package/Theme.js +8 -7
  19. package/assets/devices/Alarm Systems.svg +18 -18
  20. package/assets/devices/Amplifier.svg +21 -21
  21. package/assets/devices/Awnings.svg +4 -4
  22. package/assets/devices/Battery Status.svg +4 -4
  23. package/assets/devices/Ceiling Spotlights.svg +15 -15
  24. package/assets/devices/Chandelier.svg +6 -6
  25. package/assets/devices/Climate.svg +11 -11
  26. package/assets/devices/Coffee Makers.svg +5 -5
  27. package/assets/devices/Cold Water.svg +31 -31
  28. package/assets/devices/Computer.svg +21 -21
  29. package/assets/devices/Consumption.svg +7 -7
  30. package/assets/devices/Curtains.svg +43 -43
  31. package/assets/devices/Dishwashers.svg +11 -11
  32. package/assets/devices/Doors.svg +5 -5
  33. package/assets/devices/Doorstep.svg +35 -35
  34. package/assets/devices/Dryer.svg +13 -13
  35. package/assets/devices/Fan.svg +20 -20
  36. package/assets/devices/Floor Lamps.svg +4 -4
  37. package/assets/devices/Garage Doors.svg +9 -9
  38. package/assets/devices/Gates.svg +32 -32
  39. package/assets/devices/Hairdryer.svg +23 -23
  40. package/assets/devices/Handle.svg +6 -6
  41. package/assets/devices/Hanging Lamps.svg +8 -8
  42. package/assets/devices/Heater.svg +44 -44
  43. package/assets/devices/Hoods.svg +11 -11
  44. package/assets/devices/Hot Water.svg +9 -9
  45. package/assets/devices/Humidity.svg +41 -41
  46. package/assets/devices/Iron.svg +4 -4
  47. package/assets/devices/Irrigation.svg +22 -22
  48. package/assets/devices/Led Strip.svg +30 -30
  49. package/assets/devices/Light.svg +29 -29
  50. package/assets/devices/Lightings.svg +46 -46
  51. package/assets/devices/Lock.svg +19 -19
  52. package/assets/devices/Louvre.svg +6 -6
  53. package/assets/devices/Mowing Machine.svg +8 -8
  54. package/assets/devices/Music.svg +12 -12
  55. package/assets/devices/Outdoor Blinds.svg +6 -6
  56. package/assets/devices/People.svg +19 -19
  57. package/assets/devices/Pool.svg +7 -7
  58. package/assets/devices/Power Consumption.svg +12 -12
  59. package/assets/devices/Printer.svg +9 -9
  60. package/assets/devices/Pump.svg +9 -9
  61. package/assets/devices/Receiver.svg +18 -18
  62. package/assets/devices/Sconces.svg +9 -9
  63. package/assets/devices/Security.svg +34 -34
  64. package/assets/devices/Shading.svg +4 -4
  65. package/assets/devices/Shutters.svg +10 -10
  66. package/assets/devices/SmokeDetector.svg +12 -12
  67. package/assets/devices/Sockets.svg +13 -13
  68. package/assets/devices/Speaker.svg +35 -35
  69. package/assets/devices/Stove.svg +11 -11
  70. package/assets/devices/Table Lamps.svg +11 -11
  71. package/assets/devices/Temperature Sensors.svg +28 -28
  72. package/assets/devices/Tv.svg +7 -7
  73. package/assets/devices/Vacuum Cleaner.svg +15 -15
  74. package/assets/devices/Ventilation.svg +12 -12
  75. package/assets/devices/Washing Machines.svg +15 -15
  76. package/assets/devices/Water Consumption.svg +5 -5
  77. package/assets/devices/Water Heater.svg +8 -8
  78. package/assets/devices/Water.svg +40 -40
  79. package/assets/devices/Weather.svg +28 -28
  80. package/assets/devices/Window.svg +7 -7
  81. package/assets/lamp_ceiling.svg +8 -8
  82. package/assets/lamp_table.svg +7 -7
  83. package/assets/no_icon.svg +9 -9
  84. package/assets/rooms/Anteroom.svg +52 -52
  85. package/assets/rooms/Attic.svg +21 -21
  86. package/assets/rooms/Balcony.svg +12 -12
  87. package/assets/rooms/Barn.svg +5 -5
  88. package/assets/rooms/Basement.svg +4 -4
  89. package/assets/rooms/Bathroom.svg +38 -38
  90. package/assets/rooms/Bedroom.svg +5 -5
  91. package/assets/rooms/Boiler Room.svg +12 -12
  92. package/assets/rooms/Carport.svg +17 -17
  93. package/assets/rooms/Cellar.svg +89 -89
  94. package/assets/rooms/Chamber.svg +9 -9
  95. package/assets/rooms/Corridor.svg +52 -52
  96. package/assets/rooms/Dining Area.svg +37 -37
  97. package/assets/rooms/Dining Room.svg +37 -37
  98. package/assets/rooms/Dining.svg +37 -37
  99. package/assets/rooms/Dressing Room.svg +4 -4
  100. package/assets/rooms/Driveway.svg +14 -14
  101. package/assets/rooms/Entrance.svg +44 -44
  102. package/assets/rooms/Equipment Room.svg +14 -14
  103. package/assets/rooms/Front Yard.svg +64 -64
  104. package/assets/rooms/Gallery.svg +13 -13
  105. package/assets/rooms/Garage.svg +20 -20
  106. package/assets/rooms/Garden.svg +12 -12
  107. package/assets/rooms/Ground Floor.svg +95 -95
  108. package/assets/rooms/Guest Bathroom.svg +32 -32
  109. package/assets/rooms/Guest Room.svg +5 -5
  110. package/assets/rooms/Gym.svg +4 -4
  111. package/assets/rooms/Hall.svg +19 -19
  112. package/assets/rooms/Home Theater.svg +7 -7
  113. package/assets/rooms/Kitchen.svg +17 -17
  114. package/assets/rooms/Laundry Room.svg +11 -11
  115. package/assets/rooms/Living Area.svg +10 -10
  116. package/assets/rooms/Living Room.svg +10 -10
  117. package/assets/rooms/Locker Room.svg +16 -16
  118. package/assets/rooms/Nursery.svg +4 -4
  119. package/assets/rooms/Office.svg +8 -8
  120. package/assets/rooms/Outdoors.svg +7 -7
  121. package/assets/rooms/Playroom.svg +5 -5
  122. package/assets/rooms/Pool.svg +7 -7
  123. package/assets/rooms/Rear Wall.svg +30 -30
  124. package/assets/rooms/Second Floor.svg +95 -95
  125. package/assets/rooms/Shed.svg +16 -16
  126. package/assets/rooms/Sleeping Area.svg +22 -22
  127. package/assets/rooms/Stairway.svg +4 -4
  128. package/assets/rooms/Stairwell.svg +15 -15
  129. package/assets/rooms/Storeroom.svg +4 -4
  130. package/assets/rooms/Summer House.svg +27 -27
  131. package/assets/rooms/Swimming Pool.svg +21 -21
  132. package/assets/rooms/Terrace.svg +6 -6
  133. package/assets/rooms/Toilet.svg +10 -10
  134. package/assets/rooms/Upstairs.svg +5 -5
  135. package/assets/rooms/Wardrobe.svg +60 -60
  136. package/assets/rooms/Washroom.svg +19 -19
  137. package/assets/rooms/Wc.svg +10 -10
  138. package/assets/rooms/Windscreen.svg +60 -60
  139. package/assets/rooms/Workshop.svg +22 -22
  140. package/assets/rooms/Workspace.svg +8 -8
  141. package/craco-module-federation.js +71 -71
  142. package/icons/IconFx.js +1 -1
  143. package/icons/IconLogout.js +1 -1
  144. package/index.css +54 -54
  145. package/modulefederation.admin.config.js +31 -31
  146. package/package.json +9 -9
  147. package/src/AdminConnection.tsx +3 -3
  148. package/src/Components/404.tsx +121 -121
  149. package/src/Components/ColorPicker.tsx +315 -315
  150. package/src/Components/ComplexCron.tsx +507 -507
  151. package/src/Components/CopyToClipboard.tsx +165 -165
  152. package/src/Components/CustomModal.tsx +163 -163
  153. package/src/Components/FileBrowser.tsx +2414 -2394
  154. package/src/Components/FileViewer.tsx +393 -384
  155. package/src/Components/Icon.tsx +210 -210
  156. package/src/Components/IconPicker.tsx +149 -149
  157. package/src/Components/IconSelector.tsx +2202 -2202
  158. package/src/Components/Image.tsx +176 -176
  159. package/src/Components/Loader.tsx +304 -304
  160. package/src/Components/Logo.tsx +166 -166
  161. package/src/Components/MDUtils.tsx +100 -100
  162. package/src/Components/ObjectBrowser.tsx +8032 -7915
  163. package/src/Components/Router.tsx +90 -90
  164. package/src/Components/SaveCloseButtons.tsx +113 -113
  165. package/src/Components/Schedule.tsx +1724 -1724
  166. package/src/Components/SelectWithIcon.tsx +197 -197
  167. package/src/Components/TabContainer.tsx +55 -55
  168. package/src/Components/TabContent.tsx +37 -37
  169. package/src/Components/TabHeader.tsx +19 -19
  170. package/src/Components/TableResize.tsx +259 -259
  171. package/src/Components/TextWithIcon.tsx +148 -148
  172. package/src/Components/ToggleThemeMenu.tsx +34 -34
  173. package/src/Components/TreeTable.tsx +919 -919
  174. package/src/Components/UploadImage.tsx +599 -599
  175. package/src/Components/Utils.tsx +1794 -1794
  176. package/src/Components/loader.css +221 -221
  177. package/src/Components/withWidth.tsx +21 -21
  178. package/src/Connection.tsx +7 -7
  179. package/src/Dialogs/ComplexCron.tsx +129 -129
  180. package/src/Dialogs/Confirm.tsx +162 -162
  181. package/src/Dialogs/Cron.tsx +182 -182
  182. package/src/Dialogs/Error.tsx +72 -72
  183. package/src/Dialogs/Message.tsx +71 -71
  184. package/src/Dialogs/SelectFile.tsx +270 -270
  185. package/src/Dialogs/SelectID.tsx +298 -298
  186. package/src/Dialogs/SimpleCron.tsx +100 -100
  187. package/src/Dialogs/TextInput.tsx +107 -107
  188. package/src/GenericApp.tsx +976 -976
  189. package/src/LegacyConnection.tsx +3589 -3589
  190. package/src/Prompt.tsx +20 -20
  191. package/src/Theme.tsx +479 -479
  192. package/src/icons/IconAdapter.tsx +20 -20
  193. package/src/icons/IconAlias.tsx +20 -20
  194. package/src/icons/IconChannel.tsx +21 -21
  195. package/src/icons/IconClearFilter.tsx +22 -22
  196. package/src/icons/IconClosed.tsx +17 -17
  197. package/src/icons/IconCopy.tsx +16 -16
  198. package/src/icons/IconDevice.tsx +27 -27
  199. package/src/icons/IconDocument.tsx +17 -17
  200. package/src/icons/IconDocumentReadOnly.tsx +18 -18
  201. package/src/icons/IconExpert.tsx +18 -18
  202. package/src/icons/IconFx.tsx +36 -36
  203. package/src/icons/IconInstance.tsx +20 -20
  204. package/src/icons/IconLogout.tsx +30 -30
  205. package/src/icons/IconNoIcon.tsx +19 -19
  206. package/src/icons/IconOpen.tsx +17 -17
  207. package/src/icons/IconProps.tsx +15 -15
  208. package/src/icons/IconState.tsx +17 -17
  209. package/src/index.css +54 -54
  210. package/types.d.ts +134 -134
@@ -1,976 +1,976 @@
1
- /**
2
- * Copyright 2018-2024 Denis Haev (bluefox) <dogafox@gmail.com>
3
- *
4
- * MIT License
5
- *
6
- * */
7
- import React from 'react';
8
- import { PROGRESS, Connection, AdminConnection } from '@iobroker/socket-client';
9
- import * as Sentry from '@sentry/browser';
10
-
11
- import { Snackbar, IconButton } from '@mui/material';
12
-
13
- import { Close as IconClose } from '@mui/icons-material';
14
-
15
- import printPrompt from './Prompt';
16
- import Theme from './Theme';
17
- import Loader from './Components/Loader';
18
- import Router from './Components/Router';
19
- import Utils from './Components/Utils';
20
- import SaveCloseButtons from './Components/SaveCloseButtons';
21
- import ConfirmDialog from './Dialogs/Confirm';
22
- import I18n from './i18n';
23
- import DialogError from './Dialogs/Error';
24
- import {
25
- GenericAppProps,
26
- GenericAppState,
27
- GenericAppSettings,
28
- ThemeName,
29
- ThemeType,
30
- IobTheme,
31
- Width,
32
- } from './types';
33
-
34
- declare global {
35
- /** If config has been changed */
36
- // eslint-disable-next-line no-var
37
- var changed: boolean;
38
-
39
- interface Window {
40
- io: any;
41
- SocketClient: any;
42
- adapterName: undefined | string;
43
- socketUrl: undefined | string;
44
- oldAlert: any;
45
- changed: boolean;
46
- $iframeDialog: {
47
- close?: () => void;
48
- };
49
- }
50
- }
51
-
52
- // import './index.css';
53
- const cssStyle = `
54
- html {
55
- height: 100%;
56
- }
57
-
58
- body {
59
- margin: 0;
60
- padding: 0;
61
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
62
- -webkit-font-smoothing: antialiased;
63
- -moz-osx-font-smoothing: grayscale;
64
- width: 100%;
65
- height: 100%;
66
- overflow: hidden;
67
- }
68
-
69
- /* scrollbar */
70
- ::-webkit-scrollbar-track {
71
- background-color: #ccc;
72
- border-radius: 5px;
73
- }
74
-
75
- ::-webkit-scrollbar {
76
- width: 5px;
77
- height: 5px;
78
- background-color: #ccc;
79
- }
80
-
81
- ::-webkit-scrollbar-thumb {
82
- background-color: #575757;
83
- border-radius: 5px;
84
- }
85
-
86
- #root {
87
- height: 100%;
88
- }
89
-
90
- .App {
91
- height: 100%;
92
- }
93
-
94
- @keyframes glow {
95
- from {
96
- background-color: initial;
97
- }
98
- to {
99
- background-color: #58c458;
100
- }
101
- }
102
- `;
103
-
104
- class GenericApp<TProps extends GenericAppProps = GenericAppProps, TState extends GenericAppState = GenericAppState> extends Router<TProps, TState> {
105
- protected socket: AdminConnection;
106
-
107
- protected readonly instance: number;
108
-
109
- protected readonly adapterName: string;
110
-
111
- protected readonly instanceId: string;
112
-
113
- protected readonly newReact: boolean;
114
-
115
- protected encryptedFields: string[];
116
-
117
- protected readonly sentryDSN: string | undefined;
118
-
119
- private alertDialogRendered: boolean;
120
-
121
- private _secret: string | undefined;
122
-
123
- protected _systemConfig: ioBroker.SystemConfigCommon | undefined;
124
-
125
- // it is not readonly
126
- private savedNative: Record<string, any>;
127
-
128
- protected common: ioBroker.InstanceCommon | null = null;
129
-
130
- private sentryStarted: boolean = false;
131
-
132
- private sentryInited: boolean = false;
133
-
134
- private resizeTimer: ReturnType<typeof setTimeout> | null = null;
135
-
136
- constructor(props: TProps, settings?: GenericAppSettings) {
137
- const ConnectionClass = (props.Connection || settings?.Connection || Connection) as unknown as typeof AdminConnection;
138
- // const ConnectionClass = props.Connection === 'admin' || settings.Connection = 'admin' ? AdminConnection : (props.Connection || settings.Connection || Connection);
139
-
140
- if (!window.document.getElementById('generic-app-iobroker-component')) {
141
- const style = window.document.createElement('style');
142
- style.setAttribute('id', 'generic-app-iobroker-component');
143
- style.innerHTML = cssStyle;
144
- window.document.head.appendChild(style);
145
- }
146
-
147
- // Remove `!Connection.isWeb() && window.adapterName !== 'material'` when iobroker.socket will support native ws
148
- if (!GenericApp.isWeb() && window.io && window.location.port === '3000') {
149
- try {
150
- const io = new window.SocketClient();
151
- delete window.io;
152
- window.io = io;
153
- } catch (e) {
154
- // ignore
155
- }
156
- }
157
-
158
- super(props);
159
-
160
- printPrompt();
161
-
162
- const query = (window.location.search || '').replace(/^\?/, '').replace(/#.*$/, '');
163
- const args: Record<string, string | boolean> = {};
164
- query.trim().split('&').filter(t => t.trim()).forEach(b => {
165
- const parts = b.split('=');
166
- args[parts[0]] = parts.length === 2 ? parts[1] : true;
167
- if (args[parts[0]] === 'true') {
168
- args[parts[0]] = true;
169
- } else if (args[parts[0]] === 'false') {
170
- args[parts[0]] = false;
171
- }
172
- });
173
-
174
- // extract instance from URL
175
- this.instance = settings?.instance ?? props.instance ?? (args.instance !== undefined ? parseInt(args.instance as string, 10) || 0 : (parseInt(window.location.search.slice(1), 10) || 0));
176
- // extract adapter name from URL
177
- const tmp = window.location.pathname.split('/');
178
- this.adapterName = settings?.adapterName || props.adapterName || window.adapterName || tmp[tmp.length - 2] || 'iot';
179
- this.instanceId = `system.adapter.${this.adapterName}.${this.instance}`;
180
- this.newReact = args.newReact === true; // it is admin5
181
-
182
- const location = Router.getLocation();
183
- location.tab = location.tab || ((window as any)._localStorage || window.localStorage).getItem(`${this.adapterName}-adapter`) || '';
184
-
185
- const themeInstance = this.createTheme();
186
-
187
- this.state = {
188
- ...(this.state || {}), // keep the existing state
189
- selectedTab: ((window as any)._localStorage || window.localStorage).getItem(`${this.adapterName}-adapter`) || '',
190
- selectedTabNum: -1,
191
- native: {},
192
- errorText: '',
193
- changed: false,
194
- connected: false,
195
- loaded: false,
196
- isConfigurationError: '',
197
- expertMode: false,
198
- toast: '',
199
- theme: themeInstance,
200
- themeName: this.getThemeName(themeInstance),
201
- themeType: this.getThemeType(themeInstance),
202
- bottomButtons: (settings && settings.bottomButtons) === false ? false : (props?.bottomButtons !== false),
203
- width: GenericApp.getWidth(),
204
- confirmClose: false,
205
- _alert: false,
206
- _alertType: 'info',
207
- _alertMessage: '',
208
- } satisfies GenericAppState as TState;
209
-
210
- // init translations
211
- const translations: Record<ioBroker.Languages, Record<string, string>> = {
212
- en: require('./i18n/en.json'),
213
- de: require('./i18n/de.json'),
214
- ru: require('./i18n/ru.json'),
215
- pt: require('./i18n/pt.json'),
216
- nl: require('./i18n/nl.json'),
217
- fr: require('./i18n/fr.json'),
218
- it: require('./i18n/it.json'),
219
- es: require('./i18n/es.json'),
220
- pl: require('./i18n/pl.json'),
221
- uk: require('./i18n/uk.json'),
222
- 'zh-cn': require('./i18n/zh-cn.json'),
223
- };
224
-
225
- // merge together
226
- if (settings && settings.translations) {
227
- Object.keys(settings.translations).forEach(lang => {
228
- if (settings.translations) {
229
- translations[lang as ioBroker.Languages] = Object.assign(translations[lang as ioBroker.Languages], settings.translations[lang as ioBroker.Languages] || {});
230
- }
231
- });
232
- } else if (props.translations) {
233
- Object.keys(props.translations).forEach(lang => {
234
- if (props.translations) {
235
- translations[lang as ioBroker.Languages] = Object.assign(translations[lang as ioBroker.Languages], props.translations[lang as ioBroker.Languages] || {});
236
- }
237
- });
238
- }
239
-
240
- I18n.setTranslations(translations);
241
-
242
- this.savedNative = {}; // to detect if the config changed
243
-
244
- this.encryptedFields = props.encryptedFields || settings?.encryptedFields || [];
245
-
246
- this.sentryDSN = (settings && settings.sentryDSN) || props.sentryDSN;
247
-
248
- if (window.socketUrl) {
249
- if (window.socketUrl.startsWith(':')) {
250
- window.socketUrl = `${window.location.protocol}//${window.location.hostname}${window.socketUrl}`;
251
- } else if (!window.socketUrl.startsWith('http://') && !window.socketUrl.startsWith('https://')) {
252
- window.socketUrl = `${window.location.protocol}//${window.socketUrl}`;
253
- }
254
- }
255
-
256
- this.alertDialogRendered = false;
257
-
258
- window.oldAlert = window.alert;
259
- window.alert = message => {
260
- if (!this.alertDialogRendered) {
261
- window.oldAlert(message);
262
- return;
263
- }
264
- if (message && message.toString().toLowerCase().includes('error')) {
265
- console.error(message);
266
- this.showAlert(message.toString(), 'error');
267
- } else {
268
- console.log(message);
269
- this.showAlert(message.toString(), 'info');
270
- }
271
- };
272
-
273
- // @ts-expect-error either make props in ConnectionProps required or the constructor needs to accept than as they are (means adapt socket-client)
274
- this.socket = new ConnectionClass({
275
- ...(props?.socket || settings?.socket),
276
- name: this.adapterName,
277
- doNotLoadAllObjects: settings?.doNotLoadAllObjects,
278
- onProgress: (progress: PROGRESS) => {
279
- if (progress === PROGRESS.CONNECTING) {
280
- this.setState({ connected: false });
281
- } else if (progress === PROGRESS.READY) {
282
- this.setState({ connected: true });
283
- } else {
284
- this.setState({ connected: true });
285
- }
286
- },
287
- onReady: (/* objects, scripts */) => {
288
- I18n.setLanguage(this.socket.systemLang);
289
-
290
- // subscribe because of language and expert mode
291
- this.socket.subscribeObject('system.config', this.onSystemConfigChanged)
292
- .then(() => this.getSystemConfig())
293
- .then(obj => {
294
- this._secret = (typeof obj !== 'undefined' && obj.native && obj.native.secret) || 'Zgfr56gFe87jJOM';
295
- this._systemConfig = obj?.common || ({} as ioBroker.SystemConfigCommon);
296
- return this.socket.getObject(this.instanceId);
297
- })
298
- .then(async obj => {
299
- let waitPromise;
300
- const instanceObj: ioBroker.InstanceObject | null | undefined = obj as ioBroker.InstanceObject | null | undefined;
301
-
302
- const sentryPluginEnabled = (await this.socket.getState(`${this.instanceId}.plugins.sentry.enabled`))?.val;
303
-
304
- const sentryEnabled =
305
- sentryPluginEnabled !== false &&
306
- this._systemConfig?.diag !== 'none' &&
307
- instanceObj?.common &&
308
- instanceObj.common.name &&
309
- instanceObj.common.version &&
310
- // @ts-expect-error will be extended in js-controller TODO: (BF: 2024.05.30) this is redundant to state `${this.instanceId}.plugins.sentry.enabled`, remove this in future when admin sets the state correctly
311
- !instanceObj.common.disableDataReporting &&
312
- window.location.host !== 'localhost:3000';
313
-
314
- // activate sentry plugin
315
- if (!this.sentryStarted && this.sentryDSN && sentryEnabled) {
316
- this.sentryStarted = true;
317
-
318
- Sentry.init({
319
- dsn: this.sentryDSN,
320
- release: `iobroker.${instanceObj.common.name}@${instanceObj.common.version}`,
321
- integrations: [
322
- Sentry.dedupeIntegration(),
323
- ],
324
- });
325
-
326
- console.log('Sentry initialized');
327
- }
328
-
329
- // read UUID and init sentry with it.
330
- // for backward compatibility it will be processed separately from the above logic: some adapters could still have this.sentryDSN as undefined
331
- if (!this.sentryInited && sentryEnabled) {
332
- this.sentryInited = true;
333
-
334
- waitPromise = this.socket.getObject('system.meta.uuid')
335
- .then(uuidObj => {
336
- if (uuidObj && uuidObj.native && uuidObj.native.uuid) {
337
- const scope = Sentry.getCurrentScope();
338
- scope.setUser({ id: uuidObj.native.uuid });
339
- }
340
- });
341
- }
342
-
343
- waitPromise = waitPromise || Promise.resolve();
344
-
345
- waitPromise
346
- .then(() => {
347
- if (instanceObj) {
348
- this.common = instanceObj?.common;
349
- this.onPrepareLoad(instanceObj.native, instanceObj.encryptedNative); // decode all secrets
350
- this.savedNative = JSON.parse(JSON.stringify(instanceObj.native));
351
- this.setState({ native: instanceObj.native, loaded: true, expertMode: this.getExpertMode() }, () =>
352
- this.onConnectionReady && this.onConnectionReady());
353
- } else {
354
- console.warn('Cannot load instance settings');
355
- this.setState(
356
- {
357
- native: {},
358
- loaded: true,
359
- expertMode: this.getExpertMode(),
360
- },
361
- () => this.onConnectionReady && this.onConnectionReady(),
362
- );
363
- }
364
- });
365
- })
366
- .catch(e => window.alert(`Cannot settings: ${e}`));
367
- },
368
- onError: (err: string) => {
369
- console.error(err);
370
- this.showError(err);
371
- },
372
- });
373
- }
374
-
375
- /**
376
- * Checks if this connection is running in a web adapter and not in an admin.
377
- * @returns True if running in a web adapter or in a socketio adapter.
378
- */
379
- static isWeb(): boolean {
380
- return window.socketUrl !== undefined;
381
- }
382
-
383
- showAlert(message: string, type?: 'info' | 'warning' | 'error' | 'success') {
384
- if (type !== 'error' && type !== 'warning' && type !== 'info' && type !== 'success') {
385
- type = 'info';
386
- }
387
-
388
- this.setState({
389
- _alert: true,
390
- _alertType: type,
391
- _alertMessage: message,
392
- });
393
- }
394
-
395
- renderAlertSnackbar() {
396
- this.alertDialogRendered = true;
397
-
398
- return <Snackbar
399
- style={this.state._alertType === 'error' ?
400
- { backgroundColor: '#f44336' } :
401
- (this.state._alertType === 'success' ? { backgroundColor: '#4caf50' } : undefined)}
402
- open={this.state._alert}
403
- autoHideDuration={6000}
404
- onClose={(_e, reason) => reason !== 'clickaway' && this.setState({ _alert: false })}
405
- message={this.state._alertMessage}
406
- />;
407
- }
408
-
409
- onSystemConfigChanged = (id: string, obj: ioBroker.AnyObject | null | undefined) => {
410
- if (obj && id === 'system.config') {
411
- if (this.socket.systemLang !== (obj as ioBroker.SystemConfigObject)?.common.language) {
412
- this.socket.systemLang = (obj as ioBroker.SystemConfigObject)?.common.language || 'en';
413
- I18n.setLanguage(this.socket.systemLang);
414
- }
415
-
416
- if (this._systemConfig?.expertMode !== !!(obj as ioBroker.SystemConfigObject)?.common?.expertMode) {
417
- this._systemConfig = (obj as ioBroker.SystemConfigObject)?.common || ({} as ioBroker.SystemConfigCommon);
418
- this.setState({ expertMode: this.getExpertMode() });
419
- } else {
420
- this._systemConfig = (obj as ioBroker.SystemConfigObject)?.common || ({} as ioBroker.SystemConfigCommon);
421
- }
422
- }
423
- };
424
-
425
- /**
426
- * Called immediately after a component is mounted. Setting state here will trigger re-rendering.
427
- */
428
- componentDidMount() {
429
- window.addEventListener('resize', this.onResize, true);
430
- window.addEventListener('message', this.onReceiveMessage, false);
431
- super.componentDidMount();
432
- }
433
-
434
- /**
435
- * Called immediately before a component is destroyed.
436
- */
437
- componentWillUnmount() {
438
- window.removeEventListener('resize', this.onResize, true);
439
- window.removeEventListener('message', this.onReceiveMessage, false);
440
- super.componentWillUnmount();
441
- }
442
-
443
- onReceiveMessage = (message: { data: string } | null) => {
444
- if (message?.data) {
445
- if (message.data === 'updateTheme') {
446
- const newThemeName = Utils.getThemeName();
447
- Utils.setThemeName(Utils.getThemeName());
448
-
449
- const newTheme = this.createTheme(newThemeName);
450
-
451
- this.setState({
452
- theme: newTheme,
453
- themeName: this.getThemeName(newTheme),
454
- themeType: this.getThemeType(newTheme),
455
- }, () => {
456
- this.props.onThemeChange && this.props.onThemeChange(newThemeName);
457
- this.onThemeChanged && this.onThemeChanged(newThemeName);
458
- });
459
- } else if (message.data === 'updateExpertMode') {
460
- this.onToggleExpertMode && this.onToggleExpertMode(this.getExpertMode());
461
- } else if (message.data !== 'chartReady') { // if not "echart ready" message
462
- // eslint-disable-next-line no-console
463
- console.debug(`Received unknown message: "${JSON.stringify(message.data)}". May be it will be processed later`);
464
- }
465
- }
466
- };
467
-
468
- private onResize = () => {
469
- this.resizeTimer && clearTimeout(this.resizeTimer);
470
- this.resizeTimer = setTimeout(() => {
471
- this.resizeTimer = null;
472
- this.setState({ width: GenericApp.getWidth() });
473
- }, 200);
474
- };
475
-
476
- /**
477
- * Gets the width depending on the window inner width.
478
- * @returns {import('./types').Width}
479
- */
480
- static getWidth(): Width {
481
- /**
482
- * innerWidth |xs sm md lg xl
483
- * |-------|-------|-------|-------|------>
484
- * width | xs | sm | md | lg | xl
485
- */
486
-
487
- const SIZES: Record<Width, number> = {
488
- xs: 0,
489
- sm: 600,
490
- md: 960,
491
- lg: 1280,
492
- xl: 1920,
493
- };
494
- const width = window.innerWidth;
495
- const keys = Object.keys(SIZES).reverse();
496
- const widthComputed = keys.find(key => width >= SIZES[key as Width]) as Width;
497
-
498
- return widthComputed || 'xs';
499
- }
500
-
501
- /**
502
- * Get a theme
503
- * @param name Theme name
504
- */
505
- createTheme(name?: ThemeName | null | undefined): IobTheme {
506
- return Theme(Utils.getThemeName(name));
507
- }
508
-
509
- /**
510
- * Get the theme name
511
- */
512
- getThemeName(currentTheme: IobTheme): ThemeName {
513
- return currentTheme.name;
514
- }
515
-
516
- /**
517
- * Get the theme type
518
- */
519
- getThemeType(currentTheme: IobTheme): ThemeType {
520
- return currentTheme.palette.mode;
521
- }
522
-
523
- onThemeChanged(_newThemeName: string) {
524
-
525
- }
526
-
527
- onToggleExpertMode(_expertMode: boolean) {
528
-
529
- }
530
-
531
- /**
532
- * Changes the current theme
533
- * */
534
- toggleTheme(newThemeName?: ThemeName) {
535
- const themeName = this.state.themeName;
536
-
537
- // dark => blue => colored => light => dark
538
- newThemeName = newThemeName || (themeName === 'dark' ? 'blue' :
539
- (themeName === 'blue' ? 'colored' :
540
- (themeName === 'colored' ? 'light' : 'dark')));
541
-
542
- if (newThemeName !== themeName) {
543
- Utils.setThemeName(newThemeName);
544
-
545
- const newTheme = this.createTheme(newThemeName);
546
-
547
- this.setState({
548
- theme: newTheme,
549
- themeName: this.getThemeName(newTheme),
550
- themeType: this.getThemeType(newTheme),
551
- }, () => {
552
- this.props.onThemeChange && this.props.onThemeChange(newThemeName || 'light');
553
- this.onThemeChanged && this.onThemeChanged(newThemeName || 'light');
554
- });
555
- }
556
- }
557
-
558
- /**
559
- * Gets the system configuration.
560
- * @returns {Promise<ioBroker.OtherObject>}
561
- */
562
- getSystemConfig() {
563
- return this.socket.getSystemConfig();
564
- }
565
-
566
- /**
567
- * Get current expert mode
568
- */
569
- getExpertMode(): boolean {
570
- return window.sessionStorage.getItem('App.expertMode') === 'true' || !!this._systemConfig?.expertMode;
571
- }
572
-
573
- /**
574
- * Gets called when the socket.io connection is ready.
575
- * You can overload this function to execute own commands.
576
- */
577
- onConnectionReady() {
578
- }
579
-
580
- /**
581
- * Encrypts a string.
582
- */
583
- encrypt(value: string): string {
584
- let result = '';
585
- if (this._secret) {
586
- for (let i = 0; i < value.length; i++) {
587
- // eslint-disable-next-line no-bitwise
588
- result += String.fromCharCode(this._secret[i % this._secret.length].charCodeAt(0) ^ value.charCodeAt(i));
589
- }
590
- }
591
- return result;
592
- }
593
-
594
- /**
595
- * Decrypts a string.
596
- */
597
- decrypt(value: string): string {
598
- let result = '';
599
- if (this._secret) {
600
- for (let i = 0; i < value.length; i++) {
601
- // eslint-disable-next-line no-bitwise
602
- result += String.fromCharCode(this._secret[i % this._secret.length].charCodeAt(0) ^ value.charCodeAt(i));
603
- }
604
- }
605
- return result;
606
- }
607
-
608
- /**
609
- * Gets called when the navigation hash changes.
610
- * You may override this if needed.
611
- */
612
- onHashChanged() {
613
- const location = Router.getLocation();
614
- if (location.tab !== this.state.selectedTab) {
615
- this.selectTab(location.tab);
616
- }
617
- }
618
-
619
- /**
620
- * Selects the given tab.
621
- */
622
- selectTab(tab: string, index?: number) {
623
- ((window as any)._localStorage || window.localStorage).setItem(`${this.adapterName}-adapter`, tab);
624
- this.setState({ selectedTab: tab, selectedTabNum: index });
625
- }
626
-
627
- /**
628
- * Gets called before the settings are saved.
629
- * You may override this if needed.
630
- */
631
- onPrepareSave(settings: Record<string, any>): boolean {
632
- // here you can encode values
633
- this.encryptedFields && this.encryptedFields.forEach(attr => {
634
- if (settings[attr]) {
635
- settings[attr] = this.encrypt(settings[attr]);
636
- }
637
- });
638
-
639
- return true;
640
- }
641
-
642
- /**
643
- * Gets called after the settings are loaded.
644
- * You may override this if needed.
645
- * @param encryptedNative optional list of fields to be decrypted
646
- */
647
- onPrepareLoad(settings: Record<string, any>, encryptedNative?: string[]) {
648
- // here you can encode values
649
- this.encryptedFields && this.encryptedFields.forEach(attr => {
650
- if (settings[attr]) {
651
- settings[attr] = this.decrypt(settings[attr]);
652
- }
653
- });
654
- encryptedNative && encryptedNative.forEach(attr => {
655
- this.encryptedFields = this.encryptedFields || [];
656
- !this.encryptedFields.includes(attr) && this.encryptedFields.push(attr);
657
- if (settings[attr]) {
658
- settings[attr] = this.decrypt(settings[attr]);
659
- }
660
- });
661
- }
662
-
663
- /**
664
- * Gets the extendable instances.
665
- * @returns {Promise<any[]>}
666
- */
667
- async getExtendableInstances(): Promise<ioBroker.InstanceObject[]> {
668
- try {
669
- const instances = await this.socket.getObjectViewSystem('instance', 'system.adapter.', 'system.adapter.\u9999');
670
- return Object.values(instances).filter(instance => !!instance?.common?.webExtendable);
671
- } catch (e) {
672
- return [];
673
- }
674
- }
675
-
676
- /**
677
- * Gets the IP addresses of the given host.
678
- */
679
- async getIpAddresses(host: string): Promise<{ name: string; address: string; family: 'ipv4' | 'ipv6' }[]> {
680
- const ips = await this.socket.getHostByIp(host || this.common?.host || '');
681
- // translate names
682
- const ip4 = ips.find(ip => ip.address === '0.0.0.0');
683
- if (ip4) {
684
- ip4.name = `[IPv4] 0.0.0.0 - ${I18n.t('ra_Listen on all IPs')}`;
685
- }
686
- const ip6 = ips.find(ip => ip.address === '::');
687
- if (ip6) {
688
- ip6.name = `[IPv4] :: - ${I18n.t('ra_Listen on all IPs')}`;
689
- }
690
- return ips;
691
- }
692
-
693
- /**
694
- * Saves the settings to the server.
695
- * @param isClose True if the user is closing the dialog.
696
- */
697
- onSave(isClose?: boolean) {
698
- let oldObj: ioBroker.InstanceObject;
699
- if (this.state.isConfigurationError) {
700
- this.setState({ errorText: this.state.isConfigurationError });
701
- return;
702
- }
703
-
704
- this.socket.getObject(this.instanceId)
705
- .then(_oldObj => {
706
- oldObj = (_oldObj || {}) as ioBroker.InstanceObject;
707
-
708
- for (const a in this.state.native) {
709
- if (Object.prototype.hasOwnProperty.call(this.state.native, a)) {
710
- if (this.state.native[a] === null) {
711
- oldObj.native[a] = null;
712
- } else if (this.state.native[a] !== undefined) {
713
- oldObj.native[a] = JSON.parse(JSON.stringify(this.state.native[a]));
714
- } else {
715
- delete oldObj.native[a];
716
- }
717
- }
718
- }
719
-
720
- if (this.state.common) {
721
- for (const b in this.state.common) {
722
- if (this.state.common[b] === null) {
723
- (oldObj as Record<string, any>).common[b] = null;
724
- } else if (this.state.common[b] !== undefined) {
725
- (oldObj as Record<string, any>).common[b] = JSON.parse(JSON.stringify(this.state.common[b]));
726
- } else {
727
- delete (oldObj as Record<string, any>).common[b];
728
- }
729
- }
730
- }
731
-
732
- if (this.onPrepareSave(oldObj.native) !== false) {
733
- return this.socket.setObject(this.instanceId, oldObj);
734
- }
735
-
736
- return Promise.reject(new Error('Invalid configuration'));
737
- })
738
- .then(() => {
739
- this.savedNative = oldObj.native;
740
- globalThis.changed = false;
741
- try {
742
- window.parent.postMessage('nochange', '*');
743
- } catch (e) {
744
- // ignore
745
- }
746
-
747
- this.setState({ changed: false });
748
- isClose && GenericApp.onClose();
749
- })
750
- .catch(e => console.error(`Cannot save configuration: ${e}`));
751
- }
752
-
753
- /**
754
- * Renders the toast.
755
- */
756
- renderToast() {
757
- if (!this.state.toast) {
758
- return null;
759
- }
760
-
761
- return <Snackbar
762
- anchorOrigin={{
763
- vertical: 'bottom',
764
- horizontal: 'left',
765
- }}
766
- open={!0}
767
- autoHideDuration={6000}
768
- onClose={() => this.setState({ toast: '' })}
769
- ContentProps={{ 'aria-describedby': 'message-id' }}
770
- message={<span id="message-id">{this.state.toast}</span>}
771
- action={[
772
- <IconButton
773
- key="close"
774
- aria-label="Close"
775
- color="inherit"
776
- className={this.props.classes?.close}
777
- onClick={() => this.setState({ toast: '' })}
778
- size="large"
779
- >
780
- <IconClose />
781
- </IconButton>,
782
- ]}
783
- />;
784
- }
785
-
786
- /**
787
- * Closes the dialog.
788
- */
789
- static onClose() {
790
- if (typeof window.parent !== 'undefined' && window.parent) {
791
- try {
792
- if (window.parent.$iframeDialog && typeof window.parent.$iframeDialog.close === 'function') {
793
- window.parent.$iframeDialog.close();
794
- } else {
795
- window.parent.postMessage('close', '*');
796
- }
797
- } catch (e) {
798
- window.parent.postMessage('close', '*');
799
- }
800
- }
801
- }
802
-
803
- /**
804
- * Renders the error dialog.
805
- */
806
- renderError(): React.JSX.Element | null {
807
- if (!this.state.errorText) {
808
- return null;
809
- }
810
-
811
- return <DialogError text={this.state.errorText} onClose={() => this.setState({ errorText: '' })} />;
812
- }
813
-
814
- /**
815
- * Checks if the configuration has changed.
816
- * @param {Record<string, any>} [native] the new state
817
- */
818
- getIsChanged(native: Record<string, any>): boolean {
819
- native = native || this.state.native;
820
- const isChanged = JSON.stringify(native) !== JSON.stringify(this.savedNative);
821
-
822
- globalThis.changed = isChanged;
823
-
824
- return isChanged;
825
- }
826
-
827
- /**
828
- * Gets called when loading the configuration.
829
- * @param newNative The new configuration object.
830
- */
831
- onLoadConfig(newNative: Record<string, any>) {
832
- if (JSON.stringify(newNative) !== JSON.stringify(this.state.native)) {
833
- this.setState({ native: newNative, changed: this.getIsChanged(newNative) });
834
- }
835
- }
836
-
837
- /**
838
- * Sets the configuration error.
839
- */
840
- setConfigurationError(errorText: string) {
841
- if (this.state.isConfigurationError !== errorText) {
842
- this.setState({ isConfigurationError: errorText });
843
- }
844
- }
845
-
846
- /**
847
- * Renders the save and close buttons.
848
- */
849
- renderSaveCloseButtons(): React.JSX.Element | null {
850
- if (!this.state.confirmClose && !this.state.bottomButtons) {
851
- return null;
852
- }
853
-
854
- return <>
855
- {this.state.bottomButtons ? <SaveCloseButtons
856
- theme={this.state.theme}
857
- newReact={this.newReact}
858
- noTextOnButtons={this.state.width === 'xs' || this.state.width === 'sm' || this.state.width === 'md'}
859
- changed={this.state.changed}
860
- onSave={isClose => this.onSave(isClose)}
861
- onClose={() => {
862
- if (this.state.changed) {
863
- this.setState({ confirmClose: true });
864
- } else {
865
- GenericApp.onClose();
866
- }
867
- }}
868
- /> : null}
869
- {this.state.confirmClose ? <ConfirmDialog
870
- title={I18n.t('ra_Please confirm')}
871
- text={I18n.t('ra_Some data are not stored. Discard?')}
872
- ok={I18n.t('ra_Discard')}
873
- cancel={I18n.t('ra_Cancel')}
874
- onClose={isYes =>
875
- this.setState({ confirmClose: false }, () =>
876
- isYes && GenericApp.onClose())}
877
- /> : null}
878
- </>;
879
- }
880
-
881
- private _updateNativeValue(obj: Record<string, any>, attrs: string | string[], value: any): boolean {
882
- if (typeof attrs !== 'object') {
883
- attrs = attrs.split('.');
884
- }
885
- const attr: string = attrs.shift() || '';
886
- if (!attrs.length) {
887
- if (value && typeof value === 'object') {
888
- if (JSON.stringify(obj[attr]) !== JSON.stringify(value)) {
889
- obj[attr] = value;
890
- return true;
891
- }
892
- return false;
893
- }
894
- if (obj[attr] !== value) {
895
- obj[attr] = value;
896
- return true;
897
- }
898
-
899
- return false;
900
- }
901
-
902
- obj[attr] = obj[attr] || {};
903
- if (typeof obj[attr] !== 'object') {
904
- throw new Error(`attribute ${attr} is no object, but ${typeof obj[attr]}`);
905
- }
906
- return this._updateNativeValue(obj[attr], attrs, value);
907
- }
908
-
909
- /**
910
- * Update the native value
911
- * @param attr The attribute name with dots as delimiter.
912
- * @param value The new value.
913
- * @param cb Callback which will be called upon completion.
914
- */
915
- updateNativeValue(attr: string, value: any, cb?: () => void) {
916
- const native = JSON.parse(JSON.stringify(this.state.native));
917
- if (this._updateNativeValue(native, attr, value)) {
918
- const changed = this.getIsChanged(native);
919
-
920
- if (changed !== this.state.changed) {
921
- try {
922
- window.parent.postMessage(changed ? 'change' : 'nochange', '*');
923
- } catch (e) {
924
- // ignore
925
- }
926
- }
927
-
928
- this.setState({ native, changed }, cb);
929
- }
930
- }
931
-
932
- /**
933
- * Set the error text to be shown.
934
- */
935
- showError(text: string | React.JSX.Element) {
936
- this.setState({ errorText: text });
937
- }
938
-
939
- /**
940
- * Sets the toast to be shown.
941
- * @param {string} toast
942
- */
943
- showToast(toast: string | React.JSX.Element): void {
944
- this.setState({ toast });
945
- }
946
-
947
- /**
948
- * Renders helper dialogs
949
- */
950
- renderHelperDialogs(): React.JSX.Element {
951
- return <>
952
- {this.renderError()}
953
- {this.renderToast()}
954
- {this.renderSaveCloseButtons()}
955
- {this.renderAlertSnackbar()}
956
- </>;
957
- }
958
-
959
- /**
960
- * Renders this component.
961
- */
962
- render(): React.JSX.Element {
963
- if (!this.state.loaded) {
964
- return <Loader themeType={this.state.themeType} />;
965
- }
966
-
967
- return <div className="App">
968
- {this.renderError()}
969
- {this.renderToast()}
970
- {this.renderSaveCloseButtons()}
971
- {this.renderAlertSnackbar()}
972
- </div>;
973
- }
974
- }
975
-
976
- export default GenericApp;
1
+ /**
2
+ * Copyright 2018-2024 Denis Haev (bluefox) <dogafox@gmail.com>
3
+ *
4
+ * MIT License
5
+ *
6
+ * */
7
+ import React from 'react';
8
+ import { PROGRESS, Connection, AdminConnection } from '@iobroker/socket-client';
9
+ import * as Sentry from '@sentry/browser';
10
+
11
+ import { Snackbar, IconButton } from '@mui/material';
12
+
13
+ import { Close as IconClose } from '@mui/icons-material';
14
+
15
+ import printPrompt from './Prompt';
16
+ import Theme from './Theme';
17
+ import Loader from './Components/Loader';
18
+ import Router from './Components/Router';
19
+ import Utils from './Components/Utils';
20
+ import SaveCloseButtons from './Components/SaveCloseButtons';
21
+ import ConfirmDialog from './Dialogs/Confirm';
22
+ import I18n from './i18n';
23
+ import DialogError from './Dialogs/Error';
24
+ import {
25
+ GenericAppProps,
26
+ GenericAppState,
27
+ GenericAppSettings,
28
+ ThemeName,
29
+ ThemeType,
30
+ IobTheme,
31
+ Width,
32
+ } from './types';
33
+
34
+ declare global {
35
+ /** If config has been changed */
36
+ // eslint-disable-next-line no-var
37
+ var changed: boolean;
38
+
39
+ interface Window {
40
+ io: any;
41
+ SocketClient: any;
42
+ adapterName: undefined | string;
43
+ socketUrl: undefined | string;
44
+ oldAlert: any;
45
+ changed: boolean;
46
+ $iframeDialog: {
47
+ close?: () => void;
48
+ };
49
+ }
50
+ }
51
+
52
+ // import './index.css';
53
+ const cssStyle = `
54
+ html {
55
+ height: 100%;
56
+ }
57
+
58
+ body {
59
+ margin: 0;
60
+ padding: 0;
61
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
62
+ -webkit-font-smoothing: antialiased;
63
+ -moz-osx-font-smoothing: grayscale;
64
+ width: 100%;
65
+ height: 100%;
66
+ overflow: hidden;
67
+ }
68
+
69
+ /* scrollbar */
70
+ ::-webkit-scrollbar-track {
71
+ background-color: #ccc;
72
+ border-radius: 5px;
73
+ }
74
+
75
+ ::-webkit-scrollbar {
76
+ width: 5px;
77
+ height: 5px;
78
+ background-color: #ccc;
79
+ }
80
+
81
+ ::-webkit-scrollbar-thumb {
82
+ background-color: #575757;
83
+ border-radius: 5px;
84
+ }
85
+
86
+ #root {
87
+ height: 100%;
88
+ }
89
+
90
+ .App {
91
+ height: 100%;
92
+ }
93
+
94
+ @keyframes glow {
95
+ from {
96
+ background-color: initial;
97
+ }
98
+ to {
99
+ background-color: #58c458;
100
+ }
101
+ }
102
+ `;
103
+
104
+ class GenericApp<TProps extends GenericAppProps = GenericAppProps, TState extends GenericAppState = GenericAppState> extends Router<TProps, TState> {
105
+ protected socket: AdminConnection;
106
+
107
+ protected readonly instance: number;
108
+
109
+ protected readonly adapterName: string;
110
+
111
+ protected readonly instanceId: string;
112
+
113
+ protected readonly newReact: boolean;
114
+
115
+ protected encryptedFields: string[];
116
+
117
+ protected readonly sentryDSN: string | undefined;
118
+
119
+ private alertDialogRendered: boolean;
120
+
121
+ private _secret: string | undefined;
122
+
123
+ protected _systemConfig: ioBroker.SystemConfigCommon | undefined;
124
+
125
+ // it is not readonly
126
+ private savedNative: Record<string, any>;
127
+
128
+ protected common: ioBroker.InstanceCommon | null = null;
129
+
130
+ private sentryStarted: boolean = false;
131
+
132
+ private sentryInited: boolean = false;
133
+
134
+ private resizeTimer: ReturnType<typeof setTimeout> | null = null;
135
+
136
+ constructor(props: TProps, settings?: GenericAppSettings) {
137
+ const ConnectionClass = (props.Connection || settings?.Connection || Connection) as unknown as typeof AdminConnection;
138
+ // const ConnectionClass = props.Connection === 'admin' || settings.Connection = 'admin' ? AdminConnection : (props.Connection || settings.Connection || Connection);
139
+
140
+ if (!window.document.getElementById('generic-app-iobroker-component')) {
141
+ const style = window.document.createElement('style');
142
+ style.setAttribute('id', 'generic-app-iobroker-component');
143
+ style.innerHTML = cssStyle;
144
+ window.document.head.appendChild(style);
145
+ }
146
+
147
+ // Remove `!Connection.isWeb() && window.adapterName !== 'material'` when iobroker.socket will support native ws
148
+ if (!GenericApp.isWeb() && window.io && window.location.port === '3000') {
149
+ try {
150
+ const io = new window.SocketClient();
151
+ delete window.io;
152
+ window.io = io;
153
+ } catch (e) {
154
+ // ignore
155
+ }
156
+ }
157
+
158
+ super(props);
159
+
160
+ printPrompt();
161
+
162
+ const query = (window.location.search || '').replace(/^\?/, '').replace(/#.*$/, '');
163
+ const args: Record<string, string | boolean> = {};
164
+ query.trim().split('&').filter(t => t.trim()).forEach(b => {
165
+ const parts = b.split('=');
166
+ args[parts[0]] = parts.length === 2 ? parts[1] : true;
167
+ if (args[parts[0]] === 'true') {
168
+ args[parts[0]] = true;
169
+ } else if (args[parts[0]] === 'false') {
170
+ args[parts[0]] = false;
171
+ }
172
+ });
173
+
174
+ // extract instance from URL
175
+ this.instance = settings?.instance ?? props.instance ?? (args.instance !== undefined ? parseInt(args.instance as string, 10) || 0 : (parseInt(window.location.search.slice(1), 10) || 0));
176
+ // extract adapter name from URL
177
+ const tmp = window.location.pathname.split('/');
178
+ this.adapterName = settings?.adapterName || props.adapterName || window.adapterName || tmp[tmp.length - 2] || 'iot';
179
+ this.instanceId = `system.adapter.${this.adapterName}.${this.instance}`;
180
+ this.newReact = args.newReact === true; // it is admin5
181
+
182
+ const location = Router.getLocation();
183
+ location.tab = location.tab || ((window as any)._localStorage || window.localStorage).getItem(`${this.adapterName}-adapter`) || '';
184
+
185
+ const themeInstance = this.createTheme();
186
+
187
+ this.state = {
188
+ ...(this.state || {}), // keep the existing state
189
+ selectedTab: ((window as any)._localStorage || window.localStorage).getItem(`${this.adapterName}-adapter`) || '',
190
+ selectedTabNum: -1,
191
+ native: {},
192
+ errorText: '',
193
+ changed: false,
194
+ connected: false,
195
+ loaded: false,
196
+ isConfigurationError: '',
197
+ expertMode: false,
198
+ toast: '',
199
+ theme: themeInstance,
200
+ themeName: this.getThemeName(themeInstance),
201
+ themeType: this.getThemeType(themeInstance),
202
+ bottomButtons: (settings && settings.bottomButtons) === false ? false : (props?.bottomButtons !== false),
203
+ width: GenericApp.getWidth(),
204
+ confirmClose: false,
205
+ _alert: false,
206
+ _alertType: 'info',
207
+ _alertMessage: '',
208
+ } satisfies GenericAppState as TState;
209
+
210
+ // init translations
211
+ const translations: Record<ioBroker.Languages, Record<string, string>> = {
212
+ en: require('./i18n/en.json'),
213
+ de: require('./i18n/de.json'),
214
+ ru: require('./i18n/ru.json'),
215
+ pt: require('./i18n/pt.json'),
216
+ nl: require('./i18n/nl.json'),
217
+ fr: require('./i18n/fr.json'),
218
+ it: require('./i18n/it.json'),
219
+ es: require('./i18n/es.json'),
220
+ pl: require('./i18n/pl.json'),
221
+ uk: require('./i18n/uk.json'),
222
+ 'zh-cn': require('./i18n/zh-cn.json'),
223
+ };
224
+
225
+ // merge together
226
+ if (settings && settings.translations) {
227
+ Object.keys(settings.translations).forEach(lang => {
228
+ if (settings.translations) {
229
+ translations[lang as ioBroker.Languages] = Object.assign(translations[lang as ioBroker.Languages], settings.translations[lang as ioBroker.Languages] || {});
230
+ }
231
+ });
232
+ } else if (props.translations) {
233
+ Object.keys(props.translations).forEach(lang => {
234
+ if (props.translations) {
235
+ translations[lang as ioBroker.Languages] = Object.assign(translations[lang as ioBroker.Languages], props.translations[lang as ioBroker.Languages] || {});
236
+ }
237
+ });
238
+ }
239
+
240
+ I18n.setTranslations(translations);
241
+
242
+ this.savedNative = {}; // to detect if the config changed
243
+
244
+ this.encryptedFields = props.encryptedFields || settings?.encryptedFields || [];
245
+
246
+ this.sentryDSN = (settings && settings.sentryDSN) || props.sentryDSN;
247
+
248
+ if (window.socketUrl) {
249
+ if (window.socketUrl.startsWith(':')) {
250
+ window.socketUrl = `${window.location.protocol}//${window.location.hostname}${window.socketUrl}`;
251
+ } else if (!window.socketUrl.startsWith('http://') && !window.socketUrl.startsWith('https://')) {
252
+ window.socketUrl = `${window.location.protocol}//${window.socketUrl}`;
253
+ }
254
+ }
255
+
256
+ this.alertDialogRendered = false;
257
+
258
+ window.oldAlert = window.alert;
259
+ window.alert = message => {
260
+ if (!this.alertDialogRendered) {
261
+ window.oldAlert(message);
262
+ return;
263
+ }
264
+ if (message && message.toString().toLowerCase().includes('error')) {
265
+ console.error(message);
266
+ this.showAlert(message.toString(), 'error');
267
+ } else {
268
+ console.log(message);
269
+ this.showAlert(message.toString(), 'info');
270
+ }
271
+ };
272
+
273
+ // @ts-expect-error either make props in ConnectionProps required or the constructor needs to accept than as they are (means adapt socket-client)
274
+ this.socket = new ConnectionClass({
275
+ ...(props?.socket || settings?.socket),
276
+ name: this.adapterName,
277
+ doNotLoadAllObjects: settings?.doNotLoadAllObjects,
278
+ onProgress: (progress: PROGRESS) => {
279
+ if (progress === PROGRESS.CONNECTING) {
280
+ this.setState({ connected: false });
281
+ } else if (progress === PROGRESS.READY) {
282
+ this.setState({ connected: true });
283
+ } else {
284
+ this.setState({ connected: true });
285
+ }
286
+ },
287
+ onReady: (/* objects, scripts */) => {
288
+ I18n.setLanguage(this.socket.systemLang);
289
+
290
+ // subscribe because of language and expert mode
291
+ this.socket.subscribeObject('system.config', this.onSystemConfigChanged)
292
+ .then(() => this.getSystemConfig())
293
+ .then(obj => {
294
+ this._secret = (typeof obj !== 'undefined' && obj.native && obj.native.secret) || 'Zgfr56gFe87jJOM';
295
+ this._systemConfig = obj?.common || ({} as ioBroker.SystemConfigCommon);
296
+ return this.socket.getObject(this.instanceId);
297
+ })
298
+ .then(async obj => {
299
+ let waitPromise;
300
+ const instanceObj: ioBroker.InstanceObject | null | undefined = obj as ioBroker.InstanceObject | null | undefined;
301
+
302
+ const sentryPluginEnabled = (await this.socket.getState(`${this.instanceId}.plugins.sentry.enabled`))?.val;
303
+
304
+ const sentryEnabled =
305
+ sentryPluginEnabled !== false &&
306
+ this._systemConfig?.diag !== 'none' &&
307
+ instanceObj?.common &&
308
+ instanceObj.common.name &&
309
+ instanceObj.common.version &&
310
+ // @ts-expect-error will be extended in js-controller TODO: (BF: 2024.05.30) this is redundant to state `${this.instanceId}.plugins.sentry.enabled`, remove this in future when admin sets the state correctly
311
+ !instanceObj.common.disableDataReporting &&
312
+ window.location.host !== 'localhost:3000';
313
+
314
+ // activate sentry plugin
315
+ if (!this.sentryStarted && this.sentryDSN && sentryEnabled) {
316
+ this.sentryStarted = true;
317
+
318
+ Sentry.init({
319
+ dsn: this.sentryDSN,
320
+ release: `iobroker.${instanceObj.common.name}@${instanceObj.common.version}`,
321
+ integrations: [
322
+ Sentry.dedupeIntegration(),
323
+ ],
324
+ });
325
+
326
+ console.log('Sentry initialized');
327
+ }
328
+
329
+ // read UUID and init sentry with it.
330
+ // for backward compatibility it will be processed separately from the above logic: some adapters could still have this.sentryDSN as undefined
331
+ if (!this.sentryInited && sentryEnabled) {
332
+ this.sentryInited = true;
333
+
334
+ waitPromise = this.socket.getObject('system.meta.uuid')
335
+ .then(uuidObj => {
336
+ if (uuidObj && uuidObj.native && uuidObj.native.uuid) {
337
+ const scope = Sentry.getCurrentScope();
338
+ scope.setUser({ id: uuidObj.native.uuid });
339
+ }
340
+ });
341
+ }
342
+
343
+ waitPromise = waitPromise || Promise.resolve();
344
+
345
+ waitPromise
346
+ .then(() => {
347
+ if (instanceObj) {
348
+ this.common = instanceObj?.common;
349
+ this.onPrepareLoad(instanceObj.native, instanceObj.encryptedNative); // decode all secrets
350
+ this.savedNative = JSON.parse(JSON.stringify(instanceObj.native));
351
+ this.setState({ native: instanceObj.native, loaded: true, expertMode: this.getExpertMode() }, () =>
352
+ this.onConnectionReady && this.onConnectionReady());
353
+ } else {
354
+ console.warn('Cannot load instance settings');
355
+ this.setState(
356
+ {
357
+ native: {},
358
+ loaded: true,
359
+ expertMode: this.getExpertMode(),
360
+ },
361
+ () => this.onConnectionReady && this.onConnectionReady(),
362
+ );
363
+ }
364
+ });
365
+ })
366
+ .catch(e => window.alert(`Cannot settings: ${e}`));
367
+ },
368
+ onError: (err: string) => {
369
+ console.error(err);
370
+ this.showError(err);
371
+ },
372
+ });
373
+ }
374
+
375
+ /**
376
+ * Checks if this connection is running in a web adapter and not in an admin.
377
+ * @returns True if running in a web adapter or in a socketio adapter.
378
+ */
379
+ static isWeb(): boolean {
380
+ return window.socketUrl !== undefined;
381
+ }
382
+
383
+ showAlert(message: string, type?: 'info' | 'warning' | 'error' | 'success') {
384
+ if (type !== 'error' && type !== 'warning' && type !== 'info' && type !== 'success') {
385
+ type = 'info';
386
+ }
387
+
388
+ this.setState({
389
+ _alert: true,
390
+ _alertType: type,
391
+ _alertMessage: message,
392
+ });
393
+ }
394
+
395
+ renderAlertSnackbar() {
396
+ this.alertDialogRendered = true;
397
+
398
+ return <Snackbar
399
+ style={this.state._alertType === 'error' ?
400
+ { backgroundColor: '#f44336' } :
401
+ (this.state._alertType === 'success' ? { backgroundColor: '#4caf50' } : undefined)}
402
+ open={this.state._alert}
403
+ autoHideDuration={6000}
404
+ onClose={(_e, reason) => reason !== 'clickaway' && this.setState({ _alert: false })}
405
+ message={this.state._alertMessage}
406
+ />;
407
+ }
408
+
409
+ onSystemConfigChanged = (id: string, obj: ioBroker.AnyObject | null | undefined) => {
410
+ if (obj && id === 'system.config') {
411
+ if (this.socket.systemLang !== (obj as ioBroker.SystemConfigObject)?.common.language) {
412
+ this.socket.systemLang = (obj as ioBroker.SystemConfigObject)?.common.language || 'en';
413
+ I18n.setLanguage(this.socket.systemLang);
414
+ }
415
+
416
+ if (this._systemConfig?.expertMode !== !!(obj as ioBroker.SystemConfigObject)?.common?.expertMode) {
417
+ this._systemConfig = (obj as ioBroker.SystemConfigObject)?.common || ({} as ioBroker.SystemConfigCommon);
418
+ this.setState({ expertMode: this.getExpertMode() });
419
+ } else {
420
+ this._systemConfig = (obj as ioBroker.SystemConfigObject)?.common || ({} as ioBroker.SystemConfigCommon);
421
+ }
422
+ }
423
+ };
424
+
425
+ /**
426
+ * Called immediately after a component is mounted. Setting state here will trigger re-rendering.
427
+ */
428
+ componentDidMount() {
429
+ window.addEventListener('resize', this.onResize, true);
430
+ window.addEventListener('message', this.onReceiveMessage, false);
431
+ super.componentDidMount();
432
+ }
433
+
434
+ /**
435
+ * Called immediately before a component is destroyed.
436
+ */
437
+ componentWillUnmount() {
438
+ window.removeEventListener('resize', this.onResize, true);
439
+ window.removeEventListener('message', this.onReceiveMessage, false);
440
+ super.componentWillUnmount();
441
+ }
442
+
443
+ onReceiveMessage = (message: { data: string } | null) => {
444
+ if (message?.data) {
445
+ if (message.data === 'updateTheme') {
446
+ const newThemeName = Utils.getThemeName();
447
+ Utils.setThemeName(Utils.getThemeName());
448
+
449
+ const newTheme = this.createTheme(newThemeName);
450
+
451
+ this.setState({
452
+ theme: newTheme,
453
+ themeName: this.getThemeName(newTheme),
454
+ themeType: this.getThemeType(newTheme),
455
+ }, () => {
456
+ this.props.onThemeChange && this.props.onThemeChange(newThemeName);
457
+ this.onThemeChanged && this.onThemeChanged(newThemeName);
458
+ });
459
+ } else if (message.data === 'updateExpertMode') {
460
+ this.onToggleExpertMode && this.onToggleExpertMode(this.getExpertMode());
461
+ } else if (message.data !== 'chartReady') { // if not "echart ready" message
462
+ // eslint-disable-next-line no-console
463
+ console.debug(`Received unknown message: "${JSON.stringify(message.data)}". May be it will be processed later`);
464
+ }
465
+ }
466
+ };
467
+
468
+ private onResize = () => {
469
+ this.resizeTimer && clearTimeout(this.resizeTimer);
470
+ this.resizeTimer = setTimeout(() => {
471
+ this.resizeTimer = null;
472
+ this.setState({ width: GenericApp.getWidth() });
473
+ }, 200);
474
+ };
475
+
476
+ /**
477
+ * Gets the width depending on the window inner width.
478
+ * @returns {import('./types').Width}
479
+ */
480
+ static getWidth(): Width {
481
+ /**
482
+ * innerWidth |xs sm md lg xl
483
+ * |-------|-------|-------|-------|------>
484
+ * width | xs | sm | md | lg | xl
485
+ */
486
+
487
+ const SIZES: Record<Width, number> = {
488
+ xs: 0,
489
+ sm: 600,
490
+ md: 960,
491
+ lg: 1280,
492
+ xl: 1920,
493
+ };
494
+ const width = window.innerWidth;
495
+ const keys = Object.keys(SIZES).reverse();
496
+ const widthComputed = keys.find(key => width >= SIZES[key as Width]) as Width;
497
+
498
+ return widthComputed || 'xs';
499
+ }
500
+
501
+ /**
502
+ * Get a theme
503
+ * @param name Theme name
504
+ */
505
+ createTheme(name?: ThemeName | null | undefined): IobTheme {
506
+ return Theme(Utils.getThemeName(name));
507
+ }
508
+
509
+ /**
510
+ * Get the theme name
511
+ */
512
+ getThemeName(currentTheme: IobTheme): ThemeName {
513
+ return currentTheme.name;
514
+ }
515
+
516
+ /**
517
+ * Get the theme type
518
+ */
519
+ getThemeType(currentTheme: IobTheme): ThemeType {
520
+ return currentTheme.palette.mode;
521
+ }
522
+
523
+ onThemeChanged(_newThemeName: string) {
524
+
525
+ }
526
+
527
+ onToggleExpertMode(_expertMode: boolean) {
528
+
529
+ }
530
+
531
+ /**
532
+ * Changes the current theme
533
+ * */
534
+ toggleTheme(newThemeName?: ThemeName) {
535
+ const themeName = this.state.themeName;
536
+
537
+ // dark => blue => colored => light => dark
538
+ newThemeName = newThemeName || (themeName === 'dark' ? 'blue' :
539
+ (themeName === 'blue' ? 'colored' :
540
+ (themeName === 'colored' ? 'light' : 'dark')));
541
+
542
+ if (newThemeName !== themeName) {
543
+ Utils.setThemeName(newThemeName);
544
+
545
+ const newTheme = this.createTheme(newThemeName);
546
+
547
+ this.setState({
548
+ theme: newTheme,
549
+ themeName: this.getThemeName(newTheme),
550
+ themeType: this.getThemeType(newTheme),
551
+ }, () => {
552
+ this.props.onThemeChange && this.props.onThemeChange(newThemeName || 'light');
553
+ this.onThemeChanged && this.onThemeChanged(newThemeName || 'light');
554
+ });
555
+ }
556
+ }
557
+
558
+ /**
559
+ * Gets the system configuration.
560
+ * @returns {Promise<ioBroker.OtherObject>}
561
+ */
562
+ getSystemConfig() {
563
+ return this.socket.getSystemConfig();
564
+ }
565
+
566
+ /**
567
+ * Get current expert mode
568
+ */
569
+ getExpertMode(): boolean {
570
+ return window.sessionStorage.getItem('App.expertMode') === 'true' || !!this._systemConfig?.expertMode;
571
+ }
572
+
573
+ /**
574
+ * Gets called when the socket.io connection is ready.
575
+ * You can overload this function to execute own commands.
576
+ */
577
+ onConnectionReady() {
578
+ }
579
+
580
+ /**
581
+ * Encrypts a string.
582
+ */
583
+ encrypt(value: string): string {
584
+ let result = '';
585
+ if (this._secret) {
586
+ for (let i = 0; i < value.length; i++) {
587
+ // eslint-disable-next-line no-bitwise
588
+ result += String.fromCharCode(this._secret[i % this._secret.length].charCodeAt(0) ^ value.charCodeAt(i));
589
+ }
590
+ }
591
+ return result;
592
+ }
593
+
594
+ /**
595
+ * Decrypts a string.
596
+ */
597
+ decrypt(value: string): string {
598
+ let result = '';
599
+ if (this._secret) {
600
+ for (let i = 0; i < value.length; i++) {
601
+ // eslint-disable-next-line no-bitwise
602
+ result += String.fromCharCode(this._secret[i % this._secret.length].charCodeAt(0) ^ value.charCodeAt(i));
603
+ }
604
+ }
605
+ return result;
606
+ }
607
+
608
+ /**
609
+ * Gets called when the navigation hash changes.
610
+ * You may override this if needed.
611
+ */
612
+ onHashChanged() {
613
+ const location = Router.getLocation();
614
+ if (location.tab !== this.state.selectedTab) {
615
+ this.selectTab(location.tab);
616
+ }
617
+ }
618
+
619
+ /**
620
+ * Selects the given tab.
621
+ */
622
+ selectTab(tab: string, index?: number) {
623
+ ((window as any)._localStorage || window.localStorage).setItem(`${this.adapterName}-adapter`, tab);
624
+ this.setState({ selectedTab: tab, selectedTabNum: index });
625
+ }
626
+
627
+ /**
628
+ * Gets called before the settings are saved.
629
+ * You may override this if needed.
630
+ */
631
+ onPrepareSave(settings: Record<string, any>): boolean {
632
+ // here you can encode values
633
+ this.encryptedFields && this.encryptedFields.forEach(attr => {
634
+ if (settings[attr]) {
635
+ settings[attr] = this.encrypt(settings[attr]);
636
+ }
637
+ });
638
+
639
+ return true;
640
+ }
641
+
642
+ /**
643
+ * Gets called after the settings are loaded.
644
+ * You may override this if needed.
645
+ * @param encryptedNative optional list of fields to be decrypted
646
+ */
647
+ onPrepareLoad(settings: Record<string, any>, encryptedNative?: string[]) {
648
+ // here you can encode values
649
+ this.encryptedFields && this.encryptedFields.forEach(attr => {
650
+ if (settings[attr]) {
651
+ settings[attr] = this.decrypt(settings[attr]);
652
+ }
653
+ });
654
+ encryptedNative && encryptedNative.forEach(attr => {
655
+ this.encryptedFields = this.encryptedFields || [];
656
+ !this.encryptedFields.includes(attr) && this.encryptedFields.push(attr);
657
+ if (settings[attr]) {
658
+ settings[attr] = this.decrypt(settings[attr]);
659
+ }
660
+ });
661
+ }
662
+
663
+ /**
664
+ * Gets the extendable instances.
665
+ * @returns {Promise<any[]>}
666
+ */
667
+ async getExtendableInstances(): Promise<ioBroker.InstanceObject[]> {
668
+ try {
669
+ const instances = await this.socket.getObjectViewSystem('instance', 'system.adapter.', 'system.adapter.\u9999');
670
+ return Object.values(instances).filter(instance => !!instance?.common?.webExtendable);
671
+ } catch (e) {
672
+ return [];
673
+ }
674
+ }
675
+
676
+ /**
677
+ * Gets the IP addresses of the given host.
678
+ */
679
+ async getIpAddresses(host: string): Promise<{ name: string; address: string; family: 'ipv4' | 'ipv6' }[]> {
680
+ const ips = await this.socket.getHostByIp(host || this.common?.host || '');
681
+ // translate names
682
+ const ip4 = ips.find(ip => ip.address === '0.0.0.0');
683
+ if (ip4) {
684
+ ip4.name = `[IPv4] 0.0.0.0 - ${I18n.t('ra_Listen on all IPs')}`;
685
+ }
686
+ const ip6 = ips.find(ip => ip.address === '::');
687
+ if (ip6) {
688
+ ip6.name = `[IPv4] :: - ${I18n.t('ra_Listen on all IPs')}`;
689
+ }
690
+ return ips;
691
+ }
692
+
693
+ /**
694
+ * Saves the settings to the server.
695
+ * @param isClose True if the user is closing the dialog.
696
+ */
697
+ onSave(isClose?: boolean) {
698
+ let oldObj: ioBroker.InstanceObject;
699
+ if (this.state.isConfigurationError) {
700
+ this.setState({ errorText: this.state.isConfigurationError });
701
+ return;
702
+ }
703
+
704
+ this.socket.getObject(this.instanceId)
705
+ .then(_oldObj => {
706
+ oldObj = (_oldObj || {}) as ioBroker.InstanceObject;
707
+
708
+ for (const a in this.state.native) {
709
+ if (Object.prototype.hasOwnProperty.call(this.state.native, a)) {
710
+ if (this.state.native[a] === null) {
711
+ oldObj.native[a] = null;
712
+ } else if (this.state.native[a] !== undefined) {
713
+ oldObj.native[a] = JSON.parse(JSON.stringify(this.state.native[a]));
714
+ } else {
715
+ delete oldObj.native[a];
716
+ }
717
+ }
718
+ }
719
+
720
+ if (this.state.common) {
721
+ for (const b in this.state.common) {
722
+ if (this.state.common[b] === null) {
723
+ (oldObj as Record<string, any>).common[b] = null;
724
+ } else if (this.state.common[b] !== undefined) {
725
+ (oldObj as Record<string, any>).common[b] = JSON.parse(JSON.stringify(this.state.common[b]));
726
+ } else {
727
+ delete (oldObj as Record<string, any>).common[b];
728
+ }
729
+ }
730
+ }
731
+
732
+ if (this.onPrepareSave(oldObj.native) !== false) {
733
+ return this.socket.setObject(this.instanceId, oldObj);
734
+ }
735
+
736
+ return Promise.reject(new Error('Invalid configuration'));
737
+ })
738
+ .then(() => {
739
+ this.savedNative = oldObj.native;
740
+ globalThis.changed = false;
741
+ try {
742
+ window.parent.postMessage('nochange', '*');
743
+ } catch (e) {
744
+ // ignore
745
+ }
746
+
747
+ this.setState({ changed: false });
748
+ isClose && GenericApp.onClose();
749
+ })
750
+ .catch(e => console.error(`Cannot save configuration: ${e}`));
751
+ }
752
+
753
+ /**
754
+ * Renders the toast.
755
+ */
756
+ renderToast() {
757
+ if (!this.state.toast) {
758
+ return null;
759
+ }
760
+
761
+ return <Snackbar
762
+ anchorOrigin={{
763
+ vertical: 'bottom',
764
+ horizontal: 'left',
765
+ }}
766
+ open={!0}
767
+ autoHideDuration={6000}
768
+ onClose={() => this.setState({ toast: '' })}
769
+ ContentProps={{ 'aria-describedby': 'message-id' }}
770
+ message={<span id="message-id">{this.state.toast}</span>}
771
+ action={[
772
+ <IconButton
773
+ key="close"
774
+ aria-label="Close"
775
+ color="inherit"
776
+ className={this.props.classes?.close}
777
+ onClick={() => this.setState({ toast: '' })}
778
+ size="large"
779
+ >
780
+ <IconClose />
781
+ </IconButton>,
782
+ ]}
783
+ />;
784
+ }
785
+
786
+ /**
787
+ * Closes the dialog.
788
+ */
789
+ static onClose() {
790
+ if (typeof window.parent !== 'undefined' && window.parent) {
791
+ try {
792
+ if (window.parent.$iframeDialog && typeof window.parent.$iframeDialog.close === 'function') {
793
+ window.parent.$iframeDialog.close();
794
+ } else {
795
+ window.parent.postMessage('close', '*');
796
+ }
797
+ } catch (e) {
798
+ window.parent.postMessage('close', '*');
799
+ }
800
+ }
801
+ }
802
+
803
+ /**
804
+ * Renders the error dialog.
805
+ */
806
+ renderError(): React.JSX.Element | null {
807
+ if (!this.state.errorText) {
808
+ return null;
809
+ }
810
+
811
+ return <DialogError text={this.state.errorText} onClose={() => this.setState({ errorText: '' })} />;
812
+ }
813
+
814
+ /**
815
+ * Checks if the configuration has changed.
816
+ * @param {Record<string, any>} [native] the new state
817
+ */
818
+ getIsChanged(native: Record<string, any>): boolean {
819
+ native = native || this.state.native;
820
+ const isChanged = JSON.stringify(native) !== JSON.stringify(this.savedNative);
821
+
822
+ globalThis.changed = isChanged;
823
+
824
+ return isChanged;
825
+ }
826
+
827
+ /**
828
+ * Gets called when loading the configuration.
829
+ * @param newNative The new configuration object.
830
+ */
831
+ onLoadConfig(newNative: Record<string, any>) {
832
+ if (JSON.stringify(newNative) !== JSON.stringify(this.state.native)) {
833
+ this.setState({ native: newNative, changed: this.getIsChanged(newNative) });
834
+ }
835
+ }
836
+
837
+ /**
838
+ * Sets the configuration error.
839
+ */
840
+ setConfigurationError(errorText: string) {
841
+ if (this.state.isConfigurationError !== errorText) {
842
+ this.setState({ isConfigurationError: errorText });
843
+ }
844
+ }
845
+
846
+ /**
847
+ * Renders the save and close buttons.
848
+ */
849
+ renderSaveCloseButtons(): React.JSX.Element | null {
850
+ if (!this.state.confirmClose && !this.state.bottomButtons) {
851
+ return null;
852
+ }
853
+
854
+ return <>
855
+ {this.state.bottomButtons ? <SaveCloseButtons
856
+ theme={this.state.theme}
857
+ newReact={this.newReact}
858
+ noTextOnButtons={this.state.width === 'xs' || this.state.width === 'sm' || this.state.width === 'md'}
859
+ changed={this.state.changed}
860
+ onSave={isClose => this.onSave(isClose)}
861
+ onClose={() => {
862
+ if (this.state.changed) {
863
+ this.setState({ confirmClose: true });
864
+ } else {
865
+ GenericApp.onClose();
866
+ }
867
+ }}
868
+ /> : null}
869
+ {this.state.confirmClose ? <ConfirmDialog
870
+ title={I18n.t('ra_Please confirm')}
871
+ text={I18n.t('ra_Some data are not stored. Discard?')}
872
+ ok={I18n.t('ra_Discard')}
873
+ cancel={I18n.t('ra_Cancel')}
874
+ onClose={isYes =>
875
+ this.setState({ confirmClose: false }, () =>
876
+ isYes && GenericApp.onClose())}
877
+ /> : null}
878
+ </>;
879
+ }
880
+
881
+ private _updateNativeValue(obj: Record<string, any>, attrs: string | string[], value: any): boolean {
882
+ if (typeof attrs !== 'object') {
883
+ attrs = attrs.split('.');
884
+ }
885
+ const attr: string = attrs.shift() || '';
886
+ if (!attrs.length) {
887
+ if (value && typeof value === 'object') {
888
+ if (JSON.stringify(obj[attr]) !== JSON.stringify(value)) {
889
+ obj[attr] = value;
890
+ return true;
891
+ }
892
+ return false;
893
+ }
894
+ if (obj[attr] !== value) {
895
+ obj[attr] = value;
896
+ return true;
897
+ }
898
+
899
+ return false;
900
+ }
901
+
902
+ obj[attr] = obj[attr] || {};
903
+ if (typeof obj[attr] !== 'object') {
904
+ throw new Error(`attribute ${attr} is no object, but ${typeof obj[attr]}`);
905
+ }
906
+ return this._updateNativeValue(obj[attr], attrs, value);
907
+ }
908
+
909
+ /**
910
+ * Update the native value
911
+ * @param attr The attribute name with dots as delimiter.
912
+ * @param value The new value.
913
+ * @param cb Callback which will be called upon completion.
914
+ */
915
+ updateNativeValue(attr: string, value: any, cb?: () => void) {
916
+ const native = JSON.parse(JSON.stringify(this.state.native));
917
+ if (this._updateNativeValue(native, attr, value)) {
918
+ const changed = this.getIsChanged(native);
919
+
920
+ if (changed !== this.state.changed) {
921
+ try {
922
+ window.parent.postMessage(changed ? 'change' : 'nochange', '*');
923
+ } catch (e) {
924
+ // ignore
925
+ }
926
+ }
927
+
928
+ this.setState({ native, changed }, cb);
929
+ }
930
+ }
931
+
932
+ /**
933
+ * Set the error text to be shown.
934
+ */
935
+ showError(text: string | React.JSX.Element) {
936
+ this.setState({ errorText: text });
937
+ }
938
+
939
+ /**
940
+ * Sets the toast to be shown.
941
+ * @param {string} toast
942
+ */
943
+ showToast(toast: string | React.JSX.Element): void {
944
+ this.setState({ toast });
945
+ }
946
+
947
+ /**
948
+ * Renders helper dialogs
949
+ */
950
+ renderHelperDialogs(): React.JSX.Element {
951
+ return <>
952
+ {this.renderError()}
953
+ {this.renderToast()}
954
+ {this.renderSaveCloseButtons()}
955
+ {this.renderAlertSnackbar()}
956
+ </>;
957
+ }
958
+
959
+ /**
960
+ * Renders this component.
961
+ */
962
+ render(): React.JSX.Element {
963
+ if (!this.state.loaded) {
964
+ return <Loader themeType={this.state.themeType} />;
965
+ }
966
+
967
+ return <div className="App">
968
+ {this.renderError()}
969
+ {this.renderToast()}
970
+ {this.renderSaveCloseButtons()}
971
+ {this.renderAlertSnackbar()}
972
+ </div>;
973
+ }
974
+ }
975
+
976
+ export default GenericApp;