-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
feat: auto-validate DNS when Domains page loads #5001
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: canary
Are you sure you want to change the base?
Changes from all commits
d538902
cc85be2
095b256
b05f6d1
4fd256e
1214966
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<ValidationStates>( | ||
| {}, | ||
| ); | ||
| const autoValidatedHostsRef = useRef<Set<string>>(new Set()); | ||
| const validationRequestIdRef = useRef(0); | ||
| const hostValidationRequestIdsRef = useRef<Map<string, number>>(new Map()); | ||
| const lastAutoValidatedServerIpRef = useRef<string | undefined>(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<ColumnFiltersState>([]); | ||
| const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({}); | ||
| 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); | ||
|
Comment on lines
+271
to
+275
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the application's server IP or the global server IP changes while this page remains mounted, the effect reruns but Knowledge Base Used: Git Providers and Domains |
||
| 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, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If a user navigates between services that share a hostname while the first service's automatic check is still running, that request writes into the current host-keyed state after the ID reset, causing the new service to display a DNS result calculated with the previous service's server IP.
Knowledge Base Used: Git Providers and Domains