Merge branch 'main' into feat/showWarningOnImportedSecretDeletion

This commit is contained in:
carlosmonastyrski
2025-04-14 09:04:47 -03:00
423 changed files with 14637 additions and 1130 deletions
@@ -0,0 +1,80 @@
import { useEffect } from "react";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import { SingleValue } from "react-select";
import { faCircleInfo } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
import { FilterableSelect, FormControl, Tooltip } from "@app/components/v2";
import { useCamundaConnectionListClusters } from "@app/hooks/api/appConnections/camunda";
import { TCamundaCluster } from "@app/hooks/api/appConnections/camunda/types";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { CamundaSyncScope } from "@app/hooks/api/secretSyncs/types/camunda-sync";
import { TSecretSyncForm } from "../schemas";
export const CamundaSyncFields = () => {
const { control, setValue } = useFormContext<
TSecretSyncForm & { destination: SecretSync.Camunda }
>();
const connectionId = useWatch({ name: "connection.id", control });
const { data: clusters, isPending } = useCamundaConnectionListClusters(connectionId, {
enabled: Boolean(connectionId)
});
useEffect(() => {
setValue("destinationConfig.scope", CamundaSyncScope.Cluster);
}, []);
return (
<>
<SecretSyncConnectionField
onChange={() => {
setValue("destinationConfig.clusterUUID", "");
}}
/>
<Controller
name="destinationConfig.clusterUUID"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Cluster"
helperText={
<Tooltip
className="max-w-md"
content="Ensure that your credential has been granted access to the cluster"
>
<div>
<span>Don&#39;t see the cluster you&#39;re looking for?</span>{" "}
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
</div>
</Tooltip>
}
>
<FilterableSelect
menuPlacement="top"
isLoading={isPending && Boolean(connectionId)}
isDisabled={!connectionId}
value={clusters?.find((cluster) => cluster.uuid === value) ?? null}
onChange={(option) => {
onChange((option as SingleValue<TCamundaCluster>)?.uuid ?? null);
setValue(
"destinationConfig.clusterName",
(option as SingleValue<TCamundaCluster>)?.name ?? ""
);
}}
options={clusters}
placeholder="Select a cluster..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.uuid}
/>
</FormControl>
)}
/>
</>
);
};
@@ -7,10 +7,13 @@ import { AwsParameterStoreSyncFields } from "./AwsParameterStoreSyncFields";
import { AwsSecretsManagerSyncFields } from "./AwsSecretsManagerSyncFields";
import { AzureAppConfigurationSyncFields } from "./AzureAppConfigurationSyncFields";
import { AzureKeyVaultSyncFields } from "./AzureKeyVaultSyncFields";
import { CamundaSyncFields } from "./CamundaSyncFields";
import { DatabricksSyncFields } from "./DatabricksSyncFields";
import { GcpSyncFields } from "./GcpSyncFields";
import { GitHubSyncFields } from "./GitHubSyncFields";
import { HumanitecSyncFields } from "./HumanitecSyncFields";
import { TerraformCloudSyncFields } from "./TerraformCloudSyncFields";
import { VercelSyncFields } from "./VercelSyncFields";
export const SecretSyncDestinationFields = () => {
const { watch } = useFormContext<TSecretSyncForm>();
@@ -34,6 +37,12 @@ export const SecretSyncDestinationFields = () => {
return <DatabricksSyncFields />;
case SecretSync.Humanitec:
return <HumanitecSyncFields />;
case SecretSync.TerraformCloud:
return <TerraformCloudSyncFields />;
case SecretSync.Camunda:
return <CamundaSyncFields />;
case SecretSync.Vercel:
return <VercelSyncFields />;
default:
throw new Error(`Unhandled Destination Config Field: ${destination}`);
}
@@ -0,0 +1,244 @@
import { Controller, useFormContext, useWatch } from "react-hook-form";
import { SingleValue } from "react-select";
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
import { FilterableSelect, FormControl, Select, SelectItem } from "@app/components/v2";
import {
TERRAFORM_CLOUD_SYNC_SCOPES,
TerraformCloudSyncCategory,
TerraformCloudSyncScope,
TTerraformCloudConnectionOrganization,
TTerraformCloudConnectionVariableSet,
TTerraformCloudConnectionWorkspace,
useTerraformCloudConnectionListOrganizations
} from "@app/hooks/api/appConnections/terraform-cloud";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TSecretSyncForm } from "../schemas";
export const TerraformCloudSyncFields = () => {
const { control, watch, setValue } = useFormContext<
TSecretSyncForm & { destination: SecretSync.TerraformCloud }
>();
const connectionId = useWatch({ name: "connection.id", control });
const currentOrg = watch("destinationConfig.org");
const currentScope = watch("destinationConfig.scope");
const { data: organizations = [], isPending: isOrganizationsPending } =
useTerraformCloudConnectionListOrganizations(connectionId, {
enabled: Boolean(connectionId)
});
const selectedOrg = organizations?.find((org) => org.id === currentOrg);
const variableSets = selectedOrg?.variableSets || [];
const workspaces = selectedOrg?.workspaces || [];
return (
<>
<SecretSyncConnectionField
onChange={() => {
setValue("destinationConfig.org", "");
setValue("destinationConfig.variableSetId", "");
setValue("destinationConfig.workspaceId", "");
setValue("destinationConfig.variableSetName", "");
setValue("destinationConfig.workspaceName", "");
}}
/>
<Controller
name="destinationConfig.org"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Organization"
>
<FilterableSelect
isLoading={isOrganizationsPending && Boolean(connectionId)}
isDisabled={!connectionId}
value={organizations ? (organizations.find((org) => org.id === value) ?? null) : null}
onChange={(option) => {
onChange(
(option as SingleValue<TTerraformCloudConnectionOrganization>)?.id ?? null
);
setValue("destinationConfig.variableSetId", "");
setValue("destinationConfig.workspaceId", "");
setValue("destinationConfig.variableSetName", "");
setValue("destinationConfig.workspaceName", "");
}}
options={organizations}
placeholder="Select an organization..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id.toString()}
/>
</FormControl>
)}
/>
<Controller
name="destinationConfig.category"
control={control}
defaultValue={TerraformCloudSyncCategory.Environment}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Category"
tooltipClassName="max-w-lg py-3"
tooltipText={
<div className="flex flex-col gap-3">
<ul className="flex list-disc flex-col gap-3 pl-4">
<li>
<p className="text-mineshaft-300">
<span className="font-medium text-bunker-200">
Environment variables configure Terraform&apos;s behavior (e.g.,
credentials).
</span>
</p>
</li>
<li>
<p className="text-mineshaft-300">
<span className="font-medium text-bunker-200">
Terraform variables are used as input values in your configuration.
</span>
</p>
</li>
</ul>
</div>
}
>
<Select
value={value}
onValueChange={onChange}
className="w-full border border-mineshaft-500 capitalize"
position="popper"
defaultValue={TerraformCloudSyncCategory.Environment}
placeholder="Select category..."
dropdownContainerClassName="max-w-none"
>
{Object.entries(TerraformCloudSyncCategory).map(([envKey, envValue]) => (
<SelectItem className="capitalize" value={envValue} key={envValue}>
{envKey.replace("-", " ")}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
name="destinationConfig.scope"
control={control}
defaultValue={TerraformCloudSyncScope.VariableSet}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Scope"
tooltipClassName="max-w-lg py-3"
tooltipText={
<div className="flex flex-col gap-3">
<p>
Specify how Infisical should manage secrets from Terraform Cloud. The following
options are available:
</p>
<ul className="flex list-disc flex-col gap-3 pl-4">
{Object.values(TERRAFORM_CLOUD_SYNC_SCOPES).map(({ name, description }) => {
return (
<li key={name}>
<p className="text-mineshaft-300">
<span className="font-medium text-bunker-200">{name}</span>: {description}
</p>
</li>
);
})}
</ul>
</div>
}
>
<Select
value={value}
onValueChange={(val) => {
onChange(val);
setValue("destinationConfig.variableSetId", "");
setValue("destinationConfig.workspaceId", "");
setValue("destinationConfig.variableSetName", "");
setValue("destinationConfig.workspaceName", "");
}}
className="w-full border border-mineshaft-500 capitalize"
position="popper"
placeholder="Select a scope..."
dropdownContainerClassName="max-w-none"
>
{Object.values(TerraformCloudSyncScope).map((scope) => (
<SelectItem className="capitalize" value={scope} key={scope}>
{scope.replace("-", " ")}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
{currentScope === TerraformCloudSyncScope.VariableSet && (
<Controller
name="destinationConfig.variableSetId"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message} label="Variable Set">
<FilterableSelect
menuPlacement="top"
isLoading={isOrganizationsPending && Boolean(connectionId) && Boolean(currentOrg)}
isDisabled={!connectionId || !currentOrg}
value={variableSets.find((variableSet) => variableSet.id === value) ?? null}
onChange={(option) => {
const selectedOption =
option as SingleValue<TTerraformCloudConnectionVariableSet>;
onChange(selectedOption?.id ?? null);
if (selectedOption) {
setValue("destinationConfig.variableSetName", selectedOption.name);
} else {
setValue("destinationConfig.variableSetName", "");
}
}}
options={variableSets}
placeholder="Select a variable set..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id.toString()}
/>
</FormControl>
)}
/>
)}
{currentScope === TerraformCloudSyncScope.Workspace && (
<Controller
name="destinationConfig.workspaceId"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message} label="Workspace">
<FilterableSelect
menuPlacement="top"
isLoading={isOrganizationsPending && Boolean(connectionId) && Boolean(currentOrg)}
isDisabled={!connectionId || !currentOrg}
value={workspaces.find((workspace) => workspace.id === value) ?? null}
onChange={(option) => {
const selectedOption = option as SingleValue<TTerraformCloudConnectionWorkspace>;
onChange(selectedOption?.id ?? null);
if (selectedOption) {
setValue("destinationConfig.workspaceName", selectedOption.name);
} else {
setValue("destinationConfig.workspaceName", "");
}
}}
options={workspaces}
placeholder="Select a workspace..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id.toString()}
/>
</FormControl>
)}
/>
)}
</>
);
};
@@ -0,0 +1,195 @@
import { useMemo } from "react";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import { SingleValue } from "react-select";
import { faCircleInfo } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
import { FilterableSelect, FormControl, Tooltip } from "@app/components/v2";
import {
TVercelConnectionApp,
useVercelConnectionListOrganizations
} from "@app/hooks/api/appConnections/vercel";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TSecretSyncForm } from "../schemas";
const vercelEnvironments = [
{ name: "Development", slug: "development" },
{ name: "Preview", slug: "preview" },
{ name: "Production", slug: "production" }
];
export const VercelSyncFields = () => {
const { control, watch, setValue } = useFormContext<
TSecretSyncForm & { destination: SecretSync.Vercel }
>();
const connectionId = useWatch({ name: "connection.id", control });
const currentApp = watch("destinationConfig.app");
const currentEnv = watch("destinationConfig.env");
const { data: projects, isLoading: isProjectsLoading } = useVercelConnectionListOrganizations(
connectionId,
{
enabled: Boolean(connectionId)
}
);
const selectedProject = projects
?.find((project) => project.apps.some((app) => app.id === currentApp))
?.apps.find((app) => app.id === currentApp);
const allApps =
projects?.flatMap((project) =>
project.apps.map((app) => ({ ...app, project: project.name, projectId: project.id }))
) || [];
const environmentOptions = useMemo(() => {
return vercelEnvironments
.map((env) => ({
key: env.slug,
type: env.slug,
name: env.name
}))
.concat(
selectedProject?.envs?.map((env) => ({
key: env.id,
type: env.type,
name: env.slug
})) || []
);
}, [currentApp]);
const previewBranchOptions =
selectedProject?.previewBranches?.map((branch) => ({
id: branch,
name: branch
})) || [];
const isPreviewEnvironment = currentEnv === "preview";
return (
<>
<SecretSyncConnectionField
onChange={() => {
setValue("destinationConfig.app", "");
setValue("destinationConfig.appName", "");
setValue("destinationConfig.env", "production");
setValue("destinationConfig.branch", "");
}}
/>
<Controller
name="destinationConfig.app"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Vercel App"
helperText={
<Tooltip
className="max-w-md"
content="Ensure the project exists and the API token scope for this connection includes the desired project."
>
<div>
<span>Don&#39;t see the project you&#39;re looking for?</span>{" "}
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
</div>
</Tooltip>
}
>
<FilterableSelect
menuPlacement="top"
isLoading={isProjectsLoading && Boolean(connectionId)}
isDisabled={!connectionId}
value={allApps.find((app) => app.id === value) ?? null}
onChange={(option) => {
const appId = (option as SingleValue<TVercelConnectionApp>)?.id ?? null;
onChange(appId);
setValue("destinationConfig.branch", "");
setValue(
"destinationConfig.teamId",
(option as SingleValue<TVercelConnectionApp>)?.projectId || ""
);
setValue(
"destinationConfig.appName",
(option as SingleValue<TVercelConnectionApp>)?.name || ""
);
}}
options={allApps}
placeholder="Select a project..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id.toString()}
groupBy="project"
/>
</FormControl>
)}
/>
<Controller
name="destinationConfig.env"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Vercel App Environment"
>
<FilterableSelect
menuPlacement="top"
isDisabled={!connectionId || !currentApp}
value={
value
? {
key: environmentOptions.find((env) => env.key === value)?.key,
type: environmentOptions.find((env) => env.key === value)?.type,
name: environmentOptions.find((env) => env.key === value)?.name
}
: null
}
onChange={(option) => {
const envKey = (option as any)?.key ?? null;
onChange(envKey);
setValue("destinationConfig.branch", "");
}}
options={environmentOptions}
placeholder="Select an environment..."
getOptionLabel={(option) => option.name || option.key || ""}
getOptionValue={(option) => option.key || ""}
/>
</FormControl>
)}
/>
{isPreviewEnvironment && (
<Controller
name="destinationConfig.branch"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Vercel Preview Branch (Optional)"
>
<FilterableSelect
menuPlacement="top"
isLoading={isProjectsLoading && Boolean(connectionId) && Boolean(currentApp)}
isDisabled={!connectionId || !currentApp}
value={previewBranchOptions.find((branch) => branch.id === value) ?? null}
onChange={(option) => onChange((option as SingleValue<{ id: string }>)?.id || "")}
options={previewBranchOptions}
placeholder="Select a branch..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option?.id || ""}
isClearable
/>
</FormControl>
)}
/>
)}
</>
);
};
@@ -39,6 +39,9 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
case SecretSync.AzureAppConfiguration:
case SecretSync.Databricks:
case SecretSync.Humanitec:
case SecretSync.TerraformCloud:
case SecretSync.Camunda:
case SecretSync.Vercel:
AdditionalSyncOptionsFieldsComponent = null;
break;
default:
@@ -0,0 +1,20 @@
import { useFormContext } from "react-hook-form";
import { GenericFieldLabel } from "@app/components/secret-syncs";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { SecretSync } from "@app/hooks/api/secretSyncs";
export const CamundaSyncReviewFields = () => {
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Camunda }>();
const scope = watch("destinationConfig.scope");
const clusterName = watch("destinationConfig.clusterName");
const clusterUUID = watch("destinationConfig.clusterUUID");
return (
<>
<GenericFieldLabel label="Secret Scope">{scope}</GenericFieldLabel>
<GenericFieldLabel label="Cluster">
{clusterName} (id:{clusterUUID})
</GenericFieldLabel>
</>
);
};
@@ -17,10 +17,13 @@ import {
} from "./AwsSecretsManagerSyncReviewFields";
import { AzureAppConfigurationSyncReviewFields } from "./AzureAppConfigurationSyncReviewFields";
import { AzureKeyVaultSyncReviewFields } from "./AzureKeyVaultSyncReviewFields";
import { CamundaSyncReviewFields } from "./CamundaSyncReviewFields";
import { DatabricksSyncReviewFields } from "./DatabricksSyncReviewFields";
import { GcpSyncReviewFields } from "./GcpSyncReviewFields";
import { GitHubSyncReviewFields } from "./GitHubSyncReviewFields";
import { HumanitecSyncReviewFields } from "./HumanitecSyncReviewFields";
import { TerraformCloudSyncReviewFields } from "./TerraformCloudSyncReviewFields";
import { VercelSyncReviewFields } from "./VercelSyncReviewFields";
export const SecretSyncReviewFields = () => {
const { watch } = useFormContext<TSecretSyncForm>();
@@ -72,6 +75,15 @@ export const SecretSyncReviewFields = () => {
case SecretSync.Humanitec:
DestinationFieldsComponent = <HumanitecSyncReviewFields />;
break;
case SecretSync.TerraformCloud:
DestinationFieldsComponent = <TerraformCloudSyncReviewFields />;
break;
case SecretSync.Camunda:
DestinationFieldsComponent = <CamundaSyncReviewFields />;
break;
case SecretSync.Vercel:
DestinationFieldsComponent = <VercelSyncReviewFields />;
break;
default:
throw new Error(`Unhandled Destination Review Fields: ${destination}`);
}
@@ -0,0 +1,28 @@
import { useFormContext } from "react-hook-form";
import { GenericFieldLabel } from "@app/components/secret-syncs";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { TerraformCloudSyncScope } from "@app/hooks/api/appConnections/terraform-cloud";
import { SecretSync } from "@app/hooks/api/secretSyncs";
export const TerraformCloudSyncReviewFields = () => {
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.TerraformCloud }>();
const orgId = watch("destinationConfig.org");
const variableSetName = watch("destinationConfig.variableSetName");
const workspaceName = watch("destinationConfig.workspaceName");
const scope = watch("destinationConfig.scope");
const category = watch("destinationConfig.category");
return (
<>
<GenericFieldLabel label="Organization">{orgId}</GenericFieldLabel>
{scope === TerraformCloudSyncScope.VariableSet && (
<GenericFieldLabel label="Variable Set">{variableSetName}</GenericFieldLabel>
)}
{scope === TerraformCloudSyncScope.Workspace && (
<GenericFieldLabel label="Workspace">{workspaceName}</GenericFieldLabel>
)}
<GenericFieldLabel label="Category">{category}</GenericFieldLabel>
</>
);
};
@@ -0,0 +1,23 @@
import { useFormContext } from "react-hook-form";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { GenericFieldLabel } from "@app/components/v2";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { VercelEnvironmentType } from "@app/hooks/api/secretSyncs/types/vercel-sync";
export const VercelSyncReviewFields = () => {
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Vercel }>();
const envId = watch("destinationConfig.env");
const branchId = watch("destinationConfig.branch");
const appName = watch("destinationConfig.appName");
return (
<>
<GenericFieldLabel label="Vercel App">{appName}</GenericFieldLabel>
<GenericFieldLabel label="Environment">{envId}</GenericFieldLabel>
{envId === VercelEnvironmentType.Preview && branchId && (
<GenericFieldLabel label="Preview Branch">{branchId}</GenericFieldLabel>
)}
</>
);
};
@@ -0,0 +1,15 @@
import { z } from "zod";
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
import { SecretSync } from "@app/hooks/api/secretSyncs";
export const CamundaSyncDestinationSchema = BaseSecretSyncSchema().merge(
z.object({
destination: z.literal(SecretSync.Camunda),
destinationConfig: z.object({
scope: z.string().trim().min(1, "Camunda scope required"),
clusterUUID: z.string().trim().min(1, "Camunda cluster UUID required"),
clusterName: z.string().optional()
})
})
);
@@ -7,8 +7,11 @@ import { GitHubSyncDestinationSchema } from "@app/components/secret-syncs/forms/
import { AwsParameterStoreSyncDestinationSchema } from "./aws-parameter-store-sync-destination-schema";
import { AzureAppConfigurationSyncDestinationSchema } from "./azure-app-configuration-sync-destination-schema";
import { AzureKeyVaultSyncDestinationSchema } from "./azure-key-vault-sync-destination-schema";
import { CamundaSyncDestinationSchema } from "./camunda-sync-destination-schema";
import { GcpSyncDestinationSchema } from "./gcp-sync-destination-schema";
import { HumanitecSyncDestinationSchema } from "./humanitec-sync-destination-schema";
import { TerraformCloudSyncDestinationSchema } from "./terraform-cloud-destination-schema";
import { VercelSyncDestinationSchema } from "./vercel-sync-destination-schema";
const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
AwsParameterStoreSyncDestinationSchema,
@@ -18,7 +21,10 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
AzureKeyVaultSyncDestinationSchema,
AzureAppConfigurationSyncDestinationSchema,
DatabricksSyncDestinationSchema,
HumanitecSyncDestinationSchema
HumanitecSyncDestinationSchema,
TerraformCloudSyncDestinationSchema,
CamundaSyncDestinationSchema,
VercelSyncDestinationSchema
]);
export const SecretSyncFormSchema = SecretSyncUnionSchema;
@@ -0,0 +1,30 @@
import { z } from "zod";
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
import {
TerraformCloudSyncCategory,
TerraformCloudSyncScope
} from "@app/hooks/api/appConnections/terraform-cloud";
import { SecretSync } from "@app/hooks/api/secretSyncs";
export const TerraformCloudSyncDestinationSchema = BaseSecretSyncSchema().merge(
z.object({
destination: z.literal(SecretSync.TerraformCloud),
destinationConfig: z.discriminatedUnion("scope", [
z.object({
scope: z.literal(TerraformCloudSyncScope.VariableSet),
org: z.string().trim().min(1, "Organization required"),
variableSetId: z.string().trim().min(1, "Variable Set required"),
variableSetName: z.string().trim().min(1, "Variable set name required"),
category: z.nativeEnum(TerraformCloudSyncCategory)
}),
z.object({
scope: z.literal(TerraformCloudSyncScope.Workspace),
org: z.string().trim().min(1, "Organization required"),
workspaceId: z.string().trim().min(1, "Workspace required"),
workspaceName: z.string().trim().min(1, "Workspace name required"),
category: z.nativeEnum(TerraformCloudSyncCategory)
})
])
})
);
@@ -0,0 +1,18 @@
import { z } from "zod";
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { VercelEnvironmentType } from "@app/hooks/api/secretSyncs/types/vercel-sync";
export const VercelSyncDestinationSchema = BaseSecretSyncSchema().merge(
z.object({
destination: z.literal(SecretSync.Vercel),
destinationConfig: z.object({
app: z.string().trim().min(1, "Project required"),
appName: z.string().trim().min(1, "Project required"),
env: z.nativeEnum(VercelEnvironmentType).or(z.string()),
branch: z.string().trim().optional(),
teamId: z.string().trim()
})
})
);
@@ -0,0 +1,159 @@
/* eslint-disable no-underscore-dangle */
import { forwardRef, InputHTMLAttributes } from "react";
import { InitialConfigType, LexicalComposer } from "@lexical/react/LexicalComposer";
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
import { OnChangePlugin } from "@lexical/react/LexicalOnChangePlugin";
import { PlainTextPlugin } from "@lexical/react/LexicalPlainTextPlugin";
import { ReactNode } from "@tanstack/react-router";
import { cva, VariantProps } from "cva";
import { EditorState, LexicalEditor } from "lexical";
import { twMerge } from "tailwind-merge";
import { HighlightNode } from "./EditorHighlight";
import { EditorPlaceholderPlugin } from "./EditorPlaceholderPlugin";
// Catch any errors that occur during Lexical updates and log them
// or throw them as needed. If you don't throw them, Lexical will
// try to recover gracefully without losing user data.
function onError(error: Error) {
console.error(error);
}
const inputVariants = cva(
"input w-full py-[0.375rem] text-gray-400 placeholder:text-sm placeholder-gray-500 placeholder-opacity-50 outline-none focus:ring-2 hover:ring-bunker-400/60 duration-100",
{
variants: {
size: {
xs: ["text-xs"],
sm: ["text-sm"],
md: ["text-md"],
lg: ["text-lg"]
},
isRounded: {
true: ["rounded-md"],
false: ""
},
variant: {
filled: ["bg-mineshaft-900", "text-gray-400"],
outline: ["bg-transparent"],
plain: "bg-transparent outline-none"
},
isError: {
true: "focus:ring-red/50 placeholder-red-300",
false: "focus:ring-primary-400/50 focus:ring-1"
}
},
compoundVariants: []
}
);
const inputParentContainerVariants = cva("inline-flex font-inter items-center border relative", {
variants: {
isRounded: {
true: ["rounded-md"],
false: ""
},
isError: {
true: "border-red",
false: "border-mineshaft-500"
},
isFullWidth: {
true: "w-full",
false: ""
},
variant: {
filled: ["bg-bunker-800", "text-gray-400"],
outline: ["bg-transparent"],
plain: "border-none"
}
}
});
type Props = Omit<
InputHTMLAttributes<HTMLDivElement>,
"size" | "onChange" | "placeholder" | "aria-placeholder"
> &
VariantProps<typeof inputVariants> & {
children?: ReactNode;
namespace?: string;
placeholder?: string;
isFullWidth?: boolean;
isRequired?: boolean;
leftIcon?: ReactNode;
rightIcon?: ReactNode;
isDisabled?: boolean;
isReadOnly?: boolean;
containerClassName?: string;
onChange: (editorState: EditorState, editor: LexicalEditor, tags: Set<string>) => void;
initialValue?: string;
};
export const Editor = forwardRef<HTMLDivElement, Props>(
(
{
children,
namespace = "infisical-editor",
className,
containerClassName,
isRounded = true,
isFullWidth = true,
isDisabled,
isError = false,
isRequired,
leftIcon,
rightIcon,
variant = "filled",
size = "md",
isReadOnly,
placeholder,
onChange,
...props
},
ref
) => {
const initialConfig: InitialConfigType = {
namespace,
onError,
nodes: [HighlightNode]
};
return (
<div
className={inputParentContainerVariants({
isRounded,
isError,
isFullWidth,
variant,
className: containerClassName
})}
>
{leftIcon && <span className="absolute left-0 ml-3 text-sm">{leftIcon}</span>}
<LexicalComposer initialConfig={initialConfig}>
<PlainTextPlugin
contentEditable={
<ContentEditable
ref={ref}
aria-required={isRequired}
readOnly={isReadOnly}
disabled={isDisabled}
className={twMerge(
leftIcon ? "pl-10" : "pl-2.5",
rightIcon ? "pr-10" : "pr-2.5",
inputVariants({ className, isError, size, isRounded, variant })
)}
{...props}
placeholder={null}
/>
}
ErrorBoundary={LexicalErrorBoundary}
/>
<OnChangePlugin onChange={onChange} />
<EditorPlaceholderPlugin placeholder={placeholder} />
{children}
</LexicalComposer>
{rightIcon && <span className="absolute right-0 mr-3">{rightIcon}</span>}
</div>
);
}
);
@@ -0,0 +1,127 @@
/* eslint-disable no-underscore-dangle,@typescript-eslint/class-methods-use-this */
import { useCallback, useEffect } from "react";
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
import { useLexicalTextEntity } from "@lexical/react/useLexicalTextEntity";
import {
$applyNodeReplacement,
EditorConfig,
LexicalNode,
SerializedTextNode,
Spread,
TextNode
} from "lexical";
type HighlightTheme = { contentClassName: string };
type Trigger = { startTrigger: string; endTrigger: string };
export type SerializedHighlightNode = Spread<
{
__highlightTheme: HighlightTheme;
__trigger: Trigger;
},
SerializedTextNode
>;
export class HighlightNode extends TextNode {
__highlightTheme: HighlightTheme;
__trigger: Trigger;
constructor(
text: string,
highlightTheme: HighlightTheme = {
contentClassName: "ph-no-capture text-yellow-200/80"
},
trigger: Trigger = { startTrigger: "${", endTrigger: "}" },
key?: string
) {
super(text, key);
this.__highlightTheme = highlightTheme;
this.__trigger = trigger;
}
static getType(): string {
return "highlight";
}
static clone(node: HighlightNode): HighlightNode {
return new HighlightNode(node.__text, node.__highlightTheme, node.__trigger, node.__key);
}
static importJSON(serializedNode: SerializedHighlightNode): HighlightNode {
return $applyNodeReplacement(new HighlightNode("")).updateFromJSON(serializedNode);
}
createDOM(config: EditorConfig): HTMLElement {
const dom = super.createDOM(config);
dom.style.cursor = "default";
dom.className = this.__highlightTheme.contentClassName;
return dom;
}
canInsertTextBefore(): boolean {
return false;
}
canInsertTextAfter(): boolean {
return false;
}
isTextEntity(): true {
return true;
}
}
export function $createKeywordNode(keyword: string = ""): HighlightNode {
return $applyNodeReplacement(new HighlightNode(keyword));
}
export function $isKeywordNode(node: LexicalNode | null | undefined): boolean {
return node instanceof HighlightNode;
}
type Props = {
contentClassName?: string;
startTrigger?: string;
endTrigger?: string;
};
export const EditorHighlightPlugin = ({
endTrigger = "}",
startTrigger = "${",
contentClassName = "ph-no-capture text-yellow-200/80"
}: Props) => {
const [editor] = useLexicalComposerContext();
useEffect(() => {
if (!editor.hasNodes([HighlightNode])) {
throw new Error("HighlightsPlugin: HighlightsNode not registered on editor");
}
}, [editor]);
const createKeywordNode = useCallback((textNode: TextNode): HighlightNode => {
return $applyNodeReplacement(
new HighlightNode(
textNode.getTextContent(),
{ contentClassName },
{ startTrigger, endTrigger }
)
);
}, []);
const getKeywordMatch = useCallback((text: string) => {
for (let i = 0; i < text.length; i += 1) {
if (text.slice(i, i + 2) === startTrigger) {
const closingBracketIndex = text.indexOf(endTrigger, i + 2);
if (closingBracketIndex !== -1) {
return { start: i, end: closingBracketIndex + 1 };
}
return null;
}
}
return null;
}, []);
useLexicalTextEntity<HighlightNode>(getKeywordMatch, HighlightNode, createKeywordNode);
return null;
};
@@ -0,0 +1,22 @@
import { useEffect } from "react";
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
import { useLexicalIsTextContentEmpty } from "@lexical/react/useLexicalIsTextContentEmpty";
export const EditorPlaceholderPlugin = ({ placeholder }: { placeholder: string | undefined }) => {
const [editor] = useLexicalComposerContext();
const isEmpty = useLexicalIsTextContentEmpty(editor);
/* Set the placeholder on root. */
useEffect(() => {
const rootElement = editor.getRootElement() as HTMLElement;
if (rootElement) {
if (isEmpty && placeholder) {
rootElement.setAttribute("placeholder", placeholder);
} else {
rootElement.removeAttribute("placeholder");
}
}
}, [editor, isEmpty]); // eslint-disable-line
return null;
};
@@ -0,0 +1,2 @@
export { Editor } from "./Editor";
export { EditorHighlightPlugin } from "./EditorHighlight";
+1
View File
@@ -11,6 +11,7 @@ export * from "./DatePicker";
export * from "./DeleteActionModal";
export * from "./Drawer";
export * from "./Dropdown";
export * from "./Editor";
export * from "./EmailServiceSetupModal";
export * from "./EmptyState";
export * from "./FilterableSelect";