Check deployment status while creating project with single deployer (#15)

Part of [Service provider auctions for web deployments](https://www.notion.so/Service-provider-auctions-for-web-deployments-104a6b22d47280dbad51d28aa3a91d75)
- Use deployer API to get status of the deployments

Co-authored-by: Shreerang Kale <shreerangkale@gmail.com>
Co-authored-by: IshaVenikar <ishavenikar7@gmail.com>
Co-authored-by: Adw8 <adwaitgharpure@gmail.com>
Reviewed-on: cerc-io/snowballtools-base#15
Co-authored-by: Nabarun Gogoi <nabarun@deepstacksoft.com>
Co-committed-by: Nabarun Gogoi <nabarun@deepstacksoft.com>
This commit is contained in:
Nabarun Gogoi 2024-10-25 10:47:04 +00:00 committed by nabarun
parent be90fc76c1
commit 2a35ec1cd5
9 changed files with 157 additions and 147 deletions

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

@ -1348,7 +1348,7 @@ export class Service {
const deployers: Deployer[] = []; const deployers: Deployer[] = [];
for (const record of deployerRecords) { for (const record of deployerRecords) {
if (record.names.length > 0) { if (record.names && record.names.length > 0) {
const deployerId = record.id; const deployerId = record.id;
const deployerLrn = record.names[0]; const deployerLrn = record.names[0];
const deployerApiUrl = record.attributes.apiUrl; const deployerApiUrl = record.attributes.apiUrl;

View File

@ -1,10 +1,8 @@
services: services:
registry: registry:
rpcEndpoint: http://laconicd.laconic.com:26657 rpcEndpoint: https://laconicd-sapo.laconic.com
gqlEndpoint: http://laconicd.laconic.com:9473/api gqlEndpoint: https://laconicd-sapo.laconic.com/api
userKey: 08c0d30ed23706330468e6936316a3bc3e69e451e394f05027ad56119bb485b9 userKey:
bondId: 820587f916d9a6a056f1e6a5a250151d9fa0c1e771347a6b8bb3d6f2090fd11b bondId:
chainId: laconic_9000-2 chainId: laconic_9000-2
gas:
fees:
gasPrice: 1alnt gasPrice: 1alnt

View File

@ -98,8 +98,7 @@ fi
# Get payment address for deployer # Get payment address for deployer
paymentAddress=$(yarn --silent laconic -c config.yml registry name resolve "$DEPLOYER_LRN" | jq -r '.[0].attributes.paymentAddress') paymentAddress=$(yarn --silent laconic -c config.yml registry name resolve "$DEPLOYER_LRN" | jq -r '.[0].attributes.paymentAddress')
paymentAmount=$(yarn --silent laconic -c config.yml registry name resolve "$DEPLOYER_LRN" | jq -r '.[0].attributes.minimumPayment') paymentAmount=$(yarn --silent laconic -c config.yml registry name resolve "$DEPLOYER_LRN" | jq -r '.[0].attributes.minimumPayment' | sed 's/alnt//g')
# Pay deployer if paymentAmount is not null # Pay deployer if paymentAmount is not null
if [[ -n "$paymentAmount" && "$paymentAmount" != "null" ]]; then if [[ -n "$paymentAmount" && "$paymentAmount" != "null" ]]; then
payment=$(yarn --silent laconic -c config.yml registry tokens send --address "$paymentAddress" --type alnt --quantity "$paymentAmount") payment=$(yarn --silent laconic -c config.yml registry tokens send --address "$paymentAddress" --type alnt --quantity "$paymentAmount")

View File

@ -1,7 +1,7 @@
{ {
"name": "frontend", "name": "frontend",
"private": true, "private": true,
"version": "0.0.0", "version": "1.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --port 3000", "dev": "vite --port 3000",

View File

@ -1,3 +1,4 @@
import { useEffect } from 'react';
import { useStopwatch } from 'react-timer-hook'; import { useStopwatch } from 'react-timer-hook';
import FormatMillisecond, { FormatMilliSecondProps } from './FormatMilliSecond'; import FormatMillisecond, { FormatMilliSecondProps } from './FormatMilliSecond';
@ -12,14 +13,21 @@ const setStopWatchOffset = (time: string) => {
interface StopwatchProps extends Omit<FormatMilliSecondProps, 'time'> { interface StopwatchProps extends Omit<FormatMilliSecondProps, 'time'> {
offsetTimestamp: Date; offsetTimestamp: Date;
isPaused: boolean;
} }
const Stopwatch = ({ offsetTimestamp, ...props }: StopwatchProps) => { const Stopwatch = ({ offsetTimestamp, isPaused, ...props }: StopwatchProps) => {
const { totalSeconds } = useStopwatch({ const { totalSeconds, pause } = useStopwatch({
autoStart: true, autoStart: true,
offsetTimestamp: offsetTimestamp, offsetTimestamp: offsetTimestamp,
}); });
useEffect(() => {
if (isPaused) {
pause();
}
}, [isPaused]);
return <FormatMillisecond time={totalSeconds * 1000} {...props} />; return <FormatMillisecond time={totalSeconds * 1000} {...props} />;
}; };

View File

@ -1,5 +1,7 @@
import React, { useCallback, useEffect } 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 axios from 'axios';
import { Deployment } from 'gql-client';
import { DeployStep, DeployStatus } from './DeployStep'; import { DeployStep, DeployStatus } from './DeployStep';
import { Stopwatch, setStopWatchOffset } from '../../StopWatch'; import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
@ -7,13 +9,37 @@ import { Heading } from '../../shared/Heading';
import { Button } from '../../shared/Button'; import { Button } from '../../shared/Button';
import { ClockOutlineIcon, WarningIcon } from '../../shared/CustomIcon'; import { ClockOutlineIcon, WarningIcon } from '../../shared/CustomIcon';
import { CancelDeploymentDialog } from '../../projects/Dialog/CancelDeploymentDialog'; import { CancelDeploymentDialog } from '../../projects/Dialog/CancelDeploymentDialog';
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 TIMEOUT_DURATION = 5000;
const Deploy = () => { const Deploy = () => {
const client = useGQLClient();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const projectId = searchParams.get('projectId'); const projectId = searchParams.get('projectId');
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
const [deployment, setDeployment] = useState<Deployment>();
const [record, setRecord] = useState<Record>();
const handleOpen = () => setOpen(!open); const handleOpen = () => setOpen(!open);
const navigate = useNavigate(); const navigate = useNavigate();
@ -23,13 +49,67 @@ const Deploy = () => {
navigate(`/${orgSlug}/projects/create`); navigate(`/${orgSlug}/projects/create`);
}, []); }, []);
useEffect(() => { const isDeploymentFailed = useMemo(() => {
const timerID = setTimeout(() => { if (!record) {
navigate(`/${orgSlug}/projects/create/success/${projectId}`); return false;
}, TIMEOUT_DURATION); }
return () => clearInterval(timerID); // Not checking for `REMOVED` status as this status is received for a brief period before receiving `DEPLOYED` status
}, []); if (record.lastState === 'CANCELLED' || record.lastState === 'ERROR') {
return true;
} else {
return false;
}
}, [record]);
const fetchDeploymentRecords = useCallback(async () => {
if (!deployment) {
return;
}
try {
const response = await axios.get(
`${deployment.deployer.deployerApiUrl}/${deployment.applicationDeploymentRequestId}`,
);
const record: Record = response.data;
setRecord(record);
} catch (err: any) {
console.log('Error fetching data from deployer', err);
}
}, [deployment]);
const fetchDeployment = useCallback(async () => {
if (!projectId) {
return;
}
const { deployments } = await client.getDeployments(projectId);
setDeployment(deployments[0]);
}, [client, projectId]);
useEffect(() => {
fetchDeployment();
fetchDeploymentRecords();
const interval = setInterval(() => {
fetchDeploymentRecords();
}, FETCH_DEPLOYMENTS_INTERVAL);
return () => {
clearInterval(interval);
};
}, [fetchDeployment, fetchDeploymentRecords]);
useEffect(() => {
if (!record) {
return;
}
if (record.lastState === 'DEPLOYED') {
navigate(`/${orgSlug}/projects/create/success/${projectId}`);
}
}, [record]);
return ( return (
<div className="space-y-7"> <div className="space-y-7">
@ -42,6 +122,7 @@ const Deploy = () => {
<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())}
isPaused={isDeploymentFailed}
/> />
</div> </div>
</div> </div>
@ -60,30 +141,36 @@ const Deploy = () => {
/> />
</div> </div>
<div> {!isDeploymentFailed ? (
<DeployStep <div>
title="Building" <DeployStep
status={DeployStatus.COMPLETE} title={record ? 'Submitted' : 'Submitting'}
step="1" status={record ? DeployStatus.COMPLETE : DeployStatus.PROCESSING}
processTime="72000" step="1"
/> />
<DeployStep
title="Deployment summary" <DeployStep
status={DeployStatus.PROCESSING} title={
step="2" record && record.lastState === 'DEPLOYED'
startTime={Date.now().toString()} ? 'Deployed'
/> : 'Deploying'
<DeployStep }
title="Running checks" status={
status={DeployStatus.NOT_STARTED} !record
step="3" ? DeployStatus.NOT_STARTED
/> : record.lastState === 'DEPLOYED'
<DeployStep ? DeployStatus.COMPLETE
title="Assigning domains" : DeployStatus.PROCESSING
status={DeployStatus.NOT_STARTED} }
step="4" step="2"
/> startTime={Date.now().toString()}
</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 {
@ -32,35 +21,11 @@ interface DeployStepsProps {
processTime?: string; processTime?: string;
} }
const DeployStep = ({ const DeployStep = ({ step, status, title, startTime }: DeployStepsProps) => {
step,
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 ( 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 +38,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 */}
@ -96,7 +55,10 @@ const DeployStep = ({
{status === DeployStatus.PROCESSING && ( {status === DeployStatus.PROCESSING && (
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<ClockOutlineIcon size={16} className="text-elements-low-em" /> <ClockOutlineIcon size={16} className="text-elements-low-em" />
<Stopwatch offsetTimestamp={setStopWatchOffset(startTime!)} /> <Stopwatch
offsetTimestamp={setStopWatchOffset(startTime!)}
isPaused={false}
/>
</div> </div>
)} )}
{status === DeployStatus.COMPLETE && ( {status === DeployStatus.COMPLETE && (
@ -107,51 +69,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>
); );
}; };

View File

@ -93,12 +93,20 @@ const DeploymentDetailsCard = ({
}; };
const fetchDeploymentLogs = async () => { const fetchDeploymentLogs = async () => {
let url = `${deployment.deployer.deployerApiUrl}/log/${deployment.applicationDeploymentRequestId}`; const statusUrl = `${deployment.deployer.deployerApiUrl}/${deployment.applicationDeploymentRequestId}`;
const res = await fetch(url, { cache: 'no-store' }); const statusRes = await fetch(statusUrl, { cache: 'no-store' }).then(
handleOpenDialog(); (res) => res.json(),
if (res.ok) { );
const logs = await res.text(); if (!statusRes.logAvailable) {
setDeploymentLogs(logs); setDeploymentLogs(statusRes.lastState);
handleOpenDialog();
} else {
const logsUrl = `${deployment.deployer.deployerApiUrl}/log/${deployment.applicationDeploymentRequestId}`;
const logsRes = await fetch(logsUrl, { cache: 'no-store' }).then((res) =>
res.text(),
);
setDeploymentLogs(logsRes);
handleOpenDialog();
} }
}; };