feat(devtools): tone down obtrusive elements in the Injector Tree tab (#59499)

Some of the elements are a bit too large so they have been shrunken to fit in with the rest of the UI.

PR Close #59499
This commit is contained in:
hawkgs 2025-01-10 17:15:38 +02:00 committed by kirjs
parent 47e48b44dc
commit 0bb81c5ab4
9 changed files with 313 additions and 232 deletions

View file

@ -1,7 +1,7 @@
load("//devtools/tools:ng_module.bzl", "ng_module")
load("@io_bazel_rules_sass//:defs.bzl", "sass_binary")
load("//devtools/tools:typescript.bzl", "ts_library", "ts_test_library")
load("//devtools/tools:defaults.bzl", "karma_web_test_suite")
load("//devtools/tools:ng_module.bzl", "ng_module")
load("//devtools/tools:typescript.bzl", "ts_library", "ts_test_library")
package(default_visibility = ["//:__subpackages__"])
@ -20,10 +20,10 @@ ng_module(
"injector-tree.component.html",
],
deps = [
":injector_providers",
":injector_tree_fns",
"//devtools/projects/ng-devtools/src/lib/devtools-tabs/dependency-injection:injector_tree_visualizer",
"//devtools/projects/ng-devtools/src/lib/devtools-tabs/dependency-injection:resolution_path",
"//devtools/projects/ng-devtools/src/lib/devtools-tabs/injector-tree/injector-providers",
"//devtools/projects/ng-devtools/src/lib/vendor/angular-split",
"//devtools/projects/protocol",
"//packages/common",
@ -35,21 +35,6 @@ ng_module(
],
)
ng_module(
name = "injector_providers",
srcs = [
"injector-providers.component.ts",
],
deps = [
"//devtools/projects/ng-devtools/src/lib/devtools-tabs/dependency-injection:resolution_path",
"//devtools/projects/protocol",
"//packages/animations",
"//packages/common",
"//packages/core",
"@npm//@angular/material",
],
)
karma_web_test_suite(
name = "test",
deps = [

View file

@ -1,205 +0,0 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import {Component, computed, inject, input, signal} from '@angular/core';
import {MatOption} from '@angular/material/core';
import {MatFormField, MatLabel} from '@angular/material/form-field';
import {MatIcon} from '@angular/material/icon';
import {MatInput} from '@angular/material/input';
import {MatSelect} from '@angular/material/select';
import {MatTableModule} from '@angular/material/table';
import {MatTooltip} from '@angular/material/tooltip';
import {Events, MessageBus, SerializedInjector, SerializedProviderRecord} from 'protocol';
@Component({
selector: 'ng-injector-providers',
template: `
<h1 class="providers-title">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 (visibleProviders().length > 0) {
<table mat-table [dataSource]="visibleProviders()" 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>
}
</div>
}
`,
styles: [
`
.select {
cursor: pointer;
}
:host {
display: block;
padding: 16px;
}
.form-field-spacer {
margin: 0 4px 0 4px;
}
table {
width: 100%;
}
.column-title {
margin: 0;
}
tr.example-detail-row {
height: 0;
}
.example-element-row td {
border-bottom-width: 0;
cursor: pointer;
}
.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-symbol {
font-weight: bold;
font-size: 40px;
line-height: normal;
}
.example-element-description {
padding: 16px;
}
.example-element-description-attribution {
opacity: 0.5;
}
:host-context(.dark-theme) {
.providers-title {
color: #ffffff;
}
}
`,
],
imports: [
MatTableModule,
MatIcon,
MatTooltip,
MatInput,
MatSelect,
MatFormField,
MatLabel,
MatOption,
],
})
export class InjectorProvidersComponent {
readonly injector = input.required<SerializedInjector>();
readonly providers = input<SerializedProviderRecord[]>([]);
readonly searchToken = signal('');
readonly searchType = signal('');
readonly visibleProviders = computed(() => {
const searchToken = this.searchToken().toLowerCase();
const searchType = this.searchType();
const predicates: ((provider: SerializedProviderRecord) => boolean)[] = [];
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)),
);
});
providerTypeToLabel = {
type: 'Type',
existing: 'useExisting',
factory: 'useFactory',
class: 'useClass',
value: 'useValue',
};
providerTypes = Object.keys(this.providerTypeToLabel);
messageBus = inject<MessageBus<Events>>(MessageBus);
select(row: SerializedProviderRecord) {
const {id, type, name} = this.injector();
this.messageBus.emit('logProvider', [{id, type, name}, row]);
}
get displayedColumns(): string[] {
if (this.injector()?.type === 'element') {
return ['token', 'type', 'isViewProvider', 'log'];
}
return ['token', 'type', 'log'];
}
}

