diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/ColumnSelectPopover.test.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/ColumnSelectPopover.test.tsx index f5a2d613ced..f7046eb3203 100644 --- a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/ColumnSelectPopover.test.tsx +++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/ColumnSelectPopover.test.tsx @@ -17,7 +17,13 @@ * under the License. */ -import { render, fireEvent } from 'spec/helpers/testing-library'; +import { + render, + fireEvent, + screen, + userEvent, + within, +} from 'spec/helpers/testing-library'; import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import ColumnSelectPopover, { @@ -116,3 +122,123 @@ test('open with Custom SQL tab selected when there is a custom SQL selected', () expect(getByText('Simple')).toHaveAttribute('aria-selected', 'false'); expect(getByText('Custom SQL')).toHaveAttribute('aria-selected', 'true'); }); + +test('Should filter simple columns by column_name and verbose_name', async () => { + renderPopover({ + columns: [ + { column_name: 'revenue_amount', verbose_name: 'Total Sales' }, + { column_name: 'user_id', verbose_name: 'User Identifier' }, + { column_name: 'created_at', verbose_name: 'Creation Date' }, + { column_name: 'order_status', verbose_name: 'Status' }, + { column_name: 'updated_at', verbose_name: 'Last Update' }, + ], + editedColumn: undefined, + getCurrentTab: jest.fn(), + onChange: jest.fn(), + }); + + const combobox = screen.getByRole('combobox', { + name: 'Columns and metrics', + }); + + await userEvent.type(combobox, 'revenue'); + + let dropdown = document.querySelector('.rc-virtual-list') as HTMLElement; + expect(within(dropdown).getByText('Total Sales')).toBeInTheDocument(); + expect( + within(dropdown).queryByText('User Identifier'), + ).not.toBeInTheDocument(); + expect(within(dropdown).queryByText('Creation Date')).not.toBeInTheDocument(); + expect(within(dropdown).queryByText('Status')).not.toBeInTheDocument(); + expect(within(dropdown).queryByText('Last Update')).not.toBeInTheDocument(); + + await userEvent.clear(combobox); + await userEvent.type(combobox, 'Identifier'); + + dropdown = document.querySelector('.rc-virtual-list') as HTMLElement; + expect(within(dropdown).getByText('User Identifier')).toBeInTheDocument(); + expect(within(dropdown).queryByText('Total Sales')).not.toBeInTheDocument(); + expect(within(dropdown).queryByText('Creation Date')).not.toBeInTheDocument(); + + await userEvent.clear(combobox); + await userEvent.type(combobox, '_at'); + + dropdown = document.querySelector('.rc-virtual-list') as HTMLElement; + expect(within(dropdown).getByText('Creation Date')).toBeInTheDocument(); + expect(within(dropdown).getByText('Last Update')).toBeInTheDocument(); + expect(within(dropdown).queryByText('Total Sales')).not.toBeInTheDocument(); + expect( + within(dropdown).queryByText('User Identifier'), + ).not.toBeInTheDocument(); + expect(within(dropdown).queryByText('Status')).not.toBeInTheDocument(); +}); + +test('Should filter saved expressions by column_name and verbose_name', async () => { + const { container } = renderPopover({ + columns: [ + { + column_name: 'calc_revenue', + verbose_name: 'Total Sales', + expression: 'price * quantity', + }, + { + column_name: 'calc_tax', + verbose_name: 'Tax Amount', + expression: 'price * 0.1', + }, + { + column_name: 'calc_profit', + verbose_name: 'Net Profit', + expression: 'revenue - cost', + }, + { + column_name: 'calc_margin', + verbose_name: 'Profit Margin', + expression: 'profit / revenue', + }, + { + column_name: 'calc_discount', + verbose_name: 'Discount Rate', + expression: 'discount / price', + }, + ], + editedColumn: undefined, + getCurrentTab: jest.fn(), + onChange: jest.fn(), + }); + + const savedTab = container.querySelector('#adhoc-metric-edit-tabs-tab-saved'); + expect(savedTab).not.toBeNull(); + fireEvent.click(savedTab!); + + const combobox = screen.getByRole('combobox', { + name: 'Saved expressions', + }); + + await userEvent.type(combobox, 'revenue'); + + let dropdown = document.querySelector('.rc-virtual-list') as HTMLElement; + expect(within(dropdown).getByText('Total Sales')).toBeInTheDocument(); + expect(within(dropdown).queryByText('Tax Amount')).not.toBeInTheDocument(); + expect(within(dropdown).queryByText('Net Profit')).not.toBeInTheDocument(); + expect(within(dropdown).queryByText('Profit Margin')).not.toBeInTheDocument(); + expect(within(dropdown).queryByText('Discount Rate')).not.toBeInTheDocument(); + + await userEvent.clear(combobox); + await userEvent.type(combobox, 'Rate'); + + dropdown = document.querySelector('.rc-virtual-list') as HTMLElement; + expect(within(dropdown).getByText('Discount Rate')).toBeInTheDocument(); + expect(within(dropdown).queryByText('Total Sales')).not.toBeInTheDocument(); + expect(within(dropdown).queryByText('Tax Amount')).not.toBeInTheDocument(); + + await userEvent.clear(combobox); + await userEvent.type(combobox, 'profit'); + + dropdown = document.querySelector('.rc-virtual-list') as HTMLElement; + expect(within(dropdown).getByText('Net Profit')).toBeInTheDocument(); + expect(within(dropdown).getByText('Profit Margin')).toBeInTheDocument(); + expect(within(dropdown).queryByText('Total Sales')).not.toBeInTheDocument(); + expect(within(dropdown).queryByText('Tax Amount')).not.toBeInTheDocument(); + expect(within(dropdown).queryByText('Discount Rate')).not.toBeInTheDocument(); +}); diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/ColumnSelectPopover.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/ColumnSelectPopover.tsx index 4a9ccd63269..e2246533535 100644 --- a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/ColumnSelectPopover.tsx +++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/ColumnSelectPopover.tsx @@ -427,8 +427,12 @@ const ColumnSelectPopover = ({ /> ), key: calculatedColumn.column_name, + column_name: calculatedColumn.column_name, + verbose_name: + calculatedColumn.verbose_name ?? '', }), )} + optionFilterProps={['column_name', 'verbose_name']} /> ) : datasourceType === DatasourceType.Table ? ( @@ -544,6 +548,8 @@ const ColumnSelectPopover = ({ /> ), key: `column-${simpleColumn.column_name}`, + column_name: simpleColumn.column_name, + verbose_name: simpleColumn.verbose_name ?? '', })), ...availableMetrics.map(metric => ({ value: metric.metric_name, @@ -556,8 +562,15 @@ const ColumnSelectPopover = ({ ), key: `metric-${metric.metric_name}`, + metric_name: metric.metric_name, + verbose_name: metric.verbose_name ?? '', })), ]} + optionFilterProps={[ + 'column_name', + 'verbose_name', + 'metric_name', + ]} /> )} diff --git a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopover/AdhocMetricEditPopover.test.tsx b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopover/AdhocMetricEditPopover.test.tsx index 81b3649be44..a18346ffa0f 100644 --- a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopover/AdhocMetricEditPopover.test.tsx +++ b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopover/AdhocMetricEditPopover.test.tsx @@ -21,6 +21,7 @@ import { screen, selectOption, userEvent, + within, } from 'spec/helpers/testing-library'; import AdhocMetric from 'src/explore/components/controls/MetricControl/AdhocMetric'; import AdhocMetricEditPopover from '.'; @@ -248,3 +249,160 @@ test('Should render "Custom SQL" tab correctly', async () => { expect(await screen.findByRole('textbox')).toBeInTheDocument(); }); + +test('Should filter saved metrics by metric_name and verbose_name', async () => { + const props = { + ...createProps(), + savedMetricsOptions: [ + { + id: 1, + metric_name: 'count', + expression: 'COUNT(*)', + verbose_name: 'Total Count', + }, + { + id: 2, + metric_name: 'revenue_sum', + expression: 'sum(revenue)', + verbose_name: 'Gross Revenue', + }, + { + id: 3, + metric_name: 'avg_price', + expression: 'AVG(price)', + verbose_name: 'Average Price', + }, + { + id: 4, + metric_name: 'user_count', + expression: 'COUNT(DISTINCT user_id)', + verbose_name: 'Unique Users', + }, + { + id: 5, + metric_name: 'total_quantity', + expression: 'SUM(quantity)', + verbose_name: 'Total Quantity', + }, + ], + }; + render(); + + const combobox = screen.getByRole('combobox', { + name: 'Select saved metrics', + }); + userEvent.click(combobox); + + await userEvent.type(combobox, 'revenue'); + + let dropdown = document.querySelector('.rc-virtual-list') as HTMLElement; + expect(within(dropdown).getByText('Gross Revenue')).toBeInTheDocument(); + expect(within(dropdown).queryByText('Total Count')).not.toBeInTheDocument(); + expect(within(dropdown).queryByText('Average Price')).not.toBeInTheDocument(); + expect(within(dropdown).queryByText('Unique Users')).not.toBeInTheDocument(); + expect( + within(dropdown).queryByText('Total Quantity'), + ).not.toBeInTheDocument(); + + await userEvent.clear(combobox); + await userEvent.type(combobox, 'Unique'); + + dropdown = document.querySelector('.rc-virtual-list') as HTMLElement; + expect(within(dropdown).getByText('Unique Users')).toBeInTheDocument(); + expect(within(dropdown).queryByText('Total Count')).not.toBeInTheDocument(); + expect(within(dropdown).queryByText('Gross Revenue')).not.toBeInTheDocument(); + + await userEvent.clear(combobox); + await userEvent.type(combobox, 'total'); + + dropdown = document.querySelector('.rc-virtual-list') as HTMLElement; + expect(within(dropdown).getByText('Total Count')).toBeInTheDocument(); + expect(within(dropdown).getByText('Total Quantity')).toBeInTheDocument(); + expect(within(dropdown).queryByText('Gross Revenue')).not.toBeInTheDocument(); + expect(within(dropdown).queryByText('Average Price')).not.toBeInTheDocument(); + expect(within(dropdown).queryByText('Unique Users')).not.toBeInTheDocument(); +}); + +test('Should filter columns by column_name and verbose_name in Simple tab', async () => { + const props = { + ...createProps(), + columns: [ + { + id: 1, + column_name: 'user_id', + verbose_name: 'User Identifier', + type: 'INTEGER', + }, + { + id: 2, + column_name: 'created_at', + verbose_name: 'Creation Timestamp', + type: 'DATETIME', + }, + { + id: 3, + column_name: 'order_total', + verbose_name: 'Order Amount', + type: 'DECIMAL', + }, + { + id: 4, + column_name: 'product_name', + verbose_name: 'Product Title', + type: 'STRING', + }, + { + id: 5, + column_name: 'updated_at', + verbose_name: 'Last Modified', + type: 'DATETIME', + }, + ], + }; + props.getCurrentTab.mockImplementation(tab => { + props.adhocMetric.expressionType = tab; + }); + render(); + + const tab = screen.getByRole('tab', { name: 'Simple' }).parentElement!; + userEvent.click(tab); + + const columnCombobox = screen.getByRole('combobox', { + name: 'Select column', + }); + + await userEvent.type(columnCombobox, 'product'); + + let dropdown = document.querySelector('.rc-virtual-list') as HTMLElement; + expect(within(dropdown).getByText('Product Title')).toBeInTheDocument(); + expect( + within(dropdown).queryByText('User Identifier'), + ).not.toBeInTheDocument(); + expect( + within(dropdown).queryByText('Creation Timestamp'), + ).not.toBeInTheDocument(); + expect(within(dropdown).queryByText('Order Amount')).not.toBeInTheDocument(); + expect(within(dropdown).queryByText('Last Modified')).not.toBeInTheDocument(); + + await userEvent.clear(columnCombobox); + await userEvent.type(columnCombobox, 'Modified'); + + dropdown = document.querySelector('.rc-virtual-list') as HTMLElement; + expect(within(dropdown).getByText('Last Modified')).toBeInTheDocument(); + expect( + within(dropdown).queryByText('User Identifier'), + ).not.toBeInTheDocument(); + expect(within(dropdown).queryByText('Product Title')).not.toBeInTheDocument(); + + await userEvent.clear(columnCombobox); + await userEvent.type(columnCombobox, '_at'); + + dropdown = document.querySelector('.rc-virtual-list') as HTMLElement; + expect(within(dropdown).getByText('Creation Timestamp')).toBeInTheDocument(); + expect(within(dropdown).getByText('Last Modified')).toBeInTheDocument(); + expect( + within(dropdown).queryByText('User Identifier'), + ).not.toBeInTheDocument(); + expect(within(dropdown).queryByText('Order Amount')).not.toBeInTheDocument(); + expect(within(dropdown).queryByText('Product Title')).not.toBeInTheDocument(); +}); diff --git a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopover/index.tsx b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopover/index.tsx index fc90dbf4eb0..0eaa5b23b09 100644 --- a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopover/index.tsx +++ b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopover/index.tsx @@ -450,8 +450,11 @@ export default class AdhocMetricEditPopover extends PureComponent< value: savedMetric.metric_name, label: this.renderMetricOption(savedMetric), key: savedMetric.id, + metric_name: savedMetric.metric_name, + verbose_name: savedMetric.verbose_name ?? '', }), )} + optionFilterProps={['metric_name', 'verbose_name']} {...savedSelectProps} /> @@ -509,7 +512,10 @@ export default class AdhocMetricEditPopover extends PureComponent< value: column.column_name, key: (column as { id?: unknown }).id, label: this.renderColumnOption(column), + column_name: column.column_name, + verbose_name: column.verbose_name ?? '', }))} + optionFilterProps={['column_name', 'verbose_name']} {...columnSelectProps} />