From 63410f2341887ff5faba5c7c841595ebe2a7bcee Mon Sep 17 00:00:00 2001 From: Rohan Date: Tue, 25 Feb 2025 21:19:34 +0530 Subject: [PATCH 1/3] wip: animations --- .../[app]/_components/AppEnvironments.tsx | 178 +++++++++++------- .../apps/[app]/_components/AppSecretRow.tsx | 77 ++++---- .../[team]/apps/[app]/_hooks/useAppSecrets.ts | 142 +++++++++----- .../[environment]/[[...path]]/page.tsx | 7 +- 4 files changed, 249 insertions(+), 155 deletions(-) diff --git a/frontend/app/[team]/apps/[app]/_components/AppEnvironments.tsx b/frontend/app/[team]/apps/[app]/_components/AppEnvironments.tsx index facffd056..ab79330f9 100644 --- a/frontend/app/[team]/apps/[app]/_components/AppEnvironments.tsx +++ b/frontend/app/[team]/apps/[app]/_components/AppEnvironments.tsx @@ -12,11 +12,12 @@ import { userHasPermission } from '@/utils/access/permissions' import { useMutation } from '@apollo/client' import Link from 'next/link' import { usePathname } from 'next/navigation' -import { useContext } from 'react' +import { useContext, useEffect, useState } from 'react' import { BsListColumnsReverse } from 'react-icons/bs' import { FaArrowRight, FaBan, FaExchangeAlt, FaFolder, FaKey } from 'react-icons/fa' import { EmptyState } from '@/components/common/EmptyState' import { useAppSecrets } from '../_hooks/useAppSecrets' +import { motion } from 'framer-motion' export const AppEnvironments = ({ appId }: { appId: string }) => { const { activeOrganisation: organisation } = useContext(organisationContext) @@ -43,12 +44,23 @@ export const AppEnvironments = ({ appId }: { appId: string }) => { true ) - const { appEnvironments, fetching } = useAppSecrets( + const { appEnvironments, swapEnvironments } = useAppSecrets( appId, userCanReadEnvironments, 10000 // Poll every 10 seconds ) + // const [localEnvironments, setLocalEnvironments] = useState( + // appEnvironments || [] + // ) + + // // Sync local state with fetched environments + // useEffect(() => { + // if (!fetching) { + // setLocalEnvironments(appEnvironments) + // } + // }, [appEnvironments, fetching]) + const allowReordering = organisation?.plan !== ApiOrganisationPlanChoices.Fr && userCanUpdateEnvironments @@ -56,12 +68,28 @@ export const AppEnvironments = ({ appId }: { appId: string }) => { const [swapEnvs, { loading }] = useMutation(SwapEnvOrder) - const handleSwapEnvironments = async (env1: EnvironmentType, env2: EnvironmentType) => { - await swapEnvs({ - variables: { environment1Id: env1.id, environment2Id: env2?.id }, - refetchQueries: [{ query: GetAppEnvironments, variables: { appId } }], - }) - } + // const handleSwapEnvironments = async (env1: EnvironmentType, env2: EnvironmentType) => { + // // Optimistically update local state + // setLocalEnvironments((prev) => { + // const newEnvs = [...prev] + // const idx1 = newEnvs.findIndex((e) => e.id === env1.id) + // const idx2 = newEnvs.findIndex((e) => e.id === env2.id) + + // if (idx1 !== -1 && idx2 !== -1) { + // ;[newEnvs[idx1], newEnvs[idx2]] = [newEnvs[idx2], newEnvs[idx1]] // Swap items + // } + + // return newEnvs + // }) + + // // Trigger mutation + // setTimeout(async () => { + // await swapEnvs({ + // variables: { environment1Id: env1.id, environment2Id: env2.id }, + // refetchQueries: [{ query: GetAppEnvironments, variables: { appId } }], + // }) + // }, 300) + // } return (
@@ -89,76 +117,84 @@ export const AppEnvironments = ({ appId }: { appId: string }) => {
{appEnvironments?.map((env: EnvironmentType, index: number) => ( - -
-
-
- -
-
-
- -
{env.name}
-
- {/* Text-based secrets and folder count on wider screens */} -
- {env.secretCount} secrets across {env.folderCount} folders -
- {/* Icon-based secrets and folder count on narrower screens */} -
-
- - {env.secretCount} + + +
+
+
+ +
+
+
+ +
{env.name}
+
+ {/* Text-based secrets and folder count on wider screens */} +
+ {env.secretCount} secrets across {env.folderCount} folders
-
- - {env.folderCount} + {/* Icon-based secrets and folder count on narrower screens */} +
+
+ + {env.secretCount} +
+
+ + {env.folderCount} +
-
- - -
+ + +
-
- - - +
+ + + +
-
- {allowReordering && ( -
-
- {index !== 0 && ( - - )} + {allowReordering && ( +
+
+ {index !== 0 && ( + + )} +
+
+ {index !== appEnvironments.length - 1 && ( + + )} +
-
- {index !== appEnvironments.length - 1 && ( - - )} -
-
- )} -
-
+ )} +
+ + ))} {userCanCreateEnvironments && ( diff --git a/frontend/app/[team]/apps/[app]/_components/AppSecretRow.tsx b/frontend/app/[team]/apps/[app]/_components/AppSecretRow.tsx index 74f030490..1d4405e79 100644 --- a/frontend/app/[team]/apps/[app]/_components/AppSecretRow.tsx +++ b/frontend/app/[team]/apps/[app]/_components/AppSecretRow.tsx @@ -25,6 +25,7 @@ import { usePathname } from 'next/navigation' import { arraysEqual } from '@/utils/crypto' import { toggleBooleanKeepingCase } from '@/utils/secrets' import CopyButton from '@/components/common/CopyButton' +import { motion } from 'framer-motion' const INPUT_BASE_STYLE = 'w-full font-mono custom bg-transparent group-hover:bg-zinc-400/20 dark:group-hover:bg-zinc-400/10 transition ease ph-no-capture' @@ -447,10 +448,12 @@ export const AppSecretRow = ({
{envs.map((env) => ( -
{env.secret !== null ? ( @@ -468,18 +471,18 @@ export const AppSecretRow = ({ )}
- + ))} - - -
- {envs.map((envSecret) => ( - - ))} -
-
+
+ {envs.map((envSecret) => ( + + ))} +
)} -
+ )} diff --git a/frontend/app/[team]/apps/[app]/_hooks/useAppSecrets.ts b/frontend/app/[team]/apps/[app]/_hooks/useAppSecrets.ts index 048b38ddd..c52a08049 100644 --- a/frontend/app/[team]/apps/[app]/_hooks/useAppSecrets.ts +++ b/frontend/app/[team]/apps/[app]/_hooks/useAppSecrets.ts @@ -1,17 +1,19 @@ -import { useEffect, useState, useCallback, useContext } from 'react'; -import { useQuery } from '@apollo/client'; -import { unwrapEnvSecretsForUser, decryptEnvSecretKVs } from '@/utils/crypto'; -import { AppSecret, AppFolder, EnvSecrets, EnvFolders } from '../types'; -import { GetAppSecrets } from '@/graphql/queries/secrets/getAppSecrets.gql'; -import { KeyringContext } from '@/contexts/keyringContext'; -import { EnvironmentType } from '@/apollo/graphql'; +import { useEffect, useState, useCallback, useContext } from 'react' +import { useMutation, useQuery } from '@apollo/client' +import { unwrapEnvSecretsForUser, decryptEnvSecretKVs } from '@/utils/crypto' +import { AppSecret, AppFolder, EnvSecrets, EnvFolders } from '../types' +import { GetAppSecrets } from '@/graphql/queries/secrets/getAppSecrets.gql' +import { SwapEnvOrder } from '@/graphql/mutations/environments/swapEnvironmentOrder.gql' +import { KeyringContext } from '@/contexts/keyringContext' +import { EnvironmentType } from '@/apollo/graphql' export const useAppSecrets = (appId: string, allowFetch: boolean, pollInterval: number = 10000) => { - const [appSecrets, setAppSecrets] = useState([]); - const [appFolders, setAppFolders] = useState([]); - const [fetching, setFetching] = useState(true); + const [appSecrets, setAppSecrets] = useState([]) + const [appFolders, setAppFolders] = useState([]) + const [fetching, setFetching] = useState(true) + const [localEnvironments, setLocalEnvironments] = useState([]) - const { keyring } = useContext(KeyringContext); + const { keyring } = useContext(KeyringContext) // Fetch environments and secrets in a single query with polling const { data: appSecretsData, refetch } = useQuery(GetAppSecrets, { @@ -19,74 +21,120 @@ export const useAppSecrets = (appId: string, allowFetch: boolean, pollInterval: fetchPolicy: 'cache-and-network', skip: !allowFetch, pollInterval, // Polling for environments and secrets - }); + }) + + const [swapEnvs, { loading }] = useMutation(SwapEnvOrder) // Callback for processing secrets data const processAppSecrets = useCallback( async (appEnvironments: EnvironmentType[], secretsData: any) => { - const envSecrets: EnvSecrets[] = []; - const envFolders: EnvFolders[] = []; + const envSecrets: EnvSecrets[] = [] + const envFolders: EnvFolders[] = [] for (const env of appEnvironments) { - const secrets = secretsData[env.id]?.secrets || []; - const folders = secretsData[env.id]?.folders || []; + const secrets = secretsData[env.id]?.secrets || [] + const folders = secretsData[env.id]?.folders || [] - const { wrappedSeed, wrappedSalt } = env; + const { wrappedSeed, wrappedSalt } = env // Decrypt secrets for the environment - const { publicKey, privateKey } = await unwrapEnvSecretsForUser(wrappedSeed, wrappedSalt, keyring!); - const decryptedSecrets = await decryptEnvSecretKVs(secrets, { publicKey, privateKey }); - - envSecrets.push({ env, secrets: decryptedSecrets }); - envFolders.push({ env, folders }); + const { publicKey, privateKey } = await unwrapEnvSecretsForUser( + wrappedSeed, + wrappedSalt, + keyring! + ) + const decryptedSecrets = await decryptEnvSecretKVs(secrets, { publicKey, privateKey }) + + envSecrets.push({ env, secrets: decryptedSecrets }) + envFolders.push({ env, folders }) } // Combine secrets across environments and remove duplicates based on keys - const appSecrets = Array.from(new Set(envSecrets.flatMap(env => env.secrets.map(secret => secret.key)))).map(key => { - const envs = envSecrets.map(env => ({ + const appSecrets = Array.from( + new Set(envSecrets.flatMap((env) => env.secrets.map((secret) => secret.key))) + ).map((key) => { + const envs = envSecrets.map((env) => ({ env: env.env, - secret: env.secrets.find(secret => secret.key === key) || null, - })); + secret: env.secrets.find((secret) => secret.key === key) || null, + })) - return { id: `${appId}-${key}`, key, envs }; - }); + return { id: `${appId}-${key}`, key, envs } + }) - const appFolders = Array.from(new Set(envFolders.flatMap(env => env.folders.map(folder => folder.name)))).map(name => ({ + const appFolders = Array.from( + new Set(envFolders.flatMap((env) => env.folders.map((folder) => folder.name))) + ).map((name) => ({ name, - envs: envFolders.map(env => ({ + envs: envFolders.map((env) => ({ env: env.env, - folder: env.folders.find(folder => folder.name === name) || null, + folder: env.folders.find((folder) => folder.name === name) || null, })), - })); + })) - setAppSecrets(appSecrets); - setAppFolders(appFolders); - setFetching(false); + setAppSecrets(appSecrets) + setAppFolders(appFolders) + setFetching(false) }, [keyring, appId] - ); + ) // Watch for changes in the data and process the secrets useEffect(() => { if (keyring && appSecretsData?.appEnvironments) { - - const appEnvironments = appSecretsData.appEnvironments; + const appEnvironments = appSecretsData.appEnvironments // Process the secrets and environments once the data is available const secretsData = appEnvironments.reduce((acc: any, env: EnvironmentType) => { acc[env.id] = { secrets: env.secrets, folders: env.folders, - }; - return acc; - }, {}); + } + return acc + }, {}) // Process secrets and folders after the data is loaded - processAppSecrets(appEnvironments, secretsData); - } - }, [appSecretsData, keyring, processAppSecrets]); + processAppSecrets(appEnvironments, secretsData) - + // Update local environments only if they're empty (to persist order) + setLocalEnvironments((prev) => (prev.length === 0 ? appEnvironments : prev)) + } + }, [appSecretsData, keyring, processAppSecrets]) + + // Utility function to swap environment positions + const swapEnvironments = (environment1Id: string, environment2Id: string) => { + setLocalEnvironments((prev) => { + const newEnvs = [...prev] + const idx1 = newEnvs.findIndex((e) => e.id === environment1Id) + const idx2 = newEnvs.findIndex((e) => e.id === environment2Id) + + if (idx1 !== -1 && idx2 !== -1) { + // Swap the positions in the array + ;[newEnvs[idx1], newEnvs[idx2]] = [newEnvs[idx2], newEnvs[idx1]] + + // Swap their index values + const tempIndex = newEnvs[idx1].index + newEnvs[idx1] = { ...newEnvs[idx1], index: newEnvs[idx2].index } + newEnvs[idx2] = { ...newEnvs[idx2], index: tempIndex } + } - return { appEnvironments: appSecretsData?.appEnvironments, appSecrets, appFolders, fetching, refetch }; -}; + return newEnvs.sort((a, b) => a.index! - b.index!) + }) + + //Trigger mutation + setTimeout(async () => { + await swapEnvs({ + variables: { environment1Id, environment2Id }, + refetchQueries: [{ query: GetAppSecrets, variables: { appId } }], + }) + }, 300) + } + + return { + appEnvironments: localEnvironments, + appSecrets, + appFolders, + fetching, + refetch, + swapEnvironments, + } +} diff --git a/frontend/app/[team]/apps/[app]/environments/[environment]/[[...path]]/page.tsx b/frontend/app/[team]/apps/[app]/environments/[environment]/[[...path]]/page.tsx index bde70ff7c..81172ba44 100644 --- a/frontend/app/[team]/apps/[app]/environments/[environment]/[[...path]]/page.tsx +++ b/frontend/app/[team]/apps/[app]/environments/[environment]/[[...path]]/page.tsx @@ -63,6 +63,7 @@ import { userHasPermission } from '@/utils/access/permissions' import Spinner from '@/components/common/Spinner' import EnvFileDropZone from '@/components/environments/secrets/import/EnvFileDropZone' import SingleEnvImportDialog from '@/components/environments/secrets/import/SingleEnvImportDialog' +import { motion } from 'framer-motion' export default function EnvironmentPath({ params, @@ -1008,7 +1009,7 @@ export default function EnvironmentPath({ {organisation && filteredAndSortedSecrets.map((secret, index: number) => ( -
{index + 1} -
+ ))} {filteredAndSortedSecrets.length === 0 && filteredFolders.length === 0 && ( From f4ba6ea4938d7702f6e5abed50d38e530146ca00 Mon Sep 17 00:00:00 2001 From: Rohan Date: Wed, 26 Feb 2025 19:00:32 +0530 Subject: [PATCH 2/3] fix: misc improvements --- .../[app]/_components/AppEnvironments.tsx | 53 ++++--------------- .../apps/[app]/_components/AppSecretRow.tsx | 6 +-- .../apps/[app]/_components/AppSecrets.tsx | 7 ++- .../[team]/apps/[app]/_hooks/useAppSecrets.ts | 36 +++---------- 4 files changed, 25 insertions(+), 77 deletions(-) diff --git a/frontend/app/[team]/apps/[app]/_components/AppEnvironments.tsx b/frontend/app/[team]/apps/[app]/_components/AppEnvironments.tsx index ab79330f9..4b5e57e6f 100644 --- a/frontend/app/[team]/apps/[app]/_components/AppEnvironments.tsx +++ b/frontend/app/[team]/apps/[app]/_components/AppEnvironments.tsx @@ -1,7 +1,5 @@ 'use client' -import { GetAppEnvironments } from '@/graphql/queries/secrets/getAppEnvironments.gql' -import { SwapEnvOrder } from '@/graphql/mutations/environments/swapEnvironmentOrder.gql' import { EnvironmentType, ApiOrganisationPlanChoices } from '@/apollo/graphql' import { Button } from '@/components/common/Button' import { Card } from '@/components/common/Card' @@ -9,10 +7,9 @@ import { CreateEnvironmentDialog } from '@/components/environments/CreateEnviron import { ManageEnvironmentDialog } from '@/components/environments/ManageEnvironmentDialog' import { organisationContext } from '@/contexts/organisationContext' import { userHasPermission } from '@/utils/access/permissions' -import { useMutation } from '@apollo/client' import Link from 'next/link' import { usePathname } from 'next/navigation' -import { useContext, useEffect, useState } from 'react' +import { useContext } from 'react' import { BsListColumnsReverse } from 'react-icons/bs' import { FaArrowRight, FaBan, FaExchangeAlt, FaFolder, FaKey } from 'react-icons/fa' import { EmptyState } from '@/components/common/EmptyState' @@ -44,53 +41,17 @@ export const AppEnvironments = ({ appId }: { appId: string }) => { true ) - const { appEnvironments, swapEnvironments } = useAppSecrets( + const { appEnvironments, swapEnvironments, fetching } = useAppSecrets( appId, userCanReadEnvironments, 10000 // Poll every 10 seconds ) - // const [localEnvironments, setLocalEnvironments] = useState( - // appEnvironments || [] - // ) - - // // Sync local state with fetched environments - // useEffect(() => { - // if (!fetching) { - // setLocalEnvironments(appEnvironments) - // } - // }, [appEnvironments, fetching]) - const allowReordering = organisation?.plan !== ApiOrganisationPlanChoices.Fr && userCanUpdateEnvironments const pathname = usePathname() - const [swapEnvs, { loading }] = useMutation(SwapEnvOrder) - - // const handleSwapEnvironments = async (env1: EnvironmentType, env2: EnvironmentType) => { - // // Optimistically update local state - // setLocalEnvironments((prev) => { - // const newEnvs = [...prev] - // const idx1 = newEnvs.findIndex((e) => e.id === env1.id) - // const idx2 = newEnvs.findIndex((e) => e.id === env2.id) - - // if (idx1 !== -1 && idx2 !== -1) { - // ;[newEnvs[idx1], newEnvs[idx2]] = [newEnvs[idx2], newEnvs[idx1]] // Swap items - // } - - // return newEnvs - // }) - - // // Trigger mutation - // setTimeout(async () => { - // await swapEnvs({ - // variables: { environment1Id: env1.id, environment2Id: env2.id }, - // refetchQueries: [{ query: GetAppEnvironments, variables: { appId } }], - // }) - // }, 300) - // } - return (
@@ -117,7 +78,11 @@ export const AppEnvironments = ({ appId }: { appId: string }) => {
{appEnvironments?.map((env: EnvironmentType, index: number) => ( - +
@@ -170,7 +135,7 @@ export const AppEnvironments = ({ appId }: { appId: string }) => { {index !== 0 && (
- + ))} diff --git a/frontend/app/[team]/apps/[app]/_hooks/useAppSecrets.ts b/frontend/app/[team]/apps/[app]/_hooks/useAppSecrets.ts index c52a08049..a6592c6f0 100644 --- a/frontend/app/[team]/apps/[app]/_hooks/useAppSecrets.ts +++ b/frontend/app/[team]/apps/[app]/_hooks/useAppSecrets.ts @@ -95,38 +95,18 @@ export const useAppSecrets = (appId: string, allowFetch: boolean, pollInterval: // Process secrets and folders after the data is loaded processAppSecrets(appEnvironments, secretsData) - // Update local environments only if they're empty (to persist order) - setLocalEnvironments((prev) => (prev.length === 0 ? appEnvironments : prev)) + // Update local environments + setLocalEnvironments(appEnvironments) } }, [appSecretsData, keyring, processAppSecrets]) - // Utility function to swap environment positions - const swapEnvironments = (environment1Id: string, environment2Id: string) => { - setLocalEnvironments((prev) => { - const newEnvs = [...prev] - const idx1 = newEnvs.findIndex((e) => e.id === environment1Id) - const idx2 = newEnvs.findIndex((e) => e.id === environment2Id) - - if (idx1 !== -1 && idx2 !== -1) { - // Swap the positions in the array - ;[newEnvs[idx1], newEnvs[idx2]] = [newEnvs[idx2], newEnvs[idx1]] - - // Swap their index values - const tempIndex = newEnvs[idx1].index - newEnvs[idx1] = { ...newEnvs[idx1], index: newEnvs[idx2].index } - newEnvs[idx2] = { ...newEnvs[idx2], index: tempIndex } - } - - return newEnvs.sort((a, b) => a.index! - b.index!) + const swapEnvironments = async (environment1Id: string, environment2Id: string) => { + setFetching(true) + await swapEnvs({ + variables: { environment1Id, environment2Id }, + refetchQueries: [{ query: GetAppSecrets, variables: { appId } }], }) - - //Trigger mutation - setTimeout(async () => { - await swapEnvs({ - variables: { environment1Id, environment2Id }, - refetchQueries: [{ query: GetAppSecrets, variables: { appId } }], - }) - }, 300) + setFetching(false) } return { From 4b4fc71a5cf8b9b3c7b07764ac11489a81048d79 Mon Sep 17 00:00:00 2001 From: Rohan Date: Wed, 26 Feb 2025 20:46:42 +0530 Subject: [PATCH 3/3] fix: tweak secrets edit animation duration, easing --- .../apps/[app]/environments/[environment]/[[...path]]/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app/[team]/apps/[app]/environments/[environment]/[[...path]]/page.tsx b/frontend/app/[team]/apps/[app]/environments/[environment]/[[...path]]/page.tsx index 2c0858537..ef14cf6f5 100644 --- a/frontend/app/[team]/apps/[app]/environments/[environment]/[[...path]]/page.tsx +++ b/frontend/app/[team]/apps/[app]/environments/[environment]/[[...path]]/page.tsx @@ -1020,7 +1020,7 @@ export default function EnvironmentPath({ )} key={secret.id} layout - transition={{ duration: 0.3, ease: 'easeInOut' }} + transition={{ duration: 0.25, ease: 'easeOut' }} > {index + 1}