mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-11 07:58:06 -05:00
improvement(emcn): modal padding, api, chat, form
This commit is contained in:
@@ -123,7 +123,7 @@ export function CreateChunkModal({
|
||||
<ModalHeader>Create Chunk</ModalHeader>
|
||||
|
||||
<form>
|
||||
<ModalBody className='!pb-[16px]'>
|
||||
<ModalBody>
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
{error && <p className='text-[12px] text-[var(--text-error)]'>{error}</p>}
|
||||
|
||||
|
||||
@@ -399,7 +399,7 @@ export function DocumentTagsModal({
|
||||
</div>
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody className='!pb-[16px]'>
|
||||
<ModalBody>
|
||||
<div className='min-h-0 flex-1 overflow-y-auto'>
|
||||
<div className='space-y-[8px]'>
|
||||
<Label>Tags</Label>
|
||||
|
||||
@@ -260,7 +260,7 @@ export function EditChunkModal({
|
||||
</ModalHeader>
|
||||
|
||||
<form>
|
||||
<ModalBody className='!pb-[16px]'>
|
||||
<ModalBody>
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
{error && <p className='text-[12px] text-[var(--text-error)]'>{error}</p>}
|
||||
|
||||
|
||||
@@ -224,7 +224,7 @@ export function AddDocumentsModal({
|
||||
<ModalContent>
|
||||
<ModalHeader>Add Documents</ModalHeader>
|
||||
|
||||
<ModalBody className='!pb-[16px]'>
|
||||
<ModalBody>
|
||||
<div className='min-h-0 flex-1 overflow-y-auto'>
|
||||
<div className='space-y-[12px]'>
|
||||
{fileError && (
|
||||
@@ -242,8 +242,8 @@ export function AddDocumentsModal({
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
className={cn(
|
||||
'!bg-[var(--surface-1)] hover:!bg-[var(--surface-4)] w-full justify-center border border-[var(--c-575757)] border-dashed py-[10px]',
|
||||
isDragging && 'border-[var(--brand-primary-hex)]'
|
||||
'!bg-[var(--surface-1)] hover:!bg-[var(--surface-4)] w-full justify-center border border-[var(--border-1)] border-dashed py-[10px]',
|
||||
isDragging && 'border-[var(--surface-7)]'
|
||||
)}
|
||||
>
|
||||
<input
|
||||
|
||||
@@ -313,7 +313,7 @@ export function BaseTagsModal({ open, onOpenChange, knowledgeBaseId }: BaseTagsM
|
||||
</div>
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody className='!pb-[16px]'>
|
||||
<ModalBody>
|
||||
<div className='min-h-0 flex-1 overflow-y-auto'>
|
||||
<div className='space-y-[8px]'>
|
||||
<Label>
|
||||
@@ -458,7 +458,7 @@ export function BaseTagsModal({ open, onOpenChange, knowledgeBaseId }: BaseTagsM
|
||||
<Modal open={deleteTagDialogOpen} onOpenChange={setDeleteTagDialogOpen}>
|
||||
<ModalContent size='sm'>
|
||||
<ModalHeader>Delete Tag</ModalHeader>
|
||||
<ModalBody className='!pb-[16px]'>
|
||||
<ModalBody>
|
||||
<div className='space-y-[8px]'>
|
||||
<p className='text-[12px] text-[var(--text-secondary)]'>
|
||||
Are you sure you want to delete the "{selectedTag?.displayName}" tag? This will
|
||||
@@ -497,7 +497,7 @@ export function BaseTagsModal({ open, onOpenChange, knowledgeBaseId }: BaseTagsM
|
||||
<Modal open={viewDocumentsDialogOpen} onOpenChange={setViewDocumentsDialogOpen}>
|
||||
<ModalContent size='sm'>
|
||||
<ModalHeader>Documents using "{selectedTag?.displayName}"</ModalHeader>
|
||||
<ModalBody className='!pb-[16px]'>
|
||||
<ModalBody>
|
||||
<div className='space-y-[8px]'>
|
||||
<p className='text-[12px] text-[var(--text-tertiary)]'>
|
||||
{selectedTagUsage?.documentCount || 0} document
|
||||
|
||||
@@ -336,7 +336,7 @@ export function CreateBaseModal({ open, onOpenChange }: CreateBaseModalProps) {
|
||||
<ModalHeader>Create Knowledge Base</ModalHeader>
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)} className='flex min-h-0 flex-1 flex-col'>
|
||||
<ModalBody className='!pb-[16px]'>
|
||||
<ModalBody>
|
||||
<div ref={scrollContainerRef} className='min-h-0 flex-1 overflow-y-auto'>
|
||||
<div className='space-y-[12px]'>
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
@@ -436,8 +436,8 @@ export function CreateBaseModal({ open, onOpenChange }: CreateBaseModalProps) {
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
className={cn(
|
||||
'!bg-[var(--surface-1)] hover:!bg-[var(--surface-4)] w-full justify-center border border-[var(--c-575757)] border-dashed py-[10px]',
|
||||
isDragging && 'border-[var(--brand-primary-hex)]'
|
||||
'!bg-[var(--surface-1)] hover:!bg-[var(--surface-4)] w-full justify-center border border-[var(--border-1)] border-dashed py-[10px]',
|
||||
isDragging && 'border-[var(--surface-7)]'
|
||||
)}
|
||||
>
|
||||
<input
|
||||
|
||||
@@ -102,7 +102,7 @@ export function EditKnowledgeBaseModal({
|
||||
<ModalHeader>Edit Knowledge Base</ModalHeader>
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)} className='flex min-h-0 flex-1 flex-col'>
|
||||
<ModalBody className='!pb-[16px]'>
|
||||
<ModalBody>
|
||||
<div className='space-y-[12px]'>
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
<Label htmlFor='kb-name'>Name</Label>
|
||||
|
||||
@@ -510,26 +510,40 @@ function IdentifierInput({
|
||||
error && 'border-[var(--text-error)]'
|
||||
)}
|
||||
>
|
||||
<div className='flex items-center whitespace-nowrap bg-[var(--surface-5)] px-[8px] font-medium text-[var(--text-secondary)] text-sm dark:bg-[var(--surface-5)]'>
|
||||
<div className='flex items-center whitespace-nowrap bg-[var(--surface-5)] pr-[6px] pl-[8px] font-medium text-[var(--text-secondary)] text-sm dark:bg-[var(--surface-5)]'>
|
||||
{getDomainPrefix()}
|
||||
</div>
|
||||
<div className='relative flex-1'>
|
||||
<Input
|
||||
id='chat-url'
|
||||
placeholder='company-name'
|
||||
placeholder='my-chat'
|
||||
value={value}
|
||||
onChange={(e) => handleChange(e.target.value)}
|
||||
required
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
'rounded-none border-0 pl-0 shadow-none disabled:bg-transparent disabled:opacity-100',
|
||||
isChecking && 'pr-[32px]'
|
||||
(isChecking || (isValid && value)) && 'pr-[32px]'
|
||||
)}
|
||||
/>
|
||||
{isChecking && (
|
||||
{isChecking ? (
|
||||
<div className='-translate-y-1/2 absolute top-1/2 right-2'>
|
||||
<Loader2 className='h-4 w-4 animate-spin text-[var(--text-tertiary)]' />
|
||||
</div>
|
||||
) : (
|
||||
isValid &&
|
||||
value && (
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<div className='-translate-y-1/2 absolute top-1/2 right-2'>
|
||||
<Check className='h-4 w-4 text-[var(--brand-tertiary-2)]' />
|
||||
</div>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<span>Name is available</span>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { ChevronDown, ChevronRight, Eye, EyeOff, Loader2 } from 'lucide-react'
|
||||
import { Check, ChevronDown, ChevronRight, Eye, EyeOff, Loader2 } from 'lucide-react'
|
||||
import {
|
||||
Badge,
|
||||
ButtonGroup,
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
TagInput,
|
||||
type TagItem,
|
||||
Textarea,
|
||||
Tooltip,
|
||||
} from '@/components/emcn'
|
||||
import { Skeleton } from '@/components/ui'
|
||||
import { getEnv } from '@/lib/core/config/env'
|
||||
@@ -412,11 +413,7 @@ export function FormDeploy({
|
||||
const displayUrl = fullUrl.replace(/^https?:\/\//, '')
|
||||
|
||||
return (
|
||||
<form
|
||||
id='form-deploy-form'
|
||||
onSubmit={handleSubmit}
|
||||
className='-mx-1 space-y-4 overflow-y-auto px-1'
|
||||
>
|
||||
<form id='form-deploy-form' onSubmit={handleSubmit} className='-mx-1 space-y-4 px-1'>
|
||||
<div className='space-y-[12px]'>
|
||||
{/* URL Input - matching chat style */}
|
||||
<div>
|
||||
@@ -429,7 +426,7 @@ export function FormDeploy({
|
||||
(identifierError || errors.identifier) && 'border-[var(--text-error)]'
|
||||
)}
|
||||
>
|
||||
<div className='flex items-center whitespace-nowrap bg-[var(--surface-5)] px-[8px] font-medium text-[var(--text-secondary)] text-sm'>
|
||||
<div className='flex items-center whitespace-nowrap bg-[var(--surface-5)] pr-[6px] pl-[8px] font-medium text-[var(--text-secondary)] text-sm'>
|
||||
{getDomainPrefix()}
|
||||
</div>
|
||||
<div className='relative flex-1'>
|
||||
@@ -442,13 +439,28 @@ export function FormDeploy({
|
||||
placeholder='my-form'
|
||||
className={cn(
|
||||
'rounded-none border-0 pl-0 shadow-none',
|
||||
isCheckingIdentifier && 'pr-[32px]'
|
||||
(isCheckingIdentifier || (identifierValidationPassed && identifier)) &&
|
||||
'pr-[32px]'
|
||||
)}
|
||||
/>
|
||||
{isCheckingIdentifier && (
|
||||
{isCheckingIdentifier ? (
|
||||
<div className='-translate-y-1/2 absolute top-1/2 right-2'>
|
||||
<Loader2 className='h-4 w-4 animate-spin text-[var(--text-tertiary)]' />
|
||||
</div>
|
||||
) : (
|
||||
identifierValidationPassed &&
|
||||
identifier && (
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<div className='-translate-y-1/2 absolute top-1/2 right-2'>
|
||||
<Check className='h-4 w-4 text-[var(--brand-tertiary-2)]' />
|
||||
</div>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<span>Name is available</span>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -443,7 +443,7 @@ export function McpDeploy({
|
||||
return (
|
||||
<form
|
||||
id='mcp-deploy-form'
|
||||
className='-mx-1 space-y-[12px] overflow-y-auto px-1'
|
||||
className='-mx-1 space-y-[12px] px-1'
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
handleSave()
|
||||
|
||||
@@ -18,7 +18,11 @@ import {
|
||||
import { getEnv } from '@/lib/core/config/env'
|
||||
import { getInputFormatExample as getInputFormatExampleUtil } from '@/lib/workflows/operations/deployment-utils'
|
||||
import type { WorkflowDeploymentVersionResponse } from '@/lib/workflows/persistence/utils'
|
||||
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
|
||||
import { CreateApiKeyModal } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/api-keys/components'
|
||||
import { startsWithUuid } from '@/executor/constants'
|
||||
import { useApiKeys } from '@/hooks/queries/api-keys'
|
||||
import { useWorkspaceSettings } from '@/hooks/queries/workspace'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
import type { WorkflowState } from '@/stores/workflows/workflow/types'
|
||||
@@ -107,6 +111,22 @@ export function DeployModal({
|
||||
const [chatSuccess, setChatSuccess] = useState(false)
|
||||
const [formSuccess, setFormSuccess] = useState(false)
|
||||
|
||||
const [isCreateKeyModalOpen, setIsCreateKeyModalOpen] = useState(false)
|
||||
const userPermissions = useUserPermissionsContext()
|
||||
const canManageWorkspaceKeys = userPermissions.canAdmin
|
||||
const { data: apiKeysData, isLoading: isLoadingKeys } = useApiKeys(workflowWorkspaceId || '')
|
||||
const { data: workspaceSettingsData, isLoading: isLoadingSettings } = useWorkspaceSettings(
|
||||
workflowWorkspaceId || ''
|
||||
)
|
||||
const apiKeyWorkspaceKeys = apiKeysData?.workspaceKeys || []
|
||||
const apiKeyPersonalKeys = apiKeysData?.personalKeys || []
|
||||
const allowPersonalApiKeys =
|
||||
workspaceSettingsData?.settings?.workspace?.allowPersonalApiKeys ?? true
|
||||
const defaultKeyType = allowPersonalApiKeys ? 'personal' : 'workspace'
|
||||
const isApiKeysLoading = isLoadingKeys || isLoadingSettings
|
||||
const createButtonDisabled =
|
||||
isApiKeysLoading || (!allowPersonalApiKeys && !canManageWorkspaceKeys)
|
||||
|
||||
const getApiKeyLabel = (value?: string | null) => {
|
||||
if (value && value.trim().length > 0) {
|
||||
return value
|
||||
@@ -577,7 +597,7 @@ export function DeployModal({
|
||||
<>
|
||||
<Modal open={open} onOpenChange={handleCloseModal}>
|
||||
<ModalContent size='lg' className='h-[76vh]'>
|
||||
<ModalHeader>Deploy Workflow</ModalHeader>
|
||||
<ModalHeader>Workflow Deployment</ModalHeader>
|
||||
|
||||
<ModalTabs
|
||||
value={activeTab}
|
||||
@@ -650,7 +670,7 @@ export function DeployModal({
|
||||
)}
|
||||
</ModalTabsContent>
|
||||
|
||||
<ModalTabsContent value='form' className='h-full'>
|
||||
<ModalTabsContent value='form'>
|
||||
{workflowId && (
|
||||
<FormDeploy
|
||||
workflowId={workflowId}
|
||||
@@ -665,7 +685,7 @@ export function DeployModal({
|
||||
)}
|
||||
</ModalTabsContent>
|
||||
|
||||
<ModalTabsContent value='mcp' className='h-full'>
|
||||
<ModalTabsContent value='mcp'>
|
||||
{workflowId && (
|
||||
<McpDeploy
|
||||
workflowId={workflowId}
|
||||
@@ -691,6 +711,17 @@ export function DeployModal({
|
||||
onUndeploy={() => setShowUndeployConfirm(true)}
|
||||
/>
|
||||
)}
|
||||
{activeTab === 'api' && (
|
||||
<ModalFooter className='items-center justify-end'>
|
||||
<Button
|
||||
variant='tertiary'
|
||||
onClick={() => setIsCreateKeyModalOpen(true)}
|
||||
disabled={createButtonDisabled}
|
||||
>
|
||||
Generate API Key
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
)}
|
||||
{activeTab === 'chat' && (
|
||||
<ModalFooter className='items-center'>
|
||||
<div className='flex gap-2'>
|
||||
@@ -838,6 +869,16 @@ export function DeployModal({
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
<CreateApiKeyModal
|
||||
open={isCreateKeyModalOpen}
|
||||
onOpenChange={setIsCreateKeyModalOpen}
|
||||
workspaceId={workflowWorkspaceId || ''}
|
||||
existingKeyNames={[...apiKeyWorkspaceKeys, ...apiKeyPersonalKeys].map((k) => k.name)}
|
||||
allowPersonalApiKeys={allowPersonalApiKeys}
|
||||
canManageWorkspaceKeys={canManageWorkspaceKeys}
|
||||
defaultKeyType={defaultKeyType}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -911,7 +952,12 @@ function GeneralFooter({
|
||||
<ModalFooter className='items-center justify-between'>
|
||||
<StatusBadge isWarning={needsRedeployment} />
|
||||
<div className='flex items-center gap-2'>
|
||||
<Button variant='default' onClick={onUndeploy} disabled={isUndeploying || isSubmitting}>
|
||||
<Button
|
||||
variant='default'
|
||||
onClick={onUndeploy}
|
||||
disabled={isUndeploying || isSubmitting}
|
||||
className='px-[7px] py-[5px]'
|
||||
>
|
||||
{isUndeploying ? 'Undeploying...' : 'Undeploy'}
|
||||
</Button>
|
||||
{needsRedeployment && (
|
||||
|
||||
@@ -422,7 +422,7 @@ export function HelpModal({ open, onOpenChange }: HelpModalProps) {
|
||||
<ModalHeader>Help & Support</ModalHeader>
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)} className='flex min-h-0 flex-1 flex-col'>
|
||||
<ModalBody className='!pb-[16px]'>
|
||||
<ModalBody>
|
||||
<div ref={scrollContainerRef} className='min-h-0 flex-1 overflow-y-auto'>
|
||||
<div className='space-y-[12px]'>
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
@@ -472,9 +472,9 @@ export function HelpModal({ open, onOpenChange }: HelpModalProps) {
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
className={cn(
|
||||
'!bg-[var(--surface-1)] hover:!bg-[var(--surface-4)] w-full justify-center border border-[var(--c-575757)] border-dashed py-[10px]',
|
||||
'!bg-[var(--surface-1)] hover:!bg-[var(--surface-4)] w-full justify-center border border-[var(--border-1)] border-dashed py-[10px]',
|
||||
{
|
||||
'border-[var(--brand-primary-hex)]': isDragging,
|
||||
'border-[var(--surface-7)]': isDragging,
|
||||
}
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { Check, Copy, Info, Plus, Search } from 'lucide-react'
|
||||
import { Info, Plus, Search } from 'lucide-react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import {
|
||||
Button,
|
||||
Input as EmcnInput,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
@@ -21,11 +20,11 @@ import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/provide
|
||||
import {
|
||||
type ApiKey,
|
||||
useApiKeys,
|
||||
useCreateApiKey,
|
||||
useDeleteApiKey,
|
||||
useUpdateWorkspaceApiKeySettings,
|
||||
} from '@/hooks/queries/api-keys'
|
||||
import { useWorkspaceSettings } from '@/hooks/queries/workspace'
|
||||
import { CreateApiKeyModal } from './components'
|
||||
|
||||
const logger = createLogger('ApiKeys')
|
||||
|
||||
@@ -50,7 +49,6 @@ export function ApiKeys({ onOpenChange, registerCloseHandler }: ApiKeysProps) {
|
||||
} = useApiKeys(workspaceId)
|
||||
const { data: workspaceSettingsData, isLoading: isLoadingSettings } =
|
||||
useWorkspaceSettings(workspaceId)
|
||||
const createApiKeyMutation = useCreateApiKey()
|
||||
const deleteApiKeyMutation = useDeleteApiKey()
|
||||
const updateSettingsMutation = useUpdateWorkspaceApiKeySettings()
|
||||
|
||||
@@ -65,16 +63,10 @@ export function ApiKeys({ onOpenChange, registerCloseHandler }: ApiKeysProps) {
|
||||
|
||||
// Local UI state
|
||||
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false)
|
||||
const [newKeyName, setNewKeyName] = useState('')
|
||||
const [newKey, setNewKey] = useState<ApiKey | null>(null)
|
||||
const [showNewKeyDialog, setShowNewKeyDialog] = useState(false)
|
||||
const [deleteKey, setDeleteKey] = useState<ApiKey | null>(null)
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
|
||||
const [copySuccess, setCopySuccess] = useState(false)
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [keyType, setKeyType] = useState<'personal' | 'workspace'>('personal')
|
||||
const [shouldScrollToBottom, setShouldScrollToBottom] = useState(false)
|
||||
const [createError, setCreateError] = useState<string | null>(null)
|
||||
|
||||
const defaultKeyType = allowPersonalApiKeys ? 'personal' : 'workspace'
|
||||
const createButtonDisabled = isLoading || (!allowPersonalApiKeys && !canManageWorkspaceKeys)
|
||||
@@ -99,48 +91,6 @@ export function ApiKeys({ onOpenChange, registerCloseHandler }: ApiKeysProps) {
|
||||
.filter(({ key }) => key.name.toLowerCase().includes(searchTerm.toLowerCase()))
|
||||
}, [personalKeys, searchTerm])
|
||||
|
||||
const handleCreateKey = async () => {
|
||||
if (!userId || !newKeyName.trim()) return
|
||||
|
||||
const trimmedName = newKeyName.trim()
|
||||
const isDuplicate =
|
||||
keyType === 'workspace'
|
||||
? workspaceKeys.some((k) => k.name === trimmedName)
|
||||
: personalKeys.some((k) => k.name === trimmedName)
|
||||
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.`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
setCreateError(null)
|
||||
try {
|
||||
const data = await createApiKeyMutation.mutateAsync({
|
||||
workspaceId,
|
||||
name: trimmedName,
|
||||
keyType,
|
||||
})
|
||||
|
||||
setNewKey(data.key)
|
||||
setShowNewKeyDialog(true)
|
||||
setNewKeyName('')
|
||||
setKeyType('personal')
|
||||
setCreateError(null)
|
||||
setIsCreateDialogOpen(false)
|
||||
} catch (error: any) {
|
||||
logger.error('API key creation failed:', { error })
|
||||
const errorMessage = error.message || 'Failed to create API 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.')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteKey = async () => {
|
||||
if (!userId || !deleteKey) return
|
||||
|
||||
@@ -163,12 +113,6 @@ export function ApiKeys({ onOpenChange, registerCloseHandler }: ApiKeysProps) {
|
||||
}
|
||||
}
|
||||
|
||||
const copyToClipboard = (key: string) => {
|
||||
navigator.clipboard.writeText(key)
|
||||
setCopySuccess(true)
|
||||
setTimeout(() => setCopySuccess(false), 2000)
|
||||
}
|
||||
|
||||
const handleModalClose = (open: boolean) => {
|
||||
onOpenChange?.(open)
|
||||
}
|
||||
@@ -179,12 +123,6 @@ export function ApiKeys({ onOpenChange, registerCloseHandler }: ApiKeysProps) {
|
||||
}
|
||||
}, [registerCloseHandler])
|
||||
|
||||
useEffect(() => {
|
||||
if (!allowPersonalApiKeys && keyType === 'personal') {
|
||||
setKeyType('workspace')
|
||||
}
|
||||
}, [allowPersonalApiKeys, keyType])
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldScrollToBottom && scrollContainerRef.current) {
|
||||
scrollContainerRef.current.scrollTo({
|
||||
@@ -227,8 +165,6 @@ export function ApiKeys({ onOpenChange, registerCloseHandler }: ApiKeysProps) {
|
||||
}
|
||||
e.currentTarget.blur()
|
||||
setIsCreateDialogOpen(true)
|
||||
setKeyType(defaultKeyType)
|
||||
setCreateError(null)
|
||||
}}
|
||||
variant='tertiary'
|
||||
disabled={createButtonDisabled}
|
||||
@@ -443,164 +379,16 @@ export function ApiKeys({ onOpenChange, registerCloseHandler }: ApiKeysProps) {
|
||||
</Tooltip.Provider>
|
||||
)}
|
||||
|
||||
{/* Create API Key Dialog */}
|
||||
<Modal open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
|
||||
<ModalContent className='w-[400px]'>
|
||||
<ModalHeader>Create new API key</ModalHeader>
|
||||
<ModalBody>
|
||||
<p className='text-[12px] text-[var(--text-tertiary)]'>
|
||||
{keyType === 'workspace'
|
||||
? "This key will have access to all workflows in this workspace. Make sure to copy it after creation as you won't be able to see it again."
|
||||
: "This key will have access to your personal workflows. Make sure to copy it after creation as you won't be able to see it again."}
|
||||
</p>
|
||||
|
||||
<div className='mt-[16px] flex flex-col gap-[16px]'>
|
||||
{canManageWorkspaceKeys && (
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
<p className='font-medium text-[13px] text-[var(--text-secondary)]'>
|
||||
API Key Type
|
||||
</p>
|
||||
<div className='flex gap-[8px]'>
|
||||
<Button
|
||||
type='button'
|
||||
variant={keyType === 'personal' ? 'active' : 'default'}
|
||||
onClick={() => {
|
||||
setKeyType('personal')
|
||||
if (createError) setCreateError(null)
|
||||
}}
|
||||
disabled={!allowPersonalApiKeys}
|
||||
className='disabled:cursor-not-allowed disabled:opacity-60'
|
||||
>
|
||||
Personal
|
||||
</Button>
|
||||
<Button
|
||||
type='button'
|
||||
variant={keyType === 'workspace' ? 'active' : 'default'}
|
||||
onClick={() => {
|
||||
setKeyType('workspace')
|
||||
if (createError) setCreateError(null)
|
||||
}}
|
||||
>
|
||||
Workspace
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
<p className='font-medium text-[13px] text-[var(--text-secondary)]'>
|
||||
Enter a name for your API key to help you identify it later.
|
||||
</p>
|
||||
{/* Hidden decoy fields to prevent browser autofill */}
|
||||
<input
|
||||
type='text'
|
||||
name='fakeusernameremembered'
|
||||
autoComplete='username'
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: '-9999px',
|
||||
opacity: 0,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
tabIndex={-1}
|
||||
readOnly
|
||||
/>
|
||||
<EmcnInput
|
||||
value={newKeyName}
|
||||
onChange={(e) => {
|
||||
setNewKeyName(e.target.value)
|
||||
if (createError) setCreateError(null)
|
||||
}}
|
||||
placeholder='e.g., Development, Production'
|
||||
className='h-9'
|
||||
autoFocus
|
||||
name='api_key_label'
|
||||
autoComplete='off'
|
||||
autoCorrect='off'
|
||||
autoCapitalize='off'
|
||||
data-lpignore='true'
|
||||
data-form-type='other'
|
||||
/>
|
||||
{createError && (
|
||||
<p className='text-[11px] text-[var(--text-error)] leading-tight'>
|
||||
{createError}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
variant='default'
|
||||
onClick={() => {
|
||||
setIsCreateDialogOpen(false)
|
||||
setNewKeyName('')
|
||||
setKeyType(defaultKeyType)
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type='button'
|
||||
variant='tertiary'
|
||||
onClick={handleCreateKey}
|
||||
disabled={
|
||||
!newKeyName.trim() ||
|
||||
createApiKeyMutation.isPending ||
|
||||
(keyType === 'workspace' && !canManageWorkspaceKeys)
|
||||
}
|
||||
>
|
||||
{createApiKeyMutation.isPending ? 'Creating...' : 'Create'}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
{/* New API Key Dialog */}
|
||||
<Modal
|
||||
open={showNewKeyDialog}
|
||||
onOpenChange={(open: boolean) => {
|
||||
setShowNewKeyDialog(open)
|
||||
if (!open) {
|
||||
setNewKey(null)
|
||||
setCopySuccess(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ModalContent className='w-[400px]'>
|
||||
<ModalHeader>Your API key has been created</ModalHeader>
|
||||
<ModalBody>
|
||||
<p className='text-[12px] text-[var(--text-tertiary)]'>
|
||||
This is the only time you will see your API key.{' '}
|
||||
<span className='font-semibold text-[var(--text-primary)]'>
|
||||
Copy it now and store it securely.
|
||||
</span>
|
||||
</p>
|
||||
|
||||
{newKey && (
|
||||
<div className='relative mt-[10px]'>
|
||||
<div className='flex h-9 items-center rounded-[6px] border bg-[var(--surface-1)] px-[10px] pr-[40px]'>
|
||||
<code className='flex-1 truncate font-mono text-[13px] text-[var(--text-primary)]'>
|
||||
{newKey.key}
|
||||
</code>
|
||||
</div>
|
||||
<Button
|
||||
variant='ghost'
|
||||
className='-translate-y-1/2 absolute top-1/2 right-[4px] h-[28px] w-[28px] rounded-[4px] text-[var(--text-muted)] hover:text-[var(--text-primary)]'
|
||||
onClick={() => copyToClipboard(newKey.key)}
|
||||
>
|
||||
{copySuccess ? (
|
||||
<Check className='h-[14px] w-[14px]' />
|
||||
) : (
|
||||
<Copy className='h-[14px] w-[14px]' />
|
||||
)}
|
||||
<span className='sr-only'>Copy to clipboard</span>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
{/* Create API Key Modal */}
|
||||
<CreateApiKeyModal
|
||||
open={isCreateDialogOpen}
|
||||
onOpenChange={setIsCreateDialogOpen}
|
||||
workspaceId={workspaceId}
|
||||
existingKeyNames={[...workspaceKeys, ...personalKeys].map((k) => k.name)}
|
||||
allowPersonalApiKeys={allowPersonalApiKeys}
|
||||
canManageWorkspaceKeys={canManageWorkspaceKeys}
|
||||
defaultKeyType={defaultKeyType}
|
||||
/>
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<Modal open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { Check, Copy } from 'lucide-react'
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
ButtonGroupItem,
|
||||
Input as EmcnInput,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
} from '@/components/emcn'
|
||||
import { type ApiKey, useCreateApiKey } from '@/hooks/queries/api-keys'
|
||||
|
||||
const logger = createLogger('CreateApiKeyModal')
|
||||
|
||||
interface CreateApiKeyModalProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
workspaceId: string
|
||||
existingKeyNames?: string[]
|
||||
allowPersonalApiKeys?: boolean
|
||||
canManageWorkspaceKeys?: boolean
|
||||
defaultKeyType?: 'personal' | 'workspace'
|
||||
onKeyCreated?: (key: ApiKey) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable modal for creating API keys.
|
||||
* Used in both the API keys settings page and the deploy modal.
|
||||
*/
|
||||
export function CreateApiKeyModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
workspaceId,
|
||||
existingKeyNames = [],
|
||||
allowPersonalApiKeys = true,
|
||||
canManageWorkspaceKeys = false,
|
||||
defaultKeyType = 'personal',
|
||||
onKeyCreated,
|
||||
}: CreateApiKeyModalProps) {
|
||||
const [keyName, setKeyName] = useState('')
|
||||
const [keyType, setKeyType] = useState<'personal' | 'workspace'>(defaultKeyType)
|
||||
const [createError, setCreateError] = useState<string | null>(null)
|
||||
const [newKey, setNewKey] = useState<ApiKey | null>(null)
|
||||
const [showNewKeyDialog, setShowNewKeyDialog] = useState(false)
|
||||
const [copySuccess, setCopySuccess] = useState(false)
|
||||
|
||||
const createApiKeyMutation = useCreateApiKey()
|
||||
|
||||
const handleCreateKey = async () => {
|
||||
const trimmedName = keyName.trim()
|
||||
if (!trimmedName) return
|
||||
|
||||
const isDuplicate = existingKeyNames.some(
|
||||
(name) => name.toLowerCase() === trimmedName.toLowerCase()
|
||||
)
|
||||
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.`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
setCreateError(null)
|
||||
try {
|
||||
const data = await createApiKeyMutation.mutateAsync({
|
||||
workspaceId,
|
||||
name: trimmedName,
|
||||
keyType,
|
||||
})
|
||||
|
||||
setNewKey(data.key)
|
||||
setShowNewKeyDialog(true)
|
||||
setKeyName('')
|
||||
setKeyType(defaultKeyType)
|
||||
setCreateError(null)
|
||||
onOpenChange(false)
|
||||
onKeyCreated?.(data.key)
|
||||
} 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.'
|
||||
if (errorMessage.toLowerCase().includes('already exists')) {
|
||||
setCreateError(errorMessage)
|
||||
} else {
|
||||
setCreateError('Failed to create API key. Please check your connection and try again.')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
onOpenChange(false)
|
||||
setKeyName('')
|
||||
setKeyType(defaultKeyType)
|
||||
setCreateError(null)
|
||||
}
|
||||
|
||||
const copyToClipboard = (key: string) => {
|
||||
navigator.clipboard.writeText(key)
|
||||
setCopySuccess(true)
|
||||
setTimeout(() => setCopySuccess(false), 2000)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Create API Key Dialog */}
|
||||
<Modal open={open} onOpenChange={onOpenChange}>
|
||||
<ModalContent className='w-[400px]'>
|
||||
<ModalHeader>Create new API key</ModalHeader>
|
||||
<ModalBody>
|
||||
<p className='text-[12px] text-[var(--text-tertiary)]'>
|
||||
{keyType === 'workspace'
|
||||
? "This key will have access to all workflows in this workspace. Make sure to copy it after creation as you won't be able to see it again."
|
||||
: "This key will have access to your personal workflows. Make sure to copy it after creation as you won't be able to see it again."}
|
||||
</p>
|
||||
|
||||
<div className='mt-[16px] flex flex-col gap-[16px]'>
|
||||
{canManageWorkspaceKeys && (
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
<p className='font-medium text-[13px] text-[var(--text-secondary)]'>
|
||||
API Key Type
|
||||
</p>
|
||||
<ButtonGroup
|
||||
value={keyType}
|
||||
onValueChange={(value) => {
|
||||
setKeyType(value as 'personal' | 'workspace')
|
||||
if (createError) setCreateError(null)
|
||||
}}
|
||||
>
|
||||
<ButtonGroupItem value='personal' disabled={!allowPersonalApiKeys}>
|
||||
Personal
|
||||
</ButtonGroupItem>
|
||||
<ButtonGroupItem value='workspace'>Workspace</ButtonGroupItem>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
)}
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
<p className='font-medium text-[13px] text-[var(--text-secondary)]'>
|
||||
Enter a name for your API key to help you identify it later.
|
||||
</p>
|
||||
{/* Hidden decoy fields to prevent browser autofill */}
|
||||
<input
|
||||
type='text'
|
||||
name='fakeusernameremembered'
|
||||
autoComplete='username'
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: '-9999px',
|
||||
opacity: 0,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
tabIndex={-1}
|
||||
readOnly
|
||||
/>
|
||||
<EmcnInput
|
||||
value={keyName}
|
||||
onChange={(e) => {
|
||||
setKeyName(e.target.value)
|
||||
if (createError) setCreateError(null)
|
||||
}}
|
||||
placeholder='e.g., Development, Production'
|
||||
className='h-9'
|
||||
autoFocus
|
||||
name='api_key_label'
|
||||
autoComplete='off'
|
||||
autoCorrect='off'
|
||||
autoCapitalize='off'
|
||||
data-lpignore='true'
|
||||
data-form-type='other'
|
||||
/>
|
||||
{createError && (
|
||||
<p className='text-[11px] text-[var(--text-error)] leading-tight'>
|
||||
{createError}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button variant='default' onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type='button'
|
||||
variant='tertiary'
|
||||
onClick={handleCreateKey}
|
||||
disabled={
|
||||
!keyName.trim() ||
|
||||
createApiKeyMutation.isPending ||
|
||||
(keyType === 'workspace' && !canManageWorkspaceKeys)
|
||||
}
|
||||
>
|
||||
{createApiKeyMutation.isPending ? 'Creating...' : 'Create'}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
{/* New API Key Dialog - shows the created key */}
|
||||
<Modal
|
||||
open={showNewKeyDialog}
|
||||
onOpenChange={(dialogOpen: boolean) => {
|
||||
setShowNewKeyDialog(dialogOpen)
|
||||
if (!dialogOpen) {
|
||||
setNewKey(null)
|
||||
setCopySuccess(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ModalContent className='w-[400px]'>
|
||||
<ModalHeader>Your API key has been created</ModalHeader>
|
||||
<ModalBody>
|
||||
<p className='text-[12px] text-[var(--text-tertiary)]'>
|
||||
This is the only time you will see your API key.{' '}
|
||||
<span className='font-semibold text-[var(--text-primary)]'>
|
||||
Copy it now and store it securely.
|
||||
</span>
|
||||
</p>
|
||||
|
||||
{newKey && (
|
||||
<div className='relative mt-[10px]'>
|
||||
<div className='flex h-9 items-center rounded-[6px] border bg-[var(--surface-1)] px-[10px] pr-[40px]'>
|
||||
<code className='flex-1 truncate font-mono text-[13px] text-[var(--text-primary)]'>
|
||||
{newKey.key}
|
||||
</code>
|
||||
</div>
|
||||
<Button
|
||||
variant='ghost'
|
||||
className='-translate-y-1/2 absolute top-1/2 right-[4px] h-[28px] w-[28px] rounded-[4px] text-[var(--text-muted)] hover:text-[var(--text-primary)]'
|
||||
onClick={() => copyToClipboard(newKey.key)}
|
||||
>
|
||||
{copySuccess ? (
|
||||
<Check className='h-[14px] w-[14px]' />
|
||||
) : (
|
||||
<Copy className='h-[14px] w-[14px]' />
|
||||
)}
|
||||
<span className='sr-only'>Copy to clipboard</span>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { CreateApiKeyModal } from './create-api-key-modal'
|
||||
@@ -0,0 +1 @@
|
||||
export { CreateApiKeyModal } from './create-api-key-modal'
|
||||
@@ -122,7 +122,7 @@ export function CreditBalance({
|
||||
</ModalTrigger>
|
||||
<ModalContent size='sm'>
|
||||
<ModalHeader>Add Credits</ModalHeader>
|
||||
<ModalBody className='!pb-[16px]'>
|
||||
<ModalBody>
|
||||
{success ? (
|
||||
<p className='text-center text-[12px] text-[var(--text-primary)]'>
|
||||
Credits added successfully!
|
||||
|
||||
@@ -159,7 +159,7 @@ const ModalContent = React.forwardRef<
|
||||
className={cn(
|
||||
ANIMATION_CLASSES,
|
||||
CONTENT_ANIMATION_CLASSES,
|
||||
'fixed top-[50%] left-[50%] z-[500] flex max-h-[84vh] translate-x-[-50%] translate-y-[-50%] flex-col rounded-[8px] border bg-[var(--bg)] shadow-sm duration-200',
|
||||
'fixed top-[50%] left-[50%] z-[500] flex max-h-[84vh] translate-x-[-50%] translate-y-[-50%] flex-col overflow-hidden rounded-[8px] border bg-[var(--bg)] shadow-sm duration-200',
|
||||
MODAL_SIZES[size],
|
||||
className
|
||||
)}
|
||||
@@ -340,12 +340,13 @@ ModalTabsTrigger.displayName = 'ModalTabsTrigger'
|
||||
|
||||
/**
|
||||
* Modal tab content component. Content panel for each tab.
|
||||
* Includes bottom padding for consistent spacing across all tabbed modals.
|
||||
*/
|
||||
const ModalTabsContent = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Content ref={ref} className={cn('', className)} {...props} />
|
||||
<TabsPrimitive.Content ref={ref} className={cn('pb-[10px]', className)} {...props} />
|
||||
))
|
||||
|
||||
ModalTabsContent.displayName = 'ModalTabsContent'
|
||||
@@ -358,7 +359,7 @@ const ModalBody = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivE
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex-1 overflow-y-auto rounded-t-[8px] border-t bg-[var(--surface-2)] px-[14px] py-[10px]',
|
||||
'flex-1 overflow-y-auto border-t bg-[var(--surface-2)] px-[14px] py-[10px]',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -376,7 +377,7 @@ const ModalFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDi
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex justify-end gap-[8px] rounded-b-[8px] border-t bg-[var(--surface-2)] px-[16px] py-[10px]',
|
||||
'flex justify-end gap-[8px] border-t bg-[var(--surface-2)] px-[16px] py-[10px]',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
Reference in New Issue
Block a user