Browse Source

Add Import/Export service.

pull/2175/head
Igor Kulikov 7 years ago
parent
commit
8ee3f0bf7e
  1. 18
      ui-ngx/src/app/core/http/entity.service.ts
  2. 7
      ui-ngx/src/app/core/services/item-buffer.service.ts
  3. 13
      ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts
  4. 14
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  5. 60
      ui-ngx/src/app/modules/home/components/import-export/import-dialog.component.html
  6. 107
      ui-ngx/src/app/modules/home/components/import-export/import-dialog.component.ts
  7. 23
      ui-ngx/src/app/modules/home/components/import-export/import-export.models.ts
  8. 457
      ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts
  9. 20
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.ts
  10. 17
      ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts
  11. 45
      ui-ngx/src/app/shared/components/file-input.component.html
  12. 80
      ui-ngx/src/app/shared/components/file-input.component.scss
  13. 150
      ui-ngx/src/app/shared/components/file-input.component.ts
  14. 2
      ui-ngx/src/app/shared/components/image-input.component.html
  15. 7
      ui-ngx/src/app/shared/models/alias.models.ts
  16. 3
      ui-ngx/src/app/shared/shared.module.ts

18
ui-ngx/src/app/core/http/entity.service.ts

@ -782,6 +782,24 @@ export class EntityService {
}
}
public checkEntityAlias(entityAlias: EntityAlias): Observable<boolean> {
return this.resolveAliasFilter(entityAlias.filter, null, 1, true).pipe(
map((result) => {
if (result.stateEntity) {
return true;
} else {
const entities = result.entities;
if (entities && entities.length) {
return true;
} else {
return false;
}
}
}),
catchError(err => of(false))
);
}
private entitiesToEntitiesInfo(entities: Array<BaseData<EntityId>>): Array<EntityInfo> {
const entitiesInfo = [];
if (entities) {

7
ui-ngx/src/app/core/services/item-buffer.service.ts

@ -16,7 +16,7 @@
import { Injectable } from '@angular/core';
import { Dashboard, DashboardLayoutId } from '@app/shared/models/dashboard.models';
import { EntityAlias, EntityAliasFilter, EntityAliases, EntityAliasInfo } from '@shared/models/alias.models';
import { EntityAlias, EntityAliasFilter, EntityAliases, EntityAliasInfo, AliasesInfo } from '@shared/models/alias.models';
import { DatasourceType, Widget, WidgetPosition, WidgetSize } from '@shared/models/widget.models';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import { deepClone } from '@core/utils';
@ -29,11 +29,6 @@ const WIDGET_ITEM = 'widget_item';
const WIDGET_REFERENCE = 'widget_reference';
const RULE_NODES = 'rule_nodes';
export interface AliasesInfo {
datasourceAliases: {[datasourceIndex: number]: EntityAliasInfo};
targetDeviceAliases: {[targetDeviceAliasIndex: number]: EntityAliasInfo};
}
export interface WidgetItem {
widget: Widget;
aliasesInfo: AliasesInfo;

13
ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts

@ -37,14 +37,14 @@ 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 { deepClone } from '@core/utils';
import { deepClone, isUndefined } from '@core/utils';
import { MatDialog } from '@angular/material/dialog';
import { EntityAliasDialogComponent, EntityAliasDialogData } from './entity-alias-dialog.component';
export interface EntityAliasesDialogData {
entityAliases: EntityAliases;
widgets: Array<Widget>;
isSingleEntityAlias: boolean;
isSingleEntityAlias?: boolean;
isSingleWidget?: boolean;
allowedEntityTypes?: Array<AliasEntityType>;
disableAdd?: boolean;
@ -125,14 +125,13 @@ export class EntityAliasesDialogComponent extends DialogComponent<EntityAliasesD
const entityAliasControls: Array<AbstractControl> = [];
for (const aliasId of Object.keys(this.data.entityAliases)) {
const entityAlias = this.data.entityAliases[aliasId];
let filter = entityAlias.filter;
if (!filter) {
filter = {
if (!entityAlias.filter) {
entityAlias.filter = {
resolveMultiple: false
};
}
if (!filter.resolveMultiple) {
filter.resolveMultiple = false;
if (isUndefined(entityAlias.filter.resolveMultiple)) {
entityAlias.filter.resolveMultiple = false;
}
entityAliasControls.push(this.createEntityAliasFormControl(aliasId, entityAlias));
}

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

@ -58,6 +58,8 @@ import { CustomActionPrettyResourcesTabsComponent } from './widget/action/custom
import { CustomActionPrettyEditorComponent } from './widget/action/custom-action-pretty-editor.component';
import { CustomDialogService } from './widget/dialog/custom-dialog.service';
import { CustomDialogContainerComponent } from './widget/dialog/custom-dialog-container.component';
import { ImportExportService } from './import-export/import-export.service';
import { ImportDialogComponent } from './import-export/import-dialog.component';
@NgModule({
entryComponents: [
@ -75,7 +77,8 @@ import { CustomDialogContainerComponent } from './widget/dialog/custom-dialog-co
DataKeyConfigDialogComponent,
LegendConfigPanelComponent,
WidgetActionDialogComponent,
CustomDialogContainerComponent
CustomDialogContainerComponent,
ImportDialogComponent
],
declarations:
[
@ -117,7 +120,8 @@ import { CustomDialogContainerComponent } from './widget/dialog/custom-dialog-co
WidgetActionDialogComponent,
CustomActionPrettyResourcesTabsComponent,
CustomActionPrettyEditorComponent,
CustomDialogContainerComponent
CustomDialogContainerComponent,
ImportDialogComponent
],
imports: [
CommonModule,
@ -154,11 +158,13 @@ import { CustomDialogContainerComponent } from './widget/dialog/custom-dialog-co
WidgetActionDialogComponent,
CustomActionPrettyResourcesTabsComponent,
CustomActionPrettyEditorComponent,
CustomDialogContainerComponent
CustomDialogContainerComponent,
ImportDialogComponent
],
providers: [
WidgetComponentService,
CustomDialogService
CustomDialogService,
ImportExportService
]
})
export class HomeComponentsModule { }

60
ui-ngx/src/app/modules/home/components/import-export/import-dialog.component.html

@ -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>

107
ui-ngx/src/app/modules/home/components/import-export/import-dialog.component.ts

@ -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);
}
}

23
ui-ngx/src/app/modules/home/components/import-export/import-export.models.ts

@ -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;
}

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

@ -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;
}
}

20
ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.ts

@ -83,6 +83,7 @@ import {
ManageDashboardStatesDialogComponent,
ManageDashboardStatesDialogData
} from '@home/pages/dashboard/states/manage-dashboard-states-dialog.component';
import { ImportExportService } from '@home/components/import-export/import-export.service';
@Component({
selector: 'tb-dashboard-page',
@ -222,6 +223,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
private widgetComponentService: WidgetComponentService,
private dashboardService: DashboardService,
private itembuffer: ItemBufferService,
private importExport: ImportExportService,
private fb: FormBuilder,
private dialog: MatDialog,
private translate: TranslateService,
@ -459,8 +461,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
if ($event) {
$event.stopPropagation();
}
// TODO:
this.dialogService.todo();
this.importExport.exportDashboard(this.currentDashboardId);
}
public openEntityAliases($event: Event) {
@ -569,8 +570,16 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
if ($event) {
$event.stopPropagation();
}
// TODO:
this.dialogService.todo();
this.importExport.importWidget(this.dashboard, this.dashboardCtx.state,
this.selectTargetLayout.bind(this), this.entityAliasesUpdated.bind(this)).subscribe(
(importData) => {
if (importData) {
const widget = importData.widget;
const layoutId = importData.layoutId;
this.layouts[layoutId].layoutCtx.widgets.addWidgetId(widget.id);
}
}
);
}
public currentDashboardIdChanged(dashboardId: string) {
@ -915,8 +924,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
exportWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) {
$event.stopPropagation();
// TODO:
this.dialogService.todo();
this.importExport.exportWidget(this.dashboard, this.dashboardCtx.state, layoutCtx.id, widget);
}
widgetClicked($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) {

17
ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts

@ -63,6 +63,7 @@ import {
MakeDashboardPublicDialogData
} from '@modules/home/pages/dashboard/make-dashboard-public-dialog.component';
import { DashboardTabsComponent } from '@home/pages/dashboard/dashboard-tabs.component';
import { ImportExportService } from '@home/components/import-export/import-export.service';
@Injectable()
export class DashboardsTableConfigResolver implements Resolve<EntityTableConfig<DashboardInfo | Dashboard>> {
@ -73,6 +74,7 @@ export class DashboardsTableConfigResolver implements Resolve<EntityTableConfig<
private dashboardService: DashboardService,
private customerService: CustomerService,
private dialogService: DialogService,
private importExport: ImportExportService,
private translate: TranslateService,
private datePipe: DatePipe,
private router: Router,
@ -311,19 +313,20 @@ export class DashboardsTableConfigResolver implements Resolve<EntityTableConfig<
}
importDashboard($event: Event) {
if ($event) {
$event.stopPropagation();
}
// TODO:
this.dialogService.todo();
this.importExport.importDashboard().subscribe(
(dashboard) => {
if (dashboard) {
this.config.table.updateData();
}
}
);
}
exportDashboard($event: Event, dashboard: DashboardInfo) {
if ($event) {
$event.stopPropagation();
}
// TODO:
this.dialogService.todo();
this.importExport.exportDashboard(dashboard.id.id);
}
addDashboardsToCustomer($event: Event) {

45
ui-ngx/src/app/shared/components/file-input.component.html

@ -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>

80
ui-ngx/src/app/shared/components/file-input.component.scss

@ -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;
}
}
}
}

150
ui-ngx/src/app/shared/components/file-input.component.ts

@ -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();
}
}

2
ui-ngx/src/app/shared/components/image-input.component.html

@ -18,7 +18,7 @@
<div class="tb-container">
<label class="tb-title">{{label}}</label>
<ng-container #flow="flow"
[flowConfig]="{singleFile: true}">
[flowConfig]="{singleFile: true, allowDuplicateUploads: true}">
<div class="tb-image-select-container">
<div class="tb-image-preview-container">
<div *ngIf="!safeImageUrl" translate>dashboard.no-image</div>

7
ui-ngx/src/app/shared/models/alias.models.ts

@ -135,11 +135,16 @@ export interface EntityAliasFilter extends EntityFilters {
export interface EntityAliasInfo {
alias: string;
filter: EntityAliasFilter;
[key: string]: any;
}
export interface AliasesInfo {
datasourceAliases: {[datasourceIndex: number]: EntityAliasInfo};
targetDeviceAliases: {[targetDeviceAliasIndex: number]: EntityAliasInfo};
}
export interface EntityAlias extends EntityAliasInfo {
id: string;
[key: string]: any;
}
export interface EntityAliases {

3
ui-ngx/src/app/shared/shared.module.ts

@ -111,6 +111,7 @@ import { JsonFormComponent } from './components/json-form/json-form.component';
import { MaterialIconsDialogComponent } from '@shared/components/dialog/material-icons-dialog.component';
import { MaterialIconSelectComponent } from '@shared/components/material-icon-select.component';
import { ImageInputComponent } from './components/image-input.component';
import { FileInputComponent } from './components/file-input.component';
@NgModule({
providers: [
@ -181,6 +182,7 @@ import { ImageInputComponent } from './components/image-input.component';
MaterialIconSelectComponent,
JsonFormComponent,
ImageInputComponent,
FileInputComponent,
NospacePipe,
MillisecondsToTimeStringPipe,
EnumToArrayPipe,
@ -318,6 +320,7 @@ import { ImageInputComponent } from './components/image-input.component';
MaterialIconSelectComponent,
JsonFormComponent,
ImageInputComponent,
FileInputComponent,
NospacePipe,
MillisecondsToTimeStringPipe,
EnumToArrayPipe,

Loading…
Cancel
Save