-
Notifications
You must be signed in to change notification settings - Fork 8.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Canvas][Layout Engine] Persistent grouping #25854
Conversation
💚 Build Succeeded |
Pinging @elastic/kibana-canvas |
@rashidkpc @w33ble a question on migration: should we prepare for 7.x workpads being opened with v6.5? I just tested it, and positioning is fine, one reason for which we store the absolute positions (ie. works fine in presentation mode), except of course that in edit mode, moving the group will just move an empty frame, the contents stay put. Which one should we do?
Doing option 1 would be comforting in that we don't tamper with indices - I think that leaving an invisible group border "residue" (like a completely transparent element) is the smaller bad - visible and transparent handling, haha - compared to leaving stuff in the index and revisiting with v7.x will produce who knows what. Are there stronger reasons for doing something else? |
@monfera we have a I think it's reasonable to treat workpads with a new version differently in 6.x. What we do with that is probably up for debate, but I think option 2 makes sense. Things would still function at least, even if you lose the grouping stuff when you go backwards. It's really @rashidkpc's call though. |
I know there's still work to do on this, but I wanted to weigh in on some architecture concerns here. In general we want to keep persisted document changes to the absolute minimum required to achieve a piece of user facing functionality. The less things we store, the fewer opportunities for breaking changes. We also strive to keep our schema extremely human readable. This makes it easier for users to understand and tweak. Here's what a page array looked like before this:
And after, with the previous 2 elements grouped:
Only 1 change is being intentionally being created by the user: The creation of the group.
Let's tweak this pull to only store a single change: It would have the added benefit of not causing any immediately observable issues when loading a grouping enabled workpad in a Canvas installation that isn't grouping enabled, the elements would simply fail to be grouped and would lose their grouped status if manipulated. This would leave us with the new persisted elements, grouped, appearing as such:
|
@rashidkpc @w33ble thanks for the feedback! I'll strive to automatically generating things, if I understand, it'll mean that we don't persist in the index some stuff that's present in redux. I have to think it through with this goal in mind. There are lots of positives about it, thanks also for enlisting many of those. As it's late here, I just jot down quick thoughts that are also consequences of the compressed representation in the index, glad to receive comments for tomorrow if possible. It's not an appeal for avoiding change, more like sharing thoughts that arise during work. Summary at the end. Consequences of minimizing index level storage changes:
To wrap it up, it feels like we can get away with an encoded representation now - even this is some kind of change to the index with the
It's true it'd be possible, and desirable, even though we could draw the line at GA (ie. not promote the use of beta / pre-beta Canvas, once it's in GA) as we can go into GA only once. Even if it's a goal for now, maybe we could achieve it by saving the leaf nodes where they are, and saving the grouping tree in some new corner of the index that the current version doesn't read. Having said all, I'm glad to go the compression route if confirmed, traversing the tree to get an |
fc82399
to
aad571a
Compare
💚 Build Succeeded |
You can get around that by encapsulating calculated values in selectors. For example, the |
I'm currently working on UI mockups for element templates and it has me thinking about the settings panel for grouped elements. For this initial version of grouped elements, we're just showing the workpad level settings in the righthand panel, so I'm wondering if/how that might change once we get into element templates (which seem to evolve from grouped elements). In other words, are there derived or defined settings for an element template or grouped elements? Can we derive settings from the underlying elements? etc. I'll create a separate issue or add some thoughts to the element templates issue #25531 , but wanted to raise it here as it seems like a potential addition to the Planned improvements, as subsequent small PRs: section in the description of this issue. |
Thanks @w33ble! The layout engine is pretty much selectors all the way through, so I guess we'd do something similar outside the layout engine, for at least the group elements, because they're needed for some stuff there too (eg. selecting / unselecting a group). There might be some duplication, maybe avoidable. |
Thanks @ryankeairns! Good point, groups currently don't do anything for the side bar, the only reason I didn't add it as an item for follow-up is that as you say, it must be addressed for templates anyway, but I made a followup reference there too. |
Hierarchical grouping is coming with the next rebase+push, it's needed for the responsive build-up of widgets from the bottom up (our current baseline is that all elements are resizable, it wouldn't have been nice to break it for no good reason) (elements don't need to be adjacent btw., they can be anywhere, any gaps will resize proportionally until we eventually have more refined constraints like minimum and maximum width) |
@monfera would you like feedback this week or should I wait a bit longer? |
@alexfrancoeur please hold on, there are still three checkboxes to fill in the "Rough edges to be fixed in this PR" and "Tasks arising from PR feedback" sections on the top |
Thanks Robert! I'll wait for them to o be checked
…On Thu, Dec 6, 2018, 9:29 AM Robert Monfera ***@***.***> wrote:
@alexfrancoeur <https://github.com/alexfrancoeur> please hold on, there
are still three checkboxes to fill in the "Rough edges to be fixed in this
PR" and "Tasks arising from PR feedback" sections on the top
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#25854 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AYA-f6kHFTH8wBZILWXqhH_x2C4YJhZ1ks5u2SnhgaJpZM4YoqDf>
.
|
aad571a
to
ca66690
Compare
@alexfrancoeur it would be a good time to check this out, there are still two things I'm planning (1. do not select a group unless a constituent element is selected; now even if you click on an empty area in the group, it gets selected; 2. maybe buttons? now it's just |
💚 Build Succeeded |
@@ -137,6 +137,16 @@ const handleKeyDown = (commit, e, isEditable, remove) => { | |||
} | |||
}; | |||
|
|||
const handleKeyPress = (commit, e, isEditable) => { |
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.
Is there a reason you're not handling this in the handleKeyDown
handler? My understanding is that both handlers will be called in most cases, with alt, ctrl, shift, and meta being the exception.
MDN: The keydown event is fired when a key is pressed down. Unlike the keypress event, the keydown event is fired for keys that produce a character value and for keys that do not produce a character value.
So pressing G
or U
is still going to trigger this:
commit('keyboardEvent', {
event: 'keyDown',
code: keyCode(key), // convert to standard event code
});
Maybe that's intentional?
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 it could technically work both ways (actually, keyPress will activate on the upward trajectory), it was simply a question of showing intent - one is concerned with signaling intent with a keypress gesture - 'G'
or 'U'
here - while the other conveys up / down events.
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.
The difference would be if you wanted to include control, shift, option, etc. Keypress would ignore keys combined with option on a mac, I believe.
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, keypress does ignore those keys. My point was that onKeyDown
is always going to be called, and that internal event would always be triggered, even though those specific keys are handled differently and in a different handler. Sounds like it shouldn't cause problems though.
|
||
// select the new element | ||
if (root) { | ||
window.setTimeout(() => dispatch(selectElement(newElement.id))); |
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.
Why does this need to be wrapped in setTimeout
? What happens if you dispatch immediately?
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 want to circle back to this one, the element in aeroelastic
is not present yet if done synchronously, so it can't select (pasting will create a new group, naturally with a new id
). So the purpose is to switch selection to the newly pasted thing, even if it's a group. I think it's due to the fact that cloning is done outside aero
but selection is in good part, inside (eg. to activate the resize frame). I'm sometimes thinking about a different synchronization between aero
and the Redux actions / thunks, it got fairly convoluted. Something I'm planning to touch on on our Jan 10 discussion on the layout engine.
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.
Some small change requests mixed with some questions.
@@ -213,12 +214,48 @@ export const duplicateElement = createThunk( | |||
} | |||
); | |||
|
|||
export const rawDuplicateElement = createThunk( |
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.
There's a lot of duplication of the code in duplicateElement
here... can you pull that out into a function that both actions use? Or even better, just use duplicateElement
with a new flag that indicates it should just clone the original object instead of doing a partial clone (afaict, that'd the difference between the two actions).
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 planning to layer it, ie. duplicateElement
relying on raw...
, or using an argument, or extracting out the common part (which is almost everything). I haven't done so due to lack of time and not having settled on what the best DRY option is 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'm cool with just opening a new issue about de-duping this code and merging this PR with it then.
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.
Opened #27447
.concat((get(page, 'groups') || []).map(augment('group'))); | ||
|
||
// todo unify or DRY up with `getElements` | ||
export function getNodes(state, pageId, withAst = true) { |
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 looks like this is meant to replace getElements
, and it also looks like nothing uses getElements
anymore... can we remove that selector?
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.
Good general point (getElements
is still used eg. by getElementById
which in turn is used by getSelectedElement
etc. but the ultimate caller could/should switch to getNode...
when we eg. want to update the sidebar from the selection, which isn't happening now)
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.
Opened #27449
const element = getElements(state, pageId, []).find(el => el.id === id); | ||
if (element) { | ||
return appendAst(element); | ||
} | ||
} | ||
|
||
export function getNodeById(state, id, pageId) { |
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 looks like this is meant to replace getElementById
, and it also looks like nothing uses getElementById
anymore... can we remove that selector?
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.
getElementById
is still in use but maybe not for too long, which is why I didn't worry regularizing it for now - the group templates will definitely bring in some new aspects, which will, I think, make elements and groups more and more similar.
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, it seemed like you were moving to "nodes" instead of "elements" as the terminology, so a node might be an element or a great of elements.
The only code I see that still uses getElementById
right now is the tests for the selector ;)
// See the resolved_args reducer for more information. | ||
}, | ||
persistent: { | ||
schemaVersion: 1, | ||
schemaVersion: 2, |
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.
If we are going to increment this, do we need any kind of migration to convert from version 1 to version 2? If things will just continue to work, do we really need to increment the schema version?
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 didn't look like we neeed to increment, I just acted on an earlier feedback item, though that might have been under different assumptions.
We don't need to migrate, so this can be reverted; on the other hand, the changes do represent a schema change (new props) so I didn't question the original feedback item, looked totally sensible.
In short, it can go either way; no need for migration.
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.
OK, might as well bump it then.
aero.matrix.rotateZ(angleRadians) | ||
); | ||
const transformMatrix = | ||
//position.localTransformMatrix || |
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.
Delete this.
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.
Done; will push
trimElement(element) | ||
); | ||
}, | ||
[actions.rawDuplicateElement]: (workpadState, { payload: { pageId, element } }) => { |
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.
Like the action, this is duplicating a lot of code from duplicateElement
(in fact, I think the two are identical). Getting back to a single action would fix it, otherwise pulling this code into a helper function would be good.
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.
Legit point. I have larger concerns as well, which is why I didn't bother, besides of course not getting around to it 😄 For it's quite inefficient to duplicate subtrees element by element. Similar to positioning, I'm planning to switch to bulk subtree duplication and these both will just go away 😄
const idMap = arrayToMap(nodes.map(n => n.id)); | ||
// We simultaneously provide unique id values for all elements (across all pages) | ||
// AND ensure that parent-child relationships are retained (via matching id values within page) | ||
Object.entries(idMap).forEach(([key]) => (idMap[key] = getId(key.split('-')[0]))); // new group names to which we can map |
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.
Since you're discarding the value in the entry, wouldn't Object.keys(idMap).forEach(key => ...
do the same thing 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.
Haha good spot, I overlooked this one, apparently ES2015 zealot here 😄 Changing it now
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.
Done, will push; an initial version used the value too and it got stuck :-)
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 only dug in because I haven't used Object.entries
before and had to look it up ;)
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 @w33ble and @rashidkpc have covered a lot here... my comments are all nits and best practices you may consider adding in a follow-up.
@@ -125,7 +125,7 @@ const handleKeyDown = (commit, e, isEditable, remove) => { | |||
const { key, target } = e; | |||
|
|||
if (isEditable) { | |||
if (isNotTextInput(target) && (key === 'Backspace' || key === 'Delete')) { | |||
if ((key === 'Backspace' || key === 'Delete') && !isTextInput(target)) { |
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.
At some point, I'd like to see these become constants, perhaps in an enum in a separate module. I'd add a TODO, or an issue perhaps, for all follow-ups?
@@ -137,6 +137,16 @@ const handleKeyDown = (commit, e, isEditable, remove) => { | |||
} | |||
}; | |||
|
|||
const handleKeyPress = (commit, e, isEditable) => { |
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.
The difference would be if you wanted to include control, shift, option, etc. Keypress would ignore keys combined with option on a mac, I believe.
const upcaseKey = key && key.toUpperCase(); | ||
if (isEditable && !isTextInput(target) && 'GU'.indexOf(upcaseKey) !== -1) { | ||
commit('actionEvent', { | ||
event: upcaseKey === 'G' ? 'group' : 'ungroup', |
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.
We need to get into the habit of creating constants for strings like this.
({ dispatch, getState }, rootElementIds, pageId) => { | ||
const state = getState(); | ||
|
||
// todo consider doing the group membership collation in aeroelastic, or the Redux reducer, when adding templates |
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.
Consider matching a TODO with an issue number, even if it's a single issue that encompasses all of the follow-ups. This makes it easier to grep
for all of the changes you want to make, (since searching for todo
would probably yield far too many results)
transformMatrix, | ||
a, // we currently specify half-width, half-height as it leads to | ||
b, // more regular math (like ellipsis radii rather than diameters) | ||
}; | ||
}; | ||
|
||
const shapeToElement = shape => { | ||
return { | ||
left: shape.transformMatrix[12] - shape.a, |
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.
Either a comment or a constant that defines to what 12
and 13
refer... if I were fixing a bug in this file, I'd be scared to death to touch these lines.
When I create a new workpad, I see the following:
UPDATE: I'm starting from the flights workpad that the sample data provides.
|
💚 Build Succeeded |
this might be caused by a race condition or something. this fix is probably just covering up some other bug :(
@monfera I fixed the bug I was seeing, but I suspect the Also, I notice that the state now tracks the size and position of the group. Here's a diff view when I resize a group of 2 elements: It doesn't seem like that should exist in state, since it's a calculated value based on the size and position of the elements inside it. |
Approved so long as Joe's comments above about the |
💚 Build Succeeded |
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.
Since the open requests have their own issues now, and since this looks functionally sound, the PR LGTM.
I'm going to merge this as-is. @monfera I'll leave it up to you to make sure the associated bugs are addressed before 6.6 ships. |
* Canvas element grouping (squashed) fix: don't allow a tilted group feat: allow rotation as long as it's a multiple of 90 degrees same as previous but recursively minor refactor - predicate minor refactor - removed if minor refactor - extracted groupedShape minor refactor - extracted out magic; temporarily only enable 0 degree orientations minor refactor - recurse minor refactor - ignore annotations minor refactor - simplify recursion minor refactor - simplify recursion 2 removed key gestures remove ancestors 1 remove ancestors 2 remove ancestors 3 remove ancestors 4 * lint * separate elements and groups in storage * renamed `element...` to `node...` (except exported names and action payload props, for now) * be able to remove a group * fixing group deletion * not re-persisting unnecessarily * persist / unpersist group right on the keyboard action * solving inverted cascading for backward compatibility * fix failing test case * page cloning with group trees of arbitrary depth * extracted out cloneSubgraphs * basic copy-paste that handles a grouping tree of arbitrary depth * fix: when legacy dataset doesn't have `groups`, it should avoid an `undefined` * PR feedback: regularize group IDs * lint: curlies * schemaVersion bump * copy/paste: restore selection and 10px offset of newly pasted element * - fix regression with ad hoc groups - fix copy/paste offsetting * PR feedback: don't persist node `type` and group `expression` * chore: remove commented out code * chore: switch Object.entries to Object.keys * fix: handle undefined value this might be caused by a race condition or something. this fix is probably just covering up some other bug :(
* Canvas element grouping (squashed) fix: don't allow a tilted group feat: allow rotation as long as it's a multiple of 90 degrees same as previous but recursively minor refactor - predicate minor refactor - removed if minor refactor - extracted groupedShape minor refactor - extracted out magic; temporarily only enable 0 degree orientations minor refactor - recurse minor refactor - ignore annotations minor refactor - simplify recursion minor refactor - simplify recursion 2 removed key gestures remove ancestors 1 remove ancestors 2 remove ancestors 3 remove ancestors 4 * lint * separate elements and groups in storage * renamed `element...` to `node...` (except exported names and action payload props, for now) * be able to remove a group * fixing group deletion * not re-persisting unnecessarily * persist / unpersist group right on the keyboard action * solving inverted cascading for backward compatibility * fix failing test case * page cloning with group trees of arbitrary depth * extracted out cloneSubgraphs * basic copy-paste that handles a grouping tree of arbitrary depth * fix: when legacy dataset doesn't have `groups`, it should avoid an `undefined` * PR feedback: regularize group IDs * lint: curlies * schemaVersion bump * copy/paste: restore selection and 10px offset of newly pasted element * - fix regression with ad hoc groups - fix copy/paste offsetting * PR feedback: don't persist node `type` and group `expression` * chore: remove commented out code * chore: switch Object.entries to Object.keys * fix: handle undefined value this might be caused by a race condition or something. this fix is probably just covering up some other bug :(
6.x 2a67310 |
Summary
Add persistent grouping and ungrouping to Canvas, covering the core functionality of grouping.
Group / ungroup changes:
shift-click
and group with keyg
for persistent groupingu
Rough edges to be fixed in this PR:
Tasks arising from PR feedback:
Planned improvements, as subsequent small PRs:
Follow-up work:
Internal changes related to grouping:
element
and aparent
propertyparent
property is persisted in Redux and the indextransformMatrix
, work withlocalTransformMatrix
in clientaeroelastic.js
Other internal changes:
aeroelastic
(original feedback on the layout engine from @w33ble)Checklist
Use
strikethroughsto remove checklist items you don't feel are applicable to this PR.- [ ] Any text added follows EUI's writing guidelines, uses sentence case text and includes i18n support- [ ] Unit or functional tests were updated or added to match the most common scenariosFor maintainers