mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge pull request #1518 from rhythmbhiwani/add-secrets-from-overview-page
Added Feature to Create Secrets and Folders in all envs from overview page
This commit is contained in:
@@ -30,7 +30,7 @@ export const Checkbox = ({
|
||||
<div className="flex items-center font-inter text-bunker-300">
|
||||
<CheckboxPrimitive.Root
|
||||
className={twMerge(
|
||||
"flex items-center justify-center w-4 h-4 transition-all rounded shadow border border-mineshaft-400 hover:bg-mineshaft-500 bg-mineshaft-600",
|
||||
"flex items-center flex-shrink-0 justify-center w-4 h-4 transition-all rounded shadow border border-mineshaft-400 hover:bg-mineshaft-500 bg-mineshaft-600",
|
||||
isDisabled && "bg-bunker-400 hover:bg-bunker-400",
|
||||
isChecked && "bg-primary hover:bg-primary",
|
||||
Boolean(children) && "mr-3",
|
||||
@@ -46,7 +46,7 @@ export const Checkbox = ({
|
||||
<FontAwesomeIcon icon={faCheck} size="sm" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
<label className="text-sm whitespace-nowrap" htmlFor={id}>
|
||||
<label className="text-sm whitespace-nowrap truncate" htmlFor={id}>
|
||||
{children}
|
||||
{isRequired && <span className="pl-1 text-red">*</span>}
|
||||
</label>
|
||||
|
||||
@@ -81,7 +81,7 @@ export const CreateSecretImportForm = ({
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
const axiosError = err as AxiosError
|
||||
const axiosError = err as AxiosError;
|
||||
if (axiosError?.response?.status === 401) {
|
||||
createNotification({
|
||||
text: "You do not have access to the selected environment/path",
|
||||
|
||||
@@ -2,22 +2,31 @@ import { useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { subject } from "@casl/ability";
|
||||
import {
|
||||
faAngleDown,
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
faFolderBlank,
|
||||
faMagnifyingGlass
|
||||
faFolderPlus,
|
||||
faMagnifyingGlass,
|
||||
faPlus
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import NavHeader from "@app/components/navigation/NavHeader";
|
||||
import { PermissionDeniedBanner } from "@app/components/permissions";
|
||||
import { PermissionDeniedBanner, ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
@@ -30,7 +39,13 @@ import {
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { UpgradeProjectAlert } from "@app/components/v2/UpgradeProjectAlert";
|
||||
import { useOrganization, useWorkspace } from "@app/context";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
useOrganization,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import {
|
||||
useCreateFolder,
|
||||
useCreateSecretV3,
|
||||
@@ -42,6 +57,8 @@ import {
|
||||
} from "@app/hooks/api";
|
||||
import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
||||
|
||||
import { FolderForm } from "../SecretMainPage/components/ActionBar/FolderForm";
|
||||
import { CreateSecretForm } from "./components/CreateSecretForm";
|
||||
import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs";
|
||||
import { ProjectIndexSecretsSection } from "./components/ProjectIndexSecretsSection";
|
||||
import { SecretOverviewFolderRow } from "./components/SecretOverviewFolderRow";
|
||||
@@ -110,6 +127,40 @@ export const SecretOverviewPage = () => {
|
||||
const { mutateAsync: deleteSecretV3 } = useDeleteSecretV3();
|
||||
const { mutateAsync: createFolder } = useCreateFolder();
|
||||
|
||||
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
|
||||
"addSecretsInAllEnvs",
|
||||
"addFolder",
|
||||
"misc"
|
||||
] as const);
|
||||
|
||||
const handleFolderCreate = async (folderName: string) => {
|
||||
const promises = userAvailableEnvs.map((env) => {
|
||||
const environment = env.slug;
|
||||
return createFolder({
|
||||
name: folderName,
|
||||
path: secretPath,
|
||||
environment,
|
||||
projectId: workspaceId
|
||||
});
|
||||
});
|
||||
|
||||
const results = await Promise.allSettled(promises);
|
||||
const isFoldersAdded = results.some((result) => result.status === "fulfilled");
|
||||
|
||||
if (isFoldersAdded) {
|
||||
handlePopUpClose("addFolder");
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully created folder"
|
||||
});
|
||||
} else {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to create folder"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSecretCreate = async (env: string, key: string, value: string) => {
|
||||
try {
|
||||
// create folder if not existing
|
||||
@@ -269,210 +320,286 @@ export const SecretOverviewPage = () => {
|
||||
filteredFolderNames?.length === 0;
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-6 text-mineshaft-50 dark:[color-scheme:dark]">
|
||||
<ProjectIndexSecretsSection decryptFileKey={latestFileKey!} />
|
||||
<div className="relative right-5 ml-4">
|
||||
<NavHeader pageName={t("dashboard.title")} isProjectRelated />
|
||||
</div>
|
||||
<div className="space-y-8">
|
||||
<div className="mt-6">
|
||||
<p className="text-3xl font-semibold text-bunker-100">Secrets Overview</p>
|
||||
<p className="text-md text-bunker-300">
|
||||
Inject your secrets using
|
||||
<a
|
||||
className="ml-1 text-mineshaft-300 underline decoration-primary-800 underline-offset-4 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600"
|
||||
href="https://infisical.com/docs/cli/overview"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Infisical CLI
|
||||
</a>
|
||||
,
|
||||
<a
|
||||
className="ml-1 text-mineshaft-300 underline decoration-primary-800 underline-offset-4 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600"
|
||||
href="https://infisical.com/docs/documentation/getting-started/api"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Infisical API
|
||||
</a>
|
||||
,
|
||||
<a
|
||||
className="ml-1 text-mineshaft-300 underline decoration-primary-800 underline-offset-4 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600"
|
||||
href="https://infisical.com/docs/sdks/overview"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Infisical SDKs
|
||||
</a>
|
||||
, and
|
||||
<a
|
||||
className="ml-1 text-mineshaft-300 underline decoration-primary-800 underline-offset-4 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600"
|
||||
href="https://infisical.com/docs/documentation/getting-started/introduction"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
more
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<>
|
||||
<div className="container mx-auto px-6 text-mineshaft-50 dark:[color-scheme:dark]">
|
||||
<ProjectIndexSecretsSection decryptFileKey={latestFileKey!} />
|
||||
<div className="relative right-5 ml-4">
|
||||
<NavHeader pageName={t("dashboard.title")} isProjectRelated />
|
||||
</div>
|
||||
<div className="space-y-8">
|
||||
<div className="mt-6">
|
||||
<p className="text-3xl font-semibold text-bunker-100">Secrets Overview</p>
|
||||
<p className="text-md text-bunker-300">
|
||||
Inject your secrets using
|
||||
<a
|
||||
className="ml-1 text-mineshaft-300 underline decoration-primary-800 underline-offset-4 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600"
|
||||
href="https://infisical.com/docs/cli/overview"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Infisical CLI
|
||||
</a>
|
||||
,
|
||||
<a
|
||||
className="ml-1 text-mineshaft-300 underline decoration-primary-800 underline-offset-4 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600"
|
||||
href="https://infisical.com/docs/documentation/getting-started/api"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Infisical API
|
||||
</a>
|
||||
,
|
||||
<a
|
||||
className="ml-1 text-mineshaft-300 underline decoration-primary-800 underline-offset-4 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600"
|
||||
href="https://infisical.com/docs/sdks/overview"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Infisical SDKs
|
||||
</a>
|
||||
, and
|
||||
<a
|
||||
className="ml-1 text-mineshaft-300 underline decoration-primary-800 underline-offset-4 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600"
|
||||
href="https://infisical.com/docs/documentation/getting-started/introduction"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
more
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{currentWorkspace?.version === ProjectVersion.V1 && (
|
||||
<UpgradeProjectAlert project={currentWorkspace} />
|
||||
)}
|
||||
{currentWorkspace?.version === ProjectVersion.V1 && (
|
||||
<UpgradeProjectAlert project={currentWorkspace} />
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<FolderBreadCrumbs secretPath={secretPath} onResetSearch={handleResetSearch} />
|
||||
<div className="w-80">
|
||||
<Input
|
||||
className="h-[2.3rem] bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
|
||||
placeholder="Search by secret/folder name..."
|
||||
value={searchFilter}
|
||||
onChange={(e) => setSearchFilter(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
/>
|
||||
<div className="flex items-center justify-between">
|
||||
<FolderBreadCrumbs secretPath={secretPath} onResetSearch={handleResetSearch} />
|
||||
<div className="flex flex-row items-center justify-center space-x-2">
|
||||
<div className="w-80">
|
||||
<Input
|
||||
className="h-[2.3rem] bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
|
||||
placeholder="Search by secret/folder name..."
|
||||
value={searchFilter}
|
||||
onChange={(e) => setSearchFilter(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={subject(ProjectPermissionSub.Secrets, { secretPath })}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("addSecretsInAllEnvs")}
|
||||
className="h-10 rounded-r-none"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Add Secret
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<DropdownMenu
|
||||
open={popUp.misc.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("misc", isOpen)}
|
||||
>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="add-folder-or-import"
|
||||
variant="outline_bg"
|
||||
className="rounded-l-none bg-mineshaft-600 p-3"
|
||||
>
|
||||
<FontAwesomeIcon icon={faAngleDown} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<div className="flex flex-col space-y-1 p-1.5">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={subject(ProjectPermissionSub.Secrets, { secretPath })}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faFolderPlus} />}
|
||||
onClick={() => {
|
||||
handlePopUpOpen("addFolder");
|
||||
handlePopUpClose("misc");
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
variant="outline_bg"
|
||||
className="h-10"
|
||||
isFullWidth
|
||||
>
|
||||
Add Folder
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="thin-scrollbar mt-4" ref={parentTableRef}>
|
||||
<TableContainer className="max-h-[calc(100vh-250px)] overflow-y-auto">
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr className="sticky top-0 z-20 border-0">
|
||||
<Th className="sticky left-0 z-20 min-w-[20rem] border-b-0 p-0">
|
||||
<div className="flex items-center border-b border-r border-mineshaft-600 px-5 pt-3.5 pb-3">
|
||||
Name
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className="ml-2"
|
||||
ariaLabel="sort"
|
||||
onClick={() => setSortDir((prev) => (prev === "asc" ? "desc" : "asc"))}
|
||||
>
|
||||
<FontAwesomeIcon icon={sortDir === "asc" ? faArrowDown : faArrowUp} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
{userAvailableEnvs?.map(({ name, slug }, index) => {
|
||||
const envSecKeyCount = getEnvSecretKeyCount(slug);
|
||||
const missingKeyCount = secKeys.length - envSecKeyCount;
|
||||
return (
|
||||
<Th
|
||||
className="min-table-row min-w-[11rem] border-b-0 p-0 text-center"
|
||||
key={`secret-overview-${name}-${index + 1}`}
|
||||
>
|
||||
<div className="flex items-center justify-center border-b border-mineshaft-600 px-5 pt-3.5 pb-[0.83rem]">
|
||||
<button
|
||||
type="button"
|
||||
className="text-sm font-medium duration-100 hover:text-mineshaft-100"
|
||||
<div className="thin-scrollbar mt-4" ref={parentTableRef}>
|
||||
<TableContainer className="max-h-[calc(100vh-250px)] overflow-y-auto">
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr className="sticky top-0 z-20 border-0">
|
||||
<Th className="sticky left-0 z-20 min-w-[20rem] border-b-0 p-0">
|
||||
<div className="flex items-center border-b border-r border-mineshaft-600 px-5 pt-3.5 pb-3">
|
||||
Name
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className="ml-2"
|
||||
ariaLabel="sort"
|
||||
onClick={() => setSortDir((prev) => (prev === "asc" ? "desc" : "asc"))}
|
||||
>
|
||||
<FontAwesomeIcon icon={sortDir === "asc" ? faArrowDown : faArrowUp} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
{userAvailableEnvs?.map(({ name, slug }, index) => {
|
||||
const envSecKeyCount = getEnvSecretKeyCount(slug);
|
||||
const missingKeyCount = secKeys.length - envSecKeyCount;
|
||||
return (
|
||||
<Th
|
||||
className="min-table-row min-w-[11rem] border-b-0 p-0 text-center"
|
||||
key={`secret-overview-${name}-${index + 1}`}
|
||||
>
|
||||
<div className="flex items-center justify-center border-b border-mineshaft-600 px-5 pt-3.5 pb-[0.83rem]">
|
||||
<button
|
||||
type="button"
|
||||
className="text-sm font-medium duration-100 hover:text-mineshaft-100"
|
||||
onClick={() => handleExploreEnvClick(slug)}
|
||||
>
|
||||
{name}
|
||||
</button>
|
||||
{missingKeyCount > 0 && (
|
||||
<Tooltip
|
||||
className="max-w-none lowercase"
|
||||
content={`${missingKeyCount} secrets missing\n compared to other environments`}
|
||||
>
|
||||
<div className="ml-2 flex h-[1.1rem] cursor-default items-center justify-center rounded-sm border border-red-400 bg-red-600 p-1 text-xs font-medium text-bunker-100">
|
||||
<span className="text-bunker-100">{missingKeyCount}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</Th>
|
||||
);
|
||||
})}
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{canViewOverviewPage && isTableLoading && (
|
||||
<TableSkeleton
|
||||
columns={userAvailableEnvs.length + 1}
|
||||
innerKey="secret-overview-loading"
|
||||
rows={5}
|
||||
className="bg-mineshaft-700"
|
||||
/>
|
||||
)}
|
||||
{isTableEmpty && !isTableLoading && (
|
||||
<Tr>
|
||||
<Td colSpan={userAvailableEnvs.length + 1}>
|
||||
<EmptyState title="Let's add some secrets" icon={faFolderBlank} iconSize="3x">
|
||||
<Link
|
||||
href={{
|
||||
pathname: "/project/[id]/secrets/[env]",
|
||||
query: { id: workspaceId, env: userAvailableEnvs?.[0]?.slug }
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
className="mt-4"
|
||||
variant="outline_bg"
|
||||
colorSchema="primary"
|
||||
size="md"
|
||||
>
|
||||
Go to {userAvailableEnvs?.[0]?.name}
|
||||
</Button>
|
||||
</Link>
|
||||
</EmptyState>
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
{!isTableLoading &&
|
||||
filteredFolderNames.map((folderName, index) => (
|
||||
<SecretOverviewFolderRow
|
||||
folderName={folderName}
|
||||
isFolderPresentInEnv={isFolderPresentInEnv}
|
||||
environments={userAvailableEnvs}
|
||||
key={`overview-${folderName}-${index + 1}`}
|
||||
onClick={handleFolderClick}
|
||||
/>
|
||||
))}
|
||||
{!isTableLoading &&
|
||||
(userAvailableEnvs?.length > 0 ? (
|
||||
filteredSecretNames.map((key, index) => (
|
||||
<SecretOverviewTableRow
|
||||
secretPath={secretPath}
|
||||
onSecretCreate={handleSecretCreate}
|
||||
onSecretDelete={handleSecretDelete}
|
||||
onSecretUpdate={handleSecretUpdate}
|
||||
key={`overview-${key}-${index + 1}`}
|
||||
environments={userAvailableEnvs}
|
||||
secretKey={key}
|
||||
getSecretByKey={getSecretByKey}
|
||||
expandableColWidth={expandableTableWidth}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<PermissionDeniedBanner />
|
||||
))}
|
||||
</TBody>
|
||||
<TFoot>
|
||||
<Tr className="sticky bottom-0 z-10 border-0 bg-mineshaft-800">
|
||||
<Td className="sticky left-0 z-10 border-0 bg-mineshaft-800 p-0">
|
||||
<div
|
||||
className="w-full border-t border-r border-mineshaft-600"
|
||||
style={{ height: "45px" }}
|
||||
/>
|
||||
</Td>
|
||||
{userAvailableEnvs.map(({ name, slug }) => (
|
||||
<Td key={`explore-${name}-btn`} className="border-0 border-mineshaft-600 p-0">
|
||||
<div className="flex w-full items-center justify-center border-r border-t border-mineshaft-600 px-5 py-2">
|
||||
<Button
|
||||
size="xs"
|
||||
variant="outline_bg"
|
||||
isFullWidth
|
||||
onClick={() => handleExploreEnvClick(slug)}
|
||||
>
|
||||
{name}
|
||||
</button>
|
||||
{missingKeyCount > 0 && (
|
||||
<Tooltip
|
||||
className="max-w-none lowercase"
|
||||
content={`${missingKeyCount} secrets missing\n compared to other environments`}
|
||||
>
|
||||
<div className="ml-2 flex h-[1.1rem] cursor-default items-center justify-center rounded-sm border border-red-400 bg-red-600 p-1 text-xs font-medium text-bunker-100">
|
||||
<span className="text-bunker-100">{missingKeyCount}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</Th>
|
||||
);
|
||||
})}
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{canViewOverviewPage && isTableLoading && (
|
||||
<TableSkeleton
|
||||
columns={userAvailableEnvs.length + 1}
|
||||
innerKey="secret-overview-loading"
|
||||
rows={5}
|
||||
className="bg-mineshaft-700"
|
||||
/>
|
||||
)}
|
||||
{isTableEmpty && !isTableLoading && (
|
||||
<Tr>
|
||||
<Td colSpan={userAvailableEnvs.length + 1}>
|
||||
<EmptyState title="Let's add some secrets" icon={faFolderBlank} iconSize="3x">
|
||||
<Link
|
||||
href={{
|
||||
pathname: "/project/[id]/secrets/[env]",
|
||||
query: { id: workspaceId, env: userAvailableEnvs?.[0]?.slug }
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
className="mt-4"
|
||||
variant="outline_bg"
|
||||
colorSchema="primary"
|
||||
size="md"
|
||||
>
|
||||
Go to {userAvailableEnvs?.[0]?.name}
|
||||
Explore
|
||||
</Button>
|
||||
</Link>
|
||||
</EmptyState>
|
||||
</Td>
|
||||
</div>
|
||||
</Td>
|
||||
))}
|
||||
</Tr>
|
||||
)}
|
||||
{!isTableLoading &&
|
||||
filteredFolderNames.map((folderName, index) => (
|
||||
<SecretOverviewFolderRow
|
||||
folderName={folderName}
|
||||
isFolderPresentInEnv={isFolderPresentInEnv}
|
||||
environments={userAvailableEnvs}
|
||||
key={`overview-${folderName}-${index + 1}`}
|
||||
onClick={handleFolderClick}
|
||||
/>
|
||||
))}
|
||||
{!isTableLoading &&
|
||||
(userAvailableEnvs?.length > 0 ? (
|
||||
filteredSecretNames.map((key, index) => (
|
||||
<SecretOverviewTableRow
|
||||
secretPath={secretPath}
|
||||
onSecretCreate={handleSecretCreate}
|
||||
onSecretDelete={handleSecretDelete}
|
||||
onSecretUpdate={handleSecretUpdate}
|
||||
key={`overview-${key}-${index + 1}`}
|
||||
environments={userAvailableEnvs}
|
||||
secretKey={key}
|
||||
getSecretByKey={getSecretByKey}
|
||||
expandableColWidth={expandableTableWidth}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<PermissionDeniedBanner />
|
||||
))}
|
||||
</TBody>
|
||||
<TFoot>
|
||||
<Tr className="sticky bottom-0 z-10 border-0 bg-mineshaft-800">
|
||||
<Td className="sticky left-0 z-10 border-0 bg-mineshaft-800 p-0">
|
||||
<div
|
||||
className="w-full border-t border-r border-mineshaft-600"
|
||||
style={{ height: "45px" }}
|
||||
/>
|
||||
</Td>
|
||||
{userAvailableEnvs.map(({ name, slug }) => (
|
||||
<Td key={`explore-${name}-btn`} className="border-0 border-mineshaft-600 p-0">
|
||||
<div className="flex w-full items-center justify-center border-r border-t border-mineshaft-600 px-5 py-2">
|
||||
<Button
|
||||
size="xs"
|
||||
variant="outline_bg"
|
||||
isFullWidth
|
||||
onClick={() => handleExploreEnvClick(slug)}
|
||||
>
|
||||
Explore
|
||||
</Button>
|
||||
</div>
|
||||
</Td>
|
||||
))}
|
||||
</Tr>
|
||||
</TFoot>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</TFoot>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<CreateSecretForm
|
||||
secretPath={secretPath}
|
||||
isOpen={popUp.addSecretsInAllEnvs.isOpen}
|
||||
getSecretByKey={getSecretByKey}
|
||||
onTogglePopUp={(isOpen) => handlePopUpToggle("addSecretsInAllEnvs", isOpen)}
|
||||
onClose={() => handlePopUpClose("addSecretsInAllEnvs")}
|
||||
decryptFileKey={latestFileKey!}
|
||||
/>
|
||||
<Modal
|
||||
isOpen={popUp.addFolder.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("addFolder", isOpen)}
|
||||
>
|
||||
<ModalContent title="Create Folder">
|
||||
<FolderForm onCreateFolder={handleFolderCreate} />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faWarning } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
SecretInput,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useCreateFolder, useCreateSecretV3, useUpdateSecretV3 } from "@app/hooks/api";
|
||||
import { DecryptedSecret, UserWsKeyPair } from "@app/hooks/api/types";
|
||||
|
||||
const typeSchema = z
|
||||
.object({
|
||||
key: z.string().min(1, "Key is required"),
|
||||
value: z.string().optional(),
|
||||
environments: z.record(z.boolean().optional())
|
||||
})
|
||||
.refine((data) => data.key !== undefined, {
|
||||
message: "Please enter secret name"
|
||||
});
|
||||
|
||||
type TFormSchema = z.infer<typeof typeSchema>;
|
||||
|
||||
type Props = {
|
||||
secretPath?: string;
|
||||
decryptFileKey: UserWsKeyPair;
|
||||
getSecretByKey: (slug: string, key: string) => DecryptedSecret | undefined;
|
||||
// modal props
|
||||
isOpen?: boolean;
|
||||
onClose: () => void;
|
||||
onTogglePopUp: (isOpen: boolean) => void;
|
||||
};
|
||||
|
||||
export const CreateSecretForm = ({
|
||||
secretPath = "/",
|
||||
decryptFileKey,
|
||||
isOpen,
|
||||
getSecretByKey,
|
||||
onClose,
|
||||
onTogglePopUp
|
||||
}: Props) => {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
control,
|
||||
reset,
|
||||
watch,
|
||||
formState: { isSubmitting, errors }
|
||||
} = useForm<TFormSchema>({ resolver: zodResolver(typeSchema) });
|
||||
const newSecretKey = watch("key");
|
||||
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const workspaceId = currentWorkspace?.id || "";
|
||||
const environments = currentWorkspace?.environments || [];
|
||||
|
||||
const { createNotification } = useNotificationContext();
|
||||
|
||||
const { mutateAsync: createSecretV3 } = useCreateSecretV3();
|
||||
const { mutateAsync: updateSecretV3 } = useUpdateSecretV3();
|
||||
const { mutateAsync: createFolder } = useCreateFolder();
|
||||
|
||||
const handleFormSubmit = async ({ key, value, environments: selectedEnv }: TFormSchema) => {
|
||||
const environmentsSelected = environments.filter(({ slug }) => selectedEnv[slug]);
|
||||
const isEnvironmentsSelected = environmentsSelected.length;
|
||||
|
||||
if (!isEnvironmentsSelected) {
|
||||
createNotification({ type: "error", text: "Select atleast one environment" });
|
||||
return;
|
||||
}
|
||||
|
||||
const promises = environmentsSelected.map(async (env) => {
|
||||
const environment = env.slug;
|
||||
// create folder if not existing
|
||||
if (secretPath !== "/") {
|
||||
// /hello/world -> [hello","world"]
|
||||
const pathSegment = secretPath.split("/").filter(Boolean);
|
||||
const parentPath = `/${pathSegment.slice(0, -1).join("/")}`;
|
||||
const folderName = pathSegment.at(-1);
|
||||
if (folderName && parentPath) {
|
||||
await createFolder({
|
||||
projectId: workspaceId,
|
||||
path: parentPath,
|
||||
environment,
|
||||
name: folderName
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const isEdit = getSecretByKey(environment, key) !== undefined;
|
||||
if (isEdit) {
|
||||
return updateSecretV3({
|
||||
environment,
|
||||
workspaceId,
|
||||
secretPath,
|
||||
secretName: key,
|
||||
secretValue: value || "",
|
||||
type: "shared",
|
||||
latestFileKey: decryptFileKey
|
||||
});
|
||||
}
|
||||
|
||||
return createSecretV3({
|
||||
environment,
|
||||
workspaceId,
|
||||
secretPath,
|
||||
secretName: key,
|
||||
secretValue: value || "",
|
||||
secretComment: "",
|
||||
type: "shared",
|
||||
latestFileKey: decryptFileKey
|
||||
});
|
||||
});
|
||||
|
||||
const results = await Promise.allSettled(promises);
|
||||
const isSecretsAdded = results.some((result) => result.status === "fulfilled");
|
||||
|
||||
if (isSecretsAdded) {
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Secrets created successfully"
|
||||
});
|
||||
onClose();
|
||||
reset();
|
||||
} else {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to create secrets"
|
||||
});
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onTogglePopUp}>
|
||||
<ModalContent
|
||||
className="max-h-[80vh] overflow-y-auto"
|
||||
title="Add Secrets"
|
||||
subTitle="Add secrets in all environments at once."
|
||||
>
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||
<FormControl label="Key" isError={Boolean(errors?.key)} errorText={errors?.key?.message}>
|
||||
<Input
|
||||
{...register("key")}
|
||||
placeholder="Type your secret name"
|
||||
autoCapitalization={currentWorkspace?.autoCapitalization}
|
||||
/>
|
||||
</FormControl>
|
||||
<Controller
|
||||
control={control}
|
||||
name="value"
|
||||
render={({ field }) => (
|
||||
<FormControl
|
||||
label="Value"
|
||||
isError={Boolean(errors?.value)}
|
||||
errorText={errors?.value?.message}
|
||||
>
|
||||
<SecretInput
|
||||
{...field}
|
||||
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<FormLabel label="Environments" className="mb-2" />
|
||||
<div className="thin-scrollbar grid max-h-64 grid-cols-3 gap-4 overflow-auto ">
|
||||
{environments.map((env) => {
|
||||
return (
|
||||
<Controller
|
||||
name={`environments.${env.slug}`}
|
||||
key={`secret-input-${env.slug}`}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
isChecked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
id={`secret-input-${env.slug}`}
|
||||
>
|
||||
{env.name}
|
||||
{getSecretByKey(env.slug, newSecretKey) && (
|
||||
<Tooltip content="Secret exists. Will be overwritten">
|
||||
<FontAwesomeIcon icon={faWarning} className="ml-1 text-yellow-400" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</Checkbox>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="mt-7 flex items-center">
|
||||
<Button
|
||||
isDisabled={isSubmitting}
|
||||
isLoading={isSubmitting}
|
||||
key="layout-create-project-submit"
|
||||
className="mr-4"
|
||||
type="submit"
|
||||
>
|
||||
Create Secret
|
||||
</Button>
|
||||
<Button
|
||||
key="layout-cancel-create-project"
|
||||
onClick={onClose}
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { CreateSecretForm } from "./CreateSecretForm"
|
||||
Reference in New Issue
Block a user