feat(legacy-table-chart): add query mode switch (#609)

This commit is contained in:
Jesse Yang 2020-06-23 15:11:12 -07:00 committed by Yongjie Zhao
parent 92ac471216
commit aade5ef42c
13 changed files with 647 additions and 210 deletions

View File

@ -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"

View File

@ -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<HTMLButtonElement>) => {
onChange(e.currentTarget.value);
},
[onChange],
);
return (
<Styles>
{controlLabel && (
<div className="control-label">
{controlLabel}{' '}
{hovered && description && (
<InfoTooltipWithTrigger tooltip={description} placement="top" />
)}
</div>
)}
<div className="btn-group btn-group-sm">
{options.map(({ label, value }, i) => (
<button
key={value}
type="button"
className={`btn btn-default ${options[i].value === currentValue ? 'active' : ''}`}
value={value}
onClick={onClick}
>
{label}
</button>
))}
</div>
</Styles>
);
}

View File

@ -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';

View File

@ -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,
};

View File

@ -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();

View File

@ -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<string, unknown>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AnyDict = Record<string, any>;
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<T, O> & ExtraControlProps;
export type ControlState<T = ControlType, O extends SelectOption = SelectOption> = 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<T = unknown, O extends SelectOption = SelectOption> {
(value: T, state: ControlState<O>): boolean | string;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type ControlType = InternalControlType | React.ComponentType<any>;
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<T = unknown> {
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<T, O, V>[];
warning?: ReactNode;
error?: ReactNode;
// override control panel state props
@ -177,7 +188,14 @@ export interface BaseControlConfig<T = unknown> {
) => 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<T, O>): boolean | string;
}
/** --------------------------------------------
@ -207,7 +225,7 @@ interface FilterOption<T extends SelectOption> {
export interface SelectControlConfig<
O extends SelectOption = SelectOption,
T extends SelectControlType = SelectControlType
> extends BaseControlConfig<T> {
> extends BaseControlConfig<T, O> {
clearable?: boolean;
freeForm?: boolean;
multi?: boolean;
@ -227,24 +245,26 @@ export type SharedControlConfig<
/** --------------------------------------------
* 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
export type CustomControlConfig<P = {}> = BaseControlConfig<React.ComponentType<P>> &
// two run-time properties from superset-frontend/src/explore/components/Control.jsx
Omit<P, 'onChange' | 'hovered'>;
// 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<T, O> : CustomComponentControlConfig<T>;
> = T extends InternalControlType
? SharedControlConfig<T, O>
: T extends object
? CustomControlConfig<T> // eslint-disable-next-line @typescript-eslint/no-explicit-any
: CustomControlConfig<any>;
/** --------------------------------------------
/** ===========================================================
* 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<SharedControlConfig>;
export interface OverrideSharedControlItem<A extends SharedControlAlias = SharedControlAlias> {
name: A;
override: Partial<SharedControls[A]>;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export interface CustomControlItem<P = any> {
export type CustomControlItem = {
name: string;
config: CustomComponentControlConfig<P>;
}
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<any, any, any>;
};
// 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<ControlConfig>;
[P in SharedControlAlias]?: Partial<SharedControls[P]>;
};
export type SectionOverrides = {

View File

@ -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;
}

View File

@ -31,9 +31,9 @@ export function formatSelectOptions<T extends Formattable>(
}
/**
* 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[] = [];

View File

@ -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 = <h1>Test</h1>;
expect(expandControlConfig(input)).toBe(input);
});
it('leave unknown text untouched', () => {
const input = 'superset-ui';
expect(expandControlConfig(input as never)).toBe(input);
});
});

View File

@ -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",

View File

@ -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<number>([[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 => <ColumnOption showType column={c} />,
valueRenderer: c => <ColumnOption column={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) => <ColumnOption showType column={c} />,
valueRenderer: (c: never) => <ColumnOption column={c} />,
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']],

View File

@ -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<number>([[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 => <ColumnOption showType column={c} />,
valueRenderer: c => <ColumnOption column={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) => <ColumnOption showType column={c} />,
valueRenderer: (c: never) => <ColumnOption column={c} />,
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;

View File

@ -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,