Description
When a dynamic plugin registers a custom console.perspective and activates it via setActivePerspective(), the Console always calls navigate() internally, causing a visible page re-render even when navigating to the same URL. This makes it impossible for plugins to cleanly switch perspectives without a redundant page reload.
Root Cause
In useValuesForPerspectiveContext.ts, setPerspective always calls navigate():
const setPerspective = useCallback(
(newPerspective, next) => {
setLastPerspective(newPerspective);
setActivePerspective(newPerspective);
navigate(next || '/'); // ← always navigates
fireTelemetryEvent('Perspective Changed', { perspective: newPerspective });
},
[setLastPerspective, setActivePerspective, navigate, fireTelemetryEvent],
);
This creates three problems for custom perspective plugins:
1. Without next: redirects to /
Calling setActivePerspective('my-perspective') without a next URL navigates to /, which redirects the user to /dashboards — losing their current page.
2. With next as current URL: triggers namespace handler loop
Calling setActivePerspective('my-perspective', window.location.pathname) navigates to the same URL, which triggers the Console's namespace handler. For URLs containing all-namespaces (common in multicluster perspectives), the namespace handler tries to validate all-namespaces as a project name, fails with a 400 error, and may redirect — creating an infinite reload loop.
3. Via ?perspective= URL param: redundant re-render
The safest approach is adding ?perspective=my-perspective to the URL and letting DetectPerspective handle it. DetectPerspective calls setActivePerspective(param, createPath(location)), which passes the full current URL as next. The perspective state is updated correctly, but navigate(createPath(location)) still fires — navigating to the same URL and causing a visible second page re-render.
Steps to Reproduce
- Register a custom
console.perspective extension in a dynamic plugin
- Navigate directly to a URL owned by that perspective (e.g., via browser address bar or bookmark)
- The plugin detects the URL and needs to activate its perspective
- Any call to
setActivePerspective() triggers navigate(), causing a visible page reload
Additional Context: console.page/route perspective property creates chicken-and-egg
When a console.page/route extension has a perspective property, the Console only renders that route when the specified perspective is active. This creates a dependency cycle:
- The route component contains the hook that activates the perspective
- But the route won't render until the perspective is already active
- So the perspective can never be activated from within the route
This forces plugins to remove the perspective property from their route extensions and rely solely on unique URL path prefixes (e.g., /fleet-virtualization/...) for route isolation. While this works, it means the perspective property on RoutePage is unusable for plugins that need URL-based perspective activation.
Proposed Solutions
Option A: Skip navigate() when URL hasn't changed (minimal fix)
const setPerspective = useCallback(
(newPerspective, next) => {
setLastPerspective(newPerspective);
setActivePerspective(newPerspective);
const target = next || '/';
if (target !== createPath(window.location)) {
navigate(target);
}
fireTelemetryEvent('Perspective Changed', { perspective: newPerspective });
},
[...],
);
Option B: Make navigation optional
Add a parameter to opt out of navigation when only the perspective state needs updating:
setActivePerspective('my-perspective', { navigate: false });
Option C: Clean up ?perspective= param in DetectPerspective
After DetectPerspective processes the ?perspective= param, remove it from the URL to avoid it persisting and to reduce the navigation to a replaceState instead of a full navigate:
useEffect(() => {
if (perspectiveParam && perspectiveParam !== activePerspective) {
setActivePerspective(perspectiveParam, createPath(location));
// Remove the param after processing
const url = new URL(window.location.href);
url.searchParams.delete('perspective');
window.history.replaceState(window.history.state, '', url.pathname + url.search + url.hash);
}
}, [perspectiveParam, activePerspective, setActivePerspective, location]);
Environment
- OpenShift Console: latest main branch
- Affected SDK API:
useActivePerspective / setActivePerspective
- Plugin: kubevirt-plugin (fleet-virtualization perspective)
Description
When a dynamic plugin registers a custom
console.perspectiveand activates it viasetActivePerspective(), the Console always callsnavigate()internally, causing a visible page re-render even when navigating to the same URL. This makes it impossible for plugins to cleanly switch perspectives without a redundant page reload.Root Cause
In
useValuesForPerspectiveContext.ts,setPerspectivealways callsnavigate():This creates three problems for custom perspective plugins:
1. Without
next: redirects to/Calling
setActivePerspective('my-perspective')without anextURL navigates to/, which redirects the user to/dashboards— losing their current page.2. With
nextas current URL: triggers namespace handler loopCalling
setActivePerspective('my-perspective', window.location.pathname)navigates to the same URL, which triggers the Console's namespace handler. For URLs containingall-namespaces(common in multicluster perspectives), the namespace handler tries to validateall-namespacesas a project name, fails with a 400 error, and may redirect — creating an infinite reload loop.3. Via
?perspective=URL param: redundant re-renderThe safest approach is adding
?perspective=my-perspectiveto the URL and lettingDetectPerspectivehandle it.DetectPerspectivecallssetActivePerspective(param, createPath(location)), which passes the full current URL asnext. The perspective state is updated correctly, butnavigate(createPath(location))still fires — navigating to the same URL and causing a visible second page re-render.Steps to Reproduce
console.perspectiveextension in a dynamic pluginsetActivePerspective()triggersnavigate(), causing a visible page reloadAdditional Context:
console.page/routeperspective property creates chicken-and-eggWhen a
console.page/routeextension has aperspectiveproperty, the Console only renders that route when the specified perspective is active. This creates a dependency cycle:This forces plugins to remove the
perspectiveproperty from their route extensions and rely solely on unique URL path prefixes (e.g.,/fleet-virtualization/...) for route isolation. While this works, it means theperspectiveproperty onRoutePageis unusable for plugins that need URL-based perspective activation.Proposed Solutions
Option A: Skip
navigate()when URL hasn't changed (minimal fix)Option B: Make navigation optional
Add a parameter to opt out of navigation when only the perspective state needs updating:
Option C: Clean up
?perspective=param inDetectPerspectiveAfter
DetectPerspectiveprocesses the?perspective=param, remove it from the URL to avoid it persisting and to reduce the navigation to areplaceStateinstead of a fullnavigate:Environment
useActivePerspective/setActivePerspective