Skip to content

Commit

Permalink
feat: add syncs, misc cleanup, bugfixes
Browse files Browse the repository at this point in the history
  • Loading branch information
rohan-chaturvedi committed Jan 10, 2025
1 parent 72d376b commit 0d7517e
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 70 deletions.
13 changes: 3 additions & 10 deletions frontend/app/[team]/apps/[app]/_components/AppSecretRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -296,9 +290,9 @@ export const AppSecretRow = ({
const keyIsDuplicate = false // TODO implement

const tooltipText = (env: { env: Partial<EnvironmentType>; 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'
}

Expand Down Expand Up @@ -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()}
/>
Expand Down
145 changes: 85 additions & 60 deletions frontend/app/[team]/apps/[app]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -19,6 +20,7 @@ import { KeyringContext } from '@/contexts/keyringContext'
import {
FaArrowRight,
FaBan,
FaBoxOpen,
FaCheckCircle,
FaChevronRight,
FaCircle,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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',
Expand All @@ -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: {
Expand Down Expand Up @@ -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<void>} Resolves once the secrets and folders are processed and state is updated.
*/
const fetchAndDecryptAppEnvs = async (appEnvironments: EnvironmentType[]): Promise<void> => {
setLoading(true)
const envSecrets = [] as EnvSecrets[]
const envFolders = [] as EnvFolders[]
Expand Down Expand Up @@ -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<string>()

Expand All @@ -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<void>} Resolves once the secrets are processed and the application state is updated.
*/
const handleBulkUpdateSecrets = async () => {
const userKxKeys = {
publicKey: await getUserKxPublicKey(keyring!.publicKey),
Expand Down Expand Up @@ -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) {
Expand All @@ -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)

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -819,11 +847,11 @@ export default function Secrets({ params }: { params: { team: string; app: strin
</Button>
)}

{/* {data.envSyncs && userCanReadSyncs && (
{syncsData?.syncs && userCanReadSyncs && (
<div>
<EnvSyncStatus syncs={data.envSyncs} team={params.team} app={params.app} />
<EnvSyncStatus syncs={syncsData.syncs} team={params.team} app={params.app} />
</div>
)} */}
)}

<Button
variant={unsavedChanges ? 'primary' : 'secondary'}
Expand All @@ -850,7 +878,7 @@ export default function Secrets({ params }: { params: { team: string; app: strin
</div>
</div>

{serverAppSecrets.length > 0 || appFolders.length > 0 ? (
{clientAppSecrets.length > 0 || appFolders.length > 0 ? (
<table className="table-auto w-full border border-neutral-500/40">
<thead id="table-head" className="sticky top-0 bg-zinc-300 dark:bg-zinc-800 z-10">
<tr className="divide-x divide-neutral-500/40">
Expand Down Expand Up @@ -902,26 +930,23 @@ export default function Secrets({ params }: { params: { team: string; app: strin
<Spinner size="xl" />
</div>
) : userCanReadEnvironments && userCanReadSecrets ? (
<div className="flex flex-col items-center py-40 border border-neutral-500/40 rounded-md bg-neutral-100 dark:bg-neutral-800">
<div className="font-semibold text-black dark:text-white text-2xl">No Secrets</div>
<div className="text-neutral-500">
There are no secrets in this app yet. Click on an environment below to start adding
secrets.
</div>
<div className="flex items-center gap-4 mt-8">
{data?.appEnvironments.map((env: EnvironmentType) => (
<Link key={env.id} href={`${pathname}/environments/${env.id}`}>
<Button variant="primary">
<div className="flex items-center gap-2 justify-center text-xl group">
{env.name}
<div className="opacity-30 group-hover:opacity-100 transform -translate-x-1 group-hover:translate-x-0 transition ease">
<FaArrowRight />
</div>
</div>
</Button>
</Link>
))}
</div>
<div className="flex flex-col items-center py-10 border border-neutral-500/40 rounded-md bg-neutral-100 dark:bg-neutral-800">
<EmptyState
title="No secrets"
subtitle="There are no secrets in this app yet. Click the button below to add a secret, or create one within a specific environment.
secrets."
graphic={
<div className="text-neutral-300 dark:text-neutral-700 text-7xl text-center">
<FaBoxOpen />
</div>
}
>
<div>
<Button variant="primary" onClick={handleAddNewClientSecret}>
<FaPlus /> New Secret
</Button>
</div>
</EmptyState>
</div>
) : (
<EmptyState
Expand Down

0 comments on commit 0d7517e

Please sign in to comment.