8000 refactor(devtools): adding route details panel for selected route by sumitarora · Pull Request #59999 · angular/angular · GitHub
[go: up one dir, main page]

Skip to content

refactor(devtools): adding route details panel for selected route #59999

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

sumitarora
Copy link
Contributor

On clicking a route in the router tree, the route details panel is displayed.

  • The panel shows the details of the selected route, including the path, component, providers, guards etc.
  • Clicking on a chip will navigate to the corresponding source file in the editor.
  • Clicking on the path will navigate to the corresponding route in the router tree.

Demo Application Source: https://github.com/sumitarora/angular-routes-demo
Demo Application Deployed: https://sumitarora.github.io/angular-routes-demo/#/

image

image

@ngbot ngbot bot added this to the Backlog milestone Feb 18, 2025
@if (route.data.pathMatch) {
<tr ng-route-details-row title="Path Match" [data]="route.data.pathMatch"></tr>
}
@if (route.data.data && route.data.data.length > 0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be safe to shorten checks like these with optional chaining (?.)?

Copy link
Contributor
@dgp1130 dgp1130 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for putting this together! I know I left a lot of comments, but they're mostly pretty minor refactoring suggestions.

/cc @hawkgs in case he has any thoughts on potential UX improvements.

@@ -102,3 +109,52 @@ function childRouteName(child: AngularRoute): string {
return 'no-name-route';
}
}

export function getElementRefByName(ref: string, routes: any[], name: string): any | null {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Some doc comments on these functions would be useful to explain the intent behind them as well as provide context on the terminology. I'm not really sure what ref refers to in a routing context.

@@ -102,3 +109,52 @@ function childRouteName(child: AngularRoute): string {
return 'no-name-route';
}
}

export function getElementRefByName(ref: string, routes: any[], name: string): any | null {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider: I'm assuming the intended type signature is something like:

import type {Route, CanActivate} from '@angular/router';

export function getElementRefByName(ref: string, routes: Route[], name: string): CanActivate;

Would it be possible to actually do that via a type-only import of @angular/router? I know we can't trivially depend on Router in this context, but I feel like a type-only import might be appropriate?

router.navigateByUrl(path);
};

export const viewSourceFromRouter = (path: string, type: string) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider: A doc comment explaining the intent here would be helpful, I'm not sure what "view source" means in this context.

);
const rootInjector = (forest[0].resolutionPath ?? []).find((i) => i.name === 'Root');
const serializedProviderRecords = getSerializedProviderRecords(rootInjector!) ?? [];
const routerInstance = serializedProviderRecords.filter(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider: .find might be more appropriate than .filter, given that we expect to find a single router.

value = injector.get(provider.token, null, {optional: true});
} else if (Array.isArray(serializedProvider.index)) {
const providers = serializedProvider.index.map((index) => providerRecords[index]);
value = injector.get(providers[0].token, null, {optional: true});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider: We can immediately return in these branches (with an else { return undefined; }) rather than assigning a temporary variable.

.tag-incactive {
display: inline-block;
border-radius: 5px;
padding: 2px 8px;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A11y: Should padding and/or border-radius be em or rem units? How does this look when the user zooms in with Ctrl +?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried zooming and actually I think the padding looks fine all around.

</mat-chip>
} @else if (rowType() === 'flag') {
<span class="{{ data() ? 'tag-active' : 'tag-incactive' }}">
{{ data() ? 'Active' : 'Inactive' }}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: "Active" / "Inactive" I'm assuming apply to guards, but this is rowType() === 'flag', and I wouldn't expect that terminology for a basic boolean. Maybe this should be rowType() === 'guard'?

display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A11y: Similar question here, should these padding properties use em or rem? In my experience that usually scales better.

@hawkgs
Copy link
Member
hawkgs commented Feb 21, 2025

Here is a list with the areas of the UI that need some adjustments/fixes:

  • There are some issues with the dark theme in the side panel. Some of the text is always black (Note: We can use the new theme mixins).
  • The font size of the side panel title (Routes Details) can be decreased a bit. I suggest using the same as the one in the Injector Tree tab.
  • Same about the search input – it would be good if it matches the one in Injector Tree > Providers for Platform (side panel), at least the font size.
  • I agree with Doug that using pills for the path and component table values might not be very suitable. Maybe using a monospaced font should be enough.

Overall, I think these are the main points. Anything else can be improved as part of the general effort for introducing consistency across DevTools.

Comment on lines 394 to 438
const router = getInjectorInstance(rootInjector!, routerInstance[0]);
return router;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we cast router to Router here? I guess we can benefit from some typing because I noticed that there are some any-s in the diff that can be avoided.

@@ -102,3 +109,52 @@ function childRouteName(child: AngularRoute): string {
return 'no-name-route';
}
}

export function getElementRefByName(ref: string, routes: any[], name: string): any | null {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

routes: any[] => routes: Routes ?


export type RowType = 'text' | 'chip' | 'flag' | 'list';

@Component({
Copy link
Member
@hawkgs hawkgs Feb 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't want to steal Doug's conversation about whether this should be a component or a simple template hence the new comment.

Can we create separate files for the template and the styles, in case this remains as a component? I get that this comes up with some additional Bazel build configuration but it should promote better flexibility. Right now, we won't be able to import SCSS files, for instance. This is probably rather a bigger discussion, but in my opinion, we should try to limit the usage of this pattern unless the component has a one-liner template with a single to no styles.

cc @dgp1130 @AleksanderBodurri

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@sumitarora sumitarora force-pushed the feat-route-details branch 2 times, most recently from 8e7dffe to e3052e8 Compare February 24, 2025 16:11
(provider) => provider.token === 'Router', // get the instance of router using token
);
const routerProvider = getProviderValue(injector, routerInstance[0]);
const routerProvider = getProviderValue(injector, routerInstance!);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: I was using DevTools today (actually unrelated to this PR) and noticed errors in the console because router was not on the page and getProviderValue was being called without a router instance.

I feel like the ! assertion is wrong and we should return undefined or otherwise abort if no router could be found?

Copy link
Contributor
@dgp1130 dgp1130 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some UI feedback:

image

  • When there are too many providers it forces horizontal scrolling of the entire panel. I think we can just have those chips line-wrap and we should be good.
  • Component : text gets a newline as the column attempts to minimize its width. Typically colons don't have spaces in front of them, so Component: would be more appropriate and avoid the line-wrapping issue.
  • Blue boxes don't highlight when moused over.
  • If a single chip is long enough to overflow, the horizontal scrollbar should probably go on the details panel rather than the entire router tree panel.
  • I think I mentioned this when looking at the code, but "Active: Active" and "Auxiliary: Active" read kind of awkwardly. Should these be true / false instead (or a checkbox kind of UI)?
  • "Children: 1" doesn't seem super helpful to me given that the graph is able to more visually show that. I wonder if this just more noise than it's worth in the table view?
  • feature/team lists its Component as feature/team [Lazy] but that route doesn't actually have a component listed. I would expect this not to have a component at all since it's just there to load children. Similar issue with Component: no-name on the root node.
  • Mouseover of the details table highlights using the same color every other route is set to. I suggest either using a slightly different shade or maybe just removing highlighting on mouse hover given that the table only has two columns and I'm not sure highlighting is that useful for building a visual association of items in a row.
  • At one point I managed to break mouseover highlighting of the graph nodes and it would react exactly backwards. Mousing off a node would highlight it and mousing over a node would remove the highlighting. Resetting DevTools fixed the issue and I haven't been able to reproduce since. So I suspect there's some bug here but I can't narrow it down any more than that.

Feature idea: The "Auxiliary" row could link to the Components panel for the associated <router-outlet> it's rendering to. It would be cool if we could show some meaningful name for the outlet rather than just "Auxiliary: Active", but I'm not sure such a name exists.

General UX question about the route graph more than the sidebar: Have we looked at the accessibility here for vision deficiencies? Chrome has a feature to test out some common forms. My main takeaways are:

  • Slightly thicker borders on the graph might help low contrast users.
  • Deuteranopia (no green) users might struggle to distinguish active and eager loaded routes. I wonder if we can tweak the colors a bit to be more helpful here.
  • Colorblind users can't distinguish the boxes at all.
    • I actually think this is mostly ok since the details panel explains textually whether a route is active or lazy and is a reasonable fallback for such users.
    • I think the main challenge here is finding the active route, which is difficult without color. I vaguely recall discussing making the line thicker along the path to the active route? That would probably help colorblind users find it more effectively.
  • For everyone, I'm wondering if the highlight which applies from mousing over a route should also apply the same highlight to the legend in the corner? I wonder if the change in color might be more observable and help draw an association with the specific kind of route they're looking at.

Since this doesn't have too much to do with the route details panel, we can definitely follow up in a separate PR for this color accessibility stuff.

Bug in the demo app: If navigate to "Feature Team Admin" and then refresh, the navigation panel doesn't work and does not navigate anywhere.

// todo(aleksanderbodurri): type these properly
type AngularRoute = any;
type AngularRoute = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Is this is a copy of Route from @angular/router? Feels like it would be easy for this to fall out of date.

I was thinking we could potentially import type to get the benefits of type checking without actually duplicating the router at runtime. Something like:

import type {Route as AngularRoute} from '@angular/router';

Is there a reason we can't do that in this context?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just FYI, I did this in my split ng interface prototype and it seemed to work without any complications.

44d8a28#diff-90cdacecab0fda39eb99b06085d47ca5d180c5411542c3cd285a7dc50e896ef9

I definitely think this would be a good improvement.

.tag-incactive {
display: inline-block;
border-radius: 5px;
padding: 2px 8px;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried zooming and actually I think the padding looks fine all around.

@hawkgs
Copy link
Member
hawkgs commented Feb 28, 2025

@dgp1130, I think that we can handle colors in a separate PR as you already pointed. The described issues with the tree visualization are already present in the injector tree since it uses the same styles. We might have to consider high-contrast themes for people with vision deficiencies.

Ps. I've already started working on the color extraction.

@sumitarora sumitarora force-pushed the feat-route-details branch 5 times, most recently from 7e83df1 to 186f4dd Compare March 6, 2025 20:53
@sumitarora
Copy link
Contributor Author
sumitarora commented Mar 6, 2025

@hawkgs @dgp1130

  • Updated PR comments and UI issues
  • Fixed dark mode issues
  • Changed chips to buttons for clickable links
image image

(provider) => provider.token === 'Router', // get the instance of router using token
);

if (!routerInstance || !rootInjector) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: We check for !rootInjector here but only after it's been used in getSerializedProviderRecords. I think we want to move this check up.

@@ -371,6 +400,25 @@ export interface SerializableComponentTreeNode
children: SerializableComponentTreeNode[];
}

function getRouterInstance() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// todo(aleksanderbodurri): type these properly
type AngularRoute = any;
type AngularRoute = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just FYI, I did this in my split ng interface prototype and it seemed to work without any complications.

44d8a28#diff-90cdacecab0fda39eb99b06085d47ca5d180c5411542c3cd285a7dc50e896ef9

I definitely think this would be a good improvement.

return result;
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: This function returns undefined if it falls through despite | null being in the type. Probably hidden by the any in the return type.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is already check when executing the element so even if it returns null should be good.

@@ -26,6 +26,10 @@ export class RouterTreeVisualizer {
private root: RouterTreeD3Node | null = null;
private zoomController: d3.ZoomBehavior<HTMLElement, unknown> | null = null;

protected nodeClickListeners: ((pointerEvent: PointerEvent, node: any) => void)[] = [];
protected nodeMouseoverListeners: ((pointerEvent: PointerEvent, node: any) => void)[] = [];
protected nodeMouseoutListeners: ((pointerEvent: PointerEvent, node: any) => void)[] = [];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider: Can we type node as RouterTreeD3Node?

placeholder="Search routes"
class="filter-input"
/>
<mat-checkbox (change)="togglePathSettings()">Show Full Path </mat-checkbox>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: Trailing space.

Suggested change
<mat-checkbox (change)="togglePathSettings()">Show Full Path </mat-checkbox>
<mat-checkbox (change)="togglePathSettings()">Show Full Path</mat-checkbox>

{position: 8, name: 'Oxygen', weight: 15.9994, symbol: 'O'},
{position: 9, name: 'Fluorine', weight: 18.9984, symbol: 'F'},
{position: 10, name: 'Neon', weight: 20.1797, symbol: 'Ne'},
];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like some leftover debug data?

@@ -29,9 +66,17 @@ export class RouterTreeComponent {
private routerTreeVisualizer!: RouterTreeVisualizer;
private showFullPath = false;

private readonly _messageBus = inject(MessageBus) as MessageBus<Events>;
private readonly _appOperations = inject(ApplicationOperations);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style: No need for leading underscores on private members, TS provides protection enough here.

<div class="chips-container">
@for (p of data(); track $index) {
<button mat-stroked-button (click)="click.emit(p)">
{{ p }}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style: Is there a more meaningful name we can use than p? Maybe item?

@sumitarora sumitarora force-pushed the feat-route-details branch 3 times, most recently from 19587c6 to 94064d7 Compare March 17, 2025 15:21
@dgp1130 dgp1130 added action: review The PR is still awaiting reviews from at least one requested reviewer target: patch This PR is targeted for the next patch release labels Mar 26, 2025
@sumitarora sumitarora force-pushed the feat-route-details branch 4 times, most recently from a1096ad to 6edf626 Compare March 28, 2025 15:52
F438
@pullapprove pullapprove bot requested a review from josephperrott March 28, 2025 15:52
Copy link
Member
@josephperrott josephperrott left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Reviewed-for: dev-infra

Copy link
Contributor
@dgp1130 dgp1130 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding the tests! A couple minor comments, but nothing too concerning. If we can address the remaining open comments, I'm hopeful we can get this in pretty easily.

@sumitarora sumitarora force-pushed the feat-route-details branch 2 times, most recently from 7faa2aa to 4f08705 Compare April 6, 2025 00:25
@sumitarora
Copy link
Contributor Author

@dgp1130 Fixed all comments and pushed everything.

@@ -166,6 +172,29 @@ const checkForAngularCallback = (messageBus: MessageBus<Events>) => () =>
checkForAngular(messageBus);
const getRoutesCallback = (messageBus: MessageBus<Events>) => () => getRoutes(messageBus);

const navigateRouteCallback = (messageBus: MessageBus<Events>) => (path: string) => {
const router: any = getRouterInstance();
router.navigateByUrl(path);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should check for navigateByUrl before calling it here

initializeOrGetDirectiveForestHooks().getIndexedDirectiveForest(),
ngDebugDependencyInjectionApiIsSupported(),
);
const rootInjector = (forest[0].resolutionPath ?? []).find((i) => i.name === 'Root');
Copy link
Member
@AleksanderBodurri AleksanderBodurri Apr 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder what we should do here for apps with multiple roots 🤔 is it even possible for more than one root to have a router configured, given they'd have to share the url bar? Maybe we should search all roots for the router

cc @dgp1130

@angular-robot angular-robot bot requested a review from josephperrott April 23, 2025 00:20
initializeOrGetDirectiveForestHooks().getIndexedDirectiveForest(),
ngDebugDependencyInjectionApiIsSupported(),
);
const rootInjector = (forest[0].resolutionPath ?? []).find((i) => i.name === 'Root');
Copy link
Contributor
@alan-agius4 alan-agius4 Apr 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const rootInjector = (forest[0].resolutionPath ?? []).find((i) => i.name === 'Root');
const rootInjector = forest[0].resolutionPath?.find((i) => i.name === 'Root');

Comment on lines 425 to 426
const serializedProviderRecords = getSerializedProviderRecords(rootInjector) ?? [];
const routerInstance = serializedProviderRecords.find(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const serializedProviderRecords = getSerializedProviderRecords(rootInjector) ?? [];
const routerInstance = serializedProviderRecords.find(
const serializedProviderRecords = getSerializedProviderRecords(rootInjector);
const routerInstance = serializedProviderRecords?.find(

@sumitarora sumitarora force-pushed the feat-route-details branch 3 times, most recently from d404c5c to 12373fb Compare May 12, 2025 14:45
On clicking a route in the router tree, the route details panel is displayed.
- The panel shows the details of the selected route, including the path, component, providers, guards etc.
- Clicking on a chip will navigate to the corresponding source file in the editor.
- Clicking on the path will navigate to the corresponding route in the router tree.

Demo Application Source: https://github.com/sumitarora/angular-routes-demo
Demo Application Deployed: https://sumitarora.github.io/angular-routes-demo/#/
- Update code to fix PR comments and cleanup code
- Add unit tests for the new code
@sumitarora sumitarora force-pushed the feat-route-details branch from 12373fb to ea2938a Compare May 13, 2025 15:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
action: review The PR is still awaiting reviews from at least one requested reviewer area: devtools target: patch This PR is targeted for the next patch release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants
0