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

View File

@@ -727,7 +727,7 @@
"targetSubmit": "Add Target",
"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.",
"targetsSubmit": "Save Targets",
"targetsSubmit": "Save Settings",
"addTarget": "Add Target",
"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",
@@ -1845,7 +1845,7 @@
"documentation": "Documentation",
"saveAllSettings": "Save All Settings",
"saveResourceTargets": "Save Targets",
"saveResourceHttp": "Save Proxy Settings",
"saveResourceHttp": "Save Settings",
"saveProxyProtocol": "Save Proxy protocol settings",
"settingsUpdated": "Settings updated",
"settingsUpdatedDescription": "Settings updated successfully",
@@ -3180,6 +3180,8 @@
"warning:": "Warning:",
"forcedeModeWarning": "All traffic will be directed to the maintenance page. Your backend resources will not receive any requests.",
"pageTitle": "Page Title",
"maintenancePageContentSubsection": "Page Content",
"maintenancePageContentSubsectionDescription": "Customize the content displayed on the maintenance page",
"pageTitleDescription": "The main heading displayed on the maintenance page",
"maintenancePageMessage": "Maintenance Message",
"maintenancePageMessagePlaceholder": "We'll be back soon! Our site is currently undergoing scheduled maintenance.",

View File

@@ -262,7 +262,7 @@ export default function Page() {
id="create-client-form"
>
<SettingsFormGrid>
<SettingsFormCell span="half">
<SettingsFormCell span="quarter">
<FormField
control={form.control}
name="name"
@@ -287,7 +287,7 @@ export default function Page() {
)}
/>
</SettingsFormCell>
<SettingsFormCell className="flex items-center justify-end md:col-span-2">
<SettingsFormCell span="full">
<Button
type="button"
variant="ghost"
@@ -297,7 +297,7 @@ export default function Page() {
!showAdvancedSettings
)
}
className="flex items-center gap-2"
className="flex items-center gap-2 -ml-3"
>
{showAdvancedSettings ? (
<ChevronUp className="h-4 w-4" />
@@ -308,7 +308,7 @@ export default function Page() {
</Button>
</SettingsFormCell>
{showAdvancedSettings && (
<SettingsFormCell span="full">
<SettingsFormCell span="quarter">
<FormField
control={form.control}
name="subnet"

View File

@@ -224,23 +224,39 @@ function LogRetentionSectionForm({ org }: SectionFormProps) {
<SelectContent>
{LOG_RETENTION_OPTIONS.filter(
(option) => {
if (build != "saas") {
if (
build != "saas"
) {
return true;
}
let maxDays: number;
if (!subscriptionTier) {
if (
!subscriptionTier
) {
// No tier
maxDays = 3;
} else if (subscriptionTier == "enterprise") {
} else if (
subscriptionTier ==
"enterprise"
) {
// Enterprise - no limit
return true;
} else if (subscriptionTier == "tier3") {
} else if (
subscriptionTier ==
"tier3"
) {
maxDays = 90;
} else if (subscriptionTier == "tier2") {
} else if (
subscriptionTier ==
"tier2"
) {
maxDays = 30;
} else if (subscriptionTier == "tier1") {
} else if (
subscriptionTier ==
"tier1"
) {
maxDays = 7;
} else {
// Default to most restrictive
@@ -249,7 +265,12 @@ function LogRetentionSectionForm({ org }: SectionFormProps) {
// Filter out options that exceed the max
// 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;
}
@@ -322,24 +343,43 @@ function LogRetentionSectionForm({ org }: SectionFormProps) {
</SelectTrigger>
<SelectContent>
{LOG_RETENTION_OPTIONS.filter(
(option) => {
if (build != "saas") {
(
option
) => {
if (
build !=
"saas"
) {
return true;
}
let maxDays: number;
if (!subscriptionTier) {
if (
!subscriptionTier
) {
// No tier
maxDays = 3;
} else if (subscriptionTier == "enterprise") {
} else if (
subscriptionTier ==
"enterprise"
) {
// Enterprise - no limit
return true;
} else if (subscriptionTier == "tier3") {
} else if (
subscriptionTier ==
"tier3"
) {
maxDays = 90;
} else if (subscriptionTier == "tier2") {
} else if (
subscriptionTier ==
"tier2"
) {
maxDays = 30;
} else if (subscriptionTier == "tier1") {
} else if (
subscriptionTier ==
"tier1"
) {
maxDays = 7;
} else {
// Default to most restrictive
@@ -348,7 +388,12 @@ function LogRetentionSectionForm({ org }: SectionFormProps) {
// Filter out options that exceed the max
// 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;
}
@@ -423,24 +468,43 @@ function LogRetentionSectionForm({ org }: SectionFormProps) {
</SelectTrigger>
<SelectContent>
{LOG_RETENTION_OPTIONS.filter(
(option) => {
if (build != "saas") {
(
option
) => {
if (
build !=
"saas"
) {
return true;
}
let maxDays: number;
if (!subscriptionTier) {
if (
!subscriptionTier
) {
// No tier
maxDays = 3;
} else if (subscriptionTier == "enterprise") {
} else if (
subscriptionTier ==
"enterprise"
) {
// Enterprise - no limit
return true;
} else if (subscriptionTier == "tier3") {
} else if (
subscriptionTier ==
"tier3"
) {
maxDays = 90;
} else if (subscriptionTier == "tier2") {
} else if (
subscriptionTier ==
"tier2"
) {
maxDays = 30;
} else if (subscriptionTier == "tier1") {
} else if (
subscriptionTier ==
"tier1"
) {
maxDays = 7;
} else {
// Default to most restrictive
@@ -449,7 +513,12 @@ function LogRetentionSectionForm({ org }: SectionFormProps) {
// Filter out options that exceed the max
// 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;
}
@@ -524,24 +593,43 @@ function LogRetentionSectionForm({ org }: SectionFormProps) {
</SelectTrigger>
<SelectContent>
{LOG_RETENTION_OPTIONS.filter(
(option) => {
if (build != "saas") {
(
option
) => {
if (
build !=
"saas"
) {
return true;
}
let maxDays: number;
if (!subscriptionTier) {
if (
!subscriptionTier
) {
// No tier
maxDays = 3;
} else if (subscriptionTier == "enterprise") {
} else if (
subscriptionTier ==
"enterprise"
) {
// Enterprise - no limit
return true;
} else if (subscriptionTier == "tier3") {
} else if (
subscriptionTier ==
"tier3"
) {
maxDays = 90;
} else if (subscriptionTier == "tier2") {
} else if (
subscriptionTier ==
"tier2"
) {
maxDays = 30;
} else if (subscriptionTier == "tier1") {
} else if (
subscriptionTier ==
"tier1"
) {
maxDays = 7;
} else {
// Default to most restrictive
@@ -550,7 +638,12 @@ function LogRetentionSectionForm({ org }: SectionFormProps) {
// Filter out options that exceed the max
// 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;
}

View File

@@ -489,7 +489,7 @@ export default function GeneralForm() {
</SettingsSubsectionDescription>
</SettingsSubsectionHeader>
</SettingsFormCell>
<SettingsFormCell span="full">
<SettingsFormCell span="half">
<div className="space-y-2">
<FormLabel>
{t("sharedPolicy")}

View File

@@ -1,35 +1,21 @@
"use client";
import HealthCheckCredenza from "@/components/HealthCheckCredenza";
import { Button } from "@/components/ui/button";
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 {
PathMatchDisplay,
PathMatchModal,
PathRewriteDisplay,
PathRewriteModal
} from "@app/components/PathMatchRenameModal";
import { ResourceTargetAddressItem } from "@app/components/resource-target-address-item";
import {
SettingsContainer,
SettingsFormCell,
SettingsFormGrid,
SettingsSection,
SettingsSectionBody,
SettingsSectionDescription,
SettingsSectionFooter,
SettingsSectionForm,
SettingsSectionHeader,
SettingsSectionTitle
} from "@app/components/Settings";
import { SwitchInput } from "@app/components/SwitchInput";
import { Alert, AlertDescription } from "@app/components/ui/alert";
import {
Form,
FormControl,
@@ -43,35 +29,24 @@ import type { ResourceContextType } from "@app/contexts/resourceContext";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useResourceContext } from "@app/hooks/useResourceContext";
import { toast } from "@app/hooks/useToast";
import { createApiClient } from "@app/lib/api";
import { formatAxiosError } from "@app/lib/api/formatAxiosError";
import { createApiClient, formatAxiosError } from "@app/lib/api";
import { resourceQueries } from "@app/lib/queries";
import { zodResolver } from "@hookform/resolvers/zod";
import { UpdateResourceResponse } from "@server/routers/resource";
import { tlsNameSchema } from "@server/lib/schemas";
import { useQuery } from "@tanstack/react-query";
import {
ProxyResourceTargetsForm
} from "@app/app/[orgId]/settings/resources/public/ProxyResourceTargetsForm";
import {
AlertTriangle,
} from "lucide-react";
import { AxiosResponse } from "axios";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import {
use,
useActionState,
} from "react";
import { useParams, useRouter } from "next/navigation";
import { useActionState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
const targetsSettingsSchema = z.object({
stickySession: z.boolean()
});
export default function ReverseProxyTargetsPage(props: {
params: Promise<{ resourceId: number; orgId: string }>;
}) {
const params = use(props.params);
export default function ReverseProxyTargetsPage() {
const params = useParams();
const { resource, updateResource } = useResourceContext();
const { data: remoteTargets = [], isLoading: isLoadingTargets } = useQuery(
@@ -87,7 +62,7 @@ export default function ReverseProxyTargetsPage(props: {
return (
<SettingsContainer>
<ProxyResourceTargetsForm
orgId={params.orgId}
orgId={params.orgId as string}
isHttp={["http", "ssh", "rdp", "vnc"].includes(resource.mode)}
initialTargets={remoteTargets}
resource={resource}
@@ -100,7 +75,6 @@ export default function ReverseProxyTargetsPage(props: {
updateResource={updateResource}
/>
)}
</SettingsContainer>
);
}
@@ -110,8 +84,12 @@ function ProxyResourceHttpForm({
updateResource
}: Pick<ResourceContextType, "resource" | "updateResource">) {
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(),
tlsServerName: z
.string()
@@ -126,18 +104,7 @@ function ProxyResourceHttpForm({
{
message: t("proxyErrorTls")
}
)
});
const tlsSettingsForm = useForm({
resolver: zodResolver(tlsSettingsSchema),
defaultValues: {
ssl: resource.ssl,
tlsServerName: resource.tlsServerName || ""
}
});
const proxySettingsSchema = z.object({
),
setHostHeader: z
.string()
.optional()
@@ -154,69 +121,59 @@ function ProxyResourceHttpForm({
),
headers: z
.array(z.object({ name: z.string(), value: z.string() }))
.nullable(),
proxyProtocol: z.boolean().optional(),
proxyProtocolVersion: z.int().min(1).max(2).optional()
.nullable()
});
const proxySettingsForm = useForm({
resolver: zodResolver(proxySettingsSchema),
const form = useForm({
resolver: zodResolver(httpSettingsSchema),
defaultValues: {
stickySession: resource.stickySession,
ssl: resource.ssl,
tlsServerName: resource.tlsServerName || "",
setHostHeader: resource.setHostHeader || "",
headers: resource.headers,
proxyProtocol: resource.proxyProtocol || false,
proxyProtocolVersion: resource.proxyProtocolVersion || 1
}
headers: resource.headers
},
mode: "onChange"
});
const { env } = useEnvContext();
const api = createApiClient({ env });
const [, formAction, saveLoading] = useActionState(onSubmit, null);
const targetsSettingsForm = useForm({
resolver: zodResolver(targetsSettingsSchema),
defaultValues: {
stickySession: resource.stickySession
}
});
async function onSubmit() {
const isValid = await form.trigger();
if (!isValid) return;
const router = useRouter();
const [, formAction, isSubmitting] = useActionState(
saveResourceHttpSettings,
null
);
const data = form.getValues();
async function saveResourceHttpSettings() {
const isValidTLS = await tlsSettingsForm.trigger();
const isValidProxy = await proxySettingsForm.trigger();
const targetSettingsForm = await targetsSettingsForm.trigger();
if (!isValidTLS || !isValidProxy || !targetSettingsForm) return;
const res = await api
.post<AxiosResponse<UpdateResourceResponse>>(
`/resource/${resource.resourceId}`,
{
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 {
// 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
if (res && res.status === 200) {
updateResource({
...resource,
stickySession: stickySessionData.stickySession,
ssl: tlsData.ssl,
tlsServerName: tlsData.tlsServerName || null,
setHostHeader: proxyData.setHostHeader || null,
headers: proxyData.headers || null
stickySession: data.stickySession,
ssl: data.ssl,
tlsServerName: data.tlsServerName || null,
setHostHeader: data.setHostHeader || null,
headers: data.headers || null
});
toast({
@@ -225,16 +182,6 @@ function ProxyResourceHttpForm({
});
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")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm>
<Form {...tlsSettingsForm}>
<form
action={formAction}
className="space-y-4"
id="tls-settings-form"
>
{!env.flags.usePangolinDns && (
<FormField
control={tlsSettingsForm.control}
name="ssl"
render={({ field }) => (
<FormItem>
<FormControl>
<SwitchInput
id="ssl-toggle"
label={t("proxyEnableSSL")}
description={t(
"proxyEnableSSLDescription"
<SettingsSectionForm variant="half">
<Form {...form}>
<form action={formAction} id="http-settings-form">
<SettingsFormGrid>
{!env.flags.usePangolinDns && (
<SettingsFormCell span="full">
<FormField
control={form.control}
name="ssl"
render={({ field }) => (
<FormItem>
<FormControl>
<SwitchInput
id="ssl-toggle"
label={t(
"proxyEnableSSL"
)}
description={t(
"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}
onCheckedChange={(val) => {
field.onChange(val);
}}
/>
</FormControl>
</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>
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</SettingsFormCell>
<SettingsSectionForm>
<Form {...targetsSettingsForm}>
<form
action={formAction}
className="space-y-4"
id="targets-settings-form"
>
<FormField
control={targetsSettingsForm.control}
name="stickySession"
render={({ field }) => (
<FormItem>
<FormControl>
<SwitchInput
id="sticky-toggle"
label={t(
"targetStickySessions"
)}
description={t(
"targetStickySessionsDescription"
)}
defaultChecked={field.value}
onCheckedChange={(val) => {
field.onChange(val);
}}
/>
</FormControl>
</FormItem>
)}
/>
</form>
</Form>
</SettingsSectionForm>
<SettingsFormCell span="full">
<FormField
control={form.control}
name="stickySession"
render={({ field }) => (
<FormItem>
<FormControl>
<SwitchInput
id="sticky-toggle"
label={t(
"targetStickySessions"
)}
description={t(
"targetStickySessionsDescription"
)}
checked={field.value}
onCheckedChange={
field.onChange
}
/>
</FormControl>
</FormItem>
)}
/>
</SettingsFormCell>
<SettingsSectionForm>
<Form {...proxySettingsForm}>
<form
action={formAction}
className="space-y-4"
id="proxy-settings-form"
>
<FormField
control={proxySettingsForm.control}
name="setHostHeader"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("proxyCustomHeader")}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
{t("proxyCustomHeaderDescription")}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={proxySettingsForm.control}
name="headers"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("customHeaders")}
</FormLabel>
<FormControl>
<HeadersInput
value={field.value}
onChange={(value) => {
field.onChange(value);
}}
rows={4}
/>
</FormControl>
<FormDescription>
{t("customHeadersDescription")}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<SettingsFormCell span="half">
<FormField
control={form.control}
name="setHostHeader"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("proxyCustomHeader")}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
{t(
"proxyCustomHeaderDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</SettingsFormCell>
<SettingsFormCell span="full">
<FormField
control={form.control}
name="headers"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("customHeaders")}
</FormLabel>
<FormControl>
<HeadersInput
value={field.value}
onChange={
field.onChange
}
rows={4}
/>
</FormControl>
<FormDescription>
{t(
"customHeadersDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</SettingsFormCell>
</SettingsFormGrid>
</form>
</Form>
</SettingsSectionForm>
<form className="flex justify-end" action={formAction}>
<Button
disabled={isSubmitting}
loading={isSubmitting}
type="submit"
>
{t("saveResourceHttp")}
</Button>
</form>
</SettingsSectionBody>
<SettingsSectionFooter>
<Button
type="submit"
loading={saveLoading}
disabled={saveLoading}
form="http-settings-form"
>
{t("saveSettings")}
</Button>
</SettingsSectionFooter>
</SettingsSection>
);
}
}

View File

@@ -15,13 +15,18 @@ import { Textarea } from "@/components/ui/textarea";
import { useResourceContext } from "@app/hooks/useResourceContext";
import {
SettingsContainer,
SettingsFormCell,
SettingsFormGrid,
SettingsSection,
SettingsSectionBody,
SettingsSectionDescription,
SettingsSectionFooter,
SettingsSectionForm,
SettingsSectionHeader,
SettingsSectionTitle
SettingsSectionTitle,
SettingsSubsectionDescription,
SettingsSubsectionHeader,
SettingsSubsectionTitle
} from "@app/components/Settings";
import { SwitchInput } from "@app/components/SwitchInput";
import { useEnvContext } from "@app/hooks/useEnvContext";
@@ -37,12 +42,10 @@ import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import z from "zod";
import { Alert, AlertDescription } from "@app/components/ui/alert";
import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group";
import {
Tooltip,
TooltipProvider,
TooltipTrigger
} from "@app/components/ui/tooltip";
StrategySelect,
type StrategyOption
} from "@app/components/StrategySelect";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
@@ -158,6 +161,25 @@ export default function ResourceMaintenancePage() {
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 (
<SettingsContainer>
<SettingsSection>
@@ -172,255 +194,237 @@ export default function ResourceMaintenancePage() {
<SettingsSectionBody>
<PaidFeaturesAlert tiers={tierMatrix.maintencePage} />
<SettingsSectionForm>
<SettingsSectionForm variant="half">
<Form {...maintenanceForm}>
<form
action={maintenanceFormAction}
className="space-y-4"
id="maintenance-settings-form"
>
<FormField
control={maintenanceForm.control}
name="maintenanceModeEnabled"
render={({ field }) => {
const isDisabled = !isPaidUser(
tierMatrix.maintencePage
);
<SettingsFormGrid>
<SettingsFormCell span="full">
<FormField
control={maintenanceForm.control}
name="maintenanceModeEnabled"
render={({ field }) => {
const isDisabled = !isPaidUser(
tierMatrix.maintencePage
);
return (
<FormItem>
<div className="flex items-center space-x-2">
<FormControl>
<TooltipProvider>
<Tooltip>
<TooltipTrigger
asChild
return (
<FormItem>
<FormControl>
<SwitchInput
id="enable-maintenance"
checked={
field.value
}
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">
<SwitchInput
id="enable-maintenance"
checked={
field.value
}
label={t(
"enableMaintenanceMode"
)}
disabled={
isDisabled
}
onCheckedChange={(
val
) => {
if (
!isDisabled
) {
maintenanceForm.setValue(
"maintenanceModeEnabled",
val
);
}
}}
/>
</div>
</TooltipTrigger>
</Tooltip>
</TooltipProvider>
</FormControl>
</div>
<FormDescription>
{t(
"enableMaintenanceModeDescription"
value={
field.value
}
options={
maintenanceModeTypeOptions
}
onChange={
field.onChange
}
cols={2}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
</FormDescription>
<FormMessage />
</FormItem>
);
}}
/>
/>
</SettingsFormCell>
{isMaintenanceEnabled && (
<div className="space-y-4">
<FormField
control={maintenanceForm.control}
name="maintenanceModeType"
render={({ field }) => (
<FormItem className="space-y-3">
<FormLabel>
{maintenanceModeType ===
"forced" && (
<SettingsFormCell span="full">
<Alert variant="neutral">
<AlertCircle className="h-4 w-4" />
<AlertDescription>
{t(
"forcedeModeWarning"
)}
</AlertDescription>
</Alert>
</SettingsFormCell>
)}
<SettingsFormCell span="full">
<SettingsSubsectionHeader>
<SettingsSubsectionTitle>
{t(
"maintenanceModeType"
"maintenancePageContentSubsection"
)}
</FormLabel>
<FormControl>
<RadioGroup
onValueChange={
field.onChange
}
defaultValue={
field.value
}
disabled={
!isPaidUser(
tierMatrix.maintencePage
)
}
className="flex flex-col space-y-1"
>
<FormItem className="flex items-start space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="automatic" />
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel className="font-normal">
<strong>
{t(
"automatic"
)}
</strong>{" "}
(
{t(
"recommended"
)}
</SettingsSubsectionTitle>
<SettingsSubsectionDescription>
{t(
"maintenancePageContentSubsectionDescription"
)}
</SettingsSubsectionDescription>
</SettingsSubsectionHeader>
</SettingsFormCell>
<SettingsFormCell span="half">
<FormField
control={
maintenanceForm.control
}
name="maintenanceTitle"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("pageTitle")}
</FormLabel>
<FormControl>
<Input
{...field}
disabled={
!isPaidUser(
tierMatrix.maintencePage
)
</FormLabel>
<FormDescription>
{t(
"automaticModeDescription"
)}
</FormDescription>
</div>
</FormItem>
<FormItem className="flex items-start space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="forced" />
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel className="font-normal">
<strong>
{t(
"forced"
)}
</strong>
</FormLabel>
<FormDescription>
{t(
"forcedModeDescription"
)}
</FormDescription>
</div>
</FormItem>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
}
placeholder="We'll be back soon!"
/>
</FormControl>
<FormDescription>
{t(
"pageTitleDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</SettingsFormCell>
{maintenanceModeType === "forced" && (
<Alert variant={"neutral"}>
<AlertCircle className="h-4 w-4" />
<AlertDescription>
{t("forcedeModeWarning")}
</AlertDescription>
</Alert>
)}
<SettingsFormCell span="full">
<FormField
control={
maintenanceForm.control
}
name="maintenanceMessage"
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
control={maintenanceForm.control}
name="maintenanceTitle"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("pageTitle")}
</FormLabel>
<FormControl>
<Input
{...field}
disabled={
!isPaidUser(
tierMatrix.maintencePage
)
}
placeholder="We'll be back soon!"
/>
</FormControl>
<FormDescription>
{t(
"pageTitleDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={maintenanceForm.control}
name="maintenanceMessage"
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>
)}
/>
<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>
)}
<SettingsFormCell span="half">
<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>
)}
/>
</SettingsFormCell>
</>
)}
</SettingsFormGrid>
</form>
</Form>
</SettingsSectionForm>

View File

@@ -525,6 +525,9 @@ function SshServerForm({
name="selectedNativeSite"
render={() => (
<FormItem>
<FormLabel>
{t("sites")}
</FormLabel>
<Popover
open={nativeSiteOpen}
onOpenChange={

View File

@@ -21,6 +21,8 @@ import { toast, useToast } from "@app/hooks/useToast";
import { useRouter } from "next/navigation";
import {
SettingsContainer,
SettingsFormCell,
SettingsFormGrid,
SettingsSection,
SettingsSectionHeader,
SettingsSectionTitle,
@@ -153,48 +155,54 @@ export default function GeneralPage() {
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm>
<SettingsSectionForm variant="half">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-6"
id="general-settings-form"
>
<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"
)}
className="flex-1"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<SettingsFormGrid>
<SettingsFormCell span="half">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("name")}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</SettingsFormCell>
<SettingsFormCell span="half">
<FormField
control={form.control}
name="niceId"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("identifier")}
</FormLabel>
<FormControl>
<Input
{...field}
placeholder={t(
"enterIdentifier"
)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</SettingsFormCell>
</SettingsFormGrid>
{site && site.type === "newt" && (
<FormField

View File

@@ -7,7 +7,6 @@ import {
SettingsSection,
SettingsSectionBody,
SettingsSectionDescription,
SettingsSectionForm,
SettingsSectionHeader,
SettingsSectionTitle
} from "@app/components/Settings";
@@ -519,7 +518,7 @@ export default function Page() {
id="create-site-form"
>
<SettingsFormGrid>
<SettingsFormCell span="half">
<SettingsFormCell span="quarter">
<FormField
control={form.control}
name="name"
@@ -543,9 +542,11 @@ export default function Page() {
</FormItem>
)}
/>
{form.watch("method") ===
"newt" && (
<>
</SettingsFormCell>
{form.watch("method") ===
"newt" && (
<>
<SettingsFormCell span="full">
<Button
type="button"
variant="ghost"
@@ -566,7 +567,9 @@ export default function Page() {
"advancedSettings"
)}
</Button>
{showAdvancedSettings && (
</SettingsFormCell>
{showAdvancedSettings && (
<SettingsFormCell span="quarter">
<FormField
control={
form.control
@@ -575,7 +578,7 @@ export default function Page() {
render={({
field
}) => (
<FormItem className="mt-4">
<FormItem>
<FormLabel>
{t(
"siteAddress"
@@ -612,10 +615,10 @@ export default function Page() {
</FormItem>
)}
/>
)}
</>
)}
</SettingsFormCell>
</SettingsFormCell>
)}
</>
)}
</SettingsFormGrid>
</form>
</Form>

View File

@@ -2,10 +2,11 @@
import {
SettingsContainer,
SettingsFormCell,
SettingsFormGrid,
SettingsSection,
SettingsSectionBody,
SettingsSectionDescription,
SettingsSectionForm,
SettingsSectionHeader,
SettingsSectionTitle
} from "@app/components/Settings";
@@ -199,23 +200,25 @@ export function CreatePolicyForm({}: CreatePolicyFormProps) {
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm variant="half">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("name")}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</SettingsSectionForm>
<SettingsFormGrid>
<SettingsFormCell span="quarter">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("name")}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</SettingsFormCell>
</SettingsFormGrid>
</SettingsSectionBody>
</SettingsSection>

View File

@@ -1,6 +1,8 @@
"use client";
import {
SettingsFormCell,
SettingsFormGrid,
SettingsSection,
SettingsSectionBody,
SettingsSectionDescription,
@@ -138,45 +140,54 @@ export function EditPolicyNameSectionForm({
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm variant="half">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>{t("name")}</FormLabel>
<FormControl>
<Input
{...field}
disabled={readonly}
placeholder={t(
"resourcePolicyNamePlaceholder"
)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="niceId"
render={({ field }) => (
<FormItem>
<FormLabel>{t("identifier")}</FormLabel>
<FormControl>
<Input
{...field}
disabled={readonly}
placeholder={t(
"enterIdentifier"
)}
className="flex-1"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<SettingsFormGrid>
<SettingsFormCell span="half">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("name")}
</FormLabel>
<FormControl>
<Input
{...field}
disabled={readonly}
placeholder={t(
"resourcePolicyNamePlaceholder"
)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</SettingsFormCell>
<SettingsFormCell span="half">
<FormField
control={form.control}
name="niceId"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("identifier")}
</FormLabel>
<FormControl>
<Input
{...field}
disabled={readonly}
placeholder={t(
"enterIdentifier"
)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</SettingsFormCell>
</SettingsFormGrid>
</SettingsSectionForm>
</SettingsSectionBody>

View File

@@ -1,9 +1,18 @@
"use client";
import { SettingsSectionForm } from "@app/components/Settings";
import {
SettingsFormCell,
SettingsFormGrid,
SettingsSectionForm
} from "@app/components/Settings";
import { SwitchInput } from "@app/components/SwitchInput";
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 {
Select,
SelectContent,
@@ -49,92 +58,106 @@ export function PolicyAuthSsoSection({
const idpSelectDisabled = idpDisabled ?? disabled;
return (
<div className="space-y-4">
<SwitchInput
id="policy-auth-sso"
label={t("policyAuthSsoTitle")}
description={t("policyAuthSsoDescription")}
checked={sso}
disabled={disabled}
onCheckedChange={onSsoChange}
/>
<SettingsSectionForm variant="half">
<SettingsFormGrid>
<SettingsFormCell span="full">
<SwitchInput
id="policy-auth-sso"
label={t("policyAuthSsoTitle")}
description={t("policyAuthSsoDescription")}
checked={sso}
disabled={disabled}
onCheckedChange={onSsoChange}
/>
</SettingsFormCell>
{sso && (
<SettingsSectionForm className="max-w-none space-y-4">
<FormItem className="flex flex-col items-start">
<FormLabel>{t("roles")}</FormLabel>
{rolesEditor}
</FormItem>
<FormItem className="flex flex-col items-start">
<FormLabel>{t("users")}</FormLabel>
{usersEditor}
</FormItem>
{allIdps.length > 0 && (
<div className="space-y-2">
{skipToIdpId == null && !showIdpSelect ? (
<Button
type="button"
variant="text"
size="sm"
className="h-auto px-0"
disabled={idpSelectDisabled}
onClick={() => setShowIdpSelect(true)}
>
{t("policyAuthAddDefaultIdentityProvider")}
</Button>
) : (
<>
<label className="text-sm font-medium">
{t("defaultIdentityProvider")}
</label>
<Select
{sso && (
<>
<SettingsFormCell span="full">
<FormItem>
<FormLabel>{t("roles")}</FormLabel>
{rolesEditor}
</FormItem>
</SettingsFormCell>
<SettingsFormCell span="full">
<FormItem>
<FormLabel>{t("users")}</FormLabel>
{usersEditor}
</FormItem>
</SettingsFormCell>
{allIdps.length > 0 && (
<SettingsFormCell span="half">
{skipToIdpId == null && !showIdpSelect ? (
<Button
type="button"
variant="text"
size="sm"
className="h-auto px-0"
disabled={idpSelectDisabled}
onValueChange={(value) => {
if (value === "none") {
onSkipToIdpChange(null);
setShowIdpSelect(false);
return;
}
onSkipToIdpChange(parseInt(value));
}}
value={
skipToIdpId
? skipToIdpId.toString()
: "none"
}
onClick={() => setShowIdpSelect(true)}
>
<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(
"defaultIdentityProviderDescription"
"policyAuthAddDefaultIdentityProvider"
)}
</p>
</>
)}
</div>
)}
</SettingsSectionForm>
)}
</div>
</Button>
) : (
<FormItem>
<FormLabel>
{t("defaultIdentityProvider")}
</FormLabel>
<Select
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>
);
}

View File

@@ -158,82 +158,91 @@ export function PolicyAuthStackSectionCreate({
</SettingsFormCell>
</SettingsFormGrid>
<SettingsSubsectionHeader>
<SettingsSubsectionTitle>
{t("policyAuthOtherMethodsTitle")}
</SettingsSubsectionTitle>
<SettingsSubsectionDescription>
{t("policyAuthOtherMethodsDescription")}
</SettingsSubsectionDescription>
</SettingsSubsectionHeader>
<SettingsFormGrid>
<SettingsFormCell span="full">
<SettingsSubsectionHeader>
<SettingsSubsectionTitle>
{t("policyAuthOtherMethodsTitle")}
</SettingsSubsectionTitle>
<SettingsSubsectionDescription>
{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
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)
)
}
/>
<PolicyAuthMethodRow
id="passcode"
title={t("policyAuthPasscodeTitle")}
description={t("policyAuthPasscodeDescription")}
summary={getPasscodeSummary({ t })}
active={passcodeActive}
onConfigure={() => setEditingMethod("passcode")}
onToggle={(active) =>
handleToggle("passcode", active, () =>
parentForm.setValue("password", null)
)
}
/>
<PolicyAuthMethodRow
id="passcode"
title={t("policyAuthPasscodeTitle")}
description={t("policyAuthPasscodeDescription")}
summary={getPasscodeSummary({ t })}
active={passcodeActive}
onConfigure={() => setEditingMethod("passcode")}
onToggle={(active) =>
handleToggle("passcode", active, () =>
parentForm.setValue("password", null)
)
}
/>
<PolicyAuthMethodRow
id="email"
title={t("policyAuthEmailTitle")}
description={t("policyAuthEmailDescription")}
summary={getEmailWhitelistSummary({
t,
count: emails.length
})}
active={Boolean(emailWhitelistEnabled)}
onConfigure={() => setEditingMethod("email")}
onToggle={(active) =>
handleToggle("email", active, () =>
parentForm.setValue(
"emailWhitelistEnabled",
false
)
)
}
disabled={!emailEnabled}
/>
<PolicyAuthMethodRow
id="email"
title={t("policyAuthEmailTitle")}
description={t("policyAuthEmailDescription")}
summary={getEmailWhitelistSummary({
t,
count: emails.length
})}
active={Boolean(emailWhitelistEnabled)}
onConfigure={() => setEditingMethod("email")}
onToggle={(active) =>
handleToggle("email", active, () =>
parentForm.setValue(
"emailWhitelistEnabled",
false
)
)
}
disabled={!emailEnabled}
/>
<PolicyAuthMethodRow
id="header-auth"
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>
<PolicyAuthMethodRow
id="header-auth"
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>
</SettingsFormCell>
</SettingsFormGrid>
<PincodeCredenza
open={editingMethod === "pincode"}

View File

@@ -636,111 +636,146 @@ export function PolicyAuthStackSectionEdit({
</SettingsFormCell>
</SettingsFormGrid>
<SettingsSubsectionHeader>
<SettingsSubsectionTitle>
{t("policyAuthOtherMethodsTitle")}
</SettingsSubsectionTitle>
<SettingsSubsectionDescription>
{t("policyAuthOtherMethodsDescription")}
</SettingsSubsectionDescription>
</SettingsSubsectionHeader>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<PolicyAuthMethodRow
id="pincode"
title={t("policyAuthPincodeTitle")}
description={t(
"policyAuthPincodeDescription"
)}
summary={getPincodeSummary({ t })}
active={pinActive}
onConfigure={() =>
openMethodEditor("pincode")
}
onToggle={(active) =>
handleToggle("pincode", active, () => {
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
);
<SettingsFormGrid>
<SettingsFormCell span="full">
<SettingsSubsectionHeader>
<SettingsSubsectionTitle>
{t("policyAuthOtherMethodsTitle")}
</SettingsSubsectionTitle>
<SettingsSubsectionDescription>
{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={() =>
openMethodEditor("pincode")
}
)
}
disabled={authReadonly}
/>
</div>
onToggle={(active) =>
handleToggle(
"pincode",
active,
() => {
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>
<PincodeCredenza