diff --git a/README.md b/README.md index a6c1d5add..b3439b7f5 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ And with rest-api instance: `https://iobroker.mydomain.com/ioBrokerAPI/ => http: You can add the following lines into Reverse Proxy tab to let Intro tab run behind reverse proxy properly: | Global path | Instance | Instance path behind proxy | -| ----------------- | ------------- | -------------------------- | +|-------------------|---------------|----------------------------| | `/ioBrokerAdmin/` | `web.0` | `/ioBrokerWeb/` | | | `rest-api.0` | `/ioBrokerAPI/` | | | `admin.0` | `/ioBrokerAdmin/` | @@ -87,6 +87,10 @@ The icons may not be reused in other projects without the proper flaticon licens +### **WORK IN PROGRESS** + +- (@GermanBluefox) Showed value in object edit dialog + ### 7.4.3 (2024-12-01) - (@GermanBluefox) Changed information box on the hosts tab diff --git a/packages/adapter-react-v5/src/Components/ObjectBrowser.tsx b/packages/adapter-react-v5/src/Components/ObjectBrowser.tsx index b19f4048f..541bb907c 100644 --- a/packages/adapter-react-v5/src/Components/ObjectBrowser.tsx +++ b/packages/adapter-react-v5/src/Components/ObjectBrowser.tsx @@ -2045,6 +2045,7 @@ function formatValue(options: FormatValueOptions): { if (isCommon?.unit) { valText.u = isCommon.unit; } + let valFull: | { /** label */ diff --git a/packages/adapter-react-v5/src/i18n/en.json b/packages/adapter-react-v5/src/i18n/en.json index 55d5249d4..fb34606ff 100644 --- a/packages/adapter-react-v5/src/i18n/en.json +++ b/packages/adapter-react-v5/src/i18n/en.json @@ -115,6 +115,7 @@ "ra_License %s": "License %s", "ra_License agreement": "License agreement", "ra_License does not exist": "License does not exist", + "ra_tooltip_expire": "Expires in", "ra_License expired on %s": "License expired on %s", "ra_License for other product \"%s\"": "License for other product \"%s\"", "ra_License is for version %s, but required version is %s.": "License is for version %s, but required version is %s.", @@ -447,4 +448,4 @@ "sch_validTo": "to", "sch_wholeDay": "Whole day", "sch_yearEveryMonth": "every month" -} \ No newline at end of file +} diff --git a/packages/admin/src-admin/src/components/Hosts/HostRow.tsx b/packages/admin/src-admin/src/components/Hosts/HostRow.tsx index 8c88a81cb..08aa7b9a2 100644 --- a/packages/admin/src-admin/src/components/Hosts/HostRow.tsx +++ b/packages/admin/src-admin/src/components/Hosts/HostRow.tsx @@ -40,7 +40,7 @@ const styles: Record = { alignItems: 'baseline', }, collapse: { - height: 200, + height: 215, backgroundColor: 'rgba(128, 128, 128, 0.1)', // position: 'absolute', width: '100%', diff --git a/packages/admin/src-admin/src/components/Object/ObjectBrowserEditObject.tsx b/packages/admin/src-admin/src/components/Object/ObjectBrowserEditObject.tsx index e137e07cc..76e17f206 100644 --- a/packages/admin/src-admin/src/components/Object/ObjectBrowserEditObject.tsx +++ b/packages/admin/src-admin/src/components/Object/ObjectBrowserEditObject.tsx @@ -1,4 +1,16 @@ import React, { Component, type JSX } from 'react'; +import moment from 'moment'; + +import 'moment/locale/de'; +import 'moment/locale/es'; +import 'moment/locale/fr'; +import 'moment/locale/it'; +import 'moment/locale/nl'; +import 'moment/locale/pl'; +import 'moment/locale/pt'; +import 'moment/locale/ru'; +import 'moment/locale/uk'; +import 'moment/locale/zh-cn'; import { Dialog, @@ -187,8 +199,47 @@ const styles: Record = { tooltip: { pointerEvents: 'none', }, + stateRow: { + width: '100%', + display: 'flex', + justifyContent: 'space-between', + '&:hover': { + backgroundColor: '#00000030', + }, + }, + stateTitle: { + minWidth: 150, + fontWeight: 'bold', + }, + stateUnit: { + opacity: 0.7, + marginLeft: 4, + }, + stateValue: { + animation: 'newStateEditorAnimation 2s ease-in-out', + }, + stateTime: { + fontStyle: 'italic', + }, + stateImage: { + maxWidth: 200, + maxHeight: 200, + }, }; +function valueBlink(theme: IobTheme, color: string): any { + return { + '@keyframes newStateEditorAnimation': { + '0%': { + color: theme.palette.mode === 'dark' ? '#27cf00' : '#174e00', + }, + '100%': { + color: color || (theme.palette.mode === 'dark' ? '#ffffff' : '#000000'), + }, + }, + }; +} + const DEFAULT_ROLES = [ 'button', 'button.close.blind', @@ -495,11 +546,14 @@ interface ObjectBrowserEditObjectState { newId: string; customEditTabs?: EditSchemaTabEditor[]; lang: ioBroker.Languages; + value: ioBroker.State | null | undefined; } class ObjectBrowserEditObject extends Component { /** Original object stringified */ private originalObj: string; + private subscribed = false; + private updateTimer: ReturnType | null = null; constructor(props: ObjectBrowserEditObjectProps) { super(props); @@ -540,8 +594,11 @@ class ObjectBrowserEditObject extends Component tab.key === this.state.tab) ) { this.setState({ tab: 'object' }); } + if (this.state.tab === 'state') { + this.subscribeOnState(true); + } + void this.props.socket.subscribeObject(this.props.obj._id, this.onObjectUpdated); } componentWillUnmount(): void { + if (this.updateTimer) { + clearInterval(this.updateTimer); + this.updateTimer = null; + } + this.subscribeOnState(false); + void this.props.socket.unsubscribeObject(this.props.obj._id, this.onObjectUpdated); } @@ -920,6 +993,237 @@ class ObjectBrowserEditObject extends Component + ); + } + + renderStatePanel(): JSX.Element { + if (this.state.value === undefined || this.state.value === null) { + return
{this.props.t('State does not exist')}
; + } + if (typeof this.state.value !== 'object') { + return ( +
+
{this.props.t('State is invalid')}
+
+
{JSON.stringify(this.state.value, null, 4)}
+
+
+ ); + } + + let strVal: string | React.JSX.Element | undefined; + const styleValue: React.CSSProperties = {}; + const v = this.state.value.val; + const type = typeof v; + + if (v === undefined) { + strVal = '[undef]'; + styleValue.color = '#bc6400'; + styleValue.fontStyle = 'italic'; + } else if (v === null) { + strVal = '(null)'; + styleValue.color = '#0047b1'; + styleValue.fontStyle = 'italic'; + } else if ( + typeof this.props.obj.common.role === 'string' && + this.props.obj.common.role.match(/^value\.time|^date/) + ) { + // if timestamp + if (v && type === 'string') { + if (Utils.isStringInteger(v as string)) { + // we assume a unix ts + strVal = new Date(parseInt(v as string, 10)).toString(); + } else { + // check if parsable by new date + try { + const parsedDate = new Date(v as string); + + if (Utils.isValidDate(parsedDate)) { + strVal = parsedDate.toString(); + } + } catch { + // ignore + } + } + } else if (v && type === 'number') { + if ((v as number) > 946681200 && (v as number) < 946681200000) { + // '2000-01-01T00:00:00' => 946681200000 + strVal = new Date((v as number) * 1_000).toString(); // maybe the time is in seconds (UNIX time) + } else if ((v as number) > 946681200000000) { + // "null" and undefined could not be here. See `let v = (isCommon && isCommon.type === 'file') ....` above + strVal = new Date(v as number).toString(); + } + } + } + + if (!strVal) { + if (type === 'number') { + if (!Number.isInteger(v)) { + strVal = (Math.round((v as number) * 1_000_000_000) / 1_000_000_000).toString(); // remove 4.00000000000000001 + if (this.props.isFloatComma) { + strVal = strVal.toString().replace('.', ','); + } + } + } else if (type === 'boolean') { + strVal = v ? I18n.t('true') : I18n.t('false'); + styleValue.color = v ? '#139800' : '#cd6b55'; + } else if (type === 'object') { + strVal = JSON.stringify(v); + } else if (type === 'string' && (v as string).startsWith('data:image/')) { + strVal = ( + img + ); + } else { + strVal = v.toString(); + } + } + + Object.assign(styleValue, valueBlink(this.props.theme, styleValue.color)); + + return ( +
+
+
{I18n.t('ra_tooltip_value')}:
+ + {strVal} + {(this.props.obj.common as ioBroker.StateCommon)?.unit ? ( + {(this.props.obj.common as ioBroker.StateCommon).unit} + ) : null} + +
+
+
{I18n.t('Type')}:
+
{type}
+
+
+
{I18n.t('ra_tooltip_ts')}:
+ +
+ {moment(this.state.value.ts).fromNow()} +
+
+
+
+
{I18n.t('ra_tooltip_ack')}:
+
+ {this.state.value.ack ? I18n.t('Acknowledged') : I18n.t('Command')} + {this.state.value.ack ? ' (true)' : ' (false)'} +
+
+
+
{I18n.t('ra_tooltip_lc')}:
+ +
+ {moment(this.state.value.lc).fromNow()} +
+
+
+
+
{I18n.t('ra_tooltip_quality')}:
+
{Utils.quality2text(this.state.value.q || 0).join(', ')}
+
+
+
{I18n.t('ra_tooltip_from')}:
+
{this.state.value.from}
+
+
+
{I18n.t('ra_tooltip_user')}:
+
{this.state.value.user || '--'}
+
+ {this.state.value.expire ? ( +
+
{I18n.t('ra_tooltip_expire')}:
+
+ {this.state.value.expire} {I18n.t('sc_seconds')} +
+
+ ) : null} + {this.state.value.c ? ( +
+
{I18n.t('ra_tooltip_comment')}:
+
{this.state.value.c}
+
+ ) : null} +
+ ); + } + + onStateChange = (id: string, state: ioBroker.State | null | undefined): void => { + if (JSON.stringify(state) !== JSON.stringify(this.state.value)) { + this.setState({ value: state }); + } + }; + + subscribeOnState(enable: boolean): void { + if (enable) { + if (!this.subscribed) { + if (!this.updateTimer) { + this.updateTimer = setInterval(() => { + // update times + this.forceUpdate(); + }, 5000); + } + this.subscribed = true; + void this.props.socket.subscribeState(this.props.obj._id, this.onStateChange); + } + } else { + if (this.subscribed) { + if (this.updateTimer) { + clearInterval(this.updateTimer); + this.updateTimer = null; + } + this.subscribed = false; + void this.props.socket.unsubscribeState(this.props.obj._id, this.onStateChange); + } + } + } + renderTabs(parsedObj: ioBroker.Object | null | undefined): JSX.Element { return ( + {this.renderStateTab()} {this.props.obj._id.startsWith('alias.0') && this.props.obj.type === 'state' && ( diff --git a/packages/admin/src-admin/src/components/Object/ObjectBrowserValue.tsx b/packages/admin/src-admin/src/components/Object/ObjectBrowserValue.tsx index b0e6d4e26..9efb6bbc7 100644 --- a/packages/admin/src-admin/src/components/Object/ObjectBrowserValue.tsx +++ b/packages/admin/src-admin/src/components/Object/ObjectBrowserValue.tsx @@ -562,7 +562,9 @@ class ObjectBrowserValue extends Component - {this.props.t('Value type')} + + {this.props.t('Value type')} +