mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 21:27:31 +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">
|
<div className="flex items-center font-inter text-bunker-300">
|
||||||
<CheckboxPrimitive.Root
|
<CheckboxPrimitive.Root
|
||||||
className={twMerge(
|
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",
|
isDisabled && "bg-bunker-400 hover:bg-bunker-400",
|
||||||
isChecked && "bg-primary hover:bg-primary",
|
isChecked && "bg-primary hover:bg-primary",
|
||||||
Boolean(children) && "mr-3",
|
Boolean(children) && "mr-3",
|
||||||
@@ -46,7 +46,7 @@ export const Checkbox = ({
|
|||||||
<FontAwesomeIcon icon={faCheck} size="sm" />
|
<FontAwesomeIcon icon={faCheck} size="sm" />
|
||||||
</CheckboxPrimitive.Indicator>
|
</CheckboxPrimitive.Indicator>
|
||||||
</CheckboxPrimitive.Root>
|
</CheckboxPrimitive.Root>
|
||||||
<label className="text-sm whitespace-nowrap" htmlFor={id}>
|
<label className="text-sm whitespace-nowrap truncate" htmlFor={id}>
|
||||||
{children}
|
{children}
|
||||||
{isRequired && <span className="pl-1 text-red">*</span>}
|
{isRequired && <span className="pl-1 text-red">*</span>}
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export const CreateSecretImportForm = ({
|
|||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
const axiosError = err as AxiosError
|
const axiosError = err as AxiosError;
|
||||||
if (axiosError?.response?.status === 401) {
|
if (axiosError?.response?.status === 401) {
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "You do not have access to the selected environment/path",
|
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 { useTranslation } from "react-i18next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { subject } from "@casl/ability";
|
||||||
import {
|
import {
|
||||||
|
faAngleDown,
|
||||||
faArrowDown,
|
faArrowDown,
|
||||||
faArrowUp,
|
faArrowUp,
|
||||||
faFolderBlank,
|
faFolderBlank,
|
||||||
faMagnifyingGlass
|
faFolderPlus,
|
||||||
|
faMagnifyingGlass,
|
||||||
|
faPlus
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
import NavHeader from "@app/components/navigation/NavHeader";
|
import NavHeader from "@app/components/navigation/NavHeader";
|
||||||
import { PermissionDeniedBanner } from "@app/components/permissions";
|
import { PermissionDeniedBanner, ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuTrigger,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
IconButton,
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
Table,
|
Table,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
TableSkeleton,
|
TableSkeleton,
|
||||||
@@ -30,7 +39,13 @@ import {
|
|||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { UpgradeProjectAlert } from "@app/components/v2/UpgradeProjectAlert";
|
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 {
|
import {
|
||||||
useCreateFolder,
|
useCreateFolder,
|
||||||
useCreateSecretV3,
|
useCreateSecretV3,
|
||||||
@@ -42,6 +57,8 @@ import {
|
|||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
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 { FolderBreadCrumbs } from "./components/FolderBreadCrumbs";
|
||||||
import { ProjectIndexSecretsSection } from "./components/ProjectIndexSecretsSection";
|
import { ProjectIndexSecretsSection } from "./components/ProjectIndexSecretsSection";
|
||||||
import { SecretOverviewFolderRow } from "./components/SecretOverviewFolderRow";
|
import { SecretOverviewFolderRow } from "./components/SecretOverviewFolderRow";
|
||||||
@@ -110,6 +127,40 @@ export const SecretOverviewPage = () => {
|
|||||||
const { mutateAsync: deleteSecretV3 } = useDeleteSecretV3();
|
const { mutateAsync: deleteSecretV3 } = useDeleteSecretV3();
|
||||||
const { mutateAsync: createFolder } = useCreateFolder();
|
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) => {
|
const handleSecretCreate = async (env: string, key: string, value: string) => {
|
||||||
try {
|
try {
|
||||||
// create folder if not existing
|
// create folder if not existing
|
||||||
@@ -269,210 +320,286 @@ export const SecretOverviewPage = () => {
|
|||||||
filteredFolderNames?.length === 0;
|
filteredFolderNames?.length === 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-6 text-mineshaft-50 dark:[color-scheme:dark]">
|
<>
|
||||||
<ProjectIndexSecretsSection decryptFileKey={latestFileKey!} />
|
<div className="container mx-auto px-6 text-mineshaft-50 dark:[color-scheme:dark]">
|
||||||
<div className="relative right-5 ml-4">
|
<ProjectIndexSecretsSection decryptFileKey={latestFileKey!} />
|
||||||
<NavHeader pageName={t("dashboard.title")} isProjectRelated />
|
<div className="relative right-5 ml-4">
|
||||||
</div>
|
<NavHeader pageName={t("dashboard.title")} isProjectRelated />
|
||||||
<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>
|
</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 && (
|
{currentWorkspace?.version === ProjectVersion.V1 && (
|
||||||
<UpgradeProjectAlert project={currentWorkspace} />
|
<UpgradeProjectAlert project={currentWorkspace} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<FolderBreadCrumbs secretPath={secretPath} onResetSearch={handleResetSearch} />
|
<FolderBreadCrumbs secretPath={secretPath} onResetSearch={handleResetSearch} />
|
||||||
<div className="w-80">
|
<div className="flex flex-row items-center justify-center space-x-2">
|
||||||
<Input
|
<div className="w-80">
|
||||||
className="h-[2.3rem] bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
|
<Input
|
||||||
placeholder="Search by secret/folder name..."
|
className="h-[2.3rem] bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
|
||||||
value={searchFilter}
|
placeholder="Search by secret/folder name..."
|
||||||
onChange={(e) => setSearchFilter(e.target.value)}
|
value={searchFilter}
|
||||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
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>
|
||||||
</div>
|
<div className="thin-scrollbar mt-4" ref={parentTableRef}>
|
||||||
<div className="thin-scrollbar mt-4" ref={parentTableRef}>
|
<TableContainer className="max-h-[calc(100vh-250px)] overflow-y-auto">
|
||||||
<TableContainer className="max-h-[calc(100vh-250px)] overflow-y-auto">
|
<Table>
|
||||||
<Table>
|
<THead>
|
||||||
<THead>
|
<Tr className="sticky top-0 z-20 border-0">
|
||||||
<Tr className="sticky top-0 z-20 border-0">
|
<Th className="sticky left-0 z-20 min-w-[20rem] border-b-0 p-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">
|
||||||
<div className="flex items-center border-b border-r border-mineshaft-600 px-5 pt-3.5 pb-3">
|
Name
|
||||||
Name
|
<IconButton
|
||||||
<IconButton
|
variant="plain"
|
||||||
variant="plain"
|
className="ml-2"
|
||||||
className="ml-2"
|
ariaLabel="sort"
|
||||||
ariaLabel="sort"
|
onClick={() => setSortDir((prev) => (prev === "asc" ? "desc" : "asc"))}
|
||||||
onClick={() => setSortDir((prev) => (prev === "asc" ? "desc" : "asc"))}
|
>
|
||||||
>
|
<FontAwesomeIcon icon={sortDir === "asc" ? faArrowDown : faArrowUp} />
|
||||||
<FontAwesomeIcon icon={sortDir === "asc" ? faArrowDown : faArrowUp} />
|
</IconButton>
|
||||||
</IconButton>
|
</div>
|
||||||
</div>
|
</Th>
|
||||||
</Th>
|
{userAvailableEnvs?.map(({ name, slug }, index) => {
|
||||||
{userAvailableEnvs?.map(({ name, slug }, index) => {
|
const envSecKeyCount = getEnvSecretKeyCount(slug);
|
||||||
const envSecKeyCount = getEnvSecretKeyCount(slug);
|
const missingKeyCount = secKeys.length - envSecKeyCount;
|
||||||
const missingKeyCount = secKeys.length - envSecKeyCount;
|
return (
|
||||||
return (
|
<Th
|
||||||
<Th
|
className="min-table-row min-w-[11rem] border-b-0 p-0 text-center"
|
||||||
className="min-table-row min-w-[11rem] border-b-0 p-0 text-center"
|
key={`secret-overview-${name}-${index + 1}`}
|
||||||
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]">
|
||||||
<div className="flex items-center justify-center border-b border-mineshaft-600 px-5 pt-3.5 pb-[0.83rem]">
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
className="text-sm font-medium duration-100 hover:text-mineshaft-100"
|
||||||
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)}
|
onClick={() => handleExploreEnvClick(slug)}
|
||||||
>
|
>
|
||||||
{name}
|
Explore
|
||||||
</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>
|
</Button>
|
||||||
</Link>
|
</div>
|
||||||
</EmptyState>
|
</Td>
|
||||||
</Td>
|
))}
|
||||||
</Tr>
|
</Tr>
|
||||||
)}
|
</TFoot>
|
||||||
{!isTableLoading &&
|
</Table>
|
||||||
filteredFolderNames.map((folderName, index) => (
|
</TableContainer>
|
||||||
<SecretOverviewFolderRow
|
</div>
|
||||||
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>
|
|
||||||
</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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+222
@@ -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