@iobroker/dm-gui-components 10.0.2 → 10.0.4

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 CHANGED
@@ -1,210 +1,326 @@
1
- # ReactJS component for ioBroker device manager
2
-
3
- This component requires the [dm-utils](https://github.com/ioBroker/dm-utils) in an adapter.
4
-
5
- ## Usage
6
-
7
- ```jsx
8
- import React from 'react';
9
- import DeviceList from '@iobroker/dm-gui-components';
10
-
11
- //...
12
- render() {
13
- return <DeviceList
14
- socket={this.props.socket}
15
- selectedInstance="matter.0"
16
- />;
17
- }
18
- ```
19
-
20
- ## Icon names
21
-
22
- You can use the following icon names for actions and controls.
23
- Icons are resolved by the action/control `id` or by the `icon` property. You can also provide a `data:image/...` base64 string for custom icons.
24
-
25
- ### Action and control icons (by name)
26
-
27
- | Name(s) | MUI Icon | Description |
28
- |----------------------------|--------------------|--------------------------------------|
29
- | `edit`, `rename` | Edit | Edit or rename an item |
30
- | `delete` | Delete | Delete an item |
31
- | `refresh` | Refresh | Refresh / reload |
32
- | `newDevice`, `new`, `add` | Add | Add or create a new item |
33
- | `discover`, `search` | Search | Discover or search for devices |
34
- | `unpairDevice`, `unpair` | LinkOff | Unpair / unlink a device |
35
- | `pairDevice`, `pair` | Link | Pair / link a device |
36
- | `identify` | NotListedLocation | Identify / locate a device |
37
- | `play` | PlayArrow | Start playback |
38
- | `stop` | Stop | Stop playback |
39
- | `pause` | Pause | Pause playback |
40
- | `forward`, `next` | FastForward | Skip forward / next track |
41
- | `rewind`, `previous` | FastRewind | Skip backward / previous track |
42
- | `lamp`, `light` | Lightbulb | Light / lamp control |
43
- | `backlight` | Fluorescent | Backlight control |
44
- | `dimmer` | WbIncandescent | Dimmer control |
45
- | `socket` | Power | Power socket control |
46
- | `settings` | Settings | Settings / configuration |
47
- | `users`, `group` | Group | User group |
48
- | `user` | Person | Single user |
49
- | `update` | Upgrade | Update / upgrade |
50
- | `qrcode` | QrCode | QR code |
51
- | `info` | Info | Information |
52
- | `lines` | Article | Text lines / log |
53
- | `web` | Launch | Open web link |
54
-
55
- Any unrecognized name renders a **QuestionMark** icon as fallback.
56
-
57
- ### Legacy Font Awesome icons
58
-
59
- These names are supported for backward compatibility. Prefer the names from the table above.
60
-
61
- | Name(s) | MUI Icon | Description |
62
- |--------------------------------|--------------------|--------------------------------|
63
- | `fa-trash-can`, `fa-trash` | Delete | Delete |
64
- | `fa-pen` | Edit | Edit |
65
- | `fa-redo-alt` | Refresh | Refresh / redo |
66
- | `fa-plus` | Add | Add |
67
- | `fa-qrcode`, `qrcode` | QrCode | QR code |
68
- | `fa-wifi` | Wifi | Wi-Fi enabled |
69
- | `fa-wifi-slash` | WifiOff | Wi-Fi disabled |
70
- | `fa-bluetooth` | Bluetooth | Bluetooth enabled |
71
- | `fa-bluetooth-slash` | BluetoothDisabled | Bluetooth disabled |
72
- | `fa-eye` | Visibility | View / visible |
73
- | `fa-search` | Search | Search |
74
- | `fa-unlink` | LinkOff | Unlink |
75
- | `fa-link` | Link | Link |
76
- | `fa-search-location` | NotListedLocation | Search location / identify |
77
- | `fa-play` | PlayArrow | Play |
78
- | `fa-stop` | Stop | Stop |
79
- | `fa-pause` | Pause | Pause |
80
-
81
- Unrecognized FA names render a **QuestionMark** icon.
82
-
83
- <!--
84
- Placeholder for the next version (at the beginning of the line):
85
- ### **WORK IN PROGRESS**
86
- -->
87
-
1
+ # ReactJS component for ioBroker device manager
2
+
3
+ This component requires the [dm-utils](https://github.com/ioBroker/dm-utils) in an adapter.
4
+
5
+ ## Usage
6
+
7
+ ```jsx
8
+ import React from 'react';
9
+ import DeviceList from '@iobroker/dm-gui-components';
10
+
11
+ //...
12
+ render() {
13
+ return <DeviceList
14
+ socket={this.props.socket}
15
+ selectedInstance="matter.0"
16
+ />;
17
+ }
18
+ ```
19
+
20
+ ## Icon names
21
+
22
+ You can use the following icon names for actions and controls.
23
+ Icons are resolved by the action/control `id` or by the `icon` property. You can also provide a `data:image/...` base64 string for custom icons.
24
+
25
+ ### Action and control icons (by name)
26
+
27
+ | Name(s) | MUI Icon | Description |
28
+ |----------------------------|--------------------|--------------------------------------|
29
+ | `edit`, `rename` | Edit | Edit or rename an item |
30
+ | `delete` | Delete | Delete an item |
31
+ | `refresh` | Refresh | Refresh / reload |
32
+ | `newDevice`, `new`, `add` | Add | Add or create a new item |
33
+ | `discover`, `search` | Search | Discover or search for devices |
34
+ | `unpairDevice`, `unpair` | LinkOff | Unpair / unlink a device |
35
+ | `pairDevice`, `pair` | Link | Pair / link a device |
36
+ | `identify` | NotListedLocation | Identify / locate a device |
37
+ | `play` | PlayArrow | Start playback |
38
+ | `stop` | Stop | Stop playback |
39
+ | `pause` | Pause | Pause playback |
40
+ | `forward`, `next` | FastForward | Skip forward / next track |
41
+ | `rewind`, `previous` | FastRewind | Skip backward / previous track |
42
+ | `lamp`, `light` | Lightbulb | Light / lamp control |
43
+ | `backlight` | Fluorescent | Backlight control |
44
+ | `dimmer` | WbIncandescent | Dimmer control |
45
+ | `socket` | Power | Power socket control |
46
+ | `settings` | Settings | Settings / configuration |
47
+ | `users`, `group` | Group | User group |
48
+ | `user` | Person | Single user |
49
+ | `update` | Upgrade | Update / upgrade |
50
+ | `qrcode` | QrCode | QR code |
51
+ | `info` | Info | Information |
52
+ | `lines` | Article | Text lines / log |
53
+ | `web` | Launch | Open web link |
54
+
55
+ Any unrecognized name renders a **QuestionMark** icon as fallback.
56
+
57
+ ### Legacy Font Awesome icons
58
+
59
+ These names are supported for backward compatibility. Prefer the names from the table above.
60
+
61
+ | Name(s) | MUI Icon | Description |
62
+ |--------------------------------|--------------------|--------------------------------|
63
+ | `fa-trash-can`, `fa-trash` | Delete | Delete |
64
+ | `fa-pen` | Edit | Edit |
65
+ | `fa-redo-alt` | Refresh | Refresh / redo |
66
+ | `fa-plus` | Add | Add |
67
+ | `fa-qrcode`, `qrcode` | QrCode | QR code |
68
+ | `fa-wifi` | Wifi | Wi-Fi enabled |
69
+ | `fa-wifi-slash` | WifiOff | Wi-Fi disabled |
70
+ | `fa-bluetooth` | Bluetooth | Bluetooth enabled |
71
+ | `fa-bluetooth-slash` | BluetoothDisabled | Bluetooth disabled |
72
+ | `fa-eye` | Visibility | View / visible |
73
+ | `fa-search` | Search | Search |
74
+ | `fa-unlink` | LinkOff | Unlink |
75
+ | `fa-link` | Link | Link |
76
+ | `fa-search-location` | NotListedLocation | Search location / identify |
77
+ | `fa-play` | PlayArrow | Play |
78
+ | `fa-stop` | Stop | Stop |
79
+ | `fa-pause` | Pause | Pause |
80
+
81
+ Unrecognized FA names render a **QuestionMark** icon.
82
+
83
+ ## Status line
84
+
85
+ The status line at the top of a device card shows the built-in indicators (connection, RSSI, battery,
86
+ warning, update, enabled). Two mechanisms let an adapter add its own entries there.
87
+
88
+ > **Note:** the types are defined in `@iobroker/dm-utils` from version 3.2.0 on
89
+ > (`StatusIndicator`, `StatusIndicatorLevel`, `IndicatorColor`, `ActionPlacement`) and are only
90
+ > re-exported here. See the README of `dm-utils` for the adapter side.
91
+
92
+ ### Actions in the status line
93
+
94
+ Any action can be moved from the button row at the bottom of the card into the status line with
95
+ `placement: 'status'`:
96
+
97
+ ```js
98
+ const actions = [{ id: 'openLog', icon: 'lines', placement: 'status', handler: openLogHandler }];
99
+ ```
100
+
101
+ The action keeps all its features (`icon`, `color`, `title`, `confirmation`, `inputBefore`, `url`, …)
102
+ and is not rendered a second time in the footer.
103
+
104
+ ### Custom indicators
105
+
106
+ `DeviceInfo.indicators` (device card) and `InstanceDetails.indicators` (toolbar) describe indicators
107
+ whose appearance follows a state or object value live. Every visual property accepts either a literal
108
+ value or a `{ stateId }` / `{ objectId, property }` reference.
109
+
110
+ | Property | Description |
111
+ | ---------------- | ---------------------------------------------------------------------------------------------------------- |
112
+ | `id` | Unique ID within the device / instance |
113
+ | `value` | Live value. Controls visibility, `iconOn`/`colorOn` and the matching `levels` entry |
114
+ | `icon` | Icon name (see above), `fa-*`, `data:image/...`, URL or path |
115
+ | `iconOn` | Icon used while the value is truthy |
116
+ | `color` | `ok`, `warning`, `error`, `info`, `inactive`, `primary`, `secondary` or a CSS color |
117
+ | `colorOn` | Color used while the value is truthy |
118
+ | `text` | Text below the icon |
119
+ | `showValue` | Show the value as text below the icon |
120
+ | `unit` | Appended to the text |
121
+ | `tooltip` | Tooltip |
122
+ | `levels` | Value ranges mapped to `icon` / `color` / `text` / `tooltip`. First match wins |
123
+ | `actionId` | ID of an action of the same device/instance. Makes the indicator clickable |
124
+ | `hideIfEmpty` | Hide while the value is `undefined`, `null`, `''` or `false` (default `true`; the number `0` stays visible) |
125
+ | `order` | Sort order in the line (default 100) |
126
+ | `configurable` | Let the user show or hide this indicator in the toolbar |
127
+ | `defaultVisible` | Visibility of a configurable indicator until the user decides otherwise (default `true`) |
128
+ | `label` | Name in the visibility settings. Falls back to `tooltip` and then to `id` |
129
+
130
+ A `levels` entry matches by exact `value`, by `min`/`max` (both inclusive, numeric values only) or,
131
+ if it defines no condition at all, as a catch-all — put such an entry last.
132
+
133
+ If `actionId` is given, a click triggers that action through the normal device/instance action flow,
134
+ including `confirmation`, `inputBefore`, `url`, the progress dialog and the `refresh` handling. The
135
+ referenced action is then not rendered as a normal button anymore.
136
+
137
+ ```js
138
+ const deviceInfo = {
139
+ id: 'sensor-1',
140
+ name: 'Window sensor kitchen',
141
+ actions: [{ id: 'openLog', icon: 'lines', description: { en: 'Open device log' }, handler: openLogHandler }],
142
+ status: { connection: { stateId: 'zigbee.0.abc.available', mapping: { true: 'connected', false: 'disconnected' } } },
143
+ indicators: [
144
+ {
145
+ id: 'linkQuality',
146
+ value: { stateId: 'zigbee.0.abc.link_quality' },
147
+ icon: 'fa-wifi',
148
+ showValue: true,
149
+ unit: 'lqi',
150
+ tooltip: { en: 'Link quality', de: 'Verbindungsqualität' },
151
+ levels: [
152
+ { max: 50, color: 'error' },
153
+ { max: 100, color: 'warning' },
154
+ { color: 'ok' },
155
+ ],
156
+ actionId: 'openLog',
157
+ },
158
+ {
159
+ id: 'tamper',
160
+ value: { stateId: 'zigbee.0.abc.tamper' },
161
+ icon: 'fa-eye',
162
+ colorOn: 'error',
163
+ tooltip: { en: 'Tamper contact triggered' },
164
+ },
165
+ ],
166
+ };
167
+ ```
168
+
169
+ The indicator line wraps if it does not fit on one line, so the number of indicators is not limited.
170
+
171
+ ### Indicators the user can switch off
172
+
173
+ An indicator marked with `configurable: true` can be shown or hidden by the user through the
174
+ sliders button in the toolbar. The choice is stored in the browser (`localStorage`, one entry per
175
+ instance) and never reaches the backend. `defaultVisible: false` starts with a hidden indicator, so
176
+ rarely needed information does not clutter the cards of everybody.
177
+
178
+ Indicators with the same `id` — typically the same indicator on many devices — are configured
179
+ together. If a hidden indicator references an action, that action is hidden as well and does not
180
+ reappear as a button in the footer.
181
+
182
+ ```js
183
+ const indicator = {
184
+ id: 'tamper',
185
+ value: { stateId: 'zigbee.0.abc.tamper' },
186
+ icon: 'fa-eye',
187
+ colorOn: 'error',
188
+ label: { en: 'Tamper contact', de: 'Sabotagekontakt' },
189
+ configurable: true,
190
+ defaultVisible: false,
191
+ };
192
+ ```
193
+
194
+ <!--
195
+ Placeholder for the next version (at the beginning of the line):
196
+ ### **WORK IN PROGRESS**
197
+ -->
198
+
88
199
  ## Changelog
89
- ### 10.0.2 (2026-07-25)
200
+ ### 10.0.4 (2026-07-26)
201
+ - (@GermanBluefox) Added custom status indicators for devices and instances
202
+ - (@GermanBluefox) Added `placement: 'status'` for device actions
203
+ - (@GermanBluefox) Added user-configurable visibility of indicators
204
+
205
+ ### 10.0.3 (2026-07-25)
90
206
  # (@GermanBluefox) Breaking: React 19, MUI 9, TS 6
91
207
 
92
- ### 9.1.12 (2026-07-24)
208
+ ### 9.1.12 (2026-07-24)
93
209
  - (@GermanBluefox) Updated packages
94
210
 
95
- ### 9.1.9 (2026-07-20)
211
+ ### 9.1.9 (2026-07-20)
96
212
  - (@GermanBluefox) Fixing tabs
97
213
 
98
- ### 9.1.8 (2026-07-12)
214
+ ### 9.1.8 (2026-07-12)
99
215
  - (@GermanBluefox) Strict types
100
216
 
101
- ### 9.1.7 (2026-06-21)
217
+ ### 9.1.7 (2026-06-21)
102
218
  - (@GermanBluefox) Remove double loading of devices
103
219
 
104
- ### 9.1.6 (2026-06-21)
220
+ ### 9.1.6 (2026-06-21)
105
221
  - (@GermanBluefox) Updated packages
106
222
 
107
- ### 9.1.5 (2026-06-20)
223
+ ### 9.1.5 (2026-06-20)
108
224
  - (@GermanBluefox) Implemented filter on different fields (name, id, type, alive, enabled, connected, etc.)
109
225
 
110
- ### 9.1.1 (2026-06-13)
226
+ ### 9.1.1 (2026-06-13)
111
227
  - (@GermanBluefox) Updated packages
112
228
 
113
- ### 9.1.0 (2026-06-06)
114
- - (@GermanBluefox) Replaced the instance selector with a root page that shows all instances as cards; the toolbar shows a back arrow to return to the root page
229
+ ### 9.1.0 (2026-06-06)
230
+ - (@GermanBluefox) Replaced the instance selector with a root page that shows all instances as cards; the toolbar shows a back arrow to return to the root page
115
231
  - (@GermanBluefox) Updated packages
116
232
 
117
- ### 9.0.27 (2026-04-11)
233
+ ### 9.0.27 (2026-04-11)
118
234
  - (@GermanBluefox) Fixed filter for devices
119
235
 
120
- ### 9.0.26 (2026-03-31)
236
+ ### 9.0.26 (2026-03-31)
121
237
  - (@GermanBluefox) Added support for `applyDisabledRule`
122
238
 
123
- ### 9.0.24 (2026-03-30)
239
+ ### 9.0.24 (2026-03-30)
124
240
  - (@GermanBluefox) Layout improved
125
241
 
126
- ### 9.0.7 (2026-03-27)
242
+ ### 9.0.7 (2026-03-27)
127
243
  - (@GermanBluefox) Added possibility to show custom information on Card
128
244
 
129
- ### 9.0.6 (2026-03-26)
130
-
245
+ ### 9.0.6 (2026-03-26)
246
+
131
247
  - (@GermanBluefox) Small layout optimizations for controls
132
248
 
133
- ### 9.0.3 (2026-03-04)
134
-
135
- - (@UncleSamSwiss) Fix handling of "result" response from action handler (update, delete a device)
136
- - (@UncleSamSwiss) Fix handling of V1 protocol action responses
249
+ ### 9.0.3 (2026-03-04)
250
+
251
+ - (@UncleSamSwiss) Fix handling of "result" response from action handler (update, delete a device)
252
+ - (@UncleSamSwiss) Fix handling of V1 protocol action responses
137
253
  - (@UncleSamSwiss) Fix handling of "refresh" response from instance actions
138
254
 
139
- ### 9.0.2 (2026-03-03)
140
-
255
+ ### 9.0.2 (2026-03-03)
256
+
141
257
  - (@UncleSamSwiss) Implemented v3 protocol: added support to use states and objects as values
142
258
 
143
- ### 8.0.9 (2026-01-28)
144
-
259
+ ### 8.0.9 (2026-01-28)
260
+
145
261
  - (@GermanBluefox) Analyze an API version and do not show anything if a version is higher than supported
146
262
 
147
- ### 8.0.8 (2026-01-27)
148
-
263
+ ### 8.0.8 (2026-01-27)
264
+
149
265
  - (@GermanBluefox) Added support of instance selection if not provided
150
266
 
151
- ### 8.0.7 (2026-01-02)
152
-
267
+ ### 8.0.7 (2026-01-02)
268
+
153
269
  - (@GermanBluefox) Added `ignoreApplyDisabled` flag
154
270
 
155
- ### 8.0.6 (2025-12-30)
156
-
157
- - (@GermanBluefox) Added update icon for device actions
271
+ ### 8.0.6 (2025-12-30)
272
+
273
+ - (@GermanBluefox) Added update icon for device actions
158
274
  - (@GermanBluefox) Added indeterminate progress
159
275
 
160
- ### 8.0.4 (2025-10-25)
161
-
276
+ ### 8.0.4 (2025-10-25)
277
+
162
278
  - (@GermanBluefox) Updated packages
163
279
 
164
- ### 8.0.2 (2025-10-23)
165
-
280
+ ### 8.0.2 (2025-10-23)
281
+
166
282
  - (@GermanBluefox) Renamed gui-components to adapter-react-v5
167
283
 
168
- ### 8.0.1 (2025-10-23)
169
-
284
+ ### 8.0.1 (2025-10-23)
285
+
170
286
  - (@GermanBluefox) Make package independent
171
287
 
172
- ### 0.0.10 (2023-12-14)
173
-
288
+ ### 0.0.10 (2023-12-14)
289
+
174
290
  - (bluefox) Changed layout of the device list
175
291
 
176
- ### 0.0.7 (2023-12-14)
177
-
292
+ ### 0.0.7 (2023-12-14)
293
+
178
294
  - (bluefox) Added alive flag
179
295
 
180
- ### 0.0.4 (2023-12-12)
181
-
296
+ ### 0.0.4 (2023-12-12)
297
+
182
298
  - (bluefox) return the style of big cards
183
299
 
184
- ### 0.0.3 (2023-12-12)
185
-
300
+ ### 0.0.3 (2023-12-12)
301
+
186
302
  - (bluefox) initial commit
187
303
 
188
- ## License
189
-
190
- MIT License
191
-
192
- Copyright (c) 2023-2026 Jey Cee <iobroker@all-smart.net>
193
-
194
- Permission is hereby granted, free of charge, to any person obtaining a copy
195
- of this software and associated documentation files (the "Software"), to deal
196
- in the Software without restriction, including without limitation the rights
197
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
198
- copies of the Software, and to permit persons to whom the Software is
199
- furnished to do so, subject to the following conditions:
200
-
201
- The above copyright notice and this permission notice shall be included in all
202
- copies or substantial portions of the Software.
203
-
204
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
205
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
206
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
207
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
208
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
209
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
304
+ ## License
305
+
306
+ MIT License
307
+
308
+ Copyright (c) 2023-2026 Jey Cee <iobroker@all-smart.net>
309
+
310
+ Permission is hereby granted, free of charge, to any person obtaining a copy
311
+ of this software and associated documentation files (the "Software"), to deal
312
+ in the Software without restriction, including without limitation the rights
313
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
314
+ copies of the Software, and to permit persons to whom the Software is
315
+ furnished to do so, subject to the following conditions:
316
+
317
+ The above copyright notice and this permission notice shall be included in all
318
+ copies or substantial portions of the Software.
319
+
320
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
321
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
322
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
323
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
324
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
325
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
210
326
  SOFTWARE.
@@ -27,6 +27,8 @@ interface DeviceCardProps {
27
27
  onlyBatteryProblem?: boolean;
28
28
  /** Device field the text filter applies to. Default `name` */
29
29
  filterField?: DeviceFilterField;
30
+ /** IDs of configurable indicators the user has switched off */
31
+ hiddenIndicators?: string[];
30
32
  /** Reports the resolved model value of this device up to the list (used to build the model filter dropdown) */
31
33
  onModel?: (deviceId: DeviceId, model: string | undefined) => void;
32
34
  }
@@ -85,6 +87,18 @@ export default class DeviceCard extends Component<DeviceCardProps, DeviceCardSta
85
87
  }, parentGroupId?: string): JSX.Element[];
86
88
  renderControlDialog(): JSX.Element | null;
87
89
  renderControls(): JSX.Element | null;
90
+ /**
91
+ * IDs of all actions that are not rendered as a normal button in the footer, because they
92
+ * already have their place in the status line: the reserved actions, the actions with
93
+ * `placement: 'status'` and the actions referenced by a custom indicator.
94
+ */
95
+ private getStatusActionIds;
96
+ /** True if at least one action is left for the button row at the bottom of the card */
97
+ private hasFooterActions;
98
+ /** Resolve how a custom indicator behaves on click by looking up the referenced action */
99
+ private resolveIndicatorAction;
100
+ /** Custom indicators and the actions that requested to be shown in the status line */
101
+ renderIndicators(small?: boolean): JSX.Element | null;
88
102
  renderActions(): JSX.Element[] | null;
89
103
  renderSmall(): JSX.Element;
90
104
  getCardHeaderStyle(theme: IobTheme, maxWidth?: number): React.CSSProperties;
@@ -7,6 +7,7 @@ import DeviceControlComponent from './DeviceControl';
7
7
  import DeviceImageUpload from './DeviceImageUpload';
8
8
  import DeviceStatusComponent from './DeviceStatus';
9
9
  import JsonConfig from './JsonConfig';
10
+ import { StatusIndicators } from './StatusIndicator';
10
11
  import { getTranslation } from './Utils';
