diff --git a/packages/kbn-react-field/src/field_icon/__snapshots__/field_icon.test.tsx.snap b/packages/kbn-react-field/src/field_icon/__snapshots__/field_icon.test.tsx.snap
index 0e9ae4ee2aaaa..cd81705dd3c19 100644
--- a/packages/kbn-react-field/src/field_icon/__snapshots__/field_icon.test.tsx.snap
+++ b/packages/kbn-react-field/src/field_icon/__snapshots__/field_icon.test.tsx.snap
@@ -195,6 +195,16 @@ exports[`FieldIcon renders known field types text is rendered 1`] = `
/>
`;
+exports[`FieldIcon renders known field types version is rendered 1`] = `
+
+`;
+
exports[`FieldIcon renders with className if provided 1`] = `
{
| '_source'
| 'string'
| string
- | 'nested';
+ | 'nested'
+ | 'version';
label?: string;
scripted?: boolean;
}
@@ -54,6 +55,7 @@ export const typeToEuiIconMap: Partial> = {
text: { iconType: 'tokenString' },
keyword: { iconType: 'tokenKeyword' },
nested: { iconType: 'tokenNested' },
+ version: { iconType: 'tokenTag' },
};
/**
diff --git a/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts b/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts
index 31f886daeb4cc..f88fd3b9a0157 100644
--- a/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts
+++ b/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts
@@ -103,9 +103,16 @@ export const setupValueSuggestionProvider = (
useTimeRange ?? core!.uiSettings.get(UI_SETTINGS.AUTOCOMPLETE_USE_TIMERANGE);
const { title } = indexPattern;
+ const isVersionFieldType = field.type === 'string' && field.esTypes?.includes('version');
+
if (field.type === 'boolean') {
return [true, false];
- } else if (!shouldSuggestValues || !field.aggregatable || field.type !== 'string') {
+ } else if (
+ !shouldSuggestValues ||
+ !field.aggregatable ||
+ field.type !== 'string' ||
+ isVersionFieldType // suggestions don't work for version fields
+ ) {
return [];
}
diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx
index 726a4bf29a43c..35ac8f386946e 100644
--- a/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx
+++ b/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx
@@ -345,7 +345,7 @@ class FilterEditorUI extends Component {
private renderParamsEditor() {
const indexPattern = this.state.selectedIndexPattern;
- if (!indexPattern || !this.state.selectedOperator) {
+ if (!indexPattern || !this.state.selectedOperator || !this.state.selectedField) {
return '';
}
diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_editor_utils.ts b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_editor_utils.ts
index ea172c2ccac35..7d433bb1f273b 100644
--- a/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_editor_utils.ts
+++ b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_editor_utils.ts
@@ -8,6 +8,8 @@
import dateMath from '@elastic/datemath';
import { Filter, FieldFilter } from '@kbn/es-query';
+import { ES_FIELD_TYPES } from '@kbn/field-types';
+import isSemverValid from 'semver/functions/valid';
import { FILTER_OPERATORS, Operator } from './filter_operators';
import { isFilterable, IIndexPattern, IFieldType, IpAddress } from '../../../../../common';
@@ -27,12 +29,14 @@ export function getFilterableFields(indexPattern: IIndexPattern) {
export function getOperatorOptions(field: IFieldType) {
return FILTER_OPERATORS.filter((operator) => {
- return !operator.fieldTypes || operator.fieldTypes.includes(field.type);
+ if (operator.field) return operator.field(field);
+ if (operator.fieldTypes) return operator.fieldTypes.includes(field.type);
+ return true;
});
}
-export function validateParams(params: any, type: string) {
- switch (type) {
+export function validateParams(params: any, field: IFieldType) {
+ switch (field.type) {
case 'date':
const moment = typeof params === 'string' ? dateMath.parse(params) : null;
return Boolean(typeof params === 'string' && moment && moment.isValid());
@@ -42,6 +46,11 @@ export function validateParams(params: any, type: string) {
} catch (e) {
return false;
}
+ case 'string':
+ if (field.esTypes?.includes(ES_FIELD_TYPES.VERSION)) {
+ return isSemverValid(params);
+ }
+ return true;
default:
return true;
}
@@ -58,19 +67,19 @@ export function isFilterValid(
}
switch (operator.type) {
case 'phrase':
- return validateParams(params, field.type);
+ return validateParams(params, field);
case 'phrases':
if (!Array.isArray(params) || !params.length) {
return false;
}
- return params.every((phrase) => validateParams(phrase, field.type));
+ return params.every((phrase) => validateParams(phrase, field));
case 'range':
if (typeof params !== 'object') {
return false;
}
return (
- (!params.from || validateParams(params.from, field.type)) &&
- (!params.to || validateParams(params.to, field.type))
+ (!params.from || validateParams(params.from, field)) &&
+ (!params.to || validateParams(params.to, field))
);
case 'exists':
return true;
diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_operators.ts b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_operators.ts
index bc3f01aeb3c8f..0be9c200c8d82 100644
--- a/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_operators.ts
+++ b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_operators.ts
@@ -8,12 +8,24 @@
import { i18n } from '@kbn/i18n';
import { FILTERS } from '@kbn/es-query';
+import { ES_FIELD_TYPES } from '@kbn/field-types';
+import { IFieldType } from '../../../../../../data_views/common';
export interface Operator {
message: string;
type: FILTERS;
negate: boolean;
+
+ /**
+ * KbnFieldTypes applicable for operator
+ */
fieldTypes?: string[];
+
+ /**
+ * A filter predicate for a field,
+ * takes precedence over {@link fieldTypes}
+ */
+ field?: (field: IFieldType) => boolean;
}
export const isOperator = {
@@ -56,7 +68,14 @@ export const isBetweenOperator = {
}),
type: FILTERS.RANGE,
negate: false,
- fieldTypes: ['number', 'number_range', 'date', 'date_range', 'ip', 'ip_range'],
+ field: (field: IFieldType) => {
+ 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;
+ },
};
export const isNotBetweenOperator = {
@@ -65,7 +84,14 @@ export const isNotBetweenOperator = {
}),
type: FILTERS.RANGE,
negate: true,
- fieldTypes: ['number', 'number_range', 'date', 'date_range', 'ip', 'ip_range'],
+ field: (field: IFieldType) => {
+ 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;
+ },
};
export const existsOperator = {
diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_suggestor.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_suggestor.tsx
index fb3106e6a8f06..ba39ee78dafa4 100644
--- a/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_suggestor.tsx
+++ b/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_suggestor.tsx
@@ -16,7 +16,7 @@ import { UI_SETTINGS } from '../../../../common';
export interface PhraseSuggestorProps {
kibana: KibanaReactContextValue;
indexPattern: IIndexPattern;
- field?: IFieldType;
+ field: IFieldType;
timeRangeForSuggestionsOverride?: boolean;
}
@@ -54,7 +54,15 @@ export class PhraseSuggestorUI extends React.Com
UI_SETTINGS.FILTERS_EDITOR_SUGGEST_VALUES
);
const { field } = this.props;
- return shouldSuggestValues && field && field.aggregatable && field.type === 'string';
+ const isVersionFieldType = field?.esTypes?.includes('version');
+
+ return (
+ shouldSuggestValues &&
+ field &&
+ field.aggregatable &&
+ field.type === 'string' &&
+ !isVersionFieldType // suggestions don't work for version fields
+ );
}
protected onSearchChange = (value: string | number | boolean) => {
diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_value_input.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_value_input.tsx
index 8690387fe61ed..32a713361dbd1 100644
--- a/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_value_input.tsx
+++ b/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_value_input.tsx
@@ -43,7 +43,7 @@ class PhraseValueInputUI extends PhraseSuggestorUI {
})}
value={this.props.value}
onChange={this.props.onChange}
- type={this.props.field ? this.props.field.type : 'string'}
+ field={this.props.field}
/>
)}
diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/range_value_input.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/range_value_input.tsx
index 27642e62a9618..6561761dc623b 100644
--- a/src/plugins/data/public/ui/filter_bar/filter_editor/range_value_input.tsx
+++ b/src/plugins/data/public/ui/filter_bar/filter_editor/range_value_input.tsx
@@ -23,7 +23,7 @@ interface RangeParams {
type RangeParamsPartial = Partial;
interface Props {
- field?: IFieldType;
+ field: IFieldType;
value?: RangeParams;
onChange: (params: RangeParamsPartial) => void;
intl: InjectedIntl;
@@ -32,7 +32,6 @@ interface Props {
function RangeValueInputUI(props: Props) {
const kibana = useKibana();
- const type = props.field ? props.field.type : 'string';
const tzConfig = kibana.services.uiSettings!.get('dateFormat:tz');
const formatDateChange = (value: string | number | boolean) => {
@@ -69,7 +68,7 @@ function RangeValueInputUI(props: Props) {
startControl={
{
@@ -84,7 +83,7 @@ function RangeValueInputUI(props: Props) {
endControl={
{
diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/value_input_type.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/value_input_type.tsx
index b72743ae9d2d9..9b00c71472f37 100644
--- a/src/plugins/data/public/ui/filter_bar/filter_editor/value_input_type.tsx
+++ b/src/plugins/data/public/ui/filter_bar/filter_editor/value_input_type.tsx
@@ -11,10 +11,11 @@ import { InjectedIntl, injectI18n } from '@kbn/i18n-react';
import { isEmpty } from 'lodash';
import React, { Component } from 'react';
import { validateParams } from './lib/filter_editor_utils';
+import { IFieldType } from '../../../../../data_views/common';
interface Props {
value?: string | number;
- type: string;
+ field: IFieldType;
onChange: (value: string | number | boolean) => void;
onBlur?: (value: string | number | boolean) => void;
placeholder: string;
@@ -27,8 +28,9 @@ interface Props {
class ValueInputTypeUI extends Component {
public render() {
const value = this.props.value;
+ const type = this.props.field.type;
let inputElement: React.ReactNode;
- switch (this.props.type) {
+ switch (type) {
case 'string':
inputElement = (
{
placeholder={this.props.placeholder}
value={value}
onChange={this.onChange}
+ isInvalid={!validateParams(value, this.props.field)}
controlOnly={this.props.controlOnly}
className={this.props.className}
/>
@@ -63,7 +66,7 @@ class ValueInputTypeUI extends Component {
value={value}
onChange={this.onChange}
onBlur={this.onBlur}
- isInvalid={!isEmpty(value) && !validateParams(value, this.props.type)}
+ isInvalid={!isEmpty(value) && !validateParams(value, this.props.field)}
controlOnly={this.props.controlOnly}
className={this.props.className}
/>
@@ -77,7 +80,7 @@ class ValueInputTypeUI extends Component {
placeholder={this.props.placeholder}
value={value}
onChange={this.onChange}
- isInvalid={!isEmpty(value) && !validateParams(value, this.props.type)}
+ isInvalid={!isEmpty(value) && !validateParams(value, this.props.field)}
controlOnly={this.props.controlOnly}
className={this.props.className}
/>
diff --git a/src/plugins/discover/public/application/main/components/sidebar/lib/get_field_type_name.ts b/src/plugins/discover/public/application/main/components/sidebar/lib/get_field_type_name.ts
index f68395593bd8b..731f860058737 100644
--- a/src/plugins/discover/public/application/main/components/sidebar/lib/get_field_type_name.ts
+++ b/src/plugins/discover/public/application/main/components/sidebar/lib/get_field_type_name.ts
@@ -64,6 +64,10 @@ export function getFieldTypeName(type: string) {
return i18n.translate('discover.fieldNameIcons.nestedFieldAriaLabel', {
defaultMessage: 'Nested field',
});
+ case 'version':
+ return i18n.translate('discover.fieldNameIcons.versionFieldAriaLabel', {
+ defaultMessage: 'Version field',
+ });
default:
return i18n.translate('discover.fieldNameIcons.unknownFieldAriaLabel', {
defaultMessage: 'Unknown field',
diff --git a/test/functional/apps/discover/_filter_editor.ts b/test/functional/apps/discover/_filter_editor.ts
index 8bcb4382bb3bf..0fb8837598904 100644
--- a/test/functional/apps/discover/_filter_editor.ts
+++ b/test/functional/apps/discover/_filter_editor.ts
@@ -56,6 +56,60 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(await PageObjects.discover.getHitCount()).to.be('1');
});
});
+
+ describe('version fields', async () => {
+ const es = getService('es');
+ const indexPatterns = getService('indexPatterns');
+ const indexTitle = 'version-test';
+
+ before(async () => {
+ if (await es.indices.exists({ index: indexTitle })) {
+ await es.indices.delete({ index: indexTitle });
+ }
+
+ await es.indices.create({
+ index: indexTitle,
+ body: {
+ mappings: {
+ properties: {
+ version: {
+ type: 'version',
+ },
+ },
+ },
+ },
+ });
+
+ await es.index({
+ index: indexTitle,
+ body: {
+ version: '1.0.0',
+ },
+ refresh: 'wait_for',
+ });
+
+ await es.index({
+ index: indexTitle,
+ body: {
+ version: '2.0.0',
+ },
+ refresh: 'wait_for',
+ });
+
+ await indexPatterns.create({ title: indexTitle }, { override: true });
+
+ await PageObjects.common.navigateToApp('discover');
+ await PageObjects.discover.selectIndexPattern(indexTitle);
+ });
+
+ it('should support range filter on version fields', async () => {
+ await filterBar.addFilter('version', 'is between', '2.0.0', '3.0.0');
+ expect(await filterBar.hasFilter('version', '2.0.0 to 3.0.0')).to.be(true);
+ await retry.try(async function () {
+ expect(await PageObjects.discover.getHitCount()).to.be('1');
+ });
+ });
+ });
});
});
}