@iobroker/dm-gui-components 10.0.20 → 10.0.22

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
@@ -197,6 +197,18 @@ const indicator = {
197
197
  -->
198
198
 
199
199
  ## Changelog
200
+ ### 10.0.22 (2026-09-18)
201
+ - (@GermanBluefox) Updated packages
202
+ - (@GermanBluefox) Renamed `Config manager` to `Konfig-Manager` (DE)
203
+ - (@GermanBluefox) Text colors in toolbar was changed
204
+
205
+ ### 10.0.21 (2026-08-31)
206
+ - (@GermanBluefox) Improved: device list performance with many devices (ioBroker/ioBroker.matter#864) — cards are only rendered while they are near the viewport, a reload no longer unmounts and remounts the whole list, and a second load requested while one is running is no longer executed in parallel
207
+ - (@GermanBluefox) Changed: filtering moved from the cards into the list, so filtered out devices no longer stay mounted with all their subscriptions
208
+ - (@GermanBluefox) Changed: the device fields that may be bound to a state or an object are resolved centrally through one shared subscription handler instead of one handler per card
209
+ - (@GermanBluefox) Corrected: subscriptions leaked if a component was unmounted while `addListener` was still pending
210
+ - (@GermanBluefox) Corrected: the device list triggered reloads and state changes as a side effect of `render()`
211
+
200
212
  ### 10.0.20 (2026-08-28)
201
213
  - (@GermanBluefox) Changed the card layout
202
214
 
@@ -1,13 +1,31 @@
1
- import React, { Component, type JSX } from 'react';
1
+ import React, { PureComponent, type JSX } from 'react';
2
2
  import { type Connection, type IobTheme, type ThemeName, type ThemeType } from '@iobroker/gui-components';
3
- import type { ActionBase, ControlBase, ControlState, DeviceDetails, DeviceControl, DeviceInfo, DeviceId, ConfigConnectionType } from './protocol/api';
4
- /** Device fields that can be used for the text filter */
5
- export type DeviceFilterField = 'name' | 'identifier' | 'manufacturer' | 'model';
3
+ import type { ActionBase, ControlBase, ControlState, DeviceDetails, DeviceControl, DeviceInfo, DeviceId } from './protocol/api';
4
+ import type { StateOrObjectHandler } from './StateOrObjectHandler';
5
+ import type { ResolvedDeviceFields } from './DeviceFields';
6
+ export type { DeviceFilterField } from './DeviceFields';
7
+ /**
8
+ * Footprint of a card. The list needs it to give a not yet rendered card a placeholder of exactly
9
+ * the same size, so that neither the layout nor the length of the scrollbar changes.
10
+ */
11
+ export declare const CARD_WIDTH = 300;
12
+ export declare const CARD_MIN_HEIGHT = 280;
13
+ export declare const CARD_MARGIN = 10;
14
+ export declare const SMALL_CARD_WIDTH = 200;
15
+ export declare const SMALL_CARD_MIN_HEIGHT = 200;
16
+ export declare const SMALL_CARD_MARGIN = 5;
6
17
  interface DeviceCardProps {
7
- filter?: string;
8
18
  id: DeviceId;
9
19
  identifierLabel: ioBroker.StringOrTranslated;
10
20
  device: DeviceInfo;
21
+ /**
22
+ * The device fields resolved by the list. They may be bound to a state or an object, and the
23
+ * list resolves them centrally: a card that is not rendered cannot resolve its own name, and
24
+ * the filter of the list needs the values of all devices, not only of the rendered ones.
25
+ */
26
+ fields: ResolvedDeviceFields;
27
+ /** Handler of the whole list, so that identical states/objects are only subscribed once */
28
+ stateOrObjectHandler: StateOrObjectHandler;
11
29
  instanceId: string;
12
30
  socket: Connection;
13
31
  uploadImagesToInstance?: string;
@@ -15,63 +33,47 @@ interface DeviceCardProps {
15
33
  controlHandler: (deviceId: DeviceId, control: ControlBase, state: ControlState) => () => Promise<ioBroker.State | null>;
16
34
  controlStateHandler: (deviceId: DeviceId, control: ControlBase) => () => Promise<ioBroker.State | null>;
17
35
  smallCards?: boolean;
36
+ /** The card is rendered inside a container that already has the footprint of a card */
37
+ fillContainer?: boolean;
18
38
  alive: boolean;
19
39
  themeName: ThemeName;
20
40
  themeType: ThemeType;
21
41
  theme: IobTheme;
22
42
  isFloatComma: boolean;
23
43
  dateFormat: string;
24
- /** If true, only devices that have an available update are shown */
25
- onlyUpdatable?: boolean;
26
- /** If true, only devices that have a battery problem (empty/low battery) are shown */
27
- onlyBatteryProblem?: boolean;
28
- /** Device field the text filter applies to. Default `name` */
29
- filterField?: DeviceFilterField;
30
44
  /** IDs of configurable indicators the user has switched off */
31
45
  hiddenIndicators?: string[];
32
- /** Reports the resolved model value of this device up to the list (used to build the model filter dropdown) */
33
- onModel?: (deviceId: DeviceId, model: string | undefined) => void;
34
46
  }
35
47
  interface DeviceCardState {
36
48
  open: boolean;
37
49
  details: DeviceDetails | null;
38
50
  data: Record<string, any>;
39
51
  showControlDialog: boolean;
40
- name?: string;
41
- identifier?: string;
42
- hasDetails?: boolean;
43
- icon?: string;
44
- backgroundColor?: string;
45
- color?: string;
46
- manufacturer?: string;
47
- model?: string;
48
- connectionType?: ConfigConnectionType;
49
- enabled?: boolean;
50
- updateAvailable?: boolean;
51
- batteryProblem?: boolean;
52
+ /** Icon read from the file storage or picked by the user. Overrides `fields.icon` */
53
+ localIcon?: string;
52
54
  }
53
55
  /**
54
- * Device Card Component
56
+ * Device Card Component.
57
+ *
58
+ * A `PureComponent`: the list re-renders on every loading step, on every filter change and on every
59
+ * resolved device value. Without the shallow property comparison all cards would be re-rendered
60
+ * every time, so all properties the list passes down are kept stable there.
55
61
  */
56
- export default class DeviceCard extends Component<DeviceCardProps, DeviceCardState> {
57
- private readonly stateOrObjectHandler;
58
- private readonly subscriptions;
59
- /** Separate subscription for the nested `device.update.available` field (used for the update indicator and the "only updatable" filter) */
60
- private updateAvailableSubscription?;
61
- /** Separate subscription for the nested battery status (used for the "battery problem" filter) */
62
- private batteryProblemSubscription?;
62
+ export default class DeviceCard extends PureComponent<DeviceCardProps, DeviceCardState> {
63
+ /** True as long as the component is mounted; guards the asynchronous icon loading */
64
+ private mounted;
63
65
  constructor(props: DeviceCardProps);
66
+ /**
67
+ * Key of the icon in the file storage, or `null` if the device brings its own icon.
68
+ *
69
+ * The file is named after manufacturer and model, both of which may be bound to a state or an
70
+ * object and therefore arrive only after the first render.
71
+ */
72
+ private static iconCacheKey;
64
73
  fetchIcon(): Promise<void>;
65
- componentDidMount(): Promise<void>;
66
- private subscribeUpdateAvailable;
67
- /** Extract the battery value (literal or state/object reference) from the device status */
68
- private getBatteryItem;
69
- /** A battery problem is an explicit battery warning (`false`) or a charge level below 30 % */
70
- private static isBatteryProblem;
71
- private subscribeBatteryProblem;
72
- private addStateOrObjectListener;
73
- componentDidUpdate(prevProps: DeviceCardProps, prevState: DeviceCardState): Promise<void>;
74
- componentWillUnmount(): Promise<void>;
74
+ componentDidMount(): void;
75
+ componentDidUpdate(prevProps: DeviceCardProps): void;
76
+ componentWillUnmount(): void;
75
77
  /**
76
78
  * Load the device details
77
79
  */
@@ -106,10 +108,9 @@ export default class DeviceCard extends Component<DeviceCardProps, DeviceCardSta
106
108
  render(): JSX.Element;
107
109
  }
108
110
  type DeviceCardSkeletonProps = Pick<DeviceCardProps, 'smallCards' | 'theme'>;
109
- export declare class DeviceCardSkeleton extends Component<DeviceCardSkeletonProps> {
111
+ export declare class DeviceCardSkeleton extends PureComponent<DeviceCardSkeletonProps> {
110
112
  render(): JSX.Element;
111
113
  renderSmall(): JSX.Element;
112
114
  renderBig(): JSX.Element;
113
115
  getCardHeaderStyle(theme: IobTheme, maxWidth?: number): React.CSSProperties;
114
116
  }
115
- export {};
@@ -1,15 +1,14 @@
1
- import React, { Component } from 'react';
1
+ import React, { PureComponent } from 'react';
2
2
  import { Close as CloseIcon, VideogameAsset as ControlIcon, MoreVert as MoreVertIcon, ExpandMore, } from '@mui/icons-material';
3
3
  import { Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, Fab, IconButton, Paper, Skeleton, Tooltip, Typography, Accordion, AccordionSummary, AccordionDetails, } from '@mui/material';
4
- import { DeviceTypeIcon, I18n, Utils, Icon, } from '@iobroker/gui-components';
4
+ import { DeviceTypeIcon, Utils, Icon, } from '@iobroker/gui-components';
5
5
  import DeviceActionButton from './DeviceActionButton';
6
6
  import DeviceControlComponent from './DeviceControl';
7
7
  import DeviceImageUpload from './DeviceImageUpload';
8
8
  import DeviceStatusComponent from './DeviceStatus';
9
9
  import JsonConfig from './JsonConfig';
10
10
  import { StatusIndicators } from './StatusIndicator';
11
- import { getTranslation } from './Utils';
12
- import { StateOrObjectHandler } from './StateOrObjectHandler';
11
+ import { getText, getTranslation } from './Utils';
13
12
  /** 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) */
14
13
  const ACTIONS = {
15
14
  /** This action will be called when the user clicks on the connection icon */
@@ -21,11 +20,33 @@ const ACTIONS = {
21
20
  /** This action will be called when the user clicks on the battery indicator. The battery indicator is shown only if the node status has the "battery" property */
22
21
  BATTERY: 'battery',
23
22
  };
23
+ /**
24
+ * Footprint of a card. The list needs it to give a not yet rendered card a placeholder of exactly
25
+ * the same size, so that neither the layout nor the length of the scrollbar changes.
26
+ */
27
+ export const CARD_WIDTH = 300;
28
+ export const CARD_MIN_HEIGHT = 280;
29
+ export const CARD_MARGIN = 10;
30
+ export const SMALL_CARD_WIDTH = 200;
31
+ export const SMALL_CARD_MIN_HEIGHT = 200;
32
+ export const SMALL_CARD_MARGIN = 5;
33
+ const cardSize = { width: CARD_WIDTH, minHeight: CARD_MIN_HEIGHT, margin: CARD_MARGIN };
34
+ const smallCardSize = {
35
+ width: SMALL_CARD_WIDTH,
36
+ minHeight: SMALL_CARD_MIN_HEIGHT,
37
+ margin: SMALL_CARD_MARGIN,
38
+ };
39
+ /** A card that sits in a container which already has the footprint of a card */
40
+ const filledCardSize = { width: '100%', margin: 0 };
41
+ /**
42
+ * Icons read from the file storage of the adapter, keyed by `<instance>/<file>`.
43
+ *
44
+ * A card is unmounted and mounted again while scrolling, and without this cache every one of those
45
+ * would repeat the `readFile` round trip for an icon that is very often not there at all.
46
+ */
47
+ const fileIconCache = new Map();
24
48
  const styles = {
25
49
  cardStyle: (theme) => ({
26
- width: 300,
27
- minHeight: 280,
28
- margin: '10px',
29
50
  overflow: 'hidden',
30
51
  display: 'flex',
31
52
  flexDirection: 'column',
@@ -84,46 +105,62 @@ function NoImageIcon(props) {
84
105
  return (React.createElement("svg", { viewBox: "0 0 24 24", width: "24", height: "24", style: props.style, className: props.className },
85
106
  React.createElement("path", { fill: "currentColor", d: "M21.9,21.9l-8.49-8.49l0,0L3.59,3.59l0,0L2.1,2.1L0.69,3.51L3,5.83V19c0,1.1,0.9,2,2,2h13.17l2.31,2.31L21.9,21.9z M5,18 l3.5-4.5l2.5,3.01L12.17,15l3,3H5z M21,18.17L5.83,3H19c1.1,0,2,0.9,2,2V18.17z" })));
86
107
  }
87
- function getText(text) {
88
- if (typeof text === 'object') {
89
- return text[I18n.getLanguage()] || text.en;
90
- }
91
- return text;
92
- }
93
108
  /**
94
- * Device Card Component
109
+ * Device Card Component.
110
+ *
111
+ * A `PureComponent`: the list re-renders on every loading step, on every filter change and on every
112
+ * resolved device value. Without the shallow property comparison all cards would be re-rendered
113
+ * every time, so all properties the list passes down are kept stable there.
95
114
  */
96
- export default class DeviceCard extends Component {
97
- stateOrObjectHandler;
98
- subscriptions = new Map();
99
- /** Separate subscription for the nested `device.update.available` field (used for the update indicator and the "only updatable" filter) */
100
- updateAvailableSubscription;
101
- /** Separate subscription for the nested battery status (used for the "battery problem" filter) */
102
- batteryProblemSubscription;
115
+ export default class DeviceCard extends PureComponent {
116
+ /** True as long as the component is mounted; guards the asynchronous icon loading */
117
+ mounted = false;
103
118
  constructor(props) {
104
119
  super(props);
120
+ const cacheKey = DeviceCard.iconCacheKey(props);
105
121
  this.state = {
106
122
  open: false,
107
123
  details: null,
108
124
  data: {},
109
125
  showControlDialog: false,
126
+ // Take a known icon over directly, so a card that is scrolled back into view does not flicker
127
+ localIcon: cacheKey ? fileIconCache.get(cacheKey) : undefined,
110
128
  };
111
- this.stateOrObjectHandler = new StateOrObjectHandler(this.props.socket);
129
+ }
130
+ /**
131
+ * Key of the icon in the file storage, or `null` if the device brings its own icon.
132
+ *
133
+ * The file is named after manufacturer and model, both of which may be bound to a state or an
134
+ * object and therefore arrive only after the first render.
135
+ */
136
+ static iconCacheKey(props) {
137
+ if (props.device.icon) {
138
+ return null;
139
+ }
140
+ const manufacturer = props.fields.manufacturer;
141
+ const model = props.fields.model;
142
+ const fileName = `${manufacturer ? `${manufacturer}_` : ''}${model || JSON.stringify(props.device.id)}`;
143
+ return `${props.instanceId.replace('system.adapter.', '')}/${fileName}.webp`;
112
144
  }
113
145
  async fetchIcon() {
114
- if (!this.props.device.icon) {
115
- const manufacturer = this.state.manufacturer;
116
- const model = this.state.model;
117
- // try to load the icon from file storage
118
- const fileName = `${manufacturer ? `${manufacturer}_` : ''}${model || JSON.stringify(this.props.device.id)}`;
119
- try {
120
- const file = await this.props.socket.readFile(this.props.instanceId.replace('system.adapter.', ''), `${fileName}.webp`, true);
121
- if (file) {
122
- this.setState({ icon: `data:${file.mimeType};base64,${file.file}` });
146
+ const cacheKey = DeviceCard.iconCacheKey(this.props);
147
+ if (cacheKey) {
148
+ const cached = fileIconCache.get(cacheKey);
149
+ if (cached !== undefined) {
150
+ if (this.state.localIcon !== cached) {
151
+ this.setState({ localIcon: cached });
123
152
  }
124
- else {
125
- this.setState({ icon: '' });
153
+ return;
154
+ }
155
+ const [adapter, ...rest] = cacheKey.split('/');
156
+ try {
157
+ const file = await this.props.socket.readFile(adapter, rest.join('/'), true);
158
+ const localIcon = file ? `data:${file.mimeType};base64,${file.file}` : '';
159
+ fileIconCache.set(cacheKey, localIcon);
160
+ if (!this.mounted) {
161
+ return;
126
162
  }
163
+ this.setState({ localIcon });
127
164
  // const response = await fetch(url);
128
165
  // if (response.ok) {
129
166
  // const blob = await response.blob();
@@ -137,89 +174,28 @@ export default class DeviceCard extends Component {
137
174
  // }
138
175
  }
139
176
  catch {
140
- if (this.state.icon) {
141
- this.setState({ icon: '' });
177
+ fileIconCache.set(cacheKey, '');
178
+ if (this.mounted && this.state.localIcon) {
179
+ this.setState({ localIcon: '' });
142
180
  }
143
181
  }
144
182
  }
145
183
  }
146
- async componentDidMount() {
147
- await this.addStateOrObjectListener('name', getText);
148
- await this.addStateOrObjectListener('identifier');
149
- await this.addStateOrObjectListener('hasDetails');
150
- await this.addStateOrObjectListener('icon');
151
- await this.addStateOrObjectListener('backgroundColor');
152
- await this.addStateOrObjectListener('color');
153
- await this.addStateOrObjectListener('manufacturer', getText);
154
- await this.addStateOrObjectListener('model', getText);
155
- await this.addStateOrObjectListener('connectionType');
156
- await this.addStateOrObjectListener('enabled');
157
- await this.subscribeUpdateAvailable();
158
- await this.subscribeBatteryProblem();
159
- await this.fetchIcon().catch(e => console.error(e));
160
- }
161
- async subscribeUpdateAvailable() {
162
- this.updateAvailableSubscription = await this.stateOrObjectHandler.addListener(this.props.device.update?.available, value => this.setState({ updateAvailable: !!value }));
163
- }
164
- /** Extract the battery value (literal or state/object reference) from the device status */
165
- getBatteryItem() {
166
- const status = this.props.device.status;
167
- if (!status || typeof status === 'string') {
168
- return undefined;
169
- }
170
- const list = Array.isArray(status) ? status : [status];
171
- for (const entry of list) {
172
- if (typeof entry !== 'string' && entry.battery !== undefined) {
173
- return entry.battery;
174
- }
175
- }
176
- return undefined;
177
- }
178
- /** A battery problem is an explicit battery warning (`false`) or a charge level below 30 % */
179
- static isBatteryProblem(value) {
180
- if (value === false) {
181
- return true;
182
- }
183
- return typeof value === 'number' && value < 30;
184
- }
185
- async subscribeBatteryProblem() {
186
- this.batteryProblemSubscription = await this.stateOrObjectHandler.addListener(this.getBatteryItem(), value => this.setState({ batteryProblem: DeviceCard.isBatteryProblem(value) }));
184
+ componentDidMount() {
185
+ this.mounted = true;
186
+ void this.fetchIcon().catch(e => console.error(e));
187
187
  }
188
- async addStateOrObjectListener(key, transform) {
189
- const sub = await this.stateOrObjectHandler.addListener(this.props.device[key], value => this.setState({ [key]: transform ? transform(value) : value }));
190
- this.subscriptions.set(key, { subscription: sub, transform });
191
- }
192
- async componentDidUpdate(prevProps, prevState) {
193
- if (prevState.model !== this.state.model) {
194
- this.props.onModel?.(this.props.device.id, this.state.model);
195
- }
196
- for (const [key, { subscription, transform }] of [...this.subscriptions]) {
197
- const newItem = this.props.device[key];
198
- const prevItem = prevProps.device[key];
199
- if (newItem !== prevItem) {
200
- console.log(`${key} of device ${JSON.stringify(this.props.device.id)} updated`, prevItem, newItem);
201
- this.subscriptions.delete(key);
202
- await this.addStateOrObjectListener(key, transform);
203
- await subscription.unsubscribe();
204
- }
205
- }
206
- if (this.props.device.update?.available !== prevProps.device.update?.available) {
207
- await this.updateAvailableSubscription?.unsubscribe();
208
- await this.subscribeUpdateAvailable();
209
- }
210
- if (this.props.device.status !== prevProps.device.status) {
211
- await this.batteryProblemSubscription?.unsubscribe();
212
- await this.subscribeBatteryProblem();
188
+ componentDidUpdate(prevProps) {
189
+ // The icon is looked up in the file storage under `<manufacturer>_<model>`. Both may be bound
190
+ // to a state or an object and therefore arrive only after the first render.
191
+ if (prevProps.fields.manufacturer !== this.props.fields.manufacturer ||
192
+ prevProps.fields.model !== this.props.fields.model ||
193
+ prevProps.device.icon !== this.props.device.icon) {
194
+ void this.fetchIcon().catch(e => console.error(e));
213
195
  }
214
196
  }
215
- async componentWillUnmount() {
216
- for (const [, { subscription }] of this.subscriptions) {
217
- await subscription.unsubscribe();
218
- }
219
- this.subscriptions.clear();
220
- await this.updateAvailableSubscription?.unsubscribe();
221
- await this.batteryProblemSubscription?.unsubscribe();
222
- this.props.onModel?.(this.props.device.id, undefined);
197
+ componentWillUnmount() {
198
+ this.mounted = false;
223
199
  }
224
200
  /**
225
201
  * Load the device details
@@ -234,7 +210,7 @@ export default class DeviceCard extends Component {
234
210
  * Copy the device ID to the clipboard
235
211
  */
236
212
  copyToClipboard = () => {
237
- const textToCopy = this.state.identifier;
213
+ const textToCopy = this.props.fields.identifier;
238
214
  if (!textToCopy) {
239
215
  return;
240
216
  }
@@ -275,7 +251,7 @@ export default class DeviceCard extends Component {
275
251
  const allControls = this.props.device.controls || [];
276
252
  return (React.createElement(Dialog, { open: !0, onClose: () => this.setState({ showControlDialog: false }) },
277
253
  React.createElement(DialogTitle, { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 } },
278
- this.state.name,
254
+ this.props.fields.name,
279
255
  React.createElement(IconButton, { onClick: () => this.setState({ showControlDialog: false }) },
280
256
  React.createElement(CloseIcon, null))),
281
257
  React.createElement(DialogContent, { style: { display: 'flex', flexDirection: 'column' } }, this.renderControlItems(allControls, allControls, colors))));
@@ -348,7 +324,7 @@ export default class DeviceCard extends Component {
348
324
  if (!indicators?.length && !statusActions.length) {
349
325
  return null;
350
326
  }
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 })))));
327
+ return (React.createElement(StatusIndicators, { indicators: indicators, theme: this.props.theme, stateOrObjectHandler: this.props.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
328
  }
353
329
  renderActions() {
354
330
  const statusActionIds = this.getStatusActionIds();
@@ -363,20 +339,21 @@ export default class DeviceCard extends Component {
363
339
  : Array.isArray(this.props.device.status)
364
340
  ? this.props.device.status
365
341
  : [this.props.device.status];
366
- const icon = this.state.icon ? (React.createElement(DeviceTypeIcon, { src: this.state.icon, style: styles.imgStyle })) : (React.createElement(NoImageIcon, { style: styles.imgStyle }));
342
+ const iconSrc = this.state.localIcon || this.props.fields.icon;
343
+ const icon = iconSrc ? (React.createElement(DeviceTypeIcon, { src: iconSrc, style: styles.imgStyle })) : (React.createElement(NoImageIcon, { style: styles.imgStyle }));
367
344
  const headerStyle = this.getCardHeaderStyle(this.props.theme);
368
345
  const title = this.state.details?.data?.name || this.props.device.name || '';
369
- return (React.createElement(Paper, { style: { width: 200, minHeight: 200, margin: 5 }, sx: styles.cardStyle, key: JSON.stringify(this.props.id) },
346
+ return (React.createElement(Paper, { style: this.props.fillContainer ? filledCardSize : smallCardSize, sx: styles.cardStyle },
370
347
  React.createElement(Box, { sx: headerStyle, style: { ...styles.headerStyle, minHeight: 48 } },
371
348
  React.createElement("div", { style: { ...styles.imgAreaStyle, height: 32, width: 32 } },
372
- this.props.uploadImagesToInstance ? (React.createElement(DeviceImageUpload, { uploadImagesToInstance: this.props.uploadImagesToInstance, deviceId: this.props.device.id, manufacturer: this.state.manufacturer, model: this.state.model, onImageSelect: (imageData) => {
349
+ this.props.uploadImagesToInstance ? (React.createElement(DeviceImageUpload, { uploadImagesToInstance: this.props.uploadImagesToInstance, deviceId: this.props.device.id, manufacturer: this.props.fields.manufacturer, model: this.props.fields.model, onImageSelect: (imageData) => {
373
350
  if (imageData) {
374
- this.setState({ icon: imageData });
351
+ this.setState({ localIcon: imageData });
375
352
  }
376
353
  }, socket: this.props.socket })) : null,
377
354
  icon),
378
- React.createElement(Box, { style: { ...styles.titleStyle, fontSize: 14 }, title: title.length > 15 ? title : undefined, sx: theme => ({ color: headerStyle.color || theme.palette.secondary.contrastText }) }, this.state.details?.data?.name || this.state.name),
379
- this.state.hasDetails ? (React.createElement(Fab, { disabled: !this.props.alive, size: "small", style: styles.detailsButtonStyle, onClick: () => {
355
+ React.createElement(Box, { style: { ...styles.titleStyle, fontSize: 14 }, title: title.length > 15 ? title : undefined, sx: theme => ({ color: headerStyle.color || theme.palette.secondary.contrastText }) }, this.state.details?.data?.name || this.props.fields.name),
356
+ this.props.fields.hasDetails ? (React.createElement(Fab, { disabled: !this.props.alive, size: "small", style: styles.detailsButtonStyle, onClick: () => {
380
357
  if (!this.state.open) {
381
358
  this.loadDetails().catch(console.error);
382
359
  this.setState({ open: true });
@@ -384,19 +361,19 @@ export default class DeviceCard extends Component {
384
361
  }, color: "primary" },
385
362
  React.createElement(MoreVertIcon, null))) : null),
386
363
  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 }))),
364
+ status.map((s, i) => (React.createElement(DeviceStatusComponent, { key: i, socket: this.props.socket, deviceId: this.props.device.id, connectionType: this.props.fields.connectionType, status: s, enabled: this.props.fields.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.props.stateOrObjectHandler }))),
388
365
  this.renderIndicators(true)),
389
366
  React.createElement("div", { style: styles.bodyStyle },
390
367
  React.createElement(Typography, { variant: "body2", style: { ...styles.deviceInfoStyle, padding: '10px 10px 0 10px' } },
391
- this.state.identifier ? (React.createElement("div", { onClick: this.copyToClipboard, style: { textOverflow: 'ellipsis', overflow: 'hidden' } },
368
+ this.props.fields.identifier ? (React.createElement("div", { onClick: this.copyToClipboard, style: { textOverflow: 'ellipsis', overflow: 'hidden' } },
392
369
  React.createElement("b", null,
393
370
  getText(this.props.identifierLabel),
394
371
  ":"),
395
- React.createElement("span", { style: { marginLeft: 4 } }, this.state.identifier))) : null,
396
- this.state.manufacturer ? (React.createElement(Tooltip, { title: getTranslation('manufacturer'), slotProps: { popper: { sx: { pointerEvents: 'none' } } } },
397
- React.createElement("div", null, this.state.manufacturer))) : null,
398
- this.state.model ? (React.createElement(Tooltip, { title: getTranslation('model'), slotProps: { popper: { sx: { pointerEvents: 'none' } } } },
399
- React.createElement("div", null, this.state.model))) : null),
372
+ React.createElement("span", { style: { marginLeft: 4 } }, this.props.fields.identifier))) : null,
373
+ this.props.fields.manufacturer ? (React.createElement(Tooltip, { title: getTranslation('manufacturer'), slotProps: { popper: { sx: { pointerEvents: 'none' } } } },
374
+ React.createElement("div", null, this.props.fields.manufacturer))) : null,
375
+ this.props.fields.model ? (React.createElement(Tooltip, { title: getTranslation('model'), slotProps: { popper: { sx: { pointerEvents: 'none' } } } },
376
+ React.createElement("div", null, this.props.fields.model))) : null),
400
377
  this.props.device.customInfo ? (React.createElement("div", { style: { padding: '0 10px 10px' } },
401
378
  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) => {
402
379
  /* ignore */
@@ -417,28 +394,30 @@ export default class DeviceCard extends Component {
417
394
  this.renderControlDialog()));
418
395
  }
419
396
  getCardHeaderStyle(theme, maxWidth) {
420
- const backgroundColor = this.state.backgroundColor === 'primary'
397
+ const backgroundColor = this.props.fields.backgroundColor === 'primary'
421
398
  ? theme.palette.primary.main
422
- : this.state.backgroundColor === 'secondary'
399
+ : this.props.fields.backgroundColor === 'secondary'
423
400
  ? theme.palette.secondary.main
424
- : this.state.backgroundColor || theme.palette.secondary.main;
401
+ : this.props.fields.backgroundColor || theme.palette.secondary.main;
425
402
  let color;
426
- if (this.state.color && this.state.color !== 'primary' && this.state.color !== 'secondary') {
403
+ if (this.props.fields.color &&
404
+ this.props.fields.color !== 'primary' &&
405
+ this.props.fields.color !== 'secondary') {
427
406
  // Color was directly defined
428
- color = this.state.color;
407
+ color = this.props.fields.color;
429
408
  }
430
- else if (this.state.color === 'primary') {
409
+ else if (this.props.fields.color === 'primary') {
431
410
  color = theme.palette.primary.main;
432
411
  }
433
- else if (this.state.color === 'secondary') {
412
+ else if (this.props.fields.color === 'secondary') {
434
413
  color = theme.palette.secondary.main;
435
414
  }
436
415
  else {
437
416
  // Color was not defined
438
- if (this.state.backgroundColor === 'primary') {
417
+ if (this.props.fields.backgroundColor === 'primary') {
439
418
  color = theme.palette.primary.contrastText;
440
419
  }
441
- else if (this.state.backgroundColor === 'secondary' || !this.state.backgroundColor) {
420
+ else if (this.props.fields.backgroundColor === 'secondary' || !this.props.fields.backgroundColor) {
442
421
  color = theme.palette.secondary.contrastText;
443
422
  }
444
423
  else {
@@ -457,20 +436,21 @@ export default class DeviceCard extends Component {
457
436
  : Array.isArray(this.props.device.status)
458
437
  ? this.props.device.status
459
438
  : [this.props.device.status];
460
- const icon = this.state.icon ? (React.createElement(DeviceTypeIcon, { src: this.state.icon, style: styles.imgStyle })) : (React.createElement(NoImageIcon, { style: styles.imgStyle }));
439
+ const iconSrc = this.state.localIcon || this.props.fields.icon;
440
+ const icon = iconSrc ? (React.createElement(DeviceTypeIcon, { src: iconSrc, style: styles.imgStyle })) : (React.createElement(NoImageIcon, { style: styles.imgStyle }));
461
441
  const headerStyle = this.getCardHeaderStyle(this.props.theme);
462
442
  const title = this.state.details?.data?.name || this.props.device.name || '';
463
- return (React.createElement(Paper, { sx: styles.cardStyle, key: JSON.stringify(this.props.id) },
443
+ return (React.createElement(Paper, { style: this.props.fillContainer ? filledCardSize : cardSize, sx: styles.cardStyle },
464
444
  React.createElement(Box, { sx: headerStyle, style: styles.headerStyle },
465
445
  React.createElement("div", { style: styles.imgAreaStyle },
466
- this.props.uploadImagesToInstance ? (React.createElement(DeviceImageUpload, { uploadImagesToInstance: this.props.uploadImagesToInstance, deviceId: this.props.device.id, manufacturer: this.state.manufacturer, model: this.state.model, onImageSelect: (imageData) => {
446
+ this.props.uploadImagesToInstance ? (React.createElement(DeviceImageUpload, { uploadImagesToInstance: this.props.uploadImagesToInstance, deviceId: this.props.device.id, manufacturer: this.props.fields.manufacturer, model: this.props.fields.model, onImageSelect: (imageData) => {
467
447
  if (imageData) {
468
- this.setState({ icon: imageData });
448
+ this.setState({ localIcon: imageData });
469
449
  }
470
450
  }, socket: this.props.socket })) : null,
471
451
  icon),
472
- React.createElement(Box, { style: styles.titleStyle, title: title.length > 20 ? title : undefined, sx: theme => ({ color: headerStyle.color || theme.palette.secondary.contrastText }) }, this.state.details?.data?.name || this.state.name),
473
- this.state.hasDetails ? (React.createElement(Fab, { disabled: !this.props.alive, size: "small", style: styles.detailsButtonStyle, onClick: () => {
452
+ React.createElement(Box, { style: styles.titleStyle, title: title.length > 20 ? title : undefined, sx: theme => ({ color: headerStyle.color || theme.palette.secondary.contrastText }) }, this.state.details?.data?.name || this.props.fields.name),
453
+ this.props.fields.hasDetails ? (React.createElement(Fab, { disabled: !this.props.alive, size: "small", style: styles.detailsButtonStyle, onClick: () => {
474
454
  if (!this.state.open) {
475
455
  this.loadDetails().catch(console.error);
476
456
  this.setState({ open: true });
@@ -478,25 +458,25 @@ export default class DeviceCard extends Component {
478
458
  }, color: "primary" },
479
459
  React.createElement(MoreVertIcon, null))) : null),
480
460
  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 }))),
461
+ status.map((s, i) => (React.createElement(DeviceStatusComponent, { key: i, socket: this.props.socket, deviceId: this.props.device.id, connectionType: this.props.fields.connectionType, status: s, enabled: this.props.fields.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.props.stateOrObjectHandler }))),
482
462
  this.renderIndicators()),
483
463
  React.createElement("div", { style: styles.bodyStyle },
484
464
  React.createElement(Typography, { variant: "body1", style: styles.deviceInfoStyle },
485
- this.state.identifier ? (React.createElement("div", { onClick: this.copyToClipboard },
465
+ this.props.fields.identifier ? (React.createElement("div", { onClick: this.copyToClipboard },
486
466
  React.createElement("b", { style: { marginRight: 4 } },
487
467
  getText(this.props.identifierLabel),
488
468
  ":"),
489
- this.state.identifier)) : null,
490
- this.state.manufacturer ? (React.createElement("div", null,
469
+ this.props.fields.identifier)) : null,
470
+ this.props.fields.manufacturer ? (React.createElement("div", null,
491
471
  React.createElement("b", { style: { marginRight: 4 } },
492
472
  getTranslation('manufacturer'),
493
473
  ":"),
494
- this.state.manufacturer)) : null,
495
- this.state.model ? (React.createElement("div", null,
474
+ this.props.fields.manufacturer)) : null,
475
+ this.props.fields.model ? (React.createElement("div", null,
496
476
  React.createElement("b", { style: { marginRight: 4 } },
497
477
  getTranslation('model'),
498
478
  ":"),
499
- this.state.model)) : null),
479
+ this.props.fields.model)) : null),
500
480
  this.props.device.customInfo ? (React.createElement("div", { style: { padding: '0 16px 16px' } },
501
481
  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) => {
502
482
  /* ignore */
@@ -517,26 +497,13 @@ export default class DeviceCard extends Component {
517
497
  this.renderControlDialog()));
518
498
  }
519
499
  render() {
520
- if (this.props.filter) {
521
- const field = this.props.filterField ?? 'name';
522
- const value = String(this.state[field] ?? '').toLowerCase();
523
- if (!value.includes(this.props.filter.toLowerCase())) {
524
- return React.createElement(React.Fragment, null);
525
- }
526
- }
527
- if (this.props.onlyUpdatable && !this.state.updateAvailable) {
528
- return React.createElement(React.Fragment, null);
529
- }
530
- if (this.props.onlyBatteryProblem && !this.state.batteryProblem) {
531
- return React.createElement(React.Fragment, null);
532
- }
533
500
  if (this.props.smallCards) {
534
501
  return this.renderSmall();
535
502
  }
536
503
  return this.renderBig();
537
504
  }
538
505
  }
539
- export class DeviceCardSkeleton extends Component {
506
+ export class DeviceCardSkeleton extends PureComponent {
540
507
  render() {
541
508
  if (this.props.smallCards) {
542
509
  return this.renderSmall();
@@ -545,7 +512,7 @@ export class DeviceCardSkeleton extends Component {
545
512
  }
546
513
  renderSmall() {
547
514
  const headerStyle = this.getCardHeaderStyle(this.props.theme);
548
- return (React.createElement(Paper, { sx: styles.cardStyle, style: { width: 200, minHeight: 200, margin: 5 } },
515
+ return (React.createElement(Paper, { sx: styles.cardStyle, style: smallCardSize },
549
516
  React.createElement(Box, { sx: headerStyle, style: { ...styles.headerStyle, minHeight: 48 } },
550
517
  React.createElement("div", { style: { ...styles.imgAreaStyle, height: 32, width: 32 } },
551
518
  React.createElement(Skeleton, { variant: "rounded", width: 24, height: 24 })),
@@ -565,7 +532,7 @@ export class DeviceCardSkeleton extends Component {
565
532
  }
566
533
  renderBig() {
567
534
  const headerStyle = this.getCardHeaderStyle(this.props.theme);
568
- return (React.createElement(Paper, { sx: styles.cardStyle },
535
+ return (React.createElement(Paper, { sx: styles.cardStyle, style: cardSize },
569
536
  React.createElement(Box, { sx: headerStyle, style: styles.headerStyle },
570
537
  React.createElement("div", { style: styles.imgAreaStyle },
571
538
  React.createElement(Skeleton, { variant: "rounded", width: 24, height: 24 })),