diff --git a/plugins-extra/query_enhancements/common/index.ts b/plugins-extra/query_enhancements/common/index.ts index 1d434bcc74d8..a191a2e35d45 100644 --- a/plugins-extra/query_enhancements/common/index.ts +++ b/plugins-extra/query_enhancements/common/index.ts @@ -18,4 +18,6 @@ export const OPENSEARCH_DATACONNECTIONS_API = { export const JOBS_ENDPOINT_BASE = '/_plugins/_async_query'; +export const BASE_ML_COMMONS_URI = '/_plugins/_ml'; + export * from './utils'; diff --git a/plugins-extra/query_enhancements/public/plugin.tsx b/plugins-extra/query_enhancements/public/plugin.tsx index 256142023c38..5dba1c768926 100644 --- a/plugins-extra/query_enhancements/public/plugin.tsx +++ b/plugins-extra/query_enhancements/public/plugin.tsx @@ -52,7 +52,7 @@ export class QueryEnhancementsPlugin initialTo: moment().add(2, 'days').toISOString(), }, showFilterBar: false, - extensions: [createQueryAssistExtension(core.http)], + extensions: [createQueryAssistExtension(core.http, 'PPL')], }, fields: { visualizable: false, diff --git a/plugins-extra/query_enhancements/public/query_assist/components/call_outs.tsx b/plugins-extra/query_enhancements/public/query_assist/components/call_outs.tsx index f98569f42572..fdbe901a18cc 100644 --- a/plugins-extra/query_enhancements/public/query_assist/components/call_outs.tsx +++ b/plugins-extra/query_enhancements/public/query_assist/components/call_outs.tsx @@ -1,8 +1,9 @@ import { EuiCallOut, EuiCallOutProps } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; import React from 'react'; -type CalloutDismiss = Required>; -interface QueryAssistCallOutProps extends CalloutDismiss { +interface QueryAssistCallOutProps extends Required> { + language: string; type: QueryAssistCallOutType; } @@ -14,10 +15,12 @@ export type QueryAssistCallOutType = | 'empty_index' | 'query_generated'; -const EmptyIndexCallOut: React.FC = (props) => ( +const EmptyIndexCallOut: React.FC = (props) => ( = (props) => ( /> ); -const ProhibitedQueryCallOut: React.FC = (props) => ( +const ProhibitedQueryCallOut: React.FC = (props) => ( = (props) => ( /> ); -const EmptyQueryCallOut: React.FC = (props) => ( +const EmptyQueryCallOut: React.FC = (props) => ( = (props) => ( /> ); -const PPLGeneratedCallOut: React.FC = (props) => ( +const QueryGeneratedCallOut: React.FC = (props) => ( = (props) => ( export const QueryAssistCallOut: React.FC = (props) => { switch (props.type) { case 'empty_query': - return ; + return ; case 'empty_index': - return ; + return ; case 'invalid_query': - return ; + return ; case 'query_generated': - return ; + return ; default: break; } diff --git a/plugins-extra/query_enhancements/public/query_assist/components/query_assist_bar.tsx b/plugins-extra/query_enhancements/public/query_assist/components/query_assist_bar.tsx index c80fe5d09377..904382c1899e 100644 --- a/plugins-extra/query_enhancements/public/query_assist/components/query_assist_bar.tsx +++ b/plugins-extra/query_enhancements/public/query_assist/components/query_assist_bar.tsx @@ -1,5 +1,5 @@ import { EuiFlexGroup, EuiFlexItem, EuiForm, EuiFormRow } from '@elastic/eui'; -import React, { SyntheticEvent, useEffect, useMemo, useRef, useState } from 'react'; +import React, { SyntheticEvent, useMemo, useRef, useState } from 'react'; import { IDataPluginServices, PersistedLog } from '../../../../../src/plugins/data/public'; import { SearchBarExtensionDependencies } from '../../../../../src/plugins/data/public/ui/search_bar_extensions/search_bar_extension'; import { useOpenSearchDashboards } from '../../../../../src/plugins/opensearch_dashboards_react/public'; @@ -11,6 +11,7 @@ import { QueryAssistInput } from './query_assist_input'; import { QueryAssistSubmitButton } from './submit_button'; interface QueryAssistInputProps { + language: string; dependencies: SearchBarExtensionDependencies; } @@ -25,17 +26,12 @@ export const QueryAssistBar: React.FC = (props) => { const { generateQuery, loading } = useGenerateQuery(); const [callOutType, setCallOutType] = useState(); const dismissCallout = () => setCallOutType(undefined); - const mounted = useRef(false); - const selectedIndex = props.dependencies.indexPatterns?.at(0)?.title; + const selectedIndexPattern = props.dependencies.indexPatterns?.at(0); + const selectedIndex = + selectedIndexPattern && + (typeof selectedIndexPattern === 'string' ? selectedIndexPattern : selectedIndexPattern.title); const previousQuestionRef = useRef(); - useEffect(() => { - mounted.current = true; - return () => { - mounted.current = false; - }; - }, []); - const onSubmit = async (e: SyntheticEvent) => { e.preventDefault(); if (!inputRef.current?.value) { @@ -52,10 +48,9 @@ export const QueryAssistBar: React.FC = (props) => { const params = { question: inputRef.current.value, index: selectedIndex, - language: 'PPL', + language: props.language, }; const { response, error } = await generateQuery(params); - if (!mounted.current) return; if (error) { if (error instanceof ProhibitedQueryError) { setCallOutType('invalid_query'); @@ -89,7 +84,7 @@ export const QueryAssistBar: React.FC = (props) => { - + ); }; diff --git a/plugins-extra/query_enhancements/public/query_assist/components/query_assist_input.tsx b/plugins-extra/query_enhancements/public/query_assist/components/query_assist_input.tsx index 735ff555372a..476e2d1b7f57 100644 --- a/plugins-extra/query_enhancements/public/query_assist/components/query_assist_input.tsx +++ b/plugins-extra/query_enhancements/public/query_assist/components/query_assist_input.tsx @@ -20,18 +20,49 @@ export const QueryAssistInput: React.FC = (props) => { const [suggestionIndex, setSuggestionIndex] = useState(null); const [value, setValue] = useState(props.initialValue ?? ''); - const recentSearchSuggestions = useMemo(() => { + const sampleDataSuggestions = useMemo(() => { + switch (props.selectedIndex) { + case 'opensearch_dashboards_sample_data_ecommerce': + return [ + 'How many unique customers placed orders this week?', + 'Count the number of orders grouped by manufacturer and category', + 'find customers with first names like Eddie', + ]; + + case 'opensearch_dashboards_sample_data_logs': + return [ + 'Are there any errors in my logs?', + 'How many requests were there grouped by response code last week?', + "What's the average request size by week?", + ]; + + case 'opensearch_dashboards_sample_data_flights': + return [ + 'how many flights were there this week grouped by destination country?', + 'what were the longest flight delays this week?', + 'what carriers have the furthest flights?', + ]; + + default: + return []; + } + }, [props.selectedIndex]); + + const suggestions = useMemo(() => { if (!props.persistedLog) return []; return props.persistedLog .get() - .filter((recentSearch) => recentSearch.includes(value)) - .map((recentSearch) => ({ + .concat(sampleDataSuggestions) + .filter( + (suggestion, i, array) => array.indexOf(suggestion) === i && suggestion.includes(value) + ) + .map((suggestion) => ({ type: QuerySuggestionTypes.RecentSearch, - text: recentSearch, + text: suggestion, start: 0, end: value.length, })); - }, [props.persistedLog, value]); + }, [props.persistedLog, value, sampleDataSuggestions]); return ( setIsSuggestionsVisible(false)}> @@ -54,7 +85,7 @@ export const QueryAssistInput: React.FC = (props) => { { if (!props.inputRef.current) return; diff --git a/plugins-extra/query_enhancements/public/query_assist/components/submit_button.tsx b/plugins-extra/query_enhancements/public/query_assist/components/submit_button.tsx index 3b7a3c88fda8..2896a2e7d4ca 100644 --- a/plugins-extra/query_enhancements/public/query_assist/components/submit_button.tsx +++ b/plugins-extra/query_enhancements/public/query_assist/components/submit_button.tsx @@ -13,7 +13,7 @@ export const QueryAssistSubmitButton: React.FC = (props) => { isDisabled={props.isDisabled} size="s" type="submit" - aria-label="submit-question" + aria-label="Submit question to query assistant" /> ); }; diff --git a/plugins-extra/query_enhancements/public/query_assist/hooks/use_generate.ts b/plugins-extra/query_enhancements/public/query_assist/hooks/use_generate.ts index 0d23114fbd34..5365a7ca18f9 100644 --- a/plugins-extra/query_enhancements/public/query_assist/hooks/use_generate.ts +++ b/plugins-extra/query_enhancements/public/query_assist/hooks/use_generate.ts @@ -5,15 +5,26 @@ import { QueryAssistParameters, QueryAssistResponse } from '../../../common/quer import { formatError } from '../utils'; export const useGenerateQuery = () => { + const mounted = useRef(false); const [loading, setLoading] = useState(false); const abortControllerRef = useRef(); const { services } = useOpenSearchDashboards(); - useEffect(() => () => abortControllerRef.current?.abort(), []); + useEffect(() => { + mounted.current = true; + return () => { + mounted.current = false; + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + abortControllerRef.current = undefined; + } + }; + }, []); const generateQuery = async ( params: QueryAssistParameters ): Promise<{ response?: QueryAssistResponse; error?: Error }> => { + abortControllerRef.current?.abort(); abortControllerRef.current = new AbortController(); setLoading(true); try { @@ -24,12 +35,13 @@ export const useGenerateQuery = () => { signal: abortControllerRef.current?.signal, } ); - return { response }; + if (mounted.current) return { response }; } catch (error) { - return { error: formatError(error) }; + if (mounted.current) return { error: formatError(error) }; } finally { - setLoading(false); + if (mounted.current) setLoading(false); } + return {}; }; return { generateQuery, loading, abortControllerRef }; diff --git a/plugins-extra/query_enhancements/public/query_assist/utils/create_extension.tsx b/plugins-extra/query_enhancements/public/query_assist/utils/create_extension.tsx index f2f046d11f61..ef3bcb7fc335 100644 --- a/plugins-extra/query_enhancements/public/query_assist/utils/create_extension.tsx +++ b/plugins-extra/query_enhancements/public/query_assist/utils/create_extension.tsx @@ -3,22 +3,27 @@ import { HttpSetup } from 'opensearch-dashboards/public'; import { QueryAssistBar } from '../components'; import { SearchBarExtensionConfig } from '../../../../../src/plugins/data/public/ui/search_bar_extensions'; -export const createQueryAssistExtension = (http: HttpSetup): SearchBarExtensionConfig => { +export const createQueryAssistExtension = ( + http: HttpSetup, + language: string +): SearchBarExtensionConfig => { return { - id: 'query-assist-ppl', + id: 'query-assist', order: 1000, isEnabled: (() => { let agentConfigured: boolean; return async () => { if (agentConfigured === undefined) { agentConfigured = await http - .get<{ configured: boolean }>('/api/ql/query_assist/configured/PPL') + .get<{ configured: boolean }>(`/api/ql/query_assist/configured/${language}`) .then((response) => response.configured) .catch(() => false); } return agentConfigured; }; })(), - getComponent: (dependencies) => , + getComponent: (dependencies) => ( + + ), }; }; diff --git a/plugins-extra/query_enhancements/server/routes/query_assist/agents.ts b/plugins-extra/query_enhancements/server/routes/query_assist/agents.ts index 676ea01bab5c..f0a8dcfe382f 100644 --- a/plugins-extra/query_enhancements/server/routes/query_assist/agents.ts +++ b/plugins-extra/query_enhancements/server/routes/query_assist/agents.ts @@ -1,8 +1,8 @@ import { ApiResponse } from '@opensearch-project/opensearch'; import { RequestBody, TransportRequestPromise } from '@opensearch-project/opensearch/lib/Transport'; import { RequestHandlerContext } from 'src/core/server'; +import { BASE_ML_COMMONS_URI } from '../../../common'; -const ML_COMMONS_API_PREFIX = '/_plugins/_ml'; const AGENT_REQUEST_OPTIONS = { /** * It is time-consuming for LLM to generate final answer @@ -30,7 +30,7 @@ export const getAgentIdByConfig = async ( try { const response = (await client.transport.request({ method: 'GET', - path: `${ML_COMMONS_API_PREFIX}/config/${configName}`, + path: `${BASE_ML_COMMONS_URI}/config/${configName}`, })) as ApiResponse<{ type: string; configuration: { agent_id?: string } }>; if (!response || response.body.configuration.agent_id === undefined) { @@ -54,7 +54,7 @@ export const requestAgentByConfig = async (options: { return client.transport.request( { method: 'POST', - path: `${ML_COMMONS_API_PREFIX}/agents/${agentId}/_execute`, + path: `${BASE_ML_COMMONS_URI}/agents/${agentId}/_execute`, body, }, AGENT_REQUEST_OPTIONS diff --git a/src/plugins/data/public/ui/query_editor/_query_editor.scss b/src/plugins/data/public/ui/query_editor/_query_editor.scss index 96d1d5c04264..1103762e0593 100644 --- a/src/plugins/data/public/ui/query_editor/_query_editor.scss +++ b/src/plugins/data/public/ui/query_editor/_query_editor.scss @@ -8,6 +8,13 @@ width: 500px; } +.osdQueryEditorHeader { + max-height: 400px; + + // TODO fix styling: with "overflow: auto" the scroll bar appears although the content is below max-height + // overflow: auto; +} + @include euiBreakpoint("xs", "s") { .osdQueryEditor--withDatePicker { > :first-child { 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 88211841f335..dc428cb3fe78 100644 --- a/src/plugins/data/public/ui/query_editor/query_editor.tsx +++ b/src/plugins/data/public/ui/query_editor/query_editor.tsx @@ -55,6 +55,7 @@ export interface QueryEditorProps { className?: string; isInvalid?: boolean; queryEditorHeaderRef: React.RefObject; + queryEditorHeaderClassName?: string; } interface Props extends QueryEditorProps { @@ -492,6 +493,11 @@ export default class QueryEditorUI extends Component { // ); const className = classNames(this.props.className); + const queryEditorHeaderClassName = classNames( + 'osdQueryEditorHeader', + this.props.queryEditorHeaderClassName + ); + const queryLanguageSwitcher = ( { -
+
); } diff --git a/src/plugins/data/public/ui/search_bar_extensions/index.ts b/src/plugins/data/public/ui/search_bar_extensions/index.ts deleted file mode 100644 index d14971c671e3..000000000000 --- a/src/plugins/data/public/ui/search_bar_extensions/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -export { SearchBarExtensionConfig } from './search_bar_extension'; -export { SearchBarExtensions } from './search_bar_extensions'; diff --git a/src/plugins/data/public/ui/search_bar_extensions/index.tsx b/src/plugins/data/public/ui/search_bar_extensions/index.tsx new file mode 100644 index 000000000000..ab790aa655c9 --- /dev/null +++ b/src/plugins/data/public/ui/search_bar_extensions/index.tsx @@ -0,0 +1,17 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { ComponentProps } from 'react'; + +const Fallback = () =>
; + +const LazySearchBarExtensions = React.lazy(() => import('./search_bar_extensions')); +export const SearchBarExtensions = (props: ComponentProps) => ( + }> + + +); + +export { SearchBarExtensionConfig } from './search_bar_extension'; diff --git a/src/plugins/data/public/ui/search_bar_extensions/search_bar_extension.test.tsx b/src/plugins/data/public/ui/search_bar_extensions/search_bar_extension.test.tsx index 5abbe0200f27..194b92ca9bfa 100644 --- a/src/plugins/data/public/ui/search_bar_extensions/search_bar_extension.test.tsx +++ b/src/plugins/data/public/ui/search_bar_extensions/search_bar_extension.test.tsx @@ -8,10 +8,9 @@ import React, { ComponentProps } from 'react'; import { IIndexPattern } from '../../../common'; import { SearchBarExtension } from './search_bar_extension'; -jest.mock('@elastic/eui', () => ({ - ...jest.requireActual('@elastic/eui'), - EuiPortal: jest.fn(({ children }) =>
{children}
), - EuiErrorBoundary: jest.fn(({ children }) =>
{children}
), +jest.mock('react-dom', () => ({ + ...jest.requireActual('react-dom'), + createPortal: jest.fn((element) => element), })); type SearchBarExtensionProps = ComponentProps; @@ -45,7 +44,7 @@ describe('SearchBarExtension', () => { dependencies: { indexPatterns: [mockIndexPattern], }, - portalInsert: { sibling: document.createElement('div'), position: 'after' }, + portalContainer: document.createElement('div'), }; beforeEach(() => { @@ -78,17 +77,4 @@ describe('SearchBarExtension', () => { expect(isEnabledMock).toHaveBeenCalled(); }); - - it('calls isEnabled and getComponent correctly', async () => { - isEnabledMock.mockResolvedValue(true); - getComponentMock.mockReturnValue(
Test Component
); - - render(); - - await waitFor(() => { - expect(isEnabledMock).toHaveBeenCalled(); - }); - - expect(getComponentMock).toHaveBeenCalledWith(defaultProps.dependencies); - }); }); diff --git a/src/plugins/data/public/ui/search_bar_extensions/search_bar_extension.tsx b/src/plugins/data/public/ui/search_bar_extensions/search_bar_extension.tsx index 88a3fcdfbb08..505846c66b08 100644 --- a/src/plugins/data/public/ui/search_bar_extensions/search_bar_extension.tsx +++ b/src/plugins/data/public/ui/search_bar_extensions/search_bar_extension.tsx @@ -3,15 +3,15 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { EuiErrorBoundary, EuiPortal } from '@elastic/eui'; -import { EuiPortalProps } from '@opensearch-project/oui'; -import React, { useEffect, useMemo, useState } from 'react'; +import { EuiErrorBoundary } from '@elastic/eui'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import ReactDOM from 'react-dom'; import { IIndexPattern } from '../../../common'; interface SearchBarExtensionProps { config: SearchBarExtensionConfig; dependencies: SearchBarExtensionDependencies; - portalInsert: EuiPortalProps['insert']; + portalContainer: Element; } export interface SearchBarExtensionDependencies { @@ -34,7 +34,7 @@ export interface SearchBarExtensionConfig { * A function that determines if the search bar extension is enabled and should be rendered on UI. * @returns whether the extension is enabled. */ - isEnabled: () => Promise; + isEnabled: (dependencies: SearchBarExtensionDependencies) => Promise; /** * A function that returns the mount point for the search bar extension. * @param dependencies - The dependencies required for the extension. @@ -45,6 +45,7 @@ export interface SearchBarExtensionConfig { export const SearchBarExtension: React.FC = (props) => { const [isEnabled, setIsEnabled] = useState(false); + const isMounted = useRef(true); const component = useMemo(() => props.config.getComponent(props.dependencies), [ props.config, @@ -52,14 +53,22 @@ export const SearchBarExtension: React.FC = (props) => ]); useEffect(() => { - props.config.isEnabled().then(setIsEnabled); + isMounted.current = true; + return () => { + isMounted.current = false; + }; + }, []); + + useEffect(() => { + props.config.isEnabled(props.dependencies).then((enabled) => { + if (isMounted.current) setIsEnabled(enabled); + }); }, [props.dependencies, props.config]); if (!isEnabled) return null; - return ( - - {component} - + return ReactDOM.createPortal( + {component}, + props.portalContainer ); }; diff --git a/src/plugins/data/public/ui/search_bar_extensions/search_bar_extensions.test.tsx b/src/plugins/data/public/ui/search_bar_extensions/search_bar_extensions.test.tsx index 52b3b87dc419..2c11db0a56f8 100644 --- a/src/plugins/data/public/ui/search_bar_extensions/search_bar_extensions.test.tsx +++ b/src/plugins/data/public/ui/search_bar_extensions/search_bar_extensions.test.tsx @@ -6,7 +6,7 @@ import { render, waitFor } from '@testing-library/react'; import React, { ComponentProps } from 'react'; import { SearchBarExtension } from './search_bar_extension'; -import { SearchBarExtensions } from './search_bar_extensions'; +import SearchBarExtensions from './search_bar_extensions'; type SearchBarExtensionProps = ComponentProps; type SearchBarExtensionsProps = ComponentProps; @@ -15,32 +15,30 @@ jest.mock('./search_bar_extension', () => ({ SearchBarExtension: jest.fn(({ config, dependencies }: SearchBarExtensionProps) => (
Mocked SearchBarExtension {config.id} with{' '} - {dependencies.indexPatterns?.map((i) => i.title).join(', ')} + {dependencies.indexPatterns?.map((i) => (typeof i === 'string' ? i : i.title)).join(', ')}
)), })); describe('SearchBarExtensions', () => { const defaultProps: SearchBarExtensionsProps = { - dependencies: { - indexPatterns: [ - { - id: '1234', - title: 'logstash-*', - fields: [ - { - name: 'response', - type: 'number', - esTypes: ['integer'], - aggregatable: true, - filterable: true, - searchable: true, - }, - ], - }, - ], - }, - portalInsert: { sibling: document.createElement('div'), position: 'after' }, + indexPatterns: [ + { + id: '1234', + title: 'logstash-*', + fields: [ + { + name: 'response', + type: 'number', + esTypes: ['integer'], + aggregatable: true, + filterable: true, + searchable: true, + }, + ], + }, + ], + portalContainer: document.createElement('div'), }; beforeEach(() => { @@ -89,7 +87,7 @@ describe('SearchBarExtensions', () => { expect(SearchBarExtension).toHaveBeenCalledWith( expect.objectContaining({ - dependencies: defaultProps.dependencies, + dependencies: { indexPatterns: defaultProps.indexPatterns }, }), expect.anything() ); diff --git a/src/plugins/data/public/ui/search_bar_extensions/search_bar_extensions.tsx b/src/plugins/data/public/ui/search_bar_extensions/search_bar_extensions.tsx index cef34e46fa28..2ae444c334d1 100644 --- a/src/plugins/data/public/ui/search_bar_extensions/search_bar_extensions.tsx +++ b/src/plugins/data/public/ui/search_bar_extensions/search_bar_extensions.tsx @@ -3,7 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { EuiPortalProps } from '@elastic/eui'; import React, { useMemo } from 'react'; import { SearchBarExtension, @@ -11,37 +10,42 @@ import { SearchBarExtensionDependencies, } from './search_bar_extension'; -interface SearchBarExtensionsProps { +interface SearchBarExtensionsProps extends SearchBarExtensionDependencies { configs?: SearchBarExtensionConfig[]; - dependencies: SearchBarExtensionDependencies; - portalInsert: EuiPortalProps['insert']; + portalContainer: Element; } -export const SearchBarExtensions: React.FC = (props) => { - const configs = useMemo(() => { - if (!props.configs) return []; +const SearchBarExtensions: React.FC = React.memo((props) => { + const { configs, portalContainer, ...dependencies } = props; + + const sortedConfigs = useMemo(() => { + if (!configs) return []; const seenIds = new Set(); - props.configs.forEach((config) => { + configs.forEach((config) => { if (seenIds.has(config.id)) { throw new Error(`Duplicate search bar extension id '${config.id}' found.`); } seenIds.add(config.id); }); - return [...props.configs].sort((a, b) => a.order - b.order); - }, [props.configs]); + return [...configs].sort((a, b) => a.order - b.order); + }, [configs]); return ( <> - {configs.map((config) => ( + {sortedConfigs.map((config) => ( ))} ); -}; +}); + +// Needed for React.lazy +// eslint-disable-next-line import/no-default-export +export default SearchBarExtensions;