fix(devtools): router tree details table data

Introduce layout fixes and use dedicated buttons for the view source and navigate actions.
This commit is contained in:
Georgi Serev 2025-11-05 19:07:18 +02:00 committed by GitHub
parent 44435ea97b
commit cd0e96c1d0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 223 additions and 159 deletions

View file

@ -1,38 +1,56 @@
<th>{{label()}}</th>
<td>
@switch (type()) {
@case ('chip') {
<button
ng-button
size="compact"
(click)="btnClick.emit('')"
[disabled]="rowValue()?.toString().startsWith('Lazy ')"
>
{{ rowValue() }}
</button>
}
@case ('flag') {
<span [class]="rowValue() ? 'tag-active' : 'tag-inactive'">
{{ rowValue() }}
{{ rowValue() || 'false' }}
</span>
}
@case ('list') {
<div class="chips-container">
@for (provider of dataArray(); track $index) {
<button ng-button size="compact" (click)="btnClick.emit(provider)">
{{ provider }}
</button>
}
</div>
@for (provider of dataArray(); track $index) {
<div class="value-container">
<span>{{ provider || '[empty string] ' }}</span
><ng-container
[ngTemplateOutlet]="actionBtn"
[ngTemplateOutletContext]="{ value: provider }"
/>
</div>
}
}
@default {
<span class="row-data">
@if (renderValueAsJson()) {
{{ rowValue() | json }}
} @else {
{{ rowValue() }}
}
</span>
<div class="value-container">
<span>
@if (rowValue() && renderValueAsJson()) {
{{ rowValue() | json }}
} @else {
{{ rowValue() || '[empty string] ' }}
}</span
><ng-container
[ngTemplateOutlet]="actionBtn"
[ngTemplateOutletContext]="{ value: rowValue() }"
/>
</div>
}
}
</td>
<ng-template #actionBtn let-value="value">
@if (actionBtnType() !== 'none') {
<button
ng-button
btnType="icon"
(click)="actionBtnClick.emit(value)"
[matTooltip]="!actionBtnDisabled() ? actionBtnTooltip() || value : null"
[disabled]="actionBtnDisabled()"
>
@switch (actionBtnType()) {
@case ('view-source') {
<mat-icon class="view-source">code</mat-icon>
}
@case ('navigate') {
<mat-icon class="navigate">output</mat-icon>
}
}
</button>
}
</ng-template>

View file

