diff --git a/superset-frontend/src/explore/components/controls/DatasourceControl/DatasourceControl.test.tsx b/superset-frontend/src/explore/components/controls/DatasourceControl/DatasourceControl.test.tsx index baf72c653ad..1528f7c4b84 100644 --- a/superset-frontend/src/explore/components/controls/DatasourceControl/DatasourceControl.test.tsx +++ b/superset-frontend/src/explore/components/controls/DatasourceControl/DatasourceControl.test.tsx @@ -28,7 +28,7 @@ import { waitFor, } from 'spec/helpers/testing-library'; import { fallbackExploreInitialData } from 'src/explore/fixtures'; -import type { DatasetObject, ColumnObject } from 'src/features/datasets/types'; +import type { ColumnObject } from 'src/features/datasets/types'; import DatasourceControl from '.'; const SupersetClientGet = jest.spyOn(SupersetClient, 'get'); @@ -46,16 +46,19 @@ afterEach(() => { jest.clearAllMocks(); // Clears mock history but keeps spy in place }); -type TestDatasource = Omit< - Partial, - 'columns' | 'main_dttm_col' -> & { +interface TestDatasource { + id?: number; name: string; + datasource_name?: string; database: { name: string }; columns?: Partial[]; type?: DatasourceType; main_dttm_col?: string | null; -}; + owners?: Array<{ first_name: string; last_name: string; id: number; username?: string }>; + sql?: string; + metrics?: Array<{ id: number; metric_name: string }>; + [key: string]: unknown; +} const mockDatasource: TestDatasource = { id: 25, @@ -69,7 +72,9 @@ const mockDatasource: TestDatasource = { owners: [{ first_name: 'john', last_name: 'doe', id: 1, username: 'jd' }], sql: 'SELECT * FROM mock_datasource_sql', }; -const createProps = (overrides: JsonObject = {}) => ({ + +// Use type assertion for test props since the component is wrapped with withTheme +const createProps = (overrides: JsonObject = {}): Record => ({ hovered: false, type: 'DatasourceControl', label: 'Datasource', diff --git a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterControl/index.tsx b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterControl/index.tsx index 315babb3bfd..4dd2e5251e2 100644 --- a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterControl/index.tsx +++ b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterControl/index.tsx @@ -16,10 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -import { Component } from 'react'; +import { Component, ReactNode } from 'react'; import PropTypes from 'prop-types'; import { t, logging, SupersetClient, ensureIsArray } from '@superset-ui/core'; -import { withTheme } from '@apache-superset/core/ui'; +import { withTheme, type SupersetTheme } from '@apache-superset/core/ui'; import ControlHeader from 'src/explore/components/ControlHeader'; import adhocMetricType from 'src/explore/components/controls/MetricControl/adhocMetricType'; @@ -45,6 +45,59 @@ import columnType from 'src/explore/components/controls/FilterControl/columnType import { toQueryString } from 'src/utils/urlUtils'; import { Clauses, ExpressionTypes } from '../types'; +interface ColumnMeta { + column_name: string; + verbose_name?: string; + [key: string]: unknown; +} + +interface SavedMetric { + metric_name: string; + expression: string; + [key: string]: unknown; +} + +interface Datasource { + id?: number; + type?: string; + database?: { id: number }; + datasource_name?: string; + catalog?: string; + schema?: string; + is_sqllab_view?: boolean; + [key: string]: unknown; +} + +interface AdhocFilterControlProps { + label?: ReactNode; + name?: string; + sections?: string[]; + operators?: string[]; + onChange?: (values: AdhocFilter[]) => void; + value?: AdhocFilter[]; + datasource?: Datasource; + columns?: ColumnMeta[]; + savedMetrics?: SavedMetric[]; + selectedMetrics?: string | AdhocMetric | (string | AdhocMetric)[]; + isLoading?: boolean; + canDelete?: (filter: AdhocFilter, allFilters: AdhocFilter[]) => string | boolean | undefined; + theme?: SupersetTheme; +} + +interface FilterOption { + column_name?: string; + saved_metric_name?: string; + label?: string; + filterOptionName?: string; + [key: string]: unknown; +} + +interface AdhocFilterControlState { + values: AdhocFilter[]; + options: FilterOption[]; + partitionColumn: string | null; +} + const { warning } = Modal; const selectedMetricType = PropTypes.oneOfType([ @@ -78,11 +131,11 @@ const defaultProps = { selectedMetrics: [], }; -function isDictionaryForAdhocFilter(value) { - return value && !(value instanceof AdhocFilter) && value.expressionType; +function isDictionaryForAdhocFilter(value: unknown): boolean { + return !!(value && !(value instanceof AdhocFilter) && (value as Record).expressionType); } -function optionsForSelect(props) { +function optionsForSelect(props: AdhocFilterControlProps): FilterOption[] { const options = [ ...props.columns, ...ensureIsArray(props.selectedMetrics).map( @@ -121,8 +174,11 @@ function optionsForSelect(props) { ); } -class AdhocFilterControl extends Component { - constructor(props) { +class AdhocFilterControl extends Component { + optionRenderer: (option: FilterOption) => JSX.Element; + valueRenderer: (adhocFilter: AdhocFilter, index: number) => JSX.Element; + + constructor(props: AdhocFilterControlProps) { super(props); this.onRemoveFilter = this.onRemoveFilter.bind(this); this.onNewFilter = this.onNewFilter.bind(this); @@ -206,7 +262,7 @@ class AdhocFilterControl extends Component { } } - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps: AdhocFilterControlProps): void { if (this.props.columns !== prevProps.columns) { this.setState({ options: optionsForSelect(this.props) }); } @@ -219,7 +275,7 @@ class AdhocFilterControl extends Component { } } - removeFilter(index) { + removeFilter(index: number): void { const valuesCopy = [...this.state.values]; valuesCopy.splice(index, 1); this.setState(prevState => ({ @@ -229,7 +285,7 @@ class AdhocFilterControl extends Component { this.props.onChange(valuesCopy); } - onRemoveFilter(index) { + onRemoveFilter(index: number): void { const { canDelete } = this.props; const { values } = this.state; const result = canDelete?.(values[index], values); @@ -240,7 +296,7 @@ class AdhocFilterControl extends Component { this.removeFilter(index); } - onNewFilter(newFilter) { + onNewFilter(newFilter: FilterOption): void { const mappedOption = this.mapOption(newFilter); if (mappedOption) { this.setState( @@ -255,7 +311,7 @@ class AdhocFilterControl extends Component { } } - onFilterEdit(changedFilter) { + onFilterEdit(changedFilter: AdhocFilter): void { this.props.onChange( this.state.values.map(value => { if (value.filterOptionName === changedFilter.filterOptionName) { @@ -266,20 +322,20 @@ class AdhocFilterControl extends Component { ); } - onChange(opts) { + onChange(opts: FilterOption[] | null): void { const options = (opts || []) .map(option => this.mapOption(option)) .filter(option => option); this.props.onChange(options); } - getMetricExpression(savedMetricName) { + getMetricExpression(savedMetricName: string): string { return this.props.savedMetrics.find( savedMetric => savedMetric.metric_name === savedMetricName, ).expression; } - moveLabel(dragIndex, hoverIndex) { + moveLabel(dragIndex: number, hoverIndex: number): void { const { values } = this.state; const newValues = [...values]; @@ -290,7 +346,7 @@ class AdhocFilterControl extends Component { this.setState({ values: newValues }); } - mapOption(option) { + mapOption(option: FilterOption | AdhocFilter): AdhocFilter | null { // already a AdhocFilter, skip if (option instanceof AdhocFilter) { return option; @@ -331,7 +387,7 @@ class AdhocFilterControl extends Component { return null; } - addNewFilterPopoverTrigger(trigger) { + addNewFilterPopoverTrigger(trigger: ReactNode): JSX.Element { return ( a.value - b.value; +interface SelectOption { + value: string | number; + label: string; + [key: string]: unknown; +} -export const areAllValuesNumbers = (items, valueKey = 'value') => { +const numberComparator = (a: SelectOption, b: SelectOption): number => a.value as number - (b.value as number); + +export const areAllValuesNumbers = (items: unknown[], valueKey = 'value'): boolean => { if (!items || items.length === 0) { return false; } @@ -147,12 +153,14 @@ export const areAllValuesNumbers = (items, valueKey = 'value') => { }); }; +type SortComparator = ((a: SelectOption, b: SelectOption) => number) | undefined; + export const getSortComparator = ( - choices, - options, - valueKey, - explicitComparator, -) => { + choices: unknown[] | undefined, + options: unknown[] | undefined, + valueKey: string | undefined, + explicitComparator: SortComparator, +): SortComparator => { if (explicitComparator) { return explicitComparator; } @@ -167,7 +175,7 @@ export const getSortComparator = ( return undefined; }; -export const innerGetOptions = props => { +export const innerGetOptions = (props: SelectControlProps): SelectOption[] => { const { choices, optionRenderer, valueKey } = props; let options = []; if (props.options) {