From 579516bd38200cf37871b47fa425224e26e20e21 Mon Sep 17 00:00:00 2001 From: akhilmhdh Date: Mon, 29 May 2023 22:23:22 +0530 Subject: [PATCH] feat(folders): implemented ui for folders in dashboard --- .../src/components/navigation/NavHeader.tsx | 46 ++- .../components/v2/EmptyState/EmptyState.tsx | 23 +- .../src/components/v2/Tooltip/Tooltip.tsx | 2 +- frontend/src/hooks/api/index.tsx | 1 + .../src/hooks/api/secretFolders/index.tsx | 1 + .../src/hooks/api/secretFolders/queries.tsx | 129 ++++++++ frontend/src/hooks/api/secretFolders/types.ts | 32 ++ .../src/hooks/api/secretSnapshots/queries.tsx | 77 +++-- .../src/hooks/api/secretSnapshots/types.ts | 5 + frontend/src/hooks/api/secrets/queries.tsx | 35 ++- frontend/src/hooks/api/secrets/types.ts | 3 + .../src/views/DashboardPage/DashboardPage.tsx | 282 ++++++++++++++---- .../components/FolderSection/FolderForm.tsx | 75 +++++ .../FolderSection/FolderSection.tsx | 77 +++++ .../components/FolderSection/index.tsx | 3 + .../components/FolderSection/types.ts | 2 + .../components/SecretInputRow/MaskedInput.tsx | 6 +- .../SecretInputRow/SecretInputRow.tsx | 35 +-- .../SecretTableHeader/SecretTableHeader.tsx | 2 +- 19 files changed, 708 insertions(+), 128 deletions(-) create mode 100644 frontend/src/hooks/api/secretFolders/index.tsx create mode 100644 frontend/src/hooks/api/secretFolders/queries.tsx create mode 100644 frontend/src/hooks/api/secretFolders/types.ts create mode 100644 frontend/src/views/DashboardPage/components/FolderSection/FolderForm.tsx create mode 100644 frontend/src/views/DashboardPage/components/FolderSection/FolderSection.tsx create mode 100644 frontend/src/views/DashboardPage/components/FolderSection/index.tsx create mode 100644 frontend/src/views/DashboardPage/components/FolderSection/types.ts diff --git a/frontend/src/components/navigation/NavHeader.tsx b/frontend/src/components/navigation/NavHeader.tsx index 4df5c2d20..ba9c1a965 100644 --- a/frontend/src/components/navigation/NavHeader.tsx +++ b/frontend/src/components/navigation/NavHeader.tsx @@ -1,12 +1,23 @@ import Link from 'next/link'; import { useRouter } from 'next/router'; -import { faAngleRight } from '@fortawesome/free-solid-svg-icons'; +import { faAngleRight, faHome } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { useOrganization, useWorkspace } from '@app/context'; import { Select, SelectItem, Tooltip } from '../v2'; +type Props = { + pageName: string; + isProjectRelated?: boolean; + isOrganizationRelated?: boolean; + currentEnv?: string; + userAvailableEnvs?: any[]; + onEnvChange?: (slug: string) => void; + folders?: Array<{ id: string; name: string }>; + isFolderMode?: boolean; +}; + // TODO: make links clickable and clean up /** @@ -29,15 +40,10 @@ export default function NavHeader({ isOrganizationRelated, currentEnv, userAvailableEnvs, - onEnvChange -}: { - pageName: string; - isProjectRelated?: boolean; - isOrganizationRelated?: boolean; - currentEnv?: string; - userAvailableEnvs?: any[]; - onEnvChange?: (slug: string) => void; -}): JSX.Element { + onEnvChange, + folders, + isFolderMode +}: Props): JSX.Element { const { currentWorkspace } = useWorkspace(); const { currentOrg } = useOrganization(); const router = useRouter(); @@ -95,6 +101,26 @@ export default function NavHeader({ )} + {isFolderMode && + folders?.map(({ id, name }, index) => { + const query = { ...router.query }; + if (name !== 'root') query.folderId = id; + else delete query.folderId; + return ( +
+ + {index + 1 === folders?.length ? ( + {name} + ) : ( + + + {name === 'root' ? : name} + + + )} +
+ ); + })} ); } diff --git a/frontend/src/components/v2/EmptyState/EmptyState.tsx b/frontend/src/components/v2/EmptyState/EmptyState.tsx index 5122d9c1e..70bf234d3 100644 --- a/frontend/src/components/v2/EmptyState/EmptyState.tsx +++ b/frontend/src/components/v2/EmptyState/EmptyState.tsx @@ -1,4 +1,5 @@ import { ReactNode } from 'react'; +import { SizeProp } from '@fortawesome/fontawesome-svg-core'; import { faCubesStacked, IconDefinition } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { twMerge } from 'tailwind-merge'; @@ -8,13 +9,25 @@ type Props = { className?: string; children?: ReactNode; icon?: IconDefinition; + iconSize?: SizeProp; }; -export const EmptyState = ({ title, className, children, icon = faCubesStacked }: Props) => ( -
- -
-
{title}
+export const EmptyState = ({ + title, + className, + children, + icon = faCubesStacked, + iconSize = '2x' +}: Props) => ( +
+ +
+
{title}
{children}
diff --git a/frontend/src/components/v2/Tooltip/Tooltip.tsx b/frontend/src/components/v2/Tooltip/Tooltip.tsx index a3119d9c7..b0d009421 100644 --- a/frontend/src/components/v2/Tooltip/Tooltip.tsx +++ b/frontend/src/components/v2/Tooltip/Tooltip.tsx @@ -35,7 +35,7 @@ export const Tooltip = ({ sideOffset={5} {...props} className={twMerge( - `z-20 select-none max-w-[15rem] rounded-md bg-mineshaft-800 border border-mineshaft-600 py-2 px-4 font-light text-sm text-bunker-200 shadow-md + `z-50 max-w-[15rem] select-none rounded-md border border-mineshaft-600 bg-mineshaft-800 py-2 px-4 text-sm font-light text-bunker-200 shadow-md data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade diff --git a/frontend/src/hooks/api/index.tsx b/frontend/src/hooks/api/index.tsx index 89d947068..9c98e3b7e 100644 --- a/frontend/src/hooks/api/index.tsx +++ b/frontend/src/hooks/api/index.tsx @@ -2,6 +2,7 @@ export * from './auth'; export * from './incidentContacts'; export * from './keys'; export * from './organization'; +export * from './secretFolders'; export * from './secrets'; export * from './secretSnapshots'; export * from './serviceAccounts'; diff --git a/frontend/src/hooks/api/secretFolders/index.tsx b/frontend/src/hooks/api/secretFolders/index.tsx new file mode 100644 index 000000000..c1cc056d1 --- /dev/null +++ b/frontend/src/hooks/api/secretFolders/index.tsx @@ -0,0 +1 @@ +export { useCreateFolder, useDeleteFolder, useGetProjectFolders, useUpdateFolder } from './queries'; diff --git a/frontend/src/hooks/api/secretFolders/queries.tsx b/frontend/src/hooks/api/secretFolders/queries.tsx new file mode 100644 index 000000000..008d6f26f --- /dev/null +++ b/frontend/src/hooks/api/secretFolders/queries.tsx @@ -0,0 +1,129 @@ +import { useCallback } from 'react'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; + +import { apiRequest } from '@app/config/request'; + +import { secretSnapshotKeys } from '../secretSnapshots/queries'; +import { + CreateFolderDTO, + DeleteFolderDTO, + GetProjectFoldersDTO, + TSecretFolder, + UpdateFolderDTO +} from './types'; + +const queryKeys = { + getSecretFolders: (workspaceId: string, environment: string, parentFolderId?: string) => + ['secret-folders', { workspaceId, environment, parentFolderId }] as const +}; + +export const useGetProjectFolders = ({ + workspaceId, + parentFolderId, + environment, + isPaused, + sortDir +}: GetProjectFoldersDTO) => + useQuery({ + queryKey: queryKeys.getSecretFolders(workspaceId, environment, parentFolderId), + enabled: Boolean(workspaceId) && Boolean(environment) && !isPaused, + queryFn: async () => { + const { data } = await apiRequest.get<{ folders: TSecretFolder[]; dir: TSecretFolder[] }>( + '/api/v1/folders', + { + params: { + workspaceId, + environment, + parentFolderId + } + } + ); + return data; + }, + select: useCallback( + ({ folders, dir }: { folders: TSecretFolder[]; dir: TSecretFolder[] }) => ({ + dir, + folders: folders.sort((a, b) => + sortDir === 'asc' + ? a?.name?.localeCompare(b?.name || '') + : b?.name?.localeCompare(a?.name || '') + ) + }), + [sortDir] + ) + }); + +export const useCreateFolder = () => { + const queryClient = useQueryClient(); + + return useMutation<{}, {}, CreateFolderDTO>({ + mutationFn: async (dto) => { + const { data } = await apiRequest.post('/api/v1/folders', dto); + return data; + }, + onSuccess: (_, { workspaceId, environment, parentFolderId }) => { + queryClient.invalidateQueries( + queryKeys.getSecretFolders(workspaceId, environment, parentFolderId) + ); + queryClient.invalidateQueries( + secretSnapshotKeys.count(workspaceId, environment, parentFolderId) + ); + queryClient.invalidateQueries( + secretSnapshotKeys.list(workspaceId, environment, parentFolderId) + ); + } + }); +}; + +export const useUpdateFolder = (parentFolderId: string) => { + const queryClient = useQueryClient(); + + return useMutation<{}, {}, UpdateFolderDTO>({ + mutationFn: async ({ folderId, name, environment, workspaceId }) => { + const { data } = await apiRequest.patch(`/api/v1/folders/${folderId}`, { + name, + environment, + workspaceId + }); + return data; + }, + onSuccess: (_, { workspaceId, environment }) => { + queryClient.invalidateQueries( + queryKeys.getSecretFolders(workspaceId, environment, parentFolderId) + ); + queryClient.invalidateQueries( + secretSnapshotKeys.count(workspaceId, environment, parentFolderId) + ); + queryClient.invalidateQueries( + secretSnapshotKeys.list(workspaceId, environment, parentFolderId) + ); + } + }); +}; + +export const useDeleteFolder = (parentFolderId: string) => { + const queryClient = useQueryClient(); + + return useMutation<{}, {}, DeleteFolderDTO>({ + mutationFn: async ({ folderId, environment, workspaceId }) => { + const { data } = await apiRequest.delete(`/api/v1/folders/${folderId}`, { + data: { + environment, + workspaceId + } + }); + return data; + }, + onSuccess: (_, { workspaceId, environment }) => { + queryClient.invalidateQueries( + queryKeys.getSecretFolders(workspaceId, environment, parentFolderId) + ); + queryClient.invalidateQueries( + secretSnapshotKeys.count(workspaceId, environment, parentFolderId) + ); + queryClient.invalidateQueries( + secretSnapshotKeys.list(workspaceId, environment, parentFolderId) + ); + } + }); +}; diff --git a/frontend/src/hooks/api/secretFolders/types.ts b/frontend/src/hooks/api/secretFolders/types.ts new file mode 100644 index 000000000..a9ca17641 --- /dev/null +++ b/frontend/src/hooks/api/secretFolders/types.ts @@ -0,0 +1,32 @@ +export type TSecretFolder = { + id: string; + name: string; +}; + +export type GetProjectFoldersDTO = { + workspaceId: string; + environment: string; + parentFolderId?: string; + isPaused?: boolean; + sortDir?: 'asc' | 'desc'; +}; + +export type CreateFolderDTO = { + workspaceId: string; + environment: string; + folderName: string; + parentFolderId?: string; +}; + +export type UpdateFolderDTO = { + workspaceId: string; + environment: string; + name: string; + folderId: string; +}; + +export type DeleteFolderDTO = { + workspaceId: string; + environment: string; + folderId: string; +}; diff --git a/frontend/src/hooks/api/secretSnapshots/queries.tsx b/frontend/src/hooks/api/secretSnapshots/queries.tsx index 3aa9dfbe5..b5daf62fd 100644 --- a/frontend/src/hooks/api/secretSnapshots/queries.tsx +++ b/frontend/src/hooks/api/secretSnapshots/queries.tsx @@ -17,18 +17,31 @@ import { } from './types'; export const secretSnapshotKeys = { - list: (workspaceId: string) => [{ workspaceId }, 'secret-snapshot'] as const, + list: (workspaceId: string, env: string, folderId?: string) => + [{ workspaceId, env, folderId }, 'secret-snapshot'] as const, snapshotSecrets: (snapshotId: string) => [{ snapshotId }, 'secret-snapshot'] as const, - count: (workspaceId: string) => [{ workspaceId }, 'count', 'secret-snapshot'] + count: (workspaceId: string, env: string, folderId?: string) => [ + { workspaceId, env, folderId }, + 'count', + 'secret-snapshot' + ] }; -const fetchWorkspaceSecretSnaphots = async (workspaceId: string, limit = 10, offset = 0) => { +const fetchWorkspaceSecretSnaphots = async ( + workspaceId: string, + environment: string, + folderId?: string, + limit = 10, + offset = 0 +) => { const res = await apiRequest.get<{ secretSnapshots: TWorkspaceSecretSnapshot[] }>( `/api/v1/workspace/${workspaceId}/secret-snapshots`, { params: { limit, - offset + offset, + environment, + folderId } } ); @@ -38,9 +51,16 @@ const fetchWorkspaceSecretSnaphots = async (workspaceId: string, limit = 10, off export const useGetWorkspaceSecretSnapshots = (dto: GetWorkspaceSecretSnapshotsDTO) => useInfiniteQuery({ - enabled: Boolean(dto.workspaceId), - queryKey: secretSnapshotKeys.list(dto.workspaceId), - queryFn: ({ pageParam }) => fetchWorkspaceSecretSnaphots(dto.workspaceId, dto.limit, pageParam), + enabled: Boolean(dto.workspaceId && dto.environment), + queryKey: secretSnapshotKeys.list(dto.workspaceId, dto.environment, dto?.folder), + queryFn: ({ pageParam }) => + fetchWorkspaceSecretSnaphots( + dto.workspaceId, + dto.environment, + dto?.folder, + dto.limit, + pageParam + ), getNextPageParam: (lastPage, pages) => lastPage.length !== 0 ? pages.length * dto.limit : undefined }); @@ -115,40 +135,53 @@ export const useGetSnapshotSecrets = ({ decryptFileKey, env, snapshotId }: TSnap } }); - return { version: data.version, secrets: sharedSecrets, createdAt: data.createdAt }; + return { + version: data.version, + secrets: sharedSecrets, + createdAt: data.createdAt, + folders: data.folderVersion + }; } }); -const fetchWorkspaceSecretSnaphotCount = async (workspaceId: string) => { +const fetchWorkspaceSecretSnaphotCount = async ( + workspaceId: string, + environment: string, + folderId?: string +) => { const res = await apiRequest.get<{ count: number }>( - `/api/v1/workspace/${workspaceId}/secret-snapshots/count` + `/api/v1/workspace/${workspaceId}/secret-snapshots/count`, + { + params: { + environment, + folderId + } + } ); return res.data.count; }; -export const useGetWsSnapshotCount = (workspaceId: string) => +export const useGetWsSnapshotCount = (workspaceId: string, env: string, folderId?: string) => useQuery({ - enabled: Boolean(workspaceId), - queryKey: secretSnapshotKeys.count(workspaceId), - queryFn: () => fetchWorkspaceSecretSnaphotCount(workspaceId) + enabled: Boolean(workspaceId && env), + queryKey: secretSnapshotKeys.count(workspaceId, env, folderId), + queryFn: () => fetchWorkspaceSecretSnaphotCount(workspaceId, env, folderId) }); export const usePerformSecretRollback = () => { const queryClient = useQueryClient(); return useMutation<{}, {}, TSecretRollbackDTO>({ - mutationFn: async (dto) => { + mutationFn: async ({ workspaceId, ...dto }) => { const { data } = await apiRequest.post( - `/api/v1/workspace/${dto.workspaceId}/secret-snapshots/rollback`, - { - version: dto.version - } + `/api/v1/workspace/${workspaceId}/secret-snapshots/rollback`, + dto ); return data; }, - onSuccess: (_, dto) => { - queryClient.invalidateQueries(secretSnapshotKeys.list(dto.workspaceId)); - queryClient.invalidateQueries(secretSnapshotKeys.count(dto.workspaceId)); + onSuccess: (_, { workspaceId, environment, folderId }) => { + queryClient.invalidateQueries(secretSnapshotKeys.list(workspaceId, environment, folderId)); + queryClient.invalidateQueries(secretSnapshotKeys.count(workspaceId, environment, folderId)); } }); }; diff --git a/frontend/src/hooks/api/secretSnapshots/types.ts b/frontend/src/hooks/api/secretSnapshots/types.ts index dbe8ccd17..cc06f2bc1 100644 --- a/frontend/src/hooks/api/secretSnapshots/types.ts +++ b/frontend/src/hooks/api/secretSnapshots/types.ts @@ -13,6 +13,7 @@ export type TWorkspaceSecretSnapshot = { export type TSnapshotSecret = Omit & { secretVersions: EncryptedSecretVersion[]; + folderVersion: Array<{ name: string; id: string }>; }; export type TSnapshotSecretProps = { @@ -24,9 +25,13 @@ export type TSnapshotSecretProps = { export type GetWorkspaceSecretSnapshotsDTO = { workspaceId: string; limit: number; + environment: string; + folder?: string; }; export type TSecretRollbackDTO = { workspaceId: string; version: number; + environment: string; + folderId?: string; }; diff --git a/frontend/src/hooks/api/secrets/queries.tsx b/frontend/src/hooks/api/secrets/queries.tsx index ecc26c63d..e4ad89dd6 100644 --- a/frontend/src/hooks/api/secrets/queries.tsx +++ b/frontend/src/hooks/api/secrets/queries.tsx @@ -19,19 +19,24 @@ import { export const secretKeys = { // this is also used in secretSnapshot part - getProjectSecret: (workspaceId: string, env: string | string[]) => [ - { workspaceId, env }, + getProjectSecret: (workspaceId: string, env: string | string[], folderId?: string) => [ + { workspaceId, env, folderId }, 'secrets' ], getSecretVersion: (secretId: string) => [{ secretId }, 'secret-versions'] }; -const fetchProjectEncryptedSecrets = async (workspaceId: string, env: string | string[]) => { +const fetchProjectEncryptedSecrets = async ( + workspaceId: string, + env: string | string[], + folderId?: string +) => { if (typeof env === 'string') { const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>('/api/v2/secrets', { params: { environment: env, - workspaceId + workspaceId, + folderId: folderId || undefined } }); return data.secrets; @@ -46,7 +51,8 @@ const fetchProjectEncryptedSecrets = async (workspaceId: string, env: string | s const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>('/api/v2/secrets', { params: { environment: envPoint, - workspaceId + workspaceId, + folderId } }); allEnvData = allEnvData.concat(data.secrets); @@ -63,13 +69,14 @@ export const useGetProjectSecrets = ({ workspaceId, env, decryptFileKey, - isPaused + isPaused, + folderId }: GetProjectSecretsDTO) => useQuery({ // wait for all values to be available enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused, - queryKey: secretKeys.getProjectSecret(workspaceId, env), - queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env), + queryKey: secretKeys.getProjectSecret(workspaceId, env, folderId), + queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId), select: (data) => { const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string; const latestKey = decryptFileKey; @@ -283,9 +290,15 @@ export const useBatchSecretsOp = () => { return data; }, onSuccess: (_, dto) => { - queryClient.invalidateQueries(secretKeys.getProjectSecret(dto.workspaceId, dto.environment)); - queryClient.invalidateQueries(secretSnapshotKeys.list(dto.workspaceId)); - queryClient.invalidateQueries(secretSnapshotKeys.count(dto.workspaceId)); + queryClient.invalidateQueries( + secretKeys.getProjectSecret(dto.workspaceId, dto.environment, dto.folderId) + ); + queryClient.invalidateQueries( + secretSnapshotKeys.list(dto.workspaceId, dto.environment, dto?.folderId) + ); + queryClient.invalidateQueries( + secretSnapshotKeys.count(dto.workspaceId, dto.environment, dto?.folderId) + ); } }); }; diff --git a/frontend/src/hooks/api/secrets/types.ts b/frontend/src/hooks/api/secrets/types.ts index eb1157e91..2a5938c68 100644 --- a/frontend/src/hooks/api/secrets/types.ts +++ b/frontend/src/hooks/api/secrets/types.ts @@ -61,6 +61,7 @@ type SecretTagArg = { _id: string; name: string; slug: string }; export type UpdateSecretArg = { _id: string; + folderId?: string; type: 'shared' | 'personal'; secretName: string; secretKeyCiphertext: string; @@ -81,6 +82,7 @@ export type DeleteSecretArg = { _id: string }; export type BatchSecretDTO = { workspaceId: string; + folderId: string; environment: string; requests: Array< | { method: 'POST'; secret: CreateSecretArg } @@ -93,6 +95,7 @@ export type GetProjectSecretsDTO = { workspaceId: string; env: string | string[]; decryptFileKey: UserWsKeyPair; + folderId?: string; isPaused?: boolean; onSuccess?: (data: DecryptedSecret[]) => void; }; diff --git a/frontend/src/views/DashboardPage/DashboardPage.tsx b/frontend/src/views/DashboardPage/DashboardPage.tsx index a8f9b1275..9479e50d7 100644 --- a/frontend/src/views/DashboardPage/DashboardPage.tsx +++ b/frontend/src/views/DashboardPage/DashboardPage.tsx @@ -10,6 +10,7 @@ import { faDownload, faEye, faEyeSlash, + faFolderPlus, faMagnifyingGlass, faPlus } from '@fortawesome/free-solid-svg-icons'; @@ -21,6 +22,7 @@ import { useNotificationContext } from '@app/components/context/Notifications/No import NavHeader from '@app/components/navigation/NavHeader'; import { Button, + DeleteActionModal, IconButton, Input, Modal, @@ -37,7 +39,10 @@ import { useWorkspace } from '@app/context'; import { useLeaveConfirm, usePopUp, useToggle } from '@app/hooks'; import { useBatchSecretsOp, + useCreateFolder, useCreateWsTag, + useDeleteFolder, + useGetProjectFolders, useGetProjectSecrets, useGetSecretVersion, useGetSnapshotSecrets, @@ -48,13 +53,20 @@ import { useGetWsSnapshotCount, useGetWsTags, usePerformSecretRollback, - useRegisterUserAction + useRegisterUserAction, + useUpdateFolder } from '@app/hooks/api'; import { secretKeys } from '@app/hooks/api/secrets/queries'; import { WorkspaceEnv } from '@app/hooks/api/types'; 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'; @@ -96,7 +108,9 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => { 'addTag', 'secretSnapshots', 'uploadedSecOpts', - 'compareSecrets' + 'compareSecrets', + 'folderForm', + 'deleteFolder' ] as const); const [isSecretValueHidden, setIsSecretValueHidden] = useToggle(true); const [searchFilter, setSearchFilter] = useState(''); @@ -106,6 +120,9 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => { const deletedSecretIds = useRef([]); const { hasUnsavedChanges, setHasUnsavedChanges } = useLeaveConfirm({ initialValue: false }); + const folderId = router.query.folderId as string; + const isRollbackMode = Boolean(snapshotId); + const { currentWorkspace, isLoading } = useWorkspace(); const workspaceId = currentWorkspace?._id as string; const { data: latestFileKey } = useGetUserWsKey(workspaceId); @@ -142,7 +159,16 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => { workspaceId, env: selectedEnv?.slug || '', decryptFileKey: latestFileKey!, - isPaused: Boolean(snapshotId) + isPaused: Boolean(snapshotId), + folderId + }); + + const { data: folderData, isLoading: isFoldersLoading } = useGetProjectFolders({ + workspaceId: workspaceId || '', + environment: selectedEnv?.slug || '', + parentFolderId: folderId, + isPaused: isRollbackMode, + sortDir }); const { @@ -152,6 +178,8 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => { isFetchingNextPage } = useGetWorkspaceSecretSnapshots({ workspaceId, + environment: selectedEnv?.slug || '', + folder: folderId, limit: 10 }); @@ -165,8 +193,11 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => { decryptFileKey: latestFileKey! }); - const { data: snapshotCount, isLoading: isLoadingSnapshotCount } = - useGetWsSnapshotCount(workspaceId); + const { data: snapshotCount, isLoading: isLoadingSnapshotCount } = useGetWsSnapshotCount( + workspaceId, + selectedEnv?.slug || '', + folderId + ); const { data: wsTags } = useGetWsTags(workspaceId); // mutation calls @@ -174,6 +205,9 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => { 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 method = useForm({ // why any: well yup inferred ts expects other keys to defined as undefined @@ -192,7 +226,6 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => { reset } = method; const { fields, prepend, append, remove } = useFieldArray({ control, name: 'secrets' }); - const isRollbackMode = Boolean(snapshotId); const isReadOnly = selectedEnv?.isWriteDenied; const isAddOnly = selectedEnv?.isReadDenied && !selectedEnv?.isWriteDenied; const canDoRollback = !isReadOnly && !isAddOnly; @@ -281,7 +314,9 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => { try { await performSecretRollback({ workspaceId, - version: snapshotSecret.version + version: snapshotSecret.version, + environment: selectedEnv?.slug || '', + folderId }); setValue('isSnapshotMode', false); setSnaphotId(null); @@ -326,6 +361,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => { await batchSecretOp({ requests: batchedSecret, workspaceId, + folderId, environment: selectedEnv?.slug }); createNotification({ @@ -393,7 +429,99 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => { } }; - if (isSecretsLoading || isEnvListLoading) { + const handleFolderOpen = (id: string) => { + setSearchFilter(''); + router.push({ + pathname: router.pathname, + query: { + ...router.query, + folderId: id + } + }); + }; + + const isEditFolder = Boolean(popUp?.folderForm?.data); + + const handleFolderCreate = async (name: string) => { + try { + await createFolder({ + workspaceId, + environment: selectedEnv?.slug || '', + 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 = async (name: string) => { + const { id } = popUp?.folderForm?.data as TDeleteFolderForm; + try { + await updateFolder({ + folderId: id, + workspaceId, + environment: selectedEnv?.slug || '', + name + }); + createNotification({ + type: 'success', + text: 'Successfully updated folder' + }); + handlePopUpClose('folderForm'); + } catch (error) { + console.error(error); + createNotification({ + text: 'Failed to update folder', + type: 'error' + }); + } + }; + + const handleFolderDelete = async () => { + const { id } = popUp?.deleteFolder?.data as TDeleteFolderForm; + try { + deleteFolder({ + workspaceId, + environment: selectedEnv?.slug || '', + folderId: id + }); + createNotification({ + type: 'success', + text: 'Successfully removed folder' + }); + handlePopUpClose('deleteFolder'); + } catch (error) { + console.error(error); + createNotification({ + text: 'Failed to remove folder', + type: 'error' + }); + } + }; + + // 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 isSnapshotSecretEmtpy = + isRollbackMode && !isSnapshotSecretsLoading && !snapshotSecret?.secrets?.length; + const isSecretEmpty = (!isRollbackMode && isDashboardSecretEmpty) || isSnapshotSecretEmtpy; + + if (isSecretsLoading || isEnvListLoading || isFolderListLoading) { return (
loading animation @@ -401,64 +529,42 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => { ); } - // when secrets is not loading and secrets list is empty - const isDashboardSecretEmpty = !isSecretsLoading && false; - // when using snapshot mode and snapshot is loading and snapshot list is empty - const isSnapshotSecretEmtpy = - isRollbackMode && !isSnapshotSecretsLoading && !snapshotSecret?.secrets?.length; - const isSecretEmpty = (!isRollbackMode && isDashboardSecretEmpty) || isSnapshotSecretEmtpy; - const userAvailableEnvs = wsEnv?.filter( ({ isReadDenied, isWriteDenied }) => !isReadDenied || !isWriteDenied ); return ( -
+
-
+ {/* breadcrumb row */} -
+
envir.slug === envFromTop)[0].name || '' } + isFolderMode + folders={folderData?.dir} isProjectRelated userAvailableEnvs={userAvailableEnvs} onEnvChange={onEnvChange} />
- {/* This is only for rollbacks */} - {isRollbackMode && -
-
-

Secret Snapshot

- {isRollbackMode && Boolean(snapshotSecret) && ( - - {new Date(snapshotSecret?.createdAt || '').toLocaleString()} - - )} -
-
- -
-
} +
+
{isRollbackMode ? 'Secret Snapshot' : 'Secrets'}
+ {isRollbackMode && Boolean(snapshotSecret) && ( + + {new Date(snapshotSecret?.createdAt || '').toLocaleString()} + + )} +
{/* Environment, search and other action row */} -
-
+
+
setSearchFilter(e.target.value)} leftIcon={} @@ -500,8 +606,8 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
-
- +
+ {
-
+
{!isReadOnly && !isRollbackMode && ( <> -
- +
+ + handlePopUpOpen('folderForm')} + > + + + +
+ +
+ {
-
+
+ +
+ +
)} + {isRollbackMode && ( + + )}