From c3d5c1fa4641fe8ff56d1ddff2006b032916a998 Mon Sep 17 00:00:00 2001 From: Elizabet Oliveira Date: Mon, 30 Mar 2026 13:18:43 +0100 Subject: [PATCH 1/8] feat: add reusable EmptyState component Extract repeated empty/setup state UI pattern into a shared EmptyState component and adopt it in DBSearchPage and SessionsPage. Made-with: Cursor --- packages/app/src/DBSearchPage.tsx | 13 +-- packages/app/src/SessionsPage.tsx | 123 ++++++++++----------- packages/app/src/components/EmptyState.tsx | 36 ++++++ 3 files changed, 100 insertions(+), 72 deletions(-) create mode 100644 packages/app/src/components/EmptyState.tsx diff --git a/packages/app/src/DBSearchPage.tsx b/packages/app/src/DBSearchPage.tsx index 1d2a14390a..facbac2f49 100644 --- a/packages/app/src/DBSearchPage.tsx +++ b/packages/app/src/DBSearchPage.tsx @@ -44,7 +44,6 @@ import { Box, Button, Card, - Center, Code, Flex, Grid, @@ -77,6 +76,7 @@ import CodeMirror from '@uiw/react-codemirror'; import { ContactSupportText } from '@/components/ContactSupportText'; import { DBSearchPageFilters } from '@/components/DBSearchPageFilters'; import { DBTimeChart } from '@/components/DBTimeChart'; +import EmptyState from '@/components/EmptyState'; import { ErrorBoundary } from '@/components/Error/ErrorBoundary'; import { InputControlled } from '@/components/InputControlled'; import OnboardingModal from '@/components/OnboardingModal'; @@ -1789,12 +1789,11 @@ function DBSearchPage() { > {!queryReady ? ( -
- - Please start by selecting a source and then click the play - button to query data. - -
+ } + title="No data to display" + description="Select a source and click the play button to query data." + />
) : ( <> diff --git a/packages/app/src/SessionsPage.tsx b/packages/app/src/SessionsPage.tsx index 68d106033c..e3c387619b 100644 --- a/packages/app/src/SessionsPage.tsx +++ b/packages/app/src/SessionsPage.tsx @@ -23,9 +23,6 @@ import { Group, Stack, Stepper, - Text, - ThemeIcon, - Title, } from '@mantine/core'; import { IconDeviceLaptop, @@ -35,6 +32,7 @@ import { } from '@tabler/icons-react'; import { useVirtualizer } from '@tanstack/react-virtual'; +import EmptyState from '@/components/EmptyState'; import { SourceSelectControlled } from '@/components/SourceSelect'; import { TimePicker } from '@/components/TimePicker'; import { parseTimeQuery, useNewTimeQuery } from '@/timeQuery'; @@ -499,69 +497,64 @@ SessionsPage.getLayout = withAppNav; function SessionSetupInstructions() { const brandName = useBrandDisplayName(); return ( - <> - - - - - - - - Set up session replays - - + + + } + title="Set up session replays" + description={ + <> Follow these steps to start recording and viewing session replays with the {brandName} Otel Collector. - - - - - - Create a new source with Session type - - } - description={ - <> - Go to Team Settings, click Add Source under - Sources section, and select Session as the source - type. - - } - /> - - Choose the hyperdx_sessions table - - } - description={ - <> - Select the hyperdx_sessions table from the - dropdown, and select the corresponding trace source. - - } - /> - - Install the{' '} - - {brandName} Browser Integration - {' '} - to start recording sessions. - - } - /> - - - - + + } + /> + + + + Create a new source with Session type + + } + description={ + <> + Go to Team Settings, click Add Source under Sources + section, and select Session as the source type. + + } + /> + + Choose the hyperdx_sessions table + + } + description={ + <> + Select the hyperdx_sessions table from the + dropdown, and select the corresponding trace source. + + } + /> + + Install the{' '} + + {brandName} Browser Integration + {' '} + to start recording sessions. + + } + /> + + + ); } diff --git a/packages/app/src/components/EmptyState.tsx b/packages/app/src/components/EmptyState.tsx new file mode 100644 index 0000000000..a90ffdd094 --- /dev/null +++ b/packages/app/src/components/EmptyState.tsx @@ -0,0 +1,36 @@ +import { ReactNode } from 'react'; +import { Center, Stack, Text, ThemeIcon, Title } from '@mantine/core'; +import { IconDatabaseOff } from '@tabler/icons-react'; + +type EmptyStateProps = { + icon?: ReactNode; + title?: string; + description?: ReactNode; + children?: ReactNode; +}; + +export default function EmptyState({ + icon = , + title = 'No data available', + description, + children, +}: EmptyStateProps) { + return ( +
+ + + {icon} + + + {title} + + {description && ( + + {description} + + )} + {children} + +
+ ); +} From 8505a2cb99c82701ee8790e9bc3f4676171edc02 Mon Sep 17 00:00:00 2001 From: Elizabet Oliveira Date: Tue, 31 Mar 2026 13:23:16 +0100 Subject: [PATCH 2/8] feat: adopt EmptyState component across pages Replace ad-hoc inline empty states with the reusable EmptyState component on Alerts, Dashboards, Search, Service Map, Sessions, and Chart Editor pages. Extend EmptyState to support card/default variants, PaperProps, BoxProps, and fullWidth option. Made-with: Cursor --- .changeset/empty-state-component.md | 5 ++ agent_docs/code_style.md | 26 ++++++ packages/app/src/AlertsPage.tsx | 64 ++++++++++----- packages/app/src/DBSearchPage.tsx | 14 ++-- packages/app/src/DBServiceMapPage.tsx | 32 +++----- packages/app/src/SessionsPage.tsx | 50 +++++++----- .../src/components/DBEditTimeChartForm.tsx | 15 ++-- .../Dashboards/DashboardsListPage.tsx | 20 ++--- packages/app/src/components/EmptyState.tsx | 81 +++++++++++++++---- 9 files changed, 202 insertions(+), 105 deletions(-) create mode 100644 .changeset/empty-state-component.md diff --git a/.changeset/empty-state-component.md b/.changeset/empty-state-component.md new file mode 100644 index 0000000000..94b05a4bcd --- /dev/null +++ b/.changeset/empty-state-component.md @@ -0,0 +1,5 @@ +--- +"@hyperdx/app": patch +--- + +feat: Use EmptyState component across pages for consistent empty/no-data states diff --git a/agent_docs/code_style.md b/agent_docs/code_style.md index 535ca9f872..c219aba99b 100644 --- a/agent_docs/code_style.md +++ b/agent_docs/code_style.md @@ -93,6 +93,32 @@ The project uses Mantine UI with **custom variants** defined in `packages/app/sr This pattern cannot be enforced by ESLint and requires manual code review. +### EmptyState Component (REQUIRED) + +**Use `EmptyState` (`@/components/EmptyState`) for all empty/no-data states.** Do not create ad-hoc inline empty states. + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `icon` | `ReactNode` | — | Icon in the theme circle (hidden if not provided) | +| `title` | `string` | `"No data available"` | Heading text | +| `description` | `ReactNode` | — | Subtext below the title | +| `children` | `ReactNode` | — | Actions (buttons, links) below description | +| `variant` | `"default" \| "card"` | `"default"` | `"card"` wraps in a bordered Paper | + +```tsx +// ❌ BAD - ad-hoc inline empty states +
No data
+Nothing here + +// ✅ GOOD - use the EmptyState component +} + title="No alerts created yet" + description="Create alerts from dashboard charts or saved searches." + variant="card" +/> +``` + ## Refactoring - Edit files directly - don't create `component-v2.tsx` copies diff --git a/packages/app/src/AlertsPage.tsx b/packages/app/src/AlertsPage.tsx index 2b0545a353..085ad301d3 100644 --- a/packages/app/src/AlertsPage.tsx +++ b/packages/app/src/AlertsPage.tsx @@ -23,7 +23,6 @@ import { notifications } from '@mantine/notifications'; import { IconAlertTriangle, IconBell, - IconBrandSlack, IconChartLine, IconCheck, IconChevronRight, @@ -33,6 +32,7 @@ import { } from '@tabler/icons-react'; import { useQueryClient } from '@tanstack/react-query'; +import EmptyState from '@/components/EmptyState'; import { ErrorBoundary } from '@/components/Error/ErrorBoundary'; import { PageHeader } from '@/components/PageHeader'; @@ -463,7 +463,12 @@ function AlertCardList({ alerts }: { alerts: AlertsPageItem[] }) { OK {okData.length === 0 && ( -
No alerts
+ } + title="No alerts" + description="All alerts in OK state will appear here." + /> )} {okData.map((alert, index) => ( @@ -480,29 +485,35 @@ export default function AlertsPage() { const alerts = React.useMemo(() => data?.data || [], [data?.data]); return ( -
+
Alerts - {brandName} Alerts -
- - } - color="gray" - py="xs" - mt="md" - > - Alerts can be{' '} - + + {alerts?.length ? ( + } + color="gray" + py="xs" + mt="md" > - created - {' '} - from dashboard charts and saved searches. - + Alerts can be{' '} + + created + {' '} + from dashboard charts and saved searches. + + ) : null} {isLoading ? (
Loading...
) : isError ? ( @@ -512,7 +523,18 @@ export default function AlertsPage() { ) : ( -
No alerts created yet
+ } + title="No alerts created yet" + description={ + <> + Alerts can be created from{' '} + dashboard charts and{' '} + saved searches. + + } + /> )}
diff --git a/packages/app/src/DBSearchPage.tsx b/packages/app/src/DBSearchPage.tsx index facbac2f49..19e88872e3 100644 --- a/packages/app/src/DBSearchPage.tsx +++ b/packages/app/src/DBSearchPage.tsx @@ -66,6 +66,7 @@ import { IconLayoutSidebarLeftExpand, IconPlayerPlay, IconPlus, + IconStack2, IconTags, IconX, } from '@tabler/icons-react'; @@ -1788,13 +1789,12 @@ function DBSearchPage() { className="bg-body" > {!queryReady ? ( - - } - title="No data to display" - description="Select a source and click the play button to query data." - /> - + } + title="No data to display" + description="Select a source and click the play button to query data." + /> ) : ( <>
)} - } + title="No trace sources configured" + description="The Service Map visualizes relationships between your services using trace data. Configure a trace source to get started." + maw={600} > - - No trace sources configured - - - The Service Map visualizes relationships between your services using - trace data. Configure a trace source to get started. - {IS_LOCAL_MODE ? (