From 4df605ff5f65b75ce03a86064602f594e6f1b578 Mon Sep 17 00:00:00 2001 From: Yulong Ruan Date: Thu, 29 Aug 2024 13:45:18 +0800 Subject: [PATCH] feat: added a new API to allow adding additional editor controls Signed-off-by: Yulong Ruan --- .../public/ui/query_editor/query_editor.tsx | 26 +++++++++++++++++++ .../ui/query_editor/query_editor_top_row.tsx | 4 +++ .../ui/search_bar/create_search_bar.tsx | 11 +++++++- .../data/public/ui/search_bar/search_bar.tsx | 4 +++ src/plugins/data/public/ui/types.ts | 7 +++++ src/plugins/data/public/ui/ui_service.ts | 12 ++++++++- 6 files changed, 62 insertions(+), 2 deletions(-) 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 2ff643f74254..a3b7cbaad4ad 100644 --- a/src/plugins/data/public/ui/query_editor/query_editor.tsx +++ b/src/plugins/data/public/ui/query_editor/query_editor.tsx @@ -16,6 +16,7 @@ import { } from '@elastic/eui'; import classNames from 'classnames'; import { isEqual } from 'lodash'; +import { BehaviorSubject, Subscription } from 'rxjs'; import React, { Component, createRef, RefObject } from 'react'; import { monaco } from '@osd/monaco'; import { IDataPluginServices, IFieldType, IIndexPattern, Query, TimeRange } from '../..'; @@ -30,6 +31,7 @@ import { DatasetSelector } from '../dataset_selector'; import { QueryControls } from '../../query/query_string/language_service/get_query_control_links'; import { RecentQueriesTable } from '../../query/query_string/language_service/recent_query'; import { DefaultInputProps } from './editors'; +import { SearchBarControl } from '../types'; const LANGUAGE_ID_SQL = 'SQL'; monaco.languages.register({ id: LANGUAGE_ID_SQL }); @@ -60,6 +62,7 @@ export interface QueryEditorProps { filterBar?: any; prepend?: React.ComponentProps['prepend']; savedQueryManagement?: any; + additionalControls$?: BehaviorSubject; } interface Props extends QueryEditorProps { @@ -75,6 +78,7 @@ interface State { timeStamp: IFieldType | null; lineCount: number | undefined; isRecentQueryVisible: boolean; + additionalControls?: SearchBarControl[]; } // Needed for React.lazy @@ -90,6 +94,7 @@ export default class QueryEditorUI extends Component { timeStamp: null, lineCount: undefined, isRecentQueryVisible: false, + additionalControls: [], }; public inputRef: monaco.editor.IStandaloneCodeEditor | null = null; @@ -103,6 +108,7 @@ export default class QueryEditorUI extends Component { private headerRef: RefObject = createRef(); private bannerRef: RefObject = createRef(); private extensionMap = this.languageManager.getQueryEditorExtensionMap(); + private additionalControlsSubscription?: Subscription; private getQueryString = () => { return toUser(this.props.query.query); @@ -227,6 +233,12 @@ export default class QueryEditorUI extends Component { } this.initPersistedLog(); + + if (this.props.additionalControls$) { + this.additionalControlsSubscription = this.props.additionalControls$.subscribe((controls) => { + this.setState({ additionalControls: controls }); + }); + } } public componentDidUpdate(prevProps: Props) { @@ -253,6 +265,7 @@ export default class QueryEditorUI extends Component { public componentWillUnmount() { if (this.abortController) this.abortController.abort(); + if (this.additionalControlsSubscription) this.additionalControlsSubscription.unsubscribe(); } handleOnFocus = () => { @@ -324,6 +337,18 @@ export default class QueryEditorUI extends Component { return ; }; + private renderAdditionalControls = () => { + if (!this.state.additionalControls) return null; + const sorted = this.state.additionalControls.sort((c1, c2) => c1.order - c2.order); + return ( + <> + {sorted.map((control, i) => { + return {control.render()}; + })} + + ); + }; + public render() { const className = classNames(this.props.className); @@ -440,6 +465,7 @@ export default class QueryEditorUI extends Component {
{this.renderQueryControls(languageEditor.TopBar.Controls)} + {this.renderAdditionalControls()} {!languageEditor.TopBar.Expanded && this.renderToggleIcon()} {this.props.savedQueryManagement} 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 a5248d3d61cb..5abab8d10f56 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 @@ -15,6 +15,7 @@ import { import classNames from 'classnames'; import React, { useState } from 'react'; import { createPortal } from 'react-dom'; +import { BehaviorSubject } from 'rxjs'; import { IDataPluginServices, IIndexPattern, Query, TimeHistoryContract, TimeRange } from '../..'; import { useOpenSearchDashboards, @@ -24,6 +25,7 @@ import { UI_SETTINGS } from '../../../common'; import { getQueryLog, PersistedLog } from '../../query'; import { NoDataPopover } from './no_data_popover'; import QueryEditorUI from './query_editor'; +import { SearchBarControl } from '../types'; const QueryEditor = withOpenSearchDashboards(QueryEditorUI); @@ -54,6 +56,7 @@ export interface QueryEditorTopRowProps { indicateNoData?: boolean; datePickerRef?: React.RefObject; savedQueryManagement?: any; + additionalControls$?: BehaviorSubject; } // Needed for React.lazy @@ -186,6 +189,7 @@ export default function QueryEditorTopRow(props: QueryEditorTopRowProps) { dataTestSubj={props.dataTestSubj} filterBar={props.filterBar} savedQueryManagement={props.savedQueryManagement} + additionalControls$={props.additionalControls$} /> ); 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 b3b240dfa2f1..db835b3d5474 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 @@ -30,6 +30,7 @@ import _ from 'lodash'; import React, { useEffect, useRef } from 'react'; +import { BehaviorSubject } from 'rxjs'; import { CoreStart } from 'src/core/public'; import { OpenSearchDashboardsContextProvider } from '../../../../opensearch_dashboards_react/public'; import { QueryStart, SavedQuery } from '../../query'; @@ -40,11 +41,13 @@ import { useSavedQuery } from './lib/use_saved_query'; import { DataPublicPluginStart } from '../../types'; import { DataStorage, Filter, Query, TimeRange } from '../../../common'; import { useQueryStringManager } from './lib/use_query_string_manager'; +import { SearchBarControl } from '../types'; interface StatefulSearchBarDeps { core: CoreStart; data: Omit; storage: DataStorage; + searchBarControls$: BehaviorSubject; } export type StatefulSearchBarProps = SearchBarOwnProps & { @@ -129,7 +132,12 @@ const overrideDefaultBehaviors = (props: StatefulSearchBarProps) => { return props.useDefaultBehaviors ? {} : props; }; -export function createSearchBar({ core, storage, data }: StatefulSearchBarDeps) { +export function createSearchBar({ + core, + storage, + data, + searchBarControls$, +}: 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) => { @@ -210,6 +218,7 @@ export function createSearchBar({ core, storage, data }: StatefulSearchBarDeps) onSaved={defaultOnSavedQueryUpdated(props, setSavedQuery)} datePickerRef={props.datePickerRef} isFilterBarPortable={props.isFilterBarPortable} + searchBarControls$={searchBarControls$} {...overrideDefaultBehaviors(props)} /> 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 bb9a2c7eb28c..0298d7c76b56 100644 --- a/src/plugins/data/public/ui/search_bar/search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/search_bar.tsx @@ -32,6 +32,7 @@ import { InjectedIntl, injectI18n } from '@osd/i18n/react'; import classNames from 'classnames'; import { compact, get, isEqual } from 'lodash'; import React, { Component } from 'react'; +import { BehaviorSubject } from 'rxjs'; import ResizeObserver from 'resize-observer-polyfill'; import { OpenSearchDashboardsReactContextValue, @@ -45,6 +46,7 @@ import { QueryEditorTopRow } from '../query_editor'; import QueryBarTopRow from '../query_string_input/query_bar_top_row'; import { SavedQueryMeta, SaveQueryForm } from '../saved_query_form'; import { FilterOptions } from '../filter_bar/filter_options'; +import { SearchBarControl } from '../types'; interface SearchBarInjectedDeps { opensearchDashboards: OpenSearchDashboardsReactContextValue; @@ -91,6 +93,7 @@ export interface SearchBarOwnProps { onClearSavedQuery?: () => void; onRefresh?: (payload: { dateRange: TimeRange }) => void; + searchBarControls$?: BehaviorSubject; indicateNoData?: boolean; } @@ -550,6 +553,7 @@ class SearchBarUI extends Component { indicateNoData={this.props.indicateNoData} datePickerRef={this.props.datePickerRef} savedQueryManagement={searchBarMenu(false, true)} + additionalControls$={this.props.searchBarControls$} /> ); } diff --git a/src/plugins/data/public/ui/types.ts b/src/plugins/data/public/ui/types.ts index ec57cb2e8c2c..bb5e78154cc5 100644 --- a/src/plugins/data/public/ui/types.ts +++ b/src/plugins/data/public/ui/types.ts @@ -3,10 +3,16 @@ * SPDX-License-Identifier: Apache-2.0 */ +import React from 'react'; import { IndexPatternSelectProps } from './index_pattern_select'; import { StatefulSearchBarProps } from './search_bar'; import { SuggestionsComponentProps } from './typeahead/suggestions_component'; +export interface SearchBarControl { + order: number; + render: () => React.ReactNode; +} + /** * The setup contract exposed by the Search plugin exposes the search strategy extension * point. @@ -21,4 +27,5 @@ export interface IUiStart { IndexPatternSelect: React.ComponentType; SearchBar: React.ComponentType; SuggestionsComponent: React.ComponentType; + addSearchBarControl: (control: SearchBarControl) => void; } diff --git a/src/plugins/data/public/ui/ui_service.ts b/src/plugins/data/public/ui/ui_service.ts index 87cfcf630965..19a31f17d72c 100644 --- a/src/plugins/data/public/ui/ui_service.ts +++ b/src/plugins/data/public/ui/ui_service.ts @@ -3,13 +3,14 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { BehaviorSubject } from 'rxjs'; import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public'; import { ConfigSchema } from '../../config'; import { DataPublicPluginStart } from '../types'; import { createIndexPatternSelect } from './index_pattern_select'; import { createSearchBar } from './search_bar/create_search_bar'; import { SuggestionsComponent } from './typeahead'; -import { IUiSetup, IUiStart } from './types'; +import { IUiSetup, IUiStart, SearchBarControl } from './types'; import { DataStorage } from '../../common'; /** @internal */ @@ -24,6 +25,9 @@ export interface UiServiceStartDependencies { export class UiService implements Plugin { enhancementsConfig: ConfigSchema['enhancements']; + searchBarControls$: BehaviorSubject = new BehaviorSubject( + [] as SearchBarControl[] + ); constructor(initializerContext: PluginInitializerContext) { const { enhancements } = initializerContext.config.get(); @@ -31,6 +35,10 @@ export class UiService implements Plugin { this.enhancementsConfig = enhancements; } + addSearchBarControl = (control: SearchBarControl) => { + this.searchBarControls$.next([...this.searchBarControls$.value, control]); + }; + public setup(core: CoreSetup, {}: UiServiceSetupDependencies): IUiSetup { return {}; } @@ -40,12 +48,14 @@ export class UiService implements Plugin { core, data: dataServices, storage, + searchBarControls$: this.searchBarControls$, }); return { IndexPatternSelect: createIndexPatternSelect(core.savedObjects.client), SearchBar, SuggestionsComponent, + addSearchBarControl: this.addSearchBarControl, }; }