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
+20
View File
@@ -49,3 +49,23 @@ In the image below, you can see that the **Development** environment is the only
in the `/users` folder, being other folders `/user-a`, `/user-b`, ... `/user-f`. 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

@@ -380,7 +380,7 @@ export const ActionBar = ({
} }
}; };
// Replicate Folder Logic // Replicate Secrets Logic
const createSecretCount = Object.keys( const createSecretCount = Object.keys(
(popUp.confirmUpload?.data as TSecOverwriteOpt)?.create || {} (popUp.confirmUpload?.data as TSecOverwriteOpt)?.create || {}
).length; ).length;
@@ -439,17 +439,22 @@ export const ActionBar = ({
const processBatches = async () => { const processBatches = async () => {
await secretBatches.reduce(async (previous, batch) => { await secretBatches.reduce(async (previous, batch) => {
await previous; await previous;
try {
const { secrets: batchSecrets } = await fetchDashboardProjectSecretsByKeys({
secretPath: normalizedPath,
environment,
projectId,
keys: batch
});
const { secrets: batchSecrets } = await fetchDashboardProjectSecretsByKeys({ batchSecrets.forEach((secret) => {
secretPath: normalizedPath, existingSecretLookup.add(`${normalizedPath}-${secret.secretKey}`);
environment, });
projectId, } catch (error) {
keys: batch if (!(error instanceof AxiosError && error.response?.status === 404)) {
}); throw error;
}
batchSecrets.forEach((secret) => { }
existingSecretLookup.add(`${normalizedPath}-${secret.secretKey}`);
});
}, Promise.resolve()); }, Promise.resolve());
}; };
@@ -1050,7 +1055,7 @@ export const ActionBar = ({
className="h-10 text-left" className="h-10 text-left"
isFullWidth isFullWidth
> >
Replicate Folder Replicate Secrets
</Button> </Button>
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>
@@ -1,6 +1,6 @@
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { Controller, useForm } from "react-hook-form"; 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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
@@ -226,8 +226,27 @@ export const ReplicateFolderFromBoard = ({
<ModalContent <ModalContent
bodyClassName="overflow-visible" bodyClassName="overflow-visible"
className="max-w-2xl" className="max-w-2xl"
title="Replicate Folder Content From An Environment" title={
subTitle="Replicate folder content from other environments into this context" <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)}> <form onSubmit={handleSubmit(handleFormSubmit)}>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
@@ -235,7 +254,12 @@ export const ReplicateFolderFromBoard = ({
control={control} control={control}
name="environment" name="environment"
render={({ field: { value, onChange } }) => ( 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 <FilterableSelect
value={value} value={value}
onChange={onChange} onChange={onChange}
@@ -251,7 +275,12 @@ export const ReplicateFolderFromBoard = ({
control={control} control={control}
name="secretPath" name="secretPath"
render={({ field }) => ( 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 <SecretPathInput
{...field} {...field}
placeholder="Provide a path, default is /" placeholder="Provide a path, default is /"
@@ -261,44 +290,49 @@ export const ReplicateFolderFromBoard = ({
)} )}
/> />
</div> </div>
<div className="border-t border-mineshaft-600 pt-4"> <Controller
<Controller control={control}
control={control} name="secrets"
name="secrets" render={({ field: { onChange } }) => (
render={({ field: { onChange } }) => ( <FormControl
<FormControl className="flex-grow" isRequired> tooltipText="The folders and secrets to replicate into this context"
<SecretTreeView className="flex-grow"
data={secretsFilteredByPath} label="Affected Subjects"
basePath={debouncedEnvCopySecretPath} isRequired
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 <SecretTreeView
</Switch> data={secretsFilteredByPath}
</div> basePath={debouncedEnvCopySecretPath}
<div className="flex items-center space-x-4"> onChange={onChange}
<Button />
leftIcon={<FontAwesomeIcon icon={faClone} />} </FormControl>
type="submit" )}
isDisabled={!selectedSecrets || selectedSecrets.length === 0} />
> <div className="my-6 ml-2">
Replicate Folder <Switch
</Button> id="populate-include-value"
<Button variant="plain" colorSchema="secondary" onClick={() => onToggle(false)}> isChecked={shouldIncludeValues}
Cancel onCheckedChange={(isChecked) => {
</Button> setValue("secrets", []);
</div> 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> </div>
</form> </form>
</ModalContent> </ModalContent>