refactor(explore): improve typing for Dnd controls (#16362)

This commit is contained in:
Jesse Yang 2021-08-26 01:23:14 -07:00 committed by GitHub
parent 18be181946
commit ec087507e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 237 additions and 168 deletions

View File

@ -57,12 +57,15 @@ export type DashboardLayoutState = { present: DashboardLayout };
export type DashboardState = { export type DashboardState = {
preselectNativeFilters?: JsonObject; preselectNativeFilters?: JsonObject;
editMode: boolean; editMode: boolean;
isPublished: boolean;
directPathToChild: string[]; directPathToChild: string[];
activeTabs: ActiveTabs; activeTabs: ActiveTabs;
fullSizeChartId: number | null; fullSizeChartId: number | null;
isRefreshing: boolean; isRefreshing: boolean;
hasUnsavedChanges: boolean;
}; };
export type DashboardInfo = { export type DashboardInfo = {
id: number;
common: { common: {
flash_messages: string[]; flash_messages: string[];
conf: JsonObject; conf: JsonObject;

View File

@ -18,6 +18,7 @@
*/ */
import React, { ReactNode, useCallback, useState } from 'react'; import React, { ReactNode, useCallback, useState } from 'react';
import { ControlType } from '@superset-ui/chart-controls'; import { ControlType } from '@superset-ui/chart-controls';
import { ControlComponentProps as BaseControlComponentProps } from '@superset-ui/chart-controls/lib/shared-controls/components/types';
import { JsonValue, QueryFormData } from '@superset-ui/core'; import { JsonValue, QueryFormData } from '@superset-ui/core';
import ErrorBoundary from 'src/components/ErrorBoundary'; import ErrorBoundary from 'src/components/ErrorBoundary';
import { ExploreActions } from 'src/explore/actions/exploreActions'; import { ExploreActions } from 'src/explore/actions/exploreActions';
@ -43,6 +44,13 @@ export type ControlProps = {
renderTrigger?: boolean; renderTrigger?: boolean;
}; };
/**
*
*/
export type ControlComponentProps<
ValueType extends JsonValue = JsonValue
> = Omit<ControlProps, 'value'> & BaseControlComponentProps<ValueType>;
export default function Control(props: ControlProps) { export default function Control(props: ControlProps) {
const { const {
actions: { setControlValue }, actions: { setControlValue },

View File

@ -26,8 +26,8 @@ import Icons from 'src/components/Icons';
const propTypes = { const propTypes = {
name: PropTypes.string, name: PropTypes.string,
label: PropTypes.string, label: PropTypes.node,
description: PropTypes.string, description: PropTypes.node,
validationErrors: PropTypes.array, validationErrors: PropTypes.array,
renderTrigger: PropTypes.bool, renderTrigger: PropTypes.bool,
rightNode: PropTypes.node, rightNode: PropTypes.node,

View File

@ -45,7 +45,7 @@ const datasource = {
datasource_name: 'table1', datasource_name: 'table1',
description: 'desc', description: 'desc',
}; };
const props = { const props: DatasourcePanelProps = {
datasource, datasource,
controls: { controls: {
datasource: { datasource: {
@ -57,12 +57,7 @@ const props = {
}, },
}, },
actions: { actions: {
setControlValue: () => ({ setControlValue: jest.fn(),
type: 'type',
controlName: 'control',
value: 'val',
validationErrors: [],
}),
}, },
}; };

View File

@ -16,13 +16,7 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import React, { import React, { useEffect, useMemo, useRef, useState } from 'react';
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { ControlConfig, DatasourceMeta } from '@superset-ui/chart-controls'; import { ControlConfig, DatasourceMeta } from '@superset-ui/chart-controls';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { matchSorter, rankings } from 'match-sorter'; import { matchSorter, rankings } from 'match-sorter';
@ -193,60 +187,61 @@ export default function DataSourcePanel({
const DEFAULT_MAX_COLUMNS_LENGTH = 50; const DEFAULT_MAX_COLUMNS_LENGTH = 50;
const DEFAULT_MAX_METRICS_LENGTH = 50; const DEFAULT_MAX_METRICS_LENGTH = 50;
const search = useCallback( const search = useMemo(
debounce((value: string) => { () =>
if (value === '') { debounce((value: string) => {
setList({ columns, metrics }); if (value === '') {
return; setList({ columns, metrics });
} return;
setList({ }
columns: matchSorter(columns, value, { setList({
keys: [ columns: matchSorter(columns, value, {
{ keys: [
key: 'verbose_name', {
threshold: rankings.CONTAINS, key: 'verbose_name',
}, threshold: rankings.CONTAINS,
{ },
key: 'column_name', {
threshold: rankings.CONTAINS, key: 'column_name',
}, threshold: rankings.CONTAINS,
{ },
key: item => {
[item.description, item.expression].map( key: item =>
x => x?.replace(/[_\n\s]+/g, ' ') || '', [item.description, item.expression].map(
), x => x?.replace(/[_\n\s]+/g, ' ') || '',
threshold: rankings.CONTAINS, ),
maxRanking: rankings.CONTAINS, threshold: rankings.CONTAINS,
}, maxRanking: rankings.CONTAINS,
], },
keepDiacritics: true, ],
}), keepDiacritics: true,
metrics: matchSorter(metrics, value, { }),
keys: [ metrics: matchSorter(metrics, value, {
{ keys: [
key: 'verbose_name', {
threshold: rankings.CONTAINS, key: 'verbose_name',
}, threshold: rankings.CONTAINS,
{ },
key: 'metric_name', {
threshold: rankings.CONTAINS, key: 'metric_name',
}, threshold: rankings.CONTAINS,
{ },
key: item => {
[item.description, item.expression].map( key: item =>
x => x?.replace(/[_\n\s]+/g, ' ') || '', [item.description, item.expression].map(
), x => x?.replace(/[_\n\s]+/g, ' ') || '',
threshold: rankings.CONTAINS, ),
maxRanking: rankings.CONTAINS, threshold: rankings.CONTAINS,
}, maxRanking: rankings.CONTAINS,
], },
keepDiacritics: true, ],
baseSort: (a, b) => keepDiacritics: true,
Number(b.item.is_certified) - Number(a.item.is_certified) || baseSort: (a, b) =>
String(a.rankedValue).localeCompare(b.rankedValue), Number(b.item.is_certified) - Number(a.item.is_certified) ||
}), String(a.rankedValue).localeCompare(b.rankedValue),
}); }),
}, FAST_DEBOUNCE), });
}, FAST_DEBOUNCE),
[columns, metrics], [columns, metrics],
); );

View File

@ -18,13 +18,17 @@
*/ */
import React from 'react'; import React from 'react';
import { render, screen } from 'spec/helpers/testing-library'; import { render, screen } from 'spec/helpers/testing-library';
import { LabelProps } from 'src/explore/components/controls/DndColumnSelectControl/types'; import {
import { DndColumnSelect } from 'src/explore/components/controls/DndColumnSelectControl/DndColumnSelect'; DndColumnSelect,
DndColumnSelectProps,
} from 'src/explore/components/controls/DndColumnSelectControl/DndColumnSelect';
const defaultProps: LabelProps = { const defaultProps: DndColumnSelectProps = {
type: 'DndColumnSelect',
name: 'Filter', name: 'Filter',
onChange: jest.fn(), onChange: jest.fn(),
options: { string: { column_name: 'Column A' } }, options: { string: { column_name: 'Column A' } },
actions: { setControlValue: jest.fn() },
}; };
test('renders with default props', () => { test('renders with default props', () => {

View File

@ -20,7 +20,6 @@ import React, { useCallback, useMemo, useState } from 'react';
import { FeatureFlag, isFeatureEnabled, tn } from '@superset-ui/core'; import { FeatureFlag, isFeatureEnabled, tn } from '@superset-ui/core';
import { ColumnMeta } from '@superset-ui/chart-controls'; import { ColumnMeta } from '@superset-ui/chart-controls';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { LabelProps } from 'src/explore/components/controls/DndColumnSelectControl/types';
import DndSelectLabel from 'src/explore/components/controls/DndColumnSelectControl/DndSelectLabel'; import DndSelectLabel from 'src/explore/components/controls/DndColumnSelectControl/DndSelectLabel';
import OptionWrapper from 'src/explore/components/controls/DndColumnSelectControl/OptionWrapper'; import OptionWrapper from 'src/explore/components/controls/DndColumnSelectControl/OptionWrapper';
import { OptionSelector } from 'src/explore/components/controls/DndColumnSelectControl/utils'; import { OptionSelector } from 'src/explore/components/controls/DndColumnSelectControl/utils';
@ -28,8 +27,13 @@ import { DatasourcePanelDndItem } from 'src/explore/components/DatasourcePanel/t
import { DndItemType } from 'src/explore/components/DndItemType'; import { DndItemType } from 'src/explore/components/DndItemType';
import { useComponentDidUpdate } from 'src/common/hooks/useComponentDidUpdate'; import { useComponentDidUpdate } from 'src/common/hooks/useComponentDidUpdate';
import ColumnSelectPopoverTrigger from './ColumnSelectPopoverTrigger'; import ColumnSelectPopoverTrigger from './ColumnSelectPopoverTrigger';
import { DndControlProps } from './types';
export const DndColumnSelect = (props: LabelProps) => { export type DndColumnSelectProps = DndControlProps<string> & {
options: Record<string, ColumnMeta>;
};
export function DndColumnSelect(props: DndColumnSelectProps) {
const { const {
value, value,
options, options,
@ -68,6 +72,7 @@ export const DndColumnSelect = (props: LabelProps) => {
) { ) {
onChange(optionSelectorValues); onChange(optionSelectorValues);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify(value), JSON.stringify(optionSelector.getValues())]); }, [JSON.stringify(value), JSON.stringify(optionSelector.getValues())]);
// useComponentDidUpdate to avoid running this for the first render, to avoid // useComponentDidUpdate to avoid running this for the first render, to avoid
@ -203,7 +208,7 @@ export const DndColumnSelect = (props: LabelProps) => {
return ( return (
<div> <div>
<DndSelectLabel<string | string[], ColumnMeta[]> <DndSelectLabel
onDrop={onDrop} onDrop={onDrop}
canDrop={canDrop} canDrop={canDrop}
valuesRenderer={valuesRenderer} valuesRenderer={valuesRenderer}
@ -229,4 +234,4 @@ export const DndColumnSelect = (props: LabelProps) => {
</ColumnSelectPopoverTrigger> </ColumnSelectPopoverTrigger>
</div> </div>
); );
}; }

View File

@ -23,17 +23,24 @@ import AdhocMetric from 'src/explore/components/controls/MetricControl/AdhocMetr
import AdhocFilter, { import AdhocFilter, {
EXPRESSION_TYPES, EXPRESSION_TYPES,
} from 'src/explore/components/controls/FilterControl/AdhocFilter'; } from 'src/explore/components/controls/FilterControl/AdhocFilter';
import { DndFilterSelect } from 'src/explore/components/controls/DndColumnSelectControl/DndFilterSelect'; import {
DndFilterSelect,
DndFilterSelectProps,
} from 'src/explore/components/controls/DndColumnSelectControl/DndFilterSelect';
import { PLACEHOLDER_DATASOURCE } from 'src/dashboard/constants';
import { DEFAULT_FORM_DATA } from '@superset-ui/plugin-chart-echarts/lib/Timeseries/types';
const defaultProps = { const defaultProps: DndFilterSelectProps = {
type: 'DndFilterSelect',
name: 'Filter', name: 'Filter',
value: [], value: [],
columns: [], columns: [],
datasource: {}, datasource: PLACEHOLDER_DATASOURCE,
formData: {}, formData: null,
savedMetrics: [], savedMetrics: [],
selectedMetrics: [],
onChange: jest.fn(), onChange: jest.fn(),
options: { string: { column_name: 'Column' } }, actions: { setControlValue: jest.fn() },
}; };
test('renders with default props', () => { test('renders with default props', () => {
@ -53,9 +60,15 @@ test('renders with value', () => {
}); });
test('renders options with saved metric', () => { test('renders options with saved metric', () => {
render(<DndFilterSelect {...defaultProps} formData={['saved_metric']} />, { render(
useDnd: true, <DndFilterSelect
}); {...defaultProps}
formData={{ ...DEFAULT_FORM_DATA, metrics: ['saved_metric'] }}
/>,
{
useDnd: true,
},
);
expect(screen.getByText('Drop columns or metrics here')).toBeInTheDocument(); expect(screen.getByText('Drop columns or metrics here')).toBeInTheDocument();
}); });
@ -84,8 +97,14 @@ test('renders options with adhoc metric', () => {
expression: 'AVG(birth_names.num)', expression: 'AVG(birth_names.num)',
metric_name: 'avg__num', metric_name: 'avg__num',
}); });
render(<DndFilterSelect {...defaultProps} formData={[adhocMetric]} />, { render(
useDnd: true, <DndFilterSelect
}); {...defaultProps}
formData={{ ...DEFAULT_FORM_DATA, metrics: [adhocMetric] }}
/>,
{
useDnd: true,
},
);
expect(screen.getByText('Drop columns or metrics here')).toBeInTheDocument(); expect(screen.getByText('Drop columns or metrics here')).toBeInTheDocument();
}); });

View File

@ -22,6 +22,8 @@ import {
isFeatureEnabled, isFeatureEnabled,
logging, logging,
Metric, Metric,
QueryFormData,
QueryFormMetric,
SupersetClient, SupersetClient,
t, t,
} from '@superset-ui/core'; } from '@superset-ui/core';
@ -30,11 +32,8 @@ import {
OPERATOR_ENUM_TO_OPERATOR_TYPE, OPERATOR_ENUM_TO_OPERATOR_TYPE,
Operators, Operators,
} from 'src/explore/constants'; } from 'src/explore/constants';
import { OptionSortType } from 'src/explore/types'; import { Datasource, OptionSortType } from 'src/explore/types';
import { import { OptionValueType } from 'src/explore/components/controls/DndColumnSelectControl/types';
DndFilterSelectProps,
OptionValueType,
} from 'src/explore/components/controls/DndColumnSelectControl/types';
import AdhocFilterPopoverTrigger from 'src/explore/components/controls/FilterControl/AdhocFilterPopoverTrigger'; import AdhocFilterPopoverTrigger from 'src/explore/components/controls/FilterControl/AdhocFilterPopoverTrigger';
import OptionWrapper from 'src/explore/components/controls/DndColumnSelectControl/OptionWrapper'; import OptionWrapper from 'src/explore/components/controls/DndColumnSelectControl/OptionWrapper';
import DndSelectLabel from 'src/explore/components/controls/DndColumnSelectControl/DndSelectLabel'; import DndSelectLabel from 'src/explore/components/controls/DndColumnSelectControl/DndSelectLabel';
@ -48,6 +47,7 @@ import {
DndItemValue, DndItemValue,
} from 'src/explore/components/DatasourcePanel/types'; } from 'src/explore/components/DatasourcePanel/types';
import { DndItemType } from 'src/explore/components/DndItemType'; import { DndItemType } from 'src/explore/components/DndItemType';
import { ControlComponentProps } from 'src/explore/components/Control';
const DND_ACCEPTED_TYPES = [ const DND_ACCEPTED_TYPES = [
DndItemType.Column, DndItemType.Column,
@ -59,8 +59,16 @@ const DND_ACCEPTED_TYPES = [
const isDictionaryForAdhocFilter = (value: OptionValueType) => const isDictionaryForAdhocFilter = (value: OptionValueType) =>
!(value instanceof AdhocFilter) && value?.expressionType; !(value instanceof AdhocFilter) && value?.expressionType;
export interface DndFilterSelectProps
extends ControlComponentProps<OptionValueType[]> {
columns: ColumnMeta[];
savedMetrics: Metric[];
selectedMetrics: QueryFormMetric[];
datasource: Datasource;
}
export const DndFilterSelect = (props: DndFilterSelectProps) => { export const DndFilterSelect = (props: DndFilterSelectProps) => {
const { datasource, onChange } = props; const { datasource, onChange = () => {}, name: controlName } = props;
const propsValues = Array.from(props.value ?? []); const propsValues = Array.from(props.value ?? []);
const [values, setValues] = useState( const [values, setValues] = useState(
@ -74,7 +82,7 @@ export const DndFilterSelect = (props: DndFilterSelectProps) => {
const optionsForSelect = ( const optionsForSelect = (
columns: ColumnMeta[], columns: ColumnMeta[],
formData: Record<string, any>, formData: QueryFormData | null | undefined,
) => { ) => {
const options: OptionSortType[] = [ const options: OptionSortType[] = [
...columns, ...columns,
@ -369,7 +377,7 @@ export const DndFilterSelect = (props: DndFilterSelectProps) => {
setDroppedItem(item.value); setDroppedItem(item.value);
togglePopover(true); togglePopover(true);
}, },
[togglePopover], [controlName, togglePopover],
); );
const ghostButtonText = isFeatureEnabled(FeatureFlag.ENABLE_DND_WITH_CLICK_UX) const ghostButtonText = isFeatureEnabled(FeatureFlag.ENABLE_DND_WITH_CLICK_UX)
@ -378,7 +386,7 @@ export const DndFilterSelect = (props: DndFilterSelectProps) => {
return ( return (
<> <>
<DndSelectLabel<OptionValueType, OptionValueType[]> <DndSelectLabel
onDrop={handleDrop} onDrop={handleDrop}
canDrop={canDrop} canDrop={canDrop}
valuesRenderer={valuesRenderer} valuesRenderer={valuesRenderer}

View File

@ -19,11 +19,13 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { import {
DatasourceType,
ensureIsArray, ensureIsArray,
FeatureFlag, FeatureFlag,
GenericDataType, GenericDataType,
isFeatureEnabled, isFeatureEnabled,
Metric, Metric,
QueryFormMetric,
tn, tn,
} from '@superset-ui/core'; } from '@superset-ui/core';
import { ColumnMeta } from '@superset-ui/chart-controls'; import { ColumnMeta } from '@superset-ui/chart-controls';
@ -32,12 +34,12 @@ import { usePrevious } from 'src/common/hooks/usePrevious';
import AdhocMetric from 'src/explore/components/controls/MetricControl/AdhocMetric'; import AdhocMetric from 'src/explore/components/controls/MetricControl/AdhocMetric';
import AdhocMetricPopoverTrigger from 'src/explore/components/controls/MetricControl/AdhocMetricPopoverTrigger'; import AdhocMetricPopoverTrigger from 'src/explore/components/controls/MetricControl/AdhocMetricPopoverTrigger';
import MetricDefinitionValue from 'src/explore/components/controls/MetricControl/MetricDefinitionValue'; import MetricDefinitionValue from 'src/explore/components/controls/MetricControl/MetricDefinitionValue';
import { OptionValueType } from 'src/explore/components/controls/DndColumnSelectControl/types';
import { DatasourcePanelDndItem } from 'src/explore/components/DatasourcePanel/types'; import { DatasourcePanelDndItem } from 'src/explore/components/DatasourcePanel/types';
import { DndItemType } from 'src/explore/components/DndItemType'; import { DndItemType } from 'src/explore/components/DndItemType';
import DndSelectLabel from 'src/explore/components/controls/DndColumnSelectControl/DndSelectLabel'; import DndSelectLabel from 'src/explore/components/controls/DndColumnSelectControl/DndSelectLabel';
import { savedMetricType } from 'src/explore/components/controls/MetricControl/types'; import { savedMetricType } from 'src/explore/components/controls/MetricControl/types';
import { AGGREGATES } from 'src/explore/constants'; import { AGGREGATES } from 'src/explore/constants';
import { DndControlProps } from './types';
const EMPTY_OBJECT = {}; const EMPTY_OBJECT = {};
const DND_ACCEPTED_TYPES = [DndItemType.Column, DndItemType.Metric]; const DND_ACCEPTED_TYPES = [DndItemType.Column, DndItemType.Metric];
@ -75,10 +77,12 @@ const getOptionsForSavedMetrics = (
: savedMetric, : savedMetric,
) ?? []; ) ?? [];
type ValueType = Metric | AdhocMetric | QueryFormMetric;
const columnsContainAllMetrics = ( const columnsContainAllMetrics = (
value: (string | AdhocMetric | ColumnMeta)[], value: ValueType | ValueType[] | null | undefined,
columns: ColumnMeta[], columns: ColumnMeta[],
savedMetrics: savedMetricType[], savedMetrics: (savedMetricType | Metric)[],
) => { ) => {
const columnNames = new Set( const columnNames = new Set(
[...(columns || []), ...(savedMetrics || [])] [...(columns || []), ...(savedMetrics || [])]
@ -104,6 +108,12 @@ const columnsContainAllMetrics = (
); );
}; };
export type DndMetricSelectProps = DndControlProps<ValueType> & {
savedMetrics: savedMetricType[];
columns: ColumnMeta[];
datasourceType?: DatasourceType;
};
export const DndMetricSelect = (props: any) => { export const DndMetricSelect = (props: any) => {
const { onChange, multi, columns, savedMetrics } = props; const { onChange, multi, columns, savedMetrics } = props;
@ -130,7 +140,7 @@ export const DndMetricSelect = (props: any) => {
[multi, onChange], [multi, onChange],
); );
const [value, setValue] = useState<(AdhocMetric | Metric | string)[]>( const [value, setValue] = useState<ValueType[]>(
coerceAdhocMetrics(props.value), coerceAdhocMetrics(props.value),
); );
const [droppedItem, setDroppedItem] = useState<DatasourcePanelDndItem | null>( const [droppedItem, setDroppedItem] = useState<DatasourcePanelDndItem | null>(
@ -176,7 +186,9 @@ export const DndMetricSelect = (props: any) => {
const onNewMetric = useCallback( const onNewMetric = useCallback(
(newMetric: Metric) => { (newMetric: Metric) => {
const newValue = props.multi ? [...value, newMetric] : [newMetric]; const newValue = props.multi
? [...value, newMetric.metric_name]
: [newMetric.metric_name];
setValue(newValue); setValue(newValue);
handleChange(newValue); handleChange(newValue);
}, },
@ -191,7 +203,7 @@ export const DndMetricSelect = (props: any) => {
const newValue = value.map(value => { const newValue = value.map(value => {
if ( if (
// compare saved metrics // compare saved metrics
value === (oldMetric as Metric).metric_name || ('metric_name' in oldMetric && value === oldMetric.metric_name) ||
// compare adhoc metrics // compare adhoc metrics
typeof (value as AdhocMetric).optionName !== 'undefined' typeof (value as AdhocMetric).optionName !== 'undefined'
? (value as AdhocMetric).optionName === ? (value as AdhocMetric).optionName ===
@ -254,7 +266,7 @@ export const DndMetricSelect = (props: any) => {
); );
const valueRenderer = useCallback( const valueRenderer = useCallback(
(option: Metric | AdhocMetric | string, index: number) => ( (option: ValueType, index: number) => (
<MetricDefinitionValue <MetricDefinitionValue
key={index} key={index}
index={index} index={index}
@ -353,7 +365,7 @@ export const DndMetricSelect = (props: any) => {
return ( return (
<div className="metrics-select"> <div className="metrics-select">
<DndSelectLabel<OptionValueType, OptionValueType[]> <DndSelectLabel
onDrop={handleDrop} onDrop={handleDrop}
canDrop={canDrop} canDrop={canDrop}
valuesRenderer={valuesRenderer} valuesRenderer={valuesRenderer}

View File

@ -19,16 +19,16 @@
import React from 'react'; import React from 'react';
import { render, screen } from 'spec/helpers/testing-library'; import { render, screen } from 'spec/helpers/testing-library';
import { DndItemType } from 'src/explore/components/DndItemType'; import { DndItemType } from 'src/explore/components/DndItemType';
import DndSelectLabel from 'src/explore/components/controls/DndColumnSelectControl/DndSelectLabel'; import DndSelectLabel, {
DndSelectLabelProps,
} from 'src/explore/components/controls/DndColumnSelectControl/DndSelectLabel';
const defaultProps = { const defaultProps: DndSelectLabelProps = {
name: 'Column', name: 'Column',
accept: 'Column' as DndItemType, accept: 'Column' as DndItemType,
onDrop: jest.fn(), onDrop: jest.fn(),
canDrop: () => false, canDrop: () => false,
valuesRenderer: () => <span />, valuesRenderer: () => <span />,
onChange: jest.fn(),
options: { string: { column_name: 'Column' } },
}; };
test('renders with default props', async () => { test('renders with default props', async () => {

View File

@ -16,7 +16,7 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import React from 'react'; import React, { ReactNode } from 'react';
import { useDrop } from 'react-dnd'; import { useDrop } from 'react-dnd';
import { t, useTheme } from '@superset-ui/core'; import { t, useTheme } from '@superset-ui/core';
import ControlHeader from 'src/explore/components/ControlHeader'; import ControlHeader from 'src/explore/components/ControlHeader';
@ -25,18 +25,35 @@ import {
DndLabelsContainer, DndLabelsContainer,
HeaderContainer, HeaderContainer,
} from 'src/explore/components/controls/OptionControls'; } from 'src/explore/components/controls/OptionControls';
import { DatasourcePanelDndItem } from 'src/explore/components/DatasourcePanel/types'; import {
DatasourcePanelDndItem,
DndItemValue,
} from 'src/explore/components/DatasourcePanel/types';
import Icons from 'src/components/Icons'; import Icons from 'src/components/Icons';
import { DndColumnSelectProps } from './types'; import { DndItemType } from '../../DndItemType';
export default function DndSelectLabel<T, O>({ export type DndSelectLabelProps = {
name: string;
accept: DndItemType | DndItemType[];
ghostButtonText?: string;
onDrop: (item: DatasourcePanelDndItem) => void;
canDrop: (item: DatasourcePanelDndItem) => boolean;
canDropValue?: (value: DndItemValue) => boolean;
onDropValue?: (value: DndItemValue) => void;
valuesRenderer: () => ReactNode;
displayGhostButton?: boolean;
onClickGhostButton?: () => void;
};
export default function DndSelectLabel({
displayGhostButton = true, displayGhostButton = true,
accept,
...props ...props
}: DndColumnSelectProps<T, O>) { }: DndSelectLabelProps) {
const theme = useTheme(); const theme = useTheme();
const [{ isOver, canDrop }, datasourcePanelDrop] = useDrop({ const [{ isOver, canDrop }, datasourcePanelDrop] = useDrop({
accept: props.accept, accept,
drop: (item: DatasourcePanelDndItem) => { drop: (item: DatasourcePanelDndItem) => {
props.onDrop(item); props.onDrop(item);

View File

@ -17,13 +17,9 @@
* under the License. * under the License.
*/ */
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { Metric } from '@superset-ui/core'; import { JsonValue } from '@superset-ui/core';
import { ControlComponentProps } from 'src/explore/components/Control';
import { ColumnMeta } from '@superset-ui/chart-controls'; import { ColumnMeta } from '@superset-ui/chart-controls';
import {
DatasourcePanelDndItem,
DndItemValue,
} from '../../DatasourcePanel/types';
import { DndItemType } from '../../DndItemType';
export interface OptionProps { export interface OptionProps {
children?: ReactNode; children?: ReactNode;
@ -42,40 +38,16 @@ export interface OptionItemInterface {
dragIndex: number; dragIndex: number;
} }
export interface LabelProps<T = string[] | string> { /**
name: string; * Shared control props for all DnD control.
value?: T; */
onChange: (value?: T) => void; export type DndControlProps<
options: { string: ColumnMeta }; ValueType extends JsonValue
> = ControlComponentProps<ValueType | ValueType[] | null> & {
multi?: boolean; multi?: boolean;
canDelete?: boolean; canDelete?: boolean;
ghostButtonText?: string; ghostButtonText?: string;
label?: string; onChange: (value: ValueType | ValueType[] | null | undefined) => void;
} };
export interface DndColumnSelectProps<
T = string[] | string,
O = string[] | string
> extends LabelProps<T> {
onDrop: (item: DatasourcePanelDndItem) => void;
canDrop: (item: DatasourcePanelDndItem) => boolean;
canDropValue?: (value: DndItemValue) => boolean;
onDropValue?: (value: DndItemValue) => void;
valuesRenderer: () => ReactNode;
accept: DndItemType | DndItemType[];
ghostButtonText?: string;
displayGhostButton?: boolean;
onClickGhostButton?: () => void;
}
export type OptionValueType = Record<string, any>; export type OptionValueType = Record<string, any>;
export interface DndFilterSelectProps {
name: string;
value: OptionValueType[];
columns: ColumnMeta[];
datasource: Record<string, any>;
formData: Record<string, any>;
savedMetrics: Metric[];
onChange: (filters: OptionValueType[]) => void;
options: { string: ColumnMeta };
}

View File

@ -22,25 +22,25 @@ import { ensureIsArray } from '@superset-ui/core';
export class OptionSelector { export class OptionSelector {
values: ColumnMeta[]; values: ColumnMeta[];
options: { string: ColumnMeta }; options: Record<string, ColumnMeta>;
multi: boolean; multi: boolean;
constructor( constructor(
options: { string: ColumnMeta }, options: Record<string, ColumnMeta>,
multi: boolean, multi: boolean,
initialValues?: string[] | string, initialValues?: string[] | string | null,
) { ) {
this.options = options; this.options = options;
this.multi = multi; this.multi = multi;
this.values = ensureIsArray(initialValues) this.values = ensureIsArray(initialValues)
.map(value => { .map(value => {
if (value in options) { if (value && value in options) {
return options[value]; return options[value];
} }
return null; return null;
}) })
.filter(Boolean); .filter(Boolean) as ColumnMeta[];
} }
add(value: string) { add(value: string) {

View File

@ -22,7 +22,8 @@ import {
AnnotationData, AnnotationData,
AdhocMetric, AdhocMetric,
} from '@superset-ui/core'; } from '@superset-ui/core';
import { ColumnMeta } from '@superset-ui/chart-controls'; import { ColumnMeta, DatasourceMeta } from '@superset-ui/chart-controls';
import { DatabaseObject } from 'src/views/CRUD/types';
export { Slice, Chart } from 'src/types/Chart'; export { Slice, Chart } from 'src/types/Chart';
@ -54,3 +55,10 @@ export interface ChartState {
export type OptionSortType = Partial< export type OptionSortType = Partial<
ColumnMeta & AdhocMetric & { saved_metric_name: string } ColumnMeta & AdhocMetric & { saved_metric_name: string }
>; >;
export type Datasource = DatasourceMeta & {
database?: DatabaseObject;
datasource?: string;
schema?: string;
is_sqllab_view?: boolean;
};

View File

@ -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.
*/
export default interface Database {
id: number;
allow_run_async: boolean;
database_name: string;
encrypted_extra: string;
extra: string;
impersonate_user: boolean;
server_cert: string;
sqlalchemy_uri: string;
}

View File

@ -17,6 +17,7 @@
* under the License. * under the License.
*/ */
import { User } from 'src/types/bootstrapTypes'; import { User } from 'src/types/bootstrapTypes';
import Database from 'src/types/Database';
import Owner from 'src/types/Owner'; import Owner from 'src/types/Owner';
export type FavoriteStatus = { export type FavoriteStatus = {
@ -137,12 +138,5 @@ export type ImportResourceName =
| 'dataset' | 'dataset'
| 'saved_query'; | 'saved_query';
export type DatabaseObject = { export type DatabaseObject = Partial<Database> &
allow_run_async?: boolean; Pick<Database, 'sqlalchemy_uri'>;
database_name?: string;
encrypted_extra?: string;
extra?: string;
impersonate_user?: boolean;
server_cert?: string;
sqlalchemy_uri: string;
};