This commit is contained in:
Owen
2025-12-22 16:28:41 -05:00
parent e28ab19ed4
commit 5c67a1cb12
55 changed files with 636 additions and 490 deletions

View File

@@ -66,4 +66,3 @@ export const ClientDownloadBanner = () => {
};
export default ClientDownloadBanner;

View File

@@ -99,14 +99,12 @@ export default function ClientResourcesTable({
siteId: number
) => {
try {
await api
.delete(`/site-resource/${resourceId}`)
.then(() => {
startTransition(() => {
router.refresh();
setIsDeleteModalOpen(false);
});
await api.delete(`/site-resource/${resourceId}`).then(() => {
startTransition(() => {
router.refresh();
setIsDeleteModalOpen(false);
});
});
} catch (e) {
console.error(t("resourceErrorDelete"), e);
toast({

View File

@@ -87,7 +87,12 @@ const isValidPortRangeString = (val: string | undefined | null): boolean => {
return false;
}
if (startPort < 1 || startPort > 65535 || endPort < 1 || endPort > 65535) {
if (
startPort < 1 ||
startPort > 65535 ||
endPort < 1 ||
endPort > 65535
) {
return false;
}
@@ -131,7 +136,10 @@ const getPortModeFromString = (val: string | undefined | null): PortMode => {
};
// Helper to get the port string for API from mode and custom value
const getPortStringFromMode = (mode: PortMode, customValue: string): string | undefined => {
const getPortStringFromMode = (
mode: PortMode,
customValue: string
): string | undefined => {
if (mode === "all") return "*";
if (mode === "blocked") return "";
return customValue;
@@ -1097,8 +1105,7 @@ export default function CreateInternalResourceDialog({
size="sm"
tags={
form.getValues()
.roles ||
[]
.roles || []
}
setTags={(
newRoles
@@ -1154,8 +1161,7 @@ export default function CreateInternalResourceDialog({
)}
tags={
form.getValues()
.users ||
[]
.users || []
}
size="sm"
setTags={(
@@ -1245,9 +1251,7 @@ export default function CreateInternalResourceDialog({
restrictTagsToAutocompleteOptions={
true
}
sortTags={
true
}
sortTags={true}
/>
</FormControl>
<FormMessage />

View File

@@ -95,4 +95,3 @@ export const DismissableBanner = ({
};
export default DismissableBanner;

View File

@@ -501,13 +501,19 @@ export default function EditInternalResourceDialog({
// ]);
await queryClient.invalidateQueries(
resourceQueries.siteResourceRoles({ siteResourceId: resource.id })
resourceQueries.siteResourceRoles({
siteResourceId: resource.id
})
);
await queryClient.invalidateQueries(
resourceQueries.siteResourceUsers({ siteResourceId: resource.id })
resourceQueries.siteResourceUsers({
siteResourceId: resource.id
})
);
await queryClient.invalidateQueries(
resourceQueries.siteResourceClients({ siteResourceId: resource.id })
resourceQueries.siteResourceClients({
siteResourceId: resource.id
})
);
toast({

View File

@@ -330,7 +330,7 @@ export default function ExitNodesTable({
isRefreshing={isRefreshing}
columnVisibility={{
type: false,
address: false,
address: false
}}
enableColumnVisibility={true}
/>

View File

@@ -116,10 +116,12 @@ export function LayoutSidebar({
isCollapsed={isSidebarCollapsed}
/>
</div>
<div className={cn(
"w-full border-b border-border",
isSidebarCollapsed && "mb-2"
)} />
<div
className={cn(
"w-full border-b border-border",
isSidebarCollapsed && "mb-2"
)}
/>
<div className="flex-1 overflow-y-auto relative">
<div className="px-2 pt-1">
{!isAdminPage && user.serverAdmin && (

View File

@@ -120,7 +120,9 @@ export default function LoginForm({
const focusInput = () => {
// Try using the ref first
if (otpContainerRef.current) {
const hiddenInput = otpContainerRef.current.querySelector('input') as HTMLInputElement;
const hiddenInput = otpContainerRef.current.querySelector(
"input"
) as HTMLInputElement;
if (hiddenInput) {
hiddenInput.focus();
return;
@@ -128,17 +130,23 @@ export default function LoginForm({
}
// Fallback: query the DOM
const otpContainer = document.querySelector('[data-slot="input-otp"]');
const otpContainer = document.querySelector(
'[data-slot="input-otp"]'
);
if (!otpContainer) return;
const hiddenInput = otpContainer.querySelector('input') as HTMLInputElement;
const hiddenInput = otpContainer.querySelector(
"input"
) as HTMLInputElement;
if (hiddenInput) {
hiddenInput.focus();
return;
}
// Last resort: click the first slot
const firstSlot = otpContainer.querySelector('[data-slot="input-otp-slot"]') as HTMLElement;
const firstSlot = otpContainer.querySelector(
'[data-slot="input-otp-slot"]'
) as HTMLElement;
if (firstSlot) {
firstSlot.click();
}
@@ -508,7 +516,10 @@ export default function LoginForm({
render={({ field }) => (
<FormItem>
<FormControl>
<div ref={otpContainerRef} className="flex justify-center">
<div
ref={otpContainerRef}
className="flex justify-center"
>
<InputOTP
maxLength={6}
{...field}

View File

@@ -11,9 +11,7 @@ type MachineClientsBannerProps = {
orgId: string;
};
export const MachineClientsBanner = ({
orgId
}: MachineClientsBannerProps) => {
export const MachineClientsBanner = ({ orgId }: MachineClientsBannerProps) => {
const t = useTranslations();
return (
@@ -57,4 +55,3 @@ export const MachineClientsBanner = ({
};
export default MachineClientsBanner;

View File

@@ -39,4 +39,3 @@ export default function OrgInfoCard({}: OrgInfoCardProps) {
</Alert>
);
}

View File

@@ -119,4 +119,3 @@ export default async function OrgLoginPage({
</div>
);
}

View File

@@ -51,4 +51,3 @@ export const PrivateResourcesBanner = ({
};
export default PrivateResourcesBanner;

View File

@@ -81,10 +81,10 @@ export default function ProductUpdates({
const showNewVersionPopup = Boolean(
latestVersion &&
valid(latestVersion) &&
valid(currentVersion) &&
ignoredVersionUpdate !== latestVersion &&
gt(latestVersion, currentVersion)
valid(latestVersion) &&
valid(currentVersion) &&
ignoredVersionUpdate !== latestVersion &&
gt(latestVersion, currentVersion)
);
const filteredUpdates = data.updates.filter(

View File

@@ -20,4 +20,3 @@ export const ProxyResourcesBanner = () => {
};
export default ProxyResourcesBanner;

View File

@@ -1,6 +1,6 @@
"use client";
import {Button} from "@app/components/ui/button";
import { Button } from "@app/components/ui/button";
import {
Form,
FormControl,
@@ -9,12 +9,12 @@ import {
FormLabel,
FormMessage
} from "@app/components/ui/form";
import {Input} from "@app/components/ui/input";
import {toast} from "@app/hooks/useToast";
import {zodResolver} from "@hookform/resolvers/zod";
import {useEffect, useState} from "react";
import {useForm} from "react-hook-form";
import {z} from "zod";
import { Input } from "@app/components/ui/input";
import { toast } from "@app/hooks/useToast";
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import {
Credenza,
CredenzaBody,
@@ -25,14 +25,14 @@ import {
CredenzaHeader,
CredenzaTitle
} from "@app/components/Credenza";
import {formatAxiosError} from "@app/lib/api";
import {AxiosResponse} from "axios";
import {Resource} from "@server/db";
import {createApiClient} from "@app/lib/api";
import {useEnvContext} from "@app/hooks/useEnvContext";
import {useTranslations} from "next-intl";
import {SwitchInput} from "@/components/SwitchInput";
import {InfoPopup} from "@/components/ui/info-popup";
import { formatAxiosError } from "@app/lib/api";
import { AxiosResponse } from "axios";
import { Resource } from "@server/db";
import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useTranslations } from "next-intl";
import { SwitchInput } from "@/components/SwitchInput";
import { InfoPopup } from "@/components/ui/info-popup";
const setHeaderAuthFormSchema = z.object({
user: z.string().min(4).max(100),
@@ -56,11 +56,11 @@ type SetHeaderAuthFormProps = {
};
export default function SetResourceHeaderAuthForm({
open,
setOpen,
resourceId,
onSetHeaderAuth
}: SetHeaderAuthFormProps) {
open,
setOpen,
resourceId,
onSetHeaderAuth
}: SetHeaderAuthFormProps) {
const api = createApiClient(useEnvContext());
const t = useTranslations();
@@ -82,11 +82,14 @@ 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,
extendedCompatibility: data.extendedCompatibility
})
api.post<AxiosResponse<Resource>>(
`/resource/${resourceId}/header-auth`,
{
user: data.user,
password: data.password,
extendedCompatibility: data.extendedCompatibility
}
)
.then(() => {
toast({
title: t("resourceHeaderAuthSetup"),
@@ -100,10 +103,10 @@ export default function SetResourceHeaderAuthForm({
.catch((e) => {
toast({
variant: "destructive",
title: t('resourceErrorHeaderAuthSetup'),
title: t("resourceErrorHeaderAuthSetup"),
description: formatAxiosError(
e,
t('resourceErrorHeaderAuthSetupDescription')
t("resourceErrorHeaderAuthSetupDescription")
)
});
})
@@ -139,7 +142,7 @@ export default function SetResourceHeaderAuthForm({
<FormField
control={form.control}
name="user"
render={({field}) => (
render={({ field }) => (
<FormItem>
<FormLabel>{t("user")}</FormLabel>
<FormControl>
@@ -149,14 +152,14 @@ export default function SetResourceHeaderAuthForm({
{...field}
/>
</FormControl>
<FormMessage/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({field}) => (
render={({ field }) => (
<FormItem>
<FormLabel>
{t("password")}
@@ -168,25 +171,31 @@ export default function SetResourceHeaderAuthForm({
{...field}
/>
</FormControl>
<FormMessage/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="extendedCompatibility"
render={({field}) => (
render={({ field }) => (
<FormItem>
<FormControl>
<SwitchInput
id="header-auth-compatibility-toggle"
label={t("headerAuthCompatibility")}
info={t('headerAuthCompatibilityInfo')}
label={t(
"headerAuthCompatibility"
)}
info={t(
"headerAuthCompatibilityInfo"
)}
checked={field.value}
onCheckedChange={field.onChange}
onCheckedChange={
field.onChange
}
/>
</FormControl>
<FormMessage/>
<FormMessage />
</FormItem>
)}
/>

View File

@@ -91,10 +91,10 @@ export default function SetResourcePasswordForm({
.catch((e) => {
toast({
variant: "destructive",
title: t('resourceErrorPasswordSetup'),
title: t("resourceErrorPasswordSetup"),
description: formatAxiosError(
e,
t('resourceErrorPasswordSetupDescription')
t("resourceErrorPasswordSetupDescription")
)
});
})

View File

@@ -97,10 +97,10 @@ export default function SetResourcePincodeForm({
.catch((e) => {
toast({
variant: "destructive",
title: t('resourceErrorPincodeSetup'),
title: t("resourceErrorPincodeSetup"),
description: formatAxiosError(
e,
t('resourceErrorPincodeSetupDescription')
t("resourceErrorPincodeSetupDescription")
)
});
})

View File

@@ -37,4 +37,3 @@ export const SitesBanner = () => {
};
export default SitesBanner;

View File

@@ -1,10 +1,14 @@
import React from "react";
import {Switch} from "./ui/switch";
import {Label} from "./ui/label";
import {Button} from "@/components/ui/button";
import {Info} from "lucide-react";
import {info} from "winston";
import {Popover, PopoverContent, PopoverTrigger} from "@/components/ui/popover";
import { Switch } from "./ui/switch";
import { Label } from "./ui/label";
import { Button } from "@/components/ui/button";
import { Info } from "lucide-react";
import { info } from "winston";
import {
Popover,
PopoverContent,
PopoverTrigger
} from "@/components/ui/popover";
interface SwitchComponentProps {
id: string;
@@ -18,22 +22,22 @@ interface SwitchComponentProps {
}
export function SwitchInput({
id,
label,
description,
info,
disabled,
checked,
defaultChecked = false,
onCheckedChange
}: SwitchComponentProps) {
id,
label,
description,
info,
disabled,
checked,
defaultChecked = false,
onCheckedChange
}: SwitchComponentProps) {
const defaultTrigger = (
<Button
variant="ghost"
size="icon"
className="h-6 w-6 rounded-full p-0"
>
<Info className="h-4 w-4"/>
<Info className="h-4 w-4" />
<span className="sr-only">Show info</span>
</Button>
);
@@ -49,18 +53,20 @@ export function SwitchInput({
disabled={disabled}
/>
{label && <Label htmlFor={id}>{label}</Label>}
{info && <Popover>
<PopoverTrigger asChild>
{defaultTrigger}
</PopoverTrigger>
<PopoverContent className="w-80">
{info && (
<p className="text-sm text-muted-foreground">
{info}
</p>
)}
</PopoverContent>
</Popover>}
{info && (
<Popover>
<PopoverTrigger asChild>
{defaultTrigger}
</PopoverTrigger>
<PopoverContent className="w-80">
{info && (
<p className="text-sm text-muted-foreground">
{info}
</p>
)}
</PopoverContent>
</Popover>
)}
</div>
{description && (
<span className="text-muted-foreground text-sm">

View File

@@ -276,14 +276,15 @@ function AuthPageSettings({
<>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>{t("customDomain")}</SettingsSectionTitle>
<SettingsSectionTitle>
{t("customDomain")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("authPageDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm>
<PaidFeaturesAlert />
<Form {...form}>

View File

@@ -58,12 +58,7 @@ export default function IdpLoginButtons({
let redirectToUrl: string | undefined;
try {
console.log(
"generating",
idpId,
redirect || "/",
orgId
);
console.log("generating", idpId, redirect || "/", orgId);
const safeRedirect = cleanRedirect(redirect || "/");
const response = await generateOidcUrlProxy(
idpId,

View File

@@ -288,7 +288,10 @@ export function DataTable<TData, TValue>({
useEffect(() => {
if (persistPageSize && pagination.pageSize !== pageSize) {
// Only store if user has actually changed it from initial value
if (hasUserChangedPageSize.current && pagination.pageSize !== initialPageSize.current) {
if (
hasUserChangedPageSize.current &&
pagination.pageSize !== initialPageSize.current
) {
setStoredPageSize(pagination.pageSize, tableId);
}
setPageSize(pagination.pageSize);
@@ -298,7 +301,9 @@ export function DataTable<TData, TValue>({
useEffect(() => {
// Persist column visibility to localStorage when it changes (but not on initial mount)
if (shouldPersistColumnVisibility) {
const hasChanged = JSON.stringify(columnVisibility) !== JSON.stringify(initialColumnVisibilityState.current);
const hasChanged =
JSON.stringify(columnVisibility) !==
JSON.stringify(initialColumnVisibilityState.current);
if (hasChanged) {
// Mark as user-initiated change and persist
hasUserChangedColumnVisibility.current = true;