Commit 8ee3f0bf7e01c8269389b331a54ae25992079aa8

Authored by Igor Kulikov
1 parent de60fedf

Add Import/Export service.

@@ -782,6 +782,24 @@ export class EntityService { @@ -782,6 +782,24 @@ export class EntityService {
782 } 782 }
783 } 783 }
784 784
  785 + public checkEntityAlias(entityAlias: EntityAlias): Observable<boolean> {
  786 + return this.resolveAliasFilter(entityAlias.filter, null, 1, true).pipe(
  787 + map((result) => {
  788 + if (result.stateEntity) {
  789 + return true;
  790 + } else {
  791 + const entities = result.entities;
  792 + if (entities && entities.length) {
  793 + return true;
  794 + } else {
  795 + return false;
  796 + }
  797 + }
  798 + }),
  799 + catchError(err => of(false))
  800 + );
  801 + }
  802 +
785 private entitiesToEntitiesInfo(entities: Array<BaseData<EntityId>>): Array<EntityInfo> { 803 private entitiesToEntitiesInfo(entities: Array<BaseData<EntityId>>): Array<EntityInfo> {
786 const entitiesInfo = []; 804 const entitiesInfo = [];
787 if (entities) { 805 if (entities) {
@@ -16,7 +16,7 @@ @@ -16,7 +16,7 @@
16 16
17 import { Injectable } from '@angular/core'; 17 import { Injectable } from '@angular/core';
18 import { Dashboard, DashboardLayoutId } from '@app/shared/models/dashboard.models'; 18 import { Dashboard, DashboardLayoutId } from '@app/shared/models/dashboard.models';
19 -import { EntityAlias, EntityAliasFilter, EntityAliases, EntityAliasInfo } from '@shared/models/alias.models'; 19 +import { EntityAlias, EntityAliasFilter, EntityAliases, EntityAliasInfo, AliasesInfo } from '@shared/models/alias.models';
20 import { DatasourceType, Widget, WidgetPosition, WidgetSize } from '@shared/models/widget.models'; 20 import { DatasourceType, Widget, WidgetPosition, WidgetSize } from '@shared/models/widget.models';
21 import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; 21 import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
22 import { deepClone } from '@core/utils'; 22 import { deepClone } from '@core/utils';
@@ -29,11 +29,6 @@ const WIDGET_ITEM = 'widget_item'; @@ -29,11 +29,6 @@ const WIDGET_ITEM = 'widget_item';
29 const WIDGET_REFERENCE = 'widget_reference'; 29 const WIDGET_REFERENCE = 'widget_reference';
30 const RULE_NODES = 'rule_nodes'; 30 const RULE_NODES = 'rule_nodes';
31 31
32 -export interface AliasesInfo {  
33 - datasourceAliases: {[datasourceIndex: number]: EntityAliasInfo};  
34 - targetDeviceAliases: {[targetDeviceAliasIndex: number]: EntityAliasInfo};  
35 -}  
36 -  
37 export interface WidgetItem { 32 export interface WidgetItem {
38 widget: Widget; 33 widget: Widget;
39 aliasesInfo: AliasesInfo; 34 aliasesInfo: AliasesInfo;
@@ -37,14 +37,14 @@ import { UtilsService } from '@core/services/utils.service'; @@ -37,14 +37,14 @@ import { UtilsService } from '@core/services/utils.service';
37 import { TranslateService } from '@ngx-translate/core'; 37 import { TranslateService } from '@ngx-translate/core';
38 import { ActionNotificationShow } from '@core/notification/notification.actions'; 38 import { ActionNotificationShow } from '@core/notification/notification.actions';
39 import { DialogService } from '@core/services/dialog.service'; 39 import { DialogService } from '@core/services/dialog.service';
40 -import { deepClone } from '@core/utils'; 40 +import { deepClone, isUndefined } from '@core/utils';
41 import { MatDialog } from '@angular/material/dialog'; 41 import { MatDialog } from '@angular/material/dialog';
42 import { EntityAliasDialogComponent, EntityAliasDialogData } from './entity-alias-dialog.component'; 42 import { EntityAliasDialogComponent, EntityAliasDialogData } from './entity-alias-dialog.component';
43 43
44 export interface EntityAliasesDialogData { 44 export interface EntityAliasesDialogData {
45 entityAliases: EntityAliases; 45 entityAliases: EntityAliases;
46 widgets: Array<Widget>; 46 widgets: Array<Widget>;
47 - isSingleEntityAlias: boolean; 47 + isSingleEntityAlias?: boolean;
48 isSingleWidget?: boolean; 48 isSingleWidget?: boolean;
49 allowedEntityTypes?: Array<AliasEntityType>; 49 allowedEntityTypes?: Array<AliasEntityType>;
50 disableAdd?: boolean; 50 disableAdd?: boolean;
@@ -125,14 +125,13 @@ export class EntityAliasesDialogComponent extends DialogComponent<EntityAliasesD @@ -125,14 +125,13 @@ export class EntityAliasesDialogComponent extends DialogComponent<EntityAliasesD
125 const entityAliasControls: Array<AbstractControl> = []; 125 const entityAliasControls: Array<AbstractControl> = [];
126 for (const aliasId of Object.keys(this.data.entityAliases)) { 126 for (const aliasId of Object.keys(this.data.entityAliases)) {
127 const entityAlias = this.data.entityAliases[aliasId]; 127 const entityAlias = this.data.entityAliases[aliasId];
128 - let filter = entityAlias.filter;  
129 - if (!filter) {  
130 - filter = { 128 + if (!entityAlias.filter) {
  129 + entityAlias.filter = {
131 resolveMultiple: false 130 resolveMultiple: false
132 }; 131 };
133 } 132 }
134 - if (!filter.resolveMultiple) {  
135 - filter.resolveMultiple = false; 133 + if (isUndefined(entityAlias.filter.resolveMultiple)) {
  134 + entityAlias.filter.resolveMultiple = false;
136 } 135 }
137 entityAliasControls.push(this.createEntityAliasFormControl(aliasId, entityAlias)); 136 entityAliasControls.push(this.createEntityAliasFormControl(aliasId, entityAlias));
138 } 137 }
@@ -58,6 +58,8 @@ import { CustomActionPrettyResourcesTabsComponent } from './widget/action/custom @@ -58,6 +58,8 @@ import { CustomActionPrettyResourcesTabsComponent } from './widget/action/custom
58 import { CustomActionPrettyEditorComponent } from './widget/action/custom-action-pretty-editor.component'; 58 import { CustomActionPrettyEditorComponent } from './widget/action/custom-action-pretty-editor.component';
59 import { CustomDialogService } from './widget/dialog/custom-dialog.service'; 59 import { CustomDialogService } from './widget/dialog/custom-dialog.service';
60 import { CustomDialogContainerComponent } from './widget/dialog/custom-dialog-container.component'; 60 import { CustomDialogContainerComponent } from './widget/dialog/custom-dialog-container.component';
  61 +import { ImportExportService } from './import-export/import-export.service';
  62 +import { ImportDialogComponent } from './import-export/import-dialog.component';
61 63
62 @NgModule({ 64 @NgModule({
63 entryComponents: [ 65 entryComponents: [
@@ -75,7 +77,8 @@ import { CustomDialogContainerComponent } from './widget/dialog/custom-dialog-co @@ -75,7 +77,8 @@ import { CustomDialogContainerComponent } from './widget/dialog/custom-dialog-co
75 DataKeyConfigDialogComponent, 77 DataKeyConfigDialogComponent,
76 LegendConfigPanelComponent, 78 LegendConfigPanelComponent,
77 WidgetActionDialogComponent, 79 WidgetActionDialogComponent,
78 - CustomDialogContainerComponent 80 + CustomDialogContainerComponent,
  81 + ImportDialogComponent
79 ], 82 ],
80 declarations: 83 declarations:
81 [ 84 [
@@ -117,7 +120,8 @@ import { CustomDialogContainerComponent } from './widget/dialog/custom-dialog-co @@ -117,7 +120,8 @@ import { CustomDialogContainerComponent } from './widget/dialog/custom-dialog-co
117 WidgetActionDialogComponent, 120 WidgetActionDialogComponent,
118 CustomActionPrettyResourcesTabsComponent, 121 CustomActionPrettyResourcesTabsComponent,
119 CustomActionPrettyEditorComponent, 122 CustomActionPrettyEditorComponent,
120 - CustomDialogContainerComponent 123 + CustomDialogContainerComponent,
  124 + ImportDialogComponent
121 ], 125 ],
122 imports: [ 126 imports: [
123 CommonModule, 127 CommonModule,
@@ -154,11 +158,13 @@ import { CustomDialogContainerComponent } from './widget/dialog/custom-dialog-co @@ -154,11 +158,13 @@ import { CustomDialogContainerComponent } from './widget/dialog/custom-dialog-co
154 WidgetActionDialogComponent, 158 WidgetActionDialogComponent,
155 CustomActionPrettyResourcesTabsComponent, 159 CustomActionPrettyResourcesTabsComponent,
156 CustomActionPrettyEditorComponent, 160 CustomActionPrettyEditorComponent,
157 - CustomDialogContainerComponent 161 + CustomDialogContainerComponent,
  162 + ImportDialogComponent
158 ], 163 ],
159 providers: [ 164 providers: [
160 WidgetComponentService, 165 WidgetComponentService,
161 - CustomDialogService 166 + CustomDialogService,
  167 + ImportExportService
162 ] 168 ]
163 }) 169 })
164 export class HomeComponentsModule { } 170 export class HomeComponentsModule { }
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<form #importForm="ngForm" [formGroup]="importFormGroup" (ngSubmit)="importFromJson()">
  19 + <mat-toolbar fxLayout="row" color="primary">
  20 + <h2 translate>{{ importTitle }}</h2>
  21 + <span fxFlex></span>
  22 + <button mat-button mat-icon-button
  23 + (click)="cancel()"
  24 + type="button">
  25 + <mat-icon class="material-icons">close</mat-icon>
  26 + </button>
  27 + </mat-toolbar>
  28 + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
  29 + </mat-progress-bar>
  30 + <div mat-dialog-content>
  31 + <fieldset [disabled]="isLoading$ | async">
  32 + <div fxFlex fxLayout="column">
  33 + <tb-file-input
  34 + [contentConvertFunction]="loadDataFromJsonContent"
  35 + formControlName="jsonContent"
  36 + required
  37 + label="{{importFileLabel | translate}}"
  38 + dropLabel="{{ 'import.drop-file' | translate }}"
  39 + accept=".json,application/json"
  40 + allowedExtensions="json">
  41 + </tb-file-input>
  42 + </div>
  43 + </fieldset>
  44 + </div>
  45 + <div mat-dialog-actions fxLayout="row">
  46 + <span fxFlex></span>
  47 + <button mat-button mat-raised-button color="primary"
  48 + type="submit"
  49 + [disabled]="(isLoading$ | async) || importFormGroup.invalid || !importFormGroup.dirty">
  50 + {{ 'action.import' | translate }}
  51 + </button>
  52 + <button mat-button color="primary"
  53 + style="margin-right: 20px;"
  54 + type="button"
  55 + [disabled]="(isLoading$ | async)"
  56 + (click)="cancel()" cdkFocusInitial>
  57 + {{ 'action.cancel' | translate }}
  58 + </button>
  59 + </div>
  60 +</form>
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, Inject, OnInit, SkipSelf } from '@angular/core';
  18 +import { ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
  19 +import { Store } from '@ngrx/store';
  20 +import { AppState } from '@core/core.state';
  21 +import {
  22 + AbstractControl,
  23 + FormArray,
  24 + FormBuilder,
  25 + FormControl,
  26 + FormGroup,
  27 + FormGroupDirective,
  28 + NgForm, Validators, ValidatorFn
  29 +} from '@angular/forms';
  30 +import { Router } from '@angular/router';
  31 +import { DialogComponent } from '@app/shared/components/dialog.component';
  32 +import { AttributeData } from '@shared/models/telemetry/telemetry.models';
  33 +import { EntityAlias, EntityAliases, EntityAliasFilter } from '@shared/models/alias.models';
  34 +import { DatasourceType, Widget, widgetType } from '@shared/models/widget.models';
  35 +import { AliasEntityType, EntityType } from '@shared/models/entity-type.models';
  36 +import { UtilsService } from '@core/services/utils.service';
  37 +import { TranslateService } from '@ngx-translate/core';
  38 +import { ActionNotificationShow } from '@core/notification/notification.actions';
  39 +import { DialogService } from '@core/services/dialog.service';
  40 +import { EntityService } from '@core/http/entity.service';
  41 +import { Observable, of } from 'rxjs';
  42 +
  43 +export interface ImportDialogData {
  44 + importTitle: string;
  45 + importFileLabel: string;
  46 +}
  47 +
  48 +@Component({
  49 + selector: 'tb-import-dialog',
  50 + templateUrl: './import-dialog.component.html',
  51 + providers: [{provide: ErrorStateMatcher, useExisting: ImportDialogComponent}],
  52 + styleUrls: []
  53 +})
  54 +export class ImportDialogComponent extends DialogComponent<ImportDialogComponent, any>
  55 + implements OnInit, ErrorStateMatcher {
  56 +
  57 + importTitle: string;
  58 + importFileLabel: string;
  59 +
  60 + importFormGroup: FormGroup;
  61 +
  62 + submitted = false;
  63 +
  64 + constructor(protected store: Store<AppState>,
  65 + protected router: Router,
  66 + @Inject(MAT_DIALOG_DATA) public data: ImportDialogData,
  67 + @SkipSelf() private errorStateMatcher: ErrorStateMatcher,
  68 + public dialogRef: MatDialogRef<ImportDialogComponent, any>,
  69 + private fb: FormBuilder) {
  70 + super(store, router, dialogRef);
  71 + this.importTitle = data.importTitle;
  72 + this.importFileLabel = data.importFileLabel;
  73 +
  74 + this.importFormGroup = this.fb.group({
  75 + jsonContent: [null, [Validators.required]]
  76 + });
  77 + }
  78 +
  79 + ngOnInit(): void {
  80 + }
  81 +
  82 + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
  83 + const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
  84 + const customErrorState = !!(control && control.invalid && this.submitted);
  85 + return originalErrorState || customErrorState;
  86 + }
  87 +
  88 + loadDataFromJsonContent(content: string): any {
  89 + try {
  90 + const importData = JSON.parse(content);
  91 + return importData;
  92 + } catch (err) {
  93 + this.store.dispatch(new ActionNotificationShow({message: err.message, type: 'error'}));
  94 + return null;
  95 + }
  96 + }
  97 +
  98 + cancel(): void {
  99 + this.dialogRef.close(null);
  100 + }
  101 +
  102 + importFromJson(): void {
  103 + this.submitted = true;
  104 + const importData = this.importFormGroup.get('jsonContent').value;
  105 + this.dialogRef.close(importData);
  106 + }
  107 +}
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Widget } from '@app/shared/models/widget.models';
  18 +import { DashboardLayoutId } from '@shared/models/dashboard.models';
  19 +
  20 +export interface ImportWidgetResult {
  21 + widget: Widget;
  22 + layoutId: DashboardLayoutId;
  23 +}
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Inject, Injectable } from '@angular/core';
  18 +import { DashboardService } from '@core/http/dashboard.service';
  19 +import { TranslateService } from '@ngx-translate/core';
  20 +import { Store } from '@ngrx/store';
  21 +import { AppState } from '@core/core.state';
  22 +import { ActionNotificationShow } from '@core/notification/notification.actions';
  23 +import { Dashboard, DashboardLayoutId } from '@shared/models/dashboard.models';
  24 +import { deepClone, isDefined, isObject, isUndefined } from '@core/utils';
  25 +import { WINDOW } from '@core/services/window.service';
  26 +import { DOCUMENT } from '@angular/common';
  27 +import {
  28 + AliasesInfo,
  29 + AliasFilterType,
  30 + EntityAlias,
  31 + EntityAliases,
  32 + EntityAliasFilter,
  33 + EntityAliasInfo
  34 +} from '@shared/models/alias.models';
  35 +import { MatDialog } from '@angular/material/dialog';
  36 +import { ImportDialogComponent, ImportDialogData } from '@home/components/import-export/import-dialog.component';
  37 +import { forkJoin, Observable, of } from 'rxjs';
  38 +import { catchError, map, mergeMap } from 'rxjs/operators';
  39 +import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
  40 +import { EntityService } from '@core/http/entity.service';
  41 +import { Widget, WidgetSize } from '@shared/models/widget.models';
  42 +import {
  43 + EntityAliasesDialogComponent,
  44 + EntityAliasesDialogData
  45 +} from '@home/components/alias/entity-aliases-dialog.component';
  46 +import { ItemBufferService, WidgetItem } from '@core/services/item-buffer.service';
  47 +import { ImportWidgetResult } from './import-export.models';
  48 +import { EntityType } from '@shared/models/entity-type.models';
  49 +import { UtilsService } from '@core/services/utils.service';
  50 +
  51 +@Injectable()
  52 +export class ImportExportService {
  53 +
  54 + constructor(@Inject(WINDOW) private window: Window,
  55 + @Inject(DOCUMENT) private document: Document,
  56 + private store: Store<AppState>,
  57 + private translate: TranslateService,
  58 + private dashboardService: DashboardService,
  59 + private dashboardUtils: DashboardUtilsService,
  60 + private entityService: EntityService,
  61 + private utils: UtilsService,
  62 + private itembuffer: ItemBufferService,
  63 + private dialog: MatDialog) {
  64 +
  65 + }
  66 +
  67 + public exportDashboard(dashboardId: string) {
  68 + this.dashboardService.getDashboard(dashboardId).subscribe(
  69 + (dashboard) => {
  70 + let name = dashboard.title;
  71 + name = name.toLowerCase().replace(/\W/g, '_');
  72 + this.exportToPc(this.prepareDashboardExport(dashboard), name + '.json');
  73 + },
  74 + (e) => {
  75 + let message = e;
  76 + if (!message) {
  77 + message = this.translate.instant('error.unknown-error');
  78 + }
  79 + this.store.dispatch(new ActionNotificationShow(
  80 + {message: this.translate.instant('dashboard.export-failed-error', {error: message}),
  81 + type: 'error'}));
  82 + }
  83 + );
  84 + }
  85 +
  86 + public importDashboard(): Observable<Dashboard> {
  87 + return this.openImportDialog('dashboard.import', 'dashboard.dashboard-file').pipe(
  88 + mergeMap((dashboard: Dashboard) => {
  89 + if (!this.validateImportedDashboard(dashboard)) {
  90 + this.store.dispatch(new ActionNotificationShow(
  91 + {message: this.translate.instant('dashboard.invalid-dashboard-file-error'),
  92 + type: 'error'}));
  93 + throw new Error('Invalid dashboard file');
  94 + } else {
  95 + dashboard = this.dashboardUtils.validateAndUpdateDashboard(dashboard);
  96 + let aliasIds = null;
  97 + const entityAliases = dashboard.configuration.entityAliases;
  98 + if (entityAliases) {
  99 + aliasIds = Object.keys(entityAliases);
  100 + }
  101 + if (aliasIds && aliasIds.length > 0) {
  102 + return this.processEntityAliases(entityAliases, aliasIds).pipe(
  103 + mergeMap((missingEntityAliases) => {
  104 + if (Object.keys(missingEntityAliases).length > 0) {
  105 + return this.editMissingAliases(this.dashboardUtils.getWidgetsArray(dashboard),
  106 + false, 'dashboard.dashboard-import-missing-aliases-title',
  107 + missingEntityAliases).pipe(
  108 + mergeMap((updatedEntityAliases) => {
  109 + for (const aliasId of Object.keys(updatedEntityAliases)) {
  110 + entityAliases[aliasId] = updatedEntityAliases[aliasId];
  111 + }
  112 + return this.saveImportedDashboard(dashboard);
  113 + })
  114 + );
  115 + } else {
  116 + return this.saveImportedDashboard(dashboard);
  117 + }
  118 + }
  119 + ));
  120 + } else {
  121 + return this.saveImportedDashboard(dashboard);
  122 + }
  123 + }
  124 + }),
  125 + catchError((err) => {
  126 + return of(null);
  127 + })
  128 + );
  129 + }
  130 +
  131 + public exportWidget(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget) {
  132 + const widgetItem = this.itembuffer.prepareWidgetItem(dashboard, sourceState, sourceLayout, widget);
  133 + let name = widgetItem.widget.config.title;
  134 + name = name.toLowerCase().replace(/\W/g, '_');
  135 + this.exportToPc(this.prepareExport(widgetItem), name + '.json');
  136 + }
  137 +
  138 + public importWidget(dashboard: Dashboard, targetState: string,
  139 + targetLayoutFunction: () => Observable<DashboardLayoutId>,
  140 + onAliasesUpdateFunction: () => void): Observable<ImportWidgetResult> {
  141 + return this.openImportDialog('dashboard.import-widget', 'dashboard.widget-file').pipe(
  142 + mergeMap((widgetItem: WidgetItem) => {
  143 + if (!this.validateImportedWidget(widgetItem)) {
  144 + this.store.dispatch(new ActionNotificationShow(
  145 + {message: this.translate.instant('dashboard.invalid-widget-file-error'),
  146 + type: 'error'}));
  147 + throw new Error('Invalid widget file');
  148 + } else {
  149 + let widget = widgetItem.widget;
  150 + widget = this.dashboardUtils.validateAndUpdateWidget(widget);
  151 + const aliasesInfo = this.prepareAliasesInfo(widgetItem.aliasesInfo);
  152 + const originalColumns = widgetItem.originalColumns;
  153 + const originalSize = widgetItem.originalSize;
  154 +
  155 + const datasourceAliases = aliasesInfo.datasourceAliases;
  156 + const targetDeviceAliases = aliasesInfo.targetDeviceAliases;
  157 + if (datasourceAliases || targetDeviceAliases) {
  158 + const entityAliases: EntityAliases = {};
  159 + const datasourceAliasesMap: {[aliasId: string]: number} = {};
  160 + const targetDeviceAliasesMap: {[aliasId: string]: number} = {};
  161 + let aliasId: string;
  162 + let datasourceIndex: number;
  163 + if (datasourceAliases) {
  164 + for (const strIndex of Object.keys(datasourceAliases)) {
  165 + datasourceIndex = Number(strIndex);
  166 + aliasId = this.utils.guid();
  167 + datasourceAliasesMap[aliasId] = datasourceIndex;
  168 + entityAliases[aliasId] = {id: aliasId, ...datasourceAliases[datasourceIndex]};
  169 + }
  170 + }
  171 + if (targetDeviceAliases) {
  172 + for (const strIndex of Object.keys(targetDeviceAliases)) {
  173 + datasourceIndex = Number(strIndex);
  174 + aliasId = this.utils.guid();
  175 + targetDeviceAliasesMap[aliasId] = datasourceIndex;
  176 + entityAliases[aliasId] = {id: aliasId, ...targetDeviceAliases[datasourceIndex]};
  177 + }
  178 + }
  179 + const aliasIds = Object.keys(entityAliases);
  180 + if (aliasIds.length > 0) {
  181 + return this.processEntityAliases(entityAliases, aliasIds).pipe(
  182 + mergeMap((missingEntityAliases) => {
  183 + if (Object.keys(missingEntityAliases).length > 0) {
  184 + return this.editMissingAliases([widget],
  185 + false, 'dashboard.widget-import-missing-aliases-title',
  186 + missingEntityAliases).pipe(
  187 + mergeMap((updatedEntityAliases) => {
  188 + for (const id of Object.keys(updatedEntityAliases)) {
  189 + const entityAlias = updatedEntityAliases[id];
  190 + let index;
  191 + if (isDefined(datasourceAliasesMap[id])) {
  192 + index = datasourceAliasesMap[id];
  193 + datasourceAliases[index] = entityAlias;
  194 + } else if (isDefined(targetDeviceAliasesMap[id])) {
  195 + index = targetDeviceAliasesMap[id];
  196 + targetDeviceAliases[index] = entityAlias;
  197 + }
  198 + }
  199 + return this.addImportedWidget(dashboard, targetState, targetLayoutFunction, widget,
  200 + aliasesInfo, onAliasesUpdateFunction, originalColumns, originalSize);
  201 + }
  202 + ));
  203 + } else {
  204 + return this.addImportedWidget(dashboard, targetState, targetLayoutFunction, widget,
  205 + aliasesInfo, onAliasesUpdateFunction, originalColumns, originalSize);
  206 + }
  207 + }
  208 + )
  209 + );
  210 + } else {
  211 + return this.addImportedWidget(dashboard, targetState, targetLayoutFunction, widget,
  212 + aliasesInfo, onAliasesUpdateFunction, originalColumns, originalSize);
  213 + }
  214 + } else {
  215 + return this.addImportedWidget(dashboard, targetState, targetLayoutFunction, widget,
  216 + aliasesInfo, onAliasesUpdateFunction, originalColumns, originalSize);
  217 + }
  218 + }
  219 + }),
  220 + catchError((err) => {
  221 + return of(null);
  222 + })
  223 + );
  224 + }
  225 +
  226 + private validateImportedDashboard(dashboard: Dashboard): boolean {
  227 + if (isUndefined(dashboard.title) || isUndefined(dashboard.configuration)) {
  228 + return false;
  229 + }
  230 + return true;
  231 + }
  232 +
  233 + private validateImportedWidget(widgetItem: WidgetItem): boolean {
  234 + if (isUndefined(widgetItem.widget)
  235 + || isUndefined(widgetItem.aliasesInfo)
  236 + || isUndefined(widgetItem.originalColumns)) {
  237 + return false;
  238 + }
  239 + const widget = widgetItem.widget;
  240 + if (isUndefined(widget.isSystemType) ||
  241 + isUndefined(widget.bundleAlias) ||
  242 + isUndefined(widget.typeAlias) ||
  243 + isUndefined(widget.type)) {
  244 + return false;
  245 + }
  246 + return true;
  247 + }
  248 +
  249 + private saveImportedDashboard(dashboard: Dashboard): Observable<Dashboard> {
  250 + return this.dashboardService.saveDashboard(dashboard);
  251 + }
  252 +
  253 + private addImportedWidget(dashboard: Dashboard, targetState: string,
  254 + targetLayoutFunction: () => Observable<DashboardLayoutId>,
  255 + widget: Widget, aliasesInfo: AliasesInfo, onAliasesUpdateFunction: () => void,
  256 + originalColumns: number, originalSize: WidgetSize): Observable<ImportWidgetResult> {
  257 + return targetLayoutFunction().pipe(
  258 + mergeMap((targetLayout) => {
  259 + return this.itembuffer.addWidgetToDashboard(dashboard, targetState, targetLayout,
  260 + widget, aliasesInfo, onAliasesUpdateFunction, originalColumns, originalSize, -1, -1).pipe(
  261 + map(() => ({widget, layoutId: targetLayout} as ImportWidgetResult))
  262 + );
  263 + }
  264 + ));
  265 + }
  266 +
  267 + private processEntityAliases(entityAliases: EntityAliases, aliasIds: string[]): Observable<EntityAliases> {
  268 + const tasks: Observable<EntityAlias>[] = [];
  269 + for (const aliasId of aliasIds) {
  270 + const entityAlias = entityAliases[aliasId];
  271 + tasks.push(
  272 + this.entityService.checkEntityAlias(entityAlias).pipe(
  273 + map((result) => {
  274 + if (!result) {
  275 + const missingEntityAlias = deepClone(entityAlias);
  276 + missingEntityAlias.filter = null;
  277 + return missingEntityAlias;
  278 + }
  279 + return null;
  280 + }
  281 + )
  282 + )
  283 + );
  284 + }
  285 + return forkJoin(tasks).pipe(
  286 + map((missingAliasesArray) => {
  287 + missingAliasesArray = missingAliasesArray.filter(alias => alias !== null);
  288 + const missingEntityAliases: EntityAliases = {};
  289 + for (const missingAlias of missingAliasesArray) {
  290 + missingEntityAliases[missingAlias.id] = missingAlias;
  291 + }
  292 + return missingEntityAliases;
  293 + }
  294 + )
  295 + );
  296 + }
  297 +
  298 + private editMissingAliases(widgets: Array<Widget>, isSingleWidget: boolean,
  299 + customTitle: string, missingEntityAliases: EntityAliases): Observable<EntityAliases> {
  300 + return this.dialog.open<EntityAliasesDialogComponent, EntityAliasesDialogData,
  301 + EntityAliases>(EntityAliasesDialogComponent, {
  302 + disableClose: true,
  303 + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
  304 + data: {
  305 + entityAliases: missingEntityAliases,
  306 + widgets,
  307 + customTitle,
  308 + isSingleWidget,
  309 + disableAdd: true
  310 + }
  311 + }).afterClosed().pipe(
  312 + map((updatedEntityAliases) => {
  313 + if (updatedEntityAliases) {
  314 + return updatedEntityAliases;
  315 + } else {
  316 + throw new Error('Unable to resolve missing entity aliases!');
  317 + }
  318 + }
  319 + ));
  320 + }
  321 +
  322 + private prepareAliasesInfo(aliasesInfo: AliasesInfo): AliasesInfo {
  323 + const datasourceAliases = aliasesInfo.datasourceAliases;
  324 + const targetDeviceAliases = aliasesInfo.targetDeviceAliases;
  325 + if (datasourceAliases || targetDeviceAliases) {
  326 + if (datasourceAliases) {
  327 + for (const strIndex of Object.keys(datasourceAliases)) {
  328 + const datasourceIndex = Number(strIndex);
  329 + datasourceAliases[datasourceIndex] = this.prepareEntityAlias(datasourceAliases[datasourceIndex]);
  330 + }
  331 + }
  332 + if (targetDeviceAliases) {
  333 + for (const strIndex of Object.keys(targetDeviceAliases)) {
  334 + const datasourceIndex = Number(strIndex);
  335 + targetDeviceAliases[datasourceIndex] = this.prepareEntityAlias(targetDeviceAliases[datasourceIndex]);
  336 + }
  337 + }
  338 + }
  339 + return aliasesInfo;
  340 + }
  341 +
  342 + private prepareEntityAlias(aliasInfo: EntityAliasInfo): EntityAliasInfo {
  343 + let alias: string;
  344 + let filter: EntityAliasFilter;
  345 + if (aliasInfo.deviceId) {
  346 + alias = aliasInfo.aliasName;
  347 + filter = {
  348 + type: AliasFilterType.entityList,
  349 + entityType: EntityType.DEVICE,
  350 + entityList: [aliasInfo.deviceId],
  351 + resolveMultiple: false
  352 + };
  353 + } else if (aliasInfo.deviceFilter) {
  354 + alias = aliasInfo.aliasName;
  355 + filter = {
  356 + type: aliasInfo.deviceFilter.useFilter ? AliasFilterType.entityName : AliasFilterType.entityList,
  357 + entityType: EntityType.DEVICE,
  358 + resolveMultiple: false
  359 + };
  360 + if (filter.type === AliasFilterType.entityList) {
  361 + filter.entityList = aliasInfo.deviceFilter.deviceList;
  362 + } else {
  363 + filter.entityNameFilter = aliasInfo.deviceFilter.deviceNameFilter;
  364 + }
  365 + } else if (aliasInfo.entityFilter) {
  366 + alias = aliasInfo.aliasName;
  367 + filter = {
  368 + type: aliasInfo.entityFilter.useFilter ? AliasFilterType.entityName : AliasFilterType.entityList,
  369 + entityType: aliasInfo.entityType,
  370 + resolveMultiple: false
  371 + };
  372 + if (filter.type === AliasFilterType.entityList) {
  373 + filter.entityList = aliasInfo.entityFilter.entityList;
  374 + } else {
  375 + filter.entityNameFilter = aliasInfo.entityFilter.entityNameFilter;
  376 + }
  377 + } else {
  378 + alias = aliasInfo.alias;
  379 + filter = aliasInfo.filter;
  380 + }
  381 + return {
  382 + alias,
  383 + filter
  384 + };
  385 + }
  386 +
  387 + private openImportDialog(importTitle: string, importFileLabel: string): Observable<any> {
  388 + return this.dialog.open<ImportDialogComponent, ImportDialogData,
  389 + any>(ImportDialogComponent, {
  390 + disableClose: true,
  391 + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
  392 + data: {
  393 + importTitle,
  394 + importFileLabel
  395 + }
  396 + }).afterClosed().pipe(
  397 + map((importedData) => {
  398 + if (importedData) {
  399 + return importedData;
  400 + } else {
  401 + throw new Error('No file selected!');
  402 + }
  403 + }
  404 + ));
  405 + }
  406 +
  407 + private exportToPc(data: any, filename: string) {
  408 + if (!data) {
  409 + console.error('No data');
  410 + return;
  411 + }
  412 + if (!filename) {
  413 + filename = 'download.json';
  414 + }
  415 + if (isObject(data)) {
  416 + data = JSON.stringify(data, null, 2);
  417 + }
  418 + const blob = new Blob([data], {type: 'text/json'});
  419 + if (this.window.navigator && this.window.navigator.msSaveOrOpenBlob) {
  420 + this.window.navigator.msSaveOrOpenBlob(blob, filename);
  421 + } else {
  422 + const e = this.document.createEvent('MouseEvents');
  423 + const a = this.document.createElement('a');
  424 + a.download = filename;
  425 + a.href = this.window.URL.createObjectURL(blob);
  426 + a.dataset.downloadurl = ['text/json', a.download, a.href].join(':');
  427 + // @ts-ignore
  428 + e.initEvent('click', true, false, this.window,
  429 + 0, 0, 0, 0, 0, false, false, false, false, 0, null);
  430 + a.dispatchEvent(e);
  431 + }
  432 + }
  433 +
  434 + private prepareDashboardExport(dashboard: Dashboard): Dashboard {
  435 + dashboard = this.prepareExport(dashboard);
  436 + delete dashboard.assignedCustomers;
  437 + return dashboard;
  438 + }
  439 +
  440 + private prepareExport(data: any): any {
  441 + const exportedData = deepClone(data);
  442 + if (isDefined(exportedData.id)) {
  443 + delete exportedData.id;
  444 + }
  445 + if (isDefined(exportedData.createdTime)) {
  446 + delete exportedData.createdTime;
  447 + }
  448 + if (isDefined(exportedData.tenantId)) {
  449 + delete exportedData.tenantId;
  450 + }
  451 + if (isDefined(exportedData.customerId)) {
  452 + delete exportedData.customerId;
  453 + }
  454 + return exportedData;
  455 + }
  456 +
  457 +}
@@ -83,6 +83,7 @@ import { @@ -83,6 +83,7 @@ import {
83 ManageDashboardStatesDialogComponent, 83 ManageDashboardStatesDialogComponent,
84 ManageDashboardStatesDialogData 84 ManageDashboardStatesDialogData
85 } from '@home/pages/dashboard/states/manage-dashboard-states-dialog.component'; 85 } from '@home/pages/dashboard/states/manage-dashboard-states-dialog.component';
  86 +import { ImportExportService } from '@home/components/import-export/import-export.service';
86 87
87 @Component({ 88 @Component({
88 selector: 'tb-dashboard-page', 89 selector: 'tb-dashboard-page',
@@ -222,6 +223,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC @@ -222,6 +223,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
222 private widgetComponentService: WidgetComponentService, 223 private widgetComponentService: WidgetComponentService,
223 private dashboardService: DashboardService, 224 private dashboardService: DashboardService,
224 private itembuffer: ItemBufferService, 225 private itembuffer: ItemBufferService,
  226 + private importExport: ImportExportService,
225 private fb: FormBuilder, 227 private fb: FormBuilder,
226 private dialog: MatDialog, 228 private dialog: MatDialog,
227 private translate: TranslateService, 229 private translate: TranslateService,
@@ -459,8 +461,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC @@ -459,8 +461,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
459 if ($event) { 461 if ($event) {
460 $event.stopPropagation(); 462 $event.stopPropagation();
461 } 463 }
462 - // TODO:  
463 - this.dialogService.todo(); 464 + this.importExport.exportDashboard(this.currentDashboardId);
464 } 465 }
465 466
466 public openEntityAliases($event: Event) { 467 public openEntityAliases($event: Event) {
@@ -569,8 +570,16 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC @@ -569,8 +570,16 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
569 if ($event) { 570 if ($event) {
570 $event.stopPropagation(); 571 $event.stopPropagation();
571 } 572 }
572 - // TODO:  
573 - this.dialogService.todo(); 573 + this.importExport.importWidget(this.dashboard, this.dashboardCtx.state,
  574 + this.selectTargetLayout.bind(this), this.entityAliasesUpdated.bind(this)).subscribe(
  575 + (importData) => {
  576 + if (importData) {
  577 + const widget = importData.widget;
  578 + const layoutId = importData.layoutId;
  579 + this.layouts[layoutId].layoutCtx.widgets.addWidgetId(widget.id);
  580 + }
  581 + }
  582 + );
574 } 583 }
575 584
576 public currentDashboardIdChanged(dashboardId: string) { 585 public currentDashboardIdChanged(dashboardId: string) {
@@ -915,8 +924,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC @@ -915,8 +924,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
915 924
916 exportWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) { 925 exportWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) {
917 $event.stopPropagation(); 926 $event.stopPropagation();
918 - // TODO:  
919 - this.dialogService.todo(); 927 + this.importExport.exportWidget(this.dashboard, this.dashboardCtx.state, layoutCtx.id, widget);
920 } 928 }
921 929
922 widgetClicked($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) { 930 widgetClicked($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) {
@@ -63,6 +63,7 @@ import { @@ -63,6 +63,7 @@ import {
63 MakeDashboardPublicDialogData 63 MakeDashboardPublicDialogData
64 } from '@modules/home/pages/dashboard/make-dashboard-public-dialog.component'; 64 } from '@modules/home/pages/dashboard/make-dashboard-public-dialog.component';
65 import { DashboardTabsComponent } from '@home/pages/dashboard/dashboard-tabs.component'; 65 import { DashboardTabsComponent } from '@home/pages/dashboard/dashboard-tabs.component';
  66 +import { ImportExportService } from '@home/components/import-export/import-export.service';
66 67
67 @Injectable() 68 @Injectable()
68 export class DashboardsTableConfigResolver implements Resolve<EntityTableConfig<DashboardInfo | Dashboard>> { 69 export class DashboardsTableConfigResolver implements Resolve<EntityTableConfig<DashboardInfo | Dashboard>> {
@@ -73,6 +74,7 @@ export class DashboardsTableConfigResolver implements Resolve<EntityTableConfig< @@ -73,6 +74,7 @@ export class DashboardsTableConfigResolver implements Resolve<EntityTableConfig<
73 private dashboardService: DashboardService, 74 private dashboardService: DashboardService,
74 private customerService: CustomerService, 75 private customerService: CustomerService,
75 private dialogService: DialogService, 76 private dialogService: DialogService,
  77 + private importExport: ImportExportService,
76 private translate: TranslateService, 78 private translate: TranslateService,
77 private datePipe: DatePipe, 79 private datePipe: DatePipe,
78 private router: Router, 80 private router: Router,
@@ -311,19 +313,20 @@ export class DashboardsTableConfigResolver implements Resolve<EntityTableConfig< @@ -311,19 +313,20 @@ export class DashboardsTableConfigResolver implements Resolve<EntityTableConfig<
311 } 313 }
312 314
313 importDashboard($event: Event) { 315 importDashboard($event: Event) {
314 - if ($event) {  
315 - $event.stopPropagation();  
316 - }  
317 - // TODO:  
318 - this.dialogService.todo(); 316 + this.importExport.importDashboard().subscribe(
  317 + (dashboard) => {
  318 + if (dashboard) {
  319 + this.config.table.updateData();
  320 + }
  321 + }
  322 + );
319 } 323 }
320 324
321 exportDashboard($event: Event, dashboard: DashboardInfo) { 325 exportDashboard($event: Event, dashboard: DashboardInfo) {
322 if ($event) { 326 if ($event) {
323 $event.stopPropagation(); 327 $event.stopPropagation();
324 } 328 }
325 - // TODO:  
326 - this.dialogService.todo(); 329 + this.importExport.exportDashboard(dashboard.id.id);
327 } 330 }
328 331
329 addDashboardsToCustomer($event: Event) { 332 addDashboardsToCustomer($event: Event) {
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div class="tb-container">
  19 + <label class="tb-title">{{ label }}</label>
  20 + <ng-container #flow="flow"
  21 + [flowConfig]="{singleFile: true, allowDuplicateUploads: true}">
  22 + <div class="tb-file-select-container">
  23 + <div class="tb-file-clear-container">
  24 + <button mat-button mat-icon-button color="primary"
  25 + type="button"
  26 + (click)="clearFile()"
  27 + class="tb-file-clear-btn"
  28 + matTooltip="{{ 'action.remove' | translate }}"
  29 + matTooltipPosition="above">
  30 + <mat-icon>close</mat-icon>
  31 + </button>
  32 + </div>
  33 + <div class="drop-area tb-flow-drop"
  34 + flowDrop
  35 + [flow]="flow.flowJs">
  36 + <label for="select">{{ dropLabel }}</label>
  37 + <input class="file-input" flowButton [flow]="flow.flowJs" [flowAttributes]="{accept: accept}" id="select">
  38 + </div>
  39 + </div>
  40 + </ng-container>
  41 +</div>
  42 +<div>
  43 + <div *ngIf="!fileName" translate>import.no-file</div>
  44 + <div *ngIf="fileName">{{ fileName }}</div>
  45 +</div>
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +@import "../../../scss/constants";
  17 +
  18 +$previewSize: 100px !default;
  19 +
  20 +:host {
  21 +
  22 + .tb-container {
  23 + margin-top: 0px;
  24 + label.tb-title {
  25 + display: block;
  26 + padding-bottom: 8px;
  27 + }
  28 + }
  29 +
  30 + .tb-file-select-container {
  31 + position: relative;
  32 + width: 100%;
  33 + height: $previewSize;
  34 + }
  35 +
  36 + .tb-file-preview {
  37 + width: auto;
  38 + max-width: $previewSize;
  39 + height: auto;
  40 + max-height: $previewSize;
  41 + }
  42 +
  43 + .tb-file-clear-container {
  44 + position: relative;
  45 + float: right;
  46 + width: 48px;
  47 + height: $previewSize;
  48 + }
  49 +
  50 + .tb-file-clear-btn {
  51 + position: absolute !important;
  52 + top: 50%;
  53 + transform: translate(0%, -50%) !important;
  54 + }
  55 +
  56 + .file-input {
  57 + display: none;
  58 + }
  59 +
  60 + .tb-flow-drop {
  61 + position: relative;
  62 + height: $previewSize;
  63 + overflow: hidden;
  64 + border: dashed 2px;
  65 +
  66 + label {
  67 + display: flex;
  68 + flex-direction: column;
  69 + justify-content: center;
  70 + width: 100%;
  71 + height: 100%;
  72 + font-size: 16px;
  73 + text-align: center;
  74 +
  75 + @media #{$mat-gt-sm} {
  76 + font-size: 24px;
  77 + }
  78 + }
  79 + }
  80 +}
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { AfterViewInit, Component, forwardRef, Input, OnDestroy, ViewChild } from '@angular/core';
  18 +import { PageComponent } from '@shared/components/page.component';
  19 +import { Store } from '@ngrx/store';
  20 +import { AppState } from '@core/core.state';
  21 +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
  22 +import { Subscription } from 'rxjs';
  23 +import { coerceBooleanProperty } from '@angular/cdk/coercion';
  24 +import { FlowDirective } from '@flowjs/ngx-flow';
  25 +
  26 +@Component({
  27 + selector: 'tb-file-input',
  28 + templateUrl: './file-input.component.html',
  29 + styleUrls: ['./file-input.component.scss'],
  30 + providers: [
  31 + {
  32 + provide: NG_VALUE_ACCESSOR,
  33 + useExisting: forwardRef(() => FileInputComponent),
  34 + multi: true
  35 + }
  36 + ]
  37 +})
  38 +export class FileInputComponent extends PageComponent implements AfterViewInit, OnDestroy, ControlValueAccessor {
  39 +
  40 + @Input()
  41 + label: string;
  42 +
  43 + @Input()
  44 + accept = '*/*';
  45 +
  46 + @Input()
  47 + allowedExtensions: string;
  48 +
  49 + @Input()
  50 + dropLabel: string;
  51 +
  52 + @Input()
  53 + contentConvertFunction: (content: string) => any;
  54 +
  55 + private requiredValue: boolean;
  56 + get required(): boolean {
  57 + return this.requiredValue;
  58 + }
  59 + @Input()
  60 + set required(value: boolean) {
  61 + const newVal = coerceBooleanProperty(value);
  62 + if (this.requiredValue !== newVal) {
  63 + this.requiredValue = newVal;
  64 + }
  65 + }
  66 +
  67 + @Input()
  68 + disabled: boolean;
  69 +
  70 + fileName: string;
  71 + fileContent: any;
  72 +
  73 + @ViewChild('flow', {static: true})
  74 + flow: FlowDirective;
  75 +
  76 + autoUploadSubscription: Subscription;
  77 +
  78 + private propagateChange = null;
  79 +
  80 + constructor(protected store: Store<AppState>) {
  81 + super(store);
  82 + }
  83 +
  84 + ngAfterViewInit() {
  85 + this.autoUploadSubscription = this.flow.events$.subscribe(event => {
  86 + if (event.type === 'fileAdded') {
  87 + const file = event.event[0] as flowjs.FlowFile;
  88 + if (this.filterFile(file)) {
  89 + const reader = new FileReader();
  90 + reader.onload = (loadEvent) => {
  91 + if (typeof reader.result === 'string') {
  92 + const fileContent = reader.result;
  93 + if (fileContent && fileContent.length > 0) {
  94 + if (this.contentConvertFunction) {
  95 + this.fileContent = this.contentConvertFunction(fileContent);
  96 + } else {
  97 + this.fileContent = fileContent;
  98 + }
  99 + if (this.fileContent) {
  100 + this.fileName = file.name;
  101 + } else {
  102 + this.fileName = null;
  103 + }
  104 + this.updateModel();
  105 + }
  106 + }
  107 + };
  108 + reader.readAsText(file.file);
  109 + }
  110 + }
  111 + });
  112 + }
  113 +
  114 + private filterFile(file: flowjs.FlowFile): boolean {
  115 + if (this.allowedExtensions) {
  116 + return this.allowedExtensions.split(',').indexOf(file.getExtension()) > -1;
  117 + } else {
  118 + return true;
  119 + }
  120 + }
  121 +
  122 + ngOnDestroy() {
  123 + this.autoUploadSubscription.unsubscribe();
  124 + }
  125 +
  126 + registerOnChange(fn: any): void {
  127 + this.propagateChange = fn;
  128 + }
  129 +
  130 + registerOnTouched(fn: any): void {
  131 + }
  132 +
  133 + setDisabledState(isDisabled: boolean): void {
  134 + this.disabled = isDisabled;
  135 + }
  136 +
  137 + writeValue(value: any): void {
  138 + this.fileName = null;
  139 + }
  140 +
  141 + private updateModel() {
  142 + this.propagateChange(this.fileContent);
  143 + }
  144 +
  145 + clearFile() {
  146 + this.fileName = null;
  147 + this.fileContent = null;
  148 + this.updateModel();
  149 + }
  150 +}
@@ -18,7 +18,7 @@ @@ -18,7 +18,7 @@
18 <div class="tb-container"> 18 <div class="tb-container">
19 <label class="tb-title">{{label}}</label> 19 <label class="tb-title">{{label}}</label>
20 <ng-container #flow="flow" 20 <ng-container #flow="flow"
21 - [flowConfig]="{singleFile: true}"> 21 + [flowConfig]="{singleFile: true, allowDuplicateUploads: true}">
22 <div class="tb-image-select-container"> 22 <div class="tb-image-select-container">
23 <div class="tb-image-preview-container"> 23 <div class="tb-image-preview-container">
24 <div *ngIf="!safeImageUrl" translate>dashboard.no-image</div> 24 <div *ngIf="!safeImageUrl" translate>dashboard.no-image</div>
@@ -135,11 +135,16 @@ export interface EntityAliasFilter extends EntityFilters { @@ -135,11 +135,16 @@ export interface EntityAliasFilter extends EntityFilters {
135 export interface EntityAliasInfo { 135 export interface EntityAliasInfo {
136 alias: string; 136 alias: string;
137 filter: EntityAliasFilter; 137 filter: EntityAliasFilter;
  138 + [key: string]: any;
  139 +}
  140 +
  141 +export interface AliasesInfo {
  142 + datasourceAliases: {[datasourceIndex: number]: EntityAliasInfo};
  143 + targetDeviceAliases: {[targetDeviceAliasIndex: number]: EntityAliasInfo};
138 } 144 }
139 145
140 export interface EntityAlias extends EntityAliasInfo { 146 export interface EntityAlias extends EntityAliasInfo {
141 id: string; 147 id: string;
142 - [key: string]: any;  
143 } 148 }
144 149
145 export interface EntityAliases { 150 export interface EntityAliases {
@@ -111,6 +111,7 @@ import { JsonFormComponent } from './components/json-form/json-form.component'; @@ -111,6 +111,7 @@ import { JsonFormComponent } from './components/json-form/json-form.component';
111 import { MaterialIconsDialogComponent } from '@shared/components/dialog/material-icons-dialog.component'; 111 import { MaterialIconsDialogComponent } from '@shared/components/dialog/material-icons-dialog.component';
112 import { MaterialIconSelectComponent } from '@shared/components/material-icon-select.component'; 112 import { MaterialIconSelectComponent } from '@shared/components/material-icon-select.component';
113 import { ImageInputComponent } from './components/image-input.component'; 113 import { ImageInputComponent } from './components/image-input.component';
  114 +import { FileInputComponent } from './components/file-input.component';
114 115
115 @NgModule({ 116 @NgModule({
116 providers: [ 117 providers: [
@@ -181,6 +182,7 @@ import { ImageInputComponent } from './components/image-input.component'; @@ -181,6 +182,7 @@ import { ImageInputComponent } from './components/image-input.component';
181 MaterialIconSelectComponent, 182 MaterialIconSelectComponent,
182 JsonFormComponent, 183 JsonFormComponent,
183 ImageInputComponent, 184 ImageInputComponent,
  185 + FileInputComponent,
184 NospacePipe, 186 NospacePipe,
185 MillisecondsToTimeStringPipe, 187 MillisecondsToTimeStringPipe,
186 EnumToArrayPipe, 188 EnumToArrayPipe,
@@ -318,6 +320,7 @@ import { ImageInputComponent } from './components/image-input.component'; @@ -318,6 +320,7 @@ import { ImageInputComponent } from './components/image-input.component';
318 MaterialIconSelectComponent, 320 MaterialIconSelectComponent,
319 JsonFormComponent, 321 JsonFormComponent,
320 ImageInputComponent, 322 ImageInputComponent,
  323 + FileInputComponent,
321 NospacePipe, 324 NospacePipe,
322 MillisecondsToTimeStringPipe, 325 MillisecondsToTimeStringPipe,
323 EnumToArrayPipe, 326 EnumToArrayPipe,