Showing
16 changed files
with
991 additions
and
32 deletions
... | ... | @@ -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 | 803 | private entitiesToEntitiesInfo(entities: Array<BaseData<EntityId>>): Array<EntityInfo> { |
786 | 804 | const entitiesInfo = []; |
787 | 805 | if (entities) { | ... | ... |
... | ... | @@ -16,7 +16,7 @@ |
16 | 16 | |
17 | 17 | import { Injectable } from '@angular/core'; |
18 | 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 | 20 | import { DatasourceType, Widget, WidgetPosition, WidgetSize } from '@shared/models/widget.models'; |
21 | 21 | import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; |
22 | 22 | import { deepClone } from '@core/utils'; |
... | ... | @@ -29,11 +29,6 @@ const WIDGET_ITEM = 'widget_item'; |
29 | 29 | const WIDGET_REFERENCE = 'widget_reference'; |
30 | 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 | 32 | export interface WidgetItem { |
38 | 33 | widget: Widget; |
39 | 34 | aliasesInfo: AliasesInfo; | ... | ... |
... | ... | @@ -37,14 +37,14 @@ import { UtilsService } from '@core/services/utils.service'; |
37 | 37 | import { TranslateService } from '@ngx-translate/core'; |
38 | 38 | import { ActionNotificationShow } from '@core/notification/notification.actions'; |
39 | 39 | import { DialogService } from '@core/services/dialog.service'; |
40 | -import { deepClone } from '@core/utils'; | |
40 | +import { deepClone, isUndefined } from '@core/utils'; | |
41 | 41 | import { MatDialog } from '@angular/material/dialog'; |
42 | 42 | import { EntityAliasDialogComponent, EntityAliasDialogData } from './entity-alias-dialog.component'; |
43 | 43 | |
44 | 44 | export interface EntityAliasesDialogData { |
45 | 45 | entityAliases: EntityAliases; |
46 | 46 | widgets: Array<Widget>; |
47 | - isSingleEntityAlias: boolean; | |
47 | + isSingleEntityAlias?: boolean; | |
48 | 48 | isSingleWidget?: boolean; |
49 | 49 | allowedEntityTypes?: Array<AliasEntityType>; |
50 | 50 | disableAdd?: boolean; |
... | ... | @@ -125,14 +125,13 @@ export class EntityAliasesDialogComponent extends DialogComponent<EntityAliasesD |
125 | 125 | const entityAliasControls: Array<AbstractControl> = []; |
126 | 126 | for (const aliasId of Object.keys(this.data.entityAliases)) { |
127 | 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 | 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 | 136 | entityAliasControls.push(this.createEntityAliasFormControl(aliasId, entityAlias)); |
138 | 137 | } | ... | ... |
... | ... | @@ -58,6 +58,8 @@ import { CustomActionPrettyResourcesTabsComponent } from './widget/action/custom |
58 | 58 | import { CustomActionPrettyEditorComponent } from './widget/action/custom-action-pretty-editor.component'; |
59 | 59 | import { CustomDialogService } from './widget/dialog/custom-dialog.service'; |
60 | 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 | 64 | @NgModule({ |
63 | 65 | entryComponents: [ |
... | ... | @@ -75,7 +77,8 @@ import { CustomDialogContainerComponent } from './widget/dialog/custom-dialog-co |
75 | 77 | DataKeyConfigDialogComponent, |
76 | 78 | LegendConfigPanelComponent, |
77 | 79 | WidgetActionDialogComponent, |
78 | - CustomDialogContainerComponent | |
80 | + CustomDialogContainerComponent, | |
81 | + ImportDialogComponent | |
79 | 82 | ], |
80 | 83 | declarations: |
81 | 84 | [ |
... | ... | @@ -117,7 +120,8 @@ import { CustomDialogContainerComponent } from './widget/dialog/custom-dialog-co |
117 | 120 | WidgetActionDialogComponent, |
118 | 121 | CustomActionPrettyResourcesTabsComponent, |
119 | 122 | CustomActionPrettyEditorComponent, |
120 | - CustomDialogContainerComponent | |
123 | + CustomDialogContainerComponent, | |
124 | + ImportDialogComponent | |
121 | 125 | ], |
122 | 126 | imports: [ |
123 | 127 | CommonModule, |
... | ... | @@ -154,11 +158,13 @@ import { CustomDialogContainerComponent } from './widget/dialog/custom-dialog-co |
154 | 158 | WidgetActionDialogComponent, |
155 | 159 | CustomActionPrettyResourcesTabsComponent, |
156 | 160 | CustomActionPrettyEditorComponent, |
157 | - CustomDialogContainerComponent | |
161 | + CustomDialogContainerComponent, | |
162 | + ImportDialogComponent | |
158 | 163 | ], |
159 | 164 | providers: [ |
160 | 165 | WidgetComponentService, |
161 | - CustomDialogService | |
166 | + CustomDialogService, | |
167 | + ImportExportService | |
162 | 168 | ] |
163 | 169 | }) |
164 | 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 | 83 | ManageDashboardStatesDialogComponent, |
84 | 84 | ManageDashboardStatesDialogData |
85 | 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 | 88 | @Component({ |
88 | 89 | selector: 'tb-dashboard-page', |
... | ... | @@ -222,6 +223,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
222 | 223 | private widgetComponentService: WidgetComponentService, |
223 | 224 | private dashboardService: DashboardService, |
224 | 225 | private itembuffer: ItemBufferService, |
226 | + private importExport: ImportExportService, | |
225 | 227 | private fb: FormBuilder, |
226 | 228 | private dialog: MatDialog, |
227 | 229 | private translate: TranslateService, |
... | ... | @@ -459,8 +461,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
459 | 461 | if ($event) { |
460 | 462 | $event.stopPropagation(); |
461 | 463 | } |
462 | - // TODO: | |
463 | - this.dialogService.todo(); | |
464 | + this.importExport.exportDashboard(this.currentDashboardId); | |
464 | 465 | } |
465 | 466 | |
466 | 467 | public openEntityAliases($event: Event) { |
... | ... | @@ -569,8 +570,16 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
569 | 570 | if ($event) { |
570 | 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 | 585 | public currentDashboardIdChanged(dashboardId: string) { |
... | ... | @@ -915,8 +924,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
915 | 924 | |
916 | 925 | exportWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) { |
917 | 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 | 930 | widgetClicked($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) { | ... | ... |
... | ... | @@ -63,6 +63,7 @@ import { |
63 | 63 | MakeDashboardPublicDialogData |
64 | 64 | } from '@modules/home/pages/dashboard/make-dashboard-public-dialog.component'; |
65 | 65 | import { DashboardTabsComponent } from '@home/pages/dashboard/dashboard-tabs.component'; |
66 | +import { ImportExportService } from '@home/components/import-export/import-export.service'; | |
66 | 67 | |
67 | 68 | @Injectable() |
68 | 69 | export class DashboardsTableConfigResolver implements Resolve<EntityTableConfig<DashboardInfo | Dashboard>> { |
... | ... | @@ -73,6 +74,7 @@ export class DashboardsTableConfigResolver implements Resolve<EntityTableConfig< |
73 | 74 | private dashboardService: DashboardService, |
74 | 75 | private customerService: CustomerService, |
75 | 76 | private dialogService: DialogService, |
77 | + private importExport: ImportExportService, | |
76 | 78 | private translate: TranslateService, |
77 | 79 | private datePipe: DatePipe, |
78 | 80 | private router: Router, |
... | ... | @@ -311,19 +313,20 @@ export class DashboardsTableConfigResolver implements Resolve<EntityTableConfig< |
311 | 313 | } |
312 | 314 | |
313 | 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 | 325 | exportDashboard($event: Event, dashboard: DashboardInfo) { |
322 | 326 | if ($event) { |
323 | 327 | $event.stopPropagation(); |
324 | 328 | } |
325 | - // TODO: | |
326 | - this.dialogService.todo(); | |
329 | + this.importExport.exportDashboard(dashboard.id.id); | |
327 | 330 | } |
328 | 331 | |
329 | 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 | 18 | <div class="tb-container"> |
19 | 19 | <label class="tb-title">{{label}}</label> |
20 | 20 | <ng-container #flow="flow" |
21 | - [flowConfig]="{singleFile: true}"> | |
21 | + [flowConfig]="{singleFile: true, allowDuplicateUploads: true}"> | |
22 | 22 | <div class="tb-image-select-container"> |
23 | 23 | <div class="tb-image-preview-container"> |
24 | 24 | <div *ngIf="!safeImageUrl" translate>dashboard.no-image</div> | ... | ... |
... | ... | @@ -135,11 +135,16 @@ export interface EntityAliasFilter extends EntityFilters { |
135 | 135 | export interface EntityAliasInfo { |
136 | 136 | alias: string; |
137 | 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 | 146 | export interface EntityAlias extends EntityAliasInfo { |
141 | 147 | id: string; |
142 | - [key: string]: any; | |
143 | 148 | } |
144 | 149 | |
145 | 150 | export interface EntityAliases { | ... | ... |
... | ... | @@ -111,6 +111,7 @@ import { JsonFormComponent } from './components/json-form/json-form.component'; |
111 | 111 | import { MaterialIconsDialogComponent } from '@shared/components/dialog/material-icons-dialog.component'; |
112 | 112 | import { MaterialIconSelectComponent } from '@shared/components/material-icon-select.component'; |
113 | 113 | import { ImageInputComponent } from './components/image-input.component'; |
114 | +import { FileInputComponent } from './components/file-input.component'; | |
114 | 115 | |
115 | 116 | @NgModule({ |
116 | 117 | providers: [ |
... | ... | @@ -181,6 +182,7 @@ import { ImageInputComponent } from './components/image-input.component'; |
181 | 182 | MaterialIconSelectComponent, |
182 | 183 | JsonFormComponent, |
183 | 184 | ImageInputComponent, |
185 | + FileInputComponent, | |
184 | 186 | NospacePipe, |
185 | 187 | MillisecondsToTimeStringPipe, |
186 | 188 | EnumToArrayPipe, |
... | ... | @@ -318,6 +320,7 @@ import { ImageInputComponent } from './components/image-input.component'; |
318 | 320 | MaterialIconSelectComponent, |
319 | 321 | JsonFormComponent, |
320 | 322 | ImageInputComponent, |
323 | + FileInputComponent, | |
321 | 324 | NospacePipe, |
322 | 325 | MillisecondsToTimeStringPipe, |
323 | 326 | EnumToArrayPipe, | ... | ... |