-
Notifications
You must be signed in to change notification settings - Fork 26.2k
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
base: main
Are you sure you want to change the base?
Conversation
@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) { |
There was a problem hiding this comment.
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 (?.
)?
There was a problem hiding this 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 { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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) => { |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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}); |
There was a problem hiding this comment.
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.
devtools/projects/ng-devtools/src/lib/devtools-tabs/router-tree/route-details-row.component.ts
Show resolved
Hide resolved
.tag-incactive { | ||
display: inline-block; | ||
border-radius: 5px; | ||
padding: 2px 8px; |
There was a problem hiding this comment.
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 +?
There was a problem hiding this comment.
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' }} |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
devtools/projects/ng-devtools/src/lib/devtools-tabs/router-tree/route-details-row.component.ts
Show resolved
Hide resolved
Here is a list with the areas of the UI that need some adjustments/fixes:
Overall, I think these are the main points. Anything else can be improved as part of the general effort for introducing consistency across DevTools. |
const router = getInjectorInstance(rootInjector!, routerInstance[0]); | ||
return router; |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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({ |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
8e7dffe
to
e3052e8
Compare
(provider) => provider.token === 'Router', // get the instance of router using token | ||
); | ||
const routerProvider = getProviderValue(injector, routerInstance[0]); | ||
const routerProvider = getProviderValue(injector, routerInstance!); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some UI feedback:
- 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, soComponent:
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 itsComponent
asfeature/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 withComponent: 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 = { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
@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. |
7e83df1
to
186f4dd
Compare
(provider) => provider.token === 'Router', // get the instance of router using token | ||
); | ||
|
||
if (!routerInstance || !rootInjector) { |
There was a problem hiding this comment.
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() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider: Explicit return types.
https://techhub.social/@develwithoutacause/111842243141199473
// todo(aleksanderbodurri): type these properly | ||
type AngularRoute = any; | ||
type AngularRoute = { |
There was a problem hiding this comment.
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; | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)[] = []; |
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo: Trailing space.
<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'}, | ||
]; |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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 }} |
There was a problem hiding this comment.
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
?
19587c6
to
94064d7
Compare
a1096ad
to
6edf626
Compare
6edf626
to
8705e05
Compare
...ools/projects/ng-devtools/src/lib/devtools-tabs/router-tree/route-details-row.component.scss
Outdated
Show resolved
Hide resolved
There was a problem hiding this 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
There was a problem hiding this 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.
...s/projects/ng-devtools/src/lib/devtools-tabs/router-tree/route-details-row.component.spec.ts
Outdated
Show resolved
Hide resolved
devtools/projects/ng-devtools/src/lib/devtools-tabs/router-tree/route-details-row.component.ts
Outdated
Show resolved
Hide resolved
devtools/projects/ng-devtools/src/lib/devtools-tabs/router-tree/router-tree.component.html
Outdated
Show resolved
Hide resolved
7faa2aa
to
4f08705
Compare
@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); |
There was a problem hiding this comment.
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'); |
There was a problem hiding this comment.
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
4f08705
to
0029445
Compare
initializeOrGetDirectiveForestHooks().getIndexedDirectiveForest(), | ||
ngDebugDependencyInjectionApiIsSupported(), | ||
); | ||
const rootInjector = (forest[0].resolutionPath ?? []).find((i) => i.name === 'Root'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const rootInjector = (forest[0].resolutionPath ?? []).find((i) => i.name === 'Root'); | |
const rootInjector = forest[0].resolutionPath?.find((i) => i.name === 'Root'); |
const serializedProviderRecords = getSerializedProviderRecords(rootInjector) ?? []; | ||
const routerInstance = serializedProviderRecords.find( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const serializedProviderRecords = getSerializedProviderRecords(rootInjector) ?? []; | |
const routerInstance = serializedProviderRecords.find( | |
const serializedProviderRecords = getSerializedProviderRecords(rootInjector); | |
const routerInstance = serializedProviderRecords?.find( |
d404c5c
to
12373fb
Compare
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
12373fb
to
ea2938a
Compare
On clicking a route in the router tree, the route details panel is displayed.
Demo Application Source: https://github.com/sumitarora/angular-routes-demo
Demo Application Deployed: https://sumitarora.github.io/angular-routes-demo/#/