Browse Source

Version Conflict UI implementation

pull/11365/head
mpetrov 2 years ago
parent
commit
31259a4d56
  1. 6
      ui-ngx/src/app/app.module.ts
  2. 53
      ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.html
  3. 26
      ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.scss
  4. 63
      ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.ts
  5. 10
      ui-ngx/src/app/shared/import-export/import-export.models.ts
  6. 27
      ui-ngx/src/app/shared/import-export/import-export.service.ts
  7. 63
      ui-ngx/src/app/shared/interceptors/entity-conflict.interceptor.ts
  8. 17
      ui-ngx/src/app/shared/interceptors/public-api.ts
  9. 1
      ui-ngx/src/app/shared/public-api.ts
  10. 10
      ui-ngx/src/assets/locale/locale.constant-en_US.json

6
ui-ngx/src/app/app.module.ts

@ -26,6 +26,8 @@ import { HomeModule } from '@home/home.module';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { DashboardRoutingModule } from '@modules/dashboard/dashboard-routing.module'; import { DashboardRoutingModule } from '@modules/dashboard/dashboard-routing.module';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { EntityConflictInterceptor } from '@shared/interceptors/entity-conflict.interceptor';
const routes: Routes = [ const routes: Routes = [
{ path: '**', { path: '**',
@ -55,7 +57,9 @@ export class PageNotFoundRoutingModule { }
DashboardRoutingModule, DashboardRoutingModule,
PageNotFoundRoutingModule PageNotFoundRoutingModule
], ],
providers: [], providers: [
{ provide: HTTP_INTERCEPTORS, useClass: EntityConflictInterceptor, multi: true }
],
bootstrap: [AppComponent] bootstrap: [AppComponent]
}) })
export class AppModule { } export class AppModule { }

53
ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.html

@ -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.
-->
<mat-toolbar color="primary">
<h2 class="main-label">{{ 'entity.version-conflict.label' | translate }}</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="onCancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<div mat-dialog-content>
<div class="message-container">
<span>{{ data.message }}.</span>
<span *ngIf="ExportableEntityTypes.includes(data.entityId.entityType)">
{{ 'entity.version-conflict.link' | translate:
{ entityType: (entityTypeTranslations.get(data.entityId.entityType).type | translate) }
}}
<a class="cursor-pointer" (click)="onLinkClick($event)">{{ 'entity.link' | translate }}</a>.
</span>
<span>{{ 'entity.version-conflict.message' | translate }}</span>
</div>
</div>
<div mat-dialog-actions fxLayout="row" fxLayoutAlign="end center">
<button mat-button color="primary"
type="button"
(click)="onCancel()"
cdkFocusInitial
>
{{ 'entity.version-conflict.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="submit"
(click)="onConfirm()"
>
{{ 'entity.version-conflict.overwrite' | translate }}
</button>
</div>

26
ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.scss

@ -0,0 +1,26 @@
/**
* 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.
*/
$conflict-dialog-width: 700px;
:host {
.main-label {
padding-left: 8px;
}
.message-container {
max-width: #{$conflict-dialog-width};
}
}

63
ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.ts

@ -0,0 +1,63 @@
///
/// 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 { 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';
interface EntityConflictDialogData {
message: string;
entityId: EntityId & {entityType: EntityType};
}
@Component({
selector: 'tb-entity-conflict-dialog',
templateUrl: 'entity-conflict-dialog.component.html',
styleUrls: ['./entity-conflict-dialog.component.scss'],
standalone: true,
imports: [
CommonModule,
SharedModule,
],
})
export class EntityConflictDialogComponent {
readonly ExportableEntityTypes = ExportableEntityTypes;
readonly entityTypeTranslations = entityTypeTranslations;
constructor(
@Inject(MAT_DIALOG_DATA) public data: EntityConflictDialogData,
private dialogRef: MatDialogRef<EntityConflictDialogComponent>,
private importExportService: ImportExportService,
) {}
onCancel(): void {
this.dialogRef.close(false);
}
onConfirm(): void {
this.dialogRef.close(true);
}
onLinkClick(event: MouseEvent): void {
event.preventDefault();
this.importExportService.exportEntity(this.data.entityId);
}
}

10
ui-ngx/src/app/shared/import-export/import-export.models.ts

@ -17,6 +17,16 @@
import { Widget, WidgetTypeDetails } from '@app/shared/models/widget.models'; import { Widget, WidgetTypeDetails } from '@app/shared/models/widget.models';
import { DashboardLayoutId } from '@shared/models/dashboard.models'; import { DashboardLayoutId } from '@shared/models/dashboard.models';
import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; 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 { export interface ImportWidgetResult {
widget: Widget; widget: Widget;

27
ui-ngx/src/app/shared/import-export/import-export.service.ts

@ -34,7 +34,7 @@ import {
} from '@shared/models/alias.models'; } from '@shared/models/alias.models';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { ImportDialogComponent, ImportDialogData } from '@shared/import-export/import-dialog.component'; import { ImportDialogComponent, ImportDialogData } from '@shared/import-export/import-dialog.component';
import { forkJoin, Observable, of, Subject } from 'rxjs'; import { forkJoin, Observable, of, Subject, throwError } from 'rxjs';
import { catchError, map, mergeMap, switchMap, take, tap } from 'rxjs/operators'; import { catchError, map, mergeMap, switchMap, take, tap } from 'rxjs/operators';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import { EntityService } from '@core/http/entity.service'; import { EntityService } from '@core/http/entity.service';
@ -380,6 +380,31 @@ export class ImportExportService {
}); });
} }
public exportEntity(entityId: EntityId): void {
switch (entityId.entityType) {
case EntityType.DEVICE_PROFILE:
this.exportDeviceProfile(entityId.id);
break;
case EntityType.ASSET_PROFILE:
this.exportAssetProfile(entityId.id);
break;
case EntityType.RULE_CHAIN:
this.exportRuleChain(entityId.id);
break;
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);
break;
default:
throwError(() => 'Not supported Entity Type');
}
}
private exportWidgetsBundleWithWidgetTypes(widgetsBundle: WidgetsBundle) { private exportWidgetsBundleWithWidgetTypes(widgetsBundle: WidgetsBundle) {
this.widgetService.exportBundleWidgetTypesDetails(widgetsBundle.id.id).subscribe({ this.widgetService.exportBundleWidgetTypesDetails(widgetsBundle.id.id).subscribe({
next: (widgetTypesDetails) => { next: (widgetTypesDetails) => {

63
ui-ngx/src/app/shared/interceptors/entity-conflict.interceptor.ts

@ -0,0 +1,63 @@
///
/// 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 {
HttpEvent,
HttpHandler,
HttpInterceptor,
HttpRequest,
HttpErrorResponse,
HttpStatusCode
} from '@angular/common/http';
import { Observable, throwError, of } 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 }
@Injectable()
export class EntityConflictInterceptor implements HttpInterceptor {
constructor(private dialog: MatDialog) {}
intercept(request: HttpRequest<unknown & ConflictedEntity>, next: HttpHandler): Observable<HttpEvent<unknown>> {
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 {
return throwError(() => error);
}
})
);
}
private resolveConflictRequest(request: HttpRequest<unknown & ConflictedEntity>, message: string): Observable<HttpRequest<unknown>> {
const dialogRef = this.dialog.open(EntityConflictDialogComponent, {data: {message, entityId: request.body.id}});
return dialogRef.afterClosed().pipe(
switchMap(result => {
if (result) {
request.body.version = null;
}
return of(request);
})
);
}
}

17
ui-ngx/src/app/shared/interceptors/public-api.ts

@ -0,0 +1,17 @@
///
/// 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.
///
export * from './entity-conflict.interceptor';

1
ui-ngx/src/app/shared/public-api.ts

@ -19,3 +19,4 @@ export * from './decorators/public-api';
export * from './models/public-api'; export * from './models/public-api';
export * from './pipe/public-api'; export * from './pipe/public-api';
export * from './shared.module'; export * from './shared.module';
export * from './interceptors/public-api';

10
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -2282,6 +2282,13 @@
"type-edges": "Edges", "type-edges": "Edges",
"list-of-edges": "{ count, plural, =1 {One edge} other {List of # edges} }", "list-of-edges": "{ count, plural, =1 {One edge} other {List of # edges} }",
"edge-name-starts-with": "Edges whose names start with '{{prefix}}'", "edge-name-starts-with": "Edges whose names start with '{{prefix}}'",
"version-conflict": {
"label": "Version conflict",
"message": "Do you want to cancel your changes or overwrite existing version?",
"link": "You can download your version of the {{entityType}} using this",
"overwrite": "Overwrite version",
"cancel": "Cancel changes"
},
"type-tb-resource": "Resource", "type-tb-resource": "Resource",
"type-tb-resources": "Resources", "type-tb-resources": "Resources",
"list-of-tb-resources": "{ count, plural, =1 {One resource} other {List of # resources} }", "list-of-tb-resources": "{ count, plural, =1 {One resource} other {List of # resources} }",
@ -2300,7 +2307,8 @@
"type-notification-request": "Notification request", "type-notification-request": "Notification request",
"type-notification-template": "Notification template", "type-notification-template": "Notification template",
"type-notification-templates": "Notification templates", "type-notification-templates": "Notification templates",
"list-of-notification-templates": "{ count, plural, =1 {One notification template} other {List of # notification templates} }" "list-of-notification-templates": "{ count, plural, =1 {One notification template} other {List of # notification templates} }",
"link": "link"
}, },
"entity-field": { "entity-field": {
"created-time": "Created time", "created-time": "Created time",

Loading…
Cancel
Save