@ -13,11 +13,35 @@
border-color: var(--red-06);
}
.chips-container {
display: flex;
flex-wrap: wrap;
gap: 0.375rem;
overflow-wrap: normal;
button {
width: 16px;
height: 16px;
display: inline-flex;
align-items: center;
justify-content: center;
vertical-align: middle;
color: var(--quaternary-contrast);
&:not([disabled]):hover {
color: var(--primary-contrast);
}
&[disabled] {
cursor: default;
color: var(--quinary-contrast);
}
mat-icon {
font-size: 14px;
width: 14px;
height: 14px;
&.view-source {
font-size: 16px;
width: 16px;
height: 16px;
}
}
}
td {

View file

@ -74,10 +74,11 @@ describe('RouteDetailsRowComponent', () => {
expect(dataElements[0].nativeElement.innerText).toEqual('false');
});
it('should render a label and chip data', () => {
it('should render a label with an action button', () => {
fixture.componentRef.setInput('type', 'chip');
fixture.componentRef.setInput('data', {name: 'Component Name'});
fixture.componentRef.setInput('dataKey', 'name');
fixture.componentRef.setInput('actionBtnType', 'view-source');
fixture.detectChanges();
const labelElement = fixture.debugElement.query(By.css('th'));
@ -85,13 +86,14 @@ describe('RouteDetailsRowComponent', () => {
const dataElements = fixture.debugElement.queryAll(By.css('button'));
expect(dataElements.length).toEqual(1);
expect(dataElements[0].nativeElement.innerText).toEqual('Component Name');
});
it('should render a label and chip data disabled', () => {
it('should render a label with a disabled action button', () => {
fixture.componentRef.setInput('type', 'chip');
fixture.componentRef.setInput('data', {name: 'Lazy Component Name'});
fixture.componentRef.setInput('dataKey', 'name');
fixture.componentRef.setInput('actionBtnType', 'view-source');
fixture.componentRef.setInput('actionBtnDisabled', true);
fixture.detectChanges();
const labelElement = fixture.debugElement.query(By.css('th'));
@ -99,7 +101,6 @@ describe('RouteDetailsRowComponent', () => {
const dataElements = fixture.debugElement.queryAll(By.css('button'));
expect(dataElements.length).toEqual(1);
expect(dataElements[0].nativeElement.innerText).toEqual('Lazy Component Name');
expect(dataElements[0].nativeElement.disabled).toEqual(true);
});
@ -107,6 +108,7 @@ describe('RouteDetailsRowComponent', () => {
fixture.componentRef.setInput('type', 'list');
fixture.componentRef.setInput('data', {providers: ['Guard 1', 'Guard 2']});
fixture.componentRef.setInput('dataKey', 'providers');
fixture.componentRef.setInput('actionBtnType', 'view-source');
fixture.detectChanges();
const labelElement = fixture.debugElement.query(By.css('th'));
@ -114,7 +116,5 @@ describe('RouteDetailsRowComponent', () => {
const dataElements = fixture.debugElement.queryAll(By.css('button'));
expect(dataElements.length).toEqual(2);
expect(dataElements[0].nativeElement.innerText).toEqual('Guard 1');
expect(dataElements[1].nativeElement.innerText).toEqual('Guard 2');
});
});

View file

@ -7,17 +7,20 @@
*/
import {Component, computed, input, output, ChangeDetectionStrategy} from '@angular/core';
import {JsonPipe, NgTemplateOutlet} from '@angular/common';
import {MatIcon} from '@angular/material/icon';
import {ButtonComponent} from '../../shared/button/button.component';
import {JsonPipe} from '@angular/common';
import {RouterTreeNode} from './router-tree-fns';
import {MatTooltip} from '@angular/material/tooltip';
export type RowType = 'text' | 'chip' | 'flag' | 'list';
export type RowType = 'text' | 'flag' | 'list';
export type ActionBtnType = 'none' | 'view-source' | 'navigate';
@Component({
selector: '[ng-route-details-row]',
templateUrl: './route-details-row.component.html',
styleUrls: ['./route-details-row.component.scss'],
imports: [ButtonComponent, JsonPipe],
imports: [NgTemplateOutlet, ButtonComponent, JsonPipe, MatIcon, MatTooltip],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class RouteDetailsRowComponent {
@ -26,8 +29,11 @@ export class RouteDetailsRowComponent {
readonly dataKey = input.required<string>();
readonly renderValueAsJson = input<boolean>(false);
readonly type = input<RowType>('text');
readonly actionBtnType = input<ActionBtnType>('none');
readonly actionBtnTooltip = input<string>('');
readonly actionBtnDisabled = input<boolean>(false);
readonly btnClick = output<string>();
readonly actionBtnClick = output<string>();
readonly rowValue = computed(() => {
return this.data()[this.dataKey() as keyof RouterTreeNode];

View file

@ -47,134 +47,147 @@
</button>
<!-- TODO: Convert to a description list (<dl>) -->
<table class="ng-table">
<tr
ng-route-details-row
label="Path"
type="chip"
dataKey="path"
[data]="data"
(btnClick)="navigateRoute(route)"
></tr>
<div class="scrollable-wrapper">
<table class="ng-table">
<tr
ng-route-details-row
label="Path"
dataKey="path"
[data]="data"
actionBtnType="navigate"
[actionBtnTooltip]="'Navigate to ' + data.path"
(actionBtnClick)="navigateRoute(route)"
></tr>
@if (!data.isRedirect) {
<tr
ng-route-details-row
label="Component"
type="chip"
dataKey="component"
[data]="data"
(btnClick)="viewComponentSource(data.component)"
></tr>
}
@if (!data.redirectTo) {
<tr
ng-route-details-row
label="Component"
dataKey="component"
[data]="data"
actionBtnType="view-source"
actionBtnTooltip="View source"
[actionBtnDisabled]="data.component.includes('Lazy')"
(actionBtnClick)="viewComponentSource(data.component)"
></tr>
} @else {
<tr
ng-route-details-row
label="Redirect to"
dataKey="redirectTo"
[data]="data"
actionBtnType="view-source"
actionBtnTooltip="View source"
(actionBtnClick)="viewFunctionSource(data.redirectTo, 'redirectTo')"
></tr>
}
@if (data.pathMatch) {
<tr ng-route-details-row label="Path Match" dataKey="pathMatch" [data]="data"></tr>
}
@if (route?.data?.data?.length > 0) {
<tr
ng-route-details-row
label="Data"
[renderValueAsJson]="true"
dataKey="data"
[data]="data"
></tr>
}
@if (data.canActivateGuards && data.canActivateGuards.length > 0) {
<tr
ng-route-details-row
label="Can Activate Guards"
type="list"
dataKey="canActivateGuards"
[data]="data"
(btnClick)="viewSourceFromRouter($event, 'canActivate')"
></tr>
}
@if (data.canActivateChildGuards && data.canActivateChildGuards.length > 0) {
<tr
ng-route-details-row
label="Can Activate Child Guards"
type="list"
dataKey="canActivateChildGuards"
[data]="data"
(btnClick)="viewSourceFromRouter($event, 'canActivateChild')"
></tr>
}
@if (data.canDeactivateGuards && data.canDeactivateGuards.length > 0) {
<tr
ng-route-details-row
label="Can DeActivate Guards"
type="list"
dataKey="canDeactivateGuards"
[data]="data"
(btnClick)="viewSourceFromRouter($event, 'canDeactivate')"
></tr>
}
@if (data.canMatchGuards && data.canMatchGuards.length > 0) {
<tr
ng-route-details-row
label="Can Match Guards"
type="list"
dataKey="canMatchGuards"
[data]="data"
(btnClick)="viewSourceFromRouter($event, 'canMatch')"
></tr>
}
@if (data.pathMatch) {
<tr ng-route-details-row label="Path Match" dataKey="pathMatch" [data]="data"></tr>
}
@if (route?.data?.data?.length > 0) {
<tr
ng-route-details-row
label="Data"
[renderValueAsJson]="true"
dataKey="data"
[data]="data"
></tr>
}
@if (data.canActivateGuards && data.canActivateGuards.length > 0) {
<tr
ng-route-details-row
label="Can Activate Guards"
type="list"
dataKey="canActivateGuards"
[data]="data"
actionBtnType="view-source"
actionBtnTooltip="View source"
(actionBtnClick)="viewSourceFromRouter($event, 'canActivate')"
></tr>
}
@if (data.canActivateChildGuards && data.canActivateChildGuards.length > 0) {
<tr
ng-route-details-row
label="Can Activate Child Guards"
type="list"
dataKey="canActivateChildGuards"
[data]="data"
(actionBtnClick)="viewSourceFromRouter($event, 'canActivateChild')"
></tr>
}
@if (data.canDeactivateGuards && data.canDeactivateGuards.length > 0) {
<tr
ng-route-details-row
label="Can DeActivate Guards"
type="list"
dataKey="canDeactivateGuards"
[data]="data"
actionBtnType="view-source"
actionBtnTooltip="View source"
(actionBtnClick)="viewSourceFromRouter($event, 'canDeactivate')"
></tr>
}
@if (data.canMatchGuards && data.canMatchGuards.length > 0) {
<tr
ng-route-details-row
label="Can Match Guards"
type="list"
dataKey="canMatchGuards"
[data]="data"
actionBtnType="view-source"
actionBtnTooltip="View source"
(actionBtnClick)="viewSourceFromRouter($event, 'canMatch')"
></tr>
}
@if (data.providers && data.providers.length > 0) {
@if (data.providers && data.providers.length > 0) {
<tr
ng-route-details-row
label="Providers"
type="list"
dataKey="providers"
[data]="data"
actionBtnType="view-source"
actionBtnTooltip="View source"
(actionBtnClick)="viewSourceFromRouter($event, 'providers')"
></tr>
}
@if (data.title) {
<tr
ng-route-details-row
label="Title"
dataKey="title"
[data]="data"
actionBtnType="view-source"
actionBtnTooltip="View source"
(actionBtnClick)="viewFunctionSource(data.title, 'title')"
></tr>
}
<tr
ng-route-details-row
label="Providers"
type="list"
dataKey="providers"
label="Active"
type="flag"
dataKey="isActive"
[data]="data"
(btnClick)="viewSourceFromRouter($event, 'providers')"
></tr>
}
@if (data.title) {
<tr
ng-route-details-row
type="chip"
label="Title"
dataKey="title"
label="Auxiliary"
type="flag"
dataKey="isAux"
[data]="data"
(btnClick)="viewFunctionSource(data.title, 'title')"
></tr>
}
<tr
ng-route-details-row
label="Active"
type="flag"
dataKey="isActive"
[data]="data"
></tr>
<tr
ng-route-details-row
label="Auxiliary"
type="flag"
dataKey="isAux"
[data]="data"
></tr>
<tr ng-route-details-row label="Lazy" type="flag" dataKey="isLazy" [data]="data"></tr>
<tr
ng-route-details-row
label="Redirecting"
type="flag"
dataKey="isRedirect"
[data]="data"
></tr>
@if (data.redirectTo) {
<tr ng-route-details-row label="Lazy" type="flag" dataKey="isLazy" [data]="data"></tr>
<tr
ng-route-details-row
label="Redirect to"
type="chip"
dataKey="redirectTo"
label="Redirecting"
type="flag"
dataKey="isRedirect"
[data]="data"
(btnClick)="viewFunctionSource(data.redirectTo, 'redirectTo')"
></tr>
}
</table>
</table>
</div>
</div>
</as-split-area>
}

View file

@ -29,7 +29,9 @@
.side-pane {
position: relative;
padding: 1rem;
display: flex;
flex-direction: column;
height: 100%;
.close-btn {
position: absolute;
@ -47,10 +49,13 @@
@extend %heading-400;
margin: 0;
padding-top: 0;
padding: 1rem;
}
table {
margin-top: 1rem;
.scrollable-wrapper {
padding: 0 1rem 1rem 1rem;
height: 100%;
overflow-y: auto;
}
}

View file

@ -7,8 +7,6 @@ table.ng-table {
width: 100%;
tr {
height: 38px;
th {
@extend %body-bold-01;
margin: 0;
@ -37,6 +35,6 @@ table.ng-table {
border-bottom: 1px solid var(--senary-contrast);
overflow: hidden;
text-overflow: ellipsis;
padding: 0 0.375rem;
padding: 0.625rem 0.375rem;
}
}