diff --git a/x-pack/solutions/observability/plugins/observability_shared/kibana.jsonc b/x-pack/solutions/observability/plugins/observability_shared/kibana.jsonc index d6081b4e285f97..5b45bfc1d534f0 100644 --- a/x-pack/solutions/observability/plugins/observability_shared/kibana.jsonc +++ b/x-pack/solutions/observability/plugins/observability_shared/kibana.jsonc @@ -24,8 +24,7 @@ "data", "inspector", "kibanaReact", - "kibanaUtils", - "observabilityAIAssistant" + "kibanaUtils" ], "extraPublicDirs": [ "common" diff --git a/x-pack/solutions/observability/plugins/observability_shared/public/components/add_page_attachment_to_case_modal/add_page_attachment_to_case_modal.test.tsx b/x-pack/solutions/observability/plugins/observability_shared/public/components/add_page_attachment_to_case_modal/add_page_attachment_to_case_modal.test.tsx index 6002ba6ccee115..c7e612e14f3b6f 100644 --- a/x-pack/solutions/observability/plugins/observability_shared/public/components/add_page_attachment_to_case_modal/add_page_attachment_to_case_modal.test.tsx +++ b/x-pack/solutions/observability/plugins/observability_shared/public/components/add_page_attachment_to_case_modal/add_page_attachment_to_case_modal.test.tsx @@ -13,20 +13,6 @@ import type { CasesPublicStart } from '@kbn/cases-plugin/public'; import { mockCasesContract } from '@kbn/cases-plugin/public/mocks'; import { notificationServiceMock } from '@kbn/core-notifications-browser-mocks'; import { AddPageAttachmentToCaseModal } from './add_page_attachment_to_case_modal'; -import * as usePageSummaryHook from '../../hooks/use_page_summary'; - -jest.mock('../../hooks/use_page_summary', () => ({ - usePageSummary: jest.fn(() => ({ - isObsAIAssistantEnabled: true, - generateSummary: jest.fn(), - isLoading: false, - summary: '', - errors: [], - abortController: { signal: new AbortController().signal, abort: jest.fn() }, - screenContexts: [], - isComplete: false, - })), -})); const mockCases: Partial = mockCasesContract(); @@ -127,17 +113,6 @@ describe('AddPageAttachmentToCaseModal', () => { }); it('opens case modal when confirm button is clicked', () => { - jest.spyOn(usePageSummaryHook, 'usePageSummary').mockReturnValue({ - isObsAIAssistantEnabled: true, - generateSummary: jest.fn(), - isLoading: false, - summary: '', - errors: [], - abortController: { signal: new AbortController().signal, abort: jest.fn() }, - screenContexts: [], - isComplete: true, - }); - const mockCasesModal = { open: jest.fn(), close: jest.fn(), @@ -159,17 +134,6 @@ describe('AddPageAttachmentToCaseModal', () => { }); it('passes correct getAttachments payload when case modal is opened', () => { - jest.spyOn(usePageSummaryHook, 'usePageSummary').mockReturnValue({ - isObsAIAssistantEnabled: true, - generateSummary: jest.fn(), - isLoading: false, - summary: '', - errors: [], - abortController: { signal: new AbortController().signal, abort: jest.fn() }, - screenContexts: [], - isComplete: true, - }); - const mockCasesModal = { open: jest.fn(), close: jest.fn(), @@ -200,7 +164,6 @@ describe('AddPageAttachmentToCaseModal', () => { { persistableStateAttachmentState: { ...pageAttachmentState, - screenContext: [], summary: comment, }, persistableStateAttachmentTypeId: '.page', @@ -209,63 +172,7 @@ describe('AddPageAttachmentToCaseModal', () => { ]); }); - it('passes correct screenContexts to the case attachment', () => { - const mockCasesModal = { - open: jest.fn(), - close: jest.fn(), - }; - mockCases.hooks!.useCasesAddToExistingCaseModal = jest.fn(() => mockCasesModal); - const screenContexts = [{ screenDescription: 'testContext' }]; - jest.spyOn(usePageSummaryHook, 'usePageSummary').mockReturnValue({ - isObsAIAssistantEnabled: true, - generateSummary: jest.fn(), - isLoading: false, - summary: '', - errors: [], - abortController: { signal: new AbortController().signal, abort: jest.fn() }, - screenContexts, - isComplete: true, - }); - render( - - - - ); - fireEvent.click(screen.getByText('Confirm')); - expect(mockCasesModal!.open).toHaveBeenCalledWith({ - getAttachments: expect.any(Function), - }); - const attachments = mockCasesModal!.open.mock.calls[0][0].getAttachments(); - expect(attachments).toEqual([ - { - persistableStateAttachmentState: { - ...pageAttachmentState, - screenContext: screenContexts, - summary: '', - }, - persistableStateAttachmentTypeId: '.page', - type: 'persistableState', - }, - ]); - }); - it('can update the summary comment', () => { - jest.spyOn(usePageSummaryHook, 'usePageSummary').mockReturnValue({ - isObsAIAssistantEnabled: true, - generateSummary: jest.fn(), - isLoading: false, - summary: '', - errors: [], - abortController: { signal: new AbortController().signal, abort: jest.fn() }, - screenContexts: [], - isComplete: true, - }); - const mockCasesModal = { open: jest.fn(), close: jest.fn(), @@ -292,7 +199,6 @@ describe('AddPageAttachmentToCaseModal', () => { { persistableStateAttachmentState: { ...pageAttachmentState, - screenContext: [], summary: comment, }, persistableStateAttachmentTypeId: '.page', diff --git a/x-pack/solutions/observability/plugins/observability_shared/public/components/add_page_attachment_to_case_modal/add_page_attachment_to_case_modal.tsx b/x-pack/solutions/observability/plugins/observability_shared/public/components/add_page_attachment_to_case_modal/add_page_attachment_to_case_modal.tsx index e3ed51b61f2ce6..ea9687f048aaff 100644 --- a/x-pack/solutions/observability/plugins/observability_shared/public/components/add_page_attachment_to_case_modal/add_page_attachment_to_case_modal.tsx +++ b/x-pack/solutions/observability/plugins/observability_shared/public/components/add_page_attachment_to_case_modal/add_page_attachment_to_case_modal.tsx @@ -10,20 +10,16 @@ import type { CasesPublicStart } from '@kbn/cases-plugin/public'; import { EuiConfirmModal, EuiModalHeader, EuiModalHeaderTitle, EuiModalBody } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { AttachmentType } from '@kbn/cases-plugin/common'; -import type { ObservabilityAIAssistantPublicStart } from '@kbn/observability-ai-assistant-plugin/public'; import { type PageAttachmentPersistedState, PAGE_ATTACHMENT_TYPE, } from '@kbn/page-attachment-schema'; import { type CasesPermissions } from '@kbn/cases-plugin/common'; import type { NotificationsStart } from '@kbn/core/public'; -import { useChatService } from '../../hooks/use_chat_service'; -import { usePageSummary } from '../../hooks/use_page_summary'; import { AddToCaseComment } from '../add_to_case_comment'; export interface AddPageAttachmentToCaseModalProps { pageAttachmentState: PageAttachmentPersistedState; - observabilityAIAssistant?: ObservabilityAIAssistantPublicStart; notifications: NotificationsStart; cases: CasesPublicStart; onCloseModal: () => void; @@ -32,15 +28,11 @@ export interface AddPageAttachmentToCaseModalProps { export function AddPageAttachmentToCaseModal({ pageAttachmentState, cases, - observabilityAIAssistant, notifications, onCloseModal, }: AddPageAttachmentToCaseModalProps) { const getCasesContext = cases.ui.getCasesContext; const canUseCases = cases.helpers.canUseCases; - const { ObservabilityAIAssistantChatServiceContext, chatService } = useChatService({ - observabilityAIAssistant, - }); const casesPermissions: CasesPermissions = useMemo(() => { if (!canUseCases) { @@ -87,24 +79,11 @@ export function AddPageAttachmentToCaseModal({ owner={['observability']} features={{ alerts: { sync: false } }} > - {ObservabilityAIAssistantChatServiceContext && chatService ? ( - - - - ) : ( - - )} + ) : null; } @@ -112,25 +91,13 @@ export function AddPageAttachmentToCaseModal({ function AddToCaseButtonContent({ pageAttachmentState, cases, - observabilityAIAssistant, - notifications, onCloseModal, -}: AddPageAttachmentToCaseModalProps) { +}: Omit) { const [isCommentModalOpen, setIsCommentModalOpen] = useState(true); - const [isCommentLoading, setIsCommentLoading] = useState(true); const [comment, setComment] = useState(''); const useCasesAddToExistingCaseModal = cases.hooks.useCasesAddToExistingCaseModal!; - const { screenContexts } = usePageSummary({ - observabilityAIAssistant, - appInstructions: - 'When referring to Synthetics monitors, include the monitor name, test run timestamp, and test run location.', - }); const casesModal = useCasesAddToExistingCaseModal({ - onClose: (_, isCreateCase) => { - if (!isCreateCase) { - onCloseModal(); - } - }, + onClose: onCloseModal, }); const handleCloseModal = useCallback(() => { @@ -146,14 +113,13 @@ function AddToCaseButtonContent({ persistableStateAttachmentState: { ...pageAttachmentState, summary: comment, - screenContext: screenContexts || [], }, persistableStateAttachmentTypeId: PAGE_ATTACHMENT_TYPE, type: AttachmentType.persistableState, }, ], }); - }, [casesModal, comment, pageAttachmentState, screenContexts]); + }, [casesModal, comment, pageAttachmentState]); return isCommentModalOpen ? ( - + setComment(change)} comment={comment} /> ) : null; diff --git a/x-pack/solutions/observability/plugins/observability_shared/public/components/add_to_case_comment/add_to_case_comment.test.tsx b/x-pack/solutions/observability/plugins/observability_shared/public/components/add_to_case_comment/add_to_case_comment.test.tsx index 88e2efcc14519a..9be0b2c81365a4 100644 --- a/x-pack/solutions/observability/plugins/observability_shared/public/components/add_to_case_comment/add_to_case_comment.test.tsx +++ b/x-pack/solutions/observability/plugins/observability_shared/public/components/add_to_case_comment/add_to_case_comment.test.tsx @@ -8,9 +8,6 @@ import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import { AddToCaseComment } from '.'; -import { observabilityAIAssistantPluginMock } from '@kbn/observability-ai-assistant-plugin/public/mock'; -import { notificationServiceMock } from '@kbn/core-notifications-browser-mocks'; -import * as usePageSummaryHook from '../../hooks/use_page_summary'; // Mock i18n jest.mock('@kbn/i18n-react', () => ({ @@ -22,57 +19,14 @@ jest.mock('@kbn/i18n-react', () => ({ }), })); -const mockObservabilityAIAssistant = observabilityAIAssistantPluginMock.createStartContract(); - describe('AddToCaseComment', () => { - const addErrorMock = jest.fn(); - const notificationsContractMock = notificationServiceMock.createStartContract(); - const notificationsMock = { - ...notificationsContractMock, - toasts: { - ...notificationsContractMock.toasts, - addError: addErrorMock, - }, - }; beforeEach(() => { jest.clearAllMocks(); - jest.spyOn(usePageSummaryHook, 'usePageSummary').mockReturnValue({ - isObsAIAssistantEnabled: true, - generateSummary: jest.fn(), - isLoading: false, - summary: '', - errors: [], - abortController: { signal: new AbortController().signal, abort: jest.fn() }, - screenContexts: [], - isComplete: false, - }); - }); - - it('renders the input field with placeholder text', () => { - render( - - ); - - expect(screen.getByLabelText('Add a comment (optional)')).toBeInTheDocument(); }); it('updates the comment when text is entered', () => { const onCommentChangeMock = jest.fn(); - render( - - ); + render(); fireEvent.change(screen.getByLabelText('Add a comment (optional)'), { target: { value: 'New comment' }, @@ -80,270 +34,4 @@ describe('AddToCaseComment', () => { expect(onCommentChangeMock).toHaveBeenCalledWith('New comment'); }); - - it('shows input when AI assistant is enabled and comment is defined', () => { - jest.spyOn(usePageSummaryHook, 'usePageSummary').mockReturnValue({ - isObsAIAssistantEnabled: true, - generateSummary: jest.fn(), - isLoading: true, - summary: '', - abortController: { signal: new AbortController().signal, abort: jest.fn() }, - screenContexts: [], - errors: [], - isComplete: false, - }); - - render( - - ); - - expect(screen.getByLabelText('Add a comment (optional)')).toBeInTheDocument(); - }); - - it('shows input when AI assistant is enabled and isComplete is true', () => { - jest.spyOn(usePageSummaryHook, 'usePageSummary').mockReturnValue({ - isObsAIAssistantEnabled: true, - generateSummary: jest.fn(), - isLoading: true, - summary: '', - abortController: { signal: new AbortController().signal, abort: jest.fn() }, - screenContexts: [], - errors: [], - isComplete: true, - }); - - render( - - ); - - expect(screen.getByLabelText('Add a comment (optional)')).toBeInTheDocument(); - }); - - it('shows skeleton loader when AI assistant is enabled and comment is empty and isComplete is false', () => { - jest.spyOn(usePageSummaryHook, 'usePageSummary').mockReturnValue({ - isObsAIAssistantEnabled: true, - generateSummary: jest.fn(), - isLoading: true, - summary: '', - abortController: { signal: new AbortController().signal, abort: jest.fn() }, - screenContexts: [], - errors: [], - isComplete: false, - }); - - render( - - ); - - expect(screen.getByTestId('addPageToCaseCommentSkeleton')).toBeInTheDocument(); - }); - - it('displays AI assistant help text when enabled', () => { - render( - - ); - - expect( - screen.getByText( - '{icon} Initial comment AI generated. AI can be wrong or incomplete. Please review and edit as necessary.' - ) - ).toBeInTheDocument(); - }); - - it('does not display AI assistant help text when isObsAIAssistantEnabled is false', () => { - jest.spyOn(usePageSummaryHook, 'usePageSummary').mockReturnValue({ - isObsAIAssistantEnabled: false, - generateSummary: jest.fn(), - isLoading: false, - summary: '', - abortController: { signal: new AbortController().signal, abort: jest.fn() }, - screenContexts: [], - errors: [], - isComplete: false, - }); - - render( - - ); - - expect( - screen.queryByText( - '{icon} Initial comment AI generated. AI can be wrong or incomplete. Please review and edit as necessary.' - ) - ).not.toBeInTheDocument(); - }); - - it('does not display AI assistant help text when there are errors', () => { - jest.spyOn(usePageSummaryHook, 'usePageSummary').mockReturnValue({ - isObsAIAssistantEnabled: true, - generateSummary: jest.fn(), - isLoading: false, - summary: '', - abortController: { signal: new AbortController().signal, abort: jest.fn() }, - screenContexts: [], - errors: [new Error('Test error')], - isComplete: false, - }); - - render( - - ); - - expect( - screen.queryByText( - '{icon} Initial comment AI generated. AI can be wrong or incomplete. Please review and edit as necessary.' - ) - ).not.toBeInTheDocument(); - }); - - it('calls generateSummary when isObsAIAssistantEnabled is true and isComplete is false', () => { - const generateSummaryMock = jest.fn(); - jest.spyOn(usePageSummaryHook, 'usePageSummary').mockReturnValue({ - isObsAIAssistantEnabled: true, - generateSummary: generateSummaryMock, - isLoading: false, - summary: '', - abortController: { signal: new AbortController().signal, abort: jest.fn() }, - screenContexts: [], - errors: [], - isComplete: false, - }); - - render( - - ); - - expect(generateSummaryMock).toHaveBeenCalled(); - }); - - it('does not generateSummary when isObsAIAssistantEnabled is true and isComplete is true', () => { - const generateSummaryMock = jest.fn(); - jest.spyOn(usePageSummaryHook, 'usePageSummary').mockReturnValue({ - isObsAIAssistantEnabled: false, - generateSummary: generateSummaryMock, - isLoading: false, - summary: '', - abortController: { signal: new AbortController().signal, abort: jest.fn() }, - screenContexts: [], - errors: [], - isComplete: false, - }); - - render( - - ); - - expect(generateSummaryMock).not.toHaveBeenCalled(); - }); - - it('appends partial summaries to the comment using handleStreamingUpdate', () => { - const onCommentChangeMock = jest.fn((prevComment) => `${prevComment} Partial summary`); - const generateSummaryMock = jest.fn(); - const usePageSummarySpy = jest.spyOn(usePageSummaryHook, 'usePageSummary').mockReturnValue({ - isObsAIAssistantEnabled: true, - generateSummary: generateSummaryMock, - isLoading: false, - summary: '', - abortController: { signal: new AbortController().signal, abort: jest.fn() }, - screenContexts: [], - errors: [], - isComplete: false, - }); - - render( - - ); - - expect(onCommentChangeMock).not.toBeCalledWith(expect.any(Function)); - - expect(screen.getByText('Existing comment')).toBeInTheDocument(); - - expect(generateSummaryMock).toHaveBeenCalled(); - - const handleStreamingUpdate = usePageSummarySpy.mock.calls[0]![0]!.onChunk!; - handleStreamingUpdate(' Partial summary'); - - expect(onCommentChangeMock).toBeCalledWith(expect.any(Function)); - }); - - it('calls notifications.toasts.addError when errors are present', () => { - jest.spyOn(usePageSummaryHook, 'usePageSummary').mockReturnValue({ - isObsAIAssistantEnabled: true, - generateSummary: jest.fn(), - isLoading: false, - summary: '', - abortController: { signal: new AbortController().signal, abort: jest.fn() }, - screenContexts: [], - errors: [new Error('Test error')], - isComplete: false, - }); - - render( - - ); - - expect(notificationsMock.toasts.addError).toHaveBeenCalledWith(expect.any(Error), { - title: 'Could not initialize AI-generated summary', - }); - }); }); diff --git a/x-pack/solutions/observability/plugins/observability_shared/public/components/add_to_case_comment/index.tsx b/x-pack/solutions/observability/plugins/observability_shared/public/components/add_to_case_comment/index.tsx index 89d3679b9e4715..2fc1ccf2abb872 100644 --- a/x-pack/solutions/observability/plugins/observability_shared/public/components/add_to_case_comment/index.tsx +++ b/x-pack/solutions/observability/plugins/observability_shared/public/components/add_to_case_comment/index.tsx @@ -5,75 +5,21 @@ * 2.0. */ -import React, { useCallback, useEffect } from 'react'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { EuiTextArea, EuiFormRow, EuiSkeletonText, EuiIcon } from '@elastic/eui'; +import React from 'react'; +import { EuiTextArea, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import type { ObservabilityAIAssistantPublicStart } from '@kbn/observability-ai-assistant-plugin/public'; -import type { NotificationsStart } from '@kbn/core/public'; -import { usePageSummary } from '../../hooks/use_page_summary'; interface AddToCaseCommentProps { comment: string; - setComment: React.Dispatch>; - setIsLoading: React.Dispatch>; - observabilityAIAssistant?: ObservabilityAIAssistantPublicStart; - notifications: NotificationsStart; + onCommentChange: (comment: string) => void; } -export function AddToCaseComment({ - comment, - setComment, - setIsLoading, - observabilityAIAssistant, - notifications, -}: AddToCaseCommentProps) { - const handleStreamingUpdate = useCallback( - (partialSummary: string) => { - setComment((prevComment) => prevComment + partialSummary); - }, - [setComment] - ); - - const { generateSummary, isObsAIAssistantEnabled, errors, isComplete } = usePageSummary({ - onChunk: handleStreamingUpdate, - observabilityAIAssistant, - }); - - useEffect(() => { - if (isObsAIAssistantEnabled && !isComplete) { - generateSummary(); - } - }, [generateSummary, isObsAIAssistantEnabled, isComplete]); - - useEffect(() => { - if (isObsAIAssistantEnabled && !isComplete) { - setIsLoading(true); - } else { - setIsLoading(false); - } - }, [isObsAIAssistantEnabled, isComplete, setIsLoading]); - - useEffect(() => { - if (errors.length > 0) { - errors.forEach((error) => { - notifications.toasts.addError(error, { - title: i18n.translate( - 'xpack.observabilityShared.cases.addPageToCaseModal.errorGeneratingSummary', - { - defaultMessage: 'Could not initialize AI-generated summary', - } - ), - }); - }); - } - }, [errors, notifications]); - +export function AddToCaseComment({ comment, onCommentChange }: AddToCaseCommentProps) { const input = ( { - setComment(e.target.value); + onCommentChange(e.target.value); }} value={comment} fullWidth @@ -81,34 +27,15 @@ export function AddToCaseComment({ /> ); - const showAIEnhancedExperience = isObsAIAssistantEnabled && !errors.length; - return ( <> }} - /> - ) : undefined - } fullWidth > - {showAIEnhancedExperience ? ( - comment || isComplete ? ( - input - ) : ( - - ) - ) : ( - input - )} + {input} ); diff --git a/x-pack/solutions/observability/plugins/observability_shared/public/hooks/use_chat_service.test.ts b/x-pack/solutions/observability/plugins/observability_shared/public/hooks/use_chat_service.test.ts deleted file mode 100644 index 1cef38bd3e3117..00000000000000 --- a/x-pack/solutions/observability/plugins/observability_shared/public/hooks/use_chat_service.test.ts +++ /dev/null @@ -1,143 +0,0 @@ -/* - * 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 { renderHook, waitFor } from '@testing-library/react'; -import { useChatService } from './use_chat_service'; -import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { useAbortableAsync } from '@kbn/react-hooks'; -import { observabilityAIAssistantPluginMock } from '@kbn/observability-ai-assistant-plugin/public/mock'; - -jest.mock('@kbn/kibana-react-plugin/public', () => ({ - useKibana: jest.fn(), -})); - -jest.mock('@kbn/react-hooks', () => ({ - useAbortableAsync: jest.fn(), -})); - -describe('useChatService', () => { - const mockObservabilityAIAssistant = observabilityAIAssistantPluginMock.createStartContract(); - - beforeEach(() => { - jest.clearAllMocks(); - (useKibana as jest.Mock).mockReturnValue({ - services: { - observabilityAIAssistant: mockObservabilityAIAssistant, - }, - }); - (useAbortableAsync as jest.Mock).mockImplementation((fn) => fn({ signal: {} })); - }); - - it('should return the correct values when ObservabilityAIAssistant is enabled', () => { - mockObservabilityAIAssistant.useGenAIConnectors = jest.fn().mockReturnValue({ - connectors: [{ id: 'test-connector' }, { id: 'another-connector' }], - selectedConnector: { id: 'test-connector' }, - }); - const { result } = renderHook(() => - useChatService({ - observabilityAIAssistant: mockObservabilityAIAssistant, - }) - ); - - expect(result.current.ObservabilityAIAssistantChatServiceContext).toBe( - mockObservabilityAIAssistant.ObservabilityAIAssistantChatServiceContext - ); - expect(result.current.isObsAIAssistantEnabled).toBe(true); - expect(result.current.connectors).toEqual([ - { id: 'test-connector' }, - { id: 'another-connector' }, - ]); - expect(result.current.selectedConnector).toEqual({ id: 'test-connector' }); - }); - - it('should start the chat service when ObservabilityAIAssistant is enabled', () => { - const mockStart = jest.fn().mockResolvedValue({ complete: jest.fn() }); - mockObservabilityAIAssistant.service.start = mockStart; - const { result } = renderHook(() => - useChatService({ - observabilityAIAssistant: mockObservabilityAIAssistant, - }) - ); - expect(result.current.chatService).toBeDefined(); - expect(mockStart).toHaveBeenCalled(); - }); - - it('should return isObsAIAssistantEnabled as false when ObservabilityAIAssistant is not available', () => { - const { result } = renderHook(() => - useChatService({ - observabilityAIAssistant: undefined, - }) - ); - - expect(result.current.ObservabilityAIAssistantChatServiceContext).toBeUndefined(); - expect(result.current.isObsAIAssistantEnabled).toBe(false); - expect(result.current.connectors).toEqual([]); - }); - - it('should track errors when the chat service fails to start', async () => { - const mockError = new Error('Test error'); - jest.spyOn(mockObservabilityAIAssistant.service, 'start').mockRejectedValueOnce(mockError); - - const { result } = renderHook(() => - useChatService({ - observabilityAIAssistant: mockObservabilityAIAssistant, - }) - ); - - await waitFor(() => { - expect(result.current.errors).toContain(mockError); - }); - }); - - it('returns isObsAIAssistantEnabled as false when observability ai assistant is not available', () => { - const { result } = renderHook(() => - useChatService({ - observabilityAIAssistant: undefined, - }) - ); - - expect(result.current).toEqual({ - ObservabilityAIAssistantChatServiceContext: undefined, - chatService: null, - observabilityAIAssistantService: undefined, - isObsAIAssistantEnabled: false, - connectors: [], - selectedConnector: undefined, - errors: [], - }); - }); - - it('returns isObsAIAssistantEnabled as false when context is not available', () => { - const { result } = renderHook(() => - useChatService({ - observabilityAIAssistant: { - ...mockObservabilityAIAssistant, - // @ts-ignore - ObservabilityAIAssistantChatServiceContext: undefined, - }, - }) - ); - - expect(result.current.isObsAIAssistantEnabled).toBe(false); - }); - - it('returns isObsAIAssistantEnabled as false when service is not enabled', () => { - const mockService = { - isEnabled: jest.fn().mockReturnValue(false), - }; - const { result } = renderHook(() => - useChatService({ - observabilityAIAssistant: { - ...mockObservabilityAIAssistant, - service: mockService as any, - }, - }) - ); - - expect(result.current.isObsAIAssistantEnabled).toBe(false); - }); -}); diff --git a/x-pack/solutions/observability/plugins/observability_shared/public/hooks/use_chat_service.ts b/x-pack/solutions/observability/plugins/observability_shared/public/hooks/use_chat_service.ts deleted file mode 100644 index e3a26e30e41865..00000000000000 --- a/x-pack/solutions/observability/plugins/observability_shared/public/hooks/use_chat_service.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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 { useState } from 'react'; -import { useAbortableAsync } from '@kbn/react-hooks'; -import { type ObservabilityAIAssistantPublicStart } from '@kbn/observability-ai-assistant-plugin/public'; - -export const useChatService = ({ - observabilityAIAssistant, -}: { - observabilityAIAssistant?: ObservabilityAIAssistantPublicStart; -}) => { - const [errors, setErrors] = useState([]); - const ObservabilityAIAssistantChatServiceContext = - observabilityAIAssistant?.ObservabilityAIAssistantChatServiceContext; - const obsAIService = observabilityAIAssistant?.service; - const isObsAIAssistantEnabled = Boolean( - observabilityAIAssistant && - ObservabilityAIAssistantChatServiceContext && - obsAIService?.isEnabled() - ); - const { connectors = [], selectedConnector } = - observabilityAIAssistant?.useGenAIConnectors() || {}; - - const chatService = useAbortableAsync( - async ({ signal }) => { - if (!obsAIService || !isObsAIAssistantEnabled) { - return Promise.resolve(null); - } - return obsAIService.start({ signal }).catch((error: Error) => { - setErrors((prevErrors: Error[]) => [...prevErrors, error]); - }); - }, - [obsAIService, isObsAIAssistantEnabled] - ); - - return { - ObservabilityAIAssistantChatServiceContext, - chatService: chatService.value || null, - observabilityAIAssistantService: obsAIService, - isObsAIAssistantEnabled, - connectors, - selectedConnector, - errors, - }; -}; diff --git a/x-pack/solutions/observability/plugins/observability_shared/public/hooks/use_page_summary.test.ts b/x-pack/solutions/observability/plugins/observability_shared/public/hooks/use_page_summary.test.ts deleted file mode 100644 index 32502b45437a74..00000000000000 --- a/x-pack/solutions/observability/plugins/observability_shared/public/hooks/use_page_summary.test.ts +++ /dev/null @@ -1,215 +0,0 @@ -/* - * 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 { renderHook, act } from '@testing-library/react'; -import { usePageSummary } from './use_page_summary'; -import { observabilityAIAssistantPluginMock } from '@kbn/observability-ai-assistant-plugin/public/mock'; -import type { - MessageRole, - StreamingChatResponseEventWithoutError, -} from '@kbn/observability-ai-assistant-plugin/common'; -import { type ObservabilityAIAssistantChatService } from '@kbn/observability-ai-assistant-plugin/public'; -import * as useChatServiceHook from './use_chat_service'; -import { Observable, of, BehaviorSubject } from 'rxjs'; - -jest.mock('./use_chat_service', () => ({ - useChatService: jest.fn(() => ({ - ObservabilityAIAssistantChatServiceContext: null, - chatService: { - complete: jest.fn(), - }, - observabilityAIAssistantService: null, - isObsAIAssistantEnabled: true, - connectors: [], - selectedConnector: null, - errors: [], - })), -})); - -describe('usePageSummary', () => { - const mockObservabilityAIAssistant = observabilityAIAssistantPluginMock.createStartContract(); - - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('should initialize with default states', () => { - const { result } = renderHook(() => - usePageSummary({ observabilityAIAssistant: mockObservabilityAIAssistant }) - ); - expect(result.current.summary).toBe(null); - expect(result.current.isLoading).toBe(true); - expect(result.current.errors).toEqual([]); - }); - - it('should handle loading state correctly', () => { - const mockOnSuccess = jest.fn(); - const { result } = renderHook(() => - usePageSummary({ - onSuccess: mockOnSuccess, - observabilityAIAssistant: mockObservabilityAIAssistant, - }) - ); - - result.current.generateSummary(); - - expect(result.current.isLoading).toBe(true); - }); - - it('should handle errors correctly', () => { - const mockOnSuccess = jest.fn(); - const { result } = renderHook(() => - usePageSummary({ - onSuccess: mockOnSuccess, - observabilityAIAssistant: mockObservabilityAIAssistant, - }) - ); - - result.current.generateSummary(); - - result.current.errors.push(new Error('Test error')); - - expect(result.current.errors.length).toBe(1); - expect(result.current.errors[0].message).toBe('Test error'); - }); - - it('sets isLoading to false when observabilityAIAssistant is not enabled', () => { - jest.spyOn(useChatServiceHook, 'useChatService').mockReturnValue({ - ObservabilityAIAssistantChatServiceContext: undefined, - chatService: null, - observabilityAIAssistantService: undefined, - isObsAIAssistantEnabled: false, - connectors: [], - selectedConnector: undefined, - errors: [], - }); - const { result } = renderHook(() => - usePageSummary({ - observabilityAIAssistant: mockObservabilityAIAssistant, - }) - ); - - act(() => { - result.current.generateSummary(); - }); - - expect(result.current.isLoading).toBe(false); - expect(result.current.summary).toBe(null); - }); - - it('should call onChunk when chunk event comes in', () => { - const observable = of({ - type: 'chatCompletionChunk', - '@timestamp': new Date().toISOString(), - message: { - content: 'Generated chunk', - role: 'assistant' as MessageRole, - }, - id: '2', - } as StreamingChatResponseEventWithoutError); - jest.spyOn(useChatServiceHook, 'useChatService').mockReturnValue({ - ObservabilityAIAssistantChatServiceContext: undefined, - chatService: { - complete: jest.fn(() => observable), - sendAnalyticsEvent: jest.fn(), - chat: jest.fn(), - getFunctions: jest.fn(), - hasFunction: jest.fn(), - hasRenderFunction: jest.fn(), - getSystemMessage: jest.fn(), - getScopes: jest.fn(), - renderFunction: jest.fn(), - functions$: new BehaviorSubject( - [] - ) as unknown as ObservabilityAIAssistantChatService['functions$'], - }, - observabilityAIAssistantService: mockObservabilityAIAssistant.service, - isObsAIAssistantEnabled: true, - connectors: [], - selectedConnector: 'test-connector', - errors: [], - }); - const mockOnChunk = jest.fn(); - const { result } = renderHook(() => - usePageSummary({ - onChunk: mockOnChunk, - observabilityAIAssistant: mockObservabilityAIAssistant, - }) - ); - - act(() => { - result.current.generateSummary(); - }); - - expect(mockOnChunk).toHaveBeenCalledWith('Generated chunk'); - }); - - it('should call onSuccess when summary event comes in', () => { - const observable = of({ - type: 'chatCompletionMessage', - '@timestamp': new Date().toISOString(), - message: { - content: 'Generated summary', - role: 'assistant' as MessageRole, - }, - id: '1', - } as StreamingChatResponseEventWithoutError); - jest.spyOn(useChatServiceHook, 'useChatService').mockReturnValue({ - ObservabilityAIAssistantChatServiceContext: undefined, - chatService: { - complete: jest.fn(() => observable) as ObservabilityAIAssistantChatService['complete'], - } as ObservabilityAIAssistantChatService, - observabilityAIAssistantService: mockObservabilityAIAssistant.service, - isObsAIAssistantEnabled: true, - connectors: [], - selectedConnector: 'test-connector', - errors: [], - }); - const mockOnSuccess = jest.fn(); - const { result } = renderHook(() => - usePageSummary({ - onSuccess: mockOnSuccess, - observabilityAIAssistant: mockObservabilityAIAssistant, - }) - ); - - act(() => { - result.current.generateSummary(); - }); - - expect(mockOnSuccess).toHaveBeenCalledWith('Generated summary'); - }); - - it('should handle errors from the observable correctly', () => { - const observable = new Observable((subscriber) => { - subscriber.error(new Error('Observable error')); - }); - jest.spyOn(useChatServiceHook, 'useChatService').mockReturnValue({ - ObservabilityAIAssistantChatServiceContext: undefined, - chatService: { - complete: jest.fn(() => observable) as ObservabilityAIAssistantChatService['complete'], - } as ObservabilityAIAssistantChatService, - observabilityAIAssistantService: mockObservabilityAIAssistant.service, - isObsAIAssistantEnabled: true, - connectors: [], - selectedConnector: 'test-connector', - errors: [], - }); - const { result } = renderHook(() => - usePageSummary({ - observabilityAIAssistant: mockObservabilityAIAssistant, - }) - ); - - act(() => { - result.current.generateSummary(); - }); - - expect(result.current.errors).toEqual([new Error('Observable error')]); - expect(result.current.isLoading).toBe(false); - }); -}); diff --git a/x-pack/solutions/observability/plugins/observability_shared/public/hooks/use_page_summary.ts b/x-pack/solutions/observability/plugins/observability_shared/public/hooks/use_page_summary.ts deleted file mode 100644 index caf0d99ca0b81b..00000000000000 --- a/x-pack/solutions/observability/plugins/observability_shared/public/hooks/use_page_summary.ts +++ /dev/null @@ -1,152 +0,0 @@ -/* - * 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 dedent from 'dedent'; -import { v4 as uuidv4 } from 'uuid'; -import { useRef, useCallback, useState, useMemo, useEffect } from 'react'; -import { - MessageRole, - type ObservabilityAIAssistantPublicStart, -} from '@kbn/observability-ai-assistant-plugin/public'; -import { useChatService } from './use_chat_service'; - -interface UsePageSummaryProps { - onSuccess?: (summary: string) => void; - onChunk?: (chunk: string) => void; - isLoading?: boolean; - observabilityAIAssistant?: ObservabilityAIAssistantPublicStart; - appInstructions?: string; -} - -export const usePageSummary = ({ - onSuccess, - onChunk, - observabilityAIAssistant, - appInstructions, -}: UsePageSummaryProps = {}) => { - const [errors, setErrors] = useState([]); - const [summary, setSummary] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [isComplete, setIsComplete] = useState(false); - const abortControllerRef = useRef(new AbortController()); - const { - chatService, - observabilityAIAssistantService, - selectedConnector, - isObsAIAssistantEnabled, - errors: chatServiceErrors, - } = useChatService({ observabilityAIAssistant }); - - const screenContexts = observabilityAIAssistantService?.getScreenContexts(); - - const formattedScreenContexts = useMemo(() => { - return screenContexts - ?.map((context) => ({ - screenDescription: context.screenDescription || '', - })) - .filter((context) => context.screenDescription); - }, [screenContexts]); - - const generateSummary = useCallback(() => { - if (!isObsAIAssistantEnabled) { - setIsLoading(false); - return; - } - if ( - !observabilityAIAssistantService || - !chatService || - !selectedConnector || - chatServiceErrors.length > 0 - ) { - setSummary(''); - setErrors((prevErrors) => [...prevErrors, ...chatServiceErrors]); - return; - } - const conversationId = uuidv4(); - setIsLoading(true); - chatService - .complete({ - getScreenContexts: () => observabilityAIAssistantService?.getScreenContexts(), - conversationId, - signal: abortControllerRef.current.signal, - connectorId: selectedConnector, - messages: [ - { - '@timestamp': new Date().toISOString(), - message: { - role: MessageRole.User, - content: - dedent(`Create a 1 sentence summary of the current page. State facts directly without descriptive phrases like "shows," "indicates," or "reveals." - - Include specific numbers, percentages, error counts, response times, exact timestamps, and locations when available. - - Report anomalies, spikes, drops, or failures with their precise timing and impact. - - Use both UTC and local timestamps if provided. Use the date format provided - do not convert times yourself. When both local and UTC times are available, include the local time first and the UTC time in parentheses. - - Begin immediately with the most urgent findings. - - ${ - appInstructions - ? `Here are some additional instructions for the current page: ${appInstructions}.` - : '' - }`), - }, - }, - ], - scopes: ['observability'], - disableFunctions: true, - persist: false, - systemMessage: - 'You are an expert Site Reliability Engineering (SRE) assistant specialized in incident investigation and observability data analysis. You work with a senior SRE who is highly skilled at interpreting monitoring signals, metrics, logs, and traces.', - }) - .subscribe({ - next: (result) => { - if (result.type === 'chatCompletionMessage' && result.message.content) { - setIsLoading(false); - onSuccess?.(result.message.content); - setIsComplete(true); - } - if (result.type === 'chatCompletionChunk' && result.message.content) { - setIsLoading(false); - onChunk?.(result.message.content); - } - }, - error: (error: Error) => { - setErrors((prevErrors) => [...prevErrors, error]); - setIsComplete(true); - setIsLoading(false); - }, - }); - }, [ - chatService, - observabilityAIAssistantService, - onChunk, - isObsAIAssistantEnabled, - onSuccess, - selectedConnector, - chatServiceErrors, - appInstructions, - ]); - - useEffect(() => { - const abortController = abortControllerRef.current; - return () => { - abortController.abort(); - }; - }, []); - - return { - summary, - abortController: abortControllerRef.current, - screenContexts: formattedScreenContexts, - generateSummary, - isObsAIAssistantEnabled: Boolean(isObsAIAssistantEnabled), - isLoading, - isComplete, - errors, - }; -}; diff --git a/x-pack/solutions/observability/plugins/observability_shared/tsconfig.json b/x-pack/solutions/observability/plugins/observability_shared/tsconfig.json index 8a73c45ab2cf3b..2fb67935daeebd 100644 --- a/x-pack/solutions/observability/plugins/observability_shared/tsconfig.json +++ b/x-pack/solutions/observability/plugins/observability_shared/tsconfig.json @@ -50,8 +50,6 @@ "@kbn/home-sample-data-tab", "@kbn/config-schema", "@kbn/page-attachment-schema", - "@kbn/observability-ai-assistant-plugin", - "@kbn/react-hooks", "@kbn/core-notifications-browser-mocks", "@kbn/shared-ux-utility", ], diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/common/components/monitor_status.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/common/components/monitor_status.tsx index baca1a206d0d81..02fb2504f1d2e9 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/common/components/monitor_status.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/common/components/monitor_status.tsx @@ -8,7 +8,6 @@ import React from 'react'; import { EuiBadge, EuiDescriptionList, EuiSkeletonText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import type { EncryptedSyntheticsMonitor } from '../../../../../../common/runtime_types'; -import { useScreenContext } from '../../../hooks/use_screen_context'; export const BadgeStatus = ({ status, @@ -22,10 +21,6 @@ export const BadgeStatus = ({ const { color, dataTestSubj, labels } = badgeMapping[status || 'unknown']; const label = isBrowserType && labels.browser ? labels.browser : labels.default; - useScreenContext({ - screenDescription: `The user is viewing the monitor status based on the last test run. The current status is ${label}.`, - }); - return ( { const { failedTests } = useErrorFailedTests(); @@ -30,3 +31,28 @@ export const ErrorDuration: React.FC = () => { const ERROR_DURATION = i18n.translate('xpack.synthetics.errorDetails.errorDuration', { defaultMessage: 'Error duration', }); + +const getErrorDuration = (startedAt: Moment, endsAt: Moment) => { + // const endsAt = state.ends ? moment(state.ends) : moment(); + // const startedAt = moment(state?.started_at); + + const diffInDays = endsAt.diff(startedAt, 'days'); + if (diffInDays > 1) { + return i18n.translate('xpack.synthetics.errorDetails.errorDuration.days', { + defaultMessage: '{value} days', + values: { value: diffInDays }, + }); + } + const diffInHours = endsAt.diff(startedAt, 'hours'); + if (diffInHours > 1) { + return i18n.translate('xpack.synthetics.errorDetails.errorDuration.hours', { + defaultMessage: '{value} hours', + values: { value: diffInHours }, + }); + } + const diffInMinutes = endsAt.diff(startedAt, 'minutes'); + return i18n.translate('xpack.synthetics.errorDetails.errorDuration.mins', { + defaultMessage: '{value} mins', + values: { value: diffInMinutes }, + }); +}; diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/error_details/error_details_page.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/error_details/error_details_page.tsx index ee7b1c1c5fb5e4..201408ad6c53f2 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/error_details/error_details_page.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/error_details/error_details_page.tsx @@ -22,11 +22,9 @@ import { useErrorDetailsBreadcrumbs } from './hooks/use_error_details_breadcrumb import { StepImage } from '../step_details_page/step_screenshot/step_image'; import { MonitorDetailsPanelContainer } from '../monitor_details/monitor_summary/monitor_details_panel_container'; import { useDateFormat } from '../../../../hooks/use_date_format'; -import { useSelectedMonitor } from '../monitor_details/hooks/use_selected_monitor'; export function ErrorDetailsPage() { const { failedTests, loading } = useErrorFailedTests(); - const { monitor } = useSelectedMonitor(); const checkGroupId = failedTests?.[0]?.monitor.check_group ?? ''; @@ -43,10 +41,6 @@ export function ErrorDetailsPage() { const isBrowser = data?.details?.journey.monitor.type === 'browser'; - if (!monitor) { - return null; - } - return (
@@ -71,7 +65,6 @@ export function ErrorDetailsPage() { stepsData={data} stepsLoading={stepsLoading} isErrorDetails={true} - monitor={monitor} /> {isBrowser && ( <> diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/add_to_case_action.test.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/add_to_case_action.test.tsx index e580d361f68e4f..3d8fc810edde69 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/add_to_case_action.test.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/add_to_case_action.test.tsx @@ -152,7 +152,7 @@ describe('AddToCaseContextItem', () => { iconType: 'uptimeApp', }, summary: '', - screenContext: [], + screenContext: undefined, }, persistableStateAttachmentTypeId: '.page', type: 'persistableState', diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/add_to_case_action.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/add_to_case_action.tsx index 6f96cc76aadc65..c6e3c8012c5f3c 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/add_to_case_action.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/add_to_case_action.tsx @@ -19,7 +19,6 @@ import { useGetUrlParams, useMonitorDetailLocator } from '../../hooks'; export function AddToCaseContextItem() { const [isAddToCaseModalOpen, setIsAddToCaseModalOpen] = useState(false); const services = useKibana().services; - const observabilityAIAssistant = services.observabilityAIAssistant; const cases = services.cases; const canUseCases = cases?.helpers?.canUseCases; const notifications = services.notifications; @@ -76,7 +75,7 @@ export function AddToCaseContextItem() { return; } setIsAddToCaseModalOpen(true); - }, [setIsAddToCaseModalOpen, redirectUrl, monitor?.name, notifications.toasts]); + }, [redirectUrl, monitor?.name, notifications.toasts]); const onCloseModal = useCallback(() => { setIsAddToCaseModalOpen(false); @@ -124,7 +123,6 @@ export function AddToCaseContextItem() { cases={cases} onCloseModal={onCloseModal} notifications={notifications} - observabilityAIAssistant={observabilityAIAssistant} /> )} diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_monitor_screen_context.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_monitor_screen_context.tsx deleted file mode 100644 index 84ad2c4ee91edd..00000000000000 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_monitor_screen_context.tsx +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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 { useEffect } from 'react'; -import dedent from 'dedent'; -import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { useSelectedMonitor } from './use_selected_monitor'; -import { useSelectedLocation } from './use_selected_location'; -import type { ClientPluginsStart } from '../../../../../plugin'; - -export const useMonitorScreenContext = () => { - const { monitor } = useSelectedMonitor(); - const selectedLocation = useSelectedLocation(); - const services = useKibana().services; - const setScreenContext = services.observabilityAIAssistant?.service.setScreenContext; - - useEffect(() => { - if (!monitor || !setScreenContext) { - return; - } - - if (setScreenContext) { - const screenContext = dedent`The user is looking at the details of a monitor. - - Monitor ID: ${monitor.id} - Monitor Saved Object ID: ${monitor.config_id} - Monitor Type: ${monitor.type} - Monitor Name: ${monitor.name} - Location: ${selectedLocation.label} - - When referencing the monitor, use the monitor name. - Only utilize monitor ID when needed for a specific Elasticsearch query`; - - return setScreenContext({ - screenDescription: screenContext, - }); - } - }, [setScreenContext, monitor, selectedLocation?.label]); - return { - monitor, - selectedLocation, - }; -}; diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_history/monitor_history.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_history/monitor_history.tsx index b735a16c444346..fc151db70d1ee7 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_history/monitor_history.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_history/monitor_history.tsx @@ -24,14 +24,12 @@ import { DurationSparklines } from '../monitor_summary/duration_sparklines'; import { MonitorCompleteSparklines } from '../monitor_summary/monitor_complete_sparklines'; import { MonitorStatusPanel } from '../monitor_status/monitor_status_panel'; import { MonitorPendingWrapper } from '../monitor_pending_wrapper'; -import { useMonitorScreenContext } from '../hooks/use_monitor_screen_context'; const STATS_WIDTH_SINGLE_COLUMN_THRESHOLD = 360; // ✨ determined by trial and error export const MonitorHistory = () => { const [, updateUrlParams] = useUrlParams(); const { from, to } = useRefreshedRangeFromUrl(); - useMonitorScreenContext(); const { elementRef: statsRef, width: statsWidth } = useDimensions(); const statsColumns = statsWidth && statsWidth < STATS_WIDTH_SINGLE_COLUMN_THRESHOLD ? 1 : 2; diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/last_test_run.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/last_test_run.tsx index cd423cc8a155b7..14806e13efefda 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/last_test_run.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/last_test_run.tsx @@ -6,8 +6,6 @@ */ import React from 'react'; -import dedent from 'dedent'; -import moment from 'moment'; import { EuiButton, EuiButtonEmpty, @@ -29,13 +27,12 @@ import { useParams } from 'react-router-dom'; import { getTestRunDetailLink } from '../../common/links/test_details_link'; import { useSelectedLocation } from '../hooks/use_selected_location'; import { getErrorDetailsUrl } from '../monitor_errors/errors_list'; -import { - ConfigKey, - MonitorTypeEnum, - type EncryptedSyntheticsSavedMonitor, - type Ping, - type SyntheticsJourneyApiResponse, +import type { + EncryptedSyntheticsSavedMonitor, + Ping, + SyntheticsJourneyApiResponse, } from '../../../../../../common/runtime_types'; +import { ConfigKey, MonitorTypeEnum } from '../../../../../../common/runtime_types'; import { useSyntheticsRefreshContext, useSyntheticsSettingsContext } from '../../../contexts'; import { BrowserStepsList } from '../../common/monitor_test_result/browser_steps_list'; @@ -45,12 +42,9 @@ import { parseBadgeStatus, StatusBadge } from '../../common/monitor_test_result/ import { useJourneySteps } from '../hooks/use_journey_steps'; import { useSelectedMonitor } from '../hooks/use_selected_monitor'; import { useMonitorLatestPing } from '../hooks/use_monitor_latest_ping'; -import { useDateFormat, useUTCDateFormat } from '../../../../../hooks/use_date_format'; -import { getErrorDuration } from '../../../utils/formatting'; -import { useScreenContext } from '../../../hooks/use_screen_context'; +import { useDateFormat } from '../../../../../hooks/use_date_format'; export const LastTestRun = () => { - const { monitor } = useSelectedMonitor(); const { latestPing, loading: pingsLoading } = useMonitorLatestPing(); const { lastRefresh } = useSyntheticsRefreshContext(); @@ -61,16 +55,15 @@ export const LastTestRun = () => { const loading = stepsLoading || pingsLoading; - return monitor ? ( + return ( - ) : null; + ); }; export const LastTestRunComponent = ({ @@ -79,63 +72,23 @@ export const LastTestRunComponent = ({ stepsData, stepsLoading, isErrorDetails = false, - monitor, }: { stepsLoading: boolean; latestPing?: Ping; loading: boolean; stepsData?: SyntheticsJourneyApiResponse; isErrorDetails?: boolean; - monitor: EncryptedSyntheticsSavedMonitor; }) => { + const { monitor } = useSelectedMonitor(); const { euiTheme } = useEuiTheme(); const selectedLocation = useSelectedLocation(); const { basePath } = useSyntheticsSettingsContext(); - const isDown = latestPing?.summary?.down! > 0; - const status = parseBadgeStatus(isDown ? 'fail' : 'success'); - const formatter = useDateFormat(); - const utcFormatter = useUTCDateFormat(); - const lastRunTimestamp = formatter(latestPing?.['@timestamp']); - const lastRunTimestampUTC = utcFormatter(latestPing?.['@timestamp']); - const errorMessage = latestPing?.error?.message; - const stateStartedAt = latestPing?.state?.started_at; - const stateEndsAt = Date.now(); - const formattedStateStartedAt = formatter(latestPing?.state?.started_at); - const utcStateStartedAt = utcFormatter(latestPing?.state?.started_at); - const stateDuration = - stateStartedAt && stateEndsAt - ? getErrorDuration(moment(stateStartedAt), moment(stateEndsAt)) - : 0; - const location = latestPing?.observer?.geo?.name || ''; - - useScreenContext({ - screenDescription: dedent(`The user is viewing the last test run for monitor "${monitor.name}". - The last test run ${status} and was executed at ${lastRunTimestamp} (${lastRunTimestampUTC} UTC) - from location "${location}". - - ${errorMessage ? `The latest error was: ${errorMessage}` : ''}. - - ${ - stateStartedAt && stateDuration - ? `The monitor has been ${ - isDown ? 'down' : 'up' - } for ${stateDuration} since ${formattedStateStartedAt} (${utcStateStartedAt} UTC).` - : '' - } - `), - }); - return ( {loading && } - + {!(loading && !latestPing) && latestPing?.error ? ( { const { euiTheme } = useEuiTheme(); @@ -203,6 +154,9 @@ const PanelHeader = ({ const { monitorId } = useParams<{ monitorId: string }>(); + const formatter = useDateFormat(); + const lastRunTimestamp = formatter(latestPing?.['@timestamp']); + const isBrowserMonitor = monitor?.[ConfigKey.MONITOR_TYPE] === MonitorTypeEnum.BROWSER; const TitleNode = ( diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_summary.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_summary.tsx index 04e27f84bf6548..267fe6fbe34e3c 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_summary.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_summary.tsx @@ -30,7 +30,6 @@ import { MonitorDetailsPanelContainer } from './monitor_details_panel_container' import { LastTestRun } from './last_test_run'; import { LAST_10_TEST_RUNS, TestRunsTable } from './test_runs_table'; import { MonitorPendingWrapper } from '../monitor_pending_wrapper'; -import { useMonitorScreenContext } from '../hooks/use_monitor_screen_context'; export const MonitorSummary = () => { const { from, to } = useMonitorRangeFrom(); @@ -38,8 +37,6 @@ export const MonitorSummary = () => { const dateLabel = from === 'now-30d/d' ? LAST_30_DAYS_LABEL : TO_DATE_LABEL; const isMediumDevice = useIsWithinBreakpoints(['xs', 's', 'm', 'l']); - useMonitorScreenContext(); - const redirect = useMonitorDetailsPage(); if (redirect) { return redirect; diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/test_runs_table.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/test_runs_table.tsx index 570d9c9e4dc184..10be6e0ca99791 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/test_runs_table.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/test_runs_table.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import React, { type MouseEvent, useMemo, useState } from 'react'; -import dedent from 'dedent'; +import type { MouseEvent } from 'react'; +import React, { useMemo, useState } from 'react'; import { useHistory, useParams } from 'react-router-dom'; import { useSelector } from 'react-redux'; import { i18n } from '@kbn/i18n'; @@ -51,7 +51,6 @@ import { useSelectedLocation } from '../hooks/use_selected_location'; import { useMonitorPings } from '../hooks/use_monitor_pings'; import { JourneyLastScreenshot } from '../../common/screenshot/journey_last_screenshot'; import { useSyntheticsRefreshContext, useSyntheticsSettingsContext } from '../../../contexts'; -import { useScreenContext } from '../../../hooks/use_screen_context'; type SortableField = 'timestamp' | 'monitor.status' | 'monitor.duration.us'; @@ -91,29 +90,6 @@ export const TestRunsTable = ({ return sortPings(pings, sortField, sortDirection); }, [pings, sortField, sortDirection]); - const isLast10 = paginable === false && page.size === 10; - const timeRangeContext = `between ${from} and ${to} UTC`; - - const screenContext = dedent` - This table shows ${isLast10 ? 'the last ' : ''}${page.size} test runs for the monitor${ - isLast10 ? '' : ` ${timeRangeContext}` - }. - - ${pings - .map((ping, i) => { - return `${i + 1}. Executed at ${ping['@timestamp']} from ${ - ping.observer.geo.name - } with status ${ping.monitor.status} and duration ${formatTestDuration( - ping.monitor.duration?.us - )}.\n ${ping.error?.message ? `Error: ${ping.error.message}` : ''}\n`; - }) - .join('\n')} - `; - - useScreenContext({ - screenDescription: screenContext, - }); - const pingsError = useSelector(selectPingsError); const { monitor } = useSelectedMonitor(); const selectedLocation = useSelectedLocation(); diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/hooks/use_monitor_detail_locator.ts b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/hooks/use_monitor_detail_locator.ts index 38b951cb1eec4d..ab1f97c535a47f 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/hooks/use_monitor_detail_locator.ts +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/hooks/use_monitor_detail_locator.ts @@ -7,9 +7,8 @@ import { useEffect, useState, useMemo } from 'react'; import { syntheticsMonitorDetailLocatorID } from '@kbn/observability-plugin/common'; -import { getAbsoluteTimeRange } from '@kbn/data-plugin/common'; +import { type TimeRange, getAbsoluteTimeRange } from '@kbn/data-plugin/common'; import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { type TimeRange } from '@kbn/es-query'; import { getMonitorSpaceToAppend } from './use_edit_monitor_locator'; import { useKibanaSpace } from '../../../hooks/use_kibana_space'; import type { ClientPluginsStart } from '../../../plugin'; diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/hooks/use_screen_context.ts b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/hooks/use_screen_context.ts deleted file mode 100644 index 68d763bdd82188..00000000000000 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/hooks/use_screen_context.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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 { useEffect } from 'react'; -import { useKibana } from '@kbn/kibana-react-plugin/public'; -import type { ClientPluginsStart } from '../../../plugin'; - -export const useScreenContext = ({ screenDescription }: { screenDescription: string }) => { - const services = useKibana().services; - const setScreenContext = services.observabilityAIAssistant?.service.setScreenContext; - - useEffect(() => { - if (setScreenContext) { - return setScreenContext({ - screenDescription, - }); - } - }, [setScreenContext, screenDescription]); -}; diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/utils/formatting/format.ts b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/utils/formatting/format.ts index 4d3fff07042bab..5e4c9360fcd37b 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/utils/formatting/format.ts +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/utils/formatting/format.ts @@ -5,7 +5,6 @@ * 2.0. */ import { i18n } from '@kbn/i18n'; -import type { Moment } from 'moment'; // one second = 1 million micros const ONE_SECOND_AS_MICROS = 1000000; @@ -51,25 +50,3 @@ export const formatDuration = (durationMicros: number, { noSpace }: { noSpace?: defaultMessage: '{seconds} s', }); }; - -export const getErrorDuration = (startedAt: Moment, endsAt: Moment) => { - const diffInDays = endsAt.diff(startedAt, 'days'); - if (diffInDays > 1) { - return i18n.translate('xpack.synthetics.errorDetails.errorDuration.days', { - defaultMessage: '{value} days', - values: { value: diffInDays }, - }); - } - const diffInHours = endsAt.diff(startedAt, 'hours'); - if (diffInHours > 1) { - return i18n.translate('xpack.synthetics.errorDetails.errorDuration.hours', { - defaultMessage: '{value} hours', - values: { value: diffInHours }, - }); - } - const diffInMinutes = endsAt.diff(startedAt, 'minutes'); - return i18n.translate('xpack.synthetics.errorDetails.errorDuration.mins', { - defaultMessage: '{value} mins', - values: { value: diffInMinutes }, - }); -}; diff --git a/x-pack/solutions/observability/plugins/synthetics/public/hooks/use_date_format.ts b/x-pack/solutions/observability/plugins/synthetics/public/hooks/use_date_format.ts index 09512009f182ab..f7bdff8e32fd00 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/hooks/use_date_format.ts +++ b/x-pack/solutions/observability/plugins/synthetics/public/hooks/use_date_format.ts @@ -10,7 +10,6 @@ import { useEffect } from 'react'; import { i18n } from '@kbn/i18n'; export type DateFormatter = (timestamp?: string) => string; - export function useDateFormat(): DateFormatter { const kibanaLocale = i18n.getLocale(); const clientLocale = navigator.language; @@ -28,11 +27,3 @@ export function useDateFormat(): DateFormatter { return `${date.format('ll')} @ ${date.format('LTS')}`; }; } - -export function useUTCDateFormat(): DateFormatter { - return (timestamp?: string) => { - if (!timestamp) return ''; - const date = moment.utc(timestamp); - return date.toISOString(); - }; -}