Browse Source

merge with master

pull/10671/head
YevhenBondarenko 2 years ago
parent
commit
02368dfbe9
  1. 2
      application/src/main/data/json/system/widget_types/alarms_table.json
  2. 8
      application/src/main/data/upgrade/3.6.4/schema_update.sql
  3. 4
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java
  4. 3
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/OtaPackageTransportResource.java
  5. 1
      dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java
  6. 2
      dao/src/main/java/org/thingsboard/server/dao/relation/EntityRelationEvent.java
  7. 1
      dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java
  8. 5
      dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java
  9. 23
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathNodeTest.java
  10. 2
      ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts
  11. 4
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html
  12. 7
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts
  13. 39
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html
  14. 7
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.ts
  15. 3
      ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.ts
  16. 34
      ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts
  17. 2
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html
  18. 6
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts
  19. 5
      ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.ts
  20. 9
      ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts
  21. 5
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts
  22. 44
      ui-ngx/src/app/modules/home/components/widget/lib/legend.component.html
  23. 16
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-settings-panel.component.html
  24. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-settings-panel.component.scss
  25. 8
      ui-ngx/src/app/shared/components/color-input.component.ts
  26. 26
      ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.html
  27. 17
      ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.scss
  28. 16
      ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.ts
  29. 57
      ui-ngx/src/app/shared/components/color-picker/color-picker.component.html
  30. 49
      ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss
  31. 4
      ui-ngx/src/app/shared/components/color-picker/color-picker.component.ts
  32. 36
      ui-ngx/src/app/shared/components/color-picker/hex-input.component.html
  33. 48
      ui-ngx/src/app/shared/components/color-picker/hex-input.component.scss
  34. 75
      ui-ngx/src/app/shared/components/color-picker/hex-input.component.ts
  35. 8
      ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.html
  36. 28
      ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.scss
  37. 3
      ui-ngx/src/app/shared/components/popover.component.ts
  38. 9
      ui-ngx/src/app/shared/components/popover.service.ts
  39. 1
      ui-ngx/src/app/shared/models/dashboard.models.ts
  40. 16
      ui-ngx/src/app/shared/shared.module.ts
  41. 3
      ui-ngx/src/assets/locale/locale.constant-ca_ES.json
  42. 3
      ui-ngx/src/assets/locale/locale.constant-cs_CZ.json
  43. 6
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  44. 3
      ui-ngx/src/assets/locale/locale.constant-es_ES.json
  45. 3
      ui-ngx/src/assets/locale/locale.constant-nl_BE.json
  46. 14186
      ui-ngx/src/assets/locale/locale.constant-pl_PL.json
  47. 3
      ui-ngx/src/assets/locale/locale.constant-tr_TR.json
  48. 3
      ui-ngx/src/assets/locale/locale.constant-zh_CN.json
  49. 3
      ui-ngx/src/assets/locale/locale.constant-zh_TW.json

2
application/src/main/data/json/system/widget_types/alarms_table.json

@ -11,7 +11,7 @@
"resources": [], "resources": [],
"templateHtml": "<tb-alarms-table-widget \n [ctx]=\"ctx\">\n</tb-alarms-table-widget>", "templateHtml": "<tb-alarms-table-widget \n [ctx]=\"ctx\">\n</tb-alarms-table-widget>",
"templateCss": "", "templateCss": "",
"controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.alarmsTableWidget.onDataUpdated();\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.alarmsTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.alarmsTableWidget.onEditModeChanged();\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "", "settingsSchema": "",
"dataKeySettingsSchema": "", "dataKeySettingsSchema": "",
"settingsDirective": "tb-alarms-table-widget-settings", "settingsDirective": "tb-alarms-table-widget-settings",

8
application/src/main/data/upgrade/3.6.4/schema_update.sql

@ -107,13 +107,7 @@ BEGIN
SELECT COUNT(*) INTO row_num_old FROM attribute_kv_old; SELECT COUNT(*) INTO row_num_old FROM attribute_kv_old;
SELECT COUNT(*) INTO row_num FROM attribute_kv; SELECT COUNT(*) INTO row_num FROM attribute_kv;
RAISE NOTICE 'Migrated % of % rows', row_num, row_num_old; RAISE NOTICE 'Migrated % of % rows', row_num, row_num_old;
DROP TABLE IF EXISTS attribute_kv_old;
IF row_num != 0 THEN
DROP TABLE IF EXISTS attribute_kv_old;
ELSE
RAISE EXCEPTION 'Table attribute_kv is empty';
END IF;
CREATE INDEX IF NOT EXISTS idx_attribute_kv_by_key_and_last_update_ts ON attribute_kv(entity_id, attribute_key, last_update_ts desc); CREATE INDEX IF NOT EXISTS idx_attribute_kv_by_key_and_last_update_ts ON attribute_kv(entity_id, attribute_key, last_update_ts desc);
END IF; END IF;
EXCEPTION EXCEPTION

4
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java

@ -103,7 +103,7 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleCh
RuleChain ruleChain = service.findRuleChainById(tenantId, entityId); RuleChain ruleChain = service.findRuleChainById(tenantId, entityId);
if (ruleChain != null && RuleChainType.CORE.equals(ruleChain.getType())) { if (ruleChain != null && RuleChainType.CORE.equals(ruleChain.getType())) {
List<RuleNode> ruleNodeList = service.getRuleChainNodes(tenantId, entityId); List<RuleNode> ruleNodeList = service.getRuleChainNodes(tenantId, entityId);
log.trace("[{}][{}] Starting rule chain with {} nodes", tenantId, entityId, ruleNodeList.size()); log.debug("[{}][{}] Starting rule chain with {} nodes", tenantId, entityId, ruleNodeList.size());
// Creating and starting the actors; // Creating and starting the actors;
for (RuleNode ruleNode : ruleNodeList) { for (RuleNode ruleNode : ruleNodeList) {
log.trace("[{}][{}] Creating rule node [{}]: {}", entityId, ruleNode.getId(), ruleNode.getName(), ruleNode); log.trace("[{}][{}] Creating rule node [{}]: {}", entityId, ruleNode.getId(), ruleNode.getName(), ruleNode);
@ -124,7 +124,7 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleCh
if (ruleChain != null && RuleChainType.CORE.equals(ruleChain.getType())) { if (ruleChain != null && RuleChainType.CORE.equals(ruleChain.getType())) {
ruleChainName = ruleChain.getName(); ruleChainName = ruleChain.getName();
List<RuleNode> ruleNodeList = service.getRuleChainNodes(tenantId, entityId); List<RuleNode> ruleNodeList = service.getRuleChainNodes(tenantId, entityId);
log.trace("[{}][{}] Updating rule chain with {} nodes", tenantId, entityId, ruleNodeList.size()); log.debug("[{}][{}] Updating rule chain with {} nodes", tenantId, entityId, ruleNodeList.size());
for (RuleNode ruleNode : ruleNodeList) { for (RuleNode ruleNode : ruleNodeList) {
RuleNodeCtx existing = nodeActors.get(ruleNode.getId()); RuleNodeCtx existing = nodeActors.get(ruleNode.getId());
if (existing == null) { if (existing == null) {

3
common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/OtaPackageTransportResource.java

@ -143,8 +143,9 @@ public class OtaPackageTransportResource extends AbstractCoapTransportResource {
response.setPayload(data); response.setPayload(data);
if (exchange.getRequestOptions().getBlock2() != null) { if (exchange.getRequestOptions().getBlock2() != null) {
int chunkSize = exchange.getRequestOptions().getBlock2().getSzx(); int chunkSize = exchange.getRequestOptions().getBlock2().getSzx();
int blockNum = exchange.getRequestOptions().getBlock2().getNum();
boolean lastFlag = data.length <= chunkSize; boolean lastFlag = data.length <= chunkSize;
response.getOptions().setBlock2(chunkSize, lastFlag, 0); response.getOptions().setBlock2(chunkSize, lastFlag, blockNum);
} }
transportContext.getExecutor().submit(() -> exchange.respond(response)); transportContext.getExecutor().submit(() -> exchange.respond(response));
} }

1
dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java

@ -121,6 +121,7 @@ public class BaseRelationService implements RelationService {
keys.add(new RelationCacheKey(null, event.getTo(), event.getType(), event.getTypeGroup(), EntitySearchDirection.TO)); keys.add(new RelationCacheKey(null, event.getTo(), event.getType(), event.getTypeGroup(), EntitySearchDirection.TO));
keys.add(new RelationCacheKey(null, event.getTo(), null, event.getTypeGroup(), EntitySearchDirection.TO)); keys.add(new RelationCacheKey(null, event.getTo(), null, event.getTypeGroup(), EntitySearchDirection.TO));
cache.evict(keys); cache.evict(keys);
log.debug("Processed evict event: {}", event);
} }
@Override @Override

2
dao/src/main/java/org/thingsboard/server/dao/relation/EntityRelationEvent.java

@ -17,11 +17,13 @@ package org.thingsboard.server.dao.relation;
import lombok.Getter; import lombok.Getter;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.ToString;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.relation.RelationTypeGroup;
@RequiredArgsConstructor @RequiredArgsConstructor
@ToString
public class EntityRelationEvent { public class EntityRelationEvent {
@Getter @Getter
private final EntityId from; private final EntityId from;

1
dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java

@ -197,6 +197,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
List<RuleNodeUpdateResult> updatedRuleNodes = new ArrayList<>(); List<RuleNodeUpdateResult> updatedRuleNodes = new ArrayList<>();
List<RuleNode> existingRuleNodes = getRuleChainNodes(tenantId, ruleChainMetaData.getRuleChainId()); List<RuleNode> existingRuleNodes = getRuleChainNodes(tenantId, ruleChainMetaData.getRuleChainId());
for (RuleNode existingNode : existingRuleNodes) { for (RuleNode existingNode : existingRuleNodes) {
relationService.deleteEntityRelations(tenantId, existingNode.getId());
Integer index = ruleNodeIndexMap.get(existingNode.getId()); Integer index = ruleNodeIndexMap.get(existingNode.getId());
RuleNode newRuleNode = null; RuleNode newRuleNode = null;
if (index != null) { if (index != null) {

5
dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java

@ -135,8 +135,6 @@ public class TenantServiceImpl extends AbstractCachedEntityService<TenantId, Ten
Tenant savedTenant = tenantDao.save(tenant.getId(), tenant); Tenant savedTenant = tenantDao.save(tenant.getId(), tenant);
TenantId tenantId = savedTenant.getId(); TenantId tenantId = savedTenant.getId();
publishEvictEvent(new TenantEvictEvent(tenantId, create)); publishEvictEvent(new TenantEvictEvent(tenantId, create));
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId)
.entityId(tenantId).entity(savedTenant).created(create).build());
if (create) { if (create) {
deviceProfileService.createDefaultDeviceProfile(tenantId); deviceProfileService.createDefaultDeviceProfile(tenantId);
@ -147,6 +145,9 @@ public class TenantServiceImpl extends AbstractCachedEntityService<TenantId, Ten
defaultEntitiesCreator.accept(tenantId); defaultEntitiesCreator.accept(tenantId);
} }
} }
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId)
.entityId(tenantId).entity(savedTenant).created(create).build());
return savedTenant; return savedTenant;
} }

23
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathNodeTest.java

@ -89,13 +89,13 @@ import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
public class TbMathNodeTest { public class TbMathNodeTest {
static final int RULE_DISPATCHER_POOL_SIZE = 2; static final int RULE_DISPATCHER_POOL_SIZE = 3;
static final int DB_CALLBACK_POOL_SIZE = 3; static final int DB_CALLBACK_POOL_SIZE = 4;
static final long TIMEOUT = TimeUnit.SECONDS.toMillis(5); static final long TIMEOUT = TimeUnit.SECONDS.toMillis(5);
private final EntityId originator = DeviceId.fromString("ccd71696-0586-422d-940e-755a41ec3b0d"); private final EntityId originator = DeviceId.fromString("ccd71696-0586-422d-940e-755a41ec3b0d");
private final TenantId tenantId = TenantId.fromUUID(UUID.fromString("e7f46b23-0c7d-42f5-9b06-fc35ab17af8a")); private final TenantId tenantId = TenantId.fromUUID(UUID.fromString("e7f46b23-0c7d-42f5-9b06-fc35ab17af8a"));
@Mock(lenient = true) @Mock(strictness = Mock.Strictness.LENIENT)
private TbContext ctx; private TbContext ctx;
@Mock @Mock
private AttributesService attributesService; private AttributesService attributesService;
@ -122,8 +122,9 @@ public class TbMathNodeTest {
@AfterEach @AfterEach
public void after() { public void after() {
ruleEngineDispatcherExecutor.executor().shutdownNow(); // shutdownNow makes some tests flaky
dbCallbackExecutor.executor().shutdownNow(); ruleEngineDispatcherExecutor.destroy();
dbCallbackExecutor.destroy();
} }
private TbMathNode initNode(TbRuleNodeMathFunctionType operation, TbMathResult result, TbMathArgument... arguments) { private TbMathNode initNode(TbRuleNodeMathFunctionType operation, TbMathResult result, TbMathArgument... arguments) {
@ -538,7 +539,7 @@ public class TbMathNodeTest {
node.onMsg(ctx, msg); node.onMsg(ctx, msg);
ArgumentCaptor<Throwable> tCaptor = ArgumentCaptor.forClass(Throwable.class); ArgumentCaptor<Throwable> tCaptor = ArgumentCaptor.forClass(Throwable.class);
Mockito.verify(ctx, Mockito.timeout(5000)).tellFailure(eq(msg), tCaptor.capture()); Mockito.verify(ctx, timeout(TIMEOUT)).tellFailure(eq(msg), tCaptor.capture());
assertNotNull(tCaptor.getValue().getMessage()); assertNotNull(tCaptor.getValue().getMessage());
} }
@ -553,7 +554,7 @@ public class TbMathNodeTest {
node.onMsg(ctx, msg); node.onMsg(ctx, msg);
ArgumentCaptor<Throwable> tCaptor = ArgumentCaptor.forClass(Throwable.class); ArgumentCaptor<Throwable> tCaptor = ArgumentCaptor.forClass(Throwable.class);
Mockito.verify(ctx, Mockito.timeout(5000)).tellFailure(eq(msg), tCaptor.capture()); Mockito.verify(ctx, timeout(TIMEOUT)).tellFailure(eq(msg), tCaptor.capture());
assertNotNull(tCaptor.getValue().getMessage()); assertNotNull(tCaptor.getValue().getMessage());
} }
@ -669,11 +670,11 @@ public class TbMathNodeTest {
// submit slow msg may block all rule engine dispatcher threads // submit slow msg may block all rule engine dispatcher threads
slowMsgList.forEach(msg -> ruleEngineDispatcherExecutor.executeAsync(() -> node.onMsg(ctx, msg))); slowMsgList.forEach(msg -> ruleEngineDispatcherExecutor.executeAsync(() -> node.onMsg(ctx, msg)));
// wait until dispatcher threads started with all slowMsg // wait until dispatcher threads started with all slowMsg
verify(node, new Timeout(TimeUnit.SECONDS.toMillis(5), times(slowMsgList.size()))).onMsg(eq(ctx), argThat(slowMsgList::contains)); verify(node, new Timeout(TIMEOUT, times(slowMsgList.size()))).onMsg(eq(ctx), argThat(slowMsgList::contains));
slowProcessingLatch.countDown(); slowProcessingLatch.countDown();
verify(ctx, new Timeout(TimeUnit.SECONDS.toMillis(5), times(slowMsgList.size()))).tellFailure(any(), any()); verify(ctx, new Timeout(TIMEOUT, times(slowMsgList.size()))).tellFailure(any(), any());
verify(ctx, never()).tellSuccess(any()); verify(ctx, never()).tellSuccess(any());
} }
@ -713,7 +714,7 @@ public class TbMathNodeTest {
.toList(); .toList();
ctxNodes.forEach(ctxNode -> ruleEngineDispatcherExecutor.executeAsync(() -> ctxNode.getRight() ctxNodes.forEach(ctxNode -> ruleEngineDispatcherExecutor.executeAsync(() -> ctxNode.getRight()
.onMsg(ctxNode.getLeft(), TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, TbMsgMetaData.EMPTY, "{\"a\":2,\"b\":2}")))); .onMsg(ctxNode.getLeft(), TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, TbMsgMetaData.EMPTY, "{\"a\":2,\"b\":2}"))));
ctxNodes.forEach(ctxNode -> verify(ctxNode.getRight(), timeout(5000)).onMsg(eq(ctxNode.getLeft()), any())); ctxNodes.forEach(ctxNode -> verify(ctxNode.getRight(), timeout(TIMEOUT)).onMsg(eq(ctxNode.getLeft()), any()));
processingLatch.countDown(); processingLatch.countDown();
SoftAssertions softly = new SoftAssertions(); SoftAssertions softly = new SoftAssertions();
@ -721,7 +722,7 @@ public class TbMathNodeTest {
final TbContext ctx = ctxNode.getLeft(); final TbContext ctx = ctxNode.getLeft();
final String resultKey = ctxNode.getMiddle(); final String resultKey = ctxNode.getMiddle();
ArgumentCaptor<TbMsg> msgCaptor = ArgumentCaptor.forClass(TbMsg.class); ArgumentCaptor<TbMsg> msgCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctx, timeout(5000)).tellSuccess(msgCaptor.capture()); verify(ctx, timeout(TIMEOUT)).tellSuccess(msgCaptor.capture());
TbMsg resultMsg = msgCaptor.getValue(); TbMsg resultMsg = msgCaptor.getValue();
assertThat(resultMsg).as("result msg non null for result key " + resultKey).isNotNull(); assertThat(resultMsg).as("result msg non null for result key " + resultKey).isNotNull();

2
ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts

@ -115,7 +115,7 @@ export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink>
new DateEntityTableColumn<AlarmInfo>('createdTime', 'alarm.created-time', this.datePipe, '150px')); new DateEntityTableColumn<AlarmInfo>('createdTime', 'alarm.created-time', this.datePipe, '150px'));
this.columns.push( this.columns.push(
new EntityLinkTableColumn<AlarmInfo>('originatorName', 'alarm.originator', '25%', new EntityLinkTableColumn<AlarmInfo>('originatorName', 'alarm.originator', '25%',
(entity) => entity.originatorName, (entity) => this.utilsService.customTranslation(entity.originatorName, entity.originatorName),
(entity) => getEntityDetailsPageURL(entity.originator.id, entity.originator.entityType as EntityType))); (entity) => getEntityDetailsPageURL(entity.originator.id, entity.originator.entityType as EntityType)));
this.columns.push( this.columns.push(
new EntityTableColumn<AlarmInfo>('type', 'alarm.type', '25%', new EntityTableColumn<AlarmInfo>('type', 'alarm.type', '25%',

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

@ -296,7 +296,7 @@
[(opened)]="rightLayoutOpened"> [(opened)]="rightLayoutOpened">
<tb-dashboard-layout style="height: 100%;" <tb-dashboard-layout style="height: 100%;"
[dashboardCheatSheet]="cheatSheetComponent" [dashboardCheatSheet]="cheatSheetComponent"
[layoutCtx]="layouts.right.layoutCtx" [layoutCtx]="mobileDisplayRightLayoutFirst ? layouts.main.layoutCtx : layouts.right.layoutCtx"
[dashboardCtx]="dashboardCtx" [dashboardCtx]="dashboardCtx"
[isEdit]="isEdit" [isEdit]="isEdit"
[isEditingWidget]="isEditingWidget" [isEditingWidget]="isEditingWidget"
@ -312,7 +312,7 @@
height: mainLayoutSize.height}"> height: mainLayoutSize.height}">
<tb-dashboard-layout <tb-dashboard-layout
[dashboardCheatSheet]="cheatSheetComponent" [dashboardCheatSheet]="cheatSheetComponent"
[layoutCtx]="layouts.main.layoutCtx" [layoutCtx]="mobileDisplayRightLayoutFirst ? layouts.right.layoutCtx : layouts.main.layoutCtx"
[dashboardCtx]="dashboardCtx" [dashboardCtx]="dashboardCtx"
[isEdit]="isEdit" [isEdit]="isEdit"
[isEditingWidget]="isEditingWidget" [isEditingWidget]="isEditingWidget"

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

@ -320,6 +320,13 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
set rightLayoutOpened(rightLayoutOpened: boolean) { set rightLayoutOpened(rightLayoutOpened: boolean) {
} }
get mobileDisplayRightLayoutFirst(): boolean {
return this.isMobile && this.layouts.right.layoutCtx.gridSettings?.mobileDisplayLayoutFirst;
}
set mobileDisplayRightLayoutFirst(mobileDisplayRightLayoutFirst: boolean) {
}
@ViewChild('tbEditWidget') editWidgetComponent: EditWidgetComponent; @ViewChild('tbEditWidget') editWidgetComponent: EditWidgetComponent;
@ViewChild('dashboardWidgetSelect') dashboardWidgetSelectComponent: DashboardWidgetSelectComponent; @ViewChild('dashboardWidgetSelect') dashboardWidgetSelectComponent: DashboardWidgetSelectComponent;

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

@ -159,25 +159,30 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</fieldset> </fieldset>
<fieldset class="fields-group" fxLayout="column" fxLayout.gt-sm="row" fxLayoutAlign.gt-sm="start center" fxLayoutGap="8px"> <fieldset class="fields-group" fxLayout="column" fxLayoutGap="8px">
<legend class="group-title" translate>dashboard.mobile-layout</legend> <legend class="group-title" translate>dashboard.mobile-layout</legend>
<mat-slide-toggle fxFlex formControlName="mobileAutoFillHeight"> <mat-slide-toggle *ngIf="isRightLayout" fxFlex formControlName="mobileDisplayLayoutFirst" style="display: block;">
{{ 'dashboard.autofill-height' | translate }} {{ 'dashboard.display-first-in-mobile-view' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
<mat-form-field fxFlex class="mat-block"> <div fxLayout="column" fxLayout.gt-sm="row" fxLayoutAlign.gt-sm="start center" fxLayoutGap="8px" >
<mat-label translate>dashboard.mobile-row-height</mat-label> <mat-slide-toggle fxFlex formControlName="mobileAutoFillHeight">
<input matInput formControlName="mobileRowHeight" type="number" step="any" min="5" {{ 'dashboard.autofill-height' | translate }}
max="200" required> </mat-slide-toggle>
<mat-error *ngIf="gridSettingsFormGroup.get('mobileRowHeight').hasError('required')"> <mat-form-field fxFlex class="mat-block">
{{ 'dashboard.mobile-row-height-required' | translate }} <mat-label translate>dashboard.mobile-row-height</mat-label>
</mat-error> <input matInput formControlName="mobileRowHeight" type="number" step="any" min="5"
<mat-error *ngIf="gridSettingsFormGroup.get('mobileRowHeight').hasError('min')"> max="200" required>
{{ 'dashboard.min-mobile-row-height-message' | translate }} <mat-error *ngIf="gridSettingsFormGroup.get('mobileRowHeight').hasError('required')">
</mat-error> {{ 'dashboard.mobile-row-height-required' | translate }}
<mat-error *ngIf="gridSettingsFormGroup.get('mobileRowHeight').hasError('max')"> </mat-error>
{{ 'dashboard.max-mobile-row-height-message' | translate }} <mat-error *ngIf="gridSettingsFormGroup.get('mobileRowHeight').hasError('min')">
</mat-error> {{ 'dashboard.min-mobile-row-height-message' | translate }}
</mat-form-field> </mat-error>
<mat-error *ngIf="gridSettingsFormGroup.get('mobileRowHeight').hasError('max')">
{{ 'dashboard.max-mobile-row-height-message' | translate }}
</mat-error>
</mat-form-field>
</div>
</fieldset> </fieldset>
</div> </div>
</fieldset> </fieldset>

7
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.ts

@ -30,6 +30,7 @@ import { StatesControllerService } from './states/states-controller.service';
export interface DashboardSettingsDialogData { export interface DashboardSettingsDialogData {
settings?: DashboardSettings; settings?: DashboardSettings;
gridSettings?: GridSettings; gridSettings?: GridSettings;
isRightLayout?: boolean;
} }
@Component({ @Component({
@ -43,6 +44,7 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
settings: DashboardSettings; settings: DashboardSettings;
gridSettings: GridSettings; gridSettings: GridSettings;
isRightLayout = false;
settingsFormGroup: FormGroup; settingsFormGroup: FormGroup;
gridSettingsFormGroup: FormGroup; gridSettingsFormGroup: FormGroup;
@ -69,6 +71,7 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
this.settings = this.data.settings; this.settings = this.data.settings;
this.gridSettings = this.data.gridSettings; this.gridSettings = this.data.gridSettings;
this.isRightLayout = this.data.isRightLayout;
if (this.settings) { if (this.settings) {
const showTitle = isUndefined(this.settings.showTitle) ? true : this.settings.showTitle; const showTitle = isUndefined(this.settings.showTitle) ? true : this.settings.showTitle;
@ -165,6 +168,10 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
mobileRowHeight: [{ value: isUndefined(this.gridSettings.mobileRowHeight) ? 70 : this.gridSettings.mobileRowHeight, mobileRowHeight: [{ value: isUndefined(this.gridSettings.mobileRowHeight) ? 70 : this.gridSettings.mobileRowHeight,
disabled: mobileAutoFillHeight}, [Validators.required, Validators.min(5), Validators.max(200)]] disabled: mobileAutoFillHeight}, [Validators.required, Validators.min(5), Validators.max(200)]]
}); });
if (this.isRightLayout) {
const mobileDisplayLayoutFirst = isUndefined(this.gridSettings.mobileDisplayLayoutFirst) ? false : this.gridSettings.mobileDisplayLayoutFirst;
this.gridSettingsFormGroup.addControl('mobileDisplayLayoutFirst', this.fb.control(mobileDisplayLayoutFirst, []));
}
this.gridSettingsFormGroup.get('mobileAutoFillHeight').valueChanges.subscribe( this.gridSettingsFormGroup.get('mobileAutoFillHeight').valueChanges.subscribe(
(mobileAutoFillHeightValue: boolean) => { (mobileAutoFillHeightValue: boolean) => {
if (mobileAutoFillHeightValue) { if (mobileAutoFillHeightValue) {

3
ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.ts

@ -217,7 +217,8 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: { data: {
settings: null, settings: null,
gridSettings gridSettings,
isRightLayout: this.layoutsFormGroup.get('right').value && layoutId === 'right'
} }
}).afterClosed().subscribe((data) => { }).afterClosed().subscribe((data) => {
if (data && data.gridSettings) { if (data && data.gridSettings) {

34
ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts

@ -35,7 +35,7 @@ import { TranslateService } from '@ngx-translate/core';
import { entityFields } from '@shared/models/entity.models'; import { entityFields } from '@shared/models/entity.models';
import { Observable, of, Subject } from 'rxjs'; import { Observable, of, Subject } from 'rxjs';
import { filter, map, mergeMap, publishReplay, refCount, startWith, takeUntil } from 'rxjs/operators'; import { filter, map, mergeMap, publishReplay, refCount, startWith, takeUntil } from 'rxjs/operators';
import { isDefined } from '@core/utils'; import { isBoolean, isDefined } from '@core/utils';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { DeviceProfileService } from '@core/http/device-profile.service'; import { DeviceProfileService } from '@core/http/device-profile.service';
@ -121,22 +121,30 @@ export class KeyFilterDialogComponent extends
this.keyFilterFormGroup.get('valueType').valueChanges.pipe( this.keyFilterFormGroup.get('valueType').valueChanges.pipe(
takeUntil(this.destroy$) takeUntil(this.destroy$)
).subscribe((valueType: EntityKeyValueType) => { ).subscribe((valueType: EntityKeyValueType) => {
const prevValue: EntityKeyValueType = this.keyFilterFormGroup.value.valueType; const prevValueType: EntityKeyValueType = this.keyFilterFormGroup.value.valueType;
const predicates: KeyFilterPredicate[] = this.keyFilterFormGroup.get('predicates').value; const predicates: KeyFilterPredicate[] = this.keyFilterFormGroup.get('predicates').value;
if (prevValue && prevValue !== valueType && predicates && predicates.length) { const value = this.keyFilterFormGroup.get('value')?.value;
this.dialogs.confirm(this.translate.instant('filter.key-value-type-change-title'), if (prevValueType && prevValueType !== valueType) {
this.translate.instant('filter.key-value-type-change-message')).subscribe( if (this.isConstantKeyType && this.data.telemetryKeysOnly) {
(result) => { this.keyFilterFormGroup.get('value').setValue(null);
if (result) { }
this.keyFilterFormGroup.get('predicates').setValue([]); if (predicates && predicates.length) {
} else { this.dialogs.confirm(this.translate.instant('filter.key-value-type-change-title'),
this.keyFilterFormGroup.get('valueType').setValue(prevValue, {emitEvent: false}); this.translate.instant('filter.key-value-type-change-message')).subscribe(
(result) => {
if (result) {
this.keyFilterFormGroup.get('predicates').setValue([]);
} else {
this.keyFilterFormGroup.get('valueType').setValue(prevValueType, {emitEvent: false});
this.keyFilterFormGroup.get('value')?.setValue(value, {emitEvent: false});
}
} }
} );
); }
} }
if (valueType === EntityKeyValueType.BOOLEAN && this.isConstantKeyType) { if (this.data.telemetryKeysOnly && this.isConstantKeyType && valueType === EntityKeyValueType.BOOLEAN) {
this.keyFilterFormGroup.get('value').clearValidators(); this.keyFilterFormGroup.get('value').clearValidators();
this.keyFilterFormGroup.get('value').setValue(isBoolean(value) ? value : false);
this.keyFilterFormGroup.get('value').updateValueAndValidity(); this.keyFilterFormGroup.get('value').updateValueAndValidity();
} }
}); });

2
ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html

@ -91,8 +91,6 @@
<tb-power-mode-settings [parentForm]="clientSettingsFormGroup"> <tb-power-mode-settings [parentForm]="clientSettingsFormGroup">
</tb-power-mode-settings> </tb-power-mode-settings>
</fieldset> </fieldset>
<mat-slide-toggle class="mat-slider"
formControlName="compositeOperationsSupport">{{ 'device-profile.lwm2m.composite-operations-support' | translate }}</mat-slide-toggle>
<!-- <mat-accordion multi="true">--> <!-- <mat-accordion multi="true">-->
<!-- <div *ngIf="false">--> <!-- <div *ngIf="false">-->
<!-- <mat-expansion-panel>--> <!-- <mat-expansion-panel>-->

6
ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts

@ -116,8 +116,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro
powerMode: [PowerMode.DRX, Validators.required], powerMode: [PowerMode.DRX, Validators.required],
edrxCycle: [{disabled: true, value: 0}, Validators.required], edrxCycle: [{disabled: true, value: 0}, Validators.required],
psmActivityTimer: [{disabled: true, value: 0}, Validators.required], psmActivityTimer: [{disabled: true, value: 0}, Validators.required],
pagingTransmissionWindow: [{disabled: true, value: 0}, Validators.required], pagingTransmissionWindow: [{disabled: true, value: 0}, Validators.required]
compositeOperationsSupport: [false]
}) })
}); });
@ -275,8 +274,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro
edrxCycle: this.configurationValue.clientLwM2mSettings.edrxCycle || DEFAULT_EDRX_CYCLE, edrxCycle: this.configurationValue.clientLwM2mSettings.edrxCycle || DEFAULT_EDRX_CYCLE,
pagingTransmissionWindow: pagingTransmissionWindow:
this.configurationValue.clientLwM2mSettings.pagingTransmissionWindow || DEFAULT_PAGING_TRANSMISSION_WINDOW, this.configurationValue.clientLwM2mSettings.pagingTransmissionWindow || DEFAULT_PAGING_TRANSMISSION_WINDOW,
psmActivityTimer: this.configurationValue.clientLwM2mSettings.psmActivityTimer || DEFAULT_PSM_ACTIVITY_TIMER, psmActivityTimer: this.configurationValue.clientLwM2mSettings.psmActivityTimer || DEFAULT_PSM_ACTIVITY_TIMER
compositeOperationsSupport: this.configurationValue.clientLwM2mSettings.compositeOperationsSupport || false
} }
}, },
{emitEvent: false}); {emitEvent: false});

5
ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.ts

@ -532,10 +532,11 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
const colorPickerPopover = this.popoverService.displayPopover(trigger, this.renderer, const colorPickerPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, ColorPickerPanelComponent, ['leftTopOnly', 'leftOnly', 'leftBottomOnly'], true, null, this.viewContainerRef, ColorPickerPanelComponent, ['leftTopOnly', 'leftOnly', 'leftBottomOnly'], true, null,
{ {
color: key.color color: key.color,
colorCancelButton: true
}, },
{}, {},
{}, {}, true); {}, {}, false, () => {}, {padding: '12px 4px 12px 12px'});
colorPickerPopover.tbComponentRef.instance.popover = colorPickerPopover; colorPickerPopover.tbComponentRef.instance.popover = colorPickerPopover;
colorPickerPopover.tbComponentRef.instance.colorSelected.subscribe((color) => { colorPickerPopover.tbComponentRef.instance.colorSelected.subscribe((color) => {
colorPickerPopover.hide(); colorPickerPopover.hide();

9
ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts

@ -321,6 +321,13 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
this.ctx.detectChanges(); this.ctx.detectChanges();
} }
public onEditModeChanged() {
if (this.textSearchMode || this.enableSelection && this.alarmsDatasource.selection.hasValue()) {
this.ctx.hideTitlePanel = !this.ctx.isEdit;
this.ctx.detectChanges(true);
}
}
public pageLinkSortDirection(): SortDirection { public pageLinkSortDirection(): SortDirection {
return entityDataPageLinkSortDirection(this.pageLink); return entityDataPageLinkSortDirection(this.pageLink);
} }
@ -487,7 +494,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
this.alarmsDatasource = new AlarmsDatasource(this.subscription, latestDataKeys, this.ngZone, this.ctx, actionCellDescriptors); this.alarmsDatasource = new AlarmsDatasource(this.subscription, latestDataKeys, this.ngZone, this.ctx, actionCellDescriptors);
if (this.enableSelection) { if (this.enableSelection) {
this.alarmsDatasource.selectionModeChanged$.subscribe((selectionMode) => { this.alarmsDatasource.selectionModeChanged$.subscribe((selectionMode) => {
const hideTitlePanel = selectionMode || this.textSearchMode; const hideTitlePanel = selectionMode || this.textSearchMode && !this.ctx.isEdit;
if (this.ctx.hideTitlePanel !== hideTitlePanel) { if (this.ctx.hideTitlePanel !== hideTitlePanel) {
this.ctx.hideTitlePanel = hideTitlePanel; this.ctx.hideTitlePanel = hideTitlePanel;
this.ctx.detectChanges(true); this.ctx.detectChanges(true);

5
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts

@ -262,8 +262,11 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
tasks.push(this.attributeService.deleteEntityAttributes(this.device, scope, attributesToDelete)); tasks.push(this.attributeService.deleteEntityAttributes(this.device, scope, attributesToDelete));
} }
forkJoin(tasks).subscribe(_ => { forkJoin(tasks).subscribe(_ => {
this.showToast(!this.initialConnector
? this.translate.instant('gateway.connector-created')
: this.translate.instant('gateway.connector-updated')
);
this.initialConnector = value; this.initialConnector = value;
this.showToast('Update Successful');
this.updateData(true); this.updateData(true);
}); });
} }

44
ui-ngx/src/app/modules/home/components/widget/lib/legend.component.html

@ -32,14 +32,19 @@
<td><span class="tb-legend-line" [ngStyle]="{backgroundColor: legendKey.dataKey.color}"></span></td> <td><span class="tb-legend-line" [ngStyle]="{backgroundColor: legendKey.dataKey.color}"></span></td>
<td class="tb-legend-label" <td class="tb-legend-label"
(click)="toggleHideData(legendKey.dataIndex)" (click)="toggleHideData(legendKey.dataIndex)"
[innerHTML]="legendKey.dataKey.label | safe: 'html'"
[ngClass]="{ 'tb-hidden-label': legendKey.dataKey.hidden, 'tb-horizontal': isHorizontal }"> [ngClass]="{ 'tb-hidden-label': legendKey.dataKey.hidden, 'tb-horizontal': isHorizontal }">
{{ legendKey.dataKey.label }}
</td> </td>
<td class="tb-legend-value" *ngIf="legendConfig.showMin === true">{{ legendData.data[legendKey.dataIndex].min }}</td> <td class="tb-legend-value" *ngIf="legendConfig.showMin === true"
<td class="tb-legend-value" *ngIf="legendConfig.showMax === true">{{ legendData.data[legendKey.dataIndex].max }}</td> [innerHTML]="legendData.data[legendKey.dataIndex].min | safe: 'html'"></td>
<td class="tb-legend-value" *ngIf="legendConfig.showAvg === true">{{ legendData.data[legendKey.dataIndex].avg }}</td> <td class="tb-legend-value" *ngIf="legendConfig.showMax === true"
<td class="tb-legend-value" *ngIf="legendConfig.showTotal === true">{{ legendData.data[legendKey.dataIndex].total }}</td> [innerHTML]="legendData.data[legendKey.dataIndex].max | safe: 'html'"></td>
<td class="tb-legend-value" *ngIf="legendConfig.showLatest === true">{{ legendData.data[legendKey.dataIndex].latest }}</td> <td class="tb-legend-value" *ngIf="legendConfig.showAvg === true"
[innerHTML]="legendData.data[legendKey.dataIndex].avg | safe: 'html'"></td>
<td class="tb-legend-value" *ngIf="legendConfig.showTotal === true"
[innerHTML]="legendData.data[legendKey.dataIndex].total | safe: 'html'"></td>
<td class="tb-legend-value" *ngIf="legendConfig.showLatest === true"
[innerHTML]="legendData.data[legendKey.dataIndex].latest | safe: 'html'"></td>
</tr> </tr>
</ng-container> </ng-container>
<tr *ngIf="isRowDirection" class="tb-legend-keys" [ngClass]="{ 'tb-row-direction': !displayHeader }"> <tr *ngIf="isRowDirection" class="tb-legend-keys" [ngClass]="{ 'tb-row-direction': !displayHeader }">
@ -47,41 +52,36 @@
<td *ngFor="let legendKey of legendKeys()"> <td *ngFor="let legendKey of legendKeys()">
<span class="tb-legend-line" [ngStyle]="{backgroundColor: legendKey.dataKey.color}"></span> <span class="tb-legend-line" [ngStyle]="{backgroundColor: legendKey.dataKey.color}"></span>
<span class="tb-legend-label" <span class="tb-legend-label"
[innerHTML]="legendKey.dataKey.label | safe: 'html'"
(click)="toggleHideData(legendKey.dataIndex)" (click)="toggleHideData(legendKey.dataIndex)"
[ngClass]="{ 'tb-hidden-label': legendKey.dataKey.hidden}"> [ngClass]="{ 'tb-hidden-label': legendKey.dataKey.hidden}">
{{ legendKey.dataKey.label }}
</span> </span>
</td> </td>
</tr> </tr>
<tr class="tb-legend-keys" *ngIf="isRowDirection && legendConfig.showMin === true"> <tr class="tb-legend-keys" *ngIf="isRowDirection && legendConfig.showMin === true">
<td class="tb-legend-type">{{ 'legend.min' | translate }}</td> <td class="tb-legend-type">{{ 'legend.min' | translate }}</td>
<td class="tb-legend-value" *ngFor="let legendKey of legendKeys()"> <td class="tb-legend-value" *ngFor="let legendKey of legendKeys()"
{{ legendData.data[legendKey.dataIndex].min }} [innerHTML]="legendData.data[legendKey.dataIndex].min | safe: 'html'"></td>
</td>
</tr> </tr>
<tr class="tb-legend-keys" *ngIf="isRowDirection && legendConfig.showMax === true"> <tr class="tb-legend-keys" *ngIf="isRowDirection && legendConfig.showMax === true">
<td class="tb-legend-type">{{ 'legend.max' | translate }}</td> <td class="tb-legend-type">{{ 'legend.max' | translate }}</td>
<td class="tb-legend-value" *ngFor="let legendKey of legendKeys()"> <td class="tb-legend-value" *ngFor="let legendKey of legendKeys()"
{{ legendData.data[legendKey.dataIndex].max }} [innerHTML]="legendData.data[legendKey.dataIndex].max | safe: 'html'"></td>
</td>
</tr> </tr>
<tr class="tb-legend-keys" *ngIf="isRowDirection && legendConfig.showAvg === true"> <tr class="tb-legend-keys" *ngIf="isRowDirection && legendConfig.showAvg === true">
<td class="tb-legend-type">{{ 'legend.avg' | translate }}</td> <td class="tb-legend-type">{{ 'legend.avg' | translate }}</td>
<td class="tb-legend-value" *ngFor="let legendKey of legendKeys()"> <td class="tb-legend-value" *ngFor="let legendKey of legendKeys()"
{{ legendData.data[legendKey.dataIndex].avg }} [innerHTML]="legendData.data[legendKey.dataIndex].avg | safe: 'html'"></td>
</td>
</tr> </tr>
<tr class="tb-legend-keys" *ngIf="isRowDirection && legendConfig.showTotal === true"> <tr class="tb-legend-keys" *ngIf="isRowDirection && legendConfig.showTotal === true">
<td class="tb-legend-type">{{ 'legend.total' | translate }}</td> <td class="tb-legend-type">{{ 'legend.total' | translate }}</td>
<td class="tb-legend-value" *ngFor="let legendKey of legendKeys()"> <td class="tb-legend-value" *ngFor="let legendKey of legendKeys()"
{{ legendData.data[legendKey.dataIndex].total }} [innerHTML]="legendData.data[legendKey.dataIndex].total | safe: 'html'"></td>
</td>
</tr> </tr>
<tr class="tb-legend-keys" *ngIf="isRowDirection && legendConfig.showLatest === true"> <tr class="tb-legend-keys" *ngIf="isRowDirection && legendConfig.showLatest === true">
<td class="tb-legend-type">{{ 'legend.latest' | translate }}</td> <td class="tb-legend-type">{{ 'legend.latest' | translate }}</td>
<td class="tb-legend-value" *ngFor="let legendKey of legendKeys()"> <td class="tb-legend-value" *ngFor="let legendKey of legendKeys()"
{{ legendData.data[legendKey.dataIndex].latest }} [innerHTML]="legendData.data[legendKey.dataIndex].latest | safe: 'html'"></td>
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

16
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-settings-panel.component.html

@ -98,10 +98,10 @@
<input matInput type="number" formControlName="lineWidth" min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}"/> <input matInput type="number" formControlName="lineWidth" min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}"/>
</mat-form-field> </mat-form-field>
</div> </div>
<div class="tb-form-row space-between column-xs"> <div class="tb-form-row column-xs">
<div translate>widgets.time-series-chart.threshold.start-symbol</div> <div translate>widgets.time-series-chart.threshold.start-symbol</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> <div class="tb-flex row flex-end">
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="medium-width" fxFlex.lt-md appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="startSymbol"> <mat-select formControlName="startSymbol">
<mat-option *ngFor="let shape of chartShapes" [value]="shape"> <mat-option *ngFor="let shape of chartShapes" [value]="shape">
{{ chartShapeTranslations.get(shape) | translate }} {{ chartShapeTranslations.get(shape) | translate }}
@ -109,16 +109,16 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<div class="tb-small-label" translate>widgets.time-series-chart.threshold.symbol-size</div> <div class="tb-small-label" translate>widgets.time-series-chart.threshold.symbol-size</div>
<mat-form-field appearance="outline" class="number flex" subscriptSizing="dynamic"> <mat-form-field appearance="outline" class="number fixed-width" subscriptSizing="dynamic">
<input matInput formControlName="startSymbolSize" <input matInput formControlName="startSymbolSize"
type="number" min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}"> type="number" min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>
</div> </div>
</div> </div>
<div class="tb-form-row space-between column-xs"> <div class="tb-form-row column-xs">
<div translate>widgets.time-series-chart.threshold.end-symbol</div> <div translate>widgets.time-series-chart.threshold.end-symbol</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> <div class="tb-flex row flex-end">
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="medium-width" fxFlex.lt-md appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="endSymbol"> <mat-select formControlName="endSymbol">
<mat-option *ngFor="let shape of chartShapes" [value]="shape"> <mat-option *ngFor="let shape of chartShapes" [value]="shape">
{{ chartShapeTranslations.get(shape) | translate }} {{ chartShapeTranslations.get(shape) | translate }}
@ -126,7 +126,7 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<div class="tb-small-label" translate>widgets.time-series-chart.threshold.symbol-size</div> <div class="tb-small-label" translate>widgets.time-series-chart.threshold.symbol-size</div>
<mat-form-field appearance="outline" class="number flex" subscriptSizing="dynamic"> <mat-form-field appearance="outline" class="number fixed-width" subscriptSizing="dynamic">
<input matInput formControlName="endSymbolSize" <input matInput formControlName="endSymbolSize"
type="number" min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}"> type="number" min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-settings-panel.component.scss

@ -16,7 +16,7 @@
@import '../../../../../../../../../scss/constants'; @import '../../../../../../../../../scss/constants';
.tb-threshold-settings-panel { .tb-threshold-settings-panel {
width: 530px; width: 540px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px; gap: 16px;

8
ui-ngx/src/app/shared/components/color-input.component.ts

@ -189,13 +189,13 @@ export class ColorInputComponent extends PageComponent implements OnInit, Contro
this.popoverService.hidePopover(trigger); this.popoverService.hidePopover(trigger);
} else { } else {
const colorPickerPopover = this.popoverService.displayPopover(trigger, this.renderer, const colorPickerPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, ColorPickerPanelComponent, ['leftTopOnly', 'leftOnly', 'leftBottomOnly'], true, null, this.viewContainerRef, ColorPickerPanelComponent, ['left'], true, null,
{ {
color: this.colorFormGroup.get('color').value, color: this.colorFormGroup.get('color').value,
colorClearButton: this.colorClearButton colorClearButton: this.colorClearButton,
colorCancelButton: true
}, },
{}, {}, {}, {}, false, () => {}, {padding: '12px 4px 12px 12px'});
{}, {}, true);
colorPickerPopover.tbComponentRef.instance.popover = colorPickerPopover; colorPickerPopover.tbComponentRef.instance.popover = colorPickerPopover;
colorPickerPopover.tbComponentRef.instance.colorSelected.subscribe((color) => { colorPickerPopover.tbComponentRef.instance.colorSelected.subscribe((color) => {
colorPickerPopover.hide(); colorPickerPopover.hide();

26
ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.html

@ -16,8 +16,7 @@
--> -->
<div class="tb-color-picker-panel"> <div class="tb-color-picker-panel">
<div class="tb-color-picker-title" translate>color.color</div> <tb-color-picker class="tb-color-picker" [formControl]="colorPickerControl"></tb-color-picker>
<tb-color-picker [formControl]="colorPickerControl"></tb-color-picker>
<div class="tb-color-picker-panel-buttons"> <div class="tb-color-picker-panel-buttons">
<button *ngIf="colorClearButton" <button *ngIf="colorClearButton"
mat-button mat-button
@ -27,12 +26,21 @@
[disabled]="!colorPickerControl.value"> [disabled]="!colorPickerControl.value">
{{ 'action.clear' | translate }} {{ 'action.clear' | translate }}
</button> </button>
<button mat-raised-button <span fxFlex></span>
color="primary" <div fxLayoutGap="8px">
type="button" <button *ngIf="colorCancelButton"
(click)="selectColor()" mat-button
[disabled]="colorPickerControl.invalid || !colorPickerControl.dirty"> type="button"
{{ 'action.select' | translate }} (click)="cancelColor()">
</button> {{ 'action.cancel' | translate }}
</button>
<button mat-raised-button
color="primary"
type="button"
(click)="selectColor()"
[disabled]="colorPickerControl.invalid || !colorPickerControl.dirty">
{{ 'action.select' | translate }}
</button>
</div>
</div> </div>
</div> </div>

17
ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.scss

@ -13,11 +13,17 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
@import "../scss/constants";
.tb-color-picker-panel { .tb-color-picker-panel {
width: 328px; width: 342px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px; max-height: calc(100vh - 24px);
min-height: 100%;
@media #{$mat-sm} {
width: 578px;
}
.tb-color-picker-title { .tb-color-picker-title {
font-size: 16px; font-size: 16px;
font-weight: 500; font-weight: 500;
@ -25,12 +31,17 @@
letter-spacing: 0.25px; letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.87); color: rgba(0, 0, 0, 0.87);
} }
.tb-color-picker {
padding-right: 4px;
overflow-y: scroll;
}
.tb-color-picker-panel-buttons { .tb-color-picker-panel-buttons {
height: 60px; height: 60px;
padding: 8px 8px 0 0;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: 16px; gap: 16px;
justify-content: flex-end; justify-content: space-between;
align-items: flex-end; align-items: flex-end;
} }
} }

16
ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.ts

@ -38,12 +38,19 @@ export class ColorPickerPanelComponent extends PageComponent implements OnInit {
@coerceBoolean() @coerceBoolean()
colorClearButton = false; colorClearButton = false;
@Input()
@coerceBoolean()
colorCancelButton = false;
@Input() @Input()
popover: TbPopoverComponent<ColorPickerPanelComponent>; popover: TbPopoverComponent<ColorPickerPanelComponent>;
@Output() @Output()
colorSelected = new EventEmitter<string>(); colorSelected = new EventEmitter<string>();
@Output()
colorCancelDialog = new EventEmitter();
colorPickerControl: UntypedFormControl; colorPickerControl: UntypedFormControl;
constructor(protected store: Store<AppState>) { constructor(protected store: Store<AppState>) {
@ -61,4 +68,11 @@ export class ColorPickerPanelComponent extends PageComponent implements OnInit {
clearColor() { clearColor() {
this.colorSelected.emit(null); this.colorSelected.emit(null);
} }
}
cancelColor() {
if (this.popover) {
this.popover.hide();
} else {
this.colorCancelDialog.emit();
}
}}

57
ui-ngx/src/app/shared/components/color-picker/color-picker.component.html

@ -15,35 +15,38 @@
limitations under the License. limitations under the License.
--> -->
<saturation-component class="saturation-component" [hue]="control.hue" [(color)]="control.value"></saturation-component> <div class="color-input-container">
<saturation-component class="saturation-component" [hue]="control.hue" [(color)]="control.value"></saturation-component>
<div class="control-component">
<indicator-component class="indicator-component" <div class="control-input-container">
[colorType]="presentations[presentationControl.value]" <div class="control-component">
[color]="control.value"> <indicator-component class="indicator-component"
</indicator-component> [colorType]="presentations[presentationControl.value]"
<div class="hue-alpha-range"> [color]="control.value">
<hue-component [(hue)]="control.hue" [(color)]="control.value"></hue-component> </indicator-component>
<alpha-component [(color)]="control.value"></alpha-component> <div class="hue-alpha-range">
</div> <hue-component [(hue)]="control.hue" [(color)]="control.value"></hue-component>
</div> <alpha-component [(color)]="control.value"></alpha-component>
</div>
<div class="color-input-block"> </div>
<mat-select class="presentation-select" [formControl]="presentationControl">
<mat-option [value]="0">HEX</mat-option> <div class="color-input-block">
<mat-option [value]="1">RGBA</mat-option> <mat-select class="presentation-select" [formControl]="presentationControl">
<mat-option [value]="2">HSLA</mat-option> <mat-option [value]="0">HEX</mat-option>
</mat-select> <mat-option [value]="1">RGBA</mat-option>
<div class="color-input" [ngSwitch]="presentations[presentationControl.value]"> <mat-option [value]="2">HSLA</mat-option>
<rgba-input-component *ngSwitchCase="'rgba'" label </mat-select>
[(color)]="control.value" [(hue)]="control.hue"></rgba-input-component> <div class="color-input" [ngSwitch]="presentations[presentationControl.value]">
<hsla-input-component *ngSwitchCase="'hsla'" label <rgba-input-component *ngSwitchCase="'rgba'" label
[(color)]="control.value" [(hue)]="control.hue"></hsla-input-component> [(color)]="control.value" [(hue)]="control.hue"></rgba-input-component>
<hex-input-component *ngSwitchCase="'hex'" label prefix="#" [(color)]="control.value" <hsla-input-component *ngSwitchCase="'hsla'" label
[(hue)]="control.hue"></hex-input-component> [(color)]="control.value" [(hue)]="control.hue"></hsla-input-component>
<tb-hex-input *ngSwitchCase="'hex'" [(color)]="control.value"></tb-hex-input>
</div>
</div>
</div> </div>
</div> </div>
<div class="color-presets-block"> <div class="color-presets-block">
<color-presets-component class="color-presets-component" columns="11" [colorPresets]="colorPresets" [(color)]="control.value" [(hue)]="control.hue"></color-presets-component> <color-presets-component class="color-presets-component" [columns]="6" [colorPresets]="colorPresets" [(color)]="control.value" [(hue)]="control.hue"></color-presets-component>
</div> </div>

49
ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss

@ -13,6 +13,8 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
@import "../scss/constants";
:host { :host {
width: 100%; width: 100%;
display: flex; display: flex;
@ -20,10 +22,34 @@
gap: 32px; gap: 32px;
overflow: auto; overflow: auto;
.color-input-container {
display: flex;
flex-direction: column;
gap: 32px;
@media #{$mat-sm} {
flex-direction: row;
gap: 12px;
}
}
.control-input-container {
display: flex;
flex-direction: column;
gap: 32px;
@media #{$mat-sm} {
width: 100%;
justify-content: center;
}
}
.saturation-component { .saturation-component {
width: 100%;
height: 238px; height: 238px;
min-height: 80px; min-height: 160px;
border-radius: 8px; border-radius: 8px;
@media #{$mat-sm} {
max-height: 160px;
}
} }
.control-component { .control-component {
@ -54,7 +80,9 @@
} }
.color-input-block { .color-input-block {
height: 56px;
display: flex; display: flex;
align-items: center;
gap: 20px; gap: 20px;
.presentation-select { .presentation-select {
@ -74,8 +102,13 @@
.color-presets-block { .color-presets-block {
.color-presets-component { .color-presets-component {
display: flex; display: flex;
flex-direction: column; flex-direction: row;
gap: 12px; flex-wrap: wrap;
justify-content: space-between;
gap: 8px;
@media #{$mat-xs} {
flex-direction: column;
}
} }
} }
} }
@ -111,13 +144,21 @@
.color-presets-component { .color-presets-component {
.presets-row { .presets-row {
gap: 10px; gap: 8px;
justify-content: space-between; justify-content: space-between;
} }
color-preset { color-preset {
height: 20px; height: 20px;
width: 20px; width: 20px;
border-radius: 4px; border-radius: 4px;
@media #{$mat-xs} {
height: 40px;
width: 48px;
}
@media #{$mat-sm} {
height: 40px;
width: 40px;
}
} }
} }
} }

4
ui-ngx/src/app/shared/components/color-picker/color-picker.component.ts

@ -31,8 +31,8 @@ export enum ColorType {
} }
const colorPresetsHex = const colorPresetsHex =
['#435B63', '#F44336', '#E89623', '#F5DD00', '#8BC34A', '#4CAF50', '#009688', '#048AD3', '#673AB7', '#9C27B0', '#E91E63', ['#435B63', '#F44336', '#E89623', '#F5DD00', '#8BC34A', '#4CAF50', '#009688', '#048AD3', '#673AB7', '#9C27B0', '#E91E63', '#6F113A',
'#A1ADB1', '#F9A19B', '#FFD190', '#FFF59D', '#C5E1A4', '#A5D7A7', '#80CBC3', '#81C4E9', '#B39CDB', '#CD93D7', '#F48FB1']; '#A1ADB1', '#F9A19B', '#FFD190', '#FFF59D', '#C5E1A4', '#A5D7A7', '#80CBC3', '#81C4E9', '#B39CDB', '#CD93D7', '#F48FB1', '#BC91A4'];
@Component({ @Component({
selector: `tb-color-picker`, selector: `tb-color-picker`,

36
ui-ngx/src/app/shared/components/color-picker/hex-input.component.html

@ -0,0 +1,36 @@
<!--
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.
-->
<div class="hex-input-container">
<mat-form-field class="hex-input" subscriptSizing="dynamic">
<span matPrefix>{{prefixValue}}</span>
<input matInput #elRef type="text" [value]="value" (keyup)="onInputChange($event, elRef.value)">
<tb-copy-button
class="copy-button"
matSuffix
miniButton="false"
[copyText]="copyColor"
tooltipText="{{ 'action.copy' | translate }}"
tooltipPosition="above"
icon="content_copy">
</tb-copy-button>
</mat-form-field>
<mat-form-field class="alpha-input" subscriptSizing="dynamic">
<input matInput #hue min="0" max="100" [value]="hueValue" (keyup)="onHueInputChange($event, hue.value)">
<span matSuffix>%</span>
</mat-form-field>
</div>

48
ui-ngx/src/app/shared/components/color-picker/hex-input.component.scss

@ -0,0 +1,48 @@
/**
* 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.
*/
:host {
.hex-input-container {
display: flex;
gap: 8px;
}
.hex-input {
max-width: 190px;
}
.alpha-input {
min-width: 60px;
max-width: 60px;
}
::ng-deep {
.mdc-text-field--filled, .mat-mdc-form-field-focus-overlay {
&:before {
background-color: transparent !important;
}
}
.mat-mdc-form-field-icon-prefix, .mdc-line-ripple, .copy-button {
opacity: 0.4;
}
.alpha-input {
.mat-mdc-text-field-wrapper {
padding-left: 0;
}
}
}
}

75
ui-ngx/src/app/shared/components/color-picker/hex-input.component.ts

@ -0,0 +1,75 @@
///
/// 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 { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { Color } from '@iplab/ngx-color-picker';
@Component({
selector: `tb-hex-input`,
templateUrl: `./hex-input.component.html`,
styleUrls: ['./hex-input.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class HexInputComponent {
@Input()
public color: Color;
@Output()
public colorChange = new EventEmitter<Color>(false);
@Input()
public labelVisible = false;
@Input()
public prefixValue = '#';
public get value() {
return this.color ? this.color.toHexString(this.color.getRgba().alpha < 1).replace('#', '') : '';
}
public get copyColor() {
return this.prefixValue + this.value;
}
public get hueValue(): string {
return this.color ? Math.round(this.color.getRgba().alpha * 100).toString() : '';
}
public onHueInputChange(event: KeyboardEvent, inputValue: string): void {
const color = this.color.getRgba();
const alpha = +inputValue / 100;
if (color.getAlpha() !== alpha) {
const newColor = new Color().setRgba(color.red, color.green, color.blue, alpha).toHexString(true);
this.colorChange.emit(new Color(newColor));
}
}
public onInputChange(event: KeyboardEvent, inputValue: string): void {
const value = inputValue.toLowerCase();
if (
((event.keyCode === 13 || event.key.toLowerCase() === 'enter') && value.length === 3)
|| value.length === 6 || value.length === 8
) {
const hex = parseInt(value, 16);
const hexStr = hex.toString(16);
if (hexStr.padStart(value.length, '0') === value && this.value !== value) {
const newColor = new Color(`#${value}`);
this.colorChange.emit(newColor);
}
}
}
}

8
ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.html

@ -16,14 +16,10 @@
--> -->
<div mat-dialog-content> <div mat-dialog-content>
<button class="tb-close-button"
mat-icon-button
(click)="cancel()"
type="button">
<mat-icon>close</mat-icon>
</button>
<tb-color-picker-panel [color]="color" <tb-color-picker-panel [color]="color"
colorCancelButton
[colorClearButton]="colorClearButton" [colorClearButton]="colorClearButton"
(colorCancelDialog)="cancel()"
(colorSelected)="selectColor($event)"> (colorSelected)="selectColor($event)">
</tb-color-picker-panel> </tb-color-picker-panel>
</div> </div>

28
ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.scss

@ -13,10 +13,38 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
@import "../scss/constants";
:host { :host {
.tb-close-button { .tb-close-button {
position: absolute; position: absolute;
top: 6px; top: 6px;
right: 6px; right: 6px;
} }
::ng-deep {
.mat-mdc-dialog-content {
max-height: 100%;
overflow: hidden;
padding: 12px 4px 12px 12px !important;
}
.tb-color-picker-panel {
@media #{$mat-sm} {
width: 342px;
.color-input-container {
flex-direction: column !important;
}
.color-presets-component color-preset {
width: 48px;
}
}
@media #{$mat-xs} {
width: 100%;
.hex-input {
flex: 1;
max-width: 100% !important;
}
}
}
}
} }

3
ui-ngx/src/app/shared/components/popover.component.ts

@ -340,7 +340,7 @@ export class TbPopoverDirective implements OnChanges, OnDestroy, AfterViewInit {
<div class="tb-popover-inner" [ngStyle]="tbPopoverInnerStyle" role="tooltip"> <div class="tb-popover-inner" [ngStyle]="tbPopoverInnerStyle" role="tooltip">
<div *ngIf="tbShowCloseButton" class="tb-popover-close-button" (click)="closeButtonClick($event)">×</div> <div *ngIf="tbShowCloseButton" class="tb-popover-close-button" (click)="closeButtonClick($event)">×</div>
<div style="width: 100%; height: 100%;"> <div style="width: 100%; height: 100%;">
<div class="tb-popover-inner-content" <div class="tb-popover-inner-content" [ngStyle]="tbPopoverInnerContentStyle"
[class.strict-position]="strictPosition"> [class.strict-position]="strictPosition">
<ng-container *ngIf="tbContent"> <ng-container *ngIf="tbContent">
<ng-container *tbStringTemplateOutlet="tbContent; context: tbComponentContext"> <ng-container *tbStringTemplateOutlet="tbContent; context: tbComponentContext">
@ -378,6 +378,7 @@ export class TbPopoverComponent<T = any> implements OnDestroy, OnInit {
tbOverlayClassName!: string; tbOverlayClassName!: string;
tbOverlayStyle: { [klass: string]: any } = {}; tbOverlayStyle: { [klass: string]: any } = {};
tbPopoverInnerStyle: { [klass: string]: any } = {}; tbPopoverInnerStyle: { [klass: string]: any } = {};
tbPopoverInnerContentStyle: { [klass: string]: any } = {};
tbBackdrop = false; tbBackdrop = false;
tbMouseEnterDelay?: number; tbMouseEnterDelay?: number;
tbMouseLeaveDelay?: number; tbMouseLeaveDelay?: number;

9
ui-ngx/src/app/shared/components/popover.service.ts

@ -71,17 +71,19 @@ export class TbPopoverService {
componentType: Type<T>, preferredPlacement: PopoverPreferredPlacement = 'top', componentType: Type<T>, preferredPlacement: PopoverPreferredPlacement = 'top',
hideOnClickOutside = true, injector?: Injector, context?: any, overlayStyle: any = {}, hideOnClickOutside = true, injector?: Injector, context?: any, overlayStyle: any = {},
popoverStyle: any = {}, style?: any, popoverStyle: any = {}, style?: any,
showCloseButton = true, visibleFn: (visible: boolean) => void = () => {}): TbPopoverComponent<T> { showCloseButton = true, visibleFn: (visible: boolean) => void = () => {},
popoverContentStyle: any = {}): TbPopoverComponent<T> {
const componentRef = this.createPopoverRef(hostView); const componentRef = this.createPopoverRef(hostView);
return this.displayPopoverWithComponentRef(componentRef, trigger, renderer, componentType, preferredPlacement, hideOnClickOutside, return this.displayPopoverWithComponentRef(componentRef, trigger, renderer, componentType, preferredPlacement, hideOnClickOutside,
injector, context, overlayStyle, popoverStyle, style, showCloseButton, visibleFn); injector, context, overlayStyle, popoverStyle, style, showCloseButton, visibleFn, popoverContentStyle);
} }
displayPopoverWithComponentRef<T>(componentRef: ComponentRef<TbPopoverComponent>, trigger: Element, renderer: Renderer2, displayPopoverWithComponentRef<T>(componentRef: ComponentRef<TbPopoverComponent>, trigger: Element, renderer: Renderer2,
componentType: Type<T>, preferredPlacement: PopoverPreferredPlacement = 'top', componentType: Type<T>, preferredPlacement: PopoverPreferredPlacement = 'top',
hideOnClickOutside = true, injector?: Injector, context?: any, overlayStyle: any = {}, hideOnClickOutside = true, injector?: Injector, context?: any, overlayStyle: any = {},
popoverStyle: any = {}, style?: any, showCloseButton = true, popoverStyle: any = {}, style?: any, showCloseButton = true,
visibleFn: (visible: boolean) => void = () => {}): TbPopoverComponent<T> { visibleFn: (visible: boolean) => void = () => {},
popoverContentStyle: any = {}): TbPopoverComponent<T> {
const component = componentRef.instance; const component = componentRef.instance;
this.popoverWithTriggers.push({ this.popoverWithTriggers.push({
trigger, trigger,
@ -99,6 +101,7 @@ export class TbPopoverService {
component.tbComponentContext = context; component.tbComponentContext = context;
component.tbOverlayStyle = overlayStyle; component.tbOverlayStyle = overlayStyle;
component.tbPopoverInnerStyle = popoverStyle; component.tbPopoverInnerStyle = popoverStyle;
component.tbPopoverInnerContentStyle = popoverContentStyle;
component.tbComponentStyle = style; component.tbComponentStyle = style;
component.tbHideOnClickOutside = hideOnClickOutside; component.tbHideOnClickOutside = hideOnClickOutside;
component.tbShowCloseButton = showCloseButton; component.tbShowCloseButton = showCloseButton;

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

@ -59,6 +59,7 @@ export interface GridSettings {
autoFillHeight?: boolean; autoFillHeight?: boolean;
mobileAutoFillHeight?: boolean; mobileAutoFillHeight?: boolean;
mobileRowHeight?: number; mobileRowHeight?: number;
mobileDisplayLayoutFirst?: boolean;
layoutDimension?: LayoutDimension; layoutDimension?: LayoutDimension;
[key: string]: any; [key: string]: any;
} }

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

@ -178,15 +178,15 @@ import { TemplateAutocompleteComponent } from '@shared/components/notification/t
import { SlackConversationAutocompleteComponent } from '@shared/components/slack-conversation-autocomplete.component'; import { SlackConversationAutocompleteComponent } from '@shared/components/slack-conversation-autocomplete.component';
import { DateAgoPipe } from '@shared/pipe/date-ago.pipe'; import { DateAgoPipe } from '@shared/pipe/date-ago.pipe';
import { import {
TbBreakPointsProvider, GtMdLgLayoutAlignDirective,
MdLgLayoutDirective, GtMdLgLayoutDirective,
GtMdLgLayoutGapDirective,
GtMdLgShowHideDirective,
MdLgLayoutAlignDirective, MdLgLayoutAlignDirective,
MdLgLayoutDirective,
MdLgLayoutGapDirective, MdLgLayoutGapDirective,
MdLgShowHideDirective, MdLgShowHideDirective,
GtMdLgLayoutDirective, TbBreakPointsProvider
GtMdLgLayoutAlignDirective,
GtMdLgLayoutGapDirective,
GtMdLgShowHideDirective
} from '@shared/layout/layout.directives'; } from '@shared/layout/layout.directives';
import { ColorPickerComponent } from '@shared/components/color-picker/color-picker.component'; import { ColorPickerComponent } from '@shared/components/color-picker/color-picker.component';
import { ResourceAutocompleteComponent } from '@shared/components/resource/resource-autocomplete.component'; import { ResourceAutocompleteComponent } from '@shared/components/resource/resource-autocomplete.component';
@ -218,6 +218,7 @@ import { EmbedImageDialogComponent } from '@shared/components/image/embed-image-
import { ImageGalleryDialogComponent } from '@shared/components/image/image-gallery-dialog.component'; import { ImageGalleryDialogComponent } from '@shared/components/image/image-gallery-dialog.component';
import { RuleChainSelectPanelComponent } from '@shared/components/rule-chain/rule-chain-select-panel.component'; import { RuleChainSelectPanelComponent } from '@shared/components/rule-chain/rule-chain-select-panel.component';
import { WidgetButtonComponent } from '@shared/components/button/widget-button.component'; import { WidgetButtonComponent } from '@shared/components/button/widget-button.component';
import { HexInputComponent } from '@shared/components/color-picker/hex-input.component';
export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
return markedOptionsService; return markedOptionsService;
@ -416,7 +417,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
MultipleGalleryImageInputComponent, MultipleGalleryImageInputComponent,
EmbedImageDialogComponent, EmbedImageDialogComponent,
ImageGalleryDialogComponent, ImageGalleryDialogComponent,
WidgetButtonComponent WidgetButtonComponent,
HexInputComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,

3
ui-ngx/src/assets/locale/locale.constant-ca_ES.json

@ -1710,8 +1710,7 @@
"step": "Pas", "step": "Pas",
"min-evaluation-period": "Període mínim d'avaluació", "min-evaluation-period": "Període mínim d'avaluació",
"max-evaluation-period": "Període màxim d'avaluació" "max-evaluation-period": "Període màxim d'avaluació"
}, }
"composite-operations-support": "Admet operacions compostes de lectura/escriptura/observació"
}, },
"snmp": { "snmp": {
"add-communication-config": "Afegeix la configuració de comunicació", "add-communication-config": "Afegeix la configuració de comunicació",

3
ui-ngx/src/assets/locale/locale.constant-cs_CZ.json

@ -1301,8 +1301,7 @@
"step": "Krok", "step": "Krok",
"min-evaluation-period": "Minimální interval evaluace", "min-evaluation-period": "Minimální interval evaluace",
"max-evaluation-period": "Maximální interval evaluace" "max-evaluation-period": "Maximální interval evaluace"
}, }
"composite-operations-support": "Podporuje kompozitní Read/Write/Observe operace"
}, },
"snmp": { "snmp": {
"add-communication-config": "Přidat konfiguraci komunikace", "add-communication-config": "Přidat konfiguraci komunikace",

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

@ -1147,6 +1147,7 @@
"mobile-row-height-required": "Mobile row height value is required.", "mobile-row-height-required": "Mobile row height value is required.",
"min-mobile-row-height-message": "Only 5 pixels is allowed as minimum mobile row height value.", "min-mobile-row-height-message": "Only 5 pixels is allowed as minimum mobile row height value.",
"max-mobile-row-height-message": "Only 200 pixels is allowed as maximum mobile row height value.", "max-mobile-row-height-message": "Only 200 pixels is allowed as maximum mobile row height value.",
"display-first-in-mobile-view": "Display first in mobile view",
"title-settings": "Title settings", "title-settings": "Title settings",
"display-title": "Display dashboard title", "display-title": "Display dashboard title",
"title-color": "Title color", "title-color": "Title color",
@ -1898,8 +1899,7 @@
"step": "Step", "step": "Step",
"min-evaluation-period": "Minimum evaluation period", "min-evaluation-period": "Minimum evaluation period",
"max-evaluation-period": "Maximum evaluation period" "max-evaluation-period": "Maximum evaluation period"
}, }
"composite-operations-support": "Supports composite Read/Write/Observe operations"
}, },
"snmp": { "snmp": {
"add-communication-config": "Add communication config", "add-communication-config": "Add communication config",
@ -2729,6 +2729,8 @@
"connectors-table-actions": "Actions", "connectors-table-actions": "Actions",
"connectors-table-key": "Key", "connectors-table-key": "Key",
"connectors-table-class": "Class", "connectors-table-class": "Class",
"connector-created": "Connector created.",
"connector-updated": "Connector updated.",
"rpc-command-save-template": "Save Template", "rpc-command-save-template": "Save Template",
"rpc-command-send": "Send", "rpc-command-send": "Send",
"rpc-command-result": "Response", "rpc-command-result": "Response",

3
ui-ngx/src/assets/locale/locale.constant-es_ES.json

@ -1835,8 +1835,7 @@
"step": "Paso", "step": "Paso",
"min-evaluation-period": "Período mínimo de evaluación", "min-evaluation-period": "Período mínimo de evaluación",
"max-evaluation-period": "Período máximo de evaluación" "max-evaluation-period": "Período máximo de evaluación"
}, }
"composite-operations-support": "Soporta operaciones Lectura/Escritura/Observación Compuestas"
}, },
"snmp": { "snmp": {
"add-communication-config": "Añadir configuración de comunicaciones", "add-communication-config": "Añadir configuración de comunicaciones",

3
ui-ngx/src/assets/locale/locale.constant-nl_BE.json

@ -1935,8 +1935,7 @@
"step": "Stap", "step": "Stap",
"min-evaluation-period": "Minimale evaluatieperiode", "min-evaluation-period": "Minimale evaluatieperiode",
"max-evaluation-period": "Maximale evaluatieperiode" "max-evaluation-period": "Maximale evaluatieperiode"
}, }
"composite-operations-support": "Ondersteunt samengestelde lees-/schrijf-/observatiebewerkingen"
}, },
"snmp": { "snmp": {
"add-communication-config": "Communicatieconfiguratie toevoegen", "add-communication-config": "Communicatieconfiguratie toevoegen",

14186
ui-ngx/src/assets/locale/locale.constant-pl_PL.json

File diff suppressed because it is too large

3
ui-ngx/src/assets/locale/locale.constant-tr_TR.json

@ -1316,8 +1316,7 @@
"step": "Adım", "step": "Adım",
"min-evaluation-period": "Minimum değerlendirme süresi", "min-evaluation-period": "Minimum değerlendirme süresi",
"max-evaluation-period": "Maksimum değerlendirme süresi" "max-evaluation-period": "Maksimum değerlendirme süresi"
}, }
"composite-operations-support": "İç içe Okuma/Yazma/Gözlemleme işlemlerini destekler"
}, },
"snmp": { "snmp": {
"add-communication-config": "İletişim yapılandırması ekle", "add-communication-config": "İletişim yapılandırması ekle",

3
ui-ngx/src/assets/locale/locale.constant-zh_CN.json

@ -1872,8 +1872,7 @@
"step": "步长", "step": "步长",
"min-evaluation-period": "最小评估周期", "min-evaluation-period": "最小评估周期",
"max-evaluation-period": "最大评估周期" "max-evaluation-period": "最大评估周期"
}, }
"composite-operations-support": "支持复合读取/写入/观察操作"
}, },
"snmp": { "snmp": {
"add-communication-config": "添加通信配置", "add-communication-config": "添加通信配置",

3
ui-ngx/src/assets/locale/locale.constant-zh_TW.json

@ -1510,8 +1510,7 @@
"step": "步", "step": "步",
"min-evaluation-period": "最小評估期間", "min-evaluation-period": "最小評估期間",
"max-evaluation-period": "最大評估期間" "max-evaluation-period": "最大評估期間"
}, }
"composite-operations-support": "支持複合讀/寫/觀察 操作"
}, },
"snmp": { "snmp": {
"add-communication-config": "新增通訊配置", "add-communication-config": "新增通訊配置",

Loading…
Cancel
Save