-
Notifications
You must be signed in to change notification settings - Fork 894
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[query assist] use badge to show agent errors (#7998)
- enable query assist for index patterns without data source - add agent error parsing utils - update ml-commons response schema processing - previously ml-commons returns error.body as a string, now it is a JSON object. Ideally frontend should keep it as is to reduce serializing/deserializing, but since older version of ml-commons can be used through MDS, we'll keep it as a string for consistency - use badge to show query assist error if possible - add unit tests Signed-off-by: Joshua Li <[email protected]>
- Loading branch information
1 parent
7aecd73
commit 461a395
Showing
18 changed files
with
613 additions
and
31 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
47 changes: 47 additions & 0 deletions
47
..._enhancements/public/query_assist/components/__snapshots__/query_assist_bar.test.tsx.snap
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
218 changes: 218 additions & 0 deletions
218
src/plugins/query_enhancements/public/query_assist/components/query_assist_bar.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,218 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'; | ||
import React, { ComponentProps, PropsWithChildren } from 'react'; | ||
import { IntlProvider } from 'react-intl'; | ||
import { of } from 'rxjs'; | ||
import { QueryAssistBar } from '.'; | ||
import { notificationServiceMock, uiSettingsServiceMock } from '../../../../../core/public/mocks'; | ||
import { DataStorage } from '../../../../data/common'; | ||
import { QueryEditorExtensionDependencies, QueryStringContract } from '../../../../data/public'; | ||
import { dataPluginMock } from '../../../../data/public/mocks'; | ||
import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; | ||
import { setData, setStorage } from '../../services'; | ||
import { useGenerateQuery } from '../hooks'; | ||
import { AgentError, ProhibitedQueryError } from '../utils'; | ||
import { QueryAssistInput } from './query_assist_input'; | ||
|
||
jest.mock('../../../../opensearch_dashboards_react/public', () => ({ | ||
useOpenSearchDashboards: jest.fn(), | ||
withOpenSearchDashboards: jest.fn((component: React.Component) => component), | ||
})); | ||
|
||
jest.mock('../hooks', () => ({ | ||
useGenerateQuery: jest.fn().mockReturnValue({ generateQuery: jest.fn(), loading: false }), | ||
})); | ||
|
||
jest.mock('./query_assist_input', () => ({ | ||
QueryAssistInput: ({ inputRef, error }: ComponentProps<typeof QueryAssistInput>) => ( | ||
<> | ||
<input ref={inputRef} /> | ||
<div>{JSON.stringify(error)}</div> | ||
</> | ||
), | ||
})); | ||
|
||
const dataMock = dataPluginMock.createStartContract(true); | ||
const queryStringMock = dataMock.query.queryString as jest.Mocked<QueryStringContract>; | ||
const uiSettingsMock = uiSettingsServiceMock.createStartContract(); | ||
const notificationsMock = notificationServiceMock.createStartContract(); | ||
|
||
setData(dataMock); | ||
setStorage(new DataStorage(window.localStorage, 'mock-prefix')); | ||
|
||
const dependencies: QueryEditorExtensionDependencies = { | ||
language: 'PPL', | ||
onSelectLanguage: jest.fn(), | ||
isCollapsed: false, | ||
setIsCollapsed: jest.fn(), | ||
}; | ||
|
||
type Props = ComponentProps<typeof QueryAssistBar>; | ||
|
||
const IntlWrapper = ({ children }: PropsWithChildren<unknown>) => ( | ||
<IntlProvider locale="en">{children}</IntlProvider> | ||
); | ||
|
||
const renderQueryAssistBar = (overrideProps: Partial<Props> = {}) => { | ||
const props: Props = Object.assign<Props, Partial<Props>>({ dependencies }, overrideProps); | ||
const component = render(<QueryAssistBar {...props} />, { | ||
wrapper: IntlWrapper, | ||
}); | ||
return { component, props: props as jest.MockedObjectDeep<Props> }; | ||
}; | ||
|
||
describe('QueryAssistBar', () => { | ||
beforeEach(() => { | ||
(useOpenSearchDashboards as jest.Mock).mockReturnValue({ | ||
services: { | ||
data: dataMock, | ||
uiSettings: uiSettingsMock, | ||
notifications: notificationsMock, | ||
}, | ||
}); | ||
}); | ||
afterEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
it('renders null if collapsed', () => { | ||
const { component } = renderQueryAssistBar({ | ||
dependencies: { ...dependencies, isCollapsed: true }, | ||
}); | ||
expect(component.container).toBeEmptyDOMElement(); | ||
}); | ||
|
||
it('matches snapshot', () => { | ||
const { component } = renderQueryAssistBar(); | ||
expect(component.container).toMatchSnapshot(); | ||
}); | ||
|
||
it('displays callout when query input is empty on submit', async () => { | ||
renderQueryAssistBar(); | ||
|
||
fireEvent.click(screen.getByRole('button')); | ||
|
||
await waitFor(() => { | ||
expect(screen.getByTestId('query-assist-empty-query-callout')).toBeInTheDocument(); | ||
}); | ||
}); | ||
|
||
it('displays callout when dataset is not selected on submit', async () => { | ||
queryStringMock.getQuery.mockReturnValueOnce({ query: '', language: 'kuery' }); | ||
queryStringMock.getUpdates$.mockReturnValueOnce(of({ query: '', language: 'kuery' })); | ||
renderQueryAssistBar(); | ||
|
||
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'test query' } }); | ||
fireEvent.click(screen.getByRole('button')); | ||
|
||
await waitFor(() => { | ||
expect(screen.getByTestId('query-assist-empty-index-callout')).toBeInTheDocument(); | ||
}); | ||
}); | ||
|
||
it('displays callout for guardrail errors', async () => { | ||
const generateQueryMock = jest.fn().mockResolvedValue({ error: new ProhibitedQueryError() }); | ||
(useGenerateQuery as jest.Mock).mockReturnValue({ | ||
generateQuery: generateQueryMock, | ||
loading: false, | ||
}); | ||
|
||
renderQueryAssistBar(); | ||
|
||
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'test query' } }); | ||
fireEvent.click(screen.getByRole('button')); | ||
|
||
await waitFor(() => { | ||
expect(screen.getByTestId('query-assist-guard-callout')).toBeInTheDocument(); | ||
}); | ||
}); | ||
|
||
it('passes agent errors to input', async () => { | ||
const generateQueryMock = jest.fn().mockResolvedValue({ | ||
error: new AgentError({ | ||
error: { type: 'mock-type', reason: 'mock-reason', details: 'mock-details' }, | ||
status: 303, | ||
}), | ||
}); | ||
(useGenerateQuery as jest.Mock).mockReturnValue({ | ||
generateQuery: generateQueryMock, | ||
loading: false, | ||
}); | ||
|
||
renderQueryAssistBar(); | ||
|
||
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'test query' } }); | ||
fireEvent.click(screen.getByRole('button')); | ||
|
||
await waitFor(() => { | ||
expect(screen.getByText(/mock-reason/)).toBeInTheDocument(); | ||
}); | ||
}); | ||
|
||
it('displays toast for other unknown errors', async () => { | ||
const mockError = new Error('mock-error'); | ||
const generateQueryMock = jest.fn().mockResolvedValue({ | ||
error: mockError, | ||
}); | ||
(useGenerateQuery as jest.Mock).mockReturnValue({ | ||
generateQuery: generateQueryMock, | ||
loading: false, | ||
}); | ||
|
||
renderQueryAssistBar(); | ||
|
||
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'test query' } }); | ||
fireEvent.click(screen.getByRole('button')); | ||
|
||
await waitFor(() => { | ||
expect(notificationsMock.toasts.addError).toHaveBeenCalledWith(mockError, { | ||
title: 'Failed to generate results', | ||
}); | ||
}); | ||
}); | ||
|
||
it('submits a valid query and updates services', async () => { | ||
const generateQueryMock = jest | ||
.fn() | ||
.mockResolvedValue({ response: { query: 'generated query' } }); | ||
(useGenerateQuery as jest.Mock).mockReturnValue({ | ||
generateQuery: generateQueryMock, | ||
loading: false, | ||
}); | ||
|
||
renderQueryAssistBar(); | ||
|
||
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'test query' } }); | ||
fireEvent.click(screen.getByRole('button')); | ||
|
||
await waitFor(() => { | ||
expect(generateQueryMock).toHaveBeenCalledWith({ | ||
question: 'test query', | ||
index: 'Default Index Pattern', | ||
language: 'PPL', | ||
dataSourceId: 'mock-data-source-id', | ||
}); | ||
}); | ||
|
||
expect(queryStringMock.setQuery).toHaveBeenCalledWith({ | ||
dataset: { | ||
dataSource: { | ||
id: 'mock-data-source-id', | ||
title: 'Default Data Source', | ||
type: 'OpenSearch', | ||
}, | ||
id: 'default-index-pattern', | ||
timeFieldName: '@timestamp', | ||
title: 'Default Index Pattern', | ||
type: 'INDEX_PATTERN', | ||
}, | ||
language: 'PPL', | ||
query: 'generated query', | ||
}); | ||
expect(screen.getByTestId('query-assist-query-generated-callout')).toBeInTheDocument(); | ||
}); | ||
}); |
Oops, something went wrong.