Skip to content

Commit ed19b0a

Browse files
committed
ref(ui): Refactor EmptyMessage to use core components and simplify API
Modernizes the EmptyMessage component and removes redundant description prop: - Refactors EmptyMessage from styled-components to core layout primitives (Flex, Container, Text) - Removes redundant `description` prop - all content now uses `children` - Automatically applies text-wrap: balance to all empty state messages for better readability - Migrates 22 usage sites to use `children` instead of `description` prop - Simplifies component API by having a single way to pass content This reduces the component's surface area and makes it more consistent with the design system's layout and typography primitives.
1 parent a305a6e commit ed19b0a

File tree

33 files changed

+356
-269
lines changed

33 files changed

+356
-269
lines changed
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
---
2+
title: EmptyMessage
3+
description: A component for displaying empty states with optional icon, title, description, and action buttons. All text content uses balanced text wrapping for better readability.
4+
source: 'sentry/components/emptyMessage'
5+
resources:
6+
js: https://github.com/getsentry/sentry/blob/master/static/app/components/emptyMessage.tsx
7+
---
8+
9+
import {Button} from 'sentry/components/core/button';
10+
import EmptyMessage from 'sentry/components/emptyMessage';
11+
import Panel from 'sentry/components/panels/panel';
12+
import {IconSearch} from 'sentry/icons';
13+
import * as Storybook from 'sentry/stories';
14+
15+
To create a basic empty message, wrap your message in an `<EmptyMessage>` component.
16+
17+
```jsx
18+
<Panel>
19+
<EmptyMessage>
20+
This is a simple empty message with default styling and balanced text wrapping for
21+
better readability.
22+
</EmptyMessage>
23+
</Panel>
24+
```
25+
26+
## Props
27+
28+
- **title** (`React.ReactNode`) - The title of the empty message
29+
- **icon** (`React.ReactNode`) - Optional icon element
30+
- **action** (`React.ReactElement`) - Optional action button
31+
- **size** (`"md" | "lg"`) - Size of the text (default: md)
32+
- **children** (`React.ReactNode`) - Main content text (automatically uses balanced text wrapping)
33+
34+
## Examples
35+
36+
### With Title
37+
38+
Add a title to provide context for the empty state.
39+
40+
<Storybook.Demo>
41+
<Panel>
42+
<EmptyMessage title="No Results Found">
43+
We couldn't find any results matching your search criteria. Try adjusting your
44+
filters or search terms.
45+
</EmptyMessage>
46+
</Panel>
47+
</Storybook.Demo>
48+
```jsx
49+
<EmptyMessage title="No Results Found">
50+
We couldn't find any results matching your search criteria. Try adjusting your filters
51+
or search terms.
52+
</EmptyMessage>
53+
```
54+
55+
### With Icon
56+
57+
Add an icon to make the empty state more visually distinctive. Icons are defaulted to `lg` size, so there is typically no need to specify an icon size.
58+
59+
<Storybook.Demo>
60+
<Panel>
61+
<EmptyMessage icon={<IconSearch />} title="No Results">
62+
We couldn't find any results matching your search criteria.
63+
</EmptyMessage>
64+
</Panel>
65+
</Storybook.Demo>
66+
```jsx
67+
<EmptyMessage icon={<IconSearch />} title="No Results">
68+
We couldn't find any results matching your search criteria.
69+
</EmptyMessage>
70+
```
71+
72+
### With Action
73+
74+
Add action buttons to guide users toward resolving the empty state.
75+
76+
<Storybook.Demo>
77+
<Panel>
78+
<EmptyMessage
79+
icon={<IconSearch />}
80+
title="No Results Found"
81+
action={<Button priority="primary">Clear Filters</Button>}
82+
>
83+
We couldn't find any results matching your search criteria. Try adjusting your
84+
filters or search terms.
85+
</EmptyMessage>
86+
</Panel>
87+
</Storybook.Demo>
88+
```jsx
89+
<EmptyMessage
90+
icon={<IconSearch />}
91+
title="No Results Found"
92+
action={<Button priority="primary">Clear Filters</Button>}
93+
>
94+
We couldn't find any results matching your search criteria. Try adjusting your filters
95+
or search terms.
96+
</EmptyMessage>
97+
```
98+
99+
### Sizes
100+
101+
EmptyMessage supports two sizes: `md` (default) and `lg`.
102+
103+
<Storybook.Demo>
104+
<div style={{display: 'flex', gap: '1rem'}}>
105+
<Panel style={{flex: 1}}>
106+
<EmptyMessage icon={<IconSearch />} title="Medium Size">
107+
This is the default medium size empty message.
108+
</EmptyMessage>
109+
</Panel>
110+
<Panel style={{flex: 1}}>
111+
<EmptyMessage size="lg" icon={<IconSearch />} title="Large Size">
112+
This is a large size empty message with bigger text.
113+
</EmptyMessage>
114+
</Panel>
115+
</div>
116+
</Storybook.Demo>
117+
```jsx
118+
<EmptyMessage size="medium" icon={<IconSearch />} title="Medium Size">
119+
This is the default medium size empty message.
120+
</EmptyMessage>
121+
122+
<EmptyMessage size="lg" icon={<IconSearch />} title="Large Size">
123+
This is a large size empty message with bigger text.
124+
</EmptyMessage>
125+
```
126+
127+
### Text Wrapping
128+
129+
All text content automatically uses CSS text-wrap balance mode for better readability, especially with longer messages.
130+
131+
<Storybook.Demo>
132+
<Panel>
133+
<EmptyMessage icon={<IconSearch />} title="Text Wrapping">
134+
Sure you haven't misspelled? If you're using a lesser-known platform, consider
135+
choosing a more generic SDK like Browser JavaScript, Python, Node, .NET & Java or
136+
create a generic project.
137+
</EmptyMessage>
138+
</Panel>
139+
</Storybook.Demo>
140+
```jsx
141+
<EmptyMessage icon={<IconSearch />} title="Text Wrapping">
142+
Sure you haven't misspelled? If you're using a lesser-known platform, consider choosing
143+
a more generic SDK like Browser JavaScript, Python, Node, .NET & Java or create a
144+
generic project.
145+
</EmptyMessage>
146+
```
Lines changed: 42 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,51 @@
1-
import {css} from '@emotion/react';
2-
import styled from '@emotion/styled';
1+
import {useTheme} from '@emotion/react';
2+
import {mergeProps} from '@react-aria/utils';
33

4-
import {space} from 'sentry/styles/space';
5-
import TextBlock from 'sentry/views/settings/components/text/textBlock';
4+
import {Container, Flex} from 'sentry/components/core/layout';
5+
import {Text} from 'sentry/components/core/text';
6+
import {IconDefaultsProvider} from 'sentry/icons/useIconDefaults';
67

78
interface Props extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title'> {
8-
action?: React.ReactElement;
9-
description?: React.ReactNode;
9+
action?: React.ReactNode;
1010
icon?: React.ReactNode;
11-
leftAligned?: boolean;
12-
size?: 'large' | 'medium';
11+
size?: 'lg' | 'md';
1312
title?: React.ReactNode;
1413
}
1514

16-
type WrapperProps = Pick<Props, 'size'>;
17-
18-
const EmptyMessage = styled(
19-
({
20-
title,
21-
description,
22-
icon,
23-
children,
24-
action,
25-
leftAligned: _leftAligned,
26-
...props
27-
}: Props) => (
28-
<div data-test-id="empty-message" {...props}>
29-
{icon && <IconWrapper>{icon}</IconWrapper>}
30-
{title && <Title noMargin={!description && !children && !action}>{title}</Title>}
31-
{description && <Description>{description}</Description>}
32-
{children && <Description noMargin>{children}</Description>}
33-
{action && <Action>{action}</Action>}
34-
</div>
35-
)
36-
)<WrapperProps>`
37-
display: flex;
38-
${p =>
39-
p.leftAligned
40-
? css`
41-
max-width: 70%;
42-
align-items: flex-start;
43-
padding: ${space(4)};
44-
`
45-
: css`
46-
text-align: center;
47-
align-items: center;
48-
padding: ${space(4)} 15%;
49-
`};
50-
flex-direction: column;
51-
color: ${p => p.theme.textColor};
52-
font-size: ${p =>
53-
p.size && p.size === 'large' ? p.theme.fontSize.xl : p.theme.fontSize.md};
54-
`;
55-
56-
const IconWrapper = styled('div')`
57-
color: ${p => (p.theme.isChonk ? p.theme.gray400 : p.theme.gray200)};
58-
margin-bottom: ${space(1)};
59-
`;
60-
61-
const Title = styled('strong')<{noMargin: boolean}>`
62-
font-size: ${p => p.theme.fontSize.xl};
63-
${p => !p.noMargin && `margin-bottom: ${space(1)};`}
64-
`;
65-
66-
const Description = styled(TextBlock)`
67-
margin: 0;
68-
`;
69-
70-
const Action = styled('div')`
71-
margin-top: ${space(2)};
72-
`;
15+
function EmptyMessage({title, icon, children, action, size, ...props}: Props) {
16+
const theme = useTheme();
17+
18+
return (
19+
<Flex gap="xl" direction="column" padding="3xl">
20+
{stackProps => (
21+
<Text
22+
align="center"
23+
size={size}
24+
data-test-id="empty-message"
25+
{...mergeProps(stackProps, props)}
26+
>
27+
{icon && (
28+
<IconDefaultsProvider size="xl">
29+
<Container color={theme.isChonk ? theme.gray400 : theme.gray200}>
30+
{icon}
31+
</Container>
32+
</IconDefaultsProvider>
33+
)}
34+
{title && (
35+
<Text bold size="xl" density="comfortable">
36+
{title}
37+
</Text>
38+
)}
39+
{children && (
40+
<Text textWrap="balance" density="comfortable">
41+
{children}
42+
</Text>
43+
)}
44+
{action && <Container paddingTop="xl">{action}</Container>}
45+
</Text>
46+
)}
47+
</Flex>
48+
);
49+
}
7350

7451
export default EmptyMessage;

static/app/components/events/interfaces/crashContent/exception/stackTrace.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ function StackTrace({
5757
return (
5858
<Panel dashedBorder>
5959
<EmptyMessage
60-
icon={<IconWarning size="xl" />}
60+
icon={<IconWarning />}
6161
title={t('No app only stack trace has been found!')}
6262
/>
6363
</Panel>

static/app/components/feedback/details/feedbackEmptyDetails.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,9 @@ import FluidHeight from 'sentry/views/replays/detail/layout/fluidHeight';
77

88
const FeedbackEmptyDetails = styled((props: any) => (
99
<FluidHeight {...props}>
10-
<StyledEmptyMessage
11-
icon={<IconMail size="xl" />}
12-
description={t('No feedback selected')}
13-
/>
10+
<StyledEmptyMessage icon={<IconMail />}>
11+
{t('No feedback selected')}
12+
</StyledEmptyMessage>
1413
</FluidHeight>
1514
))`
1615
display: grid;

static/app/components/platformPicker.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ function PlatformPicker({
262262
</PlatformList>
263263
{platformList.length === 0 && (
264264
<EmptyMessage
265-
icon={<IconProject size="xl" />}
265+
icon={<IconProject />}
266266
title={t("We don't have an SDK for that yet!")}
267267
>
268268
{tct(

static/app/components/projects/missingProjectMembership.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -147,11 +147,8 @@ class MissingProjectMembership extends Component<Props, State> {
147147
<StyledPanel>
148148
{teams.length ? (
149149
<EmptyMessage
150-
icon={<IconFlag size="xl" />}
150+
icon={<IconFlag />}
151151
title={t("You're not a member of this project.")}
152-
description={t(
153-
`You'll need to join a team with access before you can view this data.`
154-
)}
155152
action={
156153
<Field>
157154
<StyledSelectControl
@@ -170,9 +167,11 @@ class MissingProjectMembership extends Component<Props, State> {
170167
)}
171168
</Field>
172169
}
173-
/>
170+
>
171+
{t(`You'll need to join a team with access before you can view this data.`)}
172+
</EmptyMessage>
174173
) : (
175-
<EmptyMessage icon={<IconFlag size="xl" />}>
174+
<EmptyMessage icon={<IconFlag />}>
176175
{t(
177176
'No teams have access to this project yet. Ask an admin to add your team to this project.'
178177
)}

static/app/views/insights/pages/platform/nextjs/serverTree.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ export function ServerTree() {
200200
{treeRequest.isLoading ? (
201201
<LoadingIndicator />
202202
) : hasData ? null : (
203-
<EmptyMessage size="large" icon={<IconSearch size="xl" />}>
203+
<EmptyMessage size="lg" icon={<IconSearch />}>
204204
{t('No results found')}
205205
</EmptyMessage>
206206
)}

static/app/views/insights/pages/platform/shared/table/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export function PlatformInsightsTable<DataRow extends Record<string, any>>({
5858
columnSortBy={[]}
5959
minimumColWidth={COL_WIDTH_MINIMUM}
6060
emptyMessage={
61-
<EmptyMessage size="large" icon={<IconSearch size="xl" />}>
61+
<EmptyMessage size="lg" icon={<IconSearch />}>
6262
{t('No results found')}
6363
</EmptyMessage>
6464
}

static/app/views/organizationStats/usageTable.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,15 @@ class UsageTable extends Component<Props> {
5252
if (errorMessage.projectStats.responseJSON.detail === 'No projects available') {
5353
return (
5454
<EmptyMessage
55-
icon={<IconWarning color="gray300" legacySize="48px" />}
55+
icon={<IconWarning />}
5656
title={t(
5757
"You don't have access to any projects, or your organization has no projects."
5858
)}
59-
description={tct('Learn more about [link:Project Access]', {
59+
>
60+
{tct('Learn more about [link:Project Access]', {
6061
link: <ExternalLink href={DOCS_URL} />,
6162
})}
62-
/>
63+
</EmptyMessage>
6364
);
6465
}
6566
return <IconWarning color="gray300" legacySize="48px" />;

static/app/views/releases/detail/commitsAndFiles/emptyState.tsx

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,11 @@ export function NoReleaseRepos() {
2929
<Main width="full">
3030
<Panel dashedBorder>
3131
<EmptyMessage
32-
icon={<IconCommit size="xl" />}
32+
icon={<IconCommit />}
3333
title={t('Releases are better with commit data!')}
34-
description={t('No commits associated with this release have been found.')}
35-
/>
34+
>
35+
{t('No commits associated with this release have been found.')}
36+
</EmptyMessage>
3637
</Panel>
3738
</Main>
3839
</Body>
@@ -45,17 +46,18 @@ export function NoRepositories({orgSlug}: {orgSlug: string}) {
4546
<Main width="full">
4647
<Panel dashedBorder>
4748
<EmptyMessage
48-
icon={<IconCommit size="xl" />}
49+
icon={<IconCommit />}
4950
title={t('Releases are better with commit data!')}
50-
description={t(
51-
'Connect a repository to see commit info, files changed, and authors involved in future releases.'
52-
)}
5351
action={
5452
<LinkButton priority="primary" to={`/settings/${orgSlug}/repos/`}>
5553
{t('Connect a repository')}
5654
</LinkButton>
5755
}
58-
/>
56+
>
57+
{t(
58+
'Connect a repository to see commit info, files changed, and authors involved in future releases.'
59+
)}
60+
</EmptyMessage>
5961
</Panel>
6062
</Main>
6163
</Body>

0 commit comments

Comments
 (0)