mirror of
https://github.com/apache/superset.git
synced 2024-09-19 12:09:42 -04:00
feat(legacy-table-chart): add query mode switch (#609)
This commit is contained in:
parent
92ac471216
commit
aade5ef42c
@ -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"
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
@ -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';
|
||||
|
@ -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,
|
||||
};
|
@ -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();
|
@ -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 = {
|
||||
|
@ -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;
|
||||
}
|
@ -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[] = [];
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
@ -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",
|
||||
|
@ -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']],
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user