From aade5ef42c1a8e0896bcb4c1aa5f60bb411f83c7 Mon Sep 17 00:00:00 2001 From: Jesse Yang Date: Tue, 23 Jun 2020 15:11:12 -0700 Subject: [PATCH] feat(legacy-table-chart): add query mode switch (#609) --- .../superset-ui-chart-controls/package.json | 1 + .../src/components/RadioButtonControl.tsx | 91 +++++++ .../superset-ui-chart-controls/src/index.ts | 4 + .../src/shared-controls/components.tsx | 28 +++ .../index.tsx} | 10 +- .../superset-ui-chart-controls/src/types.ts | 99 ++++---- .../src/utils/expandControlConfig.tsx | 82 ++++++ .../src/utils/selectOptions.ts | 6 +- .../test/utils/expandControlConfig.test.tsx | 64 +++++ .../legacy-plugin-chart-table/package.json | 1 + .../src/controlPanel.tsx | 230 +++++++++++------ .../plugin-chart-table/src/controlPanel.tsx | 234 ++++++++++++------ .../plugin-chart-table/src/transformProps.ts | 7 +- 13 files changed, 647 insertions(+), 210 deletions(-) create mode 100644 superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/components/RadioButtonControl.tsx create mode 100644 superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/shared-controls/components.tsx rename superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/{shared-controls.tsx => shared-controls/index.tsx} (98%) create mode 100644 superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/utils/expandControlConfig.tsx create mode 100644 superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/test/utils/expandControlConfig.test.tsx diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/package.json b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/package.json index 05bf2d73c8..76c8a1b62a 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/package.json +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/package.json @@ -28,6 +28,7 @@ "peerDependencies": { "@superset-ui/color": "^0.14.0", "@superset-ui/query": "^0.14.0", + "@superset-ui/style": "^0.14.0", "@superset-ui/translation": "^0.14.0", "@superset-ui/validator": "^0.14.0", "react": "^16.13.1" diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/components/RadioButtonControl.tsx b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/components/RadioButtonControl.tsx new file mode 100644 index 0000000000..7e517f373e --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/components/RadioButtonControl.tsx @@ -0,0 +1,91 @@ +/** + * 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, { ReactText, ReactNode, MouseEvent, useCallback } from 'react'; +import styled from '@superset-ui/style'; +import { InfoTooltipWithTrigger } from './InfoTooltipWithTrigger'; + +export interface RadioButtonOption { + label: string; + value: ReactText; +} + +export interface RadioButtonControlProps { + label?: ReactNode; + description?: string; + options: RadioButtonOption[]; + hovered?: boolean; + value?: string; + onChange: (opt: string) => void; +} + +const Styles = styled.div` + .btn:focus { + outline: none; + } + .control-label + .btn-group { + margin-top: 1px; + } + .btn-group .btn.active { + background: ${({ theme }) => theme.colors.secondary.light5}; + box-shadow: none; + font-weight: ${({ theme }) => theme.typography.weights.bold}; + } +`; + +export default function RadioButtonControl({ + label: controlLabel, + description, + value: initialValue, + hovered, + options, + onChange, +}: RadioButtonControlProps) { + const currentValue = initialValue || options[0].value; + const onClick = useCallback( + (e: MouseEvent) => { + onChange(e.currentTarget.value); + }, + [onChange], + ); + return ( + + {controlLabel && ( +
+ {controlLabel}{' '} + {hovered && description && ( + + )} +
+ )} +
+ {options.map(({ label, value }, i) => ( + + ))} +
+
+ ); +} diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/index.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/index.ts index 712effa6d2..5f19931d4a 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/index.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/index.ts @@ -12,10 +12,14 @@ export const sections = sectionModules; 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 './utils/expandControlConfig'; export * from './components/InfoTooltipWithTrigger'; export * from './components/ColumnOption'; export * from './components/ColumnTypeLabel'; export * from './components/MetricOption'; +// React control components +export * from './components/RadioButtonControl'; + export * from './types'; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/shared-controls/components.tsx b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/shared-controls/components.tsx new file mode 100644 index 0000000000..bc019643c3 --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/shared-controls/components.tsx @@ -0,0 +1,28 @@ +/** + * 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 RadioButtonControl from '../components/RadioButtonControl'; + +export * from '../components/RadioButtonControl'; + +/** + * Aliases for Control Components + */ +export default { + RadioButtonControl, +}; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/shared-controls.tsx b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/shared-controls/index.tsx similarity index 98% rename from superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/shared-controls.tsx rename to superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/shared-controls/index.tsx index 085e1bbdfa..a669c7fe06 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/shared-controls.tsx +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/shared-controls/index.tsx @@ -42,17 +42,17 @@ import { } from '@superset-ui/color'; import { legacyValidateInteger, validateNonEmpty } from '@superset-ui/validator'; -import { formatSelectOptions } from './utils/selectOptions'; -import { mainMetric, Metric } from './utils/mainMetric'; -import { TIME_FILTER_LABELS } from './constants'; +import { formatSelectOptions } from '../utils/selectOptions'; +import { mainMetric, Metric } from '../utils/mainMetric'; +import { TIME_FILTER_LABELS } from '../constants'; import { SharedControlConfig, ColumnMeta, DatasourceMeta, ExtraControlProps, SelectControlConfig, -} from './types'; -import { ColumnOption } from './components/ColumnOption'; +} from '../types'; +import { ColumnOption } from '../components/ColumnOption'; const categoricalSchemeRegistry = getCategoricalSchemeRegistry(); const sequentialSchemeRegistry = getSequentialSchemeRegistry(); diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/types.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/types.ts index 0eeac7e3f0..bf0a56af5f 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/types.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/types.ts @@ -20,13 +20,19 @@ import React, { ReactNode, ReactText, ReactElement } from 'react'; import { QueryFormData } from '@superset-ui/query'; import sharedControls from './shared-controls'; +import sharedControlComponents from './shared-controls/components'; -type AnyDict = Record; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type AnyDict = Record; interface Action { type: string; } interface AnyAction extends Action, AnyDict {} +export type SharedControls = typeof sharedControls; +export type SharedControlAlias = keyof typeof sharedControls; +export type SharedControlComponents = typeof sharedControlComponents; + /** ---------------------------------------------- * Input data/props while rendering * ---------------------------------------------*/ @@ -77,10 +83,11 @@ export interface ControlPanelActionDispatchers { export type ExtraControlProps = AnyDict; // Ref:superset-frontend/src/explore/store.js -export type ControlState< - T extends InternalControlType | unknown = InternalControlType, - O extends SelectOption = SelectOption -> = ControlConfig & ExtraControlProps; +export type ControlState = ControlConfig< + T, + O +> & + ExtraControlProps; export interface ControlStateMapping { [key: string]: ControlState; @@ -125,11 +132,11 @@ export type InternalControlType = | 'FilterBoxItemControl' | 'MetricsControlVerifiedOptions' | 'SelectControlVerifiedOptions' - | 'AdhocFilterControlVerifiedOptions'; + | 'AdhocFilterControlVerifiedOptions' + | keyof SharedControlComponents; // expanded in `expandControlConfig` -export interface ControlValueValidator { - (value: T, state: ControlState): boolean | string; -} +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type ControlType = InternalControlType | React.ComponentType; export type TabOverride = 'data' | boolean; @@ -160,13 +167,17 @@ export type TabOverride = 'data' | boolean; * - visibility: a function that uses control panel props to check whether a control should * be visibile. */ -export interface BaseControlConfig { +export interface BaseControlConfig< + T extends ControlType = ControlType, + O extends SelectOption = SelectOption, + V = unknown +> extends AnyDict { type: T; label?: ReactNode; description?: ReactNode; - default?: unknown; + default?: V; renderTrigger?: boolean; - validators?: ControlValueValidator[]; + validators?: ControlValueValidator[]; warning?: ReactNode; error?: ReactNode; // override control panel state props @@ -177,7 +188,14 @@ export interface BaseControlConfig { ) => ExtraControlProps; tabOverride?: TabOverride; visibility?: (props: ControlPanelsContainerProps) => boolean; - [key: string]: unknown; +} + +export interface ControlValueValidator< + T = ControlType, + O extends SelectOption = SelectOption, + V = unknown +> { + (value: V, state: ControlState): boolean | string; } /** -------------------------------------------- @@ -207,7 +225,7 @@ interface FilterOption { export interface SelectControlConfig< O extends SelectOption = SelectOption, T extends SelectControlType = SelectControlType -> extends BaseControlConfig { +> extends BaseControlConfig { clearable?: boolean; freeForm?: boolean; multi?: boolean; @@ -227,24 +245,26 @@ export type SharedControlConfig< /** -------------------------------------------- * Custom controls * --------------------------------------------- */ -export type CustomComponentControlConfig

