From 4d992bff70e50b83efea6ac500a27760f267f8a0 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 9 Aug 2024 18:49:41 +0300 Subject: [PATCH] Fixed Issue of viewing Out of Sync Connector and gateway-connectors component refactoring --- .../gateway/gateway-connectors.component.html | 20 +- .../gateway/gateway-connectors.component.ts | 409 +++++++++--------- .../models/telemetry/telemetry.models.ts | 1 + 3 files changed, 216 insertions(+), 214 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html index 5efda6811d..e6391f1782 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html @@ -154,15 +154,15 @@
- {{ initialConnector?.type ? gatewayConnectorDefaultTypes.get(initialConnector.type) : '' }} + {{ initialConnector?.type ? GatewayConnectorTypesTranslatesMap.get(initialConnector.type) : '' }} {{ 'gateway.configuration' | translate }}
- + {{ 'gateway.basic' | translate }} - + {{ 'gateway.advanced' | translate }} @@ -173,17 +173,17 @@ gateway.select-connector
- + - - - @@ -237,7 +237,7 @@
-
+
gateway.connectors-table-class
@@ -245,7 +245,7 @@
-
+
gateway.connectors-table-key
@@ -273,7 +273,7 @@
-
+
{{ 'gateway.send-change-data' | translate }} 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 a91a7c1b9f..a17ca6126e 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 @@ -81,61 +81,39 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie @Input() ctx: WidgetContext; - @Input() device: EntityId; @ViewChild('nameInput') nameInput: ElementRef; @ViewChild(MatSort, {static: false}) sort: MatSort; - pageLink: PageLink; - - connectorType = ConnectorType; - - allowBasicConfig = new Set([ + readonly ConnectorType = ConnectorType; + readonly allowBasicConfig = new Set([ ConnectorType.MQTT, ConnectorType.OPCUA, ConnectorType.MODBUS, ]); + readonly gatewayLogLevel = Object.values(GatewayLogLevel); + readonly displayedColumns = ['enabled', 'key', 'type', 'syncStatus', 'errors', 'actions']; + readonly GatewayConnectorTypesTranslatesMap = GatewayConnectorDefaultTypesTranslatesMap; + readonly ConnectorConfigurationModes = ConnectorConfigurationModes; - gatewayLogLevel = Object.values(GatewayLogLevel); - + pageLink: PageLink; dataSource: MatTableDataSource; - - displayedColumns = ['enabled', 'key', 'type', 'syncStatus', 'errors', 'actions']; - - gatewayConnectorDefaultTypes = GatewayConnectorDefaultTypesTranslatesMap; - - connectorConfigurationModes = ConnectorConfigurationModes; - connectorForm: FormGroup; - - textSearchMode: boolean; - activeConnectors: Array; - - mode: ConnectorConfigurationModes = this.connectorConfigurationModes.BASIC; - + mode: ConnectorConfigurationModes = this.ConnectorConfigurationModes.BASIC; initialConnector: GatewayConnector; private inactiveConnectors: Array; - private attributeDataSource: AttributeDatasource; - private inactiveConnectorsDataSource: AttributeDatasource; - private serverDataSource: AttributeDatasource; - private activeData: Array = []; - private inactiveData: Array = []; - private sharedAttributeData: Array = []; - private basicConfigSub: Subscription; - private jsonConfigSub: Subscription; - private subscriptionOptions: WidgetSubscriptionOptions = { callbacks: { onDataUpdated: () => this.ctx.ngZone.run(() => { @@ -146,7 +124,6 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie }) } }; - private destroy$ = new Subject(); private subscription: IWidgetSubscription; private attributeUpdateSubject = new Subject(); @@ -162,101 +139,19 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie private utils: UtilsService, private cd: ChangeDetectorRef) { super(store); - const sortOrder: SortOrder = {property: 'key', direction: Direction.ASC}; - this.pageLink = new PageLink(1000, 0, null, sortOrder); - this.attributeDataSource = new AttributeDatasource(this.attributeService, this.telemetryWsService, this.zone, this.translate); - this.inactiveConnectorsDataSource = new AttributeDatasource(this.attributeService, this.telemetryWsService, this.zone, this.translate); - this.serverDataSource = new AttributeDatasource(this.attributeService, this.telemetryWsService, this.zone, this.translate); - this.dataSource = new MatTableDataSource([]); - this.connectorForm = this.fb.group({ - mode: [ConnectorConfigurationModes.BASIC, []], - name: ['', [Validators.required, this.uniqNameRequired(), Validators.pattern(noLeadTrailSpacesRegex)]], - type: ['', [Validators.required]], - enableRemoteLogging: [false, []], - logLevel: ['', [Validators.required]], - sendDataOnlyOnChange: [false, []], - key: ['auto'], - class: [''], - configuration: [''], - configurationJson: [{}, [Validators.required]], - basicConfig: [{}] - }); - this.connectorForm.disable(); + this.initDataSources(); + this.initConnectorForm(); this.observeAttributeChange(); } ngAfterViewInit(): void { - this.connectorForm.get('type').valueChanges.pipe( - takeUntil(this.destroy$) - ).subscribe(type => { - if (type && !this.initialConnector) { - this.attributeService.getEntityAttributes(this.device, AttributeScope.CLIENT_SCOPE, - [`${type.toUpperCase()}_DEFAULT_CONFIG`], {ignoreErrors: true}).subscribe(defaultConfig=>{ - if (defaultConfig && defaultConfig.length) { - this.connectorForm.get('configurationJson').setValue( - isString(defaultConfig[0].value) ? - JSON.parse(defaultConfig[0].value) : - defaultConfig[0].value); - this.cd.detectChanges(); - } - }); - } - }); - - this.connectorForm.get('name').valueChanges.pipe( - takeUntil(this.destroy$) - ).subscribe((name) => { - if (this.connectorForm.get('type').value === ConnectorType.MQTT) { - this.connectorForm.get('basicConfig').get('broker.name')?.setValue(name); - } - }); + this.observeName(); this.dataSource.sort = this.sort; - this.dataSource.sortingDataAccessor = (data: AttributeData, sortHeaderId: string) => { - switch (sortHeaderId) { - case 'syncStatus': - return this.isConnectorSynced(data) ? 1 : 0; - - case 'enabled': - return this.activeConnectors.includes(data.key) ? 1 : 0; - - case 'errors': - const errors = this.getErrorsCount(data); - if (typeof errors === 'string') { - return this.sort.direction.toUpperCase() === Direction.DESC ? -1 : Infinity; - } - return errors; - - default: - return data[sortHeaderId] || data.value[sortHeaderId]; - } - }; - - if (this.device) { - if (this.device.id === NULL_UUID) { - return; - } - forkJoin([ - this.attributeService.getEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, ['active_connectors']), - this.attributeService.getEntityAttributes(this.device, AttributeScope.SERVER_SCOPE, ['inactive_connectors']) - ]).subscribe(attributes => { - if (attributes.length) { - this.activeConnectors = attributes[0].length ? attributes[0][0].value : []; - this.activeConnectors = isString(this.activeConnectors) ? JSON.parse(this.activeConnectors as any) : this.activeConnectors; - this.inactiveConnectors = attributes[1].length ? attributes[1][0].value : []; - this.inactiveConnectors = isString(this.inactiveConnectors) - ? JSON.parse(this.inactiveConnectors as any) - : this.inactiveConnectors; - this.updateData(true); - } else { - this.activeConnectors = []; - this.inactiveConnectors = []; - this.updateData(true); - } - }); - } + this.dataSource.sortingDataAccessor = this.getSortingDataAccessor(); + this.loadConnectors(); this.observeModeChange(); } @@ -267,57 +162,12 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie } saveConnector(): void { - const value = { ...this.connectorForm.value }; - value.configuration = camelCase(value.name) + '.json'; - delete value.basicConfig; - if (value.type !== ConnectorType.GRPC) { - delete value.key; - } - if (value.type !== ConnectorType.CUSTOM) { - delete value.class; - } - value.ts = new Date().getTime(); - const attributesToSave = [{ - key: value.name, - value - }]; - const attributesToDelete = []; + const value = this.getConnectorData(); const scope = (!this.initialConnector || this.activeConnectors.includes(this.initialConnector.name)) - ? AttributeScope.SHARED_SCOPE - : AttributeScope.SERVER_SCOPE; - let updateActiveConnectors = false; - if (this.initialConnector && this.initialConnector.name !== value.name) { - attributesToDelete.push({key: this.initialConnector.name}); - updateActiveConnectors = true; - const activeIndex = this.activeConnectors.indexOf(this.initialConnector.name); - const inactiveIndex = this.inactiveConnectors.indexOf(this.initialConnector.name); - if (activeIndex !== -1) { - this.activeConnectors.splice(activeIndex, 1); - } - if (inactiveIndex !== -1) { - this.inactiveConnectors.splice(inactiveIndex, 1); - } - } - if (!this.activeConnectors.includes(value.name) && scope === AttributeScope.SHARED_SCOPE) { - this.activeConnectors.push(value.name); - updateActiveConnectors = true; - } - if (!this.inactiveConnectors.includes(value.name) && scope === AttributeScope.SERVER_SCOPE) { - this.inactiveConnectors.push(value.name); - updateActiveConnectors = true; - } - const tasks = [this.attributeService.saveEntityAttributes(this.device, scope, attributesToSave)]; - if (updateActiveConnectors) { - tasks.push(this.attributeService.saveEntityAttributes(this.device, scope, [{ - key: scope === AttributeScope.SHARED_SCOPE ? 'active_connectors' : 'inactive_connectors', - value: scope === AttributeScope.SHARED_SCOPE ? this.activeConnectors : this.inactiveConnectors - }])); - } + ? AttributeScope.SHARED_SCOPE + : AttributeScope.SERVER_SCOPE; - if (attributesToDelete.length) { - tasks.push(this.attributeService.deleteEntityAttributes(this.device, scope, attributesToDelete)); - } - forkJoin(tasks).subscribe(_ => { + forkJoin(this.getEntityAttributeTasks(value, scope)).pipe(take(1)).subscribe(_ => { this.showToast(!this.initialConnector ? this.translate.instant('gateway.connector-created') : this.translate.instant('gateway.connector-updated') @@ -328,6 +178,69 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie }); } + private getEntityAttributeTasks(value: GatewayConnector, scope: AttributeScope): Observable[] { + const tasks = []; + const attributesToSave = [{ key: value.name, value }]; + const attributesToDelete = []; + const shouldAddToConnectorsList = !this.activeConnectors.includes(value.name) && scope === AttributeScope.SHARED_SCOPE + || !this.inactiveConnectors.includes(value.name) && scope === AttributeScope.SERVER_SCOPE; + const isNewConnector = this.initialConnector && this.initialConnector.name !== value.name; + + if (isNewConnector) { + attributesToDelete.push({ key: this.initialConnector.name }); + this.removeConnectorFromList(this.initialConnector.name, true); + this.removeConnectorFromList(this.initialConnector.name, false); + } + + if (shouldAddToConnectorsList) { + if (scope === AttributeScope.SHARED_SCOPE) { + this.activeConnectors.push(value.name); + } else { + this.inactiveConnectors.push(value.name); + } + } + + if (isNewConnector || shouldAddToConnectorsList) { + tasks.push(this.attributeService.saveEntityAttributes(this.device, scope, [{ + key: scope === AttributeScope.SHARED_SCOPE ? 'active_connectors' : 'inactive_connectors', + value: scope === AttributeScope.SHARED_SCOPE ? this.activeConnectors : this.inactiveConnectors + }])); + } + + tasks.push(this.attributeService.saveEntityAttributes(this.device, scope, attributesToSave)); + + if (attributesToDelete.length) { + tasks.push(this.attributeService.deleteEntityAttributes(this.device, scope, attributesToDelete)); + } + + return tasks; + } + + private removeConnectorFromList(connectorName: string, isActive: boolean): void { + const list = isActive? this.activeConnectors : this.inactiveConnectors; + const index = list.indexOf(connectorName); + if (index !== -1) { + list.splice(index, 1); + } + } + + private getConnectorData(): GatewayConnector { + const value = { ...this.connectorForm.value }; + value.configuration = `${camelCase(value.name)}.json`; + delete value.basicConfig; + + if (value.type !== ConnectorType.GRPC) { + delete value.key; + } + if (value.type !== ConnectorType.CUSTOM) { + delete value.class; + } + + value.ts = Date.now(); + + return value; + } + private updateData(reload: boolean = false): void { this.pageLink.sortOrder.property = this.sort.active; this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()]; @@ -349,11 +262,11 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie isConnectorSynced(attribute: AttributeData) { const connectorData = attribute.value; - if (!connectorData.ts) { + if (!connectorData.ts || attribute.skipSync) { return false; } const clientIndex = this.activeData.findIndex(data => { - const sharedData = data.value; + const sharedData = typeof data.value === 'string' ? JSON.parse(data.value) : data.value; return sharedData.name === connectorData.name; }); if (clientIndex === -1) { @@ -384,12 +297,31 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie } private combineData(): void { - this.dataSource.data = [...this.activeData, ...this.inactiveData, ...this.sharedAttributeData].filter((item, index, self) => - index === self.findIndex((t) => t.key === item.key) - ).map(attribute => { - attribute.value = typeof attribute.value === 'string' ? JSON.parse(attribute.value) : attribute.value; - return attribute; - }); + const combinedData = [ + ...this.activeData, + ...this.inactiveData, + ...this.sharedAttributeData + ]; + + const latestData = combinedData.reduce((acc, attribute) => { + const existingItemIndex = acc.findIndex(item => item.key === attribute.key); + + if (existingItemIndex === -1) { + acc.push(attribute); + } else if ( + attribute.lastUpdateTs > acc[existingItemIndex].lastUpdateTs && + !this.isConnectorSynced(acc[existingItemIndex]) + ) { + acc[existingItemIndex] = { ...attribute, skipSync: true }; + } + + return acc; + }, []); + + this.dataSource.data = latestData.map(attribute => ({ + ...attribute, + value: typeof attribute.value === 'string' ? JSON.parse(attribute.value) : attribute.value + })); } private clearOutConnectorForm(): void { @@ -447,7 +379,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie returnType(attribute: AttributeData): string { const value = attribute.value; - return this.gatewayConnectorDefaultTypes.get(value.type); + return this.GatewayConnectorTypesTranslatesMap.get(value.type); } deleteConnector(attribute: AttributeData, $event: Event): void { @@ -560,26 +492,97 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie } uniqNameRequired(): ValidatorFn { - return (c: UntypedFormControl) => { - const newName = c.value.trim().toLowerCase(); - const found = this.dataSource.data.find((connectorAttr) => { - const connectorData = connectorAttr.value; - return connectorData.name.toLowerCase() === newName; - }); - if (found) { - if (this.initialConnector && this.initialConnector.name.toLowerCase() === newName) { - return null; - } - return { - duplicateName: { - valid: false - } - }; + return (control: UntypedFormControl) => { + const newName = control.value?.trim().toLowerCase(); + const isDuplicate = this.dataSource.data.some(connectorAttr => connectorAttr.value.name.toLowerCase() === newName); + const isSameAsInitial = this.initialConnector?.name.toLowerCase() === newName; + + if (isDuplicate && !isSameAsInitial) { + return { duplicateName: { valid: false } }; } + return null; }; } + private initDataSources(): void { + const sortOrder: SortOrder = {property: 'key', direction: Direction.ASC}; + this.pageLink = new PageLink(1000, 0, null, sortOrder); + this.attributeDataSource = new AttributeDatasource(this.attributeService, this.telemetryWsService, this.zone, this.translate); + this.inactiveConnectorsDataSource = new AttributeDatasource(this.attributeService, this.telemetryWsService, this.zone, this.translate); + this.serverDataSource = new AttributeDatasource(this.attributeService, this.telemetryWsService, this.zone, this.translate); + this.dataSource = new MatTableDataSource([]); + } + + private initConnectorForm(): void { + this.connectorForm = this.fb.group({ + mode: [ConnectorConfigurationModes.BASIC], + name: ['', [Validators.required, this.uniqNameRequired(), Validators.pattern(noLeadTrailSpacesRegex)]], + type: ['', [Validators.required]], + enableRemoteLogging: [false], + logLevel: ['', [Validators.required]], + sendDataOnlyOnChange: [false], + key: ['auto'], + class: [''], + configuration: [''], + configurationJson: [{}, [Validators.required]], + basicConfig: [{}] + }); + this.connectorForm.disable(); + } + + private observeName(): void { + this.connectorForm.get('name').valueChanges + .pipe( + filter(() => this.connectorForm.get('type').value === ConnectorType.MQTT), + takeUntil(this.destroy$) + ) + .subscribe(name => this.connectorForm.get('basicConfig').get('broker.name')?.setValue(name)); + } + + private getSortingDataAccessor(): (data: AttributeData, sortHeaderId: string) => string | number { + return (data: AttributeData, sortHeaderId: string) => { + switch (sortHeaderId) { + case 'syncStatus': + return this.isConnectorSynced(data) ? 1 : 0; + + case 'enabled': + return this.activeConnectors.includes(data.key) ? 1 : 0; + + case 'errors': + const errors = this.getErrorsCount(data); + if (typeof errors === 'string') { + return this.sort.direction.toUpperCase() === Direction.DESC ? -1 : Infinity; + } + return errors; + + default: + return data[sortHeaderId] || data.value[sortHeaderId]; + } + }; + } + + private loadConnectors(): void { + if (!this.device || this.device.id === NULL_UUID) { + return; + } + + forkJoin([ + this.attributeService.getEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, ['active_connectors']), + this.attributeService.getEntityAttributes(this.device, AttributeScope.SERVER_SCOPE, ['inactive_connectors']) + ]).pipe(takeUntil(this.destroy$)).subscribe(attributes => { + this.activeConnectors = this.parseConnectors(attributes[0]); + this.inactiveConnectors = this.parseConnectors(attributes[1]); + + this.updateData(true); + }); + } + + private parseConnectors(attribute: AttributeData[]): string[] { + const connectors = attribute?.[0]?.value || []; + return isString(connectors) ? JSON.parse(connectors) : connectors; + } + private observeModeChange(): void { this.connectorForm.get('mode').valueChanges .pipe(takeUntil(this.destroy$)) @@ -636,7 +639,8 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie value: this.inactiveConnectors }]), this.attributeService.deleteEntityAttributes(this.device, scopeOld, [attribute]), - this.attributeService.saveEntityAttributes(this.device, scopeNew, [attribute])]; + this.attributeService.saveEntityAttributes(this.device, scopeNew, [attribute]) + ]; } private onDataUpdateError(e: any): void { @@ -725,20 +729,17 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie if (this.connectorForm.disabled) { this.connectorForm.enable(); } - if (!connector.configuration) { - connector.configuration = ''; - } - if (!connector.key) { - connector.key = 'auto'; - } - if (!connector.configurationJson) { - connector.configurationJson = {} as ConnectorBaseConfig; - } - connector.basicConfig = connector.configurationJson; - this.initialConnector = connector; + const connectorState = { + configuration: '', + key: 'auto', + configurationJson: {} as ConnectorBaseConfig, + ...connector, + }; - this.updateConnector(connector); + connectorState.basicConfig = connectorState.configurationJson; + this.initialConnector = connectorState; + this.updateConnector(connectorState); } private updateConnector(connector: GatewayConnector): void { diff --git a/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts b/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts index 8cf07e1128..713f629ca8 100644 --- a/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts +++ b/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts @@ -118,6 +118,7 @@ export const timeseriesDeleteStrategyTranslations = new Map