Browse Source

Merge branch 'feature/entities-version-control' of github.com:thingsboard/thingsboard into feature/entities-version-control

pull/6597/head
Andrii Shvaika 4 years ago
parent
commit
1fde08c90c
  1. 7
      application/src/main/java/org/thingsboard/server/controller/EntitiesVersionControlController.java
  2. 2
      application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitVersionControlQueueService.java
  3. 3
      common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java
  4. 11
      ui-ngx/src/app/core/auth/auth.actions.ts
  5. 1
      ui-ngx/src/app/core/auth/auth.models.ts
  6. 6
      ui-ngx/src/app/core/auth/auth.reducer.ts
  7. 5
      ui-ngx/src/app/core/auth/auth.selectors.ts
  8. 12
      ui-ngx/src/app/core/auth/auth.service.ts
  9. 27
      ui-ngx/src/app/core/http/entities-version-control.service.ts
  10. 17
      ui-ngx/src/app/core/services/menu.service.ts
  11. 13
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  12. 81
      ui-ngx/src/app/modules/home/components/vc/entity-versions-table.component.html
  13. 100
      ui-ngx/src/app/modules/home/components/vc/entity-versions-table.component.scss
  14. 244
      ui-ngx/src/app/modules/home/components/vc/entity-versions-table.component.ts
  15. 2
      ui-ngx/src/app/modules/home/components/vc/version-control-settings.component.html
  16. 3
      ui-ngx/src/app/modules/home/components/vc/version-control-settings.component.scss
  17. 53
      ui-ngx/src/app/modules/home/components/vc/version-control-settings.component.ts
  18. 25
      ui-ngx/src/app/modules/home/components/vc/version-control.component.html
  19. 18
      ui-ngx/src/app/modules/home/components/vc/version-control.component.scss
  20. 61
      ui-ngx/src/app/modules/home/components/vc/version-control.component.ts
  21. 4
      ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts
  22. 4
      ui-ngx/src/app/modules/home/pages/admin/admin.module.ts
  23. 18
      ui-ngx/src/app/modules/home/pages/admin/version-control-admin-settings.component.html
  24. 44
      ui-ngx/src/app/modules/home/pages/admin/version-control-admin-settings.component.ts
  25. 5
      ui-ngx/src/app/modules/home/pages/device/device-tabs.component.html
  26. 4
      ui-ngx/src/app/modules/home/pages/home-pages.module.ts
  27. 44
      ui-ngx/src/app/modules/home/pages/vc/vc-routing.module.ts
  28. 31
      ui-ngx/src/app/modules/home/pages/vc/vc.module.ts
  29. 5
      ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.html
  30. 147
      ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.ts
  31. 4
      ui-ngx/src/app/shared/models/asset.models.ts
  32. 6
      ui-ngx/src/app/shared/models/base-data.ts
  33. 3
      ui-ngx/src/app/shared/models/customer.model.ts
  34. 4
      ui-ngx/src/app/shared/models/dashboard.models.ts
  35. 6
      ui-ngx/src/app/shared/models/device.models.ts
  36. 4
      ui-ngx/src/app/shared/models/rule-chain.models.ts
  37. 1
      ui-ngx/src/app/shared/models/vc.models.ts
  38. 10
      ui-ngx/src/assets/locale/locale.constant-en_US.json

7
application/src/main/java/org/thingsboard/server/controller/EntitiesVersionControlController.java

@ -294,11 +294,14 @@ public class EntitiesVersionControlController extends BaseController {
String defaultBranch = versionControlService.getVersionControlSettings(tenantId).getDefaultBranch();
if (StringUtils.isNotEmpty(defaultBranch)) {
remoteBranches.remove(defaultBranch);
infos.add(new BranchInfo(defaultBranch, true));
}
remoteBranches.forEach(branch -> infos.add(new BranchInfo(branch, false)));
remoteBranches.forEach(branch -> {
if (!branch.equals(defaultBranch)) {
infos.add(new BranchInfo(branch, false));
}
});
return infos;
}, MoreExecutors.directExecutor()));
} catch (Exception e) {

2
application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitVersionControlQueueService.java

@ -411,7 +411,7 @@ public class DefaultGitVersionControlQueueService implements GitVersionControlQu
}
if (vcSettings != null) {
builder.setVcSettings(ByteString.copyFrom(encodingService.encode(vcSettings)));
} else {
} else if (request.requiresSettings()) {
throw new RuntimeException("No entity version control settings provisioned!");
}
return builder;

3
common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java

