Skip to content

setActivePerspective always triggers navigate(), causing redundant page reloads for custom perspective plugins #16254

@galkremer1

Description

@galkremer1

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

  1. Register a custom console.perspective extension in a dynamic plugin
  2. Navigate directly to a URL owned by that perspective (e.g., via browser address bar or bookmark)
  3. The plugin detects the URL and needs to activate its perspective
  4. 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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions