UI: Implements support tb-resources and added support in extension
This commit is contained in:
parent
a4433e6134
commit
cfce9b8950
@ -71,8 +71,12 @@ export class DashboardService {
|
||||
return this.http.get<Dashboard>(`/api/dashboard/${dashboardId}`, defaultHttpOptionsFromConfig(config));
|
||||
}
|
||||
|
||||
public exportDashboard(dashboardId: string, config?: RequestConfig): Observable<Dashboard> {
|
||||
return this.http.get<Dashboard>(`/api/dashboard/${dashboardId}?includeResources=true`, defaultHttpOptionsFromConfig(config));
|
||||
public exportDashboard(dashboardId: string, includeResources = true, config?: RequestConfig): Observable<Dashboard> {
|
||||
let url = `/api/dashboard/${dashboardId}`;
|
||||
if (includeResources) {
|
||||
url += '?includeResources=true';
|
||||
}
|
||||
return this.http.get<Dashboard>(url, defaultHttpOptionsFromConfig(config));
|
||||
}
|
||||
|
||||
public getDashboardInfo(dashboardId: string, config?: RequestConfig): Observable<DashboardInfo> {
|
||||
|
||||
@ -20,7 +20,7 @@ import { PageLink } from '@shared/models/page/page-link';
|
||||
import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils';
|
||||
import { forkJoin, Observable, of } from 'rxjs';
|
||||
import { PageData } from '@shared/models/page/page-data';
|
||||
import { Resource, ResourceInfo, ResourceType } from '@shared/models/resource.models';
|
||||
import { Resource, ResourceInfo, ResourceType, TBResourceScope } from '@shared/models/resource.models';
|
||||
import { catchError, mergeMap } from 'rxjs/operators';
|
||||
import { isNotEmptyStr } from '@core/utils';
|
||||
import { ResourcesService } from '@core/services/resources.service';
|
||||
@ -52,10 +52,14 @@ export class ResourceService {
|
||||
return this.http.get<Resource>(`/api/resource/${resourceId}`, defaultHttpOptionsFromConfig(config));
|
||||
}
|
||||
|
||||
public getResourceInfo(resourceId: string, config?: RequestConfig): Observable<ResourceInfo> {
|
||||
public getResourceInfoById(resourceId: string, config?: RequestConfig): Observable<ResourceInfo> {
|
||||
return this.http.get<Resource>(`/api/resource/info/${resourceId}`, defaultHttpOptionsFromConfig(config));
|
||||
}
|
||||
|
||||
public getResourceInfo(type: ResourceType, scope: TBResourceScope, key: string, config?: RequestConfig): Observable<ResourceInfo> {
|
||||
return this.http.get<Resource>(`/api/resource/${type}/${scope}/${key}/info`, defaultHttpOptionsFromConfig(config));
|
||||
}
|
||||
|
||||
public downloadResource(resourceId: string): Observable<any> {
|
||||
return this.resourcesService.downloadResource(`/api/resource/${resourceId}/download`);
|
||||
}
|
||||
|
||||
@ -31,7 +31,6 @@ import {
|
||||
WidgetTypeInfo,
|
||||
widgetTypesData
|
||||
} from '@shared/models/widget.models';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { toWidgetInfo, toWidgetTypeDetails, WidgetInfo } from '@app/modules/home/models/widget-component.models';
|
||||
import { filter, map, mergeMap, tap } from 'rxjs/operators';
|
||||
import { WidgetTypeId } from '@shared/models/id/widget-type-id';
|
||||
@ -53,7 +52,6 @@ export class WidgetService {
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private translate: TranslateService,
|
||||
private router: Router
|
||||
) {
|
||||
this.router.events.pipe(filter(event => event instanceof ActivationEnd)).subscribe(
|
||||
@ -143,9 +141,13 @@ export class WidgetService {
|
||||
}
|
||||
|
||||
public exportBundleWidgetTypesDetails(widgetsBundleId: string,
|
||||
config?: RequestConfig): Observable<Array<WidgetTypeDetails>> {
|
||||
return this.http.get<Array<WidgetTypeDetails>>(`/api/widgetTypesDetails?widgetsBundleId=${widgetsBundleId}&includeResources=true`,
|
||||
defaultHttpOptionsFromConfig(config));
|
||||
includeResources = true,
|
||||
config?: RequestConfig): Observable<Array<WidgetTypeDetails>> {
|
||||
let url = `/api/widgetTypesDetails?widgetsBundleId=${widgetsBundleId}`
|
||||
if (includeResources) {
|
||||
url += '&includeResources=true';
|
||||
}
|
||||
return this.http.get<Array<WidgetTypeDetails>>(url, defaultHttpOptionsFromConfig(config));
|
||||
}
|
||||
|
||||
public getBundleWidgetTypeFqns(widgetsBundleId: string,
|
||||
@ -211,9 +213,13 @@ export class WidgetService {
|
||||
}
|
||||
|
||||
public exportWidgetType(widgetTypeId: string,
|
||||
includeResources = true,
|
||||
config?: RequestConfig): Observable<WidgetTypeDetails> {
|
||||
return this.http.get<WidgetTypeDetails>(`/api/widgetType/${widgetTypeId}?includeResources=true`,
|
||||
defaultHttpOptionsFromConfig(config));
|
||||
let url = `/api/widgetType/${widgetTypeId}`;
|
||||
if (includeResources) {
|
||||
url += '?includeResources=true';
|
||||
}
|
||||
return this.http.get<WidgetTypeDetails>(url, defaultHttpOptionsFromConfig(config));
|
||||
}
|
||||
|
||||
public getWidgetTypeInfoById(widgetTypeId: string,
|
||||
|
||||
@ -39,6 +39,7 @@ import { AppState } from '@core/core.state';
|
||||
import { map, tap } from 'rxjs/operators';
|
||||
import { RequestConfig } from '@core/http/http-utils';
|
||||
import { getFlexLayoutModule } from '@app/shared/legacy/flex-layout.models';
|
||||
import { isJSResource, removeTbResourcePrefix } from '@shared/models/resource.models';
|
||||
|
||||
export interface ModuleInfo {
|
||||
module: ɵNgModuleDef<any>;
|
||||
@ -377,11 +378,11 @@ export class ResourcesService {
|
||||
if (isObject(resourceId)) {
|
||||
return `/api/resource/js/${(resourceId as TbResourceId).id}/download`;
|
||||
}
|
||||
return resourceId as string;
|
||||
return removeTbResourcePrefix(resourceId as string);
|
||||
}
|
||||
|
||||
private getMetaInfo(resourceId: string | TbResourceId): object {
|
||||
if (isObject(resourceId)) {
|
||||
if (isObject(resourceId) || (typeof resourceId === 'string' && isJSResource(resourceId))) {
|
||||
return {
|
||||
additionalHeaders: {
|
||||
'X-Authorization': `Bearer ${AuthService.getJwtToken()}`
|
||||
|
||||
@ -338,6 +338,7 @@ import { TimezonePanelComponent } from '@shared/components/time/timezone-panel.c
|
||||
import { DatapointsLimitComponent } from '@shared/components/time/datapoints-limit.component';
|
||||
import { Observable, map, of } from 'rxjs';
|
||||
import { getFlexLayout } from '@shared/legacy/flex-layout.models';
|
||||
import { isJSResourceUrl } from '@shared/public-api';
|
||||
|
||||
class ModulesMap implements IModulesMap {
|
||||
|
||||
@ -693,7 +694,7 @@ class ModulesMap implements IModulesMap {
|
||||
for (const moduleId of Object.keys(this.modulesMap)) {
|
||||
System.set('app:' + moduleId, this.modulesMap[moduleId]);
|
||||
}
|
||||
System.constructor.prototype.shouldFetch = (url: string) => url.endsWith('/download');
|
||||
System.constructor.prototype.shouldFetch = (url: string) => url.endsWith('/download') || isJSResourceUrl(url);
|
||||
System.constructor.prototype.fetch = (url: string, options: RequestInit & {meta?: any}) => {
|
||||
if (options?.meta?.additionalHeaders) {
|
||||
options.headers = { ...options.headers, ...options.meta.additionalHeaders };
|
||||
|
||||
@ -84,7 +84,7 @@ export class ResourcesLibraryTableConfigResolver {
|
||||
this.config.deleteEntitiesContent = () => this.translate.instant('resource.delete-resources-text');
|
||||
|
||||
this.config.entitiesFetchFunction = pageLink => this.resourceService.getResources(pageLink, this.config.componentsData.resourceType);
|
||||
this.config.loadEntity = id => this.resourceService.getResourceInfo(id.id);
|
||||
this.config.loadEntity = id => this.resourceService.getResourceInfoById(id.id);
|
||||
this.config.saveEntity = resource => this.saveResource(resource);
|
||||
this.config.deleteEntity = id => this.resourceService.deleteResource(id.id);
|
||||
|
||||
|
||||
@ -20,7 +20,14 @@ import { coerceBoolean } from '@shared/decorators/coercion';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { catchError, debounceTime, map, share, switchMap, tap } from 'rxjs/operators';
|
||||
import { isDefinedAndNotNull, isEmptyStr, isEqual, isObject } from '@core/utils';
|
||||
import { ResourceInfo, ResourceType } from '@shared/models/resource.models';
|
||||
import {
|
||||
extractParamsFromJSResourceUrl,
|
||||
isJSResource,
|
||||
prependTbResourcePrefix,
|
||||
removeTbResourcePrefix,
|
||||
ResourceInfo,
|
||||
ResourceType
|
||||
} from '@shared/models/resource.models';
|
||||
import { TbResourceId } from '@shared/models/id/tb-resource-id';
|
||||
import { ResourceService } from '@core/http/resource.service';
|
||||
import { PageLink } from '@shared/models/page/page-link';
|
||||
@ -64,7 +71,7 @@ export class ResourceAutocompleteComponent implements ControlValueAccessor, OnIn
|
||||
allowAutocomplete = false;
|
||||
|
||||
resourceFormGroup = this.fb.group({
|
||||
resource: [null]
|
||||
resource: this.fb.control<string|ResourceInfo>(null)
|
||||
});
|
||||
|
||||
filteredResources$: Observable<Array<ResourceInfo>>;
|
||||
@ -73,10 +80,10 @@ export class ResourceAutocompleteComponent implements ControlValueAccessor, OnIn
|
||||
|
||||
@ViewChild('resourceInput', {static: true}) resourceInput: ElementRef;
|
||||
|
||||
private modelValue: string | TbResourceId;
|
||||
private modelValue: string;
|
||||
private dirty = false;
|
||||
|
||||
private propagateChange = (v: any) => { };
|
||||
private propagateChange: (value: any) => void = () => {};
|
||||
|
||||
constructor(private fb: FormBuilder,
|
||||
private resourceService: ResourceService) {
|
||||
@ -91,13 +98,13 @@ export class ResourceAutocompleteComponent implements ControlValueAccessor, OnIn
|
||||
.pipe(
|
||||
debounceTime(150),
|
||||
tap(value => {
|
||||
let modelValue;
|
||||
let modelValue: string;
|
||||
if (isObject(value)) {
|
||||
modelValue = value.id;
|
||||
modelValue = prependTbResourcePrefix((value as ResourceInfo).link);
|
||||
} else if (isEmptyStr(value)) {
|
||||
modelValue = null;
|
||||
} else {
|
||||
modelValue = value;
|
||||
modelValue = value as string;
|
||||
}
|
||||
this.updateView(modelValue);
|
||||
if (value === null) {
|
||||
@ -105,7 +112,7 @@ export class ResourceAutocompleteComponent implements ControlValueAccessor, OnIn
|
||||
}
|
||||
}),
|
||||
map(value => value ? (typeof value === 'string' ? value : value.title) : ''),
|
||||
switchMap(name => this.fetchResources(name) ),
|
||||
switchMap(name => this.fetchResources(name)),
|
||||
share()
|
||||
);
|
||||
}
|
||||
@ -114,7 +121,7 @@ export class ResourceAutocompleteComponent implements ControlValueAccessor, OnIn
|
||||
this.propagateChange = fn;
|
||||
}
|
||||
|
||||
registerOnTouched(fn: any): void {
|
||||
registerOnTouched(_fn: any): void {
|
||||
}
|
||||
|
||||
setDisabledState(isDisabled: boolean) {
|
||||
@ -130,9 +137,9 @@ export class ResourceAutocompleteComponent implements ControlValueAccessor, OnIn
|
||||
if (isDefinedAndNotNull(value)) {
|
||||
this.searchText = '';
|
||||
if (isObject(value) && typeof value !== 'string' && (value as TbResourceId).id) {
|
||||
this.resourceService.getResourceInfo(value.id, {ignoreLoading: true, ignoreErrors: true}).subscribe({
|
||||
this.resourceService.getResourceInfoById(value.id, {ignoreLoading: true, ignoreErrors: true}).subscribe({
|
||||
next: resource => {
|
||||
this.modelValue = resource.id;
|
||||
this.modelValue = prependTbResourcePrefix(resource.link);
|
||||
this.resourceFormGroup.get('resource').patchValue(resource, {emitEvent: false});
|
||||
},
|
||||
error: () => {
|
||||
@ -140,9 +147,22 @@ export class ResourceAutocompleteComponent implements ControlValueAccessor, OnIn
|
||||
this.resourceFormGroup.get('resource').patchValue('');
|
||||
}
|
||||
});
|
||||
} else if (typeof value === 'string' && isJSResource(value)) {
|
||||
const url = removeTbResourcePrefix(value);
|
||||
const params = extractParamsFromJSResourceUrl(url);
|
||||
this.resourceService.getResourceInfo(params.type, params.scope, params.key, {ignoreLoading: true, ignoreErrors: true}).subscribe({
|
||||
next: resource => {
|
||||
this.modelValue = value;
|
||||
this.resourceFormGroup.get('resource').patchValue(resource, {emitEvent: false});
|
||||
},
|
||||
error: () => {
|
||||
this.modelValue = '';
|
||||
this.resourceFormGroup.get('resource').patchValue('');
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.modelValue = value;
|
||||
this.resourceFormGroup.get('resource').patchValue(value, {emitEvent: false});
|
||||
this.modelValue = value as string;
|
||||
this.resourceFormGroup.get('resource').patchValue(value as string, {emitEvent: false});
|
||||
}
|
||||
this.dirty = true;
|
||||
}
|
||||
@ -167,7 +187,7 @@ export class ResourceAutocompleteComponent implements ControlValueAccessor, OnIn
|
||||
}
|
||||
}
|
||||
|
||||
private updateView(value: string | TbResourceId ) {
|
||||
private updateView(value: string) {
|
||||
if (!isEqual(this.modelValue, value)) {
|
||||
this.modelValue = value;
|
||||
this.propagateChange(this.modelValue);
|
||||
|
||||
@ -0,0 +1,46 @@
|
||||
<!--
|
||||
|
||||
Copyright © 2016-2024 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.
|
||||
|
||||
-->
|
||||
<mat-toolbar color="primary">
|
||||
<h2>{{ title | 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) && !ignoreLoading">
|
||||
</mat-progress-bar>
|
||||
<div mat-dialog-content>
|
||||
<fieldset [disabled]="(isLoading$ | async) && !ignoreLoading">
|
||||
<mat-checkbox [formControl]="includeResourcesFormControl">{{ prompt | translate }}</mat-checkbox>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div mat-dialog-actions class="flex items-center justify-end">
|
||||
<button mat-button color="primary"
|
||||
type="button"
|
||||
[disabled]="(isLoading$ | async) && !ignoreLoading"
|
||||
(click)="cancel()">
|
||||
{{ 'action.cancel' | translate }}
|
||||
</button>
|
||||
<button mat-raised-button color="primary"
|
||||
(click)="export()"
|
||||
[disabled]="(isLoading$ | async) && !ignoreLoading">
|
||||
{{ 'action.export' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
@ -0,0 +1,73 @@
|
||||
///
|
||||
/// Copyright © 2016-2024 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 } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppState } from '@core/core.state';
|
||||
import { FormControl } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { DialogComponent } from '@app/shared/components/dialog.component';
|
||||
import { isDefinedAndNotNull } from '@core/utils';
|
||||
|
||||
export interface ExportResourceDialogData {
|
||||
title: string;
|
||||
prompt: string;
|
||||
include?: boolean;
|
||||
ignoreLoading?: boolean;
|
||||
}
|
||||
|
||||
export interface ExportResourceDialogDialogResult {
|
||||
include: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'tb-export-resource-dialog',
|
||||
templateUrl: './export-resource-dialog.component.html',
|
||||
styleUrls: []
|
||||
})
|
||||
export class ExportResourceDialogComponent extends DialogComponent<ExportResourceDialogComponent, ExportResourceDialogDialogResult> {
|
||||
|
||||
ignoreLoading = false;
|
||||
|
||||
title: string;
|
||||
prompt: string
|
||||
|
||||
includeResourcesFormControl = new FormControl(true);
|
||||
|
||||
constructor(protected store: Store<AppState>,
|
||||
protected router: Router,
|
||||
@Inject(MAT_DIALOG_DATA) private data: ExportResourceDialogData,
|
||||
public dialogRef: MatDialogRef<ExportResourceDialogComponent, ExportResourceDialogDialogResult>) {
|
||||
super(store, router, dialogRef);
|
||||
this.ignoreLoading = this.data.ignoreLoading;
|
||||
this.title = this.data.title;
|
||||
this.prompt = this.data.prompt;
|
||||
if (isDefinedAndNotNull(this.data.include)) {
|
||||
this.includeResourcesFormControl.patchValue(this.data.include, {emitEvent: false});
|
||||
}
|
||||
}
|
||||
|
||||
cancel(): void {
|
||||
this.dialogRef.close(null);
|
||||
}
|
||||
|
||||
export(): void {
|
||||
this.dialogRef.close({
|
||||
include: this.includeResourcesFormControl.value
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -87,10 +87,17 @@ import { ActionPreferencesPutUserSettings } from '@core/auth/auth.actions';
|
||||
import { ExportableEntity } from '@shared/models/base-data';
|
||||
import { EntityId } from '@shared/models/id/entity-id';
|
||||
import { Customer } from '@shared/models/customer.model';
|
||||
import {
|
||||
ExportResourceDialogComponent,
|
||||
ExportResourceDialogData,
|
||||
ExportResourceDialogDialogResult
|
||||
} from '@shared/import-export/export-resource-dialog.component';
|
||||
|
||||
export type editMissingAliasesFunction = (widgets: Array<Widget>, isSingleWidget: boolean,
|
||||
customTitle: string, missingEntityAliases: EntityAliases) => Observable<EntityAliases>;
|
||||
|
||||
type SupportEntityResources = 'includeResourcesInExportWidgetTypes' | 'includeResourcesInExportDashboard';
|
||||
|
||||
// @dynamic
|
||||
@Injectable()
|
||||
export class ImportExportService {
|
||||
@ -148,16 +155,23 @@ export class ImportExportService {
|
||||
}
|
||||
|
||||
public exportDashboard(dashboardId: string) {
|
||||
this.dashboardService.exportDashboard(dashboardId).subscribe({
|
||||
next: (dashboard) => {
|
||||
let name = dashboard.title;
|
||||
name = name.toLowerCase().replace(/\W/g, '_');
|
||||
this.exportToPc(this.prepareDashboardExport(dashboard), name);
|
||||
},
|
||||
error: (e) => {
|
||||
this.handleExportError(e, 'dashboard.export-failed-error');
|
||||
}
|
||||
});
|
||||
this.getIncludeResourcesPreference('includeResourcesInExportDashboard').subscribe(includeResources => {
|
||||
this.openExportDialog('dashboard.export', 'dashboard.export-prompt', includeResources).subscribe(result => {
|
||||
if (result) {
|
||||
this.updateUserSettingsIncludeResourcesIfNeeded(includeResources, result.include, 'includeResourcesInExportDashboard');
|
||||
this.dashboardService.exportDashboard(dashboardId, result.include).subscribe({
|
||||
next: (dashboard) => {
|
||||
let name = dashboard.title;
|
||||
name = name.toLowerCase().replace(/\W/g, '_');
|
||||
this.exportToPc(this.prepareDashboardExport(dashboard), name);
|
||||
},
|
||||
error: (e) => {
|
||||
this.handleExportError(e, 'dashboard.export-failed-error');
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
public importDashboard(onEditMissingAliases: editMissingAliasesFunction): Observable<Dashboard> {
|
||||
@ -301,36 +315,54 @@ export class ImportExportService {
|
||||
}
|
||||
|
||||
public exportWidgetType(widgetTypeId: string) {
|
||||
this.widgetService.exportWidgetType(widgetTypeId).subscribe({
|
||||
next: (widgetTypeDetails) => {
|
||||
let name = widgetTypeDetails.name;
|
||||
name = name.toLowerCase().replace(/\W/g, '_');
|
||||
this.exportToPc(this.prepareExport(widgetTypeDetails), name);
|
||||
},
|
||||
error: (e) => {
|
||||
this.handleExportError(e, 'widget-type.export-failed-error');
|
||||
}
|
||||
this.getIncludeResourcesPreference('includeResourcesInExportWidgetTypes').subscribe(includeResources => {
|
||||
this.openExportDialog('widget.export', 'widget.export-prompt', includeResources).subscribe(result => {
|
||||
if (result) {
|
||||
this.updateUserSettingsIncludeResourcesIfNeeded(includeResources, result.include, 'includeResourcesInExportWidgetTypes');
|
||||
this.widgetService.exportWidgetType(widgetTypeId, result.include).subscribe({
|
||||
next: (widgetTypeDetails) => {
|
||||
let name = widgetTypeDetails.name;
|
||||
name = name.toLowerCase().replace(/\W/g, '_');
|
||||
this.exportToPc(this.prepareExport(widgetTypeDetails), name);
|
||||
},
|
||||
error: (e) => {
|
||||
this.handleExportError(e, 'widget-type.export-failed-error');
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
public exportWidgetTypes(widgetTypeIds: string[]): Observable<void> {
|
||||
const widgetTypesObservables: Array<Observable<WidgetTypeDetails>> = [];
|
||||
for (const id of widgetTypeIds) {
|
||||
widgetTypesObservables.push(this.widgetService.exportWidgetType(id));
|
||||
}
|
||||
return forkJoin(widgetTypesObservables).pipe(
|
||||
map((widgetTypes) =>
|
||||
Object.fromEntries(widgetTypes.map(wt=> {
|
||||
let name = wt.name;
|
||||
name = name.toLowerCase().replace(/\W/g, '_') + `.${JSON_TYPE.extension}`;
|
||||
const data = JSON.stringify(this.prepareExport(wt), null, 2);
|
||||
return [name, data];
|
||||
}))),
|
||||
mergeMap(widgetTypeFiles => this.exportJSZip(widgetTypeFiles, 'widget_types')),
|
||||
catchError(e => {
|
||||
this.handleExportError(e, 'widget-type.export-failed-error');
|
||||
throw e;
|
||||
})
|
||||
return this.getIncludeResourcesPreference('includeResourcesInExportWidgetTypes').pipe(
|
||||
mergeMap(includeResources =>
|
||||
this.openExportDialog('widget.export-widgets', 'widget.export-widgets-prompt', includeResources).pipe(
|
||||
mergeMap(result => {
|
||||
if (result) {
|
||||
this.updateUserSettingsIncludeResourcesIfNeeded(includeResources, result.include, 'includeResourcesInExportWidgetTypes');
|
||||
const widgetTypesObservables: Array<Observable<WidgetTypeDetails>> = [];
|
||||
for (const id of widgetTypeIds) {
|
||||
widgetTypesObservables.push(this.widgetService.exportWidgetType(id, result.include));
|
||||
}
|
||||
return forkJoin(widgetTypesObservables).pipe(
|
||||
map((widgetTypes) =>
|
||||
Object.fromEntries(widgetTypes.map(wt => {
|
||||
let name = wt.name;
|
||||
name = name.toLowerCase().replace(/\W/g, '_') + `.${JSON_TYPE.extension}`;
|
||||
const data = JSON.stringify(this.prepareExport(wt), null, 2);
|
||||
return [name, data];
|
||||
}))),
|
||||
mergeMap(widgetTypeFiles => this.exportJSZip(widgetTypeFiles, 'widget_types')),
|
||||
catchError(e => {
|
||||
this.handleExportError(e, 'widget-type.export-failed-error');
|
||||
throw e;
|
||||
})
|
||||
);
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -1187,4 +1219,27 @@ export class ImportExportService {
|
||||
return importedData;
|
||||
}
|
||||
|
||||
private getIncludeResourcesPreference(key: SupportEntityResources): Observable<boolean> {
|
||||
return this.store.pipe(
|
||||
select(selectUserSettingsProperty(key)),
|
||||
take(1)
|
||||
);
|
||||
}
|
||||
|
||||
private openExportDialog(title: string, prompt: string, includeResources: boolean) {
|
||||
return this.dialog.open<ExportResourceDialogComponent, ExportResourceDialogData, ExportResourceDialogDialogResult>(
|
||||
ExportResourceDialogComponent, {
|
||||
disableClose: true,
|
||||
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
|
||||
data: { title, prompt, include: includeResources }
|
||||
}
|
||||
).afterClosed();
|
||||
}
|
||||
|
||||
private updateUserSettingsIncludeResourcesIfNeeded(currentValue: boolean, newValue: boolean, key: SupportEntityResources) {
|
||||
if (currentValue !== newValue) {
|
||||
this.store.dispatch(new ActionPreferencesPutUserSettings({[key]: newValue }));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -68,6 +68,8 @@ export interface TbResourceInfo<D> extends Omit<BaseData<TbResourceId>, 'name' |
|
||||
fileName: string;
|
||||
public: boolean;
|
||||
publicResourceKey?: string;
|
||||
readonly link?: string;
|
||||
readonly publicLink?: string;
|
||||
descriptor?: D;
|
||||
}
|
||||
|
||||
@ -87,10 +89,7 @@ export interface ImageDescriptor {
|
||||
previewDescriptor: ImageDescriptor;
|
||||
}
|
||||
|
||||
export interface ImageResourceInfo extends TbResourceInfo<ImageDescriptor> {
|
||||
link?: string;
|
||||
publicLink?: string;
|
||||
}
|
||||
export type ImageResourceInfo = TbResourceInfo<ImageDescriptor>;
|
||||
|
||||
export interface ImageResource extends ImageResourceInfo {
|
||||
base64?: string;
|
||||
@ -108,6 +107,7 @@ export interface ImageExportData {
|
||||
}
|
||||
|
||||
export type ImageResourceType = 'tenant' | 'system';
|
||||
export type TBResourceScope = 'tenant' | 'system';
|
||||
|
||||
export type ImageReferences = {[entityType: string]: Array<BaseData<HasId> & HasTenantId>};
|
||||
|
||||
@ -141,15 +141,19 @@ export const imageResourceType = (imageInfo: ImageResourceInfo): ImageResourceTy
|
||||
(!imageInfo.tenantId || imageInfo.tenantId?.id === NULL_UUID) ? 'system' : 'tenant';
|
||||
|
||||
export const TB_IMAGE_PREFIX = 'tb-image;';
|
||||
export const TB_RESOURCE_PREFIX = 'tb-resource;';
|
||||
|
||||
export const IMAGES_URL_REGEXP = /\/api\/images\/(tenant|system)\/(.*)/;
|
||||
export const IMAGES_URL_PREFIX = '/api/images';
|
||||
|
||||
export const RESOURCES_URL_REGEXP = /\/api\/resource\/(js_module)\/(tenant|system)\/(.*)/;
|
||||
|
||||
export const PUBLIC_IMAGES_URL_PREFIX = '/api/images/public';
|
||||
|
||||
export const IMAGE_BASE64_URL_PREFIX = 'data:image/';
|
||||
|
||||
export const removeTbImagePrefix = (url: string): string => url ? url.replace(TB_IMAGE_PREFIX, '') : url;
|
||||
export const removeTbResourcePrefix = (url: string): string => url ? url.replace(TB_RESOURCE_PREFIX, '') : url;
|
||||
|
||||
export const removeTbImagePrefixFromUrls = (urls: string[]): string[] => urls ? urls.map(url => removeTbImagePrefix(url)) : [];
|
||||
|
||||
@ -162,9 +166,18 @@ export const prependTbImagePrefix = (url: string): string => {
|
||||
|
||||
export const prependTbImagePrefixToUrls = (urls: string[]): string[] => urls ? urls.map(url => prependTbImagePrefix(url)) : [];
|
||||
|
||||
export const prependTbResourcePrefix = (url: string): string => {
|
||||
if (url && !url.startsWith(TB_RESOURCE_PREFIX)) {
|
||||
url = TB_RESOURCE_PREFIX + url;
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
export const isImageResourceUrl = (url: string): boolean => url && IMAGES_URL_REGEXP.test(url);
|
||||
|
||||
export const isJSResourceUrl = (url: string): boolean => url && RESOURCES_URL_REGEXP.test(url);
|
||||
export const isJSResource = (url: string): boolean => url?.startsWith(TB_RESOURCE_PREFIX);
|
||||
|
||||
export const extractParamsFromImageResourceUrl = (url: string): {type: ImageResourceType; key: string} => {
|
||||
const res = url.match(IMAGES_URL_REGEXP);
|
||||
if (res?.length > 2) {
|
||||
@ -174,6 +187,15 @@ export const extractParamsFromImageResourceUrl = (url: string): {type: ImageReso
|
||||
}
|
||||
};
|
||||
|
||||
export const extractParamsFromJSResourceUrl = (url: string): {type: ResourceType; scope: TBResourceScope; key: string} => {
|
||||
const res = url.match(RESOURCES_URL_REGEXP);
|
||||
if (res?.length > 3) {
|
||||
return {type: (res[1]).toUpperCase() as ResourceType, scope: res[2] as TBResourceScope, key: res[3]};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const isBase64DataImageUrl = (url: string): boolean => url && url.startsWith(IMAGE_BASE64_URL_PREFIX);
|
||||
|
||||
export const NO_IMAGE_DATA_URI = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
|
||||
|
||||
@ -19,6 +19,8 @@ export interface UserSettings {
|
||||
notDisplayConnectivityAfterAddDevice?: boolean;
|
||||
notDisplayInstructionsAfterAddEdge?: boolean;
|
||||
includeBundleWidgetsInExport?: boolean;
|
||||
includeResourcesInExportWidgetTypes?: boolean;
|
||||
includeResourcesInExportDashboard?: boolean;
|
||||
}
|
||||
|
||||
export const initialUserSettings: UserSettings = {
|
||||
|
||||
@ -195,6 +195,7 @@ import { ImportExportService } from '@shared/import-export/import-export.service
|
||||
import { ImportDialogComponent } from '@shared/import-export/import-dialog.component';
|
||||
import { ImportDialogCsvComponent } from '@shared/import-export/import-dialog-csv.component';
|
||||
import { ExportWidgetsBundleDialogComponent } from '@shared/import-export/export-widgets-bundle-dialog.component';
|
||||
import { ExportResourceDialogComponent } from '@shared/import-export/export-resource-dialog.component';
|
||||
import { TableColumnsAssignmentComponent } from '@shared/import-export/table-columns-assignment.component';
|
||||
import { ScrollGridComponent } from '@shared/components/grid/scroll-grid.component';
|
||||
import { ImageGalleryComponent } from '@shared/components/image/image-gallery.component';
|
||||
@ -407,6 +408,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
|
||||
ImportDialogComponent,
|
||||
ImportDialogCsvComponent,
|
||||
ExportWidgetsBundleDialogComponent,
|
||||
ExportResourceDialogComponent,
|
||||
TableColumnsAssignmentComponent,
|
||||
ScrollGridComponent,
|
||||
ImageGalleryComponent,
|
||||
@ -664,6 +666,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
|
||||
ImportDialogComponent,
|
||||
ImportDialogCsvComponent,
|
||||
ExportWidgetsBundleDialogComponent,
|
||||
ExportResourceDialogComponent,
|
||||
TableColumnsAssignmentComponent,
|
||||
ScrollGridComponent,
|
||||
ImageGalleryComponent,
|
||||
|
||||
@ -1262,6 +1262,7 @@
|
||||
"import": "Import dashboard",
|
||||
"export": "Export dashboard",
|
||||
"export-failed-error": "Unable to export dashboard: {{error}}",
|
||||
"export-prompt": "Include dashboard resources in exported data",
|
||||
"create-new-dashboard": "Create new dashboard",
|
||||
"dashboard-file": "Dashboard file",
|
||||
"invalid-dashboard-file-error": "Unable to import dashboard: Invalid dashboard data structure.",
|
||||
@ -5217,7 +5218,9 @@
|
||||
"selected-widgets": "{ count, plural, =1 {1 widget} other {# widgets} } selected",
|
||||
"undo": "Undo widget changes",
|
||||
"export": "Export widget",
|
||||
"export-prompt": "Include widget resources in exported data",
|
||||
"export-widgets": "Export widgets",
|
||||
"export-widgets-prompt": "Include widgets resources in exported data",
|
||||
"import": "Import widget",
|
||||
"no-data": "No data to display on widget",
|
||||
"data-overflow": "Widget displays {{count}} out of {{total}} entities",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user