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;
|
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 ───────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
.pagination {
|
.pagination {
|
||||||
|
|
|
||||||
|
|
@ -471,3 +471,20 @@ export async function revokePipeline(videoId: string): Promise<RevokeResponse> {
|
||||||
method: "POST",
|
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,
|
fetchPipelineVideos,
|
||||||
fetchPipelineEvents,
|
fetchPipelineEvents,
|
||||||
fetchWorkerStatus,
|
fetchWorkerStatus,
|
||||||
|
fetchDebugMode,
|
||||||
|
setDebugMode,
|
||||||
triggerPipeline,
|
triggerPipeline,
|
||||||
revokePipeline,
|
revokePipeline,
|
||||||
type PipelineVideoItem,
|
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 ────────────────────────────────────────────────────────────────
|
// ── Main Page ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export default function AdminPipeline() {
|
export default function AdminPipeline() {
|
||||||
|
|
@ -373,6 +462,7 @@ export default function AdminPipeline() {
|
||||||
const [expandedId, setExpandedId] = useState<string | null>(null);
|
const [expandedId, setExpandedId] = useState<string | null>(null);
|
||||||
const [actionLoading, setActionLoading] = useState<string | null>(null);
|
const [actionLoading, setActionLoading] = useState<string | null>(null);
|
||||||
const [actionMessage, setActionMessage] = useState<{ id: string; text: string; ok: boolean } | 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 () => {
|
const load = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
@ -448,6 +538,7 @@ export default function AdminPipeline() {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="admin-pipeline__header-right">
|
<div className="admin-pipeline__header-right">
|
||||||
|
<DebugModeToggle />
|
||||||
<WorkerStatus />
|
<WorkerStatus />
|
||||||
<button className="btn btn--secondary" onClick={() => void load()} disabled={loading}>
|
<button className="btn btn--secondary" onClick={() => void load()} disabled={loading}>
|
||||||
↻ Refresh
|
↻ Refresh
|
||||||
|
|
@ -462,8 +553,16 @@ export default function AdminPipeline() {
|
||||||
) : videos.length === 0 ? (
|
) : videos.length === 0 ? (
|
||||||
<div className="empty-state">No videos in pipeline.</div>
|
<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 key={video.id} className="pipeline-video">
|
||||||
<div
|
<div
|
||||||
className="pipeline-video__header"
|
className="pipeline-video__header"
|
||||||
|
|
@ -530,6 +629,7 @@ export default function AdminPipeline() {
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue