diff --git a/application/src/main/data/json/system/widget_types/alarms_table.json b/application/src/main/data/json/system/widget_types/alarms_table.json index 5a360bcc32..12716482c9 100644 --- a/application/src/main/data/json/system/widget_types/alarms_table.json +++ b/application/src/main/data/json/system/widget_types/alarms_table.json @@ -11,7 +11,7 @@ "resources": [], "templateHtml": "\n", "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", diff --git a/application/src/main/data/upgrade/3.6.4/schema_update.sql b/application/src/main/data/upgrade/3.6.4/schema_update.sql index de38753b6c..af354acf62 100644 --- a/application/src/main/data/upgrade/3.6.4/schema_update.sql +++ b/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 diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java index 650a396dc5..a16e6122f5 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java @@ -103,7 +103,7 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor 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 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) { diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/OtaPackageTransportResource.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/OtaPackageTransportResource.java index d6d766db0a..e82dbb3cce 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/OtaPackageTransportResource.java +++ b/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)); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java index ec6af730ea..5ac1d7ad3b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java +++ b/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 diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/EntityRelationEvent.java b/dao/src/main/java/org/thingsboard/server/dao/relation/EntityRelationEvent.java index 6e8939ec2f..830ccb60e3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/EntityRelationEvent.java +++ b/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; diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java index bebb3891cb..095cee42bf 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java @@ -197,6 +197,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC List updatedRuleNodes = new ArrayList<>(); List 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) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java index 83f1a636c9..01ea937ba4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java @@ -135,8 +135,6 @@ public class TenantServiceImpl extends AbstractCachedEntityService 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 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 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(); diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts index 252f0204bd..4a3fa5890b 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts @@ -115,7 +115,7 @@ export class AlarmTableConfig extends EntityTableConfig new DateEntityTableColumn('createdTime', 'alarm.created-time', this.datePipe, '150px')); this.columns.push( new EntityLinkTableColumn('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('type', 'alarm.type', '25%', diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html index d48cd6dfbf..e923036571 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html @@ -296,7 +296,7 @@ [(opened)]="rightLayoutOpened"> -
+
dashboard.mobile-layout - - {{ 'dashboard.autofill-height' | translate }} + + {{ 'dashboard.display-first-in-mobile-view' | translate }} - - dashboard.mobile-row-height - - - {{ 'dashboard.mobile-row-height-required' | translate }} - - - {{ 'dashboard.min-mobile-row-height-message' | translate }} - - - {{ 'dashboard.max-mobile-row-height-message' | translate }} - - +
+ + {{ 'dashboard.autofill-height' | translate }} + + + dashboard.mobile-row-height + + + {{ 'dashboard.mobile-row-height-required' | translate }} + + + {{ 'dashboard.min-mobile-row-height-message' | translate }} + + + {{ 'dashboard.max-mobile-row-height-message' | translate }} + + +
diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.ts index bc3af94683..a07c439e78 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.ts +++ b/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 { if (mobileAutoFillHeightValue) { diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.ts index 1dd8b91ca3..2735661b59 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.ts +++ b/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 { if (data && data.gridSettings) { diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts index a29bfac750..bd0e711cab 100644 --- a/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts +++ b/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(); } }); diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html index 677d45c6b0..4dc332afa1 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html @@ -91,8 +91,6 @@ - {{ 'device-profile.lwm2m.composite-operations-support' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts index b3516501cb..6d4fe85fd5 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts +++ b/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}); diff --git a/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.ts index 8c2df629de..2b0ed0657e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.ts +++ b/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(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts index 6cfb5fb406..b0a8fafd08 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts +++ b/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); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts index 21d232f2f1..859300561a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts +++ b/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); }); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/legend.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/legend.component.html index 155ae07df9..2e7e4b786c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/legend.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/legend.component.html @@ -32,14 +32,19 @@ - {{ legendKey.dataKey.label }} - {{ legendData.data[legendKey.dataIndex].min }} - {{ legendData.data[legendKey.dataIndex].max }} - {{ legendData.data[legendKey.dataIndex].avg }} - {{ legendData.data[legendKey.dataIndex].total }} - {{ legendData.data[legendKey.dataIndex].latest }} + + + + + @@ -47,41 +52,36 @@ - {{ legendKey.dataKey.label }} {{ 'legend.min' | translate }} - - {{ legendData.data[legendKey.dataIndex].min }} - + {{ 'legend.max' | translate }} - - {{ legendData.data[legendKey.dataIndex].max }} - + {{ 'legend.avg' | translate }} - - {{ legendData.data[legendKey.dataIndex].avg }} - + {{ 'legend.total' | translate }} - - {{ legendData.data[legendKey.dataIndex].total }} - + {{ 'legend.latest' | translate }} - - {{ legendData.data[legendKey.dataIndex].latest }} - + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-settings-panel.component.html index 196097e9e5..8d8e4a007f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-settings-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-settings-panel.component.html @@ -98,10 +98,10 @@ -
+
widgets.time-series-chart.threshold.start-symbol
-
- +
+ {{ chartShapeTranslations.get(shape) | translate }} @@ -109,16 +109,16 @@
widgets.time-series-chart.threshold.symbol-size
- +
-
+
widgets.time-series-chart.threshold.end-symbol
-
- +
+ {{ chartShapeTranslations.get(shape) | translate }} @@ -126,7 +126,7 @@
widgets.time-series-chart.threshold.symbol-size
- + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-settings-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-settings-panel.component.scss index b65fa6ad65..7e5c0e860d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-settings-panel.component.scss +++ b/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; diff --git a/ui-ngx/src/app/shared/components/color-input.component.ts b/ui-ngx/src/app/shared/components/color-input.component.ts index ad772ba861..706db50677 100644 --- a/ui-ngx/src/app/shared/components/color-input.component.ts +++ b/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(); diff --git a/ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.html b/ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.html index 40e6653867..6ddae212e1 100644 --- a/ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.html +++ b/ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.html @@ -16,8 +16,7 @@ -->
-
color.color
- +
- + +
+ + +
diff --git a/ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.scss b/ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.scss index d25f6b9d85..fc71d69822 100644 --- a/ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.scss +++ b/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; } } diff --git a/ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.ts b/ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.ts index 8ed2d304ae..443dd6967f 100644 --- a/ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.ts +++ b/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; @Output() colorSelected = new EventEmitter(); + @Output() + colorCancelDialog = new EventEmitter(); + colorPickerControl: UntypedFormControl; constructor(protected store: Store) { @@ -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(); + } + }} diff --git a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.html b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.html index 7940f1ec73..888c5b0b0b 100644 --- a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.html +++ b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.html @@ -15,35 +15,38 @@ limitations under the License. --> - - -
- - -
- - -
-
- -
- - HEX - RGBA - HSLA - -
- - - +
+ + +
+
+ + +
+ + +
+
+ +
+ + HEX + RGBA + HSLA + +
+ + + +
+
- +
diff --git a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss index 9aca580994..ef22ac203f 100644 --- a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss +++ b/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; + } } } } diff --git a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.ts b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.ts index 5cca12a968..100d1177cf 100644 --- a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.ts +++ b/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`, diff --git a/ui-ngx/src/app/shared/components/color-picker/hex-input.component.html b/ui-ngx/src/app/shared/components/color-picker/hex-input.component.html new file mode 100644 index 0000000000..c3faa497ce --- /dev/null +++ b/ui-ngx/src/app/shared/components/color-picker/hex-input.component.html @@ -0,0 +1,36 @@ + +
+ + {{prefixValue}} + + + + + + + % + +
diff --git a/ui-ngx/src/app/shared/components/color-picker/hex-input.component.scss b/ui-ngx/src/app/shared/components/color-picker/hex-input.component.scss new file mode 100644 index 0000000000..0f5125874c --- /dev/null +++ b/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; + } + } + + } +} + + diff --git a/ui-ngx/src/app/shared/components/color-picker/hex-input.component.ts b/ui-ngx/src/app/shared/components/color-picker/hex-input.component.ts new file mode 100644 index 0000000000..fcd91a2397 --- /dev/null +++ b/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(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); + } + } + } +} diff --git a/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.html b/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.html index 3d37d83c15..35b560bb3a 100644 --- a/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.html +++ b/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.html @@ -16,14 +16,10 @@ -->
-
diff --git a/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.scss b/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.scss index 2a4ec55f7d..e5b24d78e1 100644 --- a/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.scss +++ b/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; + } + } + } + } } diff --git a/ui-ngx/src/app/shared/components/popover.component.ts b/ui-ngx/src/app/shared/components/popover.component.ts index de2c328204..f0aa4981af 100644 --- a/ui-ngx/src/app/shared/components/popover.component.ts +++ b/ui-ngx/src/app/shared/components/popover.component.ts @@ -340,7 +340,7 @@ export class TbPopoverDirective implements OnChanges, OnDestroy, AfterViewInit {