Skip to content
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

Refactor useQueryExecution & add inline query execution status #4584

Merged
merged 30 commits into from
Feb 19, 2020

Conversation

arikfr
Copy link
Member

@arikfr arikfr commented Jan 23, 2020

What type of PR is this?

  • Refactor

Description

While trying to add an inline Query Execution status that won't remove the current results in display, I realized that we need to refactor useQueryExecution to allow the view to retain current results.

While the QueryResult class needs significant refactoring (probably split into two: one for execution and one for data), I did one change: add a callback to receive current execution status. This calllback allows us to remove the polling logic from useQueryResult and actually remove useQueryResult entirely. I did keep the getQueryResultData helper, but we should probably remove it when we get to do said refactoring.

I will add some more implementation notes in comments.

It's still not complete:

  • When triggering new execution while previous still running it messes up the status (both flashing).
  • Need better empty state for when no results are available (error or otherwise).
  • Empty state ("no results, try refresh") flashes on first load.
  • Make sure both View/Editor have the same query result information.
  • Add space between text and Cancel button.
  • Consider adding "inline" query execution status in Query Editor too.
  • Move QueryExecutionStatus elsewhere in mobile view.

Related Tickets & Documents

Mobile & Desktop Screenshots/Recordings (if there are UI changes)

Peek 2020-01-23 12-46

…k instead of polling on the query result status.

This is a step towards bigger refacotr. We should completly rewrite this class and split it into two:

