Major adjustments

This commit is contained in:
mpetrov 2024-08-07 16:01:08 +03:00
parent a291e1d2e3
commit 55e33d7f3d
17 changed files with 163 additions and 107 deletions

View File

@ -16,48 +16,76 @@
import { Injectable } from '@angular/core';
import {
HttpErrorResponse,
HttpEvent,
HttpHandler,
HttpInterceptor,
HttpRequest,
HttpErrorResponse,
HttpStatusCode
} from '@angular/common/http';
import { Observable, throwError, of } from 'rxjs';
import { Observable, of, throwError } from 'rxjs';
import { catchError, switchMap } from 'rxjs/operators';
import { MatDialog } from '@angular/material/dialog';
import { EntityConflictDialogComponent } from '@shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component';
import { EntityId } from '@shared/models/id/entity-id';
interface ConflictedEntity { version: number; id: EntityId }
import {
EntityConflictDialogComponent
} from '@shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component';
import { InterceptorConfigService } from '@core/services/interceptor-config.service';
import { HasId } from '@shared/models/base-data';
import { HasVersion } from '@shared/models/entity.models';
@Injectable()
export class EntityConflictInterceptor implements HttpInterceptor {
constructor(private dialog: MatDialog) {}
intercept(request: HttpRequest<unknown & ConflictedEntity>, next: HttpHandler): Observable<HttpEvent<unknown>> {
constructor(
private dialog: MatDialog,
private interceptorConfigService: InterceptorConfigService
) {}
intercept(request: HttpRequest<unknown & HasId & HasVersion>, next: HttpHandler): Observable<HttpEvent<unknown>> {
if (!request.url.startsWith('/api/')) {
return next.handle(request);
}
return next.handle(request).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === HttpStatusCode.Conflict) {
return this.resolveConflictRequest(request, error.error.message)
.pipe(switchMap(httpRequest => next.handle(httpRequest)));
} else {
if (error.status !== HttpStatusCode.Conflict) {
return throwError(() => error);
}
return this.handleConflictError(request, next, error);
})
);
}
private resolveConflictRequest(request: HttpRequest<unknown & ConflictedEntity>, message: string): Observable<HttpRequest<unknown>> {
const dialogRef = this.dialog.open(EntityConflictDialogComponent, {data: {message, entityId: request.body.id}});
private handleConflictError(
request: HttpRequest<unknown & HasId & HasVersion>,
next: HttpHandler,
error: HttpErrorResponse
): Observable<HttpEvent<unknown>> {
if (this.interceptorConfigService.getInterceptorConfig(request).ignoreVersionConflict) {
return next.handle(this.updateRequestVersion(request));
}
return dialogRef.afterClosed().pipe(
return this.openConflictDialog(request, error.error.message).pipe(
switchMap(result => {
if (result) {
request.body.version = null;
return next.handle(this.updateRequestVersion(request));
}
return of(request);
return of(null);
})
);
}
private updateRequestVersion(request: HttpRequest<unknown & HasId & HasVersion>): HttpRequest<unknown & HasId & HasVersion> {
const body = { ...request.body, version: null };
return request.clone({ body });
}
private openConflictDialog(request: HttpRequest<unknown & HasId & HasVersion>, message: string): Observable<boolean> {
const dialogRef = this.dialog.open(EntityConflictDialogComponent, {
data: { message, entity: request.body }
});
return dialogRef.afterClosed();
}
}

View File

@ -16,10 +16,9 @@
import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Observable } from 'rxjs/internal/Observable';
import { Inject, Injectable } from '@angular/core';
import { Injectable } from '@angular/core';
import { AuthService } from '@core/auth/auth.service';
import { Constants } from '@shared/models/constants';
import { InterceptorHttpParams } from './interceptor-http-params';
import { catchError, delay, finalize, mergeMap, switchMap } from 'rxjs/operators';
import { of, throwError } from 'rxjs';
import { InterceptorConfig } from './interceptor-config';
@ -30,6 +29,7 @@ import { ActionNotificationShow } from '@app/core/notification/notification.acti
import { DialogService } from '@core/services/dialog.service';
import { TranslateService } from '@ngx-translate/core';
import { parseHttpErrorMessage } from '@core/utils';
import { InterceptorConfigService } from '@core/services/interceptor-config.service';
const tmpHeaders = {};
@ -39,22 +39,19 @@ export class GlobalHttpInterceptor implements HttpInterceptor {
private AUTH_SCHEME = 'Bearer ';
private AUTH_HEADER_NAME = 'X-Authorization';
private internalUrlPrefixes = [
'/api/auth/token',
'/api/rpc'
];
private activeRequests = 0;
constructor(@Inject(Store) private store: Store<AppState>,
@Inject(DialogService) private dialogService: DialogService,
@Inject(TranslateService) private translate: TranslateService,
@Inject(AuthService) private authService: AuthService) {
}
constructor(
private store: Store<AppState>,
private dialogService: DialogService,
private translate: TranslateService,
private authService: AuthService,
private interceptorConfigService: InterceptorConfigService
) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (req.url.startsWith('/api/')) {
const config = this.getInterceptorConfig(req);
const config = this.interceptorConfigService.getInterceptorConfig(req);
this.updateLoadingState(config, true);
let observable$: Observable<HttpEvent<any>>;
if (this.isTokenBasedAuthEntryPoint(req.url)) {
@ -98,7 +95,7 @@ export class GlobalHttpInterceptor implements HttpInterceptor {
}
private handleResponseError(req: HttpRequest<any>, next: HttpHandler, errorResponse: HttpErrorResponse): Observable<HttpEvent<any>> {
const config = this.getInterceptorConfig(req);
const config = this.interceptorConfigService.getInterceptorConfig(req);
let unhandled = false;
const ignoreErrors = config.ignoreErrors;
const resendRequest = config.resendRequest;
@ -171,15 +168,6 @@ export class GlobalHttpInterceptor implements HttpInterceptor {
}
}
private isInternalUrlPrefix(url: string): boolean {
for (const index in this.internalUrlPrefixes) {
if (url.startsWith(this.internalUrlPrefixes[index])) {
return true;
}
}
return false;
}
private isTokenBasedAuthEntryPoint(url: string): boolean {
return url.startsWith('/api/') &&
!url.startsWith(Constants.entryPoints.login) &&
@ -202,19 +190,6 @@ export class GlobalHttpInterceptor implements HttpInterceptor {
}
}
private getInterceptorConfig(req: HttpRequest<any>): InterceptorConfig {
let config: InterceptorConfig;
if (req.params && req.params instanceof InterceptorHttpParams) {
config = (req.params as InterceptorHttpParams).interceptorConfig;
} else {
config = new InterceptorConfig(false, false);
}
if (this.isInternalUrlPrefix(req.url)) {
config.ignoreLoading = true;
}
return config;
}
private showError(error: string, timeout: number = 0) {
setTimeout(() => {
this.store.dispatch(new ActionNotificationShow({message: error, type: 'error'}));

View File

@ -17,5 +17,6 @@
export class InterceptorConfig {
constructor(public ignoreLoading: boolean = false,
public ignoreErrors: boolean = false,
public ignoreVersionConflict: boolean = false,
public resendRequest: boolean = false) {}
}

View File

@ -0,0 +1,53 @@
///
/// 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 { Injectable } from '@angular/core';
import { HttpRequest } from '@angular/common/http';
import { InterceptorConfig } from '@core/interceptors/interceptor-config';
import { InterceptorHttpParams } from '@core/interceptors/interceptor-http-params';
@Injectable({
providedIn: 'root'
})
export class InterceptorConfigService {
private readonly internalUrlPrefixes = [
'/api/auth/token',
'/api/rpc'
];
getInterceptorConfig(req: HttpRequest<unknown>): InterceptorConfig {
let config: InterceptorConfig;
if (req.params && req.params instanceof InterceptorHttpParams) {
config = (req.params as InterceptorHttpParams).interceptorConfig;
} else {
config = new InterceptorConfig();
}
if (this.isInternalUrlPrefix(req.url)) {
config.ignoreLoading = true;
}
return config;
}
private isInternalUrlPrefix(url: string): boolean {
for (const prefix of this.internalUrlPrefixes) {
if (url.startsWith(prefix)) {
return true;
}
}
return false;
}
}

View File

@ -27,9 +27,9 @@
<div mat-dialog-content>
<div class="message-container">
<span>{{ data.message }}.</span>
<span *ngIf="ExportableEntityTypes.includes(data.entityId.entityType)">
<span>
{{ 'entity.version-conflict.link' | translate:
{ entityType: (entityTypeTranslations.get(data.entityId.entityType).type | translate) }
{ entityType: (entityTypeTranslations.get(data.entity.id.entityType).type | translate) }
}}
<a class="cursor-pointer" (click)="onLinkClick($event)">{{ 'entity.link' | translate }}</a>.
</span>

View File

@ -18,14 +18,13 @@ import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { SharedModule } from '@shared/shared.module';
import { ImportExportService } from '@shared/import-export/import-export.service';
import { ExportableEntityTypes } from '@shared/import-export/import-export.models';
import { EntityId } from '@shared/models/id/entity-id';
import { CommonModule } from '@angular/common';
import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models';
import { entityTypeTranslations } from '@shared/models/entity-type.models';
import { EntityInfoData } from '@shared/models/entity.models';
interface EntityConflictDialogData {
message: string;
entityId: EntityId & {entityType: EntityType};
entity: EntityInfoData;
}
@Component({
@ -39,7 +38,6 @@ interface EntityConflictDialogData {
],
})
export class EntityConflictDialogComponent {
readonly ExportableEntityTypes = ExportableEntityTypes;
readonly entityTypeTranslations = entityTypeTranslations;
constructor(
@ -58,6 +56,6 @@ export class EntityConflictDialogComponent {
onLinkClick(event: MouseEvent): void {
event.preventDefault();
this.importExportService.exportEntity(this.data.entityId);
this.importExportService.exportEntity(this.data.entity);
}
}

View File

@ -17,16 +17,6 @@
import { Widget, WidgetTypeDetails } from '@app/shared/models/widget.models';
import { DashboardLayoutId } from '@shared/models/dashboard.models';
import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
import { EntityType } from '@shared/models/entity-type.models';
export const ExportableEntityTypes = [
EntityType.DEVICE_PROFILE,
EntityType.ASSET_PROFILE,
EntityType.RULE_CHAIN,
EntityType.DASHBOARD,
EntityType.WIDGET_TYPE,
EntityType.WIDGETS_BUNDLE
];
export interface ImportWidgetResult {
widget: Widget;

View File

@ -55,7 +55,7 @@ import { EntityType } from '@shared/models/entity-type.models';
import { UtilsService } from '@core/services/utils.service';
import { WidgetService } from '@core/http/widget.service';
import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
import { ImportEntitiesResultInfo, ImportEntityData } from '@shared/models/entity.models';
import { EntityInfoData, ImportEntitiesResultInfo, ImportEntityData } from '@shared/models/entity.models';
import { RequestConfig } from '@core/http/http-utils';
import { RuleChain, RuleChainImport, RuleChainMetaData, RuleChainType } from '@shared/models/rule-chain.models';
import { RuleChainService } from '@core/http/rule-chain.service';
@ -79,7 +79,7 @@ import { ImageService } from '@core/http/image.service';
import { ImageExportData, ImageResourceInfo, ImageResourceType } from '@shared/models/resource.models';
import { selectUserSettingsProperty } from '@core/auth/auth.selectors';
import { ActionPreferencesPutUserSettings } from '@core/auth/auth.actions';
import { ExportableEntity } from '@shared/models/base-data';
import { ExportableEntity, HasId } from '@shared/models/base-data';
import { EntityId } from '@shared/models/id/entity-id';
export type editMissingAliasesFunction = (widgets: Array<Widget>, isSingleWidget: boolean,
@ -380,29 +380,33 @@ export class ImportExportService {
});
}
public exportEntity(entityId: EntityId): void {
switch (entityId.entityType) {
public exportEntity(entityData: EntityInfoData): void {
let preparedData;
switch (entityData.id.entityType) {
case EntityType.DEVICE_PROFILE:
this.exportDeviceProfile(entityId.id);
break;
case EntityType.ASSET_PROFILE:
this.exportAssetProfile(entityId.id);
preparedData = this.prepareProfileExport(entityData as DeviceProfile | AssetProfile);
break;
case EntityType.RULE_CHAIN:
this.exportRuleChain(entityId.id);
break;
this.ruleChainService.getRuleChainMetadata(entityData.id.id)
.pipe(
take(1),
map((ruleChainMetaData) => {
const ruleChainExport: RuleChainImport = {
ruleChain: this.prepareRuleChain(entityData as RuleChain),
metadata: this.prepareRuleChainMetaData(ruleChainMetaData)
};
return ruleChainExport;
}))
.subscribe(ruleChainData => this.exportToPc(ruleChainData, entityData.name));
return;
case EntityType.DASHBOARD:
this.exportDashboard(entityId.id);
break;
case EntityType.WIDGET_TYPE:
this.exportWidgetType(entityId.id);
break;
case EntityType.WIDGETS_BUNDLE:
this.exportWidgetsBundle(entityId.id);
preparedData = this.prepareDashboardExport(entityData as Dashboard);
break;
default:
throwError(() => 'Not supported Entity Type');
preparedData = this.prepareExport(entityData);
}
this.exportToPc(preparedData, entityData.name);
}
private exportWidgetsBundleWithWidgetTypes(widgetsBundle: WidgetsBundle) {
@ -1133,6 +1137,9 @@ export class ImportExportService {
if (isDefined(exportedData.externalId)) {
delete exportedData.externalId;
}
if (isDefined(exportedData.version)) {
delete exportedData.version;
}
return exportedData;
}

View File

@ -22,9 +22,9 @@ import { EntitySearchQuery } from '@shared/models/relation.models';
import { AssetProfileId } from '@shared/models/id/asset-profile-id';
import { RuleChainId } from '@shared/models/id/rule-chain-id';
import { DashboardId } from '@shared/models/id/dashboard-id';
import { EntityInfoData, HasTenantId } from '@shared/models/entity.models';
import { EntityInfoData, HasTenantId, HasVersion } from '@shared/models/entity.models';
export interface AssetProfile extends BaseData<AssetProfileId>, HasTenantId, ExportableEntity<AssetProfileId> {
export interface AssetProfile extends BaseData<AssetProfileId>, HasTenantId, HasVersion, ExportableEntity<AssetProfileId> {
tenantId?: TenantId;
name: string;
description?: string;
@ -42,7 +42,7 @@ export interface AssetProfileInfo extends EntityInfoData {
defaultDashboardId?: DashboardId;
}
export interface Asset extends BaseData<AssetId>, HasTenantId, ExportableEntity<AssetId> {
export interface Asset extends BaseData<AssetId>, HasTenantId, HasVersion, ExportableEntity<AssetId> {
tenantId?: TenantId;
customerId?: CustomerId;
name: string;

View File

@ -18,9 +18,9 @@ import { CustomerId } from '@shared/models/id/customer-id';
import { ContactBased } from '@shared/models/contact-based.model';
import { TenantId } from './id/tenant-id';
import { ExportableEntity } from '@shared/models/base-data';
import { HasTenantId } from '@shared/models/entity.models';
import { HasTenantId, HasVersion } from '@shared/models/entity.models';
export interface Customer extends ContactBased<CustomerId>, HasTenantId, ExportableEntity<CustomerId> {
export interface Customer extends ContactBased<CustomerId>, HasTenantId, HasVersion, ExportableEntity<CustomerId> {
tenantId: TenantId;
title: string;
additionalInfo?: any;

View File

@ -23,9 +23,9 @@ import { Timewindow } from '@shared/models/time/time.models';
import { EntityAliases } from './alias.models';
import { Filters } from '@shared/models/query/query.models';
import { MatDialogRef } from '@angular/material/dialog';
import { HasTenantId } from '@shared/models/entity.models';
import { HasTenantId, HasVersion } from '@shared/models/entity.models';
export interface DashboardInfo extends BaseData<DashboardId>, HasTenantId, ExportableEntity<DashboardId> {
export interface DashboardInfo extends BaseData<DashboardId>, HasTenantId, HasVersion, ExportableEntity<DashboardId> {
tenantId?: TenantId;
title?: string;
image?: string;

View File

@ -22,7 +22,7 @@ import { DeviceCredentialsId } from '@shared/models/id/device-credentials-id';
import { EntitySearchQuery } from '@shared/models/relation.models';
import { DeviceProfileId } from '@shared/models/id/device-profile-id';
import { RuleChainId } from '@shared/models/id/rule-chain-id';
import { EntityInfoData, HasTenantId } from '@shared/models/entity.models';
import { EntityInfoData, HasTenantId, HasVersion } from '@shared/models/entity.models';
import { FilterPredicateValue, KeyFilter } from '@shared/models/query/query.models';
import { TimeUnit } from '@shared/models/time/time.models';
import * as _moment from 'moment';
@ -584,7 +584,7 @@ export interface DeviceProfileData {
provisionConfiguration?: DeviceProvisionConfiguration;
}
export interface DeviceProfile extends BaseData<DeviceProfileId>, HasTenantId, ExportableEntity<DeviceProfileId> {
export interface DeviceProfile extends BaseData<DeviceProfileId>, HasTenantId, HasVersion, ExportableEntity<DeviceProfileId> {
tenantId?: TenantId;
name: string;
description?: string;
@ -711,7 +711,7 @@ export interface DeviceData {
transportConfiguration: DeviceTransportConfiguration;
}
export interface Device extends BaseData<DeviceId>, HasTenantId, ExportableEntity<DeviceId> {
export interface Device extends BaseData<DeviceId>, HasTenantId, HasVersion, ExportableEntity<DeviceId> {
tenantId?: TenantId;
customerId?: CustomerId;
name: string;
@ -801,7 +801,7 @@ export const credentialTypesByTransportType = new Map<DeviceTransportType, Devic
]
);
export interface DeviceCredentials extends BaseData<DeviceCredentialsId> {
export interface DeviceCredentials extends BaseData<DeviceCredentialsId>, HasTenantId {
deviceId: DeviceId;
credentialsType: DeviceCredentialsType;
credentialsId: string;

View File

@ -22,9 +22,9 @@ import { EntitySearchQuery } from '@shared/models/relation.models';
import { RuleChainId } from '@shared/models/id/rule-chain-id';
import { BaseEventBody } from '@shared/models/event.models';
import { EventId } from '@shared/models/id/event-id';
import { HasTenantId } from '@shared/models/entity.models';
import { HasTenantId, HasVersion } from '@shared/models/entity.models';
export interface Edge extends BaseData<EdgeId>, HasTenantId {
export interface Edge extends BaseData<EdgeId>, HasTenantId, HasVersion {
tenantId?: TenantId;
customerId?: CustomerId;
name: string;

View File

@ -20,7 +20,7 @@ import { CustomerId } from '@shared/models/id/customer-id';
import { EntityViewId } from '@shared/models/id/entity-view-id';
import { EntityId } from '@shared/models/id/entity-id';
import { EntitySearchQuery } from '@shared/models/relation.models';
import { HasTenantId } from '@shared/models/entity.models';
import { HasTenantId, HasVersion } from '@shared/models/entity.models';
export interface AttributesEntityView {
cs: Array<string>;
@ -33,7 +33,7 @@ export interface TelemetryEntityView {
attributes: AttributesEntityView;
}
export interface EntityView extends BaseData<EntityViewId>, HasTenantId, ExportableEntity<EntityViewId> {
export interface EntityView extends BaseData<EntityViewId>, HasTenantId, HasVersion, ExportableEntity<EntityViewId> {
tenantId: TenantId;
customerId: CustomerId;
entityId: EntityId;

View File

@ -187,3 +187,7 @@ export const entityFields: {[fieldName: string]: EntityField} = {
export interface HasTenantId {
tenantId?: TenantId;
}
export interface HasVersion {
version?: number;
}

View File

@ -20,9 +20,9 @@ import { RuleChainId } from '@shared/models/id/rule-chain-id';
import { RuleNodeId } from '@shared/models/id/rule-node-id';
import { RuleNode, RuleNodeComponentDescriptor, RuleNodeType } from '@shared/models/rule-node.models';
import { ComponentClusteringMode, ComponentType } from '@shared/models/component-descriptor.models';
import { HasTenantId } from '@shared/models/entity.models';
import { HasTenantId, HasVersion } from '@shared/models/entity.models';
export interface RuleChain extends BaseData<RuleChainId>, HasTenantId, ExportableEntity<RuleChainId> {
export interface RuleChain extends BaseData<RuleChainId>, HasTenantId, HasVersion, ExportableEntity<RuleChainId> {
tenantId: TenantId;
name: string;
firstRuleNodeId: RuleNodeId;

View File

@ -17,9 +17,9 @@
import { BaseData, ExportableEntity } from '@shared/models/base-data';
import { TenantId } from '@shared/models/id/tenant-id';
import { WidgetsBundleId } from '@shared/models/id/widgets-bundle-id';
import { HasTenantId } from '@shared/models/entity.models';
import { HasTenantId, HasVersion } from '@shared/models/entity.models';
export interface WidgetsBundle extends BaseData<WidgetsBundleId>, HasTenantId, ExportableEntity<WidgetsBundleId> {
export interface WidgetsBundle extends BaseData<WidgetsBundleId>, HasTenantId, HasVersion, ExportableEntity<WidgetsBundleId> {
tenantId: TenantId;
alias: string;
title: string;