'use client' import { useState } from 'react' import { Badge, ChipConfirmModal, ChipModal, ChipModalBody, ChipModalError, ChipModalField, ChipModalFooter, ChipModalHeader, ChipSelect, cn, Switch, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, toast, } from '@sim/emcn' import { createLogger } from '@sim/logger' import { isOrgAdminRole } from '@sim/platform-authz/predicates' import { toError } from '@sim/utils/errors' import { ChevronDown, Plus } from 'lucide-react' import type { CreateDataDrainBody, DataDrain, DataDrainRun } from '@/lib/api/contracts/data-drains' import { useSession } from '@/lib/auth/auth-client' import { CADENCE_TYPES, DESTINATION_TYPES, SOURCE_TYPES } from '@/lib/data-drains/types' import { getUserRole } from '@/lib/workspaces/organization/utils' import { RowActionsMenu } from '@/app/workspace/[workspaceId]/settings/components/row-actions-menu' import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state' import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel' import { DESTINATION_FORM_REGISTRY } from '@/ee/data-drains/destinations/registry' import { useCreateDataDrain, useDataDrainRuns, useDataDrains, useDeleteDataDrain, useRunDataDrainNow, useTestDataDrain, useUpdateDataDrain, } from '@/ee/data-drains/hooks/data-drains' import { useOrganizations } from '@/hooks/queries/organization' const logger = createLogger('DataDrainsSettings') const SOURCE_LABELS: Record<(typeof SOURCE_TYPES)[number], string> = { workflow_logs: 'Workflow logs', job_logs: 'Job logs', audit_logs: 'Audit logs', copilot_chats: 'Chats', copilot_runs: 'Chat runs', } const DESTINATION_LABELS: Record<(typeof DESTINATION_TYPES)[number], string> = { s3: 'Amazon S3', gcs: 'Google Cloud Storage', azure_blob: 'Azure Blob Storage', datadog: 'Datadog', bigquery: 'Google BigQuery', snowflake: 'Snowflake', webhook: 'HTTPS webhook', } const CADENCE_LABELS: Record<(typeof CADENCE_TYPES)[number], string> = { hourly: 'Every hour', daily: 'Every day', } 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] })) const DESTINATION_OPTIONS = DESTINATION_TYPES.map((t) => ({ value: t, label: DESTINATION_LABELS[t], })) export function DataDrainsSettings() { const { data: session, isPending: sessionPending } = useSession() const { data: orgsData, isLoading: orgsLoading } = useOrganizations() const activeOrganization = orgsData?.activeOrganization const orgId = activeOrganization?.id const userEmail = session?.user?.email const userRole = getUserRole(activeOrganization, userEmail) const canManage = isOrgAdminRole(userRole) const { data: drains, isLoading: drainsLoading, error: drainsError } = useDataDrains(orgId) const [createOpen, setCreateOpen] = useState(false) const [expandedDrainId, setExpandedDrainId] = useState(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 null } if (!orgId) { return ( Data drains are configured per organization. Join or create one to continue. ) } if (!canManage) { return ( Only organization owners and admins can configure data drains. ) } return ( <> setCreateOpen(true), }, ]} search={{ value: searchTerm, onChange: setSearchTerm, placeholder: 'Search data drains...', }} >
{drainsError ? (

Failed to load data drains: {toError(drainsError).message}

) : drains && drains.length > 0 ? ( filteredDrains.length > 0 ? ( Name Source Destination Cadence Last run Enabled {filteredDrains.map((drain) => ( setExpandedDrainId(expandedDrainId === drain.id ? null : drain.id) } /> ))}
) : ( No results for "{searchTerm.trim()}" ) ) : ( Click "New drain" above to get started )}
{createOpen && ( setCreateOpen(false)} /> )} ) } interface DrainRowProps { drain: DataDrain organizationId: string expanded: boolean onToggleExpand: () => void } function DrainRow({ drain, organizationId, expanded, onToggleExpand }: DrainRowProps) { const updateMutation = useUpdateDataDrain() const deleteMutation = useDeleteDataDrain() const runMutation = useRunDataDrainNow() const testMutation = useTestDataDrain() const [showDeleteConfirm, setShowDeleteConfirm] = useState(false) async function handleToggleEnabled() { try { await updateMutation.mutateAsync({ organizationId, drainId: drain.id, body: { enabled: !drain.enabled }, }) toast.success(drain.enabled ? 'Drain disabled' : 'Drain enabled') } catch (error) { toast.error(toError(error).message) } } async function handleRunNow() { try { await runMutation.mutateAsync({ organizationId, drainId: drain.id }) toast.success('Drain run enqueued') } catch (error) { toast.error(toError(error).message) } } async function handleTest() { try { await testMutation.mutateAsync({ organizationId, drainId: drain.id }) toast.success('Connection test succeeded') } catch (error) { toast.error(toError(error).message) } } function handleDelete() { setShowDeleteConfirm(true) } async function handleConfirmDelete() { try { setShowDeleteConfirm(false) await deleteMutation.mutateAsync({ organizationId, drainId: drain.id }) toast.success('Drain deleted') } catch (error) { toast.error(toError(error).message) } } return ( <>
{drain.name}
{SOURCE_LABELS[drain.source]} {DESTINATION_LABELS[drain.destinationType]} {CADENCE_LABELS[drain.scheduleCadence]} {drain.lastRunAt ? new Date(drain.lastRunAt).toLocaleString() : 'Never'} e.stopPropagation()}> e.stopPropagation()}>
{expanded && ( )} ) } interface DrainRunsPanelProps { organizationId: string drainId: string } function DrainRunsPanel({ organizationId, drainId }: DrainRunsPanelProps) { const { data: runs, isLoading } = useDataDrainRuns(organizationId, drainId, 10) if (isLoading) { return
Loading runs...
} if (!runs || runs.length === 0) { return
No runs yet.
} return (
Recent runs
{runs.map((run) => ( ))}
) } function RunRow({ run }: { run: DataDrainRun }) { const statusColor = run.status === 'success' ? 'text-[var(--text-success)]' : run.status === 'failed' ? 'text-[var(--text-error)]' : 'text-[var(--text-muted)]' return (
{run.status} {run.trigger} {new Date(run.startedAt).toLocaleString()}
{run.error &&
{run.error}
}
{run.rowsExported.toLocaleString()} rows
{(run.bytesWritten / 1024).toFixed(1)} KB
) } interface CreateDrainModalProps { organizationId: string onClose: () => void } function CreateDrainModal({ organizationId, onClose }: CreateDrainModalProps) { const createMutation = useCreateDataDrain() const [name, setName] = useState('') const [source, setSource] = useState<(typeof SOURCE_TYPES)[number]>('workflow_logs') const [cadence, setCadence] = useState<(typeof CADENCE_TYPES)[number]>('daily') const [destinationType, setDestinationType] = useState<(typeof DESTINATION_TYPES)[number]>( DESTINATION_TYPES[0] ) const [destState, setDestState] = useState( () => DESTINATION_FORM_REGISTRY[DESTINATION_TYPES[0]].initialState ) const [submitError, setSubmitError] = useState(null) const spec = DESTINATION_FORM_REGISTRY[destinationType] const canSubmit = name.trim().length > 0 && spec.isComplete(destState) function handleDestinationChange(next: (typeof DESTINATION_TYPES)[number]) { setDestinationType(next) setDestState(DESTINATION_FORM_REGISTRY[next].initialState) } async function handleSubmit() { if (!canSubmit) return setSubmitError(null) try { const body = { name: name.trim(), source, scheduleCadence: cadence, ...spec.toDestinationBranch(destState), } as CreateDataDrainBody await createMutation.mutateAsync({ organizationId, body }) toast.success('Drain created') onClose() } catch (error) { const msg = toError(error).message logger.error('Failed to create data drain', { error: msg }) setSubmitError(msg) } } return ( !open && onClose()} srTitle='New data drain' size='md'> onClose()}>New data drain setSource(v as (typeof SOURCE_TYPES)[number])} options={SOURCE_OPTIONS} align='start' /> setCadence(v as (typeof CADENCE_TYPES)[number])} options={CADENCE_OPTIONS} align='start' /> handleDestinationChange(v as (typeof DESTINATION_TYPES)[number])} options={DESTINATION_OPTIONS} displayLabel={DESTINATION_LABELS[destinationType]} align='start' />
{submitError}
) }