Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: bulk save secrets #2294

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ type Props = Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, "onChange" | "val
secretPath?: string;
environment?: string;
containerClassName?: string;
handleSecretInputChange?: (val: string) => void;
};

type ReferenceItem = {
Expand All @@ -70,6 +71,7 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
containerClassName,
secretPath: propSecretPath,
environment: propEnvironment,
handleSecretInputChange,
...props
},
ref
Expand Down Expand Up @@ -281,7 +283,10 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
if (!(evt.relatedTarget?.getAttribute("aria-label") === "suggestion-item"))
setIsFocused.off();
}}
onChange={(e) => onChange?.(e.target.value)}
onChange={(e) => {
handleSecretInputChange?.(e.target.value);
onChange?.(e.target.value)
}}
containerClassName={containerClassName}
/>
</Popover.Trigger>
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/hooks/api/secrets/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,3 +210,12 @@ export type TMoveSecretsDTO = {
secretIds: string[];
shouldOverwrite: boolean;
};

export type SecretBulkUpdate = {
env: string,
key: string,
value: string,
type: SecretType,
secretId?: string,
isCreatable?: boolean
}
59 changes: 50 additions & 9 deletions frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ import {
} from "@app/hooks/api";
import { useUpdateFolderBatch } from "@app/hooks/api/secretFolders/queries";
import { TUpdateFolderBatchDTO } from "@app/hooks/api/secretFolders/types";
import { SecretType, TSecretFolder } from "@app/hooks/api/types";
import { SecretType, TSecretFolder, SecretBulkUpdate } from "@app/hooks/api/types";

import { FolderForm } from "../SecretMainPage/components/ActionBar/FolderForm";
import { CreateSecretForm } from "./components/CreateSecretForm";
Expand Down Expand Up @@ -102,6 +102,7 @@ export const SecretOverviewPage = () => {
const projectSlug = currentWorkspace?.slug as string;
const [searchFilter, setSearchFilter] = useState("");
const secretPath = (router.query?.secretPath as string) || "/";
const [bulkSecretUpdateContent,setBulkSecretUpdateContent] = useState<SecretBulkUpdate[]>([])

const [selectedEntries, setSelectedEntries] = useState<{
[EntryType.FOLDER]: Record<string, boolean>;
Expand Down Expand Up @@ -365,6 +366,25 @@ export const SecretOverviewPage = () => {
}
};

const handleBulkSecretUpdate = async() => {
bulkSecretUpdateContent.map(async(secretContent: SecretBulkUpdate)=>{
if(secretContent?.isCreatable){
await handleSecretCreate(secretContent.env,secretContent.key,secretContent.value)
}
else{
let type = secretContent.type
await handleSecretUpdate(
secretContent.env,
secretContent.key,
secretContent?.value,
type,
);
}
})
setBulkSecretUpdateContent([]);
}


const handleSecretDelete = async (env: string, key: string, secretId?: string) => {
try {
await deleteSecretV3({
Expand Down Expand Up @@ -596,15 +616,35 @@ export const SecretOverviewPage = () => {
/>
</div>
{userAvailableEnvs.length > 0 && (
<div>
<Button
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("addSecretsInAllEnvs")}
className="h-10 rounded-r-none"
<div className="flex justify-between">
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={subject(ProjectPermissionSub.Secrets, { secretPath })}
>
Add Secret
</Button>
{(isAllowed) => (
<div className="flex justify-between space-x-1">
{bulkSecretUpdateContent.length > 0 && (
<Button
variant="outline_bg"
onClick={() => handleBulkSecretUpdate()}
className="h-10"
isDisabled={!isAllowed}
>
Save
</Button>
)}
<Button
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("addSecretsInAllEnvs")}
className="h-10 rounded-r-none"
isDisabled={!isAllowed}
>
Add Secret
</Button>
</div>
)}
</ProjectPermissionCan>
<DropdownMenu
open={popUp.misc.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("misc", isOpen)}
Expand Down Expand Up @@ -814,6 +854,7 @@ export const SecretOverviewPage = () => {
secretKey={key}
getSecretByKey={getSecretByKey}
expandableColWidth={expandableTableWidth}
setBulkSecretUpdateContent={setBulkSecretUpdateContent}
/>
))}
</TBody>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback,useState } from "react";
import { useCallback,useState,React } from "react";
import { Controller, useForm } from "react-hook-form";
import { subject } from "@casl/ability";
import { faCheck, faCopy, faTrash, faXmark } from "@fortawesome/free-solid-svg-icons";
Expand All @@ -11,7 +11,7 @@ import { DeleteActionModal,IconButton, Tooltip } from "@app/components/v2";
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { useToggle } from "@app/hooks";
import { SecretType } from "@app/hooks/api/types";
import { SecretBulkUpdate, SecretType } from "@app/hooks/api/types";

type Props = {
defaultValue?: string | null;
Expand All @@ -32,6 +32,7 @@ type Props = {
secretId?: string
) => Promise<void>;
onSecretDelete: (env: string, key: string, secretId?: string) => Promise<void>;
setBulkSecretUpdateContent: React.Dispatch<React.SetStateAction<SecretBulkUpdate[]>>;
};

export const SecretEditRow = ({
Expand All @@ -46,7 +47,8 @@ export const SecretEditRow = ({
environment,
secretPath,
isVisible,
secretId
secretId,
setBulkSecretUpdateContent
}: Props) => {
const {
handleSubmit,
Expand All @@ -67,6 +69,18 @@ export const SecretEditRow = ({
}, [])

const handleFormReset = () => {
setBulkSecretUpdateContent((prevContent: SecretBulkUpdate[]) => {
let data = [...prevContent];

const secretObjectIndex = data.findIndex(secretObject =>
(secretId && secretObject.secretId === secretId && secretObject.env === environment)
);

if (secretObjectIndex !== -1) {
data.splice(secretObjectIndex, 1);
}
return data
})
reset();
};

Expand Down Expand Up @@ -100,12 +114,53 @@ export const SecretEditRow = ({
reset({ value });
};

const handleSecretInputChange = (value: string) => {
if ((value || value === "") && secretName) {
setBulkSecretUpdateContent((prevContent: SecretBulkUpdate[]) => {
let data = [...prevContent];
const secretObjectIndex = data.findIndex(secretObject =>
(secretId && secretObject.secretId === secretId && secretObject.env === environment) ||
(!secretId && secretObject.key === secretName && secretObject.env === environment)
);
if (secretObjectIndex !== -1) {
let secretObj = {...data[secretObjectIndex]};
secretObj.value = value;
data[secretObjectIndex] = secretObj;
} else {
let newSecretUpdate: SecretBulkUpdate = {
env: environment,
key: secretName,
value: value,
type: isOverride ? SecretType.Personal : SecretType.Shared,
secretId: secretId,
isCreatable: isCreatable
};
data.push(newSecretUpdate);
}
return data;
});
}
};

const handleDeleteSecret = useCallback(async () => {
setIsDeleting.on();
setIsModalOpen(false);

try {
await onSecretDelete(environment, secretName, secretId);
// updating bulksecretupdatecontent array to prevent it from being updated when secret that was changed is deleted
setBulkSecretUpdateContent((prevContent: SecretBulkUpdate[]) => {
let data = [...prevContent];

const secretObjectIndex = data.findIndex(secretObject =>
(secretId && secretObject.secretId === secretId && secretObject.env === environment)
);

if (secretObjectIndex !== -1) {
data.splice(secretObjectIndex, 1);
}
return data
})
reset({ value: null });
} finally {
setIsDeleting.off();
Expand Down Expand Up @@ -138,6 +193,7 @@ export const SecretEditRow = ({
secretPath={secretPath}
environment={environment}
isImport={isImportedSecret}
handleSecretInputChange={handleSecretInputChange}
/>
)}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ import { twMerge } from "tailwind-merge";

import { Button, Checkbox, TableContainer, Td, Tooltip, Tr } from "@app/components/v2";
import { useToggle } from "@app/hooks";
import { SecretType,SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
import { SecretBulkUpdate, SecretType,SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
import { WorkspaceEnv } from "@app/hooks/api/types";

import React from "react"
import { SecretEditRow } from "./SecretEditRow";
import SecretRenameRow from "./SecretRenameRow";

Expand All @@ -41,6 +41,7 @@ type Props = {
env: string,
secretName: string
) => { secret?: SecretV3RawSanitized; environmentInfo?: WorkspaceEnv } | undefined;
setBulkSecretUpdateContent: React.Dispatch<React.SetStateAction<SecretBulkUpdate[]>>;
};

export const SecretOverviewTableRow = ({
Expand All @@ -55,7 +56,8 @@ export const SecretOverviewTableRow = ({
getImportedSecretByKey,
expandableColWidth,
onToggleSecretSelect,
isSelected
isSelected,
setBulkSecretUpdateContent
}: Props) => {
const [isFormExpanded, setIsFormExpanded] = useToggle();
const totalCols = environments.length + 1; // secret key row
Expand Down Expand Up @@ -232,6 +234,7 @@ export const SecretOverviewTableRow = ({
onSecretCreate={onSecretCreate}
onSecretUpdate={onSecretUpdate}
environment={slug}
setBulkSecretUpdateContent={setBulkSecretUpdateContent}
/>
</td>
</tr>
Expand Down