Browse Source

UI: Implemented entity version diff component.

pull/6615/head
Igor Kulikov 4 years ago
parent
commit
0be0df6015
  1. 10
      ui-ngx/src/app/core/http/entities-version-control.service.ts
  2. 50
      ui-ngx/src/app/modules/home/components/vc/entity-version-diff.component.html
  3. 46
      ui-ngx/src/app/modules/home/components/vc/entity-version-diff.component.scss
  4. 283
      ui-ngx/src/app/modules/home/components/vc/entity-version-diff.component.ts
  5. 10
      ui-ngx/src/app/modules/home/components/vc/entity-versions-table.component.ts
  6. 41
      ui-ngx/src/app/shared/models/vc.models.ts
  7. 6
      ui-ngx/src/assets/locale/locale.constant-en_US.json

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

@ -19,7 +19,7 @@ import { HttpClient } from '@angular/common/http';
import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils';
import { combineLatest, Observable, of } from 'rxjs';
import {
BranchInfo,
BranchInfo, EntityDataDiff,
EntityVersion,
VersionCreateRequest,
VersionCreationResult,
@ -105,4 +105,12 @@ export class EntitiesVersionControlService {
public loadEntitiesVersion(request: VersionLoadRequest, config?: RequestConfig): Observable<Array<VersionLoadResult>> {
return this.http.post<Array<VersionLoadResult>>('/api/entities/vc/entity', request, defaultHttpOptionsFromConfig(config));
}
public compareEntityDataToVersion(branch: string,
entityId: EntityId,
versionId: string,
config?: RequestConfig): Observable<EntityDataDiff> {
return this.http.get<EntityDataDiff>(`/api/entities/vc/diff/${branch}/${entityId.entityType}/${entityId.id}?versionId=${versionId}`,
defaultHttpOptionsFromConfig(config));
}
}

50
ui-ngx/src/app/modules/home/components/vc/entity-version-diff.component.html

@ -15,16 +15,52 @@
limitations under the License.
-->
<section style="min-width: 400px; width: 800px; position: relative;">
<mat-toolbar>
<section class="entity-version-diff-view" [ngClass]="{'content-ready': contentReady}"
tb-fullscreen [fullscreen]="isFullscreen" (fullscreenChanged)="onFullscreenChanged($event)"
[ngStyle]="isFullscreen || !contentReady ? {} : {height: preferredDiffHeight}"
fxLayout="column">
<mat-toolbar *ngIf="contentReady">
<h2>{{ 'version-control.diff-entity-with-version' | translate: {versionName} }}</h2>
<span fxFlex></span>
<button mat-icon-button
matTooltip="{{'version-control.previous-difference' | translate}}"
matTooltipPosition="below"
[disabled]="!hasPrevious"
(click)="prevDifference($event)">
<mat-icon>arrow_upward</mat-icon>
</button>
<button mat-icon-button
matTooltip="{{'version-control.next-difference' | translate}}"
matTooltipPosition="below"
[disabled]="!hasNext"
(click)="nextDifference($event)">
<mat-icon>arrow_downward</mat-icon>
</button>
<mat-divider vertical></mat-divider>
<span class="diff-count">{{ 'version-control.differences' | translate : {count: diffCount} }}</span>
<mat-divider vertical></mat-divider>
<button mat-stroked-button color="primary"
#restoreVersionButton
[disabled]="(isLoading$ | async)"
(click)="toggleRestoreEntityVersion($event, restoreVersionButton)">
<mat-icon>restore</mat-icon>
{{'version-control.restore-version' | translate }}
</button>
<button mat-icon-button
matTooltip="{{(isFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
matTooltipPosition="below"
(click)="isFullscreen = !isFullscreen">
<mat-icon>{{ isFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" style="z-index: 10; width: 100%; margin-bottom: -4px;" mode="indeterminate"
*ngIf="isLoading$ | async">
</mat-progress-bar>
<div class="diff-viewer" #diffViewer style="height: 350px;"></div>
<div fxLayoutAlign="end center" fxLayoutGap="8px">
<div fxLayout="row" *ngIf="contentReady">
<div class="version-title" fxFlex>{{ 'version-control.current' | translate }}</div>
<div style="width: 58px;"></div>
<div class="version-title" fxFlex>{{ versionIdContent() }}</div>
</div>
<mat-spinner *ngIf="!contentReady" mode="indeterminate" diameter="80"></mat-spinner>
<div fxFlex [fxShow]="contentReady" class="diff-viewer" #diffViewer></div>
<div class="bottom-panel" *ngIf="contentReady" fxLayoutAlign="end center" fxLayoutGap="8px">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"

46
ui-ngx/src/app/modules/home/components/vc/entity-version-diff.component.scss

@ -13,11 +13,53 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
@import '../../../../../scss/constants';
.entity-version-diff-view {
position: relative;
&:not(.tb-fullscreen) {
&.content-ready {
width: 600px;
@media #{$mat-gt-sm} {
width: 800px;
}
@media #{$mat-gt-md} {
width: 1000px
}
@media #{$mat-gt-xmd} {
width: 1200px
}
}
}
&.tb-fullscreen {
height: 100%;
background: #fff;
.bottom-panel {
margin-bottom: 16px;
}
}
.mat-toolbar {
background: #fff;
.mat-divider-vertical {
height: 40px;
margin-right: 16px;
margin-left: 16px;
}
.diff-count {
font-size: 16px;
}
}
.version-title {
margin-left: 4px;
}
.diff-viewer {
position: relative;
border: 1px solid #c0c0c0;
margin-top: 16px;
margin-top: 4px;
margin-bottom: 16px;
.acediff__diffLine {
z-index: 1;
}
}
}

283
ui-ngx/src/app/modules/home/components/vc/entity-version-diff.component.ts

@ -14,20 +14,45 @@
/// limitations under the License.
///
import { Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
import {
ChangeDetectorRef,
Component,
ElementRef,
EventEmitter,
Input,
OnDestroy,
OnInit,
Output,
Renderer2,
ViewChild,
ViewContainerRef,
ViewEncapsulation
} from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { FormBuilder } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { EntitiesVersionControlService } from '@core/http/entities-version-control.service';
import { EntityId } from '@shared/models/id/entity-id';
import { TranslateService } from '@ngx-translate/core';
import { getAceDiff } from '@shared/models/ace/ace.models';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { entityExportDataToJsonString, VersionLoadResult } from '@shared/models/vc.models';
import { Ace } from 'ace-builds';
import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service';
import { EntityVersionRestoreComponent } from '@home/components/vc/entity-version-restore.component';
interface DiffInfo {
leftStartLine: number;
leftEndLine: number;
rightStartLine: number;
rightEndLine: number;
}
@Component({
selector: 'tb-entity-version-diff',
templateUrl: './entity-version-diff.component.html',
styleUrls: ['./entity-version-diff.component.scss']
styleUrls: ['./entity-version-diff.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class EntityVersionDiffComponent extends PageComponent implements OnInit, OnDestroy {
@ -43,50 +68,260 @@ export class EntityVersionDiffComponent extends PageComponent implements OnInit,
@Input()
versionId: string;
@Input()
entityId: EntityId;
@Input()
externalEntityId: EntityId;
@Output()
versionRestored = new EventEmitter<void>();
@Input()
onClose: () => void;
@Input()
popoverComponent: TbPopoverComponent;
differ: AceDiff;
contentReady = false;
preferredDiffHeight = '332px';
isFullscreen = false;
hasNext = false;
hasPrevious = false;
diffCount = 0;
constructor(protected store: Store<AppState>,
private entitiesVersionControlService: EntitiesVersionControlService,
private translate: TranslateService,
private fb: FormBuilder) {
private cd: ChangeDetectorRef,
private renderer: Renderer2,
private elementRef: ElementRef,
private viewContainerRef: ViewContainerRef,
private popoverService: TbPopoverService) {
super(store);
}
ngOnInit(): void {
getAceDiff().subscribe((aceDiff) => {
this.differ = new aceDiff.default(
{
element: this.diffViewerElmRef.nativeElement,
left: {
copyLinkEnabled: false,
editable: false,
content: 'Left content!'
},
right: {
copyLinkEnabled: false,
editable: false,
content: 'Right content!'
}
} as AceDiff.AceDiffConstructorOpts
);
this.entitiesVersionControlService
.compareEntityDataToVersion(this.branch, this.entityId, this.versionId).subscribe((diffData) => {
const leftContent = entityExportDataToJsonString(diffData.currentVersion);
const rightContent = entityExportDataToJsonString(diffData.otherVersion);
const leftLines = leftContent.split('\n').length;
const rightLines = leftContent.split('\n').length;
const totalLines = Math.max(leftLines, rightLines);
let preferredLines = Math.max(10, totalLines);
preferredLines = Math.min(40, preferredLines);
this.preferredDiffHeight = (132 + preferredLines * 16) + 'px';
getAceDiff().subscribe((aceDiff) => {
this.contentReady = true;
this.cd.detectChanges();
if (this.popoverComponent) {
this.popoverComponent.updatePosition();
}
setTimeout(() => {
this.differ = new aceDiff.default(
{
element: this.diffViewerElmRef.nativeElement,
mode: 'ace/mode/json',
left: {
copyLinkEnabled: false,
editable: false,
content: leftContent
},
right: {
copyLinkEnabled: false,
editable: false,
content: rightContent
}
} as AceDiff.AceDiffConstructorOpts
);
const leftEditor: Ace.Editor = this.differ.getEditors().left;
const rightEditor: Ace.Editor = this.differ.getEditors().right;
leftEditor.setShowFoldWidgets(false);
rightEditor.setShowFoldWidgets(false);
$('.acediff__left .ace_scrollbar-v', this.elementRef.nativeElement).on('scroll', () => {
rightEditor.getSession().setScrollTop(leftEditor.getSession().getScrollTop());
});
$('.acediff__right .ace_scrollbar-v', this.elementRef.nativeElement).on('scroll', () => {
leftEditor.getSession().setScrollTop(rightEditor.getSession().getScrollTop());
});
$('.acediff__left .ace_scrollbar-h', this.elementRef.nativeElement).on('scroll', () => {
rightEditor.getSession().setScrollLeft(leftEditor.getSession().getScrollLeft());
});
$('.acediff__right .ace_scrollbar-h', this.elementRef.nativeElement).on('scroll', () => {
leftEditor.getSession().setScrollLeft(rightEditor.getSession().getScrollLeft());
});
leftEditor.getSession().getSelection().on('changeCursor', () => this.updateHasNextAndPrevious());
rightEditor.getSession().getSelection().on('changeCursor', () => this.updateHasNextAndPrevious());
setTimeout(() => {
this.diffCount = this.differ.getNumDiffs();
this.updateHasNextAndPrevious();
});
});
});
});
}
versionIdContent(): string {
let versionId = this.versionId;
if (versionId.length > 7) {
versionId = versionId.slice(0, 7);
}
return versionId + ' (' + this.versionName + ')';
}
prevDifference($event: Event) {
if ($event) {
$event.stopPropagation();
}
this.moveToDiff(false);
}
nextDifference($event: Event) {
if ($event) {
$event.stopPropagation();
}
this.moveToDiff(true);
}
private moveToDiff(next: boolean) {
const currentRow = this.getCurrentRow();
const diff = next ? this.findNextLine(currentRow) : this.findPrevLine(currentRow);
if (diff) {
const leftEditor: Ace.Editor = this.differ.getEditors().left;
const rightEditor: Ace.Editor = this.differ.getEditors().right;
leftEditor.scrollToLine(diff.leftStartLine + 1, true, true, () => {});
leftEditor.gotoLine(diff.leftStartLine + 1, 0, true);
rightEditor.scrollToLine(diff.rightStartLine + 1, true, true, () => {});
rightEditor.gotoLine(diff.rightStartLine + 1, 0, true);
}
}
onFullscreenChanged(fullscreen: boolean) {
if (fullscreen) {
this.resizeEditors();
} else {
setTimeout(() => {
this.resizeEditors();
});
}
}
private getDiffs(): DiffInfo[] {
if (this.differ) {
// @ts-ignore
return this.differ.diffs as DiffInfo[] || [];
} else {
return [];
}
}
private getCurrentRow(): {row: number, left: boolean} {
const leftEditor: Ace.Editor = this.differ.getEditors().left;
const rightEditor: Ace.Editor = this.differ.getEditors().right;
let currentRow = 0;
let left = true;
const leftRow = leftEditor.getSession().getSelection().getCursor().row;
const rightRow = rightEditor.getSession().getSelection().getCursor().row;
if (leftRow >= leftEditor.getFirstVisibleRow() && leftRow <= leftEditor.getLastVisibleRow()) {
currentRow = leftRow;
} else if (rightRow >= rightEditor.getFirstVisibleRow() && rightRow <= rightEditor.getLastVisibleRow()) {
currentRow = rightRow;
left = false;
} else {
currentRow = leftRow;
}
return {row: currentRow, left};
}
private nextDiff(currentLine: {row: number, left: boolean}): DiffInfo | undefined {
const diffs = this.getDiffs();
return diffs.find((diff) => (currentLine.left ? diff.leftStartLine : diff.rightStartLine) > currentLine.row);
}
private prevDiff(currentLine: {row: number, left: boolean}): DiffInfo | undefined {
const diffs = this.getDiffs();
return [...diffs].reverse().find((diff) => (currentLine.left ? diff.leftEndLine : diff.rightEndLine) < currentLine.row);
}
private findNextLine(currentLine: {row: number, left: boolean}): DiffInfo | undefined {
let res = this.nextDiff(currentLine);
const diffs = this.getDiffs();
if (!res && diffs.length) {
res = diffs[diffs.length - 1];
}
return res;
}
private findPrevLine(currentLine: {row: number, left: boolean}): DiffInfo | undefined {
let res = this.prevDiff(currentLine);
const diffs = this.getDiffs();
if (!res && diffs.length) {
res = diffs[0];
}
return res;
}
private updateHasNextAndPrevious() {
const currentRow = this.getCurrentRow();
this.hasNext = !!this.nextDiff(currentRow);
this.hasPrevious = !!this.prevDiff(currentRow);
this.cd.markForCheck();
}
private resizeEditors() {
if (this.differ) {
this.differ.diff();
const leftEditor: Ace.Editor = this.differ.getEditors().left;
const rightEditor: Ace.Editor = this.differ.getEditors().right;
leftEditor.resize();
leftEditor.renderer.updateFull();
rightEditor.resize();
rightEditor.renderer.updateFull();
}
}
ngOnDestroy(): void {
if (this.differ) {
this.differ.destroy();
this.differ = null;
}
}
close(): void {
if (this.onClose) {
this.onClose();
if (this.popoverComponent) {
this.popoverComponent.hide();
}
}
toggleRestoreEntityVersion($event: Event, restoreVersionButton: MatButton) {
if ($event) {
$event.stopPropagation();
}
const trigger = restoreVersionButton._elementRef.nativeElement;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
const restoreVersionPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, EntityVersionRestoreComponent, 'left', true, null,
{
branch: this.branch,
versionName: this.versionName,
versionId: this.versionId,
externalEntityId: this.externalEntityId,
onClose: (result: Array<VersionLoadResult> | null) => {
restoreVersionPopover.hide();
if (result && result.length) {
this.close();
this.versionRestored.emit();
}
}
}, {}, {}, {}, false);
}
}
}

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

@ -224,11 +224,13 @@ export class EntityVersionsTableComponent extends PageComponent implements OnIni
branch: this.branch,
versionName: entityVersion.name,
versionId: entityVersion.id,
externalEntityId: this.externalEntityIdValue,
onClose: () => {
diffVersionPopover.hide();
}
entityId: this.entityId,
externalEntityId: this.externalEntityIdValue
}, {}, {}, {}, false);
diffVersionPopover.tbComponentRef.instance.popoverComponent = diffVersionPopover;
diffVersionPopover.tbComponentRef.instance.versionRestored.subscribe(() => {
this.versionRestored.emit();
});
}
}

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

@ -16,6 +16,10 @@
import { EntityId } from '@shared/models/id/entity-id';
import { EntityType } from '@shared/models/entity-type.models';
import { ExportableEntity } from '@shared/models/base-data';
import { EntityRelation } from '@shared/models/relation.models';
import { Device, DeviceCredentials } from '@shared/models/device.models';
import { RuleChain, RuleChainMetaData } from '@shared/models/rule-chain.models';
export interface VersionCreateConfig {
saveRelations: boolean;
@ -83,3 +87,40 @@ export interface VersionLoadResult {
updated: number;
deleted: number;
}
export interface EntityExportData<E extends ExportableEntity<EntityId>> {
entity: E;
entityType: EntityType;
relations: Array<EntityRelation>;
}
export interface DeviceExportData extends EntityExportData<Device> {
credentials: DeviceCredentials;
}
export interface RuleChainExportData extends EntityExportData<RuleChain> {
metaData: RuleChainMetaData;
}
export interface EntityDataDiff {
currentVersion: EntityExportData<any>;
otherVersion: EntityExportData<any>;
rawDiff: string;
}
export function entityExportDataToJsonString(data: EntityExportData<any>): string {
if (!data.relations) {
data.relations = [];
}
const allKeys = new Set<string>();
JSON.stringify(data, (key, value) => (allKeys.add(key), value));
return JSON.stringify(data, Array.from(allKeys).sort((key1, key2) => {
if (key1 === 'relations') {
return 1;
} else if (key2 === 'relations') {
return -1;
} else {
return 0;
}
}), 4);
}

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

@ -3133,7 +3133,11 @@
"restore-entity-from-version": "Restore entity from version '{{versionName}}'",
"load-entity-relations": "Load entity relations",
"show-version-diff": "Show version diff",
"diff-entity-with-version": "Diff with entity version '{{versionName}}'"
"diff-entity-with-version": "Diff with entity version '{{versionName}}'",
"previous-difference": "Previous Difference",
"next-difference": "Next Difference",
"current": "Current",
"differences": "{ count, plural, 1 {1 difference} other {# differences} }"
},
"widget": {
"widget-library": "Widgets Library",

Loading…
Cancel
Save