Browse Source

Merge remote-tracking branch 'upstream/develop/3.5.2' into feature/check-connectivity-device

pull/8938/head
Vladyslav_Prykhodko 3 years ago
parent
commit
2d2117e9ac
  1. 16
      application/src/main/data/json/system/widget_bundles/cards.json
  2. 6
      application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java
  3. 4
      application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/DefaultTbEntityRelationService.java
  4. 2
      application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/TbEntityRelationService.java
  5. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java
  6. 31
      dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java
  7. 4
      dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java
  8. 19
      dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java
  9. 5
      dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationRepository.java
  10. 25
      dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceTest.java
  11. 6
      ui-ngx/src/app/core/services/dashboard-utils.service.ts
  12. 2
      ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.scss
  13. 4
      ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.scss
  14. 1
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts
  15. 2
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.scss
  16. 46
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.html
  17. 10
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.ts
  18. 6
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html
  19. 13
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.scss
  20. 4
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html
  21. 12
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.scss
  22. 2
      ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.html
  23. 10
      ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.scss
  24. 10
      ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.ts
  25. 20
      ui-ngx/src/app/modules/home/components/widget/config/widget-settings.models.ts
  26. 4
      ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.scss
  27. 4
      ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.component.scss
  28. 2
      ui-ngx/src/app/modules/home/components/widget/lib/navigation-card-widget.component.scss
  29. 89
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-card-widget-settings.component.html
  30. 200
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-card-widget-settings.component.ts
  31. 87
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings-panel.component.html
  32. 73
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings-panel.component.scss
  33. 120
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings-panel.component.ts
  34. 30
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings.component.html
  35. 41
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings.component.scss
  36. 120
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings.component.ts
  37. 27
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.ts
  38. 20
      ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
  39. 2
      ui-ngx/src/app/modules/home/components/widget/lib/trip-animation/trip-animation.component.scss
  40. 3
      ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts
  41. 1
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.html
  42. 17
      ui-ngx/src/app/modules/home/components/widget/widget-container.component.html
  43. 47
      ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss
  44. 1
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  45. 2
      ui-ngx/src/app/modules/home/menu/side-menu.component.scss
  46. 2
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  47. 4
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.scss
  48. 2
      ui-ngx/src/app/modules/home/pages/rulechain/rulenode.component.scss
  49. 2
      ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.scss
  50. 6
      ui-ngx/src/app/shared/components/fab-toolbar.component.scss
  51. 4
      ui-ngx/src/app/shared/components/time/history-selector/history-selector.component.scss
  52. 2
      ui-ngx/src/app/shared/components/unit-input.component.html
  53. 19
      ui-ngx/src/app/shared/components/unit-input.component.ts
  54. 2
      ui-ngx/src/app/shared/components/user-menu.component.scss
  55. 6
      ui-ngx/src/app/shared/models/unit.models.ts
  56. 18
      ui-ngx/src/app/shared/models/widget.models.ts
  57. 16
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  58. 0
      ui-ngx/src/assets/metadata/units.json
  59. 25
      ui-ngx/src/styles.scss
  60. 2
      ui-ngx/src/theme.scss

16
application/src/main/data/json/system/widget_bundles/cards.json

File diff suppressed because one or more lines are too long

6
application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java

@ -117,8 +117,8 @@ public class EntityRelationController extends BaseController {
tbEntityRelationService.delete(getTenantId(), getCurrentUser().getCustomerId(), relation, getCurrentUser()); tbEntityRelationService.delete(getTenantId(), getCurrentUser().getCustomerId(), relation, getCurrentUser());
} }
@ApiOperation(value = "Delete Relations (deleteRelations)", @ApiOperation(value = "Delete common relations (deleteCommonRelations)",
notes = "Deletes all the relation (both 'from' and 'to' direction) for the specified entity. " + notes = "Deletes all the relations ('from' and 'to' direction) for the specified entity and relation type group: 'COMMON'. " +
SECURITY_CHECKS_ENTITY_DESCRIPTION) SECURITY_CHECKS_ENTITY_DESCRIPTION)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/relations", method = RequestMethod.DELETE, params = {"entityId", "entityType"}) @RequestMapping(value = "/relations", method = RequestMethod.DELETE, params = {"entityId", "entityType"})
@ -129,7 +129,7 @@ public class EntityRelationController extends BaseController {
checkParameter("entityType", strType); checkParameter("entityType", strType);
EntityId entityId = EntityIdFactory.getByTypeAndId(strType, strId); EntityId entityId = EntityIdFactory.getByTypeAndId(strType, strId);
checkEntityId(entityId, Operation.WRITE); checkEntityId(entityId, Operation.WRITE);
tbEntityRelationService.deleteRelations(getTenantId(), getCurrentUser().getCustomerId(), entityId, getCurrentUser()); tbEntityRelationService.deleteCommonRelations(getTenantId(), getCurrentUser().getCustomerId(), entityId, getCurrentUser());
} }
@ApiOperation(value = "Get Relation (getRelation)", @ApiOperation(value = "Get Relation (getRelation)",

4
application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/DefaultTbEntityRelationService.java

@ -72,9 +72,9 @@ public class DefaultTbEntityRelationService extends AbstractTbEntityService impl
} }
@Override @Override
public void deleteRelations(TenantId tenantId, CustomerId customerId, EntityId entityId, User user) throws ThingsboardException { public void deleteCommonRelations(TenantId tenantId, CustomerId customerId, EntityId entityId, User user) throws ThingsboardException {
try { try {
relationService.deleteEntityRelations(tenantId, entityId); relationService.deleteEntityCommonRelations(tenantId, entityId);
notificationEntityService.logEntityAction(tenantId, entityId, null, customerId, ActionType.RELATIONS_DELETED, user); notificationEntityService.logEntityAction(tenantId, entityId, null, customerId, ActionType.RELATIONS_DELETED, user);
} catch (Exception e) { } catch (Exception e) {
notificationEntityService.logEntityAction(tenantId, entityId, null, customerId, notificationEntityService.logEntityAction(tenantId, entityId, null, customerId,

2
application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/TbEntityRelationService.java

@ -28,6 +28,6 @@ public interface TbEntityRelationService {
void delete(TenantId tenantId, CustomerId customerId, EntityRelation entity, User user) throws ThingsboardException; void delete(TenantId tenantId, CustomerId customerId, EntityRelation entity, User user) throws ThingsboardException;
void deleteRelations(TenantId tenantId, CustomerId customerId, EntityId entityId, User user) throws ThingsboardException; void deleteCommonRelations(TenantId tenantId, CustomerId customerId, EntityId entityId, User user) throws ThingsboardException;
} }

2
common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java

@ -53,6 +53,8 @@ public interface RelationService {
void deleteEntityRelations(TenantId tenantId, EntityId entity); void deleteEntityRelations(TenantId tenantId, EntityId entity);
void deleteEntityCommonRelations(TenantId tenantId, EntityId entity);
List<EntityRelation> findByFrom(TenantId tenantId, EntityId from, RelationTypeGroup typeGroup); List<EntityRelation> findByFrom(TenantId tenantId, EntityId from, RelationTypeGroup typeGroup);
ListenableFuture<List<EntityRelation>> findByFromAsync(TenantId tenantId, EntityId from, RelationTypeGroup typeGroup); ListenableFuture<List<EntityRelation>> findByFromAsync(TenantId tenantId, EntityId from, RelationTypeGroup typeGroup);

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

@ -220,17 +220,36 @@ public class BaseRelationService implements RelationService {
return future; return future;
} }
@Transactional
@Override
public void deleteEntityCommonRelations(TenantId tenantId, EntityId entityId) {
deleteEntityRelations(tenantId, entityId, RelationTypeGroup.COMMON);
}
@Transactional @Transactional
@Override @Override
public void deleteEntityRelations(TenantId tenantId, EntityId entityId) { public void deleteEntityRelations(TenantId tenantId, EntityId entityId) {
deleteEntityRelations(tenantId, entityId, null);
}
@Transactional
public void deleteEntityRelations(TenantId tenantId, EntityId entityId, RelationTypeGroup relationTypeGroup) {
log.trace("Executing deleteEntityRelations [{}]", entityId); log.trace("Executing deleteEntityRelations [{}]", entityId);
validate(entityId); validate(entityId);
List<EntityRelation> inboundRelations = new ArrayList<>(relationDao.findAllByTo(tenantId, entityId)); List<EntityRelation> inboundRelations = relationTypeGroup == null
List<EntityRelation> outboundRelations = new ArrayList<>(relationDao.findAllByFrom(tenantId, entityId)); ? relationDao.findAllByTo(tenantId, entityId)
: relationDao.findAllByTo(tenantId, entityId, relationTypeGroup);
List<EntityRelation> outboundRelations = relationTypeGroup == null
? relationDao.findAllByFrom(tenantId, entityId)
: relationDao.findAllByFrom(tenantId, entityId, relationTypeGroup);
if (!inboundRelations.isEmpty()) { if (!inboundRelations.isEmpty()) {
try { try {
relationDao.deleteInboundRelations(tenantId, entityId); if (relationTypeGroup == null) {
relationDao.deleteInboundRelations(tenantId, entityId);
} else {
relationDao.deleteInboundRelations(tenantId, entityId, relationTypeGroup);
}
} catch (ConcurrencyFailureException e) { } catch (ConcurrencyFailureException e) {
log.debug("Concurrency exception while deleting relations [{}]", inboundRelations, e); log.debug("Concurrency exception while deleting relations [{}]", inboundRelations, e);
} }
@ -241,7 +260,11 @@ public class BaseRelationService implements RelationService {
} }
if (!outboundRelations.isEmpty()) { if (!outboundRelations.isEmpty()) {
relationDao.deleteOutboundRelations(tenantId, entityId); if (relationTypeGroup == null) {
relationDao.deleteOutboundRelations(tenantId, entityId);
} else {
relationDao.deleteOutboundRelations(tenantId, entityId, relationTypeGroup);
}
for (EntityRelation relation : outboundRelations) { for (EntityRelation relation : outboundRelations) {
eventPublisher.publishEvent(EntityRelationEvent.from(relation)); eventPublisher.publishEvent(EntityRelationEvent.from(relation));

4
dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java

@ -64,8 +64,12 @@ public interface RelationDao {
void deleteOutboundRelations(TenantId tenantId, EntityId entity); void deleteOutboundRelations(TenantId tenantId, EntityId entity);
void deleteOutboundRelations(TenantId tenantId, EntityId entity, RelationTypeGroup relationTypeGroup);
void deleteInboundRelations(TenantId tenantId, EntityId entity); void deleteInboundRelations(TenantId tenantId, EntityId entity);
void deleteInboundRelations(TenantId tenantId, EntityId entity, RelationTypeGroup relationTypeGroup);
ListenableFuture<Boolean> deleteOutboundRelationsAsync(TenantId tenantId, EntityId entity); ListenableFuture<Boolean> deleteOutboundRelationsAsync(TenantId tenantId, EntityId entity);
List<EntityRelation> findRuleNodeToRuleChainRelations(RuleChainType ruleChainType, int limit); List<EntityRelation> findRuleNodeToRuleChainRelations(RuleChainType ruleChainType, int limit);

19
dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java

@ -37,6 +37,7 @@ import org.thingsboard.server.dao.util.SqlDao;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -205,6 +206,15 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple
} }
} }
@Override
public void deleteOutboundRelations(TenantId tenantId, EntityId entity, RelationTypeGroup relationTypeGroup) {
try {
relationRepository.deleteByFromIdAndFromTypeAndRelationTypeGroupIn(entity.getId(), entity.getEntityType().name(), Collections.singletonList(relationTypeGroup.name()));
} catch (ConcurrencyFailureException e) {
log.debug("Concurrency exception while deleting relations [{}]", entity, e);
}
}
@Override @Override
public void deleteInboundRelations(TenantId tenantId, EntityId entity) { public void deleteInboundRelations(TenantId tenantId, EntityId entity) {
try { try {
@ -214,6 +224,15 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple
} }
} }
@Override
public void deleteInboundRelations(TenantId tenantId, EntityId entity, RelationTypeGroup relationTypeGroup) {
try {
relationRepository.deleteByToIdAndToTypeAndRelationTypeGroupIn(entity.getId(), entity.getEntityType().name(), Collections.singletonList(relationTypeGroup.name()));
} catch (ConcurrencyFailureException e) {
log.debug("Concurrency exception while deleting relations [{}]", entity, e);
}
}
@Override @Override
public ListenableFuture<Boolean> deleteOutboundRelationsAsync(TenantId tenantId, EntityId entity) { public ListenableFuture<Boolean> deleteOutboundRelationsAsync(TenantId tenantId, EntityId entity) {
return service.submit( return service.submit(

5
dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationRepository.java

@ -82,4 +82,9 @@ public interface RelationRepository
@Query("DELETE FROM RelationEntity r where r.toId = :toId and r.toType = :toType and r.relationTypeGroup in :relationTypeGroups") @Query("DELETE FROM RelationEntity r where r.toId = :toId and r.toType = :toType and r.relationTypeGroup in :relationTypeGroups")
void deleteByToIdAndToTypeAndRelationTypeGroupIn(@Param("toId") UUID toId, @Param("toType") String toType, @Param("relationTypeGroups") List<String> relationTypeGroups); void deleteByToIdAndToTypeAndRelationTypeGroupIn(@Param("toId") UUID toId, @Param("toType") String toType, @Param("relationTypeGroups") List<String> relationTypeGroups);
@Transactional
@Modifying
@Query("DELETE FROM RelationEntity r where r.fromId = :fromId and r.fromType = :fromType and r.relationTypeGroup in :relationTypeGroups")
void deleteByFromIdAndFromTypeAndRelationTypeGroupIn(@Param("fromId") UUID fromId, @Param("fromType") String fromType, @Param("relationTypeGroups") List<String> relationTypeGroups);
} }

25
dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceTest.java

@ -131,6 +131,31 @@ public class RelationServiceTest extends AbstractServiceTest {
Assert.assertFalse(relationService.checkRelation(SYSTEM_TENANT_ID, childId, subChildId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON)); Assert.assertFalse(relationService.checkRelation(SYSTEM_TENANT_ID, childId, subChildId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON));
} }
@Test
public void testDeleteEntityCommonRelations() {
AssetId parentId = new AssetId(Uuids.timeBased());
AssetId childId = new AssetId(Uuids.timeBased());
AssetId subChildId = new AssetId(Uuids.timeBased());
EntityRelation relationA = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE);
EntityRelation relationB = new EntityRelation(childId, subChildId, EntityRelation.CONTAINS_TYPE);
EntityRelation relationC = new EntityRelation(parentId, childId, EntityRelation.MANAGES_TYPE, RelationTypeGroup.EDGE);
EntityRelation relationD = new EntityRelation(childId, subChildId, EntityRelation.MANAGES_TYPE, RelationTypeGroup.EDGE);
saveRelation(relationA);
saveRelation(relationB);
saveRelation(relationC);
saveRelation(relationD);
relationService.deleteEntityCommonRelations(SYSTEM_TENANT_ID, childId);
Assert.assertFalse(relationService.checkRelation(SYSTEM_TENANT_ID, parentId, childId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON));
Assert.assertFalse(relationService.checkRelation(SYSTEM_TENANT_ID, childId, subChildId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON));
Assert.assertTrue(relationService.checkRelation(SYSTEM_TENANT_ID, parentId, childId, EntityRelation.MANAGES_TYPE, RelationTypeGroup.EDGE));
Assert.assertTrue(relationService.checkRelation(SYSTEM_TENANT_ID, childId, subChildId, EntityRelation.MANAGES_TYPE, RelationTypeGroup.EDGE));
}
@Test @Test
public void testFindFrom() throws ExecutionException, InterruptedException { public void testFindFrom() throws ExecutionException, InterruptedException {
AssetId parentA = new AssetId(Uuids.timeBased()); AssetId parentA = new AssetId(Uuids.timeBased());

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

@ -348,10 +348,8 @@ export class DashboardUtilsService {
private convertDatasourcesFromWidgetType(widgetTypeDescriptor: WidgetTypeDescriptor, private convertDatasourcesFromWidgetType(widgetTypeDescriptor: WidgetTypeDescriptor,
config: WidgetConfig, datasources?: Datasource[]): Datasource[] { config: WidgetConfig, datasources?: Datasource[]): Datasource[] {
const newDatasources: Datasource[] = []; const newDatasources: Datasource[] = [];
if (datasources) { if (datasources?.length) {
datasources.forEach(datasource => { newDatasources.push(this.convertDatasourceFromWidgetType(widgetTypeDescriptor, config, datasources[0]));
newDatasources.push(this.convertDatasourceFromWidgetType(widgetTypeDescriptor, config, datasource));
});
} }
return newDatasources; return newDatasources;
} }

2
ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.scss

@ -104,7 +104,7 @@
} }
mat-cell.tb-value-cell { mat-cell.tb-value-cell {
cursor: pointer; cursor: pointer;
mat-icon { .mat-icon {
height: 24px; height: 24px;
width: 24px; width: 24px;
font-size: 24px; font-size: 24px;

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

@ -21,9 +21,9 @@
position: relative; position: relative;
} }
@media #{$mat-gt-xs} { @media #{$mat-gt-xs} {
width: 1200px; width: 900px;
.mat-mdc-dialog-content { .mat-mdc-dialog-content {
height: 600px; height: 900px;
} }
} }
} }

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

@ -1177,6 +1177,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
Widget>(AddWidgetDialogComponent, { Widget>(AddWidgetDialogComponent, {
disableClose: true, disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
maxWidth: '95vw',
data: { data: {
dashboard: this.dashboard, dashboard: this.dashboard,
aliasController: this.dashboardCtx.aliasController, aliasController: this.dashboardCtx.aliasController,

2
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.scss

@ -45,6 +45,8 @@
} }
.preview { .preview {
width: 100%;
height: 100%;
max-width: 100%; max-width: 100%;
max-height: 100%; max-height: 100%;
object-fit: contain; object-fit: contain;

46
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.html

@ -55,7 +55,7 @@
</tb-color-settings> </tb-color-settings>
</div> </div>
</div> </div>
<div class="tb-form-row"> <div [fxShow]="iconEnabled" class="tb-form-row">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showIcon"> <mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showIcon">
{{ 'widgets.value-card.icon' | translate }} {{ 'widgets.value-card.icon' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
@ -87,18 +87,38 @@
</tb-color-settings> </tb-color-settings>
</div> </div>
</div> </div>
</div> <div [fxShow]="dateEnabled" class="tb-form-row">
<div class="tb-form-row"> <mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showDate">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showDate"> {{ 'widgets.value-card.date' | translate }}
{{ 'widgets.value-card.date' | translate }} </mat-slide-toggle>
</mat-slide-toggle> <div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> <tb-date-format-select fxFlex formControlName="dateFormat"></tb-date-format-select>
<tb-date-format-select fxFlex formControlName="dateFormat"></tb-date-format-select> <tb-font-settings formControlName="dateFont"
<tb-font-settings formControlName="dateFont" [previewText]="datePreviewFn">
[previewText]="datePreviewFn"> </tb-font-settings>
</tb-font-settings> <tb-color-settings formControlName="dateColor">
<tb-color-settings formControlName="dateColor"> </tb-color-settings>
</tb-color-settings> </div>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.background.background' | translate }}</div>
<tb-background-settings formControlName="background">
</tb-background-settings>
</div>
<div class="tb-form-row space-between">
<div translate>widget-config.show-card-buttons</div>
<mat-chip-listbox multiple formControlName="cardButtons">
<mat-chip-option value="fullscreen">{{ 'fullscreen.fullscreen' | translate }}</mat-chip-option>
</mat-chip-listbox>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widget-config.card-border-radius' | translate }}</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="borderRadius" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div> </div>
</div> </div>
<tb-widget-actions-panel
formControlName="actions">
</tb-widget-actions-panel>
</ng-container> </ng-container>

10
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.ts

@ -74,6 +74,16 @@ export class ValueCardBasicConfigComponent extends BasicWidgetConfigComponent {
datePreviewFn = this._datePreviewFn.bind(this); datePreviewFn = this._datePreviewFn.bind(this);
get dateEnabled(): boolean {
const layout: ValueCardLayout = this.valueCardWidgetConfigForm.get('layout').value;
return ![ValueCardLayout.vertical, ValueCardLayout.simplified].includes(layout);
}
get iconEnabled(): boolean {
const layout: ValueCardLayout = this.valueCardWidgetConfigForm.get('layout').value;
return layout !== ValueCardLayout.simplified;
}
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected widgetConfigComponent: WidgetConfigComponent, protected widgetConfigComponent: WidgetConfigComponent,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,

6
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html

@ -22,7 +22,7 @@
<mat-option [value]="true">{{ 'datakey.latest' | translate }}</mat-option> <mat-option [value]="true">{{ 'datakey.latest' | translate }}</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field fxFlex class="tb-inline-field tb-key-field" subscriptSizing="dynamic"> <mat-form-field class="tb-inline-field tb-key-field" subscriptSizing="dynamic">
<mat-chip-grid #chipList> <mat-chip-grid #chipList>
<mat-chip-row class="tb-datakey-chip" *ngIf="modelValue.type" <mat-chip-row class="tb-datakey-chip" *ngIf="modelValue.type"
(removed)="removeKey()"> (removed)="removeKey()">
@ -44,7 +44,7 @@
matTooltipPosition="above">timeline</mat-icon> matTooltipPosition="above">timeline</mat-icon>
</ng-container> </ng-container>
</div> </div>
<div class="tb-chip-label"> <div class="tb-chip-label tb-chip-text">
<strong> <strong>
<ng-container *ngTemplateOutlet="keyName"></ng-container> <ng-container *ngTemplateOutlet="keyName"></ng-container>
</strong> </strong>
@ -139,7 +139,7 @@
</mat-option> </mat-option>
</mat-autocomplete> </mat-autocomplete>
</mat-form-field> </mat-form-field>
<mat-form-field fxFlex class="tb-inline-field" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="tb-inline-field tb-label-field" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="label" placeholder="{{ 'widget-config.set' | translate }}"> <input matInput formControlName="label" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>
<div *ngIf="!hideDataKeyColor" class="tb-color-field"> <div *ngIf="!hideDataKeyColor" class="tb-color-field">

13
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.scss

@ -38,7 +38,16 @@
} }
.tb-source-field { .tb-source-field {
width: 140px; width: 120px;
min-width: 120px;
}
.tb-key-field {
flex: 1 1 60%;
}
.tb-label-field {
flex: 1 1 40%;
} }
.tb-color-field, .tb-units-field, .tb-decimals-field { .tb-color-field, .tb-units-field, .tb-decimals-field {
@ -50,9 +59,11 @@
.tb-units-field { .tb-units-field {
width: 80px; width: 80px;
min-width: 80px;
} }
.tb-color-field, .tb-decimals-field { .tb-color-field, .tb-decimals-field {
width: 60px; width: 60px;
min-width: 60px;
} }
} }

4
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html

@ -20,8 +20,8 @@
<div class="tb-form-table"> <div class="tb-form-table">
<div class="tb-form-table-header"> <div class="tb-form-table-header">
<div *ngIf="hasAdditionalLatestDataKeys" class="tb-form-table-header-cell tb-source-header" translate>datakey.source</div> <div *ngIf="hasAdditionalLatestDataKeys" class="tb-form-table-header-cell tb-source-header" translate>datakey.source</div>
<div class="tb-form-table-header-cell" fxFlex translate>datakey.key</div> <div class="tb-form-table-header-cell tb-key-header" translate>datakey.key</div>
<div class="tb-form-table-header-cell" fxFlex translate>datakey.label</div> <div class="tb-form-table-header-cell tb-label-header" translate>datakey.label</div>
<div *ngIf="!hideDataKeyColor" class="tb-form-table-header-cell tb-color-header" translate>datakey.color</div> <div *ngIf="!hideDataKeyColor" class="tb-form-table-header-cell tb-color-header" translate>datakey.color</div>
<div *ngIf="!hideUnits" class="tb-form-table-header-cell tb-units-header" translate>widget-config.units-short</div> <div *ngIf="!hideUnits" class="tb-form-table-header-cell tb-units-header" translate>widget-config.units-short</div>
<div *ngIf="!hideDecimals" class="tb-form-table-header-cell tb-decimals-header" translate>widget-config.decimals-short</div> <div *ngIf="!hideDecimals" class="tb-form-table-header-cell tb-decimals-header" translate>widget-config.decimals-short</div>

12
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.scss

@ -15,15 +15,25 @@
*/ */
.tb-form-table-header-cell { .tb-form-table-header-cell {
&.tb-source-header { &.tb-source-header {
width: 140px; width: 120px;
min-width: 120px;
}
&.tb-key-header {
flex: 1 1 60%;
}
&.tb-label-header {
flex: 1 1 40%;
} }
&.tb-units-header { &.tb-units-header {
width: 80px; width: 80px;
min-width: 80px;
} }
&.tb-color-header, &.tb-decimals-header { &.tb-color-header, &.tb-decimals-header {
width: 60px; width: 60px;
min-width: 60px;
} }
&.tb-actions-header { &.tb-actions-header {
width: 114px; width: 114px;
min-width: 114px;
} }
} }

2
ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.html

@ -65,7 +65,7 @@
<span *ngIf="!hideDataKeyLabel && key.label !== key.name">{{key.label}}</span> <span *ngIf="!hideDataKeyLabel && key.label !== key.name">{{key.label}}</span>
</div> </div>
<div *ngIf="!hideDataKeyLabel && key.label !== key.name" class="tb-chip-separator">: </div> <div *ngIf="!hideDataKeyLabel && key.label !== key.name" class="tb-chip-separator">: </div>
<div class="tb-chip-label"> <div class="tb-chip-label tb-chip-text">
<strong> <strong>
<ng-container *ngTemplateOutlet="keyName; context:{key: key}"></ng-container> <ng-container *ngTemplateOutlet="keyName; context:{key: key}"></ng-container>
</strong> </strong>

10
ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.scss

@ -49,7 +49,7 @@
padding: 3px; padding: 3px;
height: 24px; height: 24px;
cursor: move; cursor: move;
mat-icon { .mat-icon {
pointer-events: none; pointer-events: none;
} }
} }
@ -65,9 +65,11 @@
font-weight: normal; font-weight: normal;
font-size: 14px; font-size: 14px;
line-height: 20px; line-height: 20px;
overflow: hidden; &.tb-chip-text {
text-overflow: ellipsis; overflow: hidden;
white-space: nowrap; text-overflow: ellipsis;
white-space: nowrap;
}
.mat-icon.tb-datakey-icon { .mat-icon.tb-datakey-icon {
margin-right: 4px; margin-right: 4px;
margin-left: 4px; margin-left: 4px;

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

@ -44,6 +44,7 @@ import { widgetSettingsComponentsMap } from '@home/components/widget/lib/setting
import { Dashboard } from '@shared/models/dashboard.models'; import { Dashboard } from '@shared/models/dashboard.models';
import { WidgetService } from '@core/http/widget.service'; import { WidgetService } from '@core/http/widget.service';
import { IAliasController } from '@core/api/widget-api.models'; import { IAliasController } from '@core/api/widget-api.models';
import { WidgetConfigComponentData } from '@home/models/widget-component.models';
@Component({ @Component({
selector: 'tb-widget-settings', selector: 'tb-widget-settings',
@ -73,6 +74,9 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnInit, On
@Input() @Input()
widget: Widget; widget: Widget;
@Input()
widgetConfig: WidgetConfigComponentData;
private settingsDirective: string; private settingsDirective: string;
definedDirectiveError: string; definedDirectiveError: string;
@ -126,6 +130,11 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnInit, On
this.definedSettingsComponent.aliasController = this.aliasController; this.definedSettingsComponent.aliasController = this.aliasController;
} }
} }
if (propName === 'widgetConfig') {
if (this.definedSettingsComponent) {
this.definedSettingsComponent.widgetConfig = this.widgetConfig;
}
}
} }
} }
} }
@ -214,6 +223,7 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnInit, On
this.definedSettingsComponent.aliasController = this.aliasController; this.definedSettingsComponent.aliasController = this.aliasController;
this.definedSettingsComponent.dashboard = this.dashboard; this.definedSettingsComponent.dashboard = this.dashboard;
this.definedSettingsComponent.widget = this.widget; this.definedSettingsComponent.widget = this.widget;
this.definedSettingsComponent.widgetConfig = this.widgetConfig;
this.definedSettingsComponent.functionScopeVariables = this.widgetService.getWidgetScopeVariables(); this.definedSettingsComponent.functionScopeVariables = this.widgetService.getWidgetScopeVariables();
this.changeSubscription = this.definedSettingsComponent.settingsChanged.subscribe((settings) => { this.changeSubscription = this.definedSettingsComponent.settingsChanged.subscribe((settings) => {
this.updateModel(settings); this.updateModel(settings);

20
ui-ngx/src/app/modules/home/components/widget/config/widget-settings.models.ts

@ -276,6 +276,14 @@ export enum BackgroundType {
color = 'color' color = 'color'
} }
export const backgroundTypeTranslations = new Map<BackgroundType, string>(
[
[BackgroundType.image, 'widgets.background.background-type-image'],
[BackgroundType.imageUrl, 'widgets.background.background-type-image-url'],
[BackgroundType.color, 'widgets.background.background-type-color']
]
);
export interface OverlaySettings { export interface OverlaySettings {
enabled: boolean; enabled: boolean;
color: string; color: string;
@ -313,11 +321,13 @@ export const backgroundStyle = (background: BackgroundSettings): ComponentStyle
}; };
} else { } else {
const imageUrl = background.type === BackgroundType.image ? background.imageBase64 : background.imageUrl; const imageUrl = background.type === BackgroundType.image ? background.imageBase64 : background.imageUrl;
return { if (imageUrl) {
background: `url(${imageUrl}) no-repeat`, return {
backgroundSize: 'cover', background: `url(${imageUrl}) no-repeat 50% 50% / cover`
backgroundPosition: '50% 50%' };
}; } else {
return {};
}
} }
}; };

4
ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.scss

@ -71,7 +71,7 @@
background-size: 18px 18px; background-size: 18px 18px;
} }
mat-icon.node-icon { .mat-icon.node-icon {
width: 22px; width: 22px;
min-width: 22px; min-width: 22px;
height: 22px; height: 22px;
@ -109,7 +109,7 @@
background-size: 24px 24px; background-size: 24px 24px;
} }
mat-icon.node-icon { .mat-icon.node-icon {
width: 40px; width: 40px;
min-width: 40px; min-width: 40px;
height: 40px; height: 40px;

4
ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.component.scss

@ -64,7 +64,7 @@
background-size: 18px 18px; background-size: 18px 18px;
} }
mat-icon.node-icon { .mat-icon.node-icon {
width: 22px; width: 22px;
min-width: 22px; min-width: 22px;
height: 22px; height: 22px;
@ -102,7 +102,7 @@
background-size: 24px 24px; background-size: 24px 24px;
} }
mat-icon.node-icon { .mat-icon.node-icon {
width: 40px; width: 40px;
min-width: 40px; min-width: 40px;
height: 40px; height: 40px;

2
ui-ngx/src/app/modules/home/components/widget/lib/navigation-card-widget.component.scss

@ -31,7 +31,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
mat-icon { .mat-icon {
margin: auto !important; margin: auto !important;
} }
span.mdc-button__label { span.mdc-button__label {

89
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-card-widget-settings.component.html

@ -0,0 +1,89 @@
<!--
Copyright © 2016-2023 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.
-->
<ng-container [formGroup]="valueCardWidgetSettingsForm">
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.value-card.value-card-style</div>
<tb-image-cards-select rowHeight="{{ horizontal ? '3:1' : '7:5' }}"
[cols]="horizontal ? 2 : 4"
[colsLtMd]="horizontal ? 1 : 2"
label="{{ 'widgets.value-card.layout' | translate }}" formControlName="layout">
<tb-image-cards-select-option *ngFor="let layout of valueCardLayouts"
[value]="layout"
[image]="valueCardLayoutImageMap.get(layout)">
{{ valueCardLayoutTranslationMap.get(layout) | translate }}
</tb-image-cards-select-option>
</tb-image-cards-select>
<div class="tb-form-row space-between">
<mat-slide-toggle class="mat-slide" formControlName="showLabel">
{{ 'widgets.value-card.label' | translate }}
</mat-slide-toggle>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-font-settings formControlName="labelFont"
[previewText]="label">
</tb-font-settings>
<tb-color-settings formControlName="labelColor">
</tb-color-settings>
</div>
</div>
<div [fxShow]="iconEnabled" class="tb-form-row">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showIcon">
{{ 'widgets.value-card.icon' | translate }}
</mat-slide-toggle>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field fxFlex appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput type="number" min="0" formControlName="iconSize" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-css-unit-select fxFlex formControlName="iconSizeUnit"></tb-css-unit-select>
<tb-material-icon-select asBoxInput
[color]="valueCardWidgetSettingsForm.get('iconColor').value?.color"
formControlName="icon">
</tb-material-icon-select>
<tb-color-settings formControlName="iconColor">
</tb-color-settings>
</div>
</div>
<div class="tb-form-row space-between">
<div translate>widgets.value-card.value</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-font-settings formControlName="valueFont"
[previewText]="valuePreviewFn">
</tb-font-settings>
<tb-color-settings formControlName="valueColor">
</tb-color-settings>
</div>
</div>
<div [fxShow]="dateEnabled" class="tb-form-row">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showDate">
{{ 'widgets.value-card.date' | translate }}
</mat-slide-toggle>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-date-format-select fxFlex formControlName="dateFormat"></tb-date-format-select>
<tb-font-settings formControlName="dateFont"
[previewText]="datePreviewFn">
</tb-font-settings>
<tb-color-settings formControlName="dateColor">
</tb-color-settings>
</div>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.background.background' | translate }}</div>
<tb-background-settings formControlName="background">
</tb-background-settings>
</div>
</div>
</ng-container>

200
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-card-widget-settings.component.ts

@ -0,0 +1,200 @@
///
/// Copyright © 2016-2023 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, Injector } from '@angular/core';
import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import {
valueCardDefaultSettings,
ValueCardLayout, valueCardLayoutImages,
valueCardLayouts, valueCardLayoutTranslations
} from '@home/components/widget/lib/cards/value-card-widget.models';
import { formatValue, isDefinedAndNotNull } from '@core/utils';
import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import {
DateFormatProcessor,
DateFormatSettings,
getLabel
} from '@home/components/widget/config/widget-settings.models';
@Component({
selector: 'tb-value-card-widget-settings',
templateUrl: './value-card-widget-settings.component.html',
styleUrls: []
})
export class ValueCardWidgetSettingsComponent extends WidgetSettingsComponent {
valueCardLayouts: ValueCardLayout[] = [];
valueCardLayoutTranslationMap = valueCardLayoutTranslations;
valueCardLayoutImageMap = valueCardLayoutImages;
horizontal = false;
valueCardWidgetSettingsForm: UntypedFormGroup;
valuePreviewFn = this._valuePreviewFn.bind(this);
datePreviewFn = this._datePreviewFn.bind(this);
get label(): string {
return getLabel(this.widgetConfig.config.datasources);
}
get dateEnabled(): boolean {
const layout: ValueCardLayout = this.valueCardWidgetSettingsForm.get('layout').value;
return ![ValueCardLayout.vertical, ValueCardLayout.simplified].includes(layout);
}
get iconEnabled(): boolean {
const layout: ValueCardLayout = this.valueCardWidgetSettingsForm.get('layout').value;
return layout !== ValueCardLayout.simplified;
}
constructor(protected store: Store<AppState>,
private $injector: Injector,
private fb: UntypedFormBuilder) {
super(store);
}
protected settingsForm(): UntypedFormGroup {
return this.valueCardWidgetSettingsForm;
}
protected onWidgetConfigSet(widgetConfig: WidgetConfigComponentData) {
const params = widgetConfig.typeParameters as any;
this.horizontal = isDefinedAndNotNull(params.horizontal) ? params.horizontal : false;
this.valueCardLayouts = valueCardLayouts(this.horizontal);
}
protected defaultSettings(): WidgetSettings {
return valueCardDefaultSettings(this.horizontal);
}
protected onSettingsSet(settings: WidgetSettings) {
this.valueCardWidgetSettingsForm = this.fb.group({
layout: [settings.layout, []],
showLabel: [settings.showLabel, []],
labelFont: [settings.labelFont, []],
labelColor: [settings.labelColor, []],
showIcon: [settings.showIcon, []],
iconSize: [settings.iconSize, [Validators.min(0)]],
iconSizeUnit: [settings.iconSizeUnit, []],
icon: [settings.icon, []],
iconColor: [settings.iconColor, []],
valueFont: [settings.valueFont, []],
valueColor: [settings.valueColor, []],
showDate: [settings.showDate, []],
dateFormat: [settings.dateFormat, []],
dateFont: [settings.dateFont, []],
dateColor: [settings.dateColor, []],
background: [settings.background, []]
});
}
protected validatorTriggers(): string[] {
return ['layout', 'showLabel', 'showIcon', 'showDate'];
}
protected updateValidators(emitEvent: boolean) {
const layout: ValueCardLayout = this.valueCardWidgetSettingsForm.get('layout').value;
const showLabel: boolean = this.valueCardWidgetSettingsForm.get('showLabel').value;
const showIcon: boolean = this.valueCardWidgetSettingsForm.get('showIcon').value;
const showDate: boolean = this.valueCardWidgetSettingsForm.get('showDate').value;
const dateEnabled = ![ValueCardLayout.vertical, ValueCardLayout.simplified].includes(layout);
const iconEnabled = layout !== ValueCardLayout.simplified;
if (showLabel) {
this.valueCardWidgetSettingsForm.get('labelFont').enable();
this.valueCardWidgetSettingsForm.get('labelColor').enable();
} else {
this.valueCardWidgetSettingsForm.get('labelFont').disable();
this.valueCardWidgetSettingsForm.get('labelColor').disable();
}
if (iconEnabled) {
this.valueCardWidgetSettingsForm.get('showIcon').enable({emitEvent: false});
if (showIcon) {
this.valueCardWidgetSettingsForm.get('iconSize').enable();
this.valueCardWidgetSettingsForm.get('iconSizeUnit').enable();
this.valueCardWidgetSettingsForm.get('icon').enable();
this.valueCardWidgetSettingsForm.get('iconColor').enable();
} else {
this.valueCardWidgetSettingsForm.get('iconSize').disable();
this.valueCardWidgetSettingsForm.get('iconSizeUnit').disable();
this.valueCardWidgetSettingsForm.get('icon').disable();
this.valueCardWidgetSettingsForm.get('iconColor').disable();
}
} else {
this.valueCardWidgetSettingsForm.get('showIcon').disable({emitEvent: false});
this.valueCardWidgetSettingsForm.get('iconSize').disable();
this.valueCardWidgetSettingsForm.get('iconSizeUnit').disable();
this.valueCardWidgetSettingsForm.get('icon').disable();
this.valueCardWidgetSettingsForm.get('iconColor').disable();
}
if (dateEnabled) {
this.valueCardWidgetSettingsForm.get('showDate').enable({emitEvent: false});
if (showDate) {
this.valueCardWidgetSettingsForm.get('dateFormat').enable();
this.valueCardWidgetSettingsForm.get('dateFont').enable();
this.valueCardWidgetSettingsForm.get('dateColor').enable();
} else {
this.valueCardWidgetSettingsForm.get('dateFormat').disable();
this.valueCardWidgetSettingsForm.get('dateFont').disable();
this.valueCardWidgetSettingsForm.get('dateColor').disable();
}
} else {
this.valueCardWidgetSettingsForm.get('showDate').disable({emitEvent: false});
this.valueCardWidgetSettingsForm.get('dateFormat').disable();
this.valueCardWidgetSettingsForm.get('dateFont').disable();
this.valueCardWidgetSettingsForm.get('dateColor').disable();
}
this.valueCardWidgetSettingsForm.get('showIcon').updateValueAndValidity({emitEvent: false});
this.valueCardWidgetSettingsForm.get('showDate').updateValueAndValidity({emitEvent: false});
this.valueCardWidgetSettingsForm.get('labelFont').updateValueAndValidity({emitEvent});
this.valueCardWidgetSettingsForm.get('labelColor').updateValueAndValidity({emitEvent});
this.valueCardWidgetSettingsForm.get('iconSize').updateValueAndValidity({emitEvent});
this.valueCardWidgetSettingsForm.get('iconSizeUnit').updateValueAndValidity({emitEvent});
this.valueCardWidgetSettingsForm.get('icon').updateValueAndValidity({emitEvent});
this.valueCardWidgetSettingsForm.get('iconColor').updateValueAndValidity({emitEvent});
this.valueCardWidgetSettingsForm.get('dateFormat').updateValueAndValidity({emitEvent});
this.valueCardWidgetSettingsForm.get('dateFont').updateValueAndValidity({emitEvent});
this.valueCardWidgetSettingsForm.get('dateColor').updateValueAndValidity({emitEvent});
}
private _valuePreviewFn(): string {
const units: string = this.widgetConfig.config.units;
const decimals: number = this.widgetConfig.config.decimals;
return formatValue(22, decimals, units, true);
}
private _datePreviewFn(): string {
const dateFormat: DateFormatSettings = this.valueCardWidgetSettingsForm.get('dateFormat').value;
const processor = DateFormatProcessor.fromSettings(this.$injector, dateFormat);
processor.update(Date.now());
return processor.formatted;
}
}

87
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings-panel.component.html

@ -0,0 +1,87 @@
<!--
Copyright © 2016-2023 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="tb-background-settings-panel" [formGroup]="backgroundSettingsFormGroup">
<div class="tb-background-settings-title" translate>widgets.background.background-settings</div>
<div class="tb-form-panel">
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<div class="tb-form-panel-title" translate>widgets.background.background</div>
<tb-toggle-select formControlName="type" fxFlex>
<tb-toggle-option *ngFor="let type of backgroundTypes"
[value]="type">
{{ backgroundTypeTranslationsMap.get(type) | translate }}
</tb-toggle-option>
</tb-toggle-select>
</div>
<tb-image-input [fxShow]="backgroundSettingsFormGroup.get('type').value === backgroundType.image" formControlName="imageBase64"></tb-image-input>
<div [fxShow]="backgroundSettingsFormGroup.get('type').value === backgroundType.imageUrl" class="tb-form-row space-between">
<div translate>widgets.background.image-url</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="imageUrl" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div [fxShow]="backgroundSettingsFormGroup.get('type').value === backgroundType.color" class="tb-form-row space-between">
<div translate>widgets.color.color</div>
<tb-color-input asBoxInput
formControlName="color">
</tb-color-input>
</div>
</div>
<div class="tb-form-panel" formGroupName="overlay">
<div class="tb-form-panel-title" translate>widgets.background.overlay</div>
<mat-slide-toggle class="mat-slide" formControlName="enabled">
{{ 'widgets.background.enable-overlay' | translate }}
</mat-slide-toggle>
<div class="tb-form-row space-between">
<div translate>widgets.color.color</div>
<tb-color-input asBoxInput
formControlName="color">
</tb-color-input>
</div>
<div class="tb-form-row space-between">
<div translate>widgets.background.blur</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="blur" type="number" min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix>px</div>
</mat-form-field>
</div>
</div>
<div class="tb-background-settings-preview">
<div class="tb-background-settings-preview-title" translate>
widgets.background.preview
</div>
<div class="tb-background-settings-preview-box mat-elevation-z4" [style]="backgroundStyle">
<div class="tb-background-settings-preview-overlay" [style]="overlayStyle">
</div>
</div>
</div>
<div class="tb-background-settings-panel-buttons">
<button mat-button
color="primary"
type="button"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button
color="primary"
type="button"
(click)="applyColorSettings()"
[disabled]="backgroundSettingsFormGroup.invalid || !backgroundSettingsFormGroup.dirty">
{{ 'action.apply' | translate }}
</button>
</div>
</div>

73
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings-panel.component.scss

@ -0,0 +1,73 @@
/**
* Copyright © 2016-2023 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 '../../../../../../../../scss/constants';
.tb-background-settings-panel {
width: 620px;
display: flex;
flex-direction: column;
gap: 16px;
@media #{$mat-lt-md} {
width: 90vw;
}
.tb-background-settings-title {
font-size: 16px;
font-weight: 500;
line-height: 24px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.87);
}
.tb-background-settings-preview {
flex: 1;
background: rgba(0, 0, 0, 0.04);
display: flex;
flex-direction: column;
padding: 12px 16px 24px 16px;
align-items: center;
gap: 12px;
}
.tb-background-settings-preview-title {
align-self: stretch;
font-size: 16px;
font-style: normal;
font-weight: 500;
line-height: 24px;
color: rgba(0, 0, 0, 0.38);
}
.tb-background-settings-preview-box {
position: relative;
width: 136px;
height: 118px;
border-radius: 2.666px;
}
.tb-background-settings-preview-overlay {
position: absolute;
border-radius: 2.666px;
top: 7.998px;
bottom: 7.998px;
left: 7.998px;
right: 7.998px;
}
.tb-background-settings-panel-buttons {
height: 40px;
display: flex;
flex-direction: row;
gap: 16px;
justify-content: flex-end;
align-items: flex-end;
}
}

120
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings-panel.component.ts

@ -0,0 +1,120 @@
///
/// Copyright © 2016-2023 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, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import {
backgroundStyle,
overlayStyle,
BackgroundSettings,
BackgroundType,
backgroundTypeTranslations, ComponentStyle
} from '@home/components/widget/config/widget-settings.models';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@Component({
selector: 'tb-background-settings-panel',
templateUrl: './background-settings-panel.component.html',
providers: [],
styleUrls: ['./background-settings-panel.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class BackgroundSettingsPanelComponent extends PageComponent implements OnInit {
@Input()
backgroundSettings: BackgroundSettings;
@Input()
popover: TbPopoverComponent<BackgroundSettingsPanelComponent>;
@Output()
backgroundSettingsApplied = new EventEmitter<BackgroundSettings>();
backgroundType = BackgroundType;
backgroundTypes = Object.keys(BackgroundType) as BackgroundType[];
backgroundTypeTranslationsMap = backgroundTypeTranslations;
backgroundSettingsFormGroup: UntypedFormGroup;
backgroundStyle: ComponentStyle = {};
overlayStyle: ComponentStyle = {};
constructor(private fb: UntypedFormBuilder,
protected store: Store<AppState>) {
super(store);
}
ngOnInit(): void {
this.backgroundSettingsFormGroup = this.fb.group(
{
type: [this.backgroundSettings?.type, []],
imageBase64: [this.backgroundSettings?.imageBase64, []],
imageUrl: [this.backgroundSettings?.imageUrl, []],
color: [this.backgroundSettings?.color, []],
overlay: this.fb.group({
enabled: [this.backgroundSettings?.overlay?.enabled, []],
color: [this.backgroundSettings?.overlay?.color, []],
blur: [this.backgroundSettings?.overlay?.blur, []]
})
}
);
this.backgroundSettingsFormGroup.get('type').valueChanges.subscribe(() => {
setTimeout(() => {this.popover?.updatePosition();}, 0);
});
this.backgroundSettingsFormGroup.get('overlay').get('enabled').valueChanges.subscribe(() => {
this.updateValidators();
});
this.backgroundSettingsFormGroup.valueChanges.subscribe(() => {
this.updateBackgroundStyle();
});
this.updateValidators();
this.updateBackgroundStyle();
}
cancel() {
this.popover?.hide();
}
applyColorSettings() {
const backgroundSettings = this.backgroundSettingsFormGroup.value;
this.backgroundSettingsApplied.emit(backgroundSettings);
}
private updateValidators() {
const overlayEnabled: boolean = this.backgroundSettingsFormGroup.get('overlay').get('enabled').value;
if (overlayEnabled) {
this.backgroundSettingsFormGroup.get('overlay').get('color').enable();
this.backgroundSettingsFormGroup.get('overlay').get('blur').enable();
} else {
this.backgroundSettingsFormGroup.get('overlay').get('color').disable();
this.backgroundSettingsFormGroup.get('overlay').get('blur').disable();
}
this.backgroundSettingsFormGroup.get('overlay').get('color').updateValueAndValidity({emitEvent: false});
this.backgroundSettingsFormGroup.get('overlay').get('blur').updateValueAndValidity({emitEvent: false});
}
private updateBackgroundStyle() {
const background: BackgroundSettings = this.backgroundSettingsFormGroup.value;
this.backgroundStyle = backgroundStyle(background);
this.overlayStyle = overlayStyle(background.overlay);
}
}

30
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings.component.html

@ -0,0 +1,30 @@
<!--
Copyright © 2016-2023 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.
-->
<button type="button"
mat-stroked-button
class="tb-box-button tb-background-settings"
[disabled]="disabled"
#matButton
(click)="openBackgroundSettingsPopup($event, matButton)">
<div class="tb-color-preview box" [ngClass]="{'disabled': disabled}">
<div class="tb-color-result" [style]="backgroundStyle">
<div class="tb-color-overlay" [style]="overlayStyle">
</div>
</div>
</div>
</button>

41
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings.component.scss

@ -0,0 +1,41 @@
/**
* Copyright © 2016-2023 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.
*/
button.mat-mdc-button-base.tb-box-button.tb-background-settings {
padding: 0;
.mat-mdc-button-persistent-ripple {
z-index: 2;
}
.tb-color-preview {
width: 38px;
min-width: 38px;
height: 38px;
&.box {
.tb-color-result {
&:after {
border: none;
}
}
.tb-color-overlay {
position: absolute;
border-radius: 3px;
top: 4px;
bottom: 4px;
left: 4px;
right: 4px;
}
}
}
}

120
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings.component.ts

@ -0,0 +1,120 @@
///
/// Copyright © 2016-2023 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, forwardRef, Input, OnInit, Renderer2, ViewContainerRef, ViewEncapsulation } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import {
BackgroundSettings,
backgroundStyle,
BackgroundType,
ComponentStyle,
overlayStyle
} from '@home/components/widget/config/widget-settings.models';
import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service';
import {
BackgroundSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/background-settings-panel.component';
@Component({
selector: 'tb-background-settings',
templateUrl: './background-settings.component.html',
styleUrls: ['./background-settings.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => BackgroundSettingsComponent),
multi: true
}
],
encapsulation: ViewEncapsulation.None
})
export class BackgroundSettingsComponent implements OnInit, ControlValueAccessor {
@Input()
disabled: boolean;
backgroundType = BackgroundType;
modelValue: BackgroundSettings;
backgroundStyle: ComponentStyle = {};
overlayStyle: ComponentStyle = {};
private propagateChange = null;
constructor(private popoverService: TbPopoverService,
private renderer: Renderer2,
private viewContainerRef: ViewContainerRef) {}
ngOnInit(): void {
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
this.updateBackgroundStyle();
}
writeValue(value: BackgroundSettings): void {
this.modelValue = value;
this.updateBackgroundStyle();
}
openBackgroundSettingsPopup($event: Event, matButton: MatButton) {
if ($event) {
$event.stopPropagation();
}
const trigger = matButton._elementRef.nativeElement;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
const ctx: any = {
backgroundSettings: this.modelValue
};
const backgroundSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, BackgroundSettingsPanelComponent, 'left', true, null,
ctx,
{},
{}, {}, true);
backgroundSettingsPanelPopover.tbComponentRef.instance.popover = backgroundSettingsPanelPopover;
backgroundSettingsPanelPopover.tbComponentRef.instance.backgroundSettingsApplied.subscribe((backgroundSettings) => {
backgroundSettingsPanelPopover.hide();
this.modelValue = backgroundSettings;
this.updateBackgroundStyle();
this.propagateChange(this.modelValue);
});
}
}
private updateBackgroundStyle() {
if (!this.disabled) {
this.backgroundStyle = backgroundStyle(this.modelValue);
this.overlayStyle = overlayStyle(this.modelValue.overlay);
} else {
this.backgroundStyle = {};
this.overlayStyle = {};
}
}
}

27
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.ts

@ -21,14 +21,14 @@ import {
Directive, Directive,
ElementRef, ElementRef,
forwardRef, forwardRef,
Input, Input, OnChanges,
OnDestroy, OnInit, OnDestroy, OnInit,
QueryList, QueryList, SimpleChanges,
ViewEncapsulation ViewEncapsulation
} from '@angular/core'; } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormControl } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormControl } from '@angular/forms';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { Observable, Subject } from 'rxjs'; import { BehaviorSubject, combineLatest, Observable, Subject } from 'rxjs';
import { map, share, startWith, takeUntil } from 'rxjs/operators'; import { map, share, startWith, takeUntil } from 'rxjs/operators';
import { BreakpointObserver } from '@angular/cdk/layout'; import { BreakpointObserver } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants'; import { MediaBreakpoints } from '@shared/models/constants';
@ -73,7 +73,7 @@ export class ImageCardsSelectOptionDirective {
], ],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
export class ImageCardsSelectComponent implements ControlValueAccessor, OnInit, AfterContentInit, OnDestroy { export class ImageCardsSelectComponent implements ControlValueAccessor, OnInit, OnChanges, AfterContentInit, OnDestroy {
@ContentChildren(ImageCardsSelectOptionDirective) imageCardsSelectOptions: QueryList<ImageCardsSelectOptionDirective>; @ContentChildren(ImageCardsSelectOptionDirective) imageCardsSelectOptions: QueryList<ImageCardsSelectOptionDirective>;
@ -107,20 +107,33 @@ export class ImageCardsSelectComponent implements ControlValueAccessor, OnInit,
private _destroyed = new Subject<void>(); private _destroyed = new Subject<void>();
private _colsChanged = new BehaviorSubject<void>(null);
constructor(private breakpointObserver: BreakpointObserver) { constructor(private breakpointObserver: BreakpointObserver) {
this.valueFormControl = new UntypedFormControl(''); this.valueFormControl = new UntypedFormControl('');
} }
ngOnInit(): void { ngOnInit(): void {
const gridColumns = this.breakpointObserver.isMatched(MediaBreakpoints['lt-md']) ? this.colsLtMd : this.cols; const gridColumns = this.breakpointObserver.isMatched(MediaBreakpoints['lt-md']) ? this.colsLtMd : this.cols;
this.cols$ = this.breakpointObserver this.cols$ = combineLatest({state: this.breakpointObserver
.observe(MediaBreakpoints['lt-md']).pipe( .observe(MediaBreakpoints['lt-md']), colsChanged: this._colsChanged.asObservable()}).pipe(
map((state) => state.matches ? this.colsLtMd : this.cols), map((data) => data.state.matches ? this.colsLtMd : this.cols),
startWith(gridColumns), startWith(gridColumns),
share() share()
); );
} }
ngOnChanges(changes: SimpleChanges): void {
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (!change.firstChange && change.currentValue !== change.previousValue) {
if (['cols', 'colsLtMd'].includes(propName)) {
this._colsChanged.next(null);
}
}
}
}
ngAfterContentInit(): void { ngAfterContentInit(): void {
this.imageCardsSelectOptions.changes.pipe(startWith(null), takeUntil(this._destroyed)).subscribe(() => { this.imageCardsSelectOptions.changes.pipe(startWith(null), takeUntil(this._destroyed)).subscribe(() => {
this.syncImageCardsSelectOptions(); this.syncImageCardsSelectOptions();

20
ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts

@ -281,6 +281,13 @@ import { DateFormatSelectComponent } from '@home/components/widget/lib/settings/
import { import {
DateFormatSettingsPanelComponent DateFormatSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/date-format-settings-panel.component'; } from '@home/components/widget/lib/settings/common/date-format-settings-panel.component';
import { BackgroundSettingsComponent } from '@home/components/widget/lib/settings/common/background-settings.component';
import {
BackgroundSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/background-settings-panel.component';
import {
ValueCardWidgetSettingsComponent
} from '@home/components/widget/lib/settings/cards/value-card-widget-settings.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -391,7 +398,10 @@ import {
ColorSettingsPanelComponent, ColorSettingsPanelComponent,
CssUnitSelectComponent, CssUnitSelectComponent,
DateFormatSelectComponent, DateFormatSelectComponent,
DateFormatSettingsPanelComponent DateFormatSettingsPanelComponent,
BackgroundSettingsComponent,
BackgroundSettingsPanelComponent,
ValueCardWidgetSettingsComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -506,7 +516,10 @@ import {
ColorSettingsPanelComponent, ColorSettingsPanelComponent,
CssUnitSelectComponent, CssUnitSelectComponent,
DateFormatSelectComponent, DateFormatSelectComponent,
DateFormatSettingsPanelComponent DateFormatSettingsPanelComponent,
BackgroundSettingsComponent,
BackgroundSettingsPanelComponent,
ValueCardWidgetSettingsComponent
] ]
}) })
export class WidgetSettingsModule { export class WidgetSettingsModule {
@ -575,5 +588,6 @@ export const widgetSettingsComponentsMap: {[key: string]: Type<IWidgetSettingsCo
'tb-route-map-widget-settings': RouteMapWidgetSettingsComponent, 'tb-route-map-widget-settings': RouteMapWidgetSettingsComponent,
'tb-trip-animation-widget-settings': TripAnimationWidgetSettingsComponent, 'tb-trip-animation-widget-settings': TripAnimationWidgetSettingsComponent,
'tb-doc-links-widget-settings': DocLinksWidgetSettingsComponent, 'tb-doc-links-widget-settings': DocLinksWidgetSettingsComponent,
'tb-quick-links-widget-settings': QuickLinksWidgetSettingsComponent 'tb-quick-links-widget-settings': QuickLinksWidgetSettingsComponent,
'tb-value-card-widget-settings': ValueCardWidgetSettingsComponent
}; };

2
ui-ngx/src/app/modules/home/components/widget/lib/trip-animation/trip-animation.component.scss

@ -54,7 +54,7 @@
line-height: 24px; line-height: 24px;
z-index: 999; z-index: 999;
mat-icon { .mat-icon {
width: 24px; width: 24px;
height: 24px; height: 24px;

3
ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts

@ -546,6 +546,9 @@ export class WidgetComponentService {
if (isUndefined(result.typeParameters.previewHeight)) { if (isUndefined(result.typeParameters.previewHeight)) {
result.typeParameters.previewHeight = '70%'; result.typeParameters.previewHeight = '70%';
} }
if (isUndefined(result.typeParameters.absoluteHeader)) {
result.typeParameters.absoluteHeader = false;
}
if (isFunction(widgetTypeInstance.actionSources)) { if (isFunction(widgetTypeInstance.actionSources)) {
result.actionSources = widgetTypeInstance.actionSources(); result.actionSources = widgetTypeInstance.actionSources();
} else { } else {

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

@ -282,6 +282,7 @@
[aliasController]="aliasController" [aliasController]="aliasController"
[dashboard]="dashboard" [dashboard]="dashboard"
[widget]="widget" [widget]="widget"
[widgetConfig]="modelValue"
formControlName="settings"> formControlName="settings">
</tb-widget-settings> </tb-widget-settings>
</div> </div>

17
ui-ngx/src/app/modules/home/components/widget/widget-container.component.html

@ -19,7 +19,6 @@
[fullscreenBackgroundStyle]="dashboardStyle" [fullscreenBackgroundStyle]="dashboardStyle"
[fullscreenBackgroundImage]="backgroundImage" [fullscreenBackgroundImage]="backgroundImage"
(fullscreenChanged)="onFullscreenChanged($event)" (fullscreenChanged)="onFullscreenChanged($event)"
fxLayout="column"
class="tb-widget" class="tb-widget"
[ngClass]="{ [ngClass]="{
'tb-highlighted': isHighlighted(widget), 'tb-highlighted': isHighlighted(widget),
@ -32,8 +31,11 @@
(mousedown)="onMouseDown($event)" (mousedown)="onMouseDown($event)"
(click)="onClicked($event)" (click)="onClicked($event)"
(contextmenu)="onContextMenu($event)"> (contextmenu)="onContextMenu($event)">
<div *ngIf="!!widgetComponent.widgetContext?.inited" fxLayout="row" fxLayoutAlign="space-between start"> <div *ngIf="!!widgetComponent.widgetContext?.inited"
<div class="tb-widget-title" fxLayout="column" fxLayoutAlign="center start" *ngIf="widget.showWidgetTitlePanel"> class="tb-widget-header"
[class]="{'tb-widget-header-absolute': widgetComponent.widgetContext?.absoluteHeader}">
<div *ngIf="widget.showWidgetTitlePanel"
class="tb-widget-title">
<span *ngIf="widget.showTitle" <span *ngIf="widget.showTitle"
[ngClass]="{'single-row': widget.hasTimewindow}" [ngClass]="{'single-row': widget.hasTimewindow}"
[ngStyle]="widget.titleStyle" [ngStyle]="widget.titleStyle"
@ -57,9 +59,7 @@
</div> </div>
<div *ngIf="widget.showWidgetActions" <div *ngIf="widget.showWidgetActions"
class="tb-widget-actions" class="tb-widget-actions"
[ngClass]="{'tb-widget-actions-absolute': !(widget.showWidgetTitlePanel&&(widget.showTitle||widget.hasAggregation))}" [class]="{'tb-widget-actions-absolute': !(widget.showWidgetTitlePanel&&(widget.showTitle||widget.hasAggregation))}"
fxLayout="row"
fxLayoutAlign="start center"
(mousedown)="$event.stopPropagation()"> (mousedown)="$event.stopPropagation()">
<button mat-icon-button *ngFor="let action of widget.customHeaderActions" <button mat-icon-button *ngFor="let action of widget.customHeaderActions"
[fxShow]="!isEdit" [fxShow]="!isEdit"
@ -105,9 +105,8 @@
</button> </button>
</div> </div>
</div> </div>
<div fxFlex fxLayout="column" class="tb-widget-content" [ngClass]="{'tb-no-interaction': disableWidgetInteraction}"> <div class="tb-widget-content" [ngClass]="{'tb-no-interaction': disableWidgetInteraction}">
<tb-widget fxFlex <tb-widget #widgetComponent
#widgetComponent
[dashboardWidget]="widget" [dashboardWidget]="widget"
[isEdit]="isEdit" [isEdit]="isEdit"
[isMobile]="isMobile"> [isMobile]="isMobile">

47
ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss

@ -14,9 +14,14 @@
* limitations under the License. * limitations under the License.
*/ */
tb-widget.tb-widget { .tb-widget-container {
position: relative; position: absolute;
height: 100%; inset: 0;
}
.tb-widget {
position: absolute;
inset: 0;
margin: 0; margin: 0;
overflow: hidden; overflow: hidden;
outline: none; outline: none;
@ -25,15 +30,27 @@ tb-widget.tb-widget {
} }
div.tb-widget { div.tb-widget {
position: relative; display: flex;
height: 100%; flex-direction: column;
margin: 0; .tb-widget-header {
overflow: hidden; display: flex;
outline: none; flex-direction: row;
place-content: flex-start space-between;
transition: all .2s ease-in-out; align-items: flex-start;
&-absolute {
position: absolute;
top: 0;
right: 0;
left: 0;
z-index: 1;
}
}
.tb-widget-title { .tb-widget-title {
display: flex;
flex-direction: column;
place-content: flex-start center;
align-items: flex-start;
max-height: 65px; max-height: 65px;
padding-top: 5px; padding-top: 5px;
padding-left: 5px; padding-left: 5px;
@ -63,6 +80,10 @@ div.tb-widget {
} }
.tb-widget-actions { .tb-widget-actions {
display: flex;
flex-direction: row;
place-content: center flex-start;
align-items: center;
z-index: 19; z-index: 19;
margin: 5px 0 0; margin: 5px 0 0;
@ -104,13 +125,11 @@ div.tb-widget {
} }
.tb-widget-content { .tb-widget-content {
flex: 1;
position: relative;
&.tb-no-interaction { &.tb-no-interaction {
pointer-events: none; pointer-events: none;
} }
tb-widget {
position: relative;
width: 100%;
}
} }
&.tb-highlighted { &.tb-highlighted {

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

@ -409,6 +409,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
elem.classList.add(this.widgetContext.widgetNamespace); elem.classList.add(this.widgetContext.widgetNamespace);
this.widgetType = this.widgetInfo.widgetTypeFunction; this.widgetType = this.widgetInfo.widgetTypeFunction;
this.typeParameters = this.widgetInfo.typeParameters; this.typeParameters = this.widgetInfo.typeParameters;
this.widgetContext.absoluteHeader = this.typeParameters.absoluteHeader;
if (!this.widgetType) { if (!this.widgetType) {
this.widgetTypeInstance = {}; this.widgetTypeInstance = {};

2
ui-ngx/src/app/modules/home/menu/side-menu.component.scss

@ -49,7 +49,7 @@
&.tb-active { &.tb-active {
background-color: rgba(255, 255, 255, .15); background-color: rgba(255, 255, 255, .15);
} }
mat-icon { .mat-icon {
margin-right: 8px; margin-right: 8px;
margin-left: 0; margin-left: 0;
min-width: 1.125rem; min-width: 1.125rem;

2
ui-ngx/src/app/modules/home/models/widget-component.models.ts

@ -265,6 +265,8 @@ export class WidgetContext {
hiddenData?: Array<{data: DataSet}>; hiddenData?: Array<{data: DataSet}>;
timeWindow?: WidgetTimewindow; timeWindow?: WidgetTimewindow;
absoluteHeader?: boolean;
hideTitlePanel = false; hideTitlePanel = false;
widgetTitle?: string; widgetTitle?: string;

4
ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.scss

@ -117,7 +117,7 @@
min-height: 32px; min-height: 32px;
padding: 6px; padding: 6px;
line-height: 20px; line-height: 20px;
mat-icon { .mat-icon {
width: 20px; width: 20px;
min-width: 20px; min-width: 20px;
height: 20px; height: 20px;
@ -216,7 +216,7 @@
cursor: pointer; cursor: pointer;
box-sizing: border-box; box-sizing: border-box;
mat-icon{ .mat-icon{
width: 16px; width: 16px;
min-width: 16px; min-width: 16px;
height: 16px; height: 16px;

2
ui-ngx/src/app/modules/home/pages/rulechain/rulenode.component.scss

@ -86,7 +86,7 @@
background-color: #a3eaa9; background-color: #a3eaa9;
} }
mat-icon, img { .mat-icon, img {
margin: auto; margin: auto;
width: 20px; width: 20px;
min-width: 20px; min-width: 20px;

2
ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.scss

@ -185,7 +185,7 @@ mat-toolbar.tb-edit-toolbar {
white-space: nowrap; white-space: nowrap;
height: 28px; height: 28px;
mat-icon { .mat-icon {
height: 20px; height: 20px;
width: 20px; width: 20px;
font-size: 20px; font-size: 20px;

6
ui-ngx/src/app/shared/components/fab-toolbar.component.scss

@ -74,7 +74,7 @@ mat-fab-toolbar {
button.mat-mdc-fab { button.mat-mdc-fab {
overflow: visible !important; overflow: visible !important;
opacity: .5; opacity: .5;
mat-icon { .mat-icon {
position: relative; position: relative;
z-index: $z-index-fab + 2; z-index: $z-index-fab + 2;
opacity: 1; opacity: 1;
@ -146,7 +146,7 @@ mat-fab-toolbar {
box-shadow: none; box-shadow: none;
opacity: 1; opacity: 1;
mat-icon { .mat-icon {
opacity: 0; opacity: 0;
} }
} }
@ -163,7 +163,7 @@ mat-fab-toolbar {
mat-fab-trigger { mat-fab-trigger {
button.mat-mdc-fab { button.mat-mdc-fab {
transition: opacity .3s cubic-bezier(.55, 0, .55, .2) .2s; transition: opacity .3s cubic-bezier(.55, 0, .55, .2) .2s;
mat-icon { .mat-icon {
transition: all $icon-delay ease-in; transition: all $icon-delay ease-in;
} }
} }

4
ui-ngx/src/app/shared/components/time/history-selector/history-selector.component.scss

@ -51,7 +51,7 @@
margin: 2px; margin: 2px;
line-height: 24px; line-height: 24px;
mat-icon { .mat-icon {
width: 24px; width: 24px;
height: 24px; height: 24px;
@ -93,7 +93,7 @@
margin: 0; margin: 0;
line-height: 28px; line-height: 28px;
mat-icon { .mat-icon {
width: 24px; width: 24px;
height: 24px; height: 24px;
font-size: 24px; font-size: 24px;

2
ui-ngx/src/app/shared/components/unit-input.component.html

@ -15,7 +15,7 @@
limitations under the License. limitations under the License.
--> -->
<mat-form-field appearance="outline" class="tb-inline-field tb-suffix-show-on-hover" subscriptSizing="dynamic" style="width: 100%;"> <mat-form-field fxFlex appearance="outline" class="tb-inline-field tb-suffix-show-on-hover" subscriptSizing="dynamic">
<input matInput #unitInput [formControl]="unitsFormControl" <input matInput #unitInput [formControl]="unitsFormControl"
placeholder="{{ 'widget-config.set' | translate }}" placeholder="{{ 'widget-config.set' | translate }}"
(focusin)="onFocus()" (focusin)="onFocus()"

19
ui-ngx/src/app/shared/components/unit-input.component.ts

@ -14,16 +14,23 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; import {
Component,
ElementRef,
forwardRef,
HostBinding,
Input,
OnInit,
ViewChild,
ViewEncapsulation
} from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, FormBuilder, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Observable, of, shareReplay, switchMap } from 'rxjs'; import { Observable, of, shareReplay, switchMap } from 'rxjs';
import { searchUnits, Unit, unitBySymbol } from '@shared/models/unit.models'; import { getUnits, searchUnits, Unit, unitBySymbol } from '@shared/models/unit.models';
import { map, mergeMap, tap } from 'rxjs/operators'; import { map, mergeMap, tap } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { ResourcesService } from '@core/services/resources.service'; import { ResourcesService } from '@core/services/resources.service';
const unitsModels = '/assets/model/units.json';
@Component({ @Component({
selector: 'tb-unit-input', selector: 'tb-unit-input',
templateUrl: './unit-input.component.html', templateUrl: './unit-input.component.html',
@ -39,6 +46,8 @@ const unitsModels = '/assets/model/units.json';
}) })
export class UnitInputComponent implements ControlValueAccessor, OnInit { export class UnitInputComponent implements ControlValueAccessor, OnInit {
@HostBinding('style.display') get hostDisplay() {return 'flex';};
unitsFormControl: FormControl; unitsFormControl: FormControl;
modelValue: string | null; modelValue: string | null;
@ -147,7 +156,7 @@ export class UnitInputComponent implements ControlValueAccessor, OnInit {
private unitsConstant(): Observable<Array<Unit>> { private unitsConstant(): Observable<Array<Unit>> {
if (this.fetchUnits$ === null) { if (this.fetchUnits$ === null) {
this.fetchUnits$ = this.resourcesService.loadJsonResource<Array<Unit>>(unitsModels).pipe( this.fetchUnits$ = getUnits(this.resourcesService).pipe(
map(units => units.map(u => ({ map(units => units.map(u => ({
symbol: u.symbol, symbol: u.symbol,
name: this.translate.instant(u.name), name: this.translate.instant(u.name),

2
ui-ngx/src/app/shared/components/user-menu.component.scss

@ -36,7 +36,7 @@
} }
mat-icon.tb-mini-avatar { .mat-icon.tb-mini-avatar {
width: 36px; width: 36px;
height: 36px; height: 36px;
margin: auto 8px; margin: auto 8px;

6
ui-ngx/src/app/shared/models/unit.models.ts

@ -14,6 +14,9 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { ResourcesService } from '@core/services/resources.service';
import { Observable } from 'rxjs';
export interface Unit { export interface Unit {
name: string; name: string;
symbol: string; symbol: string;
@ -30,3 +33,6 @@ export const searchUnits = (_units: Array<Unit>, searchText: string): Array<Unit
u.name.toUpperCase().includes(searchText.toUpperCase()) || u.name.toUpperCase().includes(searchText.toUpperCase()) ||
searchUnitTags(u, searchText) searchUnitTags(u, searchText)
); );
export const getUnits = (resourcesService: ResourcesService): Observable<Array<Unit>> =>
resourcesService.loadJsonResource('/assets/metadata/units.json');

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

@ -19,7 +19,6 @@ import { TenantId } from '@shared/models/id/tenant-id';
import { WidgetTypeId } from '@shared/models/id/widget-type-id'; import { WidgetTypeId } from '@shared/models/id/widget-type-id';
import { AggregationType, ComparisonDuration, Timewindow } from '@shared/models/time/time.models'; import { AggregationType, ComparisonDuration, Timewindow } from '@shared/models/time/time.models';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { AlarmSearchStatus, AlarmSeverity } from '@shared/models/alarm.models';
import { DataKeyType } from './telemetry/telemetry.models'; import { DataKeyType } from './telemetry/telemetry.models';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import * as moment_ from 'moment'; import * as moment_ from 'moment';
@ -40,6 +39,7 @@ import { Observable } from 'rxjs';
import { Dashboard } from '@shared/models/dashboard.models'; import { Dashboard } from '@shared/models/dashboard.models';
import { IAliasController } from '@core/api/widget-api.models'; import { IAliasController } from '@core/api/widget-api.models';
import { isEmptyStr } from '@core/utils'; import { isEmptyStr } from '@core/utils';
import { WidgetConfigComponentData } from '@home/models/widget-component.models';
export enum widgetType { export enum widgetType {
timeseries = 'timeseries', timeseries = 'timeseries',
@ -182,6 +182,7 @@ export interface WidgetTypeParameters {
processNoDataByWidget?: boolean; processNoDataByWidget?: boolean;
previewWidth?: string; previewWidth?: string;
previewHeight?: string; previewHeight?: string;
absoluteHeader?: boolean;
} }
export interface WidgetControllerDescriptor { export interface WidgetControllerDescriptor {
@ -706,6 +707,7 @@ export interface IWidgetSettingsComponent {
aliasController: IAliasController; aliasController: IAliasController;
dashboard: Dashboard; dashboard: Dashboard;
widget: Widget; widget: Widget;
widgetConfig: WidgetConfigComponentData;
functionScopeVariables: string[]; functionScopeVariables: string[];
settings: WidgetSettings; settings: WidgetSettings;
settingsChanged: Observable<WidgetSettings>; settingsChanged: Observable<WidgetSettings>;
@ -737,6 +739,17 @@ export abstract class WidgetSettingsComponent extends PageComponent implements
widget: Widget; widget: Widget;
widgetConfigValue: WidgetConfigComponentData;
set widgetConfig(value: WidgetConfigComponentData) {
this.widgetConfigValue = value;
this.onWidgetConfigSet(value);
}
get widgetConfig(): WidgetConfigComponentData {
return this.widgetConfigValue;
}
functionScopeVariables: string[]; functionScopeVariables: string[];
settingsValue: WidgetSettings; settingsValue: WidgetSettings;
@ -848,4 +861,7 @@ export abstract class WidgetSettingsComponent extends PageComponent implements
return {}; return {};
} }
protected onWidgetConfigSet(widgetConfig: WidgetConfigComponentData) {
}
} }

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

@ -4712,6 +4712,7 @@
"advanced-widget-style": "Advanced widget style", "advanced-widget-style": "Advanced widget style",
"card-buttons": "Card buttons", "card-buttons": "Card buttons",
"show-card-buttons": "Show card buttons", "show-card-buttons": "Show card buttons",
"card-border-radius": "Card border radius",
"card-appearance": "Card appearance", "card-appearance": "Card appearance",
"color": "Color" "color": "Color"
}, },
@ -4724,6 +4725,18 @@
"invalid-widget-type-file-error": "Unable to import widget type: Invalid widget type data structure." "invalid-widget-type-file-error": "Unable to import widget type: Invalid widget type data structure."
}, },
"widgets": { "widgets": {
"background": {
"background": "Background",
"background-settings": "Background settings",
"background-type-image": "Upload image",
"background-type-image-url": "Image URL",
"background-type-color": "Solid color",
"image-url": "Image URL",
"overlay": "Overlay",
"enable-overlay": "Enable overlay",
"blur": "Blur",
"preview": "Preview"
},
"chart": { "chart": {
"common-settings": "Common settings", "common-settings": "Common settings",
"enable-stacking-mode": "Enable stacking mode", "enable-stacking-mode": "Enable stacking mode",
@ -5687,7 +5700,8 @@
"label": "Label", "label": "Label",
"icon": "Icon", "icon": "Icon",
"value": "Value", "value": "Value",
"date": "Date" "date": "Date",
"value-card-style": "Value card style"
}, },
"table": { "table": {
"common-table-settings": "Common Table Settings", "common-table-settings": "Common Table Settings",

0
ui-ngx/src/assets/model/units.json → ui-ngx/src/assets/metadata/units.json

25
ui-ngx/src/styles.scss

@ -306,10 +306,7 @@ pre.tb-highlight {
.tb-fullscreen { .tb-fullscreen {
position: fixed !important; position: fixed !important;
top: 0; inset: 0 !important;
left: 0;
width: 100% !important;
height: 100% !important;
} }
.tb-fullscreen-parent { .tb-fullscreen-parent {
@ -983,10 +980,7 @@ mat-label {
min-width: 100%; min-width: 100%;
max-width: none !important; max-width: none !important;
position: absolute !important; position: absolute !important;
top: 0; inset: 0;
bottom: 0;
left: 0;
right: 0;
.mat-mdc-dialog-container { .mat-mdc-dialog-container {
> *:first-child, form { > *:first-child, form {
min-width: 100% !important; min-width: 100% !important;
@ -1004,10 +998,7 @@ mat-label {
min-width: 100%; min-width: 100%;
max-width: none !important; max-width: none !important;
position: absolute !important; position: absolute !important;
top: 0; inset: 0;
bottom: 0;
left: 0;
right: 0;
.mat-mdc-dialog-container { .mat-mdc-dialog-container {
> *:first-child, form { > *:first-child, form {
min-width: 100% !important; min-width: 100% !important;
@ -1022,10 +1013,7 @@ mat-label {
.tb-absolute-fill { .tb-absolute-fill {
position: absolute; position: absolute;
top: 0; inset: 0;
bottom: 0;
left: 0;
right: 0;
} }
.tb-layout-fill { .tb-layout-fill {
@ -1037,10 +1025,7 @@ mat-label {
.tb-progress-cover { .tb-progress-cover {
position: absolute; position: absolute;
top: 0; inset: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 6; z-index: 6;
background-color: #eee; background-color: #eee;
opacity: 1; opacity: 1;

2
ui-ngx/src/theme.scss

@ -212,7 +212,7 @@ $tb-dark-theme: map_merge($tb-dark-theme, $color);
&.mat-primary { &.mat-primary {
@include _mat-toolbar-inverse-color($primary); @include _mat-toolbar-inverse-color($primary);
button.mat-mdc-icon-button { button.mat-mdc-icon-button {
mat-icon { .mat-icon {
color: mat.get-color-from-palette($primary); color: mat.get-color-from-palette($primary);
} }
} }

Loading…
Cancel
Save