diff --git a/superset-frontend/cypress-base/cypress/integration/dashboard/nativeFilters.test.ts b/superset-frontend/cypress-base/cypress/integration/dashboard/nativeFilters.test.ts index da893f4c528f7..5914a3c76af27 100644 --- a/superset-frontend/cypress-base/cypress/integration/dashboard/nativeFilters.test.ts +++ b/superset-frontend/cypress-base/cypress/integration/dashboard/nativeFilters.test.ts @@ -20,6 +20,7 @@ import qs from 'querystring'; import { dashboardView, nativeFilters } from 'cypress/support/directories'; import { testItems } from './dashboard.helper'; import { DASHBOARD_LIST } from '../dashboard_list/dashboard_list.helper'; +import { CHART_LIST } from '../chart_list/chart_list.helper'; const getTestTitle = ( test: Mocha.Suite = (Cypress as any).mocha.getRunner().suite.ctx.test, @@ -43,7 +44,8 @@ describe('Nativefilters Sanity test', () => { ).then(xhr => { const dashboards = xhr.body.result; const worldBankDashboard = dashboards.find( - d => d.dashboard_title === "World Bank's Data", + (d: { dashboard_title: string }) => + d.dashboard_title === "World Bank's Data", ); cy.visit(worldBankDashboard.url); }); @@ -65,7 +67,8 @@ describe('Nativefilters Sanity test', () => { ).then(xhr => { const dashboards = xhr.body.result; const testDashboard = dashboards.find( - d => d.dashboard_title === testItems.dashboard, + (d: { dashboard_title: string }) => + d.dashboard_title === testItems.dashboard, ); cy.visit(testDashboard.url); }); @@ -107,13 +110,14 @@ describe('Nativefilters Sanity test', () => { cy.get(nativeFilters.createFilterButton).should('be.visible').click(); cy.get(nativeFilters.modal.container) .find(nativeFilters.filtersPanel.filterName) - .click() + .click({ force: true }) .type('Country name'); cy.get(nativeFilters.modal.container) .find(nativeFilters.filtersPanel.datasetName) - .click() - .type('wb_health_population{enter}'); - + .click({ force: true }) + .within(() => + cy.get('input').type('wb_health_population{enter}', { force: true }), + ); // Add following step to avoid flaky enter value in line 177 cy.get(nativeFilters.filtersPanel.inputDropdown) .should('be.visible', { timeout: 20000 }) @@ -163,7 +167,7 @@ describe('Nativefilters Sanity test', () => { cy.get(nativeFilters.createFilterButton).click({ force: true }); cy.get(nativeFilters.modal.container) .find(nativeFilters.filtersPanel.filterName) - .click() + .click({ force: true }) .type('suffix'); cy.get(nativeFilters.modal.container) .find(nativeFilters.filtersPanel.datasetName) @@ -218,12 +222,14 @@ describe('Nativefilters Sanity test', () => { cy.get(nativeFilters.modal.container).should('be.visible'); cy.get(nativeFilters.modal.container) .find(nativeFilters.filtersPanel.filterName) - .click() + .click({ force: true }) .type('Country name'); cy.get(nativeFilters.modal.container) .find(nativeFilters.filtersPanel.datasetName) - .click() - .type('wb_health_population{enter}'); + .click({ force: true }) + .within(() => + cy.get('input').type('wb_health_population{enter}', { force: true }), + ); cy.get('.loading inline-centered css-101mkpk').should('not.exist'); // hack for unclickable country_name @@ -255,53 +261,42 @@ describe('Nativefilters Sanity test', () => { cy.get(nativeFilters.filterFromDashboardView.expand).click({ force: true }); cy.get(nativeFilters.createFilterButton).should('be.visible').click(); cy.get(nativeFilters.modal.container).should('be.visible'); - cy.get(nativeFilters.filterConfigurationSections.collapseExpandButton) - .last() - .click(); [ 'Filter has default value', - 'Multiple select', - 'Required', + 'Can select multiple values', + 'Filter value is required', 'Filter is hierarchical', - 'Default to first item', + 'Select first filter value by default', 'Inverse selection', - 'Search all filter options', + 'Dynamically search all filter values', 'Pre-filter available values', 'Sort filter values', ].forEach(el => { cy.contains(el); }); cy.get(nativeFilters.filterConfigurationSections.checkedCheckbox).contains( - 'Multiple select', + 'Can select multiple values', ); cy.get(nativeFilters.filterConfigurationSections.infoTooltip) .eq(0) - .trigger('mouseover'); - cy.contains('Allow selecting multiple values'); + .trigger('mouseover', { force: true }); + cy.contains('User must select a value before applying the filter'); cy.get(nativeFilters.filterConfigurationSections.infoTooltip) .eq(1) - .trigger('mouseover'); - cy.contains('User must select a value before applying the filter'); + .trigger('mouseover', { force: true }); + cy.contains('When using this option, default value can’t be set'); cy.get(nativeFilters.filterConfigurationSections.infoTooltip) .eq(2) - .trigger('mouseover'); + .trigger('mouseover', { force: true }); cy.contains( - 'Select first item by default (when using this option, default value can’t be set)', + 'By default, each filter loads at most 1000 choices at the initial page load. Check this box if you have more than 1000 filter values and want to enable dynamically searching that loads filter values as users type (may add stress to your database).', ); - cy.get(nativeFilters.filterConfigurationSections.infoTooltip) .eq(3) - .trigger('mouseover'); + .trigger('mouseover', { force: true }); cy.contains('Exclude selected values'); - - cy.get(nativeFilters.filterConfigurationSections.infoTooltip) - .eq(4) - .trigger('mouseover'); - cy.contains( - 'By default, each filter loads at most 1000 choices at the initial page load. Check this box if you have more than 1000 filter values and want to enable dynamically searching that loads filter values as users type (may add stress to your database).', - ); }); it("User can check 'Filter has default value'", () => { cy.get(nativeFilters.filterFromDashboardView.expand).click({ force: true }); @@ -312,11 +307,13 @@ describe('Nativefilters Sanity test', () => { cy.get(nativeFilters.modal.container) .find(nativeFilters.filtersPanel.datasetName) - .click() - .type('wb_health_population{enter}'); + .click({ force: true }) + .within(() => + cy.get('input').type('wb_health_population{enter}', { force: true }), + ); cy.get(nativeFilters.modal.container) .find(nativeFilters.filtersPanel.filterName) - .click() + .click({ force: true }) .type('country_name'); // hack for unclickable datetime cy.wait(5000); diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FilterConfigPane.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FilterConfigPane.test.tsx index 41484327151d0..3188bbe771e33 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FilterConfigPane.test.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FilterConfigPane.test.tsx @@ -100,7 +100,7 @@ test('remove filter', async () => { test('add filter', async () => { defaultRender(); // First trash icon - const addButton = screen.getByText('Add')!; + const addButton = screen.getByText('Add filters and dividers')!; fireEvent.mouseOver(addButton); const addFilterButton = await screen.findByText('Filter'); @@ -118,7 +118,7 @@ test('add filter', async () => { test('add divider', async () => { defaultRender(); - const addButton = screen.getByText('Add')!; + const addButton = screen.getByText('Add filters and dividers')!; fireEvent.mouseOver(addButton); const addFilterButton = await screen.findByText('Divider'); await act(async () => { diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FilterTitlePane.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FilterTitlePane.tsx index a90f8dc1d2a02..1a1f4cd121bb9 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FilterTitlePane.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FilterTitlePane.tsx @@ -36,27 +36,13 @@ interface Props { erroredFilters: string[]; } -const StyledPlusButton = styled.div` - color: ${({ theme }) => theme.colors.primary.dark1}; -`; - -const StyledHeader = styled.div` - ${({ theme }) => ` - color: ${theme.colors.grayscale.dark1}; - font-size: ${theme.typography.sizes.l}px; - padding-top: ${theme.gridUnit * 4}px; - padding-right: ${theme.gridUnit * 4}px; - padding-left: ${theme.gridUnit * 4}px; - padding-bottom: ${theme.gridUnit * 2}px; - `} -`; - const StyledAddBox = styled.div` ${({ theme }) => ` cursor: pointer; margin: ${theme.gridUnit * 4}px; + color: ${theme.colors.primary.base}; &:hover { - color: ${theme.colors.primary.base}; + color: ${theme.colors.primary.dark1}; } `} `; @@ -104,7 +90,12 @@ const FilterTitlePane: React.FC = ({ ); return ( - Filters + + +
{' '} + {t('Add filters and dividers')} + +
= ({ restoreFilter={restoreFilter} />
- - - {' '} - {t('Add')} - - ); }; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/FilterScope.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/FilterScope.test.tsx index 4600bb292fb61..71f05dd5ba4d7 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/FilterScope.test.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/FilterScope.test.tsx @@ -43,7 +43,7 @@ describe('FilterScope', () => { save, removedFilters: {}, handleActiveFilterPanelChange: jest.fn(), - activeFilterPanelKeys: `DefaultFilterId-${FilterPanels.basic.key}`, + activeFilterPanelKeys: `DefaultFilterId-${FilterPanels.configuration.key}`, isActive: true, }; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx index 37ae2ca8788ba..387c18e5f145b 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx @@ -45,6 +45,7 @@ import React, { useMemo, useState, } from 'react'; +import { PluginFilterSelectCustomizeProps } from 'src/filters/components/Select/types'; import { useSelector } from 'react-redux'; import { getChartDataRequest } from 'src/chart/chartAction'; import { Input, TextArea } from 'src/common/components'; @@ -118,6 +119,16 @@ const StyledRowContainer = styled.div` padding: 0px ${({ theme }) => theme.gridUnit * 4}px; `; +type ControlKey = keyof PluginFilterSelectCustomizeProps; + +const controlsOrder: ControlKey[] = [ + 'enableEmptyFilter', + 'defaultToFirstItem', + 'multiSelect', + 'searchAllOptions', + 'inverseSelection', +]; + export const StyledFormItem = styled(FormItem)` width: 49%; margin-bottom: ${({ theme }) => theme.gridUnit * 4}px; @@ -249,7 +260,7 @@ const StyledAsterisk = styled.span` const FilterTabs = { configuration: { key: 'configuration', - name: t('Configuration'), + name: t('Settings'), }, scoping: { key: 'scoping', @@ -258,13 +269,13 @@ const FilterTabs = { }; export const FilterPanels = { - basic: { - key: 'basic', - name: t('Basic'), + configuration: { + key: 'configuration', + name: t('Filter Configuration'), }, - advanced: { - key: 'advanced', - name: t('Advanced'), + settings: { + key: 'settings', + name: t('Filter Settings'), }, }; @@ -287,8 +298,6 @@ export interface FiltersConfigFormProps { const FILTERS_WITH_ADHOC_FILTERS = ['filter_select', 'filter_range']; -const BASIC_CONTROL_ITEMS = ['enableEmptyFilter', 'multiSelect']; - // TODO: Rename the filter plugins and remove this mapping const FILTER_TYPE_NAME_MAPPING = { [t('Select filter')]: t('Value'), @@ -319,7 +328,6 @@ const FiltersConfigForm = ( form, parentFilters, activeFilterPanelKeys, - isActive, restoreFilter, onFilterHierarchyChange, handleActiveFilterPanelChange, @@ -607,10 +615,6 @@ const FiltersConfigForm = ( const defaultToFirstItem = formFilter?.controlValues?.defaultToFirstItem; - const hasAdvancedSection = - formFilter?.filterType === 'filter_select' || - formFilter?.filterType === 'filter_range'; - const initialDefaultValue = formFilter?.filterType === filterToEdit?.filterType ? filterToEdit?.defaultDataMask @@ -690,28 +694,6 @@ const FiltersConfigForm = ( showDataset, ]); - useEffect(() => { - // Run only once - if (isActive) { - const hasCheckedAdvancedControl = - hasParentFilter || - hasPreFilter || - hasSorting || - hasEnableSingleValue || - Object.keys(controlItems) - .filter(key => !BASIC_CONTROL_ITEMS.includes(key)) - .some(key => controlItems[key].checked); - handleActiveFilterPanelChange( - hasCheckedAdvancedControl - ? [ - `${filterId}-${FilterPanels.basic.key}`, - `${filterId}-${FilterPanels.advanced.key}`, - ] - : `${filterId}-${FilterPanels.basic.key}`, - ); - } - }, [isActive]); - const initiallyExcludedCharts = useMemo(() => { const excluded: number[] = []; if (formFilter?.dataset?.value === undefined) { @@ -879,9 +861,280 @@ const FiltersConfigForm = ( > + {isCascadingFilter && ( + + { + formChanged(); + // execute after render + setTimeout(() => { + if (checked) { + form.validateFields([ + ['filters', filterId, 'parentFilter'], + ]); + } else { + setNativeFilterFieldValues(form, filterId, { + parentFilter: undefined, + }); + } + onFilterHierarchyChange( + filterId, + checked + ? form.getFieldValue('filters')[filterId].parentFilter + : undefined, + ); + }, 0); + }} + > + {t('Parent filter')}} + initialValue={parentFilter} + normalize={value => (value ? { value } : undefined)} + data-test="parent-filter-input" + required + rules={[ + { + required: true, + message: t('Parent filter is required'), + }, + ]} + > + + + + + )} + {hasDataset && hasAdditionalFilters && ( + + { + formChanged(); + if (checked) { + validatePreFilter(); + } + }} + > + + c.filterable, + ) || [] + } + savedMetrics={datasetDetails?.metrics || []} + datasource={datasetDetails} + onChange={(filters: AdhocFilter[]) => { + setNativeFilterFieldValues(form, filterId, { + adhoc_filters: filters, + }); + forceUpdate(); + validatePreFilter(); + }} + label={ + + {t('Pre-filter')} + {!hasTimeRange && } + + } + /> + + {showTimeRangePicker && ( + {t('Time range')}} + initialValue={filterToEdit?.time_range || 'No filter'} + required={!hasAdhoc} + rules={[ + { + validator: preFilterValidator, + }, + ]} + > + { + setNativeFilterFieldValues(form, filterId, { + time_range: timeRange, + }); + forceUpdate(); + validatePreFilter(); + }} + /> + + )} + {hasTimeRange && ( + + {t('Time column')}  + + + } + initialValue={filterToEdit?.granularity_sqla} + > + !!column.is_dttm} + datasetId={datasetId} + onChange={column => { + // We need reset default value when when column changed + setNativeFilterFieldValues(form, filterId, { + granularity_sqla: column, + }); + forceUpdate(); + }} + /> + + )} + + + )} + {formFilter?.filterType !== 'filter_range' ? ( + + { + onSortChanged(checked || undefined); + formChanged(); + }} + > + {t('Sort type')}} + > + { + onSortChanged(value.target.value); + }} + > + {t('Sort ascending')} + {t('Sort descending')} + + + {hasMetrics && ( + + {t('Sort Metric')}  + + + } + data-test="field-input" + > +