diff --git a/src/platform/packages/private/kbn-esql-editor/src/custom_commands/use_lookup_index_editor.tsx b/src/platform/packages/private/kbn-esql-editor/src/custom_commands/use_lookup_index_editor.tsx index 791dc2be27a84..2576ff5ee05ea 100644 --- a/src/platform/packages/private/kbn-esql-editor/src/custom_commands/use_lookup_index_editor.tsx +++ b/src/platform/packages/private/kbn-esql-editor/src/custom_commands/use_lookup_index_editor.tsx @@ -140,7 +140,8 @@ export const useLookupIndexCommand = ( getLookupIndices: (() => Promise<{ indices: IndexAutocompleteItem[] }>) | undefined, query: AggregateQuery, onIndexCreated: (resultQuery: string) => Promise, - onNewFieldsAddedToIndex?: (indexName: string) => void + onNewFieldsAddedToIndex?: (indexName: string) => void, + onOpenIndexInDiscover?: EditLookupIndexContentContext['onOpenIndexInDiscover'] ) => { const { euiTheme } = useEuiTheme(); const { @@ -296,9 +297,10 @@ export const useLookupIndexCommand = ( indexHasNewFields ); }, + onOpenIndexInDiscover, } as EditLookupIndexContentContext); }, - [onFlyoutClose, uiActions] + [onFlyoutClose, onOpenIndexInDiscover, uiActions] ); const openFlyoutRef = useRef(openFlyout); diff --git a/src/platform/packages/private/kbn-esql-editor/src/esql_editor.tsx b/src/platform/packages/private/kbn-esql-editor/src/esql_editor.tsx index 5625e140222bc..c6a6724bc17b6 100644 --- a/src/platform/packages/private/kbn-esql-editor/src/esql_editor.tsx +++ b/src/platform/packages/private/kbn-esql-editor/src/esql_editor.tsx @@ -126,6 +126,7 @@ const ESQLEditorInternal = function ESQLEditor({ disableAutoFocus, controlsContext, esqlVariables, + onOpenQueryInNewTab, expandToFitQueryOnMount, dataErrorsControl, formLabel, @@ -769,7 +770,8 @@ const ESQLEditorInternal = function ESQLEditor({ getJoinIndices, query, onLookupIndexCreate, - onNewFieldsAddedToLookupIndex + onNewFieldsAddedToLookupIndex, + onOpenQueryInNewTab ); useDebounceWithOptions( diff --git a/src/platform/packages/private/kbn-esql-editor/src/types.ts b/src/platform/packages/private/kbn-esql-editor/src/types.ts index 9cb0aabafd8f5..c7cf8d4265506 100644 --- a/src/platform/packages/private/kbn-esql-editor/src/types.ts +++ b/src/platform/packages/private/kbn-esql-editor/src/types.ts @@ -91,6 +91,8 @@ export interface ESQLEditorProps { disableAutoFocus?: boolean; /** Enables the creation of controls from the editor **/ controlsContext?: ControlsContext; + /** Opens the given query in a new Discover tab **/ + onOpenQueryInNewTab?: (tabName: string, esqlQuery: string) => Promise; /** The available ESQL variables from the page context this editor was opened in */ esqlVariables?: ESQLControlVariable[]; /** Resize the editor to fit the initially passed query on mount */ diff --git a/src/platform/packages/private/kbn-index-editor/src/components/flyout_content.tsx b/src/platform/packages/private/kbn-index-editor/src/components/flyout_content.tsx index 8f7fe610d6288..4016a07d2cef4 100644 --- a/src/platform/packages/private/kbn-index-editor/src/components/flyout_content.tsx +++ b/src/platform/packages/private/kbn-index-editor/src/components/flyout_content.tsx @@ -138,7 +138,7 @@ export const FlyoutContent: FC = ({ deps, props }) => { {dataView ? ( - + diff --git a/src/platform/packages/private/kbn-index-editor/src/components/modals/unsaved_changes_modal.tsx b/src/platform/packages/private/kbn-index-editor/src/components/modals/unsaved_changes_modal.tsx index 6b97ccf82e176..985cde673c78b 100644 --- a/src/platform/packages/private/kbn-index-editor/src/components/modals/unsaved_changes_modal.tsx +++ b/src/platform/packages/private/kbn-index-editor/src/components/modals/unsaved_changes_modal.tsx @@ -28,19 +28,20 @@ export const UnsavedChangesModal: React.FC = ({ onClose }) const exitAttemptWithUnsavedFields = useObservable( indexUpdateService.exitAttemptWithUnsavedChanges$, - false + { isActive: false } ); const closeWithoutSaving = () => { indexUpdateService.discardUnsavedChanges(); onClose(); + exitAttemptWithUnsavedFields.onExitCallback?.(); }; const continueEditing = () => { indexUpdateService.setExitAttemptWithUnsavedChanges(false); }; - if (!exitAttemptWithUnsavedFields) { + if (!exitAttemptWithUnsavedFields.isActive) { return null; } diff --git a/src/platform/packages/private/kbn-index-editor/src/components/query_bar.tsx b/src/platform/packages/private/kbn-index-editor/src/components/query_bar.tsx index a1b76c31c0e2a..c6d689b09b2ad 100644 --- a/src/platform/packages/private/kbn-index-editor/src/components/query_bar.tsx +++ b/src/platform/packages/private/kbn-index-editor/src/components/query_bar.tsx @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React, { useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { EuiButton, EuiFlexGroup, @@ -20,9 +20,13 @@ import { useKibana } from '@kbn/kibana-react-plugin/public'; import useObservable from 'react-use/lib/useObservable'; import { FormattedMessage } from '@kbn/i18n-react'; import { FilePicker } from './file_picker'; -import type { KibanaContextExtra } from '../types'; +import type { EditLookupIndexContentContext, KibanaContextExtra } from '../types'; -export const QueryBar = () => { +export const QueryBar = ({ + onOpenIndexInDiscover, +}: { + onOpenIndexInDiscover?: EditLookupIndexContentContext['onOpenIndexInDiscover']; +}) => { const { services: { share, data, indexUpdateService, indexEditorTelemetryService }, } = useKibana(); @@ -34,6 +38,7 @@ export const QueryBar = () => { indexUpdateService.indexCreated$, indexUpdateService.isIndexCreated() ); + const indexName = useObservable(indexUpdateService.indexName$, null); const [queryError, setQueryError] = useState(''); @@ -41,6 +46,7 @@ export const QueryBar = () => { return share?.url.locators.get('DISCOVER_APP_LOCATOR'); }, [share?.url.locators]); + // Only used as fallback if onOpenIndexInDiscover is not provided const discoverLink = isIndexCreated && esqlDiscoverQuery ? discoverLocator?.getRedirectUrl({ @@ -51,6 +57,28 @@ export const QueryBar = () => { }) : null; + const openInDiscover = useCallback( + (e: React.MouseEvent) => { + indexEditorTelemetryService.trackQueryThisIndexClicked(searchQuery); + + // If onOpenIndexInDiscover is provided, we let that handler to manage the navigation to Discover + // If not, the button href will be executed + if (onOpenIndexInDiscover && indexName && esqlDiscoverQuery) { + e.preventDefault(); + const onExitCallback = () => onOpenIndexInDiscover(indexName, esqlDiscoverQuery); + indexUpdateService.exit(onExitCallback); + } + }, + [ + indexEditorTelemetryService, + searchQuery, + onOpenIndexInDiscover, + indexName, + esqlDiscoverQuery, + indexUpdateService, + ] + ); + if (!dataView) { return null; } @@ -81,14 +109,15 @@ export const QueryBar = () => { + {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} indexEditorTelemetryService.trackQueryThisIndexClicked(searchQuery)} + size="s" + color="text" + isDisabled={!isIndexCreated || !esqlDiscoverQuery} + onClick={openInDiscover} + href={discoverLink || undefined} target="_blank" - iconType={'discoverApp'} + iconType="discoverApp" > (null); public readonly error$: Observable = this._error$.asObservable(); - private readonly _exitAttemptWithUnsavedChanges$ = new BehaviorSubject(false); + private readonly _exitAttemptWithUnsavedChanges$ = new BehaviorSubject<{ + isActive: boolean; + onExitCallback?: () => void; + }>({ isActive: false }); + public readonly exitAttemptWithUnsavedChanges$ = this._exitAttemptWithUnsavedChanges$.asObservable(); @@ -965,8 +969,8 @@ export class IndexUpdateService { this.telemetry.trackEditInteraction({ actionType: 'delete_column' }); } - public setExitAttemptWithUnsavedChanges(value: boolean) { - this._exitAttemptWithUnsavedChanges$.next(value); + public setExitAttemptWithUnsavedChanges(isActive: boolean, onExitCallback?: () => void) { + this._exitAttemptWithUnsavedChanges$.next({ isActive, onExitCallback }); } public discardUnsavedChanges() { @@ -1059,16 +1063,17 @@ export class IndexUpdateService { return lookupIndexesResult.indices.some((index) => index.name === indexName); } - public exit() { + public exit(onExitCallback?: () => void) { const hasUnsavedChanges = this._hasUnsavedChanges$.getValue(); const unsavedColumns = this._pendingColumnsToBeSaved$ .getValue() .filter((col) => !isPlaceholderColumn(col.name)); if (hasUnsavedChanges || unsavedColumns.length > 0) { - this.setExitAttemptWithUnsavedChanges(true); + this.setExitAttemptWithUnsavedChanges(true, onExitCallback); } else { this.destroy(); + onExitCallback?.(); } } } diff --git a/src/platform/packages/private/kbn-index-editor/src/types.ts b/src/platform/packages/private/kbn-index-editor/src/types.ts index 53e4e024c8f4a..ccb6548b4ae14 100644 --- a/src/platform/packages/private/kbn-index-editor/src/types.ts +++ b/src/platform/packages/private/kbn-index-editor/src/types.ts @@ -32,6 +32,7 @@ export interface EditLookupIndexContentContext { /** Indicates if new fields have been added to the index */ indexHasNewFields: boolean; }) => void; + onOpenIndexInDiscover?: (indexName: string, esqlQuery: string) => Promise; } export interface EditLookupIndexFlyoutDeps { diff --git a/src/platform/plugins/shared/discover/public/application/main/components/top_nav/discover_topnav.tsx b/src/platform/plugins/shared/discover/public/application/main/components/top_nav/discover_topnav.tsx index fd917233fc375..deb42b96c835c 100644 --- a/src/platform/plugins/shared/discover/public/application/main/components/top_nav/discover_topnav.tsx +++ b/src/platform/plugins/shared/discover/public/application/main/components/top_nav/discover_topnav.tsx @@ -99,6 +99,18 @@ export const DiscoverTopNav = ({ onUpdateESQLQuery: stateContainer.actions.updateESQLQuery, }); + const onOpenQueryInNewTab = useCallback( + async (tabName: string, esqlQuery: string) => { + dispatch( + internalStateActions.openInNewTab({ + tabLabel: tabName, + appState: { query: { esql: esqlQuery } }, + }) + ); + }, + [dispatch] + ); + useEffect(() => { return () => { // Make sure to close the editors when unmounting @@ -316,6 +328,7 @@ export const DiscoverTopNav = ({ } : undefined } + onOpenQueryInNewTab={tabsEnabled ? onOpenQueryInNewTab : undefined} /> {isESQLToDataViewTransitionModalVisible && ( diff --git a/src/platform/plugins/shared/unified_search/public/query_string_input/query_bar_top_row.tsx b/src/platform/plugins/shared/unified_search/public/query_string_input/query_bar_top_row.tsx index e89229d637e79..1babf3fa55b3b 100644 --- a/src/platform/plugins/shared/unified_search/public/query_string_input/query_bar_top_row.tsx +++ b/src/platform/plugins/shared/unified_search/public/query_string_input/query_bar_top_row.tsx @@ -232,6 +232,10 @@ export interface QueryBarTopRowProps */ controlsWrapper: React.ReactNode; }; + /** + * Optional ES|QL prop - Callback function invoked to open the given ES|QL query in a new Discover tab + */ + onOpenQueryInNewTab?: ESQLEditorProps['onOpenQueryInNewTab']; useBackgroundSearchButton?: boolean; showProjectPicker?: boolean; } @@ -919,6 +923,7 @@ export const QueryBarTopRow = React.memo( : undefined } esqlVariables={props.esqlVariablesConfig?.esqlVariables ?? []} + onOpenQueryInNewTab={props.onOpenQueryInNewTab} /> ) ); diff --git a/src/platform/plugins/shared/unified_search/public/search_bar/create_search_bar.tsx b/src/platform/plugins/shared/unified_search/public/search_bar/create_search_bar.tsx index abc1477d2baee..d8c66179e60f6 100644 --- a/src/platform/plugins/shared/unified_search/public/search_bar/create_search_bar.tsx +++ b/src/platform/plugins/shared/unified_search/public/search_bar/create_search_bar.tsx @@ -279,6 +279,7 @@ export function createSearchBar({ esqlEditorInitialState={props.esqlEditorInitialState} onEsqlEditorInitialStateChange={props.onEsqlEditorInitialStateChange} esqlVariablesConfig={props.esqlVariablesConfig} + onOpenQueryInNewTab={props.onOpenQueryInNewTab} useBackgroundSearchButton={props.useBackgroundSearchButton} /> diff --git a/src/platform/plugins/shared/unified_search/public/search_bar/search_bar.tsx b/src/platform/plugins/shared/unified_search/public/search_bar/search_bar.tsx index f236e0d5a1894..f5fe619f6a966 100644 --- a/src/platform/plugins/shared/unified_search/public/search_bar/search_bar.tsx +++ b/src/platform/plugins/shared/unified_search/public/search_bar/search_bar.tsx @@ -162,6 +162,9 @@ export interface SearchBarOwnProps { */ esqlVariablesConfig?: QueryBarTopRowProps['esqlVariablesConfig']; + /** Optional configurations for the lookup join index editor */ + onOpenQueryInNewTab?: QueryBarTopRowProps['onOpenQueryInNewTab']; + esqlEditorInitialState?: QueryBarTopRowProps['esqlEditorInitialState']; onEsqlEditorInitialStateChange?: QueryBarTopRowProps['onEsqlEditorInitialStateChange']; @@ -782,6 +785,7 @@ export class SearchBarUI ex esqlEditorInitialState={this.props.esqlEditorInitialState} onEsqlEditorInitialStateChange={this.props.onEsqlEditorInitialStateChange} esqlVariablesConfig={this.props.esqlVariablesConfig} + onOpenQueryInNewTab={this.props.onOpenQueryInNewTab} useBackgroundSearchButton={this.props.useBackgroundSearchButton} showProjectPicker={this.props.showProjectPicker} />