Adjusting the create ui

This commit is contained in:
Owen
2026-05-26 16:10:06 -07:00
parent d47ad9ac40
commit 0d4bb65a92
4 changed files with 145 additions and 324 deletions

View File

@@ -240,6 +240,7 @@
"domainType": "Domain Type", "domainType": "Domain Type",
"subdomain": "Subdomain", "subdomain": "Subdomain",
"baseDomain": "Base Domain", "baseDomain": "Base Domain",
"configure": "Configure",
"subdomnainDescription": "The subdomain where the resource will be accessible.", "subdomnainDescription": "The subdomain where the resource will be accessible.",
"resourceRawSettings": "TCP/UDP Settings", "resourceRawSettings": "TCP/UDP Settings",
"resourceRawSettingsDescription": "Configure how the resource will be accessed over TCP/UDP", "resourceRawSettingsDescription": "Configure how the resource will be accessed over TCP/UDP",

View File

@@ -23,7 +23,10 @@ import { OpenAPITags, registry } from "@server/openApi";
import { build } from "@server/build"; import { build } from "@server/build";
import { createCertificate } from "#dynamic/routers/certificates/createCertificate"; import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
import { getUniqueResourceName } from "@server/db/names"; import { getUniqueResourceName } from "@server/db/names";
import { validateAndConstructDomain, checkWildcardDomainConflict } from "@server/lib/domainUtils"; import {
validateAndConstructDomain,
checkWildcardDomainConflict
} from "@server/lib/domainUtils";
import { isSubscribed } from "#dynamic/lib/isSubscribed"; import { isSubscribed } from "#dynamic/lib/isSubscribed";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed"; import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix"; import { tierMatrix } from "@server/lib/billing/tierMatrix";
@@ -40,7 +43,8 @@ const createHttpResourceSchema = z
protocol: z.enum(["tcp", "udp"]), protocol: z.enum(["tcp", "udp"]),
domainId: z.string(), domainId: z.string(),
stickySession: z.boolean().optional(), stickySession: z.boolean().optional(),
postAuthPath: z.string().nullable().optional() postAuthPath: z.string().nullable().optional(),
browserAccessType: z.enum(["http", "ssh", "rdp", "vnc"]).optional()
}) })
.refine( .refine(
(data) => { (data) => {
@@ -198,7 +202,7 @@ async function createHttpResource(
); );
} }
const { name, domainId, postAuthPath } = parsedBody.data; const { name, domainId, postAuthPath, browserAccessType } = parsedBody.data;
const subdomain = parsedBody.data.subdomain; const subdomain = parsedBody.data.subdomain;
const stickySession = parsedBody.data.stickySession; const stickySession = parsedBody.data.stickySession;
@@ -323,6 +327,7 @@ async function createHttpResource(
name, name,
subdomain: finalSubdomain, subdomain: finalSubdomain,
http: true, http: true,
browserAccessType: browserAccessType,
protocol: "tcp", protocol: "tcp",
ssl: true, ssl: true,
stickySession: stickySession, stickySession: stickySession,

View File

@@ -121,10 +121,6 @@ export default function ReverseProxyTargetsPage(props: {
const params = use(props.params); const params = use(props.params);
const { resource, updateResource } = useResourceContext(); const { resource, updateResource } = useResourceContext();
const [targetMode, setTargetMode] = useState<
"http" | "ssh" | "rdp" | "vnc"
>((resource.browserAccessType as "http" | "ssh" | "rdp" | "vnc") || "http");
const { data: remoteTargets = [], isLoading: isLoadingTargets } = useQuery( const { data: remoteTargets = [], isLoading: isLoadingTargets } = useQuery(
resourceQueries.resourceTargets({ resourceQueries.resourceTargets({
resourceId: resource.resourceId resourceId: resource.resourceId
@@ -141,12 +137,10 @@ export default function ReverseProxyTargetsPage(props: {
orgId={params.orgId} orgId={params.orgId}
initialTargets={remoteTargets} initialTargets={remoteTargets}
resource={resource} resource={resource}
targetMode={targetMode}
setTargetMode={setTargetMode}
updateResource={updateResource} updateResource={updateResource}
/> />
{resource.http && targetMode === "http" && ( {resource.http && (
<ProxyResourceHttpForm <ProxyResourceHttpForm
resource={resource} resource={resource}
updateResource={updateResource} updateResource={updateResource}
@@ -167,15 +161,11 @@ function ProxyResourceTargetsForm({
orgId, orgId,
initialTargets, initialTargets,
resource, resource,
targetMode,
setTargetMode,
updateResource updateResource
}: { }: {
initialTargets: LocalTarget[]; initialTargets: LocalTarget[];
orgId: string; orgId: string;
resource: GetResourceResponse; resource: GetResourceResponse;
targetMode: "http" | "ssh" | "rdp" | "vnc";
setTargetMode: (mode: "http" | "ssh" | "rdp" | "vnc") => void;
updateResource: ResourceContextType["updateResource"]; updateResource: ResourceContextType["updateResource"];
}) { }) {
const t = useTranslations(); const t = useTranslations();
@@ -310,7 +300,6 @@ function ProxyResourceTargetsForm({
useEffect(() => { useEffect(() => {
if (!bgTargetsResponse?.targets?.length) return; if (!bgTargetsResponse?.targets?.length) return;
const bgt = bgTargetsResponse.targets[0]; const bgt = bgTargetsResponse.targets[0];
setTargetMode(bgt.type as "ssh" | "rdp" | "vnc");
setBgDestination(bgt.destination); setBgDestination(bgt.destination);
setBgDestinationPort(String(bgt.destinationPort)); setBgDestinationPort(String(bgt.destinationPort));
setBgSiteId(bgt.siteId); setBgSiteId(bgt.siteId);
@@ -781,55 +770,6 @@ 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) =>
@@ -944,187 +884,102 @@ function ProxyResourceTargetsForm({
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
<div className="flex items-center gap-3 mb-4"> {targets.length > 0 ? (
<span className="text-sm font-medium">Target Type</span>
<Select
value={targetMode}
onValueChange={async (v) => {
const mode = v as
| "http"
| "ssh"
| "rdp"
| "vnc";
setTargetMode(mode);
try {
await api.post(
`/resource/${resource.resourceId}`,
{ browserAccessType: mode }
);
updateResource({ browserAccessType: mode });
} catch (err) {
toast({
variant: "destructive",
title: t("settingsErrorUpdate"),
description: formatAxiosError(
err,
t("settingsErrorUpdateDescription")
)
});
}
}}
>
<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" ? (
<> <>
{targets.length > 0 ? ( <div className="overflow-x-auto">
<> <Table>
<div className="overflow-x-auto"> <TableHeader>
<Table> {table
<TableHeader> .getHeaderGroups()
{table .map((headerGroup) => (
.getHeaderGroups() <TableRow key={headerGroup.id}>
.map((headerGroup) => ( {headerGroup.headers.map(
<TableRow (header) => {
key={headerGroup.id} const isActionsColumn =
> header.column
{headerGroup.headers.map( .id ===
(header) => { "actions";
const isActionsColumn = return (
header <TableHead
.column key={
.id === header.id
"actions"; }
return ( className={
<TableHead isActionsColumn
key={ ? "sticky right-0 z-10 w-auto min-w-fit bg-card"
header.id : ""
} }
className={ >
isActionsColumn {header.isPlaceholder
? "sticky right-0 z-10 w-auto min-w-fit bg-card" ? null
: "" : flexRender(
} header
> .column
{header.isPlaceholder .columnDef
? null .header,
: flexRender( header.getContext()
header )}
.column </TableHead>
.columnDef );
.header, }
header.getContext() )}
)} </TableRow>
</TableHead> ))}
); </TableHeader>
} <TableBody>
)} {table.getRowModel().rows?.length ? (
</TableRow> table
))} .getRowModel()
</TableHeader> .rows.map((row) => (
<TableBody> <TableRow key={row.id}>
{table.getRowModel().rows {row
?.length ? ( .getVisibleCells()
table .map((cell) => {
.getRowModel() const isActionsColumn =
.rows.map((row) => ( cell.column
<TableRow .id ===
key={row.id} "actions";
> return (
{row <TableCell
.getVisibleCells() key={
.map( cell.id
(
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>
);
} }
)} className={
</TableRow> isActionsColumn
)) ? "sticky right-0 z-10 w-auto min-w-fit bg-card"
) : ( : ""
<TableRow> }
<TableCell >
colSpan={ {flexRender(
columns.length cell
} .column
className="h-24 text-center" .columnDef
> .cell,
{t("targetNoOne")} cell.getContext()
</TableCell> )}
</TableCell>
);
})}
</TableRow> </TableRow>
)} ))
</TableBody> ) : (
{/* <TableCaption> */} <TableRow>
{/* {t('targetNoOneDescription')} */} <TableCell
{/* </TableCaption> */} colSpan={columns.length}
</Table> className="h-24 text-center"
</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")} {t("targetNoOne")}
</label> </TableCell>
</div> </TableRow>
</div> )}
</div> </TableBody>
</> {/* <TableCaption> */}
) : ( {/* {t('targetNoOneDescription')} */}
<div className="text-center py-8 border-2 border-dashed border-muted rounded-lg p-4"> {/* </TableCaption> */}
<p className="text-muted-foreground mb-4"> </Table>
{t("targetNoOne")} </div>
</p> <div className="flex items-center justify-between mb-4">
<div className="flex items-center justify-between w-full gap-2">
<Button <Button
onClick={addNewTarget} onClick={addNewTarget}
variant="outline" variant="outline"
@@ -1132,91 +987,50 @@ 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> <div className="flex items-center gap-2">
)} <Switch
{build === "saas" && id="advanced-mode-toggle"
targets.length > 1 && checked={isAdvancedMode}
new Set(targets.map((t) => t.siteId)).size > onCheckedChange={setIsAdvancedMode}
1 && ( />
<p className="text-sm text-muted-foreground mt-3"> <label
{t("proxyMultiSiteRoundRobinNodeHelp")}{" "} htmlFor="advanced-mode-toggle"
<a className="text-sm"
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")} {t("advancedMode")}
<ExternalLink className="size-3.5 shrink-0" /> </label>
</a> </div>
.
</p>
)}
</>
) : (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium">
Destination
</label>
<Input
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>
</div> </div>
{sites.length > 1 && ( </>
<div className="space-y-2"> ) : (
<label className="text-sm font-medium"> <div className="text-center py-8 border-2 border-dashed border-muted rounded-lg p-4">
Site <p className="text-muted-foreground mb-4">
</label> {t("targetNoOne")}
<Select </p>
value={bgSiteId ? String(bgSiteId) : ""} <Button onClick={addNewTarget} variant="outline">
onValueChange={(v) => <Plus className="h-4 w-4 mr-2" />
setBgSiteId(Number(v)) {t("addTarget")}
} </Button>
>
<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}>

View File

@@ -250,7 +250,7 @@ export default function Page() {
// SSH-specific state // SSH-specific state
const [sshServerMode, setSshServerMode] = useState<"standard" | "native">( const [sshServerMode, setSshServerMode] = useState<"standard" | "native">(
"standard" "native"
); );
const [pamMode, setPamMode] = useState<"passthrough" | "push">( const [pamMode, setPamMode] = useState<"passthrough" | "push">(
"passthrough" "passthrough"
@@ -544,7 +544,8 @@ export default function Page() {
try { try {
const payload: any = { const payload: any = {
name: baseData.name, name: baseData.name,
http: isHttpResource http: isHttpResource,
browserAccessType: resourceType
}; };
let sanitizedSubdomain: string | undefined; let sanitizedSubdomain: string | undefined;
@@ -1265,7 +1266,7 @@ export default function Page() {
{t("resourceCreateGeneral")} {t("resourceCreateGeneral")}
</SettingsSectionTitle> </SettingsSectionTitle>
<SettingsSectionDescription> <SettingsSectionDescription>
{t("resourceCreateDescription")} {t("resourceCreateGeneralDescription")}
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>