Skip to content

Commit 881c9b8

Browse files
committed
Dashboard: shared DataList patterns, anomalies page redesign
- Add DataList + list-query-outcome helpers for list/query UI states - Redesign anomalies with WebsitePageHeader, DataList rows, anomaly-item - Align goals, funnels, flags, monitors with list patterns; RPC anomaly tweaks - Update design-system note and codebase-map skill reference
1 parent 665871a commit 881c9b8

File tree

20 files changed

+1222
-1085
lines changed

20 files changed

+1222
-1085
lines changed

.agents/skills/databuddy/references/codebase-map.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Use this file when the task spans multiple packages or when the right edit locat
1515
- client hooks
1616
- auth-aware frontend flows
1717
- query and mutation consumers
18+
- **DataList** (`components/data-list.tsx`): compound component for all list views. See `design-system.mdc` for patterns.
1819
- Agent chat: [`contexts/chat-context.tsx`](/Users/iza/Dev/Databuddy/apps/dashboard/contexts/chat-context.tsx)`useChat` must start with `messages: []` on server **and** first client paint; restore `getMessagesFromLocal` in `useLayoutEffect` and gate `saveMessagesToLocal` until `hasRestoredFromLocal`, or SSR/hydration will disagree (empty vs persisted thread). UI: [`agent-messages.tsx`](/Users/iza/Dev/Databuddy/apps/dashboard/app/(main)/websites/[id]/agent/_components/agent-messages.tsx) merges consecutive identical tool labels (`formatToolLabel`) when the same tool+input repeats (`· 2×`); bottom `StreamingIndicator` only when no assistant text **and** no active tool label (`displayMessage`), so the in-message `ToolStep` is not duplicated by the shimmer.
1920
- Analytics agent web search: [`web-search.ts`](/Users/iza/Dev/Databuddy/apps/api/src/ai/tools/web-search.ts) (Perplexity). Tool labels in dashboard [`tool-display.tsx`](/Users/iza/Dev/Databuddy/apps/dashboard/lib/tool-display.tsx).
2021

.cursor/rules/design-system.mdc

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,17 @@ This file is **intentionally small**. It grows as we lock patterns. **Interactio
2424
- Use `flex-wrap` with `gap-x-5 gap-y-1` so the bar wraps cleanly on mobile without breaking the 10px grid.
2525
- Canonical reference: `apps/dashboard/app/(main)/monitors/[id]/page.tsx` → `StatusIndicator` + stats bar.
2626

27-
## Row items (list pages)
27+
## DataList — list pages
2828

29-
- Row wrapper: single `<Link>` as root, `group flex w-full items-center gap-3 border-b px-3 py-3 transition-colors hover:bg-accent/50 sm:gap-4 sm:px-4`.
30-
- Leading icon: `size-9` container with `rounded border bg-card`, `FaviconImage` at `size={18}` with a Phosphor fallback.
31-
- Actions: `size-7` ghost button, `opacity-0 group-hover:opacity-100 data-[state=open]:opacity-100`, wrapped in a stop-propagation `div`.
32-
- Canonical reference: `components/monitors/monitor-row.tsx`, `apps/dashboard/app/(main)/links/_components/link-item.tsx`.
29+
All list views use the `DataList` compound component (`components/data-list.tsx`). Wrap in `<DataList className="rounded bg-card">` — no border on root.
30+
31+
- **Row defaults:** `align="start"` for wrapping text, `align="center"` for single-line rows (e.g. flags). `w-full` is built-in — don't repeat it.
32+
- **Cell defaults:** `shrink-0` and `min-w-0` are built-in — don't repeat them. `text-start` is the browser default — don't add it.
33+
- **Inactive/paused state:** `opacity-50` on the Row, not a badge.
34+
- **`pt-0.5`** on non-text cells (icons, stats, actions) to align with text baseline in `items-start` rows. Omit in `items-center` rows.
35+
- **Icon containers:** `size-8 rounded` with semantic `bg-{color}-500/10 text-{color}-600 dark:text-{color}-400`. Use a `TYPE_CONFIG` const map when multiple types exist.
36+
- **Skeletons:** plain `div`s, not `DataList.Row`. Merge name + secondary into one `flex-1` block.
37+
- Canonical refs: `monitor-row.tsx`, `funnel-item.tsx`, `goal-item.tsx`, `flags-list.tsx`.
3338

3439
## Component reuse principle
3540

apps/dashboard/app/(main)/monitors/_components/monitors-list.tsx

Lines changed: 0 additions & 79 deletions
This file was deleted.

apps/dashboard/app/(main)/monitors/page.tsx

Lines changed: 73 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -9,38 +9,38 @@ import {
99
import { useQuery } from "@tanstack/react-query";
1010
import { Suspense, useState } from "react";
1111
import { PageHeader } from "@/app/(main)/websites/_components/page-header";
12-
import { EmptyState } from "@/components/empty-state";
12+
import { DataList } from "@/components/data-list";
1313
import { ErrorBoundary } from "@/components/error-boundary";
1414
import { FeatureAccessGate } from "@/components/feature-access-gate";
15+
import { MonitorRow } from "@/components/monitors/monitor-row";
1516
import { MonitorSheet } from "@/components/monitors/monitor-sheet";
1617
import { FeatureInviteDialog } from "@/components/organizations/feature-invite-dialog";
1718
import { Button } from "@/components/ui/button";
18-
import { Skeleton } from "@/components/ui/skeleton";
1919
import { useFeatureAccess } from "@/hooks/use-feature-access";
20+
import type { ListQuerySlice } from "@/lib/list-query-outcome";
2021
import { orpc } from "@/lib/orpc";
2122
import { cn } from "@/lib/utils";
22-
import { type Monitor, MonitorsList } from "./_components/monitors-list";
2323

24-
const MonitorsListSkeleton = () => (
25-
<div className="w-full overflow-x-auto">
26-
{Array.from({ length: 5 }).map((_, i) => (
27-
<div
28-
className="flex h-15 items-center gap-4 border-b px-4"
29-
key={`skeleton-${i + 1}`}
30-
>
31-
<Skeleton className="size-8 shrink-0 rounded" />
32-
<Skeleton className="h-4 w-40 shrink-0 lg:w-52" />
33-
<Skeleton className="h-3 min-w-0 flex-1" />
34-
<Skeleton className="hidden h-3 w-14 shrink-0 md:block" />
35-
<Skeleton className="hidden h-5 w-16 shrink-0 md:block" />
36-
<Skeleton className="hidden h-5 w-32 shrink-0 lg:block lg:w-44" />
37-
<Skeleton className="hidden h-4 w-14 shrink-0 lg:block" />
38-
<Skeleton className="h-5 w-16 shrink-0" />
39-
<Skeleton className="size-8 shrink-0 rounded" />
40-
</div>
41-
))}
42-
</div>
43-
);
24+
export interface Monitor {
25+
id: string;
26+
websiteId: string | null;
27+
url: string | null;
28+
name: string | null;
29+
granularity: string;
30+
cron: string;
31+
isPaused: boolean;
32+
isPublic: boolean;
33+
createdAt: Date | string;
34+
updatedAt: Date | string;
35+
website: {
36+
id: string;
37+
name: string | null;
38+
domain: string;
39+
} | null;
40+
jsonParsingConfig?: {
41+
enabled: boolean;
42+
} | null;
43+
}
4444

4545
export default function MonitorsPage() {
4646
const { hasAccess, isLoading: isAccessLoading } =
@@ -58,13 +58,7 @@ export default function MonitorsPage() {
5858
} | null;
5959
} | null>(null);
6060

61-
const {
62-
data: schedules,
63-
isLoading,
64-
refetch,
65-
isFetching,
66-
isError,
67-
} = useQuery({
61+
const schedulesQuery = useQuery({
6862
...orpc.uptime.listSchedules.queryOptions({ input: {} }),
6963
enabled: hasAccess,
7064
});
@@ -87,7 +81,7 @@ export default function MonitorsPage() {
8781
};
8882

8983
const handleDelete = () => {
90-
refetch();
84+
schedulesQuery.refetch();
9185
};
9286

9387
const handleSheetClose = () => {
@@ -99,7 +93,7 @@ export default function MonitorsPage() {
9993
<ErrorBoundary>
10094
<div className="h-full overflow-y-auto">
10195
<PageHeader
102-
count={hasAccess ? schedules?.length : undefined}
96+
count={hasAccess ? schedulesQuery.data?.length : undefined}
10397
description="View and manage all your uptime monitors"
10498
icon={<HeartbeatIcon />}
10599
right={
@@ -114,13 +108,18 @@ export default function MonitorsPage() {
114108
</Button>
115109
<Button
116110
aria-label="Refresh monitors"
117-
disabled={isLoading || isFetching}
118-
onClick={() => refetch()}
111+
disabled={
112+
schedulesQuery.isLoading || schedulesQuery.isFetching
113+
}
114+
onClick={() => schedulesQuery.refetch()}
119115
size="icon"
120116
variant="outline"
121117
>
122118
<ArrowClockwiseIcon
123-
className={cn((isLoading || isFetching) && "animate-spin")}
119+
className={cn(
120+
(schedulesQuery.isLoading || schedulesQuery.isFetching) &&
121+
"animate-spin"
122+
)}
124123
/>
125124
</Button>
126125
<Button onClick={handleCreate}>
@@ -135,35 +134,52 @@ export default function MonitorsPage() {
135134

136135
<FeatureAccessGate
137136
flagKey="monitors"
138-
loadingFallback={<MonitorsListSkeleton />}
137+
loadingFallback={<DataList.DefaultLoading />}
139138
>
140-
{isError ? (
141-
<div className="flex h-full items-center justify-center py-16">
142-
<EmptyState
143-
action={{ label: "Retry", onClick: () => refetch() }}
144-
description="Something went wrong while fetching monitors."
145-
icon={<HeartbeatIcon />}
146-
title="Failed to load monitors"
147-
variant="minimal"
148-
/>
149-
</div>
150-
) : (
151-
<MonitorsList
152-
isLoading={isAccessLoading || isLoading}
153-
monitors={(schedules as unknown as Monitor[]) || []}
154-
onCreateMonitorAction={handleCreate}
155-
onDeleteMonitorAction={handleDelete}
156-
onEditMonitorAction={handleEdit}
157-
onRefetchAction={refetch}
158-
/>
159-
)}
139+
<DataList.Content<Monitor>
140+
emptyProps={{
141+
action: {
142+
label: "Create Your First Monitor",
143+
onClick: handleCreate,
144+
},
145+
description:
146+
"Create your first uptime monitor to start tracking availability and receive alerts when services go down.",
147+
icon: <HeartbeatIcon weight="duotone" />,
148+
title: "No monitors yet",
149+
}}
150+
errorProps={{
151+
action: {
152+
label: "Retry",
153+
onClick: () => schedulesQuery.refetch(),
154+
},
155+
description: "Something went wrong while fetching monitors.",
156+
icon: <HeartbeatIcon />,
157+
title: "Failed to load monitors",
158+
}}
159+
gatePending={isAccessLoading}
160+
query={schedulesQuery as ListQuerySlice<Monitor>}
161+
>
162+
{(items) => (
163+
<DataList className="rounded bg-card">
164+
{items.map((monitor) => (
165+
<MonitorRow
166+
key={monitor.id}
167+
onDeleteAction={handleDelete}
168+
onEditAction={() => handleEdit(monitor)}
169+
onRefetchAction={schedulesQuery.refetch}
170+
schedule={monitor}
171+
/>
172+
))}
173+
</DataList>
174+
)}
175+
</DataList.Content>
160176
</FeatureAccessGate>
161177

162178
{isSheetOpen && (
163179
<Suspense fallback={null}>
164180
<MonitorSheet
165181
onCloseAction={handleSheetClose}
166-
onSaveAction={refetch}
182+
onSaveAction={schedulesQuery.refetch}
167183
open={isSheetOpen}
168184
schedule={editingSchedule}
169185
/>

0 commit comments

Comments
 (0)