Merge remote-tracking branch 'origin/main' into andrehadianto/T-4935-timeline-component

This commit is contained in:
Andre H 2024-03-04 11:46:27 +08:00
commit 0aeea36dbd
8 changed files with 232 additions and 86 deletions

View File

@ -1,13 +1,22 @@
import { Duration } from 'luxon'; import { Duration } from 'luxon';
import React from 'react'; import React, { ComponentPropsWithoutRef } from 'react';
import { cn } from 'utils/classnames';
const FormatMillisecond = ({ time }: { time: number }) => { export interface FormatMilliSecondProps
extends ComponentPropsWithoutRef<'div'> {
time: number;
}
const FormatMillisecond = ({ time, ...props }: FormatMilliSecondProps) => {
const formatTime = Duration.fromMillis(time) const formatTime = Duration.fromMillis(time)
.shiftTo('days', 'hours', 'minutes', 'seconds') .shiftTo('days', 'hours', 'minutes', 'seconds')
.toObject(); .toObject();
return ( return (
<div> <div
{...props}
className={cn('text-sm text-elements-mid-em', props?.className)}
>
{formatTime.days !== 0 && <span>{formatTime.days}d&nbsp;</span>} {formatTime.days !== 0 && <span>{formatTime.days}d&nbsp;</span>}
{formatTime.hours !== 0 && <span>{formatTime.hours}h&nbsp;</span>} {formatTime.hours !== 0 && <span>{formatTime.hours}h&nbsp;</span>}
{formatTime.minutes !== 0 && <span>{formatTime.minutes}m&nbsp;</span>} {formatTime.minutes !== 0 && <span>{formatTime.minutes}m&nbsp;</span>}

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { useStopwatch } from 'react-timer-hook'; import { useStopwatch } from 'react-timer-hook';
import FormatMillisecond from './FormatMilliSecond'; import FormatMillisecond, { FormatMilliSecondProps } from './FormatMilliSecond';
const setStopWatchOffset = (time: string) => { const setStopWatchOffset = (time: string) => {
const providedTime = new Date(time); const providedTime = new Date(time);
@ -11,13 +11,17 @@ const setStopWatchOffset = (time: string) => {
return currentTime; return currentTime;
}; };
const Stopwatch = ({ offsetTimestamp }: { offsetTimestamp: Date }) => { interface StopwatchProps extends Omit<FormatMilliSecondProps, 'time'> {
offsetTimestamp: Date;
}
const Stopwatch = ({ offsetTimestamp, ...props }: StopwatchProps) => {
const { totalSeconds } = useStopwatch({ const { totalSeconds } = useStopwatch({
autoStart: true, autoStart: true,
offsetTimestamp: offsetTimestamp, offsetTimestamp: offsetTimestamp,
}); });
return <FormatMillisecond time={totalSeconds * 1000} />; return <FormatMillisecond time={totalSeconds * 1000} {...props} />;
}; };
export { Stopwatch, setStopWatchOffset }; export { Stopwatch, setStopWatchOffset };

View File

@ -1,11 +1,14 @@
import React, { useCallback, useEffect } from 'react'; import React, { useCallback, useEffect } from 'react';
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
import { Button, Typography } from '@material-tailwind/react'; import { Typography } from '@material-tailwind/react';
import { DeployStep, DeployStatus } from './DeployStep'; import { DeployStep, DeployStatus } from './DeployStep';
import { Stopwatch, setStopWatchOffset } from '../../StopWatch'; import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
import ConfirmDialog from 'components/shared/ConfirmDialog'; import ConfirmDialog from 'components/shared/ConfirmDialog';
import { Heading } from 'components/shared/Heading';
import { Button } from 'components/shared/Button';
import { ClockOutlineIcon, WarningIcon } from 'components/shared/CustomIcon';
const TIMEOUT_DURATION = 5000; const TIMEOUT_DURATION = 5000;
const Deploy = () => { const Deploy = () => {
@ -31,27 +34,27 @@ const Deploy = () => {
}, []); }, []);
return ( return (
<div> <div className="space-y-7">
<div className="flex justify-between mb-6"> <div className="flex justify-between">
<div> <div className="space-y-1.5">
<h4>Deployment started ...</h4> <Heading as="h4" className="md:text-lg font-medium">
<div className="flex"> Deployment started ...
^&nbsp; </Heading>
<div className="flex items-center gap-1.5">
<ClockOutlineIcon size={16} className="text-elements-mid-em" />
<Stopwatch <Stopwatch
offsetTimestamp={setStopWatchOffset(Date.now().toString())} offsetTimestamp={setStopWatchOffset(Date.now().toString())}
/> />
</div> </div>
</div> </div>
<div>
<Button <Button
onClick={handleOpen} onClick={handleOpen}
variant="outlined"
size="sm" size="sm"
placeholder={''} variant="tertiary"
leftIcon={<WarningIcon size={16} />}
> >
^ Cancel Cancel
</Button> </Button>
</div>
<ConfirmDialog <ConfirmDialog
dialogTitle="Cancel deployment?" dialogTitle="Cancel deployment?"
handleOpen={handleOpen} handleOpen={handleOpen}
@ -66,6 +69,8 @@ const Deploy = () => {
</Typography> </Typography>
</ConfirmDialog> </ConfirmDialog>
</div> </div>
<div>
<DeployStep <DeployStep
title="Building" title="Building"
status={DeployStatus.COMPLETE} status={DeployStatus.COMPLETE}
@ -89,6 +94,7 @@ const Deploy = () => {
step="4" step="4"
/> />
</div> </div>
</div>
); );
}; };

View File

@ -1,11 +1,22 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import toast from 'react-hot-toast';
import { Collapse, Button, Typography } from '@material-tailwind/react'; import { Collapse } from '@material-tailwind/react';
import { Stopwatch, setStopWatchOffset } from '../../StopWatch'; import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
import FormatMillisecond from '../../FormatMilliSecond'; import FormatMillisecond from '../../FormatMilliSecond';
import processLogs from '../../../assets/process-logs.json'; import processLogs from '../../../assets/process-logs.json';
import { cn } from 'utils/classnames';
import {
CheckRoundFilledIcon,
ClockOutlineIcon,
CopyIcon,
LoaderIcon,
MinusCircleIcon,
PlusIcon,
} from 'components/shared/CustomIcon';
import { Button } from 'components/shared/Button';
import { useToast } from 'components/shared/Toast';
import { useIntersectionObserver } from 'usehooks-ts';
enum DeployStatus { enum DeployStatus {
PROCESSING = 'progress', PROCESSING = 'progress',
@ -28,61 +39,110 @@ const DeployStep = ({
startTime, startTime,
processTime, processTime,
}: DeployStepsProps) => { }: DeployStepsProps) => {
const [collapse, setCollapse] = useState(false); const [isOpen, setIsOpen] = useState(false);
const { toast, dismiss } = useToast();
const { isIntersecting: hideGradientOverlay, ref } = useIntersectionObserver({
threshold: 1,
});
const disableCollapse = status !== DeployStatus.COMPLETE;
return ( return (
<div className="border-b-2"> <div className="border-b border-border-separator">
<div className="flex justify-between p-2 gap-2"> {/* Collapisble trigger */}
{status === DeployStatus.NOT_STARTED && <div>{step}</div>}
{status === DeployStatus.PROCESSING && <div>O</div>}
{status === DeployStatus.COMPLETE && (
<div>
<button <button
className={cn(
'flex justify-between w-full py-5 gap-2',
disableCollapse && 'cursor-auto',
)}
tabIndex={disableCollapse ? -1 : undefined}
onClick={() => { onClick={() => {
setCollapse(!collapse); if (!disableCollapse) {
setIsOpen((val) => !val);
}
}} }}
> >
{collapse ? '-' : '+'} <div className={cn('grow flex items-center gap-3')}>
</button> {/* Icon */}
<div className="w-6 h-6 grid place-content-center">
{status === DeployStatus.NOT_STARTED && (
<div className="grid place-content-center w-6 h-6 rounded-[48px] bg-base-bg-emphasized font-mono text-xs">
{step}
</div> </div>
)} )}
<div className="grow">{title}</div>
{status === DeployStatus.PROCESSING && ( {status === DeployStatus.PROCESSING && (
<> <LoaderIcon className="animate-spin text-elements-link" />
^<Stopwatch offsetTimestamp={setStopWatchOffset(startTime!)} />
</>
)} )}
{status === DeployStatus.COMPLETE && ( {status === DeployStatus.COMPLETE && (
<> <div className="text-controls-primary">
^<FormatMillisecond time={Number(processTime)} />{' '} {!isOpen && <PlusIcon size={24} />}
</> {isOpen && <MinusCircleIcon size={24} />}
</div>
)} )}
</div> </div>
<Collapse open={collapse}>
<div className="p-2 text-sm text-gray-500 h-36 overflow-y-scroll"> {/* Title */}
<span
className={cn(
'text-left text-sm md:text-base',
status === DeployStatus.PROCESSING && 'text-elements-link',
)}
>
{title}
</span>
</div>
{/* Timer */}
{status === DeployStatus.PROCESSING && (
<div className="flex items-center gap-1.5">
<ClockOutlineIcon size={16} className="text-elements-low-em" />
<Stopwatch offsetTimestamp={setStopWatchOffset(startTime!)} />
</div>
)}
{status === DeployStatus.COMPLETE && (
<div className="flex items-center gap-1.5">
<CheckRoundFilledIcon className="text-elements-success" size={18} />
<FormatMillisecond time={Number(processTime)} />{' '}
</div>
)}
</button>
{/* Collapsible */}
<Collapse open={isOpen}>
<div className="relative text-xs text-elements-low-em h-36 overflow-y-auto">
{/* Logs */}
{processLogs.map((log, key) => { {processLogs.map((log, key) => {
return ( return (
<Typography <p className="font-mono" key={key}>
variant="small"
color="gray"
key={key}
placeholder={''}
>
{log} {log}
</Typography> </p>
); );
})} })}
<div className="sticky bottom-0 left-1/2 flex justify-center">
{/* End of logs ref used for hiding gradient overlay */}
<div ref={ref} />
{/* Overflow gradient overlay */}
{!hideGradientOverlay && (
<div className="h-14 w-full sticky bottom-0 inset-x-0 bg-gradient-to-t from-white to-transparent" />
)}
{/* Copy log button */}
<div className={cn('sticky bottom-4 left-1/2 flex justify-center')}>
<Button <Button
size="sm" size="xs"
onClick={() => { onClick={() => {
navigator.clipboard.writeText(processLogs.join('\n')); navigator.clipboard.writeText(processLogs.join('\n'));
toast.success('Logs copied'); toast({
title: 'Logs copied',
variant: 'success',
id: 'logs',
onDismiss: dismiss,
});
}} }}
color="blue" leftIcon={<CopyIcon size={16} />}
placeholder={''}
> >
^ Copy log Copy log
</Button> </Button>
</div> </div>
</div> </div>

View File

@ -0,0 +1,21 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const CopyIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1.33301 2.66665C1.33301 1.93027 1.92996 1.33331 2.66634 1.33331H9.33301C10.0694 1.33331 10.6663 1.93027 10.6663 2.66665V5.33331H13.333C14.0694 5.33331 14.6663 5.93027 14.6663 6.66665V13.3333C14.6663 14.0697 14.0694 14.6666 13.333 14.6666H6.66634C5.92996 14.6666 5.33301 14.0697 5.33301 13.3333V10.6666H2.66634C1.92996 10.6666 1.33301 10.0697 1.33301 9.33331V2.66665ZM9.66634 5.33331H6.66634C5.92996 5.33331 5.33301 5.93027 5.33301 6.66665V9.66665H2.66634C2.48225 9.66665 2.33301 9.51741 2.33301 9.33331V2.66665C2.33301 2.48255 2.48225 2.33331 2.66634 2.33331H9.33301C9.5171 2.33331 9.66634 2.48255 9.66634 2.66665V5.33331Z"
fill="currentColor"
/>
</CustomIcon>
);
};

View File

@ -0,0 +1,22 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const LoaderIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="25"
height="24"
viewBox="0 0 25 24"
fill="none"
{...props}
>
<path
d="M12.5003 3V6M12.5003 18V21M6.13634 5.63604L8.25766 7.75736M16.7429 16.2426L18.8643 18.364M3.5 12.0007H6.5M18.5 12.0007H21.5M6.13634 18.364L8.25766 16.2426M16.7429 7.75736L18.8643 5.63604"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</CustomIcon>
);
};

View File

@ -0,0 +1,21 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const MinusCircleIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="25"
height="24"
viewBox="0 0 25 24"
fill="none"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M2.5 12C2.5 6.47715 6.97715 2 12.5 2C18.0228 2 22.5 6.47715 22.5 12C22.5 17.5228 18.0228 22 12.5 22C6.97715 22 2.5 17.5228 2.5 12ZM16.5 12.9999C17.0523 12.9999 17.5 12.5522 17.5 11.9999C17.5 11.4476 17.0523 10.9999 16.5 10.9999L8.49997 11.0001C7.94769 11.0001 7.49998 11.4479 7.5 12.0001C7.50002 12.5524 7.94774 13.0001 8.50003 13.0001L16.5 12.9999Z"
fill="#0F86F5"
/>
</CustomIcon>
);
};

View File

@ -45,6 +45,9 @@ export * from './CommitIcon';
export * from './RocketIcon'; export * from './RocketIcon';
export * from './RefreshIcon'; export * from './RefreshIcon';
export * from './UndoIcon'; export * from './UndoIcon';
export * from './LoaderIcon';
export * from './MinusCircleIcon';
export * from './CopyIcon';
// Templates // Templates
export * from './templates'; export * from './templates';