Skip to content

Commit f837ec8

Browse files
committed
Merge pull request #3133 from rhalff/addon-actions
Addon actions: fix slow logging
1 parent fd5a386 commit f837ec8

File tree

18 files changed

+279
-45
lines changed

18 files changed

+279
-45
lines changed

addons/actions/README.md

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,12 @@ Import the `action` function and use it to create actions handlers. When creatin
3535
3636
```js
3737
import { storiesOf } from '@storybook/react';
38-
import { action } from '@storybook/addon-actions';
38+
import { action, configureActions } from '@storybook/addon-actions';
3939

4040
import Button from './button';
4141

42+
action('button-click')
43+
4244
storiesOf('Button', module)
4345
.add('default view', () => (
4446
<Button onClick={ action('button-click') }>
@@ -69,3 +71,34 @@ storiesOf('Button', module)
6971
</Button>
7072
))
7173
```
74+
75+
## Configuration
76+
77+
Arguments which are passed to the action call will have to be serialized while be "transfered"
78+
over the channel.
79+
80+
This is not very optimal and can cause lag when large objects are being logged, for this reason it is possible
81+
to configure a maximum depth.
82+
83+
To apply the configuration globally use the `configureActions` function in your `config.js` file.
84+
85+
```js
86+
import { configureActions } from '@storybook/addon-actions';
87+
88+
configureActions({
89+
depth: 100
90+
})
91+
```
92+
93+
To apply the configuration per action use:
94+
```js
95+
action('my-action', {
96+
depth: 5
97+
})
98+
```
99+
100+
### Available Options
101+
102+
|Name|Type|Description|Default|
103+
|---|---|---|---|
104+
|`depth`|Number|Configures the transfered depth of any logged objects.|`10`|

addons/actions/src/index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import { action, configureActions, decorateAction } from './preview';
2+
13
// addons, panels and events get unique names using a prefix
24
export const ADDON_ID = 'storybook/actions';
35
export const PANEL_ID = `${ADDON_ID}/actions-panel`;
46
export const EVENT_ID = `${ADDON_ID}/action-event`;
57

6-
export { action, decorateAction } from './preview';
8+
export { action, configureActions, decorateAction };

addons/actions/src/lib/decycle.js

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
11
import { DecycleError } from './errors';
22

3-
import { getPropertiesList, typeReplacer } from './util';
3+
import { getPropertiesList, typeReplacer, omitProperty } from './util';
44

55
import { CYCLIC_KEY } from './';
66

77
import { objectType } from './types';
88

9+
import { DEPTH_KEY } from './types/object/configureDepth';
10+
11+
const { hasOwnProperty } = Object.prototype;
12+
913
export default function decycle(object, depth = 10) {
1014
const objects = new WeakMap();
1115

1216
let isCyclic = false;
1317

14-
const res = (function derez(value, path, _depth) {
18+
const res = (function derez(value, path, _depth, _branchDepthMax) {
1519
let oldPath;
1620
let obj;
1721

18-
if (Object(value) === value && _depth > depth) {
19-
const name = value.constructor ? value.constructor.name : typeof value;
20-
21-
return `[${name}...]`;
22-
}
22+
let maxDepth = _branchDepthMax;
2323

2424
const result = typeReplacer(value);
2525

@@ -51,19 +51,40 @@ export default function decycle(object, depth = 10) {
5151
if (Array.isArray(value)) {
5252
obj = [];
5353
for (let i = 0; i < value.length; i += 1) {
54-
obj[i] = derez(value[i], `${path}[${i}]`, _depth + 1);
54+
obj[i] = derez(value[i], `${path}[${i}]`, _depth + 1, maxDepth);
5555
}
5656
} else {
5757
obj = objectType.serialize(value);
5858

59-
getPropertiesList(value).forEach(name => {
60-
try {
61-
obj[name] = derez(value[name], `${path}[${JSON.stringify(name)}]`, _depth + 1);
62-
} catch (error) {
63-
console.error(error); // eslint-disable-line no-console
64-
obj[name] = new DecycleError(error.message);
59+
let newDepth;
60+
if (hasOwnProperty.call(obj, DEPTH_KEY)) {
61+
if (_depth + 1 < maxDepth) {
62+
const depthKey = obj[DEPTH_KEY];
63+
64+
newDepth = depthKey === 0 ? 0 : _depth + depthKey;
65+
maxDepth = newDepth >= depth ? depth : newDepth;
6566
}
66-
});
67+
68+
delete obj[DEPTH_KEY];
69+
}
70+
71+
if (_depth <= maxDepth) {
72+
getPropertiesList(value).forEach(name => {
73+
if (!omitProperty(name)) {
74+
try {
75+
obj[name] = derez(
76+
value[name],
77+
`${path}[${JSON.stringify(name)}]`,
78+
_depth + 1,
79+
maxDepth
80+
);
81+
} catch (error) {
82+
console.error(error); // eslint-disable-line no-console
83+
obj[name] = new DecycleError(error.message);
84+
}
85+
}
86+
});
87+
}
6788
}
6889

6990
if (_depth === 0 && value instanceof Object && isCyclic) {
@@ -74,7 +95,7 @@ export default function decycle(object, depth = 10) {
7495
}
7596

7697
return value;
77-
})(object, '$', 0);
98+
})(object, '$', 0, depth);
7899

79100
return res;
80101
}

addons/actions/src/lib/types/object/__tests__/index.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import objectType from '../';
2+
import { DEPTH_KEY } from '../configureDepth';
23

34
describe('Object', () => {
45
it('Serializes Object', () => {
56
function C() {}
67
const c = new C();
78

8-
expect(objectType.serialize(c)).toEqual({ [objectType.KEY]: 'C' });
9+
expect(objectType.serialize(c)).toEqual({
10+
[DEPTH_KEY]: 2,
11+
[objectType.KEY]: 'C',
12+
});
913
});
1014

1115
it('Deserializes Object', () => {
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export const DEPTH_KEY = '$___storybook.depthKey';
2+
3+
export default function configureDepth(obj, depth = 0) {
4+
obj[DEPTH_KEY] = depth; // eslint-disable-line no-param-reassign
5+
6+
return obj;
7+
}

addons/actions/src/lib/types/object/index.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
11
import createNamedObject from './createNamedObject';
22
import getObjectName from './getObjectName';
3+
import configureDepth from './configureDepth';
34

5+
const maxDepth = 2;
46
const KEY = '$___storybook.objectName';
57

68
const objectType = {
79
KEY,
810
// is: (value) => , // not used
9-
serialize: value => ({ [KEY]: getObjectName(value) }),
11+
serialize: value => {
12+
const objectName = getObjectName(value);
13+
if (objectName === 'Object') {
14+
return { [KEY]: objectName };
15+
}
16+
17+
return configureDepth({ [KEY]: objectName }, maxDepth);
18+
},
1019
deserialize: value => createNamedObject(value, KEY),
1120
};
1221

addons/actions/src/lib/util/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ export muteProperty from './muteProperty';
55
export prepareArguments from './prepareArguments';
66
export typeReviver from './typeReviver';
77
export typeReplacer from './typeReplacer';
8+
export omitProperty from './omitProperty';
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function omitProperty(name) {
2+
return name.startsWith('__') || name.startsWith('STORYBOOK_');
3+
}

addons/actions/src/lib/util/prepareArguments.js

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
import { decycle } from '../index';
22

3-
export default function prepareArguments(arg) {
4-
if (arg && typeof arg.preventDefault !== 'undefined') {
5-
return JSON.stringify(`[${arg.constructor.name}]`);
6-
}
7-
3+
export default function prepareArguments(arg, depth) {
84
try {
9-
return JSON.stringify(decycle(arg));
5+
return JSON.stringify(decycle(arg, depth));
106
} catch (error) {
117
return error.toString(); // IE still cyclic.
128
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import addons from '@storybook/addons';
2+
import { action, configureActions } from '../../';
3+
4+
jest.mock('@storybook/addons');
5+
6+
const getChannelData = channel =>
7+
channel.emit.mock.calls[channel.emit.mock.calls.length - 1][1].data;
8+
9+
describe('Action', () => {
10+
const channel = { emit: jest.fn() };
11+
addons.getChannel.mockReturnValue(channel);
12+
13+
it('with one argument', () => {
14+
action('test-action')('one');
15+
16+
expect(getChannelData(channel).args[0]).toEqual('"one"');
17+
});
18+
19+
it('with multiple arguments', () => {
20+
action('test-action')('one', 'two', 'three');
21+
22+
expect(getChannelData(channel).args).toEqual(['"one"', '"two"', '"three"']);
23+
});
24+
25+
it('with global depth configuration', () => {
26+
const depth = 1;
27+
28+
configureActions({
29+
depth,
30+
});
31+
32+
action('test-action')({
33+
root: {
34+
one: {
35+
two: 'foo',
36+
},
37+
},
38+
});
39+
40+
expect(getChannelData(channel).args[0]).toEqual(
41+
JSON.stringify({
42+
'$___storybook.objectName': 'Object',
43+
root: {
44+
'$___storybook.objectName': 'Object',
45+
one: {
46+
'$___storybook.objectName': 'Object',
47+
},
48+
},
49+
})
50+
);
51+
});
52+
53+
it('per action depth option overrides global config', () => {
54+
configureActions({
55+
depth: 1,
56+
});
57+
58+
action('test-action', { depth: 3 })({
59+
root: {
60+
one: {
61+
two: {
62+
three: {
63+
four: {
64+
five: 'foo',
65+
},
66+
},
67+
},
68+
},
69+
},
70+
});
71+
72+
expect(getChannelData(channel).args[0]).toEqual(
73+
JSON.stringify({
74+
'$___storybook.objectName': 'Object',
75+
root: {
76+
'$___storybook.objectName': 'Object',
77+
one: {
78+
'$___storybook.objectName': 'Object',
79+
two: {
80+
'$___storybook.objectName': 'Object',
81+
three: {
82+
'$___storybook.objectName': 'Object',
83+
},
84+
},
85+
},
86+
},
87+
})
88+
);
89+
});
90+
});

0 commit comments

Comments
 (0)