Browse Source

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

pull/10716/head
Vladyslav_Prykhodko 2 years ago
parent
commit
39bdf94f4f
  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. 4
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html
  18. 2
      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. 5
      ui-ngx/src/assets/locale/locale.constant-ca_ES.json
  42. 7
      ui-ngx/src/assets/locale/locale.constant-cs_CZ.json
  43. 3
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  44. 5
      ui-ngx/src/assets/locale/locale.constant-es_ES.json
  45. 5
      ui-ngx/src/assets/locale/locale.constant-nl_BE.json
  46. 5
      ui-ngx/src/assets/locale/locale.constant-tr_TR.json
  47. 5
      ui-ngx/src/assets/locale/locale.constant-zh_CN.json
  48. 5
      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();
}
});

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

@ -93,11 +93,13 @@
</fieldset>
<fieldset class="fields-group">
<legend class="group-title" translate>device-profile.lwm2m.default-object-id</legend>
<mat-form-field fxFlex>
<mat-select formControlName="defaultObjectIDVer">
<mat-option *ngFor="let objectIDVer of objectIDVers" [value]="objectIDVer">
{{ objectIDVerTranslationMap.get(objectIDVer) | translate}}
{{ objectIDVerTranslationMap.get(objectIDVer) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</fieldset>
</section>
</ng-template>

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

@ -83,7 +83,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro
lwm2mDeviceProfileFormGroup: UntypedFormGroup;
configurationValue: Lwm2mProfileConfigModels;
objectIDVers = Object.values(ObjectIDVer);
objectIDVers = Object.values(ObjectIDVer) as ObjectIDVer[];
objectIDVerTranslationMap = ObjectIDVerTranslationMap;
sortFunction: (key: string, value: object) => object;

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,

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

@ -1710,11 +1710,6 @@
"step": "Pas",
"min-evaluation-period": "Període mínim d'avaluació",
"max-evaluation-period": "Període màxim d'avaluació"
},
"default-object-id": "Versió de l'objecte predeterminat (atribut)",
"default-object-id-ver": {
"v1-0": "\"1.0\" és el valor de la versió de l'objecte si la versió de l'objecte està buida.",
"v1-1": "\"1.1\" és el valor de la versió de l'objecte si la versió de l'objecte està buida."
}
},
"snmp": {

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

@ -1301,13 +1301,8 @@
"step": "Krok",
"min-evaluation-period": "Minimální interval evaluace",
"max-evaluation-period": "Maximální interval evaluace"
},
"default-object-id": "VVýchozí verze objektu (atribut)",
"default-object-id-ver": {
"v1-0": "\"1.0\" je hodnota verze objektu, pokud je verze objektu prázdná.",
"v1-1": "\"1.1\" je hodnota verze objektu, pokud je verze objektu prázdná."
}
},
},
"snmp": {
"add-communication-config": "Přidat konfiguraci komunikace",
"add-mapping": "Přidat mapování",

3
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",
@ -2733,6 +2734,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",

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

@ -1835,11 +1835,6 @@
"step": "Paso",
"min-evaluation-period": "Período mínimo de evaluación",
"max-evaluation-period": "Período máximo de evaluación"
},
"default-object-id": "Versión de objeto predeterminada (atributo)",
"default-object-id-ver": {
"v1-0": "\"1.0\" es el valor de la versión del objeto si la versión del objeto está vacía.",
"v1-1": "\"1.1\" es el valor de la versión del objeto si la versión del objeto está vacía."
}
},
"snmp": {

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

@ -1935,11 +1935,6 @@
"step": "Stap",
"min-evaluation-period": "Minimale evaluatieperiode",
"max-evaluation-period": "Maximale evaluatieperiode"
},
"default-object-id": "Standaard objectversie (attribuut)",
"default-object-id-ver": {
"v1-0": "\"1.0\" is de versiewaarde van het object als de versie van het object leeg is.",
"v1-1": "\"1.1\" is de versiewaarde van het object als de versie van het object leeg is."
}
},
"snmp": {

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

@ -1316,11 +1316,6 @@
"step": "Adım",
"min-evaluation-period": "Minimum değerlendirme süresi",
"max-evaluation-period": "Maksimum değerlendirme süresi"
},
"default-object-id": "Varsayılan Nesne Sürümü (Öznitelik)",
"default-object-id-ver": {
"v1-0": "\"1.0\", nesnenin sürümü boşsa nesnenin sürüm değeridir.",
"v1-1": "\"1.1\", nesnenin sürümü boşsa nesnenin sürüm değeridir."
}
},
"snmp": {

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

@ -1872,11 +1872,6 @@
"step": "步长",
"min-evaluation-period": "最小评估周期",
"max-evaluation-period": "最大评估周期"
},
"default-object-id": "默认对象版本(属性",
"default-object-id-ver": {
"v1-0": "如果对象版本为空,则“1.0”是对象版本的值。",
"v1-1": "如果对象版本为空,则“1.1”是对象版本的值。"
}
},
"snmp": {

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

@ -1510,11 +1510,6 @@
"step": "步",
"min-evaluation-period": "最小評估期間",
"max-evaluation-period": "最大評估期間"
},
"default-object-id": "預設物件版本(屬性)",
"default-object-id-ver": {
"v1-0": "如果物件的版本為空,則「1.0」是物件的版本值。",
"v1-1": "如果物件的版本為空,則「1.1」是物件的版本值。"
}
},
"snmp": {

Loading…
Cancel
Save