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}": "Enabled for <1>{remoteAccessUsername}", "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 ( + + + + + + + + + + + + + + + + + + + + + + + +