From 361de24e08e6768b33040d65d0281c629a92a587 Mon Sep 17 00:00:00 2001 From: Celia Amador Date: Mon, 26 Jan 2026 12:28:28 +0100 Subject: [PATCH 1/5] EDM-3207: Add ImageBuilder logs, userConfig and additional export format --- libs/i18n/locales/en/translation.json | 25 +- libs/types/imagebuilder/index.ts | 1 + .../imagebuilder/models/ExportFormatType.ts | 1 + .../imagebuilder/models/ImageBuildSpec.ts | 2 + .../models/ImageBuildUserConfiguration.ts | 18 ++ .../steps/OutputImageStep.tsx | 27 +- .../steps/RegistrationStep.tsx | 43 +++ .../steps/ReviewStep.tsx | 4 +- .../CreateImageBuildWizard/types.ts | 12 +- .../CreateImageBuildWizard/utils.ts | 61 ++-- .../ImageBuildDetailsPage.tsx | 3 +- .../ImageBuildDetailsTab.tsx | 15 +- .../ImageBuildExportsGallery.tsx | 10 +- .../ImageBuildDetails/ImageBuildLogsTab.tsx | 287 ++++++++++++++++++ .../ImageBuilds/ImageExportCards.tsx | 16 +- .../ImageBuilds/useImageBuildLogs.ts | 159 ++++++++++ libs/ui-components/src/utils/imageBuilds.ts | 77 ++++- proxy/app.go | 2 +- 18 files changed, 696 insertions(+), 67 deletions(-) create mode 100644 libs/types/imagebuilder/models/ImageBuildUserConfiguration.ts create mode 100644 libs/ui-components/src/components/ImageBuilds/ImageBuildDetails/ImageBuildLogsTab.tsx create mode 100644 libs/ui-components/src/components/ImageBuilds/useImageBuildLogs.ts diff --git a/libs/i18n/locales/en/translation.json b/libs/i18n/locales/en/translation.json index c7a823d4d..cc46887fe 100644 --- a/libs/i18n/locales/en/translation.json +++ b/libs/i18n/locales/en/translation.json @@ -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", + "Public key": "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", @@ -875,7 +881,19 @@ "Delete image build": "Delete image build", "Build information": "Build information", "Build status": "Build status", + "Enabled for <1>{remoteAccessUsername}": "Enabled for <1>{remoteAccessUsername}", "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 (Disk Container)": "QCOW2 (Disk 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/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..332d4ae83 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,22 @@ const RegistrationStep = () => { } }; + const handleRemoteAccessToggle = (enabled: boolean) => { + if (enabled) { + // Ensure userConfiguration object exists + if (!values.userConfiguration) { + setFieldValue('userConfiguration', { username: '', publickey: '', enabled: true }); + } else { + setFieldValue('userConfiguration.enabled', true); + } + } else { + // Clear userConfiguration when disabled + setFieldValue('userConfiguration.username', ''); + setFieldValue('userConfiguration.publickey', ''); + setFieldValue('userConfiguration.enabled', false); + } + }; + return ( @@ -141,6 +162,28 @@ 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..e0c150871 100644 --- a/libs/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/steps/ReviewStep.tsx +++ b/libs/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/steps/ReviewStep.tsx @@ -117,7 +117,7 @@ const ReviewStep = ({ error }: ReviewStepProps) => { <> {values.exportFormats.map((format) => ( ))} @@ -196,7 +196,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..ebd263168 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,28 @@ 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, + }, + }; + + if (values.userConfiguration?.username && values.userConfiguration?.publickey) { + spec.userConfiguration = { + username: values.userConfiguration.username, + publickey: values.userConfiguration.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 +194,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..f20947078 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; @@ -102,7 +96,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. diff --git a/libs/ui-components/src/components/ImageBuilds/ImageBuildDetails/ImageBuildLogsTab.tsx b/libs/ui-components/src/components/ImageBuilds/ImageBuildDetails/ImageBuildLogsTab.tsx new file mode 100644 index 000000000..284cad76e --- /dev/null +++ b/libs/ui-components/src/components/ImageBuilds/ImageBuildDetails/ImageBuildLogsTab.tsx @@ -0,0 +1,287 @@ +import * as React from 'react'; +import { TFunction } from 'react-i18next'; +import { + Alert, + Badge, + Bullseye, + Card, + CardBody, + Divider, + Flex, + FlexItem, + MenuContent, + MenuGroup, + MenuItem, + MenuList, + MenuToggle, + Select, + Spinner, + Stack, + StackItem, + TextArea, + Toolbar, + ToolbarContent, + ToolbarGroup, + ToolbarItem, +} from '@patternfly/react-core'; + +import { ExportFormatType } from '@flightctl/types/imagebuilder'; +import { useTranslation } from '../../../hooks/useTranslation'; +import { + hasImageBuildFailed, + isImageBuildComplete, + isImageExportFailed, + shouldHaveImageBuildLogs, + shouldHaveImageExportLogs, +} from '../../../utils/imageBuilds'; +import { ImageBuildWithExports } from '../../../types/extraTypes'; +import { LogResourceType, useImageBuildLogs } from '../useImageBuildLogs'; +import { StatusDisplayContent } from '../../Status/StatusDisplay'; +import { StatusLevel } from '../../../utils/status/common'; + +type LogEntity = { + type: LogResourceType; + id: string; + label: string; + isActive: boolean; + isFailed: boolean; +}; + +const getExportFormatText = (t: TFunction, format: ExportFormatType) => { + 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, failedExportsCount } = 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 failedExportsCount = 0; + let hasExports = false; + imageBuild.imageExports.forEach((ie) => { + const format = ie?.spec.format; + if (format) { + const isFailed = isImageExportFailed(ie); + if (isFailed) { + failedExportsCount++; + } + hasExports = true; + entities.push({ + type: LogResourceType.EXPORT, + id: ie.metadata.name as string, + label: getExportFormatText(t, format), + isActive: shouldHaveImageExportLogs(ie), + isFailed, + }); + } + }); + return { + selectableEntities: entities, + failedExportsCount, + 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 ( + + + + + + + + + + + + + + + + + + + + + + + +