Browse Source

Merge remote-tracking branch 'origin/master' into feature/dashboard-tb-logo-as-navigate-home

pull/14057/head
deaflynx 1 year ago
parent
commit
448ce31397
  1. 1
      application/src/main/java/org/thingsboard/server/controller/ImageController.java
  2. 4
      application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java
  3. 31
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java
  4. 2
      application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java
  5. 2
      application/src/main/resources/thingsboard.yml
  6. 61
      application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java
  7. 50
      application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java
  8. 3
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/bundle/MobileAppBundle.java
  9. 1
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRule.java
  10. 2
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRuleConfig.java
  11. 12
      common/message/src/main/java/org/thingsboard/server/common/msg/tools/SchedulerUtils.java
  12. 4
      ui-ngx/src/app/core/interceptors/global-http-interceptor.ts
  13. 7
      ui-ngx/src/app/core/utils.ts
  14. 10
      ui-ngx/src/app/modules/home/components/relation/relation-table.component.html
  15. 131
      ui-ngx/src/app/modules/home/components/vc/complex-version-create.component.html
  16. 100
      ui-ngx/src/app/modules/home/components/vc/complex-version-load.component.html
  17. 158
      ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.html
  18. 4
      ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.ts
  19. 127
      ui-ngx/src/app/modules/home/components/vc/entity-types-version-load.component.html
  20. 33
      ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.html
  21. 99
      ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.html
  22. 2
      ui-ngx/src/app/modules/home/components/vc/remove-other-entities-confirm.component.html
  23. 8
      ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts
  24. 2
      ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html
  25. 3
      ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.scss
  26. 32
      ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.ts
  27. 11
      ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.ts
  28. 24
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-settings.component.models.ts
  29. 24
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.ts
  30. 2
      ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html
  31. 2
      ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts
  32. 21
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts
  33. 6
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html
  34. 7
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts
  35. 15
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard.module.ts
  36. 40
      ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts
  37. 57
      ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.html
  38. 89
      ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.ts
  39. 2
      ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html
  40. 9
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts
  41. 29
      ui-ngx/src/app/shared/components/time/datapoints-limit.component.ts
  42. 5
      ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.html
  43. 5
      ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.ts
  44. 7
      ui-ngx/src/app/shared/models/time/time.models.ts
  45. 88
      ui-ngx/src/app/shared/models/widget/home-widgets/api-usage-model.definition.ts
  46. 62
      ui-ngx/src/app/shared/models/widget/maps/map-model.definition.ts
  47. 35
      ui-ngx/src/app/shared/models/widget/maps/map.models.ts
  48. 4
      ui-ngx/src/app/shared/models/widget/widget-model.definition.ts
  49. 2440
      ui-ngx/src/assets/dashboard/api_usage.json
  50. 6
      ui-ngx/src/assets/locale/locale.constant-en_US.json

1
application/src/main/java/org/thingsboard/server/controller/ImageController.java

