-
-
-
-
- {resourceFullDomain}
-
-
-
+ {resource.http && (
+
+
+
+
+
+ {resourceFullDomain}
+
+
- )}
-
-
-
-
+
+ )}
+
+
+
+
-
-
-
-
-
+
+
+
+
+
-
setEditDomainOpen(setOpen)}
- >
-
-
- Edit Domain
-
- Select a domain for your resource
-
-
-
- setEditDomainOpen(setOpen)}
+ >
+
+
+ Edit Domain
+
+ Select a domain for your resource
+
+
+
+ {
+ const selected =
+ res === null
+ ? null
+ : {
+ domainId: res.domainId,
+ subdomain: res.subdomain,
+ fullDomain: res.fullDomain,
+ baseDomain: res.baseDomain,
+ domainNamespaceId:
+ res.domainNamespaceId
+ };
+
+ setSelectedDomain(selected);
+ }}
+ />
+
+
+
+
+
+
-
-
-
-
-
-
-
-
- >
- )
+ }}
+ >
+ Select Domain
+
+
+
+
+ >
);
}
diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/layout.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/layout.tsx
index c453b577..f410b4c8 100644
--- a/src/app/[orgId]/settings/resources/proxy/[niceId]/layout.tsx
+++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/layout.tsx
@@ -13,9 +13,10 @@ import { GetOrgResponse } from "@server/routers/org";
import OrgProvider from "@app/providers/OrgProvider";
import { cache } from "react";
import ResourceInfoBox from "@app/components/ResourceInfoBox";
-import { GetSiteResponse } from "@server/routers/site";
import { getTranslations } from "next-intl/server";
+export const dynamic = "force-dynamic";
+
interface ResourceLayoutProps {
children: React.ReactNode;
params: Promise<{ niceId: string; orgId: string }>;
diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx
index 00f487ea..ada2defe 100644
--- a/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx
+++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useEffect, useState, use } from "react";
+import HealthCheckDialog from "@/components/HealthCheckDialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
@@ -11,11 +11,34 @@ import {
SelectValue
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
-import { AxiosResponse } from "axios";
-import { ListTargetsResponse } from "@server/routers/target/listTargets";
-import { useForm } from "react-hook-form";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { z } from "zod";
+import { ContainersSelector } from "@app/components/ContainersSelector";
+import { HeadersInput } from "@app/components/HeadersInput";
+import {
+ PathMatchDisplay,
+ PathMatchModal,
+ PathRewriteDisplay,
+ PathRewriteModal
+} from "@app/components/PathMatchRenameModal";
+import {
+ SettingsContainer,
+ SettingsSection,
+ SettingsSectionBody,
+ SettingsSectionDescription,
+ SettingsSectionForm,
+ SettingsSectionHeader,
+ SettingsSectionTitle
+} from "@app/components/Settings";
+import { SwitchInput } from "@app/components/SwitchInput";
+import { Alert, AlertDescription } from "@app/components/ui/alert";
+import { Badge } from "@app/components/ui/badge";
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList
+} from "@app/components/ui/command";
import {
Form,
FormControl,
@@ -25,17 +48,11 @@ import {
FormLabel,
FormMessage
} from "@app/components/ui/form";
-import { CreateTargetResponse } from "@server/routers/target";
import {
- ColumnDef,
- getFilteredRowModel,
- getSortedRowModel,
- getPaginationRowModel,
- getCoreRowModel,
- useReactTable,
- flexRender,
- Row
-} from "@tanstack/react-table";
+ Popover,
+ PopoverContent,
+ PopoverTrigger
+} from "@app/components/ui/popover";
import {
Table,
TableBody,
@@ -44,153 +61,55 @@ import {
TableHeader,
TableRow
} from "@app/components/ui/table";
-import { toast } from "@app/hooks/useToast";
-import { useResourceContext } from "@app/hooks/useResourceContext";
-import { ArrayElement } from "@server/types/ArrayElement";
-import { formatAxiosError } from "@app/lib/api/formatAxiosError";
-import { useEnvContext } from "@app/hooks/useEnvContext";
-import { createApiClient } from "@app/lib/api";
-import { GetSiteResponse, ListSitesResponse } from "@server/routers/site";
-import {
- SettingsContainer,
- SettingsSection,
- SettingsSectionHeader,
- SettingsSectionTitle,
- SettingsSectionDescription,
- SettingsSectionBody,
- SettingsSectionForm
-} from "@app/components/Settings";
-import { SwitchInput } from "@app/components/SwitchInput";
-import { useRouter } from "next/navigation";
-import { isTargetValid } from "@server/lib/validators";
-import { tlsNameSchema } from "@server/lib/schemas";
-import {
- CheckIcon,
- ChevronsUpDown,
- Settings,
- Heart,
- Check,
- CircleCheck,
- CircleX,
- ArrowRight,
- Plus,
- MoveRight,
- ArrowUp,
- Info,
- ArrowDown,
- AlertTriangle
-} from "lucide-react";
-import { ContainersSelector } from "@app/components/ContainersSelector";
-import { useTranslations } from "next-intl";
-import { build } from "@server/build";
-import HealthCheckDialog from "@/components/HealthCheckDialog";
-import { DockerManager, DockerState } from "@app/lib/docker";
-import { Container } from "@server/routers/site";
-import {
- Popover,
- PopoverContent,
- PopoverTrigger
-} from "@app/components/ui/popover";
-import { cn } from "@app/lib/cn";
-import { CaretSortIcon } from "@radix-ui/react-icons";
-import {
- Command,
- CommandEmpty,
- CommandGroup,
- CommandInput,
- CommandItem,
- CommandList
-} from "@app/components/ui/command";
-import { parseHostTarget } from "@app/lib/parseHostTarget";
-import { HeadersInput } from "@app/components/HeadersInput";
-import {
- PathMatchDisplay,
- PathMatchModal,
- PathRewriteDisplay,
- PathRewriteModal
-} from "@app/components/PathMatchRenameModal";
-import { Badge } from "@app/components/ui/badge";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger
} from "@app/components/ui/tooltip";
-import { Alert, AlertDescription } from "@app/components/ui/alert";
-
-const addTargetSchema = z
- .object({
- ip: z.string().refine(isTargetValid),
- method: z.string().nullable(),
- port: z.coerce.number
().int().positive(),
- siteId: z.int().positive({
- error: "You must select a site for a target."
- }),
- path: z.string().optional().nullable(),
- pathMatchType: z
- .enum(["exact", "prefix", "regex"])
- .optional()
- .nullable(),
- rewritePath: z.string().optional().nullable(),
- rewritePathType: z
- .enum(["exact", "prefix", "regex", "stripPrefix"])
- .optional()
- .nullable(),
- priority: z.int().min(1).max(1000).optional()
- })
- .refine(
- (data) => {
- // If path is provided, pathMatchType must be provided
- if (data.path && !data.pathMatchType) {
- return false;
- }
- // If pathMatchType is provided, path must be provided
- if (data.pathMatchType && !data.path) {
- return false;
- }
- // Validate path based on pathMatchType
- if (data.path && data.pathMatchType) {
- switch (data.pathMatchType) {
- case "exact":
- case "prefix":
- // Path should start with /
- return data.path.startsWith("/");
- case "regex":
- // Validate regex
- try {
- new RegExp(data.path);
- return true;
- } catch {
- return false;
- }
- }
- }
- return true;
- },
- {
- error: "Invalid path configuration"
- }
- )
- .refine(
- (data) => {
- // If rewritePath is provided, rewritePathType must be provided
- if (data.rewritePath && !data.rewritePathType) {
- return false;
- }
- // If rewritePathType is provided, rewritePath must be provided
- // Exception: stripPrefix can have an empty rewritePath (to just strip the prefix)
- if (data.rewritePathType && !data.rewritePath) {
- // Allow empty rewritePath for stripPrefix type
- if (data.rewritePathType !== "stripPrefix") {
- return false;
- }
- }
- return true;
- },
- {
- error: "Invalid rewrite path configuration"
- }
- );
+import type { ResourceContextType } from "@app/contexts/resourceContext";
+import { useEnvContext } from "@app/hooks/useEnvContext";
+import { useResourceContext } from "@app/hooks/useResourceContext";
+import { toast } from "@app/hooks/useToast";
+import { createApiClient } from "@app/lib/api";
+import { formatAxiosError } from "@app/lib/api/formatAxiosError";
+import { cn } from "@app/lib/cn";
+import { DockerManager, DockerState } from "@app/lib/docker";
+import { parseHostTarget } from "@app/lib/parseHostTarget";
+import { orgQueries, resourceQueries } from "@app/lib/queries";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { CaretSortIcon } from "@radix-ui/react-icons";
+import { tlsNameSchema } from "@server/lib/schemas";
+import { type GetResourceResponse } from "@server/routers/resource";
+import type { ListSitesResponse } from "@server/routers/site";
+import { CreateTargetResponse } from "@server/routers/target";
+import { ListTargetsResponse } from "@server/routers/target/listTargets";
+import { ArrayElement } from "@server/types/ArrayElement";
+import { useQuery, useQueryClient } from "@tanstack/react-query";
+import {
+ ColumnDef,
+ flexRender,
+ getCoreRowModel,
+ getFilteredRowModel,
+ getPaginationRowModel,
+ getSortedRowModel,
+ useReactTable
+} from "@tanstack/react-table";
+import { AxiosResponse } from "axios";
+import {
+ AlertTriangle,
+ CheckIcon,
+ CircleCheck,
+ CircleX,
+ Info,
+ Plus,
+ Settings
+} from "lucide-react";
+import { useTranslations } from "next-intl";
+import { useRouter } from "next/navigation";
+import { use, useActionState, useEffect, useState } from "react";
+import { useForm } from "react-hook-form";
+import { z } from "zod";
const targetsSettingsSchema = z.object({
stickySession: z.boolean()
@@ -205,23 +124,72 @@ type LocalTarget = Omit<
"protocol"
>;
-export default function ReverseProxyTargets(props: {
+export default function ReverseProxyTargetsPage(props: {
params: Promise<{ resourceId: number; orgId: string }>;
}) {
const params = use(props.params);
- const t = useTranslations();
- const { env } = useEnvContext();
-
const { resource, updateResource } = useResourceContext();
+ const { data: remoteTargets = [], isLoading: isLoadingTargets } = useQuery(
+ resourceQueries.resourceTargets({
+ resourceId: resource.resourceId
+ })
+ );
+ const { data: sites = [], isLoading: isLoadingSites } = useQuery(
+ orgQueries.sites({
+ orgId: params.orgId
+ })
+ );
+
+ if (isLoadingSites || isLoadingTargets) {
+ return null;
+ }
+
+ return (
+
+
+
+ {resource.http && (
+
+ )}
+
+ {!resource.http && resource.protocol == "tcp" && (
+
+ )}
+
+ );
+}
+
+function ProxyResourceTargetsForm({
+ sites,
+ initialTargets,
+ resource
+}: {
+ initialTargets: LocalTarget[];
+ sites: ListSitesResponse["sites"];
+ resource: GetResourceResponse;
+}) {
+ const t = useTranslations();
const api = createApiClient(useEnvContext());
- const [targets, setTargets] = useState([]);
+ const [targets, setTargets] = useState(initialTargets);
const [targetsToRemove, setTargetsToRemove] = useState([]);
- const [sites, setSites] = useState([]);
const [dockerStates, setDockerStates] = useState