diff --git a/ui/src/features/assemble-freight/clone-freight-note.tsx b/ui/src/features/assemble-freight/clone-freight-note.tsx index 03bd6b913e..ccb5daeb56 100644 --- a/ui/src/features/assemble-freight/clone-freight-note.tsx +++ b/ui/src/features/assemble-freight/clone-freight-note.tsx @@ -15,7 +15,9 @@ export const CloneFreightNote = (props: { missingArtifacts: (Image | GitCommit | Chart)[]; className?: string; }) => { - if (!props.cloneFreight) { + const cloneFreight = props.cloneFreight; + + if (!cloneFreight) { return null; } @@ -58,23 +60,29 @@ export const CloneFreightNote = (props: { ); } + const cloneName = cloneFreight.alias || cloneFreight.metadata?.name; + const isSourceFreightPersisted = !!cloneFreight.metadata?.creationTimestamp; + const sourceFreightLink = + isSourceFreightPersisted && cloneFreight.metadata?.name + ? generatePath(paths.freight, { + name: cloneFreight.metadata?.namespace, + freightName: cloneFreight.metadata.name + }) + : undefined; + return ( - Based on{' '} - - {props.cloneFreight?.alias} - {' '} - - matching versions are pre-filled. - + sourceFreightLink && cloneName ? ( + <> + Based on {cloneName} - matching versions are + pre-filled. + + ) : ( + <>Based on historical Freight {cloneName} - matching versions are pre-filled. + ) } icon={} showIcon diff --git a/ui/src/features/common/utils.test.ts b/ui/src/features/common/utils.test.ts index a7364781d1..d3fcfdb713 100644 --- a/ui/src/features/common/utils.test.ts +++ b/ui/src/features/common/utils.test.ts @@ -13,6 +13,7 @@ import { ALIAS_LABEL_KEY, getCurrentFreightByWarehouse, getCurrentFreightForComparison, + reconstructFreightFromHistory, getShortFreightLabel } from './utils'; @@ -161,3 +162,21 @@ describe('getShortFreightLabel', () => { expect(getShortFreightLabel()).toBe(''); }); }); + +describe('reconstructFreightFromHistory', () => { + it('preserves the historical freight contents when synthesizing a Freight', () => { + const reference = ref('warehouse-a', 'ghcr.io/acme/a'); + const freight = reconstructFreightFromHistory(reference, 'test-project'); + + expect(freight).toMatchObject({ + kind: 'Freight', + apiVersion: 'kargo.akuity.io/v1alpha1', + metadata: { + name: undefined, + namespace: 'test-project' + }, + origin: { kind: 'Warehouse', name: 'warehouse-a' }, + images: [{ repoURL: 'ghcr.io/acme/a' }] + }); + }); +}); diff --git a/ui/src/features/common/utils.ts b/ui/src/features/common/utils.ts index a7e0b653af..33c4cc00b4 100644 --- a/ui/src/features/common/utils.ts +++ b/ui/src/features/common/utils.ts @@ -67,6 +67,31 @@ export function getCurrentFreightByWarehouse( return result; } +// reconstructFreightFromHistory builds a Freight object from a +// historical Stage so clone/assemble flows can still work +// even when the original Freight resource has already been garbage collected. +// +// We keep the artifact contents (images/charts/commits) +// so existing clone logic can treat this the same as a live freight. +export function reconstructFreightFromHistory( + reference: FreightReference, + namespace?: string +): Freight { + return { + apiVersion: 'kargo.akuity.io/v1alpha1', + kind: 'Freight', + metadata: { + name: reference.name, + namespace + }, + origin: reference.origin, + images: reference.images, + charts: reference.charts, + commits: reference.commits, + artifacts: reference.artifacts + } as Freight; +} + export function getShortFreightLabel(name?: string, alias?: string): string { const shortID = (name || '').slice(0, 7); return alias ? `${alias} (${shortID})` : shortID; diff --git a/ui/src/features/project/pipelines/warehouse/warehouse-details.tsx b/ui/src/features/project/pipelines/warehouse/warehouse-details.tsx index cb07d2d47e..0ac300845b 100644 --- a/ui/src/features/project/pipelines/warehouse/warehouse-details.tsx +++ b/ui/src/features/project/pipelines/warehouse/warehouse-details.tsx @@ -9,7 +9,13 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Drawer, Flex, Skeleton, Tabs, Typography } from 'antd'; import Alert from 'antd/es/alert/Alert'; import { useMemo } from 'react'; -import { generatePath, useNavigate, useParams, useSearchParams } from 'react-router-dom'; +import { + generatePath, + useLocation, + useNavigate, + useParams, + useSearchParams +} from 'react-router-dom'; import { stringify } from 'yaml'; import { paths } from '@ui/config/paths'; @@ -18,6 +24,7 @@ import { AssembleFreight } from '@ui/features/assemble-freight/assemble-freight' import { useWarehouseWithClonedFreight } from '@ui/features/assemble-freight/use-warehouse-with-cloned-freight'; import YamlEditor from '@ui/features/common/code-editor/yaml-editor-lazy'; import { useGetFreight, useGetWarehouse } from '@ui/gen/api/v2/core/core'; +import { Freight } from '@ui/gen/api/v2/models'; import { RepoSubscriptions } from './repo-subscriptions'; import { WarehouseSettings } from './tabs/settings/warehouse-settings'; @@ -32,8 +39,11 @@ export const WarehouseDetails = ({ }) => { const { name: projectName, warehouseName, tab } = useParams(); const [search] = useSearchParams(); + const { state } = useLocation(); const cloneFreight = search.get('clone-freight'); + const { cloneFreight: historicalCloneFreight } = + (state as { cloneFreight?: Freight } | undefined) ?? {}; const navigate = useNavigate(); @@ -49,10 +59,10 @@ export const WarehouseDetails = ({ ); const freightQuery = useGetFreight(projectName || '', cloneFreight || '', { - query: { enabled: !!cloneFreight && tab === 'create-freight' } + query: { enabled: !!cloneFreight && tab === 'create-freight' && !historicalCloneFreight } }); - const cloneFreightData = freightQuery.data?.data; + const cloneFreightData = historicalCloneFreight || freightQuery.data?.data; const warehouseWithClonedFreight = useWarehouseWithClonedFreight(warehouse, cloneFreightData); diff --git a/ui/src/features/stage/tabs/freight-history/freight-history.tsx b/ui/src/features/stage/tabs/freight-history/freight-history.tsx index fa6d99fc9f..6027ea9307 100644 --- a/ui/src/features/stage/tabs/freight-history/freight-history.tsx +++ b/ui/src/features/stage/tabs/freight-history/freight-history.tsx @@ -2,15 +2,14 @@ import { faTruck } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Button, Descriptions, Flex, Select, Space, Table, Tag, Typography } from 'antd'; import { formatDistance } from 'date-fns'; -import { useMemo } from 'react'; -import { useState } from 'react'; -import { useEffect } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { generatePath, Link, useNavigate } from 'react-router-dom'; import { paths } from '@ui/config/paths'; import { shortVersion } from '@ui/features/project/pipelines/freight/short-version-utils'; import { Freight, FreightReference, FreightRequest, StageStatus } from '@ui/gen/api/v2/models'; +import { reconstructFreightFromHistory } from '../../../common/utils'; import { FreightContents } from '../../../freight-timeline/freight-contents'; import { useGetFreightMap } from './use-get-freight-map'; @@ -115,14 +114,30 @@ export const FreightHistory = ({ width={240} render={(value, record, index) => { const alias = shortVersion(freightMap[record?.name || '']?.alias || record.name, 20); + const existingFreight = record?.name ? freightMap[record.name] : undefined; + const reconstructedFreight = reconstructFreightFromHistory(record, projectName); + // If the referenced Freight still exists, open its details view. + // Otherwise route to assemble freight and seed state from history. + const linkTo = existingFreight + ? generatePath(paths.freight, { + name: projectName, + freightName: record.name + }) + : generatePath(paths.warehouse, { + name: projectName, + warehouseName: record.origin?.name || '', + tab: 'create-freight' + }); + // Only pass clone state for reconstructed historical Freight. + const linkState = existingFreight + ? undefined + : { + cloneFreight: reconstructedFreight + }; + return ( - + {alias} {index === 0 && Active}