UI: Implements support tb-resources and added support in extension

This commit is contained in:
Vladyslav_Prykhodko 2024-11-11 18:46:53 +02:00
parent a4433e6134
commit cfce9b8950
14 changed files with 309 additions and 69 deletions

View File

@ -71,8 +71,12 @@ export class DashboardService {
return this.http.get<Dashboard>(`/api/dashboard/${dashboardId}`, defaultHttpOptionsFromConfig(config)); return this.http.get<Dashboard>(`/api/dashboard/${dashboardId}`, defaultHttpOptionsFromConfig(config));
} }
public exportDashboard(dashboardId: string, config?: RequestConfig): Observable<Dashboard> { public exportDashboard(dashboardId: string, includeResources = true, config?: RequestConfig): Observable<Dashboard> {
return this.http.get<Dashboard>(`/api/dashboard/${dashboardId}?includeResources=true`, defaultHttpOptionsFromConfig(config)); 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> { public getDashboardInfo(dashboardId: string, config?: RequestConfig): Observable<DashboardInfo> {

View File

@ -20,7 +20,7 @@ import { PageLink } from '@shared/models/page/page-link';
import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils';
import { forkJoin, Observable, of } from 'rxjs'; import { forkJoin, Observable, of } from 'rxjs';
import { PageData } from '@shared/models/page/page-data'; 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 { catchError, mergeMap } from 'rxjs/operators';
import { isNotEmptyStr } from '@core/utils'; import { isNotEmptyStr } from '@core/utils';
import { ResourcesService } from '@core/services/resources.service'; import { ResourcesService } from '@core/services/resources.service';
@ -52,10 +52,14 @@ export class ResourceService {
return this.http.get<Resource>(`/api/resource/${resourceId}`, defaultHttpOptionsFromConfig(config)); 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)); 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> { public downloadResource(resourceId: string): Observable<any> {
return this.resourcesService.downloadResource(`/api/resource/${resourceId}/download`); return this.resourcesService.downloadResource(`/api/resource/${resourceId}/download`);
} }

View File

@ -31,7 +31,6 @@ import {
WidgetTypeInfo, WidgetTypeInfo,
widgetTypesData widgetTypesData
} from '@shared/models/widget.models'; } from '@shared/models/widget.models';
import { TranslateService } from '@ngx-translate/core';
import { toWidgetInfo, toWidgetTypeDetails, WidgetInfo } from '@app/modules/home/models/widget-component.models'; import { toWidgetInfo, toWidgetTypeDetails, WidgetInfo } from '@app/modules/home/models/widget-component.models';
import { filter, map, mergeMap, tap } from 'rxjs/operators'; import { filter, map, mergeMap, tap } from 'rxjs/operators';
import { WidgetTypeId } from '@shared/models/id/widget-type-id'; import { WidgetTypeId } from '@shared/models/id/widget-type-id';
@ -53,7 +52,6 @@ export class WidgetService {
constructor( constructor(
private http: HttpClient, private http: HttpClient,
private translate: TranslateService,
private router: Router private router: Router
) { ) {
this.router.events.pipe(filter(event => event instanceof ActivationEnd)).subscribe( this.router.events.pipe(filter(event => event instanceof ActivationEnd)).subscribe(
@ -143,9 +141,13 @@ export class WidgetService {
} }
public exportBundleWidgetTypesDetails(widgetsBundleId: string, public exportBundleWidgetTypesDetails(widgetsBundleId: string,
includeResources = true,
config?: RequestConfig): Observable<Array<WidgetTypeDetails>> { config?: RequestConfig): Observable<Array<WidgetTypeDetails>> {
return this.http.get<Array<WidgetTypeDetails>>(`/api/widgetTypesDetails?widgetsBundleId=${widgetsBundleId}&includeResources=true`, let url = `/api/widgetTypesDetails?widgetsBundleId=${widgetsBundleId}`
defaultHttpOptionsFromConfig(config)); if (includeResources) {
url += '&includeResources=true';
}
return this.http.get<Array<WidgetTypeDetails>>(url, defaultHttpOptionsFromConfig(config));
} }
public getBundleWidgetTypeFqns(widgetsBundleId: string, public getBundleWidgetTypeFqns(widgetsBundleId: string,
@ -211,9 +213,13 @@ export class WidgetService {
} }
public exportWidgetType(widgetTypeId: string, public exportWidgetType(widgetTypeId: string,
includeResources = true,
config?: RequestConfig): Observable<WidgetTypeDetails> { config?: RequestConfig): Observable<WidgetTypeDetails> {
return this.http.get<WidgetTypeDetails>(`/api/widgetType/${widgetTypeId}?includeResources=true`, let url = `/api/widgetType/${widgetTypeId}`;
defaultHttpOptionsFromConfig(config)); if (includeResources) {
url += '?includeResources=true';
}
return this.http.get<WidgetTypeDetails>(url, defaultHttpOptionsFromConfig(config));
} }
public getWidgetTypeInfoById(widgetTypeId: string, public getWidgetTypeInfoById(widgetTypeId: string,

View File

@ -39,6 +39,7 @@ import { AppState } from '@core/core.state';
import { map, tap } from 'rxjs/operators'; import { map, tap } from 'rxjs/operators';
import { RequestConfig } from '@core/http/http-utils'; import { RequestConfig } from '@core/http/http-utils';
import { getFlexLayoutModule } from '@app/shared/legacy/flex-layout.models'; import { getFlexLayoutModule } from '@app/shared/legacy/flex-layout.models';
import { isJSResource, removeTbResourcePrefix } from '@shared/models/resource.models';
export interface ModuleInfo { export interface ModuleInfo {
module: ɵNgModuleDef<any>; module: ɵNgModuleDef<any>;
@ -377,11 +378,11 @@ export class ResourcesService {
if (isObject(resourceId)) { if (isObject(resourceId)) {
return `/api/resource/js/${(resourceId as TbResourceId).id}/download`; return `/api/resource/js/${(resourceId as TbResourceId).id}/download`;
} }
return resourceId as string; return removeTbResourcePrefix(resourceId as string);
} }
private getMetaInfo(resourceId: string | TbResourceId): object { private getMetaInfo(resourceId: string | TbResourceId): object {
if (isObject(resourceId)) { if (isObject(resourceId) || (typeof resourceId === 'string' && isJSResource(resourceId))) {
return { return {
additionalHeaders: { additionalHeaders: {
'X-Authorization': `Bearer ${AuthService.getJwtToken()}` 'X-Authorization': `Bearer ${AuthService.getJwtToken()}`

View File

@ -338,6 +338,7 @@ import { TimezonePanelComponent } from '@shared/components/time/timezone-panel.c
import { DatapointsLimitComponent } from '@shared/components/time/datapoints-limit.component'; import { DatapointsLimitComponent } from '@shared/components/time/datapoints-limit.component';
import { Observable, map, of } from 'rxjs'; import { Observable, map, of } from 'rxjs';
import { getFlexLayout } from '@shared/legacy/flex-layout.models'; import { getFlexLayout } from '@shared/legacy/flex-layout.models';
import { isJSResourceUrl } from '@shared/public-api';
class ModulesMap implements IModulesMap { class ModulesMap implements IModulesMap {
@ -693,7 +694,7 @@ class ModulesMap implements IModulesMap {
for (const moduleId of Object.keys(this.modulesMap)) { for (const moduleId of Object.keys(this.modulesMap)) {
System.set('app:' + moduleId, this.modulesMap[moduleId]); 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}) => { System.constructor.prototype.fetch = (url: string, options: RequestInit & {meta?: any}) => {
if (options?.meta?.additionalHeaders) { if (options?.meta?.additionalHeaders) {
options.headers = { ...options.headers, ...options.meta.additionalHeaders }; options.headers = { ...options.headers, ...options.meta.additionalHeaders };

View File

@ -84,7 +84,7 @@ export class ResourcesLibraryTableConfigResolver {
this.config.deleteEntitiesContent = () => this.translate.instant('resource.delete-resources-text'); this.config.deleteEntitiesContent = () => this.translate.instant('resource.delete-resources-text');
this.config.entitiesFetchFunction = pageLink => this.resourceService.getResources(pageLink, this.config.componentsData.resourceType); 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.saveEntity = resource => this.saveResource(resource);
this.config.deleteEntity = id => this.resourceService.deleteResource(id.id); this.config.deleteEntity = id => this.resourceService.deleteResource(id.id);

View File

@ -20,7 +20,14 @@ import { coerceBoolean } from '@shared/decorators/coercion';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { catchError, debounceTime, map, share, switchMap, tap } from 'rxjs/operators'; import { catchError, debounceTime, map, share, switchMap, tap } from 'rxjs/operators';
import { isDefinedAndNotNull, isEmptyStr, isEqual, isObject } from '@core/utils'; 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 { TbResourceId } from '@shared/models/id/tb-resource-id';
import { ResourceService } from '@core/http/resource.service'; import { ResourceService } from '@core/http/resource.service';
import { PageLink } from '@shared/models/page/page-link'; import { PageLink } from '@shared/models/page/page-link';
@ -64,7 +71,7 @@ export class ResourceAutocompleteComponent implements ControlValueAccessor, OnIn
allowAutocomplete = false; allowAutocomplete = false;
resourceFormGroup = this.fb.group({ resourceFormGroup = this.fb.group({
resource: [null] resource: this.fb.control<string|ResourceInfo>(null)
}); });
filteredResources$: Observable<Array<ResourceInfo>>; filteredResources$: Observable<Array<ResourceInfo>>;
@ -73,10 +80,10 @@ export class ResourceAutocompleteComponent implements ControlValueAccessor, OnIn
@ViewChild('resourceInput', {static: true}) resourceInput: ElementRef; @ViewChild('resourceInput', {static: true}) resourceInput: ElementRef;
private modelValue: string | TbResourceId; private modelValue: string;
private dirty = false; private dirty = false;
private propagateChange = (v: any) => { }; private propagateChange: (value: any) => void = () => {};
constructor(private fb: FormBuilder, constructor(private fb: FormBuilder,
private resourceService: ResourceService) { private resourceService: ResourceService) {
@ -91,13 +98,13 @@ export class ResourceAutocompleteComponent implements ControlValueAccessor, OnIn
.pipe( .pipe(
debounceTime(150), debounceTime(150),
tap(value => { tap(value => {
let modelValue; let modelValue: string;
if (isObject(value)) { if (isObject(value)) {
modelValue = value.id; modelValue = prependTbResourcePrefix((value as ResourceInfo).link);
} else if (isEmptyStr(value)) { } else if (isEmptyStr(value)) {
modelValue = null; modelValue = null;
} else { } else {
modelValue = value; modelValue = value as string;
} }
this.updateView(modelValue); this.updateView(modelValue);
if (value === null) { if (value === null) {
@ -114,7 +121,7 @@ export class ResourceAutocompleteComponent implements ControlValueAccessor, OnIn
this.propagateChange = fn; this.propagateChange = fn;
} }
registerOnTouched(fn: any): void { registerOnTouched(_fn: any): void {
} }
setDisabledState(isDisabled: boolean) { setDisabledState(isDisabled: boolean) {
@ -130,9 +137,9 @@ export class ResourceAutocompleteComponent implements ControlValueAccessor, OnIn
if (isDefinedAndNotNull(value)) { if (isDefinedAndNotNull(value)) {
this.searchText = ''; this.searchText = '';
if (isObject(value) && typeof value !== 'string' && (value as TbResourceId).id) { 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 => { next: resource => {
this.modelValue = resource.id; this.modelValue = prependTbResourcePrefix(resource.link);
this.resourceFormGroup.get('resource').patchValue(resource, {emitEvent: false}); this.resourceFormGroup.get('resource').patchValue(resource, {emitEvent: false});
}, },
error: () => { error: () => {
@ -140,9 +147,22 @@ export class ResourceAutocompleteComponent implements ControlValueAccessor, OnIn
this.resourceFormGroup.get('resource').patchValue(''); this.resourceFormGroup.get('resource').patchValue('');
} }
}); });
} else { } 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.modelValue = value;
this.resourceFormGroup.get('resource').patchValue(value, {emitEvent: false}); this.resourceFormGroup.get('resource').patchValue(resource, {emitEvent: false});
},
error: () => {
this.modelValue = '';
this.resourceFormGroup.get('resource').patchValue('');
}
})
} else {
this.modelValue = value as string;
this.resourceFormGroup.get('resource').patchValue(value as string, {emitEvent: false});
} }
this.dirty = true; 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)) { if (!isEqual(this.modelValue, value)) {
this.modelValue = value; this.modelValue = value;
this.propagateChange(this.modelValue); this.propagateChange(this.modelValue);

View File

@ -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>

View File

@ -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
});
}
}

View File

@ -87,10 +87,17 @@ import { ActionPreferencesPutUserSettings } from '@core/auth/auth.actions';
import { ExportableEntity } from '@shared/models/base-data'; import { ExportableEntity } from '@shared/models/base-data';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { Customer } from '@shared/models/customer.model'; 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, export type editMissingAliasesFunction = (widgets: Array<Widget>, isSingleWidget: boolean,
customTitle: string, missingEntityAliases: EntityAliases) => Observable<EntityAliases>; customTitle: string, missingEntityAliases: EntityAliases) => Observable<EntityAliases>;
type SupportEntityResources = 'includeResourcesInExportWidgetTypes' | 'includeResourcesInExportDashboard';
// @dynamic // @dynamic
@Injectable() @Injectable()
export class ImportExportService { export class ImportExportService {
@ -148,7 +155,11 @@ export class ImportExportService {
} }
public exportDashboard(dashboardId: string) { public exportDashboard(dashboardId: string) {
this.dashboardService.exportDashboard(dashboardId).subscribe({ 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) => { next: (dashboard) => {
let name = dashboard.title; let name = dashboard.title;
name = name.toLowerCase().replace(/\W/g, '_'); name = name.toLowerCase().replace(/\W/g, '_');
@ -159,6 +170,9 @@ export class ImportExportService {
} }
}); });
} }
})
})
}
public importDashboard(onEditMissingAliases: editMissingAliasesFunction): Observable<Dashboard> { public importDashboard(onEditMissingAliases: editMissingAliasesFunction): Observable<Dashboard> {
return this.openImportDialog('dashboard.import', 'dashboard.dashboard-file').pipe( return this.openImportDialog('dashboard.import', 'dashboard.dashboard-file').pipe(
@ -301,7 +315,11 @@ export class ImportExportService {
} }
public exportWidgetType(widgetTypeId: string) { public exportWidgetType(widgetTypeId: string) {
this.widgetService.exportWidgetType(widgetTypeId).subscribe({ 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) => { next: (widgetTypeDetails) => {
let name = widgetTypeDetails.name; let name = widgetTypeDetails.name;
name = name.toLowerCase().replace(/\W/g, '_'); name = name.toLowerCase().replace(/\W/g, '_');
@ -312,11 +330,20 @@ export class ImportExportService {
} }
}); });
} }
})
});
}
public exportWidgetTypes(widgetTypeIds: string[]): Observable<void> { public exportWidgetTypes(widgetTypeIds: string[]): Observable<void> {
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>> = []; const widgetTypesObservables: Array<Observable<WidgetTypeDetails>> = [];
for (const id of widgetTypeIds) { for (const id of widgetTypeIds) {
widgetTypesObservables.push(this.widgetService.exportWidgetType(id)); widgetTypesObservables.push(this.widgetService.exportWidgetType(id, result.include));
} }
return forkJoin(widgetTypesObservables).pipe( return forkJoin(widgetTypesObservables).pipe(
map((widgetTypes) => map((widgetTypes) =>
@ -333,6 +360,11 @@ export class ImportExportService {
}) })
); );
} }
})
)
)
);
}
public importWidgetType(): Observable<WidgetTypeDetails> { public importWidgetType(): Observable<WidgetTypeDetails> {
return this.openImportDialog('widget.import', 'widget-type.widget-file').pipe( return this.openImportDialog('widget.import', 'widget-type.widget-file').pipe(
@ -1187,4 +1219,27 @@ export class ImportExportService {
return importedData; 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 }));
}
}
} }

View File

@ -68,6 +68,8 @@ export interface TbResourceInfo<D> extends Omit<BaseData<TbResourceId>, 'name' |
fileName: string; fileName: string;
public: boolean; public: boolean;
publicResourceKey?: string; publicResourceKey?: string;
readonly link?: string;
readonly publicLink?: string;
descriptor?: D; descriptor?: D;
} }
@ -87,10 +89,7 @@ export interface ImageDescriptor {
previewDescriptor: ImageDescriptor; previewDescriptor: ImageDescriptor;
} }
export interface ImageResourceInfo extends TbResourceInfo<ImageDescriptor> { export type ImageResourceInfo = TbResourceInfo<ImageDescriptor>;
link?: string;
publicLink?: string;
}
export interface ImageResource extends ImageResourceInfo { export interface ImageResource extends ImageResourceInfo {
base64?: string; base64?: string;
@ -108,6 +107,7 @@ export interface ImageExportData {
} }
export type ImageResourceType = 'tenant' | 'system'; export type ImageResourceType = 'tenant' | 'system';
export type TBResourceScope = 'tenant' | 'system';
export type ImageReferences = {[entityType: string]: Array<BaseData<HasId> & HasTenantId>}; 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'; (!imageInfo.tenantId || imageInfo.tenantId?.id === NULL_UUID) ? 'system' : 'tenant';
export const TB_IMAGE_PREFIX = 'tb-image;'; 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_REGEXP = /\/api\/images\/(tenant|system)\/(.*)/;
export const IMAGES_URL_PREFIX = '/api/images'; 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 PUBLIC_IMAGES_URL_PREFIX = '/api/images/public';
export const IMAGE_BASE64_URL_PREFIX = 'data:image/'; export const IMAGE_BASE64_URL_PREFIX = 'data:image/';
export const removeTbImagePrefix = (url: string): string => url ? url.replace(TB_IMAGE_PREFIX, '') : url; 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)) : []; 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 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 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} => { export const extractParamsFromImageResourceUrl = (url: string): {type: ImageResourceType; key: string} => {
const res = url.match(IMAGES_URL_REGEXP); const res = url.match(IMAGES_URL_REGEXP);
if (res?.length > 2) { 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 isBase64DataImageUrl = (url: string): boolean => url && url.startsWith(IMAGE_BASE64_URL_PREFIX);
export const NO_IMAGE_DATA_URI = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='; export const NO_IMAGE_DATA_URI = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';

View File

@ -19,6 +19,8 @@ export interface UserSettings {
notDisplayConnectivityAfterAddDevice?: boolean; notDisplayConnectivityAfterAddDevice?: boolean;
notDisplayInstructionsAfterAddEdge?: boolean; notDisplayInstructionsAfterAddEdge?: boolean;
includeBundleWidgetsInExport?: boolean; includeBundleWidgetsInExport?: boolean;
includeResourcesInExportWidgetTypes?: boolean;
includeResourcesInExportDashboard?: boolean;
} }
export const initialUserSettings: UserSettings = { export const initialUserSettings: UserSettings = {

View File

@ -195,6 +195,7 @@ import { ImportExportService } from '@shared/import-export/import-export.service
import { ImportDialogComponent } from '@shared/import-export/import-dialog.component'; import { ImportDialogComponent } from '@shared/import-export/import-dialog.component';
import { ImportDialogCsvComponent } from '@shared/import-export/import-dialog-csv.component'; import { ImportDialogCsvComponent } from '@shared/import-export/import-dialog-csv.component';
import { ExportWidgetsBundleDialogComponent } from '@shared/import-export/export-widgets-bundle-dialog.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 { TableColumnsAssignmentComponent } from '@shared/import-export/table-columns-assignment.component';
import { ScrollGridComponent } from '@shared/components/grid/scroll-grid.component'; import { ScrollGridComponent } from '@shared/components/grid/scroll-grid.component';
import { ImageGalleryComponent } from '@shared/components/image/image-gallery.component'; import { ImageGalleryComponent } from '@shared/components/image/image-gallery.component';
@ -407,6 +408,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
ImportDialogComponent, ImportDialogComponent,
ImportDialogCsvComponent, ImportDialogCsvComponent,
ExportWidgetsBundleDialogComponent, ExportWidgetsBundleDialogComponent,
ExportResourceDialogComponent,
TableColumnsAssignmentComponent, TableColumnsAssignmentComponent,
ScrollGridComponent, ScrollGridComponent,
ImageGalleryComponent, ImageGalleryComponent,
@ -664,6 +666,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
ImportDialogComponent, ImportDialogComponent,
ImportDialogCsvComponent, ImportDialogCsvComponent,
ExportWidgetsBundleDialogComponent, ExportWidgetsBundleDialogComponent,
ExportResourceDialogComponent,
TableColumnsAssignmentComponent, TableColumnsAssignmentComponent,
ScrollGridComponent, ScrollGridComponent,
ImageGalleryComponent, ImageGalleryComponent,

View File

@ -1262,6 +1262,7 @@
"import": "Import dashboard", "import": "Import dashboard",
"export": "Export dashboard", "export": "Export dashboard",
"export-failed-error": "Unable to export dashboard: {{error}}", "export-failed-error": "Unable to export dashboard: {{error}}",
"export-prompt": "Include dashboard resources in exported data",
"create-new-dashboard": "Create new dashboard", "create-new-dashboard": "Create new dashboard",
"dashboard-file": "Dashboard file", "dashboard-file": "Dashboard file",
"invalid-dashboard-file-error": "Unable to import dashboard: Invalid dashboard data structure.", "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", "selected-widgets": "{ count, plural, =1 {1 widget} other {# widgets} } selected",
"undo": "Undo widget changes", "undo": "Undo widget changes",
"export": "Export widget", "export": "Export widget",
"export-prompt": "Include widget resources in exported data",
"export-widgets": "Export widgets", "export-widgets": "Export widgets",
"export-widgets-prompt": "Include widgets resources in exported data",
"import": "Import widget", "import": "Import widget",
"no-data": "No data to display on widget", "no-data": "No data to display on widget",
"data-overflow": "Widget displays {{count}} out of {{total}} entities", "data-overflow": "Widget displays {{count}} out of {{total}} entities",