diff --git a/contribute/style-guides/storybook.md b/contribute/style-guides/storybook.md
index 993b3621057..b7d89caad63 100644
--- a/contribute/style-guides/storybook.md
+++ b/contribute/style-guides/storybook.md
@@ -64,13 +64,13 @@ To link a component’s 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';
;
```
@@ -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 = () => (
-
-);
-```
+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 ;
+ };
+ ```
+
+ After
+
+ ```tsx
+ export const Simple: Story = ({ prop1, prop2 }) => {
+ return ;
+ };
+ ```
+
+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
diff --git a/jest.config.js b/jest.config.js
index a32cf1443ab..575b844b07b 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -20,6 +20,7 @@ module.exports = {
'\\.svg': '/public/test/mocks/svg.ts',
'\\.css': '/public/test/mocks/style.ts',
'monaco-editor/esm/vs/editor/editor.api': '/public/test/mocks/monaco.ts',
+ '^react($|/.+)': '/node_modules/react$1',
},
watchPathIgnorePatterns: ['/node_modules/'],
};
diff --git a/package.json b/package.json
index 03342688597..874b89160e3 100644
--- a/package.json
+++ b/package.json
@@ -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": {
diff --git a/packages/grafana-ui/.eslintrc b/packages/grafana-ui/.eslintrc
index d889d41b0bf..415003edceb 100644
--- a/packages/grafana-ui/.eslintrc
+++ b/packages/grafana-ui/.eslintrc
@@ -6,7 +6,8 @@
{
"files": ["**/*.{test,story}.{ts,tsx}"],
"rules": {
- "no-restricted-imports": "off"
+ "no-restricted-imports": "off",
+ "react/prop-types": "off"
}
}
]
diff --git a/packages/grafana-ui/.storybook/main.ts b/packages/grafana-ui/.storybook/main.ts
index 4b734bc1c89..e32a86bfa26 100644
--- a/packages/grafana-ui/.storybook/main.ts
+++ b/packages/grafana-ui/.storybook/main.ts
@@ -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;
+ },
};
diff --git a/packages/grafana-ui/.storybook/preview.ts b/packages/grafana-ui/.storybook/preview.ts
index 66e6a965dc1..21d1c945f7b 100644
--- a/packages/grafana-ui/.storybook/preview.ts
+++ b/packages/grafana-ui/.storybook/preview.ts
@@ -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);
+};
diff --git a/packages/grafana-ui/.storybook/webpack.config.js b/packages/grafana-ui/.storybook/webpack.config.js
deleted file mode 100644
index 792759122a4..00000000000
--- a/packages/grafana-ui/.storybook/webpack.config.js
+++ /dev/null
@@ -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;
-};
diff --git a/packages/grafana-ui/README.md b/packages/grafana-ui/README.md
index ea8e3ee48de..59d7ed7c419 100644
--- a/packages/grafana-ui/README.md
+++ b/packages/grafana-ui/README.md
@@ -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.
diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json
index 6e1590983d8..e951138e379 100644
--- a/packages/grafana-ui/package.json
+++ b/packages/grafana-ui/package.json
@@ -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"
}
diff --git a/packages/grafana-ui/rollup.config.ts b/packages/grafana-ui/rollup.config.ts
index cf3576fa791..bbe55cb54c5 100644
--- a/packages/grafana-ui/rollup.config.ts
+++ b/packages/grafana-ui/rollup.config.ts
@@ -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(),
diff --git a/packages/grafana-ui/src/Intro.story.mdx b/packages/grafana-ui/src/Intro.story.mdx
index 228284fafbf..6b9be9f0dcc 100644
--- a/packages/grafana-ui/src/Intro.story.mdx
+++ b/packages/grafana-ui/src/Intro.story.mdx
@@ -1,3 +1,5 @@
+import { Meta } from '@storybook/addon-docs/blocks';
+
# Grafana design system
diff --git a/packages/grafana-ui/src/components/Button/Button.story.tsx b/packages/grafana-ui/src/components/Button/Button.story.tsx
index 513628cd284..16612418b99 100644
--- a/packages/grafana-ui/src/components/Button/Button.story.tsx
+++ b/packages/grafana-ui/src/components/Button/Button.story.tsx
@@ -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 = ({ disabled, icon, children, size, variant }) => {
return (
-