Browse Source

Merge remote-tracking branch 'upstream/master' into oauth2ClientsSortFIx

pull/11579/head
rusikv 2 years ago
parent
commit
827771dabb
  1. 2
      application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java
  2. 2
      application/src/main/java/org/thingsboard/server/service/entitiy/edge/DefaultTbEdgeService.java
  3. 63
      application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
  4. 108
      application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java
  5. 2
      application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java
  6. 10
      common/cache/src/main/java/org/thingsboard/server/cache/edge/RelatedEdgesCacheValue.java
  7. 4
      common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java
  8. 10
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java
  9. 8
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNodeConfiguration.java
  10. 16
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java
  11. 65
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNodeTest.java
  12. 36
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java
  13. 2
      ui-ngx/src/app/core/auth/auth.service.ts
  14. 2
      ui-ngx/src/app/core/http/entity.service.ts
  15. 17
      ui-ngx/src/app/core/http/image.service.ts
  16. 27
      ui-ngx/src/app/core/services/dashboard-utils.service.ts
  17. 11
      ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts
  18. 42
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts
  19. 4
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html
  20. 4
      ui-ngx/src/app/modules/home/components/dashboard-page/layout/add-new-breakpoint-dialog.component.html
  21. 21
      ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.ts
  22. 1
      ui-ngx/src/app/modules/home/components/dashboard-page/layout/layout.models.ts
  23. 2
      ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.html
  24. 2
      ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.ts
  25. 13
      ui-ngx/src/app/modules/home/components/entity/contact-based.component.ts
  26. 10
      ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts
  27. 4
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.html
  28. 4
      ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html
  29. 3
      ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts
  30. 36
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.html
  31. 45
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts
  32. 291
      ui-ngx/src/app/modules/home/models/contact.models.ts
  33. 176
      ui-ngx/src/app/modules/home/models/dashboard-component.models.ts
  34. 7
      ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.html
  35. 8
      ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.ts
  36. 2
      ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/clients-table-config.resolver.ts
  37. 2
      ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain-table-config.resolver.ts
  38. 6
      ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-config.resolver.ts
  39. 3
      ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app.component.html
  40. 2
      ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app.component.ts
  41. 6
      ui-ngx/src/app/modules/home/pages/customer/customer.component.ts
  42. 6
      ui-ngx/src/app/modules/home/pages/tenant/tenant.component.ts
  43. 13
      ui-ngx/src/app/shared/components/contact.component.html
  44. 12
      ui-ngx/src/app/shared/components/contact.component.ts
  45. 58
      ui-ngx/src/app/shared/components/country-autocomplete.component.html
  46. 209
      ui-ngx/src/app/shared/components/country-autocomplete.component.ts
  47. 4
      ui-ngx/src/app/shared/components/dashboard-autocomplete.component.html
  48. 2
      ui-ngx/src/app/shared/components/image/image-gallery.component.ts
  49. 2
      ui-ngx/src/app/shared/components/image/images-datasource.ts
  50. 118
      ui-ngx/src/app/shared/models/country.models.ts
  51. 2
      ui-ngx/src/app/shared/models/dashboard.models.ts
  52. 2
      ui-ngx/src/app/shared/models/widget.models.ts
  53. 7
      ui-ngx/src/app/shared/shared.module.ts
  54. 15
      ui-ngx/src/assets/locale/locale.constant-en_US.json

2
application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java

@ -75,7 +75,7 @@ public class OAuth2Controller extends BaseController {
@ApiOperation(value = "Get OAuth2 clients (getOAuth2Clients)", notes = "Get the list of OAuth2 clients " +
"to log in with, available for such domain scheme (HTTP or HTTPS) (if x-forwarded-proto request header is present - " +
"the scheme is known from it) and domain name and port (port may be known from x-forwarded-port header)")
@PostMapping(value = "/noauth/oauth2/client")
@PostMapping(value = "/noauth/oauth2Clients")
public List<OAuth2ClientLoginInfo> getOAuth2Clients(HttpServletRequest request,
@Parameter(description = "Mobile application package name, to find OAuth2 clients " +
"where there is configured mobile application with such package name")

2
application/src/main/java/org/thingsboard/server/service/entitiy/edge/DefaultTbEdgeService.java

@ -54,7 +54,7 @@ public class DefaultTbEdgeService extends AbstractTbEntityService implements TbE
if (ActionType.ADDED.equals(actionType)) {
ruleChainService.assignRuleChainToEdge(tenantId, edgeTemplateRootRuleChain.getId(), edgeId);
edgeService.setEdgeRootRuleChain(tenantId, savedEdge, edgeTemplateRootRuleChain.getId());
savedEdge = edgeService.setEdgeRootRuleChain(tenantId, savedEdge, edgeTemplateRootRuleChain.getId());
edgeService.assignDefaultRuleChainsToEdge(tenantId, edgeId);
}

63
application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java

@ -157,7 +157,8 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
private final DbTypeInfoComponent dbTypeInfoComponent;
private final TbApiUsageReportClient apiUsageReportClient;
private final NotificationRuleProcessor notificationRuleProcessor;
@Autowired @Lazy
@Autowired
@Lazy
private TelemetrySubscriptionService tsSubService;
@Value("${state.defaultInactivityTimeoutInSec}")
@ -362,14 +363,16 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
if (proto.getAdded()) {
Futures.addCallback(fetchDeviceState(device), new FutureCallback<>() {
@Override
public void onSuccess(@Nullable DeviceStateData state) {
public void onSuccess(DeviceStateData state) {
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, device.getId());
if (addDeviceUsingState(tpi, state)) {
save(deviceId, ACTIVITY_STATE, false);
Set<DeviceId> deviceIds = partitionedEntities.get(tpi);
boolean isMyPartition = deviceIds != null;
if (isMyPartition) {
deviceIds.add(state.getDeviceId());
initializeActivityState(deviceId, state);
callback.onSuccess();
} else {
log.debug("[{}][{}] Device belongs to external partition. Probably rebalancing is in progress. Topic: {}"
, tenantId, deviceId, tpi.getFullTopicName());
log.debug("[{}][{}] Device belongs to external partition. Probably rebalancing is in progress. Topic: {}", tenantId, deviceId, tpi.getFullTopicName());
callback.onFailure(new RuntimeException("Device belongs to external partition " + tpi.getFullTopicName() + "!"));
}
}
@ -400,6 +403,21 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
}
}
private void onDeviceDeleted(TenantId tenantId, DeviceId deviceId) {
cleanupEntity(deviceId);
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId);
Set<DeviceId> deviceIdSet = partitionedEntities.get(tpi);
if (deviceIdSet != null) {
deviceIdSet.remove(deviceId);
}
}
private void initializeActivityState(DeviceId deviceId, DeviceStateData fetchedState) {
DeviceStateData cachedState = deviceStates.putIfAbsent(fetchedState.getDeviceId(), fetchedState);
boolean activityState = Objects.requireNonNullElse(cachedState, fetchedState).getState().isActive();
save(deviceId, ACTIVITY_STATE, activityState);
}
@Override
protected Map<TopicPartitionInfo, List<ListenableFuture<?>>> onAddedPartitions(Set<TopicPartitionInfo> addedPartitions) {
var result = new HashMap<TopicPartitionInfo, List<ListenableFuture<?>>>();
@ -436,10 +454,16 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
}
if (devicePackFutureHolder.future == null || !devicePackFutureHolder.future.isCancelled()) {
for (var state : states) {
if (!addDeviceUsingState(entry.getKey(), state)) {
return;
TopicPartitionInfo tpi = entry.getKey();
Set<DeviceId> deviceIds = partitionedEntities.get(tpi);
boolean isMyPartition = deviceIds != null;
if (isMyPartition) {
deviceIds.add(state.getDeviceId());
deviceStates.putIfAbsent(state.getDeviceId(), state);
checkAndUpdateState(state.getDeviceId(), state);
} else {
log.debug("[{}] Device belongs to external partition {}", state.getDeviceId(), tpi.getFullTopicName());
}
checkAndUpdateState(state.getDeviceId(), state);
}
log.info("[{}] Initialized {} out of {} device states", entry.getKey().getPartition().orElse(0), counter.addAndGet(states.size()), entry.getValue().size());
}
@ -475,18 +499,6 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
}
}
private boolean addDeviceUsingState(TopicPartitionInfo tpi, DeviceStateData state) {
Set<DeviceId> deviceIds = partitionedEntities.get(tpi);
if (deviceIds != null) {
deviceIds.add(state.getDeviceId());
deviceStates.putIfAbsent(state.getDeviceId(), state);
return true;
} else {
log.debug("[{}] Device belongs to external partition {}", state.getDeviceId(), tpi.getFullTopicName());
return false;
}
}
void checkStates() {
try {
final long ts = getCurrentTimeMillis();
@ -619,15 +631,6 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
return cleanup;
}
private void onDeviceDeleted(TenantId tenantId, DeviceId deviceId) {
cleanupEntity(deviceId);
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId);
Set<DeviceId> deviceIdSet = partitionedEntities.get(tpi);
if (deviceIdSet != null) {
deviceIdSet.remove(deviceId);
}
}
@Override
protected void cleanupEntityOnPartitionRemoval(DeviceId deviceId) {
cleanupEntity(deviceId);

108
application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.service.state;
import com.google.common.util.concurrent.Futures;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ -28,8 +29,10 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceIdInfo;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.notification.rule.trigger.DeviceActivityTrigger;
@ -41,11 +44,13 @@ import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.sql.query.EntityQueryRepository;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.QueueKey;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
@ -66,6 +71,7 @@ import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
@ -1070,4 +1076,106 @@ public class DefaultDeviceStateServiceTest {
then(service).should().fetchDeviceStateDataUsingSeparateRequests(deviceId);
}
@Test
public void givenDeviceAdded_whenOnQueueMsg_thenShouldCacheAndSaveActivityToFalse() throws InterruptedException {
// GIVEN
final long defaultTimeout = 1000;
initStateService(defaultTimeout);
given(deviceService.findDeviceById(any(TenantId.class), any(DeviceId.class))).willReturn(new Device(deviceId));
given(attributesService.find(any(TenantId.class), any(EntityId.class), any(AttributeScope.class), anyList())).willReturn(Futures.immediateFuture(Collections.emptyList()));
TransportProtos.DeviceStateServiceMsgProto proto = TransportProtos.DeviceStateServiceMsgProto.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setDeviceIdMSB(deviceId.getId().getMostSignificantBits())
.setDeviceIdLSB(deviceId.getId().getLeastSignificantBits())
.setAdded(true)
.setUpdated(false)
.setDeleted(false)
.build();
// WHEN
service.onQueueMsg(proto, TbCallback.EMPTY);
// THEN
await().atMost(1, TimeUnit.SECONDS).untilAsserted(() -> {
assertThat(service.deviceStates.get(deviceId).getState().isActive()).isEqualTo(false);
then(telemetrySubscriptionService).should().saveAttrAndNotify(eq(TenantId.SYS_TENANT_ID), eq(deviceId), eq(AttributeScope.SERVER_SCOPE), eq(ACTIVITY_STATE), eq(false), any());
});
}
@Test
public void givenDeviceActivityEventHappenedAfterAdded_whenOnDeviceActivity_thenShouldCacheAndSaveActivityToTrue() throws InterruptedException {
// GIVEN
final long defaultTimeout = 1000;
initStateService(defaultTimeout);
long currentTime = System.currentTimeMillis();
DeviceState deviceState = DeviceState.builder()
.active(false)
.inactivityTimeout(service.getDefaultInactivityTimeoutInSec())
.build();
DeviceStateData stateData = DeviceStateData.builder()
.tenantId(tenantId)
.deviceId(deviceId)
.deviceCreationTime(currentTime - 10000)
.state(deviceState)
.metaData(TbMsgMetaData.EMPTY)
.build();
service.deviceStates.put(deviceId, stateData);
// WHEN
service.onDeviceActivity(tenantId, deviceId, currentTime);
// THEN
await().atMost(1, TimeUnit.SECONDS).untilAsserted(() -> {
assertThat(service.deviceStates.get(deviceId).getState().isActive()).isEqualTo(true);
then(telemetrySubscriptionService).should().saveAttrAndNotify(eq(TenantId.SYS_TENANT_ID), eq(deviceId), eq(AttributeScope.SERVER_SCOPE), eq(LAST_ACTIVITY_TIME), eq(currentTime), any());
then(telemetrySubscriptionService).should().saveAttrAndNotify(eq(TenantId.SYS_TENANT_ID), eq(deviceId), eq(AttributeScope.SERVER_SCOPE), eq(ACTIVITY_STATE), eq(true), any());
});
}
@Test
public void givenDeviceActivityEventHappenedBeforeAdded_whenOnQueueMsg_thenShouldSaveActivityStateUsingValueFromCache() throws InterruptedException {
// GIVEN
final long defaultTimeout = 1000;
initStateService(defaultTimeout);
given(deviceService.findDeviceById(any(TenantId.class), any(DeviceId.class))).willReturn(new Device(deviceId));
given(attributesService.find(any(TenantId.class), any(EntityId.class), any(AttributeScope.class), anyList())).willReturn(Futures.immediateFuture(Collections.emptyList()));
long currentTime = System.currentTimeMillis();
DeviceState deviceState = DeviceState.builder()
.active(true)
.lastConnectTime(currentTime - 8000)
.lastActivityTime(currentTime - 4000)
.lastDisconnectTime(0)
.lastInactivityAlarmTime(0)
.inactivityTimeout(3000)
.build();
DeviceStateData stateData = DeviceStateData.builder()
.tenantId(tenantId)
.deviceId(deviceId)
.deviceCreationTime(currentTime - 10000)
.state(deviceState)
.build();
service.deviceStates.put(deviceId, stateData);
// WHEN
TransportProtos.DeviceStateServiceMsgProto proto = TransportProtos.DeviceStateServiceMsgProto.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setDeviceIdMSB(deviceId.getId().getMostSignificantBits())
.setDeviceIdLSB(deviceId.getId().getLeastSignificantBits())
.setAdded(true)
.setUpdated(false)
.setDeleted(false)
.build();
service.onQueueMsg(proto, TbCallback.EMPTY);
// THEN
await().atMost(1, TimeUnit.SECONDS).untilAsserted(() -> {
assertThat(service.deviceStates.get(deviceId).getState().isActive()).isEqualTo(true);
then(telemetrySubscriptionService).should().saveAttrAndNotify(eq(TenantId.SYS_TENANT_ID), eq(deviceId), eq(AttributeScope.SERVER_SCOPE), eq(ACTIVITY_STATE), eq(true), any());
});
}
}

2
application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java

@ -241,7 +241,7 @@ public class VersionControlTest extends AbstractControllerTest {
checkImportedEntity(tenantId1, device, tenantId1, importedDevice);
assertThat(importedDevice.getDeviceProfileId()).isEqualTo(device.getDeviceProfileId());
assertThat(findDeviceCredentials(device.getId())).isEqualTo(deviceCredentials);
assertThat(findDeviceCredentials(device.getId())).isEqualToIgnoringGivenFields(deviceCredentials, "version");
assertThat(importedDevice.getFirmwareId()).isEqualTo(firmware.getId());
assertThat(importedDevice.getSoftwareId()).isEqualTo(software.getId());
}

10
common/cache/src/main/java/org/thingsboard/server/cache/edge/RelatedEdgesCacheValue.java

@ -15,7 +15,9 @@
*/
package org.thingsboard.server.cache.edge;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.page.PageData;
@ -23,15 +25,13 @@ import java.io.Serial;
import java.io.Serializable;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class RelatedEdgesCacheValue implements Serializable {
@Serial
private static final long serialVersionUID = -2765080094748518572L;
private final PageData<EdgeId> pageData;
public RelatedEdgesCacheValue(PageData<EdgeId> pageData) {
this.pageData = pageData;
}
private PageData<EdgeId> pageData;
}

4
common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java

@ -75,9 +75,9 @@ public class Edge extends BaseDataWithAdditionalInfo<EdgeId> implements HasLabel
this.tenantId = edge.getTenantId();
this.customerId = edge.getCustomerId();
this.rootRuleChainId = edge.getRootRuleChainId();
this.name = edge.getName();
this.type = edge.getType();
this.label = edge.getLabel();
this.name = edge.getName();
this.routingKey = edge.getRoutingKey();
this.secret = edge.getSecret();
this.version = edge.getVersion();
@ -87,9 +87,9 @@ public class Edge extends BaseDataWithAdditionalInfo<EdgeId> implements HasLabel
this.tenantId = edge.getTenantId();
this.customerId = edge.getCustomerId();
this.rootRuleChainId = edge.getRootRuleChainId();
this.name = edge.getName();
this.type = edge.getType();
this.label = edge.getLabel();
this.name = edge.getName();
this.routingKey = edge.getRoutingKey();
this.secret = edge.getSecret();
this.version = edge.getVersion();

10
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java

@ -35,10 +35,13 @@ import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.exception.DataValidationException;
import java.nio.ByteBuffer;
import java.util.concurrent.TimeUnit;
import static org.thingsboard.server.dao.service.ConstraintValidator.validateFields;
@Slf4j
@RuleNode(
type = ComponentType.EXTERNAL,
@ -62,10 +65,9 @@ public class TbAwsLambdaNode extends TbAbstractExternalNode {
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
config = TbNodeUtils.convert(configuration, TbAwsLambdaNodeConfiguration.class);
if (StringUtils.isBlank(config.getFunctionName())) {
throw new TbNodeException("Function name must be set!", true);
}
String errorPrefix = "'" + ctx.getSelf().getName() + "' node configuration is invalid: ";
try {
validateFields(config, errorPrefix);
AWSCredentials awsCredentials = new BasicAWSCredentials(config.getAccessKey(), config.getSecretKey());
client = AWSLambdaAsyncClientBuilder.standard()
.withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
@ -74,6 +76,8 @@ public class TbAwsLambdaNode extends TbAbstractExternalNode {
.withConnectionTimeout((int) TimeUnit.SECONDS.toMillis(config.getConnectionTimeout()))
.withRequestTimeout((int) TimeUnit.SECONDS.toMillis(config.getRequestTimeout())))
.build();
} catch (DataValidationException e) {
throw new TbNodeException(e, true);
} catch (Exception e) {
throw new TbNodeException(e);
}

8
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNodeConfiguration.java

@ -15,6 +15,8 @@
*/
package org.thingsboard.rule.engine.aws.lambda;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import org.thingsboard.rule.engine.api.NodeConfiguration;
@ -23,12 +25,18 @@ public class TbAwsLambdaNodeConfiguration implements NodeConfiguration<TbAwsLamb
public static final String DEFAULT_QUALIFIER = "$LATEST";
@NotBlank
private String accessKey;
@NotBlank
private String secretKey;
@NotBlank
private String region;
@NotBlank
private String functionName;
private String qualifier;
@Min(0)
private int connectionTimeout;
@Min(0)
private int requestTimeout;
private boolean tellFailureIfFuncThrowsExc;

16
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java

@ -77,6 +77,8 @@ public class TbMqttNode extends TbAbstractExternalNode {
this.mqttNodeConfiguration = TbNodeUtils.convert(configuration, TbMqttNodeConfiguration.class);
try {
this.mqttClient = initClient(ctx);
} catch (TbNodeException e) {
throw e;
} catch (Exception e) {
throw new TbNodeException(e);
}
@ -119,8 +121,7 @@ public class TbMqttNode extends TbAbstractExternalNode {
MqttClientConfig config = new MqttClientConfig(getSslContext());
config.setOwnerId(getOwnerId(ctx));
if (!StringUtils.isEmpty(this.mqttNodeConfiguration.getClientId())) {
config.setClientId(this.mqttNodeConfiguration.isAppendClientIdSuffix() ?
this.mqttNodeConfiguration.getClientId() + "_" + ctx.getServiceId() : this.mqttNodeConfiguration.getClientId());
config.setClientId(getClientId(ctx));
}
config.setCleanSession(this.mqttNodeConfiguration.isCleanSession());
@ -146,6 +147,17 @@ public class TbMqttNode extends TbAbstractExternalNode {
return client;
}
private String getClientId(TbContext ctx) throws TbNodeException {
String clientId = this.mqttNodeConfiguration.isAppendClientIdSuffix() ?
this.mqttNodeConfiguration.getClientId() + "_" + ctx.getServiceId() :
this.mqttNodeConfiguration.getClientId();
if (clientId.length() > 23) {
throw new TbNodeException("Client ID is too long '" + clientId + "'. " +
"The length of Client ID cannot be longer than 23, but current length is " + clientId.length() + ".", true);
}
return clientId;
}
MqttClient getMqttClient(TbContext ctx, MqttClientConfig config) {
return MqttClient.create(config, null, ctx.getExternalCallExecutor());
}

65
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNodeTest.java

@ -41,6 +41,7 @@ import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
@ -50,8 +51,8 @@ import java.util.Map;
import java.util.UUID;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
@ -72,13 +73,17 @@ public class TbAwsLambdaNodeTest {
private AWSLambdaAsync clientMock;
@BeforeEach
void setUp() {
public void setUp() {
node = new TbAwsLambdaNode();
config = new TbAwsLambdaNodeConfiguration().defaultConfiguration();
config.setAccessKey("accessKey");
config.setSecretKey("secretKey");
config.setFunctionName("new-function");
}
@Test
public void verifyDefaultConfig() {
config = new TbAwsLambdaNodeConfiguration().defaultConfiguration();
assertThat(config.getAccessKey()).isNull();
assertThat(config.getSecretKey()).isNull();
assertThat(config.getRegion()).isEqualTo(("us-east-1"));
@ -94,10 +99,43 @@ public class TbAwsLambdaNodeTest {
@ValueSource(strings = " ")
public void givenInvalidFunctionName_whenInit_thenThrowsException(String funcName) {
config.setFunctionName(funcName);
var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
assertThatThrownBy(() -> node.init(ctx, configuration))
.isInstanceOf(TbNodeException.class)
.hasMessage("Function name must be set!");
verifyValidationExceptionOnInit();
}
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = " ")
public void givenInvalidAccessKey_whenInit_thenThrowsException(String accessKey) {
config.setAccessKey(accessKey);
verifyValidationExceptionOnInit();
}
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = " ")
public void givenInvalidSecretAccessKey_whenInit_thenThrowsException(String secretAccessKey) {
config.setSecretKey(secretAccessKey);
verifyValidationExceptionOnInit();
}
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = " ")
public void givenInvalidRegion_whenInit_thenThrowsException(String region) {
config.setRegion(region);
verifyValidationExceptionOnInit();
}
@Test
public void givenInvalidConnectionTimeout_whenInit_thenThrowsException() {
config.setConnectionTimeout(-100);
verifyValidationExceptionOnInit();
}
@Test
public void givenInvalidRequestTimeout_whenInit_thenThrowsException() {
config.setRequestTimeout(-100);
verifyValidationExceptionOnInit();
}
@ParameterizedTest
@ -280,10 +318,19 @@ public class TbAwsLambdaNodeTest {
assertThat(throwableCaptor.getValue()).isInstanceOf(AWSLambdaException.class).hasMessageStartingWith(errorMsg);
}
private void verifyValidationExceptionOnInit() {
RuleNode ruleNode = new RuleNode();
ruleNode.setName("test");
when(ctx.getSelf()).thenReturn(ruleNode);
String errorPrefix = "'test' node configuration is invalid: ";
assertThatThrownBy(() -> node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))))
.isInstanceOf(TbNodeException.class)
.hasMessageContaining(errorPrefix)
.extracting(e -> ((TbNodeException) e).isUnrecoverable())
.isEqualTo(true);
}
private void init() {
config.setAccessKey("accessKey");
config.setSecretKey("secretKey");
config.setFunctionName("new-function");
ReflectionTestUtils.setField(node, "client", clientMock);
ReflectionTestUtils.setField(node, "config", config);
}

36
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java

@ -191,6 +191,42 @@ public class TbMqttNodeTest extends AbstractRuleNodeUpgradeTest {
assertThatNoException().isThrownBy(() -> mqttNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(mqttNodeConfig))));
}
@Test
public void givenClientIdIsTooLong_whenInit_thenThrowsException() {
String invalidClientId = "vhfrbeb38ygwfwrgfwefgterhytjytj";
mqttNodeConfig.setClientId(invalidClientId);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
given(ctxMock.getSelf()).willReturn(new RuleNode(RULE_NODE_ID));
assertThatThrownBy(() -> mqttNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(mqttNodeConfig))))
.isInstanceOf(TbNodeException.class)
.hasMessage("Client ID is too long '" + invalidClientId + "'. " +
"The length of Client ID cannot be longer than 23, but current length is " + invalidClientId.length() + ".")
.extracting(e -> ((TbNodeException) e).isUnrecoverable())
.isEqualTo(true);
}
@Test
public void givenClientIdIsOkAndAppendClientIdSuffixIsTrue_whenInit_thenClientIdBecomesInvalidAndThrowsException() {
String validClientId = "fertjnhnjj4ge";
mqttNodeConfig.setClientId("fertjnhnjj4ge");
mqttNodeConfig.setAppendClientIdSuffix(true);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
given(ctxMock.getSelf()).willReturn(new RuleNode(RULE_NODE_ID));
String serviceId = "test-service";
given(ctxMock.getServiceId()).willReturn(serviceId);
String resultedClientId = validClientId + "_" + serviceId;
assertThatThrownBy(() -> mqttNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(mqttNodeConfig))))
.isInstanceOf(TbNodeException.class)
.hasMessage("Client ID is too long '" + resultedClientId + "'. " +
"The length of Client ID cannot be longer than 23, but current length is " + resultedClientId.length() + ".")
.extracting(e -> ((TbNodeException) e).isUnrecoverable())
.isEqualTo(true);
}
@Test
public void givenFailedByTimeoutConnectResult_whenInit_thenThrowsException() throws ExecutionException, InterruptedException, TimeoutException {
mqttNodeConfig.setHost("localhost");

2
ui-ngx/src/app/core/auth/auth.service.ts

@ -224,7 +224,7 @@ export class AuthService {
}
public loadOAuth2Clients(): Observable<Array<OAuth2ClientLoginInfo>> {
const url = '/api/noauth/oauth2/client?platform=' + PlatformType.WEB;
const url = '/api/noauth/oauth2Clients?platform=' + PlatformType.WEB;
return this.http.post<Array<OAuth2ClientLoginInfo>>(url,
null, defaultHttpOptions()).pipe(
catchError(err => of([])),

2
ui-ngx/src/app/core/http/entity.service.ts

@ -459,7 +459,7 @@ export class EntityService {
entitiesObservable = this.queueService.getQueueStatistics(pageLink, config);
break;
case EntityType.OAUTH2_CLIENT:
pageLink.sortOrder.property = 'createdTime';
pageLink.sortOrder.property = 'title';
entitiesObservable = this.oauth2Service.findTenantOAuth2ClientInfos(pageLink, config);
break;
}

17
ui-ngx/src/app/core/http/image.service.ts

@ -21,11 +21,15 @@ import { defaultHttpOptionsFromConfig, defaultHttpUploadOptions, RequestConfig }
import { Observable, of, ReplaySubject } from 'rxjs';
import { PageData } from '@shared/models/page/page-data';
import {
NO_IMAGE_DATA_URI,
ImageExportData,
ImageResourceInfo,
imageResourceType,
ImageResourceType,
IMAGES_URL_PREFIX, isImageResourceUrl, ImageExportData, removeTbImagePrefix, ResourceSubType
imageResourceType,
IMAGES_URL_PREFIX,
isImageResourceUrl,
NO_IMAGE_DATA_URI,
removeTbImagePrefix,
ResourceSubType
} from '@shared/models/resource.models';
import { catchError, map, switchMap } from 'rxjs/operators';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
@ -46,7 +50,8 @@ export class ImageService {
) {
}
public uploadImage(file: File, title: string, imageSubType: ResourceSubType, config?: RequestConfig): Observable<ImageResourceInfo> {
public uploadImage(file: File, title: string, imageSubType: ResourceSubType = ResourceSubType.IMAGE,
config?: RequestConfig): Observable<ImageResourceInfo> {
if (!config) {
config = {};
}
@ -82,8 +87,8 @@ export class ImageService {
imageInfo, defaultHttpOptionsFromConfig(config));
}
public getImages(pageLink: PageLink, imageSubType: ResourceSubType,
includeSystemImages = false, config?: RequestConfig): Observable<PageData<ImageResourceInfo>> {
public getImages(pageLink: PageLink, includeSystemImages = false,
imageSubType: ResourceSubType = ResourceSubType.IMAGE, config?: RequestConfig): Observable<PageData<ImageResourceInfo>> {
return this.http.get<PageData<ImageResourceInfo>>(
`${IMAGES_URL_PREFIX}${pageLink.toQuery()}&imageSubType=${imageSubType}&includeSystemImages=${includeSystemImages}`,
defaultHttpOptionsFromConfig(config));

27
ui-ngx/src/app/core/services/dashboard-utils.service.ts

@ -59,6 +59,7 @@ import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { BackgroundType, colorBackground, isBackgroundSettings } from '@shared/models/widget-settings.models';
import { MediaBreakpoints } from '@shared/models/constants';
import { TranslateService } from '@ngx-translate/core';
import { DashboardPageLayout } from '@home/components/dashboard-page/dashboard-page.models';
@Injectable({
providedIn: 'root'
@ -344,10 +345,12 @@ export class DashboardUtilsService {
return widgetConfig;
}
public prepareWidgetForScadaLayout(widget: Widget): Widget {
public prepareWidgetForScadaLayout(widget: Widget, isScada: boolean): Widget {
const config = widget.config;
config.showTitle = false;
config.dropShadow = false;
config.resizable = !isScada;
config.preserveAspectRatio = isScada;
config.padding = '0';
config.margin = '0';
config.backgroundColor = 'rgba(0,0,0,0)';
@ -653,7 +656,9 @@ export class DashboardUtilsService {
mobileOrder: widget.config.mobileOrder,
mobileHeight: widget.config.mobileHeight,
mobileHide: widget.config.mobileHide,
desktopHide: widget.config.desktopHide
desktopHide: widget.config.desktopHide,
preserveAspectRatio: widget.config.preserveAspectRatio,
resizable: widget.config.resizable
};
if (isUndefined(originalColumns)) {
originalColumns = 24;
@ -1081,4 +1086,22 @@ export class DashboardUtilsService {
const maxStr = isDefined(currentData?.maxWidth) ? `max ${currentData.maxWidth}px` : '';
return minStr && maxStr ? `${minStr} - ${maxStr}` : `${minStr}${maxStr}`;
}
updatedLayoutForBreakpoint(layout: DashboardPageLayout, breakpointId: BreakpointId) {
let selectBreakpointId: BreakpointId = 'default';
if (layout.layoutCtx.layoutData[breakpointId]) {
selectBreakpointId = breakpointId;
}
layout.layoutCtx.breakpoint = selectBreakpointId;
const layoutInfo = layout.layoutCtx.layoutData[selectBreakpointId];
if (layoutInfo.gridSettings) {
layout.layoutCtx.gridSettings = layoutInfo.gridSettings;
}
layout.layoutCtx.widgets.setWidgetIds(layoutInfo.widgetIds);
layout.layoutCtx.widgetLayouts = layoutInfo.widgetLayouts;
if (layout.show && layout.layoutCtx.ctrl) {
layout.layoutCtx.ctrl.reload();
}
layout.layoutCtx.ignoreLoading = true;
}
}

11
ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts

@ -130,7 +130,14 @@ export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogCom
this.widgetConfig = {
widgetName: widgetInfo.widgetName,
config: this.widget.config,
layout: {},
layout: {
resizable: this.widget.config.resizable,
preserveAspectRatio: this.widget.config.preserveAspectRatio,
mobileHide: this.widget.config.mobileHide,
desktopHide: this.widget.config.desktopHide,
mobileOrder: this.widget.config.mobileOrder,
mobileHeight: this.widget.config.mobileHeight
},
widgetType: this.widget.type,
typeParameters,
actionSources,
@ -187,6 +194,8 @@ export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogCom
this.widget.config.mobileHeight = widgetConfig.layout.mobileHeight;
this.widget.config.mobileHide = widgetConfig.layout.mobileHide;
this.widget.config.desktopHide = widgetConfig.layout.desktopHide;
this.widget.config.preserveAspectRatio = widgetConfig.layout.preserveAspectRatio;
this.widget.config.resizable = widgetConfig.layout.resizable;
this.dialogRef.close(this.widget);
}
}

42
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts

@ -450,7 +450,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
}),
skip(1)
).subscribe(() => {
this.layouts.main.layoutCtx.ctrl.updatedCurrentBreakpoint();
this.dashboardUtils.updatedLayoutForBreakpoint(this.layouts.main, this.dashboardCtx.breakpoint);
this.updateLayoutSizes();
}
)
@ -759,6 +759,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
const prevMainLayoutWidth = this.mainLayoutSize.width;
const prevMainLayoutHeight = this.mainLayoutSize.height;
const prevMainLayoutMaxWidth = this.mainLayoutSize.maxWidth;
const prevMainLayoutMinWidth = this.mainLayoutSize.minWidth;
if (this.isEditingWidget && this.editingLayoutCtx.id === 'main') {
this.mainLayoutSize.width = '100%';
} else {
@ -773,19 +774,19 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
const xOffset = this.dashboardContainer.nativeElement.getBoundingClientRect().x;
const breakpoint = this.dashboardUtils.getBreakpointInfoById(this.layouts.main.layoutCtx.breakpoint);
let maxWidth: string;
if (breakpoint?.maxWidth) {
if (this.isMobileSize(breakpoint)) {
maxWidth = `${breakpoint.maxWidth}px`;
} else {
maxWidth = `${breakpoint.maxWidth - xOffset}px`;
let maxWidth = '100%';
let minWidth: string;
if (breakpoint) {
const isMobile = this.isMobileSize(breakpoint);
if (breakpoint.maxWidth) {
maxWidth = isMobile ? `${breakpoint.maxWidth}px` : `${breakpoint.maxWidth - xOffset}px`;
}
if (breakpoint.minWidth) {
minWidth = isMobile ? `${breakpoint.minWidth}px` : `${breakpoint.minWidth - xOffset}px`;
}
} else {
maxWidth = '100%';
}
const minWidth = breakpoint?.minWidth ? `${breakpoint.minWidth}px` : undefined;
this.mainLayoutSize.maxWidth = maxWidth;
this.mainLayoutSize.minWidth = minWidth;
} else {
@ -793,7 +794,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.mainLayoutSize.minWidth = undefined;
}
return prevMainLayoutWidth !== this.mainLayoutSize.width || prevMainLayoutHeight !== this.mainLayoutSize.height ||
prevMainLayoutMaxWidth !== this.mainLayoutSize.maxWidth;
prevMainLayoutMaxWidth !== this.mainLayoutSize.maxWidth || prevMainLayoutMinWidth !== this.mainLayoutSize.minWidth;
}
private updateRightLayoutSize(): boolean {
@ -920,7 +921,10 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
let gridSettings: GridSettings = null;
const layoutKeys = this.dashboardUtils.isSingleLayoutDashboard(this.dashboard);
if (layoutKeys) {
gridSettings = deepClone(this.dashboard.configuration.states[layoutKeys.state].layouts[layoutKeys.layout].gridSettings);
const layouts = this.dashboardUtils.getDashboardLayoutConfig(
this.dashboard.configuration.states[layoutKeys.state].layouts[layoutKeys.layout],
this.layouts[layoutKeys.layout].layoutCtx.breakpoint);
gridSettings = deepClone(layouts.gridSettings);
}
this.dialog.open<DashboardSettingsDialogComponent, DashboardSettingsDialogData,
DashboardSettingsDialogData>(DashboardSettingsDialogComponent, {
@ -938,9 +942,11 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.updateDashboardCss();
const newGridSettings = data.gridSettings;
if (newGridSettings) {
const layout = this.dashboard.configuration.states[layoutKeys.state].layouts[layoutKeys.layout];
this.dashboardUtils.updateLayoutSettings(layout, newGridSettings);
this.updateLayouts();
const layouts = deepClone(this.dashboard.configuration.states[layoutKeys.state].layouts);
const layoutConfig = this.dashboardUtils.getDashboardLayoutConfig(
layouts[layoutKeys.layout], this.layouts[layoutKeys.layout].layoutCtx.breakpoint);
this.dashboardUtils.updateLayoutSettings(layoutConfig, newGridSettings);
this.updateDashboardLayouts(layouts);
}
}
});
@ -1130,7 +1136,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
private updateLayout(layout: DashboardPageLayout, layoutInfo: DashboardLayoutInfo) {
layout.layoutCtx.layoutData = layoutInfo;
layout.layoutCtx.layoutDataChanged.next();
layout.layoutCtx.ctrl?.updatedCurrentBreakpoint(this.isEdit ? layout.layoutCtx.breakpoint : null, layout.show);
this.dashboardUtils.updatedLayoutForBreakpoint(layout, this.isEdit ? layout.layoutCtx.breakpoint : this.dashboardCtx.breakpoint);
this.updateLayoutSizes();
}
@ -1345,7 +1351,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
}
const scada = this.isAddingToScadaLayout();
if (scada) {
newWidget = this.dashboardUtils.prepareWidgetForScadaLayout(newWidget);
newWidget = this.dashboardUtils.prepareWidgetForScadaLayout(newWidget, widgetTypeInfo.scada);
}
let showLayoutConfig = true;
if (scada || this.layouts.right.show || !this.showLayoutConfigInEdit(this.layouts.main.layoutCtx)) {

4
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html

@ -152,8 +152,8 @@
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="(gridSettingsFormGroup.get('columns').hasError('required') ? 'dashboard.columns-count-required' :
(gridSettingsFormGroup.get('columns').hasError('min') ? 'dashboard.min-columns-count-message' : 'dashboard.max-columns-count-message')) | translate"
[matTooltip]="(gridSettingsFormGroup.get('minColumns').hasError('required') ? 'dashboard.columns-count-required' :
(gridSettingsFormGroup.get('minColumns').hasError('min') ? 'dashboard.min-columns-count-message' : 'dashboard.max-columns-count-message')) | translate"
*ngIf="gridSettingsFormGroup.get('minColumns').invalid && gridSettingsFormGroup.get('minColumns').touched"
class="tb-error">
warning

4
ui-ngx/src/app/modules/home/components/dashboard-page/layout/add-new-breakpoint-dialog.component.html

@ -30,7 +30,7 @@
<mat-label translate>layout.breakpoint</mat-label>
<mat-select formControlName="newBreakpointId" [hideSingleSelectionIndicator]="false">
<mat-select-trigger>{{ getName(this.addBreakpointFormGroup.get('newBreakpointId').value) }}</mat-select-trigger>
<mat-option *ngFor="let breakpoint of allowBreakpointIds" [value]="breakpoint">
<mat-option *ngFor="let breakpoint of allowBreakpointIds" [value]="breakpoint" style="min-height:56px;">
<mat-icon><tb-icon>{{ getIcon(breakpoint) }}</tb-icon></mat-icon>
<div>{{ getName(breakpoint) }}</div>
<div style="font-size: 12px;letter-spacing: 0.4px;color: #9e9e9e;">{{ getSizeDescription(breakpoint) }}</div>
@ -41,7 +41,7 @@
<mat-label translate>layout.copy-from</mat-label>
<mat-select formControlName="copyFrom" [hideSingleSelectionIndicator]="false">
<mat-select-trigger>{{ getName(this.addBreakpointFormGroup.get('copyFrom').value) }}</mat-select-trigger>
<mat-option *ngFor="let breakpoint of selectedBreakpointIds" [value]="breakpoint">
<mat-option *ngFor="let breakpoint of selectedBreakpointIds" [value]="breakpoint" style="min-height:56px;">
<mat-icon><tb-icon>{{ getIcon(breakpoint) }}</tb-icon></mat-icon>
<div>{{ getName(breakpoint) }}</div>
<div style="font-size: 12px;letter-spacing: 0.4px;color: #9e9e9e;">{{ getSizeDescription(breakpoint) }}</div>

21
ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.ts

@ -332,25 +332,4 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
const pos = this.dashboard.getEventGridPosition($event);
this.layoutCtx.dashboardCtrl.pasteWidgetReference($event, this.layoutCtx, pos);
}
updatedCurrentBreakpoint(breakpointId?: BreakpointId, showLayout = true) {
if (!isNotEmptyStr(breakpointId)) {
breakpointId = this.dashboardCtx.breakpoint;
}
if (this.layoutCtx.layoutData[breakpointId]) {
this.layoutCtx.breakpoint = breakpointId;
} else {
this.layoutCtx.breakpoint = 'default';
}
const layoutInfo = this.layoutCtx.layoutData[this.layoutCtx.breakpoint];
if (layoutInfo.gridSettings) {
this.layoutCtx.gridSettings = layoutInfo.gridSettings;
}
this.layoutCtx.widgets.setWidgetIds(layoutInfo.widgetIds);
this.layoutCtx.widgetLayouts = layoutInfo.widgetLayouts;
if (showLayout && this.layoutCtx.ctrl) {
this.layoutCtx.ctrl.reload();
}
this.layoutCtx.ignoreLoading = true;
}
}

1
ui-ngx/src/app/modules/home/components/dashboard-page/layout/layout.models.ts

@ -21,7 +21,6 @@ export interface ILayoutController {
selectWidget(widgetId: string, delay?: number);
pasteWidget($event: MouseEvent);
pasteWidgetReference($event: MouseEvent);
updatedCurrentBreakpoint(breakpoint?: string, showLayout?: boolean);
}
export enum LayoutWidthType {

2
ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.html

@ -21,7 +21,7 @@
[(ngModel)]="selectedBreakpoint"
(ngModelChange)="selectLayoutChanged()">
<mat-select-trigger>{{ getName(selectedBreakpoint) }}</mat-select-trigger>
<mat-option *ngFor="let breakpointId of breakpointIds" [value]="breakpointId">
<mat-option *ngFor="let breakpointId of breakpointIds" [value]="breakpointId" style="min-height:56px;">
<mat-icon><tb-icon>{{ getIcon(breakpointId) }}</tb-icon></mat-icon>
<div>{{ getName(breakpointId) }}</div>
<div style="font-size: 12px;letter-spacing: 0.4px;color: #9e9e9e;">{{ getSizeDescription(breakpointId) }}</div>

2
ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.ts

@ -63,7 +63,7 @@ export class SelectDashboardBreakpointComponent implements OnInit, OnDestroy {
}
selectLayoutChanged() {
this.dashboardCtrl.layouts.main.layoutCtx.ctrl.updatedCurrentBreakpoint(this.selectedBreakpoint);
this.dashboardUtils.updatedLayoutForBreakpoint(this.dashboardCtrl.layouts.main, this.selectedBreakpoint);
this.dashboardCtrl.updateLayoutSizes();
}

13
ui-ngx/src/app/modules/home/components/entity/contact-based.component.ts

@ -19,10 +19,10 @@ import { AppState } from '@core/core.state';
import { UntypedFormBuilder, UntypedFormGroup, ValidatorFn, Validators } from '@angular/forms';
import { ContactBased } from '@shared/models/contact-based.model';
import { AfterViewInit, ChangeDetectorRef, Directive } from '@angular/core';
import { POSTAL_CODE_PATTERNS } from '@home/models/contact.models';
import { HasId } from '@shared/models/base-data';
import { EntityComponent } from './entity.component';
import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
import { CountryData } from '@shared/models/country.models';
@Directive()
export abstract class ContactBasedComponent<T extends ContactBased<HasId>> extends EntityComponent<T> implements AfterViewInit {
@ -31,7 +31,8 @@ export abstract class ContactBasedComponent<T extends ContactBased<HasId>> exten
protected fb: UntypedFormBuilder,
protected entityValue: T,
protected entitiesTableConfigValue: EntityTableConfig<T>,
protected cd: ChangeDetectorRef) {
protected cd: ChangeDetectorRef,
protected countryData: CountryData) {
super(store, fb, entityValue, entitiesTableConfigValue, cd);
}
@ -75,9 +76,11 @@ export abstract class ContactBasedComponent<T extends ContactBased<HasId>> exten
zipValidators(country: string): ValidatorFn[] {
const zipValidators = [];
if (country && POSTAL_CODE_PATTERNS[country]) {
const postalCodePattern = POSTAL_CODE_PATTERNS[country];
zipValidators.push(Validators.pattern(postalCodePattern));
if (country) {
const postCodePattern = this.countryData.allCountries.find(item => item.name === country)?.postCodePattern;
if (postCodePattern) {
zipValidators.push(Validators.pattern(postCodePattern));
}
}
return zipValidators;
}

10
ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts

@ -49,7 +49,7 @@ import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { BehaviorSubject, fromEvent, merge, Observable, Subject } from 'rxjs';
import { emptyPageData, PageData } from '@shared/models/page/page-data';
import { EntityId } from '@shared/models/id/entity-id';
import { entityTypeTranslations } from '@shared/models/entity-type.models';
import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models';
import { debounceTime, distinctUntilChanged, map, takeUntil } from 'rxjs/operators';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort, SortDirection } from '@angular/material/sort';
@ -408,7 +408,9 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
} as EntityColumn
);
this.contentsInfo.entityType = {
useCellContentFunction: false
useCellContentFunction: true,
cellContentFunction: (entityType: EntityType) =>
entityType ? this.translate.instant(entityTypeTranslations.get(entityType).type) : ''
};
this.stylesInfo.entityType = {
useCellStyleFunction: false
@ -869,9 +871,9 @@ class EntityDatasource implements DataSource<EntityData> {
}
if (datasource.entityType) {
entity.id.entityType = datasource.entityType;
entity.entityType = this.translate.instant(entityTypeTranslations.get(datasource.entityType).type);
entity.entityType = datasource.entityType;
} else {
entity.entityType = '';
entity.entityType = null;
}
this.dataKeys.forEach((dataKey, index) => {
const keyData = data[index].data;

4
ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.html

@ -48,7 +48,7 @@
{{ 'widgets.recent-dashboards.name' | translate }}
</mat-header-cell>
<mat-cell class="title" *matCellDef="let lastVisitedDashboard">
<a [routerLink]="createDashboardUrl(lastVisitedDashboard.id)">{{ lastVisitedDashboard.title }}</a>
<a [routerLink]="createDashboardUrl(lastVisitedDashboard.id)">{{ lastVisitedDashboard.title | customTranslate }}</a>
</mat-cell>
</ng-container>
<ng-container matColumnDef="lastVisited">
@ -78,7 +78,7 @@
class="star" [ngClass]="{'starred': dashboard.starred}">{{ dashboard.starred ? 'star' : 'star_border' }}</mat-icon>
</div>
<div class="tb-cell title">
<a [routerLink]="createDashboardUrl(dashboard.id)">{{ dashboard.title }}</a>
<a [routerLink]="createDashboardUrl(dashboard.id)">{{ dashboard.title | customTranslate }}</a>
</div>
</div>
</div>

4
ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html

@ -123,8 +123,8 @@
<mat-label>{{key.label}}</mat-label>
<mat-select formControlName="{{key.formId}}"
[required]="key.settings.required"
(focus)="key.isFocused = true;"
(selectionChange)="key.isFocused = false; inputChanged(source, key)">
(openedChange)="key.isFocused = $event"
(selectionChange)="inputChanged(source, key)">
<mat-option *ngFor="let option of key.settings.selectOptions"
[value]="option.value"
[disabled]="key.settings.isEditable === 'readonly'">

3
ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts

@ -25,6 +25,7 @@ import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { WidgetContext } from '@home/models/widget-component.models';
import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core';
import { EntityType } from '@shared/models/entity-type.models';
type ColumnVisibilityOptions = 'visible' | 'hidden' | 'hidden-mobile';
@ -65,7 +66,7 @@ export interface EntityData {
id: EntityId;
entityName: string;
entityLabel?: string;
entityType?: string;
entityType?: EntityType;
actionCellButtons?: TableCellButtonActionDescriptor[];
hasActions?: boolean;
[key: string]: any;

36
ui-ngx/src/app/modules/home/components/widget/widget-config.component.html

@ -168,18 +168,32 @@
</tb-manage-widget-actions>
</div>
</div>
<div [fxShow]="selectedOption === 'mobile'" [formGroup]="layoutSettings" class="mat-content">
<div class="tb-form-panel" *ngIf="isDefaultBreakpoint">
<mat-slide-toggle class="mat-slide" formControlName="mobileHide">
{{ 'widget-config.mobile-hide' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-panel" *ngIf="isDefaultBreakpoint">
<mat-slide-toggle class="mat-slide" formControlName="desktopHide">
{{ 'widget-config.desktop-hide' | translate }}
</mat-slide-toggle>
<div [fxShow]="selectedOption === 'layout'" [formGroup]="layoutSettings" class="mat-content">
<div class="tb-form-panel" *ngIf="!showLayoutConfig || isDefaultBreakpoint">
<div class="tb-form-panel-title" translate>widget-config.resize-options</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="resizable">
{{ 'widget-config.resizable' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="preserveAspectRatio">
{{ 'widget-config.preserve-aspect-ratio' | translate }}
</mat-slide-toggle>
</div>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel" *ngIf="showLayoutConfig">
<div class="tb-form-panel-title">{{ (isDefaultBreakpoint ? 'widget-config.mobile' : 'widget-config.list-layout') | translate }}</div>
<div class="tb-form-row" *ngIf="isDefaultBreakpoint">
<mat-slide-toggle class="mat-slide" formControlName="mobileHide">
{{ 'widget-config.mobile-hide' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row" *ngIf="isDefaultBreakpoint">
<mat-slide-toggle class="mat-slide" formControlName="desktopHide">
{{ 'widget-config.desktop-hide' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row space-between">
<div translate>widget-config.order</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">

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

@ -63,7 +63,7 @@ import { UtilsService } from '@core/services/utils.service';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { TranslateService } from '@ngx-translate/core';
import { EntityType } from '@shared/models/entity-type.models';
import { Observable, of, Subject, Subscription } from 'rxjs';
import { merge, Observable, of, Subject, Subscription } from 'rxjs';
import {
IBasicWidgetConfigComponent,
WidgetConfigCallbacks
@ -248,19 +248,24 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
noDataDisplayMessage: [null, []]
});
this.widgetSettings.get('showTitle').valueChanges.subscribe(() => {
this.updateWidgetSettingsEnabledState();
});
this.widgetSettings.get('showTitleIcon').valueChanges.subscribe(() => {
merge(this.widgetSettings.get('showTitle').valueChanges,
this.widgetSettings.get('showTitleIcon').valueChanges).subscribe(() => {
this.updateWidgetSettingsEnabledState();
});
this.layoutSettings = this.fb.group({
resizable: [true],
preserveAspectRatio: [false],
mobileOrder: [null, [Validators.pattern(/^-?[0-9]+$/)]],
mobileHeight: [null, [Validators.min(1), Validators.pattern(/^\d*$/)]],
mobileHide: [false],
desktopHide: [false]
});
this.layoutSettings.get('resizable').valueChanges.subscribe(() => {
this.updateLayoutEnabledState();
});
this.actionsSettings = this.fb.group({
actions: [null, []]
});
@ -349,16 +354,12 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
value: 'actions'
}
);
if (this.showLayoutConfig) {
this.headerOptions.push(
{
name: this.isDefaultBreakpoint
? this.translate.instant('widget-config.mobile')
: this.translate.instant('widget-config.list-layout'),
value: 'mobile'
}
);
}
this.headerOptions.push(
{
name: this.translate.instant('widget-config.layout'),
value: 'layout'
}
);
if (!this.selectedOption || !this.headerOptions.find(o => o.value === this.selectedOption)) {
this.selectedOption = this.headerOptions[0].value;
}
@ -569,6 +570,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
if (layout) {
this.layoutSettings.patchValue(
{
resizable: isDefined(layout.resizable) ? layout.resizable : true,
preserveAspectRatio: layout.preserveAspectRatio,
mobileOrder: layout.mobileOrder,
mobileHeight: layout.mobileHeight,
mobileHide: layout.mobileHide,
@ -579,6 +582,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
} else {
this.layoutSettings.patchValue(
{
resizable: true,
preserveAspectRatio: false,
mobileOrder: null,
mobileHeight: null,
mobileHide: false,
@ -587,6 +592,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
{emitEvent: false}
);
}
this.updateLayoutEnabledState();
}
this.createChangeSubscriptions();
}
@ -620,6 +626,15 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
}
}
private updateLayoutEnabledState() {
const resizable: boolean = this.layoutSettings.get('resizable').value;
if (resizable) {
this.layoutSettings.get('preserveAspectRatio').enable({emitEvent: false});
} else {
this.layoutSettings.get('preserveAspectRatio').disable({emitEvent: false});
}
}
private updateSchemaForm(settings?: any) {
const widgetSettingsFormData: JsonFormComponentData = {};
if (this.modelValue.settingsSchema && this.modelValue.settingsSchema.schema) {

291
ui-ngx/src/app/modules/home/models/contact.models.ts

@ -1,291 +0,0 @@
///
/// Copyright © 2016-2024 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.
///
export const COUNTRIES = [
'Afghanistan',
'Åland Islands',
'Albania',
'Algeria',
'American Samoa',
'Andorra',
'Angola',
'Anguilla',
'Antarctica',
'Antigua and Barbuda',
'Argentina',
'Armenia',
'Aruba',
'Australia',
'Austria',
'Azerbaijan',
'Bahamas',
'Bahrain',
'Bangladesh',
'Barbados',
'Belarus',
'Belgium',
'Belize',
'Benin',
'Bermuda',
'Bhutan',
'Bolivia',
'Bonaire, Sint Eustatius and Saba',
'Bosnia and Herzegovina',
'Botswana',
'Bouvet Island',
'Brazil',
'British Indian Ocean Territory',
'Brunei Darussalam',
'Bulgaria',
'Burkina Faso',
'Burundi',
'Cambodia',
'Cameroon',
'Canada',
'Cape Verde',
'Cayman Islands',
'Central African Republic',
'Chad',
'Chile',
'China',
'Christmas Island',
'Cocos (Keeling) Islands',
'Colombia',
'Comoros',
'Congo',
'Congo, The Democratic Republic of the',
'Cook Islands',
'Costa Rica',
'Côte d\'Ivoire',
'Croatia',
'Cuba',
'Curaçao',
'Cyprus',
'Czech Republic',
'Denmark',
'Djibouti',
'Dominica',
'Dominican Republic',
'Ecuador',
'Egypt',
'El Salvador',
'Equatorial Guinea',
'Eritrea',
'Estonia',
'Ethiopia',
'Falkland Islands (Malvinas)',
'Faroe Islands',
'Fiji',
'Finland',
'France',
'French Guiana',
'French Polynesia',
'French Southern Territories',
'Gabon',
'Gambia',
'Georgia',
'Germany',
'Ghana',
'Gibraltar',
'Greece',
'Greenland',
'Grenada',
'Guadeloupe',
'Guam',
'Guatemala',
'Guernsey',
'Guinea',
'Guinea-Bissau',
'Guyana',
'Haiti',
'Heard Island and McDonald Islands',
'Holy See (Vatican City State)',
'Honduras',
'Hong Kong',
'Hungary',
'Iceland',
'India',
'Indonesia',
'Iran, Islamic Republic of',
'Iraq',
'Ireland',
'Isle of Man',
'Israel',
'Italy',
'Jamaica',
'Japan',
'Jersey',
'Jordan',
'Kazakhstan',
'Kenya',
'Kiribati',
'Korea, Democratic People\'s Republic of',
'Korea, Republic of',
'Kuwait',
'Kyrgyzstan',
'Lao People\'s Democratic Republic',
'Latvia',
'Lebanon',
'Lesotho',
'Liberia',
'Libya',
'Liechtenstein',
'Lithuania',
'Luxembourg',
'Macao',
'Macedonia, Republic Of',
'Madagascar',
'Malawi',
'Malaysia',
'Maldives',
'Mali',
'Malta',
'Marshall Islands',
'Martinique',
'Mauritania',
'Mauritius',
'Mayotte',
'Mexico',
'Micronesia, Federated States of',
'Moldova, Republic of',
'Monaco',
'Mongolia',
'Montenegro',
'Montserrat',
'Morocco',
'Mozambique',
'Myanmar',
'Namibia',
'Nauru',
'Nepal',
'Netherlands',
'New Caledonia',
'New Zealand',
'Nicaragua',
'Niger',
'Nigeria',
'Niue',
'Norfolk Island',
'Northern Mariana Islands',
'Norway',
'Oman',
'Pakistan',
'Palau',
'Palestinian Territory, Occupied',
'Panama',
'Papua New Guinea',
'Paraguay',
'Peru',
'Philippines',
'Pitcairn',
'Poland',
'Portugal',
'Puerto Rico',
'Qatar',
'Reunion',
'Romania',
'Russian Federation',
'Rwanda',
'Saint Barthélemy',
'Saint Helena, Ascension and Tristan da Cunha',
'Saint Kitts and Nevis',
'Saint Lucia',
'Saint Martin (French Part)',
'Saint Pierre and Miquelon',
'Saint Vincent and the Grenadines',
'Samoa',
'San Marino',
'Sao Tome and Principe',
'Saudi Arabia',
'Senegal',
'Serbia',
'Seychelles',
'Sierra Leone',
'Singapore',
'Sint Maarten (Dutch Part)',
'Slovakia',
'Slovenia',
'Solomon Islands',
'Somalia',
'South Africa',
'South Georgia and the South Sandwich Islands',
'South Sudan',
'Spain',
'Sri Lanka',
'Sudan',
'Suriname',
'Svalbard and Jan Mayen',
'Swaziland',
'Sweden',
'Switzerland',
'Syrian Arab Republic',
'Taiwan',
'Tajikistan',
'Tanzania, United Republic of',
'Thailand',
'Timor-Leste',
'Togo',
'Tokelau',
'Tonga',
'Trinidad and Tobago',
'Tunisia',
'Turkey',
'Turkmenistan',
'Turks and Caicos Islands',
'Tuvalu',
'Uganda',
'Ukraine',
'United Arab Emirates',
'United Kingdom',
'United States',
'United States Minor Outlying Islands',
'Uruguay',
'Uzbekistan',
'Vanuatu',
'Venezuela',
'Viet Nam',
'Virgin Islands, British',
'Virgin Islands, U.S.',
'Wallis and Futuna',
'Western Sahara',
'Yemen',
'Zambia',
'Zimbabwe'
];
/* eslint-disable */
export const POSTAL_CODE_PATTERNS = {
'United States': '(\\d{5}([\\-]\\d{4})?)',
'Australia': '[0-9]{4}',
'Austria': '[0-9]{4}',
'Belgium': '[0-9]{4}',
'Brazil': '[0-9]{5}[\\-]?[0-9]{3}',
'Canada': '^(?!.*[DFIOQU])[A-VXY][0-9][A-Z][ -]?[0-9][A-Z][0-9]$',
'Denmark': '[0-9]{3,4}',
'Faroe Islands': '[0-9]{3,4}',
'Netherlands': '[1-9][0-9]{3}\\s?[a-zA-Z]{2}',
'Germany': '[0-9]{5}',
'Hungary': '[0-9]{4}',
'Italy': '[0-9]{5}',
'Japan': '\\d{3}-\\d{4}',
'Luxembourg': '(L\\s*(-|—|–))\\s*?[\\d]{4}',
'Poland': '[0-9]{2}\\-[0-9]{3}',
'Spain': '((0[1-9]|5[0-2])|[1-4][0-9])[0-9]{3}',
'Sweden': '\\d{3}\\s?\\d{2}',
'United Kingdom': '[A-Za-z]{1,2}[0-9Rr][0-9A-Za-z]? [0-9][ABD-HJLNP-UW-Zabd-hjlnp-uw-z]{2}'
};
/* eslint-enable */

176
ui-ngx/src/app/modules/home/models/dashboard-component.models.ts

@ -343,6 +343,10 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
private selectedValue = false;
private selectedCallback: (selected: boolean) => void = () => {};
resizableHandles = {} as any;
resizeEnabled = true;
isFullscreen = false;
isReference = false;
@ -387,6 +391,15 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
private gridsterItemComponentSubject = new Subject<GridsterItemComponentInterface>();
private gridsterItemComponentValue: GridsterItemComponentInterface;
private readonly aspectRatio: number;
private heightValue: number;
private widthValue: number;
private rowsValue: number;
private colsValue: number;
get mobileHide(): boolean {
return this.widgetLayout ? this.widgetLayout.mobileHide === true : false;
}
@ -397,10 +410,96 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
set gridsterItemComponent(item: GridsterItemComponentInterface) {
this.gridsterItemComponentValue = item;
if (this.widgetLayout?.preserveAspectRatio) {
this.applyPreserveAspectRatio(item);
}
this.gridsterItemComponentSubject.next(this.gridsterItemComponentValue);
this.gridsterItemComponentSubject.complete();
}
private applyPreserveAspectRatio(item: GridsterItemComponentInterface) {
this.resizableHandles.ne = false;
this.resizableHandles.sw = false;
this.resizableHandles.nw = false;
const $item = item.$item;
this.rowsValue = $item.rows;
this.colsValue = $item.cols;
Object.defineProperty($item, 'rows', {
get: () => this.rowsValue,
set: v => {
if (this.rowsValue !== v) {
if (this.preserveAspectRatio) {
this.colsValue = v * this.aspectRatio;
}
this.rowsValue = v;
}
}
});
Object.defineProperty($item, 'cols', {
get: () => this.colsValue,
set: v => {
if (this.colsValue !== v) {
if (this.preserveAspectRatio) {
this.rowsValue = v / this.aspectRatio;
}
this.colsValue = v;
}
}
});
const resizable = item.resize;
this.heightValue = resizable.height;
this.widthValue = resizable.width;
const setItemHeight = resizable.setItemHeight.bind(resizable);
const setItemWidth = resizable.setItemWidth.bind(resizable);
resizable.setItemHeight = (height) => {
setItemHeight(height);
this.heightValue = height;
if (this.preserveAspectRatio) {
setItemWidth(height * this.aspectRatio);
}
};
resizable.setItemWidth = (width) => {
setItemWidth(width);
this.widthValue = width;
if (this.preserveAspectRatio) {
setItemHeight(width / this.aspectRatio);
}
};
Object.defineProperty(resizable, 'height', {
get: () => this.heightValue,
set: v => {
if (this.heightValue !== v) {
if (this.preserveAspectRatio) {
this.widthValue = v * this.aspectRatio;
}
this.heightValue = v;
}
}
});
Object.defineProperty(resizable, 'width', {
get: () => this.widthValue,
set: v => {
if (this.widthValue !== v) {
if (this.preserveAspectRatio) {
this.heightValue = v / this.aspectRatio;
}
this.widthValue = v;
}
}
});
}
get highlighted() {
return this.highlightedValue;
}
@ -434,6 +533,13 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
public widgetLayout?: WidgetLayout,
private parentDashboard?: IDashboardComponent,
private popoverComponent?: TbPopoverComponent) {
if (isDefined(widgetLayout?.resizable)) {
this.resizeEnabled = widgetLayout.resizable;
}
if (widgetLayout?.preserveAspectRatio) {
this.aspectRatio = this.widgetLayout.sizeX / this.widgetLayout.sizeY;
}
if (!widget.id) {
widget.id = guid();
}
@ -603,30 +709,28 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
}
}
@enumerable(true)
get cols(): number {
let res;
if (this.widgetLayout) {
res = this.widgetLayout.sizeX;
get preserveAspectRatio(): boolean {
if (!this.dashboard.isMobileSize && this.widgetLayout) {
return this.widgetLayout.preserveAspectRatio;
} else {
res = this.widget.sizeX;
return false;
}
return Math.floor(res);
}
@enumerable(true)
get cols(): number {
return Math.floor(this.sizeX);
}
set cols(cols: number) {
if (!this.dashboard.isMobileSize) {
if (this.widgetLayout) {
this.widgetLayout.sizeX = cols;
} else {
this.widget.sizeX = cols;
}
this.sizeX = cols;
}
}
@enumerable(true)
get rows(): number {
let res;
let res: number;
if (this.dashboard.isMobileSize) {
let mobileHeight;
if (this.widgetLayout) {
@ -638,26 +742,50 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
if (mobileHeight) {
res = mobileHeight;
} else {
const sizeY = this.widgetLayout ? this.widgetLayout.sizeY : this.widget.sizeY;
const sizeY = this.sizeY;
res = sizeY * 24 / this.dashboard.gridsterOpts.minCols;
}
} else {
if (this.widgetLayout) {
res = this.widgetLayout.sizeY;
} else {
res = this.widget.sizeY;
}
res = this.sizeY;
}
return Math.floor(res);
}
set rows(rows: number) {
if (!this.dashboard.isMobileSize && !this.dashboard.autofillHeight) {
if (this.widgetLayout) {
this.widgetLayout.sizeY = rows;
} else {
this.widget.sizeY = rows;
}
this.sizeY = rows;
}
}
get sizeX(): number {
if (this.widgetLayout) {
return this.widgetLayout.sizeX;
} else {
return this.widget.sizeX;
}
}
set sizeX(sizeX: number) {
if (this.widgetLayout) {
this.widgetLayout.sizeX = sizeX;
} else {
this.widget.sizeX = sizeX;
}
}
get sizeY(): number {
if (this.widgetLayout) {
return this.widgetLayout.sizeY;
} else {
return this.widget.sizeY;
}
}
set sizeY(sizeY: number) {
if (this.widgetLayout) {
this.widgetLayout.sizeY = sizeY;
} else {
this.widget.sizeY = sizeY;
}
}

7
ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.html

@ -23,6 +23,9 @@
<mat-error *ngIf="entityForm.get('title').hasError('required')">
{{ 'admin.oauth2.client-title-required' | translate }}
</mat-error>
<mat-error *ngIf="entityForm.get('title').hasError('maxlength')">
{{ 'admin.oauth2.client-title-max-length' | translate }}
</mat-error>
</mat-form-field>
</div>
<div class="tb-form-row tb-standard-fields no-border no-padding column-xs">
@ -67,7 +70,7 @@
</mat-error>
</mat-form-field>
</div>
<mat-expansion-panel class="configuration-panel mat-elevation-z0" [expanded]="">
<mat-expansion-panel class="configuration-panel mat-elevation-z0" [expanded]="advancedExpanded">
<mat-expansion-panel-header>
<mat-panel-title fxLayoutAlign="start center" translate>
admin.oauth2.advanced-settings
@ -162,7 +165,7 @@
<div class="tb-form-row tb-standard-fields no-border no-padding column-xs">
<mat-form-field class="flex">
<mat-label translate>admin.oauth2.scope</mat-label>
<mat-chip-grid #scopeList [disabled]="!this.createNewDialog && !(this.isEdit || this.isAdd)">
<mat-chip-grid #scopeList required [disabled]="!this.createNewDialog && !(this.isEdit || this.isAdd)">
<mat-chip-row *ngFor="let scope of entityForm.get('scope').value; let k = index; trackBy: trackByParams"
removable (removed)="removeScope(k, entityForm)">
{{scope}}

8
ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.ts

@ -103,6 +103,7 @@ export class ClientComponent extends EntityComponent<OAuth2Client, PageLink, OAu
platformTypes = Object.values(PlatformType);
platformTypeTranslations = platformTypeTranslations;
generalSettingsMode = true;
advancedExpanded = false;
constructor(protected store: Store<AppState>,
protected translate: TranslateService,
@ -126,7 +127,7 @@ export class ClientComponent extends EntityComponent<OAuth2Client, PageLink, OAu
buildForm(entity: OAuth2Client): UntypedFormGroup {
return this.fb.group({
title: [entity?.title ? entity.title : '', [Validators.required]],
title: [entity?.title ? entity.title : '', [Validators.required, Validators.maxLength(100)]],
additionalInfo: this.fb.group({
providerName: [entity?.additionalInfo?.providerName ? entity?.additionalInfo?.providerName : '', Validators.required]
}),
@ -333,7 +334,7 @@ export class ClientComponent extends EntityComponent<OAuth2Client, PageLink, OAu
}
this.subscriptions.push(basicGroup.get('tenantNameStrategy').valueChanges.subscribe((domain) => {
if (domain === 'CUSTOM') {
if (domain === 'CUSTOM' && (this.createNewDialog || this.isEdit || this.isAdd)) {
basicGroup.get('tenantNamePattern').enable();
} else {
basicGroup.get('tenantNamePattern').disable();
@ -345,7 +346,10 @@ export class ClientComponent extends EntityComponent<OAuth2Client, PageLink, OAu
private setProviderDefaultValue(provider: string, clientRegistration: UntypedFormGroup) {
if (provider === 'Custom') {
const title = clientRegistration.get('title').value;
clientRegistration.reset(this.defaultProvider, {emitEvent: false});
clientRegistration.patchValue({title}, {emitEvent: false});
this.advancedExpanded = true;
} else {
const template = this.templates.get(provider);
template.clientId = '';

2
ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/clients-table-config.resolver.ts

@ -61,8 +61,6 @@ export class ClientsTableConfigResolver implements Resolve<EntityTableConfig<OAu
this.config.headerComponent = ClientTableHeaderComponent;
this.config.addDialogStyle = {width: '850px', maxHeight: '100vh'};
this.config.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC};
this.config.displayPagination = false;
this.config.pageMode = false;
this.config.columns.push(
new DateEntityTableColumn<OAuth2ClientInfo>('createdTime', 'common.created-time', this.datePipe, '170px'),

2
ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain-table-config.resolver.ts

@ -54,8 +54,6 @@ export class DomainTableConfigResolver implements Resolve<EntityTableConfig<Doma
this.config.headerComponent = DomainTableHeaderComponent;
this.config.addDialogStyle = {width: '850px', maxHeight: '100vh'};
this.config.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC};
this.config.displayPagination = false;
this.config.pageMode = false;
this.config.columns.push(
new DateEntityTableColumn<DomainInfo>('createdTime', 'common.created-time', this.datePipe, '170px'),

6
ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-config.resolver.ts

@ -55,8 +55,6 @@ export class MobileAppTableConfigResolver implements Resolve<EntityTableConfig<M
this.config.headerComponent = MobileAppTableHeaderComponent;
this.config.addDialogStyle = {width: '850px', maxHeight: '100vh'};
this.config.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC};
this.config.displayPagination = false;
this.config.pageMode = false;
this.config.columns.push(
new DateEntityTableColumn<MobileAppInfo>('createdTime', 'common.created-time', this.datePipe, '170px'),
@ -134,8 +132,8 @@ export class MobileAppTableConfigResolver implements Resolve<EntityTableConfig<M
private appSecretText(entity): string {
let text = entity.appSecret;
if (text.length > 40) {
text = `${text.slice(0, 40)}…`;
if (text.length > 35) {
text = `${text.slice(0, 35)}…`;
}
return text;
}

3
ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app.component.html

@ -25,6 +25,9 @@
<mat-error *ngIf="entityForm.hasError('unique')">
{{ 'admin.oauth2.mobile-package-unique' | translate }}
</mat-error>
<mat-error *ngIf="entityForm.get('pkgName').hasError('maxlength')">
{{ 'admin.oauth2.mobile-package-max-length' | translate }}
</mat-error>
</mat-form-field>
</div>
<div class="tb-form-row tb-standard-fields no-border no-padding">

2
ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app.component.ts

@ -54,7 +54,7 @@ export class MobileAppComponent extends EntityComponent<MobileAppInfo> {
buildForm(entity: MobileAppInfo): UntypedFormGroup {
return this.fb.group({
pkgName: [entity?.pkgName ? entity.pkgName : '', [Validators.required]],
pkgName: [entity?.pkgName ? entity.pkgName : '', [Validators.required, Validators.maxLength(255)]],
appSecret: [entity?.appSecret ? entity.appSecret : btoa(randomAlphanumeric(64)),
[Validators.required, this.base64Format]],
oauth2Enabled: isDefinedAndNotNull(entity?.oauth2Enabled) ? entity.oauth2Enabled : true,

6
ui-ngx/src/app/modules/home/pages/customer/customer.component.ts

@ -26,6 +26,7 @@ import { EntityTableConfig } from '@home/models/entity/entities-table-config.mod
import { isDefinedAndNotNull } from '@core/utils';
import { getCurrentAuthState } from '@core/auth/auth.selectors';
import { AuthState } from '@core/auth/auth.models';
import { CountryData } from '@shared/models/country.models';
@Component({
selector: 'tb-customer',
@ -43,8 +44,9 @@ export class CustomerComponent extends ContactBasedComponent<Customer> {
@Inject('entity') protected entityValue: Customer,
@Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<Customer>,
protected fb: UntypedFormBuilder,
protected cd: ChangeDetectorRef) {
super(store, fb, entityValue, entitiesTableConfigValue, cd);
protected cd: ChangeDetectorRef,
protected countryData: CountryData) {
super(store, fb, entityValue, entitiesTableConfigValue, cd, countryData);
}
hideDelete() {

6
ui-ngx/src/app/modules/home/pages/tenant/tenant.component.ts

@ -24,6 +24,7 @@ import { TranslateService } from '@ngx-translate/core';
import { ContactBasedComponent } from '../../components/entity/contact-based.component';
import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
import { isDefinedAndNotNull } from '@core/utils';
import { CountryData } from '@shared/models/country.models';
@Component({
selector: 'tb-tenant',
@ -37,8 +38,9 @@ export class TenantComponent extends ContactBasedComponent<TenantInfo> {
@Inject('entity') protected entityValue: TenantInfo,
@Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<TenantInfo>,
protected fb: UntypedFormBuilder,
protected cd: ChangeDetectorRef) {
super(store, fb, entityValue, entitiesTableConfigValue, cd);
protected cd: ChangeDetectorRef,
protected countryData: CountryData) {
super(store, fb, entityValue, entitiesTableConfigValue, cd, countryData);
}
hideDelete() {

13
ui-ngx/src/app/shared/components/contact.component.html

@ -16,14 +16,10 @@
-->
<section [formGroup]="parentForm">
<mat-form-field class="mat-block">
<mat-label translate>contact.country</mat-label>
<mat-select formControlName="country">
<mat-option *ngFor="let country of countries" [value]="country">
{{ country }}
</mat-option>
</mat-select>
</mat-form-field>
<tb-country-autocomplete
formControlName="country"
(selectCountryCode)="changeCountry($event)">
</tb-country-autocomplete>
<div fxLayout.gt-sm="row" fxLayoutGap.gt-sm="10px">
<mat-form-field class="mat-block">
<mat-label translate>contact.city</mat-label>
@ -56,6 +52,7 @@
<input matInput formControlName="address2">
</mat-form-field>
<tb-phone-input [required]="false"
[defaultCountry]="phoneInputDefaultCountry"
label="{{ 'contact.phone' | translate }}"
[enableFlagsSelect]="true"
formControlName="phone">

12
ui-ngx/src/app/shared/components/contact.component.ts

@ -16,7 +16,6 @@
import { Component, Input } from '@angular/core';
import { UntypedFormGroup } from '@angular/forms';
import { COUNTRIES } from '@home/models/contact.models';
@Component({
selector: 'tb-contact',
@ -29,6 +28,15 @@ export class ContactComponent {
@Input() isEdit: boolean;
countries = COUNTRIES;
phoneInputDefaultCountry = 'US';
constructor() {
}
changeCountry(countryCode: string) {
this.phoneInputDefaultCountry = countryCode ?? 'US';
setTimeout(() => {
this.parentForm.get('phone').setValue(this.parentForm.get('phone').value);
});
}
}

58
ui-ngx/src/app/shared/components/country-autocomplete.component.html

@ -0,0 +1,58 @@
<!--
Copyright © 2016-2024 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.
-->
<mat-form-field [formGroup]="countryFormGroup" class="mat-block" subscriptSizing="{{subscriptSizing}}">
<mat-label>{{ labelText }}</mat-label>
<input matInput type="text"
#countryInput
formControlName="country"
(focusin)="onFocus()"
(blur)="onTouched()"
[required]="required"
[matAutocomplete]="countryAutocomplete">
<button *ngIf="countryFormGroup.get('country').value && !disabled"
type="button"
matSuffix mat-icon-button aria-label="Clear"
(click)="clear()">
<mat-icon class="material-icons">close</mat-icon>
</button>
<mat-autocomplete class="tb-autocomplete"
#countryAutocomplete="matAutocomplete"
[displayWith]="displayCountryFn">
<mat-option *ngFor="let country of filteredCountries | async" [value]="country">
<span style="font-size: 20px;padding-right: 4px;">{{country.flag}}</span>
<span [innerHTML]="country.name | highlight:searchText"></span>
</mat-option>
<mat-option *ngIf="!(filteredCountries | async)?.length" [value]="null" class="tb-not-found">
<div class="tb-not-found-content" (click)="$event.stopPropagation()">
<div *ngIf="!textIsNotEmpty(searchText); else searchNotEmpty">
<span translate>contact.no-country-found</span>
</div>
<ng-template #searchNotEmpty>
<span>
{{ 'contact.no-country-matching' | translate:
{country: (searchText | truncate: true: 6: &apos;...&apos;)} }}
</span>
</ng-template>
</div>
</mat-option>
</mat-autocomplete>
<mat-hint *ngIf="autocompleteHint">{{ autocompleteHint }}</mat-hint>
<mat-error *ngIf="countryFormGroup.get('country').hasError('required')">
{{ requiredText }}
</mat-error>
</mat-form-field>

209
ui-ngx/src/app/shared/components/country-autocomplete.component.ts

@ -0,0 +1,209 @@
///
/// Copyright © 2016-2024 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, ElementRef, EventEmitter, forwardRef, Input, OnInit, Output, ViewChild } from '@angular/core';
import { Country, CountryData } from '@shared/models/country.models';
import {
ControlValueAccessor,
FormBuilder,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator
} from '@angular/forms';
import { isNotEmptyStr } from '@core/utils';
import { Observable, of } from 'rxjs';
import { debounceTime, distinctUntilChanged, map, share, switchMap, tap } from 'rxjs/operators';
import { SubscriptSizing } from '@angular/material/form-field';
import { coerceBoolean } from '@shared/decorators/coercion';
import { TranslateService } from '@ngx-translate/core';
interface CountrySearchData extends Country {
searchText?: string;
}
@Component({
selector: 'tb-country-autocomplete',
templateUrl: 'country-autocomplete.component.html',
providers: [
CountryData,
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CountryAutocompleteComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => CountryAutocompleteComponent),
multi: true
}
]
})
export class CountryAutocompleteComponent implements OnInit, ControlValueAccessor, Validator {
@Input()
labelText = this.translate.instant('contact.country');
@Input()
requiredText = this.translate.instant('contact.country-required');
@Input()
autocompleteHint: string;
@Input()
disabled: boolean;
@Input()
@coerceBoolean()
required = false;
@Input()
subscriptSizing: SubscriptSizing = 'fixed';
@ViewChild('countryInput', {static: true}) countryInput: ElementRef;
@Output()
selectCountryCode = new EventEmitter<string>();
countryFormGroup: FormGroup;
searchText = '';
filteredCountries: Observable<Array<Country>>;
onTouched = () => {
};
private propagateChange: (value: any) => void = () => {
};
private modelValue: Country;
private allCountries: CountrySearchData[] = this.countryData.allCountries;
private initSearchData = false;
private dirty = false;
constructor(private fb: FormBuilder,
private countryData: CountryData,
private translate: TranslateService) {
this.countryFormGroup = this.fb.group({
country: ['']
});
}
ngOnInit(): void {
this.filteredCountries = this.countryFormGroup.get('country').valueChanges.pipe(
debounceTime(150),
tap(value => {
let modelValue: Country;
if (typeof value === 'string' || !value) {
modelValue = null;
} else {
modelValue = value;
}
this.updateView(modelValue);
if (value === null) {
this.clear();
}
}),
map(value => value ? (typeof value === 'string' ? value : value.name) : ''),
distinctUntilChanged(),
switchMap(name => of(this.fetchCountries(name))),
share()
);
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (isDisabled) {
this.countryFormGroup.disable({emitEvent: false});
} else {
this.countryFormGroup.enable({emitEvent: false});
}
}
validate(): ValidationErrors | null {
return this.countryFormGroup.valid ? null : {
countryFormGroup: false
};
}
writeValue(country: string) {
this.dirty = true;
const findCountry = isNotEmptyStr(country) ? this.allCountries.find(item => item.name === country) : null;
this.modelValue = findCountry || null;
this.countryFormGroup.get('country').patchValue(this.modelValue || '', { emitEvent: false });
this.selectCountryCode.emit(this.modelValue ? this.modelValue.iso2 : null);
}
displayCountryFn(country?: Country): string | undefined {
return country ? `${country.flag} ${country.name}` : undefined;
}
onFocus() {
if (this.dirty) {
this.countryFormGroup.get('country').updateValueAndValidity({onlySelf: true});
this.dirty = false;
}
}
textIsNotEmpty(text: string): boolean {
return (text && text.length > 0);
}
clear() {
this.countryFormGroup.get('country').patchValue('', {emitEvent: true});
setTimeout(() => {
this.countryInput.nativeElement.blur();
this.countryInput.nativeElement.focus();
}, 0);
}
private fetchCountries(searchText: string): Country[] {
this.searchText = searchText;
if (!this.initSearchData) {
this.allCountries.forEach(country => {
country.searchText = `${country.name} ${country.iso2}`.toLowerCase();
});
this.initSearchData = true;
}
if (isNotEmptyStr(searchText)) {
const filterValue = searchText.toLowerCase();
return this.allCountries.filter(country => country.searchText.includes(filterValue));
}
return this.allCountries;
}
private updateView(value: Country | null) {
if (this.modelValue?.name !== value?.name) {
this.modelValue = value;
this.propagateChange(this.modelValue);
if (value) {
this.selectCountryCode.emit(value.iso2);
}
}
}
}

4
ui-ngx/src/app/shared/components/dashboard-autocomplete.component.html

@ -32,7 +32,7 @@
[matAutocomplete]="dashboardAutocomplete"
[fxShow]="!useDashboardLink || !disabled || !selectDashboardFormGroup.get('dashboard').value">
<a *ngIf="useDashboardLink && selectDashboardFormGroup.get('dashboard').value && disabled" aria-label="Open device profile" [routerLink]=dashboardURL>
{{ displayDashboardFn(selectDashboardFormGroup.get('dashboard').value) }}
{{ displayDashboardFn(selectDashboardFormGroup.get('dashboard').value) | customTranslate }}
</a>
<button *ngIf="selectDashboardFormGroup.get('dashboard').value && !disabled"
type="button"
@ -54,7 +54,7 @@
#dashboardAutocomplete="matAutocomplete"
[displayWith]="displayDashboardFn">
<mat-option *ngFor="let dashboard of filteredDashboards | async" [value]="dashboard">
<span [innerHTML]="dashboard.title | highlight:searchText"></span>
<span [innerHTML]="dashboard.title | customTranslate | highlight:searchText"></span>
</mat-option>
<mat-option *ngIf="!(filteredDashboards | async)?.length" [value]="null">
<div (click)="$event.stopPropagation()">

2
ui-ngx/src/app/shared/components/image/image-gallery.component.ts

@ -205,7 +205,7 @@ export class ImageGalleryComponent extends PageComponent implements OnInit, OnDe
property: 'createdTime',
direction: Direction.DESC
});
return this.imageService.getImages(pageLink, this.imageSubType, filter.includeSystemImages);
return this.imageService.getImages(pageLink, filter.includeSystemImages, this.imageSubType);
};
}

2
ui-ngx/src/app/shared/components/image/images-datasource.ts

@ -84,7 +84,7 @@ export class ImagesDatasource implements DataSource<ImageResourceInfo> {
}
fetchEntities(pageLink: PageLink, imageSubType: ResourceSubType, includeSystemImages = false): Observable<PageData<ImageResourceInfo>> {
return this.imageService.getImages(pageLink, imageSubType, includeSystemImages);
return this.imageService.getImages(pageLink, includeSystemImages, imageSubType);
}
isAllSelected(): Observable<boolean> {

118
ui-ngx/src/app/shared/models/country.models.ts

@ -22,6 +22,7 @@ export interface Country {
dialCode: string;
areaCodes?: string[];
flag: string;
postCodePattern?: RegExp;
}
export enum CountryISO {
@ -32,6 +33,7 @@ export enum CountryISO {
Andorra = 'AD',
Angola = 'AO',
Anguilla = 'AI',
Antarctica = 'AQ',
AntiguaAndBarbuda = 'AG',
Argentina = 'AR',
Armenia = 'AM',
@ -50,6 +52,8 @@ export enum CountryISO {
Bermuda = 'BM',
Bhutan = 'BT',
Bolivia = 'BO',
BonaireAndSintEustatiusAndSaba = 'BQ',
BouvetIsland = 'BV',
BosniaAndHerzegovina = 'BA',
Botswana = 'BW',
Brazil = 'BR',
@ -77,10 +81,10 @@ export enum CountryISO {
CongoRepublicCongoBrazzaville = 'CG',
CookIslands = 'CK',
CostaRica = 'CR',
CôteDIvoire = 'CI',
CoteDivoire = 'CI',
Croatia = 'HR',
Cuba = 'CU',
Curaçao = 'CW',
Curacao = 'CW',
Cyprus = 'CY',
CzechRepublic = 'CZ',
Denmark = 'DK',
@ -101,6 +105,7 @@ export enum CountryISO {
France = 'FR',
FrenchGuiana = 'GF',
FrenchPolynesia = 'PF',
FrenchSouthernTerritories = 'TF',
Gabon = 'GA',
Gambia = 'GM',
Georgia = 'GE',
@ -118,6 +123,7 @@ export enum CountryISO {
GuineaBissau = 'GW',
Guyana = 'GY',
Haiti = 'HT',
HeardIslandandMcDonaldIslands = 'HM',
Honduras = 'HN',
HongKong = 'HK',
Hungary = 'HU',
@ -150,7 +156,6 @@ export enum CountryISO {
Lithuania = 'LT',
Luxembourg = 'LU',
Macau = 'MO',
Macedonia = 'MK',
Madagascar = 'MG',
Malawi = 'MW',
Malaysia = 'MY',
@ -184,6 +189,7 @@ export enum CountryISO {
Niue = 'NU',
NorfolkIsland = 'NF',
NorthKorea = 'KP',
NorthMacedonia = 'MK',
NorthernMarianaIslands = 'MP',
Norway = 'NO',
Oman = 'OM',
@ -195,15 +201,16 @@ export enum CountryISO {
Paraguay = 'PY',
Peru = 'PE',
Philippines = 'PH',
PitcairnIslands = 'PN',
Poland = 'PL',
Portugal = 'PT',
PuertoRico = 'PR',
Qatar = 'QA',
Réunion = 'RE',
Reunion = 'RE',
Romania = 'RO',
Russia = 'RU',
Rwanda = 'RW',
SaintBarthélemy = 'BL',
SaintBarthelemy = 'BL',
SaintHelena = 'SH',
SaintKittsAndNevis = 'KN',
SaintLucia = 'LC',
@ -212,7 +219,7 @@ export enum CountryISO {
SaintVincentAndTheGrenadines = 'VC',
Samoa = 'WS',
SanMarino = 'SM',
SãoToméAndPríncipe = 'ST',
SaoTomeAndPrincipe = 'ST',
SaudiArabia = 'SA',
Senegal = 'SN',
Serbia = 'RS',
@ -225,6 +232,7 @@ export enum CountryISO {
SolomonIslands = 'SB',
Somalia = 'SO',
SouthAfrica = 'ZA',
SouthGeorgiaAndTheSouthSandwichIslands = 'GS',
SouthKorea = 'KR',
SouthSudan = 'SS',
Spain = 'ES',
@ -267,9 +275,10 @@ export enum CountryISO {
Yemen = 'YE',
Zambia = 'ZM',
Zimbabwe = 'ZW',
ÅlandIslands = 'AX',
AlandIslands = 'AX',
}
/* eslint-disable max-len */
@Injectable()
export class CountryData {
public allCountries: Array<Country> = [
@ -280,36 +289,39 @@ export class CountryData {
{name: 'Andorra', iso2: CountryISO.Andorra, dialCode: '376', flag: '🇦🇩'},
{name: 'Angola', iso2: CountryISO.Angola, dialCode: '244', flag: '🇦🇴'},
{name: 'Anguilla', iso2: CountryISO.Anguilla, dialCode: '1', flag: '🇦🇮'},
{name: 'Antarctica', iso2: CountryISO.Antarctica, dialCode: '672', flag: '🇦🇶'},
{name: 'Antigua and Barbuda', iso2: CountryISO.AntiguaAndBarbuda, dialCode: '1', flag: '🇦🇬'},
{name: 'Argentina', iso2: CountryISO.Argentina, dialCode: '54', flag: '🇦🇷'},
{name: 'Armenia', iso2: CountryISO.Armenia, dialCode: '374', flag: '🇦🇲'},
{name: 'Aruba', iso2: CountryISO.Aruba, dialCode: '297', flag: '🇦🇼'},
{name: 'Australia', iso2: CountryISO.Australia, dialCode: '61', flag: '🇦🇺'},
{name: 'Austria', iso2: CountryISO.Austria, dialCode: '43', flag: '🇦🇹'},
{name: 'Australia', iso2: CountryISO.Australia, dialCode: '61', flag: '🇦🇺', postCodePattern: /[0-9]{4}/},
{name: 'Austria', iso2: CountryISO.Austria, dialCode: '43', flag: '🇦🇹', postCodePattern: /[0-9]{4}/},
{name: 'Azerbaijan', iso2: CountryISO.Azerbaijan, dialCode: '994', flag: '🇦🇿'},
{name: 'Bahamas', iso2: CountryISO.Bahamas, dialCode: '1', flag: '🇧🇸'},
{name: 'Bahrain', iso2: CountryISO.Bahrain, dialCode: '973', flag: '🇧🇭'},
{name: 'Bangladesh', iso2: CountryISO.Bangladesh, dialCode: '880', flag: '🇧🇩'},
{name: 'Barbados', iso2: CountryISO.Barbados, dialCode: '1', flag: '🇧🇧'},
{name: 'Belarus', iso2: CountryISO.Belarus, dialCode: '375', flag: '🇧🇾'},
{name: 'Belgium', iso2: CountryISO.Belgium, dialCode: '32', flag: '🇧🇪'},
{name: 'Belgium', iso2: CountryISO.Belgium, dialCode: '32', flag: '🇧🇪', postCodePattern: /[0-9]{4}/},
{name: 'Belize', iso2: CountryISO.Belize, dialCode: '501', flag: '🇧🇿'},
{name: 'Benin', iso2: CountryISO.Benin, dialCode: '229', flag: '🇧🇯'},
{name: 'Bermuda', iso2: CountryISO.Bermuda, dialCode: '1', flag: '🇧🇲'},
{name: 'Bhutan', iso2: CountryISO.Bhutan, dialCode: '975', flag: '🇧🇹'},
{name: 'Bolivia', iso2: CountryISO.Bolivia, dialCode: '591', flag: '🇧🇴'},
{name: 'Bonaire, Sint Eustatius and Saba', iso2: CountryISO.BonaireAndSintEustatiusAndSaba, dialCode: '599', flag: '🇧🇶'},
{name: 'Bouvet Island', iso2: CountryISO.BouvetIsland, dialCode: '47', flag: '🇧🇻'},
{name: 'Bosnia and Herzegovina', iso2: CountryISO.BosniaAndHerzegovina, dialCode: '387', flag: '🇧🇦'},
{name: 'Botswana', iso2: CountryISO.Botswana, dialCode: '267', flag: '🇧🇼'},
{name: 'Brazil', iso2: CountryISO.Brazil, dialCode: '55', flag: '🇧🇷'},
{name: 'Brazil', iso2: CountryISO.Brazil, dialCode: '55', flag: '🇧🇷', postCodePattern: /[0-9]{5}-?[0-9]{3}/},
{name: 'British Indian Ocean Territory', iso2: CountryISO.BritishIndianOceanTerritory, dialCode: '246', flag: '🇮🇴'},
{name: 'British Virgin Islands', iso2: CountryISO.BritishVirginIslands, dialCode: '1', flag: '🇻🇬'},
{name: 'Brunei', iso2: CountryISO.Brunei, dialCode: '673', flag: '🇧🇳'},
{name: 'Brunei Darussalam', iso2: CountryISO.Brunei, dialCode: '673', flag: '🇧🇳'},
{name: 'Bulgaria', iso2: CountryISO.Bulgaria, dialCode: '359', flag: '🇧🇬'},
{name: 'Burkina Faso', iso2: CountryISO.BurkinaFaso, dialCode: '226', flag: '🇧🇫'},
{name: 'Burundi', iso2: CountryISO.Burundi, dialCode: '257', flag: '🇧🇮'},
{name: 'Cambodia', iso2: CountryISO.Cambodia, dialCode: '855', flag: '🇰🇭'},
{name: 'Cameroon', iso2: CountryISO.Cameroon, dialCode: '237', flag: '🇨🇲'},
{name: 'Canada', iso2: CountryISO.Canada, dialCode: '1', flag: '🇨🇦'},
{name: 'Canada', iso2: CountryISO.Canada, dialCode: '1', flag: '🇨🇦', postCodePattern: /^(?!.*[DFIOQU])[A-VXY][0-9][A-Z][ -]?[0-9][A-Z][0-9]$/},
{name: 'Cape Verde', iso2: CountryISO.CapeVerde, dialCode: '238', flag: '🇨🇻'},
{name: 'Caribbean Netherlands', iso2: CountryISO.CaribbeanNetherlands, dialCode: '599', flag: '🇧🇶'},
{name: 'Cayman Islands', iso2: CountryISO.CaymanIslands, dialCode: '1', flag: '🇰🇾'},
@ -318,20 +330,20 @@ export class CountryData {
{name: 'Chile', iso2: CountryISO.Chile, dialCode: '56', flag: '🇨🇱'},
{name: 'China', iso2: CountryISO.China, dialCode: '86', flag: '🇨🇳'},
{name: 'Christmas Island', iso2: CountryISO.ChristmasIsland, dialCode: '61', flag: '🇨🇽'},
{name: 'Cocos Islands', iso2: CountryISO.Cocos, dialCode: '61', flag: '🇨🇨'},
{name: 'Cocos (Keeling) Islands', iso2: CountryISO.Cocos, dialCode: '61', flag: '🇨🇨'},
{name: 'Colombia', iso2: CountryISO.Colombia, dialCode: '57', flag: '🇨🇨'},
{name: 'Comoros', iso2: CountryISO.Comoros, dialCode: '269', flag: '🇰🇲'},
{name: 'Congo-Kinshasa', iso2: CountryISO.CongoDRCJamhuriYaKidemokrasiaYaKongo, dialCode: '243', flag: '🇨🇩'},
{name: 'Congo-Brazzaville', iso2: CountryISO.CongoRepublicCongoBrazzaville, dialCode: '242', flag: '🇨🇬'},
{name: 'Congo', iso2: CountryISO.CongoDRCJamhuriYaKidemokrasiaYaKongo, dialCode: '243', flag: '🇨🇩'},
{name: 'Congo, Democratic Republic of the', iso2: CountryISO.CongoRepublicCongoBrazzaville, dialCode: '242', flag: '🇨🇬'},
{name: 'Cook Islands', iso2: CountryISO.CookIslands, dialCode: '682', flag: '🇨🇰'},
{name: 'Costa Rica', iso2: CountryISO.CostaRica, dialCode: '506', flag: '🇨🇷'},
{name: 'Côte d’Ivoire', iso2: CountryISO.CôteDIvoire, dialCode: '225', flag: '🇨🇮'},
{name: 'Côte d\'Ivoire', iso2: CountryISO.CoteDivoire, dialCode: '225', flag: '🇨🇮'},
{name: 'Croatia', iso2: CountryISO.Croatia, dialCode: '385', flag: '🇭🇷'},
{name: 'Cuba', iso2: CountryISO.Cuba, dialCode: '53', flag: '🇨🇺'},
{name: 'Curaçao', iso2: CountryISO.Curaçao, dialCode: '599', flag: '🇨🇼'},
{name: 'Curaçao', iso2: CountryISO.Curacao, dialCode: '599', flag: '🇨🇼'},
{name: 'Cyprus', iso2: CountryISO.Cyprus, dialCode: '357', flag: '🇨🇾'},
{name: 'Czech Republic', iso2: CountryISO.CzechRepublic, dialCode: '420', flag: '🇨🇿'},
{name: 'Denmark', iso2: CountryISO.Denmark, dialCode: '45', flag: '🇩🇰'},
{name: 'Denmark', iso2: CountryISO.Denmark, dialCode: '45', flag: '🇩🇰', postCodePattern: /[0-9]{3,4}/},
{name: 'Djibouti', iso2: CountryISO.Djibouti, dialCode: '253', flag: '🇩🇯'},
{name: 'Dominica', iso2: CountryISO.Dominica, dialCode: '1767', flag: '🇩🇲'},
{name: 'Dominican Republic', iso2: CountryISO.DominicanRepublic, dialCode: '1', flag: '🇩🇴'},
@ -342,17 +354,18 @@ export class CountryData {
{name: 'Eritrea', iso2: CountryISO.Eritrea, dialCode: '291', flag: '🇪🇷'},
{name: 'Estonia', iso2: CountryISO.Estonia, dialCode: '372', flag: '🇪🇪'},
{name: 'Ethiopia', iso2: CountryISO.Ethiopia, dialCode: '251', flag: '🇪🇹'},
{name: 'Falkland Islands', iso2: CountryISO.FalklandIslands, dialCode: '500', flag: '🇫🇰'},
{name: 'Faroe Islands', iso2: CountryISO.FaroeIslands, dialCode: '298', flag: '🇫🇴'},
{name: 'Falkland Islands (Malvinas)', iso2: CountryISO.FalklandIslands, dialCode: '500', flag: '🇫🇰'},
{name: 'Faroe Islands', iso2: CountryISO.FaroeIslands, dialCode: '298', flag: '🇫🇴', postCodePattern: /[0-9]{3,4}/},
{name: 'Fiji', iso2: CountryISO.Fiji, dialCode: '679', flag: '🇫🇯'},
{name: 'Finland', iso2: CountryISO.Finland, dialCode: '358', flag: '🇫🇮'},
{name: 'France', iso2: CountryISO.France, dialCode: '33', flag: '🇫🇷'},
{name: 'French Guiana', iso2: CountryISO.FrenchGuiana, dialCode: '594', flag: '🇬🇫'},
{name: 'French Polynesia', iso2: CountryISO.FrenchPolynesia, dialCode: '689', flag: '🇵🇫'},
{name: 'French Southern Territories', iso2: CountryISO.FrenchSouthernTerritories, dialCode: '262', flag: '🇹🇫'},
{name: 'Gabon', iso2: CountryISO.Gabon, dialCode: '241', flag: '🇬🇦'},
{name: 'Gambia', iso2: CountryISO.Gambia, dialCode: '220', flag: '🇬🇲'},
{name: 'Georgia', iso2: CountryISO.Georgia, dialCode: '995', flag: '🇬🇪'},
{name: 'Germany', iso2: CountryISO.Germany, dialCode: '49', flag: '🇩🇪'},
{name: 'Germany', iso2: CountryISO.Germany, dialCode: '49', flag: '🇩🇪', postCodePattern: /[0-9]{5}/},
{name: 'Ghana', iso2: CountryISO.Ghana, dialCode: '233', flag: '🇬🇭'},
{name: 'Gibraltar', iso2: CountryISO.Gibraltar, dialCode: '350', flag: '🇬🇮'},
{name: 'Greece', iso2: CountryISO.Greece, dialCode: '30', flag: '🇬🇷'},
@ -366,20 +379,21 @@ export class CountryData {
{name: 'Guinea-Bissau', iso2: CountryISO.GuineaBissau, dialCode: '245', flag: '🇬🇼'},
{name: 'Guyana', iso2: CountryISO.Guyana, dialCode: '592', flag: '🇬🇾'},
{name: 'Haiti', iso2: CountryISO.Haiti, dialCode: '509', flag: '🇭🇹'},
{name: 'Heard Island and McDonald Islands', iso2: CountryISO.HeardIslandandMcDonaldIslands, dialCode: '672', flag: '🇭🇲'},
{name: 'Honduras', iso2: CountryISO.Honduras, dialCode: '504', flag: '🇭🇳'},
{name: 'Hong Kong', iso2: CountryISO.HongKong, dialCode: '852', flag: '🇭🇰'},
{name: 'Hungary', iso2: CountryISO.Hungary, dialCode: '36', flag: '🇭🇺'},
{name: 'Hungary', iso2: CountryISO.Hungary, dialCode: '36', flag: '🇭🇺', postCodePattern: /[0-9]{4}/},
{name: 'Iceland', iso2: CountryISO.Iceland, dialCode: '354', flag: '🇮🇸'},
{name: 'India', iso2: CountryISO.India, dialCode: '91', flag: '🇮🇳'},
{name: 'Indonesia', iso2: CountryISO.Indonesia, dialCode: '62', flag: '🇮🇩'},
{name: 'Iran', iso2: CountryISO.Iran, dialCode: '98', flag: '🇮🇷'},
{name: 'Islamic Republic of Iran', iso2: CountryISO.Iran, dialCode: '98', flag: '🇮🇷'},
{name: 'Iraq', iso2: CountryISO.Iraq, dialCode: '964', flag: '🇮🇶'},
{name: 'Ireland', iso2: CountryISO.Ireland, dialCode: '353', flag: '🇮🇪'},
{name: 'Isle of Man', iso2: CountryISO.IsleOfMan, dialCode: '44', flag: '🇮🇲'},
{name: 'Israel', iso2: CountryISO.Israel, dialCode: '972', flag: '🇮🇱'},
{name: 'Italy', iso2: CountryISO.Italy, dialCode: '39', flag: '🇮🇹'},
{name: 'Italy', iso2: CountryISO.Italy, dialCode: '39', flag: '🇮🇹', postCodePattern: /[0-9]{5}/},
{name: 'Jamaica', iso2: CountryISO.Jamaica, dialCode: '1', flag: '🇯🇲'},
{name: 'Japan', iso2: CountryISO.Japan, dialCode: '81', flag: '🇯🇵'},
{name: 'Japan', iso2: CountryISO.Japan, dialCode: '81', flag: '🇯🇵', postCodePattern: /\d{3}-\d{4}/},
{name: 'Jersey', iso2: CountryISO.Jersey, dialCode: '44', flag: '🇯🇪'},
{name: 'Jordan', iso2: CountryISO.Jordan, dialCode: '962', flag: '🇯🇴'},
{name: 'Kazakhstan', iso2: CountryISO.Kazakhstan, dialCode: '7', flag: '🇰🇿'},
@ -388,7 +402,7 @@ export class CountryData {
{name: 'Kosovo', iso2: CountryISO.Kosovo, dialCode: '383', flag: '🇽🇰'},
{name: 'Kuwait', iso2: CountryISO.Kuwait, dialCode: '965', flag: '🇰🇼'},
{name: 'Kyrgyzstan', iso2: CountryISO.Kyrgyzstan, dialCode: '996', flag: '🇰🇬'},
{name: 'Laos', iso2: CountryISO.Laos, dialCode: '856', flag: '🇱🇦'},
{name: 'Lao People\'s Democratic Republic', iso2: CountryISO.Laos, dialCode: '856', flag: '🇱🇦'},
{name: 'Latvia', iso2: CountryISO.Latvia, dialCode: '371', flag: '🇱🇻'},
{name: 'Lebanon', iso2: CountryISO.Lebanon, dialCode: '961', flag: '🇱🇧'},
{name: 'Lesotho', iso2: CountryISO.Lesotho, dialCode: '266', flag: '🇱🇸'},
@ -396,9 +410,8 @@ export class CountryData {
{name: 'Libya', iso2: CountryISO.Libya, dialCode: '218', flag: '🇱🇾'},
{name: 'Liechtenstein', iso2: CountryISO.Liechtenstein, dialCode: '423', flag: '🇱🇮'},
{name: 'Lithuania', iso2: CountryISO.Lithuania, dialCode: '370', flag: '🇱🇹'},
{name: 'Luxembourg', iso2: CountryISO.Luxembourg, dialCode: '352', flag: '🇱🇺'},
{name: 'Macau', iso2: CountryISO.Macau, dialCode: '853', flag: '🇲🇴'},
{name: 'Macedonia', iso2: CountryISO.Macedonia, dialCode: '389', flag: '🇲🇰'},
{name: 'Luxembourg', iso2: CountryISO.Luxembourg, dialCode: '352', flag: '🇱🇺', postCodePattern: /(L\s*([-—–]))\s*?\d{4}/},
{name: 'Macao', iso2: CountryISO.Macau, dialCode: '853', flag: '🇲🇴'},
{name: 'Madagascar', iso2: CountryISO.Madagascar, dialCode: '261', flag: '🇲🇬'},
{name: 'Malawi', iso2: CountryISO.Malawi, dialCode: '265', flag: '🇲🇼'},
{name: 'Malaysia', iso2: CountryISO.Malaysia, dialCode: '60', flag: '🇲🇾'},
@ -411,8 +424,8 @@ export class CountryData {
{name: 'Mauritius', iso2: CountryISO.Mauritius, dialCode: '230', flag: '🇲🇺'},
{name: 'Mayotte', iso2: CountryISO.Mayotte, dialCode: '262', flag: '🇾🇹'},
{name: 'Mexico', iso2: CountryISO.Mexico, dialCode: '52', flag: '🇲🇽'},
{name: 'Micronesia', iso2: CountryISO.Micronesia, dialCode: '691', flag: '🇫🇲'},
{name: 'Moldova', iso2: CountryISO.Moldova, dialCode: '373', flag: '🇲🇩'},
{name: 'Micronesia, Federated States of', iso2: CountryISO.Micronesia, dialCode: '691', flag: '🇫🇲'},
{name: 'Moldova, Republic of', iso2: CountryISO.Moldova, dialCode: '373', flag: '🇲🇩'},
{name: 'Monaco', iso2: CountryISO.Monaco, dialCode: '377', flag: '🇲🇨'},
{name: 'Mongolia', iso2: CountryISO.Mongolia, dialCode: '976', flag: '🇲🇳'},
{name: 'Montenegro', iso2: CountryISO.Montenegro, dialCode: '382', flag: '🇲🇪'},
@ -423,7 +436,7 @@ export class CountryData {
{name: 'Namibia', iso2: CountryISO.Namibia, dialCode: '264', flag: '🇳🇦'},
{name: 'Nauru', iso2: CountryISO.Nauru, dialCode: '674', flag: '🇳🇷'},
{name: 'Nepal', iso2: CountryISO.Nepal, dialCode: '977', flag: '🇳🇵'},
{name: 'Netherlands', iso2: CountryISO.Netherlands, dialCode: '31', flag: '🇳🇱'},
{name: 'Netherlands', iso2: CountryISO.Netherlands, dialCode: '31', flag: '🇳🇱', postCodePattern: /[1-9][0-9]{3}\s?[a-zA-Z]{2}/},
{name: 'New Caledonia', iso2: CountryISO.NewCaledonia, dialCode: '687', flag: '🇳🇨'},
{name: 'New Zealand', iso2: CountryISO.NewZealand, dialCode: '64', flag: '🇳🇿'},
{name: 'Nicaragua', iso2: CountryISO.Nicaragua, dialCode: '505', flag: '🇳🇮'},
@ -432,61 +445,65 @@ export class CountryData {
{name: 'Niue', iso2: CountryISO.Niue, dialCode: '683', flag: '🇳🇺'},
{name: 'Norfolk Island', iso2: CountryISO.NorfolkIsland, dialCode: '672', flag: '🇳🇫'},
{name: 'North Korea', iso2: CountryISO.NorthKorea, dialCode: '850', flag: '🇰🇵'},
{name: 'North Macedonia', iso2: CountryISO.NorthMacedonia, dialCode: '389', flag: '🇲🇰'},
{name: 'Northern Mariana Islands', iso2: CountryISO.NorthernMarianaIslands, dialCode: '1', flag: '🇲🇵'},
{name: 'Norway', iso2: CountryISO.Norway, dialCode: '47', flag: '🇳🇴'},
{name: 'Oman', iso2: CountryISO.Oman, dialCode: '968', flag: '🇴🇲'},
{name: 'Pakistan', iso2: CountryISO.Pakistan, dialCode: '92', flag: '🇵🇰'},
{name: 'Palau', iso2: CountryISO.Palau, dialCode: '680', flag: '🇵🇼'},
{name: 'Palestine', iso2: CountryISO.Palestine, dialCode: '970', flag: '🇵🇸'},
{name: 'Palestine, State of', iso2: CountryISO.Palestine, dialCode: '970', flag: '🇵🇸'},
{name: 'Panama', iso2: CountryISO.Panama, dialCode: '507', flag: '🇵🇦'},
{name: 'Papua New Guinea', iso2: CountryISO.PapuaNewGuinea, dialCode: '675', flag: '🇵🇬'},
{name: 'Paraguay', iso2: CountryISO.Paraguay, dialCode: '595', flag: '🇵🇾'},
{name: 'Peru', iso2: CountryISO.Peru, dialCode: '51', flag: '🇵🇪'},
{name: 'Philippines', iso2: CountryISO.Philippines, dialCode: '63', flag: '🇵🇭'},
{name: 'Poland', iso2: CountryISO.Poland, dialCode: '48', flag: '🇵🇱'},
{name: 'Pitcairn Islands', iso2: CountryISO.PitcairnIslands, dialCode: '649', flag: '🇵🇳'},
{name: 'Poland', iso2: CountryISO.Poland, dialCode: '48', flag: '🇵🇱', postCodePattern: /[0-9]{2}-[0-9]{3}/},
{name: 'Portugal', iso2: CountryISO.Portugal, dialCode: '351', flag: '🇵🇹'},
{name: 'Puerto Rico', iso2: CountryISO.PuertoRico, dialCode: '1', flag: '🇵🇷'},
{name: 'Qatar', iso2: CountryISO.Qatar, dialCode: '974', flag: '🇶🇦'},
{name: 'Réunion', iso2: CountryISO.Réunion, dialCode: '262', flag: '🇷🇪'},
{name: 'Réunion', iso2: CountryISO.Reunion, dialCode: '262', flag: '🇷🇪'},
{name: 'Romania', iso2: CountryISO.Romania, dialCode: '40', flag: '🇷🇴'},
{name: 'Russia', iso2: CountryISO.Russia, dialCode: '7', flag: '🇷🇺'},
{name: 'Russian Federation', iso2: CountryISO.Russia, dialCode: '7', flag: '🇷🇺'},
{name: 'Rwanda', iso2: CountryISO.Rwanda, dialCode: '250', flag: '🇷🇼'},
{name: 'Saint Barthélemy', iso2: CountryISO.SaintBarthélemy, dialCode: '590', flag: '🇧🇱'},
{name: 'Saint Helena', iso2: CountryISO.SaintHelena, dialCode: '290', flag: '🇸🇭'},
{name: 'Saint Barthélemy', iso2: CountryISO.SaintBarthelemy, dialCode: '590', flag: '🇧🇱'},
{name: 'Saint Helena, Ascension and Tristan da Cunha', iso2: CountryISO.SaintHelena, dialCode: '290', flag: '🇸🇭'},
{name: 'Saint Kitts and Nevis', iso2: CountryISO.SaintKittsAndNevis, dialCode: '1', flag: '🇰🇳'},
{name: 'Saint Lucia', iso2: CountryISO.SaintLucia, dialCode: '1', flag: '🇱🇨'},
{name: 'Saint Martin', iso2: CountryISO.SaintMartin, dialCode: '590', flag: '🇲🇫'},
{name: 'Saint Martin (French part)', iso2: CountryISO.SaintMartin, dialCode: '590', flag: '🇲🇫'},
{name: 'Saint Pierre and Miquelon', iso2: CountryISO.SaintPierreAndMiquelon, dialCode: '508', flag: '🇵🇲'},
{name: 'Saint Vincent and the Grenadines', iso2: CountryISO.SaintVincentAndTheGrenadines, dialCode: '1', flag: '🇻🇨'},
{name: 'Samoa', iso2: CountryISO.Samoa, dialCode: '685', flag: '🇼🇸'},
{name: 'San Marino', iso2: CountryISO.SanMarino, dialCode: '378', flag: '🇸🇲'},
{name: 'São Tomé and Príncipe', iso2: CountryISO.SãoToméAndPríncipe, dialCode: '239', flag: '🇸🇹'},
{name: 'Sao Tome and Principe', iso2: CountryISO.SaoTomeAndPrincipe, dialCode: '239', flag: '🇸🇹'},
{name: 'Saudi Arabia', iso2: CountryISO.SaudiArabia, dialCode: '966', flag: '🇸🇦'},
{name: 'Senegal', iso2: CountryISO.Senegal, dialCode: '221', flag: '🇸🇳'},
{name: 'Serbia', iso2: CountryISO.Serbia, dialCode: '381', flag: '🇷🇸'},
{name: 'Seychelles', iso2: CountryISO.Seychelles, dialCode: '248', flag: '🇸🇨'},
{name: 'Sierra Leone', iso2: CountryISO.SierraLeone, dialCode: '232', flag: '🇸🇱'},
{name: 'Singapore', iso2: CountryISO.Singapore, dialCode: '65', flag: '🇸🇬'},
{name: 'Sint Maarten', iso2: CountryISO.SintMaarten, dialCode: '1', flag: '🇸🇽'},
{name: 'Sint Maarten (Dutch Part)', iso2: CountryISO.SintMaarten, dialCode: '1', flag: '🇸🇽'},
{name: 'Slovakia', iso2: CountryISO.Slovakia, dialCode: '421', flag: '🇸🇰'},
{name: 'Slovenia', iso2: CountryISO.Slovenia, dialCode: '386', flag: '🇸🇮'},
{name: 'Solomon Islands', iso2: CountryISO.SolomonIslands, dialCode: '677', flag: '🇸🇧'},
{name: 'Somalia', iso2: CountryISO.Somalia, dialCode: '252', flag: '🇸🇴'},
{name: 'South Africa', iso2: CountryISO.SouthAfrica, dialCode: '27', flag: '🇿🇦'},
{name: 'South Georgia and the South Sandwich Islands',
iso2: CountryISO.SouthGeorgiaAndTheSouthSandwichIslands, dialCode: '500', flag: '🇬🇸'},
{name: 'South Korea', iso2: CountryISO.SouthKorea, dialCode: '82', flag: '🇰🇷'},
{name: 'South Sudan', iso2: CountryISO.SouthSudan, dialCode: '211', flag: '🇸🇸'},
{name: 'Spain', iso2: CountryISO.Spain, dialCode: '34', flag: '🇪🇸'},
{name: 'Spain', iso2: CountryISO.Spain, dialCode: '34', flag: '🇪🇸', postCodePattern: /((0[1-9]|5[0-2])|[1-4][0-9])[0-9]{3}/},
{name: 'Sri Lanka', iso2: CountryISO.SriLanka, dialCode: '94', flag: '🇱🇰'},
{name: 'Sudan', iso2: CountryISO.Sudan, dialCode: '249', flag: '🇸🇩'},
{name: 'Suriname: ', iso2: CountryISO.Suriname, dialCode: '597', flag: '🇸🇷'},
{name: 'Suriname', iso2: CountryISO.Suriname, dialCode: '597', flag: '🇸🇷'},
{name: 'Svalbard and Jan Mayen', iso2: CountryISO.SvalbardAndJanMayen, dialCode: '47', flag: '🇸🇯'},
{name: 'Swaziland', iso2: CountryISO.Swaziland, dialCode: '268', flag: '🇸🇿'},
{name: 'Sweden', iso2: CountryISO.Sweden, dialCode: '46', flag: '🇸🇪'},
{name: 'Sweden', iso2: CountryISO.Sweden, dialCode: '46', flag: '🇸🇪', postCodePattern: /\d{3}\s?\d{2}/},
{name: 'Switzerland', iso2: CountryISO.Switzerland, dialCode: '41', flag: '🇨🇭'},
{name: 'Syria', iso2: CountryISO.Syria, dialCode: '963', flag: '🇸🇾'},
{name: 'Syrian Arab Republic', iso2: CountryISO.Syria, dialCode: '963', flag: '🇸🇾'},
{name: 'Taiwan', iso2: CountryISO.Taiwan, dialCode: '886', flag: '🇹🇼'},
{name: 'Tajikistan', iso2: CountryISO.Tajikistan, dialCode: '992', flag: '🇹🇯'},
{name: 'Tanzania', iso2: CountryISO.Tanzania, dialCode: '255', flag: '🇹🇿'},
{name: 'Tanzania, United Republic of', iso2: CountryISO.Tanzania, dialCode: '255', flag: '🇹🇿'},
{name: 'Thailand', iso2: CountryISO.Thailand, dialCode: '66', flag: '🇹🇭'},
{name: 'Timor-Leste', iso2: CountryISO.TimorLeste, dialCode: '670', flag: '🇹🇱'},
{name: 'Togo', iso2: CountryISO.Togo, dialCode: '228', flag: '🇹🇬'},
@ -502,8 +519,8 @@ export class CountryData {
{name: 'Uganda', iso2: CountryISO.Uganda, dialCode: '256', flag: '🇺🇬'},
{name: 'Ukraine', iso2: CountryISO.Ukraine, dialCode: '380', flag: '🇺🇦'},
{name: 'United Arab Emirates', iso2: CountryISO.UnitedArabEmirates, dialCode: '971', flag: '🇦🇪'},
{name: 'United Kingdom', iso2: CountryISO.UnitedKingdom, dialCode: '44', flag: '🇬🇧'},
{name: 'United States', iso2: CountryISO.UnitedStates, dialCode: '1', flag: '🇺🇸'},
{name: 'United Kingdom', iso2: CountryISO.UnitedKingdom, dialCode: '44', flag: '🇬🇧', postCodePattern: /[A-Za-z]{1,2}[0-9Rr][0-9A-Za-z]? [0-9][ABD-HJLNP-UW-Zabd-hjlnp-uw-z]{2}/},
{name: 'United States', iso2: CountryISO.UnitedStates, dialCode: '1', flag: '🇺🇸', postCodePattern: /(\d{5}(-\d{4})?)/},
{name: 'Uruguay', iso2: CountryISO.Uruguay, dialCode: '598', flag: '🇺🇾'},
{name: 'Uzbekistan', iso2: CountryISO.Uzbekistan, dialCode: '998', flag: '🇺🇿'},
{name: 'Vanuatu', iso2: CountryISO.Vanuatu, dialCode: '678', flag: '🇻🇺'},
@ -515,6 +532,7 @@ export class CountryData {
{name: 'Yemen', iso2: CountryISO.Yemen, dialCode: '967', flag: '🇾🇪'},
{name: 'Zambia', iso2: CountryISO.Zambia, dialCode: '260', flag: '🇿🇲'},
{name: 'Zimbabwe', iso2: CountryISO.Zimbabwe, dialCode: '263', flag: '🇿🇼'},
{name: 'Åland Islands', iso2: CountryISO.ÅlandIslands, dialCode: '358', flag: '🇦🇽'}
{name: 'Åland Islands', iso2: CountryISO.AlandIslands, dialCode: '358', flag: '🇦🇽'}
];
}
/* eslint-enable max-len */

2
ui-ngx/src/app/shared/models/dashboard.models.ts

@ -43,6 +43,8 @@ export interface WidgetLayout {
mobileOrder?: number;
col?: number;
row?: number;
resizable?: boolean;
preserveAspectRatio?: boolean;
}
export interface WidgetLayouts {

2
ui-ngx/src/app/shared/models/widget.models.ts

@ -761,6 +761,8 @@ export interface WidgetConfig {
displayTimewindow?: boolean;
timewindow?: Timewindow;
timewindowStyle?: TimewindowStyle;
resizable?: boolean;
preserveAspectRatio?: boolean;
desktopHide?: boolean;
mobileHide?: boolean;
mobileHeight?: number;

7
ui-ngx/src/app/shared/shared.module.ts

@ -222,6 +222,8 @@ import { WidgetButtonComponent } from '@shared/components/button/widget-button.c
import { HexInputComponent } from '@shared/components/color-picker/hex-input.component';
import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe';
import { ScadaSymbolInputComponent } from '@shared/components/image/scada-symbol-input.component';
import { CountryAutocompleteComponent } from '@shared/components/country-autocomplete.component';
import { CountryData } from '@shared/models/country.models';
export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
return markedOptionsService;
@ -278,7 +280,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
disableTooltipInteractivity: true
}
},
TbBreakPointsProvider
TbBreakPointsProvider,
CountryData
],
declarations: [
FooterComponent,
@ -383,6 +386,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
TogglePasswordComponent,
ProtobufContentComponent,
BranchAutocompleteComponent,
CountryAutocompleteComponent,
PhoneInputComponent,
TbScriptLangComponent,
NotificationComponent,
@ -640,6 +644,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
TogglePasswordComponent,
ProtobufContentComponent,
BranchAutocompleteComponent,
CountryAutocompleteComponent,
PhoneInputComponent,
TbScriptLangComponent,
NotificationComponent,

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

@ -234,14 +234,15 @@
"delete-mobile-app-text": "Be careful, after the confirmation the mobile application and all related data will become unrecoverable.",
"title": "Title",
"client-title-required": "Title is required",
"client-title-max-length": "Title should be less than 100",
"advanced-settings": "Advanced settings",
"domain-details": "Domain details",
"no-domains": "No domains found",
"search-domains": "Search domains",
"mobile-app-details": "Mobile application details",
"add-mobile-app": "Add mobile application",
"no-mobile-apps": "Search mobile applications",
"search-mobile-apps": "No mobile applications found",
"no-mobile-apps": "No mobile applications found",
"search-mobile-apps": "Search mobile applications",
"send-token": "Send token",
"create-new": "Create new",
"client-authentication-method": "Client authentication method",
@ -315,6 +316,7 @@
"mobile-package-placeholder": "Ex.: my.example.app",
"mobile-package-hint": "For Android: your own unique Application ID. For iOS: Product bundle identifier.",
"mobile-package-unique": "Application package must be unique.",
"mobile-package-max-length": "Application package should be less than 256",
"mobile-app-secret": "Application secret",
"mobile-app-secret-hint": "Base64 encoded string representing at least 512 bits of data.",
"mobile-app-secret-required": "Application secret is required.",
@ -992,6 +994,7 @@
},
"contact": {
"country": "Country",
"country-required": "Country is required.",
"city": "City",
"state": "State / Province",
"postal-code": "Zip / Postal Code",
@ -1001,6 +1004,8 @@
"phone": "Phone",
"email": "Email",
"no-address": "No address",
"no-country-found": "No countries found.",
"no-country-matching": "No country matching '{{country}}' were found.",
"state-max-length": "State length should be less than 256",
"phone-max-length": "Phone number should be less than 256",
"city-max-length": "Specified city should be less than 256"
@ -6040,7 +6045,11 @@
"color": "Color",
"tooltip": "Tooltip",
"units-required": "Unit is required.",
"list-layout": "List layout"
"list-layout": "List layout",
"layout": "Layout",
"resize-options": "Resize options",
"resizable": "Resizable",
"preserve-aspect-ratio": "Preserve aspect ratio"
},
"widget-type": {
"import": "Import widget type",

Loading…
Cancel
Save