reorganize components nad hooks for consistency

This commit is contained in:
miloschwartz
2026-07-10 17:05:45 -04:00
parent e1bc0b7efd
commit 39c35fa539
17 changed files with 38 additions and 34 deletions
@@ -0,0 +1,112 @@
"use client";
import { MachinesSelector } from "@app/components/machines-selector";
import { RolesSelector } from "@app/components/roles-selector";
import { UsersSelector } from "@app/components/users-selector";
import { SettingsFormCell, SettingsFormGrid } from "@app/components/Settings";
import type { Tag } from "@app/components/tags/tag-input";
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage
} from "@app/components/ui/form";
import type { PrivateResourceClient } from "@app/lib/privateResourceForm";
import { useTranslations } from "next-intl";
import type { Control } from "react-hook-form";
type AccessFormValues = {
roles?: Tag[];
users?: Tag[];
clients?: PrivateResourceClient[];
};
type PrivateResourceAccessFieldsProps = {
control: Control<AccessFormValues>;
orgId: string;
loading?: boolean;
hasMachineClients?: boolean;
};
export function PrivateResourceAccessFields({
control,
orgId,
loading = false,
hasMachineClients = false
}: PrivateResourceAccessFieldsProps) {
const t = useTranslations();
if (loading) {
return (
<div className="text-sm text-muted-foreground">{t("loading")}</div>
);
}
return (
<SettingsFormGrid>
<SettingsFormCell span="full">
<FormField
control={control}
name="roles"
render={({ field }) => (
<FormItem className="flex flex-col items-start">
<FormLabel>{t("roles")}</FormLabel>
<FormControl>
<RolesSelector
selectedRoles={field.value ?? []}
orgId={orgId}
restrictAdminRole
onSelectRoles={(newRoles) => {
field.onChange(newRoles);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</SettingsFormCell>
<SettingsFormCell span="full">
<FormField
control={control}
name="users"
render={({ field }) => (
<FormItem className="flex flex-col items-start">
<FormLabel>{t("users")}</FormLabel>
<UsersSelector
selectedUsers={field.value ?? []}
orgId={orgId}
onSelectUsers={(newUsers) => {
field.onChange(newUsers);
}}
/>
<FormMessage />
</FormItem>
)}
/>
</SettingsFormCell>
{hasMachineClients && (
<SettingsFormCell span="full">
<FormField
control={control}
name="clients"
render={({ field }) => (
<FormItem className="flex flex-col items-start">
<FormLabel>{t("machineClients")}</FormLabel>
<MachinesSelector
selectedMachines={field.value ?? []}
orgId={orgId}
onSelectMachines={(machines) => {
field.onChange(machines);
}}
/>
<FormMessage />
</FormItem>
)}
/>
</SettingsFormCell>
)}
</SettingsFormGrid>
);
}
@@ -0,0 +1,175 @@
"use client";
import { SettingsFormCell, SettingsFormGrid } from "@app/components/Settings";
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage
} from "@app/components/ui/form";
import { Input } from "@app/components/ui/input";
import { useTranslations } from "next-intl";
import type { Control, UseFormWatch } from "react-hook-form";
type PrivateResourceAliasFieldProps = {
control: Control<any>;
watch: UseFormWatch<any>;
labelPrefix?: "create" | "edit";
disabled?: boolean;
};
export function PrivateResourceAliasField({
control,
watch,
labelPrefix = "edit",
disabled = false
}: PrivateResourceAliasFieldProps) {
const t = useTranslations();
const aliasLabelKey =
labelPrefix === "create"
? "createInternalResourceDialogAlias"
: "editInternalResourceDialogAlias";
const aliasDescriptionKey =
labelPrefix === "create"
? "createInternalResourceDialogAliasDescription"
: "editInternalResourceDialogAliasDescription";
const aliasValue = watch("alias");
const aliasEndsWithLocal =
typeof aliasValue === "string" &&
aliasValue.trim().toLowerCase().endsWith(".local");
return (
<FormField
control={control}
name="alias"
render={({ field }) => (
<FormItem>
<FormLabel>{t(aliasLabelKey)}</FormLabel>
<FormControl>
<Input
{...field}
className="w-full"
value={field.value ?? ""}
disabled={disabled}
/>
</FormControl>
{aliasEndsWithLocal && (
<p className="text-xs text-amber-700/80 mt-1">
{t("internalResourceAliasLocalWarning")}
</p>
)}
<FormMessage />
<FormDescription>{t(aliasDescriptionKey)}</FormDescription>
</FormItem>
)}
/>
);
}
type PrivateResourceHostDestinationFieldsProps = {
control: Control<any>;
watch: UseFormWatch<any>;
labelPrefix?: "create" | "edit";
hideAlias?: boolean;
};
export function PrivateResourceHostDestinationFields({
control,
watch,
labelPrefix = "edit",
hideAlias = false
}: PrivateResourceHostDestinationFieldsProps) {
const t = useTranslations();
const destinationLabelKey =
labelPrefix === "create"
? "createInternalResourceDialogDestination"
: "editInternalResourceDialogDestination";
const destinationField = (
<FormField
control={control}
name="destination"
render={({ field }) => (
<FormItem>
<FormLabel>{t(destinationLabelKey)}</FormLabel>
<FormControl>
<Input
{...field}
className="w-full"
value={field.value ?? ""}
onChange={(e) =>
field.onChange(
e.target.value === ""
? null
: e.target.value
)
}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
);
if (hideAlias) {
return destinationField;
}
return (
<SettingsFormGrid>
<SettingsFormCell span="half">{destinationField}</SettingsFormCell>
<SettingsFormCell span="half">
<PrivateResourceAliasField
control={control}
watch={watch}
labelPrefix={labelPrefix}
/>
</SettingsFormCell>
</SettingsFormGrid>
);
}
export function PrivateResourceCidrDestinationField({
control,
labelPrefix = "edit"
}: {
control: Control<any>;
labelPrefix?: "create" | "edit";
}) {
const t = useTranslations();
const destinationLabelKey =
labelPrefix === "create"
? "createInternalResourceDialogDestination"
: "editInternalResourceDialogDestination";
return (
<FormField
control={control}
name="destination"
render={({ field }) => (
<FormItem>
<FormLabel>{t(destinationLabelKey)}</FormLabel>
<FormControl>
<Input
{...field}
className="w-full"
value={field.value ?? ""}
onChange={(e) =>
field.onChange(
e.target.value === ""
? null
: e.target.value
)
}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
);
}
@@ -0,0 +1,297 @@
"use client";
import DomainPicker from "@app/components/DomainPicker";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import {
SettingsFormCell,
SettingsFormGrid,
SettingsSubsectionDescription,
SettingsSubsectionHeader,
SettingsSubsectionTitle
} from "@app/components/Settings";
import { SwitchInput } from "@app/components/SwitchInput";
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage
} from "@app/components/ui/form";
import { Input } from "@app/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from "@app/components/ui/select";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { useTranslations } from "next-intl";
import type { Control, UseFormSetValue, UseFormWatch } from "react-hook-form";
type PrivateResourceHttpFieldsProps = {
control: Control<any>;
setValue: UseFormSetValue<any>;
orgId: string;
watch: UseFormWatch<any>;
disabled?: boolean;
siteResourceId?: number;
labelPrefix?: "create" | "edit";
hideDomainPicker?: boolean;
hidePaidFeaturesAlert?: boolean;
};
export function PrivateResourceHttpFields({
control,
setValue,
orgId,
watch,
disabled = false,
siteResourceId,
labelPrefix = "edit",
hideDomainPicker = false,
hidePaidFeaturesAlert = false
}: PrivateResourceHttpFieldsProps) {
const t = useTranslations();
const schemeLabelKey =
labelPrefix === "create"
? "createInternalResourceDialogScheme"
: "editInternalResourceDialogScheme";
const destinationLabelKey =
labelPrefix === "create"
? "createInternalResourceDialogDestination"
: "editInternalResourceDialogDestination";
const destinationPortLabelKey =
labelPrefix === "create"
? "createInternalResourceDialogModePort"
: "editInternalResourceDialogModePort";
const httpConfigurationTitleKey =
labelPrefix === "create"
? "createInternalResourceDialogHttpConfiguration"
: "editInternalResourceDialogHttpConfiguration";
const httpConfigurationDescriptionKey =
labelPrefix === "create"
? "createInternalResourceDialogHttpConfigurationDescription"
: "editInternalResourceDialogHttpConfigurationDescription";
const enableSslLabelKey =
labelPrefix === "create"
? "createInternalResourceDialogEnableSsl"
: "editInternalResourceDialogEnableSsl";
const enableSslDescriptionKey =
labelPrefix === "create"
? "createInternalResourceDialogEnableSslDescription"
: "editInternalResourceDialogEnableSslDescription";
const httpConfigSubdomain = watch("httpConfigSubdomain");
const httpConfigDomainId = watch("httpConfigDomainId");
const httpConfigFullDomain = watch("httpConfigFullDomain");
return (
<SettingsFormGrid>
{!hidePaidFeaturesAlert && (
<SettingsFormCell span="full">
<PaidFeaturesAlert
tiers={tierMatrix.advancedPrivateResources}
/>
</SettingsFormCell>
)}
<SettingsFormCell span="quarter">
<FormField
control={control}
name="scheme"
render={({ field }) => (
<FormItem>
<FormLabel>{t(schemeLabelKey)}</FormLabel>
<Select
onValueChange={field.onChange}
value={field.value ?? "http"}
disabled={disabled}
>
<FormControl>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="http">http</SelectItem>
<SelectItem value="https">https</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</SettingsFormCell>
<SettingsFormCell span="half">
<FormField
control={control}
name="destination"
render={({ field }) => (
<FormItem>
<FormLabel>{t(destinationLabelKey)}</FormLabel>
<FormControl>
<Input
{...field}
className="w-full"
value={field.value ?? ""}
disabled={disabled}
onChange={(e) =>
field.onChange(
e.target.value === ""
? null
: e.target.value
)
}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</SettingsFormCell>
<SettingsFormCell span="quarter">
<FormField
control={control}
name="destinationPort"
render={({ field }) => (
<FormItem>
<FormLabel>{t(destinationPortLabelKey)}</FormLabel>
<FormControl>
<Input
className="w-full"
type="number"
min={1}
max={65535}
value={field.value ?? ""}
disabled={disabled}
onChange={(e) => {
const raw = e.target.value;
if (raw === "") {
field.onChange(null);
return;
}
const n = Number(raw);
field.onChange(
Number.isFinite(n) ? n : null
);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</SettingsFormCell>
{!hideDomainPicker && (
<>
<SettingsFormCell span="full">
<SettingsSubsectionHeader>
<SettingsSubsectionTitle>
{t(httpConfigurationTitleKey)}
</SettingsSubsectionTitle>
<SettingsSubsectionDescription>
{t(httpConfigurationDescriptionKey)}
</SettingsSubsectionDescription>
</SettingsSubsectionHeader>
</SettingsFormCell>
<SettingsFormCell span="full">
<div
className={
disabled
? "pointer-events-none opacity-50"
: undefined
}
>
<DomainPicker
key={
siteResourceId
? `http-domain-${siteResourceId}`
: "http-domain-create"
}
orgId={orgId}
cols={2}
hideFreeDomain
defaultSubdomain={
httpConfigSubdomain ?? undefined
}
defaultDomainId={
httpConfigDomainId ?? undefined
}
defaultFullDomain={
httpConfigFullDomain ?? undefined
}
onDomainChange={(res) => {
if (res === null) {
setValue("httpConfigSubdomain", null);
setValue("httpConfigDomainId", null);
setValue("httpConfigFullDomain", null);
return;
}
setValue(
"httpConfigSubdomain",
res.subdomain ?? null
);
setValue(
"httpConfigDomainId",
res.domainId
);
setValue(
"httpConfigFullDomain",
res.fullDomain
);
}}
/>
</div>
</SettingsFormCell>
<SettingsFormCell span="half">
<FormField
control={control}
name="ssl"
render={({ field }) => (
<FormItem>
<FormControl>
<SwitchInput
id="private-resource-ssl"
label={t(enableSslLabelKey)}
description={t(
enableSslDescriptionKey
)}
checked={!!field.value}
onCheckedChange={field.onChange}
disabled={disabled}
/>
</FormControl>
</FormItem>
)}
/>
</SettingsFormCell>
</>
)}
{hideDomainPicker && (
<SettingsFormCell span="half">
<FormField
control={control}
name="ssl"
render={({ field }) => (
<FormItem>
<FormControl>
<SwitchInput
id="private-resource-ssl"
label={t(enableSslLabelKey)}
description={t(enableSslDescriptionKey)}
checked={!!field.value}
onCheckedChange={field.onChange}
disabled={disabled}
/>
</FormControl>
</FormItem>
)}
/>
</SettingsFormCell>
)}
</SettingsFormGrid>
);
}
@@ -0,0 +1,24 @@
"use client";
import { ExternalLink } from "lucide-react";
import { useTranslations } from "next-intl";
export function PrivateResourceMultiSiteRoutingHelp() {
const t = useTranslations();
return (
<p className="text-sm text-muted-foreground mt-2">
{t("internalResourceFormMultiSiteRoutingHelp")}{" "}
<a
href="https://docs.pangolin.net/manage/resources/private/multi-site-routing"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline inline-flex items-center gap-1"
>
{t("internalResourceFormMultiSiteRoutingHelpLearnMore")}
<ExternalLink className="size-3.5 shrink-0" />
</a>
.
</p>
);
}
@@ -0,0 +1,306 @@
"use client";
import {
SettingsFormCell,
SettingsFormGrid,
SettingsSubsectionDescription,
SettingsSubsectionHeader,
SettingsSubsectionTitle
} from "@app/components/Settings";
import { SwitchInput } from "@app/components/SwitchInput";
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage
} from "@app/components/ui/form";
import { Input } from "@app/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from "@app/components/ui/select";
import {
getPortModeFromString,
getPortStringFromMode,
type PortMode
} from "@app/lib/privateResourceForm";
import { useTranslations } from "next-intl";
import { useEffect, useState, type ReactNode } from "react";
import type { Control, UseFormSetValue } from "react-hook-form";
type PrivateResourceNetworkAccessFieldsProps = {
control: Control<any>;
setValue: UseFormSetValue<any>;
showPortRanges?: boolean;
initialTcp?: string | null;
initialUdp?: string | null;
disabled?: boolean;
icmpId?: string;
embedInParentGrid?: boolean;
};
export function PrivateResourceAllowIcmpField({
control,
id = "private-resource-allow-icmp",
disabled = false
}: {
control: Control<any>;
id?: string;
disabled?: boolean;
}) {
const t = useTranslations();
return (
<FormField
control={control}
name="disableIcmp"
render={({ field }) => (
<FormItem>
<FormControl>
<SwitchInput
id={id}
label={t("privateResourceAllowIcmpPing")}
checked={!field.value}
onCheckedChange={(checked) =>
field.onChange(!checked)
}
disabled={disabled}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
);
}
function PrivateResourceNetworkAccessHeader() {
const t = useTranslations();
return (
<SettingsFormCell span="full">
<SettingsSubsectionHeader>
<SettingsSubsectionTitle>
{t("privateResourceNetworkAccess")}
</SettingsSubsectionTitle>
<SettingsSubsectionDescription>
{t("privateResourceNetworkAccessDescription")}
</SettingsSubsectionDescription>
</SettingsSubsectionHeader>
</SettingsFormCell>
);
}
export function PrivateResourceNetworkAccessFields({
control,
setValue,
showPortRanges = true,
initialTcp,
initialUdp,
disabled = false,
icmpId = "private-resource-allow-icmp",
embedInParentGrid = false
}: PrivateResourceNetworkAccessFieldsProps) {
const t = useTranslations();
const resolvedInitialTcp = initialTcp !== undefined ? initialTcp : "*";
const resolvedInitialUdp = initialUdp !== undefined ? initialUdp : "*";
const [tcpPortMode, setTcpPortMode] = useState<PortMode>(() =>
getPortModeFromString(resolvedInitialTcp)
);
const [udpPortMode, setUdpPortMode] = useState<PortMode>(() =>
getPortModeFromString(resolvedInitialUdp)
);
const [tcpCustomPorts, setTcpCustomPorts] = useState(() =>
resolvedInitialTcp && resolvedInitialTcp !== "*"
? resolvedInitialTcp
: ""
);
const [udpCustomPorts, setUdpCustomPorts] = useState(() =>
resolvedInitialUdp && resolvedInitialUdp !== "*"
? resolvedInitialUdp
: ""
);
useEffect(() => {
if (!showPortRanges) return;
setValue(
"tcpPortRangeString",
getPortStringFromMode(tcpPortMode, tcpCustomPorts)
);
}, [showPortRanges, tcpPortMode, tcpCustomPorts, setValue]);
useEffect(() => {
if (!showPortRanges) return;
setValue(
"udpPortRangeString",
getPortStringFromMode(udpPortMode, udpCustomPorts)
);
}, [showPortRanges, udpPortMode, udpCustomPorts, setValue]);
const content: ReactNode = (
<>
<PrivateResourceNetworkAccessHeader />
{showPortRanges ? (
<>
<SettingsFormCell span="full">
<FormField
control={control}
name="tcpPortRangeString"
render={() => (
<FormItem>
<FormLabel>
{t("editInternalResourceDialogTcp")}
</FormLabel>
<div className="flex items-center gap-2">
<Select
value={tcpPortMode}
onValueChange={(v: PortMode) =>
setTcpPortMode(v)
}
>
<FormControl>
<SelectTrigger className="w-[110px]">
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="all">
{t("allPorts")}
</SelectItem>
<SelectItem value="blocked">
{t("blocked")}
</SelectItem>
<SelectItem value="custom">
{t("custom")}
</SelectItem>
</SelectContent>
</Select>
{tcpPortMode === "custom" ? (
<FormControl>
<Input
className="flex-1"
placeholder="80,443,8000-9000"
value={tcpCustomPorts}
onChange={(e) =>
setTcpCustomPorts(
e.target.value
)
}
/>
</FormControl>
) : (
<Input
className="flex-1"
disabled
placeholder={
tcpPortMode === "all"
? t("allPortsAllowed")
: t("allPortsBlocked")
}
/>
)}
</div>
<FormMessage />
</FormItem>
)}
/>
</SettingsFormCell>
<SettingsFormCell span="full">
<FormField
control={control}
name="udpPortRangeString"
render={() => (
<FormItem>
<FormLabel>
{t("editInternalResourceDialogUdp")}
</FormLabel>
<div className="flex items-center gap-2">
<Select
value={udpPortMode}
onValueChange={(v: PortMode) =>
setUdpPortMode(v)
}
>
<FormControl>
<SelectTrigger className="w-[110px]">
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="all">
{t("allPorts")}
</SelectItem>
<SelectItem value="blocked">
{t("blocked")}
</SelectItem>
<SelectItem value="custom">
{t("custom")}
</SelectItem>
</SelectContent>
</Select>
{udpPortMode === "custom" ? (
<FormControl>
<Input
className="flex-1"
placeholder="53,123,500-600"
value={udpCustomPorts}
onChange={(e) =>
setUdpCustomPorts(
e.target.value
)
}
/>
</FormControl>
) : (
<Input
className="flex-1"
disabled
placeholder={
udpPortMode === "all"
? t("allPortsAllowed")
: t("allPortsBlocked")
}
/>
)}
</div>
<FormMessage />
</FormItem>
)}
/>
</SettingsFormCell>
</>
) : null}
<SettingsFormCell span="full">
<PrivateResourceAllowIcmpField
control={control}
id={icmpId}
disabled={disabled}
/>
</SettingsFormCell>
</>
);
if (embedInParentGrid) {
return content;
}
return <SettingsFormGrid>{content}</SettingsFormGrid>;
}
export function PrivateResourcePortRanges(
props: Omit<
PrivateResourceNetworkAccessFieldsProps,
"showPortRanges" | "embedInParentGrid"
>
) {
return <PrivateResourceNetworkAccessFields showPortRanges {...props} />;
}
@@ -0,0 +1,133 @@
"use client";
import {
MultiSitesSelector,
formatMultiSitesSelectorLabel
} from "@app/components/multi-site-selector";
import { SitesSelector } from "@app/components/site-selector";
import type { Selectedsite } from "@app/components/site-selector";
import { Button } from "@app/components/ui/button";
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage
} from "@app/components/ui/form";
import { cn } from "@app/lib/cn";
import {
Popover,
PopoverContent,
PopoverTrigger
} from "@app/components/ui/popover";
import { ChevronsUpDown } from "lucide-react";
import { useTranslations } from "next-intl";
import type { Control, FieldPath, FieldValues } from "react-hook-form";
import { PrivateResourceMultiSiteRoutingHelp } from "@app/components/PrivateResourceMultiSiteRoutingHelp";
type PrivateResourceSitesFieldProps<T extends FieldValues> = {
control: Control<T>;
orgId: string;
selectedSites: Selectedsite[];
onSelectedSitesChange: (sites: Selectedsite[]) => void;
siteIdsFieldName?: FieldPath<T>;
singleSite?: boolean;
};
export function PrivateResourceSitesField<T extends FieldValues>({
control,
orgId,
selectedSites,
onSelectedSitesChange,
siteIdsFieldName = "siteIds" as FieldPath<T>,
singleSite = false
}: PrivateResourceSitesFieldProps<T>) {
const t = useTranslations();
return (
<FormField
control={control}
name={siteIdsFieldName}
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>{t("sites")}</FormLabel>
{singleSite ? (
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
role="combobox"
className={cn(
"w-full justify-between",
selectedSites.length === 0 &&
"text-muted-foreground"
)}
>
<span className="truncate text-left">
{selectedSites[0]?.name ??
t("selectSite")}
</span>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-full p-0">
<SitesSelector
orgId={orgId}
selectedSite={selectedSites[0] ?? null}
filterTypes={["newt"]}
onSelectSite={(site) => {
onSelectedSitesChange([site]);
field.onChange([site.siteId]);
}}
/>
</PopoverContent>
</Popover>
) : (
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
role="combobox"
className={cn(
"w-full justify-between",
selectedSites.length === 0 &&
"text-muted-foreground"
)}
>
<span className="truncate text-left">
{formatMultiSitesSelectorLabel(
selectedSites,
t
)}
</span>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-full p-0">
<MultiSitesSelector
orgId={orgId}
selectedSites={selectedSites}
filterTypes={["newt"]}
onSelectionChange={(sites) => {
onSelectedSitesChange(sites);
field.onChange(
sites.map((s) => s.siteId)
);
}}
/>
</PopoverContent>
</Popover>
)}
<FormMessage />
{!singleSite && selectedSites.length > 1 ? (
<PrivateResourceMultiSiteRoutingHelp />
) : null}
</FormItem>
)}
/>
);
}
+333
View File
@@ -0,0 +1,333 @@
"use client";
import {
SettingsFormCell,
SettingsFormGrid,
SettingsSubsectionDescription,
SettingsSubsectionHeader,
SettingsSubsectionTitle
} from "@app/components/Settings";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import { SshServerSettingsFields } from "@app/components/SshServerSettingsFields";
import { PrivateResourceAliasField } from "@app/components/PrivateResourceDestinationFields";
import { PrivateResourceSitesField } from "@app/components/PrivateResourceSitesField";
import { inferSshPamMode } from "@app/lib/privateResourceForm";
import { getSshUseMultiSiteTargetForm } from "@app/lib/privateResourceUtils";
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage
} from "@app/components/ui/form";
import { Input } from "@app/components/ui/input";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { useTranslations } from "next-intl";
import { useState, type ReactNode } from "react";
import type { Control, UseFormSetValue, UseFormWatch } from "react-hook-form";
import type { Selectedsite } from "@app/components/site-selector";
type PrivateResourceSshFieldsProps = {
control: Control<any>;
setValue: UseFormSetValue<any>;
watch: UseFormWatch<any>;
orgId?: string;
disabled?: boolean;
selectedSites: Selectedsite[];
onSelectedSitesChange: (sites: Selectedsite[]) => void;
labelPrefix?: "create" | "edit";
showSshSettings?: boolean;
layout?: "default" | "wizard";
showPaidFeaturesAlert?: boolean;
hideAlias?: boolean;
embedInParentGrid?: boolean;
isNativeSsh?: boolean;
};
export function PrivateResourceSshFields({
control,
setValue,
watch,
orgId,
disabled = false,
selectedSites,
onSelectedSitesChange,
labelPrefix = "edit",
showSshSettings = true,
layout = "default",
showPaidFeaturesAlert = true,
hideAlias = false,
embedInParentGrid = false,
isNativeSsh: isNativeSshProp
}: PrivateResourceSshFieldsProps) {
const t = useTranslations();
const destinationLabelKey =
labelPrefix === "create"
? "createInternalResourceDialogDestination"
: "editInternalResourceDialogDestination";
const destinationPortLabelKey =
labelPrefix === "create"
? "createInternalResourceDialogModePort"
: "editInternalResourceDialogModePort";
const authDaemonMode = watch("authDaemonMode") ?? "site";
const pamMode = inferSshPamMode(authDaemonMode, watch("pamMode"));
const standardDaemonLocation =
watch("standardDaemonLocation") ??
(authDaemonMode === "remote" ? "remote" : "site");
const formAuthDaemonPort = watch("authDaemonPort");
const [authDaemonPortInput, setAuthDaemonPortInput] = useState(() =>
formAuthDaemonPort != null ? String(formAuthDaemonPort) : "22123"
);
const isEditLayout = layout === "default";
const [sshServerMode, setSshServerMode] = useState<"standard" | "native">(
() => (authDaemonMode === "native" ? "native" : "standard")
);
const isNative =
isNativeSshProp ??
(isEditLayout
? authDaemonMode === "native"
: sshServerMode === "native");
const useMultiSiteTargetForm = getSshUseMultiSiteTargetForm(
isNative,
authDaemonMode,
pamMode
);
function trimSitesToFirst() {
if (selectedSites.length <= 1) return;
const first = selectedSites.slice(0, 1);
onSelectedSitesChange(first);
setValue(
"siteIds",
first.map((s: Selectedsite) => s.siteId),
{ shouldValidate: true }
);
}
function handlePamModeChange(value: "passthrough" | "push") {
if (disabled) return;
setValue("pamMode", value, { shouldValidate: true });
if (value === "passthrough") {
setValue("authDaemonPort", null, { shouldValidate: true });
setAuthDaemonPortInput("22123");
return;
}
if (standardDaemonLocation !== "remote" && selectedSites.length > 1) {
trimSitesToFirst();
}
}
function handleDaemonLocationChange(value: "site" | "remote") {
if (disabled) return;
setValue("standardDaemonLocation", value, { shouldValidate: true });
setValue("authDaemonMode", value, { shouldValidate: true });
if (value === "site") {
setValue("authDaemonPort", null, { shouldValidate: true });
setAuthDaemonPortInput("22123");
trimSitesToFirst();
}
}
function handleAuthDaemonPortChange(value: string) {
if (disabled) return;
setAuthDaemonPortInput(value);
const trimmed = value.trim();
setValue("authDaemonPort", trimmed ? Number(trimmed) : null, {
shouldValidate: true
});
}
function handleServerModeChange(mode: "standard" | "native") {
if (disabled) return;
setSshServerMode(mode);
if (mode === "native") {
setValue("authDaemonMode", "native", { shouldValidate: true });
setValue("authDaemonPort", null, { shouldValidate: true });
setValue("destination", null, { shouldValidate: true });
setValue("destinationPort", null, { shouldValidate: true });
setAuthDaemonPortInput("22123");
trimSitesToFirst();
return;
}
setValue("authDaemonMode", standardDaemonLocation, {
shouldValidate: true
});
setValue("destinationPort", 22, { shouldValidate: true });
}
const aliasField = hideAlias ? null : (
<PrivateResourceAliasField
control={control}
watch={watch}
labelPrefix={labelPrefix}
disabled={disabled}
/>
);
const standardSshTargetRow =
orgId && !isNative ? (
<div className="grid grid-cols-3 gap-4 items-start">
<PrivateResourceSitesField
control={control}
orgId={orgId}
selectedSites={selectedSites}
onSelectedSitesChange={onSelectedSitesChange}
singleSite={!useMultiSiteTargetForm}
/>
<FormField
control={control}
name="destination"
render={({ field }) => (
<FormItem>
<FormLabel>{t(destinationLabelKey)}</FormLabel>
<FormControl>
<Input
{...field}
className="w-full"
value={field.value ?? ""}
disabled={disabled}
onChange={(e) =>
field.onChange(
e.target.value === ""
? null
: e.target.value
)
}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="destinationPort"
render={({ field }) => (
<FormItem>
<FormLabel>{t(destinationPortLabelKey)}</FormLabel>
<FormControl>
<Input
className="w-full"
type="number"
min={1}
max={65535}
value={field.value ?? ""}
disabled={disabled}
onChange={(e) => {
const raw = e.target.value;
if (raw === "") {
field.onChange(null);
return;
}
const n = Number(raw);
field.onChange(
Number.isFinite(n) ? n : null
);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
) : null;
const sshSettingsFields = showSshSettings ? (
<SshServerSettingsFields
idPrefix={
layout === "wizard"
? "private-ssh-create"
: "private-ssh-fields"
}
pamMode={pamMode}
standardDaemonLocation={standardDaemonLocation}
authDaemonPort={authDaemonPortInput}
onPamModeChange={handlePamModeChange}
onStandardDaemonLocationChange={handleDaemonLocationChange}
onAuthDaemonPortChange={handleAuthDaemonPortChange}
sshServerMode={sshServerMode}
serverModeDisplay={layout === "wizard" ? "select" : "badge"}
onServerModeChange={handleServerModeChange}
/>
) : null;
const destinationSection = (
<>
<SettingsFormCell span="full">
<SettingsSubsectionHeader>
<SettingsSubsectionTitle>
{t("sshServerDestination")}
</SettingsSubsectionTitle>
<SettingsSubsectionDescription>
{t("sshServerDestinationDescription")}
</SettingsSubsectionDescription>
</SettingsSubsectionHeader>
</SettingsFormCell>
{isNative && orgId ? (
<>
<SettingsFormCell span="half">
<PrivateResourceSitesField
control={control}
orgId={orgId}
selectedSites={selectedSites}
onSelectedSitesChange={onSelectedSitesChange}
singleSite
/>
</SettingsFormCell>
<SettingsFormCell span="half">
<PrivateResourceAliasField
control={control}
watch={watch}
labelPrefix={labelPrefix}
disabled={disabled}
/>
</SettingsFormCell>
</>
) : null}
{!isNative && orgId ? (
<SettingsFormCell span="full">
{standardSshTargetRow}
</SettingsFormCell>
) : null}
{!isNative && !hideAlias ? (
<SettingsFormCell span="half">{aliasField}</SettingsFormCell>
) : null}
</>
);
const content: ReactNode = (
<>
{showPaidFeaturesAlert && layout === "default" && (
<SettingsFormCell span="full">
<PaidFeaturesAlert
tiers={tierMatrix.advancedPrivateResources}
/>
</SettingsFormCell>
)}
{sshSettingsFields}
{destinationSection}
</>
);
if (embedInParentGrid) {
return content;
}
return <SettingsFormGrid>{content}</SettingsFormGrid>;
}