From 4ece91a0fd0915964157ff93997f43f9cc891a0d Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Fri, 21 Nov 2025 13:48:17 +0200 Subject: [PATCH 01/35] lwm2m: fix bug progileID is null after reboot if sleep --- .../server/client/LwM2mClientContextImpl.java | 2 +- .../ota/DefaultLwM2MOtaUpdateService.java | 58 ++++++++++--------- 2 files changed, 32 insertions(+), 28 deletions(-) diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java index 4cf8d825b9..f5165f47c8 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java @@ -360,7 +360,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { @Override public Lwm2mDeviceProfileTransportConfiguration getProfile(Registration registration) { UUID profileId = getClientByEndpoint(registration.getEndpoint()).getProfileId(); - return doGetAndCache(profileId); + return profileId != null ? doGetAndCache(profileId) : null; } private Lwm2mDeviceProfileTransportConfiguration doGetAndCache(UUID profileId) { diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java index 79f077a71e..325f1e2280 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java @@ -201,35 +201,39 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl } var clientSettings = clientContext.getProfile(client.getRegistration()).getClientLwM2mSettings(); - initFwStrategy(client, clientSettings); - initSwStrategy(client, clientSettings); - - if (!attributesToFetch.isEmpty()) { - var future = attributesService.getSharedAttributes(client, attributesToFetch); - DonAsynchron.withCallback(future, attrs -> { - if (fwInfo.isSupported()) { - Optional newFwTitle = getAttributeValue(attrs, FIRMWARE_TITLE); - Optional newFwVersion = getAttributeValue(attrs, FIRMWARE_VERSION); - Optional newFwTag = getAttributeValue(attrs, FIRMWARE_TAG); - Optional newFwUrl = getAttributeValue(attrs, FIRMWARE_URL); - if (newFwTitle.isPresent() && newFwVersion.isPresent() && !isOtaDownloading(client) && !UPDATING.equals(fwInfo.status)) { - onTargetFirmwareUpdate(client, newFwTitle.get(), newFwVersion.get(), newFwUrl, newFwTag); + if (clientSettings != null) { + initFwStrategy(client, clientSettings); + initSwStrategy(client, clientSettings); + + + if (!attributesToFetch.isEmpty()) { + var future = attributesService.getSharedAttributes(client, attributesToFetch); + DonAsynchron.withCallback(future, attrs -> { + if (fwInfo.isSupported()) { + Optional newFwTitle = getAttributeValue(attrs, FIRMWARE_TITLE); + Optional newFwVersion = getAttributeValue(attrs, FIRMWARE_VERSION); + Optional newFwTag = getAttributeValue(attrs, FIRMWARE_TAG); + Optional newFwUrl = getAttributeValue(attrs, FIRMWARE_URL); + if (newFwTitle.isPresent() && newFwVersion.isPresent() && !isOtaDownloading(client) && !UPDATING.equals(fwInfo.status)) { + onTargetFirmwareUpdate(client, newFwTitle.get(), newFwVersion.get(), newFwUrl, newFwTag); + } } - } - if (swInfo.isSupported()) { - Optional newSwTitle = getAttributeValue(attrs, SOFTWARE_TITLE); - Optional newSwVersion = getAttributeValue(attrs, SOFTWARE_VERSION); - Optional newSwTag = getAttributeValue(attrs, SOFTWARE_TAG); - Optional newSwUrl = getAttributeValue(attrs, SOFTWARE_URL); - if (newSwTitle.isPresent() && newSwVersion.isPresent()) { - onTargetSoftwareUpdate(client, newSwTitle.get(), newSwVersion.get(), newSwUrl, newSwTag); + if (swInfo.isSupported()) { + Optional newSwTitle = getAttributeValue(attrs, SOFTWARE_TITLE); + Optional newSwVersion = getAttributeValue(attrs, SOFTWARE_VERSION); + Optional newSwTag = getAttributeValue(attrs, SOFTWARE_TAG); + Optional newSwUrl = getAttributeValue(attrs, SOFTWARE_URL); + if (newSwTitle.isPresent() && newSwVersion.isPresent()) { + onTargetSoftwareUpdate(client, newSwTitle.get(), newSwVersion.get(), newSwUrl, newSwTag); + } } - } - }, throwable -> { - if (fwInfo.isSupported()) { - update(fwInfo); - } - }, executor); + + }, throwable -> { + if (fwInfo.isSupported()) { + update(fwInfo); + } + }, executor); + } } } From f398a81a429f1163107ace6c2e85d7aa13034445 Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Fri, 21 Nov 2025 19:15:27 +0200 Subject: [PATCH 02/35] lwm2m: fix bug redis -> Cleaner if registration == null --- .../server/store/TbLwM2mRedisRegistrationStore.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java index df432732b2..3c9828af22 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java @@ -750,11 +750,14 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab System.currentTimeMillis(), 0, cleanLimit); for (byte[] endpoint : endpointsExpired) { - Registration r = deserializeReg(connection.get(toEndpointKey(endpoint))); - if (!r.isAlive(gracePeriod)) { - Deregistration dereg = removeRegistration(connection, r.getId(), true); - if (dereg != null) - expirationListener.registrationExpired(dereg.getRegistration(), dereg.getObservations()); + byte[] data = connection.get(toEndpointKey(endpoint)); + if (data != null && data.length > 0) { + Registration r = deserializeReg(data); + if (!r.isAlive(gracePeriod)) { + Deregistration dereg = removeRegistration(connection, r.getId(), true); + if (dereg != null) + expirationListener.registrationExpired(dereg.getRegistration(), dereg.getObservations()); + } } } } catch (Exception e) { From 63cc15091c7257cc8bdcf2dc9c16b4499f756709 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Tue, 2 Dec 2025 13:49:32 +0200 Subject: [PATCH 03/35] Added support for dynamic y axis limits determination --- .../chart/bar-chart-basic-config.component.ts | 30 +++ ...rt-with-labels-basic-config.component.html | 3 + .../latest-chart-basic-config.component.html | 78 ++++-- ...polar-area-chart-basic-config.component.ts | 30 +++ .../range-chart-basic-config.component.html | 3 + ...e-series-chart-basic-config.component.html | 3 + .../lib/chart/bar-chart-widget.models.ts | 5 +- .../widget/lib/chart/bars-chart.models.ts | 5 +- .../components/widget/lib/chart/bars-chart.ts | 225 +++++++++++++++++- .../lib/chart/time-series-chart.models.ts | 42 +++- .../widget/lib/chart/time-series-chart.ts | 201 ++++++++++++++++ .../bar-chart-widget-settings.component.ts | 30 +++ ...with-labels-widget-settings.component.html | 3 + ...atest-chart-widget-settings.component.html | 80 +++++-- .../latest-chart-widget-settings.component.ts | 10 + ...ar-area-chart-widget-settings.component.ts | 33 ++- ...range-chart-widget-settings.component.html | 3 + .../common/axis-scale-row.component.html | 71 ++++++ .../common/axis-scale-row.component.scss | 15 ++ .../common/axis-scale-row.component.ts | 204 ++++++++++++++++ ...s-chart-axis-settings-panel.component.html | 3 + ...ies-chart-axis-settings-panel.component.ts | 12 + ...-series-chart-axis-settings.component.html | 48 ++-- ...me-series-chart-axis-settings.component.ts | 41 +++- ...ime-series-chart-y-axes-panel.component.ts | 12 + .../time-series-chart-y-axis-row.component.ts | 7 +- .../common/widget-settings-common.module.ts | 7 +- .../assets/locale/locale.constant-en_US.json | 8 + ui-ngx/tailwind.config.js | 3 +- 29 files changed, 1136 insertions(+), 79 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.ts diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-basic-config.component.ts index ba9c41893e..9f036098be 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-basic-config.component.ts @@ -29,6 +29,8 @@ import { import { LatestChartBasicConfigComponent } from '@home/components/widget/config/basic/chart/latest-chart-basic-config.component'; +import { AxisLimitConfig } from '@home/components/widget/lib/chart/time-series-chart.models'; +import { ValueSourceType } from '@shared/models/widget-settings.models'; @Component({ selector: 'tb-bar-chart-basic-config', @@ -77,4 +79,32 @@ export class BarChartBasicConfigComponent extends LatestChartBasicConfigComponen this.widgetConfig.config.settings.axisTickLabelFont = config.axisTickLabelFont; this.widgetConfig.config.settings.axisTickLabelColor = config.axisTickLabelColor; } + + protected onConfigSet(configData: WidgetConfigComponentData) { + configData.config.settings.axisMin = this.normalizeAxisLimit(configData.config.settings.axisMin); + configData.config.settings.axisMax = this.normalizeAxisLimit(configData.config.settings.axisMax); + super.onConfigSet(configData); + } + + private normalizeAxisLimit(limit: any): AxisLimitConfig { + if (limit && typeof limit === 'object' && 'type' in limit) { + return { + type: limit.type || ValueSourceType.constant, + value: limit.value ?? null, + entityAlias: limit.entityAlias ?? null + }; + } + if (typeof limit === 'number') { + return { + type: ValueSourceType.constant, + value: limit, + entityAlias: null + }; + } + return { + type: ValueSourceType.constant, + value: null, + entityAlias: null + }; + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.html index 307645822d..f100ba8c88 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.html @@ -173,6 +173,9 @@
widgets.time-series-chart.axis.y-axis
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.html index 31d0e6cd7e..a1b2770085 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.html @@ -346,18 +346,8 @@
widgets.bar-chart.bar-axis
-
widgets.chart.chart-axis.scale
+
widgets.chart.chart-axis.scale-appearance
-
widgets.chart.chart-axis.scale-min
- - - -
widgets.chart.chart-axis.scale-max
- - -
+
+
widgets.chart.chart-axis.scale-limits
+
+
+
widgets.chart.chart-axis.limit
+
widgets.chart.chart-axis.source
+
widgets.chart.chart-axis.key-value
+
+
+ + + + + +
+
+
@@ -386,18 +403,8 @@
widgets.polar-area-chart.polar-axis
-
widgets.chart.chart-axis.scale
+
widgets.chart.chart-axis.scale-appearance
-
widgets.chart.chart-axis.scale-min
- - - -
widgets.chart.chart-axis.scale-max
- - -
+
+
widgets.chart.chart-axis.scale-limits
+
+
+
widgets.chart.chart-axis.limit
+
widgets.chart.chart-axis.source
+
widgets.chart.chart-axis.key-value
+
+
+ + + + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/polar-area-chart-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/polar-area-chart-basic-config.component.ts index e0dc692e3d..c38f9e9143 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/polar-area-chart-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/polar-area-chart-basic-config.component.ts @@ -29,6 +29,8 @@ import { import { LatestChartBasicConfigComponent } from '@home/components/widget/config/basic/chart/latest-chart-basic-config.component'; +import { AxisLimitConfig } from '@home/components/widget/lib/chart/time-series-chart.models'; +import { ValueSourceType } from '@shared/models/widget-settings.models'; @Component({ selector: 'tb-polar-area-chart-basic-config', @@ -79,4 +81,32 @@ export class PolarAreaChartBasicConfigComponent extends LatestChartBasicConfigCo this.widgetConfig.config.settings.axisTickLabelColor = config.axisTickLabelColor; this.widgetConfig.config.settings.angleAxisStartAngle = config.angleAxisStartAngle; } + + protected onConfigSet(configData: WidgetConfigComponentData) { + configData.config.settings.axisMin = this.normalizeAxisLimit(configData.config.settings.axisMin); + configData.config.settings.axisMax = this.normalizeAxisLimit(configData.config.settings.axisMax); + super.onConfigSet(configData); + } + + private normalizeAxisLimit(limit: any): AxisLimitConfig { + if (limit && typeof limit === 'object' && 'type' in limit) { + return { + type: limit.type || ValueSourceType.constant, + value: limit.value ?? null, + entityAlias: limit.entityAlias ?? null + }; + } + if (typeof limit === 'number') { + return { + type: ValueSourceType.constant, + value: limit, + entityAlias: null + }; + } + return { + type: ValueSourceType.constant, + value: null, + entityAlias: null + }; + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.html index 608284996e..174df4c692 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.html @@ -235,6 +235,9 @@
widgets.time-series-chart.axis.y-axis
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html index 0c5eb426b3..dd535dfd4d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html @@ -103,6 +103,9 @@ formControlName="states"> diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-widget.models.ts index 3f76eb18cf..0fd3b027ab 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-widget.models.ts @@ -31,10 +31,11 @@ import { ChartBarSettings, chartColorScheme } from '@home/components/widget/lib/chart/chart.models'; +import { AxisLimitConfig } from '@home/components/widget/lib/chart/time-series-chart.models'; export interface BarChartWidgetSettings extends LatestChartWidgetSettings { - axisMin?: number; - axisMax?: number; + axisMin?: number | string | AxisLimitConfig; + axisMax?: number | string | AxisLimitConfig; axisTickLabelFont: Font; axisTickLabelColor: string; barSettings: ChartBarSettings; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bars-chart.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bars-chart.models.ts index 2808d9f7e5..860f4da5b9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bars-chart.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bars-chart.models.ts @@ -24,11 +24,12 @@ import { chartColorScheme } from '@home/components/widget/lib/chart/chart.models'; import { Font } from '@shared/models/widget-settings.models'; +import { AxisLimitConfig } from '@home/components/widget/lib/chart/time-series-chart.models'; export interface BarsChartSettings extends LatestChartSettings { polar: boolean; - axisMin?: number | string; - axisMax?: number | string; + axisMin?: number | string | AxisLimitConfig; + axisMax?: number | string | AxisLimitConfig; axisTickLabelFont: Font; axisTickLabelColor: string; angleAxisStartAngle?: number; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bars-chart.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bars-chart.ts index 92df6dd363..61adae9c20 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bars-chart.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bars-chart.ts @@ -25,7 +25,7 @@ import { ComponentStyle } from '@shared/models/widget-settings.models'; import { LinearGradientObject } from 'zrender/lib/graphic/LinearGradient'; import tinycolor from 'tinycolor2'; import { BarDataItemOption, BarSeriesLabelOption } from 'echarts/types/src/chart/bar/BarSeries'; -import { isDefinedAndNotNull } from '@core/utils'; +import { isDefinedAndNotNull, isNumber, isString } from '@core/utils'; import { ChartFillType, ChartLabelPosition, @@ -35,9 +35,19 @@ import { } from '@home/components/widget/lib/chart/chart.models'; import { ValueAxisBaseOption } from 'echarts/types/src/coord/axisCommonTypes'; import { RadiusAxisOption, YAXisOption } from 'echarts/types/dist/shared'; +import { DataKey, Datasource, DatasourceType, widgetType } from '@shared/models/widget.models'; +import { WidgetSubscriptionOptions } from '@core/api/widget-api.models'; +import { ValueSourceType } from '@shared/models/widget-settings.models'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { AxisLimitConfig } from '@home/components/widget/lib/chart/time-series-chart.models'; export class TbBarsChart extends TbLatestChart { + private dynamicAxisMin: number | string = null; + private dynamicAxisMax: number | string = null; + private minLatestDataKey: DataKey = null; + private maxLatestDataKey: DataKey = null; + constructor(ctx: WidgetContext, inputSettings: DeepPartial, chartElement: HTMLElement, @@ -52,6 +62,212 @@ export class TbBarsChart extends TbLatestChart { return barsChartDefaultSettings; } + protected initSettings() { + super.initSettings(); + this.setupAxisLimits(); + } + + private setupAxisLimits(): void { + const axisLimitDatasources: Datasource[] = []; + + if (isDefinedAndNotNull(this.settings.axisMin)) { + this.processAxisLimit(this.settings.axisMin, 'min', axisLimitDatasources); + } + if (isDefinedAndNotNull(this.settings.axisMax)) { + this.processAxisLimit(this.settings.axisMax, 'max', axisLimitDatasources); + } + + this.subscribeForAxisLimits(axisLimitDatasources); + } + + private processAxisLimit( + limit: any, + limitType: 'min' | 'max', + axisLimitDatasources: Datasource[] + ): void { + if (limit && typeof limit === 'object' && 'type' in limit) { + const axisLimit = limit as AxisLimitConfig; + + if (axisLimit.type === ValueSourceType.latestKey) { + let latestDataKey: DataKey = null; + if (this.ctx.datasources.length) { + for (const datasource of this.ctx.datasources) { + latestDataKey = datasource.latestDataKeys?.find(d => + (d.type === DataKeyType.function && d.label === (axisLimit.value as DataKey).label) || + (d.type !== DataKeyType.function && d.name === (axisLimit.value as DataKey).name && + d.type === (axisLimit.value as DataKey).type)); + if (latestDataKey) { + break; + } + } + } + + if (latestDataKey) { + if (limitType === 'min') { + this.minLatestDataKey = latestDataKey; + } else { + this.maxLatestDataKey = latestDataKey; + } + } + } else if (axisLimit.type === ValueSourceType.entity) { + const entityAliasId = this.ctx.aliasController.getEntityAliasId(axisLimit.entityAlias); + if (entityAliasId) { + let datasource = axisLimitDatasources.find(d => d.entityAliasId === entityAliasId); + const entityDataKey: DataKey = { + type: (axisLimit.value as DataKey).type, + name: (axisLimit.value as DataKey).name, + label: (axisLimit.value as DataKey).name, + settings: { + axisLimit: limitType + } + }; + + if (datasource) { + datasource.dataKeys.push(entityDataKey); + } else { + datasource = { + type: DatasourceType.entity, + name: axisLimit.entityAlias, + aliasName: axisLimit.entityAlias, + entityAliasId, + dataKeys: [entityDataKey] + }; + axisLimitDatasources.push(datasource); + } + } + } else if (axisLimit.type === ValueSourceType.constant) { + const value = axisLimit.value as number; + if (limitType === 'min') { + this.dynamicAxisMin = value; + } else { + this.dynamicAxisMax = value; + } + } + } else if (typeof limit === 'number' || typeof limit === 'string') { + if (limitType === 'min') { + this.dynamicAxisMin = limit; + } else { + this.dynamicAxisMax = limit; + } + } + } + + private subscribeForAxisLimits(datasources: Datasource[]) { + if (datasources.length) { + const axisLimitsSubscriptionOptions: WidgetSubscriptionOptions = { + datasources, + useDashboardTimewindow: false, + type: widgetType.latest, + callbacks: { + onDataUpdated: (subscription) => { + let update = false; + if (subscription.data) { + for (const data of subscription.data) { + const limitType = data.dataKey.settings.axisLimit as ('min' | 'max'); + if (data.data[0]) { + const value = this.parseAxisLimitData(data.data[0][1]); + if (limitType === 'min') { + if (this.dynamicAxisMin !== value) { + this.dynamicAxisMin = value; + update = true; + } + } else { + if (this.dynamicAxisMax !== value) { + this.dynamicAxisMax = value; + update = true; + } + } + } + } + } + if (this.latestChart && update) { + this.updateAxisLimits(); + } + } + } + }; + this.ctx.subscriptionApi.createSubscription(axisLimitsSubscriptionOptions, true).subscribe(); + } + } + + private parseAxisLimitData(data: any): number | null { + let value: number; + if (isDefinedAndNotNull(data)) { + if (isNumber(data)) { + value = data; + } else if (isString(data)) { + value = Number(data); + } + } + if (isDefinedAndNotNull(value) && !isNaN(value)) { + return value; + } + return null; + } + + private updateAxisLimitsFromLatest(): boolean { + let update = false; + + if (this.ctx.latestData) { + if (this.minLatestDataKey) { + const data = this.ctx.latestData.find(d => d.dataKey === this.minLatestDataKey); + if (data?.data[0]) { + const value = this.parseAxisLimitData(data.data[0][1]); + if (this.dynamicAxisMin !== value) { + this.dynamicAxisMin = value; + update = true; + } + } + } + + if (this.maxLatestDataKey) { + const data = this.ctx.latestData.find(d => d.dataKey === this.maxLatestDataKey); + if (data?.data[0]) { + const value = this.parseAxisLimitData(data.data[0][1]); + if (this.dynamicAxisMax !== value) { + this.dynamicAxisMax = value; + update = true; + } + } + } + } + return update; + } + + private updateAxisLimits(): void { + if (this.latestChart && !this.latestChart.isDisposed()) { + const axisTickLabelStyle = createChartTextStyle(this.settings.axisTickLabelFont, + this.settings.axisTickLabelColor, false, 'axis.tickLabel'); + const valueAxis: ValueAxisBaseOption = { + type: 'value', + min: this.dynamicAxisMin, + max: this.dynamicAxisMax, + axisLabel: { + color: axisTickLabelStyle.color, + fontStyle: axisTickLabelStyle.fontStyle, + fontWeight: axisTickLabelStyle.fontWeight, + fontFamily: axisTickLabelStyle.fontFamily, + fontSize: axisTickLabelStyle.fontSize, + formatter: (value: any) => this.valueFormatter.format(value) + } + }; + + if (this.settings.polar) { + this.latestChartOption.radiusAxis = valueAxis as RadiusAxisOption; + } else { + this.latestChartOption.yAxis = valueAxis as YAXisOption; + } + + this.latestChart.setOption(this.latestChartOption); + } + } + + public latestUpdated() { + if (this.updateAxisLimitsFromLatest()) { + this.updateAxisLimits(); + } + } + protected prepareLatestChartOption() { let labelStyle: ComponentStyle = {}; if (this.settings.barSettings.showLabel) { @@ -89,10 +305,12 @@ export class TbBarsChart extends TbLatestChart { const axisTickLabelStyle = createChartTextStyle(this.settings.axisTickLabelFont, this.settings.axisTickLabelColor, false, 'axis.tickLabel'); + const minValue = isDefinedAndNotNull(this.dynamicAxisMin) ? this.dynamicAxisMin : undefined; + const maxValue = isDefinedAndNotNull(this.dynamicAxisMax) ? this.dynamicAxisMax : undefined; const valueAxis: ValueAxisBaseOption = { type: 'value', - min: this.settings.axisMin, - max: this.settings.axisMax, + min: minValue, + max: maxValue, axisLabel: { color: axisTickLabelStyle.color, fontStyle: axisTickLabelStyle.fontStyle, @@ -102,6 +320,7 @@ export class TbBarsChart extends TbLatestChart { formatter: (value: any) => this.valueFormatter.format(value) } }; + if (this.settings.polar) { this.latestChartOption.polar = { radius: '100%' diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts index 089c655224..b3924154d6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts @@ -381,12 +381,18 @@ export interface TimeSeriesChartYAxisSettings extends TimeSeriesChartAxisSetting decimals?: number; interval?: number; splitNumber?: number; - min?: number | string; - max?: number | string; + min?: number | string | AxisLimitConfig; + max?: number | string | AxisLimitConfig; ticksGenerator?: TimeSeriesChartTicksGenerator | string; ticksFormatter?: TimeSeriesChartTicksFormatter | string; } +export interface AxisLimitConfig { + entityAlias: string; + type: ValueSourceType; + value: number | DataKey; +} + export const timeSeriesChartYAxisValid = (axis: TimeSeriesChartYAxisSettings): boolean => !(!axis.id || isUndefinedOrNull(axis.order)); @@ -867,6 +873,11 @@ export interface TimeSeriesChartAxis { id: string; settings: TimeSeriesChartAxisSettings; option: CartesianAxisOption; + dynamicMin?: string| number; + dynamicMax?: string| number; + minLatestDataKey?: DataKey; + maxLatestDataKey?: DataKey; + unitConvertor?: (value: number) => number; } export interface TimeSeriesChartYAxis extends TimeSeriesChartAxis { @@ -926,6 +937,29 @@ export const createTimeSeriesYAxis = (units: string, return ticks?.filter(tick => tick.value >= extent[0] && tick.value <= extent[1]); }; } + + let initialMin: number | string | undefined; + if (isDefinedAndNotNull(settings.min)) { + if (typeof settings.min === 'object' && 'type' in settings.min) { + initialMin = undefined; + } else if (typeof settings.min === 'number') { + initialMin = unitConvertor ? unitConvertor(settings.min) : settings.min; + } else if (typeof settings.min === 'string') { + initialMin = settings.min; + } + } + + let initialMax: number | string | undefined; + if (isDefinedAndNotNull(settings.max)) { + if (typeof settings.max === 'object' && 'type' in settings.max) { + initialMax = undefined; + } else if (typeof settings.max === 'number') { + initialMax = unitConvertor ? unitConvertor(settings.max) : settings.max; + } else if (typeof settings.max === 'string') { + initialMax = settings.max; + } + } + return { id: settings.id, decimals, @@ -939,8 +973,8 @@ export const createTimeSeriesYAxis = (units: string, offset: 0, alignTicks: true, scale: true, - min: isDefinedAndNotNull(settings.min) ? unitConvertor(Number(settings.min)) : settings.min, - max: isDefinedAndNotNull(settings.max) ? unitConvertor(Number(settings.max)) : settings.max, + min: initialMin, + max: initialMax, minInterval, splitNumber, interval, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts index d447b51b37..7954352bb4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts @@ -17,6 +17,7 @@ import { WidgetContext } from '@home/models/widget-component.models'; import { adjustTimeAxisExtentToData, + AxisLimitConfig, calculateThresholdsOffset, createTimeSeriesVisualMapOption, createTimeSeriesXAxis, @@ -61,6 +62,8 @@ import { isDefined, isDefinedAndNotNull, isEqual, + isNumber, + isString, mergeDeep } from '@core/utils'; import { DataKey, Datasource, DatasourceType, FormattedData, widgetType } from '@shared/models/widget.models'; @@ -287,8 +290,14 @@ export class TbTimeSeriesChart { } } } + if (this.updateAxisLimitsFromLatest()) { + update = true; + } if (this.timeSeriesChart && update) { this.updateSeriesData(); + if (this.updateAxisLimitsFromLatest()) { + this.updateAxisLimits(); + } } } @@ -550,6 +559,7 @@ export class TbTimeSeriesChart { private setupYAxes(): void { const yAxisSettingsList = Object.values(this.settings.yAxes); yAxisSettingsList.sort((a1, a2) => a1.order - a2.order); + const axisLimitDatasources: Datasource[] = []; for (const yAxisSettings of yAxisSettingsList) { const axisSettings = mergeDeep({} as TimeSeriesChartYAxisSettings, defaultTimeSeriesChartYAxisSettings, yAxisSettings); @@ -563,8 +573,98 @@ export class TbTimeSeriesChart { axisSettings.ticksFormatter = this.stateValueConverter.ticksFormatter; } const yAxis = createTimeSeriesYAxis(unitSymbol, decimals, axisSettings, this.ctx.utilsService, this.darkMode, unitConvertor); + if (isDefinedAndNotNull(axisSettings.min)) { + this.processAxisLimit(axisSettings.min, 'min', yAxis, axisLimitDatasources, unitConvertor); + } + if (isDefinedAndNotNull(axisSettings.max)) { + this.processAxisLimit(axisSettings.max, 'max', yAxis, axisLimitDatasources, unitConvertor); + } + if (yAxis.minLatestDataKey || yAxis.maxLatestDataKey) { + yAxis.unitConvertor = unitConvertor; + } this.yAxisList.push(yAxis); } + this.subscribeForAxisLimits(axisLimitDatasources); + } + + private processAxisLimit( + limit: any, + limitType: 'min' | 'max', + yAxis: TimeSeriesChartYAxis, + axisLimitDatasources: Datasource[], + unitConvertor?: (value: number) => number + ): void { + if (limit && typeof limit === 'object' && 'type' in limit) { + const axisLimit = limit as AxisLimitConfig; + if (axisLimit.type === ValueSourceType.latestKey) { + let latestDataKey: DataKey = null; + if (this.ctx.datasources.length) { + for (const datasource of this.ctx.datasources) { + latestDataKey = datasource.latestDataKeys?.find(d => + (d.type === DataKeyType.function && d.label === (axisLimit.value as DataKey).label) || + (d.type !== DataKeyType.function && d.name === (axisLimit.value as DataKey).name && + d.type === (axisLimit.value as DataKey).type)); + if (latestDataKey) { + break; + } + } + } + if (latestDataKey) { + if (limitType === 'min') { + yAxis.minLatestDataKey = latestDataKey; + } else { + yAxis.maxLatestDataKey = latestDataKey; + } + } + } else if (axisLimit.type === ValueSourceType.entity) { + const entityAliasId = this.ctx.aliasController.getEntityAliasId(axisLimit.entityAlias); + if (entityAliasId) { + let datasource = axisLimitDatasources.find(d => d.entityAliasId === entityAliasId); + const entityDataKey: DataKey = { + type: (axisLimit.value as DataKey).type, + name: (axisLimit.value as DataKey).name, + label: (axisLimit.value as DataKey).name, + settings: { + yAxisId: yAxis.id, + axisLimit: limitType + } + }; + if (datasource) { + datasource.dataKeys.push(entityDataKey); + } else { + datasource = { + type: DatasourceType.entity, + name: axisLimit.entityAlias, + aliasName: axisLimit.entityAlias, + entityAliasId, + dataKeys: [entityDataKey] + }; + axisLimitDatasources.push(datasource); + } + } + } else if (axisLimit.type === ValueSourceType.constant) { + const value = unitConvertor ? unitConvertor(axisLimit.value as number) : axisLimit.value as number; + if (limitType === 'min') { + yAxis.dynamicMin = value; + yAxis.option.min = value; + } else { + yAxis.dynamicMax = value; + yAxis.option.max = value; + } + } + } else if (typeof limit === 'number' || typeof limit === 'string') { + let value = limit; + if (typeof value === 'number' && unitConvertor) { + value = unitConvertor(value); + } + if (limitType === 'min') { + yAxis.dynamicMin = value; + yAxis.option.min = value; + } else { + yAxis.dynamicMax = value; + yAxis.option.max = value; + } + } } private setupVisualMap(): void { @@ -622,6 +722,45 @@ export class TbTimeSeriesChart { } } + private subscribeForAxisLimits(datasources: Datasource[]) { + if (datasources.length) { + const axisLimitsSubscriptionOptions: WidgetSubscriptionOptions = { + datasources, + useDashboardTimewindow: false, + type: widgetType.latest, + callbacks: { + onDataUpdated: (subscription) => { + let update = false; + if (subscription.data) { + for (const yAxis of this.yAxisList) { + for (const data of subscription.data) { + if (data.dataKey.settings?.yAxisId === yAxis.id) { + const limitType = data.dataKey.settings.axisLimit as ('min' | 'max'); + if (data.data[0]) { + const value = this.parseAxisLimitData(data.data[0][1], yAxis.unitConvertor); + if (limitType === 'min') { + yAxis.dynamicMin = value; + yAxis.option.min = value; + } else { + yAxis.dynamicMax = value; + yAxis.option.max = value; + } + update = true; + } + } + } + } + } + if (this.timeSeriesChart && update) { + this.updateAxisLimits(); + } + } + } + }; + this.ctx.subscriptionApi.createSubscription(axisLimitsSubscriptionOptions, true).subscribe(); + } + } + private drawChart() { echartsModule.init(); this.renderer.setStyle(this.chartElement, 'letterSpacing', 'normal'); @@ -714,6 +853,58 @@ export class TbTimeSeriesChart { } } + private updateAxisLimitsFromLatest(): boolean { + let update = false; + + if (this.ctx.latestData) { + for (const yAxis of this.yAxisList) { + if (yAxis.minLatestDataKey) { + const data = this.ctx.latestData.find(d => d.dataKey === yAxis.minLatestDataKey); + if (data?.data[0]) { + const value = this.parseAxisLimitData(data.data[0][1], yAxis.unitConvertor); + + if (yAxis.dynamicMin !== value) { + yAxis.dynamicMin = value; + yAxis.option.min = value; + update = true; + } + } + } + + if (yAxis.maxLatestDataKey) { + const data = this.ctx.latestData.find(d => d.dataKey === yAxis.maxLatestDataKey); + if (data?.data[0]) { + const value = this.parseAxisLimitData(data.data[0][1], yAxis.unitConvertor); + if (yAxis.dynamicMax !== value) { + yAxis.dynamicMax = value; + yAxis.option.max = value; + update = true; + } + } + } + } + } + return update; + } + + private parseAxisLimitData = (data: any, unitConvertor?: (value: number) => number): number => { + let value: number; + if (isDefinedAndNotNull(data)) { + if (isNumber(data)) { + value = data; + } else if (isString(data)) { + value = Number(data); + } + } + if (isDefinedAndNotNull(value) && !isNaN(value)) { + if (unitConvertor) { + return unitConvertor(value); + } + return value; + } + return null; + }; + private updateSeries(): void { this.timeSeriesChartOptions.series = generateChartData(this.dataItems, this.thresholdItems, this.stackMode, @@ -836,6 +1027,16 @@ export class TbTimeSeriesChart { return changed; } + private updateAxisLimits(): void { + if (this.timeSeriesChart && !this.timeSeriesChart.isDisposed()) { + this.timeSeriesChartOptions.yAxis = this.yAxisList.map(axis => axis.option); + this.timeSeriesChart.setOption(this.timeSeriesChartOptions, { + replaceMerge: ['yAxis'] + }); + this.updateAxes(); + } + } + private scaleYAxis(yAxis: TimeSeriesChartYAxis): boolean { if (!this.stateData) { const axisBarDataItems = this.dataItems.filter(d => d.yAxisId === yAxis.id && d.enabled && diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-widget-settings.component.ts index 8826e9afda..b1734ff45a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-widget-settings.component.ts @@ -26,6 +26,8 @@ import { import { LatestChartWidgetSettingsComponent } from '@home/components/widget/lib/settings/chart/latest-chart-widget-settings.component'; +import { AxisLimitConfig } from '@home/components/widget/lib/chart/time-series-chart.models'; +import { ValueSourceType } from '@shared/models/widget-settings.models'; @Component({ selector: 'tb-bar-chart-widget-settings', @@ -57,4 +59,32 @@ export class BarChartWidgetSettingsComponent extends LatestChartWidgetSettingsCo latestChartWidgetSettingsForm.addControl('axisTickLabelFont', this.fb.control(settings.axisTickLabelFont, [])); latestChartWidgetSettingsForm.addControl('axisTickLabelColor', this.fb.control(settings.axisTickLabelColor, [])); } + + protected prepareInputSettings(settings: WidgetSettings): WidgetSettings { + settings.axisMin = this.normalizeAxisLimit(settings.axisMin); + settings.axisMax = this.normalizeAxisLimit(settings.axisMax); + return super.prepareInputSettings(settings); + } + + private normalizeAxisLimit(limit: any): AxisLimitConfig { + if (limit && typeof limit === 'object' && 'type' in limit) { + return { + type: limit.type || ValueSourceType.constant, + value: limit.value ?? null, + entityAlias: limit.entityAlias ?? null + }; + } + if (typeof limit === 'number') { + return { + type: ValueSourceType.constant, + value: limit, + entityAlias: null + }; + } + return { + type: ValueSourceType.constant, + value: null, + entityAlias: null + }; + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html index 488a08e161..08dd087cdb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html @@ -91,6 +91,9 @@
widgets.time-series-chart.axis.y-axis
widgets.bar-chart.bar-axis
-
widgets.chart.chart-axis.scale
+
widgets.chart.chart-axis.scale-appearance
-
widgets.chart.chart-axis.scale-min
- - - -
widgets.chart.chart-axis.scale-max
- - -
+
+
widgets.chart.chart-axis.scale-limits
+
+
+
widgets.chart.chart-axis.limit
+
widgets.chart.chart-axis.source
+
widgets.chart.chart-axis.key-value
+
+
+
+ + + + + +
+
+
+
@@ -311,18 +330,8 @@
widgets.polar-area-chart.polar-axis
-
widgets.chart.chart-axis.scale
+
widgets.chart.chart-axis.scale-appearance
-
widgets.chart.chart-axis.scale-min
- - - -
widgets.chart.chart-axis.scale-max
- - -
+
+
widgets.chart.chart-axis.scale-limits
+
+
+
widgets.chart.chart-axis.limit
+
widgets.chart.chart-axis.source
+
widgets.chart.chart-axis.key-value
+
+
+ + + + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.ts index ff7641bf1a..fadee36370 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.ts @@ -22,6 +22,7 @@ import { LatestChartWidgetSettings } from '@home/components/widget/lib/chart/latest-chart.models'; import { + Datasource, legendPositions, legendPositionTranslationMap, WidgetSettings, @@ -103,6 +104,15 @@ export abstract class LatestChartWidgetSettingsComponent, protected fb: UntypedFormBuilder) { super(store); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/polar-area-chart-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/polar-area-chart-widget-settings.component.ts index 07d43f4de2..61c533a377 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/polar-area-chart-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/polar-area-chart-widget-settings.component.ts @@ -26,6 +26,8 @@ import { import { LatestChartWidgetSettingsComponent } from '@home/components/widget/lib/settings/chart/latest-chart-widget-settings.component'; +import { AxisLimitConfig } from '@home/components/widget/lib/chart/time-series-chart.models'; +import { ValueSourceType } from '@shared/models/widget-settings.models'; @Component({ selector: 'tb-polar-area-chart-widget-settings', @@ -52,10 +54,37 @@ export class PolarAreaChartWidgetSettingsComponent extends LatestChartWidgetSett protected setupLatestChartControls(latestChartWidgetSettingsForm: UntypedFormGroup, settings: WidgetSettings) { latestChartWidgetSettingsForm.addControl('barSettings', this.fb.control(settings.barSettings, [])); - latestChartWidgetSettingsForm.addControl('axisMin', this.fb.control(settings.axisMin, [])); - latestChartWidgetSettingsForm.addControl('axisMax', this.fb.control(settings.axisMax, [])); + latestChartWidgetSettingsForm.addControl('axisMin', this.fb.control(settings.axisMin)); + latestChartWidgetSettingsForm.addControl('axisMax', this.fb.control(settings.axisMax)); latestChartWidgetSettingsForm.addControl('axisTickLabelFont', this.fb.control(settings.axisTickLabelFont, [])); latestChartWidgetSettingsForm.addControl('axisTickLabelColor', this.fb.control(settings.axisTickLabelColor, [Validators.min(0)])); latestChartWidgetSettingsForm.addControl('angleAxisStartAngle', this.fb.control(settings.angleAxisStartAngle, [])); } + protected prepareInputSettings(settings: WidgetSettings): WidgetSettings { + settings.axisMin = this.normalizeAxisLimit(settings.axisMin); + settings.axisMax = this.normalizeAxisLimit(settings.axisMax); + return super.prepareInputSettings(settings); + } + + private normalizeAxisLimit(limit: any): AxisLimitConfig { + if (limit && typeof limit === 'object' && 'type' in limit) { + return { + type: limit.type || ValueSourceType.constant, + value: limit.value ?? null, + entityAlias: limit.entityAlias ?? null + }; + } + if (typeof limit === 'number') { + return { + type: ValueSourceType.constant, + value: limit, + entityAlias: null + }; + } + return { + type: ValueSourceType.constant, + value: null, + entityAlias: null + }; + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.html index f19784735c..c27b7c46b6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.html @@ -169,6 +169,9 @@
widgets.time-series-chart.axis.y-axis
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.html new file mode 100644 index 0000000000..eed985187d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.html @@ -0,0 +1,71 @@ + +
+
+ {{ labelKey | translate}} +
+
+ + + + {{ ValueSourceTypeTranslation.get(type) | translate }} + + + + + +
+
+ + + + + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.scss new file mode 100644 index 0000000000..1a4214be18 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.scss @@ -0,0 +1,15 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.ts new file mode 100644 index 0000000000..b8103dd008 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.ts @@ -0,0 +1,204 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, DestroyRef, forwardRef, Input, OnInit } from '@angular/core'; +import { + ControlValueAccessor, NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormGroup, ValidationErrors, Validator, + Validators, +} from '@angular/forms'; +import { ValueSourceType, ValueSourceTypes, ValueSourceTypeTranslation } from '@shared/models/widget-settings.models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { AxisLimitConfig } from '@home/components/widget/lib/chart/time-series-chart.models'; +import { Datasource, DatasourceType, } from '@shared/models/widget.models'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { IAliasController } from '@core/api/widget-api.models'; +import { DataKeysCallbacks } from '@home/components/widget/lib/settings/common/key/data-keys.component.models'; +import { isEqual } from '@core/utils'; + +@Component({ + selector: 'tb-axis-scale-row', + templateUrl: './axis-scale-row.component.html', + styleUrl: './axis-scale-row.component.scss', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AxisScaleRowComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => AxisScaleRowComponent), + multi: true + }, + ] +}) +export class AxisScaleRowComponent implements ControlValueAccessor, OnInit, Validator { + + @Input() + isPanelView = false; + + @Input() + aliasController: IAliasController; + + @Input() + callbacks: DataKeysCallbacks; + + @Input() + datasource: Datasource; + + @Input() + labelKey: string; + + ValueSourceType = ValueSourceType; + + DataKeyType = DataKeyType; + + DatasourceType = DatasourceType; + + ValueSourceTypeTranslation = ValueSourceTypeTranslation; + + ValueSourceTypes = ValueSourceTypes; + + limitForm: UntypedFormGroup; + + private propagateChanges: (value: any) => void = () => {}; + + private modelValue: AxisLimitConfig | null = null; + + constructor(private fb: UntypedFormBuilder, + private destroyRef: DestroyRef) { + } + + ngOnInit() { + this.limitForm = this.fb.group({ + type: [ValueSourceType.constant], + value: [null], + entityAlias: [null] + }); + this.subscribeToTypeChanges(); + + this.limitForm.valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(value => { + this.updateValidators(); + this.updateView(value); + }); + } + + writeValue(value: AxisLimitConfig) { + this.modelValue = value; + + if (!this.limitForm) { + return; + } + + if (value) { + this.limitForm.patchValue({ + type: value.type ?? ValueSourceType.constant, + value: value.value ?? null, + entityAlias: value.entityAlias ?? null + }, + { emitEvent: false } + ); + } else { + this.limitForm.patchValue({ + type: ValueSourceType.constant, + value: null, + entityAlias: null + }, + { emitEvent: false } + ); + } + + this.updateValidators(); + } + + registerOnChange(fn: any) { + this.propagateChanges = fn; + } + + registerOnTouched(fn: any) { + } + + validate(): ValidationErrors | null { + return this.limitForm.valid ? null : { + axisLimitForm: { + valid: false, + errors: this.getFormErrors() + } + }; + } + + private getFormErrors(): any { + const errors: any = {}; + Object.keys(this.limitForm.controls).forEach(key => { + const control = this.limitForm.get(key); + if (control && control.errors) { + errors[key] = control.errors; + } + }); + return errors; + } + + private updateValidators() { + const axisTypeControl = this.limitForm.get('type'); + const axisValueControl = this.limitForm.get('value'); + const axisEntityAliasControl = this.limitForm.get('entityAlias'); + + if (axisTypeControl && axisValueControl && axisEntityAliasControl) { + const type = axisTypeControl.value; + if (type === ValueSourceType.latestKey || type === ValueSourceType.entity) { + axisValueControl.setValidators([Validators.required]); + } else { + axisValueControl.clearValidators(); + } + + if (type === ValueSourceType.entity) { + axisEntityAliasControl.setValidators([Validators.required]); + } else { + axisEntityAliasControl.clearValidators(); + } + + axisValueControl.updateValueAndValidity({ emitEvent: false }); + axisEntityAliasControl.updateValueAndValidity({ emitEvent: false }); + } + } + + private subscribeToTypeChanges() { + this.limitForm.controls.type.valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => { + const axisValueControl = this.limitForm.get('value'); + const axisEntityAliasControl = this.limitForm.get('entityAlias'); + if (axisValueControl) { + axisValueControl.setValue(null, { emitEvent: true }); + } + if (axisEntityAliasControl) { + axisEntityAliasControl.setValue(null, { emitEvent: true }); + } + }); + } + + private updateView(value: AxisLimitConfig) { + if (!isEqual(this.modelValue, value)) { + this.modelValue = value; + this.propagateChanges(value); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component.html index a462c92c03..bd6e8e888a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component.html @@ -19,6 +19,9 @@
{{ panelTitle }}
+
+
+
widgets.chart.chart-axis.scale-limits
+
+
+
widgets.chart.chart-axis.limit
+
widgets.chart.chart-axis.source
+
widgets.chart.chart-axis.key-value
+
+
+ + + + + +
+
+
+
-
-
-
widgets.chart.chart-axis.scale
-
-
widgets.chart.chart-axis.scale-min
- - - -
widgets.chart.chart-axis.scale-max
- - - -
-
-
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts index 40f2a96c69..4bf7a78f98 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts @@ -17,9 +17,11 @@ import { Component, DestroyRef, forwardRef, Input, OnInit } from '@angular/core'; import { ControlValueAccessor, + NG_VALIDATORS, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup, + ValidationErrors, Validator, Validators } from '@angular/forms'; import { @@ -32,6 +34,9 @@ import { merge } from 'rxjs'; import { coerceBoolean } from '@shared/decorators/coercion'; import { WidgetService } from '@core/http/widget.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { IAliasController } from '@app/core/public-api'; +import { Datasource } from '@app/shared/public-api'; +import { DataKeysCallbacks } from '@home/components/widget/lib/settings/common/key/data-keys.component.models'; @Component({ selector: 'tb-time-series-chart-axis-settings', @@ -42,10 +47,15 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => TimeSeriesChartAxisSettingsComponent), multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => TimeSeriesChartAxisSettingsComponent), + multi: true } ] }) -export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValueAccessor { +export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValueAccessor, Validator { @Input() @coerceBoolean() @@ -61,6 +71,15 @@ export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValu defaultXAxisTicksFormat = defaultXAxisTicksFormat; + @Input() + aliasController: IAliasController; + + @Input() + dataKeyCallbacks: DataKeysCallbacks; + + @Input() + datasource: Datasource; + @Input() disabled: boolean; @@ -147,6 +166,15 @@ export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValu registerOnTouched(_fn: any): void { } + validate(): ValidationErrors | null { + return this.axisSettingsFormGroup.valid ? null : { + axisSettings: { + valid: false, + errors: this.getFormErrors() + } + }; + } + setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; if (isDisabled) { @@ -222,6 +250,17 @@ export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValu } } + private getFormErrors(): any { + const errors: any = {}; + Object.keys(this.axisSettingsFormGroup.controls).forEach(key => { + const control = this.axisSettingsFormGroup.get(key); + if (control && control.errors) { + errors[key] = control.errors; + } + }); + return errors; + } + private updateModel() { this.modelValue = this.axisSettingsFormGroup.getRawValue(); this.propagateChange(this.modelValue); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts index 12ac24f9e3..4af0f5f613 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts @@ -47,6 +47,9 @@ import { mergeDeep } from '@core/utils'; import { CdkDragDrop } from '@angular/cdk/drag-drop'; import { coerceBoolean } from '@shared/decorators/coercion'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { IAliasController } from '@app/core/public-api'; +import { DataKeysCallbacks } from '@home/components/widget/lib/settings/common/key/data-keys.component.models'; +import { Datasource } from '@app/shared/public-api'; @Component({ selector: 'tb-time-series-chart-y-axes-panel', @@ -68,6 +71,15 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; }) export class TimeSeriesChartYAxesPanelComponent implements ControlValueAccessor, OnInit, Validator { + @Input() + aliasController: IAliasController; + + @Input() + dataKeyCallbacks: DataKeysCallbacks; + + @Input() + datasource: Datasource; + @Input() disabled: boolean; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts index ebc24ddd22..1857852afe 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts @@ -42,6 +42,7 @@ import { import { deepClone } from '@core/utils'; import { TranslateService } from '@ngx-translate/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { TimeSeriesChartYAxesPanelComponent } from '@home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component'; @Component({ selector: 'tb-time-series-chart-y-axis-row', @@ -85,6 +86,7 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O constructor(private fb: UntypedFormBuilder, private translate: TranslateService, private popoverService: TbPopoverService, + private timeSeriesChartYAxesPanel: TimeSeriesChartYAxesPanelComponent, private renderer: Renderer2, private viewContainerRef: ViewContainerRef, private cd: ChangeDetectorRef, @@ -165,7 +167,10 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O axisType: 'yAxis', panelTitle: this.translate.instant('widgets.time-series-chart.axis.y-axis-settings'), axisSettings: deepClone(this.modelValue), - advanced: this.advanced + advanced: this.advanced, + aliasController: this.timeSeriesChartYAxesPanel.aliasController, + dataKeyCallbacks: this.timeSeriesChartYAxesPanel.dataKeyCallbacks, + datasource: this.timeSeriesChartYAxesPanel.datasource }, isModal: true }); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts index 233b202684..385bc55495 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts @@ -267,6 +267,7 @@ import { import { ShapeFillStripeSettingsPanelComponent } from '@home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component'; +import { AxisScaleRowComponent } from './axis-scale-row.component'; @NgModule({ declarations: [ @@ -372,7 +373,8 @@ import { DataKeysComponent, DataKeyConfigDialogComponent, DataKeyConfigComponent, - WidgetSettingsComponent + WidgetSettingsComponent, + AxisScaleRowComponent ], imports: [ CommonModule, @@ -453,7 +455,8 @@ import { DataKeysComponent, DataKeyConfigDialogComponent, DataKeyConfigComponent, - WidgetSettingsComponent + WidgetSettingsComponent, + AxisScaleRowComponent ], providers: [ ColorSettingsComponentService, diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 061ce65f0e..25eaa16793 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -7638,6 +7638,14 @@ "update-animation-delay": "Update animation delay" }, "chart-axis": { + "limit":"Limit", + "source": "Source", + "key-value": "Key / Value", + "value-required": "Value is required.", + "entity-key-required": "Entity key is required.", + "key-required": "Key is required.", + "scale-limits": "Scale limits", + "scale-appearance": "Scale appearance", "scale": "Scale", "scale-min": "min", "scale-max": "max", diff --git a/ui-ngx/tailwind.config.js b/ui-ngx/tailwind.config.js index d675da4cda..bc516ddf40 100644 --- a/ui-ngx/tailwind.config.js +++ b/ui-ngx/tailwind.config.js @@ -84,7 +84,8 @@ module.exports = { '25': '6.25rem', '37.5': '9.375rem', '62.5': '15.625rem', - '72.5': '18.125rem' + '72.5': '18.125rem', + '40%': '40%' }, flex: { full: '1 1 100%' From b6c21e9748f5a6023092d036b7c421cf2779beb7 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 3 Dec 2025 13:04:33 +0200 Subject: [PATCH 04/35] improved env descriptions for site --- .../src/main/resources/thingsboard.yml | 38 +++++++++---------- .../src/main/resources/tb-coap-transport.yml | 14 +++---- .../src/main/resources/tb-lwm2m-transport.yml | 16 ++++---- 3 files changed, 34 insertions(+), 34 deletions(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index d3d04bcea0..f7b23f9360 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -738,13 +738,13 @@ redis: # if set false will be used pool config build from values of the pool config section useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" sentinel: - # name of the master node + # Name of the master node master: "${REDIS_MASTER:}" - # comma-separated list of "host:port" pairs of sentinels + # Comma-separated list of "host:port" pairs of sentinels sentinels: "${REDIS_SENTINELS:}" - # password to authenticate with sentinel + # Password to authenticate with sentinel password: "${REDIS_SENTINEL_PASSWORD:}" - # if set false will be used pool config build from values of the pool config section + # If set false will be used pool config build from values of the pool config section useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" # db index db: "${REDIS_DB:0}" @@ -1165,15 +1165,15 @@ transport: dtls: # RFC7925_RETRANSMISSION_TIMEOUT_IN_MILLISECONDS = 9000 retransmission_timeout: "${LWM2M_DTLS_RETRANSMISSION_TIMEOUT_MS:9000}" - # CoAP DTLS connection ID length for LWM2M. RFC 9146, Connection Identifier for DTLS 1.2 - # Default: off + # LWM2M DTLS connection ID length for LWM2M. RFC 9146, Connection Identifier for DTLS 1.2 + # Default: off.
# Control usage of DTLS connection ID length (CID). - # - 'off' to deactivate it. - # - 'on' to activate Connection ID support (same as CID 0 or more 0). - # - A positive value defines generated CID size in bytes. - # - A value of 0 means we accept using CID but will not generate one for foreign peer (enables support but not for incoming traffic). - # - A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used - # - A value that are > 4: MultiNodeConnectionIdGenerator is used + #
  • 'off' to deactivate it.
  • + #
  • 'on' to activate Connection ID support (same as CID 0 or more 0).
  • + #
  • A positive value defines generated CID size in bytes.
  • + #
  • A value of 0 means we accept using CID but will not generate one for foreign peer (enables support but not for incoming traffic).
  • + #
  • A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used
  • + #
  • A value that are > 4: MultiNodeConnectionIdGenerator is used
connection_id_length: "${LWM2M_DTLS_CONNECTION_ID_LENGTH:8}" server: # LwM2M Server ID @@ -1356,14 +1356,14 @@ coap: # CoAP DTLS bind port bind_port: "${COAP_DTLS_BIND_PORT:5684}" # CoAP DTLS connection ID length. RFC 9146, Connection Identifier for DTLS 1.2 - # Default: off + # Default: off.
# Control usage of DTLS connection ID length (CID). - # - 'off' to deactivate it. - # - 'on' to activate Connection ID support (same as CID 0 or more 0). - # - A positive value defines generated CID size in bytes. - # - A value of 0 means we accept using CID but will not generate one for foreign peer (enables support but not for incoming traffic). - # - A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used - # - A value that are > 4: MultiNodeConnectionIdGenerator is used + #
  • 'off' to deactivate it.
  • + #
  • 'on' to activate Connection ID support (same as CID 0 or more 0).
  • + #
  • A positive value defines generated CID size in bytes.
  • + #
  • A value of 0 means we accept using CID but will not generate one for foreign peer (enables support but not for incoming traffic).
  • + #
  • A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used
  • + #
  • A value that are > 4: MultiNodeConnectionIdGenerator is used
connection_id_length: "${COAP_DTLS_CONNECTION_ID_LENGTH:8}" # Specify the MTU (Maximum Transmission Unit). # Should be used if LAN MTU is not used, e.g. if IP tunnels are used or if the client uses a smaller value than the LAN MTU. diff --git a/transport/coap/src/main/resources/tb-coap-transport.yml b/transport/coap/src/main/resources/tb-coap-transport.yml index 2f3942f847..497af7b322 100644 --- a/transport/coap/src/main/resources/tb-coap-transport.yml +++ b/transport/coap/src/main/resources/tb-coap-transport.yml @@ -185,14 +185,14 @@ coap: # CoAP DTLS bind port bind_port: "${COAP_DTLS_BIND_PORT:5684}" # CoAP DTLS connection ID length. RFC 9146, Connection Identifier for DTLS 1.2 - # Default: off + # Default: off.
# Control usage of DTLS connection ID length (CID). - # - 'off' to deactivate it. - # - 'on' to activate Connection ID support (same as CID 0 or more 0). - # - A positive value defines generated CID size in bytes. - # - A value of 0 means we accept using CID but will not generate one for foreign peer (enables support but not for incoming traffic). - # - A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used - # - A value that are > 4: MultiNodeConnectionIdGenerator is used + #
  • 'off' to deactivate it.
  • + #
  • 'on' to activate Connection ID support (same as CID 0 or more 0).
  • + #
  • A positive value defines generated CID size in bytes.
  • + #
  • A value of 0 means we accept using CID but will not generate one for foreign peer (enables support but not for incoming traffic).
  • + #
  • A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used
  • + #
  • A value that are > 4: MultiNodeConnectionIdGenerator is used
connection_id_length: "${COAP_DTLS_CONNECTION_ID_LENGTH:8}" # Specify the MTU (Maximum Transmission Unit). # Should be used if LAN MTU is not used, e.g. if IP tunnels are used or if the client uses a smaller value than the LAN MTU. diff --git a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml index 323f80b999..51c93948a5 100644 --- a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml +++ b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml @@ -164,15 +164,15 @@ transport: dtls: # RFC7925_RETRANSMISSION_TIMEOUT_IN_MILLISECONDS = 9000 retransmission_timeout: "${LWM2M_DTLS_RETRANSMISSION_TIMEOUT_MS:9000}" - # CoAP DTLS connection ID length for LWM2M. RFC 9146, Connection Identifier for DTLS 1.2 - # Default: off + # LWM2M DTLS connection ID length for LWM2M. RFC 9146, Connection Identifier for DTLS 1.2 + # Default: off.
# Control usage of DTLS connection ID length (CID). - # - 'off' to deactivate it. - # - 'on' to activate Connection ID support (same as CID 0 or more 0). - # - A positive value defines generated CID size in bytes. - # - A value of 0 means we accept using CID but will not generate one for foreign peer (enables support but not for incoming traffic). - # - A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used - # - A value that are > 4: MultiNodeConnectionIdGenerator is used + #
  • 'off' to deactivate it.
  • + #
  • 'on' to activate Connection ID support (same as CID 0 or more 0).
  • + #
  • A positive value defines generated CID size in bytes.
  • + #
  • A value of 0 means we accept using CID but will not generate one for foreign peer (enables support but not for incoming traffic).
  • + #
  • A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used
  • + #
  • A value that are > 4: MultiNodeConnectionIdGenerator is used
connection_id_length: "${LWM2M_DTLS_CONNECTION_ID_LENGTH:8}" server: # LwM2M Server ID From ba6d84c40bebe4b16cb2381420de506654d7d61d Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Wed, 3 Dec 2025 17:32:21 +0200 Subject: [PATCH 05/35] fixed latest limit type display --- .../chart/bar-chart-basic-config.component.ts | 32 +-- .../latest-chart-basic-config.component.html | 132 ++++------ ...polar-area-chart-basic-config.component.ts | 32 +-- .../lib/chart/bar-chart-widget.models.ts | 5 +- .../widget/lib/chart/bars-chart.models.ts | 5 +- .../components/widget/lib/chart/bars-chart.ts | 225 +----------------- .../widget/lib/chart/latest-chart.ts | 1 + .../lib/chart/time-series-chart.models.ts | 12 +- .../widget/lib/chart/time-series-chart.ts | 133 +++++------ .../bar-chart-widget-settings.component.ts | 30 --- ...atest-chart-widget-settings.component.html | 94 +++----- .../latest-chart-widget-settings.component.ts | 10 - ...ar-area-chart-widget-settings.component.ts | 22 +- .../common/axis-scale-row.component.html | 4 +- .../common/axis-scale-row.component.ts | 129 +++++----- ...ime-series-chart-y-axes-panel.component.ts | 104 +++++++- 16 files changed, 328 insertions(+), 642 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-basic-config.component.ts index a58ddd8302..4c7da10bc6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-basic-config.component.ts @@ -29,8 +29,6 @@ import { import { LatestChartBasicConfigComponent } from '@home/components/widget/config/basic/chart/latest-chart-basic-config.component'; -import { AxisLimitConfig } from '@home/components/widget/lib/chart/time-series-chart.models'; -import { ValueSourceType } from '@shared/models/widget-settings.models'; @Component({ selector: 'tb-bar-chart-basic-config', @@ -43,7 +41,7 @@ export class BarChartBasicConfigComponent extends LatestChartBasicConfigComponen barChartConfigTemplate: TemplateRef; predefinedValues = widgetTitleAutocompleteValues; - + constructor(protected store: Store, protected widgetConfigComponent: WidgetConfigComponent, protected fb: UntypedFormBuilder) { @@ -81,32 +79,4 @@ export class BarChartBasicConfigComponent extends LatestChartBasicConfigComponen this.widgetConfig.config.settings.axisTickLabelFont = config.axisTickLabelFont; this.widgetConfig.config.settings.axisTickLabelColor = config.axisTickLabelColor; } - - protected onConfigSet(configData: WidgetConfigComponentData) { - configData.config.settings.axisMin = this.normalizeAxisLimit(configData.config.settings.axisMin); - configData.config.settings.axisMax = this.normalizeAxisLimit(configData.config.settings.axisMax); - super.onConfigSet(configData); - } - - private normalizeAxisLimit(limit: any): AxisLimitConfig { - if (limit && typeof limit === 'object' && 'type' in limit) { - return { - type: limit.type || ValueSourceType.constant, - value: limit.value ?? null, - entityAlias: limit.entityAlias ?? null - }; - } - if (typeof limit === 'number') { - return { - type: ValueSourceType.constant, - value: limit, - entityAlias: null - }; - } - return { - type: ValueSourceType.constant, - value: null, - entityAlias: null - }; - } } diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.html index 27c173bd17..d72a8d6083 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.html @@ -21,27 +21,27 @@ formControlName="timewindowConfig"> + [configMode]="basicMode" + hideDatasourceLabel + hideDataKeys + forceSingleDatasource + formControlName="datasources"> + panelTitle="{{ 'widgets.chart.series' | translate }}" + addKeyTitle="{{ 'widgets.chart.add-series' | translate }}" + keySettingsTitle="{{ 'widgets.chart.series-settings' | translate }}" + removeKeyTitle="{{ 'widgets.chart.remove-series' | translate }}" + noKeysText="{{ 'widgets.chart.no-series' | translate }}" + requiredKeysText="{{ 'widgets.chart.no-series-error' | translate }}" + hideUnits + hideDecimals + hideDataKeyUnits + hideDataKeyDecimals + [datasourceType]="datasource?.type" + [deviceId]="datasource?.deviceId" + [entityAliasId]="datasource?.entityAliasId" + formControlName="series">
widget-config.appearance
@@ -210,7 +210,7 @@
+ formControlName="animation">
widget-config.card-appearance
@@ -239,7 +239,7 @@
+ formControlName="actions"> @@ -346,15 +346,25 @@
widgets.bar-chart.bar-appearance
+ formControlName="barSettings" + [series]="false">
widgets.bar-chart.bar-axis
-
widgets.chart.chart-axis.scale-appearance
+
widgets.chart.chart-axis.scale
+
widgets.chart.chart-axis.scale-min
+ + + +
widgets.chart.chart-axis.scale-max
+ + +
-
-
widgets.chart.chart-axis.scale-limits
-
-
-
widgets.chart.chart-axis.limit
-
widgets.chart.chart-axis.source
-
widgets.chart.chart-axis.key-value
-
-
- - - - - -
-
-
@@ -402,16 +385,26 @@
widgets.bar-chart.bar-appearance
+ formControlName="barSettings" + pieLabelPosition + [series]="false">
widgets.polar-area-chart.polar-axis
-
widgets.chart.chart-axis.scale-appearance
+
widgets.chart.chart-axis.scale
+
widgets.chart.chart-axis.scale-min
+ + + +
widgets.chart.chart-axis.scale-max
+ + +
-
-
widgets.chart.chart-axis.scale-limits
-
-
-
widgets.chart.chart-axis.limit
-
widgets.chart.chart-axis.source
-
widgets.chart.chart-axis.key-value
-
-
- - - - - -
-
-
@@ -542,7 +508,7 @@
+ formControlName="fillAreaSettings">
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/polar-area-chart-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/polar-area-chart-basic-config.component.ts index d29af73b75..f60f8d2417 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/polar-area-chart-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/polar-area-chart-basic-config.component.ts @@ -29,8 +29,6 @@ import { import { LatestChartBasicConfigComponent } from '@home/components/widget/config/basic/chart/latest-chart-basic-config.component'; -import { AxisLimitConfig } from '@home/components/widget/lib/chart/time-series-chart.models'; -import { ValueSourceType } from '@shared/models/widget-settings.models'; @Component({ selector: 'tb-polar-area-chart-basic-config', @@ -43,7 +41,7 @@ export class PolarAreaChartBasicConfigComponent extends LatestChartBasicConfigCo polarAreaChartConfigTemplate: TemplateRef; predefinedValues = widgetTitleAutocompleteValues; - + constructor(protected store: Store, protected widgetConfigComponent: WidgetConfigComponent, protected fb: UntypedFormBuilder) { @@ -83,32 +81,4 @@ export class PolarAreaChartBasicConfigComponent extends LatestChartBasicConfigCo this.widgetConfig.config.settings.axisTickLabelColor = config.axisTickLabelColor; this.widgetConfig.config.settings.angleAxisStartAngle = config.angleAxisStartAngle; } - - protected onConfigSet(configData: WidgetConfigComponentData) { - configData.config.settings.axisMin = this.normalizeAxisLimit(configData.config.settings.axisMin); - configData.config.settings.axisMax = this.normalizeAxisLimit(configData.config.settings.axisMax); - super.onConfigSet(configData); - } - - private normalizeAxisLimit(limit: any): AxisLimitConfig { - if (limit && typeof limit === 'object' && 'type' in limit) { - return { - type: limit.type || ValueSourceType.constant, - value: limit.value ?? null, - entityAlias: limit.entityAlias ?? null - }; - } - if (typeof limit === 'number') { - return { - type: ValueSourceType.constant, - value: limit, - entityAlias: null - }; - } - return { - type: ValueSourceType.constant, - value: null, - entityAlias: null - }; - } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-widget.models.ts index 0fd3b027ab..3f76eb18cf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-widget.models.ts @@ -31,11 +31,10 @@ import { ChartBarSettings, chartColorScheme } from '@home/components/widget/lib/chart/chart.models'; -import { AxisLimitConfig } from '@home/components/widget/lib/chart/time-series-chart.models'; export interface BarChartWidgetSettings extends LatestChartWidgetSettings { - axisMin?: number | string | AxisLimitConfig; - axisMax?: number | string | AxisLimitConfig; + axisMin?: number; + axisMax?: number; axisTickLabelFont: Font; axisTickLabelColor: string; barSettings: ChartBarSettings; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bars-chart.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bars-chart.models.ts index 860f4da5b9..2808d9f7e5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bars-chart.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bars-chart.models.ts @@ -24,12 +24,11 @@ import { chartColorScheme } from '@home/components/widget/lib/chart/chart.models'; import { Font } from '@shared/models/widget-settings.models'; -import { AxisLimitConfig } from '@home/components/widget/lib/chart/time-series-chart.models'; export interface BarsChartSettings extends LatestChartSettings { polar: boolean; - axisMin?: number | string | AxisLimitConfig; - axisMax?: number | string | AxisLimitConfig; + axisMin?: number | string; + axisMax?: number | string; axisTickLabelFont: Font; axisTickLabelColor: string; angleAxisStartAngle?: number; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bars-chart.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bars-chart.ts index 61adae9c20..92df6dd363 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bars-chart.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bars-chart.ts @@ -25,7 +25,7 @@ import { ComponentStyle } from '@shared/models/widget-settings.models'; import { LinearGradientObject } from 'zrender/lib/graphic/LinearGradient'; import tinycolor from 'tinycolor2'; import { BarDataItemOption, BarSeriesLabelOption } from 'echarts/types/src/chart/bar/BarSeries'; -import { isDefinedAndNotNull, isNumber, isString } from '@core/utils'; +import { isDefinedAndNotNull } from '@core/utils'; import { ChartFillType, ChartLabelPosition, @@ -35,19 +35,9 @@ import { } from '@home/components/widget/lib/chart/chart.models'; import { ValueAxisBaseOption } from 'echarts/types/src/coord/axisCommonTypes'; import { RadiusAxisOption, YAXisOption } from 'echarts/types/dist/shared'; -import { DataKey, Datasource, DatasourceType, widgetType } from '@shared/models/widget.models'; -import { WidgetSubscriptionOptions } from '@core/api/widget-api.models'; -import { ValueSourceType } from '@shared/models/widget-settings.models'; -import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; -import { AxisLimitConfig } from '@home/components/widget/lib/chart/time-series-chart.models'; export class TbBarsChart extends TbLatestChart { - private dynamicAxisMin: number | string = null; - private dynamicAxisMax: number | string = null; - private minLatestDataKey: DataKey = null; - private maxLatestDataKey: DataKey = null; - constructor(ctx: WidgetContext, inputSettings: DeepPartial, chartElement: HTMLElement, @@ -62,212 +52,6 @@ export class TbBarsChart extends TbLatestChart { return barsChartDefaultSettings; } - protected initSettings() { - super.initSettings(); - this.setupAxisLimits(); - } - - private setupAxisLimits(): void { - const axisLimitDatasources: Datasource[] = []; - - if (isDefinedAndNotNull(this.settings.axisMin)) { - this.processAxisLimit(this.settings.axisMin, 'min', axisLimitDatasources); - } - if (isDefinedAndNotNull(this.settings.axisMax)) { - this.processAxisLimit(this.settings.axisMax, 'max', axisLimitDatasources); - } - - this.subscribeForAxisLimits(axisLimitDatasources); - } - - private processAxisLimit( - limit: any, - limitType: 'min' | 'max', - axisLimitDatasources: Datasource[] - ): void { - if (limit && typeof limit === 'object' && 'type' in limit) { - const axisLimit = limit as AxisLimitConfig; - - if (axisLimit.type === ValueSourceType.latestKey) { - let latestDataKey: DataKey = null; - if (this.ctx.datasources.length) { - for (const datasource of this.ctx.datasources) { - latestDataKey = datasource.latestDataKeys?.find(d => - (d.type === DataKeyType.function && d.label === (axisLimit.value as DataKey).label) || - (d.type !== DataKeyType.function && d.name === (axisLimit.value as DataKey).name && - d.type === (axisLimit.value as DataKey).type)); - if (latestDataKey) { - break; - } - } - } - - if (latestDataKey) { - if (limitType === 'min') { - this.minLatestDataKey = latestDataKey; - } else { - this.maxLatestDataKey = latestDataKey; - } - } - } else if (axisLimit.type === ValueSourceType.entity) { - const entityAliasId = this.ctx.aliasController.getEntityAliasId(axisLimit.entityAlias); - if (entityAliasId) { - let datasource = axisLimitDatasources.find(d => d.entityAliasId === entityAliasId); - const entityDataKey: DataKey = { - type: (axisLimit.value as DataKey).type, - name: (axisLimit.value as DataKey).name, - label: (axisLimit.value as DataKey).name, - settings: { - axisLimit: limitType - } - }; - - if (datasource) { - datasource.dataKeys.push(entityDataKey); - } else { - datasource = { - type: DatasourceType.entity, - name: axisLimit.entityAlias, - aliasName: axisLimit.entityAlias, - entityAliasId, - dataKeys: [entityDataKey] - }; - axisLimitDatasources.push(datasource); - } - } - } else if (axisLimit.type === ValueSourceType.constant) { - const value = axisLimit.value as number; - if (limitType === 'min') { - this.dynamicAxisMin = value; - } else { - this.dynamicAxisMax = value; - } - } - } else if (typeof limit === 'number' || typeof limit === 'string') { - if (limitType === 'min') { - this.dynamicAxisMin = limit; - } else { - this.dynamicAxisMax = limit; - } - } - } - - private subscribeForAxisLimits(datasources: Datasource[]) { - if (datasources.length) { - const axisLimitsSubscriptionOptions: WidgetSubscriptionOptions = { - datasources, - useDashboardTimewindow: false, - type: widgetType.latest, - callbacks: { - onDataUpdated: (subscription) => { - let update = false; - if (subscription.data) { - for (const data of subscription.data) { - const limitType = data.dataKey.settings.axisLimit as ('min' | 'max'); - if (data.data[0]) { - const value = this.parseAxisLimitData(data.data[0][1]); - if (limitType === 'min') { - if (this.dynamicAxisMin !== value) { - this.dynamicAxisMin = value; - update = true; - } - } else { - if (this.dynamicAxisMax !== value) { - this.dynamicAxisMax = value; - update = true; - } - } - } - } - } - if (this.latestChart && update) { - this.updateAxisLimits(); - } - } - } - }; - this.ctx.subscriptionApi.createSubscription(axisLimitsSubscriptionOptions, true).subscribe(); - } - } - - private parseAxisLimitData(data: any): number | null { - let value: number; - if (isDefinedAndNotNull(data)) { - if (isNumber(data)) { - value = data; - } else if (isString(data)) { - value = Number(data); - } - } - if (isDefinedAndNotNull(value) && !isNaN(value)) { - return value; - } - return null; - } - - private updateAxisLimitsFromLatest(): boolean { - let update = false; - - if (this.ctx.latestData) { - if (this.minLatestDataKey) { - const data = this.ctx.latestData.find(d => d.dataKey === this.minLatestDataKey); - if (data?.data[0]) { - const value = this.parseAxisLimitData(data.data[0][1]); - if (this.dynamicAxisMin !== value) { - this.dynamicAxisMin = value; - update = true; - } - } - } - - if (this.maxLatestDataKey) { - const data = this.ctx.latestData.find(d => d.dataKey === this.maxLatestDataKey); - if (data?.data[0]) { - const value = this.parseAxisLimitData(data.data[0][1]); - if (this.dynamicAxisMax !== value) { - this.dynamicAxisMax = value; - update = true; - } - } - } - } - return update; - } - - private updateAxisLimits(): void { - if (this.latestChart && !this.latestChart.isDisposed()) { - const axisTickLabelStyle = createChartTextStyle(this.settings.axisTickLabelFont, - this.settings.axisTickLabelColor, false, 'axis.tickLabel'); - const valueAxis: ValueAxisBaseOption = { - type: 'value', - min: this.dynamicAxisMin, - max: this.dynamicAxisMax, - axisLabel: { - color: axisTickLabelStyle.color, - fontStyle: axisTickLabelStyle.fontStyle, - fontWeight: axisTickLabelStyle.fontWeight, - fontFamily: axisTickLabelStyle.fontFamily, - fontSize: axisTickLabelStyle.fontSize, - formatter: (value: any) => this.valueFormatter.format(value) - } - }; - - if (this.settings.polar) { - this.latestChartOption.radiusAxis = valueAxis as RadiusAxisOption; - } else { - this.latestChartOption.yAxis = valueAxis as YAXisOption; - } - - this.latestChart.setOption(this.latestChartOption); - } - } - - public latestUpdated() { - if (this.updateAxisLimitsFromLatest()) { - this.updateAxisLimits(); - } - } - protected prepareLatestChartOption() { let labelStyle: ComponentStyle = {}; if (this.settings.barSettings.showLabel) { @@ -305,12 +89,10 @@ export class TbBarsChart extends TbLatestChart { const axisTickLabelStyle = createChartTextStyle(this.settings.axisTickLabelFont, this.settings.axisTickLabelColor, false, 'axis.tickLabel'); - const minValue = isDefinedAndNotNull(this.dynamicAxisMin) ? this.dynamicAxisMin : undefined; - const maxValue = isDefinedAndNotNull(this.dynamicAxisMax) ? this.dynamicAxisMax : undefined; const valueAxis: ValueAxisBaseOption = { type: 'value', - min: minValue, - max: maxValue, + min: this.settings.axisMin, + max: this.settings.axisMax, axisLabel: { color: axisTickLabelStyle.color, fontStyle: axisTickLabelStyle.fontStyle, @@ -320,7 +102,6 @@ export class TbBarsChart extends TbLatestChart { formatter: (value: any) => this.valueFormatter.format(value) } }; - if (this.settings.polar) { this.latestChartOption.polar = { radius: '100%' diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.ts index 9e525cd794..5911a60215 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.ts @@ -142,6 +142,7 @@ export abstract class TbLatestChart { public update(): void { for (const dsData of this.ctx.data) { + console.log("this.ctx.data",this.ctx.data) let value = 0; const tsValue = dsData.data[0]; const dataItem = this.dataItems.find(item => item.dataKey === dsData.dataKey); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts index e5987e9664..5585496ede 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts @@ -381,18 +381,12 @@ export interface TimeSeriesChartYAxisSettings extends TimeSeriesChartAxisSetting decimals?: number; interval?: number; splitNumber?: number; - min?: number | string | AxisLimitConfig; - max?: number | string | AxisLimitConfig; + min?: string | ValueSourceConfig; + max?: string | ValueSourceConfig; ticksGenerator?: TimeSeriesChartTicksGenerator | string; ticksFormatter?: TimeSeriesChartTicksFormatter | string; } -export interface AxisLimitConfig { - entityAlias: string; - type: ValueSourceType; - value: number | DataKey; -} - export const timeSeriesChartYAxisValid = (axis: TimeSeriesChartYAxisSettings): boolean => !(!axis.id || isUndefinedOrNull(axis.order)); @@ -873,8 +867,6 @@ export interface TimeSeriesChartAxis { id: string; settings: TimeSeriesChartAxisSettings; option: CartesianAxisOption; - dynamicMin?: string| number; - dynamicMax?: string| number; minLatestDataKey?: DataKey; maxLatestDataKey?: DataKey; unitConvertor?: (value: number) => number; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts index 7954352bb4..d429438f43 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts @@ -17,7 +17,6 @@ import { WidgetContext } from '@home/models/widget-component.models'; import { adjustTimeAxisExtentToData, - AxisLimitConfig, calculateThresholdsOffset, createTimeSeriesVisualMapOption, createTimeSeriesXAxis, @@ -55,7 +54,7 @@ import { getFocusedSeriesIndex, measureAxisNameSize } from '@home/components/widget/lib/chart/echarts-widget.models'; -import { DateFormatProcessor, ValueSourceType } from '@shared/models/widget-settings.models'; +import { DateFormatProcessor, ValueSourceConfig, ValueSourceType } from '@shared/models/widget-settings.models'; import { formattedDataFormDatasourceData, formatValue, @@ -289,15 +288,36 @@ export class TbTimeSeriesChart { } } } - } - if (this.updateAxisLimitsFromLatest()) { - update = true; + + for (const yAxis of this.yAxisList) { + const minType = (yAxis.settings.min as ValueSourceConfig).type; + const maxType = (yAxis.settings.max as ValueSourceConfig).type; + if (minType === ValueSourceType.latestKey && yAxis.minLatestDataKey) { + const data = this.ctx.latestData.find(d => d.dataKey === yAxis.minLatestDataKey); + if (data?.data[0]) { + const value = this.parseAxisLimitData(data.data[0][1], yAxis.unitConvertor); + if (yAxis.option.min !== value) { + yAxis.option.min = value; + update = true; + } + } + } + + if (maxType === ValueSourceType.latestKey && yAxis.maxLatestDataKey) { + const data = this.ctx.latestData.find(d => d.dataKey === yAxis.maxLatestDataKey); + if (data?.data[0]) { + const value = this.parseAxisLimitData(data.data[0][1], yAxis.unitConvertor); + if (yAxis.option.max !== value) { + yAxis.option.max = value; + update = true; + } + } + } + } } if (this.timeSeriesChart && update) { this.updateSeriesData(); - if (this.updateAxisLimitsFromLatest()) { - this.updateAxisLimits(); - } + this.updateAxisLimits(); } } @@ -574,20 +594,17 @@ export class TbTimeSeriesChart { } const yAxis = createTimeSeriesYAxis(unitSymbol, decimals, axisSettings, this.ctx.utilsService, this.darkMode, unitConvertor); if (isDefinedAndNotNull(axisSettings.min)) { - this.processAxisLimit(axisSettings.min, 'min', yAxis, axisLimitDatasources, unitConvertor); + this.processYAxisLimit(axisSettings.min, 'min', yAxis, axisLimitDatasources, unitConvertor); } if (isDefinedAndNotNull(axisSettings.max)) { - this.processAxisLimit(axisSettings.max, 'max', yAxis, axisLimitDatasources, unitConvertor); - } - if (yAxis.minLatestDataKey || yAxis.maxLatestDataKey) { - yAxis.unitConvertor = unitConvertor; + this.processYAxisLimit(axisSettings.max, 'max', yAxis, axisLimitDatasources, unitConvertor); } this.yAxisList.push(yAxis); } this.subscribeForAxisLimits(axisLimitDatasources); } - private processAxisLimit( + private processYAxisLimit( limit: any, limitType: 'min' | 'max', yAxis: TimeSeriesChartYAxis, @@ -595,15 +612,15 @@ export class TbTimeSeriesChart { unitConvertor?: (value: number) => number ): void { if (limit && typeof limit === 'object' && 'type' in limit) { - const axisLimit = limit as AxisLimitConfig; + const axisLimit = limit as ValueSourceConfig; if (axisLimit.type === ValueSourceType.latestKey) { let latestDataKey: DataKey = null; if (this.ctx.datasources.length) { for (const datasource of this.ctx.datasources) { latestDataKey = datasource.latestDataKeys?.find(d => - (d.type === DataKeyType.function && d.label === (axisLimit.value as DataKey).label) || - (d.type !== DataKeyType.function && d.name === (axisLimit.value as DataKey).name && - d.type === (axisLimit.value as DataKey).type)); + (d.type === DataKeyType.function && d.label === axisLimit.latestKey) || + (d.type !== DataKeyType.function && d.name === axisLimit.latestKey && + d.type === axisLimit.latestKeyType)); if (latestDataKey) { break; } @@ -621,9 +638,9 @@ export class TbTimeSeriesChart { if (entityAliasId) { let datasource = axisLimitDatasources.find(d => d.entityAliasId === entityAliasId); const entityDataKey: DataKey = { - type: (axisLimit.value as DataKey).type, - name: (axisLimit.value as DataKey).name, - label: (axisLimit.value as DataKey).name, + type: limit.entityKeyType, + name: limit.entityKey, + label: limit.entityKey, settings: { yAxisId: yAxis.id, axisLimit: limitType @@ -643,27 +660,14 @@ export class TbTimeSeriesChart { } } } else if (axisLimit.type === ValueSourceType.constant) { - const value = unitConvertor ? unitConvertor(axisLimit.value as number) : axisLimit.value as number; + const value = unitConvertor ? unitConvertor(axisLimit.value) : axisLimit.value; if (limitType === 'min') { - yAxis.dynamicMin = value; yAxis.option.min = value; } else { - yAxis.dynamicMax = value; yAxis.option.max = value; } } - } else if (typeof limit === 'number' || typeof limit === 'string') { - let value = limit; - if (typeof value === 'number' && unitConvertor) { - value = unitConvertor(value); - } - if (limitType === 'min') { - yAxis.dynamicMin = value; - yAxis.option.min = value; - } else { - yAxis.dynamicMax = value; - yAxis.option.max = value; - } + return; } } @@ -738,25 +742,30 @@ export class TbTimeSeriesChart { const limitType = data.dataKey.settings.axisLimit as ('min' | 'max'); if (data.data[0]) { const value = this.parseAxisLimitData(data.data[0][1], yAxis.unitConvertor); + if (isDefinedAndNotNull(value)) { if (limitType === 'min') { - yAxis.dynamicMin = value; - yAxis.option.min = value; + if (yAxis.option.min !== value) { + yAxis.option.min = value; + update = true; + } } else { - yAxis.dynamicMax = value; - yAxis.option.max = value; + if (yAxis.option.max !== value) { + yAxis.option.max = value; + update = true; + } } - update = true; } } } } } - if (this.timeSeriesChart && update) { - this.updateAxisLimits(); - } + } + if (this.timeSeriesChart && update) { + this.updateAxisLimits(); } } - }; + } + }; this.ctx.subscriptionApi.createSubscription(axisLimitsSubscriptionOptions, true).subscribe(); } } @@ -853,40 +862,6 @@ export class TbTimeSeriesChart { } } - private updateAxisLimitsFromLatest(): boolean { - let update = false; - - if (this.ctx.latestData) { - for (const yAxis of this.yAxisList) { - if (yAxis.minLatestDataKey) { - const data = this.ctx.latestData.find(d => d.dataKey === yAxis.minLatestDataKey); - if (data?.data[0]) { - const value = this.parseAxisLimitData(data.data[0][1], yAxis.unitConvertor); - - if (yAxis.dynamicMin !== value) { - yAxis.dynamicMin = value; - yAxis.option.min = value; - update = true; - } - } - } - - if (yAxis.maxLatestDataKey) { - const data = this.ctx.latestData.find(d => d.dataKey === yAxis.maxLatestDataKey); - if (data?.data[0]) { - const value = this.parseAxisLimitData(data.data[0][1], yAxis.unitConvertor); - if (yAxis.dynamicMax !== value) { - yAxis.dynamicMax = value; - yAxis.option.max = value; - update = true; - } - } - } - } - } - return update; - } - private parseAxisLimitData = (data: any, unitConvertor?: (value: number) => number): number => { let value: number; if (isDefinedAndNotNull(data)) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-widget-settings.component.ts index b1734ff45a..8826e9afda 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-widget-settings.component.ts @@ -26,8 +26,6 @@ import { import { LatestChartWidgetSettingsComponent } from '@home/components/widget/lib/settings/chart/latest-chart-widget-settings.component'; -import { AxisLimitConfig } from '@home/components/widget/lib/chart/time-series-chart.models'; -import { ValueSourceType } from '@shared/models/widget-settings.models'; @Component({ selector: 'tb-bar-chart-widget-settings', @@ -59,32 +57,4 @@ export class BarChartWidgetSettingsComponent extends LatestChartWidgetSettingsCo latestChartWidgetSettingsForm.addControl('axisTickLabelFont', this.fb.control(settings.axisTickLabelFont, [])); latestChartWidgetSettingsForm.addControl('axisTickLabelColor', this.fb.control(settings.axisTickLabelColor, [])); } - - protected prepareInputSettings(settings: WidgetSettings): WidgetSettings { - settings.axisMin = this.normalizeAxisLimit(settings.axisMin); - settings.axisMax = this.normalizeAxisLimit(settings.axisMax); - return super.prepareInputSettings(settings); - } - - private normalizeAxisLimit(limit: any): AxisLimitConfig { - if (limit && typeof limit === 'object' && 'type' in limit) { - return { - type: limit.type || ValueSourceType.constant, - value: limit.value ?? null, - entityAlias: limit.entityAlias ?? null - }; - } - if (typeof limit === 'number') { - return { - type: ValueSourceType.constant, - value: limit, - entityAlias: null - }; - } - return { - type: ValueSourceType.constant, - value: null, - entityAlias: null - }; - } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.html index 6bbb996e93..3f1e96ca84 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.html @@ -120,7 +120,7 @@
+ formControlName="animation">
widget-config.card-appearance
@@ -256,15 +256,25 @@
widgets.bar-chart.bar-appearance
+ formControlName="barSettings" + [series]="false">
widgets.bar-chart.bar-axis
-
widgets.chart.chart-axis.scale-appearance
+
widgets.chart.chart-axis.scale
+
widgets.chart.chart-axis.scale-min
+ + + +
widgets.chart.chart-axis.scale-max
+ + +
-
-
widgets.chart.chart-axis.scale-limits
-
-
-
widgets.chart.chart-axis.limit
-
widgets.chart.chart-axis.source
-
widgets.chart.chart-axis.key-value
-
-
-
- - - - - -
-
-
-
@@ -322,16 +303,26 @@
widgets.bar-chart.bar-appearance
+ formControlName="barSettings" + pieLabelPosition + [series]="false">
widgets.polar-area-chart.polar-axis
-
widgets.chart.chart-axis.scale-appearance
+
widgets.chart.chart-axis.scale
+
widgets.chart.chart-axis.scale-min
+ + + +
widgets.chart.chart-axis.scale-max
+ + +
-
-
widgets.chart.chart-axis.scale-limits
-
-
-
widgets.chart.chart-axis.limit
-
widgets.chart.chart-axis.source
-
widgets.chart.chart-axis.key-value
-
-
- - - - - -
-
-
@@ -470,7 +434,7 @@ + formControlName="fillAreaSettings">
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.ts index fadee36370..ff7641bf1a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.ts @@ -22,7 +22,6 @@ import { LatestChartWidgetSettings } from '@home/components/widget/lib/chart/latest-chart.models'; import { - Datasource, legendPositions, legendPositionTranslationMap, WidgetSettings, @@ -104,15 +103,6 @@ export abstract class LatestChartWidgetSettingsComponent, protected fb: UntypedFormBuilder) { super(store); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/polar-area-chart-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/polar-area-chart-widget-settings.component.ts index 61c533a377..355dc55207 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/polar-area-chart-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/polar-area-chart-widget-settings.component.ts @@ -26,8 +26,7 @@ import { import { LatestChartWidgetSettingsComponent } from '@home/components/widget/lib/settings/chart/latest-chart-widget-settings.component'; -import { AxisLimitConfig } from '@home/components/widget/lib/chart/time-series-chart.models'; -import { ValueSourceType } from '@shared/models/widget-settings.models'; +import { ValueSourceConfig, ValueSourceType } from '@shared/models/widget-settings.models'; @Component({ selector: 'tb-polar-area-chart-widget-settings', @@ -66,25 +65,20 @@ export class PolarAreaChartWidgetSettingsComponent extends LatestChartWidgetSett return super.prepareInputSettings(settings); } - private normalizeAxisLimit(limit: any): AxisLimitConfig { - if (limit && typeof limit === 'object' && 'type' in limit) { + private normalizeAxisLimit(limit: any): ValueSourceConfig { + if (!limit) { return { - type: limit.type || ValueSourceType.constant, - value: limit.value ?? null, - entityAlias: limit.entityAlias ?? null + type: ValueSourceType.constant, + value: null, + entityAlias: null }; - } - if (typeof limit === 'number') { + } else if (typeof limit === 'number') { return { type: ValueSourceType.constant, value: limit, entityAlias: null }; } - return { - type: ValueSourceType.constant, - value: null, - entityAlias: null - }; + return limit; } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.html index eed985187d..37c2838a21 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.html @@ -53,7 +53,7 @@ [dataKeyTypes]="[DataKeyType.attribute, DataKeyType.timeseries]" [callbacks]="callbacks" [editable]="false" - formControlName="value"> + [formControl]="latestKeyFormControl"> + [formControl]="entityKeyFormControl">
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.ts index b8103dd008..165857eac3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.ts @@ -18,18 +18,22 @@ import { Component, DestroyRef, forwardRef, Input, OnInit } from '@angular/core' import { ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, - UntypedFormBuilder, + UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, ValidationErrors, Validator, Validators, } from '@angular/forms'; -import { ValueSourceType, ValueSourceTypes, ValueSourceTypeTranslation } from '@shared/models/widget-settings.models'; +import { + ValueSourceConfig, + ValueSourceType, + ValueSourceTypes, + ValueSourceTypeTranslation +} from '@shared/models/widget-settings.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { AxisLimitConfig } from '@home/components/widget/lib/chart/time-series-chart.models'; -import { Datasource, DatasourceType, } from '@shared/models/widget.models'; +import { DataKey, Datasource, DatasourceType, } from '@shared/models/widget.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { IAliasController } from '@core/api/widget-api.models'; import { DataKeysCallbacks } from '@home/components/widget/lib/settings/common/key/data-keys.component.models'; -import { isEqual } from '@core/utils'; +import { merge } from 'rxjs'; @Component({ selector: 'tb-axis-scale-row', @@ -77,9 +81,13 @@ export class AxisScaleRowComponent implements ControlValueAccessor, OnInit, Vali limitForm: UntypedFormGroup; + latestKeyFormControl: UntypedFormControl; + + entityKeyFormControl: UntypedFormControl; + private propagateChanges: (value: any) => void = () => {}; - private modelValue: AxisLimitConfig | null = null; + private modelValue: ValueSourceConfig | null = null; constructor(private fb: UntypedFormBuilder, private destroyRef: DestroyRef) { @@ -91,42 +99,44 @@ export class AxisScaleRowComponent implements ControlValueAccessor, OnInit, Vali value: [null], entityAlias: [null] }); + this.latestKeyFormControl = this.fb.control(null, [Validators.required]); + this.entityKeyFormControl = this.fb.control(null, [Validators.required]); this.subscribeToTypeChanges(); - - this.limitForm.valueChanges - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe(value => { + merge( + this.latestKeyFormControl.valueChanges, + this.entityKeyFormControl.valueChanges, + this.limitForm.valueChanges + ).pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => { this.updateValidators(); - this.updateView(value); + this.updateModel(); }); } - writeValue(value: AxisLimitConfig) { + writeValue(value: ValueSourceConfig) { + console.log("value", value); this.modelValue = value; - - if (!this.limitForm) { - return; - } - - if (value) { - this.limitForm.patchValue({ - type: value.type ?? ValueSourceType.constant, - value: value.value ?? null, - entityAlias: value.entityAlias ?? null - }, - { emitEvent: false } - ); - } else { - this.limitForm.patchValue({ - type: ValueSourceType.constant, - value: null, - entityAlias: null - }, - { emitEvent: false } - ); + this.limitForm.patchValue( + { + type: value.type || ValueSourceType.constant, + value: value.value, + entityAlias: value.entityAlias, + }, {emitEvent: false} + ); + if (value.type === ValueSourceType.latestKey) { + this.latestKeyFormControl.patchValue({ + type: value.latestKeyType, + name: value.latestKey + }, {emitEvent: false}); + } else if (value.type === ValueSourceType.entity) { + this.entityKeyFormControl.patchValue({ + type: value.entityKeyType, + name: value.entityKey + }, {emitEvent: false}); } this.updateValidators(); + this.limitForm.markAllAsTouched(); } registerOnChange(fn: any) { @@ -158,28 +168,22 @@ export class AxisScaleRowComponent implements ControlValueAccessor, OnInit, Vali private updateValidators() { const axisTypeControl = this.limitForm.get('type'); - const axisValueControl = this.limitForm.get('value'); - const axisEntityAliasControl = this.limitForm.get('entityAlias'); - - if (axisTypeControl && axisValueControl && axisEntityAliasControl) { + if (axisTypeControl && this.entityKeyFormControl && this.latestKeyFormControl) { const type = axisTypeControl.value; - if (type === ValueSourceType.latestKey || type === ValueSourceType.entity) { - axisValueControl.setValidators([Validators.required]); - } else { - axisValueControl.clearValidators(); - } - - if (type === ValueSourceType.entity) { - axisEntityAliasControl.setValidators([Validators.required]); + if (type === ValueSourceType.latestKey) { + this.latestKeyFormControl.setValidators([Validators.required]); + this.entityKeyFormControl.clearValidators(); + } else if (type === ValueSourceType.entity) { + this.latestKeyFormControl.clearValidators(); + this.entityKeyFormControl.setValidators([Validators.required]); } else { - axisEntityAliasControl.clearValidators(); + this.latestKeyFormControl.clearValidators(); + this.entityKeyFormControl.clearValidators(); } - - axisValueControl.updateValueAndValidity({ emitEvent: false }); - axisEntityAliasControl.updateValueAndValidity({ emitEvent: false }); + this.latestKeyFormControl.updateValueAndValidity({ emitEvent: false }); + this.entityKeyFormControl.updateValueAndValidity({ emitEvent: false }); } } - private subscribeToTypeChanges() { this.limitForm.controls.type.valueChanges .pipe(takeUntilDestroyed(this.destroyRef)) @@ -187,18 +191,31 @@ export class AxisScaleRowComponent implements ControlValueAccessor, OnInit, Vali const axisValueControl = this.limitForm.get('value'); const axisEntityAliasControl = this.limitForm.get('entityAlias'); if (axisValueControl) { - axisValueControl.setValue(null, { emitEvent: true }); + axisValueControl.setValue(null, { emitEvent: false }); } if (axisEntityAliasControl) { - axisEntityAliasControl.setValue(null, { emitEvent: true }); + axisEntityAliasControl.setValue(null, { emitEvent: false }); } + this.latestKeyFormControl.setValue(null, { emitEvent: false }); + this.entityKeyFormControl.setValue(null, { emitEvent: false }); + }); } - private updateView(value: AxisLimitConfig) { - if (!isEqual(this.modelValue, value)) { - this.modelValue = value; - this.propagateChanges(value); + private updateModel() { + const value = this.limitForm.value; + this.modelValue.type = value.type ?? ValueSourceType.constant; + this.modelValue.value = value?.value; + this.modelValue.entityAlias = value?.entityAlias; + if (value.type === ValueSourceType.latestKey) { + const latestKey: DataKey = this.latestKeyFormControl.value; + this.modelValue.latestKey = latestKey?.name; + this.modelValue.latestKeyType = (latestKey?.type as any); + } else if (value.type === ValueSourceType.entity) { + const entityKey: DataKey = this.entityKeyFormControl.value; + this.modelValue.entityKey = entityKey?.name; + this.modelValue.entityKeyType = (entityKey?.type as any); } + this.propagateChanges(this.modelValue); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts index 4af0f5f613..3159a27abd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts @@ -38,7 +38,8 @@ import { import { defaultTimeSeriesChartYAxisSettings, getNextTimeSeriesYAxisId, - TimeSeriesChartYAxes, TimeSeriesChartYAxisId, + TimeSeriesChartYAxes, + TimeSeriesChartYAxisId, TimeSeriesChartYAxisSettings, timeSeriesChartYAxisValid, timeSeriesChartYAxisValidator @@ -49,7 +50,7 @@ import { coerceBoolean } from '@shared/decorators/coercion'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { IAliasController } from '@app/core/public-api'; import { DataKeysCallbacks } from '@home/components/widget/lib/settings/common/key/data-keys.component.models'; -import { Datasource } from '@app/shared/public-api'; +import { DataKey, DataKeyType, Datasource, ValueSourceConfig, ValueSourceType } from '@app/shared/public-api'; @Component({ selector: 'tb-time-series-chart-y-axes-panel', @@ -125,6 +126,7 @@ export class TimeSeriesChartYAxesPanelComponent implements ControlValueAccessor, for (const axis of axes) { yAxes[axis.id] = axis; } + this.updateLatestDataKeys(Object.values(yAxes)); this.propagateChange(yAxes); } ); @@ -147,7 +149,7 @@ export class TimeSeriesChartYAxesPanelComponent implements ControlValueAccessor, } writeValue(value: TimeSeriesChartYAxes | undefined): void { - const yAxes: TimeSeriesChartYAxes = value || {}; + const yAxes: TimeSeriesChartYAxes = this.checkLatestDataKeys(value || {}); if (!yAxes.default) { yAxes.default = mergeDeep({} as TimeSeriesChartYAxisSettings, defaultTimeSeriesChartYAxisSettings, {id: 'default', order: 0} as TimeSeriesChartYAxisSettings); @@ -202,8 +204,104 @@ export class TimeSeriesChartYAxesPanelComponent implements ControlValueAccessor, private prepareAxesFormArray(axes: TimeSeriesChartYAxisSettings[]): UntypedFormArray { const axesControls: Array = []; axes.forEach((axis) => { + axis.min = this.normalizeAxisLimit(axis.min); + axis.max = this.normalizeAxisLimit(axis.max); axesControls.push(this.fb.control(axis, [timeSeriesChartYAxisValidator])); }); return this.fb.array(axesControls); } + + private checkLatestDataKeys(yAxes: TimeSeriesChartYAxes): TimeSeriesChartYAxes { + const latestKeys = this.datasource?.latestDataKeys || []; + const result: TimeSeriesChartYAxes = {}; + + for (const [id, axis] of Object.entries(yAxes)) { + + const minCfg = axis.min as unknown as ValueSourceConfig; + const maxCfg = axis.max as unknown as ValueSourceConfig; + + const minValid = + minCfg?.type !== ValueSourceType.latestKey || + latestKeys.some(k => this.isYAxisKey(k, minCfg)); + + const maxValid = + maxCfg?.type !== ValueSourceType.latestKey || + latestKeys.some(k => this.isYAxisKey(k, maxCfg)); + + if (minValid && maxValid) { + result[id] = axis; + } + } + + return result; + } + + private updateLatestDataKeys(yAxes: TimeSeriesChartYAxisSettings[]) { + if (this.datasource) { + let latestKeys = this.datasource.latestDataKeys; + if (!latestKeys) { + latestKeys = []; + this.datasource.latestDataKeys = latestKeys; + } + const existingYAxisKeys = latestKeys.filter(k => k.settings?.__yAxisMinKey === true || k.settings?.__yAxisMaxKey === true); + const foundYAxisKeys: DataKey[] = []; + + for(const yAxis of yAxes) { + const min = yAxis.min as ValueSourceConfig; + const max = yAxis.max as ValueSourceConfig; + if (min.type === ValueSourceType.latestKey) { + const found = existingYAxisKeys.find(k => this.isYAxisKey(k, min)); + if (!found) { + const newKey = this.dataKeyCallbacks.generateDataKey(min.latestKey, min.latestKeyType, + null, true, null); + newKey.settings.__yAxisMinKey = true; + latestKeys.push(newKey); + } else if (foundYAxisKeys.indexOf(found) === -1) { + foundYAxisKeys.push(found); + } + } + if (max.type === ValueSourceType.latestKey) { + const found = existingYAxisKeys.find(k => this.isYAxisKey(k, max)); + if (!found) { + const newKey = this.dataKeyCallbacks.generateDataKey(max.latestKey, max.latestKeyType, + null, true, null); + newKey.settings.__yAxisMaxKey = true; + latestKeys.push(newKey); + } else if (foundYAxisKeys.indexOf(found) === -1) { + foundYAxisKeys.push(found); + } + } + } + const toRemove = existingYAxisKeys.filter(k => foundYAxisKeys.indexOf(k) === -1); + for (const key of toRemove) { + const index = latestKeys.indexOf(key); + if (index > -1) { + latestKeys.splice(index, 1); + } + } + } + } + + private isYAxisKey(d: DataKey, limit: ValueSourceConfig): boolean { + return (d.type === DataKeyType.function && d.label === limit.latestKey) || + (d.type !== DataKeyType.function && d.name === limit.latestKey && + d.type === limit.latestKeyType); + } + + private normalizeAxisLimit(limit: any): ValueSourceConfig { + if (!limit) { + return { + type: ValueSourceType.constant, + value: null, + entityAlias: null + }; + } else if (typeof limit === 'number') { + return { + type: ValueSourceType.constant, + value: limit, + entityAlias: null + }; + } + return limit; + } } From d898674268675ef8fbd85cadae69a69ff9a6bb14 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Wed, 3 Dec 2025 17:51:25 +0200 Subject: [PATCH 06/35] clean up --- .../chart/bar-chart-basic-config.component.ts | 2 +- .../latest-chart-basic-config.component.html | 54 +++++++++---------- ...polar-area-chart-basic-config.component.ts | 2 +- .../widget/lib/chart/latest-chart.ts | 1 - .../lib/chart/time-series-chart.models.ts | 4 +- ...atest-chart-widget-settings.component.html | 14 ++--- ...ar-area-chart-widget-settings.component.ts | 27 +--------- 7 files changed, 40 insertions(+), 64 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-basic-config.component.ts index 4c7da10bc6..b7d6b4f640 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-basic-config.component.ts @@ -41,7 +41,7 @@ export class BarChartBasicConfigComponent extends LatestChartBasicConfigComponen barChartConfigTemplate: TemplateRef; predefinedValues = widgetTitleAutocompleteValues; - + constructor(protected store: Store, protected widgetConfigComponent: WidgetConfigComponent, protected fb: UntypedFormBuilder) { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.html index d72a8d6083..c563820054 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.html @@ -21,27 +21,27 @@ formControlName="timewindowConfig"> + [configMode]="basicMode" + hideDatasourceLabel + hideDataKeys + forceSingleDatasource + formControlName="datasources"> + panelTitle="{{ 'widgets.chart.series' | translate }}" + addKeyTitle="{{ 'widgets.chart.add-series' | translate }}" + keySettingsTitle="{{ 'widgets.chart.series-settings' | translate }}" + removeKeyTitle="{{ 'widgets.chart.remove-series' | translate }}" + noKeysText="{{ 'widgets.chart.no-series' | translate }}" + requiredKeysText="{{ 'widgets.chart.no-series-error' | translate }}" + hideUnits + hideDecimals + hideDataKeyUnits + hideDataKeyDecimals + [datasourceType]="datasource?.type" + [deviceId]="datasource?.deviceId" + [entityAliasId]="datasource?.entityAliasId" + formControlName="series">
widget-config.appearance
@@ -210,7 +210,7 @@
+ formControlName="animation">
widget-config.card-appearance
@@ -239,7 +239,7 @@
+ formControlName="actions"> @@ -346,8 +346,8 @@
widgets.bar-chart.bar-appearance
+ formControlName="barSettings" + [series]="false">
@@ -385,9 +385,9 @@
widgets.bar-chart.bar-appearance
+ formControlName="barSettings" + pieLabelPosition + [series]="false">
@@ -508,7 +508,7 @@
+ formControlName="fillAreaSettings">
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/polar-area-chart-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/polar-area-chart-basic-config.component.ts index f60f8d2417..8c68ec2ff5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/polar-area-chart-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/polar-area-chart-basic-config.component.ts @@ -41,7 +41,7 @@ export class PolarAreaChartBasicConfigComponent extends LatestChartBasicConfigCo polarAreaChartConfigTemplate: TemplateRef; predefinedValues = widgetTitleAutocompleteValues; - + constructor(protected store: Store, protected widgetConfigComponent: WidgetConfigComponent, protected fb: UntypedFormBuilder) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.ts index 5911a60215..9e525cd794 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.ts @@ -142,7 +142,6 @@ export abstract class TbLatestChart { public update(): void { for (const dsData of this.ctx.data) { - console.log("this.ctx.data",this.ctx.data) let value = 0; const tsValue = dsData.data[0]; const dataItem = this.dataItems.find(item => item.dataKey === dsData.dataKey); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts index 5585496ede..0ce6a4b290 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts @@ -381,8 +381,8 @@ export interface TimeSeriesChartYAxisSettings extends TimeSeriesChartAxisSetting decimals?: number; interval?: number; splitNumber?: number; - min?: string | ValueSourceConfig; - max?: string | ValueSourceConfig; + min?: number | string | ValueSourceConfig; + max?: number | string | ValueSourceConfig; ticksGenerator?: TimeSeriesChartTicksGenerator | string; ticksFormatter?: TimeSeriesChartTicksFormatter | string; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.html index 3f1e96ca84..66d0234914 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.html @@ -120,7 +120,7 @@
+ formControlName="animation">
widget-config.card-appearance
@@ -256,8 +256,8 @@
widgets.bar-chart.bar-appearance
+ formControlName="barSettings" + [series]="false">
@@ -303,9 +303,9 @@
widgets.bar-chart.bar-appearance
+ formControlName="barSettings" + pieLabelPosition + [series]="false">
@@ -434,7 +434,7 @@
+ formControlName="fillAreaSettings">
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/polar-area-chart-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/polar-area-chart-widget-settings.component.ts index 355dc55207..07d43f4de2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/polar-area-chart-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/polar-area-chart-widget-settings.component.ts @@ -26,7 +26,6 @@ import { import { LatestChartWidgetSettingsComponent } from '@home/components/widget/lib/settings/chart/latest-chart-widget-settings.component'; -import { ValueSourceConfig, ValueSourceType } from '@shared/models/widget-settings.models'; @Component({ selector: 'tb-polar-area-chart-widget-settings', @@ -53,32 +52,10 @@ export class PolarAreaChartWidgetSettingsComponent extends LatestChartWidgetSett protected setupLatestChartControls(latestChartWidgetSettingsForm: UntypedFormGroup, settings: WidgetSettings) { latestChartWidgetSettingsForm.addControl('barSettings', this.fb.control(settings.barSettings, [])); - latestChartWidgetSettingsForm.addControl('axisMin', this.fb.control(settings.axisMin)); - latestChartWidgetSettingsForm.addControl('axisMax', this.fb.control(settings.axisMax)); + latestChartWidgetSettingsForm.addControl('axisMin', this.fb.control(settings.axisMin, [])); + latestChartWidgetSettingsForm.addControl('axisMax', this.fb.control(settings.axisMax, [])); latestChartWidgetSettingsForm.addControl('axisTickLabelFont', this.fb.control(settings.axisTickLabelFont, [])); latestChartWidgetSettingsForm.addControl('axisTickLabelColor', this.fb.control(settings.axisTickLabelColor, [Validators.min(0)])); latestChartWidgetSettingsForm.addControl('angleAxisStartAngle', this.fb.control(settings.angleAxisStartAngle, [])); } - protected prepareInputSettings(settings: WidgetSettings): WidgetSettings { - settings.axisMin = this.normalizeAxisLimit(settings.axisMin); - settings.axisMax = this.normalizeAxisLimit(settings.axisMax); - return super.prepareInputSettings(settings); - } - - private normalizeAxisLimit(limit: any): ValueSourceConfig { - if (!limit) { - return { - type: ValueSourceType.constant, - value: null, - entityAlias: null - }; - } else if (typeof limit === 'number') { - return { - type: ValueSourceType.constant, - value: limit, - entityAlias: null - }; - } - return limit; - } } From f60a0ab915a0bad34cef4d0999c449cb163916af Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Thu, 4 Dec 2025 10:30:42 +0200 Subject: [PATCH 07/35] improved validation --- .../common/axis-scale-row.component.ts | 35 +++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.ts index 165857eac3..9962bdeed9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.ts @@ -114,7 +114,6 @@ export class AxisScaleRowComponent implements ControlValueAccessor, OnInit, Vali } writeValue(value: ValueSourceConfig) { - console.log("value", value); this.modelValue = value; this.limitForm.patchValue( { @@ -147,14 +146,36 @@ export class AxisScaleRowComponent implements ControlValueAccessor, OnInit, Vali } validate(): ValidationErrors | null { - return this.limitForm.valid ? null : { - axisLimitForm: { - valid: false, - errors: this.getFormErrors() + const type = this.limitForm.get('type')?.value; + const errors: any = {}; + + if (this.limitForm.invalid) { + errors.form = this.getFormErrors(); + } + + if (type === ValueSourceType.latestKey) { + if (!this.latestKeyFormControl.value || this.latestKeyFormControl.invalid) { + errors.latestKey = { + valid: false + }; + } + } else if (type === ValueSourceType.entity) { + if (!this.limitForm.get('entityAlias')?.value) { + errors.entityAlias = { + valid: false + }; } - }; + if (!this.entityKeyFormControl.value || this.entityKeyFormControl.invalid) { + errors.entityKey = { + valid: false + }; + } + } + + return Object.keys(errors).length ? { axisLimitForm: errors } : null; } + private getFormErrors(): any { const errors: any = {}; Object.keys(this.limitForm.controls).forEach(key => { @@ -175,6 +196,7 @@ export class AxisScaleRowComponent implements ControlValueAccessor, OnInit, Vali this.entityKeyFormControl.clearValidators(); } else if (type === ValueSourceType.entity) { this.latestKeyFormControl.clearValidators(); + this.limitForm.get('entityAlias').setValidators([Validators.required]); this.entityKeyFormControl.setValidators([Validators.required]); } else { this.latestKeyFormControl.clearValidators(); @@ -184,6 +206,7 @@ export class AxisScaleRowComponent implements ControlValueAccessor, OnInit, Vali this.entityKeyFormControl.updateValueAndValidity({ emitEvent: false }); } } + private subscribeToTypeChanges() { this.limitForm.controls.type.valueChanges .pipe(takeUntilDestroyed(this.destroyRef)) From ce3b2579421e6dad5418f19c087e4e036ef7c03b Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Tue, 9 Dec 2025 12:05:11 +0200 Subject: [PATCH 08/35] clean up and propagation of limits values --- .../widget/lib/chart/time-series-chart.ts | 27 ++++---- .../common/axis-scale-row.component.scss | 15 ----- .../common/axis-scale-row.component.ts | 67 +++++-------------- ...me-series-chart-axis-settings.component.ts | 16 +---- ...ime-series-chart-y-axes-panel.component.ts | 31 +++++---- ...ime-series-chart-y-axis-row.component.html | 10 +-- .../time-series-chart-y-axis-row.component.ts | 24 ++++++- 7 files changed, 75 insertions(+), 115 deletions(-) delete mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.scss diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts index d429438f43..8568e4b13b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts @@ -594,10 +594,10 @@ export class TbTimeSeriesChart { } const yAxis = createTimeSeriesYAxis(unitSymbol, decimals, axisSettings, this.ctx.utilsService, this.darkMode, unitConvertor); if (isDefinedAndNotNull(axisSettings.min)) { - this.processYAxisLimit(axisSettings.min, 'min', yAxis, axisLimitDatasources, unitConvertor); + this.processYAxisLimit(axisSettings.min as ValueSourceConfig, 'min', yAxis, axisLimitDatasources, unitConvertor); } if (isDefinedAndNotNull(axisSettings.max)) { - this.processYAxisLimit(axisSettings.max, 'max', yAxis, axisLimitDatasources, unitConvertor); + this.processYAxisLimit(axisSettings.max as ValueSourceConfig, 'max', yAxis, axisLimitDatasources, unitConvertor); } this.yAxisList.push(yAxis); } @@ -605,22 +605,21 @@ export class TbTimeSeriesChart { } private processYAxisLimit( - limit: any, + limit: ValueSourceConfig, limitType: 'min' | 'max', yAxis: TimeSeriesChartYAxis, axisLimitDatasources: Datasource[], unitConvertor?: (value: number) => number ): void { if (limit && typeof limit === 'object' && 'type' in limit) { - const axisLimit = limit as ValueSourceConfig; - if (axisLimit.type === ValueSourceType.latestKey) { + if (limit.type === ValueSourceType.latestKey) { let latestDataKey: DataKey = null; if (this.ctx.datasources.length) { for (const datasource of this.ctx.datasources) { latestDataKey = datasource.latestDataKeys?.find(d => - (d.type === DataKeyType.function && d.label === axisLimit.latestKey) || - (d.type !== DataKeyType.function && d.name === axisLimit.latestKey && - d.type === axisLimit.latestKeyType)); + (d.type === DataKeyType.function && d.label === limit.latestKey) || + (d.type !== DataKeyType.function && d.name === limit.latestKey && + d.type === limit.latestKeyType)); if (latestDataKey) { break; } @@ -633,8 +632,8 @@ export class TbTimeSeriesChart { yAxis.maxLatestDataKey = latestDataKey; } } - } else if (axisLimit.type === ValueSourceType.entity) { - const entityAliasId = this.ctx.aliasController.getEntityAliasId(axisLimit.entityAlias); + } else if (limit.type === ValueSourceType.entity) { + const entityAliasId = this.ctx.aliasController.getEntityAliasId(limit.entityAlias); if (entityAliasId) { let datasource = axisLimitDatasources.find(d => d.entityAliasId === entityAliasId); const entityDataKey: DataKey = { @@ -651,16 +650,16 @@ export class TbTimeSeriesChart { } else { datasource = { type: DatasourceType.entity, - name: axisLimit.entityAlias, - aliasName: axisLimit.entityAlias, + name: limit.entityAlias, + aliasName: limit.entityAlias, entityAliasId, dataKeys: [entityDataKey] }; axisLimitDatasources.push(datasource); } } - } else if (axisLimit.type === ValueSourceType.constant) { - const value = unitConvertor ? unitConvertor(axisLimit.value) : axisLimit.value; + } else if (limit.type === ValueSourceType.constant) { + const value = unitConvertor ? unitConvertor(limit.value) : limit.value; if (limitType === 'min') { yAxis.option.min = value; } else { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.scss deleted file mode 100644 index 1a4214be18..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.scss +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Copyright © 2016-2025 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.ts index 9962bdeed9..f01336d22e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.ts @@ -38,7 +38,6 @@ import { merge } from 'rxjs'; @Component({ selector: 'tb-axis-scale-row', templateUrl: './axis-scale-row.component.html', - styleUrl: './axis-scale-row.component.scss', providers: [ { provide: NG_VALUE_ACCESSOR, @@ -101,7 +100,6 @@ export class AxisScaleRowComponent implements ControlValueAccessor, OnInit, Vali }); this.latestKeyFormControl = this.fb.control(null, [Validators.required]); this.entityKeyFormControl = this.fb.control(null, [Validators.required]); - this.subscribeToTypeChanges(); merge( this.latestKeyFormControl.valueChanges, this.entityKeyFormControl.valueChanges, @@ -150,43 +148,25 @@ export class AxisScaleRowComponent implements ControlValueAccessor, OnInit, Vali const errors: any = {}; if (this.limitForm.invalid) { - errors.form = this.getFormErrors(); + errors.form = false; } if (type === ValueSourceType.latestKey) { if (!this.latestKeyFormControl.value || this.latestKeyFormControl.invalid) { - errors.latestKey = { - valid: false - }; + errors.latestKey = false; } } else if (type === ValueSourceType.entity) { if (!this.limitForm.get('entityAlias')?.value) { - errors.entityAlias = { - valid: false - }; + errors.entityAlias = false; } if (!this.entityKeyFormControl.value || this.entityKeyFormControl.invalid) { - errors.entityKey = { - valid: false - }; + errors.entityKey = false; } } return Object.keys(errors).length ? { axisLimitForm: errors } : null; } - - private getFormErrors(): any { - const errors: any = {}; - Object.keys(this.limitForm.controls).forEach(key => { - const control = this.limitForm.get(key); - if (control && control.errors) { - errors[key] = control.errors; - } - }); - return errors; - } - private updateValidators() { const axisTypeControl = this.limitForm.get('type'); if (axisTypeControl && this.entityKeyFormControl && this.latestKeyFormControl) { @@ -207,38 +187,23 @@ export class AxisScaleRowComponent implements ControlValueAccessor, OnInit, Vali } } - private subscribeToTypeChanges() { - this.limitForm.controls.type.valueChanges - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe(() => { - const axisValueControl = this.limitForm.get('value'); - const axisEntityAliasControl = this.limitForm.get('entityAlias'); - if (axisValueControl) { - axisValueControl.setValue(null, { emitEvent: false }); - } - if (axisEntityAliasControl) { - axisEntityAliasControl.setValue(null, { emitEvent: false }); - } - this.latestKeyFormControl.setValue(null, { emitEvent: false }); - this.entityKeyFormControl.setValue(null, { emitEvent: false }); - - }); - } - private updateModel() { const value = this.limitForm.value; - this.modelValue.type = value.type ?? ValueSourceType.constant; - this.modelValue.value = value?.value; - this.modelValue.entityAlias = value?.entityAlias; - if (value.type === ValueSourceType.latestKey) { + const type = value.type; + let updates: Partial = { type }; + if (type === ValueSourceType.latestKey) { const latestKey: DataKey = this.latestKeyFormControl.value; - this.modelValue.latestKey = latestKey?.name; - this.modelValue.latestKeyType = (latestKey?.type as any); - } else if (value.type === ValueSourceType.entity) { + updates.latestKey = latestKey?.name; + updates.latestKeyType = latestKey?.type as any; + } else if (type === ValueSourceType.entity) { const entityKey: DataKey = this.entityKeyFormControl.value; - this.modelValue.entityKey = entityKey?.name; - this.modelValue.entityKeyType = (entityKey?.type as any); + updates.entityKey = entityKey?.name; + updates.entityKeyType = entityKey?.type as any; + updates.entityAlias = value?.entityAlias; + } else { + updates.value = value?.value; } + this.modelValue = updates as ValueSourceConfig; this.propagateChanges(this.modelValue); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts index 4bf7a78f98..9bed5d6e7c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts @@ -168,10 +168,7 @@ export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValu validate(): ValidationErrors | null { return this.axisSettingsFormGroup.valid ? null : { - axisSettings: { - valid: false, - errors: this.getFormErrors() - } + axisSettings: false }; } @@ -250,17 +247,6 @@ export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValu } } - private getFormErrors(): any { - const errors: any = {}; - Object.keys(this.axisSettingsFormGroup.controls).forEach(key => { - const control = this.axisSettingsFormGroup.get(key); - if (control && control.errors) { - errors[key] = control.errors; - } - }); - return errors; - } - private updateModel() { this.modelValue = this.axisSettingsFormGroup.getRawValue(); this.propagateChange(this.modelValue); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts index 3159a27abd..4d710a6ff4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts @@ -196,6 +196,8 @@ export class TimeSeriesChartYAxesPanelComponent implements ControlValueAccessor, const axes: TimeSeriesChartYAxisSettings[] = this.yAxesFormGroup.get('axes').value; axis.id = getNextTimeSeriesYAxisId(axes); axis.order = axes.length; + axis.min = this.normalizeAxisLimit(axis.min); + axis.max = this.normalizeAxisLimit(axis.max); const axesArray = this.yAxesFormGroup.get('axes') as UntypedFormArray; const axisControl = this.fb.control(axis, [timeSeriesChartYAxisValidator]); axesArray.push(axisControl); @@ -204,8 +206,6 @@ export class TimeSeriesChartYAxesPanelComponent implements ControlValueAccessor, private prepareAxesFormArray(axes: TimeSeriesChartYAxisSettings[]): UntypedFormArray { const axesControls: Array = []; axes.forEach((axis) => { - axis.min = this.normalizeAxisLimit(axis.min); - axis.max = this.normalizeAxisLimit(axis.max); axesControls.push(this.fb.control(axis, [timeSeriesChartYAxisValidator])); }); return this.fb.array(axesControls); @@ -216,17 +216,20 @@ export class TimeSeriesChartYAxesPanelComponent implements ControlValueAccessor, const result: TimeSeriesChartYAxes = {}; for (const [id, axis] of Object.entries(yAxes)) { + axis.min = this.normalizeAxisLimit(axis.min); + axis.max = this.normalizeAxisLimit(axis.max); + const minCfg = axis.min; + const maxCfg = axis.max; - const minCfg = axis.min as unknown as ValueSourceConfig; - const maxCfg = axis.max as unknown as ValueSourceConfig; - - const minValid = - minCfg?.type !== ValueSourceType.latestKey || - latestKeys.some(k => this.isYAxisKey(k, minCfg)); + const minValid = !!minCfg && ( + minCfg.type !== ValueSourceType.latestKey || + latestKeys.some(k => this.isYAxisKey(k, minCfg)) + ); - const maxValid = - maxCfg?.type !== ValueSourceType.latestKey || - latestKeys.some(k => this.isYAxisKey(k, maxCfg)); + const maxValid = !!maxCfg && ( + maxCfg.type !== ValueSourceType.latestKey || + latestKeys.some(k => this.isYAxisKey(k, maxCfg)) + ); if (minValid && maxValid) { result[id] = axis; @@ -288,17 +291,17 @@ export class TimeSeriesChartYAxesPanelComponent implements ControlValueAccessor, d.type === limit.latestKeyType); } - private normalizeAxisLimit(limit: any): ValueSourceConfig { + private normalizeAxisLimit(limit: string | number | ValueSourceConfig): ValueSourceConfig { if (!limit) { return { type: ValueSourceType.constant, value: null, entityAlias: null }; - } else if (typeof limit === 'number') { + } else if (typeof limit === 'number' || typeof limit === 'string') { return { type: ValueSourceType.constant, - value: limit, + value: Number(limit), entityAlias: null }; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.html index 420e834106..6389bbd6f8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.html @@ -30,14 +30,16 @@ -
+
- +
-
+
- +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts index 1857852afe..6f4c618394 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts @@ -43,6 +43,7 @@ import { deepClone } from '@core/utils'; import { TranslateService } from '@ngx-translate/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { TimeSeriesChartYAxesPanelComponent } from '@home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component'; +import { ValueSourceType } from '@shared/models/widget-settings.models'; @Component({ selector: 'tb-time-series-chart-y-axis-row', @@ -99,8 +100,8 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O position: [null, []], units: [null, []], decimals: [null, []], - min: [null, []], - max: [null, []], + min: this.createLimitFormGroup(), + max: this.createLimitFormGroup(), show: [null, []] }); this.axisFormGroup.valueChanges.pipe( @@ -195,6 +196,10 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O } } + checkIsConstantLimit(limit: 'min' | 'max') { + return this.axisFormGroup.get(`${limit}.type`)?.value === ValueSourceType.constant; + } + private updateValidators() { const show: boolean = this.axisFormGroup.get('show').value; if (show) { @@ -208,6 +213,13 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O this.axisFormGroup.get('units').disable({emitEvent: false}); this.axisFormGroup.get('decimals').disable({emitEvent: false}); } + if(!this.checkIsConstantLimit('min')){ + this.axisFormGroup.get('min').disable({emitEvent: false}); + } + if(!this.checkIsConstantLimit('max')){ + this.axisFormGroup.get('max').disable({emitEvent: false}); + } + } private updateModel() { @@ -221,4 +233,12 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O this.modelValue.show = value.show; this.propagateChange(this.modelValue); } + + private createLimitFormGroup() { + return this.fb.group({ + type: [ValueSourceType.constant, []], + value: [null, []], + entityAlias: [null, []] + }) + } } From dd7972721aa0db87ab03e2f902ec6d9e92e891c9 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Tue, 9 Dec 2025 15:31:55 +0200 Subject: [PATCH 09/35] fixed errors --- .../widget/lib/chart/time-series-chart.ts | 10 ++-- ...eries-chart-widget-settings.component.html | 3 + .../common/axis-scale-row.component.html | 2 +- ...ime-series-chart-y-axes-panel.component.ts | 2 + .../time-series-chart-y-axis-row.component.ts | 60 ++++++++++++++----- 5 files changed, 58 insertions(+), 19 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts index 8568e4b13b..9989df42e8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts @@ -307,10 +307,12 @@ export class TbTimeSeriesChart { const data = this.ctx.latestData.find(d => d.dataKey === yAxis.maxLatestDataKey); if (data?.data[0]) { const value = this.parseAxisLimitData(data.data[0][1], yAxis.unitConvertor); - if (yAxis.option.max !== value) { - yAxis.option.max = value; - update = true; - } + // if (yAxis.option.max !== value) { + // yAxis.option.max = value; + // update = true; + // } + update = true; + yAxis.option.max = 124; } } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html index 9a80362c8d..afa3c774ec 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html @@ -62,6 +62,9 @@ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.html index 37c2838a21..0129bdc490 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.html @@ -35,7 +35,7 @@ formControlName="entityAlias">
-
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts index 4d710a6ff4..690ad6cc55 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts @@ -250,6 +250,8 @@ export class TimeSeriesChartYAxesPanelComponent implements ControlValueAccessor, const foundYAxisKeys: DataKey[] = []; for(const yAxis of yAxes) { + console.log("yAxis",yAxis) + const min = yAxis.min as ValueSourceConfig; const max = yAxis.max as ValueSourceConfig; if (min.type === ValueSourceType.latestKey) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts index 6f4c618394..34f1b487a2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts @@ -135,17 +135,19 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O writeValue(value: TimeSeriesChartYAxisSettings): void { this.modelValue = value; - this.axisFormGroup.patchValue( - { - label: value.label, - position: value.position, - units: value.units, - decimals: value.decimals, - min: value.min, - max: value.max, - show: value.show, - }, {emitEvent: false} - ); + const min = this.normalizeLimit(value.min); + const max = this.normalizeLimit(value.max); + + this.axisFormGroup.patchValue({ + label: value.label, + position: value.position, + units: value.units, + decimals: value.decimals, + min, + max, + show: value.show, + }, { emitEvent: false }); + this.updateValidators(); this.cd.markForCheck(); } @@ -223,7 +225,8 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O } private updateModel() { - const value = this.axisFormGroup.value; + const value = this.axisFormGroup.getRawValue(); + console.log("value", value); this.modelValue.label = value.label; this.modelValue.position = value.position; this.modelValue.units = value.units; @@ -238,7 +241,36 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O return this.fb.group({ type: [ValueSourceType.constant, []], value: [null, []], - entityAlias: [null, []] - }) + latestKey: [null, []], + latestKeyType: [null, []], + entityAlias: [null, []], + entityKey: [null, []], + entityKeyType: [null, []] + }); + } + + private normalizeLimit(limit: any) { + const base = { + type: ValueSourceType.constant, + value: null, + latestKey: null, + latestKeyType: null, + entityAlias: null, + entityKey: null, + entityKeyType: null + }; + + if (limit == null) return base; + + if (typeof limit === 'number' || typeof limit === 'string') { + return { ...base, type: ValueSourceType.constant, value: Number(limit) }; + } + + return { + ...base, + ...limit, + }; } + + } From 631a580211360669312a194f935ff1ead0c0ef1f Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Tue, 9 Dec 2025 17:40:30 +0200 Subject: [PATCH 10/35] cleanup --- .../components/widget/lib/chart/time-series-chart.ts | 10 ++++------ .../chart/time-series-chart-y-axes-panel.component.ts | 2 -- .../chart/time-series-chart-y-axis-row.component.ts | 1 - 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts index 9989df42e8..8568e4b13b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts @@ -307,12 +307,10 @@ export class TbTimeSeriesChart { const data = this.ctx.latestData.find(d => d.dataKey === yAxis.maxLatestDataKey); if (data?.data[0]) { const value = this.parseAxisLimitData(data.data[0][1], yAxis.unitConvertor); - // if (yAxis.option.max !== value) { - // yAxis.option.max = value; - // update = true; - // } - update = true; - yAxis.option.max = 124; + if (yAxis.option.max !== value) { + yAxis.option.max = value; + update = true; + } } } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts index 690ad6cc55..4d710a6ff4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts @@ -250,8 +250,6 @@ export class TimeSeriesChartYAxesPanelComponent implements ControlValueAccessor, const foundYAxisKeys: DataKey[] = []; for(const yAxis of yAxes) { - console.log("yAxis",yAxis) - const min = yAxis.min as ValueSourceConfig; const max = yAxis.max as ValueSourceConfig; if (min.type === ValueSourceType.latestKey) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts index 34f1b487a2..46c7c989c0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts @@ -226,7 +226,6 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O private updateModel() { const value = this.axisFormGroup.getRawValue(); - console.log("value", value); this.modelValue.label = value.label; this.modelValue.position = value.position; this.modelValue.units = value.units; From e378c2b7952f21883dc7f81c8dde0a68a77115a9 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Tue, 9 Dec 2025 17:51:56 +0200 Subject: [PATCH 11/35] enhanced validators --- .../chart/time-series-chart-y-axis-row.component.html | 6 ++---- .../common/chart/time-series-chart-y-axis-row.component.ts | 6 ++++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.html index 6389bbd6f8..22d3fab103 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.html @@ -32,14 +32,12 @@
- +
- +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts index 46c7c989c0..83f1b6623a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts @@ -217,9 +217,13 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O } if(!this.checkIsConstantLimit('min')){ this.axisFormGroup.get('min').disable({emitEvent: false}); + } else { + this.axisFormGroup.get('min').enable({emitEvent: false}); } if(!this.checkIsConstantLimit('max')){ this.axisFormGroup.get('max').disable({emitEvent: false}); + } else { + this.axisFormGroup.get('max').enable({emitEvent: false}); } } @@ -270,6 +274,4 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O ...limit, }; } - - } From 602942fb63ce6fed13260cbf1acf32a33f9e4a20 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Wed, 10 Dec 2025 11:03:56 +0200 Subject: [PATCH 12/35] added fetch by ids entities endpoints --- .../app/core/http/asset-profile.service.ts | 5 +++ ui-ngx/src/app/core/http/customer.service.ts | 4 +++ ui-ngx/src/app/core/http/dashboard.service.ts | 5 +++ .../app/core/http/device-profile.service.ts | 5 +++ .../src/app/core/http/entity-view.service.ts | 5 +++ ui-ngx/src/app/core/http/entity.service.ts | 36 +++++-------------- .../src/app/core/http/rule-chain.service.ts | 5 +++ ui-ngx/src/app/core/http/tenant.service.ts | 4 +++ ui-ngx/src/app/core/http/user.service.ts | 4 +++ ui-ngx/src/app/core/http/widget.service.ts | 5 +++ 10 files changed, 51 insertions(+), 27 deletions(-) diff --git a/ui-ngx/src/app/core/http/asset-profile.service.ts b/ui-ngx/src/app/core/http/asset-profile.service.ts index 544779dcd9..8cbad08812 100644 --- a/ui-ngx/src/app/core/http/asset-profile.service.ts +++ b/ui-ngx/src/app/core/http/asset-profile.service.ts @@ -38,6 +38,11 @@ export class AssetProfileService { return this.http.get>(`/api/assetProfiles${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); } + public getAssetProfilesByIds(assetProfileIds: Array, config?: RequestConfig): Observable> { + return this.http.get>(`/api/assetProfileInfos?assetProfileIds=${assetProfileIds.join(',')}`, + defaultHttpOptionsFromConfig(config)); + } + public getAssetProfile(assetProfileId: string, config?: RequestConfig): Observable { return this.http.get(`/api/assetProfile/${assetProfileId}`, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/core/http/customer.service.ts b/ui-ngx/src/app/core/http/customer.service.ts index ec8955ca4b..3553b417f0 100644 --- a/ui-ngx/src/app/core/http/customer.service.ts +++ b/ui-ngx/src/app/core/http/customer.service.ts @@ -41,6 +41,10 @@ export class CustomerService { return this.http.get(`/api/customer/${customerId}`, defaultHttpOptionsFromConfig(config)); } + public getCustomersByIds(customerIds: Array, config?: RequestConfig): Observable> { + return this.http.get>(`/api/customers?customerIds=${customerIds.join(',')}`, defaultHttpOptionsFromConfig(config)); + } + public saveCustomer(customer: Customer, config?: RequestConfig): Observable; public saveCustomer(customer: Customer, saveParams: SaveEntityParams, config?: RequestConfig): Observable; public saveCustomer(customer: Customer, saveParamsOrConfig?: SaveEntityParams | RequestConfig, config?: RequestConfig): Observable { diff --git a/ui-ngx/src/app/core/http/dashboard.service.ts b/ui-ngx/src/app/core/http/dashboard.service.ts index 0a27287e7f..a99bdbbcaf 100644 --- a/ui-ngx/src/app/core/http/dashboard.service.ts +++ b/ui-ngx/src/app/core/http/dashboard.service.ts @@ -83,6 +83,11 @@ export class DashboardService { return this.http.get(`/api/dashboard/info/${dashboardId}`, defaultHttpOptionsFromConfig(config)); } + public getDashboards(dashboardIds: string[], config?: RequestConfig): Observable> { + return this.http.get>(`/api/dashboards?dashboardIds=${dashboardIds.join(',')}`, + defaultHttpOptionsFromConfig(config)); + } + public saveDashboard(dashboard: Dashboard, config?: RequestConfig): Observable { return this.http.post('/api/dashboard', dashboard, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/core/http/device-profile.service.ts b/ui-ngx/src/app/core/http/device-profile.service.ts index 91b7314ad2..faf5ccedb3 100644 --- a/ui-ngx/src/app/core/http/device-profile.service.ts +++ b/ui-ngx/src/app/core/http/device-profile.service.ts @@ -50,6 +50,11 @@ export class DeviceProfileService { return this.http.get>(`/api/deviceProfiles${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); } + public getDeviceProfilesByIds(deviceProfileIds: Array, config?: RequestConfig): Observable> { + return this.http.get>(`/api/deviceProfileInfos?deviceProfileIds=${deviceProfileIds.join(',')}`, + defaultHttpOptionsFromConfig(config)); + } + public getDeviceProfile(deviceProfileId: string, config?: RequestConfig): Observable { return this.http.get(`/api/deviceProfile/${deviceProfileId}`, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/core/http/entity-view.service.ts b/ui-ngx/src/app/core/http/entity-view.service.ts index 7285c5420f..d951cfee80 100644 --- a/ui-ngx/src/app/core/http/entity-view.service.ts +++ b/ui-ngx/src/app/core/http/entity-view.service.ts @@ -48,6 +48,11 @@ export class EntityViewService { return this.http.get(`/api/entityView/${entityViewId}`, defaultHttpOptionsFromConfig(config)); } + public getEntityViews(entityViewIds: Array, config?: RequestConfig): Observable> { + return this.http.get>(`/api/entityViews?entityViewIds=${entityViewIds.join(',')}`, + defaultHttpOptionsFromConfig(config)); + } + public getEntityViewInfo(entityViewId: string, config?: RequestConfig): Observable { return this.http.get(`/api/entityView/info/${entityViewId}`, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index 4daae2115d..d6556c3ced 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -245,50 +245,34 @@ export class EntityService { observable = this.edgeService.getEdges(entityIds, config); break; case EntityType.ENTITY_VIEW: - observable = this.getEntitiesByIdsObservable( - (id) => this.entityViewService.getEntityView(id, config), - entityIds); + observable = this.entityViewService.getEntityViews(entityIds, config); break; case EntityType.TENANT: - observable = this.getEntitiesByIdsObservable( - (id) => this.tenantService.getTenant(id, config), - entityIds); + observable = this.tenantService.getTenantsByIds(entityIds, config); break; case EntityType.CUSTOMER: - observable = this.getEntitiesByIdsObservable( - (id) => this.customerService.getCustomer(id, config), - entityIds); + observable = this.customerService.getCustomersByIds(entityIds, config); break; case EntityType.DASHBOARD: - observable = this.getEntitiesByIdsObservable( - (id) => this.dashboardService.getDashboardInfo(id, config), - entityIds); + observable = this.dashboardService.getDashboards(entityIds, config); break; case EntityType.USER: - observable = this.getEntitiesByIdsObservable( - (id) => this.userService.getUser(id, config), - entityIds); + observable = this.userService.getUsersByIds(entityIds, config); break; case EntityType.ALARM: console.error('Get Alarm Entity is not implemented!'); break; case EntityType.DEVICE_PROFILE: - observable = this.getEntitiesByIdsObservable( - (id) => this.deviceProfileService.getDeviceProfileInfo(id, config), - entityIds); + observable = this.deviceProfileService.getDeviceProfilesByIds(entityIds, config); break; case EntityType.TENANT_PROFILE: observable = this.tenantProfileService.getTenantProfilesByIds(entityIds, config); break; case EntityType.ASSET_PROFILE: - observable = this.getEntitiesByIdsObservable( - (id) => this.assetProfileService.getAssetProfileInfo(id, config), - entityIds); + observable = this.assetProfileService.getAssetProfilesByIds(entityIds, config); break; case EntityType.WIDGETS_BUNDLE: - observable = this.getEntitiesByIdsObservable( - (id) => this.widgetService.getWidgetsBundle(id, config), - entityIds); + observable = this.widgetService.getWidgetsBundlesByIds(entityIds, config); break; case EntityType.NOTIFICATION_TARGET: observable = this.notificationService.getNotificationTargetsByIds(entityIds, config); @@ -300,9 +284,7 @@ export class EntityService { observable = this.oauth2Service.findTenantOAuth2ClientInfosByIds(entityIds, config); break; case EntityType.RULE_CHAIN: - observable = this.getEntitiesByIdsObservable( - (id) => this.ruleChainService.getRuleChain(id, config), - entityIds); + observable = this.ruleChainService.getRuleChainsByIds(entityIds, config); break; case EntityType.TB_RESOURCE: observable = this.resourceService.getResourcesByIds(entityIds, config); diff --git a/ui-ngx/src/app/core/http/rule-chain.service.ts b/ui-ngx/src/app/core/http/rule-chain.service.ts index ba80632c40..197a1876ff 100644 --- a/ui-ngx/src/app/core/http/rule-chain.service.ts +++ b/ui-ngx/src/app/core/http/rule-chain.service.ts @@ -68,6 +68,11 @@ export class RuleChainService { defaultHttpOptionsFromConfig(config)); } + public getRuleChainsByIds(ruleChainIds: Array, config?: RequestConfig): Observable> { + return this.http.get>(`/api/ruleChains?&ruleChainIds=${ruleChainIds.join(',')}`, + defaultHttpOptionsFromConfig(config)); + } + public getRuleChain(ruleChainId: string, config?: RequestConfig): Observable { return this.http.get(`/api/ruleChain/${ruleChainId}`, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/core/http/tenant.service.ts b/ui-ngx/src/app/core/http/tenant.service.ts index f8c62c6ae3..8ac4fc68eb 100644 --- a/ui-ngx/src/app/core/http/tenant.service.ts +++ b/ui-ngx/src/app/core/http/tenant.service.ts @@ -35,6 +35,10 @@ export class TenantService { return this.http.get>(`/api/tenants${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); } + public getTenantsByIds(tenantIds: Array, config?: RequestConfig): Observable> { + return this.http.get>(`/api/tenants?tenantIds=${tenantIds.join(',')}`, defaultHttpOptionsFromConfig(config)); + } + public getTenantInfos(pageLink: PageLink, config?: RequestConfig): Observable> { return this.http.get>(`/api/tenantInfos${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/core/http/user.service.ts b/ui-ngx/src/app/core/http/user.service.ts index 4d96eca8cd..b69e0d49b2 100644 --- a/ui-ngx/src/app/core/http/user.service.ts +++ b/ui-ngx/src/app/core/http/user.service.ts @@ -61,6 +61,10 @@ export class UserService { return this.http.get(`/api/user/${userId}`, defaultHttpOptionsFromConfig(config)); } + public getUsersByIds(userIds: Array, config?: RequestConfig): Observable> { + return this.http.get>(`/api/users?userIds=${userIds.join(',')}`, defaultHttpOptionsFromConfig(config)); + } + public saveUser(user: User, sendActivationMail: boolean = false, config?: RequestConfig): Observable { let url = '/api/user'; diff --git a/ui-ngx/src/app/core/http/widget.service.ts b/ui-ngx/src/app/core/http/widget.service.ts index 00a90f632a..ae09cc4ba3 100644 --- a/ui-ngx/src/app/core/http/widget.service.ts +++ b/ui-ngx/src/app/core/http/widget.service.ts @@ -331,6 +331,11 @@ export class WidgetService { this.widgetsInfoInMemoryCache.delete(fullFqn); } + public getWidgetsBundlesByIds(widgetsBundleIds: Array, config?: RequestConfig): Observable> { + return this.http.get>(`/api/widgetsBundles?widgetsBundleIds=${widgetsBundleIds.join(',')}`, + defaultHttpOptionsFromConfig(config)); + } + private loadWidgetsBundleCache(config?: RequestConfig): Observable { if (!this.allWidgetsBundles) { if (!this.loadWidgetsBundleCacheSubject) { From cadafa1591112f1d08cee25e6d0b42675d82fed1 Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Wed, 10 Dec 2025 16:07:31 +0200 Subject: [PATCH 13/35] UI: Fixed color settings panel placement --- .../widget/lib/settings/common/color-settings.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.ts index 05c069cbf3..1e41cea52d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.ts @@ -164,7 +164,7 @@ export class ColorSettingsComponent implements OnInit, ControlValueAccessor, OnD renderer: this.renderer, componentType: ColorSettingsPanelComponent, hostView: this.viewContainerRef, - preferredPlacement: 'left', + preferredPlacement: ['leftTopOnly', 'leftOnly', 'leftBottomOnly'], context: { colorSettings: this.modelValue, settingsComponents: this.colorSettingsComponentService.getOtherColorSettingsComponents(this), From 2f9d142cc9a705a88356bfdbc4436731b34e7f90 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 10 Dec 2025 18:21:59 +0200 Subject: [PATCH 14/35] UI: Redesign login pages --- .../login/create-password.component.html | 86 ++++++------- .../login/create-password.component.scss | 1 + .../pages/login/create-password.component.ts | 14 ++- ...force-two-factor-auth-login.component.html | 22 ++-- ...force-two-factor-auth-login.component.scss | 1 + .../pages/login/link-expired.component.html | 7 +- .../pages/login/link-expired.component.scss | 1 + .../pages/login/link-expired.component.ts | 16 +-- .../login/pages/login/login.component.html | 114 +++++++++--------- .../login/pages/login/login.component.scss | 78 ++++++------ .../login/pages/login/login.component.ts | 25 ++-- .../reset-password-request.component.html | 48 ++++---- .../reset-password-request.component.scss | 1 + .../login/reset-password-request.component.ts | 5 +- .../pages/login/reset-password.component.html | 90 +++++++------- .../pages/login/reset-password.component.scss | 6 +- .../pages/login/reset-password.component.ts | 17 ++- .../two-factor-auth-login.component.html | 4 +- .../two-factor-auth-login.component.scss | 1 + .../assets/locale/locale.constant-ar_AE.json | 1 - .../assets/locale/locale.constant-ca_ES.json | 1 - .../assets/locale/locale.constant-cs_CZ.json | 1 - .../assets/locale/locale.constant-de_DE.json | 1 - .../assets/locale/locale.constant-el_GR.json | 1 - .../assets/locale/locale.constant-en_US.json | 15 +-- .../assets/locale/locale.constant-es_ES.json | 1 - .../assets/locale/locale.constant-fa_IR.json | 1 - .../assets/locale/locale.constant-fr_FR.json | 1 - .../assets/locale/locale.constant-it_IT.json | 1 - .../assets/locale/locale.constant-ja_JP.json | 1 - .../assets/locale/locale.constant-ka_GE.json | 1 - .../assets/locale/locale.constant-ko_KR.json | 1 - .../assets/locale/locale.constant-lt_LT.json | 1 - .../assets/locale/locale.constant-lv_LV.json | 1 - .../assets/locale/locale.constant-nl_BE.json | 1 - .../assets/locale/locale.constant-nl_NL.json | 1 - .../assets/locale/locale.constant-no_NO.json | 1 - .../assets/locale/locale.constant-pl_PL.json | 1 - .../assets/locale/locale.constant-pt_BR.json | 1 - .../assets/locale/locale.constant-ro_RO.json | 1 - .../assets/locale/locale.constant-sl_SI.json | 1 - .../assets/locale/locale.constant-tr_TR.json | 1 - .../assets/locale/locale.constant-uk_UA.json | 1 - .../assets/locale/locale.constant-zh_CN.json | 1 - .../assets/locale/locale.constant-zh_TW.json | 1 - ui-ngx/src/styles.scss | 33 +++++ 46 files changed, 288 insertions(+), 322 deletions(-) diff --git a/ui-ngx/src/app/modules/login/pages/login/create-password.component.html b/ui-ngx/src/app/modules/login/pages/login/create-password.component.html index e3e43b6d29..04d7b3ad03 100644 --- a/ui-ngx/src/app/modules/login/pages/login/create-password.component.html +++ b/ui-ngx/src/app/modules/login/pages/login/create-password.component.html @@ -17,55 +17,49 @@ -->
- - - login.create-password - + + login.create-password - +
+ - - + +
-
-
- - - common.password - - lock - - - {{ 'security.password-requirement.password-not-meet-requirements' | translate }} - - - - login.password-again - - lock - - - {{ 'security.password-requirement.new-passwords-not-match' | translate }} - - -
- - -
-
-
+ + common.password + + lock + + + {{ 'security.password-requirement.password-not-meet-requirements' | translate }} + + + + login.password-again + + lock + + + {{ 'security.password-requirement.new-passwords-not-match' | translate }} + + +
+ + +
diff --git a/ui-ngx/src/app/modules/login/pages/login/create-password.component.scss b/ui-ngx/src/app/modules/login/pages/login/create-password.component.scss index 6d5d687d13..ccd82a5060 100644 --- a/ui-ngx/src/app/modules/login/pages/login/create-password.component.scss +++ b/ui-ngx/src/app/modules/login/pages/login/create-password.component.scss @@ -20,6 +20,7 @@ flex: 1 1 0; .tb-create-password-content { background-color: #eee; + --mdc-elevated-card-container-elevation: none; .tb-create-password-card { @media #{$mat-gt-xs} { width: 450px !important; diff --git a/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts b/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts index 85396338d5..9e02ad4eb6 100644 --- a/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts +++ b/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts @@ -16,7 +16,6 @@ import { Component } from '@angular/core'; import { AuthService } from '@core/auth/auth.service'; -import { PageComponent } from '@shared/components/page.component'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; import { UserPasswordPolicy } from '@shared/models/settings.models'; @@ -27,17 +26,18 @@ import { passwordsMatchValidator, passwordStrengthValidator } from '@shared/mode templateUrl: './create-password.component.html', styleUrls: ['./create-password.component.scss'] }) -export class CreatePasswordComponent extends PageComponent { +export class CreatePasswordComponent { passwordPolicy: UserPasswordPolicy; createPassword: FormGroup; + isLoading = false; + private activateToken: string; constructor(private route: ActivatedRoute, private authService: AuthService, private fb: FormBuilder) { - super(); this.activateToken = this.route.snapshot.queryParams['activateToken'] || ''; this.passwordPolicy = this.route.snapshot.data['passwordPolicy']; @@ -60,9 +60,11 @@ export class CreatePasswordComponent extends PageComponent { if (this.createPassword.invalid) { this.createPassword.markAllAsTouched(); } else { - this.authService.activate( - this.activateToken, - this.createPassword.get('newPassword').value, true).subscribe(); + this.isLoading = true + this.authService.activate(this.activateToken, this.createPassword.get('newPassword').value, true) + .subscribe({ + error: () => {this.isLoading = false;} + }); } } } diff --git a/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.html b/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.html index 24f9ec1bd7..d5ee7fd9b1 100644 --- a/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.html +++ b/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.html @@ -41,7 +41,7 @@ } @if (config) { - } @@ -66,7 +66,7 @@

login.scan-qr-code

login.enter-key-manually

-
+
{{ totpAuthURLSecret }}
-
+
@@ -126,7 +126,7 @@
-
+
@@ -162,7 +162,7 @@

login.email-description

- + @@ -174,7 +174,7 @@ -
+
@@ -223,7 +223,7 @@

login.backup-code-warn

-
@@ -259,7 +259,7 @@ }

- + {{ 'login.verification-code-many-request' | translate }} } -
+
@@ -300,8 +300,8 @@

{{ twoFactorAuthProvidersSuccessCardTranslate.get(providerType).description | translate }}

-
- @if (isAnyProviderAvailable) { diff --git a/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.scss b/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.scss index f1ea95d639..35b642b11f 100644 --- a/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.scss +++ b/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.scss @@ -23,6 +23,7 @@ .tb-two-factor-auth-login-content { background-color: #eee; + --mdc-elevated-card-container-elevation: none; .progress-bar { z-index: 10; diff --git a/ui-ngx/src/app/modules/login/pages/login/link-expired.component.html b/ui-ngx/src/app/modules/login/pages/login/link-expired.component.html index e9dcd74b9c..d6d6f0c7dd 100644 --- a/ui-ngx/src/app/modules/login/pages/login/link-expired.component.html +++ b/ui-ngx/src/app/modules/login/pages/login/link-expired.component.html @@ -23,16 +23,11 @@ {{ title | translate }} - - -
{{ message | translate }}
- diff --git a/ui-ngx/src/app/modules/login/pages/login/link-expired.component.scss b/ui-ngx/src/app/modules/login/pages/login/link-expired.component.scss index c2c4e270d0..abc1bb51d2 100644 --- a/ui-ngx/src/app/modules/login/pages/login/link-expired.component.scss +++ b/ui-ngx/src/app/modules/login/pages/login/link-expired.component.scss @@ -20,6 +20,7 @@ flex: 1 1 0; .tb-expired-link-content { background-color: #eee; + --mdc-elevated-card-container-elevation: none; .tb-expired-link-card { letter-spacing: 0.15px; line-height: 24px; diff --git a/ui-ngx/src/app/modules/login/pages/login/link-expired.component.ts b/ui-ngx/src/app/modules/login/pages/login/link-expired.component.ts index 79f0ee5b43..c520cb30cd 100644 --- a/ui-ngx/src/app/modules/login/pages/login/link-expired.component.ts +++ b/ui-ngx/src/app/modules/login/pages/login/link-expired.component.ts @@ -15,33 +15,23 @@ /// import { Component } from '@angular/core'; -import { Store } from '@ngrx/store'; -import { AppState } from '@core/core.state'; -import { PageComponent } from '@shared/components/page.component'; -import { ActivatedRoute, Router } from '@angular/router'; +import { ActivatedRoute } from '@angular/router'; @Component({ selector: 'tb-link-expired', templateUrl: './link-expired.component.html', styleUrls: ['./link-expired.component.scss'] }) -export class LinkExpiredComponent extends PageComponent { +export class LinkExpiredComponent { isPasswordLinkExpired: boolean; title: string; message: string; - constructor(protected store: Store, - private route: ActivatedRoute, - private router: Router) { - super(store); + constructor(private route: ActivatedRoute) { this.isPasswordLinkExpired = this.route.snapshot.data.passwordLinkExpired; this.title = this.isPasswordLinkExpired ? 'login.reset-password-link-expired' : 'login.activation-link-expired'; this.message = this.isPasswordLinkExpired ? 'login.reset-password-link-expired-message' : 'login.activation-link-expired-message'; } - - navigateToLoginPage() { - this.router.navigateByUrl('login'); - } } diff --git a/ui-ngx/src/app/modules/login/pages/login/login.component.html b/ui-ngx/src/app/modules/login/pages/login/login.component.html index 94350c80f0..0d29df31d1 100644 --- a/ui-ngx/src/app/modules/login/pages/login/login.component.html +++ b/ui-ngx/src/app/modules/login/pages/login/login.component.html @@ -17,71 +17,67 @@ --> diff --git a/ui-ngx/src/app/modules/login/pages/login/login.component.scss b/ui-ngx/src/app/modules/login/pages/login/login.component.scss index 8859aed7e4..331bf70822 100644 --- a/ui-ngx/src/app/modules/login/pages/login/login.component.scss +++ b/ui-ngx/src/app/modules/login/pages/login/login.component.scss @@ -23,48 +23,49 @@ margin-top: 36px; margin-bottom: 76px; background-color: #eee; + --mdc-elevated-card-container-elevation: none; .tb-login-form { @media #{$mat-gt-xs} { - width: 550px !important; + width: 480px !important; } .forgot-password { - padding: 0 0.5em 1em; - .tb-reset-password { - padding: 0 6px; - } + --mat-text-button-horizontal-padding: 0; + --mdc-text-button-container-height: 20px; } .tb-action-button{ - padding: 20px 0 16px; + margin: 20px 0 16px; } } - .oauth-container{ - padding: 0; - - .container-divider { - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - width: 100%; + .container-divider { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + width: 100%; + margin-bottom: 16px; - .line { - flex: 1; - } + .mat-divider-horizontal{ + --mat-divider-color: var(--mdc-outlined-text-field-outline-color); + } - .mat-divider-horizontal{ - position: relative; - } + .text { + padding-right: 8px; + padding-left: 8px; + font-size: 16px; + line-height: 24px; + } + } - .text { - padding-right: 8px; - padding-left: 8px; - font-size: 16px; - line-height: 24px; - letter-spacing: 0.15px; - } + .oauth-container{ + .title { + font-size: 20px; + line-height: 24px; + letter-spacing: .1px; + text-align: center; + margin-bottom: 4px; } .material-icons{ @@ -74,33 +75,32 @@ a.login-with-button { color: rgba(black, 0.87); - background-color: map-get($tb-dark-theme-background, raised-button); + background-color: #ffffff; } .login-button-container { a.login-with-button { - --mdc-outlined-button-container-shape: 8px; - max-width: 123px; + max-width: 170px; min-width: 60px; flex-grow: 1; - flex-basis: 120px; + flex-basis: 130px; .tb-mat-20 { - margin: 0; - vertical-align: text-top; + margin-right: 12px; } } - &:has(> :nth-child(2):last-child) { + &:has(> :nth-child(1):last-child) { a.login-with-button { - max-width: 180px; - flex-basis: 180px; + max-width: 100%; } } - &:has(> :nth-child(3):last-child) { + &:has(> :nth-child(2):last-child), + &:has(> :nth-child(4):last-child) { a.login-with-button { - max-width: 180px; + max-width: 100%; + flex-basis: 180px; } } } diff --git a/ui-ngx/src/app/modules/login/pages/login/login.component.ts b/ui-ngx/src/app/modules/login/pages/login/login.component.ts index a4bd2853b9..2046bcb3ed 100644 --- a/ui-ngx/src/app/modules/login/pages/login/login.component.ts +++ b/ui-ngx/src/app/modules/login/pages/login/login.component.ts @@ -16,9 +16,6 @@ import { Component, OnInit } from '@angular/core'; import { AuthService } from '@core/auth/auth.service'; -import { Store } from '@ngrx/store'; -import { AppState } from '@core/core.state'; -import { PageComponent } from '@shared/components/page.component'; import { UntypedFormBuilder, Validators } from '@angular/forms'; import { HttpErrorResponse } from '@angular/common/http'; import { Constants } from '@shared/models/constants'; @@ -31,9 +28,10 @@ import { validateEmail } from '@app/core/utils'; templateUrl: './login.component.html', styleUrls: ['./login.component.scss'] }) -export class LoginComponent extends PageComponent implements OnInit { +export class LoginComponent implements OnInit { passwordViolation = false; + isLoading = false; loginFormGroup = this.fb.group({ username: ['', [Validators.required, validateEmail]], @@ -41,11 +39,9 @@ export class LoginComponent extends PageComponent implements OnInit { }); oauth2Clients: Array = null; - constructor(protected store: Store, - private authService: AuthService, + constructor(private authService: AuthService, public fb: UntypedFormBuilder, private router: Router) { - super(store); } ngOnInit() { @@ -54,9 +50,11 @@ export class LoginComponent extends PageComponent implements OnInit { login(): void { if (this.loginFormGroup.valid) { - this.authService.login(this.loginFormGroup.value).subscribe( - () => {}, - (error: HttpErrorResponse) => { + this.isLoading = true; + this.authService.login(this.loginFormGroup.value).subscribe({ + next: () => {}, + error: (error: HttpErrorResponse) => { + this.isLoading = false; if (error && error.error && error.error.errorCode) { if (error.error.errorCode === Constants.serverErrorCode.credentialsExpired) { this.router.navigateByUrl(`login/resetExpiredPassword?resetToken=${error.error.resetToken}`); @@ -65,12 +63,9 @@ export class LoginComponent extends PageComponent implements OnInit { } } } - ); - } else { - Object.keys(this.loginFormGroup.controls).forEach(field => { - const control = this.loginFormGroup.get(field); - control.markAsTouched({onlySelf: true}); }); + } else { + this.loginFormGroup.markAllAsTouched(); } } diff --git a/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.html b/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.html index abf2e6c61c..df286768ef 100644 --- a/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.html +++ b/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.html @@ -18,38 +18,32 @@
- - - login.request-password-reset - + + login.request-password-reset +
- +
-
-
- - - login.email - - email - - {{ 'user.invalid-email-format' | translate }} - - -
- - -
-
-
+ + login.email + + email + + {{ 'user.invalid-email-format' | translate }} + + +
+ + +
diff --git a/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.scss b/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.scss index 3d5a53ea16..77f76b648f 100644 --- a/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.scss +++ b/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.scss @@ -20,6 +20,7 @@ flex: 1 1 0; .tb-request-password-reset-content { background-color: #eee; + --mdc-elevated-card-container-elevation: none; .tb-request-password-reset-card { @media #{$mat-gt-xs} { width: 450px !important; diff --git a/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.ts b/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.ts index 3daeaa693d..c7d22e3274 100644 --- a/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.ts +++ b/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.ts @@ -29,7 +29,7 @@ import { validateEmail } from '@app/core/utils'; templateUrl: './reset-password-request.component.html', styleUrls: ['./reset-password-request.component.scss'] }) -export class ResetPasswordRequestComponent extends PageComponent implements OnInit { +export class ResetPasswordRequestComponent extends PageComponent { clicked: boolean = false; @@ -44,9 +44,6 @@ export class ResetPasswordRequestComponent extends PageComponent implements OnIn super(store); } - ngOnInit() { - } - disableInputs() { this.requestPasswordRequest.disable(); this.clicked = true; diff --git a/ui-ngx/src/app/modules/login/pages/login/reset-password.component.html b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.html index 7492ab654f..a5fe779ab3 100644 --- a/ui-ngx/src/app/modules/login/pages/login/reset-password.component.html +++ b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.html @@ -17,58 +17,52 @@ -->
- - - login.password-reset - - -
{{ 'login.expired-password-reset-message' | translate }}
+ + login.password-reset + + login.expired-password-reset-message - +
+ - - + +
-
-
- - - login.new-password - - lock - - - {{ 'security.password-requirement.password-not-meet-requirements' | translate }} - - - - login.new-password-again - - lock - - - {{ 'security.password-requirement.new-passwords-not-match' | translate }} - - -
- - -
-
-
+ + login.new-password + + lock + + + {{ 'security.password-requirement.password-not-meet-requirements' | translate }} + + + + login.new-password-again + + lock + + + {{ 'security.password-requirement.new-passwords-not-match' | translate }} + + +
+ + +
diff --git a/ui-ngx/src/app/modules/login/pages/login/reset-password.component.scss b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.scss index cda127d901..2a3022352e 100644 --- a/ui-ngx/src/app/modules/login/pages/login/reset-password.component.scss +++ b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.scss @@ -20,15 +20,11 @@ flex: 1 1 0; .tb-reset-password-content { background-color: #eee; + --mdc-elevated-card-container-elevation: none; .tb-reset-password-card { @media #{$mat-gt-sm} { width: 450px !important; } } - - .tb-card-title{ - padding-top: 0; - padding-bottom: 0; - } } } diff --git a/ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts index 396c0f740d..d062ff0037 100644 --- a/ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts +++ b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts @@ -16,7 +16,6 @@ import { Component } from '@angular/core'; import { AuthService } from '@core/auth/auth.service'; -import { PageComponent } from '@shared/components/page.component'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { UserPasswordPolicy } from '@shared/models/settings.models'; @@ -27,9 +26,10 @@ import { passwordsMatchValidator, passwordStrengthValidator } from '@shared/mode templateUrl: './reset-password.component.html', styleUrls: ['./reset-password.component.scss'] }) -export class ResetPasswordComponent extends PageComponent { +export class ResetPasswordComponent { isExpiredPassword: boolean; + isLoading = false; resetPassword: FormGroup; passwordPolicy: UserPasswordPolicy; @@ -40,7 +40,6 @@ export class ResetPasswordComponent extends PageComponent { private router: Router, private authService: AuthService, private fb: FormBuilder) { - super(); this.resetToken = this.route.snapshot.queryParams['resetToken'] || ''; this.passwordPolicy = this.route.snapshot.data['passwordPolicy']; @@ -62,13 +61,13 @@ export class ResetPasswordComponent extends PageComponent { onResetPassword() { if (this.resetPassword.invalid) { - this.resetPassword.markAllAsTouched(); + this.resetPassword.markAllAsTouched(); } else { - this.authService.resetPassword( - this.resetToken, - this.resetPassword.get('newPassword').value).subscribe( - () => this.router.navigateByUrl('login') - ); + this.isLoading = true; + this.authService.resetPassword(this.resetToken, this.resetPassword.get('newPassword').value).subscribe({ + next: () => this.router.navigateByUrl('login'), + error: () => {this.isLoading = false;} + }); } } } diff --git a/ui-ngx/src/app/modules/login/pages/login/two-factor-auth-login.component.html b/ui-ngx/src/app/modules/login/pages/login/two-factor-auth-login.component.html index 5175c01546..d85cbb0f8b 100644 --- a/ui-ngx/src/app/modules/login/pages/login/two-factor-auth-login.component.html +++ b/ui-ngx/src/app/modules/login/pages/login/two-factor-auth-login.component.html @@ -41,7 +41,7 @@

{{ providerDescription }}

- +
-