Skip to content

Commit

Permalink
feat(ui): freight history in stage details (#2730)
Browse files Browse the repository at this point in the history
Signed-off-by: Mayursinh Sarvaiya <[email protected]>
  • Loading branch information
Marvin9 authored Oct 14, 2024
1 parent 2444a40 commit a017329
Show file tree
Hide file tree
Showing 7 changed files with 270 additions and 72 deletions.
2 changes: 1 addition & 1 deletion ui/src/features/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export const ALIAS_LABEL_KEY = 'kargo.akuity.io/alias';
export const DESCRIPTION_ANNOTATION_KEY = 'kargo.akuity.io/description';

export const getAlias = (freight?: Freight): string | undefined => {
return freight?.metadata?.labels[ALIAS_LABEL_KEY] || undefined;
return freight?.alias || freight?.metadata?.labels[ALIAS_LABEL_KEY] || undefined;
};

export const dnsRegex = /^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$/;
Expand Down
13 changes: 10 additions & 3 deletions ui/src/features/freight-timeline/freight-timeline-wrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import { faCaretLeft, faCaretRight } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Button, Flex } from 'antd';
import classNames from 'classnames';
import { useLayoutEffect, useRef, useState } from 'react';

import { headerButtonStyle } from './utils';

export const FreightTimelineWrapper = ({ children }: { children: React.ReactNode }) => {
export const FreightTimelineWrapper = ({
children,
containerClassName
}: {
children: React.ReactNode;
containerClassName?: string;
}) => {
const timeline = useRef<HTMLDivElement>(null);
const [showScrollbars, setShowScrollbars] = useState(false);

Expand All @@ -26,8 +33,8 @@ export const FreightTimelineWrapper = ({ children }: { children: React.ReactNode
}, [timeline]);

return (
<div className='w-full py-3 flex flex-col overflow-hidden'>
<div className='flex h-48 w-full items-center px-1'>
<div className={classNames(containerClassName, 'w-full py-3 flex flex-col overflow-hidden')}>
<div className={'flex h-48 w-full items-center px-1'}>
<div
className='text-gray-500 text-sm font-semibold mb-2 w-min h-min'
style={{ transform: 'rotate(-0.25turn)' }}
Expand Down
4 changes: 3 additions & 1 deletion ui/src/features/stage/create-stage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -256,13 +256,15 @@ export const CreateStage = ({
<RequestedFreight
requestedFreight={requestedFreight}
projectName={project}
className='mb-4 grid grid-cols-2 gap-4'
className='mb-4'
itemStyle={{ width: '45%' }}
onDelete={(index) => {
field.onChange([
...field.value.slice(0, index),
...field.value.slice(index + 1)
]);
}}
hideTitle
/>
) : (
<Flex
Expand Down
169 changes: 169 additions & 0 deletions ui/src/features/stage/freight-history.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import { useQuery } from '@connectrpc/connect-query';
import { faHistory } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Empty } from 'antd';
import classNames from 'classnames';
import { useMemo } from 'react';
import { generatePath, Link, useNavigate } from 'react-router-dom';

import { paths } from '@ui/config/paths';
import freightTimelineStyles from '@ui/features/freight-timeline/freight-timeline.module.less';
import { queryFreight } from '@ui/gen/service/v1alpha1/service-KargoService_connectquery';
import {
Freight,
FreightReference,
FreightRequest,
StageStatus
} from '@ui/gen/v1alpha1/generated_pb';

import { LoadingState } from '../common';
import { FreightContents } from '../freight-timeline/freight-contents';
import { FreightItemLabel } from '../freight-timeline/freight-item-label';
import { FreightTimelineWrapper } from '../freight-timeline/freight-timeline-wrapper';

import requestedFreightStyles from './requested-freight.module.less';

export const FreightHistory = ({
projectName,
freightHistory,
requestedFreights,
className
}: {
className?: string;
requestedFreights: FreightRequest[];
projectName: string;
// show the freight history thats 1:1 with requested freight
freightHistory?: StageStatus['freightHistory'];
// freight hash name which is active at the moment
// you can get this from lastPromotion in stage status
// usually last one is active but we have to consider multi-pipeline case
currentActiveFreight?: string;
}) => {
const navigate = useNavigate();

const freightQuery = useQuery(queryFreight, { project: projectName });

const freightMap = useMemo(() => {
const freightData = freightQuery.data;
// generate metadata.name -> full freight data (because history doesn't have it all) to show in freight history
const freightMap: Record<string, Freight> = {};

for (const freight of freightData?.groups?.['']?.freight || []) {
const freightId = freight?.metadata?.name;
if (freightId) {
freightMap[freightId] = freight;
}
}

return freightMap;
}, [freightQuery.data]);

const freightHistoryPerWarehouse = useMemo(() => {
// to show the history
const freightHistoryPerWarehouse: Record<
string /* warehouse eg. Warehouse/w-1 or Warehouse/w-2 */,
FreightReference[]
> = {};

for (const freightCollection of freightHistory || []) {
// key - value
// warehouse identifier - freight reference
const items = freightCollection?.items || {};

for (const [warehouseIdentifier, freightReference] of Object.entries(items)) {
if (!freightHistoryPerWarehouse[warehouseIdentifier]) {
freightHistoryPerWarehouse[warehouseIdentifier] = [];
}

freightHistoryPerWarehouse[warehouseIdentifier].push(freightReference);
}
}

return freightHistoryPerWarehouse;
}, [freightHistory]);

if (freightQuery.isFetching) {
return <LoadingState />;
}

return (
<div className={className}>
<h3>
<FontAwesomeIcon icon={faHistory} className='mr-2' />
Freight History
</h3>

{requestedFreights?.map((freight, i) => {
const freightUniqueIdentifier = `${freight.origin?.kind}/${freight.origin?.name}`;

const freightReferences = freightHistoryPerWarehouse[freightUniqueIdentifier] || [];

return (
<>
<Link
className='block'
style={{ marginBottom: '16px', marginTop: '32px' }}
to={generatePath(paths.warehouse, {
name: projectName,
warehouseName: freight?.origin?.name
})}
>
{freightUniqueIdentifier}
</Link>
<div key={i} className='py-5 bg-gray-50'>
<div className='flex gap-8'>
{freightReferences.length === 0 && (
<Empty
description={`No freight history of ${freightUniqueIdentifier}`}
className='mx-auto'
/>
)}
{freightReferences.length > 0 && (
<FreightTimelineWrapper containerClassName='py-0'>
<div className='flex gap-2 w-full h-full'>
{freightReferences.map((freightReference, idx) => (
<div
key={freightReference.name}
className={classNames(
freightTimelineStyles.freightItem,
'cursor-pointer',
idx === 0 && requestedFreightStyles['active-freight-item']
)}
onClick={() =>
navigate(
generatePath(paths.freight, {
name: projectName,
freightName: freightReference.name
})
)
}
>
<FreightContents highlighted={false} freight={freightReference} />
<div className='text-xs mt-auto'>
<FreightItemLabel
freight={
{
...freightReference,
metadata: {
name: freightReference?.name
},
alias:
freightMap[freightReference?.name || '']?.alias ||
freightReference.name
} as Freight
}
/>
</div>
</div>
))}
</div>
</FreightTimelineWrapper>
)}
</div>
</div>
</>
);
})}
</div>
);
};
8 changes: 8 additions & 0 deletions ui/src/features/stage/requested-freight.module.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.active-freight-item {
border: 2px solid #66b2ff;
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.2), 0 2px 4px rgba(0, 0, 0, 0.1);

&:hover {
border: 2px solid #66b2ff !important;
}
}
119 changes: 63 additions & 56 deletions ui/src/features/stage/requested-freight.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ export const RequestedFreight = ({
requestedFreight,
onDelete,
className,
itemStyle
itemStyle,
hideTitle
}: {
projectName?: string;
requestedFreight?: {
Expand All @@ -26,79 +27,85 @@ export const RequestedFreight = ({
onDelete?: (index: number) => void;
className?: string;
itemStyle?: React.CSSProperties;
hideTitle?: boolean;
}) => {
const { stageColorMap } = useContext(ColorContext);

const uniqueUpstreamStages = new Set<string>();

for (const freight of requestedFreight || []) {
for (const stage of freight.sources?.stages || []) {
uniqueUpstreamStages.add(stage);
}
}

const { stageColorMap } = useContext(ColorContext);

if (!requestedFreight || requestedFreight.length === 0) {
return null;
}

return (
<div className={className}>
{requestedFreight?.map((freight, i) => {
return (
<div
key={i}
className='bg-gray-50 rounded-md p-3 border-2 border-solid border-gray-200'
style={itemStyle}
>
<Flex>
<div>
<SmallLabel className='mb-1'>
{(freight.origin?.kind || 'Unknown').toUpperCase()}
</SmallLabel>
{!hideTitle && <h3>Requested Freight</h3>}

<div className='flex gap-5 flex-wrap'>
{requestedFreight?.map((freight, i) => {
return (
<div
key={i}
className='bg-gray-50 rounded-md p-3 border-2 border-solid border-gray-200'
style={itemStyle}
>
<Flex>
<div>
<SmallLabel className='mb-1'>
{(freight.origin?.kind || 'Unknown').toUpperCase()}
</SmallLabel>

<div className='text-base mb-3 font-semibold'>{freight.origin?.name}</div>
</div>
{onDelete && (
<div className='ml-auto cursor-pointer'>
<FontAwesomeIcon icon={faTimes} onClick={() => onDelete(i)} />
<div className='text-base mb-3 font-semibold'>{freight.origin?.name}</div>
</div>
)}
</Flex>
{onDelete && (
<div className='ml-auto cursor-pointer'>
<FontAwesomeIcon icon={faTimes} onClick={() => onDelete(i)} />
</div>
)}
</Flex>

<SmallLabel className='mb-1'>SOURCE</SmallLabel>
<Flex gap={6}>
{freight.sources?.direct && (
<Link
to={generatePath(paths.warehouse, {
name: projectName,
warehouseName: freight.origin?.name
})}
>
<Flex
align='center'
justify='center'
className='bg-gray-600 text-white py-1 px-2 rounded font-semibold cursor-pointer'
<SmallLabel className='mb-1'>SOURCE</SmallLabel>
<Flex gap={6}>
{freight.sources?.direct && (
<Link
to={generatePath(paths.warehouse, {
name: projectName,
warehouseName: freight.origin?.name
})}
>
<Flex
align='center'
justify='center'
className='bg-gray-600 text-white py-1 px-2 rounded font-semibold cursor-pointer'
>
<FontAwesomeIcon icon={faArrowRightToBracket} className='mr-2' />
DIRECT
</Flex>
</Link>
)}
{freight.sources?.stages?.map((stage) => (
<Link
key={stage}
to={generatePath(paths.stage, { name: projectName, stageName: stage })}
>
<FontAwesomeIcon icon={faArrowRightToBracket} className='mr-2' />
DIRECT
</Flex>
</Link>
)}
{freight.sources?.stages?.map((stage) => (
<Link
key={stage}
to={generatePath(paths.stage, { name: projectName, stageName: stage })}
>
<StageTag
stage={{ metadata: { name: stage } } as Stage}
projectName={projectName || ''}
stageColorMap={stageColorMap}
/>
</Link>
))}
</Flex>
</div>
);
})}
<StageTag
stage={{ metadata: { name: stage } } as Stage}
projectName={projectName || ''}
stageColorMap={stageColorMap}
/>
</Link>
))}
</Flex>
</div>
);
})}
</div>
</div>
);
};
Loading

0 comments on commit a017329

Please sign in to comment.