1. QueryExecution -- executes a query and tracks status.
2. QueryResult -- represents the actual QueryResult and has the different utilities to use the results (although those might be moved into helpers).
@@ -154,20 +159,14 @@ function QuerySource(props) {
return;
}
if (isDirty || !isEmpty(selectedText)) {
executeAdhocQuery(selectedText);
executeQuery(null, () => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to improve the API here so we don't have to pass null as first argument if we're passing in custom executor.

const [fullscreen, setFullscreen] = useState(false);
const toggleFullscreen = useCallback(() => setFullscreen(!fullscreen), [fullscreen]);

useEffect(() => {
updateUrlSearch("fullscreen", fullscreen ? true : null);
}, [fullscreen]);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the sync w/ URL on purpose. I'm not sure we need it here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I actually kept it thinking about sharing the URL and to have the same behavior the Dashboard page have. But both cases are not that important anyway.

{queryResult && queryResultData.status !== "done" && (
<div className="query-alerts m-t-15 m-b-15">
{(executionError || isExecuting) && (
<div style={{ position: "absolute", bottom: "75px", right: "20px", zIndex: 1000 }}>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to move this into the stylesheet.

/>
</div>
)}
{(!queryResult || queryResultData.status === "done") && (
{true && (
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we indeed keep viz always visible, need to remove this line.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is probably the best option, the query view page just look so empty when loading and it makes the whole process of updating results look smoothier :)

shortcut="alt+f"
onClick={toggleFullscreen}>
<i className="zmdi zmdi-fullscreen" />
<Icon type={fullscreen ? "fullscreen-exit" : "fullscreen"} />
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switched to Ant's icons. If we like it, we should use it in other places where we have "fullscreen" toggle.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm leaned towards the zmdi one, but I think I'm biased anyway.

newQueryResult
.toPromise(onStatusChange)
.then(queryResult => {
// TODO: this should probably belong in the QueryEditor page.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A quick note: here (and in other similar places) we need to handle a case when component was destroyed during async operation (ignore all .then/.catch and callback calls otherwise all sort of setStates will trigger error).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@gabrieldutra
Copy link
Member

This is definitely in the direction I was thinking about (always having a result showing up). From the gif, imo the alert will need a different styling, I considered either having a notification (any from the Antd options) or just having the text saying the status of the execution in that footer. At most a colored Icon to emphasize the status.

query.latest_query_data_id = queryResult.getId();
query.queryResult = queryResult;
}
// TODO: this belongs on the query page?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tend to prefer this logic in here mainly for 2 reasons: 1 - Reduce complexity from Pages; 2 - It's a shared piece of code between View and Source.

In terms of where this code "belongs", it feels to me it could be in both the query page or in here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not for this PR, but eventually it makes sense to use useQueryExecute in Widgets, where this logic isn't needed.

useEffect(() => {
queryRef.current = query;
executeQueryRef.current = executeQuery;
}, [executeQuery, query]);
Copy link
Member

@gabrieldutra gabrieldutra Feb 4, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the safest way I could think to remove the dependencies from the useEffect hook and make it execute only once.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is useEffect really needed here? Seems you can leave only assignment - assigning value to ref.current does not re-render component.

@gabrieldutra
Copy link
Member

For "Empty state ("no results, try refresh") flashes on first load." I added a state to control whether the first results were loaded or not. So far I just keep it hidden until first results are loaded and it looks ok, apart from 2 cases:

  1. If you use "maxAge" it will trigger a whole new query execution, thus take more time before showing up;
  2. If you have a huge set of results. In this case I don't think introduce a loading state will solve the problem as the UI freezes for a while (I'm assuming it's due to the heavy transform calculations it does);

For 2, it's not in the scope in here, but explore WebWorkers for heavy calculations is passing through my mind (I think this could help with Dashboard performance issues as well).

}

setExecutionState({
queryResult,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's also the option of keeping old queryResults when a query has an error, but you were thinking about an Error State?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 not sure what I had in mind here. Will experiment with both options and decide.

@gabrieldutra
Copy link
Member

@arikfr, I think most of the functionality side of this feature is done (I checked from the checkboxes in the description). Now it's mostly a matter of iterating design for both desktop and mobile.

For those I didn't understand very well what you mean:

  1. "Need better empty state for when no results are available (error or otherwise)." -> (Refactor useQueryExecution & add inline query execution status #4584 (comment) should be related)
  2. "Make sure both View/Editor have the same query result information." -> You mean to keep the query result information when you change between view/edit? (if it's what you meant, I can do it before iterating design)
  3. "Consider adding "inline" query execution status in Query Editor too." -> Is it related to the "jumping" that happens on the existing position for the execution status?

@gabrieldutra
Copy link
Member

No, I meant show the same information (and potentially reuse the same component). Right now we show different things in view and edit.

👍 I think I got it now, I updated the QueryExecutionMetadata to be a separate component. Now it's the same for both view and edit.


  1. Updated 👍
  2. I separated in two different cases and it's a matter of refining the message now. We'll also need to handle the case where the visualization should be rendered with 0 rows. I think the only existing case now is a Counter with countRows(?) If so, we can create an exception just for it now and open an issue to make it more general as it seems out of the scope here.
  3. Added a min-width that covers the non-error messages 👍.
  4. Updated 👍

@arikfr
Copy link
Member Author

arikfr commented Feb 13, 2020

We'll also need to handle the case where the visualization should be rendered with 0 rows. I think the only existing case now is a Counter with countRows(?) If so, we can create an exception just for it now and open an issue to make it more general as it seems out of the scope here.

It makes sense for the visualization to decide how it handles 0 rows. Why not render the viz in such case?

@arikfr
Copy link
Member Author

arikfr commented Feb 13, 2020

When clicking on "Add Description" it should auto focus the input field.

@gabrieldutra
Copy link
Member

gabrieldutra commented Feb 13, 2020

It makes sense for the visualization to decide how it handles 0 rows. Why not render the viz in such case?

🤔, the simplest should be to just pass an EmptyState node to the VisualizationRenderer. I'll see how easy it is to make the default behavior for visualizations to render this empty state, but also not be hard to replace it.

Edit: What I had in mind not to be changing all visualizations was to do sth similar to cff0163 (have a property that would define when a vis is going to use the provided emptyState)

- remove inputRef and use autoFocus attr
@gabrieldutra
Copy link
Member

gabrieldutra commented Feb 13, 2020

🪲 Just noticed there's some change specifically in this PR that breaks Filters (only multi-filters perhaps), seems to happening due to multiple React updates. E.g.: https://deploy-preview-4584--redash-preview.netlify.com/queries/334

Will investigate -> It seems sth to do with this (2f45fc5) Merge commit, I'll continue to see what may have gone wrong with it.

Fixed in cf56791. Looks like it was a missing useMemo that was triggering a loop update on a piece of code added from that merge commit 😬

@arikfr
Copy link
Member Author

arikfr commented Feb 14, 2020 via email

@gabrieldutra
Copy link
Member

gabrieldutra commented Feb 14, 2020

What will pass the empty state node? The viz implementation?

QueryPage pass emptyState -> General Vis Render checks if visualization isEmpty(data, options) (default to be rows.lenght == 0, but changeable by visualizations) and renders it if true. So by default what passes the emptyState is the context (query/widget), but it can be changed to the visualization by changing that isEmpty function. This is what I could think not to go on each visualization and add an empty state... Also as much as it makes sense for it to have each visualization controlling it's own empty state, it does for it to have a "Query" or "Widget" shared one... Anyway, lmk your opinion

@arikfr
Copy link
Member Author

arikfr commented Feb 16, 2020

Let's minimize changes to the code or behavior and delegate to the visualization to handle this (which at the moment means do nothing -- like we had before).

I'll try to think about this by considering the various visualizations & states (query page vs. widget) and come up with a definition for future improvement.

@arikfr arikfr added this to the v9 milestone Feb 18, 2020
@arikfr arikfr marked this pull request as ready for review February 19, 2020 10:09
@arikfr
Copy link
Member Author

arikfr commented Feb 19, 2020

@gabrieldutra I did two small changes here:

  1. Add label to the field Data Source field.
  2. Hide navbar in full screen mode (and reuse dashboard's useFullscreenHandler).

There are still few small design fixes to do, but I will merge this (and the parent PR) now and open a separate issue detailing them.

@arikfr arikfr merged commit c6936a1 into query-view Feb 19, 2020
@arikfr arikfr deleted the refactor-execution branch February 19, 2020 13:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants