From eae065aa74408d4ecec7f61125e64090a113b760 Mon Sep 17 00:00:00 2001 From: Nikita Mazurenko Date: Fri, 26 Jun 2026 09:58:20 +0300 Subject: [PATCH 1/6] Fix dashboard config changes not syncing to edge by removing transient from Dashboard.configuration --- .../rpc/processor/BaseEdgeProcessorTest.java | 236 ++++++++++++++++++ .../server/common/data/Dashboard.java | 2 +- 2 files changed, 237 insertions(+), 1 deletion(-) create mode 100644 application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java diff --git a/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java b/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java new file mode 100644 index 0000000000..55f975b1ea --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java @@ -0,0 +1,236 @@ +/** + * ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL + * + * Copyright © 2016-2026 ThingsBoard, Inc. All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of ThingsBoard, Inc. and its suppliers, + * if any. The intellectual and technical concepts contained + * herein are proprietary to ThingsBoard, Inc. + * and its suppliers and may be covered by U.S. and Foreign Patents, + * patents in process, and are protected by trade secret or copyright law. + * + * Dissemination of this information or reproduction of this material is strictly forbidden + * unless prior written permission is obtained from COMPANY. + * + * Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees, + * managers or contractors who have executed Confidentiality and Non-disclosure agreements + * explicitly covering such access. + * + * The copyright notice above does not evidence any actual or intended publication + * or disclosure of this source code, which includes + * information that is confidential and/or proprietary, and is a trade secret, of COMPANY. + * ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE, + * OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT + * THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED, + * AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES. + * THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION + * DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS, + * OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART. + */ +package org.thingsboard.server.service.edge.rpc.processor; + +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import org.junit.jupiter.api.Test; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.device.data.DeviceData; +import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration; +import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.gen.transport.TransportProtos; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +public class BaseEdgeProcessorTest { + + private final BaseEdgeProcessor processor = new BaseEdgeProcessor() { + @Override + public ListenableFuture processEntityNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto msg) { + return Futures.immediateFuture(null); + } + }; + + @Test + public void dashboardSaveRequiredWhenConfigurationChanges() { + DashboardId dashboardId = new DashboardId(UUID.randomUUID()); + Dashboard current = new Dashboard(dashboardId); + current.setTitle("Dashboard"); + current.setConfiguration(JacksonUtil.toJsonNode("{\"widgets\":{\"w1\":{\"type\":\"timeseries\"}}}")); + + Dashboard updated = new Dashboard(current); + updated.setConfiguration(JacksonUtil.toJsonNode("{\"widgets\":{\"w1\":{\"type\":\"latest\"}}}")); + + assertThat(processor.isSaveRequired(current, updated)).isTrue(); + } + + @Test + public void dashboardSaveNotRequiredWhenOnlyVersionChanges() { + DashboardId dashboardId = new DashboardId(UUID.randomUUID()); + Dashboard current = new Dashboard(dashboardId); + current.setTitle("Dashboard"); + current.setConfiguration(JacksonUtil.toJsonNode("{\"widgets\":{\"w1\":{\"type\":\"timeseries\"}}}")); + current.setVersion(1L); + + Dashboard updated = new Dashboard(current); + updated.setVersion(2L); + + assertThat(processor.isSaveRequired(current, updated)).isFalse(); + } + + @Test + public void dashboardSaveNotRequiredWhenIdentical() { + DashboardId dashboardId = new DashboardId(UUID.randomUUID()); + Dashboard current = new Dashboard(dashboardId); + current.setTitle("Dashboard"); + current.setConfiguration(JacksonUtil.toJsonNode("{\"widgets\":{\"w1\":{\"type\":\"timeseries\"}}}")); + + Dashboard updated = new Dashboard(current); + + assertThat(processor.isSaveRequired(current, updated)).isFalse(); + } + + @Test + public void deviceSaveRequiredWhenDeviceDataChanges() { + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + Device current = new Device(deviceId); + current.setName("Device"); + current.setDeviceData(deviceDataWithTransport()); + + Device updated = new Device(current); + DeviceData withoutTransport = new DeviceData(); + withoutTransport.setConfiguration(new DefaultDeviceConfiguration()); + updated.setDeviceData(withoutTransport); + + assertThat(processor.isSaveRequired(current, updated)).isTrue(); + } + + @Test + public void deviceSaveRequiredWhenAdditionalInfoChanges() { + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + Device current = new Device(deviceId); + current.setName("Device"); + current.setAdditionalInfo(JacksonUtil.toJsonNode("{\"description\":\"old\"}")); + + Device updated = new Device(current); + updated.setAdditionalInfo(JacksonUtil.toJsonNode("{\"description\":\"new\"}")); + + assertThat(processor.isSaveRequired(current, updated)).isTrue(); + } + + @Test + public void deviceSaveNotRequiredWhenOnlyVersionChanges() { + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + Device current = new Device(deviceId); + current.setName("Device"); + current.setDeviceData(deviceDataWithTransport()); + current.setAdditionalInfo(JacksonUtil.toJsonNode("{\"description\":\"same\"}")); + current.setVersion(1L); + + Device updated = new Device(current); + updated.setVersion(2L); + + assertThat(processor.isSaveRequired(current, updated)).isFalse(); + } + + @Test + public void assetSaveRequiredWhenAdditionalInfoChanges() { + AssetId assetId = new AssetId(UUID.randomUUID()); + Asset current = new Asset(assetId); + current.setName("Asset"); + current.setAdditionalInfo(JacksonUtil.toJsonNode("{\"description\":\"old\"}")); + + Asset updated = new Asset(current); + updated.setAdditionalInfo(JacksonUtil.toJsonNode("{\"description\":\"new\"}")); + + assertThat(processor.isSaveRequired(current, updated)).isTrue(); + } + + @Test + public void assetSaveNotRequiredWhenOnlyVersionChanges() { + AssetId assetId = new AssetId(UUID.randomUUID()); + Asset current = new Asset(assetId); + current.setName("Asset"); + current.setAdditionalInfo(JacksonUtil.toJsonNode("{\"description\":\"same\"}")); + current.setVersion(1L); + + Asset updated = new Asset(current); + updated.setVersion(2L); + + assertThat(processor.isSaveRequired(current, updated)).isFalse(); + } + + @Test + public void entityViewSaveRequiredWhenAdditionalInfoChanges() { + EntityViewId entityViewId = new EntityViewId(UUID.randomUUID()); + EntityView current = new EntityView(entityViewId); + current.setName("EntityView"); + current.setAdditionalInfo(JacksonUtil.toJsonNode("{\"description\":\"old\"}")); + + EntityView updated = new EntityView(current); + updated.setAdditionalInfo(JacksonUtil.toJsonNode("{\"description\":\"new\"}")); + + assertThat(processor.isSaveRequired(current, updated)).isTrue(); + } + + @Test + public void entityViewSaveNotRequiredWhenOnlyVersionChanges() { + EntityViewId entityViewId = new EntityViewId(UUID.randomUUID()); + EntityView current = new EntityView(entityViewId); + current.setName("EntityView"); + current.setAdditionalInfo(JacksonUtil.toJsonNode("{\"description\":\"same\"}")); + current.setVersion(1L); + + EntityView updated = new EntityView(current); + updated.setVersion(2L); + + assertThat(processor.isSaveRequired(current, updated)).isFalse(); + } + + @Test + public void userSaveRequiredWhenAdditionalInfoChanges() { + UserId userId = new UserId(UUID.randomUUID()); + User current = new User(userId); + current.setEmail("user@thingsboard.io"); + current.setAdditionalInfo(JacksonUtil.toJsonNode("{\"description\":\"old\"}")); + + User updated = new User(current); + updated.setAdditionalInfo(JacksonUtil.toJsonNode("{\"description\":\"new\"}")); + + assertThat(processor.isSaveRequired(current, updated)).isTrue(); + } + + @Test + public void userSaveNotRequiredWhenOnlyVersionChanges() { + UserId userId = new UserId(UUID.randomUUID()); + User current = new User(userId); + current.setEmail("user@thingsboard.io"); + current.setAdditionalInfo(JacksonUtil.toJsonNode("{\"description\":\"same\"}")); + current.setVersion(1L); + + User updated = new User(current); + updated.setVersion(2L); + + assertThat(processor.isSaveRequired(current, updated)).isFalse(); + } + + private static DeviceData deviceDataWithTransport() { + DeviceData deviceData = new DeviceData(); + deviceData.setConfiguration(new DefaultDeviceConfiguration()); + deviceData.setTransportConfiguration(new DefaultDeviceTransportConfiguration()); + return deviceData; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Dashboard.java b/common/data/src/main/java/org/thingsboard/server/common/data/Dashboard.java index 2c65a3dfc8..2666663d97 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/Dashboard.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Dashboard.java @@ -38,7 +38,7 @@ public class Dashboard extends DashboardInfo implements ExportableEntity Date: Fri, 26 Jun 2026 10:56:01 +0300 Subject: [PATCH 2/6] Fix license header on BaseEdgeProcessorTest --- .../rpc/processor/BaseEdgeProcessorTest.java | 35 ++++++------------- 1 file changed, 10 insertions(+), 25 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java b/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java index 55f975b1ea..430df22548 100644 --- a/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java +++ b/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java @@ -1,32 +1,17 @@ /** - * ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL + * Copyright © 2016-2026 The Thingsboard Authors * - * Copyright © 2016-2026 ThingsBoard, Inc. All Rights Reserved. + * 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 * - * NOTICE: All information contained herein is, and remains - * the property of ThingsBoard, Inc. and its suppliers, - * if any. The intellectual and technical concepts contained - * herein are proprietary to ThingsBoard, Inc. - * and its suppliers and may be covered by U.S. and Foreign Patents, - * patents in process, and are protected by trade secret or copyright law. + * http://www.apache.org/licenses/LICENSE-2.0 * - * Dissemination of this information or reproduction of this material is strictly forbidden - * unless prior written permission is obtained from COMPANY. - * - * Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees, - * managers or contractors who have executed Confidentiality and Non-disclosure agreements - * explicitly covering such access. - * - * The copyright notice above does not evidence any actual or intended publication - * or disclosure of this source code, which includes - * information that is confidential and/or proprietary, and is a trade secret, of COMPANY. - * ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE, - * OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT - * THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED, - * AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES. - * THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION - * DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS, - * OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART. + * 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. */ package org.thingsboard.server.service.edge.rpc.processor; From 632193cbdfb5cfac54dfcfb3bc79882e556111f8 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Tue, 30 Jun 2026 12:30:42 +0300 Subject: [PATCH 3/6] Reserve dynamic ports for OpenAPI spec generation to avoid port conflicts --- application/pom.xml | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/application/pom.xml b/application/pom.xml index f6ceba95a1..6a976bf846 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -507,12 +507,31 @@ + + org.codehaus.mojo + build-helper-maven-plugin + + + + reserve-openapi-ports + initialize + reserve-network-port + + + jmx.port + http.port + + + + + org.springframework.boot spring-boot-maven-plugin org.thingsboard.server.ThingsboardServerApplication - 9001 + ${jmx.port} @@ -532,6 +551,7 @@ --spring.config.name=thingsboard --spring.profiles.active=openapi + --server.port=${http.port} 180 2000 @@ -557,7 +577,7 @@ - http://localhost:8080/v3/api-docs/thingsboard + http://localhost:${http.port}/v3/api-docs/thingsboard openapi.json ${project.build.directory} From c435bcdf6f10fae8ae029bce8ba593989ecf9c2b Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Tue, 30 Jun 2026 14:54:23 +0200 Subject: [PATCH 4/6] Used shared ShortNumberPipe for API usage value formatting --- .../lib/cards/api-usage-widget.component.ts | 29 ++++--------------- .../usage-info-widget.component.html | 10 +++---- 2 files changed, 10 insertions(+), 29 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.ts index b41f5ed02a..af315b226a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.ts @@ -30,6 +30,7 @@ import { ApiUsageWidgetSettings, getUniqueDataKeys } from '@home/components/widget/lib/settings/cards/api-usage-settings.component.models'; +import { ShortNumberPipe } from '@shared/pipe/short-number.pipe'; @Component({ selector: 'tb-api-usage-widget', @@ -57,18 +58,12 @@ export class ApiUsageWidgetComponent implements OnInit, OnDestroy { noDataDisplayMessageText: string; private contentResize$: ResizeObserver; - private powers: {key: string, value: number}[] = [ - { key: 'Q', value: 1e15 }, - { key: 'T', value: 1e12 }, - { key: 'B', value: 1e9 }, - { key: 'M', value: 1e6 }, - { key: 'K', value: 1e3 } - ]; constructor(private imagePipe: ImagePipe, private utils: UtilsService, private sanitizer: DomSanitizer, - private cd: ChangeDetectorRef) { + private cd: ChangeDetectorRef, + private shortNumberPipe: ShortNumberPipe) { } ngOnInit(): void { @@ -95,8 +90,8 @@ export class ApiUsageWidgetComponent implements OnInit, OnDestroy { const progress = (this.isFiniteNumber(data[0][key.maxLimit.key]) && data[0][key.maxLimit.key] !== 0) ? Math.min(100, ((data[0][key.current.key] / data[0][key.maxLimit.key]) * 100)) : 0; key.progress = isFinite(progress) ? progress : 0; key.status.value = data[0][key.status.key] ? data[0][key.status.key].toLowerCase() : 'enabled'; - key.maxLimit.value = this.isFiniteNumber(data[0][key.maxLimit.key]) && data[0][key.maxLimit.key] !== 0 ? this.toShortNumber(data[0][key.maxLimit.key]) : '∞'; - key.current.value = this.isFiniteNumber(data[0][key.current.key]) ? this.toShortNumber(data[0][key.current.key]) : 0; + key.maxLimit.value = this.isFiniteNumber(data[0][key.maxLimit.key]) && data[0][key.maxLimit.key] !== 0 ? this.shortNumberPipe.transform(data[0][key.maxLimit.key]) : '∞'; + key.current.value = this.isFiniteNumber(data[0][key.current.key]) ? this.shortNumberPipe.transform(data[0][key.current.key], {roundDown: true}) : 0; }); this.cd.detectChanges(); } @@ -145,20 +140,6 @@ export class ApiUsageWidgetComponent implements OnInit, OnDestroy { } } - private toShortNumber(number: any, decimals = 1) { - if (!Number.isFinite(number) || number < 0) { - return '0'; - } - for (const power of this.powers) { - if (number >= power.value) { - const reduced = number / power.value; - const rounded = Number(reduced.toFixed(decimals)); - return `${rounded}${power.key}`; - } - } - return `${Number(number.toFixed(decimals))}`; - } - public onInit() { const borderRadius = this.ctx.$widgetElement.css('borderRadius'); this.overlayStyle = {...this.overlayStyle, ...{borderRadius}}; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.html index d23b7b8e50..92ef58c640 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.html @@ -63,11 +63,11 @@
-
{{ usageInfo?.transportMessages | shortNumber }} / {{ maxValue(usageInfo?.maxTransportMessages) }}
-
{{ usageInfo?.jsExecutions | shortNumber }} / {{ maxValue(usageInfo?.maxJsExecutions) }}
-
{{ usageInfo?.alarms | shortNumber }} / {{ maxValue(usageInfo?.maxAlarms) }}
-
{{ usageInfo?.emails | shortNumber }} / {{ maxValue(usageInfo?.maxEmails) }}
-
{{ usageInfo?.sms | shortNumber }} / {{ maxValue(usageInfo?.maxSms) }}
+
{{ usageInfo?.transportMessages | shortNumber: {roundDown: true} }} / {{ maxValue(usageInfo?.maxTransportMessages) }}
+
{{ usageInfo?.jsExecutions | shortNumber: {roundDown: true} }} / {{ maxValue(usageInfo?.maxJsExecutions) }}
+
{{ usageInfo?.alarms | shortNumber: {roundDown: true} }} / {{ maxValue(usageInfo?.maxAlarms) }}
+
{{ usageInfo?.emails | shortNumber: {roundDown: true} }} / {{ maxValue(usageInfo?.maxEmails) }}
+
{{ usageInfo?.sms | shortNumber: {roundDown: true} }} / {{ maxValue(usageInfo?.maxSms) }}
From 091eebb40c4e9a0638fc2be900916de23ee34405 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 30 Jun 2026 18:58:36 +0300 Subject: [PATCH 5/6] Fix stray double space in api-usage-widget ternary --- .../components/widget/lib/cards/api-usage-widget.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.ts index af315b226a..65829ea723 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.ts @@ -90,7 +90,7 @@ export class ApiUsageWidgetComponent implements OnInit, OnDestroy { const progress = (this.isFiniteNumber(data[0][key.maxLimit.key]) && data[0][key.maxLimit.key] !== 0) ? Math.min(100, ((data[0][key.current.key] / data[0][key.maxLimit.key]) * 100)) : 0; key.progress = isFinite(progress) ? progress : 0; key.status.value = data[0][key.status.key] ? data[0][key.status.key].toLowerCase() : 'enabled'; - key.maxLimit.value = this.isFiniteNumber(data[0][key.maxLimit.key]) && data[0][key.maxLimit.key] !== 0 ? this.shortNumberPipe.transform(data[0][key.maxLimit.key]) : '∞'; + key.maxLimit.value = this.isFiniteNumber(data[0][key.maxLimit.key]) && data[0][key.maxLimit.key] !== 0 ? this.shortNumberPipe.transform(data[0][key.maxLimit.key]) : '∞'; key.current.value = this.isFiniteNumber(data[0][key.current.key]) ? this.shortNumberPipe.transform(data[0][key.current.key], {roundDown: true}) : 0; }); this.cd.detectChanges(); From 9d3897764377b629f4d49efac5e3b5a44e95500c Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Wed, 1 Jul 2026 09:28:17 +0200 Subject: [PATCH 6/6] Minor fixes --- .../lib/home-page/usage-info-widget.component.html | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.html index 92ef58c640..0f3f35f172 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.html @@ -36,11 +36,11 @@
-
{{ usageInfo?.devices | shortNumber }} / {{ maxValue(usageInfo?.maxDevices) }}
-
{{ usageInfo?.assets | shortNumber }} / {{ maxValue(usageInfo?.maxAssets) }}
-
{{ usageInfo?.users | shortNumber }} / {{ maxValue(usageInfo?.maxUsers) }}
-
{{ usageInfo?.dashboards | shortNumber }} / {{ maxValue(usageInfo?.maxDashboards) }}
-
{{ usageInfo?.customers | shortNumber }} / {{ maxValue(usageInfo?.maxCustomers) }}
+
{{ usageInfo?.devices | shortNumber: {roundDown: true} }} / {{ maxValue(usageInfo?.maxDevices) }}
+
{{ usageInfo?.assets | shortNumber: {roundDown: true} }} / {{ maxValue(usageInfo?.maxAssets) }}
+
{{ usageInfo?.users | shortNumber: {roundDown: true} }} / {{ maxValue(usageInfo?.maxUsers) }}
+
{{ usageInfo?.dashboards | shortNumber: {roundDown: true} }} / {{ maxValue(usageInfo?.maxDashboards) }}
+
{{ usageInfo?.customers | shortNumber: {roundDown: true} }} / {{ maxValue(usageInfo?.maxCustomers) }}