From 82995fbd020b02164dfc6cc0f65f2d5cf0ab38ed Mon Sep 17 00:00:00 2001 From: akhilmhdh Date: Thu, 4 May 2023 20:42:49 +0530 Subject: [PATCH 1/2] feat(ui): fixed lagging issues with new dashboard --- .../src/components/navigation/NavHeader.tsx | 63 +- frontend/src/hooks/api/secrets/index.ts | 7 +- frontend/src/hooks/api/secrets/queries.tsx | 118 +- frontend/src/hooks/api/secrets/types.ts | 1 + frontend/src/layouts/AppLayout/AppLayout.tsx | 46 +- frontend/src/pages/dashboard/[id].tsx | 1237 +---------------- .../DashboardPage/DashboardEnvOverview.tsx | 237 ++-- .../src/views/DashboardPage/DashboardPage.tsx | 129 +- .../DashboardPage/DashboardPage.utils.ts | 98 +- .../EnvComparisonRow/EnvComparisonRow.tsx | 215 +-- .../SecretDetailDrawer/SecretDetailDrawer.tsx | 34 +- .../components/SecretInputRow/MaskedInput.tsx | 107 ++ .../SecretInputRow/SecretInputRow.tsx | 646 ++++----- .../SecretTableHeader/SecretTableHeader.tsx | 50 +- 14 files changed, 1009 insertions(+), 1979 deletions(-) create mode 100644 frontend/src/views/DashboardPage/components/SecretInputRow/MaskedInput.tsx diff --git a/frontend/src/components/navigation/NavHeader.tsx b/frontend/src/components/navigation/NavHeader.tsx index c8af1ce46..d807b45b7 100644 --- a/frontend/src/components/navigation/NavHeader.tsx +++ b/frontend/src/components/navigation/NavHeader.tsx @@ -1,3 +1,4 @@ +import Link from 'next/link'; import { useRouter } from 'next/router'; import { faAngleRight } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; @@ -21,6 +22,7 @@ import { Select, SelectItem, Tooltip } from '../v2'; * @param {string} obj.onEnvChange - the action that happens when an env is changed * @returns */ +// TODO(akhilmhdh): simply this header and nav system later export default function NavHeader({ pageName, isProjectRelated, @@ -38,7 +40,7 @@ export default function NavHeader({ }): JSX.Element { const { currentWorkspace } = useWorkspace(); const { currentOrg } = useOrganization(); - const router = useRouter() + const router = useRouter(); return (
@@ -59,31 +61,40 @@ export default function NavHeader({ )} - {pageName === 'Secrets' - ? {pageName} - :
{pageName}
} - {currentEnv && - <> - -
- - - -
- } + {pageName === 'Secrets' ? ( + + {pageName} + + ) : ( +
{pageName}
+ )} + {currentEnv && ( + <> + +
+ + + +
+ + )}
); } diff --git a/frontend/src/hooks/api/secrets/index.ts b/frontend/src/hooks/api/secrets/index.ts index c27cecfb1..013b9aef5 100644 --- a/frontend/src/hooks/api/secrets/index.ts +++ b/frontend/src/hooks/api/secrets/index.ts @@ -1 +1,6 @@ -export { useBatchSecretsOp, useGetProjectSecrets, useGetSecretVersion } from './queries'; +export { + useBatchSecretsOp, + useGetProjectSecrets, + useGetProjectSecretsByKey, + useGetSecretVersion +} from './queries'; diff --git a/frontend/src/hooks/api/secrets/queries.tsx b/frontend/src/hooks/api/secrets/queries.tsx index 031c661d1..ecc26c63d 100644 --- a/frontend/src/hooks/api/secrets/queries.tsx +++ b/frontend/src/hooks/api/secrets/queries.tsx @@ -19,7 +19,10 @@ import { export const secretKeys = { // this is also used in secretSnapshot part - getProjectSecret: (workspaceId: string, env: string | string[]) => [{ workspaceId, env }, 'secrets'], + getProjectSecret: (workspaceId: string, env: string | string[]) => [ + { workspaceId, env }, + 'secrets' + ], getSecretVersion: (secretId: string) => [{ secretId }, 'secret-versions'] }; @@ -32,11 +35,11 @@ const fetchProjectEncryptedSecrets = async (workspaceId: string, env: string | s } }); return data.secrets; - } - + } + if (typeof env === 'object') { let allEnvData: any = []; - + // eslint-disable-next-line no-restricted-syntax for (const envPoint of env) { // eslint-disable-next-line no-await-in-loop @@ -48,13 +51,12 @@ const fetchProjectEncryptedSecrets = async (workspaceId: string, env: string | s }); allEnvData = allEnvData.concat(data.secrets); } - + return allEnvData; - // eslint-disable-next-line no-else-return + // eslint-disable-next-line no-else-return } else { return null; } - }; export const useGetProjectSecrets = ({ @@ -117,7 +119,10 @@ export const useGetProjectSecrets = ({ }; if (encSecret.type === 'personal') { - personalSecrets[`${decryptedSecret.key}-${decryptedSecret.env}`] = { id: encSecret._id, value: secretValue }; + personalSecrets[`${decryptedSecret.key}-${decryptedSecret.env}`] = { + id: encSecret._id, + value: secretValue + }; } else { if (!duplicateSecretKey?.[`${decryptedSecret.key}-${decryptedSecret.env}`]) { sharedSecrets.push(decryptedSecret); @@ -126,17 +131,106 @@ export const useGetProjectSecrets = ({ } }); sharedSecrets.forEach((val) => { - if (personalSecrets?.[val.key]) { - val.idOverride = personalSecrets[val.key].id; - val.valueOverride = personalSecrets[val.key].value; + const dupKey = `${val.key}-${val.env}`; + if (personalSecrets?.[dupKey]) { + val.idOverride = personalSecrets[dupKey].id; + val.valueOverride = personalSecrets[dupKey].value; val.overrideAction = 'modified'; } }); - return { secrets: sharedSecrets }; } }); +export const useGetProjectSecretsByKey = ({ + workspaceId, + env, + decryptFileKey, + isPaused +}: GetProjectSecretsDTO) => + useQuery({ + // wait for all values to be available + enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused, + queryKey: secretKeys.getProjectSecret(workspaceId, env), + queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env), + select: (data) => { + const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string; + const latestKey = decryptFileKey; + const key = decryptAssymmetric({ + ciphertext: latestKey.encryptedKey, + nonce: latestKey.nonce, + publicKey: latestKey.sender.publicKey, + privateKey: PRIVATE_KEY + }); + + const sharedSecrets: Record = {}; + const personalSecrets: Record = {}; + // this used for add-only mode in dashboard + // type won't be there thus only one key is shown + const duplicateSecretKey: Record = {}; + const uniqSecKeys: Record = {}; + data.forEach((encSecret: EncryptedSecret) => { + const secretKey = decryptSymmetric({ + ciphertext: encSecret.secretKeyCiphertext, + iv: encSecret.secretKeyIV, + tag: encSecret.secretKeyTag, + key + }); + if (!uniqSecKeys?.[secretKey]) uniqSecKeys[secretKey] = true; + + const secretValue = decryptSymmetric({ + ciphertext: encSecret.secretValueCiphertext, + iv: encSecret.secretValueIV, + tag: encSecret.secretValueTag, + key + }); + + const secretComment = decryptSymmetric({ + ciphertext: encSecret.secretCommentCiphertext, + iv: encSecret.secretCommentIV, + tag: encSecret.secretCommentTag, + key + }); + + const decryptedSecret = { + _id: encSecret._id, + env: encSecret.environment, + key: secretKey, + value: secretValue, + tags: encSecret.tags, + comment: secretComment, + createdAt: encSecret.createdAt, + updatedAt: encSecret.updatedAt + }; + + if (encSecret.type === 'personal') { + personalSecrets[`${decryptedSecret.key}-${decryptedSecret.env}`] = { + id: encSecret._id, + value: secretValue + }; + } else { + if (!duplicateSecretKey?.[`${decryptedSecret.key}-${decryptedSecret.env}`]) { + if (!sharedSecrets?.[secretKey]) sharedSecrets[secretKey] = []; + sharedSecrets[secretKey].push(decryptedSecret); + } + duplicateSecretKey[`${decryptedSecret.key}-${decryptedSecret.env}`] = true; + } + }); + Object.keys(sharedSecrets).forEach((secName) => { + sharedSecrets[secName].forEach((val) => { + const dupKey = `${val.key}-${val.env}`; + if (personalSecrets?.[dupKey]) { + val.idOverride = personalSecrets[dupKey].id; + val.valueOverride = personalSecrets[dupKey].value; + val.overrideAction = 'modified'; + } + }); + }); + + return { secrets: sharedSecrets, uniqueSecCount: Object.keys(uniqSecKeys).length }; + } + }); + const fetchEncryptedSecretVersion = async (secretId: string, offset: number, limit: number) => { const { data } = await apiRequest.get<{ secretVersions: EncryptedSecretVersion[] }>( `/api/v1/secret/${secretId}/secret-versions`, diff --git a/frontend/src/hooks/api/secrets/types.ts b/frontend/src/hooks/api/secrets/types.ts index 567fecc9a..eb1157e91 100644 --- a/frontend/src/hooks/api/secrets/types.ts +++ b/frontend/src/hooks/api/secrets/types.ts @@ -62,6 +62,7 @@ type SecretTagArg = { _id: string; name: string; slug: string }; export type UpdateSecretArg = { _id: string; type: 'shared' | 'personal'; + secretName: string; secretKeyCiphertext: string; secretKeyIV: string; secretKeyTag: string; diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index 39a80b7b4..4c1f15ebd 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -8,11 +8,7 @@ import { Controller, useForm } from 'react-hook-form'; import Link from 'next/link'; import { useRouter } from 'next/router'; import { useTranslation } from 'next-i18next'; -import { - faBookOpen, - faMobile, - faPlus, -} from '@fortawesome/free-solid-svg-icons'; +import { faBookOpen, faMobile, faPlus } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { yupResolver } from '@hookform/resolvers/yup'; import queryString from 'query-string'; @@ -110,7 +106,6 @@ export const AppLayout = ({ children }: LayoutProps) => { ) { router.push('/noprojects'); } else if (router.asPath !== '/noprojects') { - // const pathSegments = router.asPath.split('/').filter(segment => segment.length > 0); // let intendedWorkspaceId; @@ -123,8 +118,8 @@ export const AppLayout = ({ children }: LayoutProps) => { // .split('/') // [router.asPath.split('/').length - 1].split('?')[0]; // } - - const pathSegments = router.asPath.split('/').filter(segment => segment.length > 0); + + const pathSegments = router.asPath.split('/').filter((segment) => segment.length > 0); let intendedWorkspaceId; if (pathSegments.length >= 2 && pathSegments[0] === 'dashboard') { @@ -140,7 +135,7 @@ export const AppLayout = ({ children }: LayoutProps) => { // const lastPathSegment = router.asPath.split('/').pop().split('?'); // [intendedWorkspaceId] = lastPathSegment; } - + if (!intendedWorkspaceId) return; if (!['callback', 'create', 'authorize'].includes(intendedWorkspaceId)) { @@ -149,7 +144,8 @@ export const AppLayout = ({ children }: LayoutProps) => { // If a user is not a member of a workspace they are trying to access, just push them to one of theirs if ( - !['callback', 'create', 'authorize'].includes(intendedWorkspaceId) && userWorkspaces[0]?._id !== undefined && + !['callback', 'create', 'authorize'].includes(intendedWorkspaceId) && + userWorkspaces[0]?._id !== undefined && !userWorkspaces .map((workspace: { _id: string }) => workspace._id) .includes(intendedWorkspaceId) @@ -240,21 +236,21 @@ export const AppLayout = ({ children }: LayoutProps) => { return ( <> -
+