-
Notifications
You must be signed in to change notification settings - Fork 759
Sources Tree View #157
Sources Tree View #157
Conversation
Also, I'd like to merge this in, not squash it. These commits represent the points in history of my 3 ideas and I'd like to keep that in the history. I might want to refer back to various things I implemented in each of them (the way I generated the tree structure also changed in each of them). |
@@ -30,7 +30,9 @@ connectClient(response => { | |||
// otherwise, just show the toolbox. | |||
if (hasSelectedTab()) { | |||
const selectedTab = getSelectedTab(store.getState().tabs.get("tabs")); | |||
debugTab(selectedTab.toJS(), actions).then(renderToolbox); | |||
debugTab(selectedTab.toJS(), actions).then(() => { | |||
setTimeout(renderToolbox, 0); |
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.
What's the benefit 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.
I'll remove it, I was only using it will heavily debugging. Calling renderToolbox
within then
makes the entire rendering happen within a promise, so any errors will be gobbled up. We show unhandled errors in devtools but only when the promise is garbage collected, so you have to wait a few seconds (or even longer in worst cases).
Anyway, I hate promises because they are like a virus. They spread everywhere.
Thanks for the write up. I appreciated getting your thought process. I agree that maintaining local view state is a good balance. Also, I doubt mutating the tree will be a bottleneck. I didn't study the tree logic too much, but it seems fine. It'd be nice to add some unit tests here soon as well. |
Feel free to merge when you're ready. |
Mutating should not be the bottleneck, yeah, but when I was using persistent data structures it was a little more worrisome. Although several things were going on at that point, particularly this: in I used our performance tools and when there are thousands of sources, the thousands of |
I will write at least some basic tests before merging this. |
I moved the source tree data structure into a utility lib to clean up the I will work on more tests. I also might tweak the tree structure a little now that I'm more familiar with the problem. |
const { expanded, focusedItem } = this.state; | ||
|
||
const props = Object.assign({}, this.props, { | ||
isExpanded: item => expanded.get(item), |
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 think some code comments here would be really helpful on why this ManagedTree is needed and how some of the handlers are flowing through. It's a little hard to un-pack what is going on here, even though I know you mentioned to me briefly that you needed this. I'm finding the props on here to be particularly confusing. It somewhat breaks the metaphor in my head of how props get passed in from above, because this component is essentially shadowing the the parent's props. The only two properties that are overwritten are the onFocus
and renderItem
. Could these be different names, more explicit names to make this clearer?
I don't know if things could be refactored a little bit to make it clearer what's doing what... It could just be a matter of being more vigorous with comments for some context.
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.
It's essentially a higher-order component. It's quite useful to do this sort of stuff in React, but how we use it definitely should be documented.
The Tree component that we use has no component state (well, right now I think it does track the window height but I want to remove that). It doesn't keep track of the current focused item, or which nodes are expanded. It's all up to the consumer to handle that. It's nice to do it that way for several reasons: it makes tests easier to write, and it allows the consumer to store that info somewhere else like redux if they want to.
But it puts a lot of burden on the user, especially if they are trying to learn how to use the tree. This is a common problem with components: who manages the state? A good technique is to offer two variants of the same component: one that doesn't manage the state, and one that does (here, we are calling it "managed").
So we're just wrapping the Tree widget (so we can't change the prop names, we are passing all of that into the Tree) and handling the necessary state for it to work.
In practice, React has more going on than just "props passed down, events go up". For ease of use it's local component state is really nice. Also, there's really no reason it has to be separated from Redux state. At some point React should allow the consumer to "mount" the internal state of any component somewhere else; you can read about it in this React issue.
Anywhere, we're still figuring out the best patterns here and we'll definitely document them more.
This is a bigger PR, but hopefully it makes sense. I'm rushing to open this PR because I want to get this out there, but I plan on going through the code to see if there's anything to clean up. One thing I know I want to do is add more comments to document this.
This implements an efficient tree renderer of the sources. I went through several ideas to get here:
newSource
events. We can't re-create the whole tree on eachnewSource
event, so it's important that we incrementally update the existing tree structure as fast as possible.(Note: we call
loadSources
which gives us a big list of sources, but if the debugger hasn't seen any of those sources before if will fire anewSource
event before ourloadSources
call finished. That means when you open the debugger you get a ton ofnewSource
events and theloadSources
result. This is really confusing on the old debugger so I wanted to make that more clear)Sources
component; the reducer doesn't know anything about the tree shape, it just adds sources to theMap
. TheSources
component has internal state which is a mutable nested tree data structure. When it gets a new sourcesMap
, it diffs it with the old one to figure out which sources has been added, and adds those specific sources to the tree and rerenders.What's nice about this is that the reducer can add the same source multiple times, but if that happens it's a no-op on everything but the first add. We can also control exactly what happens with the tree structure strictly based on the sources
Map
; if it's an emptyMap
we clear the tree structure (happens on a reload). We could support removing sources, or anything we want, by analyzing the source objects. Deriving this information straight from there provides a nice division of responsibilities: the reducer just adds to theMap
and the component is responsible for generating the data it needs to render.This shouldn't break any replay, logging, or other functionality because the
SourcesTree
component should generate the tree structure on the fly.Let me know if you have any questions.