diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index e204058fee4e..f9aebccbc079 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -33,7 +33,6 @@ import './index.scss'; import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public'; import { ConfigSchema } from '../config'; import { - Storage, IStorageWrapper, createStartServicesGetter, } from '../../opensearch_dashboards_utils/public'; @@ -49,6 +48,7 @@ import { SearchService } from './search/search_service'; import { UiService } from './ui/ui_service'; import { FieldFormatsService } from './field_formats'; import { QueryService } from './query'; +import { createStorage, QueryStorage } from './ui'; import { IndexPatternsService, onRedirectNoIndexPattern, @@ -117,7 +117,7 @@ export class DataPublicPlugin private readonly uiService: UiService; private readonly fieldFormatsService: FieldFormatsService; private readonly queryService: QueryService; - private readonly storage: IStorageWrapper; + private readonly storage: QueryStorage; constructor(initializerContext: PluginInitializerContext) { this.searchService = new SearchService(initializerContext); @@ -125,7 +125,7 @@ export class DataPublicPlugin this.queryService = new QueryService(); this.fieldFormatsService = new FieldFormatsService(); this.autocomplete = new AutocompleteService(initializerContext); - this.storage = new Storage(window.localStorage); + this.storage = createStorage({ engine: window.localStorage, prefix: 'opensearchDashboards.' }); } public setup( @@ -179,7 +179,7 @@ export class DataPublicPlugin query: queryService, __enhance: (enhancements: DataPublicPluginEnhancements) => { if (enhancements.search) searchService.__enhance(enhancements.search); - if (enhancements.ui) uiService.__enhance(enhancements.ui); + if (enhancements.ui) queryService.queryString.__enhance(enhancements.ui); }, }; } diff --git a/src/plugins/data/public/query/lib/add_to_query_log.ts b/src/plugins/data/public/query/lib/add_to_query_log.ts index 206f18e31d84..2e2e016031b7 100644 --- a/src/plugins/data/public/query/lib/add_to_query_log.ts +++ b/src/plugins/data/public/query/lib/add_to_query_log.ts @@ -32,10 +32,11 @@ import { IUiSettingsClient } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/opensearch_dashboards_utils/public'; import { Query } from '../../../common'; import { getQueryLog } from './get_query_log'; +import { QueryStorage } from '../../ui'; interface AddToQueryLogDependencies { uiSettings: IUiSettingsClient; - storage: IStorageWrapper; + storage: QueryStorage; } export function createAddToQueryLog({ storage, uiSettings }: AddToQueryLogDependencies) { diff --git a/src/plugins/data/public/query/lib/get_query_log.ts b/src/plugins/data/public/query/lib/get_query_log.ts index aaf471f522cf..642a913f2784 100644 --- a/src/plugins/data/public/query/lib/get_query_log.ts +++ b/src/plugins/data/public/query/lib/get_query_log.ts @@ -29,14 +29,14 @@ */ import { IUiSettingsClient } from 'src/core/public'; -import { IStorageWrapper } from 'src/plugins/opensearch_dashboards_utils/public'; import { PersistedLog } from '../persisted_log'; import { UI_SETTINGS } from '../../../common'; +import { QueryStorage } from '../../ui'; /** @internal */ export function getQueryLog( uiSettings: IUiSettingsClient, - storage: IStorageWrapper, + storage: QueryStorage, appName: string, language: string ) { diff --git a/src/plugins/data/public/query/persisted_log/persisted_log.ts b/src/plugins/data/public/query/persisted_log/persisted_log.ts index 7b37ec51b736..d3698436e4b9 100644 --- a/src/plugins/data/public/query/persisted_log/persisted_log.ts +++ b/src/plugins/data/public/query/persisted_log/persisted_log.ts @@ -32,6 +32,7 @@ import _ from 'lodash'; import * as Rx from 'rxjs'; import { map } from 'rxjs/operators'; import { IStorageWrapper } from 'src/plugins/opensearch_dashboards_utils/public'; +import { QueryStorage } from '../../ui'; const defaultIsDuplicate = (oldItem: any, newItem: any) => { return _.isEqual(oldItem, newItem); @@ -48,12 +49,12 @@ export class PersistedLog { public maxLength?: number; public filterDuplicates?: boolean; public isDuplicate: (oldItem: T, newItem: T) => boolean; - public storage: IStorageWrapper; + public storage: QueryStorage; public items: T[]; private update$ = new Rx.BehaviorSubject(undefined); - constructor(name: string, options: PersistedLogOptions = {}, storage: IStorageWrapper) { + constructor(name: string, options: PersistedLogOptions = {}, storage: QueryStorage) { this.name = name; this.maxLength = typeof options.maxLength === 'string' diff --git a/src/plugins/data/public/query/query_service.ts b/src/plugins/data/public/query/query_service.ts index 5c9c8b309367..bccd3cc1c59e 100644 --- a/src/plugins/data/public/query/query_service.ts +++ b/src/plugins/data/public/query/query_service.ts @@ -41,6 +41,7 @@ import { DataSetContract, DataSetManager } from './dataset_manager'; import { buildOpenSearchQuery, getOpenSearchQueryConfig, IndexPatternsService } from '../../common'; import { getUiSettings } from '../services'; import { IndexPattern } from '..'; +import { QueryStorage } from '../ui'; /** * Query Service @@ -48,13 +49,13 @@ import { IndexPattern } from '..'; */ interface QueryServiceSetupDependencies { - storage: IStorageWrapper; + storage: QueryStorage; uiSettings: IUiSettingsClient; } interface QueryServiceStartDependencies { savedObjectsClient: SavedObjectsClientContract; - storage: IStorageWrapper; + storage: QueryStorage; uiSettings: IUiSettingsClient; indexPatterns: IndexPatternsService; } diff --git a/src/plugins/data/public/query/query_string/history.ts b/src/plugins/data/public/query/query_string/history.ts new file mode 100644 index 000000000000..64673f4ada03 --- /dev/null +++ b/src/plugins/data/public/query/query_string/history.ts @@ -0,0 +1,91 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +import { BehaviorSubject } from 'rxjs'; +import { QueryStorage } from '../../ui/history/storage'; +import { Query, TimeRange } from '../..'; + +export class QueryHistory { + constructor(private readonly storage: QueryStorage) {} + + private changeEmitter = new BehaviorSubject(this.getHistory() || []); + + getHistoryKeys() { + return this.storage + .keys() + .filter((key: string) => key.indexOf('query_') === 0) + .sort() + .reverse(); + } + + getHistory() { + return this.getHistoryKeys().map((key) => this.storage.get(key)); + } + + // This is used as an optimization mechanism so that different components + // can listen for changes to history and update because changes to history can + // be triggered from different places in the app. The alternative would be to store + // this in state so that we hook into the React model, but it would require loading history + // every time the application starts even if a user is not going to view history. + change(listener: (reqs: any[]) => void) { + const subscription = this.changeEmitter.subscribe(listener); + return () => subscription.unsubscribe(); + } + + addQueryToHistory(dataSet: string, query: Query, dateRange?: TimeRange) { + const keys = this.getHistoryKeys(); + keys.splice(0, 500); // only maintain most recent X; + keys.forEach((key) => { + this.storage.remove(key); + }); + + const timestamp = new Date().getTime(); + const k = 'query_' + timestamp; + this.storage.set(k, { + dataSet, + time: timestamp, + query, + dateRange, + }); + + this.changeEmitter.next(this.getHistory()); + } + + updateCurrentState(content: any) { + const timestamp = new Date().getTime(); + this.storage.set('editor_state', { + time: timestamp, + content, + }); + } + + getLegacySavedEditorState() { + const saved = this.storage.get('editor_state'); + if (!saved) return; + const { time, content } = saved; + return { time, content }; + } + + /** + * This function should only ever be called once for a user if they had legacy state. + */ + deleteLegacySavedEditorState() { + this.storage.remove('editor_state'); + } + + clearHistory() { + this.getHistoryKeys().forEach((key) => this.storage.remove(key)); + } +} + +export function createHistory(deps: { storage: QueryStorage }) { + return new QueryHistory(deps.storage); +} diff --git a/src/plugins/data/public/query/query_string/language_manager/language_manager.ts b/src/plugins/data/public/query/query_string/language_manager/language_manager.ts new file mode 100644 index 000000000000..1823c1425d91 --- /dev/null +++ b/src/plugins/data/public/query/query_string/language_manager/language_manager.ts @@ -0,0 +1,123 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { BehaviorSubject } from 'rxjs'; +import { CoreStart } from 'opensearch-dashboards/public'; +import { skip } from 'rxjs/operators'; +import { + IndexPattern, + SIMPLE_DATA_SET_TYPES, + SimpleDataSet, + SimpleDataSource, + UI_SETTINGS, +} from '../../../../common'; +import { IndexPatternsContract } from '../../../index_patterns'; +import { QueryEnhancement, UiEnhancements } from '../../../ui/types'; +import { QueryEditorExtensionConfig } from '../../../ui'; +import { DataPublicPluginEnhancements } from '../../../types'; + +export class LanguageManager { + private queryEnhancements: Map; + private queryEditorExtensionMap: Record; + //TODO + //private queryEditorUISettings: Map; + + constructor(private readonly uiSettings: CoreStart['uiSettings']) { + this.queryEnhancements = new Map(); + this.queryEditorExtensionMap = {}; + //TODO + //this.queryEditorUISettings = new Map(); + } + + public __enhance = (enhancements: UiEnhancements) => { + if (!enhancements) return; + if (enhancements.query && enhancements.query.language) { + this.queryEnhancements.set(enhancements.query.language, enhancements.query); + } + if (enhancements.queryEditorExtension) { + this.queryEditorExtensionMap[enhancements.queryEditorExtension.id] = + enhancements.queryEditorExtension; + } + + //TODO + //settings related to query editor UI + // if (enhancements.queryEditorUISettings && enhancements.queryEditorUISettings.language) { + // this.queryEditorUISettings.set( + // enhancements.queryEditorUISettings.language, + // enhancements.queryEditorUISettings + // ); + // } + }; + + // public initWithIndexPattern = (indexPattern: IndexPattern | null) => { + // if (!this.uiSettings.get(UI_SETTINGS.QUERY_ENHANCEMENTS_ENABLED)) return; + // if (!indexPattern || !indexPattern.id) { + // return undefined; + // } + + // this.defaultDataSet = { + // id: indexPattern.id, + // title: indexPattern.title, + // type: SIMPLE_DATA_SET_TYPES.INDEX_PATTERN, + // timeFieldName: indexPattern.timeFieldName, + // fields: indexPattern.fields, + // ...(indexPattern.dataSourceRef + // ? { + // dataSourceRef: { + // id: indexPattern.dataSourceRef?.id, + // name: indexPattern.dataSourceRef?.name, + // type: indexPattern.dataSourceRef?.type, + // } as SimpleDataSource, + // } + // : {}), + // }; + // }; + + // public getUpdates$ = () => { + // return this.dataSet$.asObservable().pipe(skip(1)); + // }; + + public getQueryEnhancement = () => { + return this.queryEnhancements; + }; + + public getQueryEditorExtension = () => { + return this.queryEditorExtensionMap; + }; + + // public getDefaultDataSet = () => { + // return this.defaultDataSet; + // }; + + // public fetchDefaultDataSet = async (): Promise => { + // const defaultIndexPatternId = this.uiSettings.get('defaultIndex'); + // if (!defaultIndexPatternId) { + // return undefined; + // } + + // const indexPattern = await this.indexPatterns?.get(defaultIndexPatternId); + // if (!indexPattern || !indexPattern.id) { + // return undefined; + // } + + // return { + // id: indexPattern.id, + // title: indexPattern.title, + // type: SIMPLE_DATA_SET_TYPES.INDEX_PATTERN, + // timeFieldName: indexPattern.timeFieldName, + // ...(indexPattern.dataSourceRef + // ? { + // dataSourceRef: { + // id: indexPattern.dataSourceRef?.id, + // name: indexPattern.dataSourceRef?.name, + // type: indexPattern.dataSourceRef?.type, + // } as SimpleDataSource, + // } + // : {}), + // }; + // }; +} + +export type LanguageContract = PublicMethodsOf; diff --git a/src/plugins/data/public/query/query_string/query_string_manager.ts b/src/plugins/data/public/query/query_string/query_string_manager.ts index 3747cabaf9ca..b6afc16d157a 100644 --- a/src/plugins/data/public/query/query_string/query_string_manager.ts +++ b/src/plugins/data/public/query/query_string/query_string_manager.ts @@ -31,17 +31,23 @@ import { BehaviorSubject } from 'rxjs'; import { skip } from 'rxjs/operators'; import { CoreStart } from 'opensearch-dashboards/public'; -import { IStorageWrapper } from 'src/plugins/opensearch_dashboards_utils/public'; -import { Query, UI_SETTINGS } from '../../../common'; +import { Query, TimeRange, UI_SETTINGS } from '../../../common'; +import { LanguageManager } from './language_manager/language_manager'; +import { QueryStorage, UiEnhancements } from '../../ui'; +import { QueryHistory } from './history'; export class QueryStringManager { private query$: BehaviorSubject; + private languageManager: LanguageManager; + private queryHistory: QueryHistory; constructor( - private readonly storage: IStorageWrapper, + private readonly storage: QueryStorage, private readonly uiSettings: CoreStart['uiSettings'] ) { this.query$ = new BehaviorSubject(this.getDefaultQuery()); + this.languageManager = new LanguageManager(uiSettings); + this.queryHistory = new QueryHistory(storage); } private getDefaultQueryString() { @@ -55,6 +61,24 @@ export class QueryStringManager { ); } + public addToQueryHistory(dataSet: string, query: Query, timeRange?: TimeRange) { + if (query.query) { + this.queryHistory.addQueryToHistory(dataSet, query, timeRange); + } + } + + public getQueryHistory() { + return this.queryHistory.getHistory(); + } + + public clearQueryHistory() { + this.queryHistory.clearHistory(); + } + + public changeQueryHistory(listener: (reqs: any[]) => void) { + return this.queryHistory.change(listener); + } + public getDefaultQuery() { return { query: this.getDefaultQueryString(), @@ -100,6 +124,18 @@ export class QueryStringManager { public clearQuery = () => { this.setQuery(this.getDefaultQuery()); }; + + public __enhance = (enhancements: UiEnhancements) => { + this.languageManager.__enhance(enhancements); + }; + + public getQueryEnhancement = () => { + return this.languageManager.getQueryEnhancement(); + }; + + public getQueryEditorExtension = () => { + return this.languageManager.getQueryEditorExtension(); + }; } export type QueryStringContract = PublicMethodsOf; diff --git a/src/plugins/data/public/query/state_sync/create_global_query_observable.ts b/src/plugins/data/public/query/state_sync/create_global_query_observable.ts index a77b3f159e25..1f1e71dcc352 100644 --- a/src/plugins/data/public/query/state_sync/create_global_query_observable.ts +++ b/src/plugins/data/public/query/state_sync/create_global_query_observable.ts @@ -37,6 +37,7 @@ import { createStateContainer } from '../../../../opensearch_dashboards_utils/pu import { isFilterPinned, compareFilters, COMPARE_ALL_OPTIONS } from '../../../common'; import { QueryStringContract } from '../query_string'; import { DataSetContract } from '../dataset_manager'; +import { LanguageContract } from '../query_string/language_manager/language_manager'; export function createQueryStateObservable({ timefilter: { timefilter }, @@ -56,6 +57,7 @@ export function createQueryStateObservable({ filters: filterManager.getFilters(), query: queryString.getQuery(), dataSet: dataSetManager.getDataSet(), + //language: languageManager.getQueryEnhancement(), }); let currentChange: QueryStateChange = {}; @@ -98,6 +100,10 @@ export function createQueryStateObservable({ filters: filterManager.getFilters(), }); }), + // languageManager.getUpdates$().subscribe(() => { + // currentChange.language = true; + // state.set({ ...state.get(), language: languageManager.getQueryEnhancement() }); + // }), state.state$ .pipe( map((newState) => ({ state: newState, changes: currentChange })), diff --git a/src/plugins/data/public/query/state_sync/types.ts b/src/plugins/data/public/query/state_sync/types.ts index 8134a7208f13..a97d65464907 100644 --- a/src/plugins/data/public/query/state_sync/types.ts +++ b/src/plugins/data/public/query/state_sync/types.ts @@ -39,6 +39,7 @@ export interface QueryState { filters?: Filter[]; query?: Query; dataSet?: SimpleDataSet; + language?: any; } type QueryStateChangePartial = { diff --git a/src/plugins/data/public/query/timefilter/time_history.ts b/src/plugins/data/public/query/timefilter/time_history.ts index b755fbf36da4..fd77bcc23cdd 100644 --- a/src/plugins/data/public/query/timefilter/time_history.ts +++ b/src/plugins/data/public/query/timefilter/time_history.ts @@ -32,11 +32,12 @@ import moment from 'moment'; import { IStorageWrapper } from 'src/plugins/opensearch_dashboards_utils/public'; import { PersistedLog } from '../persisted_log'; import { TimeRange } from '../../../common'; +import { QueryStorage } from '../../ui'; export class TimeHistory { private history: PersistedLog; - constructor(storage: IStorageWrapper) { + constructor(storage: QueryStorage) { const historyOptions = { maxLength: 10, filterDuplicates: true, diff --git a/src/plugins/data/public/query/timefilter/timefilter_service.ts b/src/plugins/data/public/query/timefilter/timefilter_service.ts index dbea62d46f89..da8437034f5f 100644 --- a/src/plugins/data/public/query/timefilter/timefilter_service.ts +++ b/src/plugins/data/public/query/timefilter/timefilter_service.ts @@ -29,9 +29,9 @@ */ import { IUiSettingsClient } from 'src/core/public'; -import { IStorageWrapper } from 'src/plugins/opensearch_dashboards_utils/public'; import { TimeHistory, Timefilter, TimeHistoryContract, TimefilterContract } from './index'; import { UI_SETTINGS } from '../../../common'; +import { QueryStorage } from '../../ui'; /** * Filter Service @@ -40,7 +40,7 @@ import { UI_SETTINGS } from '../../../common'; export interface TimeFilterServiceDependencies { uiSettings: IUiSettingsClient; - storage: IStorageWrapper; + storage: QueryStorage; } export class TimefilterService { diff --git a/src/plugins/data/public/ui/history/index.ts b/src/plugins/data/public/ui/history/index.ts new file mode 100644 index 000000000000..9ca70b7c3344 --- /dev/null +++ b/src/plugins/data/public/ui/history/index.ts @@ -0,0 +1,12 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +export { QueryStorage, createStorage } from './storage'; diff --git a/src/plugins/data/public/ui/history/recent_query.tsx b/src/plugins/data/public/ui/history/recent_query.tsx new file mode 100644 index 000000000000..cde758dce7d2 --- /dev/null +++ b/src/plugins/data/public/ui/history/recent_query.tsx @@ -0,0 +1,151 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'; +import { i18n } from '@osd/i18n'; +import { memoize } from 'lodash'; +import moment from 'moment'; +import { + keys, + EuiSpacer, + EuiIcon, + EuiTitle, + EuiFlexItem, + EuiFlexGroup, + EuiSmallButtonEmpty, + EuiSmallButton, + EuiText, + EuiPopover, + EuiButtonEmpty, + EuiContextMenuPanel, + EuiContextMenuItem, +} from '@elastic/eui'; +import { QueryStringContract } from '../../query/query_string'; + +interface Props { + services: QueryStringContract; +} + +const CHILD_ELEMENT_PREFIX = 'historyReq'; + +export function RecentQuery({ services }: Props) { + const [recentQueries, setRecentQueries] = useState(services.getQueryHistory()); + const [isPopoverOpen, setPopover] = useState(false); + + const onButtonClick = () => { + setPopover(!isPopoverOpen); + }; + + const clearHistory = useCallback(() => { + services?.clearQueryHistory(); + setRecentQueries(services?.getQueryHistory()); + }, [services]); + + const listRef = useRef(null); + + const [viewingReq, setViewingReq] = useState(null); + const [selectedIndex, setSelectedIndex] = useState(0); + const selectedReq = useRef(null); + + // const scrollIntoView = useCallback((idx: number) => { + // const activeDescendant = listRef.current!.querySelector(`#${CHILD_ELEMENT_PREFIX}${idx}`); + // if (activeDescendant) { + // activeDescendant.scrollIntoView(); + // } + // }, []); + + // const initialize = useCallback(() => { + // const nextSelectedIndex = 0; + // (describeReq as any).cache = new WeakMap(); + // setViewingReq(requests[nextSelectedIndex]); + // selectedReq.current = requests[nextSelectedIndex]; + // setSelectedIndex(nextSelectedIndex); + // scrollIntoView(nextSelectedIndex); + // }, [describeReq, requests, scrollIntoView]); + + const clear = () => { + clearHistory(); + //initialize(); + }; + + //const restoreRequestFromHistory = useRestoreRequestFromHistory(); + + // useEffect(() => { + // initialize(); + // }, [initialize]); + + useEffect(() => { + const done = services.changeQueryHistory(setRecentQueries); + return () => done(); + }, [services]); + + const recentQueryItems = recentQueries.map((query, idx) => { + const date = moment(query.time); + + const formattedDate = date.format('MMM D, YYYY HH:mm:ss'); + + let queryLanguage = query.query.language; + if (queryLanguage === 'kuery') { + queryLanguage = 'DQL'; + } + + return ( + setPopover(false)}> + + {query.query.query} + {queryLanguage} + {formattedDate} + + + ); + }); + + return ( + + {'Recent queries'} + + } + isOpen={isPopoverOpen} + closePopover={() => setPopover(false)} + panelPaddingSize="none" + anchorPosition={'downLeft'} + > + + + ); +} diff --git a/src/plugins/data/public/ui/history/storage.ts b/src/plugins/data/public/ui/history/storage.ts new file mode 100644 index 000000000000..184f21ee0907 --- /dev/null +++ b/src/plugins/data/public/ui/history/storage.ts @@ -0,0 +1,74 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +import { transform, startsWith, keys } from 'lodash'; +import { parse, stringify } from '@osd/std'; + +export enum StorageKeys { + WIDTH = 'widths', +} + +type IStorageEngine = typeof window.localStorage; +export class QueryStorage { + constructor(private readonly engine: IStorageEngine, private readonly prefix: string) {} + + encode(val: any) { + return stringify(val); + } + + decode(val: any) { + if (typeof val === 'string') { + return parse(val); + } + } + + encodeKey(key: string) { + return `${this.prefix}${key}`; + } + + decodeKey(key: string) { + if (startsWith(key, this.prefix)) { + return `${key.slice(this.prefix.length)}`; + } + } + + set(key: string, val: any) { + this.engine.setItem(this.encodeKey(key), this.encode(val)); + return val; + } + + has(key: string) { + return this.engine.getItem(this.encodeKey(key)) != null; + } + + get(key: string, _default?: T) { + if (this.has(key)) { + return this.decode(this.engine.getItem(this.encodeKey(key))); + } else { + return _default; + } + } + + remove(key: string) { + return this.engine.removeItem(this.encodeKey(key)); + } + + keys(): string[] { + return transform(keys(this.engine), (ours, key) => { + const ourKey = this.decodeKey(key); + if (ourKey != null) ours.push(ourKey); + }); + } +} + +export function createStorage(deps: { engine: IStorageEngine; prefix: string }) { + return new QueryStorage(deps.engine, deps.prefix); +} diff --git a/src/plugins/data/public/ui/index.ts b/src/plugins/data/public/ui/index.ts index 400887e51d57..4f9ddbd93a0e 100644 --- a/src/plugins/data/public/ui/index.ts +++ b/src/plugins/data/public/ui/index.ts @@ -50,3 +50,4 @@ export { export { SearchBar, SearchBarProps, StatefulSearchBarProps } from './search_bar'; export { SuggestionsComponent } from './typeahead'; export { DataSetNavigator } from './dataset_navigator'; +export { QueryStorage, createStorage } from './history'; diff --git a/src/plugins/data/public/ui/query_editor/language_selector.tsx b/src/plugins/data/public/ui/query_editor/language_selector.tsx index 455540d28df2..e9222dbdf8c0 100644 --- a/src/plugins/data/public/ui/query_editor/language_selector.tsx +++ b/src/plugins/data/public/ui/query_editor/language_selector.tsx @@ -13,12 +13,14 @@ import { import { i18n } from '@osd/i18n'; import React, { useState } from 'react'; import { getUiService } from '../../services'; +import { QueryEnhancement } from '../types'; export interface QueryLanguageSelectorProps { language: string; onSelectLanguage: (newLanguage: string) => void; anchorPosition?: PopoverAnchorPosition; appName?: string; + queryEnhancements: Map; } const mapExternalLanguageToOptions = (language: string) => { @@ -55,8 +57,9 @@ export const QueryLanguageSelector = (props: QueryLanguageSelectorProps) => { const uiService = getUiService(); - const queryEnhancements = uiService.Settings.getAllQueryEnhancements(); - queryEnhancements.forEach((enhancement) => { + //const queryEnhancements = uiService.Settings.getAllQueryEnhancements(); + + props.queryEnhancements.forEach((enhancement) => { if ( (enhancement.supportedAppNames && props.appName && diff --git a/src/plugins/data/public/ui/query_editor/query_editor.tsx b/src/plugins/data/public/ui/query_editor/query_editor.tsx index 2238abbc5b1b..95fe6771a91a 100644 --- a/src/plugins/data/public/ui/query_editor/query_editor.tsx +++ b/src/plugins/data/public/ui/query_editor/query_editor.tsx @@ -21,6 +21,7 @@ import { QueryEditorBtnCollapse } from './query_editor_btn_collapse'; import { SimpleDataSet } from '../../../common'; import { createDQLEditor, createDefaultEditor } from './editors'; import { getQueryService, getIndexPatterns } from '../../services'; +import { RecentQuery } from '../history/recent_query'; const LANGUAGE_ID_SQL = 'SQL'; monaco.languages.register({ id: LANGUAGE_ID_SQL }); @@ -307,12 +308,15 @@ export default class QueryEditorUI extends Component { const useQueryEditor = this.props.query.language !== 'kuery' && this.props.query.language !== 'lucene'; + const queryEnhancements = this.queryService.queryString.getQueryEnhancement(); + console.log('queryEnhancements', queryEnhancements); const languageSelector = ( ); @@ -375,6 +379,10 @@ export default class QueryEditorUI extends Component { filterBar: this.props.filterBar, }); + const recentQueries = () => { + return ; + }; + return (
{
{languageEditor.Body()}
)} + {recentQueries()} + {/* diff --git a/src/plugins/data/public/ui/query_editor/query_editor_top_row.tsx b/src/plugins/data/public/ui/query_editor/query_editor_top_row.tsx index 4621944ecce3..939eba21ca17 100644 --- a/src/plugins/data/public/ui/query_editor/query_editor_top_row.tsx +++ b/src/plugins/data/public/ui/query_editor/query_editor_top_row.tsx @@ -71,7 +71,7 @@ export default function QueryEditorTopRow(props: QueryEditorTopRowProps) { storage, appName, data: { - query: { dataSetManager: dataSetManager }, + query: { dataSetManager: dataSetManager, queryString: queryString }, }, } = opensearchDashboards.services; const { dataSet } = useDataSetManager({ dataSetManager: dataSetManager! }); 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 dc3d9191e056..6a8938e9145b 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 @@ -31,7 +31,6 @@ import _ from 'lodash'; import React, { useCallback, useEffect, useRef } from 'react'; import { CoreStart } from 'src/core/public'; -import { IStorageWrapper } from 'src/plugins/opensearch_dashboards_utils/public'; import { OpenSearchDashboardsContextProvider } from '../../../../opensearch_dashboards_react/public'; import { QueryStart, SavedQuery } from '../../query'; import { SearchBar, SearchBarOwnProps } from './'; @@ -42,11 +41,11 @@ import { DataPublicPluginStart } from '../../types'; import { Filter, Query, TimeRange } from '../../../common'; import { useQueryStringManager } from './lib/use_query_string_manager'; import { Settings } from '../types'; - +import { QueryStorage } from '../history'; interface StatefulSearchBarDeps { core: CoreStart; data: Omit; - storage: IStorageWrapper; + storage: QueryStorage; settings: Settings; setDataSetContainerRef: (ref: HTMLDivElement | null) => void; } @@ -190,7 +189,7 @@ export function createSearchBar({ }, true ); - }, [query, timeRange, useDefaultBehaviors]); + }, [props.settings, query, timeRange, useDefaultBehaviors]); return ( { private services = this.props.opensearchDashboards.services; private savedQueryService = this.services.data.query.savedQueries; + private dataSetService = this.services.data.query.dataSetManager; + private queryStringService = this.services.data.query.queryString; public filterBarRef: Element | null = null; public filterBarWrapperRef: Element | null = null; @@ -371,6 +373,14 @@ class SearchBarUI extends Component { } } ); + const dataSet = this.dataSetService.getDataSet(); + if (dataSet && queryAndDateRange.query) { + this.queryStringService.addToQueryHistory( + dataSet.id, + queryAndDateRange.query, + queryAndDateRange.dateRange + ); + } }; public onLoadSavedQuery = (savedQuery: SavedQuery) => { diff --git a/src/plugins/data/public/ui/settings/settings.ts b/src/plugins/data/public/ui/settings/settings.ts index 007800738952..44e089083f92 100644 --- a/src/plugins/data/public/ui/settings/settings.ts +++ b/src/plugins/data/public/ui/settings/settings.ts @@ -4,12 +4,12 @@ */ import { BehaviorSubject } from 'rxjs'; -import { IStorageWrapper } from '../../../../opensearch_dashboards_utils/public'; import { setOverrides as setFieldOverrides } from '../../../common'; import { ConfigSchema } from '../../../config'; import { ISearchStart } from '../../search'; import { QueryEditorExtensionConfig } from '../query_editor/query_editor_extensions'; import { QueryEnhancement } from '../types'; +import { QueryStorage } from '../history'; export interface DataSettings { userQueryLanguage: string; @@ -31,13 +31,14 @@ export class Settings { constructor( private readonly config: ConfigSchema['enhancements'], private readonly search: ISearchStart, - private readonly storage: IStorageWrapper, + private readonly storage: QueryStorage, private readonly queryEnhancements: Map, private readonly queryEditorExtensionMap: Record ) { this.isEnabled = true; this.setUserQueryEnhancementsEnabled(this.isEnabled); this.enhancedAppNames = this.isEnabled ? this.config.supportedAppNames : []; + this.storage = storage; } supportsEnhancementsEnabled(appName: string) { @@ -71,27 +72,28 @@ export class Settings { return this.queryEditorExtensionMap; } + //TODO: update all the prefixes to get rid of opensearchDashboards. getUserQueryLanguageBlocklist() { - return this.storage.get('opensearchDashboards.userQueryLanguageBlocklist') || []; + return this.storage.get('userQueryLanguageBlocklist') || []; } setUserQueryLanguageBlocklist(languages: string[]) { this.storage.set( - 'opensearchDashboards.userQueryLanguageBlocklist', + 'userQueryLanguageBlocklist', languages.map((language) => language.toLowerCase()) ); return true; } getUserQueryLanguage() { - return this.storage.get('opensearchDashboards.userQueryLanguage') || 'kuery'; + return this.storage.get('userQueryLanguage') || 'kuery'; } setUserQueryLanguage(language: string) { if (language !== this.getUserQueryLanguage()) { this.search.df.clear(); } - this.storage.set('opensearchDashboards.userQueryLanguage', language); + this.storage.set('userQueryLanguage', language); const queryEnhancement = this.queryEnhancements.get(language); this.search.__enhance({ searchInterceptor: queryEnhancement @@ -104,25 +106,26 @@ export class Settings { } getUserQueryString() { - return this.storage.get('opensearchDashboards.userQueryString') || ''; + return this.storage.get('userQueryString') || ''; } + //TODO: create a helper func to set user query that passed in a query object setUserQueryString(query: string) { - this.storage.set('opensearchDashboards.userQueryString', query); + this.storage.set('userQueryString', query); return true; } getUiOverrides() { - return this.storage.get('opensearchDashboards.uiOverrides') || {}; + return this.storage.get('uiOverrides') || {}; } setUiOverrides(overrides?: { [key: string]: any }) { if (!overrides) { - this.storage.remove('opensearchDashboards.uiOverrides'); + this.storage.remove('uiOverrides'); setFieldOverrides(undefined); return true; } - this.storage.set('opensearchDashboards.uiOverrides', overrides); + this.storage.set('uiOverrides', overrides); setFieldOverrides(overrides.fields); return true; } @@ -171,7 +174,7 @@ export class Settings { interface Deps { config: ConfigSchema['enhancements']; search: ISearchStart; - storage: IStorageWrapper; + storage: QueryStorage; queryEnhancements: Map; queryEditorExtensionMap: Record; } diff --git a/src/plugins/data/public/ui/types.ts b/src/plugins/data/public/ui/types.ts index 268e274c199d..ce9ff39b86f8 100644 --- a/src/plugins/data/public/ui/types.ts +++ b/src/plugins/data/public/ui/types.ts @@ -50,6 +50,8 @@ export interface QueryEnhancement { export interface UiEnhancements { query?: QueryEnhancement; queryEditorExtension?: QueryEditorExtensionConfig; + //TODO + //queryEditorUISettings?: QueryEditorUISettings; } /** diff --git a/src/plugins/data/public/ui/ui_service.ts b/src/plugins/data/public/ui/ui_service.ts index 8359872137b2..7491eeeac3dc 100644 --- a/src/plugins/data/public/ui/ui_service.ts +++ b/src/plugins/data/public/ui/ui_service.ts @@ -5,16 +5,15 @@ import { BehaviorSubject } from 'rxjs'; import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public'; -import { IStorageWrapper } from '../../../opensearch_dashboards_utils/public'; import { ConfigSchema } from '../../config'; import { DataPublicPluginStart } from '../types'; import { createDataSetNavigator } from './dataset_navigator'; import { createIndexPatternSelect } from './index_pattern_select'; -import { QueryEditorExtensionConfig } from './query_editor'; import { createSearchBar } from './search_bar/create_search_bar'; import { createSettings } from './settings'; import { SuggestionsComponent } from './typeahead'; -import { IUiSetup, IUiStart, QueryEnhancement, UiEnhancements } from './types'; +import { IUiSetup, IUiStart } from './types'; +import { QueryStorage } from './history'; /** @internal */ // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -23,13 +22,13 @@ export interface UiServiceSetupDependencies {} /** @internal */ export interface UiServiceStartDependencies { dataServices: Omit; - storage: IStorageWrapper; + storage: QueryStorage; } export class UiService implements Plugin { enhancementsConfig: ConfigSchema['enhancements']; - private queryEnhancements: Map = new Map(); - private queryEditorExtensionMap: Record = {}; + // private queryEnhancements: Map = new Map(); + // private queryEditorExtensionMap: Record = {}; private dataSetContainer$ = new BehaviorSubject(null); constructor(initializerContext: PluginInitializerContext) { @@ -38,19 +37,19 @@ export class UiService implements Plugin { this.enhancementsConfig = enhancements; } - public setup(core: CoreSetup, {}: UiServiceSetupDependencies): IUiSetup { - return { - __enhance: (enhancements?: UiEnhancements) => { - if (!enhancements) return; - if (enhancements.query && enhancements.query.language) { - this.queryEnhancements.set(enhancements.query.language, enhancements.query); - } - if (enhancements.queryEditorExtension) { - this.queryEditorExtensionMap[enhancements.queryEditorExtension.id] = - enhancements.queryEditorExtension; - } - }, - }; + public setup(core: CoreSetup, {}: UiServiceSetupDependencies): any { + // return { + // __enhance: (enhancements?: UiEnhancements) => { + // if (!enhancements) return; + // if (enhancements.query && enhancements.query.language) { + // this.queryEnhancements.set(enhancements.query.language, enhancements.query); + // } + // if (enhancements.queryEditorExtension) { + // this.queryEditorExtensionMap[enhancements.queryEditorExtension.id] = + // enhancements.queryEditorExtension; + // } + // }, + // }; } public start(core: CoreStart, { dataServices, storage }: UiServiceStartDependencies): IUiStart { @@ -58,8 +57,8 @@ export class UiService implements Plugin { config: this.enhancementsConfig, search: dataServices.search, storage, - queryEnhancements: this.queryEnhancements, - queryEditorExtensionMap: this.queryEditorExtensionMap, + queryEnhancements: dataServices.query.queryString.getQueryEnhancement(), + queryEditorExtensionMap: dataServices.query.queryString.getQueryEditorExtension(), }); const setDataSetContainerRef = (ref: HTMLDivElement | null) => { diff --git a/src/plugins/opensearch_dashboards_utils/public/storage/storage.ts b/src/plugins/opensearch_dashboards_utils/public/storage/storage.ts index f7a02820616a..436a01e80465 100644 --- a/src/plugins/opensearch_dashboards_utils/public/storage/storage.ts +++ b/src/plugins/opensearch_dashboards_utils/public/storage/storage.ts @@ -28,6 +28,7 @@ * under the License. */ +import { keys } from 'lodash'; import { IStorage, IStorageWrapper } from './types'; export class Storage implements IStorageWrapper { @@ -69,4 +70,8 @@ export class Storage implements IStorageWrapper { public clear = () => { return this.store.clear(); }; + + public keys = (): string[] => { + return keys(this.store); + }; } diff --git a/src/plugins/opensearch_dashboards_utils/public/storage/types.ts b/src/plugins/opensearch_dashboards_utils/public/storage/types.ts index a6aea45c4801..41beeca5b67d 100644 --- a/src/plugins/opensearch_dashboards_utils/public/storage/types.ts +++ b/src/plugins/opensearch_dashboards_utils/public/storage/types.ts @@ -33,6 +33,7 @@ export interface IStorageWrapper { set: (key: string, value: T) => S; remove: (key: string) => T | null; clear: () => void; + keys: () => string[]; } export interface IStorage {