mirror of https://github.com/apache/superset.git
feat(native-filters): add timegrain and column filter (#13484)
* feat(native-filters): add timegrain and column filter * add fetch values predicate * bump deps * lint * fix test * add python test for legacy merge * fix default value and isInitialized to not check strict equality * Address comments * add FilterValue type * address review comments
This commit is contained in:
parent
c91c45574b
commit
375797f649
File diff suppressed because it is too large
Load Diff
|
@ -65,34 +65,34 @@
|
|||
"@babel/runtime-corejs3": "^7.12.5",
|
||||
"@data-ui/sparkline": "^0.0.84",
|
||||
"@emotion/core": "^10.0.35",
|
||||
"@superset-ui/chart-controls": "^0.17.14",
|
||||
"@superset-ui/core": "^0.17.14",
|
||||
"@superset-ui/legacy-plugin-chart-calendar": "^0.17.14",
|
||||
"@superset-ui/legacy-plugin-chart-chord": "^0.17.14",
|
||||
"@superset-ui/legacy-plugin-chart-country-map": "^0.17.14",
|
||||
"@superset-ui/legacy-plugin-chart-event-flow": "^0.17.14",
|
||||
"@superset-ui/legacy-plugin-chart-force-directed": "^0.17.14",
|
||||
"@superset-ui/legacy-plugin-chart-heatmap": "^0.17.14",
|
||||
"@superset-ui/legacy-plugin-chart-histogram": "^0.17.14",
|
||||
"@superset-ui/legacy-plugin-chart-horizon": "^0.17.14",
|
||||
"@superset-ui/legacy-plugin-chart-map-box": "^0.17.14",
|
||||
"@superset-ui/legacy-plugin-chart-paired-t-test": "^0.17.14",
|
||||
"@superset-ui/legacy-plugin-chart-parallel-coordinates": "^0.17.14",
|
||||
"@superset-ui/legacy-plugin-chart-partition": "^0.17.14",
|
||||
"@superset-ui/legacy-plugin-chart-pivot-table": "^0.17.14",
|
||||
"@superset-ui/legacy-plugin-chart-rose": "^0.17.14",
|
||||
"@superset-ui/legacy-plugin-chart-sankey": "^0.17.14",
|
||||
"@superset-ui/legacy-plugin-chart-sankey-loop": "^0.17.14",
|
||||
"@superset-ui/legacy-plugin-chart-sunburst": "^0.17.14",
|
||||
"@superset-ui/legacy-plugin-chart-treemap": "^0.17.14",
|
||||
"@superset-ui/legacy-plugin-chart-world-map": "^0.17.14",
|
||||
"@superset-ui/legacy-preset-chart-big-number": "^0.17.14",
|
||||
"@superset-ui/chart-controls": "^0.17.15",
|
||||
"@superset-ui/core": "^0.17.15",
|
||||
"@superset-ui/legacy-plugin-chart-calendar": "^0.17.15",
|
||||
"@superset-ui/legacy-plugin-chart-chord": "^0.17.15",
|
||||
"@superset-ui/legacy-plugin-chart-country-map": "^0.17.15",
|
||||
"@superset-ui/legacy-plugin-chart-event-flow": "^0.17.15",
|
||||
"@superset-ui/legacy-plugin-chart-force-directed": "^0.17.15",
|
||||
"@superset-ui/legacy-plugin-chart-heatmap": "^0.17.15",
|
||||
"@superset-ui/legacy-plugin-chart-histogram": "^0.17.15",
|
||||
"@superset-ui/legacy-plugin-chart-horizon": "^0.17.15",
|
||||
"@superset-ui/legacy-plugin-chart-map-box": "^0.17.15",
|
||||
"@superset-ui/legacy-plugin-chart-paired-t-test": "^0.17.15",
|
||||
"@superset-ui/legacy-plugin-chart-parallel-coordinates": "^0.17.15",
|
||||
"@superset-ui/legacy-plugin-chart-partition": "^0.17.15",
|
||||
"@superset-ui/legacy-plugin-chart-pivot-table": "^0.17.15",
|
||||
"@superset-ui/legacy-plugin-chart-rose": "^0.17.15",
|
||||
"@superset-ui/legacy-plugin-chart-sankey": "^0.17.15",
|
||||
"@superset-ui/legacy-plugin-chart-sankey-loop": "^0.17.15",
|
||||
"@superset-ui/legacy-plugin-chart-sunburst": "^0.17.15",
|
||||
"@superset-ui/legacy-plugin-chart-treemap": "^0.17.15",
|
||||
"@superset-ui/legacy-plugin-chart-world-map": "^0.17.15",
|
||||
"@superset-ui/legacy-preset-chart-big-number": "^0.17.15",
|
||||
"@superset-ui/legacy-preset-chart-deckgl": "^0.4.6",
|
||||
"@superset-ui/legacy-preset-chart-nvd3": "^0.17.14",
|
||||
"@superset-ui/plugin-chart-echarts": "^0.17.14",
|
||||
"@superset-ui/plugin-chart-table": "^0.17.14",
|
||||
"@superset-ui/plugin-chart-word-cloud": "^0.17.14",
|
||||
"@superset-ui/preset-chart-xy": "^0.17.14",
|
||||
"@superset-ui/legacy-preset-chart-nvd3": "^0.17.15",
|
||||
"@superset-ui/plugin-chart-echarts": "^0.17.15",
|
||||
"@superset-ui/plugin-chart-table": "^0.17.15",
|
||||
"@superset-ui/plugin-chart-word-cloud": "^0.17.15",
|
||||
"@superset-ui/preset-chart-xy": "^0.17.15",
|
||||
"@vx/responsive": "^0.0.195",
|
||||
"abortcontroller-polyfill": "^1.1.9",
|
||||
"antd": "^4.9.4",
|
||||
|
|
|
@ -107,9 +107,13 @@ describe('Filter utils', () => {
|
|||
expect(getSelectExtraFormData('testCol', ['value'], true, false)).toEqual(
|
||||
{
|
||||
append_form_data: {
|
||||
extras: {
|
||||
where: '1 = 0',
|
||||
},
|
||||
adhoc_filters: [
|
||||
{
|
||||
clause: 'WHERE',
|
||||
expressionType: 'SQL',
|
||||
sqlExpression: '1 = 0',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
|
|
@ -22,6 +22,7 @@ import { NativeFiltersState } from 'src/dashboard/reducers/types';
|
|||
import { DataMaskStateWithId } from 'src/dataMask/types';
|
||||
import { Layout } from '../../types';
|
||||
import { getTreeCheckedItems } from '../nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/utils';
|
||||
import { FilterValue } from '../nativeFilters/types';
|
||||
|
||||
export enum IndicatorStatus {
|
||||
Unset = 'UNSET',
|
||||
|
@ -52,7 +53,7 @@ const selectIndicatorValue = (
|
|||
columnKey: string,
|
||||
filter: Filter,
|
||||
datasource: Datasource,
|
||||
): string[] => {
|
||||
): FilterValue => {
|
||||
const values = filter.columns[columnKey];
|
||||
const arrValues = Array.isArray(values) ? values : [values];
|
||||
|
||||
|
@ -132,7 +133,7 @@ const getRejectedColumns = (chart: any): Set<string> =>
|
|||
export type Indicator = {
|
||||
column?: string;
|
||||
name: string;
|
||||
value: string[];
|
||||
value: FilterValue;
|
||||
status: IndicatorStatus;
|
||||
path: string[];
|
||||
};
|
||||
|
@ -185,20 +186,22 @@ export const selectNativeIndicatorsForChart = (
|
|||
const rejectedColumns = getRejectedColumns(chart);
|
||||
|
||||
const getStatus = (
|
||||
value: string[],
|
||||
value: FilterValue,
|
||||
isAffectedByScope: boolean,
|
||||
column?: string,
|
||||
): IndicatorStatus => {
|
||||
// a filter is only considered unset if it's value is null
|
||||
const hasValue = value !== null;
|
||||
if (!isAffectedByScope) {
|
||||
return IndicatorStatus.Unset;
|
||||
}
|
||||
if (!column) {
|
||||
if (!column && hasValue) {
|
||||
// Filter without datasource
|
||||
return IndicatorStatus.Applied;
|
||||
}
|
||||
if (column && rejectedColumns.has(column))
|
||||
return IndicatorStatus.Incompatible;
|
||||
if (column && appliedColumns.has(column) && value.length > 0) {
|
||||
if (column && appliedColumns.has(column) && hasValue) {
|
||||
return IndicatorStatus.Applied;
|
||||
}
|
||||
return IndicatorStatus.Unset;
|
||||
|
|
|
@ -60,7 +60,7 @@ const FilterValue: React.FC<FilterProps> = ({
|
|||
column = {},
|
||||
}: Partial<{ datasetId: number; column: { name?: string } }> = target;
|
||||
const { name: groupby } = column;
|
||||
const hasDataSource = !!(datasetId && groupby);
|
||||
const hasDataSource = !!datasetId;
|
||||
const [loading, setLoading] = useState<boolean>(hasDataSource);
|
||||
useEffect(() => {
|
||||
const newFormData = getFormData({
|
||||
|
@ -137,7 +137,7 @@ const FilterValue: React.FC<FilterProps> = ({
|
|||
width={220}
|
||||
formData={formData}
|
||||
// For charts that don't have datasource we need workaround for empty placeholder
|
||||
queriesData={hasDataSource ? state : [{ data: [null] }]}
|
||||
queriesData={hasDataSource ? state : [{ data: [{}] }]}
|
||||
chartType={filterType}
|
||||
behaviors={[Behavior.NATIVE_FILTER]}
|
||||
hooks={{ setDataMask }}
|
||||
|
|
|
@ -48,7 +48,6 @@ const ControlItems: FC<ControlItemsProps> = ({
|
|||
const controlPanelRegistry = getChartControlPanelRegistry();
|
||||
const controlItems =
|
||||
getControlItems(controlPanelRegistry.get(filterType)) ?? [];
|
||||
|
||||
return (
|
||||
<>
|
||||
{controlItems
|
||||
|
|
|
@ -78,6 +78,8 @@ export interface FiltersConfigFormProps {
|
|||
parentFilters: { id: string; title: string }[];
|
||||
}
|
||||
|
||||
const FILTERS_WITH_ONLY_DATASOURCE = ['filter_timegrain', 'filter_timecolumn'];
|
||||
|
||||
/**
|
||||
* The configuration form for a specific filter.
|
||||
* Assigns field values to `filters[filterId]` in the form.
|
||||
|
@ -104,17 +106,21 @@ export const FiltersConfigForm: React.FC<FiltersConfigFormProps> = ({
|
|||
// @ts-ignore
|
||||
const hasDatasource = !!nativeFilterItems[formFilter?.filterType]?.value
|
||||
?.datasourceCount;
|
||||
const hasColumn =
|
||||
hasDatasource &&
|
||||
!FILTERS_WITH_ONLY_DATASOURCE.includes(formFilter?.filterType);
|
||||
|
||||
const hasFilledDatasource =
|
||||
(formFilter?.dataset && formFilter?.column) || !hasDatasource;
|
||||
!hasDatasource ||
|
||||
(formFilter?.dataset?.value && (formFilter?.column || !hasColumn));
|
||||
|
||||
useBackendFormUpdate(form, filterId, filterToEdit, hasDatasource);
|
||||
useBackendFormUpdate(form, filterId, filterToEdit, hasDatasource, hasColumn);
|
||||
|
||||
const initDatasetId = filterToEdit?.targets[0]?.datasetId;
|
||||
const initColumn = filterToEdit?.targets[0]?.column?.name;
|
||||
const newFormData = getFormData({
|
||||
datasetId: formFilter?.dataset?.value,
|
||||
groupby: formFilter?.column,
|
||||
groupby: hasColumn ? formFilter?.column : undefined,
|
||||
defaultValue: formFilter?.defaultValue,
|
||||
...formFilter,
|
||||
});
|
||||
|
@ -204,22 +210,24 @@ export const FiltersConfigForm: React.FC<FiltersConfigFormProps> = ({
|
|||
}}
|
||||
/>
|
||||
</StyledFormItem>
|
||||
<StyledFormItem
|
||||
// don't show the column select unless we have a dataset
|
||||
// style={{ display: datasetId == null ? undefined : 'none' }}
|
||||
name={['filters', filterId, 'column']}
|
||||
initialValue={initColumn}
|
||||
label={<StyledLabel>{t('Column')}</StyledLabel>}
|
||||
rules={[{ required: !removed, message: t('Field is required') }]}
|
||||
data-test="field-input"
|
||||
>
|
||||
<ColumnSelect
|
||||
form={form}
|
||||
filterId={filterId}
|
||||
datasetId={formFilter?.dataset?.value}
|
||||
onChange={forceUpdate}
|
||||
/>
|
||||
</StyledFormItem>
|
||||
{hasColumn && (
|
||||
<StyledFormItem
|
||||
// don't show the column select unless we have a dataset
|
||||
// style={{ display: datasetId == null ? undefined : 'none' }}
|
||||
name={['filters', filterId, 'column']}
|
||||
initialValue={initColumn}
|
||||
label={<StyledLabel>{t('Column')}</StyledLabel>}
|
||||
rules={[{ required: !removed, message: t('Field is required') }]}
|
||||
data-test="field-input"
|
||||
>
|
||||
<ColumnSelect
|
||||
form={form}
|
||||
filterId={filterId}
|
||||
datasetId={formFilter?.dataset?.value}
|
||||
onChange={forceUpdate}
|
||||
/>
|
||||
</StyledFormItem>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{hasFilledDatasource && (
|
||||
|
|
|
@ -31,6 +31,7 @@ export const useBackendFormUpdate = (
|
|||
filterId: string,
|
||||
filterToEdit?: Filter,
|
||||
hasDatasource?: boolean,
|
||||
hasColumn?: boolean,
|
||||
) => {
|
||||
const forceUpdate = useForceUpdate();
|
||||
const formFilter = (form.getFieldValue('filters') || {})[filterId];
|
||||
|
@ -42,13 +43,17 @@ export const useBackendFormUpdate = (
|
|||
}
|
||||
// No need to check data set change because it cascading update column
|
||||
// So check that column exists is enough
|
||||
if (!formFilter?.column) {
|
||||
if (hasColumn && !formFilter?.column) {
|
||||
setFilterFieldValues(form, filterId, {
|
||||
defaultValueQueriesData: [],
|
||||
defaultValue: resolvedDefaultValue,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!formFilter?.dataset?.value) {
|
||||
// no need to make chart data request if no dataset is defined
|
||||
return;
|
||||
}
|
||||
const formData = getFormData({
|
||||
datasetId: formFilter?.dataset?.value,
|
||||
groupby: formFilter?.column,
|
||||
|
@ -63,7 +68,8 @@ export const useBackendFormUpdate = (
|
|||
if (
|
||||
filterToEdit?.filterType === formFilter?.filterType &&
|
||||
filterToEdit?.targets[0].datasetId === formFilter?.dataset?.value &&
|
||||
formFilter?.column === filterToEdit?.targets[0].column?.name
|
||||
(!hasColumn ||
|
||||
formFilter?.column === filterToEdit?.targets[0].column?.name)
|
||||
) {
|
||||
resolvedDefaultValue = filterToEdit?.defaultValue;
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
import { FormInstance } from 'antd/lib/form';
|
||||
import shortid from 'shortid';
|
||||
import { FilterRemoval, NativeFiltersForm } from './types';
|
||||
import { Filter, FilterConfiguration } from '../types';
|
||||
import { Filter, FilterConfiguration, Target } from '../types';
|
||||
|
||||
export const REMOVAL_DELAY_SECS = 5;
|
||||
|
||||
|
@ -132,14 +132,12 @@ export const createHandleSave = (
|
|||
const formInputs = values.filters[id];
|
||||
// if user didn't open a filter, return the original config
|
||||
if (!formInputs) return filterConfigMap[id];
|
||||
let target = {};
|
||||
const target: Partial<Target> = {};
|
||||
if (formInputs.dataset) {
|
||||
target.datasetId = formInputs.dataset.value;
|
||||
}
|
||||
if (formInputs.dataset && formInputs.column) {
|
||||
target = {
|
||||
datasetId: formInputs.dataset.value,
|
||||
column: {
|
||||
name: formInputs.column,
|
||||
},
|
||||
};
|
||||
target.column = { name: formInputs.column };
|
||||
}
|
||||
return {
|
||||
id,
|
||||
|
|
|
@ -37,10 +37,12 @@ export interface Target {
|
|||
// clarityColumns?: Column[];
|
||||
}
|
||||
|
||||
export type FilterValue = string | number | (string | number)[] | null;
|
||||
|
||||
export interface Filter {
|
||||
cascadeParentIds: string[];
|
||||
defaultValue: any;
|
||||
currentValue?: any;
|
||||
defaultValue: FilterValue;
|
||||
currentValue?: FilterValue;
|
||||
isInstant: boolean;
|
||||
id: string; // randomly generated at filter creation
|
||||
name: string;
|
||||
|
|
|
@ -44,12 +44,12 @@ export const getFormData = ({
|
|||
cascadingFilters?: object;
|
||||
groupby?: string;
|
||||
}): Partial<QueryFormData> => {
|
||||
let otherProps: { datasource?: string; groupby?: string[] } = {};
|
||||
if (datasetId && groupby) {
|
||||
otherProps = {
|
||||
datasource: `${datasetId}__table`,
|
||||
groupby: [groupby],
|
||||
};
|
||||
const otherProps: { datasource?: string; groupby?: string[] } = {};
|
||||
if (datasetId) {
|
||||
otherProps.datasource = `${datasetId}__table`;
|
||||
}
|
||||
if (groupby) {
|
||||
otherProps.groupby = [groupby];
|
||||
}
|
||||
return {
|
||||
...controlValues,
|
||||
|
|
|
@ -16,18 +16,13 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { styled, t, DataMask, Behavior } from '@superset-ui/core';
|
||||
import { t, DataMask, Behavior } from '@superset-ui/core';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Slider } from 'src/common/components';
|
||||
import { PluginFilterRangeProps } from './types';
|
||||
import { PluginFilterStylesProps } from '../types';
|
||||
import { Styles } from '../common';
|
||||
import { getRangeExtraFormData } from '../../utils';
|
||||
|
||||
const Styles = styled.div<PluginFilterStylesProps>`
|
||||
height: ${({ height }) => height};
|
||||
width: ${({ width }) => width};
|
||||
`;
|
||||
|
||||
export default function RangeFilterPlugin(props: PluginFilterRangeProps) {
|
||||
const {
|
||||
data,
|
||||
|
|
|
@ -43,6 +43,7 @@ export default function buildQuery(formData: QueryFormData) {
|
|||
return buildQueryContext(formData, baseQueryObject => [
|
||||
{
|
||||
...baseQueryObject,
|
||||
apply_fetch_values_predicate: true,
|
||||
columns: [],
|
||||
groupby: [],
|
||||
metrics: [
|
||||
|
|
|
@ -26,7 +26,7 @@ export default class RangeFilterPlugin extends ChartPlugin {
|
|||
constructor() {
|
||||
const metadata = new ChartMetadata({
|
||||
name: t('Range filter'),
|
||||
description: 'Range filter plugin using AntD',
|
||||
description: t('Range filter plugin using AntD'),
|
||||
behaviors: [Behavior.CROSS_FILTER, Behavior.NATIVE_FILTER],
|
||||
thumbnail,
|
||||
});
|
||||
|
|
|
@ -16,18 +16,13 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { styled, Behavior, DataMask, t } from '@superset-ui/core';
|
||||
import { Behavior, DataMask, t, tn, ensureIsArray } from '@superset-ui/core';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Select } from 'src/common/components';
|
||||
import { PluginFilterSelectProps } from './types';
|
||||
import { PluginFilterStylesProps } from '../types';
|
||||
import { Styles, StyledSelect } from '../common';
|
||||
import { getSelectExtraFormData } from '../../utils';
|
||||
|
||||
const Styles = styled.div<PluginFilterStylesProps>`
|
||||
height: ${({ height }) => height};
|
||||
width: ${({ width }) => width};
|
||||
`;
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
export default function PluginFilterSelect(props: PluginFilterSelectProps) {
|
||||
|
@ -50,13 +45,9 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
|
|||
const handleChange = (
|
||||
value?: (number | string)[] | number | string | null,
|
||||
) => {
|
||||
let resultValue: (number | string)[];
|
||||
// Works only with arrays even for single select
|
||||
if (!Array.isArray(value)) {
|
||||
resultValue = value ? [value] : [];
|
||||
} else {
|
||||
resultValue = value;
|
||||
}
|
||||
const resultValue: (number | string)[] = ensureIsArray<number | string>(
|
||||
value,
|
||||
);
|
||||
setValues(resultValue);
|
||||
|
||||
const [col] = groupby;
|
||||
|
@ -89,25 +80,32 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
|
|||
|
||||
useEffect(() => {
|
||||
handleChange(currentValue ?? []);
|
||||
}, [JSON.stringify(currentValue)]);
|
||||
}, [
|
||||
JSON.stringify(currentValue),
|
||||
multiSelect,
|
||||
enableEmptyFilter,
|
||||
inverseSelection,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
handleChange(defaultValue ?? []);
|
||||
// I think after Config Modal update some filter it re-creates default value for all other filters
|
||||
// so we can process it like this `JSON.stringify` or start to use `Immer`
|
||||
}, [JSON.stringify(defaultValue)]);
|
||||
}, [
|
||||
JSON.stringify(defaultValue),
|
||||
multiSelect,
|
||||
enableEmptyFilter,
|
||||
inverseSelection,
|
||||
]);
|
||||
|
||||
const placeholderText =
|
||||
(data || []).length === 0
|
||||
? t('No data')
|
||||
: t(`%d option%s`, data.length, data.length === 1 ? '' : 's');
|
||||
: tn('%s option', '%s options', data.length, data.length);
|
||||
return (
|
||||
<Styles height={height} width={width}>
|
||||
<Select
|
||||
<StyledSelect
|
||||
allowClear
|
||||
value={values}
|
||||
showSearch={showSearch}
|
||||
style={{ width: '100%' }}
|
||||
mode={multiSelect ? 'multiple' : undefined}
|
||||
placeholder={placeholderText}
|
||||
// @ts-ignore
|
||||
|
@ -122,7 +120,7 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
|
|||
</Option>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</StyledSelect>
|
||||
</Styles>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -36,6 +36,7 @@ export default function buildQuery(formData: QueryFormData) {
|
|||
return buildQueryContext(formData, baseQueryObject => [
|
||||
{
|
||||
...baseQueryObject,
|
||||
apply_fetch_values_predicate: true,
|
||||
groupby: baseQueryObject.columns,
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -26,7 +26,7 @@ export default class FilterSelectPlugin extends ChartPlugin {
|
|||
constructor() {
|
||||
const metadata = new ChartMetadata({
|
||||
name: t('Select filter'),
|
||||
description: 'Select filter plugin using AntD',
|
||||
description: t('Select filter plugin using AntD'),
|
||||
behaviors: [Behavior.CROSS_FILTER, Behavior.NATIVE_FILTER],
|
||||
thumbnail,
|
||||
});
|
||||
|
|
|
@ -19,14 +19,12 @@
|
|||
import { styled, DataMask, Behavior } from '@superset-ui/core';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import DateFilterControl from 'src/explore/components/controls/DateFilterControl';
|
||||
import { PluginFilterStylesProps } from '../types';
|
||||
import { PluginFilterTimeProps } from './types';
|
||||
import { Styles } from '../common';
|
||||
|
||||
const DEFAULT_VALUE = 'Last week';
|
||||
|
||||
const Styles = styled.div<PluginFilterStylesProps>`
|
||||
height: ${({ height }) => height}px;
|
||||
width: ${({ width }) => width}px;
|
||||
const TimeFilterStyles = styled(Styles)`
|
||||
overflow-x: scroll;
|
||||
`;
|
||||
|
||||
|
@ -69,12 +67,12 @@ export default function TimeFilterPlugin(props: PluginFilterTimeProps) {
|
|||
|
||||
return (
|
||||
// @ts-ignore
|
||||
<Styles width={width}>
|
||||
<TimeFilterStyles width={width}>
|
||||
<DateFilterControl
|
||||
value={value}
|
||||
name="time_range"
|
||||
onChange={handleTimeRangeChange}
|
||||
/>
|
||||
</Styles>
|
||||
</TimeFilterStyles>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ export default class TimeFilterPlugin extends ChartPlugin {
|
|||
constructor() {
|
||||
const metadata = new ChartMetadata({
|
||||
name: t('Time filter'),
|
||||
description: 'Custom time filter plugin',
|
||||
description: t('Custom time filter plugin'),
|
||||
behaviors: [Behavior.CROSS_FILTER, Behavior.NATIVE_FILTER],
|
||||
thumbnail,
|
||||
datasourceCount: 0,
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
/**
|
||||
* 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 {
|
||||
Behavior,
|
||||
DataMask,
|
||||
ensureIsArray,
|
||||
GenericDataType,
|
||||
t,
|
||||
tn,
|
||||
} from '@superset-ui/core';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Select } from 'src/common/components';
|
||||
import { Styles, StyledSelect } from '../common';
|
||||
import { PluginFilterTimeColumnProps } from './types';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
export default function PluginFilterTimeColumn(
|
||||
props: PluginFilterTimeColumnProps,
|
||||
) {
|
||||
const { behaviors, data, formData, height, width, setDataMask } = props;
|
||||
const { defaultValue, currentValue, inputRef } = formData;
|
||||
|
||||
const [value, setValue] = useState<string[]>(defaultValue ?? []);
|
||||
|
||||
const handleChange = (value?: string[] | string | null) => {
|
||||
const resultValue: string[] = ensureIsArray<string>(value);
|
||||
setValue(resultValue);
|
||||
|
||||
const dataMask = {
|
||||
extraFormData: {
|
||||
override_form_data: {
|
||||
granularity_sqla: resultValue.length ? resultValue[0] : null,
|
||||
},
|
||||
},
|
||||
currentState: {
|
||||
value: resultValue.length ? resultValue : null,
|
||||
},
|
||||
};
|
||||
|
||||
const dataMaskObject: DataMask = {};
|
||||
if (behaviors.includes(Behavior.NATIVE_FILTER)) {
|
||||
dataMaskObject.nativeFilters = dataMask;
|
||||
}
|
||||
|
||||
if (behaviors.includes(Behavior.CROSS_FILTER)) {
|
||||
dataMaskObject.crossFilters = dataMask;
|
||||
}
|
||||
|
||||
setDataMask(dataMaskObject);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
handleChange(currentValue ?? null);
|
||||
}, [JSON.stringify(currentValue)]);
|
||||
|
||||
useEffect(() => {
|
||||
handleChange(defaultValue ?? null);
|
||||
// I think after Config Modal update some filter it re-creates default value for all other filters
|
||||
// so we can process it like this `JSON.stringify` or start to use `Immer`
|
||||
}, [JSON.stringify(defaultValue)]);
|
||||
|
||||
const timeColumns = (data || []).filter(
|
||||
row => row.dtype === GenericDataType.TEMPORAL,
|
||||
);
|
||||
|
||||
const placeholderText =
|
||||
timeColumns.length === 0
|
||||
? t('No time columns')
|
||||
: tn('%s option', '%s options', timeColumns.length, timeColumns.length);
|
||||
return (
|
||||
<Styles height={height} width={width}>
|
||||
<StyledSelect
|
||||
allowClear
|
||||
value={value}
|
||||
placeholder={placeholderText}
|
||||
// @ts-ignore
|
||||
onChange={handleChange}
|
||||
ref={inputRef}
|
||||
>
|
||||
{timeColumns.map(
|
||||
(row: { column_name: string; verbose_name: string | null }) => {
|
||||
const { column_name: columnName, verbose_name: verboseName } = row;
|
||||
return (
|
||||
<Option key={columnName} value={columnName}>
|
||||
{verboseName ?? columnName}
|
||||
</Option>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</StyledSelect>
|
||||
</Styles>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { buildQueryContext, QueryFormData } from '@superset-ui/core';
|
||||
|
||||
/**
|
||||
* The buildQuery function is used to create an instance of QueryContext that's
|
||||
* sent to the chart data endpoint. In addition to containing information of which
|
||||
* datasource to use, it specifies the type (e.g. full payload, samples, query) and
|
||||
* format (e.g. CSV or JSON) of the result and whether or not to force refresh the data from
|
||||
* the datasource as opposed to using a cached copy of the data, if available.
|
||||
*
|
||||
* More importantly though, QueryContext contains a property `queries`, which is an array of
|
||||
* QueryObjects specifying individual data requests to be made. A QueryObject specifies which
|
||||
* columns, metrics and filters, among others, to use during the query. Usually it will be enough
|
||||
* to specify just one query based on the baseQueryObject, but for some more advanced use cases
|
||||
* it is possible to define post processing operations in the QueryObject, or multiple queries
|
||||
* if a viz needs multiple different result sets.
|
||||
*/
|
||||
export default function buildQuery(formData: QueryFormData) {
|
||||
return buildQueryContext(formData, baseQueryObject => [
|
||||
{
|
||||
result_type: 'columns',
|
||||
columns: [],
|
||||
metrics: [],
|
||||
orderby: [],
|
||||
},
|
||||
]);
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/**
|
||||
* 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 { ControlPanelConfig } from '@superset-ui/chart-controls';
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
controlPanelSections: [],
|
||||
};
|
||||
|
||||
export default config;
|
Binary file not shown.
After Width: | Height: | Size: 5.5 KiB |
|
@ -0,0 +1,42 @@
|
|||
/**
|
||||
* 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 { Behavior, ChartMetadata, ChartPlugin, t } from '@superset-ui/core';
|
||||
import buildQuery from './buildQuery';
|
||||
import controlPanel from './controlPanel';
|
||||
import transformProps from './transformProps';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
|
||||
export default class FilterTimeColumnPlugin extends ChartPlugin {
|
||||
constructor() {
|
||||
const metadata = new ChartMetadata({
|
||||
name: t('Time column'),
|
||||
description: t('Time column filter plugin'),
|
||||
behaviors: [Behavior.CROSS_FILTER, Behavior.NATIVE_FILTER],
|
||||
thumbnail,
|
||||
});
|
||||
|
||||
super({
|
||||
buildQuery,
|
||||
controlPanel,
|
||||
loadChart: () => import('./TimeColumnFilterPlugin'),
|
||||
metadata,
|
||||
transformProps,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
/**
|
||||
* 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 { ChartProps } from '@superset-ui/core';
|
||||
import { DEFAULT_FORM_DATA } from './types';
|
||||
|
||||
export default function transformProps(chartProps: ChartProps) {
|
||||
const { behaviors, formData, height, hooks, queriesData, width } = chartProps;
|
||||
const { setDataMask = () => {} } = hooks;
|
||||
|
||||
const { data } = queriesData[0];
|
||||
|
||||
return {
|
||||
behaviors,
|
||||
width,
|
||||
height,
|
||||
data,
|
||||
formData: { ...DEFAULT_FORM_DATA, ...formData },
|
||||
setDataMask,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
/**
|
||||
* 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 {
|
||||
QueryFormData,
|
||||
DataRecord,
|
||||
SetDataMaskHook,
|
||||
Behavior,
|
||||
} from '@superset-ui/core';
|
||||
import { RefObject } from 'react';
|
||||
import { PluginFilterStylesProps } from '../types';
|
||||
|
||||
interface PluginFilterTimeColumnCustomizeProps {
|
||||
defaultValue?: string[] | null;
|
||||
currentValue?: string[] | null;
|
||||
inputRef?: RefObject<HTMLInputElement>;
|
||||
}
|
||||
|
||||
export type PluginFilterTimeColumnQueryFormData = QueryFormData &
|
||||
PluginFilterStylesProps &
|
||||
PluginFilterTimeColumnCustomizeProps;
|
||||
|
||||
export type PluginFilterTimeColumnProps = PluginFilterStylesProps & {
|
||||
behaviors: Behavior[];
|
||||
data: DataRecord[];
|
||||
setDataMask: SetDataMaskHook;
|
||||
formData: PluginFilterTimeColumnQueryFormData;
|
||||
};
|
||||
|
||||
export const DEFAULT_FORM_DATA: PluginFilterTimeColumnCustomizeProps = {
|
||||
defaultValue: null,
|
||||
currentValue: null,
|
||||
};
|
|
@ -0,0 +1,93 @@
|
|||
/**
|
||||
* 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 { ensureIsArray, QueryObjectExtras, t, tn } from '@superset-ui/core';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Select } from 'src/common/components';
|
||||
import { Styles, StyledSelect } from '../common';
|
||||
import { PluginFilterTimeGrainProps } from './types';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
export default function PluginFilterTimegrain(
|
||||
props: PluginFilterTimeGrainProps,
|
||||
) {
|
||||
const { data, formData, height, width, setDataMask } = props;
|
||||
const { defaultValue, currentValue, inputRef } = formData;
|
||||
|
||||
const [value, setValue] = useState<string[]>(defaultValue ?? []);
|
||||
|
||||
const handleChange = (values: string[] | string | undefined | null) => {
|
||||
const resultValue: string[] = ensureIsArray<string>(values);
|
||||
const [timeGrain] = resultValue;
|
||||
|
||||
const extras: QueryObjectExtras = {};
|
||||
if (timeGrain) {
|
||||
extras.time_grain_sqla = timeGrain;
|
||||
}
|
||||
setValue(resultValue);
|
||||
setDataMask({
|
||||
nativeFilters: {
|
||||
extraFormData: {
|
||||
override_form_data: {
|
||||
extras,
|
||||
},
|
||||
},
|
||||
currentState: {
|
||||
value: resultValue.length ? resultValue : null,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
handleChange(currentValue ?? []);
|
||||
}, [JSON.stringify(currentValue)]);
|
||||
|
||||
useEffect(() => {
|
||||
handleChange(defaultValue ?? []);
|
||||
// I think after Config Modal update some filter it re-creates default value for all other filters
|
||||
// so we can process it like this `JSON.stringify` or start to use `Immer`
|
||||
}, [JSON.stringify(defaultValue)]);
|
||||
|
||||
const placeholderText =
|
||||
(data || []).length === 0
|
||||
? t('No data')
|
||||
: tn('%s option', '%s options', data.length, data.length);
|
||||
return (
|
||||
<Styles height={height} width={width}>
|
||||
<StyledSelect
|
||||
allowClear
|
||||
value={value}
|
||||
placeholder={placeholderText}
|
||||
// @ts-ignore
|
||||
onChange={handleChange}
|
||||
ref={inputRef}
|
||||
>
|
||||
{(data || []).map((row: { name: string; duration: string }) => {
|
||||
const { name, duration } = row;
|
||||
return (
|
||||
<Option key={duration} value={duration}>
|
||||
{name}
|
||||
</Option>
|
||||
);
|
||||
})}
|
||||
</StyledSelect>
|
||||
</Styles>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { buildQueryContext, QueryFormData } from '@superset-ui/core';
|
||||
|
||||
/**
|
||||
* The buildQuery function is used to create an instance of QueryContext that's
|
||||
* sent to the chart data endpoint. In addition to containing information of which
|
||||
* datasource to use, it specifies the type (e.g. full payload, samples, query) and
|
||||
* format (e.g. CSV or JSON) of the result and whether or not to force refresh the data from
|
||||
* the datasource as opposed to using a cached copy of the data, if available.
|
||||
*
|
||||
* More importantly though, QueryContext contains a property `queries`, which is an array of
|
||||
* QueryObjects specifying individual data requests to be made. A QueryObject specifies which
|
||||
* columns, metrics and filters, among others, to use during the query. Usually it will be enough
|
||||
* to specify just one query based on the baseQueryObject, but for some more advanced use cases
|
||||
* it is possible to define post processing operations in the QueryObject, or multiple queries
|
||||
* if a viz needs multiple different result sets.
|
||||
*/
|
||||
export default function buildQuery(formData: QueryFormData) {
|
||||
return buildQueryContext(formData, () => [
|
||||
{
|
||||
result_type: 'timegrains',
|
||||
columns: [],
|
||||
metrics: [],
|
||||
orderby: [],
|
||||
},
|
||||
]);
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/**
|
||||
* 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 { ControlPanelConfig } from '@superset-ui/chart-controls';
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
controlPanelSections: [],
|
||||
};
|
||||
|
||||
export default config;
|
Binary file not shown.
After Width: | Height: | Size: 5.5 KiB |
|
@ -0,0 +1,42 @@
|
|||
/**
|
||||
* 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 { Behavior, ChartMetadata, ChartPlugin, t } from '@superset-ui/core';
|
||||
import buildQuery from './buildQuery';
|
||||
import controlPanel from './controlPanel';
|
||||
import transformProps from './transformProps';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
|
||||
export default class FilterTimeGrainPlugin extends ChartPlugin {
|
||||
constructor() {
|
||||
const metadata = new ChartMetadata({
|
||||
name: t('Time grain'),
|
||||
description: t('Time grain filter plugin'),
|
||||
behaviors: [Behavior.CROSS_FILTER, Behavior.NATIVE_FILTER],
|
||||
thumbnail,
|
||||
});
|
||||
|
||||
super({
|
||||
buildQuery,
|
||||
controlPanel,
|
||||
loadChart: () => import('./TimeGrainFilterPlugin'),
|
||||
metadata,
|
||||
transformProps,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/**
|
||||
* 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 { ChartProps } from '@superset-ui/core';
|
||||
import { DEFAULT_FORM_DATA } from './types';
|
||||
|
||||
export default function transformProps(chartProps: ChartProps) {
|
||||
const { formData, height, hooks, queriesData, width } = chartProps;
|
||||
const { setDataMask = () => {} } = hooks;
|
||||
|
||||
const { data } = queriesData[0];
|
||||
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
data,
|
||||
formData: { ...DEFAULT_FORM_DATA, ...formData },
|
||||
setDataMask,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
/**
|
||||
* 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 { QueryFormData, DataRecord, SetDataMaskHook } from '@superset-ui/core';
|
||||
import { RefObject } from 'react';
|
||||
import { PluginFilterStylesProps } from '../types';
|
||||
|
||||
interface PluginFilterTimeGrainCustomizeProps {
|
||||
defaultValue?: string[] | null;
|
||||
currentValue?: string[] | null;
|
||||
inputRef?: RefObject<HTMLInputElement>;
|
||||
}
|
||||
|
||||
export type PluginFilterTimeGrainQueryFormData = QueryFormData &
|
||||
PluginFilterStylesProps &
|
||||
PluginFilterTimeGrainCustomizeProps;
|
||||
|
||||
export type PluginFilterTimeGrainProps = PluginFilterStylesProps & {
|
||||
data: DataRecord[];
|
||||
setDataMask: SetDataMaskHook;
|
||||
formData: PluginFilterTimeGrainQueryFormData;
|
||||
};
|
||||
|
||||
export const DEFAULT_FORM_DATA: PluginFilterTimeGrainCustomizeProps = {
|
||||
defaultValue: null,
|
||||
currentValue: null,
|
||||
};
|
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* 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 { styled } from '@superset-ui/core';
|
||||
import { Select } from 'src/common/components';
|
||||
import { PluginFilterStylesProps } from './types';
|
||||
|
||||
export const Styles = styled.div<PluginFilterStylesProps>`
|
||||
height: ${({ height }) => height};
|
||||
width: ${({ width }) => width};
|
||||
`;
|
||||
|
||||
export const StyledSelect = styled(Select)`
|
||||
width: 100%;
|
||||
`;
|
|
@ -19,3 +19,5 @@
|
|||
export { default as SelectFilterPlugin } from './Select';
|
||||
export { default as RangeFilterPlugin } from './Range';
|
||||
export { default as TimeFilterPlugin } from './Time';
|
||||
export { default as TimeColumnFilterPlugin } from './TimeColumn';
|
||||
export { default as TimeGrainFilterPlugin } from './TimeGrain';
|
||||
|
|
|
@ -26,9 +26,13 @@ export const getSelectExtraFormData = (
|
|||
) => ({
|
||||
append_form_data: emptyFilter
|
||||
? {
|
||||
extras: {
|
||||
where: '1 = 0',
|
||||
},
|
||||
adhoc_filters: [
|
||||
{
|
||||
expressionType: 'SQL',
|
||||
clause: 'WHERE',
|
||||
sqlExpression: '1 = 0',
|
||||
},
|
||||
],
|
||||
}
|
||||
: {
|
||||
filters:
|
||||
|
|
|
@ -63,6 +63,8 @@ import {
|
|||
SelectFilterPlugin,
|
||||
RangeFilterPlugin,
|
||||
TimeFilterPlugin,
|
||||
TimeColumnFilterPlugin,
|
||||
TimeGrainFilterPlugin,
|
||||
} from 'src/filters/components/';
|
||||
import FilterBoxChartPlugin from '../FilterBox/FilterBoxChartPlugin';
|
||||
import TimeTableChartPlugin from '../TimeTable/TimeTableChartPlugin';
|
||||
|
@ -115,6 +117,8 @@ export default class MainPreset extends Preset {
|
|||
new SelectFilterPlugin().configure({ key: 'filter_select' }),
|
||||
new RangeFilterPlugin().configure({ key: 'filter_range' }),
|
||||
new TimeFilterPlugin().configure({ key: 'filter_time' }),
|
||||
new TimeColumnFilterPlugin().configure({ key: 'filter_timecolumn' }),
|
||||
new TimeGrainFilterPlugin().configure({ key: 'filter_timegrain' }),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
|
|
@ -908,6 +908,11 @@ class ChartDataQueryObjectSchema(Schema):
|
|||
allow_none=True,
|
||||
example={"__time_range": "1 year ago : now"},
|
||||
)
|
||||
apply_fetch_values_predicate = fields.Boolean(
|
||||
description="Add fetch values predicate (where clause) to query "
|
||||
"if defined in datasource",
|
||||
allow_none=True,
|
||||
)
|
||||
filters = fields.List(fields.Nested(ChartDataFilterSchema), allow_none=True)
|
||||
granularity = fields.String(
|
||||
description="Name of temporal column used for time filtering. For legacy Druid "
|
||||
|
|
|
@ -73,6 +73,7 @@ class QueryObject:
|
|||
|
||||
annotation_layers: List[Dict[str, Any]]
|
||||
applied_time_extras: Dict[str, str]
|
||||
apply_fetch_values_predicate: bool
|
||||
granularity: Optional[str]
|
||||
from_dttm: Optional[datetime]
|
||||
to_dttm: Optional[datetime]
|
||||
|
@ -100,6 +101,7 @@ class QueryObject:
|
|||
result_type: Optional[ChartDataResultType] = None,
|
||||
annotation_layers: Optional[List[Dict[str, Any]]] = None,
|
||||
applied_time_extras: Optional[Dict[str, str]] = None,
|
||||
apply_fetch_values_predicate: bool = False,
|
||||
granularity: Optional[str] = None,
|
||||
metrics: Optional[List[Union[Dict[str, Any], str]]] = None,
|
||||
groupby: Optional[List[str]] = None,
|
||||
|
@ -127,6 +129,7 @@ class QueryObject:
|
|||
)
|
||||
self.result_type = result_type
|
||||
annotation_layers = annotation_layers or []
|
||||
self.apply_fetch_values_predicate = apply_fetch_values_predicate or False
|
||||
metrics = metrics or []
|
||||
columns = columns or []
|
||||
groupby = groupby or []
|
||||
|
@ -262,6 +265,7 @@ class QueryObject:
|
|||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
query_object_dict = {
|
||||
"apply_fetch_values_predicate": self.apply_fetch_values_predicate,
|
||||
"granularity": self.granularity,
|
||||
"groupby": self.groupby,
|
||||
"from_dttm": self.from_dttm,
|
||||
|
@ -291,18 +295,24 @@ class QueryObject:
|
|||
"""
|
||||
cache_dict = self.to_dict()
|
||||
cache_dict.update(extra)
|
||||
|
||||
# TODO: the below KVs can all be cleaned up and moved to `to_dict()` at some
|
||||
# predetermined point in time when orgs are aware that the previously
|
||||
# chached results will be invalidated.
|
||||
if not self.apply_fetch_values_predicate:
|
||||
del cache_dict["apply_fetch_values_predicate"]
|
||||
if self.datasource:
|
||||
cache_dict["datasource"] = self.datasource.uid
|
||||
if self.result_type:
|
||||
cache_dict["result_type"] = self.result_type
|
||||
|
||||
for k in ["from_dttm", "to_dttm"]:
|
||||
del cache_dict[k]
|
||||
if self.time_range:
|
||||
cache_dict["time_range"] = self.time_range
|
||||
if self.post_processing:
|
||||
cache_dict["post_processing"] = self.post_processing
|
||||
|
||||
for k in ["from_dttm", "to_dttm"]:
|
||||
del cache_dict[k]
|
||||
|
||||
annotation_fields = [
|
||||
"annotationType",
|
||||
"descriptionColumns",
|
||||
|
|
|
@ -1139,11 +1139,17 @@ class DruidDatasource(Model, BaseDatasource):
|
|||
client: Optional["PyDruid"] = None,
|
||||
order_desc: bool = True,
|
||||
is_rowcount: bool = False,
|
||||
apply_fetch_values_predicate: bool = False,
|
||||
) -> str:
|
||||
"""Runs a query against Druid and returns a dataframe."""
|
||||
# is_rowcount is only supported on SQL connector
|
||||
# is_rowcount and apply_fetch_values_predicate is only
|
||||
# supported on SQL connector
|
||||
if is_rowcount:
|
||||
raise SupersetException("is_rowcount is not supported on Druid connector")
|
||||
if apply_fetch_values_predicate:
|
||||
raise SupersetException(
|
||||
"apply_fetch_values_predicate is not supported on Druid connector"
|
||||
)
|
||||
|
||||
# TODO refactor into using a TBD Query object
|
||||
client = client or self.cluster.get_pydruid_client()
|
||||
|
|
|
@ -48,7 +48,7 @@ from sqlalchemy import (
|
|||
from sqlalchemy.orm import backref, Query, relationship, RelationshipProperty, Session
|
||||
from sqlalchemy.schema import UniqueConstraint
|
||||
from sqlalchemy.sql import column, ColumnElement, literal_column, table, text
|
||||
from sqlalchemy.sql.expression import Label, Select, TextAsFrom
|
||||
from sqlalchemy.sql.expression import Label, Select, TextAsFrom, TextClause
|
||||
from sqlalchemy.types import TypeEngine
|
||||
|
||||
from superset import app, db, is_feature_enabled, security_manager
|
||||
|
@ -720,6 +720,18 @@ class SqlaTable( # pylint: disable=too-many-public-methods,too-many-instance-at
|
|||
except (TypeError, json.JSONDecodeError):
|
||||
return {}
|
||||
|
||||
def get_fetch_values_predicate(self) -> TextClause:
|
||||
tp = self.get_template_processor()
|
||||
try:
|
||||
return text(tp.process_template(self.fetch_values_predicate))
|
||||
except TemplateError as ex:
|
||||
raise QueryObjectValidationError(
|
||||
_(
|
||||
"Error in jinja expression in fetch values predicate: %(msg)s",
|
||||
msg=ex.message,
|
||||
)
|
||||
)
|
||||
|
||||
def values_for_column(self, column_name: str, limit: int = 10000) -> List[Any]:
|
||||
"""Runs query against sqla to retrieve some
|
||||
sample values for the given column.
|
||||
|
@ -737,16 +749,7 @@ class SqlaTable( # pylint: disable=too-many-public-methods,too-many-instance-at
|
|||
qry = qry.limit(limit)
|
||||
|
||||
if self.fetch_values_predicate:
|
||||
tp = self.get_template_processor()
|
||||
try:
|
||||
qry = qry.where(text(tp.process_template(self.fetch_values_predicate)))
|
||||
except TemplateError as ex:
|
||||
raise QueryObjectValidationError(
|
||||
_(
|
||||
"Error in jinja expression in fetch values predicate: %(msg)s",
|
||||
msg=ex.message,
|
||||
)
|
||||
)
|
||||
qry = qry.where(self.get_fetch_values_predicate())
|
||||
|
||||
engine = self.database.get_sqla_engine()
|
||||
sql = "{}".format(qry.compile(engine, compile_kwargs={"literal_binds": True}))
|
||||
|
@ -899,6 +902,7 @@ class SqlaTable( # pylint: disable=too-many-public-methods,too-many-instance-at
|
|||
extras: Optional[Dict[str, Any]] = None,
|
||||
order_desc: bool = True,
|
||||
is_rowcount: bool = False,
|
||||
apply_fetch_values_predicate: bool = False,
|
||||
) -> SqlaQuery:
|
||||
"""Querying any sqla table from this common interface"""
|
||||
template_kwargs = {
|
||||
|
@ -1133,6 +1137,8 @@ class SqlaTable( # pylint: disable=too-many-public-methods,too-many-instance-at
|
|||
)
|
||||
)
|
||||
having_clause_and += [sa.text("({})".format(having))]
|
||||
if apply_fetch_values_predicate and self.fetch_values_predicate:
|
||||
qry = qry.where(self.get_fetch_values_predicate())
|
||||
if granularity:
|
||||
qry = qry.where(and_(*(time_filters + where_clause_and)))
|
||||
else:
|
||||
|
|
|
@ -40,7 +40,7 @@ from typing import (
|
|||
import pandas as pd
|
||||
import sqlparse
|
||||
from flask import g
|
||||
from flask_babel import lazy_gettext as _
|
||||
from flask_babel import gettext as __, lazy_gettext as _
|
||||
from sqlalchemy import column, DateTime, select
|
||||
from sqlalchemy.engine.base import Engine
|
||||
from sqlalchemy.engine.interfaces import Compiled, Dialect
|
||||
|
@ -77,23 +77,23 @@ QueryStatus = utils.QueryStatus
|
|||
config = app.config
|
||||
|
||||
builtin_time_grains: Dict[Optional[str], str] = {
|
||||
None: "Time Column",
|
||||
"PT1S": "second",
|
||||
"PT1M": "minute",
|
||||
"PT5M": "5 minute",
|
||||
"PT10M": "10 minute",
|
||||
"PT15M": "15 minute",
|
||||
"PT0.5H": "half hour",
|
||||
"PT1H": "hour",
|
||||
"P1D": "day",
|
||||
"P1W": "week",
|
||||
"P1M": "month",
|
||||
"P0.25Y": "quarter",
|
||||
"P1Y": "year",
|
||||
"1969-12-28T00:00:00Z/P1W": "week_start_sunday",
|
||||
"1969-12-29T00:00:00Z/P1W": "week_start_monday",
|
||||
"P1W/1970-01-03T00:00:00Z": "week_ending_saturday",
|
||||
"P1W/1970-01-04T00:00:00Z": "week_ending_sunday",
|
||||
None: __("Original value"),
|
||||
"PT1S": __("Second"),
|
||||
"PT1M": __("Minute"),
|
||||
"PT5M": __("5 minute"),
|
||||
"PT10M": __("10 minute"),
|
||||
"PT15M": __("15 minute"),
|
||||
"PT0.5H": __("Half hour"),
|
||||
"PT1H": __("Hour"),
|
||||
"P1D": __("Day"),
|
||||
"P1W": __("Week"),
|
||||
"P1M": __("Month"),
|
||||
"P0.25Y": __("Quarter"),
|
||||
"P1Y": __("Year"),
|
||||
"1969-12-28T00:00:00Z/P1W": __("Week starting sunday"),
|
||||
"1969-12-29T00:00:00Z/P1W": __("Week starting monday"),
|
||||
"P1W/1970-01-03T00:00:00Z": __("Week ending saturday"),
|
||||
"P1W/1970-01-04T00:00:00Z": __("Week_ending sunday"),
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1042,12 +1042,16 @@ def merge_extra_form_data(form_data: Dict[str, Any]) -> None:
|
|||
and add applied time extras to the payload.
|
||||
"""
|
||||
time_extras = {
|
||||
"granularity": "__granularity",
|
||||
"granularity_sqla": "__granularity",
|
||||
"time_range": "__time_range",
|
||||
"granularity_sqla": "__time_col",
|
||||
}
|
||||
allowed_extra_overrides: Dict[str, Optional[str]] = {
|
||||
"time_grain_sqla": "__time_grain",
|
||||
"druid_time_origin": "__time_origin",
|
||||
"granularity": "__granularity",
|
||||
"time_range_endpoints": None,
|
||||
}
|
||||
|
||||
applied_time_extras = form_data.get("applied_time_extras", {})
|
||||
form_data["applied_time_extras"] = applied_time_extras
|
||||
extra_form_data = form_data.pop("extra_form_data", {})
|
||||
|
@ -1060,12 +1064,20 @@ def merge_extra_form_data(form_data: Dict[str, Any]) -> None:
|
|||
time_extra = time_extras.get(key)
|
||||
if time_extra:
|
||||
applied_time_extras[time_extra] = value
|
||||
extras = form_data.get("extras", {})
|
||||
for key, value in allowed_extra_overrides.items():
|
||||
extra = extras.get(key)
|
||||
if value and extra:
|
||||
applied_time_extras[value] = extra
|
||||
form_data.update(extras)
|
||||
|
||||
adhoc_filters = form_data.get("adhoc_filters", [])
|
||||
form_data["adhoc_filters"] = adhoc_filters
|
||||
append_adhoc_filters = append_form_data.get("adhoc_filters", [])
|
||||
adhoc_filters.extend({"isExtra": True, **fltr} for fltr in append_adhoc_filters)
|
||||
if append_filters:
|
||||
adhoc_filters.extend(
|
||||
[to_adhoc({"isExtra": True, **fltr}) for fltr in append_filters if fltr]
|
||||
to_adhoc({"isExtra": True, **fltr}) for fltr in append_filters if fltr
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -107,7 +107,7 @@ class TestCore(SupersetTestCase):
|
|||
self.login(username="admin")
|
||||
slc = self.get_slice("Girls", db.session)
|
||||
resp = self.get_resp("/superset/slice/{}/".format(slc.id))
|
||||
assert "Time Column" in resp
|
||||
assert "Original value" in resp
|
||||
assert "List Roles" in resp
|
||||
|
||||
# Testing overrides
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
"""Utils to provide dashboards for tests"""
|
||||
|
||||
import json
|
||||
from typing import Any, Dict, List
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from pandas import DataFrame
|
||||
|
||||
|
@ -34,6 +34,7 @@ def create_table_for_dashboard(
|
|||
database: Database,
|
||||
dtype: Dict[str, Any],
|
||||
table_description: str = "",
|
||||
fetch_values_predicate: Optional[str] = None,
|
||||
) -> SqlaTable:
|
||||
df.to_sql(
|
||||
table_name,
|
||||
|
@ -51,6 +52,8 @@ def create_table_for_dashboard(
|
|||
)
|
||||
if not table:
|
||||
table = table_source(table_name=table_name)
|
||||
if fetch_values_predicate:
|
||||
table.fetch_values_predicate = fetch_values_predicate
|
||||
table.database = database
|
||||
table.description = table_description
|
||||
db.session.merge(table)
|
||||
|
|
|
@ -225,7 +225,7 @@ class TestExportDatabasesCommand(SupersetTestCase):
|
|||
"default_endpoint": None,
|
||||
"description": "",
|
||||
"extra": None,
|
||||
"fetch_values_predicate": None,
|
||||
"fetch_values_predicate": "123 = 123",
|
||||
"filter_select_enabled": True,
|
||||
"main_dttm_col": "ds",
|
||||
"metrics": [
|
||||
|
|
|
@ -18,7 +18,7 @@ import json
|
|||
import string
|
||||
from datetime import date, datetime
|
||||
from random import choice, getrandbits, randint, random, uniform
|
||||
from typing import Any, Dict, List
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import pandas as pd
|
||||
import pytest
|
||||
|
@ -31,7 +31,7 @@ from superset.models.core import Database
|
|||
from superset.models.dashboard import Dashboard
|
||||
from superset.models.slice import Slice
|
||||
from superset.utils.core import get_example_database
|
||||
from tests.dashboard_utils import create_dashboard, create_table_for_dashboard
|
||||
from tests.dashboard_utils import create_table_for_dashboard
|
||||
from tests.test_app import app
|
||||
|
||||
|
||||
|
@ -63,7 +63,13 @@ def _load_data():
|
|||
"state": String(10),
|
||||
"name": String(255),
|
||||
}
|
||||
table = _create_table(df, table_name, database, dtype)
|
||||
table = _create_table(
|
||||
df=df,
|
||||
table_name=table_name,
|
||||
database=database,
|
||||
dtype=dtype,
|
||||
fetch_values_predicate="123 = 123",
|
||||
)
|
||||
|
||||
from superset.examples.birth_names import create_slices, create_dashboard
|
||||
|
||||
|
@ -75,9 +81,19 @@ def _load_data():
|
|||
|
||||
|
||||
def _create_table(
|
||||
df: DataFrame, table_name: str, database: "Database", dtype: Dict[str, Any]
|
||||
df: DataFrame,
|
||||
table_name: str,
|
||||
database: "Database",
|
||||
dtype: Dict[str, Any],
|
||||
fetch_values_predicate: Optional[str] = None,
|
||||
):
|
||||
table = create_table_for_dashboard(df, table_name, database, dtype)
|
||||
table = create_table_for_dashboard(
|
||||
df=df,
|
||||
table_name=table_name,
|
||||
database=database,
|
||||
dtype=dtype,
|
||||
fetch_values_predicate=fetch_values_predicate,
|
||||
)
|
||||
from superset.examples.birth_names import _add_table_metrics, _set_table_metadata
|
||||
|
||||
_set_table_metadata(table, database)
|
||||
|
|
|
@ -302,11 +302,44 @@ class TestQueryContext(SupersetTestCase):
|
|||
payload["result_type"] = ChartDataResultType.QUERY.value
|
||||
query_context = ChartDataQueryContextSchema().load(payload)
|
||||
responses = query_context.get_payload()
|
||||
self.assertEqual(len(responses), 1)
|
||||
assert len(responses) == 1
|
||||
response = responses["queries"][0]
|
||||
self.assertEqual(len(response), 2)
|
||||
self.assertEqual(response["language"], "sql")
|
||||
self.assertIn("SELECT", response["query"])
|
||||
assert len(response) == 2
|
||||
assert response["language"] == "sql"
|
||||
assert "SELECT" in response["query"]
|
||||
|
||||
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
|
||||
def test_fetch_values_predicate_in_query(self):
|
||||
"""
|
||||
Ensure that fetch values predicate is added to query
|
||||
"""
|
||||
self.login(username="admin")
|
||||
payload = get_query_context("birth_names")
|
||||
payload["result_type"] = ChartDataResultType.QUERY.value
|
||||
payload["queries"][0]["apply_fetch_values_predicate"] = True
|
||||
query_context = ChartDataQueryContextSchema().load(payload)
|
||||
responses = query_context.get_payload()
|
||||
assert len(responses) == 1
|
||||
response = responses["queries"][0]
|
||||
assert len(response) == 2
|
||||
assert response["language"] == "sql"
|
||||
assert "123 = 123" in response["query"]
|
||||
|
||||
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
|
||||
def test_fetch_values_predicate_not_in_query(self):
|
||||
"""
|
||||
Ensure that fetch values predicate is not added to query
|
||||
"""
|
||||
self.login(username="admin")
|
||||
payload = get_query_context("birth_names")
|
||||
payload["result_type"] = ChartDataResultType.QUERY.value
|
||||
query_context = ChartDataQueryContextSchema().load(payload)
|
||||
responses = query_context.get_payload()
|
||||
assert len(responses) == 1
|
||||
response = responses["queries"][0]
|
||||
assert len(response) == 2
|
||||
assert response["language"] == "sql"
|
||||
assert "123 = 123" not in response["query"]
|
||||
|
||||
def test_query_object_unknown_fields(self):
|
||||
"""
|
||||
|
|
|
@ -924,15 +924,38 @@ class TestUtils(SupersetTestCase):
|
|||
"time_range": "Last 10 days",
|
||||
"extra_form_data": {
|
||||
"append_form_data": {
|
||||
"filters": [{"col": "foo", "op": "IN", "val": "bar"}]
|
||||
"filters": [{"col": "foo", "op": "IN", "val": ["bar"]}],
|
||||
"adhoc_filters": [
|
||||
{
|
||||
"expressionType": "SQL",
|
||||
"clause": "WHERE",
|
||||
"sqlExpression": "1 = 0",
|
||||
}
|
||||
],
|
||||
},
|
||||
"override_form_data": {"time_range": "Last 100 years",},
|
||||
},
|
||||
}
|
||||
merge_extra_form_data(form_data)
|
||||
assert form_data["applied_time_extras"] == {"__time_range": "Last 100 years"}
|
||||
adhoc_filters = form_data["adhoc_filters"]
|
||||
assert adhoc_filters[0] == {
|
||||
"clause": "WHERE",
|
||||
"expressionType": "SQL",
|
||||
"isExtra": True,
|
||||
"sqlExpression": "1 = 0",
|
||||
}
|
||||
converted_filter = adhoc_filters[1]
|
||||
del converted_filter["filterOptionName"]
|
||||
assert converted_filter == {
|
||||
"clause": "WHERE",
|
||||
"comparator": ["bar"],
|
||||
"expressionType": "SIMPLE",
|
||||
"isExtra": True,
|
||||
"operator": "IN",
|
||||
"subject": "foo",
|
||||
}
|
||||
assert form_data["time_range"] == "Last 100 years"
|
||||
assert len(form_data["adhoc_filters"]) == 1
|
||||
|
||||
def test_ssl_certificate_parse(self):
|
||||
parsed_certificate = parse_ssl_cert(ssl_certificate)
|
||||
|
|
Loading…
Reference in New Issue