[Canvas] Storybook for testing and development (#29072)

## Summary

This PR adds [Storybook](https://storybook.js.org/) to our testing and development suite.

![screen shot 2019-01-21 at 4 35 32 pm](https://user-images.githubusercontent.com/297604/51502196-9f856780-1d9a-11e9-97bf-07c99c3f279b.png)

This will allow us to:

1. create a site outlining all components within Canvas, including their TS type information;
2. demonstrate usage of all components by example;
3. allow for individual component testing, both manually and by Jest;
4. iterate and fix bugs on individual components *without* having to start up Kibana, in a [HMR](https://webpack.js.org/concepts/hot-module-replacement/) environment;
5. automatically generate [snapshots](https://jestjs.io/docs/en/snapshot-testing) based on any examples written;

This PR also converts a few components to Typescript and adds examples.

## How this can help us, (with examples)

I was inspired to add this when I was fixing #25342.  In order to fix my changes, I had to run elasticsearch and kibana, as well as refresh my page whenever I needed to test a change.  Had I had a Storybook instance, I would have been done much faster.

In this PR, you'll see I converted `AdvancedFilter` from `renderers` and `FontPicker` and `ImageUpload` from `public/components`.  Would you believe I discovered and fixed bugs just by converting to Typescript and writing examples?

### `AdvancedFilter`

- `onChange` and `commit` are not marked as required in `propTypes`, but the component will error out if they're not supplied.
- `commit` was actually being called twice when 'Apply' was clicked.  This was shown in the 'Actions' panel when I was testing it.

### `FontPicker`

- The `fonts` collection was not strongly-typed, therefore any string could be passed to the `value` parameter without error.
- While the code allows for any font string to be given to the component, there is no way to currently select that value, nor type it in within the control.  This is likely a bug in design.
- The `aria-labeledby` attribute in the drop down includes `undefined`.  This is likely a bug in EUI:

![screen shot 2019-01-21 at 4 25 58 pm](https://user-images.githubusercontent.com/297604/51501908-5ed91e80-1d99-11e9-913a-ce1bb5f4e352.png)

## How to use

- `cd x-pack/plugins/canvas/`
- Run `node scripts/storybook` to start up a local development version, with HMR.
- Run `node scripts/storybook_build` to build a complete static version of the book.
- Run `node scripts/jest` which will run the Storyshots test; run `node scripts/jest --updateSnapshot` if source code has changed as expected.

## Future Work

- Adding Jest coverage and output to the info panels, ([this](https://www.npmjs.com/package/@storybook/addon-jest) is *sick* functionality).
- Adding automatic [a11y testing](https://www.npmjs.com/package/@storybook/addon-a11y), (currently [blocked](https://github.com/storybooks/storybook/issues/4889)).
- Adding generic knobs for stories
- Adding more example info, (e.g. who edited last, descriptions, etc).
This commit is contained in:
Clint Andrew Hall 2019-02-15 10:35:30 -06:00 committed by GitHub
parent 716c66fb88
commit 3727b5355a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 2757 additions and 218 deletions

1
x-pack/.gitignore vendored
View file

@ -10,6 +10,7 @@
/.env
/.kibana-plugin-helpers.dev.*
!/plugins/infra/**/target
.cache
# We don't want any yarn.lock files in here
/yarn.lock

View file

@ -30,6 +30,13 @@
"@kbn/es": "1.0.0",
"@kbn/plugin-helpers": "9.0.2",
"@kbn/test": "1.0.0",
"@storybook/addon-actions": "^4.1.7",
"@storybook/addon-console": "^1.1.0",
"@storybook/addon-info": "^4.0.7",
"@storybook/addon-knobs": "^4.0.7",
"@storybook/addon-options": "^4.1.7",
"@storybook/addon-storyshots": "^4.1.7",
"@storybook/react": "^4.0.7",
"@types/angular": "1.6.50",
"@types/d3-array": "^1.2.1",
"@types/d3-scale": "^2.0.0",
@ -52,8 +59,12 @@
"@types/react-dom": "^16.0.5",
"@types/react-redux": "^6.0.6",
"@types/react-router-dom": "^4.3.1",
"@types/recompose": "^0.30.2",
"@types/reduce-reducers": "^0.1.3",
"@types/sinon": "^5.0.1",
"@types/storybook__addon-actions": "^3.4.1",
"@types/storybook__addon-info": "^3.4.2",
"@types/storybook__react": "^4.0.0",
"@types/supertest": "^2.0.5",
"@types/uuid": "^3.4.4",
"abab": "^1.0.4",
@ -64,6 +75,7 @@
"babel-jest": "^23.6.0",
"babel-plugin-inline-react-svg": "^0.5.4",
"babel-plugin-mock-imports": "^0.0.5",
"babel-plugin-require-context-hook": "^1.0.0",
"babel-plugin-transform-react-remove-prop-types": "^0.4.14",
"chalk": "^2.4.1",
"chance": "1.0.10",
@ -76,6 +88,7 @@
"enzyme-adapter-react-16": "^1.6.0",
"enzyme-adapter-utils": "^1.8.1",
"enzyme-to-json": "^3.3.4",
"execa": "^1.0.0",
"expect.js": "0.3.1",
"fancy-log": "^1.3.2",
"fetch-mock": "7.3.0",
@ -99,13 +112,17 @@
"pdfjs-dist": "^2.0.943",
"pixelmatch": "4.0.2",
"proxyquire": "1.7.11",
"react-test-renderer": "^16.2.0",
"react-docgen-typescript-loader": "^3.0.0",
"react-docgen-typescript-webpack-plugin": "^1.1.0",
"react-test-renderer": "^16.7.0",
"redux-test-utils": "0.2.2",
"rsync": "0.4.0",
"run-sequence": "^2.2.1",
"sass-loader": "^7.1.0",
"sass-resources-loader": "^2.0.0",
"simple-git": "1.37.0",
"sinon": "^5.0.7",
"string-replace-loader": "^2.1.1",
"supertest": "^3.1.0",
"supertest-as-promised": "^4.0.2",
"tmp": "0.0.31",

View file

@ -54,4 +54,7 @@ public/style/index.css
canvas_plugin/*
# Don't commit the Webpack statistics
webpack_stats.json
webpack_stats.json
# Don't commit storybook builds
storybook

View file

@ -0,0 +1,9 @@
{
"env": {
"test": {
"plugins": [
"require-context-hook"
]
}
}
}

View file

@ -4,9 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { compose, withState } from 'recompose';
import { AdvancedFilter as Component } from './advanced_filter';
export const AdvancedFilter = compose(withState('value', 'onChange', ({ filter }) => filter || ''))(
Component
);
import '@storybook/addon-options/register';
import '@storybook/addon-actions/register';
import '@storybook/addon-knobs/register';
import '@storybook/addon-console';

View file

@ -0,0 +1,65 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { configure, addDecorator } from '@storybook/react';
import { withKnobs } from '@storybook/addon-knobs/react';
import { withInfo } from '@storybook/addon-info';
import { withOptions } from '@storybook/addon-options';
// Import dependent CSS
require('@elastic/eui/dist/eui_theme_light.css');
require('@kbn/ui-framework/dist/kui_light.css');
require('../../../../src/legacy/ui/public/styles/bootstrap_light.less');
// If we're running Storyshots, be sure to register the require context hook.
// Otherwise, add the other decorators.
if (process.env.NODE_ENV === 'test') {
require('babel-plugin-require-context-hook/register')();
} else {
// Customize the info for each story.
addDecorator(
withInfo({
inline: true,
styles: {
infoBody: {
margin: 20
},
infoStory: {
margin: '40px 60px'
}
}
})
);
// Add optional knobs to customize each story.
addDecorator(withKnobs);
}
function loadStories() {
// Pull in the built CSS produced by the Kibana server
const css = require.context('../../../../built_assets/css', true, /light.css$/);
css.keys().forEach(filename => css(filename));
// Include the legacy styles
const uiStyles = require.context('../../../../src/legacy/ui/public/styles', false, /[\/\\](?!mixins|variables|_|\.|bootstrap_(light|dark))[^\/\\]+\.less/);
uiStyles.keys().forEach(key => uiStyles(key));
// Find all files ending in *.examples.ts
const req = require.context('./..', true, /.examples.tsx$/);
req.keys().forEach(filename => req(filename));
}
// Set up the Storybook environment with custom settings.
addDecorator(
withOptions({
goFullScreen: false,
name: 'Canvas Storybook',
showAddonsPanel: true,
url: 'https://github.com/elastic/kibana/tree/master/x-pack/plugins/canvas'
})
);
configure(loadStories, module);

View file

@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
const serve = require('serve-static');
const path = require('path');
// Extend the Storybook Middleware to include a route to access ui assets
module.exports = function (router) {
router.get('/ui', serve(path.resolve(__dirname, '../../../../src/legacy/ui/public/assets')));
}

View file

@ -0,0 +1,18 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import path from 'path';
import initStoryshots, { multiSnapshotWithOptions } from '@storybook/addon-storyshots';
import styleSheetSerializer from 'jest-styled-components/src/styleSheetSerializer';
import { addSerializer } from 'jest-specific-snapshot'
jest.mock(`@elastic/eui/lib/components/form/form_row/make_id`, () => () => `generated-id`);
addSerializer(styleSheetSerializer);
initStoryshots({
configPath: path.resolve(__dirname, './../.storybook'),
test: multiSnapshotWithOptions({}),
});

View file

@ -0,0 +1,83 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
const path = require('path');
const TSDocgenPlugin = require('react-docgen-typescript-webpack-plugin');
// Extend the Storybook Webpack config with some customizations;
module.exports = (_baseConfig, _env, config) => {
// Include the React preset for Storybook JS files.
config.module.rules.push(
{
test: /\.js$/,
include: /\.storybook/,
loaders: 'babel-loader',
options: {
presets: [require.resolve('babel-preset-react')],
},
},
);
// Find and alter the CSS rule to replace the Kibana public path string with a path
// to the route we've added in middleware.js
const cssRule = config.module.rules.find(rule => rule.test.source.includes('.css$'));
cssRule.use.push({
loader: 'string-replace-loader',
options: {
search: '__REPLACE_WITH_PUBLIC_PATH__',
replace: '/',
flags: 'g'
}
});
// Configure loading LESS files from Kibana
config.module.rules.push({
test: /\.less$/,
use: [
{ loader: 'style-loader' },
{ loader: 'css-loader', options: { importLoaders: 2 } },
{
loader: 'postcss-loader',
options: {
config: { path: path.resolve(__dirname, './../../../../src/optimize/postcss.config.js') },
},
},
{ loader: 'less-loader' },
],
});
// Support .ts/x files using the tsconfig from Kibana
config.module.rules.push({
test: /\.tsx?$/,
use: [
{
loader: 'ts-loader',
options: {
transpileOnly: true,
experimentalWatchApi: true,
onlyCompileBundledFiles: true,
configFile: require.resolve('../../../../tsconfig.json'),
compilerOptions: {
sourceMap: true,
},
},
},
require.resolve('react-docgen-typescript-loader'),
],
});
// Include the TSDocgen plugin to display Typescript param comments in the stories.
config.plugins.push(new TSDocgenPlugin());
// Tell Webpack about the ts/x extensions
config.resolve.extensions.push('.ts', '.tsx');
// Alias the any imports from ui/ to the proper directory.
config.resolve.alias.ui = path.resolve(__dirname, './../../../../src/legacy/ui/public');
return config;
};

View file

@ -0,0 +1,67 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots renderers/AdvancedFilter default 1`] = `
<form
className="canvasAdvancedFilter"
onSubmit={[Function]}
>
<div
className="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
className="euiFlexItem"
>
<input
className="canvasAdvancedFilter__input"
onChange={[Function]}
placeholder="Enter filter expression"
type="text"
value=""
/>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<button
className="canvasAdvancedFilter__button"
type="submit"
>
Apply
</button>
</div>
</div>
</form>
`;
exports[`Storyshots renderers/AdvancedFilter with value 1`] = `
<form
className="canvasAdvancedFilter"
onSubmit={[Function]}
>
<div
className="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
className="euiFlexItem"
>
<input
className="canvasAdvancedFilter__input"
onChange={[Function]}
placeholder="Enter filter expression"
type="text"
value="expression"
/>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<button
className="canvasAdvancedFilter__button"
type="submit"
>
Apply
</button>
</div>
</div>
</form>
`;

View file

@ -0,0 +1,16 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { action } from '@storybook/addon-actions';
import { storiesOf } from '@storybook/react';
import React from 'react';
import { AdvancedFilter } from './advanced_filter';
storiesOf('renderers/AdvancedFilter', module)
.add('default', () => <AdvancedFilter onChange={action('onChange')} commit={action('commit')} />)
.add('with value', () => (
<AdvancedFilter onChange={action('onChange')} commit={action('commit')} value="expression" />
));

View file

@ -4,11 +4,20 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import PropTypes from 'prop-types';
import React, { SFC } from 'react';
export const AdvancedFilter = ({ value, onChange, commit }) => (
export interface Props {
/** Optional value for the component */
value?: string;
/** Function to invoke when the filter value is changed */
onChange: (value: string) => void;
/** Function to invoke when the filter value is committed */
commit: (value: string) => void;
}
export const AdvancedFilter: SFC<Props> = ({ value = '', onChange, commit }) => (
<form
onSubmit={e => {
e.preventDefault();
@ -27,11 +36,7 @@ export const AdvancedFilter = ({ value, onChange, commit }) => (
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<button
className="canvasAdvancedFilter__button"
type="submit"
onClick={() => commit(value)}
>
<button className="canvasAdvancedFilter__button" type="submit">
Apply
</button>
</EuiFlexItem>
@ -39,8 +44,12 @@ export const AdvancedFilter = ({ value, onChange, commit }) => (
</form>
);
AdvancedFilter.propTypes = {
onChange: PropTypes.func,
value: PropTypes.string,
commit: PropTypes.func,
AdvancedFilter.defaultProps = {
value: '',
};
AdvancedFilter.propTypes = {
onChange: PropTypes.func.isRequired,
value: PropTypes.string,
commit: PropTypes.func.isRequired,
};

View file

@ -0,0 +1,17 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { compose, withState } from 'recompose';
import { AdvancedFilter as Component, Props as ComponentProps } from './advanced_filter';
export interface Props {
value?: string;
commit: (value: string) => void;
}
export const AdvancedFilter = compose<ComponentProps, Props>(
withState('value', 'onChange', ({ filter }) => filter || '')
)(Component);

View file

@ -4,56 +4,106 @@
* you may not use this file except in compliance with the Elastic License.
*/
export const americanTypewriter = {
export type FontValue = typeof fonts[number]['value'];
export type FontLabel = typeof fonts[number]['label'];
export interface Font {
label: FontLabel;
value: FontValue;
}
/**
* This function allows one to create a strongly-typed font for inclusion in
* the font collection. As a result, the values and labels are known to the
* type system, preventing one from specifying a non-existent font at build
* time.
*/
function createFont<
RawFont extends { value: RawFontValue; label: RawFontLabel },
RawFontValue extends string,
RawFontLabel extends string
>(font: RawFont) {
return font;
}
export const americanTypewriter = createFont({
label: 'American Typewriter',
value: "'American Typewriter', 'Courier New', Courier, Monaco, mono",
};
export const arial = { label: 'Arial', value: 'Arial, sans-serif' };
export const baskerville = {
});
export const arial = createFont({ label: 'Arial', value: 'Arial, sans-serif' });
export const baskerville = createFont({
label: 'Baskerville',
value: "Baskerville, Georgia, Garamond, 'Times New Roman', Times, serif",
};
export const bookAntiqua = {
});
export const bookAntiqua = createFont({
label: 'Book Antiqua',
value: "'Book Antiqua', Georgia, Garamond, 'Times New Roman', Times, serif",
};
export const brushScript = {
});
export const brushScript = createFont({
label: 'Brush Script',
value: "'Brush Script MT', 'Comic Sans', sans-serif",
};
export const chalkboard = { label: 'Chalkboard', value: "Chalkboard, 'Comic Sans', sans-serif" };
export const didot = {
});
export const chalkboard = createFont({
label: 'Chalkboard',
value: "Chalkboard, 'Comic Sans', sans-serif",
});
export const didot = createFont({
label: 'Didot',
value: "Didot, Georgia, Garamond, 'Times New Roman', Times, serif",
};
export const futura = { label: 'Futura', value: 'Futura, Impact, Helvetica, Arial, sans-serif' };
export const gillSans = {
});
export const futura = createFont({
label: 'Futura',
value: 'Futura, Impact, Helvetica, Arial, sans-serif',
});
export const gillSans = createFont({
label: 'Gill Sans',
value:
"'Gill Sans', 'Lucida Grande', 'Lucida Sans Unicode', Verdana, Helvetica, Arial, sans-serif",
};
export const helveticaNeue = {
});
export const helveticaNeue = createFont({
label: 'Helvetica Neue',
value: "'Helvetica Neue', Helvetica, Arial, sans-serif",
};
export const hoeflerText = {
});
export const hoeflerText = createFont({
label: 'Hoefler Text',
value: "'Hoefler Text', Garamond, Georgia, 'Times New Roman', Times, serif",
};
export const lucidaGrande = {
});
export const lucidaGrande = createFont({
label: 'Lucida Grande',
value: "'Lucida Grande', 'Lucida Sans Unicode', Lucida, Verdana, Helvetica, Arial, sans-serif",
};
export const myriad = { label: 'Myriad', value: 'Myriad, Helvetica, Arial, sans-serif' };
export const openSans = { label: 'Open Sans', value: "'Open Sans', Helvetica, Arial, sans-serif" };
export const optima = {
});
export const myriad = createFont({
label: 'Myriad',
value: 'Myriad, Helvetica, Arial, sans-serif',
});
export const openSans = createFont({
label: 'Open Sans',
value: "'Open Sans', Helvetica, Arial, sans-serif",
});
export const optima = createFont({
label: 'Optima',
value: "Optima, 'Lucida Grande', 'Lucida Sans Unicode', Verdana, Helvetica, Arial, sans-serif",
};
export const palatino = {
});
export const palatino = createFont({
label: 'Palatino',
value: "Palatino, 'Book Antiqua', Georgia, Garamond, 'Times New Roman', Times, serif",
};
});
export const fonts = [
americanTypewriter,
arial,

View file

@ -0,0 +1,44 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots components/FileUpload default 1`] = `
<div
className="euiFilePicker euiFilePicker--compressed canvasFileUpload"
>
<div
className="euiFilePicker__wrap"
>
<input
className="euiFilePicker__input"
id=""
onChange={[Function]}
onDragLeave={[Function]}
onDragOver={[Function]}
onDrop={[Function]}
type="file"
/>
<div
className="euiFilePicker__prompt"
>
<svg
aria-hidden="true"
className="euiIcon euiIcon--medium euiFilePicker__icon"
focusable="false"
height="16"
style={null}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9 10.114l1.85-1.943a.52.52 0 0 1 .77 0c.214.228.214.6 0 .829l-1.95 2.05a1.552 1.552 0 0 1-2.31 0L5.41 9a.617.617 0 0 1 0-.829.52.52 0 0 1 .77 0L8 10.082V1.556C8 1.249 8.224 1 8.5 1s.5.249.5.556v8.558zM4.18 6a.993.993 0 0 0-.972.804l-1.189 6A.995.995 0 0 0 2.991 14h11.018a1 1 0 0 0 .972-1.196l-1.19-6a.993.993 0 0 0-.97-.804H4.18zM6 5v1h5V5h1.825c.946 0 1.76.673 1.946 1.608l1.19 6A2 2 0 0 1 14.016 15H2.984a1.992 1.992 0 0 1-1.945-2.392l1.19-6C2.414 5.673 3.229 5 4.174 5H6z"
/>
</svg>
<div
className="euiFilePicker__promptText"
>
Select or drag and drop a file
</div>
</div>
</div>
</div>
`;

View file

@ -0,0 +1,14 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { action } from '@storybook/addon-actions';
import { storiesOf } from '@storybook/react';
import React from 'react';
import { FileUpload } from './file_upload';
storiesOf('components/FileUpload', module).add('default', () => (
<FileUpload onUpload={action('onUpload')} />
));

View file

@ -0,0 +1,39 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
// @ts-ignore (elastic/eui#1262) EuiFilePicker is not exported yet
import { EuiFilePicker } from '@elastic/eui';
import PropTypes from 'prop-types';
import React, { SFC } from 'react';
interface Props {
/** Optional ID of the component */
id?: string;
/** Optional className of the component */
className?: string;
/** Function to invoke when the file is successfully uploaded */
onUpload: () => void;
}
export const FileUpload: SFC<Props> = props => (
<EuiFilePicker compressed id={props.id} className={props.className} onChange={props.onUpload} />
);
FileUpload.defaultProps = {
id: '',
className: 'canvasFileUpload',
};
FileUpload.propTypes = {
/** Optional ID of the component */
id: PropTypes.string,
/** Optional className of the component */
className: PropTypes.string,
/** Function to invoke when the file is successfully uploaded */
onUpload: PropTypes.func.isRequired,
};
FileUpload.displayName = 'FileUpload';

View file

@ -0,0 +1,165 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots components/FontPicker default 1`] = `
<div
className="euiPopover euiPopover--anchorDownCenter euiSuperSelect"
onClick={[Function]}
onKeyDown={[Function]}
>
<div
className="euiPopover__anchor euiSuperSelect__popoverAnchor"
>
<input
defaultValue=""
type="hidden"
/>
<div
className="euiFormControlLayout euiFormControlLayout--compressed"
>
<div
className="euiFormControlLayout__childrenWrapper"
>
<span
className="euiScreenReaderOnly"
id="generated-id"
>
Select an option: , is selected
</span>
<button
aria-haspopup="true"
aria-labelledby="undefined generated-id"
className="euiSuperSelectControl euiSuperSelectControl--compressed"
onChange={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
role="option"
type="button"
>
</button>
<div
className="euiFormControlLayoutIcons euiFormControlLayoutIcons--right"
>
<span
className="euiFormControlLayoutCustomIcon"
>
<svg
aria-hidden="true"
className="euiIcon euiIcon--medium euiFormControlLayoutCustomIcon__icon"
focusable="false"
height="16"
style={null}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
>
<defs>
<path
d="M13.069 5.157L8.384 9.768a.546.546 0 0 1-.768 0L2.93 5.158a.552.552 0 0 0-.771 0 .53.53 0 0 0 0 .759l4.684 4.61c.641.631 1.672.63 2.312 0l4.684-4.61a.53.53 0 0 0 0-.76.552.552 0 0 0-.771 0z"
id="arrow_down-a"
/>
</defs>
<use
fillRule="nonzero"
xlinkHref="#arrow_down-a"
/>
</svg>
</span>
</div>
</div>
</div>
</div>
</div>
`;
exports[`Storyshots components/FontPicker with value 1`] = `
<div
className="euiPopover euiPopover--anchorDownCenter euiSuperSelect"
onClick={[Function]}
onKeyDown={[Function]}
>
<div
className="euiPopover__anchor euiSuperSelect__popoverAnchor"
>
<input
type="hidden"
value="'American Typewriter', 'Courier New', Courier, Monaco, mono"
/>
<div
className="euiFormControlLayout euiFormControlLayout--compressed"
>
<div
className="euiFormControlLayout__childrenWrapper"
>
<span
className="euiScreenReaderOnly"
id="generated-id"
>
Select an option:
<div
style={
Object {
"fontFamily": "'American Typewriter', 'Courier New', Courier, Monaco, mono",
}
}
>
American Typewriter
</div>
, is selected
</span>
<button
aria-haspopup="true"
aria-labelledby="undefined generated-id"
className="euiSuperSelectControl euiSuperSelectControl--compressed"
onChange={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
role="option"
type="button"
>
<div
style={
Object {
"fontFamily": "'American Typewriter', 'Courier New', Courier, Monaco, mono",
}
}
>
American Typewriter
</div>
</button>
<div
className="euiFormControlLayoutIcons euiFormControlLayoutIcons--right"
>
<span
className="euiFormControlLayoutCustomIcon"
>
<svg
aria-hidden="true"
className="euiIcon euiIcon--medium euiFormControlLayoutCustomIcon__icon"
focusable="false"
height="16"
style={null}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
>
<defs>
<path
d="M13.069 5.157L8.384 9.768a.546.546 0 0 1-.768 0L2.93 5.158a.552.552 0 0 0-.771 0 .53.53 0 0 0 0 .759l4.684 4.61c.641.631 1.672.63 2.312 0l4.684-4.61a.53.53 0 0 0 0-.76.552.552 0 0 0-.771 0z"
id="arrow_down-a"
/>
</defs>
<use
fillRule="nonzero"
xlinkHref="#arrow_down-a"
/>
</svg>
</span>
</div>
</div>
</div>
</div>
</div>
`;

View file

@ -0,0 +1,17 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { action } from '@storybook/addon-actions';
import { storiesOf } from '@storybook/react';
import React from 'react';
import { americanTypewriter } from '../../../common/lib/fonts';
import { FontPicker } from './font_picker';
storiesOf('components/FontPicker', module)
.add('default', () => <FontPicker onSelect={action('onSelect')} />)
.add('with value', () => (
<FontPicker onSelect={action('onSelect')} value={americanTypewriter.value} />
));

View file

@ -1,35 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { EuiSuperSelect } from '@elastic/eui';
import { fonts } from '../../../common/lib/fonts';
export const FontPicker = ({ onSelect, value }) => {
if (value && !fonts.find(font => font.value === value)) {
const label = (value.indexOf(',') >= 0 ? value.split(',')[0] : value).replace(/['"]/g, '');
fonts.push({ value, label });
fonts.sort((a, b) => a.label.localeCompare(b.label));
}
return (
<EuiSuperSelect
compressed
options={fonts.map(({ value, label }) => ({
value,
inputDisplay: <div style={{ fontFamily: value }}>{label}</div>,
}))}
valueOfSelected={value}
onChange={value => onSelect(value)}
/>
);
};
FontPicker.propTypes = {
value: PropTypes.string,
onSelect: PropTypes.func,
};

View file

@ -0,0 +1,51 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
// @ts-ignore (elastic/eui#1262) EuiSuperSelect is not exported yet
import { EuiSuperSelect } from '@elastic/eui';
import PropTypes from 'prop-types';
import React, { SFC } from 'react';
import { fonts, FontValue } from '../../../common/lib/fonts';
interface Props {
onSelect?: (value: FontValue) => void;
value?: FontValue;
}
export const FontPicker: SFC<Props> = props => {
const { value, onSelect } = props;
// While fonts are strongly-typed, we also support custom fonts someone might type in.
// So let's cast the fonts and allow for additions.
const displayedFonts: Array<{ value: string; label: string }> = fonts;
if (value && !fonts.find(font => font.value === value)) {
const label = (value.indexOf(',') >= 0 ? value.split(',')[0] : value).replace(/['"]/g, '');
displayedFonts.push({ value, label });
displayedFonts.sort((a, b) => a.label.localeCompare(b.label));
}
return (
<EuiSuperSelect
compressed
options={displayedFonts.map(font => ({
value: font.value,
inputDisplay: <div style={{ fontFamily: font.value }}>{font.label}</div>,
}))}
valueOfSelected={value}
onChange={(newValue: FontValue) => onSelect && onSelect(newValue)}
/>
);
};
FontPicker.propTypes = {
/** Initial value of the Font Picker. */
value: PropTypes.string,
/** Function to execute when a Font is selected. */
onSelect: PropTypes.func,
};
FontPicker.displayName = 'FontPicker';

View file

@ -0,0 +1,37 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
const path = require('path');
const storybook = require('@storybook/react/standalone');
const execa = require('execa');
// We have to start up the Kibana server to process CSS files as we change them.
// This is pretty much a hack for the moment. We can get a separate process up
// and running in the future.
execa(
process.execPath,
[
'scripts/kibana',
'--optimize.enabled=false',
'--env.name="development"',
'--plugins.initialize=false',
'--server.port=5699',
],
{
cwd: path.resolve(__dirname, '../../../..'),
stdio: ['ignore', 'inherit', 'inherit'],
buffer: false,
}
).catch(err => {
console.log('Kibana server died:', err.message);
process.exit(1);
});
storybook({
mode: 'dev',
port: 9001,
configDir: path.resolve(__dirname, './../.storybook'),
});

View file

@ -0,0 +1,14 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
const path = require('path');
const storybook = require('@storybook/react/standalone');
storybook({
mode: 'static',
configDir: path.resolve(__dirname, './../.storybook'),
outputDir: path.resolve(__dirname, './../storybook'),
});

2099
yarn.lock

File diff suppressed because it is too large Load diff