@iobroker/adapter-react-v5 7.0.1 → 7.1.0

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