= BaseControlConfig< - InternalControlType | React.ComponentType

-> & - Omit; // two run-time properties from superset-frontend/src/explore/components/Control.jsx +export type CustomControlConfig

= BaseControlConfig> & + // two run-time properties from superset-frontend/src/explore/components/Control.jsx + Omit; // Catch-all ControlConfig -// - if T == known control types, return SharedControlConfig, +// - if T is known control types, return SharedControlConfig, +// - if T is object, assume a CustomComponent // - otherwise assume it's a custom component control export type ControlConfig< - T extends InternalControlType | unknown = InternalControlType, + T = AnyDict, O extends SelectOption = SelectOption -> = T extends InternalControlType ? SharedControlConfig : CustomComponentControlConfig; +> = T extends InternalControlType + ? SharedControlConfig + : T extends object + ? CustomControlConfig // eslint-disable-next-line @typescript-eslint/no-explicit-any + : CustomControlConfig; -/** -------------------------------------------- +/** =========================================================== * Chart plugin control panel config - * --------------------------------------------- */ -export type SharedControlAlias = keyof typeof sharedControls; - + * ========================================================= */ export type SharedSectionAlias = | 'annotations' | 'colorScheme' @@ -253,28 +273,23 @@ export type SharedSectionAlias = | 'sqlaTimeSeries' | 'NVD3TimeSeries'; -export interface OverrideSharedControlItem { - name: SharedControlAlias; - override: Partial; +export interface OverrideSharedControlItem { + name: A; + override: Partial; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export interface CustomControlItem

