mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-20 22:17:52 +00:00
feat(gateways): gateway and relay deployment CLI command interactive
helper
This commit is contained in:
+18
-2
@@ -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 (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<div className="flex grow items-center gap-2">
|
||||
<h3 className="text-lg font-medium text-mineshaft-100">Gateways</h3>
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/platform/gateways/overview"
|
||||
@@ -118,6 +122,14 @@ export const GatewayTab = withPermission(
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
<div className="flex grow" />
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("deployGateway")}
|
||||
>
|
||||
Deploy Gateway
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mb-4 text-sm text-mineshaft-400">
|
||||
@@ -257,6 +269,10 @@ export const GatewayTab = withPermission(
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={() => handleDeleteGateway()}
|
||||
/>
|
||||
<DeployGatewayModal
|
||||
isOpen={popUp.deployGateway.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("deployGateway", isOpen)}
|
||||
/>
|
||||
</TableContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+243
@@ -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 | {
|
||||
id: string;
|
||||
name: string;
|
||||
}>(null);
|
||||
const [identity, setIdentity] = useState<null | {
|
||||
id: string;
|
||||
name: string;
|
||||
}>(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 (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<ModalContent
|
||||
className="max-w-2xl"
|
||||
title="Deploy Gateway"
|
||||
subTitle="Configure and run the CLI command to deploy your gateway."
|
||||
bodyClassName="overflow-visible"
|
||||
>
|
||||
<FormLabel label="Name" tooltipText="The name for your gateway." />
|
||||
<Input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Enter gateway name..."
|
||||
isError={!isNameValid}
|
||||
/>
|
||||
|
||||
<FormLabel
|
||||
label="Relay"
|
||||
tooltipText="The relay to use with your gateway."
|
||||
className="mt-4"
|
||||
/>
|
||||
<FilterableSelect
|
||||
value={relay}
|
||||
onChange={(newValue) => {
|
||||
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 }}
|
||||
/>
|
||||
|
||||
<FormLabel
|
||||
label="Identity"
|
||||
tooltipText="The identity to use with your gateway."
|
||||
className="mt-4"
|
||||
/>
|
||||
<FilterableSelect
|
||||
value={identity}
|
||||
onChange={(e) =>
|
||||
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}
|
||||
/>
|
||||
|
||||
<FormLabel label="CLI Command" className="mt-4" />
|
||||
<div className="flex gap-2">
|
||||
<Input value={command} isDisabled isError={!isCommandReady} className="opacity-50" />
|
||||
<IconButton
|
||||
ariaLabel="copy"
|
||||
variant="outline_bg"
|
||||
colorSchema="secondary"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(command);
|
||||
|
||||
createNotification({
|
||||
text: "Command copied to clipboard",
|
||||
type: "info"
|
||||
});
|
||||
}}
|
||||
className={twMerge("w-10", !isCommandReady && "pointer-events-none opacity-50")}
|
||||
isDisabled={!isCommandReady}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCopy} />
|
||||
</IconButton>
|
||||
</div>
|
||||
<a
|
||||
href="https://infisical.com/docs/cli/overview"
|
||||
target="_blank"
|
||||
className="mt-2 flex h-4 w-fit items-center gap-2 border-b border-mineshaft-400 text-sm text-mineshaft-400 transition-colors duration-100 hover:border-yellow-400 hover:text-yellow-400"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<span>Install the Infisical CLI</span>
|
||||
<FontAwesomeIcon icon={faUpRightFromSquare} className="size-3" />
|
||||
</a>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
+204
@@ -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 | {
|
||||
id: string;
|
||||
name: string;
|
||||
}>(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 (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<ModalContent
|
||||
className="max-w-2xl"
|
||||
title="Deploy Relay"
|
||||
subTitle="Configure and run the CLI command to deploy your relay."
|
||||
bodyClassName="overflow-visible"
|
||||
>
|
||||
<FormLabel label="Name" tooltipText="The name for your relay." />
|
||||
<Input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Enter relay name..."
|
||||
isError={!isNameValid}
|
||||
/>
|
||||
|
||||
<FormLabel
|
||||
label="Host"
|
||||
tooltipText="The host of the system you're deploying the relay to."
|
||||
className="mt-4"
|
||||
/>
|
||||
<Input value={host} onChange={(e) => setHost(e.target.value)} placeholder="0.0.0.0" />
|
||||
|
||||
<FormLabel
|
||||
label="Identity"
|
||||
tooltipText="The identity to use with your relay."
|
||||
className="mt-4"
|
||||
/>
|
||||
<FilterableSelect
|
||||
value={identity}
|
||||
onChange={(e) =>
|
||||
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}
|
||||
/>
|
||||
|
||||
<FormLabel label="CLI Command" className="mt-4" />
|
||||
<div className="flex gap-2">
|
||||
<Input value={command} isDisabled isError={!isCommandReady} className="opacity-50" />
|
||||
<IconButton
|
||||
ariaLabel="copy"
|
||||
variant="outline_bg"
|
||||
colorSchema="secondary"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(command);
|
||||
|
||||
createNotification({
|
||||
text: "Command copied to clipboard",
|
||||
type: "info"
|
||||
});
|
||||
}}
|
||||
className={twMerge("w-10", !isCommandReady && "pointer-events-none opacity-50")}
|
||||
isDisabled={!isCommandReady}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCopy} />
|
||||
</IconButton>
|
||||
</div>
|
||||
<a
|
||||
href="https://infisical.com/docs/cli/overview"
|
||||
target="_blank"
|
||||
className="mt-2 flex h-4 w-fit items-center gap-2 border-b border-mineshaft-400 text-sm text-mineshaft-400 transition-colors duration-100 hover:border-yellow-400 hover:text-yellow-400"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<span>Install the Infisical CLI</span>
|
||||
<FontAwesomeIcon icon={faUpRightFromSquare} className="size-3" />
|
||||
</a>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
+32
@@ -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 (
|
||||
<components.Option isSelected={isSelected} {...props}>
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
{isCreateOption ? (
|
||||
<div className="flex items-center gap-x-1 text-mineshaft-400">
|
||||
<FontAwesomeIcon icon={faPlus} size="sm" />
|
||||
<span className="mr-auto">Deploy New Relay</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<p className="truncate">{children}</p>
|
||||
{isSelected && (
|
||||
<FontAwesomeIcon className="ml-2 text-primary" icon={faCheckCircle} size="sm" />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</components.Option>
|
||||
);
|
||||
};
|
||||
+15
-6
@@ -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 value={selectedTab} onValueChange={setSelectedTab}>
|
||||
<Tabs value={selectedTab} onValueChange={handleTabChange}>
|
||||
<TabList>
|
||||
{tabs.map((tab) => (
|
||||
<Tab variant="org" value={tab.key} key={tab.key}>
|
||||
|
||||
@@ -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 (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<div className="flex grow items-center gap-2">
|
||||
<h3 className="text-lg font-medium text-mineshaft-100">Relays</h3>
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/platform/gateways/relay-deployment"
|
||||
@@ -104,6 +110,14 @@ export const RelayTab = withPermission(
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
<div className="flex grow" />
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("deployRelay")}
|
||||
>
|
||||
Deploy Relay
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mb-4 text-sm text-mineshaft-400">
|
||||
@@ -222,6 +236,10 @@ export const RelayTab = withPermission(
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={() => handleDeleteRelay()}
|
||||
/>
|
||||
<DeployRelayModal
|
||||
isOpen={popUp.deployRelay.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("deployRelay", isOpen)}
|
||||
/>
|
||||
</TableContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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: [
|
||||
|
||||
Reference in New Issue
Block a user