diff --git a/frontend/app/[team]/apps/[app]/_components/AppEnvironments.tsx b/frontend/app/[team]/apps/[app]/_components/AppEnvironments.tsx index facffd056..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,7 +7,6 @@ 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 } from 'react' @@ -17,6 +14,7 @@ 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,7 +41,7 @@ export const AppEnvironments = ({ appId }: { appId: string }) => { true ) - const { appEnvironments, fetching } = useAppSecrets( + const { appEnvironments, swapEnvironments, fetching } = useAppSecrets( appId, userCanReadEnvironments, 10000 // Poll every 10 seconds @@ -54,15 +52,6 @@ export const AppEnvironments = ({ appId }: { appId: string }) => { const pathname = usePathname() - 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 } }], - }) - } - return (
@@ -89,76 +78,88 @@ 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 && ( - - )} -
-
- {index !== appEnvironments.length - 1 && ( - - )} + {allowReordering && ( +
+
+ {index !== 0 && ( + + )} +
+
+ {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..ddaa96650 100644 --- a/frontend/app/[team]/apps/[app]/_components/AppSecretRow.tsx +++ b/frontend/app/[team]/apps/[app]/_components/AppSecretRow.tsx @@ -1,6 +1,6 @@ import { EnvironmentType, SecretType } from '@/apollo/graphql' import { userHasPermission } from '@/utils/access/permissions' -import { Disclosure, Switch, Transition } from '@headlessui/react' +import { Disclosure, Switch } from '@headlessui/react' import clsx from 'clsx' import { FaChevronRight, @@ -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]/_components/AppSecrets.tsx b/frontend/app/[team]/apps/[app]/_components/AppSecrets.tsx index eb6ad5c25..d16fda88e 100644 --- a/frontend/app/[team]/apps/[app]/_components/AppSecrets.tsx +++ b/frontend/app/[team]/apps/[app]/_components/AppSecrets.tsx @@ -54,6 +54,7 @@ import { formatTitle } from '@/utils/meta' import MultiEnvImportDialog from '@/components/environments/secrets/import/MultiEnvImportDialog' import { TbDownload } from 'react-icons/tb' import { duplicateKeysExist } from '@/utils/secrets' +import { motion } from 'framer-motion' export const AppSecrets = ({ team, app }: { team: string; app: string }) => { const { activeOrganisation: organisation } = useContext(organisationContext) @@ -837,9 +838,11 @@ export const AppSecrets = ({ team, app }: { team: string; app: string }) => { key {appEnvironments?.map((env: EnvironmentType) => ( -
- + ))} diff --git a/frontend/app/[team]/apps/[app]/_hooks/useAppSecrets.ts b/frontend/app/[team]/apps/[app]/_hooks/useAppSecrets.ts index 048b38ddd..a6592c6f0 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,100 @@ 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) - - - return { appEnvironments: appSecretsData?.appEnvironments, appSecrets, appFolders, fetching, refetch }; -}; + // Update local environments + setLocalEnvironments(appEnvironments) + } + }, [appSecretsData, keyring, processAppSecrets]) + + const swapEnvironments = async (environment1Id: string, environment2Id: string) => { + setFetching(true) + await swapEnvs({ + variables: { environment1Id, environment2Id }, + refetchQueries: [{ query: GetAppSecrets, variables: { appId } }], + }) + setFetching(false) + } + + 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 674dcdb9a..ef14cf6f5 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, @@ -1010,7 +1011,7 @@ export default function EnvironmentPath({ {organisation && filteredAndSortedSecrets.map((secret, index: number) => ( -
{index + 1} -
+ ))} {filteredAndSortedSecrets.length === 0 && filteredFolders.length === 0 && (