-
Notifications
You must be signed in to change notification settings - Fork 14.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
feat: Pass dashboard context to explore through local storage #20743
feat: Pass dashboard context to explore through local storage #20743
Conversation
/testenv up |
@kgabryje Ephemeral environment spinning up at http://54.203.155.41:8080. Credentials are |
Codecov Report
@@ Coverage Diff @@
## master #20743 +/- ##
==========================================
- Coverage 66.33% 66.29% -0.04%
==========================================
Files 1756 1757 +1
Lines 66769 66867 +98
Branches 7059 7077 +18
==========================================
+ Hits 44288 44332 +44
- Misses 20684 20726 +42
- Partials 1797 1809 +12
Flags with carried forward coverage won't be shown. Click here to find out more.
Help us with your feedback. Take ten seconds to tell us how you rate us. |
@jinghua-qa Can you help with testing please? |
/testenv up |
element => element.id !== DASHBOARD_ROOT_ID && !element.parents, | ||
) | ||
) { | ||
updateComponentParentsList({ |
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 sample dashboards don't have parents
in their layout metadata, which caused e2e tests to fail
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.
Can we fix the sample dashboards metadata instead of introducing this logic? We can do it in a follow-up if needed.
behaviors.includes(Behavior.INTERACTIVE_CHART) && | ||
!metadata.chart_configuration[chartId].crossFilters?.chartsInScope | ||
) { | ||
metadata.chart_configuration[chartId].crossFilters.chartsInScope = |
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.
Optimization for cross filters (works the same as the one implemented for native filters a few months ago) to speed up finding charts in scope. Also, thanks to that, we don't need to save dashboard layout in local storage
@kgabryje Ephemeral environment spinning up at http://54.245.54.96:8080. Credentials are |
); | ||
return Object.fromEntries( | ||
Object.entries(dashboardsContexts).filter( | ||
([, value]) => !value.isRedundant, |
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.
A new dashboard tab id is generated on each dashboard page opening. We mark ids as redundant when user leaves the dashboard, because they won't be reused. Then we remove redundant dashboard contexts from local storage in order not to clutter it
return filterBoxData; | ||
}; | ||
|
||
const mergeNativeFiltersToFormData = ( |
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.
This logic is copied from /superset/utils/core.py::merge_extra_form_data
}, [] as SimpleAdhocFilter[]); | ||
}; | ||
|
||
const mergeFilterBoxToFormData = ( |
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.
This logic is copied from /superset/utils/core.py::merge_extra_filters
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.
Thanks for working on this! LGTM in general, just a couple of questions.
); | ||
const filterBoxFilters = useSelector<RootState, Record<string, any>>(() => | ||
getActiveFilters(), | ||
); |
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.
Can we consolidate all these into one single state object, and maybe reuse DashboardPermalinkState (or at least extend from it and consolidate variable names) for it? We can rename it to something like PersistableDashboardState
if necessary.
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 idea!
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 ended up creating a new state interface. The state that we save in local storage is trimmed specifically for the use of Explore in order to reduce the size of saved payload (to reduce the risk of overflowing the local storage), so extending interfaces used in Dashboard wasn't applicable
sharedLabelColors, | ||
colorScheme, | ||
chartConfiguration, | ||
nativeFilters, |
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 nativeFilters
the full metadata of all native filters? Do we really need to carry it over to the Explore page? I don't see it being used 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.
It's state.nativeFilters.filters
(so without filterSets). We use it in getFormDataWithExtraFilters
called in line 88. However, now I noticed that we only need chartsInScope
for each filter - the rest of metadata is redundant. Same goes for cross filters (chartConfiguration). I'll change that
return newFormData; | ||
}; | ||
|
||
export const getFormDataFromDashboardContext = (formData: JsonObject) => { |
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 this util function!
@@ -97,6 +98,7 @@ const SliceHeader: FC<SliceHeaderProps> = ({ | |||
}) => { | |||
const dispatch = useDispatch(); | |||
const uiConfig = useUiConfig(); | |||
const dashboardTabId = useContext(DashboardTabIdContext); |
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 do we use a new ReactContext for this? Can this be a Redux state?
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 only need to pass dashboardTabId deep into the component tree, with no intention of ever modifying it or interacting with other redux state variables. I think for such use cases Context is optimal
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.
This all good. I'm not a fan of Redux anyway... just thought creating a new provider has its own overhead---i.e., too many nested context provider always feels wrong.
const filterBoxFilters = useSelector<RootState, Record<string, any>>(() => | ||
getActiveFilters(), | ||
); | ||
const dashboardTabId = useMemo(() => shortid.generate(), []); |
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.
Can we rename this to dashboardPageId
to avoid confusion with dashboard tabs?
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 point!
updateDashboardTabLocalStorage(dashboardTabId, { | ||
...payload, | ||
isRedundant: 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.
Instead of adding a isRedundant
flag, can we just remove it?
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.
That might lead to race conditions between removing local storage key and consuming it in Explore.
Case 1: You open Explore in a new tab and close the tab with dashboard before Explore mounts - key doesn't exist anymore in Explore.
Case 2: You open Explore in the same tab as dashboard - Dashboard component unmounts and removes the key before Explore mounts. That case could be handled by always removing the key in Explore instead of on Dashboard unmount. However, that leads us to Case 3: you open Explore in a new tab, key is removed on Explore mount, then you go back to the tab with dashboard and open a new tab with Explore - key doesn't exist anymore in this tab.
I think flagging key as redundant and removing it on next dashboard action is the safest approach 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.
This makes sense. Thanks for the explanation.
{t('Edit chart')} | ||
</Tooltip> | ||
<Menu.Item key={MENU_KEYS.EXPLORE_CHART}> | ||
<Link to={this.props.exploreUrl ?? '#'}> |
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.
Instead of ?? '#'
why not just remove the whole link in case url is not available?
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.
Oops, forgot to remove that. Now exploreUrl is always defined so we don't need a null check.
<Button buttonStyle="secondary" buttonSize="small"> | ||
<Link to={this.props.exploreUrl ?? '#'}> | ||
{t('Edit chart')} | ||
</Link> |
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.
Would <Link >
actually add an <a>
inside a button element?
Should we use react-router-dom's useHistory
instead?
https://v5.reactrouter.com/web/api/Hooks/usehistory
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 does. I'll change that to onClick with history.push
filters: Object.fromEntries( | ||
( | ||
Object.entries(filterBoxFilters) as [ | ||
string, | ||
{ | ||
scope: number[]; | ||
values: DataRecordValue[]; | ||
}, | ||
][] | ||
) | ||
.filter(([, filter]) => filter.scope.includes(sliceId)) | ||
.map(([key, filter]) => [ | ||
getChartIdAndColumnFromFilterKey(key).column, | ||
filter.values, | ||
]), |
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.
Can L90 to L104 be of its own utility function?
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.
👍
Thanks for the PR @kgabryje. This is a nice improvement now that we have the permalink feature in place and form data keys are not shared anymore. I noticed that the |
The logic related to |
39644b5
to
cc8ea6f
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.
Code LGTM. Can we get @jinghua-qa's approval before merging the PR? @jinghua-qa It would be a good idea to run our complete test suite on this to make sure everything is working 😉
element => element.id !== DASHBOARD_ROOT_ID && !element.parents, | ||
) | ||
) { | ||
updateComponentParentsList({ |
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.
Can we fix the sample dashboards metadata instead of introducing this logic? We can do it in a follow-up if needed.
OK, i can create a test branch and deploy a workspace to run automation today. |
/testenv up |
@jinghua-qa Ephemeral environment spinning up at http://52.24.5.129:8080. Credentials are |
/testenv up FEATURE_DASHBOARD_CROSS_FILTERS=true |
@jinghua-qa Ephemeral environment spinning up at http://35.164.111.177:8080. Credentials are |
I found one issue that when open the chart from dashboard with color theme, and then save the chart as new chart, the color theme for the new chart still not able to change the color theme. Other than that i think the changes LGTM. Screen.Recording.2022-07-24.at.1.47.14.AM.mov |
cc8ea6f
to
0ea75cf
Compare
Thank you for spotting that @jinghua-qa ! Fixed Screen.Recording.2022-07-25.at.14.52.18.mov |
Ephemeral environment shutdown and build artifacts deleted. |
I'm afraid it breaks |
Yes you're right, that flag is no longer needed. Now you can click "Edit chart" like any other link to open in a new tab - either cmd + click, or right click + "open in a new tab", or middle mouse button. |
filterBoxFilters, | ||
dataMask, | ||
dashboardId, | ||
} = |
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.
SUMMARY
Currently when user clicks "Edit chart" or chart's title in Dashboard page, we first send a POST request to retrieve form_data_key, then create an Explore URL with that form_data_key and redirect to Explore. We do that in order to pass dashboard context (such as native filters, cross filter, color scheme...) to Explore.
There are several problems with this approach:
In order to refactor "Edit chart" to be a link, we need to pass dashboard context in a different way.
This PR implements the following changes:
BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF
Screen.Recording.2022-07-18.at.15.46.56.mov
TESTING INSTRUCTIONS
ADDITIONAL INFORMATION