11
12
  import { StateOrObjectHandler } from './StateOrObjectHandler';
12
13
  /** Reserved action names (this is copied from https://github.com/ioBroker/dm-utils/blob/main/src/types/base.ts as we can only have type references to dm-utils) */
@@ -75,7 +76,8 @@ const styles = {
75
76
  },
76
77
  statusStyle: {
77
78
  padding: '15px 25px 0 15px',
78
- height: 41,
79
+ // The line may grow if the device provides custom indicators, so only the minimum is fixed
80
+ minHeight: 41,
79
81
  },
80
82
  };
81
83
  function NoImageIcon(props) {
@@ -295,11 +297,62 @@ export default class DeviceCard extends Component {
295
297
  }
296
298
  return null;
297
299
  }
300
+ /**
301
+ * IDs of all actions that are not rendered as a normal button in the footer, because they
302
+ * already have their place in the status line: the reserved actions, the actions with
303
+ * `placement: 'status'` and the actions referenced by a custom indicator.
304
+ */
305
+ getStatusActionIds() {
306
+ const ids = new Set([ACTIONS.STATUS, ACTIONS.ENABLE_DISABLE, ACTIONS.UPDATE, ACTIONS.BATTERY]);
307
+ for (const indicator of this.props.device.indicators || []) {
308
+ if (indicator.actionId) {
309
+ ids.add(indicator.actionId);
310
+ }
311
+ }
312
+ for (const action of this.props.device.actions || []) {
313
+ if (action.placement === 'status') {
314
+ ids.add(action.id);
315
+ }
316
+ }
317
+ return ids;
318
+ }
319
+ /** True if at least one action is left for the button row at the bottom of the card */
320
+ hasFooterActions() {
321
+ const statusActionIds = this.getStatusActionIds();
322
+ return !!this.props.device.actions?.some(action => !statusActionIds.has(action.id));
323
+ }
324
+ /** Resolve how a custom indicator behaves on click by looking up the referenced action */
325
+ resolveIndicatorAction = (actionId) => {
326
+ const action = this.props.device.actions?.find(a => a.id === actionId);
327
+ if (!action) {
328
+ console.warn(`Indicator of device ${JSON.stringify(this.props.device.id)} references the unknown action "${actionId}"`);
329
+ return undefined;
330
+ }
331
+ if ('url' in action && action.url) {
332
+ return { url: getTranslation(action.url), disabled: action.disabled };
333
+ }
334
+ return {
335
+ onClick: this.props.deviceHandler(this.props.device.id, action),
336
+ disabled: action.disabled,
337
+ };
338
+ };
339
+ /** Custom indicators and the actions that requested to be shown in the status line */
340
+ renderIndicators(small) {
341
+ const reserved = [ACTIONS.STATUS, ACTIONS.ENABLE_DISABLE, ACTIONS.UPDATE, ACTIONS.BATTERY];
342
+ const statusActions = this.props.device.actions?.filter(a => a.placement === 'status' && !reserved.includes(a.id)) || [];
343
+ // An indicator switched off by the user is not shown at all. An action referenced by it stays
344
+ // out of the footer too, as hiding the indicator was an explicit decision of the user.
345
+ const indicators = this.props.hiddenIndicators?.length
346
+ ? this.props.device.indicators?.filter(indicator => !this.props.hiddenIndicators.includes(indicator.id))
347
+ : this.props.device.indicators;
348
+ if (!indicators?.length && !statusActions.length) {
349
+ return null;
350
+ }
351
+ return (React.createElement(StatusIndicators, { indicators: indicators, theme: this.props.theme, stateOrObjectHandler: this.stateOrObjectHandler, disabled: !this.props.alive, resolveAction: this.resolveIndicatorAction, style: { marginTop: small ? 2 : 4 } }, statusActions.map(action => (React.createElement(DeviceActionButton, { disabled: !this.props.alive, key: action.id, deviceId: this.props.device.id, action: action, deviceHandler: this.props.deviceHandler })))));
352
+ }
298
353
  renderActions() {
299
- const actions = this.props.device.actions?.filter(a => a.id !== ACTIONS.STATUS &&
300
- a.id !== ACTIONS.ENABLE_DISABLE &&
301
- a.id !== ACTIONS.UPDATE &&
302
- a.id !== ACTIONS.BATTERY);
354
+ const statusActionIds = this.getStatusActionIds();
355
+ const actions = this.props.device.actions?.filter(a => !statusActionIds.has(a.id));
303
356
  return actions?.length
304
357
  ? actions.map(a => (React.createElement(DeviceActionButton, { disabled: !this.props.alive, key: a.id, deviceId: this.props.device.id, action: a, deviceHandler: this.props.deviceHandler })))
305
358
  : null;
@@ -330,7 +383,9 @@ export default class DeviceCard extends Component {
330
383
  }
331
384
  }, color: "primary" },
