improve discoverability and UX for replicate folders/secrets feature

This commit is contained in:
x032205
2025-10-04 03:40:14 -04:00
parent 57eb8af155
commit 79278b03e0
6 changed files with 119 additions and 60 deletions

View File

@@ -3,10 +3,10 @@ title: "Folders"
description: "Learn how to organize secrets with folders."
---
Infisical Folders enable users to organize secrets using custom structures dependent on the intended use case (also known as **path-based secret storage**).
Infisical Folders enable users to organize secrets using custom structures dependent on the intended use case (also known as **path-based secret storage**).
It is great for organizing secrets around hierarchies with multiple services or types of secrets involved at large quantities.
Infisical Folders can be infinitely nested to mirror your application architecture  whether it's microservices, monorepos,
It is great for organizing secrets around hierarchies with multiple services or types of secrets involved at large quantities.
Infisical Folders can be infinitely nested to mirror your application architecture  whether it's microservices, monorepos,
or any logical grouping that best suits your needs.
Consider the following structure for a microservice architecture:
@@ -22,7 +22,7 @@ Consider the following structure for a microservice architecture:
...
```
In this example, we store environment variables for each microservice under each respective `/envars` folder.
In this example, we store environment variables for each microservice under each respective `/envars` folder.
We also store user-specific secrets for micro-service 1 under `/service1/users`. With this folder structure in place, your applications only need to specify a path like `/microservice1/envars` to fetch secrets from there.
By extending this example, you can see how path-based secret storage provides a versatile approach to manage secrets for any architecture.
@@ -45,7 +45,27 @@ To delete a folder, hover over it and press the **X** button that appears on the
It's possible to compare the contents of folders across environments in the **Secrets Overview** page.
When you click on a folder, the table will display the items within it across environments.
In the image below, you can see that the **Development** environment is the only one that contains items
In the image below, you can see that the **Development** environment is the only one that contains items
in the `/users` folder, being other folders `/user-a`, `/user-b`, ... `/user-f`.
![comparing folders](../../images/platform/folder/folders-secrets-overview.png)
![comparing folders](../../images/platform/folder/folders-secrets-overview.png)
### Replicating Folder Contents
If you want to copy secrets or folders from one path to another, you can utilize the **Replicate Secrets** functionality located in the **Add Secret** dropdown.
![replicate secrets](../../images/platform/folder/replicate-secrets.png)
![replicate secrets modal](../../images/platform/folder/replicate-secrets-modal.png)
First, select the **Source Environment** and the **Source Root Path** you want to copy secrets *from*. In the example provided, we select `/dev-folder` as the source root path from the Development environment. This means any secrets within `/dev-folder` from Development will be replicated. By default, these secrets are copied into the *currently active* folder/path in your target environment (e.g., the root folder of your Staging environment in this scenario).
As a final step, you can select the specific secrets you wish to copy and then click **Replicate Secrets**.
![replicate secrets modal](../../images/platform/folder/replicate-secrets-result.png)
The result shows two secrets successfully copied from the `/dev-folder` in the Development environment into the root folder of the Staging environment.
<Info>
If you do not select a **Source Root Path**, the replication will consider the contents of the *entire root* of the **Source Environment** (e.g., the Development environment). In this example that would mean copying the `/dev-folder` itself rather than just it's contents.
</Info>

Binary file not shown.

After

Width:  |  Height:  |  Size: 577 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 919 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 934 KiB

View File

@@ -380,7 +380,7 @@ export const ActionBar = ({
}
};
// Replicate Folder Logic
// Replicate Secrets Logic
const createSecretCount = Object.keys(
(popUp.confirmUpload?.data as TSecOverwriteOpt)?.create || {}
).length;
@@ -439,17 +439,22 @@ export const ActionBar = ({
const processBatches = async () => {
await secretBatches.reduce(async (previous, batch) => {
await previous;
try {
const { secrets: batchSecrets } = await fetchDashboardProjectSecretsByKeys({
secretPath: normalizedPath,
environment,
projectId,
keys: batch
});
const { secrets: batchSecrets } = await fetchDashboardProjectSecretsByKeys({
secretPath: normalizedPath,
environment,
projectId,
keys: batch
});
batchSecrets.forEach((secret) => {
existingSecretLookup.add(`${normalizedPath}-${secret.secretKey}`);
});
batchSecrets.forEach((secret) => {
existingSecretLookup.add(`${normalizedPath}-${secret.secretKey}`);
});
} catch (error) {
if (!(error instanceof AxiosError && error.response?.status === 404)) {
throw error;
}
}
}, Promise.resolve());
};
@@ -1050,7 +1055,7 @@ export const ActionBar = ({
className="h-10 text-left"
isFullWidth
>
Replicate Folder
Replicate Secrets
</Button>
)}
</ProjectPermissionCan>

View File

@@ -1,6 +1,6 @@
import { useEffect, useMemo, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { faClone } from "@fortawesome/free-solid-svg-icons";
import { faArrowUpRightFromSquare, faBookOpen, faClone } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
@@ -226,8 +226,27 @@ export const ReplicateFolderFromBoard = ({
<ModalContent
bodyClassName="overflow-visible"
className="max-w-2xl"
title="Replicate Folder Content From An Environment"
subTitle="Replicate folder content from other environments into this context"
title={
<div className="flex items-center gap-1 text-mineshaft-300">
<span>Replicate Secrets</span>
<a
target="_blank"
href="https://infisical.com/docs/documentation/platform/folder#replicating-folder-contents"
className="mb-1 ml-1"
rel="noopener noreferrer"
>
<div className="inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mb-[0.03rem] mr-1 text-[12px]" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1 text-[10px]"
/>
</div>
</a>
</div>
}
subTitle="Copy folder contents from other locations into this context"
>
<form onSubmit={handleSubmit(handleFormSubmit)}>
<div className="flex items-center space-x-2">
@@ -235,7 +254,12 @@ export const ReplicateFolderFromBoard = ({
control={control}
name="environment"
render={({ field: { value, onChange } }) => (
<FormControl label="Environment" isRequired className="w-1/3">
<FormControl
tooltipText="The environment to replicate secrets from"
label="Source Environment"
isRequired
className="w-1/3"
>
<FilterableSelect
value={value}
onChange={onChange}
@@ -251,7 +275,12 @@ export const ReplicateFolderFromBoard = ({
control={control}
name="secretPath"
render={({ field }) => (
<FormControl label="Secret Path" className="flex-grow" isRequired>
<FormControl
tooltipText="The folder to use as a root for replication. Using /foo as a root for /foo/bar will result in /bar being copied to this context."
label="Source Root Path"
className="flex-grow"
isRequired
>
<SecretPathInput
{...field}
placeholder="Provide a path, default is /"
@@ -261,44 +290,49 @@ export const ReplicateFolderFromBoard = ({
)}
/>
</div>
<div className="border-t border-mineshaft-600 pt-4">
<Controller
control={control}
name="secrets"
render={({ field: { onChange } }) => (
<FormControl className="flex-grow" isRequired>
<SecretTreeView
data={secretsFilteredByPath}
basePath={debouncedEnvCopySecretPath}
onChange={onChange}
/>
</FormControl>
)}
/>
<div className="my-6 ml-2">
<Switch
id="populate-include-value"
isChecked={shouldIncludeValues}
onCheckedChange={(isChecked) => {
setValue("secrets", []);
setShouldIncludeValues(isChecked as boolean);
}}
<Controller
control={control}
name="secrets"
render={({ field: { onChange } }) => (
<FormControl
tooltipText="The folders and secrets to replicate into this context"
className="flex-grow"
label="Affected Subjects"
isRequired
>
Include secret values
</Switch>
</div>
<div className="flex items-center space-x-4">
<Button
leftIcon={<FontAwesomeIcon icon={faClone} />}
type="submit"
isDisabled={!selectedSecrets || selectedSecrets.length === 0}
>
Replicate Folder
</Button>
<Button variant="plain" colorSchema="secondary" onClick={() => onToggle(false)}>
Cancel
</Button>
</div>
<SecretTreeView
data={secretsFilteredByPath}
basePath={debouncedEnvCopySecretPath}
onChange={onChange}
/>
</FormControl>
)}
/>
<div className="my-6 ml-2">
<Switch
id="populate-include-value"
isChecked={shouldIncludeValues}
onCheckedChange={(isChecked) => {
setValue("secrets", []);
setShouldIncludeValues(isChecked as boolean);
}}
>
Include secret values
</Switch>
</div>
<div className="flex items-center space-x-4">
<Button
leftIcon={<FontAwesomeIcon icon={faClone} />}
type="submit"
variant="outline_bg"
colorSchema="secondary"
isDisabled={!selectedSecrets || selectedSecrets.length === 0}
>
Replicate Secrets
</Button>
<Button variant="plain" colorSchema="secondary" onClick={() => onToggle(false)}>
Cancel
</Button>
</div>
</form>
</ModalContent>