change tabs
All checks were successful
Build and Deploy Log Center / build-and-deploy (push) Successful in 1m23s

This commit is contained in:
zyc 2026-02-10 13:20:07 +08:00
parent d857748314
commit 2d03f01ecc
3 changed files with 255 additions and 54 deletions

View File

@ -273,6 +273,148 @@ tr:hover {
border-color: var(--accent);
}
/* Project Tabs (breadcrumb navigation) */
.project-tabs {
display: flex;
align-items: center;
gap: 4px;
margin-bottom: 16px;
padding: 6px;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 12px;
overflow-x: auto;
}
.project-tab {
position: relative;
background: none;
border: none;
padding: 10px 20px;
color: var(--text-secondary);
font-size: 14px;
font-weight: 500;
cursor: pointer;
border-radius: 8px;
white-space: nowrap;
transition: all 0.2s ease;
}
.project-tab:hover {
color: var(--text-primary);
background: rgba(255, 255, 255, 0.05);
}
.project-tab.active {
background: var(--accent);
color: var(--bg-primary);
font-weight: 600;
}
/* Status Tabs */
.status-tabs {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 20px;
overflow-x: auto;
padding-bottom: 4px;
}
.status-tab {
background: none;
border: 1px solid var(--border);
padding: 6px 14px;
color: var(--text-secondary);
font-size: 12px;
font-weight: 500;
cursor: pointer;
border-radius: 20px;
white-space: nowrap;
transition: all 0.2s ease;
text-transform: uppercase;
letter-spacing: 0.3px;
}
.status-tab:hover {
border-color: var(--text-secondary);
color: var(--text-primary);
}
.status-tab.active {
border-color: var(--accent);
background: rgba(0, 212, 255, 0.15);
color: var(--accent);
}
.status-tab.active.status-color-NEW {
border-color: var(--accent);
background: rgba(0, 212, 255, 0.15);
color: var(--accent);
}
.status-tab.active.status-color-FIXED,
.status-tab.active.status-color-VERIFIED,
.status-tab.active.status-color-DEPLOYED {
border-color: var(--success);
background: rgba(0, 230, 118, 0.15);
color: var(--success);
}
.status-tab.active.status-color-PENDING_FIX {
border-color: var(--warning);
background: rgba(255, 171, 0, 0.15);
color: var(--warning);
}
.status-tab.active.status-color-FIX_FAILED {
border-color: var(--error);
background: rgba(255, 82, 82, 0.15);
color: var(--error);
}
.status-tab.active.status-color-FIXING,
.status-tab.active.status-color-VERIFYING {
border-color: var(--accent-secondary);
background: rgba(123, 44, 191, 0.15);
color: #b388ff;
}
.status-tab.active.status-color-CANNOT_REPRODUCE {
border-color: var(--text-secondary);
background: rgba(160, 160, 176, 0.15);
color: var(--text-secondary);
}
/* Project link in table */
.project-link {
background: none;
border: none;
color: var(--text-primary);
font-size: inherit;
font-family: inherit;
cursor: pointer;
padding: 2px 8px;
border-radius: 4px;
transition: all 0.2s ease;
}
.project-link:hover {
background: rgba(0, 212, 255, 0.1);
color: var(--accent);
}
/* Empty state */
.empty-state {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
color: var(--text-secondary);
font-size: 15px;
gap: 4px;
}
/* Bug Detail */
.detail-card {
background: var(--bg-card);

View File

@ -1,12 +1,16 @@
import { useState, useEffect } from 'react';
import { useParams, Link } from 'react-router-dom';
import { useParams, Link, useLocation } from 'react-router-dom';
import { getBugDetail, type ErrorLog } from '../api';
export default function BugDetail() {
const { id } = useParams<{ id: string }>();
const location = useLocation();
const [bug, setBug] = useState<ErrorLog | null>(null);
const [loading, setLoading] = useState(true);
// Preserve search params from the list page for back navigation
const backSearch = location.state?.fromSearch || '';
useEffect(() => {
const fetchBug = async () => {
if (!id) return;
@ -36,7 +40,7 @@ export default function BugDetail() {
return (
<div>
<Link to="/bugs" className="back-link">
<Link to={`/bugs${backSearch ? `?${backSearch}` : ''}`} className="back-link">
Back to Bug List
</Link>

View File

@ -1,16 +1,43 @@
import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { useState, useEffect, useCallback } from 'react';
import { Link, useSearchParams } from 'react-router-dom';
import { getBugs, getProjects, type ErrorLog } from '../api';
const STATUSES = [
'NEW', 'VERIFYING', 'CANNOT_REPRODUCE', 'PENDING_FIX',
'FIXING', 'FIXED', 'VERIFIED', 'DEPLOYED', 'FIX_FAILED'
];
export default function BugList() {
const [searchParams, setSearchParams] = useSearchParams();
const [bugs, setBugs] = useState<ErrorLog[]>([]);
const [loading, setLoading] = useState(true);
const [page, setPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [statusFilter, setStatusFilter] = useState('');
const [projectFilter, setProjectFilter] = useState('');
const [projects, setProjects] = useState<string[]>([]);
// Read filters from URL params, default status to NEW
const currentProject = searchParams.get('project') || '';
const currentStatus = searchParams.get('status') ?? 'NEW';
const currentPage = parseInt(searchParams.get('page') || '1', 10);
const updateParams = useCallback((updates: Record<string, string>) => {
setSearchParams(prev => {
const next = new URLSearchParams(prev);
for (const [key, value] of Object.entries(updates)) {
if (value) {
next.set(key, value);
} else {
next.delete(key);
}
}
// Reset page when changing filters
if ('project' in updates || 'status' in updates) {
next.delete('page');
}
return next;
});
}, [setSearchParams]);
useEffect(() => {
const fetchProjects = async () => {
try {
@ -27,9 +54,9 @@ export default function BugList() {
const fetchBugs = async () => {
setLoading(true);
try {
const params: any = { page, page_size: 20 };
if (statusFilter) params.status = statusFilter;
if (projectFilter) params.project_id = projectFilter;
const params: Record<string, string | number> = { page: currentPage, page_size: 20 };
if (currentStatus) params.status = currentStatus;
if (currentProject) params.project_id = currentProject;
const response = await getBugs(params);
setBugs(response.data.items);
@ -41,12 +68,7 @@ export default function BugList() {
}
};
fetchBugs();
}, [page, statusFilter, projectFilter]);
const statuses = [
'NEW', 'VERIFYING', 'CANNOT_REPRODUCE', 'PENDING_FIX',
'FIXING', 'FIXED', 'VERIFIED', 'DEPLOYED', 'FIX_FAILED'
];
}, [currentPage, currentStatus, currentProject]);
return (
<div>
@ -55,28 +77,42 @@ export default function BugList() {
<p className="page-subtitle">All reported errors and their current status</p>
</div>
<div className="filters">
<select
className="filter-select"
value={statusFilter}
onChange={(e) => { setStatusFilter(e.target.value); setPage(1); }}
{/* Project breadcrumb navigation */}
<div className="project-tabs">
<button
className={`project-tab ${currentProject === '' ? 'active' : ''}`}
onClick={() => updateParams({ project: '' })}
>
<option value="">All Status</option>
{statuses.map(s => (
<option key={s} value={s}>{s}</option>
))}
</select>
All Projects
</button>
{projects.map(p => (
<button
key={p}
className={`project-tab ${currentProject === p ? 'active' : ''}`}
onClick={() => updateParams({ project: p })}
>
{p}
</button>
))}
</div>
<select
className="filter-select"
value={projectFilter}
onChange={(e) => { setProjectFilter(e.target.value); setPage(1); }}
{/* Status filter tabs */}
<div className="status-tabs">
<button
className={`status-tab ${currentStatus === '' ? 'active' : ''}`}
onClick={() => updateParams({ status: '' })}
>
<option value="">All Projects</option>
{projects.map(p => (
<option key={p} value={p}>{p}</option>
))}
</select>
All Status
</button>
{STATUSES.map(s => (
<button
key={s}
className={`status-tab ${currentStatus === s ? 'active' : ''} status-color-${s}`}
onClick={() => updateParams({ status: s })}
>
{s.replace('_', ' ')}
</button>
))}
</div>
<div className="table-container">
@ -84,6 +120,12 @@ export default function BugList() {
<div className="loading">
<div className="spinner"></div>
</div>
) : bugs.length === 0 ? (
<div className="empty-state">
No bugs found
{currentProject && <span> in <strong>{currentProject}</strong></span>}
{currentStatus && <span> with status <strong>{currentStatus}</strong></span>}
</div>
) : (
<>
<table>
@ -101,11 +143,22 @@ export default function BugList() {
{bugs.map(bug => (
<tr key={bug.id}>
<td>
<Link to={`/bugs/${bug.id}`} style={{ color: 'var(--accent)' }}>
<Link
to={`/bugs/${bug.id}`}
state={{ fromSearch: searchParams.toString() }}
style={{ color: 'var(--accent)' }}
>
#{bug.id}
</Link>
</td>
<td>{bug.project_id}</td>
<td>
<button
className="project-link"
onClick={() => updateParams({ project: bug.project_id })}
>
{bug.project_id}
</button>
</td>
<td style={{ color: 'var(--error)' }}>{bug.error_type}</td>
<td style={{ fontFamily: 'monospace', fontSize: '13px' }}>
{bug.file_path}:{bug.line_number}
@ -123,23 +176,25 @@ export default function BugList() {
</tbody>
</table>
<div className="pagination">
<button
onClick={() => setPage(p => Math.max(1, p - 1))}
disabled={page === 1}
>
Previous
</button>
<span style={{ padding: '8px 16px', color: 'var(--text-secondary)' }}>
Page {page} of {totalPages}
</span>
<button
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
disabled={page === totalPages}
>
Next
</button>
</div>
{totalPages > 1 && (
<div className="pagination">
<button
onClick={() => updateParams({ page: String(Math.max(1, currentPage - 1)) })}
disabled={currentPage === 1}
>
Previous
</button>
<span style={{ padding: '8px 16px', color: 'var(--text-secondary)' }}>
Page {currentPage} of {totalPages}
</span>
<button
onClick={() => updateParams({ page: String(Math.min(totalPages, currentPage + 1)) })}
disabled={currentPage === totalPages}
>
Next
</button>
</div>
)}
</>
)}
</div>