33import { useState } from 'react'
44import { createLogger } from '@sim/logger'
55import { toError } from '@sim/utils/errors'
6+ import { ChevronDown , Plus , Search } from 'lucide-react'
67import {
78 Badge ,
89 Button ,
@@ -19,7 +20,6 @@ import {
1920 ModalContent ,
2021 ModalFooter ,
2122 ModalHeader ,
22- ModalTitle ,
2323 MoreHorizontal ,
2424 Switch ,
2525 Table ,
@@ -30,6 +30,8 @@ import {
3030 TableRow ,
3131 toast ,
3232} from '@/components/emcn'
33+ import { S3Icon } from '@/components/icons'
34+ import { Input as BaseInput } from '@/components/ui'
3335import type { CreateDataDrainBody , DataDrain , DataDrainRun } from '@/lib/api/contracts/data-drains'
3436import { useSession } from '@/lib/auth/auth-client'
3537import { cn } from '@/lib/core/utils/cn'
@@ -70,9 +72,15 @@ const CADENCE_LABELS: Record<(typeof CADENCE_TYPES)[number], string> = {
7072
7173const SOURCE_OPTIONS = SOURCE_TYPES . map ( ( t ) => ( { value : t , label : SOURCE_LABELS [ t ] } ) )
7274const CADENCE_OPTIONS = CADENCE_TYPES . map ( ( t ) => ( { value : t , label : CADENCE_LABELS [ t ] } ) )
75+ function getDestinationIcon ( type : ( typeof DESTINATION_TYPES ) [ number ] ) {
76+ if ( type !== 's3' ) return null
77+ return < S3Icon className = 'size-[14px] flex-shrink-0 text-[#1B660F]' />
78+ }
79+
7380const DESTINATION_OPTIONS = DESTINATION_TYPES . map ( ( t ) => ( {
7481 value : t ,
7582 label : DESTINATION_LABELS [ t ] ,
83+ iconElement : getDestinationIcon ( t ) ,
7684} ) )
7785
7886export function DataDrainsSettings ( ) {
@@ -89,6 +97,19 @@ export function DataDrainsSettings() {
8997
9098 const [ createOpen , setCreateOpen ] = useState ( false )
9199 const [ expandedDrainId , setExpandedDrainId ] = useState < string | null > ( null )
100+ const [ searchTerm , setSearchTerm ] = useState ( '' )
101+
102+ const query = searchTerm . trim ( ) . toLowerCase ( )
103+ const filteredDrains = ! query
104+ ? ( drains ?? [ ] )
105+ : ( drains ?? [ ] ) . filter ( ( drain ) =>
106+ [
107+ drain . name ,
108+ SOURCE_LABELS [ drain . source ] ,
109+ DESTINATION_LABELS [ drain . destinationType ] ,
110+ CADENCE_LABELS [ drain . scheduleCadence ] ,
111+ ] . some ( ( value ) => value . toLowerCase ( ) . includes ( query ) )
112+ )
92113
93114 if ( sessionPending || orgsLoading || drainsLoading ) {
94115 return < DataDrainsSkeleton />
@@ -111,61 +132,75 @@ export function DataDrainsSettings() {
111132 }
112133
113134 return (
114- < div className = 'flex flex-col gap-6 ' >
135+ < div className = 'flex h-full flex-col gap-4.5 ' >
115136 < Callout >
116137 Drains continuously export Sim data to your own storage on a schedule. Combine with Data
117138 Retention to satisfy long-term compliance archives.
118139 </ Callout >
119140
120- < div className = 'flex items-center justify-between' >
121- < div className = 'text-[13px] text-[var(--text-muted)]' >
122- { drains ?. length ?? 0 } drain{ ( drains ?. length ?? 0 ) === 1 ? '' : 's' }
141+ < div className = 'flex items-center gap-2' >
142+ < div className = 'flex flex-1 items-center gap-2 rounded-lg border border-[var(--border)] bg-transparent px-2 py-1.5 transition-colors duration-100 dark:bg-[var(--surface-4)] dark:hover-hover:border-[var(--border-1)] dark:hover-hover:bg-[var(--surface-5)]' >
143+ < Search
144+ className = 'size-[14px] flex-shrink-0 text-[var(--text-tertiary)]'
145+ strokeWidth = { 2 }
146+ />
147+ < BaseInput
148+ placeholder = 'Search data drains...'
149+ value = { searchTerm }
150+ onChange = { ( e ) => setSearchTerm ( e . target . value ) }
151+ 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'
152+ />
123153 </ div >
124154 < Button variant = 'primary' onClick = { ( ) => setCreateOpen ( true ) } >
155+ < Plus className = 'mr-1.5 size-[13px]' />
125156 New drain
126157 </ Button >
127158 </ div >
128159
129- { drainsError ? (
130- < Callout variant = 'destructive' >
131- Failed to load data drains: { toError ( drainsError ) . message }
132- </ Callout >
133- ) : drains && drains . length > 0 ? (
134- < Table >
135- < TableHeader >
136- < TableRow >
137- < TableHead > Name</ TableHead >
138- < TableHead > Source</ TableHead >
139- < TableHead > Destination</ TableHead >
140- < TableHead > Cadence</ TableHead >
141- < TableHead > Last run</ TableHead >
142- < TableHead > Enabled</ TableHead >
143- < TableHead className = 'w-[40px]' />
144- </ TableRow >
145- </ TableHeader >
146- < TableBody >
147- { drains . map ( ( drain ) => (
148- < DrainRow
149- key = { drain . id }
150- drain = { drain }
151- organizationId = { orgId }
152- expanded = { expandedDrainId === drain . id }
153- onToggleExpand = { ( ) =>
154- setExpandedDrainId ( expandedDrainId === drain . id ? null : drain . id )
155- }
156- />
157- ) ) }
158- </ TableBody >
159- </ Table >
160- ) : (
161- < div className = 'flex flex-col items-center justify-center gap-3 rounded-lg border border-[var(--border)] border-dashed py-12 text-center' >
162- < div className = 'text-[14px] text-[var(--text-primary)]' > No drains yet</ div >
163- < div className = 'max-w-[400px] text-[13px] text-[var(--text-muted)]' >
164- Create a drain to start exporting workflow logs, audit events, and copilot data to S3 or
165- your own webhook.
160+ < div className = 'min-h-0 flex-1 overflow-y-auto' >
161+ { drainsError ? (
162+ < Callout variant = 'destructive' >
163+ Failed to load data drains: { toError ( drainsError ) . message }
164+ </ Callout >
165+ ) : drains && drains . length > 0 ? (
166+ filteredDrains . length > 0 ? (
167+ < Table >
168+ < TableHeader >
169+ < TableRow >
170+ < TableHead > Name</ TableHead >
171+ < TableHead > Source</ TableHead >
172+ < TableHead > Destination</ TableHead >
173+ < TableHead > Cadence</ TableHead >
174+ < TableHead > Last run</ TableHead >
175+ < TableHead > Enabled</ TableHead >
176+ < TableHead className = 'w-[40px]' />
177+ </ TableRow >
178+ </ TableHeader >
179+ < TableBody >
180+ { filteredDrains . map ( ( drain ) => (
181+ < DrainRow
182+ key = { drain . id }
183+ drain = { drain }
184+ organizationId = { orgId }
185+ expanded = { expandedDrainId === drain . id }
186+ onToggleExpand = { ( ) =>
187+ setExpandedDrainId ( expandedDrainId === drain . id ? null : drain . id )
188+ }
189+ />
190+ ) ) }
191+ </ TableBody >
192+ </ Table >
193+ ) : (
194+ < div className = 'flex h-full items-center justify-center py-12 text-[var(--text-muted)] text-sm' >
195+ No results for "{ searchTerm . trim ( ) } "
196+ </ div >
197+ )
198+ ) : (
199+ < div className = 'flex h-full items-center justify-center py-12 text-[var(--text-muted)] text-sm' >
200+ Click "New drain" above to get started
166201 </ div >
167- </ div >
168- ) }
202+ ) }
203+ </ div >
169204
170205 { createOpen && (
171206 < CreateDrainModal organizationId = { orgId } onClose = { ( ) => setCreateOpen ( false ) } />
@@ -231,15 +266,25 @@ function DrainRow({ drain, organizationId, expanded, onToggleExpand }: DrainRowP
231266 return (
232267 < >
233268 < TableRow className = 'cursor-pointer' onClick = { onToggleExpand } >
234- < TableCell className = 'font-medium' > { drain . name } </ TableCell >
269+ < TableCell className = 'font-medium' >
270+ < div className = 'flex items-center gap-1.5' >
271+ < ChevronDown
272+ className = { cn (
273+ 'size-[14px] flex-shrink-0 text-[var(--text-muted)] transition-transform duration-200' ,
274+ expanded && 'rotate-180'
275+ ) }
276+ />
277+ < span > { drain . name } </ span >
278+ </ div >
279+ </ TableCell >
235280 < TableCell >
236281 < Badge > { SOURCE_LABELS [ drain . source ] } </ Badge >
237282 </ TableCell >
238283 < TableCell >
239284 < Badge > { DESTINATION_LABELS [ drain . destinationType ] } </ Badge >
240285 </ TableCell >
241286 < TableCell > { CADENCE_LABELS [ drain . scheduleCadence ] } </ TableCell >
242- < TableCell className = 'text-[13px] text-[var(--text-muted)]' suppressHydrationWarning >
287+ < TableCell className = 'text-[13px] text-[var(--text-muted)]' >
243288 { drain . lastRunAt ? new Date ( drain . lastRunAt ) . toLocaleString ( ) : 'Never' }
244289 </ TableCell >
245290 < TableCell onClick = { ( e ) => e . stopPropagation ( ) } >
@@ -288,7 +333,7 @@ function DrainRunsPanel({ organizationId, drainId }: DrainRunsPanelProps) {
288333 const { data : runs , isLoading } = useDataDrainRuns ( organizationId , drainId , 10 )
289334
290335 if ( isLoading ) {
291- return < div className = 'text-[13px] text-[var(--text-muted)]' > Loading runs… </ div >
336+ return < div className = 'text-[13px] text-[var(--text-muted)]' > Loading runs... </ div >
292337 }
293338 if ( ! runs || runs . length === 0 ) {
294339 return < div className = 'text-[13px] text-[var(--text-muted)]' > No runs yet.</ div >
@@ -317,7 +362,7 @@ function RunRow({ run }: { run: DataDrainRun }) {
317362 < div className = 'flex items-center gap-2' >
318363 < span className = { cn ( 'font-medium' , statusColor ) } > { run . status } </ span >
319364 < span className = 'text-[var(--text-muted)]' > { run . trigger } </ span >
320- < span className = 'text-[var(--text-muted)]' suppressHydrationWarning >
365+ < span className = 'text-[var(--text-muted)]' >
321366 { new Date ( run . startedAt ) . toLocaleString ( ) }
322367 </ span >
323368 </ div >
@@ -378,47 +423,57 @@ function CreateDrainModal({ organizationId, onClose }: CreateDrainModalProps) {
378423
379424 return (
380425 < Modal open onOpenChange = { ( open ) => ! open && onClose ( ) } >
381- < ModalContent className = 'max-w-[560px]' >
382- < ModalHeader >
383- < ModalTitle > New data drain</ ModalTitle >
384- </ ModalHeader >
385- < ModalBody className = 'flex flex-col gap-4' >
386- < FormField label = 'Name' >
387- < Input
388- value = { name }
389- onChange = { ( e ) => setName ( e . target . value ) }
390- placeholder = 'Workflow logs to S3'
391- />
392- </ FormField >
393- < FormField label = 'Source' >
394- < Combobox
395- value = { source }
396- onChange = { ( v ) => setSource ( v as ( typeof SOURCE_TYPES ) [ number ] ) }
397- options = { SOURCE_OPTIONS }
398- dropdownWidth = 'trigger'
399- />
400- </ FormField >
401- < FormField label = 'Cadence' >
402- < Combobox
403- value = { cadence }
404- onChange = { ( v ) => setCadence ( v as ( typeof CADENCE_TYPES ) [ number ] ) }
405- options = { CADENCE_OPTIONS }
406- dropdownWidth = 'trigger'
407- />
408- </ FormField >
409- < FormField label = 'Destination' >
410- < Combobox
411- value = { destinationType }
412- onChange = { ( v ) => handleDestinationChange ( v as ( typeof DESTINATION_TYPES ) [ number ] ) }
413- options = { DESTINATION_OPTIONS }
414- dropdownWidth = 'trigger'
415- />
416- </ FormField >
426+ < ModalContent size = 'md' className = 'max-h-[76vh]' >
427+ < ModalHeader > New data drain</ ModalHeader >
428+ < ModalBody className = 'flex min-h-0 flex-1 flex-col gap-3' >
429+ < section className = 'flex flex-col gap-3' >
430+ < FormField label = 'Name' >
431+ < Input
432+ value = { name }
433+ onChange = { ( e ) => setName ( e . target . value ) }
434+ placeholder = 'Workflow logs export'
435+ />
436+ </ FormField >
437+ < FormField label = 'Source' >
438+ < Combobox
439+ value = { source }
440+ onChange = { ( v ) => setSource ( v as ( typeof SOURCE_TYPES ) [ number ] ) }
441+ options = { SOURCE_OPTIONS }
442+ dropdownWidth = 'trigger'
443+ />
444+ </ FormField >
445+ < FormField label = 'Cadence' >
446+ < Combobox
447+ value = { cadence }
448+ onChange = { ( v ) => setCadence ( v as ( typeof CADENCE_TYPES ) [ number ] ) }
449+ options = { CADENCE_OPTIONS }
450+ dropdownWidth = 'trigger'
451+ />
452+ </ FormField >
453+ < FormField label = 'Destination' >
454+ < Combobox
455+ value = { destinationType }
456+ onChange = { ( v ) => handleDestinationChange ( v as ( typeof DESTINATION_TYPES ) [ number ] ) }
457+ options = { DESTINATION_OPTIONS }
458+ dropdownWidth = 'trigger'
459+ overlayContent = {
460+ < div className = 'flex items-center gap-2' >
461+ { getDestinationIcon ( destinationType ) }
462+ < span className = 'truncate text-[var(--text-primary)]' >
463+ { DESTINATION_LABELS [ destinationType ] }
464+ </ span >
465+ </ div >
466+ }
467+ />
468+ </ FormField >
469+ </ section >
417470
418- < spec . FormFields state = { destState } setState = { setDestState } />
471+ < section className = 'flex flex-col gap-3' >
472+ < spec . FormFields state = { destState } setState = { setDestState } />
473+ </ section >
419474 </ ModalBody >
420475 < ModalFooter >
421- < Button variant = 'secondary ' onClick = { onClose } >
476+ < Button variant = 'default ' onClick = { onClose } >
422477 Cancel
423478 </ Button >
424479 < Button
0 commit comments