Browse Source

UI: Single entity Git export

pull/6585/head
Igor Kulikov 4 years ago
parent
commit
841f9d5ff6
  1. 40
      ui-ngx/src/app/core/http/entities-version-control.service.ts
  2. 7
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  3. 6
      ui-ngx/src/app/modules/home/components/profile/device-profile.component.html
  4. 77
      ui-ngx/src/app/modules/home/components/vc/vc-entity-export-dialog.component.html
  5. 105
      ui-ngx/src/app/modules/home/components/vc/vc-entity-export-dialog.component.ts
  6. 17
      ui-ngx/src/app/modules/home/dialogs/home-dialogs.service.ts
  7. 6
      ui-ngx/src/app/modules/home/pages/asset/asset.component.html
  8. 10
      ui-ngx/src/app/modules/home/pages/asset/assets-table-config.resolver.ts
  9. 6
      ui-ngx/src/app/modules/home/pages/customer/customer.component.html
  10. 12
      ui-ngx/src/app/modules/home/pages/customer/customers-table-config.resolver.ts
  11. 6
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html
  12. 12
      ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts
  13. 12
      ui-ngx/src/app/modules/home/pages/device-profile/device-profiles-table-config.resolver.ts
  14. 6
      ui-ngx/src/app/modules/home/pages/device/device.component.html
  15. 10
      ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts
  16. 6
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.html
  17. 12
      ui-ngx/src/app/modules/home/pages/rulechain/rulechains-table-config.resolver.ts
  18. 43
      ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.html
  19. 199
      ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.ts
  20. 55
      ui-ngx/src/app/shared/models/vc.models.ts
  21. 7
      ui-ngx/src/app/shared/shared.module.ts
  22. 12
      ui-ngx/src/assets/locale/locale.constant-en_US.json

40
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<Array<BranchInfo>> {
return this.http.get<Array<BranchInfo>>('/api/entities/vc/branches', defaultHttpOptionsFromConfig(config));
}
public saveEntitiesVersion(request: VersionCreateRequest, config?: RequestConfig): Observable<VersionCreationResult> {
return this.http.post<VersionCreationResult>('/api/entities/vc/version', request, defaultHttpOptionsFromConfig(config));
}
}

