This package is used to generate documentation for Perses UI components using Storybook.
- Run Storybook in development mode:
npm run storybook
- Build only:
npm run storybook:build
- Build & serve static assets from build:
npm run storybook:serve
Stories SHOULD live in the first entry in the list below that matches their purpose:
- Stories that are tightly coupled with a component should be colocated with the component code in the associated package (e.g. the stories for the
LineChart
component live atui/components/src/LineChart/LineChart.stories.tsx
as a sibling ofLineChart.tsx
). - Package-specific stories (e.g. top-level "About" story describing the package, stories showing how to use multiple components in the package together) should live in the
src/stories
directory for that package. - Stories that provide project-wide documentation or involve components from multiple packages should live in the Storybook package (
ui/storybook/stories
).
Stories for components should use Component Story Format (CSF) and follow the PascalCaseName.stories.tsx
naming scheme.
See Writing stories in TypeScript for some additional guidance on typing for stories.
Stories that are primarily text-based documentation (e.g. the About
pages for each package) should be written using MDX and follow the PascalCase.stories.mdx
naming scheme.
These may include importing a few small components like the LinkTo
component from the Link addon to link out to other stories.
Stories should import package code by referencing the package, not by making a relative reference. Under the hood, this will reference source code (not compiled code), so we can benefit from some Storybook features like autogenerating documentation from types.
Import like this in a story:
// Import from the package. Storybook's webpack config will alias this to the
// associated source code.
import { LineChart } from '@perses-dev/components';
Not like this:
// Importing directly from the source file will make it easier to forget to
// export components for consumption from the main export for the package.
import { LineChart } from '.';
Stories should import from source code (NOT compiled code) because this enables us to automatically generate documentation tables for component props from types. Stories should import from the top level export for that package to help ensure the component was properly exported for consumption from the package when published. The same versions of code need to be used across the Storybook to avoid issues when using context
and singletons.
The project enforces the above contraints using the following tooling:
- aliases to source for all @perses-dev packages in Storybook's webpack config - E.g.
@perses-dev/components
will alias toui/components/src
instead of pointing toui/components/dist
). This makes it easy to import from packages in stories in a consistent way while pointing at the top levelsrc
export. It also ensures that components referenced indirectly in stories (usually a child of the component used directly in the story) point to source code to avoid accidentally mixing and matching source and compiled code, which can cause issues when usingcontext
and singletons. - alias to source for the specific @perses-dev package in package-specific tsconfig - E.g. a story in the
@perses-dev/components
package can import from@perses-dev/components
and have TypeScript look inui/components/src
for types. Without this, you would get type errors because TypeScript will expect the package to have installed itself.
Below are some notable addons this project currently uses.
- Accessibility - Helpful for making components a11y.
- Actions - Used to display data received by event handlers.
- Controls - Interact with component inputs dynamically in the Storybook UI.
- Dark Mode - Adds a toolbar toggle that switches between light and dark mode.
- Docs - Document component usage and properties in Markdown.
- Links - Use to create links that navigate between stories.
- Measure - Helpful for inspecting layouts by visualizing the box model.
- Mock Service Worker - Allows mocking of api calls using Mock Service Worker.
- Outline - Outline all elements with CSS to help with layout placement and alignment.
- Storysource - Used to show stories source in the addon panel
- Viewport - Allows stories to be displayed in different sizes and layouts.
We primarily use first party addons maintained by Storybook to avoid pain with upgrades and interoperability with the relatively complex Storybook ecosystem.
Decorators provide a way to wrap a story in extra "rendering" functionality.
- Decorators should have unique names across the project to avoid type conflicts and reduce confusion about the code.
- Decorator components should start with the word "With" and be named using
UpperCamelCase
(e.g.WithDashboard
). This provides consistency with Storybook's addons. - Parameters used to configured a decorator should be named the same as the component but using
lowerCamelCase
(e.g.withDashboard
). This provides consistency with Storybook's addons. - Recommend extending the
Parameters
interface in the@storybook/react
module in TypeScript for decorators configurable with parameters to assist with typing in story configuration.
Global decorators are specified in the Storybook configuration and are applied to all stories. These decorators live in the storybook
package in src/config/decorators
and are primarily related to global storybook behavior (e.g. theming, time zones).
Perses relies heavily on React context, which can be tedious to set up over and over again in every story. We simplify this by creating decorators to cover common cases related to React context. These decorators should:
- Live in the package that defines the associated context/providers. E.g. a decorator for
PluginRegistry
should live in theplugin-system
package. - Be located in
src/stories/shared-utils/decorators
in that package. This provides a consistent place to look for decorators that communicates it is shareable across packages. - Named starting with
With
followed by the name of the matching context (e.g. the decorator for thePluginRegistry
provider is namedWithPluginRegistry
, the decorator forTimeSeriesProvider
is namedWithTimeSeries
). - When possible, the decorator should have a default value for the context/provider that enables it to be used out-of-the-box in common use cases.
- When possible, the decorator should include an option to modify the behavior using storybook
parameters
. See the current decorators for some examples.
We do not expose Storybook utilties like decorators on our compiled packages because they are internal tooling. Shared decorators need to be imported by including a more specific path. For example.
import { WithPluginRegistry, WithTimeRange } from '@perses-dev/plugin-system/src/stories/shared-utils';
Some decorators are not related to a specific Perses package (e.g. decorators for context from an external library like use-query-params
) and are not global. These packages should:
- Live in the
storybook
package. - Be located in
src/decorators
. - If they are related to context, follow the decorator naming configuration guidelines recommended for package-specific decorators.
The storybook
package is an internal utility that is not published, so we can export shared utilities from it and import from the top level export. For example:
import { WithQueryClient, WithQueryParams } from '@perses-dev/storybook';
Storybook configuration lives in src/config
and includes the following:
main.ts
- Core configuration.preview.ts
- Rendering configurationdecorators
- Directory for global decorators that wrap all stories.WithBackground
- Decorator that allows toggling between different background colors from the projects theme. It relies on significant prior art from the backgrounds addon, which we cannot easily use here because it requires the background colors to be hardcoded (instead of dynamic based on being in dark/light mode).WithThemes
- Decorator that wraps all stories with theming for MUI and echarts.
DocsContainer
- Custom container for the docs addon to make it work with the dark mode addon.
Storybook is currently configured to build with Webpack 5.
We primarily use Jest and Playwright for writing tests. However, there are use cases where we want to test real browser-based interactions (i.e. not jsdom, which is used in Jest tests) for lower-level react components. In this case, Storybook interaction tests can be a good fit.
To test locally:
- Start storybook:
npm run storybook
- Open the specified story in your browser.
- Use the "interactions" tab in the addons drawer to see the results interactively.
- Run the tests from the command line:
npm run storybook:test
This project uses a free open source account from Happo for our visual testing. Visual tests generated for storybook use happo-plugin-storybook
(relevant documentation) and are listed under the perses-storybook
project in Happo. See the e2e
package for information about visual tests generated using that tooling.
- Use visual tests for use cases where a different type of test will not provide adequate coverage (e.g. canvas-based visualizations, styling).
- Only create visual tests that can reliably be reproduced. Flaky tests are often worse than no tests at all because they lead to toil and reduce trust in the overall test set. Some examples of things that can lead to unreliable tests are:
- Inconsistent data sources. Consider using consistent mock data to avoid this. See
mockQueryRangeRequests
inDashboardPage
for an example. Make sure to reset any mocked routes usingunroute
when the test is finished. - Time zones. Tests are automatically decorated with
TimeZoneProvider
set toUTC
when running in Happo to provide consistent time zone usage for visual testing. - Current time.
- Dynamic content. Wait for everything to load before taking a snapshot.
- If individual elements are known to cause inconsistencies, consider adding the
data-happo-hide
attribute. This will render the element invisible in the screenshot.
- Inconsistent data sources. Consider using consistent mock data to avoid this. See
By default, visual tests will be taken for every story in storybook. You can disable stories by setting parameters.happo
to false
(see relevant docs).
By default, visual tests will be taking for both light mode and dark mode. You can modify this by setting parameters.happo.themes
to an array of the themes you want (e.g. ['light']
to just use light mode). This is a customization we configured on top of happo's built in behavior, so this is not mentioned in their documentation.
Happo does its best to wait for the page to stabilize before taking a screenshot, but it does not do anything special for canvas. To work around this, you can use the parameters.happo.beforeScreenshot
in concert with an internal waitForStableCanvas
utility in the storybook package to attempt to wait for a stable canvas element before taking a screenshot.
import { waitForStableCanvas } from '@perses-dev/storybook';
const meta: Meta<typeof LineChart> = {
component: LineChart,
parameters: {
happo: {
beforeScreenshot: async () => {
await waitForStableCanvas('canvas');
},
},
},
};
We have some customization over the default happo-storybook
configuration related to some differences in our codebase.
- We explicitly build storybook before running happo and have
usePrebuiltPackage
set totrue
. This is needed because the name of the executable for storybook changed in v7 and happo hasn't updated to account for it yet. - We need to set explicitly set the
HAPPO_COMMAND
env var when callinghappo-ci-github-actions
to run in CI. Their default script looks in the wrong location because it doesn't fully account for the complexity of a monorepo setup like ours.
- Open the storybook iframe url: http://localhost:6006/iframe.html
- Run the following in the console to move through stories the way happo does when taking snapshots:
happo.nextExample().then((item) => console.log(item))
We have a small number of common helper utilities for use in stories that live in the storybook
package. In order to avoid errors around TS rootDir, you can leverage these utilities by doing the following:
- Add
@perses-dev/storybook
as adevDependency
. This is important to avoid attempting to ship these utilities when we build the public packages. Referencing the utilities this way avoids TSrootDir
errors that would occur if we attempted to reference them using a relative file path.
To make this work properly, we must do the following in the storybook
package config:
- DO NOT set other packages as dependencies in
package.json
. If we do this, turborepo will throw circular dependency errors. Instead, we use a combination of webpack and tsconfig aliases to reference the source code for these packages. - Set an alias for
@perses-dev/storybook
in the storybook webpack config. Otherwise, it has trouble finding the package code.
Below are some known issues related to Storybook that developers should be aware of.
These issues are related to our specific use of Storybook.
- Stories reference TypeScript source code (instead of compiled code) from packages to be able to autogenerate documentation from component prop types.
- The webpack configuration for Storybook includes aliases for internal packages to ensure consistent reference of source code throughout TypeScript. Without this, issues can occur with mixing and matching
src
anddist
versions of package context/providers.
- The webpack configuration for Storybook includes aliases for internal packages to ensure consistent reference of source code throughout TypeScript. Without this, issues can occur with mixing and matching
- Storybook compiles code using Webpack and packages compile code using SWC. This is not currently causing any issues, but could lead to inconsistencies in the future.
- There is a Storybook plugin for building using SWC, but it is not a core Storybook plugin, and it broke several Storybook features when we attempted to use it.
- The grid and background color toolbar items do not play nicely together because of some customizations we're doing to get the backgrounds to use our theme. For now, disabling the grid feature until we have time to dig into this more. Other Storybook addons (e.g. measure, outline) and browser extensions provide similar behavior, so this is not a priority to fix.
These issues are related to generic Storybook issues that do not appear to be specific to our setup.
Warning: ReactDOM.render is no longer supported in React 18
error in console. It seems like a known issue based on discussion in this issue and some other issues on the Storybook repository. Hoping this will get resolved eventually in a future update, but can be ignored for now.Warning: Attempted to synchronously unmount a root while React was already rendering.
- see issue