form layout improvements

This commit is contained in:
miloschwartz
2026-06-09 15:40:38 -07:00
parent c85a7f6ac5
commit fb6f5b3953
14 changed files with 1041 additions and 897 deletions
+4 -2
View File
@@ -727,7 +727,7 @@
"targetSubmit": "Add Target", "targetSubmit": "Add Target",
"targetNoOne": "This resource doesn't have any targets. Add a target to configure where to send requests to the backend.", "targetNoOne": "This resource doesn't have any targets. Add a target to configure where to send requests to the backend.",
"targetNoOneDescription": "Adding more than one target above will enable load balancing.", "targetNoOneDescription": "Adding more than one target above will enable load balancing.",
"targetsSubmit": "Save Targets", "targetsSubmit": "Save Settings",
"addTarget": "Add Target", "addTarget": "Add Target",
"proxyMultiSiteRoundRobinNodeHelp": "Round robin routing will not work between sites that are not connected to the same node, but failover will work.", "proxyMultiSiteRoundRobinNodeHelp": "Round robin routing will not work between sites that are not connected to the same node, but failover will work.",
"targetErrorInvalidIp": "Invalid IP address", "targetErrorInvalidIp": "Invalid IP address",
@@ -1845,7 +1845,7 @@
"documentation": "Documentation", "documentation": "Documentation",
"saveAllSettings": "Save All Settings", "saveAllSettings": "Save All Settings",
"saveResourceTargets": "Save Targets", "saveResourceTargets": "Save Targets",
"saveResourceHttp": "Save Proxy Settings", "saveResourceHttp": "Save Settings",
"saveProxyProtocol": "Save Proxy protocol settings", "saveProxyProtocol": "Save Proxy protocol settings",
"settingsUpdated": "Settings updated", "settingsUpdated": "Settings updated",
"settingsUpdatedDescription": "Settings updated successfully", "settingsUpdatedDescription": "Settings updated successfully",
@@ -3180,6 +3180,8 @@
"warning:": "Warning:", "warning:": "Warning:",
"forcedeModeWarning": "All traffic will be directed to the maintenance page. Your backend resources will not receive any requests.", "forcedeModeWarning": "All traffic will be directed to the maintenance page. Your backend resources will not receive any requests.",
"pageTitle": "Page Title", "pageTitle": "Page Title",
"maintenancePageContentSubsection": "Page Content",
"maintenancePageContentSubsectionDescription": "Customize the content displayed on the maintenance page",
"pageTitleDescription": "The main heading displayed on the maintenance page", "pageTitleDescription": "The main heading displayed on the maintenance page",
"maintenancePageMessage": "Maintenance Message", "maintenancePageMessage": "Maintenance Message",
"maintenancePageMessagePlaceholder": "We'll be back soon! Our site is currently undergoing scheduled maintenance.", "maintenancePageMessagePlaceholder": "We'll be back soon! Our site is currently undergoing scheduled maintenance.",
@@ -262,7 +262,7 @@ export default function Page() {
id="create-client-form" id="create-client-form"
> >
<SettingsFormGrid> <SettingsFormGrid>
<SettingsFormCell span="half"> <SettingsFormCell span="quarter">
<FormField <FormField
control={form.control} control={form.control}
name="name" name="name"
@@ -287,7 +287,7 @@ export default function Page() {
)} )}
/> />
</SettingsFormCell> </SettingsFormCell>
<SettingsFormCell className="flex items-center justify-end md:col-span-2"> <SettingsFormCell span="full">
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
@@ -297,7 +297,7 @@ export default function Page() {
!showAdvancedSettings !showAdvancedSettings
) )
} }
className="flex items-center gap-2" className="flex items-center gap-2 -ml-3"
> >
{showAdvancedSettings ? ( {showAdvancedSettings ? (
<ChevronUp className="h-4 w-4" /> <ChevronUp className="h-4 w-4" />
@@ -308,7 +308,7 @@ export default function Page() {
</Button> </Button>
</SettingsFormCell> </SettingsFormCell>
{showAdvancedSettings && ( {showAdvancedSettings && (
<SettingsFormCell span="full"> <SettingsFormCell span="quarter">
<FormField <FormField
control={form.control} control={form.control}
name="subnet" name="subnet"
@@ -224,23 +224,39 @@ function LogRetentionSectionForm({ org }: SectionFormProps) {
<SelectContent> <SelectContent>
{LOG_RETENTION_OPTIONS.filter( {LOG_RETENTION_OPTIONS.filter(
(option) => { (option) => {
if (build != "saas") { if (
build != "saas"
) {
return true; return true;
} }
let maxDays: number; let maxDays: number;
if (!subscriptionTier) { if (
!subscriptionTier
) {
// No tier // No tier
maxDays = 3; maxDays = 3;
} else if (subscriptionTier == "enterprise") { } else if (
subscriptionTier ==
"enterprise"
) {
// Enterprise - no limit // Enterprise - no limit
return true; return true;
} else if (subscriptionTier == "tier3") { } else if (
subscriptionTier ==
"tier3"
) {
maxDays = 90; maxDays = 90;
} else if (subscriptionTier == "tier2") { } else if (
subscriptionTier ==
"tier2"
) {
maxDays = 30; maxDays = 30;
} else if (subscriptionTier == "tier1") { } else if (
subscriptionTier ==
"tier1"
) {
maxDays = 7; maxDays = 7;
} else { } else {
// Default to most restrictive // Default to most restrictive
@@ -249,7 +265,12 @@ function LogRetentionSectionForm({ org }: SectionFormProps) {
// Filter out options that exceed the max // Filter out options that exceed the max
// Special values: -1 (forever) and 9001 (end of year) should be filtered // Special values: -1 (forever) and 9001 (end of year) should be filtered
if (option.value < 0 || option.value > maxDays) { if (
option.value <
0 ||
option.value >
maxDays
) {
return false; return false;
} }
@@ -322,24 +343,43 @@ function LogRetentionSectionForm({ org }: SectionFormProps) {
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{LOG_RETENTION_OPTIONS.filter( {LOG_RETENTION_OPTIONS.filter(
(option) => { (
if (build != "saas") { option
) => {
if (
build !=
"saas"
) {
return true; return true;
} }
let maxDays: number; let maxDays: number;
if (!subscriptionTier) { if (
!subscriptionTier
) {
// No tier // No tier
maxDays = 3; maxDays = 3;
} else if (subscriptionTier == "enterprise") { } else if (
subscriptionTier ==
"enterprise"
) {
// Enterprise - no limit // Enterprise - no limit
return true; return true;
} else if (subscriptionTier == "tier3") { } else if (
subscriptionTier ==
"tier3"
) {
maxDays = 90; maxDays = 90;
} else if (subscriptionTier == "tier2") { } else if (
subscriptionTier ==
"tier2"
) {
maxDays = 30; maxDays = 30;
} else if (subscriptionTier == "tier1") { } else if (
subscriptionTier ==
"tier1"
) {
maxDays = 7; maxDays = 7;
} else { } else {
// Default to most restrictive // Default to most restrictive
@@ -348,7 +388,12 @@ function LogRetentionSectionForm({ org }: SectionFormProps) {
// Filter out options that exceed the max // Filter out options that exceed the max
// Special values: -1 (forever) and 9001 (end of year) should be filtered // Special values: -1 (forever) and 9001 (end of year) should be filtered
if (option.value < 0 || option.value > maxDays) { if (
option.value <
0 ||
option.value >
maxDays
) {
return false; return false;
} }
@@ -423,24 +468,43 @@ function LogRetentionSectionForm({ org }: SectionFormProps) {
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{LOG_RETENTION_OPTIONS.filter( {LOG_RETENTION_OPTIONS.filter(
(option) => { (
if (build != "saas") { option
) => {
if (
build !=
"saas"
) {
return true; return true;
} }
let maxDays: number; let maxDays: number;
if (!subscriptionTier) { if (
!subscriptionTier
) {
// No tier // No tier
maxDays = 3; maxDays = 3;
} else if (subscriptionTier == "enterprise") { } else if (
subscriptionTier ==
"enterprise"
) {
// Enterprise - no limit // Enterprise - no limit
return true; return true;
} else if (subscriptionTier == "tier3") { } else if (
subscriptionTier ==
"tier3"
) {
maxDays = 90; maxDays = 90;
} else if (subscriptionTier == "tier2") { } else if (
subscriptionTier ==
"tier2"
) {
maxDays = 30; maxDays = 30;
} else if (subscriptionTier == "tier1") { } else if (
subscriptionTier ==
"tier1"
) {
maxDays = 7; maxDays = 7;
} else { } else {
// Default to most restrictive // Default to most restrictive
@@ -449,7 +513,12 @@ function LogRetentionSectionForm({ org }: SectionFormProps) {
// Filter out options that exceed the max // Filter out options that exceed the max
// Special values: -1 (forever) and 9001 (end of year) should be filtered // Special values: -1 (forever) and 9001 (end of year) should be filtered
if (option.value < 0 || option.value > maxDays) { if (
option.value <
0 ||
option.value >
maxDays
) {
return false; return false;
} }
@@ -524,24 +593,43 @@ function LogRetentionSectionForm({ org }: SectionFormProps) {
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{LOG_RETENTION_OPTIONS.filter( {LOG_RETENTION_OPTIONS.filter(
(option) => { (
if (build != "saas") { option
) => {
if (
build !=
"saas"
) {
return true; return true;
} }
let maxDays: number; let maxDays: number;
if (!subscriptionTier) { if (
!subscriptionTier
) {
// No tier // No tier
maxDays = 3; maxDays = 3;
} else if (subscriptionTier == "enterprise") { } else if (
subscriptionTier ==
"enterprise"
) {
// Enterprise - no limit // Enterprise - no limit
return true; return true;
} else if (subscriptionTier == "tier3") { } else if (
subscriptionTier ==
"tier3"
) {
maxDays = 90; maxDays = 90;
} else if (subscriptionTier == "tier2") { } else if (
subscriptionTier ==
"tier2"
) {
maxDays = 30; maxDays = 30;
} else if (subscriptionTier == "tier1") { } else if (
subscriptionTier ==
"tier1"
) {
maxDays = 7; maxDays = 7;
} else { } else {
// Default to most restrictive // Default to most restrictive
@@ -550,7 +638,12 @@ function LogRetentionSectionForm({ org }: SectionFormProps) {
// Filter out options that exceed the max // Filter out options that exceed the max
// Special values: -1 (forever) and 9001 (end of year) should be filtered // Special values: -1 (forever) and 9001 (end of year) should be filtered
if (option.value < 0 || option.value > maxDays) { if (
option.value <
0 ||
option.value >
maxDays
) {
return false; return false;
} }
@@ -489,7 +489,7 @@ export default function GeneralForm() {
</SettingsSubsectionDescription> </SettingsSubsectionDescription>
</SettingsSubsectionHeader> </SettingsSubsectionHeader>
</SettingsFormCell> </SettingsFormCell>
<SettingsFormCell span="full"> <SettingsFormCell span="half">
<div className="space-y-2"> <div className="space-y-2">
<FormLabel> <FormLabel>
{t("sharedPolicy")} {t("sharedPolicy")}
@@ -1,35 +1,21 @@
"use client"; "use client";
import HealthCheckCredenza from "@/components/HealthCheckCredenza";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { HeadersInput } from "@app/components/HeadersInput"; import { HeadersInput } from "@app/components/HeadersInput";
import {
PathMatchDisplay,
PathMatchModal,
PathRewriteDisplay,
PathRewriteModal
} from "@app/components/PathMatchRenameModal";
import { ResourceTargetAddressItem } from "@app/components/resource-target-address-item";
import { import {
SettingsContainer, SettingsContainer,
SettingsFormCell,
SettingsFormGrid,
SettingsSection, SettingsSection,
SettingsSectionBody, SettingsSectionBody,
SettingsSectionDescription, SettingsSectionDescription,
SettingsSectionFooter,
SettingsSectionForm, SettingsSectionForm,
SettingsSectionHeader, SettingsSectionHeader,
SettingsSectionTitle SettingsSectionTitle
} from "@app/components/Settings"; } from "@app/components/Settings";
import { SwitchInput } from "@app/components/SwitchInput"; import { SwitchInput } from "@app/components/SwitchInput";
import { Alert, AlertDescription } from "@app/components/ui/alert";
import { import {
Form, Form,
FormControl, FormControl,
@@ -43,35 +29,24 @@ import type { ResourceContextType } from "@app/contexts/resourceContext";
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
import { useResourceContext } from "@app/hooks/useResourceContext"; import { useResourceContext } from "@app/hooks/useResourceContext";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
import { createApiClient } from "@app/lib/api"; import { createApiClient, formatAxiosError } from "@app/lib/api";
import { formatAxiosError } from "@app/lib/api/formatAxiosError";
import { resourceQueries } from "@app/lib/queries"; import { resourceQueries } from "@app/lib/queries";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { UpdateResourceResponse } from "@server/routers/resource";
import { tlsNameSchema } from "@server/lib/schemas"; import { tlsNameSchema } from "@server/lib/schemas";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { import {
ProxyResourceTargetsForm ProxyResourceTargetsForm
} from "@app/app/[orgId]/settings/resources/public/ProxyResourceTargetsForm"; } from "@app/app/[orgId]/settings/resources/public/ProxyResourceTargetsForm";
import { import { AxiosResponse } from "axios";
AlertTriangle,
} from "lucide-react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation"; import { useParams, useRouter } from "next/navigation";
import { import { useActionState } from "react";
use,
useActionState,
} from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
const targetsSettingsSchema = z.object({ export default function ReverseProxyTargetsPage() {
stickySession: z.boolean() const params = useParams();
});
export default function ReverseProxyTargetsPage(props: {
params: Promise<{ resourceId: number; orgId: string }>;
}) {
const params = use(props.params);
const { resource, updateResource } = useResourceContext(); const { resource, updateResource } = useResourceContext();
const { data: remoteTargets = [], isLoading: isLoadingTargets } = useQuery( const { data: remoteTargets = [], isLoading: isLoadingTargets } = useQuery(
@@ -87,7 +62,7 @@ export default function ReverseProxyTargetsPage(props: {
return ( return (
<SettingsContainer> <SettingsContainer>
<ProxyResourceTargetsForm <ProxyResourceTargetsForm
orgId={params.orgId} orgId={params.orgId as string}
isHttp={["http", "ssh", "rdp", "vnc"].includes(resource.mode)} isHttp={["http", "ssh", "rdp", "vnc"].includes(resource.mode)}
initialTargets={remoteTargets} initialTargets={remoteTargets}
resource={resource} resource={resource}
@@ -100,7 +75,6 @@ export default function ReverseProxyTargetsPage(props: {
updateResource={updateResource} updateResource={updateResource}
/> />
)} )}
</SettingsContainer> </SettingsContainer>
); );
} }
@@ -110,8 +84,12 @@ function ProxyResourceHttpForm({
updateResource updateResource
}: Pick<ResourceContextType, "resource" | "updateResource">) { }: Pick<ResourceContextType, "resource" | "updateResource">) {
const t = useTranslations(); const t = useTranslations();
const router = useRouter();
const { env } = useEnvContext();
const api = createApiClient({ env });
const tlsSettingsSchema = z.object({ const httpSettingsSchema = z.object({
stickySession: z.boolean(),
ssl: z.boolean(), ssl: z.boolean(),
tlsServerName: z tlsServerName: z
.string() .string()
@@ -126,18 +104,7 @@ function ProxyResourceHttpForm({
{ {
message: t("proxyErrorTls") message: t("proxyErrorTls")
} }
) ),
});
const tlsSettingsForm = useForm({
resolver: zodResolver(tlsSettingsSchema),
defaultValues: {
ssl: resource.ssl,
tlsServerName: resource.tlsServerName || ""
}
});
const proxySettingsSchema = z.object({
setHostHeader: z setHostHeader: z
.string() .string()
.optional() .optional()
@@ -154,69 +121,59 @@ function ProxyResourceHttpForm({
), ),
headers: z headers: z
.array(z.object({ name: z.string(), value: z.string() })) .array(z.object({ name: z.string(), value: z.string() }))
.nullable(), .nullable()
proxyProtocol: z.boolean().optional(),
proxyProtocolVersion: z.int().min(1).max(2).optional()
}); });
const proxySettingsForm = useForm({ const form = useForm({
resolver: zodResolver(proxySettingsSchema), resolver: zodResolver(httpSettingsSchema),
defaultValues: { defaultValues: {
stickySession: resource.stickySession,
ssl: resource.ssl,
tlsServerName: resource.tlsServerName || "",
setHostHeader: resource.setHostHeader || "", setHostHeader: resource.setHostHeader || "",
headers: resource.headers, headers: resource.headers
proxyProtocol: resource.proxyProtocol || false, },
proxyProtocolVersion: resource.proxyProtocolVersion || 1 mode: "onChange"
}
}); });
const { env } = useEnvContext(); const [, formAction, saveLoading] = useActionState(onSubmit, null);
const api = createApiClient({ env });
const targetsSettingsForm = useForm({ async function onSubmit() {
resolver: zodResolver(targetsSettingsSchema), const isValid = await form.trigger();
defaultValues: { if (!isValid) return;
stickySession: resource.stickySession
}
});
const router = useRouter(); const data = form.getValues();
const [, formAction, isSubmitting] = useActionState(
saveResourceHttpSettings,
null
);
async function saveResourceHttpSettings() { const res = await api
const isValidTLS = await tlsSettingsForm.trigger(); .post<AxiosResponse<UpdateResourceResponse>>(
const isValidProxy = await proxySettingsForm.trigger(); `/resource/${resource.resourceId}`,
const targetSettingsForm = await targetsSettingsForm.trigger(); {
if (!isValidTLS || !isValidProxy || !targetSettingsForm) return; stickySession: data.stickySession,
ssl: data.ssl,
tlsServerName: data.tlsServerName || null,
setHostHeader: data.setHostHeader || null,
headers: data.headers || null
}
)
.catch((err) => {
toast({
variant: "destructive",
title: t("settingsErrorUpdate"),
description: formatAxiosError(
err,
t("settingsErrorUpdateDescription")
)
});
});
try { if (res && res.status === 200) {
// Gather all settings
const stickySessionData = targetsSettingsForm.getValues();
const tlsData = tlsSettingsForm.getValues();
const proxyData = proxySettingsForm.getValues();
// Combine into one payload
const payload = {
stickySession: stickySessionData.stickySession,
ssl: tlsData.ssl,
tlsServerName: tlsData.tlsServerName || null,
setHostHeader: proxyData.setHostHeader || null,
headers: proxyData.headers || null
};
// Single API call to update all settings
await api.post(`/resource/${resource.resourceId}`, payload);
// Update local resource context
updateResource({ updateResource({
...resource, ...resource,
stickySession: stickySessionData.stickySession, stickySession: data.stickySession,
ssl: tlsData.ssl, ssl: data.ssl,
tlsServerName: tlsData.tlsServerName || null, tlsServerName: data.tlsServerName || null,
setHostHeader: proxyData.setHostHeader || null, setHostHeader: data.setHostHeader || null,
headers: proxyData.headers || null headers: data.headers || null
}); });
toast({ toast({
@@ -225,16 +182,6 @@ function ProxyResourceHttpForm({
}); });
router.refresh(); router.refresh();
} catch (err) {
console.error(err);
toast({
variant: "destructive",
title: t("settingsErrorUpdate"),
description: formatAxiosError(
err,
t("settingsErrorUpdateDescription")
)
});
} }
} }
@@ -248,155 +195,158 @@ function ProxyResourceHttpForm({
{t("proxyAdditionalDescription")} {t("proxyAdditionalDescription")}
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
<SettingsSectionForm> <SettingsSectionForm variant="half">
<Form {...tlsSettingsForm}> <Form {...form}>
<form <form action={formAction} id="http-settings-form">
action={formAction} <SettingsFormGrid>
className="space-y-4" {!env.flags.usePangolinDns && (
id="tls-settings-form" <SettingsFormCell span="full">
> <FormField
{!env.flags.usePangolinDns && ( control={form.control}
<FormField name="ssl"
control={tlsSettingsForm.control} render={({ field }) => (
name="ssl" <FormItem>
render={({ field }) => ( <FormControl>
<FormItem> <SwitchInput
<FormControl> id="ssl-toggle"
<SwitchInput label={t(
id="ssl-toggle" "proxyEnableSSL"
label={t("proxyEnableSSL")} )}
description={t( description={t(
"proxyEnableSSLDescription" "proxyEnableSSLDescription"
)}
checked={
field.value
}
onCheckedChange={
field.onChange
}
/>
</FormControl>
</FormItem>
)}
/>
</SettingsFormCell>
)}
<SettingsFormCell span="half">
<FormField
control={form.control}
name="tlsServerName"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("targetTlsSni")}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
{t(
"targetTlsSniDescription"
)} )}
defaultChecked={field.value} </FormDescription>
onCheckedChange={(val) => { <FormMessage />
field.onChange(val); </FormItem>
}} )}
/> />
</FormControl> </SettingsFormCell>
</FormItem>
)}
/>
)}
<FormField
control={tlsSettingsForm.control}
name="tlsServerName"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("targetTlsSni")}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
{t("targetTlsSniDescription")}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</SettingsSectionForm>
<SettingsSectionForm> <SettingsFormCell span="full">
<Form {...targetsSettingsForm}> <FormField
<form control={form.control}
action={formAction} name="stickySession"
className="space-y-4" render={({ field }) => (
id="targets-settings-form" <FormItem>
> <FormControl>
<FormField <SwitchInput
control={targetsSettingsForm.control} id="sticky-toggle"
name="stickySession" label={t(
render={({ field }) => ( "targetStickySessions"
<FormItem> )}
<FormControl> description={t(
<SwitchInput "targetStickySessionsDescription"
id="sticky-toggle" )}
label={t( checked={field.value}
"targetStickySessions" onCheckedChange={
)} field.onChange
description={t( }
"targetStickySessionsDescription" />
)} </FormControl>
defaultChecked={field.value} </FormItem>
onCheckedChange={(val) => { )}
field.onChange(val); />
}} </SettingsFormCell>
/>
</FormControl>
</FormItem>
)}
/>
</form>
</Form>
</SettingsSectionForm>
<SettingsSectionForm> <SettingsFormCell span="half">
<Form {...proxySettingsForm}> <FormField
<form control={form.control}
action={formAction} name="setHostHeader"
className="space-y-4" render={({ field }) => (
id="proxy-settings-form" <FormItem>
> <FormLabel>
<FormField {t("proxyCustomHeader")}
control={proxySettingsForm.control} </FormLabel>
name="setHostHeader" <FormControl>
render={({ field }) => ( <Input {...field} />
<FormItem> </FormControl>
<FormLabel> <FormDescription>
{t("proxyCustomHeader")} {t(
</FormLabel> "proxyCustomHeaderDescription"
<FormControl> )}
<Input {...field} /> </FormDescription>
</FormControl> <FormMessage />
<FormDescription> </FormItem>
{t("proxyCustomHeaderDescription")} )}
</FormDescription> />
<FormMessage /> </SettingsFormCell>
</FormItem>
)} <SettingsFormCell span="full">
/> <FormField
<FormField control={form.control}
control={proxySettingsForm.control} name="headers"
name="headers" render={({ field }) => (
render={({ field }) => ( <FormItem>
<FormItem> <FormLabel>
<FormLabel> {t("customHeaders")}
{t("customHeaders")} </FormLabel>
</FormLabel> <FormControl>
<FormControl> <HeadersInput
<HeadersInput value={field.value}
value={field.value} onChange={
onChange={(value) => { field.onChange
field.onChange(value); }
}} rows={4}
rows={4} />
/> </FormControl>
</FormControl> <FormDescription>
<FormDescription> {t(
{t("customHeadersDescription")} "customHeadersDescription"
</FormDescription> )}
<FormMessage /> </FormDescription>
</FormItem> <FormMessage />
)} </FormItem>
/> )}
/>
</SettingsFormCell>
</SettingsFormGrid>
</form> </form>
</Form> </Form>
</SettingsSectionForm> </SettingsSectionForm>
<form className="flex justify-end" action={formAction}>
<Button
disabled={isSubmitting}
loading={isSubmitting}
type="submit"
>
{t("saveResourceHttp")}
</Button>
</form>
</SettingsSectionBody> </SettingsSectionBody>
<SettingsSectionFooter>
<Button
type="submit"
loading={saveLoading}
disabled={saveLoading}
form="http-settings-form"
>
{t("saveSettings")}
</Button>
</SettingsSectionFooter>
</SettingsSection> </SettingsSection>
); );
} }
@@ -15,13 +15,18 @@ import { Textarea } from "@/components/ui/textarea";
import { useResourceContext } from "@app/hooks/useResourceContext"; import { useResourceContext } from "@app/hooks/useResourceContext";
import { import {
SettingsContainer, SettingsContainer,
SettingsFormCell,
SettingsFormGrid,
SettingsSection, SettingsSection,
SettingsSectionBody, SettingsSectionBody,
SettingsSectionDescription, SettingsSectionDescription,
SettingsSectionFooter, SettingsSectionFooter,
SettingsSectionForm, SettingsSectionForm,
SettingsSectionHeader, SettingsSectionHeader,
SettingsSectionTitle SettingsSectionTitle,
SettingsSubsectionDescription,
SettingsSubsectionHeader,
SettingsSubsectionTitle
} from "@app/components/Settings"; } from "@app/components/Settings";
import { SwitchInput } from "@app/components/SwitchInput"; import { SwitchInput } from "@app/components/SwitchInput";
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
@@ -37,12 +42,10 @@ import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import z from "zod"; import z from "zod";
import { Alert, AlertDescription } from "@app/components/ui/alert"; import { Alert, AlertDescription } from "@app/components/ui/alert";
import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group";
import { import {
Tooltip, StrategySelect,
TooltipProvider, type StrategyOption
TooltipTrigger } from "@app/components/StrategySelect";
} from "@app/components/ui/tooltip";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { tierMatrix } from "@server/lib/billing/tierMatrix"; import { tierMatrix } from "@server/lib/billing/tierMatrix";
@@ -158,6 +161,25 @@ export default function ResourceMaintenancePage() {
return null; return null;
} }
const isMaintenanceDisabled = !isPaidUser(tierMatrix.maintencePage);
const maintenanceModeTypeOptions: StrategyOption<
"automatic" | "forced"
>[] = [
{
id: "automatic",
title: `${t("automatic")} (${t("recommended")})`,
description: t("automaticModeDescription"),
disabled: isMaintenanceDisabled
},
{
id: "forced",
title: t("forced"),
description: t("forcedModeDescription"),
disabled: isMaintenanceDisabled
}
];
return ( return (
<SettingsContainer> <SettingsContainer>
<SettingsSection> <SettingsSection>
@@ -172,255 +194,237 @@ export default function ResourceMaintenancePage() {
<SettingsSectionBody> <SettingsSectionBody>
<PaidFeaturesAlert tiers={tierMatrix.maintencePage} /> <PaidFeaturesAlert tiers={tierMatrix.maintencePage} />
<SettingsSectionForm> <SettingsSectionForm variant="half">
<Form {...maintenanceForm}> <Form {...maintenanceForm}>
<form <form
action={maintenanceFormAction} action={maintenanceFormAction}
className="space-y-4"
id="maintenance-settings-form" id="maintenance-settings-form"
> >
<FormField <SettingsFormGrid>
control={maintenanceForm.control} <SettingsFormCell span="full">
name="maintenanceModeEnabled" <FormField
render={({ field }) => { control={maintenanceForm.control}
const isDisabled = !isPaidUser( name="maintenanceModeEnabled"
tierMatrix.maintencePage render={({ field }) => {
); const isDisabled = !isPaidUser(
tierMatrix.maintencePage
);
return ( return (
<FormItem> <FormItem>
<div className="flex items-center space-x-2"> <FormControl>
<FormControl> <SwitchInput
<TooltipProvider> id="enable-maintenance"
<Tooltip> checked={
<TooltipTrigger field.value
asChild }
label={t(
"enableMaintenanceMode"
)}
description={t(
"enableMaintenanceModeDescription"
)}
disabled={
isDisabled
}
onCheckedChange={(
val
) => {
if (
!isDisabled
) {
maintenanceForm.setValue(
"maintenanceModeEnabled",
val
);
}
}}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
</SettingsFormCell>
{isMaintenanceEnabled && (
<>
<SettingsFormCell span="full">
<FormField
control={
maintenanceForm.control
}
name="maintenanceModeType"
render={({ field }) => (
<FormItem>
<FormLabel>
{t(
"maintenanceModeType"
)}
</FormLabel>
<FormControl>
<StrategySelect<
| "automatic"
| "forced"
> >
<div className="flex items-center gap-2"> value={
<SwitchInput field.value
id="enable-maintenance" }
checked={ options={
field.value maintenanceModeTypeOptions
} }
label={t( onChange={
"enableMaintenanceMode" field.onChange
)} }
disabled={ cols={2}
isDisabled />
} </FormControl>
onCheckedChange={( <FormMessage />
val </FormItem>
) => {
if (
!isDisabled
) {
maintenanceForm.setValue(
"maintenanceModeEnabled",
val
);
}
}}
/>
</div>
</TooltipTrigger>
</Tooltip>
</TooltipProvider>
</FormControl>
</div>
<FormDescription>
{t(
"enableMaintenanceModeDescription"
)} )}
</FormDescription> />
<FormMessage /> </SettingsFormCell>
</FormItem>
);
}}
/>
{isMaintenanceEnabled && ( {maintenanceModeType ===
<div className="space-y-4"> "forced" && (
<FormField <SettingsFormCell span="full">
control={maintenanceForm.control} <Alert variant="neutral">
name="maintenanceModeType" <AlertCircle className="h-4 w-4" />
render={({ field }) => ( <AlertDescription>
<FormItem className="space-y-3"> {t(
<FormLabel> "forcedeModeWarning"
)}
</AlertDescription>
</Alert>
</SettingsFormCell>
)}
<SettingsFormCell span="full">
<SettingsSubsectionHeader>
<SettingsSubsectionTitle>
{t( {t(
"maintenanceModeType" "maintenancePageContentSubsection"
)} )}
</FormLabel> </SettingsSubsectionTitle>
<FormControl> <SettingsSubsectionDescription>
<RadioGroup {t(
onValueChange={ "maintenancePageContentSubsectionDescription"
field.onChange )}
} </SettingsSubsectionDescription>
defaultValue={ </SettingsSubsectionHeader>
field.value </SettingsFormCell>
}
disabled={ <SettingsFormCell span="half">
!isPaidUser( <FormField
tierMatrix.maintencePage control={
) maintenanceForm.control
} }
className="flex flex-col space-y-1" name="maintenanceTitle"
> render={({ field }) => (
<FormItem className="flex items-start space-x-3 space-y-0"> <FormItem>
<FormControl> <FormLabel>
<RadioGroupItem value="automatic" /> {t("pageTitle")}
</FormControl> </FormLabel>
<div className="space-y-1 leading-none"> <FormControl>
<FormLabel className="font-normal"> <Input
<strong> {...field}
{t( disabled={
"automatic" !isPaidUser(
)} tierMatrix.maintencePage
</strong>{" "}
(
{t(
"recommended"
)}
) )
</FormLabel> }
<FormDescription> placeholder="We'll be back soon!"
{t( />
"automaticModeDescription" </FormControl>
)} <FormDescription>
</FormDescription> {t(
</div> "pageTitleDescription"
</FormItem> )}
<FormItem className="flex items-start space-x-3 space-y-0"> </FormDescription>
<FormControl> <FormMessage />
<RadioGroupItem value="forced" /> </FormItem>
</FormControl> )}
<div className="space-y-1 leading-none"> />
<FormLabel className="font-normal"> </SettingsFormCell>
<strong>
{t(
"forced"
)}
</strong>
</FormLabel>
<FormDescription>
{t(
"forcedModeDescription"
)}
</FormDescription>
</div>
</FormItem>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{maintenanceModeType === "forced" && ( <SettingsFormCell span="full">
<Alert variant={"neutral"}> <FormField
<AlertCircle className="h-4 w-4" /> control={
<AlertDescription> maintenanceForm.control
{t("forcedeModeWarning")} }
</AlertDescription> name="maintenanceMessage"
</Alert> render={({ field }) => (
)} <FormItem>
<FormLabel>
{t(
"maintenancePageMessage"
)}
</FormLabel>
<FormControl>
<Textarea
{...field}
rows={4}
disabled={
!isPaidUser(
tierMatrix.maintencePage
)
}
placeholder={t(
"maintenancePageMessagePlaceholder"
)}
/>
</FormControl>
<FormDescription>
{t(
"maintenancePageMessageDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</SettingsFormCell>
<FormField <SettingsFormCell span="half">
control={maintenanceForm.control} <FormField
name="maintenanceTitle" control={
render={({ field }) => ( maintenanceForm.control
<FormItem> }
<FormLabel> name="maintenanceEstimatedTime"
{t("pageTitle")} render={({ field }) => (
</FormLabel> <FormItem>
<FormControl> <FormLabel>
<Input {t(
{...field} "maintenancePageTimeTitle"
disabled={ )}
!isPaidUser( </FormLabel>
tierMatrix.maintencePage <FormControl>
) <Input
} {...field}
placeholder="We'll be back soon!" disabled={
/> !isPaidUser(
</FormControl> tierMatrix.maintencePage
<FormDescription> )
{t( }
"pageTitleDescription" placeholder={t(
)} "maintenanceTime"
</FormDescription> )}
<FormMessage /> />
</FormItem> </FormControl>
)} <FormDescription>
/> {t(
"maintenanceEstimatedTimeDescription"
<FormField )}
control={maintenanceForm.control} </FormDescription>
name="maintenanceMessage" <FormMessage />
render={({ field }) => ( </FormItem>
<FormItem> )}
<FormLabel> />
{t( </SettingsFormCell>
"maintenancePageMessage" </>
)} )}
</FormLabel> </SettingsFormGrid>
<FormControl>
<Textarea
{...field}
rows={4}
disabled={
!isPaidUser(
tierMatrix.maintencePage
)
}
placeholder={t(
"maintenancePageMessagePlaceholder"
)}
/>
</FormControl>
<FormDescription>
{t(
"maintenancePageMessageDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={maintenanceForm.control}
name="maintenanceEstimatedTime"
render={({ field }) => (
<FormItem>
<FormLabel>
{t(
"maintenancePageTimeTitle"
)}
</FormLabel>
<FormControl>
<Input
{...field}
disabled={
!isPaidUser(
tierMatrix.maintencePage
)
}
placeholder={t(
"maintenanceTime"
)}
/>
</FormControl>
<FormDescription>
{t(
"maintenanceEstimatedTimeDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
)}
</form> </form>
</Form> </Form>
</SettingsSectionForm> </SettingsSectionForm>
@@ -525,6 +525,9 @@ function SshServerForm({
name="selectedNativeSite" name="selectedNativeSite"
render={() => ( render={() => (
<FormItem> <FormItem>
<FormLabel>
{t("sites")}
</FormLabel>
<Popover <Popover
open={nativeSiteOpen} open={nativeSiteOpen}
onOpenChange={ onOpenChange={
@@ -21,6 +21,8 @@ import { toast, useToast } from "@app/hooks/useToast";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { import {
SettingsContainer, SettingsContainer,
SettingsFormCell,
SettingsFormGrid,
SettingsSection, SettingsSection,
SettingsSectionHeader, SettingsSectionHeader,
SettingsSectionTitle, SettingsSectionTitle,
@@ -153,48 +155,54 @@ export default function GeneralPage() {
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
<SettingsSectionForm> <SettingsSectionForm variant="half">
<Form {...form}> <Form {...form}>
<form <form
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}
className="space-y-6" className="space-y-6"
id="general-settings-form" id="general-settings-form"
> >
<FormField <SettingsFormGrid>
control={form.control} <SettingsFormCell span="half">
name="name" <FormField
render={({ field }) => ( control={form.control}
<FormItem> name="name"
<FormLabel>{t("name")}</FormLabel> render={({ field }) => (
<FormControl> <FormItem>
<Input {...field} /> <FormLabel>
</FormControl> {t("name")}
<FormMessage /> </FormLabel>
</FormItem> <FormControl>
)} <Input {...field} />
/> </FormControl>
<FormMessage />
<FormField </FormItem>
control={form.control} )}
name="niceId" />
render={({ field }) => ( </SettingsFormCell>
<FormItem> <SettingsFormCell span="half">
<FormLabel> <FormField
{t("identifier")} control={form.control}
</FormLabel> name="niceId"
<FormControl> render={({ field }) => (
<Input <FormItem>
{...field} <FormLabel>
placeholder={t( {t("identifier")}
"enterIdentifier" </FormLabel>
)} <FormControl>
className="flex-1" <Input
/> {...field}
</FormControl> placeholder={t(
<FormMessage /> "enterIdentifier"
</FormItem> )}
)} />
/> </FormControl>
<FormMessage />
</FormItem>
)}
/>
</SettingsFormCell>
</SettingsFormGrid>
{site && site.type === "newt" && ( {site && site.type === "newt" && (
<FormField <FormField
+14 -11
View File
@@ -7,7 +7,6 @@ import {
SettingsSection, SettingsSection,
SettingsSectionBody, SettingsSectionBody,
SettingsSectionDescription, SettingsSectionDescription,
SettingsSectionForm,
SettingsSectionHeader, SettingsSectionHeader,
SettingsSectionTitle SettingsSectionTitle
} from "@app/components/Settings"; } from "@app/components/Settings";
@@ -519,7 +518,7 @@ export default function Page() {
id="create-site-form" id="create-site-form"
> >
<SettingsFormGrid> <SettingsFormGrid>
<SettingsFormCell span="half"> <SettingsFormCell span="quarter">
<FormField <FormField
control={form.control} control={form.control}
name="name" name="name"
@@ -543,9 +542,11 @@ export default function Page() {
</FormItem> </FormItem>
)} )}
/> />
{form.watch("method") === </SettingsFormCell>
"newt" && ( {form.watch("method") ===
<> "newt" && (
<>
<SettingsFormCell span="full">
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
@@ -566,7 +567,9 @@ export default function Page() {
"advancedSettings" "advancedSettings"
)} )}
</Button> </Button>
{showAdvancedSettings && ( </SettingsFormCell>
{showAdvancedSettings && (
<SettingsFormCell span="quarter">
<FormField <FormField
control={ control={
form.control form.control
@@ -575,7 +578,7 @@ export default function Page() {
render={({ render={({
field field
}) => ( }) => (
<FormItem className="mt-4"> <FormItem>
<FormLabel> <FormLabel>
{t( {t(
"siteAddress" "siteAddress"
@@ -612,10 +615,10 @@ export default function Page() {
</FormItem> </FormItem>
)} )}
/> />
)} </SettingsFormCell>
</> )}
)} </>
</SettingsFormCell> )}
</SettingsFormGrid> </SettingsFormGrid>
</form> </form>
</Form> </Form>
@@ -2,10 +2,11 @@
import { import {
SettingsContainer, SettingsContainer,
SettingsFormCell,
SettingsFormGrid,
SettingsSection, SettingsSection,
SettingsSectionBody, SettingsSectionBody,
SettingsSectionDescription, SettingsSectionDescription,
SettingsSectionForm,
SettingsSectionHeader, SettingsSectionHeader,
SettingsSectionTitle SettingsSectionTitle
} from "@app/components/Settings"; } from "@app/components/Settings";
@@ -199,23 +200,25 @@ export function CreatePolicyForm({}: CreatePolicyFormProps) {
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
<SettingsSectionForm variant="half"> <SettingsFormGrid>
<FormField <SettingsFormCell span="quarter">
control={form.control} <FormField
name="name" control={form.control}
render={({ field }) => ( name="name"
<FormItem> render={({ field }) => (
<FormLabel> <FormItem>
{t("name")} <FormLabel>
</FormLabel> {t("name")}
<FormControl> </FormLabel>
<Input {...field} /> <FormControl>
</FormControl> <Input {...field} />
<FormMessage /> </FormControl>
</FormItem> <FormMessage />
)} </FormItem>
/> )}
</SettingsSectionForm> />
</SettingsFormCell>
</SettingsFormGrid>
</SettingsSectionBody> </SettingsSectionBody>
</SettingsSection> </SettingsSection>
@@ -1,6 +1,8 @@
"use client"; "use client";
import { import {
SettingsFormCell,
SettingsFormGrid,
SettingsSection, SettingsSection,
SettingsSectionBody, SettingsSectionBody,
SettingsSectionDescription, SettingsSectionDescription,
@@ -138,45 +140,54 @@ export function EditPolicyNameSectionForm({
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
<SettingsSectionForm variant="half"> <SettingsSectionForm variant="half">
<FormField <SettingsFormGrid>
control={form.control} <SettingsFormCell span="half">
name="name" <FormField
render={({ field }) => ( control={form.control}
<FormItem> name="name"
<FormLabel>{t("name")}</FormLabel> render={({ field }) => (
<FormControl> <FormItem>
<Input <FormLabel>
{...field} {t("name")}
disabled={readonly} </FormLabel>
placeholder={t( <FormControl>
"resourcePolicyNamePlaceholder" <Input
)} {...field}
/> disabled={readonly}
</FormControl> placeholder={t(
<FormMessage /> "resourcePolicyNamePlaceholder"
</FormItem> )}
)} />
/> </FormControl>
<FormField <FormMessage />
control={form.control} </FormItem>
name="niceId" )}
render={({ field }) => ( />
<FormItem> </SettingsFormCell>
<FormLabel>{t("identifier")}</FormLabel> <SettingsFormCell span="half">
<FormControl> <FormField
<Input control={form.control}
{...field} name="niceId"
disabled={readonly} render={({ field }) => (
placeholder={t( <FormItem>
"enterIdentifier" <FormLabel>
)} {t("identifier")}
className="flex-1" </FormLabel>
/> <FormControl>
</FormControl> <Input
<FormMessage /> {...field}
</FormItem> disabled={readonly}
)} placeholder={t(
/> "enterIdentifier"
)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</SettingsFormCell>
</SettingsFormGrid>
</SettingsSectionForm> </SettingsSectionForm>
</SettingsSectionBody> </SettingsSectionBody>
@@ -1,9 +1,18 @@
"use client"; "use client";
import { SettingsSectionForm } from "@app/components/Settings"; import {
SettingsFormCell,
SettingsFormGrid,
SettingsSectionForm
} from "@app/components/Settings";
import { SwitchInput } from "@app/components/SwitchInput"; import { SwitchInput } from "@app/components/SwitchInput";
import { Button } from "@app/components/ui/button"; import { Button } from "@app/components/ui/button";
import { FormDescription, FormItem, FormLabel } from "@app/components/ui/form"; import {
FormControl,
FormDescription,
FormItem,
FormLabel
} from "@app/components/ui/form";
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -49,92 +58,106 @@ export function PolicyAuthSsoSection({
const idpSelectDisabled = idpDisabled ?? disabled; const idpSelectDisabled = idpDisabled ?? disabled;
return ( return (
<div className="space-y-4"> <SettingsSectionForm variant="half">
<SwitchInput <SettingsFormGrid>
id="policy-auth-sso" <SettingsFormCell span="full">
label={t("policyAuthSsoTitle")} <SwitchInput
description={t("policyAuthSsoDescription")} id="policy-auth-sso"
checked={sso} label={t("policyAuthSsoTitle")}
disabled={disabled} description={t("policyAuthSsoDescription")}
onCheckedChange={onSsoChange} checked={sso}
/> disabled={disabled}
onCheckedChange={onSsoChange}
/>
</SettingsFormCell>
{sso && ( {sso && (
<SettingsSectionForm className="max-w-none space-y-4"> <>
<FormItem className="flex flex-col items-start"> <SettingsFormCell span="full">
<FormLabel>{t("roles")}</FormLabel> <FormItem>
{rolesEditor} <FormLabel>{t("roles")}</FormLabel>
</FormItem> {rolesEditor}
<FormItem className="flex flex-col items-start"> </FormItem>
<FormLabel>{t("users")}</FormLabel> </SettingsFormCell>
{usersEditor} <SettingsFormCell span="full">
</FormItem> <FormItem>
{allIdps.length > 0 && ( <FormLabel>{t("users")}</FormLabel>
<div className="space-y-2"> {usersEditor}
{skipToIdpId == null && !showIdpSelect ? ( </FormItem>
<Button </SettingsFormCell>
type="button" {allIdps.length > 0 && (
variant="text" <SettingsFormCell span="half">
size="sm" {skipToIdpId == null && !showIdpSelect ? (
className="h-auto px-0" <Button
disabled={idpSelectDisabled} type="button"
onClick={() => setShowIdpSelect(true)} variant="text"
> size="sm"
{t("policyAuthAddDefaultIdentityProvider")} className="h-auto px-0"
</Button>
) : (
<>
<label className="text-sm font-medium">
{t("defaultIdentityProvider")}
</label>
<Select
disabled={idpSelectDisabled} disabled={idpSelectDisabled}
onValueChange={(value) => { onClick={() => setShowIdpSelect(true)}
if (value === "none") {
onSkipToIdpChange(null);
setShowIdpSelect(false);
return;
}
onSkipToIdpChange(parseInt(value));
}}
value={
skipToIdpId
? skipToIdpId.toString()
: "none"
}
> >
<SelectTrigger className="w-full">
<SelectValue
placeholder={t(
"selectIdpPlaceholder"
)}
/>
</SelectTrigger>
<SelectContent>
<SelectItem value="none">
{t("none")}
</SelectItem>
{allIdps.map((idp) => (
<SelectItem
key={idp.id}
value={idp.id.toString()}
>
{idp.text}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">
{t( {t(
"defaultIdentityProviderDescription" "policyAuthAddDefaultIdentityProvider"
)} )}
</p> </Button>
</> ) : (
)} <FormItem>
</div> <FormLabel>
)} {t("defaultIdentityProvider")}
</SettingsSectionForm> </FormLabel>
)} <Select
</div> disabled={idpSelectDisabled}
onValueChange={(value) => {
if (value === "none") {
onSkipToIdpChange(null);
setShowIdpSelect(false);
return;
}
onSkipToIdpChange(
parseInt(value)
);
}}
value={
skipToIdpId
? skipToIdpId.toString()
: "none"
}
>
<FormControl>
<SelectTrigger>
<SelectValue
placeholder={t(
"selectIdpPlaceholder"
)}
/>
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="none">
{t("none")}
</SelectItem>
{allIdps.map((idp) => (
<SelectItem
key={idp.id}
value={idp.id.toString()}
>
{idp.text}
</SelectItem>
))}
</SelectContent>
</Select>
<FormDescription>
{t(
"defaultIdentityProviderDescription"
)}
</FormDescription>
</FormItem>
)}
</SettingsFormCell>
)}
</>
)}
</SettingsFormGrid>
</SettingsSectionForm>
); );
} }
@@ -158,82 +158,91 @@ export function PolicyAuthStackSectionCreate({
</SettingsFormCell> </SettingsFormCell>
</SettingsFormGrid> </SettingsFormGrid>
<SettingsSubsectionHeader> <SettingsFormGrid>
<SettingsSubsectionTitle> <SettingsFormCell span="full">
{t("policyAuthOtherMethodsTitle")} <SettingsSubsectionHeader>
</SettingsSubsectionTitle> <SettingsSubsectionTitle>
<SettingsSubsectionDescription> {t("policyAuthOtherMethodsTitle")}
{t("policyAuthOtherMethodsDescription")} </SettingsSubsectionTitle>
</SettingsSubsectionDescription> <SettingsSubsectionDescription>
</SettingsSubsectionHeader> {t("policyAuthOtherMethodsDescription")}
</SettingsSubsectionDescription>
</SettingsSubsectionHeader>
</SettingsFormCell>
<SettingsFormCell span="half">
<div className="flex flex-col gap-3">
<PolicyAuthMethodRow
id="pincode"
title={t("policyAuthPincodeTitle")}
description={t("policyAuthPincodeDescription")}
summary={getPincodeSummary({ t })}
active={pinActive}
onConfigure={() => setEditingMethod("pincode")}
onToggle={(active) =>
handleToggle("pincode", active, () =>
parentForm.setValue("pincode", null)
)
}
/>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3"> <PolicyAuthMethodRow
<PolicyAuthMethodRow id="passcode"
id="pincode" title={t("policyAuthPasscodeTitle")}
title={t("policyAuthPincodeTitle")} description={t("policyAuthPasscodeDescription")}
description={t("policyAuthPincodeDescription")} summary={getPasscodeSummary({ t })}
summary={getPincodeSummary({ t })} active={passcodeActive}
active={pinActive} onConfigure={() => setEditingMethod("passcode")}
onConfigure={() => setEditingMethod("pincode")} onToggle={(active) =>
onToggle={(active) => handleToggle("passcode", active, () =>
handleToggle("pincode", active, () => parentForm.setValue("password", null)
parentForm.setValue("pincode", null) )
) }
} />
/>
<PolicyAuthMethodRow <PolicyAuthMethodRow
id="passcode" id="email"
title={t("policyAuthPasscodeTitle")} title={t("policyAuthEmailTitle")}
description={t("policyAuthPasscodeDescription")} description={t("policyAuthEmailDescription")}
summary={getPasscodeSummary({ t })} summary={getEmailWhitelistSummary({
active={passcodeActive} t,
onConfigure={() => setEditingMethod("passcode")} count: emails.length
onToggle={(active) => })}
handleToggle("passcode", active, () => active={Boolean(emailWhitelistEnabled)}
parentForm.setValue("password", null) onConfigure={() => setEditingMethod("email")}
) onToggle={(active) =>
} handleToggle("email", active, () =>
/> parentForm.setValue(
"emailWhitelistEnabled",
false
)
)
}
disabled={!emailEnabled}
/>
<PolicyAuthMethodRow <PolicyAuthMethodRow
id="email" id="header-auth"
title={t("policyAuthEmailTitle")} title={t("policyAuthHeaderAuthTitle")}
description={t("policyAuthEmailDescription")} description={t(
summary={getEmailWhitelistSummary({ "policyAuthHeaderAuthDescription"
t, )}
count: emails.length summary={getHeaderAuthSummary({
})} t,
active={Boolean(emailWhitelistEnabled)} headerName: headerAuth?.user ?? ""
onConfigure={() => setEditingMethod("email")} })}
onToggle={(active) => active={headerAuthActive}
handleToggle("email", active, () => onConfigure={() =>
parentForm.setValue( setEditingMethod("headerAuth")
"emailWhitelistEnabled", }
false onToggle={(active) =>
) handleToggle("headerAuth", active, () =>
) parentForm.setValue("headerAuth", null)
} )
disabled={!emailEnabled} }
/> />
</div>
<PolicyAuthMethodRow </SettingsFormCell>
id="header-auth" </SettingsFormGrid>
title={t("policyAuthHeaderAuthTitle")}
description={t("policyAuthHeaderAuthDescription")}
summary={getHeaderAuthSummary({
t,
headerName: headerAuth?.user ?? ""
})}
active={headerAuthActive}
onConfigure={() => setEditingMethod("headerAuth")}
onToggle={(active) =>
handleToggle("headerAuth", active, () =>
parentForm.setValue("headerAuth", null)
)
}
/>
</div>
<PincodeCredenza <PincodeCredenza
open={editingMethod === "pincode"} open={editingMethod === "pincode"}
@@ -636,111 +636,146 @@ export function PolicyAuthStackSectionEdit({
</SettingsFormCell> </SettingsFormCell>
</SettingsFormGrid> </SettingsFormGrid>
<SettingsSubsectionHeader> <SettingsFormGrid>
<SettingsSubsectionTitle> <SettingsFormCell span="full">
{t("policyAuthOtherMethodsTitle")} <SettingsSubsectionHeader>
</SettingsSubsectionTitle> <SettingsSubsectionTitle>
<SettingsSubsectionDescription> {t("policyAuthOtherMethodsTitle")}
{t("policyAuthOtherMethodsDescription")} </SettingsSubsectionTitle>
</SettingsSubsectionDescription> <SettingsSubsectionDescription>
</SettingsSubsectionHeader> {t(
"policyAuthOtherMethodsDescription"
<div className="grid grid-cols-1 md:grid-cols-2 gap-3"> )}
<PolicyAuthMethodRow </SettingsSubsectionDescription>
id="pincode" </SettingsSubsectionHeader>
title={t("policyAuthPincodeTitle")} </SettingsFormCell>
description={t( <SettingsFormCell span="half">
"policyAuthPincodeDescription" <div className="flex flex-col gap-3">
)} <PolicyAuthMethodRow
summary={getPincodeSummary({ t })} id="pincode"
active={pinActive} title={t("policyAuthPincodeTitle")}
onConfigure={() => description={t(
openMethodEditor("pincode") "policyAuthPincodeDescription"
} )}
onToggle={(active) => summary={getPincodeSummary({ t })}
handleToggle("pincode", active, () => { active={pinActive}
setPinActive(false); onConfigure={() =>
form.setValue("pincode", null); openMethodEditor("pincode")
})
}
disabled={authReadonly}
/>
<PolicyAuthMethodRow
id="passcode"
title={t("policyAuthPasscodeTitle")}
description={t(
"policyAuthPasscodeDescription"
)}
summary={getPasscodeSummary({ t })}
active={passcodeActive}
onConfigure={() =>
openMethodEditor("passcode")
}
onToggle={(active) =>
handleToggle("passcode", active, () => {
setPasscodeActive(false);
form.setValue("password", null);
})
}
disabled={authReadonly}
/>
<PolicyAuthMethodRow
id="email"
title={t("policyAuthEmailTitle")}
description={t(
"policyAuthEmailDescription"
)}
summary={getEmailWhitelistSummary({
t,
count: emails.length
})}
active={Boolean(emailWhitelistEnabled)}
onConfigure={() =>
openMethodEditor("email")
}
onToggle={(active) =>
handleToggle("email", active, () =>
form.setValue(
"emailWhitelistEnabled",
false
)
)
}
disabled={authReadonly || !emailEnabled}
/>
<PolicyAuthMethodRow
id="header-auth"
title={t("policyAuthHeaderAuthTitle")}
description={t(
"policyAuthHeaderAuthDescription"
)}
summary={getHeaderAuthSummary({
t,
headerName: headerAuth?.user ?? ""
})}
active={headerAuthActive}
onConfigure={() =>
openMethodEditor("headerAuth")
}
onToggle={(active) =>
handleToggle(
"headerAuth",
active,
() => {
setHeaderAuthActive(false);
form.setValue(
"headerAuth",
null
);
} }
) onToggle={(active) =>
} handleToggle(
disabled={authReadonly} "pincode",
/> active,
</div> () => {
setPinActive(false);
form.setValue(
"pincode",
null
);
}
)
}
disabled={authReadonly}
/>
<PolicyAuthMethodRow
id="passcode"
title={t("policyAuthPasscodeTitle")}
description={t(
"policyAuthPasscodeDescription"
)}
summary={getPasscodeSummary({ t })}
active={passcodeActive}
onConfigure={() =>
openMethodEditor("passcode")
}
onToggle={(active) =>
handleToggle(
"passcode",
active,
() => {
setPasscodeActive(
false
);
form.setValue(
"password",
null
);
}
)
}
disabled={authReadonly}
/>
<PolicyAuthMethodRow
id="email"
title={t("policyAuthEmailTitle")}
description={t(
"policyAuthEmailDescription"
)}
summary={getEmailWhitelistSummary({
t,
count: emails.length
})}
active={Boolean(
emailWhitelistEnabled
)}
onConfigure={() =>
openMethodEditor("email")
}
onToggle={(active) =>
handleToggle(
"email",
active,
() =>
form.setValue(
"emailWhitelistEnabled",
false
)
)
}
disabled={
authReadonly || !emailEnabled
}
/>
<PolicyAuthMethodRow
id="header-auth"
title={t(
"policyAuthHeaderAuthTitle"
)}
description={t(
"policyAuthHeaderAuthDescription"
)}
summary={getHeaderAuthSummary({
t,
headerName:
headerAuth?.user ?? ""
})}
active={headerAuthActive}
onConfigure={() =>
openMethodEditor("headerAuth")
}
onToggle={(active) =>
handleToggle(
"headerAuth",
active,
() => {
setHeaderAuthActive(
false
);
form.setValue(
"headerAuth",
null
);
}
)
}
disabled={authReadonly}
/>
</div>
</SettingsFormCell>
</SettingsFormGrid>
</div> </div>
<PincodeCredenza <PincodeCredenza