mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-05 10:16:41 +00:00
♻️ refactor reverse proxy targets page
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState, use } from "react";
|
import HealthCheckDialog from "@/components/HealthCheckDialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
@@ -11,11 +11,34 @@ import {
|
|||||||
SelectValue
|
SelectValue
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { AxiosResponse } from "axios";
|
import { ContainersSelector } from "@app/components/ContainersSelector";
|
||||||
import { ListTargetsResponse } from "@server/routers/target/listTargets";
|
import { HeadersInput } from "@app/components/HeadersInput";
|
||||||
import { useForm } from "react-hook-form";
|
import {
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
PathMatchDisplay,
|
||||||
import { z } from "zod";
|
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 {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@@ -25,17 +48,11 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage
|
FormMessage
|
||||||
} from "@app/components/ui/form";
|
} from "@app/components/ui/form";
|
||||||
import { CreateTargetResponse } from "@server/routers/target";
|
|
||||||
import {
|
import {
|
||||||
ColumnDef,
|
Popover,
|
||||||
getFilteredRowModel,
|
PopoverContent,
|
||||||
getSortedRowModel,
|
PopoverTrigger
|
||||||
getPaginationRowModel,
|
} from "@app/components/ui/popover";
|
||||||
getCoreRowModel,
|
|
||||||
useReactTable,
|
|
||||||
flexRender,
|
|
||||||
Row
|
|
||||||
} from "@tanstack/react-table";
|
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@@ -44,89 +61,62 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow
|
TableRow
|
||||||
} from "@app/components/ui/table";
|
} 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 {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger
|
TooltipTrigger
|
||||||
} from "@app/components/ui/tooltip";
|
} from "@app/components/ui/tooltip";
|
||||||
import { Alert, AlertDescription } from "@app/components/ui/alert";
|
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 { isTargetValid } from "@server/lib/validators";
|
||||||
|
import { CreateTargetResponse } from "@server/routers/target";
|
||||||
|
import { ListTargetsResponse } from "@server/routers/target/listTargets";
|
||||||
|
import { ArrayElement } from "@server/types/ArrayElement";
|
||||||
|
import { useQuery } 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, useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
const addTargetSchema = z
|
const addTargetSchema = z
|
||||||
.object({
|
.object({
|
||||||
ip: z.string().refine(isTargetValid),
|
ip: z.string().refine(isTargetValid),
|
||||||
method: z.string().nullable(),
|
method: z.string().nullable(),
|
||||||
port: z.coerce.number<number>().int().positive(),
|
port: z.coerce.number<number>().int().positive(),
|
||||||
siteId: z.int()
|
siteId: z.int().positive({
|
||||||
.positive({
|
error: "You must select a site for a target."
|
||||||
error: "You must select a site for a target."
|
}),
|
||||||
}),
|
|
||||||
path: z.string().optional().nullable(),
|
path: z.string().optional().nullable(),
|
||||||
pathMatchType: z
|
pathMatchType: z
|
||||||
.enum(["exact", "prefix", "regex"])
|
.enum(["exact", "prefix", "regex"])
|
||||||
@@ -202,7 +192,7 @@ type LocalTarget = Omit<
|
|||||||
"protocol"
|
"protocol"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export default function ReverseProxyTargets(props: {
|
export default function ReverseProxyTargetsPage(props: {
|
||||||
params: Promise<{ resourceId: number; orgId: string }>;
|
params: Promise<{ resourceId: number; orgId: string }>;
|
||||||
}) {
|
}) {
|
||||||
const params = use(props.params);
|
const params = use(props.params);
|
||||||
@@ -213,9 +203,19 @@ export default function ReverseProxyTargets(props: {
|
|||||||
|
|
||||||
const api = createApiClient(useEnvContext());
|
const api = createApiClient(useEnvContext());
|
||||||
|
|
||||||
|
const { data: remoteTargets, isLoading: isLoadingTargets } = useQuery(
|
||||||
|
resourceQueries.resourceTargets({
|
||||||
|
resourceId: resource.resourceId
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const { data: sites = [], isLoading: isLoadingSites } = useQuery(
|
||||||
|
orgQueries.sites({
|
||||||
|
orgId: params.orgId
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const [targets, setTargets] = useState<LocalTarget[]>([]);
|
const [targets, setTargets] = useState<LocalTarget[]>([]);
|
||||||
const [targetsToRemove, setTargetsToRemove] = useState<number[]>([]);
|
const [targetsToRemove, setTargetsToRemove] = useState<number[]>([]);
|
||||||
const [sites, setSites] = useState<ListSitesResponse["sites"]>([]);
|
|
||||||
const [dockerStates, setDockerStates] = useState<Map<number, DockerState>>(
|
const [dockerStates, setDockerStates] = useState<Map<number, DockerState>>(
|
||||||
new Map()
|
new Map()
|
||||||
);
|
);
|
||||||
@@ -259,8 +259,6 @@ export default function ReverseProxyTargets(props: {
|
|||||||
const [targetsLoading, setTargetsLoading] = useState(false);
|
const [targetsLoading, setTargetsLoading] = useState(false);
|
||||||
const [proxySettingsLoading, setProxySettingsLoading] = useState(false);
|
const [proxySettingsLoading, setProxySettingsLoading] = useState(false);
|
||||||
|
|
||||||
const [pageLoading, setPageLoading] = useState(true);
|
|
||||||
const [isAdvancedOpen, setIsAdvancedOpen] = useState(false);
|
|
||||||
const [isAdvancedMode, setIsAdvancedMode] = useState(() => {
|
const [isAdvancedMode, setIsAdvancedMode] = useState(() => {
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
const saved = localStorage.getItem("proxy-advanced-mode");
|
const saved = localStorage.getItem("proxy-advanced-mode");
|
||||||
@@ -313,10 +311,6 @@ export default function ReverseProxyTargets(props: {
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
type ProxySettingsValues = z.infer<typeof proxySettingsSchema>;
|
|
||||||
type TlsSettingsValues = z.infer<typeof tlsSettingsSchema>;
|
|
||||||
type TargetsSettingsValues = z.infer<typeof targetsSettingsSchema>;
|
|
||||||
|
|
||||||
const tlsSettingsForm = useForm({
|
const tlsSettingsForm = useForm({
|
||||||
resolver: zodResolver(tlsSettingsSchema),
|
resolver: zodResolver(tlsSettingsSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -343,86 +337,19 @@ export default function ReverseProxyTargets(props: {
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchTargets = async () => {
|
if (!isLoadingSites && sites) {
|
||||||
try {
|
const newtSites = sites.filter((site) => site.type === "newt");
|
||||||
const res = await api.get<AxiosResponse<ListTargetsResponse>>(
|
for (const site of newtSites) {
|
||||||
`/resource/${resource.resourceId}/targets`
|
initializeDockerForSite(site.siteId);
|
||||||
);
|
|
||||||
|
|
||||||
if (res.status === 200) {
|
|
||||||
setTargets(res.data.data.targets);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
toast({
|
|
||||||
variant: "destructive",
|
|
||||||
title: t("targetErrorFetch"),
|
|
||||||
description: formatAxiosError(
|
|
||||||
err,
|
|
||||||
t("targetErrorFetchDescription")
|
|
||||||
)
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setPageLoading(false);
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
fetchTargets();
|
}, [isLoadingSites, sites]);
|
||||||
|
|
||||||
const fetchSites = async () => {
|
useEffect(() => {
|
||||||
const res = await api
|
if (!isLoadingTargets && remoteTargets) {
|
||||||
.get<
|
setTargets(remoteTargets);
|
||||||
AxiosResponse<ListSitesResponse>
|
}
|
||||||
>(`/org/${params.orgId}/sites`)
|
}, [isLoadingTargets, remoteTargets]);
|
||||||
.catch((e) => {
|
|
||||||
toast({
|
|
||||||
variant: "destructive",
|
|
||||||
title: t("sitesErrorFetch"),
|
|
||||||
description: formatAxiosError(
|
|
||||||
e,
|
|
||||||
t("sitesErrorFetchDescription")
|
|
||||||
)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res?.status === 200) {
|
|
||||||
setSites(res.data.data.sites);
|
|
||||||
|
|
||||||
// Initialize Docker for newt sites
|
|
||||||
const newtSites = res.data.data.sites.filter(
|
|
||||||
(site) => site.type === "newt"
|
|
||||||
);
|
|
||||||
for (const site of newtSites) {
|
|
||||||
initializeDockerForSite(site.siteId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sites loaded successfully
|
|
||||||
}
|
|
||||||
};
|
|
||||||
fetchSites();
|
|
||||||
|
|
||||||
// const fetchSite = async () => {
|
|
||||||
// try {
|
|
||||||
// const res = await api.get<AxiosResponse<GetSiteResponse>>(
|
|
||||||
// `/site/${resource.siteId}`
|
|
||||||
// );
|
|
||||||
//
|
|
||||||
// if (res.status === 200) {
|
|
||||||
// setSite(res.data.data);
|
|
||||||
// }
|
|
||||||
// } catch (err) {
|
|
||||||
// console.error(err);
|
|
||||||
// toast({
|
|
||||||
// variant: "destructive",
|
|
||||||
// title: t("siteErrorFetch"),
|
|
||||||
// description: formatAxiosError(
|
|
||||||
// err,
|
|
||||||
// t("siteErrorFetchDescription")
|
|
||||||
// )
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
// fetchSite();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Save advanced mode preference to localStorage
|
// Save advanced mode preference to localStorage
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -546,11 +473,11 @@ export default function ReverseProxyTargets(props: {
|
|||||||
prev.map((t) =>
|
prev.map((t) =>
|
||||||
t.targetId === target.targetId
|
t.targetId === target.targetId
|
||||||
? {
|
? {
|
||||||
...t,
|
...t,
|
||||||
targetId: response.data.data.targetId,
|
targetId: response.data.data.targetId,
|
||||||
new: false,
|
new: false,
|
||||||
updated: false
|
updated: false
|
||||||
}
|
}
|
||||||
: t
|
: t
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -607,16 +534,16 @@ export default function ReverseProxyTargets(props: {
|
|||||||
|
|
||||||
const newTarget: LocalTarget = {
|
const newTarget: LocalTarget = {
|
||||||
...data,
|
...data,
|
||||||
path: isHttp ? (data.path || null) : null,
|
path: isHttp ? data.path || null : null,
|
||||||
pathMatchType: isHttp ? (data.pathMatchType || null) : null,
|
pathMatchType: isHttp ? data.pathMatchType || null : null,
|
||||||
rewritePath: isHttp ? (data.rewritePath || null) : null,
|
rewritePath: isHttp ? data.rewritePath || null : null,
|
||||||
rewritePathType: isHttp ? (data.rewritePathType || null) : null,
|
rewritePathType: isHttp ? data.rewritePathType || null : null,
|
||||||
siteType: site?.type || null,
|
siteType: site?.type || null,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
targetId: new Date().getTime(),
|
targetId: new Date().getTime(),
|
||||||
new: true,
|
new: true,
|
||||||
resourceId: resource.resourceId,
|
resourceId: resource.resourceId,
|
||||||
priority: isHttp ? (data.priority || 100) : 100,
|
priority: isHttp ? data.priority || 100 : 100,
|
||||||
hcEnabled: false,
|
hcEnabled: false,
|
||||||
hcPath: null,
|
hcPath: null,
|
||||||
hcMethod: null,
|
hcMethod: null,
|
||||||
@@ -631,7 +558,7 @@ export default function ReverseProxyTargets(props: {
|
|||||||
hcStatus: null,
|
hcStatus: null,
|
||||||
hcMode: null,
|
hcMode: null,
|
||||||
hcUnhealthyInterval: null,
|
hcUnhealthyInterval: null,
|
||||||
hcTlsServerName: null,
|
hcTlsServerName: null
|
||||||
};
|
};
|
||||||
|
|
||||||
setTargets([...targets, newTarget]);
|
setTargets([...targets, newTarget]);
|
||||||
@@ -653,11 +580,11 @@ export default function ReverseProxyTargets(props: {
|
|||||||
targets.map((target) =>
|
targets.map((target) =>
|
||||||
target.targetId === targetId
|
target.targetId === targetId
|
||||||
? {
|
? {
|
||||||
...target,
|
...target,
|
||||||
...data,
|
...data,
|
||||||
updated: true,
|
updated: true,
|
||||||
siteType: site ? site.type : target.siteType
|
siteType: site ? site.type : target.siteType
|
||||||
}
|
}
|
||||||
: target
|
: target
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -668,10 +595,10 @@ export default function ReverseProxyTargets(props: {
|
|||||||
targets.map((target) =>
|
targets.map((target) =>
|
||||||
target.targetId === targetId
|
target.targetId === targetId
|
||||||
? {
|
? {
|
||||||
...target,
|
...target,
|
||||||
...config,
|
...config,
|
||||||
updated: true
|
updated: true
|
||||||
}
|
}
|
||||||
: target
|
: target
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -733,7 +660,7 @@ export default function ReverseProxyTargets(props: {
|
|||||||
hcStatus: target.hcStatus || null,
|
hcStatus: target.hcStatus || null,
|
||||||
hcUnhealthyInterval: target.hcUnhealthyInterval || null,
|
hcUnhealthyInterval: target.hcUnhealthyInterval || null,
|
||||||
hcMode: target.hcMode || null,
|
hcMode: target.hcMode || null,
|
||||||
hcTlsServerName: target.hcTlsServerName,
|
hcTlsServerName: target.hcTlsServerName
|
||||||
};
|
};
|
||||||
|
|
||||||
// Only include path-related fields for HTTP resources
|
// Only include path-related fields for HTTP resources
|
||||||
@@ -877,7 +804,7 @@ export default function ReverseProxyTargets(props: {
|
|||||||
|
|
||||||
const healthCheckColumn: ColumnDef<LocalTarget> = {
|
const healthCheckColumn: ColumnDef<LocalTarget> = {
|
||||||
accessorKey: "healthCheck",
|
accessorKey: "healthCheck",
|
||||||
header: () => (<span className="p-3">{t("healthCheck")}</span>),
|
header: () => <span className="p-3">{t("healthCheck")}</span>,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const status = row.original.hcHealth || "unknown";
|
const status = row.original.hcHealth || "unknown";
|
||||||
const isEnabled = row.original.hcEnabled;
|
const isEnabled = row.original.hcEnabled;
|
||||||
@@ -949,7 +876,7 @@ export default function ReverseProxyTargets(props: {
|
|||||||
|
|
||||||
const matchPathColumn: ColumnDef<LocalTarget> = {
|
const matchPathColumn: ColumnDef<LocalTarget> = {
|
||||||
accessorKey: "path",
|
accessorKey: "path",
|
||||||
header: () => (<span className="p-3">{t("matchPath")}</span>),
|
header: () => <span className="p-3">{t("matchPath")}</span>,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const hasPathMatch = !!(
|
const hasPathMatch = !!(
|
||||||
row.original.path || row.original.pathMatchType
|
row.original.path || row.original.pathMatchType
|
||||||
@@ -1011,7 +938,7 @@ export default function ReverseProxyTargets(props: {
|
|||||||
|
|
||||||
const addressColumn: ColumnDef<LocalTarget> = {
|
const addressColumn: ColumnDef<LocalTarget> = {
|
||||||
accessorKey: "address",
|
accessorKey: "address",
|
||||||
header: () => (<span className="p-3">{t("address")}</span>),
|
header: () => <span className="p-3">{t("address")}</span>,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const selectedSite = sites.find(
|
const selectedSite = sites.find(
|
||||||
(site) => site.siteId === row.original.siteId
|
(site) => site.siteId === row.original.siteId
|
||||||
@@ -1064,7 +991,7 @@ export default function ReverseProxyTargets(props: {
|
|||||||
className={cn(
|
className={cn(
|
||||||
"w-[180px] justify-between text-sm border-r pr-4 rounded-none h-8 hover:bg-transparent",
|
"w-[180px] justify-between text-sm border-r pr-4 rounded-none h-8 hover:bg-transparent",
|
||||||
!row.original.siteId &&
|
!row.original.siteId &&
|
||||||
"text-muted-foreground"
|
"text-muted-foreground"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className="truncate max-w-[150px]">
|
<span className="truncate max-w-[150px]">
|
||||||
@@ -1132,8 +1059,12 @@ export default function ReverseProxyTargets(props: {
|
|||||||
{row.original.method || "http"}
|
{row.original.method || "http"}
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="http">http</SelectItem>
|
<SelectItem value="http">
|
||||||
<SelectItem value="https">https</SelectItem>
|
http
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="https">
|
||||||
|
https
|
||||||
|
</SelectItem>
|
||||||
<SelectItem value="h2c">h2c</SelectItem>
|
<SelectItem value="h2c">h2c</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
@@ -1225,7 +1156,7 @@ export default function ReverseProxyTargets(props: {
|
|||||||
|
|
||||||
const rewritePathColumn: ColumnDef<LocalTarget> = {
|
const rewritePathColumn: ColumnDef<LocalTarget> = {
|
||||||
accessorKey: "rewritePath",
|
accessorKey: "rewritePath",
|
||||||
header: () => (<span className="p-3">{t("rewritePath")}</span>),
|
header: () => <span className="p-3">{t("rewritePath")}</span>,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const hasRewritePath = !!(
|
const hasRewritePath = !!(
|
||||||
row.original.rewritePath || row.original.rewritePathType
|
row.original.rewritePath || row.original.rewritePathType
|
||||||
@@ -1295,7 +1226,7 @@ export default function ReverseProxyTargets(props: {
|
|||||||
|
|
||||||
const enabledColumn: ColumnDef<LocalTarget> = {
|
const enabledColumn: ColumnDef<LocalTarget> = {
|
||||||
accessorKey: "enabled",
|
accessorKey: "enabled",
|
||||||
header: () => (<span className="p-3">{t("enabled")}</span>),
|
header: () => <span className="p-3">{t("enabled")}</span>,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className="flex items-center justify-center w-full">
|
<div className="flex items-center justify-center w-full">
|
||||||
<Switch
|
<Switch
|
||||||
@@ -1316,7 +1247,7 @@ export default function ReverseProxyTargets(props: {
|
|||||||
|
|
||||||
const actionsColumn: ColumnDef<LocalTarget> = {
|
const actionsColumn: ColumnDef<LocalTarget> = {
|
||||||
id: "actions",
|
id: "actions",
|
||||||
header: () => (<span className="p-3">{t("actions")}</span>),
|
header: () => <span className="p-3">{t("actions")}</span>,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className="flex items-center w-full">
|
<div className="flex items-center w-full">
|
||||||
<Button
|
<Button
|
||||||
@@ -1374,7 +1305,7 @@ export default function ReverseProxyTargets(props: {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (pageLoading) {
|
if (isLoadingSites || isLoadingTargets) {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1399,21 +1330,30 @@ export default function ReverseProxyTargets(props: {
|
|||||||
<TableRow key={headerGroup.id}>
|
<TableRow key={headerGroup.id}>
|
||||||
{headerGroup.headers.map(
|
{headerGroup.headers.map(
|
||||||
(header) => {
|
(header) => {
|
||||||
const isActionsColumn = header.column.id === "actions";
|
const isActionsColumn =
|
||||||
|
header.column
|
||||||
|
.id ===
|
||||||
|
"actions";
|
||||||
return (
|
return (
|
||||||
<TableHead
|
<TableHead
|
||||||
key={header.id}
|
key={
|
||||||
className={isActionsColumn ? "sticky right-0 z-10 w-auto min-w-fit bg-card" : ""}
|
header.id
|
||||||
|
}
|
||||||
|
className={
|
||||||
|
isActionsColumn
|
||||||
|
? "sticky right-0 z-10 w-auto min-w-fit bg-card"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{header.isPlaceholder
|
{header.isPlaceholder
|
||||||
? null
|
? null
|
||||||
: flexRender(
|
: flexRender(
|
||||||
header
|
header
|
||||||
.column
|
.column
|
||||||
.columnDef
|
.columnDef
|
||||||
.header,
|
.header,
|
||||||
header.getContext()
|
header.getContext()
|
||||||
)}
|
)}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1430,13 +1370,20 @@ export default function ReverseProxyTargets(props: {
|
|||||||
{row
|
{row
|
||||||
.getVisibleCells()
|
.getVisibleCells()
|
||||||
.map((cell) => {
|
.map((cell) => {
|
||||||
const isActionsColumn = cell.column.id === "actions";
|
const isActionsColumn =
|
||||||
|
cell.column
|
||||||
|
.id ===
|
||||||
|
"actions";
|
||||||
return (
|
return (
|
||||||
<TableCell
|
<TableCell
|
||||||
key={
|
key={
|
||||||
cell.id
|
cell.id
|
||||||
}
|
}
|
||||||
className={isActionsColumn ? "sticky right-0 z-10 w-auto min-w-fit bg-card" : ""}
|
className={
|
||||||
|
isActionsColumn
|
||||||
|
? "sticky right-0 z-10 w-auto min-w-fit bg-card"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{flexRender(
|
{flexRender(
|
||||||
cell
|
cell
|
||||||
@@ -1721,7 +1668,9 @@ export default function ReverseProxyTargets(props: {
|
|||||||
defaultChecked={
|
defaultChecked={
|
||||||
field.value || false
|
field.value || false
|
||||||
}
|
}
|
||||||
onCheckedChange={(val) => {
|
onCheckedChange={(
|
||||||
|
val
|
||||||
|
) => {
|
||||||
field.onChange(val);
|
field.onChange(val);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -1730,19 +1679,37 @@ export default function ReverseProxyTargets(props: {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{proxySettingsForm.watch("proxyProtocol") && (
|
{proxySettingsForm.watch(
|
||||||
|
"proxyProtocol"
|
||||||
|
) && (
|
||||||
<>
|
<>
|
||||||
<FormField
|
<FormField
|
||||||
control={proxySettingsForm.control}
|
control={
|
||||||
|
proxySettingsForm.control
|
||||||
|
}
|
||||||
name="proxyProtocolVersion"
|
name="proxyProtocolVersion"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{t("proxyProtocolVersion")}</FormLabel>
|
<FormLabel>
|
||||||
|
{t(
|
||||||
|
"proxyProtocolVersion"
|
||||||
|
)}
|
||||||
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Select
|
<Select
|
||||||
value={String(field.value || 1)}
|
value={String(
|
||||||
onValueChange={(value) =>
|
field.value ||
|
||||||
field.onChange(parseInt(value, 10))
|
1
|
||||||
|
)}
|
||||||
|
onValueChange={(
|
||||||
|
value
|
||||||
|
) =>
|
||||||
|
field.onChange(
|
||||||
|
parseInt(
|
||||||
|
value,
|
||||||
|
10
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
@@ -1750,16 +1717,22 @@ export default function ReverseProxyTargets(props: {
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="1">
|
<SelectItem value="1">
|
||||||
{t("version1")}
|
{t(
|
||||||
|
"version1"
|
||||||
|
)}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="2">
|
<SelectItem value="2">
|
||||||
{t("version2")}
|
{t(
|
||||||
|
"version2"
|
||||||
|
)}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
{t("versionDescription")}
|
{t(
|
||||||
|
"versionDescription"
|
||||||
|
)}
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@@ -1768,7 +1741,10 @@ export default function ReverseProxyTargets(props: {
|
|||||||
<Alert>
|
<Alert>
|
||||||
<AlertTriangle className="h-4 w-4" />
|
<AlertTriangle className="h-4 w-4" />
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
<strong>{t("warning")}:</strong> {t("proxyProtocolWarning")}
|
<strong>
|
||||||
|
{t("warning")}:
|
||||||
|
</strong>{" "}
|
||||||
|
{t("proxyProtocolWarning")}
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
</>
|
</>
|
||||||
@@ -1835,8 +1811,9 @@ export default function ReverseProxyTargets(props: {
|
|||||||
hcUnhealthyInterval:
|
hcUnhealthyInterval:
|
||||||
selectedTargetForHealthCheck.hcUnhealthyInterval ||
|
selectedTargetForHealthCheck.hcUnhealthyInterval ||
|
||||||
30,
|
30,
|
||||||
hcTlsServerName: selectedTargetForHealthCheck.hcTlsServerName ||
|
hcTlsServerName:
|
||||||
undefined,
|
selectedTargetForHealthCheck.hcTlsServerName ||
|
||||||
|
undefined
|
||||||
}}
|
}}
|
||||||
onChanges={async (config) => {
|
onChanges={async (config) => {
|
||||||
if (selectedTargetForHealthCheck) {
|
if (selectedTargetForHealthCheck) {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { remote } from "./api";
|
|||||||
import { durationToMs } from "./durationToMs";
|
import { durationToMs } from "./durationToMs";
|
||||||
import type { QueryRequestAnalyticsResponse } from "@server/routers/auditLogs";
|
import type { QueryRequestAnalyticsResponse } from "@server/routers/auditLogs";
|
||||||
import type { ListResourceNamesResponse } from "@server/routers/resource";
|
import type { ListResourceNamesResponse } from "@server/routers/resource";
|
||||||
|
import type { ListTargetsResponse } from "@server/routers/target";
|
||||||
|
|
||||||
export type ProductUpdate = {
|
export type ProductUpdate = {
|
||||||
link: string | null;
|
link: string | null;
|
||||||
@@ -228,6 +229,17 @@ export const resourceQueries = {
|
|||||||
return res.data.data.clients;
|
return res.data.data.clients;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
resourceTargets: ({ resourceId }: { resourceId: number }) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["RESOURCES", resourceId, "TARGETS"] as const,
|
||||||
|
queryFn: async ({ signal, meta }) => {
|
||||||
|
const res = await meta!.api.get<
|
||||||
|
AxiosResponse<ListTargetsResponse>
|
||||||
|
>(`/resource/${resourceId}/targets`, { signal });
|
||||||
|
|
||||||
|
return res.data.data.targets;
|
||||||
|
}
|
||||||
|
}),
|
||||||
listNamesPerOrg: (orgId: string, api: AxiosInstance) =>
|
listNamesPerOrg: (orgId: string, api: AxiosInstance) =>
|
||||||
queryOptions({
|
queryOptions({
|
||||||
queryKey: ["RESOURCES_NAMES", orgId] as const,
|
queryKey: ["RESOURCES_NAMES", orgId] as const,
|
||||||
|
|||||||
Reference in New Issue
Block a user