mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-18 14:55:22 +00:00
Adjust page to be editable
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
browserGatewayTarget,
|
||||||
db,
|
db,
|
||||||
resourceHeaderAuth,
|
resourceHeaderAuth,
|
||||||
resourceHeaderAuthExtendedCompatibility,
|
resourceHeaderAuthExtendedCompatibility,
|
||||||
@@ -433,6 +434,30 @@ export async function listResources(
|
|||||||
)
|
)
|
||||||
.leftJoin(sites, eq(targets.siteId, sites.siteId));
|
.leftJoin(sites, eq(targets.siteId, sites.siteId));
|
||||||
|
|
||||||
|
const allBgTargetSites =
|
||||||
|
resourceIdList.length === 0
|
||||||
|
? []
|
||||||
|
: await db
|
||||||
|
.select({
|
||||||
|
resourceId: browserGatewayTarget.resourceId,
|
||||||
|
siteId: browserGatewayTarget.siteId,
|
||||||
|
siteName: sites.name,
|
||||||
|
siteNiceId: sites.niceId,
|
||||||
|
siteOnline: sites.online,
|
||||||
|
siteType: sites.type
|
||||||
|
})
|
||||||
|
.from(browserGatewayTarget)
|
||||||
|
.where(
|
||||||
|
inArray(
|
||||||
|
browserGatewayTarget.resourceId,
|
||||||
|
resourceIdList
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
sites,
|
||||||
|
eq(sites.siteId, browserGatewayTarget.siteId)
|
||||||
|
);
|
||||||
|
|
||||||
// avoids TS issues with reduce/never[]
|
// avoids TS issues with reduce/never[]
|
||||||
const map = new Map<number, ResourceWithTargets>();
|
const map = new Map<number, ResourceWithTargets>();
|
||||||
|
|
||||||
@@ -493,6 +518,21 @@ export async function listResources(
|
|||||||
online: isLocal ? undefined : Boolean(t.siteOnline)
|
online: isLocal ? undefined : Boolean(t.siteOnline)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const bgRaw = allBgTargetSites.filter(
|
||||||
|
(t) => t.resourceId === entry.resourceId
|
||||||
|
);
|
||||||
|
for (const t of bgRaw) {
|
||||||
|
if (typeof t.siteId !== "number" || siteById.has(t.siteId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const isLocal = t.siteType === "local";
|
||||||
|
siteById.set(t.siteId, {
|
||||||
|
siteId: t.siteId,
|
||||||
|
siteName: t.siteName ?? "",
|
||||||
|
siteNiceId: t.siteNiceId ?? "",
|
||||||
|
online: isLocal ? undefined : Boolean(t.siteOnline)
|
||||||
|
});
|
||||||
|
}
|
||||||
entry.sites = Array.from(siteById.values());
|
entry.sites = Array.from(siteById.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -201,6 +201,14 @@ function ProxyResourceTargetsForm({
|
|||||||
const [selectedTargetForHealthCheck, setSelectedTargetForHealthCheck] =
|
const [selectedTargetForHealthCheck, setSelectedTargetForHealthCheck] =
|
||||||
useState<LocalTarget | null>(null);
|
useState<LocalTarget | null>(null);
|
||||||
|
|
||||||
|
const [targetMode, setTargetMode] = useState<
|
||||||
|
"http" | "ssh" | "rdp" | "vnc"
|
||||||
|
>("http");
|
||||||
|
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) => {
|
const initializeDockerForSite = async (siteId: number) => {
|
||||||
if (dockerStates.has(siteId)) {
|
if (dockerStates.has(siteId)) {
|
||||||
return; // Already initialized
|
return; // Already initialized
|
||||||
@@ -270,6 +278,41 @@ function ProxyResourceTargetsForm({
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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];
|
||||||
|
setTargetMode(bgt.type as "ssh" | "rdp" | "vnc");
|
||||||
|
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(
|
const updateTarget = useCallback(
|
||||||
(targetId: number, data: Partial<LocalTarget>) => {
|
(targetId: number, data: Partial<LocalTarget>) => {
|
||||||
setTargets((prevTargets) => {
|
setTargets((prevTargets) => {
|
||||||
@@ -356,7 +399,7 @@ function ProxyResourceTargetsForm({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center w-full">
|
<div className="flex items-center justify-center w-full">
|
||||||
{row.original.siteType === "newt" ? (
|
{row.original.siteType === "newt" ? (
|
||||||
<Button
|
<Button
|
||||||
@@ -375,7 +418,6 @@ function ProxyResourceTargetsForm({
|
|||||||
{getStatusText(status)}
|
{getStatusText(status)}
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
) : (
|
) : (
|
||||||
<span>-</span>
|
<span>-</span>
|
||||||
)}
|
)}
|
||||||
@@ -404,9 +446,15 @@ function ProxyResourceTargetsForm({
|
|||||||
pathMatchType: row.original.pathMatchType
|
pathMatchType: row.original.pathMatchType
|
||||||
}}
|
}}
|
||||||
onChange={(config) =>
|
onChange={(config) =>
|
||||||
updateTarget(row.original.targetId,
|
updateTarget(
|
||||||
config.path === null && config.pathMatchType === null
|
row.original.targetId,
|
||||||
? { ...config, rewritePath: null, rewritePathType: null }
|
config.path === null &&
|
||||||
|
config.pathMatchType === null
|
||||||
|
? {
|
||||||
|
...config,
|
||||||
|
rewritePath: null,
|
||||||
|
rewritePathType: null
|
||||||
|
}
|
||||||
: config
|
: config
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -432,9 +480,15 @@ function ProxyResourceTargetsForm({
|
|||||||
pathMatchType: row.original.pathMatchType
|
pathMatchType: row.original.pathMatchType
|
||||||
}}
|
}}
|
||||||
onChange={(config) =>
|
onChange={(config) =>
|
||||||
updateTarget(row.original.targetId,
|
updateTarget(
|
||||||
config.path === null && config.pathMatchType === null
|
row.original.targetId,
|
||||||
? { ...config, rewritePath: null, rewritePathType: null }
|
config.path === null &&
|
||||||
|
config.pathMatchType === null
|
||||||
|
? {
|
||||||
|
...config,
|
||||||
|
rewritePath: null,
|
||||||
|
rewritePathType: null
|
||||||
|
}
|
||||||
: config
|
: config
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -717,6 +771,55 @@ function ProxyResourceTargetsForm({
|
|||||||
const [, formAction, isSubmitting] = useActionState(saveTargets, null);
|
const [, formAction, isSubmitting] = useActionState(saveTargets, null);
|
||||||
|
|
||||||
async function saveTargets() {
|
async function saveTargets() {
|
||||||
|
if (targetMode !== "http") {
|
||||||
|
try {
|
||||||
|
if (!bgDestination || !bgDestinationPort) {
|
||||||
|
if (bgTargetId) {
|
||||||
|
await api.delete(
|
||||||
|
`/org/${orgId}/browser-gateway-target/${bgTargetId}`
|
||||||
|
);
|
||||||
|
setBgTargetId(null);
|
||||||
|
}
|
||||||
|
} else if (bgTargetId) {
|
||||||
|
await api.post(
|
||||||
|
`/org/${orgId}/browser-gateway-target/${bgTargetId}`,
|
||||||
|
{
|
||||||
|
type: targetMode,
|
||||||
|
destination: bgDestination,
|
||||||
|
destinationPort: Number(bgDestinationPort),
|
||||||
|
siteId: bgSiteId
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const res = await api.put(
|
||||||
|
`/org/${orgId}/resource/${resource.resourceId}/browser-gateway-target`,
|
||||||
|
{
|
||||||
|
siteId: bgSiteId ?? sites[0]?.siteId,
|
||||||
|
type: targetMode,
|
||||||
|
destination: bgDestination,
|
||||||
|
destinationPort: Number(bgDestinationPort)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
setBgTargetId(res.data.data.browserGatewayTargetId);
|
||||||
|
}
|
||||||
|
toast({
|
||||||
|
title: t("settingsUpdated"),
|
||||||
|
description: t("settingsUpdatedDescription")
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: t("settingsErrorUpdate"),
|
||||||
|
description: formatAxiosError(
|
||||||
|
err,
|
||||||
|
t("settingsErrorUpdateDescription")
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Validate that no targets have blank IPs or invalid ports
|
// Validate that no targets have blank IPs or invalid ports
|
||||||
const targetsWithInvalidFields = targets.filter(
|
const targetsWithInvalidFields = targets.filter(
|
||||||
(target) =>
|
(target) =>
|
||||||
@@ -791,12 +894,14 @@ function ProxyResourceTargetsForm({
|
|||||||
}
|
}
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: targets.length === 0
|
title:
|
||||||
? t("targetTargetsCleared")
|
targets.length === 0
|
||||||
: t("settingsUpdated"),
|
? t("targetTargetsCleared")
|
||||||
description: targets.length === 0
|
: t("settingsUpdated"),
|
||||||
? t("targetTargetsClearedDescription")
|
description:
|
||||||
: t("settingsUpdatedDescription")
|
targets.length === 0
|
||||||
|
? t("targetTargetsClearedDescription")
|
||||||
|
: t("settingsUpdatedDescription")
|
||||||
});
|
});
|
||||||
|
|
||||||
setTargetsToRemove([]);
|
setTargetsToRemove([]);
|
||||||
@@ -829,102 +934,168 @@ function ProxyResourceTargetsForm({
|
|||||||
</SettingsSectionDescription>
|
</SettingsSectionDescription>
|
||||||
</SettingsSectionHeader>
|
</SettingsSectionHeader>
|
||||||
<SettingsSectionBody>
|
<SettingsSectionBody>
|
||||||
{targets.length > 0 ? (
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
<span className="text-sm font-medium">Target Type</span>
|
||||||
|
<Select
|
||||||
|
value={targetMode}
|
||||||
|
onValueChange={(v) =>
|
||||||
|
setTargetMode(
|
||||||
|
v as "http" | "ssh" | "rdp" | "vnc"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-36">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="http">HTTP</SelectItem>
|
||||||
|
<SelectItem value="ssh">SSH</SelectItem>
|
||||||
|
<SelectItem value="rdp">RDP</SelectItem>
|
||||||
|
<SelectItem value="vnc">VNC</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
{targetMode === "http" ? (
|
||||||
<>
|
<>
|
||||||
<div className="overflow-x-auto">
|
{targets.length > 0 ? (
|
||||||
<Table>
|
<>
|
||||||
<TableHeader>
|
<div className="overflow-x-auto">
|
||||||
{table
|
<Table>
|
||||||
.getHeaderGroups()
|
<TableHeader>
|
||||||
.map((headerGroup) => (
|
{table
|
||||||
<TableRow key={headerGroup.id}>
|
.getHeaderGroups()
|
||||||
{headerGroup.headers.map(
|
.map((headerGroup) => (
|
||||||
(header) => {
|
<TableRow
|
||||||
const isActionsColumn =
|
key={headerGroup.id}
|
||||||
header.column
|
>
|
||||||
.id ===
|
{headerGroup.headers.map(
|
||||||
"actions";
|
(header) => {
|
||||||
return (
|
const isActionsColumn =
|
||||||
<TableHead
|
header
|
||||||
key={
|
.column
|
||||||
header.id
|
.id ===
|
||||||
}
|
"actions";
|
||||||
className={
|
return (
|
||||||
isActionsColumn
|
<TableHead
|
||||||
? "sticky right-0 z-10 w-auto min-w-fit bg-card"
|
key={
|
||||||
: ""
|
header.id
|
||||||
}
|
}
|
||||||
>
|
className={
|
||||||
{header.isPlaceholder
|
isActionsColumn
|
||||||
? null
|
? "sticky right-0 z-10 w-auto min-w-fit bg-card"
|
||||||
: flexRender(
|
: ""
|
||||||
header
|
}
|
||||||
.column
|
>
|
||||||
.columnDef
|
{header.isPlaceholder
|
||||||
.header,
|
? null
|
||||||
header.getContext()
|
: flexRender(
|
||||||
)}
|
header
|
||||||
</TableHead>
|
.column
|
||||||
);
|
.columnDef
|
||||||
}
|
.header,
|
||||||
)}
|
header.getContext()
|
||||||
</TableRow>
|
)}
|
||||||
))}
|
</TableHead>
|
||||||
</TableHeader>
|
);
|
||||||
<TableBody>
|
}
|
||||||
{table.getRowModel().rows?.length ? (
|
)}
|
||||||
table
|
</TableRow>
|
||||||
.getRowModel()
|
))}
|
||||||
.rows.map((row) => (
|
</TableHeader>
|
||||||
<TableRow key={row.id}>
|
<TableBody>
|
||||||
{row
|
{table.getRowModel().rows
|
||||||
.getVisibleCells()
|
?.length ? (
|
||||||
.map((cell) => {
|
table
|
||||||
const isActionsColumn =
|
.getRowModel()
|
||||||
cell.column
|
.rows.map((row) => (
|
||||||
.id ===
|
<TableRow
|
||||||
"actions";
|
key={row.id}
|
||||||
return (
|
>
|
||||||
<TableCell
|
{row
|
||||||
key={
|
.getVisibleCells()
|
||||||
cell.id
|
.map(
|
||||||
}
|
(
|
||||||
className={
|
|
||||||
isActionsColumn
|
|
||||||
? "sticky right-0 z-10 w-auto min-w-fit bg-card"
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{flexRender(
|
|
||||||
cell
|
cell
|
||||||
.column
|
) => {
|
||||||
.columnDef
|
const isActionsColumn =
|
||||||
.cell,
|
cell
|
||||||
cell.getContext()
|
.column
|
||||||
)}
|
.id ===
|
||||||
</TableCell>
|
"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>
|
</TableRow>
|
||||||
))
|
)}
|
||||||
) : (
|
</TableBody>
|
||||||
<TableRow>
|
{/* <TableCaption> */}
|
||||||
<TableCell
|
{/* {t('targetNoOneDescription')} */}
|
||||||
colSpan={columns.length}
|
{/* </TableCaption> */}
|
||||||
className="h-24 text-center"
|
</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("targetNoOne")}
|
{t("advancedMode")}
|
||||||
</TableCell>
|
</label>
|
||||||
</TableRow>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</TableBody>
|
</div>
|
||||||
{/* <TableCaption> */}
|
</>
|
||||||
{/* {t('targetNoOneDescription')} */}
|
) : (
|
||||||
{/* </TableCaption> */}
|
<div className="text-center py-8 border-2 border-dashed border-muted rounded-lg p-4">
|
||||||
</Table>
|
<p className="text-muted-foreground mb-4">
|
||||||
</div>
|
{t("targetNoOne")}
|
||||||
<div className="flex items-center justify-between mb-4">
|
</p>
|
||||||
<div className="flex items-center justify-between w-full gap-2">
|
|
||||||
<Button
|
<Button
|
||||||
onClick={addNewTarget}
|
onClick={addNewTarget}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -932,50 +1103,91 @@ function ProxyResourceTargetsForm({
|
|||||||
<Plus className="h-4 w-4 mr-2" />
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
{t("addTarget")}
|
{t("addTarget")}
|
||||||
</Button>
|
</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>
|
)}
|
||||||
|
{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>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center py-8 border-2 border-dashed border-muted rounded-lg p-4">
|
<div className="space-y-4">
|
||||||
<p className="text-muted-foreground mb-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
{t("targetNoOne")}
|
<div className="space-y-2">
|
||||||
</p>
|
<label className="text-sm font-medium">
|
||||||
<Button onClick={addNewTarget} variant="outline">
|
Destination
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
</label>
|
||||||
{t("addTarget")}
|
<Input
|
||||||
</Button>
|
placeholder="192.168.1.1"
|
||||||
|
value={bgDestination}
|
||||||
|
onChange={(e) =>
|
||||||
|
setBgDestination(e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">
|
||||||
|
Port
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder={
|
||||||
|
targetMode === "rdp"
|
||||||
|
? "3389"
|
||||||
|
: targetMode === "ssh"
|
||||||
|
? "22"
|
||||||
|
: "5900"
|
||||||
|
}
|
||||||
|
value={bgDestinationPort}
|
||||||
|
onChange={(e) =>
|
||||||
|
setBgDestinationPort(e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{sites.length > 1 && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">
|
||||||
|
Site
|
||||||
|
</label>
|
||||||
|
<Select
|
||||||
|
value={bgSiteId ? String(bgSiteId) : ""}
|
||||||
|
onValueChange={(v) =>
|
||||||
|
setBgSiteId(Number(v))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a site" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{sites.map((site) => (
|
||||||
|
<SelectItem
|
||||||
|
key={site.siteId}
|
||||||
|
value={String(site.siteId)}
|
||||||
|
>
|
||||||
|
{site.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</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>
|
</SettingsSectionBody>
|
||||||
|
|
||||||
<form className="self-end mt-4" action={formAction}>
|
<form className="self-end mt-4" action={formAction}>
|
||||||
|
|||||||
@@ -322,9 +322,7 @@ export default function RdpClient({
|
|||||||
<div className="min-h-screen bg-background">
|
<div className="min-h-screen bg-background">
|
||||||
{showLogin && (
|
{showLogin && (
|
||||||
<div className="mx-auto max-w-2xl p-6">
|
<div className="mx-auto max-w-2xl p-6">
|
||||||
<h1 className="mb-4 text-2xl font-semibold">
|
<h1 className="mb-4 text-2xl font-semibold">RDP</h1>
|
||||||
RDP Test Connection
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Field label="Domain" id="domain">
|
<Field label="Domain" id="domain">
|
||||||
|
|||||||
@@ -244,9 +244,7 @@ export default function SshClient({
|
|||||||
<div className="min-h-screen bg-background">
|
<div className="min-h-screen bg-background">
|
||||||
{!connected && (
|
{!connected && (
|
||||||
<div className="mx-auto max-w-2xl p-6">
|
<div className="mx-auto max-w-2xl p-6">
|
||||||
<h1 className="mb-4 text-2xl font-semibold">
|
<h1 className="mb-4 text-2xl font-semibold">SSH</h1>
|
||||||
SSH Terminal
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Field label="Username" id="username">
|
<Field label="Username" id="username">
|
||||||
|
|||||||
@@ -161,9 +161,7 @@ export default function VncClient({
|
|||||||
<div className="min-h-screen bg-background">
|
<div className="min-h-screen bg-background">
|
||||||
{!connected && (
|
{!connected && (
|
||||||
<div className="mx-auto max-w-2xl p-6">
|
<div className="mx-auto max-w-2xl p-6">
|
||||||
<h1 className="mb-4 text-2xl font-semibold">
|
<h1 className="mb-4 text-2xl font-semibold">VNC</h1>
|
||||||
VNC Test Connection
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Field label="Password (optional)" id="password">
|
<Field label="Password (optional)" id="password">
|
||||||
|
|||||||
Reference in New Issue
Block a user