feat: Added Head/Tail segmented toggle to EventLog with order param wir…

- "frontend/src/api/public-client.ts"
- "frontend/src/pages/AdminPipeline.tsx"
- "frontend/src/App.css"

GSD-Task: S02/T02
This commit is contained in:
jlightner 2026-03-30 11:15:21 +00:00
parent cd9dd6d8f9
commit d00639a2ea
4 changed files with 58 additions and 4 deletions

View file

@ -2722,6 +2722,40 @@ body {
font-weight: 500; font-weight: 500;
} }
.pipeline-events__view-toggle {
display: inline-flex;
border: 1px solid var(--color-border);
border-radius: 6px;
overflow: hidden;
}
.pipeline-events__view-btn {
background: transparent;
border: none;
color: var(--color-text-secondary);
font-size: 0.75rem;
font-weight: 500;
padding: 0.25rem 0.625rem;
cursor: pointer;
font-family: inherit;
transition: background 0.15s, color 0.15s;
}
.pipeline-events__view-btn:hover {
color: var(--color-text-primary);
background: var(--color-accent-subtle);
}
.pipeline-events__view-btn--active {
background: var(--color-accent);
color: var(--color-bg-page);
}
.pipeline-events__view-btn--active:hover {
background: var(--color-accent-hover);
color: var(--color-bg-page);
}
.pipeline-events__empty { .pipeline-events__empty {
font-size: 0.85rem; font-size: 0.85rem;
color: var(--color-text-muted); color: var(--color-text-muted);

View file

@ -439,13 +439,14 @@ export async function fetchPipelineVideos(): Promise<PipelineVideoListResponse>
export async function fetchPipelineEvents( export async function fetchPipelineEvents(
videoId: string, videoId: string,
params: { offset?: number; limit?: number; stage?: string; event_type?: string } = {}, params: { offset?: number; limit?: number; stage?: string; event_type?: string; order?: "asc" | "desc" } = {},
): Promise<PipelineEventListResponse> { ): Promise<PipelineEventListResponse> {
const qs = new URLSearchParams(); const qs = new URLSearchParams();
if (params.offset !== undefined) qs.set("offset", String(params.offset)); if (params.offset !== undefined) qs.set("offset", String(params.offset));
if (params.limit !== undefined) qs.set("limit", String(params.limit)); if (params.limit !== undefined) qs.set("limit", String(params.limit));
if (params.stage) qs.set("stage", params.stage); if (params.stage) qs.set("stage", params.stage);
if (params.event_type) qs.set("event_type", params.event_type); if (params.event_type) qs.set("event_type", params.event_type);
if (params.order) qs.set("order", params.order);
const query = qs.toString(); const query = qs.toString();
return request<PipelineEventListResponse>( return request<PipelineEventListResponse>(
`${BASE}/admin/pipeline/events/${videoId}${query ? `?${query}` : ""}`, `${BASE}/admin/pipeline/events/${videoId}${query ? `?${query}` : ""}`,

View file

@ -103,13 +103,18 @@ function EventLog({ videoId }: { videoId: string }) {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [offset, setOffset] = useState(0); const [offset, setOffset] = useState(0);
const [viewMode, setViewMode] = useState<"head" | "tail">("tail");
const limit = 50; const limit = 50;
const load = useCallback(async () => { const load = useCallback(async () => {
setLoading(true); setLoading(true);
setError(null); setError(null);
try { try {
const res = await fetchPipelineEvents(videoId, { offset, limit }); const res = await fetchPipelineEvents(videoId, {
offset,
limit,
order: viewMode === "head" ? "asc" : "desc",
});
setEvents(res.items); setEvents(res.items);
setTotal(res.total); setTotal(res.total);
} catch (err) { } catch (err) {
@ -117,7 +122,7 @@ function EventLog({ videoId }: { videoId: string }) {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [videoId, offset]); }, [videoId, offset, viewMode]);
useEffect(() => { useEffect(() => {
void load(); void load();
@ -134,6 +139,20 @@ function EventLog({ videoId }: { videoId: string }) {
<div className="pipeline-events"> <div className="pipeline-events">
<div className="pipeline-events__header"> <div className="pipeline-events__header">
<span className="pipeline-events__count">{total} event{total !== 1 ? "s" : ""}</span> <span className="pipeline-events__count">{total} event{total !== 1 ? "s" : ""}</span>
<div className="pipeline-events__view-toggle">
<button
className={`pipeline-events__view-btn${viewMode === "head" ? " pipeline-events__view-btn--active" : ""}`}
onClick={() => { setViewMode("head"); setOffset(0); }}
>
Head
</button>
<button
className={`pipeline-events__view-btn${viewMode === "tail" ? " pipeline-events__view-btn--active" : ""}`}
onClick={() => { setViewMode("tail"); setOffset(0); }}
>
Tail
</button>
</div>
<button className="btn btn--small btn--secondary" onClick={() => void load()}> Refresh</button> <button className="btn btn--small btn--secondary" onClick={() => void load()}> Refresh</button>
</div> </div>

View file

@ -1 +1 @@
{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/client.ts","./src/api/public-client.ts","./src/components/ModeToggle.tsx","./src/components/ReportIssueModal.tsx","./src/components/StatusBadge.tsx","./src/pages/AdminPipeline.tsx","./src/pages/AdminReports.tsx","./src/pages/CreatorDetail.tsx","./src/pages/CreatorsBrowse.tsx","./src/pages/Home.tsx","./src/pages/MomentDetail.tsx","./src/pages/ReviewQueue.tsx","./src/pages/SearchResults.tsx","./src/pages/TechniquePage.tsx","./src/pages/TopicsBrowse.tsx"],"version":"5.6.3"} {"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/client.ts","./src/api/public-client.ts","./src/components/AdminDropdown.tsx","./src/components/ModeToggle.tsx","./src/components/ReportIssueModal.tsx","./src/components/StatusBadge.tsx","./src/pages/AdminPipeline.tsx","./src/pages/AdminReports.tsx","./src/pages/CreatorDetail.tsx","./src/pages/CreatorsBrowse.tsx","./src/pages/Home.tsx","./src/pages/MomentDetail.tsx","./src/pages/ReviewQueue.tsx","./src/pages/SearchResults.tsx","./src/pages/TechniquePage.tsx","./src/pages/TopicsBrowse.tsx"],"version":"5.6.3"}