@ -300,6 +300,7 @@ public class ImageController extends BaseController {
tbImageService.putETag(cacheKey, descriptor.getEtag());
var result = ResponseEntity.ok()
.header("Content-Type", descriptor.getMediaType())
.header("Content-Security-Policy", "default-src 'none'")
.eTag(descriptor.getEtag());
if (!cacheKey.isPublic()) {
result

4
application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java

@ -442,13 +442,13 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService
boolean check(long threshold, long warnThreshold, long value);
}
private void checkStartOfNextCycle() {
public void checkStartOfNextCycle() {
updateLock.lock();
try {
long now = System.currentTimeMillis();
myUsageStates.values().forEach(state -> {
if ((state.getNextCycleTs() < now) && (now - state.getNextCycleTs() < TimeUnit.HOURS.toMillis(1))) {
state.setCycles(state.getNextCycleTs(), SchedulerUtils.getStartOfNextNextMonth());
state.setCycles(state.getNextCycleTs(), SchedulerUtils.getStartOfNextMonth());
if (log.isTraceEnabled()) {
log.trace("[{}][{}] Updating state cycles (currentCycleTs={},nextCycleTs={})", state.getTenantId(), state.getEntityId(), state.getCurrentCycleTs(), state.getNextCycleTs());
}

31
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java

@ -93,14 +93,13 @@ import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAS
@TbCoreComponent
public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase implements EdgeRpcService {
private static final int DESTROY_SESSION_MAX_ATTEMPTS = 10;
private final ConcurrentMap<EdgeId, EdgeGrpcSession> sessions = new ConcurrentHashMap<>();
private final ConcurrentMap<EdgeId, Lock> sessionNewEventsLocks = new ConcurrentHashMap<>();
private final Map<EdgeId, Boolean> sessionNewEvents = new HashMap<>();
private final ConcurrentMap<EdgeId, ScheduledFuture<?>> sessionEdgeEventChecks = new ConcurrentHashMap<>();
private final ConcurrentMap<UUID, Consumer<FromEdgeSyncResponse>> localSyncEdgeRequests = new ConcurrentHashMap<>();
private final ConcurrentMap<EdgeId, Boolean> edgeEventsMigrationProcessed = new ConcurrentHashMap<>();
private final List<EdgeGrpcSession> zombieSessions = new ArrayList<>();
@Value("${edges.rpc.port}")
private int rpcPort;
@ -193,7 +192,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
this.edgeEventProcessingExecutorService = ThingsBoardExecutors.newScheduledThreadPool(schedulerPoolSize, "edge-event-check-scheduler");
this.sendDownlinkExecutorService = ThingsBoardExecutors.newScheduledThreadPool(sendSchedulerPoolSize, "edge-send-scheduler");
this.executorService = ThingsBoardExecutors.newSingleThreadScheduledExecutor("edge-service");
this.executorService.scheduleAtFixedRate(this::destroyKafkaSessionIfDisconnectedAndConsumerActive, 60, 60, TimeUnit.SECONDS);
this.executorService.scheduleAtFixedRate(this::cleanupZombieSessions, 60, 60, TimeUnit.SECONDS);
log.info("Edge RPC service initialized!");
}
@ -518,14 +517,10 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
private void destroySession(EdgeGrpcSession session) {
try (session) {
for (int i = 0; i < DESTROY_SESSION_MAX_ATTEMPTS; i++) {
if (session.destroy()) {
break;
} else {
try {
Thread.sleep(100);
} catch (InterruptedException ignored) {}
}
if (!session.destroy()) {
log.warn("[{}][{}] Session destroy failed for edge [{}] with session id [{}]. Adding to zombie queue for later cleanup.",
session.getTenantId(), session.getEdge().getId(), session.getEdge().getName(), session.getSessionId());
zombieSessions.add(session);
}
}
}
@ -634,7 +629,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
}
}
private void destroyKafkaSessionIfDisconnectedAndConsumerActive() {
private void cleanupZombieSessions() {
try {
List<EdgeId> toRemove = new ArrayList<>();
for (EdgeGrpcSession session : sessions.values()) {
@ -655,9 +650,19 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
}
}
}
zombieSessions.removeIf(zombie -> {
if (zombie.destroy()) {
log.info("[{}][{}] Successfully cleaned up zombie session [{}] for edge [{}].",
zombie.getTenantId(), zombie.getEdge().getId(), zombie.getSessionId(), zombie.getEdge().getName());
return true;
} else {
log.warn("[{}][{}] Failed to remove zombie session [{}] for edge [{}].",
zombie.getTenantId(), zombie.getEdge().getId(), zombie.getSessionId(), zombie.getEdge().getName());
return false;
}
});
} catch (Exception e) {
log.warn("Failed to cleanup kafka sessions", e);
}
}
}

2
application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java

@ -238,7 +238,7 @@ public class DefaultEntityQueryService implements EntityQueryService {
entitiesSortOrder = sortOrder;
}
EntityDataPageLink edpl = new EntityDataPageLink(maxEntitiesPerAlarmSubscription, 0, null, entitiesSortOrder);
return new EntityDataQuery(query.getEntityFilter(), edpl, null, null, query.getKeyFilters());
return new EntityDataQuery(query.getEntityFilter(), edpl, query.getEntityFields(), query.getLatestValues(), query.getKeyFilters());
}
@Override

2
application/src/main/resources/thingsboard.yml

@ -1542,7 +1542,7 @@ swagger:
version: "${SWAGGER_VERSION:}"
# The group name (definition) on the API doc UI page.
group_name: "${SWAGGER_GROUP_NAME:thingsboard}"
# Control the initial display state of API operations and tags (none, list or full)
# Control the initial display state of API operations and tags (none or list)
doc_expansion: "${SWAGGER_DOC_EXPANSION:list}"
# Queue configuration parameters

61
application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java

@ -519,6 +519,67 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
Assert.assertEquals(1, filteredAssetAlamData.getTotalElements());
}
@Test
public void testFindAlarmsWithEntityFilterAndLatestValues() throws Exception {
loginTenantAdmin();
List<Device> devices = new ArrayList<>();
List<String> temps = new ArrayList<>();
List<String> deviceNames = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Device device = new Device();
device.setCustomerId(customerId);
device.setName("Device" + i);
device.setType("default");
device.setLabel("testLabel" + (int) (Math.random() * 1000));
device = doPost("/api/device", device, Device.class);
devices.add(device);
deviceNames.add(device.getName());
int temp = i * 10;
temps.add(String.valueOf(temp));
JsonNode content = JacksonUtil.toJsonNode("{\"temperature\": " + temp + "}");
doPost("/api/plugins/telemetry/" + EntityType.DEVICE.name() + "/" + device.getUuidId() + "/timeseries/SERVER_SCOPE", content)
.andExpect(status().isOk());
Thread.sleep(1);
}
for (int i = 0; i < devices.size(); i++) {
Alarm alarm = new Alarm();
alarm.setCustomerId(customerId);
alarm.setOriginator(devices.get(i).getId());
String type = "device alarm" + i;
alarm.setType(type);
alarm.setSeverity(AlarmSeverity.WARNING);
doPost("/api/alarm", alarm, Alarm.class);
Thread.sleep(1);
}
AlarmDataPageLink pageLink = new AlarmDataPageLink();
pageLink.setPage(0);
pageLink.setPageSize(100);
pageLink.setSortOrder(new EntityDataSortOrder(new EntityKey(EntityKeyType.ALARM_FIELD, "created_time")));
List<EntityKey> alarmFields = new ArrayList<>();
alarmFields.add(new EntityKey(EntityKeyType.ALARM_FIELD, "type"));
List<EntityKey> entityFields = new ArrayList<>();
entityFields.add(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"));
List<EntityKey> latestValues = new ArrayList<>();
latestValues.add(new EntityKey(EntityKeyType.TIME_SERIES, "temperature"));
EntityTypeFilter deviceTypeFilter = new EntityTypeFilter();
deviceTypeFilter.setEntityType(EntityType.DEVICE);
AlarmDataQuery deviceAlarmQuery = new AlarmDataQuery(deviceTypeFilter, pageLink, entityFields, latestValues, null, alarmFields);
PageData<AlarmData> alarmPageData = findAlarmsByQueryAndCheck(deviceAlarmQuery, 10);
List<String> retrievedAlarmTemps = alarmPageData.getData().stream().map(alarmData -> alarmData.getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue()).toList();
assertThat(retrievedAlarmTemps).containsExactlyInAnyOrderElementsOf(temps);
List<String> retrievedDeviceNames = alarmPageData.getData().stream().map(alarmData -> alarmData.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()).toList();
assertThat(retrievedDeviceNames).containsExactlyInAnyOrderElementsOf(deviceNames);
}
private void testCountAlarmsByQuery(List<Alarm> alarms) throws Exception {
AlarmCountQuery countQuery = new AlarmCountQuery();

50
application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java

@ -20,9 +20,12 @@ import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.server.common.data.ApiUsageRecordKey;
import org.thingsboard.server.common.data.ApiUsageState;
import org.thingsboard.server.common.data.ApiUsageStateValue;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.id.ApiUsageStateId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
@ -33,8 +36,17 @@ import org.thingsboard.server.dao.usagerecord.ApiUsageStateService;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import java.lang.reflect.Field;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static java.time.ZoneOffset.UTC;
import static java.time.temporal.ChronoField.DAY_OF_MONTH;
import static java.time.temporal.ChronoUnit.MONTHS;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
@DaoSqlTest
@ -48,6 +60,7 @@ public class DefaultTbApiUsageStateServiceTest extends AbstractControllerTest {
private TenantId tenantId;
private Tenant savedTenant;
private TenantProfile savedTenantProfile;
private static final int MAX_ENABLE_VALUE = 5000;
private static final long VALUE_WARNING = 4500L;
@ -59,7 +72,7 @@ public class DefaultTbApiUsageStateServiceTest extends AbstractControllerTest {
loginSysAdmin();
TenantProfile tenantProfile = createTenantProfile();
TenantProfile savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class);
savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class);
Assert.assertNotNull(savedTenantProfile);
Tenant tenant = new Tenant();
@ -109,6 +122,41 @@ public class DefaultTbApiUsageStateServiceTest extends AbstractControllerTest {
assertEquals(ApiUsageStateValue.DISABLED, apiUsageStateService.findTenantApiUsageState(tenantId).getDbStorageState());
}
@Test
public void checkStartOfNextCycle_setsNextCycleToNextMonth() throws Exception {
ApiUsageState apiUsageState = new ApiUsageState(new ApiUsageStateId(UUID.randomUUID()));
apiUsageState.setDbStorageState(ApiUsageStateValue.ENABLED);
apiUsageState.setAlarmExecState(ApiUsageStateValue.ENABLED);
apiUsageState.setSmsExecState(ApiUsageStateValue.ENABLED);
apiUsageState.setTbelExecState(ApiUsageStateValue.ENABLED);
apiUsageState.setReExecState(ApiUsageStateValue.ENABLED);
apiUsageState.setTransportState(ApiUsageStateValue.ENABLED);
apiUsageState.setEmailExecState(ApiUsageStateValue.ENABLED);
apiUsageState.setJsExecState(ApiUsageStateValue.ENABLED);
apiUsageState.setTenantId(tenantId);
apiUsageState.setEntityId(tenantId);
long now = System.currentTimeMillis();
long currentCycleTs = now - TimeUnit.DAYS.toMillis(30);
long nextCycleTs = now - TimeUnit.MINUTES.toMillis(5); // < 1h ago
TenantApiUsageState tenantApiUsageState = new TenantApiUsageState(savedTenantProfile, apiUsageState);
tenantApiUsageState.setCycles(currentCycleTs, nextCycleTs);
Map<EntityId, BaseApiUsageState> map = new HashMap<>();
map.put(tenantId, tenantApiUsageState);
Field fieldToSet = DefaultTbApiUsageStateService.class.getDeclaredField("myUsageStates");
fieldToSet.setAccessible(true);
fieldToSet.set(service, map);
service.checkStartOfNextCycle();
long firstOfNextMonth = LocalDate.now()
.with((temporal) -> temporal.with(DAY_OF_MONTH, 1)
.plus(1, MONTHS))
.atStartOfDay(UTC).toInstant().toEpochMilli();
assertThat(tenantApiUsageState.getNextCycleTs()).isEqualTo(firstOfNextMonth);
}
private TenantProfile createTenantProfile() {
TenantProfile tenantProfile = new TenantProfile();
tenantProfile.setName("Tenant Profile");

3
common/data/src/main/java/org/thingsboard/server/common/data/mobile/bundle/MobileAppBundle.java

@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.id.MobileAppId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.mobile.layout.MobileLayoutConfig;
import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss;
@EqualsAndHashCode(callSuper = true)
@Data
@ -40,9 +41,11 @@ public class MobileAppBundle extends BaseData<MobileAppBundleId> implements HasT
private TenantId tenantId;
@Schema(description = "Application bundle title. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank
@NoXss
@Length(fieldName = "title")
private String title;
@Schema(description = "Application bundle description.")
@NoXss
@Length(fieldName = "description")
private String description;
@Schema(description = "Android application id")

1
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRule.java

@ -62,6 +62,7 @@ public class NotificationRule extends BaseData<NotificationRuleId> implements Ha
@Valid
private NotificationRuleRecipientsConfig recipientsConfig;
@Valid
private NotificationRuleConfig additionalConfig;
private NotificationRuleId externalId;

2
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRuleConfig.java

@ -16,12 +16,14 @@
package org.thingsboard.server.common.data.notification.rule;
import lombok.Data;
import org.thingsboard.server.common.data.validation.NoXss;
import java.io.Serializable;
@Data
public class NotificationRuleConfig implements Serializable {
@NoXss
private String description;
}

12
common/message/src/main/java/org/thingsboard/server/common/msg/tools/SchedulerUtils.java

@ -60,16 +60,4 @@ public class SchedulerUtils {
return LocalDate.now(UTC).with(TemporalAdjusters.firstDayOfNextMonth()).atStartOfDay(zoneId).toInstant().toEpochMilli();
}
public static long getStartOfNextNextMonth() {
return getStartOfNextNextMonth(UTC);
}
public static long getStartOfNextNextMonth(ZoneId zoneId) {
return LocalDate.now(UTC).with(firstDayOfNextNextMonth()).atStartOfDay(zoneId).toInstant().toEpochMilli();
}
public static TemporalAdjuster firstDayOfNextNextMonth() {
return (temporal) -> temporal.with(DAY_OF_MONTH, 1).plus(2, MONTHS);
}
}

4
ui-ngx/src/app/core/interceptors/global-http-interceptor.ts

@ -30,6 +30,7 @@ import { DialogService } from '@core/services/dialog.service';
import { TranslateService } from '@ngx-translate/core';
import { parseHttpErrorMessage } from '@core/utils';
import { getInterceptorConfig } from './interceptor.util';
import { DomSanitizer } from '@angular/platform-browser';
const tmpHeaders = {};
@ -46,6 +47,7 @@ export class GlobalHttpInterceptor implements HttpInterceptor {
private dialogService: DialogService,
private translate: TranslateService,
private authService: AuthService,
private sanitizer: DomSanitizer
) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
@ -129,7 +131,7 @@ export class GlobalHttpInterceptor implements HttpInterceptor {
}
if (unhandled && !ignoreErrors) {
const errorMessageWithTimeout = parseHttpErrorMessage(errorResponse, this.translate, req.responseType);
const errorMessageWithTimeout = parseHttpErrorMessage(errorResponse, this.translate, req.responseType, this.sanitizer);
this.showError(errorMessageWithTimeout.message, errorMessageWithTimeout.timeout);
}
return throwError(() => errorResponse);

7
ui-ngx/src/app/core/utils.ts

@ -32,6 +32,8 @@ import {
isNotEmptyTbFunction,
TbFunction
} from '@shared/models/js-function.models';
import { DomSanitizer } from '@angular/platform-browser';
import { SecurityContext } from '@angular/core';
const varsRegex = /\${([^}]*)}/g;
@ -854,7 +856,7 @@ export function getEntityDetailsPageURL(id: string, entityType: EntityType): str
}
export function parseHttpErrorMessage(errorResponse: HttpErrorResponse,
translate: TranslateService, responseType?: string): {message: string; timeout: number} {
translate: TranslateService, responseType?: string, sanitizer?:DomSanitizer): {message: string; timeout: number} {
let error = null;
let errorMessage: string;
let timeout = 0;
@ -882,6 +884,9 @@ export function parseHttpErrorMessage(errorResponse: HttpErrorResponse,
errorText += errorKey ? translate.instant(errorKey) : errorResponse.statusText;
errorMessage = errorText;
}
if(sanitizer) {
errorMessage = sanitizer.sanitize(SecurityContext.HTML,errorMessage);
}
return {message: errorMessage, timeout};
}

10
ui-ngx/src/app/modules/home/components/relation/relation-table.component.html

@ -117,9 +117,13 @@
<ng-container *ngIf="direction === directions.FROM" matColumnDef="toName">
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 33%"> {{ 'relation.to-entity-name' | translate }} </mat-header-cell>
<mat-cell *matCellDef="let relation">
<a class="link" [routerLink]="relation.entityURL">
{{ relation.toName | customTranslate }}
</a>
@if(relation.entityURL){
<a class="link" [routerLink]="relation.entityURL">
{{ relation.toName | customTranslate }}
</a>
} @else {
{{ relation.toName | customTranslate }}
}
</mat-cell>
</ng-container>
<ng-container *ngIf="direction === directions.TO" matColumnDef="fromEntityTypeName">

131
ui-ngx/src/app/modules/home/components/vc/complex-version-create.component.html

@ -15,76 +15,79 @@
limitations under the License.
-->
<section *ngIf="!versionCreateResult$" class="flex flex-col" style="height: 100%;">
<mat-toolbar>
<h2>{{ 'version-control.create-entities-version' | translate }}</h2>
<span class="flex-1"></span>
</mat-toolbar>
<mat-progress-bar color="warn" style="z-index: 10; width: 100%; margin-bottom: -4px;" mode="indeterminate"
*ngIf="isLoading$ | async">
</mat-progress-bar>
<form [formGroup]="createVersionFormGroup" class="flex flex-col" style="flex: 1; padding-top: 16px; overflow: auto;">
<div class="flex flex-col gt-sm:flex-row gt-sm:gap-2">
<tb-branch-autocomplete
class="flex-1"
required
formControlName="branch">
</tb-branch-autocomplete>
<mat-form-field class="mat-block flex-1">
<mat-label translate>version-control.version-name</mat-label>
<input required matInput formControlName="versionName">
<mat-error *ngIf="createVersionFormGroup.get('versionName').hasError('required') ||
createVersionFormGroup.get('versionName').hasError('pattern')">
{{ 'version-control.version-name-required' | translate }}
</mat-error>
@if (!versionCreateResult$) {
<section class="flex flex-col" style="height: 100%;">
<mat-toolbar>
<h2>{{ 'version-control.create-entities-version' | translate }}</h2>
<span class="flex-1"></span>
</mat-toolbar>
<mat-progress-bar color="warn" style="z-index: 10; width: 100%; margin-bottom: -4px;" mode="indeterminate"
[class.hidden]="(isLoading$ | async) === false">
</mat-progress-bar>
<form [formGroup]="createVersionFormGroup" class="flex flex-col" style="flex: 1; padding-top: 16px; overflow: auto;">
<div class="flex flex-col gt-sm:flex-row gt-sm:gap-2">
<tb-branch-autocomplete
class="flex-1"
appearance="outline"
required
formControlName="branch">
</tb-branch-autocomplete>
<mat-form-field class="mat-block flex-1" appearance="outline">
<mat-label translate>version-control.version-name</mat-label>
<input required matInput formControlName="versionName">
<mat-error [class.hidden]="!(createVersionFormGroup.get('versionName').hasError('required') ||
createVersionFormGroup.get('versionName').hasError('pattern'))">
{{ 'version-control.version-name-required' | translate }}
</mat-error>
</mat-form-field>
</div>
<mat-form-field class="mat-block" style="margin-bottom: 24px;" appearance="outline">
<mat-label translate>version-control.default-sync-strategy</mat-label>
<mat-select required formControlName="syncStrategy">
@for (strategy of syncStrategies; track strategy) {
<mat-option [value]="strategy">
{{syncStrategyTranslations.get(strategy) | translate}}
</mat-option>
}
</mat-select>
<mat-hint [innerHTML]="(syncStrategyHints.get(createVersionFormGroup.get('syncStrategy').value) | translate) | safe:'html'"></mat-hint>
</mat-form-field>
<tb-entity-types-version-create
formControlName="entityTypes">
</tb-entity-types-version-create>
</form>
<div class="flex items-center justify-end gap-2" style="padding-top: 16px;">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="button"
(click)="export()"
[disabled]="(isLoading$ | async) || createVersionFormGroup.invalid || !createVersionFormGroup.dirty">
{{ 'action.create' | translate }}
</button>
</div>
<mat-form-field class="mat-block" style="margin-bottom: 24px;">
<mat-label translate>version-control.default-sync-strategy</mat-label>
<mat-select required formControlName="syncStrategy">
<mat-option *ngFor="let strategy of syncStrategies" [value]="strategy">
{{syncStrategyTranslations.get(strategy) | translate}}
</mat-option>
</mat-select>
<mat-hint [innerHTML]="(syncStrategyHints.get(createVersionFormGroup.get('syncStrategy').value) | translate) | safe:'html'"></mat-hint>
</mat-form-field>
<tb-entity-types-version-create
formControlName="entityTypes">
</tb-entity-types-version-create>
</form>
<div class="flex items-center justify-end gap-2" style="padding-top: 16px;">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="button"
(click)="export()"
[disabled]="(isLoading$ | async) || createVersionFormGroup.invalid || !createVersionFormGroup.dirty">
{{ 'action.create' | translate }}
</button>
</div>
</section>
<section *ngIf="versionCreateResult$">
<section *ngIf="resultMessage">
<div class="mat-headline-6 vc-result-message" [innerHtml]="resultMessage"></div>
</section>
<div *ngIf="(versionCreateResult$ | async)?.done || hasError; else progress" class="flex items-center justify-end gap-2">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.close' | translate }}
</button>
</div>
<ng-template #progress>
} @else {
<div class="mat-headline-6 vc-result-message" [class.hidden]="!resultMessage" [innerHtml]="resultMessage"></div>
@if ((versionCreateResult$ | async)?.done || hasError) {
<div class="flex items-center justify-end gap-2">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.close' | translate }}
</button>
</div>
} @else {
<section class="flex flex-col items-center justify-center">
<div class="mat-headline-6 vc-result-message progress">
<span translate>version-control.creating-version</span>
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</div>
</section>
</ng-template>
</section>
}
}

100
ui-ngx/src/app/modules/home/components/vc/complex-version-load.component.html

@ -15,59 +15,63 @@
limitations under the License.
-->
<section *ngIf="!versionLoadResult$" class="flex flex-col" style="height: 100%;">
<mat-toolbar>
<h2>{{ 'version-control.restore-entities-from-version' | translate: {versionName} }}</h2>
<span class="flex-1"></span>
</mat-toolbar>
<mat-progress-bar color="warn" style="z-index: 10; width: 100%; margin-bottom: -4px;" mode="indeterminate"
*ngIf="isLoading$ | async">
</mat-progress-bar>
<form [formGroup]="loadVersionFormGroup" class="flex flex-col" style="flex: 1; padding: 16px 0; overflow-y: auto; overflow-x: hidden">
<tb-entity-types-version-load
formControlName="entityTypes">
</tb-entity-types-version-load>
</form>
<div tb-hint-tooltip-icon="{{ 'version-control.rollback-on-error-hint' | translate }}" [formGroup]="loadVersionFormGroup">
<mat-slide-toggle formControlName="rollbackOnError">
{{ 'version-control.rollback-on-error' | translate }}
</mat-slide-toggle>
</div>
<div class="flex items-center justify-end gap-2" style="padding-top: 16px;">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="button"
(click)="restore()"
[disabled]="(isLoading$ | async) || loadVersionFormGroup.invalid">
{{ 'action.restore' | translate }}
</button>
</div>
</section>
<section *ngIf="versionLoadResult$">
<div *ngIf="(versionLoadResult$ | async)?.done && !entityTypeLoadResults?.length && !errorMessage" class="mat-headline-6 vc-result-message">
@if (!versionLoadResult$) {
<section class="flex flex-col" style="height: 100%;">
<mat-toolbar>
<h2>{{ 'version-control.restore-entities-from-version' | translate: {versionName} }}</h2>
<span class="flex-1"></span>
</mat-toolbar>
<mat-progress-bar color="warn" style="z-index: 10; width: 100%; margin-bottom: -4px;" mode="indeterminate"
[class.hidden]="(isLoading$ | async) === false">
</mat-progress-bar>
<form [formGroup]="loadVersionFormGroup" class="flex flex-col" style="flex: 1; padding: 16px 0; overflow-y: auto; overflow-x: hidden">
<tb-entity-types-version-load
formControlName="entityTypes">
</tb-entity-types-version-load>
</form>
<div tb-hint-tooltip-icon="{{ 'version-control.rollback-on-error-hint' | translate }}" [formGroup]="loadVersionFormGroup">
<mat-slide-toggle formControlName="rollbackOnError">
{{ 'version-control.rollback-on-error' | translate }}
</mat-slide-toggle>
</div>
<div class="flex items-center justify-end gap-2" style="padding-top: 16px;">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="button"
(click)="restore()"
[disabled]="(isLoading$ | async) || loadVersionFormGroup.invalid">
{{ 'action.restore' | translate }}
</button>
</div>
</section>
} @else {
<div [class.hidden]="!((versionLoadResult$ | async)?.done && !entityTypeLoadResults?.length && !errorMessage)" class="mat-headline-6 vc-result-message">
{{ 'version-control.no-entities-restored' | translate }}
</div>
<div *ngIf="errorMessage" class="mat-headline-6 vc-result-message error" [innerHTML]="errorMessage"></div>
<div *ngFor="let entityTypeLoadResult of entityTypeLoadResults" class="mat-headline-6 vc-result-message">{{ entityTypeLoadResultMessage(entityTypeLoadResult) }}</div>
<div *ngIf="(versionLoadResult$ | async)?.done || hasError; else progress" class="flex items-center justify-end gap-2">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.close' | translate }}
</button>
</div>
<ng-template #progress>
<div [class.hidden]="!errorMessage" class="mat-headline-6 vc-result-message error" [innerHTML]="errorMessage"></div>
@for (entityTypeLoadResult of entityTypeLoadResults; track entityTypeLoadResult.entityType) {
<div class="mat-headline-6 vc-result-message">{{ entityTypeLoadResultMessage(entityTypeLoadResult) }}</div>
}
@if ((versionLoadResult$ | async)?.done || hasError) {
<div class="flex items-center justify-end gap-2">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.close' | translate }}
</button>
</div>
} @else {
<section class="flex flex-col items-center justify-center">
<div class="mat-headline-6 vc-result-message progress">
<span translate>version-control.restoring-entities-from-version</span>
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</div>
</section>
</ng-template>
</section>
}
}

158
ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.html

@ -18,86 +18,90 @@
<fieldset [formGroup]="entityTypesVersionCreateFormGroup" class="fields-group flex flex-col">
<legend class="group-title" translate>version-control.entities-to-export</legend>
<div class="flex flex-col" style="flex: 1; overflow: auto;">
<div *ngFor="let entityTypeFormGroup of entityTypesFormGroupArray(); trackBy: trackByEntityType;
let $index = index; last as isLast;"
class="flex flex-row items-center justify-start" [class.pb-2]="!isLast">
<mat-expansion-panel class="entity-type-config flex-1" [class.load]="loading" [formGroup]="entityTypeFormGroup"
[expanded]="entityTypesFormGroupExpanded(entityTypeFormGroup)" (opened)="loading = false">
<mat-expansion-panel-header>
<div class="flex flex-1 flex-row items-center justify-start">
<mat-panel-title>
<div class="flex flex-1 flex-row items-center justify-start">
<div>{{ entityTypeText(entityTypeFormGroup) }}</div>
</div>
</mat-panel-title>
<span class="flex-1"></span>
<button *ngIf="!disabled" mat-icon-button style="min-width: 40px;"
type="button"
(click)="removeEntityType($index)"
matTooltip="{{ 'action.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
</mat-expansion-panel-header>
<div class="entity-type-config-content flex flex-col gap-2">
<mat-divider></mat-divider>
<div class="flex flex-row xs:flex-col gt-xs:gap-4">
<tb-entity-type-select
showLabel
formControlName="entityType"
required
[filterAllowedEntityTypes]="false"
[allowedEntityTypes]="allowedEntityTypes(entityTypeFormGroup)">
</tb-entity-type-select>
<div class="flex flex-1 flex-row xs:flex-col gt-xs:gap-4" formGroupName="config">
<mat-form-field class="mat-block flex-1">
<mat-label translate>version-control.sync-strategy</mat-label>
<mat-select formControlName="syncStrategy">
<mat-option [value]="'default'">
{{ 'version-control.default' | translate }}
</mat-option>
<mat-option *ngFor="let strategy of syncStrategies" [value]="strategy">
{{syncStrategyTranslations.get(strategy) | translate}}
</mat-option>
</mat-select>
</mat-form-field>
<div class="flex flex-1 flex-col">
<mat-checkbox *ngIf="entityTypeFormGroup.get('entityType').value === entityTypes.DEVICE" formControlName="saveCredentials">
{{ 'version-control.export-credentials' | translate }}
</mat-checkbox>
<mat-checkbox *ngIf="!entityTypesWithoutRelatedData.has(entityTypeFormGroup.get('entityType').value)" formControlName="saveAttributes">
{{ 'version-control.export-attributes' | translate }}
</mat-checkbox>
<mat-checkbox *ngIf="!entityTypesWithoutRelatedData.has(entityTypeFormGroup.get('entityType').value)" formControlName="saveRelations">
{{ 'version-control.export-relations' | translate }}
</mat-checkbox>
<mat-checkbox *ngIf="typesWithCalculatedFields.has(entityTypeFormGroup.get('entityType').value)" formControlName="saveCalculatedFields">
{{ 'version-control.export-calculated-fields' | translate }}
</mat-checkbox>
@for (entityTypeFormGroup of entityTypesFormGroupArray(); track entityTypeFormGroup; let index = $index, isLast = $last) {
<div class="flex flex-row items-center justify-start" [class.pb-2]="!isLast">
<mat-expansion-panel class="entity-type-config flex-1" [class.load]="loading" [formGroup]="entityTypeFormGroup"
[expanded]="entityTypesFormGroupExpanded(entityTypeFormGroup)" (opened)="loading = false">
<mat-expansion-panel-header>
<div class="flex flex-1 flex-row items-center justify-start">
<mat-panel-title>
<div class="flex flex-1 flex-row items-center justify-start">
<div>{{ entityTypeText(entityTypeFormGroup) }}</div>
</div>
</mat-panel-title>
<span class="flex-1"></span>
<button [class.hidden]="disabled" mat-icon-button style="min-width: 40px;"
type="button"
(click)="removeEntityType(index)"
matTooltip="{{ 'action.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
</mat-expansion-panel-header>
<div class="entity-type-config-content flex flex-col gap-2">
<mat-divider></mat-divider>
<div class="flex flex-row xs:flex-col gt-xs:gap-4">
<tb-entity-type-select
showLabel
formControlName="entityType"
appearance="outline"
required
[filterAllowedEntityTypes]="false"
[allowedEntityTypes]="allowedEntityTypes(entityTypeFormGroup)">
</tb-entity-type-select>
<div class="flex flex-1 flex-row xs:flex-col gt-xs:gap-4" formGroupName="config">
<mat-form-field class="mat-block flex-1" appearance="outline">
<mat-label translate>version-control.sync-strategy</mat-label>
<mat-select formControlName="syncStrategy">
<mat-option [value]="'default'">
{{ 'version-control.default' | translate }}
</mat-option>
@for (strategy of syncStrategies; track strategy) {
<mat-option [value]="strategy">
{{syncStrategyTranslations.get(strategy) | translate}}
</mat-option>
}
</mat-select>
</mat-form-field>
<div class="flex flex-1 flex-col">
<mat-checkbox [class.hidden]="entityTypeFormGroup.get('entityType').value !== entityTypes.DEVICE" formControlName="saveCredentials">
{{ 'version-control.export-credentials' | translate }}
</mat-checkbox>
<mat-checkbox [class.hidden]="entityTypesWithoutRelatedData.has(entityTypeFormGroup.get('entityType').value)" formControlName="saveAttributes">
{{ 'version-control.export-attributes' | translate }}
</mat-checkbox>
<mat-checkbox [class.hidden]="entityTypesWithoutRelatedData.has(entityTypeFormGroup.get('entityType').value)" formControlName="saveRelations">
{{ 'version-control.export-relations' | translate }}
</mat-checkbox>
<mat-checkbox [class.hidden]="!typesWithCalculatedFields.has(entityTypeFormGroup.get('entityType').value)" formControlName="saveCalculatedFields">
{{ 'version-control.export-calculated-fields' | translate }}
</mat-checkbox>
</div>
</div>
</div>
<div class="flex flex-row xs:flex-col gt-xs:min-h-19 gt-xs:items-center gt-xs:justify-start gt-xs:gap-4"
formGroupName="config">
<mat-slide-toggle formControlName="allEntities">
{{ 'version-control.all-entities' | translate }}
</mat-slide-toggle>
@if (!entityTypeFormGroup.get('config').get('allEntities').value) {
<tb-entity-list
class="flex-1"
appearance="outline"
subscriptSizing="dynamic"
[entityType]="entityTypeFormGroup.get('entityType').value"
required
formControlName="entityIds">
</tb-entity-list>
}
</div>
</div>
<div class="flex flex-row xs:flex-col gt-xs:min-h-19 gt-xs:items-center gt-xs:justify-start gt-xs:gap-4"
formGroupName="config">
<mat-slide-toggle formControlName="allEntities">
{{ 'version-control.all-entities' | translate }}
</mat-slide-toggle>
<tb-entity-list
class="flex-1"
[class.!hidden]="entityTypeFormGroup.get('config').get('allEntities').value"
[entityType]="entityTypeFormGroup.get('entityType').value"
required
formControlName="entityIds">
</tb-entity-list>
</div>
</div>
</mat-expansion-panel>
</div>
<div *ngIf="!entityTypesFormGroupArray().length">
<span translate
class="tb-prompt flex items-center justify-center">version-control.no-entities-to-export-prompt</span>
</div>
</mat-expansion-panel>
</div>
} @empty {
<span translate class="tb-prompt flex items-center justify-center">version-control.no-entities-to-export-prompt</span>
}
</div>
<div style="padding-top: 16px;" class="flex flex-row">
<button mat-raised-button color="primary"

4
ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.ts

@ -186,10 +186,6 @@ export class EntityTypesVersionCreateComponent extends PageComponent implements
return !!(entityTypeControl as any).expanded;
}
public trackByEntityType(index: number, entityTypeControl: AbstractControl): any {
return entityTypeControl;
}
public removeEntityType(index: number) {
(this.entityTypesVersionCreateFormGroup.get('entityTypes') as UntypedFormArray).removeAt(index);
}

127
ui-ngx/src/app/modules/home/components/vc/entity-types-version-load.component.html

@ -18,73 +18,72 @@
<fieldset class="fields-group flex flex-col" [formGroup]="entityTypesVersionLoadFormGroup">
<legend class="group-title" translate>version-control.entities-to-restore</legend>
<div class="flex flex-col" style="flex: 1; overflow: auto;">
<div *ngFor="let entityTypeFormGroup of entityTypesFormGroupArray(); trackBy: trackByEntityType;
let $index = index; last as isLast;"
class="flex flex-row items-center justify-start" [class.pb-2]="!isLast">
<mat-expansion-panel class="entity-type-config flex-1" [class.load]="loading" [formGroup]="entityTypeFormGroup"
[expanded]="entityTypesFormGroupExpanded(entityTypeFormGroup)" (opened)="loading = false">
<mat-expansion-panel-header>
<div class="flex flex-1 flex-row items-center justify-start">
<mat-panel-title>
<div class="flex flex-1 flex-row items-center justify-start">
<div>{{ entityTypeText(entityTypeFormGroup) }}</div>
</div>
</mat-panel-title>
<span class="flex-1"></span>
<button *ngIf="!disabled" mat-icon-button style="min-width: 40px;"
type="button"
(click)="removeEntityType($index)"
matTooltip="{{ 'action.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
</mat-expansion-panel-header>
<div class="entity-type-config-content flex flex-col gap-2">
<mat-divider></mat-divider>
<div class="flex flex-row xs:flex-col gt-xs:gap-8">
<tb-entity-type-select
class="flex-1"
showLabel
formControlName="entityType"
required
[filterAllowedEntityTypes]="false"
[allowedEntityTypes]="allowedEntityTypes(entityTypeFormGroup)">
</tb-entity-type-select>
<div class="flex flex-1 flex-row xs:flex-col xs:gap-2 gt-xs:gap-4" formGroupName="config">
<div class="flex flex-1 flex-col">
<mat-checkbox #removeOtherEntitiesCheckbox
formControlName="removeOtherEntities"
(change)="onRemoveOtherEntities(removeOtherEntitiesCheckbox, entityTypeFormGroup)">
{{ 'version-control.remove-other-entities' | translate }}
</mat-checkbox>
<mat-checkbox formControlName="findExistingEntityByName">
{{ 'version-control.find-existing-entity-by-name' | translate }}
</mat-checkbox>
</div>
<div class="flex flex-1 flex-col">
<mat-checkbox *ngIf="entityTypeFormGroup.get('entityType').value === entityTypes.DEVICE" formControlName="loadCredentials">
{{ 'version-control.load-credentials' | translate }}
</mat-checkbox>
<mat-checkbox *ngIf="!entityTypesWithoutRelatedData.has(entityTypeFormGroup.get('entityType').value)" formControlName="loadAttributes">
{{ 'version-control.load-attributes' | translate }}
</mat-checkbox>
<mat-checkbox *ngIf="!entityTypesWithoutRelatedData.has(entityTypeFormGroup.get('entityType').value)" formControlName="loadRelations">
{{ 'version-control.load-relations' | translate }}
</mat-checkbox>
<mat-checkbox *ngIf="typesWithCalculatedFields.has(entityTypeFormGroup.get('entityType').value)" formControlName="loadCalculatedFields">
{{ 'version-control.load-calculated-fields' | translate }}
</mat-checkbox>
@for (entityTypeFormGroup of entityTypesFormGroupArray(); track entityTypeFormGroup; let index = $index, isLast = $last) {
<div class="flex flex-row items-center justify-start" [class.pb-2]="!isLast">
<mat-expansion-panel class="entity-type-config flex-1" [class.load]="loading" [formGroup]="entityTypeFormGroup"
[expanded]="entityTypesFormGroupExpanded(entityTypeFormGroup)" (opened)="loading = false">
<mat-expansion-panel-header>
<div class="flex flex-1 flex-row items-center justify-start">
<mat-panel-title>
<div class="flex flex-1 flex-row items-center justify-start">
<div>{{ entityTypeText(entityTypeFormGroup) }}</div>
</div>
</mat-panel-title>
<span class="flex-1"></span>
<button [class.hidden]="disabled" mat-icon-button style="min-width: 40px;"
type="button"
(click)="removeEntityType(index)"
matTooltip="{{ 'action.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
</mat-expansion-panel-header>
<div class="entity-type-config-content flex flex-col gap-2">
<mat-divider></mat-divider>
<div class="flex flex-row xs:flex-col gt-xs:gap-8">
<tb-entity-type-select
class="flex-1"
showLabel
formControlName="entityType"
required
appearance="outline"
[filterAllowedEntityTypes]="false"
[allowedEntityTypes]="allowedEntityTypes(entityTypeFormGroup)">
</tb-entity-type-select>
<div class="flex flex-1 flex-row xs:flex-col xs:gap-2 gt-xs:gap-4" formGroupName="config">
<div class="flex flex-1 flex-col">
<mat-checkbox #removeOtherEntitiesCheckbox
formControlName="removeOtherEntities"
(change)="onRemoveOtherEntities(removeOtherEntitiesCheckbox, entityTypeFormGroup)">
{{ 'version-control.remove-other-entities' | translate }}
</mat-checkbox>
<mat-checkbox formControlName="findExistingEntityByName">
{{ 'version-control.find-existing-entity-by-name' | translate }}
</mat-checkbox>
</div>
<div class="flex flex-1 flex-col">
<mat-checkbox [class.hidden]="entityTypeFormGroup.get('entityType').value !== entityTypes.DEVICE" formControlName="loadCredentials">
{{ 'version-control.load-credentials' | translate }}
</mat-checkbox>
<mat-checkbox [class.hidden]="entityTypesWithoutRelatedData.has(entityTypeFormGroup.get('entityType').value)" formControlName="loadAttributes">
{{ 'version-control.load-attributes' | translate }}
</mat-checkbox>
<mat-checkbox [class.hidden]="entityTypesWithoutRelatedData.has(entityTypeFormGroup.get('entityType').value)" formControlName="loadRelations">
{{ 'version-control.load-relations' | translate }}
</mat-checkbox>
<mat-checkbox [class.hidden]="!typesWithCalculatedFields.has(entityTypeFormGroup.get('entityType').value)" formControlName="loadCalculatedFields">
{{ 'version-control.load-calculated-fields' | translate }}
</mat-checkbox>
</div>
</div>
</div>
</div>
</div>
</mat-expansion-panel>
</div>
<div *ngIf="!entityTypesFormGroupArray().length">
<span translate
class="tb-prompt flex items-center justify-center">version-control.no-entities-to-restore-prompt</span>
</div>
</mat-expansion-panel>
</div>
} @empty {
<span translate class="tb-prompt flex items-center justify-center">version-control.no-entities-to-restore-prompt</span>
}
</div>
<div style="padding-top: 16px;" class="flex flex-row">
<button mat-raised-button color="primary"

33
ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.html

@ -15,39 +15,40 @@
limitations under the License.
-->
<section *ngIf="!versionCreateResult$">
@if (!versionCreateResult$) {
<mat-toolbar>
<h2>{{ 'version-control.create-entity-version' | translate }}</h2>
<span class="flex-1"></span>
</mat-toolbar>
<mat-progress-bar color="warn" style="z-index: 10; width: 100%; margin-bottom: -4px;" mode="indeterminate"
*ngIf="isLoading$ | async">
[class.hidden]="(isLoading$ | async) === false">
</mat-progress-bar>
<form [formGroup]="createVersionFormGroup" style="padding-top: 16px;">
<fieldset [disabled]="isLoading$ | async">
<div class="flex flex-1 flex-col">
<tb-branch-autocomplete
required
appearance="outline"
formControlName="branch">
</tb-branch-autocomplete>
<mat-form-field class="mat-block flex-1">
<mat-form-field class="mat-block flex-1" appearance="outline">
<mat-label translate>version-control.version-name</mat-label>
<input required matInput formControlName="versionName">
<mat-error *ngIf="createVersionFormGroup.get('versionName').hasError('required') ||
createVersionFormGroup.get('versionName').hasError('pattern')">
<mat-error [class.hidden]="!(createVersionFormGroup.get('versionName').hasError('required') ||
createVersionFormGroup.get('versionName').hasError('pattern'))">
{{ 'version-control.version-name-required' | translate }}
</mat-error>
</mat-form-field>
<mat-checkbox *ngIf="entityId.entityType === entityTypes.DEVICE" formControlName="saveCredentials" style="margin-bottom: 16px;">
<mat-checkbox [class.hidden]="entityId.entityType !== entityTypes.DEVICE" formControlName="saveCredentials" style="margin-bottom: 16px;">
{{ 'version-control.export-credentials' | translate }}
</mat-checkbox>
<mat-checkbox *ngIf="!entityTypesWithoutRelatedData.has(entityId.entityType)" formControlName="saveAttributes" style="margin-bottom: 16px;">
<mat-checkbox [class.hidden]="entityTypesWithoutRelatedData.has(entityId.entityType)" formControlName="saveAttributes" style="margin-bottom: 16px;">
{{ 'version-control.export-attributes' | translate }}
</mat-checkbox>
<mat-checkbox *ngIf="!entityTypesWithoutRelatedData.has(entityId.entityType)" formControlName="saveRelations" style="margin-bottom: 16px;">
<mat-checkbox [class.hidden]="entityTypesWithoutRelatedData.has(entityId.entityType)" formControlName="saveRelations" style="margin-bottom: 16px;">
{{ 'version-control.export-relations' | translate }}
</mat-checkbox>
<mat-checkbox *ngIf="typesWithCalculatedFields.has(entityId.entityType)" formControlName="saveCalculatedFields" class="mb-4">
<mat-checkbox [class.hidden]="!typesWithCalculatedFields.has(entityId.entityType)" formControlName="saveCalculatedFields" class="mb-4">
{{ 'version-control.export-calculated-fields' | translate }}
</mat-checkbox>
</div>
@ -67,10 +68,9 @@
{{ 'action.create' | translate }}
</button>
</div>
</section>
<section *ngIf="versionCreateResult$">
<section *ngIf="(versionCreateResult$ | async)?.done || resultMessage; else progress">
<section *ngIf="resultMessage">
} @else {
@if ((versionCreateResult$ | async)?.done || resultMessage) {
<section [class.hidden]="!resultMessage">
<div class="mat-headline-6 vc-result-message">{{ resultMessage }}</div>
<div class="flex items-center justify-end gap-2">
<button mat-button color="primary"
@ -81,13 +81,12 @@
</button>
</div>
</section>
</section>
<ng-template #progress>
} @else {
<section class="flex flex-col items-center justify-center">
<div class="mat-headline-6 vc-result-message progress">
<span translate>version-control.creating-version</span>
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</div>
</section>
</ng-template>
</section>
}
}

99
ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.html

