diff --git a/changelogs/fragments/8716.yml b/changelogs/fragments/8716.yml new file mode 100644 index 000000000000..b3dbab4289b0 --- /dev/null +++ b/changelogs/fragments/8716.yml @@ -0,0 +1,2 @@ +feat: +- Adds data2summary agent check in data summary panel in discover. ([#8716](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8716)) \ No newline at end of file diff --git a/src/plugins/query_enhancements/common/constants.ts b/src/plugins/query_enhancements/common/constants.ts index f116da2f9724..15f59e35d53e 100644 --- a/src/plugins/query_enhancements/common/constants.ts +++ b/src/plugins/query_enhancements/common/constants.ts @@ -7,6 +7,7 @@ export const PLUGIN_ID = 'queryEnhancements'; export const PLUGIN_NAME = 'queryEnhancements'; export const BASE_API = '/api/enhancements'; +export const BASE_API_ASSISTANT = '/api/assistant'; export const DATASET = { S3: 'S3', @@ -34,6 +35,9 @@ export const API = { ASYNC_JOBS: `${BASE_API}/jobs`, CONNECTIONS: `${BASE_API}/connections`, }, + AGENT_API: { + CONFIG_EXISTS: `${BASE_API_ASSISTANT}/agent_config/_exists`, + }, }; export const URI = { diff --git a/src/plugins/query_enhancements/public/plugin.tsx b/src/plugins/query_enhancements/public/plugin.tsx index 715ba53f0153..9bcae328c74d 100644 --- a/src/plugins/query_enhancements/public/plugin.tsx +++ b/src/plugins/query_enhancements/public/plugin.tsx @@ -182,7 +182,6 @@ export class QueryEnhancementsPlugin ], }; queryString.getLanguageService().registerLanguage(sqlLanguageConfig); - data.__enhance({ editor: { queryEditorExtension: createQueryAssistExtension( @@ -201,10 +200,10 @@ export class QueryEnhancementsPlugin public start( core: CoreStart, - deps: QueryEnhancementsPluginStartDependencies + { data }: QueryEnhancementsPluginStartDependencies ): QueryEnhancementsPluginStart { setStorage(this.storage); - setData(deps.data); + setData(data); return {}; } diff --git a/src/plugins/query_enhancements/public/query_assist/components/query_assist_summary.test.tsx b/src/plugins/query_enhancements/public/query_assist/components/query_assist_summary.test.tsx index 09059494b48a..5a512ba21c14 100644 --- a/src/plugins/query_enhancements/public/query_assist/components/query_assist_summary.test.tsx +++ b/src/plugins/query_enhancements/public/query_assist/components/query_assist_summary.test.tsx @@ -10,6 +10,7 @@ import { QueryAssistSummary, convertResult } from './query_assist_summary'; import { useQueryAssist } from '../hooks'; import { IDataFrame, Query } from '../../../../data/common'; import { FeedbackStatus as FEEDBACK } from '../../../common/query_assist'; +import { checkAgentsExist } from '../utils/get_is_summary_agent'; jest.mock('react', () => ({ ...jest.requireActual('react'), @@ -21,6 +22,8 @@ jest.mock('../hooks', () => ({ useQueryAssist: jest.fn(), })); +jest.mock('../utils/get_is_summary_agent', () => ({ checkAgentsExist: jest.fn() })); + describe('query assist summary', () => { const PPL = 'ppl'; const question = 'Are there any errors in my logs?'; @@ -42,6 +45,7 @@ describe('query assist summary', () => { const setSummary = jest.fn(); const setLoading = jest.fn(); const setFeedback = jest.fn(); + const setIsSummaryAgentAvailable = jest.fn(); const setIsAssistantEnabledByCapability = jest.fn(); const getQuery = jest.fn(); const dataMock = { @@ -58,6 +62,10 @@ describe('query assist summary', () => { }, }; + beforeEach(() => { + (checkAgentsExist as jest.Mock).mockResolvedValue({ exist: false }); + }); + afterEach(() => { data$.next(undefined); question$.next(''); @@ -71,13 +79,13 @@ describe('query assist summary', () => { CLICK: 'click', }, }; + const props: ComponentProps = { data: dataMock, http: httpMock, usageCollection: usageCollectionMock, dependencies: { isCollapsed: false, - isSummaryCollapsed: false, }, core: coreSetupMock, }; @@ -115,6 +123,7 @@ describe('query assist summary', () => { summary, loading, feedback, + isSummaryAgentAvailable = false, isAssistantEnabledByCapability = true, isQueryAssistCollapsed = COLLAPSED.NO ) => { @@ -125,6 +134,10 @@ describe('query assist summary', () => { isAssistantEnabledByCapability, setIsAssistantEnabledByCapability, ]); + React.useState.mockImplementationOnce(() => [ + isSummaryAgentAvailable, + setIsSummaryAgentAvailable, + ]); useQueryAssist.mockImplementationOnce(() => ({ question: 'question', question$, @@ -133,7 +146,7 @@ describe('query assist summary', () => { }; const defaultUseStateMock = () => { - mockUseState(null, LOADING.NO, FEEDBACK.NONE); + mockUseState(null, LOADING.NO, FEEDBACK.NONE, true); }; it('should not show if collapsed is true', () => { @@ -150,30 +163,48 @@ describe('query assist summary', () => { expect(summaryPanels).toHaveLength(0); }); - it('should not show if query assistant is collapsed', () => { - mockUseState(null, LOADING.NO, FEEDBACK.NONE, true, COLLAPSED.YES); + it('should not show if is not summary agent', () => { + (checkAgentsExist as jest.Mock).mockResolvedValue({ exist: false }); + mockUseState(null, LOADING.NO, FEEDBACK.NONE, false); renderQueryAssistSummary(COLLAPSED.NO); const summaryPanels = screen.queryAllByTestId('queryAssist__summary'); expect(summaryPanels).toHaveLength(0); }); + it('should not show if query assistant is collapsed', () => { + mockUseState(null, LOADING.NO, FEEDBACK.NONE, true); + renderQueryAssistSummary(COLLAPSED.YES); + const summaryPanels = screen.queryAllByTestId('queryAssist__summary'); + expect(summaryPanels).toHaveLength(0); + }); + it('should show if collapsed is false', () => { - defaultUseStateMock(); + (checkAgentsExist as jest.Mock).mockResolvedValue({ exist: true }); + mockUseState(null, LOADING.NO, FEEDBACK.NONE, true); + renderQueryAssistSummary(COLLAPSED.NO); + const summaryPanels = screen.queryAllByTestId('queryAssist__summary'); + expect(summaryPanels).toHaveLength(1); + }); + + it('should show if summary agent', () => { + (checkAgentsExist as jest.Mock).mockResolvedValue({ exist: true }); + mockUseState(null, LOADING.NO, FEEDBACK.NONE, true); renderQueryAssistSummary(COLLAPSED.NO); const summaryPanels = screen.queryAllByTestId('queryAssist__summary'); expect(summaryPanels).toHaveLength(1); }); it('should display loading view if loading state is true', () => { - mockUseState(null, LOADING.YES, FEEDBACK.NONE); + (checkAgentsExist as jest.Mock).mockResolvedValue({ exist: true }); + mockUseState(null, LOADING.YES, FEEDBACK.NONE, true); renderQueryAssistSummary(COLLAPSED.NO); - expect(screen.getByTestId('queryAssist_summary_loading')).toBeInTheDocument(); expect(screen.queryAllByTestId('queryAssist_summary_result')).toHaveLength(0); expect(screen.queryAllByTestId('queryAssist_summary_empty_text')).toHaveLength(0); }); it('should display loading view if loading state is true even with summary', () => { - mockUseState('summary', LOADING.YES, FEEDBACK.NONE); + (checkAgentsExist as jest.Mock).mockResolvedValue({ exist: true }); + mockUseState('summary', LOADING.YES, FEEDBACK.NONE, true); renderQueryAssistSummary(COLLAPSED.NO); expect(screen.getByTestId('queryAssist_summary_loading')).toBeInTheDocument(); expect(screen.queryAllByTestId('queryAssist_summary_result')).toHaveLength(0); @@ -181,6 +212,7 @@ describe('query assist summary', () => { }); it('should display initial view if loading state is false and no summary', () => { + (checkAgentsExist as jest.Mock).mockResolvedValue({ exist: true }); defaultUseStateMock(); renderQueryAssistSummary(COLLAPSED.NO); expect(screen.getByTestId('queryAssist_summary_empty_text')).toBeInTheDocument(); @@ -189,7 +221,8 @@ describe('query assist summary', () => { }); it('should display summary result', () => { - mockUseState('summary', LOADING.NO, FEEDBACK.NONE); + (checkAgentsExist as jest.Mock).mockResolvedValue({ exist: true }); + mockUseState('summary', LOADING.NO, FEEDBACK.NONE, true); renderQueryAssistSummary(COLLAPSED.NO); expect(screen.getByTestId('queryAssist_summary_result')).toBeInTheDocument(); expect(screen.getByTestId('queryAssist_summary_result')).toHaveTextContent('summary'); @@ -198,7 +231,8 @@ describe('query assist summary', () => { }); it('should report metric for thumbup click', async () => { - mockUseState('summary', LOADING.NO, FEEDBACK.NONE); + (checkAgentsExist as jest.Mock).mockResolvedValue({ exist: true }); + mockUseState('summary', LOADING.NO, FEEDBACK.NONE, true); renderQueryAssistSummary(COLLAPSED.NO); expect(screen.getByTestId('queryAssist_summary_result')).toBeInTheDocument(); await screen.getByTestId('queryAssist_summary_buttons_thumbup'); @@ -212,7 +246,8 @@ describe('query assist summary', () => { }); it('should report metric for thumbdown click', async () => { - mockUseState('summary', LOADING.NO, FEEDBACK.NONE); + (checkAgentsExist as jest.Mock).mockResolvedValue({ exist: true }); + mockUseState('summary', LOADING.NO, FEEDBACK.NONE, true); renderQueryAssistSummary(COLLAPSED.NO); expect(screen.getByTestId('queryAssist_summary_result')).toBeInTheDocument(); await screen.getByTestId('queryAssist_summary_buttons_thumbdown'); @@ -226,7 +261,8 @@ describe('query assist summary', () => { }); it('should hide thumbdown button if thumbup button is clicked', async () => { - mockUseState('summary', LOADING.NO, FEEDBACK.THUMB_UP); + (checkAgentsExist as jest.Mock).mockResolvedValue({ exist: true }); + mockUseState('summary', LOADING.NO, FEEDBACK.THUMB_UP, true); renderQueryAssistSummary(COLLAPSED.NO); expect(screen.getByTestId('queryAssist_summary_result')).toBeInTheDocument(); await screen.getByTestId('queryAssist_summary_buttons_thumbup'); @@ -234,7 +270,9 @@ describe('query assist summary', () => { }); it('should hide thumbup button if thumbdown button is clicked', async () => { - mockUseState('summary', LOADING.NO, FEEDBACK.THUMB_DOWN); + (checkAgentsExist as jest.Mock).mockResolvedValue({ exist: true }); + + mockUseState('summary', LOADING.NO, FEEDBACK.THUMB_DOWN, true); renderQueryAssistSummary(COLLAPSED.NO); expect(screen.getByTestId('queryAssist_summary_result')).toBeInTheDocument(); await screen.getByTestId('queryAssist_summary_buttons_thumbdown'); @@ -242,7 +280,8 @@ describe('query assist summary', () => { }); it('should not fetch summary if data is empty', async () => { - mockUseState(null, LOADING.NO, FEEDBACK.NONE); + (checkAgentsExist as jest.Mock).mockResolvedValue({ exist: true }); + mockUseState(null, LOADING.NO, FEEDBACK.NONE, true); renderQueryAssistSummary(COLLAPSED.NO); question$.next(question); query$.next({ query: PPL, language: 'PPL' }); @@ -251,7 +290,7 @@ describe('query assist summary', () => { }); it('should fetch summary with expected payload and response', async () => { - mockUseState('summary', LOADING.NO, FEEDBACK.NONE); + mockUseState('summary', LOADING.NO, FEEDBACK.NONE, true); const RESPONSE_TEXT = 'response'; httpMock.post.mockResolvedValue(RESPONSE_TEXT); renderQueryAssistSummary(COLLAPSED.NO); @@ -291,6 +330,7 @@ describe('query assist summary', () => { }); it('should not update queryResults if subscription changed not in order', async () => { + (checkAgentsExist as jest.Mock).mockResolvedValue({ exist: true }); mockUseState('summary', LOADING.NO, FEEDBACK.NONE); const RESPONSE_TEXT = 'response'; httpMock.post.mockResolvedValue(RESPONSE_TEXT); @@ -303,6 +343,7 @@ describe('query assist summary', () => { }); it('should update queryResults if subscriptions changed in order', async () => { + (checkAgentsExist as jest.Mock).mockResolvedValue({ exist: true }); mockUseState('summary', LOADING.NO, FEEDBACK.NONE); const RESPONSE_TEXT = 'response'; httpMock.post.mockResolvedValue(RESPONSE_TEXT); @@ -321,6 +362,7 @@ describe('query assist summary', () => { }); it('should reset feedback state if re-fetch summary', async () => { + (checkAgentsExist as jest.Mock).mockResolvedValue({ exist: true }); mockUseState('summary', LOADING.NO, FEEDBACK.THUMB_UP); const RESPONSE_TEXT = 'response'; httpMock.post.mockResolvedValue(RESPONSE_TEXT); diff --git a/src/plugins/query_enhancements/public/query_assist/components/query_assist_summary.tsx b/src/plugins/query_enhancements/public/query_assist/components/query_assist_summary.tsx index 9a20c98fcbbd..6a2ef50c34cb 100644 --- a/src/plugins/query_enhancements/public/query_assist/components/query_assist_summary.tsx +++ b/src/plugins/query_enhancements/public/query_assist/components/query_assist_summary.tsx @@ -30,6 +30,8 @@ import { QueryAssistContextType } from '../../../common/query_assist'; import sparkleHollowSvg from '../../assets/sparkle_hollow.svg'; import sparkleSolidSvg from '../../assets/sparkle_solid.svg'; import { FeedbackStatus } from '../../../common/query_assist'; +import { checkAgentsExist } from '../utils/get_is_summary_agent'; +import { DATA2SUMMARY_AGENT_CONFIG_ID } from '../utils/constant'; export interface QueryContext { question: string; @@ -70,6 +72,7 @@ export const QueryAssistSummary: React.FC = (props) => const [loading, setLoading] = useState(false); // track loading state const [feedback, setFeedback] = useState(FeedbackStatus.NONE); const [isEnabledByCapability, setIsEnabledByCapability] = useState(false); + const [isSummaryAgentAvailable, setIsSummaryAgentAvailable] = useState(false); const selectedDataset = useRef(query.queryString.getQuery()?.dataset); const { question$, isQueryAssistCollapsed } = useQueryAssist(); const METRIC_APP = `query-assist`; @@ -206,6 +209,26 @@ export const QueryAssistSummary: React.FC = (props) => }); }, [props.core]); + useEffect(() => { + setIsSummaryAgentAvailable(false); + const fetchSummaryAgent = async () => { + try { + const summaryAgentStatus = await checkAgentsExist( + props.http, + DATA2SUMMARY_AGENT_CONFIG_ID, + selectedDataset.current?.dataSource?.id + ); + setIsSummaryAgentAvailable(summaryAgentStatus.exists); + } catch (error) { + // eslint-disable-next-line no-console + console.error(error); + } + }; + if (isEnabledByCapability) { + fetchSummaryAgent(); + } + }, [selectedDataset.current?.dataSource?.id, props.http, isEnabledByCapability]); + const onFeedback = useCallback( (satisfied: boolean) => { if (feedback !== FeedbackStatus.NONE) return; @@ -216,8 +239,15 @@ export const QueryAssistSummary: React.FC = (props) => [feedback, reportMetric] ); - if (props.dependencies.isCollapsed || isQueryAssistCollapsed || !isEnabledByCapability) + if ( + props.dependencies.isCollapsed || + isQueryAssistCollapsed || + !isEnabledByCapability || + !isSummaryAgentAvailable + ) { return null; + } + const isDarkMode = props.core.uiSettings.get('theme:darkMode'); return ( { + const coreSetupMock = coreMock.createSetup({}); + const httpMock = coreSetupMock.http; + + it('should call http.get with one agentConfigName', async () => { + const agentConfigName = 'name1'; + const dataSourceId = 'testDataSourceId'; + const response = { exists: true }; + + httpMock.get.mockResolvedValue(response); + + const result = await checkAgentsExist(httpMock, agentConfigName, dataSourceId); + + expect(httpMock.get).toHaveBeenCalledWith(expect.any(String), { + query: { + agentConfigName: 'name1', + dataSourceId: 'testDataSourceId', + }, + }); + expect(result).toEqual(response); + }); + + it('should call http.get with agentConfigName array', async () => { + const agentConfigNames = ['name1', 'name2']; + const dataSourceId = 'testDataSourceId'; + const response = { exists: true }; + + httpMock.get.mockResolvedValue(response); + + const result = await checkAgentsExist(httpMock, agentConfigNames, dataSourceId); + + expect(httpMock.get).toHaveBeenCalledWith(expect.any(String), { + query: { + agentConfigName: agentConfigNames, + dataSourceId, + }, + }); + expect(result).toEqual(response); + }); + + it('should throw an error if http.get fails', async () => { + const agentConfigName = 'name1'; + const dataSourceId = 'testDataSourceId'; + + const error = new Error('api call error'); + httpMock.get.mockRejectedValue(error); + + await expect(checkAgentsExist(httpMock, agentConfigName, dataSourceId)).rejects.toThrow(error); + }); +}); diff --git a/src/plugins/query_enhancements/public/query_assist/utils/get_is_summary_agent.ts b/src/plugins/query_enhancements/public/query_assist/utils/get_is_summary_agent.ts new file mode 100644 index 000000000000..8e8f15550f5a --- /dev/null +++ b/src/plugins/query_enhancements/public/query_assist/utils/get_is_summary_agent.ts @@ -0,0 +1,18 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { HttpSetup } from 'opensearch-dashboards/public'; +import { API } from '../../../common/constants'; + +export async function checkAgentsExist( + http: HttpSetup, + agentConfigName: string | string[], + dataSourceId?: string +) { + const response = await http.get(API.AGENT_API.CONFIG_EXISTS, { + query: { agentConfigName, dataSourceId }, + }); + return response; +}