Skip to content

Comments

CONSOLE-5030 Refactor remaining firehose impacted components#16039

Open
cajieh wants to merge 1 commit intoopenshift:mainfrom
cajieh:refactor-remaining-firehose-impacted-components
Open

CONSOLE-5030 Refactor remaining firehose impacted components#16039
cajieh wants to merge 1 commit intoopenshift:mainfrom
cajieh:refactor-remaining-firehose-impacted-components

Conversation

@cajieh
Copy link
Contributor

@cajieh cajieh commented Feb 18, 2026

CONSOLE-5030 Complete: All Firehose Components Migrated to Hooks

Successfully Refactored (9 files)
1. OLM Package (6 files)

  • ✓ packages/operator-lifecycle-manager/src/components/catalog-source.tsx
  • ✓ packages/operator-lifecycle-manager/src/components/operator-install-page.tsx
  • ✓ packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-page.tsx
  • ✓ packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-subscribe.tsx
  • ✓ packages/operator-lifecycle-manager/src/components/topology/sidebar/TopologyOperatorBackedResources.tsx
    2. Console-app Package (1 file)
  • ✓ packages/console-app/src/components/nodes/NodeTerminal.tsx
    3. Container-security Package (1 file)
  • ✓ packages/container-security/src/components/image-manifest-vuln.tsx
    4. Topology Package (1 file)
  • ✓ packages/topology/src/components/dropdowns/ApplicationDropdown.tsx
    5. Shipwright-plugin Package (1 file)
  • ✓ packages/shipwright-plugin/src/components/build-form/PushSecretDropdown.tsx

Migration Approach

  • Replaced HOC with useK8sWatchResource (single resource) or useK8sWatchResources (multiple resources)
  • Maintained functional parity for loading states and error handling
  • All files pass linting (when checked individually)
  • No Firehose imports remain in any migrated file

Summary by CodeRabbit

Release Notes

  • Refactor
    • Enhanced error handling and loading state management across Node Terminal, Operator Hub, Catalog Source, and resource management components.
    • Improved consistency in data loading patterns and error recovery.
    • Simplified component signatures in Operator Hub pages for streamlined initialization.
    • Updated resource provisioning architecture in topology and application dropdown components.

@openshift-ci openshift-ci bot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Feb 18, 2026
@openshift-ci openshift-ci bot added the component/core Related to console core functionality label Feb 18, 2026
@openshift-ci
Copy link
Contributor

openshift-ci bot commented Feb 18, 2026

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: cajieh

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@openshift-ci openshift-ci bot added component/olm Related to OLM approved Indicates a PR has been approved by an approver from all required OWNERS files. component/topology Related to topology labels Feb 18, 2026
@cajieh cajieh force-pushed the refactor-remaining-firehose-impacted-components branch 2 times, most recently from 1263161 to d7e6b94 Compare February 18, 2026 19:34
@cajieh cajieh changed the title [WIP] CONSOLE-5030 Refactor remaining firehose impacted components CONSOLE-5030 Refactor remaining firehose impacted components Feb 19, 2026
@openshift-ci openshift-ci bot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Feb 19, 2026
@cajieh
Copy link
Contributor Author

cajieh commented Feb 19, 2026

/test e2e-gcp-console

@cajieh cajieh force-pushed the refactor-remaining-firehose-impacted-components branch from d7e6b94 to a37006c Compare February 19, 2026 17:06
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 19, 2026

📝 Walkthrough

Walkthrough

This change systematically replaces Firehose-based data provisioning with direct Kubernetes watch hooks (useK8sWatchResource and useK8sWatchResources) across 10 files spanning multiple console packages. Components now fetch and manage resource subscriptions internally via hooks rather than relying on wrapper components to inject data. Type signatures transition from FirehoseResult patterns to composite objects with explicit { data, loaded, loadError } shapes. Page components lose their props parameters where Firehose previously injected resources, instead computing and passing resources directly to child components. Test mocks are updated to reflect the hook-based data flow. Approximately 317 lines changed across console-app, container-security, operator-lifecycle-manager, shipwright-plugin, and topology packages.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main refactoring effort: migrating Firehose-dependent components to React hooks. It directly describes the core change across all 9 files modified.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/packages/operator-lifecycle-manager/src/components/catalog-source.tsx (1)

240-274: ⚠️ Potential issue | 🟠 Major

Throw loadError to trigger the fallback UI when package manifest fetch fails.

The condition on line 243 only checks loaded && data, but when loaded is true with loadError set (fetch failed), the component returns <LoadingBox /> on line 266, leaving users watching a spinner indefinitely. The fallback UI (lines 268-272) is properly configured but never triggered because it only fires on thrown errors.

Check and throw loadError before testing the data:

