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