UI: Implement entitiy versions table

This commit is contained in:
Igor Kulikov 2022-05-25 12:26:23 +03:00
parent a5ff23a0a4
commit ca3c95afca
38 changed files with 948 additions and 87 deletions

View File

@ -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) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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: [

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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: [
{

View File

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

View File

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

View File

@ -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')">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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