feat: Added DebugModeToggle component and StatusFilter pill bar to Admi…
- "frontend/src/pages/AdminPipeline.tsx" - "frontend/src/api/public-client.ts" - "frontend/src/App.css" GSD-Task: S04/T01
This commit is contained in:
parent
dcadcbb5e2
commit
4186c6e208
3 changed files with 170 additions and 2 deletions
|
|
@ -541,6 +541,57 @@ a.app-footer__repo:hover {
|
|||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ── Debug Mode Toggle ────────────────────────────────────────────────────── */
|
||||
|
||||
.debug-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.debug-toggle__label {
|
||||
color: var(--color-text-on-header-label);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.debug-toggle__switch {
|
||||
position: relative;
|
||||
width: 2.5rem;
|
||||
height: 1.25rem;
|
||||
background: var(--color-toggle-track);
|
||||
border: none;
|
||||
border-radius: 9999px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.debug-toggle__switch--active {
|
||||
background: var(--color-toggle-review);
|
||||
}
|
||||
|
||||
.debug-toggle__switch::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0.125rem;
|
||||
left: 0.125rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
background: var(--color-toggle-thumb);
|
||||
border-radius: 50%;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.debug-toggle__switch--active::after {
|
||||
transform: translateX(1.25rem);
|
||||
}
|
||||
|
||||
.debug-toggle__switch:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ── Pagination ───────────────────────────────────────────────────────────── */
|
||||
|
||||
.pagination {
|
||||
|
|
|
|||
|
|
@ -471,3 +471,20 @@ export async function revokePipeline(videoId: string): Promise<RevokeResponse> {
|
|||
method: "POST",
|
||||
});
|
||||
}
|
||||
|
||||
// ── Debug Mode ──────────────────────────────────────────────────────────────
|
||||
|
||||
export interface DebugModeResponse {
|
||||
debug_mode: boolean;
|
||||
}
|
||||
|
||||
export async function fetchDebugMode(): Promise<DebugModeResponse> {
|
||||
return request<DebugModeResponse>(`${BASE}/admin/pipeline/debug-mode`);
|
||||
}
|
||||
|
||||
export async function setDebugMode(enabled: boolean): Promise<DebugModeResponse> {
|
||||
return request<DebugModeResponse>(`${BASE}/admin/pipeline/debug-mode`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ debug_mode: enabled }),
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import {
|
|||
fetchPipelineVideos,
|
||||
fetchPipelineEvents,
|
||||
fetchWorkerStatus,
|
||||
fetchDebugMode,
|
||||
setDebugMode,
|
||||
triggerPipeline,
|
||||
revokePipeline,
|
||||
type PipelineVideoItem,
|
||||
|
|
@ -364,6 +366,93 @@ function WorkerStatus() {
|
|||
);
|
||||
}
|
||||
|
||||
// ── Debug Mode Toggle ────────────────────────────────────────────────────────
|
||||
|
||||
function DebugModeToggle() {
|
||||
const [debugMode, setDebugModeState] = useState<boolean | null>(null);
|
||||
const [debugLoading, setDebugLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
fetchDebugMode()
|
||||
.then((res) => {
|
||||
if (!cancelled) setDebugModeState(res.debug_mode);
|
||||
})
|
||||
.catch(() => {
|
||||
// silently fail — toggle stays hidden
|
||||
});
|
||||
return () => { cancelled = true; };
|
||||
}, []);
|
||||
|
||||
async function handleToggle() {
|
||||
if (debugMode === null || debugLoading) return;
|
||||
setDebugLoading(true);
|
||||
try {
|
||||
const res = await setDebugMode(!debugMode);
|
||||
setDebugModeState(res.debug_mode);
|
||||
} catch {
|
||||
// swallow — leave previous state
|
||||
} finally {
|
||||
setDebugLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (debugMode === null) return null;
|
||||
|
||||
return (
|
||||
<div className="debug-toggle">
|
||||
<span className="debug-toggle__label">
|
||||
Debug {debugMode ? "On" : "Off"}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className={`debug-toggle__switch ${debugMode ? "debug-toggle__switch--active" : ""}`}
|
||||
onClick={handleToggle}
|
||||
disabled={debugLoading}
|
||||
aria-label={`Turn debug mode ${debugMode ? "off" : "on"}`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Status Filter ────────────────────────────────────────────────────────────
|
||||
|
||||
function StatusFilter({
|
||||
videos,
|
||||
activeFilter,
|
||||
onFilterChange,
|
||||
}: {
|
||||
videos: PipelineVideoItem[];
|
||||
activeFilter: string | null;
|
||||
onFilterChange: (filter: string | null) => void;
|
||||
}) {
|
||||
const statuses = Array.from(new Set(videos.map((v) => v.processing_status))).sort();
|
||||
|
||||
if (statuses.length <= 1) return null;
|
||||
|
||||
return (
|
||||
<div className="filter-tabs">
|
||||
<button
|
||||
type="button"
|
||||
className={`filter-tab ${activeFilter === null ? "filter-tab--active" : ""}`}
|
||||
onClick={() => onFilterChange(null)}
|
||||
>
|
||||
All
|
||||
</button>
|
||||
{statuses.map((status) => (
|
||||
<button
|
||||
key={status}
|
||||
type="button"
|
||||
className={`filter-tab ${activeFilter === status ? "filter-tab--active" : ""}`}
|
||||
onClick={() => onFilterChange(status)}
|
||||
>
|
||||
{status} ({videos.filter((v) => v.processing_status === status).length})
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Main Page ────────────────────────────────────────────────────────────────
|
||||
|
||||
export default function AdminPipeline() {
|
||||
|
|
@ -373,6 +462,7 @@ export default function AdminPipeline() {
|
|||
const [expandedId, setExpandedId] = useState<string | null>(null);
|
||||
const [actionLoading, setActionLoading] = useState<string | null>(null);
|
||||
const [actionMessage, setActionMessage] = useState<{ id: string; text: string; ok: boolean } | null>(null);
|
||||
const [activeFilter, setActiveFilter] = useState<string | null>(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
|
|
@ -448,6 +538,7 @@ export default function AdminPipeline() {
|
|||
</p>
|
||||
</div>
|
||||
<div className="admin-pipeline__header-right">
|
||||
<DebugModeToggle />
|
||||
<WorkerStatus />
|
||||
<button className="btn btn--secondary" onClick={() => void load()} disabled={loading}>
|
||||
↻ Refresh
|
||||
|
|
@ -462,8 +553,16 @@ export default function AdminPipeline() {
|
|||
) : videos.length === 0 ? (
|
||||
<div className="empty-state">No videos in pipeline.</div>
|
||||
) : (
|
||||
<div className="admin-pipeline__list">
|
||||
{videos.map((video) => (
|
||||
<>
|
||||
<StatusFilter
|
||||
videos={videos}
|
||||
activeFilter={activeFilter}
|
||||
onFilterChange={setActiveFilter}
|
||||
/>
|
||||
<div className="admin-pipeline__list">
|
||||
{videos
|
||||
.filter((v) => activeFilter === null || v.processing_status === activeFilter)
|
||||
.map((video) => (
|
||||
<div key={video.id} className="pipeline-video">
|
||||
<div
|
||||
className="pipeline-video__header"
|
||||
|
|
@ -530,6 +629,7 @@ export default function AdminPipeline() {
|
|||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue