From f218b5a9449302160c7e9779ad232920033bcc78 Mon Sep 17 00:00:00 2001 From: x032205 Date: Fri, 17 Oct 2025 06:26:24 -0400 Subject: [PATCH 01/10] feat(gateways): gateway and relay deployment CLI command interactive helper --- frontend/src/const/routes.ts | 6 +- .../components/GatewayTab/GatewayTab.tsx | 20 +- .../components/DeployGatewayModal.tsx | 243 ++++++++++++++++++ .../components/DeployRelayModal.tsx | 204 +++++++++++++++ .../GatewayTab/components/RelayOption.tsx | 32 +++ .../NetworkingTabGroup/NetworkingTabGroup.tsx | 21 +- .../components/RelayTab/RelayTab.tsx | 24 +- .../organization/NetworkingPage/route.tsx | 6 +- frontend/src/routeTree.gen.ts | 103 +++----- frontend/src/routes.ts | 2 +- 10 files changed, 571 insertions(+), 90 deletions(-) create mode 100644 frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/DeployGatewayModal.tsx create mode 100644 frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/DeployRelayModal.tsx create mode 100644 frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/RelayOption.tsx diff --git a/frontend/src/const/routes.ts b/frontend/src/const/routes.ts index 2e835a1db..a31306818 100644 --- a/frontend/src/const/routes.ts +++ b/frontend/src/const/routes.ts @@ -66,7 +66,11 @@ export const ROUTE_PATHS = Object.freeze({ "/organization/app-connections/$appConnection/oauth/callback", "/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback" ) - } + }, + NetworkingPage: setRoute( + "/organization/networking", + "/_authenticate/_inject-org-details/_org-layout/organization/networking" + ) }, SecretManager: { ApprovalPage: setRoute( diff --git a/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/GatewayTab.tsx b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/GatewayTab.tsx index 27ebb6820..d5e79d326 100644 --- a/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/GatewayTab.tsx +++ b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/GatewayTab.tsx @@ -8,6 +8,7 @@ import { faEllipsisV, faInfoCircle, faMagnifyingGlass, + faPlus, faSearch, faTrash } from "@fortawesome/free-solid-svg-icons"; @@ -17,6 +18,7 @@ import { useQuery } from "@tanstack/react-query"; import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions"; import { + Button, DeleteActionModal, DropdownMenu, DropdownMenuContent, @@ -47,6 +49,7 @@ import { gatewaysQueryKeys, useDeleteGatewayById } from "@app/hooks/api/gateways import { useDeleteGatewayV2ById } from "@app/hooks/api/gateways-v2"; import { EditGatewayDetailsModal } from "./components/EditGatewayDetailsModal"; +import { DeployGatewayModal } from "./components/DeployGatewayModal"; const GatewayHealthStatus = ({ heartbeat }: { heartbeat?: string }) => { const heartbeatDate = heartbeat ? new Date(heartbeat) : null; @@ -73,6 +76,7 @@ export const GatewayTab = withPermission( const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list()); const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([ + "deployGateway", "deleteGateway", "editDetails" ] as const); @@ -101,8 +105,8 @@ export const GatewayTab = withPermission( return (
-
-
+
+

Gateways

+
+

@@ -257,6 +269,10 @@ export const GatewayTab = withPermission( deleteKey="confirm" onDeleteApproved={() => handleDeleteGateway()} /> + handlePopUpToggle("deployGateway", isOpen)} + />

diff --git a/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/DeployGatewayModal.tsx b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/DeployGatewayModal.tsx new file mode 100644 index 000000000..2b8f17fe7 --- /dev/null +++ b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/DeployGatewayModal.tsx @@ -0,0 +1,243 @@ +import { createNotification } from "@app/components/notifications"; +import { + FilterableSelect, + FormLabel, + IconButton, + Input, + Modal, + ModalContent +} from "@app/components/v2"; +import { useOrganization } from "@app/context"; +import { + useAddIdentityTokenAuth, + useCreateTokenIdentityTokenAuth, + useGetIdentityMembershipOrgs, + useGetIdentityTokenAuth +} from "@app/hooks/api"; +import { useGetRelays } from "@app/hooks/api/relays"; +import { slugSchema } from "@app/lib/schemas"; +import { faCopy, faUpRightFromSquare } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useEffect, useMemo, useState } from "react"; +import { SingleValue } from "react-select"; +import { RelayOption } from "./RelayOption"; +import { useNavigate } from "@tanstack/react-router"; +import { ROUTE_PATHS } from "@app/const/routes"; +import { twMerge } from "tailwind-merge"; + +type Props = { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; +}; + +export const DeployGatewayModal = ({ isOpen, onOpenChange }: Props) => { + const { protocol, hostname, port } = window.location; + const portSuffix = port && port !== "80" ? `:${port}` : ""; + const siteURL = `${protocol}//${hostname}${portSuffix}`; + + const navigate = useNavigate({ + from: ROUTE_PATHS.Organization.NetworkingPage.path + }); + + const [name, setName] = useState(""); + const [relay, setRelay] = useState(null); + const [identity, setIdentity] = useState(null); + const [identityToken, setIdentityToken] = useState(""); + + useEffect(() => { + if (!isOpen) { + setName(""); + setRelay(null); + setIdentity(null); + setIdentityToken(""); + } + }, [isOpen]); + + const { data: relays, isPending: isRelaysLoading } = useGetRelays(); + + const { currentOrg } = useOrganization(); + const organizationId = currentOrg?.id || ""; + + const { data: identityMembershipOrgsData, isPending: isIdentitiesLoading } = + useGetIdentityMembershipOrgs({ + organizationId, + limit: 20000 + }); + const identityMembershipOrgs = identityMembershipOrgsData?.identityMemberships || []; + + const { mutateAsync: createToken } = useCreateTokenIdentityTokenAuth(); + const { mutateAsync: addIdentityTokenAuth } = useAddIdentityTokenAuth(); + const { refetch } = useGetIdentityTokenAuth(identity?.id ?? ""); + + useEffect(() => { + const generateToken = async () => { + if (!identity) return; + + try { + const { data: identityTokenAuth } = await refetch(); + if (!identityTokenAuth) { + await addIdentityTokenAuth({ + identityId: identity.id, + organizationId, + accessTokenTTL: 2592000, + accessTokenMaxTTL: 2592000, + accessTokenNumUsesLimit: 0, + accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }] + }); + createNotification({ + text: "Automatically enabled token authentication for this identity.", + type: "info" + }); + } + + const token = await createToken({ + identityId: identity.id, + name: "gateway token (autogenerated)" + }); + setIdentityToken(token.accessToken); + createNotification({ + text: "Automatically generated a token for this identity.", + type: "info" + }); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to generate token for identity", + type: "error" + }); + setIdentityToken(""); + } + }; + + generateToken(); + }, [identity, organizationId, refetch, addIdentityTokenAuth, createToken]); + + const handleIdentityChange = (selectedIdentity: SingleValue<{ id: string; name: string }>) => { + setIdentity(selectedIdentity); + setIdentityToken(""); + }; + + const isNameValid = useMemo(() => !name || slugSchema().safeParse(name).success, [name]); + + const isCommandReady = useMemo( + () => !!name && isNameValid && !!relay && !!identityToken, + [name, isNameValid, relay, identityToken] + ); + + const command = useMemo( + () => + `infisical gateway start --name=${name} --relay=${ + relay?.name || "" + } --domain=${siteURL} --token=${identityToken}`, + [name, relay, identityToken, siteURL] + ); + + return ( + + + + setName(e.target.value)} + placeholder="Enter gateway name..." + isError={!isNameValid} + /> + + + { + if ((newValue as SingleValue<{ id: string }>)?.id === "_create") { + navigate({ + search: { selectedTab: "relays" } + }); + return; + } + + setRelay(newValue as SingleValue<{ id: string; name: string }>); + }} + isLoading={isRelaysLoading} + options={[ + { + id: "_create", + name: "Deploy New Relay" + }, + ...(relays || []) + ]} + placeholder="Select relay..." + getOptionLabel={(option) => option.name} + getOptionValue={(option) => option.id} + components={{ Option: RelayOption }} + /> + + + + handleIdentityChange( + e as SingleValue<{ + id: string; + name: string; + }> + ) + } + isLoading={isIdentitiesLoading} + placeholder="Select identity..." + options={identityMembershipOrgs.map((membership) => membership.identity)} + getOptionValue={(option) => option.id} + getOptionLabel={(option) => option.name} + /> + + +
+ + { + navigator.clipboard.writeText(command); + + createNotification({ + text: "Command copied to clipboard", + type: "info" + }); + }} + className={twMerge("w-10", !isCommandReady && "pointer-events-none opacity-50")} + isDisabled={!isCommandReady} + > + + +
+ + Install the Infisical CLI + + +
+
+ ); +}; diff --git a/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/DeployRelayModal.tsx b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/DeployRelayModal.tsx new file mode 100644 index 000000000..2347c0472 --- /dev/null +++ b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/DeployRelayModal.tsx @@ -0,0 +1,204 @@ +import { createNotification } from "@app/components/notifications"; +import { + FilterableSelect, + FormLabel, + IconButton, + Input, + Modal, + ModalContent +} from "@app/components/v2"; +import { useOrganization } from "@app/context"; +import { + useAddIdentityTokenAuth, + useCreateTokenIdentityTokenAuth, + useGetIdentityMembershipOrgs, + useGetIdentityTokenAuth +} from "@app/hooks/api"; +import { slugSchema } from "@app/lib/schemas"; +import { faCopy, faUpRightFromSquare } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useEffect, useMemo, useState } from "react"; +import { SingleValue } from "react-select"; +import { twMerge } from "tailwind-merge"; + +type Props = { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; +}; + +export const DeployRelayModal = ({ isOpen, onOpenChange }: Props) => { + const { protocol, hostname, port } = window.location; + const portSuffix = port && port !== "80" ? `:${port}` : ""; + const siteURL = `${protocol}//${hostname}${portSuffix}`; + + const [name, setName] = useState(""); + const [host, setHost] = useState(""); + const [identity, setIdentity] = useState(null); + const [identityToken, setIdentityToken] = useState(""); + + useEffect(() => { + if (!isOpen) { + setName(""); + setHost(""); + setIdentity(null); + setIdentityToken(""); + } + }, [isOpen]); + + const { currentOrg } = useOrganization(); + const organizationId = currentOrg?.id || ""; + + const { data: identityMembershipOrgsData, isPending: isIdentitiesLoading } = + useGetIdentityMembershipOrgs({ + organizationId, + limit: 20000 + }); + const identityMembershipOrgs = identityMembershipOrgsData?.identityMemberships || []; + + const { mutateAsync: createToken } = useCreateTokenIdentityTokenAuth(); + const { mutateAsync: addIdentityTokenAuth } = useAddIdentityTokenAuth(); + const { refetch } = useGetIdentityTokenAuth(identity?.id ?? ""); + + useEffect(() => { + const generateToken = async () => { + if (!identity) return; + + try { + const { data: identityTokenAuth } = await refetch(); + if (!identityTokenAuth) { + await addIdentityTokenAuth({ + identityId: identity.id, + organizationId, + accessTokenTTL: 2592000, + accessTokenMaxTTL: 2592000, + accessTokenNumUsesLimit: 0, + accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }] + }); + createNotification({ + text: "Automatically enabled token authentication for this identity.", + type: "info" + }); + } + + const token = await createToken({ + identityId: identity.id, + name: "relay token (autogenerated)" + }); + setIdentityToken(token.accessToken); + createNotification({ + text: "Automatically generated a token for this identity.", + type: "info" + }); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to generate token for identity", + type: "error" + }); + setIdentityToken(""); + } + }; + + generateToken(); + }, [identity, organizationId, refetch, addIdentityTokenAuth, createToken]); + + const handleIdentityChange = (selectedIdentity: SingleValue<{ id: string; name: string }>) => { + setIdentity(selectedIdentity); + setIdentityToken(""); + }; + + const isNameValid = useMemo(() => !name || slugSchema().safeParse(name).success, [name]); + + const isCommandReady = useMemo( + () => !!name && !!host && isNameValid && !!identityToken, + [name, isNameValid, host, identityToken] + ); + + const command = useMemo( + () => + `infisical relay start --name=${name} --domain=${siteURL} --host=${host} --token=${identityToken}`, + [name, siteURL, host, identityToken] + ); + + return ( + + + + setName(e.target.value)} + placeholder="Enter relay name..." + isError={!isNameValid} + /> + + + setHost(e.target.value)} placeholder="0.0.0.0" /> + + + + handleIdentityChange( + e as SingleValue<{ + id: string; + name: string; + }> + ) + } + isLoading={isIdentitiesLoading} + placeholder="Select identity..." + options={identityMembershipOrgs.map((membership) => membership.identity)} + getOptionValue={(option) => option.id} + getOptionLabel={(option) => option.name} + /> + + +
+ + { + navigator.clipboard.writeText(command); + + createNotification({ + text: "Command copied to clipboard", + type: "info" + }); + }} + className={twMerge("w-10", !isCommandReady && "pointer-events-none opacity-50")} + isDisabled={!isCommandReady} + > + + +
+ + Install the Infisical CLI + + +
+
+ ); +}; diff --git a/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/RelayOption.tsx b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/RelayOption.tsx new file mode 100644 index 000000000..90c9a7ebd --- /dev/null +++ b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/RelayOption.tsx @@ -0,0 +1,32 @@ +import { components, OptionProps } from "react-select"; +import { faCheckCircle } from "@fortawesome/free-regular-svg-icons"; +import { faPlus } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +export const RelayOption = ({ + isSelected, + children, + ...props +}: OptionProps<{ id: string; name: string }>) => { + const isCreateOption = props.data.id === "_create"; + + return ( + +
+ {isCreateOption ? ( +
+ + Deploy New Relay +
+ ) : ( + <> +

{children}

+ {isSelected && ( + + )} + + )} +
+
+ ); +}; diff --git a/frontend/src/pages/organization/NetworkingPage/components/NetworkingTabGroup/NetworkingTabGroup.tsx b/frontend/src/pages/organization/NetworkingPage/components/NetworkingTabGroup/NetworkingTabGroup.tsx index 956a4d1ed..dc90b7f21 100644 --- a/frontend/src/pages/organization/NetworkingPage/components/NetworkingTabGroup/NetworkingTabGroup.tsx +++ b/frontend/src/pages/organization/NetworkingPage/components/NetworkingTabGroup/NetworkingTabGroup.tsx @@ -1,14 +1,19 @@ -import { useState } from "react"; -import { useSearch } from "@tanstack/react-router"; +import { useNavigate, useSearch } from "@tanstack/react-router"; import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { GatewayTab } from "../GatewayTab/GatewayTab"; import { RelayTab } from "../RelayTab/RelayTab"; +import { ROUTE_PATHS } from "@app/const/routes"; export const NetworkingTabGroup = () => { - const search = useSearch({ - from: "/_authenticate/_inject-org-details/_org-layout/organization/networking/" + const navigate = useNavigate({ + from: ROUTE_PATHS.Organization.NetworkingPage.path + }); + const selectedTab = useSearch({ + from: ROUTE_PATHS.Organization.NetworkingPage.id, + select: (el) => el.selectedTab, + structuralSharing: true }); const tabs = [ @@ -16,10 +21,14 @@ export const NetworkingTabGroup = () => { { name: "Relays", key: "relays", component: RelayTab } ]; - const [selectedTab, setSelectedTab] = useState(search.selectedTab || tabs[0].key); + const handleTabChange = (tab: string) => { + navigate({ + search: { selectedTab: tab } + }); + }; return ( - + {tabs.map((tab) => ( diff --git a/frontend/src/pages/organization/NetworkingPage/components/RelayTab/RelayTab.tsx b/frontend/src/pages/organization/NetworkingPage/components/RelayTab/RelayTab.tsx index e525b765d..2bfa8b250 100644 --- a/frontend/src/pages/organization/NetworkingPage/components/RelayTab/RelayTab.tsx +++ b/frontend/src/pages/organization/NetworkingPage/components/RelayTab/RelayTab.tsx @@ -7,6 +7,7 @@ import { faEllipsisV, faInfoCircle, faMagnifyingGlass, + faPlus, faSearch, faTrash } from "@fortawesome/free-solid-svg-icons"; @@ -16,6 +17,7 @@ import { formatRelative } from "date-fns"; import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions"; import { + Button, DeleteActionModal, DropdownMenu, DropdownMenuContent, @@ -41,6 +43,7 @@ import { import { withPermission } from "@app/hoc"; import { usePopUp } from "@app/hooks"; import { useDeleteRelayById, useGetRelays } from "@app/hooks/api/relays"; +import { DeployRelayModal } from "../GatewayTab/components/DeployRelayModal"; const RelayHealthStatus = ({ heartbeat }: { heartbeat?: string }) => { const heartbeatDate = heartbeat ? new Date(heartbeat) : null; @@ -66,7 +69,10 @@ export const RelayTab = withPermission( const [search, setSearch] = useState(""); const { data: relays, isPending: isRelaysLoading } = useGetRelays(); - const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["deleteRelay"] as const); + const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([ + "deleteRelay", + "deployRelay" + ] as const); const deleteRelayById = useDeleteRelayById(); @@ -87,8 +93,8 @@ export const RelayTab = withPermission( return (
-
-
+
+

Relays

+
+

@@ -222,6 +236,10 @@ export const RelayTab = withPermission( deleteKey="confirm" onDeleteApproved={() => handleDeleteRelay()} /> + handlePopUpToggle("deployRelay", isOpen)} + />

diff --git a/frontend/src/pages/organization/NetworkingPage/route.tsx b/frontend/src/pages/organization/NetworkingPage/route.tsx index 906b12806..433647476 100644 --- a/frontend/src/pages/organization/NetworkingPage/route.tsx +++ b/frontend/src/pages/organization/NetworkingPage/route.tsx @@ -5,16 +5,16 @@ import { z } from "zod"; import { NetworkingPage } from "./NetworkingPage"; const NetworkingPageQueryParams = z.object({ - selectedTab: z.string().catch("") + selectedTab: z.string().catch("gateways") }); export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/organization/networking/" + "/_authenticate/_inject-org-details/_org-layout/organization/networking" )({ component: NetworkingPage, validateSearch: zodValidator(NetworkingPageQueryParams), search: { - middlewares: [stripSearchParams({ selectedTab: "" })] + middlewares: [stripSearchParams({ selectedTab: "gateways" })] }, context: () => ({ breadcrumbs: [ diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index 1562f302d..bc92b6ef0 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -50,6 +50,7 @@ import { Route as adminCachingPageRouteImport } from './pages/admin/CachingPage/ import { Route as adminAuthenticationPageRouteImport } from './pages/admin/AuthenticationPage/route' import { Route as adminAccessManagementPageRouteImport } from './pages/admin/AccessManagementPage/route' import { Route as organizationProjectsPageRouteImport } from './pages/organization/ProjectsPage/route' +import { Route as organizationNetworkingPageRouteImport } from './pages/organization/NetworkingPage/route' import { Route as organizationBillingPageRouteImport } from './pages/organization/BillingPage/route' import { Route as organizationAuditLogsPageRouteImport } from './pages/organization/AuditLogsPage/route' import { Route as organizationAccessManagementPageRouteImport } from './pages/organization/AccessManagementPage/route' @@ -63,7 +64,6 @@ import { Route as organizationIdentityDetailsByIDPageRouteImport } from './pages import { Route as organizationGroupDetailsByIDPageRouteImport } from './pages/organization/GroupDetailsByIDPage/route' import { Route as organizationSettingsPageRouteImport } from './pages/organization/SettingsPage/route' import { Route as organizationSecretSharingPageRouteImport } from './pages/organization/SecretSharingPage/route' -import { Route as organizationNetworkingPageRouteImport } from './pages/organization/NetworkingPage/route' import { Route as organizationAppConnectionsAppConnectionsPageRouteImport } from './pages/organization/AppConnections/AppConnectionsPage/route' import { Route as sshLayoutImport } from './pages/ssh/layout' import { Route as secretScanningLayoutImport } from './pages/secret-scanning/layout' @@ -270,10 +270,6 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingImport = createFileRoute( '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing', )() -const AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingImport = - createFileRoute( - '/_authenticate/_inject-org-details/_org-layout/organization/networking', - )() const AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsImport = createFileRoute( '/_authenticate/_inject-org-details/_org-layout/organization/app-connections', @@ -594,14 +590,6 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRoute = AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute, } as any) -const AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRoute = - AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingImport.update({ - id: '/networking', - path: '/networking', - getParentRoute: () => - AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute, - } as any) - const AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute = AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsImport.update({ id: '/app-connections', @@ -658,6 +646,14 @@ const organizationProjectsPageRouteRoute = AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute, } as any) +const organizationNetworkingPageRouteRoute = + organizationNetworkingPageRouteImport.update({ + id: '/networking', + path: '/networking', + getParentRoute: () => + AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute, + } as any) + const organizationBillingPageRouteRoute = organizationBillingPageRouteImport.update({ id: '/billing', @@ -807,14 +803,6 @@ const organizationSecretSharingPageRouteRoute = AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRoute, } as any) -const organizationNetworkingPageRouteRoute = - organizationNetworkingPageRouteImport.update({ - id: '/', - path: '/', - getParentRoute: () => - AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRoute, - } as any) - const organizationAppConnectionsAppConnectionsPageRouteRoute = organizationAppConnectionsAppConnectionsPageRouteImport.update({ id: '/', @@ -2477,6 +2465,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof organizationBillingPageRouteImport parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport } + '/_authenticate/_inject-org-details/_org-layout/organization/networking': { + id: '/_authenticate/_inject-org-details/_org-layout/organization/networking' + path: '/networking' + fullPath: '/organization/networking' + preLoaderRoute: typeof organizationNetworkingPageRouteImport + parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport + } '/_authenticate/_inject-org-details/_org-layout/organization/projects': { id: '/_authenticate/_inject-org-details/_org-layout/organization/projects' path: '/projects' @@ -2533,13 +2528,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsImport parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport } - '/_authenticate/_inject-org-details/_org-layout/organization/networking': { - id: '/_authenticate/_inject-org-details/_org-layout/organization/networking' - path: '/networking' - fullPath: '/organization/networking' - preLoaderRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingImport - parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport - } '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing': { id: '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing' path: '/secret-sharing' @@ -2568,13 +2556,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof organizationAppConnectionsAppConnectionsPageRouteImport parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsImport } - '/_authenticate/_inject-org-details/_org-layout/organization/networking/': { - id: '/_authenticate/_inject-org-details/_org-layout/organization/networking/' - path: '/' - fullPath: '/organization/networking/' - preLoaderRoute: typeof organizationNetworkingPageRouteImport - parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingImport - } '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/': { id: '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/' path: '/' @@ -4002,20 +3983,6 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithCh AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteChildren, ) -interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRouteChildren { - organizationNetworkingPageRouteRoute: typeof organizationNetworkingPageRouteRoute -} - -const AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRouteChildren: AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRouteChildren = - { - organizationNetworkingPageRouteRoute: organizationNetworkingPageRouteRoute, - } - -const AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRouteWithChildren = - AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRoute._addFileChildren( - AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRouteChildren, - ) - interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteChildren { organizationSecretSharingPageRouteRoute: typeof organizationSecretSharingPageRouteRoute organizationSecretSharingSettingsPageRouteRoute: typeof organizationSecretSharingSettingsPageRouteRoute @@ -4055,9 +4022,9 @@ interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren { organizationAccessManagementPageRouteRoute: typeof organizationAccessManagementPageRouteRoute organizationAuditLogsPageRouteRoute: typeof organizationAuditLogsPageRouteRoute organizationBillingPageRouteRoute: typeof organizationBillingPageRouteRoute + organizationNetworkingPageRouteRoute: typeof organizationNetworkingPageRouteRoute organizationProjectsPageRouteRoute: typeof organizationProjectsPageRouteRoute AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren - AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRouteWithChildren AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteWithChildren AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRouteWithChildren organizationGroupDetailsByIDPageRouteRoute: typeof organizationGroupDetailsByIDPageRouteRoute @@ -4072,11 +4039,10 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren: Authentica organizationAccessManagementPageRouteRoute, organizationAuditLogsPageRouteRoute: organizationAuditLogsPageRouteRoute, organizationBillingPageRouteRoute: organizationBillingPageRouteRoute, + organizationNetworkingPageRouteRoute: organizationNetworkingPageRouteRoute, organizationProjectsPageRouteRoute: organizationProjectsPageRouteRoute, AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute: AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren, - AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRoute: - AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRouteWithChildren, AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRoute: AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteWithChildren, AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRoute: @@ -5057,6 +5023,7 @@ export interface FileRoutesByFullPath { '/organization/access-management': typeof organizationAccessManagementPageRouteRoute '/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute '/organization/billing': typeof organizationBillingPageRouteRoute + '/organization/networking': typeof organizationNetworkingPageRouteRoute '/organization/projects': typeof organizationProjectsPageRouteRoute '/admin/access-management': typeof adminAccessManagementPageRouteRoute '/admin/authentication': typeof adminAuthenticationPageRouteRoute @@ -5065,12 +5032,10 @@ export interface FileRoutesByFullPath { '/admin/environment': typeof adminEnvironmentPageRouteRoute '/admin/integrations': typeof adminIntegrationsPageRouteRoute '/organization/app-connections': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren - '/organization/networking': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRouteWithChildren '/organization/secret-sharing': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteWithChildren '/organization/settings': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRouteWithChildren '/secret-manager/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdRouteWithChildren '/organization/app-connections/': typeof organizationAppConnectionsAppConnectionsPageRouteRoute - '/organization/networking/': typeof organizationNetworkingPageRouteRoute '/organization/secret-sharing/': typeof organizationSecretSharingPageRouteRoute '/organization/settings/': typeof organizationSettingsPageRouteRoute '/organization/groups/$groupId': typeof organizationGroupDetailsByIDPageRouteRoute @@ -5294,6 +5259,7 @@ export interface FileRoutesByTo { '/organization/access-management': typeof organizationAccessManagementPageRouteRoute '/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute '/organization/billing': typeof organizationBillingPageRouteRoute + '/organization/networking': typeof organizationNetworkingPageRouteRoute '/organization/projects': typeof organizationProjectsPageRouteRoute '/admin/access-management': typeof adminAccessManagementPageRouteRoute '/admin/authentication': typeof adminAuthenticationPageRouteRoute @@ -5303,7 +5269,6 @@ export interface FileRoutesByTo { '/admin/integrations': typeof adminIntegrationsPageRouteRoute '/secret-manager/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdRouteWithChildren '/organization/app-connections': typeof organizationAppConnectionsAppConnectionsPageRouteRoute - '/organization/networking': typeof organizationNetworkingPageRouteRoute '/organization/secret-sharing': typeof organizationSecretSharingPageRouteRoute '/organization/settings': typeof organizationSettingsPageRouteRoute '/organization/groups/$groupId': typeof organizationGroupDetailsByIDPageRouteRoute @@ -5529,6 +5494,7 @@ export interface FileRoutesById { '/_authenticate/_inject-org-details/_org-layout/organization/access-management': typeof organizationAccessManagementPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/billing': typeof organizationBillingPageRouteRoute + '/_authenticate/_inject-org-details/_org-layout/organization/networking': typeof organizationNetworkingPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/projects': typeof organizationProjectsPageRouteRoute '/_authenticate/_inject-org-details/admin/_admin-layout/access-management': typeof adminAccessManagementPageRouteRoute '/_authenticate/_inject-org-details/admin/_admin-layout/authentication': typeof adminAuthenticationPageRouteRoute @@ -5537,12 +5503,10 @@ export interface FileRoutesById { '/_authenticate/_inject-org-details/admin/_admin-layout/environment': typeof adminEnvironmentPageRouteRoute '/_authenticate/_inject-org-details/admin/_admin-layout/integrations': typeof adminIntegrationsPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/app-connections': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren - '/_authenticate/_inject-org-details/_org-layout/organization/networking': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/organization/settings': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/': typeof organizationAppConnectionsAppConnectionsPageRouteRoute - '/_authenticate/_inject-org-details/_org-layout/organization/networking/': typeof organizationNetworkingPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/': typeof organizationSecretSharingPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/settings/': typeof organizationSettingsPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId': typeof organizationGroupDetailsByIDPageRouteRoute @@ -5778,6 +5742,7 @@ export interface FileRouteTypes { | '/organization/access-management' | '/organization/audit-logs' | '/organization/billing' + | '/organization/networking' | '/organization/projects' | '/admin/access-management' | '/admin/authentication' @@ -5786,12 +5751,10 @@ export interface FileRouteTypes { | '/admin/environment' | '/admin/integrations' | '/organization/app-connections' - | '/organization/networking' | '/organization/secret-sharing' | '/organization/settings' | '/secret-manager/$projectId' | '/organization/app-connections/' - | '/organization/networking/' | '/organization/secret-sharing/' | '/organization/settings/' | '/organization/groups/$groupId' @@ -6014,6 +5977,7 @@ export interface FileRouteTypes { | '/organization/access-management' | '/organization/audit-logs' | '/organization/billing' + | '/organization/networking' | '/organization/projects' | '/admin/access-management' | '/admin/authentication' @@ -6023,7 +5987,6 @@ export interface FileRouteTypes { | '/admin/integrations' | '/secret-manager/$projectId' | '/organization/app-connections' - | '/organization/networking' | '/organization/secret-sharing' | '/organization/settings' | '/organization/groups/$groupId' @@ -6247,6 +6210,7 @@ export interface FileRouteTypes { | '/_authenticate/_inject-org-details/_org-layout/organization/access-management' | '/_authenticate/_inject-org-details/_org-layout/organization/audit-logs' | '/_authenticate/_inject-org-details/_org-layout/organization/billing' + | '/_authenticate/_inject-org-details/_org-layout/organization/networking' | '/_authenticate/_inject-org-details/_org-layout/organization/projects' | '/_authenticate/_inject-org-details/admin/_admin-layout/access-management' | '/_authenticate/_inject-org-details/admin/_admin-layout/authentication' @@ -6255,12 +6219,10 @@ export interface FileRouteTypes { | '/_authenticate/_inject-org-details/admin/_admin-layout/environment' | '/_authenticate/_inject-org-details/admin/_admin-layout/integrations' | '/_authenticate/_inject-org-details/_org-layout/organization/app-connections' - | '/_authenticate/_inject-org-details/_org-layout/organization/networking' | '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing' | '/_authenticate/_inject-org-details/_org-layout/organization/settings' | '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId' | '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/' - | '/_authenticate/_inject-org-details/_org-layout/organization/networking/' | '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/' | '/_authenticate/_inject-org-details/_org-layout/organization/settings/' | '/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId' @@ -6703,9 +6665,9 @@ export const routeTree = rootRoute "/_authenticate/_inject-org-details/_org-layout/organization/access-management", "/_authenticate/_inject-org-details/_org-layout/organization/audit-logs", "/_authenticate/_inject-org-details/_org-layout/organization/billing", + "/_authenticate/_inject-org-details/_org-layout/organization/networking", "/_authenticate/_inject-org-details/_org-layout/organization/projects", "/_authenticate/_inject-org-details/_org-layout/organization/app-connections", - "/_authenticate/_inject-org-details/_org-layout/organization/networking", "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing", "/_authenticate/_inject-org-details/_org-layout/organization/settings", "/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId", @@ -6744,6 +6706,10 @@ export const routeTree = rootRoute "filePath": "organization/BillingPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/organization" }, + "/_authenticate/_inject-org-details/_org-layout/organization/networking": { + "filePath": "organization/NetworkingPage/route.tsx", + "parent": "/_authenticate/_inject-org-details/_org-layout/organization" + }, "/_authenticate/_inject-org-details/_org-layout/organization/projects": { "filePath": "organization/ProjectsPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/organization" @@ -6780,13 +6746,6 @@ export const routeTree = rootRoute "/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback" ] }, - "/_authenticate/_inject-org-details/_org-layout/organization/networking": { - "filePath": "", - "parent": "/_authenticate/_inject-org-details/_org-layout/organization", - "children": [ - "/_authenticate/_inject-org-details/_org-layout/organization/networking/" - ] - }, "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing": { "filePath": "", "parent": "/_authenticate/_inject-org-details/_org-layout/organization", @@ -6814,10 +6773,6 @@ export const routeTree = rootRoute "filePath": "organization/AppConnections/AppConnectionsPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/organization/app-connections" }, - "/_authenticate/_inject-org-details/_org-layout/organization/networking/": { - "filePath": "organization/NetworkingPage/route.tsx", - "parent": "/_authenticate/_inject-org-details/_org-layout/organization/networking" - }, "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/": { "filePath": "organization/SecretSharingPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing" diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts index bebc50339..e84e5873d 100644 --- a/frontend/src/routes.ts +++ b/frontend/src/routes.ts @@ -40,7 +40,7 @@ const organizationRoutes = route("/organization", [ "organization/AppConnections/OauthCallbackPage/route.tsx" ) ]), - route("/networking", [index("organization/NetworkingPage/route.tsx")]) + route("/networking", "organization/NetworkingPage/route.tsx") ]); const secretManagerRoutes = route("/projects/secret-management/$projectId", [ From 1e6beb08c945b7176a830039d5244466b25a4a52 Mon Sep 17 00:00:00 2001 From: x032205 Date: Fri, 17 Oct 2025 06:29:48 -0400 Subject: [PATCH 02/10] lint --- .../components/GatewayTab/GatewayTab.tsx | 2 +- .../GatewayTab/components/DeployGatewayModal.tsx | 16 +++++++++------- .../GatewayTab/components/DeployRelayModal.tsx | 11 ++++++----- .../NetworkingTabGroup/NetworkingTabGroup.tsx | 2 +- .../components/RelayTab/RelayTab.tsx | 1 + 5 files changed, 18 insertions(+), 14 deletions(-) diff --git a/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/GatewayTab.tsx b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/GatewayTab.tsx index d5e79d326..a93694a3d 100644 --- a/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/GatewayTab.tsx +++ b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/GatewayTab.tsx @@ -48,8 +48,8 @@ import { usePopUp } from "@app/hooks"; import { gatewaysQueryKeys, useDeleteGatewayById } from "@app/hooks/api/gateways"; import { useDeleteGatewayV2ById } from "@app/hooks/api/gateways-v2"; -import { EditGatewayDetailsModal } from "./components/EditGatewayDetailsModal"; import { DeployGatewayModal } from "./components/DeployGatewayModal"; +import { EditGatewayDetailsModal } from "./components/EditGatewayDetailsModal"; const GatewayHealthStatus = ({ heartbeat }: { heartbeat?: string }) => { const heartbeatDate = heartbeat ? new Date(heartbeat) : null; diff --git a/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/DeployGatewayModal.tsx b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/DeployGatewayModal.tsx index 2b8f17fe7..e5bc048c5 100644 --- a/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/DeployGatewayModal.tsx +++ b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/DeployGatewayModal.tsx @@ -1,3 +1,10 @@ +import { useEffect, useMemo, useState } from "react"; +import { SingleValue } from "react-select"; +import { faCopy, faUpRightFromSquare } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useNavigate } from "@tanstack/react-router"; +import { twMerge } from "tailwind-merge"; + import { createNotification } from "@app/components/notifications"; import { FilterableSelect, @@ -7,6 +14,7 @@ import { Modal, ModalContent } from "@app/components/v2"; +import { ROUTE_PATHS } from "@app/const/routes"; import { useOrganization } from "@app/context"; import { useAddIdentityTokenAuth, @@ -16,14 +24,8 @@ import { } from "@app/hooks/api"; import { useGetRelays } from "@app/hooks/api/relays"; import { slugSchema } from "@app/lib/schemas"; -import { faCopy, faUpRightFromSquare } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { useEffect, useMemo, useState } from "react"; -import { SingleValue } from "react-select"; + import { RelayOption } from "./RelayOption"; -import { useNavigate } from "@tanstack/react-router"; -import { ROUTE_PATHS } from "@app/const/routes"; -import { twMerge } from "tailwind-merge"; type Props = { isOpen: boolean; diff --git a/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/DeployRelayModal.tsx b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/DeployRelayModal.tsx index 2347c0472..399d9851c 100644 --- a/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/DeployRelayModal.tsx +++ b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/DeployRelayModal.tsx @@ -1,3 +1,9 @@ +import { useEffect, useMemo, useState } from "react"; +import { SingleValue } from "react-select"; +import { faCopy, faUpRightFromSquare } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { twMerge } from "tailwind-merge"; + import { createNotification } from "@app/components/notifications"; import { FilterableSelect, @@ -15,11 +21,6 @@ import { useGetIdentityTokenAuth } from "@app/hooks/api"; import { slugSchema } from "@app/lib/schemas"; -import { faCopy, faUpRightFromSquare } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { useEffect, useMemo, useState } from "react"; -import { SingleValue } from "react-select"; -import { twMerge } from "tailwind-merge"; type Props = { isOpen: boolean; diff --git a/frontend/src/pages/organization/NetworkingPage/components/NetworkingTabGroup/NetworkingTabGroup.tsx b/frontend/src/pages/organization/NetworkingPage/components/NetworkingTabGroup/NetworkingTabGroup.tsx index dc90b7f21..07da157ab 100644 --- a/frontend/src/pages/organization/NetworkingPage/components/NetworkingTabGroup/NetworkingTabGroup.tsx +++ b/frontend/src/pages/organization/NetworkingPage/components/NetworkingTabGroup/NetworkingTabGroup.tsx @@ -1,10 +1,10 @@ import { useNavigate, useSearch } from "@tanstack/react-router"; import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; +import { ROUTE_PATHS } from "@app/const/routes"; import { GatewayTab } from "../GatewayTab/GatewayTab"; import { RelayTab } from "../RelayTab/RelayTab"; -import { ROUTE_PATHS } from "@app/const/routes"; export const NetworkingTabGroup = () => { const navigate = useNavigate({ diff --git a/frontend/src/pages/organization/NetworkingPage/components/RelayTab/RelayTab.tsx b/frontend/src/pages/organization/NetworkingPage/components/RelayTab/RelayTab.tsx index 2bfa8b250..e731aab42 100644 --- a/frontend/src/pages/organization/NetworkingPage/components/RelayTab/RelayTab.tsx +++ b/frontend/src/pages/organization/NetworkingPage/components/RelayTab/RelayTab.tsx @@ -43,6 +43,7 @@ import { import { withPermission } from "@app/hoc"; import { usePopUp } from "@app/hooks"; import { useDeleteRelayById, useGetRelays } from "@app/hooks/api/relays"; + import { DeployRelayModal } from "../GatewayTab/components/DeployRelayModal"; const RelayHealthStatus = ({ heartbeat }: { heartbeat?: string }) => { From ab48e098d2c2df5ee65eee9a22615a4064d6ff2d Mon Sep 17 00:00:00 2001 From: x032205 Date: Fri, 17 Oct 2025 06:43:36 -0400 Subject: [PATCH 03/10] remove low opacity from input --- .../components/GatewayTab/components/DeployGatewayModal.tsx | 2 +- .../components/GatewayTab/components/DeployRelayModal.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/DeployGatewayModal.tsx b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/DeployGatewayModal.tsx index e5bc048c5..9a98ce4b1 100644 --- a/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/DeployGatewayModal.tsx +++ b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/DeployGatewayModal.tsx @@ -211,7 +211,7 @@ export const DeployGatewayModal = ({ isOpen, onOpenChange }: Props) => {
- + {
- + Date: Fri, 17 Oct 2025 23:25:48 -0400 Subject: [PATCH 04/10] make "deploy new relay" open the modal --- .../components/DeployGatewayModal.tsx | 2 +- .../components/RelayTab/RelayTab.tsx | 23 ++++++++++++++++++- .../organization/NetworkingPage/route.tsx | 3 ++- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/DeployGatewayModal.tsx b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/DeployGatewayModal.tsx index 9a98ce4b1..010126a41 100644 --- a/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/DeployGatewayModal.tsx +++ b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/DeployGatewayModal.tsx @@ -166,7 +166,7 @@ export const DeployGatewayModal = ({ isOpen, onOpenChange }: Props) => { onChange={(newValue) => { if ((newValue as SingleValue<{ id: string }>)?.id === "_create") { navigate({ - search: { selectedTab: "relays" } + search: (prev) => ({ ...prev, selectedTab: "relays", action: "deploy-relay" }) }); return; } diff --git a/frontend/src/pages/organization/NetworkingPage/components/RelayTab/RelayTab.tsx b/frontend/src/pages/organization/NetworkingPage/components/RelayTab/RelayTab.tsx index e731aab42..8c62bae1c 100644 --- a/frontend/src/pages/organization/NetworkingPage/components/RelayTab/RelayTab.tsx +++ b/frontend/src/pages/organization/NetworkingPage/components/RelayTab/RelayTab.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { faArrowUpRightFromSquare, faBookOpen, @@ -12,6 +12,7 @@ import { faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useNavigate, useSearch } from "@tanstack/react-router"; import { formatRelative } from "date-fns"; import { createNotification } from "@app/components/notifications"; @@ -36,6 +37,7 @@ import { Tooltip, Tr } from "@app/components/v2"; +import { ROUTE_PATHS } from "@app/const/routes"; import { OrgPermissionSubjects, OrgRelayPermissionActions @@ -75,6 +77,25 @@ export const RelayTab = withPermission( "deployRelay" ] as const); + const action = useSearch({ + from: ROUTE_PATHS.Organization.NetworkingPage.id, + select: (s) => s.action + }); + + const navigate = useNavigate({ + from: ROUTE_PATHS.Organization.NetworkingPage.path + }); + + useEffect(() => { + if (action === "deploy-relay") { + handlePopUpOpen("deployRelay"); + navigate({ + search: (prev) => ({ ...prev, action: undefined }), + replace: true + }); + } + }, [action]); + const deleteRelayById = useDeleteRelayById(); const handleDeleteRelay = async () => { diff --git a/frontend/src/pages/organization/NetworkingPage/route.tsx b/frontend/src/pages/organization/NetworkingPage/route.tsx index 433647476..fb81e3725 100644 --- a/frontend/src/pages/organization/NetworkingPage/route.tsx +++ b/frontend/src/pages/organization/NetworkingPage/route.tsx @@ -5,7 +5,8 @@ import { z } from "zod"; import { NetworkingPage } from "./NetworkingPage"; const NetworkingPageQueryParams = z.object({ - selectedTab: z.string().catch("gateways") + selectedTab: z.string().catch("gateways"), + action: z.string().optional() }); export const Route = createFileRoute( From 91bcc22d1b8774e570cda580cb6a356e9ef61ae8 Mon Sep 17 00:00:00 2001 From: x032205 Date: Fri, 17 Oct 2025 23:29:52 -0400 Subject: [PATCH 05/10] lower opacity for command input while its not ready --- .../components/GatewayTab/components/DeployGatewayModal.tsx | 2 +- .../components/GatewayTab/components/DeployRelayModal.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/DeployGatewayModal.tsx b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/DeployGatewayModal.tsx index 010126a41..53e061eb5 100644 --- a/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/DeployGatewayModal.tsx +++ b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/DeployGatewayModal.tsx @@ -211,7 +211,7 @@ export const DeployGatewayModal = ({ isOpen, onOpenChange }: Props) => {
- + {
- + Date: Wed, 22 Oct 2025 02:51:58 -0400 Subject: [PATCH 06/10] rewrite to use a multi-step modal flow --- frontend/src/hooks/api/index.tsx | 1 + .../components/GatewayTab/GatewayTab.tsx | 4 +- .../components/DeployGatewayModal.tsx | 245 --------------- .../components/DeployRelayModal.tsx | 205 ------------ .../components/GatewayCliDeploymentMethod.tsx | 297 ++++++++++++++++++ .../components/GatewayDeployModal.tsx | 45 +++ .../GatewayDeploymentMethodSelect.tsx | 56 ++++ .../components/RelayTab/RelayTab.tsx | 4 +- .../components/RelayCliDeploymentMethod.tsx | 268 ++++++++++++++++ .../RelayTab/components/RelayDeployModal.tsx | 45 +++ .../RelayDeploymentMethodSelect.tsx | 56 ++++ 11 files changed, 772 insertions(+), 454 deletions(-) delete mode 100644 frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/DeployGatewayModal.tsx delete mode 100644 frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/DeployRelayModal.tsx create mode 100644 frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayCliDeploymentMethod.tsx create mode 100644 frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayDeployModal.tsx create mode 100644 frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayDeploymentMethodSelect.tsx create mode 100644 frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayCliDeploymentMethod.tsx create mode 100644 frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayDeployModal.tsx create mode 100644 frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayDeploymentMethodSelect.tsx diff --git a/frontend/src/hooks/api/index.tsx b/frontend/src/hooks/api/index.tsx index 6167f6e07..e9e80feec 100644 --- a/frontend/src/hooks/api/index.tsx +++ b/frontend/src/hooks/api/index.tsx @@ -31,6 +31,7 @@ export * from "./pkiSubscriber"; export * from "./projects"; export * from "./projectUserAdditionalPrivilege"; export * from "./rateLimit"; +export * from "./relays"; export * from "./roles"; export * from "./scim"; export * from "./secretApproval"; diff --git a/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/GatewayTab.tsx b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/GatewayTab.tsx index a93694a3d..a2a24015f 100644 --- a/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/GatewayTab.tsx +++ b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/GatewayTab.tsx @@ -48,8 +48,8 @@ import { usePopUp } from "@app/hooks"; import { gatewaysQueryKeys, useDeleteGatewayById } from "@app/hooks/api/gateways"; import { useDeleteGatewayV2ById } from "@app/hooks/api/gateways-v2"; -import { DeployGatewayModal } from "./components/DeployGatewayModal"; import { EditGatewayDetailsModal } from "./components/EditGatewayDetailsModal"; +import { GatewayDeployModal } from "./components/GatewayDeployModal"; const GatewayHealthStatus = ({ heartbeat }: { heartbeat?: string }) => { const heartbeatDate = heartbeat ? new Date(heartbeat) : null; @@ -269,7 +269,7 @@ export const GatewayTab = withPermission( deleteKey="confirm" onDeleteApproved={() => handleDeleteGateway()} /> - handlePopUpToggle("deployGateway", isOpen)} /> diff --git a/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/DeployGatewayModal.tsx b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/DeployGatewayModal.tsx deleted file mode 100644 index 53e061eb5..000000000 --- a/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/DeployGatewayModal.tsx +++ /dev/null @@ -1,245 +0,0 @@ -import { useEffect, useMemo, useState } from "react"; -import { SingleValue } from "react-select"; -import { faCopy, faUpRightFromSquare } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { useNavigate } from "@tanstack/react-router"; -import { twMerge } from "tailwind-merge"; - -import { createNotification } from "@app/components/notifications"; -import { - FilterableSelect, - FormLabel, - IconButton, - Input, - Modal, - ModalContent -} from "@app/components/v2"; -import { ROUTE_PATHS } from "@app/const/routes"; -import { useOrganization } from "@app/context"; -import { - useAddIdentityTokenAuth, - useCreateTokenIdentityTokenAuth, - useGetIdentityMembershipOrgs, - useGetIdentityTokenAuth -} from "@app/hooks/api"; -import { useGetRelays } from "@app/hooks/api/relays"; -import { slugSchema } from "@app/lib/schemas"; - -import { RelayOption } from "./RelayOption"; - -type Props = { - isOpen: boolean; - onOpenChange: (isOpen: boolean) => void; -}; - -export const DeployGatewayModal = ({ isOpen, onOpenChange }: Props) => { - const { protocol, hostname, port } = window.location; - const portSuffix = port && port !== "80" ? `:${port}` : ""; - const siteURL = `${protocol}//${hostname}${portSuffix}`; - - const navigate = useNavigate({ - from: ROUTE_PATHS.Organization.NetworkingPage.path - }); - - const [name, setName] = useState(""); - const [relay, setRelay] = useState(null); - const [identity, setIdentity] = useState(null); - const [identityToken, setIdentityToken] = useState(""); - - useEffect(() => { - if (!isOpen) { - setName(""); - setRelay(null); - setIdentity(null); - setIdentityToken(""); - } - }, [isOpen]); - - const { data: relays, isPending: isRelaysLoading } = useGetRelays(); - - const { currentOrg } = useOrganization(); - const organizationId = currentOrg?.id || ""; - - const { data: identityMembershipOrgsData, isPending: isIdentitiesLoading } = - useGetIdentityMembershipOrgs({ - organizationId, - limit: 20000 - }); - const identityMembershipOrgs = identityMembershipOrgsData?.identityMemberships || []; - - const { mutateAsync: createToken } = useCreateTokenIdentityTokenAuth(); - const { mutateAsync: addIdentityTokenAuth } = useAddIdentityTokenAuth(); - const { refetch } = useGetIdentityTokenAuth(identity?.id ?? ""); - - useEffect(() => { - const generateToken = async () => { - if (!identity) return; - - try { - const { data: identityTokenAuth } = await refetch(); - if (!identityTokenAuth) { - await addIdentityTokenAuth({ - identityId: identity.id, - organizationId, - accessTokenTTL: 2592000, - accessTokenMaxTTL: 2592000, - accessTokenNumUsesLimit: 0, - accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }] - }); - createNotification({ - text: "Automatically enabled token authentication for this identity.", - type: "info" - }); - } - - const token = await createToken({ - identityId: identity.id, - name: "gateway token (autogenerated)" - }); - setIdentityToken(token.accessToken); - createNotification({ - text: "Automatically generated a token for this identity.", - type: "info" - }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to generate token for identity", - type: "error" - }); - setIdentityToken(""); - } - }; - - generateToken(); - }, [identity, organizationId, refetch, addIdentityTokenAuth, createToken]); - - const handleIdentityChange = (selectedIdentity: SingleValue<{ id: string; name: string }>) => { - setIdentity(selectedIdentity); - setIdentityToken(""); - }; - - const isNameValid = useMemo(() => !name || slugSchema().safeParse(name).success, [name]); - - const isCommandReady = useMemo( - () => !!name && isNameValid && !!relay && !!identityToken, - [name, isNameValid, relay, identityToken] - ); - - const command = useMemo( - () => - `infisical gateway start --name=${name} --relay=${ - relay?.name || "" - } --domain=${siteURL} --token=${identityToken}`, - [name, relay, identityToken, siteURL] - ); - - return ( - - - - setName(e.target.value)} - placeholder="Enter gateway name..." - isError={!isNameValid} - /> - - - { - if ((newValue as SingleValue<{ id: string }>)?.id === "_create") { - navigate({ - search: (prev) => ({ ...prev, selectedTab: "relays", action: "deploy-relay" }) - }); - return; - } - - setRelay(newValue as SingleValue<{ id: string; name: string }>); - }} - isLoading={isRelaysLoading} - options={[ - { - id: "_create", - name: "Deploy New Relay" - }, - ...(relays || []) - ]} - placeholder="Select relay..." - getOptionLabel={(option) => option.name} - getOptionValue={(option) => option.id} - components={{ Option: RelayOption }} - /> - - - - handleIdentityChange( - e as SingleValue<{ - id: string; - name: string; - }> - ) - } - isLoading={isIdentitiesLoading} - placeholder="Select identity..." - options={identityMembershipOrgs.map((membership) => membership.identity)} - getOptionValue={(option) => option.id} - getOptionLabel={(option) => option.name} - /> - - -
- - { - navigator.clipboard.writeText(command); - - createNotification({ - text: "Command copied to clipboard", - type: "info" - }); - }} - className={twMerge("w-10", !isCommandReady && "pointer-events-none opacity-50")} - isDisabled={!isCommandReady} - > - - -
- - Install the Infisical CLI - - -
-
- ); -}; diff --git a/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/DeployRelayModal.tsx b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/DeployRelayModal.tsx deleted file mode 100644 index 61501c40f..000000000 --- a/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/DeployRelayModal.tsx +++ /dev/null @@ -1,205 +0,0 @@ -import { useEffect, useMemo, useState } from "react"; -import { SingleValue } from "react-select"; -import { faCopy, faUpRightFromSquare } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { twMerge } from "tailwind-merge"; - -import { createNotification } from "@app/components/notifications"; -import { - FilterableSelect, - FormLabel, - IconButton, - Input, - Modal, - ModalContent -} from "@app/components/v2"; -import { useOrganization } from "@app/context"; -import { - useAddIdentityTokenAuth, - useCreateTokenIdentityTokenAuth, - useGetIdentityMembershipOrgs, - useGetIdentityTokenAuth -} from "@app/hooks/api"; -import { slugSchema } from "@app/lib/schemas"; - -type Props = { - isOpen: boolean; - onOpenChange: (isOpen: boolean) => void; -}; - -export const DeployRelayModal = ({ isOpen, onOpenChange }: Props) => { - const { protocol, hostname, port } = window.location; - const portSuffix = port && port !== "80" ? `:${port}` : ""; - const siteURL = `${protocol}//${hostname}${portSuffix}`; - - const [name, setName] = useState(""); - const [host, setHost] = useState(""); - const [identity, setIdentity] = useState(null); - const [identityToken, setIdentityToken] = useState(""); - - useEffect(() => { - if (!isOpen) { - setName(""); - setHost(""); - setIdentity(null); - setIdentityToken(""); - } - }, [isOpen]); - - const { currentOrg } = useOrganization(); - const organizationId = currentOrg?.id || ""; - - const { data: identityMembershipOrgsData, isPending: isIdentitiesLoading } = - useGetIdentityMembershipOrgs({ - organizationId, - limit: 20000 - }); - const identityMembershipOrgs = identityMembershipOrgsData?.identityMemberships || []; - - const { mutateAsync: createToken } = useCreateTokenIdentityTokenAuth(); - const { mutateAsync: addIdentityTokenAuth } = useAddIdentityTokenAuth(); - const { refetch } = useGetIdentityTokenAuth(identity?.id ?? ""); - - useEffect(() => { - const generateToken = async () => { - if (!identity) return; - - try { - const { data: identityTokenAuth } = await refetch(); - if (!identityTokenAuth) { - await addIdentityTokenAuth({ - identityId: identity.id, - organizationId, - accessTokenTTL: 2592000, - accessTokenMaxTTL: 2592000, - accessTokenNumUsesLimit: 0, - accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }] - }); - createNotification({ - text: "Automatically enabled token authentication for this identity.", - type: "info" - }); - } - - const token = await createToken({ - identityId: identity.id, - name: "relay token (autogenerated)" - }); - setIdentityToken(token.accessToken); - createNotification({ - text: "Automatically generated a token for this identity.", - type: "info" - }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to generate token for identity", - type: "error" - }); - setIdentityToken(""); - } - }; - - generateToken(); - }, [identity, organizationId, refetch, addIdentityTokenAuth, createToken]); - - const handleIdentityChange = (selectedIdentity: SingleValue<{ id: string; name: string }>) => { - setIdentity(selectedIdentity); - setIdentityToken(""); - }; - - const isNameValid = useMemo(() => !name || slugSchema().safeParse(name).success, [name]); - - const isCommandReady = useMemo( - () => !!name && !!host && isNameValid && !!identityToken, - [name, isNameValid, host, identityToken] - ); - - const command = useMemo( - () => - `infisical relay start --name=${name} --domain=${siteURL} --host=${host} --token=${identityToken}`, - [name, siteURL, host, identityToken] - ); - - return ( - - - - setName(e.target.value)} - placeholder="Enter relay name..." - isError={!isNameValid} - /> - - - setHost(e.target.value)} placeholder="0.0.0.0" /> - - - - handleIdentityChange( - e as SingleValue<{ - id: string; - name: string; - }> - ) - } - isLoading={isIdentitiesLoading} - placeholder="Select identity..." - options={identityMembershipOrgs.map((membership) => membership.identity)} - getOptionValue={(option) => option.id} - getOptionLabel={(option) => option.name} - /> - - -
- - { - navigator.clipboard.writeText(command); - - createNotification({ - text: "Command copied to clipboard", - type: "info" - }); - }} - className={twMerge("w-10", !isCommandReady && "pointer-events-none opacity-50")} - isDisabled={!isCommandReady} - > - - -
- - Install the Infisical CLI - - -
-
- ); -}; diff --git a/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayCliDeploymentMethod.tsx b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayCliDeploymentMethod.tsx new file mode 100644 index 000000000..279dc61c9 --- /dev/null +++ b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayCliDeploymentMethod.tsx @@ -0,0 +1,297 @@ +import { useMemo, useState } from "react"; +import { SingleValue } from "react-select"; +import { faCopy, faUpRightFromSquare } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useNavigate } from "@tanstack/react-router"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { + Button, + FilterableSelect, + FormLabel, + IconButton, + Input, + ModalClose +} from "@app/components/v2"; +import { ROUTE_PATHS } from "@app/const/routes"; +import { useOrganization } from "@app/context"; +import { + useAddIdentityTokenAuth, + useCreateTokenIdentityTokenAuth, + useGetIdentityMembershipOrgs, + useGetIdentityTokenAuth, + useGetRelays +} from "@app/hooks/api"; +import { slugSchema } from "@app/lib/schemas"; + +import { RelayOption } from "./RelayOption"; + +const formSchema = z.object({ + name: slugSchema({ field: "name" }), + instanceDomain: z.string().url("Must be a valid URL").or(z.literal("")), + relay: z + .object( + { + id: z.string(), + name: z.string() + }, + { required_error: "Relay is required" } + ) + .nullable() + .refine((val) => val !== null, { message: "Relay is required" }), + identity: z + .object( + { + id: z.string(), + name: z.string() + }, + { required_error: "Identity is required" } + ) + .nullable() + .refine((val) => val !== null, { message: "Identity is required" }) +}); + +export const GatewayCliDeploymentMethod = () => { + const { protocol, hostname, port } = window.location; + const portSuffix = port && port !== "80" ? `:${port}` : ""; + const siteURL = `${protocol}//${hostname}${portSuffix}`; + + const navigate = useNavigate({ + from: ROUTE_PATHS.Organization.NetworkingPage.path + }); + + const [step, setStep] = useState<"form" | "command">("form"); + const [name, setName] = useState(""); + const [instanceDomain, setInstanceDomain] = useState(siteURL); + const [relay, setRelay] = useState(null); + const [identity, setIdentity] = useState(null); + const [identityToken, setIdentityToken] = useState(""); + const [formErrors, setFormErrors] = useState([]); + + const errors = useMemo(() => { + const errorMap: Record = {}; + formErrors.forEach((issue) => { + if (issue.path.length > 0) { + errorMap[String(issue.path[0])] = issue.message; + } + }); + return errorMap; + }, [formErrors]); + + const { data: relays, isPending: isRelaysLoading } = useGetRelays(); + + const { currentOrg } = useOrganization(); + const organizationId = currentOrg?.id || ""; + + const { data: identityMembershipOrgsData, isPending: isIdentitiesLoading } = + useGetIdentityMembershipOrgs({ + organizationId, + limit: 20000 + }); + const identityMembershipOrgs = identityMembershipOrgsData?.identityMemberships || []; + + const { mutateAsync: createToken, isPending: isCreatingToken } = + useCreateTokenIdentityTokenAuth(); + const { mutateAsync: addIdentityTokenAuth, isPending: isAddingTokenAuth } = + useAddIdentityTokenAuth(); + const { refetch } = useGetIdentityTokenAuth(identity?.id ?? ""); + + const handleGenerateCommand = async () => { + setFormErrors([]); + const validation = formSchema.safeParse({ name, relay, identity, instanceDomain }); + if (!validation.success) { + setFormErrors(validation.error.issues); + return; + } + + const validatedIdentity = validation.data.identity; + + try { + const { data: identityTokenAuth } = await refetch(); + if (!identityTokenAuth) { + await addIdentityTokenAuth({ + identityId: validatedIdentity.id, + organizationId, + accessTokenTTL: 2592000, + accessTokenMaxTTL: 2592000, + accessTokenNumUsesLimit: 0, + accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }] + }); + createNotification({ + text: "Automatically enabled token authentication for this identity.", + type: "info" + }); + } + + const token = await createToken({ + identityId: validatedIdentity.id, + name: "gateway token (autogenerated)" + }); + setIdentityToken(token.accessToken); + createNotification({ + text: "Automatically generated a token for this identity.", + type: "info" + }); + setStep("command"); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to generate token for identity", + type: "error" + }); + setIdentityToken(""); + } + }; + + const command = useMemo(() => { + const domainFlag = instanceDomain ? ` --domain=${instanceDomain}` : ""; + return `infisical gateway start --name=${name} --relay=${ + relay?.name || "" + }${domainFlag} --token=${identityToken}`; + }, [name, relay, identityToken, instanceDomain]); + + if (step === "command") { + return ( + <> + +
+ + { + navigator.clipboard.writeText(command); + createNotification({ + text: "Command copied to clipboard", + type: "info" + }); + }} + className="w-10" + > + + +
+ + Install the Infisical CLI + + +
+ + + +
+ + ); + } + + return ( + <> + + setName(e.target.value)} + placeholder="Enter gateway name..." + isError={Boolean(errors.name)} + /> + {errors.name &&

{errors.name}

} + + + { + if ((newValue as SingleValue<{ id: string }>)?.id === "_create") { + navigate({ + search: (prev) => ({ ...prev, selectedTab: "relays", action: "deploy-relay" }) + }); + return; + } + + setRelay(newValue as SingleValue<{ id: string; name: string }>); + }} + isLoading={isRelaysLoading} + options={[ + { + id: "_create", + name: "Deploy New Relay" + }, + ...(relays || []) + ]} + placeholder="Select relay..." + getOptionLabel={(option) => option.name} + getOptionValue={(option) => option.id} + components={{ Option: RelayOption }} + /> + {errors.relay &&

{errors.relay}

} + + + setInstanceDomain(e.target.value)} + placeholder="https://app.infisical.com" + isError={Boolean(errors.instanceDomain)} + /> + {errors.instanceDomain && ( +

{errors.instanceDomain}

+ )} + + + + setIdentity( + e as SingleValue<{ + id: string; + name: string; + }> + ) + } + isLoading={isIdentitiesLoading} + placeholder="Select identity..." + options={identityMembershipOrgs.map((membership) => membership.identity)} + getOptionValue={(option) => option.id} + getOptionLabel={(option) => option.name} + /> + {errors.identity &&

{errors.identity}

} + +
+ + + + +
+ + ); +}; diff --git a/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayDeployModal.tsx b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayDeployModal.tsx new file mode 100644 index 000000000..170ea2eab --- /dev/null +++ b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayDeployModal.tsx @@ -0,0 +1,45 @@ +import { useState } from "react"; + +import { Modal, ModalContent } from "@app/components/v2"; +import { GatewayDeploymentMethodSelect } from "@app/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayDeploymentMethodSelect"; + +import { GatewayCliDeploymentMethod } from "./GatewayCliDeploymentMethod"; + +type Props = { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; +}; + +export const GatewayDeploymentInfoMap = { + cli: { name: "CLI", image: "SSH.png", component: GatewayCliDeploymentMethod } +} as const; + +export type GatewayDeploymentMethod = keyof typeof GatewayDeploymentInfoMap; + +const Content = () => { + const [selectedMethod, setSelectedMethod] = useState(null); + + if (selectedMethod) { + const ComponentToRender = GatewayDeploymentInfoMap[selectedMethod]?.component; + if (ComponentToRender) { + return ; + } + } + + return ; +}; + +export const GatewayDeployModal = ({ isOpen, onOpenChange }: Props) => { + return ( + + + + + + ); +}; diff --git a/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayDeploymentMethodSelect.tsx b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayDeploymentMethodSelect.tsx new file mode 100644 index 000000000..0a1fd5e12 --- /dev/null +++ b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayDeploymentMethodSelect.tsx @@ -0,0 +1,56 @@ +import { useMemo } from "react"; + +import { + GatewayDeploymentInfoMap, + GatewayDeploymentMethod +} from "@app/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayDeployModal"; + +type Props = { + onSelect: (method: GatewayDeploymentMethod) => void; +}; + +export const GatewayDeploymentMethodSelect = ({ onSelect }: Props) => { + const deploymentOptions = useMemo( + () => + (Object.keys(GatewayDeploymentInfoMap) as GatewayDeploymentMethod[]).map((method) => ({ + method, + name: GatewayDeploymentInfoMap[method].name, + image: GatewayDeploymentInfoMap[method].image + })), + [] + ); + + const handleResourceSelect = (method: GatewayDeploymentMethod) => { + onSelect(method); + }; + + return ( +
+ {deploymentOptions.map((option) => { + const { image, name } = option; + + return ( + + ); + })} +
+ ); +}; diff --git a/frontend/src/pages/organization/NetworkingPage/components/RelayTab/RelayTab.tsx b/frontend/src/pages/organization/NetworkingPage/components/RelayTab/RelayTab.tsx index 8c62bae1c..9ca3e1318 100644 --- a/frontend/src/pages/organization/NetworkingPage/components/RelayTab/RelayTab.tsx +++ b/frontend/src/pages/organization/NetworkingPage/components/RelayTab/RelayTab.tsx @@ -46,7 +46,7 @@ import { withPermission } from "@app/hoc"; import { usePopUp } from "@app/hooks"; import { useDeleteRelayById, useGetRelays } from "@app/hooks/api/relays"; -import { DeployRelayModal } from "../GatewayTab/components/DeployRelayModal"; +import { RelayDeployModal } from "./components/RelayDeployModal"; const RelayHealthStatus = ({ heartbeat }: { heartbeat?: string }) => { const heartbeatDate = heartbeat ? new Date(heartbeat) : null; @@ -258,7 +258,7 @@ export const RelayTab = withPermission( deleteKey="confirm" onDeleteApproved={() => handleDeleteRelay()} /> - handlePopUpToggle("deployRelay", isOpen)} /> diff --git a/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayCliDeploymentMethod.tsx b/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayCliDeploymentMethod.tsx new file mode 100644 index 000000000..543d31bae --- /dev/null +++ b/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayCliDeploymentMethod.tsx @@ -0,0 +1,268 @@ +import { useMemo, useState } from "react"; +import { SingleValue } from "react-select"; +import { faCopy, faUpRightFromSquare } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { + Button, + FilterableSelect, + FormLabel, + IconButton, + Input, + ModalClose +} from "@app/components/v2"; +import { useOrganization } from "@app/context"; +import { + useAddIdentityTokenAuth, + useCreateTokenIdentityTokenAuth, + useGetIdentityMembershipOrgs, + useGetIdentityTokenAuth +} from "@app/hooks/api"; +import { slugSchema } from "@app/lib/schemas"; + +const formSchema = z.object({ + name: slugSchema({ field: "name" }), + host: z.string().min(1, "Host is required"), + + instanceDomain: z.string().url("Must be a valid URL").or(z.literal("")), + identity: z + .object( + { + id: z.string(), + name: z.string() + }, + { required_error: "Identity is required" } + ) + .nullable() + .refine((val) => val !== null, { message: "Identity is required" }) +}); + +export const RelayCliDeploymentMethod = () => { + const { protocol, hostname, port } = window.location; + const portSuffix = port && port !== "80" ? `:${port}` : ""; + const siteURL = `${protocol}//${hostname}${portSuffix}`; + + const [step, setStep] = useState<"form" | "command">("form"); + const [name, setName] = useState(""); + const [host, setHost] = useState(""); + + const [instanceDomain, setInstanceDomain] = useState(siteURL); + const [identity, setIdentity] = useState(null); + const [identityToken, setIdentityToken] = useState(""); + const [formErrors, setFormErrors] = useState([]); + + const errors = useMemo(() => { + const errorMap: Record = {}; + formErrors.forEach((issue) => { + if (issue.path.length > 0) { + errorMap[String(issue.path[0])] = issue.message; + } + }); + return errorMap; + }, [formErrors]); + + const { currentOrg } = useOrganization(); + const organizationId = currentOrg?.id || ""; + + const { data: identityMembershipOrgsData, isPending: isIdentitiesLoading } = + useGetIdentityMembershipOrgs({ + organizationId, + limit: 20000 + }); + const identityMembershipOrgs = identityMembershipOrgsData?.identityMemberships || []; + + const { mutateAsync: createToken, isPending: isCreatingToken } = + useCreateTokenIdentityTokenAuth(); + const { mutateAsync: addIdentityTokenAuth, isPending: isAddingTokenAuth } = + useAddIdentityTokenAuth(); + const { refetch } = useGetIdentityTokenAuth(identity?.id ?? ""); + + const handleGenerateCommand = async () => { + setFormErrors([]); + const validation = formSchema.safeParse({ name, host, instanceDomain, identity }); + if (!validation.success) { + setFormErrors(validation.error.issues); + return; + } + + const validatedIdentity = validation.data.identity; + + try { + const { data: identityTokenAuth } = await refetch(); + if (!identityTokenAuth) { + await addIdentityTokenAuth({ + identityId: validatedIdentity.id, + organizationId, + accessTokenTTL: 2592000, + accessTokenMaxTTL: 2592000, + accessTokenNumUsesLimit: 0, + accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }] + }); + createNotification({ + text: "Automatically enabled token authentication for this identity.", + type: "info" + }); + } + + const token = await createToken({ + identityId: validatedIdentity.id, + name: "relay token (autogenerated)" + }); + setIdentityToken(token.accessToken); + createNotification({ + text: "Automatically generated a token for this identity.", + type: "info" + }); + setStep("command"); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to generate token for identity", + type: "error" + }); + setIdentityToken(""); + } + }; + + const handleIdentityChange = ( + selectedIdentity: SingleValue<{ + id: string; + name: string; + }> + ) => { + setIdentity(selectedIdentity); + }; + + const command = useMemo(() => { + const domainFlag = instanceDomain ? ` --domain=${instanceDomain}` : ""; + return `infisical relay start --name=${name}${domainFlag} --host=${host} --token=${identityToken}`; + }, [name, instanceDomain, host, identityToken]); + + if (step === "command") { + return ( + <> + +
+ + { + navigator.clipboard.writeText(command); + createNotification({ + text: "Command copied to clipboard", + type: "info" + }); + }} + className="w-10" + > + + +
+ + Install the Infisical CLI + + +
+ + + +
+ + ); + } + + return ( + <> + + setName(e.target.value)} + placeholder="Enter relay name..." + isError={Boolean(errors.name)} + /> + {errors.name &&

{errors.name}

} + + + setHost(e.target.value)} + placeholder="0.0.0.0" + isError={Boolean(errors.host)} + /> + {errors.host &&

{errors.host}

} + + + setInstanceDomain(e.target.value)} + placeholder="https://app.infisical.com" + isError={Boolean(errors.instanceDomain)} + /> + {errors.instanceDomain && ( +

{errors.instanceDomain}

+ )} + + + + handleIdentityChange( + e as SingleValue<{ + id: string; + name: string; + }> + ) + } + isLoading={isIdentitiesLoading} + placeholder="Select identity..." + options={identityMembershipOrgs.map((membership) => membership.identity)} + getOptionValue={(option) => option.id} + getOptionLabel={(option) => option.name} + /> + {errors.identity &&

{errors.identity}

} + +
+ + + + +
+ + ); +}; diff --git a/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayDeployModal.tsx b/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayDeployModal.tsx new file mode 100644 index 000000000..496ecb762 --- /dev/null +++ b/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayDeployModal.tsx @@ -0,0 +1,45 @@ +import { useState } from "react"; + +import { Modal, ModalContent } from "@app/components/v2"; +import { RelayDeploymentMethodSelect } from "@app/pages/organization/NetworkingPage/components/RelayTab/components/RelayDeploymentMethodSelect"; + +import { RelayCliDeploymentMethod } from "./RelayCliDeploymentMethod"; + +type Props = { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; +}; + +export const RelayDeploymentInfoMap = { + cli: { name: "CLI", image: "SSH.png", component: RelayCliDeploymentMethod } +} as const; + +export type RelayDeploymentMethod = keyof typeof RelayDeploymentInfoMap; + +const Content = () => { + const [selectedMethod, setSelectedMethod] = useState(null); + + if (selectedMethod) { + const ComponentToRender = RelayDeploymentInfoMap[selectedMethod]?.component; + if (ComponentToRender) { + return ; + } + } + + return ; +}; + +export const RelayDeployModal = ({ isOpen, onOpenChange }: Props) => { + return ( + + + + + + ); +}; diff --git a/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayDeploymentMethodSelect.tsx b/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayDeploymentMethodSelect.tsx new file mode 100644 index 000000000..cb21499e4 --- /dev/null +++ b/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayDeploymentMethodSelect.tsx @@ -0,0 +1,56 @@ +import { useMemo } from "react"; + +import { + RelayDeploymentInfoMap, + RelayDeploymentMethod +} from "@app/pages/organization/NetworkingPage/components/RelayTab/components/RelayDeployModal"; + +type Props = { + onSelect: (method: RelayDeploymentMethod) => void; +}; + +export const RelayDeploymentMethodSelect = ({ onSelect }: Props) => { + const deploymentOptions = useMemo( + () => + (Object.keys(RelayDeploymentInfoMap) as RelayDeploymentMethod[]).map((method) => ({ + method, + name: RelayDeploymentInfoMap[method].name, + image: RelayDeploymentInfoMap[method].image + })), + [] + ); + + const handleResourceSelect = (method: RelayDeploymentMethod) => { + onSelect(method); + }; + + return ( +
+ {deploymentOptions.map((option) => { + const { image, name } = option; + + return ( + + ); + })} +
+ ); +}; From e6a76187a28216825d535f6eaf74845c84e5fc80 Mon Sep 17 00:00:00 2001 From: x032205 Date: Wed, 22 Oct 2025 03:35:33 -0400 Subject: [PATCH 07/10] improve UI & greptile review fixes --- .../components/GatewayCliDeploymentMethod.tsx | 10 +++++----- .../components/GatewayDeploymentMethodSelect.tsx | 10 ++++------ .../NetworkingPage/components/RelayTab/RelayTab.tsx | 2 +- .../RelayTab/components/RelayCliDeploymentMethod.tsx | 10 +++++----- .../components/RelayDeploymentMethodSelect.tsx | 10 ++++------ 5 files changed, 19 insertions(+), 23 deletions(-) diff --git a/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayCliDeploymentMethod.tsx b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayCliDeploymentMethod.tsx index 279dc61c9..a98853bbb 100644 --- a/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayCliDeploymentMethod.tsx +++ b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayCliDeploymentMethod.tsx @@ -125,25 +125,25 @@ export const GatewayCliDeploymentMethod = () => { accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }] }); createNotification({ - text: "Automatically enabled token authentication for this identity.", - type: "info" + text: "Token authentication has been automatically enabled for the selected identity. By default, it is configured to allow all IP addresses with a default token TTL of 30 days. You can manage these settings in Access Control.", + type: "warning" }); } const token = await createToken({ identityId: validatedIdentity.id, - name: "gateway token (autogenerated)" + name: `gateway token for ${name} (autogenerated)` }); setIdentityToken(token.accessToken); createNotification({ - text: "Automatically generated a token for this identity.", + text: "Automatically generated a token for the selected identity.", type: "info" }); setStep("command"); } catch (err) { console.error(err); createNotification({ - text: "Failed to generate token for identity", + text: "Failed to generate token for the selected identity", type: "error" }); setIdentityToken(""); diff --git a/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayDeploymentMethodSelect.tsx b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayDeploymentMethodSelect.tsx index 0a1fd5e12..099c7d791 100644 --- a/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayDeploymentMethodSelect.tsx +++ b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayDeploymentMethodSelect.tsx @@ -27,21 +27,19 @@ export const GatewayDeploymentMethodSelect = ({ onSelect }: Props) => { return (
{deploymentOptions.map((option) => { - const { image, name } = option; + const { image, name, method } = option; return ( +

+ {errors.identity &&

{errors.identity}

} + + ) : ( + <> + + setIdentityToken(e.target.value)} + placeholder="Enter identity token..." + isError={Boolean(errors.identityToken)} + /> + {canCreateToken && ( + + )} + {errors.identityToken &&

{errors.identityToken}

} + + )}
-

{errors.identity &&

{errors.identity}

} ) : ( @@ -303,18 +295,42 @@ export const RelayCliDeploymentMethod = () => { placeholder="Enter identity token..." isError={Boolean(errors.identityToken)} /> - {canCreateToken && ( - - )} {errors.identityToken &&

{errors.identityToken}

} )} + {canCreateToken && ( +
+ { + setAutogenerateToken(Boolean(e)); + }} + id="autogenerate-token" + className="mr-2" + > +
+ Automatically enable token auth and generate a token for identity + + Token authentication will be automatically enabled for the selected identity if + it isn't already configured. By default, it will be configured to allow all IP + addresses with a token TTL of 30 days. You can manage these settings in Access + Control. +
+
A token will automatically be generated to be used with the CLI command. + + } + > + +
+
+
+
+ )} +