16 changed files with 991 additions and 32 deletions
@ -0,0 +1,60 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2019 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 #importForm="ngForm" [formGroup]="importFormGroup" (ngSubmit)="importFromJson()"> |
||||
|
<mat-toolbar fxLayout="row" color="primary"> |
||||
|
<h2 translate>{{ importTitle }}</h2> |
||||
|
<span fxFlex></span> |
||||
|
<button mat-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"> |
||||
|
<div fxFlex fxLayout="column"> |
||||
|
<tb-file-input |
||||
|
[contentConvertFunction]="loadDataFromJsonContent" |
||||
|
formControlName="jsonContent" |
||||
|
required |
||||
|
label="{{importFileLabel | translate}}" |
||||
|
dropLabel="{{ 'import.drop-file' | translate }}" |
||||
|
accept=".json,application/json" |
||||
|
allowedExtensions="json"> |
||||
|
</tb-file-input> |
||||
|
</div> |
||||
|
</fieldset> |
||||
|
</div> |
||||
|
<div mat-dialog-actions fxLayout="row"> |
||||
|
<span fxFlex></span> |
||||
|
<button mat-button mat-raised-button color="primary" |
||||
|
type="submit" |
||||
|
[disabled]="(isLoading$ | async) || importFormGroup.invalid || !importFormGroup.dirty"> |
||||
|
{{ 'action.import' | translate }} |
||||
|
</button> |
||||
|
<button mat-button color="primary" |
||||
|
style="margin-right: 20px;" |
||||
|
type="button" |
||||
|
[disabled]="(isLoading$ | async)" |
||||
|
(click)="cancel()" cdkFocusInitial> |
||||
|
{{ 'action.cancel' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</form> |
||||
@ -0,0 +1,107 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 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, SkipSelf } from '@angular/core'; |
||||
|
import { ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { |
||||
|
AbstractControl, |
||||
|
FormArray, |
||||
|
FormBuilder, |
||||
|
FormControl, |
||||
|
FormGroup, |
||||
|
FormGroupDirective, |
||||
|
NgForm, Validators, ValidatorFn |
||||
|
} from '@angular/forms'; |
||||
|
import { Router } from '@angular/router'; |
||||
|
import { DialogComponent } from '@app/shared/components/dialog.component'; |
||||
|
import { AttributeData } from '@shared/models/telemetry/telemetry.models'; |
||||
|
import { EntityAlias, EntityAliases, EntityAliasFilter } from '@shared/models/alias.models'; |
||||
|
import { DatasourceType, Widget, widgetType } from '@shared/models/widget.models'; |
||||
|
import { AliasEntityType, EntityType } from '@shared/models/entity-type.models'; |
||||
|
import { UtilsService } from '@core/services/utils.service'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { ActionNotificationShow } from '@core/notification/notification.actions'; |
||||
|
import { DialogService } from '@core/services/dialog.service'; |
||||
|
import { EntityService } from '@core/http/entity.service'; |
||||
|
import { Observable, of } from 'rxjs'; |
||||
|
|
||||
|
export interface ImportDialogData { |
||||
|
importTitle: string; |
||||
|
importFileLabel: string; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-import-dialog', |
||||
|
templateUrl: './import-dialog.component.html', |
||||
|
providers: [{provide: ErrorStateMatcher, useExisting: ImportDialogComponent}], |
||||
|
styleUrls: [] |
||||
|
}) |
||||
|
export class ImportDialogComponent extends DialogComponent<ImportDialogComponent, any> |
||||
|
implements OnInit, ErrorStateMatcher { |
||||
|
|
||||
|
importTitle: string; |
||||
|
importFileLabel: string; |
||||
|
|
||||
|
importFormGroup: FormGroup; |
||||
|
|
||||
|
submitted = false; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
protected router: Router, |
||||
|
@Inject(MAT_DIALOG_DATA) public data: ImportDialogData, |
||||
|
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, |
||||
|
public dialogRef: MatDialogRef<ImportDialogComponent, any>, |
||||
|
private fb: FormBuilder) { |
||||
|
super(store, router, dialogRef); |
||||
|
this.importTitle = data.importTitle; |
||||
|
this.importFileLabel = data.importFileLabel; |
||||
|
|
||||
|
this.importFormGroup = this.fb.group({ |
||||
|
jsonContent: [null, [Validators.required]] |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
} |
||||
|
|
||||
|
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { |
||||
|
const originalErrorState = this.errorStateMatcher.isErrorState(control, form); |
||||
|
const customErrorState = !!(control && control.invalid && this.submitted); |
||||
|
return originalErrorState || customErrorState; |
||||
|
} |
||||
|
|
||||
|
loadDataFromJsonContent(content: string): any { |
||||
|
try { |
||||
|
const importData = JSON.parse(content); |
||||
|
return importData; |
||||
|
} catch (err) { |
||||
|
this.store.dispatch(new ActionNotificationShow({message: err.message, type: 'error'})); |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
cancel(): void { |
||||
|
this.dialogRef.close(null); |
||||
|
} |
||||
|
|
||||
|
importFromJson(): void { |
||||
|
this.submitted = true; |
||||
|
const importData = this.importFormGroup.get('jsonContent').value; |
||||
|
this.dialogRef.close(importData); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 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 { Widget } from '@app/shared/models/widget.models'; |
||||
|
import { DashboardLayoutId } from '@shared/models/dashboard.models'; |
||||
|
|
||||
|
export interface ImportWidgetResult { |
||||
|
widget: Widget; |
||||
|
layoutId: DashboardLayoutId; |
||||
|
} |
||||
@ -0,0 +1,457 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 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 { Inject, Injectable } from '@angular/core'; |
||||
|
import { DashboardService } from '@core/http/dashboard.service'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { ActionNotificationShow } from '@core/notification/notification.actions'; |
||||
|
import { Dashboard, DashboardLayoutId } from '@shared/models/dashboard.models'; |
||||
|
import { deepClone, isDefined, isObject, isUndefined } from '@core/utils'; |
||||
|
import { WINDOW } from '@core/services/window.service'; |
||||
|
import { DOCUMENT } from '@angular/common'; |
||||
|
import { |
||||
|
AliasesInfo, |
||||
|
AliasFilterType, |
||||
|
EntityAlias, |
||||
|
EntityAliases, |
||||
|
EntityAliasFilter, |
||||
|
EntityAliasInfo |
||||
|
} from '@shared/models/alias.models'; |
||||
|
import { MatDialog } from '@angular/material/dialog'; |
||||
|
import { ImportDialogComponent, ImportDialogData } from '@home/components/import-export/import-dialog.component'; |
||||
|
import { forkJoin, Observable, of } from 'rxjs'; |
||||
|
import { catchError, map, mergeMap } from 'rxjs/operators'; |
||||
|
import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; |
||||
|
import { EntityService } from '@core/http/entity.service'; |
||||
|
import { Widget, WidgetSize } from '@shared/models/widget.models'; |
||||
|
import { |
||||
|
EntityAliasesDialogComponent, |
||||
|
EntityAliasesDialogData |
||||
|
} from '@home/components/alias/entity-aliases-dialog.component'; |
||||
|
import { ItemBufferService, WidgetItem } from '@core/services/item-buffer.service'; |
||||
|
import { ImportWidgetResult } from './import-export.models'; |
||||
|
import { EntityType } from '@shared/models/entity-type.models'; |
||||
|
import { UtilsService } from '@core/services/utils.service'; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class ImportExportService { |
||||
|
|
||||
|
constructor(@Inject(WINDOW) private window: Window, |
||||
|
@Inject(DOCUMENT) private document: Document, |
||||
|
private store: Store<AppState>, |
||||
|
private translate: TranslateService, |
||||
|
private dashboardService: DashboardService, |
||||
|
private dashboardUtils: DashboardUtilsService, |
||||
|
private entityService: EntityService, |
||||
|
private utils: UtilsService, |
||||
|
private itembuffer: ItemBufferService, |
||||
|
private dialog: MatDialog) { |
||||
|
|
||||
|
} |
||||
|
|
||||
|
public exportDashboard(dashboardId: string) { |
||||
|
this.dashboardService.getDashboard(dashboardId).subscribe( |
||||
|
(dashboard) => { |
||||
|
let name = dashboard.title; |
||||
|
name = name.toLowerCase().replace(/\W/g, '_'); |
||||
|
this.exportToPc(this.prepareDashboardExport(dashboard), name + '.json'); |
||||
|
}, |
||||
|
(e) => { |
||||
|
let message = e; |
||||
|
if (!message) { |
||||
|
message = this.translate.instant('error.unknown-error'); |
||||
|
} |
||||
|
this.store.dispatch(new ActionNotificationShow( |
||||
|
{message: this.translate.instant('dashboard.export-failed-error', {error: message}), |
||||
|
type: 'error'})); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
public importDashboard(): Observable<Dashboard> { |
||||
|
return this.openImportDialog('dashboard.import', 'dashboard.dashboard-file').pipe( |
||||
|
mergeMap((dashboard: Dashboard) => { |
||||
|
if (!this.validateImportedDashboard(dashboard)) { |
||||
|
this.store.dispatch(new ActionNotificationShow( |
||||
|
{message: this.translate.instant('dashboard.invalid-dashboard-file-error'), |
||||
|
type: 'error'})); |
||||
|
throw new Error('Invalid dashboard file'); |
||||
|
} else { |
||||
|
dashboard = this.dashboardUtils.validateAndUpdateDashboard(dashboard); |
||||
|
let aliasIds = null; |
||||
|
const entityAliases = dashboard.configuration.entityAliases; |
||||
|
if (entityAliases) { |
||||
|
aliasIds = Object.keys(entityAliases); |
||||
|
} |
||||
|
if (aliasIds && aliasIds.length > 0) { |
||||
|
return this.processEntityAliases(entityAliases, aliasIds).pipe( |
||||
|
mergeMap((missingEntityAliases) => { |
||||
|
if (Object.keys(missingEntityAliases).length > 0) { |
||||
|
return this.editMissingAliases(this.dashboardUtils.getWidgetsArray(dashboard), |
||||
|
false, 'dashboard.dashboard-import-missing-aliases-title', |
||||
|
missingEntityAliases).pipe( |
||||
|
mergeMap((updatedEntityAliases) => { |
||||
|
for (const aliasId of Object.keys(updatedEntityAliases)) { |
||||
|
entityAliases[aliasId] = updatedEntityAliases[aliasId]; |
||||
|
} |
||||
|
return this.saveImportedDashboard(dashboard); |
||||
|
}) |
||||
|
); |
||||
|
} else { |
||||
|
return this.saveImportedDashboard(dashboard); |
||||
|
} |
||||
|
} |
||||
|
)); |
||||
|
} else { |
||||
|
return this.saveImportedDashboard(dashboard); |
||||
|
} |
||||
|
} |
||||
|
}), |
||||
|
catchError((err) => { |
||||
|
return of(null); |
||||
|
}) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
public exportWidget(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget) { |
||||
|
const widgetItem = this.itembuffer.prepareWidgetItem(dashboard, sourceState, sourceLayout, widget); |
||||
|
let name = widgetItem.widget.config.title; |
||||
|
name = name.toLowerCase().replace(/\W/g, '_'); |
||||
|
this.exportToPc(this.prepareExport(widgetItem), name + '.json'); |
||||
|
} |
||||
|
|
||||
|
public importWidget(dashboard: Dashboard, targetState: string, |
||||
|
targetLayoutFunction: () => Observable<DashboardLayoutId>, |
||||
|
onAliasesUpdateFunction: () => void): Observable<ImportWidgetResult> { |
||||
|
return this.openImportDialog('dashboard.import-widget', 'dashboard.widget-file').pipe( |
||||
|
mergeMap((widgetItem: WidgetItem) => { |
||||
|
if (!this.validateImportedWidget(widgetItem)) { |
||||
|
this.store.dispatch(new ActionNotificationShow( |
||||
|
{message: this.translate.instant('dashboard.invalid-widget-file-error'), |
||||
|
type: 'error'})); |
||||
|
throw new Error('Invalid widget file'); |
||||
|
} else { |
||||
|
let widget = widgetItem.widget; |
||||
|
widget = this.dashboardUtils.validateAndUpdateWidget(widget); |
||||
|
const aliasesInfo = this.prepareAliasesInfo(widgetItem.aliasesInfo); |
||||
|
const originalColumns = widgetItem.originalColumns; |
||||
|
const originalSize = widgetItem.originalSize; |
||||
|
|
||||
|
const datasourceAliases = aliasesInfo.datasourceAliases; |
||||
|
const targetDeviceAliases = aliasesInfo.targetDeviceAliases; |
||||
|
if (datasourceAliases || targetDeviceAliases) { |
||||
|
const entityAliases: EntityAliases = {}; |
||||
|
const datasourceAliasesMap: {[aliasId: string]: number} = {}; |
||||
|
const targetDeviceAliasesMap: {[aliasId: string]: number} = {}; |
||||
|
let aliasId: string; |
||||
|
let datasourceIndex: number; |
||||
|
if (datasourceAliases) { |
||||
|
for (const strIndex of Object.keys(datasourceAliases)) { |
||||
|
datasourceIndex = Number(strIndex); |
||||
|
aliasId = this.utils.guid(); |
||||
|
datasourceAliasesMap[aliasId] = datasourceIndex; |
||||
|
entityAliases[aliasId] = {id: aliasId, ...datasourceAliases[datasourceIndex]}; |
||||
|
} |
||||
|
} |
||||
|
if (targetDeviceAliases) { |
||||
|
for (const strIndex of Object.keys(targetDeviceAliases)) { |
||||
|
datasourceIndex = Number(strIndex); |
||||
|
aliasId = this.utils.guid(); |
||||
|
targetDeviceAliasesMap[aliasId] = datasourceIndex; |
||||
|
entityAliases[aliasId] = {id: aliasId, ...targetDeviceAliases[datasourceIndex]}; |
||||
|
} |
||||
|
} |
||||
|
const aliasIds = Object.keys(entityAliases); |
||||
|
if (aliasIds.length > 0) { |
||||
|
return this.processEntityAliases(entityAliases, aliasIds).pipe( |
||||
|
mergeMap((missingEntityAliases) => { |
||||
|
if (Object.keys(missingEntityAliases).length > 0) { |
||||
|
return this.editMissingAliases([widget], |
||||
|
false, 'dashboard.widget-import-missing-aliases-title', |
||||
|
missingEntityAliases).pipe( |
||||
|
mergeMap((updatedEntityAliases) => { |
||||
|
for (const id of Object.keys(updatedEntityAliases)) { |
||||
|
const entityAlias = updatedEntityAliases[id]; |
||||
|
let index; |
||||
|
if (isDefined(datasourceAliasesMap[id])) { |
||||
|
index = datasourceAliasesMap[id]; |
||||
|
datasourceAliases[index] = entityAlias; |
||||
|
} else if (isDefined(targetDeviceAliasesMap[id])) { |
||||
|
index = targetDeviceAliasesMap[id]; |
||||
|
targetDeviceAliases[index] = entityAlias; |
||||
|
} |
||||
|
} |
||||
|
return this.addImportedWidget(dashboard, targetState, targetLayoutFunction, widget, |
||||
|
aliasesInfo, onAliasesUpdateFunction, originalColumns, originalSize); |
||||
|
} |
||||
|
)); |
||||
|
} else { |
||||
|
return this.addImportedWidget(dashboard, targetState, targetLayoutFunction, widget, |
||||
|
aliasesInfo, onAliasesUpdateFunction, originalColumns, originalSize); |
||||
|
} |
||||
|
} |
||||
|
) |
||||
|
); |
||||
|
} else { |
||||
|
return this.addImportedWidget(dashboard, targetState, targetLayoutFunction, widget, |
||||
|
aliasesInfo, onAliasesUpdateFunction, originalColumns, originalSize); |
||||
|
} |
||||
|
} else { |
||||
|
return this.addImportedWidget(dashboard, targetState, targetLayoutFunction, widget, |
||||
|
aliasesInfo, onAliasesUpdateFunction, originalColumns, originalSize); |
||||
|
} |
||||
|
} |
||||
|
}), |
||||
|
catchError((err) => { |
||||
|
return of(null); |
||||
|
}) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
private validateImportedDashboard(dashboard: Dashboard): boolean { |
||||
|
if (isUndefined(dashboard.title) || isUndefined(dashboard.configuration)) { |
||||
|
return false; |
||||
|
} |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
private validateImportedWidget(widgetItem: WidgetItem): boolean { |
||||
|
if (isUndefined(widgetItem.widget) |
||||
|
|| isUndefined(widgetItem.aliasesInfo) |
||||
|
|| isUndefined(widgetItem.originalColumns)) { |
||||
|
return false; |
||||
|
} |
||||
|
const widget = widgetItem.widget; |
||||
|
if (isUndefined(widget.isSystemType) || |
||||
|
isUndefined(widget.bundleAlias) || |
||||
|
isUndefined(widget.typeAlias) || |
||||
|
isUndefined(widget.type)) { |
||||
|
return false; |
||||
|
} |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
private saveImportedDashboard(dashboard: Dashboard): Observable<Dashboard> { |
||||
|
return this.dashboardService.saveDashboard(dashboard); |
||||
|
} |
||||
|
|
||||
|
private addImportedWidget(dashboard: Dashboard, targetState: string, |
||||
|
targetLayoutFunction: () => Observable<DashboardLayoutId>, |
||||
|
widget: Widget, aliasesInfo: AliasesInfo, onAliasesUpdateFunction: () => void, |
||||
|
originalColumns: number, originalSize: WidgetSize): Observable<ImportWidgetResult> { |
||||
|
return targetLayoutFunction().pipe( |
||||
|
mergeMap((targetLayout) => { |
||||
|
return this.itembuffer.addWidgetToDashboard(dashboard, targetState, targetLayout, |
||||
|
widget, aliasesInfo, onAliasesUpdateFunction, originalColumns, originalSize, -1, -1).pipe( |
||||
|
map(() => ({widget, layoutId: targetLayout} as ImportWidgetResult)) |
||||
|
); |
||||
|
} |
||||
|
)); |
||||
|
} |
||||
|
|
||||
|
private processEntityAliases(entityAliases: EntityAliases, aliasIds: string[]): Observable<EntityAliases> { |
||||
|
const tasks: Observable<EntityAlias>[] = []; |
||||
|
for (const aliasId of aliasIds) { |
||||
|
const entityAlias = entityAliases[aliasId]; |
||||
|
tasks.push( |
||||
|
this.entityService.checkEntityAlias(entityAlias).pipe( |
||||
|
map((result) => { |
||||
|
if (!result) { |
||||
|
const missingEntityAlias = deepClone(entityAlias); |
||||
|
missingEntityAlias.filter = null; |
||||
|
return missingEntityAlias; |
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
|
) |
||||
|
) |
||||
|
); |
||||
|
} |
||||
|
return forkJoin(tasks).pipe( |
||||
|
map((missingAliasesArray) => { |
||||
|
missingAliasesArray = missingAliasesArray.filter(alias => alias !== null); |
||||
|
const missingEntityAliases: EntityAliases = {}; |
||||
|
for (const missingAlias of missingAliasesArray) { |
||||
|
missingEntityAliases[missingAlias.id] = missingAlias; |
||||
|
} |
||||
|
return missingEntityAliases; |
||||
|
} |
||||
|
) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
private editMissingAliases(widgets: Array<Widget>, isSingleWidget: boolean, |
||||
|
customTitle: string, missingEntityAliases: EntityAliases): Observable<EntityAliases> { |
||||
|
return this.dialog.open<EntityAliasesDialogComponent, EntityAliasesDialogData, |
||||
|
EntityAliases>(EntityAliasesDialogComponent, { |
||||
|
disableClose: true, |
||||
|
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
||||
|
data: { |
||||
|
entityAliases: missingEntityAliases, |
||||
|
widgets, |
||||
|
customTitle, |
||||
|
isSingleWidget, |
||||
|
disableAdd: true |
||||
|
} |
||||
|
}).afterClosed().pipe( |
||||
|
map((updatedEntityAliases) => { |
||||
|
if (updatedEntityAliases) { |
||||
|
return updatedEntityAliases; |
||||
|
} else { |
||||
|
throw new Error('Unable to resolve missing entity aliases!'); |
||||
|
} |
||||
|
} |
||||
|
)); |
||||
|
} |
||||
|
|
||||
|
private prepareAliasesInfo(aliasesInfo: AliasesInfo): AliasesInfo { |
||||
|
const datasourceAliases = aliasesInfo.datasourceAliases; |
||||
|
const targetDeviceAliases = aliasesInfo.targetDeviceAliases; |
||||
|
if (datasourceAliases || targetDeviceAliases) { |
||||
|
if (datasourceAliases) { |
||||
|
for (const strIndex of Object.keys(datasourceAliases)) { |
||||
|
const datasourceIndex = Number(strIndex); |
||||
|
datasourceAliases[datasourceIndex] = this.prepareEntityAlias(datasourceAliases[datasourceIndex]); |
||||
|
} |
||||
|
} |
||||
|
if (targetDeviceAliases) { |
||||
|
for (const strIndex of Object.keys(targetDeviceAliases)) { |
||||
|
const datasourceIndex = Number(strIndex); |
||||
|
targetDeviceAliases[datasourceIndex] = this.prepareEntityAlias(targetDeviceAliases[datasourceIndex]); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return aliasesInfo; |
||||
|
} |
||||
|
|
||||
|
private prepareEntityAlias(aliasInfo: EntityAliasInfo): EntityAliasInfo { |
||||
|
let alias: string; |
||||
|
let filter: EntityAliasFilter; |
||||
|
if (aliasInfo.deviceId) { |
||||
|
alias = aliasInfo.aliasName; |
||||
|
filter = { |
||||
|
type: AliasFilterType.entityList, |
||||
|
entityType: EntityType.DEVICE, |
||||
|
entityList: [aliasInfo.deviceId], |
||||
|
resolveMultiple: false |
||||
|
}; |
||||
|
} else if (aliasInfo.deviceFilter) { |
||||
|
alias = aliasInfo.aliasName; |
||||
|
filter = { |
||||
|
type: aliasInfo.deviceFilter.useFilter ? AliasFilterType.entityName : AliasFilterType.entityList, |
||||
|
entityType: EntityType.DEVICE, |
||||
|
resolveMultiple: false |
||||
|
}; |
||||
|
if (filter.type === AliasFilterType.entityList) { |
||||
|
filter.entityList = aliasInfo.deviceFilter.deviceList; |
||||
|
} else { |
||||
|
filter.entityNameFilter = aliasInfo.deviceFilter.deviceNameFilter; |
||||
|
} |
||||
|
} else if (aliasInfo.entityFilter) { |
||||
|
alias = aliasInfo.aliasName; |
||||
|
filter = { |
||||
|
type: aliasInfo.entityFilter.useFilter ? AliasFilterType.entityName : AliasFilterType.entityList, |
||||
|
entityType: aliasInfo.entityType, |
||||
|
resolveMultiple: false |
||||
|
}; |
||||
|
if (filter.type === AliasFilterType.entityList) { |
||||
|
filter.entityList = aliasInfo.entityFilter.entityList; |
||||
|
} else { |
||||
|
filter.entityNameFilter = aliasInfo.entityFilter.entityNameFilter; |
||||
|
} |
||||
|
} else { |
||||
|
alias = aliasInfo.alias; |
||||
|
filter = aliasInfo.filter; |
||||
|
} |
||||
|
return { |
||||
|
alias, |
||||
|
filter |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
private openImportDialog(importTitle: string, importFileLabel: string): Observable<any> { |
||||
|
return this.dialog.open<ImportDialogComponent, ImportDialogData, |
||||
|
any>(ImportDialogComponent, { |
||||
|
disableClose: true, |
||||
|
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
||||
|
data: { |
||||
|
importTitle, |
||||
|
importFileLabel |
||||
|
} |
||||
|
}).afterClosed().pipe( |
||||
|
map((importedData) => { |
||||
|
if (importedData) { |
||||
|
return importedData; |
||||
|
} else { |
||||
|
throw new Error('No file selected!'); |
||||
|
} |
||||
|
} |
||||
|
)); |
||||
|
} |
||||
|
|
||||
|
private exportToPc(data: any, filename: string) { |
||||
|
if (!data) { |
||||
|
console.error('No data'); |
||||
|
return; |
||||
|
} |
||||
|
if (!filename) { |
||||
|
filename = 'download.json'; |
||||
|
} |
||||
|
if (isObject(data)) { |
||||
|
data = JSON.stringify(data, null, 2); |
||||
|
} |
||||
|
const blob = new Blob([data], {type: 'text/json'}); |
||||
|
if (this.window.navigator && this.window.navigator.msSaveOrOpenBlob) { |
||||
|
this.window.navigator.msSaveOrOpenBlob(blob, filename); |
||||
|
} else { |
||||
|
const e = this.document.createEvent('MouseEvents'); |
||||
|
const a = this.document.createElement('a'); |
||||
|
a.download = filename; |
||||
|
a.href = this.window.URL.createObjectURL(blob); |
||||
|
a.dataset.downloadurl = ['text/json', a.download, a.href].join(':'); |
||||
|
// @ts-ignore
|
||||
|
e.initEvent('click', true, false, this.window, |
||||
|
0, 0, 0, 0, 0, false, false, false, false, 0, null); |
||||
|
a.dispatchEvent(e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private prepareDashboardExport(dashboard: Dashboard): Dashboard { |
||||
|
dashboard = this.prepareExport(dashboard); |
||||
|
delete dashboard.assignedCustomers; |
||||
|
return dashboard; |
||||
|
} |
||||
|
|
||||
|
private prepareExport(data: any): any { |
||||
|
const exportedData = deepClone(data); |
||||
|
if (isDefined(exportedData.id)) { |
||||
|
delete exportedData.id; |
||||
|
} |
||||
|
if (isDefined(exportedData.createdTime)) { |
||||
|
delete exportedData.createdTime; |
||||
|
} |
||||
|
if (isDefined(exportedData.tenantId)) { |
||||
|
delete exportedData.tenantId; |
||||
|
} |
||||
|
if (isDefined(exportedData.customerId)) { |
||||
|
delete exportedData.customerId; |
||||
|
} |
||||
|
return exportedData; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,45 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2019 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-container"> |
||||
|
<label class="tb-title">{{ label }}</label> |
||||
|
<ng-container #flow="flow" |
||||
|
[flowConfig]="{singleFile: true, allowDuplicateUploads: true}"> |
||||
|
<div class="tb-file-select-container"> |
||||
|
<div class="tb-file-clear-container"> |
||||
|
<button mat-button mat-icon-button color="primary" |
||||
|
type="button" |
||||
|
(click)="clearFile()" |
||||
|
class="tb-file-clear-btn" |
||||
|
matTooltip="{{ 'action.remove' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>close</mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
<div class="drop-area tb-flow-drop" |
||||
|
flowDrop |
||||
|
[flow]="flow.flowJs"> |
||||
|
<label for="select">{{ dropLabel }}</label> |
||||
|
<input class="file-input" flowButton [flow]="flow.flowJs" [flowAttributes]="{accept: accept}" id="select"> |
||||
|
</div> |
||||
|
</div> |
||||
|
</ng-container> |
||||
|
</div> |
||||
|
<div> |
||||
|
<div *ngIf="!fileName" translate>import.no-file</div> |
||||
|
<div *ngIf="fileName">{{ fileName }}</div> |
||||
|
</div> |
||||
@ -0,0 +1,80 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2019 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"; |
||||
|
|
||||
|
$previewSize: 100px !default; |
||||
|
|
||||
|
:host { |
||||
|
|
||||
|
.tb-container { |
||||
|
margin-top: 0px; |
||||
|
label.tb-title { |
||||
|
display: block; |
||||
|
padding-bottom: 8px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.tb-file-select-container { |
||||
|
position: relative; |
||||
|
width: 100%; |
||||
|
height: $previewSize; |
||||
|
} |
||||
|
|
||||
|
.tb-file-preview { |
||||
|
width: auto; |
||||
|
max-width: $previewSize; |
||||
|
height: auto; |
||||
|
max-height: $previewSize; |
||||
|
} |
||||
|
|
||||
|
.tb-file-clear-container { |
||||
|
position: relative; |
||||
|
float: right; |
||||
|
width: 48px; |
||||
|
height: $previewSize; |
||||
|
} |
||||
|
|
||||
|
.tb-file-clear-btn { |
||||
|
position: absolute !important; |
||||
|
top: 50%; |
||||
|
transform: translate(0%, -50%) !important; |
||||
|
} |
||||
|
|
||||
|
.file-input { |
||||
|
display: none; |
||||
|
} |
||||
|
|
||||
|
.tb-flow-drop { |
||||
|
position: relative; |
||||
|
height: $previewSize; |
||||
|
overflow: hidden; |
||||
|
border: dashed 2px; |
||||
|
|
||||
|
label { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
justify-content: center; |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
font-size: 16px; |
||||
|
text-align: center; |
||||
|
|
||||
|
@media #{$mat-gt-sm} { |
||||
|
font-size: 24px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,150 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 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, forwardRef, Input, OnDestroy, ViewChild } from '@angular/core'; |
||||
|
import { PageComponent } from '@shared/components/page.component'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; |
||||
|
import { Subscription } from 'rxjs'; |
||||
|
import { coerceBooleanProperty } from '@angular/cdk/coercion'; |
||||
|
import { FlowDirective } from '@flowjs/ngx-flow'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-file-input', |
||||
|
templateUrl: './file-input.component.html', |
||||
|
styleUrls: ['./file-input.component.scss'], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => FileInputComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
] |
||||
|
}) |
||||
|
export class FileInputComponent extends PageComponent implements AfterViewInit, OnDestroy, ControlValueAccessor { |
||||
|
|
||||
|
@Input() |
||||
|
label: string; |
||||
|
|
||||
|
@Input() |
||||
|
accept = '*/*'; |
||||
|
|
||||
|
@Input() |
||||
|
allowedExtensions: string; |
||||
|
|
||||
|
@Input() |
||||
|
dropLabel: string; |
||||
|
|
||||
|
@Input() |
||||
|
contentConvertFunction: (content: string) => any; |
||||
|
|
||||
|
private requiredValue: boolean; |
||||
|
get required(): boolean { |
||||
|
return this.requiredValue; |
||||
|
} |
||||
|
@Input() |
||||
|
set required(value: boolean) { |
||||
|
const newVal = coerceBooleanProperty(value); |
||||
|
if (this.requiredValue !== newVal) { |
||||
|
this.requiredValue = newVal; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Input() |
||||
|
disabled: boolean; |
||||
|
|
||||
|
fileName: string; |
||||
|
fileContent: any; |
||||
|
|
||||
|
@ViewChild('flow', {static: true}) |
||||
|
flow: FlowDirective; |
||||
|
|
||||
|
autoUploadSubscription: Subscription; |
||||
|
|
||||
|
private propagateChange = null; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>) { |
||||
|
super(store); |
||||
|
} |
||||
|
|
||||
|
ngAfterViewInit() { |
||||
|
this.autoUploadSubscription = this.flow.events$.subscribe(event => { |
||||
|
if (event.type === 'fileAdded') { |
||||
|
const file = event.event[0] as flowjs.FlowFile; |
||||
|
if (this.filterFile(file)) { |
||||
|
const reader = new FileReader(); |
||||
|
reader.onload = (loadEvent) => { |
||||
|
if (typeof reader.result === 'string') { |
||||
|
const fileContent = reader.result; |
||||
|
if (fileContent && fileContent.length > 0) { |
||||
|
if (this.contentConvertFunction) { |
||||
|
this.fileContent = this.contentConvertFunction(fileContent); |
||||
|
} else { |
||||
|
this.fileContent = fileContent; |
||||
|
} |
||||
|
if (this.fileContent) { |
||||
|
this.fileName = file.name; |
||||
|
} else { |
||||
|
this.fileName = null; |
||||
|
} |
||||
|
this.updateModel(); |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
reader.readAsText(file.file); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private filterFile(file: flowjs.FlowFile): boolean { |
||||
|
if (this.allowedExtensions) { |
||||
|
return this.allowedExtensions.split(',').indexOf(file.getExtension()) > -1; |
||||
|
} else { |
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy() { |
||||
|
this.autoUploadSubscription.unsubscribe(); |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
setDisabledState(isDisabled: boolean): void { |
||||
|
this.disabled = isDisabled; |
||||
|
} |
||||
|
|
||||
|
writeValue(value: any): void { |
||||
|
this.fileName = null; |
||||
|
} |
||||
|
|
||||
|
private updateModel() { |
||||
|
this.propagateChange(this.fileContent); |
||||
|
} |
||||
|
|
||||
|
clearFile() { |
||||
|
this.fileName = null; |
||||
|
this.fileContent = null; |
||||
|
this.updateModel(); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue