diff --git a/x-pack/platform/packages/shared/kbn-page-attachment-schema/src/schema/v1.ts b/x-pack/platform/packages/shared/kbn-page-attachment-schema/src/schema/v1.ts index 1ae46596eda17..54422b013f01d 100644 --- a/x-pack/platform/packages/shared/kbn-page-attachment-schema/src/schema/v1.ts +++ b/x-pack/platform/packages/shared/kbn-page-attachment-schema/src/schema/v1.ts @@ -35,6 +35,10 @@ export const pageAttachmentPersistedStateSchema = z.object({ * can be provided to an LLM to generate a summary or perform analysis */ screenContext: z.array(z.object({ screenDescription: z.string() })).optional(), + /** + * Optional summary of the page. + */ + summary: z.string().optional(), }); export type PageAttachmentPersistedState = z.infer; diff --git a/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment_children.tsx b/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment_children.tsx index 704b0a09ac82f..90bb61cc42bd7 100644 --- a/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment_children.tsx +++ b/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment_children.tsx @@ -29,7 +29,7 @@ export function PageAttachmentChildren({ notifications: { toasts }, }, } = useKibana(); - const { url } = pageState; + const { url, summary } = pageState; const label = url?.label; const href = useMemo(() => { @@ -117,6 +117,7 @@ export function PageAttachmentChildren({ + {summary && {summary}} ); } diff --git a/x-pack/solutions/observability/plugins/observability/public/plugin.ts b/x-pack/solutions/observability/plugins/observability/public/plugin.ts index 01a70fd1af040..86896b58d25dc 100644 --- a/x-pack/solutions/observability/plugins/observability/public/plugin.ts +++ b/x-pack/solutions/observability/plugins/observability/public/plugin.ts @@ -5,8 +5,12 @@ * 2.0. */ -import type { CasesPublicStart, CasesPublicSetup } from '@kbn/cases-plugin/public'; -import { CasesDeepLinkId, getCasesDeepLinks } from '@kbn/cases-plugin/public'; +import { + type CasesPublicSetup, + CasesDeepLinkId, + type CasesPublicStart, + getCasesDeepLinks, +} from '@kbn/cases-plugin/public'; import type { DashboardStart } from '@kbn/dashboard-plugin/public'; import type { ChartsPluginStart } from '@kbn/charts-plugin/public'; import type { CloudStart } from '@kbn/cloud-plugin/public'; 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 new file mode 100644 index 0000000000000..a7ccd58e43c0a --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability_shared/public/components/add_page_attachment_to_case_modal/add_page_attachment_to_case_modal.test.tsx @@ -0,0 +1,230 @@ +/* + * 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 { render, screen, fireEvent } from '@testing-library/react'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; +import type { PageAttachmentPersistedState } from '@kbn/page-attachment-schema'; +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'; + +const mockCases: Partial = mockCasesContract(); + +describe('AddPageAttachmentToCaseModal', () => { + const notifications = notificationServiceMock.createStartContract(); + const pageAttachmentState: PageAttachmentPersistedState = { + type: 'example', + url: { + pathAndQuery: 'http://example.com', + actionLabel: 'Go to Example Page', + label: 'Example Page', + iconType: 'globe', + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + + mockCases.helpers = { + canUseCases: jest.fn().mockReturnValue({ + read: true, + update: true, + push: true, + all: true, + create: true, + delete: true, + get: true, + connectors: true, + reopenCase: true, + settings: true, + createComment: true, + getCaseUserActions: true, + assign: true, + }), + getUICapabilities: jest.fn().mockReturnValue({}), + getRuleIdFromEvent: jest.fn().mockReturnValue({}), + groupAlertsByRule: jest.fn().mockReturnValue({}), + }; + }); + + it('renders modal when user has permissions', () => { + mockCases.helpers!.canUseCases = jest.fn().mockImplementationOnce(() => ({ + read: true, + update: true, + push: true, + })); + + render( + + + + ); + + expect(screen.getByText('Add page to case')).toBeInTheDocument(); + }); + + it('does not render modal when user lacks permissions', () => { + mockCases.helpers!.canUseCases = jest.fn().mockImplementationOnce(() => ({ + read: true, + update: true, + push: false, + })); + + render( + + ); + + expect(screen.queryByText('Add page to case')).not.toBeInTheDocument(); + }); + + it('calls onCloseModal when cancel button is clicked', () => { + const onCloseModalMock = jest.fn(); + + render( + + + + ); + + fireEvent.click(screen.getByText('Cancel')); + expect(onCloseModalMock).toHaveBeenCalled(); + }); + + it('opens case modal when confirm button is clicked', () => { + const mockCasesModal = { + open: jest.fn(), + close: jest.fn(), + }; + mockCases.hooks!.useCasesAddToExistingCaseModal = jest.fn(() => mockCasesModal); + render( + + + + ); + + fireEvent.click(screen.getByText('Confirm')); + expect(mockCasesModal.open).toHaveBeenCalled(); + }); + + it('passes correct getAttachments payload when case modal is opened', () => { + const mockCasesModal = { + open: jest.fn(), + close: jest.fn(), + }; + mockCases.hooks!.useCasesAddToExistingCaseModal = jest.fn(() => mockCasesModal); + const comment = 'Test comment'; + + render( + + + + ); + + fireEvent.change(screen.getByRole('textbox'), { target: { value: comment } }); + 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, + summary: comment, + }, + persistableStateAttachmentTypeId: '.page', + type: 'persistableState', + }, + ]); + }); + + it('can update the summary comment', () => { + const mockCasesModal = { + open: jest.fn(), + close: jest.fn(), + }; + mockCases.hooks!.useCasesAddToExistingCaseModal = jest.fn(() => mockCasesModal); + const comment = 'Test comment'; + render( + + + + ); + fireEvent.change(screen.getByRole('textbox'), { target: { value: comment } }); + 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, + summary: comment, + }, + persistableStateAttachmentTypeId: '.page', + type: 'persistableState', + }, + ]); + }); + + it('should trigger a warning toast if hasCasesPermissions is false', () => { + const addWarningMock = jest.spyOn(notifications.toasts, 'addWarning'); + mockCases.helpers!.canUseCases = jest.fn().mockImplementationOnce(() => ({ + read: true, + update: true, + push: false, + })); + + render( + + ); + + expect(addWarningMock).toHaveBeenCalledWith({ + title: expect.stringContaining('Insufficient privileges to add page to case'), + }); + }); +}); 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 new file mode 100644 index 0000000000000..ea9687f048aaf --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability_shared/public/components/add_page_attachment_to_case_modal/add_page_attachment_to_case_modal.tsx @@ -0,0 +1,164 @@ +/* + * 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, { useMemo, useCallback, useState, useEffect } from 'react'; +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 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 { AddToCaseComment } from '../add_to_case_comment'; + +export interface AddPageAttachmentToCaseModalProps { + pageAttachmentState: PageAttachmentPersistedState; + notifications: NotificationsStart; + cases: CasesPublicStart; + onCloseModal: () => void; +} + +export function AddPageAttachmentToCaseModal({ + pageAttachmentState, + cases, + notifications, + onCloseModal, +}: AddPageAttachmentToCaseModalProps) { + const getCasesContext = cases.ui.getCasesContext; + const canUseCases = cases.helpers.canUseCases; + + const casesPermissions: CasesPermissions = useMemo(() => { + if (!canUseCases) { + return { + all: false, + create: false, + read: false, + update: false, + delete: false, + push: false, + connectors: false, + settings: false, + reopenCase: false, + createComment: false, + assign: false, + }; + } + return canUseCases(); + }, [canUseCases]); + + const hasCasesPermissions = useMemo(() => { + return casesPermissions.read && casesPermissions.update && casesPermissions.push; + }, [casesPermissions]); + + const CasesContext = getCasesContext(); + + useEffect(() => { + if (!hasCasesPermissions) { + notifications.toasts.addWarning({ + title: i18n.translate( + 'xpack.observabilityShared.cases.addPageToCaseModal.noPermissionsTitle', + { + defaultMessage: + 'Insufficient privileges to add page to case. Please contact your admin.', + } + ), + }); + } + }, [hasCasesPermissions, notifications.toasts]); + + return hasCasesPermissions ? ( + + + + ) : null; +} + +function AddToCaseButtonContent({ + pageAttachmentState, + cases, + onCloseModal, +}: Omit) { + const [isCommentModalOpen, setIsCommentModalOpen] = useState(true); + const [comment, setComment] = useState(''); + const useCasesAddToExistingCaseModal = cases.hooks.useCasesAddToExistingCaseModal!; + const casesModal = useCasesAddToExistingCaseModal({ + onClose: onCloseModal, + }); + + const handleCloseModal = useCallback(() => { + setIsCommentModalOpen(true); + onCloseModal(); + }, [onCloseModal]); + + const onCommentAdded = useCallback(() => { + setIsCommentModalOpen(false); + casesModal.open({ + getAttachments: () => [ + { + persistableStateAttachmentState: { + ...pageAttachmentState, + summary: comment, + }, + persistableStateAttachmentTypeId: PAGE_ATTACHMENT_TYPE, + type: AttachmentType.persistableState, + }, + ], + }); + }, [casesModal, comment, pageAttachmentState]); + + return isCommentModalOpen ? ( + + + + {i18n.translate('xpack.observabilityShared.cases.addToCaseModal.title', { + defaultMessage: 'Add page to case', + })} + + + + setComment(change)} comment={comment} /> + + + ) : null; +} + +// eslint-disable-next-line import/no-default-export +export default AddPageAttachmentToCaseModal; diff --git a/x-pack/solutions/observability/plugins/observability_shared/public/components/add_page_attachment_to_case_modal/add_page_attachment_to_case_modal_lazy.tsx b/x-pack/solutions/observability/plugins/observability_shared/public/components/add_page_attachment_to_case_modal/add_page_attachment_to_case_modal_lazy.tsx new file mode 100644 index 0000000000000..bc03f420c671f --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability_shared/public/components/add_page_attachment_to_case_modal/add_page_attachment_to_case_modal_lazy.tsx @@ -0,0 +1,17 @@ +/* + * 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 { EuiLoadingSpinner } from '@elastic/eui'; +import { dynamic } from '@kbn/shared-ux-utility'; + +export const AddPageAttachmentToCaseModal = dynamic( + () => import('./add_page_attachment_to_case_modal'), + { + fallback: , + } +); 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 new file mode 100644 index 0000000000000..9be0b2c81365a --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability_shared/public/components/add_to_case_comment/add_to_case_comment.test.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 { render, screen, fireEvent } from '@testing-library/react'; +import { AddToCaseComment } from '.'; + +// Mock i18n +jest.mock('@kbn/i18n-react', () => ({ + FormattedMessage: ({ defaultMessage }: { defaultMessage: string }) => ( + {defaultMessage} + ), + useIntl: () => ({ + formatMessage: ({ defaultMessage }: { defaultMessage: string }) => defaultMessage, + }), +})); + +describe('AddToCaseComment', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('updates the comment when text is entered', () => { + const onCommentChangeMock = jest.fn(); + render(); + + fireEvent.change(screen.getByLabelText('Add a comment (optional)'), { + target: { value: 'New comment' }, + }); + + expect(onCommentChangeMock).toHaveBeenCalledWith('New comment'); + }); +}); 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 new file mode 100644 index 0000000000000..2fc1ccf2abb87 --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability_shared/public/components/add_to_case_comment/index.tsx @@ -0,0 +1,42 @@ +/* + * 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 { EuiTextArea, EuiFormRow } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +interface AddToCaseCommentProps { + comment: string; + onCommentChange: (comment: string) => void; +} + +export function AddToCaseComment({ comment, onCommentChange }: AddToCaseCommentProps) { + const input = ( + { + onCommentChange(e.target.value); + }} + value={comment} + fullWidth + rows={5} + /> + ); + + return ( + <> + + {input} + + + ); +} diff --git a/x-pack/solutions/observability/plugins/observability_shared/public/index.ts b/x-pack/solutions/observability/plugins/observability_shared/public/index.ts index 5c0c4f1bdc7dd..e3332ad1dbf90 100644 --- a/x-pack/solutions/observability/plugins/observability_shared/public/index.ts +++ b/x-pack/solutions/observability/plugins/observability_shared/public/index.ts @@ -34,6 +34,7 @@ export { } from './components/section/section'; export type { SectionLinkProps } from './components/section/section'; export { LoadWhenInView } from './components/load_when_in_view/get_load_when_in_view_lazy'; +export { AddPageAttachmentToCaseModal } from './components/add_page_attachment_to_case_modal/add_page_attachment_to_case_modal_lazy'; export { TagsList } from './components/tags_list/tags_list_lazy'; export type { TagsListProps } from './components/tags_list/tags_list'; diff --git a/x-pack/solutions/observability/plugins/observability_shared/tsconfig.json b/x-pack/solutions/observability/plugins/observability_shared/tsconfig.json index 8cd380fb2e007..2fb67935daeeb 100644 --- a/x-pack/solutions/observability/plugins/observability_shared/tsconfig.json +++ b/x-pack/solutions/observability/plugins/observability_shared/tsconfig.json @@ -49,6 +49,9 @@ "@kbn/deeplinks-observability", "@kbn/home-sample-data-tab", "@kbn/config-schema", + "@kbn/page-attachment-schema", + "@kbn/core-notifications-browser-mocks", + "@kbn/shared-ux-utility", ], "exclude": ["target/**/*", ".storybook/**/*.js"] } diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/actions.test.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/actions.test.tsx index 61eba82c7b962..dc4750641b64b 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/actions.test.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/actions.test.tsx @@ -28,6 +28,18 @@ jest.mock('react-redux', () => ({ useSelector: jest.fn(), })); +jest.mock('./hooks/use_selected_monitor', () => ({ + useSelectedMonitor: jest + .fn() + .mockReturnValue({ monitor: { config_id: 'test-config-id', name: 'Test Monitor' } }), +})); + +jest.mock('../../hooks', () => ({ + useMonitorDetailLocator: jest.fn().mockReturnValue('/mock-monitor-url'), + useGetUrlParams: jest.fn().mockReturnValue({ dateRangeStart: 'now-15m', dateRangeEnd: 'now' }), + useEnablement: jest.fn().mockReturnValue({ isServiceAllowed: true }), +})); + describe('Actions Component', () => { let mockDispatch: jest.Mock; diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/actions.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/actions.tsx index 15f5047a0f283..4164fb2cf61c9 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/actions.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/actions.tsx @@ -47,10 +47,10 @@ export function Actions() { , - , - , - ].concat(isAddToCaseEnabled ? [] : [])} + , + , + , + ].concat(isAddToCaseEnabled ? [] : [])} /> ); 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 0ef12eeb994af..3d8fc810edde6 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 @@ -6,7 +6,7 @@ */ import React from 'react'; -import { render, screen, fireEvent } from '@testing-library/react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { AddToCaseContextItem } from './add_to_case_action'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { useSelectedMonitor } from './hooks/use_selected_monitor'; @@ -25,10 +25,12 @@ jest.mock('../../hooks', () => ({ useMonitorDetailLocator: jest .fn() .mockImplementation( - ({ timeRange, locationId, configId, tabId }) => - `/${tabId}?${locationId ? `locationId=${locationId}&` : ''}dateRangeStart=${ - timeRange.from - }&dateRangeEnd=${timeRange.to}&configId=${configId}` + ({ timeRange, locationId, configId, tabId, useAbsoluteDate }) => + `/history?locationId=${locationId}&dateRangeStart=${ + useAbsoluteDate ? '2025-06-26T12:42:38.568Z' : timeRange.from + }&dateRangeEnd=${ + useAbsoluteDate ? '2025-06-26T12:57:38.568Z' : timeRange.to + }&configId=${configId}` ), })); @@ -94,7 +96,7 @@ describe('AddToCaseContextItem', () => { expect(screen.getByText('Add to case')).toBeInTheDocument(); }); - it('opens the case modal when clicked', () => { + it('opens the page attachment modal when clicked', async () => { mockUseKibana.mockReturnValueOnce({ services: { cases: { @@ -125,26 +127,15 @@ describe('AddToCaseContextItem', () => { render(); fireEvent.click(screen.getByText('Add to case')); - expect(mockOpen).toHaveBeenCalledWith({ - getAttachments: expect.any(Function), - }); - }); - - it('shows an error toast if monitor or URL is missing', () => { - mockUseSelectedMonitor.mockReturnValueOnce({ monitor: null }); - - render(); - fireEvent.click(screen.getByText('Add to case')); - - expect(mockAddDanger).toHaveBeenCalledWith({ - title: 'Error adding monitor to case', - 'data-test-subj': 'monitorAddToCaseError', + await waitFor(() => { + expect(screen.getByText('Add page to case')).toBeInTheDocument(); }); }); it('defines persistableStateAttachmentState correctly when case modal is opened', () => { render(); fireEvent.click(screen.getByText('Add to case')); + fireEvent.click(screen.getByText('Confirm')); const getAttachments = mockOpen.mock.calls[0][0].getAttachments; const attachments = getAttachments(); @@ -160,13 +151,15 @@ describe('AddToCaseContextItem', () => { actionLabel: 'Go to Monitor History', iconType: 'uptimeApp', }, + summary: '', + screenContext: undefined, }, persistableStateAttachmentTypeId: '.page', type: 'persistableState', }); }); - it('converts relative date ranges to absolute date ranges via the component', () => { + it('converts relative date ranges to absolute date ranges via the component', async () => { mockUseGetUrlParams.mockReturnValue({ dateRangeStart: 'now-15m', dateRangeEnd: 'now', @@ -176,6 +169,12 @@ describe('AddToCaseContextItem', () => { render(); fireEvent.click(screen.getByText('Add to case')); + await waitFor(() => { + expect(screen.getByText('Add page to case')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByText('Confirm')); + const getAttachments = mockOpen.mock.calls[0][0].getAttachments; const attachments = getAttachments(); 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 904884e34a043..c6e3c8012c5f3 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 @@ -5,24 +5,33 @@ * 2.0. */ -import React, { useMemo, useCallback } from 'react'; +import React, { useMemo, useCallback, useState } from 'react'; import { EuiContextMenuItem } from '@elastic/eui'; import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { type TimeRange, getAbsoluteTimeRange } from '@kbn/data-plugin/common'; +import { type TimeRange } from '@kbn/data-plugin/common'; import { i18n } from '@kbn/i18n'; -import type { CaseAttachmentsWithoutOwner } from '@kbn/cases-plugin/public'; -import type { PageAttachmentPersistedState } from '@kbn/page-attachment-schema'; +import { AddPageAttachmentToCaseModal } from '@kbn/observability-shared-plugin/public'; import { type CasesPermissions } from '@kbn/cases-plugin/common'; import type { ClientPluginsStart } from '../../../../plugin'; import { useSelectedMonitor } from './hooks/use_selected_monitor'; import { useGetUrlParams, useMonitorDetailLocator } from '../../hooks'; export function AddToCaseContextItem() { - const { - services: { cases }, - } = useKibana(); - const getCasesContext = cases?.ui?.getCasesContext; + const [isAddToCaseModalOpen, setIsAddToCaseModalOpen] = useState(false); + const services = useKibana().services; + const cases = services.cases; const canUseCases = cases?.helpers?.canUseCases; + const notifications = services.notifications; + + const { monitor } = useSelectedMonitor(); + const { dateRangeEnd, dateRangeStart, locationId } = useGetUrlParams(); + const timeRange: TimeRange = useMemo( + () => ({ + from: dateRangeStart, + to: dateRangeEnd, + }), + [dateRangeStart, dateRangeEnd] + ); const casesPermissions: CasesPermissions = useMemo(() => { if (!canUseCases) { @@ -42,47 +51,17 @@ export function AddToCaseContextItem() { } return canUseCases(); }, [canUseCases]); - const hasCasesPermissions = - casesPermissions.read && casesPermissions.update && casesPermissions.push; - const CasesContext = useMemo(() => { - if (!getCasesContext) { - return React.Fragment; - } - return getCasesContext(); - }, [getCasesContext]); - - if (!cases) { - return null; - } - return hasCasesPermissions ? ( - - - - ) : null; -} -function AddToCaseButtonContent() { - const { monitor } = useSelectedMonitor(); - const { dateRangeEnd, dateRangeStart, locationId } = useGetUrlParams(); - const services = useKibana().services; - const notifications = services.notifications; - // type checked in wrapper component - const useCasesAddToExistingCaseModal = services.cases?.hooks?.useCasesAddToExistingCaseModal!; - const casesModal = useCasesAddToExistingCaseModal(); - const timeRange: TimeRange = { - from: dateRangeStart, - to: dateRangeEnd, - }; + const hasCasesPermissions = useMemo(() => { + return casesPermissions.read && casesPermissions.update && casesPermissions.push; + }, [casesPermissions.read, casesPermissions.update, casesPermissions.push]); const redirectUrl = useMonitorDetailLocator({ configId: monitor?.config_id ?? '', - timeRange: convertToAbsoluteTimeRange(timeRange), + timeRange, locationId, tabId: 'history', + useAbsoluteDate: true, }); const onClick = useCallback(() => { @@ -95,63 +74,57 @@ function AddToCaseButtonContent() { }); return; } + setIsAddToCaseModalOpen(true); + }, [redirectUrl, monitor?.name, notifications.toasts]); + + const onCloseModal = useCallback(() => { + setIsAddToCaseModalOpen(false); + }, [setIsAddToCaseModalOpen]); - casesModal.open({ - getAttachments: () => { - const persistableStateAttachmentState: PageAttachmentPersistedState = { - type: 'synthetics_monitor', - url: { - pathAndQuery: redirectUrl, - label: monitor.name, - actionLabel: i18n.translate( - 'xpack.synthetics.cases.addToCaseModal.goToMonitorHistoryActionLabel', - { - defaultMessage: 'Go to Monitor History', - } - ), - iconType: 'uptimeApp', - }, - }; - return [ + const pageState = useMemo(() => { + if (!redirectUrl || !monitor?.name) { + return null; + } + return { + type: 'synthetics_monitor', + url: { + pathAndQuery: redirectUrl, + label: monitor.name, + actionLabel: i18n.translate( + 'xpack.synthetics.cases.addToCaseModal.goToMonitorHistoryLinkLabel', { - persistableStateAttachmentState, - persistableStateAttachmentTypeId: '.page', - type: 'persistableState', - }, - ] as CaseAttachmentsWithoutOwner; + defaultMessage: 'Go to Monitor History', + } + ), + iconType: 'uptimeApp', }, - }); - }, [casesModal, notifications.toasts, monitor?.name, redirectUrl]); - - return ( - - {i18n.translate('xpack.synthetics.cases.addToCaseModal.buttonLabel', { - defaultMessage: 'Add to case', - })} - - ); -} + }; + }, [monitor, redirectUrl]); -export const convertToAbsoluteTimeRange = (timeRange?: TimeRange): TimeRange | undefined => { - if (!timeRange) { - return; + if (!monitor || !redirectUrl || !cases || !hasCasesPermissions || !pageState) { + return null; // Ensure monitor and redirectUrl are available before rendering } - const absRange = getAbsoluteTimeRange( - { - from: timeRange.from, - to: timeRange.to, - }, - { forceNow: new Date() } + return ( + <> + + {i18n.translate('xpack.synthetics.cases.addToCaseModal.buttonLabel', { + defaultMessage: 'Add to case', + })} + + {isAddToCaseModalOpen && ( + + )} + ); - - return { - from: absRange.from, - to: absRange.to, - }; -}; +} diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/run_test_manually.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/run_test_manually.tsx index 805c5b06533af..1f7e6c5a3d9d9 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/run_test_manually.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/run_test_manually.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import React from 'react'; import { EuiContextMenuItem, EuiFlexGroup, @@ -12,7 +13,6 @@ import { EuiIcon, EuiLoadingSpinner, } from '@elastic/eui'; -import React from 'react'; import { i18n } from '@kbn/i18n'; import { useDispatch, useSelector } from 'react-redux'; import { useSyntheticsSettingsContext } from '../../contexts'; 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 ec3fe9810b07c..ab1f97c535a47 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 @@ -5,10 +5,10 @@ * 2.0. */ -import { useEffect, useState } from 'react'; +import { useEffect, useState, useMemo } from 'react'; import { syntheticsMonitorDetailLocatorID } from '@kbn/observability-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'; @@ -19,6 +19,7 @@ export interface MonitorDetailLocatorParams { timeRange?: TimeRange; spaces?: string[]; tabId?: string; + useAbsoluteDate?: boolean; } export function useMonitorDetailLocator({ @@ -27,26 +28,44 @@ export function useMonitorDetailLocator({ spaces, timeRange, tabId, + useAbsoluteDate = false, }: MonitorDetailLocatorParams) { const { space } = useKibanaSpace(); const [monitorUrl, setMonitorUrl] = useState(undefined); - const locator = useKibana().services?.share?.url.locators.get( - syntheticsMonitorDetailLocatorID - ); + const locators = useKibana().services?.share?.url.locators; + const locator = useMemo(() => { + return locators?.get(syntheticsMonitorDetailLocatorID); + }, [locators]); useEffect(() => { - async function generateUrl() { - const url = locator?.getRedirectUrl({ - configId, - locationId, - timeRange, - tabId, - ...getMonitorSpaceToAppend(space, spaces), - }); - setMonitorUrl(url); - } - generateUrl(); - }, [locator, configId, locationId, spaces, space?.id, space, timeRange, tabId]); + const url = locator?.getRedirectUrl({ + configId, + locationId, + timeRange: useAbsoluteDate ? convertToAbsoluteTimeRange(timeRange) : timeRange, + tabId, + ...getMonitorSpaceToAppend(space, spaces), + }); + setMonitorUrl(url); + }, [locator, configId, locationId, spaces, space, timeRange, tabId, useAbsoluteDate]); return monitorUrl; } + +export const convertToAbsoluteTimeRange = (timeRange?: TimeRange): TimeRange | undefined => { + if (!timeRange) { + return; + } + + const absRange = getAbsoluteTimeRange( + { + from: timeRange.from, + to: timeRange.to, + }, + { forceNow: new Date() } + ); + + return { + from: absRange.from, + to: absRange.to, + }; +}; diff --git a/x-pack/solutions/observability/plugins/synthetics/tsconfig.json b/x-pack/solutions/observability/plugins/synthetics/tsconfig.json index db7dabe0544e4..8bf551c5d779b 100644 --- a/x-pack/solutions/observability/plugins/synthetics/tsconfig.json +++ b/x-pack/solutions/observability/plugins/synthetics/tsconfig.json @@ -118,7 +118,6 @@ "@kbn/shared-ux-error-boundary", "@kbn/field-formats-plugin", "@kbn/core-ui-settings-browser", - "@kbn/page-attachment-schema", "@kbn/licensing-types", "@kbn/content-management-plugin" ],