Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { createLogger } from '@sim/logger'
import { useQueryClient } from '@tanstack/react-query'
import { Check, Clipboard, Key, Search } from 'lucide-react'
import { useParams, useRouter } from 'next/navigation'
import {
Expand Down Expand Up @@ -42,6 +43,7 @@ import {
useWorkspaceCredentials,
type WorkspaceCredential,
type WorkspaceCredentialRole,
workspaceCredentialKeys,
} from '@/hooks/queries/credentials'
import {
usePersonalEnvironment,
Expand Down Expand Up @@ -125,6 +127,7 @@ interface WorkspaceVariableRowProps {
renamingKey: string | null
pendingKeyValue: string
hasCredential: boolean
isAdmin: boolean
onRenameStart: (key: string) => void
onPendingKeyChange: (value: string) => void
onRenameEnd: (key: string, value: string) => void
Expand All @@ -138,12 +141,18 @@ function WorkspaceVariableRow({
renamingKey,
pendingKeyValue,
hasCredential,
isAdmin,
onRenameStart,
onPendingKeyChange,
onRenameEnd,
onDelete,
onViewDetails,
}: WorkspaceVariableRowProps) {
const [valueFocused, setValueFocused] = useState(false)

const maskedValueStyle =
isAdmin && !valueFocused ? ({ WebkitTextSecurity: 'disc' } as React.CSSProperties) : undefined

return (
<div className='contents'>
<EmcnInput
Expand All @@ -163,12 +172,19 @@ function WorkspaceVariableRow({
/>
<div />
<EmcnInput
value={value ? '\u2022'.repeat(value.length) : ''}
value={isAdmin ? value : value ? '\u2022'.repeat(value.length) : ''}
readOnly
onFocus={() => {
if (isAdmin) setValueFocused(true)
}}
onBlur={() => {
if (isAdmin) setValueFocused(false)
}}
autoComplete='off'
autoCorrect='off'
autoCapitalize='off'
spellCheck='false'
style={maskedValueStyle}
className='h-9'
/>
<Button
Expand Down Expand Up @@ -298,6 +314,14 @@ export function CredentialsManager() {
)

const { data: workspacePermissions } = useWorkspacePermissionsQuery(workspaceId || null)
const queryClient = useQueryClient()

const isAdmin = useMemo(() => {
const userId = session?.user?.id
if (!userId || !workspacePermissions?.users) return false
const currentUser = workspacePermissions.users.find((user) => user.userId === userId)
return currentUser?.permissionType === 'admin'
}, [session?.user?.id, workspacePermissions?.users])

const isLoading = isPersonalLoading || isWorkspaceLoading
const variables = useMemo(() => personalEnvData || {}, [personalEnvData])
Expand Down Expand Up @@ -923,6 +947,7 @@ export function CredentialsManager() {

const prevInitialVars = [...initialVarsRef.current]
const prevInitialWorkspaceVars = { ...initialWorkspaceVarsRef.current }
const mutations: Promise<unknown>[] = []

try {
setShowUnsavedChanges(false)
Expand All @@ -944,8 +969,6 @@ export function CredentialsManager() {
.filter((v) => v.key && v.value)
.reduce<Record<string, string>>((acc, { key, value }) => ({ ...acc, [key]: value }), {})

await savePersonalMutation.mutateAsync({ variables: validVariables })

const before = prevInitialWorkspaceVars
const after = mergedWorkspaceVars
const toUpsert: Record<string, string> = {}
Expand All @@ -961,33 +984,52 @@ export function CredentialsManager() {
if (!(k in after)) toDelete.push(k)
}

if (workspaceId) {
if (Object.keys(toUpsert).length) {
await upsertWorkspaceMutation.mutateAsync({ workspaceId, variables: toUpsert })
}
if (toDelete.length) {
await removeWorkspaceMutation.mutateAsync({ workspaceId, keys: toDelete })
const personalChanged = (() => {
const initialMap = new Map(
prevInitialVars.filter((v) => v.key && v.value).map((v) => [v.key, v.value])
)
const currentKeys = Object.keys(validVariables)
if (initialMap.size !== currentKeys.length) return true
for (const [key, value] of Object.entries(validVariables)) {
if (initialMap.get(key) !== value) return true
}
return false
})()

if (personalChanged) {
mutations.push(savePersonalMutation.mutateAsync({ variables: validVariables }))
}
if (workspaceId && (Object.keys(toUpsert).length || toDelete.length)) {
mutations.push(
(async () => {
if (Object.keys(toUpsert).length) {
await upsertWorkspaceMutation.mutateAsync({ workspaceId, variables: toUpsert })
}
if (toDelete.length) {
await removeWorkspaceMutation.mutateAsync({ workspaceId, keys: toDelete })
}
})()
)
}

const results = await Promise.allSettled(mutations)
const firstFailure = results.find((r): r is PromiseRejectedResult => r.status === 'rejected')
if (firstFailure) throw firstFailure.reason

setWorkspaceVars(mergedWorkspaceVars)
setNewWorkspaceRows([createEmptyEnvVar()])
} catch (error) {
hasSavedRef.current = false
initialVarsRef.current = prevInitialVars
initialWorkspaceVarsRef.current = prevInitialWorkspaceVars
logger.error('Failed to save environment variables:', error)
} finally {
if (mutations.length > 0) {
queryClient.invalidateQueries({ queryKey: workspaceCredentialKeys.lists() })
}
}
}, [
isListSaving,
envVars,
workspaceVars,
newWorkspaceRows,
workspaceId,
savePersonalMutation,
upsertWorkspaceMutation,
removeWorkspaceMutation,
])
// eslint-disable-next-line react-hooks/exhaustive-deps -- mutation objects and queryClient are stable (TanStack Query v5)
}, [isListSaving, envVars, workspaceVars, newWorkspaceRows, workspaceId])

const handleDiscardAndNavigate = useCallback(() => {
shouldBlockNavRef.current = false
Expand Down Expand Up @@ -1494,6 +1536,7 @@ export function CredentialsManager() {
renamingKey={renamingKey}
pendingKeyValue={pendingKeyValue}
hasCredential={envKeyToCredential.has(key)}
isAdmin={isAdmin}
onRenameStart={setRenamingKey}
onPendingKeyChange={setPendingKeyValue}
onRenameEnd={handleWorkspaceKeyRename}
Expand Down
31 changes: 3 additions & 28 deletions apps/sim/hooks/queries/environment.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { createLogger } from '@sim/logger'
import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import type { EnvironmentVariable, WorkspaceEnvironmentData } from '@/lib/environment/api'
import type { WorkspaceEnvironmentData } from '@/lib/environment/api'
import { fetchPersonalEnvironment, fetchWorkspaceEnvironment } from '@/lib/environment/api'
import { workspaceCredentialKeys } from '@/hooks/queries/credentials'
import { API_ENDPOINTS } from '@/stores/constants'

const logger = createLogger('EnvironmentQueries')
Expand Down Expand Up @@ -56,40 +55,20 @@ export function useSavePersonalEnvironment() {

return useMutation({
mutationFn: async ({ variables }: SavePersonalEnvironmentParams) => {
const transformedVariables = Object.entries(variables).reduce(
(acc, [key, value]) => ({
...acc,
[key]: { key, value },
}),
{}
)

const response = await fetch(API_ENDPOINTS.ENVIRONMENT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
variables: Object.entries(transformedVariables).reduce(
(acc, [key, value]) => ({
...acc,
[key]: (value as EnvironmentVariable).value,
}),
{}
),
}),
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ variables }),
})

if (!response.ok) {
throw new Error(`Failed to save environment variables: ${response.statusText}`)
}

logger.info('Saved personal environment variables')
return transformedVariables
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: environmentKeys.personal() })
queryClient.invalidateQueries({ queryKey: workspaceCredentialKeys.lists() })
},
})
}
Expand Down Expand Up @@ -124,8 +103,6 @@ export function useUpsertWorkspaceEnvironment() {
queryClient.invalidateQueries({
queryKey: environmentKeys.workspace(variables.workspaceId),
})
queryClient.invalidateQueries({ queryKey: environmentKeys.personal() })
queryClient.invalidateQueries({ queryKey: workspaceCredentialKeys.lists() })
},
})
}
Expand Down Expand Up @@ -160,8 +137,6 @@ export function useRemoveWorkspaceEnvironment() {
queryClient.invalidateQueries({
queryKey: environmentKeys.workspace(variables.workspaceId),
})
queryClient.invalidateQueries({ queryKey: environmentKeys.personal() })
queryClient.invalidateQueries({ queryKey: workspaceCredentialKeys.lists() })
},
})
}
Loading