mirror of
https://github.com/fosrl/pangolin.git
synced 2026-06-14 19:37:29 +00:00
Merge branch 'dev' into refactor/standardize-clear-buttons
This commit is contained in:
26
src/components/BrandedAuthSurface.tsx
Normal file
26
src/components/BrandedAuthSurface.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
"use client";
|
||||
|
||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||
|
||||
type BrandedAuthSurfaceProps = {
|
||||
primaryColor?: string | null;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export default function BrandedAuthSurface({
|
||||
primaryColor,
|
||||
children
|
||||
}: BrandedAuthSurfaceProps) {
|
||||
const { isUnlocked } = useLicenseStatusContext();
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
// @ts-expect-error CSS variable
|
||||
"--primary": isUnlocked() ? primaryColor : null
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -105,7 +105,6 @@ export function BrowserGatewayTargetForm(props: BrowserGatewayTargetFormProps) {
|
||||
{t("destination")}
|
||||
</label>
|
||||
<Input
|
||||
placeholder="192.168.1.1"
|
||||
value={props.destination}
|
||||
onChange={(e) =>
|
||||
props.onDestinationChange(e.target.value)
|
||||
@@ -116,7 +115,6 @@ export function BrowserGatewayTargetForm(props: BrowserGatewayTargetFormProps) {
|
||||
<label className="text-sm font-semibold">{t("port")}</label>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder={props.defaultPort.toString()}
|
||||
value={props.destinationPort}
|
||||
onChange={(e) =>
|
||||
props.onDestinationPortChange(e.target.value)
|
||||
|
||||
@@ -23,19 +23,22 @@ import {
|
||||
isHostname,
|
||||
type InternalResourceFormValues
|
||||
} from "./PrivateResourceForm";
|
||||
import type { Selectedsite } from "./site-selector";
|
||||
|
||||
type CreateInternalResourceDialogProps = {
|
||||
open: boolean;
|
||||
setOpen: (val: boolean) => void;
|
||||
orgId: string;
|
||||
onSuccess?: () => void;
|
||||
initialSites?: Selectedsite[];
|
||||
};
|
||||
|
||||
export default function CreatePrivateResourceDialog({
|
||||
open,
|
||||
setOpen,
|
||||
orgId,
|
||||
onSuccess
|
||||
onSuccess,
|
||||
initialSites
|
||||
}: CreateInternalResourceDialogProps) {
|
||||
const t = useTranslations();
|
||||
const api = createApiClient(useEnvContext());
|
||||
@@ -175,6 +178,7 @@ export default function CreatePrivateResourceDialog({
|
||||
formId="create-internal-resource-form"
|
||||
onSubmit={handleSubmit}
|
||||
onSubmitDisabledChange={setIsHttpModeDisabled}
|
||||
initialSites={initialSites}
|
||||
/>
|
||||
</CredenzaBody>
|
||||
<CredenzaFooter>
|
||||
|
||||
@@ -84,7 +84,7 @@ const CredenzaContent = ({ className, children, ...props }: CredenzaProps) => {
|
||||
return (
|
||||
<CredenzaContent
|
||||
className={cn(
|
||||
"flex min-h-0 max-h-[100dvh] flex-col overflow-y-auto md:top-[clamp(1.5rem,12vh,200px)] md:max-h-[calc(100vh-clamp(3rem,24vh,400px))] md:translate-y-0",
|
||||
"flex min-h-0 max-h-[100dvh] flex-col overflow-y-auto md:top-[clamp(1.5rem,12vh,200px)] md:max-h-[calc(100dvh-clamp(1.5rem,12vh,200px)-1.5rem)] md:translate-y-0 md:overflow-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,9 +14,10 @@ import {
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import Link from "next/link";
|
||||
import { replacePlaceholder } from "@app/lib/replacePlaceholder";
|
||||
import PoweredByPangolin from "@app/components/PoweredByPangolin";
|
||||
import BrandedAuthSurface from "@app/components/BrandedAuthSurface";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { pullEnv } from "@app/lib/pullEnv";
|
||||
import { build } from "@server/build";
|
||||
|
||||
type OrgLoginPageProps = {
|
||||
loginPage: LoadLoginPageResponse | undefined;
|
||||
@@ -52,22 +53,8 @@ export default async function OrgLoginPage({
|
||||
const env = pullEnv();
|
||||
const t = await getTranslations();
|
||||
return (
|
||||
<div>
|
||||
{build !== "enterprise" || !env.branding.hidePoweredBy ? (
|
||||
<div className="text-center mb-2">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{t("poweredBy")}{" "}
|
||||
<Link
|
||||
href="https://pangolin.net/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
{env.branding.appName || "Pangolin"}
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
<BrandedAuthSurface primaryColor={branding?.primaryColor ?? null}>
|
||||
<PoweredByPangolin />
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader>
|
||||
{branding?.logoUrl && (
|
||||
@@ -127,6 +114,6 @@ export default async function OrgLoginPage({
|
||||
{t("loginBack")}
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</BrandedAuthSurface>
|
||||
);
|
||||
}
|
||||
|
||||
53
src/components/PoweredByPangolin.tsx
Normal file
53
src/components/PoweredByPangolin.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { build } from "@server/build";
|
||||
|
||||
function PoweredByLabel({ brandName }: { brandName: string }) {
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
<div className="text-center mb-2">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{t("poweredBy")}{" "}
|
||||
{brandName === "Pangolin" ? (
|
||||
<Link
|
||||
href="https://pangolin.net/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
Pangolin
|
||||
</Link>
|
||||
) : (
|
||||
brandName
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PoweredByPangolin() {
|
||||
const { env } = useEnvContext();
|
||||
const { isUnlocked } = useLicenseStatusContext();
|
||||
|
||||
if (isUnlocked() && build === "enterprise") {
|
||||
if (
|
||||
env.branding.resourceAuthPage?.hidePoweredBy ||
|
||||
env.branding.hidePoweredBy
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<PoweredByLabel
|
||||
brandName={env.branding.appName || "Pangolin"}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <PoweredByLabel brandName="Pangolin" />;
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
SettingsSubsectionDescription,
|
||||
SettingsSubsectionHeader,
|
||||
SettingsSubsectionTitle
|
||||
} from "@app/components/Settings";
|
||||
import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
||||
import {
|
||||
OptionSelect,
|
||||
@@ -208,6 +213,7 @@ type InternalResourceFormProps = {
|
||||
formId: string;
|
||||
onSubmit: (values: InternalResourceFormValues) => void | Promise<void>;
|
||||
onSubmitDisabledChange?: (disabled: boolean) => void;
|
||||
initialSites?: Selectedsite[];
|
||||
};
|
||||
|
||||
export function PrivateResourceForm({
|
||||
@@ -218,7 +224,8 @@ export function PrivateResourceForm({
|
||||
siteResourceId,
|
||||
formId,
|
||||
onSubmit,
|
||||
onSubmitDisabledChange
|
||||
onSubmitDisabledChange,
|
||||
initialSites = []
|
||||
}: InternalResourceFormProps) {
|
||||
const t = useTranslations();
|
||||
const { env } = useEnvContext();
|
||||
@@ -609,6 +616,8 @@ export function PrivateResourceForm({
|
||||
authDaemonMode === "remote";
|
||||
const hasInitialized = useRef(false);
|
||||
const previousResourceId = useRef<number | null>(null);
|
||||
const initialSitesRef = useRef(initialSites);
|
||||
initialSitesRef.current = initialSites;
|
||||
|
||||
useEffect(() => {
|
||||
const tcpValue = getPortStringFromMode(tcpPortMode, tcpCustomPorts);
|
||||
@@ -623,9 +632,13 @@ export function PrivateResourceForm({
|
||||
// Reset when create dialog opens
|
||||
useEffect(() => {
|
||||
if (variant === "create" && open) {
|
||||
const prefillSites =
|
||||
initialSitesRef.current.length > 0
|
||||
? initialSitesRef.current
|
||||
: [];
|
||||
form.reset({
|
||||
name: "",
|
||||
siteIds: [],
|
||||
siteIds: prefillSites.map((s) => s.siteId),
|
||||
mode: "host",
|
||||
destination: "",
|
||||
alias: null,
|
||||
@@ -645,7 +658,7 @@ export function PrivateResourceForm({
|
||||
users: [],
|
||||
clients: []
|
||||
});
|
||||
setSelectedSites([]);
|
||||
setSelectedSites(prefillSites);
|
||||
setSshServerMode("native");
|
||||
setTcpPortMode("all");
|
||||
setUdpPortMode("all");
|
||||
@@ -1799,10 +1812,10 @@ export function PrivateResourceForm({
|
||||
/>
|
||||
|
||||
{/* Mode */}
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm font-semibold">
|
||||
<div className="space-y-2">
|
||||
<SettingsSubsectionTitle>
|
||||
{t("sshServerMode")}
|
||||
</p>
|
||||
</SettingsSubsectionTitle>
|
||||
<StrategySelect<"standard" | "native">
|
||||
value={sshServerMode}
|
||||
options={[
|
||||
@@ -1856,10 +1869,10 @@ export function PrivateResourceForm({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm font-semibold">
|
||||
<div className="space-y-2">
|
||||
<SettingsSubsectionTitle>
|
||||
{t("sshAuthenticationMethod")}
|
||||
</p>
|
||||
</SettingsSubsectionTitle>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="pamMode"
|
||||
@@ -1951,10 +1964,10 @@ export function PrivateResourceForm({
|
||||
|
||||
{/* Daemon Location (standard + push) */}
|
||||
{showDaemonLocation && (
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm font-semibold">
|
||||
<div className="space-y-2">
|
||||
<SettingsSubsectionTitle>
|
||||
{t("sshAuthDaemonLocation")}
|
||||
</p>
|
||||
</SettingsSubsectionTitle>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="authDaemonMode"
|
||||
@@ -2054,51 +2067,57 @@ export function PrivateResourceForm({
|
||||
|
||||
{/* Daemon Port (standard + push + remote) */}
|
||||
{showDaemonPort && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="authDaemonPort"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("sshDaemonPort")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={65535}
|
||||
placeholder="22123"
|
||||
disabled={
|
||||
sshSectionDisabled
|
||||
}
|
||||
value={field.value ?? ""}
|
||||
onChange={(e) => {
|
||||
if (sshSectionDisabled)
|
||||
return;
|
||||
const v =
|
||||
e.target.value;
|
||||
if (v === "") {
|
||||
field.onChange(
|
||||
null
|
||||
);
|
||||
return;
|
||||
<div className="w-full md:w-1/2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="authDaemonPort"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("sshDaemonPort")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={65535}
|
||||
placeholder="22123"
|
||||
disabled={
|
||||
sshSectionDisabled
|
||||
}
|
||||
const num = parseInt(
|
||||
v,
|
||||
10
|
||||
);
|
||||
field.onChange(
|
||||
Number.isNaN(num)
|
||||
? null
|
||||
: num
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
value={
|
||||
field.value ?? ""
|
||||
}
|
||||
onChange={(e) => {
|
||||
if (
|
||||
sshSectionDisabled
|
||||
)
|
||||
return;
|
||||
const v =
|
||||
e.target.value;
|
||||
if (v === "") {
|
||||
field.onChange(
|
||||
null
|
||||
);
|
||||
return;
|
||||
}
|
||||
const num =
|
||||
parseInt(v, 10);
|
||||
field.onChange(
|
||||
Number.isNaN(
|
||||
num
|
||||
)
|
||||
? null
|
||||
: num
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -744,7 +744,7 @@ function TargetStatusCell({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="flex items-center gap-2 h-8 px-2 font-normal"
|
||||
className="flex items-center gap-2 h-8 px-0 font-normal"
|
||||
>
|
||||
<StatusIcon status={overallStatus} />
|
||||
<span className="text-sm">
|
||||
|
||||
@@ -41,8 +41,9 @@ import {
|
||||
} from "@app/actions/server";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import Link from "next/link";
|
||||
import BrandingLogo from "@app/components/BrandingLogo";
|
||||
import BrandedAuthSurface from "@app/components/BrandedAuthSurface";
|
||||
import PoweredByPangolin from "@app/components/PoweredByPangolin";
|
||||
import { useSupporterStatusContext } from "@app/hooks/useSupporterStatusContext";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { build } from "@server/build";
|
||||
@@ -366,57 +367,20 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
||||
: 100;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
// @ts-expect-error CSS variable
|
||||
"--primary": isUnlocked() ? props.branding?.primaryColor : null
|
||||
}}
|
||||
>
|
||||
<BrandedAuthSurface primaryColor={props.branding?.primaryColor}>
|
||||
{!accessDenied ? (
|
||||
<div>
|
||||
{isUnlocked() && build === "enterprise" ? (
|
||||
!env.branding.resourceAuthPage?.hidePoweredBy &&
|
||||
!env.branding.hidePoweredBy && (
|
||||
<div className="text-center mb-2">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{t("poweredBy")}{" "}
|
||||
<Link
|
||||
href="https://pangolin.net/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
{env.branding.appName || "Pangolin"}
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="text-center mb-2">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{t("poweredBy")}{" "}
|
||||
<Link
|
||||
href="https://pangolin.net/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
Pangolin
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<PoweredByPangolin />
|
||||
<Card>
|
||||
<CardHeader>
|
||||
{isUnlocked() &&
|
||||
build !== "oss" &&
|
||||
(env.branding?.resourceAuthPage?.showLogo ||
|
||||
props.branding) && (
|
||||
props.branding?.logoUrl && (
|
||||
<div className="flex flex-row items-center justify-center mb-3">
|
||||
<BrandingLogo
|
||||
height={logoHeight}
|
||||
width={logoWidth}
|
||||
logoPath={props.branding?.logoUrl}
|
||||
logoPath={props.branding.logoUrl}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -790,6 +754,6 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
||||
) : (
|
||||
<ResourceAccessDenied />
|
||||
)}
|
||||
</div>
|
||||
</BrandedAuthSurface>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -186,7 +186,7 @@ export default function SetResourceHeaderAuthForm({
|
||||
label={t(
|
||||
"headerAuthCompatibility"
|
||||
)}
|
||||
info={t(
|
||||
description={t(
|
||||
"headerAuthCompatibilityInfo"
|
||||
)}
|
||||
checked={field.value}
|
||||
|
||||
@@ -63,6 +63,42 @@ export function SettingsSectionDescription({
|
||||
return <p className="text-muted-foreground text-sm">{children}</p>;
|
||||
}
|
||||
|
||||
export function SettingsSubsectionHeader({
|
||||
children,
|
||||
className
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return <div className={cn("space-y-0.5", className)}>{children}</div>;
|
||||
}
|
||||
|
||||
export function SettingsSubsectionTitle({
|
||||
children,
|
||||
className
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<h3 className={cn("text-sm font-semibold", className)}>{children}</h3>
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingsSubsectionDescription({
|
||||
children,
|
||||
className
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<p className={cn("text-sm text-muted-foreground", className)}>
|
||||
{children}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingsSectionBody({
|
||||
children
|
||||
}: {
|
||||
|
||||
@@ -43,8 +43,8 @@ export function SwitchInput({
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center space-x-2 mb-2">
|
||||
<div className="flex flex-col space-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
{label && (
|
||||
<Label
|
||||
htmlFor={id}
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { CreateOrEditLabelResponse } from "@server/routers/labels/types";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { AxiosResponse } from "axios";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useActionState, useMemo, useState } from "react";
|
||||
import { useActionState, useMemo, useRef, useState } from "react";
|
||||
import { useDebounce } from "use-debounce";
|
||||
import { Button } from "./ui/button";
|
||||
import { Checkbox } from "./ui/checkbox";
|
||||
@@ -88,6 +88,11 @@ export function LabelsSelector({
|
||||
colorValues[Math.floor(Math.random() * colorValues.length)];
|
||||
|
||||
const [, action, isPending] = useActionState(createLabel, null);
|
||||
const createFormRef = useRef<HTMLFormElement>(null);
|
||||
|
||||
const trimmedQuery = labelSearchQuery.trim();
|
||||
const canCreateLabel =
|
||||
trimmedQuery.length > 0 && labelsShown.length === 0 && !isPending;
|
||||
|
||||
async function createLabel(_: any, formData: FormData) {
|
||||
const name = formData.get("name")?.toString();
|
||||
@@ -120,21 +125,28 @@ export function LabelsSelector({
|
||||
return (
|
||||
<Command shouldFilter={false}>
|
||||
<CommandInput
|
||||
placeholder={t("labelSearch")}
|
||||
placeholder={t("labelSearchOrCreate")}
|
||||
value={labelSearchQuery}
|
||||
onValueChange={setlabelsSearchQuery}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && canCreateLabel) {
|
||||
e.preventDefault();
|
||||
createFormRef.current?.requestSubmit();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty className="px-3 break-all wrap-anywhere text-wrap">
|
||||
<CommandEmpty className="px-3 py-6 text-center text-wrap">
|
||||
{labelSearchQuery.trim().length > 0 ? (
|
||||
<div className="flex flex-col gap-2 items-center">
|
||||
<span className="max-w-34">
|
||||
<span className="max-w-34 break-words">
|
||||
{t("createNewLabel", {
|
||||
label: labelSearchQuery.trim()
|
||||
})}
|
||||
</span>
|
||||
|
||||
<form
|
||||
ref={createFormRef}
|
||||
action={action}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
@@ -159,14 +171,17 @@ export function LabelsSelector({
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<div
|
||||
className="size-4 rounded-full bg-(--color) flex-none"
|
||||
className="size-2 rounded-full bg-(--color) flex-none"
|
||||
style={{
|
||||
// @ts-expect-error css color
|
||||
"--color": value
|
||||
}}
|
||||
/>
|
||||
<span data-name>
|
||||
{color}
|
||||
{color
|
||||
.charAt(0)
|
||||
.toUpperCase() +
|
||||
color.slice(1)}
|
||||
</span>
|
||||
</SelectItem>
|
||||
)
|
||||
@@ -176,7 +191,6 @@ export function LabelsSelector({
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
loading={isPending}
|
||||
type="submit"
|
||||
>
|
||||
@@ -185,7 +199,14 @@ export function LabelsSelector({
|
||||
</form>
|
||||
</div>
|
||||
) : (
|
||||
t("labelsNotFound")
|
||||
<div className="flex flex-col gap-1 items-center">
|
||||
<span className="text-muted-foreground">
|
||||
{t("labelsNotFound")}
|
||||
</span>
|
||||
<span className="text-sm">
|
||||
{t("labelsEmptyCreateHint")}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
|
||||
@@ -10,7 +10,15 @@ import {
|
||||
import { CheckboxWithLabel } from "./ui/checkbox";
|
||||
import { OptionSelect, type OptionSelectOption } from "./OptionSelect";
|
||||
import { useState } from "react";
|
||||
import { FaApple, FaCubes, FaDocker, FaLinux, FaWindows } from "react-icons/fa";
|
||||
import {
|
||||
FaApple,
|
||||
FaCubes,
|
||||
FaDocker,
|
||||
FaHdd,
|
||||
FaLinux,
|
||||
FaWindows
|
||||
} from "react-icons/fa";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
import { SiKubernetes, SiNixos } from "react-icons/si";
|
||||
|
||||
export type CommandItem = string | { title: string; command: string };
|
||||
@@ -20,6 +28,7 @@ const PLATFORMS = [
|
||||
"macos",
|
||||
"docker",
|
||||
"kubernetes",
|
||||
"advantech",
|
||||
"podman",
|
||||
"nixos",
|
||||
"windows"
|
||||
@@ -43,11 +52,15 @@ export function NewtSiteInstallCommands({
|
||||
const t = useTranslations();
|
||||
|
||||
const [acceptClients, setAcceptClients] = useState(true);
|
||||
const [allowPangolinSsh, setAllowPangolinSsh] = useState(true);
|
||||
const [platform, setPlatform] = useState<Platform>("linux");
|
||||
const [architecture, setArchitecture] = useState(
|
||||
() => getArchitectures(platform)[0]
|
||||
);
|
||||
|
||||
const showSiteConfiguration = platform !== "advantech";
|
||||
const supportsSshOption = platform === "linux" || platform === "nixos";
|
||||
|
||||
const acceptClientsFlag = !acceptClients ? " --disable-clients" : "";
|
||||
const acceptClientsEnv = !acceptClients
|
||||
? "\n - DISABLE_CLIENTS=true"
|
||||
@@ -57,6 +70,11 @@ export function NewtSiteInstallCommands({
|
||||
--set newtInstances[0].acceptClients=true`
|
||||
: "";
|
||||
|
||||
const disableSshFlag =
|
||||
supportsSshOption && !allowPangolinSsh ? " --disable-ssh" : "";
|
||||
const runAsRootPrefix =
|
||||
supportsSshOption && allowPangolinSsh ? "sudo " : "";
|
||||
|
||||
const commandList: Record<Platform, Record<string, CommandItem[]>> = {
|
||||
linux: {
|
||||
Run: [
|
||||
@@ -66,7 +84,7 @@ export function NewtSiteInstallCommands({
|
||||
},
|
||||
{
|
||||
title: t("run"),
|
||||
command: `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
|
||||
command: `${runAsRootPrefix}newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}${disableSshFlag}`
|
||||
}
|
||||
],
|
||||
"Systemd Service": [
|
||||
@@ -86,6 +104,11 @@ PANGOLIN_ENDPOINT=${endpoint}${
|
||||
? `
|
||||
DISABLE_CLIENTS=true`
|
||||
: ""
|
||||
}${
|
||||
!allowPangolinSsh
|
||||
? `
|
||||
DISABLE_SSH=true`
|
||||
: ""
|
||||
}
|
||||
EOF
|
||||
sudo chmod 600 /etc/newt/newt.env`
|
||||
@@ -180,6 +203,9 @@ sudo systemctl enable --now newt`
|
||||
--set-string newtInstances[0].auth.existingSecretName="newt-main-tunnel-auth"${acceptClientsHelmValue}`
|
||||
]
|
||||
},
|
||||
advantech: {
|
||||
Documentation: []
|
||||
},
|
||||
podman: {
|
||||
"Podman Quadlet": [
|
||||
`[Unit]
|
||||
@@ -205,7 +231,7 @@ WantedBy=default.target`
|
||||
},
|
||||
nixos: {
|
||||
Flake: [
|
||||
`nix run 'nixpkgs#fosrl-newt' -- --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
|
||||
`${runAsRootPrefix}nix run 'nixpkgs#fosrl-newt' -- --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}${disableSshFlag}`
|
||||
]
|
||||
}
|
||||
};
|
||||
@@ -257,45 +283,87 @@ WantedBy=default.target`
|
||||
className="mt-4"
|
||||
/>
|
||||
|
||||
<div className="pt-4">
|
||||
<p className="font-semibold mb-3">
|
||||
{t("siteConfiguration")}
|
||||
</p>
|
||||
<div className="flex items-center space-x-2 mb-2">
|
||||
<CheckboxWithLabel
|
||||
id="acceptClients"
|
||||
aria-describedby="acceptClients-desc"
|
||||
checked={acceptClients}
|
||||
onCheckedChange={(checked) => {
|
||||
const value = checked as boolean;
|
||||
setAcceptClients(value);
|
||||
}}
|
||||
label={t("siteAcceptClientConnections")}
|
||||
/>
|
||||
{showSiteConfiguration && (
|
||||
<div className="pt-4">
|
||||
<p className="font-semibold mb-3">
|
||||
{t("siteConfiguration")}
|
||||
</p>
|
||||
<div className="flex items-center space-x-2 mb-2">
|
||||
<CheckboxWithLabel
|
||||
id="acceptClients"
|
||||
aria-describedby="acceptClients-desc"
|
||||
checked={acceptClients}
|
||||
onCheckedChange={(checked) => {
|
||||
const value = checked as boolean;
|
||||
setAcceptClients(value);
|
||||
}}
|
||||
label={t("siteAcceptClientConnections")}
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
id="acceptClients-desc"
|
||||
className="text-sm text-muted-foreground"
|
||||
>
|
||||
{t("siteAcceptClientConnectionsDescription")}
|
||||
</p>
|
||||
{supportsSshOption && (
|
||||
<>
|
||||
<div className="flex items-center space-x-2 mb-2 mt-2">
|
||||
<CheckboxWithLabel
|
||||
id="allowPangolinSsh"
|
||||
checked={allowPangolinSsh}
|
||||
onCheckedChange={(checked) => {
|
||||
const value = checked as boolean;
|
||||
setAllowPangolinSsh(value);
|
||||
}}
|
||||
label="Allow Pangolin SSH"
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
id="allowPangolinSsh-desc"
|
||||
className="text-sm text-muted-foreground"
|
||||
>
|
||||
{t("sitePangolinSshDescription")}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<p
|
||||
id="acceptClients-desc"
|
||||
className="text-sm text-muted-foreground"
|
||||
>
|
||||
{t("siteAcceptClientConnectionsDescription")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="pt-4">
|
||||
<p className="font-semibold mb-3">{t("commands")}</p>
|
||||
{platform === "kubernetes" && (
|
||||
<p className="text-sm text-muted-foreground mb-3">
|
||||
For more and up to date Kubernetes installation
|
||||
information, see{" "}
|
||||
<a
|
||||
href="https://docs.pangolin.net/manage/sites/install-kubernetes"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
docs.pangolin.net/manage/sites/install-kubernetes
|
||||
</a>
|
||||
.
|
||||
{t.rich("siteInstallKubernetesDocsDescription", {
|
||||
docsLink: (chunks) => (
|
||||
<a
|
||||
href="https://docs.pangolin.net/manage/sites/install-kubernetes"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline inline-flex items-center gap-1"
|
||||
>
|
||||
{chunks}
|
||||
<ExternalLink className="size-3.5 shrink-0" />
|
||||
</a>
|
||||
)
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
{platform === "advantech" && (
|
||||
<p className="text-sm text-muted-foreground mb-3">
|
||||
{t.rich("siteInstallAdvantechDocsDescription", {
|
||||
docsLink: (chunks) => (
|
||||
<a
|
||||
href="https://docs.pangolin.net/manage/sites/install-advantech"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline inline-flex items-center gap-1"
|
||||
>
|
||||
{chunks}
|
||||
<ExternalLink className="size-3.5 shrink-0" />
|
||||
</a>
|
||||
)
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
<div className="mt-2 space-y-3">
|
||||
@@ -342,6 +410,8 @@ function getPlatformIcon(platformName: Platform) {
|
||||
return <FaDocker className="h-4 w-4 mr-2" />;
|
||||
case "kubernetes":
|
||||
return <SiKubernetes className="h-4 w-4 mr-2" />;
|
||||
case "advantech":
|
||||
return <FaHdd className="h-4 w-4 mr-2" />;
|
||||
case "podman":
|
||||
return <FaCubes className="h-4 w-4 mr-2" />;
|
||||
case "nixos":
|
||||
@@ -363,6 +433,8 @@ function getPlatformName(platformName: Platform) {
|
||||
return "Docker";
|
||||
case "kubernetes":
|
||||
return "Kubernetes";
|
||||
case "advantech":
|
||||
return "Advantech";
|
||||
case "podman":
|
||||
return "Podman";
|
||||
case "nixos":
|
||||
@@ -384,6 +456,8 @@ function getArchitectures(platform: Platform) {
|
||||
return ["Docker Compose", "Docker Run"];
|
||||
case "kubernetes":
|
||||
return ["Helm Chart"];
|
||||
case "advantech":
|
||||
return ["Documentation"];
|
||||
case "podman":
|
||||
return ["Podman Quadlet", "Podman Run"];
|
||||
case "nixos":
|
||||
|
||||
@@ -385,7 +385,7 @@ export function CreatePolicyAuthMethodsSectionForm({
|
||||
label={t(
|
||||
"headerAuthCompatibility"
|
||||
)}
|
||||
info={t(
|
||||
description={t(
|
||||
"headerAuthCompatibilityInfo"
|
||||
)}
|
||||
checked={field.value}
|
||||
@@ -426,7 +426,10 @@ export function CreatePolicyAuthMethodsSectionForm({
|
||||
{/* Password row */}
|
||||
<div className="flex items-center justify-between border rounded-md p-2 mb-4">
|
||||
<div
|
||||
className={cn("flex items-center text-sm space-x-2", password && "text-green-500")}
|
||||
className={cn(
|
||||
"flex items-center text-sm space-x-2",
|
||||
password && "text-green-500"
|
||||
)}
|
||||
>
|
||||
<Key size="14" />
|
||||
<span>
|
||||
@@ -456,7 +459,10 @@ export function CreatePolicyAuthMethodsSectionForm({
|
||||
{/* Pincode row */}
|
||||
<div className="flex items-center justify-between border rounded-md p-2">
|
||||
<div
|
||||
className={cn("flex items-center space-x-2 text-sm", pincode && "text-green-500")}
|
||||
className={cn(
|
||||
"flex items-center space-x-2 text-sm",
|
||||
pincode && "text-green-500"
|
||||
)}
|
||||
>
|
||||
<Binary size="14" />
|
||||
<span>
|
||||
@@ -484,7 +490,10 @@ export function CreatePolicyAuthMethodsSectionForm({
|
||||
{/* Header auth row */}
|
||||
<div className="flex items-center justify-between border rounded-md p-2">
|
||||
<div
|
||||
className={cn("flex items-center space-x-2 text-sm", headerAuth && "text-green-500")}
|
||||
className={cn(
|
||||
"flex items-center space-x-2 text-sm",
|
||||
headerAuth && "text-green-500"
|
||||
)}
|
||||
>
|
||||
<Bot size="14" />
|
||||
<span>
|
||||
|
||||
@@ -491,7 +491,7 @@ export function EditPolicyAuthMethodsSectionForm({
|
||||
label={t(
|
||||
"headerAuthCompatibility"
|
||||
)}
|
||||
info={t(
|
||||
description={t(
|
||||
"headerAuthCompatibilityInfo"
|
||||
)}
|
||||
checked={field.value}
|
||||
|
||||
@@ -61,6 +61,11 @@ import {
|
||||
|
||||
import { MAJOR_ASNS } from "@server/db/asns";
|
||||
import { COUNTRIES } from "@server/db/countries";
|
||||
import {
|
||||
REGIONS,
|
||||
getRegionNameById,
|
||||
isValidRegionId
|
||||
} from "@server/db/regions";
|
||||
import {
|
||||
isValidCIDR,
|
||||
isValidIP,
|
||||
@@ -210,6 +215,8 @@ export function EditPolicyRulesSectionForm({
|
||||
const [openAddRuleCountrySelect, setOpenAddRuleCountrySelect] =
|
||||
useState(false);
|
||||
const [openAddRuleAsnSelect, setOpenAddRuleAsnSelect] = useState(false);
|
||||
const [openAddRuleRegionSelect, setOpenAddRuleRegionSelect] =
|
||||
useState(false);
|
||||
|
||||
const addRuleForm = useForm({
|
||||
resolver: zodResolver(addRuleSchema),
|
||||
@@ -235,7 +242,8 @@ export function EditPolicyRulesSectionForm({
|
||||
IP: "IP",
|
||||
CIDR: t("ipAddressRange"),
|
||||
COUNTRY: t("country"),
|
||||
ASN: "ASN"
|
||||
ASN: "ASN",
|
||||
REGION: t("region")
|
||||
}),
|
||||
[t]
|
||||
);
|
||||
@@ -309,6 +317,14 @@ export function EditPolicyRulesSectionForm({
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (data.match === "REGION" && !isValidRegionId(data.value)) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t("rulesErrorInvalidRegion"),
|
||||
description: t("rulesErrorInvalidRegionDescription") || ""
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let priority = data.priority;
|
||||
if (priority === undefined) {
|
||||
@@ -378,6 +394,8 @@ export function EditPolicyRulesSectionForm({
|
||||
return t("rulesMatchCountry");
|
||||
case "ASN":
|
||||
return "Enter an Autonomous System Number (e.g., AS15169 or 15169)";
|
||||
case "REGION":
|
||||
return t("rulesMatchRegion");
|
||||
}
|
||||
},
|
||||
[t]
|
||||
@@ -476,7 +494,13 @@ export function EditPolicyRulesSectionForm({
|
||||
defaultValue={row.original.match}
|
||||
disabled={readonly || row.original.fromPolicy}
|
||||
onValueChange={(
|
||||
value: "CIDR" | "IP" | "PATH" | "COUNTRY" | "ASN"
|
||||
value:
|
||||
| "CIDR"
|
||||
| "IP"
|
||||
| "PATH"
|
||||
| "COUNTRY"
|
||||
| "ASN"
|
||||
| "REGION"
|
||||
) =>
|
||||
updateRule(row.original.ruleId, {
|
||||
match: value,
|
||||
@@ -485,7 +509,9 @@ export function EditPolicyRulesSectionForm({
|
||||
? "US"
|
||||
: value === "ASN"
|
||||
? "AS15169"
|
||||
: row.original.value
|
||||
: value === "REGION"
|
||||
? "021"
|
||||
: row.original.value
|
||||
})
|
||||
}
|
||||
>
|
||||
@@ -505,6 +531,11 @@ export function EditPolicyRulesSectionForm({
|
||||
{RuleMatch.COUNTRY}
|
||||
</SelectItem>
|
||||
)}
|
||||
{isMaxmindAvailable && (
|
||||
<SelectItem value="REGION">
|
||||
{RuleMatch.REGION}
|
||||
</SelectItem>
|
||||
)}
|
||||
{isMaxmindAsnAvailable && (
|
||||
<SelectItem value="ASN">
|
||||
{RuleMatch.ASN}
|
||||
@@ -666,6 +697,111 @@ export function EditPolicyRulesSectionForm({
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : row.original.match === "REGION" ? (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
disabled={
|
||||
readonly || row.original.fromPolicy
|
||||
}
|
||||
className="min-w-50 justify-between"
|
||||
>
|
||||
{(() => {
|
||||
const regionName = getRegionNameById(
|
||||
row.original.value
|
||||
);
|
||||
if (!regionName) {
|
||||
return t("selectRegion");
|
||||
}
|
||||
return `${t(regionName)} (${row.original.value})`;
|
||||
})()}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="min-w-50 p-0">
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder={t("searchRegions")}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>
|
||||
{t("noRegionFound")}
|
||||
</CommandEmpty>
|
||||
{REGIONS.map((continent) => (
|
||||
<CommandGroup
|
||||
key={continent.id}
|
||||
heading={t(continent.name)}
|
||||
>
|
||||
<CommandItem
|
||||
value={continent.id}
|
||||
keywords={[
|
||||
t(continent.name),
|
||||
continent.id
|
||||
]}
|
||||
onSelect={() =>
|
||||
updateRule(
|
||||
row.original.ruleId,
|
||||
{
|
||||
value: continent.id
|
||||
}
|
||||
)
|
||||
}
|
||||
>
|
||||
<Check
|
||||
className={`mr-2 h-4 w-4 ${
|
||||
row.original
|
||||
.value ===
|
||||
continent.id
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
}`}
|
||||
/>
|
||||
{t(continent.name)} (
|
||||
{continent.id})
|
||||
</CommandItem>
|
||||
{continent.includes.map(
|
||||
(subregion) => (
|
||||
<CommandItem
|
||||
key={subregion.id}
|
||||
value={subregion.id}
|
||||
keywords={[
|
||||
t(
|
||||
subregion.name
|
||||
),
|
||||
subregion.id
|
||||
]}
|
||||
onSelect={() =>
|
||||
updateRule(
|
||||
row.original
|
||||
.ruleId,
|
||||
{
|
||||
value: subregion.id
|
||||
}
|
||||
)
|
||||
}
|
||||
>
|
||||
<Check
|
||||
className={`mr-2 h-4 w-4 ${
|
||||
row.original
|
||||
.value ===
|
||||
subregion.id
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
}`}
|
||||
/>
|
||||
{t(subregion.name)}{" "}
|
||||
({subregion.id})
|
||||
</CommandItem>
|
||||
)
|
||||
)}
|
||||
</CommandGroup>
|
||||
))}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : (
|
||||
<Input
|
||||
defaultValue={row.original.value}
|
||||
@@ -988,6 +1124,13 @@ export function EditPolicyRulesSectionForm({
|
||||
}
|
||||
</SelectItem>
|
||||
)}
|
||||
{isMaxmindAvailable && (
|
||||
<SelectItem value="REGION">
|
||||
{
|
||||
RuleMatch.REGION
|
||||
}
|
||||
</SelectItem>
|
||||
)}
|
||||
{isMaxmindAsnAvailable && (
|
||||
<SelectItem value="ASN">
|
||||
{RuleMatch.ASN}
|
||||
@@ -1240,6 +1383,160 @@ export function EditPolicyRulesSectionForm({
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : addRuleForm.watch(
|
||||
"match"
|
||||
) === "REGION" ? (
|
||||
<Popover
|
||||
open={
|
||||
openAddRuleRegionSelect
|
||||
}
|
||||
onOpenChange={
|
||||
setOpenAddRuleRegionSelect
|
||||
}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
disabled={
|
||||
readonly ||
|
||||
(!isResourceOverlay &&
|
||||
!rulesEnabled)
|
||||
}
|
||||
aria-expanded={
|
||||
openAddRuleRegionSelect
|
||||
}
|
||||
className="w-full justify-between"
|
||||
>
|
||||
{field.value
|
||||
? (() => {
|
||||
const regionName =
|
||||
getRegionNameById(
|
||||
field.value
|
||||
);
|
||||
return regionName
|
||||
? `${t(regionName)} (${field.value})`
|
||||
: field.value;
|
||||
})()
|
||||
: t(
|
||||
"selectRegion"
|
||||
)}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-full p-0">
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder={t(
|
||||
"searchRegions"
|
||||
)}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>
|
||||
{t(
|
||||
"noRegionFound"
|
||||
)}
|
||||
</CommandEmpty>
|
||||
{REGIONS.map(
|
||||
(
|
||||
continent
|
||||
) => (
|
||||
<CommandGroup
|
||||
key={
|
||||
continent.id
|
||||
}
|
||||
heading={t(
|
||||
continent.name
|
||||
)}
|
||||
>
|
||||
<CommandItem
|
||||
value={
|
||||
continent.id
|
||||
}
|
||||
keywords={[
|
||||
t(
|
||||
continent.name
|
||||
),
|
||||
continent.id
|
||||
]}
|
||||
onSelect={() => {
|
||||
field.onChange(
|
||||
continent.id
|
||||
);
|
||||
setOpenAddRuleRegionSelect(
|
||||
false
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={`mr-2 h-4 w-4 ${
|
||||
field.value ===
|
||||
continent.id
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
}`}
|
||||
/>
|
||||
{t(
|
||||
continent.name
|
||||
)}{" "}
|
||||
(
|
||||
{
|
||||
continent.id
|
||||
}
|
||||
)
|
||||
</CommandItem>
|
||||
{continent.includes.map(
|
||||
(
|
||||
subregion
|
||||
) => (
|
||||
<CommandItem
|
||||
key={
|
||||
subregion.id
|
||||
}
|
||||
value={
|
||||
subregion.id
|
||||
}
|
||||
keywords={[
|
||||
t(
|
||||
subregion.name
|
||||
),
|
||||
subregion.id
|
||||
]}
|
||||
onSelect={() => {
|
||||
field.onChange(
|
||||
subregion.id
|
||||
);
|
||||
setOpenAddRuleRegionSelect(
|
||||
false
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={`mr-2 h-4 w-4 ${
|
||||
field.value ===
|
||||
subregion.id
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
}`}
|
||||
/>
|
||||
{t(
|
||||
subregion.name
|
||||
)}{" "}
|
||||
(
|
||||
{
|
||||
subregion.id
|
||||
}
|
||||
)
|
||||
</CommandItem>
|
||||
)
|
||||
)}
|
||||
</CommandGroup>
|
||||
)
|
||||
)}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : (
|
||||
<Input
|
||||
{...field}
|
||||
|
||||
@@ -670,7 +670,7 @@ export function PolicyAuthMethodsSection({
|
||||
label={t(
|
||||
"headerAuthCompatibility"
|
||||
)}
|
||||
info={t(
|
||||
description={t(
|
||||
"headerAuthCompatibilityInfo"
|
||||
)}
|
||||
checked={field.value}
|
||||
|
||||
@@ -38,7 +38,7 @@ const DialogContent = React.forwardRef<
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-card px-6 pt-6 pb-4 duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 sm:rounded-lg",
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-card px-6 pt-6 pb-4 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 sm:rounded-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
Reference in New Issue
Block a user