From 76af741399fc01f2eb2b735802f00eb2e932f755 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 20 May 2022 15:02:30 +0300 Subject: [PATCH 1/2] UI: Implement git settings form --- .../server/controller/AdminController.java | 8 +- .../DefaultEntitiesVersionControlService.java | 21 +- .../dao/sql/settings/JpaAdminSettingsDao.java | 2 + ui-ngx/src/app/core/http/admin.service.ts | 20 ++ ui-ngx/src/app/core/services/menu.service.ts | 14 +- .../home/pages/admin/admin-routing.module.ts | 14 ++ .../modules/home/pages/admin/admin.module.ts | 4 +- .../version-control-settings.component.html | 110 ++++++++++ .../version-control-settings.component.scss | 33 +++ .../version-control-settings.component.ts | 198 ++++++++++++++++++ ui-ngx/src/app/shared/models/constants.ts | 3 +- .../src/app/shared/models/settings.models.ts | 21 ++ .../assets/locale/locale.constant-en_US.json | 24 ++- 13 files changed, 459 insertions(+), 13 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/pages/admin/version-control-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/admin/version-control-settings.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/admin/version-control-settings.component.ts diff --git a/application/src/main/java/org/thingsboard/server/controller/AdminController.java b/application/src/main/java/org/thingsboard/server/controller/AdminController.java index bf8c656257..45b8aa5f95 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AdminController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AdminController.java @@ -207,10 +207,14 @@ public class AdminController extends BaseController { notes = "Creates or Updates the version control settings object. " + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @PostMapping("/vcSettings") - public void saveVersionControlSettings(@RequestBody EntitiesVersionControlSettings settings) throws ThingsboardException { + public EntitiesVersionControlSettings saveVersionControlSettings(@RequestBody EntitiesVersionControlSettings settings) throws ThingsboardException { try { accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.WRITE); - versionControlService.saveVersionControlSettings(getTenantId(), settings); + EntitiesVersionControlSettings versionControlSettings = checkNotNull(versionControlService.saveVersionControlSettings(getTenantId(), settings)); + versionControlSettings.setPassword(null); + versionControlSettings.setPrivateKey(null); + versionControlSettings.setPrivateKeyPassword(null); + return versionControlSettings; } catch (Exception e) { throw handleException(e); } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java index dcc96b7d17..d46884ca1e 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java @@ -291,11 +291,21 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont @Override public EntitiesVersionControlSettings saveVersionControlSettings(TenantId tenantId, EntitiesVersionControlSettings versionControlSettings) { - EntitiesVersionControlSettings storedSettings = getVersionControlSettings(tenantId); + AdminSettings adminSettings = adminSettingsService.findAdminSettingsByKey(tenantId, SETTINGS_KEY); + EntitiesVersionControlSettings storedSettings = null; + if (adminSettings != null) { + try { + storedSettings = JacksonUtil.convertValue(adminSettings.getJsonValue(), EntitiesVersionControlSettings.class); + } catch (Exception e) { + throw new RuntimeException("Failed to load version control settings!", e); + } + } versionControlSettings = this.restoreCredentials(versionControlSettings, storedSettings); - AdminSettings adminSettings = new AdminSettings(); - adminSettings.setTenantId(tenantId); - adminSettings.setKey(SETTINGS_KEY); + if (adminSettings == null) { + adminSettings = new AdminSettings(); + adminSettings.setKey(SETTINGS_KEY); + adminSettings.setTenantId(tenantId); + } adminSettings.setJsonValue(JacksonUtil.valueToTree(versionControlSettings)); AdminSettings savedAdminSettings = adminSettingsService.saveAdminSettings(tenantId, adminSettings); EntitiesVersionControlSettings savedVersionControlSettings; @@ -341,8 +351,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont } else if (VersionControlAuthMethod.PRIVATE_KEY.equals(authMethod) && settings.getPrivateKey() == null) { if (storedSettings != null) { settings.setPrivateKey(storedSettings.getPrivateKey()); - if (StringUtils.isEmpty(settings.getPrivateKeyPassword()) && - StringUtils.isNotEmpty(storedSettings.getPrivateKeyPassword())) { + if (settings.getPrivateKeyPassword() == null) { settings.setPrivateKeyPassword(storedSettings.getPrivateKeyPassword()); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/settings/JpaAdminSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/settings/JpaAdminSettingsDao.java index 7257cfb57b..de4a2fd5ed 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/settings/JpaAdminSettingsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/settings/JpaAdminSettingsDao.java @@ -19,6 +19,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.DaoUtil; @@ -51,6 +52,7 @@ public class JpaAdminSettingsDao extends JpaAbstractDao { + return this.http.get(`/api/admin/vcSettings`, defaultHttpOptionsFromConfig(config)); + } + + public saveEntitiesVersionControlSettings(versionControlSettings: EntitiesVersionControlSettings, + config?: RequestConfig): Observable { + return this.http.post('/api/admin/vcSettings', versionControlSettings, + defaultHttpOptionsFromConfig(config)); + } + + public deleteEntitiesVersionControlSettings(config?: RequestConfig) { + return this.http.delete('/api/admin/vcSettings', defaultHttpOptionsFromConfig(config)); + } + + public checkVersionControlAccess(versionControlSettings: EntitiesVersionControlSettings, + config?: RequestConfig): Observable { + return this.http.post('/api/admin/vcSettings/checkAccess', versionControlSettings, defaultHttpOptionsFromConfig(config)); + } + public checkUpdates(config?: RequestConfig): Observable { return this.http.get(`/api/admin/updates`, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/core/services/menu.service.ts b/ui-ngx/src/app/core/services/menu.service.ts index 4a52fc66f6..85cf24e04d 100644 --- a/ui-ngx/src/app/core/services/menu.service.ts +++ b/ui-ngx/src/app/core/services/menu.service.ts @@ -350,7 +350,7 @@ export class MenuService { name: 'admin.system-settings', type: 'toggle', path: '/settings', - height: '80px', + height: '120px', icon: 'settings', pages: [ { @@ -366,6 +366,13 @@ export class MenuService { type: 'link', path: '/settings/resources-library', icon: 'folder' + }, + { + id: guid(), + name: 'admin.git-settings', + type: 'link', + path: '/settings/vc', + icon: 'manage_history' } ] } @@ -500,6 +507,11 @@ export class MenuService { name: 'resource.resources-library', icon: 'folder', path: '/settings/resources-library' + }, + { + name: 'admin.git-settings', + icon: 'manage_history', + path: '/settings/vc', } ] } 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 ddccbb4da4..6c4963d06e 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 @@ -32,6 +32,7 @@ import { ResourcesLibraryTableConfigResolver } from '@home/pages/admin/resource/ 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 { VersionControlSettingsComponent } from '@home/pages/admin/version-control-settings.component'; @Injectable() export class OAuth2LoginProcessingUrlResolver implements Resolve { @@ -183,6 +184,19 @@ const routes: Routes = [ } } ] + }, + { + path: 'vc', + component: VersionControlSettingsComponent, + canDeactivate: [ConfirmOnExitGuard], + data: { + auth: [Authority.TENANT_ADMIN], + title: 'admin.git-settings', + breadcrumb: { + label: 'admin.git-settings', + icon: 'manage_history' + } + } } ] } 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 326b16f950..cb81e56020 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 @@ -28,6 +28,7 @@ import { SmsProviderComponent } from '@home/pages/admin/sms-provider.component'; import { SendTestSmsDialogComponent } from '@home/pages/admin/send-test-sms-dialog.component'; import { HomeSettingsComponent } from '@home/pages/admin/home-settings.component'; import { ResourcesLibraryComponent } from '@home/pages/admin/resource/resources-library.component'; +import { VersionControlSettingsComponent } from '@home/pages/admin/version-control-settings.component'; @NgModule({ declarations: @@ -39,7 +40,8 @@ import { ResourcesLibraryComponent } from '@home/pages/admin/resource/resources- SecuritySettingsComponent, OAuth2SettingsComponent, HomeSettingsComponent, - ResourcesLibraryComponent + ResourcesLibraryComponent, + VersionControlSettingsComponent ], imports: [ CommonModule, diff --git a/ui-ngx/src/app/modules/home/pages/admin/version-control-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/version-control-settings.component.html new file mode 100644 index 0000000000..b512bb96f5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/version-control-settings.component.html @@ -0,0 +1,110 @@ + +
+ + +
+ admin.git-repository-settings + +
+
+
+ + +
+ +
+
+ + admin.repository-url + + + admin.repository-url-required + + + + admin.default-branch + + +
+ admin.authentication-settings + + admin.auth-method + + + {{versionControlAuthMethodTranslations.get(method) | translate}} + + + +
+ + common.username + + + + {{ 'admin.change-password-access-token' | translate }} + + + admin.password-access-token + + + +
+
+ + + + {{ 'admin.change-passphrase' | translate }} + + + admin.passphrase + + + +
+
+
+ + + + +
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/admin/version-control-settings.component.scss b/ui-ngx/src/app/modules/home/pages/admin/version-control-settings.component.scss new file mode 100644 index 0000000000..ede3570e68 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/version-control-settings.component.scss @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2022 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 { + .fields-group { + padding: 0 16px 8px; + margin-bottom: 10px; + border: 1px groove rgba(0, 0, 0, .25); + border-radius: 4px; + + legend { + color: rgba(0, 0, 0, .7); + width: fit-content; + } + + legend + * { + display: block; + margin-top: 16px; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/version-control-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/version-control-settings.component.ts new file mode 100644 index 0000000000..c2626ec062 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/version-control-settings.component.ts @@ -0,0 +1,198 @@ +/// +/// Copyright © 2016-2022 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, OnInit } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; +import { FormBuilder, FormGroup, FormGroupDirective, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { AdminService } from '@core/http/admin.service'; +import { + EntitiesVersionControlSettings, + VersionControlAuthMethod, + versionControlAuthMethodTranslationMap +} from '@shared/models/settings.models'; +import { ActionNotificationShow } from '@core/notification/notification.actions'; +import { TranslateService } from '@ngx-translate/core'; +import { isNotEmptyStr } from '@core/utils'; +import { DialogService } from '@core/services/dialog.service'; + +@Component({ + selector: 'tb-version-control-settings', + templateUrl: './version-control-settings.component.html', + styleUrls: ['./version-control-settings.component.scss', './settings-card.scss'] +}) +export class VersionControlSettingsComponent extends PageComponent implements OnInit, HasConfirmForm { + + versionControlSettingsForm: FormGroup; + settings: EntitiesVersionControlSettings = null; + + versionControlAuthMethod = VersionControlAuthMethod; + versionControlAuthMethods = Object.values(VersionControlAuthMethod); + versionControlAuthMethodTranslations = versionControlAuthMethodTranslationMap; + + showChangePassword = false; + changePassword = false; + + showChangePrivateKeyPassword = false; + changePrivateKeyPassword = false; + + constructor(protected store: Store, + private adminService: AdminService, + private dialogService: DialogService, + private translate: TranslateService, + public fb: FormBuilder) { + super(store); + } + + ngOnInit() { + this.versionControlSettingsForm = this.fb.group({ + repositoryUri: [null, [Validators.required]], + defaultBranch: [null, []], + authMethod: [VersionControlAuthMethod.USERNAME_PASSWORD, [Validators.required]], + username: [null, []], + password: [null, []], + privateKeyFileName: [null, [Validators.required]], + privateKey: [null, []], + privateKeyPassword: [null, []] + }); + this.updateValidators(false); + this.versionControlSettingsForm.get('authMethod').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + this.versionControlSettingsForm.get('privateKeyFileName').valueChanges.subscribe(() => { + this.updateValidators(false); + }); + this.adminService.getEntitiesVersionControlSettings({ignoreErrors: true}).subscribe( + (settings) => { + this.settings = settings; + if (this.settings.authMethod === VersionControlAuthMethod.USERNAME_PASSWORD) { + this.showChangePassword = true; + } else { + this.showChangePrivateKeyPassword = true; + } + this.versionControlSettingsForm.reset(this.settings); + this.updateValidators(false); + }); + } + + checkAccess(): void { + const settings: EntitiesVersionControlSettings = this.versionControlSettingsForm.value; + this.adminService.checkVersionControlAccess(settings).subscribe(() => { + this.store.dispatch(new ActionNotificationShow({ message: this.translate.instant('admin.check-vc-access-success'), + type: 'success' })); + }); + } + + save(): void { + const settings: EntitiesVersionControlSettings = this.versionControlSettingsForm.value; + this.adminService.saveEntitiesVersionControlSettings(settings).subscribe( + (savedSettings) => { + this.settings = savedSettings; + if (this.settings.authMethod === VersionControlAuthMethod.USERNAME_PASSWORD) { + this.showChangePassword = true; + this.changePassword = false; + } else { + this.showChangePrivateKeyPassword = true; + this.changePrivateKeyPassword = false; + } + this.versionControlSettingsForm.reset(this.settings); + this.updateValidators(false); + } + ); + } + + delete(formDirective: FormGroupDirective): void { + this.dialogService.confirm( + this.translate.instant('admin.delete-git-settings-title', ), + this.translate.instant('admin.delete-git-settings-text'), null, + this.translate.instant('action.delete') + ).subscribe((data) => { + if (data) { + this.adminService.deleteEntitiesVersionControlSettings().subscribe( + () => { + this.settings = null; + this.showChangePassword = false; + this.changePassword = false; + this.showChangePrivateKeyPassword = false; + this.changePrivateKeyPassword = false; + formDirective.resetForm(); + this.versionControlSettingsForm.reset({ authMethod: VersionControlAuthMethod.USERNAME_PASSWORD }); + this.updateValidators(false); + } + ); + } + }); + } + + confirmForm(): FormGroup { + return this.versionControlSettingsForm; + } + + changePasswordChanged() { + if (this.changePassword) { + this.versionControlSettingsForm.get('password').patchValue(''); + this.versionControlSettingsForm.get('password').markAsDirty(); + } + this.updateValidators(false); + } + + changePrivateKeyPasswordChanged() { + if (this.changePrivateKeyPassword) { + this.versionControlSettingsForm.get('privateKeyPassword').patchValue(''); + this.versionControlSettingsForm.get('privateKeyPassword').markAsDirty(); + } + this.updateValidators(false); + } + + updateValidators(emitEvent?: boolean): void { + const authMethod: VersionControlAuthMethod = this.versionControlSettingsForm.get('authMethod').value; + const privateKeyFileName: string = this.versionControlSettingsForm.get('privateKeyFileName').value; + if (authMethod === VersionControlAuthMethod.USERNAME_PASSWORD) { + this.versionControlSettingsForm.get('username').enable({emitEvent}); + if (this.changePassword || !this.showChangePassword) { + this.versionControlSettingsForm.get('password').enable({emitEvent}); + } else { + this.versionControlSettingsForm.get('password').disable({emitEvent}); + } + this.versionControlSettingsForm.get('privateKeyFileName').disable({emitEvent}); + this.versionControlSettingsForm.get('privateKey').disable({emitEvent}); + this.versionControlSettingsForm.get('privateKeyPassword').disable({emitEvent}); + } else { + this.versionControlSettingsForm.get('username').disable({emitEvent}); + this.versionControlSettingsForm.get('password').disable({emitEvent}); + this.versionControlSettingsForm.get('privateKeyFileName').enable({emitEvent}); + this.versionControlSettingsForm.get('privateKey').enable({emitEvent}); + if (this.changePrivateKeyPassword || !this.showChangePrivateKeyPassword) { + this.versionControlSettingsForm.get('privateKeyPassword').enable({emitEvent}); + } else { + this.versionControlSettingsForm.get('privateKeyPassword').disable({emitEvent}); + } + if (isNotEmptyStr(privateKeyFileName)) { + this.versionControlSettingsForm.get('privateKey').clearValidators(); + } else { + this.versionControlSettingsForm.get('privateKey').setValidators([Validators.required]); + } + } + this.versionControlSettingsForm.get('username').updateValueAndValidity({emitEvent: false}); + this.versionControlSettingsForm.get('password').updateValueAndValidity({emitEvent: false}); + this.versionControlSettingsForm.get('privateKeyFileName').updateValueAndValidity({emitEvent: false}); + this.versionControlSettingsForm.get('privateKey').updateValueAndValidity({emitEvent: false}); + this.versionControlSettingsForm.get('privateKeyPassword').updateValueAndValidity({emitEvent: false}); + } + +} diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts index 4bfce6414a..7c970678b8 100644 --- a/ui-ngx/src/app/shared/models/constants.ts +++ b/ui-ngx/src/app/shared/models/constants.ts @@ -133,7 +133,8 @@ export const HelpLinks = { widgetsConfigAlarm: helpBaseUrl + '/docs/user-guide/ui/dashboards#alarm', widgetsConfigStatic: helpBaseUrl + '/docs/user-guide/ui/dashboards#static', ruleNodePushToCloud: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/action-nodes/#push-to-cloud', - ruleNodePushToEdge: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/action-nodes/#push-to-edge' + ruleNodePushToEdge: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/action-nodes/#push-to-edge', + versionControlSettings: helpBaseUrl + '/docs/user-guide/ui/version-control-settings' } }; diff --git a/ui-ngx/src/app/shared/models/settings.models.ts b/ui-ngx/src/app/shared/models/settings.models.ts index 615d0994c9..8a1b316001 100644 --- a/ui-ngx/src/app/shared/models/settings.models.ts +++ b/ui-ngx/src/app/shared/models/settings.models.ts @@ -396,3 +396,24 @@ export function createSmsProviderConfiguration(type: SmsProviderType): SmsProvid } return smsProviderConfiguration; } + +export enum VersionControlAuthMethod { + USERNAME_PASSWORD = 'USERNAME_PASSWORD', + PRIVATE_KEY = 'PRIVATE_KEY' +} + +export const versionControlAuthMethodTranslationMap = new Map([ + [VersionControlAuthMethod.USERNAME_PASSWORD, 'admin.auth-method-username-password'], + [VersionControlAuthMethod.PRIVATE_KEY, 'admin.auth-method-private-key'] +]); + +export interface EntitiesVersionControlSettings { + repositoryUri: string; + defaultBranch: string; + authMethod: VersionControlAuthMethod; + username: string; + password: string; + privateKeyFileName: string; + privateKey: string; + privateKeyPassword: 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 cf576aedbf..5aaadb2bd6 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -311,8 +311,28 @@ "scheme-music-codes": "10 - Music Codes (ISO-2022-JP)", "scheme-extended-kanji-jis": "13 - Extended Kanji JIS (X 0212-1990)", "scheme-korean-graphic-character-set": "14 - Korean Graphic Character Set (KS C 5601/KS X 1001)" - } - }, + }, + "git-settings": "Git settings", + "git-repository-settings": "Git repository settings", + "repository-url": "Repository URL", + "repository-url-required": "Repository URL is required.", + "default-branch": "Default branch name", + "authentication-settings": "Authentication settings", + "auth-method": "Authentication method", + "auth-method-username-password": "Password / access token", + "auth-method-private-key": "Private key", + "password-access-token": "Password / access token", + "change-password-access-token": "Change password / access token", + "private-key": "Private key", + "drop-private-key-file-or": "Drag and drop a private key file or", + "passphrase": "Passphrase", + "enter-passphrase": "Enter passphrase", + "change-passphrase": "Change passphrase", + "check-access": "Check access", + "check-vc-access-success": "Git repository access successfully verified!", + "delete-git-settings-title": "Are you sure you want to delete git settings?", + "delete-git-settings-text": "Be careful, after the confirmation the git settings will be removed and git synchronization feature will be unavailable." + }, "alarm": { "alarm": "Alarm", "alarms": "Alarms", From 841f9d5ff6287af4f6ba3e39b7bff3d09a76ab70 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 20 May 2022 17:50:35 +0300 Subject: [PATCH 2/2] UI: Single entity Git export --- .../http/entities-version-control.service.ts | 40 ++++ .../home/components/home-components.module.ts | 7 +- .../profile/device-profile.component.html | 6 + .../vc/vc-entity-export-dialog.component.html | 77 +++++++ .../vc/vc-entity-export-dialog.component.ts | 105 +++++++++ .../home/dialogs/home-dialogs.service.ts | 17 ++ .../home/pages/asset/asset.component.html | 6 + .../asset/assets-table-config.resolver.ts | 10 + .../pages/customer/customer.component.html | 6 + .../customers-table-config.resolver.ts | 12 ++ .../dashboard/dashboard-form.component.html | 6 + .../dashboards-table-config.resolver.ts | 12 ++ .../device-profiles-table-config.resolver.ts | 12 ++ .../home/pages/device/device.component.html | 6 + .../device/devices-table-config.resolver.ts | 10 + .../pages/rulechain/rulechain.component.html | 6 + .../rulechains-table-config.resolver.ts | 12 ++ .../vc/branch-autocomplete.component.html | 43 ++++ .../vc/branch-autocomplete.component.ts | 199 ++++++++++++++++++ ui-ngx/src/app/shared/models/vc.models.ts | 55 +++++ ui-ngx/src/app/shared/shared.module.ts | 7 +- .../assets/locale/locale.constant-en_US.json | 12 ++ 22 files changed, 662 insertions(+), 4 deletions(-) create mode 100644 ui-ngx/src/app/core/http/entities-version-control.service.ts create mode 100644 ui-ngx/src/app/modules/home/components/vc/vc-entity-export-dialog.component.html create mode 100644 ui-ngx/src/app/modules/home/components/vc/vc-entity-export-dialog.component.ts create mode 100644 ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.html create mode 100644 ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.ts create mode 100644 ui-ngx/src/app/shared/models/vc.models.ts diff --git a/ui-ngx/src/app/core/http/entities-version-control.service.ts b/ui-ngx/src/app/core/http/entities-version-control.service.ts new file mode 100644 index 0000000000..231581a02f --- /dev/null +++ b/ui-ngx/src/app/core/http/entities-version-control.service.ts @@ -0,0 +1,40 @@ +/// +/// Copyright © 2016-2022 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; +import { Observable } from 'rxjs'; +import { BranchInfo, VersionCreateRequest, VersionCreationResult } from '@shared/models/vc.models'; + +@Injectable({ + providedIn: 'root' +}) +export class EntitiesVersionControlService { + + constructor( + private http: HttpClient + ) { + } + + public listBranches(config?: RequestConfig): Observable> { + return this.http.get>('/api/entities/vc/branches', defaultHttpOptionsFromConfig(config)); + } + + public saveEntitiesVersion(request: VersionCreateRequest, config?: RequestConfig): Observable { + return this.http.post('/api/entities/vc/version', request, defaultHttpOptionsFromConfig(config)); + } +} diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index 4a721647ef..a88f0349eb 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts @@ -151,6 +151,7 @@ import { DashboardStateComponent } from '@home/components/dashboard-page/dashboa import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component'; import { WidgetSettingsModule } from '@home/components/widget/lib/settings/widget-settings.module'; import { WidgetSettingsComponent } from '@home/components/widget/widget-settings.component'; +import { VcEntityExportDialogComponent } from '@home/components/vc/vc-entity-export-dialog.component'; @NgModule({ declarations: @@ -272,7 +273,8 @@ import { WidgetSettingsComponent } from '@home/components/widget/widget-settings DashboardStateDialogComponent, DashboardImageDialogComponent, EmbedDashboardDialogComponent, - DisplayWidgetTypesPanelComponent + DisplayWidgetTypesPanelComponent, + VcEntityExportDialogComponent ], imports: [ CommonModule, @@ -388,7 +390,8 @@ import { WidgetSettingsComponent } from '@home/components/widget/widget-settings DashboardStateDialogComponent, DashboardImageDialogComponent, EmbedDashboardDialogComponent, - DisplayWidgetTypesPanelComponent + DisplayWidgetTypesPanelComponent, + VcEntityExportDialogComponent ], providers: [ WidgetComponentService, diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html index 565eb5b160..7e35dbdaf4 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html @@ -34,6 +34,12 @@ [fxShow]="!isEdit && !entity?.default"> {{'device-profile.set-default' | translate }} + + + + +
+
+
+
+ + + + version-control.version-name + + + {{ 'version-control.version-name-required' | translate }} + + + + {{ 'version-control.export-entity-relations' | translate }} + +
+
+
+
+
+
+
+
+ + +
+
+ +
+ diff --git a/ui-ngx/src/app/modules/home/components/vc/vc-entity-export-dialog.component.ts b/ui-ngx/src/app/modules/home/components/vc/vc-entity-export-dialog.component.ts new file mode 100644 index 0000000000..9d3affea42 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/vc/vc-entity-export-dialog.component.ts @@ -0,0 +1,105 @@ +/// +/// Copyright © 2016-2022 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; +import { EntityId } from '@shared/models/id/entity-id'; +import { + SingleEntityVersionCreateRequest, + VersionCreateRequestType, + VersionCreationResult +} from '@shared/models/vc.models'; +import { EntitiesVersionControlService } from '@core/http/entities-version-control.service'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; +import { TranslateService } from '@ngx-translate/core'; + +export interface VcEntityExportDialogData { + entityId: EntityId; +} + +@Component({ + selector: 'tb-vc-entity-export-dialog', + templateUrl: './vc-entity-export-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: VcEntityExportDialogComponent}], + styleUrls: [] +}) +export class VcEntityExportDialogComponent extends DialogComponent + implements OnInit, ErrorStateMatcher { + + exportFormGroup: FormGroup; + + submitted = false; + + createResult: VersionCreationResult; + + createResultMessage: SafeHtml; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: VcEntityExportDialogData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + private entitiesVersionControlService: EntitiesVersionControlService, + private translate: TranslateService, + private domSanitizer: DomSanitizer, + private fb: FormBuilder) { + super(store, router, dialogRef); + + this.exportFormGroup = this.fb.group({ + branch: [null, [Validators.required]], + versionName: [null, [Validators.required]], + saveRelations: [false, []] + }); + } + + ngOnInit(): void { + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(); + } + + export(): void { + this.submitted = true; + const request: SingleEntityVersionCreateRequest = { + entityId: this.data.entityId, + branch: this.exportFormGroup.get('branch').value, + versionName: this.exportFormGroup.get('versionName').value, + config: { + saveRelations: this.exportFormGroup.get('saveRelations').value + }, + type: VersionCreateRequestType.SINGLE_ENTITY + }; + this.entitiesVersionControlService.saveEntitiesVersion(request).subscribe((result) => { + this.createResult = result; + const message = this.translate.instant('version-control.export-entity-version-result-message', + {name: result.version.name, commitId: result.version.id}); + this.createResultMessage = this.domSanitizer.bypassSecurityTrustHtml(message); + }); + } +} diff --git a/ui-ngx/src/app/modules/home/dialogs/home-dialogs.service.ts b/ui-ngx/src/app/modules/home/dialogs/home-dialogs.service.ts index 6dc8746b2e..1730c69084 100644 --- a/ui-ngx/src/app/modules/home/dialogs/home-dialogs.service.ts +++ b/ui-ngx/src/app/modules/home/dialogs/home-dialogs.service.ts @@ -22,6 +22,11 @@ import { ImportDialogCsvComponent, ImportDialogCsvData } from '@home/components/import-export/import-dialog-csv.component'; +import { EntityId } from '@shared/models/id/entity-id'; +import { + VcEntityExportDialogComponent, + VcEntityExportDialogData +} from '@home/components/vc/vc-entity-export-dialog.component'; @Injectable() export class HomeDialogsService { @@ -41,6 +46,17 @@ export class HomeDialogsService { } } + public exportVcEntity(entityId: EntityId): Observable { + return this.dialog.open(VcEntityExportDialogComponent, + { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + entityId + } + }).afterClosed(); + } + private openImportDialogCSV(entityType: EntityType, importTitle: string, importFileLabel: string): Observable { return this.dialog.open(ImportDialogCsvComponent, { @@ -53,4 +69,5 @@ export class HomeDialogsService { } }).afterClosed(); } + } diff --git a/ui-ngx/src/app/modules/home/pages/asset/asset.component.html b/ui-ngx/src/app/modules/home/pages/asset/asset.component.html index 00ed882cd9..75171b4532 100644 --- a/ui-ngx/src/app/modules/home/pages/asset/asset.component.html +++ b/ui-ngx/src/app/modules/home/pages/asset/asset.component.html @@ -46,6 +46,12 @@ [fxShow]="!isEdit && assetScope === 'edge'"> {{ 'edge.unassign-from-edge' | translate }} + + + + + + + + + + + + {{ 'version-control.branch-required' | translate }} + + diff --git a/ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.ts b/ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.ts new file mode 100644 index 0000000000..421710181a --- /dev/null +++ b/ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.ts @@ -0,0 +1,199 @@ +/// +/// Copyright © 2016-2022 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 { AfterViewInit, Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Observable, of } from 'rxjs'; +import { + catchError, + debounceTime, + distinctUntilChanged, + map, + publishReplay, + refCount, + switchMap, + tap +} from 'rxjs/operators'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { BranchInfo } from '@shared/models/vc.models'; +import { EntitiesVersionControlService } from '@core/http/entities-version-control.service'; + +@Component({ + selector: 'tb-branch-autocomplete', + templateUrl: './branch-autocomplete.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => BranchAutocompleteComponent), + multi: true + }] +}) +export class BranchAutocompleteComponent implements ControlValueAccessor, OnInit, AfterViewInit { + + branchFormGroup: FormGroup; + + modelValue: string | null; + + private requiredValue: boolean; + + get required(): boolean { + return this.requiredValue; + } + + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + @Input() + selectDefaultBranch = true; + + @ViewChild('branchInput', {static: true}) branchInput: ElementRef; + + filteredBranches: Observable>; + + branches: Observable>; + + searchText = ''; + + private dirty = false; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private entitiesVersionControlService: EntitiesVersionControlService, + private fb: FormBuilder) { + this.branchFormGroup = this.fb.group({ + branch: [null, []] + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + + this.branches = null; + this.filteredBranches = this.branchFormGroup.get('branch').valueChanges + .pipe( + debounceTime(150), + distinctUntilChanged(), + tap(value => { + this.updateView(value); + }), + map(value => value ? value : ''), + switchMap(branch => this.fetchBranches(branch)) + ); + } + + ngAfterViewInit(): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.branchFormGroup.disable({emitEvent: false}); + } else { + this.branchFormGroup.enable({emitEvent: false}); + } + } + + selectDefaultBranchIfNeeded(): void { + if (this.selectDefaultBranch && !this.modelValue) { + this.getBranches().subscribe( + (data) => { + if (data && data.length) { + const defaultBranch = data.find(branch => branch.default); + if (defaultBranch) { + this.modelValue = defaultBranch.name; + this.branchFormGroup.get('branch').patchValue(this.modelValue, {emitEvent: false}); + this.propagateChange(this.modelValue); + } + } + } + ); + } + } + + writeValue(value: string | null): void { + this.searchText = ''; + this.modelValue = value; + if (value != null) { + this.branchFormGroup.get('branch').patchValue(value, {emitEvent: false}); + } else { + this.branchFormGroup.get('branch').patchValue('', {emitEvent: false}); + this.selectDefaultBranchIfNeeded(); + } + this.dirty = true; + } + + onFocus() { + if (this.dirty) { + this.branchFormGroup.get('branch').updateValueAndValidity({onlySelf: true, emitEvent: true}); + this.dirty = false; + } + } + + updateView(value: string | null) { + if (this.modelValue !== value) { + this.modelValue = value; + this.propagateChange(this.modelValue); + } + } + + displayBranchFn(branch?: string): string | undefined { + return branch ? branch : undefined; + } + + fetchBranches(searchText?: string): Observable> { + this.searchText = searchText; + return this.getBranches().pipe( + map(branches => branches.map(branch => branch.name).filter(branchName => { + return searchText ? branchName.toUpperCase().startsWith(searchText.toUpperCase()) : true; + })) + ); + } + + getBranches(): Observable> { + if (!this.branches) { + const branchesObservable = this.entitiesVersionControlService.listBranches({ignoreLoading: true, ignoreErrors: true}); + this.branches = branchesObservable.pipe( + catchError(() => of([] as Array)), + publishReplay(1), + refCount() + ); + } + return this.branches; + } + + clear() { + this.branchFormGroup.get('branch').patchValue(null, {emitEvent: true}); + setTimeout(() => { + this.branchInput.nativeElement.blur(); + this.branchInput.nativeElement.focus(); + }, 0); + } + +} diff --git a/ui-ngx/src/app/shared/models/vc.models.ts b/ui-ngx/src/app/shared/models/vc.models.ts new file mode 100644 index 0000000000..0c890ff9b4 --- /dev/null +++ b/ui-ngx/src/app/shared/models/vc.models.ts @@ -0,0 +1,55 @@ +/// +/// Copyright © 2016-2022 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { EntityId } from '@shared/models/id/entity-id'; + +export interface VersionCreateConfig { + saveRelations: boolean; +} + +export enum VersionCreateRequestType { + SINGLE_ENTITY = 'SINGLE_ENTITY', + COMPLEX = 'COMPLEX' +} + +export interface VersionCreateRequest { + versionName: string; + branch: string; + type: VersionCreateRequestType; +} + +export interface SingleEntityVersionCreateRequest extends VersionCreateRequest { + entityId: EntityId; + config: VersionCreateConfig; + type: VersionCreateRequestType.SINGLE_ENTITY; +} + +export interface BranchInfo { + name: string; + default: boolean; +} + +export interface EntityVersion { + id: string; + name: string; +} + +export interface VersionCreationResult { + version: EntityVersion; + added: number; + modified: number; + removed: number; +} diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 8d8de8e74a..7e73452b18 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -163,6 +163,7 @@ import { HtmlComponent } from '@shared/components/html.component'; import { SafePipe } from '@shared/pipe/safe.pipe'; import { DragDropModule } from '@angular/cdk/drag-drop'; import { MultipleImageInputComponent } from '@shared/components/multiple-image-input.component'; +import { BranchAutocompleteComponent } from '@shared/components/vc/branch-autocomplete.component'; export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { return markedOptionsService; @@ -284,7 +285,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) WidgetsBundleSearchComponent, CopyButtonComponent, TogglePasswordComponent, - ProtobufContentComponent + ProtobufContentComponent, + BranchAutocompleteComponent ], imports: [ CommonModule, @@ -484,7 +486,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) WidgetsBundleSearchComponent, CopyButtonComponent, TogglePasswordComponent, - ProtobufContentComponent + ProtobufContentComponent, + BranchAutocompleteComponent ] }) export class SharedModule { } 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 5aaadb2bd6..00880644e4 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3049,6 +3049,18 @@ "json-value-invalid": "JSON value has an invalid format", "json-value-required": "JSON value is required." }, + "version-control": { + "branch": "Branch", + "select-branch": "Select branch", + "branch-required": "Branch is required", + "export-entity-version": "Export entity version", + "entity-version-exported": "Entity version successfully exported", + "version-name": "Version name", + "version-name-required": "Version name is required", + "export-entity-relations": "Export entity relations", + "export-entity-version-result-message": "Entity exported with version '{{name}}' and commit id '{{commitId}}'.", + "export-to-git": "Export to Git" + }, "widget": { "widget-library": "Widgets Library", "widget-bundle": "Widgets Bundle",