This repository has been archived by the owner on Jun 5, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 46
Reorder actions #64
Open
zalmoxisus
wants to merge
4
commits into
alexkuz:master
Choose a base branch
from
zalmoxisus:reorder-actions
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Reorder actions #64
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
const Benchmark = require('benchmark'); | ||
require('./jsdom'); | ||
const createElement = require('react').createElement; | ||
const render = require('react-test-renderer').create; | ||
const Component = require('../lib').default; | ||
|
||
const state = { | ||
actionsById: {}, | ||
computedStates: [], | ||
currentStateIndex: -1, | ||
nextActionId: 0, | ||
skippedActionIds: [], | ||
stagedActionIds: [], | ||
monitorState: { | ||
selectedActionId: null, | ||
startActionId: null, | ||
inspectedActionPath: [], | ||
inspectedStatePath: [], | ||
tabName: 'Diff' | ||
} | ||
}; | ||
|
||
function addNewAction() { | ||
const nextId = state.nextActionId; | ||
state.stagedActionIds = state.stagedActionIds.concat(nextId); | ||
state.actionsById = Object.assign(state.actionsById, | ||
{ [nextId]: { action: { type: 'ACTION' }, timestamp: 1 } } | ||
); | ||
state.computedStates = state.computedStates.concat({ state: {} }); | ||
state.currentStateIndex++; | ||
state.nextActionId++; | ||
} | ||
|
||
function removeFirstAction() { | ||
delete state.actionsById[state.stagedActionIds[0]]; | ||
state.stagedActionIds = state.stagedActionIds.slice(1); | ||
} | ||
|
||
const instance = render(createElement(Component, state)); | ||
|
||
const suite = new Benchmark.Suite; | ||
suite.add('Insert', function() { | ||
addNewAction(); | ||
instance.update(createElement(Component, state)); | ||
}) | ||
.add('Update', function() { | ||
removeFirstAction() | ||
addNewAction(); | ||
instance.update(createElement(Component, state)); | ||
}) | ||
.on('cycle', function(event) { | ||
console.log(String(event.target)); | ||
}) | ||
.run({ 'async': true }); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
const jsdom = require('jsdom').jsdom; | ||
|
||
global.document = jsdom(''); | ||
global.window = document.defaultView; | ||
global.navigator = { userAgent: 'node.js' }; | ||
|
||
const exposedProperties = ['document', 'window', 'navigator']; | ||
Object.keys(window).forEach((property) => { | ||
if (typeof global[property] === 'undefined') { | ||
exposedProperties.push(property); | ||
global[property] = window[property]; | ||
} | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
import React, { Component } from 'react'; | ||
import ReactDOM from 'react-dom'; | ||
import dragula from 'react-dragula'; | ||
import ActionListRow from './ActionListRow'; | ||
import ActionListHeader from './ActionListHeader'; | ||
import shouldPureComponentUpdate from 'react-pure-render/function'; | ||
|
@@ -19,6 +20,31 @@ export default class ActionList extends Component { | |
|
||
componentDidMount() { | ||
this.scrollToBottom(true); | ||
if (!this.props.draggableActions) return; | ||
const container = ReactDOM.findDOMNode(this.refs.rows); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's better to use callback ref instead of string (see https://github.com/bevacqua/react-dragula#example-using-refs-es2015-syntax), as string refs are not recommended recently. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure, but it was introduced before and used in other places. Better let's keep it for another PR. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok |
||
this.drake = dragula([container], { | ||
copy: false, | ||
copySortSource: false, | ||
mirrorContainer: container, | ||
accepts: (el, target, source, sibling) => ( | ||
!sibling || parseInt(sibling.getAttribute('data-id')) | ||
), | ||
moves: (el, source, handle) => ( | ||
parseInt(el.getAttribute('data-id')) && | ||
!/\bselectorButton\b/.test(handle.className) | ||
), | ||
}).on('drop', (el, target, source, sibling) => { | ||
let beforeActionId = Infinity; | ||
if (sibling && sibling.className.indexOf('gu-mirror') === -1) { | ||
beforeActionId = parseInt(sibling.getAttribute('data-id')); | ||
} | ||
const actionId = parseInt(el.getAttribute('data-id')); | ||
this.props.onReorderAction(actionId, beforeActionId) | ||
}); | ||
} | ||
|
||
componentWillUnmount() { | ||
if (this.drake) this.drake.destroy(); | ||
} | ||
|
||
componentDidUpdate(prevProps) { | ||
|
@@ -29,6 +55,8 @@ export default class ActionList extends Component { | |
|
||
scrollToBottom(force) { | ||
const el = ReactDOM.findDOMNode(this.refs.rows); | ||
if (!el) return; | ||
|
||
const scrollHeight = el.scrollHeight; | ||
if (force || Math.abs(scrollHeight - (el.scrollTop + el.offsetHeight)) < 50) { | ||
el.scrollTop = scrollHeight; | ||
|
@@ -57,13 +85,16 @@ export default class ActionList extends Component { | |
{filteredActionIds.map(actionId => | ||
<ActionListRow key={actionId} | ||
styling={styling} | ||
actionId={actionId} | ||
isInitAction={!actionId} | ||
isSelected={ | ||
startActionId !== null && | ||
actionId >= startActionId && actionId <= selectedActionId || | ||
actionId === selectedActionId | ||
} | ||
isInFuture={actionId > currentActionId} | ||
isInFuture={ | ||
actionIds.indexOf(actionId) > actionIds.indexOf(currentActionId) | ||
} | ||
onSelect={(e) => onSelect(e, actionId)} | ||
timestamps={getTimestamps(actions, actionIds, actionId)} | ||
action={actions[actionId].action} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have mixed feelings about this lib. It's simple and it works, which is great, but it's not so react-friendly (and it uses css hardcoded classes). I would suggest at least move this with all the dirty stuff in a separate component, if it's possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried
react-sortable-hoc
first, but it requires too much changes here, and there were some issues. Didn't notice any issues withreact-dragula
. It allows us easy to switch draggable feature on/off and also we will be able to duplicate actions in future.Those classes are added only while dragging. We could just set
draggableActions
prop tofalse
by default, so it will have no impact when not enabled.However, if you think this feature is not needed here, we could just add a prop to pass a custom
ActionList
component. It would also allow for better customizations. For example, I was planning to add props to hide buttons since we'll not need then in the extension.We could just use a fork for the extension if you think the purposes are different and you don't want to add extra complexity here. For example, including
react-split-pane
per #62, will also use global classes, and even worse prefixed. It is not a problem for the extension, but could be for Redux DevTools. Let me know what you think would be the best.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't mind having this feature, but it kinda forces us to use native DOM (including
data
attributes) and it concerns me.Have you tried
react-dnd
, BTW? I know, it looks a bit scary at first, but it's pretty good, Dan did a great job there :)http://gaearon.github.io/react-dnd/examples-sortable-simple.html
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I was using it here, but those decorators add complexity as well as
react-sortable-hoc
. We need 2 differentActionList
components - one draggable (with the decorator) and another one not (when havingdraggableActions
propfalse
). Also we need to update local state to drop the element before getting the data from the client part (which takes time in case of the extension). I see your point against using the DOM here, though it does its job. Not sure I'll have time in the next days for it (as other issues and3.0
are waiting). Maybe someone else could continue the work here.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or you can just use
DragSource.canDrag
:)It's ok to use
react-dragula
for now, it just would be nice to have it separated, so it could be easily replaced later.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well,
ActionList
is a bit more than just a plain list of rows, so I would introduce<DraggableList />
instead of<div {...styling('actionListRows')} ref='rows'>
that would be responsible just for dragging, so it would be like:There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. In this case I'd pick
react-sortable-hoc
, which simplifies it and also have some killer features like locking dragging inside the parent container or axis.However, the reason I picked Dragula is not to implement that. Action list items are the components which affect the performance the most. So, due to
maxAge
, we can have lots of such components removed and added per second.Taking into account that this feature would be used occasionally by less than 5% of users, we either find another way not to introduce any complexity here or not implement it at all. A solution would be to have a button to enable dragging (so we'll rerender
ActionListRow
s with addedDraggableListRow
), but, since Dragula offers it without any costs, I don't think it is worth to add changes to the UI.Maybe I missed something, feel free to amend my pr.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah,
react-sortable-hoc
looks much better. Actually, it would be interesting to measure it - is there really noticeable difference in performance for high-loaded actions.By the way, it might be useful to utilize something like react-virtualized for this list.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking about
react-virtualized
too, but it wouldn't help much, at least for the extension wheremaxAge
is50
by default, and, anyway, we shouldn't hide (virtualize) the new added action rows. The perf waste is due to creating new elements for every dispatched action. This is the part where React is very slow, and we should also take into consideration that unlike the extension, for vanilla Redux DevTools, React is in dev mode, making it even slower.Anyway, Inspector is twice faster than LogMonitor. The fastest is Chart monitor, despite of lots of d3 animations there.
I added recently
autoincrement
button to the demo and, as expected, it was crashing the extension. Surprisingly, it was mostly due to React, not to serialization. Just bouncing the data sent to the extension solved the problem.A benchmark would be rather useful indeed, so we could do it for every new feature to see the cost it adds.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a benchmark. It starts from an empty action list and adds actions one by one.
Before:
29.16 ops/sec ±25.13% (18 runs sampled)
.After adding
react-dnd
'sDragDropContext(HTML5Backend)
andDragSource
hocs:25.05 ops/sec ±25.87% (18 runs sampled)
.When starting not from an empty list, but having already 500 actions, it's
4.98 ops/sec ±4.29%
(without hocs) and3.64 ops/sec ±4.39%
(withreact-dnd
). But we're usually avoiding such bad perf withmaxAge
. Also note that I'm benchmarking production build, since it's not recommended to benchmark React in development and we're not using it for the extension. Without the extension we're running on dev environment and the perf should be worse.Obviously, Dragula has no impact, as nothing is done on component update. Unfortunately, I wasn't able to get
react-sortable-hoc
working onjsdom
(it fails after callingfindDOMNode
inside the lib), but it shouldn't be much different fromreact-dnd
, as there's lot of work inside the hocs on every update.If the perf waste would be for a core feature, it could be negotiable, but not for this one. It's not something we'd use frequently, but nice to have (I just got inspired by Dan's workflow). We would also add the ability to duplicate actions (when dragging an action holding
Alt
key), which should be more popular.Feel free to make changes in the pr when you have some time. It's not in a hurry, we're using a fork meanwhile.
UPDATE: For the sake of the experiment, I added also a benchmark for updates when using maxAge (so a row is added and the first one is removed).
Without hocs:
With hocs: