diff --git a/public/components/common/search/__tests__/__snapshots__/search.test.tsx.snap b/public/components/common/search/__tests__/__snapshots__/search.test.tsx.snap index 7380a86ca..b820a9ca3 100644 --- a/public/components/common/search/__tests__/__snapshots__/search.test.tsx.snap +++ b/public/components/common/search/__tests__/__snapshots__/search.test.tsx.snap @@ -349,399 +349,524 @@ exports[`Explorer Search component renders basic component 1`] = ` handleTimePickerChange={[Function]} handleTimeRangePickerRefresh={[Function]} > - - - - +
- + + } + >
- + -
- + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="m" + > +
- + + - -
-
- -
+ + + + + + + + + +
+
+ + +
+ } + iconType={false} + isCustom={true} + startDateControl={
} + >
- } - iconType={false} - isCustom={true} - startDateControl={
} + +
+
+ +
+
+ +
+
+ +
+ + + + + + -
- - - - - - -
+
+ + + + + +
+ - - - - + + + + diff --git a/public/components/common/search/date_picker.tsx b/public/components/common/search/date_picker.tsx index af9154550..1a12f4cb3 100644 --- a/public/components/common/search/date_picker.tsx +++ b/public/components/common/search/date_picker.tsx @@ -3,63 +3,25 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { EuiSuperDatePicker, EuiToolTip } from '@elastic/eui'; +import { EuiSuperDatePicker } from '@elastic/eui'; import React from 'react'; -import { i18n } from '@osd/i18n'; import { uiSettingsService } from '../../../../common/utils'; -import { coreRefs } from '../../../framework/core_refs'; import { IDatePickerProps } from './search'; -import { - QUERY_ASSIST_END_TIME, - QUERY_ASSIST_START_TIME, -} from '../../../../common/constants/shared'; export function DatePicker(props: IDatePickerProps) { - const { - startTime, - endTime, - handleTimePickerChange, - handleTimeRangePickerRefresh, - isAppAnalytics, - } = props; + const { startTime, endTime, handleTimePickerChange, handleTimeRangePickerRefresh } = props; const handleTimeChange = (e: any) => handleTimePickerChange([e.start, e.end]); - let finalizedStartTime; - let finalizedEndTime; - let setDisabled; - let toolTipMessage; - - if (coreRefs.queryAssistEnabled && !isAppAnalytics) { - // is query assistant inside log explorer - finalizedStartTime = QUERY_ASSIST_START_TIME; - finalizedEndTime = QUERY_ASSIST_END_TIME; - setDisabled = true; - toolTipMessage = i18n.translate('discover.queryAssistant.timePickerDisabledMessage', { - defaultMessage: 'Date range has been disabled to accomodate timerange of all datasets', - }); - } else { - finalizedStartTime = startTime; - finalizedEndTime = endTime; - setDisabled = false; - toolTipMessage = false; - } - return ( - <> - - - - + ); } diff --git a/public/components/common/search/query_area.tsx b/public/components/common/search/query_area.tsx index 16d44e0cb..2345f0b2c 100644 --- a/public/components/common/search/query_area.tsx +++ b/public/components/common/search/query_area.tsx @@ -10,6 +10,9 @@ import { QueryAssistInput } from '../../event_analytics/explorer/query_assist/in import { useFetchEvents } from '../../event_analytics/hooks/use_fetch_events'; import './query_area.scss'; +/** + * QueryArea is currently used for query assist only. + */ export function QueryArea({ tabId, handleQueryChange, @@ -39,6 +42,7 @@ export function QueryArea({ memoizedGetAvailableFields(indexQuery); }, [selectedIndex, memoizedGetAvailableFields, memoizedHandleQueryChange]); const [lastFocusedInput, setLastFocusedInput] = useState<'query_area' | 'nlq_input'>('nlq_input'); + const [callOut, setCallOut] = useState(null); const queryEditor = ( { handleQueryChange(query); + setCallOut(null); // query is considered updated when the last run query is not the same as whats in the editor - // setUpdatedQuery(runQuery !== query); setNeedsUpdate(runQuery !== query); }} onFocus={() => setLastFocusedInput('query_area')} @@ -90,6 +94,8 @@ export function QueryArea({ lastFocusedInput={lastFocusedInput} setLastFocusedInput={setLastFocusedInput} runChanges={runChanges} + callOut={callOut} + setCallOut={setCallOut} > {queryEditor} diff --git a/public/components/common/search/search.tsx b/public/components/common/search/search.tsx index 1fff9b1c2..824539aee 100644 --- a/public/components/common/search/search.tsx +++ b/public/components/common/search/search.tsx @@ -354,7 +354,7 @@ export const Search = (props: any) => { placeholder="Select an index" isClearable={true} prepend={Index} - singleSelection={true} + singleSelection={{ asPlainText: true }} isLoading={loading} options={indicesAndIndexPatterns} selectedOptions={selectedIndex} @@ -412,7 +412,7 @@ export const Search = (props: any) => { {!(queryRedux.selectedTimestamp === '' && queryResults?.datarows) && ( // index with no timestamp, dont show timepicker - {!isLiveTailOn && ( + {!isLiveTailOn && !coreRefs.queryAssistEnabled && ( +
+
+ + + Enter a natural language question to automatically generate a query to view results. + +
+
+ +`; + +exports[`Callouts spec PPLGeneratedCallOut should match snapshot 1`] = ` +
+
+
+ + + PPL query generated + +
+ +
+
+`; + +exports[`Callouts spec ProhibitedQueryCallOut should match snapshot 1`] = ` +
+
+
+ + + I am unable to respond to this query. Try another question. + +
+
+
+`; diff --git a/public/components/event_analytics/explorer/query_assist/__tests__/callouts.test.tsx b/public/components/event_analytics/explorer/query_assist/__tests__/callouts.test.tsx new file mode 100644 index 000000000..bfe945a66 --- /dev/null +++ b/public/components/event_analytics/explorer/query_assist/__tests__/callouts.test.tsx @@ -0,0 +1,40 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiCallOutProps } from '@elastic/eui'; +import { render } from '@testing-library/react'; +import React from 'react'; +import { EmptyQueryCallOut, PPLGeneratedCallOut, ProhibitedQueryCallOut } from '../callouts'; + +const renderCallouts = ( + Component: React.FC, + overrideProps: Partial> = {} +) => { + const props: Pick = Object.assign( + { + onDismiss: jest.fn(), + }, + overrideProps + ); + const component = render(); + return { component, props }; +}; + +describe('Callouts spec', () => { + test('ProhibitedQueryCallOut should match snapshot', () => { + const { component } = renderCallouts(ProhibitedQueryCallOut); + expect(component.container).toMatchSnapshot(); + }); + + test('EmptyQueryCallOut should match snapshot', () => { + const { component } = renderCallouts(EmptyQueryCallOut); + expect(component.container).toMatchSnapshot(); + }); + + test('PPLGeneratedCallOut should match snapshot', () => { + const { component } = renderCallouts(PPLGeneratedCallOut); + expect(component.container).toMatchSnapshot(); + }); +}); diff --git a/public/components/event_analytics/explorer/query_assist/__tests__/input.test.tsx b/public/components/event_analytics/explorer/query_assist/__tests__/input.test.tsx index 31eb1c872..8cb78acb5 100644 --- a/public/components/event_analytics/explorer/query_assist/__tests__/input.test.tsx +++ b/public/components/event_analytics/explorer/query_assist/__tests__/input.test.tsx @@ -13,6 +13,7 @@ import * as coreServices from '../../../../../../common/utils/core_services'; import { coreRefs } from '../../../../../framework/core_refs'; import { rootReducer } from '../../../../../framework/redux/reducers'; import { initialTabId } from '../../../../../framework/redux/store/shared_state'; +import { PPLGeneratedCallOut, ProhibitedQueryCallOut } from '../callouts'; import { QueryAssistInput } from '../input'; const renderQueryAssistInput = ( @@ -20,7 +21,10 @@ const renderQueryAssistInput = ( ) => { const preloadedState = {}; const store = configureStore({ reducer: rootReducer, preloadedState }); - const props: ComponentProps = Object.assign( + const props: jest.Mocked> = Object.assign< + ComponentProps, + Partial> + >( { handleQueryChange: jest.fn(), handleTimeRangePickerRefresh: jest.fn(), @@ -29,6 +33,8 @@ const renderQueryAssistInput = ( selectedIndex: [{ label: 'selected-test-index' }], nlqInput: 'test-input', setNlqInput: jest.fn(), + callOut: null, + setCallOut: jest.fn(), handleTimePickerChange: jest.fn(), }, overrideProps @@ -68,6 +74,12 @@ describe(' spec', () => { body: '{"question":"test-input","index":"selected-test-index"}', }); expect(props.handleQueryChange).toBeCalledWith('source = index'); + expect(props.setCallOut.mock.calls[0][0]).toBeNull(); + expect(props.setCallOut.mock.calls[1][0]).toEqual( + expect.objectContaining({ + type: PPLGeneratedCallOut, + }) + ); }); it('should display toast for generate errors', async () => { @@ -140,7 +152,7 @@ describe(' spec', () => { body: { statusCode: 400, message: ERROR_DETAILS.GUARDRAILS_TRIGGERED }, }); - const { component } = renderQueryAssistInput(); + const { component, props } = renderQueryAssistInput(); await waitFor(() => { // splitbutton data-test-subj doesn't work in Oui 1.5, this should be query-assist-generate-and-run-button fireEvent.click(component.getByText('Generate and run')); @@ -149,6 +161,11 @@ describe(' spec', () => { expect(httpMock.post).toBeCalledWith(QUERY_ASSIST_API.GENERATE_PPL, { body: '{"question":"test-input","index":"selected-test-index"}', }); - expect(component.getByTestId('query-assist-guard-callout')).toBeInTheDocument(); + expect(props.setCallOut.mock.calls[0][0]).toBeNull(); + expect(props.setCallOut.mock.calls[1][0]).toEqual( + expect.objectContaining({ + type: ProhibitedQueryCallOut, + }) + ); }); }); diff --git a/public/components/event_analytics/explorer/query_assist/callouts.tsx b/public/components/event_analytics/explorer/query_assist/callouts.tsx new file mode 100644 index 000000000..056ce3098 --- /dev/null +++ b/public/components/event_analytics/explorer/query_assist/callouts.tsx @@ -0,0 +1,45 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiCallOut, EuiCallOutProps } from '@elastic/eui'; +import React from 'react'; + +type QueryAssistCallOutProps = Pick; + +export const ProhibitedQueryCallOut: React.FC = (props) => ( + +); + +export const EmptyQueryCallOut: React.FC = (props) => ( + +); + +export const PPLGeneratedCallOut: React.FC = (props) => ( + +); diff --git a/public/components/event_analytics/explorer/query_assist/input.tsx b/public/components/event_analytics/explorer/query_assist/input.tsx index 87d476437..d67a93bbe 100644 --- a/public/components/event_analytics/explorer/query_assist/input.tsx +++ b/public/components/event_analytics/explorer/query_assist/input.tsx @@ -5,7 +5,6 @@ import { EuiButton, - EuiCallOut, EuiComboBoxOptionOption, EuiFieldText, EuiFlexGroup, @@ -36,6 +35,7 @@ import { } from '../../redux/slices/query_assistant_summarization_slice'; import { reset, selectQueryResult } from '../../redux/slices/query_result_slice'; import { changeQuery, selectQueries } from '../../redux/slices/query_slice'; +import { EmptyQueryCallOut, PPLGeneratedCallOut, ProhibitedQueryCallOut } from './callouts'; class ProhibitedQueryError extends Error { constructor(message?: string) { @@ -43,6 +43,23 @@ class ProhibitedQueryError extends Error { } } +const formatError = (error: ResponseError | Error): Error => { + if ('body' in error) { + if (error.body.statusCode === 429) + return { + ...error.body, + message: 'Request is throttled. Try again later or contact your administrator', + } as Error; + if ( + error.body.statusCode === 400 && + error.body.message.includes(ERROR_DETAILS.GUARDRAILS_TRIGGERED) + ) + return new ProhibitedQueryError(error.body.message); + return error.body as Error; + } + return error; +}; + interface SummarizationContext { question: string; query?: string; @@ -62,6 +79,8 @@ interface Props { setNlqInput: React.Dispatch>; lastFocusedInput: 'query_area' | 'nlq_input'; setLastFocusedInput: React.Dispatch>; + callOut: React.ReactNode | null; + setCallOut: React.Dispatch>; runChanges: () => void; } @@ -88,36 +107,6 @@ const HARDCODED_SUGGESTIONS: Record = { ], }; -const prohibitedQueryCallOut = ( - -); - -const emptyQueryCallOut = ( - -); - -const pplGenerated = ( - -); - export const QueryAssistInput: React.FC> = (props) => { // @ts-ignore const queryRedux = useSelector(selectQueries)[props.tabId]; @@ -154,7 +143,7 @@ export const QueryAssistInput: React.FC> = (props const [isPopoverOpen, setIsPopoverOpen] = useState(false); // below is only used for url redirection const [autoRun, setAutoRun] = useState(false); - const [callOut, setCallOut] = useState(null); + const dismissCallOut = () => props.setCallOut(null); useEffect(() => { if (autoRun) { @@ -185,42 +174,27 @@ export const QueryAssistInput: React.FC> = (props }, }) ); - setCallOut(pplGenerated); + props.setCallOut(); return generatedPPL; }; - const formatError = (error: ResponseError | Error): Error => { - if ('body' in error) { - if (error.body.statusCode === 429) - return { - ...error.body, - message: 'Request is throttled. Try again later or contact your administrator', - } as Error; - if ( - error.body.statusCode === 400 && - error.body.message.includes(ERROR_DETAILS.GUARDRAILS_TRIGGERED) - ) - return new ProhibitedQueryError(error.body.message); - return error.body as Error; - } - return error; - }; + // used by generate query button const generatePPL = async () => { dispatch(reset({ tabId: props.tabId })); dispatch(resetSummary({ tabId: props.tabId })); if (!props.selectedIndex.length) return; if (props.nlqInput.trim().length === 0) { - setCallOut(emptyQueryCallOut); + props.setCallOut(); return; } try { dispatch(setLoading({ tabId: props.tabId, loading: true })); - setCallOut(null); + dismissCallOut(); await request(); } catch (err) { const error = formatError(err); if (error instanceof ProhibitedQueryError) { - setCallOut(prohibitedQueryCallOut); + props.setCallOut(); return; } coreRefs.toasts?.addError(error, { title: 'Failed to generate results' }); @@ -274,7 +248,7 @@ export const QueryAssistInput: React.FC> = (props } catch (err) { const error = formatError(err); if (error instanceof ProhibitedQueryError) { - setCallOut(prohibitedQueryCallOut); + props.setCallOut(); return; } coreRefs.toasts?.addError(error, { title: 'Failed to summarize results' }); @@ -301,19 +275,19 @@ export const QueryAssistInput: React.FC> = (props dispatch(resetSummary({ tabId: props.tabId })); if (!props.selectedIndex.length) return; if (props.nlqInput.trim().length === 0) { - setCallOut(emptyQueryCallOut); + props.setCallOut(); return; } try { dispatch(setLoading({ tabId: props.tabId, loading: true })); - setCallOut(null); + dismissCallOut(); await request(); await props.handleTimePickerChange([QUERY_ASSIST_START_TIME, 'now']); await props.handleTimeRangePickerRefresh(undefined, true); } catch (err) { const error = formatError(err); if (error instanceof ProhibitedQueryError) { - setCallOut(prohibitedQueryCallOut); + props.setCallOut(); return; } if (coreRefs.summarizeEnabled) { @@ -343,7 +317,7 @@ export const QueryAssistInput: React.FC> = (props value={props.nlqInput} onChange={(e) => { props.setNlqInput(e.target.value); - setCallOut(null); + dismissCallOut(); }} onKeyDown={(e) => { // listen to enter key manually. the cursor jumps to CodeEditor with EuiForm's onSubmit @@ -380,8 +354,8 @@ export const QueryAssistInput: React.FC> = (props
- {callOut} - {props.children && } + {props.callOut} + {props.children} {props.lastFocusedInput === 'query_area' ? (