[Discover] Supports SQL query language (#134429) (#136702)

* [Discover] Supports SQL query language (#134429)

* Move the add dataview action above the dataview selection panel

* Implements a new selectable on the dataview picker for the text based languages

* Implementation of the transition modal when on SQL mode and select a dataview

* Fix es lint

* Change switch modal button modal icon

* Lazy load components

* Small changes on the styling of the switch without saving button

* Initialization of mocaco editor

* Change to the type

* Fixes types checks

* New submit button for query mode

* Implememtation of the expanded mode of the editor

* Implement documentation

* Implementation of the oneliner mode with ellipsis

* Some  fixes on the resizer

* Implementation of the errors layout, WIP

* Fetch SQL data in Discover

* Fix expression test

* Fix editor zIndex

* Fix types error

* Fix type check in Discover

* Fix more types

* some CI fixes

* Fixes

* Cleanup after merge

* Remove from state

* Connect search errors with the unified search editor

* Add error mrkers in unified search editor

* Save and open saved searches

* Filter out saved searches from text based languages

* Some fixes

* Fix unit tests

* Fix checks

* On save and exit modal implementation

* Add shortcut on the editor for submit query

* Fix wrong condition

* Initial types change

* Use regex to find the index pattern string

* Fix some types and cleanup

* Fix types

* Fix some types

* Further fixes

* More fixes

* More fixes

* Fix visualize types

* more

* More fixes

* Fixes more types

* Fix dashboard types

* Fix dashboard types

* Controls plugin types

* Fix Lens types

* Fix data plugin types

* Fix types in Lens 2

* buildEsConfig type fixes

* Fix observability types

* Fix maps types

* data visualizer types

* Fix ml types

* xpack rest types

* Fix jest test

* Fix

* Move helper functions to es config

* fix bug on breadcrumb click

* Fix time field bug

* Add enableSql advanced setting to discover for enabling the sql mode

* Make the documentation component more dynamic

* Add some comments, improvements

* Enhance storybook with the textbased languages

* Update storybook with the error state of the editor

* Adds a readme for the editor and fixes the modal mobile version

* [Discover] improve test and storybook for new data type

* [Discover] add functional tests

* Add aggregate functions to the documentation

* [Discover] fix tests

* Add some unit tests

* [Discover] fix linting

* [Discover] update linting

* More unti tests

* Dataview picker unit tests

* Fix a bug on the dataview picker

* Add unit tests for the editor

* Fix jest test

* [Discover] apply suggestions

* [Discover] adjust styles

* Fix some bugs and select columns in the sql mode

* [Discover] fix eslint and tests

* [Discover] update unit tests

* Fix bug on transitioning from sql mode to dataview mode

* [Discover] fix tests

* Design fixes on the errors messages

* [Discover] fix ci

* Update the columns only if the query changes

* [Discover] change isPlainRecord retrieval method

* Fix bug on cleanup

* Fix bug on opening a saved search

* [Discover] fix comments

* [Discover] fix bug with browser refresh

* [Discover] fix functional

* [Discover] fix another functional

* Fix ordering lost when the user refreshes the browser

* [Discover] revert use_discover_state

* [Discover] revert functional impl

* Fix security solution types

* Casting dashboard plugin

* Revert change

* type param

* [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix'

* Revert types changes

* More reverts

* Types fixes

* Fix Discover jest test

* Fix context app jest test

* Final types changes

* Fixes unit test

Co-authored-by: Dzmitry Tamashevich <diaamnj@mail.ru>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Joe Reuter <johannes.reuter@elastic.co>

* Fix types

* Fix jest test

* More design fixes

* Update advanced setting description

* Further design changes

* [Discover] Remove document explorer header column edit data view field functionality (#136743)

* remove Edit data view field for SQL

* Fix the fix

* [Discover] Implement SQL data fetching for embeddable (#136793)

* remove Edit data view field for SQL

* Fix the fix

* Implement SQL for embeddable

* Fix non-saved-search embeddables

* Fix reporting bundle size

* Allow filters on dashboard level for sql searches

* Fix the radius on the editor

* Add vertical padding on the editor

* Change the theme

* Address PR comments

* Fix types

* Address some of the comments

* Fix bug on transitioning from SQL to dataview mode with the modal dismissed

* More types fixes

* Design review comments

* Discovery team review comments

* Fix jest tests

* Fix bug on navigating from the SQL mode to the dataview mode and back in sql mode by clicking the breadcrumb

* Update src/plugins/discover/public/application/main/hooks/use_discover_state.ts

Co-authored-by: Matthias Wilhelm <matthias.wilhelm@elastic.co>

* Add padding to the top of the editor without creating any bug

* Add some padding to the bottom without creating any bug

* Fixes undo bug

* Fix confusing naming of variable

* Fix nested selects

* Update texts for transition modal and warning

* Make it work with dashboard Query

* Address some of the comments

Co-authored-by: Dzmitry Tamashevich <diaamnj@mail.ru>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Joe Reuter <johannes.reuter@elastic.co>
Co-authored-by: Matthias Wilhelm <matthias.wilhelm@elastic.co>
This commit is contained in:
Stratoula Kalafateli 2022-07-26 10:51:31 +03:00 committed by GitHub
parent 68162dca7c
commit a296e4cc97
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
154 changed files with 5984 additions and 853 deletions

View file

@ -319,6 +319,9 @@ The default sort direction for time-based data views.
[[doctable-hidetimecolumn]]`doc_table:hideTimeColumn`::
Hides the "Time" column in *Discover* and in all saved searches on dashboards.
[[discover:enableSql]]`discover:enableSql`::
When enabled, allows SQL queries for search.
[[doctable-highlight]]`doc_table:highlight`::
Highlights results in *Discover* and saved searches on dashboards. Highlighting
slows requests when working on big documents.

View file

@ -11,11 +11,13 @@ import { SerializableRecord } from '@kbn/utility-types';
import { buildQueryFromKuery } from './from_kuery';
import { buildQueryFromFilters } from './from_filters';
import { buildQueryFromLucene } from './from_lucene';
import { Filter, Query } from '../filters';
import { Filter, Query, AggregateQuery } from '../filters';
import { isOfQueryType } from './es_query_sql';
import { BoolQuery, DataViewBase } from './types';
import type { KueryQueryOptions } from '../kuery';
import type { EsQueryFiltersConfig } from './from_filters';
type AnyQuery = Query | AggregateQuery;
/**
* Configurations to be used while constructing an ES query.
* @public
@ -44,7 +46,7 @@ function removeMatchAll<T>(filters: T[]) {
*/
export function buildEsQuery(
indexPattern: DataViewBase | undefined,
queries: Query | Query[],
queries: AnyQuery | AnyQuery[],
filters: Filter | Filter[],
config: EsQueryConfig = {
allowLeadingWildcards: false,
@ -55,7 +57,7 @@ export function buildEsQuery(
queries = Array.isArray(queries) ? queries : [queries];
filters = Array.isArray(filters) ? filters : [filters];
const validQueries = queries.filter((query) => has(query, 'query'));
const validQueries = queries.filter(isOfQueryType).filter((query) => has(query, 'query'));
const queriesByLanguage = groupBy(validQueries, 'language');
const kueryQuery = buildQueryFromKuery(
indexPattern,

View file

@ -0,0 +1,84 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import {
isOfQueryType,
isOfAggregateQueryType,
getAggregateQueryMode,
getIndexPatternFromSQLQuery,
} from './es_query_sql';
describe('sql query helpers', () => {
describe('isOfQueryType', () => {
it('should return true for a Query type query', () => {
const flag = isOfQueryType({ query: 'foo', language: 'test' });
expect(flag).toBe(true);
});
it('should return false for an Aggregate type query', () => {
const flag = isOfQueryType({ sql: 'SELECT * FROM foo' });
expect(flag).toBe(false);
});
});
describe('isOfAggregateQueryType', () => {
it('should return false for a Query type query', () => {
const flag = isOfAggregateQueryType({ query: 'foo', language: 'test' });
expect(flag).toBe(false);
});
it('should return true for an Aggregate type query', () => {
const flag = isOfAggregateQueryType({ sql: 'SELECT * FROM foo' });
expect(flag).toBe(true);
});
});
describe('getAggregateQueryMode', () => {
it('should return sql for an SQL AggregateQuery type', () => {
const mode = getAggregateQueryMode({ sql: 'SELECT * FROM foo' });
expect(mode).toBe('sql');
});
it('should return esql for an ESQL AggregateQuery type', () => {
const mode = getAggregateQueryMode({ esql: 'foo | where field > 100' });
expect(mode).toBe('esql');
});
});
describe('getIndexPatternFromSQLQuery', () => {
it('should return the index pattern string from sql queries', () => {
const idxPattern1 = getIndexPatternFromSQLQuery('SELECT * FROM foo');
expect(idxPattern1).toBe('foo');
const idxPattern2 = getIndexPatternFromSQLQuery('SELECT woof, meow FROM "foo"');
expect(idxPattern2).toBe('foo');
const idxPattern3 = getIndexPatternFromSQLQuery('SELECT woof, meow FROM "the_index_pattern"');
expect(idxPattern3).toBe('the_index_pattern');
const idxPattern4 = getIndexPatternFromSQLQuery('SELECT woof, meow FROM "the-index-pattern"');
expect(idxPattern4).toBe('the-index-pattern');
const idxPattern5 = getIndexPatternFromSQLQuery('SELECT woof, meow from "the-index-pattern"');
expect(idxPattern5).toBe('the-index-pattern');
const idxPattern6 = getIndexPatternFromSQLQuery('SELECT woof, meow from "logstash-*"');
expect(idxPattern6).toBe('logstash-*');
const idxPattern7 = getIndexPatternFromSQLQuery(
'SELECT woof, meow from logstash-1234! WHERE field > 100'
);
expect(idxPattern7).toBe('logstash-1234!');
const idxPattern8 = getIndexPatternFromSQLQuery(
'SELECT * FROM (SELECT woof, miaou FROM "logstash-1234!" GROUP BY woof)'
);
expect(idxPattern8).toBe('logstash-1234!');
});
});
});

View file

@ -0,0 +1,46 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { Query, AggregateQuery } from '../filters';
type Language = keyof AggregateQuery;
// Checks if the query is of type Query
export function isOfQueryType(arg?: Query | AggregateQuery): arg is Query {
return Boolean(arg && 'query' in arg);
}
// Checks if the query is of type AggregateQuery
// currently only supports the sql query type
// should be enhanced to support other query types
export function isOfAggregateQueryType(
query: AggregateQuery | Query | { [key: string]: any }
): query is AggregateQuery {
return Boolean(query && ('sql' in query || 'esql' in query));
}
// returns the language of the aggregate Query, sql, esql etc
export function getAggregateQueryMode(query: AggregateQuery): Language {
return Object.keys(query)[0] as Language;
}
// retrieves the index pattern from the aggregate query
export function getIndexPatternFromSQLQuery(sqlQuery?: string): string {
let sql = sqlQuery?.replaceAll('"', '').replaceAll("'", '');
const splitFroms = sql?.split(new RegExp(/FROM\s/, 'ig'));
const fromsLength = splitFroms?.length ?? 0;
if (splitFroms && splitFroms?.length > 2) {
sql = `${splitFroms[fromsLength - 2]} FROM ${splitFroms[fromsLength - 1]}`;
}
// case insensitive match for the index pattern
const regex = new RegExp(/FROM\s+([\w*-.!@$^()~;]+)/, 'i');
const matches = sql?.match(regex);
if (matches) {
return matches[1];
}
return '';
}

View file

@ -13,6 +13,12 @@ export { buildEsQuery } from './build_es_query';
export { buildQueryFromFilters } from './from_filters';
export { luceneStringToDsl } from './lucene_string_to_dsl';
export { decorateQuery } from './decorate_query';
export {
isOfQueryType,
isOfAggregateQueryType,
getAggregateQueryMode,
getIndexPatternFromSQLQuery,
} from './es_query_sql';
export type {
IFieldSubType,
BoolQuery,

View file

@ -82,6 +82,8 @@ export type Query = {
language: string;
};
export type AggregateQuery = { sql: string } | { esql: string };
/**
* An interface for a latitude-longitude pair
* @public

View file

@ -59,6 +59,7 @@ export {
export type {
Query,
AggregateQuery,
Filter,
LatLon,
FieldFilter,

View file

@ -29,6 +29,7 @@ export type {
PhraseFilter,
PhrasesFilter,
Query,
AggregateQuery,
QueryStringFilter,
RangeFilter,
RangeFilterMeta,
@ -52,6 +53,10 @@ export {
decorateQuery,
luceneStringToDsl,
migrateFilter,
isOfQueryType,
isOfAggregateQueryType,
getAggregateQueryMode,
getIndexPatternFromSQLQuery,
} from './es_query';
export {

View file

@ -8,6 +8,7 @@
import { History } from 'history';
import { createQueryParamObservable } from '@kbn/kibana-utils-plugin/public';
import type { Query } from '@kbn/es-query';
import { DashboardAppLocatorParams, DashboardConstants } from '../..';
import { DashboardState } from '../../types';
import { getDashboardTitle } from '../../dashboard_strings';
@ -113,7 +114,7 @@ function getLocatorParams({
timeRange: shouldRestoreSearchSession ? timefilter.getAbsoluteTime() : timefilter.getTime(),
searchSessionId: shouldRestoreSearchSession ? data.search.session.getSessionId() : undefined,
panels: getDashboardId() ? undefined : appState.panels,
query: queryString.formatQuery(appState.query),
query: queryString.formatQuery(appState.query) as Query,
filters: filterManager.getFilters(),
savedQuery: appState.savedQuery,
dashboardId: getDashboardId(),

View file

@ -9,7 +9,6 @@
import _ from 'lodash';
import { merge } from 'rxjs';
import { debounceTime, finalize, map, switchMap, tap } from 'rxjs/operators';
import { setQuery } from '../state';
import { DashboardBuildContext, DashboardState } from '../../types';
import { DashboardSavedObject } from '../../saved_dashboards';
@ -100,7 +99,7 @@ export const syncDashboardFilterState = ({
// apply filters when the filter manager changes
const filterManagerSubscription = merge(filterManager.getUpdates$(), queryString.getUpdates$())
.pipe(debounceTime(100))
.subscribe(() => applyFilters(queryString.getQuery(), filterManager.getFilters()));
.subscribe(() => applyFilters(queryString.getQuery() as Query, filterManager.getFilters()));
const timeRefreshSubscription = merge(
timefilterService.getRefreshIntervalUpdate$(),

View file

@ -8,7 +8,7 @@
import type { Filter } from '@kbn/es-query';
import type { TimeRange, RefreshInterval } from './timefilter/types';
import type { Query } from './types';
import type { Query, AggregateQuery } from './types';
/**
* All query state service state
@ -22,5 +22,5 @@ export type QueryState = {
time?: TimeRange;
refreshInterval?: RefreshInterval;
filters?: Filter[];
query?: Query;
query?: Query | AggregateQuery;
};

View file

@ -5,106 +5,74 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { DataViewsContract } from '@kbn/data-views-plugin/common';
import { queryStateToExpressionAst } from './to_expression_ast';
describe('queryStateToExpressionAst', () => {
it('returns an object with the correct structure', () => {
const actual = queryStateToExpressionAst({
it('returns an object with the correct structure', async () => {
const dataViewsService = {} as unknown as DataViewsContract;
const actual = await queryStateToExpressionAst({
filters: [],
query: { language: 'lucene', query: '' },
time: {
from: 'now',
to: 'now+7d',
},
dataViewsService,
});
expect(actual).toMatchInlineSnapshot(`
Object {
"findFunction": [Function],
"functions": Array [
Object {
"addArgument": [Function],
"arguments": Object {},
"getArgument": [Function],
"name": "kibana",
"removeArgument": [Function],
"replaceArgument": [Function],
"toAst": [Function],
"toString": [Function],
"type": "expression_function_builder",
expect(actual).toHaveProperty(
'chain.1.arguments.timeRange.0.chain.0.arguments',
expect.objectContaining({
from: ['now'],
to: ['now+7d'],
})
);
expect(actual).toHaveProperty('chain.1.arguments.filters', expect.arrayContaining([]));
});
it('returns an object with the correct structure for an SQL query', async () => {
const dataViewsService = {
getIdsWithTitle: jest.fn(() => {
return [
{
title: 'foo',
id: 'bar',
},
Object {
"addArgument": [Function],
"arguments": Object {
"filters": Array [],
"q": Array [
Object {
"findFunction": [Function],
"functions": Array [
Object {
"addArgument": [Function],
"arguments": Object {
"q": Array [
"\\"\\"",
],
},
"getArgument": [Function],
"name": "lucene",
"removeArgument": [Function],
"replaceArgument": [Function],
"toAst": [Function],
"toString": [Function],
"type": "expression_function_builder",
},
],
"toAst": [Function],
"toString": [Function],
"type": "expression_builder",
},
],
"timeRange": Array [
Object {
"findFunction": [Function],
"functions": Array [
Object {
"addArgument": [Function],
"arguments": Object {
"from": Array [
"now",
],
"to": Array [
"now+7d",
],
},
"getArgument": [Function],
"name": "timerange",
"removeArgument": [Function],
"replaceArgument": [Function],
"toAst": [Function],
"toString": [Function],
"type": "expression_function_builder",
},
],
"toAst": [Function],
"toString": [Function],
"type": "expression_builder",
},
],
},
"getArgument": [Function],
"name": "kibana_context",
"removeArgument": [Function],
"replaceArgument": [Function],
"toAst": [Function],
"toString": [Function],
"type": "expression_function_builder",
},
],
"toAst": [Function],
"toString": [Function],
"type": "expression_builder",
}
`);
];
}),
get: jest.fn(() => {
return {
title: 'foo',
id: 'bar',
timeFieldName: 'baz',
};
}),
} as unknown as DataViewsContract;
const actual = await queryStateToExpressionAst({
filters: [],
query: { sql: 'SELECT * FROM foo' },
time: {
from: 'now',
to: 'now+7d',
},
dataViewsService,
});
expect(actual).toHaveProperty(
'chain.1.arguments.timeRange.0.chain.0.arguments',
expect.objectContaining({
from: ['now'],
to: ['now+7d'],
})
);
expect(actual).toHaveProperty(
'chain.2.arguments',
expect.objectContaining({
query: ['SELECT * FROM foo'],
})
);
});
});

View file

@ -5,31 +5,73 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import {
isOfAggregateQueryType,
getAggregateQueryMode,
getIndexPatternFromSQLQuery,
Query,
} from '@kbn/es-query';
import { buildExpression, buildExpressionFunction } from '@kbn/expressions-plugin/common';
import type { DataViewsContract } from '@kbn/data-views-plugin/common';
import {
ExpressionFunctionKibana,
ExpressionFunctionKibanaContext,
filtersToAst,
QueryState,
aggregateQueryToAst,
queryToAst,
filtersToAst,
timerangeToAst,
} from '..';
interface Args extends QueryState {
dataViewsService: DataViewsContract;
inputQuery?: Query;
}
/**
* Converts QueryState to expression AST
* @param filters array of kibana filters
* @param query kibana query
* @param query kibana query or aggregate query
* @param time kibana time range
*/
export function queryStateToExpressionAst({ filters, query, time }: QueryState) {
export async function queryStateToExpressionAst({
filters,
query,
inputQuery,
time,
dataViewsService,
}: Args) {
const kibana = buildExpressionFunction<ExpressionFunctionKibana>('kibana', {});
let q;
if (inputQuery) {
q = inputQuery;
}
const kibanaContext = buildExpressionFunction<ExpressionFunctionKibanaContext>('kibana_context', {
q: query && queryToAst(query),
filters: filters && filtersToAst(filters),
q: q && queryToAst(q),
timeRange: time && timerangeToAst(time),
filters: filters && filtersToAst(filters),
});
const ast = buildExpression([kibana, kibanaContext]).toAst();
const ast = buildExpression([kibana, kibanaContext]);
if (query && isOfAggregateQueryType(query)) {
const mode = getAggregateQueryMode(query);
// sql query
if (mode === 'sql' && 'sql' in query) {
const idxPattern = getIndexPatternFromSQLQuery(query.sql);
const idsTitles = await dataViewsService.getIdsWithTitle();
const dataViewIdTitle = idsTitles.find(({ title }) => title === idxPattern);
if (dataViewIdTitle) {
const dataView = await dataViewsService.get(dataViewIdTitle.id);
const timeFieldName = dataView.timeFieldName;
const essql = aggregateQueryToAst(query, timeFieldName);
if (essql) {
ast.chain.push(essql);
}
} else {
throw new Error(`No data view found for index pattern ${idxPattern}`);
}
}
}
return ast;
}

View file

@ -10,7 +10,7 @@ import type { Query, Filter } from '@kbn/es-query';
import type { RefreshInterval, TimeRange } from './timefilter/types';
export type { RefreshInterval, TimeRange, TimeRangeBounds } from './timefilter/types';
export type { Query } from '@kbn/es-query';
export type { Query, AggregateQuery } from '@kbn/es-query';
export type SavedQueryTimeFilter = TimeRange & {
refreshInterval: RefreshInterval;

View file

@ -0,0 +1,25 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { aggregateQueryToAst } from './aggregate_query_to_ast';
describe('aggregateQueryToAst', () => {
it('should return a function', () => {
expect(aggregateQueryToAst({ sql: 'SELECT * from foo' })).toHaveProperty('type', 'function');
});
it('should forward arguments', () => {
expect(aggregateQueryToAst({ sql: 'SELECT * from foo' }, 'baz')).toHaveProperty(
'arguments',
expect.objectContaining({
query: ['SELECT * from foo'],
timeField: ['baz'],
})
);
});
});

View file

@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { buildExpressionFunction, ExpressionAstFunction } from '@kbn/expressions-plugin/common';
import { AggregateQuery } from '../../query';
import { EssqlExpressionFunctionDefinition } from './essql';
export const aggregateQueryToAst = (
query: AggregateQuery,
timeField?: string
): undefined | ExpressionAstFunction => {
if ('sql' in query) {
return buildExpressionFunction<EssqlExpressionFunctionDefinition>('essql', {
query: query.sql,
timeField,
}).toAst();
}
};

View file

@ -40,9 +40,9 @@ type Output = Observable<Datatable>;
interface Arguments {
query: string;
parameter: Array<string | number | boolean>;
count: number;
timezone: string;
parameter?: Array<string | number | boolean>;
count?: number;
timezone?: string;
timeField?: string;
}

View file

@ -27,6 +27,7 @@ export * from './numerical_range_to_ast';
export * from './query_filter';
export * from './query_filter_to_ast';
export * from './query_to_ast';
export * from './aggregate_query_to_ast';
export * from './timerange_to_ast';
export * from './kibana_context_type';
export * from './esaggs';

View file

@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n';
import { ExpressionFunctionDefinition, ExecutionContext } from '@kbn/expressions-plugin/common';
import { Adapters } from '@kbn/inspector-plugin/common';
import { Filter } from '@kbn/es-query';
import { Query, uniqFilters } from '@kbn/es-query';
import { Query, uniqFilters, AggregateQuery } from '@kbn/es-query';
import { unboxExpressionValue } from '@kbn/expressions-plugin/common';
import { SavedObjectReference } from '@kbn/core/types';
import { SavedObjectsClientCommon } from '@kbn/data-views-plugin/common';
@ -41,8 +41,11 @@ export type ExpressionFunctionKibanaContext = ExpressionFunctionDefinition<
const getParsedValue = (data: any, defaultValue: any) =>
typeof data === 'string' && data.length ? JSON.parse(data) || defaultValue : defaultValue;
const mergeQueries = (first: Query | Query[] = [], second: Query | Query[]) =>
uniqBy<Query>(
const mergeQueries = (
first: Query | AggregateQuery | Array<Query | AggregateQuery> = [],
second: Query | AggregateQuery | Array<Query | AggregateQuery>
) =>
uniqBy<Query | AggregateQuery>(
[...(Array.isArray(first) ? first : [first]), ...(Array.isArray(second) ? second : [second])],
(n: any) => JSON.stringify(n.query)
);

View file

@ -5,7 +5,7 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { Filter } from '@kbn/es-query';
import { Filter, AggregateQuery } from '@kbn/es-query';
import { ExpressionValueBoxed, ExpressionValueFilter } from '@kbn/expressions-plugin/common';
import { Query, TimeRange } from '../../query';
import { adaptToExpressionValueFilter, DataViewField } from '../..';
@ -13,7 +13,7 @@ import { adaptToExpressionValueFilter, DataViewField } from '../..';
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export type ExecutionContextSearch = {
filters?: Filter[];
query?: Query | Query[];
query?: Query | AggregateQuery | Array<Query | AggregateQuery>;
timeRange?: TimeRange;
};

View file

@ -5,9 +5,8 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { Query, AggregateQuery, isOfAggregateQueryType } from '@kbn/es-query';
import { has } from 'lodash';
import { Query } from '../../query/types';
/**
* Creates a standardized query object from old queries that were either strings or pure ES query DSL
@ -16,9 +15,15 @@ import { Query } from '../../query/types';
* @return Object
*/
export function migrateLegacyQuery(query: Query | { [key: string]: any } | string): Query {
export function migrateLegacyQuery(
query: Query | { [key: string]: any } | string | AggregateQuery
): Query | AggregateQuery {
// Lucene was the only option before, so language-less queries are all lucene
// If the query is already a AggregateQuery, just return it
if (!has(query, 'language')) {
if (typeof query === 'object' && isOfAggregateQueryType(query)) {
return query;
}
return { query, language: 'lucene' };
}

View file

@ -72,7 +72,7 @@ import {
} from 'rxjs/operators';
import { defer, EMPTY, from, lastValueFrom, Observable } from 'rxjs';
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { buildEsQuery, Filter } from '@kbn/es-query';
import { buildEsQuery, Filter, isOfQueryType } from '@kbn/es-query';
import { fieldWildcardFilter } from '@kbn/kibana-utils-plugin/common';
import { getHighlightRequest } from '@kbn/field-formats-plugin/common';
import type { DataView } from '@kbn/data-views-plugin/common';
@ -261,7 +261,11 @@ export class SearchSource {
filters = this.getFilters(originalFilters);
}
const queryString = Array.isArray(query) ? query.map((q) => q.query) : query?.query;
const queryString = Array.isArray(query)
? query.map((q) => q.query)
: isOfQueryType(query)
? query?.query
: undefined;
const indexPatternFromQuery =
typeof queryString === 'string'

View file

@ -7,13 +7,13 @@
*/
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { Query, AggregateQuery } from '@kbn/es-query';
import { SerializableRecord } from '@kbn/utility-types';
import { PersistableStateService } from '@kbn/kibana-utils-plugin/common';
import type { Filter } from '@kbn/es-query';
import type { DataView, DataViewSpec } from '@kbn/data-views-plugin/common';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { AggConfigSerialized, IAggConfigs } from '../../../public';
import { Query } from '../..';
import type { SearchSource } from './search_source';
/**
@ -78,7 +78,7 @@ export interface SearchSourceFields {
/**
* {@link Query}
*/
query?: Query;
query?: Query | AggregateQuery;
/**
* {@link Filter}
*/
@ -125,7 +125,7 @@ export type SerializedSearchSourceFields = {
/**
* {@link Query}
*/
query?: Query;
query?: Query | AggregateQuery;
/**
* {@link Filter}
*/

View file

@ -10,7 +10,7 @@ import { QueryStringManager } from './query_string_manager';
import { Storage } from '@kbn/kibana-utils-plugin/public/storage';
import { StubBrowserStorage } from '@kbn/test-jest-helpers';
import { coreMock } from '@kbn/core/public/mocks';
import { Query } from '../../../common/query';
import { Query, AggregateQuery } from '../../../common/query';
describe('QueryStringManager', () => {
let service: QueryStringManager;
@ -24,7 +24,7 @@ describe('QueryStringManager', () => {
test('getUpdates$ is a cold emits only after query changes', () => {
const obs$ = service.getUpdates$();
const emittedValues: Query[] = [];
const emittedValues: Array<Query | AggregateQuery> = [];
obs$.subscribe((v) => {
emittedValues.push(v);
});

View file

@ -10,18 +10,19 @@ import { BehaviorSubject } from 'rxjs';
import { skip } from 'rxjs/operators';
import { PublicMethodsOf } from '@kbn/utility-types';
import { CoreStart } from '@kbn/core/public';
import type { Query } from '@kbn/es-query';
import type { Query, AggregateQuery } from '@kbn/es-query';
import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public';
import { isEqual } from 'lodash';
import { KIBANA_USER_QUERY_LANGUAGE_KEY, UI_SETTINGS } from '../../../common';
export class QueryStringManager {
private query$: BehaviorSubject<Query>;
private query$: BehaviorSubject<Query | AggregateQuery>;
constructor(
private readonly storage: IStorageWrapper,
private readonly uiSettings: CoreStart['uiSettings']
) {
this.query$ = new BehaviorSubject<Query>(this.getDefaultQuery());
this.query$ = new BehaviorSubject<Query | AggregateQuery>(this.getDefaultQuery());
}
private getDefaultLanguage() {
@ -38,7 +39,7 @@ export class QueryStringManager {
};
}
public formatQuery(query: Query | string | undefined): Query {
public formatQuery(query: Query | AggregateQuery | string | undefined): Query | AggregateQuery {
if (!query) {
return this.getDefaultQuery();
} else if (typeof query === 'string') {
@ -55,17 +56,17 @@ export class QueryStringManager {
return this.query$.asObservable().pipe(skip(1));
};
public getQuery = (): Query => {
public getQuery = (): Query | AggregateQuery => {
return this.query$.getValue();
};
/**
* Updates the query.
* @param {Query} query
* @param {Query | AggregateQuery} query
*/
public setQuery = (query: Query) => {
public setQuery = (query: Query | AggregateQuery) => {
const curQuery = this.query$.getValue();
if (query?.language !== curQuery.language || query?.query !== curQuery.query) {
if (!isEqual(query, curQuery)) {
this.query$.next(query);
}
};

View file

@ -7,7 +7,7 @@
*/
import { coreMock } from '@kbn/core/server/mocks';
import { FilterStateStore } from '@kbn/es-query';
import { FilterStateStore, Query } from '@kbn/es-query';
import { DATA_VIEW_SAVED_OBJECT_TYPE } from '../../common';
import type { SavedObject, SavedQueryAttributes } from '../../common';
import { registerSavedQueryRouteHandlerContext } from './route_handler_context';
@ -438,7 +438,8 @@ describe('saved query route handler context', () => {
});
const response = await context.get('food');
expect(response.attributes.query.query).toEqual({ x: 'y' });
const query = response.attributes.query as Query;
expect(query.query).toEqual({ x: 'y' });
});
it('should handle null string', async () => {
@ -460,7 +461,8 @@ describe('saved query route handler context', () => {
});
const response = await context.get('food');
expect(response.attributes.query.query).toEqual('null');
const query = response.attributes.query as Query;
expect(query.query).toEqual('null');
});
it('should handle null quoted string', async () => {
@ -482,7 +484,8 @@ describe('saved query route handler context', () => {
});
const response = await context.get('food');
expect(response.attributes.query.query).toEqual('"null"');
const query = response.attributes.query as Query;
expect(query.query).toEqual('"null"');
});
it('should not lose quotes', async () => {
@ -504,7 +507,8 @@ describe('saved query route handler context', () => {
});
const response = await context.get('food');
expect(response.attributes.query.query).toEqual('"Bob"');
const query = response.attributes.query as Query;
expect(query.query).toEqual('"Bob"');
});
it('should inject references', async () => {

View file

@ -7,7 +7,7 @@
*/
import { CustomRequestHandlerContext, RequestHandlerContext, SavedObject } from '@kbn/core/server';
import { isFilters } from '@kbn/es-query';
import { isFilters, isOfQueryType } from '@kbn/es-query';
import { isQuery, SavedQueryAttributes } from '../../common';
import { extract, inject } from '../../common/query/filters/persistable_state';
@ -17,7 +17,7 @@ function injectReferences({
references,
}: Pick<SavedObject<SavedQueryAttributes>, 'id' | 'attributes' | 'references'>) {
const { query } = attributes;
if (typeof query.query === 'string') {
if (isOfQueryType(query) && typeof query.query === 'string') {
try {
const parsed = JSON.parse(query.query);
query.query = parsed instanceof Object ? parsed : query.query;
@ -37,13 +37,22 @@ function extractReferences({
timefilter,
}: SavedQueryAttributes) {
const { state: extractedFilters, references } = extract(filters);
const isOfQueryTypeQuery = isOfQueryType(query);
let queryString = '';
if (isOfQueryTypeQuery) {
if (typeof query.query === 'string') {
queryString = query.query;
} else {
queryString = JSON.stringify(query.query);
}
}
const attributes: SavedQueryAttributes = {
title: title.trim(),
description: description.trim(),
query: {
...query,
query: typeof query.query === 'string' ? query.query : JSON.stringify(query.query),
...(queryString && { query: queryString }),
},
filters: extractedFilters,
...(timefilter && { timefilter }),

View file

@ -28,3 +28,4 @@ export const TRUNCATE_MAX_HEIGHT = 'truncate:maxHeight';
export const ROW_HEIGHT_OPTION = 'discover:rowHeightOption';
export const SEARCH_EMBEDDABLE_TYPE = 'search';
export const HIDE_ANNOUNCEMENTS = 'hideAnnouncements';
export const ENABLE_SQL = 'discover:enableSql';

View file

@ -14,7 +14,8 @@
"uiActions",
"savedObjects",
"dataViewFieldEditor",
"dataViewEditor"
"dataViewEditor",
"expressions"
],
"optionalPlugins": ["home", "share", "usageCollection", "spaces", "triggersActionsUi"],
"requiredBundles": ["kibanaUtils", "kibanaReact", "dataViews", "unifiedSearch"],

View file

@ -0,0 +1,139 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { FunctionComponent } from 'react';
import { action } from '@storybook/addon-actions';
import { EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme';
import { LIGHT_THEME } from '@elastic/charts';
import { FieldFormat } from '@kbn/field-formats-plugin/common';
import { identity } from 'lodash';
import { CoreStart, IUiSettingsClient, PluginInitializerContext } from '@kbn/core/public';
import {
DEFAULT_COLUMNS_SETTING,
DOC_TABLE_LEGACY,
MAX_DOC_FIELDS_DISPLAYED,
ROW_HEIGHT_OPTION,
SAMPLE_SIZE_SETTING,
SEARCH_FIELDS_FROM_SOURCE,
SHOW_MULTIFIELDS,
} from '../../../common';
import { SIDEBAR_CLOSED_KEY } from '../../application/main/components/layout/discover_layout';
import { LocalStorageMock } from '../local_storage_mock';
import { DiscoverServices } from '../../build_services';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { Plugin as NavigationPublicPlugin } from '@kbn/navigation-plugin/public';
import { SearchBar, UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
import { SavedQuery } from '@kbn/data-plugin/public';
const NavigationPlugin = new NavigationPublicPlugin({} as PluginInitializerContext);
export const uiSettingsMock = {
get: (key: string) => {
if (key === MAX_DOC_FIELDS_DISPLAYED) {
return 3;
} else if (key === SAMPLE_SIZE_SETTING) {
return 10;
} else if (key === DEFAULT_COLUMNS_SETTING) {
return ['default_column'];
} else if (key === DOC_TABLE_LEGACY) {
return false;
} else if (key === SEARCH_FIELDS_FROM_SOURCE) {
return false;
} else if (key === SHOW_MULTIFIELDS) {
return false;
} else if (key === ROW_HEIGHT_OPTION) {
return 3;
} else if (key === 'dateFormat:tz') {
return true;
}
},
isDefault: () => {
return true;
},
} as unknown as IUiSettingsClient;
const services = {
core: { http: { basePath: { prepend: () => void 0 } } },
storage: new LocalStorageMock({
[SIDEBAR_CLOSED_KEY]: false,
}) as unknown as Storage,
data: {
query: {
timefilter: {
timefilter: {
setTime: action('Set timefilter time'),
getAbsoluteTime: () => {
return { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' };
},
},
},
savedQueries: { findSavedQueries: () => Promise.resolve({ queries: [] as SavedQuery[] }) },
},
dataViews: {
getIdsWithTitle: () => Promise.resolve([]),
},
},
uiSettings: uiSettingsMock,
dataViewFieldEditor: {
openEditor: () => void 0,
userPermissions: {
editIndexPattern: () => void 0,
},
},
navigation: NavigationPlugin.start({} as CoreStart, {
unifiedSearch: { ui: { SearchBar } } as unknown as UnifiedSearchPublicPluginStart,
}),
theme: {
useChartsTheme: () => ({
...EUI_CHARTS_THEME_LIGHT.theme,
chartPaddings: {
top: 0,
left: 0,
bottom: 0,
right: 0,
},
heatmap: { xAxisLabel: { rotation: {} } },
}),
useChartsBaseTheme: () => LIGHT_THEME,
},
capabilities: {
visualize: {
show: true,
},
discover: {
save: false,
},
advancedSettings: {
save: true,
},
},
docLinks: { links: { discover: {} } },
addBasePath: (path: string) => path,
filterManager: {
getGlobalFilters: () => [],
getAppFilters: () => [],
},
history: () => ({}),
fieldFormats: {
deserialize: () => {
const DefaultFieldFormat = FieldFormat.from(identity);
return new DefaultFieldFormat();
},
},
toastNotifications: {
addInfo: action('add toast'),
},
} as unknown as DiscoverServices;
export const withDiscoverServices = (Component: FunctionComponent) => {
return (props: object) => (
<KibanaContextProvider services={services}>
<Component {...props} />
</KibanaContextProvider>
);
};

View file

@ -8,6 +8,7 @@
import { EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme';
import { DiscoverServices } from '../build_services';
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
import { expressionsPluginMock } from '@kbn/expressions-plugin/public/mocks';
import { chromeServiceMock, coreMock, docLinksServiceMock } from '@kbn/core/public/mocks';
import {
CONTEXT_STEP_SETTING,
@ -25,6 +26,7 @@ import { FORMATS_UI_SETTINGS } from '@kbn/field-formats-plugin/common';
import { LocalStorageMock } from './local_storage_mock';
import { fieldFormatsMock } from '@kbn/field-formats-plugin/common/mocks';
const dataPlugin = dataPluginMock.createStartContract();
const expressionsPlugin = expressionsPluginMock.createStartContract();
export const discoverServiceMock = {
core: coreMock.createStart(),
@ -95,7 +97,7 @@ export const discoverServiceMock = {
},
},
navigation: {
ui: { TopNavMenu },
ui: { TopNavMenu, AggregateQueryTopNavMenu: TopNavMenu },
},
metadata: {
branch: 'test',
@ -110,4 +112,5 @@ export const discoverServiceMock = {
addInfo: jest.fn(),
addWarning: jest.fn(),
},
expressions: expressionsPlugin,
} as unknown as DiscoverServices;

View file

@ -24,7 +24,9 @@ import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
const mockFilterManager = createFilterManagerMock();
const mockNavigationPlugin = { ui: { TopNavMenu: mockTopNavMenu } };
const mockNavigationPlugin = {
ui: { TopNavMenu: mockTopNavMenu, AggregateQueryTopNavMenu: mockTopNavMenu },
};
describe('ContextApp test', () => {
const services = {

View file

@ -135,7 +135,7 @@ export const ContextApp = ({ indexPattern, anchorId }: ContextAppProps) => {
[filterManager, indexPatterns, indexPattern, capabilities]
);
const TopNavMenu = navigation.ui.TopNavMenu;
const TopNavMenu = navigation.ui.AggregateQueryTopNavMenu;
const getNavBarProps = () => {
return {
appName: 'context',

View file

@ -62,7 +62,7 @@ describe('Document Explorer Update callout', () => {
it('should start a tour when the button is clicked', () => {
const result = mountWithIntl(
<KibanaContextProvider services={defaultServices}>
<DiscoverTourProvider>
<DiscoverTourProvider isPlainRecord={false}>
<DocumentExplorerUpdateCallout />
</DiscoverTourProvider>
</KibanaContextProvider>

View file

@ -7,7 +7,7 @@
*/
import React, { useEffect, useMemo, useRef, useState } from 'react';
import type { Filter, Query } from '@kbn/es-query';
import type { Filter, Query, AggregateQuery } from '@kbn/es-query';
import { METRIC_TYPE, UiCounterMetricType } from '@kbn/analytics';
import type { DataViewField, DataView } from '@kbn/data-views-plugin/public';
import {
@ -26,7 +26,7 @@ import { AvailableFields$, DataRefetch$ } from '../../hooks/use_saved_search';
export interface DataVisualizerGridEmbeddableInput extends EmbeddableInput {
dataView: DataView;
savedSearch?: SavedSearch;
query?: Query;
query?: Query | AggregateQuery;
visibleFieldNames?: string[];
filters?: Filter[];
showPreviewByDefault?: boolean;
@ -65,7 +65,7 @@ export interface FieldStatisticsTableProps {
/**
* Optional query to update the table content
*/
query?: Query;
query?: Query | AggregateQuery;
/**
* Filters query to update the table content
*/

View file

@ -6,30 +6,64 @@
* Side Public License, v 1.
*/
import React, { useState } from 'react';
import { storiesOf } from '@storybook/react';
import React from 'react';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { getIndexPatternMock } from './get_index_pattern_mock';
import { getServices } from './get_services';
import { getLayoutProps } from './get_layout_props';
import { getIndexPatternMock } from '../../../../../__mocks__/__storybook_mocks__/get_index_pattern_mock';
import { withDiscoverServices } from '../../../../../__mocks__/__storybook_mocks__/with_discover_services';
import { getDocumentsLayoutProps, getPlainRecordLayoutProps } from './get_layout_props';
import { DiscoverLayout } from '../discover_layout';
import { setHeaderActionMenuMounter } from '../../../../../kibana_services';
import { AppState } from '../../../services/discover_state';
import { DiscoverLayoutProps } from '../types';
setHeaderActionMenuMounter(() => void 0);
storiesOf('components/layout/DiscoverLayout', module).add('Data view with timestamp', () => (
<IntlProvider locale="en">
<KibanaContextProvider services={getServices()}>
<DiscoverLayout {...getLayoutProps(getIndexPatternMock(true))} />
</KibanaContextProvider>
</IntlProvider>
));
const DiscoverLayoutStory = (layoutProps: DiscoverLayoutProps) => {
const [state, setState] = useState(layoutProps.state);
storiesOf('components/layout/DiscoverLayout', module).add('Data view without timestamp', () => (
<IntlProvider locale="en">
<KibanaContextProvider services={getServices()}>
<DiscoverLayout {...getLayoutProps(getIndexPatternMock(false))} />
</KibanaContextProvider>
</IntlProvider>
));
const setAppState = (newState: Partial<AppState>) => {
setState((prevState) => ({ ...prevState, ...newState }));
};
const getState = () => state;
return (
<DiscoverLayout
{...layoutProps}
state={state}
stateContainer={{
...layoutProps.stateContainer,
appStateContainer: { ...layoutProps.stateContainer.appStateContainer, getState },
setAppState,
}}
/>
);
};
storiesOf('components/layout/DiscoverLayout', module).add(
'Data view with timestamp',
withDiscoverServices(() => (
<IntlProvider locale="en">
<DiscoverLayoutStory {...getDocumentsLayoutProps(getIndexPatternMock(true))} />
</IntlProvider>
))
);
storiesOf('components/layout/DiscoverLayout', module).add(
'Data view without timestamp',
withDiscoverServices(() => (
<IntlProvider locale="en">
<DiscoverLayoutStory {...getDocumentsLayoutProps(getIndexPatternMock(false))} />
</IntlProvider>
))
);
storiesOf('components/layout/DiscoverLayout', module).add(
'SQL view',
withDiscoverServices(() => (
<IntlProvider locale="en">
<DiscoverLayoutStory {...getPlainRecordLayoutProps(getIndexPatternMock(false))} />
</IntlProvider>
))
);

View file

@ -18,81 +18,76 @@ import {
DataDocuments$,
DataMain$,
DataTotalHits$,
RecordRawType,
} from '../../../hooks/use_saved_search';
import { buildDataTableRecordList } from '../../../../../utils/build_data_record';
import { esHits } from '../../../../../__mocks__/es_hits';
import { Chart } from '../../chart/point_series';
import { SavedSearch } from '../../../../..';
import { GetStateReturn } from '../../../services/discover_state';
import { DiscoverLayoutProps } from '../types';
import { GetStateReturn } from '../../../services/discover_state';
export function getLayoutProps(indexPattern: DataView) {
const searchSourceMock = {} as unknown as SearchSource;
const chartData = {
xAxisOrderedValues: [
1623880800000, 1623967200000, 1624053600000, 1624140000000, 1624226400000, 1624312800000,
1624399200000, 1624485600000, 1624572000000, 1624658400000, 1624744800000, 1624831200000,
1624917600000, 1625004000000, 1625090400000,
],
xAxisFormat: { id: 'date', params: { pattern: 'YYYY-MM-DD' } },
xAxisLabel: 'order_date per day',
yAxisFormat: { id: 'number' },
ordered: {
date: true,
interval: {
asMilliseconds: () => 1000,
},
intervalESUnit: 'd',
intervalESValue: 1,
min: '2021-03-18T08:28:56.411Z',
max: '2021-07-01T07:28:56.411Z',
},
yAxisLabel: 'Count',
values: [
{ x: 1623880800000, y: 134 },
{ x: 1623967200000, y: 152 },
{ x: 1624053600000, y: 141 },
{ x: 1624140000000, y: 138 },
{ x: 1624226400000, y: 142 },
{ x: 1624312800000, y: 157 },
{ x: 1624399200000, y: 149 },
{ x: 1624485600000, y: 146 },
{ x: 1624572000000, y: 170 },
{ x: 1624658400000, y: 137 },
{ x: 1624744800000, y: 150 },
{ x: 1624831200000, y: 144 },
{ x: 1624917600000, y: 147 },
{ x: 1625004000000, y: 137 },
{ x: 1625090400000, y: 66 },
],
} as unknown as Chart;
const indexPatternList = [indexPattern].map((ip) => {
return { ...ip, ...{ attributes: { title: ip.title } } };
}) as unknown as Array<SavedObject<DataViewAttributes>>;
const main$ = new BehaviorSubject({
const documentObservables = {
main$: new BehaviorSubject({
fetchStatus: FetchStatus.COMPLETE,
foundDocuments: true,
}) as DataMain$;
}) as DataMain$,
const documents$ = new BehaviorSubject({
documents$: new BehaviorSubject({
fetchStatus: FetchStatus.COMPLETE,
result: buildDataTableRecordList(esHits),
}) as DataDocuments$;
}) as DataDocuments$,
const availableFields$ = new BehaviorSubject({
availableFields$: new BehaviorSubject({
fetchStatus: FetchStatus.COMPLETE,
fields: [] as string[],
}) as AvailableFields$;
}) as AvailableFields$,
const totalHits$ = new BehaviorSubject({
totalHits$: new BehaviorSubject({
fetchStatus: FetchStatus.COMPLETE,
result: Number(esHits.length),
}) as DataTotalHits$;
}) as DataTotalHits$,
const chartData = {
xAxisOrderedValues: [
1623880800000, 1623967200000, 1624053600000, 1624140000000, 1624226400000, 1624312800000,
1624399200000, 1624485600000, 1624572000000, 1624658400000, 1624744800000, 1624831200000,
1624917600000, 1625004000000, 1625090400000,
],
xAxisFormat: { id: 'date', params: { pattern: 'YYYY-MM-DD' } },
xAxisLabel: 'order_date per day',
yAxisFormat: { id: 'number' },
ordered: {
date: true,
interval: {
asMilliseconds: () => 1000,
},
intervalESUnit: 'd',
intervalESValue: 1,
min: '2021-03-18T08:28:56.411Z',
max: '2021-07-01T07:28:56.411Z',
},
yAxisLabel: 'Count',
values: [
{ x: 1623880800000, y: 134 },
{ x: 1623967200000, y: 152 },
{ x: 1624053600000, y: 141 },
{ x: 1624140000000, y: 138 },
{ x: 1624226400000, y: 142 },
{ x: 1624312800000, y: 157 },
{ x: 1624399200000, y: 149 },
{ x: 1624485600000, y: 146 },
{ x: 1624572000000, y: 170 },
{ x: 1624658400000, y: 137 },
{ x: 1624744800000, y: 150 },
{ x: 1624831200000, y: 144 },
{ x: 1624917600000, y: 147 },
{ x: 1625004000000, y: 137 },
{ x: 1625090400000, y: 66 },
],
} as unknown as Chart;
const charts$ = new BehaviorSubject({
charts$: new BehaviorSubject({
fetchStatus: FetchStatus.COMPLETE,
chartData,
bucketInterval: {
@ -100,29 +95,59 @@ export function getLayoutProps(indexPattern: DataView) {
description: 'test',
scale: 2,
},
}) as DataCharts$;
}) as DataCharts$,
};
const plainRecordObservables = {
main$: new BehaviorSubject({
fetchStatus: FetchStatus.COMPLETE,
foundDocuments: true,
recordRawType: RecordRawType.PLAIN,
}) as DataMain$,
documents$: new BehaviorSubject({
fetchStatus: FetchStatus.COMPLETE,
result: buildDataTableRecordList(esHits),
recordRawType: RecordRawType.PLAIN,
}) as DataDocuments$,
availableFields$: new BehaviorSubject({
fetchStatus: FetchStatus.COMPLETE,
fields: [] as string[],
recordRawType: RecordRawType.PLAIN,
}) as AvailableFields$,
totalHits$: new BehaviorSubject({
fetchStatus: FetchStatus.COMPLETE,
recordRawType: RecordRawType.PLAIN,
}) as DataTotalHits$,
charts$: new BehaviorSubject({
fetchStatus: FetchStatus.COMPLETE,
recordRawType: RecordRawType.PLAIN,
}) as DataCharts$,
};
const getCommonProps = (dataView: DataView) => {
const searchSourceMock = {} as unknown as SearchSource;
const dataViewList = [dataView].map((ip) => {
return { ...ip, ...{ attributes: { title: ip.title } } };
}) as unknown as Array<SavedObject<DataViewAttributes>>;
const savedSearchData$ = {
main$,
documents$,
totalHits$,
charts$,
availableFields$,
};
const savedSearchMock = {} as unknown as SavedSearch;
return {
indexPattern,
indexPatternList,
indexPattern: dataView,
indexPatternList: dataViewList,
inspectorAdapters: { requests: new RequestAdapter() },
navigateTo: action('navigate to somewhere nice'),
onChangeIndexPattern: action('change the data view'),
onUpdateQuery: action('update the query'),
resetSavedSearch: action('reset the saved search the query'),
savedSearch: savedSearchMock,
savedSearchData$,
savedSearchRefetch$: new Subject(),
searchSource: searchSourceMock,
state: { columns: ['name', 'message', 'bytes'], sort: [['date', 'desc']] },
stateContainer: {
setAppState: action('Set app state'),
appStateContainer: {
@ -133,5 +158,34 @@ export function getLayoutProps(indexPattern: DataView) {
},
} as unknown as GetStateReturn,
setExpandedDoc: action('opening an expanded doc'),
};
};
export function getDocumentsLayoutProps(dataView: DataView) {
return {
...getCommonProps(dataView),
savedSearchData$: documentObservables,
state: {
columns: ['name', 'message', 'bytes'],
sort: [['date', 'desc']],
query: {
language: 'kuery',
query: '',
},
},
} as unknown as DiscoverLayoutProps;
}
export const getPlainRecordLayoutProps = (dataView: DataView) => {
return {
...getCommonProps(dataView),
savedSearchData$: plainRecordObservables,
state: {
columns: ['name', 'message', 'bytes'],
sort: [['date', 'desc']],
query: {
sql: 'SELECT * FROM "kibana_sample_data_ecommerce"',
},
},
} as unknown as DiscoverLayoutProps;
};

View file

@ -5,13 +5,13 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { useMemo, useCallback, memo } from 'react';
import React, { memo, useCallback, useMemo } from 'react';
import {
EuiFlexItem,
EuiSpacer,
EuiText,
EuiLoadingSpinner,
EuiScreenReaderOnly,
EuiSpacer,
EuiText,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { DataView } from '@kbn/data-views-plugin/public';
@ -28,7 +28,7 @@ import {
} from '../../../../../common';
import { useColumns } from '../../../../hooks/use_data_grid_columns';
import { SavedSearch } from '../../../../services/saved_searches';
import { DataDocumentsMsg, DataDocuments$ } from '../../hooks/use_saved_search';
import { DataDocuments$, DataDocumentsMsg, RecordRawType } from '../../hooks/use_saved_search';
import { AppState, GetStateReturn } from '../../services/discover_state';
import { useDataState } from '../../hooks/use_data_state';
import { DocTableInfinite } from '../../../../components/doc_table/doc_table_infinite';
@ -37,6 +37,7 @@ import { DocumentExplorerCallout } from '../document_explorer_callout';
import { DocumentExplorerUpdateCallout } from '../document_explorer_callout/document_explorer_update_callout';
import { DiscoverTourProvider } from '../../../../components/discover_tour';
import { DataTableRecord } from '../../../../types';
import { getRawRecordType } from '../../utils/get_raw_record_type';
const DocTableInfiniteMemoized = React.memo(DocTableInfinite);
const DataGridMemoized = React.memo(DiscoverGrid);
@ -56,12 +57,12 @@ function DiscoverDocumentsComponent({
expandedDoc?: DataTableRecord;
indexPattern: DataView;
navigateTo: (url: string) => void;
onAddFilter: DocViewFilterFn;
onAddFilter?: DocViewFilterFn;
savedSearch: SavedSearch;
setExpandedDoc: (doc?: DataTableRecord) => void;
state: AppState;
stateContainer: GetStateReturn;
onFieldEdited: () => void;
onFieldEdited?: () => void;
}) {
const { capabilities, indexPatterns, uiSettings } = useDiscoverServices();
const useNewFieldsApi = useMemo(() => !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE), [uiSettings]);
@ -71,7 +72,10 @@ function DiscoverDocumentsComponent({
const documentState: DataDocumentsMsg = useDataState(documents$);
const isLoading = documentState.fetchStatus === FetchStatus.LOADING;
const isPlainRecord = useMemo(
() => getRawRecordType(state.query) === RecordRawType.PLAIN,
[state.query]
);
const rows = useMemo(() => documentState.result || [], [documentState.result]);
const { columns, onAddColumn, onRemoveColumn, onMoveColumn, onSetColumns } = useColumns({
@ -119,8 +123,11 @@ function DiscoverDocumentsComponent({
);
const showTimeCol = useMemo(
() => !uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING, false) && !!indexPattern.timeFieldName,
[uiSettings, indexPattern.timeFieldName]
() =>
!isPlainRecord &&
!uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING, false) &&
!!indexPattern.timeFieldName,
[isPlainRecord, uiSettings, indexPattern.timeFieldName]
);
if (
@ -160,7 +167,7 @@ function DiscoverDocumentsComponent({
onFilter={onAddFilter as DocViewFilterFn}
onMoveColumn={onMoveColumn}
onRemoveColumn={onRemoveColumn}
onSort={onSort}
onSort={!isPlainRecord ? onSort : undefined}
useNewFieldsApi={useNewFieldsApi}
dataTestSubj="discoverDocTable"
/>
@ -168,8 +175,8 @@ function DiscoverDocumentsComponent({
)}
{!isLegacy && (
<>
{!hideAnnouncements && (
<DiscoverTourProvider>
{!hideAnnouncements && !isPlainRecord && (
<DiscoverTourProvider isPlainRecord={isPlainRecord}>
<DocumentExplorerUpdateCallout />
</DiscoverTourProvider>
)}
@ -185,18 +192,20 @@ function DiscoverDocumentsComponent({
sampleSize={sampleSize}
searchDescription={savedSearch.description}
searchTitle={savedSearch.title}
setExpandedDoc={setExpandedDoc}
setExpandedDoc={!isPlainRecord ? setExpandedDoc : undefined}
showTimeCol={showTimeCol}
settings={state.grid}
onAddColumn={onAddColumn}
onFilter={onAddFilter as DocViewFilterFn}
onRemoveColumn={onRemoveColumn}
onSetColumns={onSetColumns}
onSort={onSort}
onSort={!isPlainRecord ? onSort : undefined}
onResize={onResize}
useNewFieldsApi={useNewFieldsApi}
rowHeightState={state.rowHeight}
onUpdateRowHeight={onUpdateRowHeight}
isSortEnabled={!isPlainRecord}
isPlainRecord={isPlainRecord}
rowsPerPageState={state.rowsPerPage}
onUpdateRowsPerPage={onUpdateRowsPerPage}
onFieldEdited={onFieldEdited}

View file

@ -9,6 +9,7 @@
import React from 'react';
import { Subject, BehaviorSubject } from 'rxjs';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import type { Query, AggregateQuery } from '@kbn/es-query';
import { setHeaderActionMenuMounter } from '../../../../kibana_services';
import { DiscoverLayout, SIDEBAR_CLOSED_KEY } from './discover_layout';
import { esHits } from '../../../../__mocks__/es_hits';
@ -26,6 +27,7 @@ import {
DataDocuments$,
DataMain$,
DataTotalHits$,
RecordRawType,
} from '../../hooks/use_saved_search';
import { discoverServiceMock } from '../../../../__mocks__/services';
import { FetchStatus } from '../../../types';
@ -42,7 +44,9 @@ setHeaderActionMenuMounter(jest.fn());
function mountComponent(
indexPattern: DataView,
prevSidebarClosed?: boolean,
mountOptions: { attachTo?: HTMLElement } = {}
mountOptions: { attachTo?: HTMLElement } = {},
query?: Query | AggregateQuery,
isPlainRecord?: boolean
) {
const searchSourceMock = createSearchSourceMock({});
const services = {
@ -61,6 +65,7 @@ function mountComponent(
const main$ = new BehaviorSubject({
fetchStatus: FetchStatus.COMPLETE,
recordRawType: isPlainRecord ? RecordRawType.PLAIN : RecordRawType.DOCUMENT,
foundDocuments: true,
}) as DataMain$;
@ -148,7 +153,7 @@ function mountComponent(
savedSearchData$,
savedSearchRefetch$: new Subject(),
searchSource: searchSourceMock,
state: { columns: [] },
state: { columns: [], query },
stateContainer: {
setAppState: () => {},
appStateContainer: {
@ -179,6 +184,17 @@ describe('Discover component', () => {
expect(component.find('[data-test-subj="discoverChartOptionsToggle"]').exists()).toBeTruthy();
});
test('sql query displays no chart toggle', () => {
const component = mountComponent(
indexPatternWithTimefieldMock,
false,
{},
{ sql: 'SELECT * FROM test' },
true
);
expect(component.find('[data-test-subj="discoverChartOptionsToggle"]').exists()).toBeFalsy();
});
test('the saved search title h1 gains focus on navigate', () => {
const container = document.createElement('div');
document.body.appendChild(container);

View file

@ -20,6 +20,7 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { METRIC_TYPE } from '@kbn/analytics';
import { isOfQueryType } from '@kbn/es-query';
import classNames from 'classnames';
import { generateFilters } from '@kbn/data-plugin/public';
import { DataView, DataViewField, DataViewType } from '@kbn/data-views-plugin/public';
@ -36,7 +37,7 @@ import { DocViewFilterFn } from '../../../../services/doc_views/doc_views_types'
import { DiscoverChart } from '../chart';
import { getResultState } from '../../utils/get_result_state';
import { DiscoverUninitialized } from '../uninitialized/uninitialized';
import { DataMainMsg } from '../../hooks/use_saved_search';
import { DataMainMsg, RecordRawType } from '../../hooks/use_saved_search';
import { useColumns } from '../../../../hooks/use_data_grid_columns';
import { DiscoverDocuments } from './discover_documents';
import { FetchStatus } from '../../../types';
@ -46,6 +47,7 @@ import { FieldStatisticsTable } from '../field_stats_table';
import { VIEW_MODE } from '../../../../components/view_mode_toggle';
import { DOCUMENTS_VIEW_CLICK, FIELD_STATISTICS_VIEW_CLICK } from '../field_stats_table/constants';
import { hasActiveFilter } from './utils';
import { getRawRecordType } from '../../utils/get_raw_record_type';
/**
* Local storage key for sidebar persistence state
@ -88,6 +90,7 @@ export function DiscoverLayout({
} = useDiscoverServices();
const { main$, charts$, totalHits$ } = savedSearchData$;
const [inspectorSession, setInspectorSession] = useState<InspectorSession | undefined>(undefined);
const dataState: DataMainMsg = useDataState(main$);
const viewMode = useMemo(() => {
if (uiSettings.get(SHOW_FIELD_STATISTICS) !== true) return VIEW_MODE.DOCUMENT_LEVEL;
@ -110,7 +113,6 @@ export function DiscoverLayout({
);
const fetchCounter = useRef<number>(0);
const dataState: DataMainMsg = useDataState(main$);
useEffect(() => {
if (dataState.fetchStatus === FetchStatus.LOADING) {
@ -130,9 +132,13 @@ export function DiscoverLayout({
const [isSidebarClosed, setIsSidebarClosed] = useState(initialSidebarClosed);
const useNewFieldsApi = useMemo(() => !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE), [uiSettings]);
const isPlainRecord = useMemo(
() => getRawRecordType(state.query) === RecordRawType.PLAIN,
[state.query]
);
const resultState = useMemo(
() => getResultState(dataState.fetchStatus, dataState.foundDocuments!),
[dataState.fetchStatus, dataState.foundDocuments]
() => getResultState(dataState.fetchStatus, dataState.foundDocuments!, isPlainRecord),
[dataState.fetchStatus, dataState.foundDocuments, isPlainRecord]
);
const onOpenInspector = useCallback(() => {
@ -207,6 +213,12 @@ export function DiscoverLayout({
savedSearchTitle.current?.focus();
}, []);
const textBasedLanguageModeErrors = useMemo(() => {
if (isPlainRecord) {
return dataState.error;
}
}, [dataState.error, isPlainRecord]);
return (
<EuiPage className="dscPage" data-fetch-counter={fetchCounter.current}>
<h1
@ -239,6 +251,8 @@ export function DiscoverLayout({
updateQuery={onUpdateQuery}
resetSavedSearch={resetSavedSearch}
onChangeIndexPattern={onChangeIndexPattern}
isPlainRecord={isPlainRecord}
textBasedLanguageModeErrors={textBasedLanguageModeErrors}
onFieldEdited={onFieldEdited}
/>
<EuiPageBody className="dscPageBody" aria-describedby="savedSearchTitle">
@ -254,7 +268,7 @@ export function DiscoverLayout({
documents$={savedSearchData$.documents$}
indexPatternList={indexPatternList}
onAddField={onAddColumn}
onAddFilter={onAddFilter}
onAddFilter={!isPlainRecord ? onAddFilter : undefined}
onRemoveField={onRemoveColumn}
onChangeIndexPattern={onChangeIndexPattern}
selectedIndexPattern={indexPattern}
@ -303,7 +317,7 @@ export function DiscoverLayout({
isTimeBased={isTimeBased}
data={data}
error={dataState.error}
hasQuery={!!state.query?.query}
hasQuery={isOfQueryType(state.query) && !!state.query?.query}
hasFilters={hasActiveFilter(state.filters)}
onDisableFilters={onDisableFilters}
/>
@ -320,33 +334,37 @@ export function DiscoverLayout({
gutterSize="none"
responsive={false}
>
<EuiFlexItem grow={false}>
<DiscoverChartMemoized
resetSavedSearch={resetSavedSearch}
savedSearch={savedSearch}
savedSearchDataChart$={charts$}
savedSearchDataTotalHits$={totalHits$}
stateContainer={stateContainer}
indexPattern={indexPattern}
viewMode={viewMode}
setDiscoverViewMode={setDiscoverViewMode}
hideChart={state.hideChart}
interval={state.interval}
/>
</EuiFlexItem>
<EuiHorizontalRule margin="none" />
{!isPlainRecord && (
<>
<EuiFlexItem grow={false}>
<DiscoverChartMemoized
resetSavedSearch={resetSavedSearch}
savedSearch={savedSearch}
savedSearchDataChart$={charts$}
savedSearchDataTotalHits$={totalHits$}
stateContainer={stateContainer}
indexPattern={indexPattern}
viewMode={viewMode}
setDiscoverViewMode={setDiscoverViewMode}
hideChart={state.hideChart}
interval={state.interval}
/>
</EuiFlexItem>
<EuiHorizontalRule margin="none" />
</>
)}
{viewMode === VIEW_MODE.DOCUMENT_LEVEL ? (
<DiscoverDocuments
documents$={savedSearchData$.documents$}
expandedDoc={expandedDoc}
indexPattern={indexPattern}
navigateTo={navigateTo}
onAddFilter={onAddFilter as DocViewFilterFn}
onAddFilter={!isPlainRecord ? (onAddFilter as DocViewFilterFn) : undefined}
savedSearch={savedSearch}
setExpandedDoc={setExpandedDoc}
state={state}
stateContainer={stateContainer}
onFieldEdited={onFieldEdited}
onFieldEdited={!isPlainRecord ? onFieldEdited : undefined}
/>
) : (
<FieldStatisticsTableMemoized
@ -357,7 +375,7 @@ export function DiscoverLayout({
filters={state.filters}
columns={columns}
stateContainer={stateContainer}
onAddFilter={onAddFilter}
onAddFilter={!isPlainRecord ? (onAddFilter as DocViewFilterFn) : undefined}
trackUiMetric={trackUiMetric}
savedSearchRefetch$={savedSearchRefetch$}
/>

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import type { Query, TimeRange } from '@kbn/es-query';
import type { Query, TimeRange, AggregateQuery } from '@kbn/es-query';
import type { SavedObject } from '@kbn/data-plugin/public';
import type { DataView, DataViewAttributes } from '@kbn/data-views-plugin/public';
import { ISearchSource } from '@kbn/data-plugin/public';
@ -22,7 +22,10 @@ export interface DiscoverLayoutProps {
inspectorAdapters: { requests: RequestAdapter };
navigateTo: (url: string) => void;
onChangeIndexPattern: (id: string) => void;
onUpdateQuery: (payload: { dateRange: TimeRange; query?: Query }, isUpdate?: boolean) => void;
onUpdateQuery: (
payload: { dateRange: TimeRange; query?: Query | AggregateQuery },
isUpdate?: boolean
) => void;
resetSavedSearch: () => void;
expandedDoc?: DataTableRecord;
setExpandedDoc: (doc?: DataTableRecord) => void;

View file

@ -27,10 +27,12 @@ function getComponent({
selected = false,
showDetails = false,
field,
onAddFilterExists = true,
}: {
selected?: boolean;
showDetails?: boolean;
field?: DataViewField;
onAddFilterExists?: boolean;
}) {
const finalField =
field ??
@ -49,7 +51,7 @@ function getComponent({
indexPattern: stubDataView,
field: finalField,
getDetails: jest.fn(() => ({ buckets: [], error: '', exists: 1, total: 2, columns: [] })),
onAddFilter: jest.fn(),
...(onAddFilterExists && { onAddFilter: jest.fn() }),
onAddField: jest.fn(),
onRemoveField: jest.fn(),
showDetails,
@ -139,4 +141,21 @@ describe('discover sidebar field', function () {
findTestSubject(comp, 'field-bytes-showDetails').simulate('click');
expect(props.getDetails.mock.calls.length).toEqual(1);
});
it('should not return the popover if onAddFilter is not provided', function () {
const field = new DataViewField({
name: '_source',
type: '_source',
esTypes: ['_source'],
searchable: true,
aggregatable: true,
readFromDocValues: true,
});
const { comp } = getComponent({
selected: true,
field,
onAddFilterExists: false,
});
const popover = findTestSubject(comp, 'discoverFieldListPanelPopover');
expect(popover.length).toBe(0);
});
});

View file

@ -167,10 +167,11 @@ interface MultiFieldsProps {
multiFields: NonNullable<DiscoverFieldProps['multiFields']>;
toggleDisplay: (field: DataViewField) => void;
alwaysShowActionButton: boolean;
isDocumentRecord: boolean;
}
const MultiFields: React.FC<MultiFieldsProps> = memo(
({ multiFields, toggleDisplay, alwaysShowActionButton }) => (
({ multiFields, toggleDisplay, alwaysShowActionButton, isDocumentRecord }) => (
<React.Fragment>
<EuiTitle size="xxxs">
<h5>
@ -186,7 +187,7 @@ const MultiFields: React.FC<MultiFieldsProps> = memo(
className="dscSidebarItem dscSidebarItem--multi"
isActive={false}
dataTestSubj={`field-${entry.field.name}-showDetails`}
fieldIcon={<DiscoverFieldTypeIcon field={entry.field} />}
fieldIcon={isDocumentRecord && <DiscoverFieldTypeIcon field={entry.field} />}
fieldAction={
<ActionButton
field={entry.field}
@ -223,7 +224,7 @@ export interface DiscoverFieldProps {
/**
* Callback to add a filter to filter bar
*/
onAddFilter: (field: DataViewField | string, value: string, type: '+' | '-') => void;
onAddFilter?: (field: DataViewField | string, value: string, type: '+' | '-') => void;
/**
* Callback to remove/deselect a the field
* @param fieldName
@ -280,6 +281,7 @@ function DiscoverFieldComponent({
showFieldStats,
}: DiscoverFieldProps) {
const [infoIsOpen, setOpen] = useState(false);
const isDocumentRecord = !!onAddFilter;
const toggleDisplay = useCallback(
(f: DataViewField) => {
@ -304,7 +306,7 @@ function DiscoverFieldComponent({
size="s"
className="dscSidebarItem"
dataTestSubj={`field-${field.name}-showDetails`}
fieldIcon={<DiscoverFieldTypeIcon field={field} />}
fieldIcon={isDocumentRecord && <DiscoverFieldTypeIcon field={field} />}
fieldAction={
<ActionButton
field={field}
@ -369,6 +371,30 @@ function DiscoverFieldComponent({
</EuiPopoverTitle>
);
const button = (
<FieldButton
size="s"
className="dscSidebarItem"
isActive={infoIsOpen}
onClick={togglePopover}
dataTestSubj={`field-${field.name}-showDetails`}
fieldIcon={isDocumentRecord && <DiscoverFieldTypeIcon field={field} />}
fieldAction={
<ActionButton
field={field}
isSelected={selected}
alwaysShow={alwaysShowActionButton}
toggleDisplay={toggleDisplay}
/>
}
fieldName={<FieldName field={field} />}
fieldInfoIcon={field.type === 'conflict' && <FieldInfoIcon />}
/>
);
if (!isDocumentRecord) {
return button;
}
const renderPopover = () => {
const details = getDetails(field);
return (
@ -398,6 +424,7 @@ function DiscoverFieldComponent({
multiFields={multiFields}
alwaysShowActionButton={alwaysShowActionButton}
toggleDisplay={toggleDisplay}
isDocumentRecord={isDocumentRecord}
/>
</>
)}
@ -415,28 +442,10 @@ function DiscoverFieldComponent({
return (
<EuiPopover
display="block"
button={
<FieldButton
size="s"
className="dscSidebarItem"
isActive={infoIsOpen}
onClick={togglePopover}
dataTestSubj={`field-${field.name}-showDetails`}
fieldIcon={<DiscoverFieldTypeIcon field={field} />}
fieldAction={
<ActionButton
field={field}
isSelected={selected}
alwaysShow={alwaysShowActionButton}
toggleDisplay={toggleDisplay}
/>
}
fieldName={<FieldName field={field} />}
fieldInfoIcon={field.type === 'conflict' && <FieldInfoIcon />}
/>
}
button={button}
isOpen={infoIsOpen}
closePopover={() => setOpen(false)}
data-test-subj="discoverFieldListPanelPopover"
anchorPosition="rightUp"
panelClassName="dscSidebarItem__fieldPopoverPanel"
>

View file

@ -17,7 +17,7 @@ import './discover_field_bucket.scss';
interface Props {
bucket: Bucket;
field: DataViewField;
onAddFilter: (field: DataViewField | string, value: string, type: '+' | '-') => void;
onAddFilter?: (field: DataViewField | string, value: string, type: '+' | '-') => void;
}
export function DiscoverFieldBucket({ field, bucket, onAddFilter }: Props) {
@ -66,7 +66,7 @@ export function DiscoverFieldBucket({ field, bucket, onAddFilter }: Props) {
count={bucket.count}
/>
</EuiFlexItem>
{field.filterable && (
{onAddFilter && field.filterable && (
<EuiFlexItem grow={false}>
<div>
<EuiButtonIcon

View file

@ -17,7 +17,7 @@ interface DiscoverFieldDetailsProps {
field: DataViewField;
indexPattern: DataView;
details: FieldDetails;
onAddFilter: (field: DataViewField | string, value: string, type: '+' | '-') => void;
onAddFilter?: (field: DataViewField | string, value: string, type: '+' | '-') => void;
}
export function DiscoverFieldDetails({
@ -43,7 +43,7 @@ export function DiscoverFieldDetails({
</div>
<EuiSpacer size="xs" />
<EuiText size="xs">
{!indexPattern.metaFields.includes(field.name) && !field.scripted ? (
{onAddFilter && !indexPattern.metaFields.includes(field.name) && !field.scripted ? (
<EuiLink
onClick={() => onAddFilter('_exists_', field.name, '+')}
data-test-subj="onAddFilterButton"

View file

@ -21,6 +21,7 @@ describe('DiscoverFieldSearch', () => {
value: 'test',
types: ['any', 'string', '_source'],
presentFieldTypes: ['string', 'date', 'boolean', 'number'],
isPlainRecord: false,
};
function mountComponent(props?: Props) {

View file

@ -64,6 +64,10 @@ export interface Props {
* the input value of the user
*/
value?: string;
/**
* is text base lang mode
*/
isPlainRecord: boolean;
}
interface FieldTypeTableItem {
@ -76,7 +80,13 @@ interface FieldTypeTableItem {
* Component is Discover's side bar to search of available fields
* Additionally there's a button displayed that allows the user to show/hide more filter fields
*/
export function DiscoverFieldSearch({ onChange, value, types, presentFieldTypes }: Props) {
export function DiscoverFieldSearch({
onChange,
value,
types,
presentFieldTypes,
isPlainRecord,
}: Props) {
const searchPlaceholder = i18n.translate('discover.fieldChooser.searchPlaceHolder', {
defaultMessage: 'Search field names',
});
@ -353,81 +363,83 @@ export function DiscoverFieldSearch({ onChange, value, types, presentFieldTypes
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="xs" />
<EuiFlexItem>
<EuiFilterGroup fullWidth>
<EuiPopover
id="dataPanelTypeFilter"
panelClassName="euiFilterGroup__popoverPanel"
panelPaddingSize="none"
anchorPosition="rightUp"
display="block"
isOpen={isPopoverOpen}
closePopover={() => {
setPopoverOpen(false);
}}
button={buttonContent}
>
<EuiPopoverTitle paddingSize="s">
{i18n.translate('discover.fieldChooser.filter.filterByTypeLabel', {
defaultMessage: 'Filter by type',
})}
</EuiPopoverTitle>
{selectionPanel}
{footer()}
</EuiPopover>
<EuiPopover
anchorPosition="rightUp"
display="block"
button={helpButton}
isOpen={isHelpOpen}
panelPaddingSize="none"
className="dscFieldTypesHelp__popover"
panelClassName="dscFieldTypesHelp__panel"
closePopover={closeHelp}
initialFocus="#dscFieldTypesHelpBasicTableId"
>
<EuiPopoverTitle paddingSize="s">
{i18n.translate('discover.fieldChooser.popoverTitle', {
defaultMessage: 'Field types',
})}
</EuiPopoverTitle>
<EuiPanel
className="eui-yScroll"
style={{ maxHeight: '50vh' }}
color="transparent"
paddingSize="s"
{!isPlainRecord && (
<EuiFlexItem>
<EuiFilterGroup fullWidth>
<EuiPopover
id="dataPanelTypeFilter"
panelClassName="euiFilterGroup__popoverPanel"
panelPaddingSize="none"
anchorPosition="rightUp"
display="block"
isOpen={isPopoverOpen}
closePopover={() => {
setPopoverOpen(false);
}}
button={buttonContent}
>
<EuiBasicTable
id="dscFieldTypesHelpBasicTableId"
tableCaption={i18n.translate('discover.fieldTypesPopover.tableTitle', {
defaultMessage: 'Description of field types',
<EuiPopoverTitle paddingSize="s">
{i18n.translate('discover.fieldChooser.filter.filterByTypeLabel', {
defaultMessage: 'Filter by type',
})}
items={items}
compressed={true}
rowHeader="firstName"
columns={columnsSidebar}
responsive={false}
/>
</EuiPanel>
<EuiPanel color="transparent" paddingSize="s">
<EuiText color="subdued" size="xs">
<p>
{i18n.translate('discover.fieldTypesPopover.learnMoreText', {
defaultMessage: 'Learn more about',
</EuiPopoverTitle>
{selectionPanel}
{footer()}
</EuiPopover>
<EuiPopover
anchorPosition="rightUp"
display="block"
button={helpButton}
isOpen={isHelpOpen}
panelPaddingSize="none"
className="dscFieldTypesHelp__popover"
panelClassName="dscFieldTypesHelp__panel"
closePopover={closeHelp}
initialFocus="#dscFieldTypesHelpBasicTableId"
>
<EuiPopoverTitle paddingSize="s">
{i18n.translate('discover.fieldChooser.popoverTitle', {
defaultMessage: 'Field types',
})}
</EuiPopoverTitle>
<EuiPanel
className="eui-yScroll"
style={{ maxHeight: '50vh' }}
color="transparent"
paddingSize="s"
>
<EuiBasicTable
id="dscFieldTypesHelpBasicTableId"
tableCaption={i18n.translate('discover.fieldTypesPopover.tableTitle', {
defaultMessage: 'Description of field types',
})}
&nbsp;
<EuiLink href={docLinks.links.discover.fieldTypeHelp} target="_blank" external>
<FormattedMessage
id="discover.fieldTypesPopover.fieldTypesDocLinkLabel"
defaultMessage="field types"
/>
</EuiLink>
</p>
</EuiText>
</EuiPanel>
</EuiPopover>
</EuiFilterGroup>
</EuiFlexItem>
items={items}
compressed={true}
rowHeader="firstName"
columns={columnsSidebar}
responsive={false}
/>
</EuiPanel>
<EuiPanel color="transparent" paddingSize="s">
<EuiText color="subdued" size="xs">
<p>
{i18n.translate('discover.fieldTypesPopover.learnMoreText', {
defaultMessage: 'Learn more about',
})}
&nbsp;
<EuiLink href={docLinks.links.discover.fieldTypeHelp} target="_blank" external>
<FormattedMessage
id="discover.fieldTypesPopover.fieldTypesDocLinkLabel"
defaultMessage="field types"
/>
</EuiLink>
</p>
</EuiText>
</EuiPanel>
</EuiPopover>
</EuiFilterGroup>
</EuiFlexItem>
)}
</React.Fragment>
);
}

View file

@ -125,6 +125,7 @@ export function DiscoverSidebarComponent({
const [fieldsToRender, setFieldsToRender] = useState(FIELDS_PER_PAGE);
const [fieldsPerPage, setFieldsPerPage] = useState(FIELDS_PER_PAGE);
const availableFieldsContainer = useRef<HTMLUListElement | null>(null);
const isPlainRecord = !onAddFilter;
useEffect(() => {
if (documents) {
@ -160,16 +161,24 @@ export function DiscoverSidebarComponent({
[fields, columns, popularLimit, fieldCounts, fieldFilter, useNewFieldsApi]
);
/**
* Popular fields are not displayed in text based lang mode
*/
const restFields = useMemo(
() => (isPlainRecord ? [...popularFields, ...unpopularFields] : unpopularFields),
[isPlainRecord, popularFields, unpopularFields]
);
const paginate = useCallback(() => {
const newFieldsToRender = fieldsToRender + Math.round(fieldsPerPage * 0.5);
setFieldsToRender(Math.max(fieldsPerPage, Math.min(newFieldsToRender, unpopularFields.length)));
}, [setFieldsToRender, fieldsToRender, unpopularFields, fieldsPerPage]);
setFieldsToRender(Math.max(fieldsPerPage, Math.min(newFieldsToRender, restFields.length)));
}, [setFieldsToRender, fieldsToRender, restFields, fieldsPerPage]);
useEffect(() => {
if (scrollContainer && unpopularFields.length && availableFieldsContainer.current) {
if (scrollContainer && restFields.length && availableFieldsContainer.current) {
const { clientHeight, scrollHeight } = scrollContainer;
const isScrollable = scrollHeight > clientHeight; // there is no scrolling currently
const allFieldsRendered = fieldsToRender >= unpopularFields.length;
const allFieldsRendered = fieldsToRender >= restFields.length;
if (!isScrollable && !allFieldsRendered) {
// Not all available fields were rendered with the given fieldsPerPage number
@ -187,7 +196,7 @@ export function DiscoverSidebarComponent({
}, [
fieldsPerPage,
scrollContainer,
unpopularFields,
restFields,
fieldsToRender,
setFieldsPerPage,
setFieldsToRender,
@ -198,11 +207,11 @@ export function DiscoverSidebarComponent({
if (scrollContainer) {
const { scrollTop, clientHeight, scrollHeight } = scrollContainer;
const nearBottom = scrollTop + clientHeight > scrollHeight * 0.9;
if (nearBottom && unpopularFields) {
if (nearBottom && restFields) {
paginate();
}
}
}, [paginate, scrollContainer, unpopularFields]);
}, [paginate, scrollContainer, restFields]);
const { fieldTypes, presentFieldTypes } = useMemo(() => {
const result = ['any'];
@ -342,6 +351,7 @@ export function DiscoverSidebarComponent({
value={fieldFilter.name}
types={fieldTypes}
presentFieldTypes={presentFieldTypes}
isPlainRecord={isPlainRecord}
/>
</form>
</EuiFlexItem>
@ -428,12 +438,12 @@ export function DiscoverSidebarComponent({
}
extraAction={
<EuiNotificationBadge size="m" color={filterChanged ? 'subdued' : 'accent'}>
{popularFields.length + unpopularFields.length}
{restFields.length}
</EuiNotificationBadge>
}
>
<EuiSpacer size="s" />
{popularFields.length > 0 && (
{!isPlainRecord && popularFields.length > 0 && (
<>
<EuiTitle size="xxxs" className="dscFieldListHeader">
<h4 id="available_fields_popular">
@ -477,7 +487,7 @@ export function DiscoverSidebarComponent({
data-test-subj={`fieldList-unpopular`}
ref={availableFieldsContainer}
>
{getPaginated(unpopularFields).map((field: DataViewField) => {
{getPaginated(restFields).map((field: DataViewField) => {
return (
<li key={`field${field.name}`} data-attr-field={field.name}>
<DiscoverField

View file

@ -21,7 +21,7 @@ import {
} from './discover_sidebar_responsive';
import { DiscoverServices } from '../../../../build_services';
import { FetchStatus } from '../../../types';
import { AvailableFields$, DataDocuments$ } from '../../hooks/use_saved_search';
import { AvailableFields$, DataDocuments$, RecordRawType } from '../../hooks/use_saved_search';
import { stubLogstashIndexPattern } from '@kbn/data-plugin/common/stubs';
import { VIEW_MODE } from '../../../../components/view_mode_toggle';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
@ -166,6 +166,29 @@ describe('discover responsive sidebar', function () {
expect(findTestSubject(comp, 'indexPattern-add-field_btn').length).toBe(1);
});
it('should not show "Add a field" button on the sql mode', () => {
const initialProps = getCompProps();
const propsWithTextBasedMode = {
...initialProps,
onAddFilter: undefined,
documents$: new BehaviorSubject({
fetchStatus: FetchStatus.COMPLETE,
recordRawType: RecordRawType.PLAIN,
result: getDataTableRecords(stubLogstashIndexPattern),
}) as DataDocuments$,
state: {
...initialProps.state,
query: { sql: 'SELECT * FROM `index`' },
},
};
const compInViewerMode = mountWithIntl(
<KibanaContextProvider services={mockServices}>
<DiscoverSidebarResponsive {...propsWithTextBasedMode} />
</KibanaContextProvider>
);
expect(findTestSubject(compInViewerMode, 'indexPattern-add-field_btn').length).toBe(0);
});
it('should not show "Add a field" button in viewer mode', () => {
const mockedServicesInViewerMode = {
...mockServices,

View file

@ -6,33 +6,34 @@
* Side Public License, v 1.
*/
import React, { useEffect, useRef, useState, useCallback, useMemo } from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { UiCounterMetricType } from '@kbn/analytics';
import {
EuiTitle,
EuiHideFor,
EuiShowFor,
EuiButton,
EuiBadge,
EuiFlyoutHeader,
EuiButton,
EuiFlyout,
EuiFlyoutHeader,
EuiHideFor,
EuiIcon,
EuiLink,
EuiPortal,
EuiShowFor,
EuiTitle,
} from '@elastic/eui';
import type { DataViewField, DataView, DataViewAttributes } from '@kbn/data-views-plugin/public';
import type { DataView, DataViewAttributes, DataViewField } from '@kbn/data-views-plugin/public';
import { SavedObject } from '@kbn/core/types';
import { useDiscoverServices } from '../../../../hooks/use_discover_services';
import { getDefaultFieldFilter } from './lib/field_filter';
import { DiscoverSidebar } from './discover_sidebar';
import { AppState } from '../../services/discover_state';
import { AvailableFields$, DataDocuments$ } from '../../hooks/use_saved_search';
import { AvailableFields$, DataDocuments$, RecordRawType } from '../../hooks/use_saved_search';
import { calcFieldCounts } from '../../utils/calc_field_counts';
import { VIEW_MODE } from '../../../../components/view_mode_toggle';
import { FetchStatus } from '../../../types';
import { DISCOVER_TOUR_STEP_ANCHOR_IDS } from '../../../../components/discover_tour';
import { getRawRecordType } from '../../utils/get_raw_record_type';
export interface DiscoverSidebarResponsiveProps {
/**
@ -62,7 +63,7 @@ export interface DiscoverSidebarResponsiveProps {
/**
* Callback function when adding a filter from sidebar
*/
onAddFilter: (field: DataViewField | string, value: string, type: '+' | '-') => void;
onAddFilter?: (field: DataViewField | string, value: string, type: '+' | '-') => void;
/**
* Callback function when changing an index pattern
*/
@ -115,6 +116,10 @@ export interface DiscoverSidebarResponsiveProps {
*/
export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) {
const services = useDiscoverServices();
const isPlainRecord = useMemo(
() => getRawRecordType(props.state.query) === RecordRawType.PLAIN,
[props.state.query]
);
const { selectedIndexPattern, onFieldEdited, onDataViewCreated } = props;
const [fieldFilter, setFieldFilter] = useState(getDefaultFieldFilter());
const [isFlyoutVisible, setIsFlyoutVisible] = useState(false);
@ -210,7 +215,7 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps)
const editField = useMemo(
() =>
canEditDataView && selectedIndexPattern
!isPlainRecord && canEditDataView && selectedIndexPattern
? (fieldName?: string) => {
const ref = dataViewFieldEditor.openEditor({
ctx: {
@ -230,11 +235,12 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps)
}
: undefined,
[
isPlainRecord,
canEditDataView,
closeFlyout,
dataViewFieldEditor,
selectedIndexPattern,
dataViewFieldEditor,
setFieldEditorRef,
closeFlyout,
onFieldEdited,
]
);

View file

@ -43,6 +43,7 @@ function getProps(savePermissions = true): DiscoverTopNavProps {
resetSavedSearch: () => {},
onFieldEdited: jest.fn(),
onChangeIndexPattern: jest.fn(),
isPlainRecord: false,
};
}

View file

@ -5,27 +5,35 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { useCallback, useMemo, useRef, useEffect } from 'react';
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import { useHistory } from 'react-router-dom';
import type { Query, TimeRange } from '@kbn/es-query';
import type { Query, TimeRange, AggregateQuery } from '@kbn/es-query';
import { DataViewType } from '@kbn/data-views-plugin/public';
import type { DataViewPickerProps } from '@kbn/unified-search-plugin/public';
import { ENABLE_SQL } from '../../../../../common';
import { useDiscoverServices } from '../../../../hooks/use_discover_services';
import { DiscoverLayoutProps } from '../layout/types';
import { getTopNavLinks } from './get_top_nav_links';
import { getHeaderActionMenuMounter } from '../../../../kibana_services';
import { GetStateReturn } from '../../services/discover_state';
import { onSaveSearch } from './on_save_search';
export type DiscoverTopNavProps = Pick<
DiscoverLayoutProps,
'indexPattern' | 'navigateTo' | 'savedSearch' | 'searchSource'
> & {
onOpenInspector: () => void;
query?: Query;
query?: Query | AggregateQuery;
savedQuery?: string;
updateQuery: (payload: { dateRange: TimeRange; query?: Query }, isUpdate?: boolean) => void;
updateQuery: (
payload: { dateRange: TimeRange; query?: Query | AggregateQuery },
isUpdate?: boolean
) => void;
stateContainer: GetStateReturn;
resetSavedSearch: () => void;
onChangeIndexPattern: (indexPattern: string) => void;
isPlainRecord: boolean;
textBasedLanguageModeErrors?: Error;
onFieldEdited: () => void;
};
@ -41,22 +49,25 @@ export const DiscoverTopNav = ({
savedSearch,
resetSavedSearch,
onChangeIndexPattern,
isPlainRecord,
textBasedLanguageModeErrors,
onFieldEdited,
}: DiscoverTopNavProps) => {
const history = useHistory();
const showDatePicker = useMemo(
() => indexPattern.isTimeBased() && indexPattern.type !== DataViewType.ROLLUP,
[indexPattern]
);
const services = useDiscoverServices();
const { dataViewEditor, navigation, dataViewFieldEditor, data } = services;
const { dataViewEditor, navigation, dataViewFieldEditor, data, uiSettings } = services;
const canEditDataView = Boolean(dataViewEditor?.userPermissions.editDataView());
const closeFieldEditor = useRef<() => void | undefined>();
const closeDataViewEditor = useRef<() => void | undefined>();
const { TopNavMenu } = navigation.ui;
const { AggregateQueryTopNavMenu } = navigation.ui;
const onOpenSavedSearch = useCallback(
(newSavedSearchId: string) => {
@ -134,6 +145,7 @@ export const DiscoverTopNav = ({
onOpenInspector,
searchSource,
onOpenSavedSearch,
isPlainRecord,
}),
[
indexPattern,
@ -144,6 +156,7 @@ export const DiscoverTopNav = ({
onOpenInspector,
searchSource,
onOpenSavedSearch,
isPlainRecord,
]
);
@ -163,7 +176,11 @@ export const DiscoverTopNav = ({
const setMenuMountPoint = useMemo(() => {
return getHeaderActionMenuMounter();
}, []);
const isSQLModeEnabled = uiSettings.get(ENABLE_SQL);
const supportedTextBasedLanguages = [];
if (isSQLModeEnabled) {
supportedTextBasedLanguages.push('SQL');
}
const dataViewPickerProps = {
trigger: {
label: indexPattern?.getName() || '',
@ -173,11 +190,27 @@ export const DiscoverTopNav = ({
currentDataViewId: indexPattern?.id,
onAddField: addField,
onDataViewCreated: createNewDataView,
onChangeDataView: (newIndexPatternId: string) => onChangeIndexPattern(newIndexPatternId),
onChangeDataView: onChangeIndexPattern,
textBasedLanguages: supportedTextBasedLanguages as DataViewPickerProps['textBasedLanguages'],
};
const onTextBasedSavedAndExit = useCallback(
async ({ onSave, onCancel }) => {
await onSaveSearch({
savedSearch,
services,
indexPattern,
navigateTo,
state: stateContainer,
onClose: onCancel,
onSaveCb: onSave,
});
},
[indexPattern, navigateTo, savedSearch, services, stateContainer]
);
return (
<TopNavMenu
<AggregateQueryTopNavMenu
appName="discover"
config={topNavMenu}
indexPatterns={[indexPattern]}
@ -188,11 +221,15 @@ export const DiscoverTopNav = ({
savedQueryId={savedQuery}
screenTitle={savedSearch.title}
showDatePicker={showDatePicker}
showSaveQuery={!!services.capabilities.discover.saveQuery}
showSaveQuery={!isPlainRecord && Boolean(services.capabilities.discover.saveQuery)}
showSearchBar={true}
useDefaultBehaviors={true}
dataViewPickerComponentProps={dataViewPickerProps}
displayStyle="detached"
textBasedLanguageModeErrors={
textBasedLanguageModeErrors ? [textBasedLanguageModeErrors] : undefined
}
onTextBasedSavedAndExit={onTextBasedSavedAndExit}
/>
);
};

View file

@ -36,6 +36,7 @@ test('getTopNavLinks result', () => {
state,
searchSource: {} as ISearchSource,
onOpenSavedSearch: () => {},
isPlainRecord: false,
});
expect(topNavLinks).toMatchInlineSnapshot(`
Array [
@ -86,3 +87,58 @@ test('getTopNavLinks result', () => {
]
`);
});
test('getTopNavLinks result for sql mode', () => {
const topNavLinks = getTopNavLinks({
indexPattern: indexPatternMock,
navigateTo: jest.fn(),
onOpenInspector: jest.fn(),
savedSearch: savedSearchMock,
services,
state,
searchSource: {} as ISearchSource,
onOpenSavedSearch: () => {},
isPlainRecord: true,
});
expect(topNavLinks).toMatchInlineSnapshot(`
Array [
Object {
"description": "Options",
"id": "options",
"label": "Options",
"run": [Function],
"testId": "discoverOptionsButton",
},
Object {
"description": "New Search",
"id": "new",
"label": "New",
"run": [Function],
"testId": "discoverNewButton",
},
Object {
"description": "Open Saved Search",
"id": "open",
"label": "Open",
"run": [Function],
"testId": "discoverOpenButton",
},
Object {
"description": "Open Inspector for search",
"id": "inspect",
"label": "Inspect",
"run": [Function],
"testId": "openInspectorButton",
},
Object {
"description": "Save Search",
"emphasize": true,
"iconType": "save",
"id": "save",
"label": "Save",
"run": [Function],
"testId": "discoverSaveButton",
},
]
`);
});

View file

@ -32,6 +32,7 @@ export const getTopNavLinks = ({
onOpenInspector,
searchSource,
onOpenSavedSearch,
isPlainRecord,
}: {
indexPattern: DataView;
navigateTo: (url: string) => void;
@ -41,6 +42,7 @@ export const getTopNavLinks = ({
onOpenInspector: () => void;
searchSource: ISearchSource;
onOpenSavedSearch: (id: string) => void;
isPlainRecord: boolean;
}): TopNavMenuData[] => {
const options = {
id: 'options',
@ -196,11 +198,12 @@ export const getTopNavLinks = ({
...(services.capabilities.advancedSettings.save ? [options] : []),
newSearch,
openSearch,
...(!isPlainRecord ? [shareSearch] : []),
...(services.triggersActionsUi &&
services.capabilities.management?.insightsAndAlerting?.triggersActions
services.capabilities.management?.insightsAndAlerting?.triggersActions &&
!isPlainRecord
? [alerts]
: []),
shareSearch,
inspectSearch,
...(services.capabilities.discover.save ? [saveSearch] : []),
];

View file

@ -24,6 +24,7 @@ async function saveDataSource({
saveOptions,
services,
state,
navigateOrReloadSavedSearch,
}: {
indexPattern: DataView;
navigateTo: (url: string) => void;
@ -31,6 +32,7 @@ async function saveDataSource({
saveOptions: SaveSavedSearchOptions;
services: DiscoverServices;
state: GetStateReturn;
navigateOrReloadSavedSearch: boolean;
}) {
const prevSavedSearchId = savedSearch.id;
function onSuccess(id: string) {
@ -44,20 +46,22 @@ async function saveDataSource({
}),
'data-test-subj': 'saveSearchSuccess',
});
if (id !== prevSavedSearchId) {
navigateTo(`/view/${encodeURIComponent(id)}`);
} else {
// Update defaults so that "reload saved query" functions correctly
state.resetAppState();
services.chrome.docTitle.change(savedSearch.title!);
if (navigateOrReloadSavedSearch) {
if (id !== prevSavedSearchId) {
navigateTo(`/view/${encodeURIComponent(id)}`);
} else {
// Update defaults so that "reload saved query" functions correctly
state.resetAppState();
services.chrome.docTitle.change(savedSearch.title!);
setBreadcrumbsTitle(
{
...savedSearch,
id: prevSavedSearchId ?? id,
},
services.chrome
);
setBreadcrumbsTitle(
{
...savedSearch,
id: prevSavedSearchId ?? id,
},
services.chrome
);
}
}
}
}
@ -90,6 +94,7 @@ export async function onSaveSearch({
services,
state,
onClose,
onSaveCb,
}: {
indexPattern: DataView;
navigateTo: (path: string) => void;
@ -97,6 +102,7 @@ export async function onSaveSearch({
services: DiscoverServices;
state: GetStateReturn;
onClose?: () => void;
onSaveCb?: () => void;
}) {
const { uiSettings } = services;
const onSave = async ({
@ -124,6 +130,7 @@ export async function onSaveSearch({
copyOnSave: newCopyOnSave,
isTitleDuplicateConfirmed,
};
const navigateOrReloadSavedSearch = !Boolean(onSaveCb);
const response = await saveDataSource({
indexPattern,
saveOptions,
@ -131,6 +138,7 @@ export async function onSaveSearch({
navigateTo,
savedSearch,
state,
navigateOrReloadSavedSearch,
});
// If the save wasn't successful, put the original values back.
if (!response.id || response.error) {
@ -139,6 +147,7 @@ export async function onSaveSearch({
} else {
state.resetInitialAppState();
}
onSaveCb?.();
return response;
};

View file

@ -6,14 +6,21 @@
* Side Public License, v 1.
*/
import { useMemo, useEffect, useState, useCallback } from 'react';
import usePrevious from 'react-use/lib/usePrevious';
import { isEqual } from 'lodash';
import { History } from 'history';
import {
isOfAggregateQueryType,
getIndexPatternFromSQLQuery,
AggregateQuery,
Query,
} from '@kbn/es-query';
import { getState } from '../services/discover_state';
import { getStateDefaults } from '../utils/get_state_defaults';
import { DiscoverServices } from '../../../build_services';
import { SavedSearch, getSavedSearch } from '../../../services/saved_searches';
import { loadIndexPattern } from '../utils/resolve_index_pattern';
import { useSavedSearch as useSavedSearchData } from './use_saved_search';
import { useSavedSearch as useSavedSearchData, DataDocumentsMsg } from './use_saved_search';
import {
MODIFY_COLUMNS_ON_SWITCH,
SEARCH_FIELDS_FROM_SOURCE,
@ -21,11 +28,14 @@ import {
SORT_DEFAULT_ORDER_SETTING,
} from '../../../../common';
import { useSearchSession } from './use_search_session';
import { useDataState } from './use_data_state';
import { FetchStatus } from '../../types';
import { getSwitchIndexPatternAppState } from '../utils/get_switch_index_pattern_app_state';
import { SortPairArr } from '../../../components/doc_table/utils/get_sort';
import { DataTableRecord } from '../../../types';
const MAX_NUM_OF_COLUMNS = 50;
export function useDiscoverState({
services,
history,
@ -69,6 +79,9 @@ export function useDiscoverState({
const { appStateContainer } = stateContainer;
const [state, setState] = useState(appStateContainer.getState());
const [documentStateCols, setDocumentStateCols] = useState<string[]>([]);
const [sqlQuery] = useState<AggregateQuery | Query | undefined>(state.query);
const prevQuery = usePrevious(state.query);
/**
* Search session logic
@ -99,6 +112,8 @@ export function useDiscoverState({
useNewFieldsApi,
});
const documentState: DataDocumentsMsg = useDataState(data$.documents$);
/**
* Reset to display loading spinner when savedSearch is changing
*/
@ -196,13 +211,23 @@ export function useDiscoverState({
state.columns || [],
(state.sort || []) as SortPairArr[],
config.get(MODIFY_COLUMNS_ON_SWITCH),
config.get(SORT_DEFAULT_ORDER_SETTING)
config.get(SORT_DEFAULT_ORDER_SETTING),
state.query
);
stateContainer.setAppState(nextAppState);
}
setExpandedDoc(undefined);
},
[config, indexPattern, indexPatterns, setExpandedDoc, state.columns, state.sort, stateContainer]
[
config,
indexPattern,
indexPatterns,
setExpandedDoc,
state.columns,
state.query,
state.sort,
stateContainer,
]
);
/**
* Function triggered when the user changes the query in the search bar
@ -220,12 +245,53 @@ export function useDiscoverState({
/**
* Trigger data fetching on indexPattern or savedSearch changes
*/
useEffect(() => {
if (!isEqual(state.query, prevQuery)) {
setDocumentStateCols([]);
}
}, [state.query, prevQuery]);
useEffect(() => {
if (indexPattern) {
refetch$.next(undefined);
}
}, [initialFetchStatus, refetch$, indexPattern, savedSearch.id]);
const getResultColumns = useCallback(() => {
if (documentState.result?.length && documentState.fetchStatus === FetchStatus.COMPLETE) {
const firstRow = documentState.result[0];
const columns = Object.keys(firstRow.raw).slice(0, MAX_NUM_OF_COLUMNS);
if (!isEqual(columns, documentStateCols) && !isEqual(state.query, sqlQuery)) {
return columns;
}
return [];
}
return [];
}, [documentState, documentStateCols, sqlQuery, state.query]);
useEffect(() => {
async function fetchDataview() {
if (state.query && isOfAggregateQueryType(state.query) && 'sql' in state.query) {
const indexPatternFromQuery = getIndexPatternFromSQLQuery(state.query.sql);
const idsTitles = await indexPatterns.getIdsWithTitle();
const dataViewObj = idsTitles.find(({ title }) => title === indexPatternFromQuery);
if (dataViewObj) {
const columns = getResultColumns();
if (columns.length) {
setDocumentStateCols(columns);
}
const nextState = {
index: dataViewObj.id,
...(columns.length && { columns }),
};
stateContainer.replaceUrlAppState(nextState);
}
}
}
fetchDataview();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [config, documentState, indexPatterns]);
return {
data$,
indexPattern,

View file

@ -10,8 +10,8 @@ import { renderHook } from '@testing-library/react-hooks';
import { createSearchSessionMock } from '../../../__mocks__/search_session';
import { discoverServiceMock } from '../../../__mocks__/services';
import { savedSearchMock } from '../../../__mocks__/saved_search';
import { useSavedSearch } from './use_saved_search';
import { getState } from '../services/discover_state';
import { RecordRawType, useSavedSearch } from './use_saved_search';
import { getState, AppState } from '../services/discover_state';
import { uiSettingsMock } from '../../../__mocks__/ui_settings';
import { useDiscoverState } from './use_discover_state';
import { FetchStatus } from '../../types';
@ -128,4 +128,31 @@ describe('test useSavedSearch', () => {
result.current.reset();
expect(result.current.data$.main$.value.fetchStatus).toBe(FetchStatus.LOADING);
});
test('useSavedSearch returns plain record raw type', async () => {
const { history, searchSessionManager } = createSearchSessionMock();
const stateContainer = getState({
getStateDefaults: () =>
({
index: 'the-index-pattern-id',
query: { sql: 'SELECT * FROM test' },
} as unknown as AppState),
history,
uiSettings: uiSettingsMock,
});
const { result } = renderHook(() => {
return useSavedSearch({
initialFetchStatus: FetchStatus.LOADING,
savedSearch: savedSearchMock,
searchSessionManager,
searchSource: savedSearchMock.searchSource.createCopy(),
services: discoverServiceMock,
stateContainer,
useNewFieldsApi: true,
});
});
expect(result.current.data$.main$.getValue().recordRawType).toBe(RecordRawType.PLAIN);
});
});

View file

@ -10,6 +10,7 @@ import { BehaviorSubject, Subject } from 'rxjs';
import type { AutoRefreshDoneFn } from '@kbn/data-plugin/public';
import { ISearchSource } from '@kbn/data-plugin/public';
import { RequestAdapter } from '@kbn/inspector-plugin/public';
import { getRawRecordType } from '../utils/get_raw_record_type';
import { DiscoverServices } from '../../../build_services';
import { DiscoverSearchSessionManager } from '../services/discover_search_session';
import { GetStateReturn } from '../services/discover_state';
@ -53,11 +54,23 @@ export interface UseSavedSearch {
inspectorAdapters: { requests: RequestAdapter };
}
export enum RecordRawType {
/**
* Documents returned Elasticsearch, nested structure
*/
DOCUMENT = 'document',
/**
* Data returned e.g. SQL queries, flat structure
* */
PLAIN = 'plain',
}
export type DataRefetchMsg = 'reset' | undefined;
export interface DataMsg {
fetchStatus: FetchStatus;
error?: Error;
recordRawType?: RecordRawType;
}
export interface DataMainMsg extends DataMsg {
@ -106,6 +119,9 @@ export const useSavedSearch = ({
}) => {
const { data, filterManager } = services;
const timefilter = data.query.timefilter.timefilter;
const { query } = stateContainer.appStateContainer.getState();
const recordRawType = useMemo(() => getRawRecordType(query), [query]);
const inspectorAdapters = useMemo(() => ({ requests: new RequestAdapter() }), []);
@ -113,17 +129,12 @@ export const useSavedSearch = ({
* The observables the UI (aka React component) subscribes to get notified about
* the changes in the data fetching process (high level: fetching started, data was received)
*/
const main$: DataMain$ = useBehaviorSubject({ fetchStatus: initialFetchStatus });
const documents$: DataDocuments$ = useBehaviorSubject({ fetchStatus: initialFetchStatus });
const totalHits$: DataTotalHits$ = useBehaviorSubject({ fetchStatus: initialFetchStatus });
const charts$: DataCharts$ = useBehaviorSubject({ fetchStatus: initialFetchStatus });
const availableFields$: AvailableFields$ = useBehaviorSubject({
fetchStatus: initialFetchStatus,
});
const initialState = { fetchStatus: initialFetchStatus, recordRawType };
const main$: DataMain$ = useBehaviorSubject(initialState) as DataMain$;
const documents$: DataDocuments$ = useBehaviorSubject(initialState) as DataDocuments$;
const totalHits$: DataTotalHits$ = useBehaviorSubject(initialState) as DataTotalHits$;
const charts$: DataCharts$ = useBehaviorSubject(initialState) as DataCharts$;
const availableFields$: AvailableFields$ = useBehaviorSubject(initialState) as AvailableFields$;
const dataSubjects = useMemo(() => {
return {

View file

@ -14,7 +14,7 @@ import {
} from './use_saved_search_messages';
import { FetchStatus } from '../../types';
import { BehaviorSubject } from 'rxjs';
import { DataMainMsg } from './use_saved_search';
import { DataMainMsg, RecordRawType } from './use_saved_search';
import { filter } from 'rxjs/operators';
describe('test useSavedSearch message generators', () => {
@ -52,14 +52,17 @@ describe('test useSavedSearch message generators', () => {
sendPartialMsg(main$);
});
test('sendLoadingMsg', (done) => {
const main$ = new BehaviorSubject<DataMainMsg>({ fetchStatus: FetchStatus.COMPLETE });
const main$ = new BehaviorSubject<DataMainMsg>({
fetchStatus: FetchStatus.COMPLETE,
});
main$.subscribe((value) => {
if (value.fetchStatus !== FetchStatus.COMPLETE) {
expect(value.fetchStatus).toBe(FetchStatus.LOADING);
expect(value.recordRawType).toBe(RecordRawType.DOCUMENT);
done();
}
});
sendLoadingMsg(main$);
sendLoadingMsg(main$, RecordRawType.DOCUMENT);
});
test('sendErrorMsg', (done) => {
const main$ = new BehaviorSubject<DataMainMsg>({ fetchStatus: FetchStatus.PARTIAL });

View file

@ -12,6 +12,7 @@ import {
DataDocuments$,
DataMain$,
DataTotalHits$,
RecordRawType,
SavedSearchData,
} from './use_saved_search';
@ -33,10 +34,12 @@ export function sendCompleteMsg(main$: DataMain$, foundDocuments = true) {
if (main$.getValue().fetchStatus === FetchStatus.COMPLETE) {
return;
}
const recordRawType = main$.getValue().recordRawType;
main$.next({
fetchStatus: FetchStatus.COMPLETE,
foundDocuments,
error: undefined,
recordRawType,
});
}
@ -45,8 +48,10 @@ export function sendCompleteMsg(main$: DataMain$, foundDocuments = true) {
*/
export function sendPartialMsg(main$: DataMain$) {
if (main$.getValue().fetchStatus === FetchStatus.LOADING) {
const recordRawType = main$.getValue().recordRawType;
main$.next({
fetchStatus: FetchStatus.PARTIAL,
recordRawType,
});
}
}
@ -54,10 +59,14 @@ export function sendPartialMsg(main$: DataMain$) {
/**
* Send LOADING message via main observable
*/
export function sendLoadingMsg(data$: DataMain$ | DataDocuments$ | DataTotalHits$ | DataCharts$) {
export function sendLoadingMsg(
data$: DataMain$ | DataDocuments$ | DataTotalHits$ | DataCharts$,
recordRawType: RecordRawType
) {
if (data$.getValue().fetchStatus !== FetchStatus.LOADING) {
data$.next({
fetchStatus: FetchStatus.LOADING,
recordRawType,
});
}
}
@ -69,9 +78,11 @@ export function sendErrorMsg(
data$: DataMain$ | DataDocuments$ | DataTotalHits$ | DataCharts$,
error: Error
) {
const recordRawType = data$.getValue().recordRawType;
data$.next({
fetchStatus: FetchStatus.ERROR,
error,
recordRawType,
});
}
@ -80,21 +91,26 @@ export function sendErrorMsg(
* Needed when index pattern is switched or a new runtime field is added
*/
export function sendResetMsg(data: SavedSearchData, initialFetchStatus: FetchStatus) {
const recordRawType = data.main$.getValue().recordRawType;
data.main$.next({
fetchStatus: initialFetchStatus,
foundDocuments: undefined,
recordRawType,
});
data.documents$.next({
fetchStatus: initialFetchStatus,
result: [],
recordRawType,
});
data.charts$.next({
fetchStatus: initialFetchStatus,
chartData: undefined,
bucketInterval: undefined,
recordRawType,
});
data.totalHits$.next({
fetchStatus: initialFetchStatus,
result: undefined,
recordRawType,
});
}

View file

@ -16,6 +16,7 @@ import {
compareFilters,
COMPARE_ALL_OPTIONS,
Query,
AggregateQuery,
} from '@kbn/es-query';
import {
createKbnUrlStateStorage,
@ -69,7 +70,7 @@ export interface AppState {
/**
* Lucence or KQL query
*/
query?: Query;
query?: Query | AggregateQuery;
/**
* Array of the used sorting [[field,direction],...]
*/

View file

@ -5,6 +5,7 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { isOfAggregateQueryType } from '@kbn/es-query';
import { migrateLegacyQuery } from '../../../utils/migrate_legacy_query';
import { AppState, AppStateUrl } from '../services/discover_state';
@ -13,7 +14,12 @@ import { AppState, AppStateUrl } from '../services/discover_state';
* @param appStateFromUrl
*/
export function cleanupUrlState(appStateFromUrl: AppStateUrl): AppState {
if (appStateFromUrl && appStateFromUrl.query && !appStateFromUrl.query.language) {
if (
appStateFromUrl &&
appStateFromUrl.query &&
!isOfAggregateQueryType(appStateFromUrl.query) &&
!appStateFromUrl.query.language
) {
appStateFromUrl.query = migrateLegacyQuery(appStateFromUrl.query);
}

View file

@ -25,6 +25,7 @@ import {
} from '../hooks/use_saved_search';
import { fetchDocuments } from './fetch_documents';
import { fetchSql } from './fetch_sql';
import { fetchChart } from './fetch_chart';
import { fetchTotalHits } from './fetch_total_hits';
import { indexPatternMock } from '../../../__mocks__/index_pattern';
@ -34,6 +35,10 @@ jest.mock('./fetch_documents', () => ({
fetchDocuments: jest.fn().mockResolvedValue([]),
}));
jest.mock('./fetch_sql', () => ({
fetchSql: jest.fn().mockResolvedValue([]),
}));
jest.mock('./fetch_chart', () => ({
fetchChart: jest.fn(),
}));
@ -45,6 +50,7 @@ jest.mock('./fetch_total_hits', () => ({
const mockFetchDocuments = fetchDocuments as unknown as jest.MockedFunction<typeof fetchDocuments>;
const mockFetchTotalHits = fetchTotalHits as unknown as jest.MockedFunction<typeof fetchTotalHits>;
const mockFetchChart = fetchChart as unknown as jest.MockedFunction<typeof fetchChart>;
const mockFetchSQL = fetchSql as unknown as jest.MockedFunction<typeof fetchSql>;
function subjectCollector<T>(subject: Subject<T>): () => Promise<T[]> {
const promise = firstValueFrom(
@ -89,6 +95,7 @@ describe('test fetchAll', () => {
searchSource = savedSearchMock.searchSource.createChild();
mockFetchDocuments.mockReset().mockResolvedValue([]);
mockFetchSQL.mockReset().mockResolvedValue([]);
mockFetchTotalHits.mockReset().mockResolvedValue(42);
mockFetchChart
.mockReset()
@ -116,14 +123,16 @@ describe('test fetchAll', () => {
{ _id: '1', _index: 'logs' },
{ _id: '2', _index: 'logs' },
];
mockFetchDocuments.mockResolvedValue(hits);
const documents = hits.map((hit) => buildDataTableRecord(hit, indexPatternMock));
mockFetchDocuments.mockResolvedValue(documents);
await fetchAll(subjects, searchSource, false, deps);
expect(await collect()).toEqual([
{ fetchStatus: FetchStatus.UNINITIALIZED },
{ fetchStatus: FetchStatus.LOADING },
{ fetchStatus: FetchStatus.LOADING, recordRawType: 'document' },
{
fetchStatus: FetchStatus.COMPLETE,
result: hits.map((hit) => buildDataTableRecord(hit, indexPatternMock)),
recordRawType: 'document',
result: documents,
},
]);
});
@ -135,14 +144,16 @@ describe('test fetchAll', () => {
{ _id: '2', _index: 'logs' },
];
searchSource.getField('index')!.isTimeBased = () => false;
mockFetchDocuments.mockResolvedValue(hits);
const documents = hits.map((hit) => buildDataTableRecord(hit, indexPatternMock));
mockFetchDocuments.mockResolvedValue(documents);
mockFetchTotalHits.mockResolvedValue(42);
await fetchAll(subjects, searchSource, false, deps);
expect(await collect()).toEqual([
{ fetchStatus: FetchStatus.UNINITIALIZED },
{ fetchStatus: FetchStatus.LOADING },
{ fetchStatus: FetchStatus.PARTIAL, result: 2 },
{ fetchStatus: FetchStatus.COMPLETE, result: 42 },
{ fetchStatus: FetchStatus.LOADING, recordRawType: 'document' },
{ fetchStatus: FetchStatus.PARTIAL, recordRawType: 'document', result: 2 },
{ fetchStatus: FetchStatus.COMPLETE, recordRawType: 'document', result: 42 },
]);
});
@ -152,8 +163,13 @@ describe('test fetchAll', () => {
await fetchAll(subjects, searchSource, false, deps);
expect(await collect()).toEqual([
{ fetchStatus: FetchStatus.UNINITIALIZED },
{ fetchStatus: FetchStatus.LOADING },
{ fetchStatus: FetchStatus.COMPLETE, bucketInterval: {}, chartData: {} },
{ fetchStatus: FetchStatus.LOADING, recordRawType: 'document' },
{
fetchStatus: FetchStatus.COMPLETE,
recordRawType: 'document',
bucketInterval: {},
chartData: {},
},
]);
});
@ -165,9 +181,9 @@ describe('test fetchAll', () => {
await fetchAll(subjects, searchSource, false, deps);
expect(await collect()).toEqual([
{ fetchStatus: FetchStatus.UNINITIALIZED },
{ fetchStatus: FetchStatus.LOADING },
{ fetchStatus: FetchStatus.PARTIAL, result: 0 }, // From documents query
{ fetchStatus: FetchStatus.COMPLETE, result: 32 },
{ fetchStatus: FetchStatus.LOADING, recordRawType: 'document' },
{ fetchStatus: FetchStatus.PARTIAL, recordRawType: 'document', result: 0 }, // From documents query
{ fetchStatus: FetchStatus.COMPLETE, recordRawType: 'document', result: 32 },
]);
expect(mockFetchTotalHits).not.toHaveBeenCalled();
});
@ -177,19 +193,26 @@ describe('test fetchAll', () => {
const collectMain = subjectCollector(subjects.main$);
searchSource.getField('index')!.isTimeBased = () => false;
mockFetchTotalHits.mockRejectedValue({ msg: 'Oh noes!' });
mockFetchDocuments.mockResolvedValue([{ _id: '1', _index: 'logs' }]);
const hits = [{ _id: '1', _index: 'logs' }];
const documents = hits.map((hit) => buildDataTableRecord(hit, indexPatternMock));
mockFetchDocuments.mockResolvedValue(documents);
await fetchAll(subjects, searchSource, false, deps);
expect(await collectTotalHits()).toEqual([
{ fetchStatus: FetchStatus.UNINITIALIZED },
{ fetchStatus: FetchStatus.LOADING },
{ fetchStatus: FetchStatus.PARTIAL, result: 1 },
{ fetchStatus: FetchStatus.ERROR, error: { msg: 'Oh noes!' } },
{ fetchStatus: FetchStatus.LOADING, recordRawType: 'document' },
{ fetchStatus: FetchStatus.PARTIAL, recordRawType: 'document', result: 1 },
{ fetchStatus: FetchStatus.ERROR, recordRawType: 'document', error: { msg: 'Oh noes!' } },
]);
expect(await collectMain()).toEqual([
{ fetchStatus: FetchStatus.UNINITIALIZED },
{ fetchStatus: FetchStatus.LOADING },
{ fetchStatus: FetchStatus.PARTIAL },
{ fetchStatus: FetchStatus.COMPLETE, foundDocuments: true },
{ fetchStatus: FetchStatus.LOADING, recordRawType: 'document' },
{ fetchStatus: FetchStatus.PARTIAL, recordRawType: 'document' },
{
fetchStatus: FetchStatus.COMPLETE,
foundDocuments: true,
error: undefined,
recordRawType: 'document',
},
]);
});
@ -200,10 +223,49 @@ describe('test fetchAll', () => {
await fetchAll(subjects, searchSource, false, deps);
expect(await collectMain()).toEqual([
{ fetchStatus: FetchStatus.UNINITIALIZED },
{ fetchStatus: FetchStatus.LOADING },
{ fetchStatus: FetchStatus.PARTIAL }, // From totalHits query
{ fetchStatus: FetchStatus.ERROR, error: { msg: 'This query failed' } },
{ fetchStatus: FetchStatus.LOADING, recordRawType: 'document' },
{ fetchStatus: FetchStatus.PARTIAL, recordRawType: 'document' }, // From totalHits query
{
fetchStatus: FetchStatus.ERROR,
error: { msg: 'This query failed' },
recordRawType: 'document',
},
// Here should be no COMPLETE coming anymore
]);
});
test('emits loading and documents on documents$ correctly for SQL query', async () => {
const collect = subjectCollector(subjects.documents$);
const hits = [
{ _id: '1', _index: 'logs' },
{ _id: '2', _index: 'logs' },
];
const documents = hits.map((hit) => buildDataTableRecord(hit, indexPatternMock));
mockFetchSQL.mockResolvedValue(documents);
deps = {
appStateContainer: {
getState: () => {
return { interval: 'auto', query: { sql: 'SELECT * from foo' } };
},
} as unknown as ReduxLikeStateContainer<AppState>,
abortController: new AbortController(),
data: discoverServiceMock.data,
inspectorAdapters: { requests: new RequestAdapter() },
searchSessionId: '123',
initialFetchStatus: FetchStatus.UNINITIALIZED,
useNewFieldsApi: true,
savedSearch: savedSearchMock,
services: discoverServiceMock,
};
await fetchAll(subjects, searchSource, false, deps);
expect(await collect()).toEqual([
{ fetchStatus: FetchStatus.UNINITIALIZED },
{ fetchStatus: FetchStatus.LOADING, recordRawType: 'plain' },
{
fetchStatus: FetchStatus.COMPLETE,
recordRawType: 'plain',
result: documents,
},
]);
});
});

View file

@ -9,7 +9,7 @@ import { DataPublicPluginStart, ISearchSource } from '@kbn/data-plugin/public';
import { Adapters } from '@kbn/inspector-plugin/common';
import { ReduxLikeStateContainer } from '@kbn/kibana-utils-plugin/common';
import { DataViewType } from '@kbn/data-views-plugin/public';
import { buildDataTableRecord } from '../../../utils/build_data_record';
import { getRawRecordType } from './get_raw_record_type';
import {
sendCompleteMsg,
sendErrorMsg,
@ -30,9 +30,11 @@ import {
DataDocuments$,
DataMain$,
DataTotalHits$,
RecordRawType,
SavedSearchData,
} from '../hooks/use_saved_search';
import { DiscoverServices } from '../../../build_services';
import { fetchSql } from './fetch_sql';
export interface FetchDeps {
abortController: AbortController;
@ -81,38 +83,42 @@ export function fetchAll(
};
try {
const indexPattern = searchSource.getField('index')!;
const dataView = searchSource.getField('index')!;
if (reset) {
sendResetMsg(dataSubjects, initialFetchStatus);
}
const { hideChart, sort, query } = appStateContainer.getState();
const recordRawType = getRawRecordType(query);
const useSql = recordRawType === RecordRawType.PLAIN;
const { hideChart, sort } = appStateContainer.getState();
// Update the base searchSource, base for all child fetches
updateSearchSource(searchSource, false, {
indexPattern,
services,
sort: sort as SortOrder[],
useNewFieldsApi,
});
if (recordRawType === RecordRawType.DOCUMENT) {
// Update the base searchSource, base for all child fetches
updateSearchSource(searchSource, false, {
indexPattern: dataView,
services,
sort: sort as SortOrder[],
useNewFieldsApi,
});
}
// Mark all subjects as loading
sendLoadingMsg(dataSubjects.main$);
sendLoadingMsg(dataSubjects.documents$);
sendLoadingMsg(dataSubjects.totalHits$);
sendLoadingMsg(dataSubjects.charts$);
sendLoadingMsg(dataSubjects.main$, recordRawType);
sendLoadingMsg(dataSubjects.documents$, recordRawType);
sendLoadingMsg(dataSubjects.totalHits$, recordRawType);
sendLoadingMsg(dataSubjects.charts$, recordRawType);
const isChartVisible =
!hideChart && indexPattern.isTimeBased() && indexPattern.type !== DataViewType.ROLLUP;
!hideChart && dataView.isTimeBased() && dataView.type !== DataViewType.ROLLUP;
// Start fetching all required requests
const documents = fetchDocuments(searchSource.createCopy(), fetchDeps);
const charts = isChartVisible ? fetchChart(searchSource.createCopy(), fetchDeps) : undefined;
const totalHits = !isChartVisible
? fetchTotalHits(searchSource.createCopy(), fetchDeps)
: undefined;
const documents =
useSql && query
? fetchSql(query, services.indexPatterns, data, services.expressions)
: fetchDocuments(searchSource.createCopy(), fetchDeps);
const charts =
isChartVisible && !useSql ? fetchChart(searchSource.createCopy(), fetchDeps) : undefined;
const totalHits =
!isChartVisible && !useSql ? fetchTotalHits(searchSource.createCopy(), fetchDeps) : undefined;
/**
* This method checks the passed in hit count and will send a PARTIAL message to main$
* if there are results, indicating that we have finished some of the requests that have been
@ -138,15 +144,14 @@ export function fetchAll(
dataSubjects.totalHits$.next({
fetchStatus: FetchStatus.PARTIAL,
result: docs.length,
recordRawType,
});
}
const dataView = searchSource.getField('index')!;
const resultDocs = docs.map((doc) => buildDataTableRecord(doc, dataView));
dataSubjects.documents$.next({
fetchStatus: FetchStatus.COMPLETE,
result: resultDocs,
result: docs,
recordRawType,
});
checkHitCount(docs.length);
@ -161,12 +166,14 @@ export function fetchAll(
dataSubjects.totalHits$.next({
fetchStatus: FetchStatus.COMPLETE,
result: chart.totalHits,
recordRawType,
});
dataSubjects.charts$.next({
fetchStatus: FetchStatus.COMPLETE,
chartData: chart.chartData,
bucketInterval: chart.bucketInterval,
recordRawType,
});
checkHitCount(chart.totalHits);
@ -175,7 +182,11 @@ export function fetchAll(
totalHits
?.then((hitCount) => {
dataSubjects.totalHits$.next({ fetchStatus: FetchStatus.COMPLETE, result: hitCount });
dataSubjects.totalHits$.next({
fetchStatus: FetchStatus.COMPLETE,
result: hitCount,
recordRawType,
});
checkHitCount(hitCount);
})
.catch(sendErrorTo(dataSubjects.totalHits$));

View file

@ -14,6 +14,9 @@ import { IKibanaSearchResponse } from '@kbn/data-plugin/public';
import { SearchResponse } from '@elastic/elasticsearch/lib/api/types';
import { FetchDeps } from './fetch_all';
import { fetchTotalHits } from './fetch_total_hits';
import type { EsHitRecord } from '../../../types';
import { buildDataTableRecord } from '../../../utils/build_data_record';
import { indexPatternMock } from '../../../__mocks__/index_pattern';
const getDeps = () =>
({
@ -30,10 +33,11 @@ describe('test fetchDocuments', () => {
const hits = [
{ _id: '1', foo: 'bar' },
{ _id: '2', foo: 'baz' },
];
] as unknown as EsHitRecord[];
const documents = hits.map((hit) => buildDataTableRecord(hit, indexPatternMock));
savedSearchMock.searchSource.fetch$ = () =>
of({ rawResponse: { hits: { hits } } } as unknown as IKibanaSearchResponse<SearchResponse>);
expect(fetchDocuments(savedSearchMock.searchSource, getDeps())).resolves.toEqual(hits);
expect(fetchDocuments(savedSearchMock.searchSource, getDeps())).resolves.toEqual(documents);
});
test('rejects on query failure', () => {

View file

@ -9,6 +9,7 @@ import { i18n } from '@kbn/i18n';
import { filter, map } from 'rxjs/operators';
import { lastValueFrom } from 'rxjs';
import { isCompleteResponse, ISearchSource } from '@kbn/data-plugin/public';
import { buildDataTableRecordList } from '../../../utils/build_data_record';
import { SAMPLE_SIZE_SETTING } from '../../../../common';
import { FetchDeps } from './fetch_all';
@ -31,6 +32,7 @@ export const fetchDocuments = (
// not a rollup index pattern.
searchSource.setOverwriteDataViewType(undefined);
}
const dataView = searchSource.getField('index')!;
const executionContext = {
description: 'fetch documents',
@ -53,7 +55,9 @@ export const fetchDocuments = (
})
.pipe(
filter((res) => isCompleteResponse(res)),
map((res) => res.rawResponse.hits.hits)
map((res) => {
return buildDataTableRecordList(res.rawResponse.hits.hits, dataView);
})
);
return lastValueFrom(fetch$);

View file

@ -0,0 +1,76 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { pluck } from 'rxjs/operators';
import { lastValueFrom } from 'rxjs';
import { Query, AggregateQuery, Filter } from '@kbn/es-query';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { ExpressionsStart } from '@kbn/expressions-plugin/public';
import type { Datatable } from '@kbn/expressions-plugin/public';
import type { DataViewsContract } from '@kbn/data-views-plugin/common';
import { queryStateToExpressionAst } from '@kbn/data-plugin/common';
import { DataTableRecord } from '../../../types';
interface SQLErrorResponse {
error: {
message: string;
};
type: 'error';
}
export function fetchSql(
query: Query | AggregateQuery,
dataViewsService: DataViewsContract,
data: DataPublicPluginStart,
expressions: ExpressionsStart,
filters?: Filter[],
inputQuery?: Query
) {
const timeRange = data.query.timefilter.timefilter.getTime();
return queryStateToExpressionAst({
filters,
query,
time: timeRange,
dataViewsService,
inputQuery,
})
.then((ast) => {
if (ast) {
const execution = expressions.run(ast, null);
let finalData: DataTableRecord[] = [];
let error: string | undefined;
execution.pipe(pluck('result')).subscribe((resp) => {
const response = resp as Datatable | SQLErrorResponse;
if (response.type === 'error') {
error = response.error.message;
} else {
const table = response as Datatable;
const rows = table?.rows ?? [];
finalData = rows.map(
(row: Record<string, string>, idx: number) =>
({
id: String(idx),
raw: row,
flattened: row,
} as unknown as DataTableRecord)
);
}
});
return lastValueFrom(execution).then(() => {
if (error) {
throw new Error(error);
} else {
return finalData || [];
}
});
}
return [];
})
.catch((err) => {
throw new Error(err.message);
});
}

View file

@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { RecordRawType } from '../hooks/use_saved_search';
import { getRawRecordType } from './get_raw_record_type';
describe('getRawRecordType', () => {
it('returns empty string for Query type query', () => {
const mode = getRawRecordType({ query: '', language: 'lucene' });
expect(mode).toEqual(RecordRawType.DOCUMENT);
});
it('returns sql for Query type query', () => {
const mode = getRawRecordType({ sql: 'SELECT * from foo' });
expect(mode).toEqual(RecordRawType.PLAIN);
});
});

View file

@ -0,0 +1,27 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import {
AggregateQuery,
Query,
isOfAggregateQueryType,
getAggregateQueryMode,
} from '@kbn/es-query';
import { RecordRawType } from '../hooks/use_saved_search';
export function getRawRecordType(query?: Query | AggregateQuery) {
if (query && isOfAggregateQueryType(query) && getAggregateQueryMode(query) === 'sql') {
return RecordRawType.PLAIN;
}
return RecordRawType.DOCUMENT;
}
export function isPlainRecord(query?: Query | AggregateQuery): query is AggregateQuery {
return getRawRecordType(query) === RecordRawType.PLAIN;
}

View file

@ -18,10 +18,15 @@ export const resultStatuses = {
* Returns the current state of the result, depends on fetchStatus and the given fetched rows
* Determines what is displayed in Discover main view (loading view, data view, empty data view, ...)
*/
export function getResultState(fetchStatus: FetchStatus, foundDocuments: boolean = false) {
export function getResultState(
fetchStatus: FetchStatus,
foundDocuments: boolean = false,
isPlainRecord?: boolean
) {
if (fetchStatus === FetchStatus.UNINITIALIZED) {
return resultStatuses.UNINITIALIZED;
}
if (isPlainRecord && fetchStatus === FetchStatus.ERROR) return resultStatuses.NO_RESULTS;
if (!foundDocuments && fetchStatus === FetchStatus.LOADING) return resultStatuses.LOADING;
else if (foundDocuments) return resultStatuses.READY;

View file

@ -5,7 +5,7 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { isOfAggregateQueryType, Query, AggregateQuery } from '@kbn/es-query';
import type { DataView } from '@kbn/data-views-plugin/public';
import { getSortArray, SortPairArr } from '../../../components/doc_table/utils/get_sort';
@ -19,7 +19,8 @@ export function getSwitchIndexPatternAppState(
currentColumns: string[],
currentSort: SortPairArr[],
modifyColumns: boolean = true,
sortDirection: string = 'desc'
sortDirection: string = 'desc',
query?: Query | AggregateQuery
) {
const nextColumns = modifyColumns
? currentColumns.filter(
@ -27,7 +28,11 @@ export function getSwitchIndexPatternAppState(
nextIndexPattern.fields.getByName(column) || !currentIndexPattern.fields.getByName(column)
)
: currentColumns;
const columns = nextColumns.length ? nextColumns : [];
let columns = nextColumns.length ? nextColumns : [];
if (query && isOfAggregateQueryType(query)) {
columns = [];
}
// when switching from an index pattern with timeField to an index pattern without timeField
// filter out sorting by timeField in case it is set. index patterns without timeField don't

View file

@ -5,7 +5,7 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { isOfAggregateQueryType } from '@kbn/es-query';
import { DataView } from '@kbn/data-views-plugin/public';
import { SavedObjectSaveOpts } from '@kbn/saved-objects-plugin/public';
import { updateSearchSource } from './update_search_source';
@ -14,7 +14,6 @@ import { AppState } from '../services/discover_state';
import type { SortOrder } from '../../../services/saved_searches';
import { DiscoverServices } from '../../../build_services';
import { saveSavedSearch } from '../../../services/saved_searches';
/**
* Helper function to update and persist the given savedSearch
*/
@ -63,6 +62,13 @@ export async function persistSavedSearch(
savedSearch.hideAggregatedPreview = state.hideAggregatedPreview;
}
// add a flag here to identify text based language queries
// these should be filtered out from the visualize editor
const isTextBasedQuery = state.query && isOfAggregateQueryType(state.query);
if (savedSearch.isTextBasedQuery || isTextBasedQuery) {
savedSearch.isTextBasedQuery = isTextBasedQuery;
}
try {
const id = await saveSavedSearch(savedSearch, saveOptions, services.core.savedObjects.client);
if (id) {

View file

@ -27,6 +27,7 @@ import {
DataViewsContract,
DataPublicPluginStart,
} from '@kbn/data-plugin/public';
import type { ExpressionsStart } from '@kbn/expressions-plugin/public';
import { Start as InspectorPublicPluginStart } from '@kbn/inspector-plugin/public';
import { SharePluginStart } from '@kbn/share-plugin/public';
import { ChartsPluginStart } from '@kbn/charts-plugin/public';
@ -81,6 +82,7 @@ export interface DiscoverServices {
spaces?: SpacesApi;
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
locator: DiscoverAppLocator;
expressions: ExpressionsStart;
}
export const buildServices = memoize(function (
@ -125,5 +127,6 @@ export const buildServices = memoize(function (
dataViewEditor: plugins.dataViewEditor,
triggersActionsUi: plugins.triggersActionsUi,
locator,
expressions: plugins.expressions,
};
});

View file

@ -3,6 +3,7 @@
max-width: 100%;
height: 100%;
overflow: hidden;
border-radius: $euiBorderRadius;
.euiDataGrid__controls {
border: none;
@ -14,12 +15,16 @@
padding: 0;
}
.dscDiscoverGrid__textLanguageMode .euiDataGridRowCell.euiDataGridRowCell--firstColumn {
padding: $euiSizeXS;
}
.euiDataGridRowCell.euiDataGridRowCell--lastColumn {
border-right: none;
}
.euiDataGridRowCell:first-of-type,
.euiDataGrid--headerShade.euiDataGrid--bordersAll .euiDataGridHeaderCell:first-of-type {
.dscDiscoverGrid__documentsMode .euiDataGridRowCell:first-of-type,
.dscDiscoverGrid__documentsMode .euiDataGrid--headerShade.euiDataGrid--bordersAll .euiDataGridHeaderCell:first-of-type {
border-left: none;
border-right: none;
}

View file

@ -117,7 +117,7 @@ export interface DiscoverGridProps {
/**
* Function to set the expanded document, which is displayed in a flyout
*/
setExpandedDoc: (doc?: DataTableRecord) => void;
setExpandedDoc?: (doc?: DataTableRecord) => void;
/**
* Grid display settings persisted in Elasticsearch (e.g. column width)
*/
@ -162,6 +162,10 @@ export interface DiscoverGridProps {
* Update row height state
*/
onUpdateRowHeight?: (rowHeight: number) => void;
/**
* Is text base lang mode enabled
*/
isPlainRecord?: boolean;
/**
* Current state value for rowsPerPage
*/
@ -207,6 +211,7 @@ export const DiscoverGrid = ({
className,
rowHeightState,
onUpdateRowHeight,
isPlainRecord = false,
rowsPerPageState,
onUpdateRowsPerPage,
onFieldEdited,
@ -391,9 +396,11 @@ export const DiscoverGrid = ({
isSortEnabled,
services,
valueToStringConverter,
onFilter,
editField,
}),
[
onFilter,
displayedColumns,
displayedRows,
indexPattern,
@ -428,8 +435,8 @@ export const DiscoverGrid = ({
return { columns: sortingColumns, onSort: () => {} };
}, [sortingColumns, onTableSort, isSortEnabled]);
const lead = useMemo(
() => getLeadControlColumns().filter(({ id }) => controlColumnIds.includes(id)),
[controlColumnIds]
() => getLeadControlColumns(setExpandedDoc).filter(({ id }) => controlColumnIds.includes(id)),
[controlColumnIds, setExpandedDoc]
);
const additionalControls = useMemo(
@ -539,7 +546,11 @@ export const DiscoverGrid = ({
data-title={searchTitle}
data-description={searchDescription}
data-document-number={displayedRows.length}
className={classnames(className, 'dscDiscoverGrid__table')}
className={classnames(
className,
'dscDiscoverGrid__table',
isPlainRecord ? 'dscDiscoverGrid__textLanguageMode' : 'dscDiscoverGrid__documentsMode'
)}
>
<EuiDataGridMemoized
aria-describedby={randomId}
@ -603,7 +614,7 @@ export const DiscoverGrid = ({
</p>
</EuiScreenReaderOnly>
)}
{expandedDoc && (
{setExpandedDoc && expandedDoc && (
<DiscoverGridFlyout
indexPattern={indexPattern}
hit={expandedDoc}

View file

@ -10,6 +10,7 @@ import React, { useContext } from 'react';
import { EuiDataGridColumnCellActionProps } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { DataViewField } from '@kbn/data-views-plugin/public';
import { DocViewFilterFn } from '../../services/doc_views/doc_views_types';
import { DiscoverGridContext, GridContext } from './discover_grid_context';
import { useDiscoverServices } from '../../hooks/use_discover_services';
import { copyValueToClipboard } from '../../utils/copy_value_to_clipboard';
@ -24,7 +25,7 @@ function onFilterCell(
const value = row.flattened[columnId];
const field = context.indexPattern.fields.getByName(columnId);
if (field) {
if (field && context.onFilter) {
context.onFilter(field, value, mode);
}
}
@ -116,10 +117,10 @@ export const CopyBtn = ({ Component, rowIndex, columnId }: EuiDataGridColumnCell
);
};
export function buildCellActions(field: DataViewField) {
export function buildCellActions(field: DataViewField, onFilter?: DocViewFilterFn) {
if (field?.type === '_source') {
return [CopyBtn];
} else if (!field.filterable) {
} else if (!onFilter || !field.filterable) {
return undefined;
}

View file

@ -24,6 +24,7 @@ describe('Discover grid columns', function () {
valueToStringConverter: discoverGridContextMock.valueToStringConverter,
rowsCount: 100,
services: discoverServiceMock,
onFilter: () => {},
});
expect(actual).toMatchInlineSnapshot(`
Array [
@ -134,6 +135,7 @@ describe('Discover grid columns', function () {
valueToStringConverter: discoverGridContextMock.valueToStringConverter,
rowsCount: 100,
services: discoverServiceMock,
onFilter: () => {},
});
expect(actual).toMatchInlineSnapshot(`
Array [
@ -238,6 +240,7 @@ describe('Discover grid columns', function () {
valueToStringConverter: discoverGridContextMock.valueToStringConverter,
rowsCount: 100,
services: discoverServiceMock,
onFilter: () => {},
});
expect(actual).toMatchInlineSnapshot(`
Array [

View file

@ -10,6 +10,7 @@ import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiDataGridColumn, EuiIcon, EuiScreenReaderOnly, EuiToolTip } from '@elastic/eui';
import type { DataView } from '@kbn/data-views-plugin/public';
import { DocViewFilterFn } from '../../services/doc_views/doc_views_types';
import { ExpandButton } from './discover_grid_expand_button';
import { DiscoverGridSettings } from './types';
import type { ValueToStringConverter } from '../../types';
@ -19,39 +20,44 @@ import { SelectButton } from './discover_grid_document_selection';
import { defaultTimeColumnWidth } from './constants';
import { buildCopyColumnNameButton, buildCopyColumnValuesButton } from './build_copy_column_button';
import { DiscoverServices } from '../../build_services';
import { DataTableRecord } from '../../types';
import { buildEditFieldButton } from './build_edit_field_button';
export function getLeadControlColumns() {
return [
{
id: 'openDetails',
width: 24,
headerCellRender: () => (
<EuiScreenReaderOnly>
<span>
{i18n.translate('discover.controlColumnHeader', {
defaultMessage: 'Control column',
})}
</span>
</EuiScreenReaderOnly>
),
rowCellRender: ExpandButton,
},
{
id: 'select',
width: 24,
rowCellRender: SelectButton,
headerCellRender: () => (
<EuiScreenReaderOnly>
<span>
{i18n.translate('discover.selectColumnHeader', {
defaultMessage: 'Select column',
})}
</span>
</EuiScreenReaderOnly>
),
},
];
const openDetails = {
id: 'openDetails',
width: 24,
headerCellRender: () => (
<EuiScreenReaderOnly>
<span>
{i18n.translate('discover.controlColumnHeader', {
defaultMessage: 'Control column',
})}
</span>
</EuiScreenReaderOnly>
),
rowCellRender: ExpandButton,
};
const select = {
id: 'select',
width: 24,
rowCellRender: SelectButton,
headerCellRender: () => (
<EuiScreenReaderOnly>
<span>
{i18n.translate('discover.selectColumnHeader', {
defaultMessage: 'Select column',
})}
</span>
</EuiScreenReaderOnly>
),
};
export function getLeadControlColumns(setExpandedDoc?: (doc?: DataTableRecord) => void) {
if (!setExpandedDoc) {
return [select];
}
return [openDetails, select];
}
function buildEuiGridColumn({
@ -63,6 +69,7 @@ function buildEuiGridColumn({
services,
valueToStringConverter,
rowsCount,
onFilter,
editField,
}: {
columnName: string;
@ -73,6 +80,7 @@ function buildEuiGridColumn({
services: DiscoverServices;
valueToStringConverter: ValueToStringConverter;
rowsCount: number;
onFilter?: DocViewFilterFn;
editField?: (fieldName: string) => void;
}) {
const indexPatternField = indexPattern.getFieldByName(columnName);
@ -115,7 +123,7 @@ function buildEuiGridColumn({
...(editFieldButton ? [editFieldButton] : []),
],
},
cellActions: indexPatternField ? buildCellActions(indexPatternField) : [],
cellActions: indexPatternField ? buildCellActions(indexPatternField, onFilter) : [],
};
if (column.id === indexPattern.timeFieldName) {
@ -161,6 +169,7 @@ export function getEuiGridColumns({
isSortEnabled,
services,
valueToStringConverter,
onFilter,
editField,
}: {
columns: string[];
@ -172,6 +181,7 @@ export function getEuiGridColumns({
isSortEnabled: boolean;
services: DiscoverServices;
valueToStringConverter: ValueToStringConverter;
onFilter: DocViewFilterFn;
editField?: (fieldName: string) => void;
}) {
const timeFieldName = indexPattern.timeFieldName;
@ -192,6 +202,7 @@ export function getEuiGridColumns({
services,
valueToStringConverter,
rowsCount,
onFilter,
editField,
})
);

View file

@ -13,9 +13,9 @@ import type { DataTableRecord, ValueToStringConverter } from '../../types';
export interface GridContext {
expanded?: DataTableRecord | undefined;
setExpanded: (hit?: DataTableRecord) => void;
setExpanded?: (hit?: DataTableRecord) => void;
rows: DataTableRecord[];
onFilter: DocViewFilterFn;
onFilter?: DocViewFilterFn;
indexPattern: DataView;
isDarkMode: boolean;
selectedDocs: string[];

View file

@ -43,6 +43,9 @@ export const ExpandButton = ({ rowIndex, setCellProps }: EuiDataGridCellValueEle
const testSubj = current.isAnchor
? 'docTableExpandToggleColumnAnchor'
: 'docTableExpandToggleColumn';
if (!setExpanded) {
return null;
}
return (
<EuiToolTip content={buttonLabel} delay="long">
@ -52,7 +55,7 @@ export const ExpandButton = ({ rowIndex, setCellProps }: EuiDataGridCellValueEle
iconSize="s"
aria-label={buttonLabel}
data-test-subj={testSubj}
onClick={() => setExpanded(isCurrentRowExpanded ? undefined : current)}
onClick={() => setExpanded?.(isCurrentRowExpanded ? undefined : current)}
color={isCurrentRowExpanded ? 'primary' : 'text'}
iconType={isCurrentRowExpanded ? 'minimize' : 'expand'}
isSelected={isCurrentRowExpanded}

View file

@ -38,7 +38,7 @@ export interface DiscoverGridFlyoutProps {
indexPattern: DataView;
onAddColumn: (column: string) => void;
onClose: () => void;
onFilter: DocViewFilterFn;
onFilter?: DocViewFilterFn;
onRemoveColumn: (column: string) => void;
setExpandedDoc: (doc: DataTableRecord) => void;
}
@ -213,14 +213,18 @@ export function DiscoverGridFlyout({
hit={actualHit}
columns={columns}
indexPattern={indexPattern}
filter={(mapping, value, mode) => {
onFilter(mapping, value, mode);
services.toastNotifications.addSuccess(
i18n.translate('discover.grid.flyout.toastFilterAdded', {
defaultMessage: `Filter was added`,
})
);
}}
filter={
onFilter
? (mapping, value, mode) => {
onFilter(mapping, value, mode);
services.toastNotifications.addSuccess(
i18n.translate('discover.grid.flyout.toastFilterAdded', {
defaultMessage: `Filter was added`,
})
);
}
: undefined
}
onRemoveColumn={(columnName: string) => {
onRemoveColumn(columnName);
services.toastNotifications.addSuccess(

View file

@ -16,10 +16,10 @@ import { useDiscoverTourContext } from './discover_tour_context';
import { DISCOVER_TOUR_STEP_ANCHORS } from './discover_tour_anchors';
describe('Discover tour', () => {
const mountComponent = (innerContent?: JSX.Element) => {
const mountComponent = (innerContent: JSX.Element) => {
return mountWithIntl(
<KibanaContextProvider services={discoverServiceMock}>
<DiscoverTourProvider>{innerContent}</DiscoverTourProvider>
<DiscoverTourProvider isPlainRecord={false}>{innerContent}</DiscoverTourProvider>
</KibanaContextProvider>
);
};

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import React, { useCallback, useMemo } from 'react';
import React, { ReactElement, useCallback, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import {
@ -40,43 +40,64 @@ interface TourStepDefinition {
imageAltText: string;
}
const ADD_FIELDS_STEP = {
anchor: DISCOVER_TOUR_STEP_ANCHORS.addFields,
title: i18n.translate('discover.dscTour.stepAddFields.title', {
defaultMessage: 'Add fields to the table',
}),
content: (
<FormattedMessage
id="discover.dscTour.stepAddFields.description"
defaultMessage="Click {plusIcon} to add the fields that interest you."
values={{
plusIcon: <EuiIcon size="s" type="plusInCircleFilled" color="primary" aria-label="+" />,
}}
/>
),
imageName: 'add_fields.gif',
imageAltText: i18n.translate('discover.dscTour.stepAddFields.imageAltText', {
defaultMessage:
'In the Available fields list, click the plus icon to toggle a field into the document table.',
}),
};
const ORDER_TABLE_COLUMNS_STEP = {
anchor: DISCOVER_TOUR_STEP_ANCHORS.reorderColumns,
title: i18n.translate('discover.dscTour.stepReorderColumns.title', {
defaultMessage: 'Order the table columns',
}),
content: (
<FormattedMessage
id="discover.dscTour.stepReorderColumns.description"
defaultMessage="Drag columns to the order you want."
/>
),
imageName: 'reorder_columns.gif',
imageAltText: i18n.translate('discover.dscTour.stepReorderColumns.imageAltText', {
defaultMessage: 'Use the Columns popover to drag the columns to the order you prefer.',
}),
};
const CHANGE_ROW_HEIGHT_STEP = {
anchor: DISCOVER_TOUR_STEP_ANCHORS.changeRowHeight,
title: i18n.translate('discover.dscTour.stepChangeRowHeight.title', {
defaultMessage: 'Change the row height',
}),
content: (
<FormattedMessage
id="discover.dscTour.stepChangeRowHeight.description"
defaultMessage="Adjust the number of lines to fit the contents."
/>
),
imageName: 'rows_per_line.gif',
imageAltText: i18n.translate('discover.dscTour.stepChangeRowHeight.imageAltText', {
defaultMessage: 'Click the display options icon to adjust the row height to fit the contents.',
}),
};
const tourStepDefinitions: TourStepDefinition[] = [
{
anchor: DISCOVER_TOUR_STEP_ANCHORS.addFields,
title: i18n.translate('discover.dscTour.stepAddFields.title', {
defaultMessage: 'Add fields to the table',
}),
content: (
<FormattedMessage
id="discover.dscTour.stepAddFields.description"
defaultMessage="Click {plusIcon} to add the fields that interest you."
values={{
plusIcon: <EuiIcon size="s" type="plusInCircleFilled" color="primary" aria-label="+" />,
}}
/>
),
imageName: 'add_fields.gif',
imageAltText: i18n.translate('discover.dscTour.stepAddFields.imageAltText', {
defaultMessage:
'In the Available fields list, click the plus icon to toggle a field into the document table.',
}),
},
{
anchor: DISCOVER_TOUR_STEP_ANCHORS.reorderColumns,
title: i18n.translate('discover.dscTour.stepReorderColumns.title', {
defaultMessage: 'Order the table columns',
}),
content: (
<FormattedMessage
id="discover.dscTour.stepReorderColumns.description"
defaultMessage="Drag columns to the order you want."
/>
),
imageName: 'reorder_columns.gif',
imageAltText: i18n.translate('discover.dscTour.stepReorderColumns.imageAltText', {
defaultMessage: 'Use the Columns popover to drag the columns to the order you prefer.',
}),
},
ADD_FIELDS_STEP,
ORDER_TABLE_COLUMNS_STEP,
{
anchor: DISCOVER_TOUR_STEP_ANCHORS.sort,
title: i18n.translate('discover.dscTour.stepSort.title', {
@ -94,23 +115,7 @@ const tourStepDefinitions: TourStepDefinition[] = [
'Click a column header and select the desired sort order. Adjust a multi-field sort using the fields sorted popover.',
}),
},
{
anchor: DISCOVER_TOUR_STEP_ANCHORS.changeRowHeight,
title: i18n.translate('discover.dscTour.stepChangeRowHeight.title', {
defaultMessage: 'Change the row height',
}),
content: (
<FormattedMessage
id="discover.dscTour.stepChangeRowHeight.description"
defaultMessage="Adjust the number of lines to fit the contents."
/>
),
imageName: 'rows_per_line.gif',
imageAltText: i18n.translate('discover.dscTour.stepChangeRowHeight.imageAltText', {
defaultMessage:
'Click the display options icon to adjust the row height to fit the contents.',
}),
},
CHANGE_ROW_HEIGHT_STEP,
{
anchor: DISCOVER_TOUR_STEP_ANCHORS.expandDocument,
title: i18n.translate('discover.dscTour.stepExpand.title', {
@ -193,7 +198,13 @@ const tourConfig: EuiTourState = {
tourSubtitle: '',
};
export const DiscoverTourProvider: React.FC = ({ children }) => {
export const DiscoverTourProvider = ({
children,
isPlainRecord,
}: {
children: ReactElement;
isPlainRecord: boolean;
}) => {
const services = useDiscoverServices();
const prependToBasePath = services.core.http.basePath.prepend;
const getAssetPath = useCallback(
@ -203,8 +214,14 @@ export const DiscoverTourProvider: React.FC = ({ children }) => {
[prependToBasePath]
);
const tourSteps = useMemo(
() => prepareTourSteps(tourStepDefinitions, getAssetPath),
[getAssetPath]
() =>
isPlainRecord
? prepareTourSteps(
[ADD_FIELDS_STEP, ORDER_TABLE_COLUMNS_STEP, CHANGE_ROW_HEIGHT_STEP],
getAssetPath
)
: prepareTourSteps(tourStepDefinitions, getAssetPath),
[getAssetPath, isPlainRecord]
);
const [steps, actions, reducerState] = useEuiTour(tourSteps, tourConfig);
const currentTourStep = reducerState.currentTourStep;

View file

@ -27,6 +27,7 @@ import { ISearchSource } from '@kbn/data-plugin/public';
import { DataView, DataViewField } from '@kbn/data-views-plugin/public';
import { UiActionsStart } from '@kbn/ui-actions-plugin/public';
import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
import { RecordRawType } from '../application/main/hooks/use_saved_search';
import { buildDataTableRecord } from '../utils/build_data_record';
import { DataTableRecord } from '../types';
import { ISearchEmbeddable, SearchInput, SearchOutput } from './types';
@ -52,6 +53,8 @@ import { SortOrder } from '../components/doc_table/components/table_header/helpe
import { VIEW_MODE } from '../components/view_mode_toggle';
import { updateSearchSource } from './utils/update_search_source';
import { FieldStatisticsTable } from '../application/main/components/field_stats_table';
import { getRawRecordType } from '../application/main/utils/get_raw_record_type';
import { fetchSql } from '../application/main/utils/fetch_sql';
export type SearchProps = Partial<DiscoverGridProps> &
Partial<DocTableProps> & {
@ -204,8 +207,36 @@ export class SavedSearchEmbeddable
}
: child;
const query = this.savedSearch.searchSource.getField('query');
const recordRawType = getRawRecordType(query);
const useSql = recordRawType === RecordRawType.PLAIN;
try {
// Make the request
// Request SQL data
if (useSql && query) {
const result = await fetchSql(
this.savedSearch.searchSource.getField('query')!,
this.services.indexPatterns,
this.services.data,
this.services.expressions,
this.input.filters,
this.input.query
);
this.updateOutput({
...this.getOutput(),
loading: false,
});
this.searchProps!.rows = result;
this.searchProps!.totalHitCount = result.length;
this.searchProps!.isLoading = false;
this.searchProps!.isPlainRecord = true;
this.searchProps!.showTimeCol = false;
this.searchProps!.isSortEnabled = false;
return;
}
// Request document data
const { rawResponse: resp } = await lastValueFrom(
searchSource.fetch$({
abortSignal: this.abortController.signal,

View file

@ -35,7 +35,11 @@ export function DiscoverGridEmbeddable(props: DiscoverGridEmbeddableProps) {
</EuiFlexItem>
)}
<EuiFlexItem style={{ minHeight: 0 }}>
<DataGridMemoized {...props} setExpandedDoc={setExpandedDoc} expandedDoc={expandedDoc} />
<DataGridMemoized
{...props}
setExpandedDoc={!props.isPlainRecord ? setExpandedDoc : undefined}
expandedDoc={expandedDoc}
/>
</EuiFlexItem>
</EuiFlexGroup>
);

View file

@ -7,7 +7,7 @@
*/
import type { SerializableRecord } from '@kbn/utility-types';
import type { Filter, TimeRange, Query } from '@kbn/es-query';
import type { Filter, TimeRange, Query, AggregateQuery } from '@kbn/es-query';
import type { GlobalQueryStateFromUrl, RefreshInterval } from '@kbn/data-plugin/public';
import type { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/public';
import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public';
@ -44,7 +44,7 @@ export interface DiscoverAppLocatorParams extends SerializableRecord {
/**
* Optionally set a query.
*/
query?: Query;
query?: Query | AggregateQuery;
/**
* If not given, will use the uiSettings configuration for `storeInSessionStorage`. useHash determines
@ -116,7 +116,7 @@ export class DiscoverAppLocatorDefinition implements LocatorDefinition<DiscoverA
} = params;
const savedSearchPath = savedSearchId ? `view/${encodeURIComponent(savedSearchId)}` : '';
const appState: {
query?: Query;
query?: Query | AggregateQuery;
filters?: Filter[];
index?: string;
columns?: string[];

View file

@ -18,6 +18,7 @@ import {
PluginInitializerContext,
} from '@kbn/core/public';
import { UiActionsSetup, UiActionsStart } from '@kbn/ui-actions-plugin/public';
import { ExpressionsSetup, ExpressionsStart } from '@kbn/expressions-plugin/public';
import { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/public';
import { ChartsPluginStart } from '@kbn/charts-plugin/public';
import { NavigationPublicPluginStart as NavigationStart } from '@kbn/navigation-plugin/public';
@ -152,6 +153,7 @@ export interface DiscoverSetupPlugins {
urlForwarding: UrlForwardingSetup;
home?: HomePublicPluginSetup;
data: DataPublicPluginSetup;
expressions: ExpressionsSetup;
}
/**
@ -173,6 +175,7 @@ export interface DiscoverStartPlugins {
dataViewFieldEditor: IndexPatternFieldEditorStart;
spaces?: SpacesPluginStart;
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
expressions: ExpressionsStart;
}
/**

View file

@ -104,6 +104,7 @@ describe('getSavedSearch', () => {
"hideAggregatedPreview": undefined,
"hideChart": false,
"id": "ccf1af80-2297-11ec-86e0-1155ffb9c7a7",
"isTextBasedQuery": undefined,
"rowHeight": undefined,
"rowsPerPage": undefined,
"searchSource": Object {
@ -149,4 +150,97 @@ describe('getSavedSearch', () => {
}
`);
});
test('should find saved search with sql mode', async () => {
savedObjectsClient.resolve = jest.fn().mockReturnValue({
saved_object: {
attributes: {
kibanaSavedObjectMeta: {
searchSourceJSON:
'{"query":{"sql":"SELECT * FROM foo"},"filter":[],"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.index"}',
},
title: 'test2',
sort: [['order_date', 'desc']],
columns: ['_source'],
description: 'description',
grid: {},
hideChart: true,
isTextBasedQuery: true,
},
id: 'ccf1af80-2297-11ec-86e0-1155ffb9c7a7',
type: 'search',
references: [
{
name: 'kibanaSavedObjectMeta.searchSourceJSON.index',
id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
type: 'index-pattern',
},
],
namespaces: ['default'],
},
outcome: 'exactMatch',
});
const savedSearch = await getSavedSearch('ccf1af80-2297-11ec-86e0-1155ffb9c7a7', {
savedObjectsClient,
search,
});
expect(savedObjectsClient.resolve).toHaveBeenCalled();
expect(savedSearch).toMatchInlineSnapshot(`
Object {
"columns": Array [
"_source",
],
"description": "description",
"grid": Object {},
"hideAggregatedPreview": undefined,
"hideChart": true,
"id": "ccf1af80-2297-11ec-86e0-1155ffb9c7a7",
"isTextBasedQuery": true,
"rowHeight": undefined,
"rowsPerPage": undefined,
"searchSource": Object {
"create": [MockFunction],
"createChild": [MockFunction],
"createCopy": [MockFunction],
"destroy": [MockFunction],
"fetch": [MockFunction],
"fetch$": [MockFunction],
"getActiveIndexFilter": [MockFunction],
"getField": [MockFunction],
"getFields": [MockFunction],
"getId": [MockFunction],
"getOwnField": [MockFunction],
"getParent": [MockFunction],
"getSearchRequestBody": [MockFunction],
"getSerializedFields": [MockFunction],
"history": Array [],
"onRequestStart": [MockFunction],
"parseActiveIndexPatternFromQueryString": [MockFunction],
"removeField": [MockFunction],
"serialize": [MockFunction],
"setField": [MockFunction],
"setFields": [MockFunction],
"setOverwriteDataViewType": [MockFunction],
"setParent": [MockFunction],
"toExpressionAst": [MockFunction],
},
"sharingSavedObjectProps": Object {
"aliasPurpose": undefined,
"aliasTargetId": undefined,
"errorJSON": undefined,
"outcome": "exactMatch",
},
"sort": Array [
Array [
"order_date",
"desc",
],
],
"title": "test2",
"viewMode": undefined,
}
`);
});
});

View file

@ -87,6 +87,7 @@ describe('saveSavedSearch', () => {
columns: [],
description: '',
grid: {},
isTextBasedQuery: false,
hideChart: false,
kibanaSavedObjectMeta: { searchSourceJSON: '{}' },
sort: [],
@ -106,6 +107,7 @@ describe('saveSavedSearch', () => {
columns: [],
description: '',
grid: {},
isTextBasedQuery: false,
hideChart: false,
kibanaSavedObjectMeta: { searchSourceJSON: '{}' },
sort: [],

View file

@ -27,6 +27,7 @@ describe('saved_searches_utils', () => {
description: 'foo',
grid: {},
hideChart: true,
isTextBasedQuery: false,
};
expect(fromSavedSearchAttributes('id', attributes, createSearchSourceMock(), {}))
@ -41,6 +42,7 @@ describe('saved_searches_utils', () => {
"hideAggregatedPreview": undefined,
"hideChart": true,
"id": "id",
"isTextBasedQuery": false,
"rowHeight": undefined,
"rowsPerPage": undefined,
"searchSource": SearchSource {
@ -104,6 +106,7 @@ describe('saved_searches_utils', () => {
description: 'description',
grid: {},
hideChart: true,
isTextBasedQuery: true,
};
expect(toSavedSearchAttributes(savedSearch, '{}')).toMatchInlineSnapshot(`
@ -116,6 +119,7 @@ describe('saved_searches_utils', () => {
"grid": Object {},
"hideAggregatedPreview": undefined,
"hideChart": true,
"isTextBasedQuery": true,
"kibanaSavedObjectMeta": Object {
"searchSourceJSON": "{}",
},

View file

@ -45,6 +45,7 @@ export const fromSavedSearchAttributes = (
viewMode: attributes.viewMode,
hideAggregatedPreview: attributes.hideAggregatedPreview,
rowHeight: attributes.rowHeight,
isTextBasedQuery: attributes.isTextBasedQuery,
rowsPerPage: attributes.rowsPerPage,
});
@ -62,5 +63,6 @@ export const toSavedSearchAttributes = (
viewMode: savedSearch.viewMode,
hideAggregatedPreview: savedSearch.hideAggregatedPreview,
rowHeight: savedSearch.rowHeight,
isTextBasedQuery: savedSearch.isTextBasedQuery ?? false,
rowsPerPage: savedSearch.rowsPerPage,
});

View file

@ -21,6 +21,7 @@ export interface SavedSearchAttributes {
columns?: Record<string, DiscoverGridSettingsColumn>;
};
hideChart: boolean;
isTextBasedQuery: boolean;
kibanaSavedObjectMeta: {
searchSourceJSON: string;
};
@ -54,5 +55,6 @@ export interface SavedSearch {
viewMode?: VIEW_MODE;
hideAggregatedPreview?: boolean;
rowHeight?: number;
isTextBasedQuery?: boolean;
rowsPerPage?: number;
}

View file

@ -38,6 +38,7 @@ export function getSavedSearchObjectType(
description: { type: 'text' },
viewMode: { type: 'keyword', index: false, doc_values: false },
hideChart: { type: 'boolean', index: false, doc_values: false },
isTextBasedQuery: { type: 'boolean', index: false, doc_values: false },
hideAggregatedPreview: { type: 'boolean', index: false, doc_values: false },
hits: { type: 'integer', index: false, doc_values: false },
kibanaSavedObjectMeta: {

View file

@ -30,9 +30,14 @@ import {
TRUNCATE_MAX_HEIGHT,
SHOW_FIELD_STATISTICS,
ROW_HEIGHT_OPTION,
ENABLE_SQL,
} from '../common';
import { DEFAULT_ROWS_PER_PAGE, ROWS_PER_PAGE_OPTIONS } from '../common/constants';
const technicalPreviewLabel = i18n.translate('discover.advancedSettings.technicalPreviewLabel', {
defaultMessage: 'technical preview',
});
export const getUiSettings: (docLinks: DocLinksServiceSetup) => Record<string, UiSettingsParams> = (
docLinks: DocLinksServiceSetup
) => ({
@ -303,4 +308,26 @@ export const getUiSettings: (docLinks: DocLinksServiceSetup) => Record<string, U
schema: schema.number({ min: 0 }),
requiresPageReload: true,
},
[ENABLE_SQL]: {
name: i18n.translate('discover.advancedSettings.enableSQLTitle', {
defaultMessage: 'Enable SQL',
}),
value: false,
description: i18n.translate('discover.advancedSettings.enableSQLDescription', {
defaultMessage:
'{technicalPreviewLabel} This tech preview feature is highly experimental--do not rely on this for production saved searches or dashboards. This setting enables SQL as a text-based query language in Discover. If you have feedback on this experience please reach out to us on {link}',
values: {
link:
`<a href="https://discuss.elastic.co/c/elastic-stack/kibana" target="_blank" rel="noopener">` +
i18n.translate('discover.advancedSettings.enableSQL.discussLinkText', {
defaultMessage: 'discuss.elastic.co/c/elastic-stack/kibana',
}) +
'</a>',
technicalPreviewLabel: `<em>[${technicalPreviewLabel}]</em>`,
},
}),
requiresPageReload: true,
category: ['discover'],
schema: schema.boolean(),
},
});

View file

@ -11,6 +11,7 @@
{ "path": "../../core/tsconfig.json" },
{ "path": "../charts/tsconfig.json" },
{ "path": "../data/tsconfig.json" },
{ "path": "../expressions/tsconfig.json" },
{ "path": "../embeddable/tsconfig.json" },
{ "path": "../inspector/tsconfig.json" },
{ "path": "../url_forwarding/tsconfig.json" },

View file

@ -486,6 +486,10 @@ export const stackManagementSchema: MakeSchemaFrom<UsageStats> = {
type: 'boolean',
_meta: { description: 'Non-default value of setting.' },
},
'discover:enableSql': {
type: 'boolean',
_meta: { description: 'Non-default value of setting.' },
},
'discover:rowHeightOption': {
type: 'integer',
_meta: { description: 'Non-default value of setting.' },

View file

@ -34,6 +34,7 @@ export interface UsageStats {
'discover:searchFieldsFromSource': boolean;
'discover:showFieldStatistics': boolean;
'discover:showMultiFields': boolean;
'discover:enableSql': boolean;
'discover:maxDocFieldsDisplayed': number;
'securitySolution:rulesTableRefresh': string;
'observability:enableInspectEsQueries': boolean;

View file

@ -23,6 +23,7 @@ const createStartContract = (): jest.Mocked<Start> => {
const startContract = {
ui: {
TopNavMenu: jest.fn(),
AggregateQueryTopNavMenu: jest.fn(),
},
};
return startContract;

View file

@ -39,6 +39,7 @@ export class NavigationPublicPlugin
return {
ui: {
TopNavMenu: createTopNav(unifiedSearch, extensions),
AggregateQueryTopNavMenu: createTopNav(unifiedSearch, extensions),
},
};
}

Some files were not shown because too many files have changed in this diff Show more