mirror of
https://github.com/fosrl/pangolin.git
synced 2026-09-02 17:29:43 +02:00
Consolidate target components
This commit is contained in:
@@ -86,6 +86,10 @@ import { ListTargetsResponse } from "@server/routers/target";
|
||||
import { ListRemoteExitNodesResponse } from "@server/routers/remoteExitNode/types";
|
||||
import { ArrayElement } from "@server/types/ArrayElement";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
LocalTarget,
|
||||
ProxyResourceTargetsForm
|
||||
} from "@app/app/[orgId]/settings/resources/proxy/ProxyResourceTargetsForm";
|
||||
import {
|
||||
ColumnDef,
|
||||
flexRender,
|
||||
@@ -206,15 +210,6 @@ const addTargetSchema = z
|
||||
|
||||
type NewResourceType = "http" | "ssh" | "rdp" | "vnc" | "tcp" | "udp";
|
||||
|
||||
export type LocalTarget = Omit<
|
||||
ArrayElement<ListTargetsResponse["targets"]> & {
|
||||
new?: boolean;
|
||||
updated?: boolean;
|
||||
siteType: string | null;
|
||||
},
|
||||
"protocol"
|
||||
>;
|
||||
|
||||
export default function Page() {
|
||||
const { env } = useEnvContext();
|
||||
const api = createApiClient({ env });
|
||||
@@ -238,15 +233,8 @@ export default function Page() {
|
||||
// Resource type state
|
||||
const [resourceType, setResourceType] = useState<NewResourceType>("http");
|
||||
|
||||
// Target management state
|
||||
// Target management state (managed by ProxyResourceTargetsForm; mirrored here for onSubmit)
|
||||
const [targets, setTargets] = useState<LocalTarget[]>([]);
|
||||
const [dockerStates, setDockerStates] = useState<Map<number, DockerState>>(
|
||||
new Map()
|
||||
);
|
||||
|
||||
const [selectedTargetForHealthCheck, setSelectedTargetForHealthCheck] =
|
||||
useState<LocalTarget | null>(null);
|
||||
const [healthCheckDialogOpen, setHealthCheckDialogOpen] = useState(false);
|
||||
|
||||
// SSH-specific state
|
||||
const [sshServerMode, setSshServerMode] = useState<"standard" | "native">(
|
||||
@@ -306,23 +294,6 @@ export default function Page() {
|
||||
fetchExitNodes();
|
||||
}, [orgId]);
|
||||
|
||||
const [isAdvancedMode, setIsAdvancedMode] = useState(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
const saved = localStorage.getItem("create-advanced-mode");
|
||||
return saved === "true";
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem(
|
||||
"create-advanced-mode",
|
||||
isAdvancedMode.toString()
|
||||
);
|
||||
}
|
||||
}, [isAdvancedMode]);
|
||||
|
||||
// Derived flags
|
||||
const isHttpResource = resourceType !== "tcp" && resourceType !== "udp";
|
||||
const isNative = sshServerMode === "native";
|
||||
@@ -334,48 +305,6 @@ export default function Page() {
|
||||
pamMode === "push" &&
|
||||
standardDaemonLocation === "remote";
|
||||
|
||||
function addNewTarget() {
|
||||
const isHttp = resourceType === "http";
|
||||
|
||||
const newTarget: LocalTarget = {
|
||||
targetId: -Date.now(),
|
||||
ip: "",
|
||||
method: isHttp ? "http" : null,
|
||||
port: 0,
|
||||
siteId: sites.length > 0 ? sites[0].siteId : 0,
|
||||
siteName: sites.length > 0 ? sites[0].name : "",
|
||||
path: null,
|
||||
pathMatchType: null,
|
||||
rewritePath: null,
|
||||
rewritePathType: null,
|
||||
priority: isHttp ? 100 : 100,
|
||||
enabled: true,
|
||||
resourceId: 0,
|
||||
hcEnabled: false,
|
||||
hcPath: null,
|
||||
hcMethod: null,
|
||||
hcInterval: null,
|
||||
hcTimeout: null,
|
||||
hcHeaders: null,
|
||||
hcScheme: null,
|
||||
hcHostname: null,
|
||||
hcPort: null,
|
||||
hcFollowRedirects: null,
|
||||
hcHealth: "unknown",
|
||||
hcStatus: null,
|
||||
hcMode: null,
|
||||
hcUnhealthyInterval: null,
|
||||
hcTlsServerName: null,
|
||||
hcHealthyThreshold: null,
|
||||
hcUnhealthyThreshold: null,
|
||||
siteType: sites.length > 0 ? sites[0].type : null,
|
||||
new: true,
|
||||
updated: false
|
||||
};
|
||||
|
||||
setTargets((prev) => [...prev, newTarget]);
|
||||
}
|
||||
|
||||
// Whether raw (TCP/UDP) resources are available
|
||||
const rawResourcesAllowed =
|
||||
env.flags.allowRawResources &&
|
||||
@@ -417,20 +346,6 @@ export default function Page() {
|
||||
}
|
||||
});
|
||||
|
||||
const addTargetForm = useForm({
|
||||
resolver: zodResolver(addTargetSchema),
|
||||
defaultValues: {
|
||||
ip: "",
|
||||
method: "http",
|
||||
port: "" as any as number,
|
||||
path: null,
|
||||
pathMatchType: null,
|
||||
rewritePath: null,
|
||||
rewritePathType: null,
|
||||
priority: 100
|
||||
} as z.infer<typeof addTargetSchema>
|
||||
});
|
||||
|
||||
// Sync form http field with resourceType
|
||||
useEffect(() => {
|
||||
baseForm.setValue("http", isHttpResource);
|
||||
@@ -470,72 +385,6 @@ export default function Page() {
|
||||
});
|
||||
};
|
||||
|
||||
const initializeDockerForSite = async (siteId: number) => {
|
||||
if (dockerStates.has(siteId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dockerManager = new DockerManager(api, siteId);
|
||||
const dockerState = await dockerManager.initializeDocker();
|
||||
|
||||
setDockerStates((prev) => new Map(prev.set(siteId, dockerState)));
|
||||
};
|
||||
|
||||
const refreshContainersForSite = useCallback(
|
||||
async (siteId: number) => {
|
||||
const dockerManager = new DockerManager(api, siteId);
|
||||
const containers = await dockerManager.fetchContainers();
|
||||
|
||||
setDockerStates((prev) => {
|
||||
const newMap = new Map(prev);
|
||||
const existingState = newMap.get(siteId);
|
||||
if (existingState) {
|
||||
newMap.set(siteId, { ...existingState, containers });
|
||||
}
|
||||
return newMap;
|
||||
});
|
||||
},
|
||||
[api]
|
||||
);
|
||||
|
||||
const getDockerStateForSite = useCallback(
|
||||
(siteId: number): DockerState => {
|
||||
return (
|
||||
dockerStates.get(siteId) || {
|
||||
isEnabled: false,
|
||||
isAvailable: false,
|
||||
containers: []
|
||||
}
|
||||
);
|
||||
},
|
||||
[dockerStates]
|
||||
);
|
||||
|
||||
const removeTarget = useCallback((targetId: number) => {
|
||||
setTargets((prevTargets) => {
|
||||
return prevTargets.filter((target) => target.targetId !== targetId);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const updateTarget = useCallback(
|
||||
(targetId: number, data: Partial<LocalTarget>) => {
|
||||
setTargets((prevTargets) => {
|
||||
const site = sites.find((site) => site.siteId === data.siteId);
|
||||
return prevTargets.map((target) =>
|
||||
target.targetId === targetId
|
||||
? {
|
||||
...target,
|
||||
...data,
|
||||
updated: true,
|
||||
siteType: site ? site.type : target.siteType
|
||||
}
|
||||
: target
|
||||
);
|
||||
});
|
||||
},
|
||||
[sites]
|
||||
);
|
||||
|
||||
async function onSubmit() {
|
||||
setCreateLoading(true);
|
||||
|
||||
@@ -787,402 +636,6 @@ export default function Page() {
|
||||
setCreateLoading(false);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
for (const site of sites) {
|
||||
if (site.type === "newt") {
|
||||
initializeDockerForSite(site.siteId);
|
||||
}
|
||||
}
|
||||
|
||||
if (sites.length > 0) {
|
||||
addTargetForm.setValue("siteId", sites[0].siteId);
|
||||
}
|
||||
}, [sites]);
|
||||
|
||||
function TargetHealthCheck(targetId: number, config: any) {
|
||||
setTargets(
|
||||
targets.map((target) =>
|
||||
target.targetId === targetId
|
||||
? {
|
||||
...target,
|
||||
...config,
|
||||
updated: true
|
||||
}
|
||||
: target
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const openHealthCheckDialog = useCallback((target: LocalTarget) => {
|
||||
console.log(target);
|
||||
setSelectedTargetForHealthCheck(target);
|
||||
setHealthCheckDialogOpen(true);
|
||||
}, []);
|
||||
|
||||
const isHttp = resourceType === "http";
|
||||
|
||||
const columns = useMemo((): ColumnDef<LocalTarget>[] => {
|
||||
const priorityColumn: ColumnDef<LocalTarget> = {
|
||||
id: "priority",
|
||||
header: () => (
|
||||
<div className="flex items-center gap-2 p-3">
|
||||
{t("priority")}
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="h-4 w-4 text-muted-foreground cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p className="max-w-xs">
|
||||
{t("priorityDescription")}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div className="p-3">
|
||||
<Input
|
||||
type="number"
|
||||
defaultValue={row.original.priority ?? 100}
|
||||
min={1}
|
||||
max={1000}
|
||||
className="w-20 h-7 text-sm"
|
||||
onChange={(e) => {
|
||||
const val = parseInt(e.target.value);
|
||||
if (!isNaN(val) && val >= 1 && val <= 1000) {
|
||||
updateTarget(row.original.targetId, {
|
||||
priority: val
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
size: 120,
|
||||
minSize: 100,
|
||||
maxSize: 150
|
||||
};
|
||||
|
||||
const healthCheckColumn: ColumnDef<LocalTarget> = {
|
||||
accessorKey: "healthCheck",
|
||||
header: () => <span className="p-3">{t("healthCheck")}</span>,
|
||||
cell: ({ row }) => {
|
||||
const status = row.original.hcHealth || "unknown";
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
switch (status) {
|
||||
case "healthy":
|
||||
return t("healthy");
|
||||
case "unhealthy":
|
||||
return t("unhealthy");
|
||||
default:
|
||||
return t("unknown");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 p-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-7 text-xs gap-1"
|
||||
onClick={() => openHealthCheckDialog(row.original)}
|
||||
>
|
||||
<Settings className="h-3.5 w-3.5" />
|
||||
{row.original.hcEnabled ? (
|
||||
<>
|
||||
{status === "healthy" && (
|
||||
<CircleCheck className="h-3.5 w-3.5 text-green-500" />
|
||||
)}
|
||||
{status === "unhealthy" && (
|
||||
<CircleX className="h-3.5 w-3.5 text-red-500" />
|
||||
)}
|
||||
{status === "unknown" && (
|
||||
<Info className="h-3.5 w-3.5 text-yellow-500" />
|
||||
)}
|
||||
{getStatusText(status)}
|
||||
</>
|
||||
) : (
|
||||
t("configure")
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
size: 200,
|
||||
minSize: 180,
|
||||
maxSize: 250
|
||||
};
|
||||
|
||||
const matchPathColumn: ColumnDef<LocalTarget> = {
|
||||
accessorKey: "path",
|
||||
header: () => <span className="p-3">{t("matchPath")}</span>,
|
||||
cell: ({ row }) => {
|
||||
const hasPathMatch = !!(
|
||||
row.original.path || row.original.pathMatchType
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="p-3">
|
||||
{hasPathMatch ? (
|
||||
<PathMatchModal
|
||||
value={{
|
||||
path: row.original.path,
|
||||
pathMatchType: row.original.pathMatchType
|
||||
}}
|
||||
onChange={(config) =>
|
||||
updateTarget(
|
||||
row.original.targetId,
|
||||
config.path === null &&
|
||||
config.pathMatchType === null
|
||||
? {
|
||||
...config,
|
||||
rewritePath: null,
|
||||
rewritePathType: null
|
||||
}
|
||||
: config
|
||||
)
|
||||
}
|
||||
trigger={
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex items-center gap-2 p-2 w-full text-left cursor-pointer max-w-[200px]"
|
||||
>
|
||||
<PathMatchDisplay
|
||||
value={{
|
||||
path: row.original.path,
|
||||
pathMatchType:
|
||||
row.original.pathMatchType
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<PathMatchModal
|
||||
value={{
|
||||
path: row.original.path,
|
||||
pathMatchType: row.original.pathMatchType
|
||||
}}
|
||||
onChange={(config) =>
|
||||
updateTarget(
|
||||
row.original.targetId,
|
||||
config.path === null &&
|
||||
config.pathMatchType === null
|
||||
? {
|
||||
...config,
|
||||
rewritePath: null,
|
||||
rewritePathType: null
|
||||
}
|
||||
: config
|
||||
)
|
||||
}
|
||||
trigger={
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full max-w-[200px]"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
{t("matchPath")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
size: 200,
|
||||
minSize: 180,
|
||||
maxSize: 200
|
||||
};
|
||||
|
||||
const addressColumn: ColumnDef<LocalTarget> = {
|
||||
accessorKey: "address",
|
||||
header: () => <span className="p-3">{t("address")}</span>,
|
||||
cell: ({ row }) => (
|
||||
<ResourceTargetAddressItem
|
||||
isHttp={isHttp}
|
||||
orgId={orgId!.toString()}
|
||||
getDockerStateForSite={getDockerStateForSite}
|
||||
proxyTarget={row.original}
|
||||
refreshContainersForSite={refreshContainersForSite}
|
||||
updateTarget={updateTarget}
|
||||
/>
|
||||
),
|
||||
size: 400,
|
||||
minSize: 350,
|
||||
maxSize: 500
|
||||
};
|
||||
|
||||
const rewritePathColumn: ColumnDef<LocalTarget> = {
|
||||
accessorKey: "rewritePath",
|
||||
header: () => <span className="p-3">{t("rewritePath")}</span>,
|
||||
cell: ({ row }) => {
|
||||
const hasRewritePath = !!(
|
||||
row.original.rewritePath || row.original.rewritePathType
|
||||
);
|
||||
const noPathMatch =
|
||||
!row.original.path && !row.original.pathMatchType;
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center w-full">
|
||||
{hasRewritePath && !noPathMatch ? (
|
||||
<PathRewriteModal
|
||||
value={{
|
||||
rewritePath: row.original.rewritePath,
|
||||
rewritePathType:
|
||||
row.original.rewritePathType
|
||||
}}
|
||||
onChange={(config) =>
|
||||
updateTarget(row.original.targetId, config)
|
||||
}
|
||||
trigger={
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex items-center gap-2 p-2 w-full text-left cursor-pointer max-w-[200px]"
|
||||
disabled={noPathMatch}
|
||||
>
|
||||
<PathRewriteDisplay
|
||||
value={{
|
||||
rewritePath:
|
||||
row.original.rewritePath,
|
||||
rewritePathType:
|
||||
row.original.rewritePathType
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<PathRewriteModal
|
||||
value={{
|
||||
rewritePath: row.original.rewritePath,
|
||||
rewritePathType:
|
||||
row.original.rewritePathType
|
||||
}}
|
||||
onChange={(config) =>
|
||||
updateTarget(row.original.targetId, config)
|
||||
}
|
||||
trigger={
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={noPathMatch}
|
||||
className="w-full max-w-[200px]"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
{t("rewritePath")}
|
||||
</Button>
|
||||
}
|
||||
disabled={noPathMatch}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
size: 200,
|
||||
minSize: 180,
|
||||
maxSize: 200
|
||||
};
|
||||
|
||||
const enabledColumn: ColumnDef<LocalTarget> = {
|
||||
accessorKey: "enabled",
|
||||
header: () => <span className="p-3">{t("enabled")}</span>,
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center justify-center w-full">
|
||||
<Switch
|
||||
defaultChecked={row.original.enabled}
|
||||
onCheckedChange={(val) =>
|
||||
updateTarget(row.original.targetId, {
|
||||
...row.original,
|
||||
enabled: val
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
size: 100,
|
||||
minSize: 80,
|
||||
maxSize: 120
|
||||
};
|
||||
|
||||
const actionsColumn: ColumnDef<LocalTarget> = {
|
||||
id: "actions",
|
||||
header: () => <span className="p-3">{t("actions")}</span>,
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center justify-end w-full">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => removeTarget(row.original.targetId)}
|
||||
>
|
||||
<CircleX className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
size: 100,
|
||||
minSize: 80,
|
||||
maxSize: 120
|
||||
};
|
||||
|
||||
if (isAdvancedMode) {
|
||||
const cols = [
|
||||
addressColumn,
|
||||
healthCheckColumn,
|
||||
enabledColumn,
|
||||
actionsColumn
|
||||
];
|
||||
|
||||
if (isHttp) {
|
||||
cols.splice(
|
||||
1,
|
||||
0,
|
||||
matchPathColumn,
|
||||
rewritePathColumn,
|
||||
priorityColumn
|
||||
);
|
||||
}
|
||||
|
||||
return cols;
|
||||
} else {
|
||||
return [
|
||||
addressColumn,
|
||||
healthCheckColumn,
|
||||
enabledColumn,
|
||||
actionsColumn
|
||||
];
|
||||
}
|
||||
}, [
|
||||
isAdvancedMode,
|
||||
isHttp,
|
||||
sites,
|
||||
updateTarget,
|
||||
getDockerStateForSite,
|
||||
refreshContainersForSite,
|
||||
openHealthCheckDialog,
|
||||
removeTarget,
|
||||
t
|
||||
]);
|
||||
|
||||
const table = useReactTable({
|
||||
data: targets,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
getRowId: (row) => String(row.targetId),
|
||||
state: {
|
||||
pagination: {
|
||||
pageIndex: 0,
|
||||
pageSize: 1000
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// SSH strategy options
|
||||
const sshModeOptions: StrategyOption<"standard" | "native">[] = [
|
||||
{
|
||||
@@ -1732,207 +1185,11 @@ export default function Page() {
|
||||
{(resourceType === "http" ||
|
||||
resourceType === "tcp" ||
|
||||
resourceType === "udp") && (
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("targets")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("targetsDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
{targets.length > 0 ? (
|
||||
<>
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table
|
||||
.getHeaderGroups()
|
||||
.map(
|
||||
(
|
||||
headerGroup
|
||||
) => (
|
||||
<TableRow
|
||||
key={
|
||||
headerGroup.id
|
||||
}
|
||||
>
|
||||
{headerGroup.headers.map(
|
||||
(
|
||||
header
|
||||
) => {
|
||||
const isActionsColumn =
|
||||
header
|
||||
.column
|
||||
.id ===
|
||||
"actions";
|
||||
return (
|
||||
<TableHead
|
||||
key={
|
||||
header.id
|
||||
}
|
||||
className={
|
||||
isActionsColumn
|
||||
? "sticky right-0 z-10 w-auto min-w-fit bg-card"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header
|
||||
.column
|
||||
.columnDef
|
||||
.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</TableHead>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</TableRow>
|
||||
)
|
||||
)}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel()
|
||||
.rows
|
||||
?.length ? (
|
||||
table
|
||||
.getRowModel()
|
||||
.rows.map(
|
||||
(
|
||||
row
|
||||
) => (
|
||||
<TableRow
|
||||
key={
|
||||
row.id
|
||||
}
|
||||
>
|
||||
{row
|
||||
.getVisibleCells()
|
||||
.map(
|
||||
(
|
||||
cell
|
||||
) => {
|
||||
const isActionsColumn =
|
||||
cell
|
||||
.column
|
||||
.id ===
|
||||
"actions";
|
||||
return (
|
||||
<TableCell
|
||||
key={
|
||||
cell.id
|
||||
}
|
||||
className={
|
||||
isActionsColumn
|
||||
? "sticky right-0 z-10 w-auto min-w-fit bg-card"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{flexRender(
|
||||
cell
|
||||
.column
|
||||
.columnDef
|
||||
.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</TableCell>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</TableRow>
|
||||
)
|
||||
)
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={
|
||||
columns.length
|
||||
}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
{t(
|
||||
"targetNoOne"
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center justify-between w-full gap-2">
|
||||
<Button
|
||||
onClick={
|
||||
addNewTarget
|
||||
}
|
||||
variant="outline"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
{t("addTarget")}
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
id="advanced-mode-toggle"
|
||||
checked={
|
||||
isAdvancedMode
|
||||
}
|
||||
onCheckedChange={
|
||||
setIsAdvancedMode
|
||||
}
|
||||
/>
|
||||
<label
|
||||
htmlFor="advanced-mode-toggle"
|
||||
className="text-sm"
|
||||
>
|
||||
{t(
|
||||
"advancedMode"
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center py-8 border-2 border-dashed border-muted rounded-lg p-4">
|
||||
<p className="text-muted-foreground mb-4">
|
||||
{t("targetNoOne")}
|
||||
</p>
|
||||
<Button
|
||||
onClick={addNewTarget}
|
||||
variant="outline"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
{t("addTarget")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{build === "saas" &&
|
||||
targets.length > 1 &&
|
||||
new Set(
|
||||
targets.map((t) => t.siteId)
|
||||
).size > 1 && (
|
||||
<p className="text-sm text-muted-foreground mt-3">
|
||||
{t(
|
||||
"proxyMultiSiteRoundRobinNodeHelp"
|
||||
)}{" "}
|
||||
<a
|
||||
href="https://docs.pangolin.net/manage/resources/public/targets#distributing-sites-load-across-servers"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline inline-flex items-center gap-1"
|
||||
>
|
||||
{t("learnMore")}
|
||||
<ExternalLink className="size-3.5 shrink-0" />
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
)}
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
<ProxyResourceTargetsForm
|
||||
orgId={orgId!.toString()}
|
||||
isHttp={resourceType === "http"}
|
||||
onChange={setTargets}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end space-x-2 mt-8">
|
||||
@@ -1977,77 +1234,6 @@ export default function Page() {
|
||||
{t("resourceCreate")}
|
||||
</Button>
|
||||
</div>
|
||||
{selectedTargetForHealthCheck && (
|
||||
<HealthCheckCredenza
|
||||
mode="autoSave"
|
||||
open={healthCheckDialogOpen}
|
||||
setOpen={setHealthCheckDialogOpen}
|
||||
targetAddress={`${selectedTargetForHealthCheck.ip}:${selectedTargetForHealthCheck.port}`}
|
||||
targetMethod={
|
||||
selectedTargetForHealthCheck.method ||
|
||||
undefined
|
||||
}
|
||||
initialConfig={{
|
||||
hcEnabled:
|
||||
selectedTargetForHealthCheck.hcEnabled ||
|
||||
false,
|
||||
hcPath:
|
||||
selectedTargetForHealthCheck.hcPath ||
|
||||
"/",
|
||||
hcMethod:
|
||||
selectedTargetForHealthCheck.hcMethod ||
|
||||
"GET",
|
||||
hcInterval:
|
||||
selectedTargetForHealthCheck.hcInterval ||
|
||||
5,
|
||||
hcTimeout:
|
||||
selectedTargetForHealthCheck.hcTimeout ||
|
||||
5,
|
||||
hcHeaders:
|
||||
selectedTargetForHealthCheck.hcHeaders ||
|
||||
undefined,
|
||||
hcScheme:
|
||||
selectedTargetForHealthCheck.hcScheme ||
|
||||
undefined,
|
||||
hcHostname:
|
||||
selectedTargetForHealthCheck.hcHostname ||
|
||||
selectedTargetForHealthCheck.ip,
|
||||
hcPort:
|
||||
selectedTargetForHealthCheck.hcPort ||
|
||||
selectedTargetForHealthCheck.port,
|
||||
hcFollowRedirects:
|
||||
selectedTargetForHealthCheck.hcFollowRedirects ??
|
||||
true,
|
||||
hcStatus:
|
||||
selectedTargetForHealthCheck.hcStatus ||
|
||||
undefined,
|
||||
hcMode:
|
||||
selectedTargetForHealthCheck.hcMode ||
|
||||
"http",
|
||||
hcUnhealthyInterval:
|
||||
selectedTargetForHealthCheck.hcUnhealthyInterval ||
|
||||
30,
|
||||
hcTlsServerName:
|
||||
selectedTargetForHealthCheck.hcTlsServerName ||
|
||||
undefined,
|
||||
hcHealthyThreshold:
|
||||
selectedTargetForHealthCheck.hcHealthyThreshold ||
|
||||
1,
|
||||
hcUnhealthyThreshold:
|
||||
selectedTargetForHealthCheck.hcUnhealthyThreshold ||
|
||||
1
|
||||
}}
|
||||
onChanges={async (config) => {
|
||||
if (selectedTargetForHealthCheck) {
|
||||
console.log(config);
|
||||
TargetHealthCheck(
|
||||
selectedTargetForHealthCheck.targetId,
|
||||
config
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</SettingsContainer>
|
||||
) : (
|
||||
<SettingsContainer>
|
||||
|
||||
Reference in New Issue
Block a user