mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-19 21:21:29 +00:00
feature: secret sync base + aws parameter store & github
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
import { useState } from "react";
|
||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||
import { Tab } from "@headlessui/react";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Switch } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
|
||||
import {
|
||||
SecretSync,
|
||||
SecretSyncInitialSyncBehavior,
|
||||
TSecretSync,
|
||||
useCreateSecretSync,
|
||||
useSecretSyncOption
|
||||
} from "@app/hooks/api/secretSyncs";
|
||||
|
||||
import { SecretSyncFormSchema, TSecretSyncForm } from "./schemas";
|
||||
import { SecretSyncDestinationFields } from "./SecretSyncDestinationFields";
|
||||
import { SecretSyncDetailsFields } from "./SecretSyncDetailsFields";
|
||||
import { SecretSyncOptionsFields } from "./SecretSyncOptionsFields";
|
||||
import { SecretSyncReviewFields } from "./SecretSyncReviewFields";
|
||||
import { SecretSyncSourceFields } from "./SecretSyncSourceFields";
|
||||
|
||||
type Props = {
|
||||
onComplete: (secretSync: TSecretSync) => void;
|
||||
destination: SecretSync;
|
||||
onCancel: () => void;
|
||||
};
|
||||
|
||||
const FORM_TABS: { name: string; key: string; fields: (keyof TSecretSyncForm)[] }[] = [
|
||||
{ name: "Source", key: "source", fields: ["secretPath", "environment"] },
|
||||
{ name: "Destination", key: "destination", fields: ["connection", "destinationConfig"] },
|
||||
{ name: "Options", key: "options", fields: ["syncOptions"] },
|
||||
{ name: "Details", key: "details", fields: ["name", "description"] },
|
||||
{ name: "Review", key: "review", fields: [] }
|
||||
];
|
||||
|
||||
export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Props) => {
|
||||
const createSecretSync = useCreateSecretSync();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { name: destinationName } = SECRET_SYNC_MAP[destination];
|
||||
|
||||
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
|
||||
|
||||
const { syncOption } = useSecretSyncOption(destination);
|
||||
|
||||
const formMethods = useForm<TSecretSyncForm>({
|
||||
resolver: zodResolver(SecretSyncFormSchema),
|
||||
defaultValues: {
|
||||
destination,
|
||||
isEnabled: true,
|
||||
syncOptions: {
|
||||
initialSyncBehavior: syncOption?.canImportSecrets
|
||||
? undefined
|
||||
: SecretSyncInitialSyncBehavior.OverwriteDestination
|
||||
}
|
||||
},
|
||||
reValidateMode: "onChange"
|
||||
});
|
||||
|
||||
const onSubmit = async ({ environment, connection, ...formData }: TSecretSyncForm) => {
|
||||
try {
|
||||
const secretSync = await createSecretSync.mutateAsync({
|
||||
...formData,
|
||||
connectionId: connection.id,
|
||||
environment: environment.slug,
|
||||
projectId: currentWorkspace.id
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: `Successfully added ${destinationName} Sync`,
|
||||
type: "success"
|
||||
});
|
||||
onComplete(secretSync);
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
title: `Failed to add ${destinationName} Sync`,
|
||||
text: err.message,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrev = () => {
|
||||
if (selectedTabIndex === 0) {
|
||||
onCancel();
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedTabIndex((prev) => prev - 1);
|
||||
};
|
||||
|
||||
const { handleSubmit, trigger } = formMethods;
|
||||
|
||||
const isStepValid = async (index: number) => trigger(FORM_TABS[index].fields);
|
||||
|
||||
const isFinalStep = selectedTabIndex === FORM_TABS.length - 1;
|
||||
|
||||
const handleNext = async () => {
|
||||
if (isFinalStep) {
|
||||
handleSubmit(onSubmit)();
|
||||
return;
|
||||
}
|
||||
|
||||
const isValid = await isStepValid(selectedTabIndex);
|
||||
|
||||
if (!isValid) return;
|
||||
|
||||
setSelectedTabIndex((prev) => prev + 1);
|
||||
};
|
||||
|
||||
const isTabEnabled = async (index: number) => {
|
||||
let isEnabled = true;
|
||||
for (let i = index - 1; i >= 0; i -= 1) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
isEnabled = isEnabled && (await isStepValid(i));
|
||||
}
|
||||
|
||||
return isEnabled;
|
||||
};
|
||||
|
||||
const { control } = formMethods;
|
||||
|
||||
return (
|
||||
<form className={twMerge(isFinalStep && "max-h-[70vh] overflow-y-auto")}>
|
||||
<FormProvider {...formMethods}>
|
||||
<Tab.Group selectedIndex={selectedTabIndex} onChange={setSelectedTabIndex}>
|
||||
<Tab.List className="-pb-1 mb-6 w-full border-b-2 border-mineshaft-600">
|
||||
{FORM_TABS.map((tab, index) => (
|
||||
<Tab
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
const isEnabled = await isTabEnabled(index);
|
||||
setSelectedTabIndex((prev) => (isEnabled ? index : prev));
|
||||
}}
|
||||
className={({ selected }) =>
|
||||
`w-30 -mb-[0.14rem] ${index > selectedTabIndex ? "opacity-30" : ""} px-4 py-2 text-sm font-medium outline-none disabled:opacity-60 ${
|
||||
selected
|
||||
? "border-b-2 border-mineshaft-300 text-mineshaft-200"
|
||||
: "text-bunker-300"
|
||||
}`
|
||||
}
|
||||
key={tab.key}
|
||||
>
|
||||
{index + 1}. {tab.name}
|
||||
</Tab>
|
||||
))}
|
||||
</Tab.List>
|
||||
<Tab.Panels>
|
||||
<Tab.Panel>
|
||||
<SecretSyncSourceFields />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<SecretSyncDestinationFields />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<SecretSyncOptionsFields />
|
||||
<Controller
|
||||
control={control}
|
||||
name="isEnabled"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
helperText={
|
||||
value
|
||||
? "Secrets will automatically be synced when changes occur in the source location"
|
||||
: "Secrets will not be synced until enabled"
|
||||
}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Switch
|
||||
className="bg-red/50 shadow-inner data-[state=checked]:bg-green/50"
|
||||
id="secret-sync-enabled"
|
||||
thumbClassName="bg-mineshaft-800"
|
||||
onCheckedChange={onChange}
|
||||
isChecked={value}
|
||||
>
|
||||
<p className="w-14">{value ? "Enabled" : "Disabled"}</p>
|
||||
</Switch>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<SecretSyncDetailsFields />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<SecretSyncReviewFields />
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
</FormProvider>
|
||||
<div className="flex w-full flex-row-reverse justify-between gap-4 pt-4">
|
||||
<Button onClick={handleNext} colorSchema="secondary">
|
||||
{isFinalStep ? "Create Sync" : "Next"}
|
||||
</Button>
|
||||
{selectedTabIndex > 0 && (
|
||||
<Button onClick={handlePrev} colorSchema="secondary">
|
||||
Back
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,103 @@
|
||||
import { ReactNode } from "react";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { SecretSyncEditFields } from "@app/components/secret-syncs/types";
|
||||
import { Button, ModalClose } from "@app/components/v2";
|
||||
import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
|
||||
import { TSecretSync, useUpdateSecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
import { SecretSyncFormSchema, TSecretSyncForm } from "./schemas";
|
||||
import { SecretSyncDestinationFields } from "./SecretSyncDestinationFields";
|
||||
import { SecretSyncDetailsFields } from "./SecretSyncDetailsFields";
|
||||
import { SecretSyncOptionsFields } from "./SecretSyncOptionsFields";
|
||||
import { SecretSyncSourceFields } from "./SecretSyncSourceFields";
|
||||
|
||||
type Props = {
|
||||
onComplete: (secretSync: TSecretSync) => void;
|
||||
secretSync: TSecretSync;
|
||||
fields: SecretSyncEditFields;
|
||||
};
|
||||
|
||||
export const EditSecretSyncForm = ({ secretSync, fields, onComplete }: Props) => {
|
||||
const updateSecretSync = useUpdateSecretSync();
|
||||
const { name: destinationName } = SECRET_SYNC_MAP[secretSync.destination];
|
||||
|
||||
const formMethods = useForm<TSecretSyncForm>({
|
||||
resolver: zodResolver(SecretSyncFormSchema),
|
||||
defaultValues: {
|
||||
...secretSync,
|
||||
secretPath: secretSync.folder.path,
|
||||
description: secretSync.description ?? ""
|
||||
},
|
||||
reValidateMode: "onChange"
|
||||
});
|
||||
|
||||
const onSubmit = async ({ environment, ...formData }: TSecretSyncForm) => {
|
||||
try {
|
||||
const updatedSecretSync = await updateSecretSync.mutateAsync({
|
||||
syncId: secretSync.id,
|
||||
...formData,
|
||||
environment: environment.slug
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: `Successfully updated ${destinationName} Sync`,
|
||||
type: "success"
|
||||
});
|
||||
onComplete(updatedSecretSync);
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
title: `Failed to update ${destinationName} Sync`,
|
||||
text: err.message,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let Component: ReactNode;
|
||||
|
||||
switch (fields) {
|
||||
case SecretSyncEditFields.Destination:
|
||||
Component = <SecretSyncDestinationFields />;
|
||||
break;
|
||||
case SecretSyncEditFields.Options:
|
||||
Component = <SecretSyncOptionsFields hideInitialSync={Boolean(secretSync.lastSyncedAt)} />;
|
||||
break;
|
||||
case SecretSyncEditFields.Source:
|
||||
Component = <SecretSyncSourceFields />;
|
||||
break;
|
||||
case SecretSyncEditFields.Details:
|
||||
default:
|
||||
Component = <SecretSyncDetailsFields />;
|
||||
break;
|
||||
}
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
formState: { isSubmitting, isDirty }
|
||||
} = formMethods;
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<FormProvider {...formMethods}>{Component}</FormProvider>
|
||||
<div className="flex w-full justify-between gap-4 pt-4">
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
<Button
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={!isDirty || isSubmitting}
|
||||
type="submit"
|
||||
colorSchema="secondary"
|
||||
>
|
||||
Update Sync
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
|
||||
import { FilterableSelect, FormControl } from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useOrgPermission } from "@app/context";
|
||||
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
|
||||
import { SECRET_SYNC_CONNECTION_MAP } from "@app/helpers/secretSyncs";
|
||||
import { useListAvailableAppConnections } from "@app/hooks/api/appConnections";
|
||||
|
||||
import { TSecretSyncForm } from "./schemas";
|
||||
|
||||
type Props = {
|
||||
onChange?: VoidFunction;
|
||||
};
|
||||
|
||||
export const SecretSyncConnectionField = ({ onChange: callback }: Props) => {
|
||||
const { permission } = useOrgPermission();
|
||||
const { control, watch } = useFormContext<TSecretSyncForm>();
|
||||
|
||||
const destination = watch("destination");
|
||||
const app = SECRET_SYNC_CONNECTION_MAP[destination];
|
||||
|
||||
const { data: options, isLoading } = useListAvailableAppConnections(app);
|
||||
|
||||
const connectionName = APP_CONNECTION_MAP[app].name;
|
||||
|
||||
const canCreateConnection = permission.can(
|
||||
OrgPermissionActions.Create,
|
||||
OrgPermissionSubjects.AppConnections
|
||||
);
|
||||
|
||||
const appName = APP_CONNECTION_MAP[SECRET_SYNC_CONNECTION_MAP[destination]].name;
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="mb-4 text-sm text-bunker-300">
|
||||
Specify the {appName} Connection to use to connect to {connectionName} and configure
|
||||
destination parameters.
|
||||
</p>
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
tooltipText="App Connections can be created from the Organization Settings page."
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label={`${connectionName} Connection`}
|
||||
>
|
||||
<FilterableSelect
|
||||
value={value}
|
||||
onChange={(newValue) => {
|
||||
onChange(newValue);
|
||||
if (callback) callback();
|
||||
}}
|
||||
isLoading={isLoading}
|
||||
options={options}
|
||||
placeholder="Select connection..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.id}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="connection"
|
||||
/>
|
||||
{options?.length === 0 && (
|
||||
<p className="-mt-2.5 mb-2.5 text-xs text-yellow">
|
||||
<FontAwesomeIcon className="mr-1" size="xs" icon={faInfoCircle} />
|
||||
{canCreateConnection ? (
|
||||
<>
|
||||
You do not have access to any {appName} Connections. Create one from the{" "}
|
||||
<Link
|
||||
to="/organization/settings"
|
||||
className="underline"
|
||||
search={{ selectedTab: "app-connections" }}
|
||||
>
|
||||
Organization Settings
|
||||
</Link>{" "}
|
||||
page.
|
||||
</>
|
||||
) : (
|
||||
`You do not have access to any ${appName} Connections. Contact an admin to create one.`
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import { components, OptionProps, SingleValue } from "react-select";
|
||||
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
|
||||
import { Badge, FilterableSelect, FormControl, Input } from "@app/components/v2";
|
||||
import { AWS_REGIONS } from "@app/helpers/appConnections";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
import { TSecretSyncForm } from "../schemas";
|
||||
|
||||
const Option = ({ isSelected, children, ...props }: OptionProps<(typeof AWS_REGIONS)[number]>) => {
|
||||
return (
|
||||
<components.Option isSelected={isSelected} {...props}>
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<p className="truncate">{children}</p>
|
||||
<Badge variant="success" className="ml-1 mr-auto cursor-pointer">
|
||||
{props.data.slug}
|
||||
</Badge>
|
||||
{isSelected && (
|
||||
<FontAwesomeIcon className="ml-2 text-primary" icon={faCheckCircle} size="sm" />
|
||||
)}
|
||||
</div>
|
||||
</components.Option>
|
||||
);
|
||||
};
|
||||
|
||||
export const AwsParameterStoreSyncFields = () => {
|
||||
const { control } = useFormContext<
|
||||
TSecretSyncForm & { destination: SecretSync.AWSParameterStore }
|
||||
>();
|
||||
|
||||
return (
|
||||
<>
|
||||
<SecretSyncConnectionField />
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message} label="Region">
|
||||
<FilterableSelect
|
||||
value={AWS_REGIONS.find((region) => region.slug === value)}
|
||||
onChange={(option) =>
|
||||
onChange((option as SingleValue<(typeof AWS_REGIONS)[number]>)?.slug)
|
||||
}
|
||||
options={AWS_REGIONS}
|
||||
placeholder="Select region..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.slug}
|
||||
components={{ Option }}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="destinationConfig.region"
|
||||
/>
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message} label="Path">
|
||||
<Input value={value} onChange={onChange} placeholder="Path..." />
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="destinationConfig.path"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+234
@@ -0,0 +1,234 @@
|
||||
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||
import { MultiValue, SingleValue } from "react-select";
|
||||
|
||||
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
|
||||
import { FilterableSelect, FormControl, Select, SelectItem } from "@app/components/v2";
|
||||
import {
|
||||
TGitHubConnectionEnvironment,
|
||||
TGitHubConnectionOrganization,
|
||||
TGitHubConnectionRepository,
|
||||
useGitHubConnectionListEnvironments,
|
||||
useGitHubConnectionListOrganizations,
|
||||
useGitHubConnectionListRepositories
|
||||
} from "@app/hooks/api/appConnections/github";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
import {
|
||||
GitHubSyncScope,
|
||||
GitHubSyncVisibility
|
||||
} from "@app/hooks/api/secretSyncs/types/github-sync";
|
||||
|
||||
import { TSecretSyncForm } from "../schemas";
|
||||
|
||||
export const GitHubSyncFields = () => {
|
||||
const {
|
||||
control,
|
||||
formState: { errors },
|
||||
watch,
|
||||
setValue
|
||||
} = useFormContext<TSecretSyncForm & { destination: SecretSync.GitHub }>();
|
||||
|
||||
console.log("errors", errors, watch());
|
||||
|
||||
const connectionId = useWatch({ name: "connection.id", control });
|
||||
const currentScope = watch("destinationConfig.scope");
|
||||
const currentVisibility = watch("destinationConfig.visibility", GitHubSyncVisibility.All);
|
||||
const currentOrg = watch("destinationConfig.org");
|
||||
const currentRepo = watch("destinationConfig.repo");
|
||||
const currentOwner = watch("destinationConfig.owner");
|
||||
|
||||
const { data: repositories = [], isPending: isRepositoriesPending } =
|
||||
useGitHubConnectionListRepositories(connectionId, {
|
||||
enabled: Boolean(connectionId)
|
||||
});
|
||||
|
||||
const { data: organizations = [], isPending: isOrganizationsPending } =
|
||||
useGitHubConnectionListOrganizations(connectionId, {
|
||||
enabled: Boolean(connectionId && currentScope === GitHubSyncScope.Organization)
|
||||
});
|
||||
|
||||
const { data: environments = [], isPending: isEnvironmentsPending } =
|
||||
useGitHubConnectionListEnvironments(
|
||||
{
|
||||
connectionId,
|
||||
repo: currentRepo,
|
||||
owner: currentOwner
|
||||
},
|
||||
{
|
||||
enabled: Boolean(
|
||||
connectionId &&
|
||||
currentRepo &&
|
||||
currentOwner &&
|
||||
currentScope === GitHubSyncScope.RepositoryEnvironment
|
||||
)
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SecretSyncConnectionField
|
||||
onChange={() => {
|
||||
setValue("destinationConfig.org", "");
|
||||
setValue("destinationConfig.repo", "");
|
||||
setValue("destinationConfig.owner", "");
|
||||
setValue("destinationConfig.selectedRepositoryIds", []);
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
name="destinationConfig.scope"
|
||||
control={control}
|
||||
defaultValue={GitHubSyncScope.Repository}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl errorText={error?.message} isError={Boolean(error?.message)} label="Scope">
|
||||
<Select
|
||||
value={value}
|
||||
onValueChange={(val) => {
|
||||
onChange(val);
|
||||
}}
|
||||
className="w-full border border-mineshaft-500 capitalize"
|
||||
position="popper"
|
||||
placeholder="Select a scope..."
|
||||
dropdownContainerClassName="max-w-none"
|
||||
>
|
||||
{Object.values(GitHubSyncScope).map((scope) => (
|
||||
<SelectItem className="capitalize" value={scope} key={scope}>
|
||||
{scope.replace("-", " ")}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{currentScope === GitHubSyncScope.Organization && (
|
||||
<>
|
||||
<Controller
|
||||
name="destinationConfig.org"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message} label="Organization">
|
||||
<FilterableSelect
|
||||
isLoading={isOrganizationsPending && Boolean(connectionId)}
|
||||
isDisabled={!connectionId}
|
||||
value={organizations.find((org) => org.login === value) ?? null}
|
||||
onChange={(option) =>
|
||||
onChange((option as SingleValue<TGitHubConnectionOrganization>)?.login ?? null)
|
||||
}
|
||||
options={organizations}
|
||||
placeholder="Select an organization..."
|
||||
getOptionLabel={(option) => option.login}
|
||||
getOptionValue={(option) => option.login}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="destinationConfig.visibility"
|
||||
control={control}
|
||||
defaultValue={GitHubSyncVisibility.All}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Visibility"
|
||||
>
|
||||
<Select
|
||||
value={value}
|
||||
onValueChange={(val) => {
|
||||
onChange(val);
|
||||
}}
|
||||
className="w-full border border-mineshaft-500 capitalize"
|
||||
position="popper"
|
||||
placeholder="Select visibility..."
|
||||
dropdownContainerClassName="max-w-none"
|
||||
>
|
||||
{Object.values(GitHubSyncVisibility).map((scope) => (
|
||||
<SelectItem className="capitalize" value={scope} key={scope}>
|
||||
{scope.replace("-", " ")} Repositories
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{currentVisibility === GitHubSyncVisibility.Selected && (
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Selected Repositories"
|
||||
>
|
||||
<FilterableSelect
|
||||
menuPlacement="top"
|
||||
isLoading={isRepositoriesPending && Boolean(currentOrg)}
|
||||
isDisabled={!currentOrg || !connectionId}
|
||||
isMulti
|
||||
value={repositories.filter((repo) => value?.includes(repo.id))}
|
||||
onChange={(option) => {
|
||||
const repos = option as MultiValue<TGitHubConnectionRepository>;
|
||||
onChange(repos.map((repo) => repo.id));
|
||||
}}
|
||||
options={repositories.filter((repo) => repo.owner.login === currentOrg)}
|
||||
placeholder="Select one or more repositories..."
|
||||
getOptionLabel={(option) => `${option.owner.login}/${option.name}`}
|
||||
getOptionValue={(option) => option.id.toString()}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="destinationConfig.selectedRepositoryIds"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{currentScope !== GitHubSyncScope.Organization && (
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message} label="Repository">
|
||||
<FilterableSelect
|
||||
menuPlacement="top"
|
||||
isLoading={isRepositoriesPending && Boolean(connectionId)}
|
||||
isDisabled={!connectionId}
|
||||
value={repositories.find((repo) => repo.name === value) ?? null}
|
||||
onChange={(option) => {
|
||||
const repo = option as SingleValue<TGitHubConnectionRepository>;
|
||||
|
||||
onChange(repo?.name);
|
||||
setValue("destinationConfig.owner", repo?.owner.login ?? "");
|
||||
}}
|
||||
options={repositories}
|
||||
placeholder="Select a repository..."
|
||||
getOptionLabel={(option) => `${option.owner.login}/${option.name}`}
|
||||
getOptionValue={(option) => option.id.toString()}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="destinationConfig.repo"
|
||||
/>
|
||||
)}
|
||||
{currentScope === GitHubSyncScope.RepositoryEnvironment && (
|
||||
<Controller
|
||||
name="destinationConfig.env"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message} label="Environment">
|
||||
<FilterableSelect
|
||||
menuPlacement="top"
|
||||
isLoading={isEnvironmentsPending && Boolean(connectionId) && Boolean(currentRepo)}
|
||||
isDisabled={!connectionId || !currentRepo}
|
||||
value={environments.find((env) => env.name === value) ?? null}
|
||||
onChange={(option) =>
|
||||
onChange((option as SingleValue<TGitHubConnectionEnvironment>)?.name ?? null)
|
||||
}
|
||||
options={environments}
|
||||
placeholder="Select an environment..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.id.toString()}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
import { TSecretSyncForm } from "../schemas";
|
||||
import { AwsParameterStoreSyncFields } from "./AwsParameterStoreSyncFields";
|
||||
import { GitHubSyncFields } from "./GitHubSyncFields";
|
||||
|
||||
export const SecretSyncDestinationFields = () => {
|
||||
const { watch } = useFormContext<TSecretSyncForm>();
|
||||
|
||||
const destination = watch("destination");
|
||||
|
||||
switch (destination) {
|
||||
case SecretSync.AWSParameterStore:
|
||||
return <AwsParameterStoreSyncFields />;
|
||||
case SecretSync.GitHub:
|
||||
return <GitHubSyncFields />;
|
||||
default:
|
||||
throw new Error(`Unhandled Destination Config Field: ${destination}`);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./SecretSyncDestinationFields";
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
|
||||
import { FormControl, Input, TextArea } from "@app/components/v2";
|
||||
|
||||
import { TSecretSyncForm } from "./schemas";
|
||||
|
||||
export const SecretSyncDetailsFields = () => {
|
||||
const { control } = useFormContext<TSecretSyncForm>();
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="mb-4 text-sm text-bunker-300">
|
||||
Provide a name and description for this Secret Sync.
|
||||
</p>
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
helperText="Must be slug-friendly"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Name"
|
||||
>
|
||||
<Input value={value} onChange={onChange} placeholder="my-secret-sync" />
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="name"
|
||||
/>
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
isOptional
|
||||
errorText={error?.message}
|
||||
label="Description"
|
||||
>
|
||||
<TextArea
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder="Describe the purpose of this sync..."
|
||||
className="!resize-none"
|
||||
rows={4}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="description"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,124 @@
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import { faTriangleExclamation } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { FormControl, Input, Select, SelectItem } from "@app/components/v2";
|
||||
import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
|
||||
import { useSecretSyncOption } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
import { TSecretSyncForm } from "./schemas";
|
||||
|
||||
type Props = {
|
||||
hideInitialSync?: boolean;
|
||||
};
|
||||
|
||||
export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
|
||||
const { control, watch } = useFormContext<TSecretSyncForm>();
|
||||
|
||||
const destination = watch("destination");
|
||||
|
||||
const destinationName = SECRET_SYNC_MAP[destination].name;
|
||||
|
||||
const { syncOption } = useSecretSyncOption(destination);
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="mb-4 text-sm text-bunker-300">Configure how secrets should be synced.</p>
|
||||
{!hideInitialSync && (
|
||||
<>
|
||||
<Controller
|
||||
name="syncOptions.initialSyncBehavior"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
tooltipClassName="max-w-lg py-3"
|
||||
tooltipText={
|
||||
syncOption?.canImportSecrets ? (
|
||||
<div className="flex flex-col gap-3">
|
||||
<p>
|
||||
Specify how Infisical should resolve the initial sync to {destinationName}.
|
||||
The following options are available:
|
||||
</p>
|
||||
<ul className="flex list-disc flex-col gap-3 pl-4">
|
||||
{Object.values(SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP).map((details) => {
|
||||
const { name, description } = details(destinationName);
|
||||
|
||||
return (
|
||||
<li key={name}>
|
||||
<p className="text-mineshaft-300">
|
||||
<span className="font-medium text-bunker-200">{name}</span>:{" "}
|
||||
{description}
|
||||
</p>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Initial Sync Behavior"
|
||||
>
|
||||
<Select
|
||||
isDisabled={!syncOption?.canImportSecrets}
|
||||
value={value}
|
||||
onValueChange={(val) => onChange(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
position="popper"
|
||||
placeholder="Select an option..."
|
||||
dropdownContainerClassName="max-w-none"
|
||||
>
|
||||
{Object.entries(SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP).map(([key, details]) => {
|
||||
const { name } = details(destinationName);
|
||||
|
||||
return (
|
||||
<SelectItem value={key} key={key}>
|
||||
{name}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{!syncOption?.canImportSecrets && (
|
||||
<p className="-mt-2.5 mb-2.5 text-xs text-yellow">
|
||||
<FontAwesomeIcon className="mr-1" size="xs" icon={faTriangleExclamation} />
|
||||
{destinationName} only supports overwriting destination secrets. Secrets not present
|
||||
in Infisical will be removed from the destination.
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
isOptional
|
||||
errorText={error?.message}
|
||||
label="Prepend Prefix"
|
||||
>
|
||||
<Input className="uppercase" value={value} onChange={onChange} placeholder="INF_" />
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="syncOptions.prependPrefix"
|
||||
/>
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
isOptional
|
||||
errorText={error?.message}
|
||||
label="Append Suffix"
|
||||
>
|
||||
<Input className="uppercase" value={value} onChange={onChange} placeholder="_INF" />
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="syncOptions.appendSuffix"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { SecretSyncLabel } from "@app/components/secret-syncs";
|
||||
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
||||
import { Badge } from "@app/components/v2";
|
||||
import { AWS_REGIONS } from "@app/helpers/appConnections";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
export const AwsParameterStoreSyncReviewFields = () => {
|
||||
const { watch } = useFormContext<
|
||||
TSecretSyncForm & { destination: SecretSync.AWSParameterStore }
|
||||
>();
|
||||
|
||||
const [region, path] = watch(["destinationConfig.region", "destinationConfig.path"]);
|
||||
|
||||
const awsRegion = AWS_REGIONS.find((r) => r.slug === region);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SecretSyncLabel label="Region">
|
||||
{awsRegion?.name}
|
||||
<Badge className="ml-1" variant="success">
|
||||
{awsRegion?.slug}{" "}
|
||||
</Badge>
|
||||
</SecretSyncLabel>
|
||||
<SecretSyncLabel label="Path">{path}</SecretSyncLabel>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import { ReactNode } from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { SecretSyncLabel } from "@app/components/secret-syncs";
|
||||
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
import { GitHubSyncScope, TGitHubSync } from "@app/hooks/api/secretSyncs/types/github-sync";
|
||||
|
||||
export const GitHubSyncReviewFields = () => {
|
||||
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.GitHub }>();
|
||||
|
||||
const config = watch("destinationConfig");
|
||||
|
||||
let ScopeComponents: ReactNode;
|
||||
|
||||
switch (config.scope) {
|
||||
case GitHubSyncScope.Repository:
|
||||
ScopeComponents = (
|
||||
<SecretSyncLabel label="Repository">
|
||||
{config.owner}/{config.repo}
|
||||
</SecretSyncLabel>
|
||||
);
|
||||
break;
|
||||
case GitHubSyncScope.Organization:
|
||||
ScopeComponents = (
|
||||
<>
|
||||
<SecretSyncLabel label="Organization">{config.org}</SecretSyncLabel>
|
||||
<SecretSyncLabel className="capitalize" label="Visibility">
|
||||
{config.visibility}
|
||||
</SecretSyncLabel>
|
||||
</>
|
||||
);
|
||||
break;
|
||||
case GitHubSyncScope.RepositoryEnvironment:
|
||||
ScopeComponents = (
|
||||
<>
|
||||
<SecretSyncLabel label="Repository">
|
||||
{config.owner}/{config.repo}
|
||||
</SecretSyncLabel>
|
||||
<SecretSyncLabel className="capitalize" label="Environment">
|
||||
{config.env}
|
||||
</SecretSyncLabel>
|
||||
</>
|
||||
);
|
||||
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled GitHub Sync Review Field Scope ${(config as TGitHubSync["destinationConfig"]).scope}`
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SecretSyncLabel className="capitalize" label="Scope">
|
||||
{config.scope.replace("-", " ")}
|
||||
</SecretSyncLabel>
|
||||
{ScopeComponents}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
import { ReactNode } from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { SecretSyncLabel } from "@app/components/secret-syncs";
|
||||
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
||||
import { Badge } from "@app/components/v2";
|
||||
import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
import { AwsParameterStoreSyncReviewFields } from "./AwsParameterStoreSyncReviewFields";
|
||||
import { GitHubSyncReviewFields } from "./GitHubSyncReviewFields";
|
||||
|
||||
export const SecretSyncReviewFields = () => {
|
||||
const { watch } = useFormContext<TSecretSyncForm>();
|
||||
|
||||
let DestinationFieldsComponent: ReactNode;
|
||||
|
||||
const {
|
||||
name,
|
||||
description,
|
||||
connection,
|
||||
environment,
|
||||
secretPath,
|
||||
syncOptions: { appendSuffix, prependPrefix, initialSyncBehavior },
|
||||
destination,
|
||||
isEnabled
|
||||
} = watch();
|
||||
|
||||
const destinationName = SECRET_SYNC_MAP[destination].name;
|
||||
|
||||
switch (destination) {
|
||||
case SecretSync.AWSParameterStore:
|
||||
DestinationFieldsComponent = <AwsParameterStoreSyncReviewFields />;
|
||||
break;
|
||||
case SecretSync.GitHub:
|
||||
DestinationFieldsComponent = <GitHubSyncReviewFields />;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unhandled Destination Review Fields: ${destination}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-4 flex flex-col gap-6">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="w-full border-b border-mineshaft-600">
|
||||
<span className="text-sm text-mineshaft-300">Source</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-8 gap-y-2">
|
||||
<SecretSyncLabel label="Environment">{environment.name}</SecretSyncLabel>
|
||||
<SecretSyncLabel label="Secret Path">{secretPath}</SecretSyncLabel>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="w-full border-b border-mineshaft-600">
|
||||
<span className="text-sm text-mineshaft-300">Destination</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-8 gap-y-2">
|
||||
<SecretSyncLabel label="Connection">{connection.name}</SecretSyncLabel>
|
||||
{DestinationFieldsComponent}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="w-full border-b border-mineshaft-600">
|
||||
<span className="text-sm text-mineshaft-300">Options</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-8 gap-y-2">
|
||||
<SecretSyncLabel label="Sync Enabled">
|
||||
<Badge variant={isEnabled ? "success" : "danger"}>
|
||||
{isEnabled ? "Enabled" : "Disabled"}
|
||||
</Badge>
|
||||
</SecretSyncLabel>
|
||||
<SecretSyncLabel label="Initial Sync Behavior">
|
||||
{SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP[initialSyncBehavior](destinationName).name}
|
||||
</SecretSyncLabel>
|
||||
<SecretSyncLabel label="Prepend Prefix">{prependPrefix}</SecretSyncLabel>
|
||||
<SecretSyncLabel label="Append Suffix">{appendSuffix}</SecretSyncLabel>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="w-full border-b border-mineshaft-600">
|
||||
<span className="text-sm text-mineshaft-300">Details</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-8 gap-y-2">
|
||||
<SecretSyncLabel label="Name">{name}</SecretSyncLabel>
|
||||
<SecretSyncLabel label="Description">{description}</SecretSyncLabel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./SecretSyncReviewFields";
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
|
||||
import { FilterableSelect, FormControl } from "@app/components/v2";
|
||||
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
|
||||
import { useWorkspace } from "@app/context";
|
||||
|
||||
import { TSecretSyncForm } from "./schemas";
|
||||
|
||||
export const SecretSyncSourceFields = () => {
|
||||
const { control, watch } = useFormContext<TSecretSyncForm>();
|
||||
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const selectedEnvironment = watch("environment");
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="mb-4 text-sm text-bunker-300">
|
||||
Specify the environment and path where you would like to sync secrets from.
|
||||
</p>
|
||||
|
||||
<Controller
|
||||
defaultValue={currentWorkspace.environments[0]}
|
||||
control={control}
|
||||
name="environment"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl label="Environment" isError={Boolean(error)} errorText={error?.message}>
|
||||
<FilterableSelect
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
options={currentWorkspace.environments}
|
||||
placeholder="Select environment..."
|
||||
getOptionLabel={(option) => option?.name}
|
||||
getOptionValue={(option) => option?.id}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
defaultValue="/"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message} label="Secret Path">
|
||||
<SecretPathInput
|
||||
environment={selectedEnvironment?.slug}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="secretPath"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./CreateSecretSyncForm";
|
||||
export * from "./EditSecretSyncForm";
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
export const AwsParameterStoreSyncDestinationSchema = z.object({
|
||||
destination: z.literal(SecretSync.AWSParameterStore),
|
||||
destinationConfig: z.object({
|
||||
path: z
|
||||
.string()
|
||||
.min(1, "Parameter Store Path required")
|
||||
.superRefine((val, ctx) => {
|
||||
if (!/^\/([/]|(([\w-]+\/)+))?$/.test(val)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Invalid path - must follow "/example/path/" format'
|
||||
});
|
||||
}
|
||||
|
||||
if (val.length > 2048) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Cannot exceed 2048 characters"
|
||||
});
|
||||
}
|
||||
}),
|
||||
region: z.string().min(1, "Region required")
|
||||
})
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
import {
|
||||
GitHubSyncScope,
|
||||
GitHubSyncVisibility
|
||||
} from "@app/hooks/api/secretSyncs/types/github-sync";
|
||||
|
||||
export const GitHubSyncDestinationSchema = z.object({
|
||||
destination: z.literal(SecretSync.GitHub),
|
||||
destinationConfig: z
|
||||
.discriminatedUnion("scope", [
|
||||
z.object({
|
||||
scope: z.literal(GitHubSyncScope.Organization),
|
||||
org: z.string().min(1, "Organization name required"),
|
||||
visibility: z.nativeEnum(GitHubSyncVisibility),
|
||||
selectedRepositoryIds: z.number().array().optional()
|
||||
}),
|
||||
z.object({
|
||||
scope: z.literal(GitHubSyncScope.Repository),
|
||||
owner: z.string().min(1, "Repository owner name required"),
|
||||
repo: z.string().min(1, "Repository name required")
|
||||
}),
|
||||
z.object({
|
||||
scope: z.literal(GitHubSyncScope.RepositoryEnvironment),
|
||||
owner: z.string().min(1, "Repository owner name required"),
|
||||
repo: z.string().min(1, "Repository name required"),
|
||||
env: z.string().min(1, "Environment name required")
|
||||
})
|
||||
])
|
||||
.superRefine((options, ctx) => {
|
||||
if (options.scope !== GitHubSyncScope.Organization) return;
|
||||
|
||||
if (
|
||||
options.visibility === GitHubSyncVisibility.Selected &&
|
||||
!options.selectedRepositoryIds?.length
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Select at least 1 repository",
|
||||
path: ["selectedRepositoryIds"]
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./secret-sync-schema";
|
||||
@@ -0,0 +1,39 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { GitHubSyncDestinationSchema } from "@app/components/secret-syncs/forms/schemas/github-sync-destination-schema";
|
||||
import { SecretSyncInitialSyncBehavior } from "@app/hooks/api/secretSyncs";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
import { AwsParameterStoreSyncDestinationSchema } from "./aws-parameter-store-sync-destination-schema";
|
||||
|
||||
const BaseSecretSyncSchema = z.object({
|
||||
name: slugSchema({ field: "Name" }),
|
||||
description: z.string().trim().max(256, "Cannot exceed 256 characters").optional(),
|
||||
connection: z.object({ name: z.string(), id: z.string().uuid() }),
|
||||
environment: z.object({ slug: z.string(), id: z.string(), name: z.string() }),
|
||||
secretPath: z.string().min(1, "Secret path required"),
|
||||
syncOptions: z.object({
|
||||
initialSyncBehavior: z.nativeEnum(SecretSyncInitialSyncBehavior),
|
||||
prependPrefix: z
|
||||
.string()
|
||||
.trim()
|
||||
.transform((str) => str.toUpperCase())
|
||||
.optional(),
|
||||
appendSuffix: z
|
||||
.string()
|
||||
.trim()
|
||||
.transform((str) => str.toUpperCase())
|
||||
.optional()
|
||||
}),
|
||||
isEnabled: z.boolean()
|
||||
});
|
||||
|
||||
// TODO: union once more supported
|
||||
export const SecretSyncFormSchema = z
|
||||
.discriminatedUnion("destination", [
|
||||
AwsParameterStoreSyncDestinationSchema,
|
||||
GitHubSyncDestinationSchema
|
||||
])
|
||||
.and(BaseSecretSyncSchema);
|
||||
|
||||
export type TSecretSyncForm = z.infer<typeof SecretSyncFormSchema>;
|
||||
Reference in New Issue
Block a user