mirror of
https://github.com/fosrl/pangolin.git
synced 2026-06-11 01:53:58 +00:00
make form grids more consistent
This commit is contained in:
@@ -10,6 +10,8 @@ import { formatAxiosError } from "@app/lib/api";
|
||||
import { AxiosResponse } from "axios";
|
||||
import {
|
||||
SettingsContainer,
|
||||
SettingsFormCell,
|
||||
SettingsFormGrid,
|
||||
SettingsSection,
|
||||
SettingsSectionHeader,
|
||||
SettingsSectionTitle,
|
||||
@@ -1324,42 +1326,44 @@ export default function BillingPage() {
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<div className="w-full md:w-1/2">
|
||||
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4 border rounded-lg p-4">
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground mb-1">
|
||||
{t("billingCurrentKeys") ||
|
||||
"Current Keys"}
|
||||
</div>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="text-3xl font-semibold">
|
||||
{getLicenseKeyCount()}
|
||||
</span>
|
||||
<span className="text-lg">
|
||||
{getLicenseKeyCount() === 1
|
||||
? "key"
|
||||
: "keys"}
|
||||
</span>
|
||||
<SettingsFormGrid>
|
||||
<SettingsFormCell span="half">
|
||||
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4 border rounded-lg p-4">
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground mb-1">
|
||||
{t("billingCurrentKeys") ||
|
||||
"Current Keys"}
|
||||
</div>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="text-3xl font-semibold">
|
||||
{getLicenseKeyCount()}
|
||||
</span>
|
||||
<span className="text-lg">
|
||||
{getLicenseKeyCount() === 1
|
||||
? "key"
|
||||
: "keys"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleModifySubscription}
|
||||
disabled={isLoading}
|
||||
loading={isLoading}
|
||||
>
|
||||
<CreditCard className="mr-2 h-4 w-4" />
|
||||
{t("billingModifyCurrentPlan") ||
|
||||
"Modify Current Plan"}
|
||||
</Button>
|
||||
<p className="text-sm text-muted-foreground mt-2">
|
||||
{t(
|
||||
"billingManageLicenseSubscriptionDescription"
|
||||
) ||
|
||||
"Manage your subscription for paid self-hosted license keys and download invoices."}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleModifySubscription}
|
||||
disabled={isLoading}
|
||||
loading={isLoading}
|
||||
>
|
||||
<CreditCard className="mr-2 h-4 w-4" />
|
||||
{t("billingModifyCurrentPlan") ||
|
||||
"Modify Current Plan"}
|
||||
</Button>
|
||||
<p className="text-sm text-muted-foreground mt-2">
|
||||
{t(
|
||||
"billingManageLicenseSubscriptionDescription"
|
||||
) ||
|
||||
"Manage your subscription for paid self-hosted license keys and download invoices."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsFormCell>
|
||||
</SettingsFormGrid>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
)}
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
} from "@app/components/InfoSection";
|
||||
import {
|
||||
SettingsContainer,
|
||||
SettingsFormCell,
|
||||
SettingsFormGrid,
|
||||
SettingsSection,
|
||||
SettingsSectionBody,
|
||||
SettingsSectionDescription,
|
||||
@@ -257,80 +259,87 @@ export default function Page() {
|
||||
e.preventDefault(); // block default enter refresh
|
||||
}
|
||||
}}
|
||||
className="space-y-4 grid gap-4 grid-cols-1 md:grid-cols-2 items-start"
|
||||
id="create-client-form"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("name")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
autoComplete="off"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
{t(
|
||||
"clientNameDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center justify-end md:col-start-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
setShowAdvancedSettings(
|
||||
!showAdvancedSettings
|
||||
)
|
||||
}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
{showAdvancedSettings ? (
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
)}
|
||||
{t("advancedSettings")}
|
||||
</Button>
|
||||
</div>
|
||||
{showAdvancedSettings && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="subnet"
|
||||
render={({ field }) => (
|
||||
<FormItem className="md:col-start-1 md:col-span-2">
|
||||
<FormLabel>
|
||||
{t("clientAddress")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
autoComplete="off"
|
||||
placeholder={t(
|
||||
"subnetPlaceholder"
|
||||
<SettingsFormGrid>
|
||||
<SettingsFormCell span="half">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("name")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
autoComplete="off"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
{t(
|
||||
"clientNameDescription"
|
||||
)}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
{t(
|
||||
"addressDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</SettingsFormCell>
|
||||
<SettingsFormCell className="flex items-center justify-end md:col-span-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
setShowAdvancedSettings(
|
||||
!showAdvancedSettings
|
||||
)
|
||||
}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
{showAdvancedSettings ? (
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
)}
|
||||
{t("advancedSettings")}
|
||||
</Button>
|
||||
</SettingsFormCell>
|
||||
{showAdvancedSettings && (
|
||||
<SettingsFormCell span="full">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="subnet"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"clientAddress"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
autoComplete="off"
|
||||
placeholder={t(
|
||||
"subnetPlaceholder"
|
||||
)}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
{t(
|
||||
"addressDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</SettingsFormCell>
|
||||
)}
|
||||
</SettingsFormGrid>
|
||||
</form>
|
||||
</Form>
|
||||
</SettingsSectionBody>
|
||||
|
||||
@@ -20,6 +20,8 @@ import {
|
||||
SettingsSectionBody,
|
||||
SettingsSectionDescription,
|
||||
SettingsSectionFooter,
|
||||
SettingsFormCell,
|
||||
SettingsFormGrid,
|
||||
SettingsSectionForm,
|
||||
SettingsSectionHeader,
|
||||
SettingsSectionTitle
|
||||
@@ -616,207 +618,225 @@ export default function GeneralForm() {
|
||||
<Form {...form}>
|
||||
<form
|
||||
action={formAction}
|
||||
className="space-y-4"
|
||||
id="general-settings-form"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="enabled"
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<SwitchInput
|
||||
id="enable-resource"
|
||||
defaultChecked={
|
||||
resource.enabled
|
||||
}
|
||||
label={t(
|
||||
"resourceEnable"
|
||||
)}
|
||||
onCheckedChange={(
|
||||
val
|
||||
) =>
|
||||
form.setValue(
|
||||
"enabled",
|
||||
val
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"disabledResourceDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("name")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="niceId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("identifier")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder={t(
|
||||
"enterIdentifier"
|
||||
)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!["http", "ssh", "rdp", "vnc"].includes(
|
||||
resource.mode
|
||||
) && (
|
||||
<>
|
||||
<SettingsFormGrid>
|
||||
<SettingsFormCell span="full">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="proxyPort"
|
||||
render={({ field }) => (
|
||||
name="enabled"
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"resourcePortNumber"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
value={
|
||||
field.value !==
|
||||
undefined
|
||||
? String(
|
||||
field.value
|
||||
)
|
||||
: ""
|
||||
<SwitchInput
|
||||
id="enable-resource"
|
||||
defaultChecked={
|
||||
resource.enabled
|
||||
}
|
||||
onChange={(e) =>
|
||||
field.onChange(
|
||||
e.target
|
||||
.value
|
||||
? parseInt(
|
||||
e
|
||||
.target
|
||||
.value
|
||||
)
|
||||
: undefined
|
||||
label={t(
|
||||
"resourceEnable"
|
||||
)}
|
||||
onCheckedChange={(
|
||||
val
|
||||
) =>
|
||||
form.setValue(
|
||||
"enabled",
|
||||
val
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
{t(
|
||||
"resourcePortNumberDescription"
|
||||
"disabledResourceDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</SettingsFormCell>
|
||||
|
||||
{["http", "ssh", "rdp", "vnc"].includes(
|
||||
resource.mode
|
||||
) && (
|
||||
<div className="space-y-4">
|
||||
<div id="resource-domain-picker">
|
||||
<DomainPicker
|
||||
allowWildcard={true}
|
||||
key={resource.resourceId}
|
||||
orgId={orgId as string}
|
||||
cols={2}
|
||||
defaultSubdomain={
|
||||
form.watch(
|
||||
"subdomain"
|
||||
) ?? undefined
|
||||
}
|
||||
defaultDomainId={
|
||||
form.watch(
|
||||
"domainId"
|
||||
) ?? undefined
|
||||
}
|
||||
defaultFullDomain={
|
||||
resourceFullDomainName ||
|
||||
undefined
|
||||
}
|
||||
onDomainChange={(res) => {
|
||||
if (res === null) {
|
||||
<SettingsFormCell span="half">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("name")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</SettingsFormCell>
|
||||
|
||||
<SettingsFormCell span="half">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="niceId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("identifier")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder={t(
|
||||
"enterIdentifier"
|
||||
)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</SettingsFormCell>
|
||||
|
||||
{!["http", "ssh", "rdp", "vnc"].includes(
|
||||
resource.mode
|
||||
) && (
|
||||
<SettingsFormCell span="half">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="proxyPort"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"resourcePortNumber"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
value={
|
||||
field.value !==
|
||||
undefined
|
||||
? String(
|
||||
field.value
|
||||
)
|
||||
: ""
|
||||
}
|
||||
onChange={(
|
||||
e
|
||||
) =>
|
||||
field.onChange(
|
||||
e
|
||||
.target
|
||||
.value
|
||||
? parseInt(
|
||||
e
|
||||
.target
|
||||
.value
|
||||
)
|
||||
: undefined
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
{t(
|
||||
"resourcePortNumberDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</SettingsFormCell>
|
||||
)}
|
||||
|
||||
{["http", "ssh", "rdp", "vnc"].includes(
|
||||
resource.mode
|
||||
) && (
|
||||
<SettingsFormCell span="full">
|
||||
<div id="resource-domain-picker">
|
||||
<DomainPicker
|
||||
allowWildcard={true}
|
||||
key={
|
||||
resource.resourceId
|
||||
}
|
||||
orgId={orgId as string}
|
||||
cols={2}
|
||||
defaultSubdomain={
|
||||
form.watch(
|
||||
"subdomain"
|
||||
) ?? undefined
|
||||
}
|
||||
defaultDomainId={
|
||||
form.watch(
|
||||
"domainId"
|
||||
) ?? undefined
|
||||
}
|
||||
defaultFullDomain={
|
||||
resourceFullDomainName ||
|
||||
undefined
|
||||
}
|
||||
onDomainChange={(
|
||||
res
|
||||
) => {
|
||||
if (res === null) {
|
||||
form.setValue(
|
||||
"domainId",
|
||||
undefined
|
||||
);
|
||||
form.setValue(
|
||||
"subdomain",
|
||||
undefined
|
||||
);
|
||||
setResourceFullDomain(
|
||||
`${resource.ssl ? "https" : "http"}://`
|
||||
);
|
||||
return;
|
||||
}
|
||||
form.setValue(
|
||||
"domainId",
|
||||
undefined
|
||||
res.domainId
|
||||
);
|
||||
form.setValue(
|
||||
"subdomain",
|
||||
undefined
|
||||
res.subdomain ??
|
||||
undefined
|
||||
);
|
||||
setResourceFullDomain(
|
||||
`${resource.ssl ? "https" : "http"}://`
|
||||
`${resource.ssl ? "https" : "http"}://${toUnicode(res.fullDomain)}`
|
||||
);
|
||||
return;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</SettingsFormCell>
|
||||
)}
|
||||
{showResourcePolicy && (
|
||||
<SettingsFormCell span="half">
|
||||
<div className="space-y-2">
|
||||
<FormLabel>
|
||||
{t("sharedPolicy")}
|
||||
</FormLabel>
|
||||
<SharedPolicySelect
|
||||
key={
|
||||
resource.resourcePolicyId ??
|
||||
"none"
|
||||
}
|
||||
form.setValue(
|
||||
"domainId",
|
||||
res.domainId
|
||||
);
|
||||
form.setValue(
|
||||
"subdomain",
|
||||
res.subdomain ??
|
||||
undefined
|
||||
);
|
||||
setResourceFullDomain(
|
||||
`${resource.ssl ? "https" : "http"}://${toUnicode(res.fullDomain)}`
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{showResourcePolicy && (
|
||||
<div className="space-y-2">
|
||||
<FormLabel>
|
||||
{t("sharedPolicy")}
|
||||
</FormLabel>
|
||||
<SharedPolicySelect
|
||||
key={
|
||||
resource.resourcePolicyId ??
|
||||
"none"
|
||||
}
|
||||
orgId={org.org.orgId}
|
||||
value={selectedSharedPolicyId}
|
||||
onChange={
|
||||
setSelectedSharedPolicyId
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
orgId={org.org.orgId}
|
||||
value={
|
||||
selectedSharedPolicyId
|
||||
}
|
||||
onChange={
|
||||
setSelectedSharedPolicyId
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</SettingsFormCell>
|
||||
)}
|
||||
</SettingsFormGrid>
|
||||
</form>
|
||||
</Form>
|
||||
</SettingsSectionForm>
|
||||
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
SettingsSection,
|
||||
SettingsSectionBody,
|
||||
SettingsSectionDescription,
|
||||
SettingsFormCell,
|
||||
SettingsFormGrid,
|
||||
SettingsSectionForm,
|
||||
SettingsSectionHeader,
|
||||
SettingsSectionTitle,
|
||||
@@ -410,174 +412,199 @@ function SshServerForm({
|
||||
<Form {...form}>
|
||||
<SettingsSectionBody>
|
||||
<SettingsSectionForm variant="half">
|
||||
<div className="space-y-2">
|
||||
<p className="font-semibold text-sm">{t("sshServerMode")}</p>
|
||||
<Badge variant="secondary">
|
||||
{sshServerMode == "standard"
|
||||
? t("sshServerModeStandard")
|
||||
: t("sshServerModePangolin")}
|
||||
</Badge>
|
||||
</div>
|
||||
<SettingsFormGrid>
|
||||
<SettingsFormCell span="full">
|
||||
<div className="space-y-2">
|
||||
<p className="font-semibold text-sm">
|
||||
{t("sshServerMode")}
|
||||
</p>
|
||||
<Badge variant="secondary">
|
||||
{sshServerMode == "standard"
|
||||
? t("sshServerModeStandard")
|
||||
: t("sshServerModePangolin")}
|
||||
</Badge>
|
||||
</div>
|
||||
</SettingsFormCell>
|
||||
|
||||
<div className="space-y-2">
|
||||
<p className="font-semibold text-sm">{t("sshAuthenticationMethod")}</p>
|
||||
<StrategySelect<"passthrough" | "push">
|
||||
value={pamMode}
|
||||
options={authMethodOptions}
|
||||
onChange={(value) =>
|
||||
form.setValue("pamMode", value, {
|
||||
shouldValidate: true
|
||||
})
|
||||
}
|
||||
cols={2}
|
||||
/>
|
||||
</div>
|
||||
<SettingsFormCell span="full">
|
||||
<div className="space-y-2">
|
||||
<p className="font-semibold text-sm">
|
||||
{t("sshAuthenticationMethod")}
|
||||
</p>
|
||||
<StrategySelect<"passthrough" | "push">
|
||||
value={pamMode}
|
||||
options={authMethodOptions}
|
||||
onChange={(value) =>
|
||||
form.setValue("pamMode", value, {
|
||||
shouldValidate: true
|
||||
})
|
||||
}
|
||||
cols={2}
|
||||
/>
|
||||
</div>
|
||||
</SettingsFormCell>
|
||||
|
||||
{showDaemonLocation && (
|
||||
<div className="space-y-2">
|
||||
<p className="font-semibold text-sm">{t("sshAuthDaemonLocation")}</p>
|
||||
<StrategySelect<"site" | "remote">
|
||||
value={standardDaemonLocation}
|
||||
options={daemonLocationOptions}
|
||||
onChange={(value) =>
|
||||
form.setValue(
|
||||
"standardDaemonLocation",
|
||||
value,
|
||||
{ shouldValidate: true }
|
||||
)
|
||||
}
|
||||
cols={2}
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("sshDaemonDisclaimer")}{" "}
|
||||
<a
|
||||
href="https://docs.pangolin.net/manage/resources/public/ssh"
|
||||
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>
|
||||
)}
|
||||
|
||||
{showDaemonPort && (
|
||||
<div className="w-full md:w-1/2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="authDaemonPort"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("sshDaemonPort")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={65535}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
<SettingsSubsectionHeader>
|
||||
<SettingsSubsectionTitle>
|
||||
{t("sshServerDestination")}
|
||||
</SettingsSubsectionTitle>
|
||||
<SettingsSubsectionDescription>
|
||||
{t("sshServerDestinationDescription")}
|
||||
</SettingsSubsectionDescription>
|
||||
</SettingsSubsectionHeader>
|
||||
{isNative ? (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="selectedNativeSite"
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<Popover
|
||||
open={nativeSiteOpen}
|
||||
onOpenChange={
|
||||
setNativeSiteOpen
|
||||
}
|
||||
{showDaemonLocation && (
|
||||
<SettingsFormCell span="full">
|
||||
<div className="space-y-2">
|
||||
<p className="font-semibold text-sm">
|
||||
{t("sshAuthDaemonLocation")}
|
||||
</p>
|
||||
<StrategySelect<"site" | "remote">
|
||||
value={standardDaemonLocation}
|
||||
options={daemonLocationOptions}
|
||||
onChange={(value) =>
|
||||
form.setValue(
|
||||
"standardDaemonLocation",
|
||||
value,
|
||||
{
|
||||
shouldValidate: true
|
||||
}
|
||||
)
|
||||
}
|
||||
cols={2}
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("sshDaemonDisclaimer")}{" "}
|
||||
<a
|
||||
href="https://docs.pangolin.net/manage/resources/public/ssh"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline inline-flex items-center gap-1"
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className="w-full max-w-xs justify-between font-normal"
|
||||
>
|
||||
<span className="truncate">
|
||||
{selectedNativeSite?.name ??
|
||||
t(
|
||||
"siteSelect"
|
||||
)}
|
||||
</span>
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0">
|
||||
<SitesSelector
|
||||
orgId={orgId}
|
||||
selectedSite={
|
||||
selectedNativeSite
|
||||
}
|
||||
onSelectSite={(
|
||||
site
|
||||
) => {
|
||||
form.setValue(
|
||||
"selectedNativeSite",
|
||||
site,
|
||||
{
|
||||
shouldValidate:
|
||||
true
|
||||
}
|
||||
);
|
||||
setNativeSiteOpen(
|
||||
false
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
) : useMultiSiteTargetForm ? (
|
||||
<BrowserGatewayTargetForm
|
||||
control={form.control}
|
||||
orgId={orgId}
|
||||
multiSite={true}
|
||||
sitesField="selectedSites"
|
||||
destinationField="destination"
|
||||
destinationPortField="destinationPort"
|
||||
learnMoreHref="https://docs.pangolin.net/manage/resources/public/ssh"
|
||||
defaultPort={22}
|
||||
/>
|
||||
) : (
|
||||
<BrowserGatewayTargetForm
|
||||
control={form.control}
|
||||
orgId={orgId}
|
||||
multiSite={false}
|
||||
siteField="selectedSite"
|
||||
destinationField="destination"
|
||||
destinationPortField="destinationPort"
|
||||
learnMoreHref="https://docs.pangolin.net/manage/resources/public/ssh"
|
||||
defaultPort={22}
|
||||
/>
|
||||
{t("learnMore")}
|
||||
<ExternalLink className="size-3.5 shrink-0" />
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</SettingsFormCell>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showDaemonPort && (
|
||||
<SettingsFormCell span="half">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="authDaemonPort"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("sshDaemonPort")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={65535}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</SettingsFormCell>
|
||||
)}
|
||||
|
||||
<SettingsFormCell span="full">
|
||||
<SettingsSubsectionHeader>
|
||||
<SettingsSubsectionTitle>
|
||||
{t("sshServerDestination")}
|
||||
</SettingsSubsectionTitle>
|
||||
<SettingsSubsectionDescription>
|
||||
{t(
|
||||
"sshServerDestinationDescription"
|
||||
)}
|
||||
</SettingsSubsectionDescription>
|
||||
</SettingsSubsectionHeader>
|
||||
</SettingsFormCell>
|
||||
|
||||
{isNative ? (
|
||||
<SettingsFormCell span="half">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="selectedNativeSite"
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<Popover
|
||||
open={nativeSiteOpen}
|
||||
onOpenChange={
|
||||
setNativeSiteOpen
|
||||
}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className="w-full justify-between font-normal"
|
||||
>
|
||||
<span className="truncate">
|
||||
{selectedNativeSite?.name ??
|
||||
t(
|
||||
"siteSelect"
|
||||
)}
|
||||
</span>
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0">
|
||||
<SitesSelector
|
||||
orgId={orgId}
|
||||
selectedSite={
|
||||
selectedNativeSite
|
||||
}
|
||||
onSelectSite={(
|
||||
site
|
||||
) => {
|
||||
form.setValue(
|
||||
"selectedNativeSite",
|
||||
site,
|
||||
{
|
||||
shouldValidate:
|
||||
true
|
||||
}
|
||||
);
|
||||
setNativeSiteOpen(
|
||||
false
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</SettingsFormCell>
|
||||
) : useMultiSiteTargetForm ? (
|
||||
<SettingsFormCell span="full">
|
||||
<BrowserGatewayTargetForm
|
||||
control={form.control}
|
||||
orgId={orgId}
|
||||
multiSite={true}
|
||||
sitesField="selectedSites"
|
||||
destinationField="destination"
|
||||
destinationPortField="destinationPort"
|
||||
learnMoreHref="https://docs.pangolin.net/manage/resources/public/ssh"
|
||||
defaultPort={22}
|
||||
/>
|
||||
</SettingsFormCell>
|
||||
) : (
|
||||
<SettingsFormCell span="full">
|
||||
<BrowserGatewayTargetForm
|
||||
control={form.control}
|
||||
orgId={orgId}
|
||||
multiSite={false}
|
||||
siteField="selectedSite"
|
||||
destinationField="destination"
|
||||
destinationPortField="destinationPort"
|
||||
learnMoreHref="https://docs.pangolin.net/manage/resources/public/ssh"
|
||||
defaultPort={22}
|
||||
/>
|
||||
</SettingsFormCell>
|
||||
)}
|
||||
</SettingsFormGrid>
|
||||
</SettingsSectionForm>
|
||||
</SettingsSectionBody>
|
||||
<form action={formAction} className="flex justify-end mt-4">
|
||||
|
||||
@@ -4,6 +4,8 @@ import CopyTextBox from "@app/components/CopyTextBox";
|
||||
import DomainPicker from "@app/components/DomainPicker";
|
||||
import {
|
||||
SettingsContainer,
|
||||
SettingsFormCell,
|
||||
SettingsFormGrid,
|
||||
SettingsSection,
|
||||
SettingsSectionBody,
|
||||
SettingsSectionDescription,
|
||||
@@ -806,172 +808,198 @@ export default function Page() {
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<SettingsSectionForm variant="half">
|
||||
{/* Name */}
|
||||
<Form {...baseForm}>
|
||||
<form
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
className="grid gap-4 grid-cols-1 md:grid-cols-2 items-start"
|
||||
id="base-resource-form"
|
||||
>
|
||||
<FormField
|
||||
control={baseForm.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("name")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
{t(
|
||||
"resourceNameDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
<SettingsFormGrid>
|
||||
<SettingsFormCell span="half">
|
||||
<Form {...baseForm}>
|
||||
<form
|
||||
onKeyDown={(e) => {
|
||||
if (
|
||||
e.key ===
|
||||
"Enter"
|
||||
) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
id="base-resource-form"
|
||||
>
|
||||
<FormField
|
||||
control={
|
||||
baseForm.control
|
||||
}
|
||||
name="name"
|
||||
render={({
|
||||
field
|
||||
}) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"name"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
{t(
|
||||
"resourceNameDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</SettingsFormCell>
|
||||
|
||||
{/* Inline Type Selector */}
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium">
|
||||
{t("type")}
|
||||
</p>
|
||||
<OptionSelect<NewResourceType>
|
||||
options={typeOptions}
|
||||
value={resourceType}
|
||||
onChange={setResourceType}
|
||||
cols={6}
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("resourceTypeDescription")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Domain/Subdomain (HTTP-based types) */}
|
||||
{isHttpResource && (
|
||||
<Form {...httpForm}>
|
||||
<FormField
|
||||
control={httpForm.control}
|
||||
name="domainId"
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<DomainPicker
|
||||
allowWildcard={
|
||||
true
|
||||
}
|
||||
orgId={
|
||||
orgId as string
|
||||
}
|
||||
warnOnProvidedDomain={
|
||||
remoteExitNodes.length >=
|
||||
1
|
||||
}
|
||||
onDomainChange={(
|
||||
res
|
||||
) => {
|
||||
if (!res)
|
||||
return;
|
||||
httpForm.setValue(
|
||||
"subdomain",
|
||||
res.subdomain,
|
||||
{
|
||||
shouldValidate:
|
||||
true
|
||||
}
|
||||
);
|
||||
httpForm.setValue(
|
||||
"domainId",
|
||||
res.domainId,
|
||||
{
|
||||
shouldValidate:
|
||||
true
|
||||
}
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
{t(
|
||||
"resourceDomainDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</Form>
|
||||
)}
|
||||
|
||||
{/* Proxy Port (TCP/UDP types) */}
|
||||
{!isHttpResource && (
|
||||
<Form {...tcpUdpForm}>
|
||||
<form
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
<SettingsFormCell span="full">
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium">
|
||||
{t("type")}
|
||||
</p>
|
||||
<OptionSelect<NewResourceType>
|
||||
options={typeOptions}
|
||||
value={resourceType}
|
||||
onChange={
|
||||
setResourceType
|
||||
}
|
||||
}}
|
||||
className="grid gap-4 grid-cols-1 md:grid-cols-2 items-start"
|
||||
id="tcp-udp-settings-form"
|
||||
>
|
||||
<FormField
|
||||
control={
|
||||
tcpUdpForm.control
|
||||
}
|
||||
name="proxyPort"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"resourcePortNumber"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
value={
|
||||
field.value ??
|
||||
""
|
||||
}
|
||||
onChange={(
|
||||
e
|
||||
) =>
|
||||
field.onChange(
|
||||
e
|
||||
.target
|
||||
.value
|
||||
? parseInt(
|
||||
e
|
||||
.target
|
||||
.value
|
||||
)
|
||||
: undefined
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
{t(
|
||||
"resourcePortDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
cols={6}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
)}
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t(
|
||||
"resourceTypeDescription"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</SettingsFormCell>
|
||||
|
||||
{isHttpResource && (
|
||||
<SettingsFormCell span="full">
|
||||
<Form {...httpForm}>
|
||||
<FormField
|
||||
control={
|
||||
httpForm.control
|
||||
}
|
||||
name="domainId"
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<DomainPicker
|
||||
allowWildcard={
|
||||
true
|
||||
}
|
||||
orgId={
|
||||
orgId as string
|
||||
}
|
||||
warnOnProvidedDomain={
|
||||
remoteExitNodes.length >=
|
||||
1
|
||||
}
|
||||
onDomainChange={(
|
||||
res
|
||||
) => {
|
||||
if (
|
||||
!res
|
||||
)
|
||||
return;
|
||||
httpForm.setValue(
|
||||
"subdomain",
|
||||
res.subdomain,
|
||||
{
|
||||
shouldValidate:
|
||||
true
|
||||
}
|
||||
);
|
||||
httpForm.setValue(
|
||||
"domainId",
|
||||
res.domainId,
|
||||
{
|
||||
shouldValidate:
|
||||
true
|
||||
}
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
{t(
|
||||
"resourceDomainDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</Form>
|
||||
</SettingsFormCell>
|
||||
)}
|
||||
|
||||
{!isHttpResource && (
|
||||
<SettingsFormCell span="half">
|
||||
<Form {...tcpUdpForm}>
|
||||
<form
|
||||
onKeyDown={(e) => {
|
||||
if (
|
||||
e.key ===
|
||||
"Enter"
|
||||
) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
id="tcp-udp-settings-form"
|
||||
>
|
||||
<FormField
|
||||
control={
|
||||
tcpUdpForm.control
|
||||
}
|
||||
name="proxyPort"
|
||||
render={({
|
||||
field
|
||||
}) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"resourcePortNumber"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
value={
|
||||
field.value ??
|
||||
""
|
||||
}
|
||||
onChange={(
|
||||
e
|
||||
) =>
|
||||
field.onChange(
|
||||
e
|
||||
.target
|
||||
.value
|
||||
? parseInt(
|
||||
e
|
||||
.target
|
||||
.value
|
||||
)
|
||||
: undefined
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
{t(
|
||||
"resourcePortDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</SettingsFormCell>
|
||||
)}
|
||||
</SettingsFormGrid>
|
||||
</SettingsSectionForm>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
@@ -1005,202 +1033,237 @@ export default function Page() {
|
||||
>
|
||||
<SettingsSectionBody>
|
||||
<SettingsSectionForm variant="half">
|
||||
{/* Mode */}
|
||||
<div className="space-y-2">
|
||||
<p className="font-semibold text-sm">{t("sshServerMode")}</p>
|
||||
<StrategySelect<
|
||||
"standard" | "native"
|
||||
>
|
||||
value={sshServerMode}
|
||||
options={sshModeOptions}
|
||||
onChange={setSshServerMode}
|
||||
cols={2}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<p className="font-semibold text-sm">{t("sshAuthenticationMethod")}</p>
|
||||
<StrategySelect<
|
||||
"passthrough" | "push"
|
||||
>
|
||||
value={pamMode}
|
||||
options={
|
||||
authMethodOptions
|
||||
}
|
||||
onChange={setPamMode}
|
||||
cols={2}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Daemon Location (standard + push) */}
|
||||
{showDaemonLocation && (
|
||||
<div className="space-y-2">
|
||||
<p className="font-semibold text-sm">{t("sshAuthDaemonLocation")}</p>
|
||||
<StrategySelect<
|
||||
"site" | "remote"
|
||||
>
|
||||
value={
|
||||
standardDaemonLocation
|
||||
}
|
||||
options={
|
||||
daemonLocationOptions
|
||||
}
|
||||
onChange={
|
||||
setStandardDaemonLocation
|
||||
}
|
||||
cols={2}
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t(
|
||||
"sshDaemonDisclaimer"
|
||||
)}{" "}
|
||||
<a
|
||||
href="https://docs.pangolin.net/manage/resources/public/ssh"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline inline-flex items-center gap-1"
|
||||
<SettingsFormGrid>
|
||||
<SettingsFormCell span="full">
|
||||
<div className="space-y-2">
|
||||
<p className="font-semibold text-sm">
|
||||
{t("sshServerMode")}
|
||||
</p>
|
||||
<StrategySelect<
|
||||
"standard" | "native"
|
||||
>
|
||||
{t("learnMore")}
|
||||
<ExternalLink className="size-3.5 shrink-0" />
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Daemon Port (standard + push + remote) */}
|
||||
{showDaemonPort && (
|
||||
<Form {...sshDaemonPortForm}>
|
||||
<div className="w-full md:w-1/2">
|
||||
<FormField
|
||||
control={
|
||||
sshDaemonPortForm.control
|
||||
value={sshServerMode}
|
||||
options={
|
||||
sshModeOptions
|
||||
}
|
||||
name="authDaemonPort"
|
||||
render={({
|
||||
field
|
||||
}) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"sshDaemonPort"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
min={
|
||||
1
|
||||
}
|
||||
max={
|
||||
65535
|
||||
}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
onChange={
|
||||
setSshServerMode
|
||||
}
|
||||
cols={2}
|
||||
/>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</SettingsFormCell>
|
||||
|
||||
{/* Server Destination */}
|
||||
<div className="space-y-3">
|
||||
<SettingsSubsectionHeader>
|
||||
<SettingsSubsectionTitle>
|
||||
{t(
|
||||
"sshServerDestination"
|
||||
)}
|
||||
</SettingsSubsectionTitle>
|
||||
<SettingsSubsectionDescription>
|
||||
{t(
|
||||
"sshServerDestinationDescription"
|
||||
)}
|
||||
</SettingsSubsectionDescription>
|
||||
</SettingsSubsectionHeader>
|
||||
{isNative ? (
|
||||
<Popover
|
||||
open={nativeSiteOpen}
|
||||
onOpenChange={
|
||||
setNativeSiteOpen
|
||||
}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className="w-full md:w-1/2 justify-between font-normal"
|
||||
<SettingsFormCell span="full">
|
||||
<div className="space-y-2">
|
||||
<p className="font-semibold text-sm">
|
||||
{t(
|
||||
"sshAuthenticationMethod"
|
||||
)}
|
||||
</p>
|
||||
<StrategySelect<
|
||||
"passthrough" | "push"
|
||||
>
|
||||
value={pamMode}
|
||||
options={
|
||||
authMethodOptions
|
||||
}
|
||||
onChange={setPamMode}
|
||||
cols={2}
|
||||
/>
|
||||
</div>
|
||||
</SettingsFormCell>
|
||||
|
||||
{showDaemonLocation && (
|
||||
<SettingsFormCell span="full">
|
||||
<div className="space-y-2">
|
||||
<p className="font-semibold text-sm">
|
||||
{t(
|
||||
"sshAuthDaemonLocation"
|
||||
)}
|
||||
</p>
|
||||
<StrategySelect<
|
||||
"site" | "remote"
|
||||
>
|
||||
<span className="truncate">
|
||||
{nativeSelectedSite?.name ??
|
||||
t(
|
||||
"siteSelect"
|
||||
)}
|
||||
</span>
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0">
|
||||
<SitesSelector
|
||||
orgId={
|
||||
orgId as string
|
||||
value={
|
||||
standardDaemonLocation
|
||||
}
|
||||
selectedSite={
|
||||
nativeSelectedSite
|
||||
options={
|
||||
daemonLocationOptions
|
||||
}
|
||||
onSelectSite={(
|
||||
site
|
||||
) => {
|
||||
setNativeSelectedSite(
|
||||
site
|
||||
);
|
||||
setNativeSiteOpen(
|
||||
false
|
||||
);
|
||||
}}
|
||||
onChange={
|
||||
setStandardDaemonLocation
|
||||
}
|
||||
cols={2}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t(
|
||||
"sshDaemonDisclaimer"
|
||||
)}{" "}
|
||||
<a
|
||||
href="https://docs.pangolin.net/manage/resources/public/ssh"
|
||||
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>
|
||||
</SettingsFormCell>
|
||||
)}
|
||||
|
||||
{showDaemonPort && (
|
||||
<SettingsFormCell span="half">
|
||||
<Form
|
||||
{...sshDaemonPortForm}
|
||||
>
|
||||
<FormField
|
||||
control={
|
||||
sshDaemonPortForm.control
|
||||
}
|
||||
name="authDaemonPort"
|
||||
render={({
|
||||
field
|
||||
}) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"sshDaemonPort"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
min={
|
||||
1
|
||||
}
|
||||
max={
|
||||
65535
|
||||
}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</Form>
|
||||
</SettingsFormCell>
|
||||
)}
|
||||
|
||||
<SettingsFormCell span="full">
|
||||
<SettingsSubsectionHeader>
|
||||
<SettingsSubsectionTitle>
|
||||
{t(
|
||||
"sshServerDestination"
|
||||
)}
|
||||
</SettingsSubsectionTitle>
|
||||
<SettingsSubsectionDescription>
|
||||
{t(
|
||||
"sshServerDestinationDescription"
|
||||
)}
|
||||
</SettingsSubsectionDescription>
|
||||
</SettingsSubsectionHeader>
|
||||
</SettingsFormCell>
|
||||
|
||||
{isNative ? (
|
||||
<SettingsFormCell span="half">
|
||||
<Popover
|
||||
open={
|
||||
nativeSiteOpen
|
||||
}
|
||||
onOpenChange={
|
||||
setNativeSiteOpen
|
||||
}
|
||||
>
|
||||
<PopoverTrigger
|
||||
asChild
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className="w-full justify-between font-normal"
|
||||
>
|
||||
<span className="truncate">
|
||||
{nativeSelectedSite?.name ??
|
||||
t(
|
||||
"siteSelect"
|
||||
)}
|
||||
</span>
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0">
|
||||
<SitesSelector
|
||||
orgId={
|
||||
orgId as string
|
||||
}
|
||||
selectedSite={
|
||||
nativeSelectedSite
|
||||
}
|
||||
onSelectSite={(
|
||||
site
|
||||
) => {
|
||||
setNativeSelectedSite(
|
||||
site
|
||||
);
|
||||
setNativeSiteOpen(
|
||||
false
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</SettingsFormCell>
|
||||
) : standardDaemonLocation !==
|
||||
"site" ||
|
||||
pamMode ===
|
||||
"passthrough" ? (
|
||||
<Form {...bgTargetForm}>
|
||||
<BrowserGatewayTargetForm
|
||||
control={
|
||||
bgTargetForm.control
|
||||
}
|
||||
orgId={
|
||||
orgId as string
|
||||
}
|
||||
multiSite={true}
|
||||
sitesField="selectedSites"
|
||||
destinationField="destination"
|
||||
destinationPortField="destinationPort"
|
||||
learnMoreHref="https://docs.pangolin.net/manage/resources/public/ssh"
|
||||
defaultPort={22}
|
||||
/>
|
||||
</Form>
|
||||
<SettingsFormCell span="full">
|
||||
<Form {...bgTargetForm}>
|
||||
<BrowserGatewayTargetForm
|
||||
control={
|
||||
bgTargetForm.control
|
||||
}
|
||||
orgId={
|
||||
orgId as string
|
||||
}
|
||||
multiSite={true}
|
||||
sitesField="selectedSites"
|
||||
destinationField="destination"
|
||||
destinationPortField="destinationPort"
|
||||
learnMoreHref="https://docs.pangolin.net/manage/resources/public/ssh"
|
||||
defaultPort={22}
|
||||
/>
|
||||
</Form>
|
||||
</SettingsFormCell>
|
||||
) : (
|
||||
<Form {...bgTargetForm}>
|
||||
<BrowserGatewayTargetForm
|
||||
control={
|
||||
bgTargetForm.control
|
||||
}
|
||||
orgId={
|
||||
orgId as string
|
||||
}
|
||||
multiSite={false}
|
||||
siteField="selectedSite"
|
||||
destinationField="destination"
|
||||
destinationPortField="destinationPort"
|
||||
learnMoreHref="https://docs.pangolin.net/manage/resources/public/ssh"
|
||||
defaultPort={22}
|
||||
/>
|
||||
</Form>
|
||||
<SettingsFormCell span="full">
|
||||
<Form {...bgTargetForm}>
|
||||
<BrowserGatewayTargetForm
|
||||
control={
|
||||
bgTargetForm.control
|
||||
}
|
||||
orgId={
|
||||
orgId as string
|
||||
}
|
||||
multiSite={
|
||||
false
|
||||
}
|
||||
siteField="selectedSite"
|
||||
destinationField="destination"
|
||||
destinationPortField="destinationPort"
|
||||
learnMoreHref="https://docs.pangolin.net/manage/resources/public/ssh"
|
||||
defaultPort={22}
|
||||
/>
|
||||
</Form>
|
||||
</SettingsFormCell>
|
||||
)}
|
||||
</div>
|
||||
</SettingsFormGrid>
|
||||
</SettingsSectionForm>
|
||||
</SettingsSectionBody>
|
||||
</fieldset>
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
import {
|
||||
SettingsContainer,
|
||||
SettingsFormCell,
|
||||
SettingsFormGrid,
|
||||
SettingsSection,
|
||||
SettingsSectionBody,
|
||||
SettingsSectionDescription,
|
||||
@@ -514,98 +516,107 @@ export default function Page() {
|
||||
e.preventDefault(); // block default enter refresh
|
||||
}
|
||||
}}
|
||||
className="space-y-4 grid gap-4 grid-cols-1 md:grid-cols-2 items-start"
|
||||
id="create-site-form"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("name")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
autoComplete="off"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
{t(
|
||||
"siteNameDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{form.watch("method") === "newt" && (
|
||||
<div className="flex items-center justify-end md:col-start-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
setShowAdvancedSettings(
|
||||
!showAdvancedSettings
|
||||
)
|
||||
}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
{showAdvancedSettings ? (
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
)}
|
||||
{t("advancedSettings")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{form.watch("method") === "newt" &&
|
||||
showAdvancedSettings && (
|
||||
<SettingsFormGrid>
|
||||
<SettingsFormCell span="half">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="clientAddress"
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem className="md:col-start-1 md:col-span-2">
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"siteAddress"
|
||||
)}
|
||||
{t("name")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
autoComplete="off"
|
||||
value={
|
||||
clientAddress
|
||||
}
|
||||
onChange={(
|
||||
e
|
||||
) => {
|
||||
setClientAddress(
|
||||
e
|
||||
.target
|
||||
.value
|
||||
);
|
||||
field.onChange(
|
||||
e
|
||||
.target
|
||||
.value
|
||||
);
|
||||
}}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
{t(
|
||||
"siteAddressDescription"
|
||||
"siteNameDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{form.watch("method") ===
|
||||
"newt" && (
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
setShowAdvancedSettings(
|
||||
!showAdvancedSettings
|
||||
)
|
||||
}
|
||||
className="mt-2 flex items-center gap-2 -ml-3"
|
||||
>
|
||||
{showAdvancedSettings ? (
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
)}
|
||||
{t(
|
||||
"advancedSettings"
|
||||
)}
|
||||
</Button>
|
||||
{showAdvancedSettings && (
|
||||
<FormField
|
||||
control={
|
||||
form.control
|
||||
}
|
||||
name="clientAddress"
|
||||
render={({
|
||||
field
|
||||
}) => (
|
||||
<FormItem className="mt-4">
|
||||
<FormLabel>
|
||||
{t(
|
||||
"siteAddress"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
autoComplete="off"
|
||||
value={
|
||||
clientAddress
|
||||
}
|
||||
onChange={(
|
||||
e
|
||||
) => {
|
||||
setClientAddress(
|
||||
e
|
||||
.target
|
||||
.value
|
||||
);
|
||||
field.onChange(
|
||||
e
|
||||
.target
|
||||
.value
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
{t(
|
||||
"siteAddressDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</SettingsFormCell>
|
||||
</SettingsFormGrid>
|
||||
</form>
|
||||
</Form>
|
||||
</SettingsSectionBody>
|
||||
|
||||
@@ -43,6 +43,49 @@ export function SettingsSectionForm({
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingsFormGrid({
|
||||
children,
|
||||
className
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"grid grid-cols-1 md:grid-cols-4 gap-4 items-start",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingsFormCell({
|
||||
children,
|
||||
span = "half",
|
||||
className
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
span?: "quarter" | "half" | "full";
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"min-w-0",
|
||||
span === "quarter" && "md:col-span-1",
|
||||
span === "half" && "md:col-span-2",
|
||||
span === "full" && "md:col-span-4",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingsSectionTitle({
|
||||
children
|
||||
}: {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
SettingsFormCell,
|
||||
SettingsFormGrid,
|
||||
SettingsSection,
|
||||
SettingsSectionBody,
|
||||
SettingsSectionDescription,
|
||||
@@ -111,65 +113,67 @@ export function PolicyAuthStackSectionCreate({
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<div className="w-full md:w-1/2">
|
||||
<PolicyAuthSsoSection
|
||||
sso={Boolean(sso)}
|
||||
onSsoChange={(active) =>
|
||||
parentForm.setValue("sso", active)
|
||||
}
|
||||
skipToIdpId={skipToIdpId}
|
||||
onSkipToIdpChange={(id) =>
|
||||
parentForm.setValue("skipToIdpId", id)
|
||||
}
|
||||
allIdps={allIdps}
|
||||
rolesEditor={
|
||||
<FormField<PolicyFormValues, "roles">
|
||||
control={parentForm.control}
|
||||
name="roles"
|
||||
render={({ field }) => (
|
||||
<TagInput
|
||||
{...field}
|
||||
activeTagIndex={activeRolesTagIndex}
|
||||
setActiveTagIndex={
|
||||
setActiveRolesTagIndex
|
||||
}
|
||||
placeholder={t("accessRoleSelect2")}
|
||||
tags={field.value ?? []}
|
||||
setTags={(newRoles) =>
|
||||
field.onChange(newRoles)
|
||||
}
|
||||
autocompleteOptions={allRoles}
|
||||
allowDuplicates={false}
|
||||
size="sm"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
usersEditor={
|
||||
<FormField<PolicyFormValues, "users">
|
||||
control={parentForm.control}
|
||||
name="users"
|
||||
render={({ field }) => (
|
||||
<TagInput
|
||||
{...field}
|
||||
activeTagIndex={activeUsersTagIndex}
|
||||
setActiveTagIndex={
|
||||
setActiveUsersTagIndex
|
||||
}
|
||||
placeholder={t("accessUserSelect")}
|
||||
tags={field.value ?? []}
|
||||
setTags={(newUsers) =>
|
||||
field.onChange(newUsers)
|
||||
}
|
||||
autocompleteOptions={allUsers}
|
||||
allowDuplicates={false}
|
||||
size="sm"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<SettingsFormGrid>
|
||||
<SettingsFormCell span="half">
|
||||
<PolicyAuthSsoSection
|
||||
sso={Boolean(sso)}
|
||||
onSsoChange={(active) =>
|
||||
parentForm.setValue("sso", active)
|
||||
}
|
||||
skipToIdpId={skipToIdpId}
|
||||
onSkipToIdpChange={(id) =>
|
||||
parentForm.setValue("skipToIdpId", id)
|
||||
}
|
||||
allIdps={allIdps}
|
||||
rolesEditor={
|
||||
<FormField<PolicyFormValues, "roles">
|
||||
control={parentForm.control}
|
||||
name="roles"
|
||||
render={({ field }) => (
|
||||
<TagInput
|
||||
{...field}
|
||||
activeTagIndex={activeRolesTagIndex}
|
||||
setActiveTagIndex={
|
||||
setActiveRolesTagIndex
|
||||
}
|
||||
placeholder={t("accessRoleSelect2")}
|
||||
tags={field.value ?? []}
|
||||
setTags={(newRoles) =>
|
||||
field.onChange(newRoles)
|
||||
}
|
||||
autocompleteOptions={allRoles}
|
||||
allowDuplicates={false}
|
||||
size="sm"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
usersEditor={
|
||||
<FormField<PolicyFormValues, "users">
|
||||
control={parentForm.control}
|
||||
name="users"
|
||||
render={({ field }) => (
|
||||
<TagInput
|
||||
{...field}
|
||||
activeTagIndex={activeUsersTagIndex}
|
||||
setActiveTagIndex={
|
||||
setActiveUsersTagIndex
|
||||
}
|
||||
placeholder={t("accessUserSelect")}
|
||||
tags={field.value ?? []}
|
||||
setTags={(newUsers) =>
|
||||
field.onChange(newUsers)
|
||||
}
|
||||
autocompleteOptions={allUsers}
|
||||
allowDuplicates={false}
|
||||
size="sm"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SettingsFormCell>
|
||||
</SettingsFormGrid>
|
||||
|
||||
<SettingsSubsectionHeader>
|
||||
<SettingsSubsectionTitle>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
SettingsFormCell,
|
||||
SettingsFormGrid,
|
||||
SettingsSection,
|
||||
SettingsSectionBody,
|
||||
SettingsSectionDescription,
|
||||
@@ -475,101 +477,109 @@ export function PolicyAuthStackSectionEdit({
|
||||
{isResourceOverlay && (
|
||||
<SharedPolicyResourceNotice section="authentication" />
|
||||
)}
|
||||
<div className="w-full md:w-1/2">
|
||||
<PolicyAuthSsoSection
|
||||
sso={Boolean(sso)}
|
||||
onSsoChange={(active) =>
|
||||
form.setValue("sso", active)
|
||||
}
|
||||
skipToIdpId={skipToIdpId}
|
||||
onSkipToIdpChange={(id) =>
|
||||
form.setValue("skipToIdpId", id)
|
||||
}
|
||||
allIdps={allIdps}
|
||||
disabled={authReadonly}
|
||||
idpDisabled={authReadonly}
|
||||
rolesEditor={
|
||||
isResourceOverlay ? (
|
||||
<RolesSelector
|
||||
orgId={orgId}
|
||||
selectedRoles={overlayRoles}
|
||||
onSelectRoles={(selected) =>
|
||||
setCombinedRoles(
|
||||
selected.map(
|
||||
(role) => ({
|
||||
...role,
|
||||
isAdmin:
|
||||
Boolean(
|
||||
role.isAdmin
|
||||
)
|
||||
})
|
||||
<SettingsFormGrid>
|
||||
<SettingsFormCell span="half">
|
||||
<PolicyAuthSsoSection
|
||||
sso={Boolean(sso)}
|
||||
onSsoChange={(active) =>
|
||||
form.setValue("sso", active)
|
||||
}
|
||||
skipToIdpId={skipToIdpId}
|
||||
onSkipToIdpChange={(id) =>
|
||||
form.setValue("skipToIdpId", id)
|
||||
}
|
||||
allIdps={allIdps}
|
||||
disabled={authReadonly}
|
||||
idpDisabled={authReadonly}
|
||||
rolesEditor={
|
||||
isResourceOverlay ? (
|
||||
<RolesSelector
|
||||
orgId={orgId}
|
||||
selectedRoles={overlayRoles}
|
||||
onSelectRoles={(selected) =>
|
||||
setCombinedRoles(
|
||||
selected.map(
|
||||
(role) => ({
|
||||
...role,
|
||||
isAdmin:
|
||||
Boolean(
|
||||
role.isAdmin
|
||||
)
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
disabled={isLoading}
|
||||
restrictAdminRole
|
||||
lockedIds={policyRoleLockedIds}
|
||||
/>
|
||||
) : (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="roles"
|
||||
render={({ field }) => (
|
||||
<RolesSelector
|
||||
orgId={orgId}
|
||||
selectedRoles={
|
||||
field.value
|
||||
}
|
||||
onSelectRoles={(
|
||||
selected
|
||||
) =>
|
||||
form.setValue(
|
||||
"roles",
|
||||
}
|
||||
disabled={isLoading}
|
||||
restrictAdminRole
|
||||
lockedIds={
|
||||
policyRoleLockedIds
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="roles"
|
||||
render={({ field }) => (
|
||||
<RolesSelector
|
||||
orgId={orgId}
|
||||
selectedRoles={
|
||||
field.value
|
||||
}
|
||||
onSelectRoles={(
|
||||
selected
|
||||
)
|
||||
}
|
||||
disabled={readonly}
|
||||
restrictAdminRole
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
usersEditor={
|
||||
isResourceOverlay ? (
|
||||
<UsersSelector
|
||||
orgId={orgId}
|
||||
selectedUsers={overlayUsers}
|
||||
onSelectUsers={setCombinedUsers}
|
||||
disabled={isLoading}
|
||||
lockedIds={policyUserLockedIds}
|
||||
/>
|
||||
) : (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="users"
|
||||
render={({ field }) => (
|
||||
<UsersSelector
|
||||
orgId={orgId}
|
||||
selectedUsers={
|
||||
field.value
|
||||
}
|
||||
onSelectUsers={(
|
||||
selected
|
||||
) =>
|
||||
form.setValue(
|
||||
"users",
|
||||
) =>
|
||||
form.setValue(
|
||||
"roles",
|
||||
selected
|
||||
)
|
||||
}
|
||||
disabled={readonly}
|
||||
restrictAdminRole
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
usersEditor={
|
||||
isResourceOverlay ? (
|
||||
<UsersSelector
|
||||
orgId={orgId}
|
||||
selectedUsers={overlayUsers}
|
||||
onSelectUsers={
|
||||
setCombinedUsers
|
||||
}
|
||||
disabled={isLoading}
|
||||
lockedIds={
|
||||
policyUserLockedIds
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="users"
|
||||
render={({ field }) => (
|
||||
<UsersSelector
|
||||
orgId={orgId}
|
||||
selectedUsers={
|
||||
field.value
|
||||
}
|
||||
onSelectUsers={(
|
||||
selected
|
||||
)
|
||||
}
|
||||
disabled={readonly}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
) =>
|
||||
form.setValue(
|
||||
"users",
|
||||
selected
|
||||
)
|
||||
}
|
||||
disabled={readonly}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</SettingsFormCell>
|
||||
</SettingsFormGrid>
|
||||
|
||||
<SettingsSubsectionHeader>
|
||||
<SettingsSubsectionTitle>
|
||||
|
||||
@@ -183,7 +183,7 @@ export function SharedPolicySelect({
|
||||
role="combobox"
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
"w-full justify-between font-normal md:w-1/2",
|
||||
"w-full justify-between font-normal",
|
||||
value !== null &&
|
||||
!resolvedLabel &&
|
||||
!fetchedPolicy?.name &&
|
||||
|
||||
Reference in New Issue
Block a user