Skip to content

Commit 567e50b

Browse files
committed
Move isActive() logic into router utils
This moves `isActive()` logic into router utils and adds logic for route claims and exact route matching. Signed-off-by: Phillip Rak <rak.phillip@gmail.com>
1 parent 4eac4b1 commit 567e50b

7 files changed

Lines changed: 425 additions & 39 deletions

File tree

shell/components/nav/Group.vue

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script>
22
import Type from '@shell/components/nav/Type';
3-
import { filterLocationValidParams } from '@shell/utils/router';
3+
import { filterLocationValidParams, isNavItemActive } from '@shell/utils/router';
44
export default {
55
name: 'Group',
66
@@ -205,14 +205,19 @@ export default {
205205
} else if (item.route) {
206206
const navLevels = ['cluster', 'product', 'resource'];
207207
const matchesNavLevel = navLevels.filter((param) => !this.$route.params[param] || this.$route.params[param] !== item.route.params[param]).length === 0;
208+
209+
// Keep the group open wherever the child itself is highlighted, otherwise pages nested under a
210+
// child's route (its create/detail pages) would collapse the group out from under it
211+
if (matchesNavLevel || isNavItemActive(this.$router, this.$route, item)) {
212+
return true;
213+
}
214+
208215
const validItemRoute = filterLocationValidParams(this.$router, item.route);
209216
210217
// Use .path instead of .fullPath to ignore query parameters and hashes when comparing routes
211218
const itemPath = this.$router.resolve(validItemRoute).path;
212219
213-
if (matchesNavLevel || itemPath === this.$route.path) {
214-
return true;
215-
} else if (parentPath && itemPath === parentPath) {
220+
if (parentPath && itemPath === parentPath) {
216221
return true;
217222
}
218223
}

shell/components/nav/Type.vue

Lines changed: 2 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import Favorite from '@shell/components/nav/Favorite';
33
import { TYPE_MODES } from '@shell/store/type-map';
44
55
import TabTitle from '@shell/components/TabTitle';
6-
import { filterLocationValidParams } from '@shell/utils/router';
6+
import { filterLocationValidParams, isNavItemActive } from '@shell/utils/router';
77
88
const showFavoritesFor = [TYPE_MODES.FAVORITE, TYPE_MODES.USED];
99
@@ -68,38 +68,7 @@ export default {
6868
},
6969
7070
isActive() {
71-
// Use .path instead of .fullPath to ignore query parameters and hashes when comparing routes
72-
const typePath = this.$router.resolve(this.typeRoute)?.path.toLowerCase();
73-
const pagePath = this.$route.path?.toLowerCase();
74-
const routeMetaNav = this.$route.meta?.nav;
75-
76-
// If the route explicitly declares the nav path that should be highlighted, then use that
77-
if (routeMetaNav) {
78-
const cluster = this.$route.params?.cluster;
79-
const product = this.$route.params?.product;
80-
const navPath = routeMetaNav
81-
.replace(':cluster', cluster)
82-
.replace(':product', product);
83-
84-
if (navPath === typePath) {
85-
return true;
86-
}
87-
}
88-
89-
if ( !this.type.exact) {
90-
const typeSplit = typePath.split('/');
91-
const pageSplit = pagePath.split('/');
92-
93-
for (let index = 0; index < typeSplit.length; ++index) {
94-
if ( index >= pageSplit.length || typeSplit[index] !== pageSplit[index] ) {
95-
return false;
96-
}
97-
}
98-
99-
return true;
100-
}
101-
102-
return typePath === pagePath;
71+
return isNavItemActive(this.$router, this.$route, this.type);
10372
},
10473
10574
typeRoute() {

shell/components/nav/__tests__/Group.test.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,118 @@ describe('component: Group', () => {
6565
expect((wrapper.vm as any).hasActiveRoute()).toBe(true);
6666
});
6767

68+
it('hasActiveRoute stays true on a route for a resource a child claims via navResources', () => {
69+
const group = {
70+
name: 'cluster',
71+
children: [
72+
{
73+
name: 'projects-namespaces',
74+
route: { name: 'c-cluster-product-projectsnamespaces', params: { cluster: 'local', product: 'explorer' } },
75+
navResources: ['management.cattle.io.project']
76+
}
77+
]
78+
};
79+
80+
const wrapper = shallowMount(Group as any, {
81+
props: {
82+
group, canCollapse: true, idPrefix: ''
83+
},
84+
global: {
85+
mocks: {
86+
// Creating a project uses the generic resource create route, which no nav item links to
87+
$route: {
88+
params: {
89+
cluster: 'local', product: 'explorer', resource: 'management.cattle.io.project'
90+
},
91+
path: '/c/local/explorer/management.cattle.io.project/create',
92+
fullPath: '/c/local/explorer/management.cattle.io.project/create',
93+
matched: []
94+
},
95+
$router: {
96+
resolve: jest.fn().mockReturnValue({ path: '/c/local/explorer/projectsnamespaces' }),
97+
getRoutes: jest.fn().mockReturnValue([])
98+
},
99+
t: (key: string) => key
100+
}
101+
}
102+
});
103+
104+
expect((wrapper.vm as any).hasActiveRoute()).toBe(true);
105+
});
106+
107+
it('hasActiveRoute is false on a route for a resource no child claims', () => {
108+
const group = {
109+
name: 'cluster',
110+
children: [
111+
{
112+
name: 'projects-namespaces',
113+
route: { name: 'c-cluster-product-projectsnamespaces', params: { cluster: 'local', product: 'explorer' } },
114+
navResources: ['management.cattle.io.project']
115+
}
116+
]
117+
};
118+
119+
const wrapper = shallowMount(Group as any, {
120+
props: {
121+
group, canCollapse: true, idPrefix: ''
122+
},
123+
global: {
124+
mocks: {
125+
$route: {
126+
params: {
127+
cluster: 'local', product: 'explorer', resource: 'apps.deployment'
128+
},
129+
path: '/c/local/explorer/apps.deployment/create',
130+
fullPath: '/c/local/explorer/apps.deployment/create',
131+
matched: []
132+
},
133+
$router: {
134+
resolve: jest.fn().mockReturnValue({ path: '/c/local/explorer/projectsnamespaces' }),
135+
getRoutes: jest.fn().mockReturnValue([])
136+
},
137+
t: (key: string) => key
138+
}
139+
}
140+
});
141+
142+
expect((wrapper.vm as any).hasActiveRoute()).toBe(false);
143+
});
144+
145+
it('hasActiveRoute stays true on a page nested under a child route', () => {
146+
// The Providers group in Cluster Management. There is no current cluster under /c/_, so nav items
147+
// resolve without a cluster param and the nav level check can't match on its own
148+
const group = {
149+
name: 'providers',
150+
children: [
151+
{ name: 'rke-kontainer-providers', route: { name: 'c-cluster-manager-driver-kontainerdriver', params: {} } },
152+
{ name: 'rke-node-providers', route: { name: 'c-cluster-manager-driver-nodedriver', params: {} } }
153+
]
154+
};
155+
156+
const mountOnPath = (path: string) => shallowMount(Group as any, {
157+
props: {
158+
group, canCollapse: true, idPrefix: ''
159+
},
160+
global: {
161+
mocks: {
162+
$route: {
163+
params: { cluster: '_' }, path, matched: []
164+
},
165+
$router: {
166+
resolve: jest.fn((route: any) => ({ path: route.name === 'c-cluster-manager-driver-kontainerdriver' ? '/c/_/manager/kontainerDriver' : '/c/_/manager/nodeDriver' })),
167+
getRoutes: jest.fn().mockReturnValue([])
168+
},
169+
t: (key: string) => key
170+
}
171+
}
172+
});
173+
174+
expect((mountOnPath('/c/_/manager/kontainerDriver').vm as any).hasActiveRoute()).toBe(true);
175+
// Clicking Create must not collapse the group out from under the highlighted child
176+
expect((mountOnPath('/c/_/manager/kontainerDriver/create').vm as any).hasActiveRoute()).toBe(true);
177+
expect((mountOnPath('/c/_/manager/hostedprovider').vm as any).hasActiveRoute()).toBe(false);
178+
});
179+
68180
describe('group header label', () => {
69181
const mountGroup = (group: any) => shallowMount(Group as any, {
70182
props: {

shell/components/nav/__tests__/Type.test.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,97 @@ describe('component: Type', () => {
171171
});
172172
});
173173

174+
describe('nested routes', () => {
175+
const mountOnPath = (type: any, path: string) => shallowMount(Type as any, {
176+
props: { type },
177+
178+
global: {
179+
directives: { cleanHtml: (identity) => identity },
180+
181+
mocks: {
182+
$store: storeMock,
183+
$router: routerMock,
184+
$route: {
185+
params: { cluster: '_' }, path, fullPath: path
186+
}
187+
},
188+
stubs: { routerLink: createChildRenderingRouterLinkStub() },
189+
},
190+
});
191+
192+
// Pages such as /c/_/manager/kontainerDriver/create are nested under the nav item's own route,
193+
// so a non-exact item has to stay highlighted on them
194+
const kontainerDrivers = {
195+
name: 'rke-kontainer-providers',
196+
route: '/c/_/manager/kontainerDriver',
197+
exact: false
198+
};
199+
200+
it('should use active class on a route nested under a non-exact type', () => {
201+
const wrapper = mountOnPath(kontainerDrivers, '/c/_/manager/kontainerDriver/create');
202+
203+
expect(wrapper.find(`.${ activeClass }`).exists()).toBe(true);
204+
});
205+
206+
it('should not use active class on a sibling route of a non-exact type', () => {
207+
const wrapper = mountOnPath(kontainerDrivers, '/c/_/manager/nodeDriver/create');
208+
209+
expect(wrapper.find(`.${ activeClass }`).exists()).toBe(false);
210+
});
211+
212+
it('should not use active class on a nested route when the type is exact', () => {
213+
const wrapper = mountOnPath({ ...kontainerDrivers, exact: true }, '/c/_/manager/kontainerDriver/create');
214+
215+
expect(wrapper.find(`.${ activeClass }`).exists()).toBe(false);
216+
});
217+
});
218+
219+
describe('navResources', () => {
220+
const projectsNamespaces = {
221+
name: 'projects-namespaces',
222+
route: 'projectsnamespaces',
223+
navResources: ['management.cattle.io.project', 'namespace']
224+
};
225+
226+
const mountOnResource = (type: any, resource: string) => shallowMount(Type as any, {
227+
props: { type },
228+
229+
global: {
230+
directives: { cleanHtml: (identity) => identity },
231+
232+
mocks: {
233+
$store: storeMock,
234+
$router: routerMock,
235+
// Creating a project uses the generic resource create route, which no nav item links to
236+
$route: {
237+
params: { resource },
238+
path: `${ resource }/create`,
239+
fullPath: `${ resource }/create`
240+
}
241+
},
242+
stubs: { routerLink: createChildRenderingRouterLinkStub() },
243+
},
244+
});
245+
246+
it('should use active class on a route for a claimed resource', () => {
247+
const wrapper = mountOnResource(projectsNamespaces, 'management.cattle.io.project');
248+
249+
expect(wrapper.find(`.${ activeClass }`).exists()).toBe(true);
250+
});
251+
252+
it('should not use active class on a route for an unclaimed resource', () => {
253+
const wrapper = mountOnResource(projectsNamespaces, 'apps.deployment');
254+
255+
expect(wrapper.find(`.${ activeClass }`).exists()).toBe(false);
256+
});
257+
258+
it('should not use active class when the type claims no resources', () => {
259+
const wrapper = mountOnResource({ name: 'namespaces', route: 'namespaces' }, 'namespace');
260+
261+
expect(wrapper.find(`.${ activeClass }`).exists()).toBe(false);
262+
});
263+
});
264+
174265
describe('should use classes if preconditions are met', () => {
175266
it('should use active class if the link is active', () => {
176267
const wrapper = shallowMount(Type as any, {

shell/config/product/manager.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ export function init(store) {
111111
namespaced: false,
112112
icon: 'globe',
113113
route: { name: 'c-cluster-manager-driver-kontainerdriver' },
114-
exact: true
114+
exact: false
115115
});
116116
virtualType({
117117
labelKey: 'drivers.node.title',
@@ -120,7 +120,7 @@ export function init(store) {
120120
namespaced: false,
121121
icon: 'globe',
122122
route: { name: 'c-cluster-manager-driver-nodedriver' },
123-
exact: true
123+
exact: false
124124
});
125125

126126
virtualType({

0 commit comments

Comments
 (0)