change tabs
All checks were successful
Build and Deploy Log Center / build-and-deploy (push) Successful in 1m23s
All checks were successful
Build and Deploy Log Center / build-and-deploy (push) Successful in 1m23s
This commit is contained in:
parent
d857748314
commit
2d03f01ecc
@ -273,6 +273,148 @@ tr:hover {
|
|||||||
border-color: var(--accent);
|
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 */
|
/* Bug Detail */
|
||||||
.detail-card {
|
.detail-card {
|
||||||
background: var(--bg-card);
|
background: var(--bg-card);
|
||||||
|
|||||||
@ -1,12 +1,16 @@
|
|||||||
import { useState, useEffect } from 'react';
|
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';
|
import { getBugDetail, type ErrorLog } from '../api';
|
||||||
|
|
||||||
export default function BugDetail() {
|
export default function BugDetail() {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
|
const location = useLocation();
|
||||||
const [bug, setBug] = useState<ErrorLog | null>(null);
|
const [bug, setBug] = useState<ErrorLog | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
// Preserve search params from the list page for back navigation
|
||||||
|
const backSearch = location.state?.fromSearch || '';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchBug = async () => {
|
const fetchBug = async () => {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
@ -36,7 +40,7 @@ export default function BugDetail() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Link to="/bugs" className="back-link">
|
<Link to={`/bugs${backSearch ? `?${backSearch}` : ''}`} className="back-link">
|
||||||
← Back to Bug List
|
← Back to Bug List
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
|
|||||||
@ -1,16 +1,43 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link, useSearchParams } from 'react-router-dom';
|
||||||
import { getBugs, getProjects, type ErrorLog } from '../api';
|
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() {
|
export default function BugList() {
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
const [bugs, setBugs] = useState<ErrorLog[]>([]);
|
const [bugs, setBugs] = useState<ErrorLog[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [page, setPage] = useState(1);
|
|
||||||
const [totalPages, setTotalPages] = useState(1);
|
const [totalPages, setTotalPages] = useState(1);
|
||||||
const [statusFilter, setStatusFilter] = useState('');
|
|
||||||
const [projectFilter, setProjectFilter] = useState('');
|
|
||||||
const [projects, setProjects] = useState<string[]>([]);
|
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(() => {
|
useEffect(() => {
|
||||||
const fetchProjects = async () => {
|
const fetchProjects = async () => {
|
||||||
try {
|
try {
|
||||||
@ -27,9 +54,9 @@ export default function BugList() {
|
|||||||
const fetchBugs = async () => {
|
const fetchBugs = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const params: any = { page, page_size: 20 };
|
const params: Record<string, string | number> = { page: currentPage, page_size: 20 };
|
||||||
if (statusFilter) params.status = statusFilter;
|
if (currentStatus) params.status = currentStatus;
|
||||||
if (projectFilter) params.project_id = projectFilter;
|
if (currentProject) params.project_id = currentProject;
|
||||||
|
|
||||||
const response = await getBugs(params);
|
const response = await getBugs(params);
|
||||||
setBugs(response.data.items);
|
setBugs(response.data.items);
|
||||||
@ -41,12 +68,7 @@ export default function BugList() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
fetchBugs();
|
fetchBugs();
|
||||||
}, [page, statusFilter, projectFilter]);
|
}, [currentPage, currentStatus, currentProject]);
|
||||||
|
|
||||||
const statuses = [
|
|
||||||
'NEW', 'VERIFYING', 'CANNOT_REPRODUCE', 'PENDING_FIX',
|
|
||||||
'FIXING', 'FIXED', 'VERIFIED', 'DEPLOYED', 'FIX_FAILED'
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -55,28 +77,42 @@ export default function BugList() {
|
|||||||
<p className="page-subtitle">All reported errors and their current status</p>
|
<p className="page-subtitle">All reported errors and their current status</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="filters">
|
{/* Project breadcrumb navigation */}
|
||||||
<select
|
<div className="project-tabs">
|
||||||
className="filter-select"
|
<button
|
||||||
value={statusFilter}
|
className={`project-tab ${currentProject === '' ? 'active' : ''}`}
|
||||||
onChange={(e) => { setStatusFilter(e.target.value); setPage(1); }}
|
onClick={() => updateParams({ project: '' })}
|
||||||
>
|
>
|
||||||
<option value="">All Status</option>
|
All Projects
|
||||||
{statuses.map(s => (
|
</button>
|
||||||
<option key={s} value={s}>{s}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select
|
|
||||||
className="filter-select"
|
|
||||||
value={projectFilter}
|
|
||||||
onChange={(e) => { setProjectFilter(e.target.value); setPage(1); }}
|
|
||||||
>
|
|
||||||
<option value="">All Projects</option>
|
|
||||||
{projects.map(p => (
|
{projects.map(p => (
|
||||||
<option key={p} value={p}>{p}</option>
|
<button
|
||||||
|
key={p}
|
||||||
|
className={`project-tab ${currentProject === p ? 'active' : ''}`}
|
||||||
|
onClick={() => updateParams({ project: p })}
|
||||||
|
>
|
||||||
|
{p}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status filter tabs */}
|
||||||
|
<div className="status-tabs">
|
||||||
|
<button
|
||||||
|
className={`status-tab ${currentStatus === '' ? 'active' : ''}`}
|
||||||
|
onClick={() => updateParams({ status: '' })}
|
||||||
|
>
|
||||||
|
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>
|
||||||
))}
|
))}
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="table-container">
|
<div className="table-container">
|
||||||
@ -84,6 +120,12 @@ export default function BugList() {
|
|||||||
<div className="loading">
|
<div className="loading">
|
||||||
<div className="spinner"></div>
|
<div className="spinner"></div>
|
||||||
</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>
|
<table>
|
||||||
@ -101,11 +143,22 @@ export default function BugList() {
|
|||||||
{bugs.map(bug => (
|
{bugs.map(bug => (
|
||||||
<tr key={bug.id}>
|
<tr key={bug.id}>
|
||||||
<td>
|
<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}
|
#{bug.id}
|
||||||
</Link>
|
</Link>
|
||||||
</td>
|
</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={{ color: 'var(--error)' }}>{bug.error_type}</td>
|
||||||
<td style={{ fontFamily: 'monospace', fontSize: '13px' }}>
|
<td style={{ fontFamily: 'monospace', fontSize: '13px' }}>
|
||||||
{bug.file_path}:{bug.line_number}
|
{bug.file_path}:{bug.line_number}
|
||||||
@ -123,23 +176,25 @@ export default function BugList() {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
{totalPages > 1 && (
|
||||||
<div className="pagination">
|
<div className="pagination">
|
||||||
<button
|
<button
|
||||||
onClick={() => setPage(p => Math.max(1, p - 1))}
|
onClick={() => updateParams({ page: String(Math.max(1, currentPage - 1)) })}
|
||||||
disabled={page === 1}
|
disabled={currentPage === 1}
|
||||||
>
|
>
|
||||||
Previous
|
Previous
|
||||||
</button>
|
</button>
|
||||||
<span style={{ padding: '8px 16px', color: 'var(--text-secondary)' }}>
|
<span style={{ padding: '8px 16px', color: 'var(--text-secondary)' }}>
|
||||||
Page {page} of {totalPages}
|
Page {currentPage} of {totalPages}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
|
onClick={() => updateParams({ page: String(Math.min(totalPages, currentPage + 1)) })}
|
||||||
disabled={page === totalPages}
|
disabled={currentPage === totalPages}
|
||||||
>
|
>
|
||||||
Next
|
Next
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user