Fix light theme support and expand/collapse UX

- Add light/dark theme support to DeploymentsTable and DeploymentLogs
- Add stopPropagation to logs container and buttons to prevent accidental collapse
- Fix absolute positioning by adding relative parent

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-02-02 02:08:40 +00:00
parent 84d5633b36
commit 887ebf0ab8
2 changed files with 50 additions and 45 deletions

View File

@@ -78,13 +78,13 @@ export function DeploymentLogs({ deploymentUuid, status, initialLogs }: Deployme
}; };
return ( return (
<div className="border-t border-stone-800 bg-stone-950"> <div className="relative border-t border-slate-200 dark:border-stone-800 bg-slate-50 dark:bg-stone-950">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between px-4 py-2 border-b border-stone-800"> <div className="flex items-center justify-between px-4 py-2 border-b border-slate-200 dark:border-stone-800">
<span className="text-sm font-medium text-stone-300">Build Logs</span> <span className="text-sm font-medium text-slate-700 dark:text-stone-300">Build Logs</span>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{isPolling && ( {isPolling && (
<span className="flex items-center gap-1 text-xs text-stone-500"> <span className="flex items-center gap-1 text-xs text-slate-500 dark:text-stone-500">
<span className="animate-spin"> <span className="animate-spin">
<Icon name="refresh-cw" size={12} /> <Icon name="refresh-cw" size={12} />
</span> </span>
@@ -92,8 +92,11 @@ export function DeploymentLogs({ deploymentUuid, status, initialLogs }: Deployme
</span> </span>
)} )}
<button <button
onClick={copyToClipboard} onClick={(e) => {
className="flex items-center gap-1 px-2 py-1 text-xs text-stone-400 hover:text-stone-200 hover:bg-stone-800 rounded transition-colors" e.stopPropagation();
copyToClipboard();
}}
className="flex items-center gap-1 px-2 py-1 text-xs text-slate-500 dark:text-stone-400 hover:text-slate-700 dark:hover:text-stone-200 hover:bg-slate-200 dark:hover:bg-stone-800 rounded transition-colors"
> >
<Icon name={copied ? 'check' : 'copy'} size={14} /> <Icon name={copied ? 'check' : 'copy'} size={14} />
{copied ? 'Copied' : 'Copy'} {copied ? 'Copied' : 'Copy'}
@@ -105,10 +108,11 @@ export function DeploymentLogs({ deploymentUuid, status, initialLogs }: Deployme
<div <div
ref={containerRef} ref={containerRef}
onScroll={handleScroll} onScroll={handleScroll}
onClick={(e) => e.stopPropagation()}
className="max-h-80 overflow-y-auto font-mono text-xs p-4 space-y-0.5" className="max-h-80 overflow-y-auto font-mono text-xs p-4 space-y-0.5"
> >
{logs.length === 0 ? ( {logs.length === 0 ? (
<div className="text-stone-500 text-center py-8"> <div className="text-slate-500 dark:text-stone-500 text-center py-8">
{isPolling ? 'Waiting for logs...' : 'No logs available'} {isPolling ? 'Waiting for logs...' : 'No logs available'}
</div> </div>
) : ( ) : (
@@ -116,11 +120,11 @@ export function DeploymentLogs({ deploymentUuid, status, initialLogs }: Deployme
<div <div
key={index} key={index}
className={`flex ${ className={`flex ${
log.type === 'stderr' ? 'text-red-400' : 'text-stone-300' log.type === 'stderr' ? 'text-red-600 dark:text-red-400' : 'text-slate-700 dark:text-stone-300'
}`} }`}
> >
{log.timestamp && ( {log.timestamp && (
<span className="text-stone-600 mr-3 select-none shrink-0"> <span className="text-slate-400 dark:text-stone-600 mr-3 select-none shrink-0">
{log.timestamp} {log.timestamp}
</span> </span>
)} )}
@@ -134,11 +138,12 @@ export function DeploymentLogs({ deploymentUuid, status, initialLogs }: Deployme
{/* Auto-scroll indicator */} {/* Auto-scroll indicator */}
{!autoScroll && logs.length > 0 && ( {!autoScroll && logs.length > 0 && (
<button <button
onClick={() => { onClick={(e) => {
e.stopPropagation();
setAutoScroll(true); setAutoScroll(true);
logsEndRef.current?.scrollIntoView({ behavior: 'smooth' }); logsEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}} }}
className="absolute bottom-4 right-4 px-3 py-1.5 bg-stone-800 text-stone-300 text-xs rounded-full shadow-lg hover:bg-stone-700 transition-colors" className="absolute bottom-4 right-4 px-3 py-1.5 bg-slate-200 dark:bg-stone-800 text-slate-700 dark:text-stone-300 text-xs rounded-full shadow-lg hover:bg-slate-300 dark:hover:bg-stone-700 transition-colors"
> >
Scroll to bottom Scroll to bottom
</button> </button>

View File

@@ -46,7 +46,7 @@ const StatusDot = ({ status }: { status: DeploymentStatus }) => (
const StatusBadge = ({ status }: { status: DeploymentStatus }) => ( const StatusBadge = ({ status }: { status: DeploymentStatus }) => (
<span className="flex items-center gap-1.5"> <span className="flex items-center gap-1.5">
<StatusDot status={status} /> <StatusDot status={status} />
<span className="text-stone-300">{STATUS_LABELS[status]}</span> <span className="text-slate-700 dark:text-stone-300">{STATUS_LABELS[status]}</span>
</span> </span>
); );
@@ -81,7 +81,7 @@ export function DeploymentsTable({ deployments, isLoading, onRefresh }: Deployme
cell: ({ row, getValue }) => ( cell: ({ row, getValue }) => (
<button <button
onClick={() => row.toggleExpanded()} onClick={() => row.toggleExpanded()}
className="flex items-center gap-2 text-stone-300 hover:text-white font-mono text-sm" className="flex items-center gap-2 text-slate-700 dark:text-stone-300 hover:text-slate-900 dark:hover:text-white font-mono text-sm"
> >
<Icon <Icon
name="chevron-right" name="chevron-right"
@@ -96,11 +96,11 @@ export function DeploymentsTable({ deployments, isLoading, onRefresh }: Deployme
header: 'Environment', header: 'Environment',
cell: ({ getValue }) => ( cell: ({ getValue }) => (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="px-2 py-0.5 text-xs rounded bg-stone-800 text-stone-300"> <span className="px-2 py-0.5 text-xs rounded bg-slate-100 dark:bg-stone-800 text-slate-700 dark:text-stone-300">
Production Production
</span> </span>
{getValue() && ( {getValue() && (
<span className="flex items-center gap-1 text-xs text-cyan-400"> <span className="flex items-center gap-1 text-xs text-cyan-600 dark:text-cyan-400">
<Icon name="check" size={12} /> <Icon name="check" size={12} />
Current Current
</span> </span>
@@ -113,7 +113,7 @@ export function DeploymentsTable({ deployments, isLoading, onRefresh }: Deployme
cell: ({ getValue }) => { cell: ({ getValue }) => {
const duration = getValue(); const duration = getValue();
return ( return (
<span className="text-stone-400 text-sm"> <span className="text-slate-500 dark:text-stone-400 text-sm">
{duration ? formatDuration(duration) : '-'} {duration ? formatDuration(duration) : '-'}
</span> </span>
); );
@@ -130,11 +130,11 @@ export function DeploymentsTable({ deployments, isLoading, onRefresh }: Deployme
href={`http://192.168.1.3:8000/project/a8484ggc88c40w4g4k004ow0/production/application/${row.original.application_uuid}`} href={`http://192.168.1.3:8000/project/a8484ggc88c40w4g4k004ow0/production/application/${row.original.application_uuid}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="flex items-center gap-2 text-stone-300 hover:text-white" className="flex items-center gap-2 text-slate-700 dark:text-stone-300 hover:text-slate-900 dark:hover:text-white"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<span className="w-5 h-5 flex items-center justify-center rounded bg-stone-800"> <span className="w-5 h-5 flex items-center justify-center rounded bg-slate-100 dark:bg-stone-800">
<Icon name="box" size={12} className="text-stone-400" /> <Icon name="box" size={12} className="text-slate-500 dark:text-stone-400" />
</span> </span>
{getValue()} {getValue()}
</a> </a>
@@ -144,16 +144,16 @@ export function DeploymentsTable({ deployments, isLoading, onRefresh }: Deployme
header: 'Source', header: 'Source',
cell: ({ getValue, row }) => ( cell: ({ getValue, row }) => (
<div className="flex items-center gap-2 text-sm"> <div className="flex items-center gap-2 text-sm">
<span className="text-stone-500"> <span className="text-slate-400 dark:text-stone-500">
<Icon name="git-branch" size={14} /> <Icon name="git-branch" size={14} />
</span> </span>
<span className="text-stone-300">{getValue() || 'main'}</span> <span className="text-slate-700 dark:text-stone-300">{getValue() || 'main'}</span>
{row.original.git_commit_sha && ( {row.original.git_commit_sha && (
<> <>
<span className="text-stone-600 font-mono"> <span className="text-slate-400 dark:text-stone-600 font-mono">
{row.original.git_commit_sha.substring(0, 7)} {row.original.git_commit_sha.substring(0, 7)}
</span> </span>
<span className="text-stone-500 truncate max-w-[200px]"> <span className="text-slate-500 dark:text-stone-500 truncate max-w-[200px]">
{truncateCommitMessage(row.original.commit_message || '')} {truncateCommitMessage(row.original.commit_message || '')}
</span> </span>
</> </>
@@ -164,9 +164,9 @@ export function DeploymentsTable({ deployments, isLoading, onRefresh }: Deployme
columnHelper.accessor('created_at', { columnHelper.accessor('created_at', {
header: 'Created', header: 'Created',
cell: ({ getValue, row }) => ( cell: ({ getValue, row }) => (
<div className="flex items-center gap-2 text-sm text-stone-400"> <div className="flex items-center gap-2 text-sm text-slate-500 dark:text-stone-400">
<span>{formatRelativeTime(getValue())}</span> <span>{formatRelativeTime(getValue())}</span>
<span className="text-stone-600">by</span> <span className="text-slate-400 dark:text-stone-600">by</span>
<span>{row.original.is_webhook ? 'webhook' : 'API'}</span> <span>{row.original.is_webhook ? 'webhook' : 'API'}</span>
</div> </div>
), ),
@@ -179,7 +179,7 @@ export function DeploymentsTable({ deployments, isLoading, onRefresh }: Deployme
href={`http://192.168.1.3:8000/project/a8484ggc88c40w4g4k004ow0/production/application/${row.original.application_uuid}/deployment/${row.original.deployment_uuid}`} href={`http://192.168.1.3:8000/project/a8484ggc88c40w4g4k004ow0/production/application/${row.original.application_uuid}/deployment/${row.original.deployment_uuid}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="p-1 text-stone-500 hover:text-stone-300 hover:bg-stone-800 rounded transition-colors" className="p-1 text-slate-400 dark:text-stone-500 hover:text-slate-600 dark:hover:text-stone-300 hover:bg-slate-100 dark:hover:bg-stone-800 rounded transition-colors"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<Icon name="external-link" size={16} /> <Icon name="external-link" size={16} />
@@ -235,7 +235,7 @@ export function DeploymentsTable({ deployments, isLoading, onRefresh }: Deployme
<select <select
value={appFilter} value={appFilter}
onChange={(e) => setAppFilter(e.target.value)} onChange={(e) => setAppFilter(e.target.value)}
className="px-3 py-1.5 text-sm bg-stone-900 border border-stone-700 rounded-lg text-stone-300 focus:outline-none focus:border-stone-500" className="px-3 py-1.5 text-sm bg-white dark:bg-stone-900 border border-slate-200 dark:border-stone-700 rounded-lg text-slate-700 dark:text-stone-300 focus:outline-none focus:border-slate-400 dark:focus:border-stone-500"
> >
<option value="all">All Applications</option> <option value="all">All Applications</option>
{applicationNames.map((name) => ( {applicationNames.map((name) => (
@@ -249,7 +249,7 @@ export function DeploymentsTable({ deployments, isLoading, onRefresh }: Deployme
<select <select
value={statusFilter} value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value as DeploymentStatus | 'all')} onChange={(e) => setStatusFilter(e.target.value as DeploymentStatus | 'all')}
className="px-3 py-1.5 text-sm bg-stone-900 border border-stone-700 rounded-lg text-stone-300 focus:outline-none focus:border-stone-500" className="px-3 py-1.5 text-sm bg-white dark:bg-stone-900 border border-slate-200 dark:border-stone-700 rounded-lg text-slate-700 dark:text-stone-300 focus:outline-none focus:border-slate-400 dark:focus:border-stone-500"
> >
<option value="all">All Statuses</option> <option value="all">All Statuses</option>
<option value="finished">Ready</option> <option value="finished">Ready</option>
@@ -265,7 +265,7 @@ export function DeploymentsTable({ deployments, isLoading, onRefresh }: Deployme
setStatusFilter('all'); setStatusFilter('all');
setAppFilter('all'); setAppFilter('all');
}} }}
className="text-xs text-stone-500 hover:text-stone-300" className="text-xs text-slate-500 dark:text-stone-500 hover:text-slate-700 dark:hover:text-stone-300"
> >
Clear filters Clear filters
</button> </button>
@@ -273,13 +273,13 @@ export function DeploymentsTable({ deployments, isLoading, onRefresh }: Deployme
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className="text-sm text-stone-500"> <span className="text-sm text-slate-500 dark:text-stone-500">
{filteredDeployments.length} deployments {filteredDeployments.length} deployments
</span> </span>
<button <button
onClick={onRefresh} onClick={onRefresh}
disabled={isLoading} disabled={isLoading}
className="flex items-center gap-1.5 px-3 py-1.5 text-sm text-stone-300 hover:text-white bg-stone-800 hover:bg-stone-700 rounded-lg transition-colors disabled:opacity-50" className="flex items-center gap-1.5 px-3 py-1.5 text-sm text-slate-700 dark:text-stone-300 hover:text-slate-900 dark:hover:text-white bg-slate-100 dark:bg-stone-800 hover:bg-slate-200 dark:hover:bg-stone-700 rounded-lg transition-colors disabled:opacity-50"
> >
<Icon <Icon
name="refresh-cw" name="refresh-cw"
@@ -292,21 +292,21 @@ export function DeploymentsTable({ deployments, isLoading, onRefresh }: Deployme
</div> </div>
{/* Table */} {/* Table */}
<div className="bg-stone-900 rounded-xl border border-stone-700/50 overflow-hidden"> <div className="bg-white dark:bg-stone-900 rounded-xl border border-slate-200 dark:border-stone-700/50 overflow-hidden shadow-sm">
<table className="w-full"> <table className="w-full">
<thead> <thead>
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id} className="border-b border-stone-800"> <tr key={headerGroup.id} className="border-b border-slate-200 dark:border-stone-800">
{headerGroup.headers.map((header) => ( {headerGroup.headers.map((header) => (
<th <th
key={header.id} key={header.id}
className="px-4 py-3 text-left text-xs font-medium text-stone-500 uppercase tracking-wider" className="px-4 py-3 text-left text-xs font-medium text-slate-500 dark:text-stone-500 uppercase tracking-wider"
> >
{header.isPlaceholder ? null : ( {header.isPlaceholder ? null : (
<button <button
className={`flex items-center gap-1 ${ className={`flex items-center gap-1 ${
header.column.getCanSort() header.column.getCanSort()
? 'cursor-pointer hover:text-stone-300' ? 'cursor-pointer hover:text-slate-700 dark:hover:text-stone-300'
: '' : ''
}`} }`}
onClick={header.column.getToggleSortingHandler()} onClick={header.column.getToggleSortingHandler()}
@@ -329,7 +329,7 @@ export function DeploymentsTable({ deployments, isLoading, onRefresh }: Deployme
{isLoading && deployments.length === 0 ? ( {isLoading && deployments.length === 0 ? (
<tr> <tr>
<td colSpan={columns.length} className="px-4 py-12 text-center"> <td colSpan={columns.length} className="px-4 py-12 text-center">
<div className="flex items-center justify-center gap-2 text-stone-500"> <div className="flex items-center justify-center gap-2 text-slate-500 dark:text-stone-500">
<Icon name="refresh-cw" size={16} className="animate-spin" /> <Icon name="refresh-cw" size={16} className="animate-spin" />
Loading deployments... Loading deployments...
</div> </div>
@@ -337,7 +337,7 @@ export function DeploymentsTable({ deployments, isLoading, onRefresh }: Deployme
</tr> </tr>
) : filteredDeployments.length === 0 ? ( ) : filteredDeployments.length === 0 ? (
<tr> <tr>
<td colSpan={columns.length} className="px-4 py-12 text-center text-stone-500"> <td colSpan={columns.length} className="px-4 py-12 text-center text-slate-500 dark:text-stone-500">
No deployments found No deployments found
</td> </td>
</tr> </tr>
@@ -345,8 +345,8 @@ export function DeploymentsTable({ deployments, isLoading, onRefresh }: Deployme
table.getRowModel().rows.map((row) => ( table.getRowModel().rows.map((row) => (
<Fragment key={row.id}> <Fragment key={row.id}>
<tr <tr
className={`border-b border-stone-800/50 hover:bg-stone-800/30 cursor-pointer transition-colors ${ className={`border-b border-slate-100 dark:border-stone-800/50 hover:bg-slate-50 dark:hover:bg-stone-800/30 cursor-pointer transition-colors ${
row.getIsExpanded() ? 'bg-stone-800/50' : '' row.getIsExpanded() ? 'bg-slate-50 dark:bg-stone-800/50' : ''
}`} }`}
onClick={() => row.toggleExpanded()} onClick={() => row.toggleExpanded()}
> >
@@ -371,13 +371,13 @@ export function DeploymentsTable({ deployments, isLoading, onRefresh }: Deployme
{/* Pagination */} {/* Pagination */}
{filteredDeployments.length > 10 && ( {filteredDeployments.length > 10 && (
<div className="flex items-center justify-between px-4 py-3 border-t border-stone-800"> <div className="flex items-center justify-between px-4 py-3 border-t border-slate-200 dark:border-stone-800">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-sm text-stone-500">Rows per page:</span> <span className="text-sm text-slate-500 dark:text-stone-500">Rows per page:</span>
<select <select
value={table.getState().pagination.pageSize} value={table.getState().pagination.pageSize}
onChange={(e) => table.setPageSize(Number(e.target.value))} onChange={(e) => table.setPageSize(Number(e.target.value))}
className="px-2 py-1 text-sm bg-stone-800 border border-stone-700 rounded text-stone-300 focus:outline-none" className="px-2 py-1 text-sm bg-slate-100 dark:bg-stone-800 border border-slate-200 dark:border-stone-700 rounded text-slate-700 dark:text-stone-300 focus:outline-none"
> >
{[10, 25, 50].map((size) => ( {[10, 25, 50].map((size) => (
<option key={size} value={size}> <option key={size} value={size}>
@@ -388,7 +388,7 @@ export function DeploymentsTable({ deployments, isLoading, onRefresh }: Deployme
</div> </div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<span className="text-sm text-stone-500"> <span className="text-sm text-slate-500 dark:text-stone-500">
Page {table.getState().pagination.pageIndex + 1} of{' '} Page {table.getState().pagination.pageIndex + 1} of{' '}
{table.getPageCount()} {table.getPageCount()}
</span> </span>
@@ -396,14 +396,14 @@ export function DeploymentsTable({ deployments, isLoading, onRefresh }: Deployme
<button <button
onClick={() => table.previousPage()} onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()} disabled={!table.getCanPreviousPage()}
className="p-1 text-stone-400 hover:text-stone-200 disabled:opacity-30 disabled:cursor-not-allowed" className="p-1 text-slate-400 dark:text-stone-400 hover:text-slate-600 dark:hover:text-stone-200 disabled:opacity-30 disabled:cursor-not-allowed"
> >
<Icon name="chevron-left" size={20} /> <Icon name="chevron-left" size={20} />
</button> </button>
<button <button
onClick={() => table.nextPage()} onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()} disabled={!table.getCanNextPage()}
className="p-1 text-stone-400 hover:text-stone-200 disabled:opacity-30 disabled:cursor-not-allowed" className="p-1 text-slate-400 dark:text-stone-400 hover:text-slate-600 dark:hover:text-stone-200 disabled:opacity-30 disabled:cursor-not-allowed"
> >
<Icon name="chevron-right" size={20} /> <Icon name="chevron-right" size={20} />
</button> </button>