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:
parent
bf126f4825
commit
ee24731e59
7 changed files with 148 additions and 5 deletions
|
|
@ -34,7 +34,7 @@
|
|||
- Estimate: 20m
|
||||
- Files: backend/routers/pipeline.py (on ub01: /vmPool/r/repos/xpltdco/chrysopedia/backend/routers/pipeline.py)
|
||||
- Verify: curl -s 'http://ub01:8096/api/v1/admin/pipeline/events/{video_id}?order=asc&limit=3' returns events in ascending timestamp order; curl with order=invalid returns HTTP 400
|
||||
- [ ] **T02: Add Head/Tail toggle to EventLog component with API wiring and CSS** — Add a Head/Tail segmented toggle to the EventLog component in AdminPipeline.tsx. Wire it to pass the `order` param through the API client. Add compact segmented-button CSS.
|
||||
- [x] **T02: Added Head/Tail segmented toggle to EventLog with order param wiring — Head shows oldest-first (asc), Tail shows newest-first (desc), switching resets pagination** — Add a Head/Tail segmented toggle to the EventLog component in AdminPipeline.tsx. Wire it to pass the `order` param through the API client. Add compact segmented-button CSS.
|
||||
|
||||
## Steps
|
||||
|
||||
|
|
|
|||
9
.gsd/milestones/M006/slices/S02/tasks/T01-VERIFY.json
Normal file
9
.gsd/milestones/M006/slices/S02/tasks/T01-VERIFY.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"schemaVersion": 1,
|
||||
"taskId": "T01",
|
||||
"unitId": "M006/S02/T01",
|
||||
"timestamp": 1774869044959,
|
||||
"passed": true,
|
||||
"discoverySource": "none",
|
||||
"checks": []
|
||||
}
|
||||
80
.gsd/milestones/M006/slices/S02/tasks/T02-SUMMARY.md
Normal file
80
.gsd/milestones/M006/slices/S02/tasks/T02-SUMMARY.md
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
---
|
||||
id: T02
|
||||
parent: S02
|
||||
milestone: M006
|
||||
provides: []
|
||||
requires: []
|
||||
affects: []
|
||||
key_files: ["frontend/src/api/public-client.ts", "frontend/src/pages/AdminPipeline.tsx", "frontend/src/App.css"]
|
||||
key_decisions: ["Placed segmented toggle between event count and refresh button in the header row for natural scan order"]
|
||||
patterns_established: []
|
||||
drill_down_paths: []
|
||||
observability_surfaces: []
|
||||
duration: ""
|
||||
verification_result: "Frontend build passed with zero TypeScript errors. Browser verification confirmed Head shows oldest events first (ascending timestamps), Tail shows newest first (descending). Token counts visible on all event rows and video summary. Pager functional in both modes. Slice-level curl checks: order=asc returns ascending timestamps, order=invalid returns HTTP 400."
|
||||
completed_at: 2026-03-30T11:15:10.303Z
|
||||
blocker_discovered: false
|
||||
---
|
||||
|
||||
# T02: Added Head/Tail segmented toggle to EventLog with order param wiring — Head shows oldest-first (asc), Tail shows newest-first (desc), switching resets pagination
|
||||
|
||||
> Added Head/Tail segmented toggle to EventLog with order param wiring — Head shows oldest-first (asc), Tail shows newest-first (desc), switching resets pagination
|
||||
|
||||
## What Happened
|
||||
---
|
||||
id: T02
|
||||
parent: S02
|
||||
milestone: M006
|
||||
key_files:
|
||||
- frontend/src/api/public-client.ts
|
||||
- frontend/src/pages/AdminPipeline.tsx
|
||||
- frontend/src/App.css
|
||||
key_decisions:
|
||||
- Placed segmented toggle between event count and refresh button in the header row for natural scan order
|
||||
duration: ""
|
||||
verification_result: passed
|
||||
completed_at: 2026-03-30T11:15:10.304Z
|
||||
blocker_discovered: false
|
||||
---
|
||||
|
||||
# T02: Added Head/Tail segmented toggle to EventLog with order param wiring — Head shows oldest-first (asc), Tail shows newest-first (desc), switching resets pagination
|
||||
|
||||
**Added Head/Tail segmented toggle to EventLog with order param wiring — Head shows oldest-first (asc), Tail shows newest-first (desc), switching resets pagination**
|
||||
|
||||
## What Happened
|
||||
|
||||
Added `order` param to `fetchPipelineEvents` in the API client. In the EventLog component, added `viewMode` state with Head/Tail toggle buttons wired to pass `order=asc` or `order=desc`. Switching mode resets offset to 0. Added compact segmented-button CSS using existing CSS custom properties. Built and deployed to ub01 — verified both modes work correctly with ascending/descending event ordering, token counts remain visible, and the pager still functions.
|
||||
|
||||
## Verification
|
||||
|
||||
Frontend build passed with zero TypeScript errors. Browser verification confirmed Head shows oldest events first (ascending timestamps), Tail shows newest first (descending). Token counts visible on all event rows and video summary. Pager functional in both modes. Slice-level curl checks: order=asc returns ascending timestamps, order=invalid returns HTTP 400.
|
||||
|
||||
## Verification Evidence
|
||||
|
||||
| # | Command | Exit Code | Verdict | Duration |
|
||||
|---|---------|-----------|---------|----------|
|
||||
| 1 | `cd frontend && npm run build` | 0 | ✅ pass | 4200ms |
|
||||
| 2 | `curl -sf 'http://ub01:8096/api/v1/admin/pipeline/events/{vid}?order=asc&limit=3'` | 0 | ✅ pass | 800ms |
|
||||
| 3 | `curl -s -w '%{http_code}' 'http://ub01:8096/api/v1/admin/pipeline/events/{vid}?order=invalid'` | 0 | ✅ pass (400) | 500ms |
|
||||
|
||||
|
||||
## Deviations
|
||||
|
||||
None.
|
||||
|
||||
## Known Issues
|
||||
|
||||
None.
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `frontend/src/api/public-client.ts`
|
||||
- `frontend/src/pages/AdminPipeline.tsx`
|
||||
- `frontend/src/App.css`
|
||||
|
||||
|
||||
## Deviations
|
||||
None.
|
||||
|
||||
## Known Issues
|
||||
None.
|
||||
|
|
@ -2722,6 +2722,40 @@ body {
|
|||
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 {
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-text-muted);
|
||||
|
|
|
|||
|
|
@ -439,13 +439,14 @@ export async function fetchPipelineVideos(): Promise<PipelineVideoListResponse>
|
|||
|
||||
export async function fetchPipelineEvents(
|
||||
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> {
|
||||
const qs = new URLSearchParams();
|
||||
if (params.offset !== undefined) qs.set("offset", String(params.offset));
|
||||
if (params.limit !== undefined) qs.set("limit", String(params.limit));
|
||||
if (params.stage) qs.set("stage", params.stage);
|
||||
if (params.event_type) qs.set("event_type", params.event_type);
|
||||
if (params.order) qs.set("order", params.order);
|
||||
const query = qs.toString();
|
||||
return request<PipelineEventListResponse>(
|
||||
`${BASE}/admin/pipeline/events/${videoId}${query ? `?${query}` : ""}`,
|
||||
|
|
|
|||
|
|
@ -103,13 +103,18 @@ function EventLog({ videoId }: { videoId: string }) {
|
|||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [offset, setOffset] = useState(0);
|
||||
const [viewMode, setViewMode] = useState<"head" | "tail">("tail");
|
||||
const limit = 50;
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await fetchPipelineEvents(videoId, { offset, limit });
|
||||
const res = await fetchPipelineEvents(videoId, {
|
||||
offset,
|
||||
limit,
|
||||
order: viewMode === "head" ? "asc" : "desc",
|
||||
});
|
||||
setEvents(res.items);
|
||||
setTotal(res.total);
|
||||
} catch (err) {
|
||||
|
|
@ -117,7 +122,7 @@ function EventLog({ videoId }: { videoId: string }) {
|
|||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [videoId, offset]);
|
||||
}, [videoId, offset, viewMode]);
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
|
|
@ -134,6 +139,20 @@ function EventLog({ videoId }: { videoId: string }) {
|
|||
<div className="pipeline-events">
|
||||
<div className="pipeline-events__header">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
Loading…
Add table
Reference in a new issue