Skip to content

Commit fc5f5fd

Browse files
Abdkhan14Abdullah Khan
andauthored
feat(explore-attr-breakdowns): Adding error and empty search state UI (#104012)
<img width="1093" height="361" alt="Screenshot 2025-11-25 at 4 21 12 PM" src="https://github.com/user-attachments/assets/af04d008-93a4-4f9a-b395-dca365279b8f" /> <img width="1073" height="580" alt="Screenshot 2025-11-25 at 3 54 48 PM" src="https://github.com/user-attachments/assets/33fe1339-e35a-4bba-837d-6d0b5f418125" /> <img width="1099" height="615" alt="Screenshot 2025-11-25 at 4 20 29 PM" src="https://github.com/user-attachments/assets/fac14c48-edf2-4873-b1e2-4e6645257c80" /> --------- Co-authored-by: Abdullah Khan <[email protected]>
1 parent 58959df commit fc5f5fd

File tree

3 files changed

+108
-34
lines changed

3 files changed

+108
-34
lines changed

static/app/views/explore/components/attributeBreakdowns/attributeDistributionContent.tsx

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import {Button} from '@sentry/scraps/button/button';
66
import {ButtonBar} from '@sentry/scraps/button/buttonBar';
77
import {Flex} from '@sentry/scraps/layout';
88

9-
import LoadingError from 'sentry/components/loadingError';
109
import Panel from 'sentry/components/panels/panel';
1110
import BaseSearchBar from 'sentry/components/searchBar';
1211
import {IconChevron} from 'sentry/icons/iconChevron';
@@ -64,13 +63,13 @@ export function AttributeDistribution() {
6463
const {
6564
data: attributeBreakdownsData,
6665
isLoading: isAttributeBreakdownsLoading,
67-
isError: isAttributeBreakdownsError,
66+
error: attributeBreakdownsError,
6867
} = useAttributeBreakdowns();
6968

7069
const {
7170
data: cohortCountResponse,
7271
isLoading: isCohortCountLoading,
73-
isError: isCohortCountError,
72+
error: cohortCountError,
7473
} = useApiQuery<{data: Array<{'count()': number}>}>(
7574
[
7675
`/organizations/${organization.slug}/events/`,
@@ -139,13 +138,11 @@ export function AttributeDistribution() {
139138
setPage(0);
140139
}, [filteredAttributeDistribution]);
141140

142-
if (isAttributeBreakdownsError || isCohortCountError) {
143-
return <LoadingError message={t('Failed to load attribute breakdowns')} />;
144-
}
141+
const error = attributeBreakdownsError ?? cohortCountError;
145142

146143
return (
147144
<Panel>
148-
<Flex direction="column" gap="xl" padding="xl">
145+
<Flex direction="column" gap="2xl" padding="xl">
149146
<Fragment>
150147
<ControlsContainer>
151148
<StyledBaseSearchBar
@@ -160,6 +157,8 @@ export function AttributeDistribution() {
160157
</ControlsContainer>
161158
{isAttributeBreakdownsLoading || isCohortCountLoading ? (
162159
<AttributeBreakdownsComponent.LoadingCharts />
160+
) : error ? (
161+
<AttributeBreakdownsComponent.ErrorState error={error} />
163162
) : filteredAttributeDistribution.length > 0 ? (
164163
<Fragment>
165164
<ChartsGrid>
@@ -204,7 +203,7 @@ export function AttributeDistribution() {
204203
</PaginationContainer>
205204
</Fragment>
206205
) : (
207-
<NoAttributesMessage>{t('No matching attributes found')}</NoAttributesMessage>
206+
<AttributeBreakdownsComponent.EmptySearchState />
208207
)}
209208
</Fragment>
210209
</Flex>
@@ -222,13 +221,6 @@ const StyledBaseSearchBar = styled(BaseSearchBar)`
222221
flex: 1;
223222
`;
224223

225-
const NoAttributesMessage = styled('div')`
226-
display: flex;
227-
justify-content: center;
228-
align-items: center;
229-
color: ${p => p.theme.subText};
230-
`;
231-
232224
const ChartsGrid = styled('div')`
233225
display: grid;
234226
grid-template-columns: repeat(${CHARTS_COLUMN_COUNT}, 1fr);

static/app/views/explore/components/attributeBreakdowns/cohortComparisonContent.tsx

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {Flex} from '@sentry/scraps/layout';
99

1010
import type {Selection} from 'sentry/components/charts/useChartXRangeSelection';
1111
import {Text} from 'sentry/components/core/text';
12-
import LoadingError from 'sentry/components/loadingError';
1312
import Panel from 'sentry/components/panels/panel';
1413
import BaseSearchBar from 'sentry/components/searchBar';
1514
import {IconChevron} from 'sentry/icons/iconChevron';
@@ -40,7 +39,7 @@ export function CohortComparison({
4039

4140
const yAxis = visualizes[chartIndex]?.yAxis ?? '';
4241

43-
const {data, isLoading, isError} = useAttributeBreakdownComparison({
42+
const {data, isLoading, error} = useAttributeBreakdownComparison({
4443
aggregateFunction: yAxis,
4544
range: selection.range,
4645
});
@@ -109,13 +108,9 @@ export function CohortComparison({
109108
};
110109
}, [selection]);
111110

112-
if (isError) {
113-
return <LoadingError message={t('Failed to load attribute breakdowns')} />;
114-
}
115-
116111
return (
117112
<Panel data-explore-chart-selection-region>
118-
<Flex direction="column" gap="xl" padding="xl">
113+
<Flex direction="column" gap="2xl" padding="xl">
119114
<ControlsContainer>
120115
<StyledBaseSearchBar
121116
placeholder={t('Search keys')}
@@ -129,6 +124,8 @@ export function CohortComparison({
129124
</ControlsContainer>
130125
{isLoading ? (
131126
<AttributeBreakdownsComponent.LoadingCharts />
127+
) : error ? (
128+
<AttributeBreakdownsComponent.ErrorState error={error} />
132129
) : (
133130
<Fragment>
134131
{selectedRangeToDates && (
@@ -187,9 +184,7 @@ export function CohortComparison({
187184
</PaginationContainer>
188185
</Fragment>
189186
) : (
190-
<NoAttributesMessage>
191-
{t('No matching attributes found')}
192-
</NoAttributesMessage>
187+
<AttributeBreakdownsComponent.EmptySearchState />
193188
)}
194189
</Fragment>
195190
)}
@@ -208,13 +203,6 @@ const StyledBaseSearchBar = styled(BaseSearchBar)`
208203
flex: 1;
209204
`;
210205

211-
const NoAttributesMessage = styled('div')`
212-
display: flex;
213-
justify-content: center;
214-
align-items: center;
215-
color: ${p => p.theme.subText};
216-
`;
217-
218206
const ChartsGrid = styled('div')`
219207
display: grid;
220208
grid-template-columns: repeat(${CHARTS_COLUMN_COUNT}, 1fr);

static/app/views/explore/components/attributeBreakdowns/styles.tsx

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import {Flex} from '@sentry/scraps/layout/flex';
88
import BaseChart from 'sentry/components/charts/baseChart';
99
import {Text} from 'sentry/components/core/text';
1010
import Placeholder from 'sentry/components/placeholder';
11+
import {IconSearch, IconTimer, IconWarning} from 'sentry/icons';
1112
import {IconMegaphone} from 'sentry/icons/iconMegaphone';
12-
import {t} from 'sentry/locale';
13+
import {t, tct} from 'sentry/locale';
14+
import type RequestError from 'sentry/utils/requestError/requestError';
1315
import {useFeedbackForm} from 'sentry/utils/useFeedbackForm';
1416

1517
function FeedbackButton() {
@@ -106,7 +108,7 @@ function LoadingCharts() {
106108
}, []);
107109

108110
return (
109-
<Flex direction="column" gap="xl">
111+
<Flex direction="column" gap="2xl">
110112
{showMessage && (
111113
<Text size="md" variant="muted">
112114
{t(
@@ -123,6 +125,96 @@ function LoadingCharts() {
123125
);
124126
}
125127

128+
function FeedbackLink() {
129+
const openForm = useFeedbackForm();
130+
131+
if (!openForm) {
132+
return (
133+
<a href="mailto:[email protected]?subject=Attribute%20breakdowns%20failed%20to%20load">
134+
{t('Send us feedback')}
135+
</a>
136+
);
137+
}
138+
139+
return (
140+
<a href="#" onClick={() => openForm?.()}>
141+
{t('Send us feedback')}
142+
</a>
143+
);
144+
}
145+
146+
const StyledIconSearch = styled(IconSearch)`
147+
color: ${p => p.theme.subText};
148+
`;
149+
150+
const StyledIconWarning = styled(IconWarning)`
151+
color: ${p => p.theme.subText};
152+
`;
153+
154+
const StyledIconTimer = styled(IconTimer)`
155+
color: ${p => p.theme.subText};
156+
`;
157+
158+
const ERROR_STATE_CONFIG: Record<
159+
number | 'default',
160+
{
161+
icon: React.ReactNode;
162+
subtitle: React.ReactNode;
163+
title: string;
164+
}
165+
> = {
166+
504: {
167+
title: t('Query timed out'),
168+
icon: <StyledIconTimer size="xl" />,
169+
subtitle: tct(
170+
'You can try narrowing the time range. Seeing this often? [feedbackLink]',
171+
{
172+
feedbackLink: <FeedbackLink />,
173+
}
174+
),
175+
},
176+
default: {
177+
title: t('Failed to load attribute breakdowns'),
178+
icon: <StyledIconWarning size="xl" />,
179+
subtitle: tct('Seeing this often? [feedbackLink]', {
180+
feedbackLink: <FeedbackLink />,
181+
}),
182+
},
183+
};
184+
185+
function ErrorState({error}: {error: RequestError}) {
186+
const config =
187+
ERROR_STATE_CONFIG[error?.status ?? 'default'] ?? ERROR_STATE_CONFIG.default;
188+
189+
return (
190+
<Flex direction="column" gap="2xl" padding="3xl" align="center" justify="center">
191+
{config.icon}
192+
<Text size="xl" variant="muted">
193+
{config.title}
194+
</Text>
195+
<Text size="md" variant="muted">
196+
{config.subtitle}
197+
</Text>
198+
</Flex>
199+
);
200+
}
201+
202+
function EmptySearchState() {
203+
return (
204+
<Flex direction="column" gap="2xl" padding="3xl" align="center" justify="center">
205+
<StyledIconSearch size="xl" />
206+
<Text size="xl" variant="muted">
207+
{t('No matching attributes found')}
208+
</Text>
209+
<Text size="md" variant="muted">
210+
{tct('Expecting results? [feedbackLink]', {
211+
feedbackLink: <FeedbackLink />,
212+
})}
213+
</Text>
214+
</Flex>
215+
);
216+
}
217+
126218
const StyledPlaceholder = styled(Placeholder)<{_height: number; _width: number}>`
127219
border-radius: ${p => p.theme.borderRadius};
128220
height: ${p => p._height}px;
@@ -187,4 +279,6 @@ const StyledFeedbackButton = styled(Button)`
187279
export const AttributeBreakdownsComponent = {
188280
FeedbackButton,
189281
LoadingCharts,
282+
ErrorState,
283+
EmptySearchState,
190284
};

0 commit comments

Comments
 (0)