import { useState, useEffect, useCallback, useRef } from "react"; import { useIntl } from "react-intl"; import { Main } from "@strapi/design-system"; import { Rocket } from "@strapi/icons"; import { useFetchClient } from "@strapi/strapi/admin"; import { Box, Button, Typography, Table, Thead, Tbody, Tr, Td, Th, Flex, } from "@strapi/design-system"; import { formatDate } from "../utils/date"; import { getTranslation } from "../utils/getTranslation"; import { Deployment, GetDeploymentsResponse } from "../types"; import { getDeploymentStatusColor } from "../utils/getDeploymentStatusColor"; const POLL_INTERVAL_MS = 30_000; // 30 seconds; adjust if needed const getErrorMessageForStatus = (status?: number) => { switch (status) { case 401: return "Unauthorized - Invalid or missing Coolify API token"; case 403: return "Forbidden - Insufficient permissions"; case 404: return "Not Found - Application or endpoint not found"; case 500: return "Internal Server Error - Coolify API error"; case 502: return "Bad Gateway - Coolify API unavailable"; case 503: return "Service Unavailable - Coolify is down"; default: return status ? `HTTP Error ${status}` : "Network error"; } }; const HomePage = () => { const { formatMessage } = useIntl(); const { post, get } = useFetchClient(); const [isLoading, setIsLoading] = useState(false); const [message, setMessage] = useState(""); const [deployments, setDeployments] = useState([]); const [isLoadingDeployments, setIsLoadingDeployments] = useState(true); const [lastRefreshTime, setLastRefreshTime] = useState(null); const [deploymentsError, setDeploymentsError] = useState<{ status?: number; message?: string; } | null>(null); useEffect(() => { fetchDeployments(); }, []); // Use a ref to store interval id so we can clear it from callbacks const pollingRef = useRef(null); const fetchDeployments = useCallback(async () => { setIsLoadingDeployments(true); try { const response = await get( "/publish-coolify/deployments", ); const { data } = response; setDeployments(data.deployments ?? []); setDeploymentsError(null); // Clear error on success } catch (error) { console.error("Failed to request deployments:", error); // Show user-friendly error for debugging if (error && typeof error === "object" && "response" in error) { const err = error as any; console.error("Response status: ", err.response?.status); console.error("Response data:", err.response?.data); // Extract the actual error message from the response let errorMessage = err.response?.data?.details || err.response?.data?.error || err.response?.data?.message; // If details contains a JSON string with a message, extract it if ( errorMessage && typeof errorMessage === "string" && errorMessage.includes('"message"') ) { try { const parsed = JSON.parse(errorMessage); errorMessage = parsed.message || errorMessage; } catch { // If parsing fails, use the original string } } setDeploymentsError({ status: err.response?.status, message: errorMessage || err.message || "Unknown error", }); } else { setDeploymentsError({ message: error instanceof Error ? error.message : "Unknown error", }); } setDeployments([]); } finally { setIsLoadingDeployments(false); setLastRefreshTime(new Date()); } }, [get]); useEffect(() => { // start polling when component mounts const startPolling = () => { if (pollingRef.current == null) { pollingRef.current = window.setInterval(() => { fetchDeployments(); }, POLL_INTERVAL_MS) as unknown as number; } }; const stopPolling = () => { if (pollingRef.current != null) { clearInterval(pollingRef.current as number); pollingRef.current = null; } }; const handleVisibilityChange = () => { if (document.hidden) { // pause polling when tab is hidden stopPolling(); } else { // when visible again fetch once and resume polling fetchDeployments(); startPolling(); } }; // initial fetch and start polling fetchDeployments(); startPolling(); document.addEventListener("visibilitychange", handleVisibilityChange); return () => { stopPolling(); document.removeEventListener("visibilitychange", handleVisibilityChange); }; }, [fetchDeployments]); const onClick = async () => { setIsLoading(true); setMessage(""); try { const response = await post("/publish-coolify/deploy"); if (response.data.success) { setMessage( "✅ " + formatMessage({ id: getTranslation("message.deploy.success") }), ); // Refresh deployments list after triggering deploy setTimeout(() => fetchDeployments(), 2000); } else { setMessage( "❌ " + formatMessage({ id: getTranslation("message.deploy.error") }) + ": " + (response.data.details || formatMessage({ id: getTranslation("message.deploy.failed") })), ); } } catch (error) { console.error("Deploy error:", error); setMessage( "❌ " + formatMessage({ id: getTranslation("message.deploy.error") }) + ": " + formatMessage({ id: getTranslation("message.deploy.unable") }), ); } finally { setIsLoading(false); } }; // TODO just because file rename broke somehow const getStatusColor = (status: Deployment["status"]) => { switch (status) { case "finished": return "success600"; case "failed": return "danger600"; case "in_progress": return "warning600"; case "queued": return "secondary600"; case "cancelled-by-user": return "neutral600"; default: return "neutral600"; } }; const getStatusLabel = (status: string) => { // Build the translation key relative to the plugin, e.g.: // getTranslation('deployment.status.queued') -> 'ssg.deployment.status.queued' const keySuffix = `deployment.status.${status.toLowerCase().replace(/-/g, "_")}`; const id = getTranslation(keySuffix); return formatMessage({ id, defaultMessage: status }); }; return (
{formatMessage({ id: getTranslation("page.title") })}
{/* Plugin {formatMessage({ id: getTranslation('plugin.name') })} */}
{formatMessage({ id: getTranslation("page.description") })}
{message && ( {message} )} {formatMessage({ id: getTranslation("deployments.title") })} {lastRefreshTime && ( {formatMessage({ id: getTranslation("deployments.lastRefresh"), })}{" "} : {formatDate(new Date(lastRefreshTime.toISOString()))} ( {formatMessage({ id: getTranslation("deployments.everyRefresh"), })}{" "} {POLL_INTERVAL_MS / 1000}s) )} {deployments.length === 0 && !isLoadingDeployments ? ( ) : ( deployments.map((deployment) => ( )) )}
{formatMessage({ id: getTranslation("deployments.table.status"), })} {formatMessage({ id: getTranslation("deployments.table.startDate"), })} {formatMessage({ id: getTranslation("deployments.table.endDate"), })} {formatMessage({ id: getTranslation("deployments.table.source"), })}
{deploymentsError ? (
{formatMessage({ id: getTranslation("deployments.error"), })} {" - "} {getErrorMessageForStatus(deploymentsError.status)} {deploymentsError.message && (
{deploymentsError.message}
)}
) : ( formatMessage({ id: getTranslation("deployments.empty"), }) )}
{getStatusLabel(deployment.status)} {formatDate(new Date(deployment.created_at))} {deployment.finished_at ? ( <> {formatDate(new Date(deployment.finished_at))} {" ("} {Math.round( (new Date(deployment.finished_at).getTime() - new Date(deployment.created_at).getTime()) / 1000, )} s) ) : ( "-" )} {deployment.is_webhook ? "🔗 " + formatMessage({ id: getTranslation("deployments.source.webhook"), }) + " (Piksail)" : deployment.is_api ? "⚡ " + formatMessage({ id: getTranslation("deployments.source.api"), }) : "🖱️ " + formatMessage({ id: getTranslation("deployments.source.manual"), })}
); }; export { HomePage };