mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[ES|QL] [Discover] Creating where clause filters from the table, sidebar and table row viewer (#181399)
## Summary
Part of https://github.com/elastic/kibana/issues/181280
This PR handles the creation of filters (where clause) in Discover ES|QL
mode. The parts that are being handled are:
- sidebar
- document viewer
- table

The creation of filters from the charts is not here.
### Checklist
Delete any items that are not applicable to this PR.
- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [x] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [x] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [x] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [x] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [x] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
470ec04b2d
commit
b64500b9e6
23 changed files with 549 additions and 585 deletions
|
@ -6,4 +6,6 @@ This package contains utilities for ES|QL.
|
|||
- *getIndexPatternFromESQLQuery*: Use this to retrieve the index pattern from the `from` command.
|
||||
- *getLimitFromESQLQuery*: Use this function to get the limit for a given query. The limit can be either set from the `limit` command or can be a default value set in ES.
|
||||
- *removeDropCommandsFromESQLQuery*: Use this function to remove all the occurences of the `drop` command from the query.
|
||||
- *appendToESQLQuery*: Use this function to append more pipes in an existing ES|QL query. It adds the additional commands in a new line.
|
||||
- *appendWhereClauseToESQLQuery*: Use this function to append where clause in an existing query.
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ export {
|
|||
getInitialESQLQuery,
|
||||
getESQLWithSafeLimit,
|
||||
appendToESQLQuery,
|
||||
appendWhereClauseToESQLQuery,
|
||||
getESQLQueryColumns,
|
||||
TextBasedLanguages,
|
||||
} from './src';
|
||||
|
|
|
@ -15,5 +15,5 @@ export {
|
|||
getLimitFromESQLQuery,
|
||||
removeDropCommandsFromESQLQuery,
|
||||
} from './utils/query_parsing_helpers';
|
||||
export { appendToESQLQuery } from './utils/append_to_query';
|
||||
export { appendToESQLQuery, appendWhereClauseToESQLQuery } from './utils/append_to_query';
|
||||
export { getESQLQueryColumns } from './utils/run_query';
|
||||
|
|
|
@ -5,21 +5,151 @@
|
|||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import { appendToESQLQuery } from './append_to_query';
|
||||
import { appendToESQLQuery, appendWhereClauseToESQLQuery } from './append_to_query';
|
||||
|
||||
describe('appendToESQLQuery', () => {
|
||||
it('append the text on a new line after the query', () => {
|
||||
expect(appendToESQLQuery('from logstash-* // meow', '| stats var = avg(woof)')).toBe(
|
||||
`from logstash-* // meow
|
||||
describe('appendToQuery', () => {
|
||||
describe('appendToESQLQuery', () => {
|
||||
it('append the text on a new line after the query', () => {
|
||||
expect(appendToESQLQuery('from logstash-* // meow', '| stats var = avg(woof)')).toBe(
|
||||
`from logstash-* // meow
|
||||
| stats var = avg(woof)`
|
||||
);
|
||||
);
|
||||
});
|
||||
|
||||
it('append the text on a new line after the query for text with variables', () => {
|
||||
const limit = 10;
|
||||
expect(appendToESQLQuery('from logstash-*', `| limit ${limit}`)).toBe(
|
||||
`from logstash-*
|
||||
| limit 10`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('append the text on a new line after the query for text with variables', () => {
|
||||
const limit = 10;
|
||||
expect(appendToESQLQuery('from logstash-*', `| limit ${limit}`)).toBe(
|
||||
`from logstash-*
|
||||
| limit 10`
|
||||
);
|
||||
describe('appendWhereClauseToESQLQuery', () => {
|
||||
it('appends a filter in where clause in an existing query', () => {
|
||||
expect(
|
||||
appendWhereClauseToESQLQuery('from logstash-* // meow', 'dest', 'tada!', '+', 'string')
|
||||
).toBe(
|
||||
`from logstash-* // meow
|
||||
| where \`dest\`=="tada!"`
|
||||
);
|
||||
});
|
||||
it('appends a filter out where clause in an existing query', () => {
|
||||
expect(
|
||||
appendWhereClauseToESQLQuery('from logstash-* // meow', 'dest', 'tada!', '-', 'string')
|
||||
).toBe(
|
||||
`from logstash-* // meow
|
||||
| where \`dest\`!="tada!"`
|
||||
);
|
||||
});
|
||||
|
||||
it('appends a where clause in an existing query with casting to string when the type is not string or number', () => {
|
||||
expect(
|
||||
appendWhereClauseToESQLQuery('from logstash-* // meow', 'dest', 'tada!', '-', 'ip')
|
||||
).toBe(
|
||||
`from logstash-* // meow
|
||||
| where \`dest\`::string!="tada!"`
|
||||
);
|
||||
});
|
||||
|
||||
it('appends a where clause in an existing query with casting to string when the type is not given', () => {
|
||||
expect(appendWhereClauseToESQLQuery('from logstash-* // meow', 'dest', 'tada!', '-')).toBe(
|
||||
`from logstash-* // meow
|
||||
| where \`dest\`::string!="tada!"`
|
||||
);
|
||||
});
|
||||
|
||||
it('appends a where clause in an existing query checking that the value is not null if the user asks for existence', () => {
|
||||
expect(
|
||||
appendWhereClauseToESQLQuery(
|
||||
'from logstash-* // meow',
|
||||
'dest',
|
||||
undefined,
|
||||
'_exists_',
|
||||
'string'
|
||||
)
|
||||
).toBe(
|
||||
`from logstash-* // meow
|
||||
| where \`dest\` is not null`
|
||||
);
|
||||
});
|
||||
|
||||
it('appends an and clause in an existing query with where command as the last pipe', () => {
|
||||
expect(
|
||||
appendWhereClauseToESQLQuery(
|
||||
'from logstash-* | where country == "GR"',
|
||||
'dest',
|
||||
'Crete',
|
||||
'+',
|
||||
'string'
|
||||
)
|
||||
).toBe(
|
||||
`from logstash-* | where country == "GR"
|
||||
and \`dest\`=="Crete"`
|
||||
);
|
||||
});
|
||||
|
||||
it('doesnt append anything in an existing query with where command as the last pipe if the filter preexists', () => {
|
||||
expect(
|
||||
appendWhereClauseToESQLQuery(
|
||||
'from logstash-* | where country == "GR"',
|
||||
'country',
|
||||
'GR',
|
||||
'+',
|
||||
'string'
|
||||
)
|
||||
).toBe(`from logstash-* | where country == "GR"`);
|
||||
});
|
||||
|
||||
it('doesnt append anything in an existing query with where command as the last pipe if the _exists_ filter preexists', () => {
|
||||
expect(
|
||||
appendWhereClauseToESQLQuery(
|
||||
'from logstash-* | where country IS NOT NULL',
|
||||
'country',
|
||||
undefined,
|
||||
'_exists_',
|
||||
'string'
|
||||
)
|
||||
).toBe(`from logstash-* | where country IS NOT NULL`);
|
||||
});
|
||||
|
||||
it('changes the operator in an existing query with where command as the last pipe if the filter preexists but has the opposite operator', () => {
|
||||
expect(
|
||||
appendWhereClauseToESQLQuery(
|
||||
'from logstash-* | where country == "GR"',
|
||||
'country',
|
||||
'GR',
|
||||
'-',
|
||||
'string'
|
||||
)
|
||||
).toBe(`from logstash-* | where country != "GR"`);
|
||||
});
|
||||
|
||||
it('changes the operator in an existing query with where command as the last pipe if the filter preexists but has the opposite operator, the field has backticks', () => {
|
||||
expect(
|
||||
appendWhereClauseToESQLQuery(
|
||||
'from logstash-* | where `country` == "GR"',
|
||||
'country',
|
||||
'GR',
|
||||
'-',
|
||||
'string'
|
||||
)
|
||||
).toBe(`from logstash-* | where \`country\`!= "GR"`);
|
||||
});
|
||||
|
||||
it('appends an and clause in an existing query with where command as the last pipe if the filter preexists but the operator is not the correct one', () => {
|
||||
expect(
|
||||
appendWhereClauseToESQLQuery(
|
||||
`from logstash-* | where CIDR_MATCH(ip1, "127.0.0.2/32", "127.0.0.3/32")`,
|
||||
'ip',
|
||||
'127.0.0.2/32',
|
||||
'-',
|
||||
'ip'
|
||||
)
|
||||
).toBe(
|
||||
`from logstash-* | where CIDR_MATCH(ip1, "127.0.0.2/32", "127.0.0.3/32")
|
||||
and \`ip\`::string!="127.0.0.2/32"`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,9 +5,86 @@
|
|||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import { getAstAndSyntaxErrors } from '@kbn/esql-ast';
|
||||
|
||||
// Append in a new line the appended text to take care of the case where the user adds a comment at the end of the query
|
||||
// in these cases a base query such as "from index // comment" will result in errors or wrong data if we don't append in a new line
|
||||
export function appendToESQLQuery(baseESQLQuery: string, appendedText: string): string {
|
||||
return `${baseESQLQuery}\n${appendedText}`;
|
||||
}
|
||||
|
||||
export function appendWhereClauseToESQLQuery(
|
||||
baseESQLQuery: string,
|
||||
field: string,
|
||||
value: unknown,
|
||||
operation: '+' | '-' | '_exists_',
|
||||
fieldType?: string
|
||||
): string {
|
||||
let operator;
|
||||
switch (operation) {
|
||||
case '_exists_':
|
||||
operator = ' is not null';
|
||||
break;
|
||||
case '-':
|
||||
operator = '!=';
|
||||
break;
|
||||
default:
|
||||
operator = '==';
|
||||
}
|
||||
let filterValue = typeof value === 'string' ? `"${value.replace(/"/g, '\\"')}"` : value;
|
||||
// Adding the backticks here are they are needed for special char fields
|
||||
let fieldName = `\`${field}\``;
|
||||
|
||||
// casting to string
|
||||
// there are some field types such as the ip that need
|
||||
// to cast in string first otherwise ES will fail
|
||||
if (fieldType !== 'string' && fieldType !== 'number' && fieldType !== 'boolean') {
|
||||
fieldName = `${fieldName}::string`;
|
||||
}
|
||||
|
||||
// checking that the value is not null
|
||||
// this is the existence filter
|
||||
if (operation === '_exists_') {
|
||||
fieldName = `\`${String(field)}\``;
|
||||
filterValue = '';
|
||||
}
|
||||
|
||||
const { ast } = getAstAndSyntaxErrors(baseESQLQuery);
|
||||
|
||||
const lastCommandIsWhere = ast[ast.length - 1].name === 'where';
|
||||
// if where command already exists in the end of the query:
|
||||
// - we need to append with and if the filter doesnt't exist
|
||||
// - we need to change the filter operator if the filter exists with different operator
|
||||
// - we do nothing if the filter exists with the same operator
|
||||
if (lastCommandIsWhere) {
|
||||
const whereCommand = ast[ast.length - 1];
|
||||
const whereAstText = whereCommand.text;
|
||||
// the filter already exists in the where clause
|
||||
if (whereAstText.includes(field) && whereAstText.includes(String(filterValue))) {
|
||||
const pipesArray = baseESQLQuery.split('|');
|
||||
const whereClause = pipesArray[pipesArray.length - 1];
|
||||
|
||||
const matches = whereClause.match(new RegExp(field + '(.*)' + String(filterValue)));
|
||||
if (matches) {
|
||||
const existingOperator = matches[1]?.trim().replace('`', '').toLowerCase();
|
||||
if (!['==', '!=', 'is not null'].includes(existingOperator.trim())) {
|
||||
return appendToESQLQuery(baseESQLQuery, `and ${fieldName}${operator}${filterValue}`);
|
||||
}
|
||||
// the filter is the same
|
||||
if (existingOperator === operator.trim()) {
|
||||
return baseESQLQuery;
|
||||
// the filter has different operator
|
||||
} else {
|
||||
const existingFilter = matches[0].trim();
|
||||
const newFilter = existingFilter.replace(existingOperator, operator);
|
||||
return baseESQLQuery.replace(existingFilter, newFilter);
|
||||
}
|
||||
}
|
||||
}
|
||||
// filter does not exist in the where clause
|
||||
const whereClause = `and ${fieldName}${operator}${filterValue}`;
|
||||
return appendToESQLQuery(baseESQLQuery, whereClause);
|
||||
}
|
||||
const whereClause = `| where ${fieldName}${operator}${filterValue}`;
|
||||
return appendToESQLQuery(baseESQLQuery, whereClause);
|
||||
}
|
||||
|
|
|
@ -96,10 +96,23 @@ Array [
|
|||
},
|
||||
"cellActions": Array [
|
||||
[Function],
|
||||
[Function],
|
||||
[Function],
|
||||
],
|
||||
"display": <Memo(DataTableColumnHeader)
|
||||
columnDisplayName="message"
|
||||
columnName="message"
|
||||
columnsMeta={
|
||||
Object {
|
||||
"extension": Object {
|
||||
"type": "string",
|
||||
},
|
||||
"message": Object {
|
||||
"esType": "keyword",
|
||||
"type": "string",
|
||||
},
|
||||
}
|
||||
}
|
||||
dataView={
|
||||
Object {
|
||||
"docvalueFields": Array [],
|
||||
|
@ -177,39 +190,6 @@ Array [
|
|||
Array [
|
||||
"message",
|
||||
],
|
||||
Array [
|
||||
"timestamp",
|
||||
],
|
||||
Array [
|
||||
"extension",
|
||||
],
|
||||
Array [
|
||||
"message",
|
||||
],
|
||||
Array [
|
||||
"extension",
|
||||
],
|
||||
Array [
|
||||
"message",
|
||||
],
|
||||
Array [
|
||||
"timestamp",
|
||||
],
|
||||
Array [
|
||||
"extension",
|
||||
],
|
||||
Array [
|
||||
"timestamp",
|
||||
],
|
||||
Array [
|
||||
"var_test",
|
||||
],
|
||||
Array [
|
||||
"extension",
|
||||
],
|
||||
Array [
|
||||
"message",
|
||||
],
|
||||
Array [
|
||||
"extension",
|
||||
],
|
||||
|
@ -305,120 +285,6 @@ Array [
|
|||
"type": "string",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Object {
|
||||
"aggregatable": true,
|
||||
"displayName": "timestamp",
|
||||
"filterable": true,
|
||||
"name": "timestamp",
|
||||
"scripted": false,
|
||||
"sortable": true,
|
||||
"type": "date",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Object {
|
||||
"aggregatable": true,
|
||||
"displayName": "extension",
|
||||
"filterable": true,
|
||||
"name": "extension",
|
||||
"scripted": false,
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Object {
|
||||
"displayName": "message",
|
||||
"filterable": false,
|
||||
"name": "message",
|
||||
"scripted": false,
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Object {
|
||||
"aggregatable": true,
|
||||
"displayName": "extension",
|
||||
"filterable": true,
|
||||
"name": "extension",
|
||||
"scripted": false,
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Object {
|
||||
"displayName": "message",
|
||||
"filterable": false,
|
||||
"name": "message",
|
||||
"scripted": false,
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Object {
|
||||
"aggregatable": true,
|
||||
"displayName": "timestamp",
|
||||
"filterable": true,
|
||||
"name": "timestamp",
|
||||
"scripted": false,
|
||||
"sortable": true,
|
||||
"type": "date",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Object {
|
||||
"aggregatable": true,
|
||||
"displayName": "extension",
|
||||
"filterable": true,
|
||||
"name": "extension",
|
||||
"scripted": false,
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Object {
|
||||
"aggregatable": true,
|
||||
"displayName": "timestamp",
|
||||
"filterable": true,
|
||||
"name": "timestamp",
|
||||
"scripted": false,
|
||||
"sortable": true,
|
||||
"type": "date",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": undefined,
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Object {
|
||||
"aggregatable": true,
|
||||
"displayName": "extension",
|
||||
"filterable": true,
|
||||
"name": "extension",
|
||||
"scripted": false,
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Object {
|
||||
"displayName": "message",
|
||||
"filterable": false,
|
||||
"name": "message",
|
||||
"scripted": false,
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Object {
|
||||
|
@ -595,15 +461,6 @@ Array [
|
|||
Array [
|
||||
"message",
|
||||
],
|
||||
Array [
|
||||
"timestamp",
|
||||
],
|
||||
Array [
|
||||
"extension",
|
||||
],
|
||||
Array [
|
||||
"message",
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
|
@ -693,39 +550,6 @@ Array [
|
|||
"type": "string",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Object {
|
||||
"aggregatable": true,
|
||||
"displayName": "timestamp",
|
||||
"filterable": true,
|
||||
"name": "timestamp",
|
||||
"scripted": false,
|
||||
"sortable": true,
|
||||
"type": "date",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Object {
|
||||
"aggregatable": true,
|
||||
"displayName": "extension",
|
||||
"filterable": true,
|
||||
"name": "extension",
|
||||
"scripted": false,
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Object {
|
||||
"displayName": "message",
|
||||
"filterable": false,
|
||||
"name": "message",
|
||||
"scripted": false,
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
"getFormatterForField": [MockFunction],
|
||||
|
@ -893,15 +717,6 @@ Array [
|
|||
Array [
|
||||
"message",
|
||||
],
|
||||
Array [
|
||||
"timestamp",
|
||||
],
|
||||
Array [
|
||||
"extension",
|
||||
],
|
||||
Array [
|
||||
"message",
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
|
@ -991,39 +806,6 @@ Array [
|
|||
"type": "string",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Object {
|
||||
"aggregatable": true,
|
||||
"displayName": "timestamp",
|
||||
"filterable": true,
|
||||
"name": "timestamp",
|
||||
"scripted": false,
|
||||
"sortable": true,
|
||||
"type": "date",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Object {
|
||||
"aggregatable": true,
|
||||
"displayName": "extension",
|
||||
"filterable": true,
|
||||
"name": "extension",
|
||||
"scripted": false,
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Object {
|
||||
"displayName": "message",
|
||||
"filterable": false,
|
||||
"name": "message",
|
||||
"scripted": false,
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
"getFormatterForField": [MockFunction],
|
||||
|
@ -1178,15 +960,6 @@ Array [
|
|||
Array [
|
||||
"message",
|
||||
],
|
||||
Array [
|
||||
"timestamp",
|
||||
],
|
||||
Array [
|
||||
"extension",
|
||||
],
|
||||
Array [
|
||||
"message",
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
|
@ -1276,39 +1049,6 @@ Array [
|
|||
"type": "string",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Object {
|
||||
"aggregatable": true,
|
||||
"displayName": "timestamp",
|
||||
"filterable": true,
|
||||
"name": "timestamp",
|
||||
"scripted": false,
|
||||
"sortable": true,
|
||||
"type": "date",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Object {
|
||||
"aggregatable": true,
|
||||
"displayName": "extension",
|
||||
"filterable": true,
|
||||
"name": "extension",
|
||||
"scripted": false,
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Object {
|
||||
"displayName": "message",
|
||||
"filterable": false,
|
||||
"name": "message",
|
||||
"scripted": false,
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
"getFormatterForField": [MockFunction],
|
||||
|
@ -1396,7 +1136,7 @@ Array [
|
|||
columnsMeta={
|
||||
Object {
|
||||
"extension": Object {
|
||||
"type": "number",
|
||||
"type": "string",
|
||||
},
|
||||
"message": Object {
|
||||
"esType": "keyword",
|
||||
|
@ -1481,15 +1221,6 @@ Array [
|
|||
Array [
|
||||
"message",
|
||||
],
|
||||
Array [
|
||||
"timestamp",
|
||||
],
|
||||
Array [
|
||||
"extension",
|
||||
],
|
||||
Array [
|
||||
"message",
|
||||
],
|
||||
Array [
|
||||
"extension",
|
||||
],
|
||||
|
@ -1585,39 +1316,6 @@ Array [
|
|||
"type": "string",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Object {
|
||||
"aggregatable": true,
|
||||
"displayName": "timestamp",
|
||||
"filterable": true,
|
||||
"name": "timestamp",
|
||||
"scripted": false,
|
||||
"sortable": true,
|
||||
"type": "date",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Object {
|
||||
"aggregatable": true,
|
||||
"displayName": "extension",
|
||||
"filterable": true,
|
||||
"name": "extension",
|
||||
"scripted": false,
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Object {
|
||||
"displayName": "message",
|
||||
"filterable": false,
|
||||
"name": "message",
|
||||
"scripted": false,
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Object {
|
||||
|
@ -1668,7 +1366,7 @@ Array [
|
|||
"displayAsText": "extension",
|
||||
"id": "extension",
|
||||
"isSortable": false,
|
||||
"schema": "numeric",
|
||||
"schema": "string",
|
||||
"visibleCellActions": undefined,
|
||||
},
|
||||
Object {
|
||||
|
@ -1719,7 +1417,7 @@ Array [
|
|||
columnsMeta={
|
||||
Object {
|
||||
"extension": Object {
|
||||
"type": "number",
|
||||
"type": "string",
|
||||
},
|
||||
"message": Object {
|
||||
"esType": "keyword",
|
||||
|
@ -1804,15 +1502,6 @@ Array [
|
|||
Array [
|
||||
"message",
|
||||
],
|
||||
Array [
|
||||
"timestamp",
|
||||
],
|
||||
Array [
|
||||
"extension",
|
||||
],
|
||||
Array [
|
||||
"message",
|
||||
],
|
||||
Array [
|
||||
"extension",
|
||||
],
|
||||
|
@ -1908,39 +1597,6 @@ Array [
|
|||
"type": "string",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Object {
|
||||
"aggregatable": true,
|
||||
"displayName": "timestamp",
|
||||
"filterable": true,
|
||||
"name": "timestamp",
|
||||
"scripted": false,
|
||||
"sortable": true,
|
||||
"type": "date",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Object {
|
||||
"aggregatable": true,
|
||||
"displayName": "extension",
|
||||
"filterable": true,
|
||||
"name": "extension",
|
||||
"scripted": false,
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Object {
|
||||
"displayName": "message",
|
||||
"filterable": false,
|
||||
"name": "message",
|
||||
"scripted": false,
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Object {
|
||||
|
@ -3042,15 +2698,6 @@ Array [
|
|||
Array [
|
||||
"message",
|
||||
],
|
||||
Array [
|
||||
"timestamp",
|
||||
],
|
||||
Array [
|
||||
"extension",
|
||||
],
|
||||
Array [
|
||||
"message",
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
|
@ -3107,39 +2754,6 @@ Array [
|
|||
"type": "string",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Object {
|
||||
"aggregatable": true,
|
||||
"displayName": "timestamp",
|
||||
"filterable": true,
|
||||
"name": "timestamp",
|
||||
"scripted": false,
|
||||
"sortable": true,
|
||||
"type": "date",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Object {
|
||||
"aggregatable": true,
|
||||
"displayName": "extension",
|
||||
"filterable": true,
|
||||
"name": "extension",
|
||||
"scripted": false,
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Object {
|
||||
"displayName": "message",
|
||||
"filterable": false,
|
||||
"name": "message",
|
||||
"scripted": false,
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
"getFormatterForField": [MockFunction],
|
||||
|
@ -3165,12 +2779,22 @@ Array [
|
|||
}
|
||||
dataViewField={
|
||||
Object {
|
||||
"aggregatable": true,
|
||||
"displayName": "timestamp",
|
||||
"filterable": true,
|
||||
"aggregatable": false,
|
||||
"conflictDescriptions": undefined,
|
||||
"count": 0,
|
||||
"customDescription": undefined,
|
||||
"customLabel": undefined,
|
||||
"defaultFormatter": undefined,
|
||||
"esTypes": Array [
|
||||
"dateTime",
|
||||
],
|
||||
"lang": undefined,
|
||||
"name": "timestamp",
|
||||
"readFromDocValues": false,
|
||||
"script": undefined,
|
||||
"scripted": false,
|
||||
"sortable": true,
|
||||
"searchable": true,
|
||||
"subType": undefined,
|
||||
"type": "date",
|
||||
}
|
||||
}
|
||||
|
@ -3230,6 +2854,21 @@ Array [
|
|||
"display": <Memo(DataTableColumnHeader)
|
||||
columnDisplayName="extension"
|
||||
columnName="extension"
|
||||
columnsMeta={
|
||||
Object {
|
||||
"extension": Object {
|
||||
"type": "string",
|
||||
},
|
||||
"message": Object {
|
||||
"esType": "keyword",
|
||||
"type": "string",
|
||||
},
|
||||
"timestamp": Object {
|
||||
"esType": "dateTime",
|
||||
"type": "date",
|
||||
},
|
||||
}
|
||||
}
|
||||
dataView={
|
||||
Object {
|
||||
"docvalueFields": Array [],
|
||||
|
@ -3298,15 +2937,6 @@ Array [
|
|||
Array [
|
||||
"message",
|
||||
],
|
||||
Array [
|
||||
"timestamp",
|
||||
],
|
||||
Array [
|
||||
"extension",
|
||||
],
|
||||
Array [
|
||||
"message",
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
|
@ -3363,39 +2993,6 @@ Array [
|
|||
"type": "string",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Object {
|
||||
"aggregatable": true,
|
||||
"displayName": "timestamp",
|
||||
"filterable": true,
|
||||
"name": "timestamp",
|
||||
"scripted": false,
|
||||
"sortable": true,
|
||||
"type": "date",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Object {
|
||||
"aggregatable": true,
|
||||
"displayName": "extension",
|
||||
"filterable": true,
|
||||
"name": "extension",
|
||||
"scripted": false,
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Object {
|
||||
"displayName": "message",
|
||||
"filterable": false,
|
||||
"name": "message",
|
||||
"scripted": false,
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
"getFormatterForField": [MockFunction],
|
||||
|
@ -3468,10 +3065,27 @@ Array [
|
|||
},
|
||||
"cellActions": Array [
|
||||
[Function],
|
||||
[Function],
|
||||
[Function],
|
||||
],
|
||||
"display": <Memo(DataTableColumnHeader)
|
||||
columnDisplayName="message"
|
||||
columnName="message"
|
||||
columnsMeta={
|
||||
Object {
|
||||
"extension": Object {
|
||||
"type": "string",
|
||||
},
|
||||
"message": Object {
|
||||
"esType": "keyword",
|
||||
"type": "string",
|
||||
},
|
||||
"timestamp": Object {
|
||||
"esType": "dateTime",
|
||||
"type": "date",
|
||||
},
|
||||
}
|
||||
}
|
||||
dataView={
|
||||
Object {
|
||||
"docvalueFields": Array [],
|
||||
|
@ -3540,15 +3154,6 @@ Array [
|
|||
Array [
|
||||
"message",
|
||||
],
|
||||
Array [
|
||||
"timestamp",
|
||||
],
|
||||
Array [
|
||||
"extension",
|
||||
],
|
||||
Array [
|
||||
"message",
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
|
@ -3605,39 +3210,6 @@ Array [
|
|||
"type": "string",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Object {
|
||||
"aggregatable": true,
|
||||
"displayName": "timestamp",
|
||||
"filterable": true,
|
||||
"name": "timestamp",
|
||||
"scripted": false,
|
||||
"sortable": true,
|
||||
"type": "date",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Object {
|
||||
"aggregatable": true,
|
||||
"displayName": "extension",
|
||||
"filterable": true,
|
||||
"name": "extension",
|
||||
"scripted": false,
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Object {
|
||||
"displayName": "message",
|
||||
"filterable": false,
|
||||
"name": "message",
|
||||
"scripted": false,
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
"getFormatterForField": [MockFunction],
|
||||
|
|
|
@ -153,7 +153,7 @@ export interface UnifiedDataTableProps {
|
|||
/**
|
||||
* Function to add a filter in the grid cell or document flyout
|
||||
*/
|
||||
onFilter: DocViewFilterFn;
|
||||
onFilter?: DocViewFilterFn;
|
||||
/**
|
||||
* Function triggered when a column is resized by the user
|
||||
*/
|
||||
|
@ -513,11 +513,13 @@ export const UnifiedDataTable = ({
|
|||
},
|
||||
valueToStringConverter,
|
||||
componentsTourSteps,
|
||||
isPlainRecord,
|
||||
}),
|
||||
[
|
||||
componentsTourSteps,
|
||||
darkMode,
|
||||
dataView,
|
||||
isPlainRecord,
|
||||
displayedRows,
|
||||
expandedDoc,
|
||||
isFilterActive,
|
||||
|
|
|
@ -93,6 +93,11 @@ describe('Data table columns', function () {
|
|||
hasEditDataViewPermission: () =>
|
||||
servicesMock.dataViewFieldEditor.userPermissions.editIndexPattern(),
|
||||
onFilter: () => {},
|
||||
columnsMeta: {
|
||||
extension: { type: 'string' },
|
||||
message: { type: 'string', esType: 'keyword' },
|
||||
timestamp: { type: 'date', esType: 'dateTime' },
|
||||
},
|
||||
});
|
||||
expect(actual).toMatchSnapshot();
|
||||
});
|
||||
|
@ -299,7 +304,7 @@ describe('Data table columns', function () {
|
|||
const actual = getEuiGridColumns({
|
||||
showColumnTokens: true,
|
||||
columnsMeta: {
|
||||
extension: { type: 'number' },
|
||||
extension: { type: 'string' },
|
||||
message: { type: 'string', esType: 'keyword' },
|
||||
},
|
||||
columns,
|
||||
|
@ -348,7 +353,7 @@ describe('Data table columns', function () {
|
|||
servicesMock.dataViewFieldEditor.userPermissions.editIndexPattern(),
|
||||
onFilter: () => {},
|
||||
columnsMeta: {
|
||||
var_test: { type: 'number' },
|
||||
extension: { type: 'string' },
|
||||
},
|
||||
});
|
||||
expect(gridColumns[1].schema).toBe('string');
|
||||
|
@ -402,6 +407,10 @@ describe('Data table columns', function () {
|
|||
hasEditDataViewPermission: () =>
|
||||
servicesMock.dataViewFieldEditor.userPermissions.editIndexPattern(),
|
||||
onFilter: () => {},
|
||||
columnsMeta: {
|
||||
extension: { type: 'string' },
|
||||
message: { type: 'string', esType: 'keyword' },
|
||||
},
|
||||
});
|
||||
|
||||
const extensionGridColumn = gridColumns[0];
|
||||
|
@ -428,6 +437,10 @@ describe('Data table columns', function () {
|
|||
servicesMock.dataViewFieldEditor.userPermissions.editIndexPattern(),
|
||||
onFilter: () => {},
|
||||
customGridColumnsConfiguration,
|
||||
columnsMeta: {
|
||||
extension: { type: 'string' },
|
||||
message: { type: 'string', esType: 'keyword' },
|
||||
},
|
||||
});
|
||||
|
||||
expect(customizedGridColumns).toMatchSnapshot();
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
type EuiDataGridColumnCellAction,
|
||||
EuiScreenReaderOnly,
|
||||
} from '@elastic/eui';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { type DataView, DataViewField } from '@kbn/data-views-plugin/public';
|
||||
import { ToastsStart, IUiSettingsClient } from '@kbn/core/public';
|
||||
import { DocViewFilterFn } from '@kbn/unified-doc-viewer/types';
|
||||
import { ExpandButton } from './data_table_expand_button';
|
||||
|
@ -118,7 +118,17 @@ function buildEuiGridColumn({
|
|||
headerRowHeight?: number;
|
||||
customGridColumnsConfiguration?: CustomGridColumnsConfiguration;
|
||||
}) {
|
||||
const dataViewField = dataView.getFieldByName(columnName);
|
||||
const dataViewField = !isPlainRecord
|
||||
? dataView.getFieldByName(columnName)
|
||||
: new DataViewField({
|
||||
name: columnName,
|
||||
type: columnsMeta?.[columnName]?.type ?? 'unknown',
|
||||
esTypes: columnsMeta?.[columnName]?.esType
|
||||
? ([columnsMeta[columnName].esType] as string[])
|
||||
: undefined,
|
||||
searchable: true,
|
||||
aggregatable: false,
|
||||
});
|
||||
const editFieldButton =
|
||||
editField &&
|
||||
dataViewField &&
|
||||
|
@ -263,7 +273,7 @@ export function getEuiGridColumns({
|
|||
};
|
||||
hasEditDataViewPermission: () => boolean;
|
||||
valueToStringConverter: ValueToStringConverter;
|
||||
onFilter: DocViewFilterFn;
|
||||
onFilter?: DocViewFilterFn;
|
||||
editField?: (fieldName: string) => void;
|
||||
visibleCellActions?: number;
|
||||
columnsMeta?: DataTableColumnsMeta;
|
||||
|
|
|
@ -63,8 +63,7 @@ describe('Default cell actions ', function () {
|
|||
dataTableContextMock.valueToStringConverter,
|
||||
jest.fn()
|
||||
);
|
||||
expect(cellActions).toContain(FilterInBtn);
|
||||
expect(cellActions).toContain(FilterOutBtn);
|
||||
expect(cellActions).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should show Copy action for _source field', async () => {
|
||||
|
@ -96,11 +95,7 @@ describe('Default cell actions ', function () {
|
|||
);
|
||||
const button = findTestSubject(component, 'filterForButton');
|
||||
await button.simulate('click');
|
||||
expect(dataTableContextMock.onFilter).toHaveBeenCalledWith(
|
||||
dataTableContextMock.dataView.fields.getByName('extension'),
|
||||
'jpg',
|
||||
'+'
|
||||
);
|
||||
expect(dataTableContextMock.onFilter).toHaveBeenCalledWith({}, 'jpg', '+');
|
||||
});
|
||||
it('triggers filter function when FilterInBtn is clicked for a non-provided value', async () => {
|
||||
const component = mountWithIntl(
|
||||
|
@ -116,11 +111,7 @@ describe('Default cell actions ', function () {
|
|||
);
|
||||
const button = findTestSubject(component, 'filterForButton');
|
||||
await button.simulate('click');
|
||||
expect(dataTableContextMock.onFilter).toHaveBeenCalledWith(
|
||||
dataTableContextMock.dataView.fields.getByName('extension'),
|
||||
undefined,
|
||||
'+'
|
||||
);
|
||||
expect(dataTableContextMock.onFilter).toHaveBeenCalledWith({}, undefined, '+');
|
||||
});
|
||||
it('triggers filter function when FilterInBtn is clicked for an empty string value', async () => {
|
||||
const component = mountWithIntl(
|
||||
|
@ -136,11 +127,7 @@ describe('Default cell actions ', function () {
|
|||
);
|
||||
const button = findTestSubject(component, 'filterForButton');
|
||||
await button.simulate('click');
|
||||
expect(dataTableContextMock.onFilter).toHaveBeenCalledWith(
|
||||
dataTableContextMock.dataView.fields.getByName('message'),
|
||||
'',
|
||||
'+'
|
||||
);
|
||||
expect(dataTableContextMock.onFilter).toHaveBeenCalledWith({}, '', '+');
|
||||
});
|
||||
it('triggers filter function when FilterOutBtn is clicked', async () => {
|
||||
const component = mountWithIntl(
|
||||
|
@ -156,11 +143,7 @@ describe('Default cell actions ', function () {
|
|||
);
|
||||
const button = findTestSubject(component, 'filterOutButton');
|
||||
await button.simulate('click');
|
||||
expect(dataTableContextMock.onFilter).toHaveBeenCalledWith(
|
||||
dataTableContextMock.dataView.fields.getByName('extension'),
|
||||
'jpg',
|
||||
'-'
|
||||
);
|
||||
expect(dataTableContextMock.onFilter).toHaveBeenCalledWith({}, 'jpg', '-');
|
||||
});
|
||||
it('triggers clipboard copy when CopyBtn is clicked', async () => {
|
||||
const component = mountWithIntl(
|
||||
|
|
|
@ -20,22 +20,21 @@ function onFilterCell(
|
|||
context: DataTableContext,
|
||||
rowIndex: EuiDataGridColumnCellActionProps['rowIndex'],
|
||||
columnId: EuiDataGridColumnCellActionProps['columnId'],
|
||||
mode: '+' | '-'
|
||||
mode: '+' | '-',
|
||||
field: DataViewField
|
||||
) {
|
||||
const row = context.rows[rowIndex];
|
||||
const value = row.flattened[columnId];
|
||||
const field = context.dataView.fields.getByName(columnId);
|
||||
|
||||
if (field && context.onFilter) {
|
||||
context.onFilter(field, value, mode);
|
||||
}
|
||||
}
|
||||
|
||||
export const FilterInBtn = ({
|
||||
Component,
|
||||
rowIndex,
|
||||
columnId,
|
||||
}: EuiDataGridColumnCellActionProps) => {
|
||||
export const FilterInBtn = (
|
||||
{ Component, rowIndex, columnId }: EuiDataGridColumnCellActionProps,
|
||||
field: DataViewField
|
||||
) => {
|
||||
const context = useContext(UnifiedDataTableContext);
|
||||
const buttonTitle = i18n.translate('unifiedDataTable.grid.filterForAria', {
|
||||
defaultMessage: 'Filter for this {value}',
|
||||
|
@ -45,7 +44,7 @@ export const FilterInBtn = ({
|
|||
return (
|
||||
<Component
|
||||
onClick={() => {
|
||||
onFilterCell(context, rowIndex, columnId, '+');
|
||||
onFilterCell(context, rowIndex, columnId, '+', field);
|
||||
}}
|
||||
iconType="plusInCircle"
|
||||
aria-label={buttonTitle}
|
||||
|
@ -59,11 +58,10 @@ export const FilterInBtn = ({
|
|||
);
|
||||
};
|
||||
|
||||
export const FilterOutBtn = ({
|
||||
Component,
|
||||
rowIndex,
|
||||
columnId,
|
||||
}: EuiDataGridColumnCellActionProps) => {
|
||||
export const FilterOutBtn = (
|
||||
{ Component, rowIndex, columnId }: EuiDataGridColumnCellActionProps,
|
||||
field: DataViewField
|
||||
) => {
|
||||
const context = useContext(UnifiedDataTableContext);
|
||||
const buttonTitle = i18n.translate('unifiedDataTable.grid.filterOutAria', {
|
||||
defaultMessage: 'Filter out this {value}',
|
||||
|
@ -73,7 +71,7 @@ export const FilterOutBtn = ({
|
|||
return (
|
||||
<Component
|
||||
onClick={() => {
|
||||
onFilterCell(context, rowIndex, columnId, '-');
|
||||
onFilterCell(context, rowIndex, columnId, '-', field);
|
||||
}}
|
||||
iconType="minusInCircle"
|
||||
aria-label={buttonTitle}
|
||||
|
@ -126,7 +124,20 @@ export function buildCellActions(
|
|||
onFilter?: DocViewFilterFn
|
||||
) {
|
||||
return [
|
||||
...(onFilter && field.filterable ? [FilterInBtn, FilterOutBtn] : []),
|
||||
...(onFilter && field.filterable
|
||||
? [
|
||||
({ Component, rowIndex, columnId }: EuiDataGridColumnCellActionProps) =>
|
||||
FilterInBtn(
|
||||
{ Component, rowIndex, columnId } as EuiDataGridColumnCellActionProps,
|
||||
field
|
||||
),
|
||||
({ Component, rowIndex, columnId }: EuiDataGridColumnCellActionProps) =>
|
||||
FilterOutBtn(
|
||||
{ Component, rowIndex, columnId } as EuiDataGridColumnCellActionProps,
|
||||
field
|
||||
),
|
||||
]
|
||||
: []),
|
||||
({ Component, rowIndex, columnId }: EuiDataGridColumnCellActionProps) =>
|
||||
buildCopyValueButton(
|
||||
{ Component, rowIndex, columnId } as EuiDataGridColumnCellActionProps,
|
||||
|
|
|
@ -23,6 +23,7 @@ export interface DataTableContext {
|
|||
setSelectedDocs: (selected: string[]) => void;
|
||||
valueToStringConverter: ValueToStringConverter;
|
||||
componentsTourSteps?: Record<string, string>;
|
||||
isPlainRecord?: boolean;
|
||||
}
|
||||
|
||||
const defaultContext = {} as unknown as DataTableContext;
|
||||
|
|
|
@ -17,6 +17,8 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { isOfAggregateQueryType } from '@kbn/es-query';
|
||||
import { appendWhereClauseToESQLQuery } from '@kbn/esql-utils';
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
import classNames from 'classnames';
|
||||
import { generateFilters } from '@kbn/data-plugin/public';
|
||||
|
@ -156,6 +158,37 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
|
|||
[filterManager, dataView, dataViews, trackUiMetric, capabilities]
|
||||
);
|
||||
|
||||
const onPopulateWhereClause = useCallback(
|
||||
(field: DataViewField | string, values: unknown, operation: '+' | '-') => {
|
||||
if (query && isOfAggregateQueryType(query) && 'esql' in query) {
|
||||
const fieldName = typeof field === 'string' ? field : field.name;
|
||||
// send the field type for casting
|
||||
const fieldType = typeof field !== 'string' ? field.type : undefined;
|
||||
// weird existence logic from Discover components
|
||||
// in the field it comes the operator _exists_ and in the value the field
|
||||
// I need to take care of it here but I think it should be handled on the fieldlist instead
|
||||
const updatedQuery = appendWhereClauseToESQLQuery(
|
||||
query.esql,
|
||||
fieldName === '_exists_' ? String(values) : fieldName,
|
||||
fieldName === '_exists_' ? undefined : values,
|
||||
fieldName === '_exists_' ? '_exists_' : operation,
|
||||
fieldType
|
||||
);
|
||||
data.query.queryString.setQuery({
|
||||
esql: updatedQuery,
|
||||
});
|
||||
if (trackUiMetric) {
|
||||
trackUiMetric(METRIC_TYPE.CLICK, 'esql_filter_added');
|
||||
}
|
||||
}
|
||||
},
|
||||
[data.query.queryString, query, trackUiMetric]
|
||||
);
|
||||
|
||||
const onFilter = isPlainRecord
|
||||
? (onPopulateWhereClause as DocViewFilterFn)
|
||||
: (onAddFilter as DocViewFilterFn);
|
||||
|
||||
const onFieldEdited = useCallback(
|
||||
async ({ removedFieldName }: { removedFieldName?: string } = {}) => {
|
||||
if (removedFieldName && currentColumns.includes(removedFieldName)) {
|
||||
|
@ -234,7 +267,7 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
|
|||
stateContainer={stateContainer}
|
||||
columns={currentColumns}
|
||||
viewMode={viewMode}
|
||||
onAddFilter={onAddFilter as DocViewFilterFn}
|
||||
onAddFilter={onFilter}
|
||||
onFieldEdited={onFieldEdited}
|
||||
container={mainContainer}
|
||||
onDropFieldToTable={onDropFieldToTable}
|
||||
|
@ -244,16 +277,16 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
|
|||
</>
|
||||
);
|
||||
}, [
|
||||
currentColumns,
|
||||
dataView,
|
||||
isPlainRecord,
|
||||
mainContainer,
|
||||
onAddFilter,
|
||||
onDropFieldToTable,
|
||||
onFieldEdited,
|
||||
resultState,
|
||||
isPlainRecord,
|
||||
dataView,
|
||||
stateContainer,
|
||||
currentColumns,
|
||||
viewMode,
|
||||
onFilter,
|
||||
onFieldEdited,
|
||||
mainContainer,
|
||||
onDropFieldToTable,
|
||||
panelsToggle,
|
||||
]);
|
||||
|
||||
|
@ -329,7 +362,7 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
|
|||
documents$={stateContainer.dataState.data$.documents$}
|
||||
onAddField={onAddColumn}
|
||||
columns={currentColumns}
|
||||
onAddFilter={!isPlainRecord ? onAddFilter : undefined}
|
||||
onAddFilter={onFilter}
|
||||
onRemoveField={onRemoveColumn}
|
||||
onChangeDataView={stateContainer.actions.onChangeDataView}
|
||||
selectedDataView={dataView}
|
||||
|
|
|
@ -125,7 +125,7 @@ export const DiscoverMainContent = ({
|
|||
<DiscoverDocuments
|
||||
viewModeToggle={viewModeToggle}
|
||||
dataView={dataView}
|
||||
onAddFilter={!isPlainRecord ? onAddFilter : undefined}
|
||||
onAddFilter={onAddFilter}
|
||||
stateContainer={stateContainer}
|
||||
onFieldEdited={!isPlainRecord ? onFieldEdited : undefined}
|
||||
/>
|
||||
|
|
|
@ -73,7 +73,7 @@ export function getTextBasedQueryFieldList(
|
|||
name: column.name,
|
||||
type: column.meta?.type ?? 'unknown',
|
||||
esTypes: column.meta?.esType ? [column.meta?.esType] : undefined,
|
||||
searchable: false,
|
||||
searchable: true,
|
||||
aggregatable: false,
|
||||
isNull: Boolean(column?.isNull),
|
||||
})
|
||||
|
|
|
@ -131,7 +131,7 @@ describe('sidebar reducer', function () {
|
|||
esTypes: undefined,
|
||||
aggregatable: false,
|
||||
isNull: true,
|
||||
searchable: false,
|
||||
searchable: true,
|
||||
}),
|
||||
new DataViewField({
|
||||
name: 'text2',
|
||||
|
@ -139,7 +139,7 @@ describe('sidebar reducer', function () {
|
|||
esTypes: ['keyword'],
|
||||
aggregatable: false,
|
||||
isNull: false,
|
||||
searchable: false,
|
||||
searchable: true,
|
||||
}),
|
||||
],
|
||||
fieldCounts: {},
|
||||
|
|
|
@ -32,7 +32,7 @@ export type DocTableRow = EsHitRecord & {
|
|||
|
||||
export interface TableRowProps {
|
||||
columns: string[];
|
||||
filter: DocViewFilterFn;
|
||||
filter?: DocViewFilterFn;
|
||||
filters?: Filter[];
|
||||
isPlainRecord?: boolean;
|
||||
savedSearchId?: string;
|
||||
|
@ -105,7 +105,7 @@ export const TableRow = ({
|
|||
const inlineFilter = useCallback(
|
||||
(column: string, type: '+' | '-') => {
|
||||
const field = dataView.fields.getByName(column);
|
||||
filter(field!, row.flattened[column], type);
|
||||
filter?.(field!, row.flattened[column], type);
|
||||
},
|
||||
[filter, dataView.fields, row.flattened]
|
||||
);
|
||||
|
|
|
@ -72,7 +72,7 @@ export interface DocTableProps {
|
|||
/**
|
||||
* Filter callback
|
||||
*/
|
||||
onFilter: DocViewFilterFn;
|
||||
onFilter?: DocViewFilterFn;
|
||||
/**
|
||||
* Sorting callback
|
||||
*/
|
||||
|
|
|
@ -459,25 +459,28 @@ export class SavedSearchEmbeddable
|
|||
});
|
||||
this.updateInput({ sort: sortOrderArr });
|
||||
},
|
||||
onFilter: async (field, value, operator) => {
|
||||
let filters = generateFilters(
|
||||
this.services.filterManager,
|
||||
// @ts-expect-error
|
||||
field,
|
||||
value,
|
||||
operator,
|
||||
dataView
|
||||
);
|
||||
filters = filters.map((filter) => ({
|
||||
...filter,
|
||||
$state: { store: FilterStateStore.APP_STATE },
|
||||
}));
|
||||
// I don't want to create filters when is embedded
|
||||
...(!this.isTextBasedSearch(savedSearch) && {
|
||||
onFilter: async (field, value, operator) => {
|
||||
let filters = generateFilters(
|
||||
this.services.filterManager,
|
||||
// @ts-expect-error
|
||||
field,
|
||||
value,
|
||||
operator,
|
||||
dataView
|
||||
);
|
||||
filters = filters.map((filter) => ({
|
||||
...filter,
|
||||
$state: { store: FilterStateStore.APP_STATE },
|
||||
}));
|
||||
|
||||
await this.executeTriggerActions(APPLY_FILTER_TRIGGER, {
|
||||
embeddable: this,
|
||||
filters,
|
||||
});
|
||||
},
|
||||
await this.executeTriggerActions(APPLY_FILTER_TRIGGER, {
|
||||
embeddable: this,
|
||||
filters,
|
||||
});
|
||||
},
|
||||
}),
|
||||
useNewFieldsApi: !this.services.uiSettings.get(SEARCH_FIELDS_FROM_SOURCE, false),
|
||||
showTimeCol: !this.services.uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING, false),
|
||||
ariaLabelledBy: 'documentsAriaLabel',
|
||||
|
|
|
@ -527,5 +527,67 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filtering by clicking on the table', () => {
|
||||
beforeEach(async () => {
|
||||
await PageObjects.common.navigateToApp('discover');
|
||||
await PageObjects.timePicker.setDefaultAbsoluteRange();
|
||||
});
|
||||
|
||||
it('should append a where clause by clicking the table', async () => {
|
||||
await PageObjects.discover.selectTextBaseLang();
|
||||
const testQuery = `from logstash-* | sort @timestamp desc | limit 10000 | stats countB = count(bytes) by geo.dest | sort countB`;
|
||||
await monacoEditor.setCodeEditorValue(testQuery);
|
||||
|
||||
await testSubjects.click('querySubmitButton');
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.discover.waitUntilSearchingHasFinished();
|
||||
await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded();
|
||||
|
||||
await testSubjects.click('TextBasedLangEditor-expand');
|
||||
await dataGrid.clickCellFilterForButton(0, 3);
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.discover.waitUntilSearchingHasFinished();
|
||||
await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded();
|
||||
|
||||
const editorValue = await monacoEditor.getCodeEditorValue();
|
||||
expect(editorValue).to.eql(
|
||||
`from logstash-* | sort @timestamp desc | limit 10000 | stats countB = count(bytes) by geo.dest | sort countB\n| where \`geo.dest\`=="BT"`
|
||||
);
|
||||
|
||||
// negate
|
||||
await dataGrid.clickCellFilterOutButton(0, 3);
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.discover.waitUntilSearchingHasFinished();
|
||||
await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded();
|
||||
|
||||
const newValue = await monacoEditor.getCodeEditorValue();
|
||||
expect(newValue).to.eql(
|
||||
`from logstash-* | sort @timestamp desc | limit 10000 | stats countB = count(bytes) by geo.dest | sort countB\n| where \`geo.dest\`!="BT"`
|
||||
);
|
||||
});
|
||||
|
||||
it('should append an end in existing where clause by clicking the table', async () => {
|
||||
await PageObjects.discover.selectTextBaseLang();
|
||||
const testQuery = `from logstash-* | sort @timestamp desc | limit 10000 | stats countB = count(bytes) by geo.dest | sort countB | where countB > 0`;
|
||||
await monacoEditor.setCodeEditorValue(testQuery);
|
||||
|
||||
await testSubjects.click('querySubmitButton');
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.discover.waitUntilSearchingHasFinished();
|
||||
await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded();
|
||||
|
||||
await testSubjects.click('TextBasedLangEditor-expand');
|
||||
await dataGrid.clickCellFilterForButton(0, 3);
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.discover.waitUntilSearchingHasFinished();
|
||||
await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded();
|
||||
|
||||
const editorValue = await monacoEditor.getCodeEditorValue();
|
||||
expect(editorValue).to.eql(
|
||||
`from logstash-* | sort @timestamp desc | limit 10000 | stats countB = count(bytes) by geo.dest | sort countB | where countB > 0\nand \`geo.dest\`=="BT"`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -144,10 +144,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
describe('text based columns', function () {
|
||||
before(async () => {
|
||||
const TEST_START_TIME = 'Sep 23, 2015 @ 06:31:44.000';
|
||||
const TEST_END_TIME = 'Sep 23, 2015 @ 18:31:44.000';
|
||||
await PageObjects.timePicker.setAbsoluteRange(TEST_START_TIME, TEST_END_TIME);
|
||||
beforeEach(async () => {
|
||||
await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings();
|
||||
await PageObjects.common.navigateToApp('discover');
|
||||
await PageObjects.discover.waitUntilSearchingHasFinished();
|
||||
|
||||
|
@ -169,6 +167,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
expect(await testSubjects.getVisibleText('dscFieldStats-statsFooter')).to.contain(
|
||||
'42 sample values'
|
||||
);
|
||||
|
||||
await PageObjects.unifiedFieldList.clickFieldListPlusFilter('bytes', '0');
|
||||
await testSubjects.click('TextBasedLangEditor-expand');
|
||||
const editorValue = await monacoEditor.getCodeEditorValue();
|
||||
expect(editorValue).to.eql(
|
||||
`from logstash-* [METADATA _index, _id] | sort @timestamp desc | limit 500\n| where \`bytes\`==0`
|
||||
);
|
||||
await PageObjects.unifiedFieldList.closeFieldPopover();
|
||||
});
|
||||
|
||||
|
@ -183,6 +188,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
expect(await testSubjects.getVisibleText('dscFieldStats-statsFooter')).to.contain(
|
||||
'500 sample values'
|
||||
);
|
||||
|
||||
await PageObjects.unifiedFieldList.clickFieldListPlusFilter('extension.raw', 'css');
|
||||
await testSubjects.click('TextBasedLangEditor-expand');
|
||||
const editorValue = await monacoEditor.getCodeEditorValue();
|
||||
expect(editorValue).to.eql(
|
||||
`from logstash-* [METADATA _index, _id] | sort @timestamp desc | limit 500\n| where \`extension.raw\`=="css"`
|
||||
);
|
||||
|
||||
await PageObjects.unifiedFieldList.closeFieldPopover();
|
||||
});
|
||||
|
||||
|
@ -197,6 +210,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
expect(await testSubjects.getVisibleText('dscFieldStats-statsFooter')).to.contain(
|
||||
'32 sample values'
|
||||
);
|
||||
|
||||
await PageObjects.unifiedFieldList.clickFieldListPlusFilter('clientip', '216.126.255.31');
|
||||
await testSubjects.click('TextBasedLangEditor-expand');
|
||||
const editorValue = await monacoEditor.getCodeEditorValue();
|
||||
expect(editorValue).to.eql(
|
||||
`from logstash-* [METADATA _index, _id] | sort @timestamp desc | limit 500\n| where \`clientip\`::string=="216.126.255.31"`
|
||||
);
|
||||
|
||||
await PageObjects.unifiedFieldList.closeFieldPopover();
|
||||
});
|
||||
|
||||
|
@ -214,8 +235,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await PageObjects.unifiedFieldList.closeFieldPopover();
|
||||
});
|
||||
|
||||
it('should not have stats for a date field yet', async () => {
|
||||
it('should not have stats for a date field yet but create an is not null filter', async () => {
|
||||
await PageObjects.unifiedFieldList.clickFieldListItem('@timestamp');
|
||||
await PageObjects.unifiedFieldList.clickFieldListExistsFilter('@timestamp');
|
||||
await testSubjects.click('TextBasedLangEditor-expand');
|
||||
const editorValue = await monacoEditor.getCodeEditorValue();
|
||||
expect(editorValue).to.eql(
|
||||
`from logstash-* [METADATA _index, _id] | sort @timestamp desc | limit 500\n| where \`@timestamp\` is not null`
|
||||
);
|
||||
await testSubjects.missingOrFail('dscFieldStats-statsFooter');
|
||||
await PageObjects.unifiedFieldList.closeFieldPopover();
|
||||
});
|
||||
|
@ -245,6 +272,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
expect(await testSubjects.getVisibleText('dscFieldStats-statsFooter')).to.contain(
|
||||
'100 sample records'
|
||||
);
|
||||
|
||||
await PageObjects.unifiedFieldList.clickFieldListPlusFilter('extension', 'css');
|
||||
await testSubjects.click('TextBasedLangEditor-expand');
|
||||
const editorValue = await monacoEditor.getCodeEditorValue();
|
||||
expect(editorValue).to.eql(
|
||||
`from logstash-* [METADATA _index, _id] | sort @timestamp desc | limit 500\n| where \`extension\`=="css"`
|
||||
);
|
||||
|
||||
await PageObjects.unifiedFieldList.closeFieldPopover();
|
||||
});
|
||||
|
||||
|
@ -277,6 +312,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
expect(await testSubjects.getVisibleText('dscFieldStats-statsFooter')).to.contain(
|
||||
'3 sample values'
|
||||
);
|
||||
|
||||
await PageObjects.unifiedFieldList.clickFieldListPlusFilter('avg(bytes)', '5453');
|
||||
await testSubjects.click('TextBasedLangEditor-expand');
|
||||
const editorValue = await monacoEditor.getCodeEditorValue();
|
||||
expect(editorValue).to.eql(
|
||||
`from logstash-* | sort @timestamp desc | limit 50 | stats avg(bytes) by geo.dest | limit 3\n| where \`avg(bytes)\`==5453`
|
||||
);
|
||||
|
||||
await PageObjects.unifiedFieldList.closeFieldPopover();
|
||||
});
|
||||
|
||||
|
@ -298,6 +341,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
expect(await testSubjects.getVisibleText('dscFieldStats-statsFooter')).to.contain(
|
||||
'1 sample value'
|
||||
);
|
||||
|
||||
await PageObjects.unifiedFieldList.clickFieldListMinusFilter('enabled', 'true');
|
||||
await testSubjects.click('TextBasedLangEditor-expand');
|
||||
const editorValue = await monacoEditor.getCodeEditorValue();
|
||||
expect(editorValue).to.eql(`row enabled = true\n| where \`enabled\`!=true`);
|
||||
await PageObjects.unifiedFieldList.closeFieldPopover();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -233,6 +233,17 @@ export class UnifiedFieldListPageObject extends FtrService {
|
|||
await this.header.waitUntilLoadingHasFinished();
|
||||
}
|
||||
|
||||
public async clickFieldListExistsFilter(field: string) {
|
||||
const existsFilterTestSubj = `discoverFieldListPanelAddExistFilter-${field}`;
|
||||
if (!(await this.testSubjects.exists(existsFilterTestSubj))) {
|
||||
// field has to be open
|
||||
await this.clickFieldListItem(field);
|
||||
}
|
||||
// this.testSubjects.find doesn't handle spaces in the data-test-subj value
|
||||
await this.testSubjects.click(existsFilterTestSubj);
|
||||
await this.header.waitUntilLoadingHasFinished();
|
||||
}
|
||||
|
||||
public async openSidebarFieldFilter() {
|
||||
await this.testSubjects.click('fieldListFiltersFieldTypeFilterToggle');
|
||||
await this.testSubjects.existOrFail('fieldListFiltersFieldTypeFilterOptions');
|
||||
|
|
|
@ -136,6 +136,11 @@ export class DataGridService extends FtrService {
|
|||
await actionButton.click();
|
||||
}
|
||||
|
||||
public async clickCellFilterOutButton(rowIndex: number = 0, columnIndex: number = 0) {
|
||||
const actionButton = await this.getCellActionButton(rowIndex, columnIndex, 'filterOutButton');
|
||||
await actionButton.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* The same as getCellElement, but useful when multiple data grids are on the page.
|
||||
*/
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue