mirror of
https://github.com/apache/superset.git
synced 2024-09-12 00:29:39 -04:00
fix(explore): Explore page boolean filter is broken for Presto DB (#14952)
* Front end update - modify OPERATORS, to have SQL operation and display value * Updated tests * More tests * Remove OPERATOR imports * Fix break tests * PR comments * fix issue with comparator loading * rename a variable * Linting
This commit is contained in:
parent
c0eff8faf6
commit
f8b270d419
@ -20,6 +20,11 @@ import React from 'react';
|
||||
import { styled } from '@superset-ui/core';
|
||||
import Select, { SelectProps } from 'antd/lib/select';
|
||||
|
||||
export {
|
||||
OptionType as NativeSelectOptionType,
|
||||
SelectProps as NativeSelectProps,
|
||||
} from 'antd/lib/select';
|
||||
|
||||
const StyledNativeSelect = styled((props: SelectProps<any>) => (
|
||||
<Select getPopupContainer={(trigger: any) => trigger.parentNode} {...props} />
|
||||
))`
|
||||
|
@ -20,7 +20,10 @@ import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { logging, SupersetClient, t, Metric } from '@superset-ui/core';
|
||||
import { ColumnMeta } from '@superset-ui/chart-controls';
|
||||
import { Tooltip } from 'src/components/Tooltip';
|
||||
import { OPERATORS } from 'src/explore/constants';
|
||||
import {
|
||||
Operators,
|
||||
OPERATOR_ENUM_TO_OPERATOR_TYPE,
|
||||
} from 'src/explore/constants';
|
||||
import { OptionSortType } from 'src/explore/types';
|
||||
import {
|
||||
DndFilterSelectProps,
|
||||
@ -191,7 +194,9 @@ export const DndFilterSelect = (props: DndFilterSelectProps) => {
|
||||
props.datasource.type === 'druid'
|
||||
? filterOptions.saved_metric_name
|
||||
: getMetricExpression(filterOptions.saved_metric_name),
|
||||
operator: OPERATORS['>'],
|
||||
operator:
|
||||
OPERATOR_ENUM_TO_OPERATOR_TYPE[Operators.GREATER_THAN].operation,
|
||||
operatorId: Operators.GREATER_THAN,
|
||||
comparator: 0,
|
||||
clause: CLAUSES.HAVING,
|
||||
});
|
||||
@ -207,7 +212,9 @@ export const DndFilterSelect = (props: DndFilterSelectProps) => {
|
||||
props.datasource.type === 'druid'
|
||||
? filterOptions.label
|
||||
: new AdhocMetric(option).translateToSql(),
|
||||
operator: OPERATORS['>'],
|
||||
operator:
|
||||
OPERATOR_ENUM_TO_OPERATOR_TYPE[Operators.GREATER_THAN].operation,
|
||||
operatorId: Operators.GREATER_THAN,
|
||||
comparator: 0,
|
||||
clause: CLAUSES.HAVING,
|
||||
});
|
||||
@ -217,7 +224,8 @@ export const DndFilterSelect = (props: DndFilterSelectProps) => {
|
||||
return new AdhocFilter({
|
||||
expressionType: EXPRESSION_TYPES.SIMPLE,
|
||||
subject: filterOptions.column_name,
|
||||
operator: OPERATORS['=='],
|
||||
operator: OPERATOR_ENUM_TO_OPERATOR_TYPE[Operators.EQUALS].operation,
|
||||
operatorId: Operators.EQUALS,
|
||||
comparator: '',
|
||||
clause: CLAUSES.WHERE,
|
||||
isNew: true,
|
||||
|
@ -20,6 +20,7 @@ import AdhocFilter, {
|
||||
EXPRESSION_TYPES,
|
||||
CLAUSES,
|
||||
} from 'src/explore/components/controls/FilterControl/AdhocFilter';
|
||||
import { Operators } from 'src/explore/constants';
|
||||
|
||||
describe('AdhocFilter', () => {
|
||||
it('sets filterOptionName in constructor', () => {
|
||||
@ -188,6 +189,22 @@ describe('AdhocFilter', () => {
|
||||
});
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
expect(adhocFilter8.isValid()).toBe(false);
|
||||
|
||||
const adhocFilter9 = new AdhocFilter({
|
||||
expressionType: EXPRESSION_TYPES.SIMPLE,
|
||||
subject: 'value',
|
||||
operator: 'IS NULL',
|
||||
clause: CLAUSES.WHERE,
|
||||
});
|
||||
expect(adhocFilter9.isValid()).toBe(true);
|
||||
const adhocFilter10 = new AdhocFilter({
|
||||
expressionType: EXPRESSION_TYPES.SIMPLE,
|
||||
subject: 'value',
|
||||
operator: 'IS NOT NULL',
|
||||
clause: CLAUSES.WHERE,
|
||||
});
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
expect(adhocFilter10.isValid()).toBe(true);
|
||||
});
|
||||
|
||||
it('can translate from simple expressions to sql expressions', () => {
|
||||
@ -209,4 +226,26 @@ describe('AdhocFilter', () => {
|
||||
});
|
||||
expect(adhocFilter2.translateToSql()).toBe('SUM(value) <> 5');
|
||||
});
|
||||
it('sets comparator to null when operator is IS_NULL', () => {
|
||||
const adhocFilter2 = new AdhocFilter({
|
||||
expressionType: EXPRESSION_TYPES.SIMPLE,
|
||||
subject: 'SUM(value)',
|
||||
operator: 'IS NULL',
|
||||
operatorId: Operators.IS_NULL,
|
||||
comparator: '5',
|
||||
clause: CLAUSES.HAVING,
|
||||
});
|
||||
expect(adhocFilter2.comparator).toBe(null);
|
||||
});
|
||||
it('sets comparator to null when operator is IS_NOT_NULL', () => {
|
||||
const adhocFilter2 = new AdhocFilter({
|
||||
expressionType: EXPRESSION_TYPES.SIMPLE,
|
||||
subject: 'SUM(value)',
|
||||
operator: 'IS NOT NULL',
|
||||
operatorId: Operators.IS_NOT_NULL,
|
||||
comparator: '5',
|
||||
clause: CLAUSES.HAVING,
|
||||
});
|
||||
expect(adhocFilter2.comparator).toBe(null);
|
||||
});
|
||||
});
|
||||
|
@ -16,7 +16,11 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { CUSTOM_OPERATORS, OPERATORS } from 'src/explore/constants';
|
||||
import {
|
||||
CUSTOM_OPERATORS,
|
||||
Operators,
|
||||
OPERATOR_ENUM_TO_OPERATOR_TYPE,
|
||||
} from 'src/explore/constants';
|
||||
import { getSimpleSQLExpression } from 'src/explore/exploreUtils';
|
||||
|
||||
export const EXPRESSION_TYPES = {
|
||||
@ -49,11 +53,16 @@ const OPERATORS_TO_SQL = {
|
||||
`= '{{ presto.latest_partition('${datasource.schema}.${datasource.datasource_name}') }}'`,
|
||||
};
|
||||
|
||||
const CUSTOM_OPERATIONS = [...CUSTOM_OPERATORS].map(
|
||||
op => OPERATOR_ENUM_TO_OPERATOR_TYPE[op].operation,
|
||||
);
|
||||
|
||||
function translateToSql(adhocMetric, { useSimple } = {}) {
|
||||
if (adhocMetric.expressionType === EXPRESSION_TYPES.SIMPLE || useSimple) {
|
||||
const { subject, comparator } = adhocMetric;
|
||||
const operator =
|
||||
adhocMetric.operator && CUSTOM_OPERATORS.has(adhocMetric.operator)
|
||||
adhocMetric.operator &&
|
||||
CUSTOM_OPERATIONS.indexOf(adhocMetric.operator) >= 0
|
||||
? OPERATORS_TO_SQL[adhocMetric.operator](adhocMetric)
|
||||
: OPERATORS_TO_SQL[adhocMetric.operator];
|
||||
return getSimpleSQLExpression(subject, operator, comparator);
|
||||
@ -70,7 +79,22 @@ export default class AdhocFilter {
|
||||
if (this.expressionType === EXPRESSION_TYPES.SIMPLE) {
|
||||
this.subject = adhocFilter.subject;
|
||||
this.operator = adhocFilter.operator?.toUpperCase();
|
||||
this.operatorId = adhocFilter.operatorId;
|
||||
this.comparator = adhocFilter.comparator;
|
||||
if (
|
||||
[Operators.IS_TRUE, Operators.IS_FALSE].indexOf(
|
||||
adhocFilter.operatorId,
|
||||
) >= 0
|
||||
) {
|
||||
this.comparator = adhocFilter.operatorId === Operators.IS_TRUE;
|
||||
}
|
||||
if (
|
||||
[Operators.IS_NULL, Operators.IS_NOT_NULL].indexOf(
|
||||
adhocFilter.operatorId,
|
||||
) >= 0
|
||||
) {
|
||||
this.comparator = null;
|
||||
}
|
||||
this.clause = adhocFilter.clause || CLAUSES.WHERE;
|
||||
this.sqlExpression = null;
|
||||
} else if (this.expressionType === EXPRESSION_TYPES.SQL) {
|
||||
@ -79,9 +103,13 @@ export default class AdhocFilter {
|
||||
? adhocFilter.sqlExpression
|
||||
: translateToSql(adhocFilter, { useSimple: true });
|
||||
this.clause = adhocFilter.clause;
|
||||
if (adhocFilter.operator && CUSTOM_OPERATORS.has(adhocFilter.operator)) {
|
||||
if (
|
||||
adhocFilter.operator &&
|
||||
CUSTOM_OPERATIONS.indexOf(adhocFilter.operator) >= 0
|
||||
) {
|
||||
this.subject = adhocFilter.subject;
|
||||
this.operator = adhocFilter.operator;
|
||||
this.operatorId = adhocFilter.operatorId;
|
||||
} else {
|
||||
this.subject = null;
|
||||
this.operator = null;
|
||||
@ -112,24 +140,26 @@ export default class AdhocFilter {
|
||||
adhocFilter.expressionType === this.expressionType &&
|
||||
adhocFilter.sqlExpression === this.sqlExpression &&
|
||||
adhocFilter.operator === this.operator &&
|
||||
adhocFilter.operatorId === this.operatorId &&
|
||||
adhocFilter.comparator === this.comparator &&
|
||||
adhocFilter.subject === this.subject
|
||||
);
|
||||
}
|
||||
|
||||
isValid() {
|
||||
const nullCheckOperators = [Operators.IS_NOT_NULL, Operators.IS_NULL].map(
|
||||
op => OPERATOR_ENUM_TO_OPERATOR_TYPE[op].operation,
|
||||
);
|
||||
const truthCheckOperators = [Operators.IS_TRUE, Operators.IS_FALSE].map(
|
||||
op => OPERATOR_ENUM_TO_OPERATOR_TYPE[op].operation,
|
||||
);
|
||||
if (this.expressionType === EXPRESSION_TYPES.SIMPLE) {
|
||||
if (
|
||||
[
|
||||
OPERATORS['IS TRUE'],
|
||||
OPERATORS['IS FALSE'],
|
||||
OPERATORS['IS NULL'],
|
||||
OPERATORS['IS NOT NULL'],
|
||||
].indexOf(this.operator) >= 0
|
||||
) {
|
||||
if (nullCheckOperators.indexOf(this.operator) >= 0) {
|
||||
return !!(this.operator && this.subject);
|
||||
}
|
||||
|
||||
if (truthCheckOperators.indexOf(this.operator) >= 0) {
|
||||
return !!(this.subject && this.comparator !== null);
|
||||
}
|
||||
if (this.operator && this.subject && this.clause) {
|
||||
if (Array.isArray(this.comparator)) {
|
||||
if (this.comparator.length > 0) {
|
||||
|
@ -27,14 +27,18 @@ import AdhocFilter, {
|
||||
CLAUSES,
|
||||
} from 'src/explore/components/controls/FilterControl/AdhocFilter';
|
||||
import { LabelsContainer } from 'src/explore/components/controls/OptionControls';
|
||||
import { AGGREGATES, OPERATORS } from 'src/explore/constants';
|
||||
import {
|
||||
AGGREGATES,
|
||||
Operators,
|
||||
OPERATOR_ENUM_TO_OPERATOR_TYPE,
|
||||
} from 'src/explore/constants';
|
||||
import AdhocMetric from 'src/explore/components/controls/MetricControl/AdhocMetric';
|
||||
import AdhocFilterControl from '.';
|
||||
|
||||
const simpleAdhocFilter = new AdhocFilter({
|
||||
expressionType: EXPRESSION_TYPES.SIMPLE,
|
||||
subject: 'value',
|
||||
operator: '>',
|
||||
operator: OPERATOR_ENUM_TO_OPERATOR_TYPE[Operators.GREATER_THAN].operation,
|
||||
comparator: '10',
|
||||
clause: CLAUSES.WHERE,
|
||||
});
|
||||
@ -92,7 +96,8 @@ describe('AdhocFilterControl', () => {
|
||||
new AdhocFilter({
|
||||
expressionType: EXPRESSION_TYPES.SQL,
|
||||
subject: savedMetric.expression,
|
||||
operator: OPERATORS['>'],
|
||||
operator:
|
||||
OPERATOR_ENUM_TO_OPERATOR_TYPE[Operators.GREATER_THAN].operation,
|
||||
comparator: 0,
|
||||
clause: CLAUSES.HAVING,
|
||||
}),
|
||||
@ -111,7 +116,8 @@ describe('AdhocFilterControl', () => {
|
||||
new AdhocFilter({
|
||||
expressionType: EXPRESSION_TYPES.SQL,
|
||||
subject: sumValueAdhocMetric.label,
|
||||
operator: OPERATORS['>'],
|
||||
operator:
|
||||
OPERATOR_ENUM_TO_OPERATOR_TYPE[Operators.GREATER_THAN].operation,
|
||||
comparator: 0,
|
||||
clause: CLAUSES.HAVING,
|
||||
}),
|
||||
@ -134,7 +140,7 @@ describe('AdhocFilterControl', () => {
|
||||
new AdhocFilter({
|
||||
expressionType: EXPRESSION_TYPES.SIMPLE,
|
||||
subject: columns[0].column_name,
|
||||
operator: OPERATORS['=='],
|
||||
operator: OPERATOR_ENUM_TO_OPERATOR_TYPE[Operators.EQUALS].operation,
|
||||
comparator: '',
|
||||
clause: CLAUSES.WHERE,
|
||||
}),
|
||||
|
@ -30,7 +30,10 @@ import ControlHeader from 'src/explore/components/ControlHeader';
|
||||
import adhocMetricType from 'src/explore/components/controls/MetricControl/adhocMetricType';
|
||||
import savedMetricType from 'src/explore/components/controls/MetricControl/savedMetricType';
|
||||
import AdhocMetric from 'src/explore/components/controls/MetricControl/AdhocMetric';
|
||||
import { OPERATORS } from 'src/explore/constants';
|
||||
import {
|
||||
Operators,
|
||||
OPERATOR_ENUM_TO_OPERATOR_TYPE,
|
||||
} from 'src/explore/constants';
|
||||
import FilterDefinitionOption from 'src/explore/components/controls/MetricControl/FilterDefinitionOption';
|
||||
import {
|
||||
AddControlLabel,
|
||||
@ -242,7 +245,8 @@ class AdhocFilterControl extends React.Component {
|
||||
this.props.datasource.type === 'druid'
|
||||
? option.saved_metric_name
|
||||
: this.getMetricExpression(option.saved_metric_name),
|
||||
operator: OPERATORS['>'],
|
||||
operator:
|
||||
OPERATOR_ENUM_TO_OPERATOR_TYPE[Operators.GREATER_THAN].operation,
|
||||
comparator: 0,
|
||||
clause: CLAUSES.HAVING,
|
||||
});
|
||||
@ -258,7 +262,8 @@ class AdhocFilterControl extends React.Component {
|
||||
this.props.datasource.type === 'druid'
|
||||
? option.label
|
||||
: new AdhocMetric(option).translateToSql(),
|
||||
operator: OPERATORS['>'],
|
||||
operator:
|
||||
OPERATOR_ENUM_TO_OPERATOR_TYPE[Operators.GREATER_THAN].operation,
|
||||
comparator: 0,
|
||||
clause: CLAUSES.HAVING,
|
||||
});
|
||||
@ -268,7 +273,7 @@ class AdhocFilterControl extends React.Component {
|
||||
return new AdhocFilter({
|
||||
expressionType: EXPRESSION_TYPES.SIMPLE,
|
||||
subject: option.column_name,
|
||||
operator: OPERATORS['=='],
|
||||
operator: OPERATOR_ENUM_TO_OPERATOR_TYPE[Operators.EQUALS].operation,
|
||||
comparator: '',
|
||||
clause: CLAUSES.WHERE,
|
||||
isNew: true,
|
||||
|
@ -1,277 +0,0 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
/* eslint-disable no-unused-expressions */
|
||||
import React from 'react';
|
||||
import sinon from 'sinon';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import AdhocFilter, {
|
||||
EXPRESSION_TYPES,
|
||||
CLAUSES,
|
||||
} from 'src/explore/components/controls/FilterControl/AdhocFilter';
|
||||
import { AGGREGATES } from 'src/explore/constants';
|
||||
import AdhocMetric from 'src/explore/components/controls/MetricControl/AdhocMetric';
|
||||
import AdhocFilterEditPopoverSimpleTabContent from '.';
|
||||
|
||||
const simpleAdhocFilter = new AdhocFilter({
|
||||
expressionType: EXPRESSION_TYPES.SIMPLE,
|
||||
subject: 'value',
|
||||
operator: '>',
|
||||
comparator: '10',
|
||||
clause: CLAUSES.WHERE,
|
||||
});
|
||||
|
||||
const simpleMultiAdhocFilter = new AdhocFilter({
|
||||
expressionType: EXPRESSION_TYPES.SIMPLE,
|
||||
subject: 'value',
|
||||
operator: 'IN',
|
||||
comparator: ['10'],
|
||||
clause: CLAUSES.WHERE,
|
||||
});
|
||||
|
||||
const sumValueAdhocMetric = new AdhocMetric({
|
||||
expressionType: EXPRESSION_TYPES.SIMPLE,
|
||||
column: { type: 'VARCHAR(255)', column_name: 'source' },
|
||||
aggregate: AGGREGATES.SUM,
|
||||
});
|
||||
|
||||
const simpleCustomFilter = new AdhocFilter({
|
||||
expressionType: EXPRESSION_TYPES.SIMPLE,
|
||||
subject: 'ds',
|
||||
operator: 'LATEST PARTITION',
|
||||
});
|
||||
|
||||
const options = [
|
||||
{ type: 'VARCHAR(255)', column_name: 'source', id: 1 },
|
||||
{ type: 'VARCHAR(255)', column_name: 'target', id: 2 },
|
||||
{ type: 'DOUBLE', column_name: 'value', id: 3 },
|
||||
{ saved_metric_name: 'my_custom_metric', id: 4 },
|
||||
sumValueAdhocMetric,
|
||||
];
|
||||
|
||||
function setup(overrides) {
|
||||
const onChange = sinon.spy();
|
||||
const onHeightChange = sinon.spy();
|
||||
const props = {
|
||||
adhocFilter: simpleAdhocFilter,
|
||||
onChange,
|
||||
onHeightChange,
|
||||
options,
|
||||
datasource: {},
|
||||
...overrides,
|
||||
};
|
||||
const wrapper = shallow(
|
||||
<AdhocFilterEditPopoverSimpleTabContent {...props} />,
|
||||
);
|
||||
return { wrapper, onChange, onHeightChange };
|
||||
}
|
||||
|
||||
describe('AdhocFilterEditPopoverSimpleTabContent', () => {
|
||||
it('renders the simple tab form', () => {
|
||||
const { wrapper } = setup();
|
||||
expect(wrapper).toExist();
|
||||
});
|
||||
|
||||
it('passes the new adhocFilter to onChange after onSubjectChange', () => {
|
||||
const { wrapper, onChange } = setup();
|
||||
wrapper.instance().onSubjectChange(1);
|
||||
expect(onChange.calledOnce).toBe(true);
|
||||
expect(onChange.lastCall.args[0]).toEqual(
|
||||
simpleAdhocFilter.duplicateWith({ subject: 'source' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('may alter the clause in onSubjectChange if the old clause is not appropriate', () => {
|
||||
const { wrapper, onChange } = setup();
|
||||
wrapper.instance().onSubjectChange(sumValueAdhocMetric.optionName);
|
||||
expect(onChange.calledOnce).toBe(true);
|
||||
expect(onChange.lastCall.args[0]).toEqual(
|
||||
simpleAdhocFilter.duplicateWith({
|
||||
subject: sumValueAdhocMetric.label,
|
||||
clause: CLAUSES.HAVING,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('will convert from individual comparator to array if the operator changes to multi', () => {
|
||||
const { wrapper, onChange } = setup();
|
||||
wrapper.instance().onOperatorChange('IN');
|
||||
expect(onChange.calledOnce).toBe(true);
|
||||
expect(onChange.lastCall.args[0]).toEqual(
|
||||
simpleAdhocFilter.duplicateWith({ operator: 'IN', comparator: ['10'] }),
|
||||
);
|
||||
});
|
||||
|
||||
it('will convert from array to individual comparators if the operator changes from multi', () => {
|
||||
const { wrapper, onChange } = setup({
|
||||
adhocFilter: simpleMultiAdhocFilter,
|
||||
});
|
||||
wrapper.instance().onOperatorChange('<');
|
||||
expect(onChange.calledOnce).toBe(true);
|
||||
expect(onChange.lastCall.args[0]).toEqual(
|
||||
simpleMultiAdhocFilter.duplicateWith({ operator: '<', comparator: '10' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('passes the new adhocFilter to onChange after onComparatorChange', () => {
|
||||
const { wrapper, onChange } = setup();
|
||||
wrapper.instance().onComparatorChange('20');
|
||||
expect(onChange.calledOnce).toBe(true);
|
||||
expect(onChange.lastCall.args[0]).toEqual(
|
||||
simpleAdhocFilter.duplicateWith({ comparator: '20' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('will filter operators for table datasources', () => {
|
||||
const { wrapper } = setup({ datasource: { type: 'table' } });
|
||||
expect(wrapper.instance().isOperatorRelevant('REGEX')).toBe(false);
|
||||
expect(wrapper.instance().isOperatorRelevant('LIKE')).toBe(true);
|
||||
});
|
||||
|
||||
it('will filter operators for druid datasources', () => {
|
||||
const { wrapper } = setup({ datasource: { type: 'druid' } });
|
||||
expect(wrapper.instance().isOperatorRelevant('REGEX')).toBe(true);
|
||||
expect(wrapper.instance().isOperatorRelevant('LIKE')).toBe(false);
|
||||
});
|
||||
|
||||
it('will show LATEST PARTITION operator', () => {
|
||||
const { wrapper } = setup({
|
||||
datasource: {
|
||||
type: 'table',
|
||||
datasource_name: 'table1',
|
||||
schema: 'schema',
|
||||
},
|
||||
adhocFilter: simpleCustomFilter,
|
||||
partitionColumn: 'ds',
|
||||
});
|
||||
|
||||
expect(
|
||||
wrapper.instance().isOperatorRelevant('LATEST PARTITION', 'ds'),
|
||||
).toBe(true);
|
||||
expect(
|
||||
wrapper.instance().isOperatorRelevant('LATEST PARTITION', 'value'),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('will generate custom sqlExpression for LATEST PARTITION operator', () => {
|
||||
const testAdhocFilter = new AdhocFilter({
|
||||
expressionType: EXPRESSION_TYPES.SIMPLE,
|
||||
subject: 'ds',
|
||||
});
|
||||
const { wrapper, onChange } = setup({
|
||||
datasource: {
|
||||
type: 'table',
|
||||
datasource_name: 'table1',
|
||||
schema: 'schema',
|
||||
},
|
||||
adhocFilter: testAdhocFilter,
|
||||
partitionColumn: 'ds',
|
||||
});
|
||||
|
||||
wrapper.instance().onOperatorChange('LATEST PARTITION');
|
||||
expect(onChange.lastCall.args[0]).toEqual(
|
||||
testAdhocFilter.duplicateWith({
|
||||
subject: 'ds',
|
||||
operator: 'LATEST PARTITION',
|
||||
comparator: null,
|
||||
clause: 'WHERE',
|
||||
expressionType: 'SQL',
|
||||
sqlExpression: "ds = '{{ presto.latest_partition('schema.table1') }}'",
|
||||
}),
|
||||
);
|
||||
});
|
||||
it('will display boolean operators only when column type is boolean', () => {
|
||||
const { wrapper } = setup({
|
||||
datasource: {
|
||||
type: 'table',
|
||||
datasource_name: 'table1',
|
||||
schema: 'schema',
|
||||
columns: [{ column_name: 'value', type: 'BOOL' }],
|
||||
},
|
||||
adhocFilter: simpleAdhocFilter,
|
||||
});
|
||||
const booleanOnlyOperators = [
|
||||
'IS TRUE',
|
||||
'IS FALSE',
|
||||
'IS NULL',
|
||||
'IS NOT NULL',
|
||||
];
|
||||
booleanOnlyOperators.forEach(operator => {
|
||||
expect(wrapper.instance().isOperatorRelevant(operator, 'value')).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
it('will display boolean operators when column type is number', () => {
|
||||
const { wrapper } = setup({
|
||||
datasource: {
|
||||
type: 'table',
|
||||
datasource_name: 'table1',
|
||||
schema: 'schema',
|
||||
columns: [{ column_name: 'value', type: 'INT' }],
|
||||
},
|
||||
adhocFilter: simpleAdhocFilter,
|
||||
});
|
||||
const booleanOnlyOperators = ['IS TRUE', 'IS FALSE'];
|
||||
booleanOnlyOperators.forEach(operator => {
|
||||
expect(wrapper.instance().isOperatorRelevant(operator, 'value')).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
it('will not display boolean operators when column type is string', () => {
|
||||
const { wrapper } = setup({
|
||||
datasource: {
|
||||
type: 'table',
|
||||
datasource_name: 'table1',
|
||||
schema: 'schema',
|
||||
columns: [{ column_name: 'value', type: 'STRING' }],
|
||||
},
|
||||
adhocFilter: simpleAdhocFilter,
|
||||
});
|
||||
const booleanOnlyOperators = ['IS TRUE', 'IS FALSE'];
|
||||
booleanOnlyOperators.forEach(operator => {
|
||||
expect(wrapper.instance().isOperatorRelevant(operator, 'value')).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
it('will display boolean operators when column is an expression', () => {
|
||||
const { wrapper } = setup({
|
||||
datasource: {
|
||||
type: 'table',
|
||||
datasource_name: 'table1',
|
||||
schema: 'schema',
|
||||
columns: [
|
||||
{
|
||||
column_name: 'value',
|
||||
expression: 'case when value is 0 then "NO"',
|
||||
},
|
||||
],
|
||||
},
|
||||
adhocFilter: simpleAdhocFilter,
|
||||
});
|
||||
const booleanOnlyOperators = ['IS TRUE', 'IS FALSE'];
|
||||
booleanOnlyOperators.forEach(operator => {
|
||||
expect(wrapper.instance().isOperatorRelevant(operator, 'value')).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,322 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
/* eslint-disable no-unused-expressions */
|
||||
import React from 'react';
|
||||
import sinon from 'sinon';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import AdhocFilter, {
|
||||
EXPRESSION_TYPES,
|
||||
CLAUSES,
|
||||
} from 'src/explore/components/controls/FilterControl/AdhocFilter';
|
||||
import {
|
||||
AGGREGATES,
|
||||
Operators,
|
||||
OPERATOR_ENUM_TO_OPERATOR_TYPE,
|
||||
} from 'src/explore/constants';
|
||||
import AdhocMetric from 'src/explore/components/controls/MetricControl/AdhocMetric';
|
||||
import AdhocFilterEditPopoverSimpleTabContent, {
|
||||
useSimpleTabFilterProps,
|
||||
} from '.';
|
||||
|
||||
const simpleAdhocFilter = new AdhocFilter({
|
||||
expressionType: EXPRESSION_TYPES.SIMPLE,
|
||||
subject: 'value',
|
||||
operatorId: Operators.GREATER_THAN,
|
||||
operator: OPERATOR_ENUM_TO_OPERATOR_TYPE[Operators.GREATER_THAN].operation,
|
||||
comparator: '10',
|
||||
clause: CLAUSES.WHERE,
|
||||
});
|
||||
|
||||
const simpleMultiAdhocFilter = new AdhocFilter({
|
||||
expressionType: EXPRESSION_TYPES.SIMPLE,
|
||||
subject: 'value',
|
||||
operatorId: Operators.IN,
|
||||
operator: OPERATOR_ENUM_TO_OPERATOR_TYPE[Operators.IN].operation,
|
||||
comparator: ['10'],
|
||||
clause: CLAUSES.WHERE,
|
||||
});
|
||||
|
||||
const sumValueAdhocMetric = new AdhocMetric({
|
||||
expressionType: EXPRESSION_TYPES.SIMPLE,
|
||||
column: { type: 'VARCHAR(255)', column_name: 'source' },
|
||||
aggregate: AGGREGATES.SUM,
|
||||
});
|
||||
|
||||
const simpleCustomFilter = new AdhocFilter({
|
||||
expressionType: EXPRESSION_TYPES.SIMPLE,
|
||||
subject: 'ds',
|
||||
operator: 'LATEST PARTITION',
|
||||
operatorId: Operators.LATEST_PARTITION,
|
||||
});
|
||||
|
||||
const options = [
|
||||
{ type: 'VARCHAR(255)', column_name: 'source', id: 1 },
|
||||
{ type: 'VARCHAR(255)', column_name: 'target', id: 2 },
|
||||
{ type: 'DOUBLE', column_name: 'value', id: 3 },
|
||||
{ saved_metric_name: 'my_custom_metric', id: 4 },
|
||||
sumValueAdhocMetric,
|
||||
];
|
||||
|
||||
function setup(overrides?: Record<string, any>) {
|
||||
const onChange = sinon.spy();
|
||||
const props = {
|
||||
adhocFilter: simpleAdhocFilter,
|
||||
onChange,
|
||||
options,
|
||||
datasource: {
|
||||
id: 'test-id',
|
||||
columns: [],
|
||||
type: 'postgres',
|
||||
filter_select: false,
|
||||
},
|
||||
partitionColumn: 'test',
|
||||
...overrides,
|
||||
};
|
||||
const wrapper = shallow(
|
||||
<AdhocFilterEditPopoverSimpleTabContent {...props} />,
|
||||
);
|
||||
return { wrapper, props };
|
||||
}
|
||||
|
||||
describe('AdhocFilterEditPopoverSimpleTabContent', () => {
|
||||
it('renders the simple tab form', () => {
|
||||
const { wrapper } = setup();
|
||||
expect(wrapper).toExist();
|
||||
});
|
||||
|
||||
it('shows boolean only operators when subject is boolean', () => {
|
||||
const { props } = setup({
|
||||
adhocFilter: new AdhocFilter({
|
||||
expressionType: EXPRESSION_TYPES.SIMPLE,
|
||||
subject: 'value',
|
||||
operatorId: null,
|
||||
operator: null,
|
||||
comparator: null,
|
||||
clause: null,
|
||||
}),
|
||||
datasource: {
|
||||
columns: [
|
||||
{
|
||||
id: 3,
|
||||
column_name: 'value',
|
||||
type: 'BOOL',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const { isOperatorRelevant } = useSimpleTabFilterProps(props);
|
||||
[
|
||||
Operators.IS_TRUE,
|
||||
Operators.IS_FALSE,
|
||||
Operators.IS_NULL,
|
||||
Operators.IS_FALSE,
|
||||
].map(operator => expect(isOperatorRelevant(operator, 'value')).toBe(true));
|
||||
});
|
||||
it('shows boolean only operators when subject is number', () => {
|
||||
const { props } = setup({
|
||||
adhocFilter: new AdhocFilter({
|
||||
expressionType: EXPRESSION_TYPES.SIMPLE,
|
||||
subject: 'value',
|
||||
operatorId: null,
|
||||
operator: null,
|
||||
comparator: null,
|
||||
clause: null,
|
||||
}),
|
||||
datasource: {
|
||||
columns: [
|
||||
{
|
||||
id: 3,
|
||||
column_name: 'value',
|
||||
type: 'INT',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const { isOperatorRelevant } = useSimpleTabFilterProps(props);
|
||||
[
|
||||
Operators.IS_TRUE,
|
||||
Operators.IS_FALSE,
|
||||
Operators.IS_NULL,
|
||||
Operators.IS_NOT_NULL,
|
||||
].map(operator => expect(isOperatorRelevant(operator, 'value')).toBe(true));
|
||||
});
|
||||
|
||||
it('will convert from individual comparator to array if the operator changes to multi', () => {
|
||||
const { props } = setup();
|
||||
const { onOperatorChange } = useSimpleTabFilterProps(props);
|
||||
onOperatorChange(Operators.IN);
|
||||
expect(props.onChange.calledOnce).toBe(true);
|
||||
expect(props.onChange.lastCall.args[0].comparator).toEqual(['10']);
|
||||
expect(props.onChange.lastCall.args[0].operatorId).toEqual(Operators.IN);
|
||||
});
|
||||
|
||||
it('will convert from array to individual comparators if the operator changes from multi', () => {
|
||||
const { props } = setup({
|
||||
adhocFilter: simpleMultiAdhocFilter,
|
||||
});
|
||||
const { onOperatorChange } = useSimpleTabFilterProps(props);
|
||||
onOperatorChange(Operators.LESS_THAN);
|
||||
expect(props.onChange.calledOnce).toBe(true);
|
||||
expect(props.onChange.lastCall.args[0]).toEqual(
|
||||
simpleMultiAdhocFilter.duplicateWith({
|
||||
operatorId: Operators.LESS_THAN,
|
||||
operator: '<',
|
||||
comparator: '10',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('passes the new adhocFilter to onChange after onComparatorChange', () => {
|
||||
const { props } = setup();
|
||||
const { onComparatorChange } = useSimpleTabFilterProps(props);
|
||||
onComparatorChange('20');
|
||||
expect(props.onChange.calledOnce).toBe(true);
|
||||
expect(props.onChange.lastCall.args[0]).toEqual(
|
||||
simpleAdhocFilter.duplicateWith({ comparator: '20' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('will filter operators for table datasources', () => {
|
||||
const { props } = setup({ datasource: { type: 'table' } });
|
||||
const { isOperatorRelevant } = useSimpleTabFilterProps(props);
|
||||
expect(isOperatorRelevant(Operators.REGEX, 'value')).toBe(false);
|
||||
expect(isOperatorRelevant(Operators.LIKE, 'value')).toBe(true);
|
||||
});
|
||||
|
||||
it('will filter operators for druid datasources', () => {
|
||||
const { props } = setup({ datasource: { type: 'druid' } });
|
||||
const { isOperatorRelevant } = useSimpleTabFilterProps(props);
|
||||
expect(isOperatorRelevant(Operators.REGEX, 'value')).toBe(true);
|
||||
expect(isOperatorRelevant(Operators.LIKE, 'value')).toBe(false);
|
||||
});
|
||||
|
||||
it('will show LATEST PARTITION operator', () => {
|
||||
const { props } = setup({
|
||||
datasource: {
|
||||
type: 'table',
|
||||
datasource_name: 'table1',
|
||||
schema: 'schema',
|
||||
},
|
||||
adhocFilter: simpleCustomFilter,
|
||||
partitionColumn: 'ds',
|
||||
});
|
||||
const { isOperatorRelevant } = useSimpleTabFilterProps(props);
|
||||
expect(isOperatorRelevant(Operators.LATEST_PARTITION, 'ds')).toBe(true);
|
||||
expect(isOperatorRelevant(Operators.LATEST_PARTITION, 'value')).toBe(false);
|
||||
});
|
||||
|
||||
it('will generate custom sqlExpression for LATEST PARTITION operator', () => {
|
||||
const testAdhocFilter = new AdhocFilter({
|
||||
expressionType: EXPRESSION_TYPES.SIMPLE,
|
||||
subject: 'ds',
|
||||
});
|
||||
const { props } = setup({
|
||||
datasource: {
|
||||
type: 'table',
|
||||
datasource_name: 'table1',
|
||||
schema: 'schema',
|
||||
},
|
||||
adhocFilter: testAdhocFilter,
|
||||
partitionColumn: 'ds',
|
||||
});
|
||||
const { onOperatorChange } = useSimpleTabFilterProps(props);
|
||||
onOperatorChange(Operators.LATEST_PARTITION);
|
||||
expect(props.onChange.calledOnce).toBe(true);
|
||||
expect(props.onChange.lastCall.args[0]).toEqual(
|
||||
testAdhocFilter.duplicateWith({
|
||||
subject: 'ds',
|
||||
operator: 'LATEST PARTITION',
|
||||
operatorId: Operators.LATEST_PARTITION,
|
||||
comparator: null,
|
||||
clause: 'WHERE',
|
||||
expressionType: 'SQL',
|
||||
sqlExpression: "ds = '{{ presto.latest_partition('schema.table1') }}'",
|
||||
}),
|
||||
);
|
||||
});
|
||||
it('will not display boolean operators when column type is string', () => {
|
||||
const { props } = setup({
|
||||
datasource: {
|
||||
type: 'table',
|
||||
datasource_name: 'table1',
|
||||
schema: 'schema',
|
||||
columns: [{ column_name: 'value', type: 'STRING' }],
|
||||
},
|
||||
adhocFilter: simpleAdhocFilter,
|
||||
});
|
||||
const { isOperatorRelevant } = useSimpleTabFilterProps(props);
|
||||
const booleanOnlyOperators = [Operators.IS_TRUE, Operators.IS_FALSE];
|
||||
booleanOnlyOperators.forEach(operator => {
|
||||
expect(isOperatorRelevant(operator, 'value')).toBe(false);
|
||||
});
|
||||
});
|
||||
it('will display boolean operators when column is an expression', () => {
|
||||
const { props } = setup({
|
||||
datasource: {
|
||||
type: 'table',
|
||||
datasource_name: 'table1',
|
||||
schema: 'schema',
|
||||
columns: [
|
||||
{
|
||||
column_name: 'value',
|
||||
expression: 'case when value is 0 then "NO"',
|
||||
},
|
||||
],
|
||||
},
|
||||
adhocFilter: simpleAdhocFilter,
|
||||
});
|
||||
const { isOperatorRelevant } = useSimpleTabFilterProps(props);
|
||||
const booleanOnlyOperators = [Operators.IS_TRUE, Operators.IS_FALSE];
|
||||
booleanOnlyOperators.forEach(operator => {
|
||||
expect(isOperatorRelevant(operator, 'value')).toBe(true);
|
||||
});
|
||||
});
|
||||
it('sets comparator to true when operator is IS_TRUE', () => {
|
||||
const { props } = setup();
|
||||
const { onOperatorChange } = useSimpleTabFilterProps(props);
|
||||
onOperatorChange(Operators.IS_TRUE);
|
||||
expect(props.onChange.calledOnce).toBe(true);
|
||||
expect(props.onChange.lastCall.args[0].operatorId).toBe(Operators.IS_TRUE);
|
||||
expect(props.onChange.lastCall.args[0].operator).toBe('==');
|
||||
expect(props.onChange.lastCall.args[0].comparator).toBe(true);
|
||||
});
|
||||
it('sets comparator to false when operator is IS_FALSE', () => {
|
||||
const { props } = setup();
|
||||
const { onOperatorChange } = useSimpleTabFilterProps(props);
|
||||
onOperatorChange(Operators.IS_FALSE);
|
||||
expect(props.onChange.calledOnce).toBe(true);
|
||||
expect(props.onChange.lastCall.args[0].operatorId).toBe(Operators.IS_FALSE);
|
||||
expect(props.onChange.lastCall.args[0].operator).toBe('==');
|
||||
expect(props.onChange.lastCall.args[0].comparator).toBe(false);
|
||||
});
|
||||
it('sets comparator to null when operator is IS_NULL or IS_NOT_NULL', () => {
|
||||
const { props } = setup();
|
||||
const { onOperatorChange } = useSimpleTabFilterProps(props);
|
||||
[Operators.IS_NULL, Operators.IS_NOT_NULL].forEach(op => {
|
||||
onOperatorChange(op);
|
||||
expect(props.onChange.called).toBe(true);
|
||||
expect(props.onChange.lastCall.args[0].operatorId).toBe(op);
|
||||
expect(props.onChange.lastCall.args[0].operator).toBe(
|
||||
OPERATOR_ENUM_TO_OPERATOR_TYPE[op].operation,
|
||||
);
|
||||
expect(props.onChange.lastCall.args[0].comparator).toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
@ -1,457 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { NativeSelect as Select } from 'src/components/Select';
|
||||
import { Input } from 'src/common/components';
|
||||
import { t, SupersetClient, styled } from '@superset-ui/core';
|
||||
|
||||
import adhocMetricType from 'src/explore/components/controls/MetricControl/adhocMetricType';
|
||||
import {
|
||||
OPERATORS,
|
||||
OPERATORS_OPTIONS,
|
||||
TABLE_ONLY_OPERATORS,
|
||||
DRUID_ONLY_OPERATORS,
|
||||
HAVING_OPERATORS,
|
||||
MULTI_OPERATORS,
|
||||
CUSTOM_OPERATORS,
|
||||
DISABLE_INPUT_OPERATORS,
|
||||
} from 'src/explore/constants';
|
||||
import FilterDefinitionOption from 'src/explore/components/controls/MetricControl/FilterDefinitionOption';
|
||||
import AdhocFilter, {
|
||||
EXPRESSION_TYPES,
|
||||
CLAUSES,
|
||||
} from 'src/explore/components/controls/FilterControl/AdhocFilter';
|
||||
import columnType from 'src/explore/components/controls/FilterControl/columnType';
|
||||
import Icons from 'src/components/Icons';
|
||||
|
||||
const SelectWithLabel = styled(Select)`
|
||||
.ant-select-selector {
|
||||
margin-bottom: ${({ theme }) => theme.gridUnit * 4}px;
|
||||
}
|
||||
|
||||
.ant-select-selector::after {
|
||||
content: '${({ labelText }) => labelText || '\\A0'}';
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
color: ${({ theme }) => theme.colors.grayscale.light1};
|
||||
width: max-content;
|
||||
}
|
||||
`;
|
||||
|
||||
const propTypes = {
|
||||
adhocFilter: PropTypes.instanceOf(AdhocFilter).isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
options: PropTypes.arrayOf(
|
||||
PropTypes.oneOfType([
|
||||
columnType,
|
||||
PropTypes.shape({ saved_metric_name: PropTypes.string.isRequired }),
|
||||
adhocMetricType,
|
||||
]),
|
||||
).isRequired,
|
||||
onHeightChange: PropTypes.func.isRequired,
|
||||
datasource: PropTypes.object,
|
||||
partitionColumn: PropTypes.string,
|
||||
popoverRef: PropTypes.object,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
datasource: {},
|
||||
};
|
||||
|
||||
function translateOperator(operator) {
|
||||
if (operator === OPERATORS['==']) {
|
||||
return 'equals';
|
||||
}
|
||||
if (operator === OPERATORS['!=']) {
|
||||
return 'not equal to';
|
||||
}
|
||||
if (operator === OPERATORS.LIKE) {
|
||||
return 'LIKE';
|
||||
}
|
||||
if (operator === OPERATORS.ILIKE) {
|
||||
return 'LIKE (case insensitive)';
|
||||
}
|
||||
if (operator === OPERATORS['LATEST PARTITION']) {
|
||||
return 'use latest_partition template';
|
||||
}
|
||||
return operator;
|
||||
}
|
||||
|
||||
export default class AdhocFilterEditPopoverSimpleTabContent extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onSubjectChange = this.onSubjectChange.bind(this);
|
||||
this.onOperatorChange = this.onOperatorChange.bind(this);
|
||||
this.onComparatorChange = this.onComparatorChange.bind(this);
|
||||
this.onInputComparatorChange = this.onInputComparatorChange.bind(this);
|
||||
this.isOperatorRelevant = this.isOperatorRelevant.bind(this);
|
||||
this.refreshComparatorSuggestions = this.refreshComparatorSuggestions.bind(
|
||||
this,
|
||||
);
|
||||
this.clearSuggestionSearch = this.clearSuggestionSearch.bind(this);
|
||||
|
||||
this.state = {
|
||||
suggestions: [],
|
||||
abortActiveRequest: null,
|
||||
currentSuggestionSearch: '',
|
||||
};
|
||||
|
||||
this.selectProps = {
|
||||
name: 'select-column',
|
||||
showSearch: true,
|
||||
};
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.refreshComparatorSuggestions();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.adhocFilter.subject !== this.props.adhocFilter.subject) {
|
||||
this.refreshComparatorSuggestions();
|
||||
}
|
||||
}
|
||||
|
||||
onSubjectChange(id) {
|
||||
const option = this.props.options.find(
|
||||
option => option.id === id || option.optionName === id,
|
||||
);
|
||||
|
||||
let subject;
|
||||
let clause;
|
||||
// infer the new clause based on what subject was selected.
|
||||
if (option && option.column_name) {
|
||||
subject = option.column_name;
|
||||
clause = CLAUSES.WHERE;
|
||||
} else if (option && (option.saved_metric_name || option.label)) {
|
||||
subject = option.saved_metric_name || option.label;
|
||||
clause = CLAUSES.HAVING;
|
||||
}
|
||||
const { operator } = this.props.adhocFilter;
|
||||
this.props.onChange(
|
||||
this.props.adhocFilter.duplicateWith({
|
||||
subject,
|
||||
clause,
|
||||
operator:
|
||||
operator && this.isOperatorRelevant(operator, subject)
|
||||
? operator
|
||||
: null,
|
||||
expressionType: EXPRESSION_TYPES.SIMPLE,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
onOperatorChange(operator) {
|
||||
const currentComparator = this.props.adhocFilter.comparator;
|
||||
let newComparator;
|
||||
// convert between list of comparators and individual comparators
|
||||
// (e.g. `in ('North America', 'Africa')` to `== 'North America'`)
|
||||
if (MULTI_OPERATORS.has(operator)) {
|
||||
newComparator = Array.isArray(currentComparator)
|
||||
? currentComparator
|
||||
: [currentComparator].filter(element => element);
|
||||
} else {
|
||||
newComparator = Array.isArray(currentComparator)
|
||||
? currentComparator[0]
|
||||
: currentComparator;
|
||||
}
|
||||
|
||||
if (operator && CUSTOM_OPERATORS.has(operator)) {
|
||||
this.props.onChange(
|
||||
this.props.adhocFilter.duplicateWith({
|
||||
subject: this.props.adhocFilter.subject,
|
||||
clause: CLAUSES.WHERE,
|
||||
operator,
|
||||
expressionType: EXPRESSION_TYPES.SQL,
|
||||
datasource: this.props.datasource,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
this.props.onChange(
|
||||
this.props.adhocFilter.duplicateWith({
|
||||
operator,
|
||||
comparator: newComparator,
|
||||
expressionType: EXPRESSION_TYPES.SIMPLE,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
onInputComparatorChange(event) {
|
||||
this.onComparatorChange(event.target.value);
|
||||
}
|
||||
|
||||
onComparatorChange(comparator) {
|
||||
this.props.onChange(
|
||||
this.props.adhocFilter.duplicateWith({
|
||||
comparator,
|
||||
expressionType: EXPRESSION_TYPES.SIMPLE,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
refreshComparatorSuggestions() {
|
||||
const { datasource } = this.props;
|
||||
const col = this.props.adhocFilter.subject;
|
||||
const having = this.props.adhocFilter.clause === CLAUSES.HAVING;
|
||||
|
||||
if (col && datasource && datasource.filter_select && !having) {
|
||||
if (this.state.abortActiveRequest) {
|
||||
this.state.abortActiveRequest();
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const { signal } = controller;
|
||||
this.setState({ abortActiveRequest: controller.abort, loading: true });
|
||||
|
||||
SupersetClient.get({
|
||||
signal,
|
||||
endpoint: `/superset/filter/${datasource.type}/${datasource.id}/${col}/`,
|
||||
})
|
||||
.then(({ json }) => {
|
||||
this.setState(() => ({
|
||||
suggestions: json,
|
||||
abortActiveRequest: null,
|
||||
loading: false,
|
||||
}));
|
||||
})
|
||||
.catch(() => {
|
||||
this.setState(() => ({
|
||||
suggestions: [],
|
||||
abortActiveRequest: null,
|
||||
loading: false,
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
isOperatorRelevant(operator, subject) {
|
||||
const column = this.props.datasource.columns?.find(
|
||||
col => col.column_name === subject,
|
||||
);
|
||||
const isColumnBoolean =
|
||||
!!column && (column.type === 'BOOL' || column.type === 'BOOLEAN');
|
||||
const isColumnNumber = !!column && column.type === 'INT';
|
||||
const isColumnFunction = !!column && !!column.expression;
|
||||
|
||||
if (operator && CUSTOM_OPERATORS.has(operator)) {
|
||||
const { partitionColumn } = this.props;
|
||||
return partitionColumn && subject && subject === partitionColumn;
|
||||
}
|
||||
if (
|
||||
operator === OPERATORS['IS TRUE'] ||
|
||||
operator === OPERATORS['IS FALSE']
|
||||
) {
|
||||
return isColumnBoolean || isColumnNumber || isColumnFunction;
|
||||
}
|
||||
if (isColumnBoolean) {
|
||||
return (
|
||||
operator === OPERATORS['IS NULL'] ||
|
||||
operator === OPERATORS['IS NOT NULL']
|
||||
);
|
||||
}
|
||||
return !(
|
||||
(this.props.datasource.type === 'druid' &&
|
||||
TABLE_ONLY_OPERATORS.indexOf(operator) >= 0) ||
|
||||
(this.props.datasource.type === 'table' &&
|
||||
DRUID_ONLY_OPERATORS.indexOf(operator) >= 0) ||
|
||||
(this.props.adhocFilter.clause === CLAUSES.HAVING &&
|
||||
HAVING_OPERATORS.indexOf(operator) === -1)
|
||||
);
|
||||
}
|
||||
|
||||
focusComparator(ref, shouldFocus) {
|
||||
if (ref && shouldFocus) {
|
||||
ref.focus();
|
||||
}
|
||||
}
|
||||
|
||||
optionsRemaining() {
|
||||
const { suggestions } = this.state;
|
||||
const { comparator } = this.props.adhocFilter;
|
||||
// if select is multi/value is array, we show the options not selected
|
||||
const valuesFromSuggestionsLength = Array.isArray(comparator)
|
||||
? comparator.filter(v => suggestions.includes(v)).length
|
||||
: 0;
|
||||
return suggestions?.length - valuesFromSuggestionsLength ?? 0;
|
||||
}
|
||||
|
||||
createSuggestionsPlaceholder() {
|
||||
const optionsRemaining = this.optionsRemaining();
|
||||
const placeholder = t('%s option(s)', optionsRemaining);
|
||||
return optionsRemaining ? placeholder : '';
|
||||
}
|
||||
|
||||
renderSubjectOptionLabel(option) {
|
||||
return <FilterDefinitionOption option={option} />;
|
||||
}
|
||||
|
||||
clearSuggestionSearch() {
|
||||
this.setState({ currentSuggestionSearch: '' });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { adhocFilter, options, datasource } = this.props;
|
||||
const { currentSuggestionSearch } = this.state;
|
||||
let columns = options;
|
||||
const { subject, operator, comparator } = adhocFilter;
|
||||
const subjectSelectProps = {
|
||||
value: subject ?? undefined,
|
||||
onChange: this.onSubjectChange,
|
||||
notFoundContent: t(
|
||||
'No such column found. To filter on a metric, try the Custom SQL tab.',
|
||||
),
|
||||
filterOption: (input, option) =>
|
||||
option.filterBy.toLowerCase().indexOf(input.toLowerCase()) >= 0,
|
||||
autoFocus: !subject,
|
||||
};
|
||||
|
||||
if (datasource.type === 'druid') {
|
||||
subjectSelectProps.placeholder = t(
|
||||
'%s column(s) and metric(s)',
|
||||
columns.length,
|
||||
);
|
||||
} else {
|
||||
// we cannot support simple ad-hoc filters for metrics because we don't know what type
|
||||
// the value should be cast to (without knowing the output type of the aggregate, which
|
||||
// becomes a rather complicated problem)
|
||||
subjectSelectProps.placeholder =
|
||||
adhocFilter.clause === CLAUSES.WHERE
|
||||
? t('%s column(s)', columns.length)
|
||||
: t('To filter on a metric, use Custom SQL tab.');
|
||||
columns = options.filter(option => option.column_name);
|
||||
}
|
||||
|
||||
const operatorSelectProps = {
|
||||
placeholder: t(
|
||||
'%s operator(s)',
|
||||
OPERATORS_OPTIONS.filter(op => this.isOperatorRelevant(op, subject))
|
||||
.length,
|
||||
),
|
||||
// like AGGREGATES_OPTIONS, operator options are string
|
||||
value: operator,
|
||||
onChange: this.onOperatorChange,
|
||||
filterOption: (input, option) =>
|
||||
option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0,
|
||||
autoFocus: !!subjectSelectProps.value && !operator,
|
||||
};
|
||||
|
||||
const focusComparator =
|
||||
!!subjectSelectProps.value && !!operatorSelectProps.value;
|
||||
const comparatorSelectProps = {
|
||||
allowClear: true,
|
||||
showSearch: true,
|
||||
mode: MULTI_OPERATORS.has(operator) && 'tags',
|
||||
tokenSeparators: [',', '\n', '\t', ';'],
|
||||
loading: this.state.loading,
|
||||
value: comparator,
|
||||
onChange: this.onComparatorChange,
|
||||
notFoundContent: t('Type a value here'),
|
||||
disabled: DISABLE_INPUT_OPERATORS.includes(operator),
|
||||
placeholder: this.createSuggestionsPlaceholder(),
|
||||
labelText: comparator?.length > 0 && this.createSuggestionsPlaceholder(),
|
||||
autoFocus: focusComparator,
|
||||
};
|
||||
const Icon =
|
||||
operator === 'NOT IN' ? Icons.StopOutlined : Icons.CheckOutlined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Select
|
||||
css={theme => ({
|
||||
marginTop: theme.gridUnit * 4,
|
||||
marginBottom: theme.gridUnit * 4,
|
||||
})}
|
||||
{...this.selectProps}
|
||||
{...subjectSelectProps}
|
||||
name="filter-column"
|
||||
getPopupContainer={triggerNode => triggerNode.parentNode}
|
||||
>
|
||||
{columns.map(column => (
|
||||
<Select.Option
|
||||
value={column.id || column.optionName}
|
||||
filterBy={
|
||||
column.saved_metric_name || column.column_name || column.label
|
||||
}
|
||||
key={column.id || column.optionName}
|
||||
>
|
||||
{this.renderSubjectOptionLabel(column)}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
css={theme => ({ marginBottom: theme.gridUnit * 4 })}
|
||||
{...this.selectProps}
|
||||
{...operatorSelectProps}
|
||||
getPopupContainer={triggerNode => triggerNode.parentNode}
|
||||
name="filter-operator"
|
||||
>
|
||||
{OPERATORS_OPTIONS.filter(op =>
|
||||
this.isOperatorRelevant(op, subject),
|
||||
).map(option => (
|
||||
<Select.Option value={option} key={option}>
|
||||
{translateOperator(option)}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
{MULTI_OPERATORS.has(operator) || this.state.suggestions.length > 0 ? (
|
||||
<SelectWithLabel
|
||||
data-test="adhoc-filter-simple-value"
|
||||
name="filter-value"
|
||||
{...comparatorSelectProps}
|
||||
getPopupContainer={triggerNode => triggerNode.parentNode}
|
||||
onSearch={val => this.setState({ currentSuggestionSearch: val })}
|
||||
onSelect={this.clearSuggestionSearch}
|
||||
onBlur={this.clearSuggestionSearch}
|
||||
menuItemSelectedIcon={<Icon iconSize="m" />}
|
||||
>
|
||||
{this.state.suggestions.map(suggestion => (
|
||||
<Select.Option value={suggestion} key={suggestion}>
|
||||
{suggestion}
|
||||
</Select.Option>
|
||||
))}
|
||||
|
||||
{/* enable selecting an option not included in suggestions */}
|
||||
{currentSuggestionSearch &&
|
||||
!this.state.suggestions.some(
|
||||
suggestion => suggestion === currentSuggestionSearch,
|
||||
) && (
|
||||
<Select.Option value={currentSuggestionSearch}>
|
||||
{`${t('Create "%s"', currentSuggestionSearch)}`}
|
||||
</Select.Option>
|
||||
)}
|
||||
</SelectWithLabel>
|
||||
) : (
|
||||
<Input
|
||||
data-test="adhoc-filter-simple-value"
|
||||
name="filter-value"
|
||||
ref={ref => this.focusComparator(ref, focusComparator)}
|
||||
onChange={this.onInputComparatorChange}
|
||||
value={comparator}
|
||||
placeholder={t('Filter value (case sensitive)')}
|
||||
disabled={DISABLE_INPUT_OPERATORS.includes(operator)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
AdhocFilterEditPopoverSimpleTabContent.propTypes = propTypes;
|
||||
AdhocFilterEditPopoverSimpleTabContent.defaultProps = defaultProps;
|
@ -0,0 +1,463 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { NativeSelect as Select } from 'src/components/Select';
|
||||
import { t, SupersetClient, styled } from '@superset-ui/core';
|
||||
import {
|
||||
Operators,
|
||||
OPERATORS_OPTIONS,
|
||||
TABLE_ONLY_OPERATORS,
|
||||
DRUID_ONLY_OPERATORS,
|
||||
HAVING_OPERATORS,
|
||||
MULTI_OPERATORS,
|
||||
CUSTOM_OPERATORS,
|
||||
DISABLE_INPUT_OPERATORS,
|
||||
AGGREGATES,
|
||||
OPERATOR_ENUM_TO_OPERATOR_TYPE,
|
||||
} from 'src/explore/constants';
|
||||
import FilterDefinitionOption from 'src/explore/components/controls/MetricControl/FilterDefinitionOption';
|
||||
import AdhocFilter, {
|
||||
EXPRESSION_TYPES,
|
||||
CLAUSES,
|
||||
} from 'src/explore/components/controls/FilterControl/AdhocFilter';
|
||||
import { Input, SelectProps } from 'src/common/components';
|
||||
|
||||
const SelectWithLabel = styled(Select)`
|
||||
.ant-select-selector {
|
||||
margin-bottom: ${({ theme }) => theme.gridUnit * 4}px;
|
||||
}
|
||||
|
||||
.ant-select-selector::after {
|
||||
content: '${(
|
||||
pr: SelectProps<any> & {
|
||||
labelText: string | boolean;
|
||||
},
|
||||
) => pr.labelText || '\\A0'}';
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
color: ${({ theme }) => theme.colors.grayscale.light1};
|
||||
width: max-content;
|
||||
}
|
||||
`;
|
||||
|
||||
export interface SimpleColumnType {
|
||||
id: number;
|
||||
column_name: string;
|
||||
expression?: string;
|
||||
type: string;
|
||||
optionName?: string;
|
||||
filterBy?: string;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
export interface SimpleExpressionType {
|
||||
expressionType: keyof typeof EXPRESSION_TYPES;
|
||||
column: SimpleColumnType;
|
||||
aggregate: keyof typeof AGGREGATES;
|
||||
label: string;
|
||||
}
|
||||
export interface SQLExpressionType {
|
||||
expressionType: keyof typeof EXPRESSION_TYPES;
|
||||
sqlExpression: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface MetricColumnType {
|
||||
saved_metric_name: string;
|
||||
}
|
||||
|
||||
export type ColumnType =
|
||||
| SimpleColumnType
|
||||
| SimpleExpressionType
|
||||
| SQLExpressionType
|
||||
| MetricColumnType;
|
||||
|
||||
export interface Props {
|
||||
adhocFilter: AdhocFilter;
|
||||
onChange: (filter: AdhocFilter) => void;
|
||||
options: ColumnType[];
|
||||
datasource: {
|
||||
id: string;
|
||||
columns: SimpleColumnType[];
|
||||
type: string;
|
||||
filter_select: boolean;
|
||||
};
|
||||
partitionColumn: string;
|
||||
}
|
||||
export const useSimpleTabFilterProps = (props: Props) => {
|
||||
const isOperatorRelevant = (operator: Operators, subject: string) => {
|
||||
const column = props.datasource.columns?.find(
|
||||
col => col.column_name === subject,
|
||||
);
|
||||
const isColumnBoolean =
|
||||
!!column && (column.type === 'BOOL' || column.type === 'BOOLEAN');
|
||||
const isColumnNumber =
|
||||
!!column && (column.type === 'INT' || column.type === 'INTEGER');
|
||||
const isColumnFunction = !!column && !!column.expression;
|
||||
|
||||
if (operator && CUSTOM_OPERATORS.has(operator)) {
|
||||
const { partitionColumn } = props;
|
||||
return partitionColumn && subject && subject === partitionColumn;
|
||||
}
|
||||
if (operator === Operators.IS_TRUE || operator === Operators.IS_FALSE) {
|
||||
return isColumnBoolean || isColumnNumber || isColumnFunction;
|
||||
}
|
||||
if (isColumnBoolean) {
|
||||
return (
|
||||
operator === Operators.IS_NULL || operator === Operators.IS_NOT_NULL
|
||||
);
|
||||
}
|
||||
return !(
|
||||
(props.datasource.type === 'druid' &&
|
||||
TABLE_ONLY_OPERATORS.indexOf(operator) >= 0) ||
|
||||
(props.datasource.type === 'table' &&
|
||||
DRUID_ONLY_OPERATORS.indexOf(operator) >= 0) ||
|
||||
(props.adhocFilter.clause === CLAUSES.HAVING &&
|
||||
HAVING_OPERATORS.indexOf(operator) === -1)
|
||||
);
|
||||
};
|
||||
const onSubjectChange = (id: string | number) => {
|
||||
const option = props.options.find(
|
||||
option =>
|
||||
('id' in option && option.id === id) ||
|
||||
('optionName' in option && option.optionName === id),
|
||||
);
|
||||
|
||||
let subject = '';
|
||||
let clause;
|
||||
// infer the new clause based on what subject was selected.
|
||||
if (option && 'column_name' in option) {
|
||||
subject = option.column_name;
|
||||
clause = CLAUSES.WHERE;
|
||||
} else if (option && 'saved_metric_name' in option) {
|
||||
subject = option.saved_metric_name;
|
||||
clause = CLAUSES.HAVING;
|
||||
} else if (option && option.label) {
|
||||
subject = option.label;
|
||||
clause = CLAUSES.HAVING;
|
||||
}
|
||||
const { operator, operatorId } = props.adhocFilter;
|
||||
props.onChange(
|
||||
props.adhocFilter.duplicateWith({
|
||||
subject,
|
||||
clause,
|
||||
operator:
|
||||
operator && isOperatorRelevant(operatorId, subject)
|
||||
? OPERATOR_ENUM_TO_OPERATOR_TYPE[operatorId].operation
|
||||
: null,
|
||||
expressionType: EXPRESSION_TYPES.SIMPLE,
|
||||
operatorId,
|
||||
}),
|
||||
);
|
||||
};
|
||||
const onOperatorChange = (operatorId: Operators) => {
|
||||
const currentComparator = props.adhocFilter.comparator;
|
||||
let newComparator;
|
||||
// convert between list of comparators and individual comparators
|
||||
// (e.g. `in ('North America', 'Africa')` to `== 'North America'`)
|
||||
if (MULTI_OPERATORS.has(operatorId)) {
|
||||
newComparator = Array.isArray(currentComparator)
|
||||
? currentComparator
|
||||
: [currentComparator].filter(element => element);
|
||||
} else {
|
||||
newComparator = Array.isArray(currentComparator)
|
||||
? currentComparator[0]
|
||||
: currentComparator;
|
||||
}
|
||||
if (operatorId === Operators.IS_TRUE || operatorId === Operators.IS_FALSE) {
|
||||
newComparator = Operators.IS_TRUE === operatorId;
|
||||
}
|
||||
if (operatorId && CUSTOM_OPERATORS.has(operatorId)) {
|
||||
props.onChange(
|
||||
props.adhocFilter.duplicateWith({
|
||||
subject: props.adhocFilter.subject,
|
||||
clause: CLAUSES.WHERE,
|
||||
operatorId,
|
||||
operator: OPERATOR_ENUM_TO_OPERATOR_TYPE[operatorId].operation,
|
||||
expressionType: EXPRESSION_TYPES.SQL,
|
||||
datasource: props.datasource,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
props.onChange(
|
||||
props.adhocFilter.duplicateWith({
|
||||
operatorId,
|
||||
operator: OPERATOR_ENUM_TO_OPERATOR_TYPE[operatorId].operation,
|
||||
comparator: newComparator,
|
||||
expressionType: EXPRESSION_TYPES.SIMPLE,
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
const onComparatorChange = (comparator: string) => {
|
||||
props.onChange(
|
||||
props.adhocFilter.duplicateWith({
|
||||
comparator,
|
||||
expressionType: EXPRESSION_TYPES.SIMPLE,
|
||||
}),
|
||||
);
|
||||
};
|
||||
return {
|
||||
onSubjectChange,
|
||||
onOperatorChange,
|
||||
onComparatorChange,
|
||||
isOperatorRelevant,
|
||||
};
|
||||
};
|
||||
|
||||
const AdhocFilterEditPopoverSimpleTabContent: React.FC<Props> = props => {
|
||||
const selectProps = {
|
||||
name: 'select-column',
|
||||
showSearch: true,
|
||||
};
|
||||
const {
|
||||
onSubjectChange,
|
||||
onOperatorChange,
|
||||
isOperatorRelevant,
|
||||
onComparatorChange,
|
||||
} = useSimpleTabFilterProps(props);
|
||||
const [suggestions, setSuggestions] = useState<Record<string, any>>([]);
|
||||
const [currentSuggestionSearch, setCurrentSuggestionSearch] = useState('');
|
||||
const [
|
||||
loadingComparatorSuggestions,
|
||||
setLoadingComparatorSuggestions,
|
||||
] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const refreshComparatorSuggestions = () => {
|
||||
const { datasource } = props;
|
||||
const col = props.adhocFilter.subject;
|
||||
const having = props.adhocFilter.clause === CLAUSES.HAVING;
|
||||
|
||||
if (col && datasource && datasource.filter_select && !having) {
|
||||
const controller = new AbortController();
|
||||
const { signal } = controller;
|
||||
if (loadingComparatorSuggestions) {
|
||||
controller.abort();
|
||||
}
|
||||
setLoadingComparatorSuggestions(true);
|
||||
SupersetClient.get({
|
||||
signal,
|
||||
endpoint: `/superset/filter/${datasource.type}/${datasource.id}/${col}/`,
|
||||
})
|
||||
.then(({ json }) => {
|
||||
setSuggestions(json);
|
||||
setLoadingComparatorSuggestions(false);
|
||||
})
|
||||
.catch(() => {
|
||||
setSuggestions([]);
|
||||
setLoadingComparatorSuggestions(false);
|
||||
});
|
||||
}
|
||||
};
|
||||
refreshComparatorSuggestions();
|
||||
}, [props.adhocFilter.subject]);
|
||||
|
||||
const onInputComparatorChange = (
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
) => {
|
||||
onComparatorChange(event.target.value);
|
||||
};
|
||||
|
||||
const renderSubjectOptionLabel = (option: ColumnType) => (
|
||||
<FilterDefinitionOption option={option} />
|
||||
);
|
||||
|
||||
const clearSuggestionSearch = () => {
|
||||
setCurrentSuggestionSearch('');
|
||||
};
|
||||
|
||||
const getOptionsRemaining = () => {
|
||||
const { comparator } = props.adhocFilter;
|
||||
// if select is multi/value is array, we show the options not selected
|
||||
const valuesFromSuggestionsLength = Array.isArray(comparator)
|
||||
? comparator.filter(v => suggestions.includes(v)).length
|
||||
: 0;
|
||||
return suggestions?.length - valuesFromSuggestionsLength ?? 0;
|
||||
};
|
||||
const createSuggestionsPlaceholder = () => {
|
||||
const optionsRemaining = getOptionsRemaining();
|
||||
const placeholder = t('%s option(s)', optionsRemaining);
|
||||
return optionsRemaining ? placeholder : '';
|
||||
};
|
||||
|
||||
let columns = props.options;
|
||||
const { subject, operator, comparator, operatorId } = props.adhocFilter;
|
||||
const subjectSelectProps = {
|
||||
value: subject ?? undefined,
|
||||
onChange: onSubjectChange,
|
||||
notFoundContent: t(
|
||||
'No such column found. To filter on a metric, try the Custom SQL tab.',
|
||||
),
|
||||
autoFocus: !subject,
|
||||
placeholder: '',
|
||||
};
|
||||
|
||||
if (props.datasource.type === 'druid') {
|
||||
subjectSelectProps.placeholder = t(
|
||||
'%s column(s) and metric(s)',
|
||||
columns.length,
|
||||
);
|
||||
} else {
|
||||
// we cannot support simple ad-hoc filters for metrics because we don't know what type
|
||||
// the value should be cast to (without knowing the output type of the aggregate, which
|
||||
// becomes a rather complicated problem)
|
||||
subjectSelectProps.placeholder =
|
||||
props.adhocFilter.clause === CLAUSES.WHERE
|
||||
? t('%s column(s)', columns.length)
|
||||
: t('To filter on a metric, use Custom SQL tab.');
|
||||
columns = props.options.filter(
|
||||
option => 'column_name' in option && option.column_name,
|
||||
);
|
||||
}
|
||||
|
||||
const operatorSelectProps = {
|
||||
placeholder: t(
|
||||
'%s operator(s)',
|
||||
OPERATORS_OPTIONS.filter(op => isOperatorRelevant(op, subject)).length,
|
||||
),
|
||||
value: OPERATOR_ENUM_TO_OPERATOR_TYPE[operatorId]?.display,
|
||||
onChange: onOperatorChange,
|
||||
autoFocus: !!subjectSelectProps.value && !operator,
|
||||
name: 'select-operator',
|
||||
};
|
||||
|
||||
const shouldFocusComparator =
|
||||
!!subjectSelectProps.value && !!operatorSelectProps.value;
|
||||
|
||||
const comparatorSelectProps: SelectProps<any> & {
|
||||
labelText: string | boolean;
|
||||
} = {
|
||||
allowClear: true,
|
||||
showSearch: true,
|
||||
mode: MULTI_OPERATORS.has(operatorId) ? 'tags' : undefined,
|
||||
tokenSeparators: [',', '\n', '\t', ';'],
|
||||
loading: loadingComparatorSuggestions,
|
||||
value: comparator,
|
||||
onChange: onComparatorChange,
|
||||
notFoundContent: t('Type a value here'),
|
||||
disabled: DISABLE_INPUT_OPERATORS.includes(operatorId),
|
||||
placeholder: createSuggestionsPlaceholder(),
|
||||
labelText:
|
||||
comparator && comparator.length > 0 && createSuggestionsPlaceholder(),
|
||||
autoFocus: shouldFocusComparator,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Select
|
||||
css={theme => ({
|
||||
marginTop: theme.gridUnit * 4,
|
||||
marginBottom: theme.gridUnit * 4,
|
||||
})}
|
||||
{...selectProps}
|
||||
{...subjectSelectProps}
|
||||
filterOption={(input, option) =>
|
||||
option && option.filterBy
|
||||
? option.filterBy.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
: false
|
||||
}
|
||||
getPopupContainer={triggerNode => triggerNode.parentNode}
|
||||
>
|
||||
{columns.map(column => (
|
||||
<Select.Option
|
||||
value={
|
||||
('id' in column && column.id) ||
|
||||
('optionName' in column && column.optionName) ||
|
||||
''
|
||||
}
|
||||
filterBy={
|
||||
('saved_metric_name' in column && column.saved_metric_name) ||
|
||||
('column_name' in column && column.column_name) ||
|
||||
('label' in column && column.label)
|
||||
}
|
||||
key={
|
||||
('id' in column && column.id) ||
|
||||
('optionName' in column && column.optionName) ||
|
||||
undefined
|
||||
}
|
||||
>
|
||||
{renderSubjectOptionLabel(column)}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
css={theme => ({ marginBottom: theme.gridUnit * 4 })}
|
||||
{...selectProps}
|
||||
{...operatorSelectProps}
|
||||
filterOption={(input, option) =>
|
||||
option && option.children
|
||||
? option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
: false
|
||||
}
|
||||
getPopupContainer={triggerNode => triggerNode.parentNode}
|
||||
>
|
||||
{OPERATORS_OPTIONS.filter(op => isOperatorRelevant(op, subject)).map(
|
||||
option => (
|
||||
<Select.Option value={option} key={option}>
|
||||
{OPERATOR_ENUM_TO_OPERATOR_TYPE[option].display}
|
||||
</Select.Option>
|
||||
),
|
||||
)}
|
||||
</Select>
|
||||
{MULTI_OPERATORS.has(operatorId) || suggestions.length > 0 ? (
|
||||
<SelectWithLabel
|
||||
data-test="adhoc-filter-simple-value"
|
||||
{...comparatorSelectProps}
|
||||
getPopupContainer={triggerNode => triggerNode.parentNode}
|
||||
onSearch={val => setCurrentSuggestionSearch(val)}
|
||||
onSelect={clearSuggestionSearch}
|
||||
onBlur={clearSuggestionSearch}
|
||||
>
|
||||
{suggestions.map((suggestion: string) => (
|
||||
<Select.Option value={suggestion} key={suggestion}>
|
||||
{suggestion}
|
||||
</Select.Option>
|
||||
))}
|
||||
|
||||
{/* enable selecting an option not included in suggestions */}
|
||||
{currentSuggestionSearch &&
|
||||
!suggestions.some(
|
||||
(suggestion: string) => suggestion === currentSuggestionSearch,
|
||||
) && (
|
||||
<Select.Option value={currentSuggestionSearch}>
|
||||
{currentSuggestionSearch}
|
||||
</Select.Option>
|
||||
)}
|
||||
</SelectWithLabel>
|
||||
) : (
|
||||
<Input
|
||||
data-test="adhoc-filter-simple-value"
|
||||
name="filter-value"
|
||||
ref={ref => {
|
||||
if (ref && shouldFocusComparator) {
|
||||
ref.blur();
|
||||
}
|
||||
}}
|
||||
onChange={onInputComparatorChange}
|
||||
value={comparator}
|
||||
placeholder={t('Filter value (case sensitive)')}
|
||||
disabled={DISABLE_INPUT_OPERATORS.includes(operatorId)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdhocFilterEditPopoverSimpleTabContent;
|
@ -17,8 +17,6 @@
|
||||
* under the License.
|
||||
*/
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { OPERATORS } from 'src/explore/constants';
|
||||
import { EXPRESSION_TYPES, CLAUSES } from './AdhocFilter';
|
||||
|
||||
export default PropTypes.oneOfType([
|
||||
@ -26,7 +24,6 @@ export default PropTypes.oneOfType([
|
||||
expressionType: PropTypes.oneOf([EXPRESSION_TYPES.SIMPLE]).isRequired,
|
||||
clause: PropTypes.oneOf([CLAUSES.HAVING, CLAUSES.WHERE]).isRequired,
|
||||
subject: PropTypes.string.isRequired,
|
||||
operator: PropTypes.oneOf(Object.keys(OPERATORS)).isRequired,
|
||||
comparator: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.arrayOf(PropTypes.string),
|
||||
|
@ -51,5 +51,6 @@ export default function FilterDefinitionOption({ option }) {
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
FilterDefinitionOption.propTypes = propTypes;
|
||||
|
@ -28,49 +28,78 @@ export const AGGREGATES = {
|
||||
};
|
||||
export const AGGREGATES_OPTIONS = Object.values(AGGREGATES);
|
||||
|
||||
export const OPERATORS = {
|
||||
'==': '==',
|
||||
'!=': '!=',
|
||||
'>': '>',
|
||||
'<': '<',
|
||||
'>=': '>=',
|
||||
'<=': '<=',
|
||||
IN: 'IN',
|
||||
'NOT IN': 'NOT IN',
|
||||
ILIKE: 'ILIKE',
|
||||
LIKE: 'LIKE',
|
||||
REGEX: 'REGEX',
|
||||
'IS NOT NULL': 'IS NOT NULL',
|
||||
'IS NULL': 'IS NULL',
|
||||
'LATEST PARTITION': 'LATEST PARTITION',
|
||||
'IS TRUE': 'IS TRUE',
|
||||
'IS FALSE': 'IS FALSE',
|
||||
export enum Operators {
|
||||
EQUALS = 'EQUALS',
|
||||
NOT_EQUALS = 'NOT_EQUALS',
|
||||
LESS_THAN = 'LESS_THAN',
|
||||
GREATER_THAN = 'GREATER_THAN',
|
||||
LESS_THAN_OR_EQUAL = 'LESS_THAN_OR_EQUAL',
|
||||
GREATER_THAN_OR_EQUAL = 'GREATER_THAN_OR_EQUAL',
|
||||
IN = 'IN',
|
||||
NOT_IN = 'NOT_IN',
|
||||
ILIKE = 'ILIKE',
|
||||
LIKE = 'LIKE',
|
||||
REGEX = 'REGEX',
|
||||
IS_NOT_NULL = 'IS_NOT_NULL',
|
||||
IS_NULL = 'IS_NULL',
|
||||
LATEST_PARTITION = 'LATEST_PARTITION',
|
||||
IS_TRUE = 'IS_TRUE',
|
||||
IS_FALSE = 'IS_FALSE',
|
||||
}
|
||||
|
||||
export interface OperatorType {
|
||||
display: string;
|
||||
operation: string;
|
||||
}
|
||||
|
||||
export const OPERATOR_ENUM_TO_OPERATOR_TYPE: {
|
||||
[key in Operators]: OperatorType;
|
||||
} = {
|
||||
[Operators.EQUALS]: { display: 'equals', operation: '==' },
|
||||
[Operators.NOT_EQUALS]: { display: 'not equals', operation: '!=' },
|
||||
[Operators.GREATER_THAN]: { display: '>', operation: '>' },
|
||||
[Operators.LESS_THAN]: { display: '<', operation: '<' },
|
||||
[Operators.GREATER_THAN_OR_EQUAL]: { display: '>=', operation: '>=' },
|
||||
[Operators.LESS_THAN_OR_EQUAL]: { display: '<=', operation: '<=' },
|
||||
[Operators.IN]: { display: 'IN', operation: 'IN' },
|
||||
[Operators.NOT_IN]: { display: 'NOT IN', operation: 'NOT IN' },
|
||||
[Operators.LIKE]: { display: 'LIKE', operation: 'LIKE' },
|
||||
[Operators.ILIKE]: { display: 'LIKE (case insensitive)', operation: 'ILIKE' },
|
||||
[Operators.REGEX]: { display: 'REGEX', operation: 'REGEX' },
|
||||
[Operators.IS_NOT_NULL]: { display: 'IS NOT NULL', operation: 'IS NOT NULL' },
|
||||
[Operators.IS_NULL]: { display: 'IS NULL', operation: 'IS NULL' },
|
||||
[Operators.LATEST_PARTITION]: {
|
||||
display: 'use latest_partition template',
|
||||
operation: 'LATEST PARTITION',
|
||||
},
|
||||
[Operators.IS_TRUE]: { display: 'IS TRUE', operation: '==' },
|
||||
[Operators.IS_FALSE]: { display: 'IS FALSE', operation: '==' },
|
||||
};
|
||||
|
||||
export const OPERATORS_OPTIONS = Object.values(OPERATORS);
|
||||
export const OPERATORS_OPTIONS = Object.values(Operators) as Operators[];
|
||||
|
||||
export const TABLE_ONLY_OPERATORS = [OPERATORS.LIKE, OPERATORS.ILIKE];
|
||||
export const DRUID_ONLY_OPERATORS = [OPERATORS.REGEX];
|
||||
export const TABLE_ONLY_OPERATORS = [Operators.LIKE, Operators.ILIKE];
|
||||
export const DRUID_ONLY_OPERATORS = [Operators.REGEX];
|
||||
export const HAVING_OPERATORS = [
|
||||
OPERATORS['=='],
|
||||
OPERATORS['!='],
|
||||
OPERATORS['>'],
|
||||
OPERATORS['<'],
|
||||
OPERATORS['>='],
|
||||
OPERATORS['<='],
|
||||
Operators.EQUALS,
|
||||
Operators.NOT_EQUALS,
|
||||
Operators.GREATER_THAN,
|
||||
Operators.LESS_THAN,
|
||||
Operators.GREATER_THAN_OR_EQUAL,
|
||||
Operators.LESS_THAN_OR_EQUAL,
|
||||
];
|
||||
export const MULTI_OPERATORS = new Set([OPERATORS.IN, OPERATORS['NOT IN']]);
|
||||
export const MULTI_OPERATORS = new Set([Operators.IN, Operators.NOT_IN]);
|
||||
// CUSTOM_OPERATORS will show operator in simple mode,
|
||||
// but will generate customized sqlExpression
|
||||
export const CUSTOM_OPERATORS = new Set([OPERATORS['LATEST PARTITION']]);
|
||||
export const CUSTOM_OPERATORS = new Set([Operators.LATEST_PARTITION]);
|
||||
// DISABLE_INPUT_OPERATORS will disable filter value input
|
||||
// in adhocFilter control
|
||||
export const DISABLE_INPUT_OPERATORS = [
|
||||
OPERATORS['IS NOT NULL'],
|
||||
OPERATORS['IS NULL'],
|
||||
OPERATORS['LATEST PARTITION'],
|
||||
OPERATORS['IS TRUE'],
|
||||
OPERATORS['IS FALSE'],
|
||||
Operators.IS_NOT_NULL,
|
||||
Operators.IS_NULL,
|
||||
Operators.LATEST_PARTITION,
|
||||
Operators.IS_TRUE,
|
||||
Operators.IS_FALSE,
|
||||
];
|
||||
|
||||
export const sqlaAutoGeneratedMetricNameRegex = /^(sum|min|max|avg|count|count_distinct)__.*$/i;
|
||||
|
@ -29,7 +29,10 @@ import {
|
||||
import { availableDomains } from 'src/utils/hostNamesConfig';
|
||||
import { safeStringify } from 'src/utils/safeStringify';
|
||||
import { URL_PARAMS } from 'src/constants';
|
||||
import { MULTI_OPERATORS } from 'src/explore/constants';
|
||||
import {
|
||||
MULTI_OPERATORS,
|
||||
OPERATOR_ENUM_TO_OPERATOR_TYPE,
|
||||
} from 'src/explore/constants';
|
||||
import { DashboardStandaloneMode } from 'src/dashboard/util/constants';
|
||||
|
||||
const MAX_URL_LENGTH = 8000;
|
||||
@ -319,7 +322,10 @@ export const useDebouncedEffect = (effect, delay, deps) => {
|
||||
};
|
||||
|
||||
export const getSimpleSQLExpression = (subject, operator, comparator) => {
|
||||
const isMulti = MULTI_OPERATORS.has(operator);
|
||||
const isMulti =
|
||||
[...MULTI_OPERATORS]
|
||||
.map(op => OPERATOR_ENUM_TO_OPERATOR_TYPE[op].operation)
|
||||
.indexOf(operator) >= 0;
|
||||
let expression = subject ?? '';
|
||||
if (subject && operator) {
|
||||
expression += ` ${operator}`;
|
||||
|
@ -218,7 +218,7 @@ class FilterOperator(str, Enum):
|
||||
|
||||
class PostProcessingBoxplotWhiskerType(str, Enum):
|
||||
"""
|
||||
Calculate cell contibution to row/column total
|
||||
Calculate cell contribution to row/column total
|
||||
"""
|
||||
|
||||
TUKEY = "tukey"
|
||||
@ -228,7 +228,7 @@ class PostProcessingBoxplotWhiskerType(str, Enum):
|
||||
|
||||
class PostProcessingContributionOrientation(str, Enum):
|
||||
"""
|
||||
Calculate cell contibution to row/column total
|
||||
Calculate cell contribution to row/column total
|
||||
"""
|
||||
|
||||
ROW = "row"
|
||||
|
Loading…
Reference in New Issue
Block a user