182 lines
4.3 KiB
TypeScript
182 lines
4.3 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* Single-fetch anatomy hook shared by the Product / Hosting tabs.
|
|
* Hardened against silent failure: 10s timeout, error surfacing, and
|
|
* graceful unmount.
|
|
*
|
|
* Uses SWR to deduplicate requests across components (ProjectStagePill,
|
|
* ProjectHeaderUrls, Hosting page) so they share a single network request
|
|
* instead of hammering the API independently.
|
|
*/
|
|
|
|
import useSWR from "swr";
|
|
import { useCallback } from "react";
|
|
|
|
export interface Anatomy {
|
|
project: {
|
|
id: string;
|
|
name: string;
|
|
gitea?: string;
|
|
coolifyProjectUuid?: string;
|
|
};
|
|
codebasesReason?: "no_repo" | "empty_repo";
|
|
product: {
|
|
codebases: Array<{
|
|
id: string;
|
|
label: string;
|
|
path: string;
|
|
hint?: string;
|
|
}>;
|
|
images: Array<{
|
|
uuid: string;
|
|
name: string;
|
|
image: string;
|
|
version: string;
|
|
serviceType?: string;
|
|
status?: string;
|
|
}>;
|
|
};
|
|
hosting: {
|
|
live: Array<{
|
|
uuid: string;
|
|
name: string;
|
|
source: "repo" | "image";
|
|
sourceLabel: string;
|
|
status: string;
|
|
fqdn?: string;
|
|
domains: string[];
|
|
branch?: string;
|
|
buildPack?: string;
|
|
lastBuild?: { status: string; finishedAt?: string; commit?: string };
|
|
inFlightBuild?: { status: string; finishedAt?: string; commit?: string };
|
|
}>;
|
|
previews: Array<{
|
|
id: string;
|
|
name: string;
|
|
command?: string;
|
|
port: number;
|
|
url: string;
|
|
state: string;
|
|
startedAt: string;
|
|
}>;
|
|
};
|
|
infrastructure: {
|
|
databases: Array<{
|
|
uuid: string;
|
|
name: string;
|
|
type: string;
|
|
status: string;
|
|
isPublic: boolean;
|
|
publicPort?: number;
|
|
internalAddress?: string;
|
|
consumerEnvKey: string;
|
|
}>;
|
|
providers: Array<{
|
|
id: string;
|
|
category: "auth" | "email" | "payments" | "llm" | "storage";
|
|
vendor: string;
|
|
attachments: Array<{
|
|
resourceUuid: string;
|
|
resourceName: string;
|
|
resourceKind: "app" | "service";
|
|
keys: string[];
|
|
}>;
|
|
}>;
|
|
bundledStorage: {
|
|
status: "ready" | "pending" | "partial" | "error" | "unprovisioned";
|
|
bucketName?: string;
|
|
hmacAccessId?: string;
|
|
region?: string;
|
|
errorMessage?: string;
|
|
};
|
|
secrets: {
|
|
total: number;
|
|
byResource: Array<{
|
|
resourceUuid: string;
|
|
resourceName: string;
|
|
resourceKind: "app" | "service";
|
|
count: number;
|
|
keys: string[];
|
|
}>;
|
|
};
|
|
};
|
|
}
|
|
|
|
export interface UseAnatomyResult {
|
|
anatomy: Anatomy | null;
|
|
loading: boolean;
|
|
error: string | null;
|
|
reload: () => void;
|
|
}
|
|
|
|
export interface UseAnatomyOptions {
|
|
/** When set, re-fetch anatomy every N ms while the component is
|
|
* mounted. Used by the project-header status pill so it surfaces
|
|
* Coolify build state transitions live (e.g. queued → in_progress
|
|
* → success) without the user having to refresh. Pass undefined or
|
|
* 0 to disable polling. */
|
|
pollMs?: number;
|
|
}
|
|
|
|
const fetcher = async (url: string) => {
|
|
const controller = new AbortController();
|
|
const timeout = setTimeout(() => controller.abort(), 10_000);
|
|
try {
|
|
const res = await fetch(url, {
|
|
credentials: "include",
|
|
signal: controller.signal,
|
|
cache: "no-store",
|
|
});
|
|
let body: unknown = {};
|
|
try {
|
|
body = await res.json();
|
|
} catch {
|
|
/* keep {} */
|
|
}
|
|
if (!res.ok) {
|
|
const msg =
|
|
(body as { error?: string }).error ||
|
|
`HTTP ${res.status} ${res.statusText}`.trim();
|
|
throw new Error(msg);
|
|
}
|
|
return body as Anatomy;
|
|
} catch (err: any) {
|
|
if (err.name === "AbortError") {
|
|
throw new Error("Request timed out after 10s.");
|
|
}
|
|
throw err;
|
|
} finally {
|
|
clearTimeout(timeout);
|
|
}
|
|
};
|
|
|
|
export function useAnatomy(
|
|
projectId: string,
|
|
options: UseAnatomyOptions = {},
|
|
): UseAnatomyResult {
|
|
const pollMs = options.pollMs && options.pollMs > 0 ? options.pollMs : 0;
|
|
|
|
const { data, error, isLoading, mutate } = useSWR<Anatomy, Error>(
|
|
projectId ? `/api/projects/${projectId}/anatomy` : null,
|
|
fetcher,
|
|
{
|
|
refreshInterval: pollMs,
|
|
dedupingInterval: 5000,
|
|
revalidateOnFocus: false,
|
|
revalidateOnReconnect: false,
|
|
},
|
|
);
|
|
|
|
const reload = useCallback(() => {
|
|
mutate();
|
|
}, [mutate]);
|
|
|
|
return {
|
|
anatomy: data ?? null,
|
|
loading: isLoading,
|
|
error: error?.message ?? null,
|
|
reload,
|
|
};
|
|
}
|