Skip to content

Conversation

@celdrake
Copy link
Collaborator

@celdrake celdrake commented Jan 20, 2026

Not implemented yet due to missing API

  • Ability to configure a user for the image
  • Ability to view the logs.

Summary by CodeRabbit

  • New Features

    • Full Image Builds UI: list, details, create/edit wizard, retry/duplicate, single & mass delete; export management and downloads (VMDK, QCOW2, ISO).
    • Wizard improvements: multi-step flow (source, output, registration, review), OCI registries context, form validation, edit mode.
  • Configuration

    • Added Image Builder API server routing and environment variable for proxying.
  • Documentation

    • Extensive i18n/localization added for image-build workflows and UI text.
  • UX

    • Table expand-row support; YAML editor and status displays now include ImageBuild resources.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 20, 2026

Warning

Rate limit exceeded

@celdrake has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 19 minutes and 57 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

Walkthrough

Adds end-to-end Image Build support: generated imagebuilder OpenAPI types and generation tooling, new frontend pages/components/wizard/hooks/contexts for ImageBuild/ImageExport, routing and proxy wiring to an imagebuilder backend, repository/OCI integrations, translations, and related UI/type adjustments.

Changes

Cohort / File(s) Summary
TypeDefs: ImageBuilder
libs/types/imagebuilder/index.ts, libs/types/imagebuilder/models/*, libs/types/package.json
New generated imagebuilder types (ImageBuild, ImageExport, enums, lists, etc.) and package export for imagebuilder types.
OpenAPI tooling
libs/types/scripts/openapi-typescript.js, libs/types/scripts/openapi-utils.js
Multi-mode OpenAPI generation added; FS helpers (rimraf, copyDir, fixImagebuilderCoreReferences) and multi-target output handling.
Frontend: ImageBuild UI
libs/ui-components/src/components/ImageBuilds/**, apps/ocp-plugin/src/components/ImageBuilds/*, apps/standalone/src/app/routes.tsx, apps/ocp-plugin/console-extensions.json, apps/ocp-plugin/package.json
New pages, wizard (Source/Output/Registration/Review), details, exports gallery, row/table components, delete/mass-delete modals, OCI registries context, hooks (useImageBuilds, useEditImageBuild), utilities, CSS, and OCP plugin exposures/routes.
API routing & proxy
apps/ocp-plugin/src/utils/apiCalls.ts, apps/standalone/src/app/utils/apiCalls.ts, proxy/bridge/handler.go, proxy/app.go, proxy/config/config.go
getFullApiUrl updated to route imagebuilds/imageexports to imagebuilder backend; proxy route/handler and config entry added for imagebuilder API.
Repository select & forms
libs/ui-components/src/components/form/RepositorySelect.tsx, libs/ui-components/src/components/form/FormSelect.tsx, libs/ui-components/src/components/modals/CreateRepositoryModal/*
New RepositorySelect with validation and create flow; SelectItem supports isDisabled; CreateRepositoryModal API changed to accept RepoSpecType and builds form options internally.
Repository status & helpers
libs/ui-components/src/components/Status/RepositoryStatus.tsx, libs/ui-components/src/utils/status/repository.ts, libs/ui-components/src/utils/imageBuilds.ts
RepositoryStatus now accepts a repository and derives status internally; added isAccessibleRepository and image-build helpers (image references, export/download parsing).
Types & integration
libs/ui-components/src/types/extraTypes.ts, libs/ui-components/src/utils/api.ts, libs/ui-components/src/components/common/CodeEditor/YamlEditor.tsx, libs/types/models/*, libs/types/index.ts, libs/types/package.json
Added GenericCondition/Kind unions, ImageBuildWithExports type, extended ApiList and getCondition signatures, YAML editor support for ImageBuild, ConditionBase extraction, and package export adjustments.
Hooks, context & routes
libs/ui-components/src/components/ImageBuilds/*, libs/ui-components/src/hooks/*.tsx, apps/standalone/src/app/routes.tsx, apps/ocp-plugin/src/components/AppContext/AppContext.tsx, libs/ansible/src/const.ts
Added OciRegistriesContext, useImageBuilds/useImageBuild hooks, and registered new image build routes in multiple app manifests.
Wizard plumbing
libs/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/**
Full wizard implementation: steps (Source/Output/Registration/Review), footer, validators, utils, edit hook, form types, and submission flow creating ImageBuild and optional ImageExports.
UI/UX & misc components
libs/ui-components/src/components/* (Table, FormSelect, EventsCard, DetailsPage, ImageUrl, ImageExportCards, etc.)
Various UI updates: isExpandable table support, FormSelect item disabled state, EventsCard kind type change, DetailsPage resourceType extension, ImageUrl component, status displays, CSS, and small prop/type signature tweaks.
i18n & text
libs/i18n/locales/en/translation.json
~100 new translation keys for image build workflow, validations, statuses, dialogs, and export flows.
Env / config & scripts
CONFIGURATION.md, apps/standalone/scripts/setup_env.sh, proxy/config/config.go
Documented and exported FLIGHTCTL_IMAGEBUILDER_SERVER in setup script and config entry FctlImageBuilderApiUrl added.
Utilities & constants
libs/ui-components/src/constants.ts, libs/ui-components/src/utils/time.ts
Added CERTIFICATE_VALIDITY_IN_YEARS constant and showSpinnerBriefly(time) utility.
Build / tsconfig adjustments
apps/ocp-plugin/tsconfig.json, apps/standalone/tsconfig.json, libs/ui-components/tsconfig.json
Added path aliases for @flightctl/types/imagebuilder and adjusted tsconfig options.
Small fixes & refactors
assorted (cypress, repo list/status changes, form tweaks, minor type updates)
Minor formatting, prop type refinements, refactoring of condition types (ConditionBase extraction), and cleanup.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant UI as Browser UI
participant Frontend as Frontend Client
participant Proxy as FlightCtl Proxy
participant IB as ImageBuilder Backend
Note over UI,IB: Image build creation / export / download flow
UI->>Frontend: POST /api/imagebuilds or POST /api/imageexports
Frontend->>Proxy: Request proxied to /imagebuilder/... (uiProxyAPI/imagebuilder)
Proxy->>IB: Forward request over TLS to ImageBuilder API
IB-->>Proxy: Response (created resource / export status / redirect URL)
Proxy-->>Frontend: Forward response
Frontend-->>UI: Return result to UI (resource created, export status, or download redirect/blob)
alt Download redirect
UI->>UI: Open temporary link -> browser initiates download
else Blob response
UI->>UI: Save received blob as file
end

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and specifically describes the main feature being added: a UI for image building to support device enrollment, which aligns with the extensive changes to add image building UI components, routes, and related infrastructure throughout the codebase.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.

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


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.

@celdrake celdrake force-pushed the EDM-2611-image-building branch 7 times, most recently from 3087310 to 8e616a6 Compare January 22, 2026 09:23
@celdrake celdrake force-pushed the EDM-2611-image-building branch from 8e616a6 to 5135c9c Compare January 22, 2026 09:26
@celdrake celdrake marked this pull request as ready for review January 22, 2026 09:32
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: 17

Caution

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

⚠️ Outside diff range comments (2)
libs/ui-components/src/components/form/FormSelect.tsx (1)

40-45: Avoid auto-selecting a disabled-only option.

Now that per-item isDisabled exists, the single-item auto-select (Line 40-45) can pick an option that should be unselectable. Consider skipping auto-select when the lone item is disabled.

🛠️ Suggested fix
 React.useEffect(() => {
   const hasOneItem = itemKeys.length === 1;
   if (hasOneItem && !field.value) {
-    setValue(itemKeys[0], true);
+    const onlyItem = items[itemKeys[0]];
+    const onlyItemDisabled = isItemObject(onlyItem) && onlyItem.isDisabled;
+    if (!onlyItemDisabled) {
+      setValue(itemKeys[0], true);
+    }
   }
-}, [itemKeys, field.value, setValue]);
+}, [itemKeys, field.value, setValue, items]);

Also applies to: 100-105

libs/ui-components/src/components/common/CodeEditor/YamlEditor.tsx (1)

20-59: Handle ImageBuild in getResourceEndpoint.

ImageBuild is now a supported YAML resource type, but attempting to save one will throw because the endpoint switch doesn't include a case for it. Even though ImageBuildYaml disables editing by default, the type signature allows ImageBuild to be passed to YamlEditor, creating a type-runtime mismatch.

🐛 Proposed fix
   switch (kind) {
     case ResourceKind.FLEET:
       return `fleets/${resourceName}`;
     case ResourceKind.DEVICE:
       return `devices/${resourceName}`;
     case ResourceKind.REPOSITORY:
       return `repositories/${resourceName}`;
     case ResourceKind.AUTH_PROVIDER:
       return `authproviders/${resourceName}`;
+    case ResourceKind.IMAGE_BUILD:
+      return `imagebuilds/${resourceName}`;
     default:
       throw new Error(`Unsupported resource kind: ${kind}`);
   }
🤖 Fix all issues with AI agents
In `@libs/types/scripts/openapi-typescript.js`:
- Around line 49-52: The fetch result for swaggerUrl isn't validated; update the
code after the fetch (where response is assigned) to check response.ok and throw
or log a clear error containing response.status and response.statusText along
with swaggerUrl and mode before attempting response.text(), so that non-2xx
responses (e.g., 404) produce an immediate, informative failure; ensure the
thrown/logged message references response, swaggerUrl and mode to aid debugging.

In
`@libs/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/steps/OutputImageStep.tsx`:
- Around line 90-95: Fix the minor punctuation in the helper text inside
OutputImageStep: in the TextField with name "destination.tag" (helperText prop),
change "Specify the version (e.g, latest or 9.6)" to use correct punctuation —
e.g., "Specify the version (e.g., latest or 9.6)". Ensure you update the
helperText string in the OutputImageStep component where FormGroup and TextField
are defined.
- Around line 47-57: The handler assumes values.exportFormats is always defined,
which can cause runtime errors; in handleFormatToggle (and the similar logic
around lines 105-125) read a local fallback like const currentFormats =
values.exportFormats || [] before using spread, filter, includes, or length,
then call setFieldValue with that safe array (when adding,
setFieldValue('exportFormats', [...currentFormats, format]); when removing,
setFieldValue('exportFormats', currentFormats.filter(f => f !== format))); also
ensure any other checks (e.g., includes/length) reference the same fallback
array.

In
`@libs/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/steps/RegistrationStep.tsx`:
- Around line 35-36: The late-binding radio/checkbox should be checked by
explicitly comparing values.bindingType to BindingType.BindingTypeLate rather
than using !isEarlyBindingSelected; update the logic where
isEarlyBindingSelected is used (and the corresponding checked prop between lines
around 35 and 120-125) to use an explicit comparison (e.g., const
isLateBindingSelected = values.bindingType === BindingType.BindingTypeLate) and
set the checked prop based on that explicit boolean so the late option is not
shown selected when bindingType is unset.

In
`@libs/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/steps/ReviewStep.tsx`:
- Around line 185-206: Remove the redundant nested StackItem wrapper around the
Alert in the ReviewStep component: locate the JSX block where a StackItem
contains another StackItem (the inner one wrapping the Alert and export error
List generated from error.errors.map) and remove the inner StackItem so the
Alert is a direct child of the outer StackItem; keep the Alert, Content, List,
and ListItem mapping (including getExportFormatLabel and getErrorMessage usage)
intact to preserve behavior and keys.

In
`@libs/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/steps/SourceImageStep.tsx`:
- Around line 19-25: The isSourceImageStepValid function currently treats any
truthy errors.source as potentially valid; update it so the step is valid only
when there are no source errors at all: if errors.source is falsy return true;
if errors.source is a string (parent-level error) return false; otherwise (when
source is an object) return true only if source.repository, source.imageName,
and source.imageTag are all falsy. Use the isSourceImageStepValid function and
the errors.source, repository, imageName, imageTag identifiers to locate and
implement this logic.

In
`@libs/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/useEditImageBuild.ts`:
- Around line 14-34: The effect in useEditImageBuild.ts currently skips refetch
when imageBuild is already set, causing stale data; update the React.useEffect
to always fetch whenever imageBuildId changes: remove the "!imageBuild" guard so
the effect runs on imageBuildId truthy, and when imageBuildId becomes falsy
reset imageBuild, error, and isLoading state. Add request cancellation using an
AbortController (or similar) inside the effect to cancel previous in-flight get
requests and avoid race conditions; pass the abort signal to get, ignore
operations when aborted, and ensure setIsLoading(true) before the fetch and
setIsLoading(false) in finally. Keep dependencies (imageBuildId, get) correct
and update setImageBuild via toImageBuildWithExports on success and setError on
failure.

In
`@libs/ui-components/src/components/ImageBuilds/DeleteImageBuildModal/DeleteImageBuildModal.tsx`:
- Around line 60-76: The delete button is currently disabled when an error
exists so users cannot retry; update the Button used in DeleteImageBuildModal
(the Button with key="confirm") to only disable based on isDeleting (remove the
!!error check from isDisabled) so retries are allowed, keep the existing
setError(undefined) at the start of the onClick handler to clear prior errors,
and leave the error handling in the catch
(setError(getErrorMessage(imageBuildErr)); setIsDeleting(false)) and success
path (onClose(true)) unchanged.

In `@libs/ui-components/src/components/ImageBuilds/ImageBuildAndExportStatus.tsx`:
- Around line 97-99: Remove the no-op self-assignment in the
ImageBuildAndExportStatus component: delete the redundant "else if (message) {
message = message; }" branch (or the entire else-if) and keep only the
meaningful branches that set or use the message variable so there is no dead
code around the message handling logic in ImageBuildAndExportStatus.

In
`@libs/ui-components/src/components/ImageBuilds/ImageBuildDetails/ImageBuildExportsGallery.tsx`:
- Around line 89-99: The code calls getExportDownloadResult(response) which
consumes response.blob() before checking response.ok; update the flow so you
check response.ok (and handle redirect status) before consuming the body: either
move the response.ok / response.status check above the getExportDownloadResult
call in the ImageBuildExportsGallery component (so you only call
getExportDownloadResult when response.ok or redirect), or modify
getExportDownloadResult to inspect response.ok and return an early
error/redirect result without calling response.blob() for non-ok responses;
adjust downstream logic that uses downloadResult (createDownloadLink,
showSpinnerBriefly, saveAs) accordingly so blobs are only read for successful
responses.

In `@libs/ui-components/src/components/ImageBuilds/ImageBuildRow.tsx`:
- Around line 100-102: In ImageBuildRow.tsx the expanded row uses <Tr
isExpanded={isExpanded}> with a <Td colSpan={7}> which doesn't match the table's
9 columns and causes misaligned/short expanded content; update the <Td> in the
expanded row to use colSpan={9} (or compute the column count dynamically) so the
ExpandableRowContent spans the full table width, keeping the isExpanded logic
and structure intact.

In `@libs/ui-components/src/components/ImageBuilds/ImageExportCards.css`:
- Around line 10-12: The CSS variable --pf-v6-c-button--FontSize is currently
set on .fctl-imageexport-card .fctl-imageexport-card__status
.pf-v6-c-button__text which PFv6 ignores; change the selector to target the
button element (e.g. .fctl-imageexport-card .fctl-imageexport-card__status
.pf-v6-c-button) and set --pf-v6-c-button--FontSize: 0.75rem there so the
button's computed font size is applied; update the rule in ImageExportCards.css
accordingly.

In `@libs/ui-components/src/components/ImageBuilds/ImageExportCards.tsx`:
- Around line 43-54: The "View logs" button is rendered without behavior; add an
optional handler to the props and wire it or disable the button when handler is
absent. Update ImageExportFormatCardProps to include onViewLogs?: () => void,
then in the ImageExportCards/ImageExportFormatCard component attach that prop to
the button's onClick (use onViewLogs as the handler) and/or set the button
disabled when onViewLogs is undefined; also apply the same change where the
button is rendered around the lines referenced (the render block at ~174-177) so
the button either invokes onViewLogs or is disabled until the logs view exists.

In
`@libs/ui-components/src/components/modals/massModals/MassDeleteImageBuildModal/MassDeleteImageBuildModal.tsx`:
- Around line 92-99: The ModalHeader title string in MassDeleteImageBuildModal
currently has a stray space before the question mark; update the title passed to
ModalHeader (in MassDeleteImageBuildModal) from t('Delete image builds ?') to
t('Delete image builds?') so the question mark is adjacent to the last word (and
adjust any i18n key if necessary).
- Around line 70-88: The deleteImageBuilds handler leaves previous errors
visible when a retry runs—clear the stale errors at the start of the function by
calling setErrors([]) before setting progress/isDeleting; keep the rest of the
logic (mapping imageBuilds to promises, setProgressTotal(promises.length), await
Promise.allSettled, filtering with isPromiseRejected and calling
onDeleteSuccess) unchanged so new attempts start with a fresh error state.

In `@libs/ui-components/src/hooks/useNavigate.tsx`:
- Around line 32-35: The ansible appRoutes mapping in const.ts is missing the
ROUTE enum entries IMAGE_BUILDS, IMAGE_BUILD_CREATE, IMAGE_BUILD_DETAILS, and
IMAGE_BUILD_EDIT; open the appRoutes object and add keys for ROUTE.IMAGE_BUILDS,
ROUTE.IMAGE_BUILD_CREATE, ROUTE.IMAGE_BUILD_DETAILS, and ROUTE.IMAGE_BUILD_EDIT
with the appropriate path (use '/' as in other apps or the intended route),
ensuring the appRoutes object now contains all ROUTE enum values so type
coverage is satisfied.

In `@libs/ui-components/src/utils/imageBuilds.ts`:
- Around line 125-162: The getRedirectUrl function currently may return an
empty/opaque redirect (response.url) which then passes isValidDownloadUrl after
being resolved against window.location.origin; update getRedirectUrl to only
return response.url when it is non-empty and not an opaque redirect (check
response.type !== 'opaqueredirect' and response.url truthy) and keep the
existing Location header branch; this ensures getExportDownloadResult and
isValidDownloadUrl are not given an empty/opaque URL to validate and prevents
resolving to the app origin.
🧹 Nitpick comments (10)
libs/ui-components/src/components/Repository/RepositoryDetails/RepositoryGeneralDetailsCard.tsx (1)

99-99: Consider removing the redundant conditional check.

The repoDetails ? ternary appears unnecessary since repoDetails is a required prop of type Repository in the DetailsTab component signature (line 74). The component won't render without it.

Suggested simplification
-              {repoDetails ? <RepositoryStatus repository={repoDetails} /> : '-'}
+              <RepositoryStatus repository={repoDetails} />
apps/standalone/scripts/setup_env.sh (2)

66-66: Consider adding conditional service detection for the imagebuilder.

Unlike FLIGHTCTL_CLI_ARTIFACTS_SERVER and FLIGHTCTL_ALERTMANAGER_PROXY which are conditionally set based on service availability (lines 68-88), FLIGHTCTL_IMAGEBUILDER_SERVER is always exported. If the imagebuilder service may not always be deployed, consider adding similar conditional detection logic to avoid configuration errors when the service is unavailable.

Example conditional detection pattern
+# ImageBuilder - get setting from kind cluster, unless it has been configured already
+if [ -z "$ENABLE_IMAGEBUILDER" ]; then
+    ENABLE_IMAGEBUILDER=$(detect_service_setting "ImageBuilder" "flightctl-imagebuilder")
+fi
+export ENABLE_IMAGEBUILDER
+if [ "$ENABLE_IMAGEBUILDER" = "true" ]; then
+    export FLIGHTCTL_IMAGEBUILDER_SERVER="https://$EXTERNAL_IP:8445"
+else
+    unset FLIGHTCTL_IMAGEBUILDER_SERVER
+fi
-export FLIGHTCTL_IMAGEBUILDER_SERVER="https://$EXTERNAL_IP:8445"

95-95: Update the echo to handle optional/disabled state if conditional detection is added.

If you adopt conditional detection for the imagebuilder service, update this line to match the pattern used for other optional services:

-echo "  FLIGHTCTL_IMAGEBUILDER_SERVER=$FLIGHTCTL_IMAGEBUILDER_SERVER" >&2
+echo "  FLIGHTCTL_IMAGEBUILDER_SERVER=${FLIGHTCTL_IMAGEBUILDER_SERVER:-'(disabled)'}" >&2
libs/ui-components/src/components/ImageBuilds/ImageBuildDetails/ImageBuildDetailsPage.tsx (3)

32-32: Initialize modal state explicitly.

useState<boolean>() without an initial value results in undefined, which requires the type to be boolean | undefined. Initialize with false for cleaner typing and to avoid potential truthy/falsy edge cases.

Suggested fix
-  const [isDeleteModalOpen, setIsDeleteModalOpen] = React.useState<boolean>();
+  const [isDeleteModalOpen, setIsDeleteModalOpen] = React.useState(false);

78-82: Remove unnecessary refetch before navigation.

After a successful deletion, refetch() is called before navigating away. Since the user immediately leaves this page, the refetch result is never displayed, making it a wasted network call.

Suggested fix
             onClose={(hasDeleted?: boolean) => {
               if (hasDeleted) {
-                refetch();
                 navigate(ROUTE.IMAGE_BUILDS);
               }
               setIsDeleteModalOpen(false);
             }}

73-73: Consider tracking the TODO for logs implementation.

The logs tab currently renders a placeholder. Consider creating a follow-up issue to track this incomplete feature.

Would you like me to open an issue to track the logs tab implementation?

libs/ui-components/src/components/ImageBuilds/ImageBuildDetails/ImageBuildExportsGallery.tsx (1)

54-55: Consider guarding against undefined metadata.name.

The cast imageBuild.metadata.name as string assumes the name is always defined. If the ImageBuild was somehow created without a name (malformed data), this would pass undefined to getImageExportResource, potentially causing downstream issues.

Suggested defensive check
   const handleExportImage = async (format: ExportFormatType) => {
+    const imageBuildName = imageBuild.metadata.name;
+    if (!imageBuildName) {
+      setError({ format, message: 'Image build name is missing', mode: 'export' });
+      return;
+    }
     setExportingFormat(format);
     setError(null);
     try {
       const imageExport = getImageExportResource(
-        imageBuild.metadata.name as string,
+        imageBuildName,
         imageBuild.spec.destination,
         format,
       );
libs/types/scripts/openapi-utils.js (1)

25-47: Misleading function name: copyDir actually moves files.

The function copies file contents then deletes the source files (Line 43: await fsPromises.unlink(srcPath)), making this a move operation rather than a copy. This could lead to confusion and accidental data loss if callers expect source files to remain intact.

Consider renaming to moveDir or removing the unlink call if copy semantics are intended.

♻️ Suggested rename
-// Recursively copy directory
-async function copyDir(src, dest) {
+// Recursively move directory contents
+async function moveDir(src, dest) {

And update the export:

 module.exports = {
   rimraf,
-  copyDir,
+  moveDir,
   fixImagebuilderCoreReferences,
 };
libs/ui-components/src/components/ImageBuilds/ImageBuildDetails/ImageBuildDetailsTab.tsx (1)

44-46: Consider simplifying the filter with a type guard.

The current approach works but the type assertion can be avoided with a type predicate.

♻️ Optional refinement
-  const existingImageExports = imageBuild.imageExports.filter(
-    (imageExport) => imageExport !== undefined,
-  ) as ImageExport[];
+  const existingImageExports = imageBuild.imageExports.filter(
+    (imageExport): imageExport is ImageExport => imageExport !== undefined,
+  );
libs/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/steps/ReviewStep.tsx (1)

28-29: Consolidate imports from the same module.

Two separate imports from ../../../../utils/imageBuilds can be combined.

♻️ Suggested consolidation
-import { getExpectedOutputImageReference, getSourceImageReference } from '../../../../utils/imageBuilds';
-import { getExportFormatLabel } from '../../../../utils/imageBuilds';
+import {
+  getExpectedOutputImageReference,
+  getExportFormatLabel,
+  getSourceImageReference,
+} from '../../../../utils/imageBuilds';

Comment on lines 59 to 62
const isDisabledStep = (stepId: string | undefined, validStepIds: string[]) => {
if (!stepId) {
return true;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

I see no need to allow stepId to be undefined

Suggested change
const isDisabledStep = (stepId: string | undefined, validStepIds: string[]) => {
if (!stepId) {
return true;
}
const isDisabledStep = (stepId: string, validStepIds: string[]) => {

Copy link
Collaborator

Choose a reason for hiding this comment

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

Actually, can we simplify the implementation ?

const isDisabledStep = (stepId: string, validStepIds: string[]) => {
  const validIndex = validStepIds.indexOf(stepId);
  return validIndex === -1 || (validIndex !== orderedIds.indexOf(stepId));
};

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, this is quite simpler. Using it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

There's a problem with this.
Now in Step1 I can't move to Step2 because the "destionation" field hasn't been filled and therefore is invalid.
So I don't get the chance to fix it and I'm stuck in step 1.

The original implementation disregards errors from future steps which allows for this.

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: 3

🤖 Fix all issues with AI agents
In
`@libs/ui-components/src/components/ImageBuilds/ImageBuildDetails/ImageBuildExportsGallery.tsx`:
- Line 111: The isDisabled logic is wrong because exportingFormat is typed as
ExportFormatType | undefined; change the null comparison in the isDisabled
expression so it checks for undefined (or a truthy value) instead of null. For
example, update the isDisabled assignment (the variable named isDisabled near
exportingFormat and format) to use exportingFormat !== undefined &&
exportingFormat !== format or exportingFormat ? exportingFormat !== format :
false so cards are only disabled when an export format is actually active.

In `@libs/ui-components/src/components/ImageBuilds/ImageBuildRow.tsx`:
- Line 119: The message string in ImageBuildRow (the JSX element
<Content>{t('Build failed. Please retry again.')}</Content>) uses redundant
phrasing "retry again"; update the translated string to a concise form such as
"Build failed. Please retry." or "Build failed. Please try again." and update
the corresponding i18n translation key/value so the t(...) call reflects the new
text; modify the string inside the Content element in ImageBuildRow and ensure
any locale files referencing that key are updated accordingly.

In `@libs/ui-components/src/components/ImageBuilds/ImageExportCards.tsx`:
- Around line 37-41: Change the loose typing of iconMap to use ExportFormatType
as the key type: replace Record<string, React.ReactElement> with
Record<ExportFormatType, React.ReactElement> for the constant iconMap (which
maps ExportFormatType values like 'vmdk', 'qcow2', 'iso' to icons
VirtualMachineIcon, CloudSecurityIcon, ServerGroupIcon) so the compiler enforces
valid keys and prevents accidental invalid lookups.
♻️ Duplicate comments (4)
libs/ui-components/src/components/ImageBuilds/ImageBuildRow.tsx (1)

100-101: Incorrect colSpan value for expanded row content.

The expanded row's Td has colSpan={7}, but the table has 9 columns (select, expand, name, base image, image output, status, export images, date, actions). This causes the expanded content to not span the full table width.

🐛 Proposed fix
       <Tr isExpanded={isExpanded}>
-        <Td colSpan={7}>
+        <Td colSpan={9}>
           <ExpandableRowContent>
libs/ui-components/src/components/ImageBuilds/ImageBuildDetails/ImageBuildExportsGallery.tsx (2)

41-45: Consider using undefined instead of null for error state.

The state initialization uses null, but other similar states in this component (exportingFormat, downloadingFormat) use undefined. For consistency, consider using undefined throughout.

♻️ Suggested change
-  const [error, setError] = React.useState<{
-    format: ExportFormatType;
-    message: string;
-    mode: 'export' | 'download';
-  } | null>(null);
+  const [error, setError] = React.useState<{
+    format: ExportFormatType;
+    message: string;
+    mode: 'export' | 'download';
+  }>();

Then use setError(undefined) instead of setError(null) on lines 52 and 127.


89-99: Check response.ok before consuming the response body.

The getExportDownloadResult function consumes response.blob() for non-redirect responses before this code checks response.ok. For error responses (e.g., 500), the blob is read unnecessarily. Consider checking response.ok before calling getExportDownloadResult, or restructure the utility to handle errors without consuming the body.

♻️ Suggested restructure
-      const downloadResult = await getExportDownloadResult(response);
-      if (downloadResult.type === 'redirect') {
-        createDownloadLink(downloadResult.url);
-        await showSpinnerBriefly(DOWNLOAD_REDIRECT_DELAY);
-      } else if (!response.ok && response.status !== 0) {
-        await showSpinnerBriefly(DOWNLOAD_REDIRECT_DELAY);
-        throw new Error(`Download failed: ${response.status} ${response.statusText}`);
-      } else {
+      // Check for errors before consuming the response body
+      if (!response.ok && response.status !== 0) {
+        await showSpinnerBriefly(DOWNLOAD_REDIRECT_DELAY);
+        throw new Error(`Download failed: ${response.status} ${response.statusText}`);
+      }
+
+      const downloadResult = await getExportDownloadResult(response);
+      if (downloadResult.type === 'redirect') {
+        createDownloadLink(downloadResult.url);
+        await showSpinnerBriefly(DOWNLOAD_REDIRECT_DELAY);
+      } else {
         const defaultFilename = `image-export-${ieName}.${format.toLowerCase()}`;
         saveAs(downloadResult.blob, downloadResult.filename || defaultFilename);
       }
libs/ui-components/src/components/ImageBuilds/ImageExportCards.tsx (1)

174-177: "View logs" button has no action handler.

The button is rendered but does nothing when clicked. Per the PR description, this feature is not implemented due to a missing API. Consider disabling the button with a tooltip explaining the feature is coming soon, or hiding it until the API is available.

♻️ Suggested fix - disable with tooltip
               <FlexItem>
                 {exists ? (
-                  <Button variant="secondary">{t('View logs')}</Button>
+                  <Button variant="secondary" isDisabled>
+                    {t('View logs')}
+                  </Button>
                 ) : (

Or add a Tooltip wrapper to explain why it's disabled.

🧹 Nitpick comments (5)
libs/ui-components/src/components/ImageBuilds/ImageBuildRow.tsx (2)

16-24: Consider renaming prop for clarity.

The prop imageBuild is typed as ImageBuildWithExports, which includes additional fields (imageExports, exportsCount) beyond a standard ImageBuild. Consider renaming to imageBuildWithExports to make it clear this isn't a plain ImageBuild.

♻️ Suggested rename
 type ImageBuildRowProps = {
-  imageBuild: ImageBuildWithExports;
+  imageBuildWithExports: ImageBuildWithExports;
   rowIndex: number;
   onRowSelect: (imageBuild: ImageBuild) => OnSelect;
   isRowSelected: (imageBuild: ImageBuild) => boolean;
   canDelete: boolean;
   onDeleteClick: VoidFunction;
   refetch: VoidFunction;
 };

94-94: Unnecessary template literal.

The string interpolation wrapping a number is redundant here. React handles number rendering directly.

♻️ Suggested simplification
-        <Td dataLabel={t('Export images')}>{`${imageBuild.exportsCount || 0}`}</Td>
+        <Td dataLabel={t('Export images')}>{imageBuild.exportsCount || 0}</Td>
libs/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/CreateImageBuildWizard.tsx (1)

143-151: Consider using reduce for cleaner error aggregation.

A past review suggested refactoring this forEach loop to use reduce for a more functional approach. While the current implementation is correct, reduce would be slightly more idiomatic.

♻️ Optional refactor using reduce
-                  const exportErrors: Array<{ format: ExportFormatType; error: unknown }> = [];
-                  exportResults.forEach((result, index) => {
-                    if (isPromiseRejected(result)) {
-                      exportErrors.push({
-                        format: values.exportFormats[index],
-                        error: result.reason,
-                      });
-                    }
-                  });
+                  const exportErrors = exportResults.reduce((acc, result, index) => {
+                    if (isPromiseRejected(result)) {
+                      acc.push({
+                        format: values.exportFormats[index],
+                        error: result.reason,
+                      });
+                    }
+                    return acc;
+                  }, [] as Array<{ format: ExportFormatType; error: unknown }>);
libs/ui-components/src/components/ImageBuilds/ImageBuildDetails/ImageBuildExportsGallery.tsx (1)

30-37: Consider defensive cleanup in createDownloadLink.

If an error occurs between appendChild and removeChild, the link element could remain in the DOM. Using try/finally ensures cleanup.

♻️ Suggested improvement
 const createDownloadLink = (url: string) => {
   const link = document.createElement('a');
   link.href = url;
   link.style.display = 'none';
   document.body.appendChild(link);
-  link.click();
-  document.body.removeChild(link);
+  try {
+    link.click();
+  } finally {
+    document.body.removeChild(link);
+  }
 };
libs/ui-components/src/components/ImageBuilds/ImageExportCards.tsx (1)

195-208: Consider if toast positioning is appropriate for inline card errors.

AlertGroup isToast typically renders alerts in a fixed position overlay. For per-card errors displayed within the card's footer, a non-toast Alert might provide better context by keeping the error visually attached to the affected card.

♻️ Alternative: Use inline Alert
           {error && (
-            <AlertGroup isToast>
-              <Alert
-                variant="danger"
-                title={
-                  error.mode === 'export' ? t("We couldn't export your image") : t("We couldn't download your image")
-                }
-                actionClose={<AlertActionCloseButton onClose={onDismissError} />}
-              >
-                {t('Something went wrong on our end. Please review the error details and try again.')}
-                <details>{error.message}</details>
-              </Alert>
-            </AlertGroup>
+            <Alert
+              variant="danger"
+              isInline
+              title={
+                error.mode === 'export' ? t("We couldn't export your image") : t("We couldn't download your image")
+              }
+              actionClose={<AlertActionCloseButton onClose={onDismissError} />}
+            >
+              {t('Something went wrong on our end. Please review the error details and try again.')}
+              <details>{error.message}</details>
+            </Alert>
           )}

@celdrake celdrake force-pushed the EDM-2611-image-building branch 2 times, most recently from 147b928 to d988dce Compare January 23, 2026 10:29
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: 3

🤖 Fix all issues with AI agents
In
`@libs/ui-components/src/components/ImageBuilds/ImageBuildDetails/ImageBuildDetailsPage.tsx`:
- Around line 53-64: The actions dropdown is gated by canDelete which hides the
"Duplicate/Retry" item for users who can edit but not delete; update the
conditional rendering so each DropdownItem is shown based on the correct
permission: keep the delete menu item wrapped with canDelete (and calling
setIsDeleteModalOpen), and render the "Duplicate/Retry" item when the user has
edit/create permission (e.g., a new canEdit or canCreate flag) which uses
navigate({ route: ROUTE.IMAGE_BUILD_EDIT, postfix: imageBuildId }) and respects
hasFailed for the label; ensure DetailsPageActions/DropdownList still render
when at least one action is visible.

In `@libs/ui-components/src/components/ImageBuilds/ImageExportCards.tsx`:
- Around line 183-187: The Created date currently calls
getDateDisplay(imageExport?.metadata.creationTimestamp) even when imageExport is
undefined, causing "undefined" to render; update the JSX in ImageExportCards.tsx
to conditionally render the StackItem/Content block (the Created: ... line using
ContentVariants.small) only when imageExport (or
imageExport?.metadata.creationTimestamp) is present, or provide a safe fallback
(e.g., nothing or a placeholder) before calling getDateDisplay so the helper is
never invoked with undefined.

In
`@libs/ui-components/src/components/modals/massModals/MassDeleteImageBuildModal/MassDeleteImageBuildModal.tsx`:
- Around line 76-79: The delete loop currently calls
remove(`imagebuilds/${imageBuild.metadata.name}`) without guarding metadata.name
which can send imagebuilds/undefined; update the logic in the imageBuilds.map
block (the mapping that calls remove and setProgress) to first read
imageBuild.metadata?.name into a local variable, skip/filter out entries with no
name (or return early from the async iterator) so you only call remove for valid
names, and only call setProgress for items you actually attempted to delete; use
the existing imageBuilds.map / remove / setProgress symbols to locate and change
the code.
♻️ Duplicate comments (1)
libs/ui-components/src/components/ImageBuilds/ImageExportCards.tsx (1)

167-180: "View logs" button is rendered without an action handler.

The button at Line 169 has no onClick handler and will do nothing when clicked. Per the PR description, this feature is not implemented due to a missing API. Consider disabling the button until the functionality is available, or removing it entirely for now.

♻️ Suggested fix - disable until implemented
               <FlexItem>
                 {exists ? (
-                  <Button variant="secondary">{t('View logs')}</Button>
+                  <Button variant="secondary" isDisabled>
+                    {t('View logs')}
+                  </Button>
                 ) : (
🧹 Nitpick comments (7)
libs/ui-components/src/components/modals/massModals/MassDeleteImageBuildModal/MassDeleteImageBuildModal.tsx (2)

125-135: Use explicit boolean check to avoid rendering 0.

The pattern {errors?.length && (...)} can render the literal 0 if errors is ever set to an empty array, since React renders falsy numbers. While the current code only sets errors to undefined or a non-empty array, this is a fragile pattern that could regress.

Suggested fix
-         {errors?.length && (
+         {errors && errors.length > 0 && (

99-101: Consider using a plural-aware translation string.

The message says "this build" (singular), but count: imageBuildsCount is passed and the modal handles multiple builds. For proper i18n/UX, the message should reflect the count:

-             {t('This will remove the record of this build and its history.', { count: imageBuildsCount })}
+             {t('This will remove the record of {{count}} build(s) and their history.', { count: imageBuildsCount })}

Or use separate singular/plural keys per your i18n setup.

libs/ui-components/src/utils/imageBuilds.ts (1)

20-29: Add a default case for exhaustive handling.

The switch statement doesn't handle unknown ExportFormatType values. If a new format is added to the enum, this function will return undefined implicitly, which may cause runtime issues.

♻️ Suggested fix
 export const getExportFormatDescription = (t: TFunction, format: ExportFormatType) => {
   switch (format) {
     case ExportFormatType.ExportFormatTypeVMDK:
       return t('For enterprise virtualization platforms.');
     case ExportFormatType.ExportFormatTypeQCOW2:
       return t('For virtualized edge workloads and OpenShift Virtualization.');
     case ExportFormatType.ExportFormatTypeISO:
       return t('For physical edge devices and bare metal.');
+    default:
+      return t('Unknown format');
   }
 };
libs/ui-components/src/components/ImageBuilds/ImageBuildRow.tsx (1)

80-84: Use functional state update for the expand toggle.

This avoids stale state if multiple toggles are queued or batched.

♻️ Suggested update
-            onToggle: () => setIsExpanded(!isExpanded),
+            onToggle: () => setIsExpanded((prev) => !prev),
libs/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/steps/SourceImageStep.tsx (1)

65-67: Nested FormSection may cause unintended spacing/styling.

There's a FormSection (Line 65) nested inside another FormSection (Line 41). This could result in extra padding/margins. Consider whether ImageUrlCard should be a direct sibling of the other form groups instead.

♻️ Suggested simplification
           </FormGroup>
-          <FormSection>
-            <ImageUrlCard imageReference={imageReference} />
-          </FormSection>
+          <ImageUrlCard imageReference={imageReference} />
         </FormSection>
libs/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/CreateImageBuildWizard.tsx (1)

124-130: Variable imageBuild shadows the hook-returned value.

The local imageBuild (Line 125) shadows the imageBuild from useEditImageBuild() (Line 72). While not a bug since they're in different scopes, consider renaming to newImageBuild or createdImageBuild for clarity.

♻️ Suggested rename
                 try {
-                  const imageBuild = getImageBuildResource(values);
-                  buildName = imageBuild.metadata.name as string;
-                  const createdBuild = await post<ImageBuild>('imagebuilds', imageBuild);
+                  const newImageBuild = getImageBuildResource(values);
+                  buildName = newImageBuild.metadata.name as string;
+                  const createdBuild = await post<ImageBuild>('imagebuilds', newImageBuild);
                   if (createdBuild.metadata.name !== buildName) {
                     throw new Error(t('ImageBuild was created but has a different name'));
                   }
libs/ui-components/src/components/ImageBuilds/ImageBuildDetails/ImageBuildDetailsPage.tsx (1)

73-73: Logs tab placeholder acknowledged.

The PR description mentions logs are not implemented due to a missing API. Consider adding a more user-friendly placeholder message instead of "TODO Logs" that explains the feature is coming soon.

Would you like me to suggest a placeholder component that displays a user-friendly message, or open an issue to track this TODO?

@celdrake celdrake force-pushed the EDM-2611-image-building branch 2 times, most recently from 2282ac5 to f1036f7 Compare January 23, 2026 10:54
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: 1

🤖 Fix all issues with AI agents
In `@libs/i18n/locales/en/translation.json`:
- Around line 800-846: Standardize terminology and casing: replace inconsistent
keys/strings so "repository" vs "registry" and "ImageBuild" vs "Image build" are
consistent across the file—use "repository" everywhere (change error keys
"Source registry is required" and "Target registry is required" to "Source
repository is required" and "Target repository is required"), and rename the
string "ImageBuild was created but has a different name" to "Image build was
created but has a different name" to match other keys like "Failed to create
image build" and "Image build created, but some exports failed"; update any
corresponding usages of these message keys in code to the new key names if
necessary (look for references to "Source repository", "Target repository",
"ImageBuild was created but has a different name", "Failed to create image
build", and "Image build created, but some exports failed").
♻️ Duplicate comments (1)
libs/ui-components/src/components/ImageBuilds/ImageExportCards.tsx (1)

167-179: "View logs" button lacks an action handler.

The button renders but performs no action. Since the PR notes this feature isn't implemented due to a missing API, consider either disabling the button or adding a tooltip explaining the limitation until the API is available.

🧹 Nitpick comments (2)
libs/ui-components/src/components/ImageBuilds/ImageExportCards.tsx (1)

188-200: Consider removing isToast for inline error display.

AlertGroup with isToast renders alerts as floating notifications at a fixed viewport position, which conflicts with the inline CardFooter placement. If multiple cards have errors, their toasts would overlap at the same position.

For inline error display within the card, remove isToast and use a plain Alert:

♻️ Suggested change for inline error display
           {error && (
-            <AlertGroup isToast>
-              <Alert
-                variant="danger"
-                title={
-                  error.mode === 'export' ? t("We couldn't export your image") : t("We couldn't download your image")
-                }
-                actionClose={<AlertActionCloseButton onClose={onDismissError} />}
-              >
-                {t('Something went wrong on our end. Please review the error details and try again.')}
-                <details>{error.message}</details>
-              </Alert>
-            </AlertGroup>
+            <Alert
+              variant="danger"
+              title={
+                error.mode === 'export' ? t("We couldn't export your image") : t("We couldn't download your image")
+              }
+              actionClose={<AlertActionCloseButton onClose={onDismissError} />}
+            >
+              {t('Something went wrong on our end. Please review the error details and try again.')}
+              <details>{error.message}</details>
+            </Alert>
           )}
libs/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/steps/SourceImageStep.tsx (1)

65-67: Consider removing the nested FormSection for consistency.

The inner FormSection wrapping only ImageUrlCard creates unnecessary DOM nesting. In OutputImageStep.tsx (line 97), ImageUrlCard is placed directly within the parent FormSection without additional wrapping.

♻️ Suggested change
           </FormGroup>
-          <FormSection>
-            <ImageUrlCard imageReference={imageReference} />
-          </FormSection>
+          <ImageUrlCard imageReference={imageReference} />
         </FormSection>

@celdrake celdrake force-pushed the EDM-2611-image-building branch from f1036f7 to 1dbd56a Compare January 23, 2026 10:59
@celdrake celdrake merged commit 6d77c7c into flightctl:main Jan 23, 2026
6 checks passed
@celdrake celdrake deleted the EDM-2611-image-building branch January 23, 2026 12:52
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