Merge pull request #4293 from vvlladd28/feature/resources/add-new-type

[3.3] UI: Added manager resources; added new entity type TB_RESOURCE
This commit is contained in:
Vladyslav 2021-03-24 11:33:49 +02:00 committed by GitHub
commit bfc03c4c69
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 635 additions and 21 deletions

View File

@ -0,0 +1,81 @@
///
/// Copyright © 2016-2021 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 { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { PageLink } from '@shared/models/page/page-link';
import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils';
import { Observable } from 'rxjs';
import { PageData } from '@shared/models/page/page-data';
import { Resource, ResourceInfo } from '@shared/models/resource.models';
import { map } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class ResourceService {
constructor(
private http: HttpClient
) {
}
public getResources(pageLink: PageLink, config?: RequestConfig): Observable<PageData<ResourceInfo>> {
return this.http.get<PageData<ResourceInfo>>(`/api/resource${pageLink.toQuery()}`,
defaultHttpOptionsFromConfig(config));
}
public getResource(resourceId: string, config?: RequestConfig): Observable<Resource> {
return this.http.get<Resource>(`/api/resource/${resourceId}`, defaultHttpOptionsFromConfig(config));
}
public downloadResource(resourceId: string): Observable<any> {
return this.http.get(`/api/resource/${resourceId}/download`, { responseType: 'arraybuffer', observe: 'response' }).pipe(
map((response) => {
const headers = response.headers;
const filename = headers.get('x-filename');
const contentType = headers.get('content-type');
const linkElement = document.createElement('a');
try {
const blob = new Blob([response.body], { type: contentType });
const url = URL.createObjectURL(blob);
linkElement.setAttribute('href', url);
linkElement.setAttribute('download', filename);
const clickEvent = new MouseEvent('click',
{
view: window,
bubbles: true,
cancelable: false
}
);
linkElement.dispatchEvent(clickEvent);
return null;
} catch (e) {
throw e;
}
})
);
}
public saveResource(resource: Resource, config?: RequestConfig): Observable<Resource> {
return this.http.post<Resource>('/api/resource', resource, defaultHttpOptionsFromConfig(config));
}
public deleteResource(resourceId: string, config?: RequestConfig) {
return this.http.delete(`/api/resource/${resourceId}`, defaultHttpOptionsFromConfig(config));
}
}

View File

@ -103,6 +103,13 @@ export class MenuService {
path: '/widgets-bundles',
icon: 'now_widgets'
},
{
id: guid(),
name: 'resource.resources-library',
type: 'link',
path: '/resources-library',
icon: 'folder'
},
{
id: guid(),
name: 'admin.system-settings',
@ -181,6 +188,16 @@ export class MenuService {
}
]
},
{
name: 'resource.management',
places: [
{
name: 'resource.resources-library',
icon: 'folder',
path: '/resources-library'
}
]
},
{
name: 'admin.system-settings',
places: [
@ -283,6 +300,13 @@ export class MenuService {
path: '/dashboards',
icon: 'dashboards'
},
{
id: guid(),
name: 'resource.resources-library',
type: 'link',
path: '/resources-library',
icon: 'folder'
},
{
id: guid(),
name: 'admin.home-settings',
@ -368,6 +392,16 @@ export class MenuService {
}
]
},
{
name: 'resource.management',
places: [
{
name: 'resource.resources-library',
icon: 'folder',
path: '/resources-library'
}
]
},
{
name: 'dashboard.management',
places: [

View File

@ -34,6 +34,7 @@ import { MODULES_MAP } from '@shared/public-api';
import { modulesMap } from '../../common/modules-map';
import { DeviceProfileModule } from './device-profile/device-profile.module';
import { ApiUsageModule } from '@home/pages/api-usage/api-usage.module';
import { ResourceModule } from '@home/pages/resource/resource.module';
@NgModule({
exports: [
@ -52,6 +53,7 @@ import { ApiUsageModule } from '@home/pages/api-usage/api-usage.module';
DashboardModule,
AuditLogModule,
ApiUsageModule,
ResourceModule,
UserModule
],
providers: [

View File

@ -0,0 +1,48 @@
///
/// Copyright © 2016-2021 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 { RouterModule, Routes } from '@angular/router';
import { EntitiesTableComponent } from '@home/components/entity/entities-table.component';
import { Authority } from '@shared/models/authority.enum';
import { NgModule } from '@angular/core';
import { ResourcesLibraryTableConfigResolver } from './resources-library-table-config.resolve';
const routes: Routes = [
{
path: 'resources-library',
component: EntitiesTableComponent,
data: {
auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN],
title: 'resource.resources-library',
breadcrumb: {
label: 'resource.resources-library',
icon: 'folder'
}
},
resolve: {
entitiesTableConfig: ResourcesLibraryTableConfigResolver
}
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
providers: [
ResourcesLibraryTableConfigResolver
]
})
export class ResourcesLibraryRoutingModule{ }

View File

@ -0,0 +1,33 @@
///
/// Copyright © 2016-2021 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 { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ResourcesLibraryRoutingModule } from '@home/pages/resource/resource-routing.module';
import { SharedModule } from '@shared/shared.module';
import { HomeComponentsModule } from '@home/components/home-components.module';
import { ResourcesLibraryComponent } from './resources-library.component';
@NgModule({
declarations: [ResourcesLibraryComponent],
imports: [
CommonModule,
SharedModule,
HomeComponentsModule,
ResourcesLibraryRoutingModule
]
})
export class ResourceModule { }

View File

@ -0,0 +1,115 @@
///
/// Copyright © 2016-2021 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 { Injectable } from '@angular/core';
import {
checkBoxCell,
DateEntityTableColumn,
EntityTableColumn,
EntityTableConfig
} from '@home/models/entity/entities-table-config.models';
import { Resolve } from '@angular/router';
import { Resource, ResourceInfo, ResourceTypeTranslationMap } from '@shared/models/resource.models';
import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models';
import { Direction } from '@shared/models/page/sort-order';
import { NULL_UUID } from '@shared/models/id/has-uuid';
import { DatePipe } from '@angular/common';
import { TranslateService } from '@ngx-translate/core';
import { ResourceService } from '@core/http/resource.service';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Authority } from '@shared/models/authority.enum';
import { ResourcesLibraryComponent } from '@home/pages/resource/resources-library.component';
import { Observable } from 'rxjs/internal/Observable';
import { PageData } from '@shared/models/page/page-data';
@Injectable()
export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableConfig<Resource>> {
private readonly config: EntityTableConfig<Resource> = new EntityTableConfig<Resource>();
private readonly resourceTypesTranslationMap = ResourceTypeTranslationMap;
constructor(private store: Store<AppState>,
private resourceService: ResourceService,
private translate: TranslateService,
private datePipe: DatePipe) {
this.config.entityType = EntityType.TB_RESOURCE;
this.config.entityComponent = ResourcesLibraryComponent;
this.config.entityTranslations = entityTypeTranslations.get(EntityType.TB_RESOURCE);
this.config.entityResources = entityTypeResources.get(EntityType.TB_RESOURCE);
this.config.defaultSortOrder = {property: 'title', direction: Direction.ASC};
this.config.entityTitle = (resource) => resource ?
resource.title : '';
this.config.columns.push(
new DateEntityTableColumn<ResourceInfo>('createdTime', 'common.created-time', this.datePipe, '150px'),
new EntityTableColumn<ResourceInfo>('title', 'widgets-bundle.title', '60%'),
new EntityTableColumn<ResourceInfo>('resourceType', 'resource.resource-type', '40%',
entity => this.resourceTypesTranslationMap.get(entity.resourceType)),
new EntityTableColumn<ResourceInfo>('tenantId', 'widgets-bundle.system', '60px',
entity => {
return checkBoxCell(entity.tenantId.id === NULL_UUID);
}),
);
this.config.cellActionDescriptors.push(
{
name: this.translate.instant('resource.export'),
icon: 'file_download',
isEnabled: () => true,
onAction: ($event, entity) => this.exportResource($event, entity)
}
);
this.config.deleteEntityTitle = resource => this.translate.instant('resource.delete-resource-title',
{ resourceTitle: resource.title });
this.config.deleteEntityContent = () => this.translate.instant('resource.delete-resource-text');
this.config.deleteEntitiesTitle = count => this.translate.instant('resource.delete-resources-title', {count});
this.config.deleteEntitiesContent = () => this.translate.instant('resource.delete-resources-text');
this.config.entitiesFetchFunction = pageLink => this.resourceService.getResources(pageLink) as Observable<PageData<Resource>>;
this.config.loadEntity = id => this.resourceService.getResource(id.id);
this.config.saveEntity = resource => this.resourceService.saveResource(resource);
this.config.deleteEntity = id => this.resourceService.deleteResource(id.id);
}
resolve(): EntityTableConfig<Resource> {
this.config.tableTitle = this.translate.instant('resource.resources-library');
const authUser = getCurrentAuthUser(this.store);
this.config.deleteEnabled = (resource) => this.isResourceEditable(resource, authUser.authority);
this.config.entitySelectionEnabled = (resource) => this.isResourceEditable(resource, authUser.authority);
this.config.detailsReadonly = (resource) => !this.isResourceEditable(resource, authUser.authority);
return this.config;
}
exportResource($event: Event, resource: ResourceInfo) {
if ($event) {
$event.stopPropagation();
}
this.resourceService.downloadResource(resource.id.id).subscribe();
}
private isResourceEditable(resource: Resource, authority: Authority): boolean {
if (authority === Authority.TENANT_ADMIN) {
return resource && resource.tenantId && resource.tenantId.id !== NULL_UUID;
} else {
return authority === Authority.SYS_ADMIN;
}
}
}

View File

@ -0,0 +1,56 @@
<!--
Copyright © 2016-2021 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.
-->
<div class="tb-details-buttons" fxLayout.xs="column">
<button mat-raised-button color="primary" fxFlex.xs
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'delete')"
[fxShow]="!hideDelete() && !isEdit">
{{'resource.delete' | translate }}
</button>
</div>
<div class="mat-padding" fxLayout="column">
<form [formGroup]="entityForm">
<fieldset [disabled]="(isLoading$ | async) || !isEdit">
<mat-form-field class="mat-block">
<mat-label translate>resource.resource-type</mat-label>
<mat-select formControlName="resourceType" required>
<mat-option *ngFor="let resourceType of resourceTypes" [value]="resourceType">
{{ resourceTypesTranslationMap.get(resourceType) }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="mat-block" *ngIf="entityForm.get('resourceType').value !== resourceType.LWM2M_MODEL || !isAdd">
<mat-label translate>resource.title</mat-label>
<input matInput formControlName="title" required [readonly]="entityForm.get('resourceType').value === resourceType.LWM2M_MODEL">
<mat-error *ngIf="entityForm.get('title').hasError('required')">
{{ 'resource.title-required' | translate }}
</mat-error>
</mat-form-field>
<tb-file-input
formControlName="data"
required
[convertToBase64]="true"
[allowedExtensions]="getAllowedExtensions()"
[accept]="getAcceptType()"
dropLabel="{{'resource.drop-file' | translate}}"
[existingFileName]="entityForm.get('fileName')?.value"
(fileNameChanged)="entityForm?.get('fileName').patchValue($event)">
</tb-file-input>
</fieldset>
</form>
</div>

View File

@ -0,0 +1,122 @@
///
/// Copyright © 2016-2021 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, OnDestroy, OnInit } from '@angular/core';
import { Subject } from 'rxjs';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { TranslateService } from '@ngx-translate/core';
import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { EntityComponent } from '@home/components/entity/entity.component';
import {
Resource,
ResourceType,
ResourceTypeExtension,
ResourceTypeMIMETypes,
ResourceTypeTranslationMap
} from '@shared/models/resource.models';
import { distinctUntilChanged, takeUntil } from 'rxjs/operators';
@Component({
selector: 'tb-resources-library',
templateUrl: './resources-library.component.html'
})
export class ResourcesLibraryComponent extends EntityComponent<Resource> implements OnInit, OnDestroy {
readonly resourceType = ResourceType;
readonly resourceTypes = Object.values(this.resourceType);
readonly resourceTypesTranslationMap = ResourceTypeTranslationMap;
private destroy$ = new Subject();
constructor(protected store: Store<AppState>,
protected translate: TranslateService,
@Inject('entity') protected entityValue: Resource,
@Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<Resource>,
public fb: FormBuilder) {
super(store, fb, entityValue, entitiesTableConfigValue);
}
ngOnInit() {
super.ngOnInit();
this.entityForm.get('resourceType').valueChanges.pipe(
distinctUntilChanged((oldValue, newValue) => [oldValue, newValue].includes(this.resourceType.LWM2M_MODEL)),
takeUntil(this.destroy$)
).subscribe((type) => {
if (type === this.resourceType.LWM2M_MODEL) {
this.entityForm.get('title').clearValidators();
} else {
this.entityForm.get('title').setValidators(Validators.required);
}
this.entityForm.get('title').updateValueAndValidity({emitEvent: false});
});
}
ngOnDestroy() {
super.ngOnDestroy();
this.destroy$.next();
this.destroy$.complete();
}
hideDelete() {
if (this.entitiesTableConfig) {
return !this.entitiesTableConfig.deleteEnabled(this.entity);
} else {
return false;
}
}
buildForm(entity: Resource): FormGroup {
return this.fb.group(
{
resourceType: [{value: entity?.resourceType ? entity.resourceType : ResourceType.LWM2M_MODEL,
disabled: this.isEdit }, [Validators.required]],
data: [entity ? entity.data : null, [Validators.required]],
fileName: [entity ? entity.fileName : null, [Validators.required]],
title: [entity ? entity.title : '', []]
}
);
}
updateForm(entity: Resource) {
this.entityForm.patchValue({resourceType: entity.resourceType});
if (this.isEdit) {
this.entityForm.get('resourceType').disable({emitEvent: false});
}
this.entityForm.patchValue({
data: entity.data,
fileName: entity.fileName,
title: entity.title
});
}
getAllowedExtensions() {
try {
return ResourceTypeExtension.get(this.entityForm.get('resourceType').value);
} catch (e) {
return '';
}
}
getAcceptType() {
try {
return ResourceTypeMIMETypes.get(this.entityForm.get('resourceType').value);
} catch (e) {
return '*/*';
}
}
}

View File

@ -101,6 +101,9 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
@Input()
existingFileName: string;
@Input()
convertToBase64 = false;
@Output()
fileNameChanged = new EventEmitter<string>();
@ -128,7 +131,7 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
const reader = new FileReader();
reader.onload = (loadEvent) => {
if (typeof reader.result === 'string') {
const fileContent = reader.result;
const fileContent = this.convertToBase64 ? window.btoa(reader.result) : reader.result;
if (fileContent && fileContent.length > 0) {
if (this.contentConvertFunction) {
this.fileContent = this.contentConvertFunction(fileContent);
@ -144,9 +147,13 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
}
}
};
if (this.convertToBase64) {
reader.readAsBinaryString(file.file);
} else {
reader.readAsText(file.file);
}
}
}
});
}
@ -159,8 +166,10 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
}
ngOnDestroy() {
if (this.autoUploadSubscription) {
this.autoUploadSubscription.unsubscribe();
}
}
registerOnChange(fn: any): void {
this.propagateChange = fn;

View File

@ -17,22 +17,6 @@
import { TenantId } from './id/tenant-id';
import { BaseData, HasId } from '@shared/models/base-data';
///
/// Copyright © 2016-2019 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.
///
export enum EntityType {
TENANT = 'TENANT',
TENANT_PROFILE = 'TENANT_PROFILE',
@ -48,7 +32,8 @@ export enum EntityType {
ENTITY_VIEW = 'ENTITY_VIEW',
WIDGETS_BUNDLE = 'WIDGETS_BUNDLE',
WIDGET_TYPE = 'WIDGET_TYPE',
API_USAGE_STATE = 'API_USAGE_STATE'
API_USAGE_STATE = 'API_USAGE_STATE',
TB_RESOURCE = 'TB_RESOURCE'
}
export enum AliasEntityType {
@ -282,7 +267,17 @@ export const entityTypeTranslations = new Map<EntityType | AliasEntityType, Enti
type: 'entity.type-current-user-owner',
list: 'entity.type-current-user-owner'
}
]
],
[
EntityType.TB_RESOURCE,
{
details: 'resource.resource-library-details',
add: 'resource.add',
noEntities: 'resource.no-resource-text',
search: 'resource.search',
selectedEntities: 'resource.selected-resources'
}
],
]
);
@ -353,6 +348,12 @@ export const entityTypeResources = new Map<EntityType, EntityTypeResource<BaseDa
{
helpLinkId: 'widgetsBundles'
}
],
[
EntityType.TB_RESOURCE,
{
helpLinkId: 'resources'
}
]
]
);

View File

@ -0,0 +1,26 @@
///
/// Copyright © 2016-2021 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 { EntityId } from '@shared/models/id/entity-id';
import { EntityType } from '@shared/models/entity-type.models';
export class TbResourceId implements EntityId {
entityType = EntityType.TB_RESOURCE;
id: string;
constructor(id: string) {
this.id = id;
}
}

View File

@ -39,6 +39,7 @@ export * from './material.models';
export * from './oauth2.models';
export * from './queue.models';
export * from './relation.models';
export * from './resource.models';
export * from './rule-chain.models';
export * from './rule-node.models';
export * from './settings.models';

View File

@ -0,0 +1,61 @@
///
/// Copyright © 2016-2021 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 { BaseData } from '@shared/models/base-data';
import { TenantId } from '@shared/models/id/tenant-id';
import { TbResourceId } from '@shared/models/id/tb-resource-id';
export enum ResourceType {
LWM2M_MODEL = 'LWM2M_MODEL',
PKCS_12 = 'PKCS_12',
JKS = 'JKS'
}
export const ResourceTypeMIMETypes = new Map<ResourceType, string>(
[
[ResourceType.LWM2M_MODEL, 'application/xml,text/xml'],
[ResourceType.PKCS_12, 'application/x-pkcs12'],
[ResourceType.JKS, 'application/x-java-keystore']
]
);
export const ResourceTypeExtension = new Map<ResourceType, string>(
[
[ResourceType.LWM2M_MODEL, 'xml'],
[ResourceType.PKCS_12, 'p12,pfx'],
[ResourceType.JKS, 'jks']
]
);
export const ResourceTypeTranslationMap = new Map<ResourceType, string>(
[
[ResourceType.LWM2M_MODEL, 'LWM2M model'],
[ResourceType.PKCS_12, 'PKCS #12'],
[ResourceType.JKS, 'JKS']
]
);
export interface ResourceInfo extends BaseData<TbResourceId> {
tenantId?: TenantId;
resourceKey?: string;
title?: string;
resourceType: ResourceType;
}
export interface Resource extends ResourceInfo {
data: string;
fileName: string;
}

View File

@ -1963,6 +1963,31 @@
"invalid-additional-info": "Unable to parse additional info json.",
"no-relations-text": "No relations found"
},
"resource": {
"add": "Add Resource",
"delete": "Delete resource",
"delete-resource-text": "Be careful, after the confirmation the resource will become unrecoverable.",
"delete-resource-title": "Are you sure you want to delete the resource '{{resourceTitle}}'?",
"delete-resources-action-title": "Delete { count, plural, 1 {1 resource} other {# resources} }",
"delete-resources-text": "Be careful, after the confirmation all selected resources will be removed.",
"delete-resources-title": "Are you sure you want to delete { count, plural, 1 {1 resource} other {# resources} }?",
"drop-file": "Drop a resource file or click to select a file to upload.",
"empty": "Resource is empty",
"export": "Export resource",
"management": "Resource management",
"no-resource-matching": "No resource matching '{{widgetsBundle}}' were found.",
"no-resource-text": "No resources found",
"open-widgets-bundle": "Open widgets bundle",
"resource": "Resource",
"resource-library-details": "Resource library details",
"resource-type": "Resource type",
"resources-library": "Resources library",
"search": "Search resources",
"selected-resources": "{ count, plural, 1 {1 resource} other {# resources} } selected",
"system": "System",
"title": "Title",
"title-required": "Title is required."
},
"rulechain": {
"rulechain": "Rule chain",
"rulechains": "Rule chains",