UI: Implement entitiy versions table
This commit is contained in:
		
							parent
							
								
									a5ff23a0a4
								
							
						
					
					
						commit
						ca3c95afca
					
				@ -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) {
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
 | 
			
		||||
@ -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();
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
 | 
			
		||||
@ -20,6 +20,7 @@ export interface SysParamsState {
 | 
			
		||||
  userTokenAccessEnabled: boolean;
 | 
			
		||||
  allowedDashboardIds: string[];
 | 
			
		||||
  edgesSupportEnabled: boolean;
 | 
			
		||||
  hasVersionControl: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface AuthPayload extends SysParamsState {
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -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(
 | 
			
		||||
 | 
			
		||||
@ -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({});
 | 
			
		||||
      })));
 | 
			
		||||
 | 
			
		||||
@ -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));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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: [
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
@ -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;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -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)
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -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>
 | 
			
		||||
@ -14,6 +14,9 @@
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
:host {
 | 
			
		||||
  mat-card.vc-settings {
 | 
			
		||||
    margin: 8px;
 | 
			
		||||
  }
 | 
			
		||||
  .fields-group {
 | 
			
		||||
    padding: 0 16px 8px;
 | 
			
		||||
    margin-bottom: 10px;
 | 
			
		||||
@ -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('');
 | 
			
		||||
@ -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>
 | 
			
		||||
@ -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 {
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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],
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
@ -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;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -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>
 | 
			
		||||
 | 
			
		||||
@ -36,6 +36,7 @@ import { DeviceProfileModule } from './device-profile/device-profile.module';
 | 
			
		||||
import { ApiUsageModule } from '@home/pages/api-usage/api-usage.module';
 | 
			
		||||
import { EdgeModule } from '@home/pages/edge/edge.module';
 | 
			
		||||
import { OtaUpdateModule } from '@home/pages/ota-update/ota-update.module';
 | 
			
		||||
import { VcModule } from '@home/pages/vc/vc.module';
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
  exports: [
 | 
			
		||||
@ -56,7 +57,8 @@ import { OtaUpdateModule } from '@home/pages/ota-update/ota-update.module';
 | 
			
		||||
    AuditLogModule,
 | 
			
		||||
    ApiUsageModule,
 | 
			
		||||
    OtaUpdateModule,
 | 
			
		||||
    UserModule
 | 
			
		||||
    UserModule,
 | 
			
		||||
    VcModule
 | 
			
		||||
  ],
 | 
			
		||||
  providers: [
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										44
									
								
								ui-ngx/src/app/modules/home/pages/vc/vc-routing.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								ui-ngx/src/app/modules/home/pages/vc/vc-routing.module.ts
									
									
									
									
									
										Normal 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 { }
 | 
			
		||||
							
								
								
									
										31
									
								
								ui-ngx/src/app/modules/home/pages/vc/vc.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								ui-ngx/src/app/modules/home/pages/vc/vc.module.ts
									
									
									
									
									
										Normal 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 { }
 | 
			
		||||
@ -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')">
 | 
			
		||||
 | 
			
		||||
@ -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();
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
 | 
			
		||||
@ -43,6 +43,7 @@ export interface BranchInfo {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface EntityVersion {
 | 
			
		||||
  timestamp: number;
 | 
			
		||||
  id: string;
 | 
			
		||||
  name: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -3109,6 +3109,8 @@
 | 
			
		||||
        "json-value-required": "JSON value is required."
 | 
			
		||||
    },
 | 
			
		||||
    "version-control": {
 | 
			
		||||
        "version-control": "Version control",
 | 
			
		||||
        "management": "Version control management",
 | 
			
		||||
        "branch": "Branch",
 | 
			
		||||
        "select-branch": "Select branch",
 | 
			
		||||
        "branch-required": "Branch is required",
 | 
			
		||||
@ -3118,7 +3120,13 @@
 | 
			
		||||
        "version-name-required": "Version name is required",
 | 
			
		||||
        "export-entity-relations": "Export entity relations",
 | 
			
		||||
        "export-entity-version-result-message": "Entity exported with version '{{name}}' and commit id '{{commitId}}'.",
 | 
			
		||||
        "export-to-git": "Export to Git"
 | 
			
		||||
        "export-to-git": "Export to Git",
 | 
			
		||||
        "entity-versions": "Entity versions",
 | 
			
		||||
        "versions": "Versions",
 | 
			
		||||
        "created-time": "Created time",
 | 
			
		||||
        "version-id": "Version ID",
 | 
			
		||||
        "no-entity-versions-text": "No entity versions found",
 | 
			
		||||
        "no-versions-text": "No versions found"
 | 
			
		||||
    },
 | 
			
		||||
    "widget": {
 | 
			
		||||
        "widget-library": "Widgets Library",
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user