feat(ui): add review empty states
This commit is contained in:
parent
192a2aafde
commit
6472a58ad1
@ -4,6 +4,8 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
|||||||
import { AppShell } from "@/components/AppShell";
|
import { AppShell } from "@/components/AppShell";
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
|
EmptyState,
|
||||||
|
LoadingSpinner,
|
||||||
Panel,
|
Panel,
|
||||||
} from "@/components/ui/Primitives";
|
} from "@/components/ui/Primitives";
|
||||||
import {
|
import {
|
||||||
@ -33,6 +35,7 @@ export default function ReviewsPage() {
|
|||||||
const [selectedApprovalId, setSelectedApprovalId] = useState<string | null>(null);
|
const [selectedApprovalId, setSelectedApprovalId] = useState<string | null>(null);
|
||||||
const [selectedBatchIds, setSelectedBatchIds] = useState<Set<string>>(new Set());
|
const [selectedBatchIds, setSelectedBatchIds] = useState<Set<string>>(new Set());
|
||||||
const [reviewNote, setReviewNote] = useState("");
|
const [reviewNote, setReviewNote] = useState("");
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
@ -50,6 +53,8 @@ export default function ReviewsPage() {
|
|||||||
setTimeline(nextTimeline);
|
setTimeline(nextTimeline);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : "Unable to load review queue");
|
setError(err instanceof Error ? err.message : "Unable to load review queue");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, []);
|
}, []);
|
||||||
@ -281,15 +286,27 @@ export default function ReviewsPage() {
|
|||||||
onClear={clearBatch}
|
onClear={clearBatch}
|
||||||
onBatchDecision={(decision) => void handleBatchDecision(decision)}
|
onBatchDecision={(decision) => void handleBatchDecision(decision)}
|
||||||
/>
|
/>
|
||||||
{error ? <div style={{ color: "var(--nl-text-secondary)" }}>{error}</div> : null}
|
{isLoading ? (
|
||||||
|
<LoadingSpinner label="Loading review queue" className="py-8" />
|
||||||
<ReviewQueueList
|
) : error && approvalQueue.length === 0 ? (
|
||||||
items={approvalQueue}
|
<EmptyState
|
||||||
batchMode={batchMode}
|
title="Unable to load review queue"
|
||||||
selectedBatchIds={selectedBatchIds}
|
description={error}
|
||||||
selectedApprovalId={featuredProposal?.id ?? null}
|
actionLabel="Retry"
|
||||||
onSelectItem={(id) => batchMode ? toggleBatchItem(id) : setSelectedApprovalId(id)}
|
onAction={() => window.location.reload()}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{error ? <EmptyState title="Review update failed" description={error} /> : null}
|
||||||
|
<ReviewQueueList
|
||||||
|
items={approvalQueue}
|
||||||
|
batchMode={batchMode}
|
||||||
|
selectedBatchIds={selectedBatchIds}
|
||||||
|
selectedApprovalId={featuredProposal?.id ?? null}
|
||||||
|
onSelectItem={(id) => batchMode ? toggleBatchItem(id) : setSelectedApprovalId(id)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Panel>
|
</Panel>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { ProposalReviewCard } from "@/components/ProposalReviewCard";
|
|||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
|
EmptyState,
|
||||||
ListItemButton,
|
ListItemButton,
|
||||||
Panel,
|
Panel,
|
||||||
PanelBody,
|
PanelBody,
|
||||||
@ -126,6 +127,15 @@ export function ReviewQueueList({
|
|||||||
selectedApprovalId: string | null;
|
selectedApprovalId: string | null;
|
||||||
onSelectItem: (id: string) => void;
|
onSelectItem: (id: string) => void;
|
||||||
}) {
|
}) {
|
||||||
|
if (items.length === 0) {
|
||||||
|
return (
|
||||||
|
<EmptyState
|
||||||
|
title="No proposals need review"
|
||||||
|
description="Agent-mediated changes that need human approval will appear here."
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: "grid", gap: "var(--nl-space-3)" }}>
|
<div style={{ display: "grid", gap: "var(--nl-space-3)" }}>
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
@ -212,5 +222,19 @@ export function ProposalDiffCard({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ReviewTimeline({ items }: { items: AgentTimelineItem[] }) {
|
export function ReviewTimeline({ items }: { items: AgentTimelineItem[] }) {
|
||||||
|
if (items.length === 0) {
|
||||||
|
return (
|
||||||
|
<Panel>
|
||||||
|
<PanelHeader>
|
||||||
|
<PanelTitle>Agent activity timeline</PanelTitle>
|
||||||
|
</PanelHeader>
|
||||||
|
<EmptyState
|
||||||
|
title="No review activity yet"
|
||||||
|
description="Approvals, rejections, and agent changes will be listed as they happen."
|
||||||
|
/>
|
||||||
|
</Panel>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return <AgentTimeline items={items} />;
|
return <AgentTimeline items={items} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,10 +23,12 @@ import {
|
|||||||
DropdownMenuSub,
|
DropdownMenuSub,
|
||||||
DropdownMenuSubTrigger,
|
DropdownMenuSubTrigger,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
|
EmptyState as BytelystEmptyState,
|
||||||
IconButton as BytelystIconButton,
|
IconButton as BytelystIconButton,
|
||||||
Input as BytelystInput,
|
Input as BytelystInput,
|
||||||
Label as BytelystLabel,
|
Label as BytelystLabel,
|
||||||
ListItemButton as BytelystListItemButton,
|
ListItemButton as BytelystListItemButton,
|
||||||
|
LoadingSpinner as BytelystLoadingSpinner,
|
||||||
Panel as BytelystPanel,
|
Panel as BytelystPanel,
|
||||||
PanelBody as BytelystPanelBody,
|
PanelBody as BytelystPanelBody,
|
||||||
PanelDescription as BytelystPanelDescription,
|
PanelDescription as BytelystPanelDescription,
|
||||||
@ -57,10 +59,12 @@ import {
|
|||||||
type DataListItemProps,
|
type DataListItemProps,
|
||||||
type DataListProps,
|
type DataListProps,
|
||||||
type DiffCardProps,
|
type DiffCardProps,
|
||||||
|
type EmptyStateProps,
|
||||||
type IconButtonProps,
|
type IconButtonProps,
|
||||||
type InputProps,
|
type InputProps,
|
||||||
type LabelProps,
|
type LabelProps,
|
||||||
type ListItemButtonProps,
|
type ListItemButtonProps,
|
||||||
|
type LoadingSpinnerProps,
|
||||||
type PanelBodyProps,
|
type PanelBodyProps,
|
||||||
type PanelDescriptionProps,
|
type PanelDescriptionProps,
|
||||||
type PanelHeaderProps,
|
type PanelHeaderProps,
|
||||||
@ -297,3 +301,11 @@ export function DataListItem({ className, ...props }: DataListItemProps) {
|
|||||||
|
|
||||||
export const OperationalList = DataList;
|
export const OperationalList = DataList;
|
||||||
export const OperationalListItem = DataListItem;
|
export const OperationalListItem = DataListItem;
|
||||||
|
|
||||||
|
export function EmptyState({ className, ...props }: EmptyStateProps) {
|
||||||
|
return <BytelystEmptyState className={mergeClassNames("rounded-[var(--nl-radius-md)]", className)} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LoadingSpinner({ className, ...props }: LoadingSpinnerProps) {
|
||||||
|
return <BytelystLoadingSpinner className={className} {...props} />;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user