mirror of
https://github.com/Infisical/infisical.git
synced 2026-01-09 07:28:09 -05:00
improve discoverability and UX for replicate folders/secrets feature
This commit is contained in:
@@ -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`.
|
||||
|
||||

|
||||

|
||||
|
||||
### 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.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
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**.
|
||||
|
||||

|
||||
|
||||
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>
|
||||
|
||||
BIN
docs/images/platform/folder/replicate-secrets-modal.png
Normal file
BIN
docs/images/platform/folder/replicate-secrets-modal.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 577 KiB |
BIN
docs/images/platform/folder/replicate-secrets-result.png
Normal file
BIN
docs/images/platform/folder/replicate-secrets-result.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 919 KiB |
BIN
docs/images/platform/folder/replicate-secrets.png
Normal file
BIN
docs/images/platform/folder/replicate-secrets.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 934 KiB |
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user