mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
* [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:
parent
68162dca7c
commit
a296e4cc97
154 changed files with 5984 additions and 853 deletions
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
|
|
84
packages/kbn-es-query/src/es_query/es_query_sql.test.ts
Normal file
84
packages/kbn-es-query/src/es_query/es_query_sql.test.ts
Normal 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!');
|
||||
});
|
||||
});
|
||||
});
|
46
packages/kbn-es-query/src/es_query/es_query_sql.ts
Normal file
46
packages/kbn-es-query/src/es_query/es_query_sql.ts
Normal 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 '';
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -82,6 +82,8 @@ export type Query = {
|
|||
language: string;
|
||||
};
|
||||
|
||||
export type AggregateQuery = { sql: string } | { esql: string };
|
||||
|
||||
/**
|
||||
* An interface for a latitude-longitude pair
|
||||
* @public
|
||||
|
|
|
@ -59,6 +59,7 @@ export {
|
|||
|
||||
export type {
|
||||
Query,
|
||||
AggregateQuery,
|
||||
Filter,
|
||||
LatLon,
|
||||
FieldFilter,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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$(),
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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'],
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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'],
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
}
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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' };
|
||||
}
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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}
|
||||
*/
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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 }),
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -14,7 +14,8 @@
|
|||
"uiActions",
|
||||
"savedObjects",
|
||||
"dataViewFieldEditor",
|
||||
"dataViewEditor"
|
||||
"dataViewEditor",
|
||||
"expressions"
|
||||
],
|
||||
"optionalPlugins": ["home", "share", "usageCollection", "spaces", "triggersActionsUi"],
|
||||
"requiredBundles": ["kibanaUtils", "kibanaReact", "dataViews", "unifiedSearch"],
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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;
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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>
|
||||
))
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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$}
|
||||
/>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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"
|
||||
>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -21,6 +21,7 @@ describe('DiscoverFieldSearch', () => {
|
|||
value: 'test',
|
||||
types: ['any', 'string', '_source'],
|
||||
presentFieldTypes: ['string', 'date', 'boolean', 'number'],
|
||||
isPlainRecord: false,
|
||||
};
|
||||
|
||||
function mountComponent(props?: Props) {
|
||||
|
|
|
@ -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',
|
||||
})}
|
||||
|
||||
<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',
|
||||
})}
|
||||
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
]
|
||||
);
|
||||
|
|
|
@ -43,6 +43,7 @@ function getProps(savePermissions = true): DiscoverTopNavProps {
|
|||
resetSavedSearch: () => {},
|
||||
onFieldEdited: jest.fn(),
|
||||
onChangeIndexPattern: jest.fn(),
|
||||
isPlainRecord: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
|
|
@ -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] : []),
|
||||
];
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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],...]
|
||||
*/
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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$));
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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$);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 [
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
);
|
||||
|
|
|
@ -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[];
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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[];
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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: [],
|
||||
|
|
|
@ -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": "{}",
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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(),
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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" },
|
||||
|
|
|
@ -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.' },
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -23,6 +23,7 @@ const createStartContract = (): jest.Mocked<Start> => {
|
|||
const startContract = {
|
||||
ui: {
|
||||
TopNavMenu: jest.fn(),
|
||||
AggregateQueryTopNavMenu: jest.fn(),
|
||||
},
|
||||
};
|
||||
return startContract;
|
||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue