mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-21 01:16:49 +00:00
Merge branch 'heads/main' into daniel/redis-secret-rotation-new
This commit is contained in:
+8
-1
@@ -42,7 +42,14 @@ const schema = z.object({
|
||||
message: "Access Token Max TTL cannot be greater than 315360000"
|
||||
}),
|
||||
accessTokenNumUsesLimit: z.string(),
|
||||
oidcDiscoveryUrl: z.string().url().min(1),
|
||||
oidcDiscoveryUrl: z
|
||||
.string()
|
||||
.url()
|
||||
.min(1)
|
||||
.refine(
|
||||
(el) => !el.endsWith("/.well-known/openid-configuration"),
|
||||
"Please remove /.well-known/openid-configuration."
|
||||
),
|
||||
caCert: z.string().trim().default(""),
|
||||
boundIssuer: z.string().min(1),
|
||||
boundAudiences: z.string().optional().default(""),
|
||||
|
||||
+8
-2
@@ -143,7 +143,7 @@ const CreateForm = ({ app, onComplete, projectId }: CreateFormProps) => {
|
||||
case AppConnection.OnePass:
|
||||
return <OnePassConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.Heroku:
|
||||
return <HerokuConnectionForm onSubmit={onSubmit} />;
|
||||
return <HerokuConnectionForm onSubmit={onSubmit} projectId={projectId} />;
|
||||
case AppConnection.Render:
|
||||
return <RenderConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.Flyio:
|
||||
@@ -288,7 +288,13 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
|
||||
case AppConnection.OnePass:
|
||||
return <OnePassConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
case AppConnection.Heroku:
|
||||
return <HerokuConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
return (
|
||||
<HerokuConnectionForm
|
||||
onSubmit={onSubmit}
|
||||
appConnection={appConnection}
|
||||
projectId={appConnection.projectId}
|
||||
/>
|
||||
);
|
||||
case AppConnection.Render:
|
||||
return <RenderConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
case AppConnection.Flyio:
|
||||
|
||||
+2
-2
@@ -39,7 +39,7 @@ import {
|
||||
} from "@app/hooks/api/appConnections";
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
|
||||
import { GithubFormData } from "../../../OauthCallbackPage/OauthCallbackPage.types";
|
||||
import { GitHubFormData } from "../../../OauthCallbackPage/OauthCallbackPage.types";
|
||||
import {
|
||||
genericAppConnectionFieldsSchema,
|
||||
GenericAppConnectionsFields
|
||||
@@ -118,7 +118,7 @@ export const GitHubConnectionForm = ({ appConnection, projectId }: Props) => {
|
||||
connectionId: appConnection?.id,
|
||||
projectId,
|
||||
returnUrl
|
||||
} as GithubFormData)
|
||||
} as GitHubFormData)
|
||||
);
|
||||
|
||||
const githubHost =
|
||||
|
||||
+2
-2
@@ -19,7 +19,7 @@ import {
|
||||
} from "@app/hooks/api/appConnections";
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
|
||||
import { GithubRadarFormData } from "../../../OauthCallbackPage/OauthCallbackPage.types";
|
||||
import { GitHubRadarFormData } from "../../../OauthCallbackPage/OauthCallbackPage.types";
|
||||
import {
|
||||
genericAppConnectionFieldsSchema,
|
||||
GenericAppConnectionsFields
|
||||
@@ -76,7 +76,7 @@ export const GitHubRadarConnectionForm = ({ appConnection, projectId }: Props) =
|
||||
connectionId: appConnection?.id,
|
||||
projectId,
|
||||
returnUrl
|
||||
} as GithubRadarFormData)
|
||||
} as GitHubRadarFormData)
|
||||
);
|
||||
|
||||
switch (formData.method) {
|
||||
|
||||
+11
-3
@@ -15,7 +15,11 @@ import {
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
|
||||
import {
|
||||
APP_CONNECTION_MAP,
|
||||
getAppConnectionMethodDetails,
|
||||
useGetAppConnectionOauthReturnUrl
|
||||
} from "@app/helpers/appConnections";
|
||||
import { isInfisicalCloud } from "@app/helpers/platform";
|
||||
import { useGetAppConnectionOption } from "@app/hooks/api/appConnections";
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
@@ -32,6 +36,7 @@ import {
|
||||
type Props = {
|
||||
appConnection?: THerokuConnection;
|
||||
onSubmit: (formData: FormData) => Promise<void>;
|
||||
projectId: string | undefined | null;
|
||||
};
|
||||
|
||||
const formSchema = z.discriminatedUnion("method", [
|
||||
@@ -53,10 +58,12 @@ const formSchema = z.discriminatedUnion("method", [
|
||||
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
export const HerokuConnectionForm = ({ appConnection, onSubmit: formSubmit }: Props) => {
|
||||
export const HerokuConnectionForm = ({ appConnection, onSubmit: formSubmit, projectId }: Props) => {
|
||||
const isUpdate = Boolean(appConnection);
|
||||
const [isRedirecting, setIsRedirecting] = useState(false);
|
||||
|
||||
const returnUrl = useGetAppConnectionOauthReturnUrl();
|
||||
|
||||
const {
|
||||
option: { oauthClientId },
|
||||
isLoading
|
||||
@@ -110,7 +117,8 @@ export const HerokuConnectionForm = ({ appConnection, onSubmit: formSubmit }: Pr
|
||||
JSON.stringify({
|
||||
...formData,
|
||||
connectionId: appConnection?.id,
|
||||
isUpdate
|
||||
returnUrl,
|
||||
projectId
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
+66
-7
@@ -12,6 +12,7 @@ import {
|
||||
AzureKeyVaultConnectionMethod,
|
||||
GitHubConnectionMethod,
|
||||
GitLabConnectionMethod,
|
||||
HerokuConnectionMethod,
|
||||
TAppConnection,
|
||||
useCreateAppConnection,
|
||||
useUpdateAppConnection
|
||||
@@ -28,7 +29,8 @@ const formDataStorageFieldMap: Partial<Record<AppConnection, string>> = {
|
||||
[AppConnection.AzureKeyVault]: "azureKeyVaultConnectionFormData",
|
||||
[AppConnection.AzureAppConfiguration]: "azureAppConfigurationConnectionFormData",
|
||||
[AppConnection.AzureClientSecrets]: "azureClientSecretsConnectionFormData",
|
||||
[AppConnection.AzureDevOps]: "azureDevOpsConnectionFormData"
|
||||
[AppConnection.AzureDevOps]: "azureDevOpsConnectionFormData",
|
||||
[AppConnection.Heroku]: "herokuConnectionFormData"
|
||||
};
|
||||
|
||||
export const OAuthCallbackPage = () => {
|
||||
@@ -83,7 +85,7 @@ export const OAuthCallbackPage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleGitlab = useCallback(async () => {
|
||||
const handleGitLab = useCallback(async () => {
|
||||
const formData = getFormData(AppConnection.GitLab);
|
||||
if (formData === null) return null;
|
||||
|
||||
@@ -375,7 +377,7 @@ export const OAuthCallbackPage = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleGithub = useCallback(async () => {
|
||||
const handleGitHub = useCallback(async () => {
|
||||
const formData = getFormData(AppConnection.GitHub);
|
||||
if (formData === null) return null;
|
||||
|
||||
@@ -463,7 +465,7 @@ export const OAuthCallbackPage = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleGithubRadar = useCallback(async () => {
|
||||
const handleGitHubRadar = useCallback(async () => {
|
||||
const formData = getFormData(AppConnection.GitHubRadar);
|
||||
if (formData === null) return null;
|
||||
|
||||
@@ -520,6 +522,61 @@ export const OAuthCallbackPage = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleHeroku = useCallback(async () => {
|
||||
const formData = getFormData(AppConnection.Heroku);
|
||||
if (formData === null) return null;
|
||||
|
||||
clearState(AppConnection.Heroku);
|
||||
|
||||
const { connectionId, name, description, returnUrl, projectId } = formData;
|
||||
|
||||
let connection: TAppConnection;
|
||||
|
||||
try {
|
||||
if (connectionId) {
|
||||
connection = await updateAppConnection.mutateAsync({
|
||||
app: AppConnection.Heroku,
|
||||
connectionId,
|
||||
credentials: {
|
||||
code: code as string
|
||||
}
|
||||
});
|
||||
} else {
|
||||
connection = await createAppConnection.mutateAsync({
|
||||
app: AppConnection.Heroku,
|
||||
name,
|
||||
description,
|
||||
method: HerokuConnectionMethod.OAuth,
|
||||
projectId,
|
||||
credentials: {
|
||||
code: code as string
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e: any) {
|
||||
createNotification({
|
||||
title: `Failed to ${connectionId ? "update" : "add"} Heroku Connection`,
|
||||
text: e.message,
|
||||
type: "error"
|
||||
});
|
||||
navigate({
|
||||
to: returnUrl,
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
connectionId,
|
||||
returnUrl,
|
||||
appConnectionName: formData.app,
|
||||
projectId,
|
||||
connection
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Ensure that the localstorage is ready for use, to avoid the form data being malformed
|
||||
useEffect(() => {
|
||||
if (!isReady) {
|
||||
@@ -540,11 +597,11 @@ export const OAuthCallbackPage = () => {
|
||||
} | null = null;
|
||||
|
||||
if (appConnection === AppConnection.GitHub) {
|
||||
data = await handleGithub();
|
||||
data = await handleGitHub();
|
||||
} else if (appConnection === AppConnection.GitHubRadar) {
|
||||
data = await handleGithubRadar();
|
||||
data = await handleGitHubRadar();
|
||||
} else if (appConnection === AppConnection.GitLab) {
|
||||
data = await handleGitlab();
|
||||
data = await handleGitLab();
|
||||
} else if (appConnection === AppConnection.AzureKeyVault) {
|
||||
data = await handleAzureKeyVault();
|
||||
} else if (appConnection === AppConnection.AzureAppConfiguration) {
|
||||
@@ -553,6 +610,8 @@ export const OAuthCallbackPage = () => {
|
||||
data = await handleAzureClientSecrets();
|
||||
} else if (appConnection === AppConnection.AzureDevOps) {
|
||||
data = await handleAzureDevOps();
|
||||
} else if (appConnection === AppConnection.Heroku) {
|
||||
data = await handleHeroku();
|
||||
}
|
||||
|
||||
if (data) {
|
||||
|
||||
+12
-5
@@ -6,7 +6,8 @@ import {
|
||||
TAzureKeyVaultConnection,
|
||||
TGitHubConnection,
|
||||
TGitHubRadarConnection,
|
||||
TGitLabConnection
|
||||
TGitLabConnection,
|
||||
THerokuConnection
|
||||
} from "@app/hooks/api/appConnections";
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
|
||||
@@ -17,10 +18,10 @@ type BaseFormData = {
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export type GithubFormData = BaseFormData &
|
||||
export type GitHubFormData = BaseFormData &
|
||||
Pick<TGitHubConnection, "name" | "method" | "description" | "gatewayId" | "credentials">;
|
||||
|
||||
export type GithubRadarFormData = BaseFormData &
|
||||
export type GitHubRadarFormData = BaseFormData &
|
||||
Pick<TGitHubRadarConnection, "name" | "method" | "description">;
|
||||
|
||||
export type GitLabFormData = BaseFormData &
|
||||
@@ -51,9 +52,12 @@ export type AzureDevOpsFormData = BaseFormData &
|
||||
Pick<TAzureDevOpsConnection, "name" | "method" | "description"> &
|
||||
(Pick<OAuthCredentials, "tenantId" | "orgName"> | Pick<AccessTokenCredentials, "orgName">);
|
||||
|
||||
export type HerokuFormData = BaseFormData &
|
||||
Pick<THerokuConnection, "name" | "method" | "description">;
|
||||
|
||||
export type FormDataMap = {
|
||||
[AppConnection.GitHub]: GithubFormData & { app: AppConnection.GitHub };
|
||||
[AppConnection.GitHubRadar]: GithubRadarFormData & { app: AppConnection.GitHubRadar };
|
||||
[AppConnection.GitHub]: GitHubFormData & { app: AppConnection.GitHub };
|
||||
[AppConnection.GitHubRadar]: GitHubRadarFormData & { app: AppConnection.GitHubRadar };
|
||||
[AppConnection.GitLab]: GitLabFormData & { app: AppConnection.GitLab };
|
||||
[AppConnection.AzureKeyVault]: AzureKeyVaultFormData & { app: AppConnection.AzureKeyVault };
|
||||
[AppConnection.AzureAppConfiguration]: AzureAppConfigurationFormData & {
|
||||
@@ -65,4 +69,7 @@ export type FormDataMap = {
|
||||
[AppConnection.AzureDevOps]: AzureDevOpsFormData & {
|
||||
app: AppConnection.AzureDevOps;
|
||||
};
|
||||
[AppConnection.Heroku]: HerokuFormData & {
|
||||
app: AppConnection.Heroku;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,267 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import {
|
||||
faArrowUpRightFromSquare,
|
||||
faBookOpen,
|
||||
faCopy,
|
||||
faDoorClosed,
|
||||
faEdit,
|
||||
faEllipsisV,
|
||||
faInfoCircle,
|
||||
faMagnifyingGlass,
|
||||
faSearch,
|
||||
faTrash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { formatRelative } from "date-fns";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
DeleteActionModal,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
PageHeader,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
OrgGatewayPermissionActions,
|
||||
OrgPermissionAppConnectionActions,
|
||||
OrgPermissionSubjects
|
||||
} from "@app/context/OrgPermissionContext/types";
|
||||
import { withPermission } from "@app/hoc";
|
||||
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";
|
||||
|
||||
export const GatewayListPage = withPermission(
|
||||
() => {
|
||||
const [search, setSearch] = useState("");
|
||||
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
|
||||
"deleteGateway",
|
||||
"editDetails"
|
||||
] as const);
|
||||
|
||||
const deleteGatewayById = useDeleteGatewayById();
|
||||
const deleteGatewayV2ById = useDeleteGatewayV2ById();
|
||||
|
||||
const handleDeleteGateway = async () => {
|
||||
const data = popUp.deleteGateway.data as { id: string; isV1: boolean };
|
||||
if (data.isV1) {
|
||||
await deleteGatewayById.mutateAsync(data.id);
|
||||
} else {
|
||||
await deleteGatewayV2ById.mutateAsync(data.id);
|
||||
}
|
||||
|
||||
handlePopUpToggle("deleteGateway");
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully deleted gateway"
|
||||
});
|
||||
};
|
||||
|
||||
const filteredGateway = gateways?.filter((el) =>
|
||||
el.name.toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="bg-bunker-800">
|
||||
<Helmet>
|
||||
<title>Infisical | Gateways</title>
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
</Helmet>
|
||||
<div className="flex w-full justify-center bg-bunker-800 text-white">
|
||||
<div className="w-full max-w-7xl">
|
||||
<PageHeader
|
||||
className="w-full"
|
||||
title={
|
||||
<div className="flex w-full items-center">
|
||||
<span>Gateways</span>
|
||||
<a
|
||||
className="-mt-1.5"
|
||||
href="https://infisical.com/docs/documentation/platform/gateways/overview"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="ml-2 inline-block rounded-md bg-yellow/20 px-1.5 text-sm font-normal text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
<span>Docs</span>
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.07rem] ml-1.5 text-[10px]"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
description="Create and configure gateway to access private network resources from Infisical"
|
||||
/>
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search gateway..."
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
<TableContainer className="mt-4">
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="w-1/3">Name</Th>
|
||||
<Th>Identity</Th>
|
||||
<Th>
|
||||
Health Check
|
||||
<Tooltip
|
||||
asChild={false}
|
||||
className="normal-case"
|
||||
content="The last known healthcheck. Triggers every 1 hour."
|
||||
>
|
||||
<FontAwesomeIcon icon={faInfoCircle} className="ml-2" />
|
||||
</Tooltip>
|
||||
</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isGatewaysLoading && (
|
||||
<TableSkeleton innerKey="gateway-table" columns={4} key="gateway-table" />
|
||||
)}
|
||||
{filteredGateway?.map((el) => (
|
||||
<Tr key={el.id}>
|
||||
<Td>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{el.name}</span>
|
||||
<span className="rounded bg-mineshaft-700 px-1.5 py-0.5 text-xs text-mineshaft-400">
|
||||
Gateway v{el.isV1 ? "1" : "2"}
|
||||
</span>
|
||||
</div>
|
||||
</Td>
|
||||
<Td>{el.identity.name}</Td>
|
||||
<Td>
|
||||
{el.heartbeat
|
||||
? formatRelative(new Date(el.heartbeat), new Date())
|
||||
: "-"}
|
||||
</Td>
|
||||
<Td className="w-5">
|
||||
<Tooltip className="max-w-sm text-center" content="Options">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Options"
|
||||
colorSchema="secondary"
|
||||
className="w-6"
|
||||
variant="plain"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon icon={faCopy} />}
|
||||
onClick={() => navigator.clipboard.writeText(el.id)}
|
||||
>
|
||||
Copy ID
|
||||
</DropdownMenuItem>
|
||||
{el.isV1 && (
|
||||
<OrgPermissionCan
|
||||
I={OrgGatewayPermissionActions.EditGateways}
|
||||
a={OrgPermissionSubjects.Gateway}
|
||||
>
|
||||
{(isAllowed: boolean) => (
|
||||
<DropdownMenuItem
|
||||
isDisabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faEdit} />}
|
||||
onClick={() => handlePopUpOpen("editDetails", el)}
|
||||
>
|
||||
Edit Details
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
)}
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionAppConnectionActions.Delete}
|
||||
a={OrgPermissionSubjects.AppConnections}
|
||||
>
|
||||
{(isAllowed: boolean) => (
|
||||
<DropdownMenuItem
|
||||
isDisabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
className="text-red"
|
||||
onClick={() => handlePopUpOpen("deleteGateway", el)}
|
||||
>
|
||||
Delete Gateway
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Tooltip>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
<Modal
|
||||
isOpen={popUp.editDetails.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("editDetails", isOpen)}
|
||||
>
|
||||
<ModalContent title="Edit Gateway">
|
||||
<EditGatewayDetailsModal
|
||||
gatewayDetails={popUp.editDetails.data}
|
||||
onClose={() => handlePopUpToggle("editDetails")}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
{!isGatewaysLoading && !filteredGateway?.length && (
|
||||
<EmptyState
|
||||
title={
|
||||
gateways?.length
|
||||
? "No Gateways match search..."
|
||||
: "No Gateways have been configured"
|
||||
}
|
||||
icon={gateways?.length ? faSearch : faDoorClosed}
|
||||
/>
|
||||
)}
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteGateway.isOpen}
|
||||
title={`Are you sure you want to delete gateway ${
|
||||
(popUp?.deleteGateway?.data as { name: string })?.name || ""
|
||||
}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteGateway", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={() => handleDeleteGateway()}
|
||||
/>
|
||||
</TableContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
{ action: OrgGatewayPermissionActions.ListGateways, subject: OrgPermissionSubjects.Gateway }
|
||||
);
|
||||
@@ -1,16 +0,0 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { GatewayListPage } from "./GatewayListPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/gateways/"
|
||||
)({
|
||||
component: GatewayListPage,
|
||||
context: () => ({
|
||||
breadcrumbs: [
|
||||
{
|
||||
label: "Gateways"
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
@@ -1,214 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import {
|
||||
faArrowUpRightFromSquare,
|
||||
faBookOpen,
|
||||
faCopy,
|
||||
faDoorClosed,
|
||||
faEllipsisV,
|
||||
faMagnifyingGlass,
|
||||
faSearch,
|
||||
faTrash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { formatRelative } from "date-fns";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
DeleteActionModal,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Input,
|
||||
PageHeader,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
OrgPermissionSubjects,
|
||||
OrgRelayPermissionActions
|
||||
} from "@app/context/OrgPermissionContext/types";
|
||||
import { withPermission } from "@app/hoc";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useDeleteRelayById, useGetRelays } from "@app/hooks/api/relays";
|
||||
|
||||
export const RelayListPage = withPermission(
|
||||
() => {
|
||||
const [search, setSearch] = useState("");
|
||||
const { data: relays, isPending: isRelaysLoading } = useGetRelays();
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["deleteRelay"] as const);
|
||||
|
||||
const deleteRelayById = useDeleteRelayById();
|
||||
|
||||
const handleDeleteRelay = async () => {
|
||||
const data = popUp.deleteRelay.data as { id: string };
|
||||
await deleteRelayById.mutateAsync(data.id);
|
||||
|
||||
handlePopUpToggle("deleteRelay");
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully deleted relay"
|
||||
});
|
||||
};
|
||||
|
||||
const filteredRelays = relays?.filter((el) =>
|
||||
el.name.toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="bg-bunker-800">
|
||||
<Helmet>
|
||||
<title>Infisical | Relays</title>
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
</Helmet>
|
||||
<div className="flex w-full justify-center bg-bunker-800 text-white">
|
||||
<div className="w-full max-w-7xl">
|
||||
<PageHeader
|
||||
className="w-full"
|
||||
title={
|
||||
<div className="flex w-full items-center">
|
||||
<span>Relays</span>
|
||||
<a
|
||||
className="-mt-1.5"
|
||||
href="https://infisical.com/docs/documentation/platform/gateways/relay-deployment"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="ml-2 inline-block rounded-md bg-yellow/20 px-1.5 text-sm font-normal text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
<span>Docs</span>
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.07rem] ml-1.5 text-[10px]"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
description="Create and configure relays to securely access private network resources from Infisical"
|
||||
/>
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search relay..."
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
<TableContainer className="mt-4">
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="w-1/3">Name</Th>
|
||||
<Th>Host</Th>
|
||||
<Th>Created</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isRelaysLoading && (
|
||||
<TableSkeleton innerKey="relay-table" columns={4} key="relay-table" />
|
||||
)}
|
||||
{filteredRelays?.map((el) => (
|
||||
<Tr key={el.id}>
|
||||
<Td>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{el.name}</span>
|
||||
{!el.orgId && (
|
||||
<Tooltip content="This is a managed relay provided by Infisical">
|
||||
<span className="rounded bg-mineshaft-700 px-1.5 py-0.5 text-xs text-mineshaft-400">
|
||||
Managed
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</Td>
|
||||
<Td>{el.host}</Td>
|
||||
<Td>{formatRelative(new Date(el.createdAt), new Date())}</Td>
|
||||
<Td className="w-5">
|
||||
<Tooltip className="max-w-sm text-center" content="Options">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Options"
|
||||
colorSchema="secondary"
|
||||
className="w-6"
|
||||
variant="plain"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon icon={faCopy} />}
|
||||
onClick={() => navigator.clipboard.writeText(el.id)}
|
||||
>
|
||||
Copy ID
|
||||
</DropdownMenuItem>
|
||||
<OrgPermissionCan
|
||||
I={OrgRelayPermissionActions.DeleteRelays}
|
||||
a={OrgPermissionSubjects.Relay}
|
||||
>
|
||||
{(isAllowed: boolean) => (
|
||||
<DropdownMenuItem
|
||||
isDisabled={!isAllowed || !el.orgId}
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
className="text-red"
|
||||
onClick={() => handlePopUpOpen("deleteRelay", el)}
|
||||
>
|
||||
Delete Relay
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Tooltip>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
{!isRelaysLoading && !filteredRelays?.length && (
|
||||
<EmptyState
|
||||
title={
|
||||
relays?.length
|
||||
? "No Relays match search..."
|
||||
: "No Relays have been configured"
|
||||
}
|
||||
icon={relays?.length ? faSearch : faDoorClosed}
|
||||
/>
|
||||
)}
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteRelay.isOpen}
|
||||
title={`Are you sure you want to delete relay ${
|
||||
(popUp?.deleteRelay?.data as { name: string })?.name || ""
|
||||
}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteRelay", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={() => handleDeleteRelay()}
|
||||
/>
|
||||
</TableContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
{ action: OrgRelayPermissionActions.ListRelays, subject: OrgPermissionSubjects.Relay }
|
||||
);
|
||||
@@ -1,16 +0,0 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { RelayListPage } from "./RelayListPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/relays/"
|
||||
)({
|
||||
component: RelayListPage,
|
||||
context: () => ({
|
||||
breadcrumbs: [
|
||||
{
|
||||
label: "Relays"
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
|
||||
import { PageHeader } from "@app/components/v2";
|
||||
|
||||
import { NetworkingTabGroup } from "./components/NetworkingTabGroup/NetworkingTabGroup";
|
||||
|
||||
export const NetworkingPage = () => {
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>Infisical | Networking</title>
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
</Helmet>
|
||||
<div className="flex w-full justify-center bg-bunker-800 text-white">
|
||||
<div className="w-full max-w-7xl">
|
||||
<PageHeader
|
||||
title="Networking"
|
||||
description="Manage gateways and relays to securely access private network resources from Infisical"
|
||||
/>
|
||||
<NetworkingTabGroup />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,266 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
faArrowUpRightFromSquare,
|
||||
faBookOpen,
|
||||
faCopy,
|
||||
faDoorClosed,
|
||||
faEdit,
|
||||
faEllipsisV,
|
||||
faInfoCircle,
|
||||
faMagnifyingGlass,
|
||||
faSearch,
|
||||
faTrash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
DeleteActionModal,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
OrgGatewayPermissionActions,
|
||||
OrgPermissionSubjects
|
||||
} from "@app/context/OrgPermissionContext/types";
|
||||
import { withPermission } from "@app/hoc";
|
||||
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";
|
||||
|
||||
const GatewayHealthStatus = ({ heartbeat }: { heartbeat?: string }) => {
|
||||
const heartbeatDate = heartbeat ? new Date(heartbeat) : null;
|
||||
const now = new Date();
|
||||
const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000);
|
||||
|
||||
const isHealthy = heartbeatDate && heartbeatDate >= oneHourAgo;
|
||||
const tooltipContent = heartbeatDate
|
||||
? `Last heartbeat: ${heartbeatDate.toLocaleString()}`
|
||||
: "No heartbeat data available";
|
||||
|
||||
return (
|
||||
<Tooltip content={tooltipContent}>
|
||||
<span className={`cursor-default ${isHealthy ? "text-green-400" : "text-red-400"}`}>
|
||||
{isHealthy ? "Healthy" : "Unreachable"}
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export const GatewayTab = withPermission(
|
||||
() => {
|
||||
const [search, setSearch] = useState("");
|
||||
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
|
||||
"deleteGateway",
|
||||
"editDetails"
|
||||
] as const);
|
||||
|
||||
const deleteGatewayById = useDeleteGatewayById();
|
||||
const deleteGatewayV2ById = useDeleteGatewayV2ById();
|
||||
|
||||
const handleDeleteGateway = async () => {
|
||||
const data = popUp.deleteGateway.data as { id: string; isV1: boolean };
|
||||
if (data.isV1) {
|
||||
await deleteGatewayById.mutateAsync(data.id);
|
||||
} else {
|
||||
await deleteGatewayV2ById.mutateAsync(data.id);
|
||||
}
|
||||
|
||||
handlePopUpToggle("deleteGateway");
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully deleted gateway"
|
||||
});
|
||||
};
|
||||
|
||||
const filteredGateway = gateways?.filter((el) =>
|
||||
el.name.toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
|
||||
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">
|
||||
<h3 className="text-lg font-semibold text-mineshaft-100">Gateways</h3>
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/platform/gateways/overview"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="inline-block rounded-md bg-yellow/20 px-1.5 py-0.5 text-sm font-normal text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
<span>Docs</span>
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.07rem] ml-1.5 text-[10px]"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mb-4 text-sm text-mineshaft-400">
|
||||
Create and configure gateway to access private network resources from Infisical
|
||||
</p>
|
||||
<div>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search gateway..."
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
<TableContainer className="mt-4">
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="w-1/2">Name</Th>
|
||||
<Th>
|
||||
Health Check
|
||||
<Tooltip
|
||||
asChild={false}
|
||||
className="normal-case"
|
||||
content="The last known healthcheck. Triggers every 1 hour."
|
||||
>
|
||||
<FontAwesomeIcon icon={faInfoCircle} className="ml-2" />
|
||||
</Tooltip>
|
||||
</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isGatewaysLoading && (
|
||||
<TableSkeleton innerKey="gateway-table" columns={4} key="gateway-table" />
|
||||
)}
|
||||
{filteredGateway?.map((el) => (
|
||||
<Tr key={el.id}>
|
||||
<Td>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{el.name}</span>
|
||||
<span className="rounded bg-mineshaft-700 px-1.5 py-0.5 text-xs text-mineshaft-400">
|
||||
Gateway v{el.isV1 ? "1" : "2"}
|
||||
</span>
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
<GatewayHealthStatus heartbeat={el.heartbeat} />
|
||||
</Td>
|
||||
<Td className="w-5">
|
||||
<Tooltip className="max-w-sm text-center" content="Options">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Options"
|
||||
colorSchema="secondary"
|
||||
className="w-6"
|
||||
variant="plain"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon icon={faCopy} />}
|
||||
onClick={() => navigator.clipboard.writeText(el.id)}
|
||||
>
|
||||
Copy ID
|
||||
</DropdownMenuItem>
|
||||
{el.isV1 && (
|
||||
<OrgPermissionCan
|
||||
I={OrgGatewayPermissionActions.EditGateways}
|
||||
a={OrgPermissionSubjects.Gateway}
|
||||
>
|
||||
{(isAllowed: boolean) => (
|
||||
<DropdownMenuItem
|
||||
isDisabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faEdit} />}
|
||||
onClick={() => handlePopUpOpen("editDetails", el)}
|
||||
>
|
||||
Edit Details
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
)}
|
||||
<OrgPermissionCan
|
||||
I={OrgGatewayPermissionActions.DeleteGateways}
|
||||
a={OrgPermissionSubjects.Gateway}
|
||||
>
|
||||
{(isAllowed: boolean) => (
|
||||
<DropdownMenuItem
|
||||
isDisabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
className="text-red"
|
||||
onClick={() => handlePopUpOpen("deleteGateway", el)}
|
||||
>
|
||||
Delete Gateway
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Tooltip>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
<Modal
|
||||
isOpen={popUp.editDetails.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("editDetails", isOpen)}
|
||||
>
|
||||
<ModalContent title="Edit Gateway">
|
||||
<EditGatewayDetailsModal
|
||||
gatewayDetails={popUp.editDetails.data}
|
||||
onClose={() => handlePopUpToggle("editDetails")}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
{!isGatewaysLoading && !filteredGateway?.length && (
|
||||
<EmptyState
|
||||
title={
|
||||
gateways?.length
|
||||
? "No Gateways match search..."
|
||||
: "No Gateways have been configured"
|
||||
}
|
||||
icon={gateways?.length ? faSearch : faDoorClosed}
|
||||
/>
|
||||
)}
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteGateway.isOpen}
|
||||
title={`Are you sure you want to delete gateway ${
|
||||
(popUp?.deleteGateway?.data as { name: string })?.name || ""
|
||||
}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteGateway", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={() => handleDeleteGateway()}
|
||||
/>
|
||||
</TableContainer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
{ action: OrgGatewayPermissionActions.ListGateways, subject: OrgPermissionSubjects.Gateway }
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
export { GatewayTab } from "./GatewayTab";
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { useState } from "react";
|
||||
import { useSearch } from "@tanstack/react-router";
|
||||
|
||||
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||
|
||||
import { GatewayTab } from "../GatewayTab/GatewayTab";
|
||||
import { RelayTab } from "../RelayTab/RelayTab";
|
||||
|
||||
export const NetworkingTabGroup = () => {
|
||||
const search = useSearch({
|
||||
from: "/_authenticate/_inject-org-details/_org-layout/organization/networking/"
|
||||
});
|
||||
|
||||
const tabs = [
|
||||
{ name: "Gateways", key: "gateways", component: GatewayTab },
|
||||
{ name: "Relays", key: "relays", component: RelayTab }
|
||||
];
|
||||
|
||||
const [selectedTab, setSelectedTab] = useState(search.selectedTab || tabs[0].key);
|
||||
|
||||
return (
|
||||
<Tabs value={selectedTab} onValueChange={setSelectedTab}>
|
||||
<TabList>
|
||||
{tabs.map((tab) => (
|
||||
<Tab value={tab.key} key={tab.key}>
|
||||
{tab.name}
|
||||
</Tab>
|
||||
))}
|
||||
</TabList>
|
||||
{tabs.map(({ key, component: Component }) => (
|
||||
<TabPanel value={key} key={`tab-panel-${key}`}>
|
||||
<Component />
|
||||
</TabPanel>
|
||||
))}
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { NetworkingTabGroup } from "./NetworkingTabGroup";
|
||||
@@ -0,0 +1,198 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
faArrowUpRightFromSquare,
|
||||
faBookOpen,
|
||||
faCopy,
|
||||
faDoorClosed,
|
||||
faEllipsisV,
|
||||
faMagnifyingGlass,
|
||||
faSearch,
|
||||
faTrash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { formatRelative } from "date-fns";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
DeleteActionModal,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Input,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
OrgPermissionSubjects,
|
||||
OrgRelayPermissionActions
|
||||
} from "@app/context/OrgPermissionContext/types";
|
||||
import { withPermission } from "@app/hoc";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useDeleteRelayById, useGetRelays } from "@app/hooks/api/relays";
|
||||
|
||||
export const RelayTab = withPermission(
|
||||
() => {
|
||||
const [search, setSearch] = useState("");
|
||||
const { data: relays, isPending: isRelaysLoading } = useGetRelays();
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["deleteRelay"] as const);
|
||||
|
||||
const deleteRelayById = useDeleteRelayById();
|
||||
|
||||
const handleDeleteRelay = async () => {
|
||||
const data = popUp.deleteRelay.data as { id: string };
|
||||
await deleteRelayById.mutateAsync(data.id);
|
||||
|
||||
handlePopUpToggle("deleteRelay");
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully deleted relay"
|
||||
});
|
||||
};
|
||||
|
||||
const filteredRelays = relays?.filter((el) =>
|
||||
el.name.toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
|
||||
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">
|
||||
<h3 className="text-lg font-semibold text-mineshaft-100">Relays</h3>
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/platform/gateways/relay-deployment"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="inline-block rounded-md bg-yellow/20 px-1.5 py-0.5 text-sm font-normal text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
<span>Docs</span>
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.07rem] ml-1.5 text-[10px]"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mb-4 text-sm text-mineshaft-400">
|
||||
Create and configure relays to securely access private network resources from Infisical
|
||||
</p>
|
||||
<div>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search relay..."
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
<TableContainer className="mt-4">
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="w-1/3">Name</Th>
|
||||
<Th>Host</Th>
|
||||
<Th>Created</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isRelaysLoading && (
|
||||
<TableSkeleton innerKey="relay-table" columns={4} key="relay-table" />
|
||||
)}
|
||||
{filteredRelays?.map((el) => (
|
||||
<Tr key={el.id}>
|
||||
<Td>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{el.name}</span>
|
||||
{!el.orgId && (
|
||||
<Tooltip content="This is a managed relay provided by Infisical">
|
||||
<span className="rounded bg-mineshaft-700 px-1.5 py-0.5 text-xs text-mineshaft-400">
|
||||
Managed
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</Td>
|
||||
<Td>{el.host}</Td>
|
||||
<Td>{formatRelative(new Date(el.createdAt), new Date())}</Td>
|
||||
<Td className="w-5">
|
||||
<Tooltip className="max-w-sm text-center" content="Options">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Options"
|
||||
colorSchema="secondary"
|
||||
className="w-6"
|
||||
variant="plain"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon icon={faCopy} />}
|
||||
onClick={() => navigator.clipboard.writeText(el.id)}
|
||||
>
|
||||
Copy ID
|
||||
</DropdownMenuItem>
|
||||
<OrgPermissionCan
|
||||
I={OrgRelayPermissionActions.DeleteRelays}
|
||||
a={OrgPermissionSubjects.Relay}
|
||||
>
|
||||
{(isAllowed: boolean) => (
|
||||
<DropdownMenuItem
|
||||
isDisabled={!isAllowed || !el.orgId}
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
className="text-red"
|
||||
onClick={() => handlePopUpOpen("deleteRelay", el)}
|
||||
>
|
||||
Delete Relay
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Tooltip>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
{!isRelaysLoading && !filteredRelays?.length && (
|
||||
<EmptyState
|
||||
title={
|
||||
relays?.length ? "No Relays match search..." : "No Relays have been configured"
|
||||
}
|
||||
icon={relays?.length ? faSearch : faDoorClosed}
|
||||
/>
|
||||
)}
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteRelay.isOpen}
|
||||
title={`Are you sure you want to delete relay ${
|
||||
(popUp?.deleteRelay?.data as { name: string })?.name || ""
|
||||
}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteRelay", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={() => handleDeleteRelay()}
|
||||
/>
|
||||
</TableContainer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
{ action: OrgRelayPermissionActions.ListRelays, subject: OrgPermissionSubjects.Relay }
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
export { RelayTab } from "./RelayTab";
|
||||
@@ -0,0 +1,3 @@
|
||||
export { GatewayTab } from "./GatewayTab/GatewayTab";
|
||||
export { NetworkingTabGroup } from "./NetworkingTabGroup/NetworkingTabGroup";
|
||||
export { RelayTab } from "./RelayTab/RelayTab";
|
||||
@@ -0,0 +1,26 @@
|
||||
import { createFileRoute, stripSearchParams } from "@tanstack/react-router";
|
||||
import { zodValidator } from "@tanstack/zod-adapter";
|
||||
import { z } from "zod";
|
||||
|
||||
import { NetworkingPage } from "./NetworkingPage";
|
||||
|
||||
const NetworkingPageQueryParams = z.object({
|
||||
selectedTab: z.string().catch("")
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/networking/"
|
||||
)({
|
||||
component: NetworkingPage,
|
||||
validateSearch: zodValidator(NetworkingPageQueryParams),
|
||||
search: {
|
||||
middlewares: [stripSearchParams({ selectedTab: "" })]
|
||||
},
|
||||
context: () => ({
|
||||
breadcrumbs: [
|
||||
{
|
||||
label: "Networking"
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
Reference in New Issue
Block a user