Proposed fix
       (createProps) => {
+        if (createProps.packageManifest.loadError) {
+          throw createProps.packageManifest.loadError;
+        }
         if (createProps.packageManifest.loaded && createProps.packageManifest.data) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/packages/operator-lifecycle-manager/src/components/catalog-source.tsx`
around lines 240 - 274, The packageManifest fetch error isn't propagated to the
withFallback fallback because the code only checks
createProps.packageManifest.loaded and data; update the component wrapped by
withFallback (the function passed into withFallback inside
requireOperatorGroup/Create) to first check if
createProps.packageManifest.loadError is set and if so throw that error so the
fallback UI is triggered; locate references to packageManifest, CreateProps,
withFallback and CreateYAML in the current function and throw
createProps.packageManifest.loadError before the existing loaded/data checks so
the ConsoleEmptyState is shown instead of the LoadingBox on fetch failure.
🧹 Nitpick comments (7)
frontend/packages/container-security/src/components/image-manifest-vuln.tsx (1)

419-427: Type definition reflects redundancy.

The type mirrors the code redundancy—loaded appears both at the top level (line 420) and nested in imageManifestVuln (line 424). Once you decide on a single source of truth for the loaded state per the previous comment, update the type accordingly.

Suggested type simplification

If relying solely on the nested structure:

 export type ContainerVulnerabilitiesProps = {
-  loaded: boolean;
   pod: PodKind;
   imageManifestVuln: {
     data: ImageManifestVuln[];
     loaded: boolean;
     loadError?: any;
   };
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/packages/container-security/src/components/image-manifest-vuln.tsx`
around lines 419 - 427, The ContainerVulnerabilitiesProps type has duplicated
loaded flags; remove the redundant top-level loaded and keep the nested
imageManifestVuln.loaded (or vice versa if you prefer a single top-level
source); update the type definition (ContainerVulnerabilitiesProps) so only one
loaded property exists and adjust consumers to read from the chosen location
(imageManifestVuln.loaded or the top-level loaded) to match the code path that
determines loading state.
frontend/packages/operator-lifecycle-manager/src/components/catalog-source.tsx (3)

223-238: Memoize URLSearchParams to avoid repeated instantiation on each render.

new URLSearchParams(location.search) is called inline within the hook configuration object. Since hooks re-run on every render, this creates unnecessary object allocations. Extract the search params once before the hook call.

♻️ Proposed refactor
   const params = useParams();
   const location = useLocation();
+  const searchParams = new URLSearchParams(location.search);

   const resources = useK8sWatchResources<{
     packageManifest: PackageManifestKind;
     operatorGroup: OperatorGroupKind[];
   }>({
     packageManifest: {
       kind: referenceForModel(PackageManifestModel),
       isList: false,
-      name: new URLSearchParams(location.search).get('pkg'),
-      namespace: new URLSearchParams(location.search).get('catalogNamespace'),
+      name: searchParams.get('pkg'),
+      namespace: searchParams.get('catalogNamespace'),
     },
     operatorGroup: {
       kind: referenceForModel(OperatorGroupModel),
       isList: true,
       namespace: params.ns,
     },
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/packages/operator-lifecycle-manager/src/components/catalog-source.tsx`
around lines 223 - 238, The hook call to useK8sWatchResources repeatedly
constructs new URLSearchParams from location.search inside its config object
causing unnecessary allocations; extract const searchParams = new
URLSearchParams(location.search) (or useMemo to memoize it) before calling
useK8sWatchResources and then use searchParams.get('pkg') and
searchParams.get('catalogNamespace') in the packageManifest config so the same
URLSearchParams instance is reused across renders (adjust any references inside
the component accordingly).

249-261: URLSearchParams instantiated again inside template; reuse extracted variable.

Lines 256-257 create new URLSearchParams objects. After extracting searchParams per the earlier suggestion, reuse it here to maintain consistency and avoid redundant allocations.

♻️ Proposed refactor (assumes searchParams extracted earlier)
           const template = `
           apiVersion: ${SubscriptionModel.apiGroup}/${SubscriptionModel.apiVersion}
           kind: ${SubscriptionModel.kind},
           metadata:
             generateName: ${pkg.metadata.name}-
             namespace: default
           spec:
-            source: ${new URLSearchParams(location.search).get('catalog')}
-            sourceNamespace: ${new URLSearchParams(location.search).get('catalogNamespace')}
+            source: ${searchParams.get('catalog')}
+            sourceNamespace: ${searchParams.get('catalogNamespace')}
             name: ${pkg.metadata.name}
             startingCSV: ${channel.currentCSV}
             channel: ${channel.name}
         `;

Note: Accessing searchParams from the outer closure is valid since Create is defined within CreateSubscriptionYAML's scope.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/packages/operator-lifecycle-manager/src/components/catalog-source.tsx`
around lines 249 - 261, The template is creating new URLSearchParams instances
for source and sourceNamespace; reuse the previously extracted searchParams
variable from the outer CreateSubscriptionYAML scope instead. In the Create
function (and specifically the template string in
CreateSubscriptionYAML/Create), replace new
URLSearchParams(location.search).get('catalog') and new
URLSearchParams(location.search).get('catalogNamespace') with
searchParams.get('catalog') and searchParams.get('catalogNamespace')
respectively so you avoid redundant allocations and keep behavior consistent.

214-218: Consider extracting CreateProps type for reuse and stricter loadError typing.

The CreateProps type is defined inline within the component. For test type safety and potential reuse, consider exporting it. Additionally, loadError?: any could be narrowed to loadError?: Error for better type inference downstream.

♻️ Optional type improvement
+export type CreateSubscriptionYAMLProps = {
+  packageManifest: { loaded: boolean; data?: PackageManifestKind; loadError?: Error };
+  operatorGroup: { loaded: boolean; data?: OperatorGroupKind[]; loadError?: Error };
+};

 export const CreateSubscriptionYAML: FC = (props) => {
-  type CreateProps = {
-    packageManifest: { loaded: boolean; data?: PackageManifestKind; loadError?: any };
-    operatorGroup: { loaded: boolean; data?: OperatorGroupKind[]; loadError?: any };
-  };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/packages/operator-lifecycle-manager/src/components/catalog-source.tsx`
around lines 214 - 218, Extract and export the inline CreateProps type so it can
be reused and imported elsewhere, and tighten its loadError types from any to
Error; specifically, refactor the CreateSubscriptionYAML component to accept the
exported CreateProps type (use CreateProps for the component props) and change
packageManifest.loadError?: any and operatorGroup.loadError?: any to loadError?:
Error so downstream code/tests get stricter typing.
frontend/packages/operator-lifecycle-manager/src/components/__tests__/catalog-source.spec.tsx (1)

177-210: Consider adding a test case for loadError scenario.

The tests cover loaded: true and loaded: false states, but there's no test verifying behavior when loadError is populated. Since the implementation now exposes loadError in CreateProps, a test ensuring the component handles fetch failures gracefully would improve coverage.

it('handles package manifest load error gracefully', () => {
  mockUseK8sWatchResources.mockReturnValue({
    packageManifest: { loaded: true, data: undefined, loadError: new Error('Not found') },
    operatorGroup: { loaded: true, data: [], loadError: null },
  });

  renderWithProviders(<CreateSubscriptionYAML />);
  // Assert fallback UI is rendered
  expect(screen.getByText(/Package not found/i)).toBeVisible();
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/packages/operator-lifecycle-manager/src/components/__tests__/catalog-source.spec.tsx`
around lines 177 - 210, Add a test covering the loadError path by mocking
mockUseK8sWatchResources to return packageManifest with loaded: true (or false,
depending on component logic), data: undefined, and loadError: new Error('Not
found'), render CreateSubscriptionYAML, and assert the component shows the
expected fallback UI (e.g., a "Package not found" message or other error UI your
component renders when packageManifest.loadError is present); target the
existing test block for CreateSubscriptionYAML and use screen.getByText to
verify the error/fallback UI is visible.
frontend/packages/operator-lifecycle-manager/src/components/topology/sidebar/TopologyOperatorBackedResources.tsx (1)

96-121: Memoize resourcesToGet to reduce watchedResources recomputation overhead.

resourcesToGet is recreated each render (via defaultResources.map), causing watchedResources to be recomputed unnecessarily even when content is identical. While useK8sWatchResources internally applies deep comparison via useDeepCompareMemoize (preventing actual watch churn), the repeated recomputation and comparison still incurs computational cost. Stabilizing resourcesToGet with useMemo would eliminate this overhead:

♻️ Suggested optimization
-  const resourcesToGet =
-    providedAPI?.resources ??
-    (defaultResources.map((kind) => ({
-      kind,
-    })) as CRDDescription['resources']);
+  const resourcesToGet = useMemo(
+    () =>
+      providedAPI?.resources ??
+      (defaultResources.map((kind) => ({
+        kind,
+      })) as CRDDescription['resources']),
+    [providedAPI?.resources],
+  );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/packages/operator-lifecycle-manager/src/components/topology/sidebar/TopologyOperatorBackedResources.tsx`
around lines 96 - 121, resourcesToGet is being recreated each render (via
defaultResources.map), causing watchedResources to be recomputed unnecessarily;
wrap the computation that builds resourcesToGet in a useMemo so it only changes
when its inputs change (e.g., defaultResources or any props used to compute it),
then keep using resourcesToGet in the existing watchedResources useMemo
dependency array so watchedResources only recomputes when the stabilized
resourcesToGet actually changes.
frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-subscribe.tsx (1)

1231-1269: DRY up URLSearchParams parsing for watch inputs.

The repeated new URLSearchParams(window.location.search) calls make the watch config harder to read and maintain. Capturing the values once improves clarity and keeps all selectors in sync.

Refactor to reuse parsed query params
 export const OperatorHubSubscribePage: FC = () => {
   const [activeNamespace] = useActiveNamespace();
+  const searchParams = new URLSearchParams(window.location.search);
+  const catalogNamespace = searchParams.get('catalogNamespace');
+  const pkg = searchParams.get('pkg');
+  const catalog = searchParams.get('catalog');
+  const targetNamespace = searchParams.get('targetNamespace');

   const resources = useK8sWatchResources<{
     operatorGroup: OperatorGroupKind[];
     packageManifest: PackageManifestKind[];
     subscription: SubscriptionKind[];
   }>({
     operatorGroup: {
       isList: true,
       kind: referenceForModel(OperatorGroupModel),
     },
     packageManifest: {
       isList: true,
       kind: referenceForModel(PackageManifestModel),
-      namespace: new URLSearchParams(window.location.search).get('catalogNamespace'),
-      fieldSelector: `metadata.name=${new URLSearchParams(window.location.search).get('pkg')}`,
+      namespace: catalogNamespace,
+      fieldSelector: `metadata.name=${pkg}`,
       selector: {
         matchLabels: {
-          catalog: new URLSearchParams(window.location.search).get('catalog'),
+          catalog,
         },
       },
     },
     subscription: {
       isList: true,
       kind: referenceForModel(SubscriptionModel),
     },
   });

   return (
     <OperatorHubSubscribe
       {...resources}
       namespace={activeNamespace}
       loaded={loaded}
       loadError={loadError}
-      targetNamespace={new URLSearchParams(window.location.search).get('targetNamespace') || null}
+      targetNamespace={targetNamespace || null}
     />
   );
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-subscribe.tsx`
around lines 1231 - 1269, The repeated new
URLSearchParams(window.location.search) calls in OperatorHubSubscribePage make
the useK8sWatchResources call hard to read and risk inconsistent selectors;
parse the query once into a const (e.g., const params = new
URLSearchParams(window.location.search)) at the top of OperatorHubSubscribePage
and replace all inline calls with params.get('catalogNamespace'),
params.get('pkg'), params.get('catalog'), and params.get('targetNamespace') when
building the packageManifest watch config and when passing targetNamespace prop
to OperatorHubSubscribe so the selectors stay consistent and the code is
cleaner.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/packages/container-security/src/components/image-manifest-vuln.tsx`:
- Around line 411-415: Remove the redundant top-level loaded prop from the
ContainerVulnerabilities call and instead derive loaded from
imageManifestVuln.loaded; also surface and handle the loadError by updating the
ContainerVulnerabilities component (e.g., inside the render path around where it
currently reads imageManifestVuln and loaded) to check
imageManifestVuln.loadError (or the passed loadError) and render an error
state/message instead of an indefinite spinner when truthy, ensuring
types/signature for ContainerVulnerabilities (props) are updated to accept
imageManifestVuln with data/loaded/loadError and no separate loaded prop.

In
`@frontend/packages/operator-lifecycle-manager/src/components/operator-install-page.tsx`:
- Around line 521-548: Mark the clusterServiceVersion watch as optional in the
useK8sWatchResources call (set optional: true for the clusterServiceVersion
entry alongside subscription and installPlans) and update OperatorInstallStatus
to check for resource load errors before relying on presence of data: inspect
resources.clusterServiceVersion.loadError (and other resources' loadError) and
surface a failure/stop the "Installing..." state when a 404 or other loadError
exists—mirror the loadError handling pattern used in operator-hub-page.tsx for
robust early-not-found handling.

In
`@frontend/packages/shipwright-plugin/src/components/build-form/PushSecretDropdown.tsx`:
- Around line 31-44: The resources variable in PushSecretDropdown is incorrectly
built as an object literal but ResourceDropdown expects an array of
FirehoseResult items; change the resources value to an array containing an
object with keys data, loaded, loadError, and kind (use SecretModel.kind) and
ensure data uses a safe fallback (e.g., secrets || []) so secrets being
undefined before loaded doesn't break getDropdownList's destructuring; update
the resources assignment where useK8sWatchResource returns [secrets, loaded,
loadError] and pass that array into ResourceDropdown.

In `@frontend/packages/topology/src/components/dropdowns/ApplicationDropdown.tsx`:
- Around line 16-25: ApplicationDropdown currently calls
getBaseWatchedResources(namespace) with an optional
ApplicationDropdownProps.namespace which can be undefined, producing invalid
watch configs; update ApplicationDropdown to guard the namespace before building
watches — e.g., check ApplicationDropdownProps.namespace and if undefined pass a
safe fallback (empty string) or short-circuit and return empty resources to use
in useK8sWatchResources, so getBaseWatchedResources and useK8sWatchResources
always receive a valid string or a no-op resource set; ensure you update the
call sites using getBaseWatchedResources and the resources prop passed into
ResourceDropdown accordingly.

---

Outside diff comments:
In
`@frontend/packages/operator-lifecycle-manager/src/components/catalog-source.tsx`:
- Around line 240-274: The packageManifest fetch error isn't propagated to the
withFallback fallback because the code only checks
createProps.packageManifest.loaded and data; update the component wrapped by
withFallback (the function passed into withFallback inside
requireOperatorGroup/Create) to first check if
createProps.packageManifest.loadError is set and if so throw that error so the
fallback UI is triggered; locate references to packageManifest, CreateProps,
withFallback and CreateYAML in the current function and throw
createProps.packageManifest.loadError before the existing loaded/data checks so
the ConsoleEmptyState is shown instead of the LoadingBox on fetch failure.

---

Nitpick comments:
In `@frontend/packages/container-security/src/components/image-manifest-vuln.tsx`:
- Around line 419-427: The ContainerVulnerabilitiesProps type has duplicated
loaded flags; remove the redundant top-level loaded and keep the nested
imageManifestVuln.loaded (or vice versa if you prefer a single top-level
source); update the type definition (ContainerVulnerabilitiesProps) so only one
loaded property exists and adjust consumers to read from the chosen location
(imageManifestVuln.loaded or the top-level loaded) to match the code path that
determines loading state.

In
`@frontend/packages/operator-lifecycle-manager/src/components/__tests__/catalog-source.spec.tsx`:
- Around line 177-210: Add a test covering the loadError path by mocking
mockUseK8sWatchResources to return packageManifest with loaded: true (or false,
depending on component logic), data: undefined, and loadError: new Error('Not
found'), render CreateSubscriptionYAML, and assert the component shows the
expected fallback UI (e.g., a "Package not found" message or other error UI your
component renders when packageManifest.loadError is present); target the
existing test block for CreateSubscriptionYAML and use screen.getByText to
verify the error/fallback UI is visible.

In
`@frontend/packages/operator-lifecycle-manager/src/components/catalog-source.tsx`:
- Around line 223-238: The hook call to useK8sWatchResources repeatedly
constructs new URLSearchParams from location.search inside its config object
causing unnecessary allocations; extract const searchParams = new
URLSearchParams(location.search) (or useMemo to memoize it) before calling
useK8sWatchResources and then use searchParams.get('pkg') and
searchParams.get('catalogNamespace') in the packageManifest config so the same
URLSearchParams instance is reused across renders (adjust any references inside
the component accordingly).
- Around line 249-261: The template is creating new URLSearchParams instances
for source and sourceNamespace; reuse the previously extracted searchParams
variable from the outer CreateSubscriptionYAML scope instead. In the Create
function (and specifically the template string in
CreateSubscriptionYAML/Create), replace new
URLSearchParams(location.search).get('catalog') and new
URLSearchParams(location.search).get('catalogNamespace') with
searchParams.get('catalog') and searchParams.get('catalogNamespace')
respectively so you avoid redundant allocations and keep behavior consistent.
- Around line 214-218: Extract and export the inline CreateProps type so it can
be reused and imported elsewhere, and tighten its loadError types from any to
Error; specifically, refactor the CreateSubscriptionYAML component to accept the
exported CreateProps type (use CreateProps for the component props) and change
packageManifest.loadError?: any and operatorGroup.loadError?: any to loadError?:
Error so downstream code/tests get stricter typing.

In
`@frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-subscribe.tsx`:
- Around line 1231-1269: The repeated new
URLSearchParams(window.location.search) calls in OperatorHubSubscribePage make
the useK8sWatchResources call hard to read and risk inconsistent selectors;
parse the query once into a const (e.g., const params = new
URLSearchParams(window.location.search)) at the top of OperatorHubSubscribePage
and replace all inline calls with params.get('catalogNamespace'),
params.get('pkg'), params.get('catalog'), and params.get('targetNamespace') when
building the packageManifest watch config and when passing targetNamespace prop
to OperatorHubSubscribe so the selectors stay consistent and the code is
cleaner.

In
`@frontend/packages/operator-lifecycle-manager/src/components/topology/sidebar/TopologyOperatorBackedResources.tsx`:
- Around line 96-121: resourcesToGet is being recreated each render (via
defaultResources.map), causing watchedResources to be recomputed unnecessarily;
wrap the computation that builds resourcesToGet in a useMemo so it only changes
when its inputs change (e.g., defaultResources or any props used to compute it),
then keep using resourcesToGet in the existing watchedResources useMemo
dependency array so watchedResources only recomputes when the stabilized
resourcesToGet actually changes.

Comment on lines 411 to 415
<ContainerVulnerabilities
pod={props.obj}
imageManifestVuln={{ data: imageManifestVuln, loaded, loadError }}
loaded={loaded}
/>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Redundant loaded prop and unhandled loadError state.

Two concerns here:

  1. Redundancy: loaded is passed both inside imageManifestVuln.loaded and as a separate top-level prop. Consider using only the top-level prop or only the nested one—not both.

  2. Error handling gap: loadError is captured and passed but never consumed by ContainerVulnerabilities. If the watch fails, loaded remains false and users see an indefinite loading spinner with no indication of the failure. The component should render an error state when loadError is truthy.

Proposed fix to handle error state and remove redundancy

Update ContainerVulnerabilities to handle the error case around line 357:

               <Td>
-                {props.loaded ? (
+                {props.imageManifestVuln.loadError ? (
+                  <span className="text-muted">{t('container-security~Unable to load vulnerability data')}</span>
+                ) : props.loaded ? (
                   withVuln(

And simplify props by removing the redundant top-level loaded:

     <ContainerVulnerabilities
       pod={props.obj}
       imageManifestVuln={{ data: imageManifestVuln, loaded, loadError }}
-      loaded={loaded}
     />

Then update type and usage to derive loaded from imageManifestVuln.loaded.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/packages/container-security/src/components/image-manifest-vuln.tsx`
around lines 411 - 415, Remove the redundant top-level loaded prop from the
ContainerVulnerabilities call and instead derive loaded from
imageManifestVuln.loaded; also surface and handle the loadError by updating the
ContainerVulnerabilities component (e.g., inside the render path around where it
currently reads imageManifestVuln and loaded) to check
imageManifestVuln.loadError (or the passed loadError) and render an error
state/message instead of an indefinite spinner when truthy, ensuring
types/signature for ContainerVulnerabilities (props) are updated to accept
imageManifestVuln with data/loaded/loadError and no separate loaded prop.

Comment on lines +521 to +548
const resources = useK8sWatchResources<{
clusterServiceVersion: ClusterServiceVersionKind;
subscription: SubscriptionKind;
installPlans: InstallPlanKind[];
}>({
clusterServiceVersion: {
kind: referenceForModel(ClusterServiceVersionModel),
namespaced: true,
isList: false,
name: currentCSV,
namespace: targetNamespace,
prop: 'clusterServiceVersion',
},
{
subscription: {
kind: referenceForModel(SubscriptionModel),
namespaced: true,
isList: false,
name: pkg,
namespace: targetNamespace,
optional: true,
prop: 'subscription',
},
{
installPlans: {
kind: referenceForModel(InstallPlanModel),
prop: 'installPlans',
namespaced: true,
namespace: targetNamespace,
isList: true,
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find k8s-watch-hook files
echo "=== Searching for k8s-watch-hook files ==="
find frontend/packages -type f -name "*k8s-watch-hook*" -o -name "*useK8sWatchResources*" 2>/dev/null | head -20

Repository: openshift/console

Length of output: 402


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# List files in operator-lifecycle-manager package
echo "=== Files in operator-lifecycle-manager ==="
find frontend/packages/operator-lifecycle-manager/src -type f -name "*.ts*" | grep -E "(hook|watch)" | head -20

Repository: openshift/console

Length of output: 1360


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find OperatorInstallStatus component
echo "=== Searching for OperatorInstallStatus ==="
find frontend/packages/operator-lifecycle-manager/src -type f -name "*OperatorInstallStatus*"

# Also search by content
rg -l "OperatorInstallStatus" frontend/packages/operator-lifecycle-manager/src --type tsx --type ts

Repository: openshift/console

Length of output: 149


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find all useK8sWatchResources usage for clusterServiceVersion
echo "=== useK8sWatchResources with ClusterServiceVersion ==="
rg -B5 -A15 "clusterServiceVersion:" frontend/packages/operator-lifecycle-manager/src -g "*.tsx" -g "*.ts" | head -100

Repository: openshift/console

Length of output: 5484


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the useK8sWatchResources hook implementation
echo "=== useK8sWatchResources implementation ==="
cat -n frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/useK8sWatchResources.ts | head -150

Repository: openshift/console

Length of output: 6582


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find OperatorInstallStatus
echo "=== Searching for OperatorInstallStatus ==="
rg -l "OperatorInstallStatus" frontend/packages/operator-lifecycle-manager/src --type ts --type tsx

# Search by pattern in operator-install-page.tsx
echo "=== Looking for OperatorInstallStatus in operator-install-page.tsx ==="
rg -n "OperatorInstallStatus" frontend/packages/operator-lifecycle-manager/src/components/operator-install-page.tsx

Repository: openshift/console

Length of output: 149


🏁 Script executed:

#!/bin/bash
set -euo pipeftp

# Get the full operator-install-page.tsx to see OperatorInstallStatus and loadError handling
echo "=== Getting line count ==="
wc -l frontend/packages/operator-lifecycle-manager/src/components/operator-install-page.tsx

Repository: openshift/console

Length of output: 113


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read more of useK8sWatchResources
cat -n frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/useK8sWatchResources.ts | tail -50

Repository: openshift/console

Length of output: 2063


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Get line count for operator-install-page.tsx
wc -l frontend/packages/operator-lifecycle-manager/src/components/operator-install-page.tsx

Repository: openshift/console

Length of output: 150


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for OperatorInstallStatus component definition
rg -n "export.*OperatorInstallStatus|const OperatorInstallStatus" frontend/packages/operator-lifecycle-manager/src --type ts

Repository: openshift/console

Length of output: 400


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read OperatorInstallStatus component starting at line 394
sed -n '394,520p' frontend/packages/operator-lifecycle-manager/src/components/operator-install-page.tsx | cat -n

Repository: openshift/console

Length of output: 5100


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for loadError handling in the file
rg -n "loadError" frontend/packages/operator-lifecycle-manager/src/components/operator-install-page.tsx -B2 -A2

Repository: openshift/console

Length of output: 540


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check k8s-watcher implementation for optional handling
echo "=== Checking k8s-watcher for optional handling ==="
rg -n "optional" frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks -g "*.ts"

Repository: openshift/console

Length of output: 111


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for how optional flag is used in k8s-watcher/related code
rg -n "\.optional" frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks -B2 -A2 | head -80

Repository: openshift/console

Length of output: 43


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check k8s-watcher to see if optional affects 404/loadError behavior
cat -n frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/k8s-watcher.ts | head -100

Repository: openshift/console

Length of output: 3790


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Look for documentation or tests about optional behavior
cat -n frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/__tests__/useK8sWatchResources.spec.tsx | head -80

Repository: openshift/console

Length of output: 3114


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for how optional affects 404/loadError in the actual action/reducer code
find frontend/packages/console-dynamic-plugin-sdk/src/app/k8s/actions -name "*.ts" | head -5

Repository: openshift/console

Length of output: 219


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for 404 handling in k8s actions
rg -n "404|optional" frontend/packages/console-dynamic-plugin-sdk/src/app/k8s/actions -g "*.ts" | head -50

Repository: openshift/console

Length of output: 43


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Look for how optional resources with 404 errors are handled in tests
rg -n "optional.*404|404.*optional" frontend/packages --type ts | head -20

Repository: openshift/console

Length of output: 43


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for other examples of optional CSV watches to see the pattern
rg -B3 -A3 "ClusterServiceVersion.*optional|optional.*ClusterServiceVersion" frontend/packages/operator-lifecycle-manager --type ts | head -80

Repository: openshift/console

Length of output: 43


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the k8s actions to understand optional behavior
head -150 frontend/packages/console-dynamic-plugin-sdk/src/app/k8s/actions/k8s.ts | cat -n

Repository: openshift/console

Length of output: 6064


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search where optional is actually used in watch resources
rg "optional.*true" frontend/packages --type ts --type tsx | grep -i "watch\|resource" | head -30

Repository: openshift/console

Length of output: 88


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Look for all CSV references and check if any are marked optional
rg -n "ClusterServiceVersion" frontend/packages/operator-lifecycle-manager/src -B2 -A2 --type ts --type tsx | grep -E "(optional|ClusterServiceVersion)" | head -50

Repository: openshift/console

Length of output: 88


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for optional usage patterns in all TypeScript files
rg -n "optional.*true" frontend/packages/operator-lifecycle-manager/src | head -40

Repository: openshift/console

Length of output: 778


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check if there are any other examples of CSV watches to compare
rg -n "useK8sWatchResource.*ClusterServiceVersion\|clusterServiceVersion.*useK8sWatchResource" frontend/packages --max-count 10

Repository: openshift/console

Length of output: 43


🏁 Script executed:

#!/antml/bash
set -euo pipefail

# Look at the complete OperatorInstallStatus component to see the full loading logic
sed -n '394,430p' frontend/packages/operator-lifecycle-manager/src/components/operator-install-page.tsx

Repository: openshift/console

Length of output: 1516


🏁 Script executed:

#!/an/bash
set -euo pipefail

# Check if there's any reference to ignoring loadError or handling 404
rg -n "resources\?" frontend/packages/operator-lifecycle-manager/src/components/operator-install-page.tsx | head -20

Repository: openshift/console

Length of output: 268


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify: look at how getIDAndDispatch handles optional resources
sed -n '82,110p' frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/k8s-watcher.ts

Repository: openshift/console

Length of output: 713


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check if optional is even used in getIDAndDispatch
rg -n "optional" frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/k8s-watcher.ts

Repository: openshift/console

Length of output: 43


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for where optional is actually handled in the hooks
rg -n "\.optional" frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/useK8sWatchResources.ts -B2 -A2

Repository: openshift/console

Length of output: 43


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check the complete OperatorInstallStatus to see all the loading logic, specifically how it handles loadError
sed -n '394,516p' frontend/packages/operator-lifecycle-manager/src/components/operator-install-page.tsx | tail -60

Repository: openshift/console

Length of output: 2031


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check WatchK8sResource type definition to understand optional
rg -n "interface WatchK8sResource|type WatchK8sResource" frontend/packages/console-dynamic-plugin-sdk/src/extensions/console-types.ts -A15

Repository: openshift/console

Length of output: 1177


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check if optional is used anywhere in the actual watch mechanism (k8sActions)
rg -n "optional" frontend/packages/console-dynamic-plugin-sdk/src/app/k8s/actions/k8s.ts

Repository: openshift/console

Length of output: 43


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Get complete picture: Read lines 521-548 of the file under review
sed -n '521,548p' frontend/packages/operator-lifecycle-manager/src/components/operator-install-page.tsx

Repository: openshift/console

Length of output: 812


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check if loadError is checked anywhere in the full component
sed -n '394,516p' frontend/packages/operator-lifecycle-manager/src/components/operator-install-page.tsx | grep -i "loaderror\|error"

Repository: openshift/console

Length of output: 146


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check if optional flag actually affects error handling anywhere
rg -n "resource\.optional\|resources\[.*\]\.optional" frontend/packages/console-dynamic-plugin-sdk/src -B3 -A3 | head -40

Repository: openshift/console

Length of output: 43


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check where the results from useK8sWatchResources are used in OperatorInstallStatus
# Look for any loadError access on the resources
sed -n '394,516p' frontend/packages/operator-lifecycle-manager/src/components/operator-install-page.tsx | grep -n "loadError\|loaded"

Repository: openshift/console

Length of output: 43


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify the exact current state: is CSV marked optional in the code under review?
sed -n '521,548p' frontend/packages/operator-lifecycle-manager/src/components/operator-install-page.tsx | grep -A2 "clusterServiceVersion:"

Repository: openshift/console

Length of output: 302


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for where optional flag is actually used in the redux store or watcher logic
rg -n "optional" frontend/packages/console-dynamic-plugin-sdk/src/app/k8s -B2 -A2 | head -80

Repository: openshift/console

Length of output: 43


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Look for any code that checks loadError in similar watch patterns
rg -n "\.loadError" frontend/packages/operator-lifecycle-manager/src/components --type ts | head -40

Repository: openshift/console

Length of output: 1248


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check if there are tests or examples showing optional behavior with 404
rg -n "optional" frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/__tests__ -B3 -A3 | head -60

Repository: openshift/console

Length of output: 43


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check for how optional resources with errors are typically handled
# Look for patterns where optional is used with loadError checks
rg -B5 -A5 "optional.*true" frontend/packages/operator-lifecycle-manager/src/components --type ts | grep -B5 -A5 "loadError" | head -80

Repository: openshift/console

Length of output: 43


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check useK8sWatchResource (singular) to see if it handles optional
cat -n frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/useK8sWatchResource.ts | head -120

Repository: openshift/console

Length of output: 3156


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Final check: does OperatorInstallStatus properly handle the case where CSV fails to load?
# Check if there's any defensive logic
sed -n '394,450p' frontend/packages/operator-lifecycle-manager/src/components/operator-install-page.tsx

Repository: openshift/console

Length of output: 1920


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Final verification: check if optional flag actually prevents watch dispatch
# Search for how optional is used in getIDAndDispatch or related code
rg -C10 "getIDAndDispatch" frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/k8s-watcher.ts | grep -A15 "export const getIDAndDispatch"

Repository: openshift/console

Length of output: 350


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check if optional flag is checked before dispatching watch
rg -n "if.*optional\|optional.*if" frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks -B3 -A3

Repository: openshift/console

Length of output: 43


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify: in OperatorInstallStatus, does it ever check loadError on CSV?
rg -n "clusterServiceVersion.*loadError\|loadError.*clusterServiceVersion" frontend/packages/operator-lifecycle-manager/src/components/operator-install-page.tsx

Repository: openshift/console

Length of output: 43


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Final verification: is the optional field actually USED anywhere?
rg -n "\.optional\b" frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks -A2 -B2 | head -100

Repository: openshift/console

Length of output: 43


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check if optional is documented or used in the action creators
rg -n "optional" frontend/packages/console-dynamic-plugin-sdk/src/app/k8s/actions/k8s.ts

Repository: openshift/console

Length of output: 43


🏁 Script executed:

#!/antml/bash
set -euo pipefail

# Summary check: does OperatorInstallStatus access loadError at all?
grep -n "\.loadError\|loadError" frontend/packages/operator-lifecycle-manager/src/components/operator-install-page.tsx

Repository: openshift/console

Length of output: 290


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Final verification: exhaustive search for where optional is actually checked
rg "resource\.optional\|r\.optional\|\[.*\]\.optional" frontend/packages/console-dynamic-plugin-sdk -n

Repository: openshift/console

Length of output: 43


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check if optional is used in the k8s resource module or action creators
rg "optional" frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/k8s-resource.ts -n

Repository: openshift/console

Length of output: 43


Mark CSV watch optional and add loadError handling to prevent stuck loading states.

When ClusterServiceVersion doesn't exist early in the install (common scenario), a non-optional watch returns a 404 loadError. However, OperatorInstallStatus ignores loadError on all resources—it only checks if data exists. This can leave the page stuck showing "Installing..." indefinitely. Mark the CSV watch optional for consistency with subscription and installPlans, and add explicit loadError checks similar to other OLM components (e.g., operator-hub-page.tsx).

✅ Minimal changes needed

1. Mark CSV optional:

     clusterServiceVersion: {
       kind: referenceForModel(ClusterServiceVersionModel),
       namespaced: true,
       isList: false,
       name: currentCSV,
       namespace: targetNamespace,
+      optional: true,
     },

2. Add loadError handling in OperatorInstallStatus (check similar to operator-hub-page.tsx:311):

 const OperatorInstallStatus: FC<OperatorInstallPageProps> = ({ resources }) => {
+  const loadError = Object.values(resources || {}).find((r) => r?.loadError)?.loadError;
+  if (loadError) {
+    return <Alert variant="danger" title="Installation Error">{loadError.toString()}</Alert>;
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/packages/operator-lifecycle-manager/src/components/operator-install-page.tsx`
around lines 521 - 548, Mark the clusterServiceVersion watch as optional in the
useK8sWatchResources call (set optional: true for the clusterServiceVersion
entry alongside subscription and installPlans) and update OperatorInstallStatus
to check for resource load errors before relying on presence of data: inspect
resources.clusterServiceVersion.loadError (and other resources' loadError) and
surface a failure/stop the "Installing..." state when a 404 or other loadError
exists—mirror the loadError handling pattern used in operator-hub-page.tsx for
robust early-not-found handling.

Comment on lines 31 to 44
const [secrets, loaded, loadError] = useK8sWatchResource<SecretKind[]>({
isList: true,
kind: SecretModel.kind,
namespace: props.namespace,
optional: true,
});

const resources = {
[SecretModel.id]: {
data: secrets,
loaded,
loadError,
},
];
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find ResourceDropdown
echo "=== Finding ResourceDropdown ==="
fd -t f "ResourceDropdown" --extension tsx --extension ts 2>/dev/null | head -20

echo ""
echo "=== Finding by pattern ==="
find . -name "*ResourceDropdown*" -type f 2>/dev/null | head -20

Repository: openshift/console

Length of output: 645


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Look for ResourceDropdown usage in packages/shipwright-plugin
echo "=== ResourceDropdown in shipwright-plugin ==="
rg "ResourceDropdown" frontend/packages/shipwright-plugin -l 2>/dev/null | head -10

echo ""
echo "=== PushSecretDropdown imports and usage ==="
rg "import.*ResourceDropdown" frontend/packages/shipwright-plugin -A 2 -B 2 2>/dev/null | head -30

Repository: openshift/console

Length of output: 1678


🏁 Script executed:

#!/bin/bash
set -euoripefail

# Examine PushSecretDropdown to see full context of ResourceDropdown usage
echo "=== PushSecretDropdown.tsx full context ==="
wc -l frontend/packages/shipwright-plugin/src/components/build-form/PushSecretDropdown.tsx

echo ""
echo "=== ResourceDropdown invocation ==="
rg "ResourceDropdown" frontend/packages/shipwright-plugin/src/components/build-form/PushSecretDropdown.tsx -B 5 -A 15

Repository: openshift/console

Length of output: 735


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find ResourceDropdown in console codebase
echo "=== Searching for ResourceDropdown in console ==="
find . -path ./node_modules -prune -o -name "*ResourceDropdown*" -type f -print 2>/dev/null | head -20

echo ""
echo "=== Searching by pattern for ResourceDropdown ==="
rg "export.*ResourceDropdown|class ResourceDropdown|function ResourceDropdown|const ResourceDropdown" -t tsx -t ts 2>/dev/null | head -20

Repository: openshift/console

Length of output: 426


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read ResourceDropdown implementation
echo "=== ResourceDropdown.tsx ==="
wc -l frontend/packages/console-shared/src/components/dropdown/ResourceDropdown.tsx
head -150 frontend/packages/console-shared/src/components/dropdown/ResourceDropdown.tsx

echo ""
echo "=== Bottom of ResourceDropdown.tsx ==="
tail -50 frontend/packages/console-shared/src/components/dropdown/ResourceDropdown.tsx

Repository: openshift/console

Length of output: 6829


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check how SecretDropdown handles resources
echo "=== SecretDropdown.tsx ==="
cat -n frontend/packages/shipwright-plugin/src/components/build-form/SecretDropdown.tsx

echo ""
echo "=== ConfigMapDropdown.tsx ==="
cat -n frontend/packages/shipwright-plugin/src/components/build-form/ConfigMapDropdown.tsx

Repository: openshift/console

Length of output: 3619


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check ResourceDropdown props and how it accesses resources
echo "=== ResourceDropdown component definition ==="
rg -n "interface.*ResourceDropdownProps|type.*ResourceDropdownProps|const ResourceDropdown|function ResourceDropdown" \
  frontend/packages/console-shared/src/components/dropdown/ResourceDropdown.tsx -A 20

echo ""
echo "=== How ResourceDropdown accesses resources data ==="
rg -n "resources\[|resources\." frontend/packages/console-shared/src/components/dropdown/ResourceDropdown.tsx -B 2 -A 2

Repository: openshift/console

Length of output: 1340


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check the full PushSecretDropdown to see ResourceDropdown invocation
echo "=== Full PushSecretDropdown.tsx ==="
cat -n frontend/packages/shipwright-plugin/src/components/build-form/PushSecretDropdown.tsx

Repository: openshift/console

Length of output: 2203


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find getResourceList method to confirm how it iterates
echo "=== getResourceList method ==="
rg -n "getResourceList" frontend/packages/console-shared/src/components/dropdown/ResourceDropdown.tsx -A 25 | head -50

echo ""
echo "=== getDropdownList method ==="
rg -n "getDropdownList" frontend/packages/console-shared/src/components/dropdown/ResourceDropdown.tsx -A 20 | head -40

Repository: openshift/console

Length of output: 3233


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify the type structure FirehoseResult
echo "=== FirehoseResult type definition ==="
rg -n "type FirehoseResult|interface FirehoseResult" --type ts --type tsx -A 15

echo ""
echo "=== Confirm what ResourceDropdown.resources expects ==="
rg -n "this\.props\.resources" frontend/packages/console-shared/src/components/dropdown/ResourceDropdown.tsx -B 2 -A 2

Repository: openshift/console

Length of output: 144


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Get full getResourceList and getDropdownList methods
echo "=== Full getResourceList and getDropdownList ==="
sed -n '176,250p' frontend/packages/console-shared/src/components/dropdown/ResourceDropdown.tsx

echo ""
echo "=== FirehoseResult type from types file ==="
fd -t f "types.ts" "types.tsx" | xargs grep -l "FirehoseResult" | head -1 | xargs cat -n | grep -A 10 "FirehoseResult"

Repository: openshift/console

Length of output: 2450


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for FirehoseResult in internal
echo "=== FirehoseResult definition ==="
rg "type FirehoseResult|interface FirehoseResult" -A 10

echo ""
echo "=== Check how resources array is iterated ==="
sed -n '176,220p' frontend/packages/console-shared/src/components/dropdown/ResourceDropdown.tsx

Repository: openshift/console

Length of output: 4153


Fix critical structural bug: resources must be an array, not an object literal.

PushSecretDropdown passes resources as a keyed object (lines 38–44), but ResourceDropdown expects resources: FirehoseResult[] — an array. The getDropdownList method iterates via _.each(resources, ({ data, kind }) => ...), which will fail to destructure an object literal. Follow the pattern from ConfigMapDropdown: wrap the resource state in an array and include the kind field:

Corrected pattern
  const [secrets, loaded, loadError] = useK8sWatchResource<SecretKind[]>({
    isList: true,
    kind: SecretModel.kind,
    namespace: props.namespace,
    optional: true,
  });

- const resources = {
-   [SecretModel.id]: {
-     data: secrets,
-     loaded,
-     loadError,
-   },
- };
+ const resources = [
+   {
+     data: secrets ?? [],
+     loaded,
+     loadError,
+     kind: SecretModel.kind,
+   },
+ ];

  return (
    <ResourceDropdown
      {...props}
      resources={resources}

Defensively guard secrets with a fallback (it may be undefined before loaded completes) and include kind for proper badge rendering and resource metadata.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/packages/shipwright-plugin/src/components/build-form/PushSecretDropdown.tsx`
around lines 31 - 44, The resources variable in PushSecretDropdown is
incorrectly built as an object literal but ResourceDropdown expects an array of
FirehoseResult items; change the resources value to an array containing an
object with keys data, loaded, loadError, and kind (use SecretModel.kind) and
ensure data uses a safe fallback (e.g., secrets || []) so secrets being
undefined before loaded doesn't break getDropdownList's destructuring; update
the resources assignment where useK8sWatchResource returns [secrets, loaded,
loadError] and pass that array into ResourceDropdown.

Comment on lines +16 to +25
const watchedBaseResources = useMemo(() => getBaseWatchedResources(namespace), [namespace]);

const resources = useK8sWatchResources(watchedBaseResources);

return (
<Firehose resources={resources}>
<ResourceDropdown
{...props}
placeholder={t('topology~Select an application')}
dataSelector={['metadata', 'labels', 'app.kubernetes.io/part-of']}
/>
</Firehose>
<ResourceDropdown
{...props}
resources={resources}
placeholder={t('topology~Select an application')}
dataSelector={['metadata', 'labels', 'app.kubernetes.io/part-of']}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find ApplicationDropdown component definition and props interface
fd -t f "ApplicationDropdown.tsx" frontend/packages/topology/src/

Repository: openshift/console

Length of output: 136


🏁 Script executed:

# Check the props interface for ApplicationDropdown
rg -n "ApplicationDropdownProps" frontend/packages/topology/src/components/dropdowns/ -A 10

Repository: openshift/console

Length of output: 1729


🏁 Script executed:

# Find getBaseWatchedResources implementation
fd -t f -name "*.ts" -o -name "*.tsx" frontend/packages/topology/src | xargs rg -l "getBaseWatchedResources" | head -5

Repository: openshift/console

Length of output: 569


🏁 Script executed:

# Find all ApplicationDropdown usages across the codebase
rg -n "ApplicationDropdown" frontend/ --type ts --type tsx -B 2 -A 5

Repository: openshift/console

Length of output: 88


🏁 Script executed:

# Read getBaseWatchedResources implementation
rg -n "getBaseWatchedResources" frontend/packages/topology/src/data-transforms/transform-utils.ts -A 15

Repository: openshift/console

Length of output: 495


🏁 Script executed:

# Find all ApplicationDropdown usages (fix file type argument)
rg -n "ApplicationDropdown" frontend/ -t ts -C 3 | head -100

Repository: openshift/console

Length of output: 11843


🏁 Script executed:

# Also check if ApplicationDropdown is exported and used in other packages
rg -n "<ApplicationDropdown" frontend/ --type ts --type js

Repository: openshift/console

Length of output: 455


🏁 Script executed:

# Read NamespaceBarApplicationSelector to see namespace prop
cat -n frontend/packages/topology/src/components/dropdowns/NamespaceBarApplicationSelector.tsx | head -70

Repository: openshift/console

Length of output: 2736


🏁 Script executed:

# Read ApplicationSelector to see namespace handling
sed -n '19,110p' frontend/packages/topology/src/components/dropdowns/ApplicationSelector.tsx | cat -n

Repository: openshift/console

Length of output: 3951


🏁 Script executed:

# Check what useK8sWatchResources does with undefined namespace in watch config
rg -n "useK8sWatchResources" frontend/packages/console-shared/src/hooks/ -A 20 | head -50

Repository: openshift/console

Length of output: 5395


Guard against undefined namespace in watch configuration.

ApplicationDropdownProps.namespace is optional, but getBaseWatchedResources expects a string and uses it directly in the K8s watch config. ApplicationSelector can pass undefined, creating invalid resource watches. NamespaceBarApplicationSelector is safe (Redux-sourced string). Add a guard or enforce namespace as required in ApplicationDropdown to prevent broken watch state.

🔧 Suggested guard
-  const watchedBaseResources = useMemo(() => getBaseWatchedResources(namespace), [namespace]);
+  const watchedBaseResources = useMemo(
+    () => (namespace ? getBaseWatchedResources(namespace) : {}),
+    [namespace],
+  );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const watchedBaseResources = useMemo(() => getBaseWatchedResources(namespace), [namespace]);
const resources = useK8sWatchResources(watchedBaseResources);
return (
<Firehose resources={resources}>
<ResourceDropdown
{...props}
placeholder={t('topology~Select an application')}
dataSelector={['metadata', 'labels', 'app.kubernetes.io/part-of']}
/>
</Firehose>
<ResourceDropdown
{...props}
resources={resources}
placeholder={t('topology~Select an application')}
dataSelector={['metadata', 'labels', 'app.kubernetes.io/part-of']}
const watchedBaseResources = useMemo(
() => (namespace ? getBaseWatchedResources(namespace) : {}),
[namespace],
);
const resources = useK8sWatchResources(watchedBaseResources);
return (
<ResourceDropdown
{...props}
resources={resources}
placeholder={t('topology~Select an application')}
dataSelector={['metadata', 'labels', 'app.kubernetes.io/part-of']}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/packages/topology/src/components/dropdowns/ApplicationDropdown.tsx`
around lines 16 - 25, ApplicationDropdown currently calls
getBaseWatchedResources(namespace) with an optional
ApplicationDropdownProps.namespace which can be undefined, producing invalid
watch configs; update ApplicationDropdown to guard the namespace before building
watches — e.g., check ApplicationDropdownProps.namespace and if undefined pass a
safe fallback (empty string) or short-circuit and return empty resources to use
in useK8sWatchResources, so getBaseWatchedResources and useK8sWatchResources
always receive a valid string or a no-op resource set; ensure you update the
call sites using getBaseWatchedResources and the resources prop passed into
ResourceDropdown accordingly.

@openshift-merge-robot openshift-merge-robot added the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Feb 20, 2026
@cajieh cajieh force-pushed the refactor-remaining-firehose-impacted-components branch from a37006c to b3b8032 Compare February 23, 2026 11:38
@openshift-merge-robot openshift-merge-robot removed the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Feb 23, 2026
@cajieh cajieh force-pushed the refactor-remaining-firehose-impacted-components branch from b3b8032 to ce609af Compare February 23, 2026 11:42
@cajieh
Copy link
Contributor Author

cajieh commented Feb 23, 2026

/hold

Until type issues due rebased are resolved.

@openshift-ci openshift-ci bot added the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Feb 23, 2026
@cajieh cajieh force-pushed the refactor-remaining-firehose-impacted-components branch from ce609af to 4ab4b82 Compare February 23, 2026 11:55
@cajieh
Copy link
Contributor Author

cajieh commented Feb 23, 2026

/cancel hold

@cajieh
Copy link
Contributor Author

cajieh commented Feb 23, 2026

/unhold

@openshift-ci openshift-ci bot removed the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Feb 23, 2026
@cajieh
Copy link
Contributor Author

cajieh commented Feb 23, 2026

/retest

@cajieh cajieh force-pushed the refactor-remaining-firehose-impacted-components branch from 4ab4b82 to 0d97c01 Compare February 23, 2026 12:20
obj?: FirehoseResult<PodKind>;
pod?: PodKind;
loaded: boolean;
loadError?: any;
Copy link
Member

Choose a reason for hiding this comment

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

better to use unknown instead of any

Copy link
Contributor Author

@cajieh cajieh Feb 23, 2026

Choose a reason for hiding this comment

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

See my take on this PR.
Let me know if we need to discuss this further.

Copy link
Member

Choose a reason for hiding this comment

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

Despite loadError being typed any it's still more semantically correct to type this as unknown so that we do not accidentally access properties which aren't present in the object in runtime

imageManifestVuln: {
data: ImageManifestVuln[];
loaded: boolean;
loadError?: any;
Copy link
Member

Choose a reason for hiding this comment

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

better to use unknown instead of any

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As stated previously.

resources?: {
[kind: string]: { data: K8sResourceKind[] };
resources: {
[kind: string]: { data: K8sResourceKind[]; loaded: boolean; loadError?: any };
Copy link
Member

Choose a reason for hiding this comment

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

better to use unknown instead of any

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As stated previously.

Comment on lines 215 to 216
packageManifest: { loaded: boolean; data?: PackageManifestKind; loadError?: any };
operatorGroup: { loaded: boolean; data?: OperatorGroupKind[]; loadError?: any };
Copy link
Member

Choose a reason for hiding this comment

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

better to use unknown instead of any

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As stated previously.

Comment on lines 551 to 552
catalogSources?: { data?: CatalogSourceKind[]; loaded?: boolean; loadError?: any };
packageManifests?: { data?: PackageManifestKind[]; loaded?: boolean; loadError?: any };
Copy link
Member

Choose a reason for hiding this comment

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

better to use unknown instead of any

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As stated previously.

Comment on lines 154 to 155
{pod.status?.containerStatuses?.[0]?.state?.terminated?.message ||
pod.status?.message}
Copy link
Member

Choose a reason for hiding this comment

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

pod is an optional prop per NodeTerminalInnerProps but we're assuming it always exists as there's no optional chanining for accessing pod.status.

either make pod a required prop or add optional chaining

Comment on lines +1239 to +1243
namespace: new URLSearchParams(window.location.search).get('catalogNamespace'),
fieldSelector: `metadata.name=${new URLSearchParams(window.location.search).get('pkg')}`,
selector: {
matchLabels: {
catalog: new URLSearchParams(window.location.search).get('catalog'),
Copy link
Member

Choose a reason for hiding this comment

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

feels like we should use useSearchParams

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As stated previously.

namespace={activeNamespace}
loaded={loaded}
loadError={loadError}
targetNamespace={new URLSearchParams(window.location.search).get('targetNamespace') || null}
Copy link
Member

Choose a reason for hiding this comment

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

feels like we should use useSearchParams

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As stated previously.

}) => {
const { t } = useTranslation();
const loaded = Object.values(resources).every((r) => r.loaded);
const loadError = Object.values(resources).find((r) => r.loadError)?.loadError;
Copy link
Member

Choose a reason for hiding this comment

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

so if there are multiple loadErrors, we will only show the first one? is this fine from a UX perspective, or maybe we should be showing every error?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I considered this as well, but I felt it would be more flexible to display one error at a time rather than aggregating them. Furthermore, components like StatusBox are designed to show only a single error. In most cases, when multiple resources fail to load, they share a common root cause such as network issues or RBAC permission, meaning resolving the first error often addresses the others.
Let’s discuss this further in today’s scrum if we want to reconsider how multiple errors are handled.

Comment on lines +229 to +230
name: new URLSearchParams(location.search).get('pkg'),
namespace: new URLSearchParams(location.search).get('catalogNamespace'),
Copy link
Member

Choose a reason for hiding this comment

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

feels like we should use useSearchParams

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not new, and appears to be out of scope for this work. We can open a ticket if this cleanup is required.

@cajieh cajieh force-pushed the refactor-remaining-firehose-impacted-components branch from 0d97c01 to fdd505b Compare February 24, 2026 17:30
@cajieh cajieh force-pushed the refactor-remaining-firehose-impacted-components branch from fdd505b to b22e4eb Compare February 24, 2026 17:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Indicates a PR has been approved by an approver from all required OWNERS files. component/core Related to console core functionality component/olm Related to OLM component/topology Related to topology

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants