refactor(control-utils): reorganize files and refine typing (#610)

This commit is contained in:
Jesse Yang 2020-06-18 11:04:51 -07:00 committed by Yongjie Zhao
parent b182b641f6
commit 6639b24563
18 changed files with 199 additions and 117 deletions

View File

@ -20,7 +20,7 @@ import React from 'react';
import { ColumnTypeLabel } from './ColumnTypeLabel';
import InfoTooltipWithTrigger from './InfoTooltipWithTrigger';
import { ColumnMeta } from './types';
import { ColumnMeta } from '../types';
export type ColumnOptionProps = {
column: ColumnMeta;

View File

@ -2,16 +2,20 @@ import * as constantsModule from './constants';
import * as sharedControlsModule from './shared-controls';
import * as sectionModules from './sections';
// explore all available shared controls
export { default as sharedControls } from './shared-controls';
// `export * as x from 'y'` doesn't work for some reason
export const constants = constantsModule;
export const internalSharedControls = sharedControlsModule;
export const sections = sectionModules;
export { D3_FORMAT_DOCS, D3_FORMAT_OPTIONS, D3_TIME_FORMAT_OPTIONS } from './D3Formatting';
export { formatSelectOptions, formatSelectOptionsForRange } from './selectOptions';
export { D3_FORMAT_DOCS, D3_FORMAT_OPTIONS, D3_TIME_FORMAT_OPTIONS } from './utils/D3Formatting';
export { formatSelectOptions, formatSelectOptionsForRange } from './utils/selectOptions';
export * from './utils/mainMetric';
export * from './components/InfoTooltipWithTrigger';
export * from './components/ColumnOption';
export * from './components/ColumnTypeLabel';
export * from './components/MetricOption';
export * from './InfoTooltipWithTrigger';
export * from './ColumnOption';
export * from './ColumnTypeLabel';
export * from './mainMetric';
export * from './MetricOption';
export * from './types';

View File

@ -42,17 +42,17 @@ import {
} from '@superset-ui/color';
import { legacyValidateInteger, validateNonEmpty } from '@superset-ui/validator';
import { formatSelectOptions } from './selectOptions';
import { mainMetric, Metric } from './mainMetric';
import { ColumnOption } from './ColumnOption';
import { formatSelectOptions } from './utils/selectOptions';
import { mainMetric, Metric } from './utils/mainMetric';
import { TIME_FILTER_LABELS } from './constants';
import {
ControlConfig,
SharedControlConfig,
ColumnMeta,
DatasourceMeta,
ExtraControlProps,
SelectControlConfig,
} from './types';
import { ColumnOption } from './components/ColumnOption';
const categoricalSchemeRegistry = getCategoricalSchemeRegistry();
const sequentialSchemeRegistry = getSequentialSchemeRegistry();
@ -102,22 +102,24 @@ type Control = {
default?: unknown;
};
const groupByControl: ControlConfig = {
const groupByControl: SharedControlConfig<'SelectControl', ColumnMeta> = {
type: 'SelectControl',
label: t('Group by'),
queryField: 'groupby',
multi: true,
freeForm: true,
label: t('Group by'),
clearable: true,
default: [],
includeTime: false,
description: t('One or many controls to group by'),
optionRenderer: (c: ColumnMeta) => <ColumnOption showType column={c} />,
valueRenderer: (c: ColumnMeta) => <ColumnOption column={c} />,
optionRenderer: c => <ColumnOption showType column={c} />,
valueRenderer: c => <ColumnOption column={c} />,
valueKey: 'column_name',
allowAll: true,
filterOption: (opt: ColumnMeta, text: string) =>
filterOption: ({ data: opt }, text: string) =>
(opt.column_name && opt.column_name.toLowerCase().includes(text.toLowerCase())) ||
(opt.verbose_name && opt.verbose_name.toLowerCase().includes(text.toLowerCase())),
(opt.verbose_name && opt.verbose_name.toLowerCase().includes(text.toLowerCase())) ||
false,
promptTextCreator: (label: unknown) => label,
mapStateToProps(state, { includeTime }) {
const newState: ExtraControlProps = {};
@ -133,7 +135,7 @@ const groupByControl: ControlConfig = {
commaChoosesOption: false,
};
const metrics: ControlConfig = {
const metrics: SharedControlConfig<'MetricsControl'> = {
type: 'MetricsControl',
queryField: 'metrics',
multi: true,
@ -152,7 +154,8 @@ const metrics: ControlConfig = {
},
description: t('One or many metrics to display'),
};
const metric: ControlConfig = {
const metric: SharedControlConfig<'MetricsControl'> = {
...metrics,
multi: false,
label: t('Metric'),
@ -169,40 +172,40 @@ export function columnChoices(datasource: DatasourceMeta) {
return [];
}
const datasourceControl: ControlConfig = {
const datasourceControl: SharedControlConfig<'DatasourceControl'> = {
type: 'DatasourceControl',
label: t('Datasource'),
default: null,
description: null,
mapStateToProps: (state, control, { setDatasource }) => ({
mapStateToProps: (state, control, actions) => ({
datasource: state.datasource,
onDatasourceSave: setDatasource,
onDatasourceSave: actions?.setDatasource,
}),
};
const viz_type: ControlConfig = {
const viz_type: SharedControlConfig<'VizTypeControl'> = {
type: 'VizTypeControl',
label: t('Visualization Type'),
default: 'table',
description: t('The type of visualization to display'),
};
const color_picker: ControlConfig = {
const color_picker: SharedControlConfig<'ColorPickerControl'> = {
type: 'ColorPickerControl',
label: t('Fixed Color'),
description: t('Use this to define a static color for all circles'),
type: 'ColorPickerControl',
default: PRIMARY_COLOR,
renderTrigger: true,
};
const metric_2: ControlConfig = {
const metric_2: SharedControlConfig<'MetricsControl'> = {
...metric,
label: t('Right Axis Metric'),
clearable: true,
description: t('Choose a metric for right axis'),
};
const linear_color_scheme: ControlConfig = {
const linear_color_scheme: SharedControlConfig<'ColorSchemeControl'> = {
type: 'ColorSchemeControl',
label: t('Linear Color Scheme'),
choices: () =>
@ -215,7 +218,7 @@ const linear_color_scheme: ControlConfig = {
isLinear: true,
};
const secondary_metric: ControlConfig = {
const secondary_metric: SharedControlConfig<'MetricsControl'> = {
...metric,
label: t('Color Metric'),
default: null,
@ -223,13 +226,13 @@ const secondary_metric: ControlConfig = {
description: t('A metric to use for color'),
};
const columnsControl: ControlConfig = {
const columnsControl: typeof groupByControl = {
...groupByControl,
label: t('Columns'),
description: t('One or many controls to pivot as columns'),
};
const druid_time_origin: ControlConfig = {
const druid_time_origin: SharedControlConfig<'SelectControl'> = {
type: 'SelectControl',
freeForm: true,
label: TIME_FILTER_LABELS.druid_time_origin,
@ -244,7 +247,7 @@ const druid_time_origin: ControlConfig = {
),
};
const granularity: ControlConfig = {
const granularity: SharedControlConfig<'SelectControl'> = {
type: 'SelectControl',
freeForm: true,
label: TIME_FILTER_LABELS.granularity,
@ -274,7 +277,7 @@ const granularity: ControlConfig = {
),
};
const granularity_sqla: ControlConfig = {
const granularity_sqla: SharedControlConfig<'SelectControl', ColumnMeta> = {
type: 'SelectControl',
label: TIME_FILTER_LABELS.granularity_sqla,
description: t(
@ -286,8 +289,8 @@ const granularity_sqla: ControlConfig = {
),
default: (c: Control) => c.default,
clearable: false,
optionRenderer: (c: ColumnMeta) => <ColumnOption showType column={c} />,
valueRenderer: (c: ColumnMeta) => <ColumnOption column={c} />,
optionRenderer: c => <ColumnOption showType column={c} />,
valueRenderer: c => <ColumnOption column={c} />,
valueKey: 'column_name',
mapStateToProps: state => {
const props: Partial<SelectControlConfig<ColumnMeta>> = {};
@ -304,7 +307,7 @@ const granularity_sqla: ControlConfig = {
},
};
const time_grain_sqla: ControlConfig = {
const time_grain_sqla: SharedControlConfig<'SelectControl'> = {
type: 'SelectControl',
label: TIME_FILTER_LABELS.time_grain_sqla,
default: 'P1D',
@ -320,7 +323,7 @@ const time_grain_sqla: ControlConfig = {
}),
};
const time_range: ControlConfig = {
const time_range: SharedControlConfig<'DateFilterControl'> = {
type: 'DateFilterControl',
freeForm: true,
label: TIME_FILTER_LABELS.time_range,
@ -338,7 +341,7 @@ const time_range: ControlConfig = {
}),
};
const row_limit: ControlConfig = {
const row_limit: SharedControlConfig<'SelectControl'> = {
type: 'SelectControl',
freeForm: true,
label: t('Row limit'),
@ -347,7 +350,7 @@ const row_limit: ControlConfig = {
choices: formatSelectOptions(ROW_LIMIT_OPTIONS),
};
const limit: ControlConfig = {
const limit: SharedControlConfig<'SelectControl'> = {
type: 'SelectControl',
freeForm: true,
label: t('Series limit'),
@ -361,7 +364,7 @@ const limit: ControlConfig = {
),
};
const timeseries_limit_metric: ControlConfig = {
const timeseries_limit_metric: SharedControlConfig<'MetricsControl'> = {
type: 'MetricsControl',
label: t('Sort By'),
default: null,
@ -373,7 +376,7 @@ const timeseries_limit_metric: ControlConfig = {
}),
};
const series: ControlConfig = {
const series: typeof groupByControl = {
...groupByControl,
label: t('Series'),
multi: false,
@ -385,7 +388,7 @@ const series: ControlConfig = {
),
};
const entity: ControlConfig = {
const entity: typeof groupByControl = {
...groupByControl,
label: t('Entity'),
default: null,
@ -394,27 +397,27 @@ const entity: ControlConfig = {
description: t('This defines the element to be plotted on the chart'),
};
const x: ControlConfig = {
const x: SharedControlConfig<'MetricsControl'> = {
...metric,
label: t('X Axis'),
description: t('Metric assigned to the [X] axis'),
default: null,
};
const y: ControlConfig = {
const y: SharedControlConfig<'MetricsControl'> = {
...metric,
label: t('Y Axis'),
default: null,
description: t('Metric assigned to the [Y] axis'),
};
const size: ControlConfig = {
const size: SharedControlConfig<'MetricsControl'> = {
...metric,
label: t('Bubble Size'),
default: null,
};
const y_axis_format: ControlConfig = {
const y_axis_format: SharedControlConfig<'SelectControl'> = {
type: 'SelectControl',
freeForm: true,
label: t('Y Axis Format'),
@ -436,7 +439,7 @@ const y_axis_format: ControlConfig = {
},
};
const adhoc_filters: ControlConfig = {
const adhoc_filters: SharedControlConfig<'AdhocFilterControl'> = {
type: 'AdhocFilterControl',
label: t('Filters'),
default: null,
@ -449,7 +452,7 @@ const adhoc_filters: ControlConfig = {
provideFormDataToProps: true,
};
const color_scheme: ControlConfig = {
const color_scheme: SharedControlConfig<'ColorSchemeControl'> = {
type: 'ColorSchemeControl',
label: t('Color Scheme'),
default: categoricalSchemeRegistry.getDefaultKey(),
@ -459,7 +462,7 @@ const color_scheme: ControlConfig = {
schemes: () => categoricalSchemeRegistry.getMap(),
};
const label_colors: ControlConfig = {
const label_colors: SharedControlConfig<'ColorMapControl'> = {
type: 'ColorMapControl',
label: t('Color Map'),
default: {},

View File

@ -17,7 +17,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import React, { ReactNode, ReactText } from 'react';
import React, { ReactNode, ReactText, ReactElement } from 'react';
import { QueryFormData } from '@superset-ui/query';
import sharedControls from './shared-controls';
@ -52,13 +52,8 @@ export interface DatasourceMeta {
export interface ControlPanelState {
form_data: QueryFormData;
datasource?: DatasourceMeta | null;
options?: ColumnMeta[];
controls?: {
comparison_type?: {
value: string;
};
};
datasource: DatasourceMeta | null;
controls: ControlStateMapping;
}
/**
@ -72,7 +67,7 @@ export interface ActionDispatcher<ARGS extends unknown[], A extends Action = Any
/**
* Mapping of action dispatchers
*/
export interface ControlPanelActionDispathers {
export interface ControlPanelActionDispatchers {
setDatasource: ActionDispatcher<[DatasourceMeta]>;
}
@ -82,8 +77,10 @@ export interface ControlPanelActionDispathers {
export type ExtraControlProps = AnyDict;
// Ref:superset-frontend/src/explore/store.js
export type ControlState<T extends SelectOption = SelectOption> = ControlConfig<T> &
ExtraControlProps;
export type ControlState<
T extends InternalControlType | unknown = InternalControlType,
O extends SelectOption = SelectOption
> = ControlConfig<T, O> & ExtraControlProps;
export interface ControlStateMapping {
[key: string]: ControlState;
@ -91,7 +88,7 @@ export interface ControlStateMapping {
// Ref: superset-frontend/src/explore/components/ControlPanelsContainer.jsx
export interface ControlPanelsContainerProps extends AnyDict {
actions: ControlPanelActionDispathers;
actions: ControlPanelActionDispatchers;
controls: ControlStateMapping;
exportState: AnyDict;
form_data: QueryFormData;
@ -130,8 +127,8 @@ export type InternalControlType =
| 'SelectControlVerifiedOptions'
| 'AdhocFilterControlVerifiedOptions';
export interface Validator {
(value: unknown): boolean | string;
export interface ControlValueValidator<T = unknown, O extends SelectOption = SelectOption> {
(value: T, state: ControlState<O>): boolean | string;
}
export type TabOverride = 'data' | boolean;
@ -163,25 +160,26 @@ export type TabOverride = 'data' | boolean;
* - visibility: a function that uses control panel props to check whether a control should
* be visibile.
*/
export interface GeneralControlConfig {
type: InternalControlType | React.ComponentType;
export interface BaseControlConfig<T = unknown> {
type: T;
label?: ReactNode;
description?: ReactNode;
default?: unknown;
renderTrigger?: boolean;
validators?: Validator[];
validators?: ControlValueValidator[];
warning?: ReactNode;
error?: ReactNode;
// override control panel state props
mapStateToProps?: (
state: ControlPanelState,
control: ControlConfig,
actions: ControlPanelActionDispathers,
control: this,
actions?: ControlPanelActionDispatchers,
) => ExtraControlProps;
tabOverride?: TabOverride;
visibility?: (props: ControlPanelsContainerProps) => boolean;
[key: string]: unknown;
}
/** --------------------------------------------
* Additional Config for specific control Types
* --------------------------------------------- */
@ -198,22 +196,49 @@ type SelectControlType =
| 'SelectControlVerifiedOptions'
| 'AdhocFilterControlVerifiedOptions';
export interface SelectControlConfig<T extends SelectOption = SelectOption>
extends GeneralControlConfig {
type: SelectControlType;
options?: T[];
// via react-select/src/filters
interface FilterOption<T extends SelectOption> {
label: string;
value: string;
data: T;
}
// Ref: superset-frontend/src/components/Select/SupersetStyledSelect.tsx
export interface SelectControlConfig<
O extends SelectOption = SelectOption,
T extends SelectControlType = SelectControlType
> extends BaseControlConfig<T> {
clearable?: boolean;
freeForm?: boolean;
multi?: boolean;
optionRenderer?: (option: T) => ReactNode;
valueRenderer?: (option: T) => ReactNode;
valueKey?: string;
labelKey?: string;
options?: O[];
optionRenderer?: (option: O) => ReactNode;
valueRenderer?: (option: O) => ReactNode;
filterOption?: ((option: FilterOption<O>, rawInput: string) => Boolean) | null;
}
export type ControlConfig<T extends SelectOption = SelectOption> =
| GeneralControlConfig
| SelectControlConfig<T>;
export type SharedControlConfig<
T extends InternalControlType = InternalControlType,
O extends SelectOption = SelectOption
> = T extends SelectControlType ? SelectControlConfig<O, T> : BaseControlConfig<T>;
/** --------------------------------------------
* Custom controls
* --------------------------------------------- */
export type CustomComponentControlConfig<P = unknown> = BaseControlConfig<
InternalControlType | React.ComponentType<P>
> &
Omit<P, 'onChange' | 'hovered'>; // two run-time properties from superset-frontend/src/explore/components/Control.jsx
// Catch-all ControlConfig
// - if T == known control types, return SharedControlConfig,
// - otherwise assume it's a custom component control
export type ControlConfig<
T extends InternalControlType | unknown = InternalControlType,
O extends SelectOption = SelectOption
> = T extends InternalControlType ? SharedControlConfig<T, O> : CustomComponentControlConfig<T>;
/** --------------------------------------------
* Chart plugin control panel config
@ -228,17 +253,28 @@ export type SharedSectionAlias =
| 'sqlaTimeSeries'
| 'NVD3TimeSeries';
export interface ControlItem {
export interface OverrideSharedControlItem {
name: SharedControlAlias;
config: Partial<ControlConfig>;
override: Partial<SharedControlConfig>;
}
export interface CustomControlItem {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export interface CustomControlItem<P = any> {
name: string;
config: ControlConfig;
config: CustomComponentControlConfig<P>;
}
export type ControlSetItem = SharedControlAlias | ControlItem | CustomControlItem | ReactNode;
export type ControlSetItem =
| SharedControlAlias
| OverrideSharedControlItem
| CustomControlItem
// use ReactElement instead of ReactNode because `string`, `number`, etc. may
// interfere with other ControlSetItem types
| ReactElement
| null;
export type ExpandedControlItem = CustomControlItem | ReactElement | null;
export type ControlSetRow = ControlSetItem[];
// Ref:
@ -246,16 +282,17 @@ export type ControlSetRow = ControlSetItem[];
// - superset-frontend/src/explore/components/ControlPanelSection.jsx
export interface ControlPanelSectionConfig {
label: ReactNode;
controlSetRows: ControlSetRow[];
description?: ReactNode;
expanded?: boolean;
tabOverride?: TabOverride;
controlSetRows: ControlSetRow[];
}
export interface ControlPanelConfig {
controlPanelSections: ControlPanelSectionConfig[];
controlOverrides?: ControlOverrides;
sectionOverrides?: SectionOverrides;
onInit?: (state: ControlStateMapping) => void;
}
export type ControlOverrides = {

View File

@ -19,9 +19,12 @@
import React from 'react';
import { shallow, ShallowWrapper } from 'enzyme';
import { ColumnOption, ColumnOptionProps } from '../../src/ColumnOption';
import { ColumnTypeLabel } from '../../src/ColumnTypeLabel';
import InfoTooltipWithTrigger from '../../src/InfoTooltipWithTrigger';
import {
ColumnOption,
ColumnOptionProps,
ColumnTypeLabel,
InfoTooltipWithTrigger,
} from '../../src';
describe('ColumnOption', () => {
const defaultProps = {

View File

@ -19,7 +19,7 @@
import React from 'react';
import { shallow } from 'enzyme';
import { ColumnTypeLabel, ColumnTypeLabelProps } from '../../src/ColumnTypeLabel';
import { ColumnTypeLabel, ColumnTypeLabelProps } from '../../src';
describe('ColumnOption', () => {
const defaultProps = {

View File

@ -19,7 +19,7 @@
import React from 'react';
import { shallow } from 'enzyme';
import { OverlayTrigger } from 'react-bootstrap';
import InfoTooltipWithTrigger from '../../src/InfoTooltipWithTrigger';
import { InfoTooltipWithTrigger } from '../../src';
describe('InfoTooltipWithTrigger', () => {
it('renders a tooltip', () => {

View File

@ -18,7 +18,7 @@
*/
import React from 'react';
import { shallow, ShallowWrapper } from 'enzyme';
import { MetricOption, MetricOptionProps } from '../../src/MetricOption';
import { MetricOption, MetricOptionProps } from '../../src';
describe('MetricOption', () => {
const defaultProps = {

View File

@ -1,3 +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.
*/
import { sections } from '../src';
describe('@superset-ui/control-utils', () => {

View File

@ -1,26 +0,0 @@
import { formatSelectOptions, formatSelectOptionsForRange } from '../src';
describe('formatSelectOptions', () => {
it('formats an array of options', () => {
expect(formatSelectOptions([1, 5, 10, 25, 50, 'unlimited'])).toEqual([
[1, '1'],
[5, '5'],
[10, '10'],
[25, '25'],
[50, '50'],
['unlimited', 'unlimited'],
]);
});
});
describe('formatSelectOptionsForRange', () => {
it('generates select options from a range', () => {
expect(formatSelectOptionsForRange(1, 5)).toEqual([
[1, '1'],
[2, '2'],
[3, '3'],
[4, '4'],
[5, '5'],
]);
});
});

View File

@ -16,8 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { mainMetric } from '../src/mainMetric';
import { mainMetric } from '../../src';
describe('mainMetric', () => {
it('is null when no options', () => {

View File

@ -0,0 +1,44 @@
/**
* 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 { formatSelectOptions, formatSelectOptionsForRange } from '../../src';
describe('formatSelectOptions', () => {
it('formats an array of options', () => {
expect(formatSelectOptions([1, 5, 10, 25, 50, 'unlimited'])).toEqual([
[1, '1'],
[5, '5'],
[10, '10'],
[25, '25'],
[50, '50'],
['unlimited', 'unlimited'],
]);
});
});
describe('formatSelectOptionsForRange', () => {
it('generates select options from a range', () => {
expect(formatSelectOptionsForRange(1, 5)).toEqual([
[1, '1'],
[2, '2'],
[3, '3'],
[4, '4'],
[5, '5'],
]);
});
});