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));
|
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> {
|
||||||
|
|||||||
@ -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`);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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()}`
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -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);
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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 { 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 }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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==';
|
||||||
|
|||||||
@ -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 = {
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user