Skip to content

Commit efa1e7d

Browse files
committed
improvement(data-drains): docs page, screenshots, and search/UI polish
Adds the enterprise data-drains docs page with two screenshots, a search bar over the drains table, and a UI cleanup pass on the settings component (size-*, useMemo removal, text-sm fixes). The previously-proposed env-var reference feature has been dropped — drain credentials remain raw values, encrypted at rest by the existing pipeline.
1 parent 10f7d36 commit efa1e7d

5 files changed

Lines changed: 150 additions & 90 deletions

File tree

apps/docs/content/docs/en/enterprise/data-drains.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ Drains are independent of [Data Retention](/enterprise/data-retention) but desig
1515

1616
Go to **Settings → Enterprise → Data Drains** in your workspace, then click **New drain**.
1717

18+
![Data Drains settings page showing two configured drains — one exporting workflow logs to Amazon S3 daily, another exporting Copilot chats to an HTTPS webhook hourly](/static/enterprise/data-drains-list.png)
19+
20+
![New data drain dialog with fields for name, source, cadence, destination, and S3 credentials](/static/enterprise/data-drains-new.png)
21+
1822
Each drain has four pieces:
1923

2024
1. A **source** — the category of data to export
51.9 KB
Loading
45.2 KB
Loading

apps/sim/ee/data-drains/components/data-drains-settings.tsx

Lines changed: 141 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { useState } from 'react'
44
import { createLogger } from '@sim/logger'
55
import { toError } from '@sim/utils/errors'
6+
import { ChevronDown, Plus, Search } from 'lucide-react'
67
import {
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'
3335
import type { CreateDataDrainBody, DataDrain, DataDrainRun } from '@/lib/api/contracts/data-drains'
3436
import { useSession } from '@/lib/auth/auth-client'
3537
import { cn } from '@/lib/core/utils/cn'
@@ -70,9 +72,15 @@ const CADENCE_LABELS: Record<(typeof CADENCE_TYPES)[number], string> = {
7072

7173
const SOURCE_OPTIONS = SOURCE_TYPES.map((t) => ({ value: t, label: SOURCE_LABELS[t] }))
7274
const 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+
7380
const DESTINATION_OPTIONS = DESTINATION_TYPES.map((t) => ({
7481
value: t,
7582
label: DESTINATION_LABELS[t],
83+
iconElement: getDestinationIcon(t),
7684
}))
7785

7886
export 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

apps/sim/ee/data-drains/components/data-drains-skeleton.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ import { Skeleton } from '@/components/emcn'
22

33
export function DataDrainsSkeleton() {
44
return (
5-
<div className='flex flex-col gap-8'>
6-
<div className='flex items-center justify-between'>
7-
<Skeleton className='h-[18px] w-[200px]' />
5+
<div className='flex h-full flex-col gap-4.5'>
6+
<Skeleton className='h-[34px] w-full rounded-lg' />
7+
<div className='flex items-center gap-2'>
8+
<Skeleton className='h-[34px] flex-1 rounded-lg' />
89
<Skeleton className='h-[34px] w-[110px] rounded-lg' />
910
</div>
10-
<div className='flex flex-col gap-3'>
11+
<div className='flex min-h-0 flex-1 flex-col gap-3'>
1112
{Array.from({ length: 3 }).map((_, i) => (
1213
<Skeleton key={i} className='h-[64px] w-full rounded-lg' />
1314
))}

0 commit comments

Comments
 (0)