diff --git a/libs/i18n/locales/en/translation.json b/libs/i18n/locales/en/translation.json
index c7a823d4d..03c3d90df 100644
--- a/libs/i18n/locales/en/translation.json
+++ b/libs/i18n/locales/en/translation.json
@@ -388,7 +388,7 @@
"Delete values file": "Delete values file",
"Fill in the existing values files before you can add more values files.": "Fill in the existing values files before you can add more values files.",
"Add values file": "Add values file",
- "Order of precedence: Files are applied in the ordered listed. If the same parameter is defined in multiple files, the value in the last file takes precedence and overrides previous values.": "Order of precedence: Files are applied in the ordered listed. If the same parameter is defined in multiple files, the value in the last file takes precedence and overrides previous values.",
+ "Order of precedence: Files are applied in the order listed. If the same parameter is defined in multiple files, the value in the last file takes precedence and overrides previous values.": "Order of precedence: Files are applied in the order listed. If the same parameter is defined in multiple files, the value in the last file takes precedence and overrides previous values.",
"Inline values": "Inline values",
"Provide a valid YAML file": "Provide a valid YAML file",
"Enter configuration values in YAML format to be applied to the Helm chart. These values take precedence over those defined in the files listed above.": "Enter configuration values in YAML format to be applied to the Helm chart. These values take precedence over those defined in the files listed above.",
@@ -837,6 +837,12 @@
"{{ validityPeriod }} years (Standard)_other": "{{ validityPeriod }} years (Standard)",
"Late binding": "Late binding",
"No additional user input required (cloud-init and ignition are enabled automatically)": "No additional user input required (cloud-init and ignition are enabled automatically)",
+ "Remote access": "Remote access",
+ "Provide an SSH public key to enable passwordless login once your image is deployed.": "Provide an SSH public key to enable passwordless login once your image is deployed.",
+ "Username": "Username",
+ "The username for the user account": "The username for the user account",
+ "SSH public key": "SSH public key",
+ "Paste the content of an SSH public key you want to use to connect to the device.": "Paste the content of an SSH public key you want to use to connect to the device.",
"Base image": "Base image",
"Source repository": "Source repository",
"Image reference URL": "Image reference URL",
@@ -845,6 +851,7 @@
"Image output reference URL": "Image output reference URL",
"Binding type": "Binding type",
"Cloud-init and ignition are enabled automatically": "Cloud-init and ignition are enabled automatically",
+ "Enabled for <1>{remoteAccessUsername}1>": "Enabled for <1>{remoteAccessUsername}1>",
"Failed to create image build": "Failed to create image build",
"Image build created, but some exports failed": "Image build created, but some exports failed",
"The image build \"{{buildName}}\" was created successfully, however the following export(s) failed:": "The image build \"{{buildName}}\" was created successfully, however the following export(s) failed:",
@@ -876,6 +883,17 @@
"Build information": "Build information",
"Build status": "Build status",
"Source image": "Source image",
+ "Virtualization (VMDK)": "Virtualization (VMDK)",
+ "OpenStack/KVM (QCOW2)": "OpenStack/KVM (QCOW2)",
+ "OpenShift Virtualization (QCOW2)": "OpenShift Virtualization (QCOW2)",
+ "Metal installer (ISO)": "Metal installer (ISO)",
+ "In progress": "In progress",
+ "Completed": "Completed",
+ "Failed to load logs": "Failed to load logs",
+ "For built and failed export tasks, only the last 500 lines are available.": "For built and failed export tasks, only the last 500 lines are available.",
+ "Export image": "Export image",
+ "Export tasks logs are only available after the image build is completed": "Export tasks logs are only available after the image build is completed",
+ "Image build logs": "Image build logs",
"Image builds cannot be edited. Use Retry to create a new image build based on this one.": "Image builds cannot be edited. Use Retry to create a new image build based on this one.",
"Date": "Date",
"Build failed. Please retry.": "Build failed. Please retry.",
@@ -886,7 +904,6 @@
"Image builds table": "Image builds table",
"Downloading...": "Downloading...",
"View logs": "View logs",
- "Export image": "Export image",
"Created: {{date}}": "Created: {{date}}",
"We couldn't export your image": "We couldn't export your image",
"We couldn't download your image": "We couldn't download your image",
@@ -1036,7 +1053,6 @@
"Failed to retrieve the resource syncs": "Failed to retrieve the resource syncs",
"The repository cannot be modified at the moment because some of its details could not be obtained.": "The repository cannot be modified at the moment because some of its details could not be obtained.",
"Basic authentication": "Basic authentication",
- "Username": "Username",
"Password": "Password",
"Skip server verification": "Skip server verification",
"CA certificate": "CA certificate",
@@ -1199,8 +1215,10 @@
"Device decommissioning already started.": "Device decommissioning already started.",
"Device is not suspended.": "Device is not suspended.",
"For enterprise virtualization platforms.": "For enterprise virtualization platforms.",
- "For virtualized edge workloads and OpenShift Virtualization.": "For virtualized edge workloads and OpenShift Virtualization.",
+ "For virtualized edge workloads.": "For virtualized edge workloads.",
+ "For OpenShift Virtualization.": "For OpenShift Virtualization.",
"For physical edge devices and bare metal.": "For physical edge devices and bare metal.",
+ "QCOW2 (Container)": "QCOW2 (Container)",
"Error": "Error",
"Degraded": "Degraded",
"No applications": "No applications",
@@ -1208,7 +1226,6 @@
"Preparing": "Preparing",
"Starting": "Starting",
"Running": "Running",
- "Completed": "Completed",
"Rebooting": "Rebooting",
"Powered Off": "Powered Off",
"Online": "Online",
diff --git a/libs/types/imagebuilder/index.ts b/libs/types/imagebuilder/index.ts
index 481579269..fbf8cebd9 100644
--- a/libs/types/imagebuilder/index.ts
+++ b/libs/types/imagebuilder/index.ts
@@ -17,6 +17,7 @@ export type { ImageBuildRefSource } from './models/ImageBuildRefSource';
export type { ImageBuildSource } from './models/ImageBuildSource';
export type { ImageBuildSpec } from './models/ImageBuildSpec';
export type { ImageBuildStatus } from './models/ImageBuildStatus';
+export type { ImageBuildUserConfiguration } from './models/ImageBuildUserConfiguration';
export type { ImageExport } from './models/ImageExport';
export type { ImageExportCondition } from './models/ImageExportCondition';
export { ImageExportConditionReason } from './models/ImageExportConditionReason';
diff --git a/libs/types/imagebuilder/models/ExportFormatType.ts b/libs/types/imagebuilder/models/ExportFormatType.ts
index 5a396c981..b778b2720 100644
--- a/libs/types/imagebuilder/models/ExportFormatType.ts
+++ b/libs/types/imagebuilder/models/ExportFormatType.ts
@@ -9,4 +9,5 @@ export enum ExportFormatType {
ExportFormatTypeVMDK = 'vmdk',
ExportFormatTypeQCOW2 = 'qcow2',
ExportFormatTypeISO = 'iso',
+ ExportFormatTypeQCOW2DiskContainer = 'qcow2-disk-container',
}
diff --git a/libs/types/imagebuilder/models/ImageBuildSpec.ts b/libs/types/imagebuilder/models/ImageBuildSpec.ts
index 408f59dfe..30c6143ac 100644
--- a/libs/types/imagebuilder/models/ImageBuildSpec.ts
+++ b/libs/types/imagebuilder/models/ImageBuildSpec.ts
@@ -5,6 +5,7 @@
import type { ImageBuildBinding } from './ImageBuildBinding';
import type { ImageBuildDestination } from './ImageBuildDestination';
import type { ImageBuildSource } from './ImageBuildSource';
+import type { ImageBuildUserConfiguration } from './ImageBuildUserConfiguration';
/**
* ImageBuildSpec describes the specification for an image build.
*/
@@ -12,5 +13,6 @@ export type ImageBuildSpec = {
source: ImageBuildSource;
destination: ImageBuildDestination;
binding: ImageBuildBinding;
+ userConfiguration?: ImageBuildUserConfiguration;
};
diff --git a/libs/types/imagebuilder/models/ImageBuildUserConfiguration.ts b/libs/types/imagebuilder/models/ImageBuildUserConfiguration.ts
new file mode 100644
index 000000000..3f34ac24c
--- /dev/null
+++ b/libs/types/imagebuilder/models/ImageBuildUserConfiguration.ts
@@ -0,0 +1,18 @@
+/* generated using openapi-typescript-codegen -- do no edit */
+/* istanbul ignore file */
+/* tslint:disable */
+/* eslint-disable */
+/**
+ * ImageBuildUserConfiguration specifies user configuration for the build.
+ */
+export type ImageBuildUserConfiguration = {
+ /**
+ * The username for the user configuration.
+ */
+ username: string;
+ /**
+ * The public key for the user configuration.
+ */
+ publickey: string;
+};
+
diff --git a/libs/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationHelmForm.tsx b/libs/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationHelmForm.tsx
index 6c01dfd07..ce4f92479 100644
--- a/libs/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationHelmForm.tsx
+++ b/libs/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationHelmForm.tsx
@@ -133,7 +133,7 @@ const ApplicationHelmForm = ({
variant="info"
className="pf-v6-u-mt-md"
title={t(
- 'Order of precedence: Files are applied in the ordered listed. If the same parameter is defined in multiple files, the value in the last file takes precedence and overrides previous values.',
+ 'Order of precedence: Files are applied in the order listed. If the same parameter is defined in multiple files, the value in the last file takes precedence and overrides previous values.',
)}
/>
>
diff --git a/libs/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/steps/OutputImageStep.tsx b/libs/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/steps/OutputImageStep.tsx
index d2a19cecd..8e5f03f13 100644
--- a/libs/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/steps/OutputImageStep.tsx
+++ b/libs/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/steps/OutputImageStep.tsx
@@ -12,10 +12,10 @@ import RepositorySelect from '../../../form/RepositorySelect';
import { usePermissionsContext } from '../../../common/PermissionsContext';
import { RESOURCE, VERB } from '../../../../types/rbac';
import { SelectImageBuildExportCard } from '../../ImageExportCards';
-import { getImageReference } from '../../../../utils/imageBuilds';
+import { getAllExportFormats, getImageReference } from '../../../../utils/imageBuilds';
import { isOciRepoSpec } from '../../../Repository/CreateRepository/utils';
-import ImageUrlCard from '../../ImageUrlCard';
import { useOciRegistriesContext } from '../../OciRegistriesContext';
+import ImageUrlCard from '../../ImageUrlCard';
export const outputImageStepId = 'output-image';
@@ -102,21 +102,14 @@ const OutputImageStep = () => {
)}
-
-
-
+ {getAllExportFormats().map((format) => (
+
+ ))}
{values.exportFormats.length > 0 && (
diff --git a/libs/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/steps/RegistrationStep.tsx b/libs/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/steps/RegistrationStep.tsx
index f0c5cc651..270167b22 100644
--- a/libs/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/steps/RegistrationStep.tsx
+++ b/libs/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/steps/RegistrationStep.tsx
@@ -10,7 +10,9 @@ import {
Divider,
Flex,
FlexItem,
+ FormGroup,
FormSection,
+ Grid,
Radio,
Stack,
StackItem,
@@ -22,6 +24,9 @@ import { BindingType } from '@flightctl/types/imagebuilder';
import { ImageBuildFormValues } from '../types';
import { useTranslation } from '../../../../hooks/useTranslation';
import FlightCtlForm from '../../../form/FlightCtlForm';
+import TextField from '../../../form/TextField';
+import UploadField from '../../../form/UploadField';
+import CheckboxField from '../../../form/CheckboxField';
import { CERTIFICATE_VALIDITY_IN_YEARS } from '../../../../constants';
export const registrationStepId = 'registration';
@@ -46,6 +51,14 @@ const RegistrationStep = () => {
}
};
+ const handleRemoteAccessToggle = (enabled: boolean) => {
+ if (enabled && !values.userConfiguration) {
+ setFieldValue('userConfiguration', { username: '', publickey: '', enabled: true });
+ return;
+ }
+ setFieldValue('userConfiguration.enabled', enabled);
+ };
+
return (
@@ -141,6 +154,30 @@ const RegistrationStep = () => {
+
+
+
+
+
+
+
+
+
+
+
+
+ {t('Paste the content of an SSH public key you want to use to connect to the device.')}
+
+
+
);
};
diff --git a/libs/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/steps/ReviewStep.tsx b/libs/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/steps/ReviewStep.tsx
index 7ee507f53..68bfdb6b0 100644
--- a/libs/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/steps/ReviewStep.tsx
+++ b/libs/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/steps/ReviewStep.tsx
@@ -1,4 +1,5 @@
import * as React from 'react';
+import { Trans } from 'react-i18next';
import {
Alert,
Card,
@@ -53,6 +54,7 @@ const ReviewStep = ({ error }: ReviewStepProps) => {
);
const isEarlyBinding = values.bindingType === BindingType.BindingTypeEarly;
+ const remoteAccessUsername = values.userConfiguration?.enabled ? values.userConfiguration?.username || '' : '';
return (
@@ -117,7 +119,7 @@ const ReviewStep = ({ error }: ReviewStepProps) => {
<>
{values.exportFormats.map((format) => (
))}
>
@@ -172,6 +174,16 @@ const ReviewStep = ({ error }: ReviewStepProps) => {
)}
+ {remoteAccessUsername && (
+
+ {t('Remote access')}
+
+
+ Enabled for {remoteAccessUsername}
+
+
+
+ )}
@@ -196,7 +208,7 @@ const ReviewStep = ({ error }: ReviewStepProps) => {
{error.errors.map(({ format, error: exportError }, index) => (
- {getExportFormatLabel(format)}: {getErrorMessage(exportError)}
+ {getExportFormatLabel(t, format)}: {getErrorMessage(exportError)}
))}
diff --git a/libs/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/types.ts b/libs/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/types.ts
index ab29222f7..6fcd3a37c 100644
--- a/libs/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/types.ts
+++ b/libs/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/types.ts
@@ -1,12 +1,22 @@
-import { BindingType, ImageBuildDestination, ImageBuildSource } from '@flightctl/types/imagebuilder';
+import {
+ BindingType,
+ ImageBuildDestination,
+ ImageBuildSource,
+ ImageBuildUserConfiguration,
+} from '@flightctl/types/imagebuilder';
import { ExportFormatType } from '@flightctl/types/imagebuilder';
+type ImageBuildUserConfigurationForm = ImageBuildUserConfiguration & {
+ enabled?: boolean;
+};
+
export type ImageBuildFormValues = {
// name is autogenereated by us
source: ImageBuildSource;
destination: ImageBuildDestination;
bindingType: BindingType;
exportFormats: ExportFormatType[];
+ userConfiguration?: ImageBuildUserConfigurationForm;
};
export type ImageBuildWizardError =
diff --git a/libs/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/utils.ts b/libs/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/utils.ts
index fdb250add..178c89e18 100644
--- a/libs/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/utils.ts
+++ b/libs/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/utils.ts
@@ -8,8 +8,6 @@ import {
ImageBuildDestination,
ImageBuildSource,
ImageExport,
- ImageExportConditionReason,
- ImageExportConditionType,
ResourceKind,
} from '@flightctl/types/imagebuilder';
import { API_VERSION } from '../../../constants';
@@ -62,9 +60,10 @@ const getImageExportsByFormat = (
return {
imageExports: [
- formatMap[ExportFormatType.ExportFormatTypeVMDK],
- formatMap[ExportFormatType.ExportFormatTypeQCOW2],
formatMap[ExportFormatType.ExportFormatTypeISO],
+ formatMap[ExportFormatType.ExportFormatTypeQCOW2DiskContainer],
+ formatMap[ExportFormatType.ExportFormatTypeQCOW2],
+ formatMap[ExportFormatType.ExportFormatTypeVMDK],
],
exportsCount: imageExports?.length || 0,
};
@@ -90,11 +89,23 @@ export const getInitialValues = (imageBuild?: ImageBuildWithExports): ImageBuild
const exportFormats = imageBuild.imageExports
.filter((ie): ie is ImageExport => ie !== undefined)
.map((imageExport) => imageExport.spec.format);
+ const userConfig = imageBuild.spec.userConfiguration;
+ const userConfiguration = userConfig
+ ? {
+ ...userConfig,
+ enabled: !!(userConfig.username || userConfig.publickey),
+ }
+ : {
+ username: '',
+ publickey: '',
+ enabled: false,
+ };
return {
source: imageBuild.spec.source,
destination: imageBuild.spec.destination,
bindingType: imageBuild.spec.binding.type as BindingType,
exportFormats: exportFormats || [],
+ userConfiguration,
};
}
@@ -111,6 +122,11 @@ export const getInitialValues = (imageBuild?: ImageBuildWithExports): ImageBuild
},
bindingType: BindingType.BindingTypeEarly,
exportFormats: [],
+ userConfiguration: {
+ username: '',
+ publickey: '',
+ enabled: false,
+ },
};
};
@@ -127,19 +143,31 @@ const generateExportName = (imageBuildName: string, format: ExportFormatType) =>
export const getImageBuildResource = (values: ImageBuildFormValues): ImageBuild => {
const name = generateBuildName();
+ const spec: ImageBuild['spec'] = {
+ source: values.source,
+ destination: values.destination,
+ binding: {
+ type: values.bindingType,
+ },
+ };
+
+ // Allow the user to uncheck the toggle without having cleared the fields
+ const username = values.userConfiguration?.username || '';
+ const publickey = values.userConfiguration?.publickey || '';
+ if (values.userConfiguration?.enabled && username && publickey) {
+ spec.userConfiguration = {
+ username,
+ publickey,
+ };
+ }
+
return {
apiVersion: API_VERSION,
kind: ResourceKind.IMAGE_BUILD,
metadata: {
name,
},
- spec: {
- source: values.source,
- destination: values.destination,
- binding: {
- type: values.bindingType,
- },
- },
+ spec,
};
};
@@ -169,17 +197,3 @@ export const getImageExportResources = (values: ImageBuildFormValues, imageBuild
return values.exportFormats.map((format) => getImageExportResource(imageBuildName, format));
};
-
-export const isImageExportFailed = (imageExport: ImageExport): boolean => {
- const readyCondition = imageExport.status?.conditions?.find(
- (c) => c.type === ImageExportConditionType.ImageExportConditionTypeReady,
- );
- return readyCondition?.reason === ImageExportConditionReason.ImageExportConditionReasonFailed;
-};
-
-export const isImageExportCompleted = (imageExport: ImageExport): boolean => {
- const readyCondition = imageExport.status?.conditions?.find(
- (c) => c.type === ImageExportConditionType.ImageExportConditionTypeReady,
- );
- return readyCondition?.reason === ImageExportConditionReason.ImageExportConditionReasonCompleted;
-};
diff --git a/libs/ui-components/src/components/ImageBuilds/ImageBuildDetails/ImageBuildDetailsPage.tsx b/libs/ui-components/src/components/ImageBuilds/ImageBuildDetails/ImageBuildDetailsPage.tsx
index f7f11445b..c7989efb7 100644
--- a/libs/ui-components/src/components/ImageBuilds/ImageBuildDetails/ImageBuildDetailsPage.tsx
+++ b/libs/ui-components/src/components/ImageBuilds/ImageBuildDetails/ImageBuildDetailsPage.tsx
@@ -16,6 +16,7 @@ import { OciRegistriesContextProvider } from '../OciRegistriesContext';
import ImageBuildYaml from './ImageBuildYaml';
import ImageBuildDetailsTab from './ImageBuildDetailsTab';
import ImageBuildExportsGallery from './ImageBuildExportsGallery';
+import ImageBuildLogsTab from './ImageBuildLogsTab';
import { hasImageBuildFailed } from '../../../utils/imageBuilds';
const imageBuildDetailsPermissions = [
@@ -77,7 +78,7 @@ const ImageBuildDetailsPageContent = () => {
} />
} />
} />
- TODO Logs} />
+ } />
{isDeleteModalOpen && (
@@ -118,6 +121,16 @@ const ImageBuildDetailsTab = ({ imageBuild }: { imageBuild: ImageBuildWithExport
>
)}
+ {remoteAccessUsername && (
+
+ {t('Remote access')}
+
+
+ Enabled for {remoteAccessUsername}
+
+
+
+ )}
@@ -178,7 +191,7 @@ const ImageBuildDetailsTab = ({ imageBuild }: { imageBuild: ImageBuildWithExport
{hasExports
? existingImageExports.map((imageExport) => (
))
: t('None')}
diff --git a/libs/ui-components/src/components/ImageBuilds/ImageBuildDetails/ImageBuildExportsGallery.tsx b/libs/ui-components/src/components/ImageBuilds/ImageBuildDetails/ImageBuildExportsGallery.tsx
index c8b4f5d7b..7fae24f67 100644
--- a/libs/ui-components/src/components/ImageBuilds/ImageBuildDetails/ImageBuildExportsGallery.tsx
+++ b/libs/ui-components/src/components/ImageBuilds/ImageBuildDetails/ImageBuildExportsGallery.tsx
@@ -10,7 +10,7 @@ import { getImageExportResource } from '../CreateImageBuildWizard/utils';
import { ViewImageBuildExportCard } from '../ImageExportCards';
import { useOciRegistriesContext } from '../OciRegistriesContext';
import { showSpinnerBriefly } from '../../../utils/time';
-import { getExportDownloadResult, getImageReference } from '../../../utils/imageBuilds';
+import { getAllExportFormats, getExportDownloadResult, getImageReference } from '../../../utils/imageBuilds';
type ImageBuildExportsGalleryProps = {
imageBuild: ImageBuildWithExports;
@@ -21,12 +21,6 @@ const REFRESH_IMAGE_BUILD_DELAY = 450;
// Delay to keep loading state while browser processes redirect
const DOWNLOAD_REDIRECT_DELAY = 1000;
-const allFormats = [
- ExportFormatType.ExportFormatTypeVMDK,
- ExportFormatType.ExportFormatTypeQCOW2,
- ExportFormatType.ExportFormatTypeISO,
-];
-
const createDownloadLink = (url: string) => {
const link = document.createElement('a');
link.href = url;
@@ -46,12 +40,13 @@ const ImageBuildExportsGallery = ({ imageBuild, refetch }: ImageBuildExportsGall
const { ociRegistries } = useOciRegistriesContext();
const [exportingFormat, setExportingFormat] = React.useState();
const [downloadingFormat, setDownloadingFormat] = React.useState();
+ const imageBuildId = imageBuild.metadata.name as string;
const handleExportImage = async (format: ExportFormatType) => {
setExportingFormat(format);
setError(undefined);
try {
- const imageExport = getImageExportResource(imageBuild.metadata.name as string, format);
+ const imageExport = getImageExportResource(imageBuildId, format);
await post('imageexports', imageExport);
// The "Export image" button wouldn't be seen as spinning without this delay.
await showSpinnerBriefly(REFRESH_IMAGE_BUILD_DELAY);
@@ -102,7 +97,7 @@ const ImageBuildExportsGallery = ({ imageBuild, refetch }: ImageBuildExportsGall
return (
- {allFormats.map((format) => {
+ {getAllExportFormats().map((format) => {
const imageExport = imageBuild.imageExports.find((imageExport) => imageExport?.spec.format === format);
const isDisabled = exportingFormat && exportingFormat !== format;
// We can only link to the generic destination for the image build.
@@ -112,6 +107,7 @@ const ImageBuildExportsGallery = ({ imageBuild, refetch }: ImageBuildExportsGall
const hasError = error?.format === format;
return (
{
+ switch (format) {
+ case ExportFormatType.ExportFormatTypeVMDK:
+ return t('Virtualization (VMDK)');
+ case ExportFormatType.ExportFormatTypeQCOW2:
+ return t('OpenStack/KVM (QCOW2)');
+ case ExportFormatType.ExportFormatTypeQCOW2DiskContainer:
+ return t('OpenShift Virtualization (QCOW2)');
+ case ExportFormatType.ExportFormatTypeISO:
+ return t('Metal installer (ISO)');
+ }
+};
+
+const ImageBuildAndExportLogStatus = ({ isActive, isFailed }: { isActive: boolean; isFailed: boolean }) => {
+ const { t } = useTranslation();
+ let label: string;
+ let level: StatusLevel;
+ if (isFailed) {
+ level = 'danger';
+ label = t('Failed');
+ } else if (isActive) {
+ level = 'info';
+ label = t('In progress');
+ } else {
+ level = 'success';
+ label = t('Completed');
+ }
+ return ;
+};
+
+const ImageBuildLogsTab = ({ imageBuild }: { imageBuild: ImageBuildWithExports }) => {
+ const { t } = useTranslation();
+ const [isLogSelectOpen, setIsLogSelectOpen] = React.useState(false);
+ const logsRef = React.useRef(null);
+
+ const { selectableEntities, hasExports } = React.useMemo(() => {
+ const entities: LogEntity[] = [];
+ const buildName = imageBuild.metadata.name as string;
+ entities.push({
+ type: LogResourceType.BUILD,
+ id: buildName,
+ label: buildName,
+ isActive: shouldHaveImageBuildLogs(imageBuild),
+ isFailed: hasImageBuildFailed(imageBuild),
+ });
+
+ // ImageExports can only exist once the ImageBuild is complete
+ if (!isImageBuildComplete(imageBuild)) {
+ return { selectableEntities: entities, availableExportFormats: [] as ExportFormatType[], failedExportsCount: 0 };
+ }
+
+ let hasExports = false;
+ imageBuild.imageExports.forEach((ie) => {
+ const format = ie?.spec.format;
+ if (format) {
+ const isFailed = isImageExportFailed(ie);
+ hasExports = true;
+ entities.push({
+ type: LogResourceType.EXPORT,
+ id: ie.metadata.name as string,
+ label: getExportFormatText(t, format),
+ isActive: shouldHaveImageExportLogs(ie),
+ isFailed,
+ });
+ }
+ });
+ return {
+ selectableEntities: entities,
+ hasExports,
+ };
+ }, [imageBuild, t]);
+
+ const [selectedEntityId, setSelectedEntityId] = React.useState(imageBuild.metadata.name as string);
+ const selectedEntity = selectableEntities.find((entity) => entity.id === selectedEntityId) || selectableEntities[0];
+ const { logs, isLoading, error, isStreaming } = useImageBuildLogs(
+ selectedEntity.id,
+ selectedEntity.type,
+ selectedEntity.isActive,
+ );
+
+ const onLogSourceSelected = React.useCallback(
+ (_event: React.MouseEvent | undefined, value: string | number | undefined) => {
+ if (value) {
+ setSelectedEntityId(value as string);
+ }
+ setIsLogSelectOpen(false);
+ },
+ [],
+ );
+
+ React.useEffect(() => {
+ // For live logs (when they are streaming), we auto-scroll to the bottom.
+ // For completed logs, we show the logs with scroll at the top
+ if (logsRef.current && logs && isStreaming) {
+ logsRef.current.scrollTop = logsRef.current.scrollHeight;
+ }
+ }, [logs, isStreaming]);
+
+ if (isLoading) {
+ return (
+
+
+
+
+
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
+ {error.message}
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default ImageBuildLogsTab;
diff --git a/libs/ui-components/src/components/ImageBuilds/ImageExportCards.tsx b/libs/ui-components/src/components/ImageBuilds/ImageExportCards.tsx
index 1e17dafa2..f0b75fe73 100644
--- a/libs/ui-components/src/components/ImageBuilds/ImageExportCards.tsx
+++ b/libs/ui-components/src/components/ImageBuilds/ImageExportCards.tsx
@@ -19,23 +19,32 @@ import {
import { VirtualMachineIcon } from '@patternfly/react-icons/dist/js/icons/virtual-machine-icon';
import { CloudSecurityIcon } from '@patternfly/react-icons/dist/js/icons/cloud-security-icon';
import { ServerGroupIcon } from '@patternfly/react-icons/dist/js/icons/server-group-icon';
+import { BuilderImageIcon } from '@patternfly/react-icons/dist/js/icons/builder-image-icon';
import { ExportFormatType, ImageExport } from '@flightctl/types/imagebuilder';
-import { getExportFormatDescription, getExportFormatLabel } from '../../utils/imageBuilds';
+import {
+ getExportFormatDescription,
+ getExportFormatLabel,
+ isImageExportCompleted,
+ isImageExportFailed,
+} from '../../utils/imageBuilds';
import { getDateDisplay } from '../../utils/dates';
import { useTranslation } from '../../hooks/useTranslation';
-import { isImageExportCompleted, isImageExportFailed } from './CreateImageBuildWizard/utils';
import { ImageExportStatusDisplay } from './ImageBuildAndExportStatus';
+import { useAppContext } from '../../hooks/useAppContext';
+import { ROUTE } from '../../hooks/useNavigate';
import './ImageExportCards.css';
const iconMap: Record = {
[ExportFormatType.ExportFormatTypeVMDK]: ,
[ExportFormatType.ExportFormatTypeQCOW2]: ,
+ [ExportFormatType.ExportFormatTypeQCOW2DiskContainer]: ,
[ExportFormatType.ExportFormatTypeISO]: ,
};
export type ImageExportFormatCardProps = {
+ imageBuildId: string;
imageReference: string | undefined;
format: ExportFormatType;
error?: { message: string; mode: 'export' | 'download' } | null;
@@ -57,7 +66,7 @@ type SelectImageBuildExportCardProps = {
export const SelectImageBuildExportCard = ({ format, isChecked, onToggle }: SelectImageBuildExportCardProps) => {
const { t } = useTranslation();
- const title = getExportFormatLabel(format);
+ const title = getExportFormatLabel(t, format);
const description = getExportFormatDescription(t, format);
const id = `export-format-${format}`;
@@ -88,6 +97,7 @@ export const SelectImageBuildExportCard = ({ format, isChecked, onToggle }: Sele
};
export const ViewImageBuildExportCard = ({
+ imageBuildId,
format,
imageExport,
imageReference,
@@ -100,12 +110,21 @@ export const ViewImageBuildExportCard = ({
error,
}: ImageExportFormatCardProps) => {
const { t } = useTranslation();
+ const {
+ router: { useNavigate: useRouterNavigate, appRoutes },
+ } = useAppContext();
+ const routerNavigate = useRouterNavigate();
const exists = !!imageExport;
const failedExport = exists && isImageExportFailed(imageExport);
const completedExport = exists && isImageExportCompleted(imageExport);
- const title = getExportFormatLabel(format);
+ const title = getExportFormatLabel(t, format);
const description = getExportFormatDescription(t, format);
+ const handleViewLogs = () => {
+ const baseRoute = appRoutes[ROUTE.IMAGE_BUILD_DETAILS];
+ routerNavigate(`${baseRoute}/${imageBuildId}/logs`);
+ };
+
return (
@@ -166,7 +185,9 @@ export const ViewImageBuildExportCard = ({
)}
{exists ? (
-
+
) : (