mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
refactor(devtools): migrate devtools to prettier formatting (#53945)
Migrate formatting to prettier for devtools from clang-format PR Close #53945
This commit is contained in:
parent
4be253483d
commit
711cb41626
161 changed files with 10689 additions and 9615 deletions
|
|
@ -7,6 +7,7 @@ export const format: FormatConfig = {
|
|||
'prettier': {
|
||||
'matchers': [
|
||||
'**/*.{yaml,yml}',
|
||||
'devtools/**/*.{js,ts}',
|
||||
'tools/**/*.{js,ts}',
|
||||
'modules/**/*.{js,ts}',
|
||||
'scripts/**/*.{js,ts}',
|
||||
|
|
@ -38,6 +39,7 @@ export const format: FormatConfig = {
|
|||
'!adev/**',
|
||||
|
||||
// Migrated to prettier
|
||||
'!devtools/**/*.{js,ts}',
|
||||
'!tools/**/*.{js,ts}',
|
||||
'!modules/**/*.{js,ts}',
|
||||
'!scripts/**/*.{js,ts}',
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@ describe('Comment nodes', () => {
|
|||
it('should find comment nodes when the setting is enabled', () => {
|
||||
showComments();
|
||||
cy.get('.tree-wrapper')
|
||||
.find('.tree-node:contains("#comment")')
|
||||
.its('length')
|
||||
.should('not.eq', 0);
|
||||
.find('.tree-node:contains("#comment")')
|
||||
.its('length')
|
||||
.should('not.eq', 0);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -19,27 +19,29 @@ describe('Tracking items from application to component tree', () => {
|
|||
});
|
||||
|
||||
cy.get('.tree-wrapper')
|
||||
.find('.tree-node:contains("app-todo[TooltipDirective]")')
|
||||
.its('length')
|
||||
.should('eq', 2);
|
||||
.find('.tree-node:contains("app-todo[TooltipDirective]")')
|
||||
.its('length')
|
||||
.should('eq', 2);
|
||||
});
|
||||
|
||||
it('should be able to detect a new todo from user and add it to the tree', () => {
|
||||
cy.enter('#sample-app')
|
||||
.then((getBody) => {
|
||||
getBody().find('input.new-todo').type('Buy cookies{enter}');
|
||||
})
|
||||
.then(() => {
|
||||
cy.enter('#sample-app').then((getBody) => {
|
||||
getBody().find('app-todo').contains('Buy milk');
|
||||
.then((getBody) => {
|
||||
getBody().find('input.new-todo').type('Buy cookies{enter}');
|
||||
})
|
||||
.then(() => {
|
||||
cy.enter('#sample-app').then((getBody) => {
|
||||
getBody().find('app-todo').contains('Buy milk');
|
||||
|
||||
getBody().find('app-todo').contains('Build something fun!');
|
||||
getBody().find('app-todo').contains('Build something fun!');
|
||||
|
||||
getBody().find('app-todo').contains('Buy cookies');
|
||||
});
|
||||
getBody().find('app-todo').contains('Buy cookies');
|
||||
});
|
||||
});
|
||||
|
||||
cy.get('.tree-wrapper .tree-node:contains("app-todo[TooltipDirective]")')
|
||||
.should('have.length', 3);
|
||||
cy.get('.tree-wrapper .tree-node:contains("app-todo[TooltipDirective]")').should(
|
||||
'have.length',
|
||||
3,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -83,9 +83,9 @@ describe('Search items in component tree', () => {
|
|||
const amountOfBreadcrumbButtons = 4;
|
||||
const amountOfScrollButtons = 2;
|
||||
cy.get('ng-breadcrumbs')
|
||||
.find('button')
|
||||
.its('length')
|
||||
.should('eq', amountOfScrollButtons + amountOfBreadcrumbButtons);
|
||||
.find('button')
|
||||
.its('length')
|
||||
.should('eq', amountOfScrollButtons + amountOfBreadcrumbButtons);
|
||||
|
||||
// should display correct text in explorer panel
|
||||
checkComponentName('app-todos');
|
||||
|
|
|
|||
|
|
@ -18,9 +18,9 @@ describe('node selection', () => {
|
|||
cy.get('.tree-wrapper').get('.tree-node.selected').should('not.exist');
|
||||
|
||||
cy.get('.tree-wrapper')
|
||||
.find('.tree-node:contains("app-todo[TooltipDirective]")')
|
||||
.first()
|
||||
.click({force: true});
|
||||
.find('.tree-node:contains("app-todo[TooltipDirective]")')
|
||||
.first()
|
||||
.click({force: true});
|
||||
|
||||
cy.get('.tree-wrapper').find('.tree-node.selected').its('length').should('eq', 1);
|
||||
|
||||
|
|
@ -39,9 +39,9 @@ describe('node selection', () => {
|
|||
});
|
||||
|
||||
cy.get('.tree-wrapper')
|
||||
.find('.tree-node:contains("app-todo[TooltipDirective]")')
|
||||
.last()
|
||||
.click({force: true});
|
||||
.find('.tree-node:contains("app-todo[TooltipDirective]")')
|
||||
.last()
|
||||
.click({force: true});
|
||||
|
||||
cy.enter('#sample-app').then((getBody) => {
|
||||
getBody().find('app-todo:contains("Buy milk")').find('.destroy').click();
|
||||
|
|
@ -52,86 +52,100 @@ describe('node selection', () => {
|
|||
|
||||
it('should select nodes with same name', () => {
|
||||
cy.get('.tree-wrapper')
|
||||
.find('.tree-node:contains("app-todo[TooltipDirective]")')
|
||||
.first()
|
||||
.click({force: true});
|
||||
.find('.tree-node:contains("app-todo[TooltipDirective]")')
|
||||
.first()
|
||||
.click({force: true});
|
||||
|
||||
cy.get('.tree-wrapper')
|
||||
.find('.tree-node:contains("app-todo[TooltipDirective]")')
|
||||
.last()
|
||||
.click({force: true});
|
||||
.find('.tree-node:contains("app-todo[TooltipDirective]")')
|
||||
.last()
|
||||
.click({force: true});
|
||||
|
||||
cy.get('ng-property-view').last().find('mat-tree-node:contains("todo")').click();
|
||||
|
||||
cy.get('ng-property-view')
|
||||
.last()
|
||||
.find('mat-tree-node:contains("Build something fun!")')
|
||||
.its('length')
|
||||
.should('eq', 1);
|
||||
.last()
|
||||
.find('mat-tree-node:contains("Build something fun!")')
|
||||
.its('length')
|
||||
.should('eq', 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('breadcrumb logic', () => {
|
||||
it('should overflow when breadcrumb list is long enough', () => {
|
||||
cy.get('.tree-wrapper')
|
||||
.find('.tree-node:contains("div[TooltipDirective]")')
|
||||
.last()
|
||||
.click({force: true})
|
||||
.then(() => {
|
||||
cy.get('ng-breadcrumbs').find('.breadcrumbs').then((breadcrumbsContainer) => {
|
||||
.find('.tree-node:contains("div[TooltipDirective]")')
|
||||
.last()
|
||||
.click({force: true})
|
||||
.then(() => {
|
||||
cy.get('ng-breadcrumbs')
|
||||
.find('.breadcrumbs')
|
||||
.then((breadcrumbsContainer) => {
|
||||
const hasOverflowX = () =>
|
||||
breadcrumbsContainer[0].scrollWidth > breadcrumbsContainer[0].clientWidth;
|
||||
breadcrumbsContainer[0].scrollWidth > breadcrumbsContainer[0].clientWidth;
|
||||
expect(hasOverflowX()).to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should scroll right when right scroll button is clicked', () => {
|
||||
cy.get('.tree-wrapper')
|
||||
.find('.tree-node:contains("div[TooltipDirective]")')
|
||||
.last()
|
||||
.click({force: true})
|
||||
.then(() => {
|
||||
cy.get('ng-breadcrumbs')
|
||||
.find('.breadcrumbs')
|
||||
.then((el) => {
|
||||
el[0].style.scrollBehavior = 'auto';
|
||||
})
|
||||
.then((breadcrumbsContainer) => {
|
||||
const scrollLeft = () => breadcrumbsContainer[0].scrollLeft;
|
||||
expect(scrollLeft()).to.eql(0);
|
||||
.find('.tree-node:contains("div[TooltipDirective]")')
|
||||
.last()
|
||||
.click({force: true})
|
||||
.then(() => {
|
||||
cy.get('ng-breadcrumbs')
|
||||
.find('.breadcrumbs')
|
||||
.then((el) => {
|
||||
el[0].style.scrollBehavior = 'auto';
|
||||
})
|
||||
.then((breadcrumbsContainer) => {
|
||||
const scrollLeft = () => breadcrumbsContainer[0].scrollLeft;
|
||||
expect(scrollLeft()).to.eql(0);
|
||||
|
||||
cy.get('ng-breadcrumbs').find('.scroll-button').last().click().then(() => {
|
||||
expect(scrollLeft()).to.be.greaterThan(0);
|
||||
});
|
||||
cy.get('ng-breadcrumbs')
|
||||
.find('.scroll-button')
|
||||
.last()
|
||||
.click()
|
||||
.then(() => {
|
||||
expect(scrollLeft()).to.be.greaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should scroll left when left scroll button is clicked', () => {
|
||||
cy.get('.tree-wrapper')
|
||||
.find('.tree-node:contains("div[TooltipDirective]")')
|
||||
.last()
|
||||
.click({force: true})
|
||||
.then(() => {
|
||||
cy.get('ng-breadcrumbs')
|
||||
.find('.breadcrumbs')
|
||||
.then((el) => {
|
||||
el[0].style.scrollBehavior = 'auto';
|
||||
})
|
||||
.then((breadcrumbsContainer) => {
|
||||
const scrollLeft = () => breadcrumbsContainer[0].scrollLeft;
|
||||
expect(scrollLeft()).to.eql(0);
|
||||
.find('.tree-node:contains("div[TooltipDirective]")')
|
||||
.last()
|
||||
.click({force: true})
|
||||
.then(() => {
|
||||
cy.get('ng-breadcrumbs')
|
||||
.find('.breadcrumbs')
|
||||
.then((el) => {
|
||||
el[0].style.scrollBehavior = 'auto';
|
||||
})
|
||||
.then((breadcrumbsContainer) => {
|
||||
const scrollLeft = () => breadcrumbsContainer[0].scrollLeft;
|
||||
expect(scrollLeft()).to.eql(0);
|
||||
|
||||
cy.get('ng-breadcrumbs').find('.scroll-button').last().click().then(() => {
|
||||
expect(scrollLeft()).to.be.greaterThan(0);
|
||||
cy.get('ng-breadcrumbs')
|
||||
.find('.scroll-button')
|
||||
.last()
|
||||
.click()
|
||||
.then(() => {
|
||||
expect(scrollLeft()).to.be.greaterThan(0);
|
||||
|
||||
cy.get('ng-breadcrumbs').find('.scroll-button').first().click().then(() => {
|
||||
cy.get('ng-breadcrumbs')
|
||||
.find('.scroll-button')
|
||||
.first()
|
||||
.click()
|
||||
.then(() => {
|
||||
expect(scrollLeft()).to.eql(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,9 +17,9 @@ describe('edit properties of directive in the property view tab', () => {
|
|||
beforeEach(() => {
|
||||
// select todo node in component tree
|
||||
cy.get('.tree-wrapper')
|
||||
.find('.tree-node:contains("app-todo[TooltipDirective]")')
|
||||
.first()
|
||||
.click({force: true});
|
||||
.find('.tree-node:contains("app-todo[TooltipDirective]")')
|
||||
.first()
|
||||
.click({force: true});
|
||||
});
|
||||
|
||||
it('should be able to enable editMode', () => {
|
||||
|
|
@ -28,13 +28,13 @@ describe('edit properties of directive in the property view tab', () => {
|
|||
});
|
||||
|
||||
cy.get('.explorer-panel:contains("app-todo")')
|
||||
.find('ng-property-view mat-tree-node:contains("editMode")')
|
||||
.find('ng-property-editor .editor')
|
||||
.click({force: true})
|
||||
.find('.editor-input')
|
||||
.clear()
|
||||
.type('true')
|
||||
.type('{enter}');
|
||||
.find('ng-property-view mat-tree-node:contains("editMode")')
|
||||
.find('ng-property-editor .editor')
|
||||
.click({force: true})
|
||||
.find('.editor-input')
|
||||
.clear()
|
||||
.type('true')
|
||||
.type('{enter}');
|
||||
|
||||
cy.enter('#sample-app').then((getBody) => {
|
||||
getBody().find('app-todo input.edit').should('be.visible');
|
||||
|
|
@ -45,8 +45,8 @@ describe('edit properties of directive in the property view tab', () => {
|
|||
beforeEach(() => {
|
||||
// expand todo state
|
||||
cy.get('.explorer-panel:contains("app-todo")')
|
||||
.find('ng-property-view mat-tree-node:contains("todo")')
|
||||
.click();
|
||||
.find('ng-property-view mat-tree-node:contains("todo")')
|
||||
.click();
|
||||
});
|
||||
|
||||
it('should change todo label in app when edited', () => {
|
||||
|
|
@ -57,13 +57,13 @@ describe('edit properties of directive in the property view tab', () => {
|
|||
|
||||
// find label variable and run through edit logic
|
||||
cy.get('.explorer-panel:contains("app-todo")')
|
||||
.find('ng-property-view mat-tree-node:contains("label")')
|
||||
.find('ng-property-editor .editor')
|
||||
.click()
|
||||
.find('.editor-input')
|
||||
.clear()
|
||||
.type('Buy cookies')
|
||||
.type('{enter}');
|
||||
.find('ng-property-view mat-tree-node:contains("label")')
|
||||
.find('ng-property-editor .editor')
|
||||
.click()
|
||||
.find('.editor-input')
|
||||
.clear()
|
||||
.type('Buy cookies')
|
||||
.type('{enter}');
|
||||
|
||||
// assert that the page has been updated
|
||||
cy.enter('#sample-app').then((getBody) => {
|
||||
|
|
@ -79,13 +79,13 @@ describe('edit properties of directive in the property view tab', () => {
|
|||
|
||||
// find completed variable and run through edit logic
|
||||
cy.get('.explorer-panel:contains("app-todo")')
|
||||
.find('ng-property-view mat-tree-node:contains("completed")')
|
||||
.find('ng-property-editor .editor')
|
||||
.click()
|
||||
.find('.editor-input')
|
||||
.clear()
|
||||
.type('true')
|
||||
.type('{enter}');
|
||||
.find('ng-property-view mat-tree-node:contains("completed")')
|
||||
.find('ng-property-editor .editor')
|
||||
.click()
|
||||
.find('.editor-input')
|
||||
.clear()
|
||||
.type('true')
|
||||
.type('{enter}');
|
||||
|
||||
// assert that the page has been updated
|
||||
cy.enter('#sample-app').then((getBody) => {
|
||||
|
|
|
|||
|
|
@ -21,20 +21,21 @@ describe('change of the state should reflect in property update', () => {
|
|||
|
||||
// Select the todo item
|
||||
cy.get('.tree-wrapper')
|
||||
.find('.tree-node:contains("app-todo[TooltipDirective]")')
|
||||
.first()
|
||||
.click({force: true});
|
||||
.find('.tree-node:contains("app-todo[TooltipDirective]")')
|
||||
.first()
|
||||
.click({force: true});
|
||||
|
||||
// Expand the todo in the property explorer
|
||||
cy.get('.explorer-panel:contains("app-todo")')
|
||||
.find('ng-property-view mat-tree-node:contains("todo")')
|
||||
.click();
|
||||
.find('ng-property-view mat-tree-node:contains("todo")')
|
||||
.click();
|
||||
|
||||
// Verify its value is now completed
|
||||
cy.contains(
|
||||
'.explorer-panel:contains("app-todo") ' +
|
||||
'ng-property-view mat-tree-node:contains("completed") ' +
|
||||
'ng-property-editor .editor',
|
||||
'true');
|
||||
'.explorer-panel:contains("app-todo") ' +
|
||||
'ng-property-view mat-tree-node:contains("completed") ' +
|
||||
'ng-property-editor .editor',
|
||||
'true',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,9 +17,9 @@ describe('Viewing component metadata', () => {
|
|||
});
|
||||
|
||||
describe('viewing TodoComponent', () => {
|
||||
beforeEach(
|
||||
() => prepareHeaderExpansionPanelForAssertions(
|
||||
'.tree-node:contains("app-todo[TooltipDirective]")'));
|
||||
beforeEach(() =>
|
||||
prepareHeaderExpansionPanelForAssertions('.tree-node:contains("app-todo[TooltipDirective]")'),
|
||||
);
|
||||
|
||||
it('should display view encapsulation', () => {
|
||||
cy.contains('.meta-data-container .mat-button:first', 'View Encapsulation: Emulated');
|
||||
|
|
@ -31,9 +31,9 @@ describe('Viewing component metadata', () => {
|
|||
});
|
||||
|
||||
describe('viewing DemoAppComponent', () => {
|
||||
beforeEach(
|
||||
() =>
|
||||
prepareHeaderExpansionPanelForAssertions('.tree-node:contains("app-demo-component")'));
|
||||
beforeEach(() =>
|
||||
prepareHeaderExpansionPanelForAssertions('.tree-node:contains("app-demo-component")'),
|
||||
);
|
||||
|
||||
it('should display view encapsulation', () => {
|
||||
cy.contains('.meta-data-container .mat-button:first', 'View Encapsulation: None');
|
||||
|
|
|
|||
|
|
@ -17,5 +17,4 @@ import {AppComponent} from './app.component';
|
|||
providers: [],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
export class AppModule {
|
||||
}
|
||||
export class AppModule {}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
|
|||
import {AppModule} from './app/app.module';
|
||||
|
||||
platformBrowserDynamic()
|
||||
.bootstrapModule(AppModule, {
|
||||
ngZone: 'noop',
|
||||
})
|
||||
.catch((err) => console.error(err));
|
||||
.bootstrapModule(AppModule, {
|
||||
ngZone: 'noop',
|
||||
})
|
||||
.catch((err) => console.error(err));
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {Router, RouterOutlet} from '@angular/router';
|
|||
selector: 'app-root',
|
||||
template: `<router-outlet></router-outlet>`,
|
||||
standalone: true,
|
||||
imports: [RouterOutlet]
|
||||
imports: [RouterOutlet],
|
||||
})
|
||||
export class AppComponent {
|
||||
constructor(public router: Router) {}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,20 @@
|
|||
*/
|
||||
|
||||
import {JsonPipe} from '@angular/common';
|
||||
import {Component, computed, CUSTOM_ELEMENTS_SCHEMA, ElementRef, EventEmitter, inject, Injector, Input, Output, signal, ViewChild, ViewEncapsulation} from '@angular/core';
|
||||
import {
|
||||
Component,
|
||||
computed,
|
||||
CUSTOM_ELEMENTS_SCHEMA,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
inject,
|
||||
Injector,
|
||||
Input,
|
||||
Output,
|
||||
signal,
|
||||
ViewChild,
|
||||
ViewEncapsulation,
|
||||
} from '@angular/core';
|
||||
import {createCustomElement} from '@angular/elements';
|
||||
import {RouterOutlet} from '@angular/router';
|
||||
import {initializeMessageBus} from 'ng-devtools-backend';
|
||||
|
|
@ -24,7 +37,7 @@ import {ZippyComponent} from './zippy.component';
|
|||
encapsulation: ViewEncapsulation.None,
|
||||
standalone: true,
|
||||
imports: [HeavyComponent, RouterOutlet, JsonPipe],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
})
|
||||
export class DemoAppComponent {
|
||||
@ViewChild(ZippyComponent) zippy!: ZippyComponent;
|
||||
|
|
@ -50,7 +63,7 @@ export class DemoAppComponent {
|
|||
customElements.define('app-zippy', el as any);
|
||||
}
|
||||
|
||||
getTitle(): '► Click to expand'|'▼ Click to collapse' {
|
||||
getTitle(): '► Click to expand' | '▼ Click to collapse' {
|
||||
if (!this.zippy || !this.zippy.visible) {
|
||||
return '► Click to expand';
|
||||
}
|
||||
|
|
@ -71,5 +84,10 @@ export const ROUTES = [
|
|||
},
|
||||
];
|
||||
|
||||
initializeMessageBus(new ZoneUnawareIFrameMessageBus(
|
||||
'angular-devtools-backend', 'angular-devtools', () => window.parent));
|
||||
initializeMessageBus(
|
||||
new ZoneUnawareIFrameMessageBus(
|
||||
'angular-devtools-backend',
|
||||
'angular-devtools',
|
||||
() => window.parent,
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -18,8 +18,7 @@ const fib = (n: number): number => {
|
|||
@Component({selector: 'app-heavy', template: `<h1>{{ calculate() }}</h1>`, standalone: true})
|
||||
export class HeavyComponent {
|
||||
@Input()
|
||||
set foo(_: any) {
|
||||
}
|
||||
set foo(_: any) {}
|
||||
|
||||
state = {
|
||||
nested: {
|
||||
|
|
@ -27,14 +26,12 @@ export class HeavyComponent {
|
|||
foo: 1,
|
||||
bar: 2,
|
||||
},
|
||||
[Symbol(3)]():
|
||||
number {
|
||||
return 1.618;
|
||||
},
|
||||
get foo():
|
||||
number {
|
||||
return 42;
|
||||
},
|
||||
[Symbol(3)](): number {
|
||||
return 1.618;
|
||||
},
|
||||
get foo(): number {
|
||||
return 42;
|
||||
},
|
||||
},
|
||||
};
|
||||
calculate(): number {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ import {RouterLink, RouterOutlet} from '@angular/router';
|
|||
<a [routerLink]="">Home</a>
|
||||
<a [routerLink]="">Home</a>
|
||||
<a [routerLink]="">Home</a>
|
||||
`
|
||||
`,
|
||||
})
|
||||
export class AboutComponent {
|
||||
}
|
||||
export class AboutComponent {}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import {FormsModule} from '@angular/forms';
|
|||
import {MAT_DIALOG_DATA, MatDialogModule, MatDialogRef} from '@angular/material/dialog';
|
||||
import {MatFormFieldModule} from '@angular/material/form-field';
|
||||
|
||||
|
||||
export interface DialogData {
|
||||
animal: string;
|
||||
name: string;
|
||||
|
|
@ -39,8 +38,9 @@ export interface DialogData {
|
|||
})
|
||||
export class DialogComponent {
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<DialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: DialogData) {}
|
||||
public dialogRef: MatDialogRef<DialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: DialogData,
|
||||
) {}
|
||||
|
||||
onNoClick(): void {
|
||||
this.dialogRef.close();
|
||||
|
|
|
|||
|
|
@ -22,17 +22,21 @@ export interface Todo {
|
|||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [TooltipDirective],
|
||||
styles: [`
|
||||
styles: [
|
||||
`
|
||||
.destroy {
|
||||
cursor: pointer;
|
||||
display: unset !important;
|
||||
}
|
||||
`],
|
||||
`,
|
||||
],
|
||||
template: `
|
||||
<li [class.completed]="todo.completed">
|
||||
<div class="view" appTooltip>
|
||||
<input class="toggle" type="checkbox" [checked]="todo.completed" (change)="toggle()" />
|
||||
<label (dblclick)="enableEditMode()" [style.display]="editMode ? 'none' : 'block'">{{ todo.label }}</label>
|
||||
<label (dblclick)="enableEditMode()" [style.display]="editMode ? 'none' : 'block'">{{
|
||||
todo.label
|
||||
}}</label>
|
||||
<button class="destroy" (click)="delete.emit(todo)"></button>
|
||||
</div>
|
||||
<input
|
||||
|
|
@ -42,7 +46,7 @@ export interface Todo {
|
|||
(keydown.enter)="completeEdit($any($event.target).value)"
|
||||
/>
|
||||
</li>
|
||||
`
|
||||
`,
|
||||
})
|
||||
export class TodoComponent {
|
||||
@Input() todo!: Todo;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,16 @@
|
|||
*/
|
||||
|
||||
import {NgForOf} from '@angular/common';
|
||||
import {ChangeDetectorRef, Component, EventEmitter, OnDestroy, OnInit, Output, Pipe, PipeTransform} from '@angular/core';
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
EventEmitter,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
Pipe,
|
||||
PipeTransform,
|
||||
} from '@angular/core';
|
||||
import {RouterLink} from '@angular/router';
|
||||
|
||||
import {SamplePipe} from './sample.pipe';
|
||||
|
|
@ -57,19 +66,25 @@ const fib = (n: number): number => {
|
|||
<section class="todoapp">
|
||||
<header class="header">
|
||||
<h1>todos</h1>
|
||||
<input (keydown.enter)="addTodo(input)" #input class="new-todo" placeholder="What needs to be done?" autofocus />
|
||||
<input
|
||||
(keydown.enter)="addTodo(input)"
|
||||
#input
|
||||
class="new-todo"
|
||||
placeholder="What needs to be done?"
|
||||
autofocus
|
||||
/>
|
||||
</header>
|
||||
<section class="main">
|
||||
<input id="toggle-all" class="toggle-all" type="checkbox" />
|
||||
<label for="toggle-all">Mark all as complete</label>
|
||||
<ul class="todo-list">
|
||||
@for (todo of todos | todosFilter: filterValue; track todo) {
|
||||
<app-todo
|
||||
appTooltip
|
||||
[todo]="todo"
|
||||
(delete)="onDelete($event)"
|
||||
(update)="onChange($event)"
|
||||
/>
|
||||
<app-todo
|
||||
appTooltip
|
||||
[todo]="todo"
|
||||
(delete)="onDelete($event)"
|
||||
(update)="onChange($event)"
|
||||
/>
|
||||
}
|
||||
</ul>
|
||||
</section>
|
||||
|
|
@ -80,7 +95,7 @@ const fib = (n: number): number => {
|
|||
<button class="clear-completed" (click)="clearCompleted()">Clear completed</button>
|
||||
</footer>
|
||||
</section>
|
||||
`
|
||||
`,
|
||||
})
|
||||
export class TodosComponent implements OnInit, OnDestroy {
|
||||
todos: Todo[] = [
|
||||
|
|
|
|||
|
|
@ -19,9 +19,16 @@ import {DialogComponent} from './dialog.component';
|
|||
@Component({
|
||||
selector: 'app-todo-demo',
|
||||
standalone: true,
|
||||
imports:
|
||||
[RouterLink, RouterOutlet, MatDialogModule, MatFormFieldModule, MatInputModule, FormsModule],
|
||||
styles: [`
|
||||
imports: [
|
||||
RouterLink,
|
||||
RouterOutlet,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
FormsModule,
|
||||
],
|
||||
styles: [
|
||||
`
|
||||
nav {
|
||||
padding-top: 20px;
|
||||
padding-bottom: 10px;
|
||||
|
|
@ -37,7 +44,8 @@ import {DialogComponent} from './dialog.component';
|
|||
padding: 10px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
`],
|
||||
`,
|
||||
],
|
||||
template: `
|
||||
<nav>
|
||||
<a routerLink="/demo-app/todos/app">Todos</a>
|
||||
|
|
@ -47,7 +55,7 @@ import {DialogComponent} from './dialog.component';
|
|||
<button class="dialog-open-button" (click)="openDialog()">Open dialog</button>
|
||||
|
||||
<router-outlet></router-outlet>
|
||||
`
|
||||
`,
|
||||
})
|
||||
export class TodoAppComponent {
|
||||
name!: string;
|
||||
|
|
|
|||
|
|
@ -11,27 +11,29 @@ import {Component, Input} from '@angular/core';
|
|||
@Component({
|
||||
selector: 'app-zippy',
|
||||
standalone: true,
|
||||
styles: [`
|
||||
:host {
|
||||
user-select: none;
|
||||
}
|
||||
styles: [
|
||||
`
|
||||
:host {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
header {
|
||||
max-width: 120px;
|
||||
border: 1px solid #ccc;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
header {
|
||||
max-width: 120px;
|
||||
border: 1px solid #ccc;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
div {
|
||||
max-width: 120px;
|
||||
border-left: 1px solid #ccc;
|
||||
border-right: 1px solid #ccc;
|
||||
border-bottom: 1px solid #ccc;
|
||||
padding: 10px;
|
||||
}
|
||||
`],
|
||||
div {
|
||||
max-width: 120px;
|
||||
border-left: 1px solid #ccc;
|
||||
border-right: 1px solid #ccc;
|
||||
border-bottom: 1px solid #ccc;
|
||||
padding: 10px;
|
||||
}
|
||||
`,
|
||||
],
|
||||
template: `
|
||||
<section>
|
||||
<header (click)="visible = !visible">{{ title }}</header>
|
||||
|
|
@ -39,7 +41,7 @@ import {Component, Input} from '@angular/core';
|
|||
<ng-content></ng-content>
|
||||
</div>
|
||||
</section>
|
||||
`
|
||||
`,
|
||||
})
|
||||
export class ZippyComponent {
|
||||
@Input() title!: string;
|
||||
|
|
|
|||
|
|
@ -19,33 +19,39 @@ import {IFrameMessageBus} from '../../../../../src/iframe-message-bus';
|
|||
{
|
||||
provide: MessageBus,
|
||||
useFactory(): MessageBus<Events> {
|
||||
return new PriorityAwareMessageBus(new IFrameMessageBus(
|
||||
'angular-devtools', 'angular-devtools-backend',
|
||||
return new PriorityAwareMessageBus(
|
||||
new IFrameMessageBus(
|
||||
'angular-devtools',
|
||||
'angular-devtools-backend',
|
||||
// tslint:disable-next-line: no-non-null-assertion
|
||||
() => (document.querySelector('#sample-app') as HTMLIFrameElement).contentWindow!));
|
||||
() => (document.querySelector('#sample-app') as HTMLIFrameElement).contentWindow!,
|
||||
),
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
styles: [`
|
||||
iframe {
|
||||
height: 340px;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
}
|
||||
styles: [
|
||||
`
|
||||
iframe {
|
||||
height: 340px;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.devtools-wrapper {
|
||||
height: calc(100vh - 345px);
|
||||
}
|
||||
`],
|
||||
.devtools-wrapper {
|
||||
height: calc(100vh - 345px);
|
||||
}
|
||||
`,
|
||||
],
|
||||
template: `
|
||||
<iframe #ref src="demo-app/todos/app" id="sample-app"></iframe>
|
||||
<br />
|
||||
<div class="devtools-wrapper">
|
||||
<ng-devtools></ng-devtools>
|
||||
</div>
|
||||
`
|
||||
`,
|
||||
})
|
||||
export class DevToolsComponent {
|
||||
messageBus: IFrameMessageBus|null = null;
|
||||
messageBus: IFrameMessageBus | null = null;
|
||||
@ViewChild('ref') iframe!: ElementRef;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,5 +8,5 @@
|
|||
|
||||
export const environment = {
|
||||
production: false,
|
||||
LATEST_SHA: 'BUILD_SCM_COMMIT_SHA', // Stamped at build time by bazel
|
||||
LATEST_SHA: 'BUILD_SCM_COMMIT_SHA', // Stamped at build time by bazel
|
||||
};
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import {DemoApplicationOperations} from '../../../src/demo-application-operation
|
|||
|
||||
import {AppComponent} from './app/app.component';
|
||||
|
||||
|
||||
bootstrapApplication(AppComponent, {
|
||||
providers: [
|
||||
provideAnimations(),
|
||||
|
|
@ -24,7 +23,7 @@ bootstrapApplication(AppComponent, {
|
|||
{
|
||||
path: '',
|
||||
loadComponent: () =>
|
||||
import('./app/devtools-app/devtools-app.component').then((m) => m.DevToolsComponent),
|
||||
import('./app/devtools-app/devtools-app.component').then((m) => m.DevToolsComponent),
|
||||
pathMatch: 'full',
|
||||
},
|
||||
{
|
||||
|
|
@ -40,5 +39,5 @@ bootstrapApplication(AppComponent, {
|
|||
provide: ApplicationEnvironment,
|
||||
useClass: DemoApplicationEnvironment,
|
||||
},
|
||||
]
|
||||
],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,12 +6,43 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {ComponentExplorerViewQuery, ComponentType, DevToolsNode, DirectivePosition, DirectiveType, ElementPosition, Events, MessageBus, ProfilerFrame, SerializedInjector, SerializedProviderRecord} from 'protocol';
|
||||
import {
|
||||
ComponentExplorerViewQuery,
|
||||
ComponentType,
|
||||
DevToolsNode,
|
||||
DirectivePosition,
|
||||
DirectiveType,
|
||||
ElementPosition,
|
||||
Events,
|
||||
MessageBus,
|
||||
ProfilerFrame,
|
||||
SerializedInjector,
|
||||
SerializedProviderRecord,
|
||||
} from 'protocol';
|
||||
import {debounceTime} from 'rxjs/operators';
|
||||
import {appIsAngularInDevMode, appIsAngularIvy, appIsSupportedAngularVersion, getAngularVersion,} from 'shared-utils';
|
||||
import {
|
||||
appIsAngularInDevMode,
|
||||
appIsAngularIvy,
|
||||
appIsSupportedAngularVersion,
|
||||
getAngularVersion,
|
||||
} from 'shared-utils';
|
||||
|
||||
import {ComponentInspector} from './component-inspector/component-inspector';
|
||||
import {getElementInjectorElement, getInjectorFromElementNode, getInjectorProviders, getInjectorResolutionPath, getLatestComponentState, idToInjector, injectorsSeen, isElementInjector, nodeInjectorToResolutionPath, queryDirectiveForest, serializeProviderRecord, serializeResolutionPath, updateState} from './component-tree';
|
||||
import {
|
||||
getElementInjectorElement,
|
||||
getInjectorFromElementNode,
|
||||
getInjectorProviders,
|
||||
getInjectorResolutionPath,
|
||||
getLatestComponentState,
|
||||
idToInjector,
|
||||
injectorsSeen,
|
||||
isElementInjector,
|
||||
nodeInjectorToResolutionPath,
|
||||
queryDirectiveForest,
|
||||
serializeProviderRecord,
|
||||
serializeResolutionPath,
|
||||
updateState,
|
||||
} from './component-tree';
|
||||
import {unHighlight} from './highlighter';
|
||||
import {disableTimingAPI, enableTimingAPI, initializeOrGetDirectiveForestHooks} from './hooks';
|
||||
import {start as startProfiling, stop as stopProfiling} from './hooks/capture';
|
||||
|
|
@ -25,7 +56,9 @@ export const subscribeToClientEvents = (messageBus: MessageBus<Events>): void =>
|
|||
messageBus.on('shutdown', shutdownCallback(messageBus));
|
||||
|
||||
messageBus.on(
|
||||
'getLatestComponentExplorerView', getLatestComponentExplorerViewCallback(messageBus));
|
||||
'getLatestComponentExplorerView',
|
||||
getLatestComponentExplorerViewCallback(messageBus),
|
||||
);
|
||||
|
||||
messageBus.on('queryNgAvailability', checkForAngularCallback(messageBus));
|
||||
|
||||
|
|
@ -54,8 +87,8 @@ export const subscribeToClientEvents = (messageBus: MessageBus<Events>): void =>
|
|||
// once every 250ms
|
||||
runOutsideAngular(() => {
|
||||
initializeOrGetDirectiveForestHooks()
|
||||
.profiler.changeDetection$.pipe(debounceTime(250))
|
||||
.subscribe(() => messageBus.emit('componentTreeDirty'));
|
||||
.profiler.changeDetection$.pipe(debounceTime(250))
|
||||
.subscribe(() => messageBus.emit('componentTreeDirty'));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -68,55 +101,58 @@ const shutdownCallback = (messageBus: MessageBus<Events>) => () => {
|
|||
messageBus.destroy();
|
||||
};
|
||||
|
||||
const getLatestComponentExplorerViewCallback = (messageBus: MessageBus<Events>) =>
|
||||
(query?: ComponentExplorerViewQuery) => {
|
||||
// We want to force re-indexing of the component tree.
|
||||
// Pressing the refresh button means the user saw stuck UI.
|
||||
const getLatestComponentExplorerViewCallback =
|
||||
(messageBus: MessageBus<Events>) => (query?: ComponentExplorerViewQuery) => {
|
||||
// We want to force re-indexing of the component tree.
|
||||
// Pressing the refresh button means the user saw stuck UI.
|
||||
|
||||
initializeOrGetDirectiveForestHooks().indexForest();
|
||||
initializeOrGetDirectiveForestHooks().indexForest();
|
||||
|
||||
const forest = prepareForestForSerialization(
|
||||
initializeOrGetDirectiveForestHooks().getIndexedDirectiveForest(),
|
||||
ngDebugDependencyInjectionApiIsSupported());
|
||||
const forest = prepareForestForSerialization(
|
||||
initializeOrGetDirectiveForestHooks().getIndexedDirectiveForest(),
|
||||
ngDebugDependencyInjectionApiIsSupported(),
|
||||
);
|
||||
|
||||
// cleanup injector id mappings
|
||||
for (const injectorId of idToInjector.keys()) {
|
||||
if (!injectorsSeen.has(injectorId)) {
|
||||
const injector = idToInjector.get(injectorId)!;
|
||||
if (isElementInjector(injector)) {
|
||||
const element = getElementInjectorElement(injector);
|
||||
if (element) {
|
||||
nodeInjectorToResolutionPath.delete(element);
|
||||
}
|
||||
// cleanup injector id mappings
|
||||
for (const injectorId of idToInjector.keys()) {
|
||||
if (!injectorsSeen.has(injectorId)) {
|
||||
const injector = idToInjector.get(injectorId)!;
|
||||
if (isElementInjector(injector)) {
|
||||
const element = getElementInjectorElement(injector);
|
||||
if (element) {
|
||||
nodeInjectorToResolutionPath.delete(element);
|
||||
}
|
||||
|
||||
idToInjector.delete(injectorId);
|
||||
}
|
||||
}
|
||||
injectorsSeen.clear();
|
||||
|
||||
if (!query) {
|
||||
messageBus.emit('latestComponentExplorerView', [{forest}]);
|
||||
return;
|
||||
idToInjector.delete(injectorId);
|
||||
}
|
||||
}
|
||||
injectorsSeen.clear();
|
||||
|
||||
const state = getLatestComponentState(
|
||||
query, initializeOrGetDirectiveForestHooks().getDirectiveForest());
|
||||
if (!query) {
|
||||
messageBus.emit('latestComponentExplorerView', [{forest}]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (state) {
|
||||
const {directiveProperties} = state;
|
||||
messageBus.emit('latestComponentExplorerView', [{forest, properties: directiveProperties}]);
|
||||
}
|
||||
};
|
||||
const state = getLatestComponentState(
|
||||
query,
|
||||
initializeOrGetDirectiveForestHooks().getDirectiveForest(),
|
||||
);
|
||||
|
||||
if (state) {
|
||||
const {directiveProperties} = state;
|
||||
messageBus.emit('latestComponentExplorerView', [{forest, properties: directiveProperties}]);
|
||||
}
|
||||
};
|
||||
|
||||
const checkForAngularCallback = (messageBus: MessageBus<Events>) => () =>
|
||||
checkForAngular(messageBus);
|
||||
checkForAngular(messageBus);
|
||||
const getRoutesCallback = (messageBus: MessageBus<Events>) => () => getRoutes(messageBus);
|
||||
|
||||
const startProfilingCallback = (messageBus: MessageBus<Events>) => () =>
|
||||
startProfiling((frame: ProfilerFrame) => {
|
||||
messageBus.emit('sendProfilerChunk', [frame]);
|
||||
});
|
||||
startProfiling((frame: ProfilerFrame) => {
|
||||
messageBus.emit('sendProfilerChunk', [frame]);
|
||||
});
|
||||
|
||||
const stopProfilingCallback = (messageBus: MessageBus<Events>) => () => {
|
||||
messageBus.emit('profilerResults', [stopProfiling()]);
|
||||
|
|
@ -124,33 +160,41 @@ const stopProfilingCallback = (messageBus: MessageBus<Events>) => () => {
|
|||
|
||||
const selectedComponentCallback = (position: ElementPosition) => {
|
||||
const node = queryDirectiveForest(
|
||||
position, initializeOrGetDirectiveForestHooks().getIndexedDirectiveForest());
|
||||
position,
|
||||
initializeOrGetDirectiveForestHooks().getIndexedDirectiveForest(),
|
||||
);
|
||||
setConsoleReference({node, position});
|
||||
};
|
||||
|
||||
const getNestedPropertiesCallback = (messageBus: MessageBus<Events>) => (
|
||||
position: DirectivePosition, propPath: string[]) => {
|
||||
const emitEmpty = () => messageBus.emit('nestedProperties', [position, {props: {}}, propPath]);
|
||||
const node = queryDirectiveForest(
|
||||
position.element, initializeOrGetDirectiveForestHooks().getIndexedDirectiveForest());
|
||||
if (!node) {
|
||||
return emitEmpty();
|
||||
}
|
||||
const current =
|
||||
position.directive === undefined ? node.component : node.directives[position.directive];
|
||||
if (!current) {
|
||||
return emitEmpty();
|
||||
}
|
||||
let data = current.instance;
|
||||
for (const prop of propPath) {
|
||||
data = data[prop];
|
||||
if (!data) {
|
||||
console.error('Cannot access the properties', propPath, 'of', node);
|
||||
const getNestedPropertiesCallback =
|
||||
(messageBus: MessageBus<Events>) => (position: DirectivePosition, propPath: string[]) => {
|
||||
const emitEmpty = () => messageBus.emit('nestedProperties', [position, {props: {}}, propPath]);
|
||||
const node = queryDirectiveForest(
|
||||
position.element,
|
||||
initializeOrGetDirectiveForestHooks().getIndexedDirectiveForest(),
|
||||
);
|
||||
if (!node) {
|
||||
return emitEmpty();
|
||||
}
|
||||
}
|
||||
messageBus.emit('nestedProperties', [position, {props: serializeDirectiveState(data)}, propPath]);
|
||||
return;
|
||||
};
|
||||
const current =
|
||||
position.directive === undefined ? node.component : node.directives[position.directive];
|
||||
if (!current) {
|
||||
return emitEmpty();
|
||||
}
|
||||
let data = current.instance;
|
||||
for (const prop of propPath) {
|
||||
data = data[prop];
|
||||
if (!data) {
|
||||
console.error('Cannot access the properties', propPath, 'of', node);
|
||||
}
|
||||
}
|
||||
messageBus.emit('nestedProperties', [
|
||||
position,
|
||||
{props: serializeDirectiveState(data)},
|
||||
propPath,
|
||||
]);
|
||||
return;
|
||||
};
|
||||
|
||||
//
|
||||
// Subscribe Helpers
|
||||
|
|
@ -209,44 +253,46 @@ export interface SerializableComponentInstanceType extends ComponentType {
|
|||
id: number;
|
||||
}
|
||||
|
||||
export interface SerializableComponentTreeNode extends
|
||||
DevToolsNode<SerializableDirectiveInstanceType, SerializableComponentInstanceType> {
|
||||
export interface SerializableComponentTreeNode
|
||||
extends DevToolsNode<SerializableDirectiveInstanceType, SerializableComponentInstanceType> {
|
||||
children: SerializableComponentTreeNode[];
|
||||
}
|
||||
|
||||
// Here we drop properties to prepare the tree for serialization.
|
||||
// We don't need the component instance, so we just traverse the tree
|
||||
// and leave the component name.
|
||||
const prepareForestForSerialization = (roots: ComponentTreeNode[], includeResolutionPath = false):
|
||||
SerializableComponentTreeNode[] => {
|
||||
const serializedNodes: SerializableComponentTreeNode[] = [];
|
||||
for (const node of roots) {
|
||||
const serializedNode: SerializableComponentTreeNode = {
|
||||
element: node.element,
|
||||
component: node.component ? {
|
||||
const prepareForestForSerialization = (
|
||||
roots: ComponentTreeNode[],
|
||||
includeResolutionPath = false,
|
||||
): SerializableComponentTreeNode[] => {
|
||||
const serializedNodes: SerializableComponentTreeNode[] = [];
|
||||
for (const node of roots) {
|
||||
const serializedNode: SerializableComponentTreeNode = {
|
||||
element: node.element,
|
||||
component: node.component
|
||||
? {
|
||||
name: node.component.name,
|
||||
isElement: node.component.isElement,
|
||||
id: initializeOrGetDirectiveForestHooks().getDirectiveId(node.component.instance)!,
|
||||
} :
|
||||
null,
|
||||
directives: node.directives.map(
|
||||
(d) => ({
|
||||
name: d.name,
|
||||
id: initializeOrGetDirectiveForestHooks().getDirectiveId(d.instance)!,
|
||||
})),
|
||||
children: prepareForestForSerialization(node.children, includeResolutionPath),
|
||||
};
|
||||
serializedNodes.push(serializedNode);
|
||||
|
||||
if (includeResolutionPath) {
|
||||
serializedNode.resolutionPath = getNodeDIResolutionPath(node);
|
||||
}
|
||||
}
|
||||
|
||||
return serializedNodes;
|
||||
}
|
||||
: null,
|
||||
directives: node.directives.map((d) => ({
|
||||
name: d.name,
|
||||
id: initializeOrGetDirectiveForestHooks().getDirectiveId(d.instance)!,
|
||||
})),
|
||||
children: prepareForestForSerialization(node.children, includeResolutionPath),
|
||||
};
|
||||
serializedNodes.push(serializedNode);
|
||||
|
||||
function getNodeDIResolutionPath(node: ComponentTreeNode): SerializedInjector[]|undefined {
|
||||
if (includeResolutionPath) {
|
||||
serializedNode.resolutionPath = getNodeDIResolutionPath(node);
|
||||
}
|
||||
}
|
||||
|
||||
return serializedNodes;
|
||||
};
|
||||
|
||||
function getNodeDIResolutionPath(node: ComponentTreeNode): SerializedInjector[] | undefined {
|
||||
const nodeInjector = getInjectorFromElementNode(node.nativeElement!);
|
||||
if (!nodeInjector) {
|
||||
return [];
|
||||
|
|
@ -274,91 +320,95 @@ function getNodeDIResolutionPath(node: ComponentTreeNode): SerializedInjector[]|
|
|||
return serializedPath;
|
||||
}
|
||||
|
||||
const getInjectorProvidersCallback = (messageBus: MessageBus<Events>) =>
|
||||
(injector: SerializedInjector) => {
|
||||
if (!idToInjector.has(injector.id)) {
|
||||
return;
|
||||
const getInjectorProvidersCallback =
|
||||
(messageBus: MessageBus<Events>) => (injector: SerializedInjector) => {
|
||||
if (!idToInjector.has(injector.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const providerRecords = getInjectorProviders(idToInjector.get(injector.id)!);
|
||||
const allProviderRecords: SerializedProviderRecord[] = [];
|
||||
|
||||
const tokenToRecords: Map<any, SerializedProviderRecord[]> = new Map();
|
||||
|
||||
for (const [index, providerRecord] of providerRecords.entries()) {
|
||||
const record = serializeProviderRecord(
|
||||
providerRecord,
|
||||
index,
|
||||
injector.type === 'environment',
|
||||
);
|
||||
|
||||
allProviderRecords.push(record);
|
||||
|
||||
const records = tokenToRecords.get(providerRecord.token) ?? [];
|
||||
records.push(record);
|
||||
tokenToRecords.set(providerRecord.token, records);
|
||||
}
|
||||
|
||||
const serializedProviderRecords: SerializedProviderRecord[] = [];
|
||||
|
||||
for (const [token, records] of tokenToRecords.entries()) {
|
||||
const multiRecords = records.filter((record) => record.multi);
|
||||
const nonMultiRecords = records.filter((record) => !record.multi);
|
||||
|
||||
for (const record of nonMultiRecords) {
|
||||
serializedProviderRecords.push(record);
|
||||
}
|
||||
|
||||
const providerRecords = getInjectorProviders(idToInjector.get(injector.id)!);
|
||||
const allProviderRecords: SerializedProviderRecord[] = [];
|
||||
|
||||
const tokenToRecords: Map<any, SerializedProviderRecord[]> = new Map();
|
||||
|
||||
for (const [index, providerRecord] of providerRecords.entries()) {
|
||||
const record =
|
||||
serializeProviderRecord(providerRecord, index, injector.type === 'environment');
|
||||
|
||||
allProviderRecords.push(record);
|
||||
|
||||
const records = tokenToRecords.get(providerRecord.token) ?? [];
|
||||
records.push(record);
|
||||
tokenToRecords.set(providerRecord.token, records);
|
||||
const [firstMultiRecord] = multiRecords;
|
||||
if (firstMultiRecord !== undefined) {
|
||||
// All multi providers will have the same token, so we can just use the first one.
|
||||
serializedProviderRecords.push({
|
||||
token: firstMultiRecord.token,
|
||||
type: 'multi',
|
||||
multi: true,
|
||||
// todo(aleksanderbodurri): implememnt way to differentiate multi providers that
|
||||
// provided as viewProviders
|
||||
isViewProvider: firstMultiRecord.isViewProvider,
|
||||
index: records.map((record) => record.index as number),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const serializedProviderRecords: SerializedProviderRecord[] = [];
|
||||
messageBus.emit('latestInjectorProviders', [injector, serializedProviderRecords]);
|
||||
};
|
||||
|
||||
for (const [token, records] of tokenToRecords.entries()) {
|
||||
const multiRecords = records.filter(record => record.multi);
|
||||
const nonMultiRecords = records.filter(record => !record.multi);
|
||||
const logProvider = (
|
||||
serializedInjector: SerializedInjector,
|
||||
serializedProvider: SerializedProviderRecord,
|
||||
): void => {
|
||||
if (!idToInjector.has(serializedInjector.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const record of nonMultiRecords) {
|
||||
serializedProviderRecords.push(record);
|
||||
}
|
||||
const injector = idToInjector.get(serializedInjector.id)!;
|
||||
|
||||
const [firstMultiRecord] = multiRecords;
|
||||
if (firstMultiRecord !== undefined) {
|
||||
// All multi providers will have the same token, so we can just use the first one.
|
||||
serializedProviderRecords.push({
|
||||
token: firstMultiRecord.token,
|
||||
type: 'multi',
|
||||
multi: true,
|
||||
// todo(aleksanderbodurri): implememnt way to differentiate multi providers that
|
||||
// provided as viewProviders
|
||||
isViewProvider: firstMultiRecord.isViewProvider,
|
||||
index: records.map(record => record.index as number),
|
||||
});
|
||||
}
|
||||
}
|
||||
const providerRecords = getInjectorProviders(injector);
|
||||
|
||||
messageBus.emit('latestInjectorProviders', [injector, serializedProviderRecords]);
|
||||
};
|
||||
console.group(
|
||||
`%c${serializedInjector.name}`,
|
||||
`color: ${
|
||||
serializedInjector.type === 'element' ? '#a7d5a9' : '#f05057'
|
||||
}; font-size: 1.25rem; font-weight: bold;`,
|
||||
);
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log('injector: ', injector);
|
||||
|
||||
const logProvider =
|
||||
(serializedInjector: SerializedInjector, serializedProvider: SerializedProviderRecord):
|
||||
void => {
|
||||
if (!idToInjector.has(serializedInjector.id)) {
|
||||
return;
|
||||
}
|
||||
if (typeof serializedProvider.index === 'number') {
|
||||
const provider = providerRecords[serializedProvider.index];
|
||||
|
||||
const injector = idToInjector.get(serializedInjector.id)!;
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log('provider: ', provider);
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log(`value: `, injector.get(provider.token, null, {optional: true}));
|
||||
} else if (Array.isArray(serializedProvider.index)) {
|
||||
const providers = serializedProvider.index.map((index) => providerRecords[index]);
|
||||
|
||||
const providerRecords = getInjectorProviders(injector);
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log('providers: ', providers);
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log(`value: `, injector.get(providers[0].token, null, {optional: true}));
|
||||
}
|
||||
|
||||
console.group(
|
||||
`%c${serializedInjector.name}`,
|
||||
`color: ${
|
||||
serializedInjector.type === 'element' ?
|
||||
'#a7d5a9' :
|
||||
'#f05057'}; font-size: 1.25rem; font-weight: bold;`);
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log('injector: ', injector);
|
||||
|
||||
if (typeof serializedProvider.index === 'number') {
|
||||
const provider = providerRecords[serializedProvider.index];
|
||||
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log('provider: ', provider);
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log(`value: `, injector.get(provider.token, null, {optional: true}));
|
||||
} else if (Array.isArray(serializedProvider.index)) {
|
||||
const providers = serializedProvider.index.map(index => providerRecords[index]);
|
||||
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log('providers: ', providers);
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log(`value: `, injector.get(providers[0].token, null, {optional: true}));
|
||||
}
|
||||
|
||||
console.groupEnd();
|
||||
};
|
||||
console.groupEnd();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import {initializeOrGetDirectiveForestHooks} from '../hooks';
|
|||
import {ComponentTreeNode} from '../interfaces';
|
||||
|
||||
interface Type<T> extends Function {
|
||||
new(...args: any[]): T;
|
||||
new (...args: any[]): T;
|
||||
}
|
||||
export interface ComponentInspectorOptions {
|
||||
onComponentEnter: (id: number) => void;
|
||||
|
|
@ -28,11 +28,13 @@ export class ComponentInspector {
|
|||
private readonly _onComponentSelect;
|
||||
private readonly _onComponentLeave;
|
||||
|
||||
constructor(componentOptions: ComponentInspectorOptions = {
|
||||
onComponentEnter: () => {},
|
||||
onComponentLeave: () => {},
|
||||
onComponentSelect: () => {},
|
||||
}) {
|
||||
constructor(
|
||||
componentOptions: ComponentInspectorOptions = {
|
||||
onComponentEnter: () => {},
|
||||
onComponentLeave: () => {},
|
||||
onComponentSelect: () => {},
|
||||
},
|
||||
) {
|
||||
this.bindMethods();
|
||||
this._onComponentEnter = componentOptions.onComponentEnter;
|
||||
this._onComponentSelect = componentOptions.onComponentSelect;
|
||||
|
|
@ -57,7 +59,8 @@ export class ComponentInspector {
|
|||
|
||||
if (this._selectedComponent.component && this._selectedComponent.host) {
|
||||
this._onComponentSelect(
|
||||
initializeOrGetDirectiveForestHooks().getDirectiveId(this._selectedComponent.component)!);
|
||||
initializeOrGetDirectiveForestHooks().getDirectiveId(this._selectedComponent.component)!,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -73,7 +76,8 @@ export class ComponentInspector {
|
|||
if (this._selectedComponent.component && this._selectedComponent.host) {
|
||||
highlight(this._selectedComponent.host);
|
||||
this._onComponentEnter(
|
||||
initializeOrGetDirectiveForestHooks().getDirectiveId(this._selectedComponent.component)!);
|
||||
initializeOrGetDirectiveForestHooks().getDirectiveId(this._selectedComponent.component)!,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -93,7 +97,7 @@ export class ComponentInspector {
|
|||
|
||||
highlightByPosition(position: ElementPosition): void {
|
||||
const forest: ComponentTreeNode[] = initializeOrGetDirectiveForestHooks().getDirectiveForest();
|
||||
const elementToHighlight: HTMLElement|null = findNodeInForest(position, forest);
|
||||
const elementToHighlight: HTMLElement | null = findNodeInForest(position, forest);
|
||||
if (elementToHighlight) {
|
||||
highlight(elementToHighlight);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,11 +5,28 @@
|
|||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {ComponentExplorerViewQuery, DirectiveMetadata, DirectivesProperties, ElementPosition, PropertyQueryTypes, SerializedInjectedService, SerializedInjector, SerializedProviderRecord, UpdatedStateData,} from 'protocol';
|
||||
import {
|
||||
ComponentExplorerViewQuery,
|
||||
DirectiveMetadata,
|
||||
DirectivesProperties,
|
||||
ElementPosition,
|
||||
PropertyQueryTypes,
|
||||
SerializedInjectedService,
|
||||
SerializedInjector,
|
||||
SerializedProviderRecord,
|
||||
UpdatedStateData,
|
||||
} from 'protocol';
|
||||
|
||||
import {buildDirectiveTree, getLViewFromDirectiveOrElementInstance} from './directive-forest/index';
|
||||
import {ngDebugApiIsSupported, ngDebugClient, ngDebugDependencyInjectionApiIsSupported} from './ng-debug-api/ng-debug-api';
|
||||
import {deeplySerializeSelectedProperties, serializeDirectiveState,} from './state-serializer/state-serializer';
|
||||
import {
|
||||
ngDebugApiIsSupported,
|
||||
ngDebugClient,
|
||||
ngDebugDependencyInjectionApiIsSupported,
|
||||
} from './ng-debug-api/ng-debug-api';
|
||||
import {
|
||||
deeplySerializeSelectedProperties,
|
||||
serializeDirectiveState,
|
||||
} from './state-serializer/state-serializer';
|
||||
|
||||
// Need to be kept in sync with Angular framework
|
||||
// We can't directly import it from framework now
|
||||
|
|
@ -22,10 +39,21 @@ enum ChangeDetectionStrategy {
|
|||
|
||||
import {ComponentTreeNode, DirectiveInstanceType, ComponentInstanceType} from './interfaces';
|
||||
|
||||
import type {ClassProvider, ExistingProvider, FactoryProvider, InjectOptions, InjectionToken, Injector, Type, ValueProvider, ɵComponentDebugMetadata as ComponentDebugMetadata, ɵProviderRecord as ProviderRecord} from '@angular/core';
|
||||
import type {
|
||||
ClassProvider,
|
||||
ExistingProvider,
|
||||
FactoryProvider,
|
||||
InjectOptions,
|
||||
InjectionToken,
|
||||
Injector,
|
||||
Type,
|
||||
ValueProvider,
|
||||
ɵComponentDebugMetadata as ComponentDebugMetadata,
|
||||
ɵProviderRecord as ProviderRecord,
|
||||
} from '@angular/core';
|
||||
import {isSignal} from './utils';
|
||||
|
||||
export const injectorToId = new WeakMap<Injector|HTMLElement, string>();
|
||||
export const injectorToId = new WeakMap<Injector | HTMLElement, string>();
|
||||
export const nodeInjectorToResolutionPath = new WeakMap<HTMLElement, SerializedInjector[]>();
|
||||
export const idToInjector = new Map<string, Injector>();
|
||||
export const injectorsSeen = new Set<string>();
|
||||
|
|
@ -47,12 +75,14 @@ export function getInjectorResolutionPath(injector: Injector): Injector[] {
|
|||
return ngDebugClient().ɵgetInjectorResolutionPath(injector);
|
||||
}
|
||||
|
||||
export function getInjectorFromElementNode(element: Node): Injector|null {
|
||||
export function getInjectorFromElementNode(element: Node): Injector | null {
|
||||
return ngDebugClient().getInjector(element);
|
||||
}
|
||||
|
||||
function getDirectivesFromElement(element: HTMLElement):
|
||||
{component: unknown|null; directives: unknown[];} {
|
||||
function getDirectivesFromElement(element: HTMLElement): {
|
||||
component: unknown | null;
|
||||
directives: unknown[];
|
||||
} {
|
||||
let component = null;
|
||||
if (element instanceof Element) {
|
||||
component = ngDebugClient().getComponent(element);
|
||||
|
|
@ -65,9 +95,9 @@ function getDirectivesFromElement(element: HTMLElement):
|
|||
}
|
||||
|
||||
export const getLatestComponentState = (
|
||||
query: ComponentExplorerViewQuery,
|
||||
directiveForest?: ComponentTreeNode[],
|
||||
): {directiveProperties: DirectivesProperties}|undefined => {
|
||||
query: ComponentExplorerViewQuery,
|
||||
directiveForest?: ComponentTreeNode[],
|
||||
): {directiveProperties: DirectivesProperties} | undefined => {
|
||||
// if a directive forest is passed in we don't have to build the forest again.
|
||||
directiveForest = directiveForest ?? buildDirectiveForest();
|
||||
|
||||
|
|
@ -81,19 +111,19 @@ export const getLatestComponentState = (
|
|||
const injector = ngDebugClient().getInjector(node.nativeElement!);
|
||||
|
||||
const injectors = getInjectorResolutionPath(injector);
|
||||
const resolutionPathWithProviders = !ngDebugDependencyInjectionApiIsSupported() ?
|
||||
[] :
|
||||
injectors.map((injector) => ({
|
||||
injector,
|
||||
providers: getInjectorProviders(injector),
|
||||
}));
|
||||
const populateResultSet = (dir: DirectiveInstanceType|ComponentInstanceType) => {
|
||||
const resolutionPathWithProviders = !ngDebugDependencyInjectionApiIsSupported()
|
||||
? []
|
||||
: injectors.map((injector) => ({
|
||||
injector,
|
||||
providers: getInjectorProviders(injector),
|
||||
}));
|
||||
const populateResultSet = (dir: DirectiveInstanceType | ComponentInstanceType) => {
|
||||
const {instance, name} = dir;
|
||||
const metadata = getDirectiveMetadata(instance);
|
||||
metadata.dependencies = getDependenciesForDirective(
|
||||
injector,
|
||||
resolutionPathWithProviders,
|
||||
instance.constructor,
|
||||
injector,
|
||||
resolutionPathWithProviders,
|
||||
instance.constructor,
|
||||
);
|
||||
|
||||
if (query.propertyQuery.type === PropertyQueryTypes.All) {
|
||||
|
|
@ -106,9 +136,9 @@ export const getLatestComponentState = (
|
|||
if (query.propertyQuery.type === PropertyQueryTypes.Specified) {
|
||||
directiveProperties[name] = {
|
||||
props: deeplySerializeSelectedProperties(
|
||||
instance,
|
||||
query.propertyQuery.properties[name] || [],
|
||||
),
|
||||
instance,
|
||||
query.propertyQuery.properties[name] || [],
|
||||
),
|
||||
metadata,
|
||||
};
|
||||
}
|
||||
|
|
@ -124,7 +154,7 @@ export const getLatestComponentState = (
|
|||
};
|
||||
};
|
||||
|
||||
function serializeElementInjectorWithId(injector: Injector): SerializedInjector|null {
|
||||
function serializeElementInjectorWithId(injector: Injector): SerializedInjector | null {
|
||||
let id: string;
|
||||
const element = getElementInjectorElement(injector);
|
||||
|
||||
|
|
@ -146,7 +176,7 @@ function serializeElementInjectorWithId(injector: Injector): SerializedInjector|
|
|||
return {id, ...serializedInjector};
|
||||
}
|
||||
|
||||
function serializeInjectorWithId(injector: Injector): SerializedInjector|null {
|
||||
function serializeInjectorWithId(injector: Injector): SerializedInjector | null {
|
||||
if (isElementInjector(injector)) {
|
||||
return serializeElementInjectorWithId(injector);
|
||||
} else {
|
||||
|
|
@ -154,7 +184,7 @@ function serializeInjectorWithId(injector: Injector): SerializedInjector|null {
|
|||
}
|
||||
}
|
||||
|
||||
function serializeEnvironmentInjectorWithId(injector: Injector): SerializedInjector|null {
|
||||
function serializeEnvironmentInjectorWithId(injector: Injector): SerializedInjector | null {
|
||||
let id: string;
|
||||
|
||||
if (!injectorToId.has(injector)) {
|
||||
|
|
@ -224,16 +254,16 @@ export function getInjectorProviders(injector: Injector) {
|
|||
}
|
||||
|
||||
const getDependenciesForDirective = (
|
||||
injector: Injector,
|
||||
resolutionPath: {injector: Injector; providers: ProviderRecord[]}[],
|
||||
directive: any,
|
||||
): SerializedInjectedService[] => {
|
||||
injector: Injector,
|
||||
resolutionPath: {injector: Injector; providers: ProviderRecord[]}[],
|
||||
directive: any,
|
||||
): SerializedInjectedService[] => {
|
||||
if (!ngDebugApiIsSupported('ɵgetDependenciesFromInjectable')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let dependencies =
|
||||
ngDebugClient().ɵgetDependenciesFromInjectable(injector, directive)?.dependencies ?? [];
|
||||
ngDebugClient().ɵgetDependenciesFromInjectable(injector, directive)?.dependencies ?? [];
|
||||
const serializedInjectedServices: SerializedInjectedService[] = [];
|
||||
|
||||
let position = 0;
|
||||
|
|
@ -256,8 +286,9 @@ const getDependenciesForDirective = (
|
|||
// dependency (2)
|
||||
const dependencyResolutionPath: SerializedInjector[] = [
|
||||
// (1)
|
||||
...resolutionPath.slice(0, foundInjectorIndex + 1)
|
||||
.map((node) => serializeInjectorWithId(node.injector)!),
|
||||
...resolutionPath
|
||||
.slice(0, foundInjectorIndex + 1)
|
||||
.map((node) => serializeInjectorWithId(node.injector)!),
|
||||
|
||||
// (2)
|
||||
// We slice the import path to remove the first element because this is the same
|
||||
|
|
@ -314,7 +345,7 @@ function stripUnderscore(str: string): string {
|
|||
return str;
|
||||
}
|
||||
|
||||
export function serializeInjector(injector: Injector): Omit<SerializedInjector, 'id'>|null {
|
||||
export function serializeInjector(injector: Injector): Omit<SerializedInjector, 'id'> | null {
|
||||
const metadata = getInjectorMetadata(injector);
|
||||
|
||||
if (metadata === null) {
|
||||
|
|
@ -354,11 +385,11 @@ export function serializeInjector(injector: Injector): Omit<SerializedInjector,
|
|||
}
|
||||
|
||||
export function serializeProviderRecord(
|
||||
providerRecord: ProviderRecord,
|
||||
index: number,
|
||||
hasImportPath = false,
|
||||
): SerializedProviderRecord {
|
||||
let type: 'type'|'class'|'value'|'factory'|'existing' = 'type';
|
||||
providerRecord: ProviderRecord,
|
||||
index: number,
|
||||
hasImportPath = false,
|
||||
): SerializedProviderRecord {
|
||||
let type: 'type' | 'class' | 'value' | 'factory' | 'existing' = 'type';
|
||||
let multi = false;
|
||||
|
||||
if (typeof providerRecord.provider === 'object') {
|
||||
|
|
@ -378,7 +409,11 @@ export function serializeProviderRecord(
|
|||
}
|
||||
|
||||
const serializedProvider: {
|
||||
token: string; type: typeof type; multi: boolean; isViewProvider: boolean; index: number;
|
||||
token: string;
|
||||
type: typeof type;
|
||||
multi: boolean;
|
||||
isViewProvider: boolean;
|
||||
index: number;
|
||||
importPath?: string[];
|
||||
} = {
|
||||
token: valueToLabel(providerRecord.token),
|
||||
|
|
@ -389,10 +424,9 @@ export function serializeProviderRecord(
|
|||
};
|
||||
|
||||
if (hasImportPath) {
|
||||
serializedProvider['importPath'] = (providerRecord.importPath ?? [])
|
||||
.map(
|
||||
(injector) => valueToLabel(injector),
|
||||
);
|
||||
serializedProvider['importPath'] = (providerRecord.importPath ?? []).map((injector) =>
|
||||
valueToLabel(injector),
|
||||
);
|
||||
}
|
||||
|
||||
return serializedProvider;
|
||||
|
|
@ -401,8 +435,8 @@ export function serializeProviderRecord(
|
|||
function elementToDirectiveNames(element: HTMLElement): string[] {
|
||||
const {component, directives} = getDirectivesFromElement(element);
|
||||
return [component, ...directives]
|
||||
.map((dir) => dir?.constructor?.name ?? '')
|
||||
.filter((dir) => !!dir);
|
||||
.map((dir) => dir?.constructor?.name ?? '')
|
||||
.filter((dir) => !!dir);
|
||||
}
|
||||
|
||||
export function getElementInjectorElement(elementInjector: Injector): HTMLElement {
|
||||
|
|
@ -413,7 +447,7 @@ export function getElementInjectorElement(elementInjector: Injector): HTMLElemen
|
|||
return getInjectorMetadata(elementInjector)!.source as HTMLElement;
|
||||
}
|
||||
|
||||
function isInjectionToken(token: Type<unknown>|InjectionToken<unknown>): boolean {
|
||||
function isInjectionToken(token: Type<unknown> | InjectionToken<unknown>): boolean {
|
||||
return token.constructor.name === 'InjectionToken';
|
||||
}
|
||||
|
||||
|
|
@ -447,7 +481,7 @@ const getRoots = () => {
|
|||
const roots = Array.from(document.documentElement.querySelectorAll('[ng-version]'));
|
||||
|
||||
const isTopLevel = (element: Element) => {
|
||||
let parent: Element|null = element;
|
||||
let parent: Element | null = element;
|
||||
|
||||
while (parent?.parentElement) {
|
||||
parent = parent.parentElement;
|
||||
|
|
@ -470,13 +504,13 @@ export const buildDirectiveForest = (): ComponentTreeNode[] => {
|
|||
// Based on an ElementID we return a specific component node.
|
||||
// If we can't find any, we return null.
|
||||
export const queryDirectiveForest = (
|
||||
position: ElementPosition,
|
||||
forest: ComponentTreeNode[],
|
||||
): ComponentTreeNode|null => {
|
||||
position: ElementPosition,
|
||||
forest: ComponentTreeNode[],
|
||||
): ComponentTreeNode | null => {
|
||||
if (!position.length) {
|
||||
return null;
|
||||
}
|
||||
let node: null|ComponentTreeNode = null;
|
||||
let node: null | ComponentTreeNode = null;
|
||||
for (const i of position) {
|
||||
node = forest[i];
|
||||
if (!node) {
|
||||
|
|
@ -488,16 +522,16 @@ export const queryDirectiveForest = (
|
|||
};
|
||||
|
||||
export const findNodeInForest = (
|
||||
position: ElementPosition,
|
||||
forest: ComponentTreeNode[],
|
||||
): HTMLElement|null => {
|
||||
const foundComponent: ComponentTreeNode|null = queryDirectiveForest(position, forest);
|
||||
position: ElementPosition,
|
||||
forest: ComponentTreeNode[],
|
||||
): HTMLElement | null => {
|
||||
const foundComponent: ComponentTreeNode | null = queryDirectiveForest(position, forest);
|
||||
return foundComponent ? (foundComponent.nativeElement as HTMLElement) : null;
|
||||
};
|
||||
|
||||
export const findNodeFromSerializedPosition = (
|
||||
serializedPosition: string,
|
||||
): ComponentTreeNode|null => {
|
||||
serializedPosition: string,
|
||||
): ComponentTreeNode | null => {
|
||||
const position: number[] = serializedPosition.split(',').map((index) => parseInt(index, 10));
|
||||
return queryDirectiveForest(position, buildDirectiveForest());
|
||||
};
|
||||
|
|
@ -507,9 +541,9 @@ export const updateState = (updatedStateData: UpdatedStateData): void => {
|
|||
const node = queryDirectiveForest(updatedStateData.directiveId.element, buildDirectiveForest());
|
||||
if (!node) {
|
||||
console.warn(
|
||||
'Could not update the state of component',
|
||||
updatedStateData,
|
||||
'because the component was not found',
|
||||
'Could not update the state of component',
|
||||
updatedStateData,
|
||||
'because the component was not found',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -551,15 +585,14 @@ const mutateComponentOrDirective = (updatedStateData: UpdatedStateData, compOrDi
|
|||
} else {
|
||||
parentObjectOfValueToUpdate[valueKey] = updatedStateData.newValue;
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
export function serializeResolutionPath(resolutionPath: Injector[]): SerializedInjector[] {
|
||||
const serializedResolutionPath: SerializedInjector[] = [];
|
||||
|
||||
for (const injector of resolutionPath) {
|
||||
let serializedInjectorWithId: SerializedInjector|null = null;
|
||||
let serializedInjectorWithId: SerializedInjector | null = null;
|
||||
|
||||
if (isElementInjector(injector)) {
|
||||
serializedInjectorWithId = serializeElementInjectorWithId(injector);
|
||||
|
|
|
|||
|
|
@ -9,12 +9,16 @@
|
|||
import {LTreeStrategy} from './ltree';
|
||||
import {RTreeStrategy} from './render-tree';
|
||||
|
||||
export {getDirectiveHostElement, getLViewFromDirectiveOrElementInstance, METADATA_PROPERTY_NAME} from './ltree';
|
||||
export {
|
||||
getDirectiveHostElement,
|
||||
getLViewFromDirectiveOrElementInstance,
|
||||
METADATA_PROPERTY_NAME,
|
||||
} from './ltree';
|
||||
|
||||
// The order of the strategies matters. Lower indices have higher priority.
|
||||
const strategies = [new RTreeStrategy(), new LTreeStrategy()];
|
||||
|
||||
let strategy: null|RTreeStrategy|LTreeStrategy = null;
|
||||
let strategy: null | RTreeStrategy | LTreeStrategy = null;
|
||||
|
||||
const selectStrategy = (element: Element) => {
|
||||
for (const s of strategies) {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ const TYPE = 1;
|
|||
const ELEMENT = 0;
|
||||
const LVIEW_TVIEW = 1;
|
||||
|
||||
|
||||
// Big oversimplification of the LView structure.
|
||||
type LView = Array<any>;
|
||||
|
||||
|
|
@ -41,7 +40,7 @@ const isLView = (value: unknown): value is LView => {
|
|||
};
|
||||
|
||||
export const METADATA_PROPERTY_NAME = '__ngContext__';
|
||||
export function getLViewFromDirectiveOrElementInstance(dir: any): null|LView {
|
||||
export function getLViewFromDirectiveOrElementInstance(dir: any): null | LView {
|
||||
if (!dir) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -80,7 +79,7 @@ export class LTreeStrategy {
|
|||
|
||||
private _getNode(lView: LView, data: any, idx: number): ComponentTreeNode {
|
||||
const directives: DirectiveInstanceType[] = [];
|
||||
let component: ComponentInstanceType|null = null;
|
||||
let component: ComponentInstanceType | null = null;
|
||||
const tNode = data[idx];
|
||||
const node = lView[idx][ELEMENT];
|
||||
const element = (node.tagName || node.nodeName).toLowerCase();
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ describe('render tree extraction', () => {
|
|||
componentMap = new Map();
|
||||
|
||||
(window as any).ng = {
|
||||
getDirectiveMetadata(): void{},
|
||||
getDirectiveMetadata(): void {},
|
||||
getComponent(element: Element): any {
|
||||
return componentMap.get(element);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -10,56 +10,60 @@ import {ComponentTreeNode} from '../interfaces';
|
|||
import {ngDebugClient} from '../ng-debug-api/ng-debug-api';
|
||||
import {isCustomElement} from '../utils';
|
||||
|
||||
const extractViewTree =
|
||||
(domNode: Node|Element, result: ComponentTreeNode[],
|
||||
getComponent: (element: Element) => {} | null,
|
||||
getDirectives: (node: Node) => {}[]): ComponentTreeNode[] => {
|
||||
const directives = getDirectives(domNode);
|
||||
if (!directives.length && !(domNode instanceof Element)) {
|
||||
return result;
|
||||
}
|
||||
const componentTreeNode: ComponentTreeNode = {
|
||||
children: [],
|
||||
component: null,
|
||||
directives: directives.map((dir) => {
|
||||
return {
|
||||
instance: dir,
|
||||
name: dir.constructor.name,
|
||||
};
|
||||
}),
|
||||
element: domNode.nodeName.toLowerCase(),
|
||||
nativeElement: domNode,
|
||||
const extractViewTree = (
|
||||
domNode: Node | Element,
|
||||
result: ComponentTreeNode[],
|
||||
getComponent: (element: Element) => {} | null,
|
||||
getDirectives: (node: Node) => {}[],
|
||||
): ComponentTreeNode[] => {
|
||||
const directives = getDirectives(domNode);
|
||||
if (!directives.length && !(domNode instanceof Element)) {
|
||||
return result;
|
||||
}
|
||||
const componentTreeNode: ComponentTreeNode = {
|
||||
children: [],
|
||||
component: null,
|
||||
directives: directives.map((dir) => {
|
||||
return {
|
||||
instance: dir,
|
||||
name: dir.constructor.name,
|
||||
};
|
||||
if (!(domNode instanceof Element)) {
|
||||
result.push(componentTreeNode);
|
||||
return result;
|
||||
}
|
||||
const component = getComponent(domNode);
|
||||
if (component) {
|
||||
componentTreeNode.component = {
|
||||
instance: component,
|
||||
isElement: isCustomElement(domNode),
|
||||
name: domNode.nodeName.toLowerCase(),
|
||||
};
|
||||
}
|
||||
if (component || componentTreeNode.directives.length) {
|
||||
result.push(componentTreeNode);
|
||||
}
|
||||
if (componentTreeNode.component || componentTreeNode.directives.length) {
|
||||
domNode.childNodes.forEach(
|
||||
(node) =>
|
||||
extractViewTree(node, componentTreeNode.children, getComponent, getDirectives));
|
||||
} else {
|
||||
domNode.childNodes.forEach(
|
||||
(node) => extractViewTree(node, result, getComponent, getDirectives));
|
||||
}
|
||||
return result;
|
||||
}),
|
||||
element: domNode.nodeName.toLowerCase(),
|
||||
nativeElement: domNode,
|
||||
};
|
||||
if (!(domNode instanceof Element)) {
|
||||
result.push(componentTreeNode);
|
||||
return result;
|
||||
}
|
||||
const component = getComponent(domNode);
|
||||
if (component) {
|
||||
componentTreeNode.component = {
|
||||
instance: component,
|
||||
isElement: isCustomElement(domNode),
|
||||
name: domNode.nodeName.toLowerCase(),
|
||||
};
|
||||
}
|
||||
if (component || componentTreeNode.directives.length) {
|
||||
result.push(componentTreeNode);
|
||||
}
|
||||
if (componentTreeNode.component || componentTreeNode.directives.length) {
|
||||
domNode.childNodes.forEach((node) =>
|
||||
extractViewTree(node, componentTreeNode.children, getComponent, getDirectives),
|
||||
);
|
||||
} else {
|
||||
domNode.childNodes.forEach((node) =>
|
||||
extractViewTree(node, result, getComponent, getDirectives),
|
||||
);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export class RTreeStrategy {
|
||||
supports(): boolean {
|
||||
return (['getDirectiveMetadata', 'getComponent', 'getDirectives'] as const)
|
||||
.every((method) => typeof ngDebugClient()[method] === 'function');
|
||||
return (['getDirectiveMetadata', 'getComponent', 'getDirectives'] as const).every(
|
||||
(method) => typeof ngDebugClient()[method] === 'function',
|
||||
);
|
||||
}
|
||||
|
||||
build(element: Element): ComponentTreeNode[] {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ let overlayContent: HTMLElement;
|
|||
declare const ng: any;
|
||||
|
||||
interface Type<T> extends Function {
|
||||
new(...args: any[]): T;
|
||||
new (...args: any[]): T;
|
||||
}
|
||||
|
||||
const DEV_TOOLS_HIGHLIGHT_NODE_ID = '____ngDevToolsHighlight';
|
||||
|
|
@ -40,26 +40,27 @@ function init(): void {
|
|||
overlay.appendChild(overlayContent);
|
||||
}
|
||||
|
||||
export const findComponentAndHost =
|
||||
(el: Node|undefined): {component: any; host: HTMLElement | null} => {
|
||||
if (!el) {
|
||||
return {component: null, host: null};
|
||||
}
|
||||
while (el) {
|
||||
const component = el instanceof HTMLElement && ng.getComponent(el);
|
||||
if (component) {
|
||||
return {component, host: el as HTMLElement};
|
||||
}
|
||||
if (!el.parentElement) {
|
||||
break;
|
||||
}
|
||||
el = el.parentElement;
|
||||
}
|
||||
return {component: null, host: null};
|
||||
};
|
||||
export const findComponentAndHost = (
|
||||
el: Node | undefined,
|
||||
): {component: any; host: HTMLElement | null} => {
|
||||
if (!el) {
|
||||
return {component: null, host: null};
|
||||
}
|
||||
while (el) {
|
||||
const component = el instanceof HTMLElement && ng.getComponent(el);
|
||||
if (component) {
|
||||
return {component, host: el as HTMLElement};
|
||||
}
|
||||
if (!el.parentElement) {
|
||||
break;
|
||||
}
|
||||
el = el.parentElement;
|
||||
}
|
||||
return {component: null, host: null};
|
||||
};
|
||||
|
||||
// Todo(aleksanderbodurri): this should not be part of the highlighter, move this somewhere else
|
||||
export function getDirectiveName(dir: Type<unknown>|undefined|null): string {
|
||||
export function getDirectiveName(dir: Type<unknown> | undefined | null): string {
|
||||
return dir ? dir.constructor.name : 'unknown';
|
||||
}
|
||||
|
||||
|
|
@ -97,11 +98,12 @@ export function inDoc(node: any): boolean {
|
|||
}
|
||||
const doc = node.ownerDocument.documentElement;
|
||||
const parent = node.parentNode;
|
||||
return doc === node || doc === parent ||
|
||||
!!(parent && parent.nodeType === 1 && doc.contains(parent));
|
||||
return (
|
||||
doc === node || doc === parent || !!(parent && parent.nodeType === 1 && doc.contains(parent))
|
||||
);
|
||||
}
|
||||
|
||||
function getComponentRect(el: Node): DOMRect|undefined {
|
||||
function getComponentRect(el: Node): DOMRect | undefined {
|
||||
if (!(el instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -137,7 +139,7 @@ function positionOverlayContent(dimensions: DOMRect) {
|
|||
|
||||
// Attempt to position the content element so that it's always in the
|
||||
// viewport along the Y axis. Prefer to position on the bottom.
|
||||
if ((dimensions.bottom + yOffset) <= viewportHeight) {
|
||||
if (dimensions.bottom + yOffset <= viewportHeight) {
|
||||
style.bottom = yOffsetValue;
|
||||
// If it doesn't fit on the bottom, try to position on top.
|
||||
} else if (dimensions.top - yOffset >= 0) {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,13 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {DirectiveProfile, ElementPosition, ElementProfile, LifecycleProfile, ProfilerFrame} from 'protocol';
|
||||
import {
|
||||
DirectiveProfile,
|
||||
ElementPosition,
|
||||
ElementProfile,
|
||||
LifecycleProfile,
|
||||
ProfilerFrame,
|
||||
} from 'protocol';
|
||||
|
||||
import {getDirectiveName} from '../highlighter';
|
||||
import {ComponentTreeNode} from '../interfaces';
|
||||
|
|
@ -59,16 +65,20 @@ const getHooks = (onFrame: (frame: ProfilerFrame) => void): Partial<Hooks> => {
|
|||
// We flush here because it's possible the current node to overwrite
|
||||
// an existing removed node.
|
||||
onCreate(
|
||||
directive: any, node: Node, _: number, isComponent: boolean, position: ElementPosition):
|
||||
void {
|
||||
eventMap.set(directive, {
|
||||
isElement: isCustomElement(node),
|
||||
name: getDirectiveName(directive),
|
||||
isComponent,
|
||||
lifecycle: {},
|
||||
outputs: {},
|
||||
});
|
||||
},
|
||||
directive: any,
|
||||
node: Node,
|
||||
_: number,
|
||||
isComponent: boolean,
|
||||
position: ElementPosition,
|
||||
): void {
|
||||
eventMap.set(directive, {
|
||||
isElement: isCustomElement(node),
|
||||
name: getDirectiveName(directive),
|
||||
isComponent,
|
||||
lifecycle: {},
|
||||
outputs: {},
|
||||
});
|
||||
},
|
||||
onChangeDetectionStart(component: any, node: Node): void {
|
||||
startEvent(timeStartMap, component, 'changeDetection');
|
||||
if (!inChangeDetection) {
|
||||
|
|
@ -111,22 +121,31 @@ const getHooks = (onFrame: (frame: ProfilerFrame) => void): Partial<Hooks> => {
|
|||
console.warn('Could not find profile for', component);
|
||||
}
|
||||
},
|
||||
onDestroy(directive: any, node: Node, _: number, isComponent: boolean, __: ElementPosition):
|
||||
void {
|
||||
// Make sure we reflect such directives in the report.
|
||||
if (!eventMap.has(directive)) {
|
||||
eventMap.set(directive, {
|
||||
isElement: isComponent && isCustomElement(node),
|
||||
name: getDirectiveName(directive),
|
||||
isComponent,
|
||||
lifecycle: {},
|
||||
outputs: {},
|
||||
});
|
||||
}
|
||||
},
|
||||
onDestroy(
|
||||
directive: any,
|
||||
node: Node,
|
||||
_: number,
|
||||
isComponent: boolean,
|
||||
__: ElementPosition,
|
||||
): void {
|
||||
// Make sure we reflect such directives in the report.
|
||||
if (!eventMap.has(directive)) {
|
||||
eventMap.set(directive, {
|
||||
isElement: isComponent && isCustomElement(node),
|
||||
name: getDirectiveName(directive),
|
||||
isComponent,
|
||||
lifecycle: {},
|
||||
outputs: {},
|
||||
});
|
||||
}
|
||||
},
|
||||
onLifecycleHookStart(
|
||||
directive: any, hookName: keyof LifecycleProfile, node: Node, __: number,
|
||||
isComponent: boolean): void {
|
||||
directive: any,
|
||||
hookName: keyof LifecycleProfile,
|
||||
node: Node,
|
||||
__: number,
|
||||
isComponent: boolean,
|
||||
): void {
|
||||
startEvent(timeStartMap, directive, hookName);
|
||||
if (!eventMap.has(directive)) {
|
||||
eventMap.set(directive, {
|
||||
|
|
@ -139,7 +158,12 @@ const getHooks = (onFrame: (frame: ProfilerFrame) => void): Partial<Hooks> => {
|
|||
}
|
||||
},
|
||||
onLifecycleHookEnd(
|
||||
directive: any, hookName: keyof LifecycleProfile, _: Node, __: number, ___: boolean): void {
|
||||
directive: any,
|
||||
hookName: keyof LifecycleProfile,
|
||||
_: Node,
|
||||
__: number,
|
||||
___: boolean,
|
||||
): void {
|
||||
const dir = eventMap.get(directive);
|
||||
const startTimestamp = getEventStart(timeStartMap, directive, hookName);
|
||||
if (startTimestamp === undefined) {
|
||||
|
|
@ -153,19 +177,23 @@ const getHooks = (onFrame: (frame: ProfilerFrame) => void): Partial<Hooks> => {
|
|||
dir.lifecycle[hookName] = (dir.lifecycle[hookName] || 0) + duration;
|
||||
frameDuration += duration;
|
||||
},
|
||||
onOutputStart(componentOrDirective: any, outputName: string, node: Node, isComponent: boolean):
|
||||
void {
|
||||
startEvent(timeStartMap, componentOrDirective, outputName);
|
||||
if (!eventMap.has(componentOrDirective)) {
|
||||
eventMap.set(componentOrDirective, {
|
||||
isElement: isCustomElement(node),
|
||||
name: getDirectiveName(componentOrDirective),
|
||||
isComponent,
|
||||
lifecycle: {},
|
||||
outputs: {},
|
||||
});
|
||||
}
|
||||
},
|
||||
onOutputStart(
|
||||
componentOrDirective: any,
|
||||
outputName: string,
|
||||
node: Node,
|
||||
isComponent: boolean,
|
||||
): void {
|
||||
startEvent(timeStartMap, componentOrDirective, outputName);
|
||||
if (!eventMap.has(componentOrDirective)) {
|
||||
eventMap.set(componentOrDirective, {
|
||||
isElement: isCustomElement(node),
|
||||
name: getDirectiveName(componentOrDirective),
|
||||
isComponent,
|
||||
lifecycle: {},
|
||||
outputs: {},
|
||||
});
|
||||
}
|
||||
},
|
||||
onOutputEnd(componentOrDirective: any, outputName: string): void {
|
||||
const name = outputName;
|
||||
const entry = eventMap.get(componentOrDirective);
|
||||
|
|
@ -175,8 +203,10 @@ const getHooks = (onFrame: (frame: ProfilerFrame) => void): Partial<Hooks> => {
|
|||
}
|
||||
if (!entry) {
|
||||
console.warn(
|
||||
'Could not find directive or component in onOutputEnd callback', componentOrDirective,
|
||||
outputName);
|
||||
'Could not find directive or component in onOutputEnd callback',
|
||||
componentOrDirective,
|
||||
outputName,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const duration = performance.now() - startTimestamp;
|
||||
|
|
@ -215,33 +245,36 @@ const insertOrMerge = (lastFrame: ElementProfile, profile: DirectiveProfile) =>
|
|||
}
|
||||
};
|
||||
|
||||
const insertElementProfile =
|
||||
(frames: ElementProfile[], position: ElementPosition, profile?: DirectiveProfile) => {
|
||||
if (!profile) {
|
||||
return;
|
||||
}
|
||||
const original = frames;
|
||||
for (let i = 0; i < position.length - 1; i++) {
|
||||
const pos = position[i];
|
||||
if (!frames[pos]) {
|
||||
// TODO(mgechev): consider how to ensure we don't hit this case
|
||||
console.warn('Unable to find parent node for', profile, original);
|
||||
return;
|
||||
}
|
||||
frames = frames[pos].children;
|
||||
}
|
||||
const lastIdx = position[position.length - 1];
|
||||
let lastFrame: ElementProfile = {
|
||||
children: [],
|
||||
directives: [],
|
||||
};
|
||||
if (frames[lastIdx]) {
|
||||
lastFrame = frames[lastIdx];
|
||||
} else {
|
||||
frames[lastIdx] = lastFrame;
|
||||
}
|
||||
insertOrMerge(lastFrame, profile);
|
||||
};
|
||||
const insertElementProfile = (
|
||||
frames: ElementProfile[],
|
||||
position: ElementPosition,
|
||||
profile?: DirectiveProfile,
|
||||
) => {
|
||||
if (!profile) {
|
||||
return;
|
||||
}
|
||||
const original = frames;
|
||||
for (let i = 0; i < position.length - 1; i++) {
|
||||
const pos = position[i];
|
||||
if (!frames[pos]) {
|
||||
// TODO(mgechev): consider how to ensure we don't hit this case
|
||||
console.warn('Unable to find parent node for', profile, original);
|
||||
return;
|
||||
}
|
||||
frames = frames[pos].children;
|
||||
}
|
||||
const lastIdx = position[position.length - 1];
|
||||
let lastFrame: ElementProfile = {
|
||||
children: [],
|
||||
directives: [],
|
||||
};
|
||||
if (frames[lastIdx]) {
|
||||
lastFrame = frames[lastIdx];
|
||||
} else {
|
||||
frames[lastIdx] = lastFrame;
|
||||
}
|
||||
insertOrMerge(lastFrame, profile);
|
||||
};
|
||||
|
||||
const prepareInitialFrame = (source: string, duration: number) => {
|
||||
const frame: ProfilerFrame = {
|
||||
|
|
@ -252,7 +285,7 @@ const prepareInitialFrame = (source: string, duration: number) => {
|
|||
const directiveForestHooks = initializeOrGetDirectiveForestHooks();
|
||||
const directiveForest = directiveForestHooks.getIndexedDirectiveForest();
|
||||
const traverse = (node: ComponentTreeNode, children = frame.directives) => {
|
||||
let position: ElementPosition|undefined;
|
||||
let position: ElementPosition | undefined;
|
||||
if (node.component) {
|
||||
position = directiveForestHooks.getDirectivePosition(node.component.instance);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export class DirectiveForestHooks {
|
|||
|
||||
profiler: Profiler = selectProfilerStrategy();
|
||||
|
||||
getDirectivePosition(dir: any): ElementPosition|undefined {
|
||||
getDirectivePosition(dir: any): ElementPosition | undefined {
|
||||
const result = this._tracker.getDirectivePosition(dir);
|
||||
if (result === undefined) {
|
||||
console.warn('Unable to find position of', dir);
|
||||
|
|
@ -36,7 +36,7 @@ export class DirectiveForestHooks {
|
|||
return result;
|
||||
}
|
||||
|
||||
getDirectiveId(dir: any): number|undefined {
|
||||
getDirectiveId(dir: any): number | undefined {
|
||||
const result = this._tracker.getDirectiveId(dir);
|
||||
if (result === undefined) {
|
||||
console.warn('Unable to find ID of', result);
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {buildDirectiveForest} from '../component-tree';
|
|||
import {ComponentInstanceType, ComponentTreeNode, DirectiveInstanceType} from '../interfaces';
|
||||
|
||||
export declare interface Type<T> extends Function {
|
||||
new(...args: any[]): T;
|
||||
new (...args: any[]): T;
|
||||
}
|
||||
|
||||
interface TreeNode {
|
||||
|
|
@ -22,7 +22,8 @@ interface TreeNode {
|
|||
}
|
||||
|
||||
export type NodeArray = {
|
||||
directive: any; isComponent: boolean;
|
||||
directive: any;
|
||||
isComponent: boolean;
|
||||
}[];
|
||||
|
||||
export class IdentityTracker {
|
||||
|
|
@ -43,11 +44,11 @@ export class IdentityTracker {
|
|||
return IdentityTracker._instance;
|
||||
}
|
||||
|
||||
getDirectivePosition(dir: any): ElementPosition|undefined {
|
||||
getDirectivePosition(dir: any): ElementPosition | undefined {
|
||||
return this._currentDirectivePosition.get(dir);
|
||||
}
|
||||
|
||||
getDirectiveId(dir: any): number|undefined {
|
||||
getDirectiveId(dir: any): number | undefined {
|
||||
return this._currentDirectiveId.get(dir);
|
||||
}
|
||||
|
||||
|
|
@ -56,7 +57,9 @@ export class IdentityTracker {
|
|||
}
|
||||
|
||||
index(): {
|
||||
newNodes: NodeArray; removedNodes: NodeArray; indexedForest: IndexedNode[];
|
||||
newNodes: NodeArray;
|
||||
removedNodes: NodeArray;
|
||||
indexedForest: IndexedNode[];
|
||||
directiveForest: ComponentTreeNode[];
|
||||
} {
|
||||
const directiveForest = buildDirectiveForest();
|
||||
|
|
@ -78,8 +81,11 @@ export class IdentityTracker {
|
|||
}
|
||||
|
||||
private _index(
|
||||
node: IndexedNode, parent: TreeNode|null, newNodes: {directive: any; isComponent: boolean}[],
|
||||
allNodes: Set<any>): void {
|
||||
node: IndexedNode,
|
||||
parent: TreeNode | null,
|
||||
newNodes: {directive: any; isComponent: boolean}[],
|
||||
allNodes: Set<any>,
|
||||
): void {
|
||||
if (node.component) {
|
||||
allNodes.add(node.component.instance);
|
||||
this.isComponent.set(node.component.instance, true);
|
||||
|
|
@ -113,7 +119,10 @@ export interface IndexedNode extends DevToolsNode<DirectiveInstanceType, Compone
|
|||
}
|
||||
|
||||
const indexTree = <T extends DevToolsNode<DirectiveInstanceType, ComponentInstanceType>>(
|
||||
node: T, idx: number, parentPosition: number[] = []): IndexedNode => {
|
||||
node: T,
|
||||
idx: number,
|
||||
parentPosition: number[] = [],
|
||||
): IndexedNode => {
|
||||
const position = parentPosition.concat([idx]);
|
||||
return {
|
||||
position,
|
||||
|
|
@ -126,4 +135,5 @@ const indexTree = <T extends DevToolsNode<DirectiveInstanceType, ComponentInstan
|
|||
};
|
||||
|
||||
export const indexForest = <T extends DevToolsNode<DirectiveInstanceType, ComponentInstanceType>>(
|
||||
forest: T[]): IndexedNode[] => forest.map((n, i) => indexTree(n, i));
|
||||
forest: T[],
|
||||
): IndexedNode[] => forest.map((n, i) => indexTree(n, i));
|
||||
|
|
|
|||
|
|
@ -15,9 +15,9 @@ import {DirectiveForestHooks} from './hooks';
|
|||
const markName = (s: string, method: Method) => `🅰️ ${s}#${method}`;
|
||||
|
||||
const supportsPerformance =
|
||||
globalThis.performance && typeof globalThis.performance.getEntriesByName === 'function';
|
||||
globalThis.performance && typeof globalThis.performance.getEntriesByName === 'function';
|
||||
|
||||
type Method = keyof LifecycleProfile|'changeDetection'|string;
|
||||
type Method = keyof LifecycleProfile | 'changeDetection' | string;
|
||||
|
||||
const recordMark = (s: string, method: Method) => {
|
||||
if (supportsPerformance) {
|
||||
|
|
|
|||
|
|
@ -15,32 +15,37 @@ import {IdentityTracker, NodeArray} from '../identity-tracker';
|
|||
|
||||
import {getLifeCycleName, Hooks, Profiler} from './shared';
|
||||
|
||||
type ProfilerCallback = (event: ɵProfilerEvent, instanceOrLView: {}|null, hookOrListener: any) =>
|
||||
void;
|
||||
type ProfilerCallback = (
|
||||
event: ɵProfilerEvent,
|
||||
instanceOrLView: {} | null,
|
||||
hookOrListener: any,
|
||||
) => void;
|
||||
|
||||
/** Implementation of Profiler that utilizes framework APIs fire profiler hooks. */
|
||||
export class NgProfiler extends Profiler {
|
||||
private _tracker = IdentityTracker.getInstance();
|
||||
private _callbacks: ProfilerCallback[] = [];
|
||||
private _lastDirectiveInstance: {}|null = null;
|
||||
private _lastDirectiveInstance: {} | null = null;
|
||||
|
||||
constructor(config: Partial<Hooks> = {}) {
|
||||
super(config);
|
||||
this._setProfilerCallback(
|
||||
(event: ɵProfilerEvent, instanceOrLView: {}|null, hookOrListener: any) => {
|
||||
if (this[event] === undefined) {
|
||||
return;
|
||||
}
|
||||
(event: ɵProfilerEvent, instanceOrLView: {} | null, hookOrListener: any) => {
|
||||
if (this[event] === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
this[event](instanceOrLView, hookOrListener);
|
||||
});
|
||||
this[event](instanceOrLView, hookOrListener);
|
||||
},
|
||||
);
|
||||
this._initialize();
|
||||
}
|
||||
|
||||
private _initialize(): void {
|
||||
ngDebugClient().ɵsetProfiler(
|
||||
(event: ɵProfilerEvent, instanceOrLView: {}|null, hookOrListener: any) =>
|
||||
this._callbacks.forEach((cb) => cb(event, instanceOrLView, hookOrListener)));
|
||||
(event: ɵProfilerEvent, instanceOrLView: {} | null, hookOrListener: any) =>
|
||||
this._callbacks.forEach((cb) => cb(event, instanceOrLView, hookOrListener)),
|
||||
);
|
||||
}
|
||||
|
||||
private _setProfilerCallback(callback: ProfilerCallback): void {
|
||||
|
|
@ -106,9 +111,11 @@ export class NgProfiler extends Profiler {
|
|||
}
|
||||
|
||||
this._onChangeDetectionStart(
|
||||
this._lastDirectiveInstance, getDirectiveHostElement(this._lastDirectiveInstance),
|
||||
this._tracker.getDirectiveId(this._lastDirectiveInstance),
|
||||
this._tracker.getDirectivePosition(this._lastDirectiveInstance));
|
||||
this._lastDirectiveInstance,
|
||||
getDirectiveHostElement(this._lastDirectiveInstance),
|
||||
this._tracker.getDirectiveId(this._lastDirectiveInstance),
|
||||
this._tracker.getDirectivePosition(this._lastDirectiveInstance),
|
||||
);
|
||||
}
|
||||
|
||||
[ɵProfilerEvent.TemplateUpdateEnd](context: any, _hookOrListener: any): void {
|
||||
|
|
@ -121,9 +128,11 @@ export class NgProfiler extends Profiler {
|
|||
}
|
||||
|
||||
this._onChangeDetectionEnd(
|
||||
this._lastDirectiveInstance, getDirectiveHostElement(this._lastDirectiveInstance),
|
||||
this._tracker.getDirectiveId(this._lastDirectiveInstance),
|
||||
this._tracker.getDirectivePosition(this._lastDirectiveInstance));
|
||||
this._lastDirectiveInstance,
|
||||
getDirectiveHostElement(this._lastDirectiveInstance),
|
||||
this._tracker.getDirectiveId(this._lastDirectiveInstance),
|
||||
this._tracker.getDirectivePosition(this._lastDirectiveInstance),
|
||||
);
|
||||
}
|
||||
|
||||
[ɵProfilerEvent.LifecycleHookStart](directive: any, hook: any): void {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,11 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {getDirectiveHostElement, getLViewFromDirectiveOrElementInstance, METADATA_PROPERTY_NAME,} from '../../directive-forest';
|
||||
import {
|
||||
getDirectiveHostElement,
|
||||
getLViewFromDirectiveOrElementInstance,
|
||||
METADATA_PROPERTY_NAME,
|
||||
} from '../../directive-forest';
|
||||
import {runOutsideAngular} from '../../utils';
|
||||
import {IdentityTracker, NodeArray} from '../identity-tracker';
|
||||
|
||||
|
|
@ -82,7 +86,7 @@ export class PatchingProfiler extends Profiler {
|
|||
if (original.patched) {
|
||||
return;
|
||||
}
|
||||
declarations.tView.template = function(_: any, component: any): void {
|
||||
declarations.tView.template = function (_: any, component: any): void {
|
||||
if (!self._inChangeDetection) {
|
||||
self._inChangeDetection = true;
|
||||
runOutsideAngular(() => {
|
||||
|
|
@ -122,7 +126,7 @@ export class PatchingProfiler extends Profiler {
|
|||
}
|
||||
if (typeof el === 'function') {
|
||||
const self = this;
|
||||
current[idx] = function(): any {
|
||||
current[idx] = function (): any {
|
||||
// We currently don't want to notify the consumer
|
||||
// for execution of lifecycle hooks of services and pipes.
|
||||
// These two abstractions don't have `__ngContext__`, and
|
||||
|
|
|
|||
|
|
@ -11,32 +11,64 @@ import {Subject} from 'rxjs';
|
|||
|
||||
import {NodeArray} from '../identity-tracker';
|
||||
|
||||
type CreationHook =
|
||||
(componentOrDirective: any, node: Node, id: number, isComponent: boolean,
|
||||
position: ElementPosition) => void;
|
||||
type CreationHook = (
|
||||
componentOrDirective: any,
|
||||
node: Node,
|
||||
id: number,
|
||||
isComponent: boolean,
|
||||
position: ElementPosition,
|
||||
) => void;
|
||||
|
||||
type LifecycleStartHook =
|
||||
(componentOrDirective: any, hook: keyof LifecycleProfile, node: Node, id: number,
|
||||
isComponent: boolean) => void;
|
||||
type LifecycleStartHook = (
|
||||
componentOrDirective: any,
|
||||
hook: keyof LifecycleProfile,
|
||||
node: Node,
|
||||
id: number,
|
||||
isComponent: boolean,
|
||||
) => void;
|
||||
|
||||
type LifecycleEndHook =
|
||||
(componentOrDirective: any, hook: keyof LifecycleProfile, node: Node, id: number,
|
||||
isComponent: boolean) => void;
|
||||
type LifecycleEndHook = (
|
||||
componentOrDirective: any,
|
||||
hook: keyof LifecycleProfile,
|
||||
node: Node,
|
||||
id: number,
|
||||
isComponent: boolean,
|
||||
) => void;
|
||||
|
||||
type ChangeDetectionStartHook =
|
||||
(component: any, node: Node, id: number, position: ElementPosition) => void;
|
||||
type ChangeDetectionStartHook = (
|
||||
component: any,
|
||||
node: Node,
|
||||
id: number,
|
||||
position: ElementPosition,
|
||||
) => void;
|
||||
|
||||
type ChangeDetectionEndHook = (component: any, node: Node, id: number, position: ElementPosition) =>
|
||||
void;
|
||||
type ChangeDetectionEndHook = (
|
||||
component: any,
|
||||
node: Node,
|
||||
id: number,
|
||||
position: ElementPosition,
|
||||
) => void;
|
||||
|
||||
type DestroyHook =
|
||||
(componentOrDirective: any, node: Node, id: number, isComponent: boolean,
|
||||
position: ElementPosition) => void;
|
||||
type DestroyHook = (
|
||||
componentOrDirective: any,
|
||||
node: Node,
|
||||
id: number,
|
||||
isComponent: boolean,
|
||||
position: ElementPosition,
|
||||
) => void;
|
||||
|
||||
type OutputStartHook =
|
||||
(componentOrDirective: any, outputName: string, node: Node, isComponent: boolean) => void;
|
||||
type OutputEndHook =
|
||||
(componentOrDirective: any, outputName: string, node: Node, isComponent: boolean) => void;
|
||||
type OutputStartHook = (
|
||||
componentOrDirective: any,
|
||||
outputName: string,
|
||||
node: Node,
|
||||
isComponent: boolean,
|
||||
) => void;
|
||||
type OutputEndHook = (
|
||||
componentOrDirective: any,
|
||||
outputName: string,
|
||||
node: Node,
|
||||
isComponent: boolean,
|
||||
) => void;
|
||||
|
||||
export interface Hooks {
|
||||
onCreate: CreationHook;
|
||||
|
|
@ -79,8 +111,12 @@ export abstract class Profiler {
|
|||
|
||||
/** @internal */
|
||||
protected _onCreate(
|
||||
_: any, __: Node, id: number|undefined, ___: boolean,
|
||||
position: ElementPosition|undefined): void {
|
||||
_: any,
|
||||
__: Node,
|
||||
id: number | undefined,
|
||||
___: boolean,
|
||||
position: ElementPosition | undefined,
|
||||
): void {
|
||||
if (id === undefined || position === undefined) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -89,8 +125,12 @@ export abstract class Profiler {
|
|||
|
||||
/** @internal */
|
||||
protected _onDestroy(
|
||||
_: any, __: Node, id: number|undefined, ___: boolean,
|
||||
position: ElementPosition|undefined): void {
|
||||
_: any,
|
||||
__: Node,
|
||||
id: number | undefined,
|
||||
___: boolean,
|
||||
position: ElementPosition | undefined,
|
||||
): void {
|
||||
if (id === undefined || position === undefined) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -99,7 +139,11 @@ export abstract class Profiler {
|
|||
|
||||
/** @internal */
|
||||
protected _onChangeDetectionStart(
|
||||
_: any, __: Node, id: number|undefined, position: ElementPosition|undefined): void {
|
||||
_: any,
|
||||
__: Node,
|
||||
id: number | undefined,
|
||||
position: ElementPosition | undefined,
|
||||
): void {
|
||||
if (id === undefined || position === undefined) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -108,7 +152,11 @@ export abstract class Profiler {
|
|||
|
||||
/** @internal */
|
||||
protected _onChangeDetectionEnd(
|
||||
_: any, __: Node, id: number|undefined, position: ElementPosition|undefined): void {
|
||||
_: any,
|
||||
__: Node,
|
||||
id: number | undefined,
|
||||
position: ElementPosition | undefined,
|
||||
): void {
|
||||
if (id === undefined || position === undefined) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -117,8 +165,12 @@ export abstract class Profiler {
|
|||
|
||||
/** @internal */
|
||||
protected _onLifecycleHookStart(
|
||||
_: any, __: keyof LifecycleProfile|'unknown', ___: Node, id: number|undefined,
|
||||
____: boolean): void {
|
||||
_: any,
|
||||
__: keyof LifecycleProfile | 'unknown',
|
||||
___: Node,
|
||||
id: number | undefined,
|
||||
____: boolean,
|
||||
): void {
|
||||
if (id === undefined) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -127,8 +179,12 @@ export abstract class Profiler {
|
|||
|
||||
/** @internal */
|
||||
protected _onLifecycleHookEnd(
|
||||
_: any, __: keyof LifecycleProfile|'unknown', ___: Node, id: number|undefined,
|
||||
____: boolean): void {
|
||||
_: any,
|
||||
__: keyof LifecycleProfile | 'unknown',
|
||||
___: Node,
|
||||
id: number | undefined,
|
||||
____: boolean,
|
||||
): void {
|
||||
if (id === undefined) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -136,8 +192,13 @@ export abstract class Profiler {
|
|||
}
|
||||
|
||||
/** @internal */
|
||||
protected _onOutputStart(_: any, __: string, ___: Node, id: number|undefined, ____: boolean):
|
||||
void {
|
||||
protected _onOutputStart(
|
||||
_: any,
|
||||
__: string,
|
||||
___: Node,
|
||||
id: number | undefined,
|
||||
____: boolean,
|
||||
): void {
|
||||
if (id === undefined) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -145,7 +206,13 @@ export abstract class Profiler {
|
|||
}
|
||||
|
||||
/** @internal */
|
||||
protected _onOutputEnd(_: any, __: string, ___: Node, id: number|undefined, ____: boolean): void {
|
||||
protected _onOutputEnd(
|
||||
_: any,
|
||||
__: string,
|
||||
___: Node,
|
||||
id: number | undefined,
|
||||
____: boolean,
|
||||
): void {
|
||||
if (id === undefined) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -176,7 +243,7 @@ const hookNames = [
|
|||
|
||||
const hookMethodNames = new Set(hookNames.map((hook) => `ng${hook}`));
|
||||
|
||||
export const getLifeCycleName = (obj: {}, fn: any): keyof LifecycleProfile|'unknown' => {
|
||||
export const getLifeCycleName = (obj: {}, fn: any): keyof LifecycleProfile | 'unknown' => {
|
||||
const proto = Object.getPrototypeOf(obj);
|
||||
const keys = Object.getOwnPropertyNames(proto);
|
||||
for (const propName of keys) {
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export interface ComponentInstanceType {
|
|||
isElement: boolean;
|
||||
}
|
||||
|
||||
export interface ComponentTreeNode extends
|
||||
DevToolsNode<DirectiveInstanceType, ComponentInstanceType> {
|
||||
export interface ComponentTreeNode
|
||||
extends DevToolsNode<DirectiveInstanceType, ComponentInstanceType> {
|
||||
children: ComponentTreeNode[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export function parseRoutes(router: Router): Route {
|
|||
return root;
|
||||
}
|
||||
|
||||
function assignChildrenToParent(parentPath: string|null, children: Routes): Route[] {
|
||||
function assignChildrenToParent(parentPath: string | null, children: Routes): Route[] {
|
||||
return children.map((child: AngularRoute) => {
|
||||
const childName = childRouteName(child);
|
||||
const childDescendents: [any] = (child as any)._loadedConfig?.routes || child.children;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {arrayEquals} from 'shared-utils';
|
|||
import {ComponentTreeNode} from './interfaces';
|
||||
|
||||
interface ConsoleReferenceNode {
|
||||
node: ComponentTreeNode|null;
|
||||
node: ComponentTreeNode | null;
|
||||
position: ElementPosition;
|
||||
}
|
||||
|
||||
|
|
@ -35,8 +35,9 @@ const _setConsoleReference = (referenceNode: ConsoleReferenceNode) => {
|
|||
};
|
||||
|
||||
const prepareCurrentReferencesForInsertion = (referenceNode: ConsoleReferenceNode) => {
|
||||
const foundIndex = nodesForConsoleReference.findIndex(
|
||||
(nodeToLookFor) => arrayEquals(nodeToLookFor.position, referenceNode.position));
|
||||
const foundIndex = nodesForConsoleReference.findIndex((nodeToLookFor) =>
|
||||
arrayEquals(nodeToLookFor.position, referenceNode.position),
|
||||
);
|
||||
if (foundIndex !== -1) {
|
||||
nodesForConsoleReference.splice(foundIndex, 1);
|
||||
} else if (nodesForConsoleReference.length === CAPACITY) {
|
||||
|
|
@ -45,12 +46,12 @@ const prepareCurrentReferencesForInsertion = (referenceNode: ConsoleReferenceNod
|
|||
};
|
||||
|
||||
const assignConsoleReferencesFrom = (referenceNodes: ConsoleReferenceNode[]) => {
|
||||
referenceNodes.forEach(
|
||||
(referenceNode, index) =>
|
||||
setDirectiveKey(referenceNode.node, getConsoleReferenceWithIndexOf(index)));
|
||||
referenceNodes.forEach((referenceNode, index) =>
|
||||
setDirectiveKey(referenceNode.node, getConsoleReferenceWithIndexOf(index)),
|
||||
);
|
||||
};
|
||||
|
||||
const setDirectiveKey = (node: ComponentTreeNode|null, key: string) => {
|
||||
const setDirectiveKey = (node: ComponentTreeNode | null, key: string) => {
|
||||
Object.defineProperty(window, key, {
|
||||
get: () => {
|
||||
if (node?.component) {
|
||||
|
|
@ -66,4 +67,4 @@ const setDirectiveKey = (node: ComponentTreeNode|null, key: string) => {
|
|||
};
|
||||
|
||||
const getConsoleReferenceWithIndexOf = (consoleReferenceIndex: number) =>
|
||||
`${CONSOLE_REFERENCE_PREFIX}${consoleReferenceIndex}`;
|
||||
`${CONSOLE_REFERENCE_PREFIX}${consoleReferenceIndex}`;
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export function getKeys(obj: {}): string[] {
|
|||
const prototypeMembers = Object.getOwnPropertyDescriptors(Object.getPrototypeOf(obj));
|
||||
|
||||
const ignoreList = ['__proto__'];
|
||||
const gettersAndSetters = Object.keys(prototypeMembers).filter(methodName => {
|
||||
const gettersAndSetters = Object.keys(prototypeMembers).filter((methodName) => {
|
||||
if (ignoreList.includes(methodName)) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -42,6 +42,6 @@ export function getKeys(obj: {}): string[] {
|
|||
* @param propName The string representation of the target property name
|
||||
* @returns The Descriptor object of the property
|
||||
*/
|
||||
export const getDescriptor = (instance: any, propName: string): PropertyDescriptor|undefined =>
|
||||
Object.getOwnPropertyDescriptor(instance, propName) ||
|
||||
Object.getOwnPropertyDescriptor(Object.getPrototypeOf(instance), propName);
|
||||
export const getDescriptor = (instance: any, propName: string): PropertyDescriptor | undefined =>
|
||||
Object.getOwnPropertyDescriptor(instance, propName) ||
|
||||
Object.getOwnPropertyDescriptor(Object.getPrototypeOf(instance), propName);
|
||||
|
|
|
|||
|
|
@ -73,11 +73,14 @@ describe('getPropType', () => {
|
|||
propTypeName: 'Set',
|
||||
},
|
||||
{
|
||||
expression:
|
||||
new Map<unknown, unknown>([['name', 'John'], ['age', 40], [{id: 123}, undefined]]),
|
||||
expression: new Map<unknown, unknown>([
|
||||
['name', 'John'],
|
||||
['age', 40],
|
||||
[{id: 123}, undefined],
|
||||
]),
|
||||
propType: PropType.Map,
|
||||
propTypeName: 'Map',
|
||||
}
|
||||
},
|
||||
];
|
||||
for (const {expression, propType, propTypeName} of testCases) {
|
||||
it(`should determine ${String(expression)} as PropType:${propTypeName}(${propType})`, () => {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import {getDescriptor, getKeys} from './object-utils';
|
|||
// todo(aleksanderbodurri) pull this out of this file
|
||||
const METADATA_PROPERTY_NAME = '__ngContext__';
|
||||
|
||||
type NestedType = PropType.Array|PropType.Object;
|
||||
type NestedType = PropType.Array | PropType.Object;
|
||||
|
||||
export interface CompositeType {
|
||||
type: Extract<PropType, NestedType>;
|
||||
|
|
@ -29,7 +29,7 @@ export interface TerminalType {
|
|||
containerType: ContainerType;
|
||||
}
|
||||
|
||||
export type PropertyData = TerminalType|CompositeType;
|
||||
export type PropertyData = TerminalType | CompositeType;
|
||||
|
||||
export type Formatter<Result> = {
|
||||
[key in PropType]: (data: any) => Result;
|
||||
|
|
@ -73,107 +73,121 @@ const typeToDescriptorPreview: Formatter<string> = {
|
|||
[PropType.Unknown]: (_: any) => 'unknown',
|
||||
};
|
||||
|
||||
type Key = string|number;
|
||||
type NestedSerializerFn =
|
||||
(instance: any, propName: string|number, nodes: NestedProp[], isReadonly: boolean,
|
||||
currentLevel: number, level?: number) => Descriptor;
|
||||
type Key = string | number;
|
||||
type NestedSerializerFn = (
|
||||
instance: any,
|
||||
propName: string | number,
|
||||
nodes: NestedProp[],
|
||||
isReadonly: boolean,
|
||||
currentLevel: number,
|
||||
level?: number,
|
||||
) => Descriptor;
|
||||
|
||||
const ignoreList: Set<Key> = new Set([METADATA_PROPERTY_NAME, '__ngSimpleChanges__']);
|
||||
|
||||
const shallowPropTypeToTreeMetaData:
|
||||
Record<Exclude<PropType, NestedType>, {editable: boolean, expandable: boolean}> = {
|
||||
[PropType.String]: {
|
||||
editable: true,
|
||||
expandable: false,
|
||||
},
|
||||
[PropType.BigInt]: {
|
||||
editable: false,
|
||||
expandable: false,
|
||||
},
|
||||
[PropType.Boolean]: {
|
||||
editable: true,
|
||||
expandable: false,
|
||||
},
|
||||
[PropType.Number]: {
|
||||
editable: true,
|
||||
expandable: false,
|
||||
},
|
||||
[PropType.Date]: {
|
||||
editable: false,
|
||||
expandable: false,
|
||||
},
|
||||
[PropType.Null]: {
|
||||
editable: true,
|
||||
expandable: false,
|
||||
},
|
||||
[PropType.Undefined]: {
|
||||
editable: true,
|
||||
expandable: false,
|
||||
},
|
||||
[PropType.Symbol]: {
|
||||
editable: false,
|
||||
expandable: false,
|
||||
},
|
||||
[PropType.Function]: {
|
||||
editable: false,
|
||||
expandable: false,
|
||||
},
|
||||
[PropType.HTMLNode]: {
|
||||
editable: false,
|
||||
expandable: false,
|
||||
},
|
||||
[PropType.Unknown]: {
|
||||
editable: false,
|
||||
expandable: false,
|
||||
},
|
||||
[PropType.Set]: {
|
||||
editable: false,
|
||||
expandable: false,
|
||||
},
|
||||
[PropType.Map]: {
|
||||
editable: false,
|
||||
expandable: false,
|
||||
},
|
||||
};
|
||||
const shallowPropTypeToTreeMetaData: Record<
|
||||
Exclude<PropType, NestedType>,
|
||||
{editable: boolean; expandable: boolean}
|
||||
> = {
|
||||
[PropType.String]: {
|
||||
editable: true,
|
||||
expandable: false,
|
||||
},
|
||||
[PropType.BigInt]: {
|
||||
editable: false,
|
||||
expandable: false,
|
||||
},
|
||||
[PropType.Boolean]: {
|
||||
editable: true,
|
||||
expandable: false,
|
||||
},
|
||||
[PropType.Number]: {
|
||||
editable: true,
|
||||
expandable: false,
|
||||
},
|
||||
[PropType.Date]: {
|
||||
editable: false,
|
||||
expandable: false,
|
||||
},
|
||||
[PropType.Null]: {
|
||||
editable: true,
|
||||
expandable: false,
|
||||
},
|
||||
[PropType.Undefined]: {
|
||||
editable: true,
|
||||
expandable: false,
|
||||
},
|
||||
[PropType.Symbol]: {
|
||||
editable: false,
|
||||
expandable: false,
|
||||
},
|
||||
[PropType.Function]: {
|
||||
editable: false,
|
||||
expandable: false,
|
||||
},
|
||||
[PropType.HTMLNode]: {
|
||||
editable: false,
|
||||
expandable: false,
|
||||
},
|
||||
[PropType.Unknown]: {
|
||||
editable: false,
|
||||
expandable: false,
|
||||
},
|
||||
[PropType.Set]: {
|
||||
editable: false,
|
||||
expandable: false,
|
||||
},
|
||||
[PropType.Map]: {
|
||||
editable: false,
|
||||
expandable: false,
|
||||
},
|
||||
};
|
||||
|
||||
const isEditable =
|
||||
(descriptor: PropertyDescriptor|undefined, propName: string|number, propData: TerminalType,
|
||||
isGetterOrSetter: boolean) => {
|
||||
if (propData.containerType === 'ReadonlySignal') {
|
||||
return false;
|
||||
}
|
||||
const isEditable = (
|
||||
descriptor: PropertyDescriptor | undefined,
|
||||
propName: string | number,
|
||||
propData: TerminalType,
|
||||
isGetterOrSetter: boolean,
|
||||
) => {
|
||||
if (propData.containerType === 'ReadonlySignal') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof propName === 'symbol') {
|
||||
return false;
|
||||
}
|
||||
if (typeof propName === 'symbol') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isGetterOrSetter) {
|
||||
return false;
|
||||
}
|
||||
if (isGetterOrSetter) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (descriptor?.writable === false) {
|
||||
return false;
|
||||
}
|
||||
if (descriptor?.writable === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return shallowPropTypeToTreeMetaData[propData.type].editable;
|
||||
};
|
||||
return shallowPropTypeToTreeMetaData[propData.type].editable;
|
||||
};
|
||||
|
||||
const isGetterOrSetter = (descriptor: any): boolean =>
|
||||
(descriptor?.set || descriptor?.get) && !('value' in descriptor);
|
||||
(descriptor?.set || descriptor?.get) && !('value' in descriptor);
|
||||
|
||||
const getPreview = (propData: TerminalType|CompositeType, isGetterOrSetter: boolean) => {
|
||||
const getPreview = (propData: TerminalType | CompositeType, isGetterOrSetter: boolean) => {
|
||||
if (propData.containerType === 'ReadonlySignal') {
|
||||
return `Readonly Signal(${typeToDescriptorPreview[propData.type](propData.prop())})`;
|
||||
} else if (propData.containerType === 'WritableSignal') {
|
||||
return `Signal(${typeToDescriptorPreview[propData.type](propData.prop())})`;
|
||||
}
|
||||
return !isGetterOrSetter ? typeToDescriptorPreview[propData.type](propData.prop) :
|
||||
typeToDescriptorPreview[PropType.Function]({name: ''});
|
||||
return !isGetterOrSetter
|
||||
? typeToDescriptorPreview[propData.type](propData.prop)
|
||||
: typeToDescriptorPreview[PropType.Function]({name: ''});
|
||||
};
|
||||
|
||||
export function createShallowSerializedDescriptor(
|
||||
instance: any, propName: string|number, propData: TerminalType,
|
||||
isReadonly: boolean): Descriptor {
|
||||
instance: any,
|
||||
propName: string | number,
|
||||
propData: TerminalType,
|
||||
isReadonly: boolean,
|
||||
): Descriptor {
|
||||
const {type, containerType} = propData;
|
||||
|
||||
const descriptor = getDescriptor(instance, propName as string);
|
||||
|
|
@ -195,10 +209,18 @@ export function createShallowSerializedDescriptor(
|
|||
}
|
||||
|
||||
export function createLevelSerializedDescriptor(
|
||||
instance: {}, propName: string|number, propData: CompositeType, levelOptions: LevelOptions,
|
||||
continuation: (
|
||||
instance: any, propName: string|number, isReadonly: boolean, level?: number,
|
||||
max?: number) => Descriptor): Descriptor {
|
||||
instance: {},
|
||||
propName: string | number,
|
||||
propData: CompositeType,
|
||||
levelOptions: LevelOptions,
|
||||
continuation: (
|
||||
instance: any,
|
||||
propName: string | number,
|
||||
isReadonly: boolean,
|
||||
level?: number,
|
||||
max?: number,
|
||||
) => Descriptor,
|
||||
): Descriptor {
|
||||
const {type, prop, containerType} = propData;
|
||||
|
||||
const descriptor = getDescriptor(instance, propName as string);
|
||||
|
|
@ -223,8 +245,13 @@ export function createLevelSerializedDescriptor(
|
|||
}
|
||||
|
||||
export function createNestedSerializedDescriptor(
|
||||
instance: {}, propName: string|number, propData: CompositeType, levelOptions: LevelOptions,
|
||||
nodes: NestedProp[], nestedSerializer: NestedSerializerFn): Descriptor {
|
||||
instance: {},
|
||||
propName: string | number,
|
||||
propData: CompositeType,
|
||||
levelOptions: LevelOptions,
|
||||
nodes: NestedProp[],
|
||||
nestedSerializer: NestedSerializerFn,
|
||||
): Descriptor {
|
||||
const {type, prop, containerType} = propData;
|
||||
|
||||
const descriptor = getDescriptor(instance, propName as string);
|
||||
|
|
@ -248,49 +275,75 @@ export function createNestedSerializedDescriptor(
|
|||
}
|
||||
|
||||
function getNestedDescriptorValue(
|
||||
propData: CompositeType, levelOptions: LevelOptions, nodes: NestedProp[],
|
||||
nestedSerializer: NestedSerializerFn) {
|
||||
propData: CompositeType,
|
||||
levelOptions: LevelOptions,
|
||||
nodes: NestedProp[],
|
||||
nestedSerializer: NestedSerializerFn,
|
||||
) {
|
||||
const {type, prop} = propData;
|
||||
const {currentLevel} = levelOptions;
|
||||
const value = unwrapSignal(prop);
|
||||
|
||||
switch (type) {
|
||||
case PropType.Array:
|
||||
return nodes.map(
|
||||
(nestedProp) => nestedSerializer(
|
||||
value, nestedProp.name, nestedProp.children, false, currentLevel + 1));
|
||||
return nodes.map((nestedProp) =>
|
||||
nestedSerializer(value, nestedProp.name, nestedProp.children, false, currentLevel + 1),
|
||||
);
|
||||
case PropType.Object:
|
||||
return nodes.reduce((accumulator, nestedProp) => {
|
||||
if (prop.hasOwnProperty(nestedProp.name) && !ignoreList.has(nestedProp.name)) {
|
||||
accumulator[nestedProp.name] = nestedSerializer(
|
||||
value, nestedProp.name, nestedProp.children, false, currentLevel + 1);
|
||||
}
|
||||
return accumulator;
|
||||
}, {} as Record<string, Descriptor>);
|
||||
return nodes.reduce(
|
||||
(accumulator, nestedProp) => {
|
||||
if (prop.hasOwnProperty(nestedProp.name) && !ignoreList.has(nestedProp.name)) {
|
||||
accumulator[nestedProp.name] = nestedSerializer(
|
||||
value,
|
||||
nestedProp.name,
|
||||
nestedProp.children,
|
||||
false,
|
||||
currentLevel + 1,
|
||||
);
|
||||
}
|
||||
return accumulator;
|
||||
},
|
||||
{} as Record<string, Descriptor>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getLevelDescriptorValue(
|
||||
propData: CompositeType, levelOptions: LevelOptions,
|
||||
continuation: (
|
||||
instance: any, propName: string|number, isReadonly: boolean, level?: number,
|
||||
max?: number) => Descriptor) {
|
||||
propData: CompositeType,
|
||||
levelOptions: LevelOptions,
|
||||
continuation: (
|
||||
instance: any,
|
||||
propName: string | number,
|
||||
isReadonly: boolean,
|
||||
level?: number,
|
||||
max?: number,
|
||||
) => Descriptor,
|
||||
) {
|
||||
const {type, prop} = propData;
|
||||
const {currentLevel, level} = levelOptions;
|
||||
const value = unwrapSignal(prop);
|
||||
const isReadonly = isSignal(prop);
|
||||
switch (type) {
|
||||
case PropType.Array:
|
||||
return prop.map(
|
||||
(_: any, idx: number) => continuation(value, idx, isReadonly, currentLevel + 1, level));
|
||||
return prop.map((_: any, idx: number) =>
|
||||
continuation(value, idx, isReadonly, currentLevel + 1, level),
|
||||
);
|
||||
case PropType.Object:
|
||||
return getKeys(prop).reduce((accumulator, propName) => {
|
||||
if (!ignoreList.has(propName)) {
|
||||
accumulator[propName] =
|
||||
continuation(value, propName, isReadonly, currentLevel + 1, level);
|
||||
}
|
||||
return accumulator;
|
||||
}, {} as Record<string, Descriptor>);
|
||||
return getKeys(prop).reduce(
|
||||
(accumulator, propName) => {
|
||||
if (!ignoreList.has(propName)) {
|
||||
accumulator[propName] = continuation(
|
||||
value,
|
||||
propName,
|
||||
isReadonly,
|
||||
currentLevel + 1,
|
||||
level,
|
||||
);
|
||||
}
|
||||
return accumulator;
|
||||
},
|
||||
{} as Record<string, Descriptor>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ describe('deeplySerializeSelectedProperties', () => {
|
|||
editable: true,
|
||||
preview: '1',
|
||||
value: 1,
|
||||
containerType: null
|
||||
containerType: null,
|
||||
},
|
||||
nested: {
|
||||
type: PropType.Object,
|
||||
|
|
@ -106,10 +106,10 @@ describe('deeplySerializeSelectedProperties', () => {
|
|||
expandable: true,
|
||||
editable: false,
|
||||
preview: 'Array(4)',
|
||||
containerType: null
|
||||
containerType: null,
|
||||
},
|
||||
},
|
||||
containerType: null
|
||||
containerType: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
@ -123,7 +123,7 @@ describe('deeplySerializeSelectedProperties', () => {
|
|||
editable: true,
|
||||
preview: '1',
|
||||
value: 1,
|
||||
containerType: null
|
||||
containerType: null,
|
||||
},
|
||||
nested: {
|
||||
type: PropType.Object,
|
||||
|
|
@ -155,26 +155,26 @@ describe('deeplySerializeSelectedProperties', () => {
|
|||
editable: true,
|
||||
preview: '1',
|
||||
value: 1,
|
||||
containerType: null
|
||||
containerType: null,
|
||||
},
|
||||
},
|
||||
containerType: null
|
||||
containerType: null,
|
||||
},
|
||||
],
|
||||
containerType: null
|
||||
containerType: null,
|
||||
},
|
||||
{
|
||||
type: PropType.Set,
|
||||
editable: false,
|
||||
expandable: false,
|
||||
preview: 'Set(2)',
|
||||
containerType: null
|
||||
containerType: null,
|
||||
},
|
||||
],
|
||||
containerType: null
|
||||
containerType: null,
|
||||
},
|
||||
},
|
||||
containerType: null
|
||||
containerType: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
@ -202,36 +202,37 @@ describe('deeplySerializeSelectedProperties', () => {
|
|||
editable: false,
|
||||
expandable: true,
|
||||
preview: 'Array(3)',
|
||||
containerType: null
|
||||
containerType: null,
|
||||
},
|
||||
},
|
||||
containerType: null
|
||||
containerType: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with getters with specified query', () => {
|
||||
const result = deeplySerializeSelectedProperties(
|
||||
{
|
||||
get foo(): any {
|
||||
return {
|
||||
baz: {
|
||||
qux: 3,
|
||||
},
|
||||
};
|
||||
},
|
||||
{
|
||||
get foo(): any {
|
||||
return {
|
||||
baz: {
|
||||
qux: 3,
|
||||
},
|
||||
};
|
||||
},
|
||||
[
|
||||
{
|
||||
name: 'foo',
|
||||
children: [
|
||||
{
|
||||
name: 'baz',
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
},
|
||||
[
|
||||
{
|
||||
name: 'foo',
|
||||
children: [
|
||||
{
|
||||
name: 'baz',
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
);
|
||||
expect(result).toEqual({
|
||||
foo: {
|
||||
type: PropType.Object,
|
||||
|
|
@ -244,28 +245,29 @@ describe('deeplySerializeSelectedProperties', () => {
|
|||
editable: false,
|
||||
expandable: true,
|
||||
preview: '{...}',
|
||||
containerType: null
|
||||
containerType: null,
|
||||
},
|
||||
},
|
||||
containerType: null
|
||||
containerType: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with getters without specified query', () => {
|
||||
const result = deeplySerializeSelectedProperties(
|
||||
{
|
||||
get foo(): any {
|
||||
return {
|
||||
baz: {
|
||||
qux: {
|
||||
cos: 3,
|
||||
},
|
||||
{
|
||||
get foo(): any {
|
||||
return {
|
||||
baz: {
|
||||
qux: {
|
||||
cos: 3,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
[]);
|
||||
},
|
||||
[],
|
||||
);
|
||||
expect(result).toEqual({
|
||||
foo: {
|
||||
type: PropType.Object,
|
||||
|
|
@ -278,26 +280,27 @@ describe('deeplySerializeSelectedProperties', () => {
|
|||
editable: false,
|
||||
expandable: true,
|
||||
preview: '{...}',
|
||||
containerType: null
|
||||
containerType: null,
|
||||
},
|
||||
},
|
||||
containerType: null
|
||||
containerType: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('both getters and setters should be readonly', () => {
|
||||
const result = deeplySerializeSelectedProperties(
|
||||
{
|
||||
get foo(): number {
|
||||
return 42;
|
||||
},
|
||||
get bar(): number {
|
||||
return 42;
|
||||
},
|
||||
set bar(val: number) {},
|
||||
{
|
||||
get foo(): number {
|
||||
return 42;
|
||||
},
|
||||
[]);
|
||||
get bar(): number {
|
||||
return 42;
|
||||
},
|
||||
set bar(val: number) {},
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// Neither getter and setter is editable
|
||||
expect(result).toEqual({
|
||||
|
|
@ -307,7 +310,7 @@ describe('deeplySerializeSelectedProperties', () => {
|
|||
editable: false,
|
||||
preview: '(...)',
|
||||
value: 42,
|
||||
containerType: null
|
||||
containerType: null,
|
||||
},
|
||||
bar: {
|
||||
type: PropType.Number,
|
||||
|
|
@ -315,58 +318,59 @@ describe('deeplySerializeSelectedProperties', () => {
|
|||
editable: false,
|
||||
preview: '(...)',
|
||||
value: 42,
|
||||
containerType: null
|
||||
containerType: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the precise path requested', () => {
|
||||
const result = deeplySerializeSelectedProperties(
|
||||
{
|
||||
state: {
|
||||
nested: {
|
||||
props: {
|
||||
foo: 1,
|
||||
bar: 2,
|
||||
},
|
||||
[Symbol(3)](): number {
|
||||
return 1.618;
|
||||
},
|
||||
get foo(): number {
|
||||
return 42;
|
||||
},
|
||||
{
|
||||
state: {
|
||||
nested: {
|
||||
props: {
|
||||
foo: 1,
|
||||
bar: 2,
|
||||
},
|
||||
[Symbol(3)](): number {
|
||||
return 1.618;
|
||||
},
|
||||
get foo(): number {
|
||||
return 42;
|
||||
},
|
||||
},
|
||||
},
|
||||
[
|
||||
{
|
||||
name: 'state',
|
||||
children: [
|
||||
{
|
||||
name: 'nested',
|
||||
children: [
|
||||
{
|
||||
name: 'props',
|
||||
children: [
|
||||
{
|
||||
name: 'foo',
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
name: 'bar',
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'foo',
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
},
|
||||
[
|
||||
{
|
||||
name: 'state',
|
||||
children: [
|
||||
{
|
||||
name: 'nested',
|
||||
children: [
|
||||
{
|
||||
name: 'props',
|
||||
children: [
|
||||
{
|
||||
name: 'foo',
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
name: 'bar',
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'foo',
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
);
|
||||
expect(result).toEqual({
|
||||
state: {
|
||||
type: PropType.Object,
|
||||
|
|
@ -392,7 +396,7 @@ describe('deeplySerializeSelectedProperties', () => {
|
|||
editable: true,
|
||||
preview: '1',
|
||||
value: 1,
|
||||
containerType: null
|
||||
containerType: null,
|
||||
},
|
||||
bar: {
|
||||
type: PropType.Number,
|
||||
|
|
@ -400,10 +404,10 @@ describe('deeplySerializeSelectedProperties', () => {
|
|||
editable: true,
|
||||
preview: '2',
|
||||
value: 2,
|
||||
containerType: null
|
||||
containerType: null,
|
||||
},
|
||||
},
|
||||
containerType: null
|
||||
containerType: null,
|
||||
},
|
||||
foo: {
|
||||
type: PropType.Number,
|
||||
|
|
@ -411,33 +415,34 @@ describe('deeplySerializeSelectedProperties', () => {
|
|||
editable: false,
|
||||
preview: '(...)',
|
||||
value: 42,
|
||||
containerType: null
|
||||
containerType: null,
|
||||
},
|
||||
},
|
||||
containerType: null
|
||||
containerType: null,
|
||||
},
|
||||
},
|
||||
containerType: null
|
||||
containerType: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('both setter and getter would get a (...) as preview', () => {
|
||||
const result = deeplySerializeSelectedProperties(
|
||||
{
|
||||
set foo(_: any) {},
|
||||
get bar(): Object {
|
||||
return {foo: 1};
|
||||
},
|
||||
{
|
||||
set foo(_: any) {},
|
||||
get bar(): Object {
|
||||
return {foo: 1};
|
||||
},
|
||||
[]);
|
||||
},
|
||||
[],
|
||||
);
|
||||
expect(result).toEqual({
|
||||
foo: {
|
||||
type: PropType.Undefined,
|
||||
editable: false,
|
||||
expandable: false,
|
||||
preview: '(...)',
|
||||
containerType: null
|
||||
containerType: null,
|
||||
},
|
||||
bar: {
|
||||
type: PropType.Object,
|
||||
|
|
@ -452,7 +457,7 @@ describe('deeplySerializeSelectedProperties', () => {
|
|||
editable: true,
|
||||
preview: '1',
|
||||
value: 1,
|
||||
containerType: null
|
||||
containerType: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -467,50 +472,49 @@ describe('deeplySerializeSelectedProperties', () => {
|
|||
editable: true,
|
||||
expandable: false,
|
||||
preview: 'undefined',
|
||||
containerType: null
|
||||
}
|
||||
containerType: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('getDescriptor should get the descriptors for both getters and setters correctly from the prototype',
|
||||
() => {
|
||||
const instance = {
|
||||
__proto__: {
|
||||
get foo(): number {
|
||||
return 42;
|
||||
},
|
||||
set bar(newNum: number) {},
|
||||
get baz(): number {
|
||||
return 42;
|
||||
},
|
||||
set baz(newNum: number) {},
|
||||
}
|
||||
};
|
||||
it('getDescriptor should get the descriptors for both getters and setters correctly from the prototype', () => {
|
||||
const instance = {
|
||||
__proto__: {
|
||||
get foo(): number {
|
||||
return 42;
|
||||
},
|
||||
set bar(newNum: number) {},
|
||||
get baz(): number {
|
||||
return 42;
|
||||
},
|
||||
set baz(newNum: number) {},
|
||||
},
|
||||
};
|
||||
|
||||
const descriptorFoo = getDescriptor(instance, 'foo');
|
||||
expect(descriptorFoo).not.toBeNull();
|
||||
expect(descriptorFoo!.get).not.toBeNull();
|
||||
expect(descriptorFoo!.set).toBeUndefined();
|
||||
expect(descriptorFoo!.value).toBeUndefined();
|
||||
expect(descriptorFoo!.enumerable).toBe(true);
|
||||
expect(descriptorFoo!.configurable).toBe(true);
|
||||
const descriptorFoo = getDescriptor(instance, 'foo');
|
||||
expect(descriptorFoo).not.toBeNull();
|
||||
expect(descriptorFoo!.get).not.toBeNull();
|
||||
expect(descriptorFoo!.set).toBeUndefined();
|
||||
expect(descriptorFoo!.value).toBeUndefined();
|
||||
expect(descriptorFoo!.enumerable).toBe(true);
|
||||
expect(descriptorFoo!.configurable).toBe(true);
|
||||
|
||||
const descriptorBar = getDescriptor(instance, 'bar');
|
||||
expect(descriptorBar).not.toBeNull();
|
||||
expect(descriptorBar!.get).toBeUndefined();
|
||||
expect(descriptorBar!.set).not.toBeNull();
|
||||
expect(descriptorBar!.value).toBeUndefined();
|
||||
expect(descriptorBar!.enumerable).toBe(true);
|
||||
expect(descriptorBar!.configurable).toBe(true);
|
||||
const descriptorBar = getDescriptor(instance, 'bar');
|
||||
expect(descriptorBar).not.toBeNull();
|
||||
expect(descriptorBar!.get).toBeUndefined();
|
||||
expect(descriptorBar!.set).not.toBeNull();
|
||||
expect(descriptorBar!.value).toBeUndefined();
|
||||
expect(descriptorBar!.enumerable).toBe(true);
|
||||
expect(descriptorBar!.configurable).toBe(true);
|
||||
|
||||
const descriptorBaz = getDescriptor(instance, 'baz');
|
||||
expect(descriptorBaz).not.toBeNull();
|
||||
expect(descriptorBaz!.get).not.toBeNull();
|
||||
expect(descriptorBaz!.set).not.toBeNull();
|
||||
expect(descriptorBaz!.value).toBeUndefined();
|
||||
expect(descriptorBaz!.enumerable).toBe(true);
|
||||
expect(descriptorBaz!.configurable).toBe(true);
|
||||
});
|
||||
const descriptorBaz = getDescriptor(instance, 'baz');
|
||||
expect(descriptorBaz).not.toBeNull();
|
||||
expect(descriptorBaz!.get).not.toBeNull();
|
||||
expect(descriptorBaz!.set).not.toBeNull();
|
||||
expect(descriptorBaz!.value).toBeUndefined();
|
||||
expect(descriptorBaz!.enumerable).toBe(true);
|
||||
expect(descriptorBaz!.configurable).toBe(true);
|
||||
});
|
||||
|
||||
it('getKeys should all keys including getters and setters', () => {
|
||||
const instance = {
|
||||
|
|
@ -520,8 +524,8 @@ describe('deeplySerializeSelectedProperties', () => {
|
|||
return 42;
|
||||
},
|
||||
set foo(newNum: number) {},
|
||||
set bar(newNum: number) {}
|
||||
}
|
||||
set bar(newNum: number) {},
|
||||
},
|
||||
};
|
||||
|
||||
expect(getKeys(instance)).toEqual(['baz', 'foo', 'bar']);
|
||||
|
|
@ -534,8 +538,8 @@ describe('deeplySerializeSelectedProperties', () => {
|
|||
set __proto__(newObj: Object) {},
|
||||
get __proto__(): Object {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(getKeys(instance)).toEqual(['baz']);
|
||||
|
|
|
|||
|
|
@ -13,7 +13,12 @@ import {isSignal, unwrapSignal} from '../utils';
|
|||
|
||||
import {getKeys} from './object-utils';
|
||||
import {getPropType} from './prop-type';
|
||||
import {createLevelSerializedDescriptor, createNestedSerializedDescriptor, createShallowSerializedDescriptor, PropertyData,} from './serialized-descriptor-factory';
|
||||
import {
|
||||
createLevelSerializedDescriptor,
|
||||
createNestedSerializedDescriptor,
|
||||
createShallowSerializedDescriptor,
|
||||
PropertyData,
|
||||
} from './serialized-descriptor-factory';
|
||||
|
||||
// todo(aleksanderbodurri) pull this out of this file
|
||||
const METADATA_PROPERTY_NAME = '__ngContext__';
|
||||
|
|
@ -23,23 +28,32 @@ const ignoreList = new Set([METADATA_PROPERTY_NAME, '__ngSimpleChanges__']);
|
|||
const MAX_LEVEL = 1;
|
||||
|
||||
function nestedSerializer(
|
||||
instance: any, propName: string|number, nodes: NestedProp[], isReadonly: boolean,
|
||||
currentLevel = 0, level = MAX_LEVEL): Descriptor {
|
||||
instance: any,
|
||||
propName: string | number,
|
||||
nodes: NestedProp[],
|
||||
isReadonly: boolean,
|
||||
currentLevel = 0,
|
||||
level = MAX_LEVEL,
|
||||
): Descriptor {
|
||||
instance = unwrapSignal(instance);
|
||||
const serializableInstance = instance[propName];
|
||||
const propData: PropertyData = {
|
||||
prop: serializableInstance,
|
||||
type: getPropType(serializableInstance),
|
||||
containerType: getContainerType(serializableInstance)
|
||||
containerType: getContainerType(serializableInstance),
|
||||
};
|
||||
|
||||
if (currentLevel < level) {
|
||||
const continuation =
|
||||
(instance: any, propName: string|number, isReadonly: boolean, nestedLevel?: number,
|
||||
_?: number) => {
|
||||
const nodeChildren = nodes.find((v) => v.name === propName)?.children ?? [];
|
||||
return nestedSerializer(instance, propName, nodeChildren, isReadonly, nestedLevel, level);
|
||||
};
|
||||
const continuation = (
|
||||
instance: any,
|
||||
propName: string | number,
|
||||
isReadonly: boolean,
|
||||
nestedLevel?: number,
|
||||
_?: number,
|
||||
) => {
|
||||
const nodeChildren = nodes.find((v) => v.name === propName)?.children ?? [];
|
||||
return nestedSerializer(instance, propName, nodeChildren, isReadonly, nestedLevel, level);
|
||||
};
|
||||
|
||||
return levelSerializer(instance, propName, isReadonly, currentLevel, level, continuation);
|
||||
}
|
||||
|
|
@ -48,27 +62,43 @@ function nestedSerializer(
|
|||
case PropType.Array:
|
||||
case PropType.Object:
|
||||
return createNestedSerializedDescriptor(
|
||||
instance, propName, propData, {level, currentLevel}, nodes, nestedSerializer);
|
||||
instance,
|
||||
propName,
|
||||
propData,
|
||||
{level, currentLevel},
|
||||
nodes,
|
||||
nestedSerializer,
|
||||
);
|
||||
default:
|
||||
return createShallowSerializedDescriptor(instance, propName, propData, isReadonly);
|
||||
}
|
||||
}
|
||||
|
||||
function levelSerializer(
|
||||
instance: any, propName: string|number, isReadonly: boolean, currentLevel = 0,
|
||||
level = MAX_LEVEL, continuation = levelSerializer): Descriptor {
|
||||
instance: any,
|
||||
propName: string | number,
|
||||
isReadonly: boolean,
|
||||
currentLevel = 0,
|
||||
level = MAX_LEVEL,
|
||||
continuation = levelSerializer,
|
||||
): Descriptor {
|
||||
const serializableInstance = instance[propName];
|
||||
const propData: PropertyData = {
|
||||
prop: serializableInstance,
|
||||
type: getPropType(serializableInstance),
|
||||
containerType: getContainerType(serializableInstance)
|
||||
containerType: getContainerType(serializableInstance),
|
||||
};
|
||||
|
||||
switch (propData.type) {
|
||||
case PropType.Array:
|
||||
case PropType.Object:
|
||||
return createLevelSerializedDescriptor(
|
||||
instance, propName, propData, {level, currentLevel}, continuation);
|
||||
instance,
|
||||
propName,
|
||||
propData,
|
||||
{level, currentLevel},
|
||||
continuation,
|
||||
);
|
||||
default:
|
||||
return createShallowSerializedDescriptor(instance, propName, propData, isReadonly);
|
||||
}
|
||||
|
|
@ -88,7 +118,9 @@ export function serializeDirectiveState(instance: object): Record<string, Descri
|
|||
}
|
||||
|
||||
export function deeplySerializeSelectedProperties(
|
||||
instance: object, props: NestedProp[]): Record<string, Descriptor> {
|
||||
instance: object,
|
||||
props: NestedProp[],
|
||||
): Record<string, Descriptor> {
|
||||
const result: Record<string, Descriptor> = {};
|
||||
const isReadonly = isSignal(instance);
|
||||
getKeys(instance).forEach((prop) => {
|
||||
|
|
@ -105,7 +137,6 @@ export function deeplySerializeSelectedProperties(
|
|||
return result;
|
||||
}
|
||||
|
||||
|
||||
function getContainerType(instance: unknown): ContainerType {
|
||||
if (isSignal(instance)) {
|
||||
return isWritableSignal(instance) ? 'WritableSignal' : 'ReadonlySignal';
|
||||
|
|
|
|||
|
|
@ -59,8 +59,7 @@ export function ngDebugApiIsSupported(api: string): boolean {
|
|||
return typeof ng[api] === 'function';
|
||||
}
|
||||
|
||||
|
||||
export function isSignal(prop: unknown): prop is() => unknown {
|
||||
export function isSignal(prop: unknown): prop is () => unknown {
|
||||
if (!ngDebugApiIsSupported('isSignal')) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,18 +27,17 @@ export type InjectorTreeD3Node = d3.HierarchyPointNode<InjectorTreeNode>;
|
|||
|
||||
export abstract class GraphRenderer<T, U> {
|
||||
abstract render(graph: T): void;
|
||||
abstract getNodeById(id: string): U|null;
|
||||
abstract getNodeById(id: string): U | null;
|
||||
abstract snapToNode(node: U): void;
|
||||
abstract snapToRoot(): void;
|
||||
abstract zoomScale(scale: number): void;
|
||||
abstract root: U|null;
|
||||
abstract root: U | null;
|
||||
abstract get graphElement(): HTMLElement;
|
||||
|
||||
protected nodeClickListeners: ((pointerEvent: PointerEvent, node: U) => void)[] = [];
|
||||
protected nodeMouseoverListeners: ((pointerEvent: PointerEvent, node: U) => void)[] = [];
|
||||
protected nodeMouseoutListeners: ((pointerEvent: PointerEvent, node: U) => void)[] = [];
|
||||
|
||||
|
||||
cleanup(): void {
|
||||
this.nodeClickListeners = [];
|
||||
this.nodeMouseoverListeners = [];
|
||||
|
|
@ -59,7 +58,7 @@ export abstract class GraphRenderer<T, U> {
|
|||
}
|
||||
|
||||
interface InjectorTreeVisualizerConfig {
|
||||
orientation: 'horizontal'|'vertical';
|
||||
orientation: 'horizontal' | 'vertical';
|
||||
nodeSize: [width: number, height: number];
|
||||
nodeSeparation: (nodeA: InjectorTreeD3Node, nodeB: InjectorTreeD3Node) => number;
|
||||
nodeLabelSize: [width: number, height: number];
|
||||
|
|
@ -69,14 +68,14 @@ export class InjectorTreeVisualizer extends GraphRenderer<InjectorTreeNode, Inje
|
|||
public config: InjectorTreeVisualizerConfig;
|
||||
|
||||
constructor(
|
||||
private _containerElement: HTMLElement,
|
||||
private _graphElement: HTMLElement,
|
||||
{
|
||||
orientation = 'horizontal',
|
||||
nodeSize = [200, 500],
|
||||
nodeSeparation = () => 2,
|
||||
nodeLabelSize = [250, 60],
|
||||
}: Partial<InjectorTreeVisualizerConfig> = {},
|
||||
private _containerElement: HTMLElement,
|
||||
private _graphElement: HTMLElement,
|
||||
{
|
||||
orientation = 'horizontal',
|
||||
nodeSize = [200, 500],
|
||||
nodeSeparation = () => 2,
|
||||
nodeLabelSize = [250, 60],
|
||||
}: Partial<InjectorTreeVisualizerConfig> = {},
|
||||
) {
|
||||
super();
|
||||
|
||||
|
|
@ -90,13 +89,15 @@ export class InjectorTreeVisualizer extends GraphRenderer<InjectorTreeNode, Inje
|
|||
|
||||
private d3 = d3;
|
||||
|
||||
override root: InjectorTreeD3Node|null = null;
|
||||
zoomController: d3.ZoomBehavior<HTMLElement, unknown>|null = null;
|
||||
override root: InjectorTreeD3Node | null = null;
|
||||
zoomController: d3.ZoomBehavior<HTMLElement, unknown> | null = null;
|
||||
|
||||
override zoomScale(scale: number) {
|
||||
if (this.zoomController) {
|
||||
this.zoomController.scaleTo(
|
||||
this.d3.select<HTMLElement, unknown>(this._containerElement), scale);
|
||||
this.d3.select<HTMLElement, unknown>(this._containerElement),
|
||||
scale,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -108,8 +109,8 @@ export class InjectorTreeVisualizer extends GraphRenderer<InjectorTreeNode, Inje
|
|||
|
||||
override snapToNode(node: InjectorTreeD3Node, scale = 1): void {
|
||||
const svg = this.d3.select(this._containerElement);
|
||||
const halfWidth = (this._containerElement.clientWidth / 2);
|
||||
const halfHeight = (this._containerElement.clientHeight / 2);
|
||||
const halfWidth = this._containerElement.clientWidth / 2;
|
||||
const halfHeight = this._containerElement.clientHeight / 2;
|
||||
const t = d3.zoomIdentity.translate(halfWidth - node.y, halfHeight - node.x).scale(scale);
|
||||
svg.transition().duration(500).call(this.zoomController!.transform, t);
|
||||
}
|
||||
|
|
@ -118,9 +119,10 @@ export class InjectorTreeVisualizer extends GraphRenderer<InjectorTreeNode, Inje
|
|||
return this._graphElement;
|
||||
}
|
||||
|
||||
override getNodeById(id: string): InjectorTreeD3Node|null {
|
||||
const selection = this.d3.select<HTMLElement, InjectorTreeD3Node>(this._containerElement)
|
||||
.select(`.node[data-id="${id}"]`);
|
||||
override getNodeById(id: string): InjectorTreeD3Node | null {
|
||||
const selection = this.d3
|
||||
.select<HTMLElement, InjectorTreeD3Node>(this._containerElement)
|
||||
.select(`.node[data-id="${id}"]`);
|
||||
if (selection.empty()) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -157,142 +159,128 @@ export class InjectorTreeVisualizer extends GraphRenderer<InjectorTreeNode, Inje
|
|||
this.root = nodes;
|
||||
|
||||
arrowDefId++;
|
||||
svg.append('svg:defs')
|
||||
.selectAll('marker')
|
||||
.data([`end${arrowDefId}`]) // Different link/path types can be defined here
|
||||
.enter()
|
||||
.append('svg:marker') // This section adds in the arrows
|
||||
.attr('id', String)
|
||||
.attr('viewBox', '0 -5 10 10')
|
||||
.attr('refX', 15)
|
||||
.attr('refY', 0)
|
||||
.attr('class', 'arrow')
|
||||
.attr('markerWidth', 6)
|
||||
.attr('markerHeight', 6)
|
||||
.attr('orient', 'auto')
|
||||
.append('svg:path')
|
||||
.attr('d', 'M0,-5L10,0L0,5');
|
||||
svg
|
||||
.append('svg:defs')
|
||||
.selectAll('marker')
|
||||
.data([`end${arrowDefId}`]) // Different link/path types can be defined here
|
||||
.enter()
|
||||
.append('svg:marker') // This section adds in the arrows
|
||||
.attr('id', String)
|
||||
.attr('viewBox', '0 -5 10 10')
|
||||
.attr('refX', 15)
|
||||
.attr('refY', 0)
|
||||
.attr('class', 'arrow')
|
||||
.attr('markerWidth', 6)
|
||||
.attr('markerHeight', 6)
|
||||
.attr('orient', 'auto')
|
||||
.append('svg:path')
|
||||
.attr('d', 'M0,-5L10,0L0,5');
|
||||
|
||||
g.selectAll('.link')
|
||||
.data(nodes.descendants().slice(1))
|
||||
.enter()
|
||||
.append('path')
|
||||
.attr(
|
||||
'class',
|
||||
(node: InjectorTreeD3Node) => {
|
||||
const parentId = node.parent?.data?.injector?.id;
|
||||
if (parentId === 'N/A') {
|
||||
return 'link-hidden';
|
||||
}
|
||||
.data(nodes.descendants().slice(1))
|
||||
.enter()
|
||||
.append('path')
|
||||
.attr('class', (node: InjectorTreeD3Node) => {
|
||||
const parentId = node.parent?.data?.injector?.id;
|
||||
if (parentId === 'N/A') {
|
||||
return 'link-hidden';
|
||||
}
|
||||
|
||||
return `link`;
|
||||
})
|
||||
.attr(
|
||||
'data-id',
|
||||
(node: InjectorTreeD3Node) => {
|
||||
const from = node.data.injector.id;
|
||||
const to = node.parent?.data?.injector?.id;
|
||||
return `link`;
|
||||
})
|
||||
.attr('data-id', (node: InjectorTreeD3Node) => {
|
||||
const from = node.data.injector.id;
|
||||
const to = node.parent?.data?.injector?.id;
|
||||
|
||||
if (from && to) {
|
||||
return `${from}-to-${to}`;
|
||||
}
|
||||
return '';
|
||||
})
|
||||
.attr('marker-end', `url(#end${arrowDefId})`)
|
||||
.attr('d', (node: InjectorTreeD3Node) => {
|
||||
const parent = node.parent!;
|
||||
if (this.config.orientation === 'horizontal') {
|
||||
return `
|
||||
if (from && to) {
|
||||
return `${from}-to-${to}`;
|
||||
}
|
||||
return '';
|
||||
})
|
||||
.attr('marker-end', `url(#end${arrowDefId})`)
|
||||
.attr('d', (node: InjectorTreeD3Node) => {
|
||||
const parent = node.parent!;
|
||||
if (this.config.orientation === 'horizontal') {
|
||||
return `
|
||||
M${node.y},${node.x}
|
||||
C${(node.y + parent.y) / 2},
|
||||
${node.x} ${(node.y + parent.y) / 2},
|
||||
${parent.x} ${parent.y},
|
||||
${parent.x}`;
|
||||
}
|
||||
}
|
||||
|
||||
return `
|
||||
return `
|
||||
M${node.x},${node.y}
|
||||
C${(node.x + parent.x) / 2},
|
||||
${node.y} ${(node.x + parent.x) / 2},
|
||||
${parent.y} ${parent.x},
|
||||
${parent.y}`;
|
||||
});
|
||||
});
|
||||
|
||||
// Declare the nodes
|
||||
const node =
|
||||
g.selectAll('g.node')
|
||||
.data(nodes.descendants())
|
||||
.enter()
|
||||
.append('g')
|
||||
.attr(
|
||||
'class',
|
||||
(node: InjectorTreeD3Node) => {
|
||||
if (node.data.injector.id === 'N/A') {
|
||||
return 'node-hidden';
|
||||
}
|
||||
return `node`;
|
||||
})
|
||||
.attr(
|
||||
'data-component-id',
|
||||
(node: InjectorTreeD3Node) => {
|
||||
const injector = node.data.injector;
|
||||
if (injector.type === 'element') {
|
||||
return injector.node?.component?.id ?? -1;
|
||||
}
|
||||
return -1;
|
||||
})
|
||||
.attr(
|
||||
'data-id',
|
||||
(node: InjectorTreeD3Node) => {
|
||||
return node.data.injector.id;
|
||||
})
|
||||
.on('click',
|
||||
(pointerEvent: PointerEvent, node: InjectorTreeD3Node) => {
|
||||
this.nodeClickListeners.forEach(listener => listener(pointerEvent, node));
|
||||
})
|
||||
.on('mouseover',
|
||||
(pointerEvent: PointerEvent, node: InjectorTreeD3Node) => {
|
||||
this.nodeMouseoverListeners.forEach(listener => listener(pointerEvent, node));
|
||||
})
|
||||
.on('mouseout',
|
||||
(pointerEvent: PointerEvent, node: InjectorTreeD3Node) => {
|
||||
this.nodeMouseoutListeners.forEach(listener => listener(pointerEvent, node));
|
||||
})
|
||||
const node = g
|
||||
.selectAll('g.node')
|
||||
.data(nodes.descendants())
|
||||
.enter()
|
||||
.append('g')
|
||||
.attr('class', (node: InjectorTreeD3Node) => {
|
||||
if (node.data.injector.id === 'N/A') {
|
||||
return 'node-hidden';
|
||||
}
|
||||
return `node`;
|
||||
})
|
||||
.attr('data-component-id', (node: InjectorTreeD3Node) => {
|
||||
const injector = node.data.injector;
|
||||
if (injector.type === 'element') {
|
||||
return injector.node?.component?.id ?? -1;
|
||||
}
|
||||
return -1;
|
||||
})
|
||||
.attr('data-id', (node: InjectorTreeD3Node) => {
|
||||
return node.data.injector.id;
|
||||
})
|
||||
.on('click', (pointerEvent: PointerEvent, node: InjectorTreeD3Node) => {
|
||||
this.nodeClickListeners.forEach((listener) => listener(pointerEvent, node));
|
||||
})
|
||||
.on('mouseover', (pointerEvent: PointerEvent, node: InjectorTreeD3Node) => {
|
||||
this.nodeMouseoverListeners.forEach((listener) => listener(pointerEvent, node));
|
||||
})
|
||||
.on('mouseout', (pointerEvent: PointerEvent, node: InjectorTreeD3Node) => {
|
||||
this.nodeMouseoutListeners.forEach((listener) => listener(pointerEvent, node));
|
||||
})
|
||||
|
||||
.attr('transform', (node: InjectorTreeD3Node) => {
|
||||
if (this.config.orientation === 'horizontal') {
|
||||
return `translate(${node.y},${node.x})`;
|
||||
}
|
||||
.attr('transform', (node: InjectorTreeD3Node) => {
|
||||
if (this.config.orientation === 'horizontal') {
|
||||
return `translate(${node.y},${node.x})`;
|
||||
}
|
||||
|
||||
return `translate(${node.x},${node.y})`;
|
||||
});
|
||||
return `translate(${node.x},${node.y})`;
|
||||
});
|
||||
|
||||
const [width, height] = this.config.nodeLabelSize!;
|
||||
|
||||
node.append('foreignObject')
|
||||
.attr('width', width)
|
||||
.attr('height', height)
|
||||
.attr('x', -1 * (width - 10))
|
||||
.attr('y', -1 * (height / 2))
|
||||
.append('xhtml:div')
|
||||
.attr(
|
||||
'title',
|
||||
(node: InjectorTreeD3Node) => {
|
||||
return node.data.injector.name;
|
||||
})
|
||||
.attr(
|
||||
'class',
|
||||
(node: InjectorTreeD3Node) => {
|
||||
return [
|
||||
injectorTypeToClassMap.get(node.data?.injector?.type) ?? '', 'node-container'
|
||||
].join(' ');
|
||||
})
|
||||
.html((node: InjectorTreeD3Node) => {
|
||||
const label = node.data.injector.name;
|
||||
const lengthLimit = 25;
|
||||
return label.length > lengthLimit ? label.slice(0, lengthLimit - '...'.length) + '...' :
|
||||
label;
|
||||
});
|
||||
node
|
||||
.append('foreignObject')
|
||||
.attr('width', width)
|
||||
.attr('height', height)
|
||||
.attr('x', -1 * (width - 10))
|
||||
.attr('y', -1 * (height / 2))
|
||||
.append('xhtml:div')
|
||||
.attr('title', (node: InjectorTreeD3Node) => {
|
||||
return node.data.injector.name;
|
||||
})
|
||||
.attr('class', (node: InjectorTreeD3Node) => {
|
||||
return [injectorTypeToClassMap.get(node.data?.injector?.type) ?? '', 'node-container'].join(
|
||||
' ',
|
||||
);
|
||||
})
|
||||
.html((node: InjectorTreeD3Node) => {
|
||||
const label = node.data.injector.name;
|
||||
const lengthLimit = 25;
|
||||
return label.length > lengthLimit
|
||||
? label.slice(0, lengthLimit - '...'.length) + '...'
|
||||
: label;
|
||||
});
|
||||
|
||||
svg.attr('height', '100%').attr('width', '100%');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,20 +14,26 @@ import {InjectorTreeNode, InjectorTreeVisualizer} from './injector-tree-visualiz
|
|||
@Component({
|
||||
selector: 'ng-resolution-path',
|
||||
template: `
|
||||
<section class="injector-graph">
|
||||
<svg #svgContainer>
|
||||
<g #mainGroup></g>
|
||||
</svg>
|
||||
</section>
|
||||
<section class="injector-graph">
|
||||
<svg #svgContainer>
|
||||
<g #mainGroup></g>
|
||||
</svg>
|
||||
</section>
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
`,
|
||||
styles: [`:host { display: block; }`],
|
||||
standalone: true
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
export class ResolutionPathComponent implements OnDestroy, AfterViewInit {
|
||||
@ViewChild('svgContainer', {static: true}) private svgContainer!: ElementRef;
|
||||
@ViewChild('mainGroup', {static: true}) private g!: ElementRef;
|
||||
|
||||
@Input() orientation: 'horizontal'|'vertical' = 'horizontal';
|
||||
@Input() orientation: 'horizontal' | 'vertical' = 'horizontal';
|
||||
|
||||
private injectorTree!: InjectorTreeVisualizer;
|
||||
private pathNode!: InjectorTreeNode;
|
||||
|
|
@ -56,10 +62,13 @@ export class ResolutionPathComponent implements OnDestroy, AfterViewInit {
|
|||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.injectorTree =
|
||||
new InjectorTreeVisualizer(this.svgContainer.nativeElement, this.g.nativeElement, {
|
||||
orientation: this.orientation,
|
||||
});
|
||||
this.injectorTree = new InjectorTreeVisualizer(
|
||||
this.svgContainer.nativeElement,
|
||||
this.g.nativeElement,
|
||||
{
|
||||
orientation: this.orientation,
|
||||
},
|
||||
);
|
||||
|
||||
if (this.pathNode) {
|
||||
this.injectorTree.render(this.pathNode);
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import {Theme, ThemeService} from '../theme-service';
|
|||
import {DirectiveExplorerComponent} from './directive-explorer/directive-explorer.component';
|
||||
import {TabUpdate} from './tab-update/index';
|
||||
|
||||
type Tabs = 'Components'|'Profiler'|'Router Tree'|'Injector Tree';
|
||||
type Tabs = 'Components' | 'Profiler' | 'Router Tree' | 'Injector Tree';
|
||||
|
||||
@Component({
|
||||
selector: 'ng-devtools-tabs',
|
||||
|
|
@ -25,7 +25,7 @@ type Tabs = 'Components'|'Profiler'|'Router Tree'|'Injector Tree';
|
|||
styleUrls: ['./devtools-tabs.component.scss'],
|
||||
})
|
||||
export class DevToolsTabsComponent implements OnInit, AfterViewInit {
|
||||
@Input() angularVersion: string|undefined = undefined;
|
||||
@Input() angularVersion: string | undefined = undefined;
|
||||
@ViewChild(DirectiveExplorerComponent) directiveExplorer!: DirectiveExplorerComponent;
|
||||
@ViewChild('navBar', {static: true}) navbar!: MatTabNav;
|
||||
|
||||
|
|
@ -41,13 +41,14 @@ export class DevToolsTabsComponent implements OnInit, AfterViewInit {
|
|||
routes: Route[] = [];
|
||||
|
||||
constructor(
|
||||
public tabUpdate: TabUpdate,
|
||||
public themeService: ThemeService,
|
||||
private _messageBus: MessageBus<Events>,
|
||||
private _applicationEnvironment: ApplicationEnvironment,
|
||||
public tabUpdate: TabUpdate,
|
||||
public themeService: ThemeService,
|
||||
private _messageBus: MessageBus<Events>,
|
||||
private _applicationEnvironment: ApplicationEnvironment,
|
||||
) {
|
||||
this.themeService.currentTheme.pipe(takeUntilDestroyed())
|
||||
.subscribe((theme) => (this.currentTheme = theme));
|
||||
this.themeService.currentTheme
|
||||
.pipe(takeUntilDestroyed())
|
||||
.subscribe((theme) => (this.currentTheme = theme));
|
||||
|
||||
this._messageBus.on('updateRouterTree', (routes) => {
|
||||
this.routes = routes || [];
|
||||
|
|
@ -99,7 +100,8 @@ export class DevToolsTabsComponent implements OnInit, AfterViewInit {
|
|||
|
||||
toggleTimingAPI(): void {
|
||||
this.timingAPIEnabled = !this.timingAPIEnabled;
|
||||
this.timingAPIEnabled ? this._messageBus.emit('enableTimingAPI') :
|
||||
this._messageBus.emit('disableTimingAPI');
|
||||
this.timingAPIEnabled
|
||||
? this._messageBus.emit('enableTimingAPI')
|
||||
: this._messageBus.emit('disableTimingAPI');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,12 +25,19 @@ import {TabUpdate} from './tab-update/index';
|
|||
@NgModule({
|
||||
declarations: [DevToolsTabsComponent],
|
||||
imports: [
|
||||
MatTabsModule, MatIconModule, DirectiveExplorerModule, ProfilerModule, RouterTreeModule,
|
||||
CommonModule, MatMenuModule, MatButtonModule, MatSlideToggleModule, MatTooltipModule,
|
||||
InjectorTreeComponent
|
||||
MatTabsModule,
|
||||
MatIconModule,
|
||||
DirectiveExplorerModule,
|
||||
ProfilerModule,
|
||||
RouterTreeModule,
|
||||
CommonModule,
|
||||
MatMenuModule,
|
||||
MatButtonModule,
|
||||
MatSlideToggleModule,
|
||||
MatTooltipModule,
|
||||
InjectorTreeComponent,
|
||||
],
|
||||
providers: [TabUpdate],
|
||||
exports: [DevToolsTabsComponent],
|
||||
})
|
||||
export class DevToolsTabModule {
|
||||
}
|
||||
export class DevToolsTabModule {}
|
||||
|
|
|
|||
|
|
@ -24,8 +24,7 @@ import {TabUpdate} from './tab-update/index';
|
|||
selector: 'ng-directive-explorer',
|
||||
template: '',
|
||||
})
|
||||
export class MockDirectiveExplorerComponent {
|
||||
}
|
||||
export class MockDirectiveExplorerComponent {}
|
||||
|
||||
describe('DevtoolsTabsComponent', () => {
|
||||
let messageBusMock: MessageBus<Events>;
|
||||
|
|
|
|||
|
|
@ -14,79 +14,82 @@ export interface MovedRecord {
|
|||
previousIndex: number;
|
||||
}
|
||||
|
||||
export const diff = <T>(differ: DefaultIterableDiffer<T>, a: T[], b: T[]):
|
||||
{newItems: T[]; removedItems: T[]; movedItems: T[];} => {
|
||||
differ.diff(a);
|
||||
differ.diff(b);
|
||||
export const diff = <T>(
|
||||
differ: DefaultIterableDiffer<T>,
|
||||
a: T[],
|
||||
b: T[],
|
||||
): {newItems: T[]; removedItems: T[]; movedItems: T[]} => {
|
||||
differ.diff(a);
|
||||
differ.diff(b);
|
||||
|
||||
const alreadySet: boolean[] = [];
|
||||
const movedItems: T[] = [];
|
||||
const alreadySet: boolean[] = [];
|
||||
const movedItems: T[] = [];
|
||||
|
||||
// We first have to set the moved items to their correct positions.
|
||||
// Keep in mind that the track by function may not guarantee
|
||||
// that we haven't changed any of the items' props.
|
||||
differ.forEachMovedItem(record => {
|
||||
if (record.currentIndex === null) {
|
||||
return;
|
||||
}
|
||||
if (record.previousIndex === null) {
|
||||
return;
|
||||
}
|
||||
// We want to preserve the reference so that a default
|
||||
// track by function used by the CDK, for instance, can
|
||||
// recognize that this item's identity hasn't changed.
|
||||
// At the same time, since we don't have the guarantee
|
||||
// that we haven't already set the previousIndex while
|
||||
// iterating, we need to check that. If we have, we assign
|
||||
// this array item to a new object. We don't want to risk
|
||||
// changing the properties of an object we'll use in the future.
|
||||
if (!alreadySet[record.previousIndex]) {
|
||||
a[record.currentIndex] = a[record.previousIndex];
|
||||
} else {
|
||||
a[record.currentIndex] = {} as unknown as T;
|
||||
}
|
||||
Object.keys(b[record.currentIndex] as unknown as {}).forEach(prop => {
|
||||
// TypeScript's type inference didn't follow the check from above.
|
||||
if (record.currentIndex === null) {
|
||||
return;
|
||||
}
|
||||
(a[record.currentIndex] as any)[prop] = (b[record.currentIndex] as any)[prop];
|
||||
});
|
||||
if (!alreadySet[record.previousIndex]) {
|
||||
// tslint:disable-next-line: no-non-null-assertion
|
||||
a[record.previousIndex] = null!;
|
||||
}
|
||||
alreadySet[record.currentIndex] = true;
|
||||
movedItems.push(a[record.currentIndex]);
|
||||
});
|
||||
|
||||
// Now we can set the new items and remove the deleted ones.
|
||||
const newItems: T[] = [];
|
||||
const removedItems: T[] = [];
|
||||
differ.forEachAddedItem(record => {
|
||||
if (record.currentIndex !== null && record.previousIndex === null) {
|
||||
a[record.currentIndex] = record.item;
|
||||
alreadySet[record.currentIndex] = true;
|
||||
newItems.push(record.item);
|
||||
}
|
||||
});
|
||||
|
||||
differ.forEachRemovedItem(record => {
|
||||
if (record.previousIndex === null) {
|
||||
return;
|
||||
}
|
||||
if (record.currentIndex === null && !alreadySet[record.previousIndex]) {
|
||||
// tslint:disable-next-line: no-non-null-assertion
|
||||
a[record.previousIndex] = null!;
|
||||
}
|
||||
removedItems.push(record.item);
|
||||
});
|
||||
|
||||
for (let i = a.length - 1; i >= 0; i--) {
|
||||
if (a[i] === null) {
|
||||
a.splice(i, 1);
|
||||
}
|
||||
// We first have to set the moved items to their correct positions.
|
||||
// Keep in mind that the track by function may not guarantee
|
||||
// that we haven't changed any of the items' props.
|
||||
differ.forEachMovedItem((record) => {
|
||||
if (record.currentIndex === null) {
|
||||
return;
|
||||
}
|
||||
if (record.previousIndex === null) {
|
||||
return;
|
||||
}
|
||||
// We want to preserve the reference so that a default
|
||||
// track by function used by the CDK, for instance, can
|
||||
// recognize that this item's identity hasn't changed.
|
||||
// At the same time, since we don't have the guarantee
|
||||
// that we haven't already set the previousIndex while
|
||||
// iterating, we need to check that. If we have, we assign
|
||||
// this array item to a new object. We don't want to risk
|
||||
// changing the properties of an object we'll use in the future.
|
||||
if (!alreadySet[record.previousIndex]) {
|
||||
a[record.currentIndex] = a[record.previousIndex];
|
||||
} else {
|
||||
a[record.currentIndex] = {} as unknown as T;
|
||||
}
|
||||
Object.keys(b[record.currentIndex] as unknown as {}).forEach((prop) => {
|
||||
// TypeScript's type inference didn't follow the check from above.
|
||||
if (record.currentIndex === null) {
|
||||
return;
|
||||
}
|
||||
(a[record.currentIndex] as any)[prop] = (b[record.currentIndex] as any)[prop];
|
||||
});
|
||||
if (!alreadySet[record.previousIndex]) {
|
||||
// tslint:disable-next-line: no-non-null-assertion
|
||||
a[record.previousIndex] = null!;
|
||||
}
|
||||
alreadySet[record.currentIndex] = true;
|
||||
movedItems.push(a[record.currentIndex]);
|
||||
});
|
||||
|
||||
return {newItems, removedItems, movedItems};
|
||||
};
|
||||
// Now we can set the new items and remove the deleted ones.
|
||||
const newItems: T[] = [];
|
||||
const removedItems: T[] = [];
|
||||
differ.forEachAddedItem((record) => {
|
||||
if (record.currentIndex !== null && record.previousIndex === null) {
|
||||
a[record.currentIndex] = record.item;
|
||||
alreadySet[record.currentIndex] = true;
|
||||
newItems.push(record.item);
|
||||
}
|
||||
});
|
||||
|
||||
differ.forEachRemovedItem((record) => {
|
||||
if (record.previousIndex === null) {
|
||||
return;
|
||||
}
|
||||
if (record.currentIndex === null && !alreadySet[record.previousIndex]) {
|
||||
// tslint:disable-next-line: no-non-null-assertion
|
||||
a[record.previousIndex] = null!;
|
||||
}
|
||||
removedItems.push(record.item);
|
||||
});
|
||||
|
||||
for (let i = a.length - 1; i >= 0; i--) {
|
||||
if (a[i] === null) {
|
||||
a.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
return {newItems, removedItems, movedItems};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,8 +6,29 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {ChangeDetectorRef, Component, ElementRef, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output, ViewChild,} from '@angular/core';
|
||||
import {ComponentExplorerView, ComponentExplorerViewQuery, DevToolsNode, DirectivePosition, ElementPosition, Events, MessageBus, PropertyQuery, PropertyQueryTypes,} from 'protocol';
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
Input,
|
||||
NgZone,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
ComponentExplorerView,
|
||||
ComponentExplorerViewQuery,
|
||||
DevToolsNode,
|
||||
DirectivePosition,
|
||||
ElementPosition,
|
||||
Events,
|
||||
MessageBus,
|
||||
PropertyQuery,
|
||||
PropertyQueryTypes,
|
||||
} from 'protocol';
|
||||
|
||||
import {SplitComponent} from '../../../lib/vendor/angular-split/public_api';
|
||||
import {ApplicationOperations} from '../../application-operations/index';
|
||||
|
|
@ -17,7 +38,10 @@ import {FlatNode} from './directive-forest/component-data-source';
|
|||
import {DirectiveForestComponent} from './directive-forest/directive-forest.component';
|
||||
import {IndexedNode} from './directive-forest/index-forest';
|
||||
import {constructPathOfKeysToPropertyValue} from './property-resolver/directive-property-resolver';
|
||||
import {ElementPropertyResolver, FlatNode as PropertyFlatNode} from './property-resolver/element-property-resolver';
|
||||
import {
|
||||
ElementPropertyResolver,
|
||||
FlatNode as PropertyFlatNode,
|
||||
} from './property-resolver/element-property-resolver';
|
||||
|
||||
const sameDirectives = (a: IndexedNode, b: IndexedNode) => {
|
||||
if ((a.component && !b.component) || (!a.component && b.component)) {
|
||||
|
|
@ -56,32 +80,37 @@ export class DirectiveExplorerComponent implements OnInit, OnDestroy {
|
|||
@ViewChild('directiveForestSplitArea', {static: true, read: ElementRef})
|
||||
directiveForestSplitArea!: ElementRef;
|
||||
|
||||
currentSelectedElement: IndexedNode|null = null;
|
||||
currentSelectedElement: IndexedNode | null = null;
|
||||
forest!: DevToolsNode[];
|
||||
splitDirection: 'horizontal'|'vertical' = 'horizontal';
|
||||
parents: FlatNode[]|null = null;
|
||||
splitDirection: 'horizontal' | 'vertical' = 'horizontal';
|
||||
parents: FlatNode[] | null = null;
|
||||
|
||||
private _resizeObserver = new ResizeObserver((entries) => this._ngZone.run(() => {
|
||||
const resizedEntry = entries[0];
|
||||
private _resizeObserver = new ResizeObserver((entries) =>
|
||||
this._ngZone.run(() => {
|
||||
const resizedEntry = entries[0];
|
||||
|
||||
if (resizedEntry.target === this.splitElementRef.nativeElement) {
|
||||
this.splitDirection = resizedEntry.contentRect.width <= 500 ? 'vertical' : 'horizontal';
|
||||
}
|
||||
if (resizedEntry.target === this.splitElementRef.nativeElement) {
|
||||
this.splitDirection = resizedEntry.contentRect.width <= 500 ? 'vertical' : 'horizontal';
|
||||
}
|
||||
|
||||
if (!this.breadcrumbs) {
|
||||
return;
|
||||
}
|
||||
if (!this.breadcrumbs) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.breadcrumbs.updateScrollButtonVisibility();
|
||||
}));
|
||||
this.breadcrumbs.updateScrollButtonVisibility();
|
||||
}),
|
||||
);
|
||||
|
||||
private _clickedElement: IndexedNode|null = null;
|
||||
private _refreshRetryTimeout: null|ReturnType<typeof setTimeout> = null;
|
||||
private _clickedElement: IndexedNode | null = null;
|
||||
private _refreshRetryTimeout: null | ReturnType<typeof setTimeout> = null;
|
||||
|
||||
constructor(
|
||||
private _appOperations: ApplicationOperations, private _messageBus: MessageBus<Events>,
|
||||
private _propResolver: ElementPropertyResolver, private _cdr: ChangeDetectorRef,
|
||||
private _ngZone: NgZone) {}
|
||||
private _appOperations: ApplicationOperations,
|
||||
private _messageBus: MessageBus<Events>,
|
||||
private _propResolver: ElementPropertyResolver,
|
||||
private _cdr: ChangeDetectorRef,
|
||||
private _ngZone: NgZone,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.subscribeToBackendEvents();
|
||||
|
|
@ -95,7 +124,7 @@ export class DirectiveExplorerComponent implements OnInit, OnDestroy {
|
|||
this._resizeObserver.unobserve(this.directiveForestSplitArea.nativeElement);
|
||||
}
|
||||
|
||||
handleNodeSelection(node: IndexedNode|null): void {
|
||||
handleNodeSelection(node: IndexedNode | null): void {
|
||||
if (node) {
|
||||
// We want to guarantee that we're not reusing any of the previous properties.
|
||||
// That's possible if the user has selected an NgForOf and after that
|
||||
|
|
@ -125,8 +154,9 @@ export class DirectiveExplorerComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
refresh(): void {
|
||||
const success =
|
||||
this._messageBus.emit('getLatestComponentExplorerView', [this._constructViewQuery()]);
|
||||
const success = this._messageBus.emit('getLatestComponentExplorerView', [
|
||||
this._constructViewQuery(),
|
||||
]);
|
||||
// If the event was not throttled, we no longer need to retry.
|
||||
if (success) {
|
||||
this._refreshRetryTimeout && clearTimeout(this._refreshRetryTimeout);
|
||||
|
|
@ -147,7 +177,8 @@ export class DirectiveExplorerComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
const directiveIndex = this.currentSelectedElement.directives.findIndex(
|
||||
directive => directive.name === directiveName);
|
||||
(directive) => directive.name === directiveName,
|
||||
);
|
||||
|
||||
if (directiveIndex === -1) {
|
||||
// view the component definition
|
||||
|
|
@ -174,7 +205,7 @@ export class DirectiveExplorerComponent implements OnInit, OnDestroy {
|
|||
this._messageBus.emit('removeHighlightOverlay');
|
||||
}
|
||||
|
||||
private _constructViewQuery(): ComponentExplorerViewQuery|undefined {
|
||||
private _constructViewQuery(): ComponentExplorerViewQuery | undefined {
|
||||
if (!this._clickedElement) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -189,8 +220,11 @@ export class DirectiveExplorerComponent implements OnInit, OnDestroy {
|
|||
// We check if we're dealing with the same instance (i.e., if we have the same
|
||||
// set of directives and component on it), if we do, we want to get the same
|
||||
// set of properties which are already expanded.
|
||||
if (!this._clickedElement || !this.currentSelectedElement ||
|
||||
!sameDirectives(this._clickedElement, this.currentSelectedElement)) {
|
||||
if (
|
||||
!this._clickedElement ||
|
||||
!this.currentSelectedElement ||
|
||||
!sameDirectives(this._clickedElement, this.currentSelectedElement)
|
||||
) {
|
||||
return {
|
||||
type: PropertyQueryTypes.All,
|
||||
};
|
||||
|
|
@ -213,13 +247,18 @@ export class DirectiveExplorerComponent implements OnInit, OnDestroy {
|
|||
this.directiveForest.handleSelect(node);
|
||||
}
|
||||
|
||||
handleSetParents(parents: FlatNode[]|null): void {
|
||||
handleSetParents(parents: FlatNode[] | null): void {
|
||||
this.parents = parents;
|
||||
this._cdr.detectChanges();
|
||||
}
|
||||
|
||||
inspect({node, directivePosition}:
|
||||
{node: PropertyFlatNode; directivePosition: DirectivePosition}): void {
|
||||
inspect({
|
||||
node,
|
||||
directivePosition,
|
||||
}: {
|
||||
node: PropertyFlatNode;
|
||||
directivePosition: DirectivePosition;
|
||||
}): void {
|
||||
const objectPath = constructPathOfKeysToPropertyValue(node.prop);
|
||||
this._appOperations.inspect(directivePosition, objectPath);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,5 +39,4 @@ import {PropertyTabModule} from './property-tab/property-tab.module';
|
|||
MatTooltipModule,
|
||||
],
|
||||
})
|
||||
export class DirectiveExplorerModule {
|
||||
}
|
||||
export class DirectiveExplorerModule {}
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@ import {PropertyQueryTypes} from 'protocol';
|
|||
|
||||
import {DirectiveExplorerComponent} from './directive-explorer.component';
|
||||
import {IndexedNode} from './directive-forest/index-forest';
|
||||
import {ElementPropertyResolver} from './property-resolver/element-property-resolver';
|
||||
|
||||
import SpyObj = jasmine.SpyObj;
|
||||
import {ElementPropertyResolver} from './property-resolver/element-property-resolver';
|
||||
|
||||
describe('DirectiveExplorerComponent', () => {
|
||||
let messageBusMock: any;
|
||||
|
|
@ -22,14 +22,20 @@ describe('DirectiveExplorerComponent', () => {
|
|||
let ngZone: any;
|
||||
|
||||
beforeEach(() => {
|
||||
applicationOperationsSpy =
|
||||
jasmine.createSpyObj('_appOperations', ['viewSource', 'selectDomElement']);
|
||||
applicationOperationsSpy = jasmine.createSpyObj('_appOperations', [
|
||||
'viewSource',
|
||||
'selectDomElement',
|
||||
]);
|
||||
messageBusMock = jasmine.createSpyObj('messageBus', ['on', 'once', 'emit', 'destroy']);
|
||||
cdr = jasmine.createSpyObj('_cdr', ['detectChanges']);
|
||||
ngZone = jasmine.createSpyObj('_ngZone', ['run']);
|
||||
comp = new DirectiveExplorerComponent(
|
||||
applicationOperationsSpy, messageBusMock, new ElementPropertyResolver(messageBusMock), cdr,
|
||||
ngZone);
|
||||
applicationOperationsSpy,
|
||||
messageBusMock,
|
||||
new ElementPropertyResolver(messageBusMock),
|
||||
cdr,
|
||||
ngZone,
|
||||
);
|
||||
});
|
||||
|
||||
it('should create instance from class', () => {
|
||||
|
|
@ -39,8 +45,10 @@ describe('DirectiveExplorerComponent', () => {
|
|||
it('subscribe to backend events', () => {
|
||||
comp.subscribeToBackendEvents();
|
||||
expect(messageBusMock.on).toHaveBeenCalledTimes(2);
|
||||
expect(messageBusMock.on)
|
||||
.toHaveBeenCalledWith('latestComponentExplorerView', jasmine.any(Function));
|
||||
expect(messageBusMock.on).toHaveBeenCalledWith(
|
||||
'latestComponentExplorerView',
|
||||
jasmine.any(Function),
|
||||
);
|
||||
expect(messageBusMock.on).toHaveBeenCalledWith('componentTreeDirty', jasmine.any(Function));
|
||||
});
|
||||
|
||||
|
|
@ -53,23 +61,24 @@ describe('DirectiveExplorerComponent', () => {
|
|||
it('should emit getLatestComponentExplorerView event with null view query', () => {
|
||||
comp.refresh();
|
||||
expect(messageBusMock.emit).toHaveBeenCalledWith('getLatestComponentExplorerView', [
|
||||
undefined
|
||||
undefined,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should emit getLatestComponentExplorerView event on refresh with view query no properties',
|
||||
() => {
|
||||
const currentSelectedElement =
|
||||
jasmine.createSpyObj('currentSelectedElement', ['position', 'children']);
|
||||
currentSelectedElement.position = [0];
|
||||
currentSelectedElement.children = [];
|
||||
comp.currentSelectedElement = currentSelectedElement;
|
||||
comp.refresh();
|
||||
expect(comp.currentSelectedElement).toBeTruthy();
|
||||
expect(messageBusMock.emit).toHaveBeenCalledWith('getLatestComponentExplorerView', [
|
||||
undefined
|
||||
]);
|
||||
});
|
||||
it('should emit getLatestComponentExplorerView event on refresh with view query no properties', () => {
|
||||
const currentSelectedElement = jasmine.createSpyObj('currentSelectedElement', [
|
||||
'position',
|
||||
'children',
|
||||
]);
|
||||
currentSelectedElement.position = [0];
|
||||
currentSelectedElement.children = [];
|
||||
comp.currentSelectedElement = currentSelectedElement;
|
||||
comp.refresh();
|
||||
expect(comp.currentSelectedElement).toBeTruthy();
|
||||
expect(messageBusMock.emit).toHaveBeenCalledWith('getLatestComponentExplorerView', [
|
||||
undefined,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('node selection event', () => {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,18 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {AfterViewInit, Component, ElementRef, EventEmitter, HostListener, Input, OnChanges, OnInit, Output, ViewChild,} from '@angular/core';
|
||||
import {
|
||||
AfterViewInit,
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
HostListener,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnInit,
|
||||
Output,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import {Subject} from 'rxjs';
|
||||
import {debounceTime} from 'rxjs/operators';
|
||||
|
||||
|
|
@ -31,8 +42,9 @@ export class BreadcrumbsComponent implements OnInit, AfterViewInit, OnChanges {
|
|||
updateScrollButtonVisibility$ = new Subject<void>();
|
||||
|
||||
ngOnInit(): void {
|
||||
this.updateScrollButtonVisibility$.pipe(debounceTime(100))
|
||||
.subscribe(() => this.updateScrollButtonVisibility());
|
||||
this.updateScrollButtonVisibility$
|
||||
.pipe(debounceTime(100))
|
||||
.subscribe(() => this.updateScrollButtonVisibility());
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
|
|
|
|||
|
|
@ -19,5 +19,4 @@ import {BreadcrumbsComponent} from './breadcrumbs.component';
|
|||
imports: [CommonModule, MatCardModule, MatButtonModule, MatIconModule],
|
||||
exports: [BreadcrumbsComponent],
|
||||
})
|
||||
export class BreadcrumbsModule {
|
||||
}
|
||||
export class BreadcrumbsModule {}
|
||||
|
|
|
|||
|
|
@ -183,8 +183,10 @@ const tree4: DevToolsNode = {
|
|||
|
||||
describe('ComponentDataSource', () => {
|
||||
let dataSource: ComponentDataSource;
|
||||
const treeControl =
|
||||
new FlatTreeControl<FlatNode>((node) => node.level, (node) => node.expandable);
|
||||
const treeControl = new FlatTreeControl<FlatNode>(
|
||||
(node) => node.level,
|
||||
(node) => node.expandable,
|
||||
);
|
||||
|
||||
beforeEach(() => (dataSource = new ComponentDataSource(treeControl)));
|
||||
|
||||
|
|
|
|||
|
|
@ -32,16 +32,18 @@ export interface FlatNode {
|
|||
const expandable = (node: IndexedNode) => !!node.children && node.children.length > 0;
|
||||
|
||||
const trackBy: TrackByFunction<FlatNode> = (_: number, item: FlatNode) =>
|
||||
`${item.id}#${item.expandable}`;
|
||||
`${item.id}#${item.expandable}`;
|
||||
|
||||
const getId = (node: IndexedNode) => {
|
||||
let prefix = '';
|
||||
if (node.component) {
|
||||
prefix = node.component.id.toString();
|
||||
}
|
||||
const dirIds = node.directives.map((d) => d.id).sort((a, b) => {
|
||||
return a - b;
|
||||
});
|
||||
const dirIds = node.directives
|
||||
.map((d) => d.id)
|
||||
.sort((a, b) => {
|
||||
return a - b;
|
||||
});
|
||||
return prefix + '-' + dirIds.join('-');
|
||||
};
|
||||
|
||||
|
|
@ -75,28 +77,28 @@ export class ComponentDataSource extends DataSource<FlatNode> {
|
|||
private _nodeToFlat = new WeakMap<IndexedNode, FlatNode>();
|
||||
|
||||
private _treeFlattener = new MatTreeFlattener(
|
||||
(node: IndexedNode, level: number) => {
|
||||
if (this._nodeToFlat.has(node)) {
|
||||
return this._nodeToFlat.get(node);
|
||||
}
|
||||
const flatNode: FlatNode = {
|
||||
expandable: expandable(node),
|
||||
id: getId(node),
|
||||
// We can compare the nodes in the navigation functions above
|
||||
// based on this identifier directly, since it's a reference type
|
||||
// and the reference is preserved after transformation.
|
||||
position: node.position,
|
||||
name: node.component ? node.component.name : node.element,
|
||||
directives: node.directives.map((d) => d.name).join(', '),
|
||||
original: node,
|
||||
level,
|
||||
};
|
||||
this._nodeToFlat.set(node, flatNode);
|
||||
return flatNode;
|
||||
},
|
||||
(node) => (node ? node.level : -1),
|
||||
(node) => (node ? node.expandable : false),
|
||||
(node) => (node ? node.children : []),
|
||||
(node: IndexedNode, level: number) => {
|
||||
if (this._nodeToFlat.has(node)) {
|
||||
return this._nodeToFlat.get(node);
|
||||
}
|
||||
const flatNode: FlatNode = {
|
||||
expandable: expandable(node),
|
||||
id: getId(node),
|
||||
// We can compare the nodes in the navigation functions above
|
||||
// based on this identifier directly, since it's a reference type
|
||||
// and the reference is preserved after transformation.
|
||||
position: node.position,
|
||||
name: node.component ? node.component.name : node.element,
|
||||
directives: node.directives.map((d) => d.name).join(', '),
|
||||
original: node,
|
||||
level,
|
||||
};
|
||||
this._nodeToFlat.set(node, flatNode);
|
||||
return flatNode;
|
||||
},
|
||||
(node) => (node ? node.level : -1),
|
||||
(node) => (node ? node.expandable : false),
|
||||
(node) => (node ? node.children : []),
|
||||
);
|
||||
|
||||
constructor(private _treeControl: FlatTreeControl<FlatNode>) {
|
||||
|
|
@ -111,14 +113,14 @@ export class ComponentDataSource extends DataSource<FlatNode> {
|
|||
return this._expandedData.value;
|
||||
}
|
||||
|
||||
getFlatNodeFromIndexedNode(indexedNode: IndexedNode): FlatNode|undefined {
|
||||
getFlatNodeFromIndexedNode(indexedNode: IndexedNode): FlatNode | undefined {
|
||||
return this._nodeToFlat.get(indexedNode);
|
||||
}
|
||||
|
||||
update(
|
||||
forest: DevToolsNode[],
|
||||
showCommentNodes: boolean,
|
||||
): {newItems: FlatNode[]; movedItems: FlatNode[]; removedItems: FlatNode[]} {
|
||||
forest: DevToolsNode[],
|
||||
showCommentNodes: boolean,
|
||||
): {newItems: FlatNode[]; movedItems: FlatNode[]; removedItems: FlatNode[]} {
|
||||
if (!forest) {
|
||||
return {newItems: [], movedItems: [], removedItems: []};
|
||||
}
|
||||
|
|
@ -151,9 +153,9 @@ export class ComponentDataSource extends DataSource<FlatNode> {
|
|||
});
|
||||
|
||||
const {newItems, movedItems, removedItems} = diff<FlatNode>(
|
||||
this._differ,
|
||||
this.data,
|
||||
flattenedCollection,
|
||||
this._differ,
|
||||
this.data,
|
||||
flattenedCollection,
|
||||
);
|
||||
this._treeControl.dataNodes = this.data;
|
||||
this._flattenedData.next(this.data);
|
||||
|
|
@ -176,18 +178,17 @@ export class ComponentDataSource extends DataSource<FlatNode> {
|
|||
this._treeControl.expansionModel.changed,
|
||||
this._flattenedData,
|
||||
];
|
||||
return merge<unknown[]>(...changes)
|
||||
.pipe(
|
||||
map(() => {
|
||||
this._expandedData.next(
|
||||
this._treeFlattener.expandFlattenedNodes(
|
||||
this.data,
|
||||
this._treeControl as FlatTreeControl<FlatNode|undefined>,
|
||||
) as FlatNode[],
|
||||
);
|
||||
return this._expandedData.value;
|
||||
}),
|
||||
return merge<unknown[]>(...changes).pipe(
|
||||
map(() => {
|
||||
this._expandedData.next(
|
||||
this._treeFlattener.expandFlattenedNodes(
|
||||
this.data,
|
||||
this._treeControl as FlatTreeControl<FlatNode | undefined>,
|
||||
) as FlatNode[],
|
||||
);
|
||||
return this._expandedData.value;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
override disconnect(): void {}
|
||||
|
|
|
|||
|
|
@ -22,13 +22,16 @@ export const isChildOf = (childPosition: number[], parentPosition: number[]) =>
|
|||
return true;
|
||||
};
|
||||
|
||||
export const parentCollapsed =
|
||||
(nodeIdx: number, all: FlatNode[], treeControl: FlatTreeControl<FlatNode>) => {
|
||||
const node = all[nodeIdx];
|
||||
for (let i = nodeIdx - 1; i >= 0; i--) {
|
||||
if (isChildOf(node.position, all[i].position) && !treeControl.isExpanded(all[i])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
export const parentCollapsed = (
|
||||
nodeIdx: number,
|
||||
all: FlatNode[],
|
||||
treeControl: FlatTreeControl<FlatNode>,
|
||||
) => {
|
||||
const node = all[nodeIdx];
|
||||
for (let i = nodeIdx - 1; i >= 0; i--) {
|
||||
if (isChildOf(node.position, all[i].position) && !treeControl.isExpanded(all[i])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,7 +8,18 @@
|
|||
|
||||
import {CdkVirtualScrollViewport} from '@angular/cdk/scrolling';
|
||||
import {FlatTreeControl} from '@angular/cdk/tree';
|
||||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, HostListener, Input, OnDestroy, OnInit, Output, ViewChild,} from '@angular/core';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
EventEmitter,
|
||||
HostListener,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||
import {DevToolsNode, ElementPosition, Events, MessageBus} from 'protocol';
|
||||
|
||||
|
|
@ -30,7 +41,7 @@ export class DirectiveForestComponent {
|
|||
this._latestForest = forest;
|
||||
const result = this._updateForest(forest);
|
||||
const changed =
|
||||
result.movedItems.length || result.newItems.length || result.removedItems.length;
|
||||
result.movedItems.length || result.newItems.length || result.removedItems.length;
|
||||
if (this.currentSelectedElement && changed) {
|
||||
this._reselectNodeOnUpdate();
|
||||
}
|
||||
|
|
@ -42,9 +53,9 @@ export class DirectiveForestComponent {
|
|||
this.forest = this._latestForest;
|
||||
}
|
||||
|
||||
@Output() selectNode = new EventEmitter<IndexedNode|null>();
|
||||
@Output() selectNode = new EventEmitter<IndexedNode | null>();
|
||||
@Output() selectDomElement = new EventEmitter<IndexedNode>();
|
||||
@Output() setParents = new EventEmitter<FlatNode[]|null>();
|
||||
@Output() setParents = new EventEmitter<FlatNode[] | null>();
|
||||
@Output() highlightComponent = new EventEmitter<ElementPosition>();
|
||||
@Output() removeComponentHighlight = new EventEmitter<void>();
|
||||
@Output() toggleInspector = new EventEmitter<void>();
|
||||
|
|
@ -54,28 +65,32 @@ export class DirectiveForestComponent {
|
|||
filterRegex = new RegExp('.^');
|
||||
currentlyMatchedIndex = -1;
|
||||
|
||||
selectedNode: FlatNode|null = null;
|
||||
selectedNode: FlatNode | null = null;
|
||||
parents!: FlatNode[];
|
||||
|
||||
private _highlightIDinTreeFromElement: number|null = null;
|
||||
private _highlightIDinTreeFromElement: number | null = null;
|
||||
private _showCommentNodes = false;
|
||||
private _latestForest!: DevToolsNode[];
|
||||
|
||||
set highlightIDinTreeFromElement(id: number|null) {
|
||||
set highlightIDinTreeFromElement(id: number | null) {
|
||||
this._highlightIDinTreeFromElement = id;
|
||||
this._cdr.markForCheck();
|
||||
}
|
||||
|
||||
readonly treeControl =
|
||||
new FlatTreeControl<FlatNode>((node) => node!.level, (node) => node.expandable);
|
||||
readonly treeControl = new FlatTreeControl<FlatNode>(
|
||||
(node) => node!.level,
|
||||
(node) => node.expandable,
|
||||
);
|
||||
readonly dataSource = new ComponentDataSource(this.treeControl);
|
||||
readonly itemHeight = 18;
|
||||
|
||||
private _initialized = false;
|
||||
|
||||
constructor(
|
||||
private _tabUpdate: TabUpdate, private _messageBus: MessageBus<Events>,
|
||||
private _cdr: ChangeDetectorRef) {
|
||||
private _tabUpdate: TabUpdate,
|
||||
private _messageBus: MessageBus<Events>,
|
||||
private _cdr: ChangeDetectorRef,
|
||||
) {
|
||||
this.subscribeToInspectorEvents();
|
||||
this._tabUpdate.tabUpdate$.pipe(takeUntilDestroyed()).subscribe(() => {
|
||||
if (this.viewport) {
|
||||
|
|
@ -111,8 +126,9 @@ export class DirectiveForestComponent {
|
|||
}
|
||||
|
||||
handleSelect(node: FlatNode): void {
|
||||
this.currentlyMatchedIndex =
|
||||
this.dataSource.data.findIndex((matchedNode) => matchedNode.id === node.id);
|
||||
this.currentlyMatchedIndex = this.dataSource.data.findIndex(
|
||||
(matchedNode) => matchedNode.id === node.id,
|
||||
);
|
||||
this.selectAndEnsureVisible(node);
|
||||
}
|
||||
|
||||
|
|
@ -156,8 +172,9 @@ export class DirectiveForestComponent {
|
|||
}
|
||||
|
||||
private _reselectNodeOnUpdate(): void {
|
||||
const nodeThatStillExists =
|
||||
this.dataSource.getFlatNodeFromIndexedNode(this.currentSelectedElement);
|
||||
const nodeThatStillExists = this.dataSource.getFlatNodeFromIndexedNode(
|
||||
this.currentSelectedElement,
|
||||
);
|
||||
if (nodeThatStillExists) {
|
||||
this.select(nodeThatStillExists);
|
||||
} else {
|
||||
|
|
@ -165,8 +182,11 @@ export class DirectiveForestComponent {
|
|||
}
|
||||
}
|
||||
|
||||
private _updateForest(forest: DevToolsNode[]):
|
||||
{newItems: FlatNode[]; movedItems: FlatNode[]; removedItems: FlatNode[];} {
|
||||
private _updateForest(forest: DevToolsNode[]): {
|
||||
newItems: FlatNode[];
|
||||
movedItems: FlatNode[];
|
||||
removedItems: FlatNode[];
|
||||
} {
|
||||
const result = this.dataSource.update(forest, this._showCommentNodes);
|
||||
if (!this._initialized && forest && forest.length) {
|
||||
this.treeControl.expandAll();
|
||||
|
|
@ -184,8 +204,9 @@ export class DirectiveForestComponent {
|
|||
this.parents = [];
|
||||
for (let i = 1; i <= position.length; i++) {
|
||||
const current = position.slice(0, i);
|
||||
const selectedNode =
|
||||
this.dataSource.data.find((item) => item.position.toString() === current.toString());
|
||||
const selectedNode = this.dataSource.data.find(
|
||||
(item) => item.position.toString() === current.toString(),
|
||||
);
|
||||
|
||||
// We might not be able to find the parent if the user has hidden the comment nodes.
|
||||
if (selectedNode) {
|
||||
|
|
@ -279,8 +300,10 @@ export class DirectiveForestComponent {
|
|||
}
|
||||
|
||||
isMatched(node: FlatNode): boolean {
|
||||
return this.filterRegex.test(node.name.toLowerCase()) ||
|
||||
this.filterRegex.test(node.directives.toLowerCase());
|
||||
return (
|
||||
this.filterRegex.test(node.name.toLowerCase()) ||
|
||||
this.filterRegex.test(node.directives.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
handleFilter(filterText: string): void {
|
||||
|
|
@ -324,8 +347,9 @@ export class DirectiveForestComponent {
|
|||
|
||||
prevMatched(): void {
|
||||
const indexesOfMatchedNodes = this._findMatchedNodes();
|
||||
this.currentlyMatchedIndex = (this.currentlyMatchedIndex - 1 + indexesOfMatchedNodes.length) %
|
||||
indexesOfMatchedNodes.length;
|
||||
this.currentlyMatchedIndex =
|
||||
(this.currentlyMatchedIndex - 1 + indexesOfMatchedNodes.length) %
|
||||
indexesOfMatchedNodes.length;
|
||||
const indexToSelect = indexesOfMatchedNodes[this.currentlyMatchedIndex];
|
||||
const nodeToSelect = this.dataSource.data[indexToSelect];
|
||||
if (indexToSelect !== undefined) {
|
||||
|
|
@ -352,11 +376,13 @@ export class DirectiveForestComponent {
|
|||
}
|
||||
|
||||
isHighlighted(node: FlatNode): boolean {
|
||||
return !!this._highlightIDinTreeFromElement &&
|
||||
this._highlightIDinTreeFromElement === node.original.component?.id;
|
||||
return (
|
||||
!!this._highlightIDinTreeFromElement &&
|
||||
this._highlightIDinTreeFromElement === node.original.component?.id
|
||||
);
|
||||
}
|
||||
|
||||
isElement(node: FlatNode): boolean|null {
|
||||
isElement(node: FlatNode): boolean | null {
|
||||
return node.original.component && node.original.component.isElement;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,5 +30,4 @@ import {FilterModule} from './filter/filter.module';
|
|||
],
|
||||
exports: [DirectiveForestComponent, BreadcrumbsModule],
|
||||
})
|
||||
export class DirectiveForestModule {
|
||||
}
|
||||
export class DirectiveForestModule {}
|
||||
|
|
|
|||
|
|
@ -19,5 +19,4 @@ import {FilterComponent} from './filter.component';
|
|||
imports: [CommonModule, MatCardModule, MatIconModule, MatButtonModule],
|
||||
exports: [FilterComponent],
|
||||
})
|
||||
export class FilterModule {
|
||||
}
|
||||
export class FilterModule {}
|
||||
|
|
|
|||
|
|
@ -14,10 +14,83 @@ describe('indexForest', () => {
|
|||
});
|
||||
|
||||
it('should index a forest', () => {
|
||||
expect(indexForest([
|
||||
expect(
|
||||
indexForest([
|
||||
{
|
||||
element: 'Parent1',
|
||||
directives: [],
|
||||
component: {
|
||||
isElement: false,
|
||||
name: 'Cmp1',
|
||||
id: 1,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
element: 'Child1_1',
|
||||
directives: [
|
||||
{
|
||||
name: 'Dir1',
|
||||
id: 1,
|
||||
},
|
||||
{
|
||||
name: 'Dir2',
|
||||
id: 1,
|
||||
},
|
||||
],
|
||||
component: null,
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
element: 'Child1_2',
|
||||
directives: [],
|
||||
component: {
|
||||
isElement: false,
|
||||
name: 'Cmp2',
|
||||
id: 1,
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
element: 'Parent2',
|
||||
directives: [],
|
||||
component: null,
|
||||
children: [
|
||||
{
|
||||
element: 'Child2_1',
|
||||
directives: [
|
||||
{
|
||||
name: 'Dir3',
|
||||
id: 1,
|
||||
},
|
||||
],
|
||||
component: null,
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
element: 'Child2_2',
|
||||
directives: [
|
||||
{
|
||||
name: 'Dir4',
|
||||
id: 1,
|
||||
},
|
||||
{
|
||||
name: 'Dir5',
|
||||
id: 1,
|
||||
},
|
||||
],
|
||||
component: null,
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
]),
|
||||
).toEqual([
|
||||
{
|
||||
element: 'Parent1',
|
||||
directives: [],
|
||||
position: [0],
|
||||
component: {
|
||||
isElement: false,
|
||||
name: 'Cmp1',
|
||||
|
|
@ -26,6 +99,7 @@ describe('indexForest', () => {
|
|||
children: [
|
||||
{
|
||||
element: 'Child1_1',
|
||||
position: [0, 0],
|
||||
directives: [
|
||||
{
|
||||
name: 'Dir1',
|
||||
|
|
@ -42,6 +116,7 @@ describe('indexForest', () => {
|
|||
{
|
||||
element: 'Child1_2',
|
||||
directives: [],
|
||||
position: [0, 1],
|
||||
component: {
|
||||
isElement: false,
|
||||
name: 'Cmp2',
|
||||
|
|
@ -55,9 +130,11 @@ describe('indexForest', () => {
|
|||
element: 'Parent2',
|
||||
directives: [],
|
||||
component: null,
|
||||
position: [1],
|
||||
children: [
|
||||
{
|
||||
element: 'Child2_1',
|
||||
position: [1, 0],
|
||||
directives: [
|
||||
{
|
||||
name: 'Dir3',
|
||||
|
|
@ -69,6 +146,7 @@ describe('indexForest', () => {
|
|||
},
|
||||
{
|
||||
element: 'Child2_2',
|
||||
position: [1, 1],
|
||||
directives: [
|
||||
{
|
||||
name: 'Dir4',
|
||||
|
|
@ -84,83 +162,6 @@ describe('indexForest', () => {
|
|||
},
|
||||
],
|
||||
},
|
||||
]))
|
||||
.toEqual([
|
||||
{
|
||||
element: 'Parent1',
|
||||
directives: [],
|
||||
position: [0],
|
||||
component: {
|
||||
isElement: false,
|
||||
name: 'Cmp1',
|
||||
id: 1,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
element: 'Child1_1',
|
||||
position: [0, 0],
|
||||
directives: [
|
||||
{
|
||||
name: 'Dir1',
|
||||
id: 1,
|
||||
},
|
||||
{
|
||||
name: 'Dir2',
|
||||
id: 1,
|
||||
},
|
||||
],
|
||||
component: null,
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
element: 'Child1_2',
|
||||
directives: [],
|
||||
position: [0, 1],
|
||||
component: {
|
||||
isElement: false,
|
||||
name: 'Cmp2',
|
||||
id: 1,
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
element: 'Parent2',
|
||||
directives: [],
|
||||
component: null,
|
||||
position: [1],
|
||||
children: [
|
||||
{
|
||||
element: 'Child2_1',
|
||||
position: [1, 0],
|
||||
directives: [
|
||||
{
|
||||
name: 'Dir3',
|
||||
id: 1,
|
||||
},
|
||||
],
|
||||
component: null,
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
element: 'Child2_2',
|
||||
position: [1, 1],
|
||||
directives: [
|
||||
{
|
||||
name: 'Dir4',
|
||||
id: 1,
|
||||
},
|
||||
{
|
||||
name: 'Dir5',
|
||||
id: 1,
|
||||
},
|
||||
],
|
||||
component: null,
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -13,16 +13,19 @@ export interface IndexedNode extends DevToolsNode {
|
|||
children: IndexedNode[];
|
||||
}
|
||||
|
||||
const indexTree =
|
||||
(node: DevToolsNode, idx: number, parentPosition: ElementPosition = []): IndexedNode => {
|
||||
const position = parentPosition.concat([idx]);
|
||||
return {
|
||||
position,
|
||||
element: node.element,
|
||||
component: node.component,
|
||||
directives: node.directives.map((d, i) => ({name: d.name, id: d.id})),
|
||||
children: node.children.map((n, i) => indexTree(n, i, position)),
|
||||
} as IndexedNode;
|
||||
};
|
||||
const indexTree = (
|
||||
node: DevToolsNode,
|
||||
idx: number,
|
||||
parentPosition: ElementPosition = [],
|
||||
): IndexedNode => {
|
||||
const position = parentPosition.concat([idx]);
|
||||
return {
|
||||
position,
|
||||
element: node.element,
|
||||
component: node.component,
|
||||
directives: node.directives.map((d, i) => ({name: d.name, id: d.id})),
|
||||
children: node.children.map((n, i) => indexTree(n, i, position)),
|
||||
} as IndexedNode;
|
||||
};
|
||||
|
||||
export const indexForest = (forest: DevToolsNode[]) => forest.map((n, i) => indexTree(n, i));
|
||||
|
|
|
|||
|
|
@ -10,17 +10,19 @@ import {Descriptor} from 'protocol';
|
|||
|
||||
import {Property} from './element-property-resolver';
|
||||
|
||||
export const arrayifyProps =
|
||||
(props: {[prop: string]: Descriptor}|Descriptor[], parent: Property|null = null): Property[] =>
|
||||
Object.entries(props)
|
||||
.map(([name, val]) => ({name, descriptor: val, parent}))
|
||||
.sort((a, b) => {
|
||||
const parsedA = parseInt(a.name, 10);
|
||||
const parsedB = parseInt(b.name, 10);
|
||||
export const arrayifyProps = (
|
||||
props: {[prop: string]: Descriptor} | Descriptor[],
|
||||
parent: Property | null = null,
|
||||
): Property[] =>
|
||||
Object.entries(props)
|
||||
.map(([name, val]) => ({name, descriptor: val, parent}))
|
||||
.sort((a, b) => {
|
||||
const parsedA = parseInt(a.name, 10);
|
||||
const parsedB = parseInt(b.name, 10);
|
||||
|
||||
if (isNaN(parsedA) || isNaN(parsedB)) {
|
||||
return a.name > b.name ? 1 : -1;
|
||||
}
|
||||
if (isNaN(parsedA) || isNaN(parsedB)) {
|
||||
return a.name > b.name ? 1 : -1;
|
||||
}
|
||||
|
||||
return parsedA - parsedB;
|
||||
});
|
||||
return parsedA - parsedB;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,7 +8,15 @@
|
|||
|
||||
import {FlatTreeControl} from '@angular/cdk/tree';
|
||||
import {ViewEncapsulation} from '@angular/core';
|
||||
import {Descriptor, DirectiveMetadata, DirectivePosition, Events, MessageBus, NestedProp, Properties} from 'protocol';
|
||||
import {
|
||||
Descriptor,
|
||||
DirectiveMetadata,
|
||||
DirectivePosition,
|
||||
Events,
|
||||
MessageBus,
|
||||
NestedProp,
|
||||
Properties,
|
||||
} from 'protocol';
|
||||
|
||||
import {FlatNode, Property} from './element-property-resolver';
|
||||
import {getTreeFlattener} from './flatten';
|
||||
|
|
@ -20,38 +28,45 @@ export interface DirectiveTreeData {
|
|||
treeControl: FlatTreeControl<FlatNode>;
|
||||
}
|
||||
|
||||
const getDirectiveControls = (dataSource: PropertyDataSource):
|
||||
{dataSource: PropertyDataSource; treeControl: FlatTreeControl<FlatNode>} => {
|
||||
const treeControl = dataSource.treeControl;
|
||||
return {
|
||||
dataSource,
|
||||
treeControl,
|
||||
};
|
||||
};
|
||||
const getDirectiveControls = (
|
||||
dataSource: PropertyDataSource,
|
||||
): {dataSource: PropertyDataSource; treeControl: FlatTreeControl<FlatNode>} => {
|
||||
const treeControl = dataSource.treeControl;
|
||||
return {
|
||||
dataSource,
|
||||
treeControl,
|
||||
};
|
||||
};
|
||||
|
||||
export const constructPathOfKeysToPropertyValue =
|
||||
(nodePropToGetKeysFor: Property, keys: string[] = []): string[] => {
|
||||
keys.unshift(nodePropToGetKeysFor.name);
|
||||
const parentNodeProp = nodePropToGetKeysFor.parent;
|
||||
if (parentNodeProp) {
|
||||
constructPathOfKeysToPropertyValue(parentNodeProp, keys);
|
||||
}
|
||||
return keys;
|
||||
};
|
||||
export const constructPathOfKeysToPropertyValue = (
|
||||
nodePropToGetKeysFor: Property,
|
||||
keys: string[] = [],
|
||||
): string[] => {
|
||||
keys.unshift(nodePropToGetKeysFor.name);
|
||||
const parentNodeProp = nodePropToGetKeysFor.parent;
|
||||
if (parentNodeProp) {
|
||||
constructPathOfKeysToPropertyValue(parentNodeProp, keys);
|
||||
}
|
||||
return keys;
|
||||
};
|
||||
|
||||
export class DirectivePropertyResolver {
|
||||
private _treeFlattener = getTreeFlattener();
|
||||
|
||||
private _treeControl =
|
||||
new FlatTreeControl<FlatNode>((node) => node.level, (node) => node.expandable);
|
||||
private _treeControl = new FlatTreeControl<FlatNode>(
|
||||
(node) => node.level,
|
||||
(node) => node.expandable,
|
||||
);
|
||||
|
||||
private _inputsDataSource: PropertyDataSource;
|
||||
private _outputsDataSource: PropertyDataSource;
|
||||
private _stateDataSource: PropertyDataSource;
|
||||
|
||||
constructor(
|
||||
private _messageBus: MessageBus<Events>, private _props: Properties,
|
||||
private _directivePosition: DirectivePosition) {
|
||||
private _messageBus: MessageBus<Events>,
|
||||
private _props: Properties,
|
||||
private _directivePosition: DirectivePosition,
|
||||
) {
|
||||
const {inputProps, outputProps, stateProps} = this._classifyProperties();
|
||||
|
||||
this._inputsDataSource = this._createDataSourceFromProps(inputProps);
|
||||
|
|
@ -71,7 +86,7 @@ export class DirectivePropertyResolver {
|
|||
return getDirectiveControls(this._stateDataSource);
|
||||
}
|
||||
|
||||
get directiveMetadata(): DirectiveMetadata|undefined {
|
||||
get directiveMetadata(): DirectiveMetadata | undefined {
|
||||
return this._props.metadata;
|
||||
}
|
||||
|
||||
|
|
@ -83,11 +98,11 @@ export class DirectivePropertyResolver {
|
|||
return this._directivePosition;
|
||||
}
|
||||
|
||||
get directiveViewEncapsulation(): ViewEncapsulation|undefined {
|
||||
get directiveViewEncapsulation(): ViewEncapsulation | undefined {
|
||||
return this._props.metadata?.encapsulation;
|
||||
}
|
||||
|
||||
get directiveHasOnPushStrategy(): boolean|undefined {
|
||||
get directiveHasOnPushStrategy(): boolean | undefined {
|
||||
return this._props.metadata?.onPush;
|
||||
}
|
||||
|
||||
|
|
@ -117,11 +132,17 @@ export class DirectivePropertyResolver {
|
|||
|
||||
private _createDataSourceFromProps(props: {[name: string]: Descriptor}): PropertyDataSource {
|
||||
return new PropertyDataSource(
|
||||
props, this._treeFlattener, this._treeControl, this._directivePosition, this._messageBus);
|
||||
props,
|
||||
this._treeFlattener,
|
||||
this._treeControl,
|
||||
this._directivePosition,
|
||||
this._messageBus,
|
||||
);
|
||||
}
|
||||
|
||||
private _classifyProperties(): {
|
||||
inputProps: {[name: string]: Descriptor}; outputProps: {[name: string]: Descriptor};
|
||||
inputProps: {[name: string]: Descriptor};
|
||||
outputProps: {[name: string]: Descriptor};
|
||||
stateProps: {[name: string]: Descriptor};
|
||||
} {
|
||||
const inputLabels: Set<string> = new Set(Object.values(this._props.metadata?.inputs || {}));
|
||||
|
|
@ -133,9 +154,11 @@ export class DirectivePropertyResolver {
|
|||
let propPointer: {[name: string]: Descriptor};
|
||||
|
||||
Object.keys(this.directiveProperties).forEach((propName) => {
|
||||
propPointer = inputLabels.has(propName) ? inputProps :
|
||||
outputLabels.has(propName) ? outputProps :
|
||||
stateProps;
|
||||
propPointer = inputLabels.has(propName)
|
||||
? inputProps
|
||||
: outputLabels.has(propName)
|
||||
? outputProps
|
||||
: stateProps;
|
||||
propPointer[propName] = this.directiveProperties[propName];
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,14 @@
|
|||
*/
|
||||
|
||||
import {Injectable} from '@angular/core';
|
||||
import {ComponentExplorerViewProperties, Descriptor, DirectivePosition, DirectivesProperties, Events, MessageBus,} from 'protocol';
|
||||
import {
|
||||
ComponentExplorerViewProperties,
|
||||
Descriptor,
|
||||
DirectivePosition,
|
||||
DirectivesProperties,
|
||||
Events,
|
||||
MessageBus,
|
||||
} from 'protocol';
|
||||
|
||||
import {IndexedNode} from '../directive-forest/index-forest';
|
||||
|
||||
|
|
@ -22,7 +29,7 @@ export interface FlatNode {
|
|||
export interface Property {
|
||||
name: string;
|
||||
descriptor: Descriptor;
|
||||
parent: Property|null;
|
||||
parent: Property | null;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
|
|
@ -52,7 +59,9 @@ export class ElementPropertyResolver {
|
|||
position.directive = indexedNode.directives.findIndex((d) => d.name === key);
|
||||
}
|
||||
this._directivePropertiesController.set(
|
||||
key, new DirectivePropertyResolver(this._messageBus, data[key], position));
|
||||
key,
|
||||
new DirectivePropertyResolver(this._messageBus, data[key], position),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -79,7 +88,7 @@ export class ElementPropertyResolver {
|
|||
return result;
|
||||
}
|
||||
|
||||
getDirectiveController(directive: string): DirectivePropertyResolver|undefined {
|
||||
getDirectiveController(directive: string): DirectivePropertyResolver | undefined {
|
||||
return this._directivePropertiesController.get(directive);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,13 +14,18 @@ import {arrayifyProps} from './arrayify-props';
|
|||
import {FlatNode, Property} from './element-property-resolver';
|
||||
|
||||
export const getTreeFlattener = () =>
|
||||
new MatTreeFlattener((node: Property, level: number): FlatNode => {
|
||||
new MatTreeFlattener(
|
||||
(node: Property, level: number): FlatNode => {
|
||||
return {
|
||||
expandable: expandable(node.descriptor),
|
||||
prop: node,
|
||||
level,
|
||||
};
|
||||
}, (node) => node.level, (node) => node.expandable, (node) => getChildren(node));
|
||||
},
|
||||
(node) => node.level,
|
||||
(node) => node.expandable,
|
||||
(node) => getChildren(node),
|
||||
);
|
||||
|
||||
export const expandable = (prop: Descriptor) => {
|
||||
if (!prop) {
|
||||
|
|
@ -32,10 +37,12 @@ export const expandable = (prop: Descriptor) => {
|
|||
return !(prop.type !== PropType.Object && prop.type !== PropType.Array);
|
||||
};
|
||||
|
||||
const getChildren = (prop: Property): Property[]|undefined => {
|
||||
const getChildren = (prop: Property): Property[] | undefined => {
|
||||
const descriptor = prop.descriptor;
|
||||
if ((descriptor.type === PropType.Object || descriptor.type === PropType.Array) &&
|
||||
!(descriptor.value instanceof Observable)) {
|
||||
if (
|
||||
(descriptor.type === PropType.Object || descriptor.type === PropType.Array) &&
|
||||
!(descriptor.value instanceof Observable)
|
||||
) {
|
||||
return arrayifyProps(descriptor.value || {}, prop);
|
||||
}
|
||||
console.error('Unexpected data type', descriptor, 'in property', prop);
|
||||
|
|
|
|||
|
|
@ -13,23 +13,29 @@ import {FlatNode} from './element-property-resolver';
|
|||
import {getTreeFlattener} from './flatten';
|
||||
import {PropertyDataSource} from './property-data-source';
|
||||
|
||||
const flatTreeControl =
|
||||
new FlatTreeControl<FlatNode>((node) => node.level, (node) => node.expandable);
|
||||
const flatTreeControl = new FlatTreeControl<FlatNode>(
|
||||
(node) => node.level,
|
||||
(node) => node.expandable,
|
||||
);
|
||||
|
||||
describe('PropertyDataSource', () => {
|
||||
it('should detect changes in the collection', () => {
|
||||
const source = new PropertyDataSource(
|
||||
{
|
||||
foo: {
|
||||
editable: true,
|
||||
expandable: false,
|
||||
preview: '42',
|
||||
type: PropType.Number,
|
||||
value: 42,
|
||||
containerType: null,
|
||||
},
|
||||
{
|
||||
foo: {
|
||||
editable: true,
|
||||
expandable: false,
|
||||
preview: '42',
|
||||
type: PropType.Number,
|
||||
value: 42,
|
||||
containerType: null,
|
||||
},
|
||||
getTreeFlattener(), flatTreeControl, {element: [1, 2, 3]}, null as any);
|
||||
},
|
||||
getTreeFlattener(),
|
||||
flatTreeControl,
|
||||
{element: [1, 2, 3]},
|
||||
null as any,
|
||||
);
|
||||
|
||||
source.update({
|
||||
foo: {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import {arrayifyProps} from './arrayify-props';
|
|||
import {FlatNode, Property} from './element-property-resolver';
|
||||
|
||||
const trackBy: TrackByFunction<FlatNode> = (_: number, item: FlatNode) =>
|
||||
`#${item.prop.name}#${item.prop.descriptor.preview}#${item.level}`;
|
||||
`#${item.prop.name}#${item.prop.descriptor.preview}#${item.level}`;
|
||||
|
||||
export class PropertyDataSource extends DataSource<FlatNode> {
|
||||
private _data = new BehaviorSubject<FlatNode[]>([]);
|
||||
|
|
@ -29,10 +29,12 @@ export class PropertyDataSource extends DataSource<FlatNode> {
|
|||
private _differ = new DefaultIterableDiffer<FlatNode>(trackBy);
|
||||
|
||||
constructor(
|
||||
props: {[prop: string]: Descriptor},
|
||||
private _treeFlattener: MatTreeFlattener<Property, FlatNode>,
|
||||
private _treeControl: FlatTreeControl<FlatNode>, private _entityPosition: DirectivePosition,
|
||||
private _messageBus: MessageBus<Events>) {
|
||||
props: {[prop: string]: Descriptor},
|
||||
private _treeFlattener: MatTreeFlattener<Property, FlatNode>,
|
||||
private _treeControl: FlatTreeControl<FlatNode>,
|
||||
private _entityPosition: DirectivePosition,
|
||||
private _messageBus: MessageBus<Events>,
|
||||
) {
|
||||
super();
|
||||
this._data.next(this._treeFlattener.flattenNodes(arrayifyProps(props)));
|
||||
}
|
||||
|
|
@ -66,14 +68,20 @@ export class PropertyDataSource extends DataSource<FlatNode> {
|
|||
});
|
||||
this._subscriptions.push(s);
|
||||
|
||||
const changes =
|
||||
[collectionViewer.viewChange, this._treeControl.expansionModel.changed, this._data];
|
||||
const changes = [
|
||||
collectionViewer.viewChange,
|
||||
this._treeControl.expansionModel.changed,
|
||||
this._data,
|
||||
];
|
||||
|
||||
return merge<unknown[]>(...changes).pipe(map(() => {
|
||||
this._expandedData.next(
|
||||
this._treeFlattener.expandFlattenedNodes(this.data, this._treeControl));
|
||||
return this._expandedData.value;
|
||||
}));
|
||||
return merge<unknown[]>(...changes).pipe(
|
||||
map(() => {
|
||||
this._expandedData.next(
|
||||
this._treeFlattener.expandFlattenedNodes(this.data, this._treeControl),
|
||||
);
|
||||
return this._expandedData.value;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
override disconnect(): void {
|
||||
|
|
@ -104,14 +112,16 @@ export class PropertyDataSource extends DataSource<FlatNode> {
|
|||
this._messageBus.emit('getNestedProperties', [this._entityPosition, parentPath]);
|
||||
|
||||
this._messageBus.once(
|
||||
'nestedProperties', (position: DirectivePosition, data: Properties, _path: string[]) => {
|
||||
node.prop.descriptor.value = data.props;
|
||||
this._treeControl.expand(node);
|
||||
const props = arrayifyProps(data.props, node.prop);
|
||||
const flatNodes = this._treeFlattener.flattenNodes(props);
|
||||
flatNodes.forEach((f) => (f.level += node.level + 1));
|
||||
this.data.splice(index + 1, 0, ...flatNodes);
|
||||
this._data.next(this.data);
|
||||
});
|
||||
'nestedProperties',
|
||||
(position: DirectivePosition, data: Properties, _path: string[]) => {
|
||||
node.prop.descriptor.value = data.props;
|
||||
this._treeControl.expand(node);
|
||||
const props = arrayifyProps(data.props, node.prop);
|
||||
const flatNodes = this._treeFlattener.flattenNodes(props);
|
||||
flatNodes.forEach((f) => (f.level += node.level + 1));
|
||||
this.data.splice(index + 1, 0, ...flatNodes);
|
||||
this._data.next(this.data);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,14 +13,14 @@ import {FlatNode} from './element-property-resolver';
|
|||
export const getExpandedDirectiveProperties = (data: FlatNode[]): NestedProp[] => {
|
||||
const getChildren = (prop: Descriptor) => {
|
||||
if ((prop.type === PropType.Object || prop.type === PropType.Array) && prop.value) {
|
||||
return Object.entries(prop.value).map(([k, v]: [
|
||||
string, any
|
||||
]): {name: number|string, children: NestedProp[]} => {
|
||||
return {
|
||||
name: prop.type === PropType.Array ? parseInt(k, 10) : k,
|
||||
children: getChildren(v),
|
||||
};
|
||||
});
|
||||
return Object.entries(prop.value).map(
|
||||
([k, v]: [string, any]): {name: number | string; children: NestedProp[]} => {
|
||||
return {
|
||||
name: prop.type === PropType.Array ? parseInt(k, 10) : k,
|
||||
children: getChildren(v),
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -25,14 +25,14 @@ export class ComponentMetadataComponent {
|
|||
|
||||
viewEncapsulationModes = ['Emulated', 'Native', 'None', 'ShadowDom'];
|
||||
|
||||
get controller(): DirectivePropertyResolver|undefined {
|
||||
get controller(): DirectivePropertyResolver | undefined {
|
||||
if (!this.currentSelectedComponent) {
|
||||
return;
|
||||
}
|
||||
return this._nestedProps.getDirectiveController(this.currentSelectedComponent.name);
|
||||
}
|
||||
|
||||
get viewEncapsulation(): string|undefined {
|
||||
get viewEncapsulation(): string | undefined {
|
||||
const encapsulationIndex = this?.controller?.directiveViewEncapsulation;
|
||||
if (encapsulationIndex !== undefined) {
|
||||
return this.viewEncapsulationModes[encapsulationIndex];
|
||||
|
|
@ -40,7 +40,7 @@ export class ComponentMetadataComponent {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
get changeDetectionStrategy(): string|undefined {
|
||||
get changeDetectionStrategy(): string | undefined {
|
||||
const onPush = this?.controller?.directiveHasOnPushStrategy;
|
||||
return onPush ? 'OnPush' : onPush !== undefined ? 'Default' : undefined;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,5 +18,5 @@ import {IndexedNode} from '../directive-forest/index-forest';
|
|||
})
|
||||
export class PropertyTabHeaderComponent {
|
||||
@Input({required: true}) currentSelectedElement!: IndexedNode;
|
||||
@Input() currentDirectives: string[]|undefined;
|
||||
@Input() currentDirectives: string[] | undefined;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,10 +21,13 @@ import {PropertyViewModule} from './property-view/property-view.module';
|
|||
@NgModule({
|
||||
declarations: [PropertyTabComponent, PropertyTabHeaderComponent, ComponentMetadataComponent],
|
||||
imports: [
|
||||
PropertyViewModule, CommonModule, MatButtonModule, MatExpansionModule, MatIconModule,
|
||||
MatTooltipModule
|
||||
PropertyViewModule,
|
||||
CommonModule,
|
||||
MatButtonModule,
|
||||
MatExpansionModule,
|
||||
MatIconModule,
|
||||
MatTooltipModule,
|
||||
],
|
||||
exports: [PropertyTabComponent],
|
||||
})
|
||||
export class PropertyTabModule {
|
||||
}
|
||||
export class PropertyTabModule {}
|
||||
|
|
|
|||
|
|
@ -6,11 +6,20 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {AfterViewChecked, ChangeDetectorRef, Component, ElementRef, EventEmitter, Input, OnInit, Output,} from '@angular/core';
|
||||
import {
|
||||
AfterViewChecked,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnInit,
|
||||
Output,
|
||||
} from '@angular/core';
|
||||
import {ContainerType} from 'protocol';
|
||||
|
||||
type EditorType = string|number|boolean;
|
||||
type EditorResult = EditorType|Array<EditorType>;
|
||||
type EditorType = string | number | boolean;
|
||||
type EditorResult = EditorType | Array<EditorType>;
|
||||
|
||||
enum PropertyEditorState {
|
||||
Read,
|
||||
|
|
@ -43,7 +52,10 @@ export class PropertyEditorComponent implements AfterViewChecked, OnInit {
|
|||
valueToSubmit!: EditorResult;
|
||||
currentPropertyState = this.readState;
|
||||
|
||||
constructor(private _cd: ChangeDetectorRef, private _elementRef: ElementRef) {}
|
||||
constructor(
|
||||
private _cd: ChangeDetectorRef,
|
||||
private _elementRef: ElementRef,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.valueToSubmit = this.initialValue;
|
||||
|
|
|
|||
|
|
@ -21,7 +21,9 @@ export class PropertyPreviewComponent {
|
|||
@Output() inspect = new EventEmitter<void>();
|
||||
|
||||
get isClickableProp(): boolean {
|
||||
return this.node.prop.descriptor.type === PropType.Function ||
|
||||
this.node.prop.descriptor.type === PropType.HTMLNode;
|
||||
return (
|
||||
this.node.prop.descriptor.type === PropType.Function ||
|
||||
this.node.prop.descriptor.type === PropType.HTMLNode
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,11 +18,11 @@ import {FlatNode} from '../../property-resolver/element-property-resolver';
|
|||
styleUrls: ['./property-tab-body.component.scss'],
|
||||
})
|
||||
export class PropertyTabBodyComponent {
|
||||
@Input({required: true}) currentSelectedElement!: IndexedNode|null;
|
||||
@Input({required: true}) currentSelectedElement!: IndexedNode | null;
|
||||
@Output() inspect = new EventEmitter<{node: FlatNode; directivePosition: DirectivePosition}>();
|
||||
@Output() viewSource = new EventEmitter<string>();
|
||||
|
||||
getCurrentDirectives(): string[]|undefined {
|
||||
getCurrentDirectives(): string[] | undefined {
|
||||
if (!this.currentSelectedElement) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,10 @@ import {CdkDragDrop, moveItemInArray} from '@angular/cdk/drag-drop';
|
|||
import {Component, EventEmitter, Input, Output} from '@angular/core';
|
||||
import {DirectivePosition, SerializedInjectedService} from 'protocol';
|
||||
|
||||
import {DirectivePropertyResolver, DirectiveTreeData} from '../../property-resolver/directive-property-resolver';
|
||||
import {
|
||||
DirectivePropertyResolver,
|
||||
DirectiveTreeData,
|
||||
} from '../../property-resolver/directive-property-resolver';
|
||||
import {FlatNode} from '../../property-resolver/element-property-resolver';
|
||||
|
||||
@Component({
|
||||
|
|
@ -29,8 +32,11 @@ export class PropertyViewBodyComponent {
|
|||
categoryOrder = [0, 1, 2];
|
||||
|
||||
get panels(): {
|
||||
title: string; hidden: boolean; controls: DirectiveTreeData; documentation: string,
|
||||
class: string;
|
||||
title: string;
|
||||
hidden: boolean;
|
||||
controls: DirectiveTreeData;
|
||||
documentation: string;
|
||||
class: string;
|
||||
}[] {
|
||||
return [
|
||||
{
|
||||
|
|
@ -58,8 +64,11 @@ export class PropertyViewBodyComponent {
|
|||
}
|
||||
|
||||
get controlsLoaded(): boolean {
|
||||
return !!this.directiveStateControls && !!this.directiveOutputControls &&
|
||||
!!this.directiveInputControls;
|
||||
return (
|
||||
!!this.directiveStateControls &&
|
||||
!!this.directiveOutputControls &&
|
||||
!!this.directiveInputControls
|
||||
);
|
||||
}
|
||||
|
||||
updateValue({node, newValue}: {node: FlatNode; newValue: unknown}): void {
|
||||
|
|
@ -78,7 +87,6 @@ export class PropertyViewBodyComponent {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'ng-dependency-viewer',
|
||||
template: `
|
||||
|
|
@ -87,23 +95,25 @@ export class PropertyViewBodyComponent {
|
|||
<mat-expansion-panel-header collapsedHeight="35px" expandedHeight="35px">
|
||||
<mat-panel-title>
|
||||
<mat-chip-listbox>
|
||||
<mat-chip matTooltipPosition="left" matTooltip="Dependency injection token" (click)="$event.stopPropagation();">{{dependency.token}}</mat-chip>
|
||||
<mat-chip
|
||||
matTooltipPosition="left"
|
||||
matTooltip="Dependency injection token"
|
||||
(click)="$event.stopPropagation()"
|
||||
>{{ dependency.token }}</mat-chip
|
||||
>
|
||||
</mat-chip-listbox>
|
||||
</mat-panel-title>
|
||||
<mat-panel-description>
|
||||
<mat-chip-listbox>
|
||||
<div class="di-flags">
|
||||
@if (dependency.flags?.optional) {
|
||||
<mat-chip [highlighted]="true" color="primary">Optional</mat-chip>
|
||||
}
|
||||
@if (dependency.flags?.host) {
|
||||
<mat-chip [highlighted]="true" color="primary">Host</mat-chip>
|
||||
}
|
||||
@if (dependency.flags?.self) {
|
||||
<mat-chip [highlighted]="true" color="primary">Self</mat-chip>
|
||||
}
|
||||
@if (dependency.flags?.skipSelf) {
|
||||
<mat-chip [highlighted]="true" color="primary">SkipSelf</mat-chip>
|
||||
<mat-chip [highlighted]="true" color="primary">Optional</mat-chip>
|
||||
} @if (dependency.flags?.host) {
|
||||
<mat-chip [highlighted]="true" color="primary">Host</mat-chip>
|
||||
} @if (dependency.flags?.self) {
|
||||
<mat-chip [highlighted]="true" color="primary">Self</mat-chip>
|
||||
} @if (dependency.flags?.skipSelf) {
|
||||
<mat-chip [highlighted]="true" color="primary">SkipSelf</mat-chip>
|
||||
}
|
||||
</div>
|
||||
</mat-chip-listbox>
|
||||
|
|
@ -112,30 +122,32 @@ export class PropertyViewBodyComponent {
|
|||
<ng-resolution-path [path]="dependency.resolutionPath"></ng-resolution-path>
|
||||
</mat-expansion-panel>
|
||||
</mat-accordion>
|
||||
`,
|
||||
styles: [`
|
||||
.di-flags {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
:host-context(.dark-theme) ng-resolution-path {
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
ng-resolution-path {
|
||||
border-top: 1px solid black;
|
||||
display: block;
|
||||
overflow-x: scroll;
|
||||
background: #f3f3f3;
|
||||
}
|
||||
|
||||
:host {
|
||||
mat-chip {
|
||||
--mdc-chip-container-height: 18px;
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
.di-flags {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
`]
|
||||
|
||||
:host-context(.dark-theme) ng-resolution-path {
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
ng-resolution-path {
|
||||
border-top: 1px solid black;
|
||||
display: block;
|
||||
overflow-x: scroll;
|
||||
background: #f3f3f3;
|
||||
}
|
||||
|
||||
:host {
|
||||
mat-chip {
|
||||
--mdc-chip-container-height: 18px;
|
||||
}
|
||||
}
|
||||
`,
|
||||
],
|
||||
})
|
||||
export class DependencyViewerComponent {
|
||||
@Input({required: true}) dependency!: SerializedInjectedService;
|
||||
|
|
@ -143,16 +155,17 @@ export class DependencyViewerComponent {
|
|||
|
||||
@Component({
|
||||
selector: 'ng-injected-services',
|
||||
template: `
|
||||
@for (dependency of dependencies; track dependency.position[0]) {
|
||||
<ng-dependency-viewer [dependency]="dependency" />
|
||||
}`,
|
||||
styles: [`
|
||||
template: ` @for (dependency of dependencies; track dependency.position[0]) {
|
||||
<ng-dependency-viewer [dependency]="dependency" />
|
||||
}`,
|
||||
styles: [
|
||||
`
|
||||
ng-dependency-viewer {
|
||||
border-bottom: 1px solid color-mix(in srgb, currentColor, #bdbdbd 85%);
|
||||
display: block;
|
||||
}
|
||||
`]
|
||||
`,
|
||||
],
|
||||
})
|
||||
export class InjectedServicesComponent {
|
||||
@Input({required: true}) controller!: DirectivePropertyResolver;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,10 @@
|
|||
import {Component, EventEmitter, Input, Output} from '@angular/core';
|
||||
import {DirectivePosition} from 'protocol';
|
||||
|
||||
import {DirectivePropertyResolver, DirectiveTreeData} from '../../property-resolver/directive-property-resolver';
|
||||
import {
|
||||
DirectivePropertyResolver,
|
||||
DirectiveTreeData,
|
||||
} from '../../property-resolver/directive-property-resolver';
|
||||
import {ElementPropertyResolver, FlatNode} from '../../property-resolver/element-property-resolver';
|
||||
|
||||
@Component({
|
||||
|
|
@ -24,19 +27,19 @@ export class PropertyViewComponent {
|
|||
|
||||
constructor(private _nestedProps: ElementPropertyResolver) {}
|
||||
|
||||
get controller(): DirectivePropertyResolver|undefined {
|
||||
get controller(): DirectivePropertyResolver | undefined {
|
||||
return this._nestedProps.getDirectiveController(this.directive);
|
||||
}
|
||||
|
||||
get directiveInputControls(): DirectiveTreeData|void {
|
||||
get directiveInputControls(): DirectiveTreeData | void {
|
||||
return this.controller?.directiveInputControls;
|
||||
}
|
||||
|
||||
get directiveOutputControls(): DirectiveTreeData|void {
|
||||
get directiveOutputControls(): DirectiveTreeData | void {
|
||||
return this.controller?.directiveOutputControls;
|
||||
}
|
||||
|
||||
get directiveStateControls(): DirectiveTreeData|void {
|
||||
get directiveStateControls(): DirectiveTreeData | void {
|
||||
return this.controller?.directiveStateControls;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,11 @@ import {ResolutionPathComponent} from '../../../dependency-injection/resolution-
|
|||
import {PropertyEditorComponent} from './property-editor.component';
|
||||
import {PropertyPreviewComponent} from './property-preview.component';
|
||||
import {PropertyTabBodyComponent} from './property-tab-body.component';
|
||||
import {DependencyViewerComponent, InjectedServicesComponent, PropertyViewBodyComponent} from './property-view-body.component';
|
||||
import {
|
||||
DependencyViewerComponent,
|
||||
InjectedServicesComponent,
|
||||
PropertyViewBodyComponent,
|
||||
} from './property-view-body.component';
|
||||
import {PropertyViewHeaderComponent} from './property-view-header.component';
|
||||
import {PropertyViewTreeComponent} from './property-view-tree.component';
|
||||
import {PropertyViewComponent} from './property-view.component';
|
||||
|
|
@ -41,9 +45,17 @@ import {PropertyViewComponent} from './property-view.component';
|
|||
DependencyViewerComponent,
|
||||
],
|
||||
imports: [
|
||||
MatToolbarModule, MatButtonModule, MatIconModule, MatTreeModule, MatTooltipModule,
|
||||
MatChipsModule, CommonModule, MatExpansionModule, DragDropModule, FormsModule,
|
||||
ResolutionPathComponent
|
||||
MatToolbarModule,
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
MatTreeModule,
|
||||
MatTooltipModule,
|
||||
MatChipsModule,
|
||||
CommonModule,
|
||||
MatExpansionModule,
|
||||
DragDropModule,
|
||||
FormsModule,
|
||||
ResolutionPathComponent,
|
||||
],
|
||||
exports: [
|
||||
PropertyViewComponent,
|
||||
|
|
@ -55,5 +67,4 @@ import {PropertyViewComponent} from './property-view.component';
|
|||
PropertyEditorComponent,
|
||||
],
|
||||
})
|
||||
export class PropertyViewModule {
|
||||
}
|
||||
export class PropertyViewModule {}
|
||||
|
|
|
|||
|
|
@ -18,126 +18,137 @@ import {Events, MessageBus, SerializedInjector, SerializedProviderRecord} from '
|
|||
@Component({
|
||||
selector: 'ng-injector-providers',
|
||||
template: `
|
||||
<h1>
|
||||
Providers for {{ injector?.name }}
|
||||
</h1>
|
||||
@if (injector) {
|
||||
<div class="injector-providers">
|
||||
<mat-form-field appearance="fill" class="form-field-spacer">
|
||||
<mat-label>Search by token</mat-label>
|
||||
<input type="text" matInput
|
||||
placeholder="Provider token"
|
||||
(input)="searchToken.set($event.target.value)"
|
||||
[value]="searchToken()"
|
||||
/>
|
||||
<mat-icon matSuffix (click)="searchToken.set('')">close</mat-icon>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field-spacer">
|
||||
<mat-label>Search by type</mat-label>
|
||||
<mat-select
|
||||
[value]="searchType()"
|
||||
(selectionChange)="searchType.set($event.value)"
|
||||
>
|
||||
<mat-option>None</mat-option>
|
||||
@for (type of providerTypes; track type) {
|
||||
<mat-option [value]="type">{{$any(providerTypeToLabel)[type]}}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
@if (providers.length > 0) {
|
||||
<table mat-table [dataSource]="providers" class="mat-elevation-z4">
|
||||
<ng-container matColumnDef="token">
|
||||
<th mat-header-cell *matHeaderCellDef> <h3 class="column-title">Token</h3> </th>
|
||||
<td mat-cell *matCellDef="let provider"> {{provider.token}} </td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="type">
|
||||
<th mat-header-cell *matHeaderCellDef> <h3 class="column-title">Type</h3> </th>
|
||||
<td mat-cell *matCellDef="let provider">
|
||||
@if (provider.type === 'multi') {
|
||||
multi (x{{ provider.index.length }})
|
||||
} @else {
|
||||
{{$any(providerTypeToLabel)[provider.type]}}
|
||||
}
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="isViewProvider">
|
||||
<th mat-header-cell *matHeaderCellDef> <h3 class="column-title">Is View Provider</h3> </th>
|
||||
<td mat-cell *matCellDef="let provider"> <mat-icon>{{provider.isViewProvider ? 'check_circle' : 'cancel'}}</mat-icon> </td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="log">
|
||||
<th mat-header-cell *matHeaderCellDef> <h3 class="column-title"></h3> </th>
|
||||
<td mat-cell *matCellDef="let provider"> <mat-icon matTooltipPosition="left" matTooltip="Log provider in console" class="select" (click)="select(provider)">send</mat-icon> </td>
|
||||
</ng-container>
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
<h1>Providers for {{ injector?.name }}</h1>
|
||||
@if (injector) {
|
||||
<div class="injector-providers">
|
||||
<mat-form-field appearance="fill" class="form-field-spacer">
|
||||
<mat-label>Search by token</mat-label>
|
||||
<input
|
||||
type="text"
|
||||
matInput
|
||||
placeholder="Provider token"
|
||||
(input)="searchToken.set($event.target.value)"
|
||||
[value]="searchToken()"
|
||||
/>
|
||||
<mat-icon matSuffix (click)="searchToken.set('')">close</mat-icon>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field-spacer">
|
||||
<mat-label>Search by type</mat-label>
|
||||
<mat-select [value]="searchType()" (selectionChange)="searchType.set($event.value)">
|
||||
<mat-option>None</mat-option>
|
||||
@for (type of providerTypes; track type) {
|
||||
<mat-option [value]="type">{{ $any(providerTypeToLabel)[type] }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
@if (providers.length > 0) {
|
||||
<table mat-table [dataSource]="providers" class="mat-elevation-z4">
|
||||
<ng-container matColumnDef="token">
|
||||
<th mat-header-cell *matHeaderCellDef><h3 class="column-title">Token</h3></th>
|
||||
<td mat-cell *matCellDef="let provider">{{ provider.token }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="type">
|
||||
<th mat-header-cell *matHeaderCellDef><h3 class="column-title">Type</h3></th>
|
||||
<td mat-cell *matCellDef="let provider">
|
||||
@if (provider.type === 'multi') { multi (x{{ provider.index.length }}) } @else {
|
||||
{{ $any(providerTypeToLabel)[provider.type] }}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
`,
|
||||
styles: [`
|
||||
.select {
|
||||
cursor: pointer;
|
||||
}
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="isViewProvider">
|
||||
<th mat-header-cell *matHeaderCellDef><h3 class="column-title">Is View Provider</h3></th>
|
||||
<td mat-cell *matCellDef="let provider">
|
||||
<mat-icon>{{ provider.isViewProvider ? 'check_circle' : 'cancel' }}</mat-icon>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="log">
|
||||
<th mat-header-cell *matHeaderCellDef><h3 class="column-title"></h3></th>
|
||||
<td mat-cell *matCellDef="let provider">
|
||||
<mat-icon
|
||||
matTooltipPosition="left"
|
||||
matTooltip="Log provider in console"
|
||||
class="select"
|
||||
(click)="select(provider)"
|
||||
>send</mat-icon
|
||||
>
|
||||
</td>
|
||||
</ng-container>
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
|
||||
</table>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
.select {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
padding: 16px;
|
||||
}
|
||||
:host {
|
||||
display: block;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.form-field-spacer {
|
||||
margin: 0 4px 0 4px;
|
||||
}
|
||||
.form-field-spacer {
|
||||
margin: 0 4px 0 4px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.column-title {
|
||||
margin: 0;
|
||||
}
|
||||
.column-title {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
tr.example-detail-row {
|
||||
height: 0;
|
||||
}
|
||||
tr.example-detail-row {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.example-element-row td {
|
||||
border-bottom-width: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.example-element-row td {
|
||||
border-bottom-width: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.example-element-detail {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
}
|
||||
.example-element-detail {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.example-element-diagram {
|
||||
min-width: 80px;
|
||||
border: 2px solid black;
|
||||
padding: 8px;
|
||||
font-weight: lighter;
|
||||
margin: 8px 0;
|
||||
height: 104px;
|
||||
}
|
||||
.example-element-diagram {
|
||||
min-width: 80px;
|
||||
border: 2px solid black;
|
||||
padding: 8px;
|
||||
font-weight: lighter;
|
||||
margin: 8px 0;
|
||||
height: 104px;
|
||||
}
|
||||
|
||||
.example-element-symbol {
|
||||
font-weight: bold;
|
||||
font-size: 40px;
|
||||
line-height: normal;
|
||||
}
|
||||
.example-element-symbol {
|
||||
font-weight: bold;
|
||||
font-size: 40px;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.example-element-description {
|
||||
padding: 16px;
|
||||
}
|
||||
.example-element-description {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.example-element-description-attribution {
|
||||
opacity: 0.5;
|
||||
}
|
||||
`],
|
||||
.example-element-description-attribution {
|
||||
opacity: 0.5;
|
||||
}
|
||||
`,
|
||||
],
|
||||
standalone: true,
|
||||
imports: [
|
||||
MatTableModule, MatIconModule, MatTooltipModule, MatInputModule, MatSelectModule,
|
||||
MatFormFieldModule
|
||||
MatTableModule,
|
||||
MatIconModule,
|
||||
MatTooltipModule,
|
||||
MatInputModule,
|
||||
MatSelectModule,
|
||||
MatFormFieldModule,
|
||||
],
|
||||
})
|
||||
export class InjectorProvidersComponent {
|
||||
|
|
@ -152,11 +163,12 @@ export class InjectorProvidersComponent {
|
|||
|
||||
const predicates: ((provider: SerializedProviderRecord) => boolean)[] = [];
|
||||
searchToken &&
|
||||
predicates.push((provider) => provider.token.toLowerCase().includes(searchToken));
|
||||
predicates.push((provider) => provider.token.toLowerCase().includes(searchToken));
|
||||
searchType && predicates.push((provider) => provider.type === searchType);
|
||||
|
||||
return this.providers.filter(
|
||||
(provider) => predicates.every((predicate) => predicate(provider)));
|
||||
return this.providers.filter((provider) =>
|
||||
predicates.every((predicate) => predicate(provider)),
|
||||
);
|
||||
});
|
||||
|
||||
providerTypeToLabel = {
|
||||
|
|
@ -178,7 +190,7 @@ export class InjectorProvidersComponent {
|
|||
type: this.injector.type,
|
||||
name: this.injector.name,
|
||||
},
|
||||
row
|
||||
row,
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -8,7 +8,10 @@
|
|||
|
||||
import {DevToolsNode, SerializedInjector} from 'protocol';
|
||||
|
||||
import {InjectorTreeD3Node, InjectorTreeNode} from '../dependency-injection/injector-tree-visualizer';
|
||||
import {
|
||||
InjectorTreeD3Node,
|
||||
InjectorTreeNode,
|
||||
} from '../dependency-injection/injector-tree-visualizer';
|
||||
|
||||
export interface InjectorPath {
|
||||
node: DevToolsNode;
|
||||
|
|
@ -42,12 +45,15 @@ export function equalInjector(a: SerializedInjector, b: SerializedInjector): boo
|
|||
}
|
||||
|
||||
export function findExistingPath(
|
||||
path: InjectorTreeNode[], value: SerializedInjector): InjectorTreeNode|null {
|
||||
return path.find(injector => equalInjector(injector.injector, value)) || null;
|
||||
path: InjectorTreeNode[],
|
||||
value: SerializedInjector,
|
||||
): InjectorTreeNode | null {
|
||||
return path.find((injector) => equalInjector(injector.injector, value)) || null;
|
||||
}
|
||||
|
||||
export function transformInjectorResolutionPathsIntoTree(injectorPaths: InjectorPath[]):
|
||||
InjectorTreeNode {
|
||||
export function transformInjectorResolutionPathsIntoTree(
|
||||
injectorPaths: InjectorPath[],
|
||||
): InjectorTreeNode {
|
||||
const injectorTree: InjectorTreeNode[] = [];
|
||||
const injectorIdToNode = new Map<string, DevToolsNode>();
|
||||
|
||||
|
|
@ -83,8 +89,9 @@ export function transformInjectorResolutionPathsIntoTree(injectorPaths: Injector
|
|||
return hiddenRoot as any;
|
||||
}
|
||||
|
||||
export function grabInjectorPathsFromDirectiveForest(directiveForest: DevToolsNode[]):
|
||||
InjectorPath[] {
|
||||
export function grabInjectorPathsFromDirectiveForest(
|
||||
directiveForest: DevToolsNode[],
|
||||
): InjectorPath[] {
|
||||
const injectorPaths: InjectorPath[] = [];
|
||||
|
||||
const grabInjectorPaths = (node: DevToolsNode) => {
|
||||
|
|
@ -92,7 +99,7 @@ export function grabInjectorPathsFromDirectiveForest(directiveForest: DevToolsNo
|
|||
injectorPaths.push({node, path: node.resolutionPath.slice().reverse()});
|
||||
}
|
||||
|
||||
node.children.forEach(child => grabInjectorPaths(child));
|
||||
node.children.forEach((child) => grabInjectorPaths(child));
|
||||
};
|
||||
|
||||
for (const directive of directiveForest) {
|
||||
|
|
@ -103,7 +110,8 @@ export function grabInjectorPathsFromDirectiveForest(directiveForest: DevToolsNo
|
|||
}
|
||||
|
||||
export function splitInjectorPathsIntoElementAndEnvironmentPaths(injectorPaths: InjectorPath[]): {
|
||||
elementPaths: InjectorPath[]; environmentPaths: InjectorPath[];
|
||||
elementPaths: InjectorPath[];
|
||||
environmentPaths: InjectorPath[];
|
||||
startingElementToEnvironmentPath: Map<string, SerializedInjector[]>;
|
||||
} {
|
||||
const elementPaths: InjectorPath[] = [];
|
||||
|
|
@ -111,7 +119,7 @@ export function splitInjectorPathsIntoElementAndEnvironmentPaths(injectorPaths:
|
|||
const startingElementToEnvironmentPath = new Map<string, SerializedInjector[]>();
|
||||
|
||||
injectorPaths.forEach(({node, path}) => {
|
||||
const firstElementIndex = path.findIndex(injector => injector.type === 'element');
|
||||
const firstElementIndex = path.findIndex((injector) => injector.type === 'element');
|
||||
|
||||
// split the path into two paths,
|
||||
// one for the element injector and one for the environment injector
|
||||
|
|
@ -131,15 +139,18 @@ export function splitInjectorPathsIntoElementAndEnvironmentPaths(injectorPaths:
|
|||
if (elementPath[elementPath.length - 1]) {
|
||||
// reverse each path to get the paths starting from the starting element
|
||||
startingElementToEnvironmentPath.set(
|
||||
elementPath[elementPath.length - 1].id, environmentPath.slice().reverse());
|
||||
elementPath[elementPath.length - 1].id,
|
||||
environmentPath.slice().reverse(),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
elementPaths:
|
||||
elementPaths.filter(({path}) => path.every(injector => injector.type === 'element')),
|
||||
elementPaths: elementPaths.filter(({path}) =>
|
||||
path.every((injector) => injector.type === 'element'),
|
||||
),
|
||||
environmentPaths,
|
||||
startingElementToEnvironmentPath
|
||||
startingElementToEnvironmentPath,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -188,17 +199,20 @@ const ANGULAR_DIRECTIVES = [
|
|||
'RouterLinkActive',
|
||||
'RouterLinkWithHref',
|
||||
'RouterOutlet',
|
||||
'UpgradeComponent'
|
||||
'UpgradeComponent',
|
||||
];
|
||||
|
||||
const ignoredAngularInjectors = new Set([
|
||||
'Null Injector', ...ANGULAR_DIRECTIVES, ...ANGULAR_DIRECTIVES.map(directive => `_${directive}`)
|
||||
'Null Injector',
|
||||
...ANGULAR_DIRECTIVES,
|
||||
...ANGULAR_DIRECTIVES.map((directive) => `_${directive}`),
|
||||
]);
|
||||
|
||||
export function filterOutInjectorsWithNoProviders(injectorPaths: InjectorPath[]): InjectorPath[] {
|
||||
for (const injectorPath of injectorPaths) {
|
||||
injectorPath.path =
|
||||
injectorPath.path.filter(({providers}) => providers === undefined || providers > 0);
|
||||
injectorPath.path = injectorPath.path.filter(
|
||||
({providers}) => providers === undefined || providers > 0,
|
||||
);
|
||||
}
|
||||
|
||||
return injectorPaths;
|
||||
|
|
@ -206,6 +220,6 @@ export function filterOutInjectorsWithNoProviders(injectorPaths: InjectorPath[])
|
|||
|
||||
export function filterOutAngularInjectors(injectorPaths: InjectorPath[]): InjectorPath[] {
|
||||
return injectorPaths.map(({node, path}) => {
|
||||
return {node, path: path.filter(injector => !ignoredAngularInjectors.has(injector.name))};
|
||||
return {node, path: path.filter((injector) => !ignoredAngularInjectors.has(injector.name))};
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,25 +14,49 @@ import {MatExpansionModule} from '@angular/material/expansion';
|
|||
import {MatIconModule} from '@angular/material/icon';
|
||||
import {MatTabsModule} from '@angular/material/tabs';
|
||||
import {MatTooltipModule} from '@angular/material/tooltip';
|
||||
import {ComponentExplorerView, DevToolsNode, Events, MessageBus, SerializedInjector, SerializedProviderRecord} from 'protocol';
|
||||
import {
|
||||
ComponentExplorerView,
|
||||
DevToolsNode,
|
||||
Events,
|
||||
MessageBus,
|
||||
SerializedInjector,
|
||||
SerializedProviderRecord,
|
||||
} from 'protocol';
|
||||
|
||||
import {AngularSplitModule} from '../../vendor/angular-split/public_api';
|
||||
import {InjectorTreeD3Node, InjectorTreeVisualizer} from '../dependency-injection/injector-tree-visualizer';
|
||||
import {
|
||||
InjectorTreeD3Node,
|
||||
InjectorTreeVisualizer,
|
||||
} from '../dependency-injection/injector-tree-visualizer';
|
||||
import {ResolutionPathComponent} from '../dependency-injection/resolution-path.component';
|
||||
|
||||
import {InjectorProvidersComponent} from './injector-providers.component';
|
||||
import {filterOutAngularInjectors, filterOutInjectorsWithNoProviders, generateEdgeIdsFromNodeIds, getInjectorIdsToRootFromNode, grabInjectorPathsFromDirectiveForest, splitInjectorPathsIntoElementAndEnvironmentPaths, transformInjectorResolutionPathsIntoTree} from './injector-tree-fns';
|
||||
|
||||
import {
|
||||
filterOutAngularInjectors,
|
||||
filterOutInjectorsWithNoProviders,
|
||||
generateEdgeIdsFromNodeIds,
|
||||
getInjectorIdsToRootFromNode,
|
||||
grabInjectorPathsFromDirectiveForest,
|
||||
splitInjectorPathsIntoElementAndEnvironmentPaths,
|
||||
transformInjectorResolutionPathsIntoTree,
|
||||
} from './injector-tree-fns';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'ng-injector-tree',
|
||||
imports: [
|
||||
MatButtonModule, AngularSplitModule, ResolutionPathComponent, MatTabsModule, MatExpansionModule,
|
||||
InjectorProvidersComponent, MatIconModule, MatTooltipModule, MatCheckboxModule
|
||||
MatButtonModule,
|
||||
AngularSplitModule,
|
||||
ResolutionPathComponent,
|
||||
MatTabsModule,
|
||||
MatExpansionModule,
|
||||
InjectorProvidersComponent,
|
||||
MatIconModule,
|
||||
MatTooltipModule,
|
||||
MatCheckboxModule,
|
||||
],
|
||||
templateUrl: `./injector-tree.component.html`,
|
||||
styleUrls: ['./injector-tree.component.scss']
|
||||
styleUrls: ['./injector-tree.component.scss'],
|
||||
})
|
||||
export class InjectorTreeComponent {
|
||||
@ViewChild('svgContainer', {static: false}) private svgContainer!: ElementRef;
|
||||
|
|
@ -45,7 +69,7 @@ export class InjectorTreeComponent {
|
|||
zone = inject(NgZone);
|
||||
|
||||
firstRender = true;
|
||||
selectedNode: InjectorTreeD3Node|null = null;
|
||||
selectedNode: InjectorTreeD3Node | null = null;
|
||||
rawDirectiveForest: DevToolsNode[] = [];
|
||||
injectorTreeGraph!: InjectorTreeVisualizer;
|
||||
elementInjectorTreeGraph!: InjectorTreeVisualizer;
|
||||
|
|
@ -71,13 +95,13 @@ export class InjectorTreeComponent {
|
|||
});
|
||||
|
||||
this._messageBus.on(
|
||||
'latestInjectorProviders',
|
||||
(_: SerializedInjector, providers: SerializedProviderRecord[]) => {
|
||||
this.providers = Array.from(providers).sort((a, b) => {
|
||||
return a.token.localeCompare(b.token);
|
||||
});
|
||||
'latestInjectorProviders',
|
||||
(_: SerializedInjector, providers: SerializedProviderRecord[]) => {
|
||||
this.providers = Array.from(providers).sort((a, b) => {
|
||||
return a.token.localeCompare(b.token);
|
||||
});
|
||||
|
||||
},
|
||||
);
|
||||
|
||||
this._messageBus.on('highlightComponent', (id: number) => {
|
||||
const injectorNode = this.getNodeByComponentId(this.elementInjectorTreeGraph, id);
|
||||
|
|
@ -139,7 +163,7 @@ export class InjectorTreeComponent {
|
|||
// In Angular we have two types of injectors, element injectors and environment injectors.
|
||||
// We want to split the resolution paths into two groups, one for each type of injector.
|
||||
const {elementPaths, environmentPaths, startingElementToEnvironmentPath} =
|
||||
splitInjectorPathsIntoElementAndEnvironmentPaths(injectorPaths);
|
||||
splitInjectorPathsIntoElementAndEnvironmentPaths(injectorPaths);
|
||||
this.elementToEnvironmentPath = startingElementToEnvironmentPath;
|
||||
|
||||
// Here for our 2 groups of resolution paths, we want to convert them into a tree structure.
|
||||
|
|
@ -216,7 +240,7 @@ export class InjectorTreeComponent {
|
|||
this.snapToRoot(this.elementInjectorTreeGraph);
|
||||
}
|
||||
|
||||
getNodeByComponentId(graph: InjectorTreeVisualizer, id: number): InjectorTreeD3Node|null {
|
||||
getNodeByComponentId(graph: InjectorTreeVisualizer, id: number): InjectorTreeD3Node | null {
|
||||
const graphElement = graph.graphElement;
|
||||
const element = graphElement.querySelector(`.node[data-component-id="${id}"]`);
|
||||
if (element === null) {
|
||||
|
|
@ -237,8 +261,10 @@ export class InjectorTreeComponent {
|
|||
}
|
||||
|
||||
this.injectorTreeGraph?.cleanup?.();
|
||||
this.injectorTreeGraph =
|
||||
new InjectorTreeVisualizer(this.svgContainer.nativeElement, this.g.nativeElement);
|
||||
this.injectorTreeGraph = new InjectorTreeVisualizer(
|
||||
this.svgContainer.nativeElement,
|
||||
this.g.nativeElement,
|
||||
);
|
||||
}
|
||||
|
||||
setUpElementInjectorVisualizer(): void {
|
||||
|
|
@ -248,8 +274,10 @@ export class InjectorTreeComponent {
|
|||
|
||||
this.elementInjectorTreeGraph?.cleanup?.();
|
||||
this.elementInjectorTreeGraph = new InjectorTreeVisualizer(
|
||||
this.elementSvgContainer.nativeElement, this.elementG.nativeElement,
|
||||
{nodeSeparation: () => 1});
|
||||
this.elementSvgContainer.nativeElement,
|
||||
this.elementG.nativeElement,
|
||||
{nodeSeparation: () => 1},
|
||||
);
|
||||
}
|
||||
|
||||
highlightPathFromSelectedInjector(): void {
|
||||
|
|
@ -266,21 +294,22 @@ export class InjectorTreeComponent {
|
|||
|
||||
if (this.selectedNode.data.injector.type === 'element') {
|
||||
const idsToRoot = getInjectorIdsToRootFromNode(this.selectedNode);
|
||||
idsToRoot.forEach(id => this.highlightNodeById(this.elementG, id));
|
||||
idsToRoot.forEach((id) => this.highlightNodeById(this.elementG, id));
|
||||
const edgeIds = generateEdgeIdsFromNodeIds(idsToRoot);
|
||||
edgeIds.forEach((edgeId => this.highlightEdgeById(this.elementG, edgeId)));
|
||||
edgeIds.forEach((edgeId) => this.highlightEdgeById(this.elementG, edgeId));
|
||||
|
||||
const environmentPath =
|
||||
this.elementToEnvironmentPath.get(this.selectedNode.data.injector.id) ?? [];
|
||||
environmentPath.forEach(injector => this.highlightNodeById(this.g, injector.id));
|
||||
const environmentEdgeIds =
|
||||
generateEdgeIdsFromNodeIds(environmentPath.map(injector => injector.id));
|
||||
environmentEdgeIds.forEach(edgeId => this.highlightEdgeById(this.g, edgeId));
|
||||
this.elementToEnvironmentPath.get(this.selectedNode.data.injector.id) ?? [];
|
||||
environmentPath.forEach((injector) => this.highlightNodeById(this.g, injector.id));
|
||||
const environmentEdgeIds = generateEdgeIdsFromNodeIds(
|
||||
environmentPath.map((injector) => injector.id),
|
||||
);
|
||||
environmentEdgeIds.forEach((edgeId) => this.highlightEdgeById(this.g, edgeId));
|
||||
} else {
|
||||
const idsToRoot = getInjectorIdsToRootFromNode(this.selectedNode);
|
||||
idsToRoot.forEach(id => this.highlightNodeById(this.g, id));
|
||||
idsToRoot.forEach((id) => this.highlightNodeById(this.g, id));
|
||||
const edgeIds = generateEdgeIdsFromNodeIds(idsToRoot);
|
||||
edgeIds.forEach(edgeId => this.highlightEdgeById(this.g, edgeId));
|
||||
edgeIds.forEach((edgeId) => this.highlightEdgeById(this.g, edgeId));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -331,10 +360,12 @@ export class InjectorTreeComponent {
|
|||
return;
|
||||
}
|
||||
const injector = this.selectedNode.data.injector;
|
||||
this._messageBus.emit('getInjectorProviders', [{
|
||||
id: injector.id,
|
||||
type: injector.type,
|
||||
name: injector.name,
|
||||
}]);
|
||||
this._messageBus.emit('getInjectorProviders', [
|
||||
{
|
||||
id: injector.id,
|
||||
type: injector.type,
|
||||
name: injector.name,
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,8 +33,9 @@ export class FileApiService {
|
|||
saveObjectAsJSON(object: object): void {
|
||||
const downloadLink = document.createElement('a');
|
||||
downloadLink.download = `NgDevTools-Profile-${toISO8601Compact(new Date())}.json`;
|
||||
downloadLink.href =
|
||||
URL.createObjectURL(new Blob([JSON.stringify(object)], {type: 'application/json'}));
|
||||
downloadLink.href = URL.createObjectURL(
|
||||
new Blob([JSON.stringify(object)], {type: 'application/json'}),
|
||||
);
|
||||
downloadLink.click();
|
||||
setTimeout(() => URL.revokeObjectURL(downloadLink.href));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ interface DialogData {
|
|||
profilerVersion?: number;
|
||||
importedVersion?: number;
|
||||
errorMessage?: string;
|
||||
status: 'ERROR'|'INVALID_VERSION';
|
||||
status: 'ERROR' | 'INVALID_VERSION';
|
||||
}
|
||||
|
||||
@Component({
|
||||
|
|
@ -23,6 +23,7 @@ interface DialogData {
|
|||
})
|
||||
export class ProfilerImportDialogComponent {
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<ProfilerImportDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: DialogData) {}
|
||||
public dialogRef: MatDialogRef<ProfilerImportDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: DialogData,
|
||||
) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import {Subject, Subscription} from 'rxjs';
|
|||
import {FileApiService} from './file-api-service';
|
||||
import {ProfilerImportDialogComponent} from './profiler-import-dialog.component';
|
||||
|
||||
type State = 'idle'|'recording'|'visualizing';
|
||||
type State = 'idle' | 'recording' | 'visualizing';
|
||||
|
||||
const SUPPORTED_VERSIONS = [1];
|
||||
const PROFILER_VERSION = 1;
|
||||
|
|
@ -32,9 +32,9 @@ export class ProfilerComponent implements OnInit {
|
|||
private _buffer: ProfilerFrame[] = [];
|
||||
|
||||
constructor(
|
||||
private _fileApiService: FileApiService,
|
||||
private _messageBus: MessageBus<Events>,
|
||||
public dialog: MatDialog,
|
||||
private _fileApiService: FileApiService,
|
||||
private _messageBus: MessageBus<Events>,
|
||||
public dialog: MatDialog,
|
||||
) {
|
||||
this._fileApiService.uploadedData.subscribe((importedFile) => {
|
||||
if (importedFile.error) {
|
||||
|
|
|
|||
|
|
@ -35,5 +35,4 @@ import {TimelineModule} from './timeline/timeline.module';
|
|||
],
|
||||
exports: [ProfilerComponent],
|
||||
})
|
||||
export class ProfilerModule {
|
||||
}
|
||||
export class ProfilerModule {}
|
||||
|
|
|
|||
|
|
@ -85,163 +85,191 @@ describe('filtering', () => {
|
|||
describe('filtering', () => {
|
||||
it('should filter results with a source query', () => {
|
||||
const filter = createFilter('source:click');
|
||||
expect(filter({
|
||||
frame: {
|
||||
directives: [],
|
||||
duration: 10,
|
||||
source: 'click',
|
||||
},
|
||||
style: {},
|
||||
toolTip: '',
|
||||
})).toBeTrue();
|
||||
expect(
|
||||
filter({
|
||||
frame: {
|
||||
directives: [],
|
||||
duration: 10,
|
||||
source: 'click',
|
||||
},
|
||||
style: {},
|
||||
toolTip: '',
|
||||
}),
|
||||
).toBeTrue();
|
||||
|
||||
expect(filter({
|
||||
frame: {
|
||||
directives: [],
|
||||
duration: 10,
|
||||
source: 'mouseenter',
|
||||
},
|
||||
style: {},
|
||||
toolTip: '',
|
||||
})).toBeFalse();
|
||||
expect(
|
||||
filter({
|
||||
frame: {
|
||||
directives: [],
|
||||
duration: 10,
|
||||
source: 'mouseenter',
|
||||
},
|
||||
style: {},
|
||||
toolTip: '',
|
||||
}),
|
||||
).toBeFalse();
|
||||
});
|
||||
|
||||
it('should filter results with a duration query', () => {
|
||||
const filter1 = createFilter('duration:>10ms');
|
||||
expect(filter1({
|
||||
frame: {
|
||||
directives: [],
|
||||
duration: 10,
|
||||
source: 'click',
|
||||
},
|
||||
style: {},
|
||||
toolTip: '',
|
||||
})).toBeFalse();
|
||||
expect(
|
||||
filter1({
|
||||
frame: {
|
||||
directives: [],
|
||||
duration: 10,
|
||||
source: 'click',
|
||||
},
|
||||
style: {},
|
||||
toolTip: '',
|
||||
}),
|
||||
).toBeFalse();
|
||||
|
||||
expect(filter1({
|
||||
frame: {
|
||||
directives: [],
|
||||
duration: 15,
|
||||
source: 'mouseenter',
|
||||
},
|
||||
style: {},
|
||||
toolTip: '',
|
||||
})).toBeTrue();
|
||||
expect(
|
||||
filter1({
|
||||
frame: {
|
||||
directives: [],
|
||||
duration: 15,
|
||||
source: 'mouseenter',
|
||||
},
|
||||
style: {},
|
||||
toolTip: '',
|
||||
}),
|
||||
).toBeTrue();
|
||||
|
||||
const filter2 = createFilter('duration:=10ms');
|
||||
expect(filter2({
|
||||
frame: {
|
||||
directives: [],
|
||||
duration: 11,
|
||||
source: 'mouseenter',
|
||||
},
|
||||
style: {},
|
||||
toolTip: '',
|
||||
})).toBeFalse();
|
||||
expect(filter2({
|
||||
frame: {
|
||||
directives: [],
|
||||
duration: 10,
|
||||
source: 'mouseenter',
|
||||
},
|
||||
style: {},
|
||||
toolTip: '',
|
||||
})).toBeTrue();
|
||||
expect(
|
||||
filter2({
|
||||
frame: {
|
||||
directives: [],
|
||||
duration: 11,
|
||||
source: 'mouseenter',
|
||||
},
|
||||
style: {},
|
||||
toolTip: '',
|
||||
}),
|
||||
).toBeFalse();
|
||||
expect(
|
||||
filter2({
|
||||
frame: {
|
||||
directives: [],
|
||||
duration: 10,
|
||||
source: 'mouseenter',
|
||||
},
|
||||
style: {},
|
||||
toolTip: '',
|
||||
}),
|
||||
).toBeTrue();
|
||||
});
|
||||
|
||||
it('should work with composite selectors', () => {
|
||||
const filter = createFilter('duration:>10ms source: click');
|
||||
expect(filter({
|
||||
frame: {
|
||||
directives: [],
|
||||
duration: 10,
|
||||
source: 'click',
|
||||
},
|
||||
style: {},
|
||||
toolTip: '',
|
||||
})).toBeFalse();
|
||||
expect(
|
||||
filter({
|
||||
frame: {
|
||||
directives: [],
|
||||
duration: 10,
|
||||
source: 'click',
|
||||
},
|
||||
style: {},
|
||||
toolTip: '',
|
||||
}),
|
||||
).toBeFalse();
|
||||
|
||||
expect(filter({
|
||||
frame: {
|
||||
directives: [],
|
||||
duration: 15,
|
||||
source: 'mouseenter',
|
||||
},
|
||||
style: {},
|
||||
toolTip: '',
|
||||
})).toBeFalse();
|
||||
expect(
|
||||
filter({
|
||||
frame: {
|
||||
directives: [],
|
||||
duration: 15,
|
||||
source: 'mouseenter',
|
||||
},
|
||||
style: {},
|
||||
toolTip: '',
|
||||
}),
|
||||
).toBeFalse();
|
||||
|
||||
expect(filter({
|
||||
frame: {
|
||||
directives: [],
|
||||
duration: 15,
|
||||
source: 'click',
|
||||
},
|
||||
style: {},
|
||||
toolTip: '',
|
||||
})).toBeTrue();
|
||||
expect(
|
||||
filter({
|
||||
frame: {
|
||||
directives: [],
|
||||
duration: 15,
|
||||
source: 'click',
|
||||
},
|
||||
style: {},
|
||||
toolTip: '',
|
||||
}),
|
||||
).toBeTrue();
|
||||
});
|
||||
|
||||
it('should work with invalid arguments', () => {
|
||||
const filter = createFilter('duration:>ms');
|
||||
|
||||
expect(filter({
|
||||
frame: {
|
||||
directives: [],
|
||||
duration: 15,
|
||||
source: 'click',
|
||||
},
|
||||
style: {},
|
||||
toolTip: '',
|
||||
})).toBeTrue();
|
||||
expect(
|
||||
filter({
|
||||
frame: {
|
||||
directives: [],
|
||||
duration: 15,
|
||||
source: 'click',
|
||||
},
|
||||
style: {},
|
||||
toolTip: '',
|
||||
}),
|
||||
).toBeTrue();
|
||||
});
|
||||
|
||||
it('should work with negation', () => {
|
||||
const filter = createFilter('!source:message');
|
||||
|
||||
expect(filter({
|
||||
frame: {
|
||||
directives: [],
|
||||
duration: 15,
|
||||
source: 'message',
|
||||
},
|
||||
style: {},
|
||||
toolTip: '',
|
||||
})).toBeFalse();
|
||||
expect(
|
||||
filter({
|
||||
frame: {
|
||||
directives: [],
|
||||
duration: 15,
|
||||
source: 'message',
|
||||
},
|
||||
style: {},
|
||||
toolTip: '',
|
||||
}),
|
||||
).toBeFalse();
|
||||
});
|
||||
|
||||
it('should work with negation and composite expressions', () => {
|
||||
const filter = createFilter('!duration:=15 !source:message source:click');
|
||||
|
||||
expect(filter({
|
||||
frame: {
|
||||
directives: [],
|
||||
duration: 15,
|
||||
source: 'click',
|
||||
},
|
||||
style: {},
|
||||
toolTip: '',
|
||||
})).toBeFalse();
|
||||
expect(
|
||||
filter({
|
||||
frame: {
|
||||
directives: [],
|
||||
duration: 15,
|
||||
source: 'click',
|
||||
},
|
||||
style: {},
|
||||
toolTip: '',
|
||||
}),
|
||||
).toBeFalse();
|
||||
|
||||
expect(filter({
|
||||
frame: {
|
||||
directives: [],
|
||||
duration: 10,
|
||||
source: 'message',
|
||||
},
|
||||
style: {},
|
||||
toolTip: '',
|
||||
})).toBeFalse();
|
||||
expect(
|
||||
filter({
|
||||
frame: {
|
||||
directives: [],
|
||||
duration: 10,
|
||||
source: 'message',
|
||||
},
|
||||
style: {},
|
||||
toolTip: '',
|
||||
}),
|
||||
).toBeFalse();
|
||||
|
||||
expect(filter({
|
||||
frame: {
|
||||
directives: [],
|
||||
duration: 14,
|
||||
source: 'click',
|
||||
},
|
||||
style: {},
|
||||
toolTip: '',
|
||||
})).toBeTrue();
|
||||
expect(
|
||||
filter({
|
||||
frame: {
|
||||
directives: [],
|
||||
duration: 14,
|
||||
source: 'click',
|
||||
},
|
||||
style: {},
|
||||
toolTip: '',
|
||||
}),
|
||||
).toBeTrue();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -16,11 +16,11 @@ export const noopFilter = (_: GraphNode) => true;
|
|||
|
||||
interface Query<Arguments = unknown> {
|
||||
readonly name: QueryType;
|
||||
parseArguments(args: string[]): Arguments|undefined;
|
||||
parseArguments(args: string[]): Arguments | undefined;
|
||||
apply(node: ProfilerFrame, args: Arguments): boolean;
|
||||
}
|
||||
|
||||
type Operator = '>'|'<'|'='|'<='|'>=';
|
||||
type Operator = '>' | '<' | '=' | '<=' | '>=';
|
||||
|
||||
const ops: {[operator in Operator]: (a: number, b: number) => boolean} = {
|
||||
'>'(a: number, b: number): boolean {
|
||||
|
|
@ -48,12 +48,12 @@ const enum QueryType {
|
|||
Source = 'source',
|
||||
}
|
||||
|
||||
type QueryArguments = DurationArgument|SourceArgument;
|
||||
type QueryArguments = DurationArgument | SourceArgument;
|
||||
|
||||
const operatorRe = /^(>=|<=|=|<|>|)/;
|
||||
class DurationQuery implements Query<DurationArgument> {
|
||||
readonly name = QueryType.Duration;
|
||||
parseArguments([arg]: string[]): DurationArgument|undefined {
|
||||
parseArguments([arg]: string[]): DurationArgument | undefined {
|
||||
arg = arg.trim();
|
||||
const operator = (arg.match(operatorRe) ?? [null])[0];
|
||||
if (!operator) {
|
||||
|
|
@ -82,15 +82,17 @@ class SourceQuery implements Query<SourceArgument> {
|
|||
}
|
||||
}
|
||||
|
||||
const queryMap: {[query in QueryType]: Query} =
|
||||
[new DurationQuery(), new SourceQuery()].reduce((map, query) => {
|
||||
map[query.name] = query;
|
||||
return map;
|
||||
}, {} as {[query in QueryType]: Query});
|
||||
const queryMap: {[query in QueryType]: Query} = [new DurationQuery(), new SourceQuery()].reduce(
|
||||
(map, query) => {
|
||||
map[query.name] = query;
|
||||
return map;
|
||||
},
|
||||
{} as {[query in QueryType]: Query},
|
||||
);
|
||||
|
||||
const queryRe = new RegExp(`!?s*(${QueryType.Duration}|${QueryType.Source})$`, 'g');
|
||||
|
||||
type Predicate = true|false;
|
||||
type Predicate = true | false;
|
||||
type QueryAST = [Predicate, QueryType, QueryArguments];
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -7,7 +7,15 @@
|
|||
*/
|
||||
|
||||
import {CdkVirtualScrollViewport} from '@angular/cdk/scrolling';
|
||||
import {Component, DestroyRef, ElementRef, EventEmitter, Input, Output, ViewChild,} from '@angular/core';
|
||||
import {
|
||||
Component,
|
||||
DestroyRef,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
Input,
|
||||
Output,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||
import {Observable, Subscription} from 'rxjs';
|
||||
|
||||
|
|
@ -28,11 +36,14 @@ export class FrameSelectorComponent {
|
|||
set graphData$(graphData: Observable<GraphNode[]>) {
|
||||
this._graphData$ = graphData;
|
||||
this._graphDataSubscription?.unsubscribe();
|
||||
this._graphDataSubscription = this._graphData$.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((items) => setTimeout(() => {
|
||||
this.frameCount = items.length;
|
||||
this.viewport.scrollToIndex(items.length);
|
||||
}));
|
||||
this._graphDataSubscription = this._graphData$
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((items) =>
|
||||
setTimeout(() => {
|
||||
this.frameCount = items.length;
|
||||
this.viewport.scrollToIndex(items.length);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
get graphData$(): Observable<GraphNode[]> {
|
||||
|
|
@ -57,10 +68,9 @@ export class FrameSelectorComponent {
|
|||
private _graphData$!: Observable<GraphNode[]>;
|
||||
private _graphDataSubscription?: Subscription;
|
||||
|
||||
|
||||
constructor(
|
||||
private _tabUpdate: TabUpdate,
|
||||
private destroyRef: DestroyRef,
|
||||
private _tabUpdate: TabUpdate,
|
||||
private destroyRef: DestroyRef,
|
||||
) {
|
||||
this._tabUpdate.tabUpdate$.pipe(takeUntilDestroyed()).subscribe(() => {
|
||||
if (this.viewport) {
|
||||
|
|
@ -84,7 +94,7 @@ export class FrameSelectorComponent {
|
|||
const sortedIndexes = indexArray.sort((a, b) => a - b);
|
||||
|
||||
const groups: number[][] = [];
|
||||
let prev: number|null = null;
|
||||
let prev: number | null = null;
|
||||
|
||||
for (const index of sortedIndexes) {
|
||||
// First iteration: create initial group and set prev variable to the first index
|
||||
|
|
@ -104,12 +114,12 @@ export class FrameSelectorComponent {
|
|||
prev = index;
|
||||
}
|
||||
|
||||
return groups.filter((group) => group.length > 0)
|
||||
.map(
|
||||
(group) => group.length === 1 ? group[0] + 1 :
|
||||
`${group[0] + 1}-${group[group.length - 1] + 1}`,
|
||||
)
|
||||
.join(', ');
|
||||
return groups
|
||||
.filter((group) => group.length > 0)
|
||||
.map((group) =>
|
||||
group.length === 1 ? group[0] + 1 : `${group[0] + 1}-${group[group.length - 1] + 1}`,
|
||||
)
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
move(value: number): void {
|
||||
|
|
@ -134,12 +144,12 @@ export class FrameSelectorComponent {
|
|||
if (shiftKey) {
|
||||
const [start, end] = [Math.min(this.startFrameIndex, idx), Math.max(this.endFrameIndex, idx)];
|
||||
this.selectedFrameIndexes = new Set(
|
||||
Array.from(Array(end - start + 1), (_, index) => index + start),
|
||||
Array.from(Array(end - start + 1), (_, index) => index + start),
|
||||
);
|
||||
} else if (ctrlKey || metaKey) {
|
||||
if (this.selectedFrameIndexes.has(idx)) {
|
||||
if (this.selectedFrameIndexes.size === 1) {
|
||||
return; // prevent deselection when only one frame is selected
|
||||
return; // prevent deselection when only one frame is selected
|
||||
}
|
||||
|
||||
this.selectedFrameIndexes.delete(idx);
|
||||
|
|
@ -190,6 +200,6 @@ export class FrameSelectorComponent {
|
|||
const dragScrollSpeed = 2;
|
||||
const dx = event.clientX - this._viewportScrollState.xCoordinate;
|
||||
this.viewport.elementRef.nativeElement.scrollLeft =
|
||||
this._viewportScrollState.scrollLeft - dx * dragScrollSpeed;
|
||||
this._viewportScrollState.scrollLeft - dx * dragScrollSpeed;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export interface BargraphNode {
|
|||
value: number;
|
||||
label: string;
|
||||
original: ElementProfile;
|
||||
count: number; // number of merged nodes with the same label
|
||||
count: number; // number of merged nodes with the same label
|
||||
directives?: DirectiveProfile[];
|
||||
}
|
||||
|
||||
|
|
@ -55,7 +55,10 @@ export class BarGraphFormatter extends RecordFormatter<BargraphNode[]> {
|
|||
}
|
||||
|
||||
override addFrame(
|
||||
nodes: BargraphNode[], elements: ElementProfile[], parents: ElementProfile[] = []): number {
|
||||
nodes: BargraphNode[],
|
||||
elements: ElementProfile[],
|
||||
parents: ElementProfile[] = [],
|
||||
): number {
|
||||
let timeSpent = 0;
|
||||
elements.forEach((element) => {
|
||||
// Possibly undefined because of the insertion on the backend.
|
||||
|
|
|
|||
|
|
@ -7,7 +7,12 @@
|
|||
*/
|
||||
|
||||
import {AppEntry} from '../record-formatter';
|
||||
import {NESTED_FORMATTED_FLAMEGRAPH_RECORD, NESTED_RECORD, SIMPLE_FORMATTED_FLAMEGRAPH_RECORD, SIMPLE_RECORD,} from '../record-formatter-spec-constants';
|
||||
import {
|
||||
NESTED_FORMATTED_FLAMEGRAPH_RECORD,
|
||||
NESTED_RECORD,
|
||||
SIMPLE_FORMATTED_FLAMEGRAPH_RECORD,
|
||||
SIMPLE_RECORD,
|
||||
} from '../record-formatter-spec-constants';
|
||||
|
||||
import {FlamegraphFormatter, FlamegraphNode} from './flamegraph-formatter';
|
||||
|
||||
|
|
|
|||
|
|
@ -24,8 +24,11 @@ export interface FlamegraphNode {
|
|||
export const ROOT_LEVEL_ELEMENT_LABEL = 'Entire application';
|
||||
|
||||
export class FlamegraphFormatter extends RecordFormatter<FlamegraphNode> {
|
||||
override formatFrame(frame: ProfilerFrame, showChangeDetection?: boolean, theme?: Theme):
|
||||
FlamegraphNode {
|
||||
override formatFrame(
|
||||
frame: ProfilerFrame,
|
||||
showChangeDetection?: boolean,
|
||||
theme?: Theme,
|
||||
): FlamegraphNode {
|
||||
const result: FlamegraphNode = {
|
||||
value: 0,
|
||||
label: ROOT_LEVEL_ELEMENT_LABEL,
|
||||
|
|
@ -40,7 +43,7 @@ export class FlamegraphFormatter extends RecordFormatter<FlamegraphNode> {
|
|||
|
||||
if (showChangeDetection) {
|
||||
result.color =
|
||||
theme === 'dark-theme' ? CHANGE_DETECTION_COLOR_DARK : CHANGE_DETECTION_COLOR_LIGHT;
|
||||
theme === 'dark-theme' ? CHANGE_DETECTION_COLOR_DARK : CHANGE_DETECTION_COLOR_LIGHT;
|
||||
}
|
||||
|
||||
this.addFrame(result.children, frame.directives, showChangeDetection, theme);
|
||||
|
|
@ -48,8 +51,11 @@ export class FlamegraphFormatter extends RecordFormatter<FlamegraphNode> {
|
|||
}
|
||||
|
||||
override addFrame(
|
||||
nodes: FlamegraphNode[], elements: ElementProfile[], showChangeDetection?: boolean,
|
||||
theme?: Theme): number {
|
||||
nodes: FlamegraphNode[],
|
||||
elements: ElementProfile[],
|
||||
showChangeDetection?: boolean,
|
||||
theme?: Theme,
|
||||
): number {
|
||||
let timeSpent = 0;
|
||||
elements.forEach((element) => {
|
||||
// Possibly undefined because of
|
||||
|
|
@ -69,7 +75,7 @@ export class FlamegraphFormatter extends RecordFormatter<FlamegraphNode> {
|
|||
};
|
||||
if (showChangeDetection) {
|
||||
const CHANGE_DETECTION_COLOR =
|
||||
theme === 'dark-theme' ? CHANGE_DETECTION_COLOR_DARK : CHANGE_DETECTION_COLOR_LIGHT;
|
||||
theme === 'dark-theme' ? CHANGE_DETECTION_COLOR_DARK : CHANGE_DETECTION_COLOR_LIGHT;
|
||||
node.color = changeDetected ? CHANGE_DETECTION_COLOR : NO_CHANGE_DETECTION_COLOR;
|
||||
}
|
||||
timeSpent += this.addFrame(node.children, element.children, showChangeDetection, theme);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import {DirectiveProfile, ElementProfile, LifecycleProfile, ProfilerFrame} from 'protocol';
|
||||
|
||||
const mergeProperty = (mergeInProp: number|undefined, value: number|undefined) => {
|
||||
const mergeProperty = (mergeInProp: number | undefined, value: number | undefined) => {
|
||||
if (mergeInProp === undefined) {
|
||||
return value;
|
||||
}
|
||||
|
|
@ -52,7 +52,7 @@ const mergeFrame = (mergeIn: ProfilerFrame, second: ProfilerFrame) => {
|
|||
mergeDirectives(mergeIn.directives, second.directives);
|
||||
};
|
||||
|
||||
export const mergeFrames = (frames: ProfilerFrame[]): ProfilerFrame|null => {
|
||||
export const mergeFrames = (frames: ProfilerFrame[]): ProfilerFrame | null => {
|
||||
if (!frames || !frames.length) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -334,15 +334,10 @@ export const NESTED_FORMATTED_FLAMEGRAPH_RECORD: FlamegraphNode[] = [
|
|||
changeDetected: true,
|
||||
children: [],
|
||||
instances: 1,
|
||||
original: NESTED_RECORD[0]
|
||||
.children[0]
|
||||
.children[0]
|
||||
.children[0]
|
||||
.children[0]
|
||||
.children[0]
|
||||
.children[0]
|
||||
.children[0]
|
||||
.children[0],
|
||||
original:
|
||||
NESTED_RECORD[0].children[0].children[0].children[0]
|
||||
.children[0].children[0].children[0].children[0]
|
||||
.children[0],
|
||||
},
|
||||
{
|
||||
value: 0,
|
||||
|
|
@ -350,15 +345,10 @@ export const NESTED_FORMATTED_FLAMEGRAPH_RECORD: FlamegraphNode[] = [
|
|||
changeDetected: true,
|
||||
children: [],
|
||||
instances: 1,
|
||||
original: NESTED_RECORD[0]
|
||||
.children[0]
|
||||
.children[0]
|
||||
.children[0]
|
||||
.children[0]
|
||||
.children[0]
|
||||
.children[0]
|
||||
.children[0]
|
||||
.children[1],
|
||||
original:
|
||||
NESTED_RECORD[0].children[0].children[0].children[0]
|
||||
.children[0].children[0].children[0].children[0]
|
||||
.children[1],
|
||||
},
|
||||
{
|
||||
value: 0,
|
||||
|
|
@ -366,38 +356,28 @@ export const NESTED_FORMATTED_FLAMEGRAPH_RECORD: FlamegraphNode[] = [
|
|||
changeDetected: true,
|
||||
children: [],
|
||||
instances: 1,
|
||||
original: NESTED_RECORD[0]
|
||||
.children[0]
|
||||
.children[0]
|
||||
.children[0]
|
||||
.children[0]
|
||||
.children[0]
|
||||
.children[0]
|
||||
.children[0]
|
||||
.children[2],
|
||||
original:
|
||||
NESTED_RECORD[0].children[0].children[0].children[0]
|
||||
.children[0].children[0].children[0].children[0]
|
||||
.children[2],
|
||||
},
|
||||
],
|
||||
instances: 1,
|
||||
original: NESTED_RECORD[0]
|
||||
.children[0]
|
||||
.children[0]
|
||||
.children[0]
|
||||
.children[0]
|
||||
.children[0]
|
||||
.children[0]
|
||||
.children[0],
|
||||
original:
|
||||
NESTED_RECORD[0].children[0].children[0].children[0].children[0]
|
||||
.children[0].children[0].children[0],
|
||||
},
|
||||
],
|
||||
instances: 1,
|
||||
original:
|
||||
NESTED_RECORD[0].children[0].children[0].children[0].children
|
||||
[0].children
|
||||
[0].children[0],
|
||||
NESTED_RECORD[0].children[0].children[0].children[0].children[0]
|
||||
.children[0].children[0],
|
||||
},
|
||||
],
|
||||
instances: 1,
|
||||
original: NESTED_RECORD[0].children[0].children[0].children[0].children
|
||||
[0].children[0],
|
||||
original:
|
||||
NESTED_RECORD[0].children[0].children[0].children[0].children[0]
|
||||
.children[0],
|
||||
},
|
||||
],
|
||||
instances: 1,
|
||||
|
|
|
|||
|
|
@ -28,13 +28,15 @@ describe('getValue cases', () => {
|
|||
it('calculates value with no lifecycle hooks', () => {
|
||||
element = {
|
||||
children: [],
|
||||
directives: [{
|
||||
changeDetection: 10,
|
||||
isElement: false,
|
||||
isComponent: true,
|
||||
lifecycle: {},
|
||||
name: 'AppComponent'
|
||||
}],
|
||||
directives: [
|
||||
{
|
||||
changeDetection: 10,
|
||||
isElement: false,
|
||||
isComponent: true,
|
||||
lifecycle: {},
|
||||
name: 'AppComponent',
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(formatter.getValue(element)).toBe(10);
|
||||
});
|
||||
|
|
@ -48,7 +50,7 @@ describe('getValue cases', () => {
|
|||
isElement: false,
|
||||
name: 'NgForOf',
|
||||
lifecycle: {ngDoCheck: 5},
|
||||
changeDetection: 0
|
||||
changeDetection: 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -64,7 +66,7 @@ describe('getValue cases', () => {
|
|||
isElement: false,
|
||||
name: 'NgForOf',
|
||||
lifecycle: {ngDoCheck: 5},
|
||||
changeDetection: 10
|
||||
changeDetection: 10,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -143,7 +145,7 @@ describe('getLabel cases', () => {
|
|||
isComponent: true,
|
||||
lifecycle: {},
|
||||
outputs: {},
|
||||
name: 'TodoComponent'
|
||||
name: 'TodoComponent',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -176,7 +178,7 @@ describe('getLabel cases', () => {
|
|||
isComponent: true,
|
||||
lifecycle: {},
|
||||
outputs: {},
|
||||
name: 'TodoComponent'
|
||||
name: 'TodoComponent',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -26,13 +26,16 @@ export interface GraphNode {
|
|||
|
||||
export abstract class RecordFormatter<T> {
|
||||
abstract formatFrame(frame: ProfilerFrame): T;
|
||||
abstract addFrame(nodes: T|T[], elements: ElementProfile[]): number|void;
|
||||
abstract addFrame(nodes: T | T[], elements: ElementProfile[]): number | void;
|
||||
|
||||
getLabel(element: ElementProfile): string {
|
||||
const name = element.directives.filter((d) => d.isComponent).map((c) => c.name).join(', ');
|
||||
const attributes =
|
||||
[...new Set(element.directives.filter((d) => !d.isComponent).map((d) => d.name))].join(
|
||||
', ');
|
||||
const name = element.directives
|
||||
.filter((d) => d.isComponent)
|
||||
.map((c) => c.name)
|
||||
.join(', ');
|
||||
const attributes = [
|
||||
...new Set(element.directives.filter((d) => !d.isComponent).map((d) => d.name)),
|
||||
].join(', ');
|
||||
return attributes === '' ? name : `${name}[${attributes}]`;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export interface TreeMapNode {
|
|||
value: number;
|
||||
size: number;
|
||||
children: TreeMapNode[];
|
||||
original: ElementProfile|null;
|
||||
original: ElementProfile | null;
|
||||
}
|
||||
|
||||
export class TreeMapFormatter extends RecordFormatter<TreeMapNode> {
|
||||
|
|
@ -37,7 +37,10 @@ export class TreeMapFormatter extends RecordFormatter<TreeMapNode> {
|
|||
}
|
||||
|
||||
override addFrame(
|
||||
nodes: TreeMapNode[], elements: ElementProfile[], prev: TreeMapNode|null = null): void {
|
||||
nodes: TreeMapNode[],
|
||||
elements: ElementProfile[],
|
||||
prev: TreeMapNode | null = null,
|
||||
): void {
|
||||
elements.forEach((element) => {
|
||||
if (!element) {
|
||||
console.error('Unable to insert undefined element');
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue