Merge pull request #13949 from LeoMorgan113/feature/dashboard-json-upload

Added feature to upload dashboard JSON file to update
This commit is contained in:
Igor Kulikov 2025-09-25 16:43:33 +03:00 committed by GitHub
commit 7e63a9b028
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 200 additions and 16 deletions

View File

@ -28,6 +28,12 @@
[class.!hidden]="isEdit || dashboardScope !== 'tenant'"> [class.!hidden]="isEdit || dashboardScope !== 'tenant'">
{{'dashboard.export' | translate }} {{'dashboard.export' | translate }}
</button> </button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'import')"
[class.!hidden]="isEdit || dashboardScope !== 'tenant'">
{{'dashboard.update-new-version' | translate }}
</button>
<button mat-raised-button color="primary" <button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'makePublic')" (click)="onEntityAction($event, 'makePublic')"

View File

@ -22,7 +22,7 @@ import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms
import { ActionNotificationShow } from '@core/notification/notification.actions'; import { ActionNotificationShow } from '@core/notification/notification.actions';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { import {
Dashboard, Dashboard, DashboardInfo,
getDashboardAssignedCustomersText, getDashboardAssignedCustomersText,
isCurrentPublicDashboardCustomer, isCurrentPublicDashboardCustomer,
isPublicDashboard isPublicDashboard
@ -31,13 +31,14 @@ import { DashboardService } from '@core/http/dashboard.service';
import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
import { isEqual } from '@core/utils'; import { isEqual } from '@core/utils';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { PageLink } from "@shared/models/page/page-link";
@Component({ @Component({
selector: 'tb-dashboard-form', selector: 'tb-dashboard-form',
templateUrl: './dashboard-form.component.html', templateUrl: './dashboard-form.component.html',
styleUrls: ['./dashboard-form.component.scss'] styleUrls: ['./dashboard-form.component.scss']
}) })
export class DashboardFormComponent extends EntityComponent<Dashboard> { export class DashboardFormComponent extends EntityComponent<Dashboard, PageLink, DashboardInfo> {
dashboardScope: 'tenant' | 'customer' | 'customer_user' | 'edge'; dashboardScope: 'tenant' | 'customer' | 'customer_user' | 'edge';
customerId: string; customerId: string;
@ -117,7 +118,7 @@ export class DashboardFormComponent extends EntityComponent<Dashboard> {
onPublicLinkCopied($event) { onPublicLinkCopied($event) {
this.store.dispatch(new ActionNotificationShow( this.store.dispatch(new ActionNotificationShow(
{ {
message: this.translate.instant('dashboard.public-link-copied-message'), message: this.translate.instant('dashboard.public-link-copied-message'),
type: 'success', type: 'success',
duration: 750, duration: 750,

View File

@ -19,18 +19,24 @@ import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module'; import { SharedModule } from '@shared/shared.module';
import { HomeDialogsModule } from '../../dialogs/home-dialogs.module'; import { HomeDialogsModule } from '../../dialogs/home-dialogs.module';
import { DashboardFormComponent } from '@modules/home/pages/dashboard/dashboard-form.component'; import { DashboardFormComponent } from '@modules/home/pages/dashboard/dashboard-form.component';
import { ManageDashboardCustomersDialogComponent } from '@modules/home/pages/dashboard/manage-dashboard-customers-dialog.component'; import {
ManageDashboardCustomersDialogComponent
} from '@modules/home/pages/dashboard/manage-dashboard-customers-dialog.component';
import { DashboardRoutingModule } from './dashboard-routing.module'; import { DashboardRoutingModule } from './dashboard-routing.module';
import { MakeDashboardPublicDialogComponent } from '@modules/home/pages/dashboard/make-dashboard-public-dialog.component'; import {
MakeDashboardPublicDialogComponent
} from '@modules/home/pages/dashboard/make-dashboard-public-dialog.component';
import { HomeComponentsModule } from '@modules/home/components/home-components.module'; import { HomeComponentsModule } from '@modules/home/components/home-components.module';
import { DashboardTabsComponent } from '@home/pages/dashboard/dashboard-tabs.component'; import { DashboardTabsComponent } from '@home/pages/dashboard/dashboard-tabs.component';
import { ImportDashboardFileDialogComponent } from "@home/pages/dashboard/import-dashboard-file-dialog.component";
@NgModule({ @NgModule({
declarations: [ declarations: [
DashboardFormComponent, DashboardFormComponent,
DashboardTabsComponent, DashboardTabsComponent,
ManageDashboardCustomersDialogComponent, ManageDashboardCustomersDialogComponent,
MakeDashboardPublicDialogComponent MakeDashboardPublicDialogComponent,
ImportDashboardFileDialogComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -40,4 +46,5 @@ import { DashboardTabsComponent } from '@home/pages/dashboard/dashboard-tabs.com
DashboardRoutingModule DashboardRoutingModule
] ]
}) })
export class DashboardModule { } export class DashboardModule {
}

View File

@ -77,11 +77,16 @@ import {
EntityAliasesDialogComponent, EntityAliasesDialogComponent,
EntityAliasesDialogData EntityAliasesDialogData
} from '@home/components/alias/entity-aliases-dialog.component'; } from '@home/components/alias/entity-aliases-dialog.component';
import {
DashboardInfoDialogData,
ImportDashboardFileDialogComponent
} from "@home/pages/dashboard/import-dashboard-file-dialog.component";
import { PageLink } from "@shared/models/page/page-link";
@Injectable() @Injectable()
export class DashboardsTableConfigResolver { export class DashboardsTableConfigResolver {
private readonly config: EntityTableConfig<DashboardInfo | Dashboard> = new EntityTableConfig<DashboardInfo | Dashboard>(); private readonly config: EntityTableConfig<Dashboard, PageLink, DashboardInfo> = new EntityTableConfig<Dashboard, PageLink, DashboardInfo>();
constructor(private store: Store<AppState>, constructor(private store: Store<AppState>,
private dashboardService: DashboardService, private dashboardService: DashboardService,
@ -375,7 +380,7 @@ export class DashboardsTableConfigResolver {
return actions; return actions;
} }
openDashboard($event: Event, dashboard: DashboardInfo) { openDashboard($event: Event, dashboard: Dashboard) {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
@ -422,13 +427,27 @@ export class DashboardsTableConfigResolver {
)); ));
} }
exportDashboard($event: Event, dashboard: DashboardInfo) { exportDashboard($event: Event, dashboard: Dashboard) {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
this.importExport.exportDashboard(dashboard.id.id); this.importExport.exportDashboard(dashboard.id.id);
} }
importDashboardFile($event: Event, dashboard: Dashboard) {
if ($event) {
$event.stopPropagation();
}
return this.dialog.open<ImportDashboardFileDialogComponent, DashboardInfoDialogData,
boolean>(ImportDashboardFileDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
dashboard
}
}).afterClosed();
}
addDashboardsToCustomer($event: Event) { addDashboardsToCustomer($event: Event) {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
@ -449,7 +468,7 @@ export class DashboardsTableConfigResolver {
}); });
} }
makePublic($event: Event, dashboard: DashboardInfo) { makePublic($event: Event, dashboard: Dashboard) {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
@ -470,7 +489,7 @@ export class DashboardsTableConfigResolver {
); );
} }
makePrivate($event: Event, dashboard: DashboardInfo) { makePrivate($event: Event, dashboard: Dashboard) {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
@ -492,7 +511,7 @@ export class DashboardsTableConfigResolver {
); );
} }
manageAssignedCustomers($event: Event, dashboard: DashboardInfo) { manageAssignedCustomers($event: Event, dashboard: Dashboard) {
const assignedCustomersIds = dashboard.assignedCustomers ? const assignedCustomersIds = dashboard.assignedCustomers ?
dashboard.assignedCustomers.map(customerInfo => customerInfo.customerId.id) : []; dashboard.assignedCustomers.map(customerInfo => customerInfo.customerId.id) : [];
this.showManageAssignedCustomersDialog($event, [dashboard.id.id], 'manage', assignedCustomersIds); this.showManageAssignedCustomersDialog($event, [dashboard.id.id], 'manage', assignedCustomersIds);
@ -529,7 +548,7 @@ export class DashboardsTableConfigResolver {
}); });
} }
unassignFromCustomer($event: Event, dashboard: DashboardInfo, customerId: string) { unassignFromCustomer($event: Event, dashboard: Dashboard, customerId: string) {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
@ -579,7 +598,7 @@ export class DashboardsTableConfigResolver {
); );
} }
onDashboardAction(action: EntityAction<DashboardInfo>): boolean { onDashboardAction(action: EntityAction<Dashboard>): boolean {
switch (action.action) { switch (action.action) {
case 'open': case 'open':
this.openDashboard(action.event, action.entity); this.openDashboard(action.event, action.entity);
@ -587,6 +606,9 @@ export class DashboardsTableConfigResolver {
case 'export': case 'export':
this.exportDashboard(action.event, action.entity); this.exportDashboard(action.event, action.entity);
return true; return true;
case 'import':
this.importDashboardFile(action.event, action.entity);
return true;
case 'makePublic': case 'makePublic':
this.makePublic(action.event, action.entity); this.makePublic(action.event, action.entity);
return true; return true;

View File

@ -0,0 +1,57 @@
<!--
Copyright © 2016-2025 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form [formGroup]="uploadFileFormGroup" (ngSubmit)="save()" style="width: 800px;">
<mat-toolbar color="primary">
<h2>{{ 'dashboard.update-new-version' | translate }}</h2>
<span class="flex-1"></span>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content>
<tb-file-input [contentConvertFunction]="loadDataFromJsonContent"
[existingFileName]="currentFileName"
(fileNameChanged)="currentFileName = $event"
label="{{'dashboard.upload-file-to-update' | translate}}"
formControlName="file"
dropLabel="{{ 'import.drop-json-file-or' | translate }}"
accept=".json,application/json"
allowedExtensions="json">
</tb-file-input>
</div>
<div mat-dialog-actions class="flex flex-row items-center justify-end">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || uploadFileFormGroup.invalid
|| !uploadFileFormGroup.dirty">
{{ 'action.save' | translate }}
</button>
</div>
</form>

View File

@ -0,0 +1,89 @@
///
/// Copyright © 2016-2025 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, Inject, OnInit } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormGroup } from '@angular/forms';
import { DashboardService } from '@core/http/dashboard.service';
import { Dashboard } from '@app/shared/models/dashboard.models';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
export interface DashboardInfoDialogData {
dashboard: Dashboard;
}
@Component({
selector: 'tb-import-dashboard-file-dialog',
templateUrl: './import-dashboard-file-dialog.component.html',
styleUrls: []
})
export class ImportDashboardFileDialogComponent extends DialogComponent<ImportDashboardFileDialogComponent> implements OnInit {
private dashboard: Dashboard;
currentFileName: string = '';
uploadFileFormGroup: FormGroup;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: DashboardInfoDialogData,
private dashboardService: DashboardService,
protected dialogRef: MatDialogRef<ImportDashboardFileDialogComponent>,
private fb: FormBuilder) {
super(store, router, dialogRef);
this.dashboard = data.dashboard;
}
ngOnInit(): void {
this.uploadFileFormGroup = this.fb.group({
file: [null]
});
}
cancel(): void {
this.dialogRef.close();
}
save() {
const fileControl = this.uploadFileFormGroup.get('file');
if (!fileControl || !fileControl.value) {
return;
}
const dashboardContent = {
...fileControl.value,
description: this.dashboard.configuration.description
};
this.dashboard.configuration = dashboardContent;
this.dashboardService.saveDashboard(this.dashboard).subscribe(() => {
this.dialogRef.close(true);
})
}
loadDataFromJsonContent(content: string): any {
try {
const importData = JSON.parse(content);
return importData ? importData['configuration'] : importData;
} catch (err) {
this.store.dispatch(new ActionNotificationShow({message: err.message, type: 'error'}));
return null;
}
}
}

View File

@ -1429,6 +1429,8 @@
"mobile-order": "Dashboard order in mobile application", "mobile-order": "Dashboard order in mobile application",
"mobile-hide": "Hide dashboard in mobile application", "mobile-hide": "Hide dashboard in mobile application",
"update-image": "Update dashboard image", "update-image": "Update dashboard image",
"update-new-version": "Upload new version",
"upload-file-to-update": "Upload file to update",
"take-screenshot": "Take screenshot", "take-screenshot": "Take screenshot",
"select-widget-title": "Select widget", "select-widget-title": "Select widget",
"select-widget-value": "{{title}}: select widget", "select-widget-value": "{{title}}: select widget",