Showing
16 changed files
with
991 additions
and
32 deletions
@@ -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, |