diff --git a/apps/dokploy/components/dashboard/application/domains/show-domains.tsx b/apps/dokploy/components/dashboard/application/domains/show-domains.tsx index 23d9e46bb6..903eb82f0f 100644 --- a/apps/dokploy/components/dashboard/application/domains/show-domains.tsx +++ b/apps/dokploy/components/dashboard/application/domains/show-domains.tsx @@ -25,7 +25,7 @@ import { XCircle, } from "lucide-react"; import Link from "next/link"; -import { useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { toast } from "sonner"; import { DialogAction } from "@/components/shared/dialog-action"; import { Badge } from "@/components/ui/badge"; @@ -84,7 +84,7 @@ export const ShowDomains = ({ id, type }: Props) => { const { data: permissions } = api.user.getPermissions.useQuery(); const canCreateDomain = permissions?.domain.create ?? false; const canDeleteDomain = permissions?.domain.delete ?? false; - const { data: application } = + const { data: application, isFetched: isApplicationFetched } = type === "application" ? api.application.one.useQuery( { @@ -105,6 +105,10 @@ export const ShowDomains = ({ id, type }: Props) => { const [validationStates, setValidationStates] = useState( {}, ); + const autoValidatedHostsRef = useRef>(new Set()); + const validationRequestIdRef = useRef(0); + const hostValidationRequestIdsRef = useRef>(new Map()); + const lastAutoValidatedServerIpRef = useRef(undefined); const [viewMode, setViewMode] = useState<"grid" | "table">(() => { if (typeof window !== "undefined") { return ( @@ -118,7 +122,7 @@ export const ShowDomains = ({ id, type }: Props) => { const [columnFilters, setColumnFilters] = useState([]); const [columnVisibility, setColumnVisibility] = useState({}); const [rowSelection, setRowSelection] = useState({}); - const { data: ip } = api.settings.getIp.useQuery(); + const { data: ip, isFetched: isIpFetched } = api.settings.getIp.useQuery(); const { data, @@ -157,42 +161,155 @@ export const ShowDomains = ({ id, type }: Props) => { } }; - const handleValidateDomain = async (host: string) => { - setValidationStates((prev) => ({ - ...prev, - [host]: { isLoading: true }, - })); + const resolveServerIp = useCallback(() => { + const remoteIp = application?.server?.ipAddress?.toString(); + if (application?.serverId) { + return remoteIp || undefined; + } - try { - const result = await validateDomain({ - domain: host, - serverIp: - application?.server?.ipAddress?.toString() || ip?.toString() || "", - }); + return ip?.toString() || undefined; + }, [application?.server?.ipAddress, application?.serverId, ip]); + + const handleValidateDomain = useCallback( + async (host: string, serverIpOverride?: string) => { + const serviceRequestId = validationRequestIdRef.current; + const hostRequestId = + (hostValidationRequestIdsRef.current.get(host) ?? 0) + 1; + hostValidationRequestIdsRef.current.set(host, hostRequestId); + + const serverIp = + serverIpOverride ?? + (application?.server?.ipAddress?.toString() || ip?.toString() || ""); + + const isCurrentRequest = () => + validationRequestIdRef.current === serviceRequestId && + hostValidationRequestIdsRef.current.get(host) === hostRequestId; + + if (!isCurrentRequest()) { + return; + } setValidationStates((prev) => ({ ...prev, - [host]: { - isLoading: false, - isValid: result.isValid, - error: result.error, - resolvedIp: result.resolvedIp, - cdnProvider: result.cdnProvider, - message: result.error && result.isValid ? result.error : undefined, - }, - })); - } catch (err) { - const error = err as Error; - setValidationStates((prev) => ({ - ...prev, - [host]: { - isLoading: false, - isValid: false, - error: error.message || "Failed to validate domain", - }, + [host]: { isLoading: true }, })); + + try { + const result = await validateDomain({ + domain: host, + serverIp, + }); + + if (!isCurrentRequest()) { + return; + } + + setValidationStates((prev) => ({ + ...prev, + [host]: { + isLoading: false, + isValid: result.isValid, + error: result.error, + resolvedIp: result.resolvedIp, + cdnProvider: result.cdnProvider, + message: result.error && result.isValid ? result.error : undefined, + }, + })); + } catch (err) { + if (!isCurrentRequest()) { + return; + } + + const error = err as Error; + setValidationStates((prev) => ({ + ...prev, + [host]: { + isLoading: false, + isValid: false, + error: error.message || "Failed to validate domain", + }, + })); + } + }, + [validateDomain, application?.server?.ipAddress, ip], + ); + + useEffect(() => { + validationRequestIdRef.current += 1; + autoValidatedHostsRef.current = new Set(); + hostValidationRequestIdsRef.current = new Map(); + lastAutoValidatedServerIpRef.current = undefined; + setValidationStates({}); + }, [id]); + + useEffect(() => { + if (!data?.length || !isApplicationFetched || !application) { + return; } - }; + + if (application.serverId) { + if (!application.server?.ipAddress) { + return; + } + } else if (!isIpFetched) { + return; + } + + const serverIp = resolveServerIp(); + if (!serverIp) { + return; + } + + if (lastAutoValidatedServerIpRef.current !== serverIp) { + lastAutoValidatedServerIpRef.current = serverIp; + autoValidatedHostsRef.current = new Set(); + } + + const hostsToValidate = data + .map((item) => item.host) + .filter((host) => { + if (autoValidatedHostsRef.current.has(host)) { + return false; + } + + autoValidatedHostsRef.current.add(host); + return true; + }); + + if (hostsToValidate.length === 0) { + return; + } + + const maxConcurrent = 5; + let nextIndex = 0; + + const runNext = async () => { + while (nextIndex < hostsToValidate.length) { + const host = hostsToValidate[nextIndex]; + nextIndex += 1; + if (!host) { + continue; + } + await handleValidateDomain(host, serverIp); + } + }; + + void Promise.all( + Array.from( + { length: Math.min(maxConcurrent, hostsToValidate.length) }, + () => runNext(), + ), + ); + }, [ + data, + isIpFetched, + isApplicationFetched, + application, + application?.serverId, + application?.server?.ipAddress, + resolveServerIp, + handleValidateDomain, + ]); const columns = createColumns({ id,