mirror of
https://github.com/fosrl/pangolin.git
synced 2026-08-19 02:32:56 +02:00
form layout improvements
This commit is contained in:
+4
-2
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user