mirror of
https://github.com/storybookjs/storybook.git
synced 2025-03-28 05:10:17 +08:00
Merge pull request #3133 from rhalff/addon-actions
Addon actions: fix slow logging
This commit is contained in:
commit
503c186c72
@ -35,10 +35,12 @@ Import the `action` function and use it to create actions handlers. When creatin
|
||||
|
||||
```js
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { action, configureActions } from '@storybook/addon-actions';
|
||||
|
||||
import Button from './button';
|
||||
|
||||
action('button-click')
|
||||
|
||||
storiesOf('Button', module)
|
||||
.add('default view', () => (
|
||||
<Button onClick={ action('button-click') }>
|
||||
@ -69,3 +71,34 @@ storiesOf('Button', module)
|
||||
</Button>
|
||||
))
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Arguments which are passed to the action call will have to be serialized while be "transfered"
|
||||
over the channel.
|
||||
|
||||
This is not very optimal and can cause lag when large objects are being logged, for this reason it is possible
|
||||
to configure a maximum depth.
|
||||
|
||||
To apply the configuration globally use the `configureActions` function in your `config.js` file.
|
||||
|
||||
```js
|
||||
import { configureActions } from '@storybook/addon-actions';
|
||||
|
||||
configureActions({
|
||||
depth: 100
|
||||
})
|
||||
```
|
||||
|
||||
To apply the configuration per action use:
|
||||
```js
|
||||
action('my-action', {
|
||||
depth: 5
|
||||
})
|
||||
```
|
||||
|
||||
### Available Options
|
||||
|
||||
|Name|Type|Description|Default|
|
||||
|---|---|---|---|
|
||||
|`depth`|Number|Configures the transfered depth of any logged objects.|`10`|
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { action, configureActions, decorateAction } from './preview';
|
||||
|
||||
// addons, panels and events get unique names using a prefix
|
||||
export const ADDON_ID = 'storybook/actions';
|
||||
export const PANEL_ID = `${ADDON_ID}/actions-panel`;
|
||||
export const EVENT_ID = `${ADDON_ID}/action-event`;
|
||||
|
||||
export { action, decorateAction } from './preview';
|
||||
export { action, configureActions, decorateAction };
|
||||
|
@ -1,25 +1,25 @@
|
||||
import { DecycleError } from './errors';
|
||||
|
||||
import { getPropertiesList, typeReplacer } from './util';
|
||||
import { getPropertiesList, typeReplacer, omitProperty } from './util';
|
||||
|
||||
import { CYCLIC_KEY } from './';
|
||||
|
||||
import { objectType } from './types';
|
||||
|
||||
import { DEPTH_KEY } from './types/object/configureDepth';
|
||||
|
||||
const { hasOwnProperty } = Object.prototype;
|
||||
|
||||
export default function decycle(object, depth = 10) {
|
||||
const objects = new WeakMap();
|
||||
|
||||
let isCyclic = false;
|
||||
|
||||
const res = (function derez(value, path, _depth) {
|
||||
const res = (function derez(value, path, _depth, _branchDepthMax) {
|
||||
let oldPath;
|
||||
let obj;
|
||||
|
||||
if (Object(value) === value && _depth > depth) {
|
||||
const name = value.constructor ? value.constructor.name : typeof value;
|
||||
|
||||
return `[${name}...]`;
|
||||
}
|
||||
let maxDepth = _branchDepthMax;
|
||||
|
||||
const result = typeReplacer(value);
|
||||
|
||||
@ -51,19 +51,40 @@ export default function decycle(object, depth = 10) {
|
||||
if (Array.isArray(value)) {
|
||||
obj = [];
|
||||
for (let i = 0; i < value.length; i += 1) {
|
||||
obj[i] = derez(value[i], `${path}[${i}]`, _depth + 1);
|
||||
obj[i] = derez(value[i], `${path}[${i}]`, _depth + 1, maxDepth);
|
||||
}
|
||||
} else {
|
||||
obj = objectType.serialize(value);
|
||||
|
||||
getPropertiesList(value).forEach(name => {
|
||||
try {
|
||||
obj[name] = derez(value[name], `${path}[${JSON.stringify(name)}]`, _depth + 1);
|
||||
} catch (error) {
|
||||
console.error(error); // eslint-disable-line no-console
|
||||
obj[name] = new DecycleError(error.message);
|
||||
let newDepth;
|
||||
if (hasOwnProperty.call(obj, DEPTH_KEY)) {
|
||||
if (_depth + 1 < maxDepth) {
|
||||
const depthKey = obj[DEPTH_KEY];
|
||||
|
||||
newDepth = depthKey === 0 ? 0 : _depth + depthKey;
|
||||
maxDepth = newDepth >= depth ? depth : newDepth;
|
||||
}
|
||||
});
|
||||
|
||||
delete obj[DEPTH_KEY];
|
||||
}
|
||||
|
||||
if (_depth <= maxDepth) {
|
||||
getPropertiesList(value).forEach(name => {
|
||||
if (!omitProperty(name)) {
|
||||
try {
|
||||
obj[name] = derez(
|
||||
value[name],
|
||||
`${path}[${JSON.stringify(name)}]`,
|
||||
_depth + 1,
|
||||
maxDepth
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error); // eslint-disable-line no-console
|
||||
obj[name] = new DecycleError(error.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (_depth === 0 && value instanceof Object && isCyclic) {
|
||||
@ -74,7 +95,7 @@ export default function decycle(object, depth = 10) {
|
||||
}
|
||||
|
||||
return value;
|
||||
})(object, '$', 0);
|
||||
})(object, '$', 0, depth);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
@ -1,11 +1,15 @@
|
||||
import objectType from '../';
|
||||
import { DEPTH_KEY } from '../configureDepth';
|
||||
|
||||
describe('Object', () => {
|
||||
it('Serializes Object', () => {
|
||||
function C() {}
|
||||
const c = new C();
|
||||
|
||||
expect(objectType.serialize(c)).toEqual({ [objectType.KEY]: 'C' });
|
||||
expect(objectType.serialize(c)).toEqual({
|
||||
[DEPTH_KEY]: 2,
|
||||
[objectType.KEY]: 'C',
|
||||
});
|
||||
});
|
||||
|
||||
it('Deserializes Object', () => {
|
||||
|
7
addons/actions/src/lib/types/object/configureDepth.js
Normal file
7
addons/actions/src/lib/types/object/configureDepth.js
Normal file
@ -0,0 +1,7 @@
|
||||
export const DEPTH_KEY = '$___storybook.depthKey';
|
||||
|
||||
export default function configureDepth(obj, depth = 0) {
|
||||
obj[DEPTH_KEY] = depth; // eslint-disable-line no-param-reassign
|
||||
|
||||
return obj;
|
||||
}
|
@ -1,12 +1,21 @@
|
||||
import createNamedObject from './createNamedObject';
|
||||
import getObjectName from './getObjectName';
|
||||
import configureDepth from './configureDepth';
|
||||
|
||||
const maxDepth = 2;
|
||||
const KEY = '$___storybook.objectName';
|
||||
|
||||
const objectType = {
|
||||
KEY,
|
||||
// is: (value) => , // not used
|
||||
serialize: value => ({ [KEY]: getObjectName(value) }),
|
||||
serialize: value => {
|
||||
const objectName = getObjectName(value);
|
||||
if (objectName === 'Object') {
|
||||
return { [KEY]: objectName };
|
||||
}
|
||||
|
||||
return configureDepth({ [KEY]: objectName }, maxDepth);
|
||||
},
|
||||
deserialize: value => createNamedObject(value, KEY),
|
||||
};
|
||||
|
||||
|
@ -5,3 +5,4 @@ export muteProperty from './muteProperty';
|
||||
export prepareArguments from './prepareArguments';
|
||||
export typeReviver from './typeReviver';
|
||||
export typeReplacer from './typeReplacer';
|
||||
export omitProperty from './omitProperty';
|
||||
|
3
addons/actions/src/lib/util/omitProperty.js
Normal file
3
addons/actions/src/lib/util/omitProperty.js
Normal file
@ -0,0 +1,3 @@
|
||||
export default function omitProperty(name) {
|
||||
return name.startsWith('__') || name.startsWith('STORYBOOK_');
|
||||
}
|
@ -1,12 +1,8 @@
|
||||
import { decycle } from '../index';
|
||||
|
||||
export default function prepareArguments(arg) {
|
||||
if (arg && typeof arg.preventDefault !== 'undefined') {
|
||||
return JSON.stringify(`[${arg.constructor.name}]`);
|
||||
}
|
||||
|
||||
export default function prepareArguments(arg, depth) {
|
||||
try {
|
||||
return JSON.stringify(decycle(arg));
|
||||
return JSON.stringify(decycle(arg, depth));
|
||||
} catch (error) {
|
||||
return error.toString(); // IE still cyclic.
|
||||
}
|
||||
|
90
addons/actions/src/preview/__tests__/action.test.js
Normal file
90
addons/actions/src/preview/__tests__/action.test.js
Normal file
@ -0,0 +1,90 @@
|
||||
import addons from '@storybook/addons';
|
||||
import { action, configureActions } from '../../';
|
||||
|
||||
jest.mock('@storybook/addons');
|
||||
|
||||
const getChannelData = channel =>
|
||||
channel.emit.mock.calls[channel.emit.mock.calls.length - 1][1].data;
|
||||
|
||||
describe('Action', () => {
|
||||
const channel = { emit: jest.fn() };
|
||||
addons.getChannel.mockReturnValue(channel);
|
||||
|
||||
it('with one argument', () => {
|
||||
action('test-action')('one');
|
||||
|
||||
expect(getChannelData(channel).args[0]).toEqual('"one"');
|
||||
});
|
||||
|
||||
it('with multiple arguments', () => {
|
||||
action('test-action')('one', 'two', 'three');
|
||||
|
||||
expect(getChannelData(channel).args).toEqual(['"one"', '"two"', '"three"']);
|
||||
});
|
||||
|
||||
it('with global depth configuration', () => {
|
||||
const depth = 1;
|
||||
|
||||
configureActions({
|
||||
depth,
|
||||
});
|
||||
|
||||
action('test-action')({
|
||||
root: {
|
||||
one: {
|
||||
two: 'foo',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(getChannelData(channel).args[0]).toEqual(
|
||||
JSON.stringify({
|
||||
'$___storybook.objectName': 'Object',
|
||||
root: {
|
||||
'$___storybook.objectName': 'Object',
|
||||
one: {
|
||||
'$___storybook.objectName': 'Object',
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('per action depth option overrides global config', () => {
|
||||
configureActions({
|
||||
depth: 1,
|
||||
});
|
||||
|
||||
action('test-action', { depth: 3 })({
|
||||
root: {
|
||||
one: {
|
||||
two: {
|
||||
three: {
|
||||
four: {
|
||||
five: 'foo',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(getChannelData(channel).args[0]).toEqual(
|
||||
JSON.stringify({
|
||||
'$___storybook.objectName': 'Object',
|
||||
root: {
|
||||
'$___storybook.objectName': 'Object',
|
||||
one: {
|
||||
'$___storybook.objectName': 'Object',
|
||||
two: {
|
||||
'$___storybook.objectName': 'Object',
|
||||
three: {
|
||||
'$___storybook.objectName': 'Object',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
@ -0,0 +1,16 @@
|
||||
import { config } from '../configureActions';
|
||||
import { configureActions } from '../../';
|
||||
|
||||
describe('Configure Actions', () => {
|
||||
it('can configure actions', () => {
|
||||
const depth = 100;
|
||||
|
||||
configureActions({
|
||||
depth,
|
||||
});
|
||||
|
||||
expect(config).toEqual({
|
||||
depth,
|
||||
});
|
||||
});
|
||||
});
|
@ -1,7 +1,7 @@
|
||||
import addons from '@storybook/addons';
|
||||
import uuid from 'uuid/v1';
|
||||
import { action } from './preview';
|
||||
import { undefinedType, symbolType } from './lib/types';
|
||||
import { action } from '../';
|
||||
import { undefinedType, symbolType } from '../../lib/types';
|
||||
|
||||
jest.mock('uuid/v1');
|
||||
jest.mock('@storybook/addons');
|
@ -1,12 +1,18 @@
|
||||
import addons from '@storybook/addons';
|
||||
import uuid from 'uuid/v1';
|
||||
import { EVENT_ID } from './';
|
||||
import { canConfigureName, prepareArguments } from './lib/util';
|
||||
import addons from '@storybook/addons';
|
||||
import { EVENT_ID } from '../';
|
||||
import { canConfigureName, prepareArguments } from '../lib/util';
|
||||
import { config } from './configureActions';
|
||||
|
||||
export default function action(name, options = {}) {
|
||||
const actionOptions = {
|
||||
...config,
|
||||
...options,
|
||||
};
|
||||
|
||||
export function action(name) {
|
||||
// eslint-disable-next-line no-shadow
|
||||
const handler = function action(..._args) {
|
||||
const args = _args.map(prepareArguments);
|
||||
const args = _args.map(arg => prepareArguments(arg, actionOptions.depth));
|
||||
const channel = addons.getChannel();
|
||||
const id = uuid();
|
||||
channel.emit(EVENT_ID, {
|
||||
@ -20,13 +26,3 @@ export function action(name) {
|
||||
}
|
||||
return handler;
|
||||
}
|
||||
|
||||
export function decorateAction(decorators) {
|
||||
return name => {
|
||||
const callAction = action(name);
|
||||
return (..._args) => {
|
||||
const decorated = decorators.reduce((args, fn) => fn(args), _args);
|
||||
callAction(...decorated);
|
||||
};
|
||||
};
|
||||
}
|
7
addons/actions/src/preview/configureActions.js
Normal file
7
addons/actions/src/preview/configureActions.js
Normal file
@ -0,0 +1,7 @@
|
||||
export const config = {
|
||||
depth: 10,
|
||||
};
|
||||
|
||||
export function configureActions(options = {}) {
|
||||
Object.assign(config, options);
|
||||
}
|
11
addons/actions/src/preview/decorateAction.js
Normal file
11
addons/actions/src/preview/decorateAction.js
Normal file
@ -0,0 +1,11 @@
|
||||
import { action } from '../preview';
|
||||
|
||||
export default function decorateAction(decorators) {
|
||||
return (name, options) => {
|
||||
const callAction = action(name, options);
|
||||
return (..._args) => {
|
||||
const decorated = decorators.reduce((args, fn) => fn(args), _args);
|
||||
callAction(...decorated);
|
||||
};
|
||||
};
|
||||
}
|
3
addons/actions/src/preview/index.js
Normal file
3
addons/actions/src/preview/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
export { default as action } from './action';
|
||||
export { configureActions } from './configureActions';
|
||||
export { default as decorateAction } from './decorateAction';
|
@ -67,6 +67,11 @@ exports[`Storyshots Addons|Actions All types 1`] = `
|
||||
>
|
||||
Plain Object
|
||||
</button>
|
||||
<button
|
||||
class="css-1yjiefr"
|
||||
>
|
||||
Object (depth: 2)
|
||||
</button>
|
||||
<button
|
||||
class="css-1yjiefr"
|
||||
>
|
||||
@ -139,3 +144,11 @@ exports[`Storyshots Addons|Actions Reserved keyword as name 1`] = `
|
||||
Delete
|
||||
</button>
|
||||
`;
|
||||
|
||||
exports[`Storyshots Addons|Actions configureActions 1`] = `
|
||||
<button
|
||||
class="css-1yjiefr"
|
||||
>
|
||||
Object (configured depth: 2)
|
||||
</button>
|
||||
`;
|
||||
|
@ -1,7 +1,7 @@
|
||||
/* global window */
|
||||
import React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { action, decorateAction } from '@storybook/addon-actions';
|
||||
import { action, configureActions, decorateAction } from '@storybook/addon-actions';
|
||||
import { setOptions } from '@storybook/addon-options';
|
||||
import { Button } from '@storybook/react/demo';
|
||||
import { File } from 'global';
|
||||
@ -66,7 +66,16 @@ storiesOf('Addons|Actions', module)
|
||||
>
|
||||
Multiple
|
||||
</Button>
|
||||
<Button onClick={() => action('Plain Object')({ foo: 'bar' })}>Plain Object</Button>
|
||||
<Button onClick={() => action('Plain Object')({ foo: { bar: { baz: { bar: 'foo' } } } })}>
|
||||
Plain Object
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
action('ObjectDepth2', { depth: 2 })({ root: { one: { two: { three: 'foo' } } } })
|
||||
}
|
||||
>
|
||||
Object (depth: 2)
|
||||
</Button>
|
||||
<Button onClick={() => action('RegExp')(reg)}>RegExp</Button>
|
||||
<Button onClick={() => action('String')('foo')}>String</Button>
|
||||
<Button onClick={() => action('Symbol')(Symbol('A_SYMBOL'))}>Symbol</Button>
|
||||
@ -75,4 +84,17 @@ storiesOf('Addons|Actions', module)
|
||||
<Button onClick={() => action('window')(window)}>Window</Button>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
.add('configureActions', () => {
|
||||
configureActions({
|
||||
depth: 2,
|
||||
});
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={() => action('ConfiguredDepth')({ root: { one: { two: { three: 'foo' } } } })}
|
||||
>
|
||||
Object (configured depth: 2)
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user