mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
feat(devtools): implement time travel player functionality (rangle/angular-devtools#46)
This commit is contained in:
parent
059ef511d0
commit
07cd82a10c
3 changed files with 160 additions and 49 deletions
|
|
@ -6,20 +6,35 @@
|
|||
}
|
||||
|
||||
.controls {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 340px
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: auto;
|
||||
width: 85%;
|
||||
}
|
||||
|
||||
/deep/ .vis-network {
|
||||
outline: none;
|
||||
.controls .control-buttons {
|
||||
width: 35%;
|
||||
}
|
||||
|
||||
.controls .control-buttons button {
|
||||
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
font-weight: 300;
|
||||
position: absolute;
|
||||
top: 25px;
|
||||
left: 340px;
|
||||
}
|
||||
|
||||
.controls .status {
|
||||
justify-content: space-between;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 45%;
|
||||
}
|
||||
|
||||
/deep/ .vis-network {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.side-panel {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,38 @@
|
|||
<div class="controls">
|
||||
Timeline:
|
||||
<mat-slider [step]="1" min="0" [max]="timeline.length - 1" value="0" (input)="update()"></mat-slider>
|
||||
<button mat-stroked-button (click)="move(-1)"><</button><button mat-stroked-button (click)="move(1)">></button>
|
||||
<div class="status">
|
||||
Timeline:
|
||||
<mat-slider [disabled]="isPlaying" [step]="1" min="0" [max]="timeline.length - 1" value="0" (input)="update()"></mat-slider>
|
||||
<h3 class="timestamp">{{ currentFrame.timestamp / 1000 | number }} seconds from the start</h3>
|
||||
</div>
|
||||
<div class="control-buttons">
|
||||
<button [disabled]="isPlaying" mat-stroked-button (click)="moveToBeginningOfTimeline()">
|
||||
<<
|
||||
</button>
|
||||
<button [disabled]="isPlaying" mat-stroked-button (click)="move(-1)">
|
||||
<
|
||||
</button>
|
||||
<button [disabled]="isPlaying" mat-stroked-button (click)="move(1)">
|
||||
>
|
||||
</button>
|
||||
<button [disabled]="isPlaying" mat-stroked-button (click)="moveToEndOfTimeline()">
|
||||
>>
|
||||
</button>
|
||||
<button mat-stroked-button (click)="isPlaying ? pause() : play()">
|
||||
<ng-container *ngIf="isPlaying; else paused">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" width="1em" height="1em"
|
||||
preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24">
|
||||
<path d="M8 7h3v10H8zm5 0h3v10h-3z" fill="#626262"/>
|
||||
</svg>
|
||||
</ng-container>
|
||||
<ng-template #paused>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" width="1em" height="1em"
|
||||
preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24">
|
||||
<path d="M7 6v12l10-6z" fill="#626262"/>
|
||||
</svg>
|
||||
</ng-template>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="timestamp">{{ currentFrame.timestamp / 1000 | number }} seconds from the start</h3>
|
||||
|
||||
<div
|
||||
class="network-canvas"
|
||||
[visNetwork]="visNetwork"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { Options, Edge, DataSet, Data, Node, VisNetworkService } from 'ngx-vis';
|
|||
import { ComponentRecord } from 'protocol';
|
||||
import { Timeline, TimelineNode, buildTimeline, TimelineNodeState, TimelineFrame } from './time-travel-builder';
|
||||
import { MatSlider } from '@angular/material/slider';
|
||||
import { interval, Observable, range, Subscription, zip } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'ng-time-travel',
|
||||
|
|
@ -54,6 +55,9 @@ export class TimeTravelComponent implements OnDestroy {
|
|||
|
||||
private _nodeIdToNodes: Map<string, TimelineNode>;
|
||||
|
||||
isPlaying = false;
|
||||
playLoopSubscription: Subscription;
|
||||
|
||||
constructor(private _visNetworkService: VisNetworkService) {}
|
||||
|
||||
@Input() set stream(stream: ComponentRecord[]) {
|
||||
|
|
@ -61,13 +65,13 @@ export class TimeTravelComponent implements OnDestroy {
|
|||
this._showFrame(this.timeline[0]);
|
||||
}
|
||||
|
||||
showSeparator() {
|
||||
return Object.keys(this.selectedEntry.instanceState.props).length > 0 && this.selectedEntry.duration;
|
||||
showSeparator(): boolean {
|
||||
return Object.keys(this.selectedEntry.instanceState.props).length > 0 && !!this.selectedEntry.duration;
|
||||
}
|
||||
|
||||
move(direction: number) {
|
||||
move(direction: number): void {
|
||||
const idx = this.slider.value + direction;
|
||||
if (idx >= this.timeline.length || idx < 0) {
|
||||
if (this._invalidSliderIndex(idx)) {
|
||||
return;
|
||||
}
|
||||
this.slider.value = idx;
|
||||
|
|
@ -75,13 +79,44 @@ export class TimeTravelComponent implements OnDestroy {
|
|||
this._visNetworkService.setData(this.visNetwork, this.visNetworkData);
|
||||
}
|
||||
|
||||
update() {
|
||||
createPlayLoopObservable(): Observable<[number, number]> {
|
||||
return zip(
|
||||
range(0, this.timeline.length - this.slider.value),
|
||||
interval(300),
|
||||
);
|
||||
}
|
||||
|
||||
play(): void {
|
||||
this.isPlaying = true;
|
||||
this._clearPlayLoopSubscription();
|
||||
this._subscribeToPlayLoop();
|
||||
}
|
||||
|
||||
pause(): void {
|
||||
this.isPlaying = false;
|
||||
this._clearPlayLoopSubscription();
|
||||
}
|
||||
|
||||
moveToBeginningOfTimeline(): void {
|
||||
this.slider.value = 0;
|
||||
this._showFrame(this.timeline[0]);
|
||||
this._visNetworkService.setData(this.visNetwork, this.visNetworkData);
|
||||
}
|
||||
|
||||
moveToEndOfTimeline(): void {
|
||||
const lastIndex = this.timeline.length - 1;
|
||||
this.slider.value = lastIndex;
|
||||
this._showFrame(this.timeline[lastIndex]);
|
||||
this._visNetworkService.setData(this.visNetwork, this.visNetworkData);
|
||||
}
|
||||
|
||||
update(): void {
|
||||
const idx = this.slider.value;
|
||||
this._showFrame(this.timeline[idx]);
|
||||
this._visNetworkService.setData(this.visNetwork, this.visNetworkData);
|
||||
}
|
||||
|
||||
onInitialize() {
|
||||
onInitialize(): void {
|
||||
this._visNetworkService.on(this.visNetwork, 'click');
|
||||
this._visNetworkService.click.subscribe((eventData: any[]) => {
|
||||
if (eventData[0] === this.visNetwork) {
|
||||
|
|
@ -90,53 +125,87 @@ export class TimeTravelComponent implements OnDestroy {
|
|||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
ngOnDestroy(): void {
|
||||
this._clearPlayLoopSubscription();
|
||||
this._visNetworkService.off(this.visNetwork, 'click');
|
||||
}
|
||||
|
||||
private _showFrame(frame: TimelineFrame | undefined) {
|
||||
private _showFrame(frame: TimelineFrame | undefined): void {
|
||||
if (!frame) {
|
||||
return;
|
||||
}
|
||||
this._prepareNodesAndEdgesForNewFrame(frame);
|
||||
this._initNodesAndEdges(frame.roots);
|
||||
this._updateVisNetworkDataState();
|
||||
}
|
||||
|
||||
private _prepareNodesAndEdgesForNewFrame(frame: TimelineFrame): void {
|
||||
this.currentFrame = frame;
|
||||
this._resetNodesAndEdges();
|
||||
this.selectedEntry = null;
|
||||
}
|
||||
|
||||
private _resetNodesAndEdges(): void {
|
||||
this.nodes = new DataSet<Node>([]);
|
||||
this.edges = new DataSet<Edge>([]);
|
||||
this._nodeIdToNodes = new Map<string, TimelineNode>();
|
||||
}
|
||||
|
||||
const initNodes = (roots: TimelineNode[], parentId?: string) => {
|
||||
roots.forEach(node => {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
private _initNodesAndEdges(roots: TimelineNode[], parentId?: string): void {
|
||||
roots.forEach(node => {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
const id = uuid();
|
||||
const id = uuid();
|
||||
|
||||
this._nodeIdToNodes.set(id, node);
|
||||
this._nodeIdToNodes.set(id, node);
|
||||
|
||||
this.nodes.add({
|
||||
id,
|
||||
color: node.state === TimelineNodeState.Check ? '#62D7C5' : '#5727E5',
|
||||
label: node.name,
|
||||
font: {
|
||||
color: node.state === TimelineNodeState.Check ? '#000000' : '#ffffff',
|
||||
},
|
||||
});
|
||||
|
||||
if (parentId) {
|
||||
this.edges.add({
|
||||
from: parentId,
|
||||
to: id
|
||||
});
|
||||
}
|
||||
|
||||
initNodes(node.children, id);
|
||||
this.nodes.add({
|
||||
id,
|
||||
color: node.state === TimelineNodeState.Check ? '#62D7C5' : '#5727E5',
|
||||
label: node.name,
|
||||
font: {
|
||||
color: node.state === TimelineNodeState.Check ? '#000000' : '#ffffff',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
initNodes(frame.roots);
|
||||
this.selectedEntry = null;
|
||||
if (parentId) {
|
||||
this.edges.add({
|
||||
from: parentId,
|
||||
to: id
|
||||
});
|
||||
}
|
||||
|
||||
this._initNodesAndEdges(node.children, id);
|
||||
});
|
||||
}
|
||||
|
||||
private _updateVisNetworkDataState(): void {
|
||||
this.visNetworkData = { nodes: this.nodes, edges: this.edges };
|
||||
}
|
||||
|
||||
private _invalidSliderIndex(index: number): boolean {
|
||||
return index >= this.timeline.length || index < 0;
|
||||
}
|
||||
|
||||
private _clearPlayLoopSubscription(): void {
|
||||
if (this.playLoopSubscription) {
|
||||
this.playLoopSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
private _subscribeToPlayLoop(): void {
|
||||
this.playLoopSubscription = this.createPlayLoopObservable().subscribe(snapshot => {
|
||||
if (this._reachedEndOfTimeLine()) {
|
||||
this.pause();
|
||||
} else {
|
||||
this.move(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _reachedEndOfTimeLine(): boolean {
|
||||
return (this.timeline.length - 1) - this.slider.value === 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue