Skip to content

FCTL catalog#533

Merged
rawagner merged 28 commits intoflightctl:mainfrom
rawagner:catalog
Mar 3, 2026
Merged

FCTL catalog#533
rawagner merged 28 commits intoflightctl:mainfrom
rawagner:catalog

Conversation

@rawagner
Copy link
Collaborator

@rawagner rawagner commented Feb 19, 2026

Summary by CodeRabbit

  • New Features

    • Added Software Catalog navigation menu item for managing applications and system images
    • Introduced Catalog page displaying available software items with search and filtering
    • Added multi-step install wizard for deploying OS images and applications to devices and fleets
    • Created edit wizards for updating installed software with version selection and change review
    • Added installed software view showing currently deployed catalog items on devices and fleets
    • Implemented update visualization showing available version upgrades with dependency tracking
  • Enhancements

    • Extended device and fleet details pages with Catalog tabs for software management

@coderabbitai
Copy link

coderabbitai bot commented Feb 19, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This pull request introduces a comprehensive Software Catalog feature enabling users to browse, install, and manage software items (OS images and applications) across the platform. It includes new navigation routes, API integrations, multi-step installation/editing wizards, dynamic form handling, and type definitions generated from OpenAPI specifications.

Changes

Cohort / File(s) Summary
Catalog Type Definitions
libs/types/alpha/*
Auto-generated TypeScript types and enums for Catalog, CatalogItem, CatalogItemVersion, CatalogItemArtifact, and related resources with support for versioning, channels, and deprecation tracking. Includes ApiVersion, Status, and list types following Kubernetes conventions.
Routing & Navigation
apps/ocp-plugin/console-extensions.json, apps/standalone/src/app/routes.tsx, libs/ui-components/src/hooks/useNavigate.tsx, apps/ocp-plugin/src/components/AppContext/AppContext.tsx, libs/ui-components/src/hooks/useAppContext.tsx
Added new routes for /catalog, /catalog/install, and device/fleet-specific catalog editing paths. New ROUTE enum members (CATALOG, CATALOG_INSTALL, CATALOG_FLEET_EDIT, CATALOG_DEVICE_EDIT) with navigation integration.
Core Catalog UI Components
libs/ui-components/src/components/Catalog/CatalogPage.tsx, CatalogPageToolbar.tsx, CatalogItemCard.tsx, CatalogItemDetails.tsx
Main catalog browsing interface with filtering, item display cards, toolbar with search/pagination, and details drawer showing item information, readme, and installation options.
Install Wizard
libs/ui-components/src/components/Catalog/InstallWizard/*
Multi-step installation workflow (Specifications, SelectTarget, AppConfig, Review) for deploying OS images and applications with version/channel selection, target picker, configuration forms, and success page.
Edit/Update Wizard
libs/ui-components/src/components/Catalog/EditWizard/*
Multi-step update workflow for modifying existing OS/app deployments including version selection with visual upgrade graph, configuration updates, and review steps.
Dynamic Form System
libs/ui-components/src/components/DynamicForm/*
RJSF-based dynamic form rendering with PatternFly widgets, field templates, array/object handling, and validation via AJV8. Supports volume image selection and asset management.
Device/Fleet Catalog Integration
libs/ui-components/src/components/Device/DeviceDetails/DeviceDetailsCatalog.tsx, libs/ui-components/src/components/Fleet/FleetDetails/FleetDetailsCatalog.tsx, ResourceCatalogPage.tsx
Renders installed software within device/fleet detail views with patch generation for updates and deletions. Integrates with catalog installation and editing flows.
Device/Fleet UI Updates
libs/ui-components/src/components/Device/DevicesPage/useDevices.ts, EnrolledDeviceTableRow.tsx, EnrolledDevicesTable.tsx, FleetRow.tsx, FleetsPage.tsx
Enhanced table columns with optional display, single-select support, pagination integration, and fleet filtering. Updated return types for devices/fleets hooks.
Device/Fleet Editing
libs/ui-components/src/components/Device/EditDeviceWizard/steps/DeviceTemplateStep.tsx, ApplicationTemplates.tsx
Added catalog label detection and read-only UI states for catalog-managed OS/application deployments; inline alerts indicating Software Catalog management.
API & Utilities
apps/ocp-plugin/src/utils/apiCalls.ts, apps/standalone/src/app/utils/apiCalls.ts, libs/ui-components/src/components/Catalog/utils.ts, const.ts
Extended API routing for catalog endpoints, patch generation utilities (getOsPatches, getAppPatches), version upgrade detection, and label constants for tracking catalog-managed resources.
Wizard Types & Validation
libs/ui-components/src/components/Catalog/InstallWizard/types.ts, utils.ts, useSubmitCatalogForm.ts, useCatalogs.ts, useCatalogFilter.ts
Type definitions for multi-step wizard forms, validation hooks, form submission handling, catalog item fetching with filtering/pagination, and configuration initialization.
Dependencies & Config
apps/ocp-plugin/package.json, apps/standalone/package.json, libs/ui-components/package.json, libs/types/package.json, tsconfig.json, webpack.config.ts
Added @patternfly/react-topology, @rjsf/core, @rjsf/utils, @rjsf/validator-ajv8, react-markdown, semver, monaco-editor. Path aliases for @flightctl/types/alpha. Webpack rules for assets.
OCP Plugin Wrappers
apps/ocp-plugin/src/components/Catalog/CatalogPage.tsx, CatalogInstallWizard.tsx, CatalogEditDeviceWizard.tsx, CatalogEditFleetWizard.tsx
Thin wrapper components that integrate UI library components into OCP plugin with page layout wrappers.
Localization & Type Support
libs/i18n/locales/en/translation.json, apps/ocp-plugin/src/typings.d.ts, libs/types/scripts/openapi-typescript.js, openapi-utils.js
Added 155+ new translation keys for catalog UI (items, assets, filters, deployment messages). Asset file type declarations. OpenAPI codegen for alpha API types with configurable core reference mappings.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant CatalogPage as Catalog UI
    participant API as Catalog API
    participant FormWizard as Install Wizard
    participant PatchAPI as Patch API
    participant Device/Fleet

    User->>CatalogPage: Browse catalog items
    CatalogPage->>API: Fetch catalog items (filtered)
    API-->>CatalogPage: Return CatalogItem list
    CatalogPage->>User: Display items

    User->>CatalogPage: Select item & click Install
    CatalogPage->>FormWizard: Pass CatalogItem
    
    User->>FormWizard: Specify version/channel (Specifications)
    User->>FormWizard: Pick target device/fleet (SelectTarget)
    FormWizard->>API: Fetch current OS/apps from target
    API-->>FormWizard: Return current state

    alt App Deployment
        User->>FormWizard: Configure app (AppConfig)
        FormWizard->>User: Render dynamic form from schema
    end

    User->>FormWizard: Review changes (Review)
    FormWizard->>User: Show patches preview

    User->>FormWizard: Submit
    FormWizard->>PatchAPI: Apply patches (setImageRef, addApplication, setLabels)
    PatchAPI->>Device/Fleet: Patch resource
    Device/Fleet-->>PatchAPI: Updated resource
    PatchAPI-->>FormWizard: Success
    FormWizard->>User: Show success page
Loading
sequenceDiagram
    participant User
    participant EditWizard as Edit Wizard
    participant CatalogAPI as Catalog API
    participant VersionGraph as Update Graph
    participant PatchAPI as Patch API
    participant Device/Fleet

    User->>EditWizard: Open deployed app/OS edit
    EditWizard->>CatalogAPI: Fetch CatalogItem + versions
    CatalogAPI-->>EditWizard: Return item with versions
    EditWizard->>EditWizard: Compute update candidates

    User->>VersionGraph: View version upgrade graph
    VersionGraph->>VersionGraph: Render nodes (current, available, deprecated)
    VersionGraph->>VersionGraph: Draw edges (replaces, skips, skipRange)
    VersionGraph->>User: Display interactive graph

    User->>VersionGraph: Select target version
    VersionGraph->>EditWizard: onSelectionChange(version)
    EditWizard->>User: Update form with selected version

    alt OS Update
        User->>EditWizard: Review OS replacement specs
    else App Update
        User->>EditWizard: Reconfigure app (AppConfig)
        EditWizard->>EditWizard: Validate schema
    end

    User->>EditWizard: Submit update
    EditWizard->>EditWizard: Generate patches (new image/app spec + labels)
    EditWizard->>PatchAPI: Send patches
    PatchAPI->>Device/Fleet: Apply patches
    Device/Fleet-->>PatchAPI: Updated
    PatchAPI-->>EditWizard: Success
    EditWizard->>User: Navigate to resource
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'FCTL catalog' is concise and clearly identifies the main change: adding catalog functionality to the FlightCtl UI platform.

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

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@rawagner rawagner force-pushed the catalog branch 10 times, most recently from a9fdb26 to 0632f21 Compare February 23, 2026 09:38
);
}
} else if (values.target === 'device') {
if (!values.device?.spec?.os?.image) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

When choosing to deploy an application on a fleetless device without an OS in its spec, the banner is displayed and this is not correct.

I suspect the same would happen if I try to deploy an app to a fleet that does not specify the OS yet.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

these were mostly placeholders..the alerts are now updated and should be correct

Copy link
Collaborator

Choose a reason for hiding this comment

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

The issue that we're deploying an application to a device that didn't specify the OS is still present.
Same as before, I think we need to check that the catalogItem.type is for an OS.


if (values.target === 'fleet') {
const numOfDevices = `${values.fleet?.status?.devicesSummary?.total || 0}`;
if (!values.fleet?.spec.template.spec.os?.image) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we should check the catalogItem.type and only show this warning when it's an OS that's being added.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

we have the isOs prop and only then render the alerts..but you are right that we can do that based on the catalogItem prop.
Fixed

);
}
} else if (values.target === 'device') {
if (!values.device?.spec?.os?.image) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

The issue that we're deploying an application to a device that didn't specify the OS is still present.
Same as before, I think we need to check that the catalogItem.type is for an OS.

@rawagner rawagner marked this pull request as ready for review February 25, 2026 09:25
Copy link

@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: 5

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

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

⚠️ Outside diff range comments (4)
libs/ui-components/src/components/common/FlightCtlWizardFooter.tsx (1)

57-63: ⚠️ Potential issue | 🟡 Minor

Close button on the read-only review step ignores onCancel.

The Cancel button (line 88) correctly uses onCancel ?? (() => navigate(-1)), but the Close button rendered on the read-only submit step (line 60) still unconditionally calls navigate(-1). If a modal-hosted wizard passes onCancel to close the modal, clicking Close on that step will attempt to navigate instead.

🐛 Proposed fix
-      <Button variant="primary" onClick={() => navigate(-1)}>
+      <Button variant="primary" onClick={onCancel ?? (() => navigate(-1))}>
         {t('Close')}
       </Button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/common/FlightCtlWizardFooter.tsx` around
lines 57 - 63, The Close button rendered in the isSubmitStep branch currently
calls navigate(-1) unconditionally; update that branch so the Button's onClick
uses the same cancellation handler as the Cancel button (i.e., onCancel ?? (()
=> navigate(-1))). Locate the isSubmitStep conditional and the primaryBtn
assignment inside FlightCtlWizardFooter (where primaryBtn and Button are set)
and replace the hardcoded navigate(-1) with the nullable onCancel fallback to
ensure modal-hosted wizards can close via the provided onCancel.
libs/types/scripts/openapi-typescript.js (2)

51-53: ⚠️ Potential issue | 🟡 Minor

Error message doesn't mention the new alphacore mode.

The error at Line 52 still reads "Use 'core' or 'imagebuilder'", but alphacore is now a valid mode.

✏️ Proposed fix
-    throw new Error(`Unknown mode: ${mode}. Use 'core' or 'imagebuilder'`);
+    throw new Error(`Unknown mode: ${mode}. Use 'core', 'alphacore', or 'imagebuilder'`);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/types/scripts/openapi-typescript.js` around lines 51 - 53, The thrown
Error when checking if (!config[mode]) still instructs "Use 'core' or
'imagebuilder'" even though alphacore is supported; update the Error message in
that validation block to include 'alphacore' (or better, derive the allowed
modes from Object.keys(config) to keep it accurate) so the message reflects the
actual valid modes for the variables config and mode.

126-131: ⚠️ Potential issue | 🟡 Minor

Initial cleanup doesn't remove alpha or tmp-alpha-types directories.

The Promise.all block cleans up models, imagebuilder, tmp-types, and tmp-imagebuilder-types, but omits the alpha equivalents. For a clean generation run, alpha and tmp-alpha-types should be included.

✏️ Proposed fix
     await Promise.all([
       rimraf(path.join(rootDir, 'models')),
       rimraf(path.join(rootDir, 'imagebuilder')),
+      rimraf(path.join(rootDir, 'alpha')),
       rimraf(path.join(rootDir, 'tmp-types')),
       rimraf(path.join(rootDir, 'tmp-imagebuilder-types')),
+      rimraf(path.join(rootDir, 'tmp-alpha-types')),
     ]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/types/scripts/openapi-typescript.js` around lines 126 - 131, The
Promise.all cleanup currently calls rimraf for 'models', 'imagebuilder',
'tmp-types', and 'tmp-imagebuilder-types' but misses the alpha equivalents;
update the Promise.all invocation to also call rimraf(path.join(rootDir,
'alpha')) and rimraf(path.join(rootDir, 'tmp-alpha-types')) so both the alpha
and temporary alpha directories are removed before generation—locate the
Promise.all block and add those two rimraf calls alongside the existing ones
(using the same rimraf and rootDir variables).
libs/ui-components/src/components/Table/Table.tsx (1)

80-91: ⚠️ Potential issue | 🟡 Minor

singleSelect and onSelectAll are not mutually exclusive — both header cells could render simultaneously.

If both singleSelect and onSelectAll are truthy, both a select-all checkbox <Th> and a single-select <Th> will render, producing two selection columns. These modes are semantically contradictory. Consider guarding or documenting that they should not be combined.

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

In `@libs/ui-components/src/components/Table/Table.tsx` around lines 80 - 91, The
header currently renders both selection cells when singleSelect and onSelectAll
are truthy; update the rendering logic in Table.tsx (around the Tr that renders
Th) to make them mutually exclusive — e.g., replace the two independent
conditions with a single conditional that prefers onSelectAll (render the
select-all Th when onSelectAll is provided) else renders the singleSelect Th;
also add a console.warn or prop validation inside the same component to surface
misuse when both props are passed.
♻️ Duplicate comments (2)
libs/types/scripts/openapi-typescript.js (1)

77-96: ⚠️ Potential issue | 🟡 Minor

Stale comments and variable names referencing "imagebuilder" instead of "alpha".

Lines 78, 87, and 90 still reference "Image builder" and use imagebuilderDir as the variable name, but this block handles the alpha core API types targeting the ../alpha directory. This was copy-pasted from the imagebuilder block without updating the comments/names.

✏️ Proposed fix
   } else if (mode === ALPHA_CORE_API) {
-    // Image builder types need to be fixed before they can be moved to their final location
+    // Alpha types need to be fixed before they can be moved to their final location
     await rimraf(finalDir);
     const modelsDir = path.join(output, 'models');
     if (fs.existsSync(modelsDir)) {
       await copyDir(modelsDir, finalDir);
     }
     console.log(`Fixing references to core API types...`);
     await fixCoreReferences(finalDir);

-    // Copy the generated index.ts to imagebuilder/index.ts
+    // Copy the generated index.ts to alpha/index.ts
     const indexPath = path.join(output, 'index.ts');
     if (fs.existsSync(indexPath)) {
-      const imagebuilderDir = path.resolve(__dirname, '../alpha');
-      if (!fs.existsSync(imagebuilderDir)) {
-        fs.mkdirSync(imagebuilderDir, { recursive: true });
+      const alphaDir = path.resolve(__dirname, '../alpha');
+      if (!fs.existsSync(alphaDir)) {
+        fs.mkdirSync(alphaDir, { recursive: true });
       }
-      await fsPromises.copyFile(indexPath, path.join(imagebuilderDir, 'index.ts'));
+      await fsPromises.copyFile(indexPath, path.join(alphaDir, 'index.ts'));
     }
     await rimraf(output);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/types/scripts/openapi-typescript.js` around lines 77 - 96, The comments
and variable names in the ALPHA_CORE_API branch are stale references to
"imagebuilder" — update the inline comment and the variable imagebuilderDir to
reflect "alpha" (e.g., alphaDir) and adjust the comment text near the block
handling mode === ALPHA_CORE_API and the copy operation that uses
indexPath/output so they clearly state they target the ../alpha directory;
ensure any mkdirSync/fsPromises.copyFile calls remain unchanged but use the
renamed symbol (alphaDir) and update the log/comment that currently says "Copy
the generated index.ts to imagebuilder/index.ts" to "Copy the generated index.ts
to alpha/index.ts".
libs/ui-components/src/components/Catalog/CatalogPage.tsx (1)

208-208: ⚠️ Potential issue | 🟠 Major

filterIsEmpty is always false — empty array is truthy.

catalogFilter.itemType is initialized as [] by useCatalogFilter, so !catalogFilter.itemType is always false. The empty state will never distinguish between "no filters active" and "filters active but no results."

🐛 Proposed fix
-  const filterIsEmpty = !catalogFilter.itemType;
+  const filterIsEmpty = catalogFilter.itemType.length === 0;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/Catalog/CatalogPage.tsx` at line 208,
filterIsEmpty incorrectly uses !catalogFilter.itemType which is always false
because catalogFilter.itemType is initialized to an array; update the check to
detect an empty array (e.g., Array.isArray(catalogFilter.itemType) ?
catalogFilter.itemType.length === 0 : !catalogFilter.itemType) so filterIsEmpty
reflects "no filters active"; change the variable computation in CatalogPage
(filterIsEmpty) and keep the rest of the logic that relies on it unchanged.
🟠 Major comments (16)
libs/ui-components/src/components/Fleet/useFleets.ts-18-18 (1)

18-18: ⚠️ Potential issue | 🟠 Major

limit is declared but never forwarded — endpoint always uses hardcoded PAGE_SIZE.

getFleetsEndpoint (lines 53–60) does not accept or use limit; it unconditionally sets limit: \${PAGE_SIZE}`. Any caller that passes limitviauseFleets/useFleetsEndpoint` will have it silently discarded, making the public API surface misleading.

Wire the field through or remove it until the implementation is ready.

🔧 Proposed fix — thread `limit` through `getFleetsEndpoint`
 const getFleetsEndpoint = ({
   name,
   addDevicesSummary,
   nextContinue,
+  limit,
 }: {
   name?: string;
   addDevicesSummary?: boolean;
   nextContinue?: string;
+  limit?: number;
 }) => {
   const params = new URLSearchParams({
-    limit: `${PAGE_SIZE}`,
+    limit: `${limit ?? PAGE_SIZE}`,
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/Fleet/useFleets.ts` at line 18, The `limit`
parameter is declared but never forwarded into the request - update
getFleetsEndpoint to accept and use the incoming `limit` (instead of
unconditionally using `PAGE_SIZE`), e.g. read `limit` from the function args and
set the request `limit` to that value or fall back to `PAGE_SIZE` when `limit`
is undefined; also propagate the `limit` parameter from `useFleets` /
`useFleetsEndpoint` into the call to `getFleetsEndpoint` and adjust any type
signatures so `limit?: number` is honored across these functions (keep
`PAGE_SIZE` as the default fallback).
libs/ui-components/src/components/Fleet/FleetRow.tsx-19-23 (1)

19-23: ⚠️ Potential issue | 🟠 Major

Require a delete handler when delete is enabled.

Line 19 makes onDeleteClick optional, but Lines 62-65 still add a clickable delete action when canDelete is true. This can render a non-functional delete action if a caller forgets the handler.

Suggested fix
-  if (canDelete) {
+  if (canDelete && onDeleteClick) {
     actions.push({
       title: t('Delete fleet'),
       onClick: onDeleteClick,
       tooltipProps: isManaged
         ? {
             content: t(
               "This fleet is managed by a resource sync and cannot be directly deleted. Either remove this fleet's definition from the resource sync configuration, or delete the resource sync first.",
             ),
           }
         : undefined,
       isAriaDisabled: isManaged,
     });
   }

Also applies to: 50-51, 62-65

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

In `@libs/ui-components/src/components/Fleet/FleetRow.tsx` around lines 19 - 23,
The component currently allows canDelete without a handler which can render a
dead delete action; update FleetRow so that delete rendering and behavior
require a handler by either making onDeleteClick non-optional in the props when
canDelete is used or, at minimum, guard the JSX and only render the delete
action when both canDelete and onDeleteClick are truthy (e.g., change the
conditional that renders the delete button to use canDelete && onDeleteClick),
and optionally add a runtime invariant (throw or console.warn) inside the
FleetRow function if canDelete is true but onDeleteClick is missing to
fail-fast; reference the onDeleteClick and canDelete props and the delete action
render logic in FleetRow.
apps/ocp-plugin/webpack.config.ts-53-59 (1)

53-59: ⚠️ Potential issue | 🟠 Major

Fix double-dot in image filenames.

Webpack 5's built-in asset/resource type includes the leading dot in the [ext] template substitution (e.g., .svg). With filename: 'assets/[name].[ext]', images will be output as assets/logo..svg instead of assets/logo.svg. The fonts rule correctly uses file-loader (where [ext] excludes the dot), but the images rule needs adjustment.

Fix
       generator: {
-        filename: 'assets/[name].[ext]',
+        filename: 'assets/[name][ext]',
       },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ocp-plugin/webpack.config.ts` around lines 53 - 59, The image asset
rule's generator.filename uses 'assets/[name].[ext]' which produces a double-dot
because Webpack's [ext] includes the leading dot; update the generator.filename
in the image rule (the object with test: /\.(jpg|jpeg|png|gif|svg)$/i and
generator) to use 'assets/[name][ext]' (remove the extra dot) so outputs become
assets/logo.svg instead of assets/logo..svg.
libs/ui-components/src/components/Catalog/InstallWizard/utils.ts-50-51 (1)

50-51: ⚠️ Potential issue | 🟠 Major

lodash/merge mutates defaultConfig in-place, corrupting the source CatalogItem data.

On Line 51, when defaultConfig is truthy it is a direct reference from catalogItem.spec.versions[].config or catalogItem.spec.defaults.config. merge(defaultConfig || {}, appConfig) mutates that object. Subsequent calls or re-renders will see the corrupted data.

Always merge into a fresh object:

🐛 Proposed fix
-    defaultConfig = merge(defaultConfig || {}, appConfig);
+    defaultConfig = merge({}, defaultConfig || {}, appConfig);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/Catalog/InstallWizard/utils.ts` around
lines 50 - 51, The current use of lodash/merge mutates the source object
(defaultConfig), corrupting the original CatalogItem data; update the merges to
use a fresh target object so we don't mutate inputs: for the defaultConfig merge
(the line with merge(defaultConfig || {}, appConfig)) change it to merge into a
new object (e.g., pass an empty object as the first arg) so you call merge with
a fresh target and the sources defaultConfig and appConfig; likewise ensure the
formValues merge (merge(formValues, appConfig)) does not unintentionally mutate
external state by merging into a new object when needed.
libs/ui-components/src/components/Catalog/const.ts-13-15 (1)

13-15: ⚠️ Potential issue | 🟠 Major

getAppVolumeName produces a leading dot when appName is undefined or empty.

When appName is undefined or '', the template literal yields ".volumeName.label" — a leading dot that results in malformed label keys like .myVolume.volume.catalog.flightctl.io/item. The function either needs to require appName: string (non-optional) or conditionally omit the prefix.

🐛 Proposed fix
 export const getAppVolumeName = (appName: string | undefined, volumeName: string, label: string) => {
-  return `${appName || ''}.${volumeName}.${label}`;
+  const prefix = appName ? `${appName}.` : '';
+  return `${prefix}${volumeName}.${label}`;
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/Catalog/const.ts` around lines 13 - 15,
getAppVolumeName currently produces a leading dot when appName is
undefined/empty; update the function (getAppVolumeName) to either make appName
required or, better, conditionally include the appName prefix: if appName is
truthy use `${appName}.${volumeName}.${label}`, otherwise return
`${volumeName}.${label}` (or similarly trim empty/whitespace appName before
deciding) so no leading dot appears and label keys remain valid.
libs/ui-components/src/components/DynamicForm/FormWidget.tsx-91-94 (1)

91-94: ⚠️ Potential issue | 🟠 Major

Select widget coerces enum values to strings and breaks typed schemas.

Casting values to string changes numeric/boolean enum values in form state, which can invalidate payload types.

🔧 Proposed fix (preserve original enum value types)
-      selected={value as string}
+      selected={value as string | number | undefined}
       onSelect={(_event, val) => {
-        onChange(val);
+        onChange(val as string | number);
         setIsOpen(false);
       }}
@@
-        {enumOptions.map((option) => (
-          <SelectOption key={String(option.value as string)} value={option.value as string}>
+        {enumOptions.map((option) => (
+          <SelectOption key={String(option.value)} value={option.value as string | number}>
             {option.label}
           </SelectOption>
         ))}

Also applies to: 112-114

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

In `@libs/ui-components/src/components/DynamicForm/FormWidget.tsx` around lines 91
- 94, In FormWidget.tsx, stop coercing enum values to strings: remove the cast
on selected and pass the original typed value through onSelect/onChange so
numeric/boolean enum values remain their original types; update the onSelect
handler (where it calls onChange(val) and setIsOpen(false)) to accept and
forward the exact value type from the option (not a string) and ensure the
selected prop uses the original value (not value as string); apply the same fix
to the other occurrence around the onSelect/onChange block near lines 112-114 so
form state preserves enum numeric/boolean types.
libs/ui-components/src/components/DynamicForm/FormWidget.tsx-128-133 (1)

128-133: ⚠️ Potential issue | 🟠 Major

Number widget currently forces 0/NaN into optional fields.

Empty values are coerced to 0, and invalid input can emit NaN. Both can create unintended state and payload values.

🔧 Proposed fix (preserve empty state, guard invalid numbers)
-  const numValue = typeof value === 'number' ? value : value ? Number(value) : 0;
+  const parsed = value === '' || value === undefined || value === null ? undefined : Number(value);
+  const numValue = typeof value === 'number' ? value : Number.isFinite(parsed) ? parsed : undefined;

   const handleChange = (event: React.FormEvent<HTMLInputElement>) => {
     const val = (event.target as HTMLInputElement).value;
-    onChange(val === '' ? undefined : Number(val));
+    if (val === '') {
+      onChange(undefined);
+      return;
+    }
+    const next = Number(val);
+    onChange(Number.isFinite(next) ? next : undefined);
   };

   return (
     <NumberInput
       id={id}
-      value={numValue}
+      value={numValue ?? ''}
       min={min}
       max={max}

Also applies to: 136-143

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

In `@libs/ui-components/src/components/DynamicForm/FormWidget.tsx` around lines
128 - 133, The number widget is coercing empty/invalid input to 0/NaN; update
the logic in the numValue calculation and handleChange so empty string or
null/undefined preserves an undefined value (not 0) and invalid parses do not
emit NaN — instead call onChange(undefined) for empty or non-numeric input and
only call onChange(Number(parsed)) when Number(parsed) is a finite number;
adjust the numValue derivation (currently using value, numValue) to display
''/undefined for empty state and apply the same guard in the other similar block
around lines 136-143 to avoid propagating 0/NaN.
libs/ui-components/src/components/Catalog/UpdateModal/UpdateGraph.tsx-122-122 (1)

122-122: ⚠️ Potential issue | 🟠 Major

Fix the Biome useIterableCallbackReturn error in the dedupe loop.

The expression-bodied forEach callback returns entriesMap.set(...), which triggers the lint error.

🧹 Proposed fix
-  directUpgradeEntries.forEach((entry) => entriesMap.set(entry.version, entry));
+  directUpgradeEntries.forEach((entry) => {
+    entriesMap.set(entry.version, entry);
+  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/Catalog/UpdateModal/UpdateGraph.tsx` at
line 122, The lint error is caused by the expression-bodied forEach callback
returning the value from entriesMap.set(...); change the callback to a
block-bodied function that returns void (or use an explicit for..of loop) so no
value is returned. Replace directUpgradeEntries.forEach((entry) =>
entriesMap.set(entry.version, entry)); with either
directUpgradeEntries.forEach((entry) => { entriesMap.set(entry.version, entry);
}); or a for (const entry of directUpgradeEntries) {
entriesMap.set(entry.version, entry); } in UpdateGraph.tsx (referencing
directUpgradeEntries and entriesMap).
libs/ui-components/src/components/Catalog/UpdateModal/UpdateGraph.tsx-227-252 (1)

227-252: ⚠️ Potential issue | 🟠 Major

Controller/model are frozen to initial props due to empty useMemo deps.

The graph model and selection handler capture initial values only. If updates, currentVersion, currentChannel, or onSelectionChange change, the visualization becomes stale.

♻️ Proposed fix (separate controller creation from model updates)
-  const controller = React.useMemo(() => {
+  const controller = React.useMemo(() => {
     const newController = new Visualization();
     newController.registerComponentFactory(customComponentFactory);
     newController.registerLayoutFactory(customLayoutFactory);
     newController.addEventListener(GRAPH_LAYOUT_END_EVENT, () => {
       newController.getGraph().fit(80);
@@
     newController.addEventListener(SELECTION_EVENT, (ids: string[]) => {
@@
     });
-
-    const model = buildTopologyModel(currentVersion, updates, currentChannel);
-    newController.fromModel(model, false);
-
     return newController;
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, []);
+  }, [onSelectionChange]);
+
+  React.useEffect(() => {
+    const model = buildTopologyModel(currentVersion, updates, currentChannel);
+    controller.fromModel(model, false);
+  }, [controller, currentVersion, updates, currentChannel]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/Catalog/UpdateModal/UpdateGraph.tsx` around
lines 227 - 252, The controller and its model are frozen because useMemo has an
empty dep array; keep creating the Visualization instance and registering static
factories once (Visualization, customComponentFactory, customLayoutFactory) but
move dynamic work into effects: create the controller with React.useMemo(() =>
new Visualization(), []), register static listeners once, then add a
React.useEffect that rebuilds the model (call buildTopologyModel(currentVersion,
updates, currentChannel)) and calls controller.fromModel(model, false) whenever
updates/currentVersion/currentChannel change; also ensure the selection callback
uses the latest onSelectionChange by either storing onSelectionChange in a ref
and reading ref.current inside the listener or by re-registering the
SELECTION_EVENT handler in an effect that depends on onSelectionChange so the
handler always calls the current onSelectionChange; reference symbols:
controller/Visualization, buildTopologyModel, controller.fromModel,
addEventListener(SELECTION_EVENT), and onSelectionChange.
libs/ui-components/src/components/Catalog/InstallWizard/types.ts-10-13 (1)

10-13: ⚠️ Potential issue | 🟠 Major

Consolidate InstallSpecFormik to the canonical location in types.ts.

The type is currently defined in both libs/ui-components/src/components/Catalog/InstallWizard/types.ts (lines 10–13) and libs/ui-components/src/components/Catalog/InstallWizard/steps/SpecificationsStep.tsx (lines 42–44). Remove the duplicate from SpecificationsStep.tsx, export it from types.ts, and update imports in VolumeImageField.tsx and CatalogItemDetails.tsx to import from types.ts instead of SpecificationsStep.tsx.

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

In `@libs/ui-components/src/components/Catalog/InstallWizard/types.ts` around
lines 10 - 13, Remove the duplicate InstallSpecFormik type from
SpecificationsStep.tsx and ensure the canonical definition in types.ts
(InstallSpecFormik) is exported; then update all consumers (imports in
VolumeImageField.tsx and CatalogItemDetails.tsx and any other files still
importing from SpecificationsStep.tsx) to import InstallSpecFormik from the
centralized types.ts instead of SpecificationsStep.tsx, and run a quick
TypeScript build to confirm no remaining references to the duplicate remain.
libs/types/package.json-20-24 (1)

20-24: ⚠️ Potential issue | 🟠 Major

./alpha export is added, but alpha directory is not whitelisted in package files.

The exports["./alpha"].source field points to ./alpha/index.ts (which exists), but alpha is missing from the files whitelist (lines 26–30). This breaks the source condition for consumers and tools that expect to resolve the source file from the published package.

Add "alpha" to the files array:

Proposed fix
   "files": [
     "dist",
     "index.ts",
-    "imagebuilder"
+    "imagebuilder",
+    "alpha"
   ],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/types/package.json` around lines 20 - 24, The package.json adds an
exports entry "./alpha" with source "./alpha/index.ts" but the "files" array
lacks the "alpha" directory, so the source won't be published; update the
package.json "files" array to include "alpha" (alongside any existing entries)
so that the exports["./alpha"].source path is included in the published package
and consumers/tools can resolve ./alpha/index.ts.
libs/ui-components/src/components/Catalog/InstallWizard/steps/SelectTargetStep.tsx-204-232 (1)

204-232: ⚠️ Potential issue | 🟠 Major

artifactType is typed incorrectly for translated labels.

artifactType is declared as CatalogItemArtifactType (an enum), but each branch assigns the result of t() which returns a translated string. This should be typed as string.

Proposed fix
-  let artifactType: CatalogItemArtifactType;
+  let artifactType: string;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@libs/ui-components/src/components/Catalog/InstallWizard/steps/SelectTargetStep.tsx`
around lines 204 - 232, The variable artifactType is incorrectly typed as
CatalogItemArtifactType while it's assigned translated strings from t(); change
its type to string by updating the declaration for artifactType (the let
artifactType declaration used in the switch on artifact.type) so all branches
that call t(...) assign a string type; ensure any usages of artifactType (after
the switch) expect a string rather than the enum.
libs/ui-components/src/components/Catalog/ResourceCatalog/ResourceCatalogPage.tsx-111-113 (1)

111-113: ⚠️ Potential issue | 🟠 Major

Avoid casting a potentially missing version to CatalogItemVersion.

find(...) can return undefined; casting bypasses the check and EditAppModal later reads currentVersion.version. Guard it before rendering.

💡 Proposed fix
+  const selectedInstallVersion = installItem?.item.spec.versions.find(
+    (v) => v.version === installItem.version,
+  );

-      {installItem?.item.spec.category === CatalogItemCategory.CatalogItemCategoryApplication && (
+      {installItem?.item.spec.category === CatalogItemCategory.CatalogItemCategoryApplication &&
+        selectedInstallVersion && (
         <EditAppModal
           currentApps={spec?.applications}
           catalogItem={installItem.item}
           currentChannel={installItem.channel}
           exisingLabels={currentLabels}
           onClose={() => setInstallItem(undefined)}
           onSubmit={async (catalogItem, catalogItemVersion, channel, values) => {
             await onUpdateApp(catalogItem, catalogItemVersion, channel, values);
             setSelectedItem(undefined);
           }}
-          currentVersion={
-            installItem.item.spec.versions.find((v) => v.version === installItem.version) as CatalogItemVersion
-          }
+          currentVersion={selectedInstallVersion}
         />
       )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@libs/ui-components/src/components/Catalog/ResourceCatalog/ResourceCatalogPage.tsx`
around lines 111 - 113, The code unsafely casts the result of
installItem.item.spec.versions.find(...) to CatalogItemVersion even though find
can return undefined; update ResourceCatalogPage so currentVersion is validated
before passing to EditAppModal (e.g., compute const currentVersion =
installItem.item.spec.versions.find(v => v.version === installItem.version); if
(!currentVersion) return or render a fallback/error; otherwise pass
currentVersion as CatalogItemVersion), or change the prop to accept undefined
and guard uses inside EditAppModal (ensure any access like
currentVersion.version is only after the undefined check).
libs/ui-components/src/components/Catalog/InstallWizard/steps/SelectTargetStep.tsx-252-262 (1)

252-262: ⚠️ Potential issue | 🟠 Major

deploymentTarget type should be string | undefined, not CatalogItemArtifactType | undefined.

The field is assigned from artifacts[0].uri (a string) and passed to getFullReferenceURI which expects its first parameter to be a string. Update the type declaration in libs/ui-components/src/components/Catalog/InstallWizard/types.ts line 23.

Proposed fix
--- a/libs/ui-components/src/components/Catalog/InstallWizard/types.ts
+++ b/libs/ui-components/src/components/Catalog/InstallWizard/types.ts
@@ -20,7 +20,7 @@ export type TargetPickerFormik = {
 
 export type InstallOsFormik = InstallSpecFormik &
   TargetPickerFormik & {
-    deploymentTarget: CatalogItemArtifactType | undefined;
+    deploymentTarget: string | undefined;
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@libs/ui-components/src/components/Catalog/InstallWizard/steps/SelectTargetStep.tsx`
around lines 252 - 262, Change the deploymentTarget type from
CatalogItemArtifactType | undefined to string | undefined in the InstallWizard
types so it matches the actual assigned value (artifacts[0].uri) and the
parameter expected by getFullReferenceURI; update the type alias/field in
libs/ui-components/src/components/Catalog/InstallWizard/types.ts (the
declaration for deploymentTarget used by SelectTargetStep.tsx and
getFullReferenceURI) to string | undefined and ensure any places constructing or
reading values.deploymentTarget use string semantics.
libs/ui-components/src/components/DynamicForm/DynamicForm.tsx-6-7 (1)

6-7: ⚠️ Potential issue | 🟠 Major

Remove import from internal @rjsf/validator-ajv8/lib/validator path and use ValidatorType from public API.

The /lib/validator path is not part of the public API and can break on package updates. Import ValidatorType from @rjsf/utils instead and use it to type the validator prop.

Proposed fix
-import { RJSFSchema, RegistryWidgetsType, TemplatesType } from '@rjsf/utils';
+import { RJSFSchema, RegistryWidgetsType, TemplatesType, ValidatorType } from '@rjsf/utils';
 import validator from '@rjsf/validator-ajv8';
 import { CatalogItem } from '@flightctl/types/alpha';
-import AJV8Validator from '@rjsf/validator-ajv8/lib/validator';

...

-      validator={validator as AJV8Validator<Record<string, unknown>, RJSFSchema, DynamicFormContext>}
+      validator={validator as ValidatorType<Record<string, unknown>, RJSFSchema, DynamicFormContext>}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/DynamicForm/DynamicForm.tsx` around lines 6
- 7, Replace the internal import of AJV8Validator from
'@rjsf/validator-ajv8/lib/validator' with the public ValidatorType from
'@rjsf/utils' and update the DynamicForm component's validator prop/type
accordingly: remove the AJV8Validator import, add an import for ValidatorType
from '@rjsf/utils', and type the validator prop (or any related
variables/functions) using ValidatorType so the component relies on the public
API instead of the internal /lib path.
libs/ui-components/src/components/Catalog/useCatalogs.ts-45-54 (1)

45-54: ⚠️ Potential issue | 🟠 Major

Operator-precedence bug in multi-value field selector conditions.

On Lines 47 and 52, > binds tighter than ||, so categories?.length || 0 > 1 is parsed as categories?.length || (0 > 1)categories?.length || false. The condition fires for any non-zero length instead of only length > 1. It happens to work today because the === 1 branch above consumes the single-element case, but it will break silently if the logic around it changes.

The same applies to Line 52: types.length || 0 > 1.

🐛 Proposed fix
-    if (categories.length === 1) {
-      parts.push(`spec.category==${categories[0]}`);
-    } else if (categories?.length || 0 > 1) {
-      parts.push(`spec.category in (${categories.join(',')})`);
-    }
-    if (types.length === 1) {
-      parts.push(`spec.type==${types[0]}`);
-    } else if (types.length || 0 > 1) {
-      parts.push(`spec.type in (${types?.join(',')})`);
-    }
+    if (categories.length === 1) {
+      parts.push(`spec.category==${categories[0]}`);
+    } else if (categories.length > 1) {
+      parts.push(`spec.category in (${categories.join(',')})`);
+    }
+    if (types.length === 1) {
+      parts.push(`spec.type==${types[0]}`);
+    } else if (types.length > 1) {
+      parts.push(`spec.type in (${types.join(',')})`);
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/Catalog/useCatalogs.ts` around lines 45 -
54, The multi-value branch conditions in useCatalogs.ts are using incorrect
operator precedence (e.g., categories?.length || 0 > 1 and types.length || 0 >
1) so they evaluate wrongly; update those checks to group the length fallback
before the comparison — e.g., use (categories?.length || 0) > 1 and
(types?.length || 0) > 1 — so the branches that push `spec.category in (...)`
and `spec.type in (...)` only run when length is greater than one; adjust both
the categories and types conditional expressions in the function that builds the
parts array.
🟡 Minor comments (13)
libs/ui-components/src/components/form/FormSelect.tsx-30-31 (1)

30-31: ⚠️ Potential issue | 🟡 Minor

Use ?? instead of || to guard against falsy-but-valid ReactNode values.

selectedLabel is typed as React.ReactNode, which admits 0, false, and '' as legitimate render values. Using || silently drops those and falls back to item.label. ?? (nullish coalescing) is correct here — it only falls back when selectedLabel is null or undefined.

🛠️ Proposed fix
 const getItemSelectLabel = (item: string | SelectItem) =>
-  isItemObject(item) ? item.selectedLabel || item.label : item;
+  isItemObject(item) ? item.selectedLabel ?? item.label : item;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/form/FormSelect.tsx` around lines 30 - 31,
The getItemSelectLabel helper drops valid falsy React nodes because it uses ||;
update the fallback to use nullish coalescing so selectedLabel only falls back
when null or undefined. In the getItemSelectLabel function (and any similar
logic handling SelectItem.selectedLabel typed as React.ReactNode), replace the
use of || with ?? so values like 0, false, and '' are preserved and only
null/undefined trigger fallback to item.label.
libs/ui-components/src/components/common/LabelsView.tsx-14-14 (1)

14-14: ⚠️ Potential issue | 🟡 Minor

Use startsWith instead of includes for prefix matching

CATALOG_LABEL = 'catalog.flightctl.io/' is a key prefix, not an arbitrary substring. Using .includes() would incorrectly filter out keys where catalog.flightctl.io/ appears mid-string (e.g. a hypothetical my-ns/catalog.flightctl.io/foo). .startsWith() expresses the intent precisely.

🐛 Proposed fix
-  const labelItems = Object.entries(labels || {}).filter(([key]) => !key.includes(CATALOG_LABEL));
+  const labelItems = Object.entries(labels || {}).filter(([key]) => !key.startsWith(CATALOG_LABEL));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/common/LabelsView.tsx` at line 14, The
filter in LabelsView.tsx uses includes on the label key which can match
mid-string; update the labelItems creation (the const labelItems =
Object.entries(...) line) to use key.startsWith(CATALOG_LABEL) and negate that
(i.e., filter(([key]) => !key.startsWith(CATALOG_LABEL))) so it only excludes
keys with the CATALOG_LABEL prefix; keep using the existing CATALOG_LABEL symbol
for the prefix check.
libs/ui-components/src/components/Catalog/CatalogPageToolbar.tsx-10-33 (1)

10-33: ⚠️ Potential issue | 🟡 Minor

itemType / setItemType accepted via CatalogFilter spread but never consumed.

By spreading the entire CatalogFilter type into CatalogPageToolbarProps, the component requires callers to provide itemType and setItemType even though the component never renders or uses them. If a type-filter chip group is planned, consider adding a // TODO comment; otherwise, narrow the props type to only what the component actually needs.

♻️ Proposed fix (narrow the props if the filter UI isn't planned for this component)
-type CatalogPageToolbarProps = CatalogFilter & {
+type CatalogPageToolbarProps = Pick<CatalogFilter, 'nameFilter' | 'setNameFilter'> & {
   pagination: PaginationDetails<CatalogItemList>;
   isUpdating: boolean;
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/Catalog/CatalogPageToolbar.tsx` around
lines 10 - 33, The CatalogPageToolbarProps currently spreads CatalogFilter which
forces callers to pass itemType and setItemType even though CatalogPageToolbar
never uses them; update the props by either (A) replacing "type
CatalogPageToolbarProps = CatalogFilter & { ... }" with an explicit props type
that only includes nameFilter, setNameFilter, pagination, and isUpdating, or (B)
if you intend to add a type-filter UI later, keep the spread but add a clear
TODO comment above CatalogPageToolbarProps referencing itemType and setItemType
and mark them optional so callers aren’t forced to provide them; modify the type
declaration and any affected usages accordingly (look for
CatalogPageToolbarProps, CatalogFilter, itemType, setItemType, and
CatalogPageToolbar).
libs/ui-components/src/components/common/CodeEditor/YamlEditorBase.tsx-153-162 (1)

153-162: ⚠️ Potential issue | 🟡 Minor

Download button has type="submit" — likely should be type="button".

The Download button at Line 155 uses type="submit" inside a <FlightCtlForm>. This could trigger unintended form submission behavior (e.g., page reload or form handler invocation) in addition to the onClick handler. Since this button only downloads the YAML content, it should be type="button".

🐛 Proposed fix
              <Button
                icon={<DownloadIcon />}
-               type="submit"
+               type="button"
                variant="secondary"
                className="pf-v6-u-ml-auto"
                aria-label={t('Download')}
                onClick={downloadYaml}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/common/CodeEditor/YamlEditorBase.tsx`
around lines 153 - 162, The Download Button in YamlEditorBase.tsx (the <Button>
rendering Download with aria-label and onClick={downloadYaml}) is set to
type="submit" and can unintentionally submit the surrounding FlightCtlForm;
change the button's type to "button" so clicking it only invokes downloadYaml
and does not trigger form submission or form handlers.
libs/ui-components/src/components/Catalog/CatalogItemDetails.css-3-3 (1)

3-3: ⚠️ Potential issue | 🟡 Minor

Remove deprecated word-break: break-wordoverflow-wrap: break-word on Line 2 already covers this.

word-break: break-word is listed as deprecated in the CSS specification; it is deprecated in favor of overflow-wrap: break-word, which is already declared on Line 2. The property is fully redundant here and Stylelint flags it as an error.

🐛 Proposed fix
 .fctl-catalog-item-details {
   overflow-wrap: break-word;
-  word-break: break-word;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/Catalog/CatalogItemDetails.css` at line 3,
Remove the deprecated redundant CSS property in CatalogItemDetails.css by
deleting the "word-break: break-word" declaration (it is redundant because
"overflow-wrap: break-word" is already present); update the
CatalogItemDetails.css rule block that contains both properties so only
"overflow-wrap: break-word" remains, ensuring Stylelint no longer flags the
deprecated property.
libs/ui-components/src/components/Catalog/CatalogItemCard.tsx-41-44 (1)

41-44: ⚠️ Potential issue | 🟡 Minor

Localize the image alt text.

Line 43 hardcodes the icon suffix, so assistive text stays English-only.

💡 Proposed fix
-              alt={`${catalogItem.metadata.name} icon`}
+              alt={t('{{name}} icon', { name: catalogItem.metadata.name })}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/Catalog/CatalogItemCard.tsx` around lines
41 - 44, The alt text for the image in CatalogItemCard currently appends the
hardcoded English word "icon" to catalogItem.metadata.name; update this to use
the app's localization utility (e.g., t/i18n.translate) so the suffix is
localized. Locate the image in CatalogItemCard that calls
getCatalogItemIcon(catalogItem) and replace the literal
`${catalogItem.metadata.name} icon` with a localized string that accepts the
item name as a variable (for example using a translation key like
"catalog.itemIcon" or similar) so assistive text is properly translated across
locales.
libs/ui-components/src/components/Catalog/UpdateModal/AppUpdateModal.tsx-185-200 (1)

185-200: ⚠️ Potential issue | 🟡 Minor

Typo in prop name: exisingLabelsexistingLabels

The misspelling appears three times: in the AppUpdateModalProps type (Line 185), in the destructured props (Line 197), and at the call site (Line 200). This is a public-facing API surface and should be corrected before the component is consumed externally.

🐛 Proposed fix
-  exisingLabels: Record<string, string> | undefined;
+  existingLabels: Record<string, string> | undefined;
 };
 
 const AppUpdateModal: React.FC<AppUpdateModalProps> = ({
   ...
-  exisingLabels,
+  existingLabels,
 }) => {
   ...
-  const appConfig = getInitialAppConfig(catalogItem, currentVersion.version, appSpec, exisingLabels);
+  const appConfig = getInitialAppConfig(catalogItem, currentVersion.version, appSpec, existingLabels);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/Catalog/UpdateModal/AppUpdateModal.tsx`
around lines 185 - 200, Rename the misspelled prop exisingLabels to
existingLabels across the public API: update the AppUpdateModalProps type
declaration, the destructured prop in the AppUpdateModal functional component,
and the call to getInitialAppConfig (replace exisingLabels with existingLabels)
so the prop name is consistent; ensure any references/tests/usage outside this
file are updated to the new existingLabels identifier to avoid runtime/typing
errors.
libs/ui-components/src/components/Catalog/InstalledSoftware.tsx-54-54 (1)

54-54: ⚠️ Potential issue | 🟡 Minor

Typo: exisingLabelsexistingLabels.

This typo is consistent across the prop definition (Line 54), destructuring (Line 63), and all usage sites (Lines 90, 399, 444). Worth fixing now before it becomes part of the public API surface.

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

In `@libs/ui-components/src/components/Catalog/InstalledSoftware.tsx` at line 54,
Rename the misspelled prop and all its references: change the prop name
exisingLabels to existingLabels in the InstalledSoftware component (update the
prop type declaration, the props destructuring in the component, and every usage
site currently referencing exisingLabels). Ensure you update any exported prop
types or interfaces and adjust any consumers/tests that import or pass
exisingLabels so they use existingLabels instead to avoid a breaking API change.
libs/ui-components/src/components/DynamicForm/VolumeImageField.tsx-33-33 (1)

33-33: ⚠️ Potential issue | 🟡 Minor

Local component CatalogItemList shadows the imported type of the same name.

Biome correctly flags this redeclaration. The imported CatalogItemList type (Line 33) is only used in the pagination prop typing (Line 120). Rename the local component or use a type-only import to avoid the collision.

Option: rename the local component
-const CatalogItemList = ({
+const CatalogItemListView = ({

And update the reference in SelectAssetModal:

-        <CatalogItemList
+        <CatalogItemListView

Also applies to: 127-127

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

In `@libs/ui-components/src/components/DynamicForm/VolumeImageField.tsx` at line
33, The local React component CatalogItemList is shadowing the imported type
CatalogItemList; change one to avoid the collision—either import the type as a
type-only import (e.g., "import type { CatalogItemList } from
'@flightctl/types/alpha'") so the name remains for the type, or rename the local
component (e.g., CatalogItemList -> CatalogItemListView) and update all usages
such as the pagination prop typing (pagination) and the reference inside
SelectAssetModal to the new component name.
libs/ui-components/src/components/Catalog/InstalledSoftware.tsx-348-365 (1)

348-365: ⚠️ Potential issue | 🟡 Minor

Empty object {} pushed into actions array when itemVersion is falsy.

Line 360 pushes {} (no title, no onClick) as an action item, which may render as a blank entry in PatternFly's ActionsColumn.

🐛 Proposed fix — filter out empty actions
-                  const actions: IAction[] = [
-                    itemVersion
-                      ? {
-                          title: t('Edit'),
-                          onClick: () =>
-                            setAppToEdit({
-                              item: app.item,
-                              name: app.name,
-                              channel: appChannel,
-                              version: itemVersion,
-                            }),
-                        }
-                      : {},
-                    {
-                      title: t('Delete'),
-                      onClick: () => setAppToDelete(app.name),
-                    },
-                  ];
+                  const actions: IAction[] = [
+                    ...(itemVersion
+                      ? [
+                          {
+                            title: t('Edit'),
+                            onClick: () =>
+                              setAppToEdit({
+                                item: app.item,
+                                name: app.name,
+                                channel: appChannel,
+                                version: itemVersion,
+                              }),
+                          },
+                        ]
+                      : []),
+                    {
+                      title: t('Delete'),
+                      onClick: () => setAppToDelete(app.name),
+                    },
+                  ];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/Catalog/InstalledSoftware.tsx` around lines
348 - 365, The actions array currently pushes an empty object when itemVersion
is falsy, causing a blank action entry; update the code that builds actions (the
const actions: IAction[] block) to only include the Edit action when itemVersion
is truthy (i.e., avoid pushing {}). Concretely, replace the ternary that yields
{} with a conditional inclusion/filter step (or build the array with a
conditional push) so that only valid IAction objects with title and onClick
(using setAppToEdit) are present, and ensure the Delete action (using
setAppToDelete) remains. Also verify the resulting actions array contains only
objects matching IAction before passing to ActionsColumn.
libs/ui-components/src/components/Catalog/CatalogItemDetails.tsx-268-273 (1)

268-273: ⚠️ Potential issue | 🟡 Minor

No guard for empty channels array on the selected version.

If latestVersion.channels is an empty array, channels[0] is undefined, violating the string return type and propagating an unexpected value into Formik.

♻️ Suggested fix
  return {
    version: latestVersion.version,
-   channel: latestVersion.channels[0],
+   channel: latestVersion.channels[0] || '',
  };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/Catalog/CatalogItemDetails.tsx` around
lines 268 - 273, The selected latestVersion (found via versions.find using
semver.prerelease) may have an empty channels array causing channel:
latestVersion.channels[0] to be undefined; update the logic in the block that
computes latestVersion and the returned object (the function that returns {
version, channel }) to guard against empty channels by falling back to a safe
default string (e.g., '' or 'default') or the first channel from another
suitable version: ensure you check latestVersion.channels?.length > 0 before
using [0], and if empty use a fallback value so the returned channel is always a
string and Formik receives a valid value.
libs/ui-components/src/components/Catalog/CatalogItemDetails.tsx-144-152 (1)

144-152: ⚠️ Potential issue | 🟡 Minor

Alert not wrapped in StackItem, unlike the sibling branch.

The <Alert> is a direct child of <Stack> while the deploy <Button> branch is wrapped in <StackItem>. This will cause inconsistent spacing from hasGutter. Wrap the Alert:

♻️ Suggested fix
  {item.spec.type === CatalogItemType.CatalogItemTypeData ? (
-   <Alert variant="info" isInline title={t('Data catalog item can be deployed as part of an application.')} />
+   <StackItem>
+     <Alert variant="info" isInline title={t('Data catalog item can be deployed as part of an application.')} />
+   </StackItem>
  ) : (
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/Catalog/CatalogItemDetails.tsx` around
lines 144 - 152, In CatalogItemDetails, the Alert branch is not wrapped in a
StackItem which causes inconsistent spacing compared to the Button branch;
update the conditional so the Alert (when item.spec.type ===
CatalogItemType.CatalogItemTypeData) is wrapped in a <StackItem> just like the
Deploy branch, ensuring the Alert is a child of StackItem (keep the existing
Alert props and text), leaving the Button branch and its onClick/disabled logic
(submitForm, installEnabled, isValid) unchanged.
libs/ui-components/src/components/Catalog/InstallWizard/steps/SpecificationsStep.tsx-58-61 (1)

58-61: ⚠️ Potential issue | 🟡 Minor

forEach callback should not return a value.

The arrow expression body (c) => (acc[c] = c) implicitly returns the assignment result. Use a block body instead. Also consider using a more idiomatic approach:

♻️ Suggested fix
- const channels = catalogItem.spec.versions.reduce((acc, v) => {
-   v.channels.forEach((c) => (acc[c] = c));
-   return acc;
- }, {});
+ const channels = catalogItem.spec.versions.reduce<Record<string, string>>((acc, v) => {
+   v.channels.forEach((c) => { acc[c] = c; });
+   return acc;
+ }, {});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@libs/ui-components/src/components/Catalog/InstallWizard/steps/SpecificationsStep.tsx`
around lines 58 - 61, The forEach callback inside the
catalogItem.spec.versions.reduce is using an arrow expression that implicitly
returns the assignment (v.channels.forEach((c) => (acc[c] = c))), which is
incorrect; change the callback to a block form that performs the assignment
without returning (e.g. v.channels.forEach(c => { acc[c] = c; })), or use a more
idiomatic approach such as iterating with for...of or using reduce/flatMap to
build the channels object; update the expression that sets the channels constant
(the reduce over catalogItem.spec.versions) accordingly so no value is returned
from the forEach callback.

Copy link

@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: 5

♻️ Duplicate comments (1)
libs/ui-components/src/components/Catalog/UpdateModal/OsUpdateModal.tsx (1)

35-52: isUpdating reset on early-return path — resolved ✅

setIsUpdating(false) at Line 41 now correctly resets the state before the early return, and the catch/finally block handles API errors. Both previously flagged issues are addressed.

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

In `@libs/ui-components/src/components/Catalog/UpdateModal/OsUpdateModal.tsx`
around lines 35 - 52, The early reset of isUpdating in handleUpdate is redundant
now that the try/catch/finally always calls setIsUpdating(false); remove the
inline setIsUpdating(false) before the early return (after checking
catalogItemVersion via catalogItem.spec.versions.find with selectedVersion) and
rely on the finally block to clear isUpdating so the state is reset exactly once
on all exit paths.
🧹 Nitpick comments (1)
libs/ui-components/src/components/DynamicForm/VolumeImageField.tsx (1)

219-220: useMemo empty-dep array is inconsistent with enableReinitialize.

initialValues is memoised without item in the dependency array (the eslint-disable suppresses the warning), while enableReinitialize on the Formik wrapper exists precisely to react to initialValues changes. In practice CatalogItemDetails is always remounted on a new selection, so there is no runtime bug today, but the combination is misleading: enableReinitialize is effectively dead, and if the parent ever optimises to keep the component mounted while swapping item, the form will not reinitialise.

Either include item in the dependency array and remove the eslint-disable, or remove enableReinitialize to document the real intent:

♻️ Proposed fix (option A — keep recomputation when item changes)
-  // eslint-disable-next-line react-hooks/exhaustive-deps
-  const initialValues = React.useMemo(() => getDefaultChannelAndVersion(item), []);
+  const initialValues = React.useMemo(() => getDefaultChannelAndVersion(item), [item]);
♻️ Proposed fix (option B — document that remount is the reset mechanism)
-  <Formik<InstallSpecFormik> initialValues={initialValues} enableReinitialize onSubmit={onSubmit}>
+  <Formik<InstallSpecFormik> initialValues={initialValues} onSubmit={onSubmit}>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/DynamicForm/VolumeImageField.tsx` around
lines 219 - 220, initialValues is memoized via React.useMemo(() =>
getDefaultChannelAndVersion(item), []) while Formik is using enableReinitialize,
which is inconsistent because the memo never updates when item changes; either
include item in the dependency array (remove the // eslint-disable-next-line
react-hooks/exhaustive-deps) so initialValues recomputes when item changes, or
remove enableReinitialize from the Formik wrapper to document that the component
relies on remount to reset the form—update the useMemo call referencing
getDefaultChannelAndVersion and the eslint-disable accordingly, or remove
enableReinitialize from the Formik props to make the intent explicit.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@libs/ui-components/src/components/Catalog/InstallWizard/InstallOsWizard.tsx`:
- Around line 112-119: The schema currently only checks presence for
channel/version; update validationSchema (the Yup.lazy for InstallOsFormik) to
validate allowed values instead of just required: for channel use
Yup.string().oneOf(availableChannelIds, t('Invalid channel
selected')).required(...), and for version compute the allowed version IDs for
the selected channel (e.g., derive allowedVersionIds from availableVersions or
from availableChannels[values.channel]) and use
Yup.string().oneOf(allowedVersionIds, t('Invalid version
selected')).required(...); ensure this uses the same
availableChannels/availableVersions data your component provides so the form
will surface errors instead of hitting the silent return path in the submit
handler.
- Around line 141-146: The code uses optional chaining when building resourceId
(selectedDevice?.metadata.name and selectedFleet?.metadata.name); instead
enforce that a selection exists and read metadata.name directly. Update the
logic around values.device / values.fleet (selectedDevice and selectedFleet) and
the installToDevice check to validate/ensure the chosen resource is present,
then construct resourceId using selectedDevice.metadata.name or
selectedFleet.metadata.name without ?. so the code relies on guaranteed resource
shapes (e.g., after validation or a guard).

In `@libs/ui-components/src/components/Catalog/UpdateModal/OsUpdateModal.tsx`:
- Line 57: The Modal's onClose must be guarded while an update is in flight:
wrap the existing onClose so it no-ops when isUpdating is true (e.g., replace
onClose={onClose} with a guarded handler that calls onClose() only if
!isUpdating) and also disable the header close / Escape-key paths while updating
by setting the Modal props that control those actions (e.g., disable Escape and
hide/disable the modal close button via the Modal props used in this codebase)
so handleUpdate can run to completion without the modal being dismissed.

In `@libs/ui-components/src/components/DynamicForm/VolumeImageField.tsx`:
- Around line 303-304: Remove the unnecessary fallback by replacing the
expression that sets assetItemName (currently using item.metadata.name || '') to
use item.metadata.name directly; since item is a CatalogItem returned from the
API, metadata.name is guaranteed defined, so update the assignment in the
VolumeImageField component (the code that sets assetCatalog and assetItemName)
to use item.metadata.name without the || '' fallback and ensure any related
locals/props expecting a possibly-empty string accept the defined name type.
- Line 33: The local component named CatalogItemList is shadowing the imported
CatalogItemList type; rename the local component (e.g. to AssetCatalogList or
CatalogItemListPanel) and update all references to it (component declaration,
any exports, and where SelectAssetModal renders or references CatalogItemList)
so the imported type PaginationDetails<CatalogItemList> continues to refer to
the type and the linter noRedeclare error is resolved; ensure the component name
change is applied consistently across props, JSX usages, and any default export.

---

Duplicate comments:
In `@libs/ui-components/src/components/Catalog/UpdateModal/OsUpdateModal.tsx`:
- Around line 35-52: The early reset of isUpdating in handleUpdate is redundant
now that the try/catch/finally always calls setIsUpdating(false); remove the
inline setIsUpdating(false) before the early return (after checking
catalogItemVersion via catalogItem.spec.versions.find with selectedVersion) and
rely on the finally block to clear isUpdating so the state is reset exactly once
on all exit paths.

---

Nitpick comments:
In `@libs/ui-components/src/components/DynamicForm/VolumeImageField.tsx`:
- Around line 219-220: initialValues is memoized via React.useMemo(() =>
getDefaultChannelAndVersion(item), []) while Formik is using enableReinitialize,
which is inconsistent because the memo never updates when item changes; either
include item in the dependency array (remove the // eslint-disable-next-line
react-hooks/exhaustive-deps) so initialValues recomputes when item changes, or
remove enableReinitialize from the Formik wrapper to document that the component
relies on remount to reset the form—update the useMemo call referencing
getDefaultChannelAndVersion and the eslint-disable accordingly, or remove
enableReinitialize from the Formik props to make the intent explicit.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 399770c and 13da7df.

📒 Files selected for processing (7)
  • libs/ui-components/src/components/Catalog/CatalogItemCard.tsx
  • libs/ui-components/src/components/Catalog/InstallWizard/InstallOsWizard.tsx
  • libs/ui-components/src/components/Catalog/UpdateModal/OsUpdateModal.tsx
  • libs/ui-components/src/components/Catalog/utils.ts
  • libs/ui-components/src/components/DynamicForm/VolumeImageField.tsx
  • libs/ui-components/src/components/form/LabelsField.tsx
  • libs/ui-components/tsconfig.json
🚧 Files skipped from review as they are similar to previous changes (4)
  • libs/ui-components/tsconfig.json
  • libs/ui-components/src/components/form/LabelsField.tsx
  • libs/ui-components/src/components/Catalog/utils.ts
  • libs/ui-components/src/components/Catalog/CatalogItemCard.tsx

Comment on lines +112 to +119
const validationSchema = Yup.lazy((values: InstallOsFormik) =>
Yup.object({
target: Yup.string().required(t('Target must be selected')),
device: values.target === 'device' ? Yup.object().required(t('Device must be selected')) : Yup.object(),
fleet: values.target === 'fleet' ? Yup.object().required(t('Fleet must be selected')) : Yup.object(),
channel: Yup.string().required(t('Channel must be selected')),
version: Yup.string().required(t('Version must be selected')),
}),
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Schema should validate allowed channel/version values, not just presence.

On Line 117 and Line 118, required() alone allows stale/tampered URL values. Downstream, that can hit the silent return path on Line 152-Line 154 with no user feedback.

💡 Proposed fix
+  const allowedChannels = catalogItem.spec.channels.map((c) => c.name);
+  const allowedVersions = catalogItem.spec.versions.map((v) => v.version);
+
   const validationSchema = Yup.lazy((values: InstallOsFormik) =>
     Yup.object({
       target: Yup.string().required(t('Target must be selected')),
       device: values.target === 'device' ? Yup.object().required(t('Device must be selected')) : Yup.object(),
       fleet: values.target === 'fleet' ? Yup.object().required(t('Fleet must be selected')) : Yup.object(),
-      channel: Yup.string().required(t('Channel must be selected')),
-      version: Yup.string().required(t('Version must be selected')),
+      channel: Yup.string()
+        .oneOf(allowedChannels, t('Channel must be selected'))
+        .required(t('Channel must be selected')),
+      version: Yup.string()
+        .oneOf(allowedVersions, t('Version must be selected'))
+        .required(t('Version must be selected')),
     }),
   );

Also applies to: 150-154

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

In `@libs/ui-components/src/components/Catalog/InstallWizard/InstallOsWizard.tsx`
around lines 112 - 119, The schema currently only checks presence for
channel/version; update validationSchema (the Yup.lazy for InstallOsFormik) to
validate allowed values instead of just required: for channel use
Yup.string().oneOf(availableChannelIds, t('Invalid channel
selected')).required(...), and for version compute the allowed version IDs for
the selected channel (e.g., derive allowedVersionIds from availableVersions or
from availableChannels[values.channel]) and use
Yup.string().oneOf(allowedVersionIds, t('Invalid version
selected')).required(...); ensure this uses the same
availableChannels/availableVersions data your component provides so the form
will surface errors instead of hitting the silent return path in the submit
handler.

Comment on lines +141 to +146
const selectedDevice = values.device;
const selectedFleet = values.fleet;
const installToDevice = values.target === 'device';
const resourceId = installToDevice
? `devices/${selectedDevice?.metadata.name}`
: `fleets/${selectedFleet?.metadata.name}`;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Avoid optional chaining when reading metadata.name for selected resources.

Line 145 and Line 146 should not use ?. for metadata.name in this flow; enforce selection first, then use direct metadata.name.

💡 Proposed fix
     const selectedDevice = values.device;
     const selectedFleet = values.fleet;
     const installToDevice = values.target === 'device';
+    if (installToDevice && !selectedDevice) {
+      setError(t('Device must be selected'));
+      return;
+    }
+    if (!installToDevice && !selectedFleet) {
+      setError(t('Fleet must be selected'));
+      return;
+    }
     const resourceId = installToDevice
-      ? `devices/${selectedDevice?.metadata.name}`
-      : `fleets/${selectedFleet?.metadata.name}`;
+      ? `devices/${selectedDevice.metadata.name}`
+      : `fleets/${selectedFleet.metadata.name}`;

As per coding guidelines, "Treat resource.metadata.name as always defined for resources returned from the API or created resources, and avoid optional chaining in these contexts; only handle undefined for partial or input payloads."

📝 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 selectedDevice = values.device;
const selectedFleet = values.fleet;
const installToDevice = values.target === 'device';
const resourceId = installToDevice
? `devices/${selectedDevice?.metadata.name}`
: `fleets/${selectedFleet?.metadata.name}`;
const selectedDevice = values.device;
const selectedFleet = values.fleet;
const installToDevice = values.target === 'device';
if (installToDevice && !selectedDevice) {
setError(t('Device must be selected'));
return;
}
if (!installToDevice && !selectedFleet) {
setError(t('Fleet must be selected'));
return;
}
const resourceId = installToDevice
? `devices/${selectedDevice.metadata.name}`
: `fleets/${selectedFleet.metadata.name}`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/Catalog/InstallWizard/InstallOsWizard.tsx`
around lines 141 - 146, The code uses optional chaining when building resourceId
(selectedDevice?.metadata.name and selectedFleet?.metadata.name); instead
enforce that a selection exists and read metadata.name directly. Update the
logic around values.device / values.fleet (selectedDevice and selectedFleet) and
the installToDevice check to validate/ensure the chosen resource is present,
then construct resourceId using selectedDevice.metadata.name or
selectedFleet.metadata.name without ?. so the code relies on guaranteed resource
shapes (e.g., after validation or a guard).

Comment on lines +303 to +304
assetCatalog: item.metadata.catalog,
assetItemName: item.metadata.name || '',
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Drop the || '' fallback on item.metadata.name.

item here is a CatalogItem returned from the API, so metadata.name must be treated as always defined per the project coding guideline. Using || '' implies it could be undefined, which contradicts the guideline.

♻️ Proposed fix
-      assetItemName: item.metadata.name || '',
+      assetItemName: item.metadata.name,

As per coding guidelines, "Treat resource.metadata.name as always defined for resources returned from the API or created resources, and avoid optional chaining in these contexts."

📝 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
assetCatalog: item.metadata.catalog,
assetItemName: item.metadata.name || '',
assetCatalog: item.metadata.catalog,
assetItemName: item.metadata.name,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/DynamicForm/VolumeImageField.tsx` around
lines 303 - 304, Remove the unnecessary fallback by replacing the expression
that sets assetItemName (currently using item.metadata.name || '') to use
item.metadata.name directly; since item is a CatalogItem returned from the API,
metadata.name is guaranteed defined, so update the assignment in the
VolumeImageField component (the code that sets assetCatalog and assetItemName)
to use item.metadata.name without the || '' fallback and ensure any related
locals/props expecting a possibly-empty string accept the defined name type.

Copy link

@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: 11

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

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

⚠️ Outside diff range comments (5)
libs/ui-components/src/components/Device/DeviceDetails/DeviceDetailsPage.tsx (1)

153-159: ⚠️ Potential issue | 🟡 Minor

'catalog' in tabKeys but the tab/route only exist when isEnrolled — direct URL navigation breaks.

tabKeys unconditionally includes 'catalog', while the <Tab> and <Route path="catalog"> are both gated on isEnrolled. If a user directly navigates to /devices/:id/catalog on a non-enrolled device, there is no matching <Route>, so nothing renders (no redirect either). Consider either removing 'catalog' from tabKeys when not enrolled, or adding a fallback <Route path="catalog" element={<Navigate to="details" replace />} /> for that case.

🐛 Proposed fix — guard tabKeys
-<TabsNav aria-label="Device details tabs" tabKeys={['details', 'catalog', 'yaml', 'terminal', 'events']}>
+<TabsNav aria-label="Device details tabs" tabKeys={['details', ...(isEnrolled ? ['catalog'] : []), 'yaml', 'terminal', 'events']}>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/Device/DeviceDetails/DeviceDetailsPage.tsx`
around lines 153 - 159, The tabs list in DeviceDetailsPage includes 'catalog'
unconditionally in the TabsNav tabKeys while the actual <Tab eventKey="catalog">
and the corresponding Route are only rendered when isEnrolled, causing broken
direct navigation; update the tabKeys construction so it conditionally includes
'catalog' only when isEnrolled (e.g., build tabKeys from an array that pushes
'catalog' if isEnrolled) or alternatively add a fallback Route for
path="catalog" that redirects to "details" when isEnrolled is false; locate the
TabsNav/tabKeys usage and the Route for "catalog" in DeviceDetailsPage to
implement the chosen fix.
libs/types/scripts/openapi-typescript.js (2)

126-131: ⚠️ Potential issue | 🟡 Minor

Missing cleanup of alpha and tmp-alpha-types directories.

The initial cleanup in main() removes models, imagebuilder, tmp-types, and tmp-imagebuilder-types, but doesn't clean alpha or tmp-alpha-types. This could leave stale alpha type files from a previous generation run.

     await Promise.all([
       rimraf(path.join(rootDir, 'models')),
       rimraf(path.join(rootDir, 'imagebuilder')),
+      rimraf(path.join(rootDir, 'alpha')),
       rimraf(path.join(rootDir, 'tmp-types')),
       rimraf(path.join(rootDir, 'tmp-imagebuilder-types')),
+      rimraf(path.join(rootDir, 'tmp-alpha-types')),
     ]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/types/scripts/openapi-typescript.js` around lines 126 - 131, The cleanup
in main() currently calls rimraf for 'models', 'imagebuilder', 'tmp-types', and
'tmp-imagebuilder-types' but misses the 'alpha' and 'tmp-alpha-types'
directories; update the Promise.all cleanup array in openapi-typescript.js to
also rimraf path.join(rootDir, 'alpha') and path.join(rootDir,
'tmp-alpha-types') so stale alpha type files are removed on each run (look for
the rimraf calls in the initial cleanup block inside main()).

52-52: ⚠️ Potential issue | 🟡 Minor

Stale error message — missing alphacore mode.

The error message still lists only 'core' or 'imagebuilder' but 'alphacore' is now a valid mode.

-    throw new Error(`Unknown mode: ${mode}. Use 'core' or 'imagebuilder'`);
+    throw new Error(`Unknown mode: ${mode}. Use 'core', 'alphacore', or 'imagebuilder'`);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/types/scripts/openapi-typescript.js` at line 52, Update the thrown Error
in openapi-typescript.js that currently reads throw new Error(`Unknown mode:
${mode}. Use 'core' or 'imagebuilder'`) to include the newly supported
'alphacore' option; locate the throw that uses the mode variable and change the
message to list 'core', 'imagebuilder', and 'alphacore' (or construct the
allowed-modes string dynamically) so the error accurately reflects valid modes.
libs/ui-components/src/components/Device/DevicesPage/useDevices.ts (1)

55-65: ⚠️ Potential issue | 🟡 Minor

Avoid building contradictory owner selectors.

If ownerFleets is set and onlyFleetless is true, the query can include both metadata.owner=... and !metadata.owner, which collapses to no results.

🔧 Proposed guard
-  if (ownerFleets?.length) {
+  if (onlyFleetless) {
+    fieldSelectors.push('!metadata.owner');
+  } else if (ownerFleets?.length) {
     queryUtils.addQueryConditions(
       fieldSelectors,
       'metadata.owner',
       ownerFleets.map((fleet) => `Fleet/${fleet}`),
     );
   }
-
-  if (onlyFleetless) {
-    fieldSelectors.push('!metadata.owner');
-  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/Device/DevicesPage/useDevices.ts` around
lines 55 - 65, The code can push contradictory owner selectors when both
ownerFleets and onlyFleetless are set; update the logic so only one wins: either
skip adding metadata.owner conditions if onlyFleetless is true, or skip the
onlyFleetless push when ownerFleets exist. Concretely, modify the block around
queryUtils.addQueryConditions (and the following onlyFleetless push) to check
onlyFleetless before adding ownerFleets (e.g., if (!onlyFleetless &&
ownerFleets?.length) { queryUtils.addQueryConditions(...); } else if
(onlyFleetless) { fieldSelectors.push('!metadata.owner'); }) so you never add
both selectors.
libs/types/package.json (1)

26-30: ⚠️ Potential issue | 🟠 Major

"alpha" is missing from the "files" array — the directory won't be included in the published package.

"imagebuilder" is explicitly listed so its source ships alongside dist, but "alpha" is absent. Any consumer of the published @flightctl/types package will get a broken ./alpha export even though it resolves fine in the monorepo.

🐛 Proposed fix
   "files": [
     "dist",
     "index.ts",
-    "imagebuilder"
+    "imagebuilder",
+    "alpha"
   ],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/types/package.json` around lines 26 - 30, The package.json "files" array
for the `@flightctl/types` package is missing the "alpha" directory so that folder
won't be included in the published package; update the "files" array in
libs/types/package.json to add "alpha" alongside "dist" and "imagebuilder" so
the ./alpha export is shipped with the package and consumers won't get a broken
import.
♻️ Duplicate comments (13)
libs/types/alpha/models/CatalogItemMeta.ts (1)

1-14: Codegen tool discrepancy already flagged in CatalogItemDeprecation.ts applies here too.

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

In `@libs/types/alpha/models/CatalogItemMeta.ts` around lines 1 - 14, This file
has the same codegen-tool discrepancy called out in CatalogItemDeprecation.ts;
apply the identical fix used there to CatalogItemMeta: update the generated type
definition for CatalogItemMeta (the ObjectMeta import and the catalog property)
to match the corrected shape applied in CatalogItemDeprecation.ts (e.g.,
optional vs required catalog, naming/typing changes), ensuring the exported
CatalogItemMeta type mirrors that corrected pattern and remove any
duplicate/incorrect generated artifacts.
libs/types/alpha/models/ApiVersion.ts (1)

1-11: Codegen tool discrepancy already flagged in CatalogItemDeprecation.ts applies here too.

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

In `@libs/types/alpha/models/ApiVersion.ts` around lines 1 - 11, The ApiVersion
enum here was generated with a different codegen configuration than other models
(same discrepancy noted for CatalogItemDeprecation); fix it by regenerating
models with the same openapi-typescript-codegen settings used for
CatalogItemDeprecation or manually align the ApiVersion enum in ApiVersion.ts so
its member names and string literals exactly match the canonical representation
used elsewhere (refer to the ApiVersion enum symbol and the
CatalogItemDeprecation usage to ensure identical identifiers/values).
libs/types/scripts/openapi-typescript.js (1)

77-96: Copy-paste comments still reference "imagebuilder" instead of "alpha".

Lines 78, 87, and 90 contain comments and variable names (imagebuilderDir) that reference "imagebuilder" but this block processes the alpha API types.

Suggested fix
   } else if (mode === ALPHA_CORE_API) {
-    // Image builder types need to be fixed before they can be moved to their final location
+    // Alpha types need to be fixed before they can be moved to their final location
     await rimraf(finalDir);
     const modelsDir = path.join(output, 'models');
     if (fs.existsSync(modelsDir)) {
@@ -87,9 +87,9 @@
     await fixCoreReferences(finalDir);

-    // Copy the generated index.ts to imagebuilder/index.ts
+    // Copy the generated index.ts to alpha/index.ts
     const indexPath = path.join(output, 'index.ts');
     if (fs.existsSync(indexPath)) {
-      const imagebuilderDir = path.resolve(__dirname, '../alpha');
-      if (!fs.existsSync(imagebuilderDir)) {
-        fs.mkdirSync(imagebuilderDir, { recursive: true });
+      const alphaDir = path.resolve(__dirname, '../alpha');
+      if (!fs.existsSync(alphaDir)) {
+        fs.mkdirSync(alphaDir, { recursive: true });
       }
-      await fsPromises.copyFile(indexPath, path.join(imagebuilderDir, 'index.ts'));
+      await fsPromises.copyFile(indexPath, path.join(alphaDir, 'index.ts'));
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/types/scripts/openapi-typescript.js` around lines 77 - 96, The block
handling ALPHA_CORE_API still uses "imagebuilder" in comments and the variable
imagebuilderDir; rename comments and variables to reference "alpha" instead
(e.g., imagebuilderDir -> alphaDir) and update the comment on copying the
generated index.ts to say "Copy the generated index.ts to alpha/index.ts";
update any related fsPromises.copyFile destination to use the new alphaDir and
ensure references to "imagebuilder" in the surrounding log messages (like the
console.log and any doc comments) are changed to "alpha" while keeping the
existing use of finalDir, modelsDir, fixCoreReferences, and indexPath intact.
apps/ocp-plugin/console-extensions.json (1)

38-47: "Software Catalog" naming — confirmed intentional.

Per prior review discussion, "Software Catalog" is the agreed naming. No action needed.

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

In `@apps/ocp-plugin/console-extensions.json` around lines 38 - 47, The "Software
Catalog" label in the console-extensions entry is intentional; no code changes
are required—leave the object with id "fctl-catalog" and the name property
"%plugin__flightctl-plugin~Software Catalog%" (and its href "/edge/catalog",
perspective "acm", section "fctl") as-is.
libs/ui-components/src/components/Catalog/CatalogPage.tsx (1)

208-208: filterIsEmpty is always false — empty-state logic is broken.

catalogFilter.itemType is an array, and !array is always false in JavaScript (arrays are truthy, even when empty). This means the "No catalog items yet" empty state is never shown; it always shows "No results found".

-  const filterIsEmpty = !catalogFilter.itemType;
+  const filterIsEmpty = catalogFilter.itemType.length === 0;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/Catalog/CatalogPage.tsx` at line 208,
filterIsEmpty is computed incorrectly because catalogFilter.itemType is an array
(which is always truthy); update the logic in CatalogPage so it checks the array
length instead of negating the array. For example, replace the current const
filterIsEmpty = !catalogFilter.itemType with a check that handles arrays safely,
e.g. use Array.isArray(catalogFilter.itemType) ? catalogFilter.itemType.length
=== 0 : !catalogFilter.itemType (referencing catalogFilter.itemType and
filterIsEmpty in the CatalogPage component).
libs/ui-components/src/components/Catalog/UpdateModal/OsUpdateModal.tsx (1)

57-57: Modal onClose is not guarded during an in-flight update.

The × button and Escape key can still dismiss the modal while handleUpdate is running, since onClose is passed unconditionally. Consider guarding it:

-<Modal isOpen onClose={onClose} variant="large">
+<Modal isOpen onClose={isUpdating ? undefined : onClose} variant="large">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/Catalog/UpdateModal/OsUpdateModal.tsx` at
line 57, The Modal's onClose is passed unconditionally which allows closing
while handleUpdate is running; guard the close action by checking the update
state (e.g., isUpdating or similar) before calling onClose: update the Modal
instantiation that currently passes onClose to only pass it (or enable closing)
when not in-flight, or alternatively wire Modal props (escape/overlay/close
button) to a flag derived from isUpdating so handleUpdate cannot be interrupted;
reference the Modal component and the handleUpdate/isUpdating state to implement
this guard.
libs/ui-components/src/components/Catalog/InstallWizard/steps/SpecificationsStep.tsx (1)

42-45: Duplicate InstallSpecFormik type — also defined in types.ts.

This duplicates the type from types.ts. Import it instead to keep a single source of truth.

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

In
`@libs/ui-components/src/components/Catalog/InstallWizard/steps/SpecificationsStep.tsx`
around lines 42 - 45, Remove the duplicate InstallSpecFormik type declaration in
SpecificationsStep.tsx and import the canonical InstallSpecFormik from the
existing types.ts instead; specifically delete the local "export type
InstallSpecFormik = { version: string; channel: string }" and add an import for
InstallSpecFormik at the top of the file, then ensure all references in
SpecificationsStep (e.g., any type annotations or props using InstallSpecFormik)
use the imported symbol.
libs/ui-components/src/components/Catalog/InstalledSoftware.tsx (1)

54-55: ⚠️ Potential issue | 🟡 Minor

Same exisingLabels typo as in AppUpdateModal.tsx.

This typo is consistent across the component tree (UpdateColumnProps, UpdateAppColumn, InstalledSoftware). Fix in tandem with AppUpdateModal.

Also applies to: 63-63

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

In `@libs/ui-components/src/components/Catalog/InstalledSoftware.tsx` around lines
54 - 55, Rename the misspelled prop/type "exisingLabels" to "existingLabels"
everywhere it appears—update the declaration in InstalledSoftware (type/props),
the UpdateColumnProps interface, the UpdateAppColumn component, and any
usages/imports (including AppUpdateModal.tsx) so names stay consistent; ensure
TypeScript types and prop references are updated together and run tests/TS
compile to catch any remaining references.
libs/ui-components/src/components/Catalog/InstallWizard/InstallOsWizard.tsx (2)

144-146: ⚠️ Potential issue | 🟠 Major

Optional chaining on metadata.name still present — prior review comment unresolved.

Lines 145–146 still use selectedDevice?.metadata.name and selectedFleet?.metadata.name. Per the prior review and coding guidelines, add a guard for the undefined case and then access .metadata.name directly.

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

In `@libs/ui-components/src/components/Catalog/InstallWizard/InstallOsWizard.tsx`
around lines 144 - 146, resourceId construction is still using optional chaining
on selectedDevice?.metadata.name and selectedFleet?.metadata.name; update the
logic so you first guard that the selected object exists then access
metadata.name directly (e.g., check installToDevice ? ensure selectedDevice is
defined and read selectedDevice.metadata.name : ensure selectedFleet is defined
and read selectedFleet.metadata.name), and handle the undefined case explicitly
(throw/error log or provide a clear fallback) so resourceId is built from a
guaranteed .metadata.name rather than using optional chaining.

150-154: ⚠️ Potential issue | 🟡 Minor

Silent return when catalogItemVersion is not found or channel is empty.

Unlike the error paths in the catch block (Line 173–175), Lines 152–154 return silently with no user feedback. The wizard stays on the review step in an indeterminate state.

💡 Proposed fix
       if (!catalogItemVersion || !values.channel) {
-        return;
+        setError(t('Version or channel is not valid'));
+        return;
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/Catalog/InstallWizard/InstallOsWizard.tsx`
around lines 150 - 154, The code currently returns silently when
catalogItemVersion is missing or values.channel is empty inside the
InstallOsWizard submission flow (check around catalogItemVersion and
values.channel), leaving the wizard stuck; update this branch to surface an
error to the user instead of returning: set a visible error state or call the
form error/notification handler (e.g., setFieldError or dispatch a toast) with a
clear message, ensure any submitting/loading flags are cleared (e.g., stop
spinner) and abort further processing; reference the catalogItemVersion check
and values.channel validation in InstallOsWizard so reviewers can find and
replace the silent return with the error/cleanup logic.
libs/ui-components/src/components/Catalog/InstallWizard/InstallAppWizard.tsx (1)

167-172: ⚠️ Potential issue | 🟠 Major

Guard against undefined device/fleet before building resourceId.

If selectedDevice or selectedFleet is undefined (e.g., due to a race or unexpected state), resourceId becomes "devices/undefined", leading to a bogus API call. Add an explicit guard, consistent with the pattern already flagged in InstallOsWizard.

💡 Proposed fix
+    if (installToDevice && !selectedDevice) {
+      return;
+    }
+    if (!installToDevice && !selectedFleet) {
+      return;
+    }
     const resourceId = installToDevice
-      ? `devices/${selectedDevice?.metadata.name}`
-      : `fleets/${selectedFleet?.metadata.name}`;
+      ? `devices/${selectedDevice.metadata.name}`
+      : `fleets/${selectedFleet.metadata.name}`;

As per coding guidelines, "Treat resource.metadata.name as always defined for resources returned from the API or created resources, and avoid optional chaining in these contexts; only handle undefined for partial or input payloads."

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

In `@libs/ui-components/src/components/Catalog/InstallWizard/InstallAppWizard.tsx`
around lines 167 - 172, The resourceId construction in InstallAppWizard uses
selectedDevice/selectedFleet without guarding for undefined, which can produce
"devices/undefined" — add an explicit guard before building resourceId: check
values.device when installToDevice is true and values.fleet when false (use the
same pattern as InstallOsWizard), and if the required resource is undefined,
return early or set an error/validation state instead of constructing the ID;
update references to installToDevice, selectedDevice, selectedFleet, and
resourceId accordingly so the API call never receives a bogus resource string.
libs/ui-components/src/components/DynamicForm/VolumeImageField.tsx (2)

304-304: ⚠️ Potential issue | 🟡 Minor

Remove the fallback from item.metadata.name.

item is API data, so metadata.name should be treated as defined here.

♻️ Proposed fix
-      assetItemName: item.metadata.name || '',
+      assetItemName: item.metadata.name,

As per coding guidelines, "Treat resource.metadata.name as always defined for resources returned from the API or created resources, and avoid optional chaining in these contexts; only handle undefined for partial or input payloads".

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

In `@libs/ui-components/src/components/DynamicForm/VolumeImageField.tsx` at line
304, In the VolumeImageField component replace the fallback usage
"item.metadata.name || ''" with the guaranteed value "item.metadata.name"
(remove the empty-string fallback and any optional chaining) so assetItemName
uses the API-provided name directly; if TypeScript complains adjust the value's
type where assetItemName is declared (or add a non-null assertion) to reflect
that resource.metadata.name is always defined.

33-33: ⚠️ Potential issue | 🟠 Major

Resolve the CatalogItemList identifier collision.

The imported CatalogItemList symbol and local const CatalogItemList collide in module scope, which is a redeclaration/lint blocker. Rename one side (or alias/type-import the model type).

♻️ Proposed fix
-import { CatalogItem, CatalogItemList, CatalogItemType, CatalogItemVersion } from '@flightctl/types/alpha';
+import { CatalogItem, CatalogItemType, CatalogItemVersion, type CatalogItemList } from '@flightctl/types/alpha';
...
-const CatalogItemList = ({
+const AssetCatalogList = ({
   assetCatalogItems,
   isLoading,
   isUpdating,
   error,
   pagination,
   onSelect,
   onClose,
   nameFilter,
   setNameFilter,
 }: CatalogItemListProps) => {
...
-        <CatalogItemList
+        <AssetCatalogList
           onSelect={setSelectedAsset}
           onClose={onClose}
           assetCatalogItems={assetCatalogItems}
#!/bin/bash
# Verify the conflicting identifier usage in this module.
rg -n -C2 --type=tsx '\bCatalogItemList\b' libs/ui-components/src/components/DynamicForm/VolumeImageField.tsx

Also applies to: 127-127

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

In `@libs/ui-components/src/components/DynamicForm/VolumeImageField.tsx` at line
33, The module imports CatalogItemList and also declares a local const named
CatalogItemList causing a redeclaration; fix by renaming one side and updating
usages — either alias the imported type (e.g., import { CatalogItemList as
CatalogItemModel } from '@flightctl/types/alpha' or import it as a type-only
import) or rename the local const (e.g., CatalogItemListOptions or
CatalogItemListComponent) and update every reference in this file (including
where the local const is used) so the identifiers no longer collide.
🟡 Minor comments (18)
libs/ui-components/src/components/Fleet/useFleets.ts-18-18 (1)

18-18: ⚠️ Potential issue | 🟡 Minor

limit is exposed but never applied to the backend request.

Line 18 adds limit?: number, but getFleetsEndpoint still always sends limit=${PAGE_SIZE} (Line 59). This makes the new API argument ineffective and misleading for callers.

Proposed fix
 const getFleetsEndpoint = ({
   name,
   addDevicesSummary,
   nextContinue,
+  limit,
 }: {
   name?: string;
   addDevicesSummary?: boolean;
   nextContinue?: string;
+  limit?: number;
 }) => {
   const params = new URLSearchParams({
-    limit: `${PAGE_SIZE}`,
+    limit: `${limit ?? PAGE_SIZE}`,
   });

Also applies to: 49-60

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

In `@libs/ui-components/src/components/Fleet/useFleets.ts` at line 18, The new
optional parameter limit on the hook is never passed to the backend: update
getFleetsEndpoint (and any other place building the fetch URL in useFleets.ts)
to use the provided limit when present (e.g., use limit ?? PAGE_SIZE) instead of
always interpolating PAGE_SIZE; ensure the query string includes limit=<value>
and retains PAGE_SIZE as the default when limit is undefined so callers who pass
limit get the requested page size.
libs/ui-components/src/components/Catalog/UpdateModal/EditOsModal.tsx-22-24 (1)

22-24: ⚠️ Potential issue | 🟡 Minor

Missing question mark in confirmation text.

The prompt reads as a question but lacks a trailing ?.

-        {t('Are you sure you want to deploy operating system {{os}}', {
+        {t('Are you sure you want to deploy operating system {{os}}?', {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/Catalog/UpdateModal/EditOsModal.tsx` around
lines 22 - 24, The confirmation string in EditOsModal is missing a trailing
question mark; update the call to t(...) inside the JSX (the template string
passed to the i18n function in EditOsModal) to include a final "?" so the
rendered prompt becomes "Are you sure you want to deploy operating system
{{os}}?" while preserving the interpolation of os: item.spec.displayName ||
item.metadata.name.
libs/ui-components/src/components/common/CodeEditor/YamlEditorBase.tsx-153-160 (1)

153-160: ⚠️ Potential issue | 🟡 Minor

Use type="button" for the Download button for semantic correctness.

Line 155 uses type="submit" for a download action. Since other action buttons (Save, Reload, Cancel) correctly use type="button" (the default), the Download button should match for consistency and semantic accuracy.

-                type="submit"
+                type="button"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/common/CodeEditor/YamlEditorBase.tsx`
around lines 153 - 160, Change the Download button in YamlEditorBase from
type="submit" to type="button" to make the action semantically correct and
consistent with the other controls; update the Button JSX that renders the
Download icon (the one with onClick={downloadYaml}) so it uses type="button"
instead of "submit" to avoid accidental form submission when invoking
downloadYaml.
libs/ui-components/src/components/Catalog/UpdateModal/EditAppModal.tsx-145-147 (1)

145-147: ⚠️ Potential issue | 🟡 Minor

Add optional chaining to guard appSpec dereference at line 145.

While appSpec is optional and the code is only reached when selectedAssets is populated (which only happens when appSpec is defined), the unsafe type assertion (appSpec as ContainerApplication).volumes creates a latent type-safety issue. Use optional chaining for clarity and robustness:

Suggested fix
-          const volumes = (appSpec as ContainerApplication).volumes;
+          const volumes = (appSpec as ContainerApplication | undefined)?.volumes;
           const volume = volumes ? volumes[volumeIdx] : undefined;
-          const imgRef = (volume as ImageVolumeProviderSpec).image?.reference;
+          const imgRef = (volume as ImageVolumeProviderSpec | undefined)?.image?.reference;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/Catalog/UpdateModal/EditAppModal.tsx`
around lines 145 - 147, The code unsafely asserts appSpec as a
ContainerApplication when reading volumes; change the access to use optional
chaining so you don't dereference undefined (e.g., read volumes via (appSpec as
ContainerApplication | undefined)?.volumes) and then propagate that safety to
subsequent uses (handle volumes possibly undefined before indexing with
volumeIdx and when computing imgRef from (volume as
ImageVolumeProviderSpec)?.image?.reference). Ensure references to appSpec,
ContainerApplication, volumes, volumeIdx, volume, and imgRef are updated so they
tolerate undefined values.
libs/i18n/locales/en/translation.json-315-315 (1)

315-315: ⚠️ Potential issue | 🟡 Minor

Missing question mark in user-facing text.

This confirmation prompt reads as a statement rather than a question.

-  "Are you sure you want to deploy operating system {{os}}": "Are you sure you want to deploy operating system {{os}}",
+  "Are you sure you want to deploy operating system {{os}}?": "Are you sure you want to deploy operating system {{os}}?",

Note: Fix this in the source code component that uses this key, then regenerate with npm run i18n.

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

In `@libs/i18n/locales/en/translation.json` at line 315, Update the user-facing
translation for the i18n key "Are you sure you want to deploy operating system
{{os}}" to include a trailing question mark in the source component that uses
this key (so the prompt reads "Are you sure you want to deploy operating system
{{os}}?"), then regenerate translation files by running npm run i18n; locate
usages by searching for the exact key string or its lookup in components that
perform OS deployment confirmation (e.g., confirmation dialog or deploy handler)
and update the source before running the i18n extraction.
libs/ui-components/src/components/common/LabelsView.tsx-14-14 (1)

14-14: ⚠️ Potential issue | 🟡 Minor

Use .startsWith() instead of .includes() for prefix matching.

CATALOG_LABEL is correctly defined as 'catalog.flightctl.io/' (a namespace prefix). However, .includes() performs substring matching, which could theoretically filter unintended labels if they contain this substring elsewhere. Use .startsWith(CATALOG_LABEL) to explicitly and robustly match the namespace prefix:

Suggested fix
- const labelItems = Object.entries(labels || {}).filter(([key]) => !key.includes(CATALOG_LABEL));
+ const labelItems = Object.entries(labels || {}).filter(([key]) => !key.startsWith(CATALOG_LABEL));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/common/LabelsView.tsx` at line 14, The
filter that builds labelItems currently uses .includes() and may match
CATALOG_LABEL as a substring anywhere; update the predicate in the labelItems
creation (the Object.entries(...).filter(([key]) => ...) expression) to use
key.startsWith(CATALOG_LABEL) instead of key.includes(CATALOG_LABEL) so only
keys prefixed with CATALOG_LABEL are excluded.
libs/ui-components/src/components/DynamicForm/DynamicForm.tsx-82-85 (1)

82-85: ⚠️ Potential issue | 🟡 Minor

onValidate won't fire until the first user interaction.

Since onValidate is only called inside onChange, the parent won't receive the initial validity state. If the form renders with invalid initial data, the parent won't know until the user makes a change. Consider firing an initial validation check after mount if consumers rely on onValidate to gate navigation (e.g., wizard "Next" button enablement).

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

In `@libs/ui-components/src/components/DynamicForm/DynamicForm.tsx` around lines
82 - 85, The component only calls onValidate inside the onChange handler so the
parent never sees initial validity; add an effect in DynamicForm that runs once
after mount to compute initial validity and call onValidate with a boolean.
Implement this by creating/using the form instance or a ref (the same form ref
used by the onChange handler) to run the form's validation method or compute
errors for the initial formData, then call onValidate(initialIsValid) so
consumers get the initial validation state even before any user interaction.
libs/ui-components/src/components/Catalog/ResourceCatalog/ResourceCatalogPage.tsx-120-124 (1)

120-124: ⚠️ Potential issue | 🟡 Minor

Throwing a translated string instead of an Error object.

throw t('Version {{version}} not found', ...) throws a raw string. Error handlers (e.g., catch (e) { e.message }) that expect an Error instance will fail to extract a message. Use new Error(...) instead.

Suggested fix
-            throw t('Version {{version}} not found', { version: installItem.version });
+            throw new Error(t('Version {{version}} not found', { version: installItem.version }));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@libs/ui-components/src/components/Catalog/ResourceCatalog/ResourceCatalogPage.tsx`
around lines 120 - 124, The onSubmit handler is throwing a raw translated string
(throw t(...)) which should be an Error object; replace the string throw with
throwing a new Error containing the translated message so downstream catch
blocks can read e.message. Update the onSubmit block where catalogItemVersion is
checked (the installItem.item.spec.versions.find(...) branch) to create and
throw new Error(t('Version {{version}} not found', { version:
installItem.version })) instead of throwing the raw string.
libs/ui-components/src/components/Catalog/InstallWizard/InstallWizard.tsx-34-47 (1)

34-47: ⚠️ Potential issue | 🟡 Minor

No fallback for unrecognized category — user sees an empty wizard area.

If catalogItem loads successfully but its spec.category is neither System nor Application (e.g., a future category type, or undefined since category is optional in CatalogItemSpec), content stays undefined and the wizard section renders empty with no feedback.

Suggested fallback
   } else if (catalogItem?.spec.category === CatalogItemCategory.CatalogItemCategoryApplication) {
     content = <InstallAppWizard catalogItem={catalogItem} />;
+  } else if (catalogItem) {
+    content = (
+      <Alert isInline variant="warning" title={t('Unsupported catalog item category')}>
+        {catalogItem.spec.category}
+      </Alert>
+    );
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/Catalog/InstallWizard/InstallWizard.tsx`
around lines 34 - 47, The current branch logic leaves content undefined when
catalogItem?.spec.category is neither
CatalogItemCategory.CatalogItemCategorySystem nor
CatalogItemCategory.CatalogItemCategoryApplication, causing an empty wizard;
update the conditional chain in InstallWizard.tsx (the content variable and the
catalogItem?.spec.category checks) to include a final fallback branch that
renders a user-facing message (for example an Alert or EmptyState) explaining
the category is unsupported or unknown, so instead of leaving content undefined
you return a clear fallback UI whenever the category is missing or unrecognized
rather than rendering nothing.
libs/ui-components/src/components/Catalog/ResourceCatalog/ResourceCatalogPage.tsx-105-105 (1)

105-105: ⚠️ Potential issue | 🟡 Minor

Typo in prop name: exisingLabels should be existingLabels.

The prop is consistently misspelled across the EditAppModal component definition and its usage. Update the prop name in EditAppModalProps, the component destructuring in EditAppModal, and the usage in ResourceCatalogPage.tsx.

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

In
`@libs/ui-components/src/components/Catalog/ResourceCatalog/ResourceCatalogPage.tsx`
at line 105, The prop name is misspelled as exisingLabels; rename it to
existingLabels across the component interface and usages: update the
EditAppModalProps type/interface to define existingLabels, change the
destructured prop in the EditAppModal component to use existingLabels, and
update every place that passes exisingLabels (including the call site in
ResourceCatalogPage where exisingLabels={currentLabels} is used) to
existingLabels={currentLabels}; also adjust any internal references, tests, and
imports that reference the old name to avoid type errors.
libs/ui-components/src/components/Catalog/InstallWizard/steps/SelectTargetStep.tsx-245-249 (1)

245-249: ⚠️ Potential issue | 🟡 Minor

sort() mutates the source array from props.

catalogItem.spec.reference.artifacts?.sort(...) sorts the array in place, which mutates the prop data. This can cause subtle issues with React reconciliation or unexpected side effects if the same catalogItem object is referenced elsewhere.

Proposed fix — spread before sorting
   const artifacts = React.useMemo(() => {
-    return catalogItem.spec.reference.artifacts?.sort((a, b) =>
+    return [...(catalogItem.spec.reference.artifacts || [])].sort((a, b) =>
       getArtifactLabel(a, t).localeCompare(getArtifactLabel(b, t)),
     );
   }, [catalogItem, t]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@libs/ui-components/src/components/Catalog/InstallWizard/steps/SelectTargetStep.tsx`
around lines 245 - 249, The current React.useMemo block creates artifacts by
calling catalogItem.spec.reference.artifacts?.sort(...), which mutates the props
array; change it to sort a shallow copy instead (e.g. clone with spread or slice
before calling sort) so that artifacts is a new sorted array without mutating
catalogItem.spec.reference.artifacts; update the React.useMemo that defines
artifacts and keep the same comparator using getArtifactLabel(a,
t).localeCompare(getArtifactLabel(b, t)).
libs/ui-components/src/components/Catalog/UpdateModal/UpdateGraph.tsx-227-252 (1)

227-252: ⚠️ Potential issue | 🟡 Minor

Stale closure: onSelectionChange is captured once and never updated.

The Visualization controller is created in a useMemo with [] deps, but the SELECTION_EVENT listener closes over onSelectionChange from the initial render. If the parent ever supplies a different callback, the old one is still invoked.

A common fix is to store the callback in a ref:

Proposed fix
+  const onSelectionChangeRef = React.useRef(onSelectionChange);
+  onSelectionChangeRef.current = onSelectionChange;
+
   const controller = React.useMemo(() => {
     const newController = new Visualization();
     newController.registerComponentFactory(customComponentFactory);
     newController.registerLayoutFactory(customLayoutFactory);
     newController.addEventListener(GRAPH_LAYOUT_END_EVENT, () => {
       newController.getGraph().fit(80);
     });
     newController.addEventListener(SELECTION_EVENT, (ids: string[]) => {
       const selectedId = ids[0];
       if (selectedId) {
         const node = newController.getNodeById(selectedId);
         if (node) {
           const data = node.getData() as VersionNodeData | undefined;
           if (data?.entryName) {
-            onSelectionChange(selectedId, data.entryName);
+            onSelectionChangeRef.current(selectedId, data.entryName);
           }
         }
       }
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/Catalog/UpdateModal/UpdateGraph.tsx` around
lines 227 - 252, The SELECTION_EVENT listener inside the Visualization
controller (created in the useMemo block that instantiates newController /
Visualization) closes over the initial onSelectionChange and never sees updates;
fix by storing onSelectionChange in a ref (e.g. selectionChangeRef) and updating
that ref in a useEffect whenever onSelectionChange changes, then update the
SELECTION_EVENT handler to call selectionChangeRef.current(selectedId,
entryName) instead of the closed-over onSelectionChange; keep the controller
creation in useMemo but reference the ref inside the listener so the latest
callback is used.
libs/ui-components/src/components/Catalog/InstallWizard/steps/SelectTargetStep.tsx-204-232 (1)

204-232: ⚠️ Potential issue | 🟡 Minor

artifactType is typed as CatalogItemArtifactType but assigned translated strings.

let artifactType: CatalogItemArtifactType; declares the variable as an enum type, but every case branch assigns a human-readable translated string (e.g., t('Bare Metal')). This is a type-safety gap that TypeScript may not catch if the enum values are plain strings. The variable should be typed as string.

-  let artifactType: CatalogItemArtifactType;
+  let artifactType: string;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@libs/ui-components/src/components/Catalog/InstallWizard/steps/SelectTargetStep.tsx`
around lines 204 - 232, The variable artifactType in SelectTargetStep.tsx is
declared with the enum type CatalogItemArtifactType but is assigned translated
human-readable strings in the switch on artifact.type; change the declaration to
a string type (or let it be inferred) so artifactType holds the t(...) strings
correctly and restore type-safety—update the line declaring artifactType and
ensure any downstream uses accept a string rather than CatalogItemArtifactType.
libs/ui-components/src/components/Catalog/InstallWizard/steps/ReviewStep.tsx-139-139 (1)

139-139: ⚠️ Potential issue | 🟡 Minor

UpdateAlerts output is not wrapped in a <StackItem>.

<Stack hasGutter> expects <StackItem> children for proper spacing. The alerts rendered by UpdateAlerts sit directly inside <Stack>, which may cause layout/gap issues.

Proposed fix
-        <UpdateAlerts catalogItem={catalogItem} />
+        <StackItem>
+          <UpdateAlerts catalogItem={catalogItem} />
+        </StackItem>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/Catalog/InstallWizard/steps/ReviewStep.tsx`
at line 139, Wrap the UpdateAlerts output in a StackItem to satisfy the Stack's
expected children and ensure consistent gutter spacing: in ReviewStep (where
<Stack hasGutter> is used) replace the direct <UpdateAlerts
catalogItem={catalogItem} /> child with a <StackItem> that contains
<UpdateAlerts catalogItem={catalogItem} /> so UpdateAlerts is a proper StackItem
child and layout/gap issues are resolved.
libs/ui-components/src/components/Catalog/InstalledSoftware.tsx-348-365 (1)

348-365: ⚠️ Potential issue | 🟡 Minor

Empty object {} pushed into actions array when itemVersion is falsy.

On Line 360, when there's no matching version, an empty {} is added to the actions list. PatternFly's ActionsColumn may render this as a blank/non-functional menu item. Use a separator or filter it out.

💡 Proposed fix — filter out empty actions
-                  const actions: IAction[] = [
-                    itemVersion
-                      ? {
-                          title: t('Edit'),
-                          onClick: () =>
-                            setAppToEdit({
-                              item: app.item,
-                              name: app.name,
-                              channel: appChannel,
-                              version: itemVersion,
-                            }),
-                        }
-                      : {},
-                    {
-                      title: t('Delete'),
-                      onClick: () => setAppToDelete(app.name),
-                    },
-                  ];
+                  const actions: IAction[] = [
+                    ...(itemVersion
+                      ? [
+                          {
+                            title: t('Edit'),
+                            onClick: () =>
+                              setAppToEdit({
+                                item: app.item,
+                                name: app.name,
+                                channel: appChannel,
+                                version: itemVersion,
+                              }),
+                          },
+                        ]
+                      : []),
+                    {
+                      title: t('Delete'),
+                      onClick: () => setAppToDelete(app.name),
+                    },
+                  ];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/Catalog/InstalledSoftware.tsx` around lines
348 - 365, The actions array in InstalledSoftware.tsx currently pushes an empty
object when itemVersion is falsy, producing a blank ActionsColumn entry; update
the construction of actions (the variable named actions used for ActionsColumn)
to only include the Edit action when itemVersion exists—either by conditionally
pushing the Edit action (calling setAppToEdit) or by building the array and
filtering out falsy/empty entries (e.g., filter(Boolean)) so no {} ends up in
the array; ensure the Delete action (setAppToDelete) remains present and that
references to setAppToEdit and setAppToDelete are unchanged.
libs/ui-components/src/components/Catalog/InstalledSoftware.tsx-249-269 (1)

249-269: ⚠️ Potential issue | 🟡 Minor

Loading state not reset when apps changes.

appsLoading starts as true but is only set to false — never back to true when the apps dependency changes. If labels or spec.applications update after the first load, the UI will show stale appItems until the new fetch completes.

💡 Proposed fix
   React.useEffect(() => {
+    setAppsLoading(true);
     (async () => {
       const appRequests = apps.map((app) => get<CatalogItem>(`catalogs/${app.catalog}/items/${app.item}`));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/Catalog/InstalledSoftware.tsx` around lines
249 - 269, In InstalledSoftware's useEffect (the async IIFE that fetches catalog
items), ensure the loading state and displayed items are reset when the apps
dependency changes by calling setAppsLoading(true) (and optionally
setAppItems([])) before starting the Promise.allSettled fetch; this guarantees
the UI shows a loading state and avoids stale appItems while get/CatalogItem
requests complete.
libs/ui-components/src/components/Catalog/UpdateModal/AppUpdateModal.tsx-185-196 (1)

185-196: ⚠️ Potential issue | 🟡 Minor

Typo: exisingLabelsexistingLabels

The prop name is misspelled here and propagated through the component tree (also visible in InstalledSoftware.tsx). Consider fixing it now to avoid a confusing public API surface.

🔤 Proposed fix
 type AppUpdateModalProps = {
   catalogItem: CatalogItem;
   onClose: VoidFunction;
   currentVersion: CatalogItemVersion;
   updates: CatalogItemVersion[];
   onUpdate: (catalogItemVersion: CatalogItemVersion, channel: string, values: AppUpdateFormik) => Promise<void>;
   currentChannel: string;
   appSpec: ApplicationProviderSpec;
-  exisingLabels: Record<string, string> | undefined;
+  existingLabels: Record<string, string> | undefined;
 };
 
 const AppUpdateModal: React.FC<AppUpdateModalProps> = ({
   catalogItem,
   onClose,
   currentVersion,
   onUpdate,
   currentChannel,
   updates,
   appSpec,
-  exisingLabels,
+  existingLabels,
 }) => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/Catalog/UpdateModal/AppUpdateModal.tsx`
around lines 185 - 196, The prop name exisingLabels is misspelled; rename it to
existingLabels across the component API and usages: update the
AppUpdateModalProps type, the AppUpdateModal function parameter list (prop
destructuring), and any references/props passed from callers such as
InstalledSoftware.tsx to use existingLabels instead of exisingLabels, and update
any downstream variable names/usages inside AppUpdateModal to match the
corrected prop name.
libs/ui-components/src/components/Catalog/utils.ts-179-180 (1)

179-180: ⚠️ Potential issue | 🟡 Minor

Drop fallback handling for catalogItem.metadata.name in label values.

These are API-backed resources, so metadata.name should be used directly.

♻️ Proposed fix
-    [OS_ITEM_LABEL_KEY]: catalogItem.metadata.name || '',
+    [OS_ITEM_LABEL_KEY]: catalogItem.metadata.name,
...
-    [`${appSpec.name}.${APP_ITEM_LABEL_KEY}`]: catalogItem.metadata.name || '',
+    [`${appSpec.name}.${APP_ITEM_LABEL_KEY}`]: catalogItem.metadata.name,

As per coding guidelines, "Treat resource.metadata.name as always defined for resources returned from the API or created resources, and avoid optional chaining in these contexts; only handle undefined for partial or input payloads".

Also applies to: 281-282

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

In `@libs/ui-components/src/components/Catalog/utils.ts` around lines 179 - 180,
The label generation is using fallbacks/optional chaining for metadata.name;
update occurrences that set [OS_ITEM_LABEL_KEY] (and the second occurrence
around the 281-282 area) to use catalogItem.metadata.name directly (remove the
optional chaining and the || '' fallback) so the code references the API-backed
resource name without defensive defaults.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a79e591 and 13da7df.

⛔ Files ignored due to path filters (2)
  • libs/ui-components/assets/flight-control-logo.png is excluded by !**/*.png
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (103)
  • apps/ocp-plugin/console-extensions.json
  • apps/ocp-plugin/package.json
  • apps/ocp-plugin/src/components/AppContext/AppContext.tsx
  • apps/ocp-plugin/src/components/Catalog/CatalogInstallWizard.tsx
  • apps/ocp-plugin/src/components/Catalog/CatalogPage.tsx
  • apps/ocp-plugin/src/typings.d.ts
  • apps/ocp-plugin/src/utils/apiCalls.ts
  • apps/ocp-plugin/tsconfig.json
  • apps/ocp-plugin/webpack.config.ts
  • apps/standalone/package.json
  • apps/standalone/src/app/routes.tsx
  • apps/standalone/src/app/utils/apiCalls.ts
  • apps/standalone/tsconfig.json
  • apps/standalone/webpack.config.ts
  • libs/i18n/locales/en/translation.json
  • libs/types/alpha/index.ts
  • libs/types/alpha/models/ApiVersion.ts
  • libs/types/alpha/models/Catalog.ts
  • libs/types/alpha/models/CatalogItem.ts
  • libs/types/alpha/models/CatalogItemArtifact.ts
  • libs/types/alpha/models/CatalogItemArtifactType.ts
  • libs/types/alpha/models/CatalogItemCategory.ts
  • libs/types/alpha/models/CatalogItemConfigurable.ts
  • libs/types/alpha/models/CatalogItemDeprecation.ts
  • libs/types/alpha/models/CatalogItemList.ts
  • libs/types/alpha/models/CatalogItemMeta.ts
  • libs/types/alpha/models/CatalogItemReference.ts
  • libs/types/alpha/models/CatalogItemSpec.ts
  • libs/types/alpha/models/CatalogItemType.ts
  • libs/types/alpha/models/CatalogItemVersion.ts
  • libs/types/alpha/models/CatalogItemVisibility.ts
  • libs/types/alpha/models/CatalogList.ts
  • libs/types/alpha/models/CatalogSpec.ts
  • libs/types/alpha/models/CatalogStatus.ts
  • libs/types/alpha/models/Status.ts
  • libs/types/package.json
  • libs/types/scripts/openapi-typescript.js
  • libs/types/scripts/openapi-utils.js
  • libs/ui-components/package.json
  • libs/ui-components/src/components/Catalog/CatalogItemCard.tsx
  • libs/ui-components/src/components/Catalog/CatalogItemDetails.css
  • libs/ui-components/src/components/Catalog/CatalogItemDetails.tsx
  • libs/ui-components/src/components/Catalog/CatalogPage.css
  • libs/ui-components/src/components/Catalog/CatalogPage.tsx
  • libs/ui-components/src/components/Catalog/CatalogPageToolbar.tsx
  • libs/ui-components/src/components/Catalog/InstallWizard/InstallAppWizard.tsx
  • libs/ui-components/src/components/Catalog/InstallWizard/InstallOsWizard.tsx
  • libs/ui-components/src/components/Catalog/InstallWizard/InstallWizard.tsx
  • libs/ui-components/src/components/Catalog/InstallWizard/UpdateSuccessPage.tsx
  • libs/ui-components/src/components/Catalog/InstallWizard/steps/AppConfigStep.css
  • libs/ui-components/src/components/Catalog/InstallWizard/steps/AppConfigStep.tsx
  • libs/ui-components/src/components/Catalog/InstallWizard/steps/ReviewStep.tsx
  • libs/ui-components/src/components/Catalog/InstallWizard/steps/SelectTargetStep.tsx
  • libs/ui-components/src/components/Catalog/InstallWizard/steps/SpecificationsStep.tsx
  • libs/ui-components/src/components/Catalog/InstallWizard/types.ts
  • libs/ui-components/src/components/Catalog/InstallWizard/utils.ts
  • libs/ui-components/src/components/Catalog/InstalledSoftware.tsx
  • libs/ui-components/src/components/Catalog/ResourceCatalog/ResourceCatalogPage.css
  • libs/ui-components/src/components/Catalog/ResourceCatalog/ResourceCatalogPage.tsx
  • libs/ui-components/src/components/Catalog/UpdateModal/AppUpdateModal.tsx
  • libs/ui-components/src/components/Catalog/UpdateModal/EditAppModal.tsx
  • libs/ui-components/src/components/Catalog/UpdateModal/EditOsModal.tsx
  • libs/ui-components/src/components/Catalog/UpdateModal/OsUpdateModal.tsx
  • libs/ui-components/src/components/Catalog/UpdateModal/UpdateGraph.tsx
  • libs/ui-components/src/components/Catalog/const.ts
  • libs/ui-components/src/components/Catalog/useCatalogFilter.ts
  • libs/ui-components/src/components/Catalog/useCatalogs.ts
  • libs/ui-components/src/components/Catalog/useSubmitCatalogForm.ts
  • libs/ui-components/src/components/Catalog/utils.ts
  • libs/ui-components/src/components/Device/DeviceDetails/DeviceDetailsCatalog.tsx
  • libs/ui-components/src/components/Device/DeviceDetails/DeviceDetailsPage.tsx
  • libs/ui-components/src/components/Device/DevicesPage/DevicesPage.tsx
  • libs/ui-components/src/components/Device/DevicesPage/EnrolledDeviceTableRow.tsx
  • libs/ui-components/src/components/Device/DevicesPage/EnrolledDevicesTable.tsx
  • libs/ui-components/src/components/Device/DevicesPage/useDevices.ts
  • libs/ui-components/src/components/Device/EditDeviceWizard/EditDeviceWizard.tsx
  • libs/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationTemplates.tsx
  • libs/ui-components/src/components/Device/EditDeviceWizard/steps/DeviceTemplateStep.tsx
  • libs/ui-components/src/components/DynamicForm/DynamicForm.tsx
  • libs/ui-components/src/components/DynamicForm/FieldErrors.tsx
  • libs/ui-components/src/components/DynamicForm/FieldTemplate.tsx
  • libs/ui-components/src/components/DynamicForm/FormWidget.tsx
  • libs/ui-components/src/components/DynamicForm/VolumeImageField.tsx
  • libs/ui-components/src/components/Fleet/CreateFleet/CreateFleetWizard.tsx
  • libs/ui-components/src/components/Fleet/FleetDetails/FleetDetailsCatalog.tsx
  • libs/ui-components/src/components/Fleet/FleetDetails/FleetDetailsPage.tsx
  • libs/ui-components/src/components/Fleet/FleetRow.tsx
  • libs/ui-components/src/components/Fleet/FleetsPage.tsx
  • libs/ui-components/src/components/Fleet/useFleets.ts
  • libs/ui-components/src/components/Table/Table.tsx
  • libs/ui-components/src/components/common/CodeEditor/YamlEditor.tsx
  • libs/ui-components/src/components/common/CodeEditor/YamlEditorBase.tsx
  • libs/ui-components/src/components/common/FlightCtlWizardFooter.tsx
  • libs/ui-components/src/components/common/LabelsView.tsx
  • libs/ui-components/src/components/common/PageNavigation.tsx
  • libs/ui-components/src/components/common/WithTooltip.tsx
  • libs/ui-components/src/components/form/FormSelect.tsx
  • libs/ui-components/src/components/form/LabelsField.tsx
  • libs/ui-components/src/constants.ts
  • libs/ui-components/src/hooks/useAppContext.tsx
  • libs/ui-components/src/hooks/useNavigate.tsx
  • libs/ui-components/src/utils/api.ts
  • libs/ui-components/tsconfig.json

CatalogItemTypeDriver = 'driver',
CatalogItemTypeContainer = 'container',
CatalogItemTypeHelm = 'helm',
CatalogItemTypeQuadlet = 'quadlet',
Copy link

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:

rg -rn "CatalogItemTypeQuadlet|quadlet" --type=tsx --type=ts -B 3 -A 3 libs/ui-components/src/ || echo "No matches found in libs/ui-components/src"

Repository: flightctl/flightctl-ui

Length of output: 153


🏁 Script executed:

# Also check other UI-related directories
fd -e ts -e tsx libs/ -path "*/src/*" | head -20

Repository: flightctl/flightctl-ui

Length of output: 234


🏁 Script executed:

# Check if there are any feature flags or guards related to quadlet
rg -rn "quadlet|Quadlet" --type=tsx --type=ts -B 2 -A 2 libs/ | head -50

Repository: flightctl/flightctl-ui

Length of output: 93


🏁 Script executed:

# Search for any usage of quadlet/Quadlet in the entire repository
rg -i "quadlettype|catalogitemtype.*quadlet" --type ts -B 2 -A 2 || echo "No matches found"

Repository: flightctl/flightctl-ui

Length of output: 2449


🏁 Script executed:

# List files in libs to understand structure
ls -la libs/

Repository: flightctl/flightctl-ui

Length of output: 399


🏁 Script executed:

# Check if CatalogItemTypeQuadlet is referenced anywhere in the codebase
rg "CatalogItemTypeQuadlet" -B 3 -A 3

Repository: flightctl/flightctl-ui

Length of output: 3222


🏁 Script executed:

# Check CatalogPage.tsx for any feature flags around quadlet
rg -B 10 -A 10 "CatalogItemType.CatalogItemTypeQuadlet" libs/ui-components/src/components/Catalog/CatalogPage.tsx

Repository: flightctl/flightctl-ui

Length of output: 785


🏁 Script executed:

# Check useCatalogs.ts for any feature flags
rg -B 10 -A 10 "appTypeIds" libs/ui-components/src/components/Catalog/useCatalogs.ts

Repository: flightctl/flightctl-ui

Length of output: 1597


🏁 Script executed:

# Search for any feature flags or environment variables that might gate quadlet
rg "feature|flag|FEATURE|FLAG|experimental|EXPERIMENTAL" libs/ui-components/src/components/Catalog/ | grep -i quadlet || echo "No feature flags found for quadlet"

Repository: flightctl/flightctl-ui

Length of output: 100


Remove or guard CatalogItemTypeQuadlet from the UI until backend support is available.

The enum value itself is acceptable in the generated types file, but verification shows quadlet is fully exposed in the UI without any feature flags or guards:

  • CatalogPage.tsx: Quadlet appears in the catalog filter UI with full translation
  • useCatalogs.ts: Included in appTypeIds array
  • utils.ts: Mapped to AppType.AppTypeQuadlet

Per PR #371 learnings, quadlet support is preparatory only and full UI implementation should be deferred until backend support ships. Either disable the quadlet filter option or add a feature flag to prevent users from selecting unsupported catalog items.

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

In `@libs/types/alpha/models/CatalogItemType.ts` at line 14,
CatalogItemTypeQuadlet is exposed in the UI but backend doesn't support it;
remove or guard its usage so users can't select it. Update CatalogPage.tsx to
hide/remove the quadlet option from the catalog filter UI (and its translation
key), update useCatalogs.ts to remove CatalogItemTypeQuadlet from the appTypeIds
array, and update utils.ts to stop mapping CatalogItemTypeQuadlet to
AppType.AppTypeQuadlet (or wrap all three places with a feature flag check e.g.,
isQuadletEnabled()). Ensure the same guard or flag is used consistently across
CatalogPage.tsx, useCatalogs.ts, and utils.ts so quadlet remains available only
when backend support is present.

Comment on lines +257 to +274
export const getDefaultChannelAndVersion = (item: CatalogItem) => {
if (!item.spec.versions.length) {
return {
version: '',
channel: '',
};
}

const versions = item.spec.versions.sort((v1, v2) => semver.rcompare(v1.version, v2.version));

// release then prerelease
const latestVersion = versions.find((v) => !semver.prerelease(v.version)) || versions[0];

return {
version: latestVersion.version,
channel: latestVersion.channels[0],
};
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Array.sort() mutates item.spec.versions in place.

item.spec.versions.sort(...) sorts the original array, which could cause unexpected side effects if other code relies on the original version order. Create a shallow copy first.

🐛 Proposed fix
 export const getDefaultChannelAndVersion = (item: CatalogItem) => {
   if (!item.spec.versions.length) {
     return {
       version: '',
       channel: '',
     };
   }
 
-  const versions = item.spec.versions.sort((v1, v2) => semver.rcompare(v1.version, v2.version));
+  const versions = [...item.spec.versions].sort((v1, v2) => semver.rcompare(v1.version, v2.version));
 
   // release then prerelease
   const latestVersion = versions.find((v) => !semver.prerelease(v.version)) || versions[0];
📝 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
export const getDefaultChannelAndVersion = (item: CatalogItem) => {
if (!item.spec.versions.length) {
return {
version: '',
channel: '',
};
}
const versions = item.spec.versions.sort((v1, v2) => semver.rcompare(v1.version, v2.version));
// release then prerelease
const latestVersion = versions.find((v) => !semver.prerelease(v.version)) || versions[0];
return {
version: latestVersion.version,
channel: latestVersion.channels[0],
};
};
export const getDefaultChannelAndVersion = (item: CatalogItem) => {
if (!item.spec.versions.length) {
return {
version: '',
channel: '',
};
}
const versions = [...item.spec.versions].sort((v1, v2) => semver.rcompare(v1.version, v2.version));
// release then prerelease
const latestVersion = versions.find((v) => !semver.prerelease(v.version)) || versions[0];
return {
version: latestVersion.version,
channel: latestVersion.channels[0],
};
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/Catalog/CatalogItemDetails.tsx` around
lines 257 - 274, The function getDefaultChannelAndVersion currently calls
item.spec.versions.sort(...), which mutates the original array; create a shallow
copy (e.g., via [...item.spec.versions] or item.spec.versions.slice()) before
sorting so you don't mutate item.spec.versions, then perform the semver.rcompare
sort and the existing prerelease selection logic on that copied array to return
version and channel.

Comment on lines +163 to +201
const { onSubmit, error, schemaErrors, setError } = useSubmitCatalogForm<InstallAppFormik>(async (values) => {
if (values.target !== 'fleet' && values.target !== 'device') {
return;
}
const selectedDevice = values.device;
const selectedFleet = values.fleet;
const installToDevice = values.target === 'device';
const resourceId = installToDevice
? `devices/${selectedDevice?.metadata.name}`
: `fleets/${selectedFleet?.metadata.name}`;

const res = await get<Device | Fleet>(resourceId);
const currentLabels = res?.metadata.labels;
const specPath = installToDevice ? '/' : '/spec/template/';
const catalogItemVersion = catalogItem.spec.versions.find((v) => v.version === values.version);

if (!catalogItemVersion || !values.channel) {
return;
}
const currentApps = installToDevice
? (res as Device)?.spec?.applications
: (res as Fleet)?.spec.template.spec.applications;
const allPatches = getAppPatches({
appName: values.appName,
currentApps,
currentLabels,
catalogItem,
catalogItemVersion,
channel: values.channel,
formValues:
values.configureVia === 'editor' ? (load(values.editorContent) as Record<string, unknown>) : values.formValues,
specPath,
selectedAssets: values.configureVia === 'form' ? values.selectedAssets : [],
});
if (!allPatches.length) {
return;
}
await patch(resourceId, allPatches);
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Silent returns on Lines 179–181 and 197–199 leave the user with no feedback.

When catalogItemVersion is not found or allPatches is empty, the submit handler returns silently — the wizard stays on the review step with no success or error indication. Consider either calling setError(...) or marking as successful (for the no-patches case, similar to how InstallOsWizard calls setIsSuccessful(true)).

💡 Proposed fix
     if (!catalogItemVersion || !values.channel) {
+      throw t('Version {{version}} not found', { version: values.version });
-      return;
     }
     ...
     if (!allPatches.length) {
-      return;
+      // Nothing to change – treat as success
+      return;  // onSubmit wrapper in useSubmitCatalogForm returns truthy → setIsSuccessful(true) in the caller
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/Catalog/InstallWizard/InstallAppWizard.tsx`
around lines 163 - 201, Inside the onSubmit callback passed to
useSubmitCatalogForm in InstallAppWizard, avoid silent returns: when
catalogItemVersion is missing (the branch after checking if (!catalogItemVersion
|| !values.channel)) call setError(...) with a clear message (e.g., "Selected
version or channel not found") so the user sees feedback; and when allPatches is
empty (the if (!allPatches.length) branch) mark the wizard as successful instead
of returning silently—mirror the InstallOsWizard approach by invoking the
success flag (e.g., call setIsSuccessful(true) if available from the form/wizard
hook) or fallback to setError("No changes required for this install") so users
get explicit feedback. Ensure these changes are made inside the same anonymous
async function passed to useSubmitCatalogForm so the UI receives the appropriate
success/error state.

return acc;
}, {});

const versions = catalogItem.spec.versions.sort((v1, v2) => semver.compare(v2.version, v1.version));
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Array.sort() mutates catalogItem.spec.versions in place — same issue as CatalogItemDetails.tsx.

This can cause subtle bugs if any parent or sibling component relies on the original version order.

🐛 Proposed fix
-  const versions = catalogItem.spec.versions.sort((v1, v2) => semver.compare(v2.version, v1.version));
+  const versions = [...catalogItem.spec.versions].sort((v1, v2) => semver.compare(v2.version, v1.version));
📝 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 versions = catalogItem.spec.versions.sort((v1, v2) => semver.compare(v2.version, v1.version));
const versions = [...catalogItem.spec.versions].sort((v1, v2) => semver.compare(v2.version, v1.version));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@libs/ui-components/src/components/Catalog/InstallWizard/steps/SpecificationsStep.tsx`
at line 63, catalogItem.spec.versions is being mutated by calling .sort()
directly; create a non-mutating copy before sorting (e.g., use spread or slice)
so the original array isn't changed, and assign the sorted result to the
existing versions variable used in SpecificationsStep.tsx (the line declaring
const versions and using semver.compare) to mirror the same fix applied in
CatalogItemDetails.tsx.

Comment on lines +264 to +268
if (!volumes || volumes.length <= volumeIndex) {
return acc;
}
const volumeName = volumes[volumeIndex].name;

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard negative volumeIndex before dereferencing volumes[volumeIndex].

The current check only handles upper bounds; a negative index slips through and can crash on .name.

♻️ Proposed fix
-    if (!volumes || volumes.length <= volumeIndex) {
+    if (!volumes || volumeIndex < 0 || volumeIndex >= volumes.length) {
       return acc;
     }
📝 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
if (!volumes || volumes.length <= volumeIndex) {
return acc;
}
const volumeName = volumes[volumeIndex].name;
if (!volumes || volumeIndex < 0 || volumeIndex >= volumes.length) {
return acc;
}
const volumeName = volumes[volumeIndex].name;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/Catalog/utils.ts` around lines 264 - 268,
The code dereferences volumes[volumeIndex] to get volumeName without guarding
against negative indexes; update the guard in the block that checks volumes and
volumeIndex so it returns acc when volumeIndex is negative or out of range
(e.g., if (!volumes || volumeIndex < 0 || volumes.length <= volumeIndex) return
acc), ensuring you validate volumeIndex before computing volumeName to avoid
crashes in utils.ts where volumeIndex, volumes, and volumeName are used.

Comment on lines +103 to +107
{!hideActions && (
<Td isActionCell>
<ActionsColumn items={actions} />
</Td>
)}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Keep table column structure consistent when actions are hidden.

Line 103 conditionally removes the action <Td>, but the table header still reserves an action column. That can create header/body column mismatch and visual/accessibility misalignment.

💡 Suggested fix
-      {!hideActions && (
-        <Td isActionCell>
-          <ActionsColumn items={actions} />
-        </Td>
-      )}
+      {hideActions ? (
+        <Td />
+      ) : (
+        <Td isActionCell>
+          <ActionsColumn items={actions} />
+        </Td>
+      )}
📝 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
{!hideActions && (
<Td isActionCell>
<ActionsColumn items={actions} />
</Td>
)}
{hideActions ? (
<Td />
) : (
<Td isActionCell>
<ActionsColumn items={actions} />
</Td>
)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/Fleet/FleetRow.tsx` around lines 103 - 107,
The conditional removal of the action <Td> in the FleetRow component causes
header/body column mismatch when hideActions is true; instead of omitting the
cell, always render the action column to preserve table structure—when
hideActions is true render an empty placeholder <Td isActionCell> (or a visually
hidden/aria-hidden placeholder) in the same spot as the <ActionsColumn
items={actions}> so header alignment and accessibility remain correct while
hiding the interactive controls.

Copy link

@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: 2

♻️ Duplicate comments (2)
libs/ui-components/src/components/DynamicForm/VolumeImageField.tsx (1)

303-304: Drop the || '' fallback on item.metadata.name.

item is a CatalogItem returned from the API, so metadata.name is always defined. The fallback contradicts the project guideline and was flagged in a prior review.

Proposed fix
-      assetItemName: item.metadata.name || '',
+      assetItemName: item.metadata.name,

As per coding guidelines, "Treat resource.metadata.name as always defined for resources returned from the API or created resources, and avoid optional chaining in these contexts."

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

In `@libs/ui-components/src/components/DynamicForm/VolumeImageField.tsx` around
lines 303 - 304, Remove the unnecessary empty-string fallback on
item.metadata.name inside the VolumeImageField component: replace the expression
assetItemName: item.metadata.name || '' with assetItemName: item.metadata.name
(leave assetCatalog: item.metadata.catalog unchanged) so metadata.name is
treated as always defined per project guidelines.
libs/ui-components/src/components/Catalog/useCatalogs.ts (1)

45-54: Operator precedence fix confirmed — LGTM.

The previously flagged categories?.length || 0 > 1 and types.length || 0 > 1 bugs have been corrected to categories.length > 1 and types.length > 1.

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

In `@libs/ui-components/src/components/Catalog/useCatalogs.ts` around lines 45 -
54, In useCatalogs.ts the query assembly uses categories.length and types.length
correctly but the types join still uses an unnecessary optional chain; update
the spec.type multi-value branch to use types.join(',') (remove the `?.`) to
match the categories branch and ensure consistency for parts.push calls
(`parts.push(\`spec.type in (${types.join(',')})\`)`); confirm both branches for
category and type produce the same CSV formatting and that parts is used
downstream unchanged.
🧹 Nitpick comments (3)
libs/ui-components/src/components/DynamicForm/VolumeImageField.tsx (1)

219-220: Consider adding item to the dependency array instead of suppressing the lint rule.

The eslint-disable is safe here because item doesn't change while CatalogItemDetails is mounted (it unmounts on "back"). However, including item in the deps would make the intent explicit and remove the need for the suppression comment, with no behavioral change.

-  // eslint-disable-next-line react-hooks/exhaustive-deps
-  const initialValues = React.useMemo(() => getDefaultChannelAndVersion(item), []);
+  const initialValues = React.useMemo(() => getDefaultChannelAndVersion(item), [item]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/DynamicForm/VolumeImageField.tsx` around
lines 219 - 220, Replace the eslint-disable hack by including item in the
dependency array of the React.useMemo that computes initialValues: change the
React.useMemo call that invokes getDefaultChannelAndVersion(item) so its deps
array is [item] (and remove the "// eslint-disable-next-line
react-hooks/exhaustive-deps" comment); this makes the dependency explicit while
preserving behavior since item is stable while CatalogItemDetails is mounted.
libs/ui-components/src/components/Catalog/useCatalogs.ts (1)

40-53: categories.length > 1 branch is unreachable; optional chaining on types is redundant.

categories is only ever appended to at line 41 (one push site, one possible value: CatalogItemCategoryApplication), so its length is always 0 or 1 — the else if (categories.length > 1) on line 47 is dead code. Separately, types is always a defined array (initialized as [] on line 38), so the ?. on types?.join(',') at line 53 is unnecessary.

♻️ Proposed cleanup
-    } else if (categories.length > 1) {
-      parts.push(`spec.category in (${categories.join(',')})`);
-    }
+    }
     if (types.length === 1) {
       parts.push(`spec.type==${types[0]}`);
     } else if (types.length > 1) {
-      parts.push(`spec.type in (${types?.join(',')})`);
+      parts.push(`spec.type in (${types.join(',')})`);
     }

If the intent is to future-proof for additional category values, the branch should be kept but the types?.join still cleaned up.

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

In `@libs/ui-components/src/components/Catalog/useCatalogs.ts` around lines 40 -
53, categories can never exceed length 1 because the only push is
CatalogItemCategory.CatalogItemCategoryApplication, so remove the unreachable
else-if branch checking categories.length > 1 (or keep it only if you
intentionally plan to add more category push sites) and simplify the types join
by removing the redundant optional chaining (replace types?.join(',') with
types.join(',')); update the block around variables categories, types and parts
(the spec.category/spec.type push logic) accordingly so you either keep a single
categories.length === 1 branch or explicitly leave a placeholder comment for
future multi-category support, and always use types.join(',') when constructing
the spec.type in-clause.
libs/ui-components/src/components/Catalog/InstallWizard/utils.ts (1)

26-31: Avoid duplicate version lookup in initial config derivation.

Line 27 and Line 30 run the same find call; caching the matched version once improves clarity and avoids drift between schema/config selection logic.

♻️ Suggested cleanup
-  const configSchema =
-    catalogItem.spec.versions.find((v) => v.version === version)?.configSchema ??
-    catalogItem?.spec.defaults?.configSchema;
-  let defaultConfig =
-    catalogItem.spec.versions.find((v) => v.version === version)?.config ?? catalogItem?.spec.defaults?.config;
+  const selectedVersion = catalogItem.spec.versions.find((v) => v.version === version);
+  const configSchema = selectedVersion?.configSchema ?? catalogItem.spec.defaults?.configSchema;
+  let defaultConfig = selectedVersion?.config ?? catalogItem.spec.defaults?.config;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/Catalog/InstallWizard/utils.ts` around
lines 26 - 31, The duplicated find call should be replaced by caching the
matched version first: compute a local const (e.g., matchedVersion) by calling
catalogItem.spec.versions.find(v => v.version === version), then derive
configSchema from matchedVersion?.configSchema ??
catalogItem?.spec.defaults?.configSchema and defaultConfig from
matchedVersion?.config ?? catalogItem?.spec.defaults?.config; update the
variables configSchema and defaultConfig in utils.ts to use this cached
matchedVersion to avoid repeating the lookup and ensure consistent selection.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@libs/i18n/locales/en/translation.json`:
- Around line 39-41: Revert the manual changes to translation.json and instead
add or fix the corresponding source-level i18n keys by using the t(...) calls
where the UI strings live (e.g., add t("Software Catalog"), t("Catalog"),
t("Install Catalog item") in the appropriate components), then run the
extraction/regeneration script (npm run i18n) to regenerate translation.json and
commit the regenerated file(s).

In
`@libs/ui-components/src/components/Catalog/InstallWizard/InstallAppWizard.tsx`:
- Around line 170-172: The resourceId construction uses optional chaining
causing possible "devices/undefined" or "fleets/undefined"; modify the logic
around installToDevice to explicitly guard that selectedDevice or selectedFleet
exists (e.g., throw or return early if missing) and then access metadata.name
directly (replace selectedDevice?.metadata.name / selectedFleet?.metadata.name
with selectedDevice.metadata.name / selectedFleet.metadata.name after the
explicit existence check) so resourceId is always built from a defined resource
name.

---

Duplicate comments:
In `@libs/ui-components/src/components/Catalog/useCatalogs.ts`:
- Around line 45-54: In useCatalogs.ts the query assembly uses categories.length
and types.length correctly but the types join still uses an unnecessary optional
chain; update the spec.type multi-value branch to use types.join(',') (remove
the `?.`) to match the categories branch and ensure consistency for parts.push
calls (`parts.push(\`spec.type in (${types.join(',')})\`)`); confirm both
branches for category and type produce the same CSV formatting and that parts is
used downstream unchanged.

In `@libs/ui-components/src/components/DynamicForm/VolumeImageField.tsx`:
- Around line 303-304: Remove the unnecessary empty-string fallback on
item.metadata.name inside the VolumeImageField component: replace the expression
assetItemName: item.metadata.name || '' with assetItemName: item.metadata.name
(leave assetCatalog: item.metadata.catalog unchanged) so metadata.name is
treated as always defined per project guidelines.

---

Nitpick comments:
In `@libs/ui-components/src/components/Catalog/InstallWizard/utils.ts`:
- Around line 26-31: The duplicated find call should be replaced by caching the
matched version first: compute a local const (e.g., matchedVersion) by calling
catalogItem.spec.versions.find(v => v.version === version), then derive
configSchema from matchedVersion?.configSchema ??
catalogItem?.spec.defaults?.configSchema and defaultConfig from
matchedVersion?.config ?? catalogItem?.spec.defaults?.config; update the
variables configSchema and defaultConfig in utils.ts to use this cached
matchedVersion to avoid repeating the lookup and ensure consistent selection.

In `@libs/ui-components/src/components/Catalog/useCatalogs.ts`:
- Around line 40-53: categories can never exceed length 1 because the only push
is CatalogItemCategory.CatalogItemCategoryApplication, so remove the unreachable
else-if branch checking categories.length > 1 (or keep it only if you
intentionally plan to add more category push sites) and simplify the types join
by removing the redundant optional chaining (replace types?.join(',') with
types.join(',')); update the block around variables categories, types and parts
(the spec.category/spec.type push logic) accordingly so you either keep a single
categories.length === 1 branch or explicitly leave a placeholder comment for
future multi-category support, and always use types.join(',') when constructing
the spec.type in-clause.

In `@libs/ui-components/src/components/DynamicForm/VolumeImageField.tsx`:
- Around line 219-220: Replace the eslint-disable hack by including item in the
dependency array of the React.useMemo that computes initialValues: change the
React.useMemo call that invokes getDefaultChannelAndVersion(item) so its deps
array is [item] (and remove the "// eslint-disable-next-line
react-hooks/exhaustive-deps" comment); this makes the dependency explicit while
preserving behavior since item is stable while CatalogItemDetails is mounted.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 13da7df and 407751a.

📒 Files selected for processing (7)
  • libs/i18n/locales/en/translation.json
  • libs/ui-components/src/components/Catalog/InstallWizard/InstallAppWizard.tsx
  • libs/ui-components/src/components/Catalog/InstallWizard/utils.ts
  • libs/ui-components/src/components/Catalog/UpdateModal/OsUpdateModal.tsx
  • libs/ui-components/src/components/Catalog/const.ts
  • libs/ui-components/src/components/Catalog/useCatalogs.ts
  • libs/ui-components/src/components/DynamicForm/VolumeImageField.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • libs/ui-components/src/components/Catalog/UpdateModal/OsUpdateModal.tsx

Comment on lines +39 to +41
"Software Catalog": "Software Catalog",
"Catalog": "Catalog",
"Install Catalog item": "Install Catalog item",
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Do not manually edit generated libs/i18n/locales/en/translation.json.

These string additions/changes should come from source code (t(...) usage) and then be regenerated via npm run i18n. Direct edits here can drift from extracted keys and break translation maintenance flow.

Recommended fix path
  1. Revert direct edits in libs/i18n/locales/en/translation.json.
  2. Add/fix the corresponding source strings in UI code.
  3. Run:
npm run i18n
  1. Commit the regenerated locale file(s).

As per coding guidelines: "Do not manually edit translation.json in libs/i18n/locales/en/; regenerate with npm run i18n by adding or fixing keys in source code".

Also applies to: 190-326, 548-548, 639-639, 659-667, 1361-1361

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

In `@libs/i18n/locales/en/translation.json` around lines 39 - 41, Revert the
manual changes to translation.json and instead add or fix the corresponding
source-level i18n keys by using the t(...) calls where the UI strings live
(e.g., add t("Software Catalog"), t("Catalog"), t("Install Catalog item") in the
appropriate components), then run the extraction/regeneration script (npm run
i18n) to regenerate translation.json and commit the regenerated file(s).

Comment on lines +170 to +172
const resourceId = installToDevice
? `devices/${selectedDevice?.metadata.name}`
: `fleets/${selectedFleet?.metadata.name}`;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove optional chaining from API resource name when building resourceId.

Line 171 and Line 172 can build invalid endpoints (devices/undefined / fleets/undefined). Guard selection explicitly, then access metadata.name directly.

✅ Suggested fix
-    const resourceId = installToDevice
-      ? `devices/${selectedDevice?.metadata.name}`
-      : `fleets/${selectedFleet?.metadata.name}`;
+    let resourceId: string;
+    if (installToDevice) {
+      if (!selectedDevice) {
+        throw t('Device must be selected');
+      }
+      resourceId = `devices/${selectedDevice.metadata.name}`;
+    } else {
+      if (!selectedFleet) {
+        throw t('Fleet must be selected');
+      }
+      resourceId = `fleets/${selectedFleet.metadata.name}`;
+    }

As per coding guidelines: Treat resource.metadata.name as always defined for resources returned from the API or created resources, and avoid optional chaining in these contexts; only handle undefined for partial or input payloads.

📝 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 resourceId = installToDevice
? `devices/${selectedDevice?.metadata.name}`
: `fleets/${selectedFleet?.metadata.name}`;
let resourceId: string;
if (installToDevice) {
if (!selectedDevice) {
throw t('Device must be selected');
}
resourceId = `devices/${selectedDevice.metadata.name}`;
} else {
if (!selectedFleet) {
throw t('Fleet must be selected');
}
resourceId = `fleets/${selectedFleet.metadata.name}`;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/Catalog/InstallWizard/InstallAppWizard.tsx`
around lines 170 - 172, The resourceId construction uses optional chaining
causing possible "devices/undefined" or "fleets/undefined"; modify the logic
around installToDevice to explicitly guard that selectedDevice or selectedFleet
exists (e.g., throw or return early if missing) and then access metadata.name
directly (replace selectedDevice?.metadata.name / selectedFleet?.metadata.name
with selectedDevice.metadata.name / selectedFleet.metadata.name after the
explicit existence check) so resourceId is always built from a defined resource
name.

Copy link

@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.

♻️ Duplicate comments (2)
libs/ui-components/src/components/DynamicForm/VolumeImageField.tsx (1)

297-301: Drop the || '' fallback on item.metadata.name.

item is a CatalogItem returned from the API, so metadata.name is always defined per project convention.

♻️ Proposed fix
-      assetItemName: item.metadata.name || '',
+      assetItemName: item.metadata.name,

As per coding guidelines, "Treat resource.metadata.name as always defined for resources returned from the API or created resources, and avoid optional chaining in these contexts."

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

In `@libs/ui-components/src/components/DynamicForm/VolumeImageField.tsx` around
lines 297 - 301, The code is using a redundant fallback (|| '') for
item.metadata.name even though CatalogItem.metadata.name is always defined;
update the VolumeImageField logic that builds the reference (where assetItemName
is set and onChange(reference) is called) to assign assetItemName:
item.metadata.name without the empty-string fallback, ensuring any related
typing remains satisfied and preserving the existing onChange(reference) call.
libs/i18n/locales/en/translation.json (1)

39-41: Do not manually edit translation.json — regenerate with npm run i18n.

These keys should be added via t(...) calls in source code and then extracted automatically. Direct edits can drift from the actual source strings.

As per coding guidelines: "Do not manually edit translation.json in libs/i18n/locales/en/; regenerate with npm run i18n by adding or fixing keys in source code."

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

In `@libs/i18n/locales/en/translation.json` around lines 39 - 41, The
translation.json file was edited directly to add keys ("Software Catalog",
"Catalog", "Install Catalog item"); undo that manual change and instead add or
correct the user-facing strings in the source code where they belong (use
t("Software Catalog"), t("Catalog"), t("Install Catalog item") in the relevant
React components / modules), then run the extraction step npm run i18n to
regenerate libs/i18n/locales/en/translation.json so the keys are added
automatically; ensure you remove the manual entries from translation.json before
committing.
🧹 Nitpick comments (3)
libs/ui-components/src/components/common/WithHelperText.tsx (1)

11-13: Make content optional in FormGroupWithHelperTextProps to match the new conditional rendering.

The runtime now skips the popover when content is falsy (line 55), but the type still requires content (inherited from HelperTextPopoverProps). This means callers passing undefined as content are relying on runtime behavior that the TypeScript contract doesn't express, and a strict type-checker may warn depending on project settings.

♻️ Proposed fix
-type FormGroupWithHelperTextProps = HelperTextPopoverProps & {
+type FormGroupWithHelperTextProps = Omit<HelperTextPopoverProps, 'content'> & {
+  content?: React.ReactNode;
   isRequired?: boolean;
 };

Also applies to: 47-60

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

In `@libs/ui-components/src/components/common/WithHelperText.tsx` around lines 11
- 13, The prop type FormGroupWithHelperTextProps currently inherits content as
required from HelperTextPopoverProps but the component conditionally skips
rendering the popover when content is falsy; update the type so content is
optional to match runtime behavior. Modify FormGroupWithHelperTextProps by
overriding/omitting the inherited content (e.g., Omit<HelperTextPopoverProps,
'content'> and then add content?: HelperTextPopoverProps['content']) and keep
isRequired?: boolean; ensure the updated type is used where WithHelperText or
FormGroupWithHelperTextProps is referenced so TypeScript no longer requires
content.
libs/ui-components/src/components/DynamicForm/FieldTemplate.tsx (1)

160-176: Stale expanded-state entries are never pruned on item removal.

The useEffect only adds missing indices but never removes entries for deleted items. Over time (add → remove → add cycles), expandedItems accumulates orphan keys. While this won't break rendering, it's a minor memory/state hygiene concern, and could lead to unexpected behavior if the user removes item 1, then adds a new item that gets assigned the same index.

Also, the items array is referenced inside the effect but only items.length is in the dependency list. This works for the intended purpose but will trigger the react-hooks/exhaustive-deps lint rule.

♻️ Suggested fix — rebuild state from current items
   React.useEffect(() => {
     setExpandedItems((prev) => {
-      const next = { ...prev };
-      items.forEach((item) => {
-        if (next[item.index] === undefined) next[item.index] = true;
-      });
+      const next: Record<number, boolean> = {};
+      items.forEach((item) => {
+        next[item.index] = prev[item.index] ?? true;
+      });
       return next;
     });
-  }, [items.length]);
+  }, [items]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/DynamicForm/FieldTemplate.tsx` around lines
160 - 176, The effect and initial state for expandedItems currently only add
missing indices and never remove stale keys; update the logic in the useEffect
(and adjust the dependency to [items]) to rebuild expandedItems from the current
items list: create a new record from items (similar to the initial items.reduce)
that preserves existing values from prev when present and defaults to true for
new indices, and replace state via setExpandedItems(next) so removed item
indices are pruned; reference the expandedItems/setExpandedItems state and the
useEffect that currently depends on items.length and items.reduce for locating
the change.
libs/ui-components/src/components/DynamicForm/VolumeImageField.tsx (1)

139-190: Consider moving the error/loading check before building the default content.

Currently, the toolbar + gallery JSX (lines 139–176) is always constructed and then discarded when error or isLoading is truthy (lines 178–190). Moving the error/loading guard first avoids the unnecessary allocation.

♻️ Suggested reorder
 const AssetsList = ({ ... }: AssetsListProps) => {
   const { t } = useTranslation();
   const hasFilters = !!nameFilter?.trim();

-  let modalContent = (
-    <>
-      <Toolbar ...>
-        ...
-      </Toolbar>
-      {/* gallery/empty state */}
-    </>
-  );
-
-  if (error) {
-    modalContent = ( ... );
-  } else if (isLoading) {
-    modalContent = ( ... );
-  }
+  let modalContent: React.ReactNode;
+
+  if (error) {
+    modalContent = ( ... );
+  } else if (isLoading) {
+    modalContent = ( ... );
+  } else {
+    modalContent = (
+      <>
+        <Toolbar ...>
+          ...
+        </Toolbar>
+        {/* gallery/empty state */}
+      </>
+    );
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/DynamicForm/VolumeImageField.tsx` around
lines 139 - 190, Move the error/isLoading guard before creating the default
modalContent so we don't build the toolbar/gallery JSX only to discard it; in
VolumeImageField.tsx check the error and isLoading flags first (the same
conditions that set modalContent to the Alert or Spinner) and return or set
modalContent to those states before constructing the default block that
references assetCatalogItems, nameFilter, pagination, CatalogItemCard and
onSelect, ensuring the Alert/Spinner branches run early and the heavy JSX is
only allocated when neither error nor isLoading is true.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@libs/i18n/locales/en/translation.json`:
- Around line 39-41: The translation.json file was edited directly to add keys
("Software Catalog", "Catalog", "Install Catalog item"); undo that manual change
and instead add or correct the user-facing strings in the source code where they
belong (use t("Software Catalog"), t("Catalog"), t("Install Catalog item") in
the relevant React components / modules), then run the extraction step npm run
i18n to regenerate libs/i18n/locales/en/translation.json so the keys are added
automatically; ensure you remove the manual entries from translation.json before
committing.

In `@libs/ui-components/src/components/DynamicForm/VolumeImageField.tsx`:
- Around line 297-301: The code is using a redundant fallback (|| '') for
item.metadata.name even though CatalogItem.metadata.name is always defined;
update the VolumeImageField logic that builds the reference (where assetItemName
is set and onChange(reference) is called) to assign assetItemName:
item.metadata.name without the empty-string fallback, ensuring any related
typing remains satisfied and preserving the existing onChange(reference) call.

---

Nitpick comments:
In `@libs/ui-components/src/components/common/WithHelperText.tsx`:
- Around line 11-13: The prop type FormGroupWithHelperTextProps currently
inherits content as required from HelperTextPopoverProps but the component
conditionally skips rendering the popover when content is falsy; update the type
so content is optional to match runtime behavior. Modify
FormGroupWithHelperTextProps by overriding/omitting the inherited content (e.g.,
Omit<HelperTextPopoverProps, 'content'> and then add content?:
HelperTextPopoverProps['content']) and keep isRequired?: boolean; ensure the
updated type is used where WithHelperText or FormGroupWithHelperTextProps is
referenced so TypeScript no longer requires content.

In `@libs/ui-components/src/components/DynamicForm/FieldTemplate.tsx`:
- Around line 160-176: The effect and initial state for expandedItems currently
only add missing indices and never remove stale keys; update the logic in the
useEffect (and adjust the dependency to [items]) to rebuild expandedItems from
the current items list: create a new record from items (similar to the initial
items.reduce) that preserves existing values from prev when present and defaults
to true for new indices, and replace state via setExpandedItems(next) so removed
item indices are pruned; reference the expandedItems/setExpandedItems state and
the useEffect that currently depends on items.length and items.reduce for
locating the change.

In `@libs/ui-components/src/components/DynamicForm/VolumeImageField.tsx`:
- Around line 139-190: Move the error/isLoading guard before creating the
default modalContent so we don't build the toolbar/gallery JSX only to discard
it; in VolumeImageField.tsx check the error and isLoading flags first (the same
conditions that set modalContent to the Alert or Spinner) and return or set
modalContent to those states before constructing the default block that
references assetCatalogItems, nameFilter, pagination, CatalogItemCard and
onSelect, ensuring the Alert/Spinner branches run early and the heavy JSX is
only allocated when neither error nor isLoading is true.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 407751a and 983e47b.

📒 Files selected for processing (4)
  • libs/i18n/locales/en/translation.json
  • libs/ui-components/src/components/DynamicForm/FieldTemplate.tsx
  • libs/ui-components/src/components/DynamicForm/VolumeImageField.tsx
  • libs/ui-components/src/components/common/WithHelperText.tsx

Copy link

@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: 2

Caution

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

⚠️ Outside diff range comments (2)
libs/types/scripts/openapi-typescript.js (2)

109-114: ⚠️ Potential issue | 🟡 Minor

Alpha directories missing from the initial cleanup.

main() cleans models, imagebuilder, tmp-types, and tmp-imagebuilder-types, but never cleans alpha or tmp-alpha-types. If a previous generation run placed a stale index.ts in alpha/ and generateTypes(ALPHA_CORE_API) fails before reaching line 97, that stale file persists silently. rimraf(finalDir) inside generateTypes only covers alpha/models, not the alpha/ root.

🛠️ Proposed fix
     await Promise.all([
       rimraf(path.join(rootDir, 'models')),
       rimraf(path.join(rootDir, 'imagebuilder')),
+      rimraf(path.join(rootDir, 'alpha')),
       rimraf(path.join(rootDir, 'tmp-types')),
       rimraf(path.join(rootDir, 'tmp-imagebuilder-types')),
+      rimraf(path.join(rootDir, 'tmp-alpha-types')),
     ]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/types/scripts/openapi-typescript.js` around lines 109 - 114, main()
currently omits cleaning the alpha root and its temp types, so stale files in
alpha/ can persist; update the initial Promise.all cleanup to also
rimraf(path.join(rootDir, 'alpha')) and rimraf(path.join(rootDir,
'tmp-alpha-types')) so both the alpha root and its temporary types are removed
before calling generateTypes(ALPHA_CORE_API); keep the existing rimraf(finalDir)
inside generateTypes() as-is but ensure the top-level cleanup includes these two
additional rimraf calls to prevent leftover index.ts files.

55-57: ⚠️ Potential issue | 🟡 Minor

Stale error message omits alphacore.

-    throw new Error(`Unknown mode: ${mode}. Use 'core' or 'imagebuilder'`);
+    throw new Error(`Unknown mode: ${mode}. Use 'core', 'alphacore', or 'imagebuilder'`);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/types/scripts/openapi-typescript.js` around lines 55 - 57, The error
thrown when config[mode] is falsy uses a stale message listing only 'core' and
'imagebuilder'; update the throw in the block that checks config[mode] to
include the actual valid modes (either add 'alphacore' to the message or
dynamically list Object.keys(config) so the message stays accurate). Locate the
check using the variables config and mode and replace the literal message with
one that shows the available modes (e.g., `Unknown mode: ${mode}. Use
${Object.keys(config).join(', ')}`).
♻️ Duplicate comments (2)
libs/ui-components/src/components/DynamicForm/VolumeImageField.tsx (1)

297-299: ⚠️ Potential issue | 🟡 Minor

Remove the fallback on API-backed metadata.name.

Line 298 still uses item.metadata.name || ''. For API-returned CatalogItem, metadata.name should be treated as always defined.

♻️ Proposed fix
-      assetItemName: item.metadata.name || '',
+      assetItemName: item.metadata.name,

As per coding guidelines, "Treat resource.metadata.name as always defined for resources returned from the API or created resources, and avoid optional chaining in these contexts; only handle undefined for partial or input payloads".

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

In `@libs/ui-components/src/components/DynamicForm/VolumeImageField.tsx` around
lines 297 - 299, The assignment that sets assetItemName currently uses a
fallback (item.metadata.name || ''), but for API-backed CatalogItem
metadata.name is always defined; update the code that constructs the object (the
assetItemName property in the block where assetCatalog and assetItemName are
set) to remove the fallback and use item.metadata.name directly so it no longer
supplies an empty-string default.
libs/types/scripts/openapi-typescript.js (1)

91-91: ⚠️ Potential issue | 🟡 Minor

Stale comment still references "imagebuilder/index.ts".

This branch now handles both imagebuilder and alpha; the comment should be updated accordingly.

-    // Copy the generated index.ts to imagebuilder/index.ts
+    // Copy the generated index.ts to the mode-specific output directory
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/types/scripts/openapi-typescript.js` at line 91, Update the stale inline
comment that currently reads "// Copy the generated index.ts to
imagebuilder/index.ts" to reference both targets handled by this branch
(imagebuilder and alpha); locate that comment string in
libs/types/scripts/openapi-typescript.js and change it to something like "//
Copy the generated index.ts to imagebuilder/index.ts and alpha/index.ts" so it
accurately reflects both destinations.
🧹 Nitpick comments (2)
libs/types/scripts/openapi-utils.js (1)

72-75: The comment about escaping special characters is misleading since no escaping is actually performed.

The prefixes currently used ('v1beta1_openapi_yaml_components_schemas' and 'core_v1beta1_openapi_yaml_components_schemas') are safe, but if a future caller passes a prefix containing regex special characters (e.g., ., +), the constructed patterns would silently misbehave. Consider either removing the misleading comment or adding actual escaping for defensive robustness.

♻️ Optional: escape the prefix for regex safety
-  // Pre-construct the Regex patterns using the dynamic prefix
-  // We escape special characters in the prefix if needed, though usually not for schema names
-  const importRegex = new RegExp(`from\\s+['"]\\.\\/${prefix}_([A-Za-z][A-Za-z0-9]*)['"]`, 'g');
-  const typeRegex = new RegExp(`\\b${prefix}_([A-Za-z][A-Za-z0-9]*)\\b`, 'g');
+  // Pre-construct the Regex patterns using the dynamic prefix
+  const escapedPrefix = prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+  const importRegex = new RegExp(`from\\s+['"]\\.\\/${escapedPrefix}_([A-Za-z][A-Za-z0-9]*)['"]`, 'g');
+  const typeRegex = new RegExp(`\\b${escapedPrefix}_([A-Za-z][A-Za-z0-9]*)\\b`, 'g');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/types/scripts/openapi-utils.js` around lines 72 - 75, The comment claims
special-character escaping is done for the dynamic prefix but no escaping
occurs; update the code that builds importRegex and typeRegex to first escape
regex metacharacters in the prefix (create or reuse an escapeRegExp helper that
replaces [.*+?^${}()|[\]\\] with escaped versions) and then use the
escapedPrefix when constructing new RegExp for importRegex and typeRegex
(referencing the constants importRegex and typeRegex), or alternatively
remove/clarify the misleading comment if you prefer not to add escaping.
libs/ui-components/src/components/DynamicForm/FieldTemplate.tsx (1)

126-128: CustomObjectField is a no-op passthrough — consider removing it.

The current implementation unconditionally delegates to DefaultObjectField with no customization. Registering a passthrough adds indirection without value and makes future readers assume there's special logic.

Either inline DefaultObjectField directly in pfFields, or keep the wrapper only when actual custom behavior is added.

♻️ Proposed simplification
-// Custom Object Field - can be extended for other special object types
-const CustomObjectField: React.FC<FieldProps> = (props) => {
-  return <DefaultObjectField {...props} />;
-};
-
 // Custom fields registry
 const pfFields: RegistryFieldsType = {
-  ObjectField: CustomObjectField,
+  ObjectField: DefaultObjectField,
   StringField: CustomStringField,
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/DynamicForm/FieldTemplate.tsx` around lines
126 - 128, CustomObjectField is an unconditional passthrough to
DefaultObjectField; remove the unnecessary wrapper to reduce indirection by
replacing references to CustomObjectField in pfFields with DefaultObjectField
(or delete CustomObjectField and inline DefaultObjectField where pfFields is
constructed). If you prefer to keep a named export for future customization, add
a comment explaining it's intentionally a passthrough. Update imports/exports
accordingly to avoid unused symbol errors.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@libs/types/scripts/openapi-typescript.js`:
- Around line 39-45: ALPHA_CORE_API's coreRef value is inconsistent with
IMAGEBUILDER_API which can cause fixCoreReferences to miss prefixed type refs;
update the ALPHA_CORE_API configuration's coreRef to match IMAGEBUILDER_API's
'core_v1beta1_openapi_yaml_components_schemas' (or make both the same canonical
string) so the fixCoreReferences step and any future openapi-typescript-codegen
outputs will be transformed consistently; locate the ALPHA_CORE_API object and
change its coreRef field accordingly and verify fixCoreReferences uses that same
key when normalizing imports.

In `@libs/ui-components/src/components/DynamicForm/FieldTemplate.tsx`:
- Around line 160-177: The expandedItems state currently uses item.index which
becomes stale when array items are removed/shifted; change the state map to use
rjsf's stable item.key instead and update all places that read/write
expandedItems (including the toggle handler and any expand checks) to use
item.key; initialize expandedItems by reducing items.map(i => i.key) to true and
modify the React.useEffect to rebuild/merge state by iterating current items and
producing a new object that keeps existing values for existing item.key entries
and drops any keys that no longer exist (so removed items are cleaned up), then
call setExpandedItems with that cleaned map.

---

Outside diff comments:
In `@libs/types/scripts/openapi-typescript.js`:
- Around line 109-114: main() currently omits cleaning the alpha root and its
temp types, so stale files in alpha/ can persist; update the initial Promise.all
cleanup to also rimraf(path.join(rootDir, 'alpha')) and
rimraf(path.join(rootDir, 'tmp-alpha-types')) so both the alpha root and its
temporary types are removed before calling generateTypes(ALPHA_CORE_API); keep
the existing rimraf(finalDir) inside generateTypes() as-is but ensure the
top-level cleanup includes these two additional rimraf calls to prevent leftover
index.ts files.
- Around line 55-57: The error thrown when config[mode] is falsy uses a stale
message listing only 'core' and 'imagebuilder'; update the throw in the block
that checks config[mode] to include the actual valid modes (either add
'alphacore' to the message or dynamically list Object.keys(config) so the
message stays accurate). Locate the check using the variables config and mode
and replace the literal message with one that shows the available modes (e.g.,
`Unknown mode: ${mode}. Use ${Object.keys(config).join(', ')}`).

---

Duplicate comments:
In `@libs/types/scripts/openapi-typescript.js`:
- Line 91: Update the stale inline comment that currently reads "// Copy the
generated index.ts to imagebuilder/index.ts" to reference both targets handled
by this branch (imagebuilder and alpha); locate that comment string in
libs/types/scripts/openapi-typescript.js and change it to something like "//
Copy the generated index.ts to imagebuilder/index.ts and alpha/index.ts" so it
accurately reflects both destinations.

In `@libs/ui-components/src/components/DynamicForm/VolumeImageField.tsx`:
- Around line 297-299: The assignment that sets assetItemName currently uses a
fallback (item.metadata.name || ''), but for API-backed CatalogItem
metadata.name is always defined; update the code that constructs the object (the
assetItemName property in the block where assetCatalog and assetItemName are
set) to remove the fallback and use item.metadata.name directly so it no longer
supplies an empty-string default.

---

Nitpick comments:
In `@libs/types/scripts/openapi-utils.js`:
- Around line 72-75: The comment claims special-character escaping is done for
the dynamic prefix but no escaping occurs; update the code that builds
importRegex and typeRegex to first escape regex metacharacters in the prefix
(create or reuse an escapeRegExp helper that replaces [.*+?^${}()|[\]\\] with
escaped versions) and then use the escapedPrefix when constructing new RegExp
for importRegex and typeRegex (referencing the constants importRegex and
typeRegex), or alternatively remove/clarify the misleading comment if you prefer
not to add escaping.

In `@libs/ui-components/src/components/DynamicForm/FieldTemplate.tsx`:
- Around line 126-128: CustomObjectField is an unconditional passthrough to
DefaultObjectField; remove the unnecessary wrapper to reduce indirection by
replacing references to CustomObjectField in pfFields with DefaultObjectField
(or delete CustomObjectField and inline DefaultObjectField where pfFields is
constructed). If you prefer to keep a named export for future customization, add
a comment explaining it's intentionally a passthrough. Update imports/exports
accordingly to avoid unused symbol errors.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 983e47b and 7854c04.

📒 Files selected for processing (4)
  • libs/types/scripts/openapi-typescript.js
  • libs/types/scripts/openapi-utils.js
  • libs/ui-components/src/components/DynamicForm/FieldTemplate.tsx
  • libs/ui-components/src/components/DynamicForm/VolumeImageField.tsx

Comment on lines +39 to +45
[ALPHA_CORE_API]: {
swaggerUrl: getSwaggerUrl('core/v1alpha1'),
output: path.resolve(__dirname, '../tmp-alpha-types'),
finalDir: path.resolve(__dirname, '../alpha/models'),
coreRef: 'v1beta1_openapi_yaml_components_schemas',
outputDir: path.resolve(__dirname, '../alpha'),
},
Copy link

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
# Check fixCoreReferences implementation and coreRef consistency

# 1. Show the fixCoreReferences implementation
echo "=== fixCoreReferences Implementation ==="
rg -n "fixCoreReferences" --type js -A 25

# 2. Check the full configuration block for both ALPHA_CORE_API and IMAGEBUILDER_API
echo -e "\n=== Checking coreRef values in config ==="
rg -n "coreRef:" libs/types/scripts/openapi-typescript.js -B 3

# 3. Check if any alpha model files exist
echo -e "\n=== Checking alpha models directory ==="
find libs/types/alpha/models -type f -name "*.ts" 2>/dev/null | head -5

Repository: flightctl/flightctl-ui

Length of output: 6905


🏁 Script executed:

#!/bin/bash
# Check what reference patterns are actually in the generated alpha model files

echo "=== Sample of alpha model file content (first file) ==="
head -50 libs/types/alpha/models/ApiVersion.ts

echo -e "\n=== Search for openapi_yaml_components_schemas patterns in alpha models ==="
rg "openapi_yaml_components_schemas" libs/types/alpha/models --type ts -A 1 -B 1 | head -50

echo -e "\n=== Search for openapi_yaml_components_schemas patterns in imagebuilder models ==="
rg "openapi_yaml_components_schemas" libs/types/imagebuilder/models --type ts -A 1 -B 1 | head -50

echo -e "\n=== Count of references in each directory ==="
echo "Alpha references:"
rg "openapi_yaml_components_schemas" libs/types/alpha/models --type ts | wc -l
echo "Imagebuilder references:"
rg "openapi_yaml_components_schemas" libs/types/imagebuilder/models --type ts | wc -l

Repository: flightctl/flightctl-ui

Length of output: 913


🏁 Script executed:

#!/bin/bash
# Check what type references and imports actually exist in the generated files

echo "=== Alpha model file imports and references ==="
rg "^import|^export|from.*models" libs/types/alpha/models --type ts | head -30

echo -e "\n=== Imagebuilder model file imports and references ==="
rg "^import|^export|from.*models" libs/types/imagebuilder/models --type ts | head -30

echo -e "\n=== Check if there are any inter-model dependencies in alpha ==="
rg "from.*\./" libs/types/alpha/models --type ts | head -20

echo -e "\n=== Check if there are any inter-model dependencies in imagebuilder ==="
rg "from.*\./" libs/types/imagebuilder/models --type ts | head -20

echo -e "\n=== Git log to see if models were recently regenerated ==="
git log --oneline -n 5 -- libs/types/alpha/models/ libs/types/imagebuilder/models/ 2>/dev/null || echo "No recent commits"

Repository: flightctl/flightctl-ui

Length of output: 10552


🏁 Script executed:

#!/bin/bash
# Check the recent commit to understand what changes were made to the generation script

echo "=== Recent commit that changed the generation script ==="
git show --stat 7854c04

echo -e "\n=== Diff of the generation script changes ==="
git show 7854c04 -- libs/types/scripts/openapi-typescript.js | head -100

Repository: flightctl/flightctl-ui

Length of output: 50378


Ensure coreRef values are consistent between configurations for future-proofing.

While the current generated alpha and imagebuilder models both contain clean, properly-formed imports without prefixed references, the coreRef values differ: ALPHA_CORE_API uses 'v1beta1_openapi_yaml_components_schemas' while IMAGEBUILDER_API uses 'core_v1beta1_openapi_yaml_components_schemas'. If openapi-typescript-codegen ever emits prefixed type references for the alpha spec matching the imagebuilder pattern, the fixCoreReferences step would silently fail to transform them. Align these coreRef values to ensure consistent handling across both API configurations.

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

In `@libs/types/scripts/openapi-typescript.js` around lines 39 - 45,
ALPHA_CORE_API's coreRef value is inconsistent with IMAGEBUILDER_API which can
cause fixCoreReferences to miss prefixed type refs; update the ALPHA_CORE_API
configuration's coreRef to match IMAGEBUILDER_API's
'core_v1beta1_openapi_yaml_components_schemas' (or make both the same canonical
string) so the fixCoreReferences step and any future openapi-typescript-codegen
outputs will be transformed consistently; locate the ALPHA_CORE_API object and
change its coreRef field accordingly and verify fixCoreReferences uses that same
key when normalizing imports.

Comment on lines +160 to +177
const [expandedItems, setExpandedItems] = React.useState<Record<number, boolean>>(() =>
items.reduce<Record<number, boolean>>((acc, item) => {
acc[item.index] = true;
return acc;
}, {}),
);

// Keep expanded state in sync when items are added/removed
React.useEffect(() => {
setExpandedItems((prev) => {
const next = { ...prev };
items.forEach((item) => {
if (next[item.index] === undefined) next[item.index] = true;
});
return next;
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [items.length]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Stale expandedItems keys after item removal can corrupt expand state for re-added items.

expandedItems is keyed by item.index (positional integer). When an item is removed, rjsf shifts indices for remaining items, but the stale key-value pairs from the removed item's index linger in state. The useEffect at line 168 only adds missing keys (for newly added items) but never removes stale ones or re-maps them. If a user removes item at index 0 and then adds a new item, that new item at index 0 will inherit the previous item's expansion state via the stale key.

Consider using item.key (rjsf's stable string key) instead of item.index as the state map key, and cleaning up keys that no longer exist:

♻️ Proposed fix
-  const [expandedItems, setExpandedItems] = React.useState<Record<number, boolean>>(() =>
-    items.reduce<Record<number, boolean>>((acc, item) => {
-      acc[item.index] = true;
+  const [expandedItems, setExpandedItems] = React.useState<Record<string, boolean>>(() =>
+    items.reduce<Record<string, boolean>>((acc, item) => {
+      acc[item.key] = true;
       return acc;
     }, {}),
   );

   // Keep expanded state in sync when items are added/removed
   React.useEffect(() => {
     setExpandedItems((prev) => {
-      const next = { ...prev };
-      items.forEach((item) => {
-        if (next[item.index] === undefined) next[item.index] = true;
-      });
-      return next;
+      const activeKeys = new Set(items.map((item) => item.key));
+      const next: Record<string, boolean> = {};
+      items.forEach((item) => {
+        next[item.key] = prev[item.key] ?? true;
+      });
+      return next;
     });
     // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [items.length]);

And update the toggle/expand references accordingly:

-                  isExpanded={expandedItems[item.index] !== false}
+                  isExpanded={expandedItems[item.key] !== false}
                   onToggle={(_, expanded) => {
-                    setExpandedItems((prev) => ({ ...prev, [item.index]: expanded }));
+                    setExpandedItems((prev) => ({ ...prev, [item.key]: expanded }));
                   }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/DynamicForm/FieldTemplate.tsx` around lines
160 - 177, The expandedItems state currently uses item.index which becomes stale
when array items are removed/shifted; change the state map to use rjsf's stable
item.key instead and update all places that read/write expandedItems (including
the toggle handler and any expand checks) to use item.key; initialize
expandedItems by reducing items.map(i => i.key) to true and modify the
React.useEffect to rebuild/merge state by iterating current items and producing
a new object that keeps existing values for existing item.key entries and drops
any keys that no longer exist (so removed items are cleaned up), then call
setExpandedItems with that cleaned map.

Copy link

@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

♻️ Duplicate comments (1)
libs/ui-components/src/components/Catalog/CatalogPage.tsx (1)

189-189: ⚠️ Potential issue | 🟡 Minor

Filter empty check is incorrect for array type.

!catalogFilter.itemType checks for undefined/null but not an empty array. Based on the usage in CatalogPageFilter, itemType is an array, so an empty array [] would still be truthy.

-  const filterIsEmpty = !catalogFilter.itemType;
+  const filterIsEmpty = catalogFilter.itemType.length === 0;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/Catalog/CatalogPage.tsx` at line 189,
filterIsEmpty currently uses "!catalogFilter.itemType" which misses empty
arrays; update the check in CatalogPage (symbol: filterIsEmpty) to treat
catalogFilter.itemType (from CatalogPageFilter) as empty when it's an array with
length 0 — e.g. use Array.isArray(catalogFilter.itemType) ?
catalogFilter.itemType.length === 0 : !catalogFilter.itemType — so empty arrays
are considered empty filters.
🧹 Nitpick comments (10)
libs/ui-components/src/components/Catalog/InstallWizard/UpdateSuccessPage.tsx (1)

22-35: Reuse UpdateSuccessPageContent to remove duplicated empty-state markup.

Both components duplicate title/body rendering. Compose UpdateSuccessPage with UpdateSuccessPageContent so message/title changes stay in one place.

Also applies to: 45-81

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

In
`@libs/ui-components/src/components/Catalog/InstallWizard/UpdateSuccessPage.tsx`
around lines 22 - 35, The UpdateSuccessPage duplicates the same EmptyState
title/body markup as UpdateSuccessPageContent; refactor UpdateSuccessPage to
render UpdateSuccessPageContent (pass isDevice and children) instead of
duplicating the EmptyState JSX so title/text changes are centralized in
UpdateSuccessPageContent, and remove the duplicated EmptyState markup in
UpdateSuccessPage (also apply the same change for the other similar component
instance that mirrors this markup).
libs/ui-components/src/components/Catalog/EditWizard/EditAppWizard.tsx (1)

154-160: Consider using an Error object instead of throwing a translated string.

Throwing a plain string works but using an Error object is more idiomatic and preserves stack traces for debugging.

Proposed improvement
   const { onSubmit, error, schemaErrors, setError } = useSubmitCatalogForm<AppUpdateFormik>(async (values) => {
     const catalogItemVersion = catalogItem.spec.versions.find((v) => v.version === values.version);
     if (!catalogItemVersion) {
-      throw t('Version {{version}} not found', { version: values.version });
+      throw new Error(t('Version {{version}} not found', { version: values.version }));
     }
     await onUpdate(catalogItemVersion, values);
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/Catalog/EditWizard/EditAppWizard.tsx`
around lines 154 - 160, The code in EditAppWizard's useSubmitCatalogForm
callback throws a translated string when the requested version isn't found;
replace that with an Error object to preserve stack traces and be idiomatic (in
the block inside useSubmitCatalogForm<AppUpdateFormik>(...) where
catalogItemVersion is checked, change throw t('Version {{version}} not found', {
version: values.version }) to throw new Error(t('Version {{version}} not found',
{ version: values.version })) so onSubmit/error handlers receive an Error
instance).
libs/ui-components/src/components/Catalog/EditWizard/EditOsWizard.tsx (2)

20-27: Unused errors parameter in validation function.

The errors parameter is declared but never used in validateUpdateWizardStep. Consider removing it if validation doesn't depend on Formik errors, or document why it's kept for future use.

✏️ Suggested fix
 const validateUpdateWizardStep = (
   activeStepId: string,
-  errors: FormikErrors<InstallSpecFormik>,
+  _errors: FormikErrors<InstallSpecFormik>,
   values: InstallSpecFormik,
 ) => {
   if (activeStepId === versionStepId) return !!values.version;
   return true;
 };

If the signature must match FlightCtlWizardFooter's validateStep callback type, prefix with underscore to indicate intentional non-use.

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

In `@libs/ui-components/src/components/Catalog/EditWizard/EditOsWizard.tsx` around
lines 20 - 27, The validateUpdateWizardStep function currently declares an
unused errors parameter; update its signature to either remove the errors
parameter (if not required) or rename it to _errors to indicate intentional
non-use so it satisfies FlightCtlWizardFooter's validateStep callback type;
touch the function validateUpdateWizardStep and any callers expecting that exact
signature and ensure versionStepId and InstallSpecFormik usage remain unchanged.

77-86: Typo and unused prop: exisingLabels should be existingLabels and appears unused.

The prop name has a typo (exisingLabelsexistingLabels), and this prop is not used within the component body. If the prop is needed for future use, consider adding a TODO comment; otherwise, remove it.

✏️ Suggested fix
 type EditOsWizardProps = {
   catalogItem: CatalogItem;
   currentVersion: CatalogItemVersion;
   onUpdate: (catalogItemVersion: CatalogItemVersion, values: InstallSpecFormik) => Promise<void>;
   currentChannel: string;
-  exisingLabels: Record<string, string> | undefined;
+  existingLabels?: Record<string, string>;
   isEdit: boolean;
   version: string;
   channel: string;
 };

If the prop is intentionally unused for now, document it or remove it from the interface.

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

In `@libs/ui-components/src/components/Catalog/EditWizard/EditOsWizard.tsx` around
lines 77 - 86, EditOsWizardProps declares a misspelled and unused prop
"exisingLabels"; rename it to "existingLabels" in the EditOsWizardProps type and
update all usages (props destructuring in the EditOsWizard component and any
callers) to the corrected name, or if it's intentionally unused remove
"exisingLabels" entirely from EditOsWizardProps and from any function/component
signatures and callers; if you keep it but don't use it yet, add a short TODO
comment above the prop in EditOsWizardProps indicating intended future use to
avoid linter distractions.
libs/ui-components/src/components/Catalog/EditWizard/EditWizard.tsx (1)

100-122: Typo in prop name: exisingLabels should be existingLabels.

Line 106 passes exisingLabels which appears to be a typo. This should be existingLabels for consistency and clarity.

✏️ Suggested fix
       content = (
         <EditOsWizard
           isEdit={!version}
           catalogItem={catalogItem}
           currentChannel={currentChannel}
           currentVersion={currentVersion}
-          exisingLabels={currentLabels}
+          existingLabels={currentLabels}
           version={version}
           channel={channel}

Note: This requires updating the EditOsWizard component's prop type definition as well (exisingLabelsexistingLabels in EditOsWizardProps).

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

In `@libs/ui-components/src/components/Catalog/EditWizard/EditWizard.tsx` around
lines 100 - 122, The prop name passed to EditOsWizard is misspelled as
exisingLabels; change it to existingLabels in the JSX (within EditWizard where
EditOsWizard is rendered) and update the EditOsWizard component's prop
interface/type (e.g., EditOsWizardProps) to rename exisingLabels →
existingLabels, then search for any other references/usages of exisingLabels in
the codebase and update them to existingLabels to keep types and consumers
consistent.
libs/ui-components/src/components/Catalog/EditWizard/steps/UpdateGraph.tsx (2)

122-127: Avoid returning values from forEach callbacks.

The static analysis tool flags that the forEach callback implicitly returns the result of Map.set(). While functionally harmless, use explicit block syntax for clarity.

✏️ Suggested fix
   // Deduplicate entries
   const entriesMap = new Map<string, CatalogItemVersion>();
-  directUpgradeEntries.forEach((entry) => entriesMap.set(entry.version, entry));
+  directUpgradeEntries.forEach((entry) => {
+    entriesMap.set(entry.version, entry);
+  });
   if (currentVersionEntry) {
     entriesMap.set(currentVersionEntry.version, currentVersionEntry);
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/Catalog/EditWizard/steps/UpdateGraph.tsx`
around lines 122 - 127, The forEach callback on directUpgradeEntries implicitly
returns Map.set(), triggering the static analysis warning; change the callback
to an explicit block-style arrow function or replace forEach with a for..of loop
so the Map is updated without an implicit return—modify the code that constructs
entriesMap (using entriesMap, directUpgradeEntries, currentVersionEntry,
allEntries) to call entriesMap.set(...) inside a statement block (e.g.,
directUpgradeEntries.forEach(entry => { entriesMap.set(...); })) or use for
(const entry of directUpgradeEntries) { entriesMap.set(...); }.

92-103: Type assertion as never suggests a type compatibility issue.

The as never cast on line 97 is a type escape hatch. This may indicate a mismatch between withSelection() HOC's return type and what ComponentFactory expects. Consider investigating the proper typing or adding a comment explaining why this is necessary.

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

In `@libs/ui-components/src/components/Catalog/EditWizard/steps/UpdateGraph.tsx`
around lines 92 - 103, The switch in customComponentFactory is masking a real
type mismatch by using the `as never` cast on VersionNode; remove the `as never`
and fix the types so VersionNode satisfies ComponentFactory's expected return
type. Inspect the ComponentFactory return signature and the props/ComponentType
of VersionNode (and any HOC wrappers like withPanZoom()/withSelection()) and
either (a) adjust VersionNode's exported component type/props to match
ComponentFactory, (b) wrap VersionNode with the proper HOC that yields the
expected ComponentFactory type, or (c) use a precise cast to the correct
ComponentFactory return type (not never) so the compiler understands the
component is compatible. Ensure changes reference customComponentFactory,
ComponentFactory, ModelKind, and VersionNode so future reviewers can find and
verify the fix.
libs/ui-components/src/components/Catalog/InstalledSoftware.tsx (3)

251-251: Unnecessary fallback for metadata.name on API-returned resources.

Per coding guidelines, resource.metadata.name is always defined for resources returned from the API. The || '' fallbacks are defensive but unnecessary here since osItem and app.item are fetched from the API.

Also applies to: 260-260, 286-286, 327-327

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

In `@libs/ui-components/src/components/Catalog/InstalledSoftware.tsx` at line 251,
Remove the unnecessary defensive fallbacks for metadata.name on API-returned
resources: replace usages like osItem.metadata.name || '' and
app.item.metadata.name || '' with the direct property (osItem.metadata.name and
app.item.metadata.name) in the onEdit/onEditOs/onEditApp calls and any similar
callbacks (e.g., the lines using onEditOs, onEditApp, and wherever osItem or
app.item metadata is passed); ensure the props/functions onEdit and related
handlers accept the non-null string as before and adjust no other logic.

48-61: Consider consolidating UpdateAppColumn and UpdateOsColumn into a single component.

These two components are nearly identical, differing only in the callback prop name (onEditApp vs onEditOs). This is a minor duplication that could be unified.

♻️ Optional: Unified component
-type UpdateColumnProps = {
-  catalogItem: CatalogItem;
-  channel: string;
-  catalogItemVersion: CatalogItemVersion;
-  appName: string | undefined;
-  onEditApp: VoidFunction;
-};
-
-const UpdateAppColumn = ({ catalogItem, channel, catalogItemVersion, onEditApp }: UpdateColumnProps) => {
-  const { t } = useTranslation();
-  const updates = getUpdates(catalogItem, channel, catalogItemVersion.version);
-
-  return (
-    <>
-      {!!updates.length && (
-        <Button variant="link" isInline onClick={onEditApp} icon={<ArrowCircleUpIcon />}>
-          {t('Update available')}
-        </Button>
-      )}
-    </>
-  );
-};
-
-type UpdateOsColumnProps = {
-  catalogItem: CatalogItem;
-  channel: string;
-  catalogItemVersion: CatalogItemVersion;
-  onEditOs: VoidFunction;
-};
-
-const UpdateOsColumn = ({ onEditOs, catalogItem, channel, catalogItemVersion }: UpdateOsColumnProps) => {
-  const { t } = useTranslation();
-  const updates = getUpdates(catalogItem, channel, catalogItemVersion.version);
-
-  return (
-    <>
-      {!!updates.length && (
-        <Button variant="link" isInline onClick={onEditOs} icon={<ArrowCircleUpIcon />}>
-          {t('Update available')}
-        </Button>
-      )}
-    </>
-  );
-};
+type UpdateColumnProps = {
+  catalogItem: CatalogItem;
+  channel: string;
+  catalogItemVersion: CatalogItemVersion;
+  onEdit: VoidFunction;
+};
+
+const UpdateColumn = ({ catalogItem, channel, catalogItemVersion, onEdit }: UpdateColumnProps) => {
+  const { t } = useTranslation();
+  const updates = getUpdates(catalogItem, channel, catalogItemVersion.version);
+
+  return (
+    <>
+      {!!updates.length && (
+        <Button variant="link" isInline onClick={onEdit} icon={<ArrowCircleUpIcon />}>
+          {t('Update available')}
+        </Button>
+      )}
+    </>
+  );
+};

Also applies to: 70-83

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

In `@libs/ui-components/src/components/Catalog/InstalledSoftware.tsx` around lines
48 - 61, Two near-duplicate components UpdateAppColumn and UpdateOsColumn should
be consolidated: create a single GenericUpdateColumn (or UpdateColumn) that
accepts props matching UpdateColumnProps but with a generic onEdit callback
(e.g., onEdit: () => void) instead of onEditApp/onEditOs; reuse useTranslation,
getUpdates(catalogItem, channel, catalogItemVersion.version), and the
ArrowCircleUpIcon/Button rendering logic inside that component, then replace
usages of UpdateAppColumn and UpdateOsColumn with the unified component passing
the appropriate onEdit handler from callers.

179-199: Effect may trigger unnecessary fetches when apps array reference changes.

The apps dependency from useMemo creates a new array reference on each render (even if contents are the same), which could cause redundant API calls. Consider adding an early return or stabilizing the dependency.

♻️ Proposed optimization
  React.useEffect(() => {
+   if (apps.length === 0) {
+     setAppItems([]);
+     setAppsLoading(false);
+     return;
+   }
    (async () => {
      const appRequests = apps.map((app) => get<CatalogItem>(`catalogs/${app.catalog}/items/${app.item}`));
      const results = await Promise.allSettled(appRequests);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/Catalog/InstalledSoftware.tsx` around lines
179 - 199, The effect in InstalledSoftware re-fetches whenever the apps array
reference changes; to avoid redundant calls, add a stability guard inside the
useEffect: keep a useRef(prevApps) and perform a shallow/deep equality check
(compare key fields like catalog,item,name or JSON.stringify) between
prevApps.current and apps, and early-return if they are equal; only update
prevApps.current and proceed to run the async fetch when the contents actually
differ, then setAppItems and setAppsLoading as before. Reference the
InstalledSoftware component, the useEffect block, the apps dependency, and the
setAppItems/setAppsLoading state updates when implementing this guard.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@libs/ui-components/src/components/Catalog/CatalogPage.tsx`:
- Around line 218-235: The key for CatalogItemCard uses only ci.metadata.name
which may collide across catalogs; update the key to include the catalog
identifier so it is globally unique (e.g., combine ci.metadata.catalog and
ci.metadata.name) and ensure any related selection logic (setSelectedItem usage)
still relies on the same composite identity (catalog + itemName) such that
CatalogItemCard keys and selection state are consistent.

In `@libs/ui-components/src/components/Catalog/EditWizard/steps/UpdateGraph.tsx`:
- Around line 235-261: The controller is currently created inside a
React.useMemo keyed only by currentChannel, causing stale visuals when
currentVersion or updates change; update the logic so the Visualization
controller still memoizes on creation (Visualization, customComponentFactory,
customLayoutFactory) but apply model updates in a React.useEffect that rebuilds
the model via buildTopologyModel(currentVersion, updates, currentChannel) and
calls controller.fromModel(model, false) whenever controller, currentVersion,
updates, or currentChannel change; keep existing event listeners
(GRAPH_LAYOUT_END_EVENT, SELECTION_EVENT) on the Visualization instance and
remove currentVersion/updates from the stale dependency gap.

In `@libs/ui-components/src/components/Catalog/InstalledSoftware.tsx`:
- Around line 282-293: The actions array in InstalledSoftware.tsx can contain an
empty object when itemVersion is falsy, which may render a broken ActionsColumn
entry; change the construction of actions (the IAction[] containing the
conditional edit action and the delete action) so the edit action is only added
when itemVersion is truthy — e.g. build actions by conditionally spreading the
edit action (or push it only when itemVersion) or filter the array to remove
empty objects (e.g. .filter(Boolean) or .filter(a => Object.keys(a).length))
before passing to ActionsColumn; ensure references to onEdit and setAppToDelete
remain unchanged.

In
`@libs/ui-components/src/components/Catalog/InstallWizard/UpdateSuccessPage.tsx`:
- Around line 68-72: The navigate call in UpdateSuccessPage.tsx currently passes
postfix using optional chaining (device?.metadata.name / fleet?.metadata.name)
and can navigate to invalid routes; update the logic that handles target to
explicitly check target === 'device' or 'fleet' and verify the corresponding
resource exists (device for device path, fleet for fleet path) before calling
navigate, and only call navigate with ROUTE.DEVICE_DETAILS and
device.metadata.name or ROUTE.FLEET_DETAILS and fleet.metadata.name when the
resource is non-null; otherwise skip navigation or route to a safe fallback.
Ensure you modify the block containing the navigate(...) invocation so that
target, device, and fleet are guarded and no undefined postfix is ever passed.

---

Duplicate comments:
In `@libs/ui-components/src/components/Catalog/CatalogPage.tsx`:
- Line 189: filterIsEmpty currently uses "!catalogFilter.itemType" which misses
empty arrays; update the check in CatalogPage (symbol: filterIsEmpty) to treat
catalogFilter.itemType (from CatalogPageFilter) as empty when it's an array with
length 0 — e.g. use Array.isArray(catalogFilter.itemType) ?
catalogFilter.itemType.length === 0 : !catalogFilter.itemType — so empty arrays
are considered empty filters.

---

Nitpick comments:
In `@libs/ui-components/src/components/Catalog/EditWizard/EditAppWizard.tsx`:
- Around line 154-160: The code in EditAppWizard's useSubmitCatalogForm callback
throws a translated string when the requested version isn't found; replace that
with an Error object to preserve stack traces and be idiomatic (in the block
inside useSubmitCatalogForm<AppUpdateFormik>(...) where catalogItemVersion is
checked, change throw t('Version {{version}} not found', { version:
values.version }) to throw new Error(t('Version {{version}} not found', {
version: values.version })) so onSubmit/error handlers receive an Error
instance).

In `@libs/ui-components/src/components/Catalog/EditWizard/EditOsWizard.tsx`:
- Around line 20-27: The validateUpdateWizardStep function currently declares an
unused errors parameter; update its signature to either remove the errors
parameter (if not required) or rename it to _errors to indicate intentional
non-use so it satisfies FlightCtlWizardFooter's validateStep callback type;
touch the function validateUpdateWizardStep and any callers expecting that exact
signature and ensure versionStepId and InstallSpecFormik usage remain unchanged.
- Around line 77-86: EditOsWizardProps declares a misspelled and unused prop
"exisingLabels"; rename it to "existingLabels" in the EditOsWizardProps type and
update all usages (props destructuring in the EditOsWizard component and any
callers) to the corrected name, or if it's intentionally unused remove
"exisingLabels" entirely from EditOsWizardProps and from any function/component
signatures and callers; if you keep it but don't use it yet, add a short TODO
comment above the prop in EditOsWizardProps indicating intended future use to
avoid linter distractions.

In `@libs/ui-components/src/components/Catalog/EditWizard/EditWizard.tsx`:
- Around line 100-122: The prop name passed to EditOsWizard is misspelled as
exisingLabels; change it to existingLabels in the JSX (within EditWizard where
EditOsWizard is rendered) and update the EditOsWizard component's prop
interface/type (e.g., EditOsWizardProps) to rename exisingLabels →
existingLabels, then search for any other references/usages of exisingLabels in
the codebase and update them to existingLabels to keep types and consumers
consistent.

In `@libs/ui-components/src/components/Catalog/EditWizard/steps/UpdateGraph.tsx`:
- Around line 122-127: The forEach callback on directUpgradeEntries implicitly
returns Map.set(), triggering the static analysis warning; change the callback
to an explicit block-style arrow function or replace forEach with a for..of loop
so the Map is updated without an implicit return—modify the code that constructs
entriesMap (using entriesMap, directUpgradeEntries, currentVersionEntry,
allEntries) to call entriesMap.set(...) inside a statement block (e.g.,
directUpgradeEntries.forEach(entry => { entriesMap.set(...); })) or use for
(const entry of directUpgradeEntries) { entriesMap.set(...); }.
- Around line 92-103: The switch in customComponentFactory is masking a real
type mismatch by using the `as never` cast on VersionNode; remove the `as never`
and fix the types so VersionNode satisfies ComponentFactory's expected return
type. Inspect the ComponentFactory return signature and the props/ComponentType
of VersionNode (and any HOC wrappers like withPanZoom()/withSelection()) and
either (a) adjust VersionNode's exported component type/props to match
ComponentFactory, (b) wrap VersionNode with the proper HOC that yields the
expected ComponentFactory type, or (c) use a precise cast to the correct
ComponentFactory return type (not never) so the compiler understands the
component is compatible. Ensure changes reference customComponentFactory,
ComponentFactory, ModelKind, and VersionNode so future reviewers can find and
verify the fix.

In `@libs/ui-components/src/components/Catalog/InstalledSoftware.tsx`:
- Line 251: Remove the unnecessary defensive fallbacks for metadata.name on
API-returned resources: replace usages like osItem.metadata.name || '' and
app.item.metadata.name || '' with the direct property (osItem.metadata.name and
app.item.metadata.name) in the onEdit/onEditOs/onEditApp calls and any similar
callbacks (e.g., the lines using onEditOs, onEditApp, and wherever osItem or
app.item metadata is passed); ensure the props/functions onEdit and related
handlers accept the non-null string as before and adjust no other logic.
- Around line 48-61: Two near-duplicate components UpdateAppColumn and
UpdateOsColumn should be consolidated: create a single GenericUpdateColumn (or
UpdateColumn) that accepts props matching UpdateColumnProps but with a generic
onEdit callback (e.g., onEdit: () => void) instead of onEditApp/onEditOs; reuse
useTranslation, getUpdates(catalogItem, channel, catalogItemVersion.version),
and the ArrowCircleUpIcon/Button rendering logic inside that component, then
replace usages of UpdateAppColumn and UpdateOsColumn with the unified component
passing the appropriate onEdit handler from callers.
- Around line 179-199: The effect in InstalledSoftware re-fetches whenever the
apps array reference changes; to avoid redundant calls, add a stability guard
inside the useEffect: keep a useRef(prevApps) and perform a shallow/deep
equality check (compare key fields like catalog,item,name or JSON.stringify)
between prevApps.current and apps, and early-return if they are equal; only
update prevApps.current and proceed to run the async fetch when the contents
actually differ, then setAppItems and setAppsLoading as before. Reference the
InstalledSoftware component, the useEffect block, the apps dependency, and the
setAppItems/setAppsLoading state updates when implementing this guard.

In
`@libs/ui-components/src/components/Catalog/InstallWizard/UpdateSuccessPage.tsx`:
- Around line 22-35: The UpdateSuccessPage duplicates the same EmptyState
title/body markup as UpdateSuccessPageContent; refactor UpdateSuccessPage to
render UpdateSuccessPageContent (pass isDevice and children) instead of
duplicating the EmptyState JSX so title/text changes are centralized in
UpdateSuccessPageContent, and remove the duplicated EmptyState markup in
UpdateSuccessPage (also apply the same change for the other similar component
instance that mirrors this markup).

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7854c04 and bca3878.

📒 Files selected for processing (23)
  • apps/ocp-plugin/console-extensions.json
  • apps/ocp-plugin/package.json
  • apps/ocp-plugin/src/components/Catalog/CatalogEditDeviceWizard.tsx
  • apps/ocp-plugin/src/components/Catalog/CatalogEditFleetWizard.tsx
  • apps/standalone/src/app/routes.tsx
  • libs/i18n/locales/en/translation.json
  • libs/ui-components/src/components/Catalog/CatalogPage.tsx
  • libs/ui-components/src/components/Catalog/EditWizard/EditAppWizard.tsx
  • libs/ui-components/src/components/Catalog/EditWizard/EditOsWizard.tsx
  • libs/ui-components/src/components/Catalog/EditWizard/EditWizard.tsx
  • libs/ui-components/src/components/Catalog/EditWizard/steps/ReviewStep.tsx
  • libs/ui-components/src/components/Catalog/EditWizard/steps/UpdateGraph.css
  • libs/ui-components/src/components/Catalog/EditWizard/steps/UpdateGraph.tsx
  • libs/ui-components/src/components/Catalog/EditWizard/steps/UpdateStep.tsx
  • libs/ui-components/src/components/Catalog/EditWizard/types.ts
  • libs/ui-components/src/components/Catalog/InstallWizard/UpdateSuccessPage.tsx
  • libs/ui-components/src/components/Catalog/InstallWizard/steps/AppConfigStep.tsx
  • libs/ui-components/src/components/Catalog/InstalledSoftware.tsx
  • libs/ui-components/src/components/Catalog/ResourceCatalog/ResourceCatalogPage.tsx
  • libs/ui-components/src/components/Device/DeviceDetails/DeviceDetailsCatalog.tsx
  • libs/ui-components/src/components/Fleet/FleetDetails/FleetDetailsCatalog.tsx
  • libs/ui-components/src/hooks/useAppContext.tsx
  • libs/ui-components/src/hooks/useNavigate.tsx
✅ Files skipped from review due to trivial changes (1)
  • libs/ui-components/src/components/Catalog/EditWizard/steps/UpdateGraph.css
🚧 Files skipped from review as they are similar to previous changes (7)
  • libs/ui-components/src/components/Catalog/ResourceCatalog/ResourceCatalogPage.tsx
  • libs/ui-components/src/hooks/useNavigate.tsx
  • libs/ui-components/src/hooks/useAppContext.tsx
  • apps/ocp-plugin/console-extensions.json
  • apps/ocp-plugin/package.json
  • apps/standalone/src/app/routes.tsx
  • libs/ui-components/src/components/Fleet/FleetDetails/FleetDetailsCatalog.tsx

Comment on lines +235 to +261
const controller = React.useMemo(() => {
const newController = new Visualization();
newController.registerComponentFactory(customComponentFactory);
newController.registerLayoutFactory(customLayoutFactory);
newController.addEventListener(GRAPH_LAYOUT_END_EVENT, () => {
newController.getGraph().fit(80);
});
newController.addEventListener(SELECTION_EVENT, (ids: string[]) => {
const selectedId = ids[0];
if (selectedId) {
const node = newController.getNodeById(selectedId);
if (node) {
const data = node.getData() as VersionNodeData | undefined;
if (data?.entryName) {
onSelectionChange(selectedId, data.entryName);
}
}
}
});

const model = buildTopologyModel(currentVersion, updates, currentChannel);
newController.fromModel(model, false);

return newController;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentChannel]);

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

useMemo dependencies may cause stale graph when currentVersion or updates change.

The controller is memoized only on currentChannel, but buildTopologyModel also depends on currentVersion and updates. If these props change without currentChannel changing, the graph won't rebuild with the new data.

🔧 Suggested fix
     const model = buildTopologyModel(currentVersion, updates, currentChannel);
     newController.fromModel(model, false);

     return newController;
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [currentChannel]);
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [currentChannel, currentVersion, updates]);

Alternatively, if recreating the controller is expensive, consider using a useEffect to update the model when currentVersion or updates change:

React.useEffect(() => {
  const model = buildTopologyModel(currentVersion, updates, currentChannel);
  controller.fromModel(model, false);
}, [controller, currentVersion, updates, currentChannel]);
📝 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 controller = React.useMemo(() => {
const newController = new Visualization();
newController.registerComponentFactory(customComponentFactory);
newController.registerLayoutFactory(customLayoutFactory);
newController.addEventListener(GRAPH_LAYOUT_END_EVENT, () => {
newController.getGraph().fit(80);
});
newController.addEventListener(SELECTION_EVENT, (ids: string[]) => {
const selectedId = ids[0];
if (selectedId) {
const node = newController.getNodeById(selectedId);
if (node) {
const data = node.getData() as VersionNodeData | undefined;
if (data?.entryName) {
onSelectionChange(selectedId, data.entryName);
}
}
}
});
const model = buildTopologyModel(currentVersion, updates, currentChannel);
newController.fromModel(model, false);
return newController;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentChannel]);
const controller = React.useMemo(() => {
const newController = new Visualization();
newController.registerComponentFactory(customComponentFactory);
newController.registerLayoutFactory(customLayoutFactory);
newController.addEventListener(GRAPH_LAYOUT_END_EVENT, () => {
newController.getGraph().fit(80);
});
newController.addEventListener(SELECTION_EVENT, (ids: string[]) => {
const selectedId = ids[0];
if (selectedId) {
const node = newController.getNodeById(selectedId);
if (node) {
const data = node.getData() as VersionNodeData | undefined;
if (data?.entryName) {
onSelectionChange(selectedId, data.entryName);
}
}
}
});
const model = buildTopologyModel(currentVersion, updates, currentChannel);
newController.fromModel(model, false);
return newController;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentChannel, currentVersion, updates]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/ui-components/src/components/Catalog/EditWizard/steps/UpdateGraph.tsx`
around lines 235 - 261, The controller is currently created inside a
React.useMemo keyed only by currentChannel, causing stale visuals when
currentVersion or updates change; update the logic so the Visualization
controller still memoizes on creation (Visualization, customComponentFactory,
customLayoutFactory) but apply model updates in a React.useEffect that rebuilds
the model via buildTopologyModel(currentVersion, updates, currentChannel) and
calls controller.fromModel(model, false) whenever controller, currentVersion,
updates, or currentChannel change; keep existing event listeners
(GRAPH_LAYOUT_END_EVENT, SELECTION_EVENT) on the Visualization instance and
remove currentVersion/updates from the stale dependency gap.

@rawagner rawagner force-pushed the catalog branch 6 times, most recently from 466ef1a to d5db25b Compare March 3, 2026 09:22
),
},
{
path: 'catalog/:fleetId/:catalogId/:itemId',
Copy link
Collaborator

Choose a reason for hiding this comment

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

The pattern here is a bit weird as for all the other fleet routes we have
fleet/<fleetId>/subpath

I assume this is necessary because we want to have this page not go via the usual FleetDetails route, so it's fine.

</Icon>
</GridItem>
<GridItem span={5}>
{updates.length ? (
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can be done later, but I think it should be possible for users to rollback to a previous version.
Now, if they're at the latest version, then they just see "Up to date" without an option to change it.

}

if (categories.length === 1) {
parts.push(`spec.category==${categories[0]}`);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit: I think it should work with the plural form even if only one category is selected, and same for the other filters.

flightctl get catalogitems --field-selector="spec.category in (aplication)" 

@rawagner rawagner merged commit 65deb96 into flightctl:main Mar 3, 2026
6 checks passed
celdrake pushed a commit to celdrake/flightctl-ui that referenced this pull request Mar 4, 2026
celdrake added a commit that referenced this pull request Mar 4, 2026
(cherry picked from commit 65deb96)

Authored-by: Rastislav Wagner <rawagner@redhat.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants