Skip to content

Commit fcd2e0d

Browse files
committed
update byok manager component
1 parent c223273 commit fcd2e0d

2 files changed

Lines changed: 155 additions & 74 deletions

File tree

apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok-key-manager.tsx

Lines changed: 86 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
CHIP_FIELD_SHELL,
2121
} from '@/app/workspace/[workspaceId]/components/credential-detail/components/chip-field'
2222
import { BYOKKeySkeleton } from '@/app/workspace/[workspaceId]/settings/components/byok/byok-skeleton'
23+
import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section'
2324

2425
const logger = createLogger('BYOKKeyManager')
2526

@@ -31,6 +32,16 @@ export interface BYOKManagerProvider {
3132
placeholder: string
3233
}
3334

35+
/**
36+
* Optional provider grouping. Each provider id should belong to exactly one
37+
* section; rows keep their {@link BYOKKeyManagerProps.providers} order within a
38+
* group. When omitted, providers render as a single flat list.
39+
*/
40+
export interface BYOKProviderSection {
41+
label: string
42+
ids: string[]
43+
}
44+
3445
interface BYOKKeyManagerProps {
3546
/** Providers to render, in display order. */
3647
providers: BYOKManagerProvider[]
@@ -43,7 +54,9 @@ interface BYOKKeyManagerProps {
4354
onDelete: (providerId: string) => Promise<void>
4455
isSaving?: boolean
4556
isDeleting?: boolean
46-
/** Subtitle shown above the provider list. */
57+
/** Labeled provider groups. When omitted, renders a single flat list. */
58+
sections?: BYOKProviderSection[]
59+
/** Optional subtitle shown above the provider list. */
4760
description?: string
4861
/** Show the provider search box (hidden when there are only a couple). */
4962
showSearch?: boolean
@@ -53,6 +66,9 @@ interface BYOKKeyManagerProps {
5366
* Shared BYOK key list + add/update/delete modals. Used by both the workspace
5467
* BYOK settings page and the enterprise mothership BYOK tab so the two stay
5568
* visually identical; only the provider set and the backing store differ.
69+
*
70+
* Renders content only (search, provider sections, modals) — the caller owns
71+
* the page chrome (background, scroll container, and `max-w` centering).
5672
*/
5773
export function BYOKKeyManager({
5874
providers,
@@ -62,7 +78,8 @@ export function BYOKKeyManager({
6278
onDelete,
6379
isSaving = false,
6480
isDeleting = false,
65-
description = 'Use your own API keys for hosted model providers.',
81+
sections,
82+
description,
6683
showSearch = true,
6784
}: BYOKKeyManagerProps) {
6885
const [searchTerm, setSearchTerm] = useState('')
@@ -82,7 +99,12 @@ export function BYOKKeyManager({
8299
)
83100
}, [searchTerm, providers])
84101

85-
const showNoResults = searchTerm.trim() && filteredProviders.length === 0
102+
const filteredIds = useMemo(
103+
() => new Set(filteredProviders.map((p) => p.id)),
104+
[filteredProviders]
105+
)
106+
107+
const showNoResults = searchTerm.trim() !== '' && filteredProviders.length === 0
86108
const editingMeta = providers.find((p) => p.id === editingProvider)
87109
const deleteMeta = providers.find((p) => p.id === deleteConfirmProvider)
88110

@@ -124,9 +146,41 @@ export function BYOKKeyManager({
124146
}
125147
}
126148

149+
const renderRow = (provider: BYOKManagerProvider) => {
150+
const hasKey = configuredProviderIds.has(provider.id)
151+
const Icon = provider.icon
152+
153+
return (
154+
<div key={provider.id} className='flex items-center justify-between gap-2.5'>
155+
<div className='flex min-w-0 items-center gap-2.5'>
156+
<div className='flex size-9 flex-shrink-0 items-center justify-center overflow-hidden rounded-xl border border-[var(--border-1)] bg-[var(--bg)]'>
157+
<Icon className='size-5' />
158+
</div>
159+
<div className='flex min-w-0 flex-col justify-center gap-[1px]'>
160+
<span className='truncate text-[14px] text-[var(--text-body)]'>{provider.name}</span>
161+
<span className='truncate text-[12px] text-[var(--text-muted)]'>
162+
{provider.description}
163+
</span>
164+
</div>
165+
</div>
166+
167+
{hasKey ? (
168+
<div className='flex flex-shrink-0 items-center gap-2'>
169+
<Chip onClick={() => openEditModal(provider.id)}>Update</Chip>
170+
<Chip onClick={() => setDeleteConfirmProvider(provider.id)}>Delete</Chip>
171+
</div>
172+
) : (
173+
<Chip variant='primary' onClick={() => openEditModal(provider.id)}>
174+
Add Key
175+
</Chip>
176+
)}
177+
</div>
178+
)
179+
}
180+
127181
return (
128182
<>
129-
<div className='flex h-full flex-col gap-4.5'>
183+
<div className='flex flex-col gap-4.5'>
130184
{showSearch && (
131185
<div className={CHIP_FIELD_SHELL}>
132186
<Search
@@ -143,56 +197,36 @@ export function BYOKKeyManager({
143197
</div>
144198
)}
145199

146-
<p className='text-[var(--text-secondary)] text-sm'>{description}</p>
200+
{description && <p className='text-[var(--text-secondary)] text-sm'>{description}</p>}
147201

148-
<div className='min-h-0 flex-1 overflow-y-auto'>
149-
{isLoading ? (
150-
<div className='flex flex-col gap-2'>
151-
{providers.map((p) => (
152-
<BYOKKeySkeleton key={p.id} />
153-
))}
154-
</div>
155-
) : (
156-
<div className='flex flex-col gap-2'>
157-
{filteredProviders.map((provider) => {
158-
const hasKey = configuredProviderIds.has(provider.id)
159-
const Icon = provider.icon
160-
161-
return (
162-
<div key={provider.id} className='flex items-center justify-between gap-3'>
163-
<div className='flex items-center gap-3'>
164-
<div className='flex size-9 flex-shrink-0 items-center justify-center overflow-hidden rounded-md bg-[var(--surface-6)]'>
165-
<Icon className='size-4' />
166-
</div>
167-
<div className='flex min-w-0 flex-col justify-center gap-[1px]'>
168-
<span className='font-medium text-base'>{provider.name}</span>
169-
<p className='truncate text-[var(--text-muted)] text-sm'>
170-
{provider.description}
171-
</p>
172-
</div>
173-
</div>
202+
{isLoading ? (
203+
<div className='flex flex-col gap-2'>
204+
{providers.map((p) => (
205+
<BYOKKeySkeleton key={p.id} />
206+
))}
207+
</div>
208+
) : showNoResults ? (
209+
<div className='py-4 text-center text-[var(--text-muted)] text-sm'>
210+
No providers found matching "{searchTerm}"
211+
</div>
212+
) : sections ? (
213+
<div className='flex flex-col gap-7'>
214+
{sections.map((section) => {
215+
const rows = providers.filter(
216+
(p) => section.ids.includes(p.id) && filteredIds.has(p.id)
217+
)
218+
if (rows.length === 0) return null
174219

175-
{hasKey ? (
176-
<div className='flex flex-shrink-0 items-center gap-2'>
177-
<Chip onClick={() => openEditModal(provider.id)}>Update</Chip>
178-
<Chip onClick={() => setDeleteConfirmProvider(provider.id)}>Delete</Chip>
179-
</div>
180-
) : (
181-
<Chip variant='primary' onClick={() => openEditModal(provider.id)}>
182-
Add Key
183-
</Chip>
184-
)}
185-
</div>
186-
)
187-
})}
188-
{showNoResults && (
189-
<div className='py-4 text-center text-[var(--text-muted)] text-sm'>
190-
No providers found matching "{searchTerm}"
191-
</div>
192-
)}
193-
</div>
194-
)}
195-
</div>
220+
return (
221+
<SettingsSection key={section.label} label={section.label}>
222+
<div className='flex flex-col gap-2'>{rows.map(renderRow)}</div>
223+
</SettingsSection>
224+
)
225+
})}
226+
</div>
227+
) : (
228+
<div className='flex flex-col gap-2'>{filteredProviders.map(renderRow)}</div>
229+
)}
196230
</div>
197231

198232
<ChipModal

apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok.tsx

Lines changed: 69 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ import {
77
BasetenIcon,
88
BrandfetchIcon,
99
ExaAIIcon,
10+
FalIcon,
1011
FindymailIcon,
1112
FirecrawlIcon,
1213
FireworksIcon,
1314
GeminiIcon,
1415
GoogleIcon,
1516
HunterIOIcon,
16-
ImageIcon,
1717
JinaAIIcon,
1818
LinkupIcon,
1919
MistralIcon,
@@ -30,6 +30,7 @@ import {
3030
import {
3131
BYOKKeyManager,
3232
type BYOKManagerProvider,
33+
type BYOKProviderSection,
3334
} from '@/app/workspace/[workspaceId]/settings/components/byok/byok-key-manager'
3435
import { useBYOKKeys, useDeleteBYOKKey, useUpsertBYOKKey } from '@/hooks/queries/byok-keys'
3536
import type { BYOKProviderId } from '@/tools/types'
@@ -94,7 +95,7 @@ const PROVIDERS: (BYOKManagerProvider & { id: BYOKProviderId })[] = [
9495
{
9596
id: 'falai',
9697
name: 'Fal.ai',
97-
icon: ImageIcon,
98+
icon: FalIcon,
9899
description: 'Image and video generation',
99100
placeholder: 'Enter your Fal.ai API key',
100101
},
@@ -198,6 +199,45 @@ const PROVIDERS: (BYOKManagerProvider & { id: BYOKProviderId })[] = [
198199
},
199200
]
200201

202+
/**
203+
* Provider groupings rendered as labeled sections. Every provider id in
204+
* {@link PROVIDERS} belongs to exactly one section; rows keep their
205+
* {@link PROVIDERS} order within each group.
206+
*/
207+
const PROVIDER_SECTIONS: BYOKProviderSection[] = [
208+
{
209+
label: 'Models',
210+
ids: [
211+
'openai',
212+
'anthropic',
213+
'google',
214+
'mistral',
215+
'fireworks',
216+
'together',
217+
'baseten',
218+
'ollama-cloud',
219+
'falai',
220+
],
221+
},
222+
{
223+
label: 'Search & web',
224+
ids: [
225+
'firecrawl',
226+
'exa',
227+
'serper',
228+
'linkup',
229+
'parallel_ai',
230+
'perplexity',
231+
'jina',
232+
'google_cloud',
233+
],
234+
},
235+
{
236+
label: 'Enrichment',
237+
ids: ['brandfetch', 'hunter', 'peopledatalabs', 'findymail', 'prospeo', 'wiza'],
238+
},
239+
]
240+
201241
export function BYOK() {
202242
const params = useParams()
203243
const workspaceId = (params?.workspaceId as string) || ''
@@ -210,25 +250,32 @@ export function BYOK() {
210250
const configuredProviderIds = useMemo(() => new Set(keys.map((k) => k.providerId)), [keys])
211251

212252
return (
213-
<BYOKKeyManager
214-
providers={PROVIDERS}
215-
configuredProviderIds={configuredProviderIds}
216-
isLoading={isLoading}
217-
isSaving={upsertKey.isPending}
218-
isDeleting={deleteKey.isPending}
219-
onSave={async (providerId, apiKey) => {
220-
await upsertKey.mutateAsync({
221-
workspaceId,
222-
providerId: providerId as BYOKProviderId,
223-
apiKey,
224-
})
225-
}}
226-
onDelete={async (providerId) => {
227-
await deleteKey.mutateAsync({
228-
workspaceId,
229-
providerId: providerId as BYOKProviderId,
230-
})
231-
}}
232-
/>
253+
<div className='flex h-full flex-col bg-[var(--bg)]'>
254+
<div className='min-h-0 flex-1 overflow-y-auto px-6 [scrollbar-gutter:stable_both-edges]'>
255+
<div className='mx-auto flex max-w-[48rem] flex-col pt-6 pb-6'>
256+
<BYOKKeyManager
257+
providers={PROVIDERS}
258+
sections={PROVIDER_SECTIONS}
259+
configuredProviderIds={configuredProviderIds}
260+
isLoading={isLoading}
261+
isSaving={upsertKey.isPending}
262+
isDeleting={deleteKey.isPending}
263+
onSave={async (providerId, apiKey) => {
264+
await upsertKey.mutateAsync({
265+
workspaceId,
266+
providerId: providerId as BYOKProviderId,
267+
apiKey,
268+
})
269+
}}
270+
onDelete={async (providerId) => {
271+
await deleteKey.mutateAsync({
272+
workspaceId,
273+
providerId: providerId as BYOKProviderId,
274+
})
275+
}}
276+
/>
277+
</div>
278+
</div>
279+
</div>
233280
)
234281
}

0 commit comments

Comments
 (0)