-
Notifications
You must be signed in to change notification settings - Fork 152
Updated UI for workflows #4604
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
base: master
Are you sure you want to change the base?
Updated UI for workflows #4604
Conversation
const connect_launcher_button = document.getElementById('btn-connect'); | ||
const delete_launcher_button = document.getElementById('btn-delete'); | ||
const selected_launcher = document.getElementById('select_launcher'); | ||
const base_launcher_url = "<%= project_launchers_path(@project.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.
Since our javascript lives outside of ERB (once it's in its own file), this will have to be reworked to get the project.id from the html somewhere.
|
||
async function makeLauncher(x, y, id, title) { | ||
const url = `${base_launcher_url}/${id}/render_button`; | ||
const response = await fetch(url, { headers: { "Accept": "text/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.
I think there is a way to do this better with jbuilder, so that you can generate small html blocks from the backend. Jquery also provides some better ways of making requests than fetch. Check out this method from active_jobs.js that does something analogous when extended details are requested.
ondemand/apps/dashboard/app/javascript/active_jobs.js
Lines 65 to 74 in 5ecfabc
$.getJSON(jobDataUrl, function (data) { | |
// Open this row | |
row.child(data.html_ganglia_graphs_table).show(); | |
// Add the data panel to the view | |
$(`div[data-jobid="${escapeHtml(row.data().pbsid)}"]`) | |
.hide() | |
.html(data.html_extended_panel) | |
.fadeIn(250); | |
// Update the status label in the parent row | |
tr.find(".status-label").html(data.status); |
a98d5f1
to
3f87947
Compare
4f03717
to
6fd4d06
Compare
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 like these changes, the grid structure makes it seem much more organized from the start and the overlap system stops most ambiguity.
On the functionality side, there are a few things I still notice
- You cannot have more than one instance of a launcher. This was something we touched on in our meeting and was possible under the current approach to running workflows. In addition to enabling reuse and saving time for users, this would make the UI much easier to test
- The grid doesn't fit neatly in the canvas panel. On my screen at full width I can fit 5.5 grid columns as you have defined them. Adjusting that it looks like the grid stays fixed as the screen resizes, so you not only can have launchers partially cut off despite being in valid grid locations, but if you adjust too much (say by moving from a fullscreen to a windowed view), you can 'lose' launchers without noticing. The easiest fix for this IMO would be a resize handler that adjusts the canvas size and keeps the grid locations balanced and visible. Obviously with the scrolling there is only so much we can do, but I think that would help a lot.
- The grid size could be expandable. This prevents an implicit size limit imposed by the grid. I think expanding should only happen in one direction just for uniformity (every graph is 12 potential nodes wide, but unbounded in length, for example). All good if this is in your plans and hasn't made it in yet.
- Edges can still overlap, which is the final spot where ambiguity may creep in. If I have nodes 1,2,3 horizontally in a row and I have edge 13 and edge 23, edge 23 is completely obscured. Maybe we detect this and add some vertical space between to prevent? The same issue could happen regardless of the axis they are arranged upon but the idea is making any overlapping lines into parallel lines that still hit the launcher's correctly.
On the implementation side there are still some points about the javascript factoring and organization that I don't think you've gotten to yet (totally fine) but one thing to keep in mind with that is limiting the prevalence of pixel computation in lots of different functions. The use of mathematical terms and patterns is great and I can generally tell what a block of computations is doing at a glance, but this is not generally the case for everyone and we should hide all of that nitty gritty stuff behind small descriptive functions to make it more readable and intuitive. It would also be nice if we could avoid some of the pixel handling to css transforms and structures but that will be easiest to do once the computations are factored into the most fundamental blocks.
I do think these changes do a great job at addressing some of my earlier concerns, it is no longer possible to overlap launchers and there is always enough room to fit an arrow neatly in between nodes, which I think will save us a lot of work later on with rendering. Great work!
The zoom feature works well, but maybe resize the initial canvas so that min zoom shows the whole thing? For me it still cuts off the last column at 50% and this is not impacted by any browser zoom. |
Oh now I understand your comment, the idea is to show complete grid cell not half cut-off; basically either resize the cell height-width or zoom such that it shows complete cells. Got it. |
Yeah absolutely. And the zoom thing does cut you some slack because you don't have to factor screen size in quite as much, if the starting size cuts something off they can just zoom out. But that only gets you around the problem if the fully zoomed out screen shows you the whole thing (ideally no scrollbars at 50% zoom). |
I have handled 2, 3. For rest I have added as next todo task in the PR description. Tested the UI code multiple time. @Bubballoo3 Now open to formatting code review, any feature request will handle in separate 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.
I didn't mean to suggest you had to use snake case, taking a peek at the rest of the javascript code in the project it looks like it is used sparingly, and it provides nice visual separation if we prefer snake case for ruby and camel case for javascript. I actually think the snake case works well for everything ending in a dimension (x,y,l,w), so cell_w
is fine compared to cellW
but for regular variables like selected_edge
I think camel case is better.
The main formatting changes I had in mind was adding some of the class structure we had discussed. I see basically 4 places that we can use classes to avoid passing around pixel values quite so much, and make it all a little clearer. I am including some example code from chatgpt but that is mostly to show the functionality I see these classes having, and don't know if the implementation is exactly right.
- Box
Stores positional information and handles movement itself. Other code can tell it to move to a specific position (when dragging) or tell it to snap into the grid (when dropped).class Box { constructor(el, row, col, w, h) { this.el = el; this.row = row; this.col = col; this.x = 0; this.y = 0; this.w = w; this.h = h; } moveTo(x, y) { this.x = x; this.y = y; this.el.style.left = x + 'px'; this.el.style.top = y + 'px'; } snap(cellToXY) { const pos = cellToXY(this.row, this.col); this.moveTo(pos.x, pos.y); } }
- Edge
Similarly to the box, we should let the edges handle their own position updates. Since we only create and update lines, this doesn't need much more.class Edge { constructor(fromBox, toBox, el) { this.from = fromBox; this.to = toBox; this.el = el; } update() { const { from, to } = this; const cx1 = from.x + from.w / 2, cy1 = from.y + from.h / 2; const cx2 = to.x + to.w / 2, cy2 = to.y + to.h / 2; const dx = cx2 - cx1, dy = cy2 - cy1; const start = intersectRect(cx1, cy1, from.w, from.h, dx, dy); const end = intersectRect(cx2, cy2, to.w, to.h, -dx, -dy); this.el.setAttribute('x1', start.x); this.el.setAttribute('y1', start.y); this.el.setAttribute('x2', end.x); this.el.setAttribute('y2', end.y); } }
- Pointer
This gives you a single source for getting the mouse position, and ensures we are only updating what we need to. Other code reads from the pointer, and the functionpointerInStage(e)
becomes clearer aspointer.update(e)
this can then be initialized in a global context and update itself as the mouse moves, which (should) compute the mouse position eagerly and make it faster to access when needed. (It also suggested adding a 'touchmove' listener, so initialization would look something likeclass Pointer { constructor(stage, zoomRef) { this.stage = stage; this.zoomRef = zoomRef; // pass a reference to the current zoom this.x = 0; this.y = 0; } update(e) { const r = this.stage.getBoundingClientRect(); const clientX = e.clientX ?? e.touches?.[0].clientX; const clientY = e.clientY ?? e.touches?.[0].clientY; this.x = (clientX - r.left) / this.zoomRef.value; this.y = (clientY - r.top) / this.zoomRef.value; } pos() { return { x: this.x, y: this.y }; } }
const zoomState = { value: 1 }; const pointer = new Pointer(stage, zoomState); workspace.addEventListener('pointermove', e => pointer.update(e)); workspace.addEventListener('touchmove', e => pointer.update(e));
- DragController
This class would bring together the drag mechanics into one place, and uses the other three to simplify. This example seems like it will need more modification than the others, but is still a useful theoretical exampleAnd then launcher creation is justclass DragController { constructor(pointer) { this.pointer = pointer; this.dragging = null; this.start = null; document.addEventListener('pointermove', e => this.onMove(e)); document.addEventListener('pointerup', e => this.onUp(e)); } beginDrag(box) { this.start = { px: this.pointer.x, py: this.pointer.y, x: box.x, y: box.y }; this.dragging = box; } onMove(e) { if (!this.dragging) return; this.pointer.update(e); const dx = this.pointer.x - this.start.px; const dy = this.pointer.y - this.start.py; this.dragging.moveTo(this.start.x + dx, this.start.y + dy); } onUp(e) { if (!this.dragging) return; // You can add snapping / occupancy checks here this.dragging = null; this.start = null; } }
$launcher.on('pointerdown', e => { pointer.update(e); drag.beginDrag(model); // model is a Box instance });
I know there is a lot there but I think this will break it up into the right steps and make the flow of the code a lot clearer. I do like the functionality as it stands, so you might consider writing tests before these changes just to keep parity with what you have now. That being said testing this will be tough, but it will help later on if we already have some rudimentary ones ensuring everything moves around properly.
Also forgot to mention that it looks like these changes conflict with an existing PM test (see CI logs), so that will have to be resolved before we can merge |
Another thing I just noticed (which the refactoring may solve, or at least make easier to solve) is that while the edges track very well with the mouse, the boxes 'stick' as you pull them across. Everything makes it to the right place eventually, but it would be nice if we set up the listeners so that both the arrow and box move in sync. |
The tests are fixed now, all review changes have been taken care of. |
Ok I dug into this and figured out what was making the launchers stick. When you set the endpoints of the SVG lines, these changes are rendered synchronously, but changing the style of the html element works asynchronously as it doesn't render until the CSS reloads. The way to fix this is by using moveTo(x, y) {
this.x = x;
this.y = y;
this.el.style.transform = `translate(${x}px, ${y}px)`;
} Which makes the movement smooth. Unfortunately this seems to do something with the initial position, and renders the box one grid place to the right (despite edges acting totally normal, pointing to the position the box should be). I think if you can finish implementing that and work out the issues this should be ready to merge. |
016446e
to
ccf3be4
Compare
@johrstrom I think this is ready for your review, there is still going to be more work on the workflows file in future PRs as we integrate this into the backend, but I am happy at the moment with the mechanics, organization and maintainability of what we have. @harshit-soora until Jeff gets to this, (and I hate to add one more thing again) there seems to be a bug if you double click the 'create launcher button', where it creates duplicate overlapping launchers and generally breaks it (because they have the same id). So we should disable the button during creation to prevent this. I also noticed that the 'connect launchers' button stays clicked until it is pressed again instead of resetting after an edge is made. I could go either way on this behavior, it does make it easy to connect everything quickly but also leads to behavior that seems buggy but is actually just because you don't realize it is still on. Either way I wanted to document that here for discussion. Nice work! |
I added button accents (even widen the border) so it is more prominent that it has been clicked. I tried having feature where you have to click it everytime you are connecting an edge. To me it felt like a lot of labour to click the button over and over. |
8da1d95
to
7f08c19
Compare
Split into css/html/js files and addressed review comments Fixed bidirectional edge issue, and added a way to delete edge Added dag class in UI to detect any cyclic dependency Fixed error of not resetting dag when edge or launcher gets deleted at the UI Added grid logic, each launcher will have fixed spot and no one can take anyone else's place. Plus scrolling logic improved A little formatting of JS files Added zoom functionality Fixed size st. launcher should fit and added mechanism to allow increase of grid till 32x32~1024 launchers Added box, edge, pointer class and refactored code a little; next will add dragController class Added dragController class, pending CI issue Made launcher identical to those on project page, should fix tests Fix the tests by allowing grabbing by title only Changed launcher-item to launcher-box to isolate ourselves from scss of projects Changed top/left style to transfrom for smooth transitions Fixed double click on launcher, added accent to buttons
7f08c19
to
337fe17
Compare
Workflows Epic Link - #4338
UI Discussion and feature demo - #4625
Just UI changes, no stitching with the backend.
Todo tasks required:
This is how new UI looks like:
