import {Plus, RefreshCw, X} from 'lucide-react' import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' import {useEffect, useState} from 'react' import {Link} from 'react-router-dom' import {createCatalogTable, deleteCatalogTable, getCloudResource} from '@/api/cloudProxyClient' import {cloudQueryKeys} from '@/api/queries/cloudQueries' import {useAccountId} from '@/lib/accountStore' import {formatDateTime} from '@/lib/format' import type {CatalogTableInput, CloudResource} from '@/types/resource' type Props = { resource?: CloudResource runtimeReachable: boolean } type Column = {name: string; type: string} type CatalogTable = { name: string description: string | null format: string location: string | null delimiter: string | null headerRow: boolean columns: Column[] partitionKeys: Column[] updatedAt: string | null createdAt: string | null } type TableForm = {name: string; location: string; format: CatalogTableInput['format']; delimiter: string; headerRow: boolean; columns: Column[]} /** Matches the types the API accepts; decimal(p,s) can also be typed in by hand through the API. */ const COLUMN_TYPES = ['string', 'int', 'bigint', 'double', 'float', 'boolean', 'date', 'timestamp', 'decimal(10,2)'] const FORMAT_LABELS: Record = {csv: 'CSV', json: 'JSON lines', parquet: 'Parquet', iceberg: 'Iceberg'} const DELIMITER_NAMES: Record = {',': 'comma-separated', ';': 'semicolon-separated', '\t': 'tab-separated', '|': 'pipe-separated'} const delimiterLabel = (delimiter: string | null) => DELIMITER_NAMES[delimiter ?? ','] ?? `separated by "${delimiter}"` const emptyForm = (): TableForm => ({name: '', location: 's3://', format: 'csv', delimiter: ',', headerRow: true, columns: [{name: '', type: 'string'}]}) export function DataCatalogPanel({resource, runtimeReachable}: Props) { const accountId = useAccountId() const queryClient = useQueryClient() const [form, setForm] = useState(emptyForm) const [notice, setNotice] = useState('') const detailKey = ['glue-database', accountId, resource?.id] const detailQuery = useQuery({ queryKey: detailKey, queryFn: ({signal}) => getCloudResource('aws', 'datacatalog', resource?.id ?? '', signal), enabled: Boolean(resource) && runtimeReachable, }) useEffect(() => { setNotice('') setForm(emptyForm()) }, [resource?.id]) const settle = (updated: CloudResource, message: string) => { queryClient.setQueryData(detailKey, updated) setNotice(message) return queryClient.invalidateQueries({queryKey: cloudQueryKeys.resources('aws', 'datacatalog')}) } const createTable = useMutation({ mutationFn: () => createCatalogTable('aws', resource?.id ?? '', { name: form.name.trim(), location: form.location.trim(), format: form.format, ...(form.format === 'csv' ? {delimiter: form.delimiter, headerRow: form.headerRow} : {}), columns: form.columns.map((column) => ({name: column.name.trim(), type: column.type})), }), onSuccess: (updated) => { const name = form.name.trim() setForm(emptyForm()) return settle(updated, `Created table ${name}. Athena can query it now.`) }, }) const deleteTable = useMutation({ mutationFn: (tableName: string) => deleteCatalogTable('aws', resource?.id ?? '', tableName), onSuccess: (updated, tableName) => settle(updated, `Deleted table ${tableName}. Its files stay in S3.`), }) if (!resource) { return

Select a database

Choose a database to see its tables, or describe files in S3 as a new table.

} const database = detailQuery.data ?? resource const tables = Array.isArray(database.metadata.tables) ? database.metadata.tables as CatalogTable[] : [] const disabled = !runtimeReachable || createTable.isPending || deleteTable.isPending const error = detailQuery.error ?? createTable.error ?? deleteTable.error const setColumn = (index: number, patch: Partial) => setForm({...form, columns: form.columns.map((column, i) => i === index ? {...column, ...patch} : column)}) return

Glue database

{database.metadata.description ? String(database.metadata.description) : 'Tables here describe files in S3 so Athena can query them.'}
{tables.length > 0 && Query in Athena}

Tables

{tables.length === 1 ? '1 table' : `${tables.length} tables`}
{tables.length === 0 ?

No tables yet. Describe a folder of CSV, JSON or Parquet files below.

:
{tables.map((table) => ( ))}
TableFormatLocationColumnsUpdated
{table.name}{table.description && <>
{table.description}}
{FORMAT_LABELS[table.format] ?? table.format}{table.format === 'csv' && <>
{delimiterLabel(table.delimiter)}{table.headerRow ? ', header row' : ''}}
{table.location ?? '—'} {table.columns.map((column) => {column.name} {column.type})} {table.partitionKeys.length > 0 && partitioned by {table.partitionKeys.map((key) => key.name).join(', ')}} {formatDateTime(table.updatedAt ?? table.createdAt) ?? '—'}
}
{ event.preventDefault(); setNotice(''); createTable.mutate() }}>

New table

{form.format === 'csv' && <> }
Columns {form.columns.map((column, index) =>
setColumn(index, {name: event.target.value})} required pattern="[a-z_][a-z0-9_]*" title="Lowercase letters, numbers and underscores"/>
)}

List the columns in the order they appear in the files. For JSON, use the field names.

{notice &&

{notice}

} {error instanceof Error &&

{error.message}

}
}