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:
@@ -78,13 +78,13 @@ export function DeploymentLogs({ deploymentUuid, status, initialLogs }: Deployme
|
||||
};
|
||||
|
||||
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 */}
|
||||
<div className="flex items-center justify-between px-4 py-2 border-b border-stone-800">
|
||||
<span className="text-sm font-medium text-stone-300">Build Logs</span>
|
||||
<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-slate-700 dark:text-stone-300">Build Logs</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{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">
|
||||
<Icon name="refresh-cw" size={12} />
|
||||
</span>
|
||||
@@ -92,8 +92,11 @@ export function DeploymentLogs({ deploymentUuid, status, initialLogs }: Deployme
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
onClick={copyToClipboard}
|
||||
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"
|
||||
onClick={(e) => {
|
||||
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} />
|
||||
{copied ? 'Copied' : 'Copy'}
|
||||
@@ -105,10 +108,11 @@ export function DeploymentLogs({ deploymentUuid, status, initialLogs }: Deployme
|
||||
<div
|
||||
ref={containerRef}
|
||||
onScroll={handleScroll}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="max-h-80 overflow-y-auto font-mono text-xs p-4 space-y-0.5"
|
||||
>
|
||||
{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'}
|
||||
</div>
|
||||
) : (
|
||||
@@ -116,11 +120,11 @@ export function DeploymentLogs({ deploymentUuid, status, initialLogs }: Deployme
|
||||
<div
|
||||
key={index}
|
||||
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 && (
|
||||
<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}
|
||||
</span>
|
||||
)}
|
||||
@@ -134,11 +138,12 @@ export function DeploymentLogs({ deploymentUuid, status, initialLogs }: Deployme
|
||||
{/* Auto-scroll indicator */}
|
||||
{!autoScroll && logs.length > 0 && (
|
||||
<button
|
||||
onClick={() => {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setAutoScroll(true);
|
||||
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
|
||||
</button>
|
||||
|
||||
@@ -46,7 +46,7 @@ const StatusDot = ({ status }: { status: DeploymentStatus }) => (
|
||||
const StatusBadge = ({ status }: { status: DeploymentStatus }) => (
|
||||
<span className="flex items-center gap-1.5">
|
||||
<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>
|
||||
);
|
||||
|
||||
@@ -81,7 +81,7 @@ export function DeploymentsTable({ deployments, isLoading, onRefresh }: Deployme
|
||||
cell: ({ row, getValue }) => (
|
||||
<button
|
||||
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
|
||||
name="chevron-right"
|
||||
@@ -96,11 +96,11 @@ export function DeploymentsTable({ deployments, isLoading, onRefresh }: Deployme
|
||||
header: 'Environment',
|
||||
cell: ({ getValue }) => (
|
||||
<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
|
||||
</span>
|
||||
{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} />
|
||||
Current
|
||||
</span>
|
||||
@@ -113,7 +113,7 @@ export function DeploymentsTable({ deployments, isLoading, onRefresh }: Deployme
|
||||
cell: ({ getValue }) => {
|
||||
const duration = getValue();
|
||||
return (
|
||||
<span className="text-stone-400 text-sm">
|
||||
<span className="text-slate-500 dark:text-stone-400 text-sm">
|
||||
{duration ? formatDuration(duration) : '-'}
|
||||
</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}`}
|
||||
target="_blank"
|
||||
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()}
|
||||
>
|
||||
<span className="w-5 h-5 flex items-center justify-center rounded bg-stone-800">
|
||||
<Icon name="box" size={12} className="text-stone-400" />
|
||||
<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-slate-500 dark:text-stone-400" />
|
||||
</span>
|
||||
{getValue()}
|
||||
</a>
|
||||
@@ -144,16 +144,16 @@ export function DeploymentsTable({ deployments, isLoading, onRefresh }: Deployme
|
||||
header: 'Source',
|
||||
cell: ({ getValue, row }) => (
|
||||
<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} />
|
||||
</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 && (
|
||||
<>
|
||||
<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)}
|
||||
</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 || '')}
|
||||
</span>
|
||||
</>
|
||||
@@ -164,9 +164,9 @@ export function DeploymentsTable({ deployments, isLoading, onRefresh }: Deployme
|
||||
columnHelper.accessor('created_at', {
|
||||
header: 'Created',
|
||||
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 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>
|
||||
</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}`}
|
||||
target="_blank"
|
||||
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()}
|
||||
>
|
||||
<Icon name="external-link" size={16} />
|
||||
@@ -235,7 +235,7 @@ export function DeploymentsTable({ deployments, isLoading, onRefresh }: Deployme
|
||||
<select
|
||||
value={appFilter}
|
||||
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>
|
||||
{applicationNames.map((name) => (
|
||||
@@ -249,7 +249,7 @@ export function DeploymentsTable({ deployments, isLoading, onRefresh }: Deployme
|
||||
<select
|
||||
value={statusFilter}
|
||||
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="finished">Ready</option>
|
||||
@@ -265,7 +265,7 @@ export function DeploymentsTable({ deployments, isLoading, onRefresh }: Deployme
|
||||
setStatusFilter('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
|
||||
</button>
|
||||
@@ -273,13 +273,13 @@ export function DeploymentsTable({ deployments, isLoading, onRefresh }: Deployme
|
||||
</div>
|
||||
|
||||
<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
|
||||
</span>
|
||||
<button
|
||||
onClick={onRefresh}
|
||||
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
|
||||
name="refresh-cw"
|
||||
@@ -292,21 +292,21 @@ export function DeploymentsTable({ deployments, isLoading, onRefresh }: Deployme
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
<thead>
|
||||
{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) => (
|
||||
<th
|
||||
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 : (
|
||||
<button
|
||||
className={`flex items-center gap-1 ${
|
||||
header.column.getCanSort()
|
||||
? 'cursor-pointer hover:text-stone-300'
|
||||
? 'cursor-pointer hover:text-slate-700 dark:hover:text-stone-300'
|
||||
: ''
|
||||
}`}
|
||||
onClick={header.column.getToggleSortingHandler()}
|
||||
@@ -329,7 +329,7 @@ export function DeploymentsTable({ deployments, isLoading, onRefresh }: Deployme
|
||||
{isLoading && deployments.length === 0 ? (
|
||||
<tr>
|
||||
<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" />
|
||||
Loading deployments...
|
||||
</div>
|
||||
@@ -337,7 +337,7 @@ export function DeploymentsTable({ deployments, isLoading, onRefresh }: Deployme
|
||||
</tr>
|
||||
) : filteredDeployments.length === 0 ? (
|
||||
<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
|
||||
</td>
|
||||
</tr>
|
||||
@@ -345,8 +345,8 @@ export function DeploymentsTable({ deployments, isLoading, onRefresh }: Deployme
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<Fragment key={row.id}>
|
||||
<tr
|
||||
className={`border-b border-stone-800/50 hover:bg-stone-800/30 cursor-pointer transition-colors ${
|
||||
row.getIsExpanded() ? 'bg-stone-800/50' : ''
|
||||
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-slate-50 dark:bg-stone-800/50' : ''
|
||||
}`}
|
||||
onClick={() => row.toggleExpanded()}
|
||||
>
|
||||
@@ -371,13 +371,13 @@ export function DeploymentsTable({ deployments, isLoading, onRefresh }: Deployme
|
||||
|
||||
{/* Pagination */}
|
||||
{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">
|
||||
<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
|
||||
value={table.getState().pagination.pageSize}
|
||||
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) => (
|
||||
<option key={size} value={size}>
|
||||
@@ -388,7 +388,7 @@ export function DeploymentsTable({ deployments, isLoading, onRefresh }: Deployme
|
||||
</div>
|
||||
|
||||
<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{' '}
|
||||
{table.getPageCount()}
|
||||
</span>
|
||||
@@ -396,14 +396,14 @@ export function DeploymentsTable({ deployments, isLoading, onRefresh }: Deployme
|
||||
<button
|
||||
onClick={() => table.previousPage()}
|
||||
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} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => table.nextPage()}
|
||||
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} />
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user