332
385
  React.createElement(MoreVertIcon, null))) : null),
333
- React.createElement("div", { style: { ...styles.statusStyle, height: 'auto', padding: '8px 15px 0 15px' } }, status.map((s, i) => (React.createElement(DeviceStatusComponent, { key: i, socket: this.props.socket, deviceId: this.props.device.id, connectionType: this.state.connectionType, status: s, enabled: this.state.enabled, statusAction: this.props.device.actions?.find(a => a.id === ACTIONS.STATUS), disableEnableAction: this.props.device.actions?.find(a => a.id === ACTIONS.ENABLE_DISABLE), update: i === 0 ? this.props.device.update : undefined, updateAction: i === 0 ? this.props.device.actions?.find(a => a.id === ACTIONS.UPDATE) : undefined, batteryAction: this.props.device.actions?.find(a => a.id === ACTIONS.BATTERY), deviceHandler: this.props.deviceHandler, theme: this.props.theme, stateOrObjectHandler: this.stateOrObjectHandler })))),
386
+ React.createElement("div", { style: { ...styles.statusStyle, height: 'auto', padding: '8px 15px 0 15px' } },
387
+ status.map((s, i) => (React.createElement(DeviceStatusComponent, { key: i, socket: this.props.socket, deviceId: this.props.device.id, connectionType: this.state.connectionType, status: s, enabled: this.state.enabled, statusAction: this.props.device.actions?.find(a => a.id === ACTIONS.STATUS), disableEnableAction: this.props.device.actions?.find(a => a.id === ACTIONS.ENABLE_DISABLE), update: i === 0 ? this.props.device.update : undefined, updateAction: i === 0 ? this.props.device.actions?.find(a => a.id === ACTIONS.UPDATE) : undefined, batteryAction: this.props.device.actions?.find(a => a.id === ACTIONS.BATTERY), deviceHandler: this.props.deviceHandler, theme: this.props.theme, stateOrObjectHandler: this.stateOrObjectHandler }))),
388
+ this.renderIndicators(true)),
334
389
  React.createElement("div", { style: styles.bodyStyle },
335
390
  React.createElement(Typography, { variant: "body2", style: { ...styles.deviceInfoStyle, padding: '10px 10px 0 10px' } },
336
391
  this.state.identifier ? (React.createElement("div", { onClick: this.copyToClipboard, style: { textOverflow: 'ellipsis', overflow: 'hidden' } },
@@ -346,7 +401,7 @@ export default class DeviceCard extends Component {
346
401
  React.createElement(JsonConfig, { instanceId: this.props.instanceId, socket: this.props.socket, schema: this.props.device.customInfo.schema, data: this.props.device.customInfo.data || {}, onChange: (_data) => {
347
402
  /* ignore */
348
403
  }, themeName: this.props.themeName, themeType: this.props.themeType, theme: this.props.theme, isFloatComma: this.props.isFloatComma, dateFormat: this.props.dateFormat }))) : null,
349
- !!(this.props.device.actions?.length || this.props.device.controls?.length) && (React.createElement("div", { style: {
404
+ !!(this.hasFooterActions() || this.props.device.controls?.length) && (React.createElement("div", { style: {
350
405
  marginTop: 'auto',
351
406
  display: 'flex',
352
407
  gap: 4,
@@ -422,7 +477,9 @@ export default class DeviceCard extends Component {
422
477
  }
423
478
  }, color: "primary" },
424
479
  React.createElement(MoreVertIcon, null))) : null),
425
- React.createElement("div", { style: styles.statusStyle }, status.map((s, i) => (React.createElement(DeviceStatusComponent, { key: i, socket: this.props.socket, deviceId: this.props.device.id, connectionType: this.state.connectionType, status: s, enabled: this.state.enabled, statusAction: this.props.device.actions?.find(a => a.id === ACTIONS.STATUS), disableEnableAction: this.props.device.actions?.find(a => a.id === ACTIONS.ENABLE_DISABLE), update: i === 0 ? this.props.device.update : undefined, updateAction: i === 0 ? this.props.device.actions?.find(a => a.id === ACTIONS.UPDATE) : undefined, batteryAction: this.props.device.actions?.find(a => a.id === ACTIONS.BATTERY), deviceHandler: this.props.deviceHandler, theme: this.props.theme, stateOrObjectHandler: this.stateOrObjectHandler })))),
480
+ React.createElement("div", { style: styles.statusStyle },
481
+ status.map((s, i) => (React.createElement(DeviceStatusComponent, { key: i, socket: this.props.socket, deviceId: this.props.device.id, connectionType: this.state.connectionType, status: s, enabled: this.state.enabled, statusAction: this.props.device.actions?.find(a => a.id === ACTIONS.STATUS), disableEnableAction: this.props.device.actions?.find(a => a.id === ACTIONS.ENABLE_DISABLE), update: i === 0 ? this.props.device.update : undefined, updateAction: i === 0 ? this.props.device.actions?.find(a => a.id === ACTIONS.UPDATE) : undefined, batteryAction: this.props.device.actions?.find(a => a.id === ACTIONS.BATTERY), deviceHandler: this.props.deviceHandler, theme: this.props.theme, stateOrObjectHandler: this.stateOrObjectHandler }))),
482
+ this.renderIndicators()),
426
483
  React.createElement("div", { style: styles.bodyStyle },
427
484
  React.createElement(Typography, { variant: "body1", style: styles.deviceInfoStyle },
428
485
  this.state.identifier ? (React.createElement("div", { onClick: this.copyToClipboard },
@@ -444,7 +501,7 @@ export default class DeviceCard extends Component {
444
501
  React.createElement(JsonConfig, { instanceId: this.props.instanceId, socket: this.props.socket, schema: this.props.device.customInfo.schema, data: this.props.device.customInfo.data || {}, onChange: (_data) => {
445
502
  /* ignore */
446
503
  }, themeName: this.props.themeName, themeType: this.props.themeType, theme: this.props.theme, isFloatComma: this.props.isFloatComma, dateFormat: this.props.dateFormat }))) : null,
447
- !!(this.props.device.actions?.length || this.props.device.controls?.length) && (React.createElement("div", { style: {
504
+ !!(this.hasFooterActions() || this.props.device.controls?.length) && (React.createElement("div", { style: {
448
505
  marginTop: 'auto',
449
506
  display: 'flex',
450
507
  gap: 8,