Skip to content

Comments

[WIP][dashboard scoped filter] part 1: scope selector modal#8404

Closed
graceguo-supercat wants to merge 158 commits intofeature--dashboard-scoped-filterfrom
scope-selector-modal
Closed

[WIP][dashboard scoped filter] part 1: scope selector modal#8404
graceguo-supercat wants to merge 158 commits intofeature--dashboard-scoped-filterfrom
scope-selector-modal

Conversation

@graceguo-supercat
Copy link

@graceguo-supercat graceguo-supercat commented Oct 17, 2019

CATEGORY

  • Bug Fix
  • Enhancement (new features, refinement)
  • Refactor
  • Add tests
  • Build / Development Environment
  • Documentation

SUMMARY

This is part 1 for Dashboard Scoped Filter project. This PR is for a new UI module: filter scope selector modal. It covers the UI/UX that display/select scope (tabs and charts) for a filter field. Update dashboard redux state and persist filter scope settings to backend, will be covered in another PR.

Superset dashboard stores layout data in a tree structure. This layout structure always contains a root component. A root component will have a grid component (for dashboard without tabs) or a root-level tabs components as its children. In the dashboard, every UI component (tabs, row, column, and chart, etc) is a tree node. For example:
61979193-0da82280-afa8-11e9-9735-cd5c4435cc1e
We can think A is root component, leaves(K,L, F, G, M, I, J) are chart components, and the rest can be container (like row, column or tabs) components.

With the growing size of the dashboard, user may want to apply one dashboard filter to some of charts in dashboard, and one filter apply to another set of charts. A very common use case is user groups similar charts in different tabs, and make a filter only applicable for this group. Currently the way to set filter's scope is not very easy to use:
Screen_Shot_2019-10-16_at_9_49_54_AM

This PR introduced a new UI component: filter scope selector.

  • left panel: All filter_box in dashboard and their filter fields. There is also a toggle link to switch between single-edit mode or multi-edit mode.
  • right panel: show dashboard layout in the tree structure. But we hide row/column container, and group charts into its tab parent. If user have nested tabs containers, we will show all the nested tabs structure from chart to dashboard root component. We show a checkbox for each of chart and tab, user can click to select or deselect single or multiple charts.

When user clicks the Save button at the bottom of the modal, we will aggregate the checked chart ids into scope object and update dashboard filters state. The algorithm for this aggregation will be covered in another PR.

Features:

  1. Start filter scope selector:
    jwMsJ1jNea

  2. User can edit a single filter, or edit multiple filter fields all together. We also allow user toggle back to a single filter field in the multi-edit mode:
    PtIvVuSV6s

  3. Search chart name:
    d2ZfBriTy2

  4. When new chart is added to dashboard, based on its layout position, it will get filters that are applicable to its tab(s):
    NvggG46SWb

  5. When user move charts between tabs, this may cause chart to be applicable to different filter(s). We will show a warning message when chart is moved to a different filter scope.
    MVYXjUXxDr

Implementation

Filter scope selector has following UI components:

  • Container component: src/dashboard/containers/FilterScope.jsx

  • modal trigger: src/dashboard/components/filterscope/FilterScopeModal.jsx

  • core function UI component: src/dashboard/components/filterscope/FilterScopeSelector.jsx

  • checkbox tree component in left pane: FilterFieldTree.jsx
    recursive traversal renderer for each filter field tree node: renderFilterFieldTreeNodes.jsx
    simple UI component for filter filed item: FilterFieldItem.jsx

  • checkbox tree component in right pane: FilterScopeTree.jsx
    recursive traversal renderer for each filter scope tree node: renderFilterScopeTreeNodes.jsx

  • util functions:

getFilterFieldNodesTree: build filter field tree from dashboard filters redux state.
getFilterScopeNodesTree: build filter scope tree from dashboard layout.

Tree and tree nodes

We used a 3rd party component to render left panel filter field selector and right panel scope selector. It best matched our layout data structure, and offers very natural and convenient user experience like single selection, group selection, expand-on-click, etc.

According to this API, aCheckboxTree component needs following props:

// the original data to be rendered in `CheckboxTree`. The data should be in tree structure. Since js doesn't have build-in Tree data structure, we use array of node object.
// for filter scope tree, it is full list of the dashboard layout (only have tabs and chart components), 
// for filter fields tree, it is all dashboard filter box and its fields.
nodes: PropTypes.arrayOf(filterScopeSelectorTreeNodePropShape).isRequired,

// when user typed keywords in search box, right pane will only show matched charts and all their parent tabs. Only used by filter scope tree component.
nodesFiltered: PropTypes.arrayOf(filterScopeSelectorTreeNodePropShape).isRequired,

// An array of  node values. `CheckboxTree` component will show these node as checked.
checked: PropTypes.arrayOf(
    PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
  ).isRequired, 

// An array of node values. `CheckboxTree` component will show these node as expanded tree nodes. 
expanded: PropTypes.arrayOf(
    PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
  ).isRequired

For filter field tree, the tree node data is:

{
  value: filter key.
  label: customized column label or column name for the chosen filter field.
  children: array of children node
}

For filter scope tree, the tree node data is:

{
  value: chart id or tab component id
  label: chart name or tab title
  children: array of children node
}

Local state in the filter scope selector modal

For each filter filed, we construct a unique filter key which is composed of filter_box's chart id and filter's column name: chartId_columnName

FilterScopeSelector component uses following data to maintain the current state in the modal:

  • showSelector: as long as dashboard has one or more filter, we show filter scope selector UI. otherwise, show message says no filter in the dashboard.
  • filterFieldNodes: filter fields tree object.
  • filterScopeMap: a map for filter scope selections. in each entry, key is filter key, value is filter scope tree object for a filter field.
  • checkedFilterFields: array of all chosen filter key(s) in the left pane.

Based on the chosen filter keys from left pane, we show the filter scope tree in the right pane, which memorize the expanded tabs and selected charts. If all charts under the tab are selected, tab will show checked state in its checkbox. If some of the charts under the tab are selected, tab will show half checked state. And if none of the charts are selected, tab will show a empty state for its checkbox.

TEST PLAN

Manual test.

ADDITIONAL INFORMATION

  • Has associated issue:
  • Changes UI
  • Requires DB Migration.
  • Confirm DB Migration upgrade and downgrade tested.
  • Introduces new feature or API
  • Removes existing feature or API

REVIEWERS

@etr2460 @williaster @mistercrunch @kenchendesign

@graceguo-supercat graceguo-supercat changed the title filter scope selector modal [WIP][feature]filter scope selector modal Oct 17, 2019
@graceguo-supercat graceguo-supercat changed the title [WIP][feature]filter scope selector modal [WIP][feature]Dashboard filter scope selector modal Oct 17, 2019
Copy link
Member

@etr2460 etr2460 left a comment

Choose a reason for hiding this comment

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

first pass!


describe('dashboardFilters reducer', () => {
// disable broken unit tests by now, will fix it in another PR
xdescribe('dashboardFilters reducer', () => {
Copy link
Member

Choose a reason for hiding this comment

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

can we fix the unit tests in this pr since the behavior is being changed (and hopefully the unit tests confirm the proper change)?

Copy link
Author

@graceguo-supercat graceguo-supercat Oct 29, 2019

Choose a reason for hiding this comment

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

fixed. this is dashboardFilters reducer unit test. I have a lot more changes for dashboardFilters reducer in next PR, to merge filter scope logic into redux state.
I want to keep minimum change for redux change in this PR to avoid merge conflicts.

*/
import React from 'react';

export default function CheckboxUnchecked() {
Copy link
Member

Choose a reason for hiding this comment

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

same comment from CheckboxChecked

Copy link
Contributor

Choose a reason for hiding this comment

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

+1

dashboardFilters = {},
isSingleEditMode = true,
}) {
if (Object.keys(dashboardFilters).length === 0) {
Copy link
Member

Choose a reason for hiding this comment

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

this is unneeded, Object.values({}).map(...) returns []

Copy link
Author

Choose a reason for hiding this comment

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

cool. fixed.

let children = [];
if (currentNode.children && currentNode.children.length) {
currentNode.children.forEach(child => {
const cNode = traverse(components[child]);
Copy link
Member

Choose a reason for hiding this comment

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

what's a cNode? full variable names please

Copy link
Author

Choose a reason for hiding this comment

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

fixed.

}
};

if (nodes && nodes.length) {
Copy link
Member

Choose a reason for hiding this comment

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

prefer nodes.length > 0

Copy link
Author

Choose a reason for hiding this comment

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

fixed.

Copy link
Contributor

@kristw kristw left a comment

Choose a reason for hiding this comment

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

I wonder if in the future we can break a PR like this into smaller chunks? It is difficult to review 1600 lines of code.
Perhaps small PRs for the individual UI components and util functions, before another PR for the integration.


this.modal = null;
this.close = this.close.bind(this);
this.setModalRef = this.setModalRef.bind(this);
Copy link
Contributor

Choose a reason for hiding this comment

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

can use React.createRef() instead of manually setting ref

Copy link
Author

Choose a reason for hiding this comment

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

fixed.

expanded={expanded}
onCheck={onCheck}
onExpand={onExpand}
onClick={() => {}}
Copy link
Contributor

Choose a reason for hiding this comment

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

same with icons

Copy link
Contributor

@williaster williaster left a comment

Choose a reason for hiding this comment

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

left some comments, will do another pass later

componentId: component.id,
directPathToFilter,
scope: DASHBOARD_ROOT_ID,
scope: 'ROOT_ID',
Copy link
Contributor

Choose a reason for hiding this comment

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

why no constant anymore?

Copy link
Author

Choose a reason for hiding this comment

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

fixed the unit test.

*/
import React from 'react';

export default function CheckboxUnchecked() {
Copy link
Contributor

Choose a reason for hiding this comment

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

+1

return `${chartId}_${column}`;
}

export function getDashboardFilterByKey(key) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't understand why this function is called getDashboardFilterByKey, maybe getChartIdAndColumnFromFilterId?

Copy link
Author

Choose a reason for hiding this comment

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

renamed to getChartIdAndColumnFromFilterKey

* specific language governing permissions and limitations
* under the License.
*/
export function getDashboardFilterKey(chartId, column) {
Copy link
Contributor

Choose a reason for hiding this comment

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

can you change this to named arguments for better type safety + readability? (you use named arguments in many other places)

Copy link
Author

Choose a reason for hiding this comment

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

fixed as much as i found.


const FILTER_SCOPE_CONTAINER_TYPES = [TAB_TYPE, DASHBOARD_ROOT_TYPE];

export default function getFilterScopeNodesTree({
Copy link
Contributor

Choose a reason for hiding this comment

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

can you please add a description for what this function does?

Copy link
Member

Choose a reason for hiding this comment

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

bumping this comment!

Copy link
Author

Choose a reason for hiding this comment

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

please see the Implementation section. This tree structure is used by react-checkbox-tree framework.

@codecov-io
Copy link

codecov-io commented Oct 23, 2019

Codecov Report

Merging #8404 into feature--dashboard-scoped-filter will decrease coverage by 1.69%.
The diff coverage is 53.41%.

Impacted file tree graph

@@                         Coverage Diff                         @@
##           feature--dashboard-scoped-filter    #8404     +/-   ##
===================================================================
- Coverage                             67.65%   65.95%   -1.7%     
===================================================================
  Files                                   448      469     +21     
  Lines                                 22498    23091    +593     
  Branches                               2364     2468    +104     
===================================================================
+ Hits                                  15220    15230     +10     
- Misses                                 7140     7700    +560     
- Partials                                138      161     +23
Impacted Files Coverage Δ
...erset/assets/src/SqlLab/components/QuerySearch.jsx 58.65% <ø> (ø) ⬆️
...uperset/assets/src/SqlLab/components/SouthPane.jsx 91.42% <ø> (ø) ⬆️
superset/examples/birth_names.py 100% <ø> (ø) ⬆️
...rset/assets/src/SqlLab/components/QueryHistory.jsx 83.33% <ø> (ø) ⬆️
superset/assets/src/components/ModalTrigger.jsx 90% <ø> (ø) ⬆️
superset/examples/sf_population_polygons.py 29.16% <ø> (ø) ⬆️
superset/examples/unicode_test_data.py 100% <ø> (ø) ⬆️
superset/examples/deck.py 11.11% <ø> (ø) ⬆️
...rc/explore/components/controls/CheckboxControl.jsx 85.71% <ø> (+14.28%) ⬆️
superset/examples/tabbed_dashboard.py 22.22% <ø> (ø) ⬆️
... and 159 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update c422b49...4456fa3. Read the comment docs.

villebro and others added 5 commits October 23, 2019 16:43
* Separate RC from VERSION

* Fix pypi_push.sh and refine instructions

* Add SUPERSET_ prefix to env variables

* Finalize release instructions

* Change one-off to First Time Only

* Add tagging of final version

* Convert some remaining env variables and add a check step to pypi deploy
francishemingway and others added 12 commits November 12, 2019 22:00
* added user tutorial and rearranged docs hierarchy

* added license header, renamed admin tutorial file

* fixed image url issue

* Fix spelling

* Tweaks to text based on feedback

* guilabel and menu test

* added guilabel and upload csv section

* tidy up of rst formatting
* [cli] Fix, import datasources exported by UI
* Update CONTRIBUTING.md

* Update CONTRIBUTING.md
* Allow customization of documentation icon and text

* Set icon width to 100%

* Use double quotes for strings
* Add support for database engine SAP Hana

* Support hana services

Increase time, minute, and second

* Fix hana return string

* Fix formatting errors
* [docs] Update CHANGELOG with 0.35.0

* [docs] Fix, github md problem
* passing onClick prop to header with the existing onChange method.

* basic test checking that label click fires the onChange method.

* cleaning up stuff caught by linting.
* Encode feature flags to JSON pessimistically

* Add unit test

* Remove old imports
@willbarrett
Copy link
Member

Hey @graceguo-supercat - this PR is getting impossible to review due to the size of the delta. Should we close this one and open something fresh that's rebased off of latest master?

@graceguo-supercat
Copy link
Author

graceguo-supercat commented Nov 13, 2019

Please comment in #8553. Please note I plan to work on feature branch feature--dashboard-scoped-filter, then merge whole feature branch to master.

@graceguo-supercat
Copy link
Author

@willbarrett @etr2460 review it for a long time, he prefer to use this old PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.