7
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 { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component';
import { WidgetSettingsModule } from '@home/components/widget/lib/settings/widget-settings.module'; import { WidgetSettingsModule } from '@home/components/widget/lib/settings/widget-settings.module';
import { WidgetSettingsComponent } from '@home/components/widget/widget-settings.component'; import { WidgetSettingsComponent } from '@home/components/widget/widget-settings.component';
import { VcEntityExportDialogComponent } from '@home/components/vc/vc-entity-export-dialog.component';
@NgModule({ @NgModule({
declarations: declarations:
@ -272,7 +273,8 @@ import { WidgetSettingsComponent } from '@home/components/widget/widget-settings
DashboardStateDialogComponent, DashboardStateDialogComponent,
DashboardImageDialogComponent, DashboardImageDialogComponent,
EmbedDashboardDialogComponent, EmbedDashboardDialogComponent,
DisplayWidgetTypesPanelComponent DisplayWidgetTypesPanelComponent,
VcEntityExportDialogComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -388,7 +390,8 @@ import { WidgetSettingsComponent } from '@home/components/widget/widget-settings
DashboardStateDialogComponent, DashboardStateDialogComponent,
DashboardImageDialogComponent, DashboardImageDialogComponent,
EmbedDashboardDialogComponent, EmbedDashboardDialogComponent,
DisplayWidgetTypesPanelComponent DisplayWidgetTypesPanelComponent,
VcEntityExportDialogComponent
], ],
providers: [ providers: [
WidgetComponentService, WidgetComponentService,

6
ui-ngx/src/app/modules/home/components/profile/device-profile.component.html

@ -34,6 +34,12 @@
[fxShow]="!isEdit && !entity?.default"> [fxShow]="!isEdit && !entity?.default">
{{'device-profile.set-default' | translate }} {{'device-profile.set-default' | translate }}
</button> </button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'vcExport')"
[fxShow]="!isEdit">
{{'version-control.export-to-git' | translate }}
</button>
<button mat-raised-button color="primary" <button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'delete')" (click)="onEntityAction($event, 'delete')"

77
ui-ngx/src/app/modules/home/components/vc/vc-entity-export-dialog.component.html

@ -0,0 +1,77 @@
<!--
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.
-->
<section style="min-width: 400px;">
<mat-toolbar color="primary">
<h2>{{ (createResult ? 'version-control.entity-version-exported' : 'version-control.export-entity-version') | translate }}</h2>
<span fxFlex></span>
<button mat-button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div mat-dialog-content>
<form *ngIf="!createResult" [formGroup]="exportFormGroup">
<fieldset [disabled]="isLoading$ | async">
<div fxFlex fxLayout="column">
<tb-branch-autocomplete
required
formControlName="branch">
</tb-branch-autocomplete>
<mat-form-field class="mat-block" fxFlex>
<mat-label translate>version-control.version-name</mat-label>
<input required matInput formControlName="versionName">
<mat-error *ngIf="exportFormGroup.get('versionName').hasError('required')">
{{ 'version-control.version-name-required' | translate }}
</mat-error>
</mat-form-field>
<mat-checkbox formControlName="saveRelations" style="margin-bottom: 16px;">
{{ 'version-control.export-entity-relations' | translate }}
</mat-checkbox>
</div>
</fieldset>
</form>
<div *ngIf="createResult" fxFlex fxLayout="column" fxLayoutAlign="center">
<div [innerHTML]="createResultMessage"></div>
</div>
</div>
<div *ngIf="!createResult" mat-dialog-actions fxLayoutAlign="end center">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="button"
(click)="export()"
[disabled]="(isLoading$ | async) || exportFormGroup.invalid || !exportFormGroup.dirty">
{{ 'action.export' | translate }}
</button>
</div>
<div *ngIf="createResult" mat-dialog-actions fxLayoutAlign="end center">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.close' | translate }}
</button>
</div>
</section>

105
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<VcEntityExportDialogComponent>
implements OnInit, ErrorStateMatcher {
exportFormGroup: FormGroup;
submitted = false;
createResult: VersionCreationResult;
createResultMessage: SafeHtml;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: VcEntityExportDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<VcEntityExportDialogComponent>,
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);
});
}
}

17
ui-ngx/src/app/modules/home/dialogs/home-dialogs.service.ts

@ -22,6 +22,11 @@ import {
ImportDialogCsvComponent, ImportDialogCsvComponent,
ImportDialogCsvData ImportDialogCsvData
} from '@home/components/import-export/import-dialog-csv.component'; } 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() @Injectable()
export class HomeDialogsService { export class HomeDialogsService {
@ -41,6 +46,17 @@ export class HomeDialogsService {
} }
} }
public exportVcEntity(entityId: EntityId): Observable<void> {
return this.dialog.open<VcEntityExportDialogComponent, VcEntityExportDialogData>(VcEntityExportDialogComponent,
{
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
entityId
}
}).afterClosed();
}
private openImportDialogCSV(entityType: EntityType, importTitle: string, importFileLabel: string): Observable<boolean> { private openImportDialogCSV(entityType: EntityType, importTitle: string, importFileLabel: string): Observable<boolean> {
return this.dialog.open<ImportDialogCsvComponent, ImportDialogCsvData, return this.dialog.open<ImportDialogCsvComponent, ImportDialogCsvData,
any>(ImportDialogCsvComponent, { any>(ImportDialogCsvComponent, {
@ -53,4 +69,5 @@ export class HomeDialogsService {
} }
}).afterClosed(); }).afterClosed();
} }
} }

6
ui-ngx/src/app/modules/home/pages/asset/asset.component.html

@ -46,6 +46,12 @@
[fxShow]="!isEdit && assetScope === 'edge'"> [fxShow]="!isEdit && assetScope === 'edge'">
{{ 'edge.unassign-from-edge' | translate }} {{ 'edge.unassign-from-edge' | translate }}
</button> </button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'vcExport')"
[fxShow]="!isEdit && (assetScope === 'customer' || assetScope === 'tenant')">
{{'version-control.export-to-git' | translate }}
</button>
<button mat-raised-button color="primary" <button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'delete')" (click)="onEntityAction($event, 'delete')"

10
ui-ngx/src/app/modules/home/pages/asset/assets-table-config.resolver.ts

@ -468,6 +468,13 @@ export class AssetsTableConfigResolver implements Resolve<EntityTableConfig<Asse
); );
} }
vcExport($event: Event, asset: Asset) {
if ($event) {
$event.stopPropagation();
}
this.homeDialogs.exportVcEntity(asset.id);
}
onAssetAction(action: EntityAction<AssetInfo>, config: EntityTableConfig<AssetInfo>): boolean { onAssetAction(action: EntityAction<AssetInfo>, config: EntityTableConfig<AssetInfo>): boolean {
switch (action.action) { switch (action.action) {
case 'open': case 'open':
@ -485,6 +492,9 @@ export class AssetsTableConfigResolver implements Resolve<EntityTableConfig<Asse
case 'unassignFromEdge': case 'unassignFromEdge':
this.unassignFromEdge(action.event, action.entity); this.unassignFromEdge(action.event, action.entity);
return true; return true;
case 'vcExport':
this.vcExport(action.event, action.entity);
return true;
} }
return false; return false;
} }

6
ui-ngx/src/app/modules/home/pages/customer/customer.component.html

@ -53,6 +53,12 @@
*ngIf="edgesSupportEnabled()"> *ngIf="edgesSupportEnabled()">
{{'customer.manage-edges' | translate }} {{'customer.manage-edges' | translate }}
</button> </button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'vcExport')"
[fxShow]="!isEdit && !isPublic">
{{'version-control.export-to-git' | translate }}
</button>
<button mat-raised-button color="primary" <button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'delete')" (click)="onEntityAction($event, 'delete')"

12
ui-ngx/src/app/modules/home/pages/customer/customers-table-config.resolver.ts

@ -34,6 +34,7 @@ import { CustomerTabsComponent } from '@home/pages/customer/customer-tabs.compon
import { getCurrentAuthState } from '@core/auth/auth.selectors'; import { getCurrentAuthState } from '@core/auth/auth.selectors';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { HomeDialogsService } from '@home/dialogs/home-dialogs.service';
@Injectable() @Injectable()
export class CustomersTableConfigResolver implements Resolve<EntityTableConfig<Customer>> { export class CustomersTableConfigResolver implements Resolve<EntityTableConfig<Customer>> {
@ -41,6 +42,7 @@ export class CustomersTableConfigResolver implements Resolve<EntityTableConfig<C
private readonly config: EntityTableConfig<Customer> = new EntityTableConfig<Customer>(); private readonly config: EntityTableConfig<Customer> = new EntityTableConfig<Customer>();
constructor(private customerService: CustomerService, constructor(private customerService: CustomerService,
private homeDialogs: HomeDialogsService,
private translate: TranslateService, private translate: TranslateService,
private datePipe: DatePipe, private datePipe: DatePipe,
private router: Router, private router: Router,
@ -180,6 +182,13 @@ export class CustomersTableConfigResolver implements Resolve<EntityTableConfig<C
this.router.navigateByUrl(`customers/${customer.id.id}/edgeInstances`); this.router.navigateByUrl(`customers/${customer.id.id}/edgeInstances`);
} }
vcExport($event: Event, customer: Customer) {
if ($event) {
$event.stopPropagation();
}
this.homeDialogs.exportVcEntity(customer.id);
}
onCustomerAction(action: EntityAction<Customer>, config: EntityTableConfig<Customer>): boolean { onCustomerAction(action: EntityAction<Customer>, config: EntityTableConfig<Customer>): boolean {
switch (action.action) { switch (action.action) {
case 'open': case 'open':
@ -200,6 +209,9 @@ export class CustomersTableConfigResolver implements Resolve<EntityTableConfig<C
case 'manageEdges': case 'manageEdges':
this.manageCustomerEdges(action.event, action.entity); this.manageCustomerEdges(action.event, action.entity);
return true; return true;
case 'vcExport':
this.vcExport(action.event, action.entity);
return true;
} }
return false; return false;
} }

6
ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html

@ -59,6 +59,12 @@
[fxShow]="!isEdit && dashboardScope === 'edge'"> [fxShow]="!isEdit && dashboardScope === 'edge'">
{{ 'edge.unassign-from-edge' | translate }} {{ 'edge.unassign-from-edge' | translate }}
</button> </button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'vcExport')"
[fxShow]="!isEdit && dashboardScope === 'tenant'">
{{'version-control.export-to-git' | translate }}
</button>
<button mat-raised-button color="primary" <button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'delete')" (click)="onEntityAction($event, 'delete')"

12
ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts

@ -69,6 +69,7 @@ import {
AddEntitiesToEdgeDialogComponent, AddEntitiesToEdgeDialogComponent,
AddEntitiesToEdgeDialogData AddEntitiesToEdgeDialogData
} from '@home/dialogs/add-entities-to-edge-dialog.component'; } from '@home/dialogs/add-entities-to-edge-dialog.component';
import { HomeDialogsService } from '@home/dialogs/home-dialogs.service';
@Injectable() @Injectable()
export class DashboardsTableConfigResolver implements Resolve<EntityTableConfig<DashboardInfo | Dashboard>> { export class DashboardsTableConfigResolver implements Resolve<EntityTableConfig<DashboardInfo | Dashboard>> {
@ -80,6 +81,7 @@ export class DashboardsTableConfigResolver implements Resolve<EntityTableConfig<
private customerService: CustomerService, private customerService: CustomerService,
private edgeService: EdgeService, private edgeService: EdgeService,
private dialogService: DialogService, private dialogService: DialogService,
private homeDialogs: HomeDialogsService,
private importExport: ImportExportService, private importExport: ImportExportService,
private translate: TranslateService, private translate: TranslateService,
private datePipe: DatePipe, private datePipe: DatePipe,
@ -537,6 +539,13 @@ export class DashboardsTableConfigResolver implements Resolve<EntityTableConfig<
); );
} }
vcExport($event: Event, dashboard: DashboardInfo) {
if ($event) {
$event.stopPropagation();
}
this.homeDialogs.exportVcEntity(dashboard.id);
}
onDashboardAction(action: EntityAction<DashboardInfo>): boolean { onDashboardAction(action: EntityAction<DashboardInfo>): boolean {
switch (action.action) { switch (action.action) {
case 'open': case 'open':
@ -560,6 +569,9 @@ export class DashboardsTableConfigResolver implements Resolve<EntityTableConfig<
case 'unassignFromEdge': case 'unassignFromEdge':
this.unassignFromEdge(action.event, action.entity); this.unassignFromEdge(action.event, action.entity);
return true; return true;
case 'vcExport':
this.vcExport(action.event, action.entity);
return true;
} }
return false; return false;
} }

12
ui-ngx/src/app/modules/home/pages/device-profile/device-profiles-table-config.resolver.ts

@ -42,6 +42,7 @@ import {
AddDeviceProfileDialogData AddDeviceProfileDialogData
} from '@home/components/profile/add-device-profile-dialog.component'; } from '@home/components/profile/add-device-profile-dialog.component';
import { ImportExportService } from '@home/components/import-export/import-export.service'; import { ImportExportService } from '@home/components/import-export/import-export.service';
import { HomeDialogsService } from '@home/dialogs/home-dialogs.service';
@Injectable() @Injectable()
export class DeviceProfilesTableConfigResolver implements Resolve<EntityTableConfig<DeviceProfile>> { export class DeviceProfilesTableConfigResolver implements Resolve<EntityTableConfig<DeviceProfile>> {
@ -50,6 +51,7 @@ export class DeviceProfilesTableConfigResolver implements Resolve<EntityTableCon
constructor(private deviceProfileService: DeviceProfileService, constructor(private deviceProfileService: DeviceProfileService,
private importExport: ImportExportService, private importExport: ImportExportService,
private homeDialogs: HomeDialogsService,
private translate: TranslateService, private translate: TranslateService,
private datePipe: DatePipe, private datePipe: DatePipe,
private dialogService: DialogService, private dialogService: DialogService,
@ -204,6 +206,13 @@ export class DeviceProfilesTableConfigResolver implements Resolve<EntityTableCon
this.importExport.exportDeviceProfile(deviceProfile.id.id); this.importExport.exportDeviceProfile(deviceProfile.id.id);
} }
vcExport($event: Event, deviceProfile: DeviceProfile) {
if ($event) {
$event.stopPropagation();
}
this.homeDialogs.exportVcEntity(deviceProfile.id);
}
onDeviceProfileAction(action: EntityAction<DeviceProfile>): boolean { onDeviceProfileAction(action: EntityAction<DeviceProfile>): boolean {
switch (action.action) { switch (action.action) {
case 'open': case 'open':
@ -215,6 +224,9 @@ export class DeviceProfilesTableConfigResolver implements Resolve<EntityTableCon
case 'export': case 'export':
this.exportDeviceProfile(action.event, action.entity); this.exportDeviceProfile(action.event, action.entity);
return true; return true;
case 'vcExport':
this.vcExport(action.event, action.entity);
return true;
} }
return false; return false;
} }

6
ui-ngx/src/app/modules/home/pages/device/device.component.html

@ -52,6 +52,12 @@
[fxShow]="!isEdit && deviceScope === 'edge'"> [fxShow]="!isEdit && deviceScope === 'edge'">
{{ 'edge.unassign-from-edge' | translate }} {{ 'edge.unassign-from-edge' | translate }}
</button> </button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'vcExport')"
[fxShow]="!isEdit && (deviceScope === 'customer' || deviceScope === 'tenant')">
{{'version-control.export-to-git' | translate }}
</button>
<button mat-raised-button color="primary" fxFlex.xs <button mat-raised-button color="primary" fxFlex.xs
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'delete')" (click)="onEntityAction($event, 'delete')"

10
ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts

@ -547,6 +547,13 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
}); });
} }
vcExport($event: Event, device: Device) {
if ($event) {
$event.stopPropagation();
}
this.homeDialogs.exportVcEntity(device.id);
}
onDeviceAction(action: EntityAction<DeviceInfo>, config: EntityTableConfig<DeviceInfo>): boolean { onDeviceAction(action: EntityAction<DeviceInfo>, config: EntityTableConfig<DeviceInfo>): boolean {
switch (action.action) { switch (action.action) {
case 'open': case 'open':
@ -567,6 +574,9 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
case 'manageCredentials': case 'manageCredentials':
this.manageCredentials(action.event, action.entity); this.manageCredentials(action.event, action.entity);
return true; return true;
case 'vcExport':
this.vcExport(action.event, action.entity);
return true;
} }
return false; return false;
} }

6
ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.html

@ -58,6 +58,12 @@
[fxShow]="!isEdit && !isEdgeRootRuleChain() && ruleChainScope === 'edge'"> [fxShow]="!isEdit && !isEdgeRootRuleChain() && ruleChainScope === 'edge'">
{{'edge.unassign-from-edge' | translate }} {{'edge.unassign-from-edge' | translate }}
</button> </button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'vcExport')"
[fxShow]="!isEdit">
{{'version-control.export-to-git' | translate }}
</button>
<button mat-raised-button color="primary" <button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'delete')" (click)="onEntityAction($event, 'delete')"

12
ui-ngx/src/app/modules/home/pages/rulechain/rulechains-table-config.resolver.ts

@ -50,6 +50,7 @@ import { PageLink } from '@shared/models/page/page-link';
import { Edge } from '@shared/models/edge.models'; import { Edge } from '@shared/models/edge.models';
import { mergeMap } from 'rxjs/operators'; import { mergeMap } from 'rxjs/operators';
import { PageData } from '@shared/models/page/page-data'; import { PageData } from '@shared/models/page/page-data';
import { HomeDialogsService } from '@home/dialogs/home-dialogs.service';
@Injectable() @Injectable()
export class RuleChainsTableConfigResolver implements Resolve<EntityTableConfig<RuleChain>> { export class RuleChainsTableConfigResolver implements Resolve<EntityTableConfig<RuleChain>> {
@ -63,6 +64,7 @@ export class RuleChainsTableConfigResolver implements Resolve<EntityTableConfig<
private importExport: ImportExportService, private importExport: ImportExportService,
private itembuffer: ItemBufferService, private itembuffer: ItemBufferService,
private edgeService: EdgeService, private edgeService: EdgeService,
private homeDialogs: HomeDialogsService,
private translate: TranslateService, private translate: TranslateService,
private datePipe: DatePipe, private datePipe: DatePipe,
private router: Router) { private router: Router) {
@ -358,6 +360,13 @@ export class RuleChainsTableConfigResolver implements Resolve<EntityTableConfig<
); );
} }
vcExport($event: Event, ruleChain: RuleChain) {
if ($event) {
$event.stopPropagation();
}
this.homeDialogs.exportVcEntity(ruleChain.id);
}
onRuleChainAction(action: EntityAction<RuleChain>): boolean { onRuleChainAction(action: EntityAction<RuleChain>): boolean {
switch (action.action) { switch (action.action) {
case 'open': case 'open':
@ -381,6 +390,9 @@ export class RuleChainsTableConfigResolver implements Resolve<EntityTableConfig<
case 'unsetAutoAssignToEdge': case 'unsetAutoAssignToEdge':
this.unsetAutoAssignToEdgeRuleChain(action.event, action.entity); this.unsetAutoAssignToEdgeRuleChain(action.event, action.entity);
return true; return true;
case 'vcExport':
this.vcExport(action.event, action.entity);
return true;
} }
return false; return false;
} }

