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

[Feature] Dashboard filter indicators #7908

Merged
merged 2 commits into from Aug 22, 2019

Conversation

graceguo-supercat
Copy link

@graceguo-supercat graceguo-supercat commented Jul 21, 2019

CATEGORY

Choose one

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

SUMMARY

  1. Show indicator on each chart which filter can apply and what filter values are applied
  2. Allow user navigate from chart to any of its applicable filter.

Implementation

  • Refactor filters in redux state.

Before this PR, filters state is a sub-state in dashboardstate:

dashboard redux state:

{
    datasources,
    sliceEntities,
    charts,
    dashboardInfo,
    dashboardState: {
      ...
      filters: {
        124: {
           __time_range: 'Last quarter',
        },
        131: {
           gender: ['boy', 'girl'],
        }
      }
      ...
    },
    dashboardLayout,
    messageToasts: [],
    impressionId: shortid.generate(),
}

In order to support UI features and in-dashboard navigation functionality, I move dashboardFilter up to be root-level state in redux store, and it will contain more props about filter value and its layout information:

dashboard redux state:

{
    datasources,
    sliceEntities,
    charts,
    dashboardInfo,
    dashboardState,
    dashboardFilters: {
      124: {
        chartId: 124,
        componentId: "CHART-CHTxAXRuJW",
        directPathToFilter: [
          'ROOT_ID',
          'TABS-VPEX_c476g',
          'TAB-1bnpXJkoS8',
          'ROW-qc1WPC4bOn',
          'CHART-CHTxAXRuJW'
        ],
        scope: "ROOT_ID",
        isDateFilter: true,
        isInstantFilter: true,
        columns: {
          __time_range(pin): "Last quarter",
        }
      },
      131: {
        ...
      }
    },
    dashboardLayout,
    messageToasts: [],
    impressionId: shortid.generate(),
}

Because filters prop is used by a lot of components in dashboard, I added a converter utility function buildActiveFilters, which converts raw dashboardFilters state into old filters format so that other components can still use filters as before, and won't be affected by this big state change.

buildActiveFilters function will be triggered whenever dashboardFilters state is changed, and the converted results will be cached. All other components in dashboard will call getActiveFilters to get the cached filters state, and they don't need to run the converter function again and again.

  • Color assignment

In this PR we introduced a new color palette for dashboard filters.

Each filter field is assigned a unique colorKey: chartId_filter_column_name. This colorKey is used by bother filter badge and filter indicator. Dashboard will pick a color from filter color palette based on colorKey and build a color map. During render time, each filter and indicators in the chart will lookup color code based on its colorKey. Similar to buildActiveFilters function, dashboardFilter state update will trigger re-build color map.

  • In-dashboard navigation
    tree1-2

Suppose A is root, B,C,D are top level tabs, F, G are charts under tabs, H is nested tab, M is chart under nested tab. For example, when user lands on M, how to navigate user to a filter component in another root level tab B?

Dashboard stores layout data in flattened tree structure: each parent node contains ids of its children, and every UI component (chart, header, row, column, tabs, etc) keeps all the parent component ids from root to itself (see #6945):

  'CHART-PuHBPak5nl': {
    children: [],
    id: 'CHART-PuHBPak5nl',
    meta: {
      chartId: 146,
      height: 50,
      sliceName: 'Chart B2',
      width: 4
    },
    parents: [
      'ROOT_ID',
      'TABS-nGgMw9Fmmj',
      'TAB-YWfqbvBaqp',
      'TABS-cGWCi1V-mr',
      'TAB-CZxYwmgZZ',
      'ROW-iC9LI5XSxU'
    ],
    type: 'CHART'
  }

From this parents list it's very easy to find out to select which root tab or nested tab to a specific filter. dashboardState has a directPathToChild prop, which is the current state of dashboard, holding a path to one of the chart (or any UI component). When user clicks on a filter indicator, dashboard will update its directPathToChild to be target filter's path. Then dashboard will render the correct list of parent tab/tabs, and display filter chart.

  • Filter indicators UI components

    • Container component: /dashboard/container/FilterIndicators.jsx
    • /dashboard/components/FilterIndicatorsContainer.jsx: indicators organizer for each chart. Usually one chart will be applicable for one or more filters, but user can set dashboard metadata that a chart might be immune to all filters in the dashboard, or some filter fields. We only display indicators for applicable filters. The logic to show/group/hide applicable filters for each chart is here.
    • /dashboard/components/FilterIndicator.jsx and /dashboard/components/FilterIndicatorGroup.jsx: little colored indicator component for each filter, if chart having > 5 filters, will group them into a group.

Screen Shot 2019-08-08 at 11 46 05 AM

  • /dashboard/components/FilterTooltipWrapper.jsx: the basic usage of react-bootstrap Tooltip components are not enough for our need. We allow user to click on tooltip (especially needed for indicator group) to navigate to actual filter chart. So I added customized wrapper component to trigger/hide Overlay with some delay.
  • /dashboard/components/FilterIndicatorTooltip.jsx: tooltip component for filter. it shows column name and selected values.

Screen Shot 2019-07-26 at 2 53 04 PM

If the filter is applicable but not applied, we will show **Not filtered**. If the filter is not applicable to the chart, we will not show indicator.

SCREENSHOTS OR ANIMATED GIF

Page landing:

  • each filter field is assigned a badge with unique color.

NXB4w4V9OB

Interact with filter indicator

  • Hover on chart, you will see filter indicators popping out. The color bar in the filter indicator matched with filter's badge color.
  • Hover on filter indicator, it will show filter's name and selected value(s), as filter tooltip.
  • We show at most 5 filter indicators for each chart. For charts that having more filters, its indicators will be grouped in the last spot.
  • For immune chart, it doesn't have any filter indicator.
  • For chart with immune fields, we only show indicators for applicable fields.

dKRnGt8z0n

Navigate from chart to filter

  • When user clicks on one of filter indicator, or click on pencil icon in the filter tooltip, page will navigate to the chart, even cross tabs.
  • The selected filter chart will be highlighted with green border.
  • If the chart has more than 1 field, the user selected field will be highlighted.

8KuUojNgUB

TEST PLAN

CI and 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

@williaster @michellethomas @etr2460 @xtinec @mistercrunch @kenchendesign
@betodealmeida @khtruong

@villebro
Copy link
Member

villebro commented Jul 22, 2019

Needs rebasing (build error fixed in master)

@codecov-io
Copy link

codecov-io commented Jul 22, 2019

Codecov Report

Merging #7908 into master will increase coverage by 0.11%.
The diff coverage is 71.09%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #7908      +/-   ##
==========================================
+ Coverage   65.57%   65.69%   +0.11%     
==========================================
  Files         469      486      +17     
  Lines       22496    22804     +308     
  Branches     2446     2515      +69     
==========================================
+ Hits        14752    14980     +228     
- Misses       7624     7696      +72     
- Partials      120      128       +8
Impacted Files Coverage Δ
...ets/src/visualizations/FilterBox/transformProps.js 0% <ø> (ø) ⬆️
.../assets/src/dashboard/containers/DashboardGrid.jsx 100% <ø> (ø) ⬆️
superset/assets/src/chart/chartReducer.js 20.33% <ø> (ø) ⬆️
superset/assets/src/dashboard/reducers/index.js 100% <ø> (ø) ⬆️
...rset/assets/src/dashboard/containers/Dashboard.jsx 0% <ø> (ø) ⬆️
...ssets/src/dashboard/containers/DashboardHeader.jsx 100% <ø> (ø) ⬆️
...sets/src/dashboard/containers/DashboardBuilder.jsx 0% <ø> (ø) ⬆️
...ts/src/dashboard/containers/DashboardComponent.jsx 100% <ø> (ø) ⬆️
...t/assets/src/dashboard/reducers/getInitialState.js 0% <0%> (ø) ⬆️
...et/assets/src/dashboard/reducers/dashboardState.js 73.46% <0%> (-9.87%) ⬇️
... and 55 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 4e7ea3f...b02b9a9. Read the comment docs.

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.

a bunch of comments, will definitely give another pass too

const { filters } = this.props;
const currentFilteredNames =
filterKey && filters[filterKey] ? Object.keys(filters[filterKey]) : [];
const filter_immune_slices = this.props.dashboardInfo.metadata
Copy link
Member

Choose a reason for hiding this comment

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

camelCase 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.

filterKey && filters[filterKey] ? Object.keys(filters[filterKey]) : [];
const filter_immune_slices = this.props.dashboardInfo.metadata
.filter_immune_slices;
const filter_immune_slice_fields = this.props.dashboardInfo.metadata
Copy link
Member

Choose a reason for hiding this comment

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

camelCase

Copy link
Author

Choose a reason for hiding this comment

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

fixed.

return;
}

