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