Check deployment status while creating project with single deployer #15
@ -20,16 +20,6 @@
|
||||
clientId = ""
|
||||
clientSecret = ""
|
||||
|
||||
[google]
|
||||
clientId = ""
|
||||
clientSecret = ""
|
||||
|
||||
[turnkey]
|
||||
apiBaseUrl = "https://api.turnkey.com"
|
||||
apiPrivateKey = ""
|
||||
apiPublicKey = ""
|
||||
defaultOrganizationId = ""
|
||||
|
||||
[registryConfig]
|
||||
fetchDeploymentRecordDelay = 5000
|
||||
checkAuctionStatusDelay = 5000
|
||||
|
@ -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 { Deployment, DeploymentStatus } from 'gql-client';
|
||||
|
||||
import { DeployStep, DeployStatus } from './DeployStep';
|
||||
import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
|
||||
@ -12,6 +11,23 @@ import { useGQLClient } from 'context/GQLClientContext';
|
||||
|
||||
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 client = useGQLClient();
|
||||
|
||||
@ -19,7 +35,7 @@ const Deploy = () => {
|
||||
const projectId = searchParams.get('projectId');
|
||||
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [deployments, setDeployments] = useState<Deployment[]>([]);
|
||||
const [record, setRecord] = useState<Record>();
|
||||
|
||||
const handleOpen = () => setOpen(!open);
|
||||
|
||||
@ -30,35 +46,84 @@ const Deploy = () => {
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
|
||||
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]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchDeployments();
|
||||
fetchDeploymentRecords();
|
||||
|
||||
const interval = setInterval(() => {
|
||||
fetchDeployments();
|
||||
fetchDeploymentRecords();
|
||||
}, FETCH_DEPLOYMENTS_INTERVAL);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [fetchDeployments]);
|
||||
}, [fetchDeploymentRecords]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
deployments.length > 0 &&
|
||||
deployments[0].status === DeploymentStatus.Ready
|
||||
) {
|
||||
if (!record) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (record.lastState === 'DEPLOYED') {
|
||||
navigate(`/${orgSlug}/projects/create/success/${projectId}`);
|
||||
}
|
||||
}, [deployments]);
|
||||
}, [record]);
|
||||
|
||||
return (
|
||||
<div className="space-y-7">
|
||||
@ -67,12 +132,14 @@ const Deploy = () => {
|
||||
<Heading as="h4" className="md:text-lg font-medium">
|
||||
Deployment started ...
|
||||
</Heading>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<ClockOutlineIcon size={16} className="text-elements-mid-em" />
|
||||
<Stopwatch
|
||||
offsetTimestamp={setStopWatchOffset(Date.now().toString())}
|
||||
/>
|
||||
</div>
|
||||
{showTimer && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<ClockOutlineIcon size={16} className="text-elements-mid-em" />
|
||||
<Stopwatch
|
||||
offsetTimestamp={setStopWatchOffset(Date.now().toString())}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleOpen}
|
||||
@ -89,30 +156,36 @@ const Deploy = () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<DeployStep
|
||||
title="Building"
|
||||
status={DeployStatus.COMPLETE}
|
||||
step="1"
|
||||
processTime="72000"
|
||||
/>
|
||||
<DeployStep
|
||||
title="Deployment summary"
|
||||
status={DeployStatus.PROCESSING}
|
||||
step="2"
|
||||
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>
|
||||
{showSteps ? (
|
||||
<div>
|
||||
<DeployStep
|
||||
title={record ? 'Submitted' : 'Submitting'}
|
||||
status={record ? DeployStatus.COMPLETE : DeployStatus.PROCESSING}
|
||||
step="1"
|
||||
/>
|
||||
|
||||
<DeployStep
|
||||
title={
|
||||
record && record.lastState === 'DEPLOYED'
|
||||
? 'Deployed'
|
||||
: 'Deploying'
|
||||
}
|
||||
status={
|
||||
!record
|
||||
? DeployStatus.NOT_STARTED
|
||||
: record.lastState === 'DEPLOYED'
|
||||
? DeployStatus.COMPLETE
|
||||
: DeployStatus.PROCESSING
|
||||
}
|
||||
step="2"
|
||||
startTime={Date.now().toString()}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<DeployStep title={record!.lastState} status={DeployStatus.ERROR} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,27 +1,16 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Collapse } from '@snowballtools/material-tailwind-react-fork';
|
||||
|
||||
import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
|
||||
import FormatMillisecond from '../../FormatMilliSecond';
|
||||
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 {
|
||||
PROCESSING = 'progress',
|
||||
COMPLETE = 'complete',
|
||||
NOT_STARTED = 'notStarted',
|
||||
ERROR = 'error',
|
||||
}
|
||||
|
||||
interface DeployStepsProps {
|
||||
@ -37,30 +26,11 @@ const DeployStep = ({
|
||||
status,
|
||||
title,
|
||||
startTime,
|
||||
processTime,
|
||||
}: DeployStepsProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { toast, dismiss } = useToast();
|
||||
const { isIntersecting: hideGradientOverlay, ref } = useIntersectionObserver({
|
||||
threshold: 1,
|
||||
});
|
||||
|
||||
const disableCollapse = status !== DeployStatus.COMPLETE;
|
||||
|
||||
return (
|
||||
<div className="border-b border-border-separator">
|
||||
{/* Collapisble trigger */}
|
||||
<button
|
||||
className={cn(
|
||||
'flex justify-between w-full py-5 gap-2',
|
||||
disableCollapse && 'cursor-auto',
|
||||
)}
|
||||
tabIndex={disableCollapse ? -1 : undefined}
|
||||
onClick={() => {
|
||||
if (!disableCollapse) {
|
||||
setIsOpen((val) => !val);
|
||||
}
|
||||
}}
|
||||
className={cn('flex justify-between w-full py-5 gap-2', 'cursor-auto')}
|
||||
>
|
||||
<div className={cn('grow flex items-center gap-3')}>
|
||||
{/* Icon */}
|
||||
@ -73,12 +43,6 @@ const DeployStep = ({
|
||||
{status === DeployStatus.PROCESSING && (
|
||||
<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>
|
||||
|
||||
{/* Title */}
|
||||
@ -107,51 +71,9 @@ const DeployStep = ({
|
||||
size={15}
|
||||
/>
|
||||
</div>
|
||||
<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) => {
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user