Deleting{' '}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/api-keys/components/create-api-key-modal/create-api-key-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/api-keys/components/create-api-key-modal/create-api-key-modal.tsx
index 439280bf25..12b0692159 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/api-keys/components/create-api-key-modal/create-api-key-modal.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/api-keys/components/create-api-key-modal/create-api-key-modal.tsx
@@ -62,8 +62,8 @@ export function CreateApiKeyModal({
if (isDuplicate) {
setCreateError(
keyType === 'workspace'
- ? `A workspace API key named "${trimmedName}" already exists. Please choose a different name.`
- : `A personal API key named "${trimmedName}" already exists. Please choose a different name.`
+ ? `A workspace Sim key named "${trimmedName}" already exists. Please choose a different name.`
+ : `A personal Sim key named "${trimmedName}" already exists. Please choose a different name.`
)
return
}
@@ -86,11 +86,11 @@ export function CreateApiKeyModal({
} catch (error: unknown) {
logger.error('API key creation failed:', { error })
const errorMessage =
- error instanceof Error ? error.message : 'Failed to create API key. Please try again.'
+ error instanceof Error ? error.message : 'Failed to create Sim key. Please try again.'
if (errorMessage.toLowerCase().includes('already exists')) {
setCreateError(errorMessage)
} else {
- setCreateError('Failed to create API key. Please check your connection and try again.')
+ setCreateError('Failed to create Sim key. Please check your connection and try again.')
}
}
}
@@ -113,7 +113,7 @@ export function CreateApiKeyModal({
{/* Create API Key Dialog */}
- Create new API key
+ Create new Sim key
{keyType === 'workspace'
@@ -125,7 +125,7 @@ export function CreateApiKeyModal({
{canManageWorkspaceKeys && (
- API Key Type
+ Sim Key Type
- Enter a name for your API key to help you identify it later.
+ Enter a name for your Sim key to help you identify it later.
{/* Hidden decoy fields to prevent browser autofill */}
- Your API key has been created
+ Your Sim key has been created
- This is the only time you will see your API key.{' '}
+ This is the only time you will see your Sim key.{' '}
Copy it now and store it securely.
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/credentials/credentials-manager.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/credentials/credentials-manager.tsx
index c2e6960f34..036f280675 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/credentials/credentials-manager.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/credentials/credentials-manager.tsx
@@ -2,7 +2,7 @@
import { createElement, useCallback, useEffect, useMemo, useState } from 'react'
import { createLogger } from '@sim/logger'
-import { AlertTriangle, Check, Copy, Plus, RefreshCw, Search, Share2, Trash2 } from 'lucide-react'
+import { AlertTriangle, Check, Copy, Plus, RefreshCw, Search, Share2, X } from 'lucide-react'
import { useParams } from 'next/navigation'
import {
Badge,
@@ -20,9 +20,8 @@ import {
Textarea,
Tooltip,
} from '@/components/emcn'
-import { Skeleton } from '@/components/ui'
+import { Skeleton, Input as UiInput } from '@/components/ui'
import { useSession } from '@/lib/auth/auth-client'
-import { cn } from '@/lib/core/utils/cn'
import {
clearPendingCredentialCreateRequest,
PENDING_CREDENTIAL_CREATE_REQUEST_EVENT,
@@ -72,8 +71,8 @@ type SecretScope = 'workspace' | 'personal'
type SecretInputMode = 'single' | 'bulk'
const createTypeOptions = [
- { value: 'oauth', label: 'OAuth Account' },
{ value: 'secret', label: 'Secret' },
+ { value: 'oauth', label: 'OAuth Account' },
] as const
interface ParsedEnvEntry {
@@ -151,9 +150,9 @@ function typeBadgeVariant(type: WorkspaceCredential['type']): 'blue' | 'amber' |
}
function typeLabel(type: WorkspaceCredential['type']): string {
- if (type === 'oauth') return 'OAuth'
- if (type === 'env_workspace') return 'Workspace Secret'
- return 'Personal Secret'
+ if (type === 'oauth') return 'oauth'
+ if (type === 'env_workspace') return 'workspace secret'
+ return 'personal secret'
}
function normalizeEnvKeyInput(raw: string): string {
@@ -162,6 +161,21 @@ function normalizeEnvKeyInput(raw: string): string {
return wrappedMatch ? wrappedMatch[1] : trimmed
}
+function CredentialSkeleton() {
+ return (
+
+ )
+}
+
export function CredentialsManager() {
const params = useParams()
const workspaceId = (params?.workspaceId as string) || ''
@@ -171,7 +185,7 @@ export function CredentialsManager() {
const [memberRole, setMemberRole] = useState('admin')
const [memberUserId, setMemberUserId] = useState('')
const [showCreateModal, setShowCreateModal] = useState(false)
- const [createType, setCreateType] = useState('oauth')
+ const [createType, setCreateType] = useState('secret')
const [createSecretScope, setCreateSecretScope] = useState('workspace')
const [createDisplayName, setCreateDisplayName] = useState('')
const [createDescription, setCreateDescription] = useState('')
@@ -179,7 +193,7 @@ export function CredentialsManager() {
const [createEnvValue, setCreateEnvValue] = useState('')
const [createOAuthProviderId, setCreateOAuthProviderId] = useState('')
const [createSecretInputMode, setCreateSecretInputMode] = useState('single')
- const [createBulkText, setCreateBulkText] = useState('')
+ const [createBulkEntries, setCreateBulkEntries] = useState([])
const [createError, setCreateError] = useState(null)
const [detailsError, setDetailsError] = useState(null)
const [selectedEnvValueDraft, setSelectedEnvValueDraft] = useState('')
@@ -188,6 +202,9 @@ export function CredentialsManager() {
const [selectedDisplayNameDraft, setSelectedDisplayNameDraft] = useState('')
const [showCreateOAuthRequiredModal, setShowCreateOAuthRequiredModal] = useState(false)
const [copyIdSuccess, setCopyIdSuccess] = useState(false)
+ const [credentialToDelete, setCredentialToDelete] = useState(null)
+ const [showDeleteConfirmDialog, setShowDeleteConfirmDialog] = useState(false)
+ const [deleteError, setDeleteError] = useState(null)
const { data: session } = useSession()
const currentUserId = session?.user?.id || ''
@@ -261,6 +278,7 @@ export function CredentialsManager() {
oauthConnections.map((service) => ({
value: service.providerId,
label: service.name,
+ icon: getServiceConfigByProviderId(service.providerId)?.icon,
})),
[oauthConnections]
)
@@ -533,14 +551,14 @@ export function CredentialsManager() {
}, [selectedCredential])
const resetCreateForm = () => {
- setCreateType('oauth')
+ setCreateType('secret')
setCreateSecretScope('workspace')
setCreateSecretInputMode('single')
setCreateDisplayName('')
setCreateDescription('')
setCreateEnvKey('')
setCreateEnvValue('')
- setCreateBulkText('')
+ setCreateBulkEntries([])
setCreateOAuthProviderId('')
setCreateError(null)
setShowCreateOAuthRequiredModal(false)
@@ -639,7 +657,7 @@ export function CredentialsManager() {
setShowCreateModal(false)
resetCreateForm()
} catch (error: unknown) {
- const message = error instanceof Error ? error.message : 'Failed to create credential'
+ const message = error instanceof Error ? error.message : 'Failed to create secret'
setCreateError(message)
logger.error('Failed to create credential', error)
}
@@ -649,14 +667,40 @@ export function CredentialsManager() {
if (!workspaceId) return
setCreateError(null)
- const { entries, errors } = parseEnvText(createBulkText)
- if (errors.length > 0) {
- setCreateError(errors.join('\n'))
+ const entries = createBulkEntries
+ .map((e) => ({ key: e.key.trim(), value: e.value.trim() }))
+ .filter((e) => e.key || e.value)
+
+ if (entries.length === 0) {
+ setCreateError('Add at least one secret.')
return
}
- if (entries.length === 0) {
- setCreateError('No valid KEY=VALUE pairs found. Add one per line, e.g. API_KEY=sk-abc123')
+ const errors: string[] = []
+ const seenKeys = new Set()
+ for (let i = 0; i < entries.length; i++) {
+ const { key, value } = entries[i]
+ if (!key) {
+ errors.push(`Row ${i + 1}: empty key`)
+ continue
+ }
+ if (!isValidEnvVarName(key)) {
+ errors.push(`Row ${i + 1}: "${key}" must contain only letters, numbers, and underscores`)
+ continue
+ }
+ if (!value) {
+ errors.push(`Row ${i + 1}: "${key}" has an empty value`)
+ continue
+ }
+ if (seenKeys.has(key.toUpperCase())) {
+ errors.push(`Row ${i + 1}: duplicate key "${key}"`)
+ continue
+ }
+ seenKeys.add(key.toUpperCase())
+ }
+
+ if (errors.length > 0) {
+ setCreateError(errors.join('\n'))
return
}
@@ -759,16 +803,48 @@ export function CredentialsManager() {
}
}
- const handleDeleteCredential = async () => {
- if (!selectedCredential) return
- if (selectedCredential.type === 'oauth') {
- await handleDisconnectSelectedCredential()
- return
- }
+ const handleDeleteClick = (credential: WorkspaceCredential) => {
+ setCredentialToDelete(credential)
+ setDeleteError(null)
+ setShowDeleteConfirmDialog(true)
+ }
+
+ const handleConfirmDelete = async () => {
+ if (!credentialToDelete) return
+ setDeleteError(null)
+
try {
- await deleteCredential.mutateAsync(selectedCredential.id)
- setSelectedCredentialId(null)
+ if (credentialToDelete.type === 'oauth') {
+ if (!credentialToDelete.accountId || !credentialToDelete.providerId) {
+ const errorMessage =
+ 'Cannot disconnect: missing account information. Please try reconnecting this credential first.'
+ setDeleteError(errorMessage)
+ logger.error('Cannot disconnect OAuth credential: missing accountId or providerId')
+ return
+ }
+ await disconnectOAuthService.mutateAsync({
+ provider: credentialToDelete.providerId.split('-')[0] || credentialToDelete.providerId,
+ providerId: credentialToDelete.providerId,
+ serviceId: credentialToDelete.providerId,
+ accountId: credentialToDelete.accountId,
+ })
+ await refetchCredentials()
+ window.dispatchEvent(
+ new CustomEvent('oauth-credentials-updated', {
+ detail: { providerId: credentialToDelete.providerId, workspaceId },
+ })
+ )
+ } else {
+ await deleteCredential.mutateAsync(credentialToDelete.id)
+ }
+ if (selectedCredentialId === credentialToDelete.id) {
+ setSelectedCredentialId(null)
+ }
+ setShowDeleteConfirmDialog(false)
+ setCredentialToDelete(null)
} catch (error) {
+ const message = error instanceof Error ? error.message : 'Failed to delete credential'
+ setDeleteError(message)
logger.error('Failed to delete credential', error)
}
}
@@ -851,31 +927,6 @@ export function CredentialsManager() {
}
}
- const handleDisconnectSelectedCredential = async () => {
- if (!selectedCredential || selectedCredential.type !== 'oauth' || !selectedCredential.accountId)
- return
- if (!selectedCredential.providerId) return
-
- try {
- await disconnectOAuthService.mutateAsync({
- provider: selectedCredential.providerId.split('-')[0] || selectedCredential.providerId,
- providerId: selectedCredential.providerId,
- serviceId: selectedCredential.providerId,
- accountId: selectedCredential.accountId,
- })
-
- setSelectedCredentialId(null)
- await refetchCredentials()
- window.dispatchEvent(
- new CustomEvent('oauth-credentials-updated', {
- detail: { providerId: selectedCredential.providerId, workspaceId },
- })
- )
- } catch (error) {
- logger.error('Failed to disconnect credential account', error)
- }
- }
-
const handleReconnectOAuth = async () => {
if (
!selectedCredential ||
@@ -964,81 +1015,430 @@ export function CredentialsManager() {
}
}
- return (
-
-
-
-
-
- setSearchTerm(event.target.value)}
- placeholder='Search credentials...'
- className='pl-[32px]'
- />
-
-
setShowCreateModal(true)}>
-
-
-
-
-
- {credentialsLoading ? (
-
-
-
-
-
- ) : sortedCredentials.length === 0 ? (
-
- No credentials available for this workspace.
+ const hasCredentials = credentials && credentials.length > 0
+ const showNoResults =
+ searchTerm.trim() && sortedCredentials.length === 0 && credentials.length > 0
+
+ const createModalJsx = (
+
{
+ setShowCreateModal(open)
+ if (!open) resetCreateForm()
+ }}
+ >
+
+ Create Secret
+
+
+
+
Type
+
+ ({
+ value: option.value,
+ label: option.label,
+ }))}
+ value={
+ createTypeOptions.find((option) => option.value === createType)?.label || ''
+ }
+ selectedValue={createType}
+ onChange={(value) => {
+ setCreateType(value as CreateCredentialType)
+ setCreateError(null)
+ }}
+ placeholder='Select type'
+ />
+
- ) : (
-
- {sortedCredentials.map((credential) => (
-
handleSelectCredential(credential)}
- >
-
-
- {credential.displayName}
-
-
+
+
+ Display name*
+
+ setCreateDisplayName(event.target.value)}
+ placeholder='Secret name'
+ autoComplete='off'
+ className='mt-[6px]'
+ />
+
+
+ Description
+
+
+
Account
+
+
option.value === createOAuthProviderId)
+ ?.label || ''
+ }
+ selectedValue={createOAuthProviderId}
+ onChange={setCreateOAuthProviderId}
+ placeholder='Select OAuth service'
+ searchable
+ searchPlaceholder='Search services...'
+ overlayContent={
+ createOAuthProviderId
+ ? (() => {
+ const config = getServiceConfigByProviderId(createOAuthProviderId)
+ const label =
+ oauthServiceOptions.find((o) => o.value === createOAuthProviderId)
+ ?.label || ''
+ return (
+
+ {config &&
+ createElement(config.icon, {
+ className: 'h-[14px] w-[14px] flex-shrink-0',
+ })}
+ {label}
+
+ )
+ })()
+ : undefined
+ }
+ />
+
+
+ {existingOAuthDisplayName && (
+
+
+
+
+ A secret named{' '}
+ {existingOAuthDisplayName.displayName} {' '}
+ already exists.
+
+
+
+ )}
+
+ ) : (
+
+ {createSecretInputMode === 'single' ? (
+ <>
+
+
+
+ Key*
+
+
+
+ Value*
+
+
+
+
+
+ Description
+
+ >
+ ) : (
+
+
Secrets ({createBulkEntries.length})
+
+
+ {createBulkEntries.map((entry, index) => (
+
+
{
+ const updated = [...createBulkEntries]
+ updated[index] = { ...entry, key: event.target.value }
+ setCreateBulkEntries(updated)
+ }}
+ placeholder='KEY'
+ autoComplete='off'
+ autoCapitalize='none'
+ autoCorrect='off'
+ spellCheck={false}
+ data-lpignore='true'
+ data-1p-ignore='true'
+ />
+
+
{
+ const updated = [...createBulkEntries]
+ updated[index] = { ...entry, value: event.target.value }
+ setCreateBulkEntries(updated)
+ }}
+ placeholder='Value'
+ autoComplete='new-password'
+ autoCapitalize='none'
+ autoCorrect='off'
+ spellCheck={false}
+ data-lpignore='true'
+ data-1p-ignore='true'
+ />
+
{
+ const updated = createBulkEntries.filter((_, i) => i !== index)
+ if (updated.length === 0) {
+ setCreateSecretInputMode('single')
+ }
+ setCreateBulkEntries(updated)
+ }}
+ >
+
+
+
+ ))}
+
+
+ )}
+
+
Scope
+
+ setCreateSecretScope(value as SecretScope)}
>
- {typeLabel(credential.type)}
-
+
+ Workspace
+
+
+ Personal
+
+
+
+
+
+ {selectedExistingEnvCredential && (
+
+
+
+
+ A secret with key{' '}
+
+ {selectedExistingEnvCredential.displayName}
+ {' '}
+ already exists.
+
+
+
+ )}
+ {!selectedExistingEnvCredential && crossScopeEnvConflict && (
+
+
+
+
+ A workspace secret with key{' '}
+ {crossScopeEnvConflict.envKey} already
+ exists. Workspace secrets take precedence at runtime.
+
+
-
- {credential.type === 'oauth'
- ? resolveProviderLabel(credential.providerId)
- : credential.envKey || credential.id}
+ )}
+
+ )}
+
+ {createError && (
+
+
+
+
+ {createError}
-
- ))}
+
+
+ )}
+
+
+
+ setShowCreateModal(false)}>
+ Cancel
+
+
+ {createType === 'oauth'
+ ? connectOAuthService.isPending
+ ? 'Connecting...'
+ : 'Connect'
+ : createSecretInputMode === 'bulk'
+ ? createCredential.isPending ||
+ savePersonalEnvironment.isPending ||
+ upsertWorkspaceEnvironment.isPending
+ ? 'Importing...'
+ : 'Import all'
+ : 'Create'}
+
+
+
+
+ )
+
+ const oauthRequiredModalJsx = showCreateOAuthRequiredModal && createOAuthProviderId && (
+
setShowCreateOAuthRequiredModal(false)}
+ provider={createOAuthProviderId as OAuthProvider}
+ toolName={resolveProviderLabel(createOAuthProviderId)}
+ requiredScopes={createOAuthRequiredScopes}
+ newScopes={[]}
+ serviceId={selectedOAuthService?.id || createOAuthProviderId}
+ onConnect={async () => {
+ await handleConnectOAuthService()
+ }}
+ />
+ )
+
+ const handleCloseDeleteDialog = () => {
+ setShowDeleteConfirmDialog(false)
+ setCredentialToDelete(null)
+ setDeleteError(null)
+ }
+
+ const deleteConfirmDialogJsx = (
+ !open && handleCloseDeleteDialog()}
+ >
+
+
+ {credentialToDelete?.type === 'oauth' ? 'Disconnect Secret' : 'Delete Secret'}
+
+
+
+ Are you sure you want to{' '}
+ {credentialToDelete?.type === 'oauth' ? 'disconnect' : 'delete'}{' '}
+
+ {credentialToDelete?.displayName}
+
+ ? This action cannot be undone.
+
+ {deleteError && (
+
)}
-
-
+
+
+
+ Cancel
+
+
+ {deleteCredential.isPending || disconnectOAuthService.isPending
+ ? 'Deleting...'
+ : credentialToDelete?.type === 'oauth'
+ ? 'Disconnect'
+ : 'Delete'}
+
+
+
+
+ )
-
- {!selectedCredential ? (
-
- Select a credential to manage members.
-
- ) : (
-
-
-
+ if (selectedCredential) {
+ return (
+ <>
+
+
+
+
+
Type
{typeLabel(selectedCredential.type)}
@@ -1051,91 +1451,15 @@ export function CredentialsManager() {
)}
- {isSelectedAdmin && (
-
-
- {isSavingDetails ? 'Saving...' : 'Save'}
-
- {selectedCredential.type === 'oauth' && (
-
-
-
-
-
-
- Reconnect account
-
- )}
- {selectedCredential.type === 'env_personal' && (
-
-
-
-
-
-
- Promote to Workspace Secret
-
- )}
- {selectedCredential.type === 'oauth' &&
- (workspaceUserOptions.length > 0 || isShareingWithWorkspace) && (
-
-
-
-
-
-
-
- {isShareingWithWorkspace ? 'Sharing...' : 'Share with workspace'}
-
-
- )}
-
-
-
-
-
-
-
- {selectedCredential.type === 'oauth'
- ? 'Disconnect account'
- : 'Delete credential'}
-
-
-
- )}
{selectedCredential.type === 'oauth' ? (
-
-
+ <>
+
-
-
Description
+
+
+
+ Description
+
-
-
Connected service
-
+
+
+
+ Connected service
+
+
{selectedOAuthServiceConfig ? (
createElement(selectedOAuthServiceConfig.icon, { className: 'h-4 w-4' })
@@ -1196,21 +1525,27 @@ export function CredentialsManager() {
-
+ >
) : (
-
-
Secret key
-
-
+ <>
+
+
+ Secret key
+
+
+
+
+
- Secret value
+
+ Secret value
+
{canEditSelectedEnvValue && (
-
-
Description
+
+
+
+ Description
+
-
+ >
)}
+
{detailsError && (
-
-
-
- Members
-
+
+
+ Members ({activeMembers.length})
+
- {membersLoading ? (
-
-
-
-
- ) : (
-
- {activeMembers.map((member) => (
-
-
-
- {member.userName || member.userEmail || member.userId}
-
-
- {member.userEmail || member.userId}
-
-
+ {membersLoading ? (
+
+
+
+
+ ) : (
+
+ {activeMembers.map((member) => (
+
+
+
+ {member.userName || member.userEmail || member.userId}
+
+
+ {member.userEmail || member.userId}
+
+
- {isSelectedAdmin ? (
- <>
-
({
- value: option.value,
- label: option.label,
- }))}
- value={
- roleOptions.find((option) => option.value === member.role)?.label ||
- ''
- }
- selectedValue={member.role}
- onChange={(value) =>
- handleChangeMemberRole(
- member.userId,
- value as WorkspaceCredentialRole
- )
- }
- placeholder='Role'
- disabled={member.role === 'admin' && adminMemberCount <= 1}
- size='sm'
- />
- {selectedCredential.type !== 'env_workspace' ? (
- handleRemoveMember(member.userId)}
+ {isSelectedAdmin ? (
+ <>
+ ({
+ value: option.value,
+ label: option.label,
+ }))}
+ value={
+ roleOptions.find((option) => option.value === member.role)?.label ||
+ ''
+ }
+ selectedValue={member.role}
+ onChange={(value) =>
+ handleChangeMemberRole(
+ member.userId,
+ value as WorkspaceCredentialRole
+ )
+ }
+ placeholder='Role'
disabled={member.role === 'admin' && adminMemberCount <= 1}
- >
- Remove
-
- ) : (
+ size='sm'
+ />
+ {selectedCredential.type !== 'env_workspace' ? (
+ handleRemoveMember(member.userId)}
+ disabled={member.role === 'admin' && adminMemberCount <= 1}
+ >
+ Remove
+
+ ) : (
+
+ )}
+ >
+ ) : (
+ <>
+
+ {member.role}
+
- )}
- >
- ) : (
- <>
-
- {member.role}
-
-
- >
- )}
-
- ))}
-
- )}
-
- {isSelectedAdmin && selectedCredential.type !== 'env_workspace' && (
-
-
Add member
-
- option.value === memberUserId)
- ?.label || ''
- }
- selectedValue={memberUserId}
- onChange={setMemberUserId}
- placeholder='Select user'
- />
- ({
- value: option.value,
- label: option.label,
- }))}
- value={roleOptions.find((option) => option.value === memberRole)?.label || ''}
- selectedValue={memberRole}
- onChange={(value) => setMemberRole(value as WorkspaceCredentialRole)}
- placeholder='Role'
- />
-
- Add
-
+ >
+ )}
+
+ ))}
-
- )}
-
-
- )}
-
-
-
{
- setShowCreateModal(open)
- if (!open) resetCreateForm()
- }}
- >
-
- Create Credential
-
-
-
-
Type
-
- ({
- value: option.value,
- label: option.label,
- }))}
- value={
- createTypeOptions.find((option) => option.value === createType)?.label || ''
- }
- selectedValue={createType}
- onChange={(value) => {
- setCreateType(value as CreateCredentialType)
- setCreateError(null)
- }}
- placeholder='Select credential type'
- />
-
-
+ )}
- {createType === 'oauth' ? (
-
-
- Display name
- setCreateDisplayName(event.target.value)}
- placeholder='Credential name'
- autoComplete='off'
- className='mt-[6px]'
- />
-
-
- Description
-
-
-
OAuth service
-
+ {isSelectedAdmin && selectedCredential.type !== 'env_workspace' && (
+
+
Add member
+
option.value === createOAuthProviderId
- )?.label || ''
+ workspaceUserOptions.find((option) => option.value === memberUserId)
+ ?.label || ''
}
- selectedValue={createOAuthProviderId}
- onChange={setCreateOAuthProviderId}
- placeholder='Select OAuth service'
+ selectedValue={memberUserId}
+ onChange={setMemberUserId}
+ placeholder='Select user'
/>
-
-
- {existingOAuthDisplayName && (
-
-
-
-
- A credential named{' '}
-
- {existingOAuthDisplayName.displayName}
- {' '}
- already exists.
-
-
-
- )}
-
- ) : (
-
-
-
Scope
-
- setCreateSecretScope(value as SecretScope)}
+ ({
+ value: option.value,
+ label: option.label,
+ }))}
+ value={
+ roleOptions.find((option) => option.value === memberRole)?.label || ''
+ }
+ selectedValue={memberRole}
+ onChange={(value) => setMemberRole(value as WorkspaceCredentialRole)}
+ placeholder='Role'
+ />
+
-
- Workspace
-
-
- Personal
-
-
+ Add
+
-
-
Mode
-
- {
- setCreateSecretInputMode(value as SecretInputMode)
- setCreateError(null)
- }}
+ )}
+
+
+
+
+
+
+ setSelectedCredentialId(null)} variant='active'>
+ Back
+
+ {isSelectedAdmin && (
+ <>
+ {selectedCredential.type === 'oauth' && (
+
+
+ Reconnect
+
+ )}
+ {selectedCredential.type === 'env_personal' && (
+
+
+ Promote
+
+ )}
+ {selectedCredential.type === 'oauth' &&
+ (workspaceUserOptions.length > 0 || isShareingWithWorkspace) && (
+
-
- Single
-
-
- Bulk
-
-
-
-
+
+ {isShareingWithWorkspace ? 'Sharing...' : 'Share'}
+
+ )}
+
handleDeleteClick(selectedCredential)}
+ disabled={
+ deleteCredential.isPending || isPromoting || disconnectOAuthService.isPending
+ }
+ >
+ {selectedCredential.type === 'oauth' ? 'Disconnect' : 'Delete'}
+
+ >
+ )}
+
+ {isSelectedAdmin && (
+
+ {isSavingDetails ? 'Saving...' : 'Save'}
+
+ )}
+
+
- {createSecretInputMode === 'single' ? (
- <>
-
-
Secret key
-
{
- setCreateEnvKey(event.target.value)
- }}
- placeholder='API_KEY'
- autoComplete='off'
- autoCapitalize='none'
- autoCorrect='off'
- spellCheck={false}
- data-lpignore='true'
- data-1p-ignore='true'
- className='mt-[6px]'
- />
-
- Use it in blocks as {'{{KEY}}'}, for example {'{{API_KEY}}'}.
-
-
-
- Secret value
- setCreateEnvValue(event.target.value)}
- placeholder='Enter secret value'
- autoComplete='new-password'
- autoCapitalize='none'
- autoCorrect='off'
- spellCheck={false}
- data-lpignore='true'
- data-1p-ignore='true'
- className='mt-[6px]'
- />
-
-
- Description
-
+ {createModalJsx}
+ {oauthRequiredModalJsx}
+ {deleteConfirmDialogJsx}
+ >
+ )
+ }
- {selectedExistingEnvCredential && (
-
-
-
-
- A secret with key{' '}
-
- {selectedExistingEnvCredential.displayName}
- {' '}
- already exists.
-
-
+ return (
+ <>
+
+
+
+
+ setSearchTerm(e.target.value)}
+ disabled={credentialsLoading}
+ className='h-auto flex-1 border-0 bg-transparent p-0 font-base leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0'
+ />
+
+
setShowCreateModal(true)}
+ disabled={credentialsLoading}
+ variant='tertiary'
+ >
+
+ Add
+
+
+
+
+ {credentialsLoading ? (
+
+
+
+
+
+ ) : !hasCredentials ? (
+
+ Click "Add" above to get started
+
+ ) : (
+
+ {sortedCredentials.map((credential) => {
+ const serviceConfig =
+ credential.type === 'oauth' && credential.providerId
+ ? getServiceConfigByProviderId(credential.providerId)
+ : null
+
+ return (
+
+
+ {serviceConfig && (
+
+ {createElement(serviceConfig.icon, { className: 'h-4 w-4' })}
)}
- {!selectedExistingEnvCredential && crossScopeEnvConflict && (
-
-
-
-
- A workspace secret with key{' '}
- {crossScopeEnvConflict.envKey} {' '}
- already exists. Workspace secrets take precedence at runtime.
-
-
-
+
+
+ {credential.displayName}
+
+
+ {credential.description || 'No description'}
+
+
+
+
+
handleSelectCredential(credential)}>
+ Details
+
+ {credential.role === 'admin' && (
+
handleDeleteClick(credential)}
+ disabled={deleteCredential.isPending || disconnectOAuthService.isPending}
+ >
+ Delete
+
)}
- >
- ) : (
-
- )}
-
- )}
-
- {createError && (
-
-
+ )
+ })}
+ {showNoResults && (
+
+ No secrets found matching “{searchTerm}”
)}
-
-
- setShowCreateModal(false)}>
- Cancel
-
-
- {createType === 'oauth'
- ? connectOAuthService.isPending
- ? 'Connecting...'
- : 'Connect'
- : createSecretInputMode === 'bulk'
- ? createCredential.isPending ||
- savePersonalEnvironment.isPending ||
- upsertWorkspaceEnvironment.isPending
- ? 'Importing...'
- : 'Import all'
- : 'Create'}
-
-
-
-
- {showCreateOAuthRequiredModal && createOAuthProviderId && (
-
setShowCreateOAuthRequiredModal(false)}
- provider={createOAuthProviderId as OAuthProvider}
- toolName={resolveProviderLabel(createOAuthProviderId)}
- requiredScopes={createOAuthRequiredScopes}
- newScopes={[]}
- serviceId={selectedOAuthService?.id || createOAuthProviderId}
- onConnect={async () => {
- await handleConnectOAuthService()
- }}
- />
- )}
-
+ )}
+
+
+
+ {createModalJsx}
+ {oauthRequiredModalJsx}
+ {deleteConfirmDialogJsx}
+ >
)
}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx
index 9c04f96113..0956f6a6d0 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx
@@ -19,7 +19,6 @@ import {
} from 'lucide-react'
import {
Card,
- Connections,
HexSimple,
Key,
SModal,
@@ -32,6 +31,7 @@ import {
SModalSidebarItem,
SModalSidebarSection,
SModalSidebarSectionTitle,
+ TerminalWindow,
} from '@/components/emcn'
import { AgentSkillsIcon, McpIcon } from '@/components/icons'
import { useSession } from '@/lib/auth/auth-client'
@@ -153,11 +153,11 @@ const allNavigationItems: NavigationItem[] = [
requiresHosted: true,
requiresTeam: true,
},
- { id: 'credentials', label: 'Credentials', icon: Connections, section: 'account' },
+ { id: 'credentials', label: 'Secrets', icon: Key, section: 'account' },
{ id: 'custom-tools', label: 'Custom Tools', icon: Wrench, section: 'tools' },
{ id: 'skills', label: 'Skills', icon: AgentSkillsIcon, section: 'tools' },
{ id: 'mcp', label: 'MCP Tools', icon: McpIcon, section: 'tools' },
- { id: 'apikeys', label: 'API Keys', icon: Key, section: 'system' },
+ { id: 'apikeys', label: 'Sim Keys', icon: TerminalWindow, section: 'system' },
{ id: 'workflow-mcp-servers', label: 'MCP Servers', icon: Server, section: 'system' },
{
id: 'byok',
@@ -461,7 +461,7 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
- Configure your workspace settings, credentials, and preferences
+ Configure your workspace settings, secrets, and preferences
diff --git a/apps/sim/components/emcn/icons/index.ts b/apps/sim/components/emcn/icons/index.ts
index 2318757ca0..9248425634 100644
--- a/apps/sim/components/emcn/icons/index.ts
+++ b/apps/sim/components/emcn/icons/index.ts
@@ -24,6 +24,7 @@ export { PanelLeft } from './panel-left'
export { Play, PlayOutline } from './play'
export { Redo } from './redo'
export { Rocket } from './rocket'
+export { TerminalWindow } from './terminal-window'
export { Trash } from './trash'
export { Trash2 } from './trash2'
export { Undo } from './undo'
diff --git a/apps/sim/components/emcn/icons/terminal-window.tsx b/apps/sim/components/emcn/icons/terminal-window.tsx
new file mode 100644
index 0000000000..670c8c83c7
--- /dev/null
+++ b/apps/sim/components/emcn/icons/terminal-window.tsx
@@ -0,0 +1,26 @@
+import type { SVGProps } from 'react'
+
+/**
+ * Terminal window icon component
+ * @param props - SVG properties including className, fill, etc.
+ */
+export function TerminalWindow(props: SVGProps
) {
+ return (
+
+
+
+
+
+
+ )
+}