@iobroker/adapter-react-v5 6.1.3 → 6.1.5

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 (66) hide show
  1. package/README.md +3 -0
  2. package/craco-module-federation.js +1 -1
  3. package/package.json +1 -1
  4. package/src/AdminConnection.tsx +3 -0
  5. package/src/Components/404.tsx +121 -0
  6. package/src/Components/ColorPicker.tsx +315 -0
  7. package/src/Components/ComplexCron.tsx +507 -0
  8. package/src/Components/CopyToClipboard.tsx +165 -0
  9. package/src/Components/CustomModal.tsx +163 -0
  10. package/src/Components/FileBrowser.tsx +2394 -0
  11. package/src/Components/FileViewer.tsx +384 -0
  12. package/src/Components/Icon.tsx +210 -0
  13. package/src/Components/IconPicker.tsx +149 -0
  14. package/src/Components/IconSelector.tsx +2202 -0
  15. package/src/Components/Image.tsx +176 -0
  16. package/src/Components/Loader.tsx +304 -0
  17. package/src/Components/Logo.tsx +166 -0
  18. package/src/Components/MDUtils.tsx +100 -0
  19. package/src/Components/ObjectBrowser.tsx +7915 -0
  20. package/src/Components/Router.tsx +90 -0
  21. package/src/Components/SaveCloseButtons.tsx +113 -0
  22. package/src/Components/Schedule.tsx +1724 -0
  23. package/src/Components/SelectWithIcon.tsx +197 -0
  24. package/src/Components/TabContainer.tsx +55 -0
  25. package/src/Components/TabContent.tsx +37 -0
  26. package/src/Components/TabHeader.tsx +19 -0
  27. package/src/Components/TableResize.tsx +259 -0
  28. package/src/Components/TextWithIcon.tsx +148 -0
  29. package/src/Components/ToggleThemeMenu.tsx +34 -0
  30. package/src/Components/TreeTable.tsx +919 -0
  31. package/src/Components/UploadImage.tsx +599 -0
  32. package/src/Components/Utils.tsx +1794 -0
  33. package/src/Components/loader.css +222 -0
  34. package/src/Components/withWidth.tsx +21 -0
  35. package/src/Connection.tsx +7 -0
  36. package/src/Dialogs/ComplexCron.tsx +129 -0
  37. package/src/Dialogs/Confirm.tsx +162 -0
  38. package/src/Dialogs/Cron.tsx +182 -0
  39. package/src/Dialogs/Error.tsx +72 -0
  40. package/src/Dialogs/Message.tsx +71 -0
  41. package/src/Dialogs/SelectFile.tsx +270 -0
  42. package/src/Dialogs/SelectID.tsx +298 -0
  43. package/src/Dialogs/SimpleCron.tsx +100 -0
  44. package/src/Dialogs/TextInput.tsx +107 -0
  45. package/src/GenericApp.tsx +976 -0
  46. package/src/LegacyConnection.tsx +3589 -0
  47. package/src/Prompt.tsx +20 -0
  48. package/src/Theme.tsx +479 -0
  49. package/src/icons/IconAdapter.tsx +20 -0
  50. package/src/icons/IconAlias.tsx +20 -0
  51. package/src/icons/IconChannel.tsx +21 -0
  52. package/src/icons/IconClearFilter.tsx +22 -0
  53. package/src/icons/IconClosed.tsx +17 -0
  54. package/src/icons/IconCopy.tsx +16 -0
  55. package/src/icons/IconDevice.tsx +27 -0
  56. package/src/icons/IconDocument.tsx +17 -0
  57. package/src/icons/IconDocumentReadOnly.tsx +18 -0
  58. package/src/icons/IconExpert.tsx +18 -0
  59. package/src/icons/IconFx.tsx +36 -0
  60. package/src/icons/IconInstance.tsx +20 -0
  61. package/src/icons/IconLogout.tsx +30 -0
  62. package/src/icons/IconNoIcon.tsx +19 -0
  63. package/src/icons/IconOpen.tsx +17 -0
  64. package/src/icons/IconProps.tsx +15 -0
  65. package/src/icons/IconState.tsx +17 -0
  66. package/src/index.css +55 -0
@@ -0,0 +1,176 @@
1
+ import React, { Component } from 'react';
2
+
3
+ import IconNoIcon from '../icons/IconNoIcon';
4
+
5
+ function getElementFromSource(src: string): HTMLElement | null {
6
+ const svgContainer = document.createElement('div');
7
+ svgContainer.innerHTML = src;
8
+ const svg: HTMLElement = svgContainer.firstElementChild as HTMLElement;
9
+ if (svg?.remove) {
10
+ svg.remove();
11
+ } else if (svg) {
12
+ svgContainer.removeChild(svg);
13
+ }
14
+
15
+ svgContainer.remove();
16
+ return svg;
17
+ }
18
+
19
+ function serializeAttrs(map?: NamedNodeMap): Record<string, string> {
20
+ const ret: Record<string, string> = {};
21
+ if (!map) {
22
+ return ret;
23
+ }
24
+ for (let prop, i = 0; i < map.length; i++) {
25
+ const key = map[i].name;
26
+ if (key === 'class') {
27
+ prop = 'className';
28
+ } else if (!key.startsWith('data-')) {
29
+ prop = key.replace(/[-|:]([a-z])/g, g => g[1].toUpperCase());
30
+ } else {
31
+ prop = key;
32
+ }
33
+
34
+ ret[prop] = map[i].value;
35
+ }
36
+ return ret;
37
+ }
38
+
39
+ interface ImageProps {
40
+ /* The color */
41
+ color?: string;
42
+ /* The source of the image */
43
+ src?: string;
44
+ /* The image prefix (default: './files/') */
45
+ imagePrefix?: string;
46
+ /* The CSS class name */
47
+ className?: string;
48
+ /* Show image errors (or just show no image)? */
49
+ showError?: boolean;
50
+ }
51
+
52
+ interface ImageState {
53
+ svg?: boolean;
54
+ created?: boolean;
55
+ color?: string;
56
+ src?: string;
57
+ imgError?: boolean;
58
+ showError?: boolean;
59
+ }
60
+
61
+ class Image extends Component<ImageProps, ImageState> {
62
+ private svg: React.JSX.Element | null;
63
+
64
+ static REMOTE_SERVER: boolean = window.location.hostname.includes('iobroker.in');
65
+
66
+ static REMOTE_PREFIX: string = window.location.pathname.substring(0, window.location.pathname.lastIndexOf('/') + 1);
67
+
68
+ constructor(props: ImageProps) {
69
+ super(props);
70
+ this.state = {
71
+ svg: !!this.props.src?.startsWith('data:'),
72
+ created: true,
73
+ color: this.props.color || '',
74
+ src: this.props.src || '',
75
+ imgError: false,
76
+ showError: !!this.props.showError,
77
+ };
78
+
79
+ this.svg = this.state.svg && this.state.src ? this.getSvgFromData(this.state.src) : null;
80
+ }
81
+
82
+ static getDerivedStateFromProps(props: ImageProps, state: ImageState) {
83
+ const newState: ImageState = {};
84
+ let changed = false;
85
+
86
+ if (props && state && props.src !== state.src) {
87
+ newState.src = props.src;
88
+ newState.svg = props.src?.startsWith('data:');
89
+ newState.created = false;
90
+ changed = true;
91
+ }
92
+
93
+ if (props && state && props.color !== state.color) {
94
+ newState.color = props.color;
95
+ newState.created = false;
96
+ changed = true;
97
+ }
98
+
99
+ if (props && state && props.showError !== state.showError) {
100
+ newState.showError = props.showError;
101
+ changed = true;
102
+ }
103
+
104
+ return changed ? newState : null;
105
+ }
106
+
107
+ getSvgFromData(src: string): React.JSX.Element | null {
108
+ const len = 'data:image/svg+xml;base64,';
109
+ if (!src.startsWith(len)) {
110
+ return null;
111
+ }
112
+ src = src.substring(len.length);
113
+ try {
114
+ src = atob(src);
115
+ const svg = getElementFromSource(src) as HTMLElement;
116
+ const inner = svg.innerHTML;
117
+ const svgProps = serializeAttrs(svg.attributes);
118
+
119
+ svg.remove();
120
+
121
+ return <svg
122
+ className={this.props.className}
123
+ style={this.state.color ? { color: this.state.color } : {}}
124
+ {...svgProps}
125
+ // eslint-disable-next-line react/no-danger
126
+ dangerouslySetInnerHTML={{ __html: inner }}
127
+ />;
128
+ } catch (e) {
129
+ // ignore
130
+ }
131
+ return null;
132
+ }
133
+
134
+ render() {
135
+ if (this.state.svg) {
136
+ if (!this.state.created) {
137
+ setTimeout(() => {
138
+ this.svg = this.state.src ? this.getSvgFromData(this.state.src) : null;
139
+ this.setState({ created: true });
140
+ }, 50);
141
+ }
142
+
143
+ return this.svg;
144
+ }
145
+ if (this.state.src) {
146
+ if (this.state.imgError || !this.state.src) {
147
+ return <IconNoIcon className={this.props.className} />;
148
+ }
149
+ if (Image.REMOTE_SERVER && !this.state.src.startsWith('http://') && !this.state.src.startsWith('https://')) {
150
+ let src = (this.props.imagePrefix || '') + this.state.src;
151
+ if (src.startsWith('./')) {
152
+ src = Image.REMOTE_PREFIX + src.substring(2);
153
+ } else if (!src.startsWith('/')) {
154
+ src = Image.REMOTE_PREFIX + src;
155
+ }
156
+ return <img
157
+ className={this.props.className}
158
+ src={`https://remote-files.iobroker.in${src}`}
159
+ alt=""
160
+ onError={() => (this.props.showError ? this.setState({ imgError: true }) : this.setState({ src: '' }))}
161
+ />;
162
+ }
163
+
164
+ return <img
165
+ className={this.props.className}
166
+ src={(this.props.imagePrefix || '') + this.state.src}
167
+ alt=""
168
+ onError={() => (this.props.showError ? this.setState({ imgError: true }) : this.setState({ src: '' }))}
169
+ />;
170
+ }
171
+
172
+ return null;
173
+ }
174
+ }
175
+
176
+ export default Image;
@@ -0,0 +1,304 @@
1
+ /**
2
+ * Copyright 2018-2024 Denis Haev (bluefox) <dogafox@gmail.com>
3
+ *
4
+ * MIT License
5
+ *
6
+ * */
7
+ import React, { useEffect } from 'react';
8
+ import { ThemeName, ThemeType } from '../types';
9
+ // import './loader.css'
10
+
11
+ declare global {
12
+ interface Window {
13
+ loadingBackgroundImage: undefined | string;
14
+ loadingBackgroundColor: undefined | string;
15
+ loadingHideLogo: undefined | 'true';
16
+ }
17
+ }
18
+
19
+ const loaderStyles = `
20
+ /**
21
+ * Copyright 2018-2024 Denis Haev (bluefox) <dogafox@gmail.com>
22
+ *
23
+ * MIT License
24
+ *
25
+ **/
26
+
27
+ .logo-background-light, .logo-background-colored {
28
+ background: white;
29
+ }
30
+ .logo-background-dark, .logo-background-blue {
31
+ background: black;
32
+ }
33
+ .logo-div {
34
+ position: absolute;
35
+ top: 50%;
36
+ left: 50%;
37
+ -ms-transform: translateX(-50%) translateY(-50%);
38
+ -webkit-transform: translate(-50%,-50%);
39
+ transform: translate(-50%,-50%);
40
+ overflow: hidden;
41
+ border-radius: 50%;
42
+ z-index: 2;
43
+ }
44
+ .logo-border {
45
+ /*border-color: #164477;*/
46
+ border-top-color: #3399CC;
47
+ border-left-color: #164477;
48
+ border-bottom-color: #164477;
49
+ border-right-color: #164477;
50
+ border-radius: 50%;
51
+ border-style: solid;
52
+ box-sizing: border-box;
53
+ width: 100%;
54
+ height: 100%;
55
+ position: absolute;
56
+ }
57
+ .logo-top {
58
+ position: absolute;
59
+ width: 4.5%;
60
+ height: 16%;
61
+ top: 0;
62
+ z-index: 2;
63
+ }
64
+ .logo-i {
65
+ position: absolute;
66
+ width: 14.5%;
67
+ height: 60%;
68
+ top: 20%;
69
+ left: 42%;
70
+ background: #3399CC;
71
+ }
72
+ .logo-i-top {
73
+ position: absolute;
74
+ width: 14.5%;
75
+ height: 4%;
76
+ left: 42%;
77
+ background: #3399CC;
78
+ border-radius: 100%;
79
+ }
80
+ .logo-back {
81
+ width: 100%;
82
+ height: 100%;
83
+ z-index: 0;
84
+ overflow: hidden;
85
+ }
86
+ @keyframes logo-grow {
87
+ 0% {
88
+ width: 230px;
89
+ height: 230px;
90
+ transform: translate(-50%,-50%) scale(1);
91
+ opacity: 1
92
+ }
93
+ 99% {
94
+ width: 230px;
95
+ height: 230px;
96
+ transform: translate(-50%,-50%) scale(10);
97
+ opacity: 0;
98
+ }
99
+ 100% {
100
+ width: 0;
101
+ height: 0;
102
+ opacity: 0;
103
+ }
104
+ }
105
+ @keyframes logo-spin { 100% { -webkit-transform: rotate(360deg); transform: rotate(360deg); } }
106
+ @keyframes logo-color-inside-light {
107
+ 0% {
108
+ background: #FEFEFE;
109
+ }
110
+ 100% {
111
+ background: #3399CC;
112
+ }
113
+ }
114
+ @keyframes logo-color-inside-dark {
115
+ 0% {
116
+ background: #030303;
117
+ }
118
+ 100% {
119
+ background: #3399CC;
120
+ }
121
+ }
122
+ @keyframes logo-color-inside-colored {
123
+ 0% {
124
+ background: #FEFEFE;
125
+ }
126
+ 100% {
127
+ background: #3399CC;
128
+ }
129
+ }
130
+ @keyframes logo-color-inside-blue {
131
+ 0% {
132
+ background: #030303;
133
+ }
134
+ 100% {
135
+ background: #3399CC;
136
+ }
137
+ }
138
+
139
+ @keyframes logo-color-outside-light {
140
+ 0% {
141
+ border-color: #FEFEFE;
142
+ }
143
+ 100% {
144
+ border-top-color: #3399CC;
145
+ border-left-color: #164477;
146
+ border-bottom-color: #164477;
147
+ border-right-color: #164477;
148
+ }
149
+ }
150
+ @keyframes logo-color-outside-dark {
151
+ 0% {
152
+ border-color: #040404;
153
+ }
154
+ 100% {
155
+ border-top-color: #3399CC;
156
+ border-left-color: #164477;
157
+ border-bottom-color: #164477;
158
+ border-right-color: #164477;
159
+ }
160
+ }
161
+ @keyframes logo-color-outside-colored {
162
+ 0% {
163
+ border-color: #FEFEFE;
164
+ }
165
+ 100% {
166
+ border-top-color: #3399CC;
167
+ border-left-color: #164477;
168
+ border-bottom-color: #164477;
169
+ border-right-color: #164477;
170
+ }
171
+ }
172
+ @keyframes logo-color-outside-blue {
173
+ 0% {
174
+ border-color: #040404;
175
+ }
176
+ 100% {
177
+ border-top-color: #3399CC;
178
+ border-left-color: #164477;
179
+ border-bottom-color: #164477;
180
+ border-right-color: #164477;
181
+ }
182
+ }
183
+
184
+ .logo-animate-wait {
185
+ animation: logo-color-outside 1.5s, logo-spin 1.5s linear infinite;
186
+ }
187
+
188
+ .logo-animate-grow-light {
189
+ background: #DDD;
190
+ }
191
+ .logo-animate-grow-dark {
192
+ background: #1d1d1d;
193
+ }
194
+ .logo-animate-grow-colored {
195
+ background: #DDD;
196
+ }
197
+ .logo-animate-grow-blue {
198
+ background: #1d1d1d;
199
+ }
200
+
201
+ .logo-animate-grow {
202
+ display: inline-block;
203
+ text-align: center;
204
+ z-index: 1;
205
+ top: 50%;
206
+ left: 50%;
207
+ -ms-transform: translateX(-50%) translateY(-50%);
208
+ -webkit-transform: translate(-50%,-50%);
209
+ transform: translate(-50%,-50%);
210
+ width: 245px;
211
+ height: 245px;
212
+ border-radius: 50%;
213
+ position: absolute;
214
+ animation: logo-grow 1s 1 ease forwards;
215
+ }
216
+
217
+ .logo-animate-color-inside-light {
218
+ animation: logo-color-inside-light 2.5s;
219
+ }
220
+ .logo-animate-color-inside-dark {
221
+ animation: logo-color-inside-dark 2.5s;
222
+ }
223
+ .logo-animate-color-inside-colored {
224
+ animation: logo-color-inside-colored 2.5s;
225
+ }
226
+ .logo-animate-color-inside-blue {
227
+ animation: logo-color-inside-blue 2.5s;
228
+ }
229
+
230
+ .logo-animate-color-outside-light {
231
+ animation: logo-color-outside-light 1.5s;
232
+ }
233
+ .logo-animate-color-outside-dark {
234
+ animation: logo-color-outside-dark 1.5s;
235
+ }
236
+ .logo-animate-color-outside-colored {
237
+ animation: logo-color-outside-colored 1.5s;
238
+ }
239
+ .logo-animate-color-outside-blue {
240
+ animation: logo-color-outside-blue 1.5s;
241
+ }
242
+ `;
243
+
244
+ interface LoaderProps {
245
+ /** The size in pixels of this loader. */
246
+ size?: number;
247
+ /** The chosen theme type. */
248
+ themeType?: ThemeType;
249
+ /** Theme name */
250
+ themeName?: ThemeName;
251
+ /** @deprecated Theme name. use themeName instead */
252
+ theme?: ThemeName;
253
+ /** Background color */
254
+ backgroundColor?: string;
255
+ /** Background image URL */
256
+ backgroundImage?: string;
257
+ }
258
+
259
+ function Loader(props: LoaderProps) {
260
+ useEffect(() => {
261
+ if (!window.document.getElementById('loader-iobroker-component')) {
262
+ const style = window.document.createElement('style');
263
+ style.setAttribute('id', 'loader-iobroker-component');
264
+ style.innerHTML = loaderStyles;
265
+ window.document.head.appendChild(style);
266
+ }
267
+ }, []);
268
+
269
+ const size = props.size || 234;
270
+ const theme = props.themeName || props.theme || props.themeType || 'light';
271
+ return <div
272
+ className={`logo-back logo-background-${theme}`}
273
+ style={{
274
+ backgroundImage: (props.backgroundImage && props.backgroundImage !== '@@loginBackgroundImage@@') ? props.backgroundImage :
275
+ (window.loadingBackgroundImage && window.loadingBackgroundImage !== '@@loginBackgroundImage@@' ? `url(${window.loadingBackgroundImage})` : undefined),
276
+ backgroundColor: (props.backgroundColor && props.backgroundColor !== '@@loginBackgroundColor@@') ? props.backgroundColor :
277
+ (window.loadingBackgroundColor && window.loadingBackgroundColor !== '@@loginBackgroundColor@@' ? window.loadingBackgroundColor : undefined),
278
+ backgroundSize: 'cover',
279
+ }}
280
+ >
281
+ {window.loadingHideLogo === 'true' ?
282
+ null
283
+ :
284
+ <>
285
+ <div className="logo-div" style={{ width: size, height: size }}>
286
+ <div className={`logo-top logo-background-${theme}`} style={{ left: '37%' }} />
287
+ <div className={`logo-top logo-background-${theme}`} style={{ left: '57%' }} />
288
+ <div
289
+ className={`logo-border logo-background-${theme} logo-animate-wait`}
290
+ style={{ borderWidth: size * 0.132 }}
291
+ />
292
+ <div className={`logo-i logo-animate-color-inside-${theme}`} />
293
+ <div className={`logo-i-top logo-animate-color-inside-${theme}`} style={{ top: '18%' }} />
294
+ <div className={`logo-i-top logo-animate-color-inside-${theme}`} style={{ bottom: '18%' }} />
295
+ </div>
296
+ <div
297
+ className={`logo-animate-grow logo-animate-grow-${theme}`}
298
+ style={{ width: size + 11, height: size + 11 }}
299
+ />
300
+ </>}
301
+ </div>;
302
+ }
303
+
304
+ export default Loader;
@@ -0,0 +1,166 @@
1
+ import React from 'react';
2
+ import { Fab } from '@mui/material';
3
+
4
+ import {
5
+ Help as IconHelp,
6
+ VerticalAlignTop as IconUpload,
7
+ VerticalAlignBottom as IconDownload,
8
+ } from '@mui/icons-material';
9
+
10
+ import I18n from '../i18n';
11
+ import Icon from './Icon';
12
+
13
+ interface LogoProps {
14
+ /* Adapter common configuration from io-package.json */
15
+ common: any;
16
+ /* Adapter native data from io-package.json */
17
+ native: any;
18
+ /* Adapter instance number. */
19
+ instance: number;
20
+ /* on Load handler */
21
+ onLoad?: (contents: any) => void;
22
+ /* on Error handler */
23
+ onError?: (error: string) => void;
24
+ className?: string;
25
+ style?: Record<string, any>;
26
+ }
27
+
28
+ class Logo extends React.Component<LogoProps> {
29
+ static generateFile(fileName: string, obj: any) {
30
+ const el = window.document.createElement('a');
31
+ el.setAttribute('href', `data:application/json;charset=utf-8,${encodeURIComponent(JSON.stringify(obj, null, 2))}`);
32
+ el.setAttribute('download', fileName);
33
+
34
+ el.style.display = 'none';
35
+ window.document.body.appendChild(el);
36
+
37
+ el.click();
38
+
39
+ window.document.body.removeChild(el);
40
+ }
41
+
42
+ handleFileSelect = (evt: Event) => {
43
+ const target = evt.target as HTMLInputElement;
44
+ const files = target.files;
45
+ if (!files || !files.length) {
46
+ return;
47
+ }
48
+ const f = files[0];
49
+
50
+ if (f) {
51
+ const r = new window.FileReader();
52
+ r.onload = () => {
53
+ // @ts-expect-error I don't know how to fix this
54
+ const contents = target.result as string;
55
+ try {
56
+ const json = JSON.parse(contents);
57
+ if (json.native && json.common) {
58
+ if (json.common.name !== this.props.common.name) {
59
+ this.props.onError && this.props.onError(I18n.t('ra_otherConfig', json.common.name));
60
+ } else {
61
+ this.props.onLoad && this.props.onLoad(json.native);
62
+ }
63
+ } else {
64
+ this.props.onError && this.props.onError(I18n.t('ra_invalidConfig'));
65
+ }
66
+ } catch (err: any) {
67
+ this.props.onError && this.props.onError(err?.toString());
68
+ }
69
+ };
70
+ r.readAsText(f);
71
+ } else {
72
+ alert('Failed to open JSON File');
73
+ }
74
+ };
75
+
76
+ download() {
77
+ const result = {
78
+ _id: `system.adapter.${this.props.common.name}.${this.props.instance}`,
79
+ common: JSON.parse(JSON.stringify(this.props.common)),
80
+ native: this.props.native,
81
+ };
82
+ // remove unimportant information
83
+ if (result.common.news) {
84
+ delete result.common.news;
85
+ }
86
+ if (result.common.titleLang) {
87
+ delete result.common.titleLang;
88
+ }
89
+ if (result.common.desc) {
90
+ delete result.common.desc;
91
+ }
92
+
93
+ // window.open('data:application/iobroker; content-disposition=attachment; filename=' + result._id + '.json,' + JSON.stringify(result, null, 2));
94
+ Logo.generateFile(`${result._id}.json`, result);
95
+ }
96
+
97
+ upload() {
98
+ const input = window.document.createElement('input');
99
+ input.setAttribute('type', 'file');
100
+ input.setAttribute('id', 'files');
101
+ input.setAttribute('opacity', '0');
102
+ input.addEventListener('change', this.handleFileSelect, false);
103
+ (input.click)();
104
+ }
105
+
106
+ render() {
107
+ return <div className={this.props.className} style={this.props.style}>
108
+ {this.props.common.icon ?
109
+ <Icon
110
+ src={this.props.common.icon}
111
+ style={{
112
+ padding: 8,
113
+ width: 64,
114
+ }}
115
+ alt="logo"
116
+ /> : null}
117
+ {this.props.common.readme ?
118
+ <Fab
119
+ size="small"
120
+ color="primary"
121
+ aria-label="Help"
122
+ style={{
123
+ marginRight: 5,
124
+ marginTop: 5,
125
+ float: 'right',
126
+ }}
127
+ onClick={() => {
128
+ const win = window.open(this.props.common.readme, '_blank');
129
+ win?.focus();
130
+ }}
131
+ >
132
+ <IconHelp />
133
+ </Fab> : null}
134
+ <Fab
135
+ size="small"
136
+ color="primary"
137
+ aria-label="Load config"
138
+ style={{
139
+ marginRight: 5,
140
+ marginTop: 5,
141
+ float: 'right',
142
+ }}
143
+ title={I18n.t('ra_Load configuration from file')}
144
+ onClick={() => this.upload()}
145
+ >
146
+ <IconUpload />
147
+ </Fab>
148
+ <Fab
149
+ size="small"
150
+ color="primary"
151
+ aria-label="Save config"
152
+ style={{
153
+ marginRight: 5,
154
+ marginTop: 5,
155
+ float: 'right',
156
+ }}
157
+ title={I18n.t('ra_Save configuration to file')}
158
+ onClick={() => this.download()}
159
+ >
160
+ <IconDownload />
161
+ </Fab>
162
+ </div>;
163
+ }
164
+ }
165
+
166
+ export default Logo;