View file

@ -0,0 +1,32 @@
load("@io_bazel_rules_sass//:defs.bzl", "sass_binary")
load("//devtools/tools:ng_module.bzl", "ng_module")
package(default_visibility = ["//visibility:public"])
sass_binary(
name = "injector_providers_component_styles",
src = "injector-providers.component.scss",
include_paths = [
"external/npm/node_modules",
],
deps = ["//devtools:material_sass_deps"],
)
ng_module(
name = "injector-providers",
srcs = [
"injector-providers.component.ts",
],
angular_assets = [
"injector-providers.component.html",
":injector_providers_component_styles",
],
deps = [
"//devtools/projects/ng-devtools/src/lib/devtools-tabs/dependency-injection:resolution_path",
"//devtools/projects/protocol",
"//packages/animations",
"//packages/common",
"//packages/core",
"@npm//@angular/material",
],
)

View file

@ -0,0 +1,65 @@
<h2 class="providers-title">Providers for {{ injector()?.name }}</h2>
@if (injector()) {
<div class="injector-providers">
<div class="filter">
<mat-form-field appearance="outline">
<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 appearance="outline">
<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>
</div>
@if (visibleProviders().length > 0) {
<table mat-table [dataSource]="visibleProviders()" 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>
}
</div>
}

View file

@ -0,0 +1,89 @@
@use '@angular/material' as mat;
h2 {
font-size: 0.875rem;
}
.select {
cursor: pointer;
}
:host {
display: block;
padding: 16px;
}
.filter {
display: flex;
gap: 0.5rem;
mat-form-field {
@include mat.form-field-density(-5);
@include mat.form-field-overrides(
(
container-text-size: 0.8rem,
outlined-label-text-size: 0.8rem,
)
);
&:first-child {
flex: 1;
}
}
}
table {
width: 100%;
--mat-table-row-item-label-text-size: 0.8rem;
--mat-table-header-container-height: 42px;
--mat-table-row-item-container-height: 42px;
}
.column-title {
margin: 0;
font-size: 0.8rem;
font-weight: bold;
}
tr.example-detail-row {
height: 0;
}
.example-element-row td {
border-bottom-width: 0;
cursor: pointer;
}
.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-symbol {
font-weight: bold;
font-size: 40px;
line-height: normal;
}
.example-element-description {
padding: 16px;
}
.example-element-description-attribution {
opacity: 0.5;
}
:host-context(.dark-theme) {
.providers-title {
color: #ffffff;
}
}

View file

@ -0,0 +1,77 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import {Component, computed, inject, input, signal} from '@angular/core';
import {MatOption} from '@angular/material/core';
import {MatFormField, MatLabel} from '@angular/material/form-field';
import {MatIcon} from '@angular/material/icon';
import {MatInput} from '@angular/material/input';
import {MatSelect} from '@angular/material/select';
import {MatTableModule} from '@angular/material/table';
import {MatTooltip} from '@angular/material/tooltip';
import {Events, MessageBus, SerializedInjector, SerializedProviderRecord} from 'protocol';
@Component({
selector: 'ng-injector-providers',
templateUrl: './injector-providers.component.html',
styleUrl: './injector-providers.component.scss',
imports: [
MatTableModule,
MatIcon,
MatTooltip,
MatInput,
MatSelect,
MatFormField,
MatLabel,
MatOption,
],
})
export class InjectorProvidersComponent {
readonly injector = input.required<SerializedInjector>();
readonly providers = input<SerializedProviderRecord[]>([]);
readonly searchToken = signal('');
readonly searchType = signal('');
readonly visibleProviders = computed(() => {
const searchToken = this.searchToken().toLowerCase();
const searchType = this.searchType();
const predicates: ((provider: SerializedProviderRecord) => boolean)[] = [];
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)),
);
});
providerTypeToLabel = {
type: 'Type',
existing: 'useExisting',
factory: 'useFactory',
class: 'useClass',
value: 'useValue',
};
providerTypes = Object.keys(this.providerTypeToLabel);
messageBus = inject<MessageBus<Events>>(MessageBus);
select(row: SerializedProviderRecord) {
const {id, type, name} = this.injector();
this.messageBus.emit('logProvider', [{id, type, name}, row]);
}
get displayedColumns(): string[] {
if (this.injector()?.type === 'element') {
return ['token', 'type', 'isViewProvider', 'log'];
}
return ['token', 'type', 'log'];
}
}

View file

@ -11,10 +11,14 @@
[disabled]="true"
>
<as-split-area size="50">
<mat-checkbox (change)="toggleHideInjectorsWithNoProviders()">
Hide injectors with no providers
</mat-checkbox>
<mat-checkbox (change)="toggleHideAngularInjectors()"> Hide framework injectors </mat-checkbox>
<div class="options">
<mat-checkbox (change)="toggleHideInjectorsWithNoProviders()">
Hide injectors with no providers
</mat-checkbox>
<mat-checkbox (change)="toggleHideAngularInjectors()">
Hide framework injectors
</mat-checkbox>
</div>
</as-split-area>
<as-split-area>
<as-split unit="percent" direction="horizontal" [gutterSize]="9">
@ -23,7 +27,7 @@
<as-split-area size="35">
<div class="injector-hierarchy">
<h2>
Environment Hierarchy
<span>Environment Hierarchy</span>
<a
class="hierarchy-ref"
href="https://angular.dev/guide/di/hierarchical-dependency-injection#types-of-injector-hierarchies"
@ -42,7 +46,7 @@
<as-split-area size="65">
<div class="injector-hierarchy">
<h2>
Element Hierarchy
<span>Element Hierarchy</span>
<a
class="hierarchy-ref"
href="https://angular.dev/guide/di/hierarchical-dependency-injection#types-of-injector-hierarchies"

View file

@ -133,6 +133,30 @@ as-split-area {
}
}
.options {
padding: 0.5rem;
display: flex;
align-items: center;
gap: 2rem;
mat-checkbox {
$checkbox-size: 14px;
--mdc-checkbox-state-layer-size: 2rem;
--mat-checkbox-label-text-size: 0.8rem;
::ng-deep .mdc-checkbox {
width: $checkbox-size;
height: $checkbox-size;
flex: 0 0 $checkbox-size;
&__background {
width: $checkbox-size;
height: $checkbox-size;
}
}
}
}
.deps {
display: flex;
overflow: auto;
@ -178,12 +202,22 @@ as-split-area {
overflow: hidden;
h2 {
padding: 4px 16px;
padding: 0.5rem 1rem;
margin: 0;
height: 50px;
display: flex;
align-items: center;
border-bottom: 1px solid #777777;
font-size: 0.875rem;
display: flex;
align-items: center;
gap: 0.375rem;
mat-icon {
font-size: 1rem;
width: 1rem;
height: 1rem;
display: block;
}
}
}

View file

@ -33,7 +33,7 @@ import {
InjectorTreeVisualizer,
} from '../dependency-injection/injector-tree-visualizer';
import {InjectorProvidersComponent} from './injector-providers.component';
import {InjectorProvidersComponent} from './injector-providers/injector-providers.component';
import {
filterOutAngularInjectors,
filterOutInjectorsWithNoProviders,