From 6b6bbebab8355a7ac8f11ea353b9737bbcde3332 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 27 Nov 2024 19:28:24 +0200 Subject: [PATCH 1/6] Implement JS Module resources support. --- .../main/data/upgrade/3.8.1/schema_update.sql | 8 +- .../controller/ControllerConstants.java | 1 + .../controller/TbResourceController.java | 8 + .../server/common/data/ResourceSubType.java | 4 +- ui-ngx/src/app/core/http/resource.service.ts | 7 +- ui-ngx/src/app/core/services/menu.models.ts | 13 ++ .../dashboard-widget-select.component.html | 2 +- .../action/widget-action.component.html | 1 + .../components/widget/widget.component.ts | 59 +++--- .../home/pages/admin/admin-routing.module.ts | 39 ++++ .../modules/home/pages/admin/admin.module.ts | 20 +- .../js-library-table-config.resolver.ts | 170 +++++++++++++++++ .../js-library-table-header.component.html | 30 +++ .../js-library-table-header.component.ts | 42 +++++ .../admin/resource/js-resource.component.html | 117 ++++++++++++ .../admin/resource/js-resource.component.ts | 176 ++++++++++++++++++ .../resource/resources-library.component.html | 6 +- .../resource/resources-library.component.ts | 10 +- .../resources-table-header.component.ts | 2 +- .../components/file-input.component.html | 10 +- .../components/file-input.component.scss | 41 +++- .../shared/components/file-input.component.ts | 10 + .../js-func-module-row.component.html | 39 ++++ .../js-func-module-row.component.scss | 23 +++ .../js-func-module-row.component.ts | 156 ++++++++++++++++ .../components/js-func-modules.component.html | 66 +++++++ .../components/js-func-modules.component.scss | 74 ++++++++ .../components/js-func-modules.component.ts | 135 ++++++++++++++ .../shared/components/js-func.component.html | 14 +- .../shared/components/js-func.component.ts | 138 +++++++++++--- .../resource-autocomplete.component.html | 8 +- .../resource-autocomplete.component.ts | 12 +- .../app/shared/models/js-function.models.ts | 126 +++++++++++++ .../src/app/shared/models/resource.models.ts | 13 +- ui-ngx/src/app/shared/models/widget.models.ts | 3 +- ui-ngx/src/app/shared/shared.module.ts | 6 + .../assets/locale/locale.constant-en_US.json | 38 +++- ui-ngx/src/form.scss | 9 + 38 files changed, 1546 insertions(+), 90 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-config.resolver.ts create mode 100644 ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-header.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-header.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.ts create mode 100644 ui-ngx/src/app/shared/components/js-func-module-row.component.html create mode 100644 ui-ngx/src/app/shared/components/js-func-module-row.component.scss create mode 100644 ui-ngx/src/app/shared/components/js-func-module-row.component.ts create mode 100644 ui-ngx/src/app/shared/components/js-func-modules.component.html create mode 100644 ui-ngx/src/app/shared/components/js-func-modules.component.scss create mode 100644 ui-ngx/src/app/shared/components/js-func-modules.component.ts create mode 100644 ui-ngx/src/app/shared/models/js-function.models.ts diff --git a/application/src/main/data/upgrade/3.8.1/schema_update.sql b/application/src/main/data/upgrade/3.8.1/schema_update.sql index e4faca82a5..b32e57fde5 100644 --- a/application/src/main/data/upgrade/3.8.1/schema_update.sql +++ b/application/src/main/data/upgrade/3.8.1/schema_update.sql @@ -176,4 +176,10 @@ $$ END IF; ALTER TABLE qr_code_settings DROP COLUMN IF EXISTS android_config, DROP COLUMN IF EXISTS ios_config; END; -$$; \ No newline at end of file +$$; + +-- UPDATE RESOURCE JS_MODULE SUB TYPE START + +UPDATE resource SET resource_sub_type = 'EXTENSION' WHERE resource_type = 'JS_MODULE' AND resource_sub_type IS NULL; + +-- UPDATE RESOURCE JS_MODULE SUB TYPE END diff --git a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java index c3666501b5..c7a59cfd83 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java +++ b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java @@ -125,6 +125,7 @@ public class ControllerConstants { protected static final String RESOURCE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the resource title."; protected static final String RESOURCE_TYPE = "A string value representing the resource type."; + protected static final String RESOURCE_SUB_TYPE = "A string value representing the resource sub-type."; protected static final String LWM2M_OBJECT_DESCRIPTION = "LwM2M Object is a object that includes information about the LwM2M model which can be used in transport configuration for the LwM2M device profile. "; diff --git a/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java b/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java index 3671cec189..0d4b988771 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java @@ -38,6 +38,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResourceInfo; @@ -70,6 +71,7 @@ import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DE import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_INFO_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_SUB_TYPE; import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_TEXT_SEARCH_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_TYPE; import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION; @@ -229,6 +231,8 @@ public class TbResourceController extends BaseController { @RequestParam int page, @Parameter(description = RESOURCE_TYPE, schema = @Schema(allowableValues = {"LWM2M_MODEL", "JKS", "PKCS_12", "JS_MODULE"})) @RequestParam(required = false) String resourceType, + @Parameter(description = RESOURCE_SUB_TYPE, schema = @Schema(allowableValues = {"EXTENSION", "MODULE"})) + @RequestParam(required = false) String resourceSubType, @Parameter(description = RESOURCE_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "title", "resourceType", "tenantId"})) @@ -241,8 +245,12 @@ public class TbResourceController extends BaseController { Set resourceTypes = new HashSet<>(); if (StringUtils.isNotEmpty(resourceType)) { resourceTypes.add(ResourceType.valueOf(resourceType)); + if (StringUtils.isNotEmpty(resourceSubType)) { + filter.resourceSubTypes(Set.of(ResourceSubType.valueOf(resourceSubType))); + } } else { Collections.addAll(resourceTypes, ResourceType.values()); + resourceTypes.remove(ResourceType.JS_MODULE); resourceTypes.remove(ResourceType.IMAGE); resourceTypes.remove(ResourceType.DASHBOARD); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ResourceSubType.java b/common/data/src/main/java/org/thingsboard/server/common/data/ResourceSubType.java index 10a033f75d..17b9621d2b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ResourceSubType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ResourceSubType.java @@ -17,5 +17,7 @@ package org.thingsboard.server.common.data; public enum ResourceSubType { IMAGE, - SCADA_SYMBOL + SCADA_SYMBOL, + EXTENSION, + MODULE } diff --git a/ui-ngx/src/app/core/http/resource.service.ts b/ui-ngx/src/app/core/http/resource.service.ts index 78f025e59d..f8e0920119 100644 --- a/ui-ngx/src/app/core/http/resource.service.ts +++ b/ui-ngx/src/app/core/http/resource.service.ts @@ -20,7 +20,7 @@ import { PageLink } from '@shared/models/page/page-link'; import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; import { forkJoin, Observable, of } from 'rxjs'; import { PageData } from '@shared/models/page/page-data'; -import { Resource, ResourceInfo, ResourceType, TBResourceScope } from '@shared/models/resource.models'; +import { Resource, ResourceInfo, ResourceSubType, ResourceType, TBResourceScope } from '@shared/models/resource.models'; import { catchError, mergeMap } from 'rxjs/operators'; import { isNotEmptyStr } from '@core/utils'; import { ResourcesService } from '@core/services/resources.service'; @@ -36,11 +36,14 @@ export class ResourceService { } - public getResources(pageLink: PageLink, resourceType?: ResourceType, config?: RequestConfig): Observable> { + public getResources(pageLink: PageLink, resourceType?: ResourceType, resourceSubType?: ResourceSubType, config?: RequestConfig): Observable> { let url = `/api/resource${pageLink.toQuery()}`; if (isNotEmptyStr(resourceType)) { url += `&resourceType=${resourceType}`; } + if (isNotEmptyStr(resourceSubType)) { + url += `&resourceSubType=${resourceSubType}`; + } return this.http.get>(url, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/core/services/menu.models.ts b/ui-ngx/src/app/core/services/menu.models.ts index cf08fe2a56..16a87770ac 100644 --- a/ui-ngx/src/app/core/services/menu.models.ts +++ b/ui-ngx/src/app/core/services/menu.models.ts @@ -59,6 +59,7 @@ export enum MenuId { images = 'images', scada_symbols = 'scada_symbols', resources_library = 'resources_library', + javascript_library = 'javascript_library', notifications_center = 'notifications_center', notification_inbox = 'notification_inbox', notification_sent = 'notification_sent', @@ -209,6 +210,16 @@ export const menuSectionMap = new Map([ icon: 'mdi:rhombus-split' } ], + [ + MenuId.javascript_library, + { + id: MenuId.javascript_library, + name: 'javascript.javascript-library', + type: 'link', + path: '/resources/javascript-library', + icon: 'mdi:language-javascript' + } + ], [ MenuId.notifications_center, { @@ -707,6 +718,7 @@ const defaultUserMenuMap = new Map([ }, {id: MenuId.images}, {id: MenuId.scada_symbols}, + {id: MenuId.javascript_library}, {id: MenuId.resources_library} ] }, @@ -803,6 +815,7 @@ const defaultUserMenuMap = new Map([ }, {id: MenuId.images}, {id: MenuId.scada_symbols}, + {id: MenuId.javascript_library}, {id: MenuId.resources_library} ] }, diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.html index bfaaf700ce..f1bbca0292 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.html @@ -51,7 +51,7 @@
- {{ item.title }} + {{ item.title }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.html index 1b3213b251..362e8eac2e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.html @@ -237,6 +237,7 @@ [globalVariables]="functionScopeVariables" [validationArgs]="[]" [editorCompleter]="customActionEditorCompleter" + withModules helpId="widget/action/custom_action_fn" > diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts index 7329eb1eaf..a9634c24a8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts @@ -120,6 +120,7 @@ import { DASHBOARD_PAGE_COMPONENT_TOKEN } from '@home/components/tokens'; import { MODULES_MAP } from '@shared/models/constants'; import { IModulesMap } from '@modules/common/modules-map.models'; import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; +import { compileTbFunction, isNotEmptyTbFunction } from '@shared/models/js-function.models'; @Component({ selector: 'tb-widget', @@ -1108,17 +1109,25 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, break; case WidgetActionType.custom: const customFunction = descriptor.customFunction; - if (customFunction && customFunction.length > 0) { - try { - if (!additionalParams) { - additionalParams = {}; + if (isNotEmptyTbFunction(customFunction)) { + compileTbFunction(this.widgetContext.http, customFunction, '$event', 'widgetContext', 'entityId', + 'entityName', 'additionalParams', 'entityLabel').subscribe( + { + next: (compiled) => { + try { + if (!additionalParams) { + additionalParams = {}; + } + compiled.execute($event, this.widgetContext, entityId, entityName, additionalParams, entityLabel); + } catch (e) { + console.error(e); + } + }, + error: (err) => { + console.error(err); + } } - const customActionFunction = new Function('$event', 'widgetContext', 'entityId', - 'entityName', 'additionalParams', 'entityLabel', customFunction); - customActionFunction($event, this.widgetContext, entityId, entityName, additionalParams, entityLabel); - } catch (e) { - console.error(e); - } + ) } break; case WidgetActionType.customPretty: @@ -1133,18 +1142,26 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, } this.loadCustomActionResources(actionNamespace, customCss, customResources, descriptor).subscribe({ next: () => { - if (isDefined(customPrettyFunction) && customPrettyFunction.length > 0) { - try { - if (!additionalParams) { - additionalParams = {}; + if (isNotEmptyTbFunction(customPrettyFunction)) { + compileTbFunction(this.widgetContext.http, customPrettyFunction, '$event', 'widgetContext', 'entityId', + 'entityName', 'htmlTemplate', 'additionalParams', 'entityLabel').subscribe( + { + next: (compiled) => { + try { + if (!additionalParams) { + additionalParams = {}; + } + this.widgetContext.customDialog.setAdditionalImports(descriptor.customImports); + compiled.execute($event, this.widgetContext, entityId, entityName, htmlTemplate, additionalParams, entityLabel); + } catch (e) { + console.error(e); + } + }, + error: (err) => { + console.error(err); + } } - const customActionPrettyFunction = new Function('$event', 'widgetContext', 'entityId', - 'entityName', 'htmlTemplate', 'additionalParams', 'entityLabel', customPrettyFunction); - this.widgetContext.customDialog.setAdditionalImports(descriptor.customImports); - customActionPrettyFunction($event, this.widgetContext, entityId, entityName, htmlTemplate, additionalParams, entityLabel); - } catch (e) { - console.error(e); - } + ) } }, error: (errorMessages: string[]) => { diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts index b914ab8b2a..7920e5b4ae 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts @@ -45,6 +45,7 @@ import { ImageService } from '@core/http/image.service'; import { ScadaSymbolData } from '@home/pages/scada-symbol/scada-symbol-editor.models'; import { MenuId } from '@core/services/menu.models'; import { catchError } from 'rxjs/operators'; +import { JsLibraryTableConfigResolver } from '@home/pages/admin/resource/js-library-table-config.resolver'; export const scadaSymbolResolver: ResolveFn = (route: ActivatedRouteSnapshot, @@ -177,6 +178,43 @@ const routes: Routes = [ } } ] + }, + { + path: 'javascript-library', + data: { + breadcrumb: { + menuId: MenuId.javascript_library + } + }, + children: [ + { + path: '', + component: EntitiesTableComponent, + data: { + auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN], + title: 'javascript.javascript-library', + }, + resolve: { + entitiesTableConfig: JsLibraryTableConfigResolver + } + }, + { + path: ':entityId', + component: EntityDetailsPageComponent, + canDeactivate: [ConfirmOnExitGuard], + data: { + breadcrumb: { + labelFunction: entityDetailsPageBreadcrumbLabelFunction, + icon: 'mdi:language-javascript' + } as BreadCrumbConfig, + auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN], + title: 'javascript.javascript-library' + }, + resolve: { + entitiesTableConfig: JsLibraryTableConfigResolver + } + } + ] } ] }, @@ -393,6 +431,7 @@ const routes: Routes = [ exports: [RouterModule], providers: [ ResourcesLibraryTableConfigResolver, + JsLibraryTableConfigResolver, QueuesTableConfigResolver ] }) diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts index 5613e953c3..704f909809 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts @@ -33,6 +33,9 @@ import { RepositoryAdminSettingsComponent } from '@home/pages/admin/repository-a import { AutoCommitAdminSettingsComponent } from '@home/pages/admin/auto-commit-admin-settings.component'; import { TwoFactorAuthSettingsComponent } from '@home/pages/admin/two-factor-auth-settings.component'; import { OAuth2Module } from '@home/pages/admin/oauth2/oauth2.module'; +import { JsLibraryTableHeaderComponent } from '@home/pages/admin/resource/js-library-table-header.component'; +import { JsResourceComponent } from '@home/pages/admin/resource/js-resource.component'; +import { NgxFlowModule } from '@flowjs/ngx-flow'; @NgModule({ declarations: @@ -45,17 +48,20 @@ import { OAuth2Module } from '@home/pages/admin/oauth2/oauth2.module'; HomeSettingsComponent, ResourcesLibraryComponent, ResourcesTableHeaderComponent, + JsResourceComponent, + JsLibraryTableHeaderComponent, QueueComponent, RepositoryAdminSettingsComponent, AutoCommitAdminSettingsComponent, TwoFactorAuthSettingsComponent ], - imports: [ - CommonModule, - SharedModule, - HomeComponentsModule, - AdminRoutingModule, - OAuth2Module - ] + imports: [ + CommonModule, + SharedModule, + HomeComponentsModule, + AdminRoutingModule, + OAuth2Module, + NgxFlowModule + ] }) export class AdminModule { } diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-config.resolver.ts new file mode 100644 index 0000000000..5f48532eff --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-config.resolver.ts @@ -0,0 +1,170 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { + checkBoxCell, + DateEntityTableColumn, + EntityTableColumn, + EntityTableConfig +} from '@home/models/entity/entities-table-config.models'; +import { Router } from '@angular/router'; +import { + Resource, + ResourceInfo, + ResourceSubType, + ResourceSubTypeTranslationMap, + ResourceType +} from '@shared/models/resource.models'; +import { EntityType, entityTypeResources } from '@shared/models/entity-type.models'; +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 { PageLink } from '@shared/models/page/page-link'; +import { EntityAction } from '@home/models/entity/entity-component.models'; +import { JsLibraryTableHeaderComponent } from '@home/pages/admin/resource/js-library-table-header.component'; +import { JsResourceComponent } from '@home/pages/admin/resource/js-resource.component'; +import { switchMap } from 'rxjs/operators'; + +@Injectable() +export class JsLibraryTableConfigResolver { + + private readonly config: EntityTableConfig = new EntityTableConfig(); + + constructor(private store: Store, + private resourceService: ResourceService, + private translate: TranslateService, + private router: Router, + private datePipe: DatePipe) { + + this.config.entityType = EntityType.TB_RESOURCE; + this.config.entityComponent = JsResourceComponent; + this.config.entityTranslations = { + details: 'javascript.javascript-resource-details', + add: 'javascript.add', + noEntities: 'javascript.no-javascript-resource-text', + search: 'javascript.search', + selectedEntities: 'javascript.selected-javascript-resources' + }; + this.config.entityResources = entityTypeResources.get(EntityType.TB_RESOURCE); + this.config.headerComponent = JsLibraryTableHeaderComponent; + + this.config.entityTitle = (resource) => resource ? + resource.title : ''; + + this.config.columns.push( + new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '150px'), + new EntityTableColumn('title', 'resource.title', '60%'), + new EntityTableColumn('resourceSubType', 'javascript.javascript-type', '40%', + entity => this.translate.instant(ResourceSubTypeTranslationMap.get(entity.resourceSubType))), + new EntityTableColumn('tenantId', 'resource.system', '60px', + entity => checkBoxCell(entity.tenantId.id === NULL_UUID)), + ); + + this.config.cellActionDescriptors.push( + { + name: this.translate.instant('javascript.download'), + icon: 'file_download', + isEnabled: () => true, + onAction: ($event, entity) => this.downloadResource($event, entity) + } + ); + + this.config.deleteEntityTitle = resource => this.translate.instant('javascript.delete-javascript-resource-title', + { resourceTitle: resource.title }); + this.config.deleteEntityContent = () => this.translate.instant('javascript.delete-javascript-resource-text'); + this.config.deleteEntitiesTitle = count => this.translate.instant('javascript.delete-javascript-resources-title', {count}); + this.config.deleteEntitiesContent = () => this.translate.instant('javascript.delete-javascript-resources-text'); + + this.config.entitiesFetchFunction = pageLink => this.resourceService.getResources(pageLink, ResourceType.JS_MODULE, this.config.componentsData.resourceSubType); + this.config.loadEntity = id => { + const current = this.config.getTable()?.dataSource?.currentEntity as ResourceInfo; + if (!current || current?.resourceSubType === ResourceSubType.MODULE) { + return this.resourceService.getResource(id.id); + } else { + return this.resourceService.getResourceInfoById(id.id) + } + }; + this.config.saveEntity = resource => { + let saveObservable = this.resourceService.saveResource(resource); + if (resource.resourceSubType === ResourceSubType.MODULE) { + saveObservable = saveObservable.pipe( + switchMap((saved) => this.resourceService.getResource(saved.id.id)) + ); + } + return saveObservable; + }; + this.config.deleteEntity = id => this.resourceService.deleteResource(id.id); + + this.config.onEntityAction = action => this.onResourceAction(action); + } + + resolve(): EntityTableConfig { + this.config.tableTitle = this.translate.instant('javascript.javascript-library'); + this.config.componentsData = { + resourceSubType: '' + }; + 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.detailsReadonly(resource, authUser.authority); + return this.config; + } + + private openResource($event: Event, resourceInfo: ResourceInfo) { + if ($event) { + $event.stopPropagation(); + } + const url = this.router.createUrlTree(['resources', 'javascript-library', resourceInfo.id.id]); + this.router.navigateByUrl(url).then(() => {}); + } + + downloadResource($event: Event, resource: ResourceInfo) { + if ($event) { + $event.stopPropagation(); + } + this.resourceService.downloadResource(resource.id.id).subscribe(); + } + + onResourceAction(action: EntityAction): boolean { + switch (action.action) { + case 'open': + this.openResource(action.event, action.entity); + return true; + case 'downloadResource': + this.downloadResource(action.event, action.entity); + return true; + } + return false; + } + + private detailsReadonly(resource: ResourceInfo, authority: Authority): boolean { + return !this.isResourceEditable(resource, authority); + } + + private isResourceEditable(resource: ResourceInfo, authority: Authority): boolean { + if (authority === Authority.TENANT_ADMIN) { + return resource && resource.tenantId && resource.tenantId.id !== NULL_UUID; + } else { + return authority === Authority.SYS_ADMIN; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-header.component.html b/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-header.component.html new file mode 100644 index 0000000000..c69ee84386 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-header.component.html @@ -0,0 +1,30 @@ + + + javascript.javascript-type + + + {{ "javascript.all-types" | translate }} + + + {{ resourceSubTypesTranslationMap.get(jsResourceSubType) | translate }} + + + diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-header.component.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-header.component.ts new file mode 100644 index 0000000000..d3718c3203 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-header.component.ts @@ -0,0 +1,42 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityTableHeaderComponent } from '@home/components/entity/entity-table-header.component'; +import { Resource, ResourceInfo, ResourceSubType, ResourceSubTypeTranslationMap } from '@shared/models/resource.models'; +import { PageLink } from '@shared/models/page/page-link'; + +@Component({ + selector: 'tb-js-library-table-header', + templateUrl: './js-library-table-header.component.html', + styleUrls: [] +}) +export class JsLibraryTableHeaderComponent extends EntityTableHeaderComponent { + + readonly jsResourceSubTypes: ResourceSubType[] = [ResourceSubType.EXTENSION, ResourceSubType.MODULE]; + readonly resourceSubTypesTranslationMap = ResourceSubTypeTranslationMap; + + constructor(protected store: Store) { + super(store); + } + + jsResourceSubTypeChanged(resourceSubType: ResourceSubType) { + this.entitiesTableConfig.componentsData.resourceSubType = resourceSubType; + this.entitiesTableConfig.getTable().resetSortAndFilter(true); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.html b/ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.html new file mode 100644 index 0000000000..d587629352 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.html @@ -0,0 +1,117 @@ + +
+ + + +
+ +
+
+
+
+
+ + javascript.javascript-type + + + {{ ResourceSubTypeTranslationMap.get(resourceSubType) | translate }} + + + + + resource.title + + + {{ 'resource.title-required' | translate }} + + + {{ 'resource.title-max-length' | translate }} + + + + + +
+ + + +
+
+
+ + resource.file-name + + +
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.ts new file mode 100644 index 0000000000..30ab7eb5e0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.ts @@ -0,0 +1,176 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ChangeDetectorRef, 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, + ResourceSubType, + ResourceSubTypeTranslationMap, + ResourceType, + ResourceTypeExtension, + ResourceTypeMIMETypes +} from '@shared/models/resource.models'; +import { startWith, takeUntil } from 'rxjs/operators'; +import { ActionNotificationShow } from '@core/notification/notification.actions'; +import { isDefinedAndNotNull } from '@core/utils'; +import { getCurrentAuthState } from '@core/auth/auth.selectors'; +import { scadaSymbolGeneralStateHighlightRules } from '@home/pages/scada-symbol/scada-symbol-editor.models'; + +@Component({ + selector: 'tb-js-resource', + templateUrl: './js-resource.component.html' +}) +export class JsResourceComponent extends EntityComponent implements OnInit, OnDestroy { + + readonly ResourceSubType = ResourceSubType; + readonly jsResourceSubTypes: ResourceSubType[] = [ResourceSubType.EXTENSION, ResourceSubType.MODULE]; + readonly ResourceSubTypeTranslationMap = ResourceSubTypeTranslationMap; + readonly maxResourceSize = getCurrentAuthState(this.store).maxResourceSize; + + private destroy$ = new Subject(); + + constructor(protected store: Store, + protected translate: TranslateService, + @Inject('entity') protected entityValue: Resource, + @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig, + public fb: FormBuilder, + protected cd: ChangeDetectorRef) { + super(store, fb, entityValue, entitiesTableConfigValue, cd); + } + + ngOnInit(): void { + super.ngOnInit(); + if (this.isAdd) { + this.observeResourceSubTypeChange(); + } + } + + ngOnDestroy(): void { + super.ngOnDestroy(); + this.destroy$.next(); + this.destroy$.complete(); + } + + hideDelete(): boolean { + if (this.entitiesTableConfig) { + return !this.entitiesTableConfig.deleteEnabled(this.entity); + } else { + return false; + } + } + + buildForm(entity: Resource): FormGroup { + return this.fb.group({ + title: [entity ? entity.title : '', [Validators.required, Validators.maxLength(255)]], + resourceSubType: [entity?.resourceSubType ? entity.resourceSubType : ResourceSubType.EXTENSION, Validators.required], + fileName: [entity ? entity.fileName : null, Validators.required], + data: [entity ? entity.data : null, this.isAdd ? [Validators.required] : []], + content: [entity?.data?.length ? window.atob(entity.data) : '', Validators.required] + }); + } + + updateForm(entity: Resource): void { + this.entityForm.patchValue(entity); + const content = entity.resourceSubType === ResourceSubType.MODULE && entity?.data?.length ? window.atob(entity.data) : ''; + this.entityForm.get('content').patchValue(content); + } + + override updateFormState(): void { + super.updateFormState(); + if (this.isEdit && this.entityForm && !this.isAdd) { + this.entityForm.get('resourceSubType').disable({ emitEvent: false }); + this.updateResourceSubTypeFieldsState(this.entityForm.get('resourceSubType').value); + } + } + + prepareFormValue(formValue: Resource): Resource { + if (this.isEdit && !isDefinedAndNotNull(formValue.data)) { + delete formValue.data; + } + if (formValue.resourceSubType === ResourceSubType.MODULE) { + if (!formValue.fileName) { + formValue.fileName = formValue.title + '.js'; + } + formValue.data = window.btoa((formValue as any).content); + delete (formValue as any).content; + } + return super.prepareFormValue(formValue); + } + + getAllowedExtensions(): string { + return ResourceTypeExtension.get(ResourceType.JS_MODULE); + } + + getAcceptType(): string { + return ResourceTypeMIMETypes.get(ResourceType.JS_MODULE); + } + + convertToBase64File(data: string): string { + return window.btoa(data); + } + + onResourceIdCopied(): void { + this.store.dispatch(new ActionNotificationShow( + { + message: this.translate.instant('resource.idCopiedMessage'), + type: 'success', + duration: 750, + verticalPosition: 'bottom', + horizontalPosition: 'right' + })); + } + + uploadContentFromFile(content: string) { + this.entityForm.get('content').patchValue(content); + this.entityForm.markAsDirty(); + } + + private observeResourceSubTypeChange(): void { + this.entityForm.get('resourceSubType').valueChanges.pipe( + startWith(ResourceSubType.EXTENSION), + takeUntil(this.destroy$) + ).subscribe((subType: ResourceSubType) => this.onResourceSubTypeChange(subType)); + } + + private onResourceSubTypeChange(subType: ResourceSubType): void { + this.updateResourceSubTypeFieldsState(subType); + this.entityForm.patchValue({ + data: null, + fileName: null + }, {emitEvent: false}); + } + + private updateResourceSubTypeFieldsState(subType: ResourceSubType) { + if (subType === ResourceSubType.EXTENSION) { + this.entityForm.get('data').enable({ emitEvent: false }); + this.entityForm.get('fileName').enable({ emitEvent: false }); + this.entityForm.get('content').disable({ emitEvent: false }); + } else { + this.entityForm.get('data').disable({ emitEvent: false }); + this.entityForm.get('fileName').disable({ emitEvent: false }); + this.entityForm.get('content').enable({ emitEvent: false }); + } + } + + protected readonly highlightRules = scadaSymbolGeneralStateHighlightRules; +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html index 0389d32aab..6f60c5b1c5 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html @@ -66,9 +66,9 @@ {{ 'resource.title-max-length' | translate }} - -
+
resource.file-name diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts index e9b602e7d3..df5c311bc9 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts @@ -41,7 +41,7 @@ import { getCurrentAuthState } from '@core/auth/auth.selectors'; export class ResourcesLibraryComponent extends EntityComponent implements OnInit, OnDestroy { readonly resourceType = ResourceType; - readonly resourceTypes: ResourceType[] = Object.values(this.resourceType); + readonly resourceTypes = [ResourceType.LWM2M_MODEL, ResourceType.PKCS_12, ResourceType.JKS]; readonly resourceTypesTranslationMap = ResourceTypeTranslationMap; readonly maxResourceSize = getCurrentAuthState(this.store).maxResourceSize; @@ -80,7 +80,7 @@ export class ResourcesLibraryComponent extends EntityComponent impleme buildForm(entity: Resource): FormGroup { return this.fb.group({ title: [entity ? entity.title : '', [Validators.required, Validators.maxLength(255)]], - resourceType: [entity?.resourceType ? entity.resourceType : ResourceType.JS_MODULE, Validators.required], + resourceType: [entity?.resourceType ? entity.resourceType : ResourceType.LWM2M_MODEL, Validators.required], fileName: [entity ? entity.fileName : null, Validators.required], data: [entity ? entity.data : null, this.isAdd ? [Validators.required] : []] }); @@ -94,9 +94,7 @@ export class ResourcesLibraryComponent extends EntityComponent impleme super.updateFormState(); if (this.isEdit && this.entityForm && !this.isAdd) { this.entityForm.get('resourceType').disable({ emitEvent: false }); - if (this.entityForm.get('resourceType').value !== ResourceType.JS_MODULE) { - this.entityForm.get('fileName').disable({ emitEvent: false }); - } + this.entityForm.get('fileName').disable({ emitEvent: false }); } } @@ -140,7 +138,7 @@ export class ResourcesLibraryComponent extends EntityComponent impleme private observeResourceTypeChange(): void { this.entityForm.get('resourceType').valueChanges.pipe( - startWith(ResourceType.JS_MODULE), + startWith(ResourceType.LWM2M_MODEL), takeUntil(this.destroy$) ).subscribe((type: ResourceType) => this.onResourceTypeChange(type)); } diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-table-header.component.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-table-header.component.ts index 85fab10661..c250d68fac 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-table-header.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-table-header.component.ts @@ -28,7 +28,7 @@ import { PageLink } from '@shared/models/page/page-link'; }) export class ResourcesTableHeaderComponent extends EntityTableHeaderComponent { - readonly resourceTypes: ResourceType[] = Object.values(ResourceType); + readonly resourceTypes = [ResourceType.LWM2M_MODEL, ResourceType.PKCS_12, ResourceType.JKS]; readonly resourceTypesTranslationMap = ResourceTypeTranslationMap; constructor(protected store: Store) { diff --git a/ui-ngx/src/app/shared/components/file-input.component.html b/ui-ngx/src/app/shared/components/file-input.component.html index 6302caa36d..e4fa14f8a9 100644 --- a/ui-ngx/src/app/shared/components/file-input.component.html +++ b/ui-ngx/src/app/shared/components/file-input.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+