improvement(data-drains): docs page, screenshots, and search/UI polish (#4547)

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.
This commit is contained in:
Waleed
2026-05-10 13:43:25 -07:00
committed by GitHub
parent 10f7d36f13
commit ae680afab2
5 changed files with 148 additions and 88 deletions
@@ -15,6 +15,10 @@ Drains are independent of [Data Retention](/enterprise/data-retention) but desig
Go to **Settings → Enterprise → Data Drains** in your workspace, then click **New drain**.
![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)
![New data drain dialog with fields for name, source, cadence, destination, and S3 credentials](/static/enterprise/data-drains-new.png)
Each drain has four pieces:
1. A **source** — the category of data to export
Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

@@ -3,6 +3,7 @@
import { useState } from 'react'
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { ChevronDown, Plus, Search } from 'lucide-react'
import {
Badge,
Button,
@@ -19,7 +20,6 @@ import {
ModalContent,
ModalFooter,
ModalHeader,
ModalTitle,
MoreHorizontal,
Switch,
Table,
@@ -30,6 +30,8 @@ import {
TableRow,
toast,
} from '@/components/emcn'
import { S3Icon } from '@/components/icons'
import { Input as BaseInput } from '@/components/ui'
import type { CreateDataDrainBody, DataDrain, DataDrainRun } from '@/lib/api/contracts/data-drains'
import { useSession } from '@/lib/auth/auth-client'
import { cn } from '@/lib/core/utils/cn'
@@ -70,9 +72,15 @@ const CADENCE_LABELS: Record<(typeof CADENCE_TYPES)[number], string> = {
const SOURCE_OPTIONS = SOURCE_TYPES.map((t) => ({ value: t, label: SOURCE_LABELS[t] }))
const CADENCE_OPTIONS = CADENCE_TYPES.map((t) => ({ value: t, label: CADENCE_LABELS[t] }))
function getDestinationIcon(type: (typeof DESTINATION_TYPES)[number]) {
if (type !== 's3') return null
return <S3Icon className='size-[14px] flex-shrink-0 text-[#1B660F]' />
}
const DESTINATION_OPTIONS = DESTINATION_TYPES.map((t) => ({
value: t,
label: DESTINATION_LABELS[t],
iconElement: getDestinationIcon(t),
}))
export function DataDrainsSettings() {
@@ -89,6 +97,19 @@ export function DataDrainsSettings() {
const [createOpen, setCreateOpen] = useState(false)
const [expandedDrainId, setExpandedDrainId] = useState<string | null>(null)
const [searchTerm, setSearchTerm] = useState('')
const query = searchTerm.trim().toLowerCase()
const filteredDrains = !query
? (drains ?? [])
: (drains ?? []).filter((drain) =>
[
drain.name,
SOURCE_LABELS[drain.source],
DESTINATION_LABELS[drain.destinationType],
CADENCE_LABELS[drain.scheduleCadence],
].some((value) => value.toLowerCase().includes(query))
)
if (sessionPending || orgsLoading || drainsLoading) {
return <DataDrainsSkeleton />
@@ -111,61 +132,75 @@ export function DataDrainsSettings() {
}
return (
<div className='flex flex-col gap-6'>
<div className='flex h-full flex-col gap-4.5'>
<Callout>
Drains continuously export Sim data to your own storage on a schedule. Combine with Data
Retention to satisfy long-term compliance archives.
</Callout>
<div className='flex items-center justify-between'>
<div className='text-[13px] text-[var(--text-muted)]'>
{drains?.length ?? 0} drain{(drains?.length ?? 0) === 1 ? '' : 's'}
<div className='flex items-center gap-2'>
<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)]'>
<Search
className='size-[14px] flex-shrink-0 text-[var(--text-tertiary)]'
strokeWidth={2}
/>
<BaseInput
placeholder='Search data drains...'
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
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'
/>
</div>
<Button variant='primary' onClick={() => setCreateOpen(true)}>
<Plus className='mr-1.5 size-[13px]' />
New drain
</Button>
</div>
{drainsError ? (
<Callout variant='destructive'>
Failed to load data drains: {toError(drainsError).message}
</Callout>
) : drains && drains.length > 0 ? (
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Source</TableHead>
<TableHead>Destination</TableHead>
<TableHead>Cadence</TableHead>
<TableHead>Last run</TableHead>
<TableHead>Enabled</TableHead>
<TableHead className='w-[40px]' />
</TableRow>
</TableHeader>
<TableBody>
{drains.map((drain) => (
<DrainRow
key={drain.id}
drain={drain}
organizationId={orgId}
expanded={expandedDrainId === drain.id}
onToggleExpand={() =>
setExpandedDrainId(expandedDrainId === drain.id ? null : drain.id)
}
/>
))}
</TableBody>
</Table>
) : (
<div className='flex flex-col items-center justify-center gap-3 rounded-lg border border-[var(--border)] border-dashed py-12 text-center'>
<div className='text-[14px] text-[var(--text-primary)]'>No drains yet</div>
<div className='max-w-[400px] text-[13px] text-[var(--text-muted)]'>
Create a drain to start exporting workflow logs, audit events, and copilot data to S3 or
your own webhook.
<div className='min-h-0 flex-1 overflow-y-auto'>
{drainsError ? (
<Callout variant='destructive'>
Failed to load data drains: {toError(drainsError).message}
</Callout>
) : drains && drains.length > 0 ? (
filteredDrains.length > 0 ? (
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Source</TableHead>
<TableHead>Destination</TableHead>
<TableHead>Cadence</TableHead>
<TableHead>Last run</TableHead>
<TableHead>Enabled</TableHead>
<TableHead className='w-[40px]' />
</TableRow>
</TableHeader>
<TableBody>
{filteredDrains.map((drain) => (
<DrainRow
key={drain.id}
drain={drain}
organizationId={orgId}
expanded={expandedDrainId === drain.id}
onToggleExpand={() =>
setExpandedDrainId(expandedDrainId === drain.id ? null : drain.id)
}
/>
))}
</TableBody>
</Table>
) : (
<div className='flex h-full items-center justify-center py-12 text-[var(--text-muted)] text-sm'>
No results for "{searchTerm.trim()}"
</div>
)
) : (
<div className='flex h-full items-center justify-center py-12 text-[var(--text-muted)] text-sm'>
Click "New drain" above to get started
</div>
</div>
)}
)}
</div>
{createOpen && (
<CreateDrainModal organizationId={orgId} onClose={() => setCreateOpen(false)} />
@@ -231,7 +266,17 @@ function DrainRow({ drain, organizationId, expanded, onToggleExpand }: DrainRowP
return (
<>
<TableRow className='cursor-pointer' onClick={onToggleExpand}>
<TableCell className='font-medium'>{drain.name}</TableCell>
<TableCell className='font-medium'>
<div className='flex items-center gap-1.5'>
<ChevronDown
className={cn(
'size-[14px] flex-shrink-0 text-[var(--text-muted)] transition-transform duration-200',
expanded && 'rotate-180'
)}
/>
<span>{drain.name}</span>
</div>
</TableCell>
<TableCell>
<Badge>{SOURCE_LABELS[drain.source]}</Badge>
</TableCell>
@@ -288,7 +333,7 @@ function DrainRunsPanel({ organizationId, drainId }: DrainRunsPanelProps) {
const { data: runs, isLoading } = useDataDrainRuns(organizationId, drainId, 10)
if (isLoading) {
return <div className='text-[13px] text-[var(--text-muted)]'>Loading runs</div>
return <div className='text-[13px] text-[var(--text-muted)]'>Loading runs...</div>
}
if (!runs || runs.length === 0) {
return <div className='text-[13px] text-[var(--text-muted)]'>No runs yet.</div>
@@ -378,47 +423,57 @@ function CreateDrainModal({ organizationId, onClose }: CreateDrainModalProps) {
return (
<Modal open onOpenChange={(open) => !open && onClose()}>
<ModalContent className='max-w-[560px]'>
<ModalHeader>
<ModalTitle>New data drain</ModalTitle>
</ModalHeader>
<ModalBody className='flex flex-col gap-4'>
<FormField label='Name'>
<Input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder='Workflow logs to S3'
/>
</FormField>
<FormField label='Source'>
<Combobox
value={source}
onChange={(v) => setSource(v as (typeof SOURCE_TYPES)[number])}
options={SOURCE_OPTIONS}
dropdownWidth='trigger'
/>
</FormField>
<FormField label='Cadence'>
<Combobox
value={cadence}
onChange={(v) => setCadence(v as (typeof CADENCE_TYPES)[number])}
options={CADENCE_OPTIONS}
dropdownWidth='trigger'
/>
</FormField>
<FormField label='Destination'>
<Combobox
value={destinationType}
onChange={(v) => handleDestinationChange(v as (typeof DESTINATION_TYPES)[number])}
options={DESTINATION_OPTIONS}
dropdownWidth='trigger'
/>
</FormField>
<ModalContent size='md' className='max-h-[76vh]'>
<ModalHeader>New data drain</ModalHeader>
<ModalBody className='flex min-h-0 flex-1 flex-col gap-3'>
<section className='flex flex-col gap-3'>
<FormField label='Name'>
<Input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder='Workflow logs export'
/>
</FormField>
<FormField label='Source'>
<Combobox
value={source}
onChange={(v) => setSource(v as (typeof SOURCE_TYPES)[number])}
options={SOURCE_OPTIONS}
dropdownWidth='trigger'
/>
</FormField>
<FormField label='Cadence'>
<Combobox
value={cadence}
onChange={(v) => setCadence(v as (typeof CADENCE_TYPES)[number])}
options={CADENCE_OPTIONS}
dropdownWidth='trigger'
/>
</FormField>
<FormField label='Destination'>
<Combobox
value={destinationType}
onChange={(v) => handleDestinationChange(v as (typeof DESTINATION_TYPES)[number])}
options={DESTINATION_OPTIONS}
dropdownWidth='trigger'
overlayContent={
<div className='flex items-center gap-2'>
{getDestinationIcon(destinationType)}
<span className='truncate text-[var(--text-primary)]'>
{DESTINATION_LABELS[destinationType]}
</span>
</div>
}
/>
</FormField>
</section>
<spec.FormFields state={destState} setState={setDestState} />
<section className='flex flex-col gap-3'>
<spec.FormFields state={destState} setState={setDestState} />
</section>
</ModalBody>
<ModalFooter>
<Button variant='secondary' onClick={onClose}>
<Button variant='default' onClick={onClose}>
Cancel
</Button>
<Button
@@ -2,12 +2,13 @@ import { Skeleton } from '@/components/emcn'
export function DataDrainsSkeleton() {
return (
<div className='flex flex-col gap-8'>
<div className='flex items-center justify-between'>
<Skeleton className='h-[18px] w-[200px]' />
<div className='flex h-full flex-col gap-4.5'>
<Skeleton className='h-[34px] w-full rounded-lg' />
<div className='flex items-center gap-2'>
<Skeleton className='h-[34px] flex-1 rounded-lg' />
<Skeleton className='h-[34px] w-[110px] rounded-lg' />
</div>
<div className='flex flex-col gap-3'>
<div className='flex min-h-0 flex-1 flex-col gap-3'>
{Array.from({ length: 3 }).map((_, i) => (
<Skeleton key={i} className='h-[64px] w-full rounded-lg' />
))}