improvement(resource): layout

This commit is contained in:
Emir Karabeg
2026-03-07 16:25:42 -08:00
parent 88a8c5f4a1
commit 6690c55721
6 changed files with 81 additions and 125 deletions

View File

@@ -17,7 +17,6 @@ import { cn } from '@/lib/core/utils/cn'
export interface ResourceColumn {
id: string
header: string
width: string
}
export interface ResourceCell {
@@ -52,8 +51,6 @@ interface ResourceProps {
onRowContextMenu?: (e: React.MouseEvent, rowId: string) => void
isLoading?: boolean
loadingRows?: number
emptyState?: { title: string; description?: string }
error?: { title: string; description?: string }
onContextMenu?: (e: React.MouseEvent) => void
}
@@ -75,8 +72,6 @@ export function Resource({
onRowContextMenu,
isLoading,
loadingRows = 5,
emptyState,
error,
onContextMenu,
}: ResourceProps) {
const hasOptionsBar = search || onSort || onFilter
@@ -152,19 +147,15 @@ export function Resource({
<div className='flex min-h-0 flex-1 flex-col'>
{isLoading ? (
<DataTableSkeleton columns={columns} rowCount={loadingRows} />
) : error ? (
<EmptyMessage title={error.title} description={error.description} />
) : rows.length === 0 && emptyState ? (
<EmptyMessage title={emptyState.title} description={emptyState.description} />
) : (
<Table className='table-fixed text-[13px]'>
<TableHeader>
<TableRow className='hover:bg-transparent'>
{columns.map((col) => (
{columns.map((col, colIdx) => (
<TableHead
key={col.id}
className={cn(
col.width,
colIdx === 0 ? 'min-w-[400px]' : 'w-[160px]',
'px-[24px] py-[10px] font-base text-[var(--text-muted)]'
)}
>
@@ -185,19 +176,37 @@ export function Resource({
onClick={() => onRowClick?.(row.id)}
onContextMenu={(e) => onRowContextMenu?.(e, row.id)}
>
{columns.map((col) => {
{columns.map((col, colIdx) => {
const cell = row.cells[col.id]
if (!cell) {
return <TableCell key={col.id} className='px-[24px] py-[10px]' />
}
return (
<TableCell key={col.id} className='px-[24px] py-[10px]'>
<CellContent cell={cell} />
<CellContent cell={cell} primary={colIdx === 0} />
</TableCell>
)
})}
</TableRow>
))}
{create && (
<TableRow
className={cn(
'border-b-0',
create.disabled
? 'opacity-40'
: 'cursor-pointer hover:bg-[var(--surface-3)]'
)}
onClick={create.disabled ? undefined : create.onClick}
>
<TableCell colSpan={columns.length} className='px-[24px] py-[10px]'>
<span className='flex items-center gap-[12px] font-medium text-[14px] text-[var(--text-secondary)]'>
<Plus className='h-[14px] w-[14px] text-[var(--text-subtle)]' />
{create.label}
</span>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
)}
@@ -208,22 +217,14 @@ export function Resource({
)
}
function EmptyMessage({ title, description }: { title: string; description?: string }) {
function CellContent({ cell, primary }: { cell: ResourceCell; primary?: boolean }) {
return (
<div className='flex flex-1 items-center justify-center'>
<div className='text-center'>
<p className='font-medium text-[14px] text-[var(--text-secondary)]'>{title}</p>
{description && (
<p className='mt-[4px] text-[12px] text-[var(--text-muted)]'>{description}</p>
)}
</div>
</div>
)
}
function CellContent({ cell }: { cell: ResourceCell }) {
return (
<span className='flex min-w-0 items-center gap-[12px] font-medium text-[14px] text-[var(--text-secondary)]'>
<span
className={cn(
'flex min-w-0 items-center gap-[12px] font-medium text-[14px]',
primary ? 'text-[var(--text-body)]' : 'text-[var(--text-secondary)]'
)}
>
{cell.icon && <span className='flex-shrink-0 text-[var(--text-subtle)]'>{cell.icon}</span>}
<span className='truncate'>{cell.label}</span>
</span>
@@ -235,10 +236,13 @@ function DataTableSkeleton({ columns, rowCount }: { columns: ResourceColumn[]; r
<Table className='table-fixed text-[13px]'>
<TableHeader>
<TableRow className='hover:bg-transparent'>
{columns.map((col) => (
{columns.map((col, colIdx) => (
<TableHead
key={col.id}
className={cn(col.width, 'px-[24px] py-[10px] font-base text-[var(--text-muted)]')}
className={cn(
colIdx === 0 ? 'min-w-[400px]' : 'w-[160px]',
'px-[24px] py-[10px] font-base text-[var(--text-muted)]'
)}
>
<div className='flex min-h-[20px] items-center'>
<Skeleton className='h-[12px] w-[56px]' />

View File

@@ -48,10 +48,10 @@ const ACCEPT_ATTR =
'.pdf,.csv,.doc,.docx,.txt,.md,.xlsx,.xls,.html,.htm,.pptx,.ppt,.json,.yaml,.yml,.mp3,.m4a,.wav,.webm,.ogg,.flac,.aac,.opus,.mp4,.mov,.avi,.mkv'
const COLUMNS: ResourceColumn[] = [
{ id: 'name', header: 'Name', width: 'w-[45%]' },
{ id: 'size', header: 'Size', width: 'w-[15%]' },
{ id: 'uploaded', header: 'Uploaded', width: 'w-[20%]' },
{ id: 'actions', header: 'Actions', width: 'w-[20%]' },
{ id: 'name', header: 'Name' },
{ id: 'size', header: 'Size' },
{ id: 'uploaded', header: 'Uploaded' },
{ id: 'actions', header: 'Actions' },
]
function formatFileSize(bytes: number): string {
@@ -76,6 +76,10 @@ export function Files() {
const { data: files = [], isLoading, error } = useWorkspaceFiles(workspaceId)
const uploadFile = useUploadWorkspaceFile()
if (error) {
logger.error('Failed to load files:', error)
}
const fileInputRef = useRef<HTMLInputElement>(null)
const [uploading, setUploading] = useState(false)
const [uploadProgress, setUploadProgress] = useState({ completed: 0, total: 0 })
@@ -160,7 +164,7 @@ export function Files() {
? `${uploadProgress.completed}/${uploadProgress.total}`
: uploading
? 'Uploading...'
: 'Upload'
: 'New file'
return (
<>
@@ -180,18 +184,6 @@ export function Files() {
columns={COLUMNS}
rows={rows}
isLoading={isLoading}
error={
error
? {
title: 'Error loading files',
description: error instanceof Error ? error.message : 'An error occurred',
}
: undefined
}
emptyState={{
title: 'No files yet',
description: 'Upload files to make them accessible across all your workflows',
}}
/>
<input

View File

@@ -30,11 +30,11 @@ interface KnowledgeBaseWithDocCount extends KnowledgeBaseData {
}
const COLUMNS: ResourceColumn[] = [
{ id: 'name', header: 'Name', width: 'w-[35%]' },
{ id: 'documents', header: 'Documents', width: 'w-[12%]' },
{ id: 'description', header: 'Description', width: 'w-[28%]' },
{ id: 'updated', header: 'Updated', width: 'w-[13%]' },
{ id: 'id', header: 'ID', width: 'w-[12%]' },
{ id: 'name', header: 'Name' },
{ id: 'documents', header: 'Documents' },
{ id: 'description', header: 'Description' },
{ id: 'updated', header: 'Updated' },
{ id: 'id', header: 'ID' },
]
export function Knowledge() {
@@ -43,6 +43,10 @@ export function Knowledge() {
const workspaceId = params.workspaceId as string
const { knowledgeBases, isLoading, error } = useKnowledgeBasesList(workspaceId)
if (error) {
logger.error('Failed to load knowledge bases:', error)
}
const userPermissions = useUserPermissionsContext()
const { mutateAsync: updateKnowledgeBaseMutation } = useUpdateKnowledgeBase(workspaceId)
@@ -183,29 +187,13 @@ export function Knowledge() {
}
}, [activeKnowledgeBase, handleDeleteKnowledgeBase])
const emptyState = useMemo(() => {
if (debouncedSearchQuery) {
return {
title: 'No knowledge bases found',
description: 'Try a different search term',
}
}
return {
title: 'No knowledge bases yet',
description:
userPermissions.canEdit === true
? 'Create a knowledge base to get started'
: 'Knowledge bases will appear here once created',
}
}, [debouncedSearchQuery, userPermissions.canEdit])
return (
<>
<Resource
icon={Database}
title='Knowledge Base'
create={{
label: 'Create',
label: 'New base',
onClick: () => setIsCreateModalOpen(true),
disabled: userPermissions.canEdit !== true,
}}
@@ -221,15 +209,6 @@ export function Knowledge() {
onRowClick={handleRowClick}
onRowContextMenu={handleRowContextMenu}
isLoading={isLoading}
error={
error
? {
title: 'Error loading knowledge bases',
description: error,
}
: undefined
}
emptyState={emptyState}
onContextMenu={handleContentContextMenu}
/>

View File

@@ -1,6 +1,7 @@
'use client'
import { useCallback, useMemo, useState } from 'react'
import { createLogger } from '@sim/logger'
import { useParams, useRouter } from 'next/navigation'
import { Calendar, MoreHorizontal } from '@/components/emcn/icons'
import { formatAbsoluteDate, formatRelativeTime } from '@/lib/core/utils/formatting'
@@ -11,6 +12,8 @@ import type { WorkspaceScheduleData } from '@/hooks/queries/schedules'
import { useWorkspaceSchedules } from '@/hooks/queries/schedules'
import { useDebounce } from '@/hooks/use-debounce'
const logger = createLogger('Schedules')
function getHumanReadable(s: WorkspaceScheduleData) {
if (!s.cronExpression && s.nextRunAt) return `Once at ${formatAbsoluteDate(s.nextRunAt)}`
if (s.cronExpression) return parseCronToHumanReadable(s.cronExpression, s.timezone)
@@ -18,12 +21,12 @@ function getHumanReadable(s: WorkspaceScheduleData) {
}
const COLUMNS: ResourceColumn[] = [
{ id: 'name', header: 'Name', width: 'w-[25%]' },
{ id: 'type', header: 'Type', width: 'w-[13%]' },
{ id: 'schedule', header: 'Schedule', width: 'w-[24%]' },
{ id: 'status', header: 'Status', width: 'w-[10%]' },
{ id: 'nextRun', header: 'Next Run', width: 'w-[18%]' },
{ id: 'actions', header: 'Actions', width: 'w-[10%]' },
{ id: 'name', header: 'Name' },
{ id: 'type', header: 'Type' },
{ id: 'schedule', header: 'Schedule' },
{ id: 'status', header: 'Status' },
{ id: 'nextRun', header: 'Next Run' },
{ id: 'actions', header: 'Actions' },
]
export function Schedules() {
@@ -33,6 +36,10 @@ export function Schedules() {
const { data: allItems = [], isLoading, error } = useWorkspaceSchedules(workspaceId)
if (error) {
logger.error('Failed to load schedules:', error)
}
const [searchQuery, setSearchQuery] = useState('')
const debouncedSearchQuery = useDebounce(searchQuery, 300)
@@ -90,22 +97,12 @@ export function Schedules() {
[filteredItems, router, workspaceId]
)
const emptyState = useMemo(() => {
if (debouncedSearchQuery) {
return { title: 'No schedules found', description: 'Try a different search term' }
}
return {
title: 'No schedules yet',
description: 'Scheduled workflows and tasks will appear here',
}
}, [debouncedSearchQuery])
return (
<Resource
icon={Calendar}
title='Schedules'
create={{
label: 'Create',
label: 'New schedule',
onClick: () => {},
}}
search={{
@@ -119,15 +116,6 @@ export function Schedules() {
rows={rows}
onRowClick={handleRowClick}
isLoading={isLoading}
error={
error
? {
title: 'Error loading schedules',
description: error instanceof Error ? error.message : 'An error occurred',
}
: undefined
}
emptyState={emptyState}
/>
)
}

View File

@@ -20,11 +20,11 @@ import { useDeleteTable, useTablesList } from '@/hooks/queries/tables'
const logger = createLogger('Tables')
const COLUMNS: ResourceColumn[] = [
{ id: 'name', header: 'Name', width: 'w-[40%]' },
{ id: 'columns', header: 'Columns', width: 'w-[15%]' },
{ id: 'rows', header: 'Rows', width: 'w-[15%]' },
{ id: 'updated', header: 'Updated', width: 'w-[18%]' },
{ id: 'id', header: 'ID', width: 'w-[12%]' },
{ id: 'name', header: 'Name' },
{ id: 'columns', header: 'Columns' },
{ id: 'rows', header: 'Rows' },
{ id: 'updated', header: 'Updated' },
{ id: 'id', header: 'ID' },
]
export function Tables() {
@@ -34,6 +34,10 @@ export function Tables() {
const userPermissions = useUserPermissionsContext()
const { data: tables = [], isLoading, error } = useTablesList(workspaceId)
if (error) {
logger.error('Failed to load tables:', error)
}
const deleteTable = useDeleteTable(workspaceId)
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false)
@@ -145,7 +149,7 @@ export function Tables() {
icon={TableIcon}
title='Tables'
create={{
label: 'Create Table',
label: 'New table',
onClick: () => setIsCreateModalOpen(true),
disabled: userPermissions.canEdit !== true,
}}
@@ -161,18 +165,6 @@ export function Tables() {
onRowClick={handleRowClick}
onRowContextMenu={handleRowContextMenu}
isLoading={isLoading}
error={
error
? {
title: 'Error loading tables',
description: error instanceof Error ? error.message : 'An error occurred',
}
: undefined
}
emptyState={{
title: 'No tables yet',
description: 'Create your first table to store structured data for your workflows',
}}
onContextMenu={handleContentContextMenu}
/>

View File

@@ -1,10 +1,9 @@
'use client'
import { useMemo } from 'react'
import { ArrowLeft } from 'lucide-react'
import Link from 'next/link'
import { useParams, usePathname, useRouter } from 'next/navigation'
import { Skeleton } from '@/components/emcn'
import { ChevronDown, Skeleton } from '@/components/emcn'
import { useSession } from '@/lib/auth/auth-client'
import { getSubscriptionStatus } from '@/lib/billing/client'
import { isHosted } from '@/lib/core/config/feature-flags'
@@ -143,7 +142,9 @@ export function SettingsSidebar() {
onClick={handleBack}
className='group mx-[2px] flex h-[30px] items-center gap-[8px] rounded-[8px] px-[8px] text-[14px] hover:bg-[var(--surface-active)]'
>
<ArrowLeft className='h-[16px] w-[16px] flex-shrink-0 text-[var(--text-icon)]' />
<span className='flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center text-[var(--text-icon)]'>
<ChevronDown className='h-[10px] w-[10px] rotate-90' />
</span>
<span className='truncate font-[var(--sidebar-font-weight)] text-[var(--text-body)]'>
Back
</span>