Skip to content

Commit

Permalink
feat(dashboards): Add documentation for widget state management (#86205)
Browse files Browse the repository at this point in the history
A longer, more practical example, with explanations of how to handle
loading, errors, and other states.
  • Loading branch information
gggritso authored Mar 3, 2025
1 parent 195be51 commit 2463cfd
Showing 1 changed file with 133 additions and 23 deletions.
156 changes: 133 additions & 23 deletions static/app/views/dashboards/widgets/widget/widget.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@ export default storyBook('Widget', (story, APIReference) => {
APIReference(types.exported);

story('Getting Started', () => {
const isLoading = false;
const hasError = false;

return (
<Fragment>
<p>
Expand All @@ -44,16 +41,10 @@ export default storyBook('Widget', (story, APIReference) => {
</Widget.WidgetToolbar>
}
Visualization={
isLoading ? (
<TimeSeriesWidgetVisualization.LoadingPlaceholder />
) : hasError ? (
<Widget.WidgetError error="Oh no!" />
) : (
<TimeSeriesWidgetVisualization
visualizationType="line"
timeSeries={[sampleDurationTimeSeries]}
/>
)
<TimeSeriesWidgetVisualization
visualizationType="line"
timeSeries={[sampleDurationTimeSeries]}
/>
}
Footer={<span>This data is incomplete!</span>}
/>
Expand Down Expand Up @@ -125,16 +116,10 @@ import {Widget} from './widget';
</Widget.WidgetToolbar>
}
Visualization={
isLoading ? (
<TimeSeriesWidgetVisualization.Placeholder />
) : hasError ? (
<Widget.Error error="Oh no!" />
) : (
<TimeSeriesWidgetVisualization
visualizationType="line"
timeSeries={[sampleDurationTimeSeries]}
/>
)
<TimeSeriesWidgetVisualization
visualizationType="line"
timeSeries={[sampleDurationTimeSeries]}
/>
}
Footer={<span>This data is incomplete!</span>}
/>
Expand All @@ -144,6 +129,131 @@ import {Widget} from './widget';
</Fragment>
);
});

story('Managing UI States', () => {
return (
<Fragment>
<p>
A common task for widget rendering is managing states. The example above omits
this aspect of widget rendering. Here are some examples of common widget states:
</p>

<ul>
<li>error (the data could not be loaded, or is invalid)</li>
<li>loading (the data is being fetched)</li>
<li>normal (data fetched successfully)</li>
<li>no data (data loaded, but nothing came back)</li>
<li>upsell (feature not configured, show instructions)</li>
</ul>

<p>
<JSXNode name="Widget" /> does not handle this, it is up to you to implement
those states. Below is an example of this kind of handling, with some guidance
on how to do it well.
</p>

<CodeSnippet language="jsx">
{`import {Widget} from './widget';
function InsightsLineChart() {
// Fictional hooks, just for this example
const {organization} = useOrganization();
const {data, isLoading, error, retry} = useInsightsData();
// Title should probably be the same in all states, but that's up to you
const Title = <Widget.WidgetTitle title="eps()" />;
if (!organization.features.includes('insights')) {
// If there's a configuration or upsell state, handle this first.
// Try to provide an action that a user can take to remedy the problem.
return (
<Widget
Title={Title}
Visualization={
<div>
<p>Sorry, this feature is not available!</p>
<Button onClick={contactSupport}>Contact Support</Button>
</div>
}
/>
);
}
if (isLoading) {
// Loading states take precedence over error states!
// Show a placeholder that makes it obvious that the data is still loading.
// This state should be different from the "no data" state, described below.
return (
<Widget
Title={Title}
Visualization={<TimeSeriesWidgetVisualization.LoadingPlaceholder />}
/>
);
}
if (error) {
// Most importantly, an error state should clearly describe what went wrong.
// Secondly, consider a "Retry" button in the toolbar for this case. If the
// error is sporadic, a retry might get the data loaded. If the widget is
// misconfigured, a retry won't help, so showing a "Retry" button is not a
// good idea
return (
<Widget
Title={Title}
Actions={
<Widget.WidgetToolbar>
<Button size="xs" onClick={retry}>
Retry
</Button>
</Widget.WidgetToolbar>
}
Visualization={<Widget.WidgetError error={error} />}
/>
);
}
if (!data) {
// Sometimes, the data loads but the response is empty. Consider why this
// might happen in your use case, and provide some information to the user.
// In most cases, "no data" is different from the loading state. It might be
// closer to an error state!
return (
<Widget
Title={Title}
Visualization={
<Widget.WidgetError
error={'No data! This is unusual, consider contacting support'}
/>
}
/>
);
}
// If all is well, show all the relevant UI! Note that some actions (like "Add
// to Dashboard") are probably fine to show in error states. Other actions,
// like "Open in Full Screen" might need to be hidden if they cannot work
// without data. Do what's right for the user!
return (
<Widget
Title={Title}
Actions={
<Widget.WidgetToolbar>
<Button size="xs" onClick={addToDashboard}>
Add to Dashboard
</Button>
<Widget.WidgetDescription description="This shows how often the event is happening" />
</Widget.WidgetToolbar>
}
Visualization={<TimeSeriesWidgetVisualization ...}
/>
);
}
`}
</CodeSnippet>
</Fragment>
);
});
});

const SmallSizingWindow = styled(SizingWindow)`
Expand Down

0 comments on commit 2463cfd

Please sign in to comment.