feat(dashboards): Filter status indicators (#10936)

* Initial commit of new filters badge.

* refactor applied/rejected filters code

* finished filter indicators

* filter badge tested

* unnecessary imports

* formatting and types

* fixes

* license

* code quality tweaks

* state management for showing focused filter scope

* clean up filter key extraction code

* remove unnecessary styles

* temp css to demonstrate highlighting

* fix focused filter logic

* no more color badges

* new toys for highlighting dash components (#11144)

* tweak style for the filter chart when filter is focused

* style: Filters p0 css2 (#11151)

* nixing background tweak

* src paths

* another quick theme color

* src paths, adjusting pill icon color, changing icons, showing applied/busted counts

* linting stuff

* fixing and tweaking tests

* show filter indicator when filters are not active

* chart title bar cleanup

* open the right panel when popover opens

* unused import

* fix EditableTitle tests

* margin on dashboard header

* show the chart dropdown menu

* fix blur filter breaking dropdowns

* style tweak - no pointer events when irrelevant charts are blurred

* fix box shadow on filter highlight

* it's an array

* attempt fixing e2e

* style: filters p0 icon churn (#11215)

* new filters icon

* icon styling

* bigger icons in list views

* better sizing of table actions and favStars

* more icon sizing...

* fixing more button size jankiness

* linting

* Filters performance (#11255)

* fixing time filter "ok" button

* making unset filter menu collapsible

* sort alphabetically

* fix highlighting when removing items

* try a flex layout (for browser render perf)

* more specific transitioning

* temp: comment out some code as a test

* temp: comment out more code

* temp: remove possibly expensive computations from ChartHolder

* Revert "temp: comment out some code as a test"

This reverts commit 309b880e90.

* Revert "temp: comment out more code"

This reverts commit 64c88b2cba.

* Revert "temp: remove possibly expensive computations from ChartHolder"

This reverts commit 37ce0214f0.

* experiment: upgrade react-select to v3

* Revert "experiment: upgrade react-select to v3"

This reverts commit c3972ba486.

* fix the damn problem

* remove code used for testing purposes

* awful hack to avoid adding a class to a container

* approaching infinity... and not beyond!

* fix ref forwarding

* add theme to tests as necessary

* fix(extra-filters): add logic for identifying applied extra filters (#11325)

* fix: use dashboard id for stable cache key (#11293)

* fix: button translations missing (#11187)

* button translations missing

* blank space before text

* feat: update time_compare description and choices (#11294)

* feat: update time_compare description and choices

* Update sections.jsx

* fix(extra-filters): add logic for identifying applied extra filters

* lint

Co-authored-by: Jesse Yang <jesse.yang@airbnb.com>
Co-authored-by: rubenSastre <ruben.sastre@decathlon.com>
Co-authored-by: Erik Ritter <erik.ritter@airbnb.com>

* address design feedback

* slight tweak to panel logic, keep panels open that user has opened

* rearrange code to be more graceful

* fix: bump superset-ui/core (#11385)

* use is_dttm instead of is_temporal

* types, names

* only show unset filter panel if there are unset filters

* fix highlighting the filter control

* fix filterbox layout

* translations

* fix cypress

* actually add the test attribute

* Update superset-frontend/src/dashboard/components/DashboardBuilder.jsx

Co-authored-by: Evan Rusackas <evan@preset.io>

* Update superset-frontend/src/dashboard/components/DashboardBuilder.jsx

Co-authored-by: Evan Rusackas <evan@preset.io>

* formatting

* add link comment to hack

* Update superset-frontend/src/dashboard/components/gridComponents/ChartHolder.jsx

Co-authored-by: Evan Rusackas <evan@preset.io>

* stop importing lodash

* Update superset-frontend/src/dashboard/components/gridComponents/ChartHolder.jsx

Co-authored-by: Evan Rusackas <evan@preset.io>

* Update superset-frontend/src/dashboard/components/FiltersBadge/Styles.tsx

Co-authored-by: Evan Rusackas <evan@preset.io>

* Update superset-frontend/src/dashboard/components/FiltersBadge/Styles.tsx

Co-authored-by: Evan Rusackas <evan@preset.io>

* Update superset-frontend/src/dashboard/components/FiltersBadge/Styles.tsx

Co-authored-by: Evan Rusackas <evan@preset.io>

* Update superset-frontend/src/dashboard/components/FiltersBadge/Styles.tsx

Co-authored-by: Evan Rusackas <evan@preset.io>

* skip broken test

* Update superset-frontend/src/dashboard/components/FiltersBadge/Styles.tsx

Co-authored-by: Evan Rusackas <evan@preset.io>

* Update superset-frontend/src/dashboard/components/FiltersBadge/Styles.tsx

Co-authored-by: Evan Rusackas <evan@preset.io>

* adjust colors of titles

* linting

* no indicators when chart is loading

* support all time fields

* fix lock file

Co-authored-by: Natalie Ruhe <natalie@preset.io>
Co-authored-by: Evan Rusackas <evan@preset.io>
Co-authored-by: Ville Brofeldt <33317356+villebro@users.noreply.github.com>
Co-authored-by: Jesse Yang <jesse.yang@airbnb.com>
Co-authored-by: rubenSastre <ruben.sastre@decathlon.com>
Co-authored-by: Erik Ritter <erik.ritter@airbnb.com>
Co-authored-by: Ville Brofeldt <ville.v.brofeldt@gmail.com>
This commit is contained in:
David Aaron Suddjian
2020-10-28 15:46:24 -07:00
committed by GitHub
parent e9dba18466
commit 18658f45be
77 changed files with 1395 additions and 1494 deletions

View File

@@ -1,57 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { shallow } from 'enzyme';
import FilterIndicatorGroup from 'src/dashboard/components/FilterIndicatorGroup';
import FilterBadgeIcon from 'src/components/FilterBadgeIcon';
import { dashboardFilters } from '../fixtures/mockDashboardFilters';
import { filterId, column } from '../fixtures/mockSliceEntities';
describe('FilterIndicatorGroup', () => {
const mockedProps = {
indicators: [
{
...dashboardFilters[filterId],
colorCode: 'badge-1',
name: column,
values: ['a', 'b', 'c'],
isFilterFieldActive: true,
chartId: 1,
componentId: 'foo',
directPathToFilter: ['foo'],
isDateFilter: false,
isInstantFilter: false,
label: 'foo',
},
],
setDirectPathToChild: () => {},
};
function setup(overrideProps) {
return shallow(
<FilterIndicatorGroup {...mockedProps} {...overrideProps} />,
);
}
it('should show indicator group with badge', () => {
const wrapper = setup();
expect(wrapper.find(FilterBadgeIcon)).toExist();
});
});

View File

@@ -1,43 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { shallow } from 'enzyme';
import FilterIndicatorTooltip from 'src/dashboard/components/FilterIndicatorTooltip';
describe('FilterIndicatorTooltip', () => {
const label = 'region';
const mockedProps = {
colorCode: 'badge-1',
label,
values: [],
clickIconHandler: jest.fn(),
};
function setup(overrideProps) {
return shallow(
<FilterIndicatorTooltip {...mockedProps} {...overrideProps} />,
);
}
it('should show label', () => {
const wrapper = setup();
expect(wrapper.find(`[htmlFor="filter-tooltip-${label}"]`)).toExist();
});
});

View File

@@ -1,63 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { shallow } from 'enzyme';
import FilterIndicator from 'src/dashboard/components/FilterIndicator';
import FilterBadgeIcon from 'src/components/FilterBadgeIcon';
import { dashboardFilters } from '../fixtures/mockDashboardFilters';
import { filterId, column } from '../fixtures/mockSliceEntities';
describe('FilterIndicator', () => {
const mockedProps = {
indicator: {
...dashboardFilters[filterId],
colorCode: 'badge-1',
name: column,
label: column,
values: ['a', 'b', 'c'],
chartId: 1,
componentId: 'foo',
isDateFilter: false,
isFilterFieldActive: true,
isInstantFilter: false,
},
setDirectPathToChild: jest.fn(),
};
function setup(overrideProps) {
return shallow(<FilterIndicator {...mockedProps} {...overrideProps} />);
}
it('should show indicator with badge', () => {
const wrapper = setup();
expect(wrapper.find(FilterBadgeIcon)).toExist();
});
it('should call setDirectPathToChild prop', () => {
const wrapper = setup();
const badge = wrapper.find('.filter-indicator');
expect(badge).toHaveLength(1);
badge.simulate('click');
expect(mockedProps.setDirectPathToChild).toHaveBeenCalledWith(
dashboardFilters[filterId].directPathToFilter,
);
});
});

View File

@@ -1,107 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { shallow } from 'enzyme';
import FilterIndicatorsContainer from 'src/dashboard/components/FilterIndicatorsContainer';
import FilterIndicator from 'src/dashboard/components/FilterIndicator';
import * as colorMap from 'src/dashboard/util/dashboardFiltersColorMap';
import { buildActiveFilters } from 'src/dashboard/util/activeDashboardFilters';
import { getDashboardFilterKey } from 'src/dashboard/util/getDashboardFilterKey';
import { DASHBOARD_ROOT_ID } from 'src/dashboard/util/constants';
import { dashboardFilters } from '../fixtures/mockDashboardFilters';
import { sliceId as chartId } from '../fixtures/mockChartQueries';
import { filterId, column } from '../fixtures/mockSliceEntities';
import { dashboardWithFilter } from '../fixtures/mockDashboardLayout';
describe('FilterIndicatorsContainer', () => {
const mockedProps = {
dashboardFilters,
chartId,
chartStatus: 'success',
setDirectPathToChild: () => {},
filterFieldOnFocus: {},
};
colorMap.getFilterColorMap = jest.fn(() => ({
[getDashboardFilterKey({ chartId, column })]: 'badge-1',
}));
buildActiveFilters({
dashboardFilters,
components: dashboardWithFilter,
});
function setup(overrideProps) {
return shallow(
<FilterIndicatorsContainer {...mockedProps} {...overrideProps} />,
);
}
it('should not show indicator when chart is loading', () => {
const wrapper = setup({ chartStatus: 'loading' });
expect(wrapper.find(FilterIndicator)).not.toExist();
});
it('should not show indicator for filter_box itself', () => {
const wrapper = setup({ chartId: filterId });
expect(wrapper.find(FilterIndicator)).not.toExist();
});
it('should show indicator', () => {
const wrapper = setup();
expect(wrapper.find(FilterIndicator)).toExist();
});
it('should not show indicator when chart is immune', () => {
const overwriteDashboardFilters = {
...dashboardFilters,
[filterId]: {
...dashboardFilters[filterId],
scopes: {
region: {
scope: [DASHBOARD_ROOT_ID],
immune: [chartId],
},
},
},
};
const wrapper = setup({ dashboardFilters: overwriteDashboardFilters });
expect(wrapper.find(FilterIndicator)).not.toExist();
});
it('should show single number type value', () => {
const overwriteDashboardFilters = {
...dashboardFilters,
[filterId]: {
...dashboardFilters[filterId],
columns: {
testField: 0,
},
},
};
const wrapper = setup({ dashboardFilters: overwriteDashboardFilters });
expect(wrapper.find(FilterIndicator)).toExist();
const indicatorProps = wrapper.find(FilterIndicator).first().props()
.indicator;
expect(indicatorProps.label).toEqual('testField');
expect(indicatorProps.values).toEqual([0]);
});
});

View File

@@ -1,68 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { Overlay, Tooltip } from 'react-bootstrap';
import FilterTooltipWrapper from 'src/dashboard/components/FilterTooltipWrapper';
import FilterIndicatorTooltip from 'src/dashboard/components/FilterIndicatorTooltip';
describe('FilterTooltipWrapper', () => {
const mockedProps = {
tooltip: (
<FilterIndicatorTooltip
label="region"
values={['a', 'b', 'c']}
clickIconHandler={jest.fn()}
/>
),
};
function setup() {
return shallow(
<FilterTooltipWrapper {...mockedProps}>
<div className="badge-1" />
</FilterTooltipWrapper>,
);
}
it('should contain Overlay and Tooltip', () => {
const wrapper = setup();
expect(wrapper.find(Overlay)).toExist();
expect(wrapper.find(Tooltip)).toExist();
});
it('should show tooltip on hover', async () => {
const wrapper = setup();
wrapper.instance().isHover = true;
wrapper.find('.indicator-container').simulate('mouseover');
await new Promise(r => setTimeout(r, 101));
expect(wrapper.state('show')).toBe(true);
});
it('should hide tooltip on hover', async () => {
const wrapper = setup();
wrapper.instance().isHover = false;
wrapper.find('.indicator-container').simulate('mouseout');
await new Promise(r => setTimeout(r, 101));
expect(wrapper.state('show')).toBe(false);
});
});

View File

@@ -0,0 +1,112 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { supersetTheme } from '@superset-ui/core';
import * as SupersetUI from '@superset-ui/core';
import { CHART_UPDATE_SUCCEEDED } from 'src/chart/chartAction';
import { buildActiveFilters } from 'src/dashboard/util/activeDashboardFilters';
import FiltersBadge from 'src/dashboard/containers/FiltersBadge';
import { getMockStoreWithFilters } from '../fixtures/mockStore';
import { sliceId } from '../fixtures/mockChartQueries';
import { dashboardFilters } from '../fixtures/mockDashboardFilters';
import { dashboardWithFilter } from '../fixtures/mockDashboardLayout';
describe('FiltersBadge', () => {
// there's this bizarre "active filters" thing
// that doesn't actually use any kind of state management.
// Have to set variables in there.
buildActiveFilters({
dashboardFilters,
components: dashboardWithFilter,
});
beforeEach(() => {
// shallow rendering in enzyme doesn't propagate contexts correctly,
// so we have to mock the hook.
// See https://medium.com/7shifts-engineering-blog/testing-usecontext-react-hook-with-enzyme-shallow-da062140fc83
jest.spyOn(SupersetUI, 'useTheme').mockImplementation(() => supersetTheme);
});
it("doesn't show number when there are no active filters", () => {
const store = getMockStoreWithFilters();
// start with basic dashboard state, dispatch an event to simulate query completion
store.dispatch({
type: CHART_UPDATE_SUCCEEDED,
key: sliceId,
queryResponse: {
status: 'success',
applied_filters: [],
rejected_filters: [],
},
dashboardFilters,
});
const wrapper = shallow(<FiltersBadge {...{ store }} chartId={sliceId} />);
expect(
wrapper.dive().find('[data-test="applied-filter-count"]'),
).not.toExist();
});
it('shows the indicator when filters have been applied', () => {
const store = getMockStoreWithFilters();
// start with basic dashboard state, dispatch an event to simulate query completion
store.dispatch({
type: CHART_UPDATE_SUCCEEDED,
key: sliceId,
queryResponse: {
status: 'success',
applied_filters: [{ column: 'region' }],
rejected_filters: [],
},
dashboardFilters,
});
const wrapper = shallow(<FiltersBadge {...{ store }} chartId={sliceId} />);
expect(wrapper.dive().find('DetailsPanelPopover')).toExist();
expect(
wrapper.dive().find('[data-test="applied-filter-count"]'),
).toHaveText('1');
expect(wrapper.dive().find('WarningFilled')).not.toExist();
});
it("shows a warning when there's a rejected filter", () => {
const store = getMockStoreWithFilters();
// start with basic dashboard state, dispatch an event to simulate query completion
store.dispatch({
type: CHART_UPDATE_SUCCEEDED,
key: sliceId,
queryResponse: {
status: 'success',
applied_filters: [],
rejected_filters: [{ column: 'region', reason: 'not_in_datasource' }],
},
dashboardFilters,
});
const wrapper = shallow(<FiltersBadge {...{ store }} chartId={sliceId} />);
expect(wrapper.dive().find('DetailsPanelPopover')).toExist();
expect(
wrapper.dive().find('[data-test="applied-filter-count"]'),
).toHaveText('0');
expect(
wrapper.dive().find('[data-test="incompatible-filter-count"]'),
).toHaveText('1');
// to look at the shape of the wrapper use:
// console.log(wrapper.dive().debug())
expect(wrapper.dive().find('Icon[name="alert-solid"]')).toExist();
});
});

View File

@@ -20,6 +20,7 @@ import { Provider } from 'react-redux';
import React from 'react';
import { mount } from 'enzyme';
import sinon from 'sinon';
import { supersetTheme, ThemeProvider } from '@superset-ui/core';
import Chart from 'src/dashboard/containers/Chart';
import ChartHolder from 'src/dashboard/components/gridComponents/ChartHolder';
@@ -61,6 +62,10 @@ describe('ChartHolder', () => {
<ChartHolder {...props} {...overrideProps} />
</WithDragDropContext>
</Provider>,
{
wrappingComponent: ThemeProvider,
wrappingComponentProps: { theme: supersetTheme },
},
);
return wrapper;
}

View File

@@ -20,6 +20,7 @@ import { Provider } from 'react-redux';
import React from 'react';
import { mount } from 'enzyme';
import sinon from 'sinon';
import { supersetTheme, ThemeProvider } from '@superset-ui/core';
import BackgroundStyleDropdown from 'src/dashboard/components/menu/BackgroundStyleDropdown';
import Column from 'src/dashboard/components/gridComponents/Column';
@@ -69,6 +70,10 @@ describe('Column', () => {
<Column {...props} {...overrideProps} />
</WithDragDropContext>
</Provider>,
{
wrappingComponent: ThemeProvider,
wrappingComponentProps: { theme: supersetTheme },
},
);
return wrapper;
}

View File

@@ -86,7 +86,9 @@ describe('Header', () => {
it('should render an EditableTitle with meta.text', () => {
const wrapper = setup();
expect(wrapper.find(EditableTitle)).toExist();
expect(wrapper.find('input').prop('value')).toBe(props.component.meta.text);
expect(wrapper.find('.editable-title')).toHaveText(
props.component.meta.text,
);
});
it('should call updateComponents when EditableTitle changes', () => {

View File

@@ -30,6 +30,7 @@ import IconButton from 'src/dashboard/components/IconButton';
import Row from 'src/dashboard/components/gridComponents/Row';
import WithPopoverMenu from 'src/dashboard/components/menu/WithPopoverMenu';
import { DASHBOARD_GRID_ID } from 'src/dashboard/util/constants';
import { supersetTheme, ThemeProvider } from '@superset-ui/core';
import { mockStore } from '../../fixtures/mockStore';
import { dashboardLayout as mockLayout } from '../../fixtures/mockDashboardLayout';
@@ -65,6 +66,10 @@ describe('Row', () => {
<Row {...props} {...overrideProps} />
</WithDragDropContext>
</Provider>,
{
wrappingComponent: ThemeProvider,
wrappingComponentProps: { theme: supersetTheme },
},
);
return wrapper;
}

View File

@@ -81,7 +81,9 @@ describe('Tabs', () => {
const wrapper = setup();
const title = wrapper.find(EditableTitle);
expect(title).toHaveLength(1);
expect(title.find('input').prop('value')).toBe(props.component.meta.text);
expect(title.find('.editable-title')).toHaveText(
props.component.meta.text,
);
});
it('should call updateComponents when EditableTitle changes', () => {

View File

@@ -21,6 +21,7 @@ import React from 'react';
import { mount, shallow } from 'enzyme';
import sinon from 'sinon';
import { Tabs as BootstrapTabs, Tab as BootstrapTab } from 'react-bootstrap';
import { supersetTheme, ThemeProvider } from '@superset-ui/core';
import DashboardComponent from 'src/dashboard/containers/DashboardComponent';
import DeleteComponentButton from 'src/dashboard/components/DeleteComponentButton';
@@ -64,6 +65,10 @@ describe('Tabs', () => {
<Tabs {...props} {...overrideProps} />
</WithDragDropContext>
</Provider>,
{
wrappingComponent: ThemeProvider,
wrappingComponentProps: { theme: supersetTheme },
},
);
return wrapper;
}

View File

@@ -27,5 +27,5 @@ export default {
isStarred: true,
isPublished: true,
css: '',
focusedFilterField: [],
focusedFilterField: null,
};

View File

@@ -23,19 +23,54 @@ import rootReducer from 'src/dashboard/reducers/index';
import mockState from './mockState';
import { dashboardLayoutWithTabs } from './mockDashboardLayout';
import { sliceId } from './mockChartQueries';
import { dashboardFilters } from './mockDashboardFilters';
export const mockStore = createStore(
rootReducer,
mockState,
compose(applyMiddleware(thunk)),
);
export const getMockStore = () =>
createStore(rootReducer, mockState, compose(applyMiddleware(thunk)));
export const mockStoreWithTabs = createStore(
rootReducer,
{
export const mockStore = getMockStore();
export const getMockStoreWithTabs = () =>
createStore(
rootReducer,
{
...mockState,
dashboardLayout: dashboardLayoutWithTabs,
dashboardFilters: {},
},
compose(applyMiddleware(thunk)),
);
export const mockStoreWithTabs = getMockStoreWithTabs();
export const sliceIdWithAppliedFilter = sliceId + 1;
export const sliceIdWithRejectedFilter = sliceId + 2;
// has one chart with a filter that has been applied,
// one chart with a filter that has been rejected,
// and one chart with no filters set.
export const getMockStoreWithFilters = () =>
createStore(rootReducer, {
...mockState,
dashboardLayout: dashboardLayoutWithTabs,
dashboardFilters: {},
},
compose(applyMiddleware(thunk)),
);
dashboardFilters,
charts: {
...mockState.charts,
[sliceIdWithAppliedFilter]: {
...mockState.charts[sliceId],
queryResponse: {
status: 'success',
applied_filters: [{ column: 'region' }],
rejected_filters: [],
},
},
[sliceIdWithRejectedFilter]: {
...mockState.charts[sliceId],
queryResponse: {
status: 'success',
applied_filters: [],
rejected_filters: [{ column: 'region', reason: 'not_in_datasource' }],
},
},
},
});

View File

@@ -27,6 +27,7 @@ import {
SET_UNSAVED_CHANGES,
TOGGLE_EXPAND_SLICE,
TOGGLE_FAVE_STAR,
UNSET_FOCUSED_FILTER_FIELD,
} from 'src/dashboard/actions/dashboardState';
import dashboardStateReducer from 'src/dashboard/reducers/dashboardState';
@@ -145,21 +146,34 @@ describe('dashboardState reducer', () => {
).toBeGreaterThanOrEqual(lastModifiedTime);
});
it('should clear focused filter field', () => {
it('should clear the focused filter field', () => {
const initState = {
focusedFilterField: {
chartId: 1,
column: 'column_1',
},
};
const cleared = dashboardStateReducer(initState, {
type: UNSET_FOCUSED_FILTER_FIELD,
chartId: 1,
column: 'column_1',
});
expect(cleared.focusedFilterField).toBeNull();
});
it('should only clear focused filter when the fields match', () => {
// dashboard only has 1 focused filter field at a time,
// but when user switch different filter boxes,
// browser didn't always fire onBlur and onFocus events in order.
// so in redux state focusedFilterField prop is a queue,
// we always shift first element in the queue
// init state: has 1 focus field
const initState = {
focusedFilterField: [
{
chartId: 1,
column: 'column_1',
},
],
focusedFilterField: {
chartId: 1,
column: 'column_1',
},
};
// when user switching filter,
// browser focus on new filter first,
@@ -170,10 +184,12 @@ describe('dashboardState reducer', () => {
column: 'column_2',
});
const step2 = dashboardStateReducer(step1, {
type: SET_FOCUSED_FILTER_FIELD,
type: UNSET_FOCUSED_FILTER_FIELD,
chartId: 1,
column: 'column_1',
});
expect(step2.focusedFilterField.slice(-1).pop()).toEqual({
expect(step2.focusedFilterField).toEqual({
chartId: 2,
column: 'column_2',
});

View File

@@ -46,16 +46,15 @@ describe('EditableTitle', () => {
expect(titleElement.props().value).toBe('my title');
expect(titleElement.props().type).toBe('button');
});
it('should not render an input if it is not editable', () => {
expect(notEditableWrapper.find('input')).not.toExist();
});
describe('should handle click', () => {
it('should change title', () => {
editableWrapper.find('input').simulate('click');
expect(editableWrapper.find('input').props().type).toBe('text');
});
it('should not change title', () => {
notEditableWrapper.find('input').simulate('click');
expect(notEditableWrapper.find('input').props().type).toBe('button');
});
});
describe('should handle change', () => {
@@ -66,10 +65,6 @@ describe('EditableTitle', () => {
editableWrapper.find('input').simulate('change', mockEvent);
expect(editableWrapper.find('input').props().value).toBe('new title');
});
it('should not change title', () => {
notEditableWrapper.find('input').simulate('change', mockEvent);
expect(editableWrapper.find('input').props().value).toBe('my title');
});
});
describe('should handle blur', () => {