import axios from 'axios' import React, { useEffect, useState } from 'react' import spacetime from 'spacetime' import useSWR from 'swr' import { ClockIcon, EditIcon, EllipsisVerticalIcon, TrashIcon, } from '@sanity/icons' import { Badge, Box, Button, Card, Code, Dialog, Flex, Grid, Heading, Inline, Menu, MenuButton, MenuItem, Stack, Switch, Text, TextInput, Tooltip, useToast, } from '@sanity/ui' import { FormField } from 'sanity' import DeployHistory from './deploy-history' import DeployStatus from './deploy-status' import { useClient } from './hook/useClient' import type { SanityDeploySchema, StatusType } from './types' const fetcher = (url: string, accessToken: string, params: any) => axios .get(url, { headers: { 'content-type': 'application/json', Authorization: `Bearer ${accessToken}`, }, params }) .then((res) => res.data) const initialDeploy = { name: '', siteId: '', buildHook: '', branch: '', accessToken: '', onlyShowProductionDeploys: true, disableDeleteAction: false, } interface DeployItemProps extends SanityDeploySchema {} const DeployItem: React.FC = ({ _id, name, siteId, buildHook, branch, accessToken, onlyShowProductionDeploys, disableDeleteAction, }) => { const client = useClient() const [isLoading, setIsLoading] = useState(true) const [isDeploying, setDeploying] = useState(false) const [isHistoryOpen, setIsHistoryOpen] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false) const [isFormOpen, setIsFormOpen] = useState(false) const [pendingDeploy, setpendingDeploy] = useState(initialDeploy) const [errorMessage, setErrorMessage] = useState(null) const [status, setStatus] = useState('LOADING') const [timestamp, setTimestamp] = useState(null) const [buildTime, setBuildTime] = useState(null) const toast = useToast() const deploymentDataParam: any = {} if (onlyShowProductionDeploys) deploymentDataParam.production = true; const { data: deploymentData } = useSWR( () => [ `https://api.netlify.com/api/v1/sites/${siteId}/deploys`, accessToken, deploymentDataParam ], ([url, token, params]) => fetcher(url, token, params), { errorRetryCount: 3, refreshInterval: 1000, onError: (err) => { setStatus('ERROR') setErrorMessage(err.response?.data?.error?.message || err?.message || 'Something went wrong!') setIsLoading(false) }, } ) const onDeploy = (_name: string, _url: string) => { setStatus('ENQUEUED') setDeploying(true) setTimestamp(null) setBuildTime(null) const options: any = {} if (branch) options.trigger_branch = branch; axios .post(_url, options) .then(() => { toast.push({ status: 'success', title: 'Success!', description: `Triggered Deployment: ${_name}`, }) }) .catch((err) => { setDeploying(false) toast.push({ status: 'error', title: 'Deploy Failed.', description: `${err}`, }) }) } const onCancel = (id: string, token: string) => { setIsLoading(true) axios .post(`https://api.netlify.com/api/v1/deploys/${id}/cancel`, null, { headers: { 'content-type': 'application/json', Authorization: `Bearer ${accessToken}`, }, }) .then((res) => res.data) .then((res) => { setStatus('CANCELED') setDeploying(false) setIsLoading(false) setBuildTime(null) setTimestamp(res.updated_at) }) } const onRemove = (_name: string, id: string) => { setIsLoading(true) client.delete(id).then(() => { toast.push({ status: 'success', title: `Successfully deleted deployment: ${_name}`, }) }) } const onEdit = () => { setpendingDeploy({ name, siteId, buildHook, branch, accessToken: '', onlyShowProductionDeploys, disableDeleteAction, }) setIsFormOpen(true) } const onSubmitEdit = async () => { const update: any = { name: pendingDeploy.name, siteId: pendingDeploy.siteId, buildHook: pendingDeploy.buildHook, branch: pendingDeploy.branch, onlyShowProductionDeploys: pendingDeploy.onlyShowProductionDeploys, disableDeleteAction: pendingDeploy.disableDeleteAction, } if (pendingDeploy.accessToken) { update.accessToken = pendingDeploy.accessToken } client .patch(_id) .set(update) .commit() .then(() => { toast.push({ status: 'success', title: 'Success!', description: `Updated Deployment: ${pendingDeploy.name}`, }) setIsFormOpen(false) setIsSubmitting(false) }) } // set status when new deployment data comes in useEffect(() => { let isSubscribed = true if (deploymentData && isSubscribed) { const latestDeployment = deploymentData[0] setStatus(latestDeployment?.state?.toUpperCase() || 'READY') if (latestDeployment?.created_at) setTimestamp(latestDeployment?.created_at) setIsLoading(false) } return () => { isSubscribed = false } }, [deploymentData]) // update deploy state after status is updated useEffect(() => { let isSubscribed = true if (isSubscribed) { if (status === 'READY' || status === 'ERROR' || status === 'CANCELED') { setDeploying(false) } else if (status === 'BUILDING' || status === 'ENQUEUED') { setDeploying(true) } } return () => { isSubscribed = false } }, [status]) // count build time const tick = (_timestamp: string | null) => { if (_timestamp) { setBuildTime(spacetime.now().since(spacetime(_timestamp)).rounded) } } useEffect(() => { let isTicking = true const timer = setInterval(() => { if (isTicking && isDeploying) { tick(timestamp) } }, 1000) if (!isDeploying) { clearInterval(timer) } return () => { isTicking = false clearInterval(timer) } }, [timestamp, isDeploying]) return ( <> {name} {deploymentData ? deploymentData[0].branch : '–'} {siteId} {isDeploying ? buildTime || '--' : timestamp ? spacetime.now().since(spacetime(timestamp)).rounded : '--'}