From 5790ee16426e8cd17a62a17b1dd5fca59b4516ec Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Fri, 11 Oct 2024 11:24:39 +0300 Subject: [PATCH] UI: Add mobile center menu item and implements application page --- .../common/data/mobile/MobileAppStatus.java | 1 + .../src/app/core/http/mobile-app.service.ts | 36 +++-- .../core/http/mobile-application.service.ts | 12 +- ui-ngx/src/app/core/services/menu.models.ts | 68 +++++--- .../entity/entities-table.component.html | 4 +- .../entity/entities-table.component.scss | 11 ++ .../lib/mobile-app-qrcode-widget.component.ts | 8 +- .../admin/mobile-app-settings.component.ts | 6 +- .../mobile-app-table-config.resolver.ts | 143 ---------------- .../mobile-apps/mobile-app.component.html | 76 --------- .../mobile-apps/mobile-app.component.ts | 106 ------------ .../admin/oauth2/oauth2-routing.module.ts | 18 +-- .../home/pages/admin/oauth2/oauth2.module.ts | 6 +- .../modules/home/pages/home-pages.module.ts | 2 + .../applications-routing.module.ts | 84 ++++++++++ .../applications/applications.module.ts | 37 +++++ .../mobile-app-table-config.resolver.ts | 151 +++++++++++++++++ .../mobile-app-table-header.component.html | 12 +- .../mobile-app-table-header.component.scss | 23 +++ .../mobile-app-table-header.component.ts | 10 +- .../applications/mobile-app.component.html | 145 +++++++++++++++++ .../applications/mobile-app.component.scss | 18 +++ .../applications/mobile-app.component.ts | 153 ++++++++++++++++++ .../pages/mobile/mobile-routing.module.ts | 78 +++++++++ .../home/pages/mobile/mobile.module.ts | 33 ++++ .../app/shared/models/entity-type.models.ts | 7 +- .../shared/models/id/mobile-app-bundle-id.ts | 27 ++++ ui-ngx/src/app/shared/models/id/public-api.ts | 2 + .../app/shared/models/mobile-app.models.ts | 95 ++++++++++- ui-ngx/src/app/shared/models/oauth2.models.ts | 12 -- .../assets/locale/locale.constant-en_US.json | 45 ++++++ 31 files changed, 1008 insertions(+), 421 deletions(-) delete mode 100644 ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-config.resolver.ts delete mode 100644 ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app.component.html delete mode 100644 ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/applications/applications-routing.module.ts create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/applications/applications.module.ts create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-config.resolver.ts rename ui-ngx/src/app/modules/home/pages/{admin/oauth2/mobile-apps => mobile/applications}/mobile-app-table-header.component.html (53%) create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-header.component.scss rename ui-ngx/src/app/modules/home/pages/{admin/oauth2/mobile-apps => mobile/applications}/mobile-app-table-header.component.ts (82%) create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/mobile-routing.module.ts create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/mobile.module.ts create mode 100644 ui-ngx/src/app/shared/models/id/mobile-app-bundle-id.ts diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppStatus.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppStatus.java index f1043c732d..3ffe9cbf10 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppStatus.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppStatus.java @@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.mobile; public enum MobileAppStatus { + DRAFT, PUBLISHED, DEPRECATED, SUSPENDED diff --git a/ui-ngx/src/app/core/http/mobile-app.service.ts b/ui-ngx/src/app/core/http/mobile-app.service.ts index a506d2805e..ac4756f26f 100644 --- a/ui-ngx/src/app/core/http/mobile-app.service.ts +++ b/ui-ngx/src/app/core/http/mobile-app.service.ts @@ -18,9 +18,9 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; import { Observable } from 'rxjs'; -import { MobileApp, MobileAppInfo } from '@shared/models/oauth2.models'; import { PageLink } from '@shared/models/page/page-link'; import { PageData } from '@shared/models/page/page-data'; +import { MobileApp, MobileAppBundle, MobileAppBundleInfo } from '@shared/models/mobile-app.models'; @Injectable({ providedIn: 'root' @@ -32,25 +32,37 @@ export class MobileAppService { ) { } - public saveMobileApp(mobileApp: MobileApp, oauth2ClientIds: Array, config?: RequestConfig): Observable { - return this.http.post(`/api/mobileApp?oauth2ClientIds=${oauth2ClientIds.join(',')}`, - mobileApp, defaultHttpOptionsFromConfig(config)); + public saveMobileApp(mobileApp: MobileApp, config?: RequestConfig): Observable { + return this.http.post(`/api/mobile/app`, mobileApp, defaultHttpOptionsFromConfig(config)); } - public updateOauth2Clients(id: string, oauth2ClientRegistrationIds: Array, config?: RequestConfig): Observable { - return this.http.put(`/api/mobileApp/${id}/oauth2Clients`, oauth2ClientRegistrationIds, defaultHttpOptionsFromConfig(config)); + public getTenantMobileAppInfos(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/mobile/app${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); } - public getTenantMobileAppInfos(pageLink: PageLink, config?: RequestConfig): Observable> { - return this.http.get>(`/api/mobileApp/infos${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + public getMobileAppInfoById(id: string, config?: RequestConfig): Observable { + return this.http.get(`/api/mobile/app/${id}`, defaultHttpOptionsFromConfig(config)); } - public getMobileAppInfoById(id: string, config?: RequestConfig): Observable { - return this.http.get(`/api/mobileApp/info/${id}`, defaultHttpOptionsFromConfig(config)); + public deleteMobileApp(id: string, config?: RequestConfig): Observable { + return this.http.delete(`/api/mobile/app/${id}`, defaultHttpOptionsFromConfig(config)); } - public deleteMobileApp(id: string, config?: RequestConfig): Observable { - return this.http.delete(`/api/mobileApp/${id}`, defaultHttpOptionsFromConfig(config)); + public saveMobileAppBundle(mobileAppBundle: MobileAppBundle, oauth2ClientIds?: Array, config?: RequestConfig) { + return this.http.post(`/api/mobile/bundle${oauth2ClientIds ? '?oauth2ClientIds=' + oauth2ClientIds.join(',') : ''}`, + mobileAppBundle, defaultHttpOptionsFromConfig(config)); + } + + public updateOauth2Clients(id: string, oauth2ClientIds?: Array, config?: RequestConfig) { + return this.http.put(`/mobile/bundle/${id}/oauth2Clients`, oauth2ClientIds, defaultHttpOptionsFromConfig(config)); + } + + public getTenantMobileAppBundleInfos(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/mobile/bundle/infos${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + } + + public getMobileAppBundleInfoById(id: string, config?: RequestConfig): Observable { + return this.http.get(`/api/mobile/bundle/infos/${id}`, defaultHttpOptionsFromConfig(config)); } } diff --git a/ui-ngx/src/app/core/http/mobile-application.service.ts b/ui-ngx/src/app/core/http/mobile-application.service.ts index 17ebdd3fef..3dfbf8a4bf 100644 --- a/ui-ngx/src/app/core/http/mobile-application.service.ts +++ b/ui-ngx/src/app/core/http/mobile-application.service.ts @@ -18,7 +18,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; import { Observable } from 'rxjs'; -import { MobileAppSettings } from '@shared/models/mobile-app.models'; +import { QrCodeSettings } from '@shared/models/mobile-app.models'; @Injectable({ providedIn: 'root' @@ -29,16 +29,16 @@ export class MobileApplicationService { private http: HttpClient ) {} - public getMobileAppSettings(config?: RequestConfig): Observable { - return this.http.get(`/api/mobile/app/settings`, defaultHttpOptionsFromConfig(config)); + public getMobileAppSettings(config?: RequestConfig): Observable { + return this.http.get(`/api/mobile/qr/settings`, defaultHttpOptionsFromConfig(config)); } - public saveMobileAppSettings(mobileAppSettings: MobileAppSettings, config?: RequestConfig): Observable { - return this.http.post(`/api/mobile/app/settings`, mobileAppSettings, defaultHttpOptionsFromConfig(config)); + public saveMobileAppSettings(mobileAppSettings: QrCodeSettings, config?: RequestConfig): Observable { + return this.http.post(`/api/mobile/qr/settings`, mobileAppSettings, defaultHttpOptionsFromConfig(config)); } public getMobileAppDeepLink(config?: RequestConfig): Observable { - return this.http.get(`/api/mobile/deepLink`, defaultHttpOptionsFromConfig(config)); + return this.http.get(`/api/mobile/qr/deepLink`, 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 ee4c36b6f2..debaefe882 100644 --- a/ui-ngx/src/app/core/services/menu.models.ts +++ b/ui-ngx/src/app/core/services/menu.models.ts @@ -65,6 +65,9 @@ export enum MenuId { notification_recipients = 'notification_recipients', notification_templates = 'notification_templates', notification_rules = 'notification_rules', + mobile_center = 'mobile_center', + mobile_apps = 'mobile_apps', + mobile_app_settings = 'mobile_app_settings', settings = 'settings', general = 'general', mail_server = 'mail_server', @@ -73,13 +76,11 @@ export enum MenuId { repository_settings = 'repository_settings', auto_commit_settings = 'auto_commit_settings', queues = 'queues', - mobile_app_settings = 'mobile_app_settings', security_settings = 'security_settings', security_settings_general = 'security_settings_general', two_fa = 'two_fa', oauth2 = 'oauth2', domains = 'domains', - mobile_apps = 'mobile_apps', clients = 'clients', audit_log = 'audit_log', alarms = 'alarms', @@ -271,6 +272,37 @@ export const menuSectionMap = new Map([ icon: 'mdi:message-cog' } ], + [ + MenuId.mobile_center, + { + id: MenuId.mobile_center, + name: 'mobile.mobile-center', + type: 'link', + path: '/mobile-center', + icon: 'smartphone' + } + ], + [ + MenuId.mobile_apps, + { + id: MenuId.mobile_apps, + name: 'mobile.applications', + type: 'link', + path: '/mobile-center/applications', + icon: 'list' + } + ], + [ + MenuId.mobile_app_settings, + { + id: MenuId.mobile_app_settings, + name: 'admin.mobile-app.mobile-app', + fullName: 'admin.mobile-app.mobile-app', + type: 'link', + path: '/mobile-center/mobile-app', + icon: 'smartphone' + } + ], [ MenuId.settings, { @@ -356,17 +388,6 @@ export const menuSectionMap = new Map([ icon: 'swap_calls' } ], - [ - MenuId.mobile_app_settings, - { - id: MenuId.mobile_app_settings, - name: 'admin.mobile-app.mobile-app', - fullName: 'admin.mobile-app.mobile-app', - type: 'link', - path: '/settings/mobile-app', - icon: 'smartphone' - } - ], [ MenuId.security_settings, { @@ -418,16 +439,6 @@ export const menuSectionMap = new Map([ icon: 'domain' } ], - [ - MenuId.mobile_apps, - { - id: MenuId.mobile_apps, - name: 'admin.oauth2.mobile-apps', - type: 'link', - path: '/security-settings/oauth2/mobile-applications', - icon: 'smartphone' - } - ], [ MenuId.clients, { @@ -687,14 +698,20 @@ const defaultUserMenuMap = new Map([ {id: MenuId.notification_rules} ] }, + { + id: MenuId.mobile_center, + pages: [ + {id: MenuId.mobile_apps}, + {id: MenuId.mobile_app_settings} + ] + }, { id: MenuId.settings, pages: [ {id: MenuId.general}, {id: MenuId.mail_server}, {id: MenuId.notification_settings}, - {id: MenuId.queues}, - {id: MenuId.mobile_app_settings} + {id: MenuId.queues} ] }, { @@ -706,7 +723,6 @@ const defaultUserMenuMap = new Map([ id: MenuId.oauth2, pages: [ {id: MenuId.domains}, - {id: MenuId.mobile_apps}, {id: MenuId.clients} ] } diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html index 9bf9443404..1700938611 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html @@ -159,7 +159,8 @@ [fxHide.lt-lg]="column.mobileHide" *matHeaderCellDef [ngStyle]="headerCellStyle(column)" mat-sort-header [disabled]="!column.sortable"> {{ column.ignoreTranslate ? column.title : (column.title | translate) }} - (); private widgetResize$: ResizeObserver; - private mobileAppSettingsValue: MobileAppSettings; + private mobileAppSettingsValue: QrCodeSettings; private deepLink: string; private deepLinkTTL: number; private deepLinkTTLTimeoutID: NodeJS.Timeout; @@ -65,13 +65,13 @@ export class MobileAppQrcodeWidgetComponent extends PageComponent implements OnI widgetTitlePanel: TemplateRef; @Input() - set mobileAppSettings(settings: MobileAppSettings) { + set mobileAppSettings(settings: QrCodeSettings) { if (settings) { this.mobileAppSettingsValue = settings; } }; - get mobileAppSettings(): MobileAppSettings { + get mobileAppSettings(): QrCodeSettings { return this.mobileAppSettingsValue; } diff --git a/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts index 7f2a48253f..d50a888d4a 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts @@ -25,7 +25,7 @@ import { MobileApplicationService } from '@core/http/mobile-application.service' import { BadgePosition, badgePositionTranslationsMap, - MobileAppSettings + QrCodeSettings } from '@shared/models/mobile-app.models'; import { ActionUpdateMobileQrCodeEnabled } from '@core/auth/auth.actions'; @@ -38,7 +38,7 @@ export class MobileAppSettingsComponent extends PageComponent implements HasConf mobileAppSettingsForm: FormGroup; - mobileAppSettings: MobileAppSettings; + mobileAppSettings: QrCodeSettings; private readonly destroy$ = new Subject(); @@ -150,7 +150,7 @@ export class MobileAppSettingsComponent extends PageComponent implements HasConf }); } - private processMobileAppSettings(mobileAppSettings: MobileAppSettings): void { + private processMobileAppSettings(mobileAppSettings: QrCodeSettings): void { this.mobileAppSettings = {...mobileAppSettings}; this.mobileAppSettingsForm.reset(this.mobileAppSettings); } diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-config.resolver.ts deleted file mode 100644 index 1f81196d11..0000000000 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-config.resolver.ts +++ /dev/null @@ -1,143 +0,0 @@ -/// -/// 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 { ActivatedRouteSnapshot } from '@angular/router'; -import { - CellActionDescriptorType, - DateEntityTableColumn, - EntityActionTableColumn, - EntityChipsEntityTableColumn, - EntityTableColumn, - EntityTableConfig -} from '@home/models/entity/entities-table-config.models'; -import { MobileApp, MobileAppInfo } from '@shared/models/oauth2.models'; -import { TranslateService } from '@ngx-translate/core'; -import { DatePipe } from '@angular/common'; -import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; -import { isEqual } from '@core/utils'; -import { Direction } from '@app/shared/models/page/sort-order'; -import { MobileAppService } from '@core/http/mobile-app.service'; -import { MobileAppComponent } from '@home/pages/admin/oauth2/mobile-apps/mobile-app.component'; -import { MobileAppTableHeaderComponent } from '@home/pages/admin/oauth2/mobile-apps/mobile-app-table-header.component'; -import { map, Observable, of, mergeMap } from 'rxjs'; - -@Injectable() -export class MobileAppTableConfigResolver { - - private readonly config: EntityTableConfig = new EntityTableConfig(); - - constructor(private translate: TranslateService, - private datePipe: DatePipe, - private mobileAppService: MobileAppService) { - this.config.tableTitle = this.translate.instant('admin.oauth2.mobile-apps'); - this.config.selectionEnabled = false; - this.config.entityType = EntityType.MOBILE_APP; - this.config.rowPointer = true; - this.config.entityTranslations = entityTypeTranslations.get(EntityType.MOBILE_APP); - this.config.entityResources = entityTypeResources.get(EntityType.MOBILE_APP); - this.config.entityComponent = MobileAppComponent; - this.config.headerComponent = MobileAppTableHeaderComponent; - this.config.addDialogStyle = {width: '850px', maxHeight: '100vh'}; - this.config.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC}; - - this.config.columns.push( - new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '170px'), - new EntityTableColumn('pkgName', 'admin.oauth2.mobile-package', '170px'), - new EntityTableColumn('appSecret', 'admin.oauth2.mobile-app-secret', '350px', - (entity) => entity.appSecret ? this.appSecretText(entity) : '', () => ({}), - false, () => ({}), () => undefined, false, - { - name: this.translate.instant('admin.oauth2.copy-mobile-app-secret'), - icon: 'content_copy', - style: { - padding: '4px', - 'font-size': '16px', - color: 'rgba(0,0,0,.87)' - }, - isEnabled: (entity) => !!entity.appSecret, - onAction: ($event, entity) => entity.appSecret, - type: CellActionDescriptorType.COPY_BUTTON - }), - new EntityChipsEntityTableColumn('oauth2ClientInfos', 'admin.oauth2.clients', '20%'), - new EntityActionTableColumn('oauth2Enabled', 'admin.oauth2.enable', - { - name: '', - nameFunction: (app) => - this.translate.instant(app.oauth2Enabled ? 'admin.oauth2.disable' : 'admin.oauth2.enable'), - icon: 'mdi:toggle-switch', - iconFunction: (entity) => entity.oauth2Enabled ? 'mdi:toggle-switch' : 'mdi:toggle-switch-off-outline', - isEnabled: () => true, - onAction: ($event, entity) => this.toggleEnableOAuth($event, entity) - }) - ); - - this.config.deleteEntityTitle = (app) => this.translate.instant('admin.oauth2.delete-mobile-app-title', {applicationName: app.pkgName}); - this.config.deleteEntityContent = () => this.translate.instant('admin.oauth2.delete-mobile-app-text'); - this.config.entitiesFetchFunction = pageLink => this.mobileAppService.getTenantMobileAppInfos(pageLink); - this.config.loadEntity = id => this.mobileAppService.getMobileAppInfoById(id.id); - this.config.saveEntity = (mobileApp, originalMobileApp) => { - const clientsIds = mobileApp.oauth2ClientInfos as Array || []; - let clientsTask: Observable; - if (mobileApp.id && !isEqual(mobileApp.oauth2ClientInfos?.sort(), - originalMobileApp.oauth2ClientInfos?.map(info => info.id ? info.id.id : info).sort())) { - clientsTask = this.mobileAppService.updateOauth2Clients(mobileApp.id.id, clientsIds); - } else { - clientsTask = of(null); - } - delete mobileApp.oauth2ClientInfos; - return clientsTask.pipe( - mergeMap(() => this.mobileAppService.saveMobileApp(mobileApp as MobileApp, mobileApp.id ? [] : clientsIds)), - map(savedMobileApp => { - (savedMobileApp as MobileAppInfo).oauth2ClientInfos = clientsIds; - return savedMobileApp; - }) - ); - }; - this.config.deleteEntity = id => this.mobileAppService.deleteMobileApp(id.id); - } - - resolve(route: ActivatedRouteSnapshot): EntityTableConfig { - return this.config; - } - - private toggleEnableOAuth($event: Event, mobileApp: MobileAppInfo): void { - if ($event) { - $event.stopPropagation(); - } - - const modifiedMobileApp: MobileAppInfo = { - ...mobileApp, - oauth2Enabled: !mobileApp.oauth2Enabled - }; - - this.mobileAppService.saveMobileApp(modifiedMobileApp, mobileApp.oauth2ClientInfos.map(clientInfo => clientInfo.id.id), - {ignoreLoading: true}) - .subscribe((result) => { - mobileApp.oauth2Enabled = result.oauth2Enabled; - this.config.getTable().detectChanges(); - }); - } - - private appSecretText(entity): string { - let text = entity.appSecret; - if (text.length > 35) { - text = `${text.slice(0, 35)}…`; - } - return text; - } - -} diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app.component.html b/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app.component.html deleted file mode 100644 index a613890c95..0000000000 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app.component.html +++ /dev/null @@ -1,76 +0,0 @@ - -
-
- - admin.oauth2.mobile-package - - admin.oauth2.mobile-package-hint - - {{ 'admin.oauth2.mobile-package-unique' | translate }} - - - {{ 'admin.oauth2.mobile-package-max-length' | translate }} - - - {{ 'admin.oauth2.mobile-package-spaces' | translate }} - - -
-
- - admin.oauth2.mobile-app-secret - - - - admin.oauth2.mobile-app-secret-hint - - {{ 'admin.oauth2.mobile-app-secret-required' | translate }} - - - {{ 'admin.oauth2.mobile-app-secret-min-length' | translate }} - - - {{ 'admin.oauth2.mobile-app-secret-base64' | translate }} - - -
-
- - {{ 'admin.oauth2.enable' | translate }} - -
- - - -
- diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app.component.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app.component.ts deleted file mode 100644 index a01699bdda..0000000000 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app.component.ts +++ /dev/null @@ -1,106 +0,0 @@ -/// -/// 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 } from '@angular/core'; -import { EntityComponent } from '@home/components/entity/entity.component'; -import { MobileAppInfo } from '@shared/models/oauth2.models'; -import { AppState } from '@core/core.state'; -import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; -import { TranslateService } from '@ngx-translate/core'; -import { Store } from '@ngrx/store'; -import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, Validators } from '@angular/forms'; -import { isDefinedAndNotNull, randomAlphanumeric } from '@core/utils'; -import { MatDialog } from '@angular/material/dialog'; -import { ClientDialogComponent } from '@home/pages/admin/oauth2/clients/client-dialog.component'; -import { EntityType } from '@shared/models/entity-type.models'; - -@Component({ - selector: 'tb-mobile-app', - templateUrl: './mobile-app.component.html', - styleUrls: [] -}) -export class MobileAppComponent extends EntityComponent { - - entityType = EntityType; - - constructor(protected store: Store, - protected translate: TranslateService, - @Inject('entity') protected entityValue: MobileAppInfo, - @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig, - protected cd: ChangeDetectorRef, - public fb: UntypedFormBuilder, - private dialog: MatDialog) { - super(store, fb, entityValue, entitiesTableConfigValue, cd); - } - - buildForm(entity: MobileAppInfo): UntypedFormGroup { - return this.fb.group({ - pkgName: [entity?.pkgName ? entity.pkgName : '', [Validators.required, Validators.maxLength(255), - Validators.pattern(/^\S+$/)]], - appSecret: [entity?.appSecret ? entity.appSecret : btoa(randomAlphanumeric(64)), - [Validators.required, this.base64Format]], - oauth2Enabled: isDefinedAndNotNull(entity?.oauth2Enabled) ? entity.oauth2Enabled : true, - oauth2ClientInfos: entity?.oauth2ClientInfos ? entity.oauth2ClientInfos.map(info => info.id.id) : [] - }); - } - - updateForm(entity: MobileAppInfo) { - this.entityForm.patchValue({ - pkgName: entity.pkgName, - appSecret: entity.appSecret, - oauth2Enabled: entity.oauth2Enabled, - oauth2ClientInfos: entity.oauth2ClientInfos?.map(info => info.id ? info.id.id : info) - }); - } - - createClient($event: Event) { - if ($event) { - $event.stopPropagation(); - $event.preventDefault(); - } - this.dialog.open(ClientDialogComponent, { - disableClose: true, - panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], - data: {} - }).afterClosed() - .subscribe((client) => { - if (client) { - const formValue = this.entityForm.get('oauth2ClientInfos').value ? - [...this.entityForm.get('oauth2ClientInfos').value] : []; - formValue.push(client.id.id); - this.entityForm.get('oauth2ClientInfos').patchValue(formValue); - this.entityForm.get('oauth2ClientInfos').markAsDirty(); - } - }); - } - - private base64Format(control: UntypedFormControl): { [key: string]: boolean } | null { - if (control.value === '') { - return null; - } - try { - const value = atob(control.value); - if (value.length < 64) { - return {minLength: true}; - } - return null; - } catch (e) { - return {base64: true}; - } - } - -} diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/oauth2-routing.module.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/oauth2-routing.module.ts index cba52e977c..9286304312 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2/oauth2-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/oauth2-routing.module.ts @@ -26,7 +26,6 @@ import { DomainTableConfigResolver } from '@home/pages/admin/oauth2/domains/doma import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component'; import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models'; import { BreadCrumbConfig } from '@shared/components/breadcrumb'; -import { MobileAppTableConfigResolver } from '@home/pages/admin/oauth2/mobile-apps/mobile-app-table-config.resolver'; import { MenuId } from '@core/services/menu.models'; @Injectable() @@ -74,20 +73,6 @@ export const oAuth2Routes: Routes = [ entitiesTableConfig: DomainTableConfigResolver } }, - { - path: 'mobile-applications', - component: EntitiesTableComponent, - data: { - auth: [Authority.SYS_ADMIN], - title: 'admin.oauth2.mobile-apps', - breadcrumb: { - menuId: MenuId.mobile_apps - } - }, - resolve: { - entitiesTableConfig: MobileAppTableConfigResolver - } - }, { path: 'clients', data: { @@ -140,8 +125,7 @@ export const oAuth2Routes: Routes = [ providers: [ OAuth2LoginProcessingUrlResolver, ClientsTableConfigResolver, - DomainTableConfigResolver, - MobileAppTableConfigResolver + DomainTableConfigResolver ], imports: [RouterModule.forChild(oAuth2Routes)], exports: [RouterModule] diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/oauth2.module.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/oauth2.module.ts index 545b9a6a63..8c80d6775a 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2/oauth2.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/oauth2.module.ts @@ -24,8 +24,6 @@ import { ClientTableHeaderComponent } from '@home/pages/admin/oauth2/clients/cli import { DomainComponent } from '@home/pages/admin/oauth2/domains/domain.component'; import { ClientDialogComponent } from '@home/pages/admin/oauth2/clients/client-dialog.component'; import { DomainTableHeaderComponent } from '@home/pages/admin/oauth2/domains/domain-table-header.component'; -import { MobileAppComponent } from '@home/pages/admin/oauth2/mobile-apps/mobile-app.component'; -import { MobileAppTableHeaderComponent } from '@home/pages/admin/oauth2/mobile-apps/mobile-app-table-header.component'; @NgModule({ declarations: [ @@ -33,9 +31,7 @@ import { MobileAppTableHeaderComponent } from '@home/pages/admin/oauth2/mobile-a ClientDialogComponent, ClientTableHeaderComponent, DomainComponent, - DomainTableHeaderComponent, - MobileAppComponent, - MobileAppTableHeaderComponent + DomainTableHeaderComponent ], imports: [ Oauth2RoutingModule, diff --git a/ui-ngx/src/app/modules/home/pages/home-pages.module.ts b/ui-ngx/src/app/modules/home/pages/home-pages.module.ts index 0b1a70996e..cd930064b6 100644 --- a/ui-ngx/src/app/modules/home/pages/home-pages.module.ts +++ b/ui-ngx/src/app/modules/home/pages/home-pages.module.ts @@ -44,6 +44,7 @@ import { FeaturesModule } from '@home/pages/features/features.module'; import { NotificationModule } from '@home/pages/notification/notification.module'; import { AccountModule } from '@home/pages/account/account.module'; import { ScadaSymbolModule } from '@home/pages/scada-symbol/scada-symbol.module'; +import { MobileModule } from '@home/pages/mobile/mobile.module'; @NgModule({ exports: [ @@ -58,6 +59,7 @@ import { ScadaSymbolModule } from '@home/pages/scada-symbol/scada-symbol.module' ProfilesModule, EntitiesModule, FeaturesModule, + MobileModule, NotificationModule, DeviceModule, AssetModule, diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/applications-routing.module.ts b/ui-ngx/src/app/modules/home/pages/mobile/applications/applications-routing.module.ts new file mode 100644 index 0000000000..59e6917371 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/applications-routing.module.ts @@ -0,0 +1,84 @@ +/// +/// 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 { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { Authority } from '@shared/models/authority.enum'; +import { MenuId } from '@core/services/menu.models'; +import { EntitiesTableComponent } from '@home/components/entity/entities-table.component'; +import { MobileAppTableConfigResolver } from '@home/pages/mobile/applications/mobile-app-table-config.resolver'; +import { DevicesTableConfigResolver } from '@home/pages/device/devices-table-config.resolver'; +import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component'; +import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; +import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models'; +import { BreadCrumbConfig } from '@shared/components/breadcrumb'; + +export const applicationsRoutes: Routes = [ + { + path: 'applications', + data: { + breadcrumb: { + menuId: MenuId.mobile_apps + } + }, + children: [ + { + path: '', + component: EntitiesTableComponent, + data: { + auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN], + title: 'mobile.applications', + }, + resolve: { + entitiesTableConfig: MobileAppTableConfigResolver + } + }, + { + path: ':entityId', + component: EntityDetailsPageComponent, + canDeactivate: [ConfirmOnExitGuard], + data: { + breadcrumb: { + labelFunction: entityDetailsPageBreadcrumbLabelFunction, + icon: 'list' + } as BreadCrumbConfig, + auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN], + title: 'mobile.applications', + }, + resolve: { + entitiesTableConfig: MobileAppTableConfigResolver + } + } + ] + } +]; + +const routes: Routes = [ + { + path: 'security-settings/oauth2/mobile-applications', + pathMatch: 'full', + redirectTo: '/mobile-center/applications' + } +]; + +@NgModule({ + providers: [ + MobileAppTableConfigResolver + ], + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class ApplicationsRoutingModule { } diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/applications.module.ts b/ui-ngx/src/app/modules/home/pages/mobile/applications/applications.module.ts new file mode 100644 index 0000000000..c213ab9ee6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/applications.module.ts @@ -0,0 +1,37 @@ +/// +/// 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 { NgModule } from '@angular/core'; +import { MobileAppComponent } from '@home/pages/mobile/applications/mobile-app.component'; +import { MobileAppTableHeaderComponent } from '@home/pages/mobile/applications/mobile-app-table-header.component'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { HomeComponentsModule } from '@home/components/home-components.module'; +import { ApplicationsRoutingModule } from '@home/pages/mobile/applications/applications-routing.module'; + +@NgModule({ + declarations: [ + MobileAppComponent, + MobileAppTableHeaderComponent + ], + imports: [ + CommonModule, + SharedModule, + HomeComponentsModule, + ApplicationsRoutingModule + ] +}) +export class ApplicationModule { } diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-config.resolver.ts new file mode 100644 index 0000000000..2c86f6124c --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-config.resolver.ts @@ -0,0 +1,151 @@ +/// +/// 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 { ActivatedRouteSnapshot } from '@angular/router'; +import { + CellActionDescriptorType, + DateEntityTableColumn, + EntityTableColumn, + EntityTableConfig +} from '@home/models/entity/entities-table-config.models'; +import { TranslateService } from '@ngx-translate/core'; +import { DatePipe } from '@angular/common'; +import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { Direction } from '@app/shared/models/page/sort-order'; +import { MobileAppService } from '@core/http/mobile-app.service'; +import { MobileAppComponent } from '@home/pages/mobile/applications/mobile-app.component'; +import { MobileAppTableHeaderComponent } from '@home/pages/mobile/applications/mobile-app-table-header.component'; +import { MobileApp, MobileAppStatus, mobileAppStatusTranslations } from '@shared/models/mobile-app.models'; +import { platformTypeTranslations } from '@shared/models/oauth2.models'; +import { TruncatePipe } from '@shared/pipe/truncate.pipe'; + +@Injectable() +export class MobileAppTableConfigResolver { + + private readonly config: EntityTableConfig = new EntityTableConfig(); + + constructor(private translate: TranslateService, + private datePipe: DatePipe, + private mobileAppService: MobileAppService, + private truncatePipe: TruncatePipe) { + this.config.selectionEnabled = false; + this.config.entityType = EntityType.MOBILE_APP; + this.config.addEnabled = false; + this.config.rowPointer = true; + this.config.entityTranslations = entityTypeTranslations.get(EntityType.MOBILE_APP); + this.config.entityResources = entityTypeResources.get(EntityType.MOBILE_APP); + this.config.entityComponent = MobileAppComponent; + this.config.headerComponent = MobileAppTableHeaderComponent; + this.config.addDialogStyle = {width: '850px', maxHeight: '100vh'}; + this.config.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC}; + + this.config.columns.push( + new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '170px'), + new EntityTableColumn('pkgName', 'mobile.application-package', '20%', (entity) => entity.pkgName ?? '', () => ({}), + false, () => ({}), () => undefined, false, + { + name: this.translate.instant('mobile.copy-application-package'), + icon: 'content_copy', + style: { + padding: '4px', + 'font-size': '16px', + color: 'rgba(0,0,0,.54)' + }, + isEnabled: (entity) => !!entity.pkgName, + onAction: (_$event, entity) => entity.pkgName, + type: CellActionDescriptorType.COPY_BUTTON + }), + new EntityTableColumn('appSecret', 'mobile.application-secret', '15%', + (entity) => this.truncatePipe.transform(entity.appSecret, true, 10, '…'), () => ({}), + false, () => ({}), () => undefined, false, + { + name: this.translate.instant('mobile.copy-application-secret'), + icon: 'content_copy', + style: { + padding: '4px', + 'font-size': '16px', + color: 'rgba(0,0,0,.54)' + }, + isEnabled: (entity) => !!entity.appSecret, + onAction: (_$event, entity) => entity.appSecret, + type: CellActionDescriptorType.COPY_BUTTON + }), + new EntityTableColumn('platformType', 'mobile.platform-type', '15%', + (entity) => this.translate.instant(platformTypeTranslations.get(entity.platformType)) + ), + new EntityTableColumn('status', 'mobile.status', '15%', + (entity) => `${this.mobileStatus(entity.status)}`, + (entity)=> this.mobileStatusStyle(entity.status) + ), + new EntityTableColumn('minVersion', 'mobile.min-version', '15%', + (entity) => entity.versionInfo?.minVersion ?? '', () => ({}), false), + new EntityTableColumn('latestVersion', 'mobile.latest-version', '15%', + (entity) => entity.versionInfo?.latestVersion ?? '', () => ({}), false), + ); + + this.config.deleteEntityTitle = (app) => this.translate.instant('mobile.delete-applications-title', {applicationName: app.pkgName}); + this.config.deleteEntityContent = () => this.translate.instant('mobile.delete-applications-text'); + this.config.entitiesFetchFunction = pageLink => this.mobileAppService.getTenantMobileAppInfos(pageLink); + this.config.loadEntity = id => this.mobileAppService.getMobileAppInfoById(id.id); + this.config.saveEntity = (mobileApp) => this.mobileAppService.saveMobileApp(mobileApp); + this.config.deleteEntity = id => this.mobileAppService.deleteMobileApp(id.id); + } + + resolve(_route: ActivatedRouteSnapshot): EntityTableConfig { + return this.config; + } + + private mobileStatus(status: MobileAppStatus): string { + const translateKey = mobileAppStatusTranslations.get(status); + let backgroundColor = 'rgba(25, 128, 56, 0.06)'; + switch (status) { + case MobileAppStatus.DEPRECATED: + backgroundColor = 'rgba(250, 164, 5, 0.06)'; + break; + case MobileAppStatus.SUSPENDED: + backgroundColor = 'rgba(209, 39, 48, 0.06)'; + break; + case MobileAppStatus.DRAFT: + backgroundColor = 'rgba(160, 160, 160, 0.06)'; + break; + } + return `
+ ${this.translate.instant(translateKey)} +
`; + } + + private mobileStatusStyle(status: MobileAppStatus): object { + const styleObj = { + fontSize: '14px', + color: '#198038' + }; + switch (status) { + case MobileAppStatus.DEPRECATED: + styleObj.color = '#FAA405'; + break; + case MobileAppStatus.SUSPENDED: + styleObj.color = '#D12730'; + break; + case MobileAppStatus.DRAFT: + styleObj.color = '#A0A0A0'; + break; + } + return styleObj; + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-header.component.html b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-header.component.html similarity index 53% rename from ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-header.component.html rename to ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-header.component.html index fd66a026da..1783b2b4dd 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-header.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-header.component.html @@ -15,4 +15,14 @@ limitations under the License. --> -
+
+
+
mobile.applications
+ +
+ +
diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-header.component.scss b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-header.component.scss new file mode 100644 index 0000000000..06f14e4712 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-header.component.scss @@ -0,0 +1,23 @@ +/** + * 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. + */ +:host{ + width: 100000px; + + .tb-entity-title-container { + display: flex; + align-items: center; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-header.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-header.component.ts similarity index 82% rename from ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-header.component.ts rename to ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-header.component.ts index ca4bbaf74b..a5ced5ddb8 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-header.component.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-header.component.ts @@ -18,16 +18,20 @@ import { Component } from '@angular/core'; import { EntityTableHeaderComponent } from '@home/components/entity/entity-table-header.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { MobileAppInfo } from '@shared/models/oauth2.models'; +import { MobileApp } from '@shared/models/mobile-app.models'; @Component({ selector: 'tb-mobile-app-table-header', templateUrl: './mobile-app-table-header.component.html', - styleUrls: [] + styleUrls: ['./mobile-app-table-header.component.scss'] }) -export class MobileAppTableHeaderComponent extends EntityTableHeaderComponent { +export class MobileAppTableHeaderComponent extends EntityTableHeaderComponent { constructor(protected store: Store) { super(store); } + + createMobile($event: Event) { + this.entitiesTableConfig.getTable().addEntity($event); + } } diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.html b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.html new file mode 100644 index 0000000000..bceaa4048f --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.html @@ -0,0 +1,145 @@ + +
+ + mobile.mobile-package + + + + + + {{ 'mobile.mobile-package-required' | translate }} + + + {{ 'mobile.mobile-package-max-length' | translate }} + + + {{ 'mobile.mobile-package-spaces' | translate }} + + + + mobile.platform-type + + + {{ platformTypeTranslations.get(platformType) | translate }} + + + + + mobile.application-secret + +
+ + + +
+ +
+ + mobile.status + + + {{ mobileAppStatusTranslations.get(mobileAppStatus) | translate }} + + + +
+
mobile.version-information
+
+ + mobile.min-version + + + + mobile.latest-version + + +
+
+
+
mobile.store-information
+
+ + {{ + (entityForm.get('platformType').value === PlatformType.ANDROID ? 'mobile.google-play-link' : 'mobile.app-store-link') | translate + }} + + + + + {{ (entityForm.get('platformType').value === PlatformType.ANDROID ? 'mobile.google-play-link-required' : 'mobile.app-store-link-required') | translate }} + + + + mobile.sha256-certificate-fingerprints + + + + + {{ 'mobile.sha256-certificate-fingerprints-required' | translate }} + + + + mobile.app-id + + + + + {{ 'mobile.app-id-required' | translate }} + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.scss b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.scss new file mode 100644 index 0000000000..0fc2bde23d --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.scss @@ -0,0 +1,18 @@ +/** + * 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. + */ +:host { + --mdc-outlined-text-field-outline-color: rgba(0,0,0,0.12); +} diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.ts new file mode 100644 index 0000000000..e801a8966e --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.ts @@ -0,0 +1,153 @@ +/// +/// 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 } from '@angular/core'; +import { EntityComponent } from '@home/components/entity/entity.component'; +import { AppState } from '@core/core.state'; +import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; +import { TranslateService } from '@ngx-translate/core'; +import { Store } from '@ngrx/store'; +import { FormBuilder, FormGroup, UntypedFormControl, Validators } from '@angular/forms'; +import { randomAlphanumeric } from '@core/utils'; +import { EntityType } from '@shared/models/entity-type.models'; +import { MobileApp, MobileAppStatus, mobileAppStatusTranslations } from '@shared/models/mobile-app.models'; +import { PlatformType, platformTypeTranslations } from '@shared/models/oauth2.models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; + +@Component({ + selector: 'tb-mobile-app', + templateUrl: './mobile-app.component.html', + styleUrls: ['./mobile-app.component.scss'] +}) +export class MobileAppComponent extends EntityComponent { + + entityType = EntityType; + + platformTypes = [PlatformType.ANDROID, PlatformType.IOS]; + + MobileAppStatus = MobileAppStatus; + PlatformType = PlatformType; + + mobileAppStatuses = Object.keys(MobileAppStatus) as MobileAppStatus[]; + + platformTypeTranslations = platformTypeTranslations; + mobileAppStatusTranslations = mobileAppStatusTranslations; + + constructor(protected store: Store, + protected translate: TranslateService, + @Inject('entity') protected entityValue: MobileApp, + @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig, + protected cd: ChangeDetectorRef, + public fb: FormBuilder) { + super(store, fb, entityValue, entitiesTableConfigValue, cd); + } + + buildForm(entity: MobileApp): FormGroup { + const form = this.fb.group({ + pkgName: [entity?.pkgName ? entity.pkgName : '', [Validators.required, Validators.maxLength(255), + Validators.pattern(/^\S+$/)]], + platformType: [entity?.platformType ? entity.platformType : PlatformType.ANDROID], + appSecret: [entity?.appSecret ? entity.appSecret : btoa(randomAlphanumeric(64)), [Validators.required, this.base64Format]], + status: [entity?.status ? entity.status : MobileAppStatus.DRAFT], + versionInfo: this.fb.group({ + minVersion: [entity?.versionInfo?.minVersion ? entity.versionInfo.minVersion : ''], + latestVersion: [entity?.versionInfo?.latestVersion ? entity.versionInfo.latestVersion : ''], + }), + storeInfo: this.fb.group({ + storeLink: [entity?.storeInfo?.storeLink ? entity.storeInfo.storeLink : ''], + sha256CertFingerprints: [entity?.storeInfo?.sha256CertFingerprints ? entity.storeInfo.sha256CertFingerprints : ''], + appId: [entity?.storeInfo?.appId ? entity.storeInfo.appId : ''], + }), + }); + + form.get('platformType').valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((value: PlatformType) => { + if (value === PlatformType.ANDROID) { + form.get('storeInfo.sha256CertFingerprints').enable({emitEvent: false}); + form.get('storeInfo.appId').disable({emitEvent: false}); + } else if (value === PlatformType.IOS) { + form.get('storeInfo.sha256CertFingerprints').disable({emitEvent: false}); + form.get('storeInfo.appId').enable({emitEvent: false}); + } + form.get('storeInfo.storeLink').setValue('', {emitEvent: false}); + }); + + form.get('status').valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((value: MobileAppStatus) => { + if (value === MobileAppStatus.PUBLISHED) { + form.get('storeInfo.storeLink').addValidators(Validators.required); + form.get('storeInfo.sha256CertFingerprints').addValidators(Validators.required); + form.get('storeInfo.appId').addValidators(Validators.required); + } else { + form.get('storeInfo.storeLink').clearValidators(); + form.get('storeInfo.sha256CertFingerprints').clearValidators(); + form.get('storeInfo.appId').clearValidators(); + } + form.get('storeInfo.storeLink').updateValueAndValidity({emitEvent: false}); + form.get('storeInfo.sha256CertFingerprints').updateValueAndValidity({emitEvent: false}); + form.get('storeInfo.appId').updateValueAndValidity({emitEvent: false}); + }); + + return form; + } + + updateForm(entity: MobileApp) { + this.entityForm.patchValue(entity, {emitEvent: false}); + } + + override updateFormState(): void { + super.updateFormState(); + if (this.isEdit && this.entityForm && !this.isAdd) { + this.entityForm.get('platformType').disable({emitEvent: false}); + if (this.entityForm.get('platformType').value === PlatformType.ANDROID) { + this.entityForm.get('storeInfo.appId').disable({emitEvent: false}); + } else if (this.entityForm.get('platformType').value === PlatformType.IOS) { + this.entityForm.get('storeInfo.sha256CertFingerprints').disable({emitEvent: false}); + } + } + if (this.entityForm && this.isAdd) { + this.entityForm.get('storeInfo.appId').disable({emitEvent: false}); + } + } + + override prepareFormValue(value: MobileApp): MobileApp { + value.storeInfo = this.entityForm.get('storeInfo').value; + return super.prepareFormValue(value); + } + + generateAppSecret($event: Event) { + $event.stopPropagation(); + this.entityForm.get('appSecret').setValue(btoa(randomAlphanumeric(64))); + this.entityForm.get('appSecret').markAsDirty(); + } + + private base64Format(control: UntypedFormControl): { [key: string]: boolean } | null { + if (control.value === '') { + return null; + } + try { + const value = atob(control.value); + if (value.length < 64) { + return {minLength: true}; + } + return null; + } catch (e) { + return {base64: true}; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/mobile/mobile-routing.module.ts b/ui-ngx/src/app/modules/home/pages/mobile/mobile-routing.module.ts new file mode 100644 index 0000000000..4240bb7b78 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/mobile-routing.module.ts @@ -0,0 +1,78 @@ +/// +/// 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 { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { RouterTabsComponent } from '@home/components/router-tabs.component'; +import { Authority } from '@shared/models/authority.enum'; +import { MenuId } from '@core/services/menu.models'; +import { MobileAppTableConfigResolver } from '@home/pages/mobile/applications/mobile-app-table-config.resolver'; +import { MobileAppSettingsComponent } from '@home/pages/admin/mobile-app-settings.component'; +import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; +import { applicationsRoutes } from '@home/pages/mobile/applications/applications-routing.module'; + +const routes: Routes = [ + { + path: 'mobile-center', + component: RouterTabsComponent, + data: { + auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN], + breadcrumb: { + menuId: MenuId.mobile_center + } + }, + children: [ + { + path: '', + children: [], + data: { + auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER, Authority.SYS_ADMIN], + redirectTo: '/mobile-center/applications' + } + }, + ...applicationsRoutes, + { + path: 'mobile-app', + component: MobileAppSettingsComponent, + canDeactivate: [ConfirmOnExitGuard], + data: { + auth: [Authority.SYS_ADMIN], + title: 'admin.mobile-app.mobile-app', + breadcrumb: { + menuId: MenuId.mobile_app_settings + } + } + } + ] + } +]; + +routes.push( + { + path: 'security-settings/oauth2/mobile-applications', + pathMatch: 'full', + redirectTo: '/mobile-center/applications' + } +); + +@NgModule({ + providers: [ + MobileAppTableConfigResolver + ], + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class MobileRoutingModule { } diff --git a/ui-ngx/src/app/modules/home/pages/mobile/mobile.module.ts b/ui-ngx/src/app/modules/home/pages/mobile/mobile.module.ts new file mode 100644 index 0000000000..477fe4ae18 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/mobile.module.ts @@ -0,0 +1,33 @@ +/// +/// 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 { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { HomeComponentsModule } from '@home/components/home-components.module'; +import { MobileRoutingModule } from '@home/pages/mobile/mobile-routing.module'; +import { ApplicationModule } from '@home/pages/mobile/applications/applications.module'; + +@NgModule({ + imports: [ + CommonModule, + SharedModule, + HomeComponentsModule, + ApplicationModule, + MobileRoutingModule, + ] +}) +export class MobileModule { } diff --git a/ui-ngx/src/app/shared/models/entity-type.models.ts b/ui-ngx/src/app/shared/models/entity-type.models.ts index 51fefefdf2..88a2ff67bf 100644 --- a/ui-ngx/src/app/shared/models/entity-type.models.ts +++ b/ui-ngx/src/app/shared/models/entity-type.models.ts @@ -47,6 +47,7 @@ export enum EntityType { NOTIFICATION_TEMPLATE = 'NOTIFICATION_TEMPLATE', OAUTH2_CLIENT = 'OAUTH2_CLIENT', DOMAIN = 'DOMAIN', + MOBILE_APP_BUNDLE = 'MOBILE_APP_BUNDLE', MOBILE_APP = 'MOBILE_APP' } @@ -458,9 +459,9 @@ export const entityTypeTranslations = new Map([ [BadgePosition.RIGHT, 'admin.mobile-app.right'], [BadgePosition.LEFT, 'admin.mobile-app.left'] ]); + +export type QrCodeConfig = AndroidConfig & IosConfig; + +export enum MobileAppStatus { + DRAFT = 'DRAFT', + PUBLISHED = 'PUBLISHED', + DEPRECATED = 'DEPRECATED', + SUSPENDED = 'SUSPENDED' +} + +export const mobileAppStatusTranslations = new Map( + [ + [MobileAppStatus.DRAFT, 'mobile.status-type.draft'], + [MobileAppStatus.PUBLISHED, 'mobile.status-type.published'], + [MobileAppStatus.DEPRECATED, 'mobile.status-type.deprecated'], + [MobileAppStatus.SUSPENDED, 'mobile.status-type.suspended'], + ] +); + +export interface VersionInfo { + minVersion: string; + minVersionReleaseNotes?: string; + latestVersion: string; + latestVersionReleaseNotes?: string; +} + +export interface StoreInfo { + sha256CertFingerprints?: string; + storeLink: string; + appId?: string; +} + +export interface MobileApp extends BaseData, HasTenantId { + pkgName: string; + appSecret: string; + platformType: PlatformType; + status: MobileAppStatus; + versionInfo: VersionInfo; + storeInfo: StoreInfo; +} + +enum MobileMenuPath { + HOME = 'HOME', + ASSETS = 'ASSETS', + DEVICES = 'DEVICES', + DEVICE_LIST = 'DEVICE_LIST', + ALARMS = 'ALARMS', + DASHBOARDS = 'DASHBOARDS', + DASHBOARD = 'DASHBOARD', + AUDIT_LOGS = 'AUDIT_LOGS', + CUSTOMERS = 'CUSTOMERS', + CUSTOMER = 'CUSTOMER', + NOTIFICATION = 'NOTIFICATION', + CUSTOM = 'CUSTOM' +} + +export interface MobileMenuItem { + label: string; + icon: string; + path: MobileMenuPath; + id: string; +} + +export interface MobileLayoutConfig { + items: MobileMenuItem[]; +} + +export interface MobileAppBundle extends Omit, 'label'>, HasTenantId { + title?: string; + description?: string; + androidAppId?: MobileAppId; + iosAppId?: MobileAppId; + layoutConfig?: MobileLayoutConfig; + oauth2Enabled: boolean; +} + +export interface MobileAppBundleInfo extends MobileAppBundle { + androidPkgName: string; + iosPkgName: string; + oauth2ClientInfos?: Array; +} diff --git a/ui-ngx/src/app/shared/models/oauth2.models.ts b/ui-ngx/src/app/shared/models/oauth2.models.ts index dd024715d5..9c7cd6f342 100644 --- a/ui-ngx/src/app/shared/models/oauth2.models.ts +++ b/ui-ngx/src/app/shared/models/oauth2.models.ts @@ -20,7 +20,6 @@ import { TenantId } from '@shared/models/id/tenant-id'; import { HasTenantId } from './entity.models'; import { DomainId } from './id/domain-id'; import { HasUUID } from '@shared/models/id/has-uuid'; -import { MobileAppId } from '@shared/models/id/mobile-app-id'; export enum DomainSchema { HTTP = 'HTTP', @@ -88,17 +87,6 @@ export interface DomainInfo extends Domain, HasOauth2Clients { oauth2ClientInfos?: Array | Array; } -export interface MobileApp extends BaseData, HasTenantId { - tenantId?: TenantId; - pkgName: string; - appSecret: string; - oauth2Enabled: boolean; -} - -export interface MobileAppInfo extends MobileApp, HasOauth2Clients { - oauth2ClientInfos?: Array | Array; -} - export interface OAuth2Client extends BaseData, HasTenantId { tenantId?: TenantId; title: string; diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index d4a8cffa53..61305d3b77 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -322,6 +322,7 @@ "mobile-package-placeholder": "Ex.: my.example.app", "mobile-package-hint": "For Android: your own unique Application ID. For iOS: Product bundle identifier.", "mobile-package-unique": "Application package must be unique.", + "mobile-package-required": "Application package is required.", "mobile-package-max-length": "Application package should be less than 256", "mobile-package-spaces": "Application package should not contain spaces", "mobile-app-secret": "Application secret", @@ -4057,6 +4058,50 @@ "copy-code": "Click to copy", "copied": "Copied!" }, + "mobile": { + "add-application": "Add application", + "app-id": "App ID", + "app-id-required": "App ID is required", + "app-store-link": "App Store link", + "app-store-link-required": "App Store link is required", + "application-details": "Application details", + "application-package": "Application Package", + "application-secret": "Application Secret", + "applications": "Applications", + "copy-app-id": "Copy App ID", + "copy-app-store-link": "Copy App Store link", + "copy-application-package": "Copy application package", + "copy-application-secret": "Copy application secret", + "copy-google-play-link": "Copy Google Play link", + "copy-sha256-certificate-fingerprints": "Copy SHA256 certificate fingerprints", + "delete-applications-text": "Be careful, after the confirmation the mobile application and all related data will become unrecoverable.", + "delete-applications-title": "Are you sure you want to delete the mobile application '{{applicationName}}'?", + "generate-application-secret": "Generate application secret", + "google-play-link": "Google Play link", + "google-play-link-required": "Google Play link is required", + "latest-version": "Latest version", + "min-version": "Min version", + "mobile-center": "Mobile center", + "mobile-package": "Application package", + "mobile-package-max-length": "Application package should be less than 256", + "mobile-package-required": "Application package is required.", + "mobile-package-spaces": "Application package should not contain spaces", + "no=application": "No applications found", + "platform-type": "Platform type", + "search-application": "Search applications", + "set": "Set", + "sha256-certificate-fingerprints": "SHA256 certificate fingerprints", + "sha256-certificate-fingerprints-required": "SHA256 certificate fingerprints is required", + "status": "Status", + "status-type": { + "deprecated": "Deprecated", + "draft": "Draft", + "published": "Published", + "suspended": "Suspended" + }, + "store-information": "Store information", + "version-information": "Version information" + }, "notification": { "action-button": "Action button", "action-type": "Action type",