43
ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.html

@ -0,0 +1,43 @@
<!--
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.
-->
<mat-form-field [formGroup]="branchFormGroup" class="mat-block">
<mat-label>{{ 'version-control.branch' | translate }}</mat-label>
<input matInput type="text" placeholder="{{ 'version-control.select-branch' | translate }}"
#branchInput
formControlName="branch"
(focusin)="onFocus()"
[required]="required"
[matAutocomplete]="subTypeAutocomplete">
<button *ngIf="branchFormGroup.get('branch').value && !disabled"
type="button"
matSuffix mat-button mat-icon-button aria-label="Clear"
(click)="clear()">
<mat-icon class="material-icons">close</mat-icon>
</button>
<mat-autocomplete
class="tb-autocomplete"
#subTypeAutocomplete="matAutocomplete"
[displayWith]="displayBranchFn">
<mat-option *ngFor="let branch of filteredBranches | async" [value]="branch">
<span [innerHTML]="branch | highlight:searchText"></span>
</mat-option>
</mat-autocomplete>
<mat-error *ngIf="branchFormGroup.get('branch').hasError('required')">
{{ 'version-control.branch-required' | translate }}
</mat-error>
</mat-form-field>

199
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<Array<string>>;
branches: Observable<Array<BranchInfo>>;
searchText = '';
private dirty = false;
private propagateChange = (v: any) => { };
constructor(private store: Store<AppState>,
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<Array<string>> {
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<Array<BranchInfo>> {
if (!this.branches) {
const branchesObservable = this.entitiesVersionControlService.listBranches({ignoreLoading: true, ignoreErrors: true});
this.branches = branchesObservable.pipe(
catchError(() => of([] as Array<BranchInfo>)),
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);
}
}

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

7
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 { SafePipe } from '@shared/pipe/safe.pipe';
import { DragDropModule } from '@angular/cdk/drag-drop'; import { DragDropModule } from '@angular/cdk/drag-drop';
import { MultipleImageInputComponent } from '@shared/components/multiple-image-input.component'; import { MultipleImageInputComponent } from '@shared/components/multiple-image-input.component';
import { BranchAutocompleteComponent } from '@shared/components/vc/branch-autocomplete.component';
export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
return markedOptionsService; return markedOptionsService;
@ -284,7 +285,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
WidgetsBundleSearchComponent, WidgetsBundleSearchComponent,
CopyButtonComponent, CopyButtonComponent,
TogglePasswordComponent, TogglePasswordComponent,
ProtobufContentComponent ProtobufContentComponent,
BranchAutocompleteComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -484,7 +486,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
WidgetsBundleSearchComponent, WidgetsBundleSearchComponent,
CopyButtonComponent, CopyButtonComponent,
TogglePasswordComponent, TogglePasswordComponent,
ProtobufContentComponent ProtobufContentComponent,
BranchAutocompleteComponent
] ]
}) })
export class SharedModule { } export class SharedModule { }

12
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-invalid": "JSON value has an invalid format",
"json-value-required": "JSON value is required." "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": {
"widget-library": "Widgets Library", "widget-library": "Widgets Library",
"widget-bundle": "Widgets Bundle", "widget-bundle": "Widgets Bundle",

Loading…
Cancel
Save