Commit bfc03c4c69c23360607fcd8e8e9cc6f646f6dbc6
Committed by
GitHub
Merge pull request #4293 from vvlladd28/feature/resources/add-new-type
[3.3] UI: Added manager resources; added new entity type TB_RESOURCE
Showing
14 changed files
with
635 additions
and
21 deletions
ui-ngx/src/app/core/http/resource.service.ts
0 → 100644
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,6 +105,13 @@ export class MenuService { | ||
105 | }, | 105 | }, |
106 | { | 106 | { |
107 | id: guid(), | 107 | id: guid(), |
108 | + name: 'resource.resources-library', | ||
109 | + type: 'link', | ||
110 | + path: '/resources-library', | ||
111 | + icon: 'folder' | ||
112 | + }, | ||
113 | + { | ||
114 | + id: guid(), | ||
108 | name: 'admin.system-settings', | 115 | name: 'admin.system-settings', |
109 | type: 'toggle', | 116 | type: 'toggle', |
110 | path: '/settings', | 117 | path: '/settings', |
@@ -182,6 +189,16 @@ export class MenuService { | @@ -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 | name: 'admin.system-settings', | 202 | name: 'admin.system-settings', |
186 | places: [ | 203 | places: [ |
187 | { | 204 | { |
@@ -285,6 +302,13 @@ export class MenuService { | @@ -285,6 +302,13 @@ export class MenuService { | ||
285 | }, | 302 | }, |
286 | { | 303 | { |
287 | id: guid(), | 304 | id: guid(), |
305 | + name: 'resource.resources-library', | ||
306 | + type: 'link', | ||
307 | + path: '/resources-library', | ||
308 | + icon: 'folder' | ||
309 | + }, | ||
310 | + { | ||
311 | + id: guid(), | ||
288 | name: 'admin.home-settings', | 312 | name: 'admin.home-settings', |
289 | type: 'link', | 313 | type: 'link', |
290 | path: '/settings/home', | 314 | path: '/settings/home', |
@@ -369,6 +393,16 @@ export class MenuService { | @@ -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 | name: 'dashboard.management', | 406 | name: 'dashboard.management', |
373 | places: [ | 407 | places: [ |
374 | { | 408 | { |
@@ -34,6 +34,7 @@ import { MODULES_MAP } from '@shared/public-api'; | @@ -34,6 +34,7 @@ import { MODULES_MAP } from '@shared/public-api'; | ||
34 | import { modulesMap } from '../../common/modules-map'; | 34 | import { modulesMap } from '../../common/modules-map'; |
35 | import { DeviceProfileModule } from './device-profile/device-profile.module'; | 35 | import { DeviceProfileModule } from './device-profile/device-profile.module'; |
36 | import { ApiUsageModule } from '@home/pages/api-usage/api-usage.module'; | 36 | import { ApiUsageModule } from '@home/pages/api-usage/api-usage.module'; |
37 | +import { ResourceModule } from '@home/pages/resource/resource.module'; | ||
37 | 38 | ||
38 | @NgModule({ | 39 | @NgModule({ |
39 | exports: [ | 40 | exports: [ |
@@ -52,6 +53,7 @@ import { ApiUsageModule } from '@home/pages/api-usage/api-usage.module'; | @@ -52,6 +53,7 @@ import { ApiUsageModule } from '@home/pages/api-usage/api-usage.module'; | ||
52 | DashboardModule, | 53 | DashboardModule, |
53 | AuditLogModule, | 54 | AuditLogModule, |
54 | ApiUsageModule, | 55 | ApiUsageModule, |
56 | + ResourceModule, | ||
55 | UserModule | 57 | UserModule |
56 | ], | 58 | ], |
57 | providers: [ | 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,6 +101,9 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, | ||
101 | @Input() | 101 | @Input() |
102 | existingFileName: string; | 102 | existingFileName: string; |
103 | 103 | ||
104 | + @Input() | ||
105 | + convertToBase64 = false; | ||
106 | + | ||
104 | @Output() | 107 | @Output() |
105 | fileNameChanged = new EventEmitter<string>(); | 108 | fileNameChanged = new EventEmitter<string>(); |
106 | 109 | ||
@@ -128,7 +131,7 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, | @@ -128,7 +131,7 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, | ||
128 | const reader = new FileReader(); | 131 | const reader = new FileReader(); |
129 | reader.onload = (loadEvent) => { | 132 | reader.onload = (loadEvent) => { |
130 | if (typeof reader.result === 'string') { | 133 | if (typeof reader.result === 'string') { |
131 | - const fileContent = reader.result; | 134 | + const fileContent = this.convertToBase64 ? window.btoa(reader.result) : reader.result; |
132 | if (fileContent && fileContent.length > 0) { | 135 | if (fileContent && fileContent.length > 0) { |
133 | if (this.contentConvertFunction) { | 136 | if (this.contentConvertFunction) { |
134 | this.fileContent = this.contentConvertFunction(fileContent); | 137 | this.fileContent = this.contentConvertFunction(fileContent); |
@@ -144,7 +147,11 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, | @@ -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,7 +166,9 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, | ||
159 | } | 166 | } |
160 | 167 | ||
161 | ngOnDestroy() { | 168 | ngOnDestroy() { |
162 | - this.autoUploadSubscription.unsubscribe(); | 169 | + if (this.autoUploadSubscription) { |
170 | + this.autoUploadSubscription.unsubscribe(); | ||
171 | + } | ||
163 | } | 172 | } |
164 | 173 | ||
165 | registerOnChange(fn: any): void { | 174 | registerOnChange(fn: any): void { |
@@ -17,22 +17,6 @@ | @@ -17,22 +17,6 @@ | ||
17 | import { TenantId } from './id/tenant-id'; | 17 | import { TenantId } from './id/tenant-id'; |
18 | import { BaseData, HasId } from '@shared/models/base-data'; | 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 | export enum EntityType { | 20 | export enum EntityType { |
37 | TENANT = 'TENANT', | 21 | TENANT = 'TENANT', |
38 | TENANT_PROFILE = 'TENANT_PROFILE', | 22 | TENANT_PROFILE = 'TENANT_PROFILE', |
@@ -48,7 +32,8 @@ export enum EntityType { | @@ -48,7 +32,8 @@ export enum EntityType { | ||
48 | ENTITY_VIEW = 'ENTITY_VIEW', | 32 | ENTITY_VIEW = 'ENTITY_VIEW', |
49 | WIDGETS_BUNDLE = 'WIDGETS_BUNDLE', | 33 | WIDGETS_BUNDLE = 'WIDGETS_BUNDLE', |
50 | WIDGET_TYPE = 'WIDGET_TYPE', | 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 | export enum AliasEntityType { | 39 | export enum AliasEntityType { |
@@ -282,7 +267,17 @@ export const entityTypeTranslations = new Map<EntityType | AliasEntityType, Enti | @@ -282,7 +267,17 @@ export const entityTypeTranslations = new Map<EntityType | AliasEntityType, Enti | ||
282 | type: 'entity.type-current-user-owner', | 267 | type: 'entity.type-current-user-owner', |
283 | list: 'entity.type-current-user-owner' | 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,6 +348,12 @@ export const entityTypeResources = new Map<EntityType, EntityTypeResource<BaseDa | ||
353 | { | 348 | { |
354 | helpLinkId: 'widgetsBundles' | 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,6 +39,7 @@ export * from './material.models'; | ||
39 | export * from './oauth2.models'; | 39 | export * from './oauth2.models'; |
40 | export * from './queue.models'; | 40 | export * from './queue.models'; |
41 | export * from './relation.models'; | 41 | export * from './relation.models'; |
42 | +export * from './resource.models'; | ||
42 | export * from './rule-chain.models'; | 43 | export * from './rule-chain.models'; |
43 | export * from './rule-node.models'; | 44 | export * from './rule-node.models'; |
44 | export * from './settings.models'; | 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,6 +1963,31 @@ | ||
1963 | "invalid-additional-info": "Unable to parse additional info json.", | 1963 | "invalid-additional-info": "Unable to parse additional info json.", |
1964 | "no-relations-text": "No relations found" | 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 | "rulechain": { | 1991 | "rulechain": { |
1967 | "rulechain": "Rule chain", | 1992 | "rulechain": "Rule chain", |
1968 | "rulechains": "Rule chains", | 1993 | "rulechains": "Rule chains", |