@ -15,51 +15,53 @@
limitations under the License.
-->
<section *ngIf="!versionLoadResult$">
<mat-toolbar *ngIf="entityDataInfo">
<h2>{{ 'version-control.restore-entity-from-version' | translate: {versionName} }}</h2>
<span class="flex-1"></span>
</mat-toolbar>
<mat-progress-bar color="warn" style="z-index: 10; width: 100%; margin-bottom: -4px;" mode="indeterminate"
*ngIf="entityDataInfo && (isLoading$ | async)">
</mat-progress-bar>
<mat-spinner *ngIf="!entityDataInfo" mode="indeterminate" diameter="80"></mat-spinner>
<form *ngIf="entityDataInfo" [formGroup]="restoreFormGroup" style="padding-top: 16px;">
<fieldset [disabled]="isLoading$ | async">
<div class="flex flex-1 flex-col">
<mat-checkbox *ngIf="entityDataInfo.hasCredentials" formControlName="loadCredentials" style="margin-bottom: 16px;">
{{ 'version-control.load-credentials' | translate }}
</mat-checkbox>
<mat-checkbox *ngIf="entityDataInfo.hasAttributes" formControlName="loadAttributes" style="margin-bottom: 16px;">
{{ 'version-control.load-attributes' | translate }}
</mat-checkbox>
<mat-checkbox *ngIf="entityDataInfo.hasRelations" formControlName="loadRelations" style="margin-bottom: 16px;">
{{ 'version-control.load-relations' | translate }}
</mat-checkbox>
<mat-checkbox *ngIf="entityDataInfo.hasCalculatedFields" formControlName="loadCalculatedFields" class="mb-4">
{{ 'version-control.load-calculated-fields' | translate }}
</mat-checkbox>
</div>
</fieldset>
</form>
<div *ngIf="entityDataInfo" class="flex items-center justify-end gap-2">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="button"
(click)="restore()"
[disabled]="(isLoading$ | async) || restoreFormGroup.invalid">
{{ 'action.restore' | translate }}
</button>
</div>
</section>
<section *ngIf="versionLoadResult$">
<section *ngIf="(versionLoadResult$ | async)?.done || errorMessage; else progress">
<section *ngIf="errorMessage">
@if (!versionLoadResult$) {
@if (entityDataInfo) {
<mat-toolbar>
<h2 tbTruncateWithTooltip>{{ 'version-control.restore-entity-from-version' | translate: {versionName} }}</h2>
<span class="flex-1"></span>
</mat-toolbar>
<mat-progress-bar color="warn" style="z-index: 10; width: 100%; margin-bottom: -4px;" mode="indeterminate"
[class.hidden]="(isLoading$ | async) === false">
</mat-progress-bar>
<form [formGroup]="restoreFormGroup" style="padding-top: 16px;">
<fieldset [disabled]="isLoading$ | async">
<div class="flex flex-1 flex-col">
<mat-checkbox [class.hidden]="!entityDataInfo?.hasCredentials" formControlName="loadCredentials" style="margin-bottom: 16px;">
{{ 'version-control.load-credentials' | translate }}
</mat-checkbox>
<mat-checkbox [class.hidden]="!entityDataInfo?.hasAttributes" formControlName="loadAttributes" style="margin-bottom: 16px;">
{{ 'version-control.load-attributes' | translate }}
</mat-checkbox>
<mat-checkbox [class.hidden]="!entityDataInfo?.hasRelations" formControlName="loadRelations" style="margin-bottom: 16px;">
{{ 'version-control.load-relations' | translate }}
</mat-checkbox>
<mat-checkbox [class.hidden]="!entityDataInfo?.hasCalculatedFields" formControlName="loadCalculatedFields" class="mb-4">
{{ 'version-control.load-calculated-fields' | translate }}
</mat-checkbox>
</div>
</fieldset>
</form>
<div class="flex items-center justify-end gap-2">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="button"
(click)="restore()"
[disabled]="(isLoading$ | async) || restoreFormGroup.invalid">
{{ 'action.restore' | translate }}
</button>
</div>
} @else {
<mat-spinner mode="indeterminate" diameter="80"></mat-spinner>
}
} @else {
@if ((versionLoadResult$ | async)?.done || errorMessage) {
<section [class.hidden]="!errorMessage">
<div class="mat-headline-6 vc-result-message error" [innerHTML]="errorMessage"></div>
<div class="flex items-center justify-end gap-2">
<button mat-button color="primary"
@ -70,13 +72,12 @@
</button>
</div>
</section>
</section>
<ng-template #progress>
} @else {
<section class="flex flex-col items-center justify-center">
<div class="mat-headline-6 vc-result-message progress">
<span translate>version-control.restoring-entity-version</span>
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</div>
</section>
</ng-template>
</section>
}
}

2
ui-ngx/src/app/modules/home/components/vc/remove-other-entities-confirm.component.html

@ -19,7 +19,7 @@
<form [formGroup]="confirmFormGroup" style="padding-top: 16px;">
<fieldset [disabled]="(isLoading$ | async)">
<div style="user-select: none;" [innerHTML]="removeOtherEntitiesConfirmText"></div>
<mat-form-field class="mat-block">
<mat-form-field class="mat-block" appearance="outline">
<input matInput formControlName="verification"/>
</mat-form-field>
</fieldset>

8
ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts

@ -24,6 +24,7 @@ import {
NgZone,
OnDestroy,
OnInit,
SecurityContext,
ViewChild
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
@ -53,6 +54,7 @@ import {
import { deepClone } from '@core/utils';
import { hidePageSizePixelValue } from '@shared/models/constants';
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
import { DomSanitizer } from '@angular/platform-browser';
@Component({
selector: 'tb-manage-widget-actions',
@ -106,7 +108,8 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni
private dialogs: DialogService,
private cd: ChangeDetectorRef,
private elementRef: ElementRef,
private zone: NgZone) {
private zone: NgZone,
private sanitizer: DomSanitizer) {
super();
const sortOrder: SortOrder = { property: 'actionSourceName', direction: Direction.ASC };
this.pageLink = new PageLink(10, 0, null, sortOrder);
@ -289,7 +292,8 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni
}
const title = this.translate.instant('widget-config.delete-action-title');
const content = this.translate.instant('widget-config.delete-action-text', {actionName: action.name});
this.dialogs.confirm(title, content,
const safeContent = this.sanitizer.sanitize(SecurityContext.HTML, content);
this.dialogs.confirm(title, safeContent,
this.translate.instant('action.no'),
this.translate.instant('action.yes'), true).subscribe(
(res) => {

2
ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html

@ -57,7 +57,7 @@
<mat-option *ngFor="let column of configuredColumns; let $index = index"
[value]="$index"
[disabled]="usedCellClickColumns.includes($index)">
{{ getCellClickColumnInfo($index, column) }}
{{ getCellClickColumnInfo($index, column) | customTranslate }}
</mat-option>
</mat-select>
<mat-icon matSuffix

3
ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.scss

@ -50,6 +50,7 @@ $warning-color: #FAA405;
color: $enabled-color;
}
.mat-mdc-progress-bar {
--mdc-linear-progress-track-color: #{rgba($enabled-color, 0.06)};
--mdc-linear-progress-active-indicator-color: #{$enabled-color};
}
}
@ -58,6 +59,7 @@ $warning-color: #FAA405;
color: $disabled-color;
}
.mat-mdc-progress-bar {
--mdc-linear-progress-track-color: #{rgba($disabled-color, 0.06)};
--mdc-linear-progress-active-indicator-color: #{$disabled-color};
}
}
@ -66,6 +68,7 @@ $warning-color: #FAA405;
color: $warning-color;
}
.mat-mdc-progress-bar {
--mdc-linear-progress-track-color: #{rgba($warning-color, 0.06)};
--mdc-linear-progress-active-indicator-color: #{$warning-color};
}
}

32
ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.ts

@ -20,16 +20,16 @@ import { backgroundStyle, ComponentStyle, overlayStyle } from '@shared/models/wi
import { Observable } from 'rxjs';
import { ImagePipe } from '@shared/pipe/image.pipe';
import { DomSanitizer } from '@angular/platform-browser';
import { DataKey, DatasourceType, widgetType } from "@shared/models/widget.models";
import { WidgetSubscriptionOptions } from "@core/api/widget-api.models";
import { formattedDataFormDatasourceData } from "@core/utils";
import { DatasourceType, widgetType } from '@shared/models/widget.models';
import { WidgetSubscriptionOptions } from '@core/api/widget-api.models';
import { formattedDataFormDatasourceData } from '@core/utils';
import { UtilsService } from "@core/services/utils.service";
import { UtilsService } from '@core/services/utils.service';
import {
ApiUsageDataKeysSettings,
apiUsageDefaultSettings,
ApiUsageWidgetSettings
} from "@home/components/widget/lib/settings/cards/api-usage-settings.component.models";
ApiUsageWidgetSettings,
getUniqueDataKeys
} from '@home/components/widget/lib/settings/cards/api-usage-settings.component.models';
@Component({
selector: 'tb-api-usage-widget',
@ -80,7 +80,7 @@ export class ApiUsageWidgetComponent implements OnInit, OnDestroy {
type: DatasourceType.entity,
name: '',
entityAliasId: this.settings.dsEntityAliasId,
dataKeys: this.getUniqueDataKeys(this.settings.dataKeys)
dataKeys: getUniqueDataKeys(this.settings.apiUsageDataKeys)
}
const apiUsageSubscriptionOptions: WidgetSubscriptionOptions = {
@ -122,7 +122,7 @@ export class ApiUsageWidgetComponent implements OnInit, OnDestroy {
}
parseApiUsages() {
this.settings.dataKeys.forEach((key) => {
this.settings.apiUsageDataKeys.forEach((key) => {
this.apiUsages.push({
label: this.utils.customTranslation(key.label, key.label),
state: key.state,
@ -134,20 +134,6 @@ export class ApiUsageWidgetComponent implements OnInit, OnDestroy {
})
}
getUniqueDataKeys(data: ApiUsageDataKeysSettings[]): DataKey[] {
const seenNames = new Set<string>();
return data
.flatMap(item => [item.status, item.maxLimit, item.current])
.filter(key => {
if (seenNames.has(key.name)) {
return false;
}
seenNames.add(key.name);
return true;
});
};
ngOnDestroy() {
if (this.contentResize$) {
this.contentResize$.disconnect();

11
ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.ts

@ -510,11 +510,9 @@ export class LiquidLevelWidgetComponent implements OnInit {
let content: string;
let container: JQuery<HTMLElement>;
const jQueryContainerElement = $(this.liquidLevelContent.nativeElement);
let value = 'N/A';
let value: number | string = 'N/A';
if (isNumeric(data)) {
value = this.widgetUnitsConvertor(convertLiters(this.convertOutputData(percentage), this.widgetUnits as CapacityUnits, ConversionType.from))
.toFixed(this.settings.decimals || 0);
value = +this.widgetUnitsConvertor(convertLiters(this.convertOutputData(percentage), this.widgetUnits as CapacityUnits, ConversionType.from)).toFixed(this.ctx.widgetConfig.decimals || 0);
}
this.valueColor.update(value);
const valueTextStyle = cssTextFromInlineStyle({...inlineTextStyle(this.settings.valueFont),
@ -528,10 +526,9 @@ export class LiquidLevelWidgetComponent implements OnInit {
let volume: number | string;
if (this.widgetUnits !== CapacityUnits.percent) {
const volumeInLiters: number = convertLiters(this.volume, this.volumeUnits as CapacityUnits, ConversionType.to);
volume = this.widgetUnitsConvertor(convertLiters(volumeInLiters, this.widgetUnits as CapacityUnits, ConversionType.from))
.toFixed(this.settings.decimals || 0);
volume = +this.widgetUnitsConvertor(convertLiters(volumeInLiters, this.widgetUnits as CapacityUnits, ConversionType.from)).toFixed(this.ctx.widgetConfig.decimals || 0);
} else {
volume = this.widgetUnitsConvertor(this.volume).toFixed(this.settings.decimals || 0);
volume = +this.widgetUnitsConvertor(this.volume).toFixed(this.ctx.widgetConfig.decimals || 0);
}
const volumeTextStyle = cssTextFromInlineStyle({...inlineTextStyle(this.settings.volumeFont),

24
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-settings.component.models.ts

@ -17,10 +17,10 @@
import { IAliasController } from '@core/api/widget-api.models';
import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models';
import { DataKey, Widget, widgetType } from '@shared/models/widget.models';
import { Observable } from "rxjs";
import { BackgroundSettings, BackgroundType } from "@shared/models/widget-settings.models";
import { DataKeyType } from "@shared/models/telemetry/telemetry.models";
import { materialColors } from "@shared/models/material.models";
import { Observable } from 'rxjs';
import { BackgroundSettings, BackgroundType } from '@shared/models/widget-settings.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { materialColors } from '@shared/models/material.models';
export interface ApiUsageSettingsContext {
aliasController: IAliasController;
@ -33,7 +33,7 @@ export interface ApiUsageSettingsContext {
export interface ApiUsageWidgetSettings {
dsEntityAliasId: string;
dataKeys: ApiUsageDataKeysSettings[];
apiUsageDataKeys: ApiUsageDataKeysSettings[];
targetDashboardState: string;
background: BackgroundSettings;
padding: string;
@ -80,7 +80,7 @@ const generateDataKey = (label: string, status: string, maxLimit: string, curren
export const apiUsageDefaultSettings: ApiUsageWidgetSettings = {
dsEntityAliasId: '',
dataKeys: [
apiUsageDataKeys: [
generateDataKey('{i18n:api-usage.transport-messages}', 'transportApiState', 'transportMsgLimit', 'transportMsgCount'),
generateDataKey('{i18n:api-usage.transport-data-points}', 'transportApiState', 'transportDataPointsLimit', 'transportDataPointsCount'),
generateDataKey('{i18n:api-usage.rule-engine-executions}', 'ruleEngineApiState', 'ruleEngineExecutionLimit', 'ruleEngineExecutionCount'),
@ -104,3 +104,15 @@ export const apiUsageDefaultSettings: ApiUsageWidgetSettings = {
padding: '0'
};
export const getUniqueDataKeys = (data: ApiUsageDataKeysSettings[]): DataKey[] => {
const seenNames = new Set<string>();
return data
.flatMap(item => [item.status, item.maxLimit, item.current])
.filter(key => {
if (seenNames.has(key.name)) {
return false;
}
seenNames.add(key.name);
return true;
});
};

24
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.ts

@ -37,15 +37,15 @@ import {
ApiUsageDataKeysSettings,
apiUsageDefaultSettings,
ApiUsageSettingsContext
} from "@home/components/widget/lib/settings/cards/api-usage-settings.component.models";
import { deepClone } from "@core/utils";
import { Observable, of } from "rxjs";
} from '@home/components/widget/lib/settings/cards/api-usage-settings.component.models';
import { deepClone } from '@core/utils';
import { Observable, of } from 'rxjs';
import {
DataKeyConfigDialogComponent,
DataKeyConfigDialogData
} from "@home/components/widget/lib/settings/common/key/data-key-config-dialog.component";
import { MatDialog } from "@angular/material/dialog";
import { CdkDragDrop } from "@angular/cdk/drag-drop";
} from '@home/components/widget/lib/settings/common/key/data-key-config-dialog.component';
import { MatDialog } from '@angular/material/dialog';
import { CdkDragDrop } from '@angular/cdk/drag-drop';
@Component({
selector: 'tb-api-usage-widget-settings',
@ -82,11 +82,11 @@ export class ApiUsageWidgetSettingsComponent extends WidgetSettingsComponent {
}
protected doUpdateSettings(settingsForm: UntypedFormGroup, settings: WidgetSettings) {
settingsForm.setControl('dataKeys', this.prepareDataKeysFormArray(settings?.dataKeys), {emitEvent: false});
settingsForm.setControl('apiUsageDataKeys', this.prepareDataKeysFormArray(settings?.apiUsageDataKeys), {emitEvent: false});
}
dataKeysFormArray(): UntypedFormArray {
return this.apiUsageWidgetSettingsForm.get('dataKeys') as UntypedFormArray;
return this.apiUsageWidgetSettingsForm.get('apiUsageDataKeys') as UntypedFormArray;
}
trackByDataKey(index: number): any {
@ -104,7 +104,7 @@ export class ApiUsageWidgetSettingsComponent extends WidgetSettingsComponent {
}
removeDataKey(index: number) {
(this.apiUsageWidgetSettingsForm.get('dataKeys') as UntypedFormArray).removeAt(index);
(this.apiUsageWidgetSettingsForm.get('apiUsageDataKeys') as UntypedFormArray).removeAt(index);
}
addDataKey() {
@ -115,7 +115,7 @@ export class ApiUsageWidgetSettingsComponent extends WidgetSettingsComponent {
maxLimit: null,
current: null
};
const dataKeysArray = this.apiUsageWidgetSettingsForm.get('dataKeys') as UntypedFormArray;
const dataKeysArray = this.apiUsageWidgetSettingsForm.get('apiUsageDataKeys') as UntypedFormArray;
const dataKeyControl = this.fb.control(dataKey, [this.apiUsageDataKeyValidator()]);
dataKeysArray.push(dataKeyControl);
}
@ -131,7 +131,7 @@ export class ApiUsageWidgetSettingsComponent extends WidgetSettingsComponent {
protected prepareInputSettings(settings: WidgetSettings): WidgetSettings {
return {
dsEntityAliasId: settings?.dsEntityAliasId,
dataKeys: settings?.dataKeys,
apiUsageDataKeys: settings?.apiUsageDataKeys,
targetDashboardState: settings?.targetDashboardState,
background: settings?.background,
padding: settings.padding
@ -141,7 +141,7 @@ export class ApiUsageWidgetSettingsComponent extends WidgetSettingsComponent {
protected onSettingsSet(settings: WidgetSettings) {
this.apiUsageWidgetSettingsForm = this.fb.group({
dsEntityAliasId: [settings?.dsEntityAliasId],
dataKeys: this.prepareDataKeysFormArray(settings?.dataKeys),
apiUsageDataKeys: this.prepareDataKeysFormArray(settings?.apiUsageDataKeys),
targetDashboardState: [settings?.targetDashboardState],
background: [settings?.background, []],
padding: [settings.padding, []]

2
ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html

@ -47,7 +47,7 @@
<table mat-table [dataSource]="source.timeseriesDatasource" [trackBy]="trackByRowTimestamp"
matSort [matSortActive]="source.pageLink.sortOrder.property" [matSortDirection]="source.pageLink.sortDirection()" matSortDisableClear>
<ng-container *ngIf="showTimestamp" [matColumnDef]="'0'">
<mat-header-cell *matHeaderCellDef mat-sort-header>Timestamp</mat-header-cell>
<mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'widgets.table.timestamp-column-name' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let row; let rowIndex = index"
[innerHTML]="cellContent(source, null, 0, row, row[0], rowIndex) | async"
[style]="cellStyle(source, null, 0, row, row[0], rowIndex) | async">

2
ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts

@ -513,7 +513,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
let title = '';
const header = this.sources[index].header.find(column => column.index.toString() === value);
if (value === '0') {
title = 'Timestamp';
title = this.translate.instant('widgets.table.timestamp-column-name');
} else if (value === 'actions') {
title = 'Actions';
} else {

21
ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts

@ -926,7 +926,26 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
if (this.modelValue) {
const configuredColumns = new Array<CellClickColumnInfo>();
if (this.modelValue.config?.datasources[0]?.dataKeys?.length) {
configuredColumns.push(...this.keysToCellClickColumns(this.modelValue.config.datasources[0].dataKeys));
const {
displayEntityLabel,
displayEntityName,
displayEntityType,
entityNameColumnTitle,
entityLabelColumnTitle
} = this.modelValue.config.settings;
const displayEntitiesArray = [];
if (isDefined(displayEntityName)) {
const displayName = entityNameColumnTitle ? entityNameColumnTitle : 'entityName';
displayEntitiesArray.push({name: displayName, label: displayName});
}
if (isDefined(displayEntityLabel)) {
const displayLabel = entityLabelColumnTitle ? entityLabelColumnTitle : 'entityLabel';
displayEntitiesArray.push({name: displayLabel, label: displayLabel});
}
if (isDefined(displayEntityType)) {
displayEntitiesArray.push({name: 'entityType', label: 'entityType'});
}
configuredColumns.push(...displayEntitiesArray, ...this.keysToCellClickColumns(this.modelValue.config.datasources[0].dataKeys));
}
if (this.modelValue.config?.alarmSource?.dataKeys?.length) {
configuredColumns.push(...this.keysToCellClickColumns(this.modelValue.config.alarmSource.dataKeys));

6
ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html

@ -28,6 +28,12 @@
[class.!hidden]="isEdit || dashboardScope !== 'tenant'">
{{'dashboard.export' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'import')"
[class.!hidden]="isEdit || dashboardScope !== 'tenant'">
{{'dashboard.update-new-version' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'makePublic')"

7
ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts

@ -22,7 +22,7 @@ import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { TranslateService } from '@ngx-translate/core';
import {
Dashboard,
Dashboard, DashboardInfo,
getDashboardAssignedCustomersText,
isCurrentPublicDashboardCustomer,
isPublicDashboard
@ -31,13 +31,14 @@ import { DashboardService } from '@core/http/dashboard.service';
import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
import { isEqual } from '@core/utils';
import { EntityType } from '@shared/models/entity-type.models';
import { PageLink } from "@shared/models/page/page-link";
@Component({
selector: 'tb-dashboard-form',
templateUrl: './dashboard-form.component.html',
styleUrls: ['./dashboard-form.component.scss']
})
export class DashboardFormComponent extends EntityComponent<Dashboard> {
export class DashboardFormComponent extends EntityComponent<Dashboard, PageLink, DashboardInfo> {
dashboardScope: 'tenant' | 'customer' | 'customer_user' | 'edge';
customerId: string;
@ -117,7 +118,7 @@ export class DashboardFormComponent extends EntityComponent<Dashboard> {
onPublicLinkCopied($event) {
this.store.dispatch(new ActionNotificationShow(
{
{
message: this.translate.instant('dashboard.public-link-copied-message'),
type: 'success',
duration: 750,

15
ui-ngx/src/app/modules/home/pages/dashboard/dashboard.module.ts

@ -19,18 +19,24 @@ import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import { HomeDialogsModule } from '../../dialogs/home-dialogs.module';
import { DashboardFormComponent } from '@modules/home/pages/dashboard/dashboard-form.component';
import { ManageDashboardCustomersDialogComponent } from '@modules/home/pages/dashboard/manage-dashboard-customers-dialog.component';
import {
ManageDashboardCustomersDialogComponent
} from '@modules/home/pages/dashboard/manage-dashboard-customers-dialog.component';
import { DashboardRoutingModule } from './dashboard-routing.module';
import { MakeDashboardPublicDialogComponent } from '@modules/home/pages/dashboard/make-dashboard-public-dialog.component';
import {
MakeDashboardPublicDialogComponent
} from '@modules/home/pages/dashboard/make-dashboard-public-dialog.component';
import { HomeComponentsModule } from '@modules/home/components/home-components.module';
import { DashboardTabsComponent } from '@home/pages/dashboard/dashboard-tabs.component';
import { ImportDashboardFileDialogComponent } from "@home/pages/dashboard/import-dashboard-file-dialog.component";
@NgModule({
declarations: [
DashboardFormComponent,
DashboardTabsComponent,
ManageDashboardCustomersDialogComponent,
MakeDashboardPublicDialogComponent
MakeDashboardPublicDialogComponent,
ImportDashboardFileDialogComponent
],
imports: [
CommonModule,
@ -40,4 +46,5 @@ import { DashboardTabsComponent } from '@home/pages/dashboard/dashboard-tabs.com
DashboardRoutingModule
]
})
export class DashboardModule { }
export class DashboardModule {
}

40
ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts

@ -77,11 +77,16 @@ import {
EntityAliasesDialogComponent,
EntityAliasesDialogData
} from '@home/components/alias/entity-aliases-dialog.component';
import {
DashboardInfoDialogData,
ImportDashboardFileDialogComponent
} from "@home/pages/dashboard/import-dashboard-file-dialog.component";
import { PageLink } from "@shared/models/page/page-link";
@Injectable()
export class DashboardsTableConfigResolver {
export class DashboardsTableConfigResolver {
private readonly config: EntityTableConfig<DashboardInfo | Dashboard> = new EntityTableConfig<DashboardInfo | Dashboard>();
private readonly config: EntityTableConfig<Dashboard, PageLink, DashboardInfo> = new EntityTableConfig<Dashboard, PageLink, DashboardInfo>();
constructor(private store: Store<AppState>,
private dashboardService: DashboardService,
@ -375,7 +380,7 @@ export class DashboardsTableConfigResolver {
return actions;
}
openDashboard($event: Event, dashboard: DashboardInfo) {
openDashboard($event: Event, dashboard: Dashboard) {
if ($event) {
$event.stopPropagation();
}
@ -422,13 +427,27 @@ export class DashboardsTableConfigResolver {
));
}
exportDashboard($event: Event, dashboard: DashboardInfo) {
exportDashboard($event: Event, dashboard: Dashboard) {
if ($event) {
$event.stopPropagation();
}
this.importExport.exportDashboard(dashboard.id.id);
}
importDashboardFile($event: Event, dashboard: Dashboard) {
if ($event) {
$event.stopPropagation();
}
return this.dialog.open<ImportDashboardFileDialogComponent, DashboardInfoDialogData,
boolean>(ImportDashboardFileDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
dashboard
}
}).afterClosed();
}
addDashboardsToCustomer($event: Event) {
if ($event) {
$event.stopPropagation();
@ -449,7 +468,7 @@ export class DashboardsTableConfigResolver {
});
}
makePublic($event: Event, dashboard: DashboardInfo) {
makePublic($event: Event, dashboard: Dashboard) {
if ($event) {
$event.stopPropagation();
}
@ -470,7 +489,7 @@ export class DashboardsTableConfigResolver {
);
}
makePrivate($event: Event, dashboard: DashboardInfo) {
makePrivate($event: Event, dashboard: Dashboard) {
if ($event) {
$event.stopPropagation();
}
@ -492,7 +511,7 @@ export class DashboardsTableConfigResolver {
);
}
manageAssignedCustomers($event: Event, dashboard: DashboardInfo) {
manageAssignedCustomers($event: Event, dashboard: Dashboard) {
const assignedCustomersIds = dashboard.assignedCustomers ?
dashboard.assignedCustomers.map(customerInfo => customerInfo.customerId.id) : [];
this.showManageAssignedCustomersDialog($event, [dashboard.id.id], 'manage', assignedCustomersIds);
@ -529,7 +548,7 @@ export class DashboardsTableConfigResolver {
});
}
unassignFromCustomer($event: Event, dashboard: DashboardInfo, customerId: string) {
unassignFromCustomer($event: Event, dashboard: Dashboard, customerId: string) {
if ($event) {
$event.stopPropagation();
}
@ -579,7 +598,7 @@ export class DashboardsTableConfigResolver {
);
}
onDashboardAction(action: EntityAction<DashboardInfo>): boolean {
onDashboardAction(action: EntityAction<Dashboard>): boolean {
switch (action.action) {
case 'open':
this.openDashboard(action.event, action.entity);
@ -587,6 +606,9 @@ export class DashboardsTableConfigResolver {
case 'export':
this.exportDashboard(action.event, action.entity);
return true;
case 'import':
this.importDashboardFile(action.event, action.entity);
return true;
case 'makePublic':
this.makePublic(action.event, action.entity);
return true;

57
ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.html

@ -0,0 +1,57 @@
<!--
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.
-->
<form [formGroup]="uploadFileFormGroup" (ngSubmit)="save()" style="width: 800px;">
<mat-toolbar color="primary">
<h2>{{ 'dashboard.update-new-version' | translate }}</h2>
<span class="flex-1"></span>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content>
<tb-file-input [contentConvertFunction]="loadDataFromJsonContent"
[existingFileName]="currentFileName"
(fileNameChanged)="currentFileName = $event"
label="{{'dashboard.upload-file-to-update' | translate}}"
formControlName="file"
dropLabel="{{ 'import.drop-json-file-or' | translate }}"
accept=".json,application/json"
allowedExtensions="json">
</tb-file-input>
</div>
<div mat-dialog-actions class="flex flex-row items-center justify-end">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || uploadFileFormGroup.invalid
|| !uploadFileFormGroup.dirty">
{{ 'action.save' | translate }}
</button>
</div>
</form>

89
ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.ts

@ -0,0 +1,89 @@
///
/// 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, Inject, OnInit } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormGroup } from '@angular/forms';
import { DashboardService } from '@core/http/dashboard.service';
import { Dashboard } from '@app/shared/models/dashboard.models';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
export interface DashboardInfoDialogData {
dashboard: Dashboard;
}
@Component({
selector: 'tb-import-dashboard-file-dialog',
templateUrl: './import-dashboard-file-dialog.component.html',
styleUrls: []
})
export class ImportDashboardFileDialogComponent extends DialogComponent<ImportDashboardFileDialogComponent> implements OnInit {
private dashboard: Dashboard;
currentFileName: string = '';
uploadFileFormGroup: FormGroup;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: DashboardInfoDialogData,
private dashboardService: DashboardService,
protected dialogRef: MatDialogRef<ImportDashboardFileDialogComponent>,
private fb: FormBuilder) {
super(store, router, dialogRef);
this.dashboard = data.dashboard;
}
ngOnInit(): void {
this.uploadFileFormGroup = this.fb.group({
file: [null]
});
}
cancel(): void {
this.dialogRef.close();
}
save() {
const fileControl = this.uploadFileFormGroup.get('file');
if (!fileControl || !fileControl.value) {
return;
}
const dashboardContent = {
...fileControl.value,
description: this.dashboard.configuration.description
};
this.dashboard.configuration = dashboardContent;
this.dashboardService.saveDashboard(this.dashboard).subscribe(() => {
this.dialogRef.close(true);
})
}
loadDataFromJsonContent(content: string): any {
try {
const importData = JSON.parse(content);
return importData ? importData['configuration'] : importData;
} catch (err) {
this.store.dispatch(new ActionNotificationShow({message: err.message, type: 'error'}));
return null;
}
}
}

2
ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html

@ -233,7 +233,7 @@
<div class="notification-content mini" [style.background-color]="preview.processedTemplates.MICROSOFT_TEAMS?.themeColor">
<div class="subject">{{ preview.processedTemplates.MICROSOFT_TEAMS.subject }}</div>
{{ preview.processedTemplates.MICROSOFT_TEAMS.body }}
<button mat-raised-button>{{ preview.processedTemplates.MICROSOFT_TEAMS.button.text }}</button>
<button mat-raised-button>{{ preview.processedTemplates.MICROSOFT_TEAMS.button?.text }}</button>
</div>
</section>
</div>

9
ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts

@ -26,6 +26,7 @@ import {
OnInit,
QueryList,
Renderer2,
SecurityContext,
SkipSelf,
ViewChild,
ViewChildren,
@ -97,6 +98,7 @@ import { HttpStatusCode } from '@angular/common/http';
import { TbContextMenuEvent } from '@shared/models/jquery-event.models';
import { EntityDebugSettings } from '@shared/models/entity.models';
import Timeout = NodeJS.Timeout;
import { DomSanitizer } from '@angular/platform-browser';
@Component({
selector: 'tb-rulechain-page',
@ -273,6 +275,7 @@ export class RuleChainPageComponent extends PageComponent
private renderer: Renderer2,
private viewContainerRef: ViewContainerRef,
private changeDetector: ChangeDetectorRef,
private sanitizer:DomSanitizer,
public dialog: MatDialog,
public dialogService: DialogService,
public fb: FormBuilder) {
@ -1360,9 +1363,13 @@ export class RuleChainPageComponent extends PageComponent
name = node.name;
desc = this.translate.instant(ruleNodeTypeDescriptors.get(node.component.type).name) + ' - ' + node.component.name;
if (node.additionalInfo) {
details = node.additionalInfo.description;
details = this.sanitizer.sanitize(SecurityContext.HTML, node.additionalInfo.description);
}
}
name = this.sanitizer.sanitize(SecurityContext.HTML, name);
desc = this.sanitizer.sanitize(SecurityContext.HTML, desc);
let tooltipContent = '<div class="tb-rule-node-tooltip">' +
'<div id="tb-node-content">' +
'<div class="tb-node-title">' + name + '</div>' +

29
ui-ngx/src/app/shared/components/time/datapoints-limit.component.ts

@ -29,6 +29,7 @@ import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { TimeService } from '@core/services/time.service';
import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { isDefined } from '@core/utils';
@Component({
selector: 'tb-datapoints-limit',
@ -69,7 +70,11 @@ export class DatapointsLimitComponent implements ControlValueAccessor, Validator
@Input()
disabled: boolean;
private propagateChange = (v: any) => { };
private propagateChangeValue: any;
private propagateChange = (v: any) => {
this.propagateChangeValue = v;
};
private destroy$ = new Subject<void>();
@ -79,6 +84,9 @@ export class DatapointsLimitComponent implements ControlValueAccessor, Validator
registerOnChange(fn: any): void {
this.propagateChange = fn;
if (isDefined(this.propagateChangeValue)) {
this.propagateChange(this.propagateChangeValue);
}
}
registerOnTouched(fn: any): void {
@ -115,19 +123,20 @@ export class DatapointsLimitComponent implements ControlValueAccessor, Validator
}
}
private checkLimit(limit?: number): number {
if (!limit || limit < this.minDatapointsLimit()) {
return this.minDatapointsLimit();
writeValue(value: number | null): void {
this.modelValue = value;
let limit = this.modelValue;
if (!limit) {
limit = Math.ceil(this.maxDatapointsLimit() / 2);
} else if (limit < this.minDatapointsLimit()) {
limit = this.minDatapointsLimit();
} else if (limit > this.maxDatapointsLimit()) {
return this.maxDatapointsLimit();
limit = this.maxDatapointsLimit();
}
return limit;
}
writeValue(value: number | null): void {
this.modelValue = this.checkLimit(value);
this.updateView(limit);
this.datapointsLimitFormGroup.patchValue(
{ limit: this.modelValue }, {emitEvent: false}
{ limit: limit }, {emitEvent: false}
);
}

5
ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.html

@ -15,7 +15,10 @@
limitations under the License.
-->
<mat-form-field [formGroup]="branchFormGroup" class="mat-block" [floatLabel]="(selectionMode || emptyPlaceholder) ? 'always' : 'auto'" subscriptSizing="{{ subscriptSizing }}">
<mat-form-field [formGroup]="branchFormGroup" class="mat-block"
[floatLabel]="(selectionMode || emptyPlaceholder) ? 'always' : 'auto'"
[appearance]="appearance"
[subscriptSizing]="subscriptSizing">
<mat-label>{{ 'version-control.branch' | translate }}</mat-label>
<input matInput type="text" placeholder="{{emptyPlaceholder || ((loading ? 'common.loading' : 'version-control.select-branch') | translate)}}"
#branchInput

5
ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.ts

@ -35,7 +35,7 @@ import { BranchInfo } from '@shared/models/vc.models';
import { EntitiesVersionControlService } from '@core/http/entities-version-control.service';
import { isNotEmptyStr } from '@core/utils';
import { MatAutocomplete, MatAutocompleteTrigger } from '@angular/material/autocomplete';
import { SubscriptSizing } from '@angular/material/form-field';
import { SubscriptSizing, MatFormFieldAppearance } from '@angular/material/form-field';
@Component({
selector: 'tb-branch-autocomplete',
@ -57,6 +57,9 @@ export class BranchAutocompleteComponent implements ControlValueAccessor, OnInit
@Input()
subscriptSizing: SubscriptSizing = 'fixed';
@Input()
appearance: MatFormFieldAppearance = 'fill';
private requiredValue: boolean;
get required(): boolean {

7
ui-ngx/src/app/shared/models/time/time.models.ts

@ -1125,6 +1125,7 @@ export const cloneSelectedTimewindow = (timewindow: Timewindow): Timewindow => {
export const clearTimewindowConfig = (timewindow: Timewindow, quickIntervalOnly: boolean,
historyOnly: boolean, hasAggregation: boolean, hasTimezone = true): Timewindow => {
const noneAggregation = hasAggregation && timewindow.aggregation?.type === AggregationType.NONE;
if (timewindow.selectedTab === TimewindowType.REALTIME) {
if (quickIntervalOnly || timewindow.realtime.realtimeType === RealtimeWindowType.INTERVAL) {
delete timewindow.realtime.timewindowMs;
@ -1138,7 +1139,7 @@ export const clearTimewindowConfig = (timewindow: Timewindow, quickIntervalOnly:
delete timewindow.history?.quickInterval;
delete timewindow.history?.interval;
if (!hasAggregation) {
if (!hasAggregation || noneAggregation) {
delete timewindow.realtime.interval;
}
} else {
@ -1162,13 +1163,15 @@ export const clearTimewindowConfig = (timewindow: Timewindow, quickIntervalOnly:
delete timewindow.realtime?.quickInterval;
delete timewindow.realtime?.interval;
if (!hasAggregation) {
if (!hasAggregation || noneAggregation) {
delete timewindow.history.interval;
}
}
if (!hasAggregation) {
delete timewindow.aggregation;
} else if (!noneAggregation) {
delete timewindow.aggregation.limit;
}
if (historyOnly) {

88
ui-ngx/src/app/shared/models/widget/home-widgets/api-usage-model.definition.ts

@ -0,0 +1,88 @@
///
/// 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 { EntityAliases, EntityAliasInfo, getEntityAliasId } from '@shared/models/alias.models';
import { FilterInfo, Filters } from '@shared/models/query/query.models';
import { Dashboard } from '@shared/models/dashboard.models';
import { Datasource, DatasourceType, Widget } from '@shared/models/widget.models';
import { WidgetModelDefinition } from '@shared/models/widget/widget-model.definition';
import {
ApiUsageWidgetSettings,
getUniqueDataKeys
} from '@home/components/widget/lib/settings/cards/api-usage-settings.component.models';
interface AliasFilterPair {
alias?: EntityAliasInfo;
filter?: FilterInfo;
}
interface ApiUsageDatasourcesInfo {
ds?: AliasFilterPair;
}
export const ApiUsageModelDefinition: WidgetModelDefinition<ApiUsageDatasourcesInfo> = {
testWidget(widget: Widget): boolean {
if (widget?.config?.settings) {
const settings = widget.config.settings;
if (settings.apiUsageDataKeys && Array.isArray(settings.apiUsageDataKeys)) {
return true;
}
}
return false;
},
prepareExportInfo(dashboard: Dashboard, widget: Widget): ApiUsageDatasourcesInfo {
const settings: ApiUsageWidgetSettings = widget.config.settings as ApiUsageWidgetSettings;
const info: ApiUsageDatasourcesInfo = {};
if (settings.dsEntityAliasId) {
info.ds = prepareExportDataSourcesInfo(dashboard, settings.dsEntityAliasId);
}
return info;
},
updateFromExportInfo(widget: Widget, entityAliases: EntityAliases, filters: Filters, info: ApiUsageDatasourcesInfo): void {
const settings: ApiUsageWidgetSettings = widget.config.settings as ApiUsageWidgetSettings;
if (info?.ds?.alias) {
settings.dsEntityAliasId = getEntityAliasId(entityAliases, info.ds.alias);
}
},
datasources(widget: Widget): Datasource[] {
const settings: ApiUsageWidgetSettings = widget.config.settings as ApiUsageWidgetSettings;
const datasources: Datasource[] = [];
if (settings.apiUsageDataKeys?.length && settings.dsEntityAliasId) {
datasources.push({
type: DatasourceType.entity,
name: '',
entityAliasId: settings.dsEntityAliasId,
dataKeys: getUniqueDataKeys(settings.apiUsageDataKeys)
});
}
return datasources;
},
hasTimewindow(): boolean {
return false;
}
};
const prepareExportDataSourcesInfo = (dashboard: Dashboard, settings: string): AliasFilterPair => {
const aliasAndFilter: AliasFilterPair = {};
const entityAlias = dashboard.configuration.entityAliases[settings];
if (entityAlias) {
aliasAndFilter.alias = {
alias: entityAlias.alias,
filter: entityAlias.filter
};
}
return aliasAndFilter;
}

62
ui-ngx/src/app/shared/models/widget/maps/map-model.definition.ts

@ -17,18 +17,15 @@
import { EntityAliases, EntityAliasInfo, getEntityAliasId } from '@shared/models/alias.models';
import { FilterInfo, Filters, getFilterId } from '@shared/models/query/query.models';
import { Dashboard } from '@shared/models/dashboard.models';
import { DataKey, Datasource, datasourcesHasAggregation, DatasourceType, Widget } from '@shared/models/widget.models';
import { Datasource, datasourcesHasAggregation, DatasourceType, Widget } from '@shared/models/widget.models';
import {
additionalMapDataSourcesToDatasources,
BaseMapSettings,
CirclesDataLayerSettings,
MapDataLayerSettings,
MapDataLayerType,
MapDataSourceSettings,
mapDataSourceSettingsToDatasource,
MapType,
MarkersDataLayerSettings,
PolygonsDataLayerSettings
MapType
} from '@shared/models/widget/maps/map.models';
import { WidgetModelDefinition } from '@shared/models/widget/widget-model.definition';
@ -126,7 +123,7 @@ export const MapModelDefinition: WidgetModelDefinition<MapDatasourcesInfo> = {
datasources.push(...getMapDataLayersDatasources(settings.circles));
}
if (settings.additionalDataSources?.length) {
datasources.push(...getMapDataLayersDatasources(settings.additionalDataSources));
datasources.push(...additionalMapDataSourcesToDatasources(settings.additionalDataSources));
}
return datasources;
},
@ -137,13 +134,13 @@ export const MapModelDefinition: WidgetModelDefinition<MapDatasourcesInfo> = {
} else {
const datasources: Datasource[] = [];
if (settings.markers?.length) {
datasources.push(...getMapLatestDataLayersDatasources(settings.markers, 'markers'));
datasources.push(...getMapDataLayersDatasources(settings.markers, true, 'markers'));
}
if (settings.polygons?.length) {
datasources.push(...getMapLatestDataLayersDatasources(settings.polygons, 'polygons'));
datasources.push(...getMapDataLayersDatasources(settings.polygons, true, 'polygons'));
}
if (settings.circles?.length) {
datasources.push(...getMapLatestDataLayersDatasources(settings.circles, 'circles'));
datasources.push(...getMapDataLayersDatasources(settings.circles, true, 'circles'));
}
if (settings.additionalDataSources?.length) {
datasources.push(...additionalMapDataSourcesToDatasources(settings.additionalDataSources));
@ -237,52 +234,21 @@ const prepareAliasAndFilterPair = (dashboard: Dashboard, settings: MapDataSource
}
}
const getMapDataLayersDatasources = (settings: MapDataLayerSettings[] | MapDataSourceSettings[]): Datasource[] => {
const getMapDataLayersDatasources = (settings: MapDataLayerSettings[],
includeDataKeys = false, dataLayerType?: MapDataLayerType): Datasource[] => {
const datasources: Datasource[] = [];
settings.forEach((dsSettings) => {
datasources.push(mapDataSourceSettingsToDatasource(dsSettings));
if ((dsSettings as MapDataLayerSettings).additionalDataSources?.length) {
(dsSettings as MapDataLayerSettings).additionalDataSources.forEach((ds) => {
datasources.push(mapDataSourceSettingsToDatasource(ds));
});
}
});
return datasources;
};
const getMapLatestDataLayersDatasources = (settings: MapDataLayerSettings[],
dataLayerType: MapDataLayerType): Datasource[] => {
const datasources: Datasource[] = [];
settings.forEach((dsSettings) => {
const dataKeys: DataKey[] = getMapLatestDataLayerDatasourceDataKeys(dsSettings, dataLayerType);
const datasource: Datasource = mapDataSourceSettingsToDatasource(dsSettings);
datasource.dataKeys.push(...dataKeys);
const datasource: Datasource = mapDataSourceSettingsToDatasource(dsSettings, null, includeDataKeys, dataLayerType);
datasources.push(datasource);
if ((dsSettings).additionalDataSources?.length) {
(dsSettings).additionalDataSources.forEach((ds) => {
if (dsSettings.additionalDataSources?.length) {
dsSettings.additionalDataSources.forEach((ds) => {
const additionalDatasource: Datasource = mapDataSourceSettingsToDatasource(ds);
additionalDatasource.dataKeys.push(...dataKeys);
if (includeDataKeys) {
additionalDatasource.dataKeys.push(...datasource.dataKeys);
}
datasources.push(additionalDatasource);
});
}
});
return datasources;
};
const getMapLatestDataLayerDatasourceDataKeys = (settings: MapDataLayerSettings,
dataLayerType: MapDataLayerType): DataKey[] => {
const dataKeys = settings.additionalDataKeys || [];
switch (dataLayerType) {
case 'markers':
const markersSettings = settings as MarkersDataLayerSettings;
dataKeys.push(markersSettings.xKey, markersSettings.yKey);
break;
case 'polygons':
dataKeys.push((settings as PolygonsDataLayerSettings).polygonKey);
break;
case 'circles':
dataKeys.push((settings as CirclesDataLayerSettings).circleKey);
break;
}
return dataKeys;
};

35
ui-ngx/src/app/shared/models/widget/maps/map.models.ts

@ -24,6 +24,7 @@ import {
} from '@shared/models/widget.models';
import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry.models';
import {
deepClone,
guid,
hashCode,
isDefinedAndNotNull,
@ -61,19 +62,44 @@ export interface TbMapDatasource extends Datasource {
mapDataIds: string[];
}
export const mapDataSourceSettingsToDatasource = (settings: MapDataSourceSettings, id = guid()): TbMapDatasource => {
export const mapDataSourceSettingsToDatasource = (settings: MapDataSourceSettings | MapDataLayerSettings,
id = guid(),
includeDataKeys = false, dataLayerType?: MapDataLayerType): TbMapDatasource => {
const dataKeys = includeDataKeys ? mapDataLayerDatasourceDataKeys((settings as MapDataLayerSettings), dataLayerType) : [];
return {
type: settings.dsType,
name: settings.dsLabel,
deviceId: settings.dsDeviceId,
entityAliasId: settings.dsEntityAliasId,
filterId: settings.dsFilterId,
dataKeys: [],
dataKeys: dataKeys,
latestDataKeys: [],
mapDataIds: [id]
};
};
const mapDataLayerDatasourceDataKeys = (settings: MapDataLayerSettings,
dataLayerType: MapDataLayerType): DataKey[] => {
const dataKeys = settings.additionalDataKeys?.length ? deepClone(settings.additionalDataKeys) : [];
switch (dataLayerType) {
case 'trips':
const tripsSettings = settings as TripsDataLayerSettings;
dataKeys.push(tripsSettings.xKey, tripsSettings.yKey);
break;
case 'markers':
const markersSettings = settings as MarkersDataLayerSettings;
dataKeys.push(markersSettings.xKey, markersSettings.yKey);
break;
case 'polygons':
dataKeys.push((settings as PolygonsDataLayerSettings).polygonKey);
break;
case 'circles':
dataKeys.push((settings as CirclesDataLayerSettings).circleKey);
break;
}
return dataKeys;
};
export enum DataLayerPatternType {
pattern = 'pattern',
@ -658,10 +684,11 @@ export interface AdditionalMapDataSourceSettings extends MapDataSourceSettings {
dataKeys: DataKey[];
}
export const additionalMapDataSourcesToDatasources = (additionalMapDataSources: AdditionalMapDataSourceSettings[]): TbMapDatasource[] => {
export const additionalMapDataSourcesToDatasources = (additionalMapDataSources: AdditionalMapDataSourceSettings[],
includeDataKeys = true): TbMapDatasource[] => {
return additionalMapDataSources.map(addDs => {
const res = mapDataSourceSettingsToDatasource(addDs);
res.dataKeys = addDs.dataKeys;
res.dataKeys = includeDataKeys ? addDs.dataKeys : [];
return res;
});
};

4
ui-ngx/src/app/shared/models/widget/widget-model.definition.ts

@ -19,6 +19,7 @@ import { Dashboard } from '@shared/models/dashboard.models';
import { EntityAliases } from '@shared/models/alias.models';
import { Filters } from '@shared/models/query/query.models';
import { MapModelDefinition } from '@shared/models/widget/maps/map-model.definition';
import { ApiUsageModelDefinition } from '@shared/models/widget/home-widgets/api-usage-model.definition';
export interface WidgetModelDefinition<T = any> {
testWidget(widget: Widget): boolean;
@ -29,7 +30,8 @@ export interface WidgetModelDefinition<T = any> {
}
const widgetModelRegistry: WidgetModelDefinition[] = [
MapModelDefinition
MapModelDefinition,
ApiUsageModelDefinition
];
export const findWidgetModelDefinition = (widget: Widget): WidgetModelDefinition => {

2440
ui-ngx/src/assets/dashboard/api_usage.json

File diff suppressed because it is too large

6
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -1429,6 +1429,8 @@
"mobile-order": "Dashboard order in mobile application",
"mobile-hide": "Hide dashboard in mobile application",
"update-image": "Update dashboard image",
"update-new-version": "Upload new version",
"upload-file-to-update": "Upload file to update",
"take-screenshot": "Take screenshot",
"select-widget-title": "Select widget",
"select-widget-value": "{{title}}: select widget",
@ -9030,6 +9032,7 @@
"show-empty-space-hidden-action": "Show empty space instead of hidden cell button action",
"dont-reserve-space-hidden-action": "Don't reserve space for hidden action buttons",
"display-timestamp": "Timestamp",
"timestamp-column-name":"Timestamp",
"display-pagination": "Display pagination",
"default-page-size": "Default page size",
"page-step-settings": "Page step settings",
@ -9601,7 +9604,8 @@
"add-key": "Add key",
"no-key": "No key",
"delete-key": "Delete key",
"target-dashboard-state": "Target dashboard state"
"target-dashboard-state": "Target dashboard state",
"go-to-main-state": "Go to default view"
}
},
"color": {

Loading…
Cancel
Save