@ -170,7 +170,7 @@ public class GitRepository {
public PageData<Commit> listCommits(String branch, String path, PageLink pageLink) throws IOException, GitAPIException {
ObjectId branchId = resolve("origin/" + branch);
if (branchId == null) {
throw new IllegalArgumentException("Branch not found");
return new PageData<>();
}
LogCommand command = git.log()
.add(branchId)
@ -313,6 +313,7 @@ public class GitRepository {
Function<? super T, ? extends R> mapper,
PageLink pageLink,
Function<PageLink, Comparator<T>> comparatorFunction) {
iterable = Streams.stream(iterable).collect(Collectors.toList());
int totalElements = Iterables.size(iterable);
int totalPages = pageLink.getPageSize() > 0 ? (int) Math.ceil((float) totalElements / pageLink.getPageSize()) : 1;
int startIndex = pageLink.getPageSize() * pageLink.getPage();

11
ui-ngx/src/app/core/auth/auth.actions.ts

@ -23,7 +23,8 @@ export enum AuthActionTypes {
UNAUTHENTICATED = '[Auth] Unauthenticated',
LOAD_USER = '[Auth] Load User',
UPDATE_USER_DETAILS = '[Auth] Update User Details',
UPDATE_LAST_PUBLIC_DASHBOARD_ID = '[Auth] Update Last Public Dashboard Id'
UPDATE_LAST_PUBLIC_DASHBOARD_ID = '[Auth] Update Last Public Dashboard Id',
UPDATE_HAS_VERSION_CONTROL = '[Auth] Change Has Version Control'
}
export class ActionAuthAuthenticated implements Action {
@ -54,5 +55,11 @@ export class ActionAuthUpdateLastPublicDashboardId implements Action {
constructor(readonly payload: { lastPublicDashboardId: string }) {}
}
export class ActionAuthUpdateHasVersionControl implements Action {
readonly type = AuthActionTypes.UPDATE_HAS_VERSION_CONTROL;
constructor(readonly payload: { hasVersionControl: boolean }) {}
}
export type AuthActions = ActionAuthAuthenticated | ActionAuthUnauthenticated |
ActionAuthLoadUser | ActionAuthUpdateUserDetails | ActionAuthUpdateLastPublicDashboardId;
ActionAuthLoadUser | ActionAuthUpdateUserDetails | ActionAuthUpdateLastPublicDashboardId | ActionAuthUpdateHasVersionControl;

1
ui-ngx/src/app/core/auth/auth.models.ts

@ -20,6 +20,7 @@ export interface SysParamsState {
userTokenAccessEnabled: boolean;
allowedDashboardIds: string[];
edgesSupportEnabled: boolean;
hasVersionControl: boolean;
}
export interface AuthPayload extends SysParamsState {

6
ui-ngx/src/app/core/auth/auth.reducer.ts

@ -23,7 +23,8 @@ const emptyUserAuthState: AuthPayload = {
userTokenAccessEnabled: false,
forceFullscreen: false,
allowedDashboardIds: [],
edgesSupportEnabled: false
edgesSupportEnabled: false,
hasVersionControl: false
};
export const initialState: AuthState = {
@ -54,6 +55,9 @@ export function authReducer(
case AuthActionTypes.UPDATE_LAST_PUBLIC_DASHBOARD_ID:
return { ...state, ...action.payload};
case AuthActionTypes.UPDATE_HAS_VERSION_CONTROL:
return { ...state, ...action.payload};
default:
return state;
}

5
ui-ngx/src/app/core/auth/auth.selectors.ts

@ -55,6 +55,11 @@ export const selectUserTokenAccessEnabled = createSelector(
(state: AuthState) => state.userTokenAccessEnabled
);
export const selectHasVersionControl = createSelector(
selectAuthState,
(state: AuthState) => state.hasVersionControl
);
export function getCurrentAuthState(store: Store<AppState>): AuthState {
let state: AuthState;
store.pipe(select(selectAuth), take(1)).subscribe(

12
ui-ngx/src/app/core/auth/auth.service.ts

@ -437,17 +437,27 @@ export class AuthService {
return this.http.get<boolean>('/api/edges/enabled', defaultHttpOptions());
}
private loadHasVersionControl(authUser: AuthUser): Observable<boolean> {
if (authUser.authority === Authority.TENANT_ADMIN) {
return this.http.get<boolean>('/api/admin/vcSettings/exists', defaultHttpOptions());
} else {
return of(false);
}
}
private loadSystemParams(authPayload: AuthPayload): Observable<SysParamsState> {
const sources = [this.loadIsUserTokenAccessEnabled(authPayload.authUser),
this.fetchAllowedDashboardIds(authPayload),
this.loadIsEdgesSupportEnabled(),
this.loadHasVersionControl(authPayload.authUser),
this.timeService.loadMaxDatapointsLimit()];
return forkJoin(sources)
.pipe(map((data) => {
const userTokenAccessEnabled: boolean = data[0] as boolean;
const allowedDashboardIds: string[] = data[1] as string[];
const edgesSupportEnabled: boolean = data[2] as boolean;
return {userTokenAccessEnabled, allowedDashboardIds, edgesSupportEnabled};
const hasVersionControl: boolean = data[3] as boolean;
return {userTokenAccessEnabled, allowedDashboardIds, edgesSupportEnabled, hasVersionControl};
}, catchError((err) => {
return of({});
})));

27
ui-ngx/src/app/core/http/entities-version-control.service.ts

@ -18,7 +18,12 @@ 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';
import { BranchInfo, EntityVersion, VersionCreateRequest, VersionCreationResult } from '@shared/models/vc.models';
import { PageLink } from '@shared/models/page/page-link';
import { PageData } from '@shared/models/page/page-data';
import { DeviceInfo } from '@shared/models/device.models';
import { EntityId } from '@shared/models/id/entity-id';
import { EntityType } from '@shared/models/entity-type.models';
@Injectable({
providedIn: 'root'
@ -37,4 +42,24 @@ export class EntitiesVersionControlService {
public saveEntitiesVersion(request: VersionCreateRequest, config?: RequestConfig): Observable<VersionCreationResult> {
return this.http.post<VersionCreationResult>('/api/entities/vc/version', request, defaultHttpOptionsFromConfig(config));
}
public listEntityVersions(pageLink: PageLink, branch: string,
externalEntityId: EntityId,
config?: RequestConfig): Observable<PageData<EntityVersion>> {
return this.http.get<PageData<EntityVersion>>(`/api/entities/vc/version/${branch}/${externalEntityId.entityType}/${externalEntityId.id}${pageLink.toQuery()}`,
defaultHttpOptionsFromConfig(config));
}
public listEntityTypeVersions(pageLink: PageLink, branch: string,
entityType: EntityType,
config?: RequestConfig): Observable<PageData<EntityVersion>> {
return this.http.get<PageData<EntityVersion>>(`/api/entities/vc/version/${branch}/${entityType}${pageLink.toQuery()}`,
defaultHttpOptionsFromConfig(config));
}
public listVersions(pageLink: PageLink, branch: string,
config?: RequestConfig): Observable<PageData<EntityVersion>> {
return this.http.get<PageData<EntityVersion>>(`/api/entities/vc/version/${branch}${pageLink.toQuery()}`,
defaultHttpOptionsFromConfig(config));
}
}

17
ui-ngx/src/app/core/services/menu.service.ts

@ -343,6 +343,13 @@ export class MenuService {
path: '/dashboards',
icon: 'dashboards'
},
{
id: guid(),
name: 'version-control.version-control',
type: 'link',
path: '/vc',
icon: 'history'
},
{
id: guid(),
name: 'audit-log.audit-logs',
@ -492,6 +499,16 @@ export class MenuService {
}
]
},
{
name: 'version-control.management',
places: [
{
name: 'version-control.version-control',
icon: 'history',
path: '/vc'
}
]
},
{
name: 'audit-log.audit',
places: [

13
ui-ngx/src/app/modules/home/components/home-components.module.ts

@ -154,6 +154,9 @@ import { QueueFormComponent } from '@home/components/queue/queue-form.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';
import { VersionControlSettingsComponent } from '@home/components/vc/version-control-settings.component';
import { VersionControlComponent } from '@home/components/vc/version-control.component';
import { EntityVersionsTableComponent } from '@home/components/vc/entity-versions-table.component';
@NgModule({
declarations:
@ -278,7 +281,10 @@ import { VcEntityExportDialogComponent } from '@home/components/vc/vc-entity-exp
DisplayWidgetTypesPanelComponent,
TenantProfileQueuesComponent,
QueueFormComponent,
VcEntityExportDialogComponent
VcEntityExportDialogComponent,
VersionControlSettingsComponent,
VersionControlComponent,
EntityVersionsTableComponent
],
imports: [
CommonModule,
@ -397,7 +403,10 @@ import { VcEntityExportDialogComponent } from '@home/components/vc/vc-entity-exp
DisplayWidgetTypesPanelComponent,
TenantProfileQueuesComponent,
QueueFormComponent,
VcEntityExportDialogComponent
VcEntityExportDialogComponent,
VersionControlSettingsComponent,
VersionControlComponent,
EntityVersionsTableComponent
],
providers: [
WidgetComponentService,

81
ui-ngx/src/app/modules/home/components/vc/entity-versions-table.component.html

@ -0,0 +1,81 @@
<!--
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 class="mat-padding tb-entity-table tb-absolute-fill">
<div fxFlex fxLayout="column" class="mat-elevation-z1 tb-entity-table-content">
<mat-toolbar class="mat-table-toolbar">
<div class="mat-toolbar-tools">
<div fxLayout="row" fxLayoutAlign="start center" fxLayout.xs="column" fxLayoutAlign.xs="center start" class="title-container">
<span class="tb-entity-table-title">{{(singleEntityMode ? 'version-control.entity-versions' : 'version-control.versions') | translate}}</span>
<tb-branch-autocomplete
#branchAutocompleteComponent
[selectionMode]="true"
[selectDefaultBranch]="false"
[disabled]="isLoading$ | async"
[ngModel]="branch"
(ngModelChange)="branchChanged($event)">
</tb-branch-autocomplete>
</div>
<span fxFlex></span>
<button *ngIf="singleEntityMode" mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="vcExport($event)">
{{'version-control.export-to-git' | translate }}
</button>
</div>
</mat-toolbar>
<div fxFlex class="table-container">
<table mat-table [dataSource]="dataSource"
matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()" matSortDisableClear>
<ng-container matColumnDef="timestamp">
<mat-header-cell *matHeaderCellDef mat-sort-header style="min-width: 150px; max-width: 150px; width: 150px;"> {{ 'version-control.created-time' | translate }} </mat-header-cell>
<mat-cell *matCellDef="let entityVersion">
{{ entityVersion.timestamp | date:'yyyy-MM-dd HH:mm:ss' }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="id">
<mat-header-cell *matHeaderCellDef style="width: 40%"> {{ 'version-control.version-id' | translate }} </mat-header-cell>
<mat-cell *matCellDef="let entityVersion">
{{ entityVersion.id }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="name">
<mat-header-cell *matHeaderCellDef style="width: 60%"> {{ 'version-control.version-name' | translate }} </mat-header-cell>
<mat-cell *matCellDef="let entityVersion">
{{ entityVersion.name }}
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
<mat-row *matRowDef="let entityVersion; columns: displayedColumns;"></mat-row>
</table>
<span [fxShow]="dataSource.isEmpty() | async"
fxLayoutAlign="center center"
class="no-data-found" translate>{{
singleEntityMode
? 'version-control.no-entity-versions-text'
: 'version-control.no-versions-text'
}}</span>
</div>
<mat-divider></mat-divider>
<mat-paginator [length]="dataSource.total() | async"
[pageIndex]="pageLink.page"
[pageSize]="pageLink.pageSize"
[pageSizeOptions]="[10, 20, 30]"
[hidePageSize]="hidePageSize"
showFirstLastButtons></mat-paginator>
</div>
</div>

100
ui-ngx/src/app/modules/home/components/vc/entity-versions-table.component.scss

@ -0,0 +1,100 @@
/**
* 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 '../../../../../scss/constants';
:host {
width: 100%;
height: 100%;
display: block;
.tb-entity-table {
.tb-entity-table-content {
width: 100%;
height: 100%;
background: #fff;
.mat-toolbar-tools{
min-height: auto;
}
.title-container{
overflow: hidden;
}
.tb-entity-table-title {
padding-right: 20px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.table-container {
overflow: auto;
}
.tb-entity-table-info{
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.button-widget-action{
margin-left: auto;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
@media #{$mat-xs} {
.mat-toolbar {
height: auto;
min-height: 100px;
.tb-entity-table-title{
padding-bottom: 5px;
width: 100%;
}
}
}
}
:host ::ng-deep {
.mat-sort-header-sorted .mat-sort-header-arrow {
opacity: 1 !important;
}
tb-branch-autocomplete {
mat-form-field {
font-size: 16px;
width: 200px;
.mat-form-field-wrapper {
padding-bottom: 0;
}
.mat-form-field-underline {
bottom: 0;
}
@media #{$mat-xs} {
width: 100%;
.mat-form-field-infix {
width: auto !important;
}
}
}
}
}

244
ui-ngx/src/app/modules/home/components/vc/entity-versions-table.component.ts

@ -0,0 +1,244 @@
///
/// 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,
ChangeDetectorRef,
Component,
ElementRef,
Input,
OnDestroy,
OnInit,
ViewChild
} from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { EntityId } from '@shared/models/id/entity-id';
import { CollectionViewer, DataSource } from '@angular/cdk/collections';
import { BehaviorSubject, merge, Observable, of, ReplaySubject } from 'rxjs';
import { emptyPageData, PageData } from '@shared/models/page/page-data';
import { PageLink } from '@shared/models/page/page-link';
import { catchError, map, tap } from 'rxjs/operators';
import { EntityVersion } from '@shared/models/vc.models';
import { EntitiesVersionControlService } from '@core/http/entities-version-control.service';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { ResizeObserver } from '@juggle/resize-observer';
import { hidePageSizePixelValue } from '@shared/models/constants';
import { Direction, SortOrder } from '@shared/models/page/sort-order';
import { BranchAutocompleteComponent } from '@shared/components/vc/branch-autocomplete.component';
@Component({
selector: 'tb-entity-versions-table',
templateUrl: './entity-versions-table.component.html',
styleUrls: ['./entity-versions-table.component.scss']
})
export class EntityVersionsTableComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy {
@ViewChild('branchAutocompleteComponent') branchAutocompleteComponent: BranchAutocompleteComponent;
@Input()
singleEntityMode = false;
displayedColumns = ['timestamp', 'id', 'name'];
pageLink: PageLink;
dataSource: EntityVersionsDatasource;
hidePageSize = false;
branch: string = null;
activeValue = false;
dirtyValue = false;
externalEntityIdValue: EntityId;
viewsInited = false;
private componentResize$: ResizeObserver;
@Input()
set active(active: boolean) {
if (this.activeValue !== active) {
this.activeValue = active;
if (this.activeValue && this.dirtyValue) {
this.dirtyValue = false;
if (this.viewsInited) {
this.initFromDefaultBranch();
}
}
}
}
@Input()
set externalEntityId(externalEntityId: EntityId) {
if (this.externalEntityIdValue !== externalEntityId) {
this.externalEntityIdValue = externalEntityId;
this.resetSortAndFilter(this.activeValue);
if (!this.activeValue) {
this.dirtyValue = true;
}
}
}
@ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort;
constructor(protected store: Store<AppState>,
private entitiesVersionControlService: EntitiesVersionControlService,
private cd: ChangeDetectorRef,
private elementRef: ElementRef) {
super(store);
this.dirtyValue = !this.activeValue;
const sortOrder: SortOrder = { property: 'timestamp', direction: Direction.DESC };
this.pageLink = new PageLink(10, 0, null, sortOrder);
this.dataSource = new EntityVersionsDatasource(this.entitiesVersionControlService);
}
ngOnInit() {
this.componentResize$ = new ResizeObserver(() => {
const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue;
if (showHidePageSize !== this.hidePageSize) {
this.hidePageSize = showHidePageSize;
this.cd.markForCheck();
}
});
this.componentResize$.observe(this.elementRef.nativeElement);
}
ngOnDestroy() {
if (this.componentResize$) {
this.componentResize$.disconnect();
}
}
branchChanged(newBranch: string) {
this.branch = newBranch;
this.paginator.pageIndex = 0;
if (this.activeValue) {
this.updateData();
}
}
ngAfterViewInit() {
this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0);
merge(this.sort.sortChange, this.paginator.page)
.pipe(
tap(() => this.updateData())
)
.subscribe();
this.viewsInited = true;
if (!this.singleEntityMode) {
this.initFromDefaultBranch();
}
}
vcExport($event: Event) {
if ($event) {
$event.stopPropagation();
}
}
private initFromDefaultBranch() {
this.branchAutocompleteComponent.selectDefaultBranchIfNeeded(false, true);
}
private updateData() {
this.pageLink.page = this.paginator.pageIndex;
this.pageLink.pageSize = this.paginator.pageSize;
this.pageLink.sortOrder.property = this.sort.active;
this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()];
this.dataSource.loadEntityVersions(this.singleEntityMode, this.branch, this.externalEntityIdValue, this.pageLink);
}
private resetSortAndFilter(update: boolean) {
this.branch = null;
this.pageLink.textSearch = null;
if (this.viewsInited) {
this.paginator.pageIndex = 0;
const sortable = this.sort.sortables.get('timestamp');
this.sort.active = sortable.id;
this.sort.direction = 'desc';
if (update) {
this.initFromDefaultBranch();
}
}
}
}
class EntityVersionsDatasource implements DataSource<EntityVersion> {
private entityVersionsSubject = new BehaviorSubject<EntityVersion[]>([]);
private pageDataSubject = new BehaviorSubject<PageData<EntityVersion>>(emptyPageData<EntityVersion>());
public pageData$ = this.pageDataSubject.asObservable();
constructor(private entitiesVersionControlService: EntitiesVersionControlService) {}
connect(collectionViewer: CollectionViewer): Observable<EntityVersion[] | ReadonlyArray<EntityVersion>> {
return this.entityVersionsSubject.asObservable();
}
disconnect(collectionViewer: CollectionViewer): void {
this.entityVersionsSubject.complete();
this.pageDataSubject.complete();
}
loadEntityVersions(singleEntityMode: boolean,
branch: string, externalEntityId: EntityId,
pageLink: PageLink): Observable<PageData<EntityVersion>> {
const result = new ReplaySubject<PageData<EntityVersion>>();
this.fetchEntityVersions(singleEntityMode, branch, externalEntityId, pageLink).pipe(
catchError(() => of(emptyPageData<EntityVersion>())),
).subscribe(
(pageData) => {
this.entityVersionsSubject.next(pageData.data);
this.pageDataSubject.next(pageData);
result.next(pageData);
}
);
return result;
}
fetchEntityVersions(singleEntityMode: boolean,
branch: string, externalEntityId: EntityId,
pageLink: PageLink): Observable<PageData<EntityVersion>> {
if (!branch) {
return of(emptyPageData<EntityVersion>());
} else {
if (singleEntityMode) {
if (externalEntityId) {
return this.entitiesVersionControlService.listEntityVersions(pageLink, branch, externalEntityId, {ignoreErrors: true});
} else {
return of(emptyPageData<EntityVersion>());
}
} else {
return this.entitiesVersionControlService.listVersions(pageLink, branch, {ignoreErrors: true});
}
}
}
isEmpty(): Observable<boolean> {
return this.entityVersionsSubject.pipe(
map((entityVersions) => !entityVersions.length)
);
}
total(): Observable<number> {
return this.pageDataSubject.pipe(
map((pageData) => pageData.totalElements)
);
}
}

2
ui-ngx/src/app/modules/home/pages/admin/version-control-settings.component.html → ui-ngx/src/app/modules/home/components/vc/version-control-settings.component.html

@ -16,7 +16,7 @@
-->
<div>
<mat-card class="settings-card">
<mat-card class="vc-settings" [ngClass]="{'settings-card': !detailsMode}">
<mat-card-title>
<div fxLayout="row">
<span class="mat-headline" translate>admin.git-repository-settings</span>

3
ui-ngx/src/app/modules/home/pages/admin/version-control-settings.component.scss → ui-ngx/src/app/modules/home/components/vc/version-control-settings.component.scss

@ -14,6 +14,9 @@
* limitations under the License.
*/
:host {
mat-card.vc-settings {
margin: 8px;
}
.fields-group {
padding: 0 16px 8px;
margin-bottom: 10px;

53
ui-ngx/src/app/modules/home/pages/admin/version-control-settings.component.ts → ui-ngx/src/app/modules/home/components/vc/version-control-settings.component.ts

@ -14,11 +14,11 @@
/// limitations under the License.
///
import { Component, OnInit } from '@angular/core';
import { Component, Input, 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 { select, Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { AdminService } from '@core/http/admin.service';
import {
@ -30,13 +30,21 @@ 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';
import { ActionSettingsChangeLanguage } from '@core/settings/settings.actions';
import { ActionAuthUpdateHasVersionControl } from '@core/auth/auth.actions';
import { selectHasVersionControl } from '@core/auth/auth.selectors';
import { catchError, mergeMap } from 'rxjs/operators';
import { of } from 'rxjs';
@Component({
selector: 'tb-version-control-settings',
templateUrl: './version-control-settings.component.html',
styleUrls: ['./version-control-settings.component.scss', './settings-card.scss']
styleUrls: ['./version-control-settings.component.scss', './../../pages/admin/settings-card.scss']
})
export class VersionControlSettingsComponent extends PageComponent implements OnInit, HasConfirmForm {
export class VersionControlSettingsComponent extends PageComponent implements OnInit {
@Input()
detailsMode = false;
versionControlSettingsForm: FormGroup;
settings: EntitiesVersionControlSettings = null;
@ -62,7 +70,7 @@ export class VersionControlSettingsComponent extends PageComponent implements On
ngOnInit() {
this.versionControlSettingsForm = this.fb.group({
repositoryUri: [null, [Validators.required]],
defaultBranch: [null, []],
defaultBranch: ['main', []],
authMethod: [VersionControlAuthMethod.USERNAME_PASSWORD, [Validators.required]],
username: [null, []],
password: [null, []],
@ -77,16 +85,29 @@ export class VersionControlSettingsComponent extends PageComponent implements On
this.versionControlSettingsForm.get('privateKeyFileName').valueChanges.subscribe(() => {
this.updateValidators(false);
});
this.adminService.getEntitiesVersionControlSettings({ignoreErrors: true}).subscribe(
this.store.pipe(
select(selectHasVersionControl),
mergeMap((hasVersionControl) => {
if (hasVersionControl) {
return this.adminService.getEntitiesVersionControlSettings({ignoreErrors: true}).pipe(
catchError(() => of(null))
);
} else {
return of(null);
}
})
).subscribe(
(settings) => {
this.settings = settings;
if (this.settings.authMethod === VersionControlAuthMethod.USERNAME_PASSWORD) {
this.showChangePassword = true;
} else {
this.showChangePrivateKeyPassword = true;
if (this.settings != null) {
if (this.settings.authMethod === VersionControlAuthMethod.USERNAME_PASSWORD) {
this.showChangePassword = true;
} else {
this.showChangePrivateKeyPassword = true;
}
this.versionControlSettingsForm.reset(this.settings);
this.updateValidators(false);
}
this.versionControlSettingsForm.reset(this.settings);
this.updateValidators(false);
});
}
@ -112,6 +133,7 @@ export class VersionControlSettingsComponent extends PageComponent implements On
}
this.versionControlSettingsForm.reset(this.settings);
this.updateValidators(false);
this.store.dispatch(new ActionAuthUpdateHasVersionControl({ hasVersionControl: true }));
}
);
}
@ -131,18 +153,15 @@ export class VersionControlSettingsComponent extends PageComponent implements On
this.showChangePrivateKeyPassword = false;
this.changePrivateKeyPassword = false;
formDirective.resetForm();
this.versionControlSettingsForm.reset({ authMethod: VersionControlAuthMethod.USERNAME_PASSWORD });
this.versionControlSettingsForm.reset({ defaultBranch: 'main', authMethod: VersionControlAuthMethod.USERNAME_PASSWORD });
this.updateValidators(false);
this.store.dispatch(new ActionAuthUpdateHasVersionControl({ hasVersionControl: false }));
}
);
}
});
}
confirmForm(): FormGroup {
return this.versionControlSettingsForm;
}
changePasswordChanged() {
if (this.changePassword) {
this.versionControlSettingsForm.get('password').patchValue('');

25
ui-ngx/src/app/modules/home/components/vc/version-control.component.html

@ -0,0 +1,25 @@
<!--
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.
-->
<tb-version-control-settings #versionControlSettingsComponent [detailsMode]="detailsMode"
*ngIf="!(hasVersionControl$ | async); else versionsTable">
</tb-version-control-settings>
<ng-template #versionsTable>
<tb-entity-versions-table [singleEntityMode]="singleEntityMode"
[active]="active"
[externalEntityId]="externalEntityId"></tb-entity-versions-table>
</ng-template>

18
ui-ngx/src/app/modules/home/components/vc/version-control.component.scss

@ -0,0 +1,18 @@
/**
* 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 {
}

61
ui-ngx/src/app/modules/home/components/vc/version-control.component.ts

@ -0,0 +1,61 @@
///
/// 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, Input, OnInit, ViewChild } from '@angular/core';
import { select, Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { selectHasVersionControl } from '@core/auth/auth.selectors';
import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard';
import { VersionControlSettingsComponent } from '@home/components/vc/version-control-settings.component';
import { FormGroup } from '@angular/forms';
import { EntityId } from '@shared/models/id/entity-id';
@Component({
selector: 'tb-version-control',
templateUrl: './version-control.component.html',
styleUrls: ['./version-control.component.scss']
})
export class VersionControlComponent implements OnInit, HasConfirmForm {
@ViewChild('versionControlSettingsComponent', {static: false}) versionControlSettingsComponent: VersionControlSettingsComponent;
@Input()
detailsMode = false;
@Input()
active = true;
@Input()
singleEntityMode = false;
@Input()
externalEntityId: EntityId;
hasVersionControl$ = this.store.pipe(select(selectHasVersionControl));
constructor(private store: Store<AppState>) {
}
ngOnInit() {
}
confirmForm(): FormGroup {
return this.versionControlSettingsComponent?.versionControlSettingsForm;
}
}

4
ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts

@ -33,7 +33,7 @@ import { EntityDetailsPageComponent } from '@home/components/entity/entity-detai
import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models';
import { BreadCrumbConfig } from '@shared/components/breadcrumb';
import { QueuesTableConfigResolver } from '@home/pages/admin/queue/queues-table-config.resolver';
import { VersionControlSettingsComponent } from '@home/pages/admin/version-control-settings.component';
import { VersionControlAdminSettingsComponent } from '@home/pages/admin/version-control-admin-settings.component';
@Injectable()
export class OAuth2LoginProcessingUrlResolver implements Resolve<string> {
@ -226,7 +226,7 @@ const routes: Routes = [
},
{
path: 'vc',
component: VersionControlSettingsComponent,
component: VersionControlAdminSettingsComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
auth: [Authority.TENANT_ADMIN],

4
ui-ngx/src/app/modules/home/pages/admin/admin.module.ts

@ -29,7 +29,7 @@ import { SendTestSmsDialogComponent } from '@home/pages/admin/send-test-sms-dial
import { HomeSettingsComponent } from '@home/pages/admin/home-settings.component';
import { ResourcesLibraryComponent } from '@home/pages/admin/resource/resources-library.component';
import { QueueComponent} from '@home/pages/admin/queue/queue.component';
import { VersionControlSettingsComponent } from '@home/pages/admin/version-control-settings.component';
import { VersionControlAdminSettingsComponent } from '@home/pages/admin/version-control-admin-settings.component';
@NgModule({
declarations:
@ -43,7 +43,7 @@ import { VersionControlSettingsComponent } from '@home/pages/admin/version-contr
HomeSettingsComponent,
ResourcesLibraryComponent,
QueueComponent,
VersionControlSettingsComponent
VersionControlAdminSettingsComponent
],
imports: [
CommonModule,

18
ui-ngx/src/app/modules/home/pages/admin/version-control-admin-settings.component.html

@ -0,0 +1,18 @@
<!--
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.
-->
<tb-version-control-settings #versionControlSettingsComponent></tb-version-control-settings>

44
ui-ngx/src/app/modules/home/pages/admin/version-control-admin-settings.component.ts

@ -0,0 +1,44 @@
///
/// 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, ViewChild } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormGroup } from '@angular/forms';
import { VersionControlSettingsComponent } from '@home/components/vc/version-control-settings.component';
@Component({
selector: 'tb-version-control-admin-settings',
templateUrl: './version-control-admin-settings.component.html',
styleUrls: []
})
export class VersionControlAdminSettingsComponent extends PageComponent implements OnInit, HasConfirmForm {
@ViewChild('versionControlSettingsComponent') versionControlSettingsComponent: VersionControlSettingsComponent;
constructor(protected store: Store<AppState>) {
super(store);
}
ngOnInit() {
}
confirmForm(): FormGroup {
return this.versionControlSettingsComponent?.versionControlSettingsForm;
}
}

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

@ -49,3 +49,8 @@
label="{{ 'audit-log.audit-logs' | translate }}" #auditLogsTab="matTab">
<tb-audit-log-table detailsMode="true" [active]="auditLogsTab.isActive" [auditLogMode]="auditLogModes.ENTITY" [entityId]="entity.id"></tb-audit-log-table>
</mat-tab>
<mat-tab *ngIf="entity && authUser.authority === authorities.TENANT_ADMIN"
label="{{ 'version-control.version-control' | translate }}" #versionControlTab="matTab">
<tb-version-control detailsMode="true" singleEntityMode="true"
[active]="versionControlTab.isActive" [externalEntityId]="entity.externalId || entity.id"></tb-version-control>
</mat-tab>

4
ui-ngx/src/app/modules/home/pages/home-pages.module.ts

@ -36,6 +36,7 @@ import { DeviceProfileModule } from './device-profile/device-profile.module';
import { ApiUsageModule } from '@home/pages/api-usage/api-usage.module';
import { EdgeModule } from '@home/pages/edge/edge.module';
import { OtaUpdateModule } from '@home/pages/ota-update/ota-update.module';
import { VcModule } from '@home/pages/vc/vc.module';
@NgModule({
exports: [
@ -56,7 +57,8 @@ import { OtaUpdateModule } from '@home/pages/ota-update/ota-update.module';
AuditLogModule,
ApiUsageModule,
OtaUpdateModule,
UserModule
UserModule,
VcModule
],
providers: [
{

44
ui-ngx/src/app/modules/home/pages/vc/vc-routing.module.ts

@ -0,0 +1,44 @@
///
/// 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 { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
import { Authority } from '@shared/models/authority.enum';
import { VersionControlComponent } from '@home/components/vc/version-control.component';
const routes: Routes = [
{
path: 'vc',
component: VersionControlComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
auth: [Authority.TENANT_ADMIN],
title: 'version-control.version-control',
breadcrumb: {
label: 'version-control.version-control',
icon: 'history'
}
}
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
providers: []
})
export class VcRoutingModule { }

31
ui-ngx/src/app/modules/home/pages/vc/vc.module.ts

@ -0,0 +1,31 @@
///
/// 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 { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import { VcRoutingModule } from '@home/pages/vc/vc-routing.module';
@NgModule({
declarations: [
],
imports: [
CommonModule,
SharedModule,
VcRoutingModule
]
})
export class VcModule { }

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

@ -15,7 +15,7 @@
limitations under the License.
-->
<mat-form-field [formGroup]="branchFormGroup" class="mat-block">
<mat-form-field [formGroup]="branchFormGroup" class="mat-block" [floatLabel]="selectionMode ? 'always' : 'auto'">
<mat-label>{{ 'version-control.branch' | translate }}</mat-label>
<input matInput type="text" placeholder="{{ 'version-control.select-branch' | translate }}"
#branchInput
@ -31,10 +31,11 @@
</button>
<mat-autocomplete
class="tb-autocomplete"
(closed)="onPanelClosed()"
#subTypeAutocomplete="matAutocomplete"
[displayWith]="displayBranchFn">
<mat-option *ngFor="let branch of filteredBranches | async" [value]="branch">
<span [innerHTML]="branch | highlight:searchText"></span>
<span [innerHTML]="branch.name | highlight:searchText"></span>
</mat-option>
</mat-autocomplete>
<mat-error *ngIf="branchFormGroup.get('branch').hasError('required')">

147
ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.ts

@ -14,7 +14,17 @@
/// limitations under the License.
///
import { AfterViewInit, Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core';
import {
AfterViewInit,
ChangeDetectorRef,
Component,
ElementRef,
forwardRef,
Input,
NgZone,
OnInit,
ViewChild
} from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Observable, of } from 'rxjs';
import {
@ -24,6 +34,7 @@ import {
map,
publishReplay,
refCount,
share,
switchMap,
tap
} from 'rxjs/operators';
@ -32,6 +43,7 @@ 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';
import { isNotEmptyStr } from '@core/utils';
@Component({
selector: 'tb-branch-autocomplete',
@ -60,27 +72,48 @@ export class BranchAutocompleteComponent implements ControlValueAccessor, OnInit
this.requiredValue = coerceBooleanProperty(value);
}
private disabledValue: boolean;
get disabled(): boolean {
return this.disabledValue;
}
@Input()
disabled: boolean;
set disabled(value: boolean) {
this.disabledValue = coerceBooleanProperty(value);
if (this.disabledValue) {
this.branchFormGroup.disable({emitEvent: false});
} else {
this.branchFormGroup.enable({emitEvent: false});
}
}
@Input()
selectDefaultBranch = true;
@Input()
selectionMode = false;
@ViewChild('branchInput', {static: true}) branchInput: ElementRef;
filteredBranches: Observable<Array<string>>;
filteredBranches: Observable<Array<BranchInfo>>;
branches: Observable<Array<BranchInfo>>;
branches: Observable<Array<BranchInfo>> = null;
defaultBranch: BranchInfo = null;
searchText = '';
private dirty = false;
private ignoreClosedPanel = false;
private propagateChange = (v: any) => { };
constructor(private store: Store<AppState>,
private entitiesVersionControlService: EntitiesVersionControlService,
private fb: FormBuilder) {
private fb: FormBuilder,
private zone: NgZone) {
this.branchFormGroup = this.fb.group({
branch: [null, []]
});
@ -94,17 +127,36 @@ export class BranchAutocompleteComponent implements ControlValueAccessor, OnInit
}
ngOnInit() {
this.branches = null;
this.filteredBranches = this.branchFormGroup.get('branch').valueChanges
.pipe(
tap((value: BranchInfo | string) => {
let modelValue: BranchInfo | null;
if (typeof value === 'string' || !value) {
if (!this.selectionMode && typeof value === 'string' && isNotEmptyStr(value)) {
modelValue = {name: value, default: false};
} else {
modelValue = null;
}
} else {
modelValue = value;
}
this.updateView(modelValue);
}),
map(value => {
if (value) {
if (typeof value === 'string') {
return value;
} else {
return value.name;
}
} else {
return '';
}
}),
debounceTime(150),
distinctUntilChanged(),
tap(value => {
this.updateView(value);
}),
map(value => value ? value : ''),
switchMap(branch => this.fetchBranches(branch))
switchMap(name => this.fetchBranches(name)),
share()
);
}
@ -113,24 +165,16 @@ export class BranchAutocompleteComponent implements ControlValueAccessor, OnInit
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(
selectDefaultBranchIfNeeded(ignoreLoading = true, force = false): void {
if ((this.selectDefaultBranch && !this.modelValue) || force) {
this.getBranches(ignoreLoading).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);
}
if (this.defaultBranch || force) {
this.branchFormGroup.get('branch').patchValue(this.defaultBranch, {emitEvent: false});
this.modelValue = this.defaultBranch?.name;
this.propagateChange(this.modelValue);
}
}
);
@ -141,9 +185,9 @@ export class BranchAutocompleteComponent implements ControlValueAccessor, OnInit
this.searchText = '';
this.modelValue = value;
if (value != null) {
this.branchFormGroup.get('branch').patchValue(value, {emitEvent: false});
this.branchFormGroup.get('branch').patchValue({name: value}, {emitEvent: false});
} else {
this.branchFormGroup.get('branch').patchValue('', {emitEvent: false});
this.branchFormGroup.get('branch').patchValue(null, {emitEvent: false});
this.selectDefaultBranchIfNeeded();
}
this.dirty = true;
@ -156,31 +200,53 @@ export class BranchAutocompleteComponent implements ControlValueAccessor, OnInit
}
}
updateView(value: string | null) {
if (this.modelValue !== value) {
this.modelValue = value;
onPanelClosed() {
if (this.ignoreClosedPanel) {
this.ignoreClosedPanel = false;
} else {
if (this.selectionMode && !this.branchFormGroup.get('branch').value && this.defaultBranch) {
this.zone.run(() => {
this.branchFormGroup.get('branch').patchValue(this.defaultBranch, {emitEvent: true});
}, 0);
}
}
}
updateView(value: BranchInfo | null) {
if (this.modelValue !== value?.name) {
this.modelValue = value?.name;
this.propagateChange(this.modelValue);
}
}
displayBranchFn(branch?: string): string | undefined {
return branch ? branch : undefined;
displayBranchFn(branch?: BranchInfo): string | undefined {
return branch ? branch.name : undefined;
}
fetchBranches(searchText?: string): Observable<Array<string>> {
fetchBranches(searchText?: string): Observable<Array<BranchInfo>> {
this.searchText = searchText;
return this.getBranches().pipe(
map(branches => branches.map(branch => branch.name).filter(branchName => {
return searchText ? branchName.toUpperCase().startsWith(searchText.toUpperCase()) : true;
}))
map(branches => {
let res = branches.filter(branch => {
return searchText ? branch.name.toUpperCase().startsWith(searchText.toUpperCase()) : true;
});
if (!this.selectionMode && isNotEmptyStr(searchText) && !res.find(b => b.name === searchText)) {
res = [{name: searchText, default: false}, ...res];
}
return res;
}
)
);
}
getBranches(): Observable<Array<BranchInfo>> {
getBranches(ignoreLoading = true): Observable<Array<BranchInfo>> {
if (!this.branches) {
const branchesObservable = this.entitiesVersionControlService.listBranches({ignoreLoading: true, ignoreErrors: true});
const branchesObservable = this.entitiesVersionControlService.listBranches({ignoreLoading, ignoreErrors: true});
this.branches = branchesObservable.pipe(
catchError(() => of([] as Array<BranchInfo>)),
tap((data) => {
this.defaultBranch = data.find(branch => branch.default);
}),
publishReplay(1),
refCount()
);
@ -189,6 +255,7 @@ export class BranchAutocompleteComponent implements ControlValueAccessor, OnInit
}
clear() {
this.ignoreClosedPanel = true;
this.branchFormGroup.get('branch').patchValue(null, {emitEvent: true});
setTimeout(() => {
this.branchInput.nativeElement.blur();

4
ui-ngx/src/app/shared/models/asset.models.ts

@ -14,13 +14,13 @@
/// limitations under the License.
///
import { BaseData } from '@shared/models/base-data';
import { BaseData, ExportableEntity } from '@shared/models/base-data';
import { AssetId } from './id/asset-id';
import { TenantId } from '@shared/models/id/tenant-id';
import { CustomerId } from '@shared/models/id/customer-id';
import { EntitySearchQuery } from '@shared/models/relation.models';
export interface Asset extends BaseData<AssetId> {
export interface Asset extends BaseData<AssetId>, ExportableEntity<AssetId> {
tenantId?: TenantId;
customerId?: CustomerId;
name: string;

6
ui-ngx/src/app/shared/models/base-data.ts

@ -27,6 +27,12 @@ export interface BaseData<T extends HasId> {
label?: string;
}
export interface ExportableEntity<T extends EntityId> {
createdTime?: number;
id?: T;
externalId?: T;
}
export function hasIdEquals(id1: HasId, id2: HasId): boolean {
if (isDefinedAndNotNull(id1) && isDefinedAndNotNull(id2)) {
return id1.id === id2.id;

3
ui-ngx/src/app/shared/models/customer.model.ts

@ -17,8 +17,9 @@
import { CustomerId } from '@shared/models/id/customer-id';
import { ContactBased } from '@shared/models/contact-based.model';
import { TenantId } from './id/tenant-id';
import { ExportableEntity } from '@shared/models/base-data';
export interface Customer extends ContactBased<CustomerId> {
export interface Customer extends ContactBased<CustomerId>, ExportableEntity<CustomerId> {
tenantId: TenantId;
title: string;
additionalInfo?: any;

4
ui-ngx/src/app/shared/models/dashboard.models.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { BaseData } from '@shared/models/base-data';
import { BaseData, ExportableEntity } from '@shared/models/base-data';
import { DashboardId } from '@shared/models/id/dashboard-id';
import { TenantId } from '@shared/models/id/tenant-id';
import { ShortCustomerInfo } from '@shared/models/customer.model';
@ -23,7 +23,7 @@ import { Timewindow } from '@shared/models/time/time.models';
import { EntityAliases } from './alias.models';
import { Filters } from '@shared/models/query/query.models';
export interface DashboardInfo extends BaseData<DashboardId> {
export interface DashboardInfo extends BaseData<DashboardId>, ExportableEntity<DashboardId> {
tenantId?: TenantId;
title?: string;
image?: string;

6
ui-ngx/src/app/shared/models/device.models.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { BaseData } from '@shared/models/base-data';
import { BaseData, ExportableEntity } from '@shared/models/base-data';
import { DeviceId } from './id/device-id';
import { TenantId } from '@shared/models/id/tenant-id';
import { CustomerId } from '@shared/models/id/customer-id';
@ -560,7 +560,7 @@ export interface DeviceProfileData {
provisionConfiguration?: DeviceProvisionConfiguration;
}
export interface DeviceProfile extends BaseData<DeviceProfileId> {
export interface DeviceProfile extends BaseData<DeviceProfileId>, ExportableEntity<DeviceProfileId> {
tenantId?: TenantId;
name: string;
description?: string;
@ -685,7 +685,7 @@ export interface DeviceData {
transportConfiguration: DeviceTransportConfiguration;
}
export interface Device extends BaseData<DeviceId> {
export interface Device extends BaseData<DeviceId>, ExportableEntity<DeviceId> {
tenantId?: TenantId;
customerId?: CustomerId;
name: string;

4
ui-ngx/src/app/shared/models/rule-chain.models.ts

@ -14,14 +14,14 @@
/// limitations under the License.
///
import { BaseData } from '@shared/models/base-data';
import { BaseData, ExportableEntity } from '@shared/models/base-data';
import { TenantId } from '@shared/models/id/tenant-id';
import { RuleChainId } from '@shared/models/id/rule-chain-id';
import { RuleNodeId } from '@shared/models/id/rule-node-id';
import { RuleNode, RuleNodeComponentDescriptor, RuleNodeType } from '@shared/models/rule-node.models';
import { ComponentType } from '@shared/models/component-descriptor.models';
export interface RuleChain extends BaseData<RuleChainId> {
export interface RuleChain extends BaseData<RuleChainId>, ExportableEntity<RuleChainId> {
tenantId: TenantId;
name: string;
firstRuleNodeId: RuleNodeId;

1
ui-ngx/src/app/shared/models/vc.models.ts

@ -43,6 +43,7 @@ export interface BranchInfo {
}
export interface EntityVersion {
timestamp: number;
id: string;
name: string;
}

10
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -3109,6 +3109,8 @@
"json-value-required": "JSON value is required."
},
"version-control": {
"version-control": "Version control",
"management": "Version control management",
"branch": "Branch",
"select-branch": "Select branch",
"branch-required": "Branch is required",
@ -3118,7 +3120,13 @@
"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"
"export-to-git": "Export to Git",
"entity-versions": "Entity versions",
"versions": "Versions",
"created-time": "Created time",
"version-id": "Version ID",
"no-entity-versions-text": "No entity versions found",
"no-versions-text": "No versions found"
},
"widget": {
"widget-library": "Widgets Library",

Loading…
Cancel
Save