From 0d7517eeca099b3764143f8a18dc180730f6a48f Mon Sep 17 00:00:00 2001 From: Rohan Date: Fri, 10 Jan 2025 13:31:26 +0530 Subject: [PATCH] feat: add syncs, misc cleanup, bugfixes --- .../apps/[app]/_components/AppSecretRow.tsx | 13 +- frontend/app/[team]/apps/[app]/page.tsx | 145 ++++++++++-------- 2 files changed, 88 insertions(+), 70 deletions(-) diff --git a/frontend/app/[team]/apps/[app]/_components/AppSecretRow.tsx b/frontend/app/[team]/apps/[app]/_components/AppSecretRow.tsx index e69b33c6..9061c642 100644 --- a/frontend/app/[team]/apps/[app]/_components/AppSecretRow.tsx +++ b/frontend/app/[team]/apps/[app]/_components/AppSecretRow.tsx @@ -271,12 +271,6 @@ export const AppSecretRow = ({ updateKey(clientAppSecret.id, sanitizedK) } - const handleKeyInputBlur = () => { - // if (key !== clientAppSecret.key) { - // updateKey(clientAppSecret.id, key) - // } - } - // Permisssions const userCanUpdateSecrets = userHasPermission(organisation?.role?.permissions, 'Secrets', 'update', true) || secretIsNew @@ -296,9 +290,9 @@ export const AppSecretRow = ({ const keyIsDuplicate = false // TODO implement const tooltipText = (env: { env: Partial; secret: SecretType | null }) => { - if (env.secret === null) return `This secret is missing in ${env.env.envType}` - else if (env.secret.value.length === 0) return `This secret is blank in ${env.env.envType}` - else if (secretIsSameAsProd(env)) return `This secret is the same as PROD.` + if (env.secret === null) return `This secret is missing in ${env.env.name}` + else if (env.secret.value.length === 0) return `This secret is blank in ${env.env.name}` + else if (secretIsSameAsProd(env)) return `This secret is the same as Production.` else return 'This secret is present' } @@ -394,7 +388,6 @@ export const AppSecretRow = ({ )} value={key} onChange={(e) => handleUpdateKey(e.target.value)} - onBlur={handleKeyInputBlur} onClick={(e) => e.stopPropagation()} onFocus={(e) => e.stopPropagation()} /> diff --git a/frontend/app/[team]/apps/[app]/page.tsx b/frontend/app/[team]/apps/[app]/page.tsx index 2760cff6..4b11495f 100644 --- a/frontend/app/[team]/apps/[app]/page.tsx +++ b/frontend/app/[team]/apps/[app]/page.tsx @@ -4,6 +4,7 @@ import { GetAppEnvironments } from '@/graphql/queries/secrets/getAppEnvironments import { GetEnvSecretsKV } from '@/graphql/queries/secrets/getSecretKVs.gql' import { InitAppEnvironments } from '@/graphql/mutations/environments/initAppEnvironments.gql' import { BulkProcessSecrets } from '@/graphql/mutations/environments/bulkProcessSecrets.gql' +import { GetAppSyncStatus } from '@/graphql/queries/syncing/getAppSyncStatus.gql' import { useLazyQuery, useMutation, useQuery } from '@apollo/client' import { useContext, useEffect, useState } from 'react' import { @@ -19,6 +20,7 @@ import { KeyringContext } from '@/contexts/keyringContext' import { FaArrowRight, FaBan, + FaBoxOpen, FaCheckCircle, FaChevronRight, FaCircle, @@ -57,6 +59,7 @@ import { SplitButton } from '@/components/common/SplitButton' import { AppSecretRow } from './_components/AppSecretRow' import { AppSecret, AppFolder, EnvSecrets, EnvFolders } from './types' import { toast } from 'react-toastify' +import { EnvSyncStatus } from '@/components/syncing/EnvSyncStatus' const Environments = (props: { environments: EnvironmentType[]; appId: string }) => { const { activeOrganisation: organisation } = useContext(organisationContext) @@ -168,6 +171,7 @@ const Environments = (props: { environments: EnvironmentType[]; appId: string }) export default function Secrets({ params }: { params: { team: string; app: string } }) { const { activeOrganisation: organisation } = useContext(organisationContext) + // Permissions const userCanReadEnvironments = userHasPermission( organisation?.role?.permissions, 'Environments', @@ -180,7 +184,12 @@ export default function Secrets({ params }: { params: { team: string; app: strin 'read', true ) - const userCanReadMembers = userHasPermission(organisation?.role?.permissions, 'Members', 'read') + const userCanReadSyncs = userHasPermission( + organisation?.role?.permissions, + 'Integrations', + 'read', + true + ) const { data } = useQuery(GetAppEnvironments, { variables: { @@ -210,7 +219,43 @@ export default function Secrets({ params }: { params: { team: string; app: strin const { keyring } = useContext(KeyringContext) - const fetchAndDecryptAppEnvs = async (appEnvironments: EnvironmentType[]) => { + const unsavedChanges = + secretsToDelete.length > 0 || + JSON.stringify(serverAppSecrets) !== JSON.stringify(clientAppSecrets) + + const filteredSecrets = + searchQuery === '' + ? clientAppSecrets + : clientAppSecrets.filter((secret) => { + const searchRegex = new RegExp(searchQuery, 'i') + return searchRegex.test(secret.key) + }) + + const filteredFolders = + searchQuery === '' + ? appFolders + : appFolders.filter((folder) => { + const searchRegex = new RegExp(searchQuery, 'i') + return searchRegex.test(folder.name) + }) + + const { data: syncsData } = useQuery(GetAppSyncStatus, { + variables: { + appId: params.app, + }, + skip: !userCanReadSyncs, + pollInterval: unsavedChanges ? 0 : 5000, + }) + + /** + * Fetches encrypted secrets and folders for the given application environments, + * decrypts them using the user's keyring, and processes the data into a unified + * format for managing secrets and folders in the application state. + * + * @param {EnvironmentType[]} appEnvironments - Array of application environments to fetch and decrypt secrets for. + * @returns {Promise} Resolves once the secrets and folders are processed and state is updated. + */ + const fetchAndDecryptAppEnvs = async (appEnvironments: EnvironmentType[]): Promise => { setLoading(true) const envSecrets = [] as EnvSecrets[] const envFolders = [] as EnvFolders[] @@ -277,22 +322,6 @@ export default function Secrets({ params }: { params: { team: string; app: strin const serverSecret = (id: string) => serverAppSecrets.find((secret) => secret.id === id) - const filteredSecrets = - searchQuery === '' - ? clientAppSecrets - : clientAppSecrets.filter((secret) => { - const searchRegex = new RegExp(searchQuery, 'i') - return searchRegex.test(secret.key) - }) - - const filteredFolders = - searchQuery === '' - ? appFolders - : appFolders.filter((folder) => { - const searchRegex = new RegExp(searchQuery, 'i') - return searchRegex.test(folder.name) - }) - const duplicateKeysExist = () => { const keySet = new Set() @@ -313,6 +342,13 @@ export default function Secrets({ params }: { params: { team: string; app: strin return secretKeysToSync.includes('') } + /** + * Handles bulk updating of secrets by comparing client-side secrets with server-side secrets + * and determining which secrets need to be created, updated, or deleted. Secrets are encrypted + * using the user's keyring before being sent to the server for processing. + * + * @returns {Promise} Resolves once the secrets are processed and the application state is updated. + */ const handleBulkUpdateSecrets = async () => { const userKxKeys = { publicKey: await getUserKxPublicKey(keyring!.publicKey), @@ -403,6 +439,14 @@ export default function Secrets({ params }: { params: { team: string; app: strin secretsToUpdate, secretsToDelete, }, + refetchQueries: [ + { + query: GetAppSyncStatus, + variables: { + appId: params.app, + }, + }, + ], }) if (!errors) { @@ -415,6 +459,7 @@ export default function Secrets({ params }: { params: { team: string; app: strin } } + // Wraps handleBulkUpdateSecrets with some basic validation checks, loading state updates and toasts const handleSaveChanges = async () => { setLoading(true) @@ -528,19 +573,6 @@ export default function Secrets({ params }: { params: { team: string; app: strin } const stageEnvValueForDelete = (appSecretId: string, environment: EnvironmentType) => { - // setClientAppSecrets((prevSecrets) => - // prevSecrets.map((appSecret) => { - // if (appSecret.id === appSecretId) { - // const { id, key, envs } = appSecret - - // return { - // id, - // key, - // envs: envs.filter((env) => env.id !== environment.id), - // } - // } else return appSecret - // }) - // ) const appSecret = clientAppSecrets.find((appSecret) => appSecret.id === appSecretId) const secretToDelete = appSecret?.envs.find((env) => env.env.id === environment.id) if (secretToDelete) { @@ -571,7 +603,7 @@ export default function Secrets({ params }: { params: { team: string; app: strin const existingSecrets = secretIds.filter((secretId) => !secretId.includes('new')) // filter out secrets that only exist client-side and can be deleted in memory - secretToDelete.envs = secretToDelete.envs.filter((env) => !env.secret!.id.includes('new')) + secretToDelete.envs = secretToDelete.envs.filter((env) => !env.secret?.id.includes('new')) // if this app secret no longer has any env-values client-side, remove it from memory entirely if (secretToDelete.envs.length === 0) { @@ -600,10 +632,6 @@ export default function Secrets({ params }: { params: { team: string; app: strin setAppSecretsToDelete([]) } - const unsavedChanges = - secretsToDelete.length > 0 || - JSON.stringify(serverAppSecrets) !== JSON.stringify(clientAppSecrets) - useEffect(() => { if (keyring !== null && data?.appEnvironments && userCanReadSecrets) fetchAndDecryptAppEnvs(data?.appEnvironments) @@ -819,11 +847,11 @@ export default function Secrets({ params }: { params: { team: string; app: strin )} - {/* {data.envSyncs && userCanReadSyncs && ( + {syncsData?.syncs && userCanReadSyncs && (
- +
- )} */} + )}