diff --git a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx
index 78a2380d0..a611eb030 100644
--- a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx
+++ b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx
@@ -19,6 +19,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useNavigate, useRouter, useSearch } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
+import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
@@ -49,8 +50,10 @@ import {
import { ROUTE_PATHS } from "@app/const/routes";
import {
ProjectPermissionActions,
+ ProjectPermissionDynamicSecretActions,
ProjectPermissionSub,
useProjectPermission,
+ useSubscription,
useWorkspace
} from "@app/context";
import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
@@ -71,6 +74,7 @@ import { SecretType, SecretV3RawSanitized, TSecretFolder } from "@app/hooks/api/
import { ProjectType, ProjectVersion } from "@app/hooks/api/workspace/types";
import { useDynamicSecretOverview, useFolderOverview, useSecretOverview } from "@app/hooks/utils";
+import { CreateDynamicSecretForm } from "../SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm";
import { FolderForm } from "../SecretDashboardPage/components/ActionBar/FolderForm";
import { CreateSecretForm } from "./components/CreateSecretForm";
import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs";
@@ -131,6 +135,7 @@ export const OverviewPage = () => {
const [searchFilter, setSearchFilter] = useState("");
const [debouncedSearchFilter, setDebouncedSearchFilter] = useDebounce(searchFilter);
const secretPath = (routerSearch?.secretPath as string) || "/";
+ const { subscription } = useSubscription();
const [filter, setFilter] = useState(DEFAULT_FILTER_STATE);
const [filterHistory, setFilterHistory] = useState<
@@ -178,6 +183,17 @@ export const OverviewPage = () => {
}, []);
const userAvailableEnvs = currentWorkspace?.environments || [];
+ const userAvailableDynamicSecretEnvs = userAvailableEnvs.filter((env) =>
+ permission.can(
+ ProjectPermissionDynamicSecretActions.CreateRootCredential,
+ subject(ProjectPermissionSub.DynamicSecrets, {
+ environment: env.id,
+ secretPath,
+ secretName: "*",
+ secretTags: ["*"]
+ })
+ )
+ );
const [visibleEnvs, setVisibleEnvs] = useState(userAvailableEnvs);
@@ -249,7 +265,9 @@ export const OverviewPage = () => {
"addSecretsInAllEnvs",
"addFolder",
"misc",
- "updateFolder"
+ "updateFolder",
+ "addDynamicSecret",
+ "upgradePlan"
] as const);
const handleFolderCreate = async (folderName: string, description: string | null) => {
@@ -845,6 +863,22 @@ export const OverviewPage = () => {
+
}
+ onClick={() => {
+ if (subscription && subscription.dynamicSecret) {
+ handlePopUpOpen("addDynamicSecret");
+ return;
+ }
+ handlePopUpOpen("upgradePlan");
+ }}
+ isDisabled={userAvailableDynamicSecretEnvs.length === 0}
+ variant="outline_bg"
+ className="h-10 text-left"
+ isFullWidth
+ >
+ Add Dynamic Secret
+
{
/>
+ handlePopUpToggle("addDynamicSecret", isOpen)}
+ projectSlug={projectSlug}
+ environments={userAvailableDynamicSecretEnvs}
+ secretPath={secretPath}
+ />
+ {subscription && (
+ handlePopUpToggle("upgradePlan", isOpen)}
+ 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"
+ }
+ />
+ )}
);
};
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx
index d545ba748..28c58ec5f 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx
@@ -655,7 +655,7 @@ export const ActionBar = ({
isOpen={popUp.addDynamicSecret.isOpen}
onToggle={(isOpen) => handlePopUpToggle("addDynamicSecret", isOpen)}
projectSlug={projectSlug}
- environment={environment}
+ environments={[{ slug: environment, name: "not-used", id: "not-used" }]}
secretPath={secretPath}
/>
24 * 60 * 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
}),
- name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
+ name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
+ environments: z.object({ name: z.string(), slug: z.string() }).array()
});
type TForm = z.infer;
@@ -59,13 +62,13 @@ type Props = {
onCancel: () => void;
secretPath: string;
projectSlug: string;
- environment: string;
+ environments: WorkspaceEnv[];
};
export const AwsElastiCacheInputForm = ({
onCompleted,
onCancel,
- environment,
+ environments,
secretPath,
projectSlug
}: Props) => {
@@ -87,32 +90,42 @@ export const AwsElastiCacheInputForm = ({
revocationStatement: `{
"UserId": "{{username}}"
}`
- }
+ },
+ environments: environments.length === 1 ? environments : []
}
});
const createDynamicSecret = useCreateDynamicSecret();
- const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
+ const handleCreateDynamicSecret = async ({
+ name,
+ maxTTL,
+ provider,
+ defaultTTL,
+ environments: selectedEnvs
+ }: TForm) => {
// wait till previous request is finished
if (createDynamicSecret.isPending) return;
- try {
- await createDynamicSecret.mutateAsync({
- provider: { type: DynamicSecretProviders.AwsElastiCache, inputs: provider },
- maxTTL,
- name,
- path: secretPath,
- defaultTTL,
- projectSlug,
- environmentSlug: environment
- });
- onCompleted();
- } catch {
- createNotification({
- type: "error",
- text: "Failed to create dynamic secret"
- });
- }
+ const promises = selectedEnvs.map(async (env) => {
+ try {
+ await createDynamicSecret.mutateAsync({
+ provider: { type: DynamicSecretProviders.AwsElastiCache, inputs: provider },
+ maxTTL,
+ name,
+ path: secretPath,
+ defaultTTL,
+ projectSlug,
+ environmentSlug: env.slug
+ });
+ onCompleted();
+ } catch {
+ createNotification({
+ type: "error",
+ text: "Failed to create dynamic secret"
+ });
+ }
+ });
+ await Promise.all(promises);
};
return (
@@ -299,6 +312,29 @@ export const AwsElastiCacheInputForm = ({
+ {environments.length > 1 && (
+ (
+
+ option.name}
+ getOptionValue={(option) => option.slug}
+ />
+
+ )}
+ />
+ )}
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsIamInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsIamInputForm.tsx
index 1acb24e8f..f0590a4e1 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsIamInputForm.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsIamInputForm.tsx
@@ -5,9 +5,10 @@ import { z } from "zod";
import { TtlFormLabel } from "@app/components/features";
import { createNotification } from "@app/components/notifications";
-import { Button, FormControl, Input, TextArea } from "@app/components/v2";
+import { Button, FilterableSelect, FormControl, Input, TextArea } from "@app/components/v2";
import { useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
+import { WorkspaceEnv } from "@app/hooks/api/types";
const formSchema = z.object({
provider: z.object({
@@ -40,7 +41,8 @@ const formSchema = z.object({
if (valMs > 24 * 60 * 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
}),
- name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
+ name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
+ environments: z.object({ name: z.string(), slug: z.string() }).array()
});
type TForm = z.infer;
@@ -49,13 +51,13 @@ type Props = {
onCancel: () => void;
secretPath: string;
projectSlug: string;
- environment: string;
+ environments: WorkspaceEnv[];
};
export const AwsIamInputForm = ({
onCompleted,
onCancel,
- environment,
+ environments,
secretPath,
projectSlug
}: Props) => {
@@ -64,31 +66,43 @@ export const AwsIamInputForm = ({
formState: { isSubmitting },
handleSubmit
} = useForm({
- resolver: zodResolver(formSchema)
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ environments: environments.length === 1 ? environments : []
+ }
});
const createDynamicSecret = useCreateDynamicSecret();
- const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
+ const handleCreateDynamicSecret = async ({
+ name,
+ maxTTL,
+ provider,
+ defaultTTL,
+ environments: selectedEnvs
+ }: TForm) => {
// wait till previous request is finished
if (createDynamicSecret.isPending) return;
- try {
- await createDynamicSecret.mutateAsync({
- provider: { type: DynamicSecretProviders.AwsIam, inputs: provider },
- maxTTL,
- name,
- path: secretPath,
- defaultTTL,
- projectSlug,
- environmentSlug: environment
- });
- onCompleted();
- } catch {
- createNotification({
- type: "error",
- text: "Failed to create dynamic secret"
- });
- }
+ const promises = selectedEnvs.map(async (environment) => {
+ try {
+ await createDynamicSecret.mutateAsync({
+ provider: { type: DynamicSecretProviders.AwsIam, inputs: provider },
+ maxTTL,
+ name,
+ path: secretPath,
+ defaultTTL,
+ projectSlug,
+ environmentSlug: environment.slug
+ });
+ onCompleted();
+ } catch {
+ createNotification({
+ type: "error",
+ text: "Failed to create dynamic secret"
+ });
+ }
+ });
+ await Promise.all(promises);
};
return (
@@ -286,6 +300,30 @@ export const AwsIamInputForm = ({
)}
/>
+ {environments.length > 1 && (
+ (
+
+ option.name}
+ getOptionValue={(option) => option.slug}
+ menuPlacement="top"
+ />
+
+ )}
+ />
+ )}
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AzureEntraIdInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AzureEntraIdInputForm.tsx
index 55cebe173..4ab20c064 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AzureEntraIdInputForm.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AzureEntraIdInputForm.tsx
@@ -12,7 +12,7 @@ import { z } from "zod";
import { TtlFormLabel } from "@app/components/features";
import { createNotification } from "@app/components/notifications";
-import { Button, FormControl, Input } from "@app/components/v2";
+import { Button, FilterableSelect, FormControl, Input } from "@app/components/v2";
import {
DropdownMenu,
DropdownMenuContent,
@@ -23,6 +23,7 @@ import { Tooltip } from "@app/components/v2/Tooltip";
import { useCreateDynamicSecret } from "@app/hooks/api";
import { useGetDynamicSecretProviderData } from "@app/hooks/api/dynamicSecret/queries";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
+import { WorkspaceEnv } from "@app/hooks/api/types";
const formSchema = z.object({
selectedUsers: z.array(
@@ -60,7 +61,8 @@ const formSchema = z.object({
name: z
.string()
.min(1)
- .refine((val) => val.toLowerCase() === val, "Must be lowercase")
+ .refine((val) => val.toLowerCase() === val, "Must be lowercase"),
+ environments: z.object({ name: z.string(), slug: z.string() }).array()
});
type TForm = z.infer;
@@ -69,13 +71,13 @@ type Props = {
onCancel: () => void;
secretPath: string;
projectSlug: string;
- environment: string;
+ environments: WorkspaceEnv[];
};
export const AzureEntraIdInputForm = ({
onCompleted,
onCancel,
- environment,
+ environments,
secretPath,
projectSlug
}: Props) => {
@@ -85,7 +87,10 @@ export const AzureEntraIdInputForm = ({
watch,
handleSubmit
} = useForm({
- resolver: zodResolver(formSchema)
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ environments: environments.length === 1 ? environments : []
+ }
});
const tenantId = watch("provider.tenantId");
const applicationId = watch("provider.applicationId");
@@ -107,38 +112,42 @@ export const AzureEntraIdInputForm = ({
selectedUsers,
provider,
maxTTL,
- defaultTTL
+ defaultTTL,
+ environments: selectedEnvs
}: TForm) => {
// wait till previous request is finished
- if (createDynamicSecret.isPending) return;
- try {
- selectedUsers.map(async (user: { id: string; name: string; email: string }) => {
- await createDynamicSecret.mutateAsync({
- provider: {
- type: DynamicSecretProviders.AzureEntraId,
- inputs: {
- userId: user.id,
- tenantId: provider.tenantId,
- email: user.email,
- applicationId: provider.applicationId,
- clientSecret: provider.clientSecret
- }
- },
- maxTTL,
- name: `${name}-${user.name}`,
- path: secretPath,
- defaultTTL,
- projectSlug,
- environmentSlug: environment
+ const promises = selectedEnvs.map(async (environment) => {
+ if (createDynamicSecret.isPending) return;
+ try {
+ selectedUsers.map(async (user: { id: string; name: string; email: string }) => {
+ await createDynamicSecret.mutateAsync({
+ provider: {
+ type: DynamicSecretProviders.AzureEntraId,
+ inputs: {
+ userId: user.id,
+ tenantId: provider.tenantId,
+ email: user.email,
+ applicationId: provider.applicationId,
+ clientSecret: provider.clientSecret
+ }
+ },
+ maxTTL,
+ name: `${name}-${user.name}`,
+ path: secretPath,
+ defaultTTL,
+ projectSlug,
+ environmentSlug: environment.slug
+ });
});
- });
- onCompleted();
- } catch {
- createNotification({
- type: "error",
- text: "Failed to create dynamic secret"
- });
- }
+ onCompleted();
+ } catch {
+ createNotification({
+ type: "error",
+ text: "Failed to create dynamic secret"
+ });
+ }
+ });
+ await Promise.all(promises);
};
return (
@@ -373,6 +382,30 @@ export const AzureEntraIdInputForm = ({
+ {environments.length > 1 && (
+ (
+
+ option.name}
+ getOptionValue={(option) => option.slug}
+ menuPlacement="top"
+ />
+
+ )}
+ />
+ )}
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx
index c8389fb38..a49ab620e 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx
@@ -17,6 +17,7 @@ import { AnimatePresence, motion } from "framer-motion";
import { Modal, ModalContent } from "@app/components/v2";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
+import { WorkspaceEnv } from "@app/hooks/api/types";
import { AwsElastiCacheInputForm } from "./AwsElastiCacheInputForm";
import { AwsIamInputForm } from "./AwsIamInputForm";
@@ -38,7 +39,7 @@ type Props = {
isOpen?: boolean;
onToggle: (isOpen: boolean) => void;
projectSlug: string;
- environment: string;
+ environments: WorkspaceEnv[];
secretPath: string;
};
@@ -129,7 +130,7 @@ export const CreateDynamicSecretForm = ({
isOpen,
onToggle,
projectSlug,
- environment,
+ environments,
secretPath
}: Props) => {
const [wizardStep, setWizardStep] = useState(WizardSteps.SelectProvider);
@@ -197,7 +198,7 @@ export const CreateDynamicSecretForm = ({
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
- environment={environment}
+ environments={environments}
/>
)}
@@ -215,7 +216,7 @@ export const CreateDynamicSecretForm = ({
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
- environment={environment}
+ environments={environments}
/>
)}
@@ -233,7 +234,7 @@ export const CreateDynamicSecretForm = ({
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
- environment={environment}
+ environments={environments}
/>
)}
@@ -251,7 +252,7 @@ export const CreateDynamicSecretForm = ({
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
- environment={environment}
+ environments={environments}
/>
)}
@@ -269,7 +270,7 @@ export const CreateDynamicSecretForm = ({
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
- environment={environment}
+ environments={environments}
/>
)}
@@ -287,7 +288,7 @@ export const CreateDynamicSecretForm = ({
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
- environment={environment}
+ environments={environments}
/>
)}
@@ -305,7 +306,7 @@ export const CreateDynamicSecretForm = ({
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
- environment={environment}
+ environments={environments}
/>
)}
@@ -323,7 +324,7 @@ export const CreateDynamicSecretForm = ({
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
- environment={environment}
+ environments={environments}
/>
)}
@@ -341,7 +342,7 @@ export const CreateDynamicSecretForm = ({
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
- environment={environment}
+ environments={environments}
/>
)}
@@ -359,7 +360,7 @@ export const CreateDynamicSecretForm = ({
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
- environment={environment}
+ environments={environments}
/>
)}
@@ -377,7 +378,7 @@ export const CreateDynamicSecretForm = ({
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
- environment={environment}
+ environments={environments}
/>
)}
@@ -395,7 +396,7 @@ export const CreateDynamicSecretForm = ({
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
- environment={environment}
+ environments={environments}
/>
)}
@@ -413,7 +414,7 @@ export const CreateDynamicSecretForm = ({
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
- environment={environment}
+ environments={environments}
/>
)}
@@ -432,7 +433,7 @@ export const CreateDynamicSecretForm = ({
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
- environment={environment}
+ environments={environments}
/>
)}
@@ -450,7 +451,7 @@ export const CreateDynamicSecretForm = ({
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
- environment={environment}
+ environments={environments}
/>
)}
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/ElasticSearchInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/ElasticSearchInputForm.tsx
index eb0a7008f..828f98110 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/ElasticSearchInputForm.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/ElasticSearchInputForm.tsx
@@ -9,6 +9,7 @@ import { TtlFormLabel } from "@app/components/features";
import { createNotification } from "@app/components/notifications";
import {
Button,
+ FilterableSelect,
FormControl,
FormLabel,
IconButton,
@@ -19,6 +20,7 @@ import {
} from "@app/components/v2";
import { useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
+import { WorkspaceEnv } from "@app/hooks/api/types";
const authMethods = [
{
@@ -73,7 +75,8 @@ const formSchema = z.object({
if (valMs > 24 * 60 * 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
}),
- name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
+ name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
+ environments: z.object({ name: z.string(), slug: z.string() }).array()
});
type TForm = z.infer;
@@ -82,13 +85,13 @@ type Props = {
onCancel: () => void;
secretPath: string;
projectSlug: string;
- environment: string;
+ environments: WorkspaceEnv[];
};
export const ElasticSearchInputForm = ({
onCompleted,
onCancel,
- environment,
+ environments,
secretPath,
projectSlug
}: Props) => {
@@ -107,32 +110,42 @@ export const ElasticSearchInputForm = ({
},
roles: ["superuser"],
port: 443
- }
+ },
+ environments: environments.length === 1 ? environments : []
}
});
const createDynamicSecret = useCreateDynamicSecret();
- const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
+ const handleCreateDynamicSecret = async ({
+ name,
+ maxTTL,
+ provider,
+ defaultTTL,
+ environments: selectedEnvs
+ }: TForm) => {
// wait till previous request is finished
if (createDynamicSecret.isPending) return;
- try {
- await createDynamicSecret.mutateAsync({
- provider: { type: DynamicSecretProviders.ElasticSearch, inputs: provider },
- maxTTL,
- name,
- path: secretPath,
- defaultTTL,
- projectSlug,
- environmentSlug: environment
- });
- onCompleted();
- } catch {
- createNotification({
- type: "error",
- text: "Failed to create dynamic secret"
- });
- }
+ const promises = selectedEnvs.map(async (environment) => {
+ try {
+ await createDynamicSecret.mutateAsync({
+ provider: { type: DynamicSecretProviders.ElasticSearch, inputs: provider },
+ maxTTL,
+ name,
+ path: secretPath,
+ defaultTTL,
+ projectSlug,
+ environmentSlug: environment.slug
+ });
+ onCompleted();
+ } catch {
+ createNotification({
+ type: "error",
+ text: "Failed to create dynamic secret"
+ });
+ }
+ });
+ await Promise.all(promises);
};
const selectedAuthType = watch("provider.auth.type");
@@ -406,6 +419,30 @@ export const ElasticSearchInputForm = ({
)}
/>
+ {environments.length > 1 && (
+ (
+
+ option.name}
+ getOptionValue={(option) => option.slug}
+ menuPlacement="top"
+ />
+
+ )}
+ />
+ )}
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/LdapInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/LdapInputForm.tsx
index ef74db5b8..361067d1b 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/LdapInputForm.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/LdapInputForm.tsx
@@ -7,9 +7,18 @@ import { z } from "zod";
import { TtlFormLabel } from "@app/components/features";
import { createNotification } from "@app/components/notifications";
-import { Button, FormControl, Input, Select, SelectItem, TextArea } from "@app/components/v2";
+import {
+ Button,
+ FilterableSelect,
+ FormControl,
+ Input,
+ Select,
+ SelectItem,
+ TextArea
+} from "@app/components/v2";
import { useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
+import { WorkspaceEnv } from "@app/hooks/api/types";
enum CredentialType {
Dynamic = "dynamic",
@@ -69,7 +78,8 @@ const formSchema = z.object({
if (valMs > 24 * 60 * 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
}),
- name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
+ name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
+ environments: z.object({ name: z.string(), slug: z.string() }).array()
});
type TForm = z.infer;
@@ -79,7 +89,7 @@ type Props = {
onCancel: () => void;
secretPath: string;
projectSlug: string;
- environment: string;
+ environments: WorkspaceEnv[];
};
export const LdapInputForm = ({
@@ -87,7 +97,7 @@ export const LdapInputForm = ({
onCancel,
secretPath,
projectSlug,
- environment
+ environments
}: Props) => {
const {
control,
@@ -107,7 +117,8 @@ export const LdapInputForm = ({
revocationLdif: "",
rollbackLdif: "",
credentialType: CredentialType.Dynamic
- }
+ },
+ environments: environments.length === 1 ? environments : []
}
});
@@ -115,26 +126,35 @@ export const LdapInputForm = ({
const createDynamicSecret = useCreateDynamicSecret();
- const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
+ const handleCreateDynamicSecret = async ({
+ name,
+ maxTTL,
+ provider,
+ defaultTTL,
+ environments: selectedEnvs
+ }: TForm) => {
// wait till previous request is finished
if (createDynamicSecret.isPending) return;
- try {
- await createDynamicSecret.mutateAsync({
- provider: { type: DynamicSecretProviders.Ldap, inputs: provider },
- maxTTL,
- name,
- path: secretPath,
- defaultTTL,
- projectSlug,
- environmentSlug: environment
- });
- onCompleted();
- } catch {
- createNotification({
- type: "error",
- text: "Failed to create dynamic secret"
- });
- }
+ const promises = selectedEnvs.map(async (environment) => {
+ try {
+ await createDynamicSecret.mutateAsync({
+ provider: { type: DynamicSecretProviders.Ldap, inputs: provider },
+ maxTTL,
+ name,
+ path: secretPath,
+ defaultTTL,
+ projectSlug,
+ environmentSlug: environment.slug
+ });
+ onCompleted();
+ } catch {
+ createNotification({
+ type: "error",
+ text: "Failed to create dynamic secret"
+ });
+ }
+ });
+ await Promise.all(promises);
};
return (
@@ -369,6 +389,30 @@ export const LdapInputForm = ({
)}
/>
)}
+ {environments.length > 1 && (
+ (
+
+ option.name}
+ getOptionValue={(option) => option.slug}
+ menuPlacement="top"
+ />
+
+ )}
+ />
+ )}
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoAtlasInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoAtlasInputForm.tsx
index 4de9b8acc..a6725b652 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoAtlasInputForm.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoAtlasInputForm.tsx
@@ -13,6 +13,7 @@ import {
AccordionItem,
AccordionTrigger,
Button,
+ FilterableSelect,
FormControl,
FormLabel,
IconButton,
@@ -22,6 +23,7 @@ import {
} from "@app/components/v2";
import { useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
+import { WorkspaceEnv } from "@app/hooks/api/types";
const formSchema = z.object({
provider: z.object({
@@ -63,7 +65,8 @@ const formSchema = z.object({
if (valMs > 24 * 60 * 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
}),
- name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
+ name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
+ environments: z.object({ name: z.string(), slug: z.string() }).array()
});
type TForm = z.infer;
@@ -72,7 +75,7 @@ type Props = {
onCancel: () => void;
secretPath: string;
projectSlug: string;
- environment: string;
+ environments: WorkspaceEnv[];
};
const ATLAS_SCOPE_TYPES = [
@@ -93,7 +96,7 @@ const ATLAS_SCOPE_TYPES = [
export const MongoAtlasInputForm = ({
onCompleted,
onCancel,
- environment,
+ environments,
secretPath,
projectSlug
}: Props) => {
@@ -108,7 +111,8 @@ export const MongoAtlasInputForm = ({
defaultValues: {
provider: {
roles: [{ databaseName: "", roleName: "" }]
- }
+ },
+ environments: environments.length === 1 ? environments : []
}
});
@@ -124,26 +128,35 @@ export const MongoAtlasInputForm = ({
const createDynamicSecret = useCreateDynamicSecret();
- const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
+ const handleCreateDynamicSecret = async ({
+ name,
+ maxTTL,
+ provider,
+ defaultTTL,
+ environments: selectedEnvs
+ }: TForm) => {
// wait till previous request is finished
if (createDynamicSecret.isPending) return;
- try {
- await createDynamicSecret.mutateAsync({
- provider: { type: DynamicSecretProviders.MongoAtlas, inputs: provider },
- maxTTL,
- name,
- path: secretPath,
- defaultTTL,
- projectSlug,
- environmentSlug: environment
- });
- onCompleted();
- } catch {
- createNotification({
- type: "error",
- text: "Failed to create dynamic secret"
- });
- }
+ const promises = selectedEnvs.map(async (environment) => {
+ try {
+ await createDynamicSecret.mutateAsync({
+ provider: { type: DynamicSecretProviders.MongoAtlas, inputs: provider },
+ maxTTL,
+ name,
+ path: secretPath,
+ defaultTTL,
+ projectSlug,
+ environmentSlug: environment.slug
+ });
+ onCompleted();
+ } catch {
+ createNotification({
+ type: "error",
+ text: "Failed to create dynamic secret"
+ });
+ }
+ });
+ await Promise.all(promises);
};
return (
@@ -438,6 +451,30 @@ export const MongoAtlasInputForm = ({
+ {environments.length > 1 && (
+ (
+
+ option.name}
+ getOptionValue={(option) => option.slug}
+ menuPlacement="top"
+ />
+
+ )}
+ />
+ )}
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoDBInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoDBInputForm.tsx
index b2418f629..0a28a6913 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoDBInputForm.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoDBInputForm.tsx
@@ -7,9 +7,18 @@ import { z } from "zod";
import { TtlFormLabel } from "@app/components/features";
import { createNotification } from "@app/components/notifications";
-import { Button, FormControl, FormLabel, IconButton, Input, SecretInput } from "@app/components/v2";
+import {
+ Button,
+ FilterableSelect,
+ FormControl,
+ FormLabel,
+ IconButton,
+ Input,
+ SecretInput
+} from "@app/components/v2";
import { useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
+import { WorkspaceEnv } from "@app/hooks/api/types";
const formSchema = z.object({
provider: z.object({
@@ -46,7 +55,8 @@ const formSchema = z.object({
if (valMs > 24 * 60 * 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
}),
- name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
+ name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
+ environments: z.object({ name: z.string(), slug: z.string() }).array()
});
type TForm = z.infer;
@@ -55,13 +65,13 @@ type Props = {
onCancel: () => void;
secretPath: string;
projectSlug: string;
- environment: string;
+ environments: WorkspaceEnv[];
};
export const MongoDBDatabaseInputForm = ({
onCompleted,
onCancel,
- environment,
+ environments,
secretPath,
projectSlug
}: Props) => {
@@ -76,7 +86,8 @@ export const MongoDBDatabaseInputForm = ({
defaultValues: {
provider: {
roles: [{ roleName: "readWrite" }]
- }
+ },
+ environments: environments.length === 1 ? environments : []
}
});
@@ -87,33 +98,42 @@ export const MongoDBDatabaseInputForm = ({
const createDynamicSecret = useCreateDynamicSecret();
- const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
+ const handleCreateDynamicSecret = async ({
+ name,
+ maxTTL,
+ provider,
+ defaultTTL,
+ environments: selectedEnvs
+ }: TForm) => {
// wait till previous request is finished
if (createDynamicSecret.isPending) return;
- try {
- await createDynamicSecret.mutateAsync({
- provider: {
- type: DynamicSecretProviders.MongoDB,
- inputs: {
- ...provider,
- port: provider?.port ? provider.port : undefined,
- roles: provider.roles.map((el) => el.roleName)
- }
- },
- maxTTL,
- name,
- path: secretPath,
- defaultTTL,
- projectSlug,
- environmentSlug: environment
- });
- onCompleted();
- } catch {
- createNotification({
- type: "error",
- text: "Failed to create dynamic secret"
- });
- }
+ const promises = selectedEnvs.map(async (environment) => {
+ try {
+ await createDynamicSecret.mutateAsync({
+ provider: {
+ type: DynamicSecretProviders.MongoDB,
+ inputs: {
+ ...provider,
+ port: provider?.port ? provider.port : undefined,
+ roles: provider.roles.map((el) => el.roleName)
+ }
+ },
+ maxTTL,
+ name,
+ path: secretPath,
+ defaultTTL,
+ projectSlug,
+ environmentSlug: environment.slug
+ });
+ onCompleted();
+ } catch {
+ createNotification({
+ type: "error",
+ text: "Failed to create dynamic secret"
+ });
+ }
+ });
+ await Promise.all(promises);
};
return (
@@ -321,6 +341,30 @@ export const MongoDBDatabaseInputForm = ({
)}
/>
+ {environments.length > 1 && (
+ (
+
+ option.name}
+ getOptionValue={(option) => option.slug}
+ menuPlacement="top"
+ />
+
+ )}
+ />
+ )}
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RabbitMqInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RabbitMqInputForm.tsx
index 21f8b40de..0a6b84d41 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RabbitMqInputForm.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RabbitMqInputForm.tsx
@@ -7,9 +7,18 @@ import { z } from "zod";
import { TtlFormLabel } from "@app/components/features";
import { createNotification } from "@app/components/notifications";
-import { Button, FormControl, FormLabel, IconButton, Input, SecretInput } from "@app/components/v2";
+import {
+ Button,
+ FilterableSelect,
+ FormControl,
+ FormLabel,
+ IconButton,
+ Input,
+ SecretInput
+} from "@app/components/v2";
import { useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
+import { WorkspaceEnv } from "@app/hooks/api/types";
const formSchema = z.object({
provider: z.object({
@@ -50,7 +59,8 @@ const formSchema = z.object({
if (valMs > 24 * 60 * 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
}),
- name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
+ name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
+ environments: z.object({ name: z.string(), slug: z.string() }).array()
});
type TForm = z.infer;
@@ -59,13 +69,13 @@ type Props = {
onCancel: () => void;
secretPath: string;
projectSlug: string;
- environment: string;
+ environments: WorkspaceEnv[];
};
export const RabbitMqInputForm = ({
onCompleted,
onCancel,
- environment,
+ environments,
secretPath,
projectSlug
}: Props) => {
@@ -89,32 +99,42 @@ export const RabbitMqInputForm = ({
}
},
tags: []
- }
+ },
+ environments: environments.length === 1 ? environments : []
}
});
const createDynamicSecret = useCreateDynamicSecret();
- const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
+ const handleCreateDynamicSecret = async ({
+ name,
+ maxTTL,
+ provider,
+ defaultTTL,
+ environments: selectedEnvs
+ }: TForm) => {
// wait till previous request is finished
if (createDynamicSecret.isPending) return;
- try {
- await createDynamicSecret.mutateAsync({
- provider: { type: DynamicSecretProviders.RabbitMq, inputs: provider },
- maxTTL,
- name,
- path: secretPath,
- defaultTTL,
- projectSlug,
- environmentSlug: environment
- });
- onCompleted();
- } catch {
- createNotification({
- type: "error",
- text: "Failed to create dynamic secret"
- });
- }
+ const promises = selectedEnvs.map(async (environment) => {
+ try {
+ await createDynamicSecret.mutateAsync({
+ provider: { type: DynamicSecretProviders.RabbitMq, inputs: provider },
+ maxTTL,
+ name,
+ path: secretPath,
+ defaultTTL,
+ projectSlug,
+ environmentSlug: environment.slug
+ });
+ onCompleted();
+ } catch {
+ createNotification({
+ type: "error",
+ text: "Failed to create dynamic secret"
+ });
+ }
+ });
+ await Promise.all(promises);
};
const selectedTags = watch("provider.tags");
@@ -405,6 +425,30 @@ export const RabbitMqInputForm = ({
)}
/>
+ {environments.length > 1 && (
+ (
+
+ option.name}
+ getOptionValue={(option) => option.slug}
+ menuPlacement="top"
+ />
+
+ )}
+ />
+ )}
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RedisInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RedisInputForm.tsx
index 9d9933b44..35f165056 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RedisInputForm.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RedisInputForm.tsx
@@ -11,6 +11,7 @@ import {
AccordionItem,
AccordionTrigger,
Button,
+ FilterableSelect,
FormControl,
Input,
SecretInput,
@@ -18,6 +19,7 @@ import {
} from "@app/components/v2";
import { useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
+import { WorkspaceEnv } from "@app/hooks/api/types";
const formSchema = z.object({
provider: z.object({
@@ -50,7 +52,8 @@ const formSchema = z.object({
if (valMs > 24 * 60 * 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
}),
- name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
+ name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
+ environments: z.object({ name: z.string(), slug: z.string() }).array()
});
type TForm = z.infer;
@@ -59,13 +62,13 @@ type Props = {
onCancel: () => void;
secretPath: string;
projectSlug: string;
- environment: string;
+ environments: WorkspaceEnv[];
};
export const RedisInputForm = ({
onCompleted,
onCancel,
- environment,
+ environments,
secretPath,
projectSlug
}: Props) => {
@@ -81,32 +84,42 @@ export const RedisInputForm = ({
port: 6379,
creationStatement: "ACL SETUSER {{username}} on >{{password}} ~* &* +@all",
revocationStatement: "ACL DELUSER {{username}}"
- }
+ },
+ environments: environments.length === 1 ? environments : []
}
});
const createDynamicSecret = useCreateDynamicSecret();
- const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
+ const handleCreateDynamicSecret = async ({
+ name,
+ maxTTL,
+ provider,
+ defaultTTL,
+ environments: selectedEnvs
+ }: TForm) => {
// wait till previous request is finished
if (createDynamicSecret.isPending) return;
- try {
- await createDynamicSecret.mutateAsync({
- provider: { type: DynamicSecretProviders.Redis, inputs: provider },
- maxTTL,
- name,
- path: secretPath,
- defaultTTL,
- projectSlug,
- environmentSlug: environment
- });
- onCompleted();
- } catch {
- createNotification({
- type: "error",
- text: "Failed to create dynamic secret"
- });
- }
+ const promises = selectedEnvs.map(async (environment) => {
+ try {
+ await createDynamicSecret.mutateAsync({
+ provider: { type: DynamicSecretProviders.Redis, inputs: provider },
+ maxTTL,
+ name,
+ path: secretPath,
+ defaultTTL,
+ projectSlug,
+ environmentSlug: environment.slug
+ });
+ onCompleted();
+ } catch {
+ createNotification({
+ type: "error",
+ text: "Failed to create dynamic secret"
+ });
+ }
+ });
+ await Promise.all(promises);
};
return (
@@ -313,6 +326,30 @@ export const RedisInputForm = ({
+ {environments.length > 1 && (
+ (
+
+ option.name}
+ getOptionValue={(option) => option.slug}
+ menuPlacement="top"
+ />
+
+ )}
+ />
+ )}
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapAseInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapAseInputForm.tsx
index efe6e0269..de32565da 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapAseInputForm.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapAseInputForm.tsx
@@ -11,12 +11,14 @@ import {
AccordionItem,
AccordionTrigger,
Button,
+ FilterableSelect,
FormControl,
Input,
TextArea
} from "@app/components/v2";
import { useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
+import { WorkspaceEnv } from "@app/hooks/api/types";
const formSchema = z.object({
provider: z.object({
@@ -48,7 +50,8 @@ const formSchema = z.object({
if (valMs > 24 * 60 * 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
}),
- name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
+ name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
+ environments: z.object({ name: z.string(), slug: z.string() }).array()
});
type TForm = z.infer;
@@ -57,13 +60,13 @@ type Props = {
onCancel: () => void;
secretPath: string;
projectSlug: string;
- environment: string;
+ environments: WorkspaceEnv[];
};
export const SapAseInputForm = ({
onCompleted,
onCancel,
- environment,
+ environments,
secretPath,
projectSlug
}: Props) => {
@@ -82,32 +85,42 @@ sp_adduser '{{username}}', '{{username}}', null;
sp_role 'grant', 'mon_role', '{{username}}';`,
revocationStatement: `sp_dropuser '{{username}}';
sp_droplogin '{{username}}';`
- }
+ },
+ environments: environments.length === 1 ? environments : []
}
});
const createDynamicSecret = useCreateDynamicSecret();
- const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
+ const handleCreateDynamicSecret = async ({
+ name,
+ maxTTL,
+ provider,
+ defaultTTL,
+ environments: selectedEnvs
+ }: TForm) => {
// wait till previous request is finished
if (createDynamicSecret.isPending) return;
- try {
- await createDynamicSecret.mutateAsync({
- provider: { type: DynamicSecretProviders.SapAse, inputs: provider },
- maxTTL,
- name,
- path: secretPath,
- defaultTTL,
- projectSlug,
- environmentSlug: environment
- });
- onCompleted();
- } catch {
- createNotification({
- type: "error",
- text: "Failed to create dynamic secret"
- });
- }
+ const promises = selectedEnvs.map(async (environment) => {
+ try {
+ await createDynamicSecret.mutateAsync({
+ provider: { type: DynamicSecretProviders.SapAse, inputs: provider },
+ maxTTL,
+ name,
+ path: secretPath,
+ defaultTTL,
+ projectSlug,
+ environmentSlug: environment.slug
+ });
+ onCompleted();
+ } catch {
+ createNotification({
+ type: "error",
+ text: "Failed to create dynamic secret"
+ });
+ }
+ });
+ await Promise.all(promises);
};
return (
@@ -291,6 +304,30 @@ sp_droplogin '{{username}}';`
+ {environments.length > 1 && (
+ (
+
+ option.name}
+ getOptionValue={(option) => option.slug}
+ menuPlacement="top"
+ />
+
+ )}
+ />
+ )}
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapHanaInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapHanaInputForm.tsx
index 1b0bd64b8..d31464665 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapHanaInputForm.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapHanaInputForm.tsx
@@ -11,6 +11,7 @@ import {
AccordionItem,
AccordionTrigger,
Button,
+ FilterableSelect,
FormControl,
Input,
SecretInput,
@@ -18,6 +19,7 @@ import {
} from "@app/components/v2";
import { useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
+import { WorkspaceEnv } from "@app/hooks/api/types";
const formSchema = z.object({
provider: z.object({
@@ -50,7 +52,8 @@ const formSchema = z.object({
if (valMs > 24 * 60 * 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
}),
- name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
+ name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
+ environments: z.object({ name: z.string(), slug: z.string() }).array()
});
type TForm = z.infer;
@@ -59,13 +62,13 @@ type Props = {
onCancel: () => void;
secretPath: string;
projectSlug: string;
- environment: string;
+ environments: WorkspaceEnv[];
};
export const SapHanaInputForm = ({
onCompleted,
onCancel,
- environment,
+ environments,
secretPath,
projectSlug
}: Props) => {
@@ -82,32 +85,42 @@ GRANT "MONITORING" TO {{username}};`,
revocationStatement: `REVOKE "MONITORING" FROM {{username}};
DROP USER {{username}};`,
renewStatement: "ALTER USER {{username}} VALID UNTIL '{{expiration}}';"
- }
+ },
+ environments: environments.length === 1 ? environments : []
}
});
const createDynamicSecret = useCreateDynamicSecret();
- const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
+ const handleCreateDynamicSecret = async ({
+ name,
+ maxTTL,
+ provider,
+ defaultTTL,
+ environments: selectedEnvs
+ }: TForm) => {
// wait till previous request is finished
if (createDynamicSecret.isPending) return;
- try {
- await createDynamicSecret.mutateAsync({
- provider: { type: DynamicSecretProviders.SapHana, inputs: provider },
- maxTTL,
- name,
- path: secretPath,
- defaultTTL,
- projectSlug,
- environmentSlug: environment
- });
- onCompleted();
- } catch {
- createNotification({
- type: "error",
- text: "Failed to create dynamic secret"
- });
- }
+ const promises = selectedEnvs.map(async (environment) => {
+ try {
+ await createDynamicSecret.mutateAsync({
+ provider: { type: DynamicSecretProviders.SapHana, inputs: provider },
+ maxTTL,
+ name,
+ path: secretPath,
+ defaultTTL,
+ projectSlug,
+ environmentSlug: environment.slug
+ });
+ onCompleted();
+ } catch {
+ createNotification({
+ type: "error",
+ text: "Failed to create dynamic secret"
+ });
+ }
+ });
+ await Promise.all(promises);
};
return (
@@ -311,6 +324,30 @@ DROP USER {{username}};`,
+ {environments.length > 1 && (
+ (
+
+ option.name}
+ getOptionValue={(option) => option.slug}
+ menuPlacement="top"
+ />
+
+ )}
+ />
+ )}
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SnowflakeInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SnowflakeInputForm.tsx
index a5f8b25d9..c5077f2ad 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SnowflakeInputForm.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SnowflakeInputForm.tsx
@@ -13,12 +13,14 @@ import {
AccordionItem,
AccordionTrigger,
Button,
+ FilterableSelect,
FormControl,
Input,
TextArea
} from "@app/components/v2";
import { useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
+import { WorkspaceEnv } from "@app/hooks/api/types";
const formSchema = z.object({
provider: z.object({
@@ -54,7 +56,8 @@ const formSchema = z.object({
.string()
.trim()
.min(1)
- .refine((val) => val.toLowerCase() === val, "Must be lowercase")
+ .refine((val) => val.toLowerCase() === val, "Must be lowercase"),
+ environments: z.object({ name: z.string(), slug: z.string() }).array()
});
type TForm = z.infer;
@@ -63,13 +66,13 @@ type Props = {
onCancel: () => void;
secretPath: string;
projectSlug: string;
- environment: string;
+ environments: WorkspaceEnv[];
};
export const SnowflakeInputForm = ({
onCompleted,
onCancel,
- environment,
+ environments,
secretPath,
projectSlug
}: Props) => {
@@ -85,32 +88,42 @@ export const SnowflakeInputForm = ({
"CREATE USER {{username}} PASSWORD = '{{password}}' DEFAULT_ROLE = public DEFAULT_SECONDARY_ROLES = ('ALL') MUST_CHANGE_PASSWORD = FALSE DAYS_TO_EXPIRY = {{expiration}};",
revocationStatement: "DROP USER {{username}};",
renewStatement: "ALTER USER {{username}} SET DAYS_TO_EXPIRY = {{expiration}};"
- }
+ },
+ environments: environments.length === 1 ? environments : []
}
});
const createDynamicSecret = useCreateDynamicSecret();
- const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
+ const handleCreateDynamicSecret = async ({
+ name,
+ maxTTL,
+ provider,
+ defaultTTL,
+ environments: selectedEnvs
+ }: TForm) => {
// wait till previous request is finished
if (createDynamicSecret.isPending) return;
- try {
- await createDynamicSecret.mutateAsync({
- provider: { type: DynamicSecretProviders.Snowflake, inputs: provider },
- maxTTL,
- name,
- path: secretPath,
- defaultTTL,
- projectSlug,
- environmentSlug: environment
- });
- onCompleted();
- } catch (err) {
- createNotification({
- type: "error",
- text: err instanceof Error ? err.message : "Failed to create dynamic secret"
- });
- }
+ const promises = selectedEnvs.map(async (environment) => {
+ try {
+ await createDynamicSecret.mutateAsync({
+ provider: { type: DynamicSecretProviders.Snowflake, inputs: provider },
+ maxTTL,
+ name,
+ path: secretPath,
+ defaultTTL,
+ projectSlug,
+ environmentSlug: environment.slug
+ });
+ onCompleted();
+ } catch (err) {
+ createNotification({
+ type: "error",
+ text: err instanceof Error ? err.message : "Failed to create dynamic secret"
+ });
+ }
+ });
+ await Promise.all(promises);
};
return (
@@ -314,6 +327,30 @@ export const SnowflakeInputForm = ({
+ {environments.length > 1 && (
+ (
+
+ option.name}
+ getOptionValue={(option) => option.slug}
+ menuPlacement="top"
+ />
+
+ )}
+ />
+ )}
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx
index 4af92b210..416d9c35b 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx
@@ -12,6 +12,7 @@ import {
AccordionItem,
AccordionTrigger,
Button,
+ FilterableSelect,
FormControl,
Input,
SecretInput,
@@ -22,6 +23,7 @@ import {
import { useWorkspace } from "@app/context";
import { gatewaysQueryKeys, useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders, SqlProviders } from "@app/hooks/api/dynamicSecret/types";
+import { WorkspaceEnv } from "@app/hooks/api/types";
const passwordRequirementsSchema = z
.object({
@@ -79,7 +81,8 @@ const formSchema = z.object({
if (valMs > 24 * 60 * 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
}),
- name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
+ name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
+ environments: z.object({ name: z.string(), slug: z.string() }).array()
});
type TForm = z.infer;
@@ -88,7 +91,7 @@ type Props = {
onCancel: () => void;
secretPath: string;
projectSlug: string;
- environment: string;
+ environments: WorkspaceEnv[];
};
const getSqlStatements = (provider: SqlProviders) => {
@@ -145,7 +148,7 @@ const getDefaultPort = (provider: SqlProviders) => {
export const SqlDatabaseInputForm = ({
onCompleted,
onCancel,
- environment,
+ environments,
secretPath,
projectSlug
}: Props) => {
@@ -172,7 +175,8 @@ export const SqlDatabaseInputForm = ({
},
allowedSymbols: "-_.~!*"
}
- }
+ },
+ environments: environments.length === 1 ? environments : []
}
});
@@ -181,26 +185,35 @@ export const SqlDatabaseInputForm = ({
gatewaysQueryKeys.listProjectGateways({ projectId: currentWorkspace.id })
);
- const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
+ const handleCreateDynamicSecret = async ({
+ name,
+ maxTTL,
+ provider,
+ defaultTTL,
+ environments: selectedEnvs
+ }: TForm) => {
// wait till previous request is finished
if (createDynamicSecret.isPending) return;
- try {
- await createDynamicSecret.mutateAsync({
- provider: { type: DynamicSecretProviders.SqlDatabase, inputs: provider },
- maxTTL,
- name,
- path: secretPath,
- defaultTTL,
- projectSlug,
- environmentSlug: environment
- });
- onCompleted();
- } catch {
- createNotification({
- type: "error",
- text: "Failed to create dynamic secret"
- });
- }
+ const promises = selectedEnvs.map(async (env) => {
+ try {
+ await createDynamicSecret.mutateAsync({
+ provider: { type: DynamicSecretProviders.SqlDatabase, inputs: provider },
+ maxTTL,
+ name,
+ path: secretPath,
+ defaultTTL,
+ projectSlug,
+ environmentSlug: env.slug
+ });
+ onCompleted();
+ } catch {
+ createNotification({
+ type: "error",
+ text: "Failed to create dynamic secret"
+ });
+ }
+ });
+ await Promise.all(promises);
};
const handleDatabaseChange = (type: SqlProviders) => {
@@ -649,6 +662,30 @@ export const SqlDatabaseInputForm = ({
+ {environments.length > 1 && (
+ (
+
+ option.name}
+ getOptionValue={(option) => option.slug}
+ menuPlacement="top"
+ />
+
+ )}
+ />
+ )}
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/TotpInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/TotpInputForm.tsx
index 01d5398f0..4ff83a741 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/TotpInputForm.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/TotpInputForm.tsx
@@ -5,9 +5,17 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
-import { Button, FormControl, Input, Select, SelectItem } from "@app/components/v2";
+import {
+ Button,
+ FilterableSelect,
+ FormControl,
+ Input,
+ Select,
+ SelectItem
+} from "@app/components/v2";
import { useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
+import { WorkspaceEnv } from "@app/hooks/api/types";
enum ConfigType {
URL = "url",
@@ -52,7 +60,8 @@ const formSchema = z.object({
.string()
.trim()
.min(1)
- .refine((val) => val.toLowerCase() === val, "Must be lowercase")
+ .refine((val) => val.toLowerCase() === val, "Must be lowercase"),
+ environments: z.object({ name: z.string(), slug: z.string() }).array()
});
type TForm = z.infer;
@@ -62,13 +71,13 @@ type Props = {
onCancel: () => void;
secretPath: string;
projectSlug: string;
- environment: string;
+ environments: WorkspaceEnv[];
};
export const TotpInputForm = ({
onCompleted,
onCancel,
- environment,
+ environments,
secretPath,
projectSlug
}: Props) => {
@@ -82,7 +91,8 @@ export const TotpInputForm = ({
defaultValues: {
provider: {
configType: ConfigType.URL
- }
+ },
+ environments: environments.length === 1 ? environments : []
}
});
@@ -90,26 +100,33 @@ export const TotpInputForm = ({
const createDynamicSecret = useCreateDynamicSecret();
- const handleCreateDynamicSecret = async ({ name, provider }: TForm) => {
+ const handleCreateDynamicSecret = async ({
+ name,
+ provider,
+ environments: selectedEnvs
+ }: TForm) => {
// wait till previous request is finished
if (createDynamicSecret.isPending) return;
- try {
- await createDynamicSecret.mutateAsync({
- provider: { type: DynamicSecretProviders.Totp, inputs: provider },
- maxTTL: "24h",
- name,
- path: secretPath,
- defaultTTL: "1m",
- projectSlug,
- environmentSlug: environment
- });
- onCompleted();
- } catch (err) {
- createNotification({
- type: "error",
- text: err instanceof Error ? err.message : "Failed to create dynamic secret"
- });
- }
+ const promises = selectedEnvs.map(async (environment) => {
+ try {
+ await createDynamicSecret.mutateAsync({
+ provider: { type: DynamicSecretProviders.Totp, inputs: provider },
+ maxTTL: "24h",
+ name,
+ path: secretPath,
+ defaultTTL: "1m",
+ projectSlug,
+ environmentSlug: environment.slug
+ });
+ onCompleted();
+ } catch (err) {
+ createNotification({
+ type: "error",
+ text: err instanceof Error ? err.message : "Failed to create dynamic secret"
+ });
+ }
+ });
+ await Promise.all(promises);
};
return (
@@ -295,6 +312,30 @@ export const TotpInputForm = ({
>
)}
+ {environments.length > 1 && (
+ (
+
+ option.name}
+ getOptionValue={(option) => option.slug}
+ menuPlacement="top"
+ />
+
+ )}
+ />
+ )}