feat(L+M): configurable dashboard widget system + test framework
Phase L: Dashboard widget system - Migration 046: dashboard_configs table (user/tenant/role fallback cascade) - DashboardConfig model + dashboard_service with get/upsert per-user and tenant-default - API router: GET/PUT /api/dashboard/config, GET/PUT /api/dashboard/tenant-default - Frontend: 5 widget components (ProductionStats, QueueStatus, RecentRenders, CostOverview, WorkerStatus) - DashboardGrid with API-backed config, DashboardCustomizeModal (checkbox selection, save/cancel) - Dashboard.tsx: widget grid + analytics section (privileged users) - Admin.tsx: restructured with new section order and Maintenance 2-col grid Phase M: Test framework - Backend: pytest-asyncio + pytest-cov + factory-boy in pyproject.toml - conftest.py: excel_dir fixtures + async DB fixtures + mock storage/celery stubs - Domain tests: billing_service, cache_service, notifications_service, imports_sanity - Integration: test_api_health.py smoke test (requires running backend) - Frontend: vitest + @testing-library/react + msw added to package.json - vite.config.ts: test block (jsdom + globals + setupFiles) - tsconfig.json: exclude src/__tests__ from main tsc (test runner handles its own types) - MSW handlers for /api/auth/me, Billing.test.tsx, formatters.test.ts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { Activity } from 'lucide-react'
|
||||
import api from '../../../api/client'
|
||||
|
||||
interface ActivityEntry {
|
||||
id: string
|
||||
filename: string
|
||||
status: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
function Skeleton() {
|
||||
return (
|
||||
<div className="animate-pulse space-y-2">
|
||||
{[0, 1, 2].map((i) => (
|
||||
<div key={i} className="h-8 rounded bg-surface-muted" />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function QueueStatusWidget() {
|
||||
const { data, isLoading, error } = useQuery<ActivityEntry[]>({
|
||||
queryKey: ['worker-activity-widget'],
|
||||
queryFn: async () => {
|
||||
const res = await api.get('/worker/activity')
|
||||
return res.data as ActivityEntry[]
|
||||
},
|
||||
refetchInterval: 15_000,
|
||||
staleTime: 10_000,
|
||||
retry: 1,
|
||||
})
|
||||
|
||||
if (isLoading) return <Skeleton />
|
||||
if (error) {
|
||||
return <p className="text-xs text-red-500">Failed to load queue status</p>
|
||||
}
|
||||
|
||||
const entries = data ?? []
|
||||
const processing = entries.filter((e) => e.status === 'processing').length
|
||||
const failed = entries.filter((e) => e.status === 'failed').length
|
||||
const recent = entries.slice(0, 5)
|
||||
|
||||
const statusDot = processing > 0
|
||||
? 'bg-blue-500'
|
||||
: failed > 0
|
||||
? 'bg-red-500'
|
||||
: 'bg-green-500'
|
||||
|
||||
const statusLabel = processing > 0
|
||||
? `${processing} processing`
|
||||
: failed > 0
|
||||
? `${failed} failed`
|
||||
: 'Idle'
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{/* Summary row */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`inline-block w-2.5 h-2.5 rounded-full ${statusDot}`} />
|
||||
<span className="text-sm font-medium text-content">{statusLabel}</span>
|
||||
<span className="text-xs text-content-muted ml-auto">
|
||||
{entries.length} recent tasks
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Recent activity */}
|
||||
<div className="space-y-1">
|
||||
{recent.length === 0 && (
|
||||
<p className="text-xs text-content-muted text-center py-2">No recent activity</p>
|
||||
)}
|
||||
{recent.map((entry) => (
|
||||
<div
|
||||
key={entry.id}
|
||||
className="flex items-center gap-2 rounded px-2 py-1.5 text-xs"
|
||||
style={{ backgroundColor: 'var(--color-bg-surface)' }}
|
||||
>
|
||||
<Activity size={12} className="text-content-muted shrink-0" />
|
||||
<span className="flex-1 truncate text-content-secondary" title={entry.filename}>
|
||||
{entry.filename}
|
||||
</span>
|
||||
<span
|
||||
className={`font-medium shrink-0 ${
|
||||
entry.status === 'completed'
|
||||
? 'text-green-600'
|
||||
: entry.status === 'failed'
|
||||
? 'text-red-500'
|
||||
: entry.status === 'processing'
|
||||
? 'text-blue-500'
|
||||
: 'text-content-muted'
|
||||
}`}
|
||||
>
|
||||
{entry.status}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user