diff --git a/package.json b/package.json index c728618fcbcd..f1fa8e46f069 100644 --- a/package.json +++ b/package.json @@ -136,7 +136,7 @@ }, "dependencies": { "@aws-crypto/client-node": "^3.1.1", - "@elastic/datemath": "5.0.3", + "@elastic/datemath": "link:packages/opensearch-datemath", "@elastic/eui": "npm:@opensearch-project/oui@1.5.1", "@elastic/good": "^9.0.1-kibana3", "@elastic/numeral": "npm:@amoo-miki/numeral@2.6.0", diff --git a/packages/opensearch-datemath/index.d.ts b/packages/opensearch-datemath/index.d.ts index d5a38f0176ea..0706d7d0dccf 100644 --- a/packages/opensearch-datemath/index.d.ts +++ b/packages/opensearch-datemath/index.d.ts @@ -43,6 +43,8 @@ declare const datemath: { unitsAsc: Unit[]; unitsDesc: Unit[]; + isDateTime(input: any): boolean; + /** * Parses a string into a moment object. The string can be something like "now - 15m". * @param options.forceNow If this optional parameter is supplied, "now" will be treated as this diff --git a/packages/opensearch-datemath/index.js b/packages/opensearch-datemath/index.js index 4367949d7cf0..eba9fd2be5a6 100644 --- a/packages/opensearch-datemath/index.js +++ b/packages/opensearch-datemath/index.js @@ -49,6 +49,7 @@ const isDate = (d) => Object.prototype.toString.call(d) === '[object Date]'; const isValidDate = (d) => isDate(d) && !isNaN(d.valueOf()); +const isDateTime = (d) => moment.isMoment(d); /* * This is a simplified version of opensearch's date parser. * If you pass in a momentjs instance as the third parameter the calculation @@ -57,7 +58,7 @@ const isValidDate = (d) => isDate(d) && !isNaN(d.valueOf()); */ function parse(text, { roundUp = false, momentInstance = moment, forceNow } = {}) { if (!text) return undefined; - if (momentInstance.isMoment(text)) return text; + if (isDateTime(text)) return text; if (isDate(text)) return momentInstance(text); if (forceNow !== undefined && !isValidDate(forceNow)) { throw new Error('forceNow must be a valid Date'); @@ -164,6 +165,7 @@ function parseDateMath(mathString, time, roundUp) { module.exports = { parse: parse, + isDateTime: isDateTime, unitsMap: Object.freeze(unitsMap), units: Object.freeze(units), unitsAsc: Object.freeze(unitsAsc), diff --git a/packages/osd-opensearch/src/cli_commands/snapshot.js b/packages/osd-opensearch/src/cli_commands/snapshot.js index ff21dbe851c8..84d6acee104e 100644 --- a/packages/osd-opensearch/src/cli_commands/snapshot.js +++ b/packages/osd-opensearch/src/cli_commands/snapshot.js @@ -50,6 +50,7 @@ exports.help = (defaults = {}) => { --download-only Download the snapshot but don't actually start it --ssl Sets up SSL on OpenSearch --security Installs and sets up the OpenSearch Security plugin on the cluster + --sql Installs and sets up the required OpenSearch SQL/PPL plugins on the cluster --P OpenSearch plugin artifact URL to install it on the cluster. We can use the flag multiple times to install multiple plugins on the cluster snapshot. The argument value can be url to zip file, maven coordinates of the plugin or for local zip files, use file:. @@ -77,6 +78,8 @@ exports.run = async (defaults = {}) => { boolean: ['security'], + boolean: ['sql'], + default: defaults, }); @@ -98,6 +101,10 @@ exports.run = async (defaults = {}) => { await cluster.setupSecurity(installPath, options.version ?? defaults.version); } + if (options.sql) { + await cluster.setupSql(installPath, options.version ?? defaults.version); + } + options.bundledJDK = true; await cluster.run(installPath, options); diff --git a/packages/osd-opensearch/src/cluster.js b/packages/osd-opensearch/src/cluster.js index 455a1e5f919f..4b1c8b38259d 100644 --- a/packages/osd-opensearch/src/cluster.js +++ b/packages/osd-opensearch/src/cluster.js @@ -70,10 +70,11 @@ const first = (stream, map) => }); exports.Cluster = class Cluster { - constructor({ log = defaultLog, ssl = false, security = false } = {}) { + constructor({ log = defaultLog, ssl = false, security = false, sql = false } = {}) { this._log = log; this._ssl = ssl; this._security = security; + this._sql = sql; this._caCertPromise = ssl ? readFile(CA_CERT_PATH) : undefined; } @@ -224,6 +225,28 @@ exports.Cluster = class Cluster { } } + /** + * Setups cluster with SQL/PPL plugins + * + * @param {string} installPath + * @property {String} version - version of OpenSearch + */ + async setupSql(installPath, version) { + await this.installSqlPlugin(installPath, version, 'opensearch-sql'); + await this.installSqlPlugin(installPath, version, 'opensearch-observability'); + } + + async installSqlPlugin(installPath, version, id) { + this._log.info(`Setting up: ${id}`); + try { + const pluginUrl = generateEnginePluginUrl(version, id); + await this.installOpenSearchPlugins(installPath, pluginUrl); + this._log.info(`Completed setup: ${id}`); + } catch (ex) { + this._log.warning(`Failed to setup: ${id}`); + } + } + /** * Starts OpenSearch and returns resolved promise once started * diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index aed5d74a2c01..c1e489afb4ef 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -249,6 +249,7 @@ export default function (program) { .option('--dev', 'Run the server with development mode defaults') .option('--ssl', 'Run the dev server using HTTPS') .option('--security', 'Run the dev server using security defaults') + .option('--sql', 'Run the dev server using SQL/PPL defaults') .option('--dist', 'Use production assets from osd/optimizer') .option( '--no-base-path', diff --git a/src/plugins/data/common/constants.ts b/src/plugins/data/common/constants.ts index 43db1fe72b9e..cbbd31665de5 100644 --- a/src/plugins/data/common/constants.ts +++ b/src/plugins/data/common/constants.ts @@ -60,4 +60,5 @@ export const UI_SETTINGS = { INDEXPATTERN_PLACEHOLDER: 'indexPattern:placeholder', FILTERS_PINNED_BY_DEFAULT: 'filters:pinnedByDefault', FILTERS_EDITOR_SUGGEST_VALUES: 'filterEditor:suggestValues', + DATAFRAME_HYDRATION_STRATEGY: 'dataframe:hydrationStrategy', } as const; diff --git a/src/plugins/data/common/data_frames/_df_cache.ts b/src/plugins/data/common/data_frames/_df_cache.ts new file mode 100644 index 000000000000..177f26840874 --- /dev/null +++ b/src/plugins/data/common/data_frames/_df_cache.ts @@ -0,0 +1,29 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { IDataFrame } from '..'; + +export interface DfCache { + get: () => IDataFrame | undefined; + set: (value: IDataFrame) => IDataFrame; + clear: () => void; +} + +export function createDataFrameCache(): DfCache { + let df: IDataFrame | undefined; + const cache: DfCache = { + get: () => { + return df; + }, + set: (prom: IDataFrame) => { + df = prom; + return prom; + }, + clear: () => { + df = undefined; + }, + }; + return cache; +} diff --git a/src/plugins/data/common/data_frames/fields/index.ts b/src/plugins/data/common/data_frames/fields/index.ts new file mode 100644 index 000000000000..9f269633f307 --- /dev/null +++ b/src/plugins/data/common/data_frames/fields/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './types'; diff --git a/src/plugins/data/common/data_frames/fields/types.ts b/src/plugins/data/common/data_frames/fields/types.ts new file mode 100644 index 000000000000..47144c0c0198 --- /dev/null +++ b/src/plugins/data/common/data_frames/fields/types.ts @@ -0,0 +1,18 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export interface IFieldType { + name: string; + type: string; + values: any[]; + count?: number; + aggregatable?: boolean; + filterable?: boolean; + searchable?: boolean; + sortable?: boolean; + visualizable?: boolean; + displayName?: string; + format?: any; +} diff --git a/src/plugins/data/common/data_frames/index.ts b/src/plugins/data/common/data_frames/index.ts new file mode 100644 index 000000000000..8b6a31eaea68 --- /dev/null +++ b/src/plugins/data/common/data_frames/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './types'; +export * from './utils'; diff --git a/src/plugins/data/common/data_frames/types.ts b/src/plugins/data/common/data_frames/types.ts new file mode 100644 index 000000000000..ce845281c7c7 --- /dev/null +++ b/src/plugins/data/common/data_frames/types.ts @@ -0,0 +1,35 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { IFieldType } from './fields'; + +export * from './_df_cache'; + +export interface IDataFrame { + name?: string; + schema?: Array>; + fields: IFieldType[]; + size: number; +} + +export interface DataFrameAgg { + key: string; + value: number; +} + +export interface PartialDataFrame extends Omit { + fields: Array>; +} + +/** + * To be utilize with aggregations and will map to buckets + * Plugins can get the aggreted value by their own logic + * Setting to null will disable the aggregation if plugin wishes + * In future, if the plugin doesn't intentionally set the value to null, + * we can calculate the value based on the fields. + */ +export interface IDataFrameWithAggs extends IDataFrame { + aggs: DataFrameAgg[] | null; +} diff --git a/src/plugins/data/common/data_frames/utils.ts b/src/plugins/data/common/data_frames/utils.ts new file mode 100644 index 000000000000..591be5c268e4 --- /dev/null +++ b/src/plugins/data/common/data_frames/utils.ts @@ -0,0 +1,149 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SearchResponse } from 'elasticsearch'; +import datemath from '@elastic/datemath'; +import { IDataFrame, IDataFrameWithAggs, PartialDataFrame } from './types'; +import { IFieldType } from './fields'; +import { IndexPatternFieldMap, IndexPatternSpec } from '../index_patterns'; +import { IOpenSearchDashboardsSearchRequest } from '../search'; + +const name = 'data_frame'; + +export interface IDataFrameResponse extends SearchResponse { + type: typeof name; + body: IDataFrame | IDataFrameWithAggs; + took: number; +} + +export const getRawQueryString = ( + searchRequest: IOpenSearchDashboardsSearchRequest +): string | undefined => { + return searchRequest.params?.body?.query?.queries[0]?.query; +}; + +export const convertResult = (response: IDataFrameResponse): SearchResponse => { + const data = response.body; + const hits: any[] = []; + for (let index = 0; index < data.size; index++) { + const hit: { [key: string]: any } = {}; + data.fields.forEach((field) => { + hit[field.name] = field.values[index]; + }); + hits.push({ + _index: data.name ?? '', + _type: '', + _id: '', + _score: 0, + _source: hit, + }); + } + const searchResponse: SearchResponse = { + took: response.took, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0, + }, + hits: { + total: 0, + max_score: 0, + hits, + }, + }; + + if (data.hasOwnProperty('aggs')) { + const dataWithAggs = data as IDataFrameWithAggs; + if (!dataWithAggs.aggs) { + // TODO: SQL best guess, get timestamp field and caculate it here + return searchResponse; + } + searchResponse.aggregations = { + 2: { + buckets: dataWithAggs.aggs.map((agg) => { + searchResponse.hits.total += agg.value; + return { + key: new Date(agg.key).getTime(), + key_as_string: agg.key, + doc_count: agg.value, + }; + }), + }, + }; + } + + return searchResponse; +}; + +export const formatFieldValue = (field: IFieldType | Partial, value: any): any => { + return field.format && field.format.convert ? field.format.convert(value) : value; +}; + +export const getFieldType = (field: IFieldType | Partial): string | undefined => { + if (field.name) { + const fieldName = field.name.toLowerCase(); + // TODO: feels little biased to check if timestamp. + // Has to be a better way to know so to be fair to all data sources + if (fieldName.includes('date') || fieldName.includes('timestamp')) { + return 'date'; + } + } + if (!field.values) return field.type; + const firstValue = field.values.filter((value) => value !== null && value !== undefined)[0]; + if (firstValue instanceof Date || datemath.isDateTime(firstValue)) { + return 'date'; + } + return field.type; +}; + +export const getTimeField = (data: IDataFrame): IFieldType | undefined => { + return data.fields.find((field) => field.type === 'date'); +}; + +export const createDataFrame = (partial: PartialDataFrame): IDataFrame | IDataFrameWithAggs => { + let size = 0; + const fields = partial.fields.map((field) => { + if (!field.values) { + field.values = new Array(size); + } else if (field.values.length > size) { + size = field.values.length; + } + field.type = getFieldType(field); + // if (!field.type) { + // need to think if this needs to be mapped to OSD field type for example + // PPL type for date is TIMESTAMP + // OSD is expecting date + // field.type = get type + // } + // get timeseries field + return field as IFieldType; + }); + + return { + ...partial, + fields, + size, + }; +}; + +export const dataFrameToSpec = (dataFrame: IDataFrame): IndexPatternSpec => { + return { + id: 'data_frame', + title: dataFrame.name, + timeFieldName: getTimeField(dataFrame)?.name, + fields: dataFrame.fields.reduce((acc, field) => { + acc[field.name] = { + name: field.name, + type: field.type, + aggregatable: true, + searchable: true, + }; + return acc; + }, {} as IndexPatternFieldMap), + // TODO: SQL dataSourceRef + }; +}; diff --git a/src/plugins/data/common/index.ts b/src/plugins/data/common/index.ts index 1eefb2383f8b..d7b7e56e2280 100644 --- a/src/plugins/data/common/index.ts +++ b/src/plugins/data/common/index.ts @@ -30,6 +30,7 @@ export * from './constants'; export * from './opensearch_query'; +export * from './data_frames'; export * from './field_formats'; export * from './field_mapping'; export * from './index_patterns'; diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts index 688605821097..5b2bf71433b9 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts @@ -208,6 +208,10 @@ export class IndexPatternsService { return this.savedObjectsCache; }; + getIndexPatternCache = () => { + return indexPatternCache; + }; + /** * Get default index pattern */ diff --git a/src/plugins/data/common/opensearch_query/opensearch_query/build_opensearch_query.ts b/src/plugins/data/common/opensearch_query/opensearch_query/build_opensearch_query.ts index 481eae12d121..5a00faf890f0 100644 --- a/src/plugins/data/common/opensearch_query/opensearch_query/build_opensearch_query.ts +++ b/src/plugins/data/common/opensearch_query/opensearch_query/build_opensearch_query.ts @@ -66,6 +66,17 @@ export function buildOpenSearchQuery( const validQueries = queries.filter((query) => has(query, 'query')); const queriesByLanguage = groupBy(validQueries, 'language'); + const unsupportedQueries = Object.keys(queriesByLanguage).filter( + (language) => language !== 'kuery' && language !== 'lucene' + ); + if (unsupportedQueries.length > 0) { + return { + type: 'unsupported', + queries, + filters, + }; + } + const kueryQuery = buildQueryFromKuery( indexPattern, queriesByLanguage.kuery, diff --git a/src/plugins/data/common/opensearch_query/opensearch_query/index.ts b/src/plugins/data/common/opensearch_query/opensearch_query/index.ts index ba3fe8817006..263bb1970a65 100644 --- a/src/plugins/data/common/opensearch_query/opensearch_query/index.ts +++ b/src/plugins/data/common/opensearch_query/opensearch_query/index.ts @@ -31,5 +31,6 @@ export { buildOpenSearchQuery, OpenSearchQueryConfig } from './build_opensearch_query'; export { buildQueryFromFilters } from './from_filters'; export { luceneStringToDsl } from './lucene_string_to_dsl'; +export { getIndexPatternFromSql, sqlStringToDsl } from './sql_string_to_dsl'; export { decorateQuery } from './decorate_query'; export { getOpenSearchQueryConfig } from './get_opensearch_query_config'; diff --git a/src/plugins/data/common/search/opensearch_search/types.ts b/src/plugins/data/common/search/opensearch_search/types.ts index 3b93177bf201..f90a3f1de245 100644 --- a/src/plugins/data/common/search/opensearch_search/types.ts +++ b/src/plugins/data/common/search/opensearch_search/types.ts @@ -57,6 +57,7 @@ export type ISearchRequestParams> = { export interface IOpenSearchSearchRequest extends IOpenSearchDashboardsSearchRequest { indexType?: string; + language?: string; dataSourceId?: string; } diff --git a/src/plugins/data/common/search/search_source/create_search_source.test.ts b/src/plugins/data/common/search/search_source/create_search_source.test.ts index 467ecec59f5a..68dfa7699e13 100644 --- a/src/plugins/data/common/search/search_source/create_search_source.test.ts +++ b/src/plugins/data/common/search/search_source/create_search_source.test.ts @@ -50,6 +50,11 @@ describe('createSearchSource', () => { callMsearch: jest.fn(), loadingCount$: new BehaviorSubject(0), }, + df: { + get: jest.fn().mockReturnValue({}), + set: jest.fn().mockReturnValue({}), + clear: jest.fn(), + }, }; indexPatternContractMock = ({ diff --git a/src/plugins/data/common/search/search_source/mocks.ts b/src/plugins/data/common/search/search_source/mocks.ts index 959d1aebfe53..3f69f14ec688 100644 --- a/src/plugins/data/common/search/search_source/mocks.ts +++ b/src/plugins/data/common/search/search_source/mocks.ts @@ -47,6 +47,8 @@ export const searchSourceInstanceMock: MockedKeys = { createChild: jest.fn().mockReturnThis(), setParent: jest.fn(), getParent: jest.fn().mockReturnThis(), + setDataFrame: jest.fn(), + getDataFrame: jest.fn().mockReturnThis(), fetch: jest.fn().mockResolvedValue({}), onRequestStart: jest.fn(), getSearchRequestBody: jest.fn(), @@ -54,6 +56,7 @@ export const searchSourceInstanceMock: MockedKeys = { history: [], getSerializedFields: jest.fn(), serialize: jest.fn(), + flatten: jest.fn().mockReturnThis(), }; export const searchSourceCommonMock: jest.Mocked = { diff --git a/src/plugins/data/common/search/search_source/search_source.test.ts b/src/plugins/data/common/search/search_source/search_source.test.ts index 92cc0682a136..09adc867d213 100644 --- a/src/plugins/data/common/search/search_source/search_source.test.ts +++ b/src/plugins/data/common/search/search_source/search_source.test.ts @@ -84,6 +84,11 @@ describe('SearchSource', () => { callMsearch: jest.fn(), loadingCount$: new BehaviorSubject(0), }, + df: { + get: jest.fn().mockReturnValue({}), + set: jest.fn().mockReturnValue({}), + clear: jest.fn(), + }, }; }); diff --git a/src/plugins/data/common/search/search_source/search_source.ts b/src/plugins/data/common/search/search_source/search_source.ts index abe6fa1b5cb4..586d84480acf 100644 --- a/src/plugins/data/common/search/search_source/search_source.ts +++ b/src/plugins/data/common/search/search_source/search_source.ts @@ -87,6 +87,7 @@ import { normalizeSortRequest } from './normalize_sort_request'; import { filterDocvalueFields } from './filter_docvalue_fields'; import { fieldWildcardFilter } from '../../../../opensearch_dashboards_utils/common'; import { IIndexPattern } from '../../index_patterns'; +import { IDataFrame, IDataFrameResponse, convertResult, dataFrameToSpec } from '../../data_frames'; import { IOpenSearchSearchRequest, IOpenSearchSearchResponse, ISearchOptions } from '../..'; import { IOpenSearchDashboardsSearchRequest, IOpenSearchDashboardsSearchResponse } from '../types'; import { ISearchSource, SearchSourceOptions, SearchSourceFields } from './types'; @@ -116,6 +117,7 @@ export const searchSourceRequiredUiSettings = [ UI_SETTINGS.QUERY_STRING_OPTIONS, UI_SETTINGS.SEARCH_INCLUDE_FROZEN, UI_SETTINGS.SORT_OPTIONS, + UI_SETTINGS.DATAFRAME_HYDRATION_STRATEGY, ]; export interface SearchSourceDependencies extends FetchHandlers { @@ -123,11 +125,18 @@ export interface SearchSourceDependencies extends FetchHandlers { // search options required here and returning a promise instead of observable. search: < SearchStrategyRequest extends IOpenSearchDashboardsSearchRequest = IOpenSearchSearchRequest, - SearchStrategyResponse extends IOpenSearchDashboardsSearchResponse = IOpenSearchSearchResponse + SearchStrategyResponse extends + | IOpenSearchDashboardsSearchResponse + | IDataFrameResponse = IOpenSearchSearchResponse >( request: SearchStrategyRequest, options: ISearchOptions ) => Promise; + df: { + get: () => IDataFrame | undefined; + set: (dataFrame: IDataFrame) => Promise; + clear: () => void; + }; } /** @public **/ @@ -267,6 +276,39 @@ export class SearchSource { return this.parent; } + /** + * Get the data frame of this SearchSource + * @return {undefined|IDataFrame} + */ + getDataFrame() { + return this.dependencies.df.get(); + } + + /** + * Set the data frame of this SearchSource + * @return {undefined|IDataFrame} + */ + async setDataFrame(dataFrame: IDataFrame | undefined) { + if (dataFrame) { + await this.dependencies.df.set(dataFrame); + } else { + this.dependencies.df.clear(); + } + return this.getDataFrame(); + } + + /** + * Get the data set of this SearchSource + * @return {undefined|IndexPattern} + */ + getDataSet() { + const df = this.dependencies.df.get(); + if (!df) { + return undefined; + } + return dataFrameToSpec(df); + } + /** * Fetch this source and reject the returned Promise on error * @@ -282,6 +324,8 @@ export class SearchSource { let response; if (getConfig(UI_SETTINGS.COURIER_BATCH_SEARCHES)) { response = await this.legacyFetch(searchRequest, options); + } else if (this.isUnsupportedRequest(searchRequest)) { + response = await this.fetchExternalSearch(searchRequest, options); } else { const indexPattern = this.getField('index'); searchRequest.dataSourceId = indexPattern?.dataSourceRef?.id; @@ -335,6 +379,11 @@ export class SearchSource { private fetchSearch(searchRequest: SearchRequest, options: ISearchOptions) { const { search, getConfig, onResponse } = this.dependencies; + if (this.getDataFrame()) { + delete searchRequest.body!.df; + this.setDataFrame(undefined); + } + const params = getSearchParamsFromRequest(searchRequest, { getConfig, }); @@ -342,7 +391,35 @@ export class SearchSource { return search( { params, indexType: searchRequest.indexType, dataSourceId: searchRequest.dataSourceId }, options - ).then(({ rawResponse }) => onResponse(searchRequest, rawResponse)); + ).then((response: any) => onResponse(searchRequest, response.rawResponse)); + } + + /** + * Run a non-native search using the search service + * @return {Promise>} + */ + private async fetchExternalSearch(searchRequest: SearchRequest, options: ISearchOptions) { + const { search, getConfig, onResponse } = this.dependencies; + + const params = getSearchParamsFromRequest(searchRequest, { + getConfig, + }); + + if (this.getDataFrame() && this.getDataFrame()!.name === searchRequest.index.title) { + params.body!.df = this.getDataFrame(); + } + + return search({ params }, options).then(async (response: any) => { + if (response.hasOwnProperty('type')) { + if ((response as IDataFrameResponse).type === 'data_frame') { + const dataFrameResponse = response as IDataFrameResponse; + await this.setDataFrame(dataFrameResponse.body as IDataFrame); + return onResponse(searchRequest, convertResult(response as IDataFrameResponse)); + } + // TODO: SQL else if data_frame_poll then poll for the data frame updating the df fields only + } + return onResponse(searchRequest, response.rawResponse); + }); } /** @@ -366,6 +443,10 @@ export class SearchSource { ); } + private isUnsupportedRequest(request: SearchRequest): boolean { + return request.body!.query.hasOwnProperty('type') && request.body!.query.type === 'unsupported'; + } + /** * Called by requests of this search source when they are started * @param options diff --git a/src/plugins/data/common/search/search_source/types.ts b/src/plugins/data/common/search/search_source/types.ts index 13b100aab1a7..9f3fd75e1ce9 100644 --- a/src/plugins/data/common/search/search_source/types.ts +++ b/src/plugins/data/common/search/search_source/types.ts @@ -29,7 +29,7 @@ */ import { NameList } from 'elasticsearch'; -import { Filter, IndexPattern, Query } from '../..'; +import { Filter, IDataFrame, IndexPattern, Query } from '../..'; import { SearchSource } from './search_source'; /** @@ -103,6 +103,7 @@ export interface SearchSourceFields { searchAfter?: OpenSearchQuerySearchAfter; timeout?: string; terminate_after?: number; + df?: IDataFrame; } export interface SearchSourceOptions { diff --git a/src/plugins/data/common/search/types.ts b/src/plugins/data/common/search/types.ts index e05c0adb46f0..d2bcdc0f4d05 100644 --- a/src/plugins/data/common/search/types.ts +++ b/src/plugins/data/common/search/types.ts @@ -34,6 +34,7 @@ import { IOpenSearchSearchResponse, ISearchOptions, } from '../../common/search'; +import { IDataFrameResponse } from '../data_frames'; export type ISearch = ( request: IOpenSearchDashboardsSearchRequest, @@ -42,7 +43,9 @@ export type ISearch = ( export type ISearchGeneric = < SearchStrategyRequest extends IOpenSearchDashboardsSearchRequest = IOpenSearchSearchRequest, - SearchStrategyResponse extends IOpenSearchDashboardsSearchResponse = IOpenSearchSearchResponse + SearchStrategyResponse extends + | IOpenSearchDashboardsSearchResponse + | IDataFrameResponse = IOpenSearchSearchResponse >( request: SearchStrategyRequest, options?: ISearchOptions diff --git a/src/plugins/data/common/types.ts b/src/plugins/data/common/types.ts index 361ba39edfec..fba5473624dc 100644 --- a/src/plugins/data/common/types.ts +++ b/src/plugins/data/common/types.ts @@ -31,6 +31,7 @@ export * from './query/types'; export * from './osd_field_types/types'; export * from './index_patterns/types'; +export * from './data_frames/types'; /** * If a service is being shared on both the client and the server, and diff --git a/src/plugins/data/public/data_sources/datasource_services/datasource_service.ts b/src/plugins/data/public/data_sources/datasource_services/datasource_service.ts index 8ead65633c7f..5a77d7b60d72 100644 --- a/src/plugins/data/public/data_sources/datasource_services/datasource_service.ts +++ b/src/plugins/data/public/data_sources/datasource_services/datasource_service.ts @@ -14,6 +14,7 @@ import { DataSource } from '../datasource/datasource'; export class DataSourceService { private static dataSourceService: DataSourceService; + // A record to store all registered data sources, using the data source name as the key. private dataSources: Record = {}; private dataSourcesSubject: BehaviorSubject>; diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 127e3dc720da..726facad30e5 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -434,6 +434,7 @@ export { IndexPatternSelectProps, QueryStringInput, QueryStringInputProps, + DataPublicPluginStartUi, } from './ui'; /** @@ -490,7 +491,6 @@ export { DataPublicPluginSetup, DataPublicPluginStart, IDataPluginServices, - DataPublicPluginStartUi, DataPublicPluginStartActions, } from './types'; diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index 179b6c0a8c83..edf73dbfbd0d 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -46,9 +46,9 @@ import { } from './types'; import { AutocompleteService } from './autocomplete'; import { SearchService } from './search/search_service'; +import { UiService } from './ui/ui_service'; import { FieldFormatsService } from './field_formats'; import { QueryService } from './query'; -import { createIndexPatternSelect } from './ui/index_pattern_select'; import { IndexPatternsService, onRedirectNoIndexPattern, @@ -63,9 +63,9 @@ import { setOverlays, setQueryService, setSearchService, + setUiService, setUiSettings, } from './services'; -import { createSearchBar } from './ui/search_bar/create_search_bar'; import { opensearchaggs } from './search/expressions'; import { SELECT_RANGE_TRIGGER, @@ -110,12 +110,14 @@ export class DataPublicPlugin > { private readonly autocomplete: AutocompleteService; private readonly searchService: SearchService; + private readonly uiService: UiService; private readonly fieldFormatsService: FieldFormatsService; private readonly queryService: QueryService; private readonly storage: IStorageWrapper; constructor(initializerContext: PluginInitializerContext) { this.searchService = new SearchService(initializerContext); + this.uiService = new UiService(initializerContext); this.queryService = new QueryService(); this.fieldFormatsService = new FieldFormatsService(); this.autocomplete = new AutocompleteService(initializerContext); @@ -159,13 +161,17 @@ export class DataPublicPlugin expressions, }); + const uiService = this.uiService.setup(core, {}); + return { + // TODO: SQL autocomplete: this.autocomplete.setup(core), search: searchService, fieldFormats: this.fieldFormatsService.setup(core), query: queryService, __enhance: (enhancements: DataPublicPluginEnhancements) => { - searchService.__enhance(enhancements.search); + if (enhancements.search) searchService.__enhance(enhancements.search); + if (enhancements.ui) uiService.__enhance(enhancements.ui); }, }; } @@ -234,21 +240,14 @@ export class DataPublicPlugin dataSourceFactory, }, }; - registerDefaultDatasource(dataServices); - const SearchBar = createSearchBar({ - core, - data: dataServices, - storage: this.storage, - }); + const uiService = this.uiService.start(core, { dataServices, storage: this.storage }); + setUiService(uiService); return { ...dataServices, - ui: { - IndexPatternSelect: createIndexPatternSelect(core.savedObjects.client), - SearchBar, - }, + ui: uiService, }; } @@ -256,5 +255,6 @@ export class DataPublicPlugin this.autocomplete.clearProviders(); this.queryService.stop(); this.searchService.stop(); + this.uiService.stop(); } } diff --git a/src/plugins/data/public/search/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor.ts index 153ac80a249c..adb02450d609 100644 --- a/src/plugins/data/public/search/search_interceptor.ts +++ b/src/plugins/data/public/search/search_interceptor.ts @@ -28,6 +28,8 @@ * under the License. */ +// TODO: SQL this file seems important + import { get, trimEnd, debounce } from 'lodash'; import { BehaviorSubject, throwError, timer, defer, from, Observable, NEVER } from 'rxjs'; import { catchError, finalize } from 'rxjs/operators'; diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index c73e7881faa6..77e58feac7de 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -57,6 +57,12 @@ import { getShardDelayBucketAgg, } from '../../common/search/aggs/buckets/shard_delay'; import { aggShardDelay } from '../../common/search/aggs/buckets/shard_delay_fn'; +import { + IDataFrame, + IDataFrameResponse, + createDataFrameCache, + dataFrameToSpec, +} from '../../common'; /** @internal */ export interface SearchServiceSetupDependencies { @@ -73,7 +79,9 @@ export interface SearchServiceStartDependencies { export class SearchService implements Plugin { private readonly aggsService = new AggsService(); private readonly searchSourceService = new SearchSourceService(); + private readonly dfCache = createDataFrameCache(); private searchInterceptor!: ISearchInterceptor; + private defaultSearchInterceptor!: ISearchInterceptor; private usageCollector?: SearchUsageCollector; constructor(private initializerContext: PluginInitializerContext) {} @@ -95,6 +103,7 @@ export class SearchService implements Plugin { startServices: getStartServices(), usageCollector: this.usageCollector!, }); + this.defaultSearchInterceptor = this.searchInterceptor; expressions.registerFunction(opensearchdsl); expressions.registerType(opensearchRawResponse); @@ -133,7 +142,9 @@ export class SearchService implements Plugin { getConfig: uiSettings.get.bind(uiSettings), search: < SearchStrategyRequest extends IOpenSearchDashboardsSearchRequest = IOpenSearchSearchRequest, - SearchStrategyResponse extends IOpenSearchDashboardsSearchResponse = IOpenSearchSearchResponse + SearchStrategyResponse extends + | IOpenSearchDashboardsSearchResponse + | IDataFrameResponse = IOpenSearchSearchResponse >( request: SearchStrategyRequest, options: ISearchOptions @@ -145,6 +156,19 @@ export class SearchService implements Plugin { callMsearch: getCallMsearch({ http }), loadingCount$, }, + df: { + get: () => this.dfCache.get(), + set: async (dataFrame: IDataFrame) => { + this.dfCache.set(dataFrame); + const dataSet = await indexPatterns.create(dataFrameToSpec(dataFrame), true); + indexPatterns.getIndexPatternCache().set(dataSet.title!, dataSet); + }, + clear: () => { + if (this.dfCache.get() === undefined) return; + indexPatterns.getIndexPatternCache().clear(this.dfCache.get()!.name!); + this.dfCache.clear(); + }, + }, }; return { @@ -154,6 +178,10 @@ export class SearchService implements Plugin { this.searchInterceptor.showError(e); }, searchSource: this.searchSourceService.start(indexPatterns, searchSourceDependencies), + __enhance: (enhancements: SearchEnhancements) => { + this.searchInterceptor = enhancements.searchInterceptor; + }, + getDefaultSearchInterceptor: () => this.defaultSearchInterceptor, }; } diff --git a/src/plugins/data/public/search/types.ts b/src/plugins/data/public/search/types.ts index 8a0d82b855c8..679c6624bad4 100644 --- a/src/plugins/data/public/search/types.ts +++ b/src/plugins/data/public/search/types.ts @@ -78,6 +78,8 @@ export interface ISearchStart { * {@link ISearchStartSearchSource} */ searchSource: ISearchStartSearchSource; + __enhance: (enhancements: SearchEnhancements) => void; + getDefaultSearchInterceptor: () => ISearchInterceptor; } export { SEARCH_EVENT_TYPE } from './collectors'; diff --git a/src/plugins/data/public/services.ts b/src/plugins/data/public/services.ts index 3bcc9d69a9a4..d75dab2986ca 100644 --- a/src/plugins/data/public/services.ts +++ b/src/plugins/data/public/services.ts @@ -59,3 +59,5 @@ export const [getQueryService, setQueryService] = createGetterSetter< export const [getSearchService, setSearchService] = createGetterSetter< DataPublicPluginStart['search'] >('Search'); + +export const [getUiService, setUiService] = createGetterSetter('Ui'); diff --git a/src/plugins/data/public/types.ts b/src/plugins/data/public/types.ts index 5870ea7def8e..f3ff3f4968d6 100644 --- a/src/plugins/data/public/types.ts +++ b/src/plugins/data/public/types.ts @@ -28,7 +28,6 @@ * under the License. */ -import React from 'react'; import { CoreStart } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/opensearch_dashboards_utils/public'; import { ExpressionsSetup } from 'src/plugins/expressions/public'; @@ -39,12 +38,14 @@ import { createFiltersFromRangeSelectAction, createFiltersFromValueClickAction } import { ISearchSetup, ISearchStart, SearchEnhancements } from './search'; import { QuerySetup, QueryStart } from './query'; import { IndexPatternsContract } from './index_patterns'; -import { IndexPatternSelectProps, StatefulSearchBarProps } from './ui'; import { UsageCollectionSetup } from '../../usage_collection/public'; import { DataSourceStart } from './data_sources/datasource_services/types'; +import { UiEnhancements } from './ui'; +import { DataPublicPluginStartUi } from './ui/types'; export interface DataPublicPluginEnhancements { - search: SearchEnhancements; + search?: SearchEnhancements; + ui?: UiEnhancements; } export interface DataSetupDependencies { @@ -71,14 +72,6 @@ export interface DataPublicPluginSetup { __enhance: (enhancements: DataPublicPluginEnhancements) => void; } -/** - * Data plugin prewired UI components - */ -export interface DataPublicPluginStartUi { - IndexPatternSelect: React.ComponentType; - SearchBar: React.ComponentType; -} - /** * utilities to generate filters from action context */ diff --git a/src/plugins/data/public/ui/index.ts b/src/plugins/data/public/ui/index.ts index c618df1783b5..8d92ef1f6e3c 100644 --- a/src/plugins/data/public/ui/index.ts +++ b/src/plugins/data/public/ui/index.ts @@ -28,6 +28,7 @@ * under the License. */ +export { UiEnhancements, DataPublicPluginStartUi } from './types'; export { IndexPatternSelectProps } from './index_pattern_select'; export { FilterLabel } from './filter_bar'; export { QueryStringInput, QueryStringInputProps } from './query_string_input'; diff --git a/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx b/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx index 9d2d98591bc9..869e122478f3 100644 --- a/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx +++ b/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx @@ -110,6 +110,18 @@ export default class IndexPatternSelect extends Component { + // if (this.props.df) { + // const df = this.props.df.getDataFrame(); + // if (df && this.isMounted) { + // this.setState({ + // selectedIndexPattern: { + // value: df.name, + // label: df.name, + // }, + // }); + // return; + // } + // } if (!indexPatternId) { this.setState({ selectedIndexPattern: undefined, diff --git a/src/plugins/data/public/ui/query_string_input/_index.scss b/src/plugins/data/public/ui/query_string_input/_index.scss index 8686490016c5..f21b9cbb4327 100644 --- a/src/plugins/data/public/ui/query_string_input/_index.scss +++ b/src/plugins/data/public/ui/query_string_input/_index.scss @@ -1 +1,2 @@ @import "./query_bar"; +@import "./language_switcher" diff --git a/src/plugins/data/public/ui/query_string_input/_language_switcher.scss b/src/plugins/data/public/ui/query_string_input/_language_switcher.scss new file mode 100644 index 000000000000..9ee71b9fc7e2 --- /dev/null +++ b/src/plugins/data/public/ui/query_string_input/_language_switcher.scss @@ -0,0 +1,4 @@ +// From: https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5623 +.languageSwitcher { + max-width: 150px; +} diff --git a/src/plugins/data/public/ui/query_string_input/language_switcher.tsx b/src/plugins/data/public/ui/query_string_input/language_switcher.tsx index 816c21bc0848..43c76ffb010e 100644 --- a/src/plugins/data/public/ui/query_string_input/language_switcher.tsx +++ b/src/plugins/data/public/ui/query_string_input/language_switcher.tsx @@ -28,21 +28,10 @@ * under the License. */ -import { - EuiButtonEmpty, - EuiForm, - EuiFormRow, - EuiLink, - EuiPopover, - EuiPopoverTitle, - EuiSpacer, - EuiSwitch, - EuiText, - PopoverAnchorPosition, -} from '@elastic/eui'; -import { FormattedMessage } from '@osd/i18n/react'; -import React, { useState } from 'react'; -import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; +import { EuiComboBox, EuiComboBoxOptionOption, PopoverAnchorPosition } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; +import React from 'react'; +import { getSearchService, getUiService } from '../../services'; interface Props { language: string; @@ -50,94 +39,70 @@ interface Props { anchorPosition?: PopoverAnchorPosition; } +function mapExternalLanguageToOptions(language: string) { + return { + label: language, + value: language, + }; +} + export function QueryLanguageSwitcher(props: Props) { - const osdDQLDocs = useOpenSearchDashboards().services.docLinks?.links.opensearchDashboards.dql - .base; - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const luceneLabel = ( - - ); - const dqlLabel = ( - - ); - const dqlFullName = ( - - ); + const luceneLabel = i18n.translate('data.query.queryBar.luceneLanguageName', { + defaultMessage: 'Lucene', + }); + const dqlLabel = i18n.translate('data.query.queryBar.dqlLanguageName', { + defaultMessage: 'DQL', + }); + + const languageOptions: EuiComboBoxOptionOption[] = [ + { + label: luceneLabel, + value: 'lucene', + }, + { + label: dqlLabel, + value: 'dql', + }, + ]; - const button = ( - setIsPopoverOpen(!isPopoverOpen)} - className="euiFormControlLayout__append dqlQueryBar__languageSwitcherButton" - data-test-subj={'switchQueryLanguageButton'} - > - {props.language === 'lucene' ? luceneLabel : dqlLabel} - + const queryEnhancements = getUiService().queryEnhancements; + queryEnhancements.forEach((enhancement) => + languageOptions.push(mapExternalLanguageToOptions(enhancement.language)) ); - return ( - setIsPopoverOpen(false)} - repositionOnScroll - > - - - -
- -

- - {dqlFullName} - - ), - }} - /> -

-
+ const selectedLanguage = { + label: props.language === 'kuery' ? 'DQL' : props.language, + }; + + const setSearchEnhance = (queryLanguage: string) => { + const queryEnhancement = queryEnhancements.get(queryLanguage); + const searchService = getSearchService(); + + searchService.__enhance({ + searchInterceptor: queryEnhancement + ? queryEnhancement.search + : searchService.getDefaultSearchInterceptor(), + }); + }; - + const handleLanguageChange = (newLanguage: EuiComboBoxOptionOption[]) => { + const queryLanguage = newLanguage[0].label === 'DQL' ? 'kuery' : newLanguage[0].label; + props.onSelectLanguage(queryLanguage); + setSearchEnhance(queryLanguage); + }; - - - - ) : ( - - ) - } - checked={props.language === 'kuery'} - onChange={() => { - const newLanguage = props.language === 'lucene' ? 'kuery' : 'lucene'; - props.onSelectLanguage(newLanguage); - }} - data-test-subj="languageToggle" - /> - - -
-
+ setSearchEnhance(props.language); + + return ( + ); } diff --git a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx index 8c509d573e1b..cd7d7b02fb4d 100644 --- a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx @@ -56,12 +56,14 @@ import QueryStringInputUI from './query_string_input'; import { doesKueryExpressionHaveLuceneSyntaxError, UI_SETTINGS } from '../../../common'; import { PersistedLog, getQueryLog } from '../../query'; import { NoDataPopover } from './no_data_popover'; +import { QueryEnhancement } from '../types'; const QueryStringInput = withOpenSearchDashboards(QueryStringInputUI); // @internal export interface QueryBarTopRowProps { query?: Query; + queryEnhancements?: Map; onSubmit: (payload: { dateRange: TimeRange; query?: Query }) => void; onChange: (payload: { dateRange: TimeRange; query?: Query }) => void; onRefresh?: (payload: { dateRange: TimeRange }) => void; @@ -97,6 +99,11 @@ export default function QueryBarTopRow(props: QueryBarTopRowProps) { const osdDQLDocs: string = docLinks!.links.opensearchDashboards.dql.base; const queryLanguage = props.query && props.query.language; + const queryUiEnhancement = + (queryLanguage && + props.queryEnhancements && + props.queryEnhancements.get(queryLanguage)?.searchBar) || + null; const persistedLog: PersistedLog | undefined = React.useMemo( () => queryLanguage && uiSettings && storage && appName @@ -205,6 +212,7 @@ export default function QueryBarTopRow(props: QueryBarTopRowProps) { indexPatterns={props.indexPatterns!} prepend={props.prepend} query={props.query!} + queryEnhancements={props.queryEnhancements} screenTitle={props.screenTitle} onChange={onQueryChange} onChangeQueryInputFocus={onChangeQueryInputFocus} @@ -233,10 +241,15 @@ export default function QueryBarTopRow(props: QueryBarTopRowProps) { } function shouldRenderDatePicker(): boolean { - return Boolean(props.showDatePicker || props.showAutoRefreshOnly); + return Boolean( + (props.showDatePicker && (queryUiEnhancement?.showDatePicker ?? true)) ?? + (props.showAutoRefreshOnly && (queryUiEnhancement?.showAutoRefreshOnly ?? true)) + ); } function shouldRenderQueryInput(): boolean { + // TODO: SQL probably can modify to not care about index patterns + // TODO: call queryUiEnhancement?.showQueryInput return Boolean(props.showQueryInput && props.indexPatterns && props.query && storage); } diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx index 5d071748700c..c7e6ff89b390 100644 --- a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx @@ -59,10 +59,12 @@ import { QueryLanguageSwitcher } from './language_switcher'; import { PersistedLog, getQueryLog, matchPairs, toUser, fromUser } from '../../query'; import { SuggestionsListSize } from '../typeahead/suggestions_component'; import { SuggestionsComponent } from '..'; +import { QueryEnhancement } from '../types'; export interface QueryStringInputProps { indexPatterns: Array; query: Query; + queryEnhancements?: Map; disableAutoFocus?: boolean; screenTitle?: string; prepend?: any; @@ -108,6 +110,7 @@ const KEY_CODES = { }; // Needed for React.lazy +// TODO: SQL export this and let people extended this // eslint-disable-next-line import/no-default-export export default class QueryStringInputUI extends Component { public state: State = { @@ -129,10 +132,25 @@ export default class QueryStringInputUI extends Component { private componentIsUnmounting = false; private queryBarInputDivRefInstance: RefObject = createRef(); + private getQueryStringInitialValue = (language: string) => { + const input = this.props.queryEnhancements?.get(language)?.searchBar?.queryStringInput; + // TODO: SQL replace with data_source length + if (!input || !input.initialValue || this.state.indexPatterns.length === 0) return ''; + const defaultDataSource = this.state.indexPatterns[0]; + return input.initialValue.replace( + '', + typeof defaultDataSource === 'string' ? defaultDataSource : defaultDataSource.title + ); + }; + private getQueryString = () => { + if (!this.props.query.query) { + return this.getQueryStringInitialValue(this.props.query.language); + } return toUser(this.props.query.query); }; + // TODO: SQL don't do this here? || Fetch data sources private fetchIndexPatterns = async () => { const stringPatterns = this.props.indexPatterns.filter( (indexPattern) => typeof indexPattern === 'string' @@ -457,6 +475,8 @@ export default class QueryStringInputUI extends Component { } }; + // TODO: SQL consider moving language select language of setting search source here + // Should consider creating search source then private onSelectLanguage = (language: string) => { // Send telemetry info every time the user opts in or out of kuery // As a result it is important this function only ever gets called in the @@ -466,9 +486,18 @@ export default class QueryStringInputUI extends Component { }); this.services.storage.set('opensearchDashboards.userQueryLanguage', language); - - const newQuery = { query: '', language }; + // TODO: SQL proof you can modify search bar, consider remove the extra stuff here + // Will work on adding more to search bar of UI service + // const input = this.props.queryEnhancement?.ui?.queryStringInput; + // const submitOnLanguageSelect = input?.submitOnLanguageSelect ?? true; + const newQuery = { + query: this.getQueryStringInitialValue(language), + language, + }; this.onChange(newQuery); + // if (submitOnLanguageSelect) { + // this.onSubmit(newQuery); + // } this.onSubmit(newQuery); }; diff --git a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx index c739b955ff19..a480a7b34d68 100644 --- a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx @@ -41,11 +41,13 @@ import { useSavedQuery } from './lib/use_saved_query'; import { DataPublicPluginStart } from '../../types'; import { Filter, Query, TimeRange } from '../../../common'; import { useQueryStringManager } from './lib/use_query_string_manager'; +import { QueryEnhancement } from '../types'; interface StatefulSearchBarDeps { core: CoreStart; data: Omit; storage: IStorageWrapper; + queryEnhancements: Map; } export type StatefulSearchBarProps = SearchBarOwnProps & { @@ -130,11 +132,12 @@ const overrideDefaultBehaviors = (props: StatefulSearchBarProps) => { return props.useDefaultBehaviors ? {} : props; }; -export function createSearchBar({ core, storage, data }: StatefulSearchBarDeps) { +export function createSearchBar({ core, storage, data, queryEnhancements }: StatefulSearchBarDeps) { // App name should come from the core application service. // Until it's available, we'll ask the user to provide it for the pre-wired component. return (props: StatefulSearchBarProps) => { const { useDefaultBehaviors } = props; + // Handle queries const onQuerySubmitRef = useRef(props.onQuerySubmit); @@ -148,6 +151,7 @@ export function createSearchBar({ core, storage, data }: StatefulSearchBarDeps) query: props.query, queryStringManager: data.query.queryString, }); + const { timeRange, refreshInterval } = useTimefilter({ dateRangeFrom: props.dateRangeFrom, dateRangeTo: props.dateRangeTo, @@ -201,6 +205,7 @@ export function createSearchBar({ core, storage, data }: StatefulSearchBarDeps) isRefreshPaused={refreshInterval.pause} filters={filters} query={query} + queryEnhancements={queryEnhancements} onFiltersUpdated={defaultFiltersUpdated(data.query)} onRefreshChange={defaultOnRefreshChange(data.query)} savedQuery={savedQuery} diff --git a/src/plugins/data/public/ui/search_bar/search_bar.tsx b/src/plugins/data/public/ui/search_bar/search_bar.tsx index b05b18b6d64e..04550f81fa94 100644 --- a/src/plugins/data/public/ui/search_bar/search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/search_bar.tsx @@ -47,6 +47,7 @@ import { TimeRange, Query, Filter, IIndexPattern } from '../../../common'; import { FilterBar } from '../filter_bar/filter_bar'; import { SavedQueryMeta, SaveQueryForm } from '../saved_query_form'; import { SavedQueryManagementComponent } from '../saved_query_management'; +import { QueryEnhancement } from '../types'; interface SearchBarInjectedDeps { opensearchDashboards: OpenSearchDashboardsReactContextValue; @@ -78,6 +79,7 @@ export interface SearchBarOwnProps { dateRangeTo?: string; // Query bar - should be in SearchBarInjectedDeps query?: Query; + queryEnhancements: Map; // Show when user has privileges to save showSaveQuery?: boolean; savedQuery?: SavedQuery; @@ -96,6 +98,7 @@ export interface SearchBarOwnProps { export type SearchBarProps = SearchBarOwnProps & SearchBarInjectedDeps; +// TODO: SQL: include query enhancement in state in case make adding data sources at runtime? interface State { isFiltersVisible: boolean; showSaveQueryModal: boolean; @@ -202,6 +205,7 @@ class SearchBarUI extends Component { }; private shouldRenderQueryBar() { + // TODO: SQL handle no index patterns? const showDatePicker = this.props.showDatePicker || this.props.showAutoRefreshOnly; const showQueryInput = this.props.showQueryInput && this.props.indexPatterns && this.state.query; @@ -209,11 +213,14 @@ class SearchBarUI extends Component { } private shouldRenderFilterBar() { + // TODO: SQL handle no index patterns? return ( this.props.showFilterBar && this.props.filters && this.props.indexPatterns && - compact(this.props.indexPatterns).length > 0 + compact(this.props.indexPatterns).length > 0 && + (this.props.queryEnhancements.get(this.state.query?.language!)?.searchBar?.showFilterBar ?? + true) ); } @@ -393,9 +400,11 @@ class SearchBarUI extends Component { let queryBar; if (this.shouldRenderQueryBar()) { + // TODO: SQL make this default query bar top row but this.props.queryEnhancements.get(language) can pass a component queryBar = ( ; + searchBar?: { + showQueryInput?: boolean; + showFilterBar?: boolean; + showDatePicker?: boolean; + showAutoRefreshOnly?: boolean; + queryStringInput?: { + // will replace '' with the data source name + initialValue?: string; + }; + }; +} + +export interface UiEnhancements { + query?: QueryEnhancement; +} + +/** + * Data plugin prewired UI components + */ +export interface DataPublicPluginStartUi { + queryEnhancements: Map; + IndexPatternSelect: React.ComponentType; + SearchBar: React.ComponentType; +} diff --git a/src/plugins/data/public/ui/ui_service.ts b/src/plugins/data/public/ui/ui_service.ts new file mode 100644 index 000000000000..59ab47e8d607 --- /dev/null +++ b/src/plugins/data/public/ui/ui_service.ts @@ -0,0 +1,59 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Plugin, CoreSetup, CoreStart, PluginInitializerContext } from 'src/core/public'; +import { DataPublicPluginStartUi, QueryEnhancement, UiEnhancements } from './types'; + +import { ConfigSchema } from '../../config'; +import { createIndexPatternSelect } from './index_pattern_select'; +import { createSearchBar } from './search_bar/create_search_bar'; +import { DataPublicPluginStart } from '../types'; +import { IStorageWrapper } from '../../../opensearch_dashboards_utils/public'; + +/** @internal */ +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface UiServiceSetupDependencies {} + +/** @internal */ +export interface UiServiceStartDependencies { + dataServices: Omit; + storage: IStorageWrapper; +} + +export class UiService implements Plugin { + constructor(private _: PluginInitializerContext) {} + private queryEnhancements: Map = new Map(); + + public setup( + { http, getStartServices, notifications, uiSettings }: CoreSetup, + {}: UiServiceSetupDependencies + ) { + return { + __enhance: (enhancements?: UiEnhancements) => { + if (!enhancements) return; + if (enhancements.query && enhancements.query.language) { + this.queryEnhancements.set(enhancements.query.language, enhancements.query); + } + }, + }; + } + + public start(core: CoreStart, { dataServices, storage }: UiServiceStartDependencies) { + const SearchBar = createSearchBar({ + core, + data: dataServices, + storage, + queryEnhancements: this.queryEnhancements, + }); + + return { + queryEnhancements: this.queryEnhancements, + IndexPatternSelect: createIndexPatternSelect(core.savedObjects.client), + SearchBar, + }; + } + + public stop() {} +} diff --git a/src/plugins/data/server/search/opensearch_search/get_default_search_params.ts b/src/plugins/data/server/search/opensearch_search/get_default_search_params.ts index d7cbd48a6507..172acf9f0e2e 100644 --- a/src/plugins/data/server/search/opensearch_search/get_default_search_params.ts +++ b/src/plugins/data/server/search/opensearch_search/get_default_search_params.ts @@ -45,10 +45,14 @@ export async function getDefaultSearchParams(uiSettingsClient: IUiSettingsClient const maxConcurrentShardRequests = await uiSettingsClient.get( UI_SETTINGS.COURIER_MAX_CONCURRENT_SHARD_REQUESTS ); + const dataFrameHydrationStrategy = await uiSettingsClient.get( + UI_SETTINGS.DATAFRAME_HYDRATION_STRATEGY + ); return { maxConcurrentShardRequests: maxConcurrentShardRequests > 0 ? maxConcurrentShardRequests : undefined, ignoreThrottled, + dataFrameHydrationStrategy, ignoreUnavailable: true, // Don't fail if the index/indices don't exist trackTotalHits: true, }; diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index b955596922a0..b2173e00085d 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -73,6 +73,12 @@ import { } from '../../common/search/aggs/buckets/shard_delay'; import { aggShardDelay } from '../../common/search/aggs/buckets/shard_delay_fn'; import { ConfigSchema } from '../../config'; +import { + IDataFrame, + IDataFrameResponse, + createDataFrameCache, + dataFrameToSpec, +} from '../../common'; type StrategyMap = Record>; @@ -98,6 +104,7 @@ export interface SearchRouteDependencies { export class SearchService implements Plugin { private readonly aggsService = new AggsService(); private readonly searchSourceService = new SearchSourceService(); + private readonly dfCache = createDataFrameCache(); private defaultSearchStrategyName: string = OPENSEARCH_SEARCH_STRATEGY; private searchStrategies: StrategyMap = {}; @@ -166,7 +173,8 @@ export class SearchService implements Plugin { }); return { - __enhance: (enhancements: SearchEnhancements) => { + __enhance: (enhancements?: SearchEnhancements) => { + if (!enhancements) return; if (this.searchStrategies.hasOwnProperty(enhancements.defaultStrategy)) { this.defaultSearchStrategyName = enhancements.defaultStrategy; } @@ -237,6 +245,19 @@ export class SearchService implements Plugin { }), loadingCount$: new BehaviorSubject(0), }, + df: { + get: () => this.dfCache.get(), + set: async (dataFrame: IDataFrame) => { + this.dfCache.set(dataFrame); + const dataSet = await scopedIndexPatterns.create(dataFrameToSpec(dataFrame), true); + scopedIndexPatterns.getIndexPatternCache().set(dataSet.title!, dataSet); + }, + clear: () => { + if (this.dfCache.get() === undefined) return; + scopedIndexPatterns.getIndexPatternCache().clear(this.dfCache.get()!.name!); + this.dfCache.clear(); + }, + }, }; return this.searchSourceService.start(scopedIndexPatterns, searchSourceDependencies); @@ -251,7 +272,9 @@ export class SearchService implements Plugin { private registerSearchStrategy = < SearchStrategyRequest extends IOpenSearchDashboardsSearchRequest = IOpenSearchSearchRequest, - SearchStrategyResponse extends IOpenSearchDashboardsSearchResponse = IOpenSearchSearchResponse + SearchStrategyResponse extends + | IOpenSearchDashboardsSearchResponse + | IDataFrameResponse = IOpenSearchSearchResponse >( name: string, strategy: ISearchStrategy @@ -261,7 +284,9 @@ export class SearchService implements Plugin { private search = < SearchStrategyRequest extends IOpenSearchDashboardsSearchRequest = IOpenSearchSearchRequest, - SearchStrategyResponse extends IOpenSearchDashboardsSearchResponse = IOpenSearchSearchResponse + SearchStrategyResponse extends + | IOpenSearchDashboardsSearchResponse + | IDataFrameResponse = IOpenSearchSearchResponse >( context: RequestHandlerContext, searchRequest: SearchStrategyRequest, @@ -274,7 +299,9 @@ export class SearchService implements Plugin { private getSearchStrategy = < SearchStrategyRequest extends IOpenSearchDashboardsSearchRequest = IOpenSearchSearchRequest, - SearchStrategyResponse extends IOpenSearchDashboardsSearchResponse = IOpenSearchSearchResponse + SearchStrategyResponse extends + | IOpenSearchDashboardsSearchResponse + | IDataFrameResponse = IOpenSearchSearchResponse >( name: string ): ISearchStrategy => { diff --git a/src/plugins/data/server/search/types.ts b/src/plugins/data/server/search/types.ts index 75f21d39c0bf..6927d1289673 100644 --- a/src/plugins/data/server/search/types.ts +++ b/src/plugins/data/server/search/types.ts @@ -38,6 +38,7 @@ import { import { AggsSetup, AggsStart } from './aggs'; import { SearchUsage } from './collectors'; import { IOpenSearchSearchRequest, IOpenSearchSearchResponse } from './opensearch_search'; +import { IDataFrameResponse } from '../../common'; export interface SearchEnhancements { defaultStrategy: string; @@ -51,7 +52,9 @@ export interface ISearchSetup { */ registerSearchStrategy: < SearchStrategyRequest extends IOpenSearchDashboardsSearchRequest = IOpenSearchSearchRequest, - SearchStrategyResponse extends IOpenSearchDashboardsSearchResponse = IOpenSearchSearchResponse + SearchStrategyResponse extends + | IOpenSearchDashboardsSearchResponse + | IDataFrameResponse = IOpenSearchSearchResponse >( name: string, strategy: ISearchStrategy @@ -96,7 +99,9 @@ export interface ISearchStart< */ export interface ISearchStrategy< SearchStrategyRequest extends IOpenSearchDashboardsSearchRequest = IOpenSearchSearchRequest, - SearchStrategyResponse extends IOpenSearchDashboardsSearchResponse = IOpenSearchSearchResponse + SearchStrategyResponse extends + | IOpenSearchDashboardsSearchResponse + | IDataFrameResponse = IOpenSearchSearchResponse > { search: ( context: RequestHandlerContext, diff --git a/src/plugins/data/server/ui_settings.ts b/src/plugins/data/server/ui_settings.ts index 77f4afd11887..76a46eb148b5 100644 --- a/src/plugins/data/server/ui_settings.ts +++ b/src/plugins/data/server/ui_settings.ts @@ -55,6 +55,21 @@ const requestPreferenceOptionLabels = { }), }; +const dataFrameHydrationStrategyOptionLabels = { + perSource: i18n.translate('data.advancedSettings.dataFrameHydrationStrategyPerSourceText', { + defaultMessage: 'On data source change', + }), + perQuery: i18n.translate('data.advancedSettings.dataFrameHydrationStrategyPerQueryText', { + defaultMessage: 'Per query', + }), + perResponse: i18n.translate('data.advancedSettings.dataFrameHydrationStrategyPerResponseText', { + defaultMessage: 'Per response', + }), + advanced: i18n.translate('data.advancedSettings.dataFrameHydrationStrategyAdvancedText', { + defaultMessage: 'Advanced', + }), +}; + // We add the `en` key manually here, since that's not a real numeral locale, but the // default fallback in case the locale is not found. const numeralLanguageIds = [ @@ -690,5 +705,36 @@ export function getUiSettings(): Record> { }), schema: schema.boolean(), }, + [UI_SETTINGS.DATAFRAME_HYDRATION_STRATEGY]: { + name: i18n.translate('data.advancedSettings.dataFrameHydrationStrategyTitle', { + defaultMessage: 'Data frame hydration strategy', + }), + value: 'perSource', + // TODO: SQL does PPL/SQL support this ? Where if the schema contains all fields but nulls out the values that are not present? + options: ['perSource', 'perQuery'], + optionLabels: dataFrameHydrationStrategyOptionLabels, + type: 'select', + description: i18n.translate('data.advancedSettings.dataFrameHydrationStrategyText', { + defaultMessage: `Allows you to set how often the data frame schema is updated. +
    +
  • {perSource}: hydrates the schema when the data source changes. + For example, any time the index pattern is change the data frame schema is hydrated.
  • +
  • {perQuery}: hydrates the schema per query to the data source. + Could be expensive, but ensures the schema of the data frame fits the result set.
  • +
  • {perResponse}: hydrates the schema if the data source returns a schema. + Not Implemented.
  • +
  • {advanced}: hydrates the schema in intervals. If the schema hasn't changed the interval increases. + If the schema has changed the interval resets. Not Implemented.
  • +
`, + values: { + perSource: dataFrameHydrationStrategyOptionLabels.perSource, + perQuery: dataFrameHydrationStrategyOptionLabels.perQuery, + perResponse: dataFrameHydrationStrategyOptionLabels.perResponse, + advanced: dataFrameHydrationStrategyOptionLabels.advanced, + }, + }), + category: ['search'], + schema: schema.string(), + }, }; } diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field.tsx index b7aa9cf7b36e..0436804501f8 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field.tsx @@ -46,6 +46,7 @@ import { IndexPatternField, IndexPattern } from '../../../../../data/public'; import { shortenDottedString } from '../../helpers'; import { getFieldTypeName } from './lib/get_field_type_name'; import './discover_field.scss'; +import { DiscoverFieldEdit } from './discover_field_edit'; export interface DiscoverFieldProps { /** @@ -113,9 +114,14 @@ export const DiscoverField = ({ defaultMessage: 'View {field} summary', values: { field: field.name }, }); + const editLabelAria = i18n.translate('discover.fieldChooser.discoverField.editButtonAriaLabel', { + defaultMessage: 'Edit {field}', + values: { field: field.name }, + }); const isSourceField = field.name === '_source'; const [infoIsOpen, setOpen] = useState(false); + const [editIsOpen, setEditIsOpen] = useState(false); const toggleDisplay = (f: IndexPatternField) => { if (selected) { @@ -244,6 +250,43 @@ export const DiscoverField = ({ )} + {!isSourceField && indexPattern.id === 'data_frame' && ( + + setEditIsOpen(false)} + anchorPosition="rightUp" + button={ + setEditIsOpen((state) => !state)} + aria-label={editLabelAria} + data-test-subj={`field-${field.name}-showEdit`} + className="dscSidebarField__actionButton" + /> + } + panelClassName="dscSidebarItem__fieldPopoverPanel" + > + + {' '} + {i18n.translate('discover.fieldChooser.discoverField.editFieldLabel', { + defaultMessage: 'Edit field', + })} + + {editIsOpen && ( + + )} + + + )} {!isSourceField && {actionButton}} ); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_edit.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_edit.tsx new file mode 100644 index 000000000000..77aeb425a6c0 --- /dev/null +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_edit.tsx @@ -0,0 +1,56 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState } from 'react'; +import { EuiText, EuiSelect } from '@elastic/eui'; +import { FieldDetails } from './types'; +import { IndexPatternField, IndexPattern, OSD_FIELD_TYPES } from '../../../../../data/public'; + +interface DiscoverFieldEditProps { + columns: string[]; + details: FieldDetails; + field: IndexPatternField; + indexPattern: IndexPattern; +} + +export function DiscoverFieldEdit({ + columns, + details, + field, + indexPattern, +}: DiscoverFieldEditProps) { + const options = Object.keys(OSD_FIELD_TYPES).map((fieldType) => ({ + value: fieldType.toLowerCase(), + text: fieldType, + })); + options.unshift({ value: 'unknown', text: 'UNKNOWN' }); + const [value, setValue] = useState(field.type.toLowerCase()); + + return ( + <> +
+ {details.error && ( + + {details.error} + + )} + + {!details.error && ( +
+ { + setValue(e.target.value); + // TODO: Handle the selected type + }} + /> +
+ )} +
+ + ); +} diff --git a/src/plugins/discover/public/application/view_components/panel/index.tsx b/src/plugins/discover/public/application/view_components/panel/index.tsx index 6b4cd2a87c91..8a6920ace3e7 100644 --- a/src/plugins/discover/public/application/view_components/panel/index.tsx +++ b/src/plugins/discover/public/application/view_components/panel/index.tsx @@ -92,8 +92,12 @@ export default function DiscoverPanel(props: ViewProps) { fieldCounts={fetchState.fieldCounts || {}} hits={fetchState.rows || []} onAddField={(fieldName, index) => { - if (indexPattern && capabilities.discover?.save) { - popularizeField(indexPattern, fieldName, indexPatterns); + const dataSet = + fetchState.title && fetchState.title !== indexPattern?.title + ? indexPatterns.getIndexPatternCache().get(fetchState.title!) + : indexPattern; + if (dataSet && capabilities.discover?.save) { + popularizeField(dataSet!, fieldName, indexPatterns); } dispatch( @@ -104,8 +108,12 @@ export default function DiscoverPanel(props: ViewProps) { ); }} onRemoveField={(fieldName) => { - if (indexPattern && capabilities.discover?.save) { - popularizeField(indexPattern, fieldName, indexPatterns); + const dataSet = + fetchState.title && fetchState.title !== indexPattern?.title + ? indexPatterns.getIndexPatternCache().get(fetchState.title!) + : indexPattern; + if (dataSet && capabilities.discover?.save) { + popularizeField(dataSet!, fieldName, indexPatterns); } dispatch(removeColumn(fieldName)); @@ -118,7 +126,11 @@ export default function DiscoverPanel(props: ViewProps) { }) ); }} - selectedIndexPattern={indexPattern} + selectedIndexPattern={ + fetchState.title && fetchState.title !== indexPattern?.title + ? indexPatterns.getIndexPatternCache().get(fetchState.title!) + : indexPattern + } onAddFilter={onAddFilter} /> ); diff --git a/src/plugins/discover/public/application/view_components/utils/update_search_source.ts b/src/plugins/discover/public/application/view_components/utils/update_search_source.ts index 1404773eb9d4..b8a11e012d34 100644 --- a/src/plugins/discover/public/application/view_components/utils/update_search_source.ts +++ b/src/plugins/discover/public/application/view_components/utils/update_search_source.ts @@ -30,9 +30,19 @@ export const updateSearchSource = async ({ histogramConfigs, }: Props) => { const { uiSettings, data } = services; + let dataSet = indexPattern; + if ( + searchSource && + searchSource.getDataFrame() && + dataSet.title !== searchSource.getDataFrame()?.name + ) { + dataSet = await data.indexPatterns.get(searchSource.getDataFrame()?.name!); + searchSource.setField('index', dataSet); + } + const sortForSearchSource = getSortForSearchSource( sort, - indexPattern, + dataSet, uiSettings.get(SORT_DEFAULT_ORDER_SETTING) ); const size = uiSettings.get(SAMPLE_SIZE_SETTING); @@ -43,18 +53,18 @@ export const updateSearchSource = async ({ // searchSource which applies time range const timeRangeSearchSource = await data.search.searchSource.create(); const { isDefault } = indexPatternUtils; - if (isDefault(indexPattern)) { + if (isDefault(dataSet)) { const timefilter = data.query.timefilter.timefilter; timeRangeSearchSource.setField('filter', () => { - return timefilter.createFilter(indexPattern); + return timefilter.createFilter(dataSet); }); } searchSourceInstance.setParent(timeRangeSearchSource); searchSourceInstance.setFields({ - index: indexPattern, + index: dataSet, sort: sortForSearchSource, size, query: data.query.queryString.getQuery() || null, diff --git a/src/plugins/discover/public/application/view_components/utils/use_search.ts b/src/plugins/discover/public/application/view_components/utils/use_search.ts index 94e415b21111..5e15186e0c2a 100644 --- a/src/plugins/discover/public/application/view_components/utils/use_search.ts +++ b/src/plugins/discover/public/application/view_components/utils/use_search.ts @@ -49,6 +49,7 @@ export interface SearchData { rows?: OpenSearchSearchHit[]; bucketInterval?: TimechartHeaderBucketInterval | {}; chartData?: Chart; + title?: string; } export type SearchRefetch = 'refetch' | undefined; @@ -81,6 +82,7 @@ export const useSearch = (services: DiscoverViewServices) => { core, toastNotifications, osdUrlStateStorage, + chrome, } = services; const timefilter = data.query.timefilter.timefilter; const fetchStateRef = useRef<{ @@ -115,13 +117,15 @@ export const useSearch = (services: DiscoverViewServices) => { const refetch$ = useMemo(() => new Subject(), []); const fetch = useCallback(async () => { - if (!indexPattern) { + let dataSet = indexPattern; + if (!dataSet) { data$.next({ status: shouldSearchOnPageLoad() ? ResultStatus.LOADING : ResultStatus.UNINITIALIZED, }); return; } + // TODO: SQL disable this check BASED on ui service if (!validateTimeRange(timefilter.getTime(), toastNotifications)) { return data$.next({ status: ResultStatus.NO_RESULTS, @@ -132,17 +136,19 @@ export const useSearch = (services: DiscoverViewServices) => { // Abort any in-progress requests before fetching again if (fetchStateRef.current.abortController) fetchStateRef.current.abortController.abort(); fetchStateRef.current.abortController = new AbortController(); - const histogramConfigs = indexPattern.timeFieldName - ? createHistogramConfigs(indexPattern, interval || 'auto', data) + const histogramConfigs = dataSet.timeFieldName + ? createHistogramConfigs(dataSet, interval || 'auto', data) : undefined; const searchSource = await updateSearchSource({ - indexPattern, + indexPattern: dataSet, services, sort, searchSource: savedSearch?.searchSource, histogramConfigs, }); + dataSet = searchSource.getField('index'); + try { // Only show loading indicator if we are fetching when the rows are empty if (fetchStateRef.current.rows?.length === 0) { @@ -159,7 +165,7 @@ export const useSearch = (services: DiscoverViewServices) => { }); const inspectorRequest = inspectorAdapters.requests.start(title, { description }); inspectorRequest.stats(getRequestInspectorStats(searchSource)); - searchSource.getSearchRequestBody().then((body) => { + searchSource.getSearchRequestBody().then((body: object) => { inspectorRequest.json(body); }); @@ -177,7 +183,7 @@ export const useSearch = (services: DiscoverViewServices) => { let bucketInterval = {}; let chartData; for (const row of rows) { - const fields = Object.keys(indexPattern.flattenHit(row)); + const fields = Object.keys(dataSet!.flattenHit(row)); for (const fieldName of fields) { fetchStateRef.current.fieldCounts[fieldName] = (fetchStateRef.current.fieldCounts[fieldName] || 0) + 1; @@ -206,6 +212,10 @@ export const useSearch = (services: DiscoverViewServices) => { rows, bucketInterval, chartData, + title: + indexPattern?.title !== searchSource.getDataFrame()?.name + ? searchSource.getDataFrame()?.name + : indexPattern?.title, }); } catch (error) { // If the request was aborted then no need to surface this error in the UI diff --git a/yarn.lock b/yarn.lock index 1e6f3635138d..50061aad092e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1486,12 +1486,9 @@ utility-types "^3.10.0" uuid "^3.3.2" -"@elastic/datemath@5.0.3": - version "5.0.3" - resolved "https://registry.yarnpkg.com/@elastic/datemath/-/datemath-5.0.3.tgz#7baccdab672b9a3ecb7fe8387580670936b58573" - integrity sha512-8Hbr1Uyjm5OcYBfEB60K7sCP6U3IXuWDaLaQmYv3UxgI4jqBWbakoemwWvsqPVUvnwEjuX6z7ghPZbefs8xiaA== - dependencies: - tslib "^1.9.3" +"@elastic/datemath@link:packages/opensearch-datemath": + version "0.0.0" + uid "" "@elastic/ecs-helpers@^1.1.0": version "1.1.0"