mirror of
https://github.com/BradGroux/veritas-kanban.git
synced 2026-10-08 12:47:51 +00:00
feat(US-1305): Add status history to activity sidebar
- Added daily summary card showing active/idle time and utilization % - Added Status History tab with today's status transitions - Display previous/new status with duration of each state - Mini progress bar for visual time breakdown - Backend was already complete (status-history-service logs all status changes)
This commit is contained in:
parent
b53ef09dcd
commit
8a9416b7cc
3 changed files with 224 additions and 34 deletions
|
|
@ -1107,3 +1107,4 @@
|
|||
{"type":"task.created","taskId":"task_20260128_b2CW4g","project":"project-a","status":"todo","id":"evt_-g7kMhjBnCYG","timestamp":"2026-01-28T14:13:49.936Z"}
|
||||
{"type":"task.created","taskId":"task_20260128_NEm1K_","project":"project-b","status":"todo","id":"evt_aCXwbrsfHVkW","timestamp":"2026-01-28T14:13:49.937Z"}
|
||||
{"type":"task.created","taskId":"task_20260128_eCyXeF","status":"todo","id":"evt_qx8JflCKJ_1V","timestamp":"2026-01-28T14:13:49.944Z"}
|
||||
{"type":"task.status_changed","taskId":"task_20260128_d47DtQ","project":"platform","status":"blocked","previousStatus":"todo","id":"evt_dcyHNA6aIlp5","timestamp":"2026-01-28T14:13:59.205Z"}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,33 @@
|
|||
[
|
||||
{
|
||||
"id": "activity_1769609642656_9lea2p5ac",
|
||||
"type": "comment_added",
|
||||
"taskId": "task_20260128_V0MVwS",
|
||||
"taskTitle": "US-1405: Dashboard metrics cards refresh",
|
||||
"details": {
|
||||
"author": "Veritas",
|
||||
"preview": "Verified dashboard metrics cards are fully functio..."
|
||||
},
|
||||
"timestamp": "2026-01-28T14:14:02.656Z"
|
||||
},
|
||||
{
|
||||
"id": "activity_1769609642614_cy9x653r2",
|
||||
"type": "task_updated",
|
||||
"taskId": "task_20260128_V0MVwS",
|
||||
"taskTitle": "US-1405: Dashboard metrics cards refresh",
|
||||
"timestamp": "2026-01-28T14:14:02.614Z"
|
||||
},
|
||||
{
|
||||
"id": "activity_1769609639205_sn6jggief",
|
||||
"type": "status_changed",
|
||||
"taskId": "task_20260128_d47DtQ",
|
||||
"taskTitle": "FEATURE REQUEST: Host Kimi K2.5 Model In Azure",
|
||||
"details": {
|
||||
"from": "todo",
|
||||
"status": "blocked"
|
||||
},
|
||||
"timestamp": "2026-01-28T14:13:59.205Z"
|
||||
},
|
||||
{
|
||||
"id": "activity_1769609585007_vp6ouz8mc",
|
||||
"type": "comment_added",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useState } from 'react';
|
||||
import { Activity, Trash2, RefreshCw, Clock, Coffee, ArrowRight, Zap } from 'lucide-react';
|
||||
import { Activity, Trash2, RefreshCw, Coffee, ArrowRight, Zap } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import {
|
||||
|
|
@ -102,8 +102,147 @@ function ActivityRow({ activity }: { activity: ActivityItem }) {
|
|||
);
|
||||
}
|
||||
|
||||
// Status Badge component
|
||||
function StatusBadge({ status }: { status: string }) {
|
||||
const colorClass = getStatusColor(status);
|
||||
|
||||
return (
|
||||
<span className={cn(
|
||||
'inline-flex items-center px-2 py-0.5 rounded text-xs font-medium text-white',
|
||||
colorClass
|
||||
)}>
|
||||
{status}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// Daily Summary Card
|
||||
function DailySummaryCard() {
|
||||
const { data: summary, isLoading } = useDailySummary();
|
||||
|
||||
if (isLoading || !summary) {
|
||||
return (
|
||||
<div className="px-4 py-3 border-b bg-muted/30">
|
||||
<div className="animate-pulse flex items-center gap-4">
|
||||
<div className="h-10 w-20 bg-muted rounded"></div>
|
||||
<div className="h-10 w-20 bg-muted rounded"></div>
|
||||
<div className="h-10 w-20 bg-muted rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const total = summary.activeMs + summary.idleMs + summary.errorMs;
|
||||
const activePercent = total > 0 ? Math.round((summary.activeMs / total) * 100) : 0;
|
||||
|
||||
return (
|
||||
<div className="px-4 py-3 border-b bg-muted/30">
|
||||
<div className="text-xs font-medium text-muted-foreground mb-2">Today's Summary</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Zap className="h-4 w-4 text-green-500" />
|
||||
<div>
|
||||
<div className="text-sm font-bold text-green-500">{formatDurationMs(summary.activeMs)}</div>
|
||||
<div className="text-xs text-muted-foreground">Active</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Coffee className="h-4 w-4 text-muted-foreground" />
|
||||
<div>
|
||||
<div className="text-sm font-bold text-muted-foreground">{formatDurationMs(summary.idleMs)}</div>
|
||||
<div className="text-xs text-muted-foreground">Idle</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 ml-auto">
|
||||
<div className="text-right">
|
||||
<div className="text-sm font-bold">{activePercent}%</div>
|
||||
<div className="text-xs text-muted-foreground">Utilization</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Mini progress bar */}
|
||||
{total > 0 && (
|
||||
<div className="mt-2 h-1.5 rounded-full overflow-hidden flex bg-muted">
|
||||
<div
|
||||
className="bg-green-500 transition-all"
|
||||
style={{ width: `${(summary.activeMs / total) * 100}%` }}
|
||||
/>
|
||||
<div
|
||||
className="bg-gray-400 transition-all"
|
||||
style={{ width: `${(summary.idleMs / total) * 100}%` }}
|
||||
/>
|
||||
{summary.errorMs > 0 && (
|
||||
<div
|
||||
className="bg-red-500 transition-all"
|
||||
style={{ width: `${(summary.errorMs / total) * 100}%` }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Status Transition Row
|
||||
function StatusTransitionRow({ entry }: { entry: StatusHistoryEntry }) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 py-3 px-2 hover:bg-muted/50 rounded-md transition-colors">
|
||||
<span className="text-xs text-muted-foreground w-14 shrink-0">
|
||||
{new Date(entry.timestamp).toLocaleTimeString([], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}
|
||||
</span>
|
||||
<StatusBadge status={entry.previousStatus} />
|
||||
<ArrowRight className="h-3 w-3 text-muted-foreground shrink-0" />
|
||||
<StatusBadge status={entry.newStatus} />
|
||||
{entry.durationMs && (
|
||||
<span className="text-xs text-muted-foreground ml-auto">
|
||||
{formatDurationMs(entry.durationMs)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Status History List
|
||||
function StatusHistoryList() {
|
||||
const { data: history, isLoading } = useStatusHistory(50);
|
||||
|
||||
// Filter to today's entries
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const todayEntries = history?.filter(entry =>
|
||||
entry.timestamp.startsWith(today)
|
||||
) || [];
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="text-center text-muted-foreground py-8">
|
||||
Loading status history...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (todayEntries.length === 0) {
|
||||
return (
|
||||
<div className="text-center text-muted-foreground py-8">
|
||||
No status changes today
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="divide-y divide-border">
|
||||
{todayEntries.map(entry => (
|
||||
<StatusTransitionRow key={entry.id} entry={entry} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ActivitySidebar({ open, onOpenChange }: ActivitySidebarProps) {
|
||||
const [filter, setFilter] = useState<string>('all');
|
||||
const [tab, setTab] = useState<string>('tasks');
|
||||
const { data: activities, isLoading, refetch, isRefetching } = useActivities(100);
|
||||
const clearActivities = useClearActivities();
|
||||
|
||||
|
|
@ -146,43 +285,64 @@ export function ActivitySidebar({ open, onOpenChange }: ActivitySidebarProps) {
|
|||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{activityTypes.length > 1 && (
|
||||
<Select value={filter} onValueChange={setFilter}>
|
||||
<SelectTrigger className="mt-2">
|
||||
<SelectValue placeholder="Filter activities" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Activities</SelectItem>
|
||||
{activityTypes.map(type => (
|
||||
<SelectItem key={type} value={type}>
|
||||
{activityIcons[type]} {activityLabels[type]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</SheetHeader>
|
||||
|
||||
<ScrollArea className="h-[calc(100vh-120px)]">
|
||||
<div className="px-2 py-2">
|
||||
{isLoading ? (
|
||||
<div className="text-center text-muted-foreground py-8">
|
||||
Loading activities...
|
||||
</div>
|
||||
) : filteredActivities.length === 0 ? (
|
||||
<div className="text-center text-muted-foreground py-8">
|
||||
No activities yet
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-border">
|
||||
{filteredActivities.map(activity => (
|
||||
<ActivityRow key={activity.id} activity={activity} />
|
||||
))}
|
||||
{/* Daily Summary always visible at top */}
|
||||
<DailySummaryCard />
|
||||
|
||||
<Tabs value={tab} onValueChange={setTab} className="flex flex-col h-[calc(100vh-200px)]">
|
||||
<TabsList className="mx-4 mt-2 grid w-[calc(100%-32px)] grid-cols-2">
|
||||
<TabsTrigger value="tasks">Task Activity</TabsTrigger>
|
||||
<TabsTrigger value="status">Status History</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="tasks" className="flex-1 mt-0">
|
||||
{activityTypes.length > 1 && (
|
||||
<div className="px-4 py-2">
|
||||
<Select value={filter} onValueChange={setFilter}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Filter activities" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Activities</SelectItem>
|
||||
{activityTypes.map(type => (
|
||||
<SelectItem key={type} value={type}>
|
||||
{activityIcons[type]} {activityLabels[type]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
<ScrollArea className="h-full">
|
||||
<div className="px-2 py-2">
|
||||
{isLoading ? (
|
||||
<div className="text-center text-muted-foreground py-8">
|
||||
Loading activities...
|
||||
</div>
|
||||
) : filteredActivities.length === 0 ? (
|
||||
<div className="text-center text-muted-foreground py-8">
|
||||
No activities yet
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-border">
|
||||
{filteredActivities.map(activity => (
|
||||
<ActivityRow key={activity.id} activity={activity} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="status" className="flex-1 mt-0">
|
||||
<ScrollArea className="h-full">
|
||||
<div className="px-2 py-2">
|
||||
<StatusHistoryList />
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue