Check deployment status while creating project with single deployer #15

Merged
nabarun merged 7 commits from sk-check-deployment-status into main 2024-10-25 10:47:05 +00:00
3 changed files with 118 additions and 133 deletions
Showing only changes of commit 1c13bcf66d - Show all commits

View File

@ -20,16 +20,6 @@
clientId = "" clientId = ""
clientSecret = "" clientSecret = ""
[google]
clientId = ""
clientSecret = ""
[turnkey]
apiBaseUrl = "https://api.turnkey.com"
apiPrivateKey = ""
apiPublicKey = ""
defaultOrganizationId = ""
[registryConfig] [registryConfig]
fetchDeploymentRecordDelay = 5000 fetchDeploymentRecordDelay = 5000
checkAuctionStatusDelay = 5000 checkAuctionStatusDelay = 5000

View File

@ -1,6 +1,5 @@
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
import { Deployment, DeploymentStatus } from 'gql-client';
import { DeployStep, DeployStatus } from './DeployStep'; import { DeployStep, DeployStatus } from './DeployStep';
import { Stopwatch, setStopWatchOffset } from '../../StopWatch'; import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
@ -12,6 +11,23 @@ import { useGQLClient } from 'context/GQLClientContext';
const FETCH_DEPLOYMENTS_INTERVAL = 5000; const FETCH_DEPLOYMENTS_INTERVAL = 5000;
type RequestState =
| 'SUBMITTED'
| 'DEPLOYING'
| 'DEPLOYED'
| 'REMOVED'
| 'CANCELLED'
| 'ERROR';
type Record = {
id: string;
createTime: string;
app: string;
lastState: RequestState;
lastUpdate: string;
logAvailable: boolean;
};
const Deploy = () => { const Deploy = () => {
const client = useGQLClient(); const client = useGQLClient();
@ -19,7 +35,7 @@ const Deploy = () => {
const projectId = searchParams.get('projectId'); const projectId = searchParams.get('projectId');
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
const [deployments, setDeployments] = useState<Deployment[]>([]); const [record, setRecord] = useState<Record>();
const handleOpen = () => setOpen(!open); const handleOpen = () => setOpen(!open);
@ -30,35 +46,84 @@ const Deploy = () => {
navigate(`/${orgSlug}/projects/create`); navigate(`/${orgSlug}/projects/create`);
}, []); }, []);
const fetchDeployments = useCallback(async () => { const showSteps = useMemo(() => {
if (!record) {
return true;
}
if (
record.lastState === 'CANCELLED' ||
record.lastState === 'REMOVED' ||
record.lastState === 'ERROR'
) {
return false;
} else {
return true;
}
}, [record]);
const showTimer = useMemo(() => {
if (!record) {
return true;
}
if (
record.lastState === 'CANCELLED' ||
record.lastState === 'REMOVED' ||
record.lastState === 'ERROR' ||
record.lastState === 'DEPLOYED'
) {
return false;
} else {
return true;
}
}, [record]);
const fetchDeploymentRecords = useCallback(async () => {
if (!projectId) { if (!projectId) {
return; return;
} }
const { deployments } = await client.getDeployments(projectId); const { deployments } = await client.getDeployments(projectId);
setDeployments(deployments); const deployment = deployments[0];
try {
const response = await fetch(
`${deployment.deployer.deployerApiUrl}/${deployment.applicationDeploymentRequestId}`,
);
if (!response.ok) {
throw new Error('Network response was not ok');
}
const record: Record = await response.json();
setRecord(record);
} catch (err: any) {
console.log('Error fetching data from deployer', err);
}
}, [client, projectId]); }, [client, projectId]);
useEffect(() => { useEffect(() => {
fetchDeployments(); fetchDeploymentRecords();
const interval = setInterval(() => { const interval = setInterval(() => {
fetchDeployments(); fetchDeploymentRecords();
}, FETCH_DEPLOYMENTS_INTERVAL); }, FETCH_DEPLOYMENTS_INTERVAL);
return () => { return () => {
clearInterval(interval); clearInterval(interval);
}; };
}, [fetchDeployments]); }, [fetchDeploymentRecords]);
useEffect(() => { useEffect(() => {
if ( if (!record) {
deployments.length > 0 && return;
deployments[0].status === DeploymentStatus.Ready }
) {
if (record.lastState === 'DEPLOYED') {
navigate(`/${orgSlug}/projects/create/success/${projectId}`); navigate(`/${orgSlug}/projects/create/success/${projectId}`);
} }
}, [deployments]); }, [record]);
return ( return (
<div className="space-y-7"> <div className="space-y-7">
@ -67,12 +132,14 @@ const Deploy = () => {
<Heading as="h4" className="md:text-lg font-medium"> <Heading as="h4" className="md:text-lg font-medium">
Deployment started ... Deployment started ...
</Heading> </Heading>
{showTimer && (
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<ClockOutlineIcon size={16} className="text-elements-mid-em" /> <ClockOutlineIcon size={16} className="text-elements-mid-em" />
<Stopwatch <Stopwatch
offsetTimestamp={setStopWatchOffset(Date.now().toString())} offsetTimestamp={setStopWatchOffset(Date.now().toString())}
/> />
</div> </div>
)}
</div> </div>
<Button <Button
onClick={handleOpen} onClick={handleOpen}
@ -89,30 +156,36 @@ const Deploy = () => {
/> />
</div> </div>
{showSteps ? (
<div> <div>
<DeployStep <DeployStep
title="Building" title={record ? 'Submitted' : 'Submitting'}
status={DeployStatus.COMPLETE} status={record ? DeployStatus.COMPLETE : DeployStatus.PROCESSING}
step="1" step="1"
processTime="72000"
/> />
<DeployStep <DeployStep
title="Deployment summary" title={
status={DeployStatus.PROCESSING} record && record.lastState === 'DEPLOYED'
? 'Deployed'
: 'Deploying'
}
status={
!record
? DeployStatus.NOT_STARTED
: record.lastState === 'DEPLOYED'
? DeployStatus.COMPLETE
: DeployStatus.PROCESSING
}
step="2" step="2"
startTime={Date.now().toString()} startTime={Date.now().toString()}
/> />
<DeployStep
title="Running checks"
status={DeployStatus.NOT_STARTED}
step="3"
/>
<DeployStep
title="Assigning domains"
status={DeployStatus.NOT_STARTED}
step="4"
/>
</div> </div>
) : (
<div>
<DeployStep title={record!.lastState} status={DeployStatus.ERROR} />
</div>
)}
</div> </div>
); );
}; };

View File

@ -1,27 +1,16 @@
import { useState } from 'react';
import { Collapse } from '@snowballtools/material-tailwind-react-fork';
import { Stopwatch, setStopWatchOffset } from '../../StopWatch'; import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
import FormatMillisecond from '../../FormatMilliSecond';
import processLogs from '../../../assets/process-logs.json';
import { cn } from 'utils/classnames'; import { cn } from 'utils/classnames';
import { import {
CheckRoundFilledIcon, CheckRoundFilledIcon,
ClockOutlineIcon, ClockOutlineIcon,
CopyIcon,
LoaderIcon, LoaderIcon,
MinusCircleIcon,
PlusIcon,
} from 'components/shared/CustomIcon'; } 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',
COMPLETE = 'complete', COMPLETE = 'complete',
NOT_STARTED = 'notStarted', NOT_STARTED = 'notStarted',
ERROR = 'error',
} }
interface DeployStepsProps { interface DeployStepsProps {
@ -37,30 +26,11 @@ const DeployStep = ({
status, status,
title, title,
startTime, startTime,
processTime,
}: DeployStepsProps) => { }: DeployStepsProps) => {
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 border-border-separator"> <div className="border-b border-border-separator">
{/* Collapisble trigger */}
<button <button
className={cn( className={cn('flex justify-between w-full py-5 gap-2', 'cursor-auto')}
'flex justify-between w-full py-5 gap-2',
disableCollapse && 'cursor-auto',
)}
tabIndex={disableCollapse ? -1 : undefined}
onClick={() => {
if (!disableCollapse) {
setIsOpen((val) => !val);
}
}}
> >
<div className={cn('grow flex items-center gap-3')}> <div className={cn('grow flex items-center gap-3')}>
{/* Icon */} {/* Icon */}
@ -73,12 +43,6 @@ const DeployStep = ({
{status === DeployStatus.PROCESSING && ( {status === DeployStatus.PROCESSING && (
<LoaderIcon className="animate-spin text-elements-link" /> <LoaderIcon className="animate-spin text-elements-link" />
)} )}
{status === DeployStatus.COMPLETE && (
<div className="text-controls-primary">
{!isOpen && <PlusIcon size={24} />}
{isOpen && <MinusCircleIcon size={24} />}
</div>
)}
</div> </div>
{/* Title */} {/* Title */}
@ -107,51 +71,9 @@ const DeployStep = ({
size={15} size={15}
/> />
</div> </div>
<FormatMillisecond time={Number(processTime)} />{' '}
</div> </div>
)} )}
</button> </button>
{/* Collapsible */}
<Collapse open={isOpen}>
<div className="relative text-xs text-elements-low-em h-36 overflow-y-auto">
{/* Logs */}
{processLogs.map((log, key) => {
return (
<p className="font-mono" key={key}>
{log}
</p>
);
})}
{/* 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
size="xs"
onClick={() => {
navigator.clipboard.writeText(processLogs.join('\n'));
toast({
title: 'Logs copied',
variant: 'success',
id: 'logs',
onDismiss: dismiss,
});
}}
leftIcon={<CopyIcon size={16} />}
>
Copy log
</Button>
</div>
</div>
</Collapse>
</div> </div>
); );
}; };