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": [],
"templateHtml": "<tb-alarms-table-widget \n [ctx]=\"ctx\">\n</tb-alarms-table-widget>",
"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": "",
"dataKeySettingsSchema": "",
"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 FROM attribute_kv;
RAISE NOTICE 'Migrated % of % rows', row_num, row_num_old;
IF row_num != 0 THEN
DROP TABLE IF EXISTS attribute_kv_old;
ELSE
RAISE EXCEPTION 'Table attribute_kv is empty';
END IF;
DROP TABLE IF EXISTS attribute_kv_old;
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;
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);
if (ruleChain != null && RuleChainType.CORE.equals(ruleChain.getType())) {
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;
for (RuleNode ruleNode : ruleNodeList) {
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())) {
ruleChainName = ruleChain.getName();
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) {
RuleNodeCtx existing = nodeActors.get(ruleNode.getId());
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);
if (exchange.getRequestOptions().getBlock2() != null) {
int chunkSize = exchange.getRequestOptions().getBlock2().getSzx();
int blockNum = exchange.getRequestOptions().getBlock2().getNum();
boolean lastFlag = data.length <= chunkSize;
response.getOptions().setBlock2(chunkSize, lastFlag, 0);
response.getOptions().setBlock2(chunkSize, lastFlag, blockNum);
}
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(), null, event.getTypeGroup(), EntitySearchDirection.TO));
cache.evict(keys);
log.debug("Processed evict event: {}", event);
}
@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.RequiredArgsConstructor;
import lombok.ToString;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
@RequiredArgsConstructor
@ToString
public class EntityRelationEvent {
@Getter
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<RuleNode> existingRuleNodes = getRuleChainNodes(tenantId, ruleChainMetaData.getRuleChainId());
for (RuleNode existingNode : existingRuleNodes) {
relationService.deleteEntityRelations(tenantId, existingNode.getId());
Integer index = ruleNodeIndexMap.get(existingNode.getId());
RuleNode newRuleNode = 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);
TenantId tenantId = savedTenant.getId();
publishEvictEvent(new TenantEvictEvent(tenantId, create));
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId)
.entityId(tenantId).entity(savedTenant).created(create).build());
if (create) {
deviceProfileService.createDefaultDeviceProfile(tenantId);
@ -147,6 +145,9 @@ public class TenantServiceImpl extends AbstractCachedEntityService<TenantId, Ten
defaultEntitiesCreator.accept(tenantId);
}
}
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId)
.entityId(tenantId).entity(savedTenant).created(create).build());
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)
public class TbMathNodeTest {
static final int RULE_DISPATCHER_POOL_SIZE = 2;
static final int DB_CALLBACK_POOL_SIZE = 3;
static final int RULE_DISPATCHER_POOL_SIZE = 3;
static final int DB_CALLBACK_POOL_SIZE = 4;
static final long TIMEOUT = TimeUnit.SECONDS.toMillis(5);
private final EntityId originator = DeviceId.fromString("ccd71696-0586-422d-940e-755a41ec3b0d");
private final TenantId tenantId = TenantId.fromUUID(UUID.fromString("e7f46b23-0c7d-42f5-9b06-fc35ab17af8a"));
@Mock(lenient = true)
@Mock(strictness = Mock.Strictness.LENIENT)
private TbContext ctx;
@Mock
private AttributesService attributesService;
@ -122,8 +122,9 @@ public class TbMathNodeTest {
@AfterEach
public void after() {
ruleEngineDispatcherExecutor.executor().shutdownNow();
dbCallbackExecutor.executor().shutdownNow();
// shutdownNow makes some tests flaky
ruleEngineDispatcherExecutor.destroy();
dbCallbackExecutor.destroy();
}
private TbMathNode initNode(TbRuleNodeMathFunctionType operation, TbMathResult result, TbMathArgument... arguments) {
@ -538,7 +539,7 @@ public class TbMathNodeTest {
node.onMsg(ctx, msg);
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());
}
@ -553,7 +554,7 @@ public class TbMathNodeTest {
node.onMsg(ctx, msg);
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());
}
@ -669,11 +670,11 @@ public class TbMathNodeTest {
// submit slow msg may block all rule engine dispatcher threads
slowMsgList.forEach(msg -> ruleEngineDispatcherExecutor.executeAsync(() -> node.onMsg(ctx, msg)));
// 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();
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());
}
@ -713,7 +714,7 @@ public class TbMathNodeTest {
.toList();
ctxNodes.forEach(ctxNode -> ruleEngineDispatcherExecutor.executeAsync(() -> ctxNode.getRight()
.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();
SoftAssertions softly = new SoftAssertions();
@ -721,7 +722,7 @@ public class TbMathNodeTest {
final TbContext ctx = ctxNode.getLeft();
final String resultKey = ctxNode.getMiddle();
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();
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'));
this.columns.push(
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)));
this.columns.push(
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">
<tb-dashboard-layout style="height: 100%;"
[dashboardCheatSheet]="cheatSheetComponent"
[layoutCtx]="layouts.right.layoutCtx"
[layoutCtx]="mobileDisplayRightLayoutFirst ? layouts.main.layoutCtx : layouts.right.layoutCtx"
[dashboardCtx]="dashboardCtx"
[isEdit]="isEdit"
[isEditingWidget]="isEditingWidget"
@ -312,7 +312,7 @@
height: mainLayoutSize.height}">
<tb-dashboard-layout
[dashboardCheatSheet]="cheatSheetComponent"
[layoutCtx]="layouts.main.layoutCtx"
[layoutCtx]="mobileDisplayRightLayoutFirst ? layouts.right.layoutCtx : layouts.main.layoutCtx"
[dashboardCtx]="dashboardCtx"
[isEdit]="isEdit"
[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) {
}
get mobileDisplayRightLayoutFirst(): boolean {
return this.isMobile && this.layouts.right.layoutCtx.gridSettings?.mobileDisplayLayoutFirst;
}
set mobileDisplayRightLayoutFirst(mobileDisplayRightLayoutFirst: boolean) {
}
@ViewChild('tbEditWidget') editWidgetComponent: EditWidgetComponent;
@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-form-field>
</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>
<mat-slide-toggle fxFlex formControlName="mobileAutoFillHeight">
{{ 'dashboard.autofill-height' | translate }}
<mat-slide-toggle *ngIf="isRightLayout" fxFlex formControlName="mobileDisplayLayoutFirst" style="display: block;">
{{ 'dashboard.display-first-in-mobile-view' | translate }}
</mat-slide-toggle>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>dashboard.mobile-row-height</mat-label>
<input matInput formControlName="mobileRowHeight" type="number" step="any" min="5"
max="200" required>
<mat-error *ngIf="gridSettingsFormGroup.get('mobileRowHeight').hasError('required')">
{{ 'dashboard.mobile-row-height-required' | translate }}
</mat-error>
<mat-error *ngIf="gridSettingsFormGroup.get('mobileRowHeight').hasError('min')">
{{ 'dashboard.min-mobile-row-height-message' | translate }}
</mat-error>
<mat-error *ngIf="gridSettingsFormGroup.get('mobileRowHeight').hasError('max')">
{{ 'dashboard.max-mobile-row-height-message' | translate }}
</mat-error>
</mat-form-field>
<div fxLayout="column" fxLayout.gt-sm="row" fxLayoutAlign.gt-sm="start center" fxLayoutGap="8px" >
<mat-slide-toggle fxFlex formControlName="mobileAutoFillHeight">
{{ 'dashboard.autofill-height' | translate }}
</mat-slide-toggle>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>dashboard.mobile-row-height</mat-label>
<input matInput formControlName="mobileRowHeight" type="number" step="any" min="5"
max="200" required>
<mat-error *ngIf="gridSettingsFormGroup.get('mobileRowHeight').hasError('required')">
{{ 'dashboard.mobile-row-height-required' | translate }}
</mat-error>
<mat-error *ngIf="gridSettingsFormGroup.get('mobileRowHeight').hasError('min')">
{{ 'dashboard.min-mobile-row-height-message' | translate }}
</mat-error>
<mat-error *ngIf="gridSettingsFormGroup.get('mobileRowHeight').hasError('max')">
{{ 'dashboard.max-mobile-row-height-message' | translate }}
</mat-error>
</mat-form-field>
</div>
</fieldset>
</div>
</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 {
settings?: DashboardSettings;
gridSettings?: GridSettings;
isRightLayout?: boolean;
}
@Component({
@ -43,6 +44,7 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
settings: DashboardSettings;
gridSettings: GridSettings;
isRightLayout = false;
settingsFormGroup: FormGroup;
gridSettingsFormGroup: FormGroup;
@ -69,6 +71,7 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
this.settings = this.data.settings;
this.gridSettings = this.data.gridSettings;
this.isRightLayout = this.data.isRightLayout;
if (this.settings) {
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,
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(
(mobileAutoFillHeightValue: boolean) => {
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'],
data: {
settings: null,
gridSettings
gridSettings,
isRightLayout: this.layoutsFormGroup.get('right').value && layoutId === 'right'
}
}).afterClosed().subscribe((data) => {
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 { Observable, of, Subject } from 'rxjs';
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 { DeviceProfileService } from '@core/http/device-profile.service';
@ -121,22 +121,30 @@ export class KeyFilterDialogComponent extends
this.keyFilterFormGroup.get('valueType').valueChanges.pipe(
takeUntil(this.destroy$)
).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;
if (prevValue && prevValue !== valueType && predicates && predicates.length) {
this.dialogs.confirm(this.translate.instant('filter.key-value-type-change-title'),
this.translate.instant('filter.key-value-type-change-message')).subscribe(
(result) => {
if (result) {
this.keyFilterFormGroup.get('predicates').setValue([]);
} else {
this.keyFilterFormGroup.get('valueType').setValue(prevValue, {emitEvent: false});
const value = this.keyFilterFormGroup.get('value')?.value;
if (prevValueType && prevValueType !== valueType) {
if (this.isConstantKeyType && this.data.telemetryKeysOnly) {
this.keyFilterFormGroup.get('value').setValue(null);
}
if (predicates && predicates.length) {
this.dialogs.confirm(this.translate.instant('filter.key-value-type-change-title'),
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').setValue(isBoolean(value) ? value : false);
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>
</fieldset>
<mat-slide-toggle class="mat-slider"
formControlName="compositeOperationsSupport">{{ 'device-profile.lwm2m.composite-operations-support' | translate }}</mat-slide-toggle>
<!-- <mat-accordion multi="true">-->
<!-- <div *ngIf="false">-->
<!-- <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],
edrxCycle: [{disabled: true, value: 0}, Validators.required],
psmActivityTimer: [{disabled: true, value: 0}, Validators.required],
pagingTransmissionWindow: [{disabled: true, value: 0}, Validators.required],
compositeOperationsSupport: [false]
pagingTransmissionWindow: [{disabled: true, value: 0}, Validators.required]
})
});
@ -275,8 +274,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro
edrxCycle: this.configurationValue.clientLwM2mSettings.edrxCycle || DEFAULT_EDRX_CYCLE,
pagingTransmissionWindow:
this.configurationValue.clientLwM2mSettings.pagingTransmissionWindow || DEFAULT_PAGING_TRANSMISSION_WINDOW,
psmActivityTimer: this.configurationValue.clientLwM2mSettings.psmActivityTimer || DEFAULT_PSM_ACTIVITY_TIMER,
compositeOperationsSupport: this.configurationValue.clientLwM2mSettings.compositeOperationsSupport || false
psmActivityTimer: this.configurationValue.clientLwM2mSettings.psmActivityTimer || DEFAULT_PSM_ACTIVITY_TIMER
}
},
{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,
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.colorSelected.subscribe((color) => {
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();
}
public onEditModeChanged() {
if (this.textSearchMode || this.enableSelection && this.alarmsDatasource.selection.hasValue()) {
this.ctx.hideTitlePanel = !this.ctx.isEdit;
this.ctx.detectChanges(true);
}
}
public pageLinkSortDirection(): SortDirection {
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);
if (this.enableSelection) {
this.alarmsDatasource.selectionModeChanged$.subscribe((selectionMode) => {
const hideTitlePanel = selectionMode || this.textSearchMode;
const hideTitlePanel = selectionMode || this.textSearchMode && !this.ctx.isEdit;
if (this.ctx.hideTitlePanel !== hideTitlePanel) {
this.ctx.hideTitlePanel = hideTitlePanel;
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));
}
forkJoin(tasks).subscribe(_ => {
this.showToast(!this.initialConnector
? this.translate.instant('gateway.connector-created')
: this.translate.instant('gateway.connector-updated')
);
this.initialConnector = value;
this.showToast('Update Successful');
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 class="tb-legend-label"
(click)="toggleHideData(legendKey.dataIndex)"
[innerHTML]="legendKey.dataKey.label | safe: 'html'"
[ngClass]="{ 'tb-hidden-label': legendKey.dataKey.hidden, 'tb-horizontal': isHorizontal }">
{{ legendKey.dataKey.label }}
</td>
<td class="tb-legend-value" *ngIf="legendConfig.showMin === true">{{ legendData.data[legendKey.dataIndex].min }}</td>
<td class="tb-legend-value" *ngIf="legendConfig.showMax === true">{{ legendData.data[legendKey.dataIndex].max }}</td>
<td class="tb-legend-value" *ngIf="legendConfig.showAvg === true">{{ legendData.data[legendKey.dataIndex].avg }}</td>
<td class="tb-legend-value" *ngIf="legendConfig.showTotal === true">{{ legendData.data[legendKey.dataIndex].total }}</td>
<td class="tb-legend-value" *ngIf="legendConfig.showLatest === true">{{ legendData.data[legendKey.dataIndex].latest }}</td>
<td class="tb-legend-value" *ngIf="legendConfig.showMin === true"
[innerHTML]="legendData.data[legendKey.dataIndex].min | safe: 'html'"></td>
<td class="tb-legend-value" *ngIf="legendConfig.showMax === true"
[innerHTML]="legendData.data[legendKey.dataIndex].max | safe: 'html'"></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>
</ng-container>
<tr *ngIf="isRowDirection" class="tb-legend-keys" [ngClass]="{ 'tb-row-direction': !displayHeader }">
@ -47,41 +52,36 @@
<td *ngFor="let legendKey of legendKeys()">
<span class="tb-legend-line" [ngStyle]="{backgroundColor: legendKey.dataKey.color}"></span>
<span class="tb-legend-label"
[innerHTML]="legendKey.dataKey.label | safe: 'html'"
(click)="toggleHideData(legendKey.dataIndex)"
[ngClass]="{ 'tb-hidden-label': legendKey.dataKey.hidden}">
{{ legendKey.dataKey.label }}
</span>
</td>
</tr>
<tr class="tb-legend-keys" *ngIf="isRowDirection && legendConfig.showMin === true">
<td class="tb-legend-type">{{ 'legend.min' | translate }}</td>
<td class="tb-legend-value" *ngFor="let legendKey of legendKeys()">
{{ legendData.data[legendKey.dataIndex].min }}
</td>
<td class="tb-legend-value" *ngFor="let legendKey of legendKeys()"
[innerHTML]="legendData.data[legendKey.dataIndex].min | safe: 'html'"></td>
</tr>
<tr class="tb-legend-keys" *ngIf="isRowDirection && legendConfig.showMax === true">
<td class="tb-legend-type">{{ 'legend.max' | translate }}</td>
<td class="tb-legend-value" *ngFor="let legendKey of legendKeys()">
{{ legendData.data[legendKey.dataIndex].max }}
</td>
<td class="tb-legend-value" *ngFor="let legendKey of legendKeys()"
[innerHTML]="legendData.data[legendKey.dataIndex].max | safe: 'html'"></td>
</tr>
<tr class="tb-legend-keys" *ngIf="isRowDirection && legendConfig.showAvg === true">
<td class="tb-legend-type">{{ 'legend.avg' | translate }}</td>
<td class="tb-legend-value" *ngFor="let legendKey of legendKeys()">
{{ legendData.data[legendKey.dataIndex].avg }}
</td>
<td class="tb-legend-value" *ngFor="let legendKey of legendKeys()"
[innerHTML]="legendData.data[legendKey.dataIndex].avg | safe: 'html'"></td>
</tr>
<tr class="tb-legend-keys" *ngIf="isRowDirection && legendConfig.showTotal === true">
<td class="tb-legend-type">{{ 'legend.total' | translate }}</td>
<td class="tb-legend-value" *ngFor="let legendKey of legendKeys()">
{{ legendData.data[legendKey.dataIndex].total }}
</td>
<td class="tb-legend-value" *ngFor="let legendKey of legendKeys()"
[innerHTML]="legendData.data[legendKey.dataIndex].total | safe: 'html'"></td>
</tr>
<tr class="tb-legend-keys" *ngIf="isRowDirection && legendConfig.showLatest === true">
<td class="tb-legend-type">{{ 'legend.latest' | translate }}</td>
<td class="tb-legend-value" *ngFor="let legendKey of legendKeys()">
{{ legendData.data[legendKey.dataIndex].latest }}
</td>
<td class="tb-legend-value" *ngFor="let legendKey of legendKeys()"
[innerHTML]="legendData.data[legendKey.dataIndex].latest | safe: 'html'"></td>
</tr>
</tbody>
</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 }}"/>
</mat-form-field>
</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 fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<div class="tb-flex row flex-end">
<mat-form-field class="medium-width" fxFlex.lt-md appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="startSymbol">
<mat-option *ngFor="let shape of chartShapes" [value]="shape">
{{ chartShapeTranslations.get(shape) | translate }}
@ -109,16 +109,16 @@
</mat-select>
</mat-form-field>
<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"
type="number" min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}">
</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.end-symbol</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<div class="tb-flex row flex-end">
<mat-form-field class="medium-width" fxFlex.lt-md appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="endSymbol">
<mat-option *ngFor="let shape of chartShapes" [value]="shape">
{{ chartShapeTranslations.get(shape) | translate }}
@ -126,7 +126,7 @@
</mat-select>
</mat-form-field>
<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"
type="number" min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}">
</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';
.tb-threshold-settings-panel {
width: 530px;
width: 540px;
display: flex;
flex-direction: column;
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);
} else {
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,
colorClearButton: this.colorClearButton
colorClearButton: this.colorClearButton,
colorCancelButton: true
},
{},
{}, {}, true);
{}, {}, {}, false, () => {}, {padding: '12px 4px 12px 12px'});
colorPickerPopover.tbComponentRef.instance.popover = colorPickerPopover;
colorPickerPopover.tbComponentRef.instance.colorSelected.subscribe((color) => {
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-title" translate>color.color</div>
<tb-color-picker [formControl]="colorPickerControl"></tb-color-picker>
<tb-color-picker class="tb-color-picker" [formControl]="colorPickerControl"></tb-color-picker>
<div class="tb-color-picker-panel-buttons">
<button *ngIf="colorClearButton"
mat-button
@ -27,12 +26,21 @@
[disabled]="!colorPickerControl.value">
{{ 'action.clear' | translate }}
</button>
<button mat-raised-button
color="primary"
type="button"
(click)="selectColor()"
[disabled]="colorPickerControl.invalid || !colorPickerControl.dirty">
{{ 'action.select' | translate }}
</button>
<span fxFlex></span>
<div fxLayoutGap="8px">
<button *ngIf="colorCancelButton"
mat-button
type="button"
(click)="cancelColor()">
{{ '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>

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
* limitations under the License.
*/
@import "../scss/constants";
.tb-color-picker-panel {
width: 328px;
width: 342px;
display: flex;
flex-direction: column;
gap: 16px;
max-height: calc(100vh - 24px);
min-height: 100%;
@media #{$mat-sm} {
width: 578px;
}
.tb-color-picker-title {
font-size: 16px;
font-weight: 500;
@ -25,12 +31,17 @@
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.87);
}
.tb-color-picker {
padding-right: 4px;
overflow-y: scroll;
}
.tb-color-picker-panel-buttons {
height: 60px;
padding: 8px 8px 0 0;
display: flex;
flex-direction: row;
gap: 16px;
justify-content: flex-end;
justify-content: space-between;
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()
colorClearButton = false;
@Input()
@coerceBoolean()
colorCancelButton = false;
@Input()
popover: TbPopoverComponent<ColorPickerPanelComponent>;
@Output()
colorSelected = new EventEmitter<string>();
@Output()
colorCancelDialog = new EventEmitter();
colorPickerControl: UntypedFormControl;
constructor(protected store: Store<AppState>) {
@ -61,4 +68,11 @@ export class ColorPickerPanelComponent extends PageComponent implements OnInit {
clearColor() {
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.
-->
<saturation-component class="saturation-component" [hue]="control.hue" [(color)]="control.value"></saturation-component>
<div class="control-component">
<indicator-component class="indicator-component"
[colorType]="presentations[presentationControl.value]"
[color]="control.value">
</indicator-component>
<div class="hue-alpha-range">
<hue-component [(hue)]="control.hue" [(color)]="control.value"></hue-component>
<alpha-component [(color)]="control.value"></alpha-component>
</div>
</div>
<div class="color-input-block">
<mat-select class="presentation-select" [formControl]="presentationControl">
<mat-option [value]="0">HEX</mat-option>
<mat-option [value]="1">RGBA</mat-option>
<mat-option [value]="2">HSLA</mat-option>
</mat-select>
<div class="color-input" [ngSwitch]="presentations[presentationControl.value]">
<rgba-input-component *ngSwitchCase="'rgba'" label
[(color)]="control.value" [(hue)]="control.hue"></rgba-input-component>
<hsla-input-component *ngSwitchCase="'hsla'" label
[(color)]="control.value" [(hue)]="control.hue"></hsla-input-component>
<hex-input-component *ngSwitchCase="'hex'" label prefix="#" [(color)]="control.value"
[(hue)]="control.hue"></hex-input-component>
<div class="color-input-container">
<saturation-component class="saturation-component" [hue]="control.hue" [(color)]="control.value"></saturation-component>
<div class="control-input-container">
<div class="control-component">
<indicator-component class="indicator-component"
[colorType]="presentations[presentationControl.value]"
[color]="control.value">
</indicator-component>
<div class="hue-alpha-range">
<hue-component [(hue)]="control.hue" [(color)]="control.value"></hue-component>
<alpha-component [(color)]="control.value"></alpha-component>
</div>
</div>
<div class="color-input-block">
<mat-select class="presentation-select" [formControl]="presentationControl">
<mat-option [value]="0">HEX</mat-option>
<mat-option [value]="1">RGBA</mat-option>
<mat-option [value]="2">HSLA</mat-option>
</mat-select>
<div class="color-input" [ngSwitch]="presentations[presentationControl.value]">
<rgba-input-component *ngSwitchCase="'rgba'" label
[(color)]="control.value" [(hue)]="control.hue"></rgba-input-component>
<hsla-input-component *ngSwitchCase="'hsla'" label
[(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 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>

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
* limitations under the License.
*/
@import "../scss/constants";
:host {
width: 100%;
display: flex;
@ -20,10 +22,34 @@
gap: 32px;
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 {
width: 100%;
height: 238px;
min-height: 80px;
min-height: 160px;
border-radius: 8px;
@media #{$mat-sm} {
max-height: 160px;
}
}
.control-component {
@ -54,7 +80,9 @@
}
.color-input-block {
height: 56px;
display: flex;
align-items: center;
gap: 20px;
.presentation-select {
@ -74,8 +102,13 @@
.color-presets-block {
.color-presets-component {
display: flex;
flex-direction: column;
gap: 12px;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
gap: 8px;
@media #{$mat-xs} {
flex-direction: column;
}
}
}
}
@ -111,13 +144,21 @@
.color-presets-component {
.presets-row {
gap: 10px;
gap: 8px;
justify-content: space-between;
}
color-preset {
height: 20px;
width: 20px;
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 =
['#435B63', '#F44336', '#E89623', '#F5DD00', '#8BC34A', '#4CAF50', '#009688', '#048AD3', '#673AB7', '#9C27B0', '#E91E63',
'#A1ADB1', '#F9A19B', '#FFD190', '#FFF59D', '#C5E1A4', '#A5D7A7', '#80CBC3', '#81C4E9', '#B39CDB', '#CD93D7', '#F48FB1'];
['#435B63', '#F44336', '#E89623', '#F5DD00', '#8BC34A', '#4CAF50', '#009688', '#048AD3', '#673AB7', '#9C27B0', '#E91E63', '#6F113A',
'#A1ADB1', '#F9A19B', '#FFD190', '#FFF59D', '#C5E1A4', '#A5D7A7', '#80CBC3', '#81C4E9', '#B39CDB', '#CD93D7', '#F48FB1', '#BC91A4'];
@Component({
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>
<button class="tb-close-button"
mat-icon-button
(click)="cancel()"
type="button">
<mat-icon>close</mat-icon>
</button>
<tb-color-picker-panel [color]="color"
colorCancelButton
[colorClearButton]="colorClearButton"
(colorCancelDialog)="cancel()"
(colorSelected)="selectColor($event)">
</tb-color-picker-panel>
</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
* limitations under the License.
*/
@import "../scss/constants";
:host {
.tb-close-button {
position: absolute;
top: 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 *ngIf="tbShowCloseButton" class="tb-popover-close-button" (click)="closeButtonClick($event)">×</div>
<div style="width: 100%; height: 100%;">
<div class="tb-popover-inner-content"
<div class="tb-popover-inner-content" [ngStyle]="tbPopoverInnerContentStyle"
[class.strict-position]="strictPosition">
<ng-container *ngIf="tbContent">
<ng-container *tbStringTemplateOutlet="tbContent; context: tbComponentContext">
@ -378,6 +378,7 @@ export class TbPopoverComponent<T = any> implements OnDestroy, OnInit {
tbOverlayClassName!: string;
tbOverlayStyle: { [klass: string]: any } = {};
tbPopoverInnerStyle: { [klass: string]: any } = {};
tbPopoverInnerContentStyle: { [klass: string]: any } = {};
tbBackdrop = false;
tbMouseEnterDelay?: 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',
hideOnClickOutside = true, injector?: Injector, context?: any, overlayStyle: 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);
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,
componentType: Type<T>, preferredPlacement: PopoverPreferredPlacement = 'top',
hideOnClickOutside = true, injector?: Injector, context?: any, overlayStyle: any = {},
popoverStyle: any = {}, style?: any, showCloseButton = true,
visibleFn: (visible: boolean) => void = () => {}): TbPopoverComponent<T> {
visibleFn: (visible: boolean) => void = () => {},
popoverContentStyle: any = {}): TbPopoverComponent<T> {
const component = componentRef.instance;
this.popoverWithTriggers.push({
trigger,
@ -99,6 +101,7 @@ export class TbPopoverService {
component.tbComponentContext = context;
component.tbOverlayStyle = overlayStyle;
component.tbPopoverInnerStyle = popoverStyle;
component.tbPopoverInnerContentStyle = popoverContentStyle;
component.tbComponentStyle = style;
component.tbHideOnClickOutside = hideOnClickOutside;
component.tbShowCloseButton = showCloseButton;

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

@ -59,6 +59,7 @@ export interface GridSettings {
autoFillHeight?: boolean;
mobileAutoFillHeight?: boolean;
mobileRowHeight?: number;
mobileDisplayLayoutFirst?: boolean;
layoutDimension?: LayoutDimension;
[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 { DateAgoPipe } from '@shared/pipe/date-ago.pipe';
import {
TbBreakPointsProvider,
MdLgLayoutDirective,
GtMdLgLayoutAlignDirective,
GtMdLgLayoutDirective,
GtMdLgLayoutGapDirective,
GtMdLgShowHideDirective,
MdLgLayoutAlignDirective,
MdLgLayoutDirective,
MdLgLayoutGapDirective,
MdLgShowHideDirective,
GtMdLgLayoutDirective,
GtMdLgLayoutAlignDirective,
GtMdLgLayoutGapDirective,
GtMdLgShowHideDirective
TbBreakPointsProvider
} from '@shared/layout/layout.directives';
import { ColorPickerComponent } from '@shared/components/color-picker/color-picker.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 { RuleChainSelectPanelComponent } from '@shared/components/rule-chain/rule-chain-select-panel.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) {
return markedOptionsService;
@ -416,7 +417,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
MultipleGalleryImageInputComponent,
EmbedImageDialogComponent,
ImageGalleryDialogComponent,
WidgetButtonComponent
WidgetButtonComponent,
HexInputComponent
],
imports: [
CommonModule,

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

@ -1710,8 +1710,7 @@
"step": "Pas",
"min-evaluation-period": "Període mínim d'avaluació",
"max-evaluation-period": "Període màxim d'avaluació"
},
"composite-operations-support": "Admet operacions compostes de lectura/escriptura/observació"
}
},
"snmp": {
"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",
"min-evaluation-period": "Minimální interval evaluace",
"max-evaluation-period": "Maximální interval evaluace"
},
"composite-operations-support": "Podporuje kompozitní Read/Write/Observe operace"
}
},
"snmp": {
"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.",
"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.",
"display-first-in-mobile-view": "Display first in mobile view",
"title-settings": "Title settings",
"display-title": "Display dashboard title",
"title-color": "Title color",
@ -1898,8 +1899,7 @@
"step": "Step",
"min-evaluation-period": "Minimum evaluation period",
"max-evaluation-period": "Maximum evaluation period"
},
"composite-operations-support": "Supports composite Read/Write/Observe operations"
}
},
"snmp": {
"add-communication-config": "Add communication config",
@ -2729,6 +2729,8 @@
"connectors-table-actions": "Actions",
"connectors-table-key": "Key",
"connectors-table-class": "Class",
"connector-created": "Connector created.",
"connector-updated": "Connector updated.",
"rpc-command-save-template": "Save Template",
"rpc-command-send": "Send",
"rpc-command-result": "Response",

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

@ -1835,8 +1835,7 @@
"step": "Paso",
"min-evaluation-period": "Período mínimo de evaluación",
"max-evaluation-period": "Período máximo de evaluación"
},
"composite-operations-support": "Soporta operaciones Lectura/Escritura/Observación Compuestas"
}
},
"snmp": {
"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",
"min-evaluation-period": "Minimale evaluatieperiode",
"max-evaluation-period": "Maximale evaluatieperiode"
},
"composite-operations-support": "Ondersteunt samengestelde lees-/schrijf-/observatiebewerkingen"
}
},
"snmp": {
"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",
"min-evaluation-period": "Minimum 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": {
"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": "步长",
"min-evaluation-period": "最小评估周期",
"max-evaluation-period": "最大评估周期"
},
"composite-operations-support": "支持复合读取/写入/观察操作"
}
},
"snmp": {
"add-communication-config": "添加通信配置",

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

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

Loading…
Cancel
Save