Fixed Issue of viewing Out of Sync Connector and gateway-connectors component refactoring

This commit is contained in:
mpetrov 2024-08-09 18:49:41 +03:00
parent bce943cb5b
commit 4d992bff70
3 changed files with 216 additions and 214 deletions

View File

@ -154,15 +154,15 @@
<section [formGroup]="connectorForm" class="tb-form-panel section-container flex">
<div class="tb-form-panel-title tb-flex no-flex space-between align-center">
<div class="tb-form-panel-title">
{{ initialConnector?.type ? gatewayConnectorDefaultTypes.get(initialConnector.type) : '' }}
{{ initialConnector?.type ? GatewayConnectorTypesTranslatesMap.get(initialConnector.type) : '' }}
{{ 'gateway.configuration' | translate }}
</div>
<tb-toggle-select *ngIf="initialConnector && allowBasicConfig.has(initialConnector.type)"
formControlName="mode" appearance="fill">
<tb-toggle-option [value]="connectorConfigurationModes.BASIC">
<tb-toggle-option [value]="ConnectorConfigurationModes.BASIC">
{{ 'gateway.basic' | translate }}
</tb-toggle-option>
<tb-toggle-option [value]="connectorConfigurationModes.ADVANCED">
<tb-toggle-option [value]="ConnectorConfigurationModes.ADVANCED">
{{ 'gateway.advanced' | translate }}
</tb-toggle-option>
</tb-toggle-select>
@ -173,17 +173,17 @@
gateway.select-connector
</span>
<section class="tb-form-panel section-container no-border no-padding tb-flex space-between" *ngIf="initialConnector">
<ng-container *ngIf="connectorForm.get('mode')?.value === connectorConfigurationModes.BASIC else defaultConfig">
<ng-container *ngIf="connectorForm.get('mode')?.value === ConnectorConfigurationModes.BASIC else defaultConfig">
<ng-container [ngSwitch]="initialConnector.type">
<tb-mqtt-basic-config *ngSwitchCase="connectorType.MQTT"
<tb-mqtt-basic-config *ngSwitchCase="ConnectorType.MQTT"
formControlName="basicConfig"
[generalTabContent]="generalTabContent">
</tb-mqtt-basic-config>
<tb-opc-ua-basic-config *ngSwitchCase="connectorType.OPCUA"
<tb-opc-ua-basic-config *ngSwitchCase="ConnectorType.OPCUA"
formControlName="basicConfig"
[generalTabContent]="generalTabContent">
</tb-opc-ua-basic-config>
<tb-modbus-basic-config *ngSwitchCase="connectorType.MODBUS"
<tb-modbus-basic-config *ngSwitchCase="ConnectorType.MODBUS"
formControlName="basicConfig"
[generalTabContent]="generalTabContent">
</tb-modbus-basic-config>
@ -237,7 +237,7 @@
</mat-form-field>
</div>
</div>
<div *ngIf="connectorForm.get('type').value === connectorType.CUSTOM" class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div *ngIf="connectorForm.get('type').value === ConnectorType.CUSTOM" class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.connectors-table-class</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
@ -245,7 +245,7 @@
</mat-form-field>
</div>
</div>
<div *ngIf="connectorForm.get('type').value === connectorType.GRPC" class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div *ngIf="connectorForm.get('type').value === ConnectorType.GRPC" class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.connectors-table-key</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
@ -273,7 +273,7 @@
</div>
</div>
</div>
<div *ngIf="connectorForm.get('type').value === connectorType.MQTT" class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div *ngIf="connectorForm.get('type').value === ConnectorType.MQTT" class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" formControlName="sendDataOnlyOnChange">
<mat-label tb-hint-tooltip-icon="{{ 'gateway.send-change-data-hint' | translate }}">
{{ 'gateway.send-change-data' | translate }}

View File

@ -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<ConnectorType>([
readonly ConnectorType = ConnectorType;
readonly allowBasicConfig = new Set<ConnectorType>([
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<AttributeData>;
displayedColumns = ['enabled', 'key', 'type', 'syncStatus', 'errors', 'actions'];
gatewayConnectorDefaultTypes = GatewayConnectorDefaultTypesTranslatesMap;
connectorConfigurationModes = ConnectorConfigurationModes;
connectorForm: FormGroup;
textSearchMode: boolean;
activeConnectors: Array<string>;
mode: ConnectorConfigurationModes = this.connectorConfigurationModes.BASIC;
mode: ConnectorConfigurationModes = this.ConnectorConfigurationModes.BASIC;
initialConnector: GatewayConnector;
private inactiveConnectors: Array<string>;
private attributeDataSource: AttributeDatasource;
private inactiveConnectorsDataSource: AttributeDatasource;
private serverDataSource: AttributeDatasource;
private activeData: Array<any> = [];
private inactiveData: Array<any> = [];
private sharedAttributeData: Array<AttributeData> = [];
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<void>();
private subscription: IWidgetSubscription;
private attributeUpdateSubject = new Subject<AttributeData>();
@ -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<AttributeData>([]);
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
}]));
}
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<any>[] {
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,24 +492,95 @@ 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;
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<AttributeData>([]);
}
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: [{}]
});
if (found) {
if (this.initialConnector && this.initialConnector.name.toLowerCase() === newName) {
return null;
this.connectorForm.disable();
}
return {
duplicateName: {
valid: false
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];
}
};
}
return null;
};
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 {
@ -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 {

View File

@ -118,6 +118,7 @@ export const timeseriesDeleteStrategyTranslations = new Map<TimeseriesDeleteStra
export interface AttributeData {
lastUpdateTs?: number;
skipSync?: boolean;
key: string;
value: any;
}