diff --git a/ui/src/features/stage/promotion-details-modal.tsx b/ui/src/features/stage/promotion-details-modal.tsx index 1dc24f767..db3a92913 100644 --- a/ui/src/features/stage/promotion-details-modal.tsx +++ b/ui/src/features/stage/promotion-details-modal.tsx @@ -1,15 +1,16 @@ import { - faCaretDown, - faCaretUp, faCheck, faCircleNotch, + faCog, faFileLines, + faLinesLeaning, faShoePrints, faTimes } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Flex, Modal, Tabs } from 'antd'; +import { Collapse, Flex, Modal, Segmented, Tabs, Tag } from 'antd'; import Alert from 'antd/es/alert/Alert'; +import { SegmentedOptions } from 'antd/es/segmented'; import classNames from 'classnames'; import { useMemo, useState } from 'react'; @@ -25,7 +26,15 @@ import { Runner } from '@ui/features/promotion-directives/registry/types'; import { Promotion, PromotionStep } from '@ui/gen/v1alpha1/generated_pb'; import { decodeRawData } from '@ui/utils/decode-raw-data'; -const Step = ({ step, result }: { step: PromotionStep; result: PromotionDirectiveStepStatus }) => { +const Step = ({ + step, + result, + logs +}: { + step: PromotionStep; + result: PromotionDirectiveStepStatus; + logs?: object; +}) => { const [showDetails, setShowDetails] = useState(false); const { registry } = usePromotionDirectivesRegistryContext(); @@ -59,15 +68,43 @@ const Step = ({ step, result }: { step: PromotionStep; result: PromotionDirectiv const success = result === PromotionDirectiveStepStatus.SUCCESS; const failed = result === PromotionDirectiveStepStatus.FAILED; - return ( - - + const opts: SegmentedOptions = []; + + if (logs) { + opts.push({ + label: 'Output', + value: 'output', + icon: , + className: 'p-2' + }); + } + + if (meta?.config) { + opts.push({ + label: 'Config', + value: 'config', + icon: , + className: 'p-2' + }); + } + + const [selectedOpts, setSelectedOpts] = useState( + // @ts-expect-error value is there + opts?.[0]?.value + ); + + const yamlView = { + config: meta?.config, + output: logs ? JSON.stringify(logs || {}, null, ' ') : '' + }; + + return { + className: classNames('', { + 'border-green-500': progressing, + 'border-gray-200': !progressing + }), + label: ( + setShowDetails(!showDetails)}> {meta.spec.identifier} - + {!!step?.as && ( + + {step.as} + + )} + ))} - {step.config && ( - setShowDetails(!showDetails)} - className='mr-2 text-blue-500 cursor-pointer' - /> - )} - {showDetails && } - - ); + ), + children: ( + <> + {opts.length > 1 && ( + + )} + + + ) + }; }; export const PromotionDetailsModal = ({ @@ -111,6 +163,21 @@ export const PromotionDetailsModal = ({ }: { promotion: Promotion; } & ModalProps) => { + const logsByStepAlias: Record = useMemo(() => { + if (promotion?.status?.state?.raw) { + try { + const raw = decodeRawData({ result: { case: 'raw', value: promotion.status.state.raw } }); + + return JSON.parse(raw); + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + } + } + + return {}; + }, [promotion]); + return ( {promotion.spec?.steps && ( }> - {promotion.spec.steps.map((step, i) => ( - - ))} + { + return Step({ + step, + result: getPromotionDirectiveStepStatus(i, promotion.status), + logs: logsByStepAlias?.[step?.as || ''] + }); + })} + /> {!!promotion?.status?.message && ( - + )} )} diff --git a/ui/src/features/stage/promotions.tsx b/ui/src/features/stage/promotions.tsx index 02b0aaa2f..76d327368 100644 --- a/ui/src/features/stage/promotions.tsx +++ b/ui/src/features/stage/promotions.tsx @@ -17,7 +17,6 @@ import { KargoService } from '@ui/gen/service/v1alpha1/service_connect'; import { ListPromotionsResponse } from '@ui/gen/service/v1alpha1/service_pb'; import { Freight, Promotion } from '@ui/gen/v1alpha1/generated_pb'; -import { useModal } from '../common/modal/use-modal'; import { PromotionStatusIcon } from '../common/promotion-status/promotion-status-icon'; import { PrLinks } from './pr-links'; @@ -26,8 +25,6 @@ import { PromotionDetailsModal } from './promotion-details-modal'; export const Promotions = ({ repoUrls }: { repoUrls?: string[] }) => { const client = useQueryClient(); - const { show } = useModal(); - const { name: projectName, stageName } = useParams(); const { data: promotionsResponse, isLoading } = useQuery( listPromotions, @@ -45,6 +42,9 @@ export const Promotions = ({ repoUrls }: { repoUrls?: string[] }) => { } ); + // modal kept in the same component for live view + const [selectedPromotion, setSelectedPromotion] = useState(); + useEffect(() => { if (isLoading || !promotionsResponse) { return; @@ -114,9 +114,7 @@ export const Promotions = ({ repoUrls }: { repoUrls?: string[] }) => { { title: 'Name', render: (_, promotion) => ( - show((p) => )}> - {promotion.metadata?.name} - + setSelectedPromotion(promotion)}>{promotion.metadata?.name} ) }, { @@ -161,13 +159,26 @@ export const Promotions = ({ repoUrls }: { repoUrls?: string[] }) => { ]; return ( - p.metadata?.uid || ''} - loading={isLoading} - /> + <> +
p.metadata?.uid || ''} + loading={isLoading} + /> + + {selectedPromotion && ( + p?.metadata?.name === selectedPromotion?.metadata?.name + )} + visible={!!selectedPromotion} + hide={() => setSelectedPromotion(undefined)} + /> + )} + ); };