UI: Implement git settings form

This commit is contained in:
Igor Kulikov 2022-05-20 15:02:30 +03:00
parent 4b01aa6f1e
commit 76af741399
13 changed files with 459 additions and 13 deletions

View File

@ -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);
}

View File

@ -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());
}
}

View File

@ -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<AdminSettingsEntity, Adm
}
@Override
@Transactional
public boolean removeByTenantIdAndKey(UUID tenantId, String key) {
if (adminSettingsRepository.existsByTenantIdAndKey(tenantId, key)) {
adminSettingsRepository.deleteByTenantIdAndKey(tenantId, key);

View File

@ -20,6 +20,7 @@ import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import {
AdminSettings,
EntitiesVersionControlSettings,
MailServerSettings,
SecuritySettings,
TestSmsRequest,
@ -64,6 +65,25 @@ export class AdminService {
defaultHttpOptionsFromConfig(config));
}
public getEntitiesVersionControlSettings(config?: RequestConfig): Observable<EntitiesVersionControlSettings> {
return this.http.get<EntitiesVersionControlSettings>(`/api/admin/vcSettings`, defaultHttpOptionsFromConfig(config));
}
public saveEntitiesVersionControlSettings(versionControlSettings: EntitiesVersionControlSettings,
config?: RequestConfig): Observable<EntitiesVersionControlSettings> {
return this.http.post<EntitiesVersionControlSettings>('/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<void> {
return this.http.post<void>('/api/admin/vcSettings/checkAccess', versionControlSettings, defaultHttpOptionsFromConfig(config));
}
public checkUpdates(config?: RequestConfig): Observable<UpdateMessage> {
return this.http.get<UpdateMessage>(`/api/admin/updates`, defaultHttpOptionsFromConfig(config));
}

View File

@ -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',
}
]
}

View File

@ -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<string> {
@ -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'
}
}
}
]
}

View File

@ -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,

View File

@ -0,0 +1,110 @@
<!--
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.
-->
<div>
<mat-card class="settings-card">
<mat-card-title>
<div fxLayout="row">
<span class="mat-headline" translate>admin.git-repository-settings</span>
<span fxFlex></span>
<div tb-help="versionControlSettings"></div>
</div>
</mat-card-title>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<mat-card-content style="padding-top: 16px;">
<form [formGroup]="versionControlSettingsForm" #formDirective="ngForm" (ngSubmit)="save()">
<fieldset [disabled]="isLoading$ | async">
<mat-form-field class="mat-block">
<mat-label translate>admin.repository-url</mat-label>
<input matInput required formControlName="repositoryUri">
<mat-error translate *ngIf="versionControlSettingsForm.get('repositoryUri').hasError('required')">
admin.repository-url-required
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>admin.default-branch</mat-label>
<input matInput formControlName="defaultBranch">
</mat-form-field>
<fieldset [disabled]="isLoading$ | async" class="fields-group">
<legend class="group-title" translate>admin.authentication-settings</legend>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.auth-method</mat-label>
<mat-select required formControlName="authMethod">
<mat-option *ngFor="let method of versionControlAuthMethods" [value]="method">
{{versionControlAuthMethodTranslations.get(method) | translate}}
</mat-option>
</mat-select>
</mat-form-field>
<section [fxShow]="versionControlSettingsForm.get('authMethod').value === versionControlAuthMethod.USERNAME_PASSWORD" fxLayout="column">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>common.username</mat-label>
<input matInput formControlName="username" placeholder="{{ 'common.enter-username' | translate }}"
autocomplete="new-username"/>
</mat-form-field>
<mat-checkbox *ngIf="showChangePassword" (change)="changePasswordChanged()"
[(ngModel)]="changePassword" [ngModelOptions]="{ standalone: true }" style="padding-bottom: 16px;">
{{ 'admin.change-password-access-token' | translate }}
</mat-checkbox>
<mat-form-field class="mat-block" *ngIf="changePassword || !showChangePassword">
<mat-label translate>admin.password-access-token</mat-label>
<input matInput formControlName="password" type="password"
placeholder="{{ 'common.enter-password' | translate }}" autocomplete="new-password"/>
<tb-toggle-password matSuffix></tb-toggle-password>
</mat-form-field>
</section>
<section [fxShow]="versionControlSettingsForm.get('authMethod').value === versionControlAuthMethod.PRIVATE_KEY" fxLayout="column">
<tb-file-input style="margin-bottom: 16px;"
[existingFileName]="versionControlSettingsForm.get('privateKeyFileName').value"
required
formControlName="privateKey"
dropLabel="{{ 'admin.drop-private-key-file-or' | translate }}"
[label]="'admin.private-key' | translate"
(fileNameChanged)="versionControlSettingsForm.get('privateKeyFileName').patchValue($event)">
</tb-file-input>
<mat-checkbox *ngIf="showChangePrivateKeyPassword" (change)="changePrivateKeyPasswordChanged()"
[(ngModel)]="changePrivateKeyPassword" [ngModelOptions]="{ standalone: true }" style="padding-bottom: 16px;">
{{ 'admin.change-passphrase' | translate }}
</mat-checkbox>
<mat-form-field class="mat-block" *ngIf="changePrivateKeyPassword || !showChangePrivateKeyPassword">
<mat-label translate>admin.passphrase</mat-label>
<input matInput formControlName="privateKeyPassword" type="password"
placeholder="{{ 'admin.enter-passphrase' | translate }}" autocomplete="new-password"/>
<tb-toggle-password matSuffix></tb-toggle-password>
</mat-form-field>
</section>
</fieldset>
<div fxLayout="row" fxLayoutAlign="end center" fxLayout.xs="column" fxLayoutAlign.xs="end" fxLayoutGap="16px">
<button mat-raised-button color="warn" type="button" [fxShow]="settings !== null"
[disabled]="(isLoading$ | async)" (click)="delete(formDirective)">
{{'action.delete' | translate}}
</button>
<span fxFlex></span>
<button mat-raised-button type="button"
[disabled]="(isLoading$ | async) || versionControlSettingsForm.invalid" (click)="checkAccess()">
{{'admin.check-access' | translate}}
</button>
<button mat-raised-button color="primary" [disabled]="(isLoading$ | async) || versionControlSettingsForm.invalid || !versionControlSettingsForm.dirty"
type="submit">{{'action.save' | translate}}
</button>
</div>
</fieldset>
</form>
</mat-card-content>
</mat-card>
</div>

View File

@ -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;
}
}
}

View File

@ -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<AppState>,
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});
}
}

View File

@ -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'
}
};

View File

@ -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, string>([
[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;
}

View File

@ -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",