Skip to content

Commit a819c46

Browse files
jhfclaude
andcommitted
feat: Show import as active when waiting for review
Include waiting_for_review in is_importing() so the navbar highlights when user action is needed. Add needs_review field to distinguish "system working" from "user must act." Navbar pulses amber, jobs table badge changes from blue/Clock to amber/AlertCircle, sidebar shows "Review needed" with attention indicator. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 64677a4 commit a819c46

File tree

7 files changed

+113
-29
lines changed

7 files changed

+113
-29
lines changed

app/src/app/getting-started/@progress/nav-item.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22
import Link from "next/link";
3-
import { Check, Loader2 } from "lucide-react";
3+
import { AlertCircle, Check, Loader2 } from "lucide-react";
44
import { usePathname } from "next/navigation";
55
import { cn } from "@/lib/utils";
66
import { ReactNode } from "react";
@@ -11,13 +11,15 @@ export const NavItem = ({
1111
done,
1212
subtitle,
1313
processing,
14+
needsAttention,
1415
icon,
1516
}: {
1617
readonly title: string;
1718
readonly subtitle?: string;
1819
readonly href: string;
1920
readonly done?: boolean;
2021
readonly processing?: boolean;
22+
readonly needsAttention?: boolean;
2123
readonly icon?: ReactNode;
2224
}) => {
2325
const pathname = usePathname();
@@ -33,13 +35,15 @@ export const NavItem = ({
3335
{icon && <span className="text-gray-500">{icon}</span>}
3436
{title}
3537
</Link>
36-
{processing ? (
38+
{needsAttention ? (
39+
<AlertCircle className="w-5 h-5 text-amber-500 animate-pulse" />
40+
) : processing ? (
3741
<Loader2 className="w-5 h-5 text-yellow-500 animate-spin" />
3842
) : (
3943
done && <Check className="w-5 h-5" />
4044
)}
4145
</div>
42-
{(done || processing) && subtitle && (
46+
{(done || processing || needsAttention) && subtitle && (
4347
<span className="text-xs text-gray-700">{subtitle}</span>
4448
)}
4549
</>

app/src/app/import/@progress/default.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export default function ImportStatus() {
3131

3232
// Determine if any import process is active using workerStatus only
3333
const isImporting = mounted ? (safeWorkerStatus.isImporting ?? false) : false;
34+
const needsReview = mounted ? (safeWorkerStatus.importing?.needs_review ?? false) : false;
3435
const isDeriving = mounted ?
3536
((safeWorkerStatus.isDerivingUnits || safeWorkerStatus.isDerivingReports) ?? false) : false;
3637

@@ -95,9 +96,10 @@ export default function ImportStatus() {
9596
done={false}
9697
title="View Jobs"
9798
href="/import/jobs"
98-
subtitle="Monitor ongoing imports"
99+
subtitle={needsReview ? "Review needed" : "Monitor ongoing imports"}
99100
icon={<FileText className="w-4 h-4" />}
100-
processing={isImporting}
101+
processing={isImporting && !needsReview}
102+
needsAttention={needsReview}
101103
/>
102104
</li>
103105
<li className="mb-6">

app/src/app/import/jobs/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ const jobStatuses = [
5858
{ value: "preparing_data", label: "Preparing", icon: Loader },
5959
{ value: "analysing_data", label: "Analyzing", icon: Hourglass },
6060
{ value: "processing_data", label: "Processing", icon: Hourglass },
61-
{ value: "waiting_for_review", label: "Review", icon: Clock },
61+
{ value: "waiting_for_review", label: "Review", icon: AlertCircle },
6262
{ value: "approved", label: "Approved", icon: ThumbsUp },
6363
{ value: "finished", label: "Finished", icon: CheckCircle },
6464
{ value: "rejected", label: "Rejected", icon: ThumbsDown },
@@ -353,7 +353,7 @@ export default function ImportJobsPage() {
353353
const statusBadge = (
354354
<Badge
355355
variant="secondary"
356-
className={job.state === 'waiting_for_review' ? 'bg-blue-100 text-blue-800 hover:bg-blue-200' : ''}
356+
className={job.state === 'waiting_for_review' ? 'bg-amber-100 text-amber-800 hover:bg-amber-200' : ''}
357357
>
358358
{status?.icon && <status.icon className="mr-2 h-4 w-4" />}
359359
{status?.label ?? job.state}

app/src/atoms/worker_status.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export interface ImportJobProgress {
4747

4848
export interface ImportStatus {
4949
active: boolean;
50+
needs_review: boolean;
5051
jobs: ImportJobProgress[];
5152
}
5253

@@ -152,15 +153,16 @@ export const setWorkerStatusAtom = atom(
152153
newJobs.push(updatedJob);
153154
}
154155

155-
// Only keep actively importing jobs
156-
const activeJobs = newJobs.filter(j => j.state === 'analysing_data' || j.state === 'processing_data');
156+
// Only keep actively importing or review-waiting jobs
157+
const activeJobs = newJobs.filter(j => j.state === 'analysing_data' || j.state === 'processing_data' || j.state === 'waiting_for_review');
158+
const needsReview = activeJobs.some(j => j.state === 'waiting_for_review');
157159

158160
set(workerStatusAtom, {
159161
...prevStatus,
160162
loading: false,
161163
error: null,
162164
isImporting: activeJobs.length > 0 ? true : prevStatus.isImporting,
163-
importing: { active: activeJobs.length > 0, jobs: activeJobs },
165+
importing: { active: activeJobs.length > 0, needs_review: needsReview, jobs: activeJobs },
164166
});
165167
return;
166168
}
@@ -207,10 +209,10 @@ export const setWorkerStatusAtom = atom(
207209
updatedStatus.isImporting = status;
208210
if (!status) {
209211
// Import completed — clear jobs
210-
updatedStatus.importing = { active: false, jobs: [] };
212+
updatedStatus.importing = { active: false, needs_review: false, jobs: [] };
211213
} else if (!prevStatus.importing?.active) {
212214
// New import starting — mark active, keep any jobs from initial RPC
213-
updatedStatus.importing = { active: true, jobs: prevStatus.importing?.jobs ?? [] };
215+
updatedStatus.importing = { active: true, needs_review: prevStatus.importing?.needs_review ?? false, jobs: prevStatus.importing?.jobs ?? [] };
214216
}
215217
} else if (type === 'is_deriving_statistical_units') {
216218
updatedStatus.isDerivingUnits = status;

app/src/components/navbar.tsx

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ function NavLink({
202202
icon: Icon,
203203
label,
204204
isActive,
205+
needsAttention,
205206
isCurrent,
206207
progressPct,
207208
popoverContent,
@@ -210,10 +211,25 @@ function NavLink({
210211
icon: React.ComponentType<{ size: number }>;
211212
label: string;
212213
isActive: boolean | null;
214+
needsAttention?: boolean;
213215
isCurrent: boolean;
214216
progressPct: number | null;
215217
popoverContent: React.ReactNode | null;
216218
}) {
219+
const borderColor = needsAttention
220+
? "border-amber-400"
221+
: isActive
222+
? "border-yellow-400"
223+
: isCurrent
224+
? "border-white"
225+
: "border-transparent";
226+
227+
const infoColor = needsAttention
228+
? "text-amber-400 hover:bg-white/20"
229+
: isActive
230+
? "text-yellow-400 hover:bg-white/20"
231+
: "invisible";
232+
217233
return (
218234
<div className="relative hidden lg:flex items-center">
219235
<Link
@@ -222,18 +238,17 @@ function NavLink({
222238
buttonVariants({ variant: "ghost", size: "sm" }),
223239
"space-x-2 relative overflow-hidden",
224240
"border-1",
225-
isActive
226-
? "border-yellow-400"
227-
: isCurrent
228-
? "border-white"
229-
: "border-transparent"
241+
borderColor,
242+
needsAttention && "animate-pulse"
230243
)}
231244
style={
232-
isActive && progressPct !== null
233-
? {
234-
backgroundImage: `linear-gradient(to right, rgba(250, 204, 21, 0.25) ${progressPct}%, transparent ${progressPct}%)`,
235-
}
236-
: undefined
245+
needsAttention
246+
? { backgroundColor: "rgba(245, 158, 11, 0.2)" }
247+
: isActive && progressPct !== null
248+
? {
249+
backgroundImage: `linear-gradient(to right, rgba(250, 204, 21, 0.25) ${progressPct}%, transparent ${progressPct}%)`,
250+
}
251+
: undefined
237252
}
238253
>
239254
<Icon size={16} />
@@ -245,17 +260,15 @@ function NavLink({
245260
<button
246261
className={cn(
247262
"ml-0.5 p-0.5 rounded",
248-
isActive
249-
? "text-yellow-400 hover:bg-white/20"
250-
: "invisible"
263+
infoColor
251264
)}
252265
aria-label={`${label} progress details`}
253266
>
254267
<Info size={14} />
255268
</button>
256269
</PopoverTrigger>
257270
<PopoverContent className="w-80">
258-
<h4 className="font-medium mb-2 text-sm">{label} Progress</h4>
271+
<h4 className="font-medium mb-2 text-sm">{needsAttention ? `${label} — Action Needed` : `${label} Progress`}</h4>
259272
{popoverContent}
260273
</PopoverContent>
261274
</Popover>
@@ -334,12 +347,15 @@ export default function Navbar() {
334347
icon={Upload}
335348
label="Import"
336349
isActive={isImporting}
350+
needsAttention={importing?.needs_review ?? false}
337351
isCurrent={pathname.startsWith("/import")}
338352
progressPct={importPct}
339353
popoverContent={isImporting
340-
? (importing?.active && importing.jobs.length > 0
341-
? <ImportProgressPopover importing={importing} />
342-
: <p className="text-sm text-gray-500">Import is active...</p>)
354+
? (importing?.needs_review
355+
? <p className="text-sm text-amber-700">Import needs review — {importing.jobs.filter(j => j.state === 'waiting_for_review').length} job(s) waiting for your approval</p>
356+
: importing?.active && importing.jobs.length > 0
357+
? <ImportProgressPopover importing={importing} />
358+
: <p className="text-sm text-gray-500">Import is active...</p>)
343359
: null}
344360
/>
345361

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
BEGIN;
2+
3+
CREATE OR REPLACE FUNCTION public.is_importing()
4+
RETURNS jsonb
5+
LANGUAGE sql
6+
STABLE
7+
AS $function$
8+
SELECT jsonb_build_object(
9+
'active', EXISTS (
10+
SELECT 1 FROM public.import_job
11+
WHERE state IN ('preparing_data', 'analysing_data', 'processing_data')
12+
),
13+
'jobs', COALESCE(
14+
(SELECT jsonb_agg(jsonb_build_object(
15+
'id', ij.id,
16+
'state', ij.state,
17+
'total_rows', ij.total_rows,
18+
'imported_rows', ij.imported_rows,
19+
'analysis_completed_pct', ij.analysis_completed_pct,
20+
'import_completed_pct', ij.import_completed_pct
21+
)) FROM public.import_job AS ij
22+
WHERE ij.state IN ('preparing_data', 'analysing_data', 'processing_data')),
23+
'[]'::jsonb
24+
)
25+
);
26+
$function$;
27+
28+
END;
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
BEGIN;
2+
3+
CREATE OR REPLACE FUNCTION public.is_importing()
4+
RETURNS jsonb
5+
LANGUAGE sql
6+
STABLE
7+
AS $function$
8+
SELECT jsonb_build_object(
9+
'active', EXISTS (
10+
SELECT 1 FROM public.import_job
11+
WHERE state IN ('preparing_data', 'analysing_data', 'processing_data', 'waiting_for_review')
12+
),
13+
'needs_review', EXISTS (
14+
SELECT 1 FROM public.import_job
15+
WHERE state = 'waiting_for_review'
16+
),
17+
'jobs', COALESCE(
18+
(SELECT jsonb_agg(jsonb_build_object(
19+
'id', ij.id,
20+
'state', ij.state,
21+
'total_rows', ij.total_rows,
22+
'imported_rows', ij.imported_rows,
23+
'analysis_completed_pct', ij.analysis_completed_pct,
24+
'import_completed_pct', ij.import_completed_pct
25+
)) FROM public.import_job AS ij
26+
WHERE ij.state IN ('preparing_data', 'analysing_data', 'processing_data', 'waiting_for_review')),
27+
'[]'::jsonb
28+
)
29+
);
30+
$function$;
31+
32+
END;

0 commit comments

Comments
 (0)