Skip to content

Commit

Permalink
Merge pull request #3133 from rhalff/addon-actions
Browse files Browse the repository at this point in the history
Addon actions: fix slow logging
  • Loading branch information
Hypnosphi authored Apr 16, 2018
2 parents 3affc30 + bc883bc commit 503c186
Show file tree
Hide file tree
Showing 18 changed files with 279 additions and 45 deletions.
35 changes: 34 additions & 1 deletion addons/actions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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') }>
Expand Down Expand Up @@ -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`|
4 changes: 3 additions & 1 deletion addons/actions/src/index.js
Original file line number Diff line number Diff line change
@@ -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 };
53 changes: 37 additions & 16 deletions addons/actions/src/lib/decycle.js
Original file line number Diff line number Diff line change
@@ -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);

Expand Down Expand Up @@ -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) {
Expand All @@ -74,7 +95,7 @@ export default function decycle(object, depth = 10) {
}

return value;
})(object, '$', 0);
})(object, '$', 0, depth);

return res;
}
6 changes: 5 additions & 1 deletion addons/actions/src/lib/types/object/__tests__/index.js
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down
7 changes: 7 additions & 0 deletions addons/actions/src/lib/types/object/configureDepth.js
Original file line number Diff line number Diff line change
@@ -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;
}
11 changes: 10 additions & 1 deletion addons/actions/src/lib/types/object/index.js
Original file line number Diff line number Diff line change
@@ -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),
};

Expand Down
1 change: 1 addition & 0 deletions addons/actions/src/lib/util/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 3 additions & 0 deletions addons/actions/src/lib/util/omitProperty.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function omitProperty(name) {
return name.startsWith('__') || name.startsWith('STORYBOOK_');
}
8 changes: 2 additions & 6 deletions addons/actions/src/lib/util/prepareArguments.js
Original file line number Diff line number Diff line change
@@ -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.
}
Expand Down
90 changes: 90 additions & 0 deletions addons/actions/src/preview/__tests__/action.test.js
Original file line number Diff line number Diff line change
@@ -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',
},
},
},
},
})
);
});
});
16 changes: 16 additions & 0 deletions addons/actions/src/preview/__tests__/configureActions.test.js
Original file line number Diff line number Diff line change
@@ -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,
});
});
});
Original file line number Diff line number Diff line change
@@ -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');
Expand Down
Original file line number Diff line number Diff line change
@@ -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, {
Expand All @@ -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 changes: 7 additions & 0 deletions addons/actions/src/preview/configureActions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const config = {
depth: 10,
};

export function configureActions(options = {}) {
Object.assign(config, options);
}
11 changes: 11 additions & 0 deletions addons/actions/src/preview/decorateAction.js
Original file line number Diff line number Diff line change
@@ -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);
};
};
}
Loading

0 comments on commit 503c186

Please sign in to comment.