Commit bfc03c4c69c23360607fcd8e8e9cc6f646f6dbc6

Authored by Vladyslav
Committed by GitHub
2 parents 0d62b289 b033b517

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

[3.3] UI: Added manager resources; added new entity type TB_RESOURCE
  1 +///
  2 +/// Copyright © 2016-2021 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Injectable } from '@angular/core';
  18 +import { HttpClient } from '@angular/common/http';
  19 +import { PageLink } from '@shared/models/page/page-link';
  20 +import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils';
  21 +import { Observable } from 'rxjs';
  22 +import { PageData } from '@shared/models/page/page-data';
  23 +import { Resource, ResourceInfo } from '@shared/models/resource.models';
  24 +import { map } from 'rxjs/operators';
  25 +
  26 +@Injectable({
  27 + providedIn: 'root'
  28 +})
  29 +export class ResourceService {
  30 + constructor(
  31 + private http: HttpClient
  32 + ) {
  33 +
  34 + }
  35 +
  36 + public getResources(pageLink: PageLink, config?: RequestConfig): Observable<PageData<ResourceInfo>> {
  37 + return this.http.get<PageData<ResourceInfo>>(`/api/resource${pageLink.toQuery()}`,
  38 + defaultHttpOptionsFromConfig(config));
  39 + }
  40 +
  41 + public getResource(resourceId: string, config?: RequestConfig): Observable<Resource> {
  42 + return this.http.get<Resource>(`/api/resource/${resourceId}`, defaultHttpOptionsFromConfig(config));
  43 + }
  44 +
  45 + public downloadResource(resourceId: string): Observable<any> {
  46 + return this.http.get(`/api/resource/${resourceId}/download`, { responseType: 'arraybuffer', observe: 'response' }).pipe(
  47 + map((response) => {
  48 + const headers = response.headers;
  49 + const filename = headers.get('x-filename');
  50 + const contentType = headers.get('content-type');
  51 + const linkElement = document.createElement('a');
  52 + try {
  53 + const blob = new Blob([response.body], { type: contentType });
  54 + const url = URL.createObjectURL(blob);
  55 + linkElement.setAttribute('href', url);
  56 + linkElement.setAttribute('download', filename);
  57 + const clickEvent = new MouseEvent('click',
  58 + {
  59 + view: window,
  60 + bubbles: true,
  61 + cancelable: false
  62 + }
  63 + );
  64 + linkElement.dispatchEvent(clickEvent);
  65 + return null;
  66 + } catch (e) {
  67 + throw e;
  68 + }
  69 + })
  70 + );
  71 + }
  72 +
  73 + public saveResource(resource: Resource, config?: RequestConfig): Observable<Resource> {
  74 + return this.http.post<Resource>('/api/resource', resource, defaultHttpOptionsFromConfig(config));
  75 + }
  76 +
  77 + public deleteResource(resourceId: string, config?: RequestConfig) {
  78 + return this.http.delete(`/api/resource/${resourceId}`, defaultHttpOptionsFromConfig(config));
  79 + }
  80 +
  81 +}
