diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 28ea1859e..f34d3fbab 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -51,6 +51,7 @@ "cookies": "^0.8.0", "cva": "npm:class-variance-authority@^0.4.0", "date-fns": "^2.30.0", + "file-saver": "^2.0.5", "framer-motion": "^6.2.3", "fs": "^0.0.2", "gray-matter": "^4.0.3", @@ -105,6 +106,7 @@ "@storybook/react": "^7.0.23", "@storybook/testing-library": "^0.2.0", "@tailwindcss/typography": "^0.5.4", + "@types/file-saver": "^2.0.5", "@types/jsrp": "^0.2.4", "@types/node": "^18.11.9", "@types/picomatch": "^2.3.0", @@ -8208,6 +8210,12 @@ "@types/send": "*" } }, + "node_modules/@types/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-zv9kNf3keYegP5oThGLaPk8E081DFDuwfqjtiTzm6PoxChdJ1raSuADf2YGCVIyrSynLrgc8JWv296s7Q7pQSQ==", + "dev": true + }, "node_modules/@types/find-cache-dir": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/@types/find-cache-dir/-/find-cache-dir-3.2.1.tgz", @@ -13444,6 +13452,11 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==" + }, "node_modules/file-system-cache": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/file-system-cache/-/file-system-cache-2.3.0.tgz", @@ -29227,6 +29240,12 @@ "@types/send": "*" } }, + "@types/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-zv9kNf3keYegP5oThGLaPk8E081DFDuwfqjtiTzm6PoxChdJ1raSuADf2YGCVIyrSynLrgc8JWv296s7Q7pQSQ==", + "dev": true + }, "@types/find-cache-dir": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/@types/find-cache-dir/-/find-cache-dir-3.2.1.tgz", @@ -33358,6 +33377,11 @@ "flat-cache": "^3.0.4" } }, + "file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==" + }, "file-system-cache": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/file-system-cache/-/file-system-cache-2.3.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 42cd32385..0ff0c89eb 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -59,6 +59,7 @@ "cookies": "^0.8.0", "cva": "npm:class-variance-authority@^0.4.0", "date-fns": "^2.30.0", + "file-saver": "^2.0.5", "framer-motion": "^6.2.3", "fs": "^0.0.2", "gray-matter": "^4.0.3", @@ -113,6 +114,7 @@ "@storybook/react": "^7.0.23", "@storybook/testing-library": "^0.2.0", "@tailwindcss/typography": "^0.5.4", + "@types/file-saver": "^2.0.5", "@types/jsrp": "^0.2.4", "@types/node": "^18.11.9", "@types/picomatch": "^2.3.0", diff --git a/frontend/src/pages/project/[id]/secrets/[env].tsx b/frontend/src/pages/project/[id]/secrets/[env].tsx index 883850592..bba0704dd 100644 --- a/frontend/src/pages/project/[id]/secrets/[env].tsx +++ b/frontend/src/pages/project/[id]/secrets/[env].tsx @@ -1,7 +1,7 @@ import { useTranslation } from "react-i18next"; import Head from "next/head"; -import { DashboardPage } from "@app/views/DashboardPage"; +import { SecretMainPage } from "@app/views/SecretMainPage"; const Dashboard = () => { const { t } = useTranslation(); @@ -16,7 +16,7 @@ const Dashboard = () => {
- +
); diff --git a/frontend/src/pages/project/[id]/secrets/v2/[env].tsx b/frontend/src/pages/project/[id]/secrets/v2/[env].tsx new file mode 100644 index 000000000..bba0704dd --- /dev/null +++ b/frontend/src/pages/project/[id]/secrets/v2/[env].tsx @@ -0,0 +1,27 @@ +import { useTranslation } from "react-i18next"; +import Head from "next/head"; + +import { SecretMainPage } from "@app/views/SecretMainPage"; + +const Dashboard = () => { + const { t } = useTranslation(); + + return ( + <> + + {t("common.head-title", { title: t("dashboard.title") })} + + + + + +
+ +
+ + ); +}; + +export default Dashboard; + +Dashboard.requireAuth = true; diff --git a/frontend/src/views/DashboardPage/DashboardPage.tsx b/frontend/src/views/DashboardPage/DashboardPage.tsx deleted file mode 100644 index 220ddb146..000000000 --- a/frontend/src/views/DashboardPage/DashboardPage.tsx +++ /dev/null @@ -1,1263 +0,0 @@ -import { useCallback, useEffect, useRef, useState } from "react"; -import { FormProvider, useFieldArray, useForm } from "react-hook-form"; -import { useTranslation } from "react-i18next"; -import { useRouter } from "next/router"; -import { subject } from "@casl/ability"; -import { - closestCenter, - DndContext, - DragEndEvent, - KeyboardSensor, - MouseSensor, - TouchSensor, - useSensor, - useSensors -} from "@dnd-kit/core"; -import { restrictToVerticalAxis } from "@dnd-kit/modifiers"; -import { arrayMove } from "@dnd-kit/sortable"; -import { - faAngleDown, - faArrowLeft, - faCheck, - faClockRotateLeft, - faCodeCommit, - faDownload, - faEye, - faEyeSlash, - faFileImport, - faFolderPlus, - faMagnifyingGlass, - faPlus -} from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { yupResolver } from "@hookform/resolvers/yup"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuTrigger -} from "@radix-ui/react-dropdown-menu"; -import { useQueryClient } from "@tanstack/react-query"; - -import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; -import NavHeader from "@app/components/navigation/NavHeader"; -import { PermissionDeniedBanner, ProjectPermissionCan } from "@app/components/permissions"; -import { - Button, - DeleteActionModal, - IconButton, - Input, - Modal, - ModalContent, - Popover, - PopoverContent, - PopoverTrigger, - TableContainer, - Tag, - Tooltip, - UpgradePlanModal -} from "@app/components/v2"; -import { leaveConfirmDefaultMessage } from "@app/const"; -import { - ProjectPermissionActions, - ProjectPermissionSub, - useOrganization, - useProjectPermission, - useSubscription, - useWorkspace -} from "@app/context"; -import { useLeaveConfirm, usePopUp, useToggle } from "@app/hooks"; -import { - useBatchSecretsOp, - useCreateFolder, - useCreateSecretImport, - useCreateWsTag, - useDeleteFolder, - useDeleteSecretImport, - useGetImportedSecrets, - useGetProjectFolders, - useGetProjectSecrets, - useGetSecretImports, - useGetSecretVersion, - useGetSnapshotSecrets, - useGetUserAction, - useGetUserWsKey, - useGetWorkspaceSecretSnapshots, - useGetWsSnapshotCount, - useGetWsTags, - usePerformSecretRollback, - useRegisterUserAction, - useUpdateFolder, - useUpdateSecretImport -} from "@app/hooks/api"; -import { secretKeys } from "@app/hooks/api/secrets/queries"; - -import { CompareSecret } from "./components/CompareSecret"; -import { CreateTagModal } from "./components/CreateTagModal"; -import { - FolderForm, - FolderSection, - TDeleteFolderForm, - TEditFolderForm -} from "./components/FolderSection"; -import { PitDrawer } from "./components/PitDrawer"; -import { SecretDetailDrawer } from "./components/SecretDetailDrawer"; -import { SecretDropzone } from "./components/SecretDropzone"; -import { SecretImportForm } from "./components/SecretImportForm"; -import { SecretImportSection } from "./components/SecretImportSection"; -import { SecretInputRow } from "./components/SecretInputRow"; -import { SecretTableHeader } from "./components/SecretTableHeader"; -import { - DEFAULT_SECRET_VALUE, - downloadSecret, - FormData, - schema, - transformSecretsToBatchSecretReq, - TSecOverwriteOpt, - TSecretDetailsOpen -} from "./DashboardPage.utils"; - -const USER_ACTION_PUSH = "first_time_secrets_pushed"; -type TDeleteSecretImport = { environment: string; secretPath: string }; -/* - * Some imp aspects to consider. Here there are multiple stats changing - * Thus ideally we need to use a context. But instead we rely on react hook form - * React hook form provides context and high performance proxy based rendering - * It also handles error handling and transferring states between inputs - * - * Another thing is the purpose of overrideAction - * Before we would remove the value for personal secret when user toggle and user couldn't get it back - * They have to reload the browser or go back all over again - * Instead when user delete we raise a flag so if user decides to go back to toggle personal before saving - * They will get it back - */ -export const DashboardPage = () => { - const { subscription } = useSubscription(); - const { t } = useTranslation(); - const router = useRouter(); - const { createNotification } = useNotificationContext(); - const queryClient = useQueryClient(); - const environment = router.query.env as string; - const permission = useProjectPermission(); - - const secretContainer = useRef(null); - const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([ - "secretDetails", - "addTag", - "secretSnapshots", - "uploadedSecOpts", - "compareSecrets", - "folderForm", - "deleteFolder", - "upgradePlan", - "addSecretImport", - "deleteSecretImport" - ] as const); - const [isSecretValueHidden, setIsSecretValueHidden] = useToggle(true); - const [searchFilter, setSearchFilter] = useState(""); - const [snapshotId, setSnaphotId] = useState(null); - const [sortDir, setSortDir] = useState<"asc" | "desc">("asc"); - const deletedSecretIds = useRef<{ id: string; secretName: string }[]>([]); - const { hasUnsavedChanges, setHasUnsavedChanges } = useLeaveConfirm({ initialValue: false }); - - const folderId = router.query.folderId as string; - const isRollbackMode = Boolean(snapshotId); - - const { currentWorkspace, isLoading } = useWorkspace(); - const { currentOrg } = useOrganization(); - const workspaceId = currentWorkspace?._id as string; - - const { data: latestFileKey } = useGetUserWsKey(workspaceId); - - useEffect(() => { - if (!isLoading && !workspaceId && router.isReady) { - router.push(`/org/${currentOrg?._id}/overview`); - } - }, [isLoading, workspaceId, router.isReady]); - - // fetching data - const { data: userAction } = useGetUserAction(USER_ACTION_PUSH); - const hasUserPushed = Boolean(userAction); - - const { data: secretVersion } = useGetSecretVersion({ - limit: 10, - offset: 0, - secretId: (popUp?.secretDetails?.data as TSecretDetailsOpen)?.id, - decryptFileKey: latestFileKey! - }); - - const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({ - workspaceId, - env: environment, - decryptFileKey: latestFileKey!, - isPaused: Boolean(snapshotId), - folderId - }); - - const { data: folderData, isLoading: isFoldersLoading } = useGetProjectFolders({ - workspaceId: workspaceId || "", - environment, - parentFolderId: folderId, - isPaused: isRollbackMode, - sortDir - }); - - const { - data: secretSnaphots, - fetchNextPage, - hasNextPage, - isFetchingNextPage - } = useGetWorkspaceSecretSnapshots({ - workspaceId, - environment, - folder: folderId, - limit: 10 - }); - - const { - data: snapshotSecret, - isLoading: isSnapshotSecretsLoading, - isFetching: isSnapshotChanging - } = useGetSnapshotSecrets({ - snapshotId: snapshotId || "", - env: environment, - decryptFileKey: latestFileKey! - }); - - const { data: snapshotCount, isLoading: isLoadingSnapshotCount } = useGetWsSnapshotCount( - workspaceId, - environment, - folderId - ); - - const { data: wsTags } = useGetWsTags(workspaceId); - - // mutation calls - const { mutateAsync: batchSecretOp } = useBatchSecretsOp(); - const { mutateAsync: performSecretRollback } = usePerformSecretRollback(); - const { mutateAsync: registerUserAction } = useRegisterUserAction(); - const { mutateAsync: createWsTag } = useCreateWsTag(); - const { mutateAsync: createFolder } = useCreateFolder(); - const { mutateAsync: updateFolder } = useUpdateFolder(folderId); - const { mutateAsync: deleteFolder } = useDeleteFolder(folderId); - - const { data: secretImportCfg, isFetching: isSecretImportCfgFetching } = useGetSecretImports( - workspaceId, - environment, - folderId - ); - - const { data: importedSecrets } = useGetImportedSecrets({ - workspaceId, - decryptFileKey: latestFileKey!, - environment, - folderId - }); - - const secretPath = `/${(folderData?.dir || []) - ?.filter(({ name }) => name !== "root") - ?.map(({ name }) => name) - .join("/")}`; - - const userAvailableEnvs = currentWorkspace?.environments || []; - - // This is for dnd-kit. As react-query state mutation async - // This will act as a placeholder to avoid a glitching animation on dropping items - const [items, setItems] = useState< - Array<{ environment: string; secretPath: string; id: string }> - >([]); - - useEffect(() => { - if ( - !isSecretImportCfgFetching || - // case in which u go to a folder and come back to fill in with cache data - (items.length === 0 && secretImportCfg?.imports?.length !== 0 && isSecretImportCfgFetching) - ) { - setItems( - secretImportCfg?.imports?.map((el) => ({ - ...el, - id: `${el.environment}-${el.secretPath}` - })) || [] - ); - } - }, [isSecretImportCfgFetching]); - - const { mutateAsync: createSecretImport } = useCreateSecretImport(); - const { mutate: updateSecretImportSync } = useUpdateSecretImport(); - const { mutateAsync: deleteSecretImport } = useDeleteSecretImport(); - - const sensors = useSensors( - useSensor(MouseSensor, {}), - useSensor(TouchSensor, {}), - useSensor(KeyboardSensor, {}) - ); - - const method = useForm({ - // why any: well yup inferred ts expects other keys to defined as undefined - defaultValues: secrets as any, - values: secrets as any, - mode: "onBlur", - resolver: yupResolver(schema) - }); - - const { - register, - control, - handleSubmit, - getValues, - setValue, - formState: { isSubmitting, isDirty, errors }, - reset - } = method; - const { fields, prepend, append, remove } = useFieldArray({ control, name: "secrets" }); - - const isReadOnly = isFoldersLoading - ? true - : permission.can( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ) && - permission.cannot( - ProjectPermissionActions.Edit, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ) && - permission.cannot( - ProjectPermissionActions.Create, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ) && - permission.cannot( - ProjectPermissionActions.Delete, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); - - const canDoRollback = !isReadOnly; - const isSubmitDisabled = - isReadOnly || - // not in rollback mode and no form has changed - (!isRollbackMode && !isDirty) || - // in rollback mode and don't have permission to do it - (isRollbackMode && - permission.cannot(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback)) || - isSubmitting; - - useEffect(() => { - if (!isSnapshotChanging && Boolean(snapshotId)) { - reset({ secrets: snapshotSecret?.secrets, isSnapshotMode: true }); - } - }, [isSnapshotChanging]); - - useEffect(() => { - setHasUnsavedChanges(!isSubmitDisabled); - }, [isSubmitDisabled]); - - const onSortSecrets = () => { - const dir = sortDir === "asc" ? "desc" : "asc"; - const sec = getValues("secrets") || []; - const sortedSec = sec.sort((a, b) => - dir === "asc" ? a?.key?.localeCompare(b?.key || "") : b?.key?.localeCompare(a?.key || "") - ); - setValue("secrets", sortedSec); - setSortDir(dir); - }; - - const handleUploadedEnv = (uploadedSec: TSecOverwriteOpt["secrets"]) => { - const sec = getValues("secrets") || []; - const conflictingSec = sec.filter(({ key }) => Boolean(uploadedSec?.[key])); - const conflictingSecIds = conflictingSec.reduce>( - (prev, curr) => ({ - ...prev, - [curr.key]: true - }), - {} - ); - // filter to get all conflicting ones - const conflictingUploadedSec = { ...uploadedSec }; - // append non conflicting ones - Object.keys(uploadedSec).forEach((key) => { - if (!conflictingSecIds?.[key]) { - delete conflictingUploadedSec[key]; - sec.push({ - ...DEFAULT_SECRET_VALUE, - key, - value: uploadedSec[key].value, - comment: uploadedSec[key].comments.join(",") - }); - } - }); - setValue("secrets", sec, { shouldDirty: true }); - if (conflictingSec.length > 0) { - handlePopUpOpen("uploadedSecOpts", { secrets: conflictingUploadedSec }); - } - }; - - const onOverwriteSecrets = () => { - const sec = getValues("secrets") || []; - const uploadedSec = (popUp?.uploadedSecOpts?.data as TSecOverwriteOpt)?.secrets; - const data: Array<{ key: string; index: number }> = []; - sec.forEach(({ key }, index) => { - if (uploadedSec?.[key]) data.push({ key, index }); - }); - data.forEach(({ key, index }) => { - const { value, comments } = uploadedSec[key]; - const comment = comments.join(", "); - sec[index] = { - ...DEFAULT_SECRET_VALUE, - key, - value, - comment, - tags: sec[index].tags - }; - }); - setValue("secrets", sec, { shouldDirty: true }); - handlePopUpClose("uploadedSecOpts"); - }; - - const onSecretRollback = async () => { - if (!snapshotSecret?.version) { - createNotification({ - text: "Failed to find secret version", - type: "success" - }); - return; - } - try { - await performSecretRollback({ - workspaceId, - version: snapshotSecret.version, - environment, - folderId - }); - setValue("isSnapshotMode", false); - setSnaphotId(null); - queryClient.invalidateQueries(secretKeys.getProjectSecret(workspaceId, environment)); - createNotification({ - text: "Successfully rollback secrets", - type: "success" - }); - } catch (error) { - console.log(error); - createNotification({ - text: "Failed to rollback secrets", - type: "error" - }); - } - }; - - const onAppendSecret = () => { - setSearchFilter(""); - append(DEFAULT_SECRET_VALUE); - }; - - const onSaveSecret = async ({ secrets: userSec = [], isSnapshotMode }: FormData) => { - if (isSnapshotMode) { - await onSecretRollback(); - return; - } - // just closing this if save is triggered from drawer - handlePopUpClose("secretDetails"); - // encrypt and format the secrets to batch api format - // requests = [ {method:"", secret:""} ] - const batchedSecret = transformSecretsToBatchSecretReq( - deletedSecretIds.current, - latestFileKey, - userSec, - secrets?.secrets - ); - // type check - if (batchedSecret.length === 0) { - reset(); - return; - } - try { - await batchSecretOp({ - requests: batchedSecret, - workspaceId, - folderId, - environment - }); - createNotification({ - text: "Successfully saved changes", - type: "success" - }); - deletedSecretIds.current = []; - if (!hasUserPushed) { - await registerUserAction(USER_ACTION_PUSH); - } - } catch (error) { - console.log(error); - createNotification({ - text: "Failed to save changes", - type: "error" - }); - } - }; - - const onDrawerOpen = useCallback((id: string | undefined, index: number) => { - handlePopUpOpen("secretDetails", { id, index } as TSecretDetailsOpen); - }, []); - - const onEnvChange = (slug: string) => { - if (hasUnsavedChanges) { - // eslint-disable-next-line no-alert - if (!window.confirm(leaveConfirmDefaultMessage)) return; - } - - const query: Record = { ...router.query, env: slug }; - delete query.folderId; - router.push({ - pathname: router.pathname, - query - }); - }; - - const handleDownloadSecret = () => { - const secretsFromImport: { key: string; value: string; comment: string }[] = []; - importedSecrets?.forEach(({ secrets: impSec }) => { - impSec.forEach((el) => { - secretsFromImport.push({ key: el.key, value: el.value, comment: el.comment }); - }); - }); - downloadSecret(getValues("secrets"), secretsFromImport, environment); - }; - - // record all deleted ids - // This will make final deletion easier - const onSecretDelete = useCallback( - (index: number, secretName: string, id?: string, overrideId?: string) => { - if (id) - deletedSecretIds.current.push({ - id, - secretName - }); - if (overrideId) - deletedSecretIds.current.push({ - id: overrideId, - secretName - }); - remove(index); - // just the case if this is called from drawer - handlePopUpClose("secretDetails"); - }, - [] - ); - - const onCreateWsTag = useCallback( - async (tagName: string, tagColor: string) => { - try { - await createWsTag({ - workspaceID: workspaceId, - tagName, - tagColor, - tagSlug: tagName.replace(" ", "_") - }); - handlePopUpClose("addTag"); - createNotification({ - text: "Successfully created a tag", - type: "success" - }); - } catch (error) { - console.error(error); - createNotification({ - text: "Failed to create a tag", - type: "error" - }); - } - }, - [workspaceId] - ); - - const handleFolderOpen = useCallback( - (id: string) => { - setSearchFilter(""); - router.push({ - pathname: router.pathname, - query: { - id: workspaceId, - env: environment, - folderId: id - } - }); - }, - [environment, workspaceId] - ); - - const isEditFolder = Boolean(popUp?.folderForm?.data); - - // FOLDER SECTION - const handleFolderCreate = async (name: string) => { - try { - await createFolder({ - workspaceId, - environment, - folderName: name, - parentFolderId: folderId - }); - createNotification({ - type: "success", - text: "Successfully created folder" - }); - handlePopUpClose("folderForm"); - } catch (error) { - console.error(error); - createNotification({ - text: "Failed to create folder", - type: "error" - }); - } - }; - - const handleFolderUpdate = useCallback( - async (name: string) => { - const { id } = popUp?.folderForm?.data as TDeleteFolderForm; - try { - await updateFolder({ - folderId: id, - workspaceId, - environment, - name - }); - createNotification({ - type: "success", - text: "Successfully updated folder" - }); - handlePopUpClose("folderForm"); - } catch (error) { - console.error(error); - createNotification({ - text: "Failed to update folder", - type: "error" - }); - } - }, - [environment, (popUp?.folderForm?.data as TDeleteFolderForm)?.id] - ); - - const handleFolderDelete = useCallback(async () => { - const { id } = popUp?.deleteFolder?.data as TDeleteFolderForm; - try { - deleteFolder({ - workspaceId, - environment, - folderId: id - }); - createNotification({ - type: "success", - text: "Successfully removed folder" - }); - handlePopUpClose("deleteFolder"); - } catch (error) { - console.error(error); - createNotification({ - text: "Failed to remove folder", - type: "error" - }); - } - }, [(popUp?.deleteFolder?.data as TDeleteFolderForm)?.id]); - - // SECRET IMPORT SECTION - const handleSecretImportCreate = async (env: string, secPath: string) => { - try { - await createSecretImport({ - workspaceId, - environment, - folderId, - secretImport: { - environment: env, - secretPath: secPath - } - }); - createNotification({ - type: "success", - text: "Successfully create secret link" - }); - handlePopUpClose("addSecretImport"); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to create secret link", - type: "error" - }); - } - }; - - const handleSecretImportDelete = async () => { - const { environment: importEnv, secretPath: impSecPath } = popUp.deleteSecretImport - ?.data as TDeleteSecretImport; - try { - if (secretImportCfg?._id) { - await deleteSecretImport({ - workspaceId, - environment, - folderId, - id: secretImportCfg?._id, - secretImportEnv: importEnv, - secretImportPath: impSecPath - }); - handlePopUpClose("deleteSecretImport"); - createNotification({ - type: "success", - text: "Successfully removed secret link" - }); - } - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to remove secret link", - type: "error" - }); - } - }; - - const handleDragEnd = (evt: DragEndEvent) => { - const { active, over } = evt; - if (over?.id && active.id !== over.id) { - const oldIndex = items.findIndex(({ id }) => id === active.id); - const newIndex = items.findIndex(({ id }) => id === over.id); - const newImportOrder = arrayMove(items, oldIndex, newIndex); - setItems(newImportOrder); - updateSecretImportSync({ - workspaceId, - environment, - folderId, - id: secretImportCfg?._id || "", - secretImports: newImportOrder.map((el) => ({ - environment: el.environment, - secretPath: el.secretPath - })) - }); - } - }; - - // OPTIMIZATION HOOKS PURELY FOR PERFORMANCE AND TO AVOID RE-RENDERING - const handleCreateTagModalOpen = useCallback(() => handlePopUpOpen("addTag"), []); - const handleFolderCreatePopUpOpen = useCallback( - (id: string, name: string) => handlePopUpOpen("folderForm", { id, name }), - [] - ); - const handleFolderDeletePopUpOpen = useCallback( - (id: string, name: string) => handlePopUpOpen("deleteFolder", { id, name }), - [] - ); - const handleSecretImportDelPopUpOpen = useCallback( - (impSecEnv: string, impSecPath: string) => - handlePopUpOpen("deleteSecretImport", { - environment: impSecEnv, - secretPath: impSecPath - }), - [] - ); - - // when secrets is not loading and secrets list is empty - const isDashboardSecretEmpty = !isSecretsLoading && !fields?.length; - - // folder list checks - const isFolderListLoading = isRollbackMode ? isSnapshotSecretsLoading : isFoldersLoading; - const folderList = isRollbackMode ? snapshotSecret?.folders : folderData?.folders; - - // when using snapshot mode and snapshot is loading and snapshot list is empty - const isFoldersEmpty = !isFolderListLoading && !folderList?.length; - const isSnapshotSecretEmtpy = - isRollbackMode && !isSnapshotSecretsLoading && !snapshotSecret?.secrets?.length; - const isSecretEmpty = (!isRollbackMode && isDashboardSecretEmpty) || isSnapshotSecretEmtpy; - const isSecretImportEmpty = !secretImportCfg?.imports?.length; - const isEmptyPage = isFoldersEmpty && isSecretEmpty && isSecretImportEmpty; - - if (isSecretsLoading) { - return ( -
- loading animation -
- ); - } - - return ( -
-
- {/* breadcrumb row */} -
- envir.slug === environment)?.[0]?.name || "" - } - isFolderMode - folders={folderData?.dir} - isProjectRelated - userAvailableEnvs={userAvailableEnvs} - onEnvChange={onEnvChange} - /> -
-
-
{isRollbackMode ? "Secret Snapshot" : ""}
- {isRollbackMode && Boolean(snapshotSecret) && ( - - {new Date(snapshotSecret?.createdAt || "").toLocaleString()} - - )} -
- {/* Environment, search and other action row */} -
-
- setSearchFilter(e.target.value)} - leftIcon={} - /> -
-
-
- - - - - - - -
- -
-
-
-
-
- - setIsSecretValueHidden.toggle()} - > - - - -
- - {(isAllowed) => ( -
- - handlePopUpOpen("secretSnapshots")} - > - - - -
- )} -
- - {(isAllowed) => ( -
- -
- )} -
- {!isReadOnly && !isRollbackMode && ( -
- - {(isAllowed) => ( - - )} - - - -
- -
-
- -
-
- - {(isAllowed) => ( - - )} - -
-
- - {(isAllowed) => ( - - )} - -
-
-
-
-
- )} - {isRollbackMode && ( - - )} - - - - -
-
-
- {!isEmptyPage && ( - - - - - - - - {permission.can( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ) ? ( - fields.map(({ id, _id }, index) => ( - - )) - ) : ( - - - - )} - {!isReadOnly && !isRollbackMode && ( - - - - )} - -
- -
- - {(isAllowed) => ( - - )} - -
-
-
- )} - - handlePopUpToggle("secretSnapshots", isOpen)} - fetchNextPage={fetchNextPage} - hasNextPage={hasNextPage} - snapshotId={snapshotId} - isFetchingNextPage={isFetchingNextPage} - secretSnaphots={secretSnaphots} - onSelectSnapshot={setSnaphotId} - /> - handlePopUpToggle("secretDetails", isOpen)} - secretVersion={secretVersion} - index={(popUp?.secretDetails?.data as TSecretDetailsOpen)?.index} - onEnvCompare={(key) => handlePopUpOpen("compareSecrets", key)} - /> - - -
- {/* secrets table and drawers, modals */} -
- {/* Create a new tag modal */} - { - handlePopUpToggle("addTag", open); - }} - > - - - - - {/* Uploaded env override or not confirmation modal */} - handlePopUpToggle("uploadedSecOpts", open)} - > - handlePopUpClose("uploadedSecOpts")} - > - Keep old - , - - ]} - > -
-
Your file contains following duplicate secrets
-
- {Object.keys((popUp?.uploadedSecOpts?.data as TSecOverwriteOpt)?.secrets || {}) - ?.map((key) => key) - .join(", ")} -
-
Are you sure you want to overwrite these secrets?
-
-
-
- handlePopUpToggle("folderForm", isOpen)} - > - - - - - handlePopUpToggle("addSecretImport", isOpen)} - > - - - - - handlePopUpToggle("deleteFolder", isOpen)} - onDeleteApproved={handleFolderDelete} - /> - handlePopUpToggle("deleteSecretImport", isOpen)} - onDeleteApproved={handleSecretImportDelete} - /> - handlePopUpToggle("compareSecrets", open)} - > - - - - - {subscription && ( - handlePopUpToggle("upgradePlan", isOpen)} - text={ - subscription.slug === null - ? "You can perform point-in-time recovery under an Enterprise license" - : "You can perform point-in-time recovery if you switch to Infisical's Team plan" - } - /> - )} -
- ); -}; diff --git a/frontend/src/views/DashboardPage/DashboardPage.utils.ts b/frontend/src/views/DashboardPage/DashboardPage.utils.ts deleted file mode 100644 index bd0131e8e..000000000 --- a/frontend/src/views/DashboardPage/DashboardPage.utils.ts +++ /dev/null @@ -1,289 +0,0 @@ -/* eslint-disable @typescript-eslint/naming-convention */ -import crypto from "crypto"; - -import * as yup from "yup"; - -import { - decryptAssymmetric, - encryptSymmetric -} from "@app/components/utilities/cryptography/crypto"; -import { BatchSecretDTO, DecryptedSecret } from "@app/hooks/api/secrets/types"; - -export enum SecretActionType { - Created = "created", - Modified = "modified", - Deleted = "deleted" -} - -export const DEFAULT_SECRET_VALUE = { - _id: undefined, - overrideAction: undefined, - idOverride: undefined, - valueOverride: undefined, - comment: "", - key: "", - value: "", - tags: [] -}; - -const secretSchema = yup.object({ - _id: yup.string(), - key: yup - .string() - .trim() - .required() - .label("Secret key") - .test("starts-with-number", "Should start with an alphabet", (val) => - Boolean(val?.charAt(0)?.match(/[a-zA-Z]/i)) - ) - .test({ - name: "duplicate-keys", - // TODO:(akhilmhdh) ts keeps throwing from not found need to see how to resolve this - test: (val, ctx: any) => { - const secrets: Array<{ key: string }> = ctx?.from?.[1]?.value?.secrets || []; - const duplicateKeys: Record = {}; - secrets?.forEach(({ key }, index) => { - if (key === val) duplicateKeys[index + 1] = true; - }); - const pos = Object.keys(duplicateKeys); - if (pos.length <= 1) { - return true; - } - return ctx.createError({ message: `Same key in row ${pos.join(", ")}` }); - } - }), - value: yup.string().trim(), - comment: yup.string().trim(), - tags: yup.array( - yup.object({ - _id: yup.string().required(), - name: yup.string().required(), - slug: yup.string().required(), - tagColor: yup.string().nullable(), - }) - ), - overrideAction: yup.string().notRequired().oneOf(Object.values(SecretActionType)), - idOverride: yup.string().notRequired(), - valueOverride: yup.string().trim().notRequired() -}); - -export const schema = yup.object({ - isSnapshotMode: yup.bool().notRequired(), - secrets: yup.array(secretSchema) -}); - -export type FormData = yup.InferType; -export type TSecretDetailsOpen = { index: number; id: string }; -export type TSecOverwriteOpt = { secrets: Record }; - -// to convert multi line into single line ones by quoting them and changing to string \n -const formatMultiValueEnv = (val?: string) => { - if (!val) return ""; - if (!val.match("\n")) return val; - return `"${val.replace(/\n/g, "\\n")}"`; -}; - -export const downloadSecret = ( - secrets: FormData["secrets"] = [], - importedSecrets: { key: string; value?: string; comment?: string }[] = [], - env: string = "unknown" -) => { - const importSecPos: Record = {}; - importedSecrets.forEach((el, index) => { - importSecPos[el.key] = index; - }); - const finalSecret = [...importedSecrets]; - secrets.forEach(({ key, value, valueOverride, overrideAction, comment }) => { - const finalVal = - overrideAction && overrideAction !== SecretActionType.Deleted ? valueOverride : value; - const newValue = { - key, - value: formatMultiValueEnv(finalVal), - comment - }; - // can also be zero thus failing - if (typeof importSecPos?.[key] === "undefined") { - finalSecret.push(newValue); - } else { - finalSecret[importSecPos[key]] = newValue; - } - }); - - let file = ""; - finalSecret.forEach(({ key, value, comment }) => { - if (comment) { - file += `# ${comment}\n${key}=${value}\n`; - return; - } - file += `${key}=${value}\n`; - }); - - const blob = new Blob([file]); - const fileDownloadUrl = URL.createObjectURL(blob); - const alink = document.createElement("a"); - alink.href = fileDownloadUrl; - alink.download = `${env}.env`; - alink.click(); -}; - -/* - * Below functions are used convert the dashboard secrets to the bulk secret creation request format - * They are encrypted back - * Formatted to [ { request: "", secret:{} } ] - */ -const encryptASecret = (randomBytes: string, key: string, value?: string, comment?: string) => { - // encrypt key - const { - ciphertext: secretKeyCiphertext, - iv: secretKeyIV, - tag: secretKeyTag - } = encryptSymmetric({ - plaintext: key, - key: randomBytes - }); - - // encrypt value - const { - ciphertext: secretValueCiphertext, - iv: secretValueIV, - tag: secretValueTag - } = encryptSymmetric({ - plaintext: value ?? "", - key: randomBytes - }); - - // encrypt comment - const { - ciphertext: secretCommentCiphertext, - iv: secretCommentIV, - tag: secretCommentTag - } = encryptSymmetric({ - plaintext: comment ?? "", - key: randomBytes - }); - - return { - secretKeyCiphertext, - secretKeyIV, - secretKeyTag, - secretValueCiphertext, - secretValueIV, - secretValueTag, - secretCommentCiphertext, - secretCommentIV, - secretCommentTag - }; -}; - -const deepCompareSecrets = (lhs: DecryptedSecret, rhs: any) => - lhs.key === rhs.key && - lhs.value === rhs.value && - lhs.comment === rhs.comment && - lhs?.valueOverride === rhs?.valueOverride && - JSON.stringify(lhs.tags) === JSON.stringify(rhs.tags); - -export const transformSecretsToBatchSecretReq = ( - deletedSecretIds: { id: string; secretName: string; }[], - latestFileKey: any, - secrets: FormData["secrets"], - intialValues: DecryptedSecret[] = [] -) => { - // deleted secrets - const secretsToBeDeleted: BatchSecretDTO["requests"] = deletedSecretIds.map(({ id, secretName }) => ({ - method: "DELETE", - secret: { - _id: id, - secretName - } - })); - - const secretsToBeUpdated: BatchSecretDTO["requests"] = []; - const secretsToBeCreated: BatchSecretDTO["requests"] = []; - const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string; - - const randomBytes = latestFileKey - ? decryptAssymmetric({ - ciphertext: latestFileKey.encryptedKey, - nonce: latestFileKey.nonce, - publicKey: latestFileKey.sender.publicKey, - privateKey: PRIVATE_KEY - }) - : crypto.randomBytes(16).toString("hex"); - - secrets?.forEach((secret) => { - const { - _id, - idOverride, - value, - valueOverride, - overrideAction, - tags = [], - comment, - key - } = secret; - if (!idOverride && overrideAction === SecretActionType.Created) { - secretsToBeCreated.push({ - method: "POST", - secret: { - type: "personal", - tags, - secretName: key, - ...encryptASecret(randomBytes, key, valueOverride, comment) - } - }); - } - // to be created ones as they don't have server generated id - if (!_id) { - secretsToBeCreated.push({ - method: "POST", - secret: { - type: "shared", - tags, - secretName: key, - ...encryptASecret(randomBytes, key, value, comment) - } - }); - return; // exit as updated and delete case won't happen when created - } - // has an id means this is updated one - if (_id) { - // check value has changed or not - const initialSecretValue = intialValues?.find(({ _id: secId }) => secId === _id)!; - if (!deepCompareSecrets(initialSecretValue, secret)) { - secretsToBeUpdated.push({ - method: "PATCH", - secret: { - _id, - type: "shared", - tags, - secretName: key, - ...encryptASecret(randomBytes, key, value, comment) - } - }); - } - } - if (idOverride) { - // if action is deleted meaning override has been removed but id is kept to collect at this point - if (overrideAction === SecretActionType.Deleted) { - secretsToBeDeleted.push({ method: "DELETE", secret: { _id: idOverride, secretName: key } }); - } else { - // if not deleted action then as id is there its an updated - const initialSecretValue = intialValues?.find(({ _id: secId }) => secId === _id)!; - if (!deepCompareSecrets(initialSecretValue, secret)) { - secretsToBeUpdated.push({ - method: "PATCH", - secret: { - _id: idOverride, - type: "personal", - tags, - secretName: key, - ...encryptASecret(randomBytes, key, valueOverride, comment) - } - }); - } - } - } - }); - - return secretsToBeCreated.concat(secretsToBeUpdated, secretsToBeDeleted); -}; diff --git a/frontend/src/views/DashboardPage/components/CompareSecret/CompareSecret.tsx b/frontend/src/views/DashboardPage/components/CompareSecret/CompareSecret.tsx deleted file mode 100644 index a14ccedd2..000000000 --- a/frontend/src/views/DashboardPage/components/CompareSecret/CompareSecret.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { useCallback } from "react"; - -import { FormControl, Input, Spinner } from "@app/components/v2"; -import { useGetProjectSecrets, useGetUserWsKey } from "@app/hooks/api"; - -type SecretValueProps = { - workspaceId: string; - envName: string; - env: string; - secretKey: string; -}; - -const SecretValue = ({ workspaceId, env, envName, secretKey }: SecretValueProps) => { - const { data: latestFileKey } = useGetUserWsKey(workspaceId); - const { data: secret, isLoading: isSecretsLoading } = useGetProjectSecrets({ - workspaceId, - env, - decryptFileKey: latestFileKey! - }); - - const getValue = useCallback( - (data: typeof secret) => { - const sec = data?.secrets?.find(({ key: secKey }) => secKey === secretKey); - return sec?.value || "Not found"; - }, - [secretKey] - ); - - return ( - - : undefined} - /> - - ); -}; - -type Props = { - workspaceId: string; - secretKey: string; - envs: Array<{ name: string; slug: string }>; -}; - -export const CompareSecret = ({ workspaceId, secretKey, envs }: Props): JSX.Element => { - // should not do anything until secretKey is available - if (!secretKey) return
; - - return ( -
- {envs.map(({ name, slug }) => ( - - ))} -
- ); -}; diff --git a/frontend/src/views/DashboardPage/components/CompareSecret/index.tsx b/frontend/src/views/DashboardPage/components/CompareSecret/index.tsx deleted file mode 100644 index 260a702df..000000000 --- a/frontend/src/views/DashboardPage/components/CompareSecret/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { CompareSecret } from "./CompareSecret"; diff --git a/frontend/src/views/DashboardPage/components/CreateTagModal/CreateTagModal.tsx b/frontend/src/views/DashboardPage/components/CreateTagModal/CreateTagModal.tsx deleted file mode 100644 index b399ee886..000000000 --- a/frontend/src/views/DashboardPage/components/CreateTagModal/CreateTagModal.tsx +++ /dev/null @@ -1,191 +0,0 @@ -import { useEffect, useState } from "react"; -import { Controller, useForm } from "react-hook-form"; -import { - faCheck -} from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { yupResolver } from "@hookform/resolvers/yup"; -import * as yup from "yup"; - -import { Button, FormControl, Input, ModalClose, Tooltip } from "@app/components/v2"; - -import { isValidHexColor } from "../../../../components/utilities/isValidHexColor"; -import { secretTagsColors } from "../../../../const" -import { TagColor } from "../../../../hooks/api/tags/types"; - - -type Props = { - onCreateTag: (tagName: string, tagColor: string) => Promise; -}; - -const createTagSchema = yup.object({ - name: yup.string().required().trim().label("Tag Name") -}); -type FormData = yup.InferType; - -export const CreateTagModal = ({ onCreateTag }: Props): JSX.Element => { - const { - control, - reset, - formState: { isSubmitting }, - handleSubmit - } = useForm({ - resolver: yupResolver(createTagSchema) - }); - - const [tagsColors] = useState(secretTagsColors) - const [selectedTagColor, setSelectedTagColor] = useState(tagsColors[0]) - const [showHexInput, setShowHexInput] = useState(false) - const [tagColor, setTagColor] = useState("") - - - const onFormSubmit = async ({ name }: FormData) => { - await onCreateTag(name, tagColor); - reset(); - }; - - useEffect(() => { - const clonedTagColors = [...tagsColors] - const selectedTagBgColor = clonedTagColors.find($tagColor => $tagColor.selected); - - if (selectedTagBgColor) { - setSelectedTagColor(selectedTagBgColor); - setTagColor(selectedTagBgColor.hex); - } - }, []) - - useEffect(() => { - const tagsList = document.querySelector(".secret-tags-wrapper") - const tagsHexWrapper = document.querySelector(".tags-hex-wrapper") - - if (showHexInput) { - tagsList?.classList.add("hide-tags") - tagsList?.classList.remove("show-tags") - tagsHexWrapper?.classList.add("show-hex-input") - tagsHexWrapper?.classList.remove("hide-hex-input") - } else { - tagsList?.classList.remove("hide-tags") - tagsList?.classList.add("show-tags") - tagsHexWrapper?.classList.remove("show-hex-input") - tagsHexWrapper?.classList.add("hide-hex-input") - } - }, [showHexInput]) - - - const handleColorChange = (clickedTagColor: TagColor) => { - const updatedTagColors = [...tagsColors]; - const clickedTagColorIndex = updatedTagColors.findIndex(($tagColor) => $tagColor.id === clickedTagColor.id); - const updatedClickedTagColor = updatedTagColors[clickedTagColorIndex]; - - updatedTagColors.forEach((tgColor) => { - // eslint-disable-next-line no-param-reassign - tgColor.selected = false; - }); - - if (selectedTagColor.id !== clickedTagColor.id) { - updatedClickedTagColor.selected = !updatedClickedTagColor.selected; - setSelectedTagColor(updatedClickedTagColor); - setTagColor(updatedClickedTagColor.hex); - } - }; - - return ( -
- ( - - - - )} - /> - -
-
Tag Color
-
-
-
-
- -
-
- { - tagsColors.map(($tagColor: TagColor) => { - return ( -
- -
handleColorChange($tagColor)} - tabIndex={0} role="button" - onKeyDown={() => { }} - > - { - $tagColor.selected && - } -
-
-
- ) - }) - } -
- -
-
- { - isValidHexColor(tagColor) && ( -
- -
- ) - } - - { - !isValidHexColor(tagColor) && ( -
- ) - } -
-
- ) => setTagColor(e.target.value)} - /> -
-
- -
-
-
setShowHexInput((prev) => !prev)} style={{ border: "1px solid rgba(220, 216, 254, 0.376)" }} - tabIndex={0} role="button" - onKeyDown={() => { }}> - { - !showHexInput && # - } -
-
-
-
-
- -
- - - - -
- - ); -}; diff --git a/frontend/src/views/DashboardPage/components/CreateTagModal/index.tsx b/frontend/src/views/DashboardPage/components/CreateTagModal/index.tsx deleted file mode 100644 index a448ba55c..000000000 --- a/frontend/src/views/DashboardPage/components/CreateTagModal/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export {CreateTagModal} from "./CreateTagModal" \ No newline at end of file diff --git a/frontend/src/views/DashboardPage/components/FolderSection/FolderSection.tsx b/frontend/src/views/DashboardPage/components/FolderSection/FolderSection.tsx deleted file mode 100644 index 4c02fab91..000000000 --- a/frontend/src/views/DashboardPage/components/FolderSection/FolderSection.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { memo } from "react"; -import { subject } from "@casl/ability"; -import { faEdit, faFolder, faXmark } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; - -import { ProjectPermissionCan } from "@app/components/permissions"; -import { IconButton, Tooltip } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; - -type Props = { - folders?: Array<{ id: string; name: string }>; - search?: string; - environment: string; - secretPath: string; - onFolderUpdate: (folderId: string, name: string) => void; - onFolderDelete: (folderId: string, name: string) => void; - onFolderOpen: (folderId: string) => void; -}; - -export const FolderSection = memo( - ({ - onFolderUpdate: handleFolderUpdate, - onFolderDelete: handleFolderDelete, - onFolderOpen: handleFolderOpen, - search = "", - folders = [], - environment, - secretPath - }: Props) => { - return ( - <> - {folders - .filter(({ name }) => name.toLowerCase().includes(search.toLowerCase())) - .map(({ id, name }) => ( - - - - - -
null} - tabIndex={0} - role="button" - onClick={() => handleFolderOpen(id)} - > - {name} -
-
- - {(isAllowed) => ( -
- - handleFolderUpdate(id, name)} - ariaLabel="expand" - > - - - -
- )} -
- - {(isAllowed) => ( -
- - handleFolderDelete(id, name)} - > - - - -
- )} -
-
- - - ))} - - ); - } -); - -FolderSection.displayName = "FolderSection"; diff --git a/frontend/src/views/DashboardPage/components/FolderSection/index.tsx b/frontend/src/views/DashboardPage/components/FolderSection/index.tsx deleted file mode 100644 index 861ebd934..000000000 --- a/frontend/src/views/DashboardPage/components/FolderSection/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export { FolderForm } from "./FolderForm"; -export { FolderSection } from "./FolderSection"; -export * from "./types"; diff --git a/frontend/src/views/DashboardPage/components/FolderSection/types.ts b/frontend/src/views/DashboardPage/components/FolderSection/types.ts deleted file mode 100644 index f2fe30b10..000000000 --- a/frontend/src/views/DashboardPage/components/FolderSection/types.ts +++ /dev/null @@ -1,2 +0,0 @@ -export type TEditFolderForm = { id: string; name: string }; -export type TDeleteFolderForm = { id: string; name: string }; diff --git a/frontend/src/views/DashboardPage/components/SecretDetailDrawer/SecretDetailDrawer.tsx b/frontend/src/views/DashboardPage/components/SecretDetailDrawer/SecretDetailDrawer.tsx deleted file mode 100644 index ef85c4896..000000000 --- a/frontend/src/views/DashboardPage/components/SecretDetailDrawer/SecretDetailDrawer.tsx +++ /dev/null @@ -1,237 +0,0 @@ -import { useFormContext, useWatch } from "react-hook-form"; -import { subject } from "@casl/ability"; -import { faCircle, faCircleDot, faShuffle } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; - -import { ProjectPermissionCan } from "@app/components/permissions"; -import { - Button, - Drawer, - DrawerContent, - FormControl, - Input, - Popover, - PopoverContent, - PopoverTrigger, - Switch, - TextArea -} from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; -import { useToggle } from "@app/hooks"; - -import { FormData, SecretActionType } from "../../DashboardPage.utils"; -import { GenRandomNumber } from "./GenRandomNumber"; - -type Props = { - isDrawerOpen: boolean; - environment: string; - secretPath: string; - onOpenChange: (isOpen: boolean) => void; - index: number; - isReadOnly?: boolean; - onEnvCompare: (secretKey: string) => void; - secretVersion?: Array<{ id: string; createdAt: string; value: string }>; - // to record the ids of deleted ones - onSecretDelete: (index: number, secretName: string, id?: string, overrideId?: string) => void; - onSave: () => void; -}; - -export const SecretDetailDrawer = ({ - isDrawerOpen, - onOpenChange, - index, - secretVersion = [], - isReadOnly, - onSecretDelete, - onSave, - onEnvCompare, - environment, - secretPath -}: Props): JSX.Element => { - const [canRevealSecVal, setCanRevealSecVal] = useToggle(); - const [canRevealSecOverride, setCanRevealSecOverride] = useToggle(); - - const { register, setValue, control, getValues } = useFormContext(); - - const overrideAction = useWatch({ control, name: `secrets.${index}.overrideAction` }); - const isOverridden = - overrideAction === SecretActionType.Created || overrideAction === SecretActionType.Modified; - - const onSecretOverride = () => { - const secret = getValues(`secrets.${index}`); - if (isOverridden) { - // when user created a new override but then removes - if (SecretActionType.Created) { - setValue(`secrets.${index}.valueOverride`, "", { shouldDirty: true }); - } - setValue(`secrets.${index}.overrideAction`, SecretActionType.Deleted, { shouldDirty: true }); - } else { - setValue( - `secrets.${index}.overrideAction`, - secret?.idOverride ? SecretActionType.Modified : SecretActionType.Created, - { shouldDirty: true } - ); - } - }; - - return ( - - -
- -
-
- - {(isAllowed) => ( - - )} - - - {(isAllowed) => ( - - )} - -
-
- } - > -
- - - - - - - - - } - /> - - - setValue(`secrets.${index}.value`, val, { shouldDirty: true }) - } - /> - - - -
- - Override with a personal value - -
- - - - - - } - /> - - - setValue(`secrets.${index}.valueOverride`, val, { shouldDirty: true }) - } - /> - - - -
-
Version History
-
- {secretVersion?.map(({ createdAt, value, id }, i) => ( -
-
-
- -
-
- {new Date(createdAt).toLocaleDateString("en-US", { - year: "numeric", - month: "2-digit", - day: "2-digit", - hour: "2-digit", - minute: "2-digit", - second: "2-digit" - })} -
-
-
-
Value:
-
{value}
-
-
- ))} -
-
- -