mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-27 19:22:50 +00:00
Consolidate target components
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -69,7 +69,11 @@ import type { ListSitesResponse } from "@server/routers/site";
|
|||||||
import { CreateTargetResponse } from "@server/routers/target";
|
import { CreateTargetResponse } from "@server/routers/target";
|
||||||
import { ListTargetsResponse } from "@server/routers/target/listTargets";
|
import { ListTargetsResponse } from "@server/routers/target/listTargets";
|
||||||
import { ArrayElement } from "@server/types/ArrayElement";
|
import { ArrayElement } from "@server/types/ArrayElement";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import {
|
||||||
|
LocalTarget,
|
||||||
|
ProxyResourceTargetsForm
|
||||||
|
} from "@app/app/[orgId]/settings/resources/proxy/ProxyResourceTargetsForm";
|
||||||
import {
|
import {
|
||||||
ColumnDef,
|
ColumnDef,
|
||||||
flexRender,
|
flexRender,
|
||||||
@@ -106,15 +110,6 @@ const targetsSettingsSchema = z.object({
|
|||||||
stickySession: z.boolean()
|
stickySession: z.boolean()
|
||||||
});
|
});
|
||||||
|
|
||||||
type LocalTarget = Omit<
|
|
||||||
ArrayElement<ListTargetsResponse["targets"]> & {
|
|
||||||
new?: boolean;
|
|
||||||
updated?: boolean;
|
|
||||||
siteType: string | null;
|
|
||||||
},
|
|
||||||
"protocol"
|
|
||||||
>;
|
|
||||||
|
|
||||||
export default function ReverseProxyTargetsPage(props: {
|
export default function ReverseProxyTargetsPage(props: {
|
||||||
params: Promise<{ resourceId: number; orgId: string }>;
|
params: Promise<{ resourceId: number; orgId: string }>;
|
||||||
}) {
|
}) {
|
||||||
@@ -135,6 +130,7 @@ export default function ReverseProxyTargetsPage(props: {
|
|||||||
<SettingsContainer>
|
<SettingsContainer>
|
||||||
<ProxyResourceTargetsForm
|
<ProxyResourceTargetsForm
|
||||||
orgId={params.orgId}
|
orgId={params.orgId}
|
||||||
|
isHttp={resource.http}
|
||||||
initialTargets={remoteTargets}
|
initialTargets={remoteTargets}
|
||||||
resource={resource}
|
resource={resource}
|
||||||
updateResource={updateResource}
|
updateResource={updateResource}
|
||||||
@@ -157,954 +153,6 @@ export default function ReverseProxyTargetsPage(props: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProxyResourceTargetsForm({
|
|
||||||
orgId,
|
|
||||||
initialTargets,
|
|
||||||
resource,
|
|
||||||
updateResource
|
|
||||||
}: {
|
|
||||||
initialTargets: LocalTarget[];
|
|
||||||
orgId: string;
|
|
||||||
resource: GetResourceResponse;
|
|
||||||
updateResource: ResourceContextType["updateResource"];
|
|
||||||
}) {
|
|
||||||
const t = useTranslations();
|
|
||||||
const api = createApiClient(useEnvContext());
|
|
||||||
|
|
||||||
const [targets, setTargets] = useState<LocalTarget[]>(initialTargets);
|
|
||||||
const [targetsToRemove, setTargetsToRemove] = useState<number[]>([]);
|
|
||||||
|
|
||||||
const { data: polledTargets } = useQuery({
|
|
||||||
...resourceQueries.resourceTargets({
|
|
||||||
resourceId: resource.resourceId
|
|
||||||
}),
|
|
||||||
refetchInterval: 10_000
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!polledTargets) return;
|
|
||||||
setTargets((prev) =>
|
|
||||||
prev.map((t) => {
|
|
||||||
const fresh = polledTargets.find(
|
|
||||||
(p) => p.targetId === t.targetId
|
|
||||||
);
|
|
||||||
if (!fresh) return t;
|
|
||||||
return {
|
|
||||||
...t,
|
|
||||||
hcHealth: fresh.hcHealth,
|
|
||||||
hcEnabled: t.updated ? t.hcEnabled : fresh.hcEnabled
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}, [polledTargets]);
|
|
||||||
const [dockerStates, setDockerStates] = useState<Map<number, DockerState>>(
|
|
||||||
new Map()
|
|
||||||
);
|
|
||||||
const [healthCheckDialogOpen, setHealthCheckDialogOpen] = useState(false);
|
|
||||||
const [selectedTargetForHealthCheck, setSelectedTargetForHealthCheck] =
|
|
||||||
useState<LocalTarget | null>(null);
|
|
||||||
|
|
||||||
const [bgDestination, setBgDestination] = useState("");
|
|
||||||
const [bgDestinationPort, setBgDestinationPort] = useState("");
|
|
||||||
const [bgSiteId, setBgSiteId] = useState<number | null>(null);
|
|
||||||
const [bgTargetId, setBgTargetId] = useState<number | null>(null);
|
|
||||||
|
|
||||||
const initializeDockerForSite = async (siteId: number) => {
|
|
||||||
if (dockerStates.has(siteId)) {
|
|
||||||
return; // Already initialized
|
|
||||||
}
|
|
||||||
|
|
||||||
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 [isAdvancedMode, setIsAdvancedMode] = useState(() => {
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
const saved = localStorage.getItem("proxy-advanced-mode");
|
|
||||||
return saved === "true";
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
const isHttp = resource.http;
|
|
||||||
|
|
||||||
const removeTarget = useCallback((targetId: number) => {
|
|
||||||
setTargets((prevTargets) => {
|
|
||||||
const targetToRemove = prevTargets.find(
|
|
||||||
(target) => target.targetId === targetId
|
|
||||||
);
|
|
||||||
if (targetToRemove && !targetToRemove.new) {
|
|
||||||
setTargetsToRemove((prev) => [...prev, targetId]);
|
|
||||||
}
|
|
||||||
return prevTargets.filter((target) => target.targetId !== targetId);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const { data: sites = [] } = useQuery(
|
|
||||||
orgQueries.sites({
|
|
||||||
orgId
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: bgTargetsResponse } = useQuery({
|
|
||||||
queryKey: ["browserGatewayTargets", resource.resourceId, orgId],
|
|
||||||
queryFn: async () => {
|
|
||||||
const res = await api.get(
|
|
||||||
`/org/${orgId}/resource/${resource.resourceId}/browser-gateway-targets`
|
|
||||||
);
|
|
||||||
return res.data.data as {
|
|
||||||
targets: Array<{
|
|
||||||
browserGatewayTargetId: number;
|
|
||||||
resourceId: number;
|
|
||||||
siteId: number;
|
|
||||||
type: string;
|
|
||||||
destination: string;
|
|
||||||
destinationPort: number;
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!bgTargetsResponse?.targets?.length) return;
|
|
||||||
const bgt = bgTargetsResponse.targets[0];
|
|
||||||
setBgDestination(bgt.destination);
|
|
||||||
setBgDestinationPort(String(bgt.destinationPort));
|
|
||||||
setBgSiteId(bgt.siteId);
|
|
||||||
setBgTargetId(bgt.browserGatewayTargetId);
|
|
||||||
}, [bgTargetsResponse]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (sites.length > 0 && bgSiteId === null) {
|
|
||||||
setBgSiteId(sites[0].siteId);
|
|
||||||
}
|
|
||||||
}, [sites, bgSiteId]);
|
|
||||||
|
|
||||||
const updateTarget = useCallback(
|
|
||||||
(targetId: number, data: Partial<LocalTarget>) => {
|
|
||||||
setTargets((prevTargets) => {
|
|
||||||
return prevTargets.map((target) =>
|
|
||||||
target.targetId === targetId
|
|
||||||
? {
|
|
||||||
...target,
|
|
||||||
...data,
|
|
||||||
updated: true
|
|
||||||
}
|
|
||||||
: target
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[sites]
|
|
||||||
);
|
|
||||||
|
|
||||||
const openHealthCheckDialog = useCallback((target: LocalTarget) => {
|
|
||||||
setSelectedTargetForHealthCheck(target);
|
|
||||||
setHealthCheckDialogOpen(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const columns = useMemo((): ColumnDef<LocalTarget>[] => {
|
|
||||||
const priorityColumn: ColumnDef<LocalTarget> = {
|
|
||||||
id: "priority",
|
|
||||||
header: () => (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{t("priority")}
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<Info className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent className="max-w-xs">
|
|
||||||
<p>{t("priorityDescription")}</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
cell: ({ row }) => {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center w-full">
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
max="1000"
|
|
||||||
onClick={(e) => e.currentTarget.focus()}
|
|
||||||
defaultValue={row.original.priority || 100}
|
|
||||||
className="w-full max-w-20"
|
|
||||||
onBlur={(e) => {
|
|
||||||
const value = parseInt(e.target.value, 10);
|
|
||||||
if (value >= 1 && value <= 1000) {
|
|
||||||
updateTarget(row.original.targetId, {
|
|
||||||
...row.original,
|
|
||||||
priority: value
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</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("healthCheckHealthy");
|
|
||||||
case "unhealthy":
|
|
||||||
return t("healthCheckUnhealthy");
|
|
||||||
case "unknown":
|
|
||||||
default:
|
|
||||||
return t("healthCheckUnknown");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center w-full">
|
|
||||||
{row.original.siteType === "newt" ? (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="flex items-center gap-2 w-full text-left cursor-pointer"
|
|
||||||
onClick={() =>
|
|
||||||
openHealthCheckDialog(row.original)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`flex items-center gap-2 ${status === "healthy" ? "text-green-500" : status === "unhealthy" ? "text-destructive" : "text-neutral-500"}`}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`w-2 h-2 rounded-full ${status === "healthy" ? "bg-green-500" : status === "unhealthy" ? "bg-destructive" : "bg-neutral-500"}`}
|
|
||||||
></div>
|
|
||||||
{getStatusText(status)}
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<span>-</span>
|
|
||||||
)}
|
|
||||||
</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="flex items-center justify-center w-full">
|
|
||||||
{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 }) => {
|
|
||||||
return (
|
|
||||||
<ResourceTargetAddressItem
|
|
||||||
isHttp={isHttp}
|
|
||||||
orgId={orgId}
|
|
||||||
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 w-full">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => removeTarget(row.original.targetId)}
|
|
||||||
>
|
|
||||||
{t("delete")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
size: 100,
|
|
||||||
minSize: 80,
|
|
||||||
maxSize: 120
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isAdvancedMode) {
|
|
||||||
const columns = [
|
|
||||||
addressColumn,
|
|
||||||
healthCheckColumn,
|
|
||||||
enabledColumn,
|
|
||||||
actionsColumn
|
|
||||||
];
|
|
||||||
|
|
||||||
// Only include path-related columns for HTTP resources
|
|
||||||
if (isHttp) {
|
|
||||||
columns.unshift(matchPathColumn);
|
|
||||||
columns.splice(3, 0, rewritePathColumn, priorityColumn);
|
|
||||||
}
|
|
||||||
|
|
||||||
return columns;
|
|
||||||
} else {
|
|
||||||
return [
|
|
||||||
addressColumn,
|
|
||||||
healthCheckColumn,
|
|
||||||
enabledColumn,
|
|
||||||
actionsColumn
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
isAdvancedMode,
|
|
||||||
isHttp,
|
|
||||||
sites,
|
|
||||||
updateTarget,
|
|
||||||
getDockerStateForSite,
|
|
||||||
refreshContainersForSite,
|
|
||||||
openHealthCheckDialog,
|
|
||||||
removeTarget,
|
|
||||||
t
|
|
||||||
]);
|
|
||||||
|
|
||||||
function addNewTarget() {
|
|
||||||
const isHttp = resource.http;
|
|
||||||
|
|
||||||
const newTarget: LocalTarget = {
|
|
||||||
targetId: -Date.now(), // Use negative timestamp as temporary ID
|
|
||||||
ip: "",
|
|
||||||
method: isHttp ? "http" : null,
|
|
||||||
port: 0,
|
|
||||||
siteId: sites.length > 0 ? sites[0].siteId : 0,
|
|
||||||
siteName: sites.length > 0 ? sites[0].name : "",
|
|
||||||
path: isHttp ? null : null,
|
|
||||||
pathMatchType: isHttp ? null : null,
|
|
||||||
rewritePath: isHttp ? null : null,
|
|
||||||
rewritePathType: isHttp ? null : null,
|
|
||||||
priority: isHttp ? 100 : 100,
|
|
||||||
enabled: true,
|
|
||||||
resourceId: resource.resourceId,
|
|
||||||
hcEnabled: false,
|
|
||||||
hcPath: null,
|
|
||||||
hcMethod: null,
|
|
||||||
hcInterval: null,
|
|
||||||
hcTimeout: null,
|
|
||||||
hcHeaders: null,
|
|
||||||
hcFollowRedirects: null,
|
|
||||||
hcScheme: null,
|
|
||||||
hcHostname: null,
|
|
||||||
hcPort: 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]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateTargetHealthCheck(targetId: number, config: any) {
|
|
||||||
setTargets(
|
|
||||||
targets.map((target) =>
|
|
||||||
target.targetId === targetId
|
|
||||||
? {
|
|
||||||
...target,
|
|
||||||
...config,
|
|
||||||
updated: true
|
|
||||||
}
|
|
||||||
: target
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const newtSites = sites.filter((site) => site.type === "newt");
|
|
||||||
for (const site of newtSites) {
|
|
||||||
initializeDockerForSite(site.siteId);
|
|
||||||
}
|
|
||||||
}, [sites]);
|
|
||||||
|
|
||||||
// Save advanced mode preference to localStorage
|
|
||||||
useEffect(() => {
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
localStorage.setItem(
|
|
||||||
"proxy-advanced-mode",
|
|
||||||
isAdvancedMode.toString()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, [isAdvancedMode]);
|
|
||||||
|
|
||||||
const [, formAction, isSubmitting] = useActionState(saveTargets, null);
|
|
||||||
|
|
||||||
async function saveTargets() {
|
|
||||||
// Validate that no targets have blank IPs or invalid ports
|
|
||||||
const targetsWithInvalidFields = targets.filter(
|
|
||||||
(target) =>
|
|
||||||
!target.ip ||
|
|
||||||
target.ip.trim() === "" ||
|
|
||||||
!target.port ||
|
|
||||||
target.port <= 0 ||
|
|
||||||
isNaN(target.port)
|
|
||||||
);
|
|
||||||
console.log(targetsWithInvalidFields);
|
|
||||||
if (targetsWithInvalidFields.length > 0) {
|
|
||||||
toast({
|
|
||||||
variant: "destructive",
|
|
||||||
title: t("targetErrorInvalidIp"),
|
|
||||||
description: t("targetErrorInvalidIpDescription")
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Promise.all(
|
|
||||||
targetsToRemove.map((targetId) =>
|
|
||||||
api.delete(`/target/${targetId}`)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Save targets
|
|
||||||
for (const target of targets) {
|
|
||||||
const data: any = {
|
|
||||||
ip: target.ip,
|
|
||||||
port: target.port,
|
|
||||||
method: target.method,
|
|
||||||
enabled: target.enabled,
|
|
||||||
siteId: target.siteId,
|
|
||||||
hcEnabled: target.hcEnabled,
|
|
||||||
hcPath: target.hcPath || null,
|
|
||||||
hcScheme: target.hcScheme || null,
|
|
||||||
hcHostname: target.hcHostname || null,
|
|
||||||
hcPort: target.hcPort || null,
|
|
||||||
hcInterval: target.hcInterval || null,
|
|
||||||
hcTimeout: target.hcTimeout || null,
|
|
||||||
hcHeaders: target.hcHeaders || null,
|
|
||||||
hcFollowRedirects: target.hcFollowRedirects || null,
|
|
||||||
hcMethod: target.hcMethod || null,
|
|
||||||
hcStatus: target.hcStatus || null,
|
|
||||||
hcUnhealthyInterval: target.hcUnhealthyInterval || null,
|
|
||||||
hcMode: target.hcMode || null,
|
|
||||||
hcTlsServerName: target.hcTlsServerName,
|
|
||||||
hcHealthyThreshold: target.hcHealthyThreshold || null,
|
|
||||||
hcUnhealthyThreshold: target.hcUnhealthyThreshold || null
|
|
||||||
};
|
|
||||||
|
|
||||||
// Only include path-related fields for HTTP resources
|
|
||||||
if (resource.http) {
|
|
||||||
data.path = target.path;
|
|
||||||
data.pathMatchType = target.pathMatchType;
|
|
||||||
data.rewritePath = target.rewritePath;
|
|
||||||
data.rewritePathType = target.rewritePathType;
|
|
||||||
data.priority = target.priority;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (target.new) {
|
|
||||||
const res = await api.put<
|
|
||||||
AxiosResponse<CreateTargetResponse>
|
|
||||||
>(`/resource/${resource.resourceId}/target`, data);
|
|
||||||
target.targetId = res.data.data.targetId;
|
|
||||||
target.new = false;
|
|
||||||
} else if (target.updated) {
|
|
||||||
await api.post(`/target/${target.targetId}`, data);
|
|
||||||
target.updated = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title:
|
|
||||||
targets.length === 0
|
|
||||||
? t("targetTargetsCleared")
|
|
||||||
: t("settingsUpdated"),
|
|
||||||
description:
|
|
||||||
targets.length === 0
|
|
||||||
? t("targetTargetsClearedDescription")
|
|
||||||
: t("settingsUpdatedDescription")
|
|
||||||
});
|
|
||||||
|
|
||||||
setTargetsToRemove([]);
|
|
||||||
router.refresh();
|
|
||||||
await queryClient.invalidateQueries(
|
|
||||||
resourceQueries.resourceTargets({
|
|
||||||
resourceId: resource.resourceId
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
toast({
|
|
||||||
variant: "destructive",
|
|
||||||
title: t("settingsErrorUpdate"),
|
|
||||||
description: formatAxiosError(
|
|
||||||
err,
|
|
||||||
t("settingsErrorUpdateDescription")
|
|
||||||
)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<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>
|
|
||||||
{/* <TableCaption> */}
|
|
||||||
{/* {t('targetNoOneDescription')} */}
|
|
||||||
{/* </TableCaption> */}
|
|
||||||
</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>
|
|
||||||
|
|
||||||
<form className="self-end mt-4" action={formAction}>
|
|
||||||
<Button
|
|
||||||
disabled={isSubmitting}
|
|
||||||
loading={isSubmitting}
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
{t("saveResourceTargets")}
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</SettingsSection>
|
|
||||||
|
|
||||||
{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) {
|
|
||||||
updateTargetHealthCheck(
|
|
||||||
selectedTargetForHealthCheck.targetId,
|
|
||||||
config
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ProxyResourceHttpForm({
|
function ProxyResourceHttpForm({
|
||||||
resource,
|
resource,
|
||||||
updateResource
|
updateResource
|
||||||
|
|||||||
@@ -86,6 +86,10 @@ import { ListTargetsResponse } from "@server/routers/target";
|
|||||||
import { ListRemoteExitNodesResponse } from "@server/routers/remoteExitNode/types";
|
import { ListRemoteExitNodesResponse } from "@server/routers/remoteExitNode/types";
|
||||||
import { ArrayElement } from "@server/types/ArrayElement";
|
import { ArrayElement } from "@server/types/ArrayElement";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import {
|
||||||
|
LocalTarget,
|
||||||
|
ProxyResourceTargetsForm
|
||||||
|
} from "@app/app/[orgId]/settings/resources/proxy/ProxyResourceTargetsForm";
|
||||||
import {
|
import {
|
||||||
ColumnDef,
|
ColumnDef,
|
||||||
flexRender,
|
flexRender,
|
||||||
@@ -206,15 +210,6 @@ const addTargetSchema = z
|
|||||||
|
|
||||||
type NewResourceType = "http" | "ssh" | "rdp" | "vnc" | "tcp" | "udp";
|
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() {
|
export default function Page() {
|
||||||
const { env } = useEnvContext();
|
const { env } = useEnvContext();
|
||||||
const api = createApiClient({ env });
|
const api = createApiClient({ env });
|
||||||
@@ -238,15 +233,8 @@ export default function Page() {
|
|||||||
// Resource type state
|
// Resource type state
|
||||||
const [resourceType, setResourceType] = useState<NewResourceType>("http");
|
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 [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
|
// SSH-specific state
|
||||||
const [sshServerMode, setSshServerMode] = useState<"standard" | "native">(
|
const [sshServerMode, setSshServerMode] = useState<"standard" | "native">(
|
||||||
@@ -306,23 +294,6 @@ export default function Page() {
|
|||||||
fetchExitNodes();
|
fetchExitNodes();
|
||||||
}, [orgId]);
|
}, [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
|
// Derived flags
|
||||||
const isHttpResource = resourceType !== "tcp" && resourceType !== "udp";
|
const isHttpResource = resourceType !== "tcp" && resourceType !== "udp";
|
||||||
const isNative = sshServerMode === "native";
|
const isNative = sshServerMode === "native";
|
||||||
@@ -334,48 +305,6 @@ export default function Page() {
|
|||||||
pamMode === "push" &&
|
pamMode === "push" &&
|
||||||
standardDaemonLocation === "remote";
|
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
|
// Whether raw (TCP/UDP) resources are available
|
||||||
const rawResourcesAllowed =
|
const rawResourcesAllowed =
|
||||||
env.flags.allowRawResources &&
|
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
|
// Sync form http field with resourceType
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
baseForm.setValue("http", isHttpResource);
|
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() {
|
async function onSubmit() {
|
||||||
setCreateLoading(true);
|
setCreateLoading(true);
|
||||||
|
|
||||||
@@ -787,402 +636,6 @@ export default function Page() {
|
|||||||
setCreateLoading(false);
|
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
|
// SSH strategy options
|
||||||
const sshModeOptions: StrategyOption<"standard" | "native">[] = [
|
const sshModeOptions: StrategyOption<"standard" | "native">[] = [
|
||||||
{
|
{
|
||||||
@@ -1732,207 +1185,11 @@ export default function Page() {
|
|||||||
{(resourceType === "http" ||
|
{(resourceType === "http" ||
|
||||||
resourceType === "tcp" ||
|
resourceType === "tcp" ||
|
||||||
resourceType === "udp") && (
|
resourceType === "udp") && (
|
||||||
<SettingsSection>
|
<ProxyResourceTargetsForm
|
||||||
<SettingsSectionHeader>
|
orgId={orgId!.toString()}
|
||||||
<SettingsSectionTitle>
|
isHttp={resourceType === "http"}
|
||||||
{t("targets")}
|
onChange={setTargets}
|
||||||
</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>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex justify-end space-x-2 mt-8">
|
<div className="flex justify-end space-x-2 mt-8">
|
||||||
@@ -1977,77 +1234,6 @@ export default function Page() {
|
|||||||
{t("resourceCreate")}
|
{t("resourceCreate")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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>
|
||||||
) : (
|
) : (
|
||||||
<SettingsContainer>
|
<SettingsContainer>
|
||||||
|
|||||||
Reference in New Issue
Block a user