From 15b6ace45737fb6ce7c0bf3a42b142c408118d85 Mon Sep 17 00:00:00 2001 From: Srdjan Lulic Date: Fri, 13 Jun 2025 12:28:52 +0100 Subject: [PATCH 1/2] [Obs AI Assistant] Display warning banner when re-indexing is in progress (#222593) Closes #221837 ## Summary This PR adds a warning callout to the Assistant Page, Flyout and Knowledge Base tab, to make users aware when knowledge base is re-indexing. This will help avoid users' worry that their data is lost since it's not always shown during re-indexing. The banner relies on the `isReIndexing` flag from the knowledge base status. Once the state changes to `isReIndexing=false`, the banner disappears. ### Screenshots image image image #### When shown simultaneously with Elastic Managed LLM Callout image ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The `release_note:breaking` label should be applied in these situations. - [] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed -**I'm not sure about this one - let me know if there are any related tests I should execute here**. - [x] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) (cherry picked from commit 1d2ab08596bbf99f79e764455a8b3d4bba8443fe) # Conflicts: # x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_timeline.tsx --- .../kbn-ai-assistant/src/chat/chat_body.tsx | 9 +- .../src/chat/chat_timeline.tsx | 34 +++-- .../src/chat/welcome_message.test.tsx | 144 ++++++++++++++++++ .../src/chat/welcome_message.tsx | 4 + .../knowledge_base_reindexing_callout.tsx | 37 +++++ .../public/helpers/test_helper.tsx | 17 ++- .../components/knowledge_base_tab.test.tsx | 43 +++++- .../routes/components/knowledge_base_tab.tsx | 3 + 8 files changed, 271 insertions(+), 20 deletions(-) create mode 100644 x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/welcome_message.test.tsx create mode 100644 x-pack/platform/packages/shared/kbn-ai-assistant/src/knowledge_base/knowledge_base_reindexing_callout.tsx diff --git a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_body.tsx b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_body.tsx index fbc5696e85998..82c8659da31a6 100644 --- a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_body.tsx +++ b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_body.tsx @@ -33,6 +33,7 @@ import { type Feedback, aiAssistantSimulatedFunctionCalling, getElasticManagedLlmConnector, + KnowledgeBaseState, } from '@kbn/observability-ai-assistant-plugin/public'; import type { AuthenticatedUser } from '@kbn/security-plugin/common'; import { euiThemeVars } from '@kbn/ui-theme'; @@ -382,6 +383,11 @@ export function ChatBody({ !conversationCalloutDismissed && tourCalloutDismissed; + const showKnowledgeBaseReIndexingCallout = + knowledgeBase.status.value?.enabled === true && + knowledgeBase.status.value?.kbState === KnowledgeBaseState.READY && + knowledgeBase.status.value?.isReIndexing === true; + const isPublic = conversation.value?.public; const isArchived = !!conversation.value?.archived; const showPromptEditor = !isArchived && (!isPublic || isConversationOwnedByCurrentUser); @@ -523,12 +529,12 @@ export function ChatBody({ ) } showElasticLlmCalloutInChat={showElasticLlmCalloutInChat} + showKnowledgeBaseReIndexingCallout={showKnowledgeBaseReIndexingCallout} /> ) : ( )} diff --git a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_timeline.tsx b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_timeline.tsx index dc28e3dbdc0e6..589401dbe1ac0 100644 --- a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_timeline.tsx +++ b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_timeline.tsx @@ -7,7 +7,7 @@ import React, { type ReactNode, useMemo } from 'react'; import { css } from '@emotion/css'; -import { EuiCommentList } from '@elastic/eui'; +import { EuiCode, EuiCommentList, useEuiTheme } from '@elastic/eui'; import type { AuthenticatedUser } from '@kbn/security-plugin/common'; import { omit } from 'lodash'; import type { Message } from '@kbn/observability-ai-assistant-plugin/common'; @@ -18,11 +18,11 @@ import { type ObservabilityAIAssistantChatService, type TelemetryEventTypeWithPayload, } from '@kbn/observability-ai-assistant-plugin/public'; -import type { UseKnowledgeBaseResult } from '../hooks/use_knowledge_base'; import { ChatItem } from './chat_item'; import { ChatConsolidatedItems } from './chat_consolidated_items'; import { getTimelineItemsfromConversation } from '../utils/get_timeline_items_from_conversation'; import { ElasticLlmConversationCallout } from './elastic_llm_conversation_callout'; +import { KnowledgeBaseReindexingCallout } from '../knowledge_base/knowledge_base_reindexing_callout'; export interface ChatTimelineItem extends Pick { @@ -49,7 +49,6 @@ export interface ChatTimelineItem export interface ChatTimelineProps { conversationId?: string; messages: Message[]; - knowledgeBase: UseKnowledgeBaseResult; chatService: ObservabilityAIAssistantChatService; hasConnector: boolean; chatState: ChatState; @@ -57,6 +56,7 @@ export interface ChatTimelineProps { isArchived: boolean; currentUser?: Pick; showElasticLlmCalloutInChat: boolean; + showKnowledgeBaseReIndexingCallout: boolean; onEdit: (message: Message, messageAfterEdit: Message) => void; onFeedback: (feedback: Feedback) => void; onRegenerate: (message: Message) => void; @@ -75,12 +75,6 @@ const euiCommentListClassName = css` padding-bottom: 32px; `; -const stickyElasticLlmCalloutContainerClassName = css` - position: sticky; - top: 0; - z-index: 1; -`; - export function ChatTimeline({ conversationId, messages, @@ -90,6 +84,7 @@ export function ChatTimeline({ isConversationOwnedByCurrentUser, isArchived, showElasticLlmCalloutInChat, + showKnowledgeBaseReIndexingCallout, onEdit, onFeedback, onRegenerate, @@ -98,6 +93,18 @@ export function ChatTimeline({ onActionClick, chatState, }: ChatTimelineProps) { + + const { euiTheme } = useEuiTheme(); + const stickyCalloutContainerClassName = css` + position: sticky; + top: 0; + z-index: 1; + background: ${euiTheme.colors.backgroundBasePlain}; + &:empty { + display: none; + } + `; + const items = useMemo(() => { const timelineItems = getTimelineItemsfromConversation({ conversationId, @@ -145,11 +152,10 @@ export function ChatTimeline({ return ( - {showElasticLlmCalloutInChat ? ( -
- -
- ) : null} +
+ {showKnowledgeBaseReIndexingCallout ? : null} + {showElasticLlmCalloutInChat ? : null} +
{items.map((item, index) => { return Array.isArray(item) ? ( ({ + useKibana: () => ({ + services: { + triggersActionsUi: { + getAddConnectorFlyout: jest.fn(() => null), + }, + observabilityAIAssistant: { + service: { + getScreenContexts: jest.fn(() => []), + }, + useGenAIConnectors: jest.fn(() => mockConnectors), + }, + }, + }), +})); + +const createMockKnowledgeBase = ( + partial: Partial = {} +): UseKnowledgeBaseResult => ({ + isInstalling: false, + isPolling: false, + install: jest.fn(), + warmupModel: jest.fn(), + isWarmingUpModel: false, + status: { + value: { + enabled: true, + errorMessage: undefined, + kbState: KnowledgeBaseState.NOT_INSTALLED, + concreteWriteIndex: undefined, + currentInferenceId: undefined, + isReIndexing: false, + }, + loading: false, + error: undefined, + refresh: jest.fn(), + }, + ...partial, +}); + +describe('WelcomeMessage', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('renders a warning callout while knowledge base is re-indexing', async () => { + const knowledgeBase = createMockKnowledgeBase({ + status: { + value: { + kbState: KnowledgeBaseState.READY, + enabled: true, + concreteWriteIndex: 'my-index', + currentInferenceId: 'inference_id', + isReIndexing: true, + }, + loading: false, + error: undefined, + refresh: jest.fn(), + }, + }); + + const { rerender } = render( + + ); + + // Callout is shown during re-indexing + expect(screen.queryByText(/Re-indexing in progress/i)).toBeInTheDocument(); + expect( + screen.queryByText(/Knowledge base is currently being re-indexed./i) + ).toBeInTheDocument(); + + // Knowledge base finished re-indexing + const updatedKnowledgeBase = createMockKnowledgeBase({ + status: { + value: { + kbState: KnowledgeBaseState.READY, + enabled: true, + concreteWriteIndex: 'my-index', + currentInferenceId: 'inference_id', + isReIndexing: false, + }, + loading: false, + error: undefined, + refresh: jest.fn(), + }, + }); + + await act(async () => { + rerender( + + ); + }); + + // Callout is no longer shown + expect(screen.queryByText(/Re-indexing in progress/i)).not.toBeInTheDocument(); + expect( + screen.queryByText(/Knowledge base is currently being re-indexed./i) + ).not.toBeInTheDocument(); + }); +}); diff --git a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/welcome_message.tsx b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/welcome_message.tsx index 29d024d8d0ffd..0b9d5d31f2d36 100644 --- a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/welcome_message.tsx +++ b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/welcome_message.tsx @@ -20,6 +20,7 @@ import { WelcomeMessageKnowledgeBase } from '../knowledge_base/welcome_message_k import { StarterPrompts } from './starter_prompts'; import { useKibana } from '../hooks/use_kibana'; import { ElasticLlmConversationCallout } from './elastic_llm_conversation_callout'; +import { KnowledgeBaseReindexingCallout } from '../knowledge_base/knowledge_base_reindexing_callout'; const fullHeightClassName = css` height: 100%; @@ -34,11 +35,13 @@ export function WelcomeMessage({ connectors, knowledgeBase, showElasticLlmCalloutInChat, + showKnowledgeBaseReIndexingCallout, onSelectPrompt, }: { connectors: UseGenAIConnectorsResult; knowledgeBase: UseKnowledgeBaseResult; showElasticLlmCalloutInChat: boolean; + showKnowledgeBaseReIndexingCallout: boolean; onSelectPrompt: (prompt: string) => void; }) { const breakpoint = useCurrentEuiBreakpoint(); @@ -78,6 +81,7 @@ export function WelcomeMessage({ gutterSize="none" className={fullHeightClassName} > + {showKnowledgeBaseReIndexingCallout ? : null} {showElasticLlmCalloutInChat ? : null} diff --git a/x-pack/platform/packages/shared/kbn-ai-assistant/src/knowledge_base/knowledge_base_reindexing_callout.tsx b/x-pack/platform/packages/shared/kbn-ai-assistant/src/knowledge_base/knowledge_base_reindexing_callout.tsx new file mode 100644 index 0000000000000..111a08e1d3b2e --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-ai-assistant/src/knowledge_base/knowledge_base_reindexing_callout.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { css } from '@emotion/css'; +import { EuiCallOut, useEuiTheme } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export const KnowledgeBaseReindexingCallout = () => { + const { euiTheme } = useEuiTheme(); + + const knowledgeBaseReindexingCalloutName = css` + margin-bottom: ${euiTheme.size.s}; + width: 100%; + `; + + return ( + + {i18n.translate('xpack.aiAssistant.knowledgeBase.reindexingCalloutBody', { + defaultMessage: + 'Knowledge base is currently being re-indexed. If you have entries, some may be unavailable until the operation completes.', + })} + + ); +}; diff --git a/x-pack/solutions/observability/plugins/observability_ai_assistant_management/public/helpers/test_helper.tsx b/x-pack/solutions/observability/plugins/observability_ai_assistant_management/public/helpers/test_helper.tsx index e520cc4052b59..133ad49e5c6b2 100644 --- a/x-pack/solutions/observability/plugins/observability_ai_assistant_management/public/helpers/test_helper.tsx +++ b/x-pack/solutions/observability/plugins/observability_ai_assistant_management/public/helpers/test_helper.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { createMemoryHistory } from 'history'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { render as testLibRender } from '@testing-library/react'; +import { RenderResult, render as testLibRender } from '@testing-library/react'; import { coreMock } from '@kbn/core/public/mocks'; import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; import { observabilityAIAssistantPluginMock } from '@kbn/observability-ai-assistant-plugin/public/mock'; @@ -41,7 +41,7 @@ const queryClient = new QueryClient({ export const render = ( component: React.ReactNode, mocks?: { coreStart?: DeepPartial; appContextValue?: AppContextValue } -) => { +): RenderResult => { const history = createMemoryHistory(); const startDeps = { @@ -76,7 +76,7 @@ export const render = ( }, }; - return testLibRender( + const TestWrapper = ({ children }: { children: React.ReactNode }) => ( // @ts-ignore @@ -87,7 +87,7 @@ export const render = ( history={history} router={aIAssistantManagementObservabilityRouter as any} > - {component} + {children} @@ -95,4 +95,13 @@ export const render = ( ); + + const renderResult = testLibRender(component, { wrapper: TestWrapper }); + + return { + ...renderResult, + rerender: (newComponent: React.ReactNode) => { + renderResult.rerender(newComponent); + }, + }; }; diff --git a/x-pack/solutions/observability/plugins/observability_ai_assistant_management/public/routes/components/knowledge_base_tab.test.tsx b/x-pack/solutions/observability/plugins/observability_ai_assistant_management/public/routes/components/knowledge_base_tab.test.tsx index f282ddfe51962..c06bed775bd60 100644 --- a/x-pack/solutions/observability/plugins/observability_ai_assistant_management/public/routes/components/knowledge_base_tab.test.tsx +++ b/x-pack/solutions/observability/plugins/observability_ai_assistant_management/public/routes/components/knowledge_base_tab.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { fireEvent } from '@testing-library/react'; +import { act, fireEvent } from '@testing-library/react'; import { KnowledgeBaseState } from '@kbn/observability-ai-assistant-plugin/public'; import { useGenAIConnectors, useKnowledgeBase } from '@kbn/ai-assistant/src/hooks'; import { render } from '../../helpers/test_helper'; @@ -104,6 +104,47 @@ describe('KnowledgeBaseTab', () => { }); }); + describe('when the knowledge base is re-indexing', () => { + beforeEach(() => { + useKnowledgeBaseMock.mockReturnValue({ + status: { + value: { + kbState: KnowledgeBaseState.READY, + enabled: true, + isReIndexing: true, + }, + }, + isInstalling: false, + install: jest.fn(), + }); + }); + + it('should show a warning callout while re-indexing is in progress', async () => { + const { getByTestId, queryByTestId, rerender } = render(); + expect(getByTestId('knowledgeBaseReindexingCallOut')).toBeInTheDocument(); + + // Re-indexing completed + useKnowledgeBaseMock.mockReturnValue({ + status: { + value: { + kbState: KnowledgeBaseState.READY, + enabled: true, + isReIndexing: false, + }, + }, + isInstalling: false, + install: jest.fn(), + }); + + await act(async () => { + rerender(); + }); + + // Callout is no longer shown + expect(queryByTestId('knowledgeBaseReindexingCallOut')).not.toBeInTheDocument(); + }); + }); + describe('when the knowledge base is installed and ready', () => { beforeEach(() => { useKnowledgeBaseMock.mockReturnValue({ diff --git a/x-pack/solutions/observability/plugins/observability_ai_assistant_management/public/routes/components/knowledge_base_tab.tsx b/x-pack/solutions/observability/plugins/observability_ai_assistant_management/public/routes/components/knowledge_base_tab.tsx index 2907b53a4848f..9cf2dbdd75928 100644 --- a/x-pack/solutions/observability/plugins/observability_ai_assistant_management/public/routes/components/knowledge_base_tab.tsx +++ b/x-pack/solutions/observability/plugins/observability_ai_assistant_management/public/routes/components/knowledge_base_tab.tsx @@ -39,6 +39,7 @@ import { useKnowledgeBase } from '@kbn/ai-assistant/src/hooks'; import { KnowledgeBaseInstallationStatusPanel } from '@kbn/ai-assistant/src/knowledge_base/knowledge_base_installation_status_panel'; import { SettingUpKnowledgeBase } from '@kbn/ai-assistant/src/knowledge_base/setting_up_knowledge_base'; import { InspectKnowledgeBasePopover } from '@kbn/ai-assistant/src/knowledge_base/inspect_knowlegde_base_popover'; +import { KnowledgeBaseReindexingCallout } from '@kbn/ai-assistant/src/knowledge_base/knowledge_base_reindexing_callout'; import { useGetKnowledgeBaseEntries } from '../../hooks/use_get_knowledge_base_entries'; import { categorizeEntries, KnowledgeBaseEntryCategory } from '../../helpers/categorize_entries'; import { KnowledgeBaseEditManualEntryFlyout } from './knowledge_base_edit_manual_entry_flyout'; @@ -256,6 +257,8 @@ export function KnowledgeBaseTab() { return ( <> + {knowledgeBase.status.value?.isReIndexing && } + From cbbe7f17e2479c76e0a5116a77b11bcd8d56df7e Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 13 Jun 2025 13:29:57 +0000 Subject: [PATCH 2/2] [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' --- .../shared/kbn-ai-assistant/src/chat/chat_timeline.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_timeline.tsx b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_timeline.tsx index 589401dbe1ac0..2b0931ab1eb15 100644 --- a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_timeline.tsx +++ b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_timeline.tsx @@ -7,7 +7,7 @@ import React, { type ReactNode, useMemo } from 'react'; import { css } from '@emotion/css'; -import { EuiCode, EuiCommentList, useEuiTheme } from '@elastic/eui'; +import { EuiCommentList, useEuiTheme } from '@elastic/eui'; import type { AuthenticatedUser } from '@kbn/security-plugin/common'; import { omit } from 'lodash'; import type { Message } from '@kbn/observability-ai-assistant-plugin/common'; @@ -93,7 +93,6 @@ export function ChatTimeline({ onActionClick, chatState, }: ChatTimelineProps) { - const { euiTheme } = useEuiTheme(); const stickyCalloutContainerClassName = css` position: sticky;