Merge dev into fix/log-analytics-adjustments

This commit is contained in:
Fred KISSIE
2025-12-10 03:19:14 +01:00
parent 9db2feff77
commit d490cab48c
555 changed files with 9375 additions and 9287 deletions

View File

@@ -14,21 +14,21 @@ export default function AccessPageHeaderAndNav({
hasInvitations
}: AccessPageHeaderAndNavProps) {
const t = useTranslations();
const navItems = [
{
title: t('users'),
title: t("users"),
href: `/{orgId}/settings/access/users`
},
{
title: t('roles'),
title: t("roles"),
href: `/{orgId}/settings/access/roles`
}
];
if (hasInvitations) {
navItems.push({
title: t('invite'),
title: t("invite"),
href: `/{orgId}/settings/access/invitations`
});
}
@@ -36,13 +36,11 @@ export default function AccessPageHeaderAndNav({
return (
<>
<SettingsSectionTitle
title={t('accessUsersRoles')}
description={t('accessUsersRolesDescription')}
title={t("accessUsersRoles")}
description={t("accessUsersRolesDescription")}
/>
<HorizontalTabs items={navItems}>
{children}
</HorizontalTabs>
<HorizontalTabs items={navItems}>{children}</HorizontalTabs>
</>
);
}

View File

@@ -20,10 +20,7 @@ type AccessTokenProps = {
resourceId?: number;
};
export default function AccessToken({
token,
resourceId
}: AccessTokenProps) {
export default function AccessToken({ token, resourceId }: AccessTokenProps) {
const [loading, setLoading] = useState(true);
const [isValid, setIsValid] = useState(false);
@@ -79,7 +76,7 @@ export default function AccessToken({
);
}
} catch (e) {
console.error(t('accessTokenError'), e);
console.error(t("accessTokenError"), e);
} finally {
setLoading(false);
}
@@ -102,7 +99,7 @@ export default function AccessToken({
);
}
} catch (e) {
console.error(t('accessTokenError'), e);
console.error(t("accessTokenError"), e);
} finally {
setLoading(false);
}
@@ -118,26 +115,22 @@ export default function AccessToken({
function renderTitle() {
if (isValid) {
return t('accessGranted');
return t("accessGranted");
} else {
return t('accessUrlInvalid');
return t("accessUrlInvalid");
}
}
function renderContent() {
if (isValid) {
return (
<div>
{t('accessGrantedDescription')}
</div>
);
return <div>{t("accessGrantedDescription")}</div>;
} else {
return (
<div>
{t('accessUrlInvalidDescription')}
{t("accessUrlInvalidDescription")}
<div className="text-center mt-4">
<Button>
<Link href="/">{t('goHome')}</Link>
<Link href="/">{t("goHome")}</Link>
</Button>
</div>
</div>

View File

@@ -44,31 +44,35 @@ export default function AccessTokenSection({
<>
<div className="flex items-start space-x-2">
<p className="text-sm text-muted-foreground">
{t('shareTokenDescription')}
{t("shareTokenDescription")}
</p>
</div>
<Tabs defaultValue="token" className="w-full mt-4">
<TabsList className="grid grid-cols-2">
<TabsTrigger value="token">{t('accessToken')}</TabsTrigger>
<TabsTrigger value="usage">{t('usageExamples')}</TabsTrigger>
<TabsTrigger value="token">{t("accessToken")}</TabsTrigger>
<TabsTrigger value="usage">
{t("usageExamples")}
</TabsTrigger>
</TabsList>
<TabsContent value="token" className="space-y-4">
<div className="space-y-1">
<div className="font-bold">{t('tokenId')}</div>
<div className="font-bold">{t("tokenId")}</div>
<CopyToClipboard text={tokenId} isLink={false} />
</div>
<div className="space-y-1">
<div className="font-bold">{t('token')}</div>
<div className="font-bold">{t("token")}</div>
<CopyToClipboard text={token} isLink={false} />
</div>
</TabsContent>
<TabsContent value="usage" className="space-y-4">
<div className="space-y-2">
<h3 className="text-sm font-medium">{t('requestHeades')}</h3>
<h3 className="text-sm font-medium">
{t("requestHeades")}
</h3>
<CopyTextBox
text={`${env.server.resourceAccessTokenHeadersId}: ${tokenId}
${env.server.resourceAccessTokenHeadersToken}: ${token}`}
@@ -76,7 +80,9 @@ ${env.server.resourceAccessTokenHeadersToken}: ${token}`}
</div>
<div className="space-y-2">
<h3 className="text-sm font-medium">{t('queryParameter')}</h3>
<h3 className="text-sm font-medium">
{t("queryParameter")}
</h3>
<CopyTextBox
text={`${resourceUrl}?${env.server.resourceAccessTokenParam}=${tokenId}.${token}`}
/>
@@ -85,17 +91,17 @@ ${env.server.resourceAccessTokenHeadersToken}: ${token}`}
<Alert variant="neutral">
<InfoIcon className="h-4 w-4" />
<AlertTitle className="font-semibold">
{t('importantNote')}
{t("importantNote")}
</AlertTitle>
<AlertDescription>
{t('shareImportantDescription')}
{t("shareImportantDescription")}
</AlertDescription>
</Alert>
</TabsContent>
</Tabs>
<div className="text-sm text-muted-foreground mt-4">
{t('shareTokenSecurety')}
{t("shareTokenSecurety")}
</div>
</>
);

View File

@@ -26,10 +26,10 @@ export function IdpDataTable<TData, TValue>({
columns={columns}
data={data}
persistPageSize="idp-table"
title={t('idp')}
searchPlaceholder={t('idpSearch')}
title={t("idp")}
searchPlaceholder={t("idpSearch")}
searchColumn="name"
addButtonText={t('idpAdd')}
addButtonText={t("idpAdd")}
onAdd={() => {
router.push("/admin/idp/create");
}}

View File

@@ -175,9 +175,7 @@ export default function IdpTable({ idps }: Props) {
</DropdownMenuContent>
</DropdownMenu>
<Link href={`/admin/idp/${siteRow.idpId}/general`}>
<Button
variant={"outline"}
>
<Button variant={"outline"}>
{t("edit")}
<ArrowRight className="ml-2 w-4 h-4" />
</Button>

View File

@@ -1,8 +1,6 @@
"use client";
import {
ColumnDef,
} from "@tanstack/react-table";
import { ColumnDef } from "@tanstack/react-table";
import { DataTable } from "@app/components/ui/data-table";
import { useTranslations } from "next-intl";
@@ -19,7 +17,6 @@ export function UsersDataTable<TData, TValue>({
onRefresh,
isRefreshing
}: DataTableProps<TData, TValue>) {
const t = useTranslations();
return (
@@ -27,8 +24,8 @@ export function UsersDataTable<TData, TValue>({
columns={columns}
data={data}
persistPageSize="userServer-table"
title={t('userServer')}
searchPlaceholder={t('userSearch')}
title={t("userServer")}
searchPlaceholder={t("userSearch")}
searchColumn="email"
onRefresh={onRefresh}
isRefreshing={isRefreshing}

View File

@@ -44,7 +44,6 @@ export function ApiKeysDataTable<TData, TValue>({
onRefresh,
isRefreshing
}: DataTableProps<TData, TValue>) {
const t = useTranslations();
return (
@@ -52,11 +51,11 @@ export function ApiKeysDataTable<TData, TValue>({
columns={columns}
data={data}
persistPageSize="apiKeys-table"
title={t('apiKeys')}
searchPlaceholder={t('searchApiKeys')}
title={t("apiKeys")}
searchPlaceholder={t("searchApiKeys")}
searchColumn="name"
onAdd={addApiKey}
addButtonText={t('apiKeysAdd')}
addButtonText={t("apiKeysAdd")}
onRefresh={onRefresh}
isRefreshing={isRefreshing}
enableColumnVisibility={true}

View File

@@ -108,7 +108,7 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) {
{
accessorKey: "key",
friendlyName: t("key"),
header: () => (<span className="p-3">{t("key")}</span>),
header: () => <span className="p-3">{t("key")}</span>,
cell: ({ row }) => {
const r = row.original;
return <span className="font-mono">{r.key}</span>;
@@ -117,7 +117,7 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) {
{
accessorKey: "createdAt",
friendlyName: t("createdAt"),
header: () => (<span className="p-3">{t("createdAt")}</span>),
header: () => <span className="p-3">{t("createdAt")}</span>,
cell: ({ row }) => {
const r = row.original;
return <span>{moment(r.createdAt).format("lll")} </span>;
@@ -161,9 +161,7 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) {
</DropdownMenuContent>
</DropdownMenu>
<Link href={`/admin/api-keys/${r.id}`}>
<Button
variant={"outline"}
>
<Button variant={"outline"}>
{t("edit")}
<ArrowRight className="ml-2 w-4 h-4" />
</Button>

View File

@@ -67,7 +67,8 @@ export default function AutoLoginHandler({
console.error("Failed to generate OIDC URL:", e);
setError(
t("autoLoginErrorGeneratingUrl", {
defaultValue: "An unexpected error occurred. Please try again."
defaultValue:
"An unexpected error occurred. Please try again."
})
);
} finally {

View File

@@ -38,7 +38,7 @@ export default function BrandingLogo(props: BrandingLogoProps) {
if (isUnlocked() && env.branding.logo?.darkPath) {
return env.branding.logo.darkPath;
}
return "/logo/word_mark_white.png";
return "/logo/word_mark_white.png";
}
const path = getPath();

View File

@@ -20,7 +20,10 @@ type ChangePasswordDialogProps = {
setOpen: (val: boolean) => void;
};
export default function ChangePasswordDialog({ open, setOpen }: ChangePasswordDialogProps) {
export default function ChangePasswordDialog({
open,
setOpen
}: ChangePasswordDialogProps) {
const t = useTranslations();
const [currentStep, setCurrentStep] = useState(1);
const [loading, setLoading] = useState(false);
@@ -47,18 +50,16 @@ export default function ChangePasswordDialog({ open, setOpen }: ChangePasswordDi
>
<CredenzaContent>
<CredenzaHeader>
<CredenzaTitle>
{t('changePassword')}
</CredenzaTitle>
<CredenzaTitle>{t("changePassword")}</CredenzaTitle>
<CredenzaDescription>
{t('changePasswordDescription')}
{t("changePasswordDescription")}
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
<ChangePasswordForm
ref={formRef}
isDialog={true}
submitButtonText={t('submit')}
submitButtonText={t("submit")}
cancelButtonText="Close"
showCancelButton={false}
onComplete={() => setOpen(false)}
@@ -77,7 +78,7 @@ export default function ChangePasswordDialog({ open, setOpen }: ChangePasswordDi
disabled={loading}
onClick={handleSubmit}
>
{t('submit')}
{t("submit")}
</Button>
)}
</CredenzaFooter>

View File

@@ -22,11 +22,7 @@ import {
import { toast } from "@app/hooks/useToast";
import { formatAxiosError } from "@app/lib/api";
import { useTranslations } from "next-intl";
import {
InputOTP,
InputOTPGroup,
InputOTPSlot
} from "./ui/input-otp";
import { InputOTP, InputOTPGroup, InputOTPSlot } from "./ui/input-otp";
import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp";
import { ChangePasswordResponse } from "@server/routers/auth";
import { cn } from "@app/lib/cn";
@@ -114,14 +110,22 @@ const ChangePasswordForm = forwardRef<
onLoadingChange?.(loading);
}, [loading, onLoadingChange]);
const passwordSchema = z.object({
oldPassword: z.string().min(1, { message: t("passwordRequired") }),
newPassword: z.string().min(8, { message: t("passwordRequirementsChars") }),
confirmPassword: z.string().min(1, { message: t("passwordRequired") })
}).refine((data) => data.newPassword === data.confirmPassword, {
message: t("passwordsDoNotMatch"),
path: ["confirmPassword"],
});
const passwordSchema = z
.object({
oldPassword: z
.string()
.min(1, { message: t("passwordRequired") }),
newPassword: z
.string()
.min(8, { message: t("passwordRequirementsChars") }),
confirmPassword: z
.string()
.min(1, { message: t("passwordRequired") })
})
.refine((data) => data.newPassword === data.confirmPassword, {
message: t("passwordsDoNotMatch"),
path: ["confirmPassword"]
});
const mfaSchema = z.object({
code: z.string().length(6, { message: t("pincodeInvalid") })
@@ -143,11 +147,13 @@ const ChangePasswordForm = forwardRef<
}
});
const changePassword = async (values: z.infer<typeof passwordSchema>) => {
const changePassword = async (
values: z.infer<typeof passwordSchema>
) => {
setLoading(true);
const endpoint = `/auth/change-password`;
const payload = {
const payload = {
oldPassword: values.oldPassword,
newPassword: values.newPassword
};
@@ -181,7 +187,7 @@ const ChangePasswordForm = forwardRef<
const endpoint = `/auth/change-password`;
const passwordValues = passwordForm.getValues();
const payload = {
const payload = {
oldPassword: passwordValues.oldPassword,
newPassword: passwordValues.newPassword,
code: values.code
@@ -303,7 +309,9 @@ const ChangePasswordForm = forwardRef<
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm font-medium text-foreground">
{t("passwordStrength")}
{t(
"passwordStrength"
)}
</span>
<span
className={cn(
@@ -335,7 +343,9 @@ const ChangePasswordForm = forwardRef<
{/* Requirements Checklist */}
<div className="bg-muted rounded-lg p-3 space-y-2">
<div className="text-sm font-medium text-foreground mb-2">
{t("passwordRequirements")}
{t(
"passwordRequirements"
)}
</div>
<div className="grid grid-cols-1 gap-1.5">
<div className="flex items-center gap-2">
@@ -505,13 +515,14 @@ const ChangePasswordForm = forwardRef<
{confirmPasswordValue.length > 0 &&
!doPasswordsMatch && (
<p className="text-sm text-red-600 mt-1">
{t("passwordsDoNotMatch")}
{t(
"passwordsDoNotMatch"
)}
</p>
)}
{/* Only show FormMessage when field is empty */}
{confirmPasswordValue.length === 0 && (
<FormMessage />
)}
{confirmPasswordValue.length ===
0 && <FormMessage />}
</FormItem>
)}
/>
@@ -523,7 +534,9 @@ const ChangePasswordForm = forwardRef<
{step === 2 && (
<div className="space-y-4">
<div className="text-center">
<h3 className="text-lg font-medium">{t("otpAuth")}</h3>
<h3 className="text-lg font-medium">
{t("otpAuth")}
</h3>
<p className="text-sm text-muted-foreground">
{t("otpAuthDescription")}
</p>
@@ -551,9 +564,12 @@ const ChangePasswordForm = forwardRef<
onChange={(
value: string
) => {
field.onChange(value);
field.onChange(
value
);
if (
value.length === 6
value.length ===
6
) {
mfaForm.handleSubmit(
confirmMfa
@@ -630,10 +646,7 @@ const ChangePasswordForm = forwardRef<
</Button>
)}
{step === 3 && (
<Button
onClick={handleComplete}
className="w-full"
>
<Button onClick={handleComplete} className="w-full">
{t("continueToApplication")}
</Button>
)}
@@ -644,4 +657,4 @@ const ChangePasswordForm = forwardRef<
}
);
export default ChangePasswordForm;
export default ChangePasswordForm;

View File

@@ -1,8 +1,6 @@
"use client";
import {
ColumnDef,
} from "@tanstack/react-table";
import { ColumnDef } from "@tanstack/react-table";
import { DataTable } from "@app/components/ui/data-table";
type TabFilter = {

View File

@@ -1,7 +1,18 @@
import { useState } from "react";
import { Button } from "@app/components/ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "@app/components/ui/popover";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@app/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger
} from "@app/components/ui/popover";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList
} from "@app/components/ui/command";
import { CheckIcon, ChevronDownIcon, Filter } from "lucide-react";
import { cn } from "@app/lib/cn";
@@ -31,7 +42,9 @@ export function ColumnFilter({
}: ColumnFilterProps) {
const [open, setOpen] = useState(false);
const selectedOption = options.find(option => option.value === selectedValue);
const selectedOption = options.find(
(option) => option.value === selectedValue
);
return (
<Popover open={open} onOpenChange={setOpen}>
@@ -49,7 +62,9 @@ export function ColumnFilter({
<div className="flex items-center gap-2">
<Filter className="h-4 w-4" />
<span className="truncate">
{selectedOption ? selectedOption.label : placeholder}
{selectedOption
? selectedOption.label
: placeholder}
</span>
</div>
<ChevronDownIcon className="h-4 w-4 shrink-0 opacity-50" />
@@ -79,7 +94,9 @@ export function ColumnFilter({
value={option.label}
onSelect={() => {
onValueChange(
selectedValue === option.value ? undefined : option.value
selectedValue === option.value
? undefined
: option.value
);
setOpen(false);
}}
@@ -101,4 +118,4 @@ export function ColumnFilter({
</PopoverContent>
</Popover>
);
}
}

View File

@@ -186,7 +186,7 @@ const DockerContainersTable: FC<{
{
accessorKey: "name",
friendlyName: t("containerName"),
header: () => (<span className="p-3">{t("containerName")}</span>),
header: () => <span className="p-3">{t("containerName")}</span>,
cell: ({ row }) => (
<div className="font-medium">{row.original.name}</div>
)
@@ -194,7 +194,7 @@ const DockerContainersTable: FC<{
{
accessorKey: "image",
friendlyName: t("containerImage"),
header: () => (<span className="p-3">{t("containerImage")}</span>),
header: () => <span className="p-3">{t("containerImage")}</span>,
cell: ({ row }) => (
<div className="text-sm text-muted-foreground">
{row.original.image}
@@ -204,7 +204,7 @@ const DockerContainersTable: FC<{
{
accessorKey: "state",
friendlyName: t("containerState"),
header: () => (<span className="p-3">{t("containerState")}</span>),
header: () => <span className="p-3">{t("containerState")}</span>,
cell: ({ row }) => (
<Badge
variant={
@@ -220,7 +220,7 @@ const DockerContainersTable: FC<{
{
accessorKey: "networks",
friendlyName: t("containerNetworks"),
header: () => (<span className="p-3">{t("containerNetworks")}</span>),
header: () => <span className="p-3">{t("containerNetworks")}</span>,
cell: ({ row }) => {
const networks = Object.keys(row.original.networks);
return (
@@ -239,7 +239,9 @@ const DockerContainersTable: FC<{
{
accessorKey: "hostname",
friendlyName: t("containerHostnameIp"),
header: () => (<span className="p-3">{t("containerHostnameIp")}</span>),
header: () => (
<span className="p-3">{t("containerHostnameIp")}</span>
),
enableHiding: false,
cell: ({ row }) => (
<div className="text-sm font-mono">
@@ -250,7 +252,7 @@ const DockerContainersTable: FC<{
{
accessorKey: "labels",
friendlyName: t("containerLabels"),
header: () => (<span className="p-3">{t("containerLabels")}</span>),
header: () => <span className="p-3">{t("containerLabels")}</span>,
cell: ({ row }) => {
const labels = row.original.labels || {};
const labelEntries = Object.entries(labels);
@@ -302,7 +304,7 @@ const DockerContainersTable: FC<{
},
{
accessorKey: "ports",
header: () => (<span className="p-3">{t("containerPorts")}</span>),
header: () => <span className="p-3">{t("containerPorts")}</span>,
enableHiding: false,
cell: ({ row }) => {
const ports = getExposedPorts(row.original);
@@ -360,7 +362,7 @@ const DockerContainersTable: FC<{
},
{
id: "actions",
header: () => (<span className="p-3">{t("containerActions")}</span>),
header: () => <span className="p-3">{t("containerActions")}</span>,
cell: ({ row }) => {
const ports = getExposedPorts(row.original);
return (

View File

@@ -29,7 +29,7 @@ export default function CopyTextBox({
setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000);
} catch (err) {
console.error(t('copyTextFailed'), err);
console.error(t("copyTextFailed"), err);
}
}
};
@@ -54,7 +54,7 @@ export default function CopyTextBox({
type="button"
className="absolute top-0.5 right-0 z-10 bg-card"
onClick={copyToClipboard}
aria-label={t('copyTextClipboard')}
aria-label={t("copyTextClipboard")}
>
{isCopied ? (
<Check className="h-4 w-4 text-green-500" />

View File

@@ -9,7 +9,11 @@ type CopyToClipboardProps = {
isLink?: boolean;
};
const CopyToClipboard = ({ text, displayText, isLink }: CopyToClipboardProps) => {
const CopyToClipboard = ({
text,
displayText,
isLink
}: CopyToClipboardProps) => {
const [copied, setCopied] = useState(false);
const handleCopy = () => {
@@ -60,7 +64,7 @@ const CopyToClipboard = ({ text, displayText, isLink }: CopyToClipboardProps) =>
) : (
<Check className="text-green-500 h-4 w-4" />
)}
<span className="sr-only">{t('copyText')}</span>
<span className="sr-only">{t("copyText")}</span>
</button>
</div>
);

View File

@@ -45,11 +45,16 @@ import {
} from "@app/components/InfoSection";
import { useOrgContext } from "@app/hooks/useOrgContext";
import { build } from "@server/build";
import { toASCII, toUnicode } from 'punycode';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
import { toASCII, toUnicode } from "punycode";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from "./ui/select";
import { useRouter } from "next/navigation";
// Helper functions for Unicode domain handling
function toPunycode(domain: string): string {
try {
@@ -76,9 +81,9 @@ function isValidDomainFormat(domain: string): boolean {
return false;
}
const parts = domain.split('.');
const parts = domain.split(".");
for (const part of parts) {
if (part.length === 0 || part.startsWith('-') || part.endsWith('-')) {
if (part.length === 0 || part.startsWith("-") || part.endsWith("-")) {
return false;
}
if (part.length > 63) {
@@ -137,7 +142,8 @@ export default function CreateDomainForm({
resolver: zodResolver(formSchema),
defaultValues: {
baseDomain: "",
type: build == "oss" || !env.flags.usePangolinDns ? "wildcard" : "ns",
type:
build == "oss" || !env.flags.usePangolinDns ? "wildcard" : "ns",
certResolver: null,
preferWildcardCert: false
}
@@ -172,7 +178,9 @@ export default function CreateDomainForm({
description: t("domainCreatedDescription")
});
onCreated?.(domainData);
router.push(`/${org.org.orgId}/settings/domains/${domainData.domainId}`);
router.push(
`/${org.org.orgId}/settings/domains/${domainData.domainId}`
);
} catch (e) {
toast({
title: t("error"),
@@ -182,7 +190,7 @@ export default function CreateDomainForm({
} finally {
setLoading(false);
}
};
}
// Domain type options
let domainOptions: any = [];
@@ -225,145 +233,213 @@ export default function CreateDomainForm({
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-4"
id="create-domain-form"
>
<FormField
control={form.control}
name="type"
render={({ field }) => (
<FormItem>
<StrategySelect
options={domainOptions}
defaultValue={field.value}
onChange={field.onChange}
cols={1}
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="baseDomain"
render={({ field }) => (
<FormItem>
<FormLabel>{t("domain")}</FormLabel>
<FormControl>
<Input
placeholder="example.com"
{...field}
/>
</FormControl>
{punycodePreview && (
<FormDescription className="flex items-center gap-2 text-xs">
<Alert>
<Globe className="h-4 w-4" />
<AlertTitle>{t("internationaldomaindetected")}</AlertTitle>
<AlertDescription>
<div className="mt-2 space-y-1">
<p>{t("willbestoredas")} <code className="font-mono px-1 py-0.5 rounded">{punycodePreview}</code></p>
</div>
</AlertDescription>
</Alert>
</FormDescription>
)}
<FormMessage />
</FormItem>
)}
/>
{domainType === "wildcard" && (
<>
<FormField
control={form.control}
name="certResolver"
render={({ field }) => (
<FormItem>
<FormLabel>{t("certResolver")}</FormLabel>
<FormControl>
<Select
value={
field.value === null ? "default" :
(field.value === "" || (field.value && field.value !== "default")) ? "custom" :
"default"
}
onValueChange={(val) => {
if (val === "default") {
field.onChange(null);
} else if (val === "custom") {
field.onChange("");
} else {
field.onChange(val);
}
}}
>
<SelectTrigger>
<SelectValue placeholder={t("selectCertResolver")} />
</SelectTrigger>
<SelectContent>
{certResolverOptions.map((opt) => (
<SelectItem key={opt.id} value={opt.id}>
{opt.title}
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-4"
id="create-domain-form"
>
<FormField
control={form.control}
name="type"
render={({ field }) => (
<FormItem>
<StrategySelect
options={domainOptions}
defaultValue={field.value}
onChange={field.onChange}
cols={1}
/>
{form.watch("certResolver") !== null &&
form.watch("certResolver") !== "default" && (
<FormField
control={form.control}
name="certResolver"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
placeholder={t("enterCustomResolver")}
value={field.value || ""}
onChange={(e) => field.onChange(e.target.value)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="baseDomain"
render={({ field }) => (
<FormItem>
<FormLabel>{t("domain")}</FormLabel>
<FormControl>
<Input
placeholder="example.com"
{...field}
/>
</FormControl>
{punycodePreview && (
<FormDescription className="flex items-center gap-2 text-xs">
<Alert>
<Globe className="h-4 w-4" />
<AlertTitle>
{t(
"internationaldomaindetected"
)}
</AlertTitle>
<AlertDescription>
<div className="mt-2 space-y-1">
<p>
{t(
"willbestoredas"
)}{" "}
<code className="font-mono px-1 py-0.5 rounded">
{
punycodePreview
}
</code>
</p>
</div>
</AlertDescription>
</Alert>
</FormDescription>
)}
<FormMessage />
</FormItem>
)}
/>
{domainType === "wildcard" && (
<>
<FormField
control={form.control}
name="certResolver"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("certResolver")}
</FormLabel>
<FormControl>
<Select
value={
field.value === null
? "default"
: field.value ===
"" ||
(field.value &&
field.value !==
"default")
? "custom"
: "default"
}
onValueChange={(
val
) => {
if (
val ===
"default"
) {
field.onChange(
null
);
} else if (
val === "custom"
) {
field.onChange(
""
);
} else {
field.onChange(
val
);
}
}}
>
<SelectTrigger>
<SelectValue
placeholder={t(
"selectCertResolver"
)}
/>
</SelectTrigger>
<SelectContent>
{certResolverOptions.map(
(opt) => (
<SelectItem
key={
opt.id
}
value={
opt.id
}
>
{
opt.title
}
</SelectItem>
)
)}
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{form.watch("certResolver") !== null &&
form.watch("certResolver") !==
"default" && (
<FormField
control={form.control}
name="certResolver"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
placeholder={t(
"enterCustomResolver"
)}
value={
field.value ||
""
}
onChange={(e) =>
field.onChange(
e.target
.value
)
}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
{form.watch("certResolver") !== null &&
form.watch("certResolver") !== "default" && (
<FormField
control={form.control}
name="preferWildcardCert"
render={({ field: checkboxField }) => (
<FormItem className="flex flex-row items-center space-x-3 space-y-0">
<FormControl>
<CheckboxWithLabel
label={t("preferWildcardCert")}
checked={checkboxField.value}
onCheckedChange={checkboxField.onChange}
/>
</FormControl>
{/* <div className="space-y-1 leading-none">
{form.watch("certResolver") !== null &&
form.watch("certResolver") !==
"default" && (
<FormField
control={form.control}
name="preferWildcardCert"
render={({
field: checkboxField
}) => (
<FormItem className="flex flex-row items-center space-x-3 space-y-0">
<FormControl>
<CheckboxWithLabel
label={t(
"preferWildcardCert"
)}
checked={
checkboxField.value
}
onCheckedChange={
checkboxField.onChange
}
/>
</FormControl>
{/* <div className="space-y-1 leading-none">
<FormLabel>
{t("preferWildcardCert")}
</FormLabel>
</div> */}
</FormItem>
)}
/>
)}
</>
)}
</form>
</Form>
</FormItem>
)}
/>
)}
</>
)}
</form>
</Form>
</CredenzaBody>
<CredenzaFooter>
<CredenzaClose asChild>

View File

@@ -272,7 +272,7 @@ export default function CreateInternalResourceDialog({
// an alias is required
if (data.mode === "host" && isHostname(data.destination)) {
const currentAlias = data.alias?.trim() || "";
if (!currentAlias) {
// Prefill alias based on destination
let aliasValue = data.destination;
@@ -281,7 +281,7 @@ export default function CreateInternalResourceDialog({
const cleanedName = cleanForFQDN(data.name);
aliasValue = `${cleanedName}.internal`;
}
// Update the form with the prefilled alias
form.setValue("alias", aliasValue);
data.alias = aliasValue;

View File

@@ -48,7 +48,7 @@ export default function CreateRoleForm({
const t = useTranslations();
const formSchema = z.object({
name: z.string({ message: t('nameRequired') }).max(32),
name: z.string({ message: t("nameRequired") }).max(32),
description: z.string().max(255).optional()
});
@@ -78,10 +78,10 @@ export default function CreateRoleForm({
.catch((e) => {
toast({
variant: "destructive",
title: t('accessRoleErrorCreate'),
title: t("accessRoleErrorCreate"),
description: formatAxiosError(
e,
t('accessRoleErrorCreateDescription')
t("accessRoleErrorCreateDescription")
)
});
});
@@ -89,8 +89,8 @@ export default function CreateRoleForm({
if (res && res.status === 201) {
toast({
variant: "default",
title: t('accessRoleCreated'),
description: t('accessRoleCreatedDescription')
title: t("accessRoleCreated"),
description: t("accessRoleCreatedDescription")
});
if (open) {
@@ -117,9 +117,9 @@ export default function CreateRoleForm({
>
<CredenzaContent>
<CredenzaHeader>
<CredenzaTitle>{t('accessRoleCreate')}</CredenzaTitle>
<CredenzaTitle>{t("accessRoleCreate")}</CredenzaTitle>
<CredenzaDescription>
{t('accessRoleCreateDescription')}
{t("accessRoleCreateDescription")}
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
@@ -134,7 +134,9 @@ export default function CreateRoleForm({
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>{t('accessRoleName')}</FormLabel>
<FormLabel>
{t("accessRoleName")}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@@ -147,7 +149,9 @@ export default function CreateRoleForm({
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>{t('description')}</FormLabel>
<FormLabel>
{t("description")}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@@ -160,7 +164,7 @@ export default function CreateRoleForm({
</CredenzaBody>
<CredenzaFooter>
<CredenzaClose asChild>
<Button variant="outline">{t('close')}</Button>
<Button variant="outline">{t("close")}</Button>
</CredenzaClose>
<Button
type="submit"
@@ -168,7 +172,7 @@ export default function CreateRoleForm({
loading={loading}
disabled={loading}
>
{t('accessRoleCreateSubmit')}
{t("accessRoleCreateSubmit")}
</Button>
</CredenzaFooter>
</CredenzaContent>

View File

@@ -67,7 +67,7 @@ import {
} from "@app/components/ui/collapsible";
import AccessTokenSection from "@app/components/AccessTokenUsage";
import { useTranslations } from "next-intl";
import { toUnicode } from 'punycode';
import { toUnicode } from "punycode";
type FormProps = {
open: boolean;
@@ -104,7 +104,7 @@ export default function CreateShareLinkForm({
>([]);
const formSchema = z.object({
resourceId: z.number({ message: t('shareErrorSelectResource') }),
resourceId: z.number({ message: t("shareErrorSelectResource") }),
resourceName: z.string(),
resourceUrl: z.string(),
timeUnit: z.string(),
@@ -113,12 +113,12 @@ export default function CreateShareLinkForm({
});
const timeUnits = [
{ unit: "minutes", name: t('minutes') },
{ unit: "hours", name: t('hours') },
{ unit: "days", name: t('days') },
{ unit: "weeks", name: t('weeks') },
{ unit: "months", name: t('months') },
{ unit: "years", name: t('years') }
{ unit: "minutes", name: t("minutes") },
{ unit: "hours", name: t("hours") },
{ unit: "days", name: t("days") },
{ unit: "weeks", name: t("weeks") },
{ unit: "months", name: t("months") },
{ unit: "years", name: t("years") }
];
const form = useForm<z.infer<typeof formSchema>>({
@@ -144,10 +144,10 @@ export default function CreateShareLinkForm({
console.error(e);
toast({
variant: "destructive",
title: t('shareErrorFetchResource'),
title: t("shareErrorFetchResource"),
description: formatAxiosError(
e,
t('shareErrorFetchResourceDescription')
t("shareErrorFetchResourceDescription")
)
});
});
@@ -204,17 +204,21 @@ export default function CreateShareLinkForm({
validForSeconds: neverExpire ? undefined : timeInSeconds,
title:
values.title ||
t('shareLink', {resource: (values.resourceName || "Resource" + values.resourceId)})
t("shareLink", {
resource:
values.resourceName ||
"Resource" + values.resourceId
})
}
)
.catch((e) => {
console.error(e);
toast({
variant: "destructive",
title: t('shareErrorCreate'),
title: t("shareErrorCreate"),
description: formatAxiosError(
e,
t('shareErrorCreateDescription')
t("shareErrorCreateDescription")
)
});
});
@@ -263,9 +267,9 @@ export default function CreateShareLinkForm({
>
<CredenzaContent>
<CredenzaHeader>
<CredenzaTitle>{t('shareCreate')}</CredenzaTitle>
<CredenzaTitle>{t("shareCreate")}</CredenzaTitle>
<CredenzaDescription>
{t('shareCreateDescription')}
{t("shareCreateDescription")}
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
@@ -283,7 +287,7 @@ export default function CreateShareLinkForm({
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>
{t('resource')}
{t("resource")}
</FormLabel>
<Popover>
<PopoverTrigger asChild>
@@ -301,17 +305,25 @@ export default function CreateShareLinkForm({
? getSelectedResourceName(
field.value
)
: t('resourceSelect')}
: t(
"resourceSelect"
)}
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="p-0">
<Command>
<CommandInput placeholder={t('resourceSearch')} />
<CommandInput
placeholder={t(
"resourceSearch"
)}
/>
<CommandList>
<CommandEmpty>
{t('resourcesNotFound')}
{t(
"resourcesNotFound"
)}
</CommandEmpty>
<CommandGroup>
{resources.map(
@@ -367,7 +379,9 @@ export default function CreateShareLinkForm({
render={({ field }) => (
<FormItem>
<FormLabel>
{t('shareTitleOptional')}
{t(
"shareTitleOptional"
)}
</FormLabel>
<FormControl>
<Input {...field} />
@@ -379,7 +393,9 @@ export default function CreateShareLinkForm({
<div className="space-y-4">
<div className="space-y-2">
<FormLabel>{t('expireIn')}</FormLabel>
<FormLabel>
{t("expireIn")}
</FormLabel>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
@@ -391,11 +407,17 @@ export default function CreateShareLinkForm({
field.onChange
}
defaultValue={field.value.toString()}
disabled={neverExpire}
disabled={
neverExpire
}
>
<FormControl>
<SelectTrigger className="w-full">
<SelectValue placeholder={t('selectDuration')} />
<SelectValue
placeholder={t(
"selectDuration"
)}
/>
</SelectTrigger>
</FormControl>
<SelectContent>
@@ -458,12 +480,12 @@ export default function CreateShareLinkForm({
htmlFor="terms"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
{t('neverExpire')}
{t("neverExpire")}
</label>
</div>
<p className="text-sm text-muted-foreground">
{t('shareExpireDescription')}
{t("shareExpireDescription")}
</p>
</div>
</form>
@@ -471,16 +493,15 @@ export default function CreateShareLinkForm({
)}
{link && (
<div className="max-w-md space-y-4">
<p>
{t('shareSeeOnce')}
</p>
<p>
{t('shareAccessHint')}
</p>
<p>{t("shareSeeOnce")}</p>
<p>{t("shareAccessHint")}</p>
<div className="h-[250px] w-full mx-auto flex items-center justify-center">
<div className="bg-white p-6 border rounded-md">
<QRCodeCanvas value={link} size={200} />
<QRCodeCanvas
value={link}
size={200}
/>
</div>
</div>
@@ -503,12 +524,12 @@ export default function CreateShareLinkForm({
className="p-0 flex items-center justify-between w-full"
>
<h4 className="text-sm font-semibold">
{t('shareTokenUsage')}
{t("shareTokenUsage")}
</h4>
<div>
<ChevronsUpDown className="h-4 w-4" />
<span className="sr-only">
{t('toggle')}
{t("toggle")}
</span>
</div>
</Button>
@@ -538,7 +559,7 @@ export default function CreateShareLinkForm({
</CredenzaBody>
<CredenzaFooter>
<CredenzaClose asChild>
<Button variant="outline">{t('close')}</Button>
<Button variant="outline">{t("close")}</Button>
</CredenzaClose>
<Button
type="button"
@@ -546,7 +567,7 @@ export default function CreateShareLinkForm({
loading={loading}
disabled={link !== null || loading}
>
{t('createLink')}
{t("createLink")}
</Button>
</CredenzaFooter>
</CredenzaContent>

View File

@@ -78,7 +78,10 @@ const CredenzaClose = ({ className, children, ...props }: CredenzaProps) => {
const CredenzaClose = isDesktop ? DialogClose : DrawerClose;
return (
<CredenzaClose className={cn("mb-3 mt-3 md:mt-0 md:mb-0", className)} {...props}>
<CredenzaClose
className={cn("mb-3 mt-3 md:mt-0 md:mb-0", className)}
{...props}
>
{children}
</CredenzaClose>
);
@@ -128,7 +131,7 @@ const CredenzaHeader = ({ className, children, ...props }: CredenzaProps) => {
const CredenzaHeader = isDesktop ? DialogHeader : SheetHeader;
return (
<CredenzaHeader className={className} {...props}>
<CredenzaHeader className={cn("-mx-6 px-6 pb-6 border-b border-border", className)} {...props}>
{children}
</CredenzaHeader>
);
@@ -155,7 +158,13 @@ const CredenzaBody = ({ className, children, ...props }: CredenzaProps) => {
// );
return (
<div className={cn("px-0 mb-4 space-y-4 overflow-x-hidden min-w-0", className)} {...props}>
<div
className={cn(
"px-0 mb-4 space-y-4 overflow-x-hidden min-w-0",
className
)}
{...props}
>
{children}
</div>
);
@@ -168,7 +177,7 @@ const CredenzaFooter = ({ className, children, ...props }: CredenzaProps) => {
const CredenzaFooter = isDesktop ? DialogFooter : SheetFooter;
return (
<CredenzaFooter className={cn("mt-8 md:mt-0", className)} {...props}>
<CredenzaFooter className={cn("mt-8 md:mt-0 -mx-6 px-6 pt-6 border-t border-border", className)} {...props}>
{children}
</CredenzaFooter>
);

View File

@@ -21,10 +21,7 @@ type Props = {
type: string | null;
};
export default function DNSRecordsTable({
records,
type
}: Props) {
export default function DNSRecordsTable({ records, type }: Props) {
const t = useTranslations();
const env = useEnvContext();
@@ -114,11 +111,5 @@ export default function DNSRecordsTable({
...(env.env.flags.usePangolinDns ? [statusColumn] : [])
];
return (
<DNSRecordsDataTable
columns={columns}
data={records}
type={type}
/>
);
return <DNSRecordsDataTable columns={columns} data={records} type={type} />;
}

View File

@@ -110,7 +110,11 @@ export function DNSRecordsDataTable<TData, TValue>({
<h1 className="font-bold">{t("dnsRecord")}</h1>
<Badge variant="secondary">{t("required")}</Badge>
</div>
<Link href="https://docs.pangolin.net/manage/domains" target="_blank" rel="noopener noreferrer">
<Link
href="https://docs.pangolin.net/manage/domains"
target="_blank"
rel="noopener noreferrer"
>
<Button variant="outline">
<ExternalLink className="h-4 w-4 mr-1" />
{t("howToAddRecords")}
@@ -122,9 +126,7 @@ export function DNSRecordsDataTable<TData, TValue>({
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow
key={headerGroup.id}
>
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder

View File

@@ -40,52 +40,54 @@ export function DataTablePagination<TData>({
const handlePageSizeChange = (value: string) => {
const newPageSize = Number(value);
table.setPageSize(newPageSize);
// Call the callback if provided (for persistence)
if (onPageSizeChange) {
onPageSizeChange(newPageSize);
}
};
const handlePageNavigation = (action: 'first' | 'previous' | 'next' | 'last') => {
const handlePageNavigation = (
action: "first" | "previous" | "next" | "last"
) => {
if (isServerPagination && onPageChange) {
const currentPage = table.getState().pagination.pageIndex;
const pageCount = table.getPageCount();
let newPage: number;
switch (action) {
case 'first':
case "first":
newPage = 0;
break;
case 'previous':
case "previous":
newPage = Math.max(0, currentPage - 1);
break;
case 'next':
case "next":
newPage = Math.min(pageCount - 1, currentPage + 1);
break;
case 'last':
case "last":
newPage = pageCount - 1;
break;
default:
return;
}
if (newPage !== currentPage) {
onPageChange(newPage);
}
} else {
// Use table's built-in navigation for client-side pagination
switch (action) {
case 'first':
case "first":
table.setPageIndex(0);
break;
case 'previous':
case "previous":
table.previousPage();
break;
case 'next':
case "next":
table.nextPage();
break;
case 'last':
case "last":
table.setPageIndex(table.getPageCount() - 1);
break;
}
@@ -117,50 +119,66 @@ export function DataTablePagination<TData>({
<div className="flex items-center space-x-3 lg:space-x-8">
<div className="flex items-center justify-center text-sm font-medium">
{isServerPagination && totalCount !== undefined ? (
t('paginator', {
current: table.getState().pagination.pageIndex + 1,
last: Math.ceil(totalCount / table.getState().pagination.pageSize)
})
) : (
t('paginator', {current: table.getState().pagination.pageIndex + 1, last: table.getPageCount()})
)}
{isServerPagination && totalCount !== undefined
? t("paginator", {
current:
table.getState().pagination.pageIndex + 1,
last: Math.ceil(
totalCount /
table.getState().pagination.pageSize
)
})
: t("paginator", {
current:
table.getState().pagination.pageIndex + 1,
last: table.getPageCount()
})}
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => handlePageNavigation('first')}
disabled={!table.getCanPreviousPage() || isLoading || disabled}
onClick={() => handlePageNavigation("first")}
disabled={
!table.getCanPreviousPage() || isLoading || disabled
}
>
<span className="sr-only">{t('paginatorToFirst')}</span>
<span className="sr-only">{t("paginatorToFirst")}</span>
<DoubleArrowLeftIcon className="h-4 w-4" />
</Button>
<Button
variant="outline"
className="h-8 w-8 p-0"
onClick={() => handlePageNavigation('previous')}
disabled={!table.getCanPreviousPage() || isLoading || disabled}
onClick={() => handlePageNavigation("previous")}
disabled={
!table.getCanPreviousPage() || isLoading || disabled
}
>
<span className="sr-only">{t('paginatorToPrevious')}</span>
<span className="sr-only">
{t("paginatorToPrevious")}
</span>
<ChevronLeftIcon className="h-4 w-4" />
</Button>
<Button
variant="outline"
className="h-8 w-8 p-0"
onClick={() => handlePageNavigation('next')}
disabled={!table.getCanNextPage() || isLoading || disabled}
onClick={() => handlePageNavigation("next")}
disabled={
!table.getCanNextPage() || isLoading || disabled
}
>
<span className="sr-only">{t('paginatorToNext')}</span>
<span className="sr-only">{t("paginatorToNext")}</span>
<ChevronRightIcon className="h-4 w-4" />
</Button>
<Button
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => handlePageNavigation('last')}
disabled={!table.getCanNextPage() || isLoading || disabled}
onClick={() => handlePageNavigation("last")}
disabled={
!table.getCanNextPage() || isLoading || disabled
}
>
<span className="sr-only">{t('paginatorToLast')}</span>
<span className="sr-only">{t("paginatorToLast")}</span>
<DoubleArrowRightIcon className="h-4 w-4" />
</Button>
</div>

View File

@@ -62,7 +62,7 @@ export default function DeleteRoleForm({
const api = createApiClient(useEnvContext());
const formSchema = z.object({
newRoleId: z.string({ message: t('accessRoleErrorNewRequired') })
newRoleId: z.string({ message: t("accessRoleErrorNewRequired") })
});
useEffect(() => {
@@ -75,10 +75,10 @@ export default function DeleteRoleForm({
console.error(e);
toast({
variant: "destructive",
title: t('accessRoleErrorFetch'),
title: t("accessRoleErrorFetch"),
description: formatAxiosError(
e,
t('accessRoleErrorFetchDescription')
t("accessRoleErrorFetchDescription")
)
});
});
@@ -114,10 +114,10 @@ export default function DeleteRoleForm({
.catch((e) => {
toast({
variant: "destructive",
title: t('accessRoleErrorRemove'),
title: t("accessRoleErrorRemove"),
description: formatAxiosError(
e,
t('accessRoleErrorRemoveDescription')
t("accessRoleErrorRemoveDescription")
)
});
});
@@ -125,8 +125,8 @@ export default function DeleteRoleForm({
if (res && res.status === 200) {
toast({
variant: "default",
title: t('accessRoleRemoved'),
description: t('accessRoleRemovedDescription')
title: t("accessRoleRemoved"),
description: t("accessRoleRemovedDescription")
});
if (open) {
@@ -153,66 +153,66 @@ export default function DeleteRoleForm({
>
<CredenzaContent>
<CredenzaHeader>
<CredenzaTitle>{t('accessRoleRemove')}</CredenzaTitle>
<CredenzaTitle>{t("accessRoleRemove")}</CredenzaTitle>
<CredenzaDescription>
{t('accessRoleRemoveDescription')}
{t("accessRoleRemoveDescription")}
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
<div className="space-y-4">
<p>
{t('accessRoleQuestionRemove', {name: roleToDelete.name})}
</p>
<p>
{t('accessRoleRequiredRemove')}
</p>
</div>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-4"
id="remove-role-form"
>
<FormField
control={form.control}
name="newRoleId"
render={({ field }) => (
<FormItem>
<FormLabel>{t('role')}</FormLabel>
<Select
onValueChange={
field.onChange
}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t('accessRoleSelect')} />
</SelectTrigger>
</FormControl>
<SelectContent>
{roles.map((role) => (
<SelectItem
key={
role.roleId
}
value={role.roleId.toString()}
>
{role.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
<div className="space-y-4">
<p>
{t("accessRoleQuestionRemove", {
name: roleToDelete.name
})}
</p>
<p>{t("accessRoleRequiredRemove")}</p>
</div>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-4"
id="remove-role-form"
>
<FormField
control={form.control}
name="newRoleId"
render={({ field }) => (
<FormItem>
<FormLabel>{t("role")}</FormLabel>
<Select
onValueChange={field.onChange}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue
placeholder={t(
"accessRoleSelect"
)}
/>
</SelectTrigger>
</FormControl>
<SelectContent>
{roles.map((role) => (
<SelectItem
key={role.roleId}
value={role.roleId.toString()}
>
{role.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</CredenzaBody>
<CredenzaFooter>
<CredenzaClose asChild>
<Button variant="outline">{t('close')}</Button>
<Button variant="outline">{t("close")}</Button>
</CredenzaClose>
<Button
variant="destructive"
@@ -221,7 +221,7 @@ export default function DeleteRoleForm({
loading={loading}
disabled={loading}
>
{t('accessRoleRemoveSubmit')}
{t("accessRoleRemoveSubmit")}
</Button>
</CredenzaFooter>
</CredenzaContent>

View File

@@ -71,7 +71,9 @@ export function DeviceAuthConfirmation({
<BrandingLogo height={logoHeight} width={logoWidth} />
</div>
<div className="text-center space-y-1 pt-3">
<p className="text-muted-foreground">{t("deviceActivation")}</p>
<p className="text-muted-foreground">
{t("deviceActivation")}
</p>
</div>
</CardHeader>
<CardContent className="pt-6 space-y-4">
@@ -93,7 +95,9 @@ export function DeviceAuthConfirmation({
</p>
{metadata.deviceName && (
<p className="text-xs text-muted-foreground mt-1">
{t("deviceLabel", { deviceName: metadata.deviceName })}
{t("deviceLabel", {
deviceName: metadata.deviceName
})}
</p>
)}
<p className="text-xs text-muted-foreground mt-1">
@@ -103,7 +107,9 @@ export function DeviceAuthConfirmation({
</div>
<div className="space-y-2 pt-2">
<p className="text-sm font-medium">{t("deviceExistingAccess")}</p>
<p className="text-sm font-medium">
{t("deviceExistingAccess")}
</p>
<div className="space-y-1 pl-4">
<div className="flex items-center gap-2 text-sm">
<CheckCircle2 className="h-4 w-4 text-green-500" />
@@ -111,9 +117,7 @@ export function DeviceAuthConfirmation({
</div>
<div className="flex items-center gap-2 text-sm">
<CheckCircle2 className="h-4 w-4 text-green-500" />
<span>
{t("deviceOrganizationsAccess")}
</span>
<span>{t("deviceOrganizationsAccess")}</span>
</div>
</div>
</div>
@@ -136,7 +140,9 @@ export function DeviceAuthConfirmation({
disabled={loading}
loading={loading}
>
{t("deviceAuthorize", { applicationName: metadata.applicationName })}
{t("deviceAuthorize", {
applicationName: metadata.applicationName
})}
</Button>
</CardFooter>
</Card>

View File

@@ -51,8 +51,8 @@ export default function Disable2FaForm({ open, setOpen }: Disable2FaProps) {
const t = useTranslations();
const disableSchema = z.object({
password: z.string().min(1, { message: t('passwordRequired') }),
code: z.string().min(1, { message: t('verificationCodeRequired') })
password: z.string().min(1, { message: t("passwordRequired") }),
code: z.string().min(1, { message: t("verificationCodeRequired") })
});
const disableForm = useForm<z.infer<typeof disableSchema>>({
@@ -73,10 +73,10 @@ export default function Disable2FaForm({ open, setOpen }: Disable2FaProps) {
} as Disable2faBody)
.catch((e) => {
toast({
title: t('otpErrorDisable'),
title: t("otpErrorDisable"),
description: formatAxiosError(
e,
t('otpErrorDisableDescription')
t("otpErrorDisableDescription")
),
variant: "destructive"
});
@@ -111,11 +111,9 @@ export default function Disable2FaForm({ open, setOpen }: Disable2FaProps) {
>
<CredenzaContent>
<CredenzaHeader>
<CredenzaTitle>
{t('otpRemove')}
</CredenzaTitle>
<CredenzaTitle>{t("otpRemove")}</CredenzaTitle>
<CredenzaDescription>
{t('otpRemoveDescription')}
{t("otpRemoveDescription")}
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
@@ -132,7 +130,9 @@ export default function Disable2FaForm({ open, setOpen }: Disable2FaProps) {
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>{t('password')}</FormLabel>
<FormLabel>
{t("password")}
</FormLabel>
<FormControl>
<Input
type="password"
@@ -150,7 +150,7 @@ export default function Disable2FaForm({ open, setOpen }: Disable2FaProps) {
render={({ field }) => (
<FormItem>
<FormLabel>
{t('otpSetupSecretCode')}
{t("otpSetupSecretCode")}
</FormLabel>
<FormControl>
<Input {...field} />
@@ -171,17 +171,15 @@ export default function Disable2FaForm({ open, setOpen }: Disable2FaProps) {
size={48}
/>
<p className="font-semibold text-lg">
{t('otpRemoveSuccess')}
</p>
<p>
{t('otpRemoveSuccessMessage')}
{t("otpRemoveSuccess")}
</p>
<p>{t("otpRemoveSuccessMessage")}</p>
</div>
)}
</CredenzaBody>
<CredenzaFooter>
<CredenzaClose asChild>
<Button variant="outline">{t('close')}</Button>
<Button variant="outline">{t("close")}</Button>
</CredenzaClose>
{step === "password" && (
<Button
@@ -190,7 +188,7 @@ export default function Disable2FaForm({ open, setOpen }: Disable2FaProps) {
loading={loading}
disabled={loading}
>
{t('otpRemoveSubmit')}
{t("otpRemoveSubmit")}
</Button>
)}
</CredenzaFooter>

View File

@@ -295,7 +295,7 @@ export default function EditInternalResourceDialog({
// an alias is required
if (data.mode === "host" && isHostname(data.destination)) {
const currentAlias = data.alias?.trim() || "";
if (!currentAlias) {
// Prefill alias based on destination
let aliasValue = data.destination;
@@ -304,7 +304,7 @@ export default function EditInternalResourceDialog({
const cleanedName = cleanForFQDN(data.name);
aliasValue = `${cleanedName}.internal`;
}
// Update the form with the prefilled alias
form.setValue("alias", aliasValue);
data.alias = aliasValue;
@@ -389,7 +389,7 @@ export default function EditInternalResourceDialog({
useEffect(() => {
if (open) {
const resourceChanged = previousResourceId.current !== resource.id;
if (resourceChanged) {
form.reset({
name: resource.name,
@@ -402,10 +402,18 @@ export default function EditInternalResourceDialog({
});
previousResourceId.current = resource.id;
}
hasInitialized.current = false;
}
}, [open, resource.id, resource.name, resource.mode, resource.destination, resource.alias, form]);
}, [
open,
resource.id,
resource.name,
resource.mode,
resource.destination,
resource.alias,
form
]);
useEffect(() => {
if (open && !loadingRolesUsers && !hasInitialized.current) {

View File

@@ -21,7 +21,10 @@ type Enable2FaDialogProps = {
setOpen: (val: boolean) => void;
};
export default function Enable2FaDialog({ open, setOpen }: Enable2FaDialogProps) {
export default function Enable2FaDialog({
open,
setOpen
}: Enable2FaDialogProps) {
const t = useTranslations();
const [currentStep, setCurrentStep] = useState(1);
const [loading, setLoading] = useState(false);
@@ -49,21 +52,22 @@ export default function Enable2FaDialog({ open, setOpen }: Enable2FaDialogProps)
>
<CredenzaContent>
<CredenzaHeader>
<CredenzaTitle>
{t('otpSetup')}
</CredenzaTitle>
<CredenzaTitle>{t("otpSetup")}</CredenzaTitle>
<CredenzaDescription>
{t('otpSetupDescription')}
{t("otpSetupDescription")}
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
<TwoFactorSetupForm
ref={formRef}
isDialog={true}
submitButtonText={t('submit')}
submitButtonText={t("submit")}
cancelButtonText="Close"
showCancelButton={false}
onComplete={() => {setOpen(false); updateUser({ twoFactorEnabled: true });}}
onComplete={() => {
setOpen(false);
updateUser({ twoFactorEnabled: true });
}}
onStepChange={setCurrentStep}
onLoadingChange={setLoading}
/>
@@ -79,11 +83,11 @@ export default function Enable2FaDialog({ open, setOpen }: Enable2FaDialogProps)
disabled={loading}
onClick={handleSubmit}
>
{t('submit')}
{t("submit")}
</Button>
)}
</CredenzaFooter>
</CredenzaContent>
</Credenza>
);
}
}

View File

@@ -21,24 +21,24 @@ export default function ExitNodeInfoCard({}: ExitNodeInfoCardProps) {
<Alert>
<AlertDescription className="mt-4">
<InfoSections cols={2}>
<>
<InfoSection>
<InfoSectionTitle>{t("status")}</InfoSectionTitle>
<InfoSectionContent>
{remoteExitNode.online ? (
<div className="text-green-500 flex items-center space-x-2">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span>{t("online")}</span>
</div>
) : (
<div className="text-neutral-500 flex items-center space-x-2">
<div className="w-2 h-2 bg-gray-500 rounded-full"></div>
<span>{t("offline")}</span>
</div>
)}
</InfoSectionContent>
</InfoSection>
</>
<>
<InfoSection>
<InfoSectionTitle>{t("status")}</InfoSectionTitle>
<InfoSectionContent>
{remoteExitNode.online ? (
<div className="text-green-500 flex items-center space-x-2">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span>{t("online")}</span>
</div>
) : (
<div className="text-neutral-500 flex items-center space-x-2">
<div className="w-2 h-2 bg-gray-500 rounded-full"></div>
<span>{t("offline")}</span>
</div>
)}
</InfoSectionContent>
</InfoSection>
</>
<InfoSection>
<InfoSectionTitle>{t("address")}</InfoSectionTitle>
<InfoSectionContent>

View File

@@ -3,18 +3,17 @@
import { useEffect, useState, useRef } from "react";
import { Textarea } from "@/components/ui/textarea";
interface HeadersInputProps {
value?: { name: string, value: string }[] | null;
onChange: (value: { name: string, value: string }[] | null) => void;
value?: { name: string; value: string }[] | null;
onChange: (value: { name: string; value: string }[] | null) => void;
placeholder?: string;
rows?: number;
className?: string;
}
export function HeadersInput({
value = [],
onChange,
export function HeadersInput({
value = [],
onChange,
placeholder = `X-Example-Header: example-value
X-Another-Header: another-value`,
rows = 4,
@@ -25,34 +24,40 @@ X-Another-Header: another-value`,
const isUserEditingRef = useRef(false);
// Convert header objects array to newline-separated string for display
const convertToNewlineSeparated = (headers: { name: string, value: string }[] | null): string => {
const convertToNewlineSeparated = (
headers: { name: string; value: string }[] | null
): string => {
if (!headers || headers.length === 0) return "";
return headers
.map(header => `${header.name}: ${header.value}`)
.join('\n');
.map((header) => `${header.name}: ${header.value}`)
.join("\n");
};
// Convert newline-separated string to header objects array
const convertToHeadersArray = (newlineSeparated: string): { name: string, value: string }[] | null => {
const convertToHeadersArray = (
newlineSeparated: string
): { name: string; value: string }[] | null => {
if (!newlineSeparated || newlineSeparated.trim() === "") return [];
return newlineSeparated
.split('\n')
.map(line => line.trim())
.filter(line => line.length > 0 && line.includes(':'))
.map(line => {
const colonIndex = line.indexOf(':');
.split("\n")
.map((line) => line.trim())
.filter((line) => line.length > 0 && line.includes(":"))
.map((line) => {
const colonIndex = line.indexOf(":");
const name = line.substring(0, colonIndex).trim();
const value = line.substring(colonIndex + 1).trim();
// Ensure header name conforms to HTTP header requirements
// Header names should be case-insensitive, contain only ASCII letters, digits, and hyphens
const normalizedName = name.replace(/[^a-zA-Z0-9\-]/g, '').toLowerCase();
const normalizedName = name
.replace(/[^a-zA-Z0-9\-]/g, "")
.toLowerCase();
return { name: normalizedName, value };
})
.filter(header => header.name.length > 0); // Filter out headers with invalid names
.filter((header) => header.name.length > 0); // Filter out headers with invalid names
};
// Update internal value when external value changes
@@ -66,26 +71,28 @@ X-Another-Header: another-value`,
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const newValue = e.target.value;
setInternalValue(newValue);
// Mark that user is actively editing
isUserEditingRef.current = true;
// Only update parent if the input is in a valid state
// Valid states: empty/whitespace only, or contains properly formatted headers
if (newValue.trim() === "") {
// Empty input is valid - represents no headers
onChange([]);
} else {
// Check if all non-empty lines are properly formatted (contain ':')
const lines = newValue.split('\n');
const lines = newValue.split("\n");
const nonEmptyLines = lines
.map(line => line.trim())
.filter(line => line.length > 0);
.map((line) => line.trim())
.filter((line) => line.length > 0);
// If there are no non-empty lines, or all non-empty lines contain ':', it's valid
const isValid = nonEmptyLines.length === 0 || nonEmptyLines.every(line => line.includes(':'));
const isValid =
nonEmptyLines.length === 0 ||
nonEmptyLines.every((line) => line.includes(":"));
if (isValid) {
// Safe to convert and update parent
const headersArray = convertToHeadersArray(newValue);

View File

@@ -88,7 +88,7 @@ export function HorizontalTabs({
variant="outlinePrimary"
className="ml-2"
>
{t('licenseBadge')}
{t("licenseBadge")}
</Badge>
)}
</div>

View File

@@ -51,22 +51,26 @@ type IdpCreateWizardProps = {
loading?: boolean;
};
export function IdpCreateWizard({ onSubmit, defaultValues, loading = false }: IdpCreateWizardProps) {
export function IdpCreateWizard({
onSubmit,
defaultValues,
loading = false
}: IdpCreateWizardProps) {
const t = useTranslations();
const createIdpFormSchema = z.object({
name: z.string().min(2, { message: t('nameMin', {len: 2}) }),
name: z.string().min(2, { message: t("nameMin", { len: 2 }) }),
type: z.enum(["oidc"]),
clientId: z.string().min(1, { message: t('idpClientIdRequired') }),
clientSecret: z.string().min(1, { message: t('idpClientSecretRequired') }),
authUrl: z.url({ message: t('idpErrorAuthUrlInvalid') }),
tokenUrl: z.url({ message: t('idpErrorTokenUrlInvalid') }),
identifierPath: z
clientId: z.string().min(1, { message: t("idpClientIdRequired") }),
clientSecret: z
.string()
.min(1, { message: t('idpPathRequired') }),
.min(1, { message: t("idpClientSecretRequired") }),
authUrl: z.url({ message: t("idpErrorAuthUrlInvalid") }),
tokenUrl: z.url({ message: t("idpErrorTokenUrlInvalid") }),
identifierPath: z.string().min(1, { message: t("idpPathRequired") }),
emailPath: z.string().optional(),
namePath: z.string().optional(),
scopes: z.string().min(1, { message: t('idpScopeRequired') }),
scopes: z.string().min(1, { message: t("idpScopeRequired") }),
autoProvision: z.boolean().default(false)
});
@@ -80,7 +84,7 @@ export function IdpCreateWizard({ onSubmit, defaultValues, loading = false }: Id
{
id: "oidc",
title: "OAuth2/OIDC",
description: t('idpOidcDescription')
description: t("idpOidcDescription")
}
];
@@ -110,11 +114,9 @@ export function IdpCreateWizard({ onSubmit, defaultValues, loading = false }: Id
<SettingsContainer>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t('idpTitle')}
</SettingsSectionTitle>
<SettingsSectionTitle>{t("idpTitle")}</SettingsSectionTitle>
<SettingsSectionDescription>
{t('idpCreateSettingsDescription')}
{t("idpCreateSettingsDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
@@ -130,12 +132,15 @@ export function IdpCreateWizard({ onSubmit, defaultValues, loading = false }: Id
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>{t('name')}</FormLabel>
<FormLabel>{t("name")}</FormLabel>
<FormControl>
<Input {...field} disabled={loading} />
<Input
{...field}
disabled={loading}
/>
</FormControl>
<FormDescription>
{t('idpDisplayName')}
{t("idpDisplayName")}
</FormDescription>
<FormMessage />
</FormItem>
@@ -145,7 +150,7 @@ export function IdpCreateWizard({ onSubmit, defaultValues, loading = false }: Id
<div className="flex items-start mb-0">
<SwitchInput
id="auto-provision-toggle"
label={t('idpAutoProvisionUsers')}
label={t("idpAutoProvisionUsers")}
defaultChecked={form.getValues(
"autoProvision"
)}
@@ -159,7 +164,7 @@ export function IdpCreateWizard({ onSubmit, defaultValues, loading = false }: Id
/>
</div>
<span className="text-sm text-muted-foreground">
{t('idpAutoProvisionUsersDescription')}
{t("idpAutoProvisionUsersDescription")}
</span>
</form>
</Form>
@@ -169,11 +174,9 @@ export function IdpCreateWizard({ onSubmit, defaultValues, loading = false }: Id
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t('idpType')}
</SettingsSectionTitle>
<SettingsSectionTitle>{t("idpType")}</SettingsSectionTitle>
<SettingsSectionDescription>
{t('idpTypeDescription')}
{t("idpTypeDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
@@ -193,10 +196,10 @@ export function IdpCreateWizard({ onSubmit, defaultValues, loading = false }: Id
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t('idpOidcConfigure')}
{t("idpOidcConfigure")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t('idpOidcConfigureDescription')}
{t("idpOidcConfigureDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
@@ -212,13 +215,18 @@ export function IdpCreateWizard({ onSubmit, defaultValues, loading = false }: Id
render={({ field }) => (
<FormItem>
<FormLabel>
{t('idpClientId')}
{t("idpClientId")}
</FormLabel>
<FormControl>
<Input {...field} disabled={loading} />
<Input
{...field}
disabled={loading}
/>
</FormControl>
<FormDescription>
{t('idpClientIdDescription')}
{t(
"idpClientIdDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
@@ -231,7 +239,7 @@ export function IdpCreateWizard({ onSubmit, defaultValues, loading = false }: Id
render={({ field }) => (
<FormItem>
<FormLabel>
{t('idpClientSecret')}
{t("idpClientSecret")}
</FormLabel>
<FormControl>
<Input
@@ -241,7 +249,9 @@ export function IdpCreateWizard({ onSubmit, defaultValues, loading = false }: Id
/>
</FormControl>
<FormDescription>
{t('idpClientSecretDescription')}
{t(
"idpClientSecretDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
@@ -254,7 +264,7 @@ export function IdpCreateWizard({ onSubmit, defaultValues, loading = false }: Id
render={({ field }) => (
<FormItem>
<FormLabel>
{t('idpAuthUrl')}
{t("idpAuthUrl")}
</FormLabel>
<FormControl>
<Input
@@ -264,7 +274,7 @@ export function IdpCreateWizard({ onSubmit, defaultValues, loading = false }: Id
/>
</FormControl>
<FormDescription>
{t('idpAuthUrlDescription')}
{t("idpAuthUrlDescription")}
</FormDescription>
<FormMessage />
</FormItem>
@@ -277,7 +287,7 @@ export function IdpCreateWizard({ onSubmit, defaultValues, loading = false }: Id
render={({ field }) => (
<FormItem>
<FormLabel>
{t('idpTokenUrl')}
{t("idpTokenUrl")}
</FormLabel>
<FormControl>
<Input
@@ -287,7 +297,9 @@ export function IdpCreateWizard({ onSubmit, defaultValues, loading = false }: Id
/>
</FormControl>
<FormDescription>
{t('idpTokenUrlDescription')}
{t(
"idpTokenUrlDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
@@ -299,10 +311,10 @@ export function IdpCreateWizard({ onSubmit, defaultValues, loading = false }: Id
<Alert variant="neutral">
<InfoIcon className="h-4 w-4" />
<AlertTitle className="font-semibold">
{t('idpOidcConfigureAlert')}
{t("idpOidcConfigureAlert")}
</AlertTitle>
<AlertDescription>
{t('idpOidcConfigureAlertDescription')}
{t("idpOidcConfigureAlertDescription")}
</AlertDescription>
</Alert>
</SettingsSectionBody>
@@ -311,10 +323,10 @@ export function IdpCreateWizard({ onSubmit, defaultValues, loading = false }: Id
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t('idpToken')}
{t("idpToken")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t('idpTokenDescription')}
{t("idpTokenDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
@@ -327,17 +339,19 @@ export function IdpCreateWizard({ onSubmit, defaultValues, loading = false }: Id
<Alert variant="neutral">
<InfoIcon className="h-4 w-4" />
<AlertTitle className="font-semibold">
{t('idpJmespathAbout')}
{t("idpJmespathAbout")}
</AlertTitle>
<AlertDescription>
{t('idpJmespathAboutDescription')}{" "}
{t("idpJmespathAboutDescription")}{" "}
<a
href="https://jmespath.org"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline inline-flex items-center"
>
{t('idpJmespathAboutDescriptionLink')}{" "}
{t(
"idpJmespathAboutDescriptionLink"
)}{" "}
<ExternalLink className="ml-1 h-4 w-4" />
</a>
</AlertDescription>
@@ -349,13 +363,18 @@ export function IdpCreateWizard({ onSubmit, defaultValues, loading = false }: Id
render={({ field }) => (
<FormItem>
<FormLabel>
{t('idpJmespathLabel')}
{t("idpJmespathLabel")}
</FormLabel>
<FormControl>
<Input {...field} disabled={loading} />
<Input
{...field}
disabled={loading}
/>
</FormControl>
<FormDescription>
{t('idpJmespathLabelDescription')}
{t(
"idpJmespathLabelDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
@@ -368,13 +387,20 @@ export function IdpCreateWizard({ onSubmit, defaultValues, loading = false }: Id
render={({ field }) => (
<FormItem>
<FormLabel>
{t('idpJmespathEmailPathOptional')}
{t(
"idpJmespathEmailPathOptional"
)}
</FormLabel>
<FormControl>
<Input {...field} disabled={loading} />
<Input
{...field}
disabled={loading}
/>
</FormControl>
<FormDescription>
{t('idpJmespathEmailPathOptionalDescription')}
{t(
"idpJmespathEmailPathOptionalDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
@@ -387,13 +413,20 @@ export function IdpCreateWizard({ onSubmit, defaultValues, loading = false }: Id
render={({ field }) => (
<FormItem>
<FormLabel>
{t('idpJmespathNamePathOptional')}
{t(
"idpJmespathNamePathOptional"
)}
</FormLabel>
<FormControl>
<Input {...field} disabled={loading} />
<Input
{...field}
disabled={loading}
/>
</FormControl>
<FormDescription>
{t('idpJmespathNamePathOptionalDescription')}
{t(
"idpJmespathNamePathOptionalDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
@@ -406,13 +439,20 @@ export function IdpCreateWizard({ onSubmit, defaultValues, loading = false }: Id
render={({ field }) => (
<FormItem>
<FormLabel>
{t('idpOidcConfigureScopes')}
{t(
"idpOidcConfigureScopes"
)}
</FormLabel>
<FormControl>
<Input {...field} disabled={loading} />
<Input
{...field}
disabled={loading}
/>
</FormControl>
<FormDescription>
{t('idpOidcConfigureScopesDescription')}
{t(
"idpOidcConfigureScopesDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>

View File

@@ -1,10 +1,8 @@
"use client";
import {
ColumnDef,
} from "@tanstack/react-table";
import { ColumnDef } from "@tanstack/react-table";
import { DataTable } from "@app/components/ui/data-table";
import { useTranslations } from 'next-intl';
import { useTranslations } from "next-intl";
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
@@ -19,7 +17,6 @@ export function InvitationsDataTable<TData, TValue>({
onRefresh,
isRefreshing
}: DataTableProps<TData, TValue>) {
const t = useTranslations();
return (
@@ -27,8 +24,8 @@ export function InvitationsDataTable<TData, TValue>({
columns={columns}
data={data}
persistPageSize="invitations-table"
title={t('invite')}
searchPlaceholder={t('inviteSearch')}
title={t("invite")}
searchPlaceholder={t("inviteSearch")}
searchColumn="email"
onRefresh={onRefresh}
isRefreshing={isRefreshing}

View File

@@ -72,12 +72,12 @@ export default function InvitationsTable({
accessorKey: "email",
enableHiding: false,
friendlyName: t("email"),
header: () => (<span className="p-3">{t("email")}</span>)
header: () => <span className="p-3">{t("email")}</span>
},
{
accessorKey: "expiresAt",
friendlyName: t("expiresAt"),
header: () => (<span className="p-3">{t("expiresAt")}</span>),
header: () => <span className="p-3">{t("expiresAt")}</span>,
cell: ({ row }) => {
const expiresAt = new Date(row.original.expiresAt);
const isExpired = expiresAt < new Date();
@@ -92,7 +92,7 @@ export default function InvitationsTable({
{
accessorKey: "role",
friendlyName: t("role"),
header: () => (<span className="p-3">{t("role")}</span>)
header: () => <span className="p-3">{t("role")}</span>
},
{
id: "dots",
@@ -183,9 +183,7 @@ export default function InvitationsTable({
}}
dialog={
<div>
<p>
{t("inviteQuestionRemove")}
</p>
<p>{t("inviteQuestionRemove")}</p>
<p>{t("inviteMessageRemove")}</p>
</div>
}

View File

@@ -72,10 +72,12 @@ export async function Layout({
{/* Main content */}
<main className="flex-1 overflow-y-auto p-3 md:p-6 w-full">
<div className={cn(
"container mx-auto max-w-12xl mb-12",
showHeader && "md:pt-16" // Add top padding only on desktop to account for fixed header
)}>
<div
className={cn(
"container mx-auto max-w-12xl mb-12",
showHeader && "md:pt-16" // Add top padding only on desktop to account for fixed header
)}
>
{children}
</div>
</main>

View File

@@ -133,7 +133,8 @@ export function LicenseKeysDataTable({
header: () => <span className="p-3"></span>,
cell: ({ row }) => (
<div className="flex items-center gap-2 justify-end">
<Button variant={"outline"}
<Button
variant={"outline"}
onClick={() => onDelete(row.original)}
>
{t("delete")}

View File

@@ -17,15 +17,13 @@ export default function LicenseViolation() {
return (
<div className="fixed bottom-0 left-0 right-0 w-full bg-red-500 text-white p-4 text-center z-50">
<div className="flex justify-between items-center">
<p>
{t('componentsInvalidKey')}
</p>
<p>{t("componentsInvalidKey")}</p>
<Button
variant={"ghost"}
className="hover:bg-yellow-500"
onClick={() => setIsDismissed(true)}
>
{t('dismiss')}
{t("dismiss")}
</Button>
</div>
</div>

View File

@@ -64,4 +64,4 @@ export default function LocaleSwitcher() {
]}
/>
);
}
}

View File

@@ -49,7 +49,10 @@ const STORAGE_KEYS = {
tableId ? `${tableId}-size` : STORAGE_KEYS.PAGE_SIZE
};
export const getStoredPageSize = (tableId?: string, defaultSize = 20): number => {
export const getStoredPageSize = (
tableId?: string,
defaultSize = 20
): number => {
if (typeof window === "undefined") return defaultSize;
try {
@@ -145,7 +148,7 @@ export function LogDataTable<TData, TValue>({
onPageSizeChange: onPageSizeChangeProp,
isLoading = false,
expandable = false,
disabled=false,
disabled = false,
renderExpandedRow
}: DataTableProps<TData, TValue>) {
const t = useTranslations();
@@ -313,7 +316,7 @@ export function LogDataTable<TData, TValue>({
const handleTabChange = (value: string) => {
if (disabled) return;
setActiveTab(value);
// Reset to first page when changing tabs
table.setPageIndex(0);
@@ -322,7 +325,7 @@ export function LogDataTable<TData, TValue>({
// Enhanced pagination component that updates our local state
const handlePageSizeChange = (newPageSize: number) => {
if (disabled) return;
// setPageSize(newPageSize);
table.setPageSize(newPageSize);
@@ -340,7 +343,7 @@ export function LogDataTable<TData, TValue>({
// Handle page changes for server pagination
const handlePageChange = (newPageIndex: number) => {
if (disabled) return;
if (isServerPagination && onPageChange) {
onPageChange(newPageIndex);
}
@@ -351,7 +354,7 @@ export function LogDataTable<TData, TValue>({
end: DateTimeValue
) => {
if (disabled) return;
setStartDate(start);
setEndDate(end);
onDateRangeChange?.(start, end);
@@ -397,7 +400,10 @@ export function LogDataTable<TData, TValue>({
</Button>
)}
{onExport && (
<Button onClick={() => !disabled && onExport()} disabled={isExporting || disabled}>
<Button
onClick={() => !disabled && onExport()}
disabled={isExporting || disabled}
>
<Download
className={`mr-2 h-4 w-4 ${isExporting ? "animate-spin" : ""}`}
/>
@@ -427,61 +433,63 @@ export function LogDataTable<TData, TValue>({
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => {
const isExpanded =
expandable && expandedRows.has(row.id);
return [
<TableRow
key={row.id}
data-state={
row.getIsSelected() &&
"selected"
}
onClick={() =>
expandable && !disabled
? toggleRowExpansion(
row.id
)
: undefined
}
className="text-xs" // made smaller
>
{row
.getVisibleCells()
.map((cell) => {
const originalRow =
row.original as any;
const actionValue =
originalRow?.action;
let className = "";
table
.getRowModel()
.rows.map((row) => {
const isExpanded =
expandable &&
expandedRows.has(row.id);
return [
<TableRow
key={row.id}
data-state={
row.getIsSelected() &&
"selected"
}
onClick={() =>
expandable && !disabled
? toggleRowExpansion(
row.id
)
: undefined
}
className="text-xs" // made smaller
>
{row
.getVisibleCells()
.map((cell) => {
const originalRow =
row.original as any;
const actionValue =
originalRow?.action;
let className = "";
if (
typeof actionValue ===
"boolean"
) {
className =
actionValue
? "bg-green-100 dark:bg-green-900/50"
: "bg-red-100 dark:bg-red-900/50";
}
if (
typeof actionValue ===
"boolean"
) {
className =
actionValue
? "bg-green-100 dark:bg-green-900/50"
: "bg-red-100 dark:bg-red-900/50";
}
return (
<TableCell
key={cell.id}
className={`${className} py-2`} // made smaller
>
{flexRender(
cell.column
.columnDef
.cell,
cell.getContext()
)}
</TableCell>
);
})}
</TableRow>,
isExpanded &&
renderExpandedRow && (
return (
<TableCell
key={cell.id}
className={`${className} py-2`} // made smaller
>
{flexRender(
cell.column
.columnDef
.cell,
cell.getContext()
)}
</TableCell>
);
})}
</TableRow>,
isExpanded && renderExpandedRow && (
<TableRow
key={`${row.id}-expanded`}
>
@@ -497,8 +505,9 @@ export function LogDataTable<TData, TValue>({
</TableCell>
</TableRow>
)
].filter(Boolean);
}).flat()
].filter(Boolean);
})
.flat()
) : (
<TableRow>
<TableCell

View File

@@ -239,10 +239,10 @@ export default function LoginForm({
try {
const response = await loginProxy(
{
email,
password,
code,
resourceGuid: resourceGuid as string
email,
password,
code,
resourceGuid: resourceGuid as string
},
forceLogin
);
@@ -364,7 +364,7 @@ export default function LoginForm({
{forceLogin && (
<Alert variant="neutral">
<AlertDescription className="flex items-center gap-2">
<LockIcon className="w-4 h-4" />
<LockIcon className="w-4 h-4" />
{t("loginRequiredForDevice")}
</AlertDescription>
</Alert>

View File

@@ -19,7 +19,6 @@ export function OrgApiKeysDataTable<TData, TValue>({
onRefresh,
isRefreshing
}: DataTableProps<TData, TValue>) {
const t = useTranslations();
return (
@@ -27,13 +26,13 @@ export function OrgApiKeysDataTable<TData, TValue>({
columns={columns}
data={data}
persistPageSize="Org-apikeys-table"
title={t('apiKeys')}
searchPlaceholder={t('searchApiKeys')}
title={t("apiKeys")}
searchPlaceholder={t("searchApiKeys")}
searchColumn="name"
onAdd={addApiKey}
onRefresh={onRefresh}
isRefreshing={isRefreshing}
addButtonText={t('apiKeysAdd')}
addButtonText={t("apiKeysAdd")}
enableColumnVisibility={true}
stickyLeftColumn="name"
stickyRightColumn="actions"

View File

@@ -111,7 +111,7 @@ export default function OrgApiKeysTable({
{
accessorKey: "key",
friendlyName: t("key"),
header: () => (<span className="p-3">{t("key")}</span>),
header: () => <span className="p-3">{t("key")}</span>,
cell: ({ row }) => {
const r = row.original;
return <span className="font-mono">{r.key}</span>;
@@ -120,7 +120,7 @@ export default function OrgApiKeysTable({
{
accessorKey: "createdAt",
friendlyName: t("createdAt"),
header: () => (<span className="p-3">{t("createdAt")}</span>),
header: () => <span className="p-3">{t("createdAt")}</span>,
cell: ({ row }) => {
const r = row.original;
return <span>{moment(r.createdAt).format("lll")}</span>;
@@ -149,7 +149,9 @@ export default function OrgApiKeysTable({
setSelected(r);
}}
>
<Link href={`/${orgId}/settings/api-keys/${r.id}`}>
<Link
href={`/${orgId}/settings/api-keys/${r.id}`}
>
{t("viewSettings")}
</Link>
</DropdownMenuItem>
@@ -166,9 +168,7 @@ export default function OrgApiKeysTable({
</DropdownMenuContent>
</DropdownMenu>
<Link href={`/${orgId}/settings/api-keys/${r.id}`}>
<Button
variant={"outline"}
>
<Button variant={"outline"}>
{t("edit")}
<ArrowRight className="ml-2 w-4 h-4" />
</Button>
@@ -190,13 +190,9 @@ export default function OrgApiKeysTable({
}}
dialog={
<div>
<p>
{t("apiKeysQuestionRemove")}
</p>
<p>{t("apiKeysQuestionRemove")}</p>
<p>
{t("apiKeysMessageRemove")}
</p>
<p>{t("apiKeysMessageRemove")}</p>
</div>
}
buttonText={t("apiKeysDeleteConfirm")}

View File

@@ -41,7 +41,8 @@ export default function OrgPolicyResult({
accessRes
}: OrgPolicyResultProps) {
const [show2FaDialog, setShow2FaDialog] = useState(false);
const [showChangePasswordDialog, setShowChangePasswordDialog] = useState(false);
const [showChangePasswordDialog, setShowChangePasswordDialog] =
useState(false);
const t = useTranslations();
const { user } = useUserContext();
const router = useRouter();
@@ -77,15 +78,15 @@ export default function OrgPolicyResult({
if (accessRes.policies?.maxSessionLength) {
const maxSessionPolicy = accessRes.policies?.maxSessionLength;
const maxHours = maxSessionPolicy.maxSessionLengthHours;
// Use hours if less than 24, otherwise convert to days
const useHours = maxHours < 24;
const maxTime = useHours ? maxHours : Math.round(maxHours / 24);
const descriptionKey = useHours
const descriptionKey = useHours
? "reauthenticationDescriptionHours"
: "reauthenticationDescription";
const description = useHours
? t(descriptionKey, { maxHours })
: t(descriptionKey, { maxDays: maxTime });

View File

@@ -19,7 +19,7 @@ import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
TooltipTrigger
} from "@app/components/ui/tooltip";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { cn } from "@app/lib/cn";
@@ -36,7 +36,11 @@ interface OrgSelectorProps {
isCollapsed?: boolean;
}
export function OrgSelector({ orgId, orgs, isCollapsed = false }: OrgSelectorProps) {
export function OrgSelector({
orgId,
orgs,
isCollapsed = false
}: OrgSelectorProps) {
const { user } = useUserContext();
const [open, setOpen] = useState(false);
const router = useRouter();
@@ -65,10 +69,10 @@ export function OrgSelector({ orgId, orgs, isCollapsed = false }: OrgSelectorPro
<Building2 className="h-4 w-4 mr-3 shrink-0" />
<div className="flex flex-col items-start min-w-0 flex-1">
<span className="font-bold text-sm">
{t('org')}
{t("org")}
</span>
<span className="text-sm text-muted-foreground truncate w-full text-left">
{selectedOrg?.name || t('noneSelected')}
{selectedOrg?.name || t("noneSelected")}
</span>
</div>
</div>
@@ -80,17 +84,20 @@ export function OrgSelector({ orgId, orgs, isCollapsed = false }: OrgSelectorPro
<PopoverContent className="w-[320px] p-0" align="start">
<Command className="rounded-lg">
<CommandInput
placeholder={t('searchProgress')}
placeholder={t("searchProgress")}
className="border-0 focus:ring-0"
/>
<CommandEmpty className="py-6 text-center">
<div className="text-muted-foreground text-sm">
{t('orgNotFound2')}
{t("orgNotFound2")}
</div>
</CommandEmpty>
{(!env.flags.disableUserCreateOrg || user.serverAdmin) && (
<>
<CommandGroup heading={t('create')} className="py-2">
<CommandGroup
heading={t("create")}
className="py-2"
>
<CommandList>
<CommandItem
onSelect={() => {
@@ -103,8 +110,12 @@ export function OrgSelector({ orgId, orgs, isCollapsed = false }: OrgSelectorPro
<Plus className="h-4 w-4 text-primary" />
</div>
<div className="flex flex-col">
<span className="font-medium">{t('setupNewOrg')}</span>
<span className="text-xs text-muted-foreground">{t('createNewOrgDescription')}</span>
<span className="font-medium">
{t("setupNewOrg")}
</span>
<span className="text-xs text-muted-foreground">
{t("createNewOrgDescription")}
</span>
</div>
</CommandItem>
</CommandList>
@@ -112,7 +123,7 @@ export function OrgSelector({ orgId, orgs, isCollapsed = false }: OrgSelectorPro
<CommandSeparator className="my-2" />
</>
)}
<CommandGroup heading={t('orgs')} className="py-2">
<CommandGroup heading={t("orgs")} className="py-2">
<CommandList>
{orgs?.map((org) => (
<CommandItem
@@ -127,13 +138,19 @@ export function OrgSelector({ orgId, orgs, isCollapsed = false }: OrgSelectorPro
<Users className="h-4 w-4 text-muted-foreground" />
</div>
<div className="flex flex-col flex-1">
<span className="font-medium">{org.name}</span>
<span className="text-xs text-muted-foreground">{t('organization')}</span>
<span className="font-medium">
{org.name}
</span>
<span className="text-xs text-muted-foreground">
{t("organization")}
</span>
</div>
<Check
className={cn(
"h-4 w-4 text-primary",
orgId === org.orgId ? "opacity-100" : "opacity-0"
orgId === org.orgId
? "opacity-100"
: "opacity-0"
)}
/>
</CommandItem>
@@ -154,8 +171,12 @@ export function OrgSelector({ orgId, orgs, isCollapsed = false }: OrgSelectorPro
</TooltipTrigger>
<TooltipContent side="right" sideOffset={8}>
<div className="text-center">
<p className="font-medium">{selectedOrg?.name || t('noneSelected')}</p>
<p className="text-xs text-muted-foreground">{t('org')}</p>
<p className="font-medium">
{selectedOrg?.name || t("noneSelected")}
</p>
<p className="text-xs text-muted-foreground">
{t("org")}
</p>
</div>
</TooltipContent>
</Tooltip>

View File

@@ -10,7 +10,15 @@ import {
CardFooter
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Users, Globe, Database, Cog, Settings, Waypoints, Combine } from "lucide-react";
import {
Users,
Globe,
Database,
Cog,
Settings,
Waypoints,
Combine
} from "lucide-react";
import { useTranslations } from "next-intl";
interface OrgStat {
@@ -43,17 +51,17 @@ export default function OrganizationLandingCard(
const orgStats: OrgStat[] = [
{
label: t('sites'),
label: t("sites"),
value: orgData.overview.stats.sites,
icon: <Combine className="h-6 w-6" />
},
{
label: t('resources'),
label: t("resources"),
value: orgData.overview.stats.resources,
icon: <Waypoints className="h-6 w-6" />
},
{
label: t('users'),
label: t("users"),
value: orgData.overview.stats.users,
icon: <Users className="h-6 w-6" />
}
@@ -84,9 +92,11 @@ export default function OrganizationLandingCard(
))}
</div>
<div className="text-center text-lg">
{t('accessRoleYour')}{" "}
{t("accessRoleYour")}{" "}
<span className="font-semibold">
{orgData.overview.isOwner ? t('accessRoleOwner') : orgData.overview.userRole}
{orgData.overview.isOwner
? t("accessRoleOwner")
: orgData.overview.userRole}
</span>
</div>
</CardContent>
@@ -95,7 +105,7 @@ export default function OrganizationLandingCard(
<Link href={`/${orgData.overview.orgId}/settings`}>
<Button size="lg" className="w-full md:w-auto">
<Settings className="mr-2 h-4 w-4" />
{t('orgGeneralSettings')}
{t("orgGeneralSettings")}
</Button>
</Link>
</CardFooter>

View File

@@ -60,7 +60,10 @@ export function PathMatchModal({
setOpen(false);
};
const getPlaceholder = () => (matchType === "regex" ? t("pathMatchRegexPlaceholder") : t("pathMatchDefaultPlaceholder"));
const getPlaceholder = () =>
matchType === "regex"
? t("pathMatchRegexPlaceholder")
: t("pathMatchDefaultPlaceholder");
const getHelpText = () => {
switch (matchType) {
@@ -93,14 +96,22 @@ export function PathMatchModal({
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="prefix">{t("pathMatchPrefix")}</SelectItem>
<SelectItem value="exact">{t("pathMatchExact")}</SelectItem>
<SelectItem value="regex">{t("pathMatchRegex")}</SelectItem>
<SelectItem value="prefix">
{t("pathMatchPrefix")}
</SelectItem>
<SelectItem value="exact">
{t("pathMatchExact")}
</SelectItem>
<SelectItem value="regex">
{t("pathMatchRegex")}
</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid gap-2">
<Label htmlFor="path-value">{t("pathMatchValue")}</Label>
<Label htmlFor="path-value">
{t("pathMatchValue")}
</Label>
<Input
id="path-value"
placeholder={getPlaceholder()}
@@ -115,9 +126,9 @@ export function PathMatchModal({
<CredenzaFooter className="gap-2">
{/* {value?.path && (
)} */}
<Button variant="outline" onClick={handleClear}>
{t("clear")}
</Button>
<Button variant="outline" onClick={handleClear}>
{t("clear")}
</Button>
<Button onClick={handleSave} disabled={!path.trim()}>
{t("saveChanges")}
</Button>
@@ -206,7 +217,9 @@ export function PathRewriteModal({
</CredenzaHeader>
<div className="grid gap-4">
<div className="grid gap-2">
<Label htmlFor="rewrite-type">{t("pathRewriteType")}</Label>
<Label htmlFor="rewrite-type">
{t("pathRewriteType")}
</Label>
<Select
value={rewriteType}
onValueChange={setRewriteType}
@@ -231,7 +244,9 @@ export function PathRewriteModal({
</Select>
</div>
<div className="grid gap-2">
<Label htmlFor="rewrite-value">{t("pathRewriteValue")}</Label>
<Label htmlFor="rewrite-value">
{t("pathRewriteValue")}
</Label>
<Input
id="rewrite-value"
placeholder={getPlaceholder()}
@@ -269,7 +284,7 @@ export function PathMatchDisplay({
value: { path: string | null; pathMatchType: string | null };
}) {
const t = useTranslations();
if (!value?.path) return null;
const getTypeLabel = (type: string | null) => {
@@ -300,7 +315,7 @@ export function PathRewriteDisplay({
value: { rewritePath: string | null; rewritePathType: string | null };
}) {
const t = useTranslations();
if (!value?.rewritePath && value?.rewritePathType !== "stripPrefix")
return null;

View File

@@ -23,17 +23,17 @@ function getActionsCategories(root: boolean) {
const actionsByCategory: Record<string, Record<string, string>> = {
Organization: {
[t('actionGetOrg')]: "getOrg",
[t('actionUpdateOrg')]: "updateOrg",
[t('actionGetOrgUser')]: "getOrgUser",
[t('actionInviteUser')]: "inviteUser",
[t('actionListInvitations')]: "listInvitations",
[t('actionRemoveUser')]: "removeUser",
[t('actionListUsers')]: "listUsers",
[t('actionListOrgDomains')]: "listOrgDomains",
[t('updateOrgUser')]: "updateOrgUser",
[t('createOrgUser')]: "createOrgUser",
[t('actionApplyBlueprint')]: "applyBlueprint",
[t("actionGetOrg")]: "getOrg",
[t("actionUpdateOrg")]: "updateOrg",
[t("actionGetOrgUser")]: "getOrgUser",
[t("actionInviteUser")]: "inviteUser",
[t("actionListInvitations")]: "listInvitations",
[t("actionRemoveUser")]: "removeUser",
[t("actionListUsers")]: "listUsers",
[t("actionListOrgDomains")]: "listOrgDomains",
[t("updateOrgUser")]: "updateOrgUser",
[t("createOrgUser")]: "createOrgUser",
[t("actionApplyBlueprint")]: "applyBlueprint"
},
Site: {
@@ -46,25 +46,25 @@ function getActionsCategories(root: boolean) {
},
Resource: {
[t('actionCreateResource')]: "createResource",
[t('actionDeleteResource')]: "deleteResource",
[t('actionGetResource')]: "getResource",
[t('actionListResource')]: "listResources",
[t('actionUpdateResource')]: "updateResource",
[t('actionListResourceUsers')]: "listResourceUsers",
[t('actionSetResourceUsers')]: "setResourceUsers",
[t('actionSetAllowedResourceRoles')]: "setResourceRoles",
[t('actionListAllowedResourceRoles')]: "listResourceRoles",
[t('actionSetResourcePassword')]: "setResourcePassword",
[t('actionSetResourcePincode')]: "setResourcePincode",
[t('actionSetResourceHeaderAuth')]: "setResourceHeaderAuth",
[t('actionSetResourceEmailWhitelist')]: "setResourceWhitelist",
[t('actionGetResourceEmailWhitelist')]: "getResourceWhitelist",
[t('actionCreateSiteResource')]: "createSiteResource",
[t('actionDeleteSiteResource')]: "deleteSiteResource",
[t('actionGetSiteResource')]: "getSiteResource",
[t('actionListSiteResources')]: "listSiteResources",
[t('actionUpdateSiteResource')]: "updateSiteResource"
[t("actionCreateResource")]: "createResource",
[t("actionDeleteResource")]: "deleteResource",
[t("actionGetResource")]: "getResource",
[t("actionListResource")]: "listResources",
[t("actionUpdateResource")]: "updateResource",
[t("actionListResourceUsers")]: "listResourceUsers",
[t("actionSetResourceUsers")]: "setResourceUsers",
[t("actionSetAllowedResourceRoles")]: "setResourceRoles",
[t("actionListAllowedResourceRoles")]: "listResourceRoles",
[t("actionSetResourcePassword")]: "setResourcePassword",
[t("actionSetResourcePincode")]: "setResourcePincode",
[t("actionSetResourceHeaderAuth")]: "setResourceHeaderAuth",
[t("actionSetResourceEmailWhitelist")]: "setResourceWhitelist",
[t("actionGetResourceEmailWhitelist")]: "getResourceWhitelist",
[t("actionCreateSiteResource")]: "createSiteResource",
[t("actionDeleteSiteResource")]: "deleteSiteResource",
[t("actionGetSiteResource")]: "getSiteResource",
[t("actionListSiteResources")]: "listSiteResources",
[t("actionUpdateSiteResource")]: "updateSiteResource"
},
Target: {
@@ -91,23 +91,23 @@ function getActionsCategories(root: boolean) {
},
"Resource Rule": {
[t('actionCreateResourceRule')]: "createResourceRule",
[t('actionDeleteResourceRule')]: "deleteResourceRule",
[t('actionListResourceRules')]: "listResourceRules",
[t('actionUpdateResourceRule')]: "updateResourceRule"
[t("actionCreateResourceRule")]: "createResourceRule",
[t("actionDeleteResourceRule")]: "deleteResourceRule",
[t("actionListResourceRules")]: "listResourceRules",
[t("actionUpdateResourceRule")]: "updateResourceRule"
},
"Client": {
[t('actionCreateClient')]: "createClient",
[t('actionDeleteClient')]: "deleteClient",
[t('actionUpdateClient')]: "updateClient",
[t('actionListClients')]: "listClients",
[t('actionGetClient')]: "getClient"
Client: {
[t("actionCreateClient")]: "createClient",
[t("actionDeleteClient")]: "deleteClient",
[t("actionUpdateClient")]: "updateClient",
[t("actionListClients")]: "listClients",
[t("actionGetClient")]: "getClient"
},
"Logs": {
[t('actionExportLogs')]: "exportLogs",
[t('actionViewLogs')]: "viewLogs",
Logs: {
[t("actionExportLogs")]: "exportLogs",
[t("actionViewLogs")]: "viewLogs"
}
};
@@ -144,7 +144,7 @@ function getActionsCategories(root: boolean) {
if (build == "saas") {
actionsByCategory["SAAS"] = {
["Send Usage Notification Email"]: "sendUsageNotification",
["Send Usage Notification Email"]: "sendUsageNotification"
};
}
}

View File

@@ -15,18 +15,17 @@ export function PolicyDataTable<TData, TValue>({
data,
onAdd
}: DataTableProps<TData, TValue>) {
const t = useTranslations();
return (
<DataTable
columns={columns}
data={data}
persistPageSize="orgPolicies-table"
title={t('orgPolicies')}
searchPlaceholder={t('orgPoliciesSearch')}
title={t("orgPolicies")}
searchPlaceholder={t("orgPoliciesSearch")}
searchColumn="orgId"
addButtonText={t('orgPoliciesAdd')}
addButtonText={t("orgPoliciesAdd")}
onAdd={onAdd}
enableColumnVisibility={true}
stickyLeftColumn="orgId"

View File

@@ -35,13 +35,18 @@ interface Props {
onEdit: (policy: PolicyRow) => void;
}
export default function PolicyTable({ policies, onDelete, onAdd, onEdit }: Props) {
export default function PolicyTable({
policies,
onDelete,
onAdd,
onEdit
}: Props) {
const t = useTranslations();
const columns: ExtendedColumnDef<PolicyRow>[] = [
{
accessorKey: "orgId",
enableHiding: false,
friendlyName: t('orgId'),
friendlyName: t("orgId"),
header: ({ column }) => {
return (
<Button
@@ -50,7 +55,7 @@ export default function PolicyTable({ policies, onDelete, onAdd, onEdit }: Props
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('orgId')}
{t("orgId")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -58,7 +63,7 @@ export default function PolicyTable({ policies, onDelete, onAdd, onEdit }: Props
},
{
accessorKey: "roleMapping",
friendlyName: t('roleMapping'),
friendlyName: t("roleMapping"),
header: ({ column }) => {
return (
<Button
@@ -67,7 +72,7 @@ export default function PolicyTable({ policies, onDelete, onAdd, onEdit }: Props
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('roleMapping')}
{t("roleMapping")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -76,7 +81,11 @@ export default function PolicyTable({ policies, onDelete, onAdd, onEdit }: Props
const mapping = row.original.roleMapping;
return mapping ? (
<InfoPopup
text={mapping.length > 50 ? `${mapping.substring(0, 50)}...` : mapping}
text={
mapping.length > 50
? `${mapping.substring(0, 50)}...`
: mapping
}
info={mapping}
/>
) : (
@@ -86,7 +95,7 @@ export default function PolicyTable({ policies, onDelete, onAdd, onEdit }: Props
},
{
accessorKey: "orgMapping",
friendlyName: t('orgMapping'),
friendlyName: t("orgMapping"),
header: ({ column }) => {
return (
<Button
@@ -95,7 +104,7 @@ export default function PolicyTable({ policies, onDelete, onAdd, onEdit }: Props
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('orgMapping')}
{t("orgMapping")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -104,7 +113,11 @@ export default function PolicyTable({ policies, onDelete, onAdd, onEdit }: Props
const mapping = row.original.orgMapping;
return mapping ? (
<InfoPopup
text={mapping.length > 50 ? `${mapping.substring(0, 50)}...` : mapping}
text={
mapping.length > 50
? `${mapping.substring(0, 50)}...`
: mapping
}
info={mapping}
/>
) : (
@@ -123,7 +136,9 @@ export default function PolicyTable({ policies, onDelete, onAdd, onEdit }: Props
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">{t('openMenu')}</span>
<span className="sr-only">
{t("openMenu")}
</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
@@ -133,7 +148,9 @@ export default function PolicyTable({ policies, onDelete, onAdd, onEdit }: Props
onDelete(policy.orgId);
}}
>
<span className="text-red-500">{t('delete')}</span>
<span className="text-red-500">
{t("delete")}
</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -141,7 +158,7 @@ export default function PolicyTable({ policies, onDelete, onAdd, onEdit }: Props
variant={"outline"}
onClick={() => onEdit(policy)}
>
{t('edit')}
{t("edit")}
</Button>
</div>
);

View File

@@ -20,6 +20,7 @@ import {
import { useTranslations } from "next-intl";
import { Transition } from "@headlessui/react";
import * as React from "react";
import { gt, valid } from "semver";
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
import { Button } from "./ui/button";
import { Badge } from "./ui/badge";
@@ -72,11 +73,15 @@ export default function ProductUpdates({
if (!data) return null;
const latestVersion = data?.latestVersion?.data?.pangolin.latestVersion;
const currentVersion = env.app.version;
const showNewVersionPopup = Boolean(
data?.latestVersion?.data &&
ignoredVersionUpdate !==
data.latestVersion.data?.pangolin.latestVersion &&
env.app.version !== data.latestVersion.data?.pangolin.latestVersion
latestVersion &&
valid(latestVersion) &&
valid(currentVersion) &&
ignoredVersionUpdate !== latestVersion &&
gt(latestVersion, currentVersion)
);
const filteredUpdates = data.updates.filter(

View File

@@ -25,10 +25,10 @@ export function ProfessionalContentOverlay({
<div className="absolute inset-0 flex items-center justify-center bg-background/80 z-50">
<div className="text-center p-6 bg-primary/10 rounded-lg">
<h3 className="text-lg font-semibold mb-2">
{t('licenseTierProfessionalRequired')}
{t("licenseTierProfessionalRequired")}
</h3>
<p className="text-muted-foreground">
{t('licenseTierProfessionalRequiredDescription')}
{t("licenseTierProfessionalRequiredDescription")}
</p>
</div>
</div>

View File

@@ -82,13 +82,13 @@ export default function ProfileIcon() {
open={openSecurityKey}
setOpen={setOpenSecurityKey}
/>
<ChangePasswordDialog
open={openChangePassword}
setOpen={setOpenChangePassword}
<ChangePasswordDialog
open={openChangePassword}
setOpen={setOpenChangePassword}
/>
<ViewDevicesDialog
open={openViewDevices}
setOpen={setOpenViewDevices}
<ViewDevicesDialog
open={openViewDevices}
setOpen={setOpenViewDevices}
/>
<DropdownMenu>
@@ -152,9 +152,7 @@ export default function ProfileIcon() {
<DropdownMenuSeparator />
</>
)}
<DropdownMenuItem
onClick={() => setOpenViewDevices(true)}
>
<DropdownMenuItem onClick={() => setOpenViewDevices(true)}>
<Smartphone className="mr-2 h-4 w-4" />
<span>{t("viewDevices") || "View Devices"}</span>
</DropdownMenuItem>

View File

@@ -1,17 +1,9 @@
"use client";
export default function QRContainer({
children = <div/>,
outline = true
}) {
export default function QRContainer({ children = <div />, outline = true }) {
return (
<div
className={`relative w-fit border-2 rounded-md`}
>
<div className="bg-white p-6 rounded-md">
{children}
</div>
<div className={`relative w-fit border-2 rounded-md`}>
<div className="bg-white p-6 rounded-md">{children}</div>
</div>
);
}

View File

@@ -29,11 +29,7 @@ export default function RefreshButton() {
};
return (
<Button
variant="outline"
onClick={refreshData}
disabled={isRefreshing}
>
<Button variant="outline" onClick={refreshData} disabled={isRefreshing}>
<RefreshCw
className={`mr-2 h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`}
/>

View File

@@ -60,13 +60,13 @@ export default function RegenerateInvitationForm({
const t = useTranslations();
const validForOptions = [
{ hours: 24, name: t('day', {count: 1}) },
{ hours: 48, name: t('day', {count: 2}) },
{ hours: 72, name: t('day', {count: 3}) },
{ hours: 96, name: t('day', {count: 4}) },
{ hours: 120, name: t('day', {count: 5}) },
{ hours: 144, name: t('day', {count: 6}) },
{ hours: 168, name: t('day', {count: 7}) }
{ hours: 24, name: t("day", { count: 1 }) },
{ hours: 48, name: t("day", { count: 2 }) },
{ hours: 72, name: t("day", { count: 3 }) },
{ hours: 96, name: t("day", { count: 4 }) },
{ hours: 120, name: t("day", { count: 5 }) },
{ hours: 144, name: t("day", { count: 6 }) },
{ hours: 168, name: t("day", { count: 7 }) }
];
useEffect(() => {
@@ -82,8 +82,8 @@ export default function RegenerateInvitationForm({
if (!org?.org.orgId) {
toast({
variant: "destructive",
title: t('orgMissing'),
description: t('orgMissingMessage'),
title: t("orgMissing"),
description: t("orgMissingMessage"),
duration: 5000
});
return;
@@ -107,15 +107,19 @@ export default function RegenerateInvitationForm({
if (sendEmail) {
toast({
variant: "default",
title: t('inviteRegenerated'),
description: t('inviteSent', {email: invitation.email}),
title: t("inviteRegenerated"),
description: t("inviteSent", {
email: invitation.email
}),
duration: 5000
});
} else {
toast({
variant: "default",
title: t('inviteRegenerated'),
description: t('inviteGenerate', {email: invitation.email}),
title: t("inviteRegenerated"),
description: t("inviteGenerate", {
email: invitation.email
}),
duration: 5000
});
}
@@ -132,22 +136,22 @@ export default function RegenerateInvitationForm({
if (error.response?.status === 409) {
toast({
variant: "destructive",
title: t('inviteDuplicateError'),
description: t('inviteDuplicateErrorDescription'),
title: t("inviteDuplicateError"),
description: t("inviteDuplicateErrorDescription"),
duration: 5000
});
} else if (error.response?.status === 429) {
toast({
variant: "destructive",
title: t('inviteRateLimitError'),
description: t('inviteRateLimitErrorDescription'),
title: t("inviteRateLimitError"),
description: t("inviteRateLimitErrorDescription"),
duration: 5000
});
} else {
toast({
variant: "destructive",
title: t('inviteRegenerateError'),
description: t('inviteRegenerateErrorDescription'),
title: t("inviteRegenerateError"),
description: t("inviteRegenerateErrorDescription"),
duration: 5000
});
}
@@ -168,16 +172,18 @@ export default function RegenerateInvitationForm({
>
<CredenzaContent>
<CredenzaHeader>
<CredenzaTitle>{t('inviteRegenerate')}</CredenzaTitle>
<CredenzaTitle>{t("inviteRegenerate")}</CredenzaTitle>
<CredenzaDescription>
{t('inviteRegenerateDescription')}
{t("inviteRegenerateDescription")}
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
{!inviteLink ? (
<div>
<p>
{t('inviteQuestionRegenerate', {email: invitation?.email || ""})}
{t("inviteQuestionRegenerate", {
email: invitation?.email || ""
})}
</p>
<div className="flex items-center space-x-2 mt-4">
<Checkbox
@@ -188,13 +194,11 @@ export default function RegenerateInvitationForm({
}
/>
<label htmlFor="send-email">
{t('inviteSentEmail')}
{t("inviteSentEmail")}
</label>
</div>
<div className="mt-4 space-y-2">
<Label>
{t('inviteValidityPeriod')}
</Label>
<Label>{t("inviteValidityPeriod")}</Label>
<Select
value={validHours.toString()}
onValueChange={(value) =>
@@ -202,7 +206,11 @@ export default function RegenerateInvitationForm({
}
>
<SelectTrigger className="w-full">
<SelectValue placeholder={t('inviteValidityPeriodSelect')} />
<SelectValue
placeholder={t(
"inviteValidityPeriodSelect"
)}
/>
</SelectTrigger>
<SelectContent>
{validForOptions.map((option) => (
@@ -219,9 +227,7 @@ export default function RegenerateInvitationForm({
</div>
) : (
<div className="space-y-4 max-w-md">
<p>
{t('inviteRegenerateMessage')}
</p>
<p>{t("inviteRegenerateMessage")}</p>
<CopyTextBox text={inviteLink} wrapText={false} />
</div>
)}
@@ -230,18 +236,18 @@ export default function RegenerateInvitationForm({
{!inviteLink ? (
<>
<CredenzaClose asChild>
<Button variant="outline">{t('cancel')}</Button>
<Button variant="outline">{t("cancel")}</Button>
</CredenzaClose>
<Button
onClick={handleRegenerate}
loading={loading}
>
{t('inviteRegenerateButton')}
{t("inviteRegenerateButton")}
</Button>
</>
) : (
<CredenzaClose asChild>
<Button variant="outline">{t('close')}</Button>
<Button variant="outline">{t("close")}</Button>
</CredenzaClose>
)}
</CredenzaFooter>

View File

@@ -342,7 +342,10 @@ export default function ResetPasswordForm({
<FormControl>
<Input
{...field}
disabled={env.email.emailEnabled}
disabled={
env.email
.emailEnabled
}
/>
</FormControl>
<FormMessage />

View File

@@ -6,7 +6,7 @@ import {
CardContent,
CardFooter,
CardHeader,
CardTitle,
CardTitle
} from "@app/components/ui/card";
import Link from "next/link";
import { useTranslations } from "next-intl";
@@ -18,14 +18,14 @@ export default function ResourceAccessDenied() {
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle className="text-center text-2xl font-bold">
{t('accessDenied')}
{t("accessDenied")}
</CardTitle>
</CardHeader>
<CardContent>
{t('accessDeniedDescription')}
{t("accessDeniedDescription")}
<div className="text-center mt-4">
<Button>
<Link href="/">{t('goHome')}</Link>
<Link href="/">{t("goHome")}</Link>
</Button>
</div>
</CardContent>

View File

@@ -335,8 +335,12 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
: t("authenticationRequest", { name: resourceName });
}
const logoWidth = isUnlocked() ? env.branding.logo?.authPage?.width || 100 : 100;
const logoHeight = isUnlocked() ? env.branding.logo?.authPage?.height || 100 : 100;
const logoWidth = isUnlocked()
? env.branding.logo?.authPage?.width || 100
: 100;
const logoHeight = isUnlocked()
? env.branding.logo?.authPage?.height || 100
: 100;
return (
<div>

View File

@@ -17,7 +17,7 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
type ResourceInfoBoxType = {};
export default function ResourceInfoBox({ }: ResourceInfoBoxType) {
export default function ResourceInfoBox({}: ResourceInfoBoxType) {
const { resource, authInfo, updateResource } = useResourceContext();
const { env } = useEnvContext();
@@ -25,7 +25,6 @@ export default function ResourceInfoBox({ }: ResourceInfoBoxType) {
const fullUrl = `${resource.ssl ? "https" : "http"}://${toUnicode(resource.fullDomain || "")}`;
return (
<Alert>
<AlertDescription>
@@ -34,9 +33,7 @@ export default function ResourceInfoBox({ }: ResourceInfoBoxType) {
cols={resource.http && env.flags.usePangolinDns ? 5 : 4}
>
<InfoSection>
<InfoSectionTitle>
{t("identifier")}
</InfoSectionTitle>
<InfoSectionTitle>{t("identifier")}</InfoSectionTitle>
<InfoSectionContent>
{resource.niceId}
</InfoSectionContent>
@@ -49,10 +46,10 @@ export default function ResourceInfoBox({ }: ResourceInfoBoxType) {
</InfoSectionTitle>
<InfoSectionContent>
{authInfo.password ||
authInfo.pincode ||
authInfo.sso ||
authInfo.whitelist ||
authInfo.headerAuth ? (
authInfo.pincode ||
authInfo.sso ||
authInfo.whitelist ||
authInfo.headerAuth ? (
<div className="flex items-start space-x-2">
<ShieldCheck className="w-4 h-4 mt-0.5 flex-shrink-0 text-green-500" />
<span>{t("protected")}</span>

View File

@@ -4,27 +4,26 @@ import {
CardContent,
CardFooter,
CardHeader,
CardTitle,
CardTitle
} from "@app/components/ui/card";
import Link from "next/link";
import { getTranslations } from "next-intl/server";
export default async function ResourceNotFound() {
const t = await getTranslations();
return (
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle className="text-center text-2xl font-bold">
{t('resourceNotFound')}
{t("resourceNotFound")}
</CardTitle>
</CardHeader>
<CardContent>
{t('resourceNotFoundDescription')}
{t("resourceNotFoundDescription")}
<div className="text-center mt-4">
<Button>
<Link href="/">{t('goHome')}</Link>
<Link href="/">{t("goHome")}</Link>
</Button>
</div>
</CardContent>

View File

@@ -14,7 +14,10 @@ interface RestartDomainButtonProps {
domainId: string;
}
export default function RestartDomainButton({ orgId, domainId }: RestartDomainButtonProps) {
export default function RestartDomainButton({
orgId,
domainId
}: RestartDomainButtonProps) {
const router = useRouter();
const api = createApiClient(useEnvContext());
const [isRestarting, setIsRestarting] = useState(false);

View File

@@ -1,10 +1,8 @@
"use client";
import {
ColumnDef,
} from "@tanstack/react-table";
import { ColumnDef } from "@tanstack/react-table";
import { DataTable } from "@app/components/ui/data-table";
import { useTranslations } from 'next-intl';
import { useTranslations } from "next-intl";
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
@@ -21,7 +19,6 @@ export function RolesDataTable<TData, TValue>({
onRefresh,
isRefreshing
}: DataTableProps<TData, TValue>) {
const t = useTranslations();
return (
@@ -29,13 +26,13 @@ export function RolesDataTable<TData, TValue>({
columns={columns}
data={data}
persistPageSize="roles-table"
title={t('roles')}
searchPlaceholder={t('accessRolesSearch')}
title={t("roles")}
searchPlaceholder={t("accessRolesSearch")}
searchColumn="name"
onAdd={createRole}
onRefresh={onRefresh}
isRefreshing={isRefreshing}
addButtonText={t('accessRolesAdd')}
addButtonText={t("accessRolesAdd")}
enableColumnVisibility={true}
stickyLeftColumn="name"
stickyRightColumn="actions"

View File

@@ -84,7 +84,7 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
{
accessorKey: "description",
friendlyName: t("description"),
header: () => (<span className="p-3">{t("description")}</span>)
header: () => <span className="p-3">{t("description")}</span>
},
{
id: "actions",

View File

@@ -30,4 +30,3 @@ export function SecurityFeaturesAlert() {
</>
);
}

View File

@@ -530,10 +530,14 @@ export default function SecurityKeyForm({
<div className="flex flex-col items-center justify-center py-8 text-center">
<Shield className="mb-2 h-12 w-12 text-muted-foreground" />
<p className="text-sm text-muted-foreground">
{t("securityKeyNoKeysRegistered")}
{t(
"securityKeyNoKeysRegistered"
)}
</p>
<p className="text-xs text-muted-foreground">
{t("securityKeyNoKeysDescription")}
{t(
"securityKeyNoKeysDescription"
)}
</p>
</div>
)}
@@ -717,7 +721,9 @@ export default function SecurityKeyForm({
{t("securityKeyRemoveTitle")}
</CredenzaTitle>
<CredenzaDescription>
{t("securityKeyRemoveDescription", { name: selectedSecurityKey!.name! })}
{t("securityKeyRemoveDescription", {
name: selectedSecurityKey!.name!
})}
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
@@ -805,7 +811,9 @@ export default function SecurityKeyForm({
render={({ field }) => (
<FormItem>
<FormLabel>
{t("securityKeyTwoFactorCode")}
{t(
"securityKeyTwoFactorCode"
)}
</FormLabel>
<FormControl>
<Input

View File

@@ -78,24 +78,27 @@ export default function SetResourceHeaderAuthForm({
async function onSubmit(data: SetHeaderAuthFormValues) {
setLoading(true);
api.post<AxiosResponse<Resource>>(`/resource/${resourceId}/header-auth`, {
user: data.user,
password: data.password
})
api.post<AxiosResponse<Resource>>(
`/resource/${resourceId}/header-auth`,
{
user: data.user,
password: data.password
}
)
.catch((e) => {
toast({
variant: "destructive",
title: t('resourceErrorHeaderAuthSetup'),
title: t("resourceErrorHeaderAuthSetup"),
description: formatAxiosError(
e,
t('resourceErrorHeaderAuthSetupDescription')
t("resourceErrorHeaderAuthSetupDescription")
)
});
})
.then(() => {
toast({
title: t('resourceHeaderAuthSetup'),
description: t('resourceHeaderAuthSetupDescription')
title: t("resourceHeaderAuthSetup"),
description: t("resourceHeaderAuthSetupDescription")
});
if (onSetHeaderAuth) {
@@ -117,9 +120,11 @@ export default function SetResourceHeaderAuthForm({
>
<CredenzaContent>
<CredenzaHeader>
<CredenzaTitle>{t('resourceHeaderAuthSetupTitle')}</CredenzaTitle>
<CredenzaTitle>
{t("resourceHeaderAuthSetupTitle")}
</CredenzaTitle>
<CredenzaDescription>
{t('resourceHeaderAuthSetupTitleDescription')}
{t("resourceHeaderAuthSetupTitleDescription")}
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
@@ -134,7 +139,7 @@ export default function SetResourceHeaderAuthForm({
name="user"
render={({ field }) => (
<FormItem>
<FormLabel>{t('user')}</FormLabel>
<FormLabel>{t("user")}</FormLabel>
<FormControl>
<Input
autoComplete="off"
@@ -151,7 +156,9 @@ export default function SetResourceHeaderAuthForm({
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>{t('password')}</FormLabel>
<FormLabel>
{t("password")}
</FormLabel>
<FormControl>
<Input
autoComplete="off"
@@ -168,7 +175,7 @@ export default function SetResourceHeaderAuthForm({
</CredenzaBody>
<CredenzaFooter>
<CredenzaClose asChild>
<Button variant="outline">{t('close')}</Button>
<Button variant="outline">{t("close")}</Button>
</CredenzaClose>
<Button
type="submit"
@@ -176,7 +183,7 @@ export default function SetResourceHeaderAuthForm({
loading={loading}
disabled={loading}
>
{t('resourceHeaderAuthSubmit')}
{t("resourceHeaderAuthSubmit")}
</Button>
</CredenzaFooter>
</CredenzaContent>

View File

@@ -81,17 +81,17 @@ export default function SetResourcePasswordForm({
.catch((e) => {
toast({
variant: "destructive",
title: t('resourceErrorPasswordSetup'),
title: t("resourceErrorPasswordSetup"),
description: formatAxiosError(
e,
t('resourceErrorPasswordSetupDescription')
t("resourceErrorPasswordSetupDescription")
)
});
})
.then(() => {
toast({
title: t('resourcePasswordSetup'),
description: t('resourcePasswordSetupDescription')
title: t("resourcePasswordSetup"),
description: t("resourcePasswordSetupDescription")
});
if (onSetPassword) {
@@ -113,9 +113,11 @@ export default function SetResourcePasswordForm({
>
<CredenzaContent>
<CredenzaHeader>
<CredenzaTitle>{t('resourcePasswordSetupTitle')}</CredenzaTitle>
<CredenzaTitle>
{t("resourcePasswordSetupTitle")}
</CredenzaTitle>
<CredenzaDescription>
{t('resourcePasswordSetupTitleDescription')}
{t("resourcePasswordSetupTitleDescription")}
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
@@ -130,7 +132,9 @@ export default function SetResourcePasswordForm({
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>{t('password')}</FormLabel>
<FormLabel>
{t("password")}
</FormLabel>
<FormControl>
<Input
autoComplete="off"
@@ -147,7 +151,7 @@ export default function SetResourcePasswordForm({
</CredenzaBody>
<CredenzaFooter>
<CredenzaClose asChild>
<Button variant="outline">{t('close')}</Button>
<Button variant="outline">{t("close")}</Button>
</CredenzaClose>
<Button
type="submit"
@@ -155,7 +159,7 @@ export default function SetResourcePasswordForm({
loading={loading}
disabled={loading}
>
{t('resourcePasswordSubmit')}
{t("resourcePasswordSubmit")}
</Button>
</CredenzaFooter>
</CredenzaContent>

View File

@@ -87,17 +87,17 @@ export default function SetResourcePincodeForm({
.catch((e) => {
toast({
variant: "destructive",
title: t('resourceErrorPincodeSetup'),
title: t("resourceErrorPincodeSetup"),
description: formatAxiosError(
e,
t('resourceErrorPincodeSetupDescription')
t("resourceErrorPincodeSetupDescription")
)
});
})
.then(() => {
toast({
title: t('resourcePincodeSetup'),
description: t('resourcePincodeSetupDescription')
title: t("resourcePincodeSetup"),
description: t("resourcePincodeSetupDescription")
});
if (onSetPincode) {
@@ -119,9 +119,11 @@ export default function SetResourcePincodeForm({
>
<CredenzaContent>
<CredenzaHeader>
<CredenzaTitle>{t('resourcePincodeSetupTitle')}</CredenzaTitle>
<CredenzaTitle>
{t("resourcePincodeSetupTitle")}
</CredenzaTitle>
<CredenzaDescription>
{t('resourcePincodeSetupTitleDescription')}
{t("resourcePincodeSetupTitleDescription")}
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
@@ -136,7 +138,9 @@ export default function SetResourcePincodeForm({
name="pincode"
render={({ field }) => (
<FormItem>
<FormLabel>{t('resourcePincode')}</FormLabel>
<FormLabel>
{t("resourcePincode")}
</FormLabel>
<FormControl>
<div className="flex justify-center">
<InputOTP
@@ -182,7 +186,7 @@ export default function SetResourcePincodeForm({
</CredenzaBody>
<CredenzaFooter>
<CredenzaClose asChild>
<Button variant="outline">{t('close')}</Button>
<Button variant="outline">{t("close")}</Button>
</CredenzaClose>
<Button
type="submit"
@@ -190,7 +194,7 @@ export default function SetResourcePincodeForm({
loading={loading}
disabled={loading}
>
{t('resourcePincodeSubmit')}
{t("resourcePincodeSubmit")}
</Button>
</CredenzaFooter>
</CredenzaContent>

View File

@@ -1,8 +1,6 @@
"use client";
import {
ColumnDef,
} from "@tanstack/react-table";
import { ColumnDef } from "@tanstack/react-table";
import { DataTable } from "@app/components/ui/data-table";
import { useTranslations } from "next-intl";
@@ -21,7 +19,6 @@ export function ShareLinksDataTable<TData, TValue>({
onRefresh,
isRefreshing
}: DataTableProps<TData, TValue>) {
const t = useTranslations();
return (
@@ -29,13 +26,13 @@ export function ShareLinksDataTable<TData, TValue>({
columns={columns}
data={data}
persistPageSize="shareLinks-table"
title={t('shareLinks')}
searchPlaceholder={t('shareSearch')}
title={t("shareLinks")}
searchPlaceholder={t("shareSearch")}
searchColumn="name"
onAdd={createShareLink}
onRefresh={onRefresh}
isRefreshing={isRefreshing}
addButtonText={t('shareCreate')}
addButtonText={t("shareCreate")}
enableColumnVisibility={true}
stickyLeftColumn="resourceName"
stickyRightColumn="delete"

View File

@@ -34,7 +34,7 @@ export const ShareableLinksSplash = () => {
<button
onClick={handleDismiss}
className="absolute top-2 right-2 p-2"
aria-label={t('dismiss')}
aria-label={t("dismiss")}
>
<X className="w-5 h-5" />
</button>
@@ -42,23 +42,21 @@ export const ShareableLinksSplash = () => {
<div className="space-y-4">
<h3 className="text-xl font-semibold flex items-center gap-2">
<Link className="text-blue-500" />
{t('share')}
{t("share")}
</h3>
<p className="text-sm">
{t('shareDescription2')}
</p>
<p className="text-sm">{t("shareDescription2")}</p>
<ul className="text-sm text-muted-foreground space-y-2">
<li className="flex items-center gap-2">
<Share className="text-green-500 w-4 h-4" />
{t('shareEasyCreate')}
{t("shareEasyCreate")}
</li>
<li className="flex items-center gap-2">
<Clock className="text-yellow-500 w-4 h-4" />
{t('shareConfigurableExpirationDuration')}
{t("shareConfigurableExpirationDuration")}
</li>
<li className="flex items-center gap-2">
<Lock className="text-red-500 w-4 h-4" />
{t('shareSecureAndRevocable')}
{t("shareSecureAndRevocable")}
</li>
</ul>
</div>

View File

@@ -124,7 +124,9 @@ export default function ShareLinksTable({
cell: ({ row }) => {
const r = row.original;
return (
<Link href={`/${orgId}/settings/resources/proxy/${r.resourceNiceId}`}>
<Link
href={`/${orgId}/settings/resources/proxy/${r.resourceNiceId}`}
>
<Button variant="outline">
{r.resourceName}
<ArrowUpRight className="ml-2 h-4 w-4" />
@@ -289,7 +291,8 @@ export default function ShareLinksTable({
{/* </DropdownMenuItem> */}
{/* </DropdownMenuContent> */}
{/* </DropdownMenu> */}
<Button variant={"outline"}
<Button
variant={"outline"}
onClick={() =>
deleteSharelink(row.original.accessTokenId)
}

View File

@@ -72,7 +72,7 @@ function CollapsibleNavItem({
isUnlocked
}: CollapsibleNavItemProps) {
const storageKey = `pangolin-sidebar-expanded-${item.title}`;
// Get initial state from localStorage or use isChildActive
const getInitialState = (): boolean => {
if (typeof window === "undefined") {

View File

@@ -56,13 +56,15 @@ export function SidebarSupportButton({
const t = useTranslations();
const form = useForm<SupportFormValues>({
resolver: zodResolver(z.object({
subject: z
.string()
.min(1, t("supportSubjectRequired"))
.max(255, t("supportSubjectMaxLength")),
body: z.string().min(1, t("supportMessageRequired"))
})),
resolver: zodResolver(
z.object({
subject: z
.string()
.min(1, t("supportSubjectRequired"))
.max(255, t("supportSubjectMaxLength")),
body: z.string().min(1, t("supportMessageRequired"))
})
),
defaultValues: {
subject: "",
body: ""
@@ -127,7 +129,9 @@ export function SidebarSupportButton({
</PopoverTrigger>
</TooltipTrigger>
<TooltipContent side="right" sideOffset={8}>
<p>{t("support", { defaultValue: "Support" })}</p>
<p>
{t("support", { defaultValue: "Support" })}
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
@@ -195,7 +199,9 @@ export function SidebarSupportButton({
{isSuccess ? (
<div className="flex flex-col items-center justify-center py-8 space-y-4">
<CheckCircle2 className="h-16 w-16 text-green-500" />
<h3 className="text-lg font-semibold">{t("supportMessageSent")}</h3>
<h3 className="text-lg font-semibold">
{t("supportMessageSent")}
</h3>
<p className="text-sm text-muted-foreground text-center">
{t("supportWillContact")}
</p>
@@ -206,68 +212,77 @@ export function SidebarSupportButton({
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-4"
>
<FormItem>
<FormLabel>{t("supportReplyTo")}</FormLabel>
<FormControl>
<Input
value={user?.email || ""}
disabled
/>
</FormControl>
</FormItem>
<FormItem>
<FormLabel>{t("supportReplyTo")}</FormLabel>
<FormControl>
<Input value={user?.email || ""} disabled />
</FormControl>
</FormItem>
<FormField
control={form.control}
name="subject"
render={({ field }) => (
<FormItem>
<FormLabel>{t("supportSubject")}</FormLabel>
<FormControl>
<Input
placeholder={t("supportSubjectPlaceholder")}
disabled={isSubmitting}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="subject"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("supportSubject")}
</FormLabel>
<FormControl>
<Input
placeholder={t(
"supportSubjectPlaceholder"
)}
disabled={isSubmitting}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="body"
render={({ field }) => (
<FormItem>
<FormLabel>{t("supportMessage")}</FormLabel>
<FormControl>
<Textarea
placeholder={t("supportMessagePlaceholder")}
rows={5}
disabled={isSubmitting}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="body"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("supportMessage")}
</FormLabel>
<FormControl>
<Textarea
placeholder={t(
"supportMessagePlaceholder"
)}
rows={5}
disabled={isSubmitting}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end gap-2">
<Button
type="button"
variant="outline"
onClick={() => setIsOpen(false)}
disabled={isSubmitting}
>
{t("cancel")}
</Button>
<Button type="submit" disabled={isSubmitting} loading={isSubmitting}>
{t("supportSend")}
</Button>
</div>
</form>
</Form>
<div className="flex justify-end gap-2">
<Button
type="button"
variant="outline"
onClick={() => setIsOpen(false)}
disabled={isSubmitting}
>
{t("cancel")}
</Button>
<Button
type="submit"
disabled={isSubmitting}
loading={isSubmitting}
>
{t("supportSend")}
</Button>
</div>
</form>
</Form>
)}
</PopoverContent>
</Popover>

View File

@@ -11,10 +11,9 @@ import {
import { useTranslations } from "next-intl";
import { useEnvContext } from "@app/hooks/useEnvContext";
type SiteInfoCardProps = {};
export default function SiteInfoCard({ }: SiteInfoCardProps) {
export default function SiteInfoCard({}: SiteInfoCardProps) {
const { site, updateSite } = useSiteContext();
const t = useTranslations();
const { env } = useEnvContext();
@@ -31,18 +30,13 @@ export default function SiteInfoCard({ }: SiteInfoCardProps) {
}
};
return (
<Alert>
<AlertDescription>
<InfoSections cols={4}>
<InfoSection>
<InfoSectionTitle>
{t("identifier")}
</InfoSectionTitle>
<InfoSectionContent>
{site.niceId}
</InfoSectionContent>
<InfoSectionTitle>{t("identifier")}</InfoSectionTitle>
<InfoSectionContent>{site.niceId}</InfoSectionContent>
</InfoSection>
{(site.type == "newt" || site.type == "wireguard") && (
<>

View File

@@ -64,18 +64,20 @@ export function SitePriceCalculator({
<CredenzaHeader>
<CredenzaTitle>
{mode === "license"
? t('licensePurchase')
: t('licensePurchaseSites')}
? t("licensePurchase")
: t("licensePurchaseSites")}
</CredenzaTitle>
<CredenzaDescription>
{t('licensePurchaseDescription', {selectedMode: mode})}
{t("licensePurchaseDescription", {
selectedMode: mode
})}
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
<div className="space-y-6">
<div className="flex flex-col items-center space-y-4">
<div className="text-sm font-medium text-muted-foreground">
{t('numberOfSites')}
{t("numberOfSites")}
</div>
<div className="flex items-center space-x-4">
<Button
@@ -83,7 +85,7 @@ export function SitePriceCalculator({
size="icon"
onClick={decrementSites}
disabled={siteCount <= 1}
aria-label={t('sitestCountDecrease')}
aria-label={t("sitestCountDecrease")}
>
<MinusCircle className="h-5 w-5" />
</Button>
@@ -94,7 +96,7 @@ export function SitePriceCalculator({
variant="ghost"
size="icon"
onClick={incrementSites}
aria-label={t('sitestCountIncrease')}
aria-label={t("sitestCountIncrease")}
>
<PlusCircle className="h-5 w-5" />
</Button>
@@ -103,14 +105,14 @@ export function SitePriceCalculator({
<div className="border-t pt-4">
<p className="text-muted-foreground text-sm mt-2 text-center">
{t('licensePricingPage')}
{t("licensePricingPage")}
<a
href="https://docs.fossorial.io/pricing"
target="_blank"
rel="noopener noreferrer"
className="underline"
>
{t('pricingPage')}
{t("pricingPage")}
</a>
.
</p>
@@ -119,10 +121,10 @@ export function SitePriceCalculator({
</CredenzaBody>
<CredenzaFooter>
<CredenzaClose asChild>
<Button variant="outline">{t('cancel')}</Button>
<Button variant="outline">{t("cancel")}</Button>
</CredenzaClose>
<Button onClick={continueToPayment}>
{t('pricingPortal')}
{t("pricingPortal")}
</Button>
</CredenzaFooter>
</CredenzaContent>

View File

@@ -23,7 +23,6 @@ export function SitesDataTable<TData, TValue>({
columnVisibility,
enableColumnVisibility
}: DataTableProps<TData, TValue>) {
const t = useTranslations();
return (
@@ -31,11 +30,11 @@ export function SitesDataTable<TData, TValue>({
columns={columns}
data={data}
persistPageSize="sites-table"
title={t('sites')}
searchPlaceholder={t('searchSitesProgress')}
title={t("sites")}
searchPlaceholder={t("searchSitesProgress")}
searchColumn="name"
onAdd={createSite}
addButtonText={t('siteAdd')}
addButtonText={t("siteAdd")}
onRefresh={onRefresh}
isRefreshing={isRefreshing}
defaultSort={{

View File

@@ -6,7 +6,7 @@ import { Button } from "@/components/ui/button";
import { ArrowRight, DockIcon as Docker, Globe, Server, X } from "lucide-react";
import Link from "next/link";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useTranslations } from 'next-intl';
import { useTranslations } from "next-intl";
export const SitesSplashCard = () => {
const [isDismissed, setIsDismissed] = useState(true);
@@ -38,7 +38,7 @@ export const SitesSplashCard = () => {
<button
onClick={handleDismiss}
className="absolute top-2 right-2 p-2"
aria-label={t('dismiss')}
aria-label={t("dismiss")}
>
<X className="w-5 h-5" />
</button>
@@ -46,19 +46,17 @@ export const SitesSplashCard = () => {
<div className="space-y-4">
<h3 className="text-xl font-semibold flex items-center gap-2">
<Globe className="text-blue-500" />
Newt ({t('recommended')})
Newt ({t("recommended")})
</h3>
<p className="text-sm">
{t('siteNewtDescription')}
</p>
<p className="text-sm">{t("siteNewtDescription")}</p>
<ul className="text-sm text-muted-foreground space-y-2">
<li className="flex items-center gap-2">
<Server className="text-green-500 w-4 h-4" />
{t('siteRunsInDocker')}
{t("siteRunsInDocker")}
</li>
<li className="flex items-center gap-2">
<Server className="text-green-500 w-4 h-4" />
{t('siteRunsInShell')}
{t("siteRunsInShell")}
</li>
</ul>
@@ -72,7 +70,7 @@ export const SitesSplashCard = () => {
className="w-full flex items-center"
variant="secondary"
>
{t('siteInstallNewt')}{" "}
{t("siteInstallNewt")}{" "}
<ArrowRight className="ml-2 w-4 h-4" />
</Button>
</Link>
@@ -80,19 +78,17 @@ export const SitesSplashCard = () => {
</div>
<div className="space-y-4">
<h3 className="text-xl font-semibold flex items-center gap-2">
{t('siteWg')}
{t("siteWg")}
</h3>
<p className="text-sm">
{t('siteWgAnyClients')}
</p>
<p className="text-sm">{t("siteWgAnyClients")}</p>
<ul className="text-sm text-muted-foreground space-y-2">
<li className="flex items-center gap-2">
<Docker className="text-purple-500 w-4 h-4" />
{t('siteWgCompatibleAllClients')}
{t("siteWgCompatibleAllClients")}
</li>
<li className="flex items-center gap-2">
<Server className="text-purple-500 w-4 h-4" />
{t('siteWgManualConfigurationRequired')}
{t("siteWgManualConfigurationRequired")}
</li>
</ul>
</div>

View File

@@ -61,14 +61,14 @@ export function StrategySelect<TValue extends string>({
/>
<div className="flex gap-3 pl-7">
{option.icon && (
<div className="mt-1">
{option.icon}
</div>
<div className="mt-1">{option.icon}</div>
)}
<div className="flex-1">
<div className="font-medium">{option.title}</div>
<div className="text-sm text-muted-foreground">
{typeof option.description === 'string' ? option.description : option.description}
{typeof option.description === "string"
? option.description
: option.description}
</div>
</div>
</div>

View File

@@ -3,10 +3,9 @@
import React from "react";
import confetti from "canvas-confetti";
import { Star } from "lucide-react";
import { useTranslations } from 'next-intl';
import { useTranslations } from "next-intl";
export default function SupporterMessage({ tier }: { tier: string }) {
const t = useTranslations();
return (
@@ -33,9 +32,9 @@ export default function SupporterMessage({ tier }: { tier: string }) {
>
Pangolin
</span>
<Star className="w-3 h-3"/>
<Star className="w-3 h-3" />
<div className="absolute left-1/2 transform -translate-x-1/2 -top-10 hidden group-hover:block text-primary text-sm rounded-md border shadow-md px-4 py-2 pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity">
{t('componentsSupporterMessage', {tier: tier})}
{t("componentsSupporterMessage", { tier: tier })}
</div>
</div>
);

View File

@@ -13,7 +13,7 @@ import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
TooltipTrigger
} from "@app/components/ui/tooltip";
import { Button } from "./ui/button";
import {
@@ -60,7 +60,9 @@ interface SupporterStatusProps {
isCollapsed?: boolean;
}
export default function SupporterStatus({ isCollapsed = false }: SupporterStatusProps) {
export default function SupporterStatus({
isCollapsed = false
}: SupporterStatusProps) {
const { supporterStatus, updateSupporterStatus } =
useSupporterStatusContext();
const [supportOpen, setSupportOpen] = useState(false);
@@ -72,11 +74,9 @@ export default function SupporterStatus({ isCollapsed = false }: SupporterStatus
const t = useTranslations();
const formSchema = z.object({
githubUsername: z
.string()
.nonempty({
error: "GitHub username is required"
}),
githubUsername: z.string().nonempty({
error: "GitHub username is required"
}),
key: z.string().nonempty({
error: "Supporter key is required"
})
@@ -112,8 +112,8 @@ export default function SupporterStatus({ isCollapsed = false }: SupporterStatus
if (!data || !data.valid) {
toast({
variant: "destructive",
title: t('supportKeyInvalid'),
description: t('supportKeyInvalidDescription')
title: t("supportKeyInvalid"),
description: t("supportKeyInvalidDescription")
});
return;
}
@@ -121,8 +121,8 @@ export default function SupporterStatus({ isCollapsed = false }: SupporterStatus
// Trigger the toast
toast({
variant: "default",
title: t('supportKeyValid'),
description: t('supportKeyValidDescription')
title: t("supportKeyValid"),
description: t("supportKeyValidDescription")
});
// Fireworks-style confetti
@@ -178,10 +178,10 @@ export default function SupporterStatus({ isCollapsed = false }: SupporterStatus
} catch (error) {
toast({
variant: "destructive",
title: t('error'),
title: t("error"),
description: formatAxiosError(
error,
t('supportKeyErrorValidationDescription')
t("supportKeyErrorValidationDescription")
)
});
return;
@@ -198,48 +198,44 @@ export default function SupporterStatus({ isCollapsed = false }: SupporterStatus
>
<CredenzaContent className="max-w-3xl">
<CredenzaHeader>
<CredenzaTitle>
{t('supportKey')}
</CredenzaTitle>
<CredenzaTitle>{t("supportKey")}</CredenzaTitle>
</CredenzaHeader>
<CredenzaBody>
<p>
{t('supportKeyDescription')}
</p>
<p>{t("supportKeyDescription")}</p>
<p>{t("supportKeyPet")}</p>
<p>
{t('supportKeyPet')}
</p>
<p>
{t('supportKeyPurchase')}{" "}
{t("supportKeyPurchase")}{" "}
<Link
href="https://supporters.fossorial.io/"
target="_blank"
rel="noopener noreferrer"
className="underline"
>
{t('supportKeyPurchaseLink')}
{t("supportKeyPurchaseLink")}
</Link>{" "}
{t('supportKeyPurchase2')}{" "}
{t("supportKeyPurchase2")}{" "}
<Link
href="https://docs.pangolin.net/self-host/supporter-program"
target="_blank"
rel="noopener noreferrer"
className="underline"
>
{t('supportKeyLearnMore')}
{t("supportKeyLearnMore")}
</Link>
</p>
<div className="py-6">
<p className="mb-3 text-center">
{t('supportKeyOptions')}
{t("supportKeyOptions")}
</p>
<div className="grid md:grid-cols-2 grid-cols-1 gap-8">
<Card>
<CardHeader>
<CardTitle>{t('supportKetOptionFull')}</CardTitle>
<CardTitle>
{t("supportKetOptionFull")}
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-4xl mb-6">$95</p>
@@ -247,19 +243,19 @@ export default function SupporterStatus({ isCollapsed = false }: SupporterStatus
<li className="flex items-center gap-2">
<Check className="h-6 w-6 text-green-500" />
<span className="text-muted-foreground">
{t('forWholeServer')}
{t("forWholeServer")}
</span>
</li>
<li className="flex items-center gap-2">
<Check className="h-6 w-6 text-green-500" />
<span className="text-muted-foreground">
{t('lifetimePurchase')}
{t("lifetimePurchase")}
</span>
</li>
<li className="flex items-center gap-2">
<Check className="h-6 w-6 text-green-500" />
<span className="text-muted-foreground">
{t('supporterStatus')}
{t("supporterStatus")}
</span>
</li>
</ul>
@@ -272,7 +268,7 @@ export default function SupporterStatus({ isCollapsed = false }: SupporterStatus
className="w-full"
>
<Button className="w-full">
{t('buy')}
{t("buy")}
</Button>
</Link>
</CardFooter>
@@ -282,7 +278,9 @@ export default function SupporterStatus({ isCollapsed = false }: SupporterStatus
className={`${supporterStatus?.tier === "Limited Supporter" ? "opacity-50" : ""}`}
>
<CardHeader>
<CardTitle>{t('supportKeyOptionLimited')}</CardTitle>
<CardTitle>
{t("supportKeyOptionLimited")}
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-4xl mb-6">$25</p>
@@ -290,19 +288,19 @@ export default function SupporterStatus({ isCollapsed = false }: SupporterStatus
<li className="flex items-center gap-2">
<Check className="h-6 w-6 text-green-500" />
<span className="text-muted-foreground">
{t('forFiveUsers')}
{t("forFiveUsers")}
</span>
</li>
<li className="flex items-center gap-2">
<Check className="h-6 w-6 text-green-500" />
<span className="text-muted-foreground">
{t('lifetimePurchase')}
{t("lifetimePurchase")}
</span>
</li>
<li className="flex items-center gap-2">
<Check className="h-6 w-6 text-green-500" />
<span className="text-muted-foreground">
{t('supporterStatus')}
{t("supporterStatus")}
</span>
</li>
</ul>
@@ -317,7 +315,7 @@ export default function SupporterStatus({ isCollapsed = false }: SupporterStatus
className="w-full"
>
<Button className="w-full">
{t('buy')}
{t("buy")}
</Button>
</Link>
) : (
@@ -328,7 +326,7 @@ export default function SupporterStatus({ isCollapsed = false }: SupporterStatus
"Limited Supporter"
}
>
{t('buy')}
{t("buy")}
</Button>
)}
</CardFooter>
@@ -344,20 +342,20 @@ export default function SupporterStatus({ isCollapsed = false }: SupporterStatus
setKeyOpen(true);
}}
>
{t('supportKeyRedeem')}
{t("supportKeyRedeem")}
</Button>
<Button
variant="ghost"
className="w-full"
onClick={() => hide()}
>
{t('supportKeyHideSevenDays')}
{t("supportKeyHideSevenDays")}
</Button>
</div>
</CredenzaBody>
<CredenzaFooter>
<CredenzaClose asChild>
<Button variant="outline">{t('close')}</Button>
<Button variant="outline">{t("close")}</Button>
</CredenzaClose>
</CredenzaFooter>
</CredenzaContent>
@@ -371,9 +369,9 @@ export default function SupporterStatus({ isCollapsed = false }: SupporterStatus
>
<CredenzaContent>
<CredenzaHeader>
<CredenzaTitle>{t('supportKeyEnter')}</CredenzaTitle>
<CredenzaTitle>{t("supportKeyEnter")}</CredenzaTitle>
<CredenzaDescription>
{t('supportKeyEnterDescription')}
{t("supportKeyEnterDescription")}
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
@@ -389,7 +387,7 @@ export default function SupporterStatus({ isCollapsed = false }: SupporterStatus
render={({ field }) => (
<FormItem>
<FormLabel>
{t('githubUsername')}
{t("githubUsername")}
</FormLabel>
<FormControl>
<Input {...field} />
@@ -403,7 +401,9 @@ export default function SupporterStatus({ isCollapsed = false }: SupporterStatus
name="key"
render={({ field }) => (
<FormItem>
<FormLabel>{t('supportKeyInput')}</FormLabel>
<FormLabel>
{t("supportKeyInput")}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@@ -416,10 +416,10 @@ export default function SupporterStatus({ isCollapsed = false }: SupporterStatus
</CredenzaBody>
<CredenzaFooter>
<CredenzaClose asChild>
<Button variant="outline">{t('close')}</Button>
<Button variant="outline">{t("close")}</Button>
</CredenzaClose>
<Button type="submit" form="form">
{t('submit')}
{t("submit")}
</Button>
</CredenzaFooter>
</CredenzaContent>
@@ -441,7 +441,7 @@ export default function SupporterStatus({ isCollapsed = false }: SupporterStatus
</Button>
</TooltipTrigger>
<TooltipContent side="right" sideOffset={8}>
<p>{t('supportKeyBuy')}</p>
<p>{t("supportKeyBuy")}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
@@ -453,7 +453,7 @@ export default function SupporterStatus({ isCollapsed = false }: SupporterStatus
setPurchaseOptionsOpen(true);
}}
>
{t('supportKeyBuy')}
{t("supportKeyBuy")}
</Button>
)
) : null}

View File

@@ -39,7 +39,9 @@ export function TopbarNav({
href={item.href.replace("{orgId}", orgId || "")}
className={cn(
"relative md:px-3 px-1 py-3 text-md",
pathname.startsWith(item.href.replace("{orgId}", orgId || ""))
pathname.startsWith(
item.href.replace("{orgId}", orgId || "")
)
? "border-b-2 border-primary text-primary font-medium"
: "hover:text-primary text-muted-foreground font-medium",
"whitespace-nowrap",

View File

@@ -231,7 +231,7 @@ const TwoFactorSetupForm = forwardRef<
<p>{t("otpSetupScanQr")}</p>
<div className="h-[250px] mx-auto flex items-center justify-center">
<div className="bg-white p-6 border rounded-md">
<QRCodeCanvas value={secretUri} size={200} />
<QRCodeCanvas value={secretUri} size={200} />
</div>
</div>
<div className="max-w-md mx-auto">

View File

@@ -1,10 +1,8 @@
"use client";
import {
ColumnDef,
} from "@tanstack/react-table";
import { ColumnDef } from "@tanstack/react-table";
import { DataTable } from "@app/components/ui/data-table";
import { useTranslations } from 'next-intl';
import { useTranslations } from "next-intl";
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
@@ -21,7 +19,6 @@ export function UsersDataTable<TData, TValue>({
onRefresh,
isRefreshing
}: DataTableProps<TData, TValue>) {
const t = useTranslations();
return (
@@ -29,13 +26,13 @@ export function UsersDataTable<TData, TValue>({
columns={columns}
data={data}
persistPageSize="users-table"
title={t('users')}
searchPlaceholder={t('accessUsersSearch')}
title={t("users")}
searchPlaceholder={t("accessUsersSearch")}
searchColumn="email"
onAdd={inviteUser}
onRefresh={onRefresh}
isRefreshing={isRefreshing}
addButtonText={t('accessUserCreate')}
addButtonText={t("accessUserCreate")}
enableColumnVisibility={true}
stickyLeftColumn="displayUsername"
stickyRightColumn="actions"

View File

@@ -94,7 +94,8 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) {
console.error(e);
setError(
t("idpErrorOidcTokenValidating", {
defaultValue: "An unexpected error occurred. Please try again."
defaultValue:
"An unexpected error occurred. Please try again."
})
);
} finally {

View File

@@ -459,8 +459,7 @@ const AuthPageSettings = forwardRef<AuthPageSettingsRef, AuthPageSettingsProps>(
</div>
)}
{env.flags
.usePangolinDns &&
{env.flags.usePangolinDns &&
(build === "enterprise" ||
(build === "saas" &&
subscription?.subscribed)) &&

View File

@@ -29,14 +29,15 @@ export default function CertificateStatus({
pollingInterval = 5000
}: CertificateStatusProps) {
const t = useTranslations();
const { cert, certLoading, certError, refreshing, refreshCert } = useCertificate({
orgId,
domainId,
fullDomain,
autoFetch,
polling,
pollingInterval
});
const { cert, certLoading, certError, refreshing, refreshCert } =
useCertificate({
orgId,
domainId,
fullDomain,
autoFetch,
polling,
pollingInterval
});
const handleRefresh = async () => {
await refreshCert();
@@ -63,7 +64,8 @@ export default function CertificateStatus({
status === "failed" ||
status === "expired" ||
(status === "requested" &&
updatedAt && new Date(updatedAt).getTime() < Date.now() - 5 * 60 * 1000)
updatedAt &&
new Date(updatedAt).getTime() < Date.now() - 5 * 60 * 1000)
);
};
@@ -90,9 +92,7 @@ export default function CertificateStatus({
{t("certificateStatus")}:
</span>
)}
<span className="text-sm text-red-500">
{certError}
</span>
<span className="text-sm text-red-500">{certError}</span>
</div>
);
}
@@ -129,7 +129,9 @@ export default function CertificateStatus({
className="ml-2 p-0 h-auto align-middle"
onClick={handleRefresh}
disabled={refreshing}
title={t("restartCertificate", { defaultValue: "Restart Certificate" })}
title={t("restartCertificate", {
defaultValue: "Restart Certificate"
})}
>
<RotateCw
className={`w-4 h-4 ${refreshing ? "animate-spin" : ""}`}

View File

@@ -59,7 +59,6 @@ export default function IdpTable({ idps, orgId }: Props) {
}
};
const columns: ExtendedColumnDef<IdpRow>[] = [
{
accessorKey: "idpId",
@@ -114,14 +113,12 @@ export default function IdpTable({ idps, orgId }: Props) {
cell: ({ row }) => {
const type = row.original.type;
const variant = row.original.variant;
return (
<IdpTypeBadge type={type} variant={variant} />
);
return <IdpTypeBadge type={type} variant={variant} />;
}
},
{
id: "actions",
header: () => (<span className="p-3">{t("actions")}</span>),
header: () => <span className="p-3">{t("actions")}</span>,
cell: ({ row }) => {
const siteRow = row.original;
return (
@@ -156,10 +153,10 @@ export default function IdpTable({ idps, orgId }: Props) {
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Link href={`/${orgId}/settings/idp/${siteRow.idpId}/general`}>
<Button
variant={"outline"}
>
<Link
href={`/${orgId}/settings/idp/${siteRow.idpId}/general`}
>
<Button variant={"outline"}>
{t("edit")}
<ArrowRight className="ml-2 w-4 h-4" />
</Button>
@@ -181,12 +178,8 @@ export default function IdpTable({ idps, orgId }: Props) {
}}
dialog={
<div>
<p>
{t("idpQuestionRemove")}
</p>
<p>
{t("idpMessageRemove")}
</p>
<p>{t("idpQuestionRemove")}</p>
<p>{t("idpMessageRemove")}</p>
</div>
}
buttonText={t("idpConfirmDelete")}

View File

@@ -45,13 +45,13 @@ export default function RegionSelector() {
return (
<div className="flex flex-col items-center space-y-2">
<label className="flex items-center gap-1 text-sm font-medium text-muted-foreground">
{t('regionSelectorTitle')}
<InfoPopup info={t('regionSelectorInfo')} />
{t("regionSelectorTitle")}
<InfoPopup info={t("regionSelectorInfo")} />
</label>
<Select value={selectedRegion} onValueChange={handleRegionChange}>
<SelectTrigger className="w-[200px]">
<SelectValue placeholder={t('regionSelectorPlaceholder')} />
<SelectValue placeholder={t("regionSelectorPlaceholder")} />
</SelectTrigger>
<SelectContent>
{regions.map((region) => (
@@ -74,7 +74,7 @@ export default function RegionSelector() {
</span>
{region.value === "eu" && (
<span className="text-xs text-muted-foreground">
{t('regionSelectorComingSoon')}
{t("regionSelectorComingSoon")}
</span>
)}
</div>

View File

@@ -344,7 +344,7 @@ export const Autocomplete: React.FC<AutocompleteProps> = ({
</div>
) : (
<div className="py-6 text-center text-sm">
{t('noResults')}
{t("noResults")}
</div>
)}
</div>

View File

@@ -54,8 +54,7 @@ export interface TagInputStyleClassesProps {
}
export interface TagInputProps
extends OmittedInputProps,
VariantProps<typeof tagVariants> {
extends OmittedInputProps, VariantProps<typeof tagVariants> {
placeholder?: string;
tags: Tag[];
setTags: React.Dispatch<React.SetStateAction<Tag[]>>;

View File

@@ -186,10 +186,10 @@ export const TagPopover: React.FC<TagPopoverProps> = ({
>
<div className="space-y-1">
<h4 className="text-sm font-medium leading-none">
{t('tagsEntered')}
{t("tagsEntered")}
</h4>
<p className="text-sm text-muted-foregrounsd text-left">
{t('tagsEnteredDescription')}
{t("tagsEnteredDescription")}
</p>
</div>
<TagList

View File

@@ -6,44 +6,44 @@ import * as AvatarPrimitive from "@radix-ui/react-avatar";
import { cn } from "@app/lib/cn";
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
));
Avatar.displayName = AvatarPrimitive.Root.displayName;
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
));
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
{...props}
/>
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
{...props}
/>
));
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;

View File

@@ -29,7 +29,8 @@ const badgeVariants = cva(
);
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
extends
React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {

View File

@@ -5,111 +5,111 @@ import { ChevronRight, MoreHorizontal } from "lucide-react";
import { cn } from "@app/lib/cn";
const Breadcrumb = React.forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & {
separator?: React.ReactNode
}
HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & {
separator?: React.ReactNode;
}
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />);
Breadcrumb.displayName = "Breadcrumb";
const BreadcrumbList = React.forwardRef<
HTMLOListElement,
React.ComponentPropsWithoutRef<"ol">
HTMLOListElement,
React.ComponentPropsWithoutRef<"ol">
>(({ className, ...props }, ref) => (
<ol
ref={ref}
className={cn(
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
className
)}
{...props}
/>
<ol
ref={ref}
className={cn(
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
className
)}
{...props}
/>
));
BreadcrumbList.displayName = "BreadcrumbList";
const BreadcrumbItem = React.forwardRef<
HTMLLIElement,
React.ComponentPropsWithoutRef<"li">
HTMLLIElement,
React.ComponentPropsWithoutRef<"li">
>(({ className, ...props }, ref) => (
<li
ref={ref}
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
<li
ref={ref}
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
));
BreadcrumbItem.displayName = "BreadcrumbItem";
const BreadcrumbLink = React.forwardRef<
HTMLAnchorElement,
React.ComponentPropsWithoutRef<"a"> & {
asChild?: boolean
}
HTMLAnchorElement,
React.ComponentPropsWithoutRef<"a"> & {
asChild?: boolean;
}
>(({ asChild, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a";
const Comp = asChild ? Slot : "a";
return (
<Comp
ref={ref}
className={cn("transition-colors hover:text-foreground", className)}
{...props}
/>
);
return (
<Comp
ref={ref}
className={cn("transition-colors hover:text-foreground", className)}
{...props}
/>
);
});
BreadcrumbLink.displayName = "BreadcrumbLink";
const BreadcrumbPage = React.forwardRef<
HTMLSpanElement,
React.ComponentPropsWithoutRef<"span">
HTMLSpanElement,
React.ComponentPropsWithoutRef<"span">
>(({ className, ...props }, ref) => (
<span
ref={ref}
role="link"
aria-disabled="true"
aria-current="page"
className={cn("font-normal text-foreground", className)}
{...props}
/>
<span
ref={ref}
role="link"
aria-disabled="true"
aria-current="page"
className={cn("font-normal text-foreground", className)}
{...props}
/>
));
BreadcrumbPage.displayName = "BreadcrumbPage";
const BreadcrumbSeparator = ({
children,
className,
...props
children,
className,
...props
}: React.ComponentProps<"li">) => (
<li
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:w-3.5 [&>svg]:h-3.5", className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
<li
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:w-3.5 [&>svg]:h-3.5", className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
);
BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
const BreadcrumbEllipsis = ({
className,
...props
className,
...props
}: React.ComponentProps<"span">) => (
<span
role="presentation"
aria-hidden="true"
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More</span>
</span>
<span
role="presentation"
aria-hidden="true"
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More</span>
</span>
);
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis
};

View File

@@ -45,7 +45,8 @@ const buttonVariants = cva(
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
extends
React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
loading?: boolean; // Add loading prop

View File

@@ -2,9 +2,9 @@
import * as React from "react";
import {
ChevronDownIcon,
ChevronLeftIcon,
ChevronRightIcon,
ChevronDownIcon,
ChevronLeftIcon,
ChevronRightIcon
} from "lucide-react";
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker";
@@ -12,202 +12,217 @@ import { Button, buttonVariants } from "@/components/ui/button";
import { cn } from "@app/lib/cn";
function Calendar({
className,
classNames,
showOutsideDays = true,
captionLayout = "label",
buttonVariant = "ghost",
formatters,
components,
...props
className,
classNames,
showOutsideDays = true,
captionLayout = "label",
buttonVariant = "ghost",
formatters,
components,
...props
}: React.ComponentProps<typeof DayPicker> & {
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
buttonVariant?: React.ComponentProps<typeof Button>["variant"];
}) {
const defaultClassNames = getDefaultClassNames();
const defaultClassNames = getDefaultClassNames();
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn(
"bg-background group/calendar p-3 [--cell-size:2rem] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
className
)}
captionLayout={captionLayout}
formatters={{
formatMonthDropdown: (date) =>
date.toLocaleString("default", { month: "short" }),
...formatters,
}}
classNames={{
root: cn("w-fit", defaultClassNames.root),
months: cn(
"relative flex flex-col gap-4 md:flex-row",
defaultClassNames.months
),
month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
nav: cn(
"absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
defaultClassNames.nav
),
button_previous: cn(
buttonVariants({ variant: buttonVariant }),
"h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50",
defaultClassNames.button_previous
),
button_next: cn(
buttonVariants({ variant: buttonVariant }),
"h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50",
defaultClassNames.button_next
),
month_caption: cn(
"flex h-[--cell-size] w-full items-center justify-center px-[--cell-size]",
defaultClassNames.month_caption
),
dropdowns: cn(
"flex h-[--cell-size] w-full items-center justify-center gap-1.5 text-sm font-medium",
defaultClassNames.dropdowns
),
dropdown_root: cn(
"has-focus:border-ring border-input has-focus:ring-ring/50 relative rounded-md border",
defaultClassNames.dropdown_root
),
dropdown: cn(
"bg-popover absolute inset-0 opacity-0",
defaultClassNames.dropdown
),
caption_label: cn(
"select-none font-medium",
captionLayout === "label"
? "text-sm"
: "[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5",
defaultClassNames.caption_label
),
table: "w-full border-collapse",
weekdays: cn("flex", defaultClassNames.weekdays),
weekday: cn(
"text-muted-foreground flex-1 select-none rounded-md text-[0.8rem] font-normal",
defaultClassNames.weekday
),
week: cn("mt-2 flex w-full", defaultClassNames.week),
week_number_header: cn(
"w-[--cell-size] select-none",
defaultClassNames.week_number_header
),
week_number: cn(
"text-muted-foreground select-none text-[0.8rem]",
defaultClassNames.week_number
),
day: cn(
"group/day relative aspect-square h-full w-full select-none p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md",
defaultClassNames.day
),
range_start: cn(
"bg-accent rounded-l-md",
defaultClassNames.range_start
),
range_middle: cn("rounded-none", defaultClassNames.range_middle),
range_end: cn("bg-accent rounded-r-md", defaultClassNames.range_end),
today: cn(
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
defaultClassNames.today
),
outside: cn(
"text-muted-foreground aria-selected:text-muted-foreground",
defaultClassNames.outside
),
disabled: cn(
"text-muted-foreground opacity-50",
defaultClassNames.disabled
),
hidden: cn("invisible", defaultClassNames.hidden),
...classNames,
}}
components={{
Root: ({ className, rootRef, ...props }) => {
return (
<div
data-slot="calendar"
ref={rootRef}
className={cn(className)}
{...props}
/>
);
},
Chevron: ({ className, orientation, ...props }) => {
if (orientation === "left") {
return (
<ChevronLeftIcon className={cn("size-4", className)} {...props} />
);
}
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn(
"bg-background group/calendar p-3 [--cell-size:2rem] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
className
)}
captionLayout={captionLayout}
formatters={{
formatMonthDropdown: (date) =>
date.toLocaleString("default", { month: "short" }),
...formatters
}}
classNames={{
root: cn("w-fit", defaultClassNames.root),
months: cn(
"relative flex flex-col gap-4 md:flex-row",
defaultClassNames.months
),
month: cn(
"flex w-full flex-col gap-4",
defaultClassNames.month
),
nav: cn(
"absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
defaultClassNames.nav
),
button_previous: cn(
buttonVariants({ variant: buttonVariant }),
"h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50",
defaultClassNames.button_previous
),
button_next: cn(
buttonVariants({ variant: buttonVariant }),
"h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50",
defaultClassNames.button_next
),
month_caption: cn(
"flex h-[--cell-size] w-full items-center justify-center px-[--cell-size]",
defaultClassNames.month_caption
),
dropdowns: cn(
"flex h-[--cell-size] w-full items-center justify-center gap-1.5 text-sm font-medium",
defaultClassNames.dropdowns
),
dropdown_root: cn(
"has-focus:border-ring border-input has-focus:ring-ring/50 relative rounded-md border",
defaultClassNames.dropdown_root
),
dropdown: cn(
"bg-popover absolute inset-0 opacity-0",
defaultClassNames.dropdown
),
caption_label: cn(
"select-none font-medium",
captionLayout === "label"
? "text-sm"
: "[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5",
defaultClassNames.caption_label
),
table: "w-full border-collapse",
weekdays: cn("flex", defaultClassNames.weekdays),
weekday: cn(
"text-muted-foreground flex-1 select-none rounded-md text-[0.8rem] font-normal",
defaultClassNames.weekday
),
week: cn("mt-2 flex w-full", defaultClassNames.week),
week_number_header: cn(
"w-[--cell-size] select-none",
defaultClassNames.week_number_header
),
week_number: cn(
"text-muted-foreground select-none text-[0.8rem]",
defaultClassNames.week_number
),
day: cn(
"group/day relative aspect-square h-full w-full select-none p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md",
defaultClassNames.day
),
range_start: cn(
"bg-accent rounded-l-md",
defaultClassNames.range_start
),
range_middle: cn(
"rounded-none",
defaultClassNames.range_middle
),
range_end: cn(
"bg-accent rounded-r-md",
defaultClassNames.range_end
),
today: cn(
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
defaultClassNames.today
),
outside: cn(
"text-muted-foreground aria-selected:text-muted-foreground",
defaultClassNames.outside
),
disabled: cn(
"text-muted-foreground opacity-50",
defaultClassNames.disabled
),
hidden: cn("invisible", defaultClassNames.hidden),
...classNames
}}
components={{
Root: ({ className, rootRef, ...props }) => {
return (
<div
data-slot="calendar"
ref={rootRef}
className={cn(className)}
{...props}
/>
);
},
Chevron: ({ className, orientation, ...props }) => {
if (orientation === "left") {
return (
<ChevronLeftIcon
className={cn("size-4", className)}
{...props}
/>
);
}
if (orientation === "right") {
return (
<ChevronRightIcon
className={cn("size-4", className)}
{...props}
/>
);
}
if (orientation === "right") {
return (
<ChevronRightIcon
className={cn("size-4", className)}
{...props}
/>
);
}
return (
<ChevronDownIcon className={cn("size-4", className)} {...props} />
);
},
DayButton: CalendarDayButton,
WeekNumber: ({ children, ...props }) => {
return (
<td {...props}>
<div className="flex size-[--cell-size] items-center justify-center text-center">
{children}
</div>
</td>
);
},
...components,
}}
{...props}
/>
);
return (
<ChevronDownIcon
className={cn("size-4", className)}
{...props}
/>
);
},
DayButton: CalendarDayButton,
WeekNumber: ({ children, ...props }) => {
return (
<td {...props}>
<div className="flex size-[--cell-size] items-center justify-center text-center">
{children}
</div>
</td>
);
},
...components
}}
{...props}
/>
);
}
function CalendarDayButton({
className,
day,
modifiers,
...props
className,
day,
modifiers,
...props
}: React.ComponentProps<typeof DayButton>) {
const defaultClassNames = getDefaultClassNames();
const defaultClassNames = getDefaultClassNames();
const ref = React.useRef<HTMLButtonElement>(null);
React.useEffect(() => {
if (modifiers.focused) ref.current?.focus();
}, [modifiers.focused]);
const ref = React.useRef<HTMLButtonElement>(null);
React.useEffect(() => {
if (modifiers.focused) ref.current?.focus();
}, [modifiers.focused]);
return (
<Button
ref={ref}
variant="ghost"
size="icon"
data-day={day.date.toLocaleDateString()}
data-selected-single={
modifiers.selected &&
!modifiers.range_start &&
!modifiers.range_end &&
!modifiers.range_middle
}
data-range-start={modifiers.range_start}
data-range-end={modifiers.range_end}
data-range-middle={modifiers.range_middle}
className={cn(
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 flex aspect-square h-auto w-full min-w-[--cell-size] flex-col gap-1 font-normal leading-none data-[range-end=true]:rounded-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 [&>span]:text-xs [&>span]:opacity-70",
defaultClassNames.day,
className
)}
{...props}
/>
);
return (
<Button
ref={ref}
variant="ghost"
size="icon"
data-day={day.date.toLocaleDateString()}
data-selected-single={
modifiers.selected &&
!modifiers.range_start &&
!modifiers.range_end &&
!modifiers.range_middle
}
data-range-start={modifiers.range_start}
data-range-end={modifiers.range_end}
data-range-middle={modifiers.range_middle}
className={cn(
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 flex aspect-square h-auto w-full min-w-[--cell-size] flex-col gap-1 font-normal leading-none data-[range-end=true]:rounded-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 [&>span]:text-xs [&>span]:opacity-70",
defaultClassNames.day,
className
)}
{...props}
/>
);
}
export { Calendar, CalendarDayButton };

View File

@@ -10,7 +10,7 @@ const Card = React.forwardRef<
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground",
className,
className
)}
{...props}
/>
@@ -37,7 +37,7 @@ const CardTitle = React.forwardRef<
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className,
className
)}
{...props}
/>
@@ -82,5 +82,5 @@ export {
CardFooter,
CardTitle,
CardDescription,
CardContent,
CardContent
};

View File

@@ -30,7 +30,8 @@ const checkboxVariants = cva(
);
interface CheckboxProps
extends React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>,
extends
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>,
VariantProps<typeof checkboxVariants> {}
const Checkbox = React.forwardRef<
@@ -49,8 +50,9 @@ const Checkbox = React.forwardRef<
));
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
interface CheckboxWithLabelProps
extends React.ComponentPropsWithoutRef<typeof Checkbox> {
interface CheckboxWithLabelProps extends React.ComponentPropsWithoutRef<
typeof Checkbox
> {
label: string;
}

Some files were not shown because too many files have changed in this diff Show More