diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-control-utils/package.json b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-control-utils/package.json index 251d0388bc7e..705daf72f152 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-control-utils/package.json +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-control-utils/package.json @@ -27,6 +27,7 @@ }, "peerDependencies": { "@superset-ui/color": "^0.13.3", + "@superset-ui/query": "^0.13.27", "@superset-ui/translation": "^0.13", "@superset-ui/validator": "^0.13", "react": "^16.13.1" diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-control-utils/src/ColumnOption.tsx b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-control-utils/src/ColumnOption.tsx index f39763e85b86..158e70309bbe 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-control-utils/src/ColumnOption.tsx +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-control-utils/src/ColumnOption.tsx @@ -16,25 +16,14 @@ * specific language governing permissions and limitations * under the License. */ -/* eslint-disable camelcase */ import React from 'react'; import { ColumnTypeLabel } from './ColumnTypeLabel'; import InfoTooltipWithTrigger from './InfoTooltipWithTrigger'; - -export type ColumnOptionColumn = { - column_name: string; - groupby?: string; - verbose_name?: string; - description?: string; - expression?: string; - is_dttm?: boolean; - type?: string; - filterable?: boolean; -}; +import { ColumnMeta } from './types'; export type ColumnOptionProps = { - column: ColumnOptionColumn; + column: ColumnMeta; showType?: boolean; }; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-control-utils/src/index.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-control-utils/src/index.ts index 22726d7cb916..2c8fe383f5e5 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-control-utils/src/index.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-control-utils/src/index.ts @@ -8,8 +8,10 @@ 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 * from './InfoTooltipWithTrigger'; export * from './ColumnOption'; export * from './ColumnTypeLabel'; export * from './mainMetric'; export * from './MetricOption'; +export * from './types'; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-control-utils/src/shared-controls.tsx b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-control-utils/src/shared-controls.tsx index 526ec7d999ea..f4784c2cc6e8 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-control-utils/src/shared-controls.tsx +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-control-utils/src/shared-controls.tsx @@ -1,3 +1,4 @@ +/* eslint-disable camelcase */ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file @@ -17,8 +18,6 @@ * under the License. */ -/* eslint-disable camelcase */ - /** * This file exports all controls available for use in chart plugins internal to Superset. * It is not recommended to use the controls here for any third-party plugins. @@ -32,33 +31,7 @@ * * While the keys defined in the control itself get passed to the controlType as props, * here's a list of the keys that are common to all controls, and as a result define the - * control interface: - * - * - type: the control type, referencing a React component of the same name - * - label: the label as shown in the control's header - * - description: shown in the info tooltip of the control's header - * - default: the default value when opening a new chart, or changing visualization type - * - renderTrigger: a bool that defines whether the visualization should be re-rendered - when changed. This should `true` for controls that only affect the rendering (client side) - and don't affect the query or backend data processing as those require to re run a query - and fetch the data - * - validators: an array of functions that will receive the value of the component and - should return error messages when the value is not valid. The error message gets - bubbled up to the control header, section header and query panel header. - * - warning: text shown as a tooltip on a warning icon in the control's header - * - error: text shown as a tooltip on a error icon in the control's header - * - mapStateToProps: a function that receives the App's state and return an object of k/v - to overwrite configuration at runtime. This is useful to alter a component based on - anything external to it, like another control's value. For instance it's possible to - show a warning based on the value of another component. It's also possible to bind - arbitrary data from the redux store to the component this way. - * - tabOverride: set to 'data' if you want to force a renderTrigger to show up on the `Data` - tab, otherwise `renderTrigger: true` components will show up on the `Style` tab. - * - * Note that the keys defined in controls in this file that are not listed above represent - * props specific for the React component defined as `type`. Also note that this module work - * in tandem with `controlPanels/index.js` that defines how controls are composed into sections for - * each and every visualization type. + * control interface. */ import React from 'react'; import { t } from '@superset-ui/translation'; @@ -71,8 +44,15 @@ import { legacyValidateInteger, validateNonEmpty } from '@superset-ui/validator' import { formatSelectOptions } from './selectOptions'; import { mainMetric, Metric } from './mainMetric'; -import { ColumnOption, ColumnOptionColumn } from './ColumnOption'; +import { ColumnOption } from './ColumnOption'; import { TIME_FILTER_LABELS } from './constants'; +import { + ControlConfig, + ColumnMeta, + DatasourceMeta, + ExtraControlProps, + SelectControlConfig, +} from './types'; const categoricalSchemeRegistry = getCategoricalSchemeRegistry(); const sequentialSchemeRegistry = getSequentialSchemeRegistry(); @@ -97,13 +77,12 @@ export const D3_FORMAT_OPTIONS = [ ]; const ROW_LIMIT_OPTIONS = [10, 50, 100, 250, 500, 1000, 5000, 10000, 50000]; - const SERIES_LIMITS = [0, 5, 10, 25, 50, 100, 500]; -export const D3_FORMAT_DOCS = 'D3 format syntax: https://github.com/d3/d3-format'; +export const D3_FORMAT_DOCS = t('D3 format syntax: https://github.com/d3/d3-format'); export const D3_TIME_FORMAT_OPTIONS = [ - ['smart_date', 'Adaptative formating'], + ['smart_date', t('Adaptative formating')], ['%d/%m/%Y', '%d/%m/%Y | 14/01/2019'], ['%m/%d/%Y', '%m/%d/%Y | 01/14/2019'], ['%Y-%m-%d', '%Y-%m-%d | 2019-01-14'], @@ -118,31 +97,12 @@ const timeColumnOption = { description: t('A reference to the [Time] configuration, taking granularity into account'), }; -type StateDatasource = { - columns: ColumnOptionColumn[]; - metrics: unknown[]; - type: unknown; - main_dttm_col: unknown; - time_grain_sqla: unknown; -}; - -type State = { - form_data: { [key: string]: unknown }; - datasource?: StateDatasource | null; - options?: ColumnOptionColumn[]; - controls?: { - comparison_type?: { - value: string; - }; - }; -}; - type Control = { savedMetrics?: Metric[] | null; default?: unknown; }; -const groupByControl = { +const groupByControl: ControlConfig = { type: 'SelectControl', queryField: 'groupby', multi: true, @@ -151,28 +111,29 @@ const groupByControl = { default: [], includeTime: false, description: t('One or many controls to group by'), - optionRenderer: (c: ColumnOptionColumn) => , - valueRenderer: (c: ColumnOptionColumn) => , + optionRenderer: (c: ColumnMeta) => , + valueRenderer: (c: ColumnMeta) => , valueKey: 'column_name', allowAll: true, - filterOption: (opt: ColumnOptionColumn, text: string) => + filterOption: (opt: ColumnMeta, text: string) => (opt.column_name && opt.column_name.toLowerCase().includes(text.toLowerCase())) || (opt.verbose_name && opt.verbose_name.toLowerCase().includes(text.toLowerCase())), promptTextCreator: (label: unknown) => label, - mapStateToProps: (state: State, control?: { includeTime: boolean }) => { - const newState: State = {} as any; + mapStateToProps(state, { includeTime }) { + const newState: ExtraControlProps = {}; if (state.datasource) { - newState.options = state.datasource.columns.filter(c => c.groupby); - if (control?.includeTime) { - newState.options.push(timeColumnOption); + const options = state.datasource.columns.filter(c => c.groupby); + if (includeTime) { + options.push(timeColumnOption); } + newState.options = options; } return newState; }, commaChoosesOption: false, }; -const metrics = { +const metrics: ControlConfig = { type: 'MetricsControl', queryField: 'metrics', multi: true, @@ -182,7 +143,7 @@ const metrics = { const metric = mainMetric(c.savedMetrics); return metric ? [metric] : null; }, - mapStateToProps: ({ datasource }: State) => { + mapStateToProps: ({ datasource }) => { return { columns: datasource ? datasource.columns : [], savedMetrics: datasource ? datasource.metrics : [], @@ -191,7 +152,7 @@ const metrics = { }, description: t('One or many metrics to display'), }; -const metric = { +const metric: ControlConfig = { ...metrics, multi: false, label: t('Metric'), @@ -199,7 +160,7 @@ const metric = { default: (c: Control) => mainMetric(c.savedMetrics), }; -export function columnChoices(datasource: StateDatasource) { +export function columnChoices(datasource: DatasourceMeta) { if (datasource?.columns) { return datasource.columns .map(col => [col.column_name, col.verbose_name || col.column_name]) @@ -208,315 +169,335 @@ export function columnChoices(datasource: StateDatasource) { return []; } -export const controls = { - metrics, - - metric, - - datasource: { - type: 'DatasourceControl', - label: t('Datasource'), - default: null, - description: null, - mapStateToProps: (state: State, control: unknown, actions: { setDatasource: unknown }) => ({ - datasource: state.datasource, - onDatasourceSave: actions ? actions.setDatasource : () => {}, - }), - }, - - viz_type: { - type: 'VizTypeControl', - label: t('Visualization Type'), - default: 'table', - description: t('The type of visualization to display'), - }, +const datasourceControl: ControlConfig = { + type: 'DatasourceControl', + label: t('Datasource'), + default: null, + description: null, + mapStateToProps: (state, control, { setDatasource }) => ({ + datasource: state.datasource, + onDatasourceSave: setDatasource, + }), +}; - color_picker: { - label: t('Fixed Color'), - description: t('Use this to define a static color for all circles'), - type: 'ColorPickerControl', - default: PRIMARY_COLOR, - renderTrigger: true, - }, +const viz_type: ControlConfig = { + type: 'VizTypeControl', + label: t('Visualization Type'), + default: 'table', + description: t('The type of visualization to display'), +}; - metric_2: { - ...metric, - label: t('Right Axis Metric'), - clearable: true, - description: t('Choose a metric for right axis'), - }, +const color_picker: ControlConfig = { + label: t('Fixed Color'), + description: t('Use this to define a static color for all circles'), + type: 'ColorPickerControl', + default: PRIMARY_COLOR, + renderTrigger: true, +}; - linear_color_scheme: { - type: 'ColorSchemeControl', - label: t('Linear Color Scheme'), - choices: () => - (sequentialSchemeRegistry.values() as SequentialScheme[]).map(value => [ - value.id, - value.label, - ]), - default: sequentialSchemeRegistry.getDefaultKey(), - clearable: false, - description: '', - renderTrigger: true, - schemes: () => sequentialSchemeRegistry.getMap(), - isLinear: true, - }, +const metric_2: ControlConfig = { + ...metric, + label: t('Right Axis Metric'), + clearable: true, + description: t('Choose a metric for right axis'), +}; - secondary_metric: { - ...metric, - label: t('Color Metric'), - default: null, - validators: [], - description: t('A metric to use for color'), - }, +const linear_color_scheme: ControlConfig = { + type: 'ColorSchemeControl', + label: t('Linear Color Scheme'), + choices: () => + (sequentialSchemeRegistry.values() as SequentialScheme[]).map(value => [value.id, value.label]), + default: sequentialSchemeRegistry.getDefaultKey(), + clearable: false, + description: '', + renderTrigger: true, + schemes: () => sequentialSchemeRegistry.getMap(), + isLinear: true, +}; - groupby: groupByControl, +const secondary_metric: ControlConfig = { + ...metric, + label: t('Color Metric'), + default: null, + validators: [], + description: t('A metric to use for color'), +}; - columns: { - ...groupByControl, - label: t('Columns'), - description: t('One or many controls to pivot as columns'), - }, +const columnsControl: ControlConfig = { + ...groupByControl, + label: t('Columns'), + description: t('One or many controls to pivot as columns'), +}; - druid_time_origin: { - type: 'SelectControl', - freeForm: true, - label: TIME_FILTER_LABELS.druid_time_origin, - choices: [ - ['', 'default'], - ['now', 'now'], - ], - default: null, - description: t( - 'Defines the origin where time buckets start, ' + - 'accepts natural dates as in `now`, `sunday` or `1970-01-01`', - ), - }, +const druid_time_origin: ControlConfig = { + type: 'SelectControl', + freeForm: true, + label: TIME_FILTER_LABELS.druid_time_origin, + choices: [ + ['', 'default'], + ['now', 'now'], + ], + default: null, + description: t( + 'Defines the origin where time buckets start, ' + + 'accepts natural dates as in `now`, `sunday` or `1970-01-01`', + ), +}; - granularity: { - type: 'SelectControl', - freeForm: true, - label: TIME_FILTER_LABELS.granularity, - default: 'one day', - choices: [ - [null, 'all'], - ['PT5S', '5 seconds'], - ['PT30S', '30 seconds'], - ['PT1M', '1 minute'], - ['PT5M', '5 minutes'], - ['PT30M', '30 minutes'], - ['PT1H', '1 hour'], - ['PT6H', '6 hour'], - ['P1D', '1 day'], - ['P7D', '7 days'], - ['P1W', 'week'], - ['week_starting_sunday', 'week starting Sunday'], - ['week_ending_saturday', 'week ending Saturday'], - ['P1M', 'month'], - ['P3M', 'quarter'], - ['P1Y', 'year'], - ], - description: t( - 'The time granularity for the visualization. Note that you ' + - 'can type and use simple natural language as in `10 seconds`, ' + - '`1 day` or `56 weeks`', - ), - }, +const granularity: ControlConfig = { + type: 'SelectControl', + freeForm: true, + label: TIME_FILTER_LABELS.granularity, + default: 'one day', + choices: [ + [null, 'all'], + ['PT5S', '5 seconds'], + ['PT30S', '30 seconds'], + ['PT1M', '1 minute'], + ['PT5M', '5 minutes'], + ['PT30M', '30 minutes'], + ['PT1H', '1 hour'], + ['PT6H', '6 hour'], + ['P1D', '1 day'], + ['P7D', '7 days'], + ['P1W', 'week'], + ['week_starting_sunday', 'week starting Sunday'], + ['week_ending_saturday', 'week ending Saturday'], + ['P1M', 'month'], + ['P3M', 'quarter'], + ['P1Y', 'year'], + ], + description: t( + 'The time granularity for the visualization. Note that you ' + + 'can type and use simple natural language as in `10 seconds`, ' + + '`1 day` or `56 weeks`', + ), +}; - granularity_sqla: { - type: 'SelectControl', - label: TIME_FILTER_LABELS.granularity_sqla, - description: t( - 'The time column for the visualization. Note that you ' + - 'can define arbitrary expression that return a DATETIME ' + - 'column in the table. Also note that the ' + - 'filter below is applied against this column or ' + - 'expression', - ), - default: (c: Control) => c.default, - clearable: false, - optionRenderer: (c: ColumnOptionColumn) => , - valueRenderer: (c: ColumnOptionColumn) => , - valueKey: 'column_name', - mapStateToProps: (state: State) => { - const props: any = {}; - if (state.datasource) { - props.options = state.datasource.columns.filter(c => c.is_dttm); - props.default = null; - if (state.datasource.main_dttm_col) { - props.default = state.datasource.main_dttm_col; - } else if (props.options && props.options.length > 0) { - props.default = props.options[0].column_name; - } +const granularity_sqla: ControlConfig = { + type: 'SelectControl', + label: TIME_FILTER_LABELS.granularity_sqla, + description: t( + 'The time column for the visualization. Note that you ' + + 'can define arbitrary expression that return a DATETIME ' + + 'column in the table. Also note that the ' + + 'filter below is applied against this column or ' + + 'expression', + ), + default: (c: Control) => c.default, + clearable: false, + optionRenderer: (c: ColumnMeta) => , + valueRenderer: (c: ColumnMeta) => , + valueKey: 'column_name', + mapStateToProps: state => { + const props: Partial> = {}; + if (state.datasource) { + props.options = state.datasource.columns.filter(c => c.is_dttm); + props.default = null; + if (state.datasource.main_dttm_col) { + props.default = state.datasource.main_dttm_col; + } else if (props.options && props.options.length > 0) { + props.default = props.options[0].column_name; } - return props; - }, + } + return props; }, +}; - time_grain_sqla: { - type: 'SelectControl', - label: TIME_FILTER_LABELS.time_grain_sqla, - default: 'P1D', - description: t( - 'The time granularity for the visualization. This ' + - 'applies a date transformation to alter ' + - 'your time column and defines a new time granularity. ' + - 'The options here are defined on a per database ' + - 'engine basis in the Superset source code.', - ), - mapStateToProps: (state: State) => ({ - choices: state.datasource ? state.datasource.time_grain_sqla : null, - }), - }, +const time_grain_sqla: ControlConfig = { + type: 'SelectControl', + label: TIME_FILTER_LABELS.time_grain_sqla, + default: 'P1D', + description: t( + 'The time granularity for the visualization. This ' + + 'applies a date transformation to alter ' + + 'your time column and defines a new time granularity. ' + + 'The options here are defined on a per database ' + + 'engine basis in the Superset source code.', + ), + mapStateToProps: ({ datasource }) => ({ + choices: datasource?.time_grain_sqla || null, + }), +}; - time_range: { - type: 'DateFilterControl', - freeForm: true, - label: TIME_FILTER_LABELS.time_range, - default: t('Last week'), // this value is translated, but the backend wouldn't understand a translated value? - description: t( - 'The time range for the visualization. All relative times, e.g. "Last month", ' + - '"Last 7 days", "now", etc. are evaluated on the server using the server\'s ' + - 'local time (sans timezone). All tooltips and placeholder times are expressed ' + - 'in UTC (sans timezone). The timestamps are then evaluated by the database ' + - "using the engine's local timezone. Note one can explicitly set the timezone " + - 'per the ISO 8601 format if specifying either the start and/or end time.', - ), - mapStateToProps: (state: State) => ({ - endpoints: state.form_data ? state.form_data.time_range_endpoints : null, - }), - }, +const time_range: ControlConfig = { + type: 'DateFilterControl', + freeForm: true, + label: TIME_FILTER_LABELS.time_range, + default: t('Last week'), // this value is translated, but the backend wouldn't understand a translated value? + description: t( + 'The time range for the visualization. All relative times, e.g. "Last month", ' + + '"Last 7 days", "now", etc. are evaluated on the server using the server\'s ' + + 'local time (sans timezone). All tooltips and placeholder times are expressed ' + + 'in UTC (sans timezone). The timestamps are then evaluated by the database ' + + "using the engine's local timezone. Note one can explicitly set the timezone " + + 'per the ISO 8601 format if specifying either the start and/or end time.', + ), + mapStateToProps: ({ form_data }) => ({ + endpoints: form_data?.time_range_endpoints || null, + }), +}; - row_limit: { - type: 'SelectControl', - freeForm: true, - label: t('Row limit'), - validators: [legacyValidateInteger], - default: 10000, - choices: formatSelectOptions(ROW_LIMIT_OPTIONS), - }, +const row_limit: ControlConfig = { + type: 'SelectControl', + freeForm: true, + label: t('Row limit'), + validators: [legacyValidateInteger], + default: 10000, + choices: formatSelectOptions(ROW_LIMIT_OPTIONS), +}; - limit: { - type: 'SelectControl', - freeForm: true, - label: t('Series limit'), - validators: [legacyValidateInteger], - choices: formatSelectOptions(SERIES_LIMITS), - description: t( - 'Limits the number of time series that get displayed. A sub query ' + - '(or an extra phase where sub queries are not supported) is applied to limit ' + - 'the number of time series that get fetched and displayed. This feature is useful ' + - 'when grouping by high cardinality dimension(s).', - ), - }, +const limit: ControlConfig = { + type: 'SelectControl', + freeForm: true, + label: t('Series limit'), + validators: [legacyValidateInteger], + choices: formatSelectOptions(SERIES_LIMITS), + description: t( + 'Limits the number of time series that get displayed. A sub query ' + + '(or an extra phase where sub queries are not supported) is applied to limit ' + + 'the number of time series that get fetched and displayed. This feature is useful ' + + 'when grouping by high cardinality dimension(s).', + ), +}; - timeseries_limit_metric: { - type: 'MetricsControl', - label: t('Sort By'), - default: null, - description: t('Metric used to define the top series'), - mapStateToProps: (state: State) => ({ - columns: state.datasource ? state.datasource.columns : [], - savedMetrics: state.datasource ? state.datasource.metrics : [], - datasourceType: state.datasource && state.datasource.type, - }), - }, +const timeseries_limit_metric: ControlConfig = { + type: 'MetricsControl', + label: t('Sort By'), + default: null, + description: t('Metric used to define the top series'), + mapStateToProps: ({ datasource }) => ({ + columns: datasource?.columns || [], + savedMetrics: datasource?.metrics || [], + datasourceType: datasource?.type, + }), +}; - series: { - ...groupByControl, - label: t('Series'), - multi: false, - default: null, - description: t( - 'Defines the grouping of entities. ' + - 'Each series is shown as a specific color on the chart and ' + - 'has a legend toggle', - ), - }, +const series: ControlConfig = { + ...groupByControl, + label: t('Series'), + multi: false, + default: null, + description: t( + 'Defines the grouping of entities. ' + + 'Each series is shown as a specific color on the chart and ' + + 'has a legend toggle', + ), +}; - entity: { - ...groupByControl, - label: t('Entity'), - default: null, - multi: false, - validators: [validateNonEmpty], - description: t('This defines the element to be plotted on the chart'), - }, +const entity: ControlConfig = { + ...groupByControl, + label: t('Entity'), + default: null, + multi: false, + validators: [validateNonEmpty], + description: t('This defines the element to be plotted on the chart'), +}; - x: { - ...metric, - label: t('X Axis'), - description: t('Metric assigned to the [X] axis'), - default: null, - }, +const x: ControlConfig = { + ...metric, + label: t('X Axis'), + description: t('Metric assigned to the [X] axis'), + default: null, +}; - y: { - ...metric, - label: t('Y Axis'), - default: null, - description: t('Metric assigned to the [Y] axis'), - }, +const y: ControlConfig = { + ...metric, + label: t('Y Axis'), + default: null, + description: t('Metric assigned to the [Y] axis'), +}; - size: { - ...metric, - label: t('Bubble Size'), - default: null, - }, +const size: ControlConfig = { + ...metric, + label: t('Bubble Size'), + default: null, +}; - y_axis_format: { - type: 'SelectControl', - freeForm: true, - label: t('Y Axis Format'), - renderTrigger: true, - default: 'SMART_NUMBER', - choices: D3_FORMAT_OPTIONS, - description: D3_FORMAT_DOCS, - mapStateToProps: (state: State) => { - const showWarning = state.controls?.comparison_type?.value === 'percentage'; - return { - warning: showWarning - ? t( - 'When `Calculation type` is set to "Percentage change", the Y ' + - 'Axis Format is forced to `.1%`', - ) - : null, - disabled: showWarning, - }; - }, +const y_axis_format: ControlConfig = { + type: 'SelectControl', + freeForm: true, + label: t('Y Axis Format'), + renderTrigger: true, + default: 'SMART_NUMBER', + choices: D3_FORMAT_OPTIONS, + description: D3_FORMAT_DOCS, + mapStateToProps: state => { + const showWarning = state.controls?.comparison_type?.value === 'percentage'; + return { + warning: showWarning + ? t( + 'When `Calculation type` is set to "Percentage change", the Y ' + + 'Axis Format is forced to `.1%`', + ) + : null, + disabled: showWarning, + }; }, +}; - adhoc_filters: { - type: 'AdhocFilterControl', - label: t('Filters'), - default: null, - description: '', - mapStateToProps: (state: State) => ({ - columns: state.datasource?.columns.filter(c => c.filterable) || [], - savedMetrics: state.datasource?.metrics || [], - datasource: state.datasource, - }), - provideFormDataToProps: true, - }, +const adhoc_filters: ControlConfig = { + type: 'AdhocFilterControl', + label: t('Filters'), + default: null, + description: '', + mapStateToProps: ({ datasource }) => ({ + columns: datasource?.columns.filter(c => c.filterable) || [], + savedMetrics: datasource?.metrics || [], + datasource, + }), + provideFormDataToProps: true, +}; - color_scheme: { - type: 'ColorSchemeControl', - label: t('Color Scheme'), - default: categoricalSchemeRegistry.getDefaultKey(), - renderTrigger: true, - choices: () => categoricalSchemeRegistry.keys().map(s => [s, s]), - description: t('The color scheme for rendering chart'), - schemes: () => categoricalSchemeRegistry.getMap(), - }, +const color_scheme: ControlConfig = { + type: 'ColorSchemeControl', + label: t('Color Scheme'), + default: categoricalSchemeRegistry.getDefaultKey(), + renderTrigger: true, + choices: () => categoricalSchemeRegistry.keys().map(s => [s, s]), + description: t('The color scheme for rendering chart'), + schemes: () => categoricalSchemeRegistry.getMap(), +}; - label_colors: { - type: 'ColorMapControl', - label: t('Color Map'), - default: {}, - renderTrigger: true, - mapStateToProps: (state: State) => ({ - colorNamespace: state.form_data.color_namespace, - colorScheme: state.form_data.color_scheme, - }), - }, +const label_colors: ControlConfig = { + type: 'ColorMapControl', + label: t('Color Map'), + default: {}, + renderTrigger: true, + mapStateToProps: ({ + form_data: { color_namespace: colorNamespace, color_scheme: colorScheme }, + }) => ({ + colorNamespace, + colorScheme, + }), +}; + +export default { + metrics, + metric, + datasource: datasourceControl, + viz_type, + color_picker, + metric_2, + linear_color_scheme, + secondary_metric, + groupby: groupByControl, + columns: columnsControl, + druid_time_origin, + granularity, + granularity_sqla, + time_grain_sqla, + time_range, + row_limit, + limit, + timeseries_limit_metric, + series, + entity, + x, + y, + size, + y_axis_format, + adhoc_filters, + color_scheme, + label_colors, }; -export default controls; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-control-utils/src/types.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-control-utils/src/types.ts new file mode 100644 index 000000000000..4e0e7d09ad4f --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-control-utils/src/types.ts @@ -0,0 +1,267 @@ +/* eslint-disable camelcase */ +/** + * 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 React, { ReactNode, ReactText } from 'react'; +import { QueryFormData } from '@superset-ui/query'; +import sharedControls from './shared-controls'; + +type AnyDict = Record; +interface Action { + type: string; +} +interface AnyAction extends Action, AnyDict {} + +/** ---------------------------------------------- + * Input data/props while rendering + * ---------------------------------------------*/ +export interface ColumnMeta extends AnyDict { + column_name: string; + groupby?: string; + verbose_name?: string; + description?: string; + expression?: string; + is_dttm?: boolean; + type?: string; + filterable?: boolean; +} + +export interface DatasourceMeta { + columns: ColumnMeta[]; + metrics: unknown[]; + type: unknown; + main_dttm_col: unknown; + time_grain_sqla: unknown; + order_by_choices?: [] | null; +} + +export interface ControlPanelState { + form_data: QueryFormData; + datasource?: DatasourceMeta | null; + options?: ColumnMeta[]; + controls?: { + comparison_type?: { + value: string; + }; + }; +} + +/** + * The action dispather will call Redux `dispatch` internally and return what's + * returned from `dispatch`, which by default is the original or another action. + */ +export interface ActionDispatcher { + (...args: ARGS): A; +} + +/** + * Mapping of action dispatchers + */ +export interface ControlPanelActionDispathers { + setDatasource: ActionDispatcher<[DatasourceMeta]>; +} + +/** + * Additional control props obtained from `mapStateToProps`. + */ +export type ExtraControlProps = AnyDict; + +// Ref:superset-frontend/src/explore/store.js +export type ControlState = ControlConfig & + ExtraControlProps; + +export interface ControlStateMapping { + [key: string]: ControlState; +} + +// Ref: superset-frontend/src/explore/components/ControlPanelsContainer.jsx +export interface ControlPanelsContainerProps extends AnyDict { + actions: ControlPanelActionDispathers; + controls: ControlStateMapping; + exportState: AnyDict; + form_data: QueryFormData; +} + +/** ---------------------------------------------- + * Config for a chart Control + * ---------------------------------------------*/ + +// Ref: superset-frontend/src/explore/components/controls/index.js +export type InternalControlType = + | 'AnnotationLayerControl' + | 'BoundsControl' + | 'CheckboxControl' + | 'CollectionControl' + | 'ColorMapControl' + | 'ColorPickerControl' + | 'ColorSchemeControl' + | 'DatasourceControl' + | 'DateFilterControl' + | 'FixedOrMetricControl' + | 'HiddenControl' + | 'SelectAsyncControl' + | 'SelectControl' + | 'SliderControl' + | 'SpatialControl' + | 'TextAreaControl' + | 'TextControl' + | 'TimeSeriesColumnControl' + | 'ViewportControl' + | 'VizTypeControl' + | 'MetricsControl' + | 'AdhocFilterControl' + | 'FilterBoxItemControl' + | 'MetricsControlVerifiedOptions' + | 'SelectControlVerifiedOptions' + | 'AdhocFilterControlVerifiedOptions'; + +export interface Validator { + (value: unknown): boolean | string; +} + +export type TabOverride = 'data' | boolean; + +/** + * Control config specifying how chart controls appear in the control panel, all + * these configs will be passed to the UI component for control as props. + * + * - type: the control type, referencing a React component of the same name + * - label: the label as shown in the control's header + * - description: shown in the info tooltip of the control's header + * - default: the default value when opening a new chart, or changing visualization type + * - renderTrigger: a bool that defines whether the visualization should be re-rendered + * when changed. This should `true` for controls that only affect the rendering (client side) + * and don't affect the query or backend data processing as those require to re run a query + * and fetch the data + * - validators: an array of functions that will receive the value of the component and + * should return error messages when the value is not valid. The error message gets + * bubbled up to the control header, section header and query panel header. + * - warning: text shown as a tooltip on a warning icon in the control's header + * - error: text shown as a tooltip on a error icon in the control's header + * - mapStateToProps: a function that receives the App's state and return an object of k/v + * to overwrite configuration at runtime. This is useful to alter a component based on + * anything external to it, like another control's value. For instance it's possible to + * show a warning based on the value of another component. It's also possible to bind + * arbitrary data from the redux store to the component this way. + * - tabOverride: set to 'data' if you want to force a renderTrigger to show up on the `Data` + * tab, otherwise `renderTrigger: true` components will show up on the `Style` tab. + * - visibility: a function that uses control panel props to check whether a control should + * be visibile. + */ +export interface GeneralControlConfig { + type: InternalControlType | React.ComponentType; + label?: ReactNode; + description?: ReactNode; + default?: unknown; + renderTrigger?: boolean; + validators?: Validator[]; + warning?: ReactNode; + error?: ReactNode; + // override control panel state props + mapStateToProps?: ( + state: ControlPanelState, + control: ControlConfig, + actions: ControlPanelActionDispathers, + ) => ExtraControlProps; + tabOverride?: TabOverride; + visibility?: (props: ControlPanelsContainerProps) => boolean; + [key: string]: unknown; +} +/** -------------------------------------------- + * Additional Config for specific control Types + * --------------------------------------------- */ +type SelectOption = AnyDict | string | [ReactText, ReactNode]; +type SelectControlType = + | 'SelectControl' + | 'SelectAsyncControl' + | 'SelectControl' + | 'MetricsControl' + | 'FixedOrMetricControl' + | 'AdhocFilterControl' + | 'FilterBoxItemControl' + | 'MetricsControlVerifiedOptions' + | 'SelectControlVerifiedOptions' + | 'AdhocFilterControlVerifiedOptions'; + +export interface SelectControlConfig + extends GeneralControlConfig { + type: SelectControlType; + options?: T[]; + clearable?: boolean; + freeForm?: boolean; + multi?: boolean; + optionRenderer?: (option: T) => ReactNode; + valueRenderer?: (option: T) => ReactNode; + valueKey?: string; + labelKey?: string; +} + +export type ControlConfig = + | GeneralControlConfig + | SelectControlConfig; + +/** -------------------------------------------- + * Chart plugin control panel config + * --------------------------------------------- */ +export type SharedControlAlias = keyof typeof sharedControls; + +export type SharedSectionAlias = + | 'annotations' + | 'colorScheme' + | 'datasourceAndVizType' + | 'druidTimeSeries' + | 'sqlaTimeSeries' + | 'NVD3TimeSeries'; + +export interface ControlItem { + name: SharedControlAlias; + config: Partial; +} + +export interface CustomControlItem { + name: string; + config: ControlConfig; +} + +export type ControlSetItem = SharedControlAlias | ControlItem | CustomControlItem | ReactNode; +export type ControlSetRow = ControlSetItem[]; + +// Ref: +// - superset-frontend/src/explore/components/ControlPanelsContainer.jsx +// - superset-frontend/src/explore/components/ControlPanelSection.jsx +export interface ControlPanelSectionConfig { + label: ReactNode; + controlSetRows: ControlSetRow[]; + description?: ReactNode; + expanded?: boolean; + tabOverride?: TabOverride; +} + +export interface ControlPanelConfig { + controlPanelSections: ControlPanelSectionConfig[]; + controlOverrides?: ControlOverrides; + sectionOverrides?: SectionOverrides; +} + +export type ControlOverrides = { + [P in SharedControlAlias]?: Partial; +}; + +export type SectionOverrides = { + [P in SharedSectionAlias]?: Partial; +}; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/legacy-plugin-chart-table/src/controlPanel.tsx b/superset-frontend/temporary_superset_ui/superset-ui/plugins/legacy-plugin-chart-table/src/controlPanel.tsx index 527d76b88ad1..345ffab2d1b6 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/plugins/legacy-plugin-chart-table/src/controlPanel.tsx +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/legacy-plugin-chart-table/src/controlPanel.tsx @@ -1,3 +1,4 @@ +/* eslint-disable camelcase */ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file @@ -22,10 +23,11 @@ import { formatSelectOptions, D3_TIME_FORMAT_OPTIONS, ColumnOption, + ControlPanelConfig, } from '@superset-ui/control-utils'; import { validateNonEmpty } from '@superset-ui/validator'; -export default { +const config: ControlPanelConfig = { controlPanelSections: [ { label: t('GROUP BY'), @@ -40,13 +42,11 @@ export default { config: { type: 'MetricsControl', multi: true, - mapStateToProps: (state: never) => { - const { datasource } = state; - const { columns, metrics, type } = datasource; + mapStateToProps: ({ datasource }) => { return { - columns: datasource ? columns : [], - savedMetrics: datasource ? metrics : [], - datasourceType: datasource && type, + columns: datasource?.columns || [], + savedMetrics: datasource?.metrics || [], + datasourceType: datasource?.type, }; }, default: [], @@ -99,8 +99,8 @@ export default { valueRenderer: (c: never) => , valueKey: 'column_name', allowAll: true, - mapStateToProps: (state: { datasource: { columns: unknown } }) => ({ - options: state.datasource ? state.datasource.columns : [], + mapStateToProps: ({ datasource }) => ({ + options: datasource?.columns || [], }), commaChoosesOption: false, freeForm: true, @@ -116,9 +116,8 @@ export default { label: t('Ordering'), default: [], description: t('One or many metrics to display'), - // eslint-disable-next-line camelcase - mapStateToProps: (state: { datasource: { order_by_choices: never } }) => ({ - choices: state.datasource ? state.datasource.order_by_choices : [], + mapStateToProps: ({ datasource }) => ({ + choices: datasource?.order_by_choices || [], }), }, }, @@ -240,3 +239,5 @@ export default { }, }, }; + +export default config; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/legacy-preset-chart-big-number/src/BigNumber/controlPanel.tsx b/superset-frontend/temporary_superset_ui/superset-ui/plugins/legacy-preset-chart-big-number/src/BigNumber/controlPanel.tsx index a98696121df9..1f2968aa30a5 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/plugins/legacy-preset-chart-big-number/src/BigNumber/controlPanel.tsx +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/legacy-preset-chart-big-number/src/BigNumber/controlPanel.tsx @@ -17,16 +17,11 @@ * under the License. */ import { t } from '@superset-ui/translation'; -import { formatSelectOptions } from '@superset-ui/control-utils'; +import { formatSelectOptions, ControlPanelConfig } from '@superset-ui/control-utils'; import React from 'react'; import { headerFontSize, subheaderFontSize } from '../sharedControls'; -type VisibilityProps = { - // eslint-disable-next-line camelcase - form_data: { time_range: string }; -}; - -export default { +const config: ControlPanelConfig = { controlPanelSections: [ { label: t('Query'), @@ -92,10 +87,10 @@ export default { 'Fix the trend line to the full time range specified in case filtered results do not include the start or end dates', ), renderTrigger: true, - visibility(props: VisibilityProps) { + visibility(props) { const { time_range: timeRange } = props.form_data; // only display this option when a time range is selected - return timeRange && timeRange !== 'No filter'; + return !!timeRange && timeRange !== 'No filter'; }, }, }, @@ -172,3 +167,5 @@ export default { }, }, }; + +export default config; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/legacy-preset-chart-big-number/src/sharedControls.ts b/superset-frontend/temporary_superset_ui/superset-ui/plugins/legacy-preset-chart-big-number/src/sharedControls.ts index a4df9bcede6d..1165155a618f 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/plugins/legacy-preset-chart-big-number/src/sharedControls.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/legacy-preset-chart-big-number/src/sharedControls.ts @@ -19,8 +19,9 @@ // These are control configurations that are shared ONLY within the BigNumber viz plugin repo. import { t } from '@superset-ui/translation'; +import { CustomControlItem } from '@superset-ui/control-utils'; -export const headerFontSize = { +export const headerFontSize: CustomControlItem = { name: 'header_font_size', config: { type: 'SelectControl', @@ -54,7 +55,7 @@ export const headerFontSize = { }, }; -export const subheaderFontSize = { +export const subheaderFontSize: CustomControlItem = { name: 'subheader_font_size', config: { type: 'SelectControl',