diff --git a/superset/assets/spec/javascripts/components/AnchorLink_spec.jsx b/superset/assets/spec/javascripts/components/AnchorLink_spec.jsx
index 42811094592d..8a5e803ac32a 100644
--- a/superset/assets/spec/javascripts/components/AnchorLink_spec.jsx
+++ b/superset/assets/spec/javascripts/components/AnchorLink_spec.jsx
@@ -28,6 +28,19 @@ describe('AnchorLink', () => {
anchorLinkId: 'CHART-123',
};
+ beforeEach(() => {
+ global.window = Object.create(window);
+ Object.defineProperty(window, 'location', {
+ value: {
+ hash: '#' + props.anchorLinkId,
+ },
+ });
+ });
+
+ afterEach(() => {
+ delete global.window.location.value;
+ });
+
it('should scroll the AnchorLink into view upon mount', () => {
const callback = sinon.spy();
const clock = sinon.useFakeTimers();
@@ -35,11 +48,7 @@ describe('AnchorLink', () => {
scrollIntoView: callback,
});
- const wrapper = shallow();
- wrapper.instance().getLocationHash = () => (props.anchorLinkId);
- wrapper.update();
-
- wrapper.instance().componentDidMount();
+ shallow();
clock.tick(2000);
expect(callback.callCount).toEqual(1);
stub.restore();
diff --git a/superset/assets/spec/javascripts/dashboard/actions/dashboardState_spec.js b/superset/assets/spec/javascripts/dashboard/actions/dashboardState_spec.js
index 8ba499e32107..c2263c2df321 100644
--- a/superset/assets/spec/javascripts/dashboard/actions/dashboardState_spec.js
+++ b/superset/assets/spec/javascripts/dashboard/actions/dashboardState_spec.js
@@ -19,17 +19,29 @@
import sinon from 'sinon';
import { SupersetClient } from '@superset-ui/connection';
-import { saveDashboardRequest } from '../../../../src/dashboard/actions/dashboardState';
+import {
+ removeSliceFromDashboard,
+ saveDashboardRequest,
+} from '../../../../src/dashboard/actions/dashboardState';
+import { REMOVE_FILTER } from '../../../../src/dashboard/actions/dashboardFilters';
import { UPDATE_COMPONENTS_PARENTS_LIST } from '../../../../src/dashboard/actions/dashboardLayout';
+import {
+ filterId,
+ sliceEntitiesForDashboard as sliceEntities,
+} from '../fixtures/mockSliceEntities';
+import { emptyFilters } from '../fixtures/mockDashboardFilters';
import mockDashboardData from '../fixtures/mockDashboardData';
import { DASHBOARD_GRID_ID } from '../../../../src/dashboard/util/constants';
describe('dashboardState actions', () => {
const mockState = {
dashboardState: {
+ sliceIds: [filterId],
hasUnsavedChanges: true,
},
dashboardInfo: {},
+ sliceEntities,
+ dashboardFilters: emptyFilters,
dashboardLayout: {
past: [],
present: mockDashboardData.positions,
@@ -97,4 +109,14 @@ describe('dashboardState actions', () => {
);
});
});
+
+ it('should dispatch removeFilter if a removed slice is a filter_box', () => {
+ const { getState, dispatch } = setup(mockState);
+ const thunk = removeSliceFromDashboard(filterId);
+ thunk(dispatch, getState);
+
+ const removeFilter = dispatch.getCall(0).args[0];
+ removeFilter(dispatch, getState);
+ expect(dispatch.getCall(3).args[0].type).toBe(REMOVE_FILTER);
+ });
});
diff --git a/superset/assets/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx
index 16dc33dea052..ef81bd47c70c 100644
--- a/superset/assets/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx
+++ b/superset/assets/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx
@@ -67,7 +67,7 @@ describe('DashboardBuilder', () => {
setColorSchemeAndUnsavedChanges() {},
colorScheme: undefined,
handleComponentDrop() {},
- toggleBuilderPane() {},
+ setDirectPathToChild: sinon.spy(),
};
function setup(overrideProps, useProvider = false, store = mockStore) {
@@ -171,7 +171,7 @@ describe('DashboardBuilder', () => {
expect(wrapper.find(BuilderComponentPane)).toHaveLength(1);
});
- it('should change tabs if a top-level Tab is clicked', () => {
+ it('should change redux state if a top-level Tab is clicked', () => {
const wrapper = setup(
{ dashboardLayout: layoutWithTabs },
true,
@@ -185,6 +185,6 @@ describe('DashboardBuilder', () => {
.at(1)
.simulate('click');
- expect(wrapper.find(TabContainer).prop('activeKey')).toBe(1);
+ expect(props.setDirectPathToChild.callCount).toBe(1);
});
});
diff --git a/superset/assets/spec/javascripts/dashboard/components/Dashboard_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/Dashboard_spec.jsx
index 1f6fa6d9ae64..22ed6d6169a7 100644
--- a/superset/assets/spec/javascripts/dashboard/components/Dashboard_spec.jsx
+++ b/superset/assets/spec/javascripts/dashboard/components/Dashboard_spec.jsx
@@ -46,6 +46,7 @@ describe('Dashboard', () => {
dashboardState,
dashboardInfo,
charts: chartQueries,
+ filters: {},
slices: sliceEntities.slices,
datasources,
layout: dashboardLayout.present,
@@ -60,27 +61,23 @@ describe('Dashboard', () => {
return wrapper;
}
+ const OVERRIDE_FILTERS = {
+ 1: { region: [] },
+ 2: { country_name: ['USA'] },
+ 3: { region: [], country_name: ['USA'] },
+ };
+
it('should render a DashboardBuilder', () => {
const wrapper = setup();
expect(wrapper.find(DashboardBuilder)).toHaveLength(1);
});
describe('refreshExcept', () => {
- const overrideDashboardState = {
- ...dashboardState,
- filters: {
- 1: { region: [] },
- 2: { country_name: ['USA'] },
- 3: { region: [], country_name: ['USA'] },
- },
- refresh: true,
- };
-
const overrideDashboardInfo = {
...dashboardInfo,
metadata: {
...dashboardInfo.metadata,
- filter_immune_slice_fields: { [chartQueries[chartId].id]: ['region'] },
+ filterImmuneSliceFields: { [chartQueries[chartId].id]: ['region'] },
},
};
@@ -108,14 +105,14 @@ describe('Dashboard', () => {
expect(spy.callCount).toBe(Object.keys(overrideCharts).length - 1);
});
- it('should not call triggerQuery for filter_immune_slices', () => {
+ it('should not call triggerQuery for filterImmuneSlices', () => {
const wrapper = setup({
charts: overrideCharts,
dashboardInfo: {
...dashboardInfo,
metadata: {
...dashboardInfo.metadata,
- filter_immune_slices: Object.keys(overrideCharts).map(id =>
+ filterImmuneSlices: Object.keys(overrideCharts).map(id =>
Number(id),
),
},
@@ -127,9 +124,9 @@ describe('Dashboard', () => {
expect(spy.callCount).toBe(0);
});
- it('should not call triggerQuery for filter_immune_slice_fields', () => {
+ it('should not call triggerQuery for filterImmuneSliceFields', () => {
const wrapper = setup({
- dashboardState: overrideDashboardState,
+ filters: OVERRIDE_FILTERS,
dashboardInfo: overrideDashboardInfo,
});
const spy = sinon.spy(props.actions, 'triggerQuery');
@@ -140,7 +137,7 @@ describe('Dashboard', () => {
it('should call triggerQuery if filter has more filter-able fields', () => {
const wrapper = setup({
- dashboardState: overrideDashboardState,
+ filters: OVERRIDE_FILTERS,
dashboardInfo: overrideDashboardInfo,
});
const spy = sinon.spy(props.actions, 'triggerQuery');
@@ -187,117 +184,78 @@ describe('Dashboard', () => {
});
describe('componentDidUpdate', () => {
- const overrideDashboardState = {
- ...dashboardState,
- filters: {
- 1: { region: [] },
- 2: { country_name: ['USA'] },
- },
- refresh: true,
- };
+ let wrapper;
+ let prevProps;
+ let refreshExceptSpy;
- it('should not call refresh when there is no change', () => {
- const wrapper = setup({ dashboardState: overrideDashboardState });
- const refreshExceptSpy = sinon.spy(wrapper.instance(), 'refreshExcept');
- const prevProps = wrapper.instance().props;
+ beforeEach(() => {
+ wrapper = setup({ filters: OVERRIDE_FILTERS });
+ wrapper.instance().appliedFilters = OVERRIDE_FILTERS;
+ prevProps = wrapper.instance().props;
+ refreshExceptSpy = sinon.spy(wrapper.instance(), 'refreshExcept');
+ });
+
+ afterEach(() => {
+ refreshExceptSpy.restore();
+ });
+
+ it('should not call refresh when is editMode', () => {
wrapper.setProps({
dashboardState: {
- ...overrideDashboardState,
+ ...dashboardState,
+ editMode: true,
},
});
wrapper.instance().componentDidUpdate(prevProps);
- refreshExceptSpy.restore();
expect(refreshExceptSpy.callCount).toBe(0);
});
- it('should call refresh if a filter is added', () => {
- const wrapper = setup({ dashboardState: overrideDashboardState });
- const refreshExceptSpy = sinon.spy(wrapper.instance(), 'refreshExcept');
+ it('should not call refresh when there is no change', () => {
wrapper.setProps({
- dashboardState: {
- ...overrideDashboardState,
- filters: {
- ...overrideDashboardState.filters,
- 3: { another_filter: ['please'] },
- },
- },
+ filters: OVERRIDE_FILTERS,
});
- refreshExceptSpy.restore();
- expect(refreshExceptSpy.callCount).toBe(1);
+ wrapper.instance().componentDidUpdate(prevProps);
+ expect(refreshExceptSpy.callCount).toBe(0);
+ expect(wrapper.instance().appliedFilters).toBe(OVERRIDE_FILTERS);
});
- it('should call refresh if a filter is removed', () => {
- const wrapper = setup({ dashboardState: overrideDashboardState });
- const refreshExceptSpy = sinon.spy(wrapper.instance(), 'refreshExcept');
+ it('should call refresh if a filter is added', () => {
+ const newFilter = {
+ gender: ['boy', 'girl'],
+ };
wrapper.setProps({
- dashboardState: {
- ...overrideDashboardState,
- filters: {},
+ filters: {
+ ...OVERRIDE_FILTERS,
+ ...newFilter,
},
});
- refreshExceptSpy.restore();
expect(refreshExceptSpy.callCount).toBe(1);
+ expect(wrapper.instance().appliedFilters).toEqual({
+ ...OVERRIDE_FILTERS,
+ ...newFilter,
+ });
});
- it('should call refresh if a filter is changed', () => {
- const wrapper = setup({ dashboardState: overrideDashboardState });
- const refreshExceptSpy = sinon.spy(wrapper.instance(), 'refreshExcept');
+ it('should call refresh if a filter is removed', () => {
wrapper.setProps({
- dashboardState: {
- ...overrideDashboardState,
- filters: {
- ...overrideDashboardState.filters,
- 2: { country_name: ['Canada'] },
- },
- },
+ filters: {},
});
- refreshExceptSpy.restore();
expect(refreshExceptSpy.callCount).toBe(1);
+ expect(wrapper.instance().appliedFilters).toEqual({});
});
- it('should not call refresh if filters change and refresh is false', () => {
- const wrapper = setup({ dashboardState: overrideDashboardState });
- const refreshExceptSpy = sinon.spy(wrapper.instance(), 'refreshExcept');
+ it('should call refresh if a filter is changed', () => {
wrapper.setProps({
- dashboardState: {
- ...overrideDashboardState,
- filters: {
- ...overrideDashboardState.filters,
- 2: { country_name: ['Canada'] },
- },
- refresh: false,
- },
- });
- refreshExceptSpy.restore();
- expect(refreshExceptSpy.callCount).toBe(0);
- });
-
- it('should not refresh filter_immune_slices', () => {
- const wrapper = setup({
- dashboardState: overrideDashboardState,
- dashboardInfo: {
- ...dashboardInfo,
- metadata: {
- ...dashboardInfo.metadata,
- filter_immune_slices: [chartId],
- },
+ filters: {
+ ...OVERRIDE_FILTERS,
+ region: ['Canada'],
},
});
- const refreshExceptSpy = sinon.spy(wrapper.instance(), 'refreshExcept');
- const prevProps = wrapper.instance().props;
- wrapper.setProps({
- dashboardState: {
- ...overrideDashboardState,
- filters: {
- ...overrideDashboardState.filters,
- 2: { country_name: ['Canada'] },
- },
- refresh: false,
- },
+ expect(refreshExceptSpy.callCount).toBe(1);
+ expect(wrapper.instance().appliedFilters).toEqual({
+ ...OVERRIDE_FILTERS,
+ region: ['Canada'],
});
- wrapper.instance().componentDidUpdate(prevProps);
- refreshExceptSpy.restore();
- expect(refreshExceptSpy.callCount).toBe(0);
});
});
});
diff --git a/superset/assets/spec/javascripts/dashboard/components/FilterIndicatorGroup_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/FilterIndicatorGroup_spec.jsx
new file mode 100644
index 000000000000..e54a698cd1b7
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/components/FilterIndicatorGroup_spec.jsx
@@ -0,0 +1,50 @@
+/**
+ * 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 { dashboardFilters } from '../fixtures/mockDashboardFilters';
+import { filterId, column } from '../fixtures/mockSliceEntities';
+import FilterIndicatorGroup from '../../../../src/dashboard/components/FilterIndicatorGroup';
+import FilterBadgeIcon from '../../../../src/components/FilterBadgeIcon';
+
+describe('FilterIndicatorGroup', () => {
+ const mockedProps = {
+ indicators: [
+ {
+ ...dashboardFilters[filterId],
+ colorCode: 'badge-1',
+ name: column,
+ values: ['a', 'b', 'c'],
+ },
+ ],
+ setDirectPathToChild: () => {},
+ };
+
+ function setup(overrideProps) {
+ return shallow(
+ ,
+ );
+ }
+
+ it('should show indicator group with badge', () => {
+ const wrapper = setup();
+ expect(wrapper.find(FilterBadgeIcon)).toHaveLength(1);
+ });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/components/FilterIndicatorTooltip_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/FilterIndicatorTooltip_spec.jsx
new file mode 100644
index 000000000000..546a324619ae
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/components/FilterIndicatorTooltip_spec.jsx
@@ -0,0 +1,43 @@
+/**
+ * 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(
+ ,
+ );
+ }
+
+ it('should show label', () => {
+ const wrapper = setup();
+ expect(wrapper.find(`[htmlFor="filter-tooltip-${label}"]`)).toHaveLength(1);
+ });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/components/FilterIndicator_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/FilterIndicator_spec.jsx
new file mode 100644
index 000000000000..629828347e47
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/components/FilterIndicator_spec.jsx
@@ -0,0 +1,58 @@
+/**
+ * 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 { dashboardFilters } from '../fixtures/mockDashboardFilters';
+import { filterId, column } from '../fixtures/mockSliceEntities';
+import FilterIndicator from '../../../../src/dashboard/components/FilterIndicator';
+import FilterBadgeIcon from '../../../../src/components/FilterBadgeIcon';
+
+describe('FilterIndicator', () => {
+ const mockedProps = {
+ indicator: {
+ ...dashboardFilters[filterId],
+ colorCode: 'badge-1',
+ name: column,
+ label: column,
+ values: ['a', 'b', 'c'],
+ },
+ setDirectPathToChild: jest.fn(),
+ };
+
+ function setup(overrideProps) {
+ return shallow();
+ }
+
+ it('should show indicator with badge', () => {
+ const wrapper = setup();
+ expect(wrapper.find(FilterBadgeIcon)).toHaveLength(1);
+ });
+
+ 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,
+ );
+ });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/components/FilterIndicatorsContainer_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/FilterIndicatorsContainer_spec.jsx
new file mode 100644
index 000000000000..0fa06abbdf5f
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/components/FilterIndicatorsContainer_spec.jsx
@@ -0,0 +1,74 @@
+/**
+ * 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 { dashboardFilters } from '../fixtures/mockDashboardFilters';
+import { filterId, column } from '../fixtures/mockSliceEntities';
+import FilterIndicatorsContainer from '../../../../src/dashboard/components/FilterIndicatorsContainer';
+import FilterIndicator from '../../../../src/dashboard/components/FilterIndicator';
+import * as colorMap from '../../../../src/dashboard/util/dashboardFiltersColorMap';
+
+describe('FilterIndicatorsContainer', () => {
+ const chartId = 1;
+ const mockedProps = {
+ dashboardFilters,
+ chartId,
+ chartStatus: 'success',
+ filterImmuneSlices: [],
+ filterImmuneSliceFields: {},
+ setDirectPathToChild: () => {},
+ };
+
+ colorMap.getFilterColorKey = jest.fn(() => 'id_column');
+ colorMap.getFilterColorMap = jest.fn(() => ({
+ id_column: 'badge-1',
+ }));
+
+ function setup(overrideProps) {
+ return shallow(
+ ,
+ );
+ }
+
+ it('should not show indicator when chart is loading', () => {
+ const wrapper = setup({ chartStatus: 'loading' });
+ expect(wrapper.find(FilterIndicator)).toHaveLength(0);
+ });
+
+ it('should not show indicator for filter_box itself', () => {
+ const wrapper = setup({ chartId: filterId });
+ expect(wrapper.find(FilterIndicator)).toHaveLength(0);
+ });
+
+ it('should not show indicator when chart is immune', () => {
+ const wrapper = setup({ filterImmuneSlices: [chartId] });
+ expect(wrapper.find(FilterIndicator)).toHaveLength(0);
+ });
+
+ it('should not show indicator when chart field is immune', () => {
+ const wrapper = setup({ filterImmuneSliceFields: { [chartId]: [column] } });
+ expect(wrapper.find(FilterIndicator)).toHaveLength(0);
+ });
+
+ it('should show indicator', () => {
+ const wrapper = setup();
+ expect(wrapper.find(FilterIndicator)).toHaveLength(1);
+ });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/components/FilterTooltipWrapper_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/FilterTooltipWrapper_spec.jsx
new file mode 100644
index 000000000000..ab5170030b09
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/components/FilterTooltipWrapper_spec.jsx
@@ -0,0 +1,70 @@
+/**
+ * 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: (
+
+ ),
+ };
+
+ function setup() {
+ return shallow(
+
+
+ ,
+ );
+ }
+
+ it('should contain Overlay and Tooltip', () => {
+ const wrapper = setup();
+ expect(wrapper.find(Overlay)).toHaveLength(1);
+ expect(wrapper.find(Tooltip)).toHaveLength(1);
+ });
+
+ it('should show tooltip on hover', () => {
+ const wrapper = setup();
+ wrapper.instance().isHover = true;
+
+ jest.useFakeTimers();
+ wrapper.find('.indicator-container').simulate('mouseover');
+ jest.runAllTimers();
+ expect(wrapper.state('show')).toBe(true);
+ });
+
+ it('should hide tooltip on hover', () => {
+ const wrapper = setup();
+ wrapper.instance().isHover = false;
+
+ jest.useFakeTimers();
+ wrapper.find('.indicator-container').simulate('mouseout');
+ jest.runAllTimers();
+ expect(wrapper.state('show')).toBe(false);
+ });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/components/Header_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/Header_spec.jsx
index 45f987414a0d..dd53e74f43fe 100644
--- a/superset/assets/spec/javascripts/dashboard/components/Header_spec.jsx
+++ b/superset/assets/spec/javascripts/dashboard/components/Header_spec.jsx
@@ -52,7 +52,6 @@ describe('Header', () => {
setEditMode: () => {},
showBuilderPane: () => {},
builderPaneType: BUILDER_PANE_TYPE.NONE,
- toggleBuilderPane: () => {},
updateCss: () => {},
hasUnsavedChanges: false,
maxUndoHistoryExceeded: false,
diff --git a/superset/assets/spec/javascripts/dashboard/components/gridComponents/Chart_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/gridComponents/Chart_spec.jsx
index a96a44caa73f..f097c2d78176 100644
--- a/superset/assets/spec/javascripts/dashboard/components/gridComponents/Chart_spec.jsx
+++ b/superset/assets/spec/javascripts/dashboard/components/gridComponents/Chart_spec.jsx
@@ -25,17 +25,14 @@ import SliceHeader from '../../../../../src/dashboard/components/SliceHeader';
import ChartContainer from '../../../../../src/chart/ChartContainer';
import mockDatasource from '../../../../fixtures/mockDatasource';
-import {
- sliceEntitiesForChart as sliceEntities,
- sliceId,
-} from '../../fixtures/mockSliceEntities';
+import { sliceEntitiesForChart as sliceEntities } from '../../fixtures/mockSliceEntities';
import chartQueries, {
sliceId as queryId,
} from '../../fixtures/mockChartQueries';
describe('Chart', () => {
const props = {
- id: sliceId,
+ id: queryId,
width: 100,
height: 100,
updateSliceName() {},
@@ -43,12 +40,12 @@ describe('Chart', () => {
// from redux
chart: chartQueries[queryId],
formData: chartQueries[queryId].formData,
- datasource: mockDatasource[sliceEntities.slices[sliceId].datasource],
+ datasource: mockDatasource[sliceEntities.slices[queryId].datasource],
slice: {
- ...sliceEntities.slices[sliceId],
+ ...sliceEntities.slices[queryId],
description_markeddown: 'markdown',
},
- sliceName: sliceEntities.slices[sliceId].slice_name,
+ sliceName: sliceEntities.slices[queryId].slice_name,
timeout: 60,
filters: {},
refreshChart() {},
@@ -92,10 +89,10 @@ describe('Chart', () => {
expect(refreshChart.callCount).toBe(1);
});
- it('should call addFilter when ChartContainer calls addFilter', () => {
- const addFilter = sinon.spy();
- const wrapper = setup({ addFilter });
- wrapper.instance().addFilter();
- expect(addFilter.callCount).toBe(1);
+ it('should call changeFilter when ChartContainer calls changeFilter', () => {
+ const changeFilter = sinon.spy();
+ const wrapper = setup({ changeFilter });
+ wrapper.instance().changeFilter();
+ expect(changeFilter.callCount).toBe(1);
});
});
diff --git a/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardFilters.js b/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardFilters.js
new file mode 100644
index 000000000000..e72e13e09abf
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardFilters.js
@@ -0,0 +1,46 @@
+/**
+ * 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 { filterId } from './mockSliceEntities';
+
+export const emptyFilters = {};
+
+export const dashboardFilters = {
+ [filterId]: {
+ chartId: filterId,
+ componentId: 'CHART-rwDfbGqeEn',
+ directPathToFilter: [
+ 'ROOT_ID',
+ 'TABS-VPEX_c476g',
+ 'TAB-PMJyKM1yB',
+ 'TABS-YdylzDMTMQ',
+ 'TAB-O9AaU9FT0',
+ 'ROW-l6PrlhwSjh',
+ 'CHART-rwDfbGqeEn',
+ ],
+ scope: 'ROOT_ID',
+ isDateFilter: false,
+ isInstantFilter: true,
+ columns: {
+ region: ['a', 'b'],
+ },
+ labels: {
+ region: 'region',
+ },
+ },
+};
diff --git a/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardInfo.js b/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardInfo.js
index 46d128122506..426a05e977ba 100644
--- a/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardInfo.js
+++ b/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardInfo.js
@@ -20,8 +20,8 @@ export default {
id: 1234,
slug: 'dashboardSlug',
metadata: {
- filter_immune_slices: [],
- filter_immune_slice_fields: {},
+ filterImmuneSlices: [],
+ filterImmuneSliceFields: {},
},
userId: 'mock_user_id',
dash_edit_perm: true,
diff --git a/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardLayout.js b/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardLayout.js
index f2f965ca085b..aad44452ff84 100644
--- a/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardLayout.js
+++ b/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardLayout.js
@@ -37,6 +37,7 @@ import {
import newComponentFactory from '../../../../src/dashboard/util/newComponentFactory';
import { sliceId as chartId } from './mockChartQueries';
+import { filterId } from './mockDashboardFilters';
export const sliceId = chartId;
@@ -187,3 +188,22 @@ export const dashboardLayoutWithTabs = {
},
future: [],
};
+
+export const filterComponent = {
+ ...newComponentFactory(CHART_TYPE),
+ id: 'CHART-rwDfbGqeEn',
+ parents: [
+ 'ROOT_ID',
+ 'TABS-VPEX_c476g',
+ 'TAB-PMJyKM1yB',
+ 'TABS-YdylzDMTMQ',
+ 'TAB-O9AaU9FT0',
+ 'ROW-l6PrlhwSjh',
+ ],
+ meta: {
+ chartId: filterId,
+ width: 3,
+ height: 10,
+ chartName: 'Filter',
+ },
+};
diff --git a/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardState.js b/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardState.js
index d3887110a587..22d7bfc60a9a 100644
--- a/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardState.js
+++ b/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardState.js
@@ -21,13 +21,12 @@ import { BUILDER_PANE_TYPE } from '../../../../src/dashboard/util/constants';
export default {
sliceIds: [sliceId],
- refresh: false,
- filters: {},
expandedSlices: {},
editMode: false,
builderPaneType: BUILDER_PANE_TYPE.NONE,
hasUnsavedChanges: false,
maxUndoHistoryExceeded: false,
isStarred: true,
+ isPublished: true,
css: '',
};
diff --git a/superset/assets/spec/javascripts/dashboard/fixtures/mockSliceEntities.js b/superset/assets/spec/javascripts/dashboard/fixtures/mockSliceEntities.js
index f69968d5e604..9f33c2963ec6 100644
--- a/superset/assets/spec/javascripts/dashboard/fixtures/mockSliceEntities.js
+++ b/superset/assets/spec/javascripts/dashboard/fixtures/mockSliceEntities.js
@@ -16,10 +16,11 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { sliceId as id } from './mockChartQueries';
import { datasourceId } from '../../../fixtures/mockDatasource';
+import { sliceId } from './mockChartQueries';
-export const sliceId = id;
+export const filterId = 127;
+export const column = 'region';
export const sliceEntitiesForChart = {
slices: {
@@ -49,6 +50,8 @@ export const sliceEntitiesForChart = {
datasource: datasourceId,
description: null,
description_markeddown: '',
+ modified: '23 hours ago',
+ changed_on: 1529453332615,
},
},
isLoading: false,
@@ -58,11 +61,23 @@ export const sliceEntitiesForChart = {
export const sliceEntitiesForDashboard = {
slices: {
- 127: {
- slice_id: 127,
+ [filterId]: {
+ slice_id: filterId,
slice_url: '/superset/explore/?form_data=%7B%22slice_id%22%3A%20127%7D',
slice_name: 'Region Filter',
- form_data: {},
+ form_data: {
+ instant_filtering: true,
+ filter_configs: [
+ {
+ asc: true,
+ clearable: true,
+ column,
+ key: 'JknLrSlNL',
+ multiple: true,
+ label: column,
+ },
+ ],
+ },
edit_url: '/chart/edit/127',
viz_type: 'filter_box',
datasource: '2__table',
diff --git a/superset/assets/spec/javascripts/dashboard/fixtures/mockState.js b/superset/assets/spec/javascripts/dashboard/fixtures/mockState.js
index b42b960bd8c9..35eeb8be86a7 100644
--- a/superset/assets/spec/javascripts/dashboard/fixtures/mockState.js
+++ b/superset/assets/spec/javascripts/dashboard/fixtures/mockState.js
@@ -19,6 +19,7 @@
import chartQueries from './mockChartQueries';
import { dashboardLayout } from './mockDashboardLayout';
import dashboardInfo from './mockDashboardInfo';
+import { emptyFilters } from './mockDashboardFilters';
import dashboardState from './mockDashboardState';
import messageToasts from '../../messageToasts/mockMessageToasts';
import datasources from '../../../fixtures/mockDatasource';
@@ -29,6 +30,7 @@ export default {
sliceEntities,
charts: chartQueries,
dashboardInfo,
+ dashboardFilters: emptyFilters,
dashboardState,
dashboardLayout,
messageToasts,
diff --git a/superset/assets/spec/javascripts/dashboard/fixtures/mockStore.js b/superset/assets/spec/javascripts/dashboard/fixtures/mockStore.js
index 63148bf696fd..5d877101b3f3 100644
--- a/superset/assets/spec/javascripts/dashboard/fixtures/mockStore.js
+++ b/superset/assets/spec/javascripts/dashboard/fixtures/mockStore.js
@@ -35,6 +35,7 @@ export const mockStoreWithTabs = createStore(
{
...mockState,
dashboardLayout: dashboardLayoutWithTabs,
+ dashboardFilters: {},
},
compose(applyMiddleware(thunk)),
);
diff --git a/superset/assets/spec/javascripts/dashboard/reducers/dashboardFilters_spec.js b/superset/assets/spec/javascripts/dashboard/reducers/dashboardFilters_spec.js
new file mode 100644
index 000000000000..88c67140a51d
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/reducers/dashboardFilters_spec.js
@@ -0,0 +1,138 @@
+/**
+ * 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.
+ */
+/* eslint-disable camelcase */
+import {
+ ADD_FILTER,
+ REMOVE_FILTER,
+ CHANGE_FILTER,
+} from '../../../../src/dashboard/actions/dashboardFilters';
+import dashboardFiltersReducer from '../../../../src/dashboard/reducers/dashboardFilters';
+import {
+ emptyFilters,
+ dashboardFilters,
+} from '../fixtures/mockDashboardFilters';
+import {
+ sliceEntitiesForDashboard,
+ filterId,
+ column,
+} from '../fixtures/mockSliceEntities';
+import { filterComponent } from '../fixtures/mockDashboardLayout';
+import { DASHBOARD_ROOT_ID } from '../../../../src/dashboard/util/constants';
+
+describe('dashboardFilters reducer', () => {
+ const form_data = sliceEntitiesForDashboard.slices[filterId].form_data;
+ const component = filterComponent;
+ const directPathToFilter = (component.parents || []).slice();
+ directPathToFilter.push(component.id);
+
+ it('should add a new filter if it does not exist', () => {
+ expect(
+ dashboardFiltersReducer(emptyFilters, {
+ type: ADD_FILTER,
+ chartId: filterId,
+ component,
+ form_data,
+ }),
+ ).toEqual({
+ [filterId]: {
+ chartId: filterId,
+ componentId: component.id,
+ directPathToFilter,
+ scope: DASHBOARD_ROOT_ID,
+ isDateFilter: false,
+ isInstantFilter: !!form_data.instant_filtering,
+ columns: {
+ [column]: undefined,
+ },
+ labels: {
+ [column]: column,
+ },
+ },
+ });
+ });
+
+ it('should overwrite a filter if merge is false', () => {
+ expect(
+ dashboardFiltersReducer(dashboardFilters, {
+ type: CHANGE_FILTER,
+ chartId: filterId,
+ newSelectedValues: {
+ region: ['c'],
+ gender: ['body', 'girl'],
+ },
+ merge: false,
+ }),
+ ).toEqual({
+ [filterId]: {
+ chartId: filterId,
+ componentId: component.id,
+ directPathToFilter,
+ scope: DASHBOARD_ROOT_ID,
+ isDateFilter: false,
+ isInstantFilter: !!form_data.instant_filtering,
+ columns: {
+ region: ['c'],
+ gender: ['body', 'girl'],
+ },
+ labels: {
+ [column]: column,
+ },
+ },
+ });
+ });
+
+ it('should merge a filter if merge is true', () => {
+ expect(
+ dashboardFiltersReducer(dashboardFilters, {
+ type: CHANGE_FILTER,
+ chartId: filterId,
+ newSelectedValues: {
+ region: ['c'],
+ gender: ['body', 'girl'],
+ },
+ merge: true,
+ }),
+ ).toEqual({
+ [filterId]: {
+ chartId: filterId,
+ componentId: component.id,
+ directPathToFilter,
+ scope: DASHBOARD_ROOT_ID,
+ isDateFilter: false,
+ isInstantFilter: !!form_data.instant_filtering,
+ columns: {
+ region: ['a', 'b', 'c'],
+ gender: ['body', 'girl'],
+ },
+ labels: {
+ [column]: column,
+ },
+ },
+ });
+ });
+
+ it('should remove the filter if values are empty', () => {
+ expect(
+ dashboardFiltersReducer(dashboardFilters, {
+ type: REMOVE_FILTER,
+ chartId: filterId,
+ }),
+ ).toEqual({});
+ });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/reducers/dashboardState_spec.js b/superset/assets/spec/javascripts/dashboard/reducers/dashboardState_spec.js
index dadcf06c8bf1..e6117819fd6c 100644
--- a/superset/assets/spec/javascripts/dashboard/reducers/dashboardState_spec.js
+++ b/superset/assets/spec/javascripts/dashboard/reducers/dashboardState_spec.js
@@ -18,7 +18,6 @@
*/
import {
ADD_SLICE,
- CHANGE_FILTER,
ON_CHANGE,
ON_SAVE,
REMOVE_SLICE,
@@ -52,16 +51,7 @@ describe('dashboardState reducer', () => {
{ sliceIds: [1, 2], filters: {} },
{ type: REMOVE_SLICE, sliceId: 2 },
),
- ).toEqual({ sliceIds: [1], refresh: false, filters: {} });
- });
-
- it('should reset filters if a removed slice is a filter', () => {
- expect(
- dashboardStateReducer(
- { sliceIds: [1, 2], filters: { 2: {}, 1: {} } },
- { type: REMOVE_SLICE, sliceId: 2 },
- ),
- ).toEqual({ sliceIds: [1], filters: { 1: {} }, refresh: true });
+ ).toEqual({ sliceIds: [1], filters: {} });
});
it('should toggle fav star', () => {
@@ -141,104 +131,4 @@ describe('dashboardState reducer', () => {
updatedColorScheme: false,
});
});
-
- describe('change filter', () => {
- it('should add a new filter if it does not exist', () => {
- expect(
- dashboardStateReducer(
- {
- filters: {},
- sliceIds: [1],
- },
- {
- type: CHANGE_FILTER,
- chart: { id: 1, formData: { groupby: 'column' } },
- col: 'column',
- vals: ['b', 'a'],
- refresh: true,
- merge: true,
- },
- ),
- ).toEqual({
- filters: { 1: { column: ['b', 'a'] } },
- refresh: true,
- sliceIds: [1],
- });
- });
-
- it('should overwrite a filter if merge is false', () => {
- expect(
- dashboardStateReducer(
- {
- filters: {
- 1: { column: ['z'] },
- },
- sliceIds: [1],
- },
- {
- type: CHANGE_FILTER,
- chart: { id: 1, formData: { groupby: 'column' } },
- col: 'column',
- vals: ['b', 'a'],
- refresh: true,
- merge: false,
- },
- ),
- ).toEqual({
- filters: { 1: { column: ['b', 'a'] } },
- refresh: true,
- sliceIds: [1],
- });
- });
-
- it('should merge a filter if merge is true', () => {
- expect(
- dashboardStateReducer(
- {
- filters: {
- 1: { column: ['z'] },
- },
- sliceIds: [1],
- },
- {
- type: CHANGE_FILTER,
- chart: { id: 1, formData: { groupby: 'column' } },
- col: 'column',
- vals: ['b', 'a'],
- refresh: true,
- merge: true,
- },
- ),
- ).toEqual({
- filters: { 1: { column: ['z', 'b', 'a'] } },
- refresh: true,
- sliceIds: [1],
- });
- });
-
- it('should remove the filter if values are empty', () => {
- expect(
- dashboardStateReducer(
- {
- filters: {
- 1: { column: ['z'] },
- },
- sliceIds: [1],
- },
- {
- type: CHANGE_FILTER,
- chart: { id: 1, formData: { groupby: 'column' } },
- col: 'column',
- vals: [],
- refresh: true,
- merge: false,
- },
- ),
- ).toEqual({
- filters: {},
- refresh: true,
- sliceIds: [1],
- });
- });
- });
});
diff --git a/superset/assets/spec/javascripts/dashboard/util/findTabIndexByComponentId_spec.js b/superset/assets/spec/javascripts/dashboard/util/findTabIndexByComponentId_spec.js
index 3e3d0f7b9d64..fcaec0100579 100644
--- a/superset/assets/spec/javascripts/dashboard/util/findTabIndexByComponentId_spec.js
+++ b/superset/assets/spec/javascripts/dashboard/util/findTabIndexByComponentId_spec.js
@@ -49,22 +49,22 @@ describe('findTabIndexByComponentId', () => {
];
const badPath = ['ROOT_ID', 'TABS-MNQQSW-kyd', 'TAB-ABC', 'TABS-Oduxop1L7I'];
- it('should return 0 if no directPathToChild', () => {
+ it('should return -1 if no directPathToChild', () => {
expect(
findTabIndexByComponentId({
currentComponent: topLevelTabsComponent,
directPathToChild: [],
}),
- ).toBe(0);
+ ).toBe(-1);
});
- it('should return 0 if not found tab id', () => {
+ it('should return -1 if not found tab id', () => {
expect(
findTabIndexByComponentId({
currentComponent: topLevelTabsComponent,
directPathToChild: badPath,
}),
- ).toBe(0);
+ ).toBe(-1);
});
it('should return children index if matched an id in the path', () => {
diff --git a/superset/assets/spec/javascripts/dashboard/util/getFormDataWithExtraFilters_spec.js b/superset/assets/spec/javascripts/dashboard/util/getFormDataWithExtraFilters_spec.js
index e0c16a5312cf..544b2d484f5e 100644
--- a/superset/assets/spec/javascripts/dashboard/util/getFormDataWithExtraFilters_spec.js
+++ b/superset/assets/spec/javascripts/dashboard/util/getFormDataWithExtraFilters_spec.js
@@ -34,8 +34,8 @@ describe('getFormDataWithExtraFilters', () => {
},
},
dashboardMetadata: {
- filter_immune_slices: [],
- filter_immune_slice_fields: {},
+ filterImmuneSlices: [],
+ filterImmuneSliceFields: {},
},
filters: {
filterId: {
@@ -65,7 +65,7 @@ describe('getFormDataWithExtraFilters', () => {
const result = getFormDataWithExtraFilters({
...mockArgs,
dashboardMetadata: {
- filter_immune_slices: [chartId],
+ filterImmuneSlices: [chartId],
},
});
expect(result.extra_filters).toHaveLength(0);
@@ -75,7 +75,7 @@ describe('getFormDataWithExtraFilters', () => {
const result = getFormDataWithExtraFilters({
...mockArgs,
dashboardMetadata: {
- filter_immune_slice_fields: {
+ filterImmuneSliceFields: {
[chartId]: ['region'],
},
},
diff --git a/superset/assets/spec/javascripts/dashboard/util/getLeafComponentIdFromPath_spec.js b/superset/assets/spec/javascripts/dashboard/util/getLeafComponentIdFromPath_spec.js
new file mode 100644
index 000000000000..cb6331f942d0
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/util/getLeafComponentIdFromPath_spec.js
@@ -0,0 +1,37 @@
+/**
+ * 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 getLeafComponentIdFromPath from '../../../../src/dashboard/util/getLeafComponentIdFromPath';
+import { filterId } from '../fixtures/mockSliceEntities';
+import { dashboardFilters } from '../fixtures/mockDashboardFilters';
+
+describe('getLeafComponentIdFromPath', () => {
+ const path = dashboardFilters[filterId].directPathToFilter;
+ const leaf = path.slice().pop();
+
+ it('should return component id', () => {
+ expect(getLeafComponentIdFromPath(path)).toBe(leaf);
+ });
+
+ it('should not return label component', () => {
+ const updatedPath = dashboardFilters[filterId].directPathToFilter.concat(
+ 'LABEL-test123',
+ );
+ expect(getLeafComponentIdFromPath(updatedPath)).toBe(leaf);
+ });
+});
diff --git a/superset/assets/src/chart/chartReducer.js b/superset/assets/src/chart/chartReducer.js
index e7ad6c6a5661..02df8cb28d15 100644
--- a/superset/assets/src/chart/chartReducer.js
+++ b/superset/assets/src/chart/chartReducer.js
@@ -182,7 +182,7 @@ export default function chartReducer(charts = {}, action) {
if (action.type in actionHandlers) {
return {
...charts,
- [action.key]: actionHandlers[action.type](charts[action.key], action),
+ [action.key]: actionHandlers[action.type](charts[action.key]),
};
}
diff --git a/superset/assets/src/components/AnchorLink.jsx b/superset/assets/src/components/AnchorLink.jsx
index c21bb4bb7228..e703013f4cf9 100644
--- a/superset/assets/src/components/AnchorLink.jsx
+++ b/superset/assets/src/components/AnchorLink.jsx
@@ -22,42 +22,52 @@ import { t } from '@superset-ui/translation';
import URLShortLinkButton from './URLShortLinkButton';
import getDashboardUrl from '../dashboard/util/getDashboardUrl';
+import getLocationHash from '../dashboard/util/getLocationHash';
const propTypes = {
anchorLinkId: PropTypes.string.isRequired,
filters: PropTypes.object,
showShortLinkButton: PropTypes.bool,
+ inFocus: PropTypes.bool,
placement: PropTypes.oneOf(['right', 'left', 'top', 'bottom']),
};
const defaultProps = {
+ inFocus: false,
showShortLinkButton: false,
placement: 'right',
filters: {},
};
-
class AnchorLink extends React.PureComponent {
componentDidMount() {
- const hash = this.getLocationHash();
+ const hash = getLocationHash();
const { anchorLinkId } = this.props;
if (hash && anchorLinkId === hash) {
- const directLinkComponent = document.getElementById(anchorLinkId);
- if (directLinkComponent) {
- setTimeout(() => {
- directLinkComponent.scrollIntoView({
- block: 'center',
- behavior: 'smooth',
- });
- }, 1000);
- }
+ this.scrollToView();
+ }
+ }
+
+ componentWillReceiveProps(nextProps) {
+ const { inFocus = false } = nextProps;
+ if (inFocus) {
+ this.scrollToView();
}
}
- getLocationHash() {
- return (window.location.hash || '').substring(1);
+ scrollToView(delay = 0) {
+ const { anchorLinkId } = this.props;
+ const directLinkComponent = document.getElementById(anchorLinkId);
+ if (directLinkComponent) {
+ setTimeout(() => {
+ directLinkComponent.scrollIntoView({
+ block: 'center',
+ behavior: 'smooth',
+ });
+ }, delay);
+ }
}
render() {
diff --git a/superset/assets/src/components/FilterBadgeIcon.css b/superset/assets/src/components/FilterBadgeIcon.css
new file mode 100644
index 000000000000..4cb6a713e56c
--- /dev/null
+++ b/superset/assets/src/components/FilterBadgeIcon.css
@@ -0,0 +1,29 @@
+/**
+ * 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.
+ */
+.filter-badge {
+ width: 20px;
+ height: 20px;
+ background-color: #bababa;
+ border-radius: 2px;
+ z-index: 10;
+}
+.filter-badge:hover {
+ cursor: pointer;
+ background-color: #9e9e9e;
+}
diff --git a/superset/assets/src/components/FilterBadgeIcon.jsx b/superset/assets/src/components/FilterBadgeIcon.jsx
new file mode 100644
index 000000000000..e523aa28df7e
--- /dev/null
+++ b/superset/assets/src/components/FilterBadgeIcon.jsx
@@ -0,0 +1,36 @@
+/**
+ * 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 PropTypes from 'prop-types';
+
+import './FilterBadgeIcon.css';
+
+const propTypes = {
+ colorCode: PropTypes.string,
+};
+
+export default function FilterBadgeIcon({ colorCode = '' }) {
+ return (
+
+ );
+}
+
+FilterBadgeIcon.propTypes = propTypes;
diff --git a/superset/assets/src/components/FilterEditIcon.css b/superset/assets/src/components/FilterEditIcon.css
new file mode 100644
index 000000000000..d3eef85466a8
--- /dev/null
+++ b/superset/assets/src/components/FilterEditIcon.css
@@ -0,0 +1,21 @@
+/**
+ * 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.
+ */
+.filter-edit {
+ cursor: pointer;
+}
diff --git a/superset/assets/src/components/FilterEditIcon.jsx b/superset/assets/src/components/FilterEditIcon.jsx
new file mode 100644
index 000000000000..91ac04ccca72
--- /dev/null
+++ b/superset/assets/src/components/FilterEditIcon.jsx
@@ -0,0 +1,42 @@
+/**
+ * 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 PropTypes from 'prop-types';
+
+import './FilterEditIcon.css';
+
+const propTypes = {
+ clickIconHandler: PropTypes.func,
+};
+
+export default function FilterEditIcon({ clickIconHandler = () => {} }) {
+ return (
+
+ );
+}
+
+FilterEditIcon.propTypes = propTypes;
diff --git a/superset/assets/src/dashboard/actions/dashboardFilters.js b/superset/assets/src/dashboard/actions/dashboardFilters.js
new file mode 100644
index 000000000000..c2b8b9c1d99a
--- /dev/null
+++ b/superset/assets/src/dashboard/actions/dashboardFilters.js
@@ -0,0 +1,68 @@
+/**
+ * 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.
+ */
+/* eslint-disable camelcase */
+// util function to make sure filter is a valid slice in current dashboard
+function isValidFilter(getState, chartId) {
+ return getState().dashboardState.sliceIds.includes(chartId);
+}
+
+export const ADD_FILTER = 'ADD_FILTER';
+export function addFilter(chartId, component, form_data) {
+ return (dispatch, getState) => {
+ if (isValidFilter(getState, chartId)) {
+ return dispatch({ type: ADD_FILTER, chartId, component, form_data });
+ }
+ return getState().dashboardFilters;
+ };
+}
+
+export const REMOVE_FILTER = 'REMOVE_FILTER';
+export function removeFilter(chartId) {
+ return (dispatch, getState) => {
+ if (isValidFilter(getState, chartId)) {
+ return dispatch({ type: REMOVE_FILTER, chartId });
+ }
+ return getState().dashboardFilters;
+ };
+}
+
+export const CHANGE_FILTER = 'CHANGE_FILTER';
+export function changeFilter(chartId, newSelectedValues, merge) {
+ return (dispatch, getState) => {
+ if (isValidFilter(getState, chartId)) {
+ return dispatch({
+ type: CHANGE_FILTER,
+ chartId,
+ newSelectedValues,
+ merge,
+ });
+ }
+ return getState().dashboardFilters;
+ };
+}
+
+export const UPDATE_DIRECT_PATH_TO_FILTER = 'UPDATE_DIRECT_PATH_TO_FILTER';
+export function updateDirectPathToFilter(chartId, path) {
+ return (dispatch, getState) => {
+ if (isValidFilter(getState, chartId)) {
+ return dispatch({ type: UPDATE_DIRECT_PATH_TO_FILTER, chartId, path });
+ }
+ return getState().dashboardFilters;
+ };
+}
diff --git a/superset/assets/src/dashboard/actions/dashboardState.js b/superset/assets/src/dashboard/actions/dashboardState.js
index 3318c33eb6c4..4d8430f592bc 100644
--- a/superset/assets/src/dashboard/actions/dashboardState.js
+++ b/superset/assets/src/dashboard/actions/dashboardState.js
@@ -24,6 +24,11 @@ import { SupersetClient } from '@superset-ui/connection';
import { addChart, removeChart, refreshChart } from '../../chart/chartAction';
import { chart as initChart } from '../../chart/chartReducer';
import { fetchDatasourceMetadata } from '../../dashboard/actions/datasources';
+import {
+ addFilter,
+ removeFilter,
+ updateDirectPathToFilter,
+} from '../../dashboard/actions/dashboardFilters';
import { applyDefaultFormData } from '../../explore/store';
import getClientErrorObject from '../../utils/getClientErrorObject';
import { SAVE_TYPE_OVERWRITE } from '../util/constants';
@@ -39,11 +44,6 @@ export function setUnsavedChanges(hasUnsavedChanges) {
return { type: SET_UNSAVED_CHANGES, payload: { hasUnsavedChanges } };
}
-export const CHANGE_FILTER = 'CHANGE_FILTER';
-export function changeFilter(chart, col, vals, merge = true, refresh = true) {
- return { type: CHANGE_FILTER, chart, col, vals, merge, refresh };
-}
-
export const ADD_SLICE = 'ADD_SLICE';
export function addSlice(slice) {
return { type: ADD_SLICE, slice };
@@ -166,9 +166,19 @@ export function saveDashboardRequestSuccess() {
export function saveDashboardRequest(data, id, saveType) {
const path = saveType === SAVE_TYPE_OVERWRITE ? 'save_dash' : 'copy_dash';
- return dispatch => {
+ return (dispatch, getState) => {
dispatch({ type: UPDATE_COMPONENTS_PARENTS_LIST });
+ const { dashboardFilters, dashboardLayout } = getState();
+ const layout = dashboardLayout.present;
+ Object.values(dashboardFilters).forEach(filter => {
+ const { chartId } = filter;
+ const componentId = filter.directPathToFilter.slice().pop();
+ const directPathToFilter = (layout[componentId].parents || []).slice();
+ directPathToFilter.push(componentId);
+ dispatch(updateDirectPathToFilter(chartId, directPathToFilter));
+ });
+
return SupersetClient.post({
endpoint: `/superset/${path}/${id}/`,
postPayload: { data },
@@ -257,7 +267,7 @@ export function showBuilderPane(builderPaneType) {
return { type: SHOW_BUILDER_PANE, builderPaneType };
}
-export function addSliceToDashboard(id) {
+export function addSliceToDashboard(id, component) {
return (dispatch, getState) => {
const { sliceEntities } = getState();
const selectedSlice = sliceEntities.slices[id];
@@ -282,12 +292,23 @@ export function addSliceToDashboard(id) {
return Promise.all([
dispatch(addChart(newChart, id)),
dispatch(fetchDatasourceMetadata(form_data.datasource)),
- ]).then(() => dispatch(addSlice(selectedSlice)));
+ ]).then(() => {
+ dispatch(addSlice(selectedSlice));
+
+ if (selectedSlice && selectedSlice.viz_type === 'filter_box') {
+ dispatch(addFilter(id, component, selectedSlice.form_data));
+ }
+ });
};
}
export function removeSliceFromDashboard(id) {
- return dispatch => {
+ return (dispatch, getState) => {
+ const sliceEntity = getState().sliceEntities.slices[id];
+ if (sliceEntity && sliceEntity.viz_type === 'filter_box') {
+ dispatch(removeFilter(id));
+ }
+
dispatch(removeSlice(id));
dispatch(removeChart(id));
};
@@ -305,6 +326,11 @@ export function setColorSchemeAndUnsavedChanges(colorScheme) {
};
}
+export const SET_DIRECT_PATH = 'SET_DIRECT_PATH';
+export function setDirectPathToChild(path) {
+ return { type: SET_DIRECT_PATH, path };
+}
+
// Undo history ---------------------------------------------------------------
export const SET_MAX_UNDO_HISTORY_EXCEEDED = 'SET_MAX_UNDO_HISTORY_EXCEEDED';
export function setMaxUndoHistoryExceeded(maxUndoHistoryExceeded = true) {
diff --git a/superset/assets/src/dashboard/components/Dashboard.jsx b/superset/assets/src/dashboard/components/Dashboard.jsx
index 13222c6ec915..72d3d949fe7c 100644
--- a/superset/assets/src/dashboard/components/Dashboard.jsx
+++ b/superset/assets/src/dashboard/components/Dashboard.jsx
@@ -16,12 +16,12 @@
* specific language governing permissions and limitations
* under the License.
*/
-/* eslint-disable camelcase */
import React from 'react';
import PropTypes from 'prop-types';
import { t } from '@superset-ui/translation';
import getChartIdsFromLayout from '../util/getChartIdsFromLayout';
+import getLayoutComponentFromChartId from '../util/getLayoutComponentFromChartId';
import DashboardBuilder from '../containers/DashboardBuilder';
import {
chartPropShape,
@@ -33,6 +33,7 @@ import {
import { areObjectsEqual } from '../../reduxUtils';
import { LOG_ACTIONS_MOUNT_DASHBOARD } from '../../logger/LogUtils';
import OmniContainer from '../../components/OmniContainer';
+import { safeStringify } from '../../utils/safeStringify';
import '../stylesheets/index.less';
@@ -47,6 +48,7 @@ const propTypes = {
dashboardState: dashboardStatePropShape.isRequired,
charts: PropTypes.objectOf(chartPropShape).isRequired,
slices: PropTypes.objectOf(slicePropShape).isRequired,
+ filters: PropTypes.object.isRequired,
datasources: PropTypes.object.isRequired,
loadStats: loadStatsPropShape.isRequired,
layout: PropTypes.object.isRequired,
@@ -78,6 +80,11 @@ class Dashboard extends React.PureComponent {
return message; // Gecko + Webkit, Safari, Chrome etc.
}
+ constructor(props) {
+ super(props);
+ this.appliedFilters = {};
+ }
+
componentDidMount() {
this.props.actions.logEvent(LOG_ACTIONS_MOUNT_DASHBOARD);
}
@@ -91,7 +98,10 @@ class Dashboard extends React.PureComponent {
key => currentChartIds.indexOf(key) === -1,
);
newChartIds.forEach(newChartId =>
- this.props.actions.addSliceToDashboard(newChartId),
+ this.props.actions.addSliceToDashboard(
+ newChartId,
+ getLayoutComponentFromChartId(nextProps.layout, newChartId),
+ ),
);
} else if (currentChartIds.length > nextChartIds.length) {
// remove chart
@@ -104,20 +114,23 @@ class Dashboard extends React.PureComponent {
}
}
- componentDidUpdate(prevProps) {
- const { refresh, filters, hasUnsavedChanges } = this.props.dashboardState;
- if (refresh) {
+ componentDidUpdate() {
+ const { hasUnsavedChanges, editMode } = this.props.dashboardState;
+
+ const appliedFilters = this.appliedFilters;
+ const { filters } = this.props;
+ // do not apply filter when dashboard in edit mode
+ if (!editMode && safeStringify(appliedFilters) !== safeStringify(filters)) {
// refresh charts if a filter was removed, added, or changed
let changedFilterKey = null;
const currFilterKeys = Object.keys(filters);
- const prevFilterKeys = Object.keys(prevProps.dashboardState.filters);
+ const appliedFilterKeys = Object.keys(appliedFilters);
currFilterKeys.forEach(key => {
- const prevFilter = prevProps.dashboardState.filters[key];
if (
// filter was added or changed
- typeof prevFilter === 'undefined' ||
- !areObjectsEqual(prevFilter, filters[key])
+ typeof appliedFilters[key] === 'undefined' ||
+ !areObjectsEqual(appliedFilters[key], filters[key])
) {
changedFilterKey = key;
}
@@ -125,9 +138,10 @@ class Dashboard extends React.PureComponent {
if (
!!changedFilterKey ||
- currFilterKeys.length !== prevFilterKeys.length
+ currFilterKeys.length !== appliedFilterKeys.length // remove 1 or more filters
) {
this.refreshExcept(changedFilterKey);
+ this.appliedFilters = filters;
}
}
@@ -144,30 +158,30 @@ class Dashboard extends React.PureComponent {
}
refreshExcept(filterKey) {
- const { filters } = this.props.dashboardState || {};
+ const { filters } = this.props;
const currentFilteredNames =
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
- .filter_immune_slice_fields;
+ const filterImmuneSlices = this.props.dashboardInfo.metadata
+ .filterImmuneSlices;
+ const filterImmuneSliceFields = this.props.dashboardInfo.metadata
+ .filterImmuneSliceFields;
this.getAllCharts().forEach(chart => {
// filterKey is a string, filter_immune_slices array contains numbers
if (
String(chart.id) === filterKey ||
- filter_immune_slices.includes(chart.id)
+ filterImmuneSlices.includes(chart.id)
) {
return;
}
- const filter_immune_slice_fields_names =
- filter_immune_slice_fields[chart.id] || [];
+ const filterImmuneSliceFieldsNames =
+ filterImmuneSliceFields[chart.id] || [];
// has filter-able field names
if (
currentFilteredNames.length === 0 ||
currentFilteredNames.some(
- name => !filter_immune_slice_fields_names.includes(name),
+ name => !filterImmuneSliceFieldsNames.includes(name),
)
) {
this.props.actions.triggerQuery(true, chart.id);
diff --git a/superset/assets/src/dashboard/components/DashboardBuilder.jsx b/superset/assets/src/dashboard/components/DashboardBuilder.jsx
index eadaab4e8050..a5e3d2997529 100644
--- a/superset/assets/src/dashboard/components/DashboardBuilder.jsx
+++ b/superset/assets/src/dashboard/components/DashboardBuilder.jsx
@@ -44,6 +44,8 @@ import {
DASHBOARD_ROOT_ID,
DASHBOARD_ROOT_DEPTH,
} from '../util/constants';
+import getDirectPathToTabIndex from '../util/getDirectPathToTabIndex';
+import getLeafComponentIdFromPath from '../util/getLeafComponentIdFromPath';
const TABS_HEIGHT = 47;
const HEADER_HEIGHT = 67;
@@ -55,11 +57,11 @@ const propTypes = {
editMode: PropTypes.bool.isRequired,
showBuilderPane: PropTypes.func.isRequired,
builderPaneType: PropTypes.string.isRequired,
- setColorSchemeAndUnsavedChanges: PropTypes.func.isRequired,
colorScheme: PropTypes.string,
+ setColorSchemeAndUnsavedChanges: PropTypes.func.isRequired,
handleComponentDrop: PropTypes.func.isRequired,
- toggleBuilderPane: PropTypes.func.isRequired,
directPathToChild: PropTypes.arrayOf(PropTypes.string),
+ setDirectPathToChild: PropTypes.func.isRequired,
};
const defaultProps = {
@@ -78,23 +80,38 @@ class DashboardBuilder extends React.Component {
);
}
+ static getRootLevelTabIndex(dashboardLayout, directPathToChild) {
+ return Math.max(
+ 0,
+ findTabIndexByComponentId({
+ currentComponent: DashboardBuilder.getRootLevelTabsComponent(
+ dashboardLayout,
+ ),
+ directPathToChild,
+ }),
+ );
+ }
+
+ static getRootLevelTabsComponent(dashboardLayout) {
+ const dashboardRoot = dashboardLayout[DASHBOARD_ROOT_ID];
+ const rootChildId = dashboardRoot.children[0];
+ return rootChildId === DASHBOARD_GRID_ID
+ ? dashboardLayout[DASHBOARD_ROOT_ID]
+ : dashboardLayout[rootChildId];
+ }
+
constructor(props) {
super(props);
const { dashboardLayout, directPathToChild } = props;
- const dashboardRoot = dashboardLayout[DASHBOARD_ROOT_ID];
- const rootChildId = dashboardRoot.children[0];
- const tabIndex = findTabIndexByComponentId({
- currentComponent:
- rootChildId === DASHBOARD_GRID_ID
- ? dashboardLayout[DASHBOARD_ROOT_ID]
- : dashboardLayout[rootChildId],
+ const tabIndex = DashboardBuilder.getRootLevelTabIndex(
+ dashboardLayout,
directPathToChild,
- });
-
+ );
this.state = {
tabIndex,
};
+
this.handleChangeTab = this.handleChangeTab.bind(this);
this.handleDeleteTopLevelTabs = this.handleDeleteTopLevelTabs.bind(this);
}
@@ -105,20 +122,37 @@ class DashboardBuilder extends React.Component {
};
}
+ componentWillReceiveProps(nextProps) {
+ const nextFocusComponent = getLeafComponentIdFromPath(
+ nextProps.directPathToChild,
+ );
+ const currentFocusComponent = getLeafComponentIdFromPath(
+ this.props.directPathToChild,
+ );
+ if (nextFocusComponent !== currentFocusComponent) {
+ const { dashboardLayout, directPathToChild } = nextProps;
+ const nextTabIndex = DashboardBuilder.getRootLevelTabIndex(
+ dashboardLayout,
+ directPathToChild,
+ );
+
+ this.setState(() => ({ tabIndex: nextTabIndex }));
+ }
+ }
+
handleDeleteTopLevelTabs() {
this.props.deleteTopLevelTabs();
- this.setState({ tabIndex: 0 });
+
+ const { dashboardLayout } = this.props;
+ const firstTab = getDirectPathToTabIndex(
+ DashboardBuilder.getRootLevelTabsComponent(dashboardLayout),
+ 0,
+ );
+ this.props.setDirectPathToChild(firstTab);
}
- handleChangeTab({ tabIndex }) {
- this.setState(() => ({ tabIndex }));
- setTimeout(() => {
- if (window)
- window.scrollTo({
- top: 0,
- behavior: 'smooth',
- });
- }, 100);
+ handleChangeTab({ pathToTabIndex }) {
+ this.props.setDirectPathToChild(pathToTabIndex);
}
render() {
diff --git a/superset/assets/src/dashboard/components/DashboardGrid.jsx b/superset/assets/src/dashboard/components/DashboardGrid.jsx
index 0666f477380a..4f74b65f5f3a 100644
--- a/superset/assets/src/dashboard/components/DashboardGrid.jsx
+++ b/superset/assets/src/dashboard/components/DashboardGrid.jsx
@@ -32,6 +32,7 @@ const propTypes = {
handleComponentDrop: PropTypes.func.isRequired,
isComponentVisible: PropTypes.bool.isRequired,
resizeComponent: PropTypes.func.isRequired,
+ setDirectPathToChild: PropTypes.func.isRequired,
width: PropTypes.number.isRequired,
};
@@ -51,6 +52,7 @@ class DashboardGrid extends React.PureComponent {
this.handleTopDropTargetDrop = this.handleTopDropTargetDrop.bind(this);
this.getRowGuidePosition = this.getRowGuidePosition.bind(this);
this.setGridRef = this.setGridRef.bind(this);
+ this.handleChangeTab = this.handleChangeTab.bind(this);
}
getRowGuidePosition(resizeRef) {
@@ -108,6 +110,10 @@ class DashboardGrid extends React.PureComponent {
}
}
+ handleChangeTab({ pathToTabIndex }) {
+ this.props.setDirectPathToChild(pathToTabIndex);
+ }
+
render() {
const {
gridComponent,
@@ -160,6 +166,7 @@ class DashboardGrid extends React.PureComponent {
onResizeStart={this.handleResizeStart}
onResize={this.handleResize}
onResizeStop={this.handleResizeStop}
+ onChangeTab={this.handleChangeTab}
/>
))}
diff --git a/superset/assets/src/dashboard/components/FilterIndicator.jsx b/superset/assets/src/dashboard/components/FilterIndicator.jsx
new file mode 100644
index 000000000000..5fd238d867b7
--- /dev/null
+++ b/superset/assets/src/dashboard/components/FilterIndicator.jsx
@@ -0,0 +1,73 @@
+/**
+ * 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 PropTypes from 'prop-types';
+import { t } from '@superset-ui/translation';
+
+import { filterIndicatorPropShape } from '../util/propShapes';
+import FilterBadgeIcon from '../../components/FilterBadgeIcon';
+import FilterIndicatorTooltip from './FilterIndicatorTooltip';
+import FilterTooltipWrapper from './FilterTooltipWrapper';
+
+const propTypes = {
+ indicator: filterIndicatorPropShape.isRequired,
+ setDirectPathToChild: PropTypes.func.isRequired,
+};
+
+class FilterIndicator extends React.PureComponent {
+ constructor(props) {
+ super(props);
+
+ const { indicator, setDirectPathToChild } = props;
+ const { directPathToFilter } = indicator;
+ this.focusToFilterComponent = setDirectPathToChild.bind(
+ this,
+ directPathToFilter,
+ );
+ }
+
+ render() {
+ const { colorCode, label, values } = this.props.indicator;
+
+ const filterTooltip = (
+
+ );
+
+ return (
+
+
+
+
+
+
+ );
+ }
+}
+
+FilterIndicator.propTypes = propTypes;
+
+export default FilterIndicator;
diff --git a/superset/assets/src/dashboard/components/FilterIndicatorGroup.jsx b/superset/assets/src/dashboard/components/FilterIndicatorGroup.jsx
new file mode 100644
index 000000000000..bc7a22c862b9
--- /dev/null
+++ b/superset/assets/src/dashboard/components/FilterIndicatorGroup.jsx
@@ -0,0 +1,77 @@
+/**
+ * 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 PropTypes from 'prop-types';
+import { t } from '@superset-ui/translation';
+
+import FilterBadgeIcon from '../../components/FilterBadgeIcon';
+import FilterIndicatorTooltip from './FilterIndicatorTooltip';
+import FilterTooltipWrapper from './FilterTooltipWrapper';
+import { filterIndicatorPropShape } from '../util/propShapes';
+
+const propTypes = {
+ indicators: PropTypes.arrayOf(filterIndicatorPropShape).isRequired,
+ setDirectPathToChild: PropTypes.func.isRequired,
+};
+
+class FilterIndicatorGroup extends React.PureComponent {
+ constructor(props) {
+ super(props);
+
+ const { indicators, setDirectPathToChild } = this.props;
+ this.onClickIcons = indicators.map(indicator =>
+ setDirectPathToChild.bind(this, indicator.directPathToFilter),
+ );
+ }
+
+ render() {
+ const { indicators } = this.props;
+ return (
+
+
+ {t('%s more filters', indicators.length)}
+
+
+ {indicators.map((indicator, index) => (
+
+
+
+ ))}
+
+
+ }
+ >
+
+
+
+
+
+ );
+ }
+}
+
+FilterIndicatorGroup.propTypes = propTypes;
+
+export default FilterIndicatorGroup;
diff --git a/superset/assets/src/dashboard/components/FilterIndicatorTooltip.jsx b/superset/assets/src/dashboard/components/FilterIndicatorTooltip.jsx
new file mode 100644
index 000000000000..6aab00d9073a
--- /dev/null
+++ b/superset/assets/src/dashboard/components/FilterIndicatorTooltip.jsx
@@ -0,0 +1,58 @@
+/**
+ * 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 PropTypes from 'prop-types';
+import { t } from '@superset-ui/translation';
+import { isEmpty } from 'lodash';
+
+import FilterEditIcon from '../../components/FilterEditIcon';
+
+const propTypes = {
+ label: PropTypes.string.isRequired,
+ values: PropTypes.array.isRequired,
+ clickIconHandler: PropTypes.func,
+};
+
+const defaultProps = {
+ clickIconHandler: undefined,
+};
+
+export default function FilterIndicatorTooltip({
+ label,
+ values,
+ clickIconHandler,
+}) {
+ const displayValue = isEmpty(values) ? t('Not filtered') : values.join(', ');
+
+ return (
+
+
+
+ {displayValue}
+
+
+ {clickIconHandler && (
+
+ )}
+
+ );
+}
+
+FilterIndicatorTooltip.propTypes = propTypes;
+FilterIndicatorTooltip.defaultProps = defaultProps;
diff --git a/superset/assets/src/dashboard/components/FilterIndicatorsContainer.jsx b/superset/assets/src/dashboard/components/FilterIndicatorsContainer.jsx
new file mode 100644
index 000000000000..09a74780f4c9
--- /dev/null
+++ b/superset/assets/src/dashboard/components/FilterIndicatorsContainer.jsx
@@ -0,0 +1,181 @@
+/**
+ * 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 PropTypes from 'prop-types';
+import { isEmpty } from 'lodash';
+
+import FilterIndicator from './FilterIndicator';
+import FilterIndicatorGroup from './FilterIndicatorGroup';
+import { FILTER_INDICATORS_DISPLAY_LENGTH } from '../util/constants';
+import {
+ getFilterColorKey,
+ getFilterColorMap,
+} from '../util/dashboardFiltersColorMap';
+
+const propTypes = {
+ // from props
+ dashboardFilters: PropTypes.object.isRequired,
+ chartId: PropTypes.number.isRequired,
+ chartStatus: PropTypes.string,
+
+ // from redux
+ filterImmuneSlices: PropTypes.arrayOf(PropTypes.number).isRequired,
+ filterImmuneSliceFields: PropTypes.object.isRequired,
+ setDirectPathToChild: PropTypes.func.isRequired,
+};
+
+const defaultProps = {
+ chartStatus: 'loading',
+};
+
+function sortByIndicatorLabel(indicator1, indicator2) {
+ const s1 = (indicator1.label || indicator1.name).toLowerCase();
+ const s2 = (indicator2.label || indicator2.name).toLowerCase();
+ if (s1 < s2) {
+ return -1;
+ } else if (s1 > s2) {
+ return 1;
+ }
+ return 0;
+}
+
+export default class FilterIndicatorsContainer extends React.PureComponent {
+ getFilterIndicators() {
+ const {
+ dashboardFilters,
+ chartId: currentChartId,
+ filterImmuneSlices,
+ filterImmuneSliceFields,
+ } = this.props;
+
+ if (Object.keys(dashboardFilters).length === 0) {
+ return [];
+ }
+
+ const dashboardFiltersColorMap = getFilterColorMap();
+
+ const sortIndicatorsByEmptiness = Object.values(dashboardFilters).reduce(
+ (indicators, dashboardFilter) => {
+ const {
+ chartId,
+ componentId,
+ directPathToFilter,
+ scope,
+ isDateFilter,
+ isInstantFilter,
+ columns,
+ labels,
+ } = dashboardFilter;
+
+ // do not apply filter on filter_box itself
+ // do not apply filter on filterImmuneSlices list
+ if (
+ currentChartId !== chartId &&
+ !filterImmuneSlices.includes(currentChartId)
+ ) {
+ Object.keys(columns).forEach(name => {
+ const colorMapKey = getFilterColorKey(chartId, name);
+ const directPathToLabel = directPathToFilter.slice();
+ directPathToLabel.push(`LABEL-${name}`);
+ const indicator = {
+ chartId,
+ colorCode: dashboardFiltersColorMap[colorMapKey],
+ componentId,
+ directPathToFilter: directPathToLabel,
+ scope,
+ isDateFilter,
+ isInstantFilter,
+ name,
+ label: labels[name] || name,
+ values:
+ isEmpty(columns[name]) ||
+ (isDateFilter && columns[name] === 'No filter')
+ ? []
+ : [].concat(columns[name]),
+ };
+
+ // do not apply filter on fields in the filterImmuneSliceFields map
+ if (
+ filterImmuneSliceFields[currentChartId] &&
+ filterImmuneSliceFields[currentChartId].includes(name)
+ ) {
+ return;
+ }
+
+ if (isEmpty(indicator.values)) {
+ indicators[1].push(indicator);
+ } else {
+ indicators[0].push(indicator);
+ }
+ });
+ }
+
+ return indicators;
+ },
+ [[], []],
+ );
+
+ // cypress' electron don't support [].flat():
+ return [
+ ...sortIndicatorsByEmptiness[0].sort(sortByIndicatorLabel),
+ ...sortIndicatorsByEmptiness[1].sort(sortByIndicatorLabel),
+ ];
+ }
+
+ render() {
+ const { chartStatus, setDirectPathToChild } = this.props;
+ if (chartStatus === 'loading') {
+ return null;
+ }
+
+ const indicators = this.getFilterIndicators();
+ // if total indicators <= 5, show all
+ // else: show top 4 indicators, and show all the rest in group
+ const showExtraIndicatorsInGroup =
+ indicators.length > FILTER_INDICATORS_DISPLAY_LENGTH + 1;
+
+ return (
+
+ );
+ }
+}
+
+FilterIndicatorsContainer.propTypes = propTypes;
+FilterIndicatorsContainer.defaultProps = defaultProps;
diff --git a/superset/assets/src/dashboard/components/FilterTooltipWrapper.jsx b/superset/assets/src/dashboard/components/FilterTooltipWrapper.jsx
new file mode 100644
index 000000000000..953f3cd78c74
--- /dev/null
+++ b/superset/assets/src/dashboard/components/FilterTooltipWrapper.jsx
@@ -0,0 +1,82 @@
+/**
+ * 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 PropTypes from 'prop-types';
+import { Overlay, Tooltip } from 'react-bootstrap';
+
+const propTypes = {
+ tooltip: PropTypes.node.isRequired,
+ children: PropTypes.node.isRequired,
+};
+
+class FilterTooltipWrapper extends React.Component {
+ constructor(props) {
+ super(props);
+
+ // internal instance variable to make tooltip show/hide have delay
+ this.isHover = false;
+ this.state = {
+ show: false,
+ };
+
+ this.showTooltip = this.showTooltip.bind(this);
+ this.hideTooltip = this.hideTooltip.bind(this);
+ this.attachRef = target => this.setState({ target });
+ }
+
+ showTooltip() {
+ this.isHover = true;
+
+ setTimeout(() => this.isHover && this.setState({ show: true }), 100);
+ }
+
+ hideTooltip() {
+ this.isHover = false;
+
+ setTimeout(() => !this.isHover && this.setState({ show: false }), 300);
+ }
+
+ render() {
+ const { show, target } = this.state;
+ return (
+
+
+
+