{ +export type CustomControlItem = { name: string; - config: CustomComponentControlConfig

; -} - -export type ControlSetItem = - | SharedControlAlias - | OverrideSharedControlItem - | CustomControlItem - // use ReactElement instead of ReactNode because `string`, `number`, etc. may - // interfere with other ControlSetItem types - | ReactElement - | null; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + config: BaseControlConfig; +}; +// use ReactElement instead of ReactNode because `string`, `number`, etc. may +// interfere with other ControlSetItem types export type ExpandedControlItem = CustomControlItem | ReactElement | null; +export type ControlSetItem = SharedControlAlias | OverrideSharedControlItem | ExpandedControlItem; + export type ControlSetRow = ControlSetItem[]; // Ref: @@ -296,7 +311,7 @@ export interface ControlPanelConfig { } export type ControlOverrides = { - [P in SharedControlAlias]?: Partial; + [P in SharedControlAlias]?: Partial; }; export type SectionOverrides = { diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/utils/expandControlConfig.tsx b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/utils/expandControlConfig.tsx new file mode 100644 index 0000000000..b167786d9c --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/utils/expandControlConfig.tsx @@ -0,0 +1,82 @@ +/** + * 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, { ReactElement } from 'react'; +import sharedControls from '../shared-controls'; +import sharedControlComponents from '../shared-controls/components'; +import { ControlType, ControlSetItem, ExpandedControlItem, ControlOverrides } from '../types'; + +export function expandControlType(controlType: ControlType) { + if (typeof controlType === 'string' && controlType in sharedControlComponents) { + return sharedControlComponents[controlType as keyof typeof sharedControlComponents]; + } + return controlType; +} + +/** + * Expand a shorthand control config item to full config in the format of + * { + * name: ..., + * config: { + * type: ..., + * ... + * } + * } + */ +export function expandControlConfig( + control: ControlSetItem, + controlOverrides: ControlOverrides = {}, +): ExpandedControlItem { + // one of the named shared controls + if (typeof control === 'string' && control in sharedControls) { + const name = control; + return { + name, + config: { + ...sharedControls[name], + ...controlOverrides[name], + }, + }; + } + // JSX/React element or NULL + if (!control || typeof control === 'string' || React.isValidElement(control)) { + return control as ReactElement; + } + // already fully expanded control config + if ('name' in control && 'config' in control) { + return { + ...control, + config: { + ...control.config, + type: expandControlType(control.config.type as ControlType), + }, + }; + } + // apply overrides with shared controls + if ('override' in control && control.name in sharedControls) { + const { name, override } = control; + return { + name, + config: { + ...sharedControls[name], + ...override, + }, + }; + } + return null; +} diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/utils/selectOptions.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/utils/selectOptions.ts index c4bb0ecdf1..666ab35f45 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/utils/selectOptions.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/utils/selectOptions.ts @@ -31,9 +31,9 @@ export function formatSelectOptions( } /** - * outputs array of arrays - * formatSelectOptionsForRange(1, 5) - * returns [[1,'1'], [2,'2'], [3,'3'], [4,'4'], [5,'5']] + * Outputs array of arrays + * >> formatSelectOptionsForRange(1, 5) + * >> [[1,'1'], [2,'2'], [3,'3'], [4,'4'], [5,'5']] */ export function formatSelectOptionsForRange(start: number, end: number) { const options: Formatted[] = []; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/test/utils/expandControlConfig.test.tsx b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/test/utils/expandControlConfig.test.tsx new file mode 100644 index 0000000000..bc1370db30 --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/test/utils/expandControlConfig.test.tsx @@ -0,0 +1,64 @@ +/** + * 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 from 'react'; +import { expandControlConfig, sharedControls } from '../../src'; + +describe('expandControlConfig()', () => { + it('expands shared control alias', () => { + expect(expandControlConfig('metrics')).toEqual({ + name: 'metrics', + config: sharedControls.metrics, + }); + }); + it('expands control with overrides', () => { + expect( + expandControlConfig({ + name: 'metrics', + override: { + label: 'Custom Metric', + }, + }), + ).toEqual({ + name: 'metrics', + config: { + ...sharedControls.metrics, + label: 'Custom Metric', + }, + }); + }); + it('leave full control untouched', () => { + const input = { + name: 'metrics', + config: { + type: 'SelectControl', + label: 'Custom Metric', + }, + }; + expect(expandControlConfig(input)).toEqual(input); + }); + it('leave NULL and ReactElement untouched', () => { + expect(expandControlConfig(null)).toBeNull(); + const input =

Test

; + expect(expandControlConfig(input)).toBe(input); + }); + it('leave unknown text untouched', () => { + const input = 'superset-ui'; + expect(expandControlConfig(input as never)).toBe(input); + }); +}); diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/legacy-plugin-chart-table/package.json b/superset-frontend/temporary_superset_ui/superset-ui/plugins/legacy-plugin-chart-table/package.json index 864652131d..c09ddd6247 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/plugins/legacy-plugin-chart-table/package.json +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/legacy-plugin-chart-table/package.json @@ -38,6 +38,7 @@ "@superset-ui/chart-controls": "^0.14.0", "@superset-ui/number-format": "^0.14.0", "@superset-ui/query": "^0.14.0", + "@superset-ui/style": "^0.14.0", "@superset-ui/time-format": "^0.14.0", "@superset-ui/translation": "^0.14.0", "@superset-ui/validator": "^0.14.0", 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 ccd25c0dcd..a8296a4d3c 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 @@ -22,41 +22,168 @@ import { t } from '@superset-ui/translation'; import { formatSelectOptions, D3_TIME_FORMAT_OPTIONS, + ControlConfig, ColumnOption, + ControlStateMapping, ControlPanelConfig, + ControlPanelsContainerProps, + sharedControls, } from '@superset-ui/chart-controls'; import { validateNonEmpty } from '@superset-ui/validator'; +import { smartDateFormatter } from '@superset-ui/time-format'; + +export const PAGE_SIZE_OPTIONS = formatSelectOptions([[0, t('All')], 10, 20, 50, 100, 200]); + +export enum QueryMode { + aggregate = 'aggregate', + raw = 'raw', +} + +const QueryModeLabel = { + [QueryMode.aggregate]: t('Aggregate'), + [QueryMode.raw]: t('Raw Records'), +}; + +function getQueryMode(controls: ControlStateMapping): QueryMode { + const mode = controls?.query_mode?.value; + if (mode === QueryMode.aggregate || mode === QueryMode.raw) { + return mode as QueryMode; + } + const groupby = controls?.groupby?.value; + const hasGroupBy = groupby && (groupby as string[])?.length > 0; + return hasGroupBy ? QueryMode.aggregate : QueryMode.raw; +} + +/** + * Visibility check + */ +function isQueryMode(mode: QueryMode) { + return ({ controls }: ControlPanelsContainerProps) => { + return getQueryMode(controls) === mode; + }; +} + +const isAggMode = isQueryMode(QueryMode.aggregate); +const isRawMode = isQueryMode(QueryMode.raw); + +const queryMode: ControlConfig<'RadioButtonControl'> = { + type: 'RadioButtonControl', + label: t('Query Mode'), + default: QueryMode.aggregate, + options: [ + { + label: QueryModeLabel[QueryMode.aggregate], + value: QueryMode.aggregate, + }, + { + label: QueryModeLabel[QueryMode.raw], + value: QueryMode.raw, + }, + ], + mapStateToProps: ({ controls }) => { + return { value: getQueryMode(controls) }; + }, +}; + +const all_columns: typeof sharedControls.groupby = { + type: 'SelectControl', + label: t('Columns'), + description: t('Columns to display'), + multi: true, + freeForm: true, + allowAll: true, + commaChoosesOption: false, + default: [], + optionRenderer: c => , + valueRenderer: c => , + valueKey: 'column_name', + mapStateToProps: ({ datasource, controls }) => ({ + options: datasource?.columns || [], + queryMode: getQueryMode(controls), + }), + visibility: isRawMode, +}; + +const percent_metrics: typeof sharedControls.metrics = { + type: 'MetricsControl', + label: t('Percentage Metrics'), + description: t('Metrics for which percentage of total are to be displayed'), + multi: true, + visibility: isAggMode, + mapStateToProps: ({ datasource, controls }) => { + return { + columns: datasource?.columns || [], + savedMetrics: datasource?.metrics || [], + datasourceType: datasource?.type, + queryMode: getQueryMode(controls), + }; + }, + default: [], + validators: [], +}; const config: ControlPanelConfig = { controlPanelSections: [ { - label: t('GROUP BY'), - description: t('Use this section if you want a query that aggregates'), + label: t('Query'), expanded: true, controlSetRows: [ - ['groupby'], - ['metrics'], [ { - name: 'percent_metrics', - config: { - type: 'MetricsControl', - multi: true, - mapStateToProps: ({ datasource }) => { - return { - columns: datasource?.columns || [], - savedMetrics: datasource?.metrics || [], - datasourceType: datasource?.type, - }; - }, - default: [], - label: t('Percentage Metrics'), - validators: [], - description: t('Metrics for which percentage of total are to be displayed'), + name: 'query_mode', + config: queryMode, + }, + ], + [ + { + name: 'groupby', + override: { + visibility: isAggMode, }, }, ], - ['timeseries_limit_metric', 'row_limit'], + [ + { + name: 'metrics', + override: { + validators: [], + visibility: isAggMode, + }, + }, + { + name: 'all_columns', + config: all_columns, + }, + ], + [ + { + name: 'percent_metrics', + config: percent_metrics, + }, + ], + [ + { + name: 'timeseries_limit_metric', + override: { + visibility: isAggMode, + }, + }, + { + name: 'order_by_cols', + config: { + type: 'SelectControl', + label: t('Ordering'), + description: t('One or many metrics to display'), + multi: true, + default: [], + mapStateToProps: ({ datasource }) => ({ + choices: datasource?.order_by_choices || [], + }), + visibility: isRawMode, + }, + }, + ], + ['row_limit'], [ { name: 'include_time', @@ -67,6 +194,7 @@ const config: ControlPanelConfig = { 'Whether to include the time granularity as defined in the time section', ), default: false, + visibility: isAggMode, }, }, { @@ -76,60 +204,13 @@ const config: ControlPanelConfig = { label: t('Sort Descending'), default: true, description: t('Whether to sort descending or ascending'), + visibility: isAggMode, }, }, ], + ['adhoc_filters'], ], }, - { - label: t('NOT GROUPED BY'), - description: t('Use this section if you want to query atomic rows'), - expanded: true, - controlSetRows: [ - [ - { - name: 'all_columns', - config: { - type: 'SelectControl', - multi: true, - label: t('Columns'), - default: [], - description: t('Columns to display'), - optionRenderer: (c: never) => , - valueRenderer: (c: never) => , - valueKey: 'column_name', - allowAll: true, - mapStateToProps: ({ datasource }) => ({ - options: datasource?.columns || [], - }), - commaChoosesOption: false, - freeForm: true, - }, - }, - ], - [ - { - name: 'order_by_cols', - config: { - type: 'SelectControl', - multi: true, - label: t('Ordering'), - default: [], - description: t('One or many metrics to display'), - mapStateToProps: ({ datasource }) => ({ - choices: datasource?.order_by_choices || [], - }), - }, - }, - ], - ['row_limit', null], - ], - }, - { - label: t('Query'), - expanded: true, - controlSetRows: [['adhoc_filters']], - }, { label: t('Options'), expanded: true, @@ -141,7 +222,7 @@ const config: ControlPanelConfig = { type: 'SelectControl', freeForm: true, label: t('Table Timestamp Format'), - default: '%Y-%m-%d %H:%M:%S', + default: smartDateFormatter.id, renderTrigger: true, validators: [validateNonEmpty], clearable: false, @@ -158,8 +239,8 @@ const config: ControlPanelConfig = { freeForm: true, renderTrigger: true, label: t('Page Length'), - default: 0, - choices: formatSelectOptions([0, 10, 25, 40, 50, 75, 100, 150, 200]), + default: null, + choices: PAGE_SIZE_OPTIONS, description: t('Rows per page, 0 means no pagination'), }, }, @@ -225,11 +306,6 @@ const config: ControlPanelConfig = { ], }, ], - controlOverrides: { - metrics: { - validators: [], - }, - }, sectionOverrides: { druidTimeSeries: { controlSetRows: [['granularity', 'druid_time_origin'], ['time_range']], diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-table/src/controlPanel.tsx b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-table/src/controlPanel.tsx index 2eb6b6e629..a8296a4d3c 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-table/src/controlPanel.tsx +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/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 @@ -21,45 +22,168 @@ import { t } from '@superset-ui/translation'; import { formatSelectOptions, D3_TIME_FORMAT_OPTIONS, + ControlConfig, ColumnOption, + ControlStateMapping, + ControlPanelConfig, + ControlPanelsContainerProps, + sharedControls, } from '@superset-ui/chart-controls'; import { validateNonEmpty } from '@superset-ui/validator'; import { smartDateFormatter } from '@superset-ui/time-format'; export const PAGE_SIZE_OPTIONS = formatSelectOptions([[0, t('All')], 10, 20, 50, 100, 200]); -export default { +export enum QueryMode { + aggregate = 'aggregate', + raw = 'raw', +} + +const QueryModeLabel = { + [QueryMode.aggregate]: t('Aggregate'), + [QueryMode.raw]: t('Raw Records'), +}; + +function getQueryMode(controls: ControlStateMapping): QueryMode { + const mode = controls?.query_mode?.value; + if (mode === QueryMode.aggregate || mode === QueryMode.raw) { + return mode as QueryMode; + } + const groupby = controls?.groupby?.value; + const hasGroupBy = groupby && (groupby as string[])?.length > 0; + return hasGroupBy ? QueryMode.aggregate : QueryMode.raw; +} + +/** + * Visibility check + */ +function isQueryMode(mode: QueryMode) { + return ({ controls }: ControlPanelsContainerProps) => { + return getQueryMode(controls) === mode; + }; +} + +const isAggMode = isQueryMode(QueryMode.aggregate); +const isRawMode = isQueryMode(QueryMode.raw); + +const queryMode: ControlConfig<'RadioButtonControl'> = { + type: 'RadioButtonControl', + label: t('Query Mode'), + default: QueryMode.aggregate, + options: [ + { + label: QueryModeLabel[QueryMode.aggregate], + value: QueryMode.aggregate, + }, + { + label: QueryModeLabel[QueryMode.raw], + value: QueryMode.raw, + }, + ], + mapStateToProps: ({ controls }) => { + return { value: getQueryMode(controls) }; + }, +}; + +const all_columns: typeof sharedControls.groupby = { + type: 'SelectControl', + label: t('Columns'), + description: t('Columns to display'), + multi: true, + freeForm: true, + allowAll: true, + commaChoosesOption: false, + default: [], + optionRenderer: c => , + valueRenderer: c => , + valueKey: 'column_name', + mapStateToProps: ({ datasource, controls }) => ({ + options: datasource?.columns || [], + queryMode: getQueryMode(controls), + }), + visibility: isRawMode, +}; + +const percent_metrics: typeof sharedControls.metrics = { + type: 'MetricsControl', + label: t('Percentage Metrics'), + description: t('Metrics for which percentage of total are to be displayed'), + multi: true, + visibility: isAggMode, + mapStateToProps: ({ datasource, controls }) => { + return { + columns: datasource?.columns || [], + savedMetrics: datasource?.metrics || [], + datasourceType: datasource?.type, + queryMode: getQueryMode(controls), + }; + }, + default: [], + validators: [], +}; + +const config: ControlPanelConfig = { controlPanelSections: [ { - label: t('GROUP BY'), - description: t('Use this section if you want a query that aggregates'), + label: t('Query'), expanded: true, controlSetRows: [ - ['groupby'], - ['metrics'], [ { - name: 'percent_metrics', - config: { - type: 'MetricsControl', - multi: true, - mapStateToProps: (state: never) => { - const { datasource } = state; - const { columns, metrics, type } = datasource; - return { - columns: datasource ? columns : [], - savedMetrics: datasource ? metrics : [], - datasourceType: datasource && type, - }; - }, - default: [], - label: t('Percentage Metrics'), - validators: [], - description: t('Metrics for which percentage of total are to be displayed'), + name: 'query_mode', + config: queryMode, + }, + ], + [ + { + name: 'groupby', + override: { + visibility: isAggMode, }, }, ], - ['timeseries_limit_metric', 'row_limit'], + [ + { + name: 'metrics', + override: { + validators: [], + visibility: isAggMode, + }, + }, + { + name: 'all_columns', + config: all_columns, + }, + ], + [ + { + name: 'percent_metrics', + config: percent_metrics, + }, + ], + [ + { + name: 'timeseries_limit_metric', + override: { + visibility: isAggMode, + }, + }, + { + name: 'order_by_cols', + config: { + type: 'SelectControl', + label: t('Ordering'), + description: t('One or many metrics to display'), + multi: true, + default: [], + mapStateToProps: ({ datasource }) => ({ + choices: datasource?.order_by_choices || [], + }), + visibility: isRawMode, + }, + }, + ], + ['row_limit'], [ { name: 'include_time', @@ -70,6 +194,7 @@ export default { 'Whether to include the time granularity as defined in the time section', ), default: false, + visibility: isAggMode, }, }, { @@ -79,61 +204,13 @@ export default { label: t('Sort Descending'), default: true, description: t('Whether to sort descending or ascending'), + visibility: isAggMode, }, }, ], + ['adhoc_filters'], ], }, - { - label: t('NOT GROUPED BY'), - description: t('Use this section if you want to query atomic rows'), - expanded: true, - controlSetRows: [ - [ - { - name: 'all_columns', - config: { - type: 'SelectControl', - multi: true, - label: t('Columns'), - default: [], - description: t('Columns to display'), - optionRenderer: (c: never) => , - valueRenderer: (c: never) => , - valueKey: 'column_name', - allowAll: true, - mapStateToProps: (state: { datasource: { columns: unknown } }) => ({ - options: state.datasource ? state.datasource.columns : [], - }), - commaChoosesOption: false, - freeForm: true, - }, - }, - ], - [ - { - name: 'order_by_cols', - config: { - type: 'SelectControl', - multi: true, - 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 : [], - }), - }, - }, - ], - ['row_limit', null], - ], - }, - { - label: t('Query'), - expanded: true, - controlSetRows: [['adhoc_filters']], - }, { label: t('Options'), expanded: true, @@ -164,9 +241,7 @@ export default { label: t('Page Length'), default: null, choices: PAGE_SIZE_OPTIONS, - description: t( - 'Rows per page, 0 means no pagination. Leave empty to automatically add pagination for large tables.', - ), + description: t('Rows per page, 0 means no pagination'), }, }, null, @@ -231,11 +306,6 @@ export default { ], }, ], - controlOverrides: { - metrics: { - validators: [], - }, - }, sectionOverrides: { druidTimeSeries: { controlSetRows: [['granularity', 'druid_time_origin'], ['time_range']], @@ -245,3 +315,5 @@ export default { }, }, }; + +export default config; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-table/src/transformProps.ts b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-table/src/transformProps.ts index 7e23fc55ae..13f2392545 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-table/src/transformProps.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-table/src/transformProps.ts @@ -144,7 +144,10 @@ const processColumns = memoizeOne(function processColumns(props: TableChartProps ] as [typeof metrics, typeof percentMetrics, typeof columns]; }, isEqualColumns); -const getDefaultPageSize = ( +/** + * Automatically set page size based on number of cells. + */ +const getPageSize = ( pageSize: number | string | null | undefined, numRecords: number, numColumns: number, @@ -194,7 +197,7 @@ export default function transformProps(chartProps: TableChartProps): TableChartT showCellBars, sortDesc, includeSearch, - pageSize: getDefaultPageSize(pageSize, data.length, columns.length), + pageSize: getPageSize(pageSize, data.length, columns.length), filters, emitFilter: tableFilter === true, onChangeFilter,