... ...
... ... @@ -105,6 +105,13 @@ export class MenuService {
105 105 },
106 106 {
107 107 id: guid(),
  108 + name: 'resource.resources-library',
  109 + type: 'link',
  110 + path: '/resources-library',
  111 + icon: 'folder'
  112 + },
  113 + {
  114 + id: guid(),
108 115 name: 'admin.system-settings',
109 116 type: 'toggle',
110 117 path: '/settings',
... ... @@ -182,6 +189,16 @@ export class MenuService {
182 189 ]
183 190 },
184 191 {
  192 + name: 'resource.management',
  193 + places: [
  194 + {
  195 + name: 'resource.resources-library',
  196 + icon: 'folder',
  197 + path: '/resources-library'
  198 + }
  199 + ]
  200 + },
  201 + {
185 202 name: 'admin.system-settings',
186 203 places: [
187 204 {
... ... @@ -285,6 +302,13 @@ export class MenuService {
285 302 },
286 303 {
287 304 id: guid(),
  305 + name: 'resource.resources-library',
  306 + type: 'link',
  307 + path: '/resources-library',
  308 + icon: 'folder'
  309 + },
  310 + {
  311 + id: guid(),
288 312 name: 'admin.home-settings',
289 313 type: 'link',
290 314 path: '/settings/home',
... ... @@ -369,6 +393,16 @@ export class MenuService {
369 393 ]
370 394 },
371 395 {
  396 + name: 'resource.management',
  397 + places: [
  398 + {
  399 + name: 'resource.resources-library',
  400 + icon: 'folder',
  401 + path: '/resources-library'
  402 + }
  403 + ]
  404 + },
  405 + {
372 406 name: 'dashboard.management',
373 407 places: [
374 408 {
... ...
... ... @@ -34,6 +34,7 @@ import { MODULES_MAP } from '@shared/public-api';
34 34 import { modulesMap } from '../../common/modules-map';
35 35 import { DeviceProfileModule } from './device-profile/device-profile.module';
36 36 import { ApiUsageModule } from '@home/pages/api-usage/api-usage.module';
  37 +import { ResourceModule } from '@home/pages/resource/resource.module';
37 38
38 39 @NgModule({
39 40 exports: [
... ... @@ -52,6 +53,7 @@ import { ApiUsageModule } from '@home/pages/api-usage/api-usage.module';
52 53 DashboardModule,
53 54 AuditLogModule,
54 55 ApiUsageModule,
  56 + ResourceModule,
55 57 UserModule
56 58 ],
57 59 providers: [
... ...
  1 +///
  2 +/// Copyright © 2016-2021 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { RouterModule, Routes } from '@angular/router';
  18 +import { EntitiesTableComponent } from '@home/components/entity/entities-table.component';
  19 +import { Authority } from '@shared/models/authority.enum';
  20 +import { NgModule } from '@angular/core';
  21 +import { ResourcesLibraryTableConfigResolver } from './resources-library-table-config.resolve';
  22 +
  23 +const routes: Routes = [
  24 + {
  25 + path: 'resources-library',
  26 + component: EntitiesTableComponent,
  27 + data: {
  28 + auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN],
  29 + title: 'resource.resources-library',
  30 + breadcrumb: {
  31 + label: 'resource.resources-library',
  32 + icon: 'folder'
  33 + }
  34 + },
  35 + resolve: {
  36 + entitiesTableConfig: ResourcesLibraryTableConfigResolver
  37 + }
  38 + }
  39 +];
  40 +
  41 +@NgModule({
  42 + imports: [RouterModule.forChild(routes)],
  43 + exports: [RouterModule],
  44 + providers: [
  45 + ResourcesLibraryTableConfigResolver
  46 + ]
  47 +})
  48 +export class ResourcesLibraryRoutingModule{ }
... ...
  1 +///
  2 +/// Copyright © 2016-2021 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { NgModule } from '@angular/core';
  18 +import { CommonModule } from '@angular/common';
  19 +import { ResourcesLibraryRoutingModule } from '@home/pages/resource/resource-routing.module';
  20 +import { SharedModule } from '@shared/shared.module';
  21 +import { HomeComponentsModule } from '@home/components/home-components.module';
  22 +import { ResourcesLibraryComponent } from './resources-library.component';
  23 +
  24 +@NgModule({
  25 + declarations: [ResourcesLibraryComponent],
  26 + imports: [
  27 + CommonModule,
  28 + SharedModule,
  29 + HomeComponentsModule,
  30 + ResourcesLibraryRoutingModule
  31 + ]
  32 +})
  33 +export class ResourceModule { }
... ...
  1 +///
  2 +/// Copyright © 2016-2021 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Injectable } from '@angular/core';
  18 +import {
  19 + checkBoxCell,
  20 + DateEntityTableColumn,
  21 + EntityTableColumn,
  22 + EntityTableConfig
  23 +} from '@home/models/entity/entities-table-config.models';
  24 +import { Resolve } from '@angular/router';
  25 +import { Resource, ResourceInfo, ResourceTypeTranslationMap } from '@shared/models/resource.models';
  26 +import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models';
  27 +import { Direction } from '@shared/models/page/sort-order';
  28 +import { NULL_UUID } from '@shared/models/id/has-uuid';
  29 +import { DatePipe } from '@angular/common';
  30 +import { TranslateService } from '@ngx-translate/core';
  31 +import { ResourceService } from '@core/http/resource.service';
  32 +import { getCurrentAuthUser } from '@core/auth/auth.selectors';
  33 +import { Store } from '@ngrx/store';
  34 +import { AppState } from '@core/core.state';
  35 +import { Authority } from '@shared/models/authority.enum';
  36 +import { ResourcesLibraryComponent } from '@home/pages/resource/resources-library.component';
  37 +import { Observable } from 'rxjs/internal/Observable';
  38 +import { PageData } from '@shared/models/page/page-data';
  39 +
  40 +@Injectable()
  41 +export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableConfig<Resource>> {
  42 +
  43 + private readonly config: EntityTableConfig<Resource> = new EntityTableConfig<Resource>();
  44 + private readonly resourceTypesTranslationMap = ResourceTypeTranslationMap;
  45 +
  46 + constructor(private store: Store<AppState>,
  47 + private resourceService: ResourceService,
  48 + private translate: TranslateService,
  49 + private datePipe: DatePipe) {
  50 +
  51 + this.config.entityType = EntityType.TB_RESOURCE;
  52 + this.config.entityComponent = ResourcesLibraryComponent;
  53 + this.config.entityTranslations = entityTypeTranslations.get(EntityType.TB_RESOURCE);
  54 + this.config.entityResources = entityTypeResources.get(EntityType.TB_RESOURCE);
  55 + this.config.defaultSortOrder = {property: 'title', direction: Direction.ASC};
  56 +
  57 + this.config.entityTitle = (resource) => resource ?
  58 + resource.title : '';
  59 +
  60 + this.config.columns.push(
  61 + new DateEntityTableColumn<ResourceInfo>('createdTime', 'common.created-time', this.datePipe, '150px'),
  62 + new EntityTableColumn<ResourceInfo>('title', 'widgets-bundle.title', '60%'),
  63 + new EntityTableColumn<ResourceInfo>('resourceType', 'resource.resource-type', '40%',
  64 + entity => this.resourceTypesTranslationMap.get(entity.resourceType)),
  65 + new EntityTableColumn<ResourceInfo>('tenantId', 'widgets-bundle.system', '60px',
  66 + entity => {
  67 + return checkBoxCell(entity.tenantId.id === NULL_UUID);
  68 + }),
  69 + );
  70 +
  71 + this.config.cellActionDescriptors.push(
  72 + {
  73 + name: this.translate.instant('resource.export'),
  74 + icon: 'file_download',
  75 + isEnabled: () => true,
  76 + onAction: ($event, entity) => this.exportResource($event, entity)
  77 + }
  78 + );
  79 +
  80 + this.config.deleteEntityTitle = resource => this.translate.instant('resource.delete-resource-title',
  81 + { resourceTitle: resource.title });
  82 + this.config.deleteEntityContent = () => this.translate.instant('resource.delete-resource-text');
  83 + this.config.deleteEntitiesTitle = count => this.translate.instant('resource.delete-resources-title', {count});
  84 + this.config.deleteEntitiesContent = () => this.translate.instant('resource.delete-resources-text');
  85 +
  86 + this.config.entitiesFetchFunction = pageLink => this.resourceService.getResources(pageLink) as Observable<PageData<Resource>>;
  87 + this.config.loadEntity = id => this.resourceService.getResource(id.id);
  88 + this.config.saveEntity = resource => this.resourceService.saveResource(resource);
  89 + this.config.deleteEntity = id => this.resourceService.deleteResource(id.id);
  90 + }
  91 +
  92 + resolve(): EntityTableConfig<Resource> {
  93 + this.config.tableTitle = this.translate.instant('resource.resources-library');
  94 + const authUser = getCurrentAuthUser(this.store);
  95 + this.config.deleteEnabled = (resource) => this.isResourceEditable(resource, authUser.authority);
  96 + this.config.entitySelectionEnabled = (resource) => this.isResourceEditable(resource, authUser.authority);
  97 + this.config.detailsReadonly = (resource) => !this.isResourceEditable(resource, authUser.authority);
  98 + return this.config;
  99 + }
  100 +
  101 + exportResource($event: Event, resource: ResourceInfo) {
  102 + if ($event) {
  103 + $event.stopPropagation();
  104 + }
  105 + this.resourceService.downloadResource(resource.id.id).subscribe();
  106 + }
  107 +
  108 + private isResourceEditable(resource: Resource, authority: Authority): boolean {
  109 + if (authority === Authority.TENANT_ADMIN) {
  110 + return resource && resource.tenantId && resource.tenantId.id !== NULL_UUID;
  111 + } else {
  112 + return authority === Authority.SYS_ADMIN;
  113 + }
  114 + }
  115 +}
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2021 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div class="tb-details-buttons" fxLayout.xs="column">
  19 + <button mat-raised-button color="primary" fxFlex.xs
  20 + [disabled]="(isLoading$ | async)"
  21 + (click)="onEntityAction($event, 'delete')"
  22 + [fxShow]="!hideDelete() && !isEdit">
  23 + {{'resource.delete' | translate }}
  24 + </button>
  25 +</div>
  26 +<div class="mat-padding" fxLayout="column">
  27 + <form [formGroup]="entityForm">
  28 + <fieldset [disabled]="(isLoading$ | async) || !isEdit">
  29 + <mat-form-field class="mat-block">
  30 + <mat-label translate>resource.resource-type</mat-label>
  31 + <mat-select formControlName="resourceType" required>
  32 + <mat-option *ngFor="let resourceType of resourceTypes" [value]="resourceType">
  33 + {{ resourceTypesTranslationMap.get(resourceType) }}
  34 + </mat-option>
  35 + </mat-select>
  36 + </mat-form-field>
  37 + <mat-form-field class="mat-block" *ngIf="entityForm.get('resourceType').value !== resourceType.LWM2M_MODEL || !isAdd">
  38 + <mat-label translate>resource.title</mat-label>
  39 + <input matInput formControlName="title" required [readonly]="entityForm.get('resourceType').value === resourceType.LWM2M_MODEL">
  40 + <mat-error *ngIf="entityForm.get('title').hasError('required')">
  41 + {{ 'resource.title-required' | translate }}
  42 + </mat-error>
  43 + </mat-form-field>
  44 + <tb-file-input
  45 + formControlName="data"
  46 + required
  47 + [convertToBase64]="true"
  48 + [allowedExtensions]="getAllowedExtensions()"
  49 + [accept]="getAcceptType()"
  50 + dropLabel="{{'resource.drop-file' | translate}}"
  51 + [existingFileName]="entityForm.get('fileName')?.value"
  52 + (fileNameChanged)="entityForm?.get('fileName').patchValue($event)">
  53 + </tb-file-input>
  54 + </fieldset>
  55 + </form>
  56 +</div>
... ...
  1 +///
  2 +/// Copyright © 2016-2021 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, Inject, OnDestroy, OnInit } from '@angular/core';
  18 +import { Subject } from 'rxjs';
  19 +import { Store } from '@ngrx/store';
  20 +import { AppState } from '@core/core.state';
  21 +import { TranslateService } from '@ngx-translate/core';
  22 +import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
  23 +import { FormBuilder, FormGroup, Validators } from '@angular/forms';
  24 +import { EntityComponent } from '@home/components/entity/entity.component';
  25 +import {
  26 + Resource,
  27 + ResourceType,
  28 + ResourceTypeExtension,
  29 + ResourceTypeMIMETypes,
  30 + ResourceTypeTranslationMap
  31 +} from '@shared/models/resource.models';
  32 +import { distinctUntilChanged, takeUntil } from 'rxjs/operators';
  33 +
  34 +@Component({
  35 + selector: 'tb-resources-library',
  36 + templateUrl: './resources-library.component.html'
  37 +})
  38 +export class ResourcesLibraryComponent extends EntityComponent<Resource> implements OnInit, OnDestroy {
  39 +
  40 + readonly resourceType = ResourceType;
  41 + readonly resourceTypes = Object.values(this.resourceType);
  42 + readonly resourceTypesTranslationMap = ResourceTypeTranslationMap;
  43 +
  44 + private destroy$ = new Subject();
  45 +
  46 + constructor(protected store: Store<AppState>,
  47 + protected translate: TranslateService,
  48 + @Inject('entity') protected entityValue: Resource,
  49 + @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<Resource>,
  50 + public fb: FormBuilder) {
  51 + super(store, fb, entityValue, entitiesTableConfigValue);
  52 + }
  53 +
  54 + ngOnInit() {
  55 + super.ngOnInit();
  56 + this.entityForm.get('resourceType').valueChanges.pipe(
  57 + distinctUntilChanged((oldValue, newValue) => [oldValue, newValue].includes(this.resourceType.LWM2M_MODEL)),
  58 + takeUntil(this.destroy$)
  59 + ).subscribe((type) => {
  60 + if (type === this.resourceType.LWM2M_MODEL) {
  61 + this.entityForm.get('title').clearValidators();
  62 + } else {
  63 + this.entityForm.get('title').setValidators(Validators.required);
  64 + }
  65 + this.entityForm.get('title').updateValueAndValidity({emitEvent: false});
  66 + });
  67 + }
  68 +
  69 + ngOnDestroy() {
  70 + super.ngOnDestroy();
  71 + this.destroy$.next();
  72 + this.destroy$.complete();
  73 + }
  74 +
  75 + hideDelete() {
  76 + if (this.entitiesTableConfig) {
  77 + return !this.entitiesTableConfig.deleteEnabled(this.entity);
  78 + } else {
  79 + return false;
  80 + }
  81 + }
  82 +
  83 + buildForm(entity: Resource): FormGroup {
  84 + return this.fb.group(
  85 + {
  86 + resourceType: [{value: entity?.resourceType ? entity.resourceType : ResourceType.LWM2M_MODEL,
  87 + disabled: this.isEdit }, [Validators.required]],
  88 + data: [entity ? entity.data : null, [Validators.required]],
  89 + fileName: [entity ? entity.fileName : null, [Validators.required]],
  90 + title: [entity ? entity.title : '', []]
  91 + }
  92 + );
  93 + }
  94 +
  95 + updateForm(entity: Resource) {
  96 + this.entityForm.patchValue({resourceType: entity.resourceType});
  97 + if (this.isEdit) {
  98 + this.entityForm.get('resourceType').disable({emitEvent: false});
  99 + }
  100 + this.entityForm.patchValue({
  101 + data: entity.data,
  102 + fileName: entity.fileName,
  103 + title: entity.title
  104 + });
  105 + }
  106 +
  107 + getAllowedExtensions() {
  108 + try {
  109 + return ResourceTypeExtension.get(this.entityForm.get('resourceType').value);
  110 + } catch (e) {
  111 + return '';
  112 + }
  113 + }
  114 +
  115 + getAcceptType() {
  116 + try {
  117 + return ResourceTypeMIMETypes.get(this.entityForm.get('resourceType').value);
  118 + } catch (e) {
  119 + return '*/*';
  120 + }
  121 + }
  122 +}
... ...
... ... @@ -101,6 +101,9 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
101 101 @Input()
102 102 existingFileName: string;
103 103
  104 + @Input()
  105 + convertToBase64 = false;
  106 +
104 107 @Output()
105 108 fileNameChanged = new EventEmitter<string>();
106 109
... ... @@ -128,7 +131,7 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
128 131 const reader = new FileReader();
129 132 reader.onload = (loadEvent) => {
130 133 if (typeof reader.result === 'string') {
131   - const fileContent = reader.result;
  134 + const fileContent = this.convertToBase64 ? window.btoa(reader.result) : reader.result;
132 135 if (fileContent && fileContent.length > 0) {
133 136 if (this.contentConvertFunction) {
134 137 this.fileContent = this.contentConvertFunction(fileContent);
... ... @@ -144,7 +147,11 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
144 147 }
145 148 }
146 149 };
147   - reader.readAsText(file.file);
  150 + if (this.convertToBase64) {
  151 + reader.readAsBinaryString(file.file);
  152 + } else {
  153 + reader.readAsText(file.file);
  154 + }
148 155 }
149 156 }
150 157 });
... ... @@ -159,7 +166,9 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
159 166 }
160 167
161 168 ngOnDestroy() {
162   - this.autoUploadSubscription.unsubscribe();
  169 + if (this.autoUploadSubscription) {
  170 + this.autoUploadSubscription.unsubscribe();
  171 + }
163 172 }
164 173
165 174 registerOnChange(fn: any): void {
... ...
... ... @@ -17,22 +17,6 @@
17 17 import { TenantId } from './id/tenant-id';
18 18 import { BaseData, HasId } from '@shared/models/base-data';
19 19
20   -///
21   -/// Copyright © 2016-2019 The Thingsboard Authors
22   -///
23   -/// Licensed under the Apache License, Version 2.0 (the "License");
24   -/// you may not use this file except in compliance with the License.
25   -/// You may obtain a copy of the License at
26   -///
27   -/// http://www.apache.org/licenses/LICENSE-2.0
28   -///
29   -/// Unless required by applicable law or agreed to in writing, software
30   -/// distributed under the License is distributed on an "AS IS" BASIS,
31   -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
32   -/// See the License for the specific language governing permissions and
33   -/// limitations under the License.
34   -///
35   -
36 20 export enum EntityType {
37 21 TENANT = 'TENANT',
38 22 TENANT_PROFILE = 'TENANT_PROFILE',
... ... @@ -48,7 +32,8 @@ export enum EntityType {
48 32 ENTITY_VIEW = 'ENTITY_VIEW',
49 33 WIDGETS_BUNDLE = 'WIDGETS_BUNDLE',
50 34 WIDGET_TYPE = 'WIDGET_TYPE',
51   - API_USAGE_STATE = 'API_USAGE_STATE'
  35 + API_USAGE_STATE = 'API_USAGE_STATE',
  36 + TB_RESOURCE = 'TB_RESOURCE'
52 37 }
53 38
54 39 export enum AliasEntityType {
... ... @@ -282,7 +267,17 @@ export const entityTypeTranslations = new Map<EntityType | AliasEntityType, Enti
282 267 type: 'entity.type-current-user-owner',
283 268 list: 'entity.type-current-user-owner'
284 269 }
285   - ]
  270 + ],
  271 + [
  272 + EntityType.TB_RESOURCE,
  273 + {
  274 + details: 'resource.resource-library-details',
  275 + add: 'resource.add',
  276 + noEntities: 'resource.no-resource-text',
  277 + search: 'resource.search',
  278 + selectedEntities: 'resource.selected-resources'
  279 + }
  280 + ],
286 281 ]
287 282 );
288 283
... ... @@ -353,6 +348,12 @@ export const entityTypeResources = new Map<EntityType, EntityTypeResource<BaseDa
353 348 {
354 349 helpLinkId: 'widgetsBundles'
355 350 }
  351 + ],
  352 + [
  353 + EntityType.TB_RESOURCE,
  354 + {
  355 + helpLinkId: 'resources'
  356 + }
356 357 ]
357 358 ]
358 359 );
... ...
  1 +///
  2 +/// Copyright © 2016-2021 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { EntityId } from '@shared/models/id/entity-id';
  18 +import { EntityType } from '@shared/models/entity-type.models';
  19 +
  20 +export class TbResourceId implements EntityId {
  21 + entityType = EntityType.TB_RESOURCE;
  22 + id: string;
  23 + constructor(id: string) {
  24 + this.id = id;
  25 + }
  26 +}
... ...
... ... @@ -39,6 +39,7 @@ export * from './material.models';
39 39 export * from './oauth2.models';
40 40 export * from './queue.models';
41 41 export * from './relation.models';
  42 +export * from './resource.models';
42 43 export * from './rule-chain.models';
43 44 export * from './rule-node.models';
44 45 export * from './settings.models';
... ...
  1 +///
  2 +/// Copyright © 2016-2021 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { BaseData } from '@shared/models/base-data';
  18 +import { TenantId } from '@shared/models/id/tenant-id';
  19 +import { TbResourceId } from '@shared/models/id/tb-resource-id';
  20 +
  21 +export enum ResourceType {
  22 + LWM2M_MODEL = 'LWM2M_MODEL',
  23 + PKCS_12 = 'PKCS_12',
  24 + JKS = 'JKS'
  25 +}
  26 +
  27 +export const ResourceTypeMIMETypes = new Map<ResourceType, string>(
  28 + [
  29 + [ResourceType.LWM2M_MODEL, 'application/xml,text/xml'],
  30 + [ResourceType.PKCS_12, 'application/x-pkcs12'],
  31 + [ResourceType.JKS, 'application/x-java-keystore']
  32 + ]
  33 +);
  34 +
  35 +export const ResourceTypeExtension = new Map<ResourceType, string>(
  36 + [
  37 + [ResourceType.LWM2M_MODEL, 'xml'],
  38 + [ResourceType.PKCS_12, 'p12,pfx'],
  39 + [ResourceType.JKS, 'jks']
  40 + ]
  41 +);
  42 +
  43 +export const ResourceTypeTranslationMap = new Map<ResourceType, string>(
  44 + [
  45 + [ResourceType.LWM2M_MODEL, 'LWM2M model'],
  46 + [ResourceType.PKCS_12, 'PKCS #12'],
  47 + [ResourceType.JKS, 'JKS']
  48 + ]
  49 +);
  50 +
  51 +export interface ResourceInfo extends BaseData<TbResourceId> {
  52 + tenantId?: TenantId;
  53 + resourceKey?: string;
  54 + title?: string;
  55 + resourceType: ResourceType;
  56 +}
  57 +
  58 +export interface Resource extends ResourceInfo {
  59 + data: string;
  60 + fileName: string;
  61 +}
... ...
... ... @@ -1963,6 +1963,31 @@
1963 1963 "invalid-additional-info": "Unable to parse additional info json.",
1964 1964 "no-relations-text": "No relations found"
1965 1965 },
  1966 + "resource": {
  1967 + "add": "Add Resource",
  1968 + "delete": "Delete resource",
  1969 + "delete-resource-text": "Be careful, after the confirmation the resource will become unrecoverable.",
  1970 + "delete-resource-title": "Are you sure you want to delete the resource '{{resourceTitle}}'?",
  1971 + "delete-resources-action-title": "Delete { count, plural, 1 {1 resource} other {# resources} }",
  1972 + "delete-resources-text": "Be careful, after the confirmation all selected resources will be removed.",
  1973 + "delete-resources-title": "Are you sure you want to delete { count, plural, 1 {1 resource} other {# resources} }?",
  1974 + "drop-file": "Drop a resource file or click to select a file to upload.",
  1975 + "empty": "Resource is empty",
  1976 + "export": "Export resource",
  1977 + "management": "Resource management",
  1978 + "no-resource-matching": "No resource matching '{{widgetsBundle}}' were found.",
  1979 + "no-resource-text": "No resources found",
  1980 + "open-widgets-bundle": "Open widgets bundle",
  1981 + "resource": "Resource",
  1982 + "resource-library-details": "Resource library details",
  1983 + "resource-type": "Resource type",
  1984 + "resources-library": "Resources library",
  1985 + "search": "Search resources",
  1986 + "selected-resources": "{ count, plural, 1 {1 resource} other {# resources} } selected",
  1987 + "system": "System",
  1988 + "title": "Title",
  1989 + "title-required": "Title is required."
  1990 + },
1966 1991 "rulechain": {
1967 1992 "rulechain": "Rule chain",
1968 1993 "rulechains": "Rule chains",
... ...