52 changed files with 1986 additions and 727 deletions
@ -0,0 +1,46 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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. |
||||
|
|
||||
|
--> |
||||
|
<mat-toolbar color="primary"> |
||||
|
<h2 translate>widgets-bundle.export</h2> |
||||
|
<span fxFlex></span> |
||||
|
<button mat-icon-button |
||||
|
(click)="cancel()" |
||||
|
type="button"> |
||||
|
<mat-icon class="material-icons">close</mat-icon> |
||||
|
</button> |
||||
|
</mat-toolbar> |
||||
|
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> |
||||
|
</mat-progress-bar> |
||||
|
<div mat-dialog-content> |
||||
|
<fieldset [disabled]="isLoading$ | async"> |
||||
|
<mat-checkbox [formControl]="exportWidgetsFormControl">{{ 'widgets-bundle.export-widgets-bundle-widgets-prompt' | translate }}</mat-checkbox> |
||||
|
</fieldset> |
||||
|
</div> |
||||
|
<div mat-dialog-actions fxLayoutAlign="end center"> |
||||
|
<button mat-button color="primary" |
||||
|
type="button" |
||||
|
[disabled]="(isLoading$ | async)" |
||||
|
(click)="cancel()"> |
||||
|
{{ 'action.cancel' | translate }} |
||||
|
</button> |
||||
|
<button mat-raised-button color="primary" |
||||
|
(click)="export()" |
||||
|
[disabled]="(isLoading$ | async)"> |
||||
|
{{ 'action.export' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
@ -0,0 +1,67 @@ |
|||||
|
///
|
||||
|
/// 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 } from '@angular/core'; |
||||
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { FormControl } from '@angular/forms'; |
||||
|
import { Router } from '@angular/router'; |
||||
|
import { DialogComponent } from '@app/shared/components/dialog.component'; |
||||
|
import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; |
||||
|
|
||||
|
export interface ExportWidgetsBundleDialogData { |
||||
|
widgetsBundle: WidgetsBundle; |
||||
|
} |
||||
|
|
||||
|
export interface ExportWidgetsBundleDialogResult { |
||||
|
exportWidgets: boolean; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-export-widgets-bundle-dialog', |
||||
|
templateUrl: './export-widgets-bundle-dialog.component.html', |
||||
|
providers: [], |
||||
|
styleUrls: [] |
||||
|
}) |
||||
|
export class ExportWidgetsBundleDialogComponent extends DialogComponent<ExportWidgetsBundleDialogComponent, ExportWidgetsBundleDialogResult> |
||||
|
implements OnInit { |
||||
|
|
||||
|
widgetsBundle: WidgetsBundle; |
||||
|
|
||||
|
exportWidgetsFormControl = new FormControl(false); |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
protected router: Router, |
||||
|
@Inject(MAT_DIALOG_DATA) public data: ExportWidgetsBundleDialogData, |
||||
|
public dialogRef: MatDialogRef<ExportWidgetsBundleDialogComponent, ExportWidgetsBundleDialogResult>) { |
||||
|
super(store, router, dialogRef); |
||||
|
this.widgetsBundle = data.widgetsBundle; |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
} |
||||
|
|
||||
|
cancel(): void { |
||||
|
this.dialogRef.close(null); |
||||
|
} |
||||
|
|
||||
|
export(): void { |
||||
|
this.dialogRef.close({ |
||||
|
exportWidgets: this.exportWidgetsFormControl.value |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -1,56 +0,0 @@ |
|||||
<!-- |
|
||||
|
|
||||
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. |
|
||||
|
|
||||
--> |
|
||||
<form [formGroup]="moveWidgetTypeFormGroup" (ngSubmit)="move()"> |
|
||||
<mat-toolbar color="primary"> |
|
||||
<h2 translate>widget.move-widget-type</h2> |
|
||||
<span fxFlex></span> |
|
||||
<button mat-icon-button |
|
||||
(click)="cancel()" |
|
||||
type="button"> |
|
||||
<mat-icon class="material-icons">close</mat-icon> |
|
||||
</button> |
|
||||
</mat-toolbar> |
|
||||
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> |
|
||||
</mat-progress-bar> |
|
||||
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> |
|
||||
<div mat-dialog-content> |
|
||||
<fieldset> |
|
||||
<span translate>widget.move-widget-type-text</span> |
|
||||
<tb-widgets-bundle-select fxFlex |
|
||||
formControlName="widgetsBundle" |
|
||||
required |
|
||||
[excludeBundleIds]="[data.currentBundleId]" |
|
||||
bundlesScope="{{bundlesScope}}"> |
|
||||
</tb-widgets-bundle-select> |
|
||||
</fieldset> |
|
||||
</div> |
|
||||
<div mat-dialog-actions fxLayoutAlign="end center"> |
|
||||
<button mat-button color="primary" |
|
||||
type="button" |
|
||||
[disabled]="(isLoading$ | async)" |
|
||||
(click)="cancel()" cdkFocusInitial> |
|
||||
{{ 'action.cancel' | translate }} |
|
||||
</button> |
|
||||
<button mat-raised-button color="primary" |
|
||||
type="submit" |
|
||||
[disabled]="(isLoading$ | async) || moveWidgetTypeFormGroup.invalid |
|
||||
|| !moveWidgetTypeFormGroup.dirty"> |
|
||||
{{ 'action.move' | translate }} |
|
||||
</button> |
|
||||
</div> |
|
||||
</form> |
|
||||
@ -1,82 +0,0 @@ |
|||||
///
|
|
||||
/// 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 } from '@angular/core'; |
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; |
|
||||
import { Store } from '@ngrx/store'; |
|
||||
import { AppState } from '@core/core.state'; |
|
||||
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; |
|
||||
import { DialogComponent } from '@shared/components/dialog.component'; |
|
||||
import { Router } from '@angular/router'; |
|
||||
import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; |
|
||||
import { getCurrentAuthUser } from '@core/auth/auth.selectors'; |
|
||||
import { Authority } from '@shared/models/authority.enum'; |
|
||||
|
|
||||
export interface MoveWidgetTypeDialogResult { |
|
||||
bundleId: string; |
|
||||
bundleAlias: string; |
|
||||
} |
|
||||
|
|
||||
export interface MoveWidgetTypeDialogData { |
|
||||
currentBundleId: string; |
|
||||
} |
|
||||
|
|
||||
@Component({ |
|
||||
selector: 'tb-move-widget-type-dialog', |
|
||||
templateUrl: './move-widget-type-dialog.component.html', |
|
||||
styleUrls: [] |
|
||||
}) |
|
||||
export class MoveWidgetTypeDialogComponent extends |
|
||||
DialogComponent<MoveWidgetTypeDialogComponent, MoveWidgetTypeDialogResult> implements OnInit { |
|
||||
|
|
||||
moveWidgetTypeFormGroup: UntypedFormGroup; |
|
||||
|
|
||||
bundlesScope: string; |
|
||||
|
|
||||
constructor(protected store: Store<AppState>, |
|
||||
protected router: Router, |
|
||||
@Inject(MAT_DIALOG_DATA) public data: MoveWidgetTypeDialogData, |
|
||||
public dialogRef: MatDialogRef<MoveWidgetTypeDialogComponent, MoveWidgetTypeDialogResult>, |
|
||||
public fb: UntypedFormBuilder) { |
|
||||
super(store, router, dialogRef); |
|
||||
|
|
||||
const authUser = getCurrentAuthUser(store); |
|
||||
if (authUser.authority === Authority.TENANT_ADMIN) { |
|
||||
this.bundlesScope = 'tenant'; |
|
||||
} else { |
|
||||
this.bundlesScope = 'system'; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
ngOnInit(): void { |
|
||||
this.moveWidgetTypeFormGroup = this.fb.group({ |
|
||||
widgetsBundle: [null, [Validators.required]] |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
cancel(): void { |
|
||||
this.dialogRef.close(null); |
|
||||
} |
|
||||
|
|
||||
move(): void { |
|
||||
const widgetsBundle: WidgetsBundle = this.moveWidgetTypeFormGroup.get('widgetsBundle').value; |
|
||||
const result: MoveWidgetTypeDialogResult = { |
|
||||
bundleId: widgetsBundle.id.id, |
|
||||
bundleAlias: widgetsBundle.alias |
|
||||
}; |
|
||||
this.dialogRef.close(result); |
|
||||
} |
|
||||
} |
|
||||
@ -1,42 +0,0 @@ |
|||||
<!-- |
|
||||
|
|
||||
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. |
|
||||
|
|
||||
--> |
|
||||
<section [fxShow]="!(isLoading$ | async) && widgetsData.widgets.length === 0" fxLayoutAlign="center center" |
|
||||
style="display: flex; z-index: 1;" |
|
||||
class="tb-absolute-fill"> |
|
||||
<button mat-button *ngIf="!isReadOnly" class="tb-add-new-widget" (click)="addWidgetType($event)"> |
|
||||
<mat-icon class="tb-mat-96">add</mat-icon> |
|
||||
{{ 'widget.add-widget-type' | translate }} |
|
||||
</button> |
|
||||
<span translate *ngIf="isReadOnly" |
|
||||
fxLayoutAlign="center center" |
|
||||
style="display: flex;" |
|
||||
class="mat-headline-5 tb-absolute-fill">widgets-bundle.empty</span> |
|
||||
</section> |
|
||||
<tb-dashboard class="tb-widget-library" |
|
||||
#dashboard |
|
||||
[aliasController]="aliasController" |
|
||||
[widgets]="widgetsData.widgets" |
|
||||
[widgetLayouts]="widgetsData.widgetLayouts" |
|
||||
[isEdit]="false" |
|
||||
[isEditActionEnabled]="true" |
|
||||
[isExportActionEnabled]="true" |
|
||||
[isRemoveActionEnabled]="!isReadOnly" |
|
||||
[disableWidgetInteraction]="true" |
|
||||
[callbacks]="dashboardCallbacks"></tb-dashboard> |
|
||||
<tb-footer-fab-buttons [fxShow]="!isReadOnly" [footerFabButtons]="footerFabButtons"> |
|
||||
</tb-footer-fab-buttons> |
|
||||
@ -1,205 +0,0 @@ |
|||||
///
|
|
||||
/// 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, OnInit, ViewChild } from '@angular/core'; |
|
||||
import { Store } from '@ngrx/store'; |
|
||||
import { AppState } from '@core/core.state'; |
|
||||
import { PageComponent } from '@shared/components/page.component'; |
|
||||
import { AuthUser } from '@shared/models/user.model'; |
|
||||
import { getCurrentAuthUser } from '@core/auth/auth.selectors'; |
|
||||
import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; |
|
||||
import { ActivatedRoute, Router } from '@angular/router'; |
|
||||
import { Authority } from '@shared/models/authority.enum'; |
|
||||
import { NULL_UUID } from '@shared/models/id/has-uuid'; |
|
||||
import { of } from 'rxjs'; |
|
||||
import { Widget, widgetType } from '@app/shared/models/widget.models'; |
|
||||
import { WidgetService } from '@core/http/widget.service'; |
|
||||
import { map, mergeMap } from 'rxjs/operators'; |
|
||||
import { DialogService } from '@core/services/dialog.service'; |
|
||||
import { FooterFabButtons } from '@app/shared/components/footer-fab-buttons.component'; |
|
||||
import { DashboardCallbacks, IDashboardComponent, WidgetsData } from '@home/models/dashboard-component.models'; |
|
||||
import { IAliasController, IStateController, StateParams } from '@app/core/api/widget-api.models'; |
|
||||
import { AliasController } from '@core/api/alias-controller'; |
|
||||
import { MatDialog } from '@angular/material/dialog'; |
|
||||
import { SelectWidgetTypeDialogComponent } from '@home/pages/widget/select-widget-type-dialog.component'; |
|
||||
import { TranslateService } from '@ngx-translate/core'; |
|
||||
import { ImportExportService } from '@home/components/import-export/import-export.service'; |
|
||||
import { UtilsService } from '@core/services/utils.service'; |
|
||||
import { EntityService } from '@core/http/entity.service'; |
|
||||
|
|
||||
@Component({ |
|
||||
selector: 'tb-widget-library', |
|
||||
templateUrl: './widget-library.component.html', |
|
||||
styleUrls: ['./widget-library.component.scss'] |
|
||||
}) |
|
||||
export class WidgetLibraryComponent extends PageComponent implements OnInit { |
|
||||
|
|
||||
authUser: AuthUser; |
|
||||
|
|
||||
isReadOnly: boolean; |
|
||||
|
|
||||
widgetsBundle: WidgetsBundle; |
|
||||
|
|
||||
widgetsData: WidgetsData; |
|
||||
|
|
||||
footerFabButtons: FooterFabButtons = { |
|
||||
fabTogglerName: 'widget.add-widget-type', |
|
||||
fabTogglerIcon: 'add', |
|
||||
buttons: [ |
|
||||
{ |
|
||||
name: 'widget-type.create-new-widget-type', |
|
||||
icon: 'insert_drive_file', |
|
||||
onAction: ($event) => { |
|
||||
this.addWidgetType($event); |
|
||||
} |
|
||||
}, |
|
||||
{ |
|
||||
name: 'widget-type.import', |
|
||||
icon: 'file_upload', |
|
||||
onAction: ($event) => { |
|
||||
this.importWidgetType($event); |
|
||||
} |
|
||||
} |
|
||||
] |
|
||||
}; |
|
||||
|
|
||||
dashboardCallbacks: DashboardCallbacks = { |
|
||||
onEditWidget: this.openWidgetType.bind(this), |
|
||||
onWidgetClicked: this.openWidgetType.bind(this), |
|
||||
onExportWidget: this.exportWidgetType.bind(this), |
|
||||
onRemoveWidget: this.removeWidgetType.bind(this) |
|
||||
}; |
|
||||
|
|
||||
aliasController: IAliasController = new AliasController(this.utils, |
|
||||
this.entityService, |
|
||||
this.translate, |
|
||||
() => ({ |
|
||||
getStateParams: (): StateParams => ({}) |
|
||||
} as IStateController), |
|
||||
{}, |
|
||||
{}); |
|
||||
|
|
||||
@ViewChild('dashboard', {static: true}) dashboard: IDashboardComponent; |
|
||||
|
|
||||
constructor(protected store: Store<AppState>, |
|
||||
private route: ActivatedRoute, |
|
||||
private router: Router, |
|
||||
private widgetService: WidgetService, |
|
||||
private dialogService: DialogService, |
|
||||
private importExport: ImportExportService, |
|
||||
private dialog: MatDialog, |
|
||||
private translate: TranslateService, |
|
||||
private utils: UtilsService, |
|
||||
private entityService: EntityService) { |
|
||||
super(store); |
|
||||
|
|
||||
this.authUser = getCurrentAuthUser(this.store); |
|
||||
this.widgetsBundle = this.route.snapshot.data.widgetsBundle; |
|
||||
this.widgetsData = this.route.snapshot.data.widgetsData; |
|
||||
if (this.authUser.authority === Authority.TENANT_ADMIN) { |
|
||||
this.isReadOnly = !this.widgetsBundle || this.widgetsBundle.tenantId.id === NULL_UUID; |
|
||||
} else { |
|
||||
this.isReadOnly = this.authUser.authority !== Authority.SYS_ADMIN; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
ngOnInit(): void { |
|
||||
} |
|
||||
|
|
||||
addWidgetType($event: Event): void { |
|
||||
this.openWidgetType($event); |
|
||||
} |
|
||||
|
|
||||
importWidgetType($event: Event): void { |
|
||||
if ($event) { |
|
||||
$event.stopPropagation(); |
|
||||
} |
|
||||
this.importExport.importWidgetType(this.widgetsBundle.alias).subscribe( |
|
||||
(widgetTypeInstance) => { |
|
||||
if (widgetTypeInstance) { |
|
||||
this.reload(); |
|
||||
} |
|
||||
} |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
private reload() { |
|
||||
const bundleAlias = this.widgetsBundle.alias; |
|
||||
const isSystem = this.widgetsBundle.tenantId.id === NULL_UUID; |
|
||||
this.widgetService.loadBundleLibraryWidgets(bundleAlias, isSystem).subscribe( |
|
||||
(widgets) => { |
|
||||
this.widgetsData = {widgets}; |
|
||||
} |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
openWidgetType($event: Event, widget?: Widget): void { |
|
||||
if ($event) { |
|
||||
$event.stopPropagation(); |
|
||||
} |
|
||||
if (widget) { |
|
||||
this.router.navigate([widget.typeId.id], {relativeTo: this.route}); |
|
||||
} else { |
|
||||
this.dialog.open<SelectWidgetTypeDialogComponent, any, |
|
||||
widgetType>(SelectWidgetTypeDialogComponent, { |
|
||||
disableClose: true, |
|
||||
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'] |
|
||||
}).afterClosed().subscribe( |
|
||||
(type) => { |
|
||||
if (type) { |
|
||||
this.router.navigate([type], {relativeTo: this.route}); |
|
||||
} |
|
||||
} |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
exportWidgetType($event: Event, widget: Widget): void { |
|
||||
if ($event) { |
|
||||
$event.stopPropagation(); |
|
||||
} |
|
||||
this.importExport.exportWidgetType(widget.typeId.id); |
|
||||
} |
|
||||
|
|
||||
removeWidgetType($event: Event, widget: Widget): void { |
|
||||
if ($event) { |
|
||||
$event.stopPropagation(); |
|
||||
} |
|
||||
this.dialogService.confirm( |
|
||||
this.translate.instant('widget.remove-widget-type-title', {widgetName: widget.config.title}), |
|
||||
this.translate.instant('widget.remove-widget-type-text'), |
|
||||
this.translate.instant('action.no'), |
|
||||
this.translate.instant('action.yes'), |
|
||||
).pipe( |
|
||||
mergeMap((result) => { |
|
||||
if (result) { |
|
||||
return this.widgetService.deleteWidgetType(widget.typeFullFqn); |
|
||||
} else { |
|
||||
return of(false); |
|
||||
} |
|
||||
}), |
|
||||
map((result) => { |
|
||||
if (result !== false) { |
|
||||
this.reload(); |
|
||||
return true; |
|
||||
} else { |
|
||||
return false; |
|
||||
} |
|
||||
} |
|
||||
)).subscribe(); |
|
||||
} |
|
||||
|
|
||||
} |
|
||||
@ -0,0 +1,59 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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. |
||||
|
|
||||
|
--> |
||||
|
<mat-form-field [formGroup]="selectWidgetTypeFormGroup" class="mat-block" [floatLabel]="floatLabel" |
||||
|
[appearance]="appearance" [subscriptSizing]="subscriptSizing"> |
||||
|
<mat-label *ngIf="label">{{ label }}</mat-label> |
||||
|
<input matInput type="text" |
||||
|
#widgetTypeInput |
||||
|
placeholder="{{ placeholder }}" |
||||
|
formControlName="widgetType" |
||||
|
(focusin)="onFocus()" |
||||
|
[required]="required" |
||||
|
[matAutocomplete]="widgetTypeAutocomplete"> |
||||
|
<button *ngIf="selectWidgetTypeFormGroup.get('widgetType').value && !disabled" |
||||
|
type="button" |
||||
|
matSuffix mat-icon-button aria-label="Clear" |
||||
|
(click)="clear()"> |
||||
|
<mat-icon class="material-icons">close</mat-icon> |
||||
|
</button> |
||||
|
<mat-autocomplete |
||||
|
class="tb-autocomplete tb-widget-type-autocomplete" |
||||
|
#widgetTypeAutocomplete |
||||
|
[displayWith]="displayWidgetTypeFn"> |
||||
|
<mat-option *ngFor="let widgetType of filteredWidgetTypes | async" [value]="widgetType"> |
||||
|
<div class="tb-widget-type-option-container"> |
||||
|
<img class="tb-widget-type-option-image-preview" [src]="getPreviewImage(widgetType.image)" alt="{{ widgetType.name }}"> |
||||
|
<div class="tb-widget-type-option-details"> |
||||
|
<div class="tb-widget-type-option-text" [innerHTML]="widgetType.name | highlight:searchText:true"></div> |
||||
|
<div *ngIf="widgetType.deprecated" class="tb-widget-type-option-deprecated">{{ 'widget.deprecated' | translate }}</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</mat-option> |
||||
|
<mat-option *ngIf="!(filteredWidgetTypes | async)?.length" [value]="null"> |
||||
|
<span> |
||||
|
{{ translate.get('widget.no-widgets-matching', {entity: searchText}) | async }} |
||||
|
</span> |
||||
|
</mat-option> |
||||
|
</mat-autocomplete> |
||||
|
<mat-error> |
||||
|
<ng-content select="[tb-error]"></ng-content> |
||||
|
</mat-error> |
||||
|
<mat-hint> |
||||
|
<ng-content select="[tb-hint]"></ng-content> |
||||
|
</mat-hint> |
||||
|
</mat-form-field> |
||||
@ -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. |
||||
|
*/ |
||||
|
.tb-widget-type-autocomplete { |
||||
|
.mat-mdc-option { |
||||
|
.mdc-list-item__primary-text { |
||||
|
width: 100%; |
||||
|
.tb-widget-type-option-container { |
||||
|
display: flex; |
||||
|
gap: 8px; |
||||
|
align-items: center; |
||||
|
} |
||||
|
.tb-widget-type-option-image-preview { |
||||
|
width: 36px; |
||||
|
max-height: 100%; |
||||
|
object-fit: contain; |
||||
|
border-radius: 6px; |
||||
|
} |
||||
|
.tb-widget-type-option-details { |
||||
|
flex: 1; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: space-between; |
||||
|
.tb-widget-type-option-text { |
||||
|
font-size: 14px; |
||||
|
font-weight: 400; |
||||
|
letter-spacing: 0.2px; |
||||
|
} |
||||
|
.tb-widget-type-option-deprecated { |
||||
|
font-size: 13px; |
||||
|
font-weight: 400; |
||||
|
color: rgba(0, 0, 0, 0.54); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,227 @@ |
|||||
|
///
|
||||
|
/// 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 { |
||||
|
AfterViewInit, |
||||
|
Component, |
||||
|
ElementRef, |
||||
|
forwardRef, |
||||
|
Input, |
||||
|
OnInit, |
||||
|
ViewChild, |
||||
|
ViewEncapsulation |
||||
|
} from '@angular/core'; |
||||
|
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; |
||||
|
import { Observable, of } from 'rxjs'; |
||||
|
import { PageLink } from '@shared/models/page/page-link'; |
||||
|
import { Direction } from '@shared/models/page/sort-order'; |
||||
|
import { catchError, debounceTime, distinctUntilChanged, map, share, switchMap, tap } from 'rxjs/operators'; |
||||
|
import { emptyPageData } from '@shared/models/page/page-data'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@app/core/core.state'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { FloatLabelType, MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field'; |
||||
|
import { WidgetTypeInfo } from '@shared/models/widget.models'; |
||||
|
import { coerceBoolean } from '@shared/decorators/coercion'; |
||||
|
import { WidgetService } from '@core/http/widget.service'; |
||||
|
import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; |
||||
|
import { isDefinedAndNotNull } from '@core/utils'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-widget-type-autocomplete', |
||||
|
templateUrl: './widget-type-autocomplete.component.html', |
||||
|
styleUrls: ['./widget-type-autocomplete.component.scss'], |
||||
|
providers: [{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => WidgetTypeAutocompleteComponent), |
||||
|
multi: true |
||||
|
}], |
||||
|
encapsulation: ViewEncapsulation.None |
||||
|
}) |
||||
|
export class WidgetTypeAutocompleteComponent implements ControlValueAccessor, OnInit, AfterViewInit { |
||||
|
|
||||
|
private dirty = false; |
||||
|
|
||||
|
selectWidgetTypeFormGroup: UntypedFormGroup; |
||||
|
|
||||
|
modelValue: WidgetTypeInfo | null; |
||||
|
|
||||
|
@Input() |
||||
|
label = this.translate.instant('widget.widget'); |
||||
|
|
||||
|
@Input() |
||||
|
placeholder: string; |
||||
|
|
||||
|
@Input() |
||||
|
floatLabel: FloatLabelType = 'auto'; |
||||
|
|
||||
|
@Input() |
||||
|
appearance: MatFormFieldAppearance = 'fill'; |
||||
|
|
||||
|
@Input() |
||||
|
subscriptSizing: SubscriptSizing = 'fixed'; |
||||
|
|
||||
|
@Input() |
||||
|
@coerceBoolean() |
||||
|
required: boolean; |
||||
|
|
||||
|
@Input() |
||||
|
disabled: boolean; |
||||
|
|
||||
|
@Input() |
||||
|
excludeWidgetTypeIds: Array<string>; |
||||
|
|
||||
|
@ViewChild('widgetTypeInput', {static: true}) widgetTypeInput: ElementRef; |
||||
|
|
||||
|
filteredWidgetTypes: Observable<Array<WidgetTypeInfo>>; |
||||
|
|
||||
|
searchText = ''; |
||||
|
|
||||
|
private propagateChange = (v: any) => { }; |
||||
|
|
||||
|
constructor(private store: Store<AppState>, |
||||
|
public translate: TranslateService, |
||||
|
private widgetService: WidgetService, |
||||
|
private sanitizer: DomSanitizer, |
||||
|
private fb: UntypedFormBuilder) { |
||||
|
this.selectWidgetTypeFormGroup = this.fb.group({ |
||||
|
widgetType: [null] |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.filteredWidgetTypes = this.selectWidgetTypeFormGroup.get('widgetType').valueChanges |
||||
|
.pipe( |
||||
|
debounceTime(150), |
||||
|
tap(value => { |
||||
|
let modelValue; |
||||
|
if (typeof value === 'string' || !value) { |
||||
|
modelValue = null; |
||||
|
} else { |
||||
|
modelValue = value; |
||||
|
} |
||||
|
this.updateView(modelValue); |
||||
|
}), |
||||
|
map(value => value ? (typeof value === 'string' ? value : value.name) : ''), |
||||
|
distinctUntilChanged(), |
||||
|
switchMap(name => this.fetchWidgetTypes(name) ), |
||||
|
share() |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
ngAfterViewInit(): void { |
||||
|
} |
||||
|
|
||||
|
setDisabledState(isDisabled: boolean): void { |
||||
|
this.disabled = isDisabled; |
||||
|
if (this.disabled) { |
||||
|
this.selectWidgetTypeFormGroup.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.selectWidgetTypeFormGroup.enable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
writeValue(value: WidgetTypeInfo | string | null): void { |
||||
|
this.searchText = ''; |
||||
|
if (value != null) { |
||||
|
if (typeof value === 'string') { |
||||
|
this.widgetService.getWidgetTypeInfoById(value).subscribe( |
||||
|
(widgetType) => { |
||||
|
this.modelValue = widgetType; |
||||
|
this.selectWidgetTypeFormGroup.get('widgetType').patchValue(widgetType, {emitEvent: false}); |
||||
|
} |
||||
|
); |
||||
|
} else { |
||||
|
this.modelValue = value; |
||||
|
this.selectWidgetTypeFormGroup.get('widgetType').patchValue(value, {emitEvent: false}); |
||||
|
} |
||||
|
} else { |
||||
|
this.modelValue = null; |
||||
|
this.selectWidgetTypeFormGroup.get('widgetType').patchValue('', {emitEvent: false}); |
||||
|
} |
||||
|
this.dirty = true; |
||||
|
} |
||||
|
|
||||
|
updateView(value: WidgetTypeInfo | null) { |
||||
|
if (this.modelValue !== value) { |
||||
|
this.modelValue = value; |
||||
|
this.propagateChange(this.modelValue); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
displayWidgetTypeFn(widgetType?: WidgetTypeInfo): string | undefined { |
||||
|
return widgetType ? widgetType.name : undefined; |
||||
|
} |
||||
|
|
||||
|
getPreviewImage(imageUrl: string | null): SafeUrl | string { |
||||
|
if (isDefinedAndNotNull(imageUrl)) { |
||||
|
return this.sanitizer.bypassSecurityTrustUrl(imageUrl); |
||||
|
} |
||||
|
return '/assets/widget-preview-empty.svg'; |
||||
|
} |
||||
|
|
||||
|
fetchWidgetTypes(searchText?: string): Observable<Array<WidgetTypeInfo>> { |
||||
|
this.searchText = searchText; |
||||
|
const pageLink = new PageLink(10, 0, searchText, { |
||||
|
property: 'name', |
||||
|
direction: Direction.ASC |
||||
|
}); |
||||
|
return this.getWidgetTypes(pageLink); |
||||
|
} |
||||
|
|
||||
|
getWidgetTypes(pageLink: PageLink, |
||||
|
result: Array<WidgetTypeInfo> = []): Observable<Array<WidgetTypeInfo>> { |
||||
|
return this.widgetService.getWidgetTypes(pageLink, true).pipe( |
||||
|
catchError(() => of(emptyPageData<WidgetTypeInfo>())), |
||||
|
switchMap((data) => { |
||||
|
if (this.excludeWidgetTypeIds?.length) { |
||||
|
const filtered = data.data.filter(w => !this.excludeWidgetTypeIds.includes(w.id.id)); |
||||
|
result = result.concat(filtered); |
||||
|
} else { |
||||
|
result = data.data; |
||||
|
} |
||||
|
if (result.length >= pageLink.pageSize || !this.excludeWidgetTypeIds?.length || !data.hasNext) { |
||||
|
return of(result); |
||||
|
} else { |
||||
|
return this.getWidgetTypes(pageLink.nextPageLink(), result); |
||||
|
} |
||||
|
}) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
onFocus() { |
||||
|
if (this.dirty) { |
||||
|
this.selectWidgetTypeFormGroup.get('widgetType').updateValueAndValidity({onlySelf: true}); |
||||
|
this.dirty = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
clear() { |
||||
|
this.selectWidgetTypeFormGroup.get('widgetType').patchValue(''); |
||||
|
setTimeout(() => { |
||||
|
this.widgetTypeInput.nativeElement.blur(); |
||||
|
this.widgetTypeInput.nativeElement.focus(); |
||||
|
}, 0); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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. |
||||
|
|
||||
|
--> |
||||
|
<mat-tab *ngIf="isTenantWidgetType() && authUser.authority === authorities.TENANT_ADMIN" |
||||
|
label="{{ 'version-control.version-control' | translate }}" #versionControlTab="matTab"> |
||||
|
<tb-version-control detailsMode="true" singleEntityMode="true" |
||||
|
(versionRestored)="entitiesTableConfig.updateData()" |
||||
|
[active]="versionControlTab.isActive" [entityId]="entity.id" [entityName]="entity.name" [externalEntityId]="entity.externalId || entity.id"></tb-version-control> |
||||
|
</mat-tab> |
||||
@ -0,0 +1,43 @@ |
|||||
|
///
|
||||
|
/// 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 } from '@angular/core'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { EntityTabsComponent } from '../../components/entity/entity-tabs.component'; |
||||
|
import { NULL_UUID } from '@shared/models/id/has-uuid'; |
||||
|
import { WidgetTypeDetails } from '@shared/models/widget.models'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-widget-type-tabs', |
||||
|
templateUrl: './widget-type-tabs.component.html', |
||||
|
styleUrls: [] |
||||
|
}) |
||||
|
export class WidgetTypeTabsComponent extends EntityTabsComponent<WidgetTypeDetails> { |
||||
|
|
||||
|
constructor(protected store: Store<AppState>) { |
||||
|
super(store); |
||||
|
} |
||||
|
|
||||
|
isTenantWidgetType() { |
||||
|
return this.entity && this.entity.tenantId.id !== NULL_UUID; |
||||
|
} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
super.ngOnInit(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,66 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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. |
||||
|
|
||||
|
--> |
||||
|
<div class="tb-details-buttons" fxLayout.xs="column"> |
||||
|
<button mat-raised-button color="primary" |
||||
|
[disabled]="(isLoading$ | async)" |
||||
|
(click)="onEntityAction($event, 'edit')" |
||||
|
[fxShow]="!isEdit"> |
||||
|
{{'widget.edit-widget-type' | translate }} |
||||
|
</button> |
||||
|
<button mat-raised-button color="primary" |
||||
|
[disabled]="(isLoading$ | async)" |
||||
|
(click)="onEntityAction($event, 'export')" |
||||
|
[fxShow]="!isEdit"> |
||||
|
{{'widget-type.export' | translate }} |
||||
|
</button> |
||||
|
<button mat-raised-button color="primary" |
||||
|
[disabled]="(isLoading$ | async)" |
||||
|
(click)="onEntityAction($event, 'delete')" |
||||
|
[fxShow]="!hideDelete() && !isEdit"> |
||||
|
{{'widget.delete-widget-type' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
<div class="mat-padding" fxLayout="column"> |
||||
|
<form [formGroup]="entityForm"> |
||||
|
<fieldset [disabled]="(isLoading$ | async) || !isEdit"> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label translate>widget.title</mat-label> |
||||
|
<input matInput formControlName="name" required> |
||||
|
<mat-error *ngIf="entityForm.get('name').hasError('required')"> |
||||
|
{{ 'widget.title-required' | translate }} |
||||
|
</mat-error> |
||||
|
<mat-error *ngIf="entityForm.get('name').hasError('maxlength')"> |
||||
|
{{ 'widget.title-max-length' | translate }} |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
<tb-image-input fxFlex |
||||
|
label="{{'widget.image-preview' | translate}}" |
||||
|
maxSizeByte="524288" |
||||
|
formControlName="image"> |
||||
|
</tb-image-input> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label translate>widget.description</mat-label> |
||||
|
<textarea matInput formControlName="description" rows="2" maxlength="255" #descriptionInput></textarea> |
||||
|
<mat-hint align="end">{{descriptionInput.value?.length || 0}}/255</mat-hint> |
||||
|
</mat-form-field> |
||||
|
<mat-slide-toggle formControlName="deprecated"> |
||||
|
{{ 'widget.deprecated' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
</fieldset> |
||||
|
</form> |
||||
|
</div> |
||||
@ -0,0 +1,68 @@ |
|||||
|
///
|
||||
|
/// 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, Inject } from '@angular/core'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { EntityComponent } from '../../components/entity/entity.component'; |
||||
|
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; |
||||
|
import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; |
||||
|
import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; |
||||
|
import { WidgetTypeDetails } from '@shared/models/widget.models'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-widget-type', |
||||
|
templateUrl: './widget-type.component.html', |
||||
|
styleUrls: [] |
||||
|
}) |
||||
|
export class WidgetTypeComponent extends EntityComponent<WidgetTypeDetails> { |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
@Inject('entity') protected entityValue: WidgetTypeDetails, |
||||
|
@Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<WidgetTypeDetails>, |
||||
|
public fb: UntypedFormBuilder, |
||||
|
protected cd: ChangeDetectorRef) { |
||||
|
super(store, fb, entityValue, entitiesTableConfigValue, cd); |
||||
|
} |
||||
|
|
||||
|
hideDelete() { |
||||
|
if (this.entitiesTableConfig) { |
||||
|
return !this.entitiesTableConfig.deleteEnabled(this.entity); |
||||
|
} else { |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
buildForm(entity: WidgetTypeDetails): UntypedFormGroup { |
||||
|
return this.fb.group( |
||||
|
{ |
||||
|
name: [entity ? entity.name : '', [Validators.required, Validators.maxLength(255)]], |
||||
|
image: [entity ? entity.image : ''], |
||||
|
description: [entity ? entity.description : '', Validators.maxLength(255)], |
||||
|
deprecated: [entity ? entity.deprecated : false] |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
updateForm(entity: WidgetTypeDetails) { |
||||
|
this.entityForm.patchValue({ |
||||
|
name: entity.name, |
||||
|
image: entity.image, |
||||
|
description: entity.description, |
||||
|
deprecated: entity.deprecated |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,232 @@ |
|||||
|
///
|
||||
|
/// 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 { Injectable } from '@angular/core'; |
||||
|
|
||||
|
import { Resolve, Router } from '@angular/router'; |
||||
|
import { |
||||
|
checkBoxCell, |
||||
|
DateEntityTableColumn, |
||||
|
EntityTableColumn, |
||||
|
EntityTableConfig |
||||
|
} from '@home/models/entity/entities-table-config.models'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { DatePipe } from '@angular/common'; |
||||
|
import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; |
||||
|
import { EntityAction } from '@home/models/entity/entity-component.models'; |
||||
|
import { WidgetService } from '@app/core/http/widget.service'; |
||||
|
import { NULL_UUID } from '@shared/models/id/has-uuid'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { getCurrentAuthState, getCurrentAuthUser } from '@app/core/auth/auth.selectors'; |
||||
|
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 { Direction } from '@shared/models/page/sort-order'; |
||||
|
import { |
||||
|
BaseWidgetType, |
||||
|
WidgetTypeDetails, |
||||
|
WidgetTypeInfo, |
||||
|
widgetType as WidgetDataType, |
||||
|
widgetTypesData |
||||
|
} from '@shared/models/widget.models'; |
||||
|
import { WidgetTypeComponent } from '@home/pages/widget/widget-type.component'; |
||||
|
import { WidgetTypeTabsComponent } from '@home/pages/widget/widget-type-tabs.component'; |
||||
|
import { SelectWidgetTypeDialogComponent } from '@home/pages/widget/select-widget-type-dialog.component'; |
||||
|
import { MatDialog } from '@angular/material/dialog'; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class WidgetTypesTableConfigResolver implements Resolve<EntityTableConfig<WidgetTypeInfo | WidgetTypeDetails>> { |
||||
|
|
||||
|
private readonly config: EntityTableConfig<WidgetTypeInfo | WidgetTypeDetails> = |
||||
|
new EntityTableConfig<WidgetTypeInfo | WidgetTypeDetails>(); |
||||
|
|
||||
|
constructor(private store: Store<AppState>, |
||||
|
private dialog: MatDialog, |
||||
|
private widgetsService: WidgetService, |
||||
|
private translate: TranslateService, |
||||
|
private importExport: ImportExportService, |
||||
|
private datePipe: DatePipe, |
||||
|
private router: Router) { |
||||
|
|
||||
|
this.config.entityType = EntityType.WIDGETS_BUNDLE; |
||||
|
this.config.entityComponent = WidgetTypeComponent; |
||||
|
this.config.entityTabsComponent = WidgetTypeTabsComponent; |
||||
|
this.config.entityTranslations = entityTypeTranslations.get(EntityType.WIDGET_TYPE); |
||||
|
this.config.entityResources = entityTypeResources.get(EntityType.WIDGET_TYPE); |
||||
|
this.config.defaultSortOrder = {property: 'name', direction: Direction.ASC}; |
||||
|
|
||||
|
this.config.rowPointer = true; |
||||
|
|
||||
|
this.config.entityTitle = (widgetType) => widgetType ? |
||||
|
widgetType.name : ''; |
||||
|
|
||||
|
this.config.columns.push( |
||||
|
new DateEntityTableColumn<WidgetTypeInfo>('createdTime', 'common.created-time', this.datePipe, '150px'), |
||||
|
new EntityTableColumn<WidgetTypeInfo>('name', 'widget.title', '100%'), |
||||
|
new EntityTableColumn<WidgetTypeInfo>('widgetType', 'widget.type', '150px', entity => |
||||
|
entity?.widgetType ? this.translate.instant(widgetTypesData.get(entity.widgetType).name) : '', undefined, false), |
||||
|
new EntityTableColumn<WidgetTypeInfo>('tenantId', 'widget.system', '60px', |
||||
|
entity => checkBoxCell(entity.tenantId.id === NULL_UUID)), |
||||
|
new EntityTableColumn<WidgetTypeInfo>('deprecated', 'widget.deprecated', '60px', |
||||
|
entity => checkBoxCell(entity.deprecated)) |
||||
|
); |
||||
|
|
||||
|
this.config.addActionDescriptors.push( |
||||
|
{ |
||||
|
name: this.translate.instant('widget-type.create-new-widget-type'), |
||||
|
icon: 'insert_drive_file', |
||||
|
isEnabled: () => true, |
||||
|
onAction: ($event) => this.addWidgetType($event) |
||||
|
}, |
||||
|
{ |
||||
|
name: this.translate.instant('widget-type.import'), |
||||
|
icon: 'file_upload', |
||||
|
isEnabled: () => true, |
||||
|
onAction: ($event) => this.importWidgetType($event) |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
this.config.cellActionDescriptors.push( |
||||
|
{ |
||||
|
name: this.translate.instant('widget-type.export'), |
||||
|
icon: 'file_download', |
||||
|
isEnabled: () => true, |
||||
|
onAction: ($event, entity) => this.exportWidgetType($event, entity) |
||||
|
}, |
||||
|
{ |
||||
|
name: this.translate.instant('widget.widget-type-details'), |
||||
|
icon: 'edit', |
||||
|
isEnabled: () => true, |
||||
|
onAction: ($event, entity) => this.config.toggleEntityDetails($event, entity) |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
this.config.groupActionDescriptors.push( |
||||
|
{ |
||||
|
name: this.translate.instant('widget-type.export-widget-types'), |
||||
|
icon: 'file_download', |
||||
|
isEnabled: true, |
||||
|
onAction: ($event, entities) => this.exportWidgetTypes($event, entities) |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
this.config.deleteEntityTitle = widgetType => this.translate.instant('widget.delete-widget-type-title', |
||||
|
{ widgetTypeName: widgetType.name }); |
||||
|
this.config.deleteEntityContent = () => this.translate.instant('widget.delete-widget-type-text'); |
||||
|
this.config.deleteEntitiesTitle = count => this.translate.instant('widget.delete-widget-types-title', {count}); |
||||
|
this.config.deleteEntitiesContent = () => this.translate.instant('widget.delete-widget-types-text'); |
||||
|
|
||||
|
|
||||
|
this.config.loadEntity = id => this.widgetsService.getWidgetTypeById(id.id); |
||||
|
this.config.saveEntity = widgetType => this.widgetsService.saveWidgetType(widgetType as WidgetTypeDetails); |
||||
|
this.config.deleteEntity = id => this.widgetsService.deleteWidgetType(id.id); |
||||
|
this.config.onEntityAction = action => this.onWidgetTypeAction(action); |
||||
|
|
||||
|
this.config.handleRowClick = ($event, widgetType) => { |
||||
|
if (this.config.isDetailsOpen()) { |
||||
|
this.config.toggleEntityDetails($event, widgetType); |
||||
|
} else { |
||||
|
this.openWidgetEditor($event, widgetType); |
||||
|
} |
||||
|
return true; |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
resolve(): EntityTableConfig<WidgetTypeInfo | WidgetTypeDetails> { |
||||
|
this.config.tableTitle = this.translate.instant('widget.widget-types'); |
||||
|
const authUser = getCurrentAuthUser(this.store); |
||||
|
this.config.deleteEnabled = (widgetType) => this.isWidgetTypeEditable(widgetType, authUser.authority); |
||||
|
this.config.entitySelectionEnabled = (widgetType) => this.isWidgetTypeEditable(widgetType, authUser.authority); |
||||
|
this.config.detailsReadonly = (widgetType) => !this.isWidgetTypeEditable(widgetType, authUser.authority); |
||||
|
const authState = getCurrentAuthState(this.store); |
||||
|
this.config.entitiesFetchFunction = pageLink => this.widgetsService.getWidgetTypes(pageLink); |
||||
|
return this.config; |
||||
|
} |
||||
|
|
||||
|
isWidgetTypeEditable(widgetType: BaseWidgetType, authority: Authority): boolean { |
||||
|
if (authority === Authority.TENANT_ADMIN) { |
||||
|
return widgetType && widgetType.tenantId && widgetType.tenantId.id !== NULL_UUID; |
||||
|
} else { |
||||
|
return authority === Authority.SYS_ADMIN; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
addWidgetType($event: Event): void { |
||||
|
if ($event) { |
||||
|
$event.stopPropagation(); |
||||
|
} |
||||
|
this.dialog.open<SelectWidgetTypeDialogComponent, any, |
||||
|
WidgetDataType>(SelectWidgetTypeDialogComponent, { |
||||
|
disableClose: true, |
||||
|
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'] |
||||
|
}).afterClosed().subscribe( |
||||
|
(type) => { |
||||
|
if (type) { |
||||
|
this.router.navigateByUrl(`resources/widgets-library/widget-types/${type}`); |
||||
|
} |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
importWidgetType($event: Event) { |
||||
|
this.importExport.importWidgetType().subscribe( |
||||
|
(widgetType) => { |
||||
|
if (widgetType) { |
||||
|
this.config.updateData(); |
||||
|
} |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
openWidgetEditor($event: Event, widgetType: BaseWidgetType) { |
||||
|
if ($event) { |
||||
|
$event.stopPropagation(); |
||||
|
} |
||||
|
this.router.navigateByUrl(`resources/widgets-library/widget-types/${widgetType.id.id}`); |
||||
|
} |
||||
|
|
||||
|
exportWidgetType($event: Event, widgetType: BaseWidgetType) { |
||||
|
if ($event) { |
||||
|
$event.stopPropagation(); |
||||
|
} |
||||
|
this.importExport.exportWidgetType(widgetType.id.id); |
||||
|
} |
||||
|
|
||||
|
exportWidgetTypes($event: Event, widgetTypes: BaseWidgetType[]) { |
||||
|
if ($event) { |
||||
|
$event.stopPropagation(); |
||||
|
} |
||||
|
this.importExport.exportWidgetTypes(widgetTypes.map(w => w.id.id)).subscribe( |
||||
|
() => { |
||||
|
this.config.getTable().clearSelection(); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
onWidgetTypeAction(action: EntityAction<BaseWidgetType>): boolean { |
||||
|
switch (action.action) { |
||||
|
case 'edit': |
||||
|
this.openWidgetEditor(action.event, action.entity); |
||||
|
return true; |
||||
|
case 'export': |
||||
|
this.exportWidgetType(action.event, action.entity); |
||||
|
return true; |
||||
|
} |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,134 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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. |
||||
|
|
||||
|
--> |
||||
|
<mat-card appearance="outlined" class="tb-bundle-widgets-card"> |
||||
|
<mat-card-header> |
||||
|
<mat-card-title> |
||||
|
<div class="title-container"> |
||||
|
<button mat-icon-button |
||||
|
matTooltip="{{ 'action.back' | translate }}" |
||||
|
matTooltipPosition="above" |
||||
|
(click)="goBack()"> |
||||
|
<mat-icon>arrow_back</mat-icon> |
||||
|
</button> |
||||
|
<span class="mat-headline-5">{{ widgetsBundle.title }}: {{ 'widget.widgets' | translate }}</span> |
||||
|
<span fxFlex></span> |
||||
|
<button mat-icon-button |
||||
|
matTooltip="{{ 'widgets-bundle.export' | translate }}" |
||||
|
matTooltipPosition="above" |
||||
|
(click)="exportWidgetsBundle()"> |
||||
|
<mat-icon>file_download</mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
</mat-card-title> |
||||
|
</mat-card-header> |
||||
|
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> |
||||
|
</mat-progress-bar> |
||||
|
<div style="height: 4px;" *ngIf="(isLoading$ | async) === false"></div> |
||||
|
<mat-card-content> |
||||
|
<div class="tb-drop-list tb-bundle-widgets-container" cdkDropList cdkDropListOrientation="vertical" |
||||
|
(cdkDropListDropped)="widgetDrop($event)" [cdkDropListDisabled]="!editMode || widgets?.length < 2"> |
||||
|
<div cdkDrag class="tb-draggable tb-bundle-widget-row" *ngFor="let widget of widgets; trackBy: trackByWidget; |
||||
|
let $index = index; last as isLast;"> |
||||
|
<div class="tb-bundle-widget-row-details"> |
||||
|
<img class="tb-bundle-widget-image-preview" [src]="getPreviewImage(widget.image)" alt="{{ widget.name }}"> |
||||
|
<div class="tb-bundle-widget-details"> |
||||
|
<div class="tb-bundle-widget-title">{{ widget.name }}</div> |
||||
|
<div *ngIf="widget.deprecated" class="tb-bundle-widget-deprecated">{{ 'widget.deprecated' | translate }}</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<button *ngIf="!editMode" |
||||
|
mat-icon-button |
||||
|
(click)="openWidgetEditor($event, widget)" |
||||
|
matTooltip="{{'widget.edit-widget-type' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>edit</mat-icon> |
||||
|
</button> |
||||
|
<button *ngIf="!editMode" |
||||
|
mat-icon-button |
||||
|
(click)="exportWidgetType($event, widget)" |
||||
|
matTooltip="{{'widget-type.export' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>file_download</mat-icon> |
||||
|
</button> |
||||
|
<button *ngIf="editMode && !addMode" |
||||
|
mat-icon-button |
||||
|
(click)="removeWidgetType($event, widget)" |
||||
|
matTooltip="{{'widget.remove' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>delete</mat-icon> |
||||
|
</button> |
||||
|
<button cdkDragHandle *ngIf="editMode" |
||||
|
[fxShow]="widgets?.length > 1" |
||||
|
mat-icon-button |
||||
|
matTooltip="{{ 'action.drag' | translate }}" |
||||
|
matTooltipPosition="above" |
||||
|
class="tb-drag-handle"> |
||||
|
<tb-icon>drag_indicator</tb-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div *ngIf="editMode && !addMode && widgets?.length" class="tb-add-widget-button" |
||||
|
matTooltip="{{ 'widget.add' | translate }}" |
||||
|
matTooltipPosition="above" |
||||
|
(click)="addWidgetMode()"> |
||||
|
<tb-icon color="primary" class="tb-add-widget-icon">add</tb-icon> |
||||
|
</div> |
||||
|
<div *ngIf="editMode && !addMode && !widgets?.length" class="tb-add-widget-button-panel"> |
||||
|
<div class="tb-add-widget-button-with-text" |
||||
|
matTooltip="{{ 'widget.add' | translate }}" |
||||
|
matTooltipPosition="above" |
||||
|
(click)="addWidgetMode()"> |
||||
|
<tb-icon color="primary" class="tb-add-widget-icon">add</tb-icon> |
||||
|
<div class="tb-add-widget-button-text" translate>widget.add</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div *ngIf="isReadOnly && !widgets?.length" class="tb-no-data-available"> |
||||
|
<div class="tb-no-data-bg"></div> |
||||
|
<div class="tb-no-data-text" translate>widget.no-widgets</div> |
||||
|
</div> |
||||
|
<div *ngIf="addMode" class="tb-add-bundle-widget-form"> |
||||
|
<tb-widget-type-autocomplete |
||||
|
required |
||||
|
placeholder="{{ 'widget.select-widget' | translate }}" |
||||
|
[excludeWidgetTypeIds]="excludeWidgetTypeIds" |
||||
|
[formControl]="addWidgetFormControl"> |
||||
|
</tb-widget-type-autocomplete> |
||||
|
<div class="tb-add-bundle-widget-actions"> |
||||
|
<button mat-button color="primary" |
||||
|
[disabled]="(isLoading$ | async)" |
||||
|
(click)="cancelAddWidgetMode()">{{'action.cancel' | translate}} |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</mat-card-content> |
||||
|
<mat-card-actions> |
||||
|
<button mat-button color="primary" *ngIf="editMode && (widgets?.length || isDirty)" |
||||
|
[disabled]="(isLoading$ | async)" |
||||
|
(click)="cancel()">{{'action.cancel' | translate}} |
||||
|
</button> |
||||
|
<button mat-raised-button color="primary" *ngIf="editMode && (widgets?.length || isDirty)" |
||||
|
[disabled]="(isLoading$ | async) || !isDirty" |
||||
|
(click)="save()">{{'action.save' | translate}} |
||||
|
</button> |
||||
|
<button mat-raised-button color="primary" *ngIf="!isReadOnly && !editMode" |
||||
|
[disabled]="(isLoading$ | async)" |
||||
|
(click)="edit()">{{'action.edit' | translate}} |
||||
|
</button> |
||||
|
</mat-card-actions> |
||||
|
</mat-card> |
||||
|
|
||||
@ -0,0 +1,184 @@ |
|||||
|
/** |
||||
|
* 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 "../../../../../scss/constants"; |
||||
|
|
||||
|
:host { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
padding: 8px; |
||||
|
} |
||||
|
|
||||
|
:host ::ng-deep { |
||||
|
.tb-bundle-widgets-card { |
||||
|
.mat-mdc-card-header-text { |
||||
|
flex: 1; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.tb-bundle-widgets-card { |
||||
|
padding: 0; |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
|
||||
|
@media #{$mat-md} { |
||||
|
width: 80%; |
||||
|
} |
||||
|
|
||||
|
@media #{$mat-gt-md} { |
||||
|
width: 60%; |
||||
|
} |
||||
|
|
||||
|
.title-container { |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
align-items: center; |
||||
|
gap: 8px; |
||||
|
.mat-headline-5 { |
||||
|
margin: 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.mat-mdc-card-content { |
||||
|
flex: 1; |
||||
|
padding: 16px; |
||||
|
min-height: 0; |
||||
|
overflow: hidden; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
gap: 8px; |
||||
|
} |
||||
|
|
||||
|
.tb-add-widget-button { |
||||
|
min-height: 56px; |
||||
|
cursor: pointer; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
background: #FFFFFF; |
||||
|
padding: 8px; |
||||
|
border: 2px dashed rgba(0, 0, 0, 0.08); |
||||
|
border-radius: 10px; |
||||
|
} |
||||
|
|
||||
|
.tb-add-widget-button-panel { |
||||
|
flex: 1; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
.tb-add-widget-button-with-text { |
||||
|
cursor: pointer; |
||||
|
display: flex; |
||||
|
padding: 24px 40px 24px 24px; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
gap: 8px; |
||||
|
border: 2px dashed rgba(0, 0, 0, 0.12); |
||||
|
border-radius: 10px; |
||||
|
.tb-add-widget-button-text { |
||||
|
font-size: 16px; |
||||
|
font-style: normal; |
||||
|
font-weight: 500; |
||||
|
line-height: 24px; |
||||
|
letter-spacing: 0.25px; |
||||
|
color: $tb-primary-color; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.tb-add-bundle-widget-form { |
||||
|
display: flex; |
||||
|
padding: 16px; |
||||
|
flex-direction: column; |
||||
|
gap: 12px; |
||||
|
border-radius: 10px; |
||||
|
border: 1px solid rgba(0, 0, 0, 0.05); |
||||
|
box-shadow: 0 5px 16px 0 rgba(0, 0, 0, 0.04); |
||||
|
} |
||||
|
|
||||
|
.tb-add-bundle-widget-actions { |
||||
|
display: flex; |
||||
|
justify-content: flex-end; |
||||
|
align-items: flex-start; |
||||
|
gap: 16px; |
||||
|
} |
||||
|
|
||||
|
.mat-mdc-card-actions { |
||||
|
justify-content: end; |
||||
|
gap: 8px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.tb-bundle-widgets-container { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
gap: 8px; |
||||
|
min-height: 0; |
||||
|
overflow: auto; |
||||
|
padding-top: 8px; |
||||
|
padding-bottom: 8px; |
||||
|
} |
||||
|
|
||||
|
.tb-bundle-widget-row { |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
align-items: center; |
||||
|
padding: 6px 12px 6px 6px; |
||||
|
background: #fff; |
||||
|
gap: 12px; |
||||
|
border-radius: 10px; |
||||
|
border: 1px solid rgba(0, 0, 0, 0.05); |
||||
|
box-shadow: 0 5px 16px 0 rgba(0, 0, 0, 0.04); |
||||
|
.tb-bundle-widget-row-details { |
||||
|
flex: 1; |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
align-items: center; |
||||
|
gap: 12px; |
||||
|
.tb-bundle-widget-image-preview { |
||||
|
width: 64px; |
||||
|
max-height: 100%; |
||||
|
object-fit: contain; |
||||
|
border-radius: 6px; |
||||
|
} |
||||
|
.tb-bundle-widget-details { |
||||
|
padding: 18px 0; |
||||
|
flex: 1; |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
align-items: center; |
||||
|
justify-content: space-between; |
||||
|
.tb-bundle-widget-title { |
||||
|
font-size: 14px; |
||||
|
font-weight: 400; |
||||
|
letter-spacing: 0.2px; |
||||
|
color: rgba(0, 0, 0, 0.76); |
||||
|
} |
||||
|
.tb-bundle-widget-deprecated { |
||||
|
font-size: 13px; |
||||
|
font-weight: 400; |
||||
|
color: rgba(0, 0, 0, 0.54); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
.mat-icon { |
||||
|
color: rgba(0, 0, 0, 0.54); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,178 @@ |
|||||
|
///
|
||||
|
/// 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, OnInit } from '@angular/core'; |
||||
|
import { PageComponent } from '@shared/components/page.component'; |
||||
|
import { AuthUser } from '@shared/models/user.model'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { ActivatedRoute, Router } from '@angular/router'; |
||||
|
import { getCurrentAuthUser } from '@core/auth/auth.selectors'; |
||||
|
import { Authority } from '@shared/models/authority.enum'; |
||||
|
import { NULL_UUID } from '@shared/models/id/has-uuid'; |
||||
|
import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; |
||||
|
import { WidgetTypeInfo } from '@shared/models/widget.models'; |
||||
|
import { CdkDragDrop } from '@angular/cdk/drag-drop'; |
||||
|
import { ImportExportService } from '@home/components/import-export/import-export.service'; |
||||
|
import { WidgetService } from '@core/http/widget.service'; |
||||
|
import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; |
||||
|
import { isDefinedAndNotNull } from '@core/utils'; |
||||
|
import { FormControl, Validators } from '@angular/forms'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-widgets-bundle-widget', |
||||
|
templateUrl: './widgets-bundle-widgets.component.html', |
||||
|
styleUrls: ['./widgets-bundle-widgets.component.scss'] |
||||
|
}) |
||||
|
export class WidgetsBundleWidgetsComponent extends PageComponent implements OnInit { |
||||
|
|
||||
|
authUser: AuthUser; |
||||
|
|
||||
|
isReadOnly: boolean; |
||||
|
editMode = false; |
||||
|
addMode = false; |
||||
|
isDirty = false; |
||||
|
|
||||
|
widgetsBundle: WidgetsBundle; |
||||
|
widgets: Array<WidgetTypeInfo>; |
||||
|
excludeWidgetTypeIds: Array<string>; |
||||
|
|
||||
|
addWidgetFormControl = new FormControl(null, [Validators.required]); |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
private router: Router, |
||||
|
private route: ActivatedRoute, |
||||
|
private widgetsService: WidgetService, |
||||
|
private importExport: ImportExportService, |
||||
|
private sanitizer: DomSanitizer, |
||||
|
private cd: ChangeDetectorRef) { |
||||
|
super(store); |
||||
|
this.authUser = getCurrentAuthUser(this.store); |
||||
|
this.widgetsBundle = this.route.snapshot.data.widgetsBundle; |
||||
|
this.widgets = [...this.route.snapshot.data.widgets]; |
||||
|
if (this.authUser.authority === Authority.TENANT_ADMIN) { |
||||
|
this.isReadOnly = !this.widgetsBundle || this.widgetsBundle.tenantId.id === NULL_UUID; |
||||
|
} else { |
||||
|
this.isReadOnly = this.authUser.authority !== Authority.SYS_ADMIN; |
||||
|
} |
||||
|
if (!this.isReadOnly && !this.widgets.length) { |
||||
|
this.editMode = true; |
||||
|
} |
||||
|
this.addWidgetFormControl.valueChanges.subscribe((newWidget) => { |
||||
|
if (newWidget) { |
||||
|
this.addWidget(newWidget); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
} |
||||
|
|
||||
|
getPreviewImage(imageUrl: string | null): SafeUrl | string { |
||||
|
if (isDefinedAndNotNull(imageUrl)) { |
||||
|
return this.sanitizer.bypassSecurityTrustUrl(imageUrl); |
||||
|
} |
||||
|
return '/assets/widget-preview-empty.svg'; |
||||
|
} |
||||
|
|
||||
|
trackByWidget(index: number, widget: WidgetTypeInfo): any { |
||||
|
return widget; |
||||
|
} |
||||
|
|
||||
|
widgetDrop(event: CdkDragDrop<string[]>) { |
||||
|
const widget = this.widgets[event.previousIndex]; |
||||
|
this.widgets.splice(event.previousIndex, 1); |
||||
|
this.widgets.splice(event.currentIndex, 0, widget); |
||||
|
this.isDirty = true; |
||||
|
} |
||||
|
|
||||
|
addWidgetMode() { |
||||
|
this.addWidgetFormControl.patchValue(null, {emitEvent: false}); |
||||
|
this.excludeWidgetTypeIds = this.widgets.map(w => w.id.id); |
||||
|
this.addMode = true; |
||||
|
} |
||||
|
|
||||
|
cancelAddWidgetMode() { |
||||
|
this.addMode = false; |
||||
|
} |
||||
|
|
||||
|
private addWidget(newWidget: WidgetTypeInfo) { |
||||
|
this.widgets.push(newWidget); |
||||
|
this.isDirty = true; |
||||
|
this.addMode = false; |
||||
|
} |
||||
|
|
||||
|
openWidgetEditor($event: Event, widgetType: WidgetTypeInfo) { |
||||
|
if ($event) { |
||||
|
$event.stopPropagation(); |
||||
|
} |
||||
|
this.router.navigateByUrl(`resources/widgets-library/widget-types/${widgetType.id.id}`); |
||||
|
} |
||||
|
|
||||
|
exportWidgetType($event: Event, widgetType: WidgetTypeInfo) { |
||||
|
if ($event) { |
||||
|
$event.stopPropagation(); |
||||
|
} |
||||
|
this.importExport.exportWidgetType(widgetType.id.id); |
||||
|
} |
||||
|
|
||||
|
removeWidgetType($event: Event, widgetType: WidgetTypeInfo) { |
||||
|
if ($event) { |
||||
|
$event.stopPropagation(); |
||||
|
} |
||||
|
const index = this.widgets.indexOf(widgetType); |
||||
|
this.widgets.splice(index, 1); |
||||
|
this.isDirty = true; |
||||
|
} |
||||
|
|
||||
|
goBack() { |
||||
|
this.router.navigate(['..'], { relativeTo: this.route }); |
||||
|
} |
||||
|
|
||||
|
exportWidgetsBundle() { |
||||
|
this.importExport.exportWidgetsBundle(this.widgetsBundle.id.id); |
||||
|
} |
||||
|
|
||||
|
edit() { |
||||
|
this.editMode = true; |
||||
|
} |
||||
|
|
||||
|
cancel() { |
||||
|
if (this.isDirty) { |
||||
|
this.widgetsService.getBundleWidgetTypeInfos(this.widgetsBundle.id.id).subscribe( |
||||
|
(widgets) => { |
||||
|
this.widgets = [...widgets]; |
||||
|
this.isDirty = false; |
||||
|
this.addMode = false; |
||||
|
this.editMode = !this.widgets.length; |
||||
|
this.cd.markForCheck(); |
||||
|
} |
||||
|
); |
||||
|
} else { |
||||
|
this.addMode = false; |
||||
|
this.editMode = !this.widgets.length; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
save() { |
||||
|
const widgetTypeIds = this.widgets.map(w => w.id.id); |
||||
|
this.widgetsService.updateWidgetsBundleWidgetTypes(this.widgetsBundle.id.id, widgetTypeIds).subscribe(() => { |
||||
|
this.isDirty = false; |
||||
|
this.editMode = false; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
} |
||||
Loading…
Reference in new issue