Chore: Bump storybook to v6 (#28926)

* Wip

* feat: get storybook and app building locally

* docs: comment webpack react alias

* build(grafana-ui): put back ts-loader

* build: prefer storybook essentials over actions and docs. bump dark-mode

* chore(storybook): migrate to latest config

* build: prevent test env throwing Invalid hook call errors

* chore: lodash resolves to package dependency rather than project

* use decorators as variable instead of function

* perf(storybook): reduce bundling time by splitting type check and compilation

* refactor(storybook): use sortOrder.order param to sort intro story first

* build: use yarn workspace command

* refactor(storybook): use previous knobs addon registration

* migrate button story to controls

* build(storybook): silence warnings in console

* build: bump storybook related ts packages

* style: remove trailing whitespace

* refactor(graphng): export interface for storybook

* controls migration guide

* fix typo

* docs(storybook): default docs to use dark theme as per current implementation

* revert(grafana-ui): put back react-is namedExport

this was changed for react 17 bump but causes rollup to fail during bundling

* chore: bump storybook to 6.1, enable fastRefresh, silence eslint prop-types

* docs(grafana-ui): move knobs -> controls migration guide to storybook style-guide

* chore(storybook): silence terminal warning about order of docs addon

* Update contribute/style-guides/storybook.md

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>

* Apply documentation suggestions

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>

* chore(storybook): bump to 6.1.2

Co-authored-by: Peter Holmberg <peter.hlmbrg@gmail.com>
Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>
This commit is contained in:
Jack Westbrook 2020-11-24 10:38:41 +01:00 committed by GitHub
parent ce39e12e00
commit 0fc8426bf1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 3703 additions and 3234 deletions

View File

@ -64,13 +64,13 @@ To link a components stories with an MDX file you have to do this:
```jsx
// In TabsBar.story.tsx
import { TabsBar } from "./TabsBar";
import { TabsBar } from './TabsBar';
// Import the MDX file
import mdx from "./TabsBar.mdx";
import mdx from './TabsBar.mdx';
export default {
title: "General/Tabs/TabsBar",
title: 'General/Tabs/TabsBar',
component: TabsBar,
parameters: {
docs: {
@ -93,8 +93,8 @@ There are some things that the MDX file should contain:
```jsx
// In MyComponent.mdx
import { Props } from "@storybook/addon-docs/blocks";
import { MyComponent } from "./MyComponent";
import { Props } from '@storybook/addon-docs/blocks';
import { MyComponent } from './MyComponent';
<Props of={MyComponent} />;
```
@ -141,39 +141,66 @@ interface MyProps {
}
```
### Knobs
### Controls
Knobs is an [addon to Storybook](https://github.com/storybookjs/storybook/tree/master/addons/knobs) which can be used to easily switch values in the UI. A good use case for it is to try different props for the component. Using knobs is easy. Grafana is set up so knobs can be used straight out of the box. Here is an example of how you might use it.
The [controls addon](https://storybook.js.org/docs/react/essentials/controls) provides a way to interact with a component's properties dynamically and requires much less code than knobs. We're deprecating knobs in favor of using controls.
```jsx
// In MyComponent.story.tsx
#### Migrating a story from Knobs to Controls
import { number, text } from "@storybook/addon-knobs";
As a test, we migrated the [button story](https://github.com/grafana/grafana/blob/master/packages/grafana-ui/src/components/Button/Button.story.tsx). Here's the guide on how to migrate a story to controls.
export const basicStory = () => (
<MyComponent
max={number("Max value", 10)}
min={number("Min value", -10)}
title={text("Title", "Look at the value!")}
/>
);
```
1. Remove the `@storybook/addon-knobs` dependency.
2. Import the Story type from `@storybook/react`
The general convention is that the first parameter of the knob is its name and the second is the default value. There are some more types:
`import { Story } from @storybook/react`
| Knob | Description |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `text` | Any text field |
| `number` | Any number input. Also [available as range](https://github.com/storybookjs/storybook/tree/master/addons/knobs#number-bound-by-range) |
| `boolean` | A switch between true/false |
| `color` | Color picker |
| `object` | JSON input or array. Good to use if the property requires more complex data structures. |
| `array` | Array of strings separated by a comma |
| `select` | Select a value from an options object. Good for trying different test cases. |
| `options` | Configurable UI for selecting a range of options |
| `files` | File selector |
| `date` | Select date as stringified Unix timestamp |
| `button` | Has a handler which is called when clicked |
3. Import the props interface from the component you're working on (these must be exported in the component).
`import { Props } from './Component'`
4. Add the Story type to all stories in the file, then replace the props sent to the component
and remove any knobs.
Before
```tsx
export const Simple = () => {
const prop1 = text('Prop1', 'Example text');
const prop2 = select('Prop2', ['option1', 'option2'], 'option1');
return <Component prop1={prop1} prop2={prop2} />;
};
```
After
```tsx
export const Simple: Story<Props> = ({ prop1, prop2 }) => {
return <Component prop1={prop1} prop2={prop2} />;
};
```
5. Add default props (or args in Storybook language).
```tsx
Simple.args = {
prop1: 'Example text',
prop2: 'option 1',
};
```
6. If the component has advanced props type (ie. other than string, number, boolean), you need to
specify these in an `argTypes`. This is done in the default export of the story.
```tsx
export default {
title: 'Component/Component',
component: Component,
argTypes: {
prop2: { control: { type: 'select', options: ['option1', 'option2'] } },
},
};
```
## Best practices

View File

@ -20,6 +20,7 @@ module.exports = {
'\\.svg': '<rootDir>/public/test/mocks/svg.ts',
'\\.css': '<rootDir>/public/test/mocks/style.ts',
'monaco-editor/esm/vs/editor/editor.api': '<rootDir>/public/test/mocks/monaco.ts',
'^react($|/.+)': '<rootDir>/node_modules/react$1',
},
watchPathIgnorePatterns: ['<rootDir>/node_modules/'],
};

View File

@ -35,8 +35,8 @@
"start:ignoreTheme": "grafana-toolkit core:start --hot",
"start:noTsCheck": "grafana-toolkit core:start --noTsCheck",
"stats": "webpack --mode production --config scripts/webpack/webpack.prod.js --profile --json > compilation-stats.json",
"storybook": "cd packages/grafana-ui && yarn storybook --ci",
"storybook:build": "cd packages/grafana-ui && yarn storybook:build",
"storybook": "yarn workspace @grafana/ui storybook --ci",
"storybook:build": "yarn workspace @grafana/ui storybook:build",
"themes:generate": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/generateSassVariableFiles.ts",
"typecheck": "tsc --noEmit",
"plugins:build-bundled": "grafana-toolkit plugin:bundle-managed",
@ -308,7 +308,10 @@
],
"nohoist": [
"**/@types/*",
"**/@types/*/**"
"**/@types/*/**",
"@storybook",
"**/@storybook",
"**/@storybook/**"
]
},
"_moduleAliases": {

View File

@ -6,7 +6,8 @@
{
"files": ["**/*.{test,story}.{ts,tsx}"],
"rules": {
"no-restricted-imports": "off"
"no-restricted-imports": "off",
"react/prop-types": "off"
}
}
]

View File

@ -1,3 +1,8 @@
const path = require('path');
const TerserPlugin = require('terser-webpack-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const FilterWarningsPlugin = require('webpack-filter-warnings-plugin');
const stories = ['../src/**/*.story.{js,jsx,ts,tsx,mdx}'];
if (process.env.NODE_ENV !== 'production') {
@ -7,10 +12,140 @@ if (process.env.NODE_ENV !== 'production') {
module.exports = {
stories: stories,
addons: [
'@storybook/addon-docs',
'@storybook/addon-controls',
'@storybook/addon-knobs',
'@storybook/addon-actions',
'@storybook/addon-docs',
'storybook-dark-mode/register',
'@storybook/addon-storysource',
],
reactOptions: {
fastRefresh: true,
},
typescript: {
check: true,
reactDocgen: 'react-docgen-typescript',
reactDocgenTypescriptOptions: {
shouldExtractLiteralValuesFromEnum: true,
propFilter: (prop: any) => (prop.parent ? !/node_modules/.test(prop.parent.fileName) : true),
},
},
webpackFinal: async (config: any, { configType }: any) => {
const isProductionBuild = configType === 'PRODUCTION';
config.module.rules = [
...(config.module.rules || []),
{
test: /\.tsx?$/,
use: [
{
loader: require.resolve('ts-loader'),
options: {
transpileOnly: true,
configFile: path.resolve(__dirname, 'tsconfig.json'),
},
},
{
loader: require.resolve('react-docgen-typescript-loader'),
options: {
tsconfigPath: path.resolve(__dirname, 'tsconfig.json'),
// https://github.com/styleguidist/react-docgen-typescript#parseroptions
// @ts-ignore
propFilter: prop => {
if (prop.parent) {
return !prop.parent.fileName.includes('node_modules/@types/react/');
}
return true;
},
},
},
],
},
{
test: /\.scss$/,
use: [
{
loader: 'style-loader',
options: { injectType: 'lazyStyleTag' },
},
{
loader: 'css-loader',
options: {
importLoaders: 2,
},
},
{
loader: 'postcss-loader',
options: {
sourceMap: false,
config: { path: __dirname + '../../../../scripts/webpack/postcss.config.js' },
},
},
{
loader: 'sass-loader',
options: {
sourceMap: false,
},
},
],
},
{
test: require.resolve('jquery'),
use: [
{
loader: 'expose-loader',
query: 'jQuery',
},
{
loader: 'expose-loader',
query: '$',
},
],
},
];
config.optimization = {
nodeEnv: 'production',
moduleIds: 'hashed',
runtimeChunk: 'single',
splitChunks: {
chunks: 'all',
minChunks: 1,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/].*[jt]sx?$/,
chunks: 'initial',
priority: -10,
reuseExistingChunk: true,
enforce: true,
},
default: {
priority: -20,
chunks: 'all',
test: /.*[jt]sx?$/,
reuseExistingChunk: true,
},
},
},
minimize: isProductionBuild,
minimizer: isProductionBuild
? [
new TerserPlugin({ cache: false, parallel: false, sourceMap: false, exclude: /monaco|bizcharts/ }),
new OptimizeCSSAssetsPlugin({}),
]
: [],
};
config.resolve.alias['@grafana/ui'] = path.resolve(__dirname, '..');
// Silence "export not found" webpack warnings with transpileOnly
// https://github.com/TypeStrong/ts-loader#transpileonly
config.plugins.push(
new FilterWarningsPlugin({
exclude: /export .* was not found in/,
})
);
return config;
},
};

View File

@ -15,8 +15,8 @@ import lightTheme from '../../../public/sass/grafana.light.scss';
// @ts-ignore
import darkTheme from '../../../public/sass/grafana.dark.scss';
import { GrafanaLight, GrafanaDark } from './storybookTheme';
import { configure, addDecorator, addParameters } from '@storybook/react';
import { withKnobs } from '@storybook/addon-knobs';
import { configure } from '@storybook/react';
import addons from '@storybook/addons';
const handleThemeChange = (theme: any) => {
if (theme !== 'light') {
@ -27,37 +27,36 @@ const handleThemeChange = (theme: any) => {
lightTheme.use();
}
};
addDecorator(withTheme(handleThemeChange));
addDecorator(withKnobs);
addDecorator(withPaddedStory);
addParameters({
addons.setConfig({
showRoots: false,
theme: GrafanaDark,
});
export const decorators = [withTheme(handleThemeChange), withPaddedStory];
export const parameters = {
info: {},
docs: {
theme: GrafanaDark,
},
darkMode: {
dark: GrafanaDark,
light: GrafanaLight,
},
options: {
theme: GrafanaDark,
showPanel: true,
showRoots: true,
panelPosition: 'right',
showNav: true,
isFullscreen: false,
isToolshown: true,
storySort: (a: any, b: any) => {
if (a[1].kind.split('/')[0] === 'Docs Overview') {
return -1;
} else if (b[1].kind.split('/')[0] === 'Docs Overview') {
return 1;
}
return a[1].id.localeCompare(b[1].id);
storySort: {
method: 'alphabetical',
// Order Docs Overview and Docs Overview/Intro story first
order: ['Docs Overview', ['Intro']],
},
},
knobs: {
escapeHTML: false,
},
});
// @ts-ignore
configure(require.context('../src', true, /\.story\.(js|jsx|ts|tsx|mdx)$/), module);
};

View File

@ -1,121 +0,0 @@
const path = require('path');
const TerserPlugin = require('terser-webpack-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
module.exports = ({ config, mode }) => {
const isProductionBuild = mode === 'PRODUCTION';
config.module.rules = [
...(config.module.rules || []),
{
test: /\.tsx?$/,
use: [
{
loader: require.resolve('ts-loader'),
options: {
// transpileOnly: true,
configFile: path.resolve(__dirname, 'tsconfig.json'),
},
},
{
loader: require.resolve('react-docgen-typescript-loader'),
options: {
tsconfigPath: path.resolve(__dirname, 'tsconfig.json'),
// https://github.com/styleguidist/react-docgen-typescript#parseroptions
// @ts-ignore
propFilter: prop => {
if (prop.parent) {
return !prop.parent.fileName.includes('node_modules/@types/react/');
}
return true;
},
},
},
],
},
];
config.module.rules.push({
test: /\.scss$/,
use: [
{
loader: 'style-loader',
options: { injectType: 'lazyStyleTag' },
},
{
loader: 'css-loader',
options: {
importLoaders: 2,
},
},
{
loader: 'postcss-loader',
options: {
sourceMap: false,
config: { path: __dirname + '../../../../scripts/webpack/postcss.config.js' },
},
},
{
loader: 'sass-loader',
options: {
sourceMap: false,
},
},
],
});
config.module.rules.push({
test: require.resolve('jquery'),
use: [
{
loader: 'expose-loader',
query: 'jQuery',
},
{
loader: 'expose-loader',
query: '$',
},
],
});
config.optimization = {
nodeEnv: 'production',
moduleIds: 'hashed',
runtimeChunk: 'single',
splitChunks: {
chunks: 'all',
minChunks: 1,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/].*[jt]sx?$/,
chunks: 'initial',
priority: -10,
reuseExistingChunk: true,
enforce: true,
},
default: {
priority: -20,
chunks: 'all',
test: /.*[jt]sx?$/,
reuseExistingChunk: true,
},
},
},
minimize: isProductionBuild,
minimizer: isProductionBuild
? [
new TerserPlugin({ cache: false, parallel: false, sourceMap: false, exclude: /monaco|bizcharts/ }),
new OptimizeCSSAssetsPlugin({}),
]
: [],
};
config.resolve.extensions.push('.ts', '.tsx', '.mdx');
config.resolve.alias = config.resolve.alias || {};
config.resolve.alias['@grafana/ui'] = path.resolve(__dirname, '..');
config.stats = {
warningsFilter: /export .* was not found in/,
};
return config;
};

View File

@ -17,3 +17,7 @@ See [package source](https://github.com/grafana/grafana/tree/master/packages/gra
## Development
For development purposes we suggest using `yarn link` that will create symlink to @grafana/ui lib. To do so navigate to `packages/grafana-ui` and run `yarn link`. Then, navigate to your project and run `yarn link @grafana/ui` to use the linked version of the lib. To unlink follow the same procedure, but use `yarn unlink` instead.
### Storybook 6.x migration
We've upgraded Storybook to version 6 and with that we will convert to using [controls](https://storybook.js.org/docs/react/essentials/controls) instead of knobs for manipulating components. Controls will not require as much coding as knobs do. Please refer to the [storybook style-guide](https://github.com/grafana/grafana/blob/master/contribute/style-guides/storybook.md#contrls) for further information.

View File

@ -77,13 +77,12 @@
"@rollup/plugin-commonjs": "11.0.2",
"@rollup/plugin-image": "2.0.4",
"@rollup/plugin-node-resolve": "7.1.1",
"@storybook/addon-actions": "5.3.21",
"@storybook/addon-docs": "5.3.21",
"@storybook/addon-info": "5.3.21",
"@storybook/addon-knobs": "5.3.21",
"@storybook/addon-storysource": "5.3.21",
"@storybook/react": "5.3.21",
"@storybook/theming": "5.3.21",
"@storybook/addon-essentials": "6.1.2",
"@storybook/addon-controls": "6.1.2",
"@storybook/addon-knobs": "6.1.2",
"@storybook/addon-storysource": "6.1.2",
"@storybook/react": "6.1.2",
"@storybook/theming": "6.1.2",
"@types/classnames": "2.2.7",
"@types/common-tags": "^1.8.0",
"@types/d3": "5.7.2",
@ -101,16 +100,18 @@
"@types/tinycolor2": "1.4.1",
"common-tags": "^1.8.0",
"pretty-format": "25.1.0",
"react-docgen-typescript-loader": "3.7.1",
"react-docgen-typescript-loader": "3.7.2",
"react-test-renderer": "16.13.1",
"react-is": "16.8.0",
"rollup": "2.0.6",
"rollup-plugin-sourcemaps": "0.5.0",
"rollup-plugin-terser": "5.3.0",
"rollup-plugin-typescript2": "0.26.0",
"rollup-plugin-visualizer": "3.3.1",
"storybook-dark-mode": "0.6.1",
"ts-loader": "6.2.1",
"typescript": "4.0.2"
"storybook-dark-mode": "1.0.3",
"ts-loader": "8.0.11",
"typescript": "4.0.2",
"webpack-filter-warnings-plugin": "1.2.1"
},
"types": "src/index.ts"
}

View File

@ -43,7 +43,7 @@ const buildCjsPackage = ({ env }) => {
// When 'rollup-plugin-commonjs' fails to properly convert the CommonJS modules to ES6 one has to manually name the exports
// https://github.com/rollup/rollup-plugin-commonjs#custom-named-exports
namedExports: {
'../../node_modules/lodash/lodash.js': [
'node_modules/lodash/lodash.js': [
'flatten',
'find',
'upperFirst',
@ -82,8 +82,7 @@ const buildCjsPackage = ({ env }) => {
'useAbsoluteLayout',
'useFilters',
],
'../../node_modules/rc-tooltip/node_modules/react-is/index.js': ['isMemo'],
'../../node_modules/rc-motion/node_modules/react-is/index.js': ['isMemo'],
'../../node_modules/react-is/index.js': ['isMemo'],
},
}),
resolve(),

View File

@ -1,3 +1,5 @@
import { Meta } from '@storybook/addon-docs/blocks';
<Meta title="Docs Overview/Intro" parameters={{ options: { isToolshown: false }, id: 1 }} />
# Grafana design system

View File

@ -1,15 +1,21 @@
import React from 'react';
import { select, text, boolean } from '@storybook/addon-knobs';
import { Button, ButtonVariant } from '@grafana/ui';
import { Story } from '@storybook/react';
import { Button, ButtonProps } from './Button';
import { withCenteredStory, withHorizontallyCenteredStory } from '../../utils/storybook/withCenteredStory';
import { getIconKnob } from '../../utils/storybook/knobs';
import { iconOptions } from '../../utils/storybook/knobs';
import mdx from './Button.mdx';
import { ComponentSize } from '../../types/size';
export default {
title: 'Buttons/Button',
component: Button,
decorators: [withCenteredStory, withHorizontallyCenteredStory],
argTypes: {
variant: { control: { type: 'select', options: ['primary', 'secondary', 'destructive', 'link'] } },
size: { control: { type: 'select', options: ['sm', 'md', 'lg'] } },
icon: { control: { type: 'select', options: iconOptions } },
css: { control: { disable: true } },
className: { control: { disable: true } },
},
parameters: {
docs: {
page: mdx,
@ -17,19 +23,18 @@ export default {
},
};
const variants = ['primary', 'secondary', 'destructive', 'link'];
const sizes = ['sm', 'md', 'lg'];
export const simple = () => {
const variant = select('Variant', variants, 'primary');
const size = select('Size', sizes, 'md');
const buttonText = text('Text', 'Button');
const disabled = boolean('Disabled', false);
const icon = getIconKnob();
export const Simple: Story<ButtonProps> = ({ disabled, icon, children, size, variant }) => {
return (
<Button variant={variant as ButtonVariant} size={size as ComponentSize} icon={icon} disabled={disabled}>
{buttonText}
<Button variant={variant} size={size} icon={icon} disabled={disabled}>
{children}
</Button>
);
};
Simple.args = {
variant: 'primary',
size: 'md',
disabled: false,
children: 'Button',
icon: undefined,
};

View File

@ -8,7 +8,7 @@ import { LegendPlacement, LegendDisplayMode } from '../Legend/Legend';
import { GraphSeriesXY, FieldType, ArrayVector, dateTime, FieldColorModeId } from '@grafana/data';
export default {
title: 'Visualizations/Graph',
title: 'Visualizations/Graph/GraphWithLegend',
component: GraphWithLegend,
decorator: [withCenteredStory],
};

View File

@ -22,7 +22,7 @@ import { useRevision } from '../uPlot/hooks';
const defaultFormatter = (v: any) => (v == null ? '-' : v.toFixed(1));
interface GraphNGProps extends Omit<PlotProps, 'data' | 'config'> {
export interface GraphNGProps extends Omit<PlotProps, 'data' | 'config'> {
data: DataFrame[];
legend?: LegendOptions;
}

View File

@ -5,7 +5,7 @@ import { InfoTooltip } from './InfoTooltip';
import { Tooltip } from '../Chart/Tooltip';
export default {
title: 'Overlays/Tooltip',
title: 'Overlays/TooltipInternal',
component: Tooltip,
decorators: [withCenteredStory],
};

View File

@ -3,7 +3,7 @@ import { getAvailableIcons } from '../../types';
const VISUAL_GROUP = 'Visual options';
const iconOptions = {
export const iconOptions = {
None: undefined,
...getAvailableIcons().reduce<Record<string, string>>((prev, c) => {
return {

View File

@ -45,6 +45,9 @@ module.exports = {
// rc-trigger uses babel-runtime which has internal dependency to core-js@2
// this alias maps that dependency to core-js@t3
'core-js/library/fn': 'core-js/stable',
// storybook v6 bump caused the app to bundle multiple versions of react breaking hooks
// make sure to resolve only from the project: https://github.com/facebook/react/issues/13991#issuecomment-435587809
react: path.resolve(__dirname, '../../node_modules/react'),
},
modules: [
'node_modules',

6456
yarn.lock

File diff suppressed because it is too large Load Diff