From 2a35ec1cd5d87b9bf4448eee8bc6120c4e264357 Mon Sep 17 00:00:00 2001 From: Nabarun Gogoi Date: Fri, 25 Oct 2024 10:47:04 +0000 Subject: [PATCH] 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 Co-authored-by: IshaVenikar Co-authored-by: Adw8 Reviewed-on: https://git.vdb.to/cerc-io/snowballtools-base/pulls/15 Co-authored-by: Nabarun Gogoi Co-committed-by: Nabarun Gogoi --- .../backend/environments/local.toml.example | 10 -- packages/backend/src/service.ts | 2 +- packages/deployer/config.yml | 10 +- packages/deployer/deploy-frontend.sh | 3 +- packages/frontend/package.json | 2 +- .../frontend/src/components/StopWatch.tsx | 12 +- .../src/components/projects/create/Deploy.tsx | 151 ++++++++++++++---- .../components/projects/create/DeployStep.tsx | 94 +---------- .../deployments/DeploymentDetailsCard.tsx | 20 ++- 9 files changed, 157 insertions(+), 147 deletions(-) diff --git a/packages/backend/environments/local.toml.example b/packages/backend/environments/local.toml.example index 2c8eca54..efebff59 100644 --- a/packages/backend/environments/local.toml.example +++ b/packages/backend/environments/local.toml.example @@ -20,16 +20,6 @@ clientId = "" clientSecret = "" -[google] - clientId = "" - clientSecret = "" - -[turnkey] - apiBaseUrl = "https://api.turnkey.com" - apiPrivateKey = "" - apiPublicKey = "" - defaultOrganizationId = "" - [registryConfig] fetchDeploymentRecordDelay = 5000 checkAuctionStatusDelay = 5000 diff --git a/packages/backend/src/service.ts b/packages/backend/src/service.ts index bcd7e4b8..028b662b 100644 --- a/packages/backend/src/service.ts +++ b/packages/backend/src/service.ts @@ -1348,7 +1348,7 @@ export class Service { const deployers: Deployer[] = []; for (const record of deployerRecords) { - if (record.names.length > 0) { + if (record.names && record.names.length > 0) { const deployerId = record.id; const deployerLrn = record.names[0]; const deployerApiUrl = record.attributes.apiUrl; diff --git a/packages/deployer/config.yml b/packages/deployer/config.yml index 2ffbadc5..ce7d6b54 100644 --- a/packages/deployer/config.yml +++ b/packages/deployer/config.yml @@ -1,10 +1,8 @@ services: registry: - rpcEndpoint: http://laconicd.laconic.com:26657 - gqlEndpoint: http://laconicd.laconic.com:9473/api - userKey: 08c0d30ed23706330468e6936316a3bc3e69e451e394f05027ad56119bb485b9 - bondId: 820587f916d9a6a056f1e6a5a250151d9fa0c1e771347a6b8bb3d6f2090fd11b + rpcEndpoint: https://laconicd-sapo.laconic.com + gqlEndpoint: https://laconicd-sapo.laconic.com/api + userKey: + bondId: chainId: laconic_9000-2 - gas: - fees: gasPrice: 1alnt diff --git a/packages/deployer/deploy-frontend.sh b/packages/deployer/deploy-frontend.sh index 6631f274..4af80bf1 100755 --- a/packages/deployer/deploy-frontend.sh +++ b/packages/deployer/deploy-frontend.sh @@ -98,8 +98,7 @@ fi # Get payment address for deployer 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 if [[ -n "$paymentAmount" && "$paymentAmount" != "null" ]]; then payment=$(yarn --silent laconic -c config.yml registry tokens send --address "$paymentAddress" --type alnt --quantity "$paymentAmount") diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 3a8b9a21..d395b256 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "0.0.0", + "version": "1.0.0", "type": "module", "scripts": { "dev": "vite --port 3000", diff --git a/packages/frontend/src/components/StopWatch.tsx b/packages/frontend/src/components/StopWatch.tsx index d9c62775..fb512e24 100644 --- a/packages/frontend/src/components/StopWatch.tsx +++ b/packages/frontend/src/components/StopWatch.tsx @@ -1,3 +1,4 @@ +import { useEffect } from 'react'; import { useStopwatch } from 'react-timer-hook'; import FormatMillisecond, { FormatMilliSecondProps } from './FormatMilliSecond'; @@ -12,14 +13,21 @@ const setStopWatchOffset = (time: string) => { interface StopwatchProps extends Omit { offsetTimestamp: Date; + isPaused: boolean; } -const Stopwatch = ({ offsetTimestamp, ...props }: StopwatchProps) => { - const { totalSeconds } = useStopwatch({ +const Stopwatch = ({ offsetTimestamp, isPaused, ...props }: StopwatchProps) => { + const { totalSeconds, pause } = useStopwatch({ autoStart: true, offsetTimestamp: offsetTimestamp, }); + useEffect(() => { + if (isPaused) { + pause(); + } + }, [isPaused]); + return ; }; diff --git a/packages/frontend/src/components/projects/create/Deploy.tsx b/packages/frontend/src/components/projects/create/Deploy.tsx index e943dfed..3bf8ac13 100644 --- a/packages/frontend/src/components/projects/create/Deploy.tsx +++ b/packages/frontend/src/components/projects/create/Deploy.tsx @@ -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 axios from 'axios'; +import { Deployment } from 'gql-client'; import { DeployStep, DeployStatus } from './DeployStep'; import { Stopwatch, setStopWatchOffset } from '../../StopWatch'; @@ -7,13 +9,37 @@ import { Heading } from '../../shared/Heading'; import { Button } from '../../shared/Button'; import { ClockOutlineIcon, WarningIcon } from '../../shared/CustomIcon'; 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 client = useGQLClient(); + const [searchParams] = useSearchParams(); const projectId = searchParams.get('projectId'); const [open, setOpen] = React.useState(false); + const [deployment, setDeployment] = useState(); + const [record, setRecord] = useState(); + const handleOpen = () => setOpen(!open); const navigate = useNavigate(); @@ -23,13 +49,67 @@ const Deploy = () => { navigate(`/${orgSlug}/projects/create`); }, []); - useEffect(() => { - const timerID = setTimeout(() => { - navigate(`/${orgSlug}/projects/create/success/${projectId}`); - }, TIMEOUT_DURATION); + const isDeploymentFailed = useMemo(() => { + if (!record) { + return false; + } - 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 (
@@ -42,6 +122,7 @@ const Deploy = () => {
@@ -60,30 +141,36 @@ const Deploy = () => { /> -
- - - - -
+ {!isDeploymentFailed ? ( +
+ + + +
+ ) : ( +
+ +
+ )} ); }; diff --git a/packages/frontend/src/components/projects/create/DeployStep.tsx b/packages/frontend/src/components/projects/create/DeployStep.tsx index 2453b7ee..4ee5e5c2 100644 --- a/packages/frontend/src/components/projects/create/DeployStep.tsx +++ b/packages/frontend/src/components/projects/create/DeployStep.tsx @@ -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 { @@ -32,35 +21,11 @@ interface DeployStepsProps { processTime?: string; } -const DeployStep = ({ - 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; - +const DeployStep = ({ step, status, title, startTime }: DeployStepsProps) => { return (
- {/* Collapisble trigger */}
- {' '} )} - - {/* Collapsible */} - -
- {/* Logs */} - {processLogs.map((log, key) => { - return ( -

- {log} -

- ); - })} - - {/* End of logs ref used for hiding gradient overlay */} -
- - {/* Overflow gradient overlay */} - {!hideGradientOverlay && ( -
- )} - - {/* Copy log button */} -
- -
-
-
); }; diff --git a/packages/frontend/src/components/projects/project/deployments/DeploymentDetailsCard.tsx b/packages/frontend/src/components/projects/project/deployments/DeploymentDetailsCard.tsx index cac7e6d1..5dbabfee 100644 --- a/packages/frontend/src/components/projects/project/deployments/DeploymentDetailsCard.tsx +++ b/packages/frontend/src/components/projects/project/deployments/DeploymentDetailsCard.tsx @@ -93,12 +93,20 @@ const DeploymentDetailsCard = ({ }; const fetchDeploymentLogs = async () => { - let url = `${deployment.deployer.deployerApiUrl}/log/${deployment.applicationDeploymentRequestId}`; - const res = await fetch(url, { cache: 'no-store' }); - handleOpenDialog(); - if (res.ok) { - const logs = await res.text(); - setDeploymentLogs(logs); + const statusUrl = `${deployment.deployer.deployerApiUrl}/${deployment.applicationDeploymentRequestId}`; + const statusRes = await fetch(statusUrl, { cache: 'no-store' }).then( + (res) => res.json(), + ); + if (!statusRes.logAvailable) { + 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(); } };