improvement(resources): segmented API

This commit is contained in:
Emir Karabeg
2026-03-07 20:47:57 -08:00
parent 76486ebcc8
commit 4b7a9b20c4
25 changed files with 419 additions and 315 deletions

View File

@@ -0,0 +1,64 @@
'use client'
import { useEffect } from 'react'
import { createLogger } from '@sim/logger'
import { RefreshCw } from 'lucide-react'
import { Button } from '@/components/emcn'
interface ErrorAction {
label: string
icon?: React.ReactNode
onClick: () => void
variant?: 'default' | 'ghost'
}
export interface ErrorStateProps {
error: Error & { digest?: string }
reset: () => void
title: string
description: string
loggerName: string
secondaryAction?: ErrorAction
}
export function ErrorState({
error,
reset,
title,
description,
loggerName,
secondaryAction,
}: ErrorStateProps) {
const logger = createLogger(loggerName)
useEffect(() => {
logger.error(`${loggerName} error:`, { error: error.message, digest: error.digest })
}, [error, logger, loggerName])
return (
<div className='flex flex-1 items-center justify-center'>
<div className='flex flex-col items-center gap-[16px] text-center'>
<div className='flex flex-col gap-[8px]'>
<h2 className='font-semibold text-[16px] text-[var(--text-primary)]'>{title}</h2>
<p className='max-w-[300px] text-[13px] text-[var(--text-tertiary)]'>{description}</p>
</div>
<div className='flex items-center gap-[8px]'>
{secondaryAction && (
<Button
variant={secondaryAction.variant ?? 'ghost'}
size='sm'
onClick={secondaryAction.onClick}
>
{secondaryAction.icon}
{secondaryAction.label}
</Button>
)}
<Button variant='default' size='sm' onClick={reset}>
<RefreshCw className='mr-[6px] h-[14px] w-[14px]' />
Try again
</Button>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1 @@
export { ErrorState, type ErrorStateProps } from './error'

View File

@@ -1,4 +1,8 @@
export { ErrorState, type ErrorStateProps } from './error'
export { ownerCell } from './resource/components/owner-cell/owner-cell'
export type { BreadcrumbItem } from './resource/components/resource-header'
export { ResourceHeader } from './resource/components/resource-header'
export { ResourceOptionsBar } from './resource/components/resource-options-bar'
export { timeCell } from './resource/components/time-cell/time-cell'
export type { ResourceCell, ResourceColumn, ResourceRow } from './resource/resource'
export { Resource } from './resource/resource'

View File

@@ -1,2 +1,4 @@
export * from './owner-cell'
export * from './resource-header'
export * from './resource-options-bar'
export * from './time-cell'

View File

@@ -0,0 +1,2 @@
export type { BreadcrumbItem } from './resource-header'
export { ResourceHeader } from './resource-header'

View File

@@ -0,0 +1,105 @@
import { Fragment } from 'react'
import { Plus } from 'lucide-react'
import { Button } from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
export interface BreadcrumbItem {
label: string
onClick?: () => void
}
interface ResourceHeaderProps {
icon?: React.ElementType
title?: string
breadcrumbs?: BreadcrumbItem[]
create?: {
label: string
onClick: () => void
disabled?: boolean
}
}
export function ResourceHeader({ icon: Icon, title, breadcrumbs, create }: ResourceHeaderProps) {
const hasBreadcrumbs = breadcrumbs && breadcrumbs.length > 0
return (
<div
className={cn(
'border-[var(--border)] border-b',
hasBreadcrumbs ? 'px-[16px] py-[8.5px]' : 'px-[24px] py-[10px]'
)}
>
<div className='flex items-center justify-between'>
<div className='flex items-center gap-[12px]'>
{hasBreadcrumbs ? (
breadcrumbs.map((crumb, i) => (
<Fragment key={i}>
{i > 0 && (
<span className='select-none text-[14px] text-[var(--text-icon)]'>/</span>
)}
<BreadcrumbSegment
icon={i === 0 ? Icon : undefined}
label={crumb.label}
onClick={crumb.onClick}
/>
</Fragment>
))
) : (
<>
{Icon && <Icon className='h-[14px] w-[14px] text-[var(--text-icon)]' />}
{title && (
<h1 className='font-medium text-[14px] text-[var(--text-body)]'>{title}</h1>
)}
</>
)}
</div>
{create && (
<Button
onClick={create.onClick}
disabled={create.disabled}
variant='subtle'
className='px-[8px] py-[4px] text-[12px]'
>
<Plus className='mr-[6px] h-[14px] w-[14px]' />
{create.label}
</Button>
)}
</div>
</div>
)
}
function BreadcrumbSegment({
icon: Icon,
label,
onClick,
}: {
icon?: React.ElementType
label: string
onClick?: () => void
}) {
const content = (
<>
{Icon && <Icon className='mr-[12px] h-[14px] w-[14px] text-[var(--text-icon)]' />}
{label}
</>
)
if (onClick) {
return (
<Button
variant='subtle'
className='px-[8px] py-[4px] font-medium text-[14px]'
onClick={onClick}
>
{content}
</Button>
)
}
return (
<span className='inline-flex items-center px-[8px] py-[4px] font-medium text-[14px] text-[var(--text-body)]'>
{content}
</span>
)
}

View File

@@ -0,0 +1 @@
export { ResourceOptionsBar } from './resource-options-bar'

View File

@@ -0,0 +1,64 @@
import type { ReactNode } from 'react'
import { ArrowUpDown, ListFilter, Search } from 'lucide-react'
import { Button } from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
interface ResourceOptionsBarProps {
search?: {
value: string
onChange: (value: string) => void
placeholder?: string
}
onSort?: () => void
onFilter?: () => void
toolbarActions?: ReactNode
}
export function ResourceOptionsBar({
search,
onSort,
onFilter,
toolbarActions,
}: ResourceOptionsBarProps) {
const hasContent = search || onSort || onFilter
if (!hasContent) return null
return (
<div
className={cn(
'border-[var(--border)] border-b py-[10px]',
search ? 'px-[24px]' : 'px-[16px]'
)}
>
<div className='flex items-center justify-between'>
{search && (
<div className='relative flex-1'>
<Search className='-translate-y-1/2 pointer-events-none absolute top-1/2 left-0 h-[14px] w-[14px] text-[var(--text-muted)]' />
<input
type='text'
value={search.value}
onChange={(e) => search.onChange(e.target.value)}
placeholder={search.placeholder ?? 'Search...'}
className='w-full bg-transparent py-[4px] pl-[24px] font-base text-[12px] text-[var(--text-secondary)] outline-none placeholder:text-[var(--text-subtle)]'
/>
</div>
)}
<div className='flex items-center gap-[6px]'>
{onFilter && (
<Button variant='subtle' className='px-[8px] py-[4px] text-[12px]' onClick={onFilter}>
<ListFilter className='mr-[6px] h-[14px] w-[14px]' />
Filter
</Button>
)}
{onSort && (
<Button variant='subtle' className='px-[8px] py-[4px] text-[12px]' onClick={onSort}>
<ArrowUpDown className='mr-[6px] h-[14px] w-[14px]' />
Sort
</Button>
)}
{toolbarActions}
</div>
</div>
</div>
)
}

View File

@@ -1,9 +1,12 @@
'use client'
import type { ReactNode } from 'react'
import { ArrowUpDown, ListFilter, Plus, Search } from 'lucide-react'
import { Button, Skeleton } from '@/components/emcn'
import { useCallback, useRef } from 'react'
import { Plus } from 'lucide-react'
import { Skeleton } from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
import { ResourceHeader } from './components/resource-header'
import { ResourceOptionsBar } from './components/resource-options-bar'
export interface ResourceColumn {
id: string
@@ -52,7 +55,7 @@ const EMPTY_CELL_PLACEHOLDER = '- - -'
* Renders the header, toolbar with search, and a data table from column/row definitions.
*/
export function Resource({
icon: Icon,
icon,
title,
create,
search,
@@ -67,90 +70,49 @@ export function Resource({
loadingRows = 5,
onContextMenu,
}: ResourceProps) {
const hasOptionsBar = search || onSort || onFilter
const headerRef = useRef<HTMLDivElement>(null)
const handleBodyScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
if (headerRef.current) {
headerRef.current.scrollLeft = e.currentTarget.scrollLeft
}
}, [])
return (
<div
className='flex h-full flex-1 flex-col overflow-hidden bg-white dark:bg-[var(--bg)]'
onContextMenu={onContextMenu}
>
<div className='border-[var(--border)] border-b px-[24px] py-[10px]'>
<div className='flex items-center justify-between'>
<div className='flex items-center gap-[12px]'>
<Icon className='h-[14px] w-[14px] text-[var(--text-icon)]' />
<h1 className='font-medium text-[14px] text-[var(--text-body)]'>{title}</h1>
</div>
{create && (
<Button
onClick={create.onClick}
disabled={create.disabled}
variant='subtle'
className='px-[8px] py-[4px] text-[12px]'
>
<Plus className='mr-[6px] h-[14px] w-[14px]' />
{create.label}
</Button>
)}
</div>
</div>
{hasOptionsBar && (
<div className='border-[var(--border)] border-b px-[24px] py-[10px]'>
<div className='flex items-center justify-between'>
{search && (
<div className='relative flex-1'>
<Search className='-translate-y-1/2 pointer-events-none absolute top-1/2 left-0 h-[14px] w-[14px] text-[var(--text-muted)]' />
<input
type='text'
value={search.value}
onChange={(e) => search.onChange(e.target.value)}
placeholder={search.placeholder ?? 'Search...'}
className='w-full bg-transparent py-[4px] pl-[24px] font-base text-[12px] text-[var(--text-secondary)] outline-none placeholder:text-[var(--text-subtle)]'
/>
</div>
)}
<div className='flex items-center gap-[6px]'>
{onFilter && (
<Button
variant='subtle'
className='px-[8px] py-[4px] text-[12px]'
onClick={onFilter}
>
<ListFilter className='mr-[6px] h-[14px] w-[14px]' />
Filter
</Button>
)}
{onSort && (
<Button variant='subtle' className='px-[8px] py-[4px] text-[12px]' onClick={onSort}>
<ArrowUpDown className='mr-[6px] h-[14px] w-[14px]' />
Sort
</Button>
)}
{toolbarActions}
</div>
</div>
</div>
)}
<ResourceHeader icon={icon} title={title} create={create} />
<ResourceOptionsBar
search={search}
onSort={onSort}
onFilter={onFilter}
toolbarActions={toolbarActions}
/>
{isLoading ? (
<DataTableSkeleton columns={columns} rowCount={loadingRows} />
) : (
<>
<table className='w-full table-fixed text-[13px]'>
<ResourceColGroup columns={columns} />
<thead className='shadow-[inset_0_-1px_0_var(--border)]'>
<tr>
{columns.map((col) => (
<th
key={col.id}
className='h-10 px-[24px] py-[10px] text-left align-middle font-base text-[var(--text-muted)]'
>
{col.header}
</th>
))}
</tr>
</thead>
</table>
<div className='min-h-0 flex-1 overflow-auto'>
<div ref={headerRef} className='overflow-hidden'>
<table className='w-full table-fixed text-[13px]'>
<ResourceColGroup columns={columns} />
<thead className='shadow-[inset_0_-1px_0_var(--border)]'>
<tr>
{columns.map((col) => (
<th
key={col.id}
className='h-10 px-[24px] py-[10px] text-left align-middle font-base text-[var(--text-muted)]'
>
{col.header}
</th>
))}
</tr>
</thead>
</table>
</div>
<div className='min-h-0 flex-1 overflow-auto' onScroll={handleBodyScroll}>
<table className='w-full table-fixed text-[13px]'>
<ResourceColGroup columns={columns} />
<tbody>
@@ -182,7 +144,9 @@ export function Resource({
<tr
className={cn(
'transition-colors',
create.disabled ? 'opacity-40' : 'cursor-pointer hover:bg-[var(--surface-3)]'
create.disabled
? 'cursor-not-allowed'
: 'cursor-pointer hover:bg-[var(--surface-3)]'
)}
onClick={create.disabled ? undefined : create.onClick}
>
@@ -221,7 +185,7 @@ function ResourceColGroup({ columns }: { columns: ResourceColumn[] }) {
return (
<colgroup>
{columns.map((col, colIdx) => (
<col key={col.id} className={colIdx === 0 ? undefined : 'w-[160px]'} />
<col key={col.id} className={colIdx === 0 ? 'min-w-[200px]' : 'w-[160px]'} />
))}
</colgroup>
)
@@ -230,23 +194,25 @@ function ResourceColGroup({ columns }: { columns: ResourceColumn[] }) {
function DataTableSkeleton({ columns, rowCount }: { columns: ResourceColumn[]; rowCount: number }) {
return (
<>
<table className='w-full table-fixed text-[13px]'>
<ResourceColGroup columns={columns} />
<thead className='shadow-[inset_0_-1px_0_var(--border)]'>
<tr>
{columns.map((col) => (
<th
key={col.id}
className='h-10 px-[24px] py-[10px] text-left align-middle font-base text-[var(--text-muted)]'
>
<div className='flex min-h-[20px] items-center'>
<Skeleton className='h-[12px] w-[56px]' />
</div>
</th>
))}
</tr>
</thead>
</table>
<div className='overflow-hidden'>
<table className='w-full table-fixed text-[13px]'>
<ResourceColGroup columns={columns} />
<thead className='shadow-[inset_0_-1px_0_var(--border)]'>
<tr>
{columns.map((col) => (
<th
key={col.id}
className='h-10 px-[24px] py-[10px] text-left align-middle font-base text-[var(--text-muted)]'
>
<div className='flex min-h-[20px] items-center'>
<Skeleton className='h-[12px] w-[56px]' />
</div>
</th>
))}
</tr>
</thead>
</table>
</div>
<div className='min-h-0 flex-1 overflow-auto'>
<table className='w-full table-fixed text-[13px]'>
<ResourceColGroup columns={columns} />

View File

@@ -1,11 +1,6 @@
'use client'
import { useEffect } from 'react'
import { createLogger } from '@sim/logger'
import { RefreshCw } from 'lucide-react'
import { Button } from '@/components/emcn'
const logger = createLogger('WorkspaceError')
import { ErrorState } from '@/app/workspace/[workspaceId]/components'
interface WorkspaceErrorProps {
error: Error & { digest?: string }
@@ -13,26 +8,13 @@ interface WorkspaceErrorProps {
}
export default function WorkspaceError({ error, reset }: WorkspaceErrorProps) {
useEffect(() => {
logger.error('Workspace error:', { error: error.message, digest: error.digest })
}, [error])
return (
<div className='flex h-full flex-1 items-center justify-center bg-white dark:bg-[var(--bg)]'>
<div className='flex flex-col items-center gap-[16px] text-center'>
<div className='flex flex-col gap-[8px]'>
<h2 className='font-semibold text-[16px] text-[var(--text-primary)]'>
Something went wrong
</h2>
<p className='max-w-[300px] text-[13px] text-[var(--text-tertiary)]'>
An unexpected error occurred. Please try again or refresh the page.
</p>
</div>
<Button variant='default' size='sm' onClick={reset}>
<RefreshCw className='mr-[6px] h-[14px] w-[14px]' />
Try again
</Button>
</div>
</div>
<ErrorState
error={error}
reset={reset}
title='Something went wrong'
description='An unexpected error occurred. Please try again or refresh the page.'
loggerName='WorkspaceError'
/>
)
}

View File

@@ -1,63 +0,0 @@
import { Info, RefreshCw } from 'lucide-react'
import { Badge, Button, Tooltip } from '@/components/emcn'
import { Skeleton } from '@/components/ui/skeleton'
interface HeaderBarProps {
tableName: string
totalCount: number
isLoading: boolean
onNavigateBack: () => void
onShowSchema: () => void
onRefresh: () => void
}
export function HeaderBar({
tableName,
totalCount,
isLoading,
onNavigateBack,
onShowSchema,
onRefresh,
}: HeaderBarProps) {
return (
<div className='flex h-[48px] shrink-0 items-center justify-between border-[var(--border)] border-b px-[16px]'>
<div className='flex items-center gap-[8px]'>
<button
onClick={onNavigateBack}
className='text-[13px] text-[var(--text-tertiary)] transition-colors hover:text-[var(--text-primary)]'
>
Tables
</button>
<span className='text-[var(--text-muted)]'>/</span>
<span className='font-medium text-[13px] text-[var(--text-primary)]'>{tableName}</span>
{isLoading ? (
<Skeleton className='h-[18px] w-[60px] rounded-full' />
) : (
<Badge variant='gray-secondary' size='sm'>
{totalCount} {totalCount === 1 ? 'row' : 'rows'}
</Badge>
)}
</div>
<div className='flex items-center gap-[8px]'>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button variant='ghost' size='sm' onClick={onShowSchema}>
<Info className='h-[14px] w-[14px]' />
</Button>
</Tooltip.Trigger>
<Tooltip.Content>View Schema</Tooltip.Content>
</Tooltip.Root>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button variant='ghost' size='sm' onClick={onRefresh}>
<RefreshCw className='h-[14px] w-[14px]' />
</Button>
</Tooltip.Trigger>
<Tooltip.Content>Refresh</Tooltip.Content>
</Tooltip.Root>
</div>
</div>
)
}

View File

@@ -1 +0,0 @@
export { HeaderBar } from './header-bar'

View File

@@ -3,11 +3,10 @@ export * from './body-states'
export * from './cell-renderer'
export * from './cell-viewer-modal'
export * from './context-menu'
export * from './header-bar'
export * from './inline-cell-editor'
export * from './pagination'
export * from './query-builder'
export * from './row-modal'
export * from './schema-modal'
export * from './table'
export * from './table-row-cells'
export * from './table-viewer'

View File

@@ -1 +0,0 @@
export { TableViewer } from './table-viewer'

View File

@@ -0,0 +1 @@
export { Table } from './table'

View File

@@ -5,13 +5,15 @@ import { useParams, useRouter } from 'next/navigation'
import {
Badge,
Checkbox,
Table,
Table as EmcnTable,
TableBody,
TableHead,
TableHeader,
TableRow,
} from '@/components/emcn'
import { Table as TableIcon } from '@/components/emcn/icons'
import type { TableRow as TableRowType } from '@/lib/table'
import { ResourceHeader, ResourceOptionsBar } from '@/app/workspace/[workspaceId]/components'
import { useUpdateTableRow } from '@/hooks/queries/tables'
import { useContextMenu, useRowSelection, useTableData } from '../../hooks'
import type { CellViewerData, EditingCell, QueryOptions } from '../../types'
@@ -19,16 +21,14 @@ import { ActionBar } from '../action-bar'
import { EmptyRows, LoadingRows } from '../body-states'
import { CellViewerModal } from '../cell-viewer-modal'
import { ContextMenu } from '../context-menu'
import { HeaderBar } from '../header-bar'
import { Pagination } from '../pagination'
import { QueryBuilder } from '../query-builder'
import { RowModal } from '../row-modal'
import { SchemaModal } from '../schema-modal'
import { TableRowCells } from '../table-row-cells'
const EMPTY_COLUMNS: never[] = []
export function TableViewer() {
export function Table() {
const params = useParams()
const router = useRouter()
@@ -50,13 +50,12 @@ export function TableViewer() {
const [cellViewer, setCellViewer] = useState<CellViewerData | null>(null)
const [copied, setCopied] = useState(false)
const { tableData, isLoadingTable, rows, totalCount, totalPages, isLoadingRows, refetchRows } =
useTableData({
workspaceId,
tableId,
queryOptions,
currentPage,
})
const { tableData, isLoadingTable, rows, totalCount, totalPages, isLoadingRows } = useTableData({
workspaceId,
tableId,
queryOptions,
currentPage,
})
const { selectedRows, handleSelectAll, handleSelectRow, clearSelection } = useRowSelection(rows)
@@ -90,18 +89,13 @@ export function TableViewer() {
router.push(`/workspace/${workspaceId}/tables`)
}, [router, workspaceId])
const handleShowSchema = useCallback(() => {
setShowSchemaModal(true)
}, [])
const handleAddRow = useCallback(() => {
setShowAddModal(true)
}, [])
const handleApplyQueryOptions = useCallback((options: QueryOptions) => {
setQueryOptions(options)
setCurrentPage(0)
}, [])
const handleSort = useCallback(() => {}, [])
const handleFilter = useCallback(() => {}, [])
const handleDeleteSelected = useCallback(() => {
setDeletingRows(Array.from(selectedRows))
@@ -214,26 +208,12 @@ export function TableViewer() {
return (
<div className='flex h-full flex-col'>
<HeaderBar
tableName={tableData.name}
totalCount={totalCount}
isLoading={isLoadingRows}
onNavigateBack={handleNavigateBack}
onShowSchema={handleShowSchema}
onRefresh={refetchRows}
<ResourceHeader
icon={TableIcon}
breadcrumbs={[{ label: 'Tables', onClick: handleNavigateBack }, { label: tableData.name }]}
/>
<div className='flex shrink-0 flex-col gap-[8px] border-[var(--border)] border-b px-[16px] py-[10px]'>
<QueryBuilder
columns={columns}
onApply={handleApplyQueryOptions}
onAddRow={handleAddRow}
isLoading={isLoadingRows}
/>
{hasSelection && (
<span className='text-[11px] text-[var(--text-tertiary)]'>{selectedCount} selected</span>
)}
</div>
<ResourceOptionsBar onSort={handleSort} onFilter={handleFilter} />
{hasSelection && (
<ActionBar
@@ -244,7 +224,7 @@ export function TableViewer() {
)}
<div className='flex-1 overflow-auto'>
<Table>
<EmcnTable>
<TableHeader className='sticky top-0 z-10 bg-[var(--surface-3)]'>
<TableRow>
<TableHead className='w-[40px]'>
@@ -293,7 +273,7 @@ export function TableViewer() {
))
)}
</TableBody>
</Table>
</EmcnTable>
</div>
<Pagination

View File

@@ -1,68 +1,31 @@
'use client'
import { useEffect } from 'react'
import { createLogger } from '@sim/logger'
import { ArrowLeft, RefreshCw } from 'lucide-react'
import { ArrowLeft } from 'lucide-react'
import { useParams, useRouter } from 'next/navigation'
import { Button } from '@/components/emcn'
import { ErrorState } from '@/app/workspace/[workspaceId]/components'
const logger = createLogger('TableViewerError')
interface TableViewerErrorProps {
interface TableErrorProps {
error: Error & { digest?: string }
reset: () => void
}
export default function TableViewerError({ error, reset }: TableViewerErrorProps) {
export default function TableError({ error, reset }: TableErrorProps) {
const router = useRouter()
const params = useParams()
const workspaceId = params.workspaceId as string
useEffect(() => {
logger.error('Table viewer error:', { error: error.message, digest: error.digest })
}, [error])
return (
<div className='flex h-full flex-1 flex-col'>
{/* Header */}
<div className='flex h-[48px] shrink-0 items-center border-[var(--border)] border-b px-[16px]'>
<button
onClick={() => router.push(`/workspace/${workspaceId}/tables`)}
className='flex items-center gap-[6px] text-[13px] text-[var(--text-tertiary)] transition-colors hover:text-[var(--text-primary)]'
>
<ArrowLeft className='h-[14px] w-[14px]' />
Back to Tables
</button>
</div>
{/* Error Content */}
<div className='flex flex-1 items-center justify-center'>
<div className='flex flex-col items-center gap-[16px] text-center'>
<div className='flex flex-col gap-[8px]'>
<h2 className='font-semibold text-[16px] text-[var(--text-primary)]'>
Failed to load table
</h2>
<p className='max-w-[300px] text-[13px] text-[var(--text-tertiary)]'>
Something went wrong while loading this table. The table may have been deleted or you
may not have permission to view it.
</p>
</div>
<div className='flex items-center gap-[8px]'>
<Button
variant='ghost'
size='sm'
onClick={() => router.push(`/workspace/${workspaceId}/tables`)}
>
<ArrowLeft className='mr-[6px] h-[14px] w-[14px]' />
Go back
</Button>
<Button variant='default' size='sm' onClick={reset}>
<RefreshCw className='mr-[6px] h-[14px] w-[14px]' />
Try again
</Button>
</div>
</div>
</div>
</div>
<ErrorState
error={error}
reset={reset}
title='Failed to load table'
description='Something went wrong while loading this table. The table may have been deleted or you may not have permission to view it.'
loggerName='TableError'
secondaryAction={{
label: 'Go back',
icon: <ArrowLeft className='mr-[6px] h-[14px] w-[14px]' />,
onClick: () => router.push(`/workspace/${workspaceId}/tables`),
}}
/>
)
}

View File

@@ -1,5 +1,5 @@
import { TableViewer } from './components'
import { Table } from './components'
export default function TablePage() {
return <TableViewer />
return <Table />
}

View File

@@ -1,11 +1,6 @@
'use client'
import { useEffect } from 'react'
import { createLogger } from '@sim/logger'
import { RefreshCw } from 'lucide-react'
import { Button } from '@/components/emcn'
const logger = createLogger('TablesError')
import { ErrorState } from '@/app/workspace/[workspaceId]/components'
interface TablesErrorProps {
error: Error & { digest?: string }
@@ -13,26 +8,13 @@ interface TablesErrorProps {
}
export default function TablesError({ error, reset }: TablesErrorProps) {
useEffect(() => {
logger.error('Tables error:', { error: error.message, digest: error.digest })
}, [error])
return (
<div className='flex h-full flex-1 items-center justify-center bg-white dark:bg-[var(--bg)]'>
<div className='flex flex-col items-center gap-[16px] text-center'>
<div className='flex flex-col gap-[8px]'>
<h2 className='font-semibold text-[16px] text-[var(--text-primary)]'>
Failed to load tables
</h2>
<p className='max-w-[300px] text-[13px] text-[var(--text-tertiary)]'>
Something went wrong while loading the tables. Please try again.
</p>
</div>
<Button variant='default' size='sm' onClick={reset}>
<RefreshCw className='mr-[6px] h-[14px] w-[14px]' />
Try again
</Button>
</div>
</div>
<ErrorState
error={error}
reset={reset}
title='Failed to load tables'
description='Something went wrong while loading the tables. Please try again.'
loggerName='TablesError'
/>
)
}

View File

@@ -2,10 +2,9 @@
import { useCallback, useMemo, useState } from 'react'
import { createLogger } from '@sim/logger'
import { Columns, Rows3 } from 'lucide-react'
import { useParams, useRouter } from 'next/navigation'
import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from '@/components/emcn'
import { Table as TableIcon } from '@/components/emcn/icons'
import { Columns3, Rows3, Table as TableIcon } from '@/components/emcn/icons'
import type { TableDefinition } from '@/lib/table'
import type { ResourceColumn, ResourceRow } from '@/app/workspace/[workspaceId]/components'
import { ownerCell, Resource, timeCell } from '@/app/workspace/[workspaceId]/components'
@@ -80,7 +79,7 @@ export function Tables() {
label: table.name,
},
columns: {
icon: <Columns className='h-[14px] w-[14px]' />,
icon: <Columns3 className='h-[14px] w-[14px]' />,
label: String(table.schema.columns.length),
},
rows: {

View File

@@ -20,7 +20,7 @@ const buttonVariants = cva(
tertiary:
'!bg-[var(--brand-tertiary-2)] !text-[var(--text-inverse)] hover:brightness-106 hover:!text-[var(--text-inverse)] ![transition-property:background-color,border-color,fill,stroke]',
ghost: '',
subtle: 'text-[var(--text-body)] hover:text-[var(--text-body)] hover:bg-[var(--surface-5)]',
subtle: 'text-[var(--text-body)] hover:text-[var(--text-body)] hover:bg-[var(--surface-4)]',
'ghost-secondary': 'text-[var(--text-muted)]',
/** Branded button - requires branded-button-gradient or branded-button-custom class for colors */
branded:

View File

@@ -15,13 +15,13 @@ const Switch = React.forwardRef<
<SwitchPrimitives.Root
disabled={disabled}
className={cn(
'peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full bg-[var(--border-1)] transition-colors focus-visible:outline-none data-[disabled]:cursor-not-allowed data-[state=checked]:bg-[var(--c-2A2A2A)] data-[disabled]:opacity-50',
'peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full bg-[var(--border-1)] transition-colors focus-visible:outline-none data-[disabled]:cursor-not-allowed data-[state=checked]:bg-[var(--text-primary)] data-[disabled]:opacity-50',
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb className='pointer-events-none block h-4 w-4 rounded-full bg-[var(--white)] shadow-sm ring-0 transition-transform data-[state=checked]:translate-x-[18px] data-[state=unchecked]:translate-x-0.5' />
<SwitchPrimitives.Thumb className='pointer-events-none block h-4 w-4 rounded-full bg-[var(--surface-2)] shadow-sm ring-0 transition-transform data-[state=checked]:translate-x-[18px] data-[state=unchecked]:translate-x-0.5' />
</SwitchPrimitives.Root>
))

View File

@@ -0,0 +1,26 @@
import type { SVGProps } from 'react'
/**
* Columns3 icon component - displays three vertical columns in a rounded container
* @param props - SVG properties including className, fill, etc.
*/
export function Columns3(props: SVGProps<SVGSVGElement>) {
return (
<svg
width='24'
height='24'
viewBox='-1 -2 24 24'
fill='none'
stroke='currentColor'
strokeWidth='1.75'
strokeLinecap='round'
strokeLinejoin='round'
xmlns='http://www.w3.org/2000/svg'
{...props}
>
<path d='M0.75 3.25C0.75 1.86929 1.86929 0.75 3.25 0.75H17.25C18.6307 0.75 19.75 1.86929 19.75 3.25V16.25C19.75 17.6307 18.6307 18.75 17.25 18.75H3.25C1.86929 18.75 0.75 17.6307 0.75 16.25V3.25Z' />
<path d='M7.25 0.75V18.75' />
<path d='M13.25 0.75V18.75' />
</svg>
)
}

View File

@@ -4,6 +4,7 @@ export { BubbleChatPreview } from './bubble-chat-preview'
export { Calendar } from './calendar'
export { Card } from './card'
export { ChevronDown } from './chevron-down'
export { Columns3 } from './columns3'
export { Connections } from './connections'
export { Copy } from './copy'
export { Cursor } from './cursor'
@@ -27,6 +28,7 @@ export { PanelLeft } from './panel-left'
export { Play, PlayOutline } from './play'
export { Redo } from './redo'
export { Rocket } from './rocket'
export { Rows3 } from './rows3'
export { Table } from './table'
export { TerminalWindow } from './terminal-window'
export { Trash } from './trash'

View File

@@ -0,0 +1,26 @@
import type { SVGProps } from 'react'
/**
* Rows3 icon component - displays three horizontal rows in a rounded container
* @param props - SVG properties including className, fill, etc.
*/
export function Rows3(props: SVGProps<SVGSVGElement>) {
return (
<svg
width='24'
height='24'
viewBox='-1 -2 24 24'
fill='none'
stroke='currentColor'
strokeWidth='1.75'
strokeLinecap='round'
strokeLinejoin='round'
xmlns='http://www.w3.org/2000/svg'
{...props}
>
<path d='M0.75 3.25C0.75 1.86929 1.86929 0.75 3.25 0.75H17.25C18.6307 0.75 19.75 1.86929 19.75 3.25V16.25C19.75 17.6307 18.6307 18.75 17.25 18.75H3.25C1.86929 18.75 0.75 17.6307 0.75 16.25V3.25Z' />
<path d='M0.75 6.75H19.75' />
<path d='M0.75 12.75H19.75' />
</svg>
)
}