Skip to content

Commit

Permalink
[Unified Search] Add GTE, LT options to filter options for date range…
Browse files Browse the repository at this point in the history
…s and numbers (elastic#174283)

## Summary


Fixes elastic#158878

### Adds two options for the unified search filters
`greater or equal` and `less than`

<img width="897" alt="Screenshot 2024-01-12 at 10 43 14"
src="https://github.com/elastic/kibana/assets/4283304/bd8b502a-8f77-4207-8d48-e8d66851f065">

<img width="898" alt="Screenshot 2024-01-12 at 10 43 31"
src="https://github.com/elastic/kibana/assets/4283304/34a23c56-9334-4d43-8820-175028672760">

### Changes labels for `between` filter pills. 

#### For empty values:
Before:
<img width="637" alt="Screenshot 2024-01-09 at 16 03 47"
src="https://github.com/elastic/kibana/assets/4283304/acd3b6ee-4774-45df-ade9-ab36daa744e1">

After:
<img width="906" alt="Screenshot 2024-01-09 at 15 58 37"
src="https://github.com/elastic/kibana/assets/4283304/318679fe-0078-43a0-815a-7f6936efa884">

#### For only one boundary:
Before:
<img width="641" alt="Screenshot 2024-01-09 at 16 03 34"
src="https://github.com/elastic/kibana/assets/4283304/b8d52abf-2556-4485-9aea-1bf0725b2945">

After:
<img width="733" alt="Screenshot 2024-01-09 at 16 02 09"
src="https://github.com/elastic/kibana/assets/4283304/2fc1b83a-97b5-4717-a244-042a78471fb0">

A few comments:

1. if you negate any of the new filters (gte, lt) it becomes a "not
between" filter.
2. If you only fill one boundary in the `between` filter it converts to
`greater or equal` or `less than` filter
  • Loading branch information
mbondyra authored and CoenWarmer committed Feb 15, 2024
1 parent 67618f7 commit 921a1af
Show file tree
Hide file tree
Showing 8 changed files with 187 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ describe('filter manager utilities', () => {
});

describe('getRangeDisplayValue()', () => {
test('no boundaries defined', () => {
const params = {};
const filter = { meta: { params } } as RangeFilter;
const result = getRangeDisplayValue(filter);
expect(result).toMatchInlineSnapshot(`"-"`);
});
test('gt & lt', () => {
const params = { gt: 10, lt: 100 };
const filter = { meta: { params } } as RangeFilter;
Expand Down Expand Up @@ -69,28 +75,28 @@ describe('filter manager utilities', () => {
const params = { gt: 50 };
const filter = { meta: { params } } as RangeFilter;
const result = getRangeDisplayValue(filter);
expect(result).toMatchInlineSnapshot(`"50 to Infinity"`);
expect(result).toMatchInlineSnapshot(`"> 50"`);
});

test('gte', () => {
const params = { gte: 60 };
const filter = { meta: { params } } as RangeFilter;
const result = getRangeDisplayValue(filter);
expect(result).toMatchInlineSnapshot(`"60 to Infinity"`);
expect(result).toMatchInlineSnapshot(`"≥ 60"`);
});

test('lt', () => {
const params = { lt: 70 };
const filter = { meta: { params } } as RangeFilter;
const result = getRangeDisplayValue(filter);
expect(result).toMatchInlineSnapshot(`"-Infinity to 70"`);
expect(result).toMatchInlineSnapshot(`"< 70"`);
});

test('lte', () => {
const params = { lte: 80 };
const filter = { meta: { params } } as RangeFilter;
const result = getRangeDisplayValue(filter);
expect(result).toMatchInlineSnapshot(`"-Infinity to 80"`);
expect(result).toMatchInlineSnapshot(`" 80"`);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* Side Public License, v 1.
*/

import { get } from 'lodash';
import { get, identity } from 'lodash';
import {
ScriptedRangeFilter,
RangeFilter,
Expand All @@ -21,11 +21,28 @@ export function getRangeDisplayValue(
{ meta: { params } }: RangeFilter | ScriptedRangeFilter,
formatter?: FieldFormat
) {
const left = params?.gte ?? params?.gt ?? -Infinity;
const right = params?.lte ?? params?.lt ?? Infinity;
if (!formatter) return `${left} to ${right}`;
const convert = formatter.getConverterFor('text');
return `${convert(left)} to ${convert(right)}`;
const convert = formatter ? formatter.getConverterFor('text') : identity;
const { gte, gt, lte, lt } = params || {};

const left = gte ?? gt;
const right = lte ?? lt;

if (left !== undefined && right !== undefined) {
return `${convert(left)} to ${convert(right)}`;
}
if (gte !== undefined) {
return `≥ ${convert(gte)}`;
}
if (gt !== undefined) {
return `> ${convert(gt)}`;
}
if (lte !== undefined) {
return `≤ ${convert(lte)}`;
}
if (lt !== undefined) {
return `< ${convert(lt)}`;
}
return '-';
}

const getFirstRangeKey = (filter: RangeFilter) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,42 @@
*/

import dateMath from '@kbn/datemath';
import { Filter } from '@kbn/es-query';
import { Filter, RangeFilter, ScriptedRangeFilter, isRangeFilter } from '@kbn/es-query';
import { ES_FIELD_TYPES } from '@kbn/field-types';
import isSemverValid from 'semver/functions/valid';
import { isFilterable, IpAddress } from '@kbn/data-plugin/common';
import type { DataView, DataViewField } from '@kbn/data-views-plugin/common';
import { FILTER_OPERATORS, Operator } from './filter_operators';
import { FILTER_OPERATORS, OPERATORS, Operator } from './filter_operators';

export function getFieldFromFilter(filter: Filter, indexPattern?: DataView) {
return indexPattern?.fields.find((field) => field.name === filter.meta.key);
}

function getRangeOperatorFromFilter({
meta: { params: { gte, gt, lte, lt } = {}, negate },
}: RangeFilter | ScriptedRangeFilter) {
if (negate) {
// if filter is negated, always use 'is not between' operator
return OPERATORS.NOT_BETWEEN;
}
const left = gte ?? gt;
const right = lte ?? lt;

if (left !== undefined && right === undefined) {
return OPERATORS.GREATER_OR_EQUAL;
}

if (left === undefined && right !== undefined) {
return OPERATORS.LESS;
}
return OPERATORS.BETWEEN;
}

export function getOperatorFromFilter(filter: Filter) {
return FILTER_OPERATORS.find((operator) => {
if (isRangeFilter(filter)) {
return getRangeOperatorFromFilter(filter) === operator.id;
}
return filter.meta.type === operator.type && filter.meta.negate === operator.negate;
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ export const strings = {
i18n.translate('unifiedSearch.filter.filterEditor.isBetweenOperatorOptionLabel', {
defaultMessage: 'is between',
}),
getIsGreaterOrEqualOperatorOptionLabel: () =>
i18n.translate('unifiedSearch.filter.filterEditor.greaterThanOrEqualOptionLabel', {
defaultMessage: 'greater or equal',
}),
getLessThanOperatorOptionLabel: () =>
i18n.translate('unifiedSearch.filter.filterEditor.lessThanOrEqualOptionLabel', {
defaultMessage: 'less than',
}),
getIsNotBetweenOperatorOptionLabel: () =>
i18n.translate('unifiedSearch.filter.filterEditor.isNotBetweenOperatorOptionLabel', {
defaultMessage: 'is not between',
Expand All @@ -46,10 +54,24 @@ export const strings = {
}),
};

export enum OPERATORS {
LESS = 'less',
GREATER_OR_EQUAL = 'greater_or_equal',
BETWEEN = 'between',
IS = 'is',
NOT_BETWEEN = 'not_between',
IS_NOT = 'is_not',
IS_ONE_OF = 'is_one_of',
IS_NOT_ONE_OF = 'is_not_one_of',
EXISTS = 'exists',
DOES_NOT_EXIST = 'does_not_exist',
}

export interface Operator {
message: string;
type: FILTERS;
negate: boolean;
id: OPERATORS;

/**
* KbnFieldTypes applicable for operator
Expand All @@ -67,32 +89,34 @@ export const isOperator = {
message: strings.getIsOperatorOptionLabel(),
type: FILTERS.PHRASE,
negate: false,
id: OPERATORS.IS,
};

export const isNotOperator = {
message: strings.getIsNotOperatorOptionLabel(),
type: FILTERS.PHRASE,
negate: true,
id: OPERATORS.IS_NOT,
};

export const isOneOfOperator = {
message: strings.getIsOneOfOperatorOptionLabel(),
type: FILTERS.PHRASES,
negate: false,
fieldTypes: ['string', 'number', 'date', 'ip', 'geo_point', 'geo_shape'],
id: OPERATORS.IS_ONE_OF,
};

export const isNotOneOfOperator = {
message: strings.getIsNotOneOfOperatorOptionLabel(),
type: FILTERS.PHRASES,
negate: true,
fieldTypes: ['string', 'number', 'date', 'ip', 'geo_point', 'geo_shape'],
id: OPERATORS.IS_NOT_ONE_OF,
};

export const isBetweenOperator = {
message: strings.getIsBetweenOperatorOptionLabel(),
const rangeOperatorsSharedProps = {
type: FILTERS.RANGE,
negate: false,
field: (field: DataViewField) => {
if (['number', 'number_range', 'date', 'date_range', 'ip', 'ip_range'].includes(field.type))
return true;
Expand All @@ -103,37 +127,55 @@ export const isBetweenOperator = {
},
};

export const isBetweenOperator = {
...rangeOperatorsSharedProps,
message: strings.getIsBetweenOperatorOptionLabel(),
id: OPERATORS.BETWEEN,
negate: false,
};

export const isLessThanOperator = {
...rangeOperatorsSharedProps,
message: strings.getLessThanOperatorOptionLabel(),
id: OPERATORS.LESS,
negate: false,
};

export const isGreaterOrEqualOperator = {
...rangeOperatorsSharedProps,
message: strings.getIsGreaterOrEqualOperatorOptionLabel(),
id: OPERATORS.GREATER_OR_EQUAL,
negate: false,
};

export const isNotBetweenOperator = {
...rangeOperatorsSharedProps,
message: strings.getIsNotBetweenOperatorOptionLabel(),
type: FILTERS.RANGE,
negate: true,
field: (field: DataViewField) => {
if (['number', 'number_range', 'date', 'date_range', 'ip', 'ip_range'].includes(field.type))
return true;

if (field.type === 'string' && field.esTypes?.includes(ES_FIELD_TYPES.VERSION)) return true;

return false;
},
id: OPERATORS.NOT_BETWEEN,
};

export const existsOperator = {
message: strings.getExistsOperatorOptionLabel(),
type: FILTERS.EXISTS,
negate: false,
id: OPERATORS.EXISTS,
};

export const doesNotExistOperator = {
message: strings.getDoesNotExistOperatorOptionLabel(),
type: FILTERS.EXISTS,
negate: true,
id: OPERATORS.DOES_NOT_EXIST,
};

export const FILTER_OPERATORS: Operator[] = [
isOperator,
isNotOperator,
isOneOfOperator,
isNotOneOfOperator,
isGreaterOrEqualOperator,
isLessThanOperator,
isBetweenOperator,
isNotBetweenOperator,
existsOperator,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { MIDDLE_TRUNCATION_PROPS, SINGLE_SELECTION_AS_TEXT_PROPS } from './lib/h
interface PhraseValueInputProps extends PhraseSuggestorProps {
value?: string;
onChange: (value: string | number | boolean) => void;
onBlur?: (value: string | number | boolean) => void;
intl: InjectedIntl;
fullWidth?: boolean;
compressed?: boolean;
Expand All @@ -43,6 +44,7 @@ class PhraseValueInputUI extends PhraseSuggestorUI<PhraseValueInputProps> {
id: 'unifiedSearch.filter.filterEditor.valueInputPlaceholder',
defaultMessage: 'Enter a value',
})}
onBlur={this.props.onBlur}
value={this.props.value}
onChange={this.props.onChange}
field={this.props.field}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ import { EuiFormControlLayoutDelimited } from '@elastic/eui';
import { InjectedIntl, injectI18n } from '@kbn/i18n-react';
import { get } from 'lodash';
import React from 'react';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { KibanaReactContextValue, useKibana } from '@kbn/kibana-react-plugin/public';
import type { DataViewField } from '@kbn/data-views-plugin/common';
import { CoreStart } from '@kbn/core/public';
import { ValueInputType } from './value_input_type';

interface RangeParams {
Expand All @@ -36,19 +37,22 @@ export function isRangeParams(params: any): params is RangeParams {
return Boolean(params && 'from' in params && 'to' in params);
}

function RangeValueInputUI(props: Props) {
const kibana = useKibana();
export const formatDateChange = (
value: string | number | boolean,
kibana: KibanaReactContextValue<Partial<CoreStart>>
) => {
if (typeof value !== 'string' && typeof value !== 'number') return value;

const formatDateChange = (value: string | number | boolean) => {
if (typeof value !== 'string' && typeof value !== 'number') return value;
const tzConfig = kibana.services.uiSettings!.get('dateFormat:tz');
const tz = !tzConfig || tzConfig === 'Browser' ? moment.tz.guess() : tzConfig;
const momentParsedValue = moment(value).tz(tz);
if (momentParsedValue.isValid()) return momentParsedValue?.format('YYYY-MM-DDTHH:mm:ss.SSSZ');

const tzConfig = kibana.services.uiSettings!.get('dateFormat:tz');
const tz = !tzConfig || tzConfig === 'Browser' ? moment.tz.guess() : tzConfig;
const momentParsedValue = moment(value).tz(tz);
if (momentParsedValue.isValid()) return momentParsedValue?.format('YYYY-MM-DDTHH:mm:ss.SSSZ');
return value;
};

return value;
};
function RangeValueInputUI(props: Props) {
const kibana = useKibana();

const onFromChange = (value: string | number | boolean) => {
if (typeof value !== 'string' && typeof value !== 'number') {
Expand Down Expand Up @@ -81,7 +85,7 @@ function RangeValueInputUI(props: Props) {
value={props.value ? props.value.from : undefined}
onChange={onFromChange}
onBlur={(value) => {
onFromChange(formatDateChange(value));
onFromChange(formatDateChange(value, kibana));
}}
placeholder={props.intl.formatMessage({
id: 'unifiedSearch.filter.filterEditor.rangeStartInputPlaceholder',
Expand All @@ -99,7 +103,7 @@ function RangeValueInputUI(props: Props) {
value={props.value ? props.value.to : undefined}
onChange={onToChange}
onBlur={(value) => {
onToChange(formatDateChange(value));
onToChange(formatDateChange(value, kibana));
}}
placeholder={props.intl.formatMessage({
id: 'unifiedSearch.filter.filterEditor.rangeEndInputPlaceholder',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,22 +105,26 @@ export function FilterItem({
const conditionalOperationType = getBooleanRelationType(filter);
const { euiTheme } = useEuiTheme();
let field: DataViewField | undefined;
let operator: Operator | undefined;
let params: Filter['meta']['params'];
const isMaxNesting = isMaxFilterNesting(path);
if (!conditionalOperationType) {
field = getFieldFromFilter(filter, dataView!);
if (field) {
operator = getOperatorFromFilter(filter);
params = getFilterParams(filter);
}
}
const [operator, setOperator] = useState<Operator | undefined>(() => {
if (!conditionalOperationType && field) {
return getOperatorFromFilter(filter);
}
});
const [multiValueFilterParams, setMultiValueFilterParams] = useState<
Array<Filter | boolean | string | number>
>(Array.isArray(params) ? params : []);

const onHandleField = useCallback(
(selectedField: DataViewField) => {
setOperator(undefined);
dispatch({
type: 'updateFilter',
payload: { dest: { path, index }, field: selectedField },
Expand All @@ -131,6 +135,7 @@ export function FilterItem({

const onHandleOperator = useCallback(
(selectedOperator: Operator) => {
setOperator(selectedOperator);
dispatch({
type: 'updateFilter',
payload: { dest: { path, index }, field, operator: selectedOperator },
Expand Down
Loading

0 comments on commit 921a1af

Please sign in to comment.