Browse Source

UI: Implement entitiy versions table

pull/6615/head
Igor Kulikov 4 years ago
parent
commit
ca3c95afca
  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(); String defaultBranch = versionControlService.getVersionControlSettings(tenantId).getDefaultBranch();
if (StringUtils.isNotEmpty(defaultBranch)) { if (StringUtils.isNotEmpty(defaultBranch)) {
remoteBranches.remove(defaultBranch);
infos.add(new BranchInfo(defaultBranch, true)); 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; return infos;
}, MoreExecutors.directExecutor())); }, MoreExecutors.directExecutor()));
} catch (Exception e) { } 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) { if (vcSettings != null) {
builder.setVcSettings(ByteString.copyFrom(encodingService.encode(vcSettings))); builder.setVcSettings(ByteString.copyFrom(encodingService.encode(vcSettings)));
} else { } else if (request.requiresSettings()) {
throw new RuntimeException("No entity version control settings provisioned!"); throw new RuntimeException("No entity version control settings provisioned!");
} }
return builder; 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 { public PageData<Commit> listCommits(String branch, String path, PageLink pageLink) throws IOException, GitAPIException {
ObjectId branchId = resolve("origin/" + branch); ObjectId branchId = resolve("origin/" + branch);
if (branchId == null) { if (branchId == null) {
throw new IllegalArgumentException("Branch not found"); return new PageData<>();
} }
LogCommand command = git.log() LogCommand command = git.log()
.add(branchId) .add(branchId)
@ -313,6 +313,7 @@ public class GitRepository {
Function<? super T, ? extends R> mapper, Function<? super T, ? extends R> mapper,
PageLink pageLink, PageLink pageLink,
Function<PageLink, Comparator<T>> comparatorFunction) { Function<PageLink, Comparator<T>> comparatorFunction) {
iterable = Streams.stream(iterable).collect(Collectors.toList());
int totalElements = Iterables.size(iterable); int totalElements = Iterables.size(iterable);
int totalPages = pageLink.getPageSize() > 0 ? (int) Math.ceil((float) totalElements / pageLink.getPageSize()) : 1; int totalPages = pageLink.getPageSize() > 0 ? (int) Math.ceil((float) totalElements / pageLink.getPageSize()) : 1;
int startIndex = pageLink.getPageSize() * pageLink.getPage(); 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', UNAUTHENTICATED = '[Auth] Unauthenticated',
LOAD_USER = '[Auth] Load User', LOAD_USER = '[Auth] Load User',
UPDATE_USER_DETAILS = '[Auth] Update User Details', 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 { export class ActionAuthAuthenticated implements Action {
@ -54,5 +55,11 @@ export class ActionAuthUpdateLastPublicDashboardId implements Action {
constructor(readonly payload: { lastPublicDashboardId: string }) {} 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 | 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; userTokenAccessEnabled: boolean;
allowedDashboardIds: string[]; allowedDashboardIds: string[];
edgesSupportEnabled: boolean; edgesSupportEnabled: boolean;
hasVersionControl: boolean;
} }
export interface AuthPayload extends SysParamsState { export interface AuthPayload extends SysParamsState {

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

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

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

@ -55,6 +55,11 @@ export const selectUserTokenAccessEnabled = createSelector(
(state: AuthState) => state.userTokenAccessEnabled (state: AuthState) => state.userTokenAccessEnabled
); );
export const selectHasVersionControl = createSelector(
selectAuthState,
(state: AuthState) => state.hasVersionControl
);
export function getCurrentAuthState(store: Store<AppState>): AuthState { export function getCurrentAuthState(store: Store<AppState>): AuthState {
let state: AuthState; let state: AuthState;
store.pipe(select(selectAuth), take(1)).subscribe( 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()); 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> { private loadSystemParams(authPayload: AuthPayload): Observable<SysParamsState> {
const sources = [this.loadIsUserTokenAccessEnabled(authPayload.authUser), const sources = [this.loadIsUserTokenAccessEnabled(authPayload.authUser),
this.fetchAllowedDashboardIds(authPayload), this.fetchAllowedDashboardIds(authPayload),
this.loadIsEdgesSupportEnabled(), this.loadIsEdgesSupportEnabled(),
this.loadHasVersionControl(authPayload.authUser),
this.timeService.loadMaxDatapointsLimit()]; this.timeService.loadMaxDatapointsLimit()];
return forkJoin(sources) return forkJoin(sources)
.pipe(map((data) => { .pipe(map((data) => {
const userTokenAccessEnabled: boolean = data[0] as boolean; const userTokenAccessEnabled: boolean = data[0] as boolean;
const allowedDashboardIds: string[] = data[1] as string[]; const allowedDashboardIds: string[] = data[1] as string[];
const edgesSupportEnabled: boolean = data[2] as boolean; 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) => { }, catchError((err) => {
return of({}); 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 { HttpClient } from '@angular/common/http';
import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils';
import { Observable } from 'rxjs'; 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({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -37,4 +42,24 @@ export class EntitiesVersionControlService {
public saveEntitiesVersion(request: VersionCreateRequest, config?: RequestConfig): Observable<VersionCreationResult> { public saveEntitiesVersion(request: VersionCreateRequest, config?: RequestConfig): Observable<VersionCreationResult> {
return this.http.post<VersionCreationResult>('/api/entities/vc/version', request, defaultHttpOptionsFromConfig(config)); 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', path: '/dashboards',
icon: 'dashboards' icon: 'dashboards'
}, },
{
id: guid(),
name: 'version-control.version-control',
type: 'link',
path: '/vc',
icon: 'history'
},
{ {
id: guid(), id: guid(),
name: 'audit-log.audit-logs', 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', name: 'audit-log.audit',
places: [ 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 { 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'; 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({ @NgModule({
declarations: declarations:
@ -278,7 +281,10 @@ import { VcEntityExportDialogComponent } from '@home/components/vc/vc-entity-exp
DisplayWidgetTypesPanelComponent, DisplayWidgetTypesPanelComponent,
TenantProfileQueuesComponent, TenantProfileQueuesComponent,
QueueFormComponent, QueueFormComponent,
VcEntityExportDialogComponent VcEntityExportDialogComponent,
VersionControlSettingsComponent,
VersionControlComponent,
EntityVersionsTableComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -397,7 +403,10 @@ import { VcEntityExportDialogComponent } from '@home/components/vc/vc-entity-exp
DisplayWidgetTypesPanelComponent, DisplayWidgetTypesPanelComponent,
TenantProfileQueuesComponent, TenantProfileQueuesComponent,
QueueFormComponent, QueueFormComponent,
VcEntityExportDialogComponent VcEntityExportDialogComponent,
VersionControlSettingsComponent,
VersionControlComponent,
EntityVersionsTableComponent
], ],
providers: [ providers: [
WidgetComponentService, 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> <div>
<mat-card class="settings-card"> <mat-card class="vc-settings" [ngClass]="{'settings-card': !detailsMode}">
<mat-card-title> <mat-card-title>
<div fxLayout="row"> <div fxLayout="row">
<span class="mat-headline" translate>admin.git-repository-settings</span> <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. * limitations under the License.
*/ */
:host { :host {
mat-card.vc-settings {
margin: 8px;
}
.fields-group { .fields-group {
padding: 0 16px 8px; padding: 0 16px 8px;
margin-bottom: 10px; 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. /// 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 { PageComponent } from '@shared/components/page.component';
import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard';
import { FormBuilder, FormGroup, FormGroupDirective, Validators } from '@angular/forms'; 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 { AppState } from '@core/core.state';
import { AdminService } from '@core/http/admin.service'; import { AdminService } from '@core/http/admin.service';
import { import {
@ -30,13 +30,21 @@ import { ActionNotificationShow } from '@core/notification/notification.actions'
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { isNotEmptyStr } from '@core/utils'; import { isNotEmptyStr } from '@core/utils';
import { DialogService } from '@core/services/dialog.service'; 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({ @Component({
selector: 'tb-version-control-settings', selector: 'tb-version-control-settings',
templateUrl: './version-control-settings.component.html', 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; versionControlSettingsForm: FormGroup;
settings: EntitiesVersionControlSettings = null; settings: EntitiesVersionControlSettings = null;
@ -62,7 +70,7 @@ export class VersionControlSettingsComponent extends PageComponent implements On
ngOnInit() { ngOnInit() {
this.versionControlSettingsForm = this.fb.group({ this.versionControlSettingsForm = this.fb.group({
repositoryUri: [null, [Validators.required]], repositoryUri: [null, [Validators.required]],
defaultBranch: [null, []], defaultBranch: ['main', []],
authMethod: [VersionControlAuthMethod.USERNAME_PASSWORD, [Validators.required]], authMethod: [VersionControlAuthMethod.USERNAME_PASSWORD, [Validators.required]],
username: [null, []], username: [null, []],
password: [null, []], password: [null, []],
@ -77,16 +85,29 @@ export class VersionControlSettingsComponent extends PageComponent implements On
this.versionControlSettingsForm.get('privateKeyFileName').valueChanges.subscribe(() => { this.versionControlSettingsForm.get('privateKeyFileName').valueChanges.subscribe(() => {
this.updateValidators(false); 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) => { (settings) => {
this.settings = settings; this.settings = settings;
if (this.settings.authMethod === VersionControlAuthMethod.USERNAME_PASSWORD) { if (this.settings != null) {
this.showChangePassword = true; if (this.settings.authMethod === VersionControlAuthMethod.USERNAME_PASSWORD) {
} else { this.showChangePassword = true;
this.showChangePrivateKeyPassword = 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.versionControlSettingsForm.reset(this.settings);
this.updateValidators(false); 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.showChangePrivateKeyPassword = false;
this.changePrivateKeyPassword = false; this.changePrivateKeyPassword = false;
formDirective.resetForm(); formDirective.resetForm();
this.versionControlSettingsForm.reset({ authMethod: VersionControlAuthMethod.USERNAME_PASSWORD }); this.versionControlSettingsForm.reset({ defaultBranch: 'main', authMethod: VersionControlAuthMethod.USERNAME_PASSWORD });
this.updateValidators(false); this.updateValidators(false);
this.store.dispatch(new ActionAuthUpdateHasVersionControl({ hasVersionControl: false }));
} }
); );
} }
}); });
} }
confirmForm(): FormGroup {
return this.versionControlSettingsForm;
}
changePasswordChanged() { changePasswordChanged() {
if (this.changePassword) { if (this.changePassword) {
this.versionControlSettingsForm.get('password').patchValue(''); 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 { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models';
import { BreadCrumbConfig } from '@shared/components/breadcrumb'; import { BreadCrumbConfig } from '@shared/components/breadcrumb';
import { QueuesTableConfigResolver } from '@home/pages/admin/queue/queues-table-config.resolver'; 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() @Injectable()
export class OAuth2LoginProcessingUrlResolver implements Resolve<string> { export class OAuth2LoginProcessingUrlResolver implements Resolve<string> {
@ -226,7 +226,7 @@ const routes: Routes = [
}, },
{ {
path: 'vc', path: 'vc',
component: VersionControlSettingsComponent, component: VersionControlAdminSettingsComponent,
canDeactivate: [ConfirmOnExitGuard], canDeactivate: [ConfirmOnExitGuard],
data: { data: {
auth: [Authority.TENANT_ADMIN], 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 { HomeSettingsComponent } from '@home/pages/admin/home-settings.component';
import { ResourcesLibraryComponent } from '@home/pages/admin/resource/resources-library.component'; import { ResourcesLibraryComponent } from '@home/pages/admin/resource/resources-library.component';
import { QueueComponent} from '@home/pages/admin/queue/queue.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({ @NgModule({
declarations: declarations:
@ -43,7 +43,7 @@ import { VersionControlSettingsComponent } from '@home/pages/admin/version-contr
HomeSettingsComponent, HomeSettingsComponent,
ResourcesLibraryComponent, ResourcesLibraryComponent,
QueueComponent, QueueComponent,
VersionControlSettingsComponent VersionControlAdminSettingsComponent
], ],
imports: [ imports: [
CommonModule, 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"> 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> <tb-audit-log-table detailsMode="true" [active]="auditLogsTab.isActive" [auditLogMode]="auditLogModes.ENTITY" [entityId]="entity.id"></tb-audit-log-table>
</mat-tab> </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 { ApiUsageModule } from '@home/pages/api-usage/api-usage.module';
import { EdgeModule } from '@home/pages/edge/edge.module'; import { EdgeModule } from '@home/pages/edge/edge.module';
import { OtaUpdateModule } from '@home/pages/ota-update/ota-update.module'; import { OtaUpdateModule } from '@home/pages/ota-update/ota-update.module';
import { VcModule } from '@home/pages/vc/vc.module';
@NgModule({ @NgModule({
exports: [ exports: [
@ -56,7 +57,8 @@ import { OtaUpdateModule } from '@home/pages/ota-update/ota-update.module';
AuditLogModule, AuditLogModule,
ApiUsageModule, ApiUsageModule,
OtaUpdateModule, OtaUpdateModule,
UserModule UserModule,
VcModule
], ],
providers: [ 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. 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> <mat-label>{{ 'version-control.branch' | translate }}</mat-label>
<input matInput type="text" placeholder="{{ 'version-control.select-branch' | translate }}" <input matInput type="text" placeholder="{{ 'version-control.select-branch' | translate }}"
#branchInput #branchInput
@ -31,10 +31,11 @@
</button> </button>
<mat-autocomplete <mat-autocomplete
class="tb-autocomplete" class="tb-autocomplete"
(closed)="onPanelClosed()"
#subTypeAutocomplete="matAutocomplete" #subTypeAutocomplete="matAutocomplete"
[displayWith]="displayBranchFn"> [displayWith]="displayBranchFn">
<mat-option *ngFor="let branch of filteredBranches | async" [value]="branch"> <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-option>
</mat-autocomplete> </mat-autocomplete>
<mat-error *ngIf="branchFormGroup.get('branch').hasError('required')"> <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. /// 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 { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { import {
@ -24,6 +34,7 @@ import {
map, map,
publishReplay, publishReplay,
refCount, refCount,
share,
switchMap, switchMap,
tap tap
} from 'rxjs/operators'; } from 'rxjs/operators';
@ -32,6 +43,7 @@ import { AppState } from '@app/core/core.state';
import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { BranchInfo } from '@shared/models/vc.models'; import { BranchInfo } from '@shared/models/vc.models';
import { EntitiesVersionControlService } from '@core/http/entities-version-control.service'; import { EntitiesVersionControlService } from '@core/http/entities-version-control.service';
import { isNotEmptyStr } from '@core/utils';
@Component({ @Component({
selector: 'tb-branch-autocomplete', selector: 'tb-branch-autocomplete',
@ -60,27 +72,48 @@ export class BranchAutocompleteComponent implements ControlValueAccessor, OnInit
this.requiredValue = coerceBooleanProperty(value); this.requiredValue = coerceBooleanProperty(value);
} }
private disabledValue: boolean;
get disabled(): boolean {
return this.disabledValue;
}
@Input() @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() @Input()
selectDefaultBranch = true; selectDefaultBranch = true;
@Input()
selectionMode = false;
@ViewChild('branchInput', {static: true}) branchInput: ElementRef; @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 = ''; searchText = '';
private dirty = false; private dirty = false;
private ignoreClosedPanel = false;
private propagateChange = (v: any) => { }; private propagateChange = (v: any) => { };
constructor(private store: Store<AppState>, constructor(private store: Store<AppState>,
private entitiesVersionControlService: EntitiesVersionControlService, private entitiesVersionControlService: EntitiesVersionControlService,
private fb: FormBuilder) { private fb: FormBuilder,
private zone: NgZone) {
this.branchFormGroup = this.fb.group({ this.branchFormGroup = this.fb.group({
branch: [null, []] branch: [null, []]
}); });
@ -94,17 +127,36 @@ export class BranchAutocompleteComponent implements ControlValueAccessor, OnInit
} }
ngOnInit() { ngOnInit() {
this.branches = null;
this.filteredBranches = this.branchFormGroup.get('branch').valueChanges this.filteredBranches = this.branchFormGroup.get('branch').valueChanges
.pipe( .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), debounceTime(150),
distinctUntilChanged(), distinctUntilChanged(),
tap(value => { switchMap(name => this.fetchBranches(name)),
this.updateView(value); share()
}),
map(value => value ? value : ''),
switchMap(branch => this.fetchBranches(branch))
); );
} }
@ -113,24 +165,16 @@ export class BranchAutocompleteComponent implements ControlValueAccessor, OnInit
setDisabledState(isDisabled: boolean): void { setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled; this.disabled = isDisabled;
if (this.disabled) {
this.branchFormGroup.disable({emitEvent: false});
} else {
this.branchFormGroup.enable({emitEvent: false});
}
} }
selectDefaultBranchIfNeeded(): void { selectDefaultBranchIfNeeded(ignoreLoading = true, force = false): void {
if (this.selectDefaultBranch && !this.modelValue) { if ((this.selectDefaultBranch && !this.modelValue) || force) {
this.getBranches().subscribe( this.getBranches(ignoreLoading).subscribe(
(data) => { (data) => {
if (data && data.length) { if (this.defaultBranch || force) {
const defaultBranch = data.find(branch => branch.default); this.branchFormGroup.get('branch').patchValue(this.defaultBranch, {emitEvent: false});
if (defaultBranch) { this.modelValue = this.defaultBranch?.name;
this.modelValue = defaultBranch.name; this.propagateChange(this.modelValue);
this.branchFormGroup.get('branch').patchValue(this.modelValue, {emitEvent: false});
this.propagateChange(this.modelValue);
}
} }
} }
); );
@ -141,9 +185,9 @@ export class BranchAutocompleteComponent implements ControlValueAccessor, OnInit
this.searchText = ''; this.searchText = '';
this.modelValue = value; this.modelValue = value;
if (value != null) { if (value != null) {
this.branchFormGroup.get('branch').patchValue(value, {emitEvent: false}); this.branchFormGroup.get('branch').patchValue({name: value}, {emitEvent: false});
} else { } else {
this.branchFormGroup.get('branch').patchValue('', {emitEvent: false}); this.branchFormGroup.get('branch').patchValue(null, {emitEvent: false});
this.selectDefaultBranchIfNeeded(); this.selectDefaultBranchIfNeeded();
} }
this.dirty = true; this.dirty = true;
@ -156,31 +200,53 @@ export class BranchAutocompleteComponent implements ControlValueAccessor, OnInit
} }
} }
updateView(value: string | null) { onPanelClosed() {
if (this.modelValue !== value) { if (this.ignoreClosedPanel) {
this.modelValue = value; 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); this.propagateChange(this.modelValue);
} }
} }
displayBranchFn(branch?: string): string | undefined { displayBranchFn(branch?: BranchInfo): string | undefined {
return branch ? branch : undefined; return branch ? branch.name : undefined;
} }
fetchBranches(searchText?: string): Observable<Array<string>> { fetchBranches(searchText?: string): Observable<Array<BranchInfo>> {
this.searchText = searchText; this.searchText = searchText;
return this.getBranches().pipe( return this.getBranches().pipe(
map(branches => branches.map(branch => branch.name).filter(branchName => { map(branches => {
return searchText ? branchName.toUpperCase().startsWith(searchText.toUpperCase()) : true; 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) { if (!this.branches) {
const branchesObservable = this.entitiesVersionControlService.listBranches({ignoreLoading: true, ignoreErrors: true}); const branchesObservable = this.entitiesVersionControlService.listBranches({ignoreLoading, ignoreErrors: true});
this.branches = branchesObservable.pipe( this.branches = branchesObservable.pipe(
catchError(() => of([] as Array<BranchInfo>)), catchError(() => of([] as Array<BranchInfo>)),
tap((data) => {
this.defaultBranch = data.find(branch => branch.default);
}),
publishReplay(1), publishReplay(1),
refCount() refCount()
); );
@ -189,6 +255,7 @@ export class BranchAutocompleteComponent implements ControlValueAccessor, OnInit
} }
clear() { clear() {
this.ignoreClosedPanel = true;
this.branchFormGroup.get('branch').patchValue(null, {emitEvent: true}); this.branchFormGroup.get('branch').patchValue(null, {emitEvent: true});
setTimeout(() => { setTimeout(() => {
this.branchInput.nativeElement.blur(); this.branchInput.nativeElement.blur();

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

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

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

@ -27,6 +27,12 @@ export interface BaseData<T extends HasId> {
label?: string; label?: string;
} }
export interface ExportableEntity<T extends EntityId> {
createdTime?: number;
id?: T;
externalId?: T;
}
export function hasIdEquals(id1: HasId, id2: HasId): boolean { export function hasIdEquals(id1: HasId, id2: HasId): boolean {
if (isDefinedAndNotNull(id1) && isDefinedAndNotNull(id2)) { if (isDefinedAndNotNull(id1) && isDefinedAndNotNull(id2)) {
return id1.id === id2.id; 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 { CustomerId } from '@shared/models/id/customer-id';
import { ContactBased } from '@shared/models/contact-based.model'; import { ContactBased } from '@shared/models/contact-based.model';
import { TenantId } from './id/tenant-id'; 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; tenantId: TenantId;
title: string; title: string;
additionalInfo?: any; additionalInfo?: any;

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

@ -14,7 +14,7 @@
/// limitations under the License. /// 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 { DashboardId } from '@shared/models/id/dashboard-id';
import { TenantId } from '@shared/models/id/tenant-id'; import { TenantId } from '@shared/models/id/tenant-id';
import { ShortCustomerInfo } from '@shared/models/customer.model'; 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 { EntityAliases } from './alias.models';
import { Filters } from '@shared/models/query/query.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; tenantId?: TenantId;
title?: string; title?: string;
image?: string; image?: string;

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

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

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

@ -14,14 +14,14 @@
/// limitations under the License. /// 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 { TenantId } from '@shared/models/id/tenant-id';
import { RuleChainId } from '@shared/models/id/rule-chain-id'; import { RuleChainId } from '@shared/models/id/rule-chain-id';
import { RuleNodeId } from '@shared/models/id/rule-node-id'; import { RuleNodeId } from '@shared/models/id/rule-node-id';
import { RuleNode, RuleNodeComponentDescriptor, RuleNodeType } from '@shared/models/rule-node.models'; import { RuleNode, RuleNodeComponentDescriptor, RuleNodeType } from '@shared/models/rule-node.models';
import { ComponentType } from '@shared/models/component-descriptor.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; tenantId: TenantId;
name: string; name: string;
firstRuleNodeId: RuleNodeId; firstRuleNodeId: RuleNodeId;

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

@ -43,6 +43,7 @@ export interface BranchInfo {
} }
export interface EntityVersion { export interface EntityVersion {
timestamp: number;
id: string; id: string;
name: 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." "json-value-required": "JSON value is required."
}, },
"version-control": { "version-control": {
"version-control": "Version control",
"management": "Version control management",
"branch": "Branch", "branch": "Branch",
"select-branch": "Select branch", "select-branch": "Select branch",
"branch-required": "Branch is required", "branch-required": "Branch is required",
@ -3118,7 +3120,13 @@
"version-name-required": "Version name is required", "version-name-required": "Version name is required",
"export-entity-relations": "Export entity relations", "export-entity-relations": "Export entity relations",
"export-entity-version-result-message": "Entity exported with version '{{name}}' and commit id '{{commitId}}'.", "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": {
"widget-library": "Widgets Library", "widget-library": "Widgets Library",

Loading…
Cancel
Save