const filter_immune_slice_fields_names =
Copy link
Member

Choose a reason for hiding this comment

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

camelCase

Copy link
Author

Choose a reason for hiding this comment

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

fixed.

@@ -97,4 +106,14 @@ describe('dashboardState actions', () => {
);
});
});

it('should dispatch removeSlice if a removed slice is a filter_box', () => {
Copy link
Member

Choose a reason for hiding this comment

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

nit: should be 'should dispatch removeFilter i think

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
Author

Choose a reason for hiding this comment

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

yes!!

@@ -60,12 +61,26 @@ describe('Dashboard', () => {
return wrapper;
}

const overrideFilters = {
Copy link
Member

Choose a reason for hiding this comment

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

maybe just a stylistic preference, but i tend to name constant fixtures like this as OVERRIDE_FILTERS

Copy link
Author

Choose a reason for hiding this comment

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

fixed.

* specific language governing permissions and limitations
* under the License.
*/
const FILTER_COLORS_COUNT = 20;
Copy link
Member

Choose a reason for hiding this comment

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

maybe note this var in the css file (and vice versa) so that people know where to update it if something changes

Copy link
Author

Choose a reason for hiding this comment

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

fixed.

/* eslint-disable camelcase */
import { TIME_RANGE } from '../../visualizations/FilterBox/FilterBox';

export default function getFilterConfigsFromFormdata(form_data = {}) {
Copy link
Member

Choose a reason for hiding this comment

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

camelCase throughout please!


return filter_configs.reduce((result, config) => {
/* eslint-disable no-param-reassign */
result[config.column] = config.vals;
Copy link
Member

Choose a reason for hiding this comment

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

i'd prefer to not disable the lint rule here and just make an additional const in this function if needed

Copy link
Author

Choose a reason for hiding this comment

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

fixed.

* specific language governing permissions and limitations
* under the License.
*/
export default function isEmptyValue(value) {
Copy link
Member

Choose a reason for hiding this comment

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

i'd imagine there's a lodash function or something that already implements this, any reason why we chose to write our own here?

Copy link
Author

Choose a reason for hiding this comment

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

thanks! Use lodash now.

const TIME_RANGE = '__time_range';
export const TIME_RANGE = '__time_range';
export const FILTER_LABELS = {
[TIME_RANGE]: 'Time range',
Copy link
Member

Choose a reason for hiding this comment

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

should this be wrapped in t()?

Copy link
Author

Choose a reason for hiding this comment

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

we don't translate values in map. the UI component which uses this map will translate the needed value.

@graceguo-supercat graceguo-supercat changed the title [Feature] Dashboard filter indicators [WiP][Feature] Dashboard filter indicators Jul 31, 2019
@graceguo-supercat graceguo-supercat force-pushed the gg-FilterIndicator branch 2 times, most recently from ea70f42 to 568ea9c Compare August 1, 2019 06:01
@graceguo-supercat graceguo-supercat changed the title [WiP][Feature] Dashboard filter indicators [Feature] Dashboard filter indicators Aug 1, 2019
@graceguo-supercat
Copy link
Author

graceguo-supercat commented Aug 1, 2019

ping @etr2460 I rebased onto master, added a few unit tests to increase coverage (you have to click the Codecov link to see updated stats), fixed most of comments. Please take a second look. Thanks.

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.

a few more comments!

componentWillReceiveProps(nextProps) {
const { inFocus = false } = nextProps;
if (inFocus) {
this.scrollToView(0);
}
}

getLocationHash() {
Copy link
Member

Choose a reason for hiding this comment

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

You don't need this function anymore right?

Copy link
Author

Choose a reason for hiding this comment

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

thanks. i added this instance method before, because I had difficulty to add unit test, which needs to mock window.location.hash. now i found a better solution here.


render() {
const { indicators } = this.props;
const tooltipList = indicators.map((indicator, index) => (
Copy link
Member

Choose a reason for hiding this comment

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

personal preference, but i find it easier to read if we just put this map in the return JSX as well. Co-locating all of it makes it easier to see the whole dom structure

Copy link
Author

Choose a reason for hiding this comment

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

fixed.

return Array.isArray(values) ? values.join(', ') : values;
}

export default function FilterIndicatorTooltip({
Copy link
Member

Choose a reason for hiding this comment

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

still not sure if this is needed?

...dashboardFilters,
};
/* eslint-disable no-param-reassign */
delete updatedFilters[action.chartId];
Copy link
Member

Choose a reason for hiding this comment

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

prefer: const { [action.chartId]: deletedItem, ...updatedFilters };

https://github.com/airbnb/javascript/blob/master/README.md#objects--rest-spread

Copy link
Author

Choose a reason for hiding this comment

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

thanks! this problem bothered me for a long time. now i knew solution.

*/
.dashboard-filter-indicators-container {
position: absolute;
z-index: 10;
Copy link
Member

Choose a reason for hiding this comment

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

is a z index absolutely needed here? Have we ensured that 10 is appropriate compared to all other possible z indexes?

Copy link
Author

Choose a reason for hiding this comment

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

be honest, i don't know how to make ensured 10 is appropriate. Just by manual test.

Copy link
Member

Choose a reason for hiding this comment

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

got it, we should probably make an issue to manage the z-indexes better at some point. this is fine for now though

overflow: hidden;
display: flex;
background-color: #bababa;
transition: all 0.3s;
Copy link
Member

Choose a reason for hiding this comment

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

not a big fan of transition all, can we explicitly call out what styles should be transitioned over?

Copy link
Author

Choose a reason for hiding this comment

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

fixed. only width attribute.

@graceguo-supercat graceguo-supercat force-pushed the gg-FilterIndicator branch 2 times, most recently from b318ea2 to f0be2fc Compare August 6, 2019 19:16
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.

just a couple final nits, but otherwise lgtm! I'm super excited to see this go out!

componentWillReceiveProps(nextProps) {
const { inFocus = false } = nextProps;
if (inFocus) {
this.scrollToView(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 doesn't need an argument right? or you can remove the default arg on the function

Copy link
Author

Choose a reason for hiding this comment

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

fixed.

return (
<FilterTooltipWrapper
tooltip={
<React.Fragment>
Copy link
Member

Choose a reason for hiding this comment

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

We're using react 16.8 now so this can be changed to <> and the closing tag can be </>

Copy link
Author

Choose a reason for hiding this comment

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

I search the whole code base and found about 9 places used <React.Fragment> but 0 place used <>. Maybe consistency is more important, if this is only a syntax sugar?

* under the License.
*/
/* eslint-disable no-param-reassign */
/* eslint-disable camelcase */
Copy link
Member

Choose a reason for hiding this comment

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

👍

*/
.dashboard-filter-indicators-container {
position: absolute;
z-index: 10;
Copy link
Member

Choose a reason for hiding this comment

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

got it, we should probably make an issue to manage the z-indexes better at some point. this is fine for now though

@mistercrunch
Copy link
Member

I can't wait to see this through. Hats off to everyone involved!
hats-off-gif-9

@tvorogme
Copy link

tvorogme commented Aug 13, 2019

Any updates? Very cool PR....

@etr2460
Copy link
Member

etr2460 commented Aug 13, 2019

This will probably get merged today!

@tvorogme
Copy link

YAY!!!!!

@graceguo-supercat graceguo-supercat merged commit 40776bd into apache:master Aug 22, 2019
@graceguo-supercat graceguo-supercat deleted the gg-FilterIndicator branch September 21, 2019 08:09
@mistercrunch mistercrunch added the 🏷️ bot A label used by `supersetbot` to keep track of which PR where auto-tagged with release labels label Feb 28, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🏷️ bot A label used by `supersetbot` to keep track of which PR where auto-tagged with release labels size/XXL 🚢 0.35.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants