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",
|
"@babel/runtime-corejs3": "^7.12.5",
|
||||||
"@data-ui/sparkline": "^0.0.84",
|
"@data-ui/sparkline": "^0.0.84",
|
||||||
"@emotion/core": "^10.0.35",
|
"@emotion/core": "^10.0.35",
|
||||||
"@superset-ui/chart-controls": "^0.17.14",
|
"@superset-ui/chart-controls": "^0.17.15",
|
||||||
"@superset-ui/core": "^0.17.14",
|
"@superset-ui/core": "^0.17.15",
|
||||||
"@superset-ui/legacy-plugin-chart-calendar": "^0.17.14",
|
"@superset-ui/legacy-plugin-chart-calendar": "^0.17.15",
|
||||||
"@superset-ui/legacy-plugin-chart-chord": "^0.17.14",
|
"@superset-ui/legacy-plugin-chart-chord": "^0.17.15",
|
||||||
"@superset-ui/legacy-plugin-chart-country-map": "^0.17.14",
|
"@superset-ui/legacy-plugin-chart-country-map": "^0.17.15",
|
||||||
"@superset-ui/legacy-plugin-chart-event-flow": "^0.17.14",
|
"@superset-ui/legacy-plugin-chart-event-flow": "^0.17.15",
|
||||||
"@superset-ui/legacy-plugin-chart-force-directed": "^0.17.14",
|
"@superset-ui/legacy-plugin-chart-force-directed": "^0.17.15",
|
||||||
"@superset-ui/legacy-plugin-chart-heatmap": "^0.17.14",
|
"@superset-ui/legacy-plugin-chart-heatmap": "^0.17.15",
|
||||||
"@superset-ui/legacy-plugin-chart-histogram": "^0.17.14",
|
"@superset-ui/legacy-plugin-chart-histogram": "^0.17.15",
|
||||||
"@superset-ui/legacy-plugin-chart-horizon": "^0.17.14",
|
"@superset-ui/legacy-plugin-chart-horizon": "^0.17.15",
|
||||||
"@superset-ui/legacy-plugin-chart-map-box": "^0.17.14",
|
"@superset-ui/legacy-plugin-chart-map-box": "^0.17.15",
|
||||||
"@superset-ui/legacy-plugin-chart-paired-t-test": "^0.17.14",
|
"@superset-ui/legacy-plugin-chart-paired-t-test": "^0.17.15",
|
||||||
"@superset-ui/legacy-plugin-chart-parallel-coordinates": "^0.17.14",
|
"@superset-ui/legacy-plugin-chart-parallel-coordinates": "^0.17.15",
|
||||||
"@superset-ui/legacy-plugin-chart-partition": "^0.17.14",
|
"@superset-ui/legacy-plugin-chart-partition": "^0.17.15",
|
||||||
"@superset-ui/legacy-plugin-chart-pivot-table": "^0.17.14",
|
"@superset-ui/legacy-plugin-chart-pivot-table": "^0.17.15",
|
||||||
"@superset-ui/legacy-plugin-chart-rose": "^0.17.14",
|
"@superset-ui/legacy-plugin-chart-rose": "^0.17.15",
|
||||||
"@superset-ui/legacy-plugin-chart-sankey": "^0.17.14",
|
"@superset-ui/legacy-plugin-chart-sankey": "^0.17.15",
|
||||||
"@superset-ui/legacy-plugin-chart-sankey-loop": "^0.17.14",
|
"@superset-ui/legacy-plugin-chart-sankey-loop": "^0.17.15",
|
||||||
"@superset-ui/legacy-plugin-chart-sunburst": "^0.17.14",
|
"@superset-ui/legacy-plugin-chart-sunburst": "^0.17.15",
|
||||||
"@superset-ui/legacy-plugin-chart-treemap": "^0.17.14",
|
"@superset-ui/legacy-plugin-chart-treemap": "^0.17.15",
|
||||||
"@superset-ui/legacy-plugin-chart-world-map": "^0.17.14",
|
"@superset-ui/legacy-plugin-chart-world-map": "^0.17.15",
|
||||||
"@superset-ui/legacy-preset-chart-big-number": "^0.17.14",
|
"@superset-ui/legacy-preset-chart-big-number": "^0.17.15",
|
||||||
"@superset-ui/legacy-preset-chart-deckgl": "^0.4.6",
|
"@superset-ui/legacy-preset-chart-deckgl": "^0.4.6",
|
||||||
"@superset-ui/legacy-preset-chart-nvd3": "^0.17.14",
|
"@superset-ui/legacy-preset-chart-nvd3": "^0.17.15",
|
||||||
"@superset-ui/plugin-chart-echarts": "^0.17.14",
|
"@superset-ui/plugin-chart-echarts": "^0.17.15",
|
||||||
"@superset-ui/plugin-chart-table": "^0.17.14",
|
"@superset-ui/plugin-chart-table": "^0.17.15",
|
||||||
"@superset-ui/plugin-chart-word-cloud": "^0.17.14",
|
"@superset-ui/plugin-chart-word-cloud": "^0.17.15",
|
||||||
"@superset-ui/preset-chart-xy": "^0.17.14",
|
"@superset-ui/preset-chart-xy": "^0.17.15",
|
||||||
"@vx/responsive": "^0.0.195",
|
"@vx/responsive": "^0.0.195",
|
||||||
"abortcontroller-polyfill": "^1.1.9",
|
"abortcontroller-polyfill": "^1.1.9",
|
||||||
"antd": "^4.9.4",
|
"antd": "^4.9.4",
|
||||||
|
|
|
@ -107,9 +107,13 @@ describe('Filter utils', () => {
|
||||||
expect(getSelectExtraFormData('testCol', ['value'], true, false)).toEqual(
|
expect(getSelectExtraFormData('testCol', ['value'], true, false)).toEqual(
|
||||||
{
|
{
|
||||||
append_form_data: {
|
append_form_data: {
|
||||||
extras: {
|
adhoc_filters: [
|
||||||
where: '1 = 0',
|
{
|
||||||
|
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 { DataMaskStateWithId } from 'src/dataMask/types';
|
||||||
import { Layout } from '../../types';
|
import { Layout } from '../../types';
|
||||||
import { getTreeCheckedItems } from '../nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/utils';
|
import { getTreeCheckedItems } from '../nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/utils';
|
||||||
|
import { FilterValue } from '../nativeFilters/types';
|
||||||
|
|
||||||
export enum IndicatorStatus {
|
export enum IndicatorStatus {
|
||||||
Unset = 'UNSET',
|
Unset = 'UNSET',
|
||||||
|
@ -52,7 +53,7 @@ const selectIndicatorValue = (
|
||||||
columnKey: string,
|
columnKey: string,
|
||||||
filter: Filter,
|
filter: Filter,
|
||||||
datasource: Datasource,
|
datasource: Datasource,
|
||||||
): string[] => {
|
): FilterValue => {
|
||||||
const values = filter.columns[columnKey];
|
const values = filter.columns[columnKey];
|
||||||
const arrValues = Array.isArray(values) ? values : [values];
|
const arrValues = Array.isArray(values) ? values : [values];
|
||||||
|
|
||||||
|
@ -132,7 +133,7 @@ const getRejectedColumns = (chart: any): Set<string> =>
|
||||||
export type Indicator = {
|
export type Indicator = {
|
||||||
column?: string;
|
column?: string;
|
||||||
name: string;
|
name: string;
|
||||||
value: string[];
|
value: FilterValue;
|
||||||
status: IndicatorStatus;
|
status: IndicatorStatus;
|
||||||
path: string[];
|
path: string[];
|
||||||
};
|
};
|
||||||
|
@ -185,20 +186,22 @@ export const selectNativeIndicatorsForChart = (
|
||||||
const rejectedColumns = getRejectedColumns(chart);
|
const rejectedColumns = getRejectedColumns(chart);
|
||||||
|
|
||||||
const getStatus = (
|
const getStatus = (
|
||||||
value: string[],
|
value: FilterValue,
|
||||||
isAffectedByScope: boolean,
|
isAffectedByScope: boolean,
|
||||||
column?: string,
|
column?: string,
|
||||||
): IndicatorStatus => {
|
): IndicatorStatus => {
|
||||||
|
// a filter is only considered unset if it's value is null
|
||||||
|
const hasValue = value !== null;
|
||||||
if (!isAffectedByScope) {
|
if (!isAffectedByScope) {
|
||||||
return IndicatorStatus.Unset;
|
return IndicatorStatus.Unset;
|
||||||
}
|
}
|
||||||
if (!column) {
|
if (!column && hasValue) {
|
||||||
// Filter without datasource
|
// Filter without datasource
|
||||||
return IndicatorStatus.Applied;
|
return IndicatorStatus.Applied;
|
||||||
}
|
}
|
||||||
if (column && rejectedColumns.has(column))
|
if (column && rejectedColumns.has(column))
|
||||||
return IndicatorStatus.Incompatible;
|
return IndicatorStatus.Incompatible;
|
||||||
if (column && appliedColumns.has(column) && value.length > 0) {
|
if (column && appliedColumns.has(column) && hasValue) {
|
||||||
return IndicatorStatus.Applied;
|
return IndicatorStatus.Applied;
|
||||||
}
|
}
|
||||||
return IndicatorStatus.Unset;
|
return IndicatorStatus.Unset;
|
||||||
|
|
|
@ -60,7 +60,7 @@ const FilterValue: React.FC<FilterProps> = ({
|
||||||
column = {},
|
column = {},
|
||||||
}: Partial<{ datasetId: number; column: { name?: string } }> = target;
|
}: Partial<{ datasetId: number; column: { name?: string } }> = target;
|
||||||
const { name: groupby } = column;
|
const { name: groupby } = column;
|
||||||
const hasDataSource = !!(datasetId && groupby);
|
const hasDataSource = !!datasetId;
|
||||||
const [loading, setLoading] = useState<boolean>(hasDataSource);
|
const [loading, setLoading] = useState<boolean>(hasDataSource);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const newFormData = getFormData({
|
const newFormData = getFormData({
|
||||||
|
@ -137,7 +137,7 @@ const FilterValue: React.FC<FilterProps> = ({
|
||||||
width={220}
|
width={220}
|
||||||
formData={formData}
|
formData={formData}
|
||||||
// For charts that don't have datasource we need workaround for empty placeholder
|
// For charts that don't have datasource we need workaround for empty placeholder
|
||||||
queriesData={hasDataSource ? state : [{ data: [null] }]}
|
queriesData={hasDataSource ? state : [{ data: [{}] }]}
|
||||||
chartType={filterType}
|
chartType={filterType}
|
||||||
behaviors={[Behavior.NATIVE_FILTER]}
|
behaviors={[Behavior.NATIVE_FILTER]}
|
||||||
hooks={{ setDataMask }}
|
hooks={{ setDataMask }}
|
||||||
|
|
|
@ -48,7 +48,6 @@ const ControlItems: FC<ControlItemsProps> = ({
|
||||||
const controlPanelRegistry = getChartControlPanelRegistry();
|
const controlPanelRegistry = getChartControlPanelRegistry();
|
||||||
const controlItems =
|
const controlItems =
|
||||||
getControlItems(controlPanelRegistry.get(filterType)) ?? [];
|
getControlItems(controlPanelRegistry.get(filterType)) ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{controlItems
|
{controlItems
|
||||||
|
|
|
@ -78,6 +78,8 @@ export interface FiltersConfigFormProps {
|
||||||
parentFilters: { id: string; title: string }[];
|
parentFilters: { id: string; title: string }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FILTERS_WITH_ONLY_DATASOURCE = ['filter_timegrain', 'filter_timecolumn'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The configuration form for a specific filter.
|
* The configuration form for a specific filter.
|
||||||
* Assigns field values to `filters[filterId]` in the form.
|
* Assigns field values to `filters[filterId]` in the form.
|
||||||
|
@ -104,17 +106,21 @@ export const FiltersConfigForm: React.FC<FiltersConfigFormProps> = ({
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const hasDatasource = !!nativeFilterItems[formFilter?.filterType]?.value
|
const hasDatasource = !!nativeFilterItems[formFilter?.filterType]?.value
|
||||||
?.datasourceCount;
|
?.datasourceCount;
|
||||||
|
const hasColumn =
|
||||||
|
hasDatasource &&
|
||||||
|
!FILTERS_WITH_ONLY_DATASOURCE.includes(formFilter?.filterType);
|
||||||
|
|
||||||
const hasFilledDatasource =
|
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 initDatasetId = filterToEdit?.targets[0]?.datasetId;
|
||||||
const initColumn = filterToEdit?.targets[0]?.column?.name;
|
const initColumn = filterToEdit?.targets[0]?.column?.name;
|
||||||
const newFormData = getFormData({
|
const newFormData = getFormData({
|
||||||
datasetId: formFilter?.dataset?.value,
|
datasetId: formFilter?.dataset?.value,
|
||||||
groupby: formFilter?.column,
|
groupby: hasColumn ? formFilter?.column : undefined,
|
||||||
defaultValue: formFilter?.defaultValue,
|
defaultValue: formFilter?.defaultValue,
|
||||||
...formFilter,
|
...formFilter,
|
||||||
});
|
});
|
||||||
|
@ -204,6 +210,7 @@ export const FiltersConfigForm: React.FC<FiltersConfigFormProps> = ({
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</StyledFormItem>
|
</StyledFormItem>
|
||||||
|
{hasColumn && (
|
||||||
<StyledFormItem
|
<StyledFormItem
|
||||||
// don't show the column select unless we have a dataset
|
// don't show the column select unless we have a dataset
|
||||||
// style={{ display: datasetId == null ? undefined : 'none' }}
|
// style={{ display: datasetId == null ? undefined : 'none' }}
|
||||||
|
@ -220,6 +227,7 @@ export const FiltersConfigForm: React.FC<FiltersConfigFormProps> = ({
|
||||||
onChange={forceUpdate}
|
onChange={forceUpdate}
|
||||||
/>
|
/>
|
||||||
</StyledFormItem>
|
</StyledFormItem>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{hasFilledDatasource && (
|
{hasFilledDatasource && (
|
||||||
|
|
|
@ -31,6 +31,7 @@ export const useBackendFormUpdate = (
|
||||||
filterId: string,
|
filterId: string,
|
||||||
filterToEdit?: Filter,
|
filterToEdit?: Filter,
|
||||||
hasDatasource?: boolean,
|
hasDatasource?: boolean,
|
||||||
|
hasColumn?: boolean,
|
||||||
) => {
|
) => {
|
||||||
const forceUpdate = useForceUpdate();
|
const forceUpdate = useForceUpdate();
|
||||||
const formFilter = (form.getFieldValue('filters') || {})[filterId];
|
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
|
// No need to check data set change because it cascading update column
|
||||||
// So check that column exists is enough
|
// So check that column exists is enough
|
||||||
if (!formFilter?.column) {
|
if (hasColumn && !formFilter?.column) {
|
||||||
setFilterFieldValues(form, filterId, {
|
setFilterFieldValues(form, filterId, {
|
||||||
defaultValueQueriesData: [],
|
defaultValueQueriesData: [],
|
||||||
defaultValue: resolvedDefaultValue,
|
defaultValue: resolvedDefaultValue,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!formFilter?.dataset?.value) {
|
||||||
|
// no need to make chart data request if no dataset is defined
|
||||||
|
return;
|
||||||
|
}
|
||||||
const formData = getFormData({
|
const formData = getFormData({
|
||||||
datasetId: formFilter?.dataset?.value,
|
datasetId: formFilter?.dataset?.value,
|
||||||
groupby: formFilter?.column,
|
groupby: formFilter?.column,
|
||||||
|
@ -63,7 +68,8 @@ export const useBackendFormUpdate = (
|
||||||
if (
|
if (
|
||||||
filterToEdit?.filterType === formFilter?.filterType &&
|
filterToEdit?.filterType === formFilter?.filterType &&
|
||||||
filterToEdit?.targets[0].datasetId === formFilter?.dataset?.value &&
|
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;
|
resolvedDefaultValue = filterToEdit?.defaultValue;
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
import { FormInstance } from 'antd/lib/form';
|
import { FormInstance } from 'antd/lib/form';
|
||||||
import shortid from 'shortid';
|
import shortid from 'shortid';
|
||||||
import { FilterRemoval, NativeFiltersForm } from './types';
|
import { FilterRemoval, NativeFiltersForm } from './types';
|
||||||
import { Filter, FilterConfiguration } from '../types';
|
import { Filter, FilterConfiguration, Target } from '../types';
|
||||||
|
|
||||||
export const REMOVAL_DELAY_SECS = 5;
|
export const REMOVAL_DELAY_SECS = 5;
|
||||||
|
|
||||||
|
@ -132,14 +132,12 @@ export const createHandleSave = (
|
||||||
const formInputs = values.filters[id];
|
const formInputs = values.filters[id];
|
||||||
// if user didn't open a filter, return the original config
|
// if user didn't open a filter, return the original config
|
||||||
if (!formInputs) return filterConfigMap[id];
|
if (!formInputs) return filterConfigMap[id];
|
||||||
let target = {};
|
const target: Partial<Target> = {};
|
||||||
|
if (formInputs.dataset) {
|
||||||
|
target.datasetId = formInputs.dataset.value;
|
||||||
|
}
|
||||||
if (formInputs.dataset && formInputs.column) {
|
if (formInputs.dataset && formInputs.column) {
|
||||||
target = {
|
target.column = { name: formInputs.column };
|
||||||
datasetId: formInputs.dataset.value,
|
|
||||||
column: {
|
|
||||||
name: formInputs.column,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
|
|
|
@ -37,10 +37,12 @@ export interface Target {
|
||||||
// clarityColumns?: Column[];
|
// clarityColumns?: Column[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type FilterValue = string | number | (string | number)[] | null;
|
||||||
|
|
||||||
export interface Filter {
|
export interface Filter {
|
||||||
cascadeParentIds: string[];
|
cascadeParentIds: string[];
|
||||||
defaultValue: any;
|
defaultValue: FilterValue;
|
||||||
currentValue?: any;
|
currentValue?: FilterValue;
|
||||||
isInstant: boolean;
|
isInstant: boolean;
|
||||||
id: string; // randomly generated at filter creation
|
id: string; // randomly generated at filter creation
|
||||||
name: string;
|
name: string;
|
||||||
|
|
|
@ -44,12 +44,12 @@ export const getFormData = ({
|
||||||
cascadingFilters?: object;
|
cascadingFilters?: object;
|
||||||
groupby?: string;
|
groupby?: string;
|
||||||
}): Partial<QueryFormData> => {
|
}): Partial<QueryFormData> => {
|
||||||
let otherProps: { datasource?: string; groupby?: string[] } = {};
|
const otherProps: { datasource?: string; groupby?: string[] } = {};
|
||||||
if (datasetId && groupby) {
|
if (datasetId) {
|
||||||
otherProps = {
|
otherProps.datasource = `${datasetId}__table`;
|
||||||
datasource: `${datasetId}__table`,
|
}
|
||||||
groupby: [groupby],
|
if (groupby) {
|
||||||
};
|
otherProps.groupby = [groupby];
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...controlValues,
|
...controlValues,
|
||||||
|
|
|
@ -16,18 +16,13 @@
|
||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* 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 React, { useEffect, useState } from 'react';
|
||||||
import { Slider } from 'src/common/components';
|
import { Slider } from 'src/common/components';
|
||||||
import { PluginFilterRangeProps } from './types';
|
import { PluginFilterRangeProps } from './types';
|
||||||
import { PluginFilterStylesProps } from '../types';
|
import { Styles } from '../common';
|
||||||
import { getRangeExtraFormData } from '../../utils';
|
import { getRangeExtraFormData } from '../../utils';
|
||||||
|
|
||||||
const Styles = styled.div<PluginFilterStylesProps>`
|
|
||||||
height: ${({ height }) => height};
|
|
||||||
width: ${({ width }) => width};
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default function RangeFilterPlugin(props: PluginFilterRangeProps) {
|
export default function RangeFilterPlugin(props: PluginFilterRangeProps) {
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
|
|
|
@ -43,6 +43,7 @@ export default function buildQuery(formData: QueryFormData) {
|
||||||
return buildQueryContext(formData, baseQueryObject => [
|
return buildQueryContext(formData, baseQueryObject => [
|
||||||
{
|
{
|
||||||
...baseQueryObject,
|
...baseQueryObject,
|
||||||
|
apply_fetch_values_predicate: true,
|
||||||
columns: [],
|
columns: [],
|
||||||
groupby: [],
|
groupby: [],
|
||||||
metrics: [
|
metrics: [
|
||||||
|
|
|
@ -26,7 +26,7 @@ export default class RangeFilterPlugin extends ChartPlugin {
|
||||||
constructor() {
|
constructor() {
|
||||||
const metadata = new ChartMetadata({
|
const metadata = new ChartMetadata({
|
||||||
name: t('Range filter'),
|
name: t('Range filter'),
|
||||||
description: 'Range filter plugin using AntD',
|
description: t('Range filter plugin using AntD'),
|
||||||
behaviors: [Behavior.CROSS_FILTER, Behavior.NATIVE_FILTER],
|
behaviors: [Behavior.CROSS_FILTER, Behavior.NATIVE_FILTER],
|
||||||
thumbnail,
|
thumbnail,
|
||||||
});
|
});
|
||||||
|
|
|
@ -16,18 +16,13 @@
|
||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* 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 React, { useEffect, useState } from 'react';
|
||||||
import { Select } from 'src/common/components';
|
import { Select } from 'src/common/components';
|
||||||
import { PluginFilterSelectProps } from './types';
|
import { PluginFilterSelectProps } from './types';
|
||||||
import { PluginFilterStylesProps } from '../types';
|
import { Styles, StyledSelect } from '../common';
|
||||||
import { getSelectExtraFormData } from '../../utils';
|
import { getSelectExtraFormData } from '../../utils';
|
||||||
|
|
||||||
const Styles = styled.div<PluginFilterStylesProps>`
|
|
||||||
height: ${({ height }) => height};
|
|
||||||
width: ${({ width }) => width};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
|
|
||||||
export default function PluginFilterSelect(props: PluginFilterSelectProps) {
|
export default function PluginFilterSelect(props: PluginFilterSelectProps) {
|
||||||
|
@ -50,13 +45,9 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
|
||||||
const handleChange = (
|
const handleChange = (
|
||||||
value?: (number | string)[] | number | string | null,
|
value?: (number | string)[] | number | string | null,
|
||||||
) => {
|
) => {
|
||||||
let resultValue: (number | string)[];
|
const resultValue: (number | string)[] = ensureIsArray<number | string>(
|
||||||
// Works only with arrays even for single select
|
value,
|
||||||
if (!Array.isArray(value)) {
|
);
|
||||||
resultValue = value ? [value] : [];
|
|
||||||
} else {
|
|
||||||
resultValue = value;
|
|
||||||
}
|
|
||||||
setValues(resultValue);
|
setValues(resultValue);
|
||||||
|
|
||||||
const [col] = groupby;
|
const [col] = groupby;
|
||||||
|
@ -89,25 +80,32 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handleChange(currentValue ?? []);
|
handleChange(currentValue ?? []);
|
||||||
}, [JSON.stringify(currentValue)]);
|
}, [
|
||||||
|
JSON.stringify(currentValue),
|
||||||
|
multiSelect,
|
||||||
|
enableEmptyFilter,
|
||||||
|
inverseSelection,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handleChange(defaultValue ?? []);
|
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 =
|
const placeholderText =
|
||||||
(data || []).length === 0
|
(data || []).length === 0
|
||||||
? t('No data')
|
? t('No data')
|
||||||
: t(`%d option%s`, data.length, data.length === 1 ? '' : 's');
|
: tn('%s option', '%s options', data.length, data.length);
|
||||||
return (
|
return (
|
||||||
<Styles height={height} width={width}>
|
<Styles height={height} width={width}>
|
||||||
<Select
|
<StyledSelect
|
||||||
allowClear
|
allowClear
|
||||||
value={values}
|
value={values}
|
||||||
showSearch={showSearch}
|
showSearch={showSearch}
|
||||||
style={{ width: '100%' }}
|
|
||||||
mode={multiSelect ? 'multiple' : undefined}
|
mode={multiSelect ? 'multiple' : undefined}
|
||||||
placeholder={placeholderText}
|
placeholder={placeholderText}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -122,7 +120,7 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
|
||||||
</Option>
|
</Option>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Select>
|
</StyledSelect>
|
||||||
</Styles>
|
</Styles>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,7 @@ export default function buildQuery(formData: QueryFormData) {
|
||||||
return buildQueryContext(formData, baseQueryObject => [
|
return buildQueryContext(formData, baseQueryObject => [
|
||||||
{
|
{
|
||||||
...baseQueryObject,
|
...baseQueryObject,
|
||||||
|
apply_fetch_values_predicate: true,
|
||||||
groupby: baseQueryObject.columns,
|
groupby: baseQueryObject.columns,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -26,7 +26,7 @@ export default class FilterSelectPlugin extends ChartPlugin {
|
||||||
constructor() {
|
constructor() {
|
||||||
const metadata = new ChartMetadata({
|
const metadata = new ChartMetadata({
|
||||||
name: t('Select filter'),
|
name: t('Select filter'),
|
||||||
description: 'Select filter plugin using AntD',
|
description: t('Select filter plugin using AntD'),
|
||||||
behaviors: [Behavior.CROSS_FILTER, Behavior.NATIVE_FILTER],
|
behaviors: [Behavior.CROSS_FILTER, Behavior.NATIVE_FILTER],
|
||||||
thumbnail,
|
thumbnail,
|
||||||
});
|
});
|
||||||
|
|
|
@ -19,14 +19,12 @@
|
||||||
import { styled, DataMask, Behavior } from '@superset-ui/core';
|
import { styled, DataMask, Behavior } from '@superset-ui/core';
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import DateFilterControl from 'src/explore/components/controls/DateFilterControl';
|
import DateFilterControl from 'src/explore/components/controls/DateFilterControl';
|
||||||
import { PluginFilterStylesProps } from '../types';
|
|
||||||
import { PluginFilterTimeProps } from './types';
|
import { PluginFilterTimeProps } from './types';
|
||||||
|
import { Styles } from '../common';
|
||||||
|
|
||||||
const DEFAULT_VALUE = 'Last week';
|
const DEFAULT_VALUE = 'Last week';
|
||||||
|
|
||||||
const Styles = styled.div<PluginFilterStylesProps>`
|
const TimeFilterStyles = styled(Styles)`
|
||||||
height: ${({ height }) => height}px;
|
|
||||||
width: ${({ width }) => width}px;
|
|
||||||
overflow-x: scroll;
|
overflow-x: scroll;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -69,12 +67,12 @@ export default function TimeFilterPlugin(props: PluginFilterTimeProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
<Styles width={width}>
|
<TimeFilterStyles width={width}>
|
||||||
<DateFilterControl
|
<DateFilterControl
|
||||||
value={value}
|
value={value}
|
||||||
name="time_range"
|
name="time_range"
|
||||||
onChange={handleTimeRangeChange}
|
onChange={handleTimeRangeChange}
|
||||||
/>
|
/>
|
||||||
</Styles>
|
</TimeFilterStyles>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@ export default class TimeFilterPlugin extends ChartPlugin {
|
||||||
constructor() {
|
constructor() {
|
||||||
const metadata = new ChartMetadata({
|
const metadata = new ChartMetadata({
|
||||||
name: t('Time filter'),
|
name: t('Time filter'),
|
||||||
description: 'Custom time filter plugin',
|
description: t('Custom time filter plugin'),
|
||||||
behaviors: [Behavior.CROSS_FILTER, Behavior.NATIVE_FILTER],
|
behaviors: [Behavior.CROSS_FILTER, Behavior.NATIVE_FILTER],
|
||||||
thumbnail,
|
thumbnail,
|
||||||
datasourceCount: 0,
|
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 SelectFilterPlugin } from './Select';
|
||||||
export { default as RangeFilterPlugin } from './Range';
|
export { default as RangeFilterPlugin } from './Range';
|
||||||
export { default as TimeFilterPlugin } from './Time';
|
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
|
append_form_data: emptyFilter
|
||||||
? {
|
? {
|
||||||
extras: {
|
adhoc_filters: [
|
||||||
where: '1 = 0',
|
{
|
||||||
|
expressionType: 'SQL',
|
||||||
|
clause: 'WHERE',
|
||||||
|
sqlExpression: '1 = 0',
|
||||||
},
|
},
|
||||||
|
],
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
filters:
|
filters:
|
||||||
|
|
|
@ -63,6 +63,8 @@ import {
|
||||||
SelectFilterPlugin,
|
SelectFilterPlugin,
|
||||||
RangeFilterPlugin,
|
RangeFilterPlugin,
|
||||||
TimeFilterPlugin,
|
TimeFilterPlugin,
|
||||||
|
TimeColumnFilterPlugin,
|
||||||
|
TimeGrainFilterPlugin,
|
||||||
} from 'src/filters/components/';
|
} from 'src/filters/components/';
|
||||||
import FilterBoxChartPlugin from '../FilterBox/FilterBoxChartPlugin';
|
import FilterBoxChartPlugin from '../FilterBox/FilterBoxChartPlugin';
|
||||||
import TimeTableChartPlugin from '../TimeTable/TimeTableChartPlugin';
|
import TimeTableChartPlugin from '../TimeTable/TimeTableChartPlugin';
|
||||||
|
@ -115,6 +117,8 @@ export default class MainPreset extends Preset {
|
||||||
new SelectFilterPlugin().configure({ key: 'filter_select' }),
|
new SelectFilterPlugin().configure({ key: 'filter_select' }),
|
||||||
new RangeFilterPlugin().configure({ key: 'filter_range' }),
|
new RangeFilterPlugin().configure({ key: 'filter_range' }),
|
||||||
new TimeFilterPlugin().configure({ key: 'filter_time' }),
|
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,
|
allow_none=True,
|
||||||
example={"__time_range": "1 year ago : now"},
|
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)
|
filters = fields.List(fields.Nested(ChartDataFilterSchema), allow_none=True)
|
||||||
granularity = fields.String(
|
granularity = fields.String(
|
||||||
description="Name of temporal column used for time filtering. For legacy Druid "
|
description="Name of temporal column used for time filtering. For legacy Druid "
|
||||||
|
|
|
@ -73,6 +73,7 @@ class QueryObject:
|
||||||
|
|
||||||
annotation_layers: List[Dict[str, Any]]
|
annotation_layers: List[Dict[str, Any]]
|
||||||
applied_time_extras: Dict[str, str]
|
applied_time_extras: Dict[str, str]
|
||||||
|
apply_fetch_values_predicate: bool
|
||||||
granularity: Optional[str]
|
granularity: Optional[str]
|
||||||
from_dttm: Optional[datetime]
|
from_dttm: Optional[datetime]
|
||||||
to_dttm: Optional[datetime]
|
to_dttm: Optional[datetime]
|
||||||
|
@ -100,6 +101,7 @@ class QueryObject:
|
||||||
result_type: Optional[ChartDataResultType] = None,
|
result_type: Optional[ChartDataResultType] = None,
|
||||||
annotation_layers: Optional[List[Dict[str, Any]]] = None,
|
annotation_layers: Optional[List[Dict[str, Any]]] = None,
|
||||||
applied_time_extras: Optional[Dict[str, str]] = None,
|
applied_time_extras: Optional[Dict[str, str]] = None,
|
||||||
|
apply_fetch_values_predicate: bool = False,
|
||||||
granularity: Optional[str] = None,
|
granularity: Optional[str] = None,
|
||||||
metrics: Optional[List[Union[Dict[str, Any], str]]] = None,
|
metrics: Optional[List[Union[Dict[str, Any], str]]] = None,
|
||||||
groupby: Optional[List[str]] = None,
|
groupby: Optional[List[str]] = None,
|
||||||
|
@ -127,6 +129,7 @@ class QueryObject:
|
||||||
)
|
)
|
||||||
self.result_type = result_type
|
self.result_type = result_type
|
||||||
annotation_layers = annotation_layers or []
|
annotation_layers = annotation_layers or []
|
||||||
|
self.apply_fetch_values_predicate = apply_fetch_values_predicate or False
|
||||||
metrics = metrics or []
|
metrics = metrics or []
|
||||||
columns = columns or []
|
columns = columns or []
|
||||||
groupby = groupby or []
|
groupby = groupby or []
|
||||||
|
@ -262,6 +265,7 @@ class QueryObject:
|
||||||
|
|
||||||
def to_dict(self) -> Dict[str, Any]:
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
query_object_dict = {
|
query_object_dict = {
|
||||||
|
"apply_fetch_values_predicate": self.apply_fetch_values_predicate,
|
||||||
"granularity": self.granularity,
|
"granularity": self.granularity,
|
||||||
"groupby": self.groupby,
|
"groupby": self.groupby,
|
||||||
"from_dttm": self.from_dttm,
|
"from_dttm": self.from_dttm,
|
||||||
|
@ -291,18 +295,24 @@ class QueryObject:
|
||||||
"""
|
"""
|
||||||
cache_dict = self.to_dict()
|
cache_dict = self.to_dict()
|
||||||
cache_dict.update(extra)
|
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:
|
if self.datasource:
|
||||||
cache_dict["datasource"] = self.datasource.uid
|
cache_dict["datasource"] = self.datasource.uid
|
||||||
if self.result_type:
|
if self.result_type:
|
||||||
cache_dict["result_type"] = 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:
|
if self.time_range:
|
||||||
cache_dict["time_range"] = self.time_range
|
cache_dict["time_range"] = self.time_range
|
||||||
if self.post_processing:
|
if self.post_processing:
|
||||||
cache_dict["post_processing"] = self.post_processing
|
cache_dict["post_processing"] = self.post_processing
|
||||||
|
|
||||||
|
for k in ["from_dttm", "to_dttm"]:
|
||||||
|
del cache_dict[k]
|
||||||
|
|
||||||
annotation_fields = [
|
annotation_fields = [
|
||||||
"annotationType",
|
"annotationType",
|
||||||
"descriptionColumns",
|
"descriptionColumns",
|
||||||
|
|
|
@ -1139,11 +1139,17 @@ class DruidDatasource(Model, BaseDatasource):
|
||||||
client: Optional["PyDruid"] = None,
|
client: Optional["PyDruid"] = None,
|
||||||
order_desc: bool = True,
|
order_desc: bool = True,
|
||||||
is_rowcount: bool = False,
|
is_rowcount: bool = False,
|
||||||
|
apply_fetch_values_predicate: bool = False,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Runs a query against Druid and returns a dataframe."""
|
"""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:
|
if is_rowcount:
|
||||||
raise SupersetException("is_rowcount is not supported on Druid connector")
|
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
|
# TODO refactor into using a TBD Query object
|
||||||
client = client or self.cluster.get_pydruid_client()
|
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.orm import backref, Query, relationship, RelationshipProperty, Session
|
||||||
from sqlalchemy.schema import UniqueConstraint
|
from sqlalchemy.schema import UniqueConstraint
|
||||||
from sqlalchemy.sql import column, ColumnElement, literal_column, table, text
|
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 sqlalchemy.types import TypeEngine
|
||||||
|
|
||||||
from superset import app, db, is_feature_enabled, security_manager
|
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):
|
except (TypeError, json.JSONDecodeError):
|
||||||
return {}
|
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]:
|
def values_for_column(self, column_name: str, limit: int = 10000) -> List[Any]:
|
||||||
"""Runs query against sqla to retrieve some
|
"""Runs query against sqla to retrieve some
|
||||||
sample values for the given column.
|
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)
|
qry = qry.limit(limit)
|
||||||
|
|
||||||
if self.fetch_values_predicate:
|
if self.fetch_values_predicate:
|
||||||
tp = self.get_template_processor()
|
qry = qry.where(self.get_fetch_values_predicate())
|
||||||
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,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
engine = self.database.get_sqla_engine()
|
engine = self.database.get_sqla_engine()
|
||||||
sql = "{}".format(qry.compile(engine, compile_kwargs={"literal_binds": True}))
|
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,
|
extras: Optional[Dict[str, Any]] = None,
|
||||||
order_desc: bool = True,
|
order_desc: bool = True,
|
||||||
is_rowcount: bool = False,
|
is_rowcount: bool = False,
|
||||||
|
apply_fetch_values_predicate: bool = False,
|
||||||
) -> SqlaQuery:
|
) -> SqlaQuery:
|
||||||
"""Querying any sqla table from this common interface"""
|
"""Querying any sqla table from this common interface"""
|
||||||
template_kwargs = {
|
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))]
|
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:
|
if granularity:
|
||||||
qry = qry.where(and_(*(time_filters + where_clause_and)))
|
qry = qry.where(and_(*(time_filters + where_clause_and)))
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -40,7 +40,7 @@ from typing import (
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import sqlparse
|
import sqlparse
|
||||||
from flask import g
|
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 import column, DateTime, select
|
||||||
from sqlalchemy.engine.base import Engine
|
from sqlalchemy.engine.base import Engine
|
||||||
from sqlalchemy.engine.interfaces import Compiled, Dialect
|
from sqlalchemy.engine.interfaces import Compiled, Dialect
|
||||||
|
@ -77,23 +77,23 @@ QueryStatus = utils.QueryStatus
|
||||||
config = app.config
|
config = app.config
|
||||||
|
|
||||||
builtin_time_grains: Dict[Optional[str], str] = {
|
builtin_time_grains: Dict[Optional[str], str] = {
|
||||||
None: "Time Column",
|
None: __("Original value"),
|
||||||
"PT1S": "second",
|
"PT1S": __("Second"),
|
||||||
"PT1M": "minute",
|
"PT1M": __("Minute"),
|
||||||
"PT5M": "5 minute",
|
"PT5M": __("5 minute"),
|
||||||
"PT10M": "10 minute",
|
"PT10M": __("10 minute"),
|
||||||
"PT15M": "15 minute",
|
"PT15M": __("15 minute"),
|
||||||
"PT0.5H": "half hour",
|
"PT0.5H": __("Half hour"),
|
||||||
"PT1H": "hour",
|
"PT1H": __("Hour"),
|
||||||
"P1D": "day",
|
"P1D": __("Day"),
|
||||||
"P1W": "week",
|
"P1W": __("Week"),
|
||||||
"P1M": "month",
|
"P1M": __("Month"),
|
||||||
"P0.25Y": "quarter",
|
"P0.25Y": __("Quarter"),
|
||||||
"P1Y": "year",
|
"P1Y": __("Year"),
|
||||||
"1969-12-28T00:00:00Z/P1W": "week_start_sunday",
|
"1969-12-28T00:00:00Z/P1W": __("Week starting sunday"),
|
||||||
"1969-12-29T00:00:00Z/P1W": "week_start_monday",
|
"1969-12-29T00:00:00Z/P1W": __("Week starting monday"),
|
||||||
"P1W/1970-01-03T00:00:00Z": "week_ending_saturday",
|
"P1W/1970-01-03T00:00:00Z": __("Week ending saturday"),
|
||||||
"P1W/1970-01-04T00:00:00Z": "week_ending_sunday",
|
"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.
|
and add applied time extras to the payload.
|
||||||
"""
|
"""
|
||||||
time_extras = {
|
time_extras = {
|
||||||
|
"granularity": "__granularity",
|
||||||
|
"granularity_sqla": "__granularity",
|
||||||
"time_range": "__time_range",
|
"time_range": "__time_range",
|
||||||
"granularity_sqla": "__time_col",
|
}
|
||||||
|
allowed_extra_overrides: Dict[str, Optional[str]] = {
|
||||||
"time_grain_sqla": "__time_grain",
|
"time_grain_sqla": "__time_grain",
|
||||||
"druid_time_origin": "__time_origin",
|
"druid_time_origin": "__time_origin",
|
||||||
"granularity": "__granularity",
|
"time_range_endpoints": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
applied_time_extras = form_data.get("applied_time_extras", {})
|
applied_time_extras = form_data.get("applied_time_extras", {})
|
||||||
form_data["applied_time_extras"] = applied_time_extras
|
form_data["applied_time_extras"] = applied_time_extras
|
||||||
extra_form_data = form_data.pop("extra_form_data", {})
|
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)
|
time_extra = time_extras.get(key)
|
||||||
if time_extra:
|
if time_extra:
|
||||||
applied_time_extras[time_extra] = value
|
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", [])
|
adhoc_filters = form_data.get("adhoc_filters", [])
|
||||||
form_data["adhoc_filters"] = 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:
|
if append_filters:
|
||||||
adhoc_filters.extend(
|
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")
|
self.login(username="admin")
|
||||||
slc = self.get_slice("Girls", db.session)
|
slc = self.get_slice("Girls", db.session)
|
||||||
resp = self.get_resp("/superset/slice/{}/".format(slc.id))
|
resp = self.get_resp("/superset/slice/{}/".format(slc.id))
|
||||||
assert "Time Column" in resp
|
assert "Original value" in resp
|
||||||
assert "List Roles" in resp
|
assert "List Roles" in resp
|
||||||
|
|
||||||
# Testing overrides
|
# Testing overrides
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
"""Utils to provide dashboards for tests"""
|
"""Utils to provide dashboards for tests"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
@ -34,6 +34,7 @@ def create_table_for_dashboard(
|
||||||
database: Database,
|
database: Database,
|
||||||
dtype: Dict[str, Any],
|
dtype: Dict[str, Any],
|
||||||
table_description: str = "",
|
table_description: str = "",
|
||||||
|
fetch_values_predicate: Optional[str] = None,
|
||||||
) -> SqlaTable:
|
) -> SqlaTable:
|
||||||
df.to_sql(
|
df.to_sql(
|
||||||
table_name,
|
table_name,
|
||||||
|
@ -51,6 +52,8 @@ def create_table_for_dashboard(
|
||||||
)
|
)
|
||||||
if not table:
|
if not table:
|
||||||
table = table_source(table_name=table_name)
|
table = table_source(table_name=table_name)
|
||||||
|
if fetch_values_predicate:
|
||||||
|
table.fetch_values_predicate = fetch_values_predicate
|
||||||
table.database = database
|
table.database = database
|
||||||
table.description = table_description
|
table.description = table_description
|
||||||
db.session.merge(table)
|
db.session.merge(table)
|
||||||
|
|
|
@ -225,7 +225,7 @@ class TestExportDatabasesCommand(SupersetTestCase):
|
||||||
"default_endpoint": None,
|
"default_endpoint": None,
|
||||||
"description": "",
|
"description": "",
|
||||||
"extra": None,
|
"extra": None,
|
||||||
"fetch_values_predicate": None,
|
"fetch_values_predicate": "123 = 123",
|
||||||
"filter_select_enabled": True,
|
"filter_select_enabled": True,
|
||||||
"main_dttm_col": "ds",
|
"main_dttm_col": "ds",
|
||||||
"metrics": [
|
"metrics": [
|
||||||
|
|
|
@ -18,7 +18,7 @@ import json
|
||||||
import string
|
import string
|
||||||
from datetime import date, datetime
|
from datetime import date, datetime
|
||||||
from random import choice, getrandbits, randint, random, uniform
|
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 pandas as pd
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -31,7 +31,7 @@ from superset.models.core import Database
|
||||||
from superset.models.dashboard import Dashboard
|
from superset.models.dashboard import Dashboard
|
||||||
from superset.models.slice import Slice
|
from superset.models.slice import Slice
|
||||||
from superset.utils.core import get_example_database
|
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
|
from tests.test_app import app
|
||||||
|
|
||||||
|
|
||||||
|
@ -63,7 +63,13 @@ def _load_data():
|
||||||
"state": String(10),
|
"state": String(10),
|
||||||
"name": String(255),
|
"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
|
from superset.examples.birth_names import create_slices, create_dashboard
|
||||||
|
|
||||||
|
@ -75,9 +81,19 @@ def _load_data():
|
||||||
|
|
||||||
|
|
||||||
def _create_table(
|
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
|
from superset.examples.birth_names import _add_table_metrics, _set_table_metadata
|
||||||
|
|
||||||
_set_table_metadata(table, database)
|
_set_table_metadata(table, database)
|
||||||
|
|
|
@ -302,11 +302,44 @@ class TestQueryContext(SupersetTestCase):
|
||||||
payload["result_type"] = ChartDataResultType.QUERY.value
|
payload["result_type"] = ChartDataResultType.QUERY.value
|
||||||
query_context = ChartDataQueryContextSchema().load(payload)
|
query_context = ChartDataQueryContextSchema().load(payload)
|
||||||
responses = query_context.get_payload()
|
responses = query_context.get_payload()
|
||||||
self.assertEqual(len(responses), 1)
|
assert len(responses) == 1
|
||||||
response = responses["queries"][0]
|
response = responses["queries"][0]
|
||||||
self.assertEqual(len(response), 2)
|
assert len(response) == 2
|
||||||
self.assertEqual(response["language"], "sql")
|
assert response["language"] == "sql"
|
||||||
self.assertIn("SELECT", response["query"])
|
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):
|
def test_query_object_unknown_fields(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -924,15 +924,38 @@ class TestUtils(SupersetTestCase):
|
||||||
"time_range": "Last 10 days",
|
"time_range": "Last 10 days",
|
||||||
"extra_form_data": {
|
"extra_form_data": {
|
||||||
"append_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",},
|
"override_form_data": {"time_range": "Last 100 years",},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
merge_extra_form_data(form_data)
|
merge_extra_form_data(form_data)
|
||||||
assert form_data["applied_time_extras"] == {"__time_range": "Last 100 years"}
|
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 form_data["time_range"] == "Last 100 years"
|
||||||
assert len(form_data["adhoc_filters"]) == 1
|
|
||||||
|
|
||||||
def test_ssl_certificate_parse(self):
|
def test_ssl_certificate_parse(self):
|
||||||
parsed_certificate = parse_ssl_cert(ssl_certificate)
|
parsed_certificate = parse_ssl_cert(ssl_certificate)
|
||||||
|
|
Loading…
Reference in New Issue