Browse Source

UI: Images import/export suppoer. Implement flow to delete images used by other entities.

pull/9542/head
Igor Kulikov 3 years ago
parent
commit
12fafa40f5
  1. 16
      ui-ngx/src/app/core/http/image.service.ts
  2. 2
      ui-ngx/src/app/modules/home/components/details-panel.component.html
  3. 2
      ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.html
  4. 6
      ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.ts
  5. 10
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  6. 8
      ui-ngx/src/app/modules/home/components/image/image-gallery.component.html
  7. 218
      ui-ngx/src/app/modules/home/components/image/image-gallery.component.ts
  8. 49
      ui-ngx/src/app/modules/home/components/image/image-references.component.html
  9. 171
      ui-ngx/src/app/modules/home/components/image/image-references.component.ts
  10. 130
      ui-ngx/src/app/modules/home/components/image/images-datasource.ts
  11. 82
      ui-ngx/src/app/modules/home/components/image/images-in-use-dialog.component.html
  12. 39
      ui-ngx/src/app/modules/home/components/image/images-in-use-dialog.component.scss
  13. 112
      ui-ngx/src/app/modules/home/components/image/images-in-use-dialog.component.ts
  14. 44
      ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts
  15. 48
      ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts
  16. 6
      ui-ngx/src/app/modules/home/pages/widget/widget-type.component.html
  17. 20
      ui-ngx/src/app/modules/home/pages/widget/widget-types-table-config.resolver.ts
  18. 6
      ui-ngx/src/app/modules/home/pages/widget/widgets-bundle.component.html
  19. 16
      ui-ngx/src/app/modules/home/pages/widget/widgets-bundles-table-config.resolver.ts
  20. 3
      ui-ngx/src/app/shared/components/popover.component.ts
  21. 11
      ui-ngx/src/app/shared/components/popover.service.ts
  22. 3
      ui-ngx/src/app/shared/models/alarm.models.ts
  23. 6
      ui-ngx/src/app/shared/models/asset.models.ts
  24. 3
      ui-ngx/src/app/shared/models/customer.model.ts
  25. 3
      ui-ngx/src/app/shared/models/dashboard.models.ts
  26. 8
      ui-ngx/src/app/shared/models/device.models.ts
  27. 3
      ui-ngx/src/app/shared/models/edge.models.ts
  28. 4
      ui-ngx/src/app/shared/models/entity-type.models.ts
  29. 3
      ui-ngx/src/app/shared/models/entity-view.models.ts
  30. 5
      ui-ngx/src/app/shared/models/entity.models.ts
  31. 9
      ui-ngx/src/app/shared/models/notification.models.ts
  32. 3
      ui-ngx/src/app/shared/models/ota-package.models.ts
  33. 3
      ui-ngx/src/app/shared/models/queue.models.ts
  34. 42
      ui-ngx/src/app/shared/models/resource.models.ts
  35. 3
      ui-ngx/src/app/shared/models/rule-chain.models.ts
  36. 3
      ui-ngx/src/app/shared/models/user.model.ts
  37. 3
      ui-ngx/src/app/shared/models/widget.models.ts
  38. 3
      ui-ngx/src/app/shared/models/widgets-bundle.model.ts
  39. 17
      ui-ngx/src/assets/locale/locale.constant-en_US.json

16
ui-ngx/src/app/core/http/image.service.ts

@ -25,7 +25,7 @@ import {
ImageResourceInfo, ImageResourceInfo,
imageResourceType, imageResourceType,
ImageResourceType, ImageResourceType,
IMAGES_URL_PREFIX, isImageResourceUrl IMAGES_URL_PREFIX, isImageResourceUrl, ImageExportData
} from '@shared/models/resource.models'; } from '@shared/models/resource.models';
import { catchError, map, switchMap } from 'rxjs/operators'; import { catchError, map, switchMap } from 'rxjs/operators';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
@ -133,8 +133,18 @@ export class ImageService {
); );
} }
public deleteImage(type: ImageResourceType, key: string, config?: RequestConfig) { public deleteImage(type: ImageResourceType, key: string, force = false, config?: RequestConfig) {
return this.http.delete(`${IMAGES_URL_PREFIX}/${type}/${encodeURIComponent(key)}`, defaultHttpOptionsFromConfig(config)); return this.http.delete(`${IMAGES_URL_PREFIX}/${type}/${encodeURIComponent(key)}?force=${force}`, defaultHttpOptionsFromConfig(config));
}
public exportImage(type: ImageResourceType, key: string, config?: RequestConfig): Observable<ImageExportData> {
return this.http.get<ImageExportData>(`${IMAGES_URL_PREFIX}/${type}/${encodeURIComponent(key)}/export`,
defaultHttpOptionsFromConfig(config));
}
public importImage(imageData: ImageExportData, config?: RequestConfig): Observable<ImageResourceInfo> {
return this.http.put<ImageResourceInfo>('/api/image/import',
imageData, defaultHttpOptionsFromConfig(config));
} }
} }

2
ui-ngx/src/app/modules/home/components/details-panel.component.html

@ -56,7 +56,7 @@
</button> </button>
<button [disabled]="(isLoading$ | async) || (isAlwaysEdit && !theForm?.dirty)" <button [disabled]="(isLoading$ | async) || (isAlwaysEdit && !theForm?.dirty)"
mat-fab mat-fab
matTooltip="{{ (isAlwaysEdit ? 'action.decline-changes' : 'details.toggle-edit-mode') | translate }}" matTooltip="{{ (isAlwaysEdit ? 'action.decline-changes' : (isEdit ? 'action.decline-changes' : 'details.toggle-edit-mode')) | translate }}"
matTooltipPosition="above" matTooltipPosition="above"
color="accent" class="tb-btn-header mat-fab-bottom-right" color="accent" class="tb-btn-header mat-fab-bottom-right"
(click)="onToggleDetailsEditMode()"> (click)="onToggleDetailsEditMode()">

2
ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.html

@ -42,7 +42,7 @@
</button> </button>
<button [disabled]="(isLoading$ | async)" <button [disabled]="(isLoading$ | async)"
mat-fab mat-fab
matTooltip="{{ 'action.decline-changes' | translate }}" matTooltip="{{ (isEdit ? 'action.decline-changes' : 'details.toggle-edit-mode') | translate }}"
matTooltipPosition="above" matTooltipPosition="above"
color="accent" class="tb-btn-header" color="accent" class="tb-btn-header"
(click)="onToggleDetailsEditMode()"> (click)="onToggleDetailsEditMode()">

6
ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.ts

@ -51,6 +51,8 @@ export class EntityDetailsPageComponent extends EntityDetailsPanelComponent impl
isReadOnly = false; isReadOnly = false;
backNavigationCommands?: any[];
set entitiesTableConfig(entitiesTableConfig: EntityTableConfig<BaseData<HasId>>) { set entitiesTableConfig(entitiesTableConfig: EntityTableConfig<BaseData<HasId>>) {
if (this.entitiesTableConfigValue !== entitiesTableConfig) { if (this.entitiesTableConfigValue !== entitiesTableConfig) {
this.entitiesTableConfigValue = entitiesTableConfig; this.entitiesTableConfigValue = entitiesTableConfig;
@ -79,6 +81,7 @@ export class EntityDetailsPageComponent extends EntityDetailsPanelComponent impl
protected store: Store<AppState>) { protected store: Store<AppState>) {
super(store, injector, cd, componentFactoryResolver); super(store, injector, cd, componentFactoryResolver);
this.entitiesTableConfig = this.route.snapshot.data.entitiesTableConfig; this.entitiesTableConfig = this.route.snapshot.data.entitiesTableConfig;
this.backNavigationCommands = this.route.snapshot.data.backNavigationCommands;
} }
ngOnInit() { ngOnInit() {
@ -145,7 +148,8 @@ export class EntityDetailsPageComponent extends EntityDetailsPanelComponent impl
} }
goBack(): void { goBack(): void {
this.router.navigate(['../'], { relativeTo: this.route }); const commands = this.backNavigationCommands || ['../'];
this.router.navigate(commands, { relativeTo: this.route });
} }
private onUpdateEntity() { private onUpdateEntity() {

10
ui-ngx/src/app/modules/home/components/home-components.module.ts

@ -185,6 +185,8 @@ import { ScrollGridComponent } from '@home/components/grid/scroll-grid.component
import { ImageGalleryComponent } from '@home/components/image/image-gallery.component'; import { ImageGalleryComponent } from '@home/components/image/image-gallery.component';
import { UploadImageDialogComponent } from '@home/components/image/upload-image-dialog.component'; import { UploadImageDialogComponent } from '@home/components/image/upload-image-dialog.component';
import { ImageDialogComponent } from '@home/components/image/image-dialog.component'; import { ImageDialogComponent } from '@home/components/image/image-dialog.component';
import { ImagesInUseDialogComponent } from '@home/components/image/images-in-use-dialog.component';
import { ImageReferencesComponent } from '@home/components/image/image-references.component';
@NgModule({ @NgModule({
declarations: declarations:
@ -333,7 +335,9 @@ import { ImageDialogComponent } from '@home/components/image/image-dialog.compon
ScrollGridComponent, ScrollGridComponent,
ImageGalleryComponent, ImageGalleryComponent,
UploadImageDialogComponent, UploadImageDialogComponent,
ImageDialogComponent ImageDialogComponent,
ImageReferencesComponent,
ImagesInUseDialogComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -475,7 +479,9 @@ import { ImageDialogComponent } from '@home/components/image/image-dialog.compon
ScrollGridComponent, ScrollGridComponent,
ImageGalleryComponent, ImageGalleryComponent,
UploadImageDialogComponent, UploadImageDialogComponent,
ImageDialogComponent ImageDialogComponent,
ImageReferencesComponent,
ImagesInUseDialogComponent
], ],
providers: [ providers: [
WidgetComponentService, WidgetComponentService,

8
ui-ngx/src/app/modules/home/components/image/image-gallery.component.html

@ -129,7 +129,7 @@
</mat-header-cell> </mat-header-cell>
<mat-cell *matCellDef="let image"> <mat-cell *matCellDef="let image">
<mat-checkbox (click)="$event.stopPropagation()" <mat-checkbox (click)="$event.stopPropagation()"
[fxShow]="!readonly(image)" [fxShow]="deleteEnabled(image)"
(change)="$event ? dataSource.selection.toggle(image) : null" (change)="$event ? dataSource.selection.toggle(image) : null"
[checked]="dataSource.selection.isSelected(image)"> [checked]="dataSource.selection.isSelected(image)">
</mat-checkbox> </mat-checkbox>
@ -194,7 +194,7 @@
(click)="editImage($event, image)"> (click)="editImage($event, image)">
<mat-icon>edit</mat-icon> <mat-icon>edit</mat-icon>
</button> </button>
<button mat-icon-button [disabled]="(isLoading$ | async) || readonly(image)" <button mat-icon-button [disabled]="(isLoading$ | async) || !deleteEnabled(image)"
matTooltip="{{ 'image.delete-image' | translate }}" matTooltip="{{ 'image.delete-image' | translate }}"
matTooltipPosition="above" matTooltipPosition="above"
(click)="deleteImage($event, image)"> (click)="deleteImage($event, image)">
@ -228,7 +228,7 @@
</button> </button>
<button mat-menu-item <button mat-menu-item
[disabled]="isLoading$ | async" [disabled]="isLoading$ | async"
[fxShow]="!readonly(image)" [fxShow]="deleteEnabled(image)"
(click)="deleteImage($event, image)"> (click)="deleteImage($event, image)">
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>
<span translate>image.delete-image</span> <span translate>image.delete-image</span>
@ -302,7 +302,7 @@
(click)="exportImage($event, item)"> (click)="exportImage($event, item)">
<tb-icon>mdi:file-export</tb-icon> <tb-icon>mdi:file-export</tb-icon>
</button> </button>
<button *ngIf="!readonly(item)" <button *ngIf="deleteEnabled(item)"
mat-icon-button [disabled]="(isLoading$ | async)" mat-icon-button [disabled]="(isLoading$ | async)"
matTooltip="{{ 'image.delete-image' | translate }}" matTooltip="{{ 'image.delete-image' | translate }}"
matTooltipPosition="above" matTooltipPosition="above"

218
ui-ngx/src/app/modules/home/components/image/image-gallery.component.ts

@ -14,14 +14,17 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { CollectionViewer, DataSource, SelectionModel } from '@angular/cdk/collections'; import {
import { ImageResourceInfo, imageResourceType } from '@shared/models/resource.models'; ImageResourceInfo,
import { BehaviorSubject, forkJoin, merge, Observable, of, ReplaySubject, Subject, Subscription } from 'rxjs'; ImageResourceInfoWithReferences,
import { emptyPageData, PageData } from '@shared/models/page/page-data'; imageResourceType,
toImageDeleteResult
} from '@shared/models/resource.models';
import { forkJoin, merge, Observable, of, Subject, Subscription } from 'rxjs';
import { ImageService } from '@core/http/image.service'; import { ImageService } from '@core/http/image.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { PageLink, PageQueryParam } from '@shared/models/page/page-link'; import { PageLink, PageQueryParam } from '@shared/models/page/page-link';
import { catchError, debounceTime, distinctUntilChanged, map, skip, take, takeUntil, tap } from 'rxjs/operators'; import { catchError, debounceTime, distinctUntilChanged, map, skip, takeUntil } from 'rxjs/operators';
import { import {
AfterViewInit, AfterViewInit,
ChangeDetectorRef, ChangeDetectorRef,
@ -45,7 +48,7 @@ import { ResizeObserver } from '@juggle/resize-observer';
import { hidePageSizePixelValue } from '@shared/models/constants'; import { hidePageSizePixelValue } from '@shared/models/constants';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { ActivatedRoute, QueryParamsHandling, Router } from '@angular/router'; import { ActivatedRoute, QueryParamsHandling, Router } from '@angular/router';
import { isEqual, isNotEmptyStr } from '@core/utils'; import { isEqual, isNotEmptyStr, parseHttpErrorMessage } from '@core/utils';
import { BaseData, HasId } from '@shared/models/base-data'; import { BaseData, HasId } from '@shared/models/base-data';
import { NULL_UUID } from '@shared/models/id/has-uuid'; import { NULL_UUID } from '@shared/models/id/has-uuid';
import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { getCurrentAuthUser } from '@core/auth/auth.selectors';
@ -58,7 +61,13 @@ import {
UploadImageDialogData UploadImageDialogData
} from '@home/components/image/upload-image-dialog.component'; } from '@home/components/image/upload-image-dialog.component';
import { ImageDialogComponent, ImageDialogData } from '@home/components/image/image-dialog.component'; import { ImageDialogComponent, ImageDialogData } from '@home/components/image/image-dialog.component';
import { EntityBooleanFunction } from '@home/models/entity/entities-table-config.models'; import { ImportExportService } from '@home/components/import-export/import-export.service';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import {
ImagesInUseDialogComponent,
ImagesInUseDialogData
} from '@home/components/image/images-in-use-dialog.component';
import { ImagesDatasource } from '@home/components/image/images-datasource';
@Component({ @Component({
selector: 'tb-image-gallery', selector: 'tb-image-gallery',
@ -132,6 +141,7 @@ export class ImageGalleryComponent extends PageComponent implements OnInit, OnDe
public translate: TranslateService, public translate: TranslateService,
private imageService: ImageService, private imageService: ImageService,
private dialogService: DialogService, private dialogService: DialogService,
private importExportService: ImportExportService,
private elementRef: ElementRef, private elementRef: ElementRef,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
private fb: FormBuilder) { private fb: FormBuilder) {
@ -177,7 +187,8 @@ export class ImageGalleryComponent extends PageComponent implements OnInit, OnDe
} }
} }
if (this.mode === 'list') { if (this.mode === 'list') {
this.dataSource = new ImagesDatasource(this.imageService, entity => !this.readonly(entity)); this.dataSource = new ImagesDatasource(this.imageService, null,
entity => this.deleteEnabled(entity));
} }
} }
@ -233,7 +244,8 @@ export class ImageGalleryComponent extends PageComponent implements OnInit, OnDe
} }
this.mode = targetMode; this.mode = targetMode;
if (this.mode === 'list') { if (this.mode === 'list') {
this.dataSource = new ImagesDatasource(this.imageService, entity => !this.readonly(entity)); this.dataSource = new ImagesDatasource(this.imageService, null,
entity => this.deleteEnabled(entity));
} }
setTimeout(() => { setTimeout(() => {
this.updateMode(); this.updateMode();
@ -381,6 +393,10 @@ export class ImageGalleryComponent extends PageComponent implements OnInit, OnDe
return this.authUser.authority !== Authority.SYS_ADMIN && this.isSystem(image); return this.authUser.authority !== Authority.SYS_ADMIN && this.isSystem(image);
} }
deleteEnabled(image?: ImageResourceInfo): boolean {
return this.authUser.authority === Authority.SYS_ADMIN || !this.isSystem(image);
}
deleteImage($event: Event, image: ImageResourceInfo, itemIndex = -1) { deleteImage($event: Event, image: ImageResourceInfo, itemIndex = -1) {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
@ -391,11 +407,38 @@ export class ImageGalleryComponent extends PageComponent implements OnInit, OnDe
this.translate.instant('action.no'), this.translate.instant('action.no'),
this.translate.instant('action.yes')).subscribe((result) => { this.translate.instant('action.yes')).subscribe((result) => {
if (result) { if (result) {
this.imageService.deleteImage(imageResourceType(image), image.resourceKey).subscribe( this.imageService.deleteImage(imageResourceType(image), image.resourceKey, false, {ignoreErrors: true}).pipe(
() => { map(() => toImageDeleteResult(image)),
this.imageDeleted(itemIndex); catchError((err) => of(toImageDeleteResult(image, err)))
} ).subscribe(
); (deleteResult) => {
if (deleteResult.success) {
this.imageDeleted(itemIndex);
} else if (deleteResult.imageIsReferencedError) {
this.dialog.open<ImagesInUseDialogComponent, ImagesInUseDialogData,
ImageResourceInfo[]>(ImagesInUseDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
multiple: false,
images: [{...image, ...{references: deleteResult.references}}]
}
}).afterClosed().subscribe((images) => {
if (images) {
this.imageService.deleteImage(imageResourceType(image), image.resourceKey, true).subscribe(
() => {
this.imageDeleted(itemIndex);
}
);
}
});
} else {
const errorMessageWithTimeout = parseHttpErrorMessage(deleteResult.error, this.translate);
setTimeout(() => {
this.store.dispatch(new ActionNotificationShow({message: errorMessageWithTimeout.message, type: 'error'}));
}, errorMessageWithTimeout.timeout);
}
});
} }
}); });
} }
@ -413,10 +456,48 @@ export class ImageGalleryComponent extends PageComponent implements OnInit, OnDe
this.translate.instant('action.yes')).subscribe((result) => { this.translate.instant('action.yes')).subscribe((result) => {
if (result) { if (result) {
const tasks = selectedImages.map((image) => const tasks = selectedImages.map((image) =>
this.imageService.deleteImage(imageResourceType(image), image.resourceKey)); this.imageService.deleteImage(imageResourceType(image), image.resourceKey, false, {ignoreErrors: true}).pipe(
map(() => toImageDeleteResult(image)),
catchError((err) => of(toImageDeleteResult(image, err)))
)
);
forkJoin(tasks).subscribe( forkJoin(tasks).subscribe(
() => { (deleteResults) => {
this.updateData(); const anySuccess = deleteResults.some(res => res.success);
const referenceErrors = deleteResults.filter(res => res.imageIsReferencedError);
const otherError = deleteResults.find(res => !res.success);
if (anySuccess) {
this.updateData();
}
if (referenceErrors?.length) {
const imagesWithReferences: ImageResourceInfoWithReferences[] =
referenceErrors.map(ref => ({...ref.image, ...{references: ref.references}}));
this.dialog.open<ImagesInUseDialogComponent, ImagesInUseDialogData,
ImageResourceInfo[]>(ImagesInUseDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
multiple: true,
images: imagesWithReferences
}
}).afterClosed().subscribe((forceDeleteImages) => {
if (forceDeleteImages && forceDeleteImages.length) {
const forceDeleteTasks = forceDeleteImages.map((image) =>
this.imageService.deleteImage(imageResourceType(image), image.resourceKey, true)
);
forkJoin(forceDeleteTasks).subscribe(
() => {
this.updateData();
}
);
}
});
} else if (otherError) {
const errorMessageWithTimeout = parseHttpErrorMessage(otherError.error, this.translate);
setTimeout(() => {
this.store.dispatch(new ActionNotificationShow({message: errorMessageWithTimeout.message, type: 'error'}));
}, errorMessageWithTimeout.timeout);
}
} }
); );
} }
@ -435,11 +516,15 @@ export class ImageGalleryComponent extends PageComponent implements OnInit, OnDe
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
// TODO: this.importExportService.exportImage(imageResourceType(image), image.resourceKey);
} }
importImage(): void { importImage(): void {
// TODO: this.importExportService.importImage().subscribe((image) => {
if (image) {
this.updateData();
}
});
} }
uploadImage(): void { uploadImage(): void {
@ -490,98 +575,3 @@ export class ImageGalleryComponent extends PageComponent implements OnInit, OnDe
} }
} }
class ImagesDatasource implements DataSource<ImageResourceInfo> {
private entitiesSubject = new BehaviorSubject<ImageResourceInfo[]>([]);
private pageDataSubject = new BehaviorSubject<PageData<ImageResourceInfo>>(emptyPageData<ImageResourceInfo>());
public pageData$ = this.pageDataSubject.asObservable();
public selection = new SelectionModel<ImageResourceInfo>(true, []);
public dataLoading = true;
constructor(private imageService: ImageService,
protected selectionEnabledFunction: EntityBooleanFunction<ImageResourceInfo>) {
}
connect(collectionViewer: CollectionViewer):
Observable<ImageResourceInfo[] | ReadonlyArray<ImageResourceInfo>> {
return this.entitiesSubject.asObservable();
}
disconnect(collectionViewer: CollectionViewer): void {
this.entitiesSubject.complete();
this.pageDataSubject.complete();
}
reset() {
const pageData = emptyPageData<ImageResourceInfo>();
this.entitiesSubject.next(pageData.data);
this.pageDataSubject.next(pageData);
}
loadEntities(pageLink: PageLink): Observable<PageData<ImageResourceInfo>> {
this.dataLoading = true;
const result = new ReplaySubject<PageData<ImageResourceInfo>>();
this.fetchEntities(pageLink).pipe(
tap(() => {
this.selection.clear();
}),
catchError(() => of(emptyPageData<ImageResourceInfo>())),
).subscribe(
(pageData) => {
this.entitiesSubject.next(pageData.data);
this.pageDataSubject.next(pageData);
result.next(pageData);
this.dataLoading = false;
}
);
return result;
}
fetchEntities(pageLink: PageLink): Observable<PageData<ImageResourceInfo>> {
return this.imageService.getImages(pageLink);
}
isAllSelected(): Observable<boolean> {
const numSelected = this.selection.selected.length;
return this.entitiesSubject.pipe(
map((entities) => numSelected === entities.length)
);
}
isEmpty(): Observable<boolean> {
return this.entitiesSubject.pipe(
map((entities) => !entities.length)
);
}
total(): Observable<number> {
return this.pageDataSubject.pipe(
map((pageData) => pageData.totalElements)
);
}
masterToggle() {
this.entitiesSubject.pipe(
tap((entities) => {
const numSelected = this.selection.selected.length;
if (numSelected === this.selectableEntitiesCount(entities)) {
this.selection.clear();
} else {
entities.forEach(row => {
if (this.selectionEnabledFunction(row)) {
this.selection.select(row);
}
});
}
}),
take(1)
).subscribe();
}
private selectableEntitiesCount(entities: Array<ImageResourceInfo>): number {
return entities.filter((entity) => this.selectionEnabledFunction(entity)).length;
}
}

49
ui-ngx/src/app/modules/home/components/image/image-references.component.html

@ -0,0 +1,49 @@
<!--
Copyright © 2016-2023 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.
-->
<ng-container *ngIf="simpleList; else complexEntityReferences">
<ng-container *ngTemplateOutlet="referencesList; context:{entriesList: referencedEntitiesList, showDetailsLink: true}"></ng-container>
</ng-container>
<ng-template #complexEntityReferences>
<ng-container *ngIf="contentReady; else loading">
<ul>
<ng-container *ngFor="let entry of referencedEntitiesEntries">
<li *ngIf="isSystem(entry[0]); else tenantEntities">
{{ 'image.system-entities' | translate }}
<ng-container *ngTemplateOutlet="referencesList; context:{entriesList: entry[1].entities, showDetailsLink: true}"></ng-container>
</li>
<ng-template #tenantEntities>
<li>
{{ 'tenant.tenant' | translate }} <a [routerLink]="entry[1].tenantDetailsUrl">{{ '"' + entry[1].tenantName + '"' }}</a> {{ 'image.entities' | translate }}
<ng-container *ngTemplateOutlet="referencesList; context:{entriesList: entry[1].entities, showDetailsLink: false}"></ng-container>
</li>
</ng-template>
</ng-container>
</ul>
</ng-container>
</ng-template>
<ng-template #referencesList let-entriesList="entriesList" let-showDetailsLink="showDetailsLink">
<ul>
<li *ngFor="let referencedEntity of entriesList">
{{ referencedEntity.typeName }} <a *ngIf="showDetailsLink" [routerLink]="referencedEntity.detailsUrl">{{ '"' + referencedEntity.entity.name + '"' }}</a>
<b *ngIf="!showDetailsLink">{{ '"' + referencedEntity.entity.name + '"' }}</b>
</li>
</ul>
</ng-template>
<ng-template #loading>
<mat-spinner [diameter]="32" strokeWidth="2"></mat-spinner>
</ng-template>

171
ui-ngx/src/app/modules/home/components/image/image-references.component.ts

@ -0,0 +1,171 @@
///
/// Copyright © 2016-2023 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 { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { ImageReferences } from '@shared/models/resource.models';
import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models';
import { TranslateService } from '@ngx-translate/core';
import { getEntityDetailsPageURL } from '@core/utils';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { NULL_UUID } from '@shared/models/id/has-uuid';
import { Authority } from '@shared/models/authority.enum';
import { Observable } from 'rxjs';
import { EntityService } from '@core/http/entity.service';
import { BaseData, HasId } from '@shared/models/base-data';
import { HasTenantId } from '@shared/models/entity.models';
import { map } from 'rxjs/operators';
import { TbPopoverComponent } from '@shared/components/popover.component';
interface ReferencedEntityInfo {
entity: BaseData<HasId> & HasTenantId;
typeName: string;
detailsUrl: string;
}
interface TenantReferencedEntities {
tenantName?: string;
tenantDetailsUrl?: string;
entities: ReferencedEntityInfo[];
}
type ReferencedEntities = {[tenantId: string]: TenantReferencedEntities};
type ReferencedEntitiesEntry = [string, TenantReferencedEntities];
@Component({
selector: 'tb-image-references',
templateUrl: './image-references.component.html',
styleUrls: []
})
export class ImageReferencesComponent implements OnInit {
@Input()
references: ImageReferences;
popoverComponent: TbPopoverComponent<ImageReferencesComponent>;
contentReady = false;
authUser = getCurrentAuthUser(this.store);
simpleList = true;
referencedEntitiesList: ReferencedEntityInfo[];
referencedEntitiesEntries: ReferencedEntitiesEntry[];
constructor(protected store: Store<AppState>,
private entityService: EntityService,
private cd: ChangeDetectorRef,
private translate: TranslateService) {
}
ngOnInit(): void {
if (this.authUser.authority === Authority.SYS_ADMIN && this.hasNonSystemEntities(this.references)) {
this.simpleList = false;
this.toReferencedEntitiesEntries(this.references).subscribe(
(entries) => {
this.referencedEntitiesEntries = entries;
this.contentReady = true;
this.cd.detectChanges();
if (this.popoverComponent) {
Promise.resolve().then(() => {
this.popoverComponent.updatePosition();
});
}
}
);
} else {
this.referencedEntitiesList = this.toReferencedEntitiesList(this.references);
this.contentReady = true;
}
}
isSystem(tenantId: string): boolean {
return tenantId === NULL_UUID;
}
private hasNonSystemEntities(references: ImageReferences): boolean {
for (const entityTypeStr of Object.keys(references)) {
const entities = this.references[entityTypeStr];
if (entities.some(e => e.tenantId && e.tenantId.id && e.tenantId.id !== NULL_UUID)) {
return true;
}
}
return false;
}
private toReferencedEntitiesList(references: ImageReferences): ReferencedEntityInfo[] {
const result: ReferencedEntityInfo[] = [];
for (const entityTypeStr of Object.keys(references)) {
const entityType = entityTypeStr as EntityType;
const entityTypeName = this.translate.instant(entityTypeTranslations.get(entityType).type);
const entities = references[entityTypeStr];
for (const entity of entities) {
const detailsUrl = getEntityDetailsPageURL(entity.id.id, entityType);
result.push({
entity,
typeName: entityTypeName,
detailsUrl
});
}
}
return result;
}
private toReferencedEntitiesEntries(references: ImageReferences): Observable<ReferencedEntitiesEntry[]> {
let referencedEntities: ReferencedEntities = {};
const referencedEntitiesList = this.toReferencedEntitiesList(references);
for (const referencedEntityInfo of referencedEntitiesList) {
const tenantId = referencedEntityInfo.entity.tenantId?.id || NULL_UUID;
let tenantEntitiesInfo = referencedEntities[tenantId];
if (!tenantEntitiesInfo) {
tenantEntitiesInfo = {
entities: []
};
referencedEntities[tenantId] = tenantEntitiesInfo;
}
tenantEntitiesInfo.entities.push(referencedEntityInfo);
}
referencedEntities = Object.keys(referencedEntities).sort((tenantId1, tenantId2) => {
if (tenantId1 === NULL_UUID) {
return -1;
} else if (tenantId2 === NULL_UUID) {
return 1;
}
return 0;
}).reduce(
(obj, key) => {
obj[key] = referencedEntities[key];
return obj;
},
{}
);
const tenantIds = Object.keys(referencedEntities).filter(id => id !== NULL_UUID);
return this.entityService.getEntities(EntityType.TENANT, tenantIds).pipe(
map((tenants) => {
for (const tenant of tenants) {
const tenantEntitiesInfo = referencedEntities[tenant.id.id];
tenantEntitiesInfo.tenantName = tenant.name;
tenantEntitiesInfo.tenantDetailsUrl = getEntityDetailsPageURL(tenant.id.id, EntityType.TENANT);
}
return Object.entries(referencedEntities);
})
);
}
}

130
ui-ngx/src/app/modules/home/components/image/images-datasource.ts

@ -0,0 +1,130 @@
///
/// Copyright © 2016-2023 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 { CollectionViewer, DataSource, SelectionModel } from '@angular/cdk/collections';
import { ImageResourceInfo } from '@shared/models/resource.models';
import { BehaviorSubject, Observable, of, ReplaySubject, Subject } from 'rxjs';
import { emptyPageData, PageData } from '@shared/models/page/page-data';
import { ImageService } from '@core/http/image.service';
import { EntityBooleanFunction } from '@home/models/entity/entities-table-config.models';
import { PageLink } from '@shared/models/page/page-link';
import { catchError, map, take, tap } from 'rxjs/operators';
export class ImagesDatasource implements DataSource<ImageResourceInfo> {
private entitiesSubject: Subject<ImageResourceInfo[]>;
private readonly pageDataSubject: Subject<PageData<ImageResourceInfo>>;
public pageData$: Observable<PageData<ImageResourceInfo>>;
public selection = new SelectionModel<ImageResourceInfo>(true, []);
public dataLoading = true;
constructor(private imageService: ImageService,
private images: ImageResourceInfo[],
private selectionEnabledFunction: EntityBooleanFunction<ImageResourceInfo>) {
if (this.images && this.images.length) {
this.entitiesSubject = new BehaviorSubject<ImageResourceInfo[]>(this.images);
} else {
this.entitiesSubject = new BehaviorSubject<ImageResourceInfo[]>([]);
this.pageDataSubject = new BehaviorSubject<PageData<ImageResourceInfo>>(emptyPageData<ImageResourceInfo>());
this.pageData$ = this.pageDataSubject.asObservable();
}
}
connect(collectionViewer: CollectionViewer):
Observable<ImageResourceInfo[] | ReadonlyArray<ImageResourceInfo>> {
return this.entitiesSubject.asObservable();
}
disconnect(collectionViewer: CollectionViewer): void {
this.entitiesSubject.complete();
if (this.pageDataSubject) {
this.pageDataSubject.complete();
}
}
reset() {
this.entitiesSubject.next([]);
if (this.pageDataSubject) {
this.pageDataSubject.next(emptyPageData<ImageResourceInfo>());
}
}
loadEntities(pageLink: PageLink): Observable<PageData<ImageResourceInfo>> {
this.dataLoading = true;
const result = new ReplaySubject<PageData<ImageResourceInfo>>();
this.fetchEntities(pageLink).pipe(
tap(() => {
this.selection.clear();
}),
catchError(() => of(emptyPageData<ImageResourceInfo>())),
).subscribe(
(pageData) => {
this.entitiesSubject.next(pageData.data);
this.pageDataSubject.next(pageData);
result.next(pageData);
this.dataLoading = false;
}
);
return result;
}
fetchEntities(pageLink: PageLink): Observable<PageData<ImageResourceInfo>> {
return this.imageService.getImages(pageLink);
}
isAllSelected(): Observable<boolean> {
const numSelected = this.selection.selected.length;
return this.entitiesSubject.pipe(
map((entities) => numSelected === entities.length)
);
}
isEmpty(): Observable<boolean> {
return this.entitiesSubject.pipe(
map((entities) => !entities.length)
);
}
total(): Observable<number> {
return this.pageDataSubject.pipe(
map((pageData) => pageData.totalElements)
);
}
masterToggle() {
this.entitiesSubject.pipe(
tap((entities) => {
const numSelected = this.selection.selected.length;
if (numSelected === this.selectableEntitiesCount(entities)) {
this.selection.clear();
} else {
entities.forEach(row => {
if (this.selectionEnabledFunction(row)) {
this.selection.select(row);
}
});
}
}),
take(1)
).subscribe();
}
private selectableEntitiesCount(entities: Array<ImageResourceInfo>): number {
return entities.filter((entity) => this.selectionEnabledFunction(entity)).length;
}
}

82
ui-ngx/src/app/modules/home/components/image/images-in-use-dialog.component.html

@ -0,0 +1,82 @@
<!--
Copyright © 2016-2023 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.
-->
<h2 mat-dialog-title>{{title | translate}}</h2>
<div mat-dialog-content class="tb-images-in-use-content" [class]="{multiple: data.multiple}">
<div [innerHTML]="message | translate | safe: 'html'"></div>
<ng-container *ngIf="data.multiple; else singleImageReferences">
<div class="table-container">
<table mat-table [dataSource]="dataSource">
<ng-container matColumnDef="select" sticky>
<mat-header-cell *matHeaderCellDef style="width: 30px;">
<mat-checkbox (change)="$event ? dataSource.masterToggle() : null"
[checked]="dataSource.selection.hasValue() && (dataSource.isAllSelected() | async)"
[indeterminate]="dataSource.selection.hasValue() && (dataSource.isAllSelected() | async) === false">
</mat-checkbox>
</mat-header-cell>
<mat-cell *matCellDef="let image">
<mat-checkbox (click)="$event.stopPropagation()"
(change)="$event ? dataSource.selection.toggle(image) : null"
[checked]="dataSource.selection.isSelected(image)">
</mat-checkbox>
</mat-cell>
</ng-container>
<ng-container matColumnDef="preview">
<mat-header-cell *matHeaderCellDef style="width: 50px; min-width: 50px;"></mat-header-cell>
<mat-cell *matCellDef="let image">
<img class="tb-image-preview" [src]="image.link | image: {preview: true} | async" alt="{{ image.title }}">
</mat-cell>
</ng-container>
<ng-container matColumnDef="title">
<mat-header-cell *matHeaderCellDef style="width: 100%;">
<ng-container *ngIf="dataSource.selection.isEmpty(); else selectedImages">
{{ 'image.name' | translate }}
</ng-container>
<ng-template #selectedImages>
{{ translate.get('image.selected-images', {count: dataSource.selection.selected.length}) | async }}
</ng-template>
</mat-header-cell>
<mat-cell *matCellDef="let image">
{{ image.title }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="references">
<mat-header-cell *matHeaderCellDef></mat-header-cell>
<mat-cell *matCellDef="let image">
<button #showReferencesButton
mat-stroked-button
color="primary"
(click)="toggleShowReferences($event, image, showReferencesButton)">{{ 'image.references' | translate }}</button>
</mat-cell>
</ng-container>
<mat-header-row [ngClass]="{'mat-row-select': true}" *matHeaderRowDef="['select', 'preview', 'title', 'references']; sticky: true"></mat-header-row>
<mat-row [ngClass]="{'mat-row-select': true,
'mat-selected': dataSource.selection.isSelected(image)}"
*matRowDef="let image; columns: ['select', 'preview', 'title', 'references'];"></mat-row>
</table>
</div>
</ng-container>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-button color="primary" (click)="cancel()">{{'action.cancel' | translate}}</button>
<button mat-raised-button color="accent" (click)="delete()" [disabled]="data.multiple && dataSource.selection.isEmpty()"
cdkFocusInitial>{{ (data.multiple ? 'action.delete-selected' : 'action.delete-anyway') | translate}}</button>
</div>
<ng-template #singleImageReferences>
<tb-image-references [references]="references"></tb-image-references>
<div [innerHTML]="'image.delete-image-in-use-text' | translate | safe: 'html'"></div>
</ng-template>

39
ui-ngx/src/app/modules/home/components/image/images-in-use-dialog.component.scss

@ -0,0 +1,39 @@
/**
* Copyright © 2016-2023 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 {
.tb-images-in-use-content {
display: flex;
flex-direction: column;
&.multiple {
gap: 16px;
}
.table-container {
overflow: auto;
.mat-mdc-cell.mat-column-preview {
width: 50px;
height: 50px;
padding: 2px 12px;
display: block;
}
}
.tb-image-preview {
width: 50px;
height: 50px;
object-fit: contain;
border-radius: 4px;
}
}
}

112
ui-ngx/src/app/modules/home/components/image/images-in-use-dialog.component.ts

@ -0,0 +1,112 @@
///
/// Copyright © 2016-2023 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, OnInit, Renderer2, ViewContainerRef } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
import { ImageReferences, ImageResourceInfo, ImageResourceInfoWithReferences } from '@shared/models/resource.models';
import { ImagesDatasource } from '@home/components/image/images-datasource';
import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service';
import { ImageReferencesComponent } from '@home/components/image/image-references.component';
import { TranslateService } from '@ngx-translate/core';
export interface ImagesInUseDialogData {
multiple: boolean;
images: ImageResourceInfoWithReferences[];
}
@Component({
selector: 'tb-images-in-use-dialog',
templateUrl: './images-in-use-dialog.component.html',
styleUrls: ['./images-in-use-dialog.component.scss']
})
export class ImagesInUseDialogComponent extends
DialogComponent<ImagesInUseDialogComponent, ImageResourceInfo[]> implements OnInit {
title: string;
message: string;
references: ImageReferences;
dataSource: ImagesDatasource;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: ImagesInUseDialogData,
public dialogRef: MatDialogRef<ImagesInUseDialogComponent, ImageResourceInfo[]>,
public translate: TranslateService,
private renderer: Renderer2,
private viewContainerRef: ViewContainerRef,
private popoverService: TbPopoverService) {
super(store, router, dialogRef);
}
ngOnInit(): void {
if (this.data.multiple) {
this.title = 'image.images-are-in-use';
this.message = 'image.images-are-in-use-text';
this.dataSource = new ImagesDatasource(null, this.data.images, entity => true);
} else {
this.title = 'image.image-is-in-use';
this.message = 'image.image-is-in-use-text';
this.references = this.data.images[0].references;
}
}
cancel() {
this.dialogRef.close(null);
}
delete() {
if (this.data.multiple) {
this.dialogRef.close(this.dataSource.selection.selected);
} else {
this.dialogRef.close(this.data.images);
}
}
toggleShowReferences($event: Event, image: ImageResourceInfoWithReferences, referencesButton: MatButton) {
if ($event) {
$event.stopPropagation();
}
const trigger = referencesButton._elementRef.nativeElement;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
const referencesPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, ImageReferencesComponent, 'top', true, null,
{
references: image.references
}, {}, {}, {},
false,
visible => {
const addClasses = visible ? 'mdc-button--unelevated mat-mdc-unelevated-button' : 'mdc-button--outlined mat-mdc-outlined-button';
const removeClasses = visible ? 'mdc-button--outlined mat-mdc-outlined-button' : 'mdc-button--unelevated mat-mdc-unelevated-button';
addClasses.split(' ').forEach(clazz => {
this.renderer.addClass(trigger, clazz);
});
removeClasses.split(' ').forEach(clazz => {
this.renderer.removeClass(trigger, clazz);
});
});
referencesPopover.tbComponentRef.instance.popoverComponent = referencesPopover;
}
}
}

44
ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts

@ -21,7 +21,7 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { ActionNotificationShow } from '@core/notification/notification.actions'; import { ActionNotificationShow } from '@core/notification/notification.actions';
import { Dashboard, DashboardLayoutId } from '@shared/models/dashboard.models'; import { Dashboard, DashboardLayoutId } from '@shared/models/dashboard.models';
import { deepClone, guid, isDefined, isObject, isString, isUndefined } from '@core/utils'; import { deepClone, guid, isDefined, isNotEmptyStr, isObject, isString, isUndefined } from '@core/utils';
import { WINDOW } from '@core/services/window.service'; import { WINDOW } from '@core/services/window.service';
import { DOCUMENT } from '@angular/common'; import { DOCUMENT } from '@angular/common';
import { import {
@ -79,6 +79,8 @@ import {
ExportWidgetsBundleDialogData, ExportWidgetsBundleDialogData,
ExportWidgetsBundleDialogResult ExportWidgetsBundleDialogResult
} from '@home/components/import-export/export-widgets-bundle-dialog.component'; } from '@home/components/import-export/export-widgets-bundle-dialog.component';
import { ImageService } from '@core/http/image.service';
import { ImageExportData, ImageResourceInfo, ImageResourceType } from '@shared/models/resource.models';
// @dynamic // @dynamic
@Injectable() @Injectable()
@ -99,12 +101,44 @@ export class ImportExportService {
private deviceService: DeviceService, private deviceService: DeviceService,
private assetService: AssetService, private assetService: AssetService,
private edgeService: EdgeService, private edgeService: EdgeService,
private imageService: ImageService,
private utils: UtilsService, private utils: UtilsService,
private itembuffer: ItemBufferService, private itembuffer: ItemBufferService,
private dialog: MatDialog) { private dialog: MatDialog) {
} }
public exportImage(type: ImageResourceType, key: string) {
this.imageService.exportImage(type, key).subscribe(
{
next: (imageData) => {
let name = imageData.title;
name = name.toLowerCase().replace(/\W/g, '_');
this.exportToPc(imageData, name);
},
error: (e) => {
this.handleExportError(e, 'image.export-failed-error');
}
}
);
}
public importImage(): Observable<ImageResourceInfo> {
return this.openImportDialog('image.import-image', 'image.image-json-file').pipe(
mergeMap((imageData: ImageExportData) => {
if (!this.validateImportedImage(imageData)) {
this.store.dispatch(new ActionNotificationShow(
{message: this.translate.instant('image.invalid-image-json-file-error'),
type: 'error'}));
throw new Error('Invalid image JSON file');
} else {
return this.imageService.importImage(imageData);
}
}),
catchError(() => of(null))
);
}
public exportDashboard(dashboardId: string) { public exportDashboard(dashboardId: string) {
this.dashboardService.getDashboard(dashboardId).subscribe( this.dashboardService.getDashboard(dashboardId).subscribe(
(dashboard) => { (dashboard) => {
@ -818,6 +852,14 @@ export class ImportExportService {
type: 'error'})); type: 'error'}));
} }
private validateImportedImage(image: ImageExportData): boolean {
return !(!isNotEmptyStr(image.data)
|| !isNotEmptyStr(image.title)
|| !isNotEmptyStr(image.fileName)
|| !isNotEmptyStr(image.mediaType)
|| !isNotEmptyStr(image.resourceKey));
}
private validateImportedDashboard(dashboard: Dashboard): boolean { private validateImportedDashboard(dashboard: Dashboard): boolean {
if (isUndefined(dashboard.title) || isUndefined(dashboard.configuration)) { if (isUndefined(dashboard.title) || isUndefined(dashboard.configuration)) {
return false; return false;

48
ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts

@ -31,6 +31,8 @@ import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
import { RouterTabsComponent } from '@home/components/router-tabs.component'; import { RouterTabsComponent } from '@home/components/router-tabs.component';
import { WidgetTypesTableConfigResolver } from '@home/pages/widget/widget-types-table-config.resolver'; import { WidgetTypesTableConfigResolver } from '@home/pages/widget/widget-types-table-config.resolver';
import { WidgetsBundleWidgetsComponent } from '@home/pages/widget/widgets-bundle-widgets.component'; import { WidgetsBundleWidgetsComponent } from '@home/pages/widget/widgets-bundle-widgets.component';
import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component';
import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models';
export interface WidgetEditorData { export interface WidgetEditorData {
widgetTypeDetails: WidgetTypeDetails; widgetTypeDetails: WidgetTypeDetails;
@ -122,6 +124,29 @@ const widgetTypesRoutes: Routes = [
entitiesTableConfig: WidgetTypesTableConfigResolver entitiesTableConfig: WidgetTypesTableConfigResolver
} }
}, },
{
path: 'details',
children: [
{
path: ':entityId',
component: EntityDetailsPageComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
breadcrumb: {
labelFunction: entityDetailsPageBreadcrumbLabelFunction,
icon: 'now_widgets'
} as BreadCrumbConfig<EntityDetailsPageComponent>,
auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
title: 'widget.widgets',
hideTabs: true,
backNavigationCommands: ['../..']
},
resolve: {
entitiesTableConfig: WidgetTypesTableConfigResolver
}
}
]
},
...widgetEditorRouter ...widgetEditorRouter
] ]
}, },
@ -148,6 +173,29 @@ const widgetsBundlesRoutes: Routes = [
entitiesTableConfig: WidgetsBundlesTableConfigResolver entitiesTableConfig: WidgetsBundlesTableConfigResolver
} }
}, },
{
path: 'details',
children: [
{
path: ':entityId',
component: EntityDetailsPageComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
breadcrumb: {
labelFunction: entityDetailsPageBreadcrumbLabelFunction,
icon: 'now_widgets'
} as BreadCrumbConfig<EntityDetailsPageComponent>,
auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
title: 'widgets-bundle.widgets-bundles',
hideTabs: true,
backNavigationCommands: ['../..']
},
resolve: {
entitiesTableConfig: WidgetsBundlesTableConfigResolver
}
}
]
},
{ {
path: ':widgetsBundleId', path: ':widgetsBundleId',
data: { data: {

6
ui-ngx/src/app/modules/home/pages/widget/widget-type.component.html

@ -22,6 +22,12 @@
[fxShow]="!isEdit"> [fxShow]="!isEdit">
{{ 'widget.edit' | translate }} {{ 'widget.edit' | translate }}
</button> </button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'openDetails')"
[fxShow]="!isEdit && !isDetailsPage">
{{'common.open-details-page' | translate }}
</button>
<button mat-raised-button color="primary" <button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'export')" (click)="onEntityAction($event, 'export')"

20
ui-ngx/src/app/modules/home/pages/widget/widget-types-table-config.resolver.ts

@ -31,16 +31,15 @@ import { WidgetService } from '@app/core/http/widget.service';
import { NULL_UUID } from '@shared/models/id/has-uuid'; import { NULL_UUID } from '@shared/models/id/has-uuid';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { getCurrentAuthState, getCurrentAuthUser } from '@app/core/auth/auth.selectors'; import { getCurrentAuthUser } from '@app/core/auth/auth.selectors';
import { Authority } from '@shared/models/authority.enum'; import { Authority } from '@shared/models/authority.enum';
import { DialogService } from '@core/services/dialog.service';
import { ImportExportService } from '@home/components/import-export/import-export.service'; import { ImportExportService } from '@home/components/import-export/import-export.service';
import { Direction } from '@shared/models/page/sort-order'; import { Direction } from '@shared/models/page/sort-order';
import { import {
BaseWidgetType, BaseWidgetType,
widgetType as WidgetDataType,
WidgetTypeDetails, WidgetTypeDetails,
WidgetTypeInfo, WidgetTypeInfo,
widgetType as WidgetDataType,
widgetTypesData widgetTypesData
} from '@shared/models/widget.models'; } from '@shared/models/widget.models';
import { WidgetTypeComponent } from '@home/pages/widget/widget-type.component'; import { WidgetTypeComponent } from '@home/pages/widget/widget-type.component';
@ -134,7 +133,7 @@ export class WidgetTypesTableConfigResolver implements Resolve<EntityTableConfig
this.config.loadEntity = id => this.widgetsService.getWidgetTypeById(id.id); this.config.loadEntity = id => this.widgetsService.getWidgetTypeById(id.id);
this.config.saveEntity = widgetType => this.widgetsService.saveWidgetType(widgetType as WidgetTypeDetails); this.config.saveEntity = widgetType => this.widgetsService.saveWidgetType(widgetType as WidgetTypeDetails);
this.config.deleteEntity = id => this.widgetsService.deleteWidgetType(id.id); this.config.deleteEntity = id => this.widgetsService.deleteWidgetType(id.id);
this.config.onEntityAction = action => this.onWidgetTypeAction(action); this.config.onEntityAction = action => this.onWidgetTypeAction(action, this.config);
this.config.handleRowClick = ($event, widgetType) => { this.config.handleRowClick = ($event, widgetType) => {
if (this.config.isDetailsOpen()) { if (this.config.isDetailsOpen()) {
@ -198,6 +197,14 @@ export class WidgetTypesTableConfigResolver implements Resolve<EntityTableConfig
this.router.navigateByUrl(`resources/widgets-library/widget-types/${widgetType.id.id}`); this.router.navigateByUrl(`resources/widgets-library/widget-types/${widgetType.id.id}`);
} }
private openWidgetTypeDetails($event: Event, widgetType: BaseWidgetType, config: EntityTableConfig<BaseWidgetType>) {
if ($event) {
$event.stopPropagation();
}
const url = this.router.createUrlTree(['details', widgetType.id.id], {relativeTo: config.getActivatedRoute()});
this.router.navigateByUrl(url);
}
exportWidgetType($event: Event, widgetType: BaseWidgetType) { exportWidgetType($event: Event, widgetType: BaseWidgetType) {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
@ -216,11 +223,14 @@ export class WidgetTypesTableConfigResolver implements Resolve<EntityTableConfig
); );
} }
onWidgetTypeAction(action: EntityAction<BaseWidgetType>): boolean { onWidgetTypeAction(action: EntityAction<BaseWidgetType>, config: EntityTableConfig<BaseWidgetType>): boolean {
switch (action.action) { switch (action.action) {
case 'edit': case 'edit':
this.openWidgetEditor(action.event, action.entity); this.openWidgetEditor(action.event, action.entity);
return true; return true;
case 'openDetails':
this.openWidgetTypeDetails(action.event, action.entity, config);
return true;
case 'export': case 'export':
this.exportWidgetType(action.event, action.entity); this.exportWidgetType(action.event, action.entity);
return true; return true;

6
ui-ngx/src/app/modules/home/pages/widget/widgets-bundle.component.html

@ -22,6 +22,12 @@
[fxShow]="!isEdit"> [fxShow]="!isEdit">
{{'widgets-bundle.open-widgets-bundle' | translate }} {{'widgets-bundle.open-widgets-bundle' | translate }}
</button> </button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'openDetails')"
[fxShow]="!isEdit && !isDetailsPage">
{{'common.open-details-page' | translate }}
</button>
<button mat-raised-button color="primary" <button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'export')" (click)="onEntityAction($event, 'export')"

16
ui-ngx/src/app/modules/home/pages/widget/widgets-bundles-table-config.resolver.ts

@ -40,6 +40,7 @@ import { ImportExportService } from '@home/components/import-export/import-expor
import { Direction } from '@shared/models/page/sort-order'; import { Direction } from '@shared/models/page/sort-order';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { WidgetsBundleTabsComponent } from '@home/pages/widget/widgets-bundle-tabs.component'; import { WidgetsBundleTabsComponent } from '@home/pages/widget/widgets-bundle-tabs.component';
import { TenantInfo } from '@shared/models/tenant.model';
@Injectable() @Injectable()
export class WidgetsBundlesTableConfigResolver implements Resolve<EntityTableConfig<WidgetsBundle>> { export class WidgetsBundlesTableConfigResolver implements Resolve<EntityTableConfig<WidgetsBundle>> {
@ -115,7 +116,7 @@ export class WidgetsBundlesTableConfigResolver implements Resolve<EntityTableCon
this.config.loadEntity = id => this.widgetsService.getWidgetsBundle(id.id); this.config.loadEntity = id => this.widgetsService.getWidgetsBundle(id.id);
this.config.saveEntity = widgetsBundle => this.widgetsService.saveWidgetsBundle(widgetsBundle); this.config.saveEntity = widgetsBundle => this.widgetsService.saveWidgetsBundle(widgetsBundle);
this.config.deleteEntity = id => this.widgetsService.deleteWidgetsBundle(id.id); this.config.deleteEntity = id => this.widgetsService.deleteWidgetsBundle(id.id);
this.config.onEntityAction = action => this.onWidgetsBundleAction(action); this.config.onEntityAction = action => this.onWidgetsBundleAction(action, this.config);
this.config.handleRowClick = ($event, widgetsBundle) => { this.config.handleRowClick = ($event, widgetsBundle) => {
if (this.config.isDetailsOpen()) { if (this.config.isDetailsOpen()) {
@ -167,6 +168,14 @@ export class WidgetsBundlesTableConfigResolver implements Resolve<EntityTableCon
this.router.navigateByUrl(`resources/widgets-bundles/${widgetsBundle.id.id}/widgetTypes`); this.router.navigateByUrl(`resources/widgets-bundles/${widgetsBundle.id.id}/widgetTypes`);
} }
private openWidgetsBundleDetails($event: Event, widgetsBundle: WidgetsBundle, config: EntityTableConfig<WidgetsBundle>) {
if ($event) {
$event.stopPropagation();
}
const url = this.router.createUrlTree(['details', widgetsBundle.id.id], {relativeTo: config.getActivatedRoute()});
this.router.navigateByUrl(url);
}
exportWidgetsBundle($event: Event, widgetsBundle: WidgetsBundle) { exportWidgetsBundle($event: Event, widgetsBundle: WidgetsBundle) {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
@ -174,11 +183,14 @@ export class WidgetsBundlesTableConfigResolver implements Resolve<EntityTableCon
this.importExport.exportWidgetsBundle(widgetsBundle.id.id); this.importExport.exportWidgetsBundle(widgetsBundle.id.id);
} }
onWidgetsBundleAction(action: EntityAction<WidgetsBundle>): boolean { onWidgetsBundleAction(action: EntityAction<WidgetsBundle>, config: EntityTableConfig<WidgetsBundle>): boolean {
switch (action.action) { switch (action.action) {
case 'open': case 'open':
this.openWidgetsBundle(action.event, action.entity); this.openWidgetsBundle(action.event, action.entity);
return true; return true;
case 'openDetails':
this.openWidgetsBundleDetails(action.event, action.entity, config);
return true;
case 'export': case 'export':
this.exportWidgetsBundle(action.event, action.entity); this.exportWidgetsBundle(action.event, action.entity);
return true; return true;

3
ui-ngx/src/app/shared/components/popover.component.ts

@ -378,6 +378,7 @@ export class TbPopoverComponent<T = any> implements OnDestroy, OnInit {
tbAnimationState = 'active'; tbAnimationState = 'active';
tbHideStart = new Subject<void>();
tbVisibleChange = new Subject<boolean>(); tbVisibleChange = new Subject<boolean>();
tbAnimationDone = new Subject<void>(); tbAnimationDone = new Subject<void>();
tbComponentChange = new Subject<ComponentRef<any>>(); tbComponentChange = new Subject<ComponentRef<any>>();
@ -486,6 +487,7 @@ export class TbPopoverComponent<T = any> implements OnDestroy, OnInit {
} }
this.intersectionObserver.disconnect(); this.intersectionObserver.disconnect();
this.intersectionObserver = null; this.intersectionObserver = null;
this.tbHideStart.complete();
this.tbVisibleChange.complete(); this.tbVisibleChange.complete();
this.tbAnimationDone.complete(); this.tbAnimationDone.complete();
this.tbDestroy.next(); this.tbDestroy.next();
@ -528,6 +530,7 @@ export class TbPopoverComponent<T = any> implements OnDestroy, OnInit {
if (!this.tbVisible) { if (!this.tbVisible) {
return; return;
} }
this.tbHideStart.next();
if (this.parentScrollSubscription) { if (this.parentScrollSubscription) {
this.parentScrollSubscription.unsubscribe(); this.parentScrollSubscription.unsubscribe();
this.parentScrollSubscription = null; this.parentScrollSubscription = null;

11
ui-ngx/src/app/shared/components/popover.service.ts

@ -65,16 +65,17 @@ export class TbPopoverService {
displayPopover<T>(trigger: Element, renderer: Renderer2, hostView: ViewContainerRef, displayPopover<T>(trigger: Element, renderer: Renderer2, hostView: ViewContainerRef,
componentType: Type<T>, preferredPlacement: PopoverPlacement = 'top', hideOnClickOutside = true, componentType: Type<T>, preferredPlacement: PopoverPlacement = 'top', hideOnClickOutside = true,
injector?: Injector, context?: any, overlayStyle: any = {}, popoverStyle: any = {}, style?: any, injector?: Injector, context?: any, overlayStyle: any = {}, popoverStyle: any = {}, style?: any,
showCloseButton = true): TbPopoverComponent<T> { showCloseButton = true, visibleFn: (visible: boolean) => void = () => {}): TbPopoverComponent<T> {
const componentRef = this.createPopoverRef(hostView); const componentRef = this.createPopoverRef(hostView);
return this.displayPopoverWithComponentRef(componentRef, trigger, renderer, componentType, preferredPlacement, hideOnClickOutside, return this.displayPopoverWithComponentRef(componentRef, trigger, renderer, componentType, preferredPlacement, hideOnClickOutside,
injector, context, overlayStyle, popoverStyle, style, showCloseButton); injector, context, overlayStyle, popoverStyle, style, showCloseButton, visibleFn);
} }
displayPopoverWithComponentRef<T>(componentRef: ComponentRef<TbPopoverComponent>, trigger: Element, renderer: Renderer2, displayPopoverWithComponentRef<T>(componentRef: ComponentRef<TbPopoverComponent>, trigger: Element, renderer: Renderer2,
componentType: Type<T>, preferredPlacement: PopoverPlacement = 'top', componentType: Type<T>, preferredPlacement: PopoverPlacement = 'top',
hideOnClickOutside = true, injector?: Injector, context?: any, overlayStyle: any = {}, hideOnClickOutside = true, injector?: Injector, context?: any, overlayStyle: any = {},
popoverStyle: any = {}, style?: any, showCloseButton = true): TbPopoverComponent<T> { popoverStyle: any = {}, style?: any, showCloseButton = true,
visibleFn: (visible: boolean) => void = () => {}): TbPopoverComponent<T> {
const component = componentRef.instance; const component = componentRef.instance;
this.popoverWithTriggers.push({ this.popoverWithTriggers.push({
trigger, trigger,
@ -103,7 +104,11 @@ export class TbPopoverService {
component.tbDestroy.subscribe(() => { component.tbDestroy.subscribe(() => {
this.removePopoverByComponent(component); this.removePopoverByComponent(component);
}); });
component.tbHideStart.subscribe(() => {
visibleFn(false);
});
component.show(); component.show();
visibleFn(true);
return component; return component;
} }

3
ui-ngx/src/app/shared/models/alarm.models.ts

@ -26,6 +26,7 @@ import { TableCellButtonActionDescriptor } from '@home/components/widget/lib/tab
import { AlarmCommentId } from '@shared/models/id/alarm-comment-id'; import { AlarmCommentId } from '@shared/models/id/alarm-comment-id';
import { UserId } from '@shared/models/id/user-id'; import { UserId } from '@shared/models/id/user-id';
import { AlarmFilter } from '@shared/models/query/query.models'; import { AlarmFilter } from '@shared/models/query/query.models';
import { HasTenantId } from '@shared/models/entity.models';
export enum AlarmsMode { export enum AlarmsMode {
ALL, ALL,
@ -94,7 +95,7 @@ export const alarmSeverityColors = new Map<AlarmSeverity, string>(
] ]
); );
export interface Alarm extends BaseData<AlarmId> { export interface Alarm extends BaseData<AlarmId>, HasTenantId {
tenantId: TenantId; tenantId: TenantId;
customerId: CustomerId; customerId: CustomerId;
assigneeId: UserId; assigneeId: UserId;

6
ui-ngx/src/app/shared/models/asset.models.ts

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

3
ui-ngx/src/app/shared/models/customer.model.ts

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

3
ui-ngx/src/app/shared/models/dashboard.models.ts

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

8
ui-ngx/src/app/shared/models/device.models.ts

@ -22,7 +22,7 @@ import { DeviceCredentialsId } from '@shared/models/id/device-credentials-id';
import { EntitySearchQuery } from '@shared/models/relation.models'; import { EntitySearchQuery } from '@shared/models/relation.models';
import { DeviceProfileId } from '@shared/models/id/device-profile-id'; import { DeviceProfileId } from '@shared/models/id/device-profile-id';
import { RuleChainId } from '@shared/models/id/rule-chain-id'; import { RuleChainId } from '@shared/models/id/rule-chain-id';
import { EntityInfoData } from '@shared/models/entity.models'; import { EntityInfoData, HasTenantId } from '@shared/models/entity.models';
import { FilterPredicateValue, KeyFilter } from '@shared/models/query/query.models'; import { FilterPredicateValue, KeyFilter } from '@shared/models/query/query.models';
import { TimeUnit } from '@shared/models/time/time.models'; import { TimeUnit } from '@shared/models/time/time.models';
import * as _moment from 'moment'; import * as _moment from 'moment';
@ -584,7 +584,7 @@ export interface DeviceProfileData {
provisionConfiguration?: DeviceProvisionConfiguration; provisionConfiguration?: DeviceProvisionConfiguration;
} }
export interface DeviceProfile extends BaseData<DeviceProfileId>, ExportableEntity<DeviceProfileId> { export interface DeviceProfile extends BaseData<DeviceProfileId>, HasTenantId, ExportableEntity<DeviceProfileId> {
tenantId?: TenantId; tenantId?: TenantId;
name: string; name: string;
description?: string; description?: string;
@ -603,7 +603,7 @@ export interface DeviceProfile extends BaseData<DeviceProfileId>, ExportableEnti
defaultEdgeRuleChainId?: RuleChainId; defaultEdgeRuleChainId?: RuleChainId;
} }
export interface DeviceProfileInfo extends EntityInfoData { export interface DeviceProfileInfo extends EntityInfoData, HasTenantId {
tenantId?: TenantId; tenantId?: TenantId;
type: DeviceProfileType; type: DeviceProfileType;
transportType: DeviceTransportType; transportType: DeviceTransportType;
@ -711,7 +711,7 @@ export interface DeviceData {
transportConfiguration: DeviceTransportConfiguration; transportConfiguration: DeviceTransportConfiguration;
} }
export interface Device extends BaseData<DeviceId>, ExportableEntity<DeviceId> { export interface Device extends BaseData<DeviceId>, HasTenantId, ExportableEntity<DeviceId> {
tenantId?: TenantId; tenantId?: TenantId;
customerId?: CustomerId; customerId?: CustomerId;
name: string; name: string;

3
ui-ngx/src/app/shared/models/edge.models.ts

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

4
ui-ngx/src/app/shared/models/entity-type.models.ts

@ -537,7 +537,9 @@ export const baseDetailsPageByEntityType = new Map<EntityType, string>([
[EntityType.ENTITY_VIEW, '/entities/entityViews'], [EntityType.ENTITY_VIEW, '/entities/entityViews'],
[EntityType.TB_RESOURCE, '/resources/resources-library'], [EntityType.TB_RESOURCE, '/resources/resources-library'],
[EntityType.OTA_PACKAGE, '/features/otaUpdates'], [EntityType.OTA_PACKAGE, '/features/otaUpdates'],
[EntityType.QUEUE, '/settings/queues'] [EntityType.QUEUE, '/settings/queues'],
[EntityType.WIDGETS_BUNDLE, '/resources/widgets-library/widgets-bundles/details'],
[EntityType.WIDGET_TYPE, '/resources/widgets-library/widget-types/details']
]); ]);
export interface EntitySubtype { export interface EntitySubtype {

3
ui-ngx/src/app/shared/models/entity-view.models.ts

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

5
ui-ngx/src/app/shared/models/entity.models.ts

@ -19,6 +19,7 @@ import { AttributeData } from './telemetry/telemetry.models';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { DeviceCredentialMQTTBasic } from '@shared/models/device.models'; import { DeviceCredentialMQTTBasic } from '@shared/models/device.models';
import { Lwm2mSecurityConfigModels } from '@shared/models/lwm2m-security-config.models'; import { Lwm2mSecurityConfigModels } from '@shared/models/lwm2m-security-config.models';
import { TenantId } from '@shared/models/id/tenant-id';
export interface EntityInfo { export interface EntityInfo {
name?: string; name?: string;
@ -162,3 +163,7 @@ export const entityFields: {[fieldName: string]: EntityField} = {
value: 'label' value: 'label'
} }
}; };
export interface HasTenantId {
tenantId?: TenantId;
}

9
ui-ngx/src/app/shared/models/notification.models.ts

@ -27,6 +27,7 @@ import { AlarmSearchStatus, AlarmSeverity, AlarmStatus } from '@shared/models/al
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { ApiFeature, ApiUsageStateValue } from '@shared/models/api-usage.models'; import { ApiFeature, ApiUsageStateValue } from '@shared/models/api-usage.models';
import { LimitedApi } from '@shared/models/limited-api.models'; import { LimitedApi } from '@shared/models/limited-api.models';
import { HasTenantId } from '@shared/models/entity.models';
export interface Notification { export interface Notification {
readonly id: NotificationId; readonly id: NotificationId;
@ -109,7 +110,7 @@ export interface SlackConversation {
type: string; type: string;
} }
export interface NotificationRule extends Omit<BaseData<NotificationRuleId>, 'label'>, ExportableEntity<NotificationRuleId> { export interface NotificationRule extends Omit<BaseData<NotificationRuleId>, 'label'>, HasTenantId, ExportableEntity<NotificationRuleId> {
tenantId: TenantId; tenantId: TenantId;
enabled: boolean; enabled: boolean;
templateId: NotificationTemplateId; templateId: NotificationTemplateId;
@ -241,7 +242,8 @@ export interface NonConfirmedNotificationEscalation {
targets: Array<string>; targets: Array<string>;
} }
export interface NotificationTarget extends Omit<BaseData<NotificationTargetId>, 'label'>, ExportableEntity<NotificationTargetId> { export interface NotificationTarget extends Omit<BaseData<NotificationTargetId>, 'label'>, HasTenantId,
ExportableEntity<NotificationTargetId> {
tenantId: TenantId; tenantId: TenantId;
configuration: NotificationTargetConfig; configuration: NotificationTargetConfig;
} }
@ -295,7 +297,8 @@ export const NotificationTargetTypeTranslationMap = new Map<NotificationTargetTy
[NotificationTargetType.MICROSOFT_TEAMS, 'notification.delivery-method.microsoft-teams'], [NotificationTargetType.MICROSOFT_TEAMS, 'notification.delivery-method.microsoft-teams'],
]); ]);
export interface NotificationTemplate extends Omit<BaseData<NotificationTemplateId>, 'label'>, ExportableEntity<NotificationTemplateId> { export interface NotificationTemplate extends Omit<BaseData<NotificationTemplateId>, 'label'>,
HasTenantId, ExportableEntity<NotificationTemplateId> {
tenantId: TenantId; tenantId: TenantId;
notificationType: NotificationType; notificationType: NotificationType;
configuration: NotificationTemplateConfig; configuration: NotificationTemplateConfig;

3
ui-ngx/src/app/shared/models/ota-package.models.ts

@ -18,6 +18,7 @@ import { BaseData } from '@shared/models/base-data';
import { TenantId } from '@shared/models/id/tenant-id'; import { TenantId } from '@shared/models/id/tenant-id';
import { OtaPackageId } from '@shared/models/id/ota-package-id'; import { OtaPackageId } from '@shared/models/id/ota-package-id';
import { DeviceProfileId } from '@shared/models/id/device-profile-id'; import { DeviceProfileId } from '@shared/models/id/device-profile-id';
import { HasTenantId } from '@shared/models/entity.models';
export enum ChecksumAlgorithm { export enum ChecksumAlgorithm {
MD5 = 'MD5', MD5 = 'MD5',
@ -85,7 +86,7 @@ export interface OtaPagesIds {
softwareId?: OtaPackageId; softwareId?: OtaPackageId;
} }
export interface OtaPackageInfo extends BaseData<OtaPackageId> { export interface OtaPackageInfo extends BaseData<OtaPackageId>, HasTenantId {
tenantId?: TenantId; tenantId?: TenantId;
type: OtaUpdateType; type: OtaUpdateType;
deviceProfileId?: DeviceProfileId; deviceProfileId?: DeviceProfileId;

3
ui-ngx/src/app/shared/models/queue.models.ts

@ -17,6 +17,7 @@
import { BaseData } from '@shared/models/base-data'; import { BaseData } from '@shared/models/base-data';
import { TenantId } from '@shared/models/id/tenant-id'; import { TenantId } from '@shared/models/id/tenant-id';
import { QueueId } from '@shared/models/id/queue-id'; import { QueueId } from '@shared/models/id/queue-id';
import { HasTenantId } from '@shared/models/entity.models';
export enum ServiceType { export enum ServiceType {
TB_CORE = 'TB_CORE', TB_CORE = 'TB_CORE',
@ -99,7 +100,7 @@ export const QueueProcessingStrategyTypesMap = new Map<QueueProcessingStrategyTy
}] }]
]); ]);
export interface QueueInfo extends BaseData<QueueId> { export interface QueueInfo extends BaseData<QueueId>, HasTenantId {
generatedId?: string; generatedId?: string;
name: string; name: string;
packProcessingTimeout: number; packProcessingTimeout: number;

42
ui-ngx/src/app/shared/models/resource.models.ts

@ -14,11 +14,11 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { BaseData, ExportableEntity } from '@shared/models/base-data'; import { BaseData, ExportableEntity, HasId } from '@shared/models/base-data';
import { TenantId } from '@shared/models/id/tenant-id'; import { TenantId } from '@shared/models/id/tenant-id';
import { TbResourceId } from '@shared/models/id/tb-resource-id'; import { TbResourceId } from '@shared/models/id/tb-resource-id';
import { NULL_UUID } from '@shared/models/id/has-uuid'; import { NULL_UUID } from '@shared/models/id/has-uuid';
import { BaseWidgetType } from '@shared/models/widget.models'; import { HasTenantId } from '@shared/models/entity.models';
export enum ResourceType { export enum ResourceType {
LWM2M_MODEL = 'LWM2M_MODEL', LWM2M_MODEL = 'LWM2M_MODEL',
@ -57,7 +57,7 @@ export const ResourceTypeTranslationMap = new Map<ResourceType, string>(
] ]
); );
export interface TbResourceInfo<D> extends Omit<BaseData<TbResourceId>, 'name' | 'label'>, ExportableEntity<TbResourceId> { export interface TbResourceInfo<D> extends Omit<BaseData<TbResourceId>, 'name' | 'label'>, HasTenantId, ExportableEntity<TbResourceId> {
tenantId?: TenantId; tenantId?: TenantId;
resourceKey?: string; resourceKey?: string;
title?: string; title?: string;
@ -86,8 +86,44 @@ export interface ImageResourceInfo extends TbResourceInfo<ImageDescriptor> {
link?: string; link?: string;
} }
export interface ImageExportData {
mediaType: string;
fileName: string;
title: string;
resourceKey: string;
data: string;
}
export type ImageResourceType = 'tenant' | 'system'; export type ImageResourceType = 'tenant' | 'system';
export type ImageReferences = {[entityType: string]: Array<BaseData<HasId> & HasTenantId>};
export interface ImageResourceInfoWithReferences extends ImageResourceInfo {
references: ImageReferences;
}
export interface ImageDeleteResult {
image: ImageResourceInfo;
success: boolean;
imageIsReferencedError?: boolean;
error?: any;
references?: ImageReferences;
}
export const toImageDeleteResult = (image: ImageResourceInfo, e?: any): ImageDeleteResult => {
if (!e) {
return {image, success: true};
} else {
const result: ImageDeleteResult = {image, success: false, error: e};
if (e?.status === 400 && e?.error?.success === false && e?.error?.references) {
const references: ImageReferences = e?.error?.references;
result.imageIsReferencedError = true;
result.references = references;
}
return result;
}
};
export const imageResourceType = (imageInfo: ImageResourceInfo): ImageResourceType => export const imageResourceType = (imageInfo: ImageResourceInfo): ImageResourceType =>
(!imageInfo.tenantId || imageInfo.tenantId?.id === NULL_UUID) ? 'system' : 'tenant'; (!imageInfo.tenantId || imageInfo.tenantId?.id === NULL_UUID) ? 'system' : 'tenant';

3
ui-ngx/src/app/shared/models/rule-chain.models.ts

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

3
ui-ngx/src/app/shared/models/user.model.ts

@ -19,8 +19,9 @@ import { UserId } from './id/user-id';
import { CustomerId } from './id/customer-id'; import { CustomerId } from './id/customer-id';
import { Authority } from './authority.enum'; import { Authority } from './authority.enum';
import { TenantId } from './id/tenant-id'; import { TenantId } from './id/tenant-id';
import { HasTenantId } from '@shared/models/entity.models';
export interface User extends BaseData<UserId> { export interface User extends BaseData<UserId>, HasTenantId {
tenantId: TenantId; tenantId: TenantId;
customerId: CustomerId; customerId: CustomerId;
email: string; email: string;

3
ui-ngx/src/app/shared/models/widget.models.ts

@ -42,6 +42,7 @@ import { isNotEmptyStr } from '@core/utils';
import { WidgetConfigComponentData } from '@home/models/widget-component.models'; import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import { ComponentStyle, Font, TimewindowStyle } from '@shared/models/widget-settings.models'; import { ComponentStyle, Font, TimewindowStyle } from '@shared/models/widget-settings.models';
import { NULL_UUID } from '@shared/models/id/has-uuid'; import { NULL_UUID } from '@shared/models/id/has-uuid';
import { HasTenantId } from '@shared/models/entity.models';
export enum widgetType { export enum widgetType {
timeseries = 'timeseries', timeseries = 'timeseries',
@ -193,7 +194,7 @@ export interface WidgetControllerDescriptor {
actionSources?: {[actionSourceId: string]: WidgetActionSource}; actionSources?: {[actionSourceId: string]: WidgetActionSource};
} }
export interface BaseWidgetType extends BaseData<WidgetTypeId> { export interface BaseWidgetType extends BaseData<WidgetTypeId>, HasTenantId {
tenantId: TenantId; tenantId: TenantId;
fqn: string; fqn: string;
name: string; name: string;

3
ui-ngx/src/app/shared/models/widgets-bundle.model.ts

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

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

@ -78,7 +78,9 @@
"dont-show-again": "Do not show again", "dont-show-again": "Do not show again",
"see-documentation": "See documentation", "see-documentation": "See documentation",
"clear": "Clear", "clear": "Clear",
"upload": "Upload" "upload": "Upload",
"delete-anyway": "Delete anyway",
"delete-selected": "Delete selected"
}, },
"aggregation": { "aggregation": {
"aggregation": "Aggregation", "aggregation": "Aggregation",
@ -2968,7 +2970,18 @@
"list-mode": "List view", "list-mode": "List view",
"grid-mode": "Grid view", "grid-mode": "Grid view",
"image-preview": "Image preview", "image-preview": "Image preview",
"update-image": "Update image" "update-image": "Update image",
"export-failed-error": "Unable to export image: {{error}}",
"image-json-file": "Image JSON file",
"invalid-image-json-file-error": "Unable to import image from JSON: Invalid image JSON data structure.",
"image-is-in-use": "Image is used by other entities",
"images-are-in-use": "Images are used by other entities",
"image-is-in-use-text": "The image was not deleted because it is used by the following entities:",
"images-are-in-use-text": "Not all images have been deleted because they are used by other entities.</br>You can view referenced entities by clicking the <b>References</b> button in the corresponding image row.</br>If you still want to delete these images, select them in the table below and click the <b>Delete selected</b> button.",
"delete-image-in-use-text": "If you still want to delete the image, click the <b>Delete anyway</b> button.",
"system-entities": "System entities:",
"entities": "entities:",
"references": "References"
}, },
"image-input": { "image-input": {
"drop-images-or": "Drag and drop an images or", "drop-images-or": "Drag and drop an images or",

Loading…
Cancel
Save