Merge branch 'main' into feature/north-flank-app-connection

This commit is contained in:
Victor Santos
2025-10-24 18:07:23 -03:00
100 changed files with 4611 additions and 660 deletions
@@ -8,22 +8,40 @@ type Props = {
isOpen?: boolean;
onOpenChange?: (isOpen: boolean) => void;
text: string;
isEnterpriseFeature?: boolean;
};
export const UpgradePlanModal = ({ text, isOpen, onOpenChange }: Props): JSX.Element => {
export const UpgradePlanModal = ({
text,
isOpen,
onOpenChange,
isEnterpriseFeature = false
}: Props): JSX.Element => {
const { subscription } = useSubscription();
const { currentOrg } = useOrganization();
const { mutateAsync, isPending } = useGetOrgTrialUrl();
const link =
subscription && subscription.slug !== null
? ("/organization/billing" as const)
: "https://infisical.com/scheduledemo";
const getLink = () => {
// self-hosting
if (!subscription || subscription.slug === null) {
return "https://infisical.com/scheduledemo";
}
// Infisical cloud
if (isEnterpriseFeature) {
return "https://infisical.com/talk-to-us";
}
return "/organization/billing" as const;
};
const link = getLink();
const handleUpgradeBtnClick = async () => {
try {
if (!subscription || !currentOrg) return;
if (!subscription.has_used_trial) {
if (!subscription.has_used_trial && !isEnterpriseFeature) {
// direct user to start pro trial
const url = await mutateAsync({
@@ -40,6 +58,17 @@ export const UpgradePlanModal = ({ text, isOpen, onOpenChange }: Props): JSX.Ele
console.error(err);
}
};
const getUpgradePlanLabel = () => {
if (subscription) {
if (isEnterpriseFeature) {
return "Talk to Us";
}
if (!subscription.has_used_trial) {
return "Start Pro Free Trial";
}
}
return "Upgrade Plan";
};
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
@@ -55,7 +84,7 @@ export const UpgradePlanModal = ({ text, isOpen, onOpenChange }: Props): JSX.Ele
onClick={handleUpgradeBtnClick}
className="mr-4"
>
{subscription && !subscription.has_used_trial ? "Start Pro Free Trial" : "Upgrade Plan"}
{getUpgradePlanLabel()}
</Button>
<Button
colorSchema="secondary"
@@ -0,0 +1,307 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { Link } from "@tanstack/react-router";
import {
BanIcon,
BoxesIcon,
BoxIcon,
CheckIcon,
ChevronsUpDownIcon,
CircleXIcon,
ExternalLinkIcon,
GlobeIcon,
InfoIcon,
RadarIcon,
TriangleAlertIcon
} from "lucide-react";
import { Badge } from "./Badge";
/**
* Badges act as an indicator that can optionally be made interactable.
* You can place text and icons inside a badge.
* Badges are often used for the indication of a status, state or scope.
*/
const meta = {
title: "Generic/Badge",
component: Badge,
parameters: {
layout: "centered"
},
tags: ["autodocs"],
argTypes: {
variant: {
control: "select",
options: ["neutral", "success", "info", "warning", "danger", "project", "org", "sub-org"]
},
isTruncatable: {
table: {
disable: true
}
},
asChild: {
table: {
disable: true
}
},
children: {
table: {
disable: true
}
}
},
args: { children: "Badge", isTruncatable: false }
} satisfies Meta<typeof Badge>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Neutral: Story = {
name: "Variant: Neutral",
args: {
variant: "neutral",
children: (
<>
<BanIcon />
Disabled
</>
)
},
parameters: {
docs: {
description: {
story: "Use this variant when indicating neutral or disabled states."
}
}
}
};
export const Success: Story = {
name: "Variant: Success",
args: {
variant: "success",
children: (
<>
<CheckIcon />
Success
</>
)
},
parameters: {
docs: {
description: {
story: "Use this variant when indicating successful or healthy states."
}
}
}
};
export const Info: Story = {
name: "Variant: Info",
args: {
variant: "info",
children: (
<>
<InfoIcon />
Info
</>
)
},
parameters: {
docs: {
description: {
story:
"Use this variant when indicating informational states or linking to external references."
}
}
}
};
export const Warning: Story = {
name: "Variant: Warning",
args: {
variant: "warning",
children: (
<>
<TriangleAlertIcon />
Warning
</>
)
},
parameters: {
docs: {
description: {
story: "Use this variant when indicating activity or attention warranting states."
}
}
}
};
export const Danger: Story = {
name: "Variant: Danger",
args: {
variant: "danger",
children: (
<>
<CircleXIcon />
Danger
</>
)
},
parameters: {
docs: {
description: {
story: "Use this variant when indicating destructive or error states."
}
}
}
};
export const Organization: Story = {
name: "Variant: Organization",
args: {
variant: "org",
children: (
<>
<GlobeIcon />
Organization
</>
)
},
parameters: {
docs: {
description: {
story: "Use this variant when indicating organization scope or links."
}
}
}
};
export const SubOrganization: Story = {
name: "Variant: Sub-Organization",
args: {
variant: "sub-org",
children: (
<>
<BoxesIcon />
Sub-Organization
</>
)
},
parameters: {
docs: {
description: {
story: "Use this variant when indicating sub-organization scope or links."
}
}
}
};
export const Project: Story = {
name: "Variant: Project",
args: {
variant: "project",
children: (
<>
<BoxIcon />
Project
</>
)
},
parameters: {
docs: {
description: {
story: "Use this variant when indicating project scope or links."
}
}
}
};
export const AsExternalLink: Story = {
name: "Example: As External Link",
args: {
variant: "info",
asChild: true,
children: (
<a target="_blank" rel="noopener noreferrer" href="https://infisical.com/">
Link <ExternalLinkIcon />
</a>
)
},
parameters: {
docs: {
description: {
story: "Use the `asChild` prop with an `a` tag to use a badge as an external link."
}
}
}
};
export const AsRouterLink: Story = {
name: "Example: As Router Link",
args: {
variant: "project",
asChild: true,
children: (
<Link to=".">
<RadarIcon />
Secret Scanning
</Link>
)
},
parameters: {
docs: {
description: {
story: "Use the `asChild` prop with a `Link` component to use a badge as an internal link."
}
}
}
};
export const AsButton: Story = {
name: "Example: As Button",
args: {
variant: "org",
asChild: true,
children: (
<button type="button" onClick={() => console.log("click")}>
<GlobeIcon />
Organization
<ChevronsUpDownIcon />
</button>
)
},
parameters: {
docs: {
description: {
story:
"Use the `asChild` prop with a `button` tag to use a badge as a button. Do not use a styled `Button` component."
}
}
}
};
export const IsTruncatable: Story = {
name: "Example: isTruncatable",
args: {
isTruncatable: true,
children: (
<>
<GlobeIcon />
<span>Infisical Infrastructure</span>
</>
)
},
parameters: {
docs: {
description: {
story:
"Use the `isTruncatable` prop with a `span` tag wrapping the text content to support truncation."
}
}
},
decorators: (Story) => (
<div className="flex w-32">
<Story />
</div>
)
};
@@ -0,0 +1,58 @@
import { forwardRef } from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "cva";
import { cn } from "@app/components/v3/utils";
const badgeVariants = cva(
[
"select-none items-center rounded-sm px-1.5 py-0.5 text-xs",
"gap-x-1 [a&,button&]:cursor-pointer inline-flex",
"[&>svg]:pointer-events-none [&>svg]:shrink-0 [&>svg]:stroke-[2.25] [&>svg]:size-3",
"transition duration-200 ease-in-out"
],
{
variants: {
isTruncatable: {
true: "[&>span,&>p]:truncate min-w-0",
false: "w-fit shrink-0 whitespace-nowrap overflow-hidden"
},
variant: {
neutral: "bg-neutral/30 text-neutral [a&,button&]:hover:bg-neutral/40",
success: "bg-success/30 text-success [a&,button&]:hover:bg-success/40",
info: "bg-info/30 text-info [a&,button&]:hover:bg-info/40",
warning: "bg-warning/30 text-warning [a&,button&]:hover:bg-warning/40",
danger: "bg-danger/30 text-danger [a&,button&]:hover:bg-danger/40",
project: "bg-project/30 text-project [a&,button&]:hover:bg-project/40",
org: "bg-org/30 text-org [a&,button&]:hover:bg-org/40",
"sub-org": "bg-sub-org/30 text-sub-org [a&,button&]:hover:bg-sub-org/40"
}
},
defaultVariants: {
variant: "success"
}
}
);
type TBadgeProps = VariantProps<typeof badgeVariants> &
React.ComponentProps<"span"> & {
asChild?: boolean;
};
const Badge = forwardRef<HTMLSpanElement, TBadgeProps>(
({ className, variant, asChild = false, isTruncatable = false, ...props }, ref): JSX.Element => {
const Comp = asChild ? Slot : "span";
return (
<Comp
ref={ref}
data-slot="badge"
className={cn(badgeVariants({ variant, isTruncatable }), className)}
{...props}
/>
);
}
);
Badge.displayName = "Badge";
export { Badge, badgeVariants, type TBadgeProps };
@@ -0,0 +1 @@
export * from "./Badge";
@@ -0,0 +1 @@
export * from "./Badge";
View File
@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
+5 -1
View File
@@ -62,7 +62,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(
+1
View File
@@ -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";
@@ -66,6 +66,8 @@ export type PlanBillingInfo = {
interval: "month" | "year";
intervalCount: number;
quantity: number;
users: number;
identities: number;
};
export type Invoice = {
+1
View File
@@ -1,5 +1,6 @@
export enum PamResourceType {
Postgres = "postgres",
MySQL = "mysql",
RDP = "rdp",
SSH = "ssh",
Kubernetes = "kubernetes"
+1
View File
@@ -5,6 +5,7 @@ export const PAM_RESOURCE_TYPE_MAP: Record<
{ name: string; image: string; size?: number }
> = {
[PamResourceType.Postgres]: { name: "PostgreSQL", image: "Postgres.png" },
[PamResourceType.MySQL]: { name: "MySQL", image: "MySql.png" },
[PamResourceType.RDP]: { name: "RDP", image: "RDP.png" },
[PamResourceType.SSH]: { name: "SSH", image: "SSH.png" },
[PamResourceType.Kubernetes]: { name: "Kubernetes", image: "Kubernetes.png" }
+4 -2
View File
@@ -1,11 +1,13 @@
import { PamResourceType, PamSessionStatus } from "../enums";
import { TPostgresAccount, TPostgresResource } from "./postgres-resource";
import { TMySQLAccount, TMySQLResource } from "./mysql-resource";
export * from "./postgres-resource";
export * from "./mysql-resource";
export type TPamResource = TPostgresResource;
export type TPamResource = TPostgresResource | TMySQLResource;
export type TPamAccount = TPostgresAccount;
export type TPamAccount = TPostgresAccount | TMySQLAccount;
export type TPamFolder = {
id: string;
@@ -0,0 +1,14 @@
import { PamResourceType } from "../enums";
import { TBaseSqlConnectionDetails, TBaseSqlCredentials } from "./shared/sql-resource";
import { TBasePamAccount } from "./base-account";
import { TBasePamResource } from "./base-resource";
// Resources
export type TMySQLResource = TBasePamResource & { resourceType: PamResourceType.MySQL } & {
connectionDetails: TBaseSqlConnectionDetails;
};
// Accounts
export type TMySQLAccount = TBasePamAccount & {
credentials: TBaseSqlCredentials;
};
+16 -2
View File
@@ -37,12 +37,26 @@
}
@theme {
/*legacy color schema */
/* Fonts */
--font-inter: "Inter", sans-serif;
--max-width-8xl: 88rem; /* 1408px */
/* Colors v2 */
--color-background: #19191c;
--color-foreground: white;
--color-success: #2ecc71;
--color-info: #34c2db;
--color-warning: #f1c40f;
--color-danger: #e74c3c;
--color-org: #30B3FF;
--color-sub-org: #96ff59;
--color-project: #e0ed34;
--color-neutral: #adaeb0;
/*legacy color schema */
--color-org-v1: #30B3FF;
--color-namespace-v1: #96ff59;
--max-width-8xl: 88rem; /* 1408px */
/* Primary */
--color-primary-50: #fffff5;
--color-primary-100: #fcfce8;
@@ -111,6 +111,7 @@ type TCertificateDetails = {
export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }: Props) => {
const [certificateDetails, setCertificateDetails] = useState<TCertificateDetails | null>(null);
const [shouldShowSubjectSection, setShouldShowSubjectSection] = useState(true);
const { currentProject } = useProject();
const inputSerialNumber =
@@ -127,19 +128,9 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
const { mutateAsync: createCertificate } = useCreateCertificateV3();
const selectedProfileId = useMemo(() => {
const form = document.querySelector('select[name="profileId"]') as HTMLSelectElement;
return form?.value || profileId || "";
}, [profileId]);
const selectedProfile = useMemo(
() => profilesData?.certificateProfiles?.find((p) => p.id === selectedProfileId),
[profilesData?.certificateProfiles, selectedProfileId]
);
const { data: templateData } = useGetCertificateTemplateV2ById({
templateId: selectedProfile?.certificateTemplateId || ""
});
const formResolver = useMemo(() => {
return zodResolver(createSchema(shouldShowSubjectSection));
}, [shouldShowSubjectSection]);
const {
control,
@@ -150,7 +141,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
formState,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(createSchema((templateData?.subject?.length || 0) > 0)),
resolver: formResolver,
defaultValues: {
profileId: profileId || "",
subjectAttributes: [],
@@ -169,6 +160,16 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
[profilesData?.certificateProfiles, actualSelectedProfileId]
);
const { data: templateData } = useGetCertificateTemplateV2ById({
templateId: actualSelectedProfile?.certificateTemplateId || ""
});
useEffect(() => {
if (templateData !== undefined) {
setShouldShowSubjectSection((templateData?.subject?.length || 0) > 0);
}
}, [templateData]);
const {
constraints,
filteredKeyUsages,
@@ -186,6 +187,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
const resetAllState = useCallback(() => {
setCertificateDetails(null);
setShouldShowSubjectSection(true);
resetConstraints();
reset();
}, [reset, resetConstraints]);
@@ -1,10 +1,10 @@
import { useEffect } from "react";
import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons";
import { faArrowUpRightFromSquare, faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useQueryClient } from "@tanstack/react-query";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button } from "@app/components/v2";
import { Button, Tooltip } from "@app/components/v2";
import {
OrgPermissionBillingActions,
OrgPermissionSubjects,
@@ -27,6 +27,9 @@ export const PreviewSection = () => {
const { subscription } = useSubscription(true);
const queryClient = useQueryClient();
const { data, isPending } = useGetOrgPlanBillingInfo(currentOrg?.id ?? "");
const totalAmount = data?.amount ? data.amount * (data.users + data.identities) : 0;
const getOrgTrialUrl = useGetOrgTrialUrl();
const createCustomerPortalSession = useCreateCustomerPortalSession();
@@ -190,14 +193,28 @@ export const PreviewSection = () => {
</OrgPermissionCan>
)}
</div>
<div className="mr-4 flex-1 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<p className="mb-2 text-gray-400">Price</p>
<p className="mb-8 text-2xl font-medium text-mineshaft-50">
{subscription.status === "trialing"
? "$0.00 / month"
: `${formatAmount(data.amount)} / ${data.interval}`}
</p>
</div>
{subscription.slug !== "enterprise" ? (
<div className="mr-4 flex-1 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<p className="mb-2 text-gray-400">Price</p>
<p className="mb-8 text-2xl font-medium text-mineshaft-50">
{subscription.status === "trialing" ? (
"$0.00 / month"
) : (
<>
{formatAmount(totalAmount)} / {data.interval}
{(subscription.slug === "pro" || subscription.slug === "pro-annual") && (
<Tooltip
content={`Total price is based on the number of users and machine identities at ${formatAmount(data.amount)} each. You have ${data.users} ${data.users > 1 ? "users" : "user"} and ${data.identities} ${data.identities > 1 ? "machine identities" : "machine identity"}.`}
className="max-w-lg"
>
<FontAwesomeIcon icon={faInfoCircle} className="ml-2" size="xs" />
</Tooltip>
)}
</>
)}
</p>
</div>
) : null}
<div className="flex-1 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<p className="mb-2 text-gray-400">Subscription renews on</p>
<p className="mb-8 text-2xl font-medium text-mineshaft-50">
@@ -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 { GatewayDeployModal } from "./components/GatewayDeployModal";
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()}
/>
<GatewayDeployModal
isOpen={popUp.deployGateway.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("deployGateway", isOpen)}
/>
</TableContainer>
</div>
</div>
@@ -0,0 +1,387 @@
import { useMemo, useState } from "react";
import { SingleValue } from "react-select";
import { faCopy, faQuestionCircle, 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,
Checkbox,
FilterableSelect,
FormLabel,
IconButton,
Input,
ModalClose,
Tooltip
} from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import {
OrgPermissionIdentityActions,
OrgPermissionSubjects,
useOrganization,
useOrgPermission
} from "@app/context";
import {
useAddIdentityTokenAuth,
useCreateTokenIdentityTokenAuth,
useGetIdentityMembershipOrgs,
useGetIdentityTokenAuth,
useGetRelays
} from "@app/hooks/api";
import { slugSchema } from "@app/lib/schemas";
import { RelayOption } from "./RelayOption";
const baseFormSchema = 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" })
});
const formSchemaWithIdentity = baseFormSchema.extend({
identity: z
.object(
{
id: z.string(),
name: z.string()
},
{ required_error: "Identity is required" }
)
.nullable()
.refine((val) => val !== null, { message: "Identity is required" })
});
const formSchemaWithToken = baseFormSchema.extend({
identityToken: z.string().min(1, "Token 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 [autogenerateToken, setAutogenerateToken] = useState(true);
const [step, setStep] = useState<"form" | "command">("form");
const [name, setName] = useState("");
const [instanceDomain, setInstanceDomain] = useState(siteURL);
const [relay, setRelay] = useState<null | {
id: string;
name: string;
}>(null);
const [identity, setIdentity] = useState<null | {
id: string;
name: string;
}>(null);
const [identityToken, setIdentityToken] = useState("");
const [formErrors, setFormErrors] = useState<z.ZodIssue[]>([]);
const errors = useMemo(() => {
const errorMap: Record<string, string | undefined> = {};
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 { permission } = useOrgPermission();
const canCreateToken = permission.can(
OrgPermissionIdentityActions.CreateToken,
OrgPermissionSubjects.Identity
);
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([]);
if (canCreateToken && autogenerateToken) {
const validation = formSchemaWithIdentity.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: "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 for ${name} (autogenerated)`
});
setIdentityToken(token.accessToken);
createNotification({
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 the selected identity",
type: "error"
});
setIdentityToken("");
}
} else {
const validation = formSchemaWithToken.safeParse({
name,
relay,
identityToken,
instanceDomain
});
if (!validation.success) {
setFormErrors(validation.error.issues);
return;
}
setStep("command");
}
};
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 (
<>
<FormLabel label="CLI Command" />
<div className="flex gap-2">
<Input value={command} isDisabled />
<IconButton
ariaLabel="copy"
variant="outline_bg"
colorSchema="secondary"
onClick={() => {
navigator.clipboard.writeText(command);
createNotification({
text: "Command copied to clipboard",
type: "info"
});
}}
className="w-10"
>
<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>
<div className="mt-6 flex items-center">
<ModalClose asChild>
<Button className="mr-4" size="sm" colorSchema="secondary">
Done
</Button>
</ModalClose>
</div>
</>
);
}
return (
<>
<FormLabel label="Name" tooltipText="The name for your gateway." />
<Input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter gateway name..."
isError={Boolean(errors.name)}
/>
{errors.name && <p className="mt-1 text-sm text-red">{errors.name}</p>}
<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: (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 && <p className="mt-1 text-sm text-red">{errors.relay}</p>}
<FormLabel
label="Infisical Instance Host Address"
tooltipText="The host address of the infisical instance that's accessible by the gateway."
className="mt-4"
/>
<Input
value={instanceDomain}
onChange={(e) => setInstanceDomain(e.target.value)}
placeholder="https://app.infisical.com"
isError={Boolean(errors.instanceDomain)}
/>
{errors.instanceDomain && <p className="mt-1 text-sm text-red">{errors.instanceDomain}</p>}
{canCreateToken && autogenerateToken ? (
<>
<FormLabel
label="Identity"
tooltipText="The identity that your gateway will use for authentication."
className="mt-4"
/>
<FilterableSelect
value={identity}
onChange={(e) =>
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 && <p className="mt-1 text-sm text-red">{errors.identity}</p>}
</>
) : (
<>
<FormLabel
label="Identity Token"
tooltipText="The identity token that your relay will use for authentication."
className="mt-4"
/>
<Input
value={identityToken}
onChange={(e) => setIdentityToken(e.target.value)}
placeholder="Enter identity token..."
isError={Boolean(errors.identityToken)}
/>
{errors.identityToken && <p className="mt-1 text-sm text-red">{errors.identityToken}</p>}
</>
)}
{canCreateToken && (
<div className="mt-2">
<Checkbox
isChecked={autogenerateToken}
onCheckedChange={(e) => {
setAutogenerateToken(Boolean(e));
}}
id="autogenerate-token"
className="mr-2"
>
<div className="flex items-center">
<span>Automatically enable token auth and generate a token for identity</span>
<Tooltip
className="max-w-md"
content={
<>
Token authentication will be automatically enabled for the selected identity if
it isn&apos;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.
<br />
<br />A token will automatically be generated to be used with the CLI command.
</>
}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="mt-0.5 ml-1" />
</Tooltip>
</div>
</Checkbox>
</div>
)}
<div className="mt-6 flex items-center">
<Button
className="mr-4"
size="sm"
colorSchema="secondary"
onClick={handleGenerateCommand}
isLoading={isCreatingToken || isAddingTokenAuth}
>
Continue
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</>
);
};
@@ -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 | GatewayDeploymentMethod>(null);
if (selectedMethod) {
const ComponentToRender = GatewayDeploymentInfoMap[selectedMethod]?.component;
if (ComponentToRender) {
return <ComponentToRender />;
}
}
return <GatewayDeploymentMethodSelect onSelect={setSelectedMethod} />;
};
export const GatewayDeployModal = ({ isOpen, onOpenChange }: Props) => {
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent
className="max-w-2xl"
title="Deploy Gateway"
subTitle="Select a deployment method to use for the gateway."
bodyClassName="overflow-visible"
>
<Content />
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,54 @@
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 (
<div className="grid h-fit grid-cols-4 content-start gap-2">
{deploymentOptions.map((option) => {
const { image, name, method } = option;
return (
<button
key={method}
type="button"
onClick={() => handleResourceSelect(method)}
className="group relative flex h-28 cursor-pointer flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-700 p-4 duration-200 hover:bg-mineshaft-600"
>
<div className="relative">
<img
src={`/images/integrations/${image}`}
className="mt-auto w-12"
alt={`${name} logo`}
/>
</div>
<div className="mt-auto max-w-xs text-center text-xs font-medium text-gray-300 duration-200 group-hover:text-gray-200">
{name}
</div>
</button>
);
})}
</div>
);
};
@@ -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>
);
};
@@ -1,15 +1,20 @@
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 { ROUTE_PATHS } from "@app/const/routes";
import { useOrganization } from "@app/context";
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 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 = [
@@ -17,12 +22,16 @@ 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 }
});
};
const { isSubOrganization } = useOrganization();
return (
<Tabs orientation="vertical" value={selectedTab} onValueChange={setSelectedTab}>
<Tabs orientation="vertical" value={selectedTab} onValueChange={handleTabChange}>
<TabList>
{tabs.map((tab) => (
<Tab variant={isSubOrganization ? "namespace" : "org"} value={tab.key} key={tab.key}>
@@ -1,4 +1,4 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import {
faArrowUpRightFromSquare,
faBookOpen,
@@ -7,15 +7,18 @@ import {
faEllipsisV,
faInfoCircle,
faMagnifyingGlass,
faPlus,
faSearch,
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";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Button,
DeleteActionModal,
DropdownMenu,
DropdownMenuContent,
@@ -34,6 +37,7 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import {
OrgPermissionSubjects,
OrgRelayPermissionActions
@@ -42,6 +46,8 @@ import { withPermission } from "@app/hoc";
import { usePopUp } from "@app/hooks";
import { useDeleteRelayById, useGetRelays } from "@app/hooks/api/relays";
import { RelayDeployModal } from "./components/RelayDeployModal";
const RelayHealthStatus = ({ heartbeat }: { heartbeat?: string }) => {
const heartbeatDate = heartbeat ? new Date(heartbeat) : null;
const now = new Date();
@@ -66,7 +72,29 @@ 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 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, handlePopUpOpen, navigate]);
const deleteRelayById = useDeleteRelayById();
@@ -87,8 +115,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 +132,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 +258,10 @@ export const RelayTab = withPermission(
deleteKey="confirm"
onDeleteApproved={() => handleDeleteRelay()}
/>
<RelayDeployModal
isOpen={popUp.deployRelay.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("deployRelay", isOpen)}
/>
</TableContainer>
</div>
</div>
@@ -0,0 +1,352 @@
import { useMemo, useState } from "react";
import { SingleValue } from "react-select";
import { faCopy, faQuestionCircle, 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,
Checkbox,
FilterableSelect,
FormLabel,
IconButton,
Input,
ModalClose,
Tooltip
} from "@app/components/v2";
import {
OrgPermissionIdentityActions,
OrgPermissionSubjects,
useOrganization,
useOrgPermission
} from "@app/context";
import {
useAddIdentityTokenAuth,
useCreateTokenIdentityTokenAuth,
useGetIdentityMembershipOrgs,
useGetIdentityTokenAuth
} from "@app/hooks/api";
import { slugSchema } from "@app/lib/schemas";
const baseFormSchema = 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(""))
});
const formSchemaWithIdentity = baseFormSchema.extend({
identity: z
.object(
{
id: z.string(),
name: z.string()
},
{ required_error: "Identity is required" }
)
.nullable()
.refine((val) => val !== null, { message: "Identity is required" })
});
const formSchemaWithToken = baseFormSchema.extend({
identityToken: z.string().min(1, "Token is required")
});
export const RelayCliDeploymentMethod = () => {
const { protocol, hostname, port } = window.location;
const portSuffix = port && port !== "80" ? `:${port}` : "";
const siteURL = `${protocol}//${hostname}${portSuffix}`;
const [autogenerateToken, setAutogenerateToken] = useState(true);
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 | {
id: string;
name: string;
}>(null);
const [identityToken, setIdentityToken] = useState("");
const [formErrors, setFormErrors] = useState<z.ZodIssue[]>([]);
const errors = useMemo(() => {
const errorMap: Record<string, string | undefined> = {};
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 { permission } = useOrgPermission();
const canCreateToken = permission.can(
OrgPermissionIdentityActions.CreateToken,
OrgPermissionSubjects.Identity
);
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([]);
if (canCreateToken && autogenerateToken) {
const validation = formSchemaWithIdentity.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: "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: `relay token for ${name} (autogenerated)`
});
setIdentityToken(token.accessToken);
createNotification({
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 the selected identity",
type: "error"
});
setIdentityToken("");
}
} else {
const validation = formSchemaWithToken.safeParse({
name,
host,
instanceDomain,
identityToken
});
if (!validation.success) {
setFormErrors(validation.error.issues);
return;
}
setStep("command");
}
};
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 (
<>
<FormLabel label="CLI Command" />
<div className="flex gap-2">
<Input value={command} isDisabled />
<IconButton
ariaLabel="copy"
variant="outline_bg"
colorSchema="secondary"
onClick={() => {
navigator.clipboard.writeText(command);
createNotification({
text: "Command copied to clipboard",
type: "info"
});
}}
className="w-10"
>
<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>
<div className="mt-6 flex items-center">
<ModalClose asChild>
<Button className="mr-4" size="sm" colorSchema="secondary">
Done
</Button>
</ModalClose>
</div>
</>
);
}
return (
<>
<FormLabel label="Name" tooltipText="The name for your relay." />
<Input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter relay name..."
isError={Boolean(errors.name)}
/>
{errors.name && <p className="mt-1 text-sm text-red">{errors.name}</p>}
<FormLabel
label="Host"
tooltipText="The public IP address 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"
isError={Boolean(errors.host)}
/>
{errors.host && <p className="mt-1 text-sm text-red">{errors.host}</p>}
<FormLabel
label="Infisical Instance Host Address"
tooltipText="The host address of the infisical instance that's accessible by the relay."
className="mt-4"
/>
<Input
value={instanceDomain}
onChange={(e) => setInstanceDomain(e.target.value)}
placeholder="https://app.infisical.com"
isError={Boolean(errors.instanceDomain)}
/>
{errors.instanceDomain && <p className="mt-1 text-sm text-red">{errors.instanceDomain}</p>}
{canCreateToken && autogenerateToken ? (
<>
<FormLabel
label="Identity"
tooltipText="The identity that your relay will use for authentication."
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}
/>
{errors.identity && <p className="mt-1 text-sm text-red">{errors.identity}</p>}
</>
) : (
<>
<FormLabel
label="Identity Token"
tooltipText="The identity token that your relay will use for authentication."
className="mt-4"
/>
<Input
value={identityToken}
onChange={(e) => setIdentityToken(e.target.value)}
placeholder="Enter identity token..."
isError={Boolean(errors.identityToken)}
/>
{errors.identityToken && <p className="mt-1 text-sm text-red">{errors.identityToken}</p>}
</>
)}
{canCreateToken && (
<div className="mt-2">
<Checkbox
isChecked={autogenerateToken}
onCheckedChange={(e) => {
setAutogenerateToken(Boolean(e));
}}
id="autogenerate-token"
className="mr-2"
>
<div className="flex items-center">
<span>Automatically enable token auth and generate a token for identity</span>
<Tooltip
className="max-w-md"
content={
<>
Token authentication will be automatically enabled for the selected identity if
it isn&apos;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.
<br />
<br />A token will automatically be generated to be used with the CLI command.
</>
}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="mt-0.5 ml-1" />
</Tooltip>
</div>
</Checkbox>
</div>
)}
<div className="mt-6 flex items-center">
<Button
className="mr-4"
size="sm"
colorSchema="secondary"
onClick={handleGenerateCommand}
isLoading={isCreatingToken || isAddingTokenAuth}
>
Continue
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</>
);
};
@@ -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 | RelayDeploymentMethod>(null);
if (selectedMethod) {
const ComponentToRender = RelayDeploymentInfoMap[selectedMethod]?.component;
if (ComponentToRender) {
return <ComponentToRender />;
}
}
return <RelayDeploymentMethodSelect onSelect={setSelectedMethod} />;
};
export const RelayDeployModal = ({ isOpen, onOpenChange }: Props) => {
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent
className="max-w-2xl"
title="Deploy Relay"
subTitle="Select a deployment method to use for the relay."
bodyClassName="overflow-visible"
>
<Content />
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,54 @@
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 (
<div className="grid h-fit grid-cols-4 content-start gap-2">
{deploymentOptions.map((option) => {
const { image, name, method } = option;
return (
<button
key={method}
type="button"
onClick={() => handleResourceSelect(method)}
className="group relative flex h-28 cursor-pointer flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-700 p-4 duration-200 hover:bg-mineshaft-600"
>
<div className="relative">
<img
src={`/images/integrations/${image}`}
className="mt-auto w-12"
alt={`${name} logo`}
/>
</div>
<div className="mt-auto max-w-xs text-center text-xs font-medium text-gray-300 duration-200 group-hover:text-gray-200">
{name}
</div>
</button>
);
})}
</div>
);
};
@@ -5,16 +5,17 @@ import { z } from "zod";
import { NetworkingPage } from "./NetworkingPage";
const NetworkingPageQueryParams = z.object({
selectedTab: z.string().catch("")
selectedTab: z.string().catch("gateways"),
action: z.string().optional()
});
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: [
@@ -19,12 +19,53 @@ export const PamAccessAccountModal = ({ isOpen, onOpenChange, account }: Props)
const isDurationValid = useMemo(() => duration && ms(duration || "1s") > 0, [duration]);
const cliDuration = useMemo(() => {
if (!duration) return duration;
const unit = duration.replace(/[\d\s.-]/g, "");
const dayOrLargerUnits = [
"d",
"day",
"days",
"w",
"week",
"weeks",
"y",
"yr",
"yrs",
"year",
"years"
];
// ms library does not handle months (M) so we do it separately
if (unit === "M") {
const value = parseInt(duration, 10);
if (!Number.isNaN(value) && value > 0) {
const hours = value * 30 * 24;
return `${hours}h`;
}
} else if (dayOrLargerUnits.includes(unit.toLowerCase())) {
const valueInMs = ms(duration);
const oneHourInMs = 1000 * 60 * 60;
if (typeof valueInMs === "number" && valueInMs > 0) {
const hours = Math.floor(valueInMs / oneHourInMs);
return `${hours}h`;
}
}
return duration;
}, [duration]);
const command = useMemo(
() =>
account && account.resource.resourceType === PamResourceType.Postgres
? `infisical pam db access-account ${account.id} --duration ${duration}`
account &&
(account.resource.resourceType === PamResourceType.Postgres ||
account.resource.resourceType === PamResourceType.MySQL)
? `infisical pam db access-account ${account.id} --duration ${cliDuration}`
: "",
[account, duration]
[account, cliDuration]
);
if (!account) return null;
@@ -48,7 +89,7 @@ export const PamAccessAccountModal = ({ isOpen, onOpenChange, account }: Props)
/>
<FormLabel label="CLI Command" className="mt-4" />
<div className="flex gap-2">
<Input value={command} isDisabled className="opacity-50" />
<Input value={command} isDisabled />
<IconButton
ariaLabel="copy"
variant="outline_bg"
@@ -0,0 +1,79 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { FormProvider, useForm } from "react-hook-form";
import { z } from "zod";
import { Button, ModalClose } from "@app/components/v2";
import { PamResourceType, TMySQLAccount } from "@app/hooks/api/pam";
import { UNCHANGED_PASSWORD_SENTINEL } from "@app/hooks/api/pam/constants";
import { GenericAccountFields, genericAccountFieldsSchema } from "./GenericAccountFields";
import { BaseSqlAccountSchema } from "./shared/sql-account-schemas";
import { SqlAccountFields } from "./shared/SqlAccountFields";
type Props = {
account?: TMySQLAccount;
resourceId?: string;
resourceType?: PamResourceType;
onSubmit: (formData: FormData) => Promise<void>;
};
const formSchema = genericAccountFieldsSchema.extend({
credentials: BaseSqlAccountSchema,
// We don't support rotation for now, just feed a false value to
// make the schema happy
rotationEnabled: z.boolean().default(false)
});
type FormData = z.infer<typeof formSchema>;
export const MySQLAccountForm = ({ account, onSubmit }: Props) => {
const isUpdate = Boolean(account);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: account
? {
...account,
credentials: {
...account.credentials,
password: UNCHANGED_PASSWORD_SENTINEL
}
}
: undefined
});
const {
handleSubmit,
formState: { isSubmitting, isDirty }
} = form;
return (
<FormProvider {...form}>
<form
onSubmit={(e) => {
handleSubmit(onSubmit)(e);
}}
>
<GenericAccountFields />
<SqlAccountFields isUpdate={isUpdate} />
<div className="mt-6 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
colorSchema="secondary"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
{isUpdate ? "Update Account" : "Create Account"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
</FormProvider>
);
};
@@ -8,6 +8,7 @@ import {
import { DiscriminativePick } from "@app/types";
import { PamAccountHeader } from "../PamAccountHeader";
import { MySQLAccountForm } from "./MySQLAccountForm";
import { PostgresAccountForm } from "./PostgresAccountForm";
type FormProps = {
@@ -35,10 +36,7 @@ const CreateForm = ({
const createPamAccount = useCreatePamAccount();
const onSubmit = async (
formData: DiscriminativePick<
TPamAccount,
"name" | "description" | "credentials" | "rotationEnabled" | "rotationIntervalSeconds"
>
formData: DiscriminativePick<TPamAccount, "name" | "description" | "credentials">
) => {
try {
const account = await createPamAccount.mutateAsync({
@@ -72,6 +70,10 @@ const CreateForm = ({
resourceType={resourceType}
/>
);
case PamResourceType.MySQL:
return (
<MySQLAccountForm onSubmit={onSubmit} resourceId={resourceId} resourceType={resourceType} />
);
default:
throw new Error(`Unhandled resource: ${resourceType}`);
}
@@ -81,10 +83,7 @@ const UpdateForm = ({ account, onComplete }: UpdateFormProps) => {
const updatePamAccount = useUpdatePamAccount();
const onSubmit = async (
formData: DiscriminativePick<
TPamAccount,
"name" | "description" | "credentials" | "rotationEnabled" | "rotationIntervalSeconds"
>
formData: DiscriminativePick<TPamAccount, "name" | "description" | "credentials">
) => {
try {
const updatedAccount = await updatePamAccount.mutateAsync({
@@ -110,6 +109,8 @@ const UpdateForm = ({ account, onComplete }: UpdateFormProps) => {
switch (account.resource.resourceType) {
case PamResourceType.Postgres:
return <PostgresAccountForm account={account} onSubmit={onSubmit} />;
case PamResourceType.MySQL:
return <MySQLAccountForm account={account} onSubmit={onSubmit} />;
default:
throw new Error(`Unhandled resource: ${account.resource.resourceType}`);
}
@@ -1,16 +1,21 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect, useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button, ModalClose } from "@app/components/v2";
import { PamResourceType, TPostgresAccount, useGetPamResourceById } from "@app/hooks/api/pam";
import {
PamResourceType,
TPostgresAccount,
TPostgresResource,
useGetPamResourceById
} from "@app/hooks/api/pam";
import { UNCHANGED_PASSWORD_SENTINEL } from "@app/hooks/api/pam/constants";
import { BaseSqlAccountSchema } from "./shared/sql-account-schemas";
import { SqlAccountFields } from "./shared/SqlAccountFields";
import { GenericAccountFields, genericAccountFieldsSchema } from "./GenericAccountFields";
import { RotateAccountFields, rotateAccountFieldsSchema } from "./RotateAccountFields";
import { BaseSqlAccountSchema } from "./shared/sql-account-schemas";
import { SqlAccountFields } from "./shared/SqlAccountFields";
type Props = {
account?: TPostgresAccount;
@@ -56,7 +61,9 @@ export const PostgresAccountForm = ({ account, resourceId, resourceType, onSubmi
if (account) {
setRotationCredentialsConfigured(account.resource.rotationCredentialsConfigured);
} else {
setRotationCredentialsConfigured(!!resource?.rotationAccountCredentials);
setRotationCredentialsConfigured(
!!(resource as TPostgresResource)?.rotationAccountCredentials
);
}
}, [account, resource]);
@@ -0,0 +1,84 @@
import { useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button, ModalClose } from "@app/components/v2";
import { PamResourceType, TMySQLResource } from "@app/hooks/api/pam";
import { BaseSqlResourceSchema } from "./shared/sql-resource-schemas";
import { SqlResourceFields } from "./shared/SqlResourceFields";
import { GenericResourceFields, genericResourceFieldsSchema } from "./GenericResourceFields";
type Props = {
resource?: TMySQLResource;
onSubmit: (formData: FormData) => Promise<void>;
};
const formSchema = genericResourceFieldsSchema.extend({
resourceType: z.literal(PamResourceType.MySQL),
connectionDetails: BaseSqlResourceSchema.extend({
database: z.string().trim().optional().default("")
})
});
type FormData = z.infer<typeof formSchema>;
export const MySQLResourceForm = ({ resource, onSubmit }: Props) => {
const isUpdate = Boolean(resource);
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: resource ?? {
resourceType: PamResourceType.MySQL,
connectionDetails: {
host: "",
port: 3306,
database: "",
sslEnabled: true,
sslRejectUnauthorized: true,
sslCertificate: undefined
}
}
});
const {
handleSubmit,
formState: { isSubmitting, isDirty }
} = form;
return (
<FormProvider {...form}>
<form
onSubmit={(e) => {
setSelectedTabIndex(0);
handleSubmit(onSubmit)(e);
}}
>
<GenericResourceFields />
<SqlResourceFields
selectedTabIndex={selectedTabIndex}
setSelectedTabIndex={setSelectedTabIndex}
/>
<div className="mt-6 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
colorSchema="secondary"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
{isUpdate ? "Update Details" : "Create Resource"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
</FormProvider>
);
};
@@ -9,6 +9,7 @@ import {
import { DiscriminativePick } from "@app/types";
import { PamResourceHeader } from "../PamResourceHeader";
import { MySQLResourceForm } from "./MySQLResourceForm";
import { PostgresResourceForm } from "./PostgresResourceForm";
type FormProps = {
@@ -57,6 +58,8 @@ const CreateForm = ({ resourceType, onComplete, projectId }: CreateFormProps) =>
switch (resourceType) {
case PamResourceType.Postgres:
return <PostgresResourceForm onSubmit={onSubmit} />;
case PamResourceType.MySQL:
return <MySQLResourceForm onSubmit={onSubmit} />;
default:
throw new Error(`Unhandled resource: ${resourceType}`);
}
@@ -92,8 +95,10 @@ const UpdateForm = ({ resource, onComplete }: UpdateFormProps) => {
switch (resource.resourceType) {
case PamResourceType.Postgres:
return <PostgresResourceForm resource={resource} onSubmit={onSubmit} />;
case PamResourceType.MySQL:
return <MySQLResourceForm resource={resource} onSubmit={onSubmit} />;
default:
throw new Error(`Unhandled resource: ${resource.resourceType}`);
throw new Error(`Unhandled resource: ${(resource as any).resourceType}`);
}
};
@@ -110,7 +110,7 @@ export const SqlResourceFields = ({ setSelectedTabIndex, selectedTabIndex }: Pro
errorText={error?.message}
isError={Boolean(error?.message)}
className={sslEnabled ? "" : "opacity-50"}
label="SSL Certificate"
label="Trusted CA SSL Certificate"
isOptional
>
<TextArea className="h-14 resize-none!" {...field} isDisabled={!sslEnabled} />
@@ -1172,7 +1172,9 @@ export const OverviewPage = () => {
handlePopUpClose("misc");
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
isEnterpriseFeature: true
});
}}
isDisabled={userAvailableDynamicSecretEnvs.length === 0}
variant="outline_bg"
@@ -1679,10 +1681,11 @@ export const OverviewPage = () => {
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
isEnterpriseFeature={popUp.upgradePlan.data?.isEnterpriseFeature}
text={
subscription.slug === null
? "You can perform this action under an Enterprise license"
: "You can perform this action if you switch to Infisical's Team plan"
: "You can perform this action if you switch to Infisical's Enterprise plan"
}
/>
)}
@@ -1019,7 +1019,9 @@ export const ActionBar = ({
handlePopUpClose("misc");
return;
}
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
isEnterpriseFeature: true
});
}}
isDisabled={!isAllowed}
variant="outline_bg"
@@ -1274,10 +1276,13 @@ export const ActionBar = ({
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
isEnterpriseFeature={popUp.upgradePlan.data?.isEnterpriseFeature}
text={
subscription.slug === null
? "You can perform this action under an Enterprise license"
: "You can perform this action if you switch to Infisical's Team plan"
: `You can perform this action if you switch to Infisical's ${
popUp.upgradePlan.data.isEnterpriseFeature ? "Enterprise" : "Pro"
} plan`
}
/>
)}
@@ -226,7 +226,7 @@ export const EnvironmentTabs = ({ secretPath }: Props) => {
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can add custom environments if you switch to Infisical's Team plan."
text="You can add custom environments if you switch to Infisical's Pro plan."
/>
<AddEnvironmentModal
isOpen={popUp.createEnvironment.isOpen}
@@ -397,7 +397,7 @@ const Page = () => {
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can add secret rotation if you switch to Infisical's Team plan."
text="You can add secret rotation if you switch to Infisical's Pro plan."
/>
<Modal
isOpen={popUp.secretRotationV2.isOpen}
@@ -123,7 +123,7 @@ export const EnvironmentSection = () => {
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can add custom environments if you switch to Infisical's Team plan."
text="You can add custom environments if you switch to Infisical's Pro plan."
/>
</div>
);
+29 -74
View File
@@ -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'
@@ -62,7 +63,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',
@@ -799,14 +795,6 @@ const organizationSecretSharingPageRouteRoute =
AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRoute,
} as any)
const organizationNetworkingPageRouteRoute =
organizationNetworkingPageRouteImport.update({
id: '/',
path: '/',
getParentRoute: () =>
AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRoute,
} as any)
const organizationAppConnectionsAppConnectionsPageRouteRoute =
organizationAppConnectionsAppConnectionsPageRouteImport.update({
id: '/',
@@ -2476,6 +2464,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'
@@ -2532,13 +2527,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'
@@ -2567,13 +2555,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: '/'
@@ -4001,20 +3982,6 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithCh
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteChildren,
)
interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRouteChildren {
organizationNetworkingPageRouteRoute: typeof organizationNetworkingPageRouteRoute
}
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRouteChildren: AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRouteChildren =
{
organizationNetworkingPageRouteRoute: organizationNetworkingPageRouteRoute,
}
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRouteWithChildren =
AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRoute._addFileChildren(
AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRouteChildren,
)
interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteChildren {
organizationSecretSharingPageRouteRoute: typeof organizationSecretSharingPageRouteRoute
}
@@ -4051,9 +4018,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
@@ -4068,11 +4035,10 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren: Authentica
organizationAccessManagementPageRouteRoute,
organizationAuditLogsPageRouteRoute: organizationAuditLogsPageRouteRoute,
organizationBillingPageRouteRoute: organizationBillingPageRouteRoute,
organizationNetworkingPageRouteRoute: organizationNetworkingPageRouteRoute,
organizationProjectsPageRouteRoute: organizationProjectsPageRouteRoute,
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute:
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren,
AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRoute:
AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRouteWithChildren,
AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRoute:
AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteWithChildren,
AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRoute:
@@ -5055,6 +5021,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
@@ -5063,12 +5030,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
@@ -5292,6 +5257,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
@@ -5301,7 +5267,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
@@ -5527,6 +5492,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
@@ -5535,12 +5501,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
@@ -5776,6 +5740,7 @@ export interface FileRouteTypes {
| '/organization/access-management'
| '/organization/audit-logs'
| '/organization/billing'
| '/organization/networking'
| '/organization/projects'
| '/admin/access-management'
| '/admin/authentication'
@@ -5784,12 +5749,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'
@@ -6012,6 +5975,7 @@ export interface FileRouteTypes {
| '/organization/access-management'
| '/organization/audit-logs'
| '/organization/billing'
| '/organization/networking'
| '/organization/projects'
| '/admin/access-management'
| '/admin/authentication'
@@ -6021,7 +5985,6 @@ export interface FileRouteTypes {
| '/admin/integrations'
| '/secret-manager/$projectId'
| '/organization/app-connections'
| '/organization/networking'
| '/organization/secret-sharing'
| '/organization/settings'
| '/organization/groups/$groupId'
@@ -6245,6 +6208,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'
@@ -6253,12 +6217,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'
@@ -6701,9 +6663,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",
@@ -6742,6 +6704,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"
@@ -6778,13 +6744,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",
@@ -6811,10 +6770,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"
+1 -1
View File
@@ -37,7 +37,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", [