From cfa82b51fb1dcc628a70362914396ed45e16d7b4 Mon Sep 17 00:00:00 2001 From: Pallavi Kumari Date: Sat, 4 Oct 2025 12:20:46 +0530 Subject: [PATCH 01/18] refresh button in clients page --- src/components/ClientsDataTable.tsx | 8 +++- src/components/ClientsTable.tsx | 74 +++++++++++++++++++---------- 2 files changed, 55 insertions(+), 27 deletions(-) diff --git a/src/components/ClientsDataTable.tsx b/src/components/ClientsDataTable.tsx index 6242ba05..619f1fad 100644 --- a/src/components/ClientsDataTable.tsx +++ b/src/components/ClientsDataTable.tsx @@ -8,13 +8,17 @@ import { DataTable } from "@app/components/ui/data-table"; interface DataTableProps { columns: ColumnDef[]; data: TData[]; + onRefresh?: () => void; + isRefreshing?: boolean; addClient?: () => void; } export function ClientsDataTable({ columns, data, - addClient + addClient, + onRefresh, + isRefreshing }: DataTableProps) { return ( ({ searchPlaceholder="Search clients..." searchColumn="name" onAdd={addClient} + onRefresh={onRefresh} + isRefreshing={isRefreshing} addButtonText="Add Client" /> ); diff --git a/src/components/ClientsTable.tsx b/src/components/ClientsTable.tsx index fc7c7c84..425b8395 100644 --- a/src/components/ClientsTable.tsx +++ b/src/components/ClientsTable.tsx @@ -25,6 +25,7 @@ import { toast } from "@app/hooks/useToast"; import { formatAxiosError } from "@app/lib/api"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; +import { useTranslations } from "next-intl"; export type ClientRow = { id: number; @@ -53,6 +54,25 @@ export default function ClientsTable({ clients, orgId }: ClientTableProps) { const [rows, setRows] = useState(clients); const api = createApiClient(useEnvContext()); + const [isRefreshing, setIsRefreshing] = useState(false); + const t = useTranslations(); + + const refreshData = async () => { + console.log("Data refreshed"); + setIsRefreshing(true); + try { + await new Promise((resolve) => setTimeout(resolve, 200)); + router.refresh(); + } catch (error) { + toast({ + title: t("error"), + description: t("refreshError"), + variant: "destructive" + }); + } finally { + setIsRefreshing(false); + } + }; const deleteClient = (clientId: number) => { api.delete(`/client/${clientId}`) @@ -207,32 +227,32 @@ export default function ClientsTable({ clients, orgId }: ClientTableProps) { return (
- - - - - - {/* */} - {/* */} - {/* View settings */} - {/* */} - {/* */} - { - setSelectedClient(clientRow); - setIsDeleteModalOpen(true); - }} - > - Delete - - - + + + + + + {/* */} + {/* */} + {/* View settings */} + {/* */} + {/* */} + { + setSelectedClient(clientRow); + setIsDeleteModalOpen(true); + }} + > + Delete + + + @@ -292,6 +312,8 @@ export default function ClientsTable({ clients, orgId }: ClientTableProps) { addClient={() => { router.push(`/${orgId}/settings/clients/create`); }} + onRefresh={refreshData} + isRefreshing={isRefreshing} /> ); From ccd27733311e84430e9a66e0b615cf0a81a765b0 Mon Sep 17 00:00:00 2001 From: Pallavi Kumari Date: Sat, 4 Oct 2025 12:58:30 +0530 Subject: [PATCH 02/18] refresh button on resources page --- src/components/ResourcesTable.tsx | 39 +++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/src/components/ResourcesTable.tsx b/src/components/ResourcesTable.tsx index 7a645bc7..ad60c685 100644 --- a/src/components/ResourcesTable.tsx +++ b/src/components/ResourcesTable.tsx @@ -24,7 +24,8 @@ import { MoreHorizontal, ArrowUpRight, ShieldOff, - ShieldCheck + ShieldCheck, + RefreshCw } from "lucide-react"; import Link from "next/link"; import { useRouter } from "next/navigation"; @@ -179,9 +180,27 @@ export default function ResourcesTable({ const [internalColumnFilters, setInternalColumnFilters] = useState([]); const [internalGlobalFilter, setInternalGlobalFilter] = useState([]); + const [isRefreshing, setIsRefreshing] = useState(false); const currentView = searchParams.get("view") || defaultView; + const refreshData = async () => { + console.log("Data refreshed"); + setIsRefreshing(true); + try { + await new Promise((resolve) => setTimeout(resolve, 200)); + router.refresh(); + } catch (error) { + toast({ + title: t("error"), + description: t("refreshError"), + variant: "destructive" + }); + } finally { + setIsRefreshing(false); + } + }; + useEffect(() => { const fetchSites = async () => { try { @@ -753,7 +772,23 @@ export default function ResourcesTable({ )}
- {getActionButton()} +
+ {refreshData && ( + + )} +
+
+ {getActionButton()} +
From b1e212721e2db320dbd6082a6f78f5eea2523e3a Mon Sep 17 00:00:00 2001 From: Pallavi Kumari Date: Sat, 4 Oct 2025 13:31:48 +0530 Subject: [PATCH 03/18] refresh button for role, user, share-link, invitation table --- src/components/InvitationsDataTable.tsx | 8 +++++++- src/components/InvitationsTable.tsx | 27 ++++++++++++++++++++++++- src/components/ResourcesTable.tsx | 22 +++++++++----------- src/components/RolesDataTable.tsx | 8 +++++++- src/components/RolesTable.tsx | 22 ++++++++++++++++++++ src/components/ShareLinksDataTable.tsx | 8 +++++++- src/components/ShareLinksTable.tsx | 21 +++++++++++++++++++ src/components/UsersDataTable.tsx | 8 +++++++- src/components/UsersTable.tsx | 20 ++++++++++++++++++ 9 files changed, 127 insertions(+), 17 deletions(-) diff --git a/src/components/InvitationsDataTable.tsx b/src/components/InvitationsDataTable.tsx index 396a3c20..d73ad2ca 100644 --- a/src/components/InvitationsDataTable.tsx +++ b/src/components/InvitationsDataTable.tsx @@ -9,11 +9,15 @@ import { useTranslations } from 'next-intl'; interface DataTableProps { columns: ColumnDef[]; data: TData[]; + onRefresh?: () => void; + isRefreshing?: boolean; } export function InvitationsDataTable({ columns, - data + data, + onRefresh, + isRefreshing }: DataTableProps) { const t = useTranslations(); @@ -26,6 +30,8 @@ export function InvitationsDataTable({ title={t('invite')} searchPlaceholder={t('inviteSearch')} searchColumn="email" + onRefresh={onRefresh} + isRefreshing={isRefreshing} /> ); } diff --git a/src/components/InvitationsTable.tsx b/src/components/InvitationsTable.tsx index a97220f2..900003d7 100644 --- a/src/components/InvitationsTable.tsx +++ b/src/components/InvitationsTable.tsx @@ -19,6 +19,7 @@ import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { useTranslations } from "next-intl"; import moment from "moment"; +import { useRouter } from "next/navigation"; export type InvitationRow = { id: string; @@ -45,6 +46,25 @@ export default function InvitationsTable({ const api = createApiClient(useEnvContext()); const { org } = useOrgContext(); + const router = useRouter(); + const [isRefreshing, setIsRefreshing] = useState(false); + + const refreshData = async () => { + console.log("Data refreshed"); + setIsRefreshing(true); + try { + await new Promise((resolve) => setTimeout(resolve, 200)); + router.refresh(); + } catch (error) { + toast({ + title: t("error"), + description: t("refreshError"), + variant: "destructive" + }); + } finally { + setIsRefreshing(false); + } + }; const columns: ColumnDef[] = [ { @@ -185,7 +205,12 @@ export default function InvitationsTable({ }} /> - + ); } diff --git a/src/components/ResourcesTable.tsx b/src/components/ResourcesTable.tsx index ad60c685..ad8b4fab 100644 --- a/src/components/ResourcesTable.tsx +++ b/src/components/ResourcesTable.tsx @@ -773,18 +773,16 @@ export default function ResourcesTable({
- {refreshData && ( - - )} +
{getActionButton()} diff --git a/src/components/RolesDataTable.tsx b/src/components/RolesDataTable.tsx index e88f9a2f..8043fc23 100644 --- a/src/components/RolesDataTable.tsx +++ b/src/components/RolesDataTable.tsx @@ -10,12 +10,16 @@ interface DataTableProps { columns: ColumnDef[]; data: TData[]; createRole?: () => void; + onRefresh?: () => void; + isRefreshing?: boolean; } export function RolesDataTable({ columns, data, - createRole + createRole, + onRefresh, + isRefreshing }: DataTableProps) { const t = useTranslations(); @@ -29,6 +33,8 @@ export function RolesDataTable({ searchPlaceholder={t('accessRolesSearch')} searchColumn="name" onAdd={createRole} + onRefresh={onRefresh} + isRefreshing={isRefreshing} addButtonText={t('accessRolesAdd')} /> ); diff --git a/src/components/RolesTable.tsx b/src/components/RolesTable.tsx index e92e71b6..292384a8 100644 --- a/src/components/RolesTable.tsx +++ b/src/components/RolesTable.tsx @@ -20,6 +20,7 @@ import DeleteRoleForm from "@app/components/DeleteRoleForm"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { useTranslations } from "next-intl"; +import { useRouter } from "next/navigation"; export type RoleRow = Role; @@ -30,6 +31,7 @@ type RolesTableProps = { export default function UsersTable({ roles: r }: RolesTableProps) { const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const router = useRouter(); const [roles, setRoles] = useState(r); @@ -40,6 +42,24 @@ export default function UsersTable({ roles: r }: RolesTableProps) { const { org } = useOrgContext(); const t = useTranslations(); + const [isRefreshing, setIsRefreshing] = useState(false); + + const refreshData = async () => { + console.log("Data refreshed"); + setIsRefreshing(true); + try { + await new Promise((resolve) => setTimeout(resolve, 200)); + router.refresh(); + } catch (error) { + toast({ + title: t("error"), + description: t("refreshError"), + variant: "destructive" + }); + } finally { + setIsRefreshing(false); + } + }; const columns: ColumnDef[] = [ { @@ -116,6 +136,8 @@ export default function UsersTable({ roles: r }: RolesTableProps) { createRole={() => { setIsCreateModalOpen(true); }} + onRefresh={refreshData} + isRefreshing={isRefreshing} /> ); diff --git a/src/components/ShareLinksDataTable.tsx b/src/components/ShareLinksDataTable.tsx index dd266bcf..f2753bcf 100644 --- a/src/components/ShareLinksDataTable.tsx +++ b/src/components/ShareLinksDataTable.tsx @@ -10,12 +10,16 @@ interface DataTableProps { columns: ColumnDef[]; data: TData[]; createShareLink?: () => void; + onRefresh?: () => void; + isRefreshing?: boolean; } export function ShareLinksDataTable({ columns, data, - createShareLink + createShareLink, + onRefresh, + isRefreshing }: DataTableProps) { const t = useTranslations(); @@ -29,6 +33,8 @@ export function ShareLinksDataTable({ searchPlaceholder={t('shareSearch')} searchColumn="name" onAdd={createShareLink} + onRefresh={onRefresh} + isRefreshing={isRefreshing} addButtonText={t('shareCreate')} /> ); diff --git a/src/components/ShareLinksTable.tsx b/src/components/ShareLinksTable.tsx index 2943311f..ba9169c1 100644 --- a/src/components/ShareLinksTable.tsx +++ b/src/components/ShareLinksTable.tsx @@ -61,6 +61,25 @@ export default function ShareLinksTable({ const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [rows, setRows] = useState(shareLinks); + const [isRefreshing, setIsRefreshing] = useState(false); + + const refreshData = async () => { + console.log("Data refreshed"); + setIsRefreshing(true); + try { + await new Promise((resolve) => setTimeout(resolve, 200)); + router.refresh(); + } catch (error) { + toast({ + title: t("error"), + description: t("refreshError"), + variant: "destructive" + }); + } finally { + setIsRefreshing(false); + } + }; + function formatLink(link: string) { return link.substring(0, 20) + "..." + link.substring(link.length - 20); } @@ -292,6 +311,8 @@ export default function ShareLinksTable({ createShareLink={() => { setIsCreateModalOpen(true); }} + onRefresh={refreshData} + isRefreshing={isRefreshing} /> ); diff --git a/src/components/UsersDataTable.tsx b/src/components/UsersDataTable.tsx index 1999b620..db12b697 100644 --- a/src/components/UsersDataTable.tsx +++ b/src/components/UsersDataTable.tsx @@ -10,12 +10,16 @@ interface DataTableProps { columns: ColumnDef[]; data: TData[]; inviteUser?: () => void; + onRefresh?: () => void; + isRefreshing?: boolean; } export function UsersDataTable({ columns, data, - inviteUser + inviteUser, + onRefresh, + isRefreshing }: DataTableProps) { const t = useTranslations(); @@ -29,6 +33,8 @@ export function UsersDataTable({ searchPlaceholder={t('accessUsersSearch')} searchColumn="email" onAdd={inviteUser} + onRefresh={onRefresh} + isRefreshing={isRefreshing} addButtonText={t('accessUserCreate')} /> ); diff --git a/src/components/UsersTable.tsx b/src/components/UsersTable.tsx index 2d4c122f..be8aea49 100644 --- a/src/components/UsersTable.tsx +++ b/src/components/UsersTable.tsx @@ -51,6 +51,24 @@ export default function UsersTable({ users: u }: UsersTableProps) { const { user, updateUser } = useUserContext(); const { org } = useOrgContext(); const t = useTranslations(); + const [isRefreshing, setIsRefreshing] = useState(false); + + const refreshData = async () => { + console.log("Data refreshed"); + setIsRefreshing(true); + try { + await new Promise((resolve) => setTimeout(resolve, 200)); + router.refresh(); + } catch (error) { + toast({ + title: t("error"), + description: t("refreshError"), + variant: "destructive" + }); + } finally { + setIsRefreshing(false); + } + }; const columns: ColumnDef[] = [ { @@ -290,6 +308,8 @@ export default function UsersTable({ users: u }: UsersTableProps) { `/${org?.org.orgId}/settings/access/users/create` ); }} + onRefresh={refreshData} + isRefreshing={isRefreshing} /> ); From cd27f6459c0566737d822853703f1316a3fc52f8 Mon Sep 17 00:00:00 2001 From: Pallavi Kumari Date: Sun, 5 Oct 2025 12:30:01 +0530 Subject: [PATCH 04/18] refresh button --- src/components/OrgApiKeysDataTable.tsx | 8 +++++++- src/components/OrgApiKeysTable.tsx | 20 ++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/components/OrgApiKeysDataTable.tsx b/src/components/OrgApiKeysDataTable.tsx index 773b2141..b6ad4bc3 100644 --- a/src/components/OrgApiKeysDataTable.tsx +++ b/src/components/OrgApiKeysDataTable.tsx @@ -8,12 +8,16 @@ interface DataTableProps { columns: ColumnDef[]; data: TData[]; addApiKey?: () => void; + onRefresh?: () => void; + isRefreshing?: boolean; } export function OrgApiKeysDataTable({ addApiKey, columns, - data + data, + onRefresh, + isRefreshing }: DataTableProps) { const t = useTranslations(); @@ -27,6 +31,8 @@ export function OrgApiKeysDataTable({ searchPlaceholder={t('searchApiKeys')} searchColumn="name" onAdd={addApiKey} + onRefresh={onRefresh} + isRefreshing={isRefreshing} addButtonText={t('apiKeysAdd')} /> ); diff --git a/src/components/OrgApiKeysTable.tsx b/src/components/OrgApiKeysTable.tsx index 52030b66..d4c81e80 100644 --- a/src/components/OrgApiKeysTable.tsx +++ b/src/components/OrgApiKeysTable.tsx @@ -46,6 +46,24 @@ export default function OrgApiKeysTable({ const api = createApiClient(useEnvContext()); const t = useTranslations(); + const [isRefreshing, setIsRefreshing] = useState(false); + + const refreshData = async () => { + console.log("Data refreshed"); + setIsRefreshing(true); + try { + await new Promise((resolve) => setTimeout(resolve, 200)); + router.refresh(); + } catch (error) { + toast({ + title: t("error"), + description: t("refreshError"), + variant: "destructive" + }); + } finally { + setIsRefreshing(false); + } + }; const deleteSite = (apiKeyId: string) => { api.delete(`/org/${orgId}/api-key/${apiKeyId}`) @@ -195,6 +213,8 @@ export default function OrgApiKeysTable({ addApiKey={() => { router.push(`/${orgId}/settings/api-keys/create`); }} + onRefresh={refreshData} + isRefreshing={isRefreshing} /> ); From c7c3e3ee7393f3445a07262c9a63651bcd871c96 Mon Sep 17 00:00:00 2001 From: Pallavi Kumari Date: Sun, 5 Oct 2025 23:19:35 +0530 Subject: [PATCH 05/18] refresh button inside admin --- src/components/AdminIdpDataTable.tsx | 8 +++++++- src/components/AdminIdpTable.tsx | 25 ++++++++++++++++++++++- src/components/AdminUsersDataTable.tsx | 8 +++++++- src/components/AdminUsersTable.tsx | 28 ++++++++++++++++++++++++-- src/components/ApiKeysDataTable.tsx | 10 +++++++-- src/components/ApiKeysTable.tsx | 21 +++++++++++++++++++ 6 files changed, 93 insertions(+), 7 deletions(-) diff --git a/src/components/AdminIdpDataTable.tsx b/src/components/AdminIdpDataTable.tsx index 2efd9e7c..63a0b4bb 100644 --- a/src/components/AdminIdpDataTable.tsx +++ b/src/components/AdminIdpDataTable.tsx @@ -8,11 +8,15 @@ import { useTranslations } from "next-intl"; interface DataTableProps { columns: ColumnDef[]; data: TData[]; + onRefresh?: () => void; + isRefreshing?: boolean; } export function IdpDataTable({ columns, - data + data, + onRefresh, + isRefreshing }: DataTableProps) { const router = useRouter(); const t = useTranslations(); @@ -29,6 +33,8 @@ export function IdpDataTable({ onAdd={() => { router.push("/admin/idp/create"); }} + onRefresh={onRefresh} + isRefreshing={isRefreshing} /> ); } diff --git a/src/components/AdminIdpTable.tsx b/src/components/AdminIdpTable.tsx index 8849ba25..2db1415e 100644 --- a/src/components/AdminIdpTable.tsx +++ b/src/components/AdminIdpTable.tsx @@ -39,8 +39,26 @@ export default function IdpTable({ idps }: Props) { const [selectedIdp, setSelectedIdp] = useState(null); const api = createApiClient(useEnvContext()); const router = useRouter(); + const [isRefreshing, setIsRefreshing] = useState(false); const t = useTranslations(); + const refreshData = async () => { + console.log("Data refreshed"); + setIsRefreshing(true); + try { + await new Promise((resolve) => setTimeout(resolve, 200)); + router.refresh(); + } catch (error) { + toast({ + title: t("error"), + description: t("refreshError"), + variant: "destructive" + }); + } finally { + setIsRefreshing(false); + } + }; + const deleteIdp = async (idpId: number) => { try { await api.delete(`/idp/${idpId}`); @@ -194,7 +212,12 @@ export default function IdpTable({ idps }: Props) { /> )} - + ); } diff --git a/src/components/AdminUsersDataTable.tsx b/src/components/AdminUsersDataTable.tsx index fecba7fb..b0f38587 100644 --- a/src/components/AdminUsersDataTable.tsx +++ b/src/components/AdminUsersDataTable.tsx @@ -9,11 +9,15 @@ import { useTranslations } from "next-intl"; interface DataTableProps { columns: ColumnDef[]; data: TData[]; + onRefresh?: () => void; + isRefreshing?: boolean; } export function UsersDataTable({ columns, - data + data, + onRefresh, + isRefreshing }: DataTableProps) { const t = useTranslations(); @@ -26,6 +30,8 @@ export function UsersDataTable({ title={t('userServer')} searchPlaceholder={t('userSearch')} searchColumn="email" + onRefresh={onRefresh} + isRefreshing={isRefreshing} /> ); } diff --git a/src/components/AdminUsersTable.tsx b/src/components/AdminUsersTable.tsx index 8e75ff24..6bca4a74 100644 --- a/src/components/AdminUsersTable.tsx +++ b/src/components/AdminUsersTable.tsx @@ -46,6 +46,25 @@ export default function UsersTable({ users }: Props) { const api = createApiClient(useEnvContext()); + const [isRefreshing, setIsRefreshing] = useState(false); + + const refreshData = async () => { + console.log("Data refreshed"); + setIsRefreshing(true); + try { + await new Promise((resolve) => setTimeout(resolve, 200)); + router.refresh(); + } catch (error) { + toast({ + title: t("error"), + description: t("refreshError"), + variant: "destructive" + }); + } finally { + setIsRefreshing(false); + } + }; + const deleteUser = (id: string) => { api.delete(`/user/${id}`) .catch((e) => { @@ -168,7 +187,7 @@ export default function UsersTable({ users }: Props) {
{userRow.twoFactorEnabled || - userRow.twoFactorSetupRequested ? ( + userRow.twoFactorSetupRequested ? ( {t("enabled")} @@ -263,7 +282,12 @@ export default function UsersTable({ users }: Props) { /> )} - + ); } diff --git a/src/components/ApiKeysDataTable.tsx b/src/components/ApiKeysDataTable.tsx index 6ac8d68b..58ab9252 100644 --- a/src/components/ApiKeysDataTable.tsx +++ b/src/components/ApiKeysDataTable.tsx @@ -33,16 +33,20 @@ interface DataTableProps { columns: ColumnDef[]; data: TData[]; addApiKey?: () => void; + onRefresh?: () => void; + isRefreshing?: boolean; } export function ApiKeysDataTable({ addApiKey, columns, - data + data, + onRefresh, + isRefreshing }: DataTableProps) { const t = useTranslations(); - + return ( ({ searchColumn="name" onAdd={addApiKey} addButtonText={t('apiKeysAdd')} + onRefresh={onRefresh} + isRefreshing={isRefreshing} /> ); } diff --git a/src/components/ApiKeysTable.tsx b/src/components/ApiKeysTable.tsx index 99094651..adc150cf 100644 --- a/src/components/ApiKeysTable.tsx +++ b/src/components/ApiKeysTable.tsx @@ -43,6 +43,25 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) { const t = useTranslations(); + const [isRefreshing, setIsRefreshing] = useState(false); + + const refreshData = async () => { + console.log("Data refreshed"); + setIsRefreshing(true); + try { + await new Promise((resolve) => setTimeout(resolve, 200)); + router.refresh(); + } catch (error) { + toast({ + title: t("error"), + description: t("refreshError"), + variant: "destructive" + }); + } finally { + setIsRefreshing(false); + } + }; + const deleteSite = (apiKeyId: string) => { api.delete(`/api-key/${apiKeyId}`) .catch((e) => { @@ -186,6 +205,8 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) { addApiKey={() => { router.push(`/admin/api-keys/create`); }} + onRefresh={refreshData} + isRefreshing={isRefreshing} /> ); From ca146a1b5788a6318c69fcb2f58275c221124009 Mon Sep 17 00:00:00 2001 From: Pallavi Kumari Date: Mon, 6 Oct 2025 00:50:38 +0530 Subject: [PATCH 06/18] adjust target config column --- messages/en-US.json | 2 + .../resources/[niceId]/proxy/page.tsx | 270 +++++++++--------- src/components/TargetDisplay.tsx | 44 +++ src/components/TargetModal.tsx | 144 ++++++++++ 4 files changed, 322 insertions(+), 138 deletions(-) create mode 100644 src/components/TargetDisplay.tsx create mode 100644 src/components/TargetModal.tsx diff --git a/messages/en-US.json b/messages/en-US.json index f725f853..3cbc4a04 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -469,6 +469,8 @@ "proxyErrorInvalidHeader": "Invalid custom Host Header value. Use domain name format, or save empty to unset custom Host Header.", "proxyErrorTls": "Invalid TLS Server Name. Use domain name format, or save empty to remove the TLS Server Name.", "proxyEnableSSL": "Enable SSL (https)", + "target": "Target", + "configureTargets": "Configure Targets", "targetErrorFetch": "Failed to fetch targets", "targetErrorFetchDescription": "An error occurred while fetching targets", "siteErrorFetch": "Failed to fetch resource", diff --git a/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx b/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx index 302d16d2..028c97c7 100644 --- a/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx +++ b/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx @@ -110,6 +110,8 @@ import { } from "@app/components/PathMatchRenameModal"; import { Badge } from "@app/components/ui/badge"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@app/components/ui/tooltip"; +import { TargetModal } from "@app/components/TargetModal"; +import { TargetDisplay } from "@app/components/TargetDisplay"; const addTargetSchema = z .object({ @@ -537,11 +539,11 @@ export default function ReverseProxyTargets(props: { targets.map((target) => target.targetId === targetId ? { - ...target, - ...data, - updated: true, - siteType: site?.type || null - } + ...target, + ...data, + updated: true, + siteType: site?.type || null + } : target ) ); @@ -552,10 +554,10 @@ export default function ReverseProxyTargets(props: { targets.map((target) => target.targetId === targetId ? { - ...target, - ...config, - updated: true - } + ...target, + ...config, + updated: true + } : target ) ); @@ -743,9 +745,9 @@ export default function ReverseProxyTargets(props: { } /> - {/* */} +
) : ( +
+ } + /> + + +
+ ) : ( + updateTarget(row.original.targetId, { ...row.original, - ip: input - }); + ...config + }) } - }} - /> - ) - }, - { - accessorKey: "port", - header: t("targetPort"), - cell: ({ row }) => ( - - updateTarget(row.original.targetId, { - ...row.original, - port: parseInt(e.target.value, 10) - }) - } - /> - ) + showMethod={resource.http} + trigger={ + + } + /> + ); + } }, { accessorKey: "rewritePath", @@ -990,7 +985,6 @@ export default function ReverseProxyTargets(props: { return hasRewritePath && !noPathMatch ? (
- {/* */} @@ -1318,34 +1312,34 @@ export default function ReverseProxyTargets(props: { ); return selectedSite && selectedSite.type === - "newt" + "newt" ? (() => { - const dockerState = - getDockerStateForSite( - selectedSite.siteId - ); - return ( - - refreshContainersForSite( - selectedSite.siteId - ) - } - /> - ); - })() + const dockerState = + getDockerStateForSite( + selectedSite.siteId + ); + return ( + + refreshContainersForSite( + selectedSite.siteId + ) + } + /> + ); + })() : null; })()}
@@ -1558,7 +1552,7 @@ export default function ReverseProxyTargets(props: { -
+
{table @@ -1573,12 +1567,12 @@ export default function ReverseProxyTargets(props: { {header.isPlaceholder ? null : flexRender( - header - .column - .columnDef - .header, - header.getContext() - )} + header + .column + .columnDef + .header, + header.getContext() + )} ) )} diff --git a/src/components/TargetDisplay.tsx b/src/components/TargetDisplay.tsx new file mode 100644 index 00000000..5ca0a9a7 --- /dev/null +++ b/src/components/TargetDisplay.tsx @@ -0,0 +1,44 @@ +import { Globe, Hash, Shield } from "lucide-react"; + +interface TargetDisplayProps { + value: { + method?: string | null; + ip?: string; + port?: number; + }; + showMethod?: boolean; +} + +export function TargetDisplay({ value, showMethod = true }: TargetDisplayProps) { + const { method, ip, port } = value; + + if (!ip && !port && !method) { + return Not configured; + } + + + + return ( +
+ {showMethod && method && ( + + {method === "https" && } + + {method}:// + + + )} + {ip && ( + + {ip} + {port && :} + + )} + {port && ( + + {port} + + )} +
+ ); +} diff --git a/src/components/TargetModal.tsx b/src/components/TargetModal.tsx new file mode 100644 index 00000000..8a5ec336 --- /dev/null +++ b/src/components/TargetModal.tsx @@ -0,0 +1,144 @@ + +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { useState } from "react"; + +interface TargetConfig { + method?: string | null; + ip?: string; + port?: number; +} + +interface TargetModalProps { + value: TargetConfig; + onChange: (config: TargetConfig) => void; + trigger: React.ReactNode; + showMethod?: boolean; +} + +export function TargetModal({ + value, + onChange, + trigger, + showMethod = true +}: TargetModalProps) { + const [open, setOpen] = useState(false); + const [config, setConfig] = useState(value); + + const handleSave = () => { + onChange(config); + setOpen(false); + }; + + const parseHostTarget = (input: string) => { + const protocolMatch = input.match(/^(https?|h2c):\/\//); + const protocol = protocolMatch ? protocolMatch[1] : null; + const withoutProtocol = input.replace(/^(https?|h2c):\/\//, ''); + + const portMatch = withoutProtocol.match(/:(\d+)(?:\/|$)/); + const port = portMatch ? parseInt(portMatch[1], 10) : null; + const host = withoutProtocol.replace(/:\d+(?:\/|$)/, '').replace(/\/$/, ''); + + return { protocol, host, port }; + }; + + const handleHostChange = (input: string) => { + const trimmed = input.trim(); + const hasProtocol = /^(https?|h2c):\/\//.test(trimmed); + const hasPort = /:\d+(?:\/|$)/.test(trimmed); + + if (hasProtocol || hasPort) { + const parsed = parseHostTarget(trimmed); + setConfig({ + ...config, + ...(hasProtocol && parsed.protocol ? { method: parsed.protocol } : {}), + ip: parsed.host, + ...(hasPort && parsed.port ? { port: parsed.port } : {}) + }); + } else { + setConfig({ ...config, ip: trimmed }); + } + }; + + return ( + + {trigger} + + + Configure Target + +
+ {showMethod && ( +
+ + +
+ )} +
+ + setConfig({ ...config, ip: e.target.value })} + onBlur={(e) => handleHostChange(e.target.value)} + /> +

+ You can also paste: http://example.com:8080 +

+
+
+ + + setConfig({ + ...config, + port: parseInt(e.target.value, 10) || undefined + }) + } + /> +
+
+
+ + +
+
+
+ ); +} \ No newline at end of file From d20e0a228a6ab8f07d14d1409e8a35ce175ea229 Mon Sep 17 00:00:00 2001 From: Pallavi Kumari Date: Mon, 6 Oct 2025 01:06:43 +0530 Subject: [PATCH 07/18] adjust target config ui inside create resource --- messages/en-US.json | 2 +- .../resources/[niceId]/proxy/page.tsx | 2 +- .../settings/resources/create/page.tsx | 145 +++++++++--------- 3 files changed, 74 insertions(+), 75 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index 3cbc4a04..b58f1493 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -470,7 +470,7 @@ "proxyErrorTls": "Invalid TLS Server Name. Use domain name format, or save empty to remove the TLS Server Name.", "proxyEnableSSL": "Enable SSL (https)", "target": "Target", - "configureTargets": "Configure Targets", + "configureTarget": "Configure Targets", "targetErrorFetch": "Failed to fetch targets", "targetErrorFetchDescription": "An error occurred while fetching targets", "siteErrorFetch": "Failed to fetch resource", diff --git a/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx b/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx index 028c97c7..eae1d116 100644 --- a/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx +++ b/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx @@ -918,7 +918,7 @@ export default function ReverseProxyTargets(props: { trigger={ } - } else { + /> + + + + ) : ( + updateTarget(row.original.targetId, { ...row.original, - ip: input - }); + ...config + }) } - }} - /> - ) - }, - { - accessorKey: "port", - header: t("targetPort"), - cell: ({ row }) => ( - - updateTarget(row.original.targetId, { - ...row.original, - port: parseInt(e.target.value, 10) - }) - } - /> - ) + showMethod={baseForm.watch("http")} + trigger={ + + } + /> + ); + } }, { accessorKey: "rewritePath", From 0a377150e342304bb5d66be594fef29a828d6acd Mon Sep 17 00:00:00 2001 From: Pallavi Kumari Date: Wed, 8 Oct 2025 22:35:40 +0530 Subject: [PATCH 08/18] reorder columns --- .../resources/[niceId]/proxy/page.tsx | 260 ++++++++---------- 1 file changed, 109 insertions(+), 151 deletions(-) diff --git a/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx b/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx index eae1d116..1f7a3f3d 100644 --- a/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx +++ b/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx @@ -671,6 +671,21 @@ export default function ReverseProxyTargets(props: { } const columns: ColumnDef[] = [ + { + accessorKey: "enabled", + header: t("enabled"), + cell: ({ row }) => ( + + updateTarget(row.original.targetId, { + ...row.original, + enabled: val + }) + } + /> + ) + }, { id: "priority", header: () => ( @@ -711,6 +726,82 @@ export default function ReverseProxyTargets(props: { ); } }, + { + accessorKey: "healthCheck", + header: t("healthCheck"), + cell: ({ row }) => { + const status = row.original.hcHealth || "unknown"; + const isEnabled = row.original.hcEnabled; + + const getStatusColor = (status: string) => { + switch (status) { + case "healthy": + return "green"; + case "unhealthy": + return "red"; + case "unknown": + default: + return "secondary"; + } + }; + + const getStatusText = (status: string) => { + switch (status) { + case "healthy": + return t("healthCheckHealthy"); + case "unhealthy": + return t("healthCheckUnhealthy"); + case "unknown": + default: + return t("healthCheckUnknown"); + } + }; + + const getStatusIcon = (status: string) => { + switch (status) { + case "healthy": + return ; + case "unhealthy": + return ; + case "unknown": + default: + return null; + } + }; + + return ( + <> + {row.original.siteType === "newt" ? ( + + + + ) : ( + + {t("healthCheckNotAvailable")} + + )} + + ); + } + }, { accessorKey: "path", header: t("matchPath"), @@ -744,42 +835,27 @@ export default function ReverseProxyTargets(props: { } /> - - ) : ( - - updateTarget(row.original.targetId, config) - } - trigger={ - - } - /> +
+ + updateTarget(row.original.targetId, config) + } + trigger={ + + } + /> + +
); } }, @@ -931,22 +1007,6 @@ export default function ReverseProxyTargets(props: { } /> - ) : ( @@ -1010,21 +1070,6 @@ export default function ReverseProxyTargets(props: { } /> - ) : ( // ), // }, - { - accessorKey: "healthCheck", - header: t("healthCheck"), - cell: ({ row }) => { - const status = row.original.hcHealth || "unknown"; - const isEnabled = row.original.hcEnabled; - const getStatusColor = (status: string) => { - switch (status) { - case "healthy": - return "green"; - case "unhealthy": - return "red"; - case "unknown": - default: - return "secondary"; - } - }; - - const getStatusText = (status: string) => { - switch (status) { - case "healthy": - return t("healthCheckHealthy"); - case "unhealthy": - return t("healthCheckUnhealthy"); - case "unknown": - default: - return t("healthCheckUnknown"); - } - }; - - const getStatusIcon = (status: string) => { - switch (status) { - case "healthy": - return ; - case "unhealthy": - return ; - case "unknown": - default: - return null; - } - }; - - return ( - <> - {row.original.siteType === "newt" ? ( -
- -
- {getStatusIcon(status)} - {getStatusText(status)} -
-
- -
- ) : ( - - {t("healthCheckNotAvailable")} - - )} - - ); - } - }, - { - accessorKey: "enabled", - header: t("enabled"), - cell: ({ row }) => ( - - updateTarget(row.original.targetId, { - ...row.original, - enabled: val - }) - } - /> - ) - }, { id: "actions", cell: ({ row }) => ( From a6086d3724d233b5c81acd13e8733fba619a1d77 Mon Sep 17 00:00:00 2001 From: Pallavi Kumari Date: Thu, 9 Oct 2025 00:53:45 +0530 Subject: [PATCH 09/18] address input design --- .../resources/[niceId]/proxy/page.tsx | 255 +++++++++--------- 1 file changed, 126 insertions(+), 129 deletions(-) diff --git a/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx b/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx index 1f7a3f3d..fdb7ff91 100644 --- a/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx +++ b/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx @@ -860,17 +860,14 @@ export default function ReverseProxyTargets(props: { } }, { - accessorKey: "siteId", - header: t("site"), + accessorKey: "address", + header: t("address"), cell: ({ row }) => { const selectedSite = sites.find( (site) => site.siteId === row.original.siteId ); - const handleContainerSelectForTarget = ( - hostname: string, - port?: number - ) => { + const handleContainerSelectForTarget = (hostname: string, port?: number) => { updateTarget(row.original.targetId, { ...row.original, ip: hostname @@ -884,67 +881,53 @@ export default function ReverseProxyTargets(props: { }; return ( -
- - - - - - - - - - {t("siteNotFound")} - - - {sites.map((site) => ( - { - updateTarget( - row.original - .targetId, - { - siteId: site.siteId - } - ); - }} - > - - {site.name} - - ))} - - - - - - {selectedSite && +
+ + + + + + + {t("siteNotFound")} + + {sites.map((site) => ( + + updateTarget(row.original.targetId, { siteId: site.siteId }) + } + > + + {site.name} + + ))} + + + + + + {selectedSite && selectedSite.type === "newt" && (() => { const dockerState = getDockerStateForSite( @@ -966,70 +949,84 @@ export default function ReverseProxyTargets(props: { /> ); })()} -
- ); - } - }, - { - accessorKey: "target", - header: t("target"), - cell: ({ row }) => { - const hasTarget = !!(row.original.ip || row.original.port || row.original.method); - return hasTarget ? ( -
- - updateTarget(row.original.targetId, { - ...row.original, - ...config - }) - } - showMethod={resource.http} - trigger={ - - } - /> - + + +
+ {"://"} +
+ + { + const input = e.target.value.trim(); + const hasProtocol = /^(https?|h2c):\/\//.test(input); + const hasPort = /:\d+(?:\/|$)/.test(input); + + if (hasProtocol || hasPort) { + const parsed = parseHostTarget(input); + if (parsed) { + updateTarget(row.original.targetId, { + ...row.original, + method: hasProtocol + ? parsed.protocol + : row.original.method, + ip: parsed.host, + port: hasPort + ? parsed.port + : row.original.port + }); + } else { + updateTarget(row.original.targetId, { + ...row.original, + ip: input + }); + } + } else { + updateTarget(row.original.targetId, { + ...row.original, + ip: input + }); + } + }} + /> +
+ {":"} +
+ + updateTarget(row.original.targetId, { + ...row.original, + port: parseInt(e.target.value, 10) + }) + } + /> + +
- ) : ( - - updateTarget(row.original.targetId, { - ...row.original, - ...config - }) - } - showMethod={resource.http} - trigger={ - - } - /> ); } }, From 94137e587c66f04a5d146ef7aea7aa010f00f642 Mon Sep 17 00:00:00 2001 From: Pallavi Kumari Date: Thu, 9 Oct 2025 01:33:42 +0530 Subject: [PATCH 10/18] change target config ui for create resource --- .../resources/[niceId]/proxy/page.tsx | 5 +- .../settings/resources/create/page.tsx | 423 ++++++++---------- 2 files changed, 198 insertions(+), 230 deletions(-) diff --git a/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx b/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx index fdb7ff91..40cf217d 100644 --- a/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx +++ b/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx @@ -969,7 +969,7 @@ export default function ReverseProxyTargets(props: { -
+
{"://"}
@@ -1009,11 +1009,10 @@ export default function ReverseProxyTargets(props: { } }} /> -
+
{":"}
[] = [ + { + accessorKey: "enabled", + header: t("enabled"), + cell: ({ row }) => ( + + updateTarget(row.original.targetId, { + ...row.original, + enabled: val + }) + } + /> + ) + }, { id: "priority", header: () => ( @@ -674,55 +690,37 @@ export default function Page() { } /> - - - {/* */} +
) : ( - + updateTarget(row.original.targetId, config)} - trigger={ - - } - /> + trigger={ + + } + /> + +
); - }, + } }, { - accessorKey: "siteId", - header: t("site"), + accessorKey: "address", + header: t("address"), cell: ({ row }) => { const selectedSite = sites.find( (site) => site.siteId === row.original.siteId ); - const handleContainerSelectForTarget = ( - hostname: string, - port?: number - ) => { + const handleContainerSelectForTarget = (hostname: string, port?: number) => { updateTarget(row.original.targetId, { ...row.original, ip: hostname @@ -736,158 +734,151 @@ export default function Page() { }; return ( -
- - - - - - - - - - {t("siteNotFound")} - - - {sites.map((site) => ( - { - updateTarget( - row.original - .targetId, - { - siteId: site.siteId - } - ); - }} - > - - {site.name} - - ))} - - - - - - {selectedSite && selectedSite.type === "newt" && (() => { - const dockerState = getDockerStateForSite(selectedSite.siteId); - return ( - refreshContainersForSite(selectedSite.siteId)} - /> - ); - })()} -
- ); - } - }, - { - accessorKey: "target", - header: t("target"), - cell: ({ row }) => { - const hasTarget = !!(row.original.ip || row.original.port || row.original.method); - - return hasTarget ? (
- - updateTarget(row.original.targetId, { - ...row.original, - ...config - }) - } - showMethod={baseForm.watch("http")} - trigger={ - - } - /> - + + + + + + {t("siteNotFound")} + + {sites.map((site) => ( + + updateTarget(row.original.targetId, { siteId: site.siteId }) + } + > + + {site.name} + + ))} + + + + + + {selectedSite && + selectedSite.type === "newt" && + (() => { + const dockerState = getDockerStateForSite( + selectedSite.siteId + ); + return ( + + refreshContainersForSite( + selectedSite.siteId + ) + } + /> + ); + })()} + + + +
+ {"://"} +
+ + { + const input = e.target.value.trim(); + const hasProtocol = /^(https?|h2c):\/\//.test(input); + const hasPort = /:\d+(?:\/|$)/.test(input); + + if (hasProtocol || hasPort) { + const parsed = parseHostTarget(input); + if (parsed) { + updateTarget(row.original.targetId, { + ...row.original, + method: hasProtocol + ? parsed.protocol + : row.original.method, + ip: parsed.host, + port: hasPort + ? parsed.port + : row.original.port + }); + } else { + updateTarget(row.original.targetId, { + ...row.original, + ip: input + }); + } + } else { + updateTarget(row.original.targetId, { + ...row.original, + ip: input + }); + } + }} + /> +
+ {":"} +
+ + updateTarget(row.original.targetId, { + ...row.original, + port: parseInt(e.target.value, 10) + }) + } + /> - +
- ) : ( - - updateTarget(row.original.targetId, { - ...row.original, - ...config - }) - } - showMethod={baseForm.watch("http")} - trigger={ - - } - /> ); } }, @@ -895,18 +886,22 @@ export default function Page() { accessorKey: "rewritePath", header: t("rewritePath"), cell: ({ row }) => { - const hasRewritePath = !!(row.original.rewritePath || row.original.rewritePathType); - const noPathMatch = !row.original.path && !row.original.pathMatchType; + const hasRewritePath = !!( + row.original.rewritePath || row.original.rewritePathType + ); + const noPathMatch = + !row.original.path && !row.original.pathMatchType; return hasRewritePath && !noPathMatch ? (
- {/* */} updateTarget(row.original.targetId, config)} + onChange={(config) => + updateTarget(row.original.targetId, config) + } trigger={ } /> -
) : ( updateTarget(row.original.targetId, config)} + onChange={(config) => + updateTarget(row.original.targetId, config) + } trigger={ - -
- - - ); -} \ No newline at end of file From c0cc81ed9643ed157186673eee2b162793a858f0 Mon Sep 17 00:00:00 2001 From: Pallavi Kumari Date: Fri, 10 Oct 2025 23:25:33 +0530 Subject: [PATCH 12/18] standardizing the targets input table --- server/routers/resource/createResource.ts | 7 +- .../settings/resources/create/page.tsx | 216 +++++++++++++++++- 2 files changed, 217 insertions(+), 6 deletions(-) diff --git a/server/routers/resource/createResource.ts b/server/routers/resource/createResource.ts index f4fe352e..2a4e67a7 100644 --- a/server/routers/resource/createResource.ts +++ b/server/routers/resource/createResource.ts @@ -37,7 +37,8 @@ const createHttpResourceSchema = z subdomain: z.string().nullable().optional(), http: z.boolean(), protocol: z.enum(["tcp", "udp"]), - domainId: z.string() + domainId: z.string(), + stickySession: z.boolean().optional(), }) .strict() .refine( @@ -191,6 +192,7 @@ async function createHttpResource( const { name, domainId } = parsedBody.data; const subdomain = parsedBody.data.subdomain; + const stickySession=parsedBody.data.stickySession; // Validate domain and construct full domain const domainResult = await validateAndConstructDomain( @@ -254,7 +256,8 @@ async function createHttpResource( subdomain: finalSubdomain, http: true, protocol: "tcp", - ssl: true + ssl: true, + stickySession: stickySession }) .returning(); diff --git a/src/app/[orgId]/settings/resources/create/page.tsx b/src/app/[orgId]/settings/resources/create/page.tsx index 10828275..1036538d 100644 --- a/src/app/[orgId]/settings/resources/create/page.tsx +++ b/src/app/[orgId]/settings/resources/create/page.tsx @@ -58,7 +58,7 @@ import { } from "@app/components/ui/popover"; import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons"; import { cn } from "@app/lib/cn"; -import { ArrowRight, CircleCheck, CircleX, Info, MoveRight, Plus, SquareArrowOutUpRight } from "lucide-react"; +import { ArrowRight, CircleCheck, CircleX, Info, MoveRight, Plus, Settings, SquareArrowOutUpRight } from "lucide-react"; import CopyTextBox from "@app/components/CopyTextBox"; import Link from "next/link"; import { useTranslations } from "next-intl"; @@ -95,6 +95,8 @@ import { finalizeSubdomainSanitize } from "@app/lib/subdomain-utils"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@app/components/ui/tooltip"; import { PathMatchDisplay, PathMatchModal, PathRewriteDisplay, PathRewriteModal } from "@app/components/PathMatchRenameModal"; import { Badge } from "@app/components/ui/badge"; +import HealthCheckDialog from "@app/components/HealthCheckDialog"; +import { SwitchInput } from "@app/components/SwitchInput"; const baseResourceFormSchema = z.object({ @@ -113,6 +115,11 @@ const tcpUdpResourceFormSchema = z.object({ // enableProxy: z.boolean().default(false) }); +const targetsSettingsSchema = z.object({ + stickySession: z.boolean() +}); + + const addTargetSchema = z.object({ ip: z.string().refine(isTargetValid), method: z.string().nullable(), @@ -216,6 +223,10 @@ export default function Page() { const [targetsToRemove, setTargetsToRemove] = useState([]); const [dockerStates, setDockerStates] = useState>(new Map()); + const [selectedTargetForHealthCheck, setSelectedTargetForHealthCheck] = + useState(null); + const [healthCheckDialogOpen, setHealthCheckDialogOpen] = useState(false); + const resourceTypes: ReadonlyArray = [ { id: "http", @@ -269,6 +280,13 @@ export default function Page() { } as z.infer }); + const targetsSettingsForm = useForm({ + resolver: zodResolver(targetsSettingsSchema), + defaultValues: { + stickySession: false + } + }); + const watchedIp = addTargetForm.watch("ip"); const watchedPort = addTargetForm.watch("port"); const watchedSiteId = addTargetForm.watch("siteId"); @@ -406,11 +424,13 @@ export default function Page() { const baseData = baseForm.getValues(); const isHttp = baseData.http; + const stickySessionData = targetsSettingsForm.getValues() try { const payload = { name: baseData.name, - http: baseData.http + http: baseData.http, + stickySession: stickySessionData.stickySession }; let sanitizedSubdomain: string | undefined; @@ -604,6 +624,26 @@ export default function Page() { load(); }, []); + function TargetHealthCheck(targetId: number, config: any) { + setTargets( + targets.map((target) => + target.targetId === targetId + ? { + ...target, + ...config, + updated: true + } + : target + ) + ); + } + + const openHealthCheckDialog = (target: LocalTarget) => { + console.log(target); + setSelectedTargetForHealthCheck(target); + setHealthCheckDialogOpen(true); + }; + const columns: ColumnDef[] = [ { accessorKey: "enabled", @@ -660,6 +700,82 @@ export default function Page() { ); } }, + { + accessorKey: "healthCheck", + header: t("healthCheck"), + cell: ({ row }) => { + const status = row.original.hcHealth || "unknown"; + const isEnabled = row.original.hcEnabled; + + const getStatusColor = (status: string) => { + switch (status) { + case "healthy": + return "green"; + case "unhealthy": + return "red"; + case "unknown": + default: + return "secondary"; + } + }; + + const getStatusText = (status: string) => { + switch (status) { + case "healthy": + return t("healthCheckHealthy"); + case "unhealthy": + return t("healthCheckUnhealthy"); + case "unknown": + default: + return t("healthCheckUnknown"); + } + }; + + const getStatusIcon = (status: string) => { + switch (status) { + case "healthy": + return ; + case "unhealthy": + return ; + case "unknown": + default: + return null; + } + }; + + return ( + <> + {row.original.siteType === "newt" ? ( + + + + ) : ( + + {t("healthCheckNotAvailable")} + + )} + + ); + } + }, { accessorKey: "path", header: t("matchPath"), @@ -695,9 +811,9 @@ export default function Page() { updateTarget(row.original.targetId, config)} + onChange={(config) => updateTarget(row.original.targetId, config)} trigger={
@@ -1679,6 +1838,55 @@ export default function Page() { {t("resourceCreate")} + {selectedTargetForHealthCheck && ( + { + if (selectedTargetForHealthCheck) { + console.log(config); + TargetHealthCheck( + selectedTargetForHealthCheck.targetId, + config + ); + } + }} + /> + )} ) : ( From 2f5e6248cda5614400ff084c6a30eb5a74b578c8 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 8 Oct 2025 16:42:30 -0700 Subject: [PATCH 13/18] Small ui adjustments --- .../resources/[niceId]/proxy/page.tsx | 274 ++++++++++-------- src/components/PathMatchRenameModal.tsx | 4 +- 2 files changed, 161 insertions(+), 117 deletions(-) diff --git a/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx b/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx index 4b6b358b..ab434b32 100644 --- a/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx +++ b/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx @@ -109,7 +109,12 @@ import { PathRewriteModal } from "@app/components/PathMatchRenameModal"; import { Badge } from "@app/components/ui/badge"; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@app/components/ui/tooltip"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger +} from "@app/components/ui/tooltip"; const addTargetSchema = z .object({ @@ -517,7 +522,7 @@ export default function ReverseProxyTargets(props: { pathMatchType: null, rewritePath: null, rewritePathType: null, - priority: 100, + priority: 100 }); } @@ -537,11 +542,11 @@ export default function ReverseProxyTargets(props: { targets.map((target) => target.targetId === targetId ? { - ...target, - ...data, - updated: true, - siteType: site?.type || null - } + ...target, + ...data, + updated: true, + siteType: site?.type || null + } : target ) ); @@ -552,10 +557,10 @@ export default function ReverseProxyTargets(props: { targets.map((target) => target.targetId === targetId ? { - ...target, - ...config, - updated: true - } + ...target, + ...config, + updated: true + } : target ) ); @@ -695,7 +700,12 @@ export default function ReverseProxyTargets(props: { -

Higher priority routes are evaluated first. Priority = 100 means automatic ordering (system decides). Use another number to enforce manual priority.

+

+ Higher priority routes are evaluated first. + Priority = 100 means automatic ordering + (system decides). Use another number to + enforce manual priority. +

@@ -770,8 +780,13 @@ export default function ReverseProxyTargets(props: { return ( <> {row.original.siteType === "newt" ? ( - ) : ( - - {t("healthCheckNotAvailable")} - + )} ); @@ -865,7 +879,10 @@ export default function ReverseProxyTargets(props: { (site) => site.siteId === row.original.siteId ); - const handleContainerSelectForTarget = (hostname: string, port?: number) => { + const handleContainerSelectForTarget = ( + hostname: string, + port?: number + ) => { updateTarget(row.original.targetId, { ...row.original, ip: hostname @@ -880,7 +897,10 @@ export default function ReverseProxyTargets(props: { return (
- - + - {t("siteNotFound")} + + {t("siteNotFound")} + {sites.map((site) => ( - updateTarget(row.original.targetId, { siteId: site.siteId }) + updateTarget( + row.original + .targetId, + { + siteId: site.siteId + } + ) } > {selectedSite && - selectedSite.type === "newt" && - (() => { - const dockerState = getDockerStateForSite( - selectedSite.siteId - ); - return ( - - refreshContainersForSite( - selectedSite.siteId - ) - } - /> - ); - })()} + selectedSite.type === "newt" && + (() => { + const dockerState = getDockerStateForSite( + selectedSite.siteId + ); + return ( + + refreshContainersForSite( + selectedSite.siteId + ) + } + /> + ); + })()} updateTarget(row.original.targetId, { ...row.original, @@ -1184,21 +1228,21 @@ export default function ReverseProxyTargets(props: { className={cn( "justify-between flex-1", !field.value && - "text-muted-foreground" + "text-muted-foreground" )} > {field.value ? sites.find( - ( - site - ) => - site.siteId === - field.value - ) - ?.name + ( + site + ) => + site.siteId === + field.value + ) + ?.name : t( - "siteSelect" - )} + "siteSelect" + )} @@ -1264,34 +1308,34 @@ export default function ReverseProxyTargets(props: { ); return selectedSite && selectedSite.type === - "newt" + "newt" ? (() => { - const dockerState = - getDockerStateForSite( - selectedSite.siteId - ); - return ( - - refreshContainersForSite( - selectedSite.siteId - ) - } - /> - ); - })() + const dockerState = + getDockerStateForSite( + selectedSite.siteId + ); + return ( + + refreshContainersForSite( + selectedSite.siteId + ) + } + /> + ); + })() : null; })()}
@@ -1519,12 +1563,12 @@ export default function ReverseProxyTargets(props: { {header.isPlaceholder ? null : flexRender( - header - .column - .columnDef - .header, - header.getContext() - )} + header + .column + .columnDef + .header, + header.getContext() + )} ) )} diff --git a/src/components/PathMatchRenameModal.tsx b/src/components/PathMatchRenameModal.tsx index 574c9c70..f67f44c3 100644 --- a/src/components/PathMatchRenameModal.tsx +++ b/src/components/PathMatchRenameModal.tsx @@ -250,7 +250,7 @@ export function PathMatchDisplay({ return (
- + {getTypeLabel(value.pathMatchType)} @@ -281,7 +281,7 @@ export function PathRewriteDisplay({ return (
- + {getTypeLabel(value.rewritePathType)} From 24d564b79bcf8e90a11d7742ad5da18061250c61 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Thu, 9 Oct 2025 18:24:09 -0700 Subject: [PATCH 14/18] add advanced toggle to targets table --- messages/en-US.json | 20 +- .../resources/[niceId]/proxy/page.tsx | 1088 ++++++++--------- src/components/HealthCheckDialog.tsx | 24 +- src/components/PathMatchRenameModal.tsx | 6 +- src/components/ui/textarea.tsx | 2 +- 5 files changed, 525 insertions(+), 615 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index b58f1493..7189af3c 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -468,7 +468,8 @@ "createdAt": "Created At", "proxyErrorInvalidHeader": "Invalid custom Host Header value. Use domain name format, or save empty to unset custom Host Header.", "proxyErrorTls": "Invalid TLS Server Name. Use domain name format, or save empty to remove the TLS Server Name.", - "proxyEnableSSL": "Enable SSL (https)", + "proxyEnableSSL": "Enable SSL", + "proxyEnableSSLDescription": "Enable SSL/TLS encryption for secure HTTPS connections to your targets.", "target": "Target", "configureTarget": "Configure Targets", "targetErrorFetch": "Failed to fetch targets", @@ -497,7 +498,7 @@ "targetTlsSettings": "Secure Connection Configuration", "targetTlsSettingsDescription": "Configure SSL/TLS settings for your resource", "targetTlsSettingsAdvanced": "Advanced TLS Settings", - "targetTlsSni": "TLS Server Name (SNI)", + "targetTlsSni": "TLS Server Name", "targetTlsSniDescription": "The TLS Server Name to use for SNI. Leave empty to use the default.", "targetTlsSubmit": "Save Settings", "targets": "Targets Configuration", @@ -506,9 +507,21 @@ "targetStickySessionsDescription": "Keep connections on the same backend target for their entire session.", "methodSelect": "Select method", "targetSubmit": "Add Target", - "targetNoOne": "No targets. Add a target using the form.", + "targetNoOne": "This resource doesn't have any targets. Add a target to configure where to send requests to your backend.", "targetNoOneDescription": "Adding more than one target above will enable load balancing.", "targetsSubmit": "Save Targets", + "addTarget": "Add Target", + "targetErrorInvalidIp": "Invalid IP address", + "targetErrorInvalidIpDescription": "Please enter a valid IP address or hostname", + "targetErrorInvalidPort": "Invalid port", + "targetErrorInvalidPortDescription": "Please enter a valid port number", + "targetErrorNoSite": "No site selected", + "targetErrorNoSiteDescription": "Please select a site for the target", + "targetCreated": "Target created", + "targetCreatedDescription": "Target has been created successfully", + "targetErrorCreate": "Failed to create target", + "targetErrorCreateDescription": "An error occurred while creating the target", + "save": "Save", "proxyAdditional": "Additional Proxy Settings", "proxyAdditionalDescription": "Configure how your resource handles proxy settings", "proxyCustomHeader": "Custom Host Header", @@ -1412,6 +1425,7 @@ "externalProxyEnabled": "External Proxy Enabled", "addNewTarget": "Add New Target", "targetsList": "Targets List", + "advancedMode": "Advanced Mode", "targetErrorDuplicateTargetFound": "Duplicate target found", "healthCheckHealthy": "Healthy", "healthCheckUnhealthy": "Unhealthy", diff --git a/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx b/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx index ab434b32..92e02aa2 100644 --- a/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx +++ b/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx @@ -121,7 +121,10 @@ const addTargetSchema = z ip: z.string().refine(isTargetValid), method: z.string().nullable(), port: z.coerce.number().int().positive(), - siteId: z.number().int().positive(), + siteId: z + .number() + .int() + .positive({ message: "You must select a site for a target." }), path: z.string().optional().nullable(), pathMatchType: z .enum(["exact", "prefix", "regex"]) @@ -255,6 +258,13 @@ export default function ReverseProxyTargets(props: { const [pageLoading, setPageLoading] = useState(true); const [isAdvancedOpen, setIsAdvancedOpen] = useState(false); + const [isAdvancedMode, setIsAdvancedMode] = useState(() => { + if (typeof window !== "undefined") { + const saved = localStorage.getItem("proxy-advanced-mode"); + return saved === "true"; + } + return false; + }); const [healthCheckDialogOpen, setHealthCheckDialogOpen] = useState(false); const [selectedTargetForHealthCheck, setSelectedTargetForHealthCheck] = useState(null); @@ -302,31 +312,6 @@ export default function ReverseProxyTargets(props: { type TlsSettingsValues = z.infer; type TargetsSettingsValues = z.infer; - const addTargetForm = useForm({ - resolver: zodResolver(addTargetSchema), - defaultValues: { - ip: "", - method: resource.http ? "http" : null, - port: "" as any as number, - path: null, - pathMatchType: null, - rewritePath: null, - rewritePathType: null, - priority: 100 - } as z.infer - }); - - const watchedIp = addTargetForm.watch("ip"); - const watchedPort = addTargetForm.watch("port"); - const watchedSiteId = addTargetForm.watch("siteId"); - - const handleContainerSelect = (hostname: string, port?: number) => { - addTargetForm.setValue("ip", hostname); - if (port) { - addTargetForm.setValue("port", port); - } - }; - const tlsSettingsForm = useForm({ resolver: zodResolver(tlsSettingsSchema), defaultValues: { @@ -403,13 +388,7 @@ export default function ReverseProxyTargets(props: { initializeDockerForSite(site.siteId); } - // If there's only one site, set it as the default in the form - if (res.data.data.sites.length) { - addTargetForm.setValue( - "siteId", - res.data.data.sites[0].siteId - ); - } + // Sites loaded successfully } }; fetchSites(); @@ -438,6 +417,158 @@ export default function ReverseProxyTargets(props: { // fetchSite(); }, []); + // Save advanced mode preference to localStorage + useEffect(() => { + if (typeof window !== "undefined") { + localStorage.setItem( + "proxy-advanced-mode", + isAdvancedMode.toString() + ); + } + }, [isAdvancedMode]); + + function addNewTarget() { + const newTarget: LocalTarget = { + targetId: -Date.now(), // Use negative timestamp as temporary ID + ip: "", + method: resource.http ? "http" : null, + port: 0, + siteId: sites.length > 0 ? sites[0].siteId : 0, + path: null, + pathMatchType: null, + rewritePath: null, + rewritePathType: null, + priority: 100, + enabled: true, + resourceId: resource.resourceId, + hcEnabled: false, + hcPath: null, + hcMethod: null, + hcInterval: null, + hcTimeout: null, + hcHeaders: null, + hcScheme: null, + hcHostname: null, + hcPort: null, + hcFollowRedirects: null, + hcHealth: "unknown", + hcStatus: null, + hcMode: null, + hcUnhealthyInterval: null, + siteType: sites.length > 0 ? sites[0].type : null, + new: true, + updated: false + }; + + setTargets((prev) => [...prev, newTarget]); + } + + async function saveNewTarget(target: LocalTarget) { + // Validate the target + if (!isTargetValid(target.ip)) { + toast({ + variant: "destructive", + title: t("targetErrorInvalidIp"), + description: t("targetErrorInvalidIpDescription") + }); + return; + } + + if (!target.port || target.port <= 0) { + toast({ + variant: "destructive", + title: t("targetErrorInvalidPort"), + description: t("targetErrorInvalidPortDescription") + }); + return; + } + + if (!target.siteId) { + toast({ + variant: "destructive", + title: t("targetErrorNoSite"), + description: t("targetErrorNoSiteDescription") + }); + return; + } + + // Check if target with same IP, port and method already exists + const isDuplicate = targets.some( + (t) => + t.targetId !== target.targetId && + t.ip === target.ip && + t.port === target.port && + t.method === target.method && + t.siteId === target.siteId + ); + + if (isDuplicate) { + toast({ + variant: "destructive", + title: t("targetErrorDuplicate"), + description: t("targetErrorDuplicateDescription") + }); + return; + } + + try { + setTargetsLoading(true); + + const response = await api.post< + AxiosResponse + >(`/target`, { + resourceId: resource.resourceId, + siteId: target.siteId, + ip: target.ip, + method: target.method, + port: target.port, + path: target.path, + pathMatchType: target.pathMatchType, + rewritePath: target.rewritePath, + rewritePathType: target.rewritePathType, + priority: target.priority, + enabled: target.enabled, + hcEnabled: target.hcEnabled, + hcPath: target.hcPath, + hcInterval: target.hcInterval, + hcTimeout: target.hcTimeout + }); + + if (response.status === 200) { + // Update the target with the new ID and remove the new flag + setTargets((prev) => + prev.map((t) => + t.targetId === target.targetId + ? { + ...t, + targetId: response.data.data.targetId, + new: false, + updated: false + } + : t + ) + ); + + toast({ + title: t("targetCreated"), + description: t("targetCreatedDescription") + }); + } + } catch (err) { + console.error(err); + toast({ + variant: "destructive", + title: t("targetErrorCreate"), + description: formatAxiosError( + err, + t("targetErrorCreateDescription") + ) + }); + } finally { + setTargetsLoading(false); + } + } + async function addTarget(data: z.infer) { // Check if target with same IP, port and method already exists const isDuplicate = targets.some( @@ -514,16 +645,6 @@ export default function ReverseProxyTargets(props: { }; setTargets([...targets, newTarget]); - addTargetForm.reset({ - ip: "", - method: resource.http ? "http" : null, - port: "" as any as number, - path: null, - pathMatchType: null, - rewritePath: null, - rewritePathType: null, - priority: 100 - }); } const removeTarget = (targetId: number) => { @@ -573,6 +694,24 @@ export default function ReverseProxyTargets(props: { }; async function saveAllSettings() { + // Validate that no targets have blank IPs or invalid ports + const targetsWithInvalidFields = targets.filter( + (target) => + !target.ip || + target.ip.trim() === "" || + !target.port || + target.port <= 0 || + isNaN(target.port) + ); + if (targetsWithInvalidFields.length > 0) { + toast({ + variant: "destructive", + title: t("targetErrorInvalidIp"), + description: t("targetErrorInvalidIpDescription") + }); + return; + } + try { setTargetsLoading(true); setHttpsTlsLoading(true); @@ -673,23 +812,10 @@ export default function ReverseProxyTargets(props: { } } - const columns: ColumnDef[] = [ - { - accessorKey: "enabled", - header: t("enabled"), - cell: ({ row }) => ( - - updateTarget(row.original.targetId, { - ...row.original, - enabled: val - }) - } - /> - ) - }, - { + const getColumns = (): ColumnDef[] => { + const baseColumns: ColumnDef[] = []; + + const priorityColumn: ColumnDef = { id: "priority", header: () => (
@@ -713,13 +839,13 @@ export default function ReverseProxyTargets(props: { ), cell: ({ row }) => { return ( -
+
{ const value = parseInt(e.target.value, 10); if (value >= 1 && value <= 1000) { @@ -732,9 +858,13 @@ export default function ReverseProxyTargets(props: { />
); - } - }, - { + }, + size: 120, + minSize: 100, + maxSize: 150 + }; + + const healthCheckColumn: ColumnDef = { accessorKey: "healthCheck", header: t("healthCheck"), cell: ({ row }) => { @@ -778,43 +908,35 @@ export default function ReverseProxyTargets(props: { }; return ( - <> +
{row.original.siteType === "newt" ? ( ) : ( - + - )} - +
); - } - }, - { + }, + size: 200, + minSize: 180, + maxSize: 250 + }; + + const matchPathColumn: ColumnDef = { accessorKey: "path", header: t("matchPath"), cell: ({ row }) => { @@ -822,56 +944,61 @@ export default function ReverseProxyTargets(props: { row.original.path || row.original.pathMatchType ); - return hasPathMatch ? ( -
- - updateTarget(row.original.targetId, config) - } - trigger={ - - } - /> - -
- ) : ( -
- - updateTarget(row.original.targetId, config) - } - trigger={ - - } - /> - + return ( +
+ {hasPathMatch ? ( + + updateTarget(row.original.targetId, config) + } + trigger={ + + } + /> + ) : ( + + updateTarget(row.original.targetId, config) + } + trigger={ + + } + /> + )}
); - } - }, - { + }, + size: 200, + minSize: 180, + maxSize: 200 + }; + + const addressColumn: ColumnDef = { accessorKey: "address", header: t("address"), cell: ({ row }) => { @@ -896,25 +1023,24 @@ export default function ReverseProxyTargets(props: { }; return ( -
- @@ -1004,14 +1130,14 @@ export default function ReverseProxyTargets(props: { -
+
{"://"}
{ const input = e.target.value.trim(); const hasProtocol = @@ -1051,27 +1177,42 @@ export default function ReverseProxyTargets(props: { } }} /> -
+
{":"}
- updateTarget(row.original.targetId, { - ...row.original, - port: parseInt(e.target.value, 10) - }) + defaultValue={ + row.original.port === 0 + ? "" + : row.original.port } + className="w-[75px] pl-0 border-none placeholder-gray-400" + onBlur={(e) => { + const value = parseInt(e.target.value, 10); + if (!isNaN(value) && value > 0) { + updateTarget(row.original.targetId, { + ...row.original, + port: value + }); + } else { + updateTarget(row.original.targetId, { + ...row.original, + port: 0 + }); + } + }} /> - - +
); - } - }, - { + }, + size: 400, + minSize: 350, + maxSize: 500 + }; + + const rewritePathColumn: ColumnDef = { accessorKey: "rewritePath", header: t("rewritePath"), cell: ({ row }) => { @@ -1081,98 +1222,125 @@ export default function ReverseProxyTargets(props: { const noPathMatch = !row.original.path && !row.original.pathMatchType; - return hasRewritePath && !noPathMatch ? ( -
- - updateTarget(row.original.targetId, config) - } - trigger={ - - } - /> + return ( +
+ {hasRewritePath && !noPathMatch ? ( + + updateTarget(row.original.targetId, config) + } + trigger={ + + } + /> + ) : ( + + updateTarget(row.original.targetId, config) + } + trigger={ + + } + disabled={noPathMatch} + /> + )}
- ) : ( - - updateTarget(row.original.targetId, config) - } - trigger={ - - } - disabled={noPathMatch} - /> ); - } - }, + }, + size: 200, + minSize: 180, + maxSize: 200 + }; - // { - // accessorKey: "protocol", - // header: t('targetProtocol'), - // cell: ({ row }) => ( - // - // ), - // }, + const enabledColumn: ColumnDef = { + accessorKey: "enabled", + header: t("enabled"), + cell: ({ row }) => ( +
+ + updateTarget(row.original.targetId, { + ...row.original, + enabled: val + }) + } + /> +
+ ), + size: 100, + minSize: 80, + maxSize: 120 + }; - { + const actionsColumn: ColumnDef = { id: "actions", cell: ({ row }) => ( - <> -
- {/* */} +
+ +
+ ), + size: 100, + minSize: 80, + maxSize: 120 + }; - -
- - ) + if (isAdvancedMode) { + return [ + matchPathColumn, + addressColumn, + rewritePathColumn, + priorityColumn, + healthCheckColumn, + enabledColumn, + actionsColumn + ]; + } else { + return [ + addressColumn, + healthCheckColumn, + enabledColumn, + actionsColumn + ]; } - ]; + }; + + const columns = getColumns(); const table = useReactTable({ data: targets, @@ -1203,351 +1371,8 @@ export default function ReverseProxyTargets(props: { -
-
- -
- ( - - - {t("site")} - -
- - - - - - - - - - - - {t( - "siteNotFound" - )} - - - {sites.map( - ( - site - ) => ( - { - addTargetForm.setValue( - "siteId", - site.siteId - ); - }} - > - - { - site.name - } - - ) - )} - - - - - - - {field.value && - (() => { - const selectedSite = - sites.find( - (site) => - site.siteId === - field.value - ); - return selectedSite && - selectedSite.type === - "newt" - ? (() => { - const dockerState = - getDockerStateForSite( - selectedSite.siteId - ); - return ( - - refreshContainersForSite( - selectedSite.siteId - ) - } - /> - ); - })() - : null; - })()} -
- -
- )} - /> - - {resource.http && ( - ( - - - {t("method")} - - - - - - - )} - /> - )} - - ( - - - {t("targetAddr")} - - - { - const input = - e.target.value.trim(); - const hasProtocol = - /^(https?|h2c):\/\//.test( - input - ); - const hasPort = - /:\d+(?:\/|$)/.test( - input - ); - - if ( - hasProtocol || - hasPort - ) { - const parsed = - parseHostTarget( - input - ); - if (parsed) { - if ( - hasProtocol || - !addTargetForm.getValues( - "method" - ) - ) { - addTargetForm.setValue( - "method", - parsed.protocol - ); - } - addTargetForm.setValue( - "ip", - parsed.host - ); - if ( - hasPort || - !addTargetForm.getValues( - "port" - ) - ) { - addTargetForm.setValue( - "port", - parsed.port - ); - } - } - } else { - field.onBlur(); - } - }} - /> - - - - )} - /> - ( - - - {t("targetPort")} - - - - - - - )} - /> - -
- - -
- {targets.length > 0 ? ( <> -
- {t("targetsList")} -
- -
- - ( - - - { - field.onChange( - val - ); - }} - /> - - - )} - /> - - -
@@ -1616,12 +1441,40 @@ export default function ReverseProxyTargets(props: { {/* */}
+
+
+ +
+ + +
+
+
) : ( -
-

+

+

{t("targetNoOne")}

+
)} @@ -1659,6 +1512,9 @@ export default function ReverseProxyTargets(props: { label={t( "proxyEnableSSL" )} + description={t( + "proxyEnableSSLDescription" + )} defaultChecked={ field.value } @@ -1699,6 +1555,46 @@ export default function ReverseProxyTargets(props: { + +
+ + ( + + + { + field.onChange(val); + }} + /> + + + )} + /> + + +
+
( - + {t("customHeaders")} diff --git a/src/components/HealthCheckDialog.tsx b/src/components/HealthCheckDialog.tsx index 1942e1d8..6fa36a5b 100644 --- a/src/components/HealthCheckDialog.tsx +++ b/src/components/HealthCheckDialog.tsx @@ -107,7 +107,7 @@ export default function HealthCheckDialog({ useEffect(() => { if (!open) return; - + // Determine default scheme from target method const getDefaultScheme = () => { if (initialConfig?.hcScheme) { @@ -177,7 +177,7 @@ export default function HealthCheckDialog({ render={({ field }) => (
- + {t("enableHealthChecks")} @@ -210,7 +210,7 @@ export default function HealthCheckDialog({ name="hcScheme" render={({ field }) => ( - + {t("healthScheme")} ( - + {t( "healthyIntervalSeconds" )} @@ -425,7 +425,7 @@ export default function HealthCheckDialog({ name="hcUnhealthyInterval" render={({ field }) => ( - + {t( "unhealthyIntervalSeconds" )} @@ -460,7 +460,7 @@ export default function HealthCheckDialog({ name="hcTimeout" render={({ field }) => ( - + {t("timeoutSeconds")} @@ -499,7 +499,7 @@ export default function HealthCheckDialog({ name="hcStatus" render={({ field }) => ( - + {t("expectedResponseCodes")} @@ -541,7 +541,7 @@ export default function HealthCheckDialog({ name="hcHeaders" render={({ field }) => ( - + {t("customHeaders")} diff --git a/src/components/PathMatchRenameModal.tsx b/src/components/PathMatchRenameModal.tsx index f67f44c3..13d83bd1 100644 --- a/src/components/PathMatchRenameModal.tsx +++ b/src/components/PathMatchRenameModal.tsx @@ -1,4 +1,4 @@ -import { Pencil } from "lucide-react"; +import { Settings } from "lucide-react"; import { Select, SelectContent, @@ -256,7 +256,7 @@ export function PathMatchDisplay({ {value.path} - +
); } @@ -287,7 +287,7 @@ export function PathRewriteDisplay({ {value.rewritePath || (strip)} - +
); } diff --git a/src/components/ui/textarea.tsx b/src/components/ui/textarea.tsx index dcfc646d..83e84073 100644 --- a/src/components/ui/textarea.tsx +++ b/src/components/ui/textarea.tsx @@ -10,7 +10,7 @@ const Textarea = React.forwardRef( return (