@iobroker/dm-gui-components 10.0.22 → 10.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/build/AnimatedIcons.d.ts +37 -0
- package/build/AnimatedIcons.js +108 -0
- package/build/AnimatedIcons.js.map +1 -0
- package/build/Communication.d.ts +7 -0
- package/build/Communication.js +43 -11
- package/build/Communication.js.map +1 -1
- package/build/DeviceCard.d.ts +25 -13
- package/build/DeviceCard.js +177 -282
- package/build/DeviceCard.js.map +1 -1
- package/build/DeviceControl.js +38 -26
- package/build/DeviceControl.js.map +1 -1
- package/build/DeviceImageUpload.d.ts +5 -1
- package/build/DeviceImageUpload.js +79 -62
- package/build/DeviceImageUpload.js.map +1 -1
- package/build/DeviceList.d.ts +8 -0
- package/build/DeviceList.js +74 -59
- package/build/DeviceList.js.map +1 -1
- package/build/DeviceStatus.d.ts +20 -4
- package/build/DeviceStatus.js +146 -191
- package/build/DeviceStatus.js.map +1 -1
- package/build/StatusIndicator.js +9 -37
- package/build/StatusIndicator.js.map +1 -1
- package/build/StatusPill.d.ts +18 -0
- package/build/StatusPill.js +30 -0
- package/build/StatusPill.js.map +1 -0
- package/build/Switch.d.ts +3 -0
- package/build/Switch.js +36 -26
- package/build/Switch.js.map +1 -1
- package/build/Utils.js +92 -60
- package/build/Utils.js.map +1 -1
- package/build/i18n/de.json +3 -0
- package/build/i18n/en.json +3 -0
- package/build/i18n/es.json +3 -0
- package/build/i18n/fr.json +3 -0
- package/build/i18n/it.json +3 -0
- package/build/i18n/nl.json +3 -0
- package/build/i18n/pl.json +3 -0
- package/build/i18n/pt.json +3 -0
- package/build/i18n/ru.json +3 -0
- package/build/i18n/uk.json +3 -0
- package/build/i18n/zh-cn.json +3 -0
- package/build/styles.d.ts +17 -0
- package/build/styles.js +226 -0
- package/build/styles.js.map +1 -0
- package/package.json +10 -10
package/README.md
CHANGED
|
@@ -197,6 +197,19 @@ const indicator = {
|
|
|
197
197
|
-->
|
|
198
198
|
|
|
199
199
|
## Changelog
|
|
200
|
+
### 10.1.0 (2026-11-05)
|
|
201
|
+
- (@GermanBluefox) Redesigned the device cards (big and small): device tile with a live connection LED, compact status pills that are only tinted when a value needs attention, a facts grid and an action footer
|
|
202
|
+
- (@GermanBluefox) Added animations: the animated icons of `@iobroker/gui-components` (refresh, filter, delete, upload, folder, copy, play/pause, result) and own ones (battery, signal and update icons, hover animations of the actions, value ticks, LED pulse on status changes, snapping switch)
|
|
203
|
+
- (@GermanBluefox) Added: a successful action is confirmed with a check mark, the error toast shows an animated cross
|
|
204
|
+
- (@GermanBluefox) Changed: "No response from the backend" and the copy confirmations no longer use `window.alert`
|
|
205
|
+
- (@GermanBluefox) Added: "Reset filters" button in the toolbar and in the empty list
|
|
206
|
+
- (@GermanBluefox) Redesigned the instance selection and the empty states
|
|
207
|
+
- (@GermanBluefox) Animations respect `prefers-reduced-motion`
|
|
208
|
+
- (@GermanBluefox) Corrected: a slider or select control without `stateId` never got its range or options and showed only "..."
|
|
209
|
+
|
|
210
|
+
### 10.0.23 (2026-09-21)
|
|
211
|
+
- (@BenAhrdt) Supported explicit line breaks in status indicator tooltips
|
|
212
|
+
|
|
200
213
|
### 10.0.22 (2026-09-18)
|
|
201
214
|
- (@GermanBluefox) Updated packages
|
|
202
215
|
- (@GermanBluefox) Renamed `Config manager` to `Konfig-Manager` (DE)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Own animated icons of the device manager, in the style of the animated icons of `@iobroker/gui-components`:
|
|
3
|
+
* plain CSS/SVG, `currentColor`, the size of the MUI icons and less motion if the user asked for it.
|
|
4
|
+
*/
|
|
5
|
+
import React, { type CSSProperties } from 'react';
|
|
6
|
+
interface OwnIconProps {
|
|
7
|
+
className?: string;
|
|
8
|
+
style?: CSSProperties;
|
|
9
|
+
/** Accessible name. Without it, the icon is decorative. */
|
|
10
|
+
ariaLabel?: string;
|
|
11
|
+
}
|
|
12
|
+
/** Color of the charge of a battery with this level */
|
|
13
|
+
export declare function batteryColor(level: number): string;
|
|
14
|
+
export interface IconBatteryProps extends OwnIconProps {
|
|
15
|
+
/** Charge in percent. Without it the battery is shown without a level (e.g. for a voltage). */
|
|
16
|
+
level?: number;
|
|
17
|
+
/** The battery is charging: the charge fills up again and again */
|
|
18
|
+
charging?: boolean;
|
|
19
|
+
}
|
|
20
|
+
/** Battery whose charge slides to the new level when it changes */
|
|
21
|
+
export declare function IconBattery(props: IconBatteryProps): React.JSX.Element;
|
|
22
|
+
export interface IconSignalProps extends OwnIconProps {
|
|
23
|
+
/** Signal strength (RSSI) in dBm */
|
|
24
|
+
rssi: number;
|
|
25
|
+
}
|
|
26
|
+
/** Four bars for the signal strength. When it changes, the bars light up or go out one after another. */
|
|
27
|
+
export declare function IconSignal(props: IconSignalProps): React.JSX.Element;
|
|
28
|
+
/** An update can be installed: the arrow drops into the tray while the mouse is over the icon or its button */
|
|
29
|
+
export declare function IconUpdate(props: OwnIconProps): React.JSX.Element;
|
|
30
|
+
/**
|
|
31
|
+
* Lets the new value of a status slide in when it changes (not on the first render).
|
|
32
|
+
*
|
|
33
|
+
* @param value the shown value
|
|
34
|
+
* @returns the ref for the element that shows the value
|
|
35
|
+
*/
|
|
36
|
+
export declare function useValueTick<T extends HTMLElement>(value: unknown): React.RefObject<T | null>;
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Own animated icons of the device manager, in the style of the animated icons of `@iobroker/gui-components`:
|
|
3
|
+
* plain CSS/SVG, `currentColor`, the size of the MUI icons and less motion if the user asked for it.
|
|
4
|
+
*/
|
|
5
|
+
import React, { useEffect, useRef } from 'react';
|
|
6
|
+
import { useDmStyles } from './styles';
|
|
7
|
+
function svgProps(props, className) {
|
|
8
|
+
return {
|
|
9
|
+
viewBox: '0 0 24 24',
|
|
10
|
+
width: '1em',
|
|
11
|
+
height: '1em',
|
|
12
|
+
className: `${className}${props.className ? ` ${props.className}` : ''}`,
|
|
13
|
+
style: { fontSize: '1.5rem', flexShrink: 0, ...props.style },
|
|
14
|
+
role: props.ariaLabel ? 'img' : undefined,
|
|
15
|
+
'aria-label': props.ariaLabel,
|
|
16
|
+
'aria-hidden': props.ariaLabel ? undefined : true,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** True if the user asked the system for less motion */
|
|
20
|
+
function prefersReducedMotion() {
|
|
21
|
+
return !!window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
|
|
22
|
+
}
|
|
23
|
+
/** Color of the charge of a battery with this level */
|
|
24
|
+
export function batteryColor(level) {
|
|
25
|
+
if (level > 55) {
|
|
26
|
+
return 'var(--dm-ok, #00ac00)';
|
|
27
|
+
}
|
|
28
|
+
if (level > 25) {
|
|
29
|
+
return '#e6b800';
|
|
30
|
+
}
|
|
31
|
+
if (level > 10) {
|
|
32
|
+
return 'var(--dm-warn, #da8200)';
|
|
33
|
+
}
|
|
34
|
+
return 'var(--dm-err, #ff0000)';
|
|
35
|
+
}
|
|
36
|
+
/** Battery whose charge slides to the new level when it changes */
|
|
37
|
+
export function IconBattery(props) {
|
|
38
|
+
useDmStyles();
|
|
39
|
+
const known = typeof props.level === 'number' && !props.charging;
|
|
40
|
+
const level = known ? Math.max(0, Math.min(100, props.level)) : 100;
|
|
41
|
+
// An empty battery still shows a sliver, so that its color is visible
|
|
42
|
+
const scale = known ? Math.max(0.08, level / 100) : 1;
|
|
43
|
+
let className = 'dm-bat';
|
|
44
|
+
if (props.charging) {
|
|
45
|
+
className += ' dm-bat-charging';
|
|
46
|
+
}
|
|
47
|
+
else if (!known) {
|
|
48
|
+
className += ' dm-bat-unknown';
|
|
49
|
+
}
|
|
50
|
+
const svg = svgProps(props, className);
|
|
51
|
+
return (React.createElement("svg", { ...svg, style: { ...svg.style, '--dm-bat': known ? batteryColor(level) : undefined } },
|
|
52
|
+
React.createElement("rect", { className: "dm-bat-body", x: "2.8", y: "6.8", width: "16.4", height: "10.4", rx: "2.2" }),
|
|
53
|
+
React.createElement("rect", { className: "dm-bat-cap", x: "19.9", y: "9.9", width: "2", height: "4.2", rx: "0.8" }),
|
|
54
|
+
React.createElement("rect", { className: "dm-bat-fill", x: "4.6", y: "8.6", width: "12.8", height: "6.8", rx: "0.9", style: { transform: `scaleX(${scale})` } }),
|
|
55
|
+
props.charging ? (React.createElement("path", { className: "dm-bat-bolt", d: "M11.9 7.4 8.4 12.6h2.9l-.6 4 3.6-5.3h-2.9z" })) : null));
|
|
56
|
+
}
|
|
57
|
+
/** Number of the lit bars for a signal strength in dBm */
|
|
58
|
+
function signalBars(rssi) {
|
|
59
|
+
if (rssi >= -55) {
|
|
60
|
+
return 4;
|
|
61
|
+
}
|
|
62
|
+
if (rssi >= -67) {
|
|
63
|
+
return 3;
|
|
64
|
+
}
|
|
65
|
+
if (rssi >= -80) {
|
|
66
|
+
return 2;
|
|
67
|
+
}
|
|
68
|
+
return 1;
|
|
69
|
+
}
|
|
70
|
+
/** Four bars for the signal strength. When it changes, the bars light up or go out one after another. */
|
|
71
|
+
export function IconSignal(props) {
|
|
72
|
+
useDmStyles();
|
|
73
|
+
const bars = signalBars(props.rssi);
|
|
74
|
+
return (React.createElement("svg", { ...svgProps(props, 'dm-sig') }, [0, 1, 2, 3].map(i => (React.createElement("rect", { key: i, className: i < bars ? 'dm-sig-on' : undefined, x: 3 + i * 5, y: 16 - i * 4, width: "3.2", height: 4 + i * 4, rx: "1" })))));
|
|
75
|
+
}
|
|
76
|
+
/** An update can be installed: the arrow drops into the tray while the mouse is over the icon or its button */
|
|
77
|
+
export function IconUpdate(props) {
|
|
78
|
+
useDmStyles();
|
|
79
|
+
return (React.createElement("svg", { ...svgProps(props, 'dm-upd') },
|
|
80
|
+
React.createElement("path", { d: "M4 14.5v3.2A2.3 2.3 0 0 0 6.3 20h11.4a2.3 2.3 0 0 0 2.3-2.3v-3.2" }),
|
|
81
|
+
React.createElement("path", { className: "dm-upd-arrow", d: "M12 3.5v10.5M7.6 9.8 12 14.2l4.4-4.4" })));
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Lets the new value of a status slide in when it changes (not on the first render).
|
|
85
|
+
*
|
|
86
|
+
* @param value the shown value
|
|
87
|
+
* @returns the ref for the element that shows the value
|
|
88
|
+
*/
|
|
89
|
+
export function useValueTick(value) {
|
|
90
|
+
const ref = useRef(null);
|
|
91
|
+
const previous = useRef(value);
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
if (Object.is(previous.current, value)) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const goesUp = typeof value === 'number' && typeof previous.current === 'number' && value > previous.current;
|
|
97
|
+
previous.current = value;
|
|
98
|
+
if (!ref.current?.animate || prefersReducedMotion()) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
ref.current.animate([
|
|
102
|
+
{ transform: `translateY(${goesUp ? 45 : -45}%)`, opacity: 0 },
|
|
103
|
+
{ transform: 'none', opacity: 1 },
|
|
104
|
+
], { duration: 300, easing: 'cubic-bezier(0.2, 0.8, 0.3, 1)' });
|
|
105
|
+
}, [value]);
|
|
106
|
+
return ref;
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=AnimatedIcons.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AnimatedIcons.js","sourceRoot":"./src/","sources":["AnimatedIcons.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * Own animated icons of the device manager, in the style of the animated icons of `@iobroker/gui-components`:\n * plain CSS/SVG, `currentColor`, the size of the MUI icons and less motion if the user asked for it.\n */\nimport React, { useEffect, useRef, type CSSProperties } from 'react';\n\nimport { useDmStyles } from './styles';\n\ninterface OwnIconProps {\n className?: string;\n style?: CSSProperties;\n /** Accessible name. Without it, the icon is decorative. */\n ariaLabel?: string;\n}\n\nfunction svgProps(props: OwnIconProps, className: string): React.SVGProps<SVGSVGElement> {\n return {\n viewBox: '0 0 24 24',\n width: '1em',\n height: '1em',\n className: `${className}${props.className ? ` ${props.className}` : ''}`,\n style: { fontSize: '1.5rem', flexShrink: 0, ...props.style },\n role: props.ariaLabel ? 'img' : undefined,\n 'aria-label': props.ariaLabel,\n 'aria-hidden': props.ariaLabel ? undefined : true,\n };\n}\n\n/** True if the user asked the system for less motion */\nfunction prefersReducedMotion(): boolean {\n return !!window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;\n}\n\n/** Color of the charge of a battery with this level */\nexport function batteryColor(level: number): string {\n if (level > 55) {\n return 'var(--dm-ok, #00ac00)';\n }\n if (level > 25) {\n return '#e6b800';\n }\n if (level > 10) {\n return 'var(--dm-warn, #da8200)';\n }\n return 'var(--dm-err, #ff0000)';\n}\n\nexport interface IconBatteryProps extends OwnIconProps {\n /** Charge in percent. Without it the battery is shown without a level (e.g. for a voltage). */\n level?: number;\n /** The battery is charging: the charge fills up again and again */\n charging?: boolean;\n}\n\n/** Battery whose charge slides to the new level when it changes */\nexport function IconBattery(props: IconBatteryProps): React.JSX.Element {\n useDmStyles();\n const known = typeof props.level === 'number' && !props.charging;\n const level = known ? Math.max(0, Math.min(100, props.level!)) : 100;\n // An empty battery still shows a sliver, so that its color is visible\n const scale = known ? Math.max(0.08, level / 100) : 1;\n let className = 'dm-bat';\n if (props.charging) {\n className += ' dm-bat-charging';\n } else if (!known) {\n className += ' dm-bat-unknown';\n }\n const svg = svgProps(props, className);\n return (\n <svg\n {...svg}\n style={{ ...svg.style, '--dm-bat': known ? batteryColor(level) : undefined } as CSSProperties}\n >\n <rect\n className=\"dm-bat-body\"\n x=\"2.8\"\n y=\"6.8\"\n width=\"16.4\"\n height=\"10.4\"\n rx=\"2.2\"\n />\n <rect\n className=\"dm-bat-cap\"\n x=\"19.9\"\n y=\"9.9\"\n width=\"2\"\n height=\"4.2\"\n rx=\"0.8\"\n />\n <rect\n className=\"dm-bat-fill\"\n x=\"4.6\"\n y=\"8.6\"\n width=\"12.8\"\n height=\"6.8\"\n rx=\"0.9\"\n style={{ transform: `scaleX(${scale})` }}\n />\n {props.charging ? (\n <path\n className=\"dm-bat-bolt\"\n d=\"M11.9 7.4 8.4 12.6h2.9l-.6 4 3.6-5.3h-2.9z\"\n />\n ) : null}\n </svg>\n );\n}\n\n/** Number of the lit bars for a signal strength in dBm */\nfunction signalBars(rssi: number): number {\n if (rssi >= -55) {\n return 4;\n }\n if (rssi >= -67) {\n return 3;\n }\n if (rssi >= -80) {\n return 2;\n }\n return 1;\n}\n\nexport interface IconSignalProps extends OwnIconProps {\n /** Signal strength (RSSI) in dBm */\n rssi: number;\n}\n\n/** Four bars for the signal strength. When it changes, the bars light up or go out one after another. */\nexport function IconSignal(props: IconSignalProps): React.JSX.Element {\n useDmStyles();\n const bars = signalBars(props.rssi);\n return (\n <svg {...svgProps(props, 'dm-sig')}>\n {[0, 1, 2, 3].map(i => (\n <rect\n key={i}\n className={i < bars ? 'dm-sig-on' : undefined}\n x={3 + i * 5}\n y={16 - i * 4}\n width=\"3.2\"\n height={4 + i * 4}\n rx=\"1\"\n />\n ))}\n </svg>\n );\n}\n\n/** An update can be installed: the arrow drops into the tray while the mouse is over the icon or its button */\nexport function IconUpdate(props: OwnIconProps): React.JSX.Element {\n useDmStyles();\n return (\n <svg {...svgProps(props, 'dm-upd')}>\n <path d=\"M4 14.5v3.2A2.3 2.3 0 0 0 6.3 20h11.4a2.3 2.3 0 0 0 2.3-2.3v-3.2\" />\n <path\n className=\"dm-upd-arrow\"\n d=\"M12 3.5v10.5M7.6 9.8 12 14.2l4.4-4.4\"\n />\n </svg>\n );\n}\n\n/**\n * Lets the new value of a status slide in when it changes (not on the first render).\n *\n * @param value the shown value\n * @returns the ref for the element that shows the value\n */\nexport function useValueTick<T extends HTMLElement>(value: unknown): React.RefObject<T | null> {\n const ref = useRef<T>(null);\n const previous = useRef(value);\n useEffect(() => {\n if (Object.is(previous.current, value)) {\n return;\n }\n const goesUp = typeof value === 'number' && typeof previous.current === 'number' && value > previous.current;\n previous.current = value;\n if (!ref.current?.animate || prefersReducedMotion()) {\n return;\n }\n ref.current.animate(\n [\n { transform: `translateY(${goesUp ? 45 : -45}%)`, opacity: 0 },\n { transform: 'none', opacity: 1 },\n ],\n { duration: 300, easing: 'cubic-bezier(0.2, 0.8, 0.3, 1)' },\n );\n }, [value]);\n return ref;\n}\n"]}
|
package/build/Communication.d.ts
CHANGED
|
@@ -49,6 +49,8 @@ export type CommunicationState = {
|
|
|
49
49
|
showInput: InputAction | null;
|
|
50
50
|
inputValue: string | boolean | number | null;
|
|
51
51
|
selectedInstance: string;
|
|
52
|
+
/** Changes after each successful action: a check mark confirms it for a moment. 0 - nothing to show */
|
|
53
|
+
resultFlash?: number;
|
|
52
54
|
};
|
|
53
55
|
/**
|
|
54
56
|
* Communication Component
|
|
@@ -56,12 +58,15 @@ export type CommunicationState = {
|
|
|
56
58
|
export default class Communication<P extends CommunicationProps, S extends CommunicationState> extends Component<P, S> {
|
|
57
59
|
private protocol;
|
|
58
60
|
private responseTimeout;
|
|
61
|
+
private resultFlashTimeout;
|
|
59
62
|
instanceHandler: (action: ActionBase) => () => void;
|
|
60
63
|
deviceHandler: (deviceId: DeviceId, action: ActionBase) => () => void;
|
|
61
64
|
controlHandler: (deviceId: DeviceId, control: ControlBase, state: ControlState) => () => Promise<ioBroker.State | null>;
|
|
62
65
|
controlStateHandler: (deviceId: DeviceId, control: ControlBase) => () => Promise<ioBroker.State | null>;
|
|
63
66
|
constructor(props: P);
|
|
64
67
|
componentWillUnmount(): void;
|
|
68
|
+
/** Confirms a successful action: the spinner is followed by a check mark that draws itself */
|
|
69
|
+
showResultFlash(): void;
|
|
65
70
|
loadAllData(): Promise<void>;
|
|
66
71
|
loadDeviceList(): void;
|
|
67
72
|
updateDevice(_update: DeviceInfo): void;
|
|
@@ -76,7 +81,9 @@ export default class Communication<P extends CommunicationProps, S extends Commu
|
|
|
76
81
|
loadInstanceInfos(): Promise<InstanceDetails>;
|
|
77
82
|
renderMessageDialog(): React.JSX.Element | null;
|
|
78
83
|
renderConfirmDialog(): React.JSX.Element | null;
|
|
84
|
+
/** The toast shows errors: the cross draws itself and shakes */
|
|
79
85
|
renderSnackbar(): React.JSX.Element;
|
|
86
|
+
renderResultFlash(): React.JSX.Element | null;
|
|
80
87
|
getOkButton(button?: ActionButton | 'apply' | 'cancel' | 'close'): React.JSX.Element;
|
|
81
88
|
getCancelButton(button?: ActionButton | 'apply' | 'cancel' | 'close'): React.JSX.Element;
|
|
82
89
|
renderFormDialog(): React.JSX.Element | null;
|
package/build/Communication.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { I18n, Icon, Utils, } from '@iobroker/gui-components';
|
|
2
|
-
import { Check, Close
|
|
1
|
+
import { I18n, Icon, IconCopyToClipboard, IconResult, Utils, } from '@iobroker/gui-components';
|
|
2
|
+
import { Check, Close } from '@mui/icons-material';
|
|
3
3
|
import { Backdrop, Box, Button, Checkbox, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, FormControl, FormControlLabel, Grid, IconButton, Input, InputAdornment, InputLabel, LinearProgress, MenuItem, Select, Slider, Snackbar, TextField, ThemeProvider, Typography, } from '@mui/material';
|
|
4
4
|
import React, { Component } from 'react';
|
|
5
5
|
import JsonConfig from './JsonConfig';
|
|
@@ -8,12 +8,16 @@ import { DmProtocolV2 } from './protocol/DmProtocolV2';
|
|
|
8
8
|
import { DmProtocolV3 } from './protocol/DmProtocolV3';
|
|
9
9
|
import { UnknownDmProtocol } from './protocol/UnknownDmProtocol';
|
|
10
10
|
import { getTranslation } from './Utils';
|
|
11
|
+
import { ensureDmStyles, getThemeVars } from './styles';
|
|
12
|
+
/** How long the check mark after a successful action is shown (the CSS animation takes as long) */
|
|
13
|
+
const RESULT_FLASH_DURATION = 1250;
|
|
11
14
|
/**
|
|
12
15
|
* Communication Component
|
|
13
16
|
*/
|
|
14
17
|
export default class Communication extends Component {
|
|
15
18
|
protocol = new UnknownDmProtocol();
|
|
16
19
|
responseTimeout = null;
|
|
20
|
+
resultFlashTimeout = null;
|
|
17
21
|
// eslint-disable-next-line react/no-unused-class-component-methods
|
|
18
22
|
instanceHandler;
|
|
19
23
|
// eslint-disable-next-line react/no-unused-class-component-methods
|
|
@@ -24,6 +28,7 @@ export default class Communication extends Component {
|
|
|
24
28
|
controlStateHandler;
|
|
25
29
|
constructor(props) {
|
|
26
30
|
super(props);
|
|
31
|
+
ensureDmStyles();
|
|
27
32
|
this.state = {
|
|
28
33
|
showSpinner: false,
|
|
29
34
|
showToast: null,
|
|
@@ -34,6 +39,7 @@ export default class Communication extends Component {
|
|
|
34
39
|
showConfirmation: null,
|
|
35
40
|
showInput: null,
|
|
36
41
|
inputValue: null,
|
|
42
|
+
resultFlash: 0,
|
|
37
43
|
selectedInstance: this.props.selectedInstance ??
|
|
38
44
|
this.props.instance ??
|
|
39
45
|
(window.localStorage.getItem('dmSelectedInstance') || ''),
|
|
@@ -76,6 +82,21 @@ export default class Communication extends Component {
|
|
|
76
82
|
clearTimeout(this.responseTimeout);
|
|
77
83
|
this.responseTimeout = null;
|
|
78
84
|
}
|
|
85
|
+
if (this.resultFlashTimeout) {
|
|
86
|
+
clearTimeout(this.resultFlashTimeout);
|
|
87
|
+
this.resultFlashTimeout = null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** Confirms a successful action: the spinner is followed by a check mark that draws itself */
|
|
91
|
+
showResultFlash() {
|
|
92
|
+
if (this.resultFlashTimeout) {
|
|
93
|
+
clearTimeout(this.resultFlashTimeout);
|
|
94
|
+
}
|
|
95
|
+
this.setState({ resultFlash: Date.now() });
|
|
96
|
+
this.resultFlashTimeout = setTimeout(() => {
|
|
97
|
+
this.resultFlashTimeout = null;
|
|
98
|
+
this.setState({ resultFlash: 0 });
|
|
99
|
+
}, RESULT_FLASH_DURATION);
|
|
79
100
|
}
|
|
80
101
|
// eslint-disable-next-line class-methods-use-this
|
|
81
102
|
loadAllData() {
|
|
@@ -98,8 +119,7 @@ export default class Communication extends Component {
|
|
|
98
119
|
const send = async () => {
|
|
99
120
|
this.setState({ showSpinner: true });
|
|
100
121
|
this.responseTimeout = setTimeout(() => {
|
|
101
|
-
this.setState({ showSpinner: false });
|
|
102
|
-
window.alert(I18n.t('ra_No response from the backend'));
|
|
122
|
+
this.setState({ showSpinner: false, showToast: I18n.t('ra_No response from the backend') });
|
|
103
123
|
}, messageToSend.timeout || 5000);
|
|
104
124
|
const response = await this.protocol.sendAction(command, messageToSend);
|
|
105
125
|
if (this.responseTimeout) {
|
|
@@ -219,6 +239,7 @@ export default class Communication extends Component {
|
|
|
219
239
|
}
|
|
220
240
|
else {
|
|
221
241
|
this.setState({ showSpinner: false });
|
|
242
|
+
this.showResultFlash();
|
|
222
243
|
}
|
|
223
244
|
break;
|
|
224
245
|
default:
|
|
@@ -295,8 +316,18 @@ export default class Communication extends Component {
|
|
|
295
316
|
React.createElement(Button, { variant: "contained", color: "primary", onClick: () => confirm.handleClose(true), autoFocus: true }, getTranslation('yesButtonText')),
|
|
296
317
|
React.createElement(Button, { variant: "contained", color: "grey", onClick: () => confirm.handleClose(false), autoFocus: true }, getTranslation('noButtonText')))));
|
|
297
318
|
}
|
|
319
|
+
/** The toast shows errors: the cross draws itself and shakes */
|
|
298
320
|
renderSnackbar() {
|
|
299
|
-
return (React.createElement(Snackbar, { open: !!this.state.showToast, autoHideDuration: 6_000, onClose: () => this.setState({ showToast: null }), message: this.state.showToast }
|
|
321
|
+
return (React.createElement(Snackbar, { open: !!this.state.showToast, autoHideDuration: 6_000, onClose: () => this.setState({ showToast: null }), message: this.state.showToast ? (React.createElement("span", { className: "dm-toast" },
|
|
322
|
+
React.createElement(IconResult, { type: "error", replay: this.state.showToast }),
|
|
323
|
+
this.state.showToast)) : null }));
|
|
324
|
+
}
|
|
325
|
+
renderResultFlash() {
|
|
326
|
+
if (!this.state.resultFlash) {
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
return (React.createElement("div", { key: this.state.resultFlash, className: "dm-result", style: getThemeVars(this.props.theme) },
|
|
330
|
+
React.createElement(IconResult, { type: "success" })));
|
|
300
331
|
}
|
|
301
332
|
getOkButton(button) {
|
|
302
333
|
if (typeof button === 'string') {
|
|
@@ -359,7 +390,9 @@ export default class Communication extends Component {
|
|
|
359
390
|
? button.color
|
|
360
391
|
: undefined,
|
|
361
392
|
...(button.style || undefined),
|
|
362
|
-
},
|
|
393
|
+
},
|
|
394
|
+
// The copy symbol confirms the copy with a check mark
|
|
395
|
+
onClick: () => {
|
|
363
396
|
if (button.copyToClipboardAttr && form.data) {
|
|
364
397
|
const val = form.data[button.copyToClipboardAttr];
|
|
365
398
|
if (typeof val === 'string') {
|
|
@@ -368,16 +401,14 @@ export default class Communication extends Component {
|
|
|
368
401
|
else {
|
|
369
402
|
Utils.copyToClipboard(JSON.stringify(val, null, 2));
|
|
370
403
|
}
|
|
371
|
-
window.alert(I18n.t('copied'));
|
|
372
404
|
}
|
|
373
405
|
else if (form.data) {
|
|
374
406
|
Utils.copyToClipboard(JSON.stringify(form.data, null, 2));
|
|
375
|
-
window.alert(I18n.t('copied'));
|
|
376
407
|
}
|
|
377
408
|
else {
|
|
378
|
-
|
|
409
|
+
this.setState({ showToast: I18n.t('nothingToCopy') });
|
|
379
410
|
}
|
|
380
|
-
}, startIcon: button?.icon ? React.createElement(Icon, { src: button?.icon }) : React.createElement(
|
|
411
|
+
}, startIcon: button?.icon ? React.createElement(Icon, { src: button?.icon }) : React.createElement(IconCopyToClipboard, null) }, getTranslation(button?.label || 'ctcButtonText', button?.noTranslation)));
|
|
381
412
|
}
|
|
382
413
|
else if (button === 'apply' || button.type === 'apply') {
|
|
383
414
|
buttons.push(this.getOkButton(button));
|
|
@@ -587,7 +618,8 @@ export default class Communication extends Component {
|
|
|
587
618
|
this.renderProgressDialog(),
|
|
588
619
|
this.renderConfirmationDialog(),
|
|
589
620
|
this.renderInputDialog(),
|
|
590
|
-
this.renderSpinner()
|
|
621
|
+
this.renderSpinner(),
|
|
622
|
+
this.renderResultFlash()));
|
|
591
623
|
}
|
|
592
624
|
}
|
|
593
625
|
//# sourceMappingURL=Communication.js.map
|