From 77d172f22b35881e93157962683323bc551fcddb Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Tue, 10 Jun 2025 15:22:26 -0400 Subject: [PATCH 01/72] add link attachment type --- .../plugins/shared/dashboard/kibana.jsonc | 3 +- .../dashboard_app/_dashboard_app_strings.ts | 8 +++ .../public/dashboard_app/dashboard_router.tsx | 45 +++++++++------- .../top_nav/cases/add_to_case_modal.tsx | 52 +++++++++++++++++++ .../top_nav/use_dashboard_menu_items.tsx | 14 +++++ .../internal_dashboard_top_nav.tsx | 16 ++++++ .../shared/dashboard/public/plugin.tsx | 2 + .../public/services/kibana_services.ts | 3 ++ .../shared/cases/common/constants/index.ts | 1 + .../shared/cases/common/constants/links.ts | 8 +++ .../plugins/shared/cases/common/index.ts | 1 + .../internal_attachments/link/attachment.tsx | 37 +++++++++++++ .../link/attachment_children.tsx | 27 ++++++++++ .../link/translations.tsx | 15 ++++++ .../internal_attachments/link/types.ts | 13 +++++ .../actions/__mocks__/action_wrapper.tsx | 0 .../actions/action_wrapper.test.tsx | 12 ++--- .../visualizations/actions/action_wrapper.tsx | 10 ++-- .../actions/add_to_existing_case.tsx | 0 .../convert_to_absolute_time_range.test.tsx | 0 .../actions/convert_to_absolute_time_range.ts | 0 .../visualizations/actions/index.ts | 0 .../actions/is_compatible.test.ts | 8 +-- .../visualizations/actions/is_compatible.ts | 4 +- .../visualizations/actions/mocks.ts | 0 .../actions/open_modal.test.tsx | 8 +-- .../visualizations/actions/open_modal.tsx | 4 +- .../visualizations/actions/register.ts | 0 .../visualizations/actions/translations.ts | 0 .../visualizations/actions/types.ts | 4 +- .../visualizations/actions/utils.test.ts | 0 .../visualizations/actions/utils.ts | 6 +-- .../visualizations/attachment.test.tsx | 12 ++--- .../visualizations/attachment.tsx | 6 +-- .../visualizations/index.mock.ts | 0 .../visualizations/lens_renderer.test.tsx | 4 +- .../visualizations/lens_renderer.tsx | 2 +- .../visualizations/open_lens_button.test.tsx | 4 +- .../visualizations/open_lens_button.tsx | 2 +- .../visualizations/translations.tsx | 0 .../visualizations/types.ts | 0 .../markdown_editor/plugins/lens/index.ts | 2 +- .../markdown_editor/plugins/lens/plugin.tsx | 2 +- .../lens/use_lens_open_visualization.tsx | 2 +- .../public/containers/user_profiles/api.ts | 14 ++--- .../use_get_current_user_profile.ts | 4 +- .../public/internal_attachments/index.ts | 4 +- .../plugins/shared/cases/public/plugin.ts | 2 +- .../server/internal_attachments/index.ts | 10 +++- 49 files changed, 292 insertions(+), 79 deletions(-) create mode 100644 src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/add_to_case_modal.tsx create mode 100644 x-pack/platform/plugins/shared/cases/common/constants/links.ts create mode 100644 x-pack/platform/plugins/shared/cases/public/components/internal_attachments/link/attachment.tsx create mode 100644 x-pack/platform/plugins/shared/cases/public/components/internal_attachments/link/attachment_children.tsx create mode 100644 x-pack/platform/plugins/shared/cases/public/components/internal_attachments/link/translations.tsx create mode 100644 x-pack/platform/plugins/shared/cases/public/components/internal_attachments/link/types.ts rename x-pack/platform/plugins/shared/cases/public/components/{ => internal_attachments}/visualizations/actions/__mocks__/action_wrapper.tsx (100%) rename x-pack/platform/plugins/shared/cases/public/components/{ => internal_attachments}/visualizations/actions/action_wrapper.test.tsx (91%) rename x-pack/platform/plugins/shared/cases/public/components/{ => internal_attachments}/visualizations/actions/action_wrapper.tsx (87%) rename x-pack/platform/plugins/shared/cases/public/components/{ => internal_attachments}/visualizations/actions/add_to_existing_case.tsx (100%) rename x-pack/platform/plugins/shared/cases/public/components/{ => internal_attachments}/visualizations/actions/convert_to_absolute_time_range.test.tsx (100%) rename x-pack/platform/plugins/shared/cases/public/components/{ => internal_attachments}/visualizations/actions/convert_to_absolute_time_range.ts (100%) rename x-pack/platform/plugins/shared/cases/public/components/{ => internal_attachments}/visualizations/actions/index.ts (100%) rename x-pack/platform/plugins/shared/cases/public/components/{ => internal_attachments}/visualizations/actions/is_compatible.test.ts (86%) rename x-pack/platform/plugins/shared/cases/public/components/{ => internal_attachments}/visualizations/actions/is_compatible.ts (88%) rename x-pack/platform/plugins/shared/cases/public/components/{ => internal_attachments}/visualizations/actions/mocks.ts (100%) rename x-pack/platform/plugins/shared/cases/public/components/{ => internal_attachments}/visualizations/actions/open_modal.test.tsx (95%) rename x-pack/platform/plugins/shared/cases/public/components/{ => internal_attachments}/visualizations/actions/open_modal.tsx (95%) rename x-pack/platform/plugins/shared/cases/public/components/{ => internal_attachments}/visualizations/actions/register.ts (100%) rename x-pack/platform/plugins/shared/cases/public/components/{ => internal_attachments}/visualizations/actions/translations.ts (100%) rename x-pack/platform/plugins/shared/cases/public/components/{ => internal_attachments}/visualizations/actions/types.ts (84%) rename x-pack/platform/plugins/shared/cases/public/components/{ => internal_attachments}/visualizations/actions/utils.test.ts (100%) rename x-pack/platform/plugins/shared/cases/public/components/{ => internal_attachments}/visualizations/actions/utils.ts (84%) rename x-pack/platform/plugins/shared/cases/public/components/{ => internal_attachments}/visualizations/attachment.test.tsx (91%) rename x-pack/platform/plugins/shared/cases/public/components/{ => internal_attachments}/visualizations/attachment.tsx (92%) rename x-pack/platform/plugins/shared/cases/public/components/{ => internal_attachments}/visualizations/index.mock.ts (100%) rename x-pack/platform/plugins/shared/cases/public/components/{ => internal_attachments}/visualizations/lens_renderer.test.tsx (94%) rename x-pack/platform/plugins/shared/cases/public/components/{ => internal_attachments}/visualizations/lens_renderer.tsx (96%) rename x-pack/platform/plugins/shared/cases/public/components/{ => internal_attachments}/visualizations/open_lens_button.test.tsx (94%) rename x-pack/platform/plugins/shared/cases/public/components/{ => internal_attachments}/visualizations/open_lens_button.tsx (96%) rename x-pack/platform/plugins/shared/cases/public/components/{ => internal_attachments}/visualizations/translations.tsx (100%) rename x-pack/platform/plugins/shared/cases/public/components/{ => internal_attachments}/visualizations/types.ts (100%) diff --git a/src/platform/plugins/shared/dashboard/kibana.jsonc b/src/platform/plugins/shared/dashboard/kibana.jsonc index c03b46f7a5b59..dece2ea4ab314 100644 --- a/src/platform/plugins/shared/dashboard/kibana.jsonc +++ b/src/platform/plugins/shared/dashboard/kibana.jsonc @@ -26,7 +26,8 @@ "uiActions", "urlForwarding", "presentationUtil", - "unifiedSearch" + "unifiedSearch", + "cases" ], "optionalPlugins": [ "home", diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/_dashboard_app_strings.ts b/src/platform/plugins/shared/dashboard/public/dashboard_app/_dashboard_app_strings.ts index 16e865a1b6155..d4a0597d8ca2a 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/_dashboard_app_strings.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/_dashboard_app_strings.ts @@ -246,6 +246,14 @@ export const topNavStrings = { defaultMessage: 'Create a copy of your dashboard', }), }, + addToCase: { + label: i18n.translate('dashboard.topNave.addToCaseButtonAriaLabel', { + defaultMessage: 'add to case', + }), + description: i18n.translate('dashboard.topNave.addToCaseConfigDescription', { + defaultMessage: 'Add this dashboard to a case', + }), + }, }; export const getControlButtonTitle = () => diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/dashboard_router.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_app/dashboard_router.tsx index a17fbe3e1908c..2d2366d8f0eb1 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/dashboard_router.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/dashboard_router.tsx @@ -12,6 +12,7 @@ import './_dashboard_app.scss'; import { AppMountParameters, CoreStart } from '@kbn/core/public'; import { createKbnUrlStateStorage, withNotifyOnErrors } from '@kbn/kibana-utils-plugin/public'; import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { Route, Routes } from '@kbn/shared-ux-router'; import { parse, ParsedQuery } from 'query-string'; import React from 'react'; @@ -146,25 +147,31 @@ export async function mountApp({ const app = ( - - - - - - - - - - - - + + + + + + + + + + + + + + ); diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/add_to_case_modal.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/add_to_case_modal.tsx new file mode 100644 index 0000000000000..bd64d667e3a06 --- /dev/null +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/add_to_case_modal.tsx @@ -0,0 +1,52 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React, { useEffect } from 'react'; +import { AttachmentType } from '@kbn/cases-plugin/common'; +import { LINK_ATTACHMENT_TYPE } from '@kbn/cases-plugin/common'; +import { DASHBOARD_APP_LOCATOR } from '@kbn/deeplinks-analytics'; +import { DashboardLocatorParams } from '../../../../common/types'; +import { cases, shareService } from '../../../services/kibana_services'; + +export const AddToCaseOpenModal = ({ + savedObjectId, + dashboardTitle, +}: { + savedObjectId: string; + dashboardTitle: string; +}) => { + const { + hooks: { useCasesAddToExistingCaseModal }, + } = cases; + const { url: urlService } = shareService; + const dashboardLocator = urlService.locators.get(DASHBOARD_APP_LOCATOR); + const casesModal = useCasesAddToExistingCaseModal({ + onClose: () => {}, + onSuccess: () => {}, + }); + + useEffect(() => { + const url = dashboardLocator.getRedirectUrl({ + dashboardId: savedObjectId, + }); + const attachments = { + persistableStateAttachmentState: { + pathname: url, + label: dashboardTitle, + type: 'dashboard', + icon: 'dashboardApp', + }, + persistableStateAttachmentTypeId: LINK_ATTACHMENT_TYPE, + type: AttachmentType.persistableState, + }; + casesModal.open({ getAttachments: () => [attachments] }); + }, [casesModal, dashboardLocator, savedObjectId, dashboardTitle]); + + return <>; +}; diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx index 52d132b8d5582..7dbe30a18fe64 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx @@ -27,11 +27,13 @@ import { ShowShareModal } from './share/show_share_modal'; export const useDashboardMenuItems = ({ isLabsShown, setIsLabsShown, + setIsAddToCaseModalOpen, maybeRedirect, showResetChange, }: { isLabsShown: boolean; setIsLabsShown: Dispatch>; + setIsAddToCaseModalOpen: Dispatch>; maybeRedirect: (result?: SaveDashboardReturn) => void; showResetChange?: boolean; }) => { @@ -214,6 +216,14 @@ export const useDashboardMenuItems = ({ disableButton: disableTopNav, run: () => openSettingsFlyout(dashboardApi), }, + + addToCase: { + ...topNavStrings.addToCase, + id: 'add-to-case', + testId: 'dashboardAddToCaseButton', + disableButton: disableTopNav, + run: () => setIsAddToCaseModalOpen(true), + }, }; }, [ disableTopNav, @@ -229,6 +239,7 @@ export const useDashboardMenuItems = ({ quickSaveDashboard, resetChanges, isResetting, + setIsAddToCaseModalOpen, ]); const resetChangesMenuItem = useMemo(() => { @@ -275,6 +286,7 @@ export const useDashboardMenuItems = ({ ].filter(Boolean) as TopNavMenuData[]) : []; const duplicateMenuItem = showWriteControls ? [menuItems.interactiveSave] : []; + const addToCaseMenuItem = [menuItems.addToCase]; const editMenuItem = showWriteControls && !dashboardApi.isManaged ? [menuItems.edit] : []; const mayberesetChangesMenuItem = showResetChange ? [resetChangesMenuItem] : []; @@ -282,6 +294,7 @@ export const useDashboardMenuItems = ({ ...labsMenuItem, menuItems.fullScreen, ...duplicateMenuItem, + ...addToCaseMenuItem, ...mayberesetChangesMenuItem, ...shareMenuItem, ...editMenuItem, @@ -294,6 +307,7 @@ export const useDashboardMenuItems = ({ menuItems.interactiveSave, menuItems.edit, menuItems.fullScreen, + menuItems.addToCase, hasExportIntegration, dashboardApi.isManaged, showResetChange, diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx index 8a467567972ce..09e926ffbda87 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx @@ -55,6 +55,8 @@ import { getDashboardCapabilities } from '../utils/get_dashboard_capabilities'; import './_dashboard_top_nav.scss'; import { getFullEditPath } from '../utils/urls'; import { DashboardFavoriteButton } from './dashboard_favorite_button'; +import { cases } from '../services/kibana_services'; +import { AddToCaseOpenModal } from '../dashboard_app/top_nav/cases/add_to_case_modal'; export interface InternalDashboardTopNavProps { customLeadingBreadCrumbs?: EuiBreadcrumb[]; @@ -79,9 +81,17 @@ export function InternalDashboardTopNav({ }: InternalDashboardTopNavProps) { const [isChromeVisible, setIsChromeVisible] = useState(false); const [isLabsShown, setIsLabsShown] = useState(false); + const [isAddToCaseModalOpen, setIsAddToCaseModalOpen] = useState(false); const dashboardTitleRef = useRef(null); + const { + ui: { getCasesContext }, + helpers: { canUseCases }, + } = cases; + const isLabsEnabled = useMemo(() => coreServices.uiSettings.get(UI_SETTINGS.ENABLE_LABS_UI), []); + const canCases = useMemo(() => canUseCases(), [canUseCases]); + const CasesContext = useMemo(() => getCasesContext(), [getCasesContext]); const { setHeaderActionMenu, onAppLeave } = useDashboardMountContext(); const dashboardApi = useDashboardApi(); @@ -256,6 +266,7 @@ export function InternalDashboardTopNav({ const { viewModeTopNavConfig, editModeTopNavConfig } = useDashboardMenuItems({ isLabsShown, setIsLabsShown, + setIsAddToCaseModalOpen, maybeRedirect, showResetChange, }); @@ -381,6 +392,11 @@ export function InternalDashboardTopNav({ {viewMode !== 'print' && isLabsEnabled && isLabsShown ? ( setIsLabsShown(false)} /> ) : null} + {isAddToCaseModalOpen && canCases ? ( + + + + ) : null} {viewMode === 'edit' ? : null} {showBorderBottom && } diff --git a/src/platform/plugins/shared/dashboard/public/plugin.tsx b/src/platform/plugins/shared/dashboard/public/plugin.tsx index e3d96f50316bd..e0e183c4d2e6a 100644 --- a/src/platform/plugins/shared/dashboard/public/plugin.tsx +++ b/src/platform/plugins/shared/dashboard/public/plugin.tsx @@ -32,6 +32,7 @@ import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public'; import type { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/public'; import { FieldFormatsStart } from '@kbn/field-formats-plugin/public/plugin'; import type { HomePublicPluginSetup } from '@kbn/home-plugin/public'; +import type { CasesPublicStart } from '@kbn/cases-plugin/public'; import { i18n } from '@kbn/i18n'; import type { Start as InspectorStartContract } from '@kbn/inspector-plugin/public'; import { replaceUrlHashQuery } from '@kbn/kibana-utils-plugin/common'; @@ -115,6 +116,7 @@ export interface DashboardStartDependencies { serverless?: ServerlessPluginStart; noDataPage?: NoDataPagePluginStart; lens?: LensPublicStart; + cases?: CasesPublicStart; observabilityAIAssistant?: ObservabilityAIAssistantPublicStart; } diff --git a/src/platform/plugins/shared/dashboard/public/services/kibana_services.ts b/src/platform/plugins/shared/dashboard/public/services/kibana_services.ts index 9aed7fffb6497..3cfefc77a1c55 100644 --- a/src/platform/plugins/shared/dashboard/public/services/kibana_services.ts +++ b/src/platform/plugins/shared/dashboard/public/services/kibana_services.ts @@ -11,6 +11,7 @@ import { BehaviorSubject } from 'rxjs'; import type { ContentManagementPublicStart } from '@kbn/content-management-plugin/public'; import type { CoreStart } from '@kbn/core/public'; +import type { CasesPublicStart } from '@kbn/cases-plugin/public'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public'; import type { EmbeddableStart } from '@kbn/embeddable-plugin/public'; @@ -50,6 +51,7 @@ export let spacesService: SpacesApi | undefined; export let uiActionsService: UiActionsPublicStart; export let urlForwardingService: UrlForwardingStart; export let usageCollectionService: UsageCollectionStart | undefined; +export let cases: CasesPublicStart | undefined; const servicesReady$ = new BehaviorSubject(false); @@ -73,6 +75,7 @@ export const setKibanaServices = (kibanaCore: CoreStart, deps: DashboardStartDep uiActionsService = deps.uiActions; urlForwardingService = deps.urlForwarding; usageCollectionService = deps.usageCollection; + cases = deps.cases; servicesReady$.next(true); }; diff --git a/x-pack/platform/plugins/shared/cases/common/constants/index.ts b/x-pack/platform/plugins/shared/cases/common/constants/index.ts index 475b32644d449..043a33b058c99 100644 --- a/x-pack/platform/plugins/shared/cases/common/constants/index.ts +++ b/x-pack/platform/plugins/shared/cases/common/constants/index.ts @@ -11,6 +11,7 @@ export * from './owners'; export * from './files'; export * from './application'; export { LENS_ATTACHMENT_TYPE } from './visualizations'; +export { LINK_ATTACHMENT_TYPE } from './links'; export const DEFAULT_DATE_FORMAT = 'dateFormat' as const; export const DEFAULT_DATE_FORMAT_TZ = 'dateFormat:tz' as const; diff --git a/x-pack/platform/plugins/shared/cases/common/constants/links.ts b/x-pack/platform/plugins/shared/cases/common/constants/links.ts new file mode 100644 index 0000000000000..449c91b116353 --- /dev/null +++ b/x-pack/platform/plugins/shared/cases/common/constants/links.ts @@ -0,0 +1,8 @@ +/* + * 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. + */ + +export const LINK_ATTACHMENT_TYPE = '.link'; diff --git a/x-pack/platform/plugins/shared/cases/common/index.ts b/x-pack/platform/plugins/shared/cases/common/index.ts index 37f0115f027fe..b0c1781bc4a68 100644 --- a/x-pack/platform/plugins/shared/cases/common/index.ts +++ b/x-pack/platform/plugins/shared/cases/common/index.ts @@ -50,6 +50,7 @@ export { UPDATE_CASES_CAPABILITY, INTERNAL_BULK_GET_CASES_URL, LENS_ATTACHMENT_TYPE, + LINK_ATTACHMENT_TYPE, INTERNAL_BULK_CREATE_ATTACHMENTS_URL, SAVED_OBJECT_TYPES, CASE_COMMENT_SAVED_OBJECT, diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/link/attachment.tsx b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/link/attachment.tsx new file mode 100644 index 0000000000000..dc586e0e048bb --- /dev/null +++ b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/link/attachment.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 { LINK_ATTACHMENT_TYPE } from '../../../../common/constants/links'; +import * as i18n from './translations'; + +import type { + PersistableStateAttachmentType, + PersistableStateAttachmentViewProps, +} from '../../../client/attachment_framework/types'; + +const AttachmentChildrenLazy = React.lazy(() => import('./attachment_children')); + +const getLinkAttachmentViewObject = () => { + return { + event: i18n.ADDED_LINK, + timelineAvatar: 'link', + hideDefaultActions: false, + children: AttachmentChildrenLazy as unknown as React.LazyExoticComponent< + React.FC + >, + }; +}; + +export const getLinkAttachmentType = (): PersistableStateAttachmentType => ({ + id: LINK_ATTACHMENT_TYPE, + icon: 'link', + displayName: 'Links', + getAttachmentViewObject: getLinkAttachmentViewObject, + getAttachmentRemovalObject: () => ({ event: i18n.REMOVED_LINK }), +}); diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/link/attachment_children.tsx b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/link/attachment_children.tsx new file mode 100644 index 0000000000000..23ec5ae5aa8a1 --- /dev/null +++ b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/link/attachment_children.tsx @@ -0,0 +1,27 @@ +/* + * 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 { EuiLink } from '@elastic/eui'; +import type { LinkAttachmentPersistedState } from './types'; + +interface AttachmentChildrenProps { + persistableStateAttachmentState: LinkAttachmentPersistedState; +} + +export const LinkAttachmentChildren: React.FC = ({ + persistableStateAttachmentState, +}) => { + const { pathname, label } = persistableStateAttachmentState; + return {label}; +}; + +LinkAttachmentChildren.displayName = 'LinkAttachmentChildren'; + +// Note: This is for lazy loading +// eslint-disable-next-line import/no-default-export +export default LinkAttachmentChildren; diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/link/translations.tsx b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/link/translations.tsx new file mode 100644 index 0000000000000..e44ca70c86f7b --- /dev/null +++ b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/link/translations.tsx @@ -0,0 +1,15 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const ADDED_LINK = i18n.translate('xpack.cases.caseView.links.addedLink', { + defaultMessage: 'added link', +}); +export const REMOVED_LINK = i18n.translate('xpack.cases.caseView.links.removedLink', { + defaultMessage: 'removed link', +}); diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/link/types.ts b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/link/types.ts new file mode 100644 index 0000000000000..e36ce3caac6da --- /dev/null +++ b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/link/types.ts @@ -0,0 +1,13 @@ +/* + * 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. + */ + +export interface LinkAttachmentPersistedState { + pathname: string; + type: string; + icon: string; + label: string; +} diff --git a/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/__mocks__/action_wrapper.tsx b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/__mocks__/action_wrapper.tsx similarity index 100% rename from x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/__mocks__/action_wrapper.tsx rename to x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/__mocks__/action_wrapper.tsx diff --git a/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/action_wrapper.test.tsx b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/action_wrapper.test.tsx similarity index 91% rename from x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/action_wrapper.test.tsx rename to x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/action_wrapper.test.tsx index 8437da2f28b9c..add585362ca3e 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/action_wrapper.test.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/action_wrapper.test.tsx @@ -7,19 +7,19 @@ import { render } from '@testing-library/react'; import React from 'react'; -import { SECURITY_SOLUTION_OWNER } from '../../../../common'; -import { canUseCases } from '../../../client/helpers/can_use_cases'; -import CasesProvider from '../../cases_context'; +import { SECURITY_SOLUTION_OWNER } from '../../../../../common'; +import { canUseCases } from '../../../../client/helpers/can_use_cases'; +import CasesProvider from '../../../cases_context'; import { ActionWrapper } from './action_wrapper'; import { getMockServices } from './mocks'; import type { CasesActionContextProps } from './types'; -jest.mock('../../cases_context', () => +jest.mock('../../../cases_context', () => jest.fn().mockImplementation(({ children, ...props }) =>
{children}
) ); -jest.mock('../../../client/helpers/can_use_cases', () => { - const actual = jest.requireActual('../../../client/helpers/can_use_cases'); +jest.mock('../../../../client/helpers/can_use_cases', () => { + const actual = jest.requireActual('../../../../client/helpers/can_use_cases'); return { ...actual, canUseCases: jest.fn(), diff --git a/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/action_wrapper.tsx b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/action_wrapper.tsx similarity index 87% rename from x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/action_wrapper.tsx rename to x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/action_wrapper.tsx index e5f931492dabd..b2b0b79cfd2cb 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/action_wrapper.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/action_wrapper.tsx @@ -10,12 +10,12 @@ import React from 'react'; import { Router } from '@kbn/shared-ux-router'; import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; -import { SECURITY_SOLUTION_OWNER } from '../../../../common'; +import { SECURITY_SOLUTION_OWNER } from '../../../../../common'; import type { CasesActionContextProps, Services } from './types'; -import { KibanaContextProvider, useKibana } from '../../../common/lib/kibana'; -import CasesProvider from '../../cases_context'; -import { getCaseOwnerByAppId } from '../../../../common/utils/owner'; -import { canUseCases } from '../../../client/helpers/can_use_cases'; +import { KibanaContextProvider, useKibana } from '../../../../common/lib/kibana'; +import CasesProvider from '../../../cases_context'; +import { getCaseOwnerByAppId } from '../../../../../common/utils/owner'; +import { canUseCases } from '../../../../client/helpers/can_use_cases'; export const DEFAULT_DARK_MODE = 'theme:darkMode' as const; diff --git a/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/add_to_existing_case.tsx b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/add_to_existing_case.tsx similarity index 100% rename from x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/add_to_existing_case.tsx rename to x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/add_to_existing_case.tsx diff --git a/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/convert_to_absolute_time_range.test.tsx b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/convert_to_absolute_time_range.test.tsx similarity index 100% rename from x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/convert_to_absolute_time_range.test.tsx rename to x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/convert_to_absolute_time_range.test.tsx diff --git a/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/convert_to_absolute_time_range.ts b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/convert_to_absolute_time_range.ts similarity index 100% rename from x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/convert_to_absolute_time_range.ts rename to x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/convert_to_absolute_time_range.ts diff --git a/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/index.ts b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/index.ts similarity index 100% rename from x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/index.ts rename to x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/index.ts diff --git a/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/is_compatible.test.ts b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/is_compatible.test.ts similarity index 86% rename from x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/is_compatible.test.ts rename to x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/is_compatible.test.ts index 40280083e3cbb..ded572d3d0491 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/is_compatible.test.ts +++ b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/is_compatible.test.ts @@ -8,15 +8,15 @@ import { BehaviorSubject } from 'rxjs'; import { coreMock } from '@kbn/core/public/mocks'; import { isCompatible } from './is_compatible'; -import { canUseCases } from '../../../client/helpers/can_use_cases'; +import { canUseCases } from '../../../../client/helpers/can_use_cases'; import { getMockLensApi } from './mocks'; -jest.mock('../../../../common/utils/owner', () => ({ +jest.mock('../../../../../common/utils/owner', () => ({ getCaseOwnerByAppId: () => 'securitySolution', })); -jest.mock('../../../client/helpers/can_use_cases', () => { - const actual = jest.requireActual('../../../client/helpers/can_use_cases'); +jest.mock('../../../../client/helpers/can_use_cases', () => { + const actual = jest.requireActual('../../../../client/helpers/can_use_cases'); return { ...actual, canUseCases: jest.fn(), diff --git a/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/is_compatible.ts b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/is_compatible.ts similarity index 88% rename from x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/is_compatible.ts rename to x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/is_compatible.ts index 90347cb8d4067..d31bb7390d904 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/is_compatible.ts +++ b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/is_compatible.ts @@ -8,8 +8,8 @@ import type { CoreStart } from '@kbn/core-lifecycle-browser'; import { isLensApi } from '@kbn/lens-plugin/public'; import { apiPublishesTimeRange, hasBlockingError } from '@kbn/presentation-publishing'; -import { canUseCases } from '../../../client/helpers/can_use_cases'; -import { getCaseOwnerByAppId } from '../../../../common/utils/owner'; +import { canUseCases } from '../../../../client/helpers/can_use_cases'; +import { getCaseOwnerByAppId } from '../../../../../common/utils/owner'; export function isCompatible( embeddable: unknown, diff --git a/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/mocks.ts b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/mocks.ts similarity index 100% rename from x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/mocks.ts rename to x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/mocks.ts diff --git a/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/open_modal.test.tsx b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/open_modal.test.tsx similarity index 95% rename from x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/open_modal.test.tsx rename to x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/open_modal.test.tsx index 41cf590c4edb3..de8c24aa06b9a 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/open_modal.test.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/open_modal.test.tsx @@ -6,7 +6,7 @@ */ import { unmountComponentAtNode } from 'react-dom'; -import { useCasesAddToExistingCaseModal } from '../../all_cases/selector_modal/use_cases_add_to_existing_case_modal'; +import { useCasesAddToExistingCaseModal } from '../../../all_cases/selector_modal/use_cases_add_to_existing_case_modal'; import type { PropsWithChildren } from 'react'; import React from 'react'; import { @@ -16,7 +16,7 @@ import { mockLensAttributes, getMockServices, } from './mocks'; -import { useKibana } from '../../../common/lib/kibana'; +import { useKibana } from '../../../../common/lib/kibana'; import { waitFor } from '@testing-library/react'; import { openModal } from './open_modal'; import type { CasesActionContextProps } from './types'; @@ -24,7 +24,7 @@ import type { CasesActionContextProps } from './types'; const element = document.createElement('div'); document.body.appendChild(element); -jest.mock('../../all_cases/selector_modal/use_cases_add_to_existing_case_modal', () => ({ +jest.mock('../../../all_cases/selector_modal/use_cases_add_to_existing_case_modal', () => ({ useCasesAddToExistingCaseModal: jest.fn(), })); @@ -34,7 +34,7 @@ jest.mock('@kbn/kibana-react-plugin/public', () => ({ .mockImplementation(({ children }: PropsWithChildren) => <>{children}), })); -jest.mock('../../../common/lib/kibana', () => { +jest.mock('../../../../common/lib/kibana', () => { return { useKibana: jest.fn(), KibanaContextProvider: jest diff --git a/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/open_modal.tsx b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/open_modal.tsx similarity index 95% rename from x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/open_modal.tsx rename to x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/open_modal.tsx index 1175a4c928006..13f88047d71d5 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/open_modal.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/open_modal.tsx @@ -13,9 +13,9 @@ import { apiPublishesTimeRange, useStateFromPublishingSubject } from '@kbn/prese import { BehaviorSubject } from 'rxjs'; import { ActionWrapper } from './action_wrapper'; import type { CasesActionContextProps, Services } from './types'; -import type { CaseUI } from '../../../../common'; +import type { CaseUI } from '../../../../../common'; import { getLensCaseAttachment } from './utils'; -import { useCasesAddToExistingCaseModal } from '../../all_cases/selector_modal/use_cases_add_to_existing_case_modal'; +import { useCasesAddToExistingCaseModal } from '../../../all_cases/selector_modal/use_cases_add_to_existing_case_modal'; import { convertToAbsoluteTimeRange } from './convert_to_absolute_time_range'; interface Props { diff --git a/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/register.ts b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/register.ts similarity index 100% rename from x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/register.ts rename to x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/register.ts diff --git a/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/translations.ts b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/translations.ts similarity index 100% rename from x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/translations.ts rename to x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/translations.ts diff --git a/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/types.ts b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/types.ts similarity index 84% rename from x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/types.ts rename to x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/types.ts index b1ccf7bad921c..fba01f3a32e14 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/types.ts +++ b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/types.ts @@ -9,8 +9,8 @@ import type { CoreStart } from '@kbn/core/public'; import type * as H from 'history'; import type { Storage } from '@kbn/kibana-utils-plugin/public'; -import type { CasesPublicStartDependencies } from '../../../types'; -import type { CasesContextProps } from '../../cases_context'; +import type { CasesPublicStartDependencies } from '../../../../types'; +import type { CasesContextProps } from '../../../cases_context'; export type CasesActionContextProps = Pick< CasesContextProps, diff --git a/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/utils.test.ts b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/utils.test.ts similarity index 100% rename from x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/utils.test.ts rename to x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/utils.test.ts diff --git a/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/utils.ts b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/utils.ts similarity index 84% rename from x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/utils.ts rename to x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/utils.ts index e51f1fb13bdee..7f0bab409712b 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/utils.ts +++ b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/utils.ts @@ -5,9 +5,9 @@ * 2.0. */ import type { LensSavedObjectAttributes } from '@kbn/lens-plugin/public'; -import { LENS_ATTACHMENT_TYPE } from '../../../../common/constants/visualizations'; -import type { PersistableStateAttachmentPayload } from '../../../../common/types/domain'; -import { AttachmentType } from '../../../../common/types/domain'; +import { LENS_ATTACHMENT_TYPE } from '../../../../../common/constants/visualizations'; +import type { PersistableStateAttachmentPayload } from '../../../../../common/types/domain'; +import { AttachmentType } from '../../../../../common/types/domain'; import type { LensProps } from '../types'; type PersistableStateAttachmentWithoutOwner = Omit; diff --git a/x-pack/platform/plugins/shared/cases/public/components/visualizations/attachment.test.tsx b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/attachment.test.tsx similarity index 91% rename from x-pack/platform/plugins/shared/cases/public/components/visualizations/attachment.test.tsx rename to x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/attachment.test.tsx index 1805b1b88a555..0b512c918565f 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/visualizations/attachment.test.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/attachment.test.tsx @@ -7,14 +7,14 @@ import React, { Suspense } from 'react'; import { screen, waitFor } from '@testing-library/react'; -import { LENS_ATTACHMENT_TYPE } from '../../../common'; -import type { PersistableStateAttachmentViewProps } from '../../client/attachment_framework/types'; -import { AttachmentActionType } from '../../client/attachment_framework/types'; +import { LENS_ATTACHMENT_TYPE } from '../../../../common'; +import type { PersistableStateAttachmentViewProps } from '../../../client/attachment_framework/types'; +import { AttachmentActionType } from '../../../client/attachment_framework/types'; -import { basicCase } from '../../containers/mock'; +import { basicCase } from '../../../containers/mock'; import { getVisualizationAttachmentType } from './attachment'; -import { createStartServicesMock } from '../../common/lib/kibana/kibana_react.mock'; -import { renderWithTestingProviders } from '../../common/mock'; +import { createStartServicesMock } from '../../../common/lib/kibana/kibana_react.mock'; +import { renderWithTestingProviders } from '../../../common/mock'; describe('getVisualizationAttachmentType', () => { const mockEmbeddableComponent = jest diff --git a/x-pack/platform/plugins/shared/cases/public/components/visualizations/attachment.tsx b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/attachment.tsx similarity index 92% rename from x-pack/platform/plugins/shared/cases/public/components/visualizations/attachment.tsx rename to x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/attachment.tsx index 013187afc5481..c57b5f0ec89ae 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/visualizations/attachment.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/attachment.tsx @@ -8,14 +8,14 @@ import React from 'react'; import deepEqual from 'fast-deep-equal'; -import { LENS_ATTACHMENT_TYPE } from '../../../common/constants/visualizations'; +import { LENS_ATTACHMENT_TYPE } from '../../../../common/constants/visualizations'; import * as i18n from './translations'; import type { PersistableStateAttachmentType, PersistableStateAttachmentViewProps, -} from '../../client/attachment_framework/types'; -import { AttachmentActionType } from '../../client/attachment_framework/types'; +} from '../../../client/attachment_framework/types'; +import { AttachmentActionType } from '../../../client/attachment_framework/types'; import type { LensProps } from './types'; import { OpenLensButton } from './open_lens_button'; import { LensRenderer } from './lens_renderer'; diff --git a/x-pack/platform/plugins/shared/cases/public/components/visualizations/index.mock.ts b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/index.mock.ts similarity index 100% rename from x-pack/platform/plugins/shared/cases/public/components/visualizations/index.mock.ts rename to x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/index.mock.ts diff --git a/x-pack/platform/plugins/shared/cases/public/components/visualizations/lens_renderer.test.tsx b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/lens_renderer.test.tsx similarity index 94% rename from x-pack/platform/plugins/shared/cases/public/components/visualizations/lens_renderer.test.tsx rename to x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/lens_renderer.test.tsx index 0a4b6f4f96bb9..fe115698010af 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/visualizations/lens_renderer.test.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/lens_renderer.test.tsx @@ -10,8 +10,8 @@ import { screen } from '@testing-library/react'; import { LensRenderer } from './lens_renderer'; import { lensVisualization } from './index.mock'; -import { createStartServicesMock } from '../../common/lib/kibana/kibana_react.mock'; -import { renderWithTestingProviders } from '../../common/mock'; +import { createStartServicesMock } from '../../../common/lib/kibana/kibana_react.mock'; +import { renderWithTestingProviders } from '../../../common/mock'; describe('LensRenderer', () => { const mockEmbeddableComponent = jest diff --git a/x-pack/platform/plugins/shared/cases/public/components/visualizations/lens_renderer.tsx b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/lens_renderer.tsx similarity index 96% rename from x-pack/platform/plugins/shared/cases/public/components/visualizations/lens_renderer.tsx rename to x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/lens_renderer.tsx index 8596b8f8bd30f..f805fab8c116a 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/visualizations/lens_renderer.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/lens_renderer.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { Global, css } from '@emotion/react'; import { EuiSpacer, useEuiTheme } from '@elastic/eui'; -import { useKibana } from '../../common/lib/kibana'; +import { useKibana } from '../../../common/lib/kibana'; import type { LensProps } from './types'; const LENS_VISUALIZATION_HEIGHT = 200; diff --git a/x-pack/platform/plugins/shared/cases/public/components/visualizations/open_lens_button.test.tsx b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/open_lens_button.test.tsx similarity index 94% rename from x-pack/platform/plugins/shared/cases/public/components/visualizations/open_lens_button.test.tsx rename to x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/open_lens_button.test.tsx index 8f8a2063ec8a1..442f0f2e0a926 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/visualizations/open_lens_button.test.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/open_lens_button.test.tsx @@ -12,8 +12,8 @@ import { screen } from '@testing-library/react'; import { OpenLensButton } from './open_lens_button'; import { lensVisualization } from './index.mock'; import userEvent from '@testing-library/user-event'; -import { createStartServicesMock } from '../../common/lib/kibana/kibana_react.mock'; -import { renderWithTestingProviders } from '../../common/mock'; +import { createStartServicesMock } from '../../../common/lib/kibana/kibana_react.mock'; +import { renderWithTestingProviders } from '../../../common/mock'; describe('OpenLensButton', () => { const props = { diff --git a/x-pack/platform/plugins/shared/cases/public/components/visualizations/open_lens_button.tsx b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/open_lens_button.tsx similarity index 96% rename from x-pack/platform/plugins/shared/cases/public/components/visualizations/open_lens_button.tsx rename to x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/open_lens_button.tsx index c6a49bb285a2f..aa164ab4360fa 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/visualizations/open_lens_button.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/open_lens_button.tsx @@ -8,7 +8,7 @@ import { isOfAggregateQueryType } from '@kbn/es-query'; import { EuiButtonEmpty } from '@elastic/eui'; import React, { useCallback } from 'react'; -import { useKibana } from '../../common/lib/kibana'; +import { useKibana } from '../../../common/lib/kibana'; import { OPEN_IN_VISUALIZATION } from './translations'; import type { LensProps } from './types'; diff --git a/x-pack/platform/plugins/shared/cases/public/components/visualizations/translations.tsx b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/translations.tsx similarity index 100% rename from x-pack/platform/plugins/shared/cases/public/components/visualizations/translations.tsx rename to x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/translations.tsx diff --git a/x-pack/platform/plugins/shared/cases/public/components/visualizations/types.ts b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/types.ts similarity index 100% rename from x-pack/platform/plugins/shared/cases/public/components/visualizations/types.ts rename to x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/types.ts diff --git a/x-pack/platform/plugins/shared/cases/public/components/markdown_editor/plugins/lens/index.ts b/x-pack/platform/plugins/shared/cases/public/components/markdown_editor/plugins/lens/index.ts index 38f81fb6d525f..c150572f9a336 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/markdown_editor/plugins/lens/index.ts +++ b/x-pack/platform/plugins/shared/cases/public/components/markdown_editor/plugins/lens/index.ts @@ -8,6 +8,6 @@ import { plugin } from './plugin'; import { LensParser } from './parser'; import { VISUALIZATION } from './translations'; -import { LensRenderer } from '../../../visualizations/lens_renderer'; +import { LensRenderer } from '../../../internal_attachments/visualizations/lens_renderer'; export { plugin, LensParser as parser, LensRenderer as renderer, VISUALIZATION }; diff --git a/x-pack/platform/plugins/shared/cases/public/components/markdown_editor/plugins/lens/plugin.tsx b/x-pack/platform/plugins/shared/cases/public/components/markdown_editor/plugins/lens/plugin.tsx index 59c652b07bfd0..87e98d4ae9d58 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/markdown_editor/plugins/lens/plugin.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/markdown_editor/plugins/lens/plugin.tsx @@ -37,7 +37,7 @@ import { CommentEditorContext } from '../../context'; import { useLensDraftComment } from './use_lens_draft_comment'; import { VISUALIZATION } from './translations'; import { useIsMainApplication } from '../../../../common/hooks'; -import { convertToAbsoluteTimeRange } from '../../../visualizations/actions/convert_to_absolute_time_range'; +import { convertToAbsoluteTimeRange } from '../../../internal_attachments/visualizations/actions/convert_to_absolute_time_range'; const DEFAULT_TIMERANGE: TimeRange = { from: 'now-7d', diff --git a/x-pack/platform/plugins/shared/cases/public/components/markdown_editor/plugins/lens/use_lens_open_visualization.tsx b/x-pack/platform/plugins/shared/cases/public/components/markdown_editor/plugins/lens/use_lens_open_visualization.tsx index d4f6ddee3a657..519ca4800f5de 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/markdown_editor/plugins/lens/use_lens_open_visualization.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/markdown_editor/plugins/lens/use_lens_open_visualization.tsx @@ -15,7 +15,7 @@ import { parseCommentString, getLensVisualizations, } from '../../../../../common/utils/markdown_plugins/utils'; -import { OPEN_IN_VISUALIZATION } from '../../../visualizations/translations'; +import { OPEN_IN_VISUALIZATION } from '../../../internal_attachments/visualizations/translations'; export const useLensOpenVisualization = ({ comment }: { comment: string }) => { const parsedComment = parseCommentString(comment); diff --git a/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/api.ts b/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/api.ts index 79bece053bb37..0044ba5aa3b2c 100644 --- a/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/api.ts +++ b/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/api.ts @@ -7,7 +7,7 @@ import type { HttpStart } from '@kbn/core/public'; import type { UserProfile } from '@kbn/security-plugin/common'; -import type { SecurityPluginStart } from '@kbn/security-plugin/public'; +import type { UserProfileService } from '@kbn/core-user-profile-browser'; import { isEmpty } from 'lodash'; import { INTERNAL_SUGGEST_USER_PROFILES_URL, DEFAULT_USER_SIZE } from '../../../common/constants'; @@ -35,12 +35,12 @@ export const suggestUserProfiles = async ({ }; export interface BulkGetUserProfilesArgs { - security: SecurityPluginStart; + userProfile: UserProfileService; uids: string[]; } export const bulkGetUserProfiles = async ({ - security, + userProfile, uids, }: BulkGetUserProfilesArgs): Promise => { const cleanUids: string[] = uids.filter((uid) => !isEmpty(uid)); @@ -48,15 +48,15 @@ export const bulkGetUserProfiles = async ({ return []; } - return security.userProfiles.bulkGet({ uids: new Set(cleanUids), dataPath: 'avatar' }); + return userProfile.bulkGet({ uids: new Set(cleanUids), dataPath: 'avatar' }); }; export interface GetCurrentUserProfileArgs { - security: SecurityPluginStart; + userProfile: UserProfileService; } export const getCurrentUserProfile = async ({ - security, + userProfile, }: GetCurrentUserProfileArgs): Promise => { - return security.userProfiles.getCurrent({ dataPath: 'avatar' }); + return userProfile.getCurrent({ dataPath: 'avatar' }); }; diff --git a/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/use_get_current_user_profile.ts b/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/use_get_current_user_profile.ts index e98f08ab486f9..a82bd2acb4e99 100644 --- a/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/use_get_current_user_profile.ts +++ b/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/use_get_current_user_profile.ts @@ -15,14 +15,14 @@ import { casesQueriesKeys } from '../constants'; import { getCurrentUserProfile } from './api'; export const useGetCurrentUserProfile = () => { - const { security } = useKibana().services; + const { userProfile } = useKibana().services; const toasts = useToasts(); return useQuery( casesQueriesKeys.currentUser(), () => { - return getCurrentUserProfile({ security }); + return getCurrentUserProfile({ userProfile }); }, { retry: false, diff --git a/x-pack/platform/plugins/shared/cases/public/internal_attachments/index.ts b/x-pack/platform/plugins/shared/cases/public/internal_attachments/index.ts index 6d177fe5d7d84..29bff801794ba 100644 --- a/x-pack/platform/plugins/shared/cases/public/internal_attachments/index.ts +++ b/x-pack/platform/plugins/shared/cases/public/internal_attachments/index.ts @@ -8,7 +8,8 @@ import type { ExternalReferenceAttachmentTypeRegistry } from '../client/attachment_framework/external_reference_registry'; import type { PersistableStateAttachmentTypeRegistry } from '../client/attachment_framework/persistable_state_registry'; import { getFileType } from '../components/files/file_type'; -import { getVisualizationAttachmentType } from '../components/visualizations/attachment'; +import { getVisualizationAttachmentType } from '../components/internal_attachments/visualizations/attachment'; +import { getLinkAttachmentType } from '../components/internal_attachments/link/attachment'; export const registerInternalAttachments = ( externalRefRegistry: ExternalReferenceAttachmentTypeRegistry, @@ -16,4 +17,5 @@ export const registerInternalAttachments = ( ) => { externalRefRegistry.register(getFileType()); persistableStateRegistry.register(getVisualizationAttachmentType()); + persistableStateRegistry.register(getLinkAttachmentType()); }; diff --git a/x-pack/platform/plugins/shared/cases/public/plugin.ts b/x-pack/platform/plugins/shared/cases/public/plugin.ts index 67295a29437db..72f88a3cec420 100644 --- a/x-pack/platform/plugins/shared/cases/public/plugin.ts +++ b/x-pack/platform/plugins/shared/cases/public/plugin.ts @@ -30,7 +30,7 @@ import { ExternalReferenceAttachmentTypeRegistry } from './client/attachment_fra import { PersistableStateAttachmentTypeRegistry } from './client/attachment_framework/persistable_state_registry'; import { registerCaseFileKinds } from './files'; import { registerInternalAttachments } from './internal_attachments'; -import { registerActions } from './components/visualizations/actions'; +import { registerActions } from './components/internal_attachments/visualizations/actions'; import type { CasesPublicSetup, CasesPublicStart, diff --git a/x-pack/platform/plugins/shared/cases/server/internal_attachments/index.ts b/x-pack/platform/plugins/shared/cases/server/internal_attachments/index.ts index a6fb48d79a72e..45c5c0636fd4c 100644 --- a/x-pack/platform/plugins/shared/cases/server/internal_attachments/index.ts +++ b/x-pack/platform/plugins/shared/cases/server/internal_attachments/index.ts @@ -7,8 +7,11 @@ import { badRequest } from '@hapi/boom'; import { FileAttachmentMetadataRt } from '../../common/types/domain'; -import { LENS_ATTACHMENT_TYPE } from '../../common/constants/visualizations'; -import { FILE_ATTACHMENT_TYPE } from '../../common/constants'; +import { + FILE_ATTACHMENT_TYPE, + LINK_ATTACHMENT_TYPE, + LENS_ATTACHMENT_TYPE, +} from '../../common/constants'; import { decodeWithExcessOrThrow } from '../common/runtime_types'; import type { ExternalReferenceAttachmentTypeRegistry } from '../attachment_framework/external_reference_registry'; @@ -20,6 +23,9 @@ export const registerInternalAttachments = ( ) => { externalRefRegistry.register({ id: FILE_ATTACHMENT_TYPE, schemaValidator }); persistableStateRegistry.register({ id: LENS_ATTACHMENT_TYPE }); + persistableStateRegistry.register({ + id: LINK_ATTACHMENT_TYPE, + }); }; const schemaValidator = (data: unknown): void => { From 3bcb4d4d1af8ebe6e1ac9fc8aeffcecd8e2a9c54 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 10 Jun 2025 19:35:33 +0000 Subject: [PATCH 02/72] [CI] Auto-commit changed files from 'node scripts/yarn_deduplicate' --- src/platform/plugins/shared/dashboard/tsconfig.json | 3 ++- x-pack/platform/plugins/shared/cases/tsconfig.json | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/platform/plugins/shared/dashboard/tsconfig.json b/src/platform/plugins/shared/dashboard/tsconfig.json index 9eb807a7baf6e..cc9d7f08d4194 100644 --- a/src/platform/plugins/shared/dashboard/tsconfig.json +++ b/src/platform/plugins/shared/dashboard/tsconfig.json @@ -85,7 +85,8 @@ "@kbn/esql-types", "@kbn/saved-objects-tagging-plugin", "@kbn/react-kibana-context-theme", - "@kbn/std" + "@kbn/std", + "@kbn/cases-plugin" ], "exclude": ["target/**/*"] } diff --git a/x-pack/platform/plugins/shared/cases/tsconfig.json b/x-pack/platform/plugins/shared/cases/tsconfig.json index 6fe1f7cf370b3..0cdd23fcc4b40 100644 --- a/x-pack/platform/plugins/shared/cases/tsconfig.json +++ b/x-pack/platform/plugins/shared/cases/tsconfig.json @@ -83,6 +83,7 @@ "@kbn/monaco", "@kbn/code-editor", "@kbn/core-test-helpers-model-versions", + "@kbn/core-user-profile-browser", ], "exclude": [ "target/**/*", From bc5c03d82f052b5f31c2a681acf1640bbe3c8c1f Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Wed, 11 Jun 2025 16:21:56 -0400 Subject: [PATCH 03/72] remove dashboard dependency from triggers actions ui --- x-pack/platform/plugins/shared/triggers_actions_ui/kibana.jsonc | 1 - .../triggers_actions_ui/public/application/connectors_app.tsx | 2 -- .../shared/triggers_actions_ui/public/application/rules_app.tsx | 2 -- .../public/common/lib/kibana/kibana_react.mock.ts | 2 -- .../plugins/shared/triggers_actions_ui/public/plugin.ts | 2 -- .../platform/plugins/shared/triggers_actions_ui/tsconfig.json | 1 - 6 files changed, 10 deletions(-) diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/kibana.jsonc b/x-pack/platform/plugins/shared/triggers_actions_ui/kibana.jsonc index 040ff249ffcd5..28393f1debc6a 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/kibana.jsonc +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/kibana.jsonc @@ -26,7 +26,6 @@ "dataViewEditor", "alerting", "actions", - "dashboard", "licensing", "expressions", "lens", diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/connectors_app.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/connectors_app.tsx index d4a03747e6b27..0b58163af8f26 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/connectors_app.tsx +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/connectors_app.tsx @@ -24,7 +24,6 @@ import { QueryClientProvider } from '@tanstack/react-query'; import { Storage } from '@kbn/kibana-utils-plugin/public'; import { ActionsPublicPluginSetup } from '@kbn/actions-plugin/public'; -import { DashboardStart } from '@kbn/dashboard-plugin/public'; import { SharePluginStart } from '@kbn/share-plugin/public'; import { suspendedComponentWithProps } from './lib/suspended_component_with_props'; import { ActionTypeRegistryContract, RuleTypeRegistryContract } from '../types'; @@ -44,7 +43,6 @@ export interface TriggersAndActionsUiServices extends CoreStart { data: DataPublicPluginStart; dataViews: DataViewsPublicPluginStart; dataViewEditor: DataViewEditorStart; - dashboard: DashboardStart; charts: ChartsPluginStart; alerting?: AlertingStart; spaces?: SpacesPluginStart; diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/rules_app.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/rules_app.tsx index 1680711739e01..d8afa534ad63d 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/rules_app.tsx +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/rules_app.tsx @@ -33,7 +33,6 @@ import { Storage } from '@kbn/kibana-utils-plugin/public'; import { ActionsPublicPluginSetup } from '@kbn/actions-plugin/public'; import { ruleDetailsRoute, createRuleRoute, editRuleRoute } from '@kbn/rule-data-utils'; import { QueryClientProvider } from '@tanstack/react-query'; -import { DashboardStart } from '@kbn/dashboard-plugin/public'; import { ExpressionsStart } from '@kbn/expressions-plugin/public'; import { CloudSetup } from '@kbn/cloud-plugin/public'; import { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/public'; @@ -66,7 +65,6 @@ export interface TriggersAndActionsUiServices extends CoreStart { data: DataPublicPluginStart; dataViews: DataViewsPublicPluginStart; dataViewEditor: DataViewEditorStart; - dashboard: DashboardStart; charts: ChartsPluginStart; alerting?: AlertingStart; spaces?: SpacesPluginStart; diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/lib/kibana/kibana_react.mock.ts b/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/lib/kibana/kibana_react.mock.ts index 62f2b9decbb30..9ef6c85dc5a46 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/lib/kibana/kibana_react.mock.ts +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/lib/kibana/kibana_react.mock.ts @@ -10,7 +10,6 @@ import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; -import { dashboardPluginMock } from '@kbn/dashboard-plugin/public/mocks'; import { fieldFormatsServiceMock } from '@kbn/field-formats-plugin/public/mocks'; import { coreMock, scopedHistoryMock, themeServiceMock } from '@kbn/core/public/mocks'; import { licensingMock } from '@kbn/licensing-plugin/public/mocks'; @@ -44,7 +43,6 @@ export const createStartServicesMock = (): TriggersAndActionsUiServices => { }, history: scopedHistoryMock.create(), setBreadcrumbs: jest.fn(), - dashboard: dashboardPluginMock.createStartContract(), data: dataPluginMock.createStartContract(), dataViews: dataViewPluginMocks.createStartContract(), dataViewEditor: { diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/plugin.ts b/x-pack/platform/plugins/shared/triggers_actions_ui/public/plugin.ts index 01803a7c4c85f..f83ab1fa91448 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/plugin.ts +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/plugin.ts @@ -348,7 +348,6 @@ export class Plugin return renderApp({ ...coreStart, actions: plugins.actions, - dashboard: pluginsStart.dashboard, data: pluginsStart.data, dataViews: pluginsStart.dataViews, dataViewEditor: pluginsStart.dataViewEditor, @@ -393,7 +392,6 @@ export class Plugin return renderApp({ ...coreStart, actions: plugins.actions, - dashboard: pluginsStart.dashboard, data: pluginsStart.data, dataViews: pluginsStart.dataViews, dataViewEditor: pluginsStart.dataViewEditor, diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/tsconfig.json b/x-pack/platform/plugins/shared/triggers_actions_ui/tsconfig.json index 7b5c1ad6e438b..7f7af3e691631 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/tsconfig.json +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/tsconfig.json @@ -48,7 +48,6 @@ "@kbn/safer-lodash-set", "@kbn/field-types", "@kbn/core-ui-settings-common", - "@kbn/dashboard-plugin", "@kbn/licensing-plugin", "@kbn/expressions-plugin", "@kbn/core-saved-objects-api-server", From 0aeebda66f1d268fe2770bb6e011e769da4d4fd0 Mon Sep 17 00:00:00 2001 From: Bena Kansara Date: Fri, 13 Jun 2025 18:32:39 +0200 Subject: [PATCH 04/72] integrate screenshot --- package.json | 1 + .../top_nav/cases/add_to_case_modal.tsx | 11 ++++-- .../top_nav/use_dashboard_menu_items.tsx | 28 +++++++++++++-- .../internal_dashboard_top_nav.tsx | 12 +++++-- .../link/attachment_children.tsx | 14 ++++++-- .../internal_attachments/link/types.ts | 3 ++ yarn.lock | 34 +++++++++++++++++++ 7 files changed, 94 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 7ed249d26c7e8..ccc7e43550716 100644 --- a/package.json +++ b/package.json @@ -1218,6 +1218,7 @@ "he": "^1.2.0", "history": "^4.9.0", "hjson": "3.2.1", + "html2canvas": "^1.4.1", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.1", "i18n-iso-countries": "^7.14.0", diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/add_to_case_modal.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/add_to_case_modal.tsx index bd64d667e3a06..fe483bab714d9 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/add_to_case_modal.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/add_to_case_modal.tsx @@ -17,9 +17,13 @@ import { cases, shareService } from '../../../services/kibana_services'; export const AddToCaseOpenModal = ({ savedObjectId, dashboardTitle, + screenshot, + onClose, }: { savedObjectId: string; dashboardTitle: string; + screenshot: any; + onClose: () => void; }) => { const { hooks: { useCasesAddToExistingCaseModal }, @@ -27,7 +31,7 @@ export const AddToCaseOpenModal = ({ const { url: urlService } = shareService; const dashboardLocator = urlService.locators.get(DASHBOARD_APP_LOCATOR); const casesModal = useCasesAddToExistingCaseModal({ - onClose: () => {}, + onClose, onSuccess: () => {}, }); @@ -41,12 +45,15 @@ export const AddToCaseOpenModal = ({ label: dashboardTitle, type: 'dashboard', icon: 'dashboardApp', + snapshot: { + imgData: screenshot, + }, }, persistableStateAttachmentTypeId: LINK_ATTACHMENT_TYPE, type: AttachmentType.persistableState, }; casesModal.open({ getAttachments: () => [attachments] }); - }, [casesModal, dashboardLocator, savedObjectId, dashboardTitle]); + }, [casesModal, dashboardLocator, savedObjectId, dashboardTitle, screenshot]); return <>; }; diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx index 7dbe30a18fe64..6237e8623e23d 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx @@ -13,6 +13,7 @@ import type { TopNavMenuData } from '@kbn/navigation-plugin/public'; import useMountedState from 'react-use/lib/useMountedState'; import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing'; +import html2canvas from 'html2canvas'; import { UI_SETTINGS } from '../../../common/constants'; import { useDashboardApi } from '../../dashboard_api/use_dashboard_api'; import { openSettingsFlyout } from '../../dashboard_renderer/settings/open_settings_flyout'; @@ -27,13 +28,17 @@ import { ShowShareModal } from './share/show_share_modal'; export const useDashboardMenuItems = ({ isLabsShown, setIsLabsShown, + isAddToCaseModalOpen, setIsAddToCaseModalOpen, + setScreenshot, maybeRedirect, showResetChange, }: { isLabsShown: boolean; setIsLabsShown: Dispatch>; + isAddToCaseModalOpen: boolean; setIsAddToCaseModalOpen: Dispatch>; + setScreenshot: Dispatch>; maybeRedirect: (result?: SaveDashboardReturn) => void; showResetChange?: boolean; }) => { @@ -118,8 +123,22 @@ export const useDashboardMenuItems = ({ /** * Register all of the top nav configs that can be used by dashboard. */ - const menuItems = useMemo(() => { + const generateScreenshot = async () => { + const element = document.getElementById('kibana-body'); + if (!element) { + return null; + } + + try { + const canvas = await html2canvas(element); + const dataUrl = canvas.toDataURL('image/png'); + setScreenshot(dataUrl); + } catch (error) { + setScreenshot(null); + } + }; + return { fullScreen: { ...topNavStrings.fullScreen, @@ -222,7 +241,10 @@ export const useDashboardMenuItems = ({ id: 'add-to-case', testId: 'dashboardAddToCaseButton', disableButton: disableTopNav, - run: () => setIsAddToCaseModalOpen(true), + run: async () => { + await generateScreenshot(); + setIsAddToCaseModalOpen(!isAddToCaseModalOpen); + }, }, }; }, [ @@ -240,6 +262,8 @@ export const useDashboardMenuItems = ({ resetChanges, isResetting, setIsAddToCaseModalOpen, + isAddToCaseModalOpen, + setScreenshot, ]); const resetChangesMenuItem = useMemo(() => { diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx index 09e926ffbda87..158d416e3a0bf 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx @@ -82,6 +82,7 @@ export function InternalDashboardTopNav({ const [isChromeVisible, setIsChromeVisible] = useState(false); const [isLabsShown, setIsLabsShown] = useState(false); const [isAddToCaseModalOpen, setIsAddToCaseModalOpen] = useState(false); + const [screenshot, setScreenshot] = useState(); const dashboardTitleRef = useRef(null); const { @@ -266,7 +267,9 @@ export function InternalDashboardTopNav({ const { viewModeTopNavConfig, editModeTopNavConfig } = useDashboardMenuItems({ isLabsShown, setIsLabsShown, + isAddToCaseModalOpen, setIsAddToCaseModalOpen, + setScreenshot, maybeRedirect, showResetChange, }); @@ -392,9 +395,14 @@ export function InternalDashboardTopNav({ {viewMode !== 'print' && isLabsEnabled && isLabsShown ? ( setIsLabsShown(false)} /> ) : null} - {isAddToCaseModalOpen && canCases ? ( + {isAddToCaseModalOpen && canCases && lastSavedId ? ( - + setIsAddToCaseModalOpen(false)} + /> ) : null} {viewMode === 'edit' ? : null} diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/link/attachment_children.tsx b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/link/attachment_children.tsx index 23ec5ae5aa8a1..ee8b9138b1fd0 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/link/attachment_children.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/link/attachment_children.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { EuiLink } from '@elastic/eui'; +import { EuiImage, EuiLink } from '@elastic/eui'; import type { LinkAttachmentPersistedState } from './types'; interface AttachmentChildrenProps { @@ -16,8 +16,16 @@ interface AttachmentChildrenProps { export const LinkAttachmentChildren: React.FC = ({ persistableStateAttachmentState, }) => { - const { pathname, label } = persistableStateAttachmentState; - return {label}; + const { pathname, label, snapshot } = persistableStateAttachmentState; + + return ( + <> + {label} + {snapshot?.imgData && ( + + )} + + ); }; LinkAttachmentChildren.displayName = 'LinkAttachmentChildren'; diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/link/types.ts b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/link/types.ts index e36ce3caac6da..4e9fb0ad9d44c 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/link/types.ts +++ b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/link/types.ts @@ -10,4 +10,7 @@ export interface LinkAttachmentPersistedState { type: string; icon: string; label: string; + snapshot: { + imgData: string; + }; } diff --git a/yarn.lock b/yarn.lock index af5a9dcf898d5..ab637cb29a3a4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14275,6 +14275,11 @@ bare-stream@^2.0.0: dependencies: streamx "^2.21.0" +base64-arraybuffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc" + integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ== + base64-js@1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" @@ -16131,6 +16136,13 @@ css-in-js-utils@^2.0.0: hyphenate-style-name "^1.0.2" isobject "^3.0.1" +css-line-break@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/css-line-break/-/css-line-break-2.1.0.tgz#bfef660dfa6f5397ea54116bb3cb4873edbc4fa0" + integrity sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w== + dependencies: + utrie "^1.0.2" + css-loader@^6.7.1: version "6.11.0" resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.11.0.tgz#33bae3bf6363d0a7c2cf9031c96c744ff54d85ba" @@ -20635,6 +20647,14 @@ html-webpack-plugin@^5.5.0: pretty-error "^4.0.0" tapable "^2.0.0" +html2canvas@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.4.1.tgz#7cef1888311b5011d507794a066041b14669a543" + integrity sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA== + dependencies: + css-line-break "^2.1.0" + text-segmentation "^1.0.3" + htmlparser2@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7" @@ -30507,6 +30527,13 @@ text-hex@1.0.x: resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5" integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg== +text-segmentation@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/text-segmentation/-/text-segmentation-1.0.3.tgz#52a388159efffe746b24a63ba311b6ac9f2d7943" + integrity sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw== + dependencies: + utrie "^1.0.2" + text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" @@ -31630,6 +31657,13 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= +utrie@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/utrie/-/utrie-1.0.2.tgz#d42fe44de9bc0119c25de7f564a6ed1b2c87a645" + integrity sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw== + dependencies: + base64-arraybuffer "^1.0.2" + uuid@11.1.0: version "11.1.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.1.0.tgz#9549028be1753bb934fc96e2bca09bb4105ae912" From 5266320c74b08ea88fd4dd44911af4635037b3f4 Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Mon, 16 Jun 2025 16:48:37 -0400 Subject: [PATCH 05/72] adjust types --- .../top_nav/cases/add_to_case_modal.tsx | 4 +- .../shared/cases/common/constants/index.ts | 2 +- .../shared/cases/common/constants/links.ts | 2 +- .../plugins/shared/cases/common/index.ts | 2 +- .../common/types/domain/attachment/v1.ts | 4 +- .../client/attachment_framework/types.ts | 10 +- .../internal_attachments/link/attachment.tsx | 37 ------- .../page/attachment.test.tsx | 103 ++++++++++++++++++ .../internal_attachments/page/attachment.tsx | 39 +++++++ .../page/attachment_children.test.tsx | 48 ++++++++ .../{link => page}/attachment_children.tsx | 10 +- .../{link => page}/translations.tsx | 11 +- .../{link => page}/types.ts | 12 +- .../public/internal_attachments/index.ts | 5 +- .../server/internal_attachments/index.ts | 4 +- 15 files changed, 230 insertions(+), 63 deletions(-) delete mode 100644 x-pack/platform/plugins/shared/cases/public/components/internal_attachments/link/attachment.tsx create mode 100644 x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment.test.tsx create mode 100644 x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment.tsx create mode 100644 x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment_children.test.tsx rename x-pack/platform/plugins/shared/cases/public/components/internal_attachments/{link => page}/attachment_children.tsx (73%) rename x-pack/platform/plugins/shared/cases/public/components/internal_attachments/{link => page}/translations.tsx (52%) rename x-pack/platform/plugins/shared/cases/public/components/internal_attachments/{link => page}/types.ts (58%) diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/add_to_case_modal.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/add_to_case_modal.tsx index fe483bab714d9..f9fd90da9dea6 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/add_to_case_modal.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/add_to_case_modal.tsx @@ -9,7 +9,7 @@ import React, { useEffect } from 'react'; import { AttachmentType } from '@kbn/cases-plugin/common'; -import { LINK_ATTACHMENT_TYPE } from '@kbn/cases-plugin/common'; +import { PAGE_ATTACHMENT_TYPE } from '@kbn/cases-plugin/common'; import { DASHBOARD_APP_LOCATOR } from '@kbn/deeplinks-analytics'; import { DashboardLocatorParams } from '../../../../common/types'; import { cases, shareService } from '../../../services/kibana_services'; @@ -49,7 +49,7 @@ export const AddToCaseOpenModal = ({ imgData: screenshot, }, }, - persistableStateAttachmentTypeId: LINK_ATTACHMENT_TYPE, + persistableStateAttachmentTypeId: PAGE_ATTACHMENT_TYPE, type: AttachmentType.persistableState, }; casesModal.open({ getAttachments: () => [attachments] }); diff --git a/x-pack/platform/plugins/shared/cases/common/constants/index.ts b/x-pack/platform/plugins/shared/cases/common/constants/index.ts index 043a33b058c99..1ee74467e271b 100644 --- a/x-pack/platform/plugins/shared/cases/common/constants/index.ts +++ b/x-pack/platform/plugins/shared/cases/common/constants/index.ts @@ -11,7 +11,7 @@ export * from './owners'; export * from './files'; export * from './application'; export { LENS_ATTACHMENT_TYPE } from './visualizations'; -export { LINK_ATTACHMENT_TYPE } from './links'; +export { PAGE_ATTACHMENT_TYPE } from './links'; export const DEFAULT_DATE_FORMAT = 'dateFormat' as const; export const DEFAULT_DATE_FORMAT_TZ = 'dateFormat:tz' as const; diff --git a/x-pack/platform/plugins/shared/cases/common/constants/links.ts b/x-pack/platform/plugins/shared/cases/common/constants/links.ts index 449c91b116353..6a14dc5355d9e 100644 --- a/x-pack/platform/plugins/shared/cases/common/constants/links.ts +++ b/x-pack/platform/plugins/shared/cases/common/constants/links.ts @@ -5,4 +5,4 @@ * 2.0. */ -export const LINK_ATTACHMENT_TYPE = '.link'; +export const PAGE_ATTACHMENT_TYPE = '.page'; diff --git a/x-pack/platform/plugins/shared/cases/common/index.ts b/x-pack/platform/plugins/shared/cases/common/index.ts index b0c1781bc4a68..c5641df3e134e 100644 --- a/x-pack/platform/plugins/shared/cases/common/index.ts +++ b/x-pack/platform/plugins/shared/cases/common/index.ts @@ -50,7 +50,7 @@ export { UPDATE_CASES_CAPABILITY, INTERNAL_BULK_GET_CASES_URL, LENS_ATTACHMENT_TYPE, - LINK_ATTACHMENT_TYPE, + PAGE_ATTACHMENT_TYPE, INTERNAL_BULK_CREATE_ATTACHMENTS_URL, SAVED_OBJECT_TYPES, CASE_COMMENT_SAVED_OBJECT, diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/attachment/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/attachment/v1.ts index e1e01052d4a89..ff8b835f63f06 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/attachment/v1.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/attachment/v1.ts @@ -262,12 +262,13 @@ export type ExternalReferenceAttachment = rt.TypeOf; +export type PersistableStateAttachmentState = rt.TypeOf; export type PersistableStateAttachment = rt.TypeOf; export type PersistableStateAttachmentAttributes = rt.TypeOf< typeof PersistableStateAttachmentAttributesRt diff --git a/x-pack/platform/plugins/shared/cases/public/client/attachment_framework/types.ts b/x-pack/platform/plugins/shared/cases/public/client/attachment_framework/types.ts index cd946c885aaa3..5c60dd5d45018 100644 --- a/x-pack/platform/plugins/shared/cases/public/client/attachment_framework/types.ts +++ b/x-pack/platform/plugins/shared/cases/public/client/attachment_framework/types.ts @@ -57,9 +57,11 @@ export interface ExternalReferenceAttachmentViewProps extends CommonAttachmentVi externalReferenceMetadata: ExternalReferenceAttachmentPayload['externalReferenceMetadata']; } -export interface PersistableStateAttachmentViewProps extends CommonAttachmentViewProps { +export interface PersistableStateAttachmentViewProps< + Payload extends PersistableStateAttachmentPayload['persistableStateAttachmentState'] = PersistableStateAttachmentPayload['persistableStateAttachmentState'] +> extends CommonAttachmentViewProps { persistableStateAttachmentTypeId: PersistableStateAttachmentPayload['persistableStateAttachmentTypeId']; - persistableStateAttachmentState: PersistableStateAttachmentPayload['persistableStateAttachmentState']; + persistableStateAttachmentState: Payload; } export interface AttachmentType { @@ -71,7 +73,9 @@ export interface AttachmentType { } export type ExternalReferenceAttachmentType = AttachmentType; -export type PersistableStateAttachmentType = AttachmentType; +export type PersistableStateAttachmentType< + Payload extends PersistableStateAttachmentPayload['persistableStateAttachmentState'] = PersistableStateAttachmentPayload['persistableStateAttachmentState'] +> = AttachmentType>; export interface AttachmentFramework { registerExternalReference: ( diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/link/attachment.tsx b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/link/attachment.tsx deleted file mode 100644 index dc586e0e048bb..0000000000000 --- a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/link/attachment.tsx +++ /dev/null @@ -1,37 +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 React from 'react'; - -import { LINK_ATTACHMENT_TYPE } from '../../../../common/constants/links'; -import * as i18n from './translations'; - -import type { - PersistableStateAttachmentType, - PersistableStateAttachmentViewProps, -} from '../../../client/attachment_framework/types'; - -const AttachmentChildrenLazy = React.lazy(() => import('./attachment_children')); - -const getLinkAttachmentViewObject = () => { - return { - event: i18n.ADDED_LINK, - timelineAvatar: 'link', - hideDefaultActions: false, - children: AttachmentChildrenLazy as unknown as React.LazyExoticComponent< - React.FC - >, - }; -}; - -export const getLinkAttachmentType = (): PersistableStateAttachmentType => ({ - id: LINK_ATTACHMENT_TYPE, - icon: 'link', - displayName: 'Links', - getAttachmentViewObject: getLinkAttachmentViewObject, - getAttachmentRemovalObject: () => ({ event: i18n.REMOVED_LINK }), -}); diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment.test.tsx b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment.test.tsx new file mode 100644 index 0000000000000..8dea7d7888c57 --- /dev/null +++ b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment.test.tsx @@ -0,0 +1,103 @@ +/* + * 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, { Suspense } from 'react'; +import { screen, waitFor } from '@testing-library/react'; +import { PAGE_ATTACHMENT_TYPE } from '../../../../common/constants/links'; +import type { PersistableStateAttachmentViewProps } from '../../../client/attachment_framework/types'; +import { getPageAttachmentType } from './attachment'; +import { renderWithTestingProviders } from '../../../common/mock'; +import type { PageAttachmentPersistedState } from './types'; + +describe('getPageAttachmentType', () => { + const attachmentViewProps: PersistableStateAttachmentViewProps = { + persistableStateAttachmentTypeId: PAGE_ATTACHMENT_TYPE, + persistableStateAttachmentState: { + pathname: '/test/path', + type: PAGE_ATTACHMENT_TYPE, + icon: 'link', + label: 'Test Page', + url: { + pathAndQuery: '/test/path?query=1', + label: 'Test Link', + }, + snapshot: { + imgData: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA...', + }, + renderComponentId: 'PageAttachmentChildren', + }, + attachmentId: 'test', + caseData: { title: 'Test Case', id: 'case-id' }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('creates the attachment type correctly', () => { + const linkType = getPageAttachmentType(); + + expect(linkType).toStrictEqual({ + id: PAGE_ATTACHMENT_TYPE, + icon: 'link', + displayName: 'Page', + getAttachmentViewObject: expect.any(Function), + getAttachmentRemovalObject: expect.any(Function), + }); + }); + + describe('getAttachmentViewObject', () => { + it('renders the event correctly', () => { + const linkType = getPageAttachmentType(); + const event = linkType.getAttachmentViewObject(attachmentViewProps).event; + + expect(event).toBe('added page'); + }); + + it('renders the timelineAvatar correctly', () => { + const linkType = getPageAttachmentType(); + const avatar = linkType.getAttachmentViewObject(attachmentViewProps).timelineAvatar; + + expect(avatar).toBe('link'); + }); + + it('does not hide the default actions', () => { + const linkType = getPageAttachmentType(); + const hideDefaultActions = + linkType.getAttachmentViewObject(attachmentViewProps).hideDefaultActions; + + expect(hideDefaultActions).toBe(false); + }); + + it('renders the children correctly', async () => { + const linkType = getPageAttachmentType(); + const Component = linkType.getAttachmentViewObject( + attachmentViewProps + // eslint-disable-next-line testing-library/no-node-access + ).children!; + + renderWithTestingProviders( + + + + ); + + await waitFor(() => { + expect(screen.getByText('Loading...')).toBeInTheDocument(); + }); + }); + }); + + describe('getAttachmentRemovalObject', () => { + it('renders the removal event correctly', () => { + const linkType = getPageAttachmentType(); + const event = linkType.getAttachmentRemovalObject?.(attachmentViewProps); + + expect(event).toEqual({ event: 'removed page' }); + }); + }); +}); diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment.tsx b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment.tsx new file mode 100644 index 0000000000000..f8a09d707f758 --- /dev/null +++ b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment.tsx @@ -0,0 +1,39 @@ +/* + * 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 { PAGE_ATTACHMENT_TYPE } from '../../../../common/constants/links'; +import * as i18n from './translations'; + +import type { + PersistableStateAttachmentType, + PersistableStateAttachmentViewProps, + AttachmentViewObject, +} from '../../../client/attachment_framework/types'; +import type { PageAttachmentPersistedState } from './types'; + +const AttachmentChildrenLazy = React.lazy(() => import('./attachment_children')); + +const getLinkAttachmentViewObject = (): AttachmentViewObject< + PersistableStateAttachmentViewProps +> => { + return { + event: i18n.ADDED_PAGE, + timelineAvatar: 'link', + hideDefaultActions: false, + children: AttachmentChildrenLazy, + }; +}; + +export const getPageAttachmentType = + (): PersistableStateAttachmentType => ({ + id: PAGE_ATTACHMENT_TYPE, + icon: 'link', + displayName: i18n.PAGE_LABEL, + getAttachmentViewObject: getLinkAttachmentViewObject, + getAttachmentRemovalObject: () => ({ event: i18n.REMOVED_PAGE }), + }); diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment_children.test.tsx b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment_children.test.tsx new file mode 100644 index 0000000000000..7b26c9f69806a --- /dev/null +++ b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment_children.test.tsx @@ -0,0 +1,48 @@ +/* + * 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 } from '@testing-library/react'; +import { PageAttachmentChildren } from './attachment_children'; +import type { PageAttachmentPersistedState } from './types'; + +describe('PageAttachmentChildren', () => { + const mockPersistableState: PageAttachmentPersistedState = { + label: 'Test Page', + icon: 'link', + snapshot: { + imgData: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA...', + }, + type: 'mockType', + url: { + pathAndQuery: '/test/path?query=1', + label: 'Test Link', + }, + }; + + it('renders the link with correct label and href', () => { + render(); + + const link = screen.getByRole('link', { name: 'Test Page' }); + expect(link).toHaveAttribute('href', '/test/path'); + }); + + it('renders the image when snapshot data is provided', () => { + render(); + + const image = screen.getByAltText('screenshot'); + expect(image).toHaveAttribute('src', mockPersistableState.snapshot?.imgData); + }); + + it('does not render the image when snapshot data is missing', () => { + const stateWithoutSnapshot = { ...mockPersistableState, snapshot: null }; + render(); + + const image = screen.queryByAltText('screenshot'); + expect(image).not.toBeInTheDocument(); + }); +}); diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/link/attachment_children.tsx b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment_children.tsx similarity index 73% rename from x-pack/platform/plugins/shared/cases/public/components/internal_attachments/link/attachment_children.tsx rename to x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment_children.tsx index ee8b9138b1fd0..18c7612aaf4c3 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/link/attachment_children.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment_children.tsx @@ -7,13 +7,13 @@ import React from 'react'; import { EuiImage, EuiLink } from '@elastic/eui'; -import type { LinkAttachmentPersistedState } from './types'; +import type { PageAttachmentPersistedState } from './types'; interface AttachmentChildrenProps { - persistableStateAttachmentState: LinkAttachmentPersistedState; + persistableStateAttachmentState: PageAttachmentPersistedState; } -export const LinkAttachmentChildren: React.FC = ({ +export const PageAttachmentChildren: React.FC = ({ persistableStateAttachmentState, }) => { const { pathname, label, snapshot } = persistableStateAttachmentState; @@ -28,8 +28,8 @@ export const LinkAttachmentChildren: React.FC = ({ ); }; -LinkAttachmentChildren.displayName = 'LinkAttachmentChildren'; +PageAttachmentChildren.displayName = 'PageAttachmentChildren'; // Note: This is for lazy loading // eslint-disable-next-line import/no-default-export -export default LinkAttachmentChildren; +export default PageAttachmentChildren; diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/link/translations.tsx b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/translations.tsx similarity index 52% rename from x-pack/platform/plugins/shared/cases/public/components/internal_attachments/link/translations.tsx rename to x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/translations.tsx index e44ca70c86f7b..2fcd1d4667426 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/link/translations.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/translations.tsx @@ -7,9 +7,12 @@ import { i18n } from '@kbn/i18n'; -export const ADDED_LINK = i18n.translate('xpack.cases.caseView.links.addedLink', { - defaultMessage: 'added link', +export const ADDED_PAGE = i18n.translate('xpack.cases.caseView.links.addedLink', { + defaultMessage: 'added page', }); -export const REMOVED_LINK = i18n.translate('xpack.cases.caseView.links.removedLink', { - defaultMessage: 'removed link', +export const REMOVED_PAGE = i18n.translate('xpack.cases.caseView.links.removedLink', { + defaultMessage: 'removed page', +}); +export const PAGE_LABEL = i18n.translate('xpack.cases.caseView.links.pageLabel', { + defaultMessage: 'Page', }); diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/link/types.ts b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/types.ts similarity index 58% rename from x-pack/platform/plugins/shared/cases/public/components/internal_attachments/link/types.ts rename to x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/types.ts index 4e9fb0ad9d44c..c9f00ba11fec1 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/link/types.ts +++ b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/types.ts @@ -4,13 +4,17 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import type { PersistableStateAttachmentState } from '../../../../common/types/domain'; -export interface LinkAttachmentPersistedState { - pathname: string; +export interface PageAttachmentPersistedState extends PersistableStateAttachmentState { type: string; - icon: string; label: string; + icon: string; + url: { + pathAndQuery: string; + label: string; + }; snapshot: { imgData: string; - }; + } | null; } diff --git a/x-pack/platform/plugins/shared/cases/public/internal_attachments/index.ts b/x-pack/platform/plugins/shared/cases/public/internal_attachments/index.ts index 29bff801794ba..5c6eb53fde2ea 100644 --- a/x-pack/platform/plugins/shared/cases/public/internal_attachments/index.ts +++ b/x-pack/platform/plugins/shared/cases/public/internal_attachments/index.ts @@ -7,9 +7,10 @@ import type { ExternalReferenceAttachmentTypeRegistry } from '../client/attachment_framework/external_reference_registry'; import type { PersistableStateAttachmentTypeRegistry } from '../client/attachment_framework/persistable_state_registry'; +import type { PersistableStateAttachmentType } from '../client/attachment_framework/types'; import { getFileType } from '../components/files/file_type'; import { getVisualizationAttachmentType } from '../components/internal_attachments/visualizations/attachment'; -import { getLinkAttachmentType } from '../components/internal_attachments/link/attachment'; +import { getPageAttachmentType } from '../components/internal_attachments/page/attachment'; export const registerInternalAttachments = ( externalRefRegistry: ExternalReferenceAttachmentTypeRegistry, @@ -17,5 +18,5 @@ export const registerInternalAttachments = ( ) => { externalRefRegistry.register(getFileType()); persistableStateRegistry.register(getVisualizationAttachmentType()); - persistableStateRegistry.register(getLinkAttachmentType()); + persistableStateRegistry.register(getPageAttachmentType() as PersistableStateAttachmentType); }; diff --git a/x-pack/platform/plugins/shared/cases/server/internal_attachments/index.ts b/x-pack/platform/plugins/shared/cases/server/internal_attachments/index.ts index 45c5c0636fd4c..aa25c0dec8f88 100644 --- a/x-pack/platform/plugins/shared/cases/server/internal_attachments/index.ts +++ b/x-pack/platform/plugins/shared/cases/server/internal_attachments/index.ts @@ -9,7 +9,7 @@ import { badRequest } from '@hapi/boom'; import { FileAttachmentMetadataRt } from '../../common/types/domain'; import { FILE_ATTACHMENT_TYPE, - LINK_ATTACHMENT_TYPE, + PAGE_ATTACHMENT_TYPE, LENS_ATTACHMENT_TYPE, } from '../../common/constants'; @@ -24,7 +24,7 @@ export const registerInternalAttachments = ( externalRefRegistry.register({ id: FILE_ATTACHMENT_TYPE, schemaValidator }); persistableStateRegistry.register({ id: LENS_ATTACHMENT_TYPE }); persistableStateRegistry.register({ - id: LINK_ATTACHMENT_TYPE, + id: PAGE_ATTACHMENT_TYPE, }); }; From 14e8f826ae556d3c48d9dfb0a4d81a28cd4eeadd Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 16 Jun 2025 21:02:17 +0000 Subject: [PATCH 06/72] [CI] Auto-commit changed files from 'node scripts/styled_components_mapping' --- .../platform/plugins/shared/triggers_actions_ui/tsconfig.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/tsconfig.json b/x-pack/platform/plugins/shared/triggers_actions_ui/tsconfig.json index 7f7af3e691631..3db0ec8933cca 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/tsconfig.json +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/tsconfig.json @@ -87,7 +87,8 @@ "@kbn/response-ops-alerts-delete", "@kbn/content-management-plugin", "@kbn/esql-types", - "@kbn/presentation-containers" + "@kbn/presentation-containers", + "@kbn/dashboard-plugin" ], "exclude": ["target/**/*"] } From ac2ef574665c5867449c651e256349bf14997c6d Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Mon, 16 Jun 2025 18:31:30 -0400 Subject: [PATCH 07/72] quick checks --- renovate.json | 3 ++- x-pack/platform/plugins/shared/cases/public/types.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/renovate.json b/renovate.json index 2d12e44d4391d..1d580fe986660 100644 --- a/renovate.json +++ b/renovate.json @@ -3722,7 +3722,8 @@ { "groupName": "react-hook-form", "matchDepNames": [ - "react-hook-form" + "react-hook-form", + "html2canvas" ], "reviewers": [ "team:obs-ux-management-team" diff --git a/x-pack/platform/plugins/shared/cases/public/types.ts b/x-pack/platform/plugins/shared/cases/public/types.ts index 08e5787681a9e..9c2f27859f98d 100644 --- a/x-pack/platform/plugins/shared/cases/public/types.ts +++ b/x-pack/platform/plugins/shared/cases/public/types.ts @@ -185,6 +185,6 @@ export type SupportedCaseAttachment = export type CaseAttachments = SupportedCaseAttachment[]; export type CaseAttachmentWithoutOwner = DistributiveOmit; export type CaseAttachmentsWithoutOwner = CaseAttachmentWithoutOwner[]; -export type { LensProps } from './components/visualizations/types'; +export type { LensProps } from './components/internal_attachments/visualizations/types'; export type ServerError = IHttpFetchError; From 9d0d253de4a834b68a83f3576567d500a36792d8 Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Mon, 16 Jun 2025 19:12:50 -0400 Subject: [PATCH 08/72] remove unnecessary dashboard dependency --- .../plugins/shared/triggers_actions_ui/public/plugin.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/plugin.ts b/x-pack/platform/plugins/shared/triggers_actions_ui/public/plugin.ts index f83ab1fa91448..4c7bfc4deb97e 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/plugin.ts +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/plugin.ts @@ -25,7 +25,6 @@ import { Storage } from '@kbn/kibana-utils-plugin/public'; import type { SpacesPluginStart } from '@kbn/spaces-plugin/public'; import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; import { triggersActionsRoute } from '@kbn/rule-data-utils'; -import type { DashboardStart } from '@kbn/dashboard-plugin/public'; import type { LicensingPluginStart } from '@kbn/licensing-plugin/public'; import type { ExpressionsStart } from '@kbn/expressions-plugin/public'; import type { ServerlessPluginStart } from '@kbn/serverless/public'; @@ -160,7 +159,6 @@ interface PluginsStart { data: DataPublicPluginStart; dataViews: DataViewsPublicPluginStart; dataViewEditor: DataViewEditorStart; - dashboard: DashboardStart; charts: ChartsPluginStart; alerting?: AlertingStart; spaces?: SpacesPluginStart; @@ -291,7 +289,6 @@ export class Plugin return renderApp({ ...coreStart, actions: plugins.actions, - dashboard: pluginsStart.dashboard, cloud: plugins.cloud, data: pluginsStart.data, dataViews: pluginsStart.dataViews, From b45cdbaf94899ba7c7a345acb0f0bdd92f640627 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 16 Jun 2025 23:25:42 +0000 Subject: [PATCH 09/72] [CI] Auto-commit changed files from 'node scripts/styled_components_mapping' --- x-pack/platform/plugins/shared/triggers_actions_ui/tsconfig.json | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/tsconfig.json b/x-pack/platform/plugins/shared/triggers_actions_ui/tsconfig.json index 3db0ec8933cca..d55197b26ddf6 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/tsconfig.json +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/tsconfig.json @@ -88,7 +88,6 @@ "@kbn/content-management-plugin", "@kbn/esql-types", "@kbn/presentation-containers", - "@kbn/dashboard-plugin" ], "exclude": ["target/**/*"] } From 6d1c98bfdcadc2756436957b193386e3414721eb Mon Sep 17 00:00:00 2001 From: Bena Kansara Date: Tue, 17 Jun 2025 15:37:56 +0200 Subject: [PATCH 10/72] split and merge screenshot images --- .../top_nav/cases/add_to_case_modal.tsx | 5 +- .../top_nav/use_dashboard_menu_items.tsx | 45 ++++++++++++++- .../page/attachment_children.tsx | 55 +++++++++++++++++-- .../internal_attachments/page/types.ts | 3 +- 4 files changed, 98 insertions(+), 10 deletions(-) diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/add_to_case_modal.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/add_to_case_modal.tsx index f9fd90da9dea6..d46a06e104ee5 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/add_to_case_modal.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/add_to_case_modal.tsx @@ -41,7 +41,10 @@ export const AddToCaseOpenModal = ({ }); const attachments = { persistableStateAttachmentState: { - pathname: url, + url: { + pathAndQuery: url, + label: dashboardTitle, + }, label: dashboardTitle, type: 'dashboard', icon: 'dashboardApp', diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx index 6237e8623e23d..90783841f1d15 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx @@ -120,6 +120,34 @@ export const useDashboardMenuItems = ({ [dashboardApi, hasUnsavedChanges, viewMode, isMounted] ); + function splitCanvasVertically( + canvas: HTMLCanvasElement, + maxChunkHeight = 2000 + ): HTMLCanvasElement[] { + const parts: HTMLCanvasElement[] = []; + const width = canvas.width; + const height = canvas.height; + + const partsCount = Math.ceil(height / maxChunkHeight); + + for (let i = 0; i < partsCount; i++) { + const chunkHeight = i === partsCount - 1 ? height - i * maxChunkHeight : maxChunkHeight; + + const chunkCanvas = document.createElement('canvas'); + chunkCanvas.width = width; + chunkCanvas.height = chunkHeight; + + const ctx = chunkCanvas.getContext('2d'); + if (!ctx) continue; + + ctx.drawImage(canvas, 0, i * maxChunkHeight, width, chunkHeight, 0, 0, width, chunkHeight); + + parts.push(chunkCanvas); + } + + return parts; + } + /** * Register all of the top nav configs that can be used by dashboard. */ @@ -130,10 +158,21 @@ export const useDashboardMenuItems = ({ return null; } + const originalScrollTop = element.scrollTop; + element.scrollTop = 0; + try { - const canvas = await html2canvas(element); - const dataUrl = canvas.toDataURL('image/png'); - setScreenshot(dataUrl); + const canvas = await html2canvas(element, { + scrollY: -window.scrollY, + scale: 0.9, + }); + + element.scrollTop = originalScrollTop; + + const splitCanvases = splitCanvasVertically(canvas); + const base64Chunks = splitCanvases.map((chunk) => chunk.toDataURL('image/jpeg', 0.6)); + + setScreenshot(base64Chunks); } catch (error) { setScreenshot(null); } diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment_children.tsx b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment_children.tsx index 18c7612aaf4c3..1869e275b1d36 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment_children.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment_children.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { EuiImage, EuiLink } from '@elastic/eui'; import type { PageAttachmentPersistedState } from './types'; @@ -13,16 +13,61 @@ interface AttachmentChildrenProps { persistableStateAttachmentState: PageAttachmentPersistedState; } +const base64ToImage = (src: string): Promise => { + return new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => resolve(img); + img.onerror = reject; + img.src = src; + }); +}; + +const stitchBase64ImagesVertically = async (base64Slices: string[]): Promise => { + const images = await Promise.all(base64Slices.map(base64ToImage)); + + const totalHeight = images.reduce((sum, img) => sum + img.height, 0); + const width = images[0]?.width || 0; + + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = totalHeight; + const ctx = canvas.getContext('2d'); + + let y = 0; + + if (ctx) { + for (const img of images) { + ctx.drawImage(img, 0, y); + y += img.height; + } + } + + return canvas.toDataURL('image/jpeg', 0.8); +}; + export const PageAttachmentChildren: React.FC = ({ persistableStateAttachmentState, }) => { - const { pathname, label, snapshot } = persistableStateAttachmentState; + const { url, label, snapshot } = persistableStateAttachmentState; + + const [screenshot, setScreenshot] = useState(null); + + useEffect(() => { + async function stitchImgData() { + if (snapshot && snapshot.imgData) { + const stitchedBase64 = await stitchBase64ImagesVertically(snapshot.imgData); + setScreenshot(stitchedBase64); + } + } + + stitchImgData(); + }, [snapshot]); return ( <> - {label} - {snapshot?.imgData && ( - + {label} + {screenshot && ( + )} ); diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/types.ts b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/types.ts index c9f00ba11fec1..fd8f95330e264 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/types.ts +++ b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/types.ts @@ -15,6 +15,7 @@ export interface PageAttachmentPersistedState extends PersistableStateAttachment label: string; }; snapshot: { - imgData: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + imgData: any; } | null; } From 5bd03838f5264cdcb882c2d40df2f4f3482620fa Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Tue, 17 Jun 2025 12:41:25 -0400 Subject: [PATCH 11/72] add actions --- .../plugins/shared/dashboard/kibana.jsonc | 7 +- .../top_nav/cases/add_to_case_modal.tsx | 59 ----- .../dashboard_add_to_case_modal.test.tsx | 230 ++++++++++++++++++ .../cases/dashboard_add_to_case_modal.tsx | 181 ++++++++++++++ .../top_nav/cases/use_screenshot.test.tsx | 62 +++++ .../top_nav/cases/use_screenshot.ts | 34 +++ .../top_nav/use_dashboard_menu_items.tsx | 27 +- .../internal_dashboard_top_nav.tsx | 30 +-- .../shared/dashboard/public/plugin.tsx | 2 +- .../public/services/kibana_services.ts | 2 +- .../page/attachment.test.tsx | 5 +- .../internal_attachments/page/attachment.tsx | 42 +++- .../page/attachment_children.test.tsx | 7 +- .../page/attachment_children.tsx | 53 +++- .../page/go_to_action.test.tsx | 57 +++++ .../page/go_to_action.tsx | 39 +++ .../page/translations.tsx | 2 +- .../internal_attachments/page/types.ts | 27 +- .../plugins/shared/cases/public/index.tsx | 1 + 19 files changed, 736 insertions(+), 131 deletions(-) delete mode 100644 src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/add_to_case_modal.tsx create mode 100644 src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/dashboard_add_to_case_modal.test.tsx create mode 100644 src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/dashboard_add_to_case_modal.tsx create mode 100644 src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/use_screenshot.test.tsx create mode 100644 src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/use_screenshot.ts create mode 100644 x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/go_to_action.test.tsx create mode 100644 x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/go_to_action.tsx diff --git a/src/platform/plugins/shared/dashboard/kibana.jsonc b/src/platform/plugins/shared/dashboard/kibana.jsonc index dece2ea4ab314..a6b74ca59db28 100644 --- a/src/platform/plugins/shared/dashboard/kibana.jsonc +++ b/src/platform/plugins/shared/dashboard/kibana.jsonc @@ -27,7 +27,6 @@ "urlForwarding", "presentationUtil", "unifiedSearch", - "cases" ], "optionalPlugins": [ "home", @@ -40,14 +39,16 @@ "serverless", "noDataPage", "observabilityAIAssistant", - "lens" + "lens", + "cases" ], "requiredBundles": [ "kibanaReact", "kibanaUtils", "presentationUtil", "presentationPanel", - "savedObjects" + "savedObjects", + "cases" ] } } diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/add_to_case_modal.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/add_to_case_modal.tsx deleted file mode 100644 index f9fd90da9dea6..0000000000000 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/add_to_case_modal.tsx +++ /dev/null @@ -1,59 +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", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import React, { useEffect } from 'react'; -import { AttachmentType } from '@kbn/cases-plugin/common'; -import { PAGE_ATTACHMENT_TYPE } from '@kbn/cases-plugin/common'; -import { DASHBOARD_APP_LOCATOR } from '@kbn/deeplinks-analytics'; -import { DashboardLocatorParams } from '../../../../common/types'; -import { cases, shareService } from '../../../services/kibana_services'; - -export const AddToCaseOpenModal = ({ - savedObjectId, - dashboardTitle, - screenshot, - onClose, -}: { - savedObjectId: string; - dashboardTitle: string; - screenshot: any; - onClose: () => void; -}) => { - const { - hooks: { useCasesAddToExistingCaseModal }, - } = cases; - const { url: urlService } = shareService; - const dashboardLocator = urlService.locators.get(DASHBOARD_APP_LOCATOR); - const casesModal = useCasesAddToExistingCaseModal({ - onClose, - onSuccess: () => {}, - }); - - useEffect(() => { - const url = dashboardLocator.getRedirectUrl({ - dashboardId: savedObjectId, - }); - const attachments = { - persistableStateAttachmentState: { - pathname: url, - label: dashboardTitle, - type: 'dashboard', - icon: 'dashboardApp', - snapshot: { - imgData: screenshot, - }, - }, - persistableStateAttachmentTypeId: PAGE_ATTACHMENT_TYPE, - type: AttachmentType.persistableState, - }; - casesModal.open({ getAttachments: () => [attachments] }); - }, [casesModal, dashboardLocator, savedObjectId, dashboardTitle, screenshot]); - - return <>; -}; diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/dashboard_add_to_case_modal.test.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/dashboard_add_to_case_modal.test.tsx new file mode 100644 index 0000000000000..8a16513a08177 --- /dev/null +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/dashboard_add_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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { AddToCaseModal } from './dashboard_add_to_case_modal'; +import { coreServices, cases, shareService } from '../../../services/kibana_services'; +import { DashboardContext } from '../../../dashboard_api/use_dashboard_api'; +import { DashboardInternalContext } from '../../../dashboard_api/use_dashboard_internal_api'; + +import { buildMockDashboardApi } from '../../../mocks'; + +jest.mock('../../../services/kibana_services', () => ({ + cases: { + ui: { + getCasesContext: jest.fn(() => ({ children }: any) => <>{children}), + }, + helpers: { + canUseCases: jest.fn(() => ({ + all: true, + create: true, + read: true, + update: true, + delete: true, + push: true, + connectors: true, + settings: true, + reopenCase: true, + createComment: true, + assign: true, + })), + }, + hooks: { + useCasesAddToExistingCaseModal: jest.fn(() => ({ + open: jest.fn(), + })), + }, + }, + shareService: { + url: { + locators: { + get: jest.fn(() => ({ + getRedirectUrl: jest.fn().mockImplementation(({ timeRange }) => { + return `/mock-redirect-url?from=${encodeURIComponent( + timeRange.from + )}&to=${encodeURIComponent(timeRange.to)}`; + }), + })), + }, + }, + }, + coreServices: { + notifications: { + toasts: { + addDanger: jest.fn(), + }, + }, + application: { + capabilities: { + dashboard_v2: { + save: true, + }, + }, + }, + }, + dataService: { + query: { + timefilter: { + timefilter: { + getTime: jest.fn(() => ({ from: 'now-15m', to: 'now' })), + getRefreshInterval: jest.fn(() => ({ pause: true, value: 0 })), + }, + }, + }, + }, +})); + +describe('AddToCaseModal', () => { + const { api: mockDashboardApi, internalApi } = buildMockDashboardApi({ + savedObjectId: 'mockSavedId', + overrides: { + timeRange: { from: 'now-15m', to: 'now' }, + }, + }); + + const mockDate = new Date('2025-06-17T15:37:53.937Z'); + beforeAll(() => { + jest.useFakeTimers().setSystemTime(mockDate); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + beforeEach(() => { + jest.clearAllMocks(); + // @ts-ignore next line + shareService.url.locators.get = jest.fn(() => ({ + getRedirectUrl: jest.fn().mockImplementation(({ timeRange }) => { + return `/mock-redirect-url?from=${encodeURIComponent( + timeRange.from + )}&to=${encodeURIComponent(timeRange.to)}`; + }), + })); + }); + + it('renders null if cases is not available', () => { + jest.mock('../../../services/kibana_services', () => ({ cases: null })); + + const { container } = render( + + + + + + ); + + expect(container.firstChild).toBeNull(); + }); + + it('renders the modal when isOpen is true and required props are provided', () => { + const open = jest.fn(); + const casesMock = cases!; + jest.spyOn(casesMock.hooks, 'useCasesAddToExistingCaseModal').mockReturnValue({ + close: jest.fn(), + open, + }); + render( + + + + + + ); + + expect(open).toBeCalledWith( + expect.objectContaining({ + getAttachments: expect.any(Function), + }) + ); + + // Optionally, you can invoke the function and test its return value + const callArgs = open.mock.calls[0][0]; + expect(callArgs.getAttachments()).toEqual([ + { + persistableStateAttachmentTypeId: '.page', + type: 'persistableState', + persistableStateAttachmentState: { + snapshot: { + imgData: 'mockScreenshot', + }, + type: 'dashboard', + url: { + iconType: 'dashboardApp', + pathAndQuery: + '/mock-redirect-url?from=2025-06-17T15%3A22%3A53.937Z&to=2025-06-17T15%3A37%3A53.937Z', + label: 'My Dashboard', + actionLabel: 'Go to dashboard', + }, + screenContext: null, + }, + }, + ]); + }); + + it('does not render the modal when isOpen is false', () => { + const { container } = render( + + + + + + ); + + expect(container.firstChild).toBeNull(); + }); + + it('shows a danger toast when the dashboard locator is not defined', () => { + shareService.url.locators.get = jest.fn(() => undefined); + + render( + + + + + + ); + + expect(coreServices.notifications.toasts.addDanger).toHaveBeenCalledWith({ + title: expect.any(String), + 'data-test-subj': 'dashboardAddToCaseError', + }); + }); + + it('saves the time range in the URL as absolute time', () => { + const open = jest.fn(); + const casesMock = cases!; + jest.spyOn(casesMock.hooks, 'useCasesAddToExistingCaseModal').mockReturnValue({ + close: jest.fn(), + open, + }); + + render( + + + + + + ); + + expect(open).toBeCalledWith( + expect.objectContaining({ + getAttachments: expect.any(Function), + }) + ); + + const callArgs = open.mock.calls[0][0]; + const attachmentState = callArgs.getAttachments()[0].persistableStateAttachmentState; + + expect(attachmentState.url.pathAndQuery).toContain( + '/mock-redirect-url?from=2025-06-17T15%3A22%3A53.937Z&to=2025-06-17T15%3A37%3A53.937Z' + ); + }); +}); diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/dashboard_add_to_case_modal.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/dashboard_add_to_case_modal.tsx new file mode 100644 index 0000000000000..b602ca029d690 --- /dev/null +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/dashboard_add_to_case_modal.tsx @@ -0,0 +1,181 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React, { useMemo, useEffect } from 'react'; +import { type TimeRange, getAbsoluteTimeRange } from '@kbn/data-plugin/common'; +import { i18n } from '@kbn/i18n'; +import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing'; +import type { + PageAttachmentPersistedState, + CaseAttachmentsWithoutOwner, +} from '@kbn/cases-plugin/public'; +import { + AttachmentType, + PAGE_ATTACHMENT_TYPE, + type CasesPermissions, +} from '@kbn/cases-plugin/common'; +import { DASHBOARD_APP_LOCATOR } from '@kbn/deeplinks-analytics'; +import { useDashboardApi } from '../../../dashboard_api/use_dashboard_api'; +import { getDashboardTitle } from '../../_dashboard_app_strings'; +import { cases, shareService, coreServices } from '../../../services/kibana_services'; +import { DashboardLocatorParams } from '../../../../common/types'; + +export function AddToCaseModal({ + screenshot, + isOpen, +}: { + screenshot: string | null; + isOpen: boolean; +}) { + 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 = + casesPermissions.read && casesPermissions.update && casesPermissions.push; + const CasesContext = useMemo(() => { + if (!getCasesContext) { + return React.Fragment; + } + return getCasesContext(); + }, [getCasesContext]); + + const dashboardApi = useDashboardApi(); + + const [lastSavedId, title, viewMode, timeRange] = useBatchedPublishingSubjects( + dashboardApi.savedObjectId$, + dashboardApi.title$, + dashboardApi.viewMode$, + dashboardApi.timeRange$ + ); + + const dashboardTitle = useMemo(() => { + return getDashboardTitle(title, viewMode, !lastSavedId); + }, [title, viewMode, lastSavedId]); + + if (!cases) { + return null; + } + + return isOpen && hasCasesPermissions && lastSavedId ? ( + + + + ) : null; +} + +interface OpenAddToCaseModalProps { + savedObjectId: string; + dashboardTitle: string; + screenshot: string | null; + timeRange?: TimeRange; +} + +const OpenAddToCaseOpenModal = ({ + savedObjectId, + dashboardTitle, + screenshot, + timeRange, +}: OpenAddToCaseModalProps) => { + // Conditionally checked in wrapper to ensure cases is available + const useCasesAddToExistingCaseModal = cases?.hooks.useCasesAddToExistingCaseModal!; + const { url: urlService } = shareService; + const dashboardLocator = urlService.locators.get(DASHBOARD_APP_LOCATOR); + const casesModal = useCasesAddToExistingCaseModal(); + + useEffect(() => { + if (!dashboardLocator) { + coreServices.notifications.toasts.addDanger({ + title: i18n.translate( + 'xpack.dashboard.topNav.cases.addToCaseModal.error.noDashboardLocator', + { + defaultMessage: 'Error adding dashboard to case', + } + ), + 'data-test-subj': 'dashboardAddToCaseError', + }); + return; + } + const url = dashboardLocator.getRedirectUrl({ + dashboardId: savedObjectId, + timeRange: convertToAbsoluteTimeRange(timeRange), + }); + const persistableStateAttachmentState: PageAttachmentPersistedState = { + type: 'dashboard', + url: { + pathAndQuery: url, + label: dashboardTitle, + actionLabel: i18n.translate( + 'xpack.dashboard.topNav.cases.addToCaseModal.goToDashboardActionLabel', + { + defaultMessage: 'Go to dashboard', + } + ), + iconType: 'dashboardApp', + }, + snapshot: screenshot + ? { + imgData: screenshot, + } + : null, + screenContext: null, + }; + const attachments = { + persistableStateAttachmentState, + persistableStateAttachmentTypeId: PAGE_ATTACHMENT_TYPE, + type: AttachmentType.persistableState, + }; + casesModal.open({ + getAttachments: () => [attachments] as CaseAttachmentsWithoutOwner, + }); + }, [casesModal, dashboardLocator, savedObjectId, dashboardTitle, screenshot, timeRange]); + + return <>; +}; + +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/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/use_screenshot.test.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/use_screenshot.test.tsx new file mode 100644 index 0000000000000..52839bbac69a5 --- /dev/null +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/use_screenshot.test.tsx @@ -0,0 +1,62 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ +import React from 'react'; +import { renderHook, act } from '@testing-library/react'; +import html2canvas from 'html2canvas'; +import { useScreenshot } from './use_screenshot'; + +jest.mock('html2canvas'); + +describe('useScreenshot', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + const Wrapper = ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ); + + it('returns null screenshot initially', () => { + const { result } = renderHook(() => useScreenshot(), { + wrapper: Wrapper, + }); + expect(result.current.screenshot).toBeNull(); + }); + + it('sets screenshot when generateScreenshot is called successfully', async () => { + const mockCanvas = { + toDataURL: jest.fn().mockReturnValue('data:image/png;base64,mockImageData'), + }; + (html2canvas as jest.Mock).mockResolvedValue(mockCanvas); + + const { result } = renderHook(() => useScreenshot(), { + wrapper: Wrapper, + }); + await act(async () => { + await result.current.generateScreenshot(); + }); + + expect(html2canvas).toHaveBeenCalled(); + expect(mockCanvas.toDataURL).toHaveBeenCalledWith('image/png'); + expect(result.current.screenshot).toBe('data:image/png;base64,mockImageData'); + }); + + it('sets screenshot to null if an error occurs', async () => { + (html2canvas as jest.Mock).mockRejectedValue(new Error('mock error')); + + const { result } = renderHook(() => useScreenshot(), { + wrapper: Wrapper, + }); + await act(async () => { + await result.current.generateScreenshot(); + }); + + expect(result.current.screenshot).toBeNull(); + }); +}); diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/use_screenshot.ts b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/use_screenshot.ts new file mode 100644 index 0000000000000..b308b8a807512 --- /dev/null +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/use_screenshot.ts @@ -0,0 +1,34 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { useCallback, useState } from 'react'; +import html2canvas from 'html2canvas'; + +export function useScreenshot() { + const [screenshot, setScreenshot] = useState(null); + const generateScreenshot = useCallback(async () => { + const element = document.getElementById('kibana-body'); + if (!element) { + return null; + } + + try { + const canvas = await html2canvas(element); + const dataUrl = canvas.toDataURL('image/png'); + setScreenshot(dataUrl); + } catch (error) { + setScreenshot(null); + } + }, [setScreenshot]); + + return { + generateScreenshot, + screenshot, + }; +} diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx index 6237e8623e23d..af764b63ed056 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx @@ -13,7 +13,6 @@ import type { TopNavMenuData } from '@kbn/navigation-plugin/public'; import useMountedState from 'react-use/lib/useMountedState'; import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing'; -import html2canvas from 'html2canvas'; import { UI_SETTINGS } from '../../../common/constants'; import { useDashboardApi } from '../../dashboard_api/use_dashboard_api'; import { openSettingsFlyout } from '../../dashboard_renderer/settings/open_settings_flyout'; @@ -28,17 +27,15 @@ import { ShowShareModal } from './share/show_share_modal'; export const useDashboardMenuItems = ({ isLabsShown, setIsLabsShown, - isAddToCaseModalOpen, setIsAddToCaseModalOpen, - setScreenshot, + generateScreenshot, maybeRedirect, showResetChange, }: { isLabsShown: boolean; setIsLabsShown: Dispatch>; - isAddToCaseModalOpen: boolean; setIsAddToCaseModalOpen: Dispatch>; - setScreenshot: Dispatch>; + generateScreenshot: () => Promise; maybeRedirect: (result?: SaveDashboardReturn) => void; showResetChange?: boolean; }) => { @@ -124,21 +121,6 @@ export const useDashboardMenuItems = ({ * Register all of the top nav configs that can be used by dashboard. */ const menuItems = useMemo(() => { - const generateScreenshot = async () => { - const element = document.getElementById('kibana-body'); - if (!element) { - return null; - } - - try { - const canvas = await html2canvas(element); - const dataUrl = canvas.toDataURL('image/png'); - setScreenshot(dataUrl); - } catch (error) { - setScreenshot(null); - } - }; - return { fullScreen: { ...topNavStrings.fullScreen, @@ -243,7 +225,7 @@ export const useDashboardMenuItems = ({ disableButton: disableTopNav, run: async () => { await generateScreenshot(); - setIsAddToCaseModalOpen(!isAddToCaseModalOpen); + setIsAddToCaseModalOpen(true); }, }, }; @@ -262,8 +244,7 @@ export const useDashboardMenuItems = ({ resetChanges, isResetting, setIsAddToCaseModalOpen, - isAddToCaseModalOpen, - setScreenshot, + generateScreenshot, ]); const resetChangesMenuItem = useMemo(() => { diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx index 7e476827965f2..413ab69e143b6 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx @@ -9,7 +9,6 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import UseUnmount from 'react-use/lib/useUnmount'; - import { EuiBadge, EuiBreadcrumb, @@ -56,9 +55,8 @@ import { import { getDashboardCapabilities } from '../utils/get_dashboard_capabilities'; import { getFullEditPath } from '../utils/urls'; import { DashboardFavoriteButton } from './dashboard_favorite_button'; -import { cases } from '../services/kibana_services'; -import { AddToCaseOpenModal } from '../dashboard_app/top_nav/cases/add_to_case_modal'; - +import { AddToCaseModal } from '../dashboard_app/top_nav/cases/dashboard_add_to_case_modal'; +import { useScreenshot } from '../dashboard_app/top_nav/cases/use_screenshot'; export interface InternalDashboardTopNavProps { customLeadingBreadCrumbs?: EuiBreadcrumb[]; embedSettings?: DashboardEmbedSettings; @@ -83,19 +81,13 @@ export function InternalDashboardTopNav({ const [isChromeVisible, setIsChromeVisible] = useState(false); const [isLabsShown, setIsLabsShown] = useState(false); const [isAddToCaseModalOpen, setIsAddToCaseModalOpen] = useState(false); - const [screenshot, setScreenshot] = useState(); const dashboardTitleRef = useRef(null); - const { - ui: { getCasesContext }, - helpers: { canUseCases }, - } = cases; - const isLabsEnabled = useMemo(() => coreServices.uiSettings.get(UI_SETTINGS.ENABLE_LABS_UI), []); - const canCases = useMemo(() => canUseCases(), [canUseCases]); - const CasesContext = useMemo(() => getCasesContext(), [getCasesContext]); const { setHeaderActionMenu, onAppLeave } = useDashboardMountContext(); + const { generateScreenshot, screenshot } = useScreenshot(); + const dashboardApi = useDashboardApi(); const [ @@ -277,9 +269,8 @@ export function InternalDashboardTopNav({ const { viewModeTopNavConfig, editModeTopNavConfig } = useDashboardMenuItems({ isLabsShown, setIsLabsShown, - isAddToCaseModalOpen, setIsAddToCaseModalOpen, - setScreenshot, + generateScreenshot, maybeRedirect, showResetChange, }); @@ -405,16 +396,7 @@ export function InternalDashboardTopNav({ {viewMode !== 'print' && isLabsEnabled && isLabsShown ? ( setIsLabsShown(false)} /> ) : null} - {isAddToCaseModalOpen && canCases && lastSavedId ? ( - - setIsAddToCaseModalOpen(false)} - /> - - ) : null} + {viewMode === 'edit' ? : null} {showBorderBottom && } diff --git a/src/platform/plugins/shared/dashboard/public/plugin.tsx b/src/platform/plugins/shared/dashboard/public/plugin.tsx index e0e183c4d2e6a..94bb7a0450458 100644 --- a/src/platform/plugins/shared/dashboard/public/plugin.tsx +++ b/src/platform/plugins/shared/dashboard/public/plugin.tsx @@ -106,7 +106,7 @@ export interface DashboardStartDependencies { savedObjectsManagement: SavedObjectsManagementPluginStart; savedObjectsTaggingOss?: SavedObjectTaggingOssPluginStart; screenshotMode: ScreenshotModePluginStart; - share?: SharePluginStart; + share: SharePluginStart; spaces?: SpacesPluginStart; uiActions: UiActionsStart; unifiedSearch: UnifiedSearchPublicPluginStart; diff --git a/src/platform/plugins/shared/dashboard/public/services/kibana_services.ts b/src/platform/plugins/shared/dashboard/public/services/kibana_services.ts index 3cfefc77a1c55..1f7a1d470d73e 100644 --- a/src/platform/plugins/shared/dashboard/public/services/kibana_services.ts +++ b/src/platform/plugins/shared/dashboard/public/services/kibana_services.ts @@ -46,7 +46,7 @@ export let presentationUtilService: PresentationUtilPluginStart; export let savedObjectsTaggingService: SavedObjectTaggingOssPluginStart | undefined; export let screenshotModeService: ScreenshotModePluginStart; export let serverlessService: ServerlessPluginStart | undefined; -export let shareService: SharePluginStart | undefined; +export let shareService: SharePluginStart; export let spacesService: SpacesApi | undefined; export let uiActionsService: UiActionsPublicStart; export let urlForwardingService: UrlForwardingStart; diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment.test.tsx b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment.test.tsx index 8dea7d7888c57..0ae0f791d06ee 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment.test.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment.test.tsx @@ -19,16 +19,17 @@ describe('getPageAttachmentType', () => { persistableStateAttachmentState: { pathname: '/test/path', type: PAGE_ATTACHMENT_TYPE, - icon: 'link', - label: 'Test Page', url: { pathAndQuery: '/test/path?query=1', + iconType: 'link', + actionLabel: 'View in Dashboards', label: 'Test Link', }, snapshot: { imgData: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA...', }, renderComponentId: 'PageAttachmentChildren', + screenContext: 'This is a sample screen context for testing purposes.', }, attachmentId: 'test', caseData: { title: 'Test Case', id: 'case-id' }, diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment.tsx b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment.tsx index f8a09d707f758..fc16fc1e7fe6d 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment.tsx @@ -5,20 +5,25 @@ * 2.0. */ -import React from 'react'; +import React, { Suspense } from 'react'; +import { EuiLoadingSpinner } from '@elastic/eui'; import { PAGE_ATTACHMENT_TYPE } from '../../../../common/constants/links'; import * as i18n from './translations'; -import type { - PersistableStateAttachmentType, - PersistableStateAttachmentViewProps, - AttachmentViewObject, +import { + type PersistableStateAttachmentType, + type PersistableStateAttachmentViewProps, + type AttachmentViewObject, + type AttachmentAction, + AttachmentActionType, } from '../../../client/attachment_framework/types'; + import type { PageAttachmentPersistedState } from './types'; const AttachmentChildrenLazy = React.lazy(() => import('./attachment_children')); +const GoToAction = React.lazy(() => import('./go_to_action')); -const getLinkAttachmentViewObject = (): AttachmentViewObject< +const getPageAttachmentViewObject = (): AttachmentViewObject< PersistableStateAttachmentViewProps > => { return { @@ -26,6 +31,8 @@ const getLinkAttachmentViewObject = (): AttachmentViewObject< timelineAvatar: 'link', hideDefaultActions: false, children: AttachmentChildrenLazy, + getActions: (props: PersistableStateAttachmentViewProps) => + getPageAttachmentActions(props.persistableStateAttachmentState), }; }; @@ -34,6 +41,27 @@ export const getPageAttachmentType = id: PAGE_ATTACHMENT_TYPE, icon: 'link', displayName: i18n.PAGE_LABEL, - getAttachmentViewObject: getLinkAttachmentViewObject, + getAttachmentViewObject: getPageAttachmentViewObject, getAttachmentRemovalObject: () => ({ event: i18n.REMOVED_PAGE }), }); + +const getPageAttachmentActions = (state: PageAttachmentPersistedState): AttachmentAction[] => [ + { + type: AttachmentActionType.CUSTOM as const, + render: () => getGoToAction({ state }), + isPrimary: true, + }, + { + type: AttachmentActionType.CUSTOM as const, + render: () => getGoToAction({ state }), + isPrimary: false, + }, +]; + +function getGoToAction({ state }: { state: PageAttachmentPersistedState }) { + return ( + }> + + + ); +} diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment_children.test.tsx b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment_children.test.tsx index 7b26c9f69806a..599288dfbd001 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment_children.test.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment_children.test.tsx @@ -12,16 +12,17 @@ import type { PageAttachmentPersistedState } from './types'; describe('PageAttachmentChildren', () => { const mockPersistableState: PageAttachmentPersistedState = { - label: 'Test Page', - icon: 'link', snapshot: { imgData: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA...', }, type: 'mockType', url: { + actionLabel: 'View in Dashboards', pathAndQuery: '/test/path?query=1', - label: 'Test Link', + label: 'Sample Dashboard', + iconType: 'link', }, + screenContext: 'This is a sample screen context for testing purposes.', }; it('renders the link with correct label and href', () => { diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment_children.tsx b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment_children.tsx index 18c7612aaf4c3..529bd80a5021a 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment_children.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment_children.tsx @@ -6,7 +6,17 @@ */ import React from 'react'; -import { EuiImage, EuiLink } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { + EuiImage, + EuiSpacer, + EuiLink, + EuiCallOut, + EuiText, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, +} from '@elastic/eui'; import type { PageAttachmentPersistedState } from './types'; interface AttachmentChildrenProps { @@ -16,13 +26,46 @@ interface AttachmentChildrenProps { export const PageAttachmentChildren: React.FC = ({ persistableStateAttachmentState, }) => { - const { pathname, label, snapshot } = persistableStateAttachmentState; - + const { url, snapshot } = persistableStateAttachmentState; + if (!url) { + return ( + + +

+ {i18n.translate('xpack.cases.caseView.pageAttachment.noUrlProvided', { + defaultMessage: 'This page attachment does not contain a valid URL.', + })} +

+
+
+ ); + } + const href = url.pathAndQuery || ''; + const label = url.label; return ( <> - {label} + + + + + + + + {label} + + + {snapshot?.imgData && ( - + <> + + + )} ); diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/go_to_action.test.tsx b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/go_to_action.test.tsx new file mode 100644 index 0000000000000..8cc30842366d3 --- /dev/null +++ b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/go_to_action.test.tsx @@ -0,0 +1,57 @@ +/* + * 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, within } from '@testing-library/react'; +import GoToAction from './go_to_action'; + +describe('GoToAction', () => { + it('renders the button with correct props when state is valid', () => { + const state = { + type: 'discoverSession', + url: { + pathAndQuery: '/app/discover#/', + label: 'Discover session', + actionLabel: 'Go to Discover', + iconType: 'discoverApp', + }, + screenContext: 'This is a sample screen context for testing purposes.', + snapshot: { + imgData: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA...', + }, + }; + + render(); + const button = screen.getByTestId('cases-go-to-action'); + + expect(button).toBeInTheDocument(); + expect(within(button).getByText('Go to Discover')).toBeInTheDocument(); + expect(button).toHaveAttribute('href', '/app/discover#/'); + expect(button).toHaveAttribute('aria-label', 'Go to Discover'); + expect(button).toHaveAttribute('data-test-subj', 'cases-go-to-action'); + }); + + it('returns null when href or actionLabel is missing', () => { + const state = { + url: { + pathAndQuery: null, + label: 'Discover session', + actionLabel: 'actionLabel', + }, + iconType: 'discoverApp', + type: 'discoverSession', + screenContext: 'This is a sample screen context for testing purposes.', + snapshot: { + imgData: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA...', + }, + }; + + // @ts-ignore next line + render(); + expect(screen.queryByText('actionLabel')).not.toBeInTheDocument(); + }); +}); diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/go_to_action.tsx b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/go_to_action.tsx new file mode 100644 index 0000000000000..d2a123a1efd52 --- /dev/null +++ b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/go_to_action.tsx @@ -0,0 +1,39 @@ +/* + * 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 { EuiButtonEmpty, type IconType } from '@elastic/eui'; +import type { PageAttachmentPersistedState } from './types'; + +export interface GoToDiscoverButtonProps { + state: PageAttachmentPersistedState; +} + +const GoToAction: React.FC = ({ state }) => { + const url = state?.url; + const href = url?.pathAndQuery; + + const actionLabel = url?.actionLabel; + const iconType = state?.iconType as IconType; + if (!href || !actionLabel) { + return null; + } + const buttonProps = { + iconType, + 'aria-label': actionLabel, + href, + 'data-test-subj': 'cases-go-to-action', + }; + + return {actionLabel}; +}; + +GoToAction.displayName = 'PageAttachmentGoToAction'; + +// Note: This is for lazy loading +// eslint-disable-next-line import/no-default-export +export default GoToAction; diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/translations.tsx b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/translations.tsx index 2fcd1d4667426..2ccf5c0a462d7 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/translations.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/translations.tsx @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; export const ADDED_PAGE = i18n.translate('xpack.cases.caseView.links.addedLink', { - defaultMessage: 'added page', + defaultMessage: 'added a page', }); export const REMOVED_PAGE = i18n.translate('xpack.cases.caseView.links.removedLink', { defaultMessage: 'removed page', diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/types.ts b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/types.ts index c9f00ba11fec1..bd13f8f795845 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/types.ts +++ b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/types.ts @@ -7,14 +7,37 @@ import type { PersistableStateAttachmentState } from '../../../../common/types/domain'; export interface PageAttachmentPersistedState extends PersistableStateAttachmentState { + /** + * The type of page or page asset, e.g., 'dashboard', 'synthetics-test-run', 'slo-history', etc + */ type: string; - label: string; - icon: string; url: { + /** + * The URL to the page or page asset, excluding the base path. + */ pathAndQuery: string; + /** + * The icon representing the page type, displayed in the comment and action + */ + iconType: string; + /** + * The label to render in the "Go to" action, example "View in Dashboard" for the asset + */ + actionLabel: string; + /** + * The label to render in the comment for the page or page asset + */ label: string; }; + /** + * Optional snapshot of the page, used to render a screenshot in the comment + */ snapshot: { imgData: string; } | null; + /** + * Optional screen context for the page. A plain text description that + * can be provided to an LLM to generate a summary or perform analysis + */ + screenContext: string | null; } diff --git a/x-pack/platform/plugins/shared/cases/public/index.tsx b/x-pack/platform/plugins/shared/cases/public/index.tsx index af6e71580f944..7ec924f26cfab 100644 --- a/x-pack/platform/plugins/shared/cases/public/index.tsx +++ b/x-pack/platform/plugins/shared/cases/public/index.tsx @@ -18,6 +18,7 @@ export type { CasesPublicStart, CasesPublicSetup } from './types'; export type { GetCreateCaseFlyoutProps } from './client/ui/get_create_case_flyout'; export type { GetAllCasesSelectorModalProps } from './client/ui/get_all_cases_selector_modal'; export type { GetRecentCasesProps } from './client/ui/get_recent_cases'; +export type { PageAttachmentPersistedState } from './components/internal_attachments/page/types'; export type { CaseAttachments, CaseAttachmentsWithoutOwner } from './types'; From e9818ad378f84b54d4e3fc1e9e657431347f4a91 Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Tue, 17 Jun 2025 13:01:18 -0400 Subject: [PATCH 12/72] remove screenshot functionality --- package.json | 1 - renovate.json | 3 +- .../cases/dashboard_add_to_case_modal.tsx | 18 +---- .../top_nav/cases/use_screenshot.test.tsx | 62 ---------------- .../top_nav/cases/use_screenshot.ts | 73 ------------------- .../top_nav/use_dashboard_menu_items.tsx | 4 - .../internal_dashboard_top_nav.tsx | 6 +- .../page/attachment_children.test.tsx | 18 ----- .../page/attachment_children.tsx | 59 +-------------- .../internal_attachments/page/types.ts | 7 -- yarn.lock | 34 --------- 11 files changed, 6 insertions(+), 279 deletions(-) delete mode 100644 src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/use_screenshot.test.tsx delete mode 100644 src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/use_screenshot.ts diff --git a/package.json b/package.json index 42e677c27ca5f..55ee7ed2f54de 100644 --- a/package.json +++ b/package.json @@ -1218,7 +1218,6 @@ "he": "^1.2.0", "history": "^4.9.0", "hjson": "3.2.1", - "html2canvas": "^1.4.1", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.1", "i18n-iso-countries": "^7.14.0", diff --git a/renovate.json b/renovate.json index 1d580fe986660..2d12e44d4391d 100644 --- a/renovate.json +++ b/renovate.json @@ -3722,8 +3722,7 @@ { "groupName": "react-hook-form", "matchDepNames": [ - "react-hook-form", - "html2canvas" + "react-hook-form" ], "reviewers": [ "team:obs-ux-management-team" diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/dashboard_add_to_case_modal.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/dashboard_add_to_case_modal.tsx index b602ca029d690..675a141022cef 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/dashboard_add_to_case_modal.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/dashboard_add_to_case_modal.tsx @@ -26,13 +26,7 @@ import { getDashboardTitle } from '../../_dashboard_app_strings'; import { cases, shareService, coreServices } from '../../../services/kibana_services'; import { DashboardLocatorParams } from '../../../../common/types'; -export function AddToCaseModal({ - screenshot, - isOpen, -}: { - screenshot: string | null; - isOpen: boolean; -}) { +export function AddToCaseModal({ isOpen }: { isOpen: boolean }) { const getCasesContext = cases?.ui?.getCasesContext; const canUseCases = cases?.helpers?.canUseCases; @@ -85,7 +79,6 @@ export function AddToCaseModal({ @@ -95,14 +88,12 @@ export function AddToCaseModal({ interface OpenAddToCaseModalProps { savedObjectId: string; dashboardTitle: string; - screenshot: string | null; timeRange?: TimeRange; } const OpenAddToCaseOpenModal = ({ savedObjectId, dashboardTitle, - screenshot, timeRange, }: OpenAddToCaseModalProps) => { // Conditionally checked in wrapper to ensure cases is available @@ -141,11 +132,6 @@ const OpenAddToCaseOpenModal = ({ ), iconType: 'dashboardApp', }, - snapshot: screenshot - ? { - imgData: screenshot, - } - : null, screenContext: null, }; const attachments = { @@ -156,7 +142,7 @@ const OpenAddToCaseOpenModal = ({ casesModal.open({ getAttachments: () => [attachments] as CaseAttachmentsWithoutOwner, }); - }, [casesModal, dashboardLocator, savedObjectId, dashboardTitle, screenshot, timeRange]); + }, [casesModal, dashboardLocator, savedObjectId, dashboardTitle, timeRange]); return <>; }; diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/use_screenshot.test.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/use_screenshot.test.tsx deleted file mode 100644 index 52839bbac69a5..0000000000000 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/use_screenshot.test.tsx +++ /dev/null @@ -1,62 +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", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ -import React from 'react'; -import { renderHook, act } from '@testing-library/react'; -import html2canvas from 'html2canvas'; -import { useScreenshot } from './use_screenshot'; - -jest.mock('html2canvas'); - -describe('useScreenshot', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - const Wrapper = ({ children }: { children: React.ReactNode }) => ( -
{children}
- ); - - it('returns null screenshot initially', () => { - const { result } = renderHook(() => useScreenshot(), { - wrapper: Wrapper, - }); - expect(result.current.screenshot).toBeNull(); - }); - - it('sets screenshot when generateScreenshot is called successfully', async () => { - const mockCanvas = { - toDataURL: jest.fn().mockReturnValue('data:image/png;base64,mockImageData'), - }; - (html2canvas as jest.Mock).mockResolvedValue(mockCanvas); - - const { result } = renderHook(() => useScreenshot(), { - wrapper: Wrapper, - }); - await act(async () => { - await result.current.generateScreenshot(); - }); - - expect(html2canvas).toHaveBeenCalled(); - expect(mockCanvas.toDataURL).toHaveBeenCalledWith('image/png'); - expect(result.current.screenshot).toBe('data:image/png;base64,mockImageData'); - }); - - it('sets screenshot to null if an error occurs', async () => { - (html2canvas as jest.Mock).mockRejectedValue(new Error('mock error')); - - const { result } = renderHook(() => useScreenshot(), { - wrapper: Wrapper, - }); - await act(async () => { - await result.current.generateScreenshot(); - }); - - expect(result.current.screenshot).toBeNull(); - }); -}); diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/use_screenshot.ts b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/use_screenshot.ts deleted file mode 100644 index f33c4b58aedb9..0000000000000 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/use_screenshot.ts +++ /dev/null @@ -1,73 +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", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { useCallback, useState } from 'react'; -import html2canvas from 'html2canvas'; - -export function useScreenshot() { - const [screenshot, setScreenshot] = useState(null); - const generateScreenshot = useCallback(async () => { - const element = document.getElementById('kibana-body'); - if (!element) { - return null; - } - - const originalScrollTop = element.scrollTop; - element.scrollTop = 0; - - try { - const canvas = await html2canvas(element, { - scrollY: -window.scrollY, - scale: 0.9, - }); - - element.scrollTop = originalScrollTop; - - const splitCanvases = splitCanvasVertically(canvas); - const base64Chunks = splitCanvases.map((chunk) => chunk.toDataURL('image/jpeg', 0.6)); - - setScreenshot(base64Chunks); - } catch (error) { - setScreenshot(null); - } - }, [setScreenshot]); - - return { - generateScreenshot, - screenshot, - }; -} - -function splitCanvasVertically( - canvas: HTMLCanvasElement, - maxChunkHeight = 2000 -): HTMLCanvasElement[] { - const parts: HTMLCanvasElement[] = []; - const width = canvas.width; - const height = canvas.height; - - const partsCount = Math.ceil(height / maxChunkHeight); - - for (let i = 0; i < partsCount; i++) { - const chunkHeight = i === partsCount - 1 ? height - i * maxChunkHeight : maxChunkHeight; - - const chunkCanvas = document.createElement('canvas'); - chunkCanvas.width = width; - chunkCanvas.height = chunkHeight; - - const ctx = chunkCanvas.getContext('2d'); - if (!ctx) continue; - - ctx.drawImage(canvas, 0, i * maxChunkHeight, width, chunkHeight, 0, 0, width, chunkHeight); - - parts.push(chunkCanvas); - } - - return parts; -} diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx index af764b63ed056..75cffd8044c23 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx @@ -28,14 +28,12 @@ export const useDashboardMenuItems = ({ isLabsShown, setIsLabsShown, setIsAddToCaseModalOpen, - generateScreenshot, maybeRedirect, showResetChange, }: { isLabsShown: boolean; setIsLabsShown: Dispatch>; setIsAddToCaseModalOpen: Dispatch>; - generateScreenshot: () => Promise; maybeRedirect: (result?: SaveDashboardReturn) => void; showResetChange?: boolean; }) => { @@ -224,7 +222,6 @@ export const useDashboardMenuItems = ({ testId: 'dashboardAddToCaseButton', disableButton: disableTopNav, run: async () => { - await generateScreenshot(); setIsAddToCaseModalOpen(true); }, }, @@ -244,7 +241,6 @@ export const useDashboardMenuItems = ({ resetChanges, isResetting, setIsAddToCaseModalOpen, - generateScreenshot, ]); const resetChangesMenuItem = useMemo(() => { diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx index 413ab69e143b6..a328e5f181833 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx @@ -56,7 +56,6 @@ import { getDashboardCapabilities } from '../utils/get_dashboard_capabilities'; import { getFullEditPath } from '../utils/urls'; import { DashboardFavoriteButton } from './dashboard_favorite_button'; import { AddToCaseModal } from '../dashboard_app/top_nav/cases/dashboard_add_to_case_modal'; -import { useScreenshot } from '../dashboard_app/top_nav/cases/use_screenshot'; export interface InternalDashboardTopNavProps { customLeadingBreadCrumbs?: EuiBreadcrumb[]; embedSettings?: DashboardEmbedSettings; @@ -86,8 +85,6 @@ export function InternalDashboardTopNav({ const isLabsEnabled = useMemo(() => coreServices.uiSettings.get(UI_SETTINGS.ENABLE_LABS_UI), []); const { setHeaderActionMenu, onAppLeave } = useDashboardMountContext(); - const { generateScreenshot, screenshot } = useScreenshot(); - const dashboardApi = useDashboardApi(); const [ @@ -270,7 +267,6 @@ export function InternalDashboardTopNav({ isLabsShown, setIsLabsShown, setIsAddToCaseModalOpen, - generateScreenshot, maybeRedirect, showResetChange, }); @@ -396,7 +392,7 @@ export function InternalDashboardTopNav({ {viewMode !== 'print' && isLabsEnabled && isLabsShown ? ( setIsLabsShown(false)} /> ) : null} - + {viewMode === 'edit' ? : null} {showBorderBottom && } diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment_children.test.tsx b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment_children.test.tsx index 599288dfbd001..7a0bc631a3741 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment_children.test.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment_children.test.tsx @@ -12,9 +12,6 @@ import type { PageAttachmentPersistedState } from './types'; describe('PageAttachmentChildren', () => { const mockPersistableState: PageAttachmentPersistedState = { - snapshot: { - imgData: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA...', - }, type: 'mockType', url: { actionLabel: 'View in Dashboards', @@ -31,19 +28,4 @@ describe('PageAttachmentChildren', () => { const link = screen.getByRole('link', { name: 'Test Page' }); expect(link).toHaveAttribute('href', '/test/path'); }); - - it('renders the image when snapshot data is provided', () => { - render(); - - const image = screen.getByAltText('screenshot'); - expect(image).toHaveAttribute('src', mockPersistableState.snapshot?.imgData); - }); - - it('does not render the image when snapshot data is missing', () => { - const stateWithoutSnapshot = { ...mockPersistableState, snapshot: null }; - render(); - - const image = screen.queryByAltText('screenshot'); - expect(image).not.toBeInTheDocument(); - }); }); diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment_children.tsx b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment_children.tsx index 14bbe04fd4f02..e56f56fe9a6ee 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment_children.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment_children.tsx @@ -5,10 +5,9 @@ * 2.0. */ -import React, { useEffect, useState } from 'react'; +import React from 'react'; import { i18n } from '@kbn/i18n'; import { - EuiImage, EuiSpacer, EuiLink, EuiCallOut, @@ -23,54 +22,10 @@ interface AttachmentChildrenProps { persistableStateAttachmentState: PageAttachmentPersistedState; } -const base64ToImage = (src: string): Promise => { - return new Promise((resolve, reject) => { - const img = new Image(); - img.onload = () => resolve(img); - img.onerror = reject; - img.src = src; - }); -}; - -const stitchBase64ImagesVertically = async (base64Slices: string[]): Promise => { - const images = await Promise.all(base64Slices.map(base64ToImage)); - - const totalHeight = images.reduce((sum, img) => sum + img.height, 0); - const width = images[0]?.width || 0; - - const canvas = document.createElement('canvas'); - canvas.width = width; - canvas.height = totalHeight; - const ctx = canvas.getContext('2d'); - - let y = 0; - - if (ctx) { - for (const img of images) { - ctx.drawImage(img, 0, y); - y += img.height; - } - } - - return canvas.toDataURL('image/jpeg', 0.8); -}; - export const PageAttachmentChildren: React.FC = ({ persistableStateAttachmentState, }) => { - const { url, snapshot } = persistableStateAttachmentState; - const [screenshot, setScreenshot] = useState(null); - - useEffect(() => { - async function stitchImgData() { - if (snapshot && snapshot.imgData) { - const stitchedBase64 = await stitchBase64ImagesVertically(snapshot.imgData); - setScreenshot(stitchedBase64); - } - } - - stitchImgData(); - }, [snapshot]); + const { url } = persistableStateAttachmentState; if (!url) { return ( @@ -94,10 +49,6 @@ export const PageAttachmentChildren: React.FC = ({ const href = url.pathAndQuery || ''; const label = url.label; - if (screenshot === null) { - return null; - } - return ( <> @@ -111,12 +62,6 @@ export const PageAttachmentChildren: React.FC = ({ - {snapshot?.imgData && ( - <> - - - - )} ); }; diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/types.ts b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/types.ts index d140614ce7485..25b35e41490ec 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/types.ts +++ b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/types.ts @@ -29,13 +29,6 @@ export interface PageAttachmentPersistedState extends PersistableStateAttachment */ label: string; }; - /** - * Optional snapshot of the page, used to render a screenshot in the comment - */ - snapshot: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - imgData: any; - } | null; /** * Optional screen context for the page. A plain text description that * can be provided to an LLM to generate a summary or perform analysis diff --git a/yarn.lock b/yarn.lock index c4655d4d2024f..48323a859485a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14286,11 +14286,6 @@ bare-stream@^2.0.0: dependencies: streamx "^2.21.0" -base64-arraybuffer@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc" - integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ== - base64-js@*, base64-js@^1.1.2, base64-js@^1.2.0, base64-js@^1.3.0, base64-js@^1.3.1, base64-js@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" @@ -16147,13 +16142,6 @@ css-in-js-utils@^2.0.0: hyphenate-style-name "^1.0.2" isobject "^3.0.1" -css-line-break@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/css-line-break/-/css-line-break-2.1.0.tgz#bfef660dfa6f5397ea54116bb3cb4873edbc4fa0" - integrity sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w== - dependencies: - utrie "^1.0.2" - css-loader@^6.7.1: version "6.11.0" resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.11.0.tgz#33bae3bf6363d0a7c2cf9031c96c744ff54d85ba" @@ -20659,14 +20647,6 @@ html-webpack-plugin@^5.5.0: pretty-error "^4.0.0" tapable "^2.0.0" -html2canvas@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.4.1.tgz#7cef1888311b5011d507794a066041b14669a543" - integrity sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA== - dependencies: - css-line-break "^2.1.0" - text-segmentation "^1.0.3" - htmlparser2@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7" @@ -30518,13 +30498,6 @@ text-hex@1.0.x: resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5" integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg== -text-segmentation@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/text-segmentation/-/text-segmentation-1.0.3.tgz#52a388159efffe746b24a63ba311b6ac9f2d7943" - integrity sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw== - dependencies: - utrie "^1.0.2" - text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" @@ -31648,13 +31621,6 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= -utrie@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/utrie/-/utrie-1.0.2.tgz#d42fe44de9bc0119c25de7f564a6ed1b2c87a645" - integrity sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw== - dependencies: - base64-arraybuffer "^1.0.2" - uuid@11.1.0: version "11.1.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.1.0.tgz#9549028be1753bb934fc96e2bca09bb4105ae912" From b988b6771eed013c9ee21368fbff22aeee6d018f Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Tue, 17 Jun 2025 13:10:09 -0400 Subject: [PATCH 13/72] adjust rendered component --- .../internal_attachments/page/attachment_children.tsx | 11 +---------- .../internal_attachments/page/go_to_action.tsx | 2 +- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment_children.tsx b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment_children.tsx index e56f56fe9a6ee..e868e264619b4 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment_children.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment_children.tsx @@ -7,15 +7,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { - EuiSpacer, - EuiLink, - EuiCallOut, - EuiText, - EuiFlexGroup, - EuiFlexItem, - EuiIcon, -} from '@elastic/eui'; +import { EuiLink, EuiCallOut, EuiText, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; import type { PageAttachmentPersistedState } from './types'; interface AttachmentChildrenProps { @@ -51,7 +43,6 @@ export const PageAttachmentChildren: React.FC = ({ return ( <> - diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/go_to_action.tsx b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/go_to_action.tsx index d2a123a1efd52..a205ac2d2bcb6 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/go_to_action.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/go_to_action.tsx @@ -18,7 +18,7 @@ const GoToAction: React.FC = ({ state }) => { const href = url?.pathAndQuery; const actionLabel = url?.actionLabel; - const iconType = state?.iconType as IconType; + const iconType = url?.iconType as IconType; if (!href || !actionLabel) { return null; } From 6565e1bd20d64fb7b5655943f7d45a589adb26a2 Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Tue, 17 Jun 2025 14:10:00 -0400 Subject: [PATCH 14/72] adjust types and linting --- .../cases/dashboard_add_to_case_modal.tsx | 9 ++---- .../containers/user_profiles/api.test.ts | 31 +++++++++--------- .../apps/synthetics/hooks/use_screenshot.ts | 32 ------------------- 3 files changed, 19 insertions(+), 53 deletions(-) delete mode 100644 x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/hooks/use_screenshot.ts diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/dashboard_add_to_case_modal.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/dashboard_add_to_case_modal.tsx index 675a141022cef..a4c9578637c84 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/dashboard_add_to_case_modal.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/dashboard_add_to_case_modal.tsx @@ -105,12 +105,9 @@ const OpenAddToCaseOpenModal = ({ useEffect(() => { if (!dashboardLocator) { coreServices.notifications.toasts.addDanger({ - title: i18n.translate( - 'xpack.dashboard.topNav.cases.addToCaseModal.error.noDashboardLocator', - { - defaultMessage: 'Error adding dashboard to case', - } - ), + title: i18n.translate('dashboard.topNav.cases.addToCaseModal.error.noDashboardLocator', { + defaultMessage: 'Error adding dashboard to case', + }), 'data-test-subj': 'dashboardAddToCaseError', }); return; diff --git a/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/api.test.ts b/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/api.test.ts index 656bc699fefff..ad547925f40af 100644 --- a/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/api.test.ts +++ b/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/api.test.ts @@ -5,8 +5,7 @@ * 2.0. */ -import { securityMock } from '@kbn/security-plugin/public/mocks'; -import type { SecurityPluginStart } from '@kbn/security-plugin/public'; +import type { UserProfileService } from '@kbn/core-user-profile-browser'; import { GENERAL_CASES_OWNER } from '../../../common/constants'; import { createStartServicesMock } from '../../common/lib/kibana/kibana_react.mock'; import { bulkGetUserProfiles, getCurrentUserProfile, suggestUserProfiles } from './api'; @@ -49,17 +48,18 @@ describe('User profiles API', () => { }); describe('bulkGetUserProfiles', () => { - let security: SecurityPluginStart; + let userProfileService: UserProfileService; beforeEach(() => { jest.clearAllMocks(); - security = securityMock.createStart(); - security.userProfiles.bulkGet = jest.fn().mockResolvedValue(userProfiles); + userProfileService = { + bulkGet: jest.fn().mockResolvedValue(userProfiles), + } as unknown as UserProfileService; }); it('returns the user profiles correctly', async () => { const res = await bulkGetUserProfiles({ - security, + userProfile: userProfileService, uids: userProfilesIds, }); @@ -68,7 +68,7 @@ describe('User profiles API', () => { it('should filter out empty user profiles', async () => { const res = await bulkGetUserProfiles({ - security, + userProfile: userProfileService, uids: [...userProfilesIds, ''], }); @@ -77,11 +77,11 @@ describe('User profiles API', () => { it('calls bulkGet correctly', async () => { await bulkGetUserProfiles({ - security, + userProfile: userProfileService, uids: userProfilesIds, }); - expect(security.userProfiles.bulkGet).toHaveBeenCalledWith({ + expect(userProfileService.bulkGet).toHaveBeenCalledWith({ dataPath: 'avatar', uids: new Set([ 'u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0', @@ -93,19 +93,20 @@ describe('User profiles API', () => { }); describe('getCurrentUserProfile', () => { - let security: SecurityPluginStart; + let userProfileService: UserProfileService; const currentProfile = userProfiles[0]; beforeEach(() => { jest.clearAllMocks(); - security = securityMock.createStart(); - security.userProfiles.getCurrent = jest.fn().mockResolvedValue(currentProfile); + userProfileService = { + getCurrent: jest.fn().mockResolvedValue(currentProfile), + } as unknown as UserProfileService; }); it('returns the current user profile correctly', async () => { const res = await getCurrentUserProfile({ - security, + userProfile: userProfileService, }); expect(res).toEqual(currentProfile); @@ -113,10 +114,10 @@ describe('User profiles API', () => { it('calls getCurrent correctly', async () => { await getCurrentUserProfile({ - security, + userProfile: userProfileService, }); - expect(security.userProfiles.getCurrent).toHaveBeenCalledWith({ + expect(userProfileService.getCurrent).toHaveBeenCalledWith({ dataPath: 'avatar', }); }); diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/hooks/use_screenshot.ts b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/hooks/use_screenshot.ts deleted file mode 100644 index a6c3740c561a1..0000000000000 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/hooks/use_screenshot.ts +++ /dev/null @@ -1,32 +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 html2canvas from 'html2canvas'; - -export const useScreenshot = () => { - const [screenshot, setScreenshot] = useState(null); - - const takeScreenshot = async () => { - const element = document.getElementById('kibana-body'); - if (!element) { - return null; - } - - try { - const canvas = await html2canvas(element); - const dataUrl = canvas.toDataURL('image/png'); - setScreenshot(dataUrl); - } catch (error) { - setScreenshot(null); - } - }; - - return { - screenshot, - takeScreenshot, - }; -}; From 711442bb5fd67e44ab3065ad8b4056b46bb7342e Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Tue, 17 Jun 2025 14:39:47 -0400 Subject: [PATCH 15/72] adjust types --- .../top_nav/cases/dashboard_add_to_case_modal.tsx | 2 +- .../containers/user_profiles/use_bulk_get_user_profiles.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/dashboard_add_to_case_modal.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/dashboard_add_to_case_modal.tsx index a4c9578637c84..e632071aee88c 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/dashboard_add_to_case_modal.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/dashboard_add_to_case_modal.tsx @@ -122,7 +122,7 @@ const OpenAddToCaseOpenModal = ({ pathAndQuery: url, label: dashboardTitle, actionLabel: i18n.translate( - 'xpack.dashboard.topNav.cases.addToCaseModal.goToDashboardActionLabel', + 'dashboard.topNav.cases.addToCaseModal.goToDashboardActionLabel', { defaultMessage: 'Go to dashboard', } diff --git a/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/use_bulk_get_user_profiles.ts b/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/use_bulk_get_user_profiles.ts index 8b1b9580ca84f..29e7127159279 100644 --- a/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/use_bulk_get_user_profiles.ts +++ b/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/use_bulk_get_user_profiles.ts @@ -21,14 +21,14 @@ const profilesToMap = (profiles: UserProfileWithAvatar[]): Map()); export const useBulkGetUserProfiles = ({ uids }: { uids: string[] }) => { - const { security } = useKibana().services; + const { userProfile } = useKibana().services; const toasts = useToasts(); return useQuery>( casesQueriesKeys.userProfilesList(uids), () => { - return bulkGetUserProfiles({ security, uids }); + return bulkGetUserProfiles({ userProfile, uids }); }, { select: profilesToMap, From ad3b528cce4a479e4197df1dfdbbcc8f2e1e2dd3 Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Tue, 17 Jun 2025 16:20:02 -0400 Subject: [PATCH 16/72] adjust test --- .../cases/dashboard_add_to_case_modal.test.tsx | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/dashboard_add_to_case_modal.test.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/dashboard_add_to_case_modal.test.tsx index 8a16513a08177..a1c46044a3225 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/dashboard_add_to_case_modal.test.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/dashboard_add_to_case_modal.test.tsx @@ -116,7 +116,7 @@ describe('AddToCaseModal', () => { const { container } = render( - + ); @@ -134,7 +134,7 @@ describe('AddToCaseModal', () => { render( - + ); @@ -152,9 +152,6 @@ describe('AddToCaseModal', () => { persistableStateAttachmentTypeId: '.page', type: 'persistableState', persistableStateAttachmentState: { - snapshot: { - imgData: 'mockScreenshot', - }, type: 'dashboard', url: { iconType: 'dashboardApp', @@ -173,7 +170,7 @@ describe('AddToCaseModal', () => { const { container } = render( - + ); @@ -187,7 +184,7 @@ describe('AddToCaseModal', () => { render( - + ); @@ -209,7 +206,7 @@ describe('AddToCaseModal', () => { render( - + ); From 072a08796d109bd46f65aa9842ded1e7b65b2a75 Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Tue, 17 Jun 2025 21:25:29 -0400 Subject: [PATCH 17/72] adjust types --- src/platform/plugins/shared/dashboard/kibana.jsonc | 3 +-- .../top_nav/cases/dashboard_add_to_case_modal.tsx | 10 +++------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/platform/plugins/shared/dashboard/kibana.jsonc b/src/platform/plugins/shared/dashboard/kibana.jsonc index a6b74ca59db28..a2428cdcb6a7e 100644 --- a/src/platform/plugins/shared/dashboard/kibana.jsonc +++ b/src/platform/plugins/shared/dashboard/kibana.jsonc @@ -47,8 +47,7 @@ "kibanaUtils", "presentationUtil", "presentationPanel", - "savedObjects", - "cases" + "savedObjects" ] } } diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/dashboard_add_to_case_modal.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/dashboard_add_to_case_modal.tsx index e632071aee88c..220b88588d3a9 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/dashboard_add_to_case_modal.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/dashboard_add_to_case_modal.tsx @@ -15,11 +15,7 @@ import type { PageAttachmentPersistedState, CaseAttachmentsWithoutOwner, } from '@kbn/cases-plugin/public'; -import { - AttachmentType, - PAGE_ATTACHMENT_TYPE, - type CasesPermissions, -} from '@kbn/cases-plugin/common'; +import { type CasesPermissions } from '@kbn/cases-plugin/common'; import { DASHBOARD_APP_LOCATOR } from '@kbn/deeplinks-analytics'; import { useDashboardApi } from '../../../dashboard_api/use_dashboard_api'; import { getDashboardTitle } from '../../_dashboard_app_strings'; @@ -133,8 +129,8 @@ const OpenAddToCaseOpenModal = ({ }; const attachments = { persistableStateAttachmentState, - persistableStateAttachmentTypeId: PAGE_ATTACHMENT_TYPE, - type: AttachmentType.persistableState, + persistableStateAttachmentTypeId: '.page', + type: 'persistableState', }; casesModal.open({ getAttachments: () => [attachments] as CaseAttachmentsWithoutOwner, From d81aec3bf5c2454dde9ecb7f27708ccb55b50989 Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Wed, 18 Jun 2025 09:10:36 -0400 Subject: [PATCH 18/72] adjust tests --- .../public/components/case_form_fields/assignees.test.tsx | 2 +- .../components/internal_attachments/page/attachment.test.tsx | 2 +- .../internal_attachments/page/attachment_children.test.tsx | 4 ++-- .../user_profiles/use_bulk_get_user_profiles.test.ts | 2 +- .../user_profiles/use_get_current_user_profile.test.ts | 2 +- .../registered_persistable_state_basic.ts | 1 + .../registered_persistable_state_trial.ts | 1 + 7 files changed, 8 insertions(+), 6 deletions(-) diff --git a/x-pack/platform/plugins/shared/cases/public/components/case_form_fields/assignees.test.tsx b/x-pack/platform/plugins/shared/cases/public/components/case_form_fields/assignees.test.tsx index ab96016784f68..d7671b63e0820 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/case_form_fields/assignees.test.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/case_form_fields/assignees.test.tsx @@ -222,7 +222,7 @@ describe('Assignees', () => { }); expect(spyOnBulkGetUserProfiles).toHaveBeenCalledWith({ - security: expect.anything(), + userProfile: expect.anything(), uids: [userProfile.uid], }); diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment.test.tsx b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment.test.tsx index 0ae0f791d06ee..88987b50eb57e 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment.test.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment.test.tsx @@ -56,7 +56,7 @@ describe('getPageAttachmentType', () => { const linkType = getPageAttachmentType(); const event = linkType.getAttachmentViewObject(attachmentViewProps).event; - expect(event).toBe('added page'); + expect(event).toBe('added a page'); }); it('renders the timelineAvatar correctly', () => { diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment_children.test.tsx b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment_children.test.tsx index 7a0bc631a3741..7425b6e32eb5a 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment_children.test.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment_children.test.tsx @@ -25,7 +25,7 @@ describe('PageAttachmentChildren', () => { it('renders the link with correct label and href', () => { render(); - const link = screen.getByRole('link', { name: 'Test Page' }); - expect(link).toHaveAttribute('href', '/test/path'); + const link = screen.getByRole('link', { name: 'Sample Dashboard' }); + expect(link).toHaveAttribute('href', '/test/path?query=1'); }); }); diff --git a/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/use_bulk_get_user_profiles.test.ts b/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/use_bulk_get_user_profiles.test.ts index 3cefde7eb8527..814ecd791da65 100644 --- a/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/use_bulk_get_user_profiles.test.ts +++ b/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/use_bulk_get_user_profiles.test.ts @@ -44,7 +44,7 @@ describe('useBulkGetUserProfiles', () => { await waitFor(() => expect(spyOnBulkGetUserProfiles).toBeCalledWith({ ...props, - security: expect.anything(), + userProfile: expect.anything(), }) ); }); diff --git a/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/use_get_current_user_profile.test.ts b/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/use_get_current_user_profile.test.ts index 9a3e0c2018af3..5facd95bd1d0e 100644 --- a/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/use_get_current_user_profile.test.ts +++ b/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/use_get_current_user_profile.test.ts @@ -42,7 +42,7 @@ describe('useGetCurrentUserProfile', () => { }); expect(spyOnGetCurrentUserProfile).toBeCalledWith({ - security: expect.anything(), + userProfile: expect.anything(), }); }); diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/basic/attachments_framework/registered_persistable_state_basic.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/basic/attachments_framework/registered_persistable_state_basic.ts index 27a7d19831475..1f1f0d3755286 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/basic/attachments_framework/registered_persistable_state_basic.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/basic/attachments_framework/registered_persistable_state_basic.ts @@ -34,6 +34,7 @@ export default ({ getService }: FtrProviderContext): void => { expect(types).to.eql({ '.lens': '78559fd806809ac3a1008942ead2a079864054f5', + '.page': '8e5143e79705c8cebe2d5d9949e22d52944629d0', '.test': 'ab2204830c67f5cf992c9aa2f7e3ead752cc60a1', }); }); diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/attachments_framework/registered_persistable_state_trial.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/attachments_framework/registered_persistable_state_trial.ts index f44a47e51922a..c19de14a3c784 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/attachments_framework/registered_persistable_state_trial.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/attachments_framework/registered_persistable_state_trial.ts @@ -34,6 +34,7 @@ export default ({ getService }: FtrProviderContext): void => { expect(types).to.eql({ '.lens': '78559fd806809ac3a1008942ead2a079864054f5', + '.page': '8e5143e79705c8cebe2d5d9949e22d52944629d0', '.test': 'ab2204830c67f5cf992c9aa2f7e3ead752cc60a1', aiopsChangePointChart: 'a1212d71947ec34487b374cecc47ab9941b5d91c', ml_anomaly_charts: '23e92e824af9db6e8b8bb1d63c222e04f57d2147', From c7166c590c3346ff2ae6a5384a89bf801a1a55a0 Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Wed, 18 Jun 2025 13:03:30 -0400 Subject: [PATCH 19/72] adjust types --- .../components/internal_attachments/page/attachment.test.tsx | 2 +- .../internal_attachments/page/attachment_children.test.tsx | 2 +- .../internal_attachments/page/go_to_action.test.tsx | 4 ++-- .../public/components/internal_attachments/page/types.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment.test.tsx b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment.test.tsx index 88987b50eb57e..a52998f968800 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment.test.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment.test.tsx @@ -29,7 +29,7 @@ describe('getPageAttachmentType', () => { imgData: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA...', }, renderComponentId: 'PageAttachmentChildren', - screenContext: 'This is a sample screen context for testing purposes.', + screenContext: null, }, attachmentId: 'test', caseData: { title: 'Test Case', id: 'case-id' }, diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment_children.test.tsx b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment_children.test.tsx index 7425b6e32eb5a..69c17ab89ea04 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment_children.test.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment_children.test.tsx @@ -19,7 +19,7 @@ describe('PageAttachmentChildren', () => { label: 'Sample Dashboard', iconType: 'link', }, - screenContext: 'This is a sample screen context for testing purposes.', + screenContext: null, }; it('renders the link with correct label and href', () => { diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/go_to_action.test.tsx b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/go_to_action.test.tsx index 8cc30842366d3..24f681fe39ff6 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/go_to_action.test.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/go_to_action.test.tsx @@ -19,7 +19,7 @@ describe('GoToAction', () => { actionLabel: 'Go to Discover', iconType: 'discoverApp', }, - screenContext: 'This is a sample screen context for testing purposes.', + screenContext: null, snapshot: { imgData: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA...', }, @@ -44,7 +44,7 @@ describe('GoToAction', () => { }, iconType: 'discoverApp', type: 'discoverSession', - screenContext: 'This is a sample screen context for testing purposes.', + screenContext: null, snapshot: { imgData: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA...', }, diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/types.ts b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/types.ts index 25b35e41490ec..49b96c25c32dd 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/types.ts +++ b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/types.ts @@ -33,5 +33,5 @@ export interface PageAttachmentPersistedState extends PersistableStateAttachment * Optional screen context for the page. A plain text description that * can be provided to an LLM to generate a summary or perform analysis */ - screenContext: string | null; + screenContext: Array<{ screenDescription: string }> | null; } From 479cf7c681b27c0493fb6be87f0bbb2ceb208370 Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Wed, 25 Jun 2025 09:33:28 -0400 Subject: [PATCH 20/72] remove dashboard implementation --- .../plugins/shared/dashboard/kibana.jsonc | 5 +- .../dashboard_app/_dashboard_app_strings.ts | 8 - .../public/dashboard_app/dashboard_router.tsx | 45 ++-- .../dashboard_add_to_case_modal.test.tsx | 227 ------------------ .../cases/dashboard_add_to_case_modal.tsx | 160 ------------ .../top_nav/use_dashboard_menu_items.tsx | 16 -- .../internal_dashboard_top_nav.tsx | 6 +- .../shared/dashboard/public/plugin.tsx | 4 +- .../public/services/kibana_services.ts | 5 +- .../plugins/shared/dashboard/tsconfig.json | 3 +- 10 files changed, 26 insertions(+), 453 deletions(-) delete mode 100644 src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/dashboard_add_to_case_modal.test.tsx delete mode 100644 src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/dashboard_add_to_case_modal.tsx diff --git a/src/platform/plugins/shared/dashboard/kibana.jsonc b/src/platform/plugins/shared/dashboard/kibana.jsonc index a2428cdcb6a7e..c03b46f7a5b59 100644 --- a/src/platform/plugins/shared/dashboard/kibana.jsonc +++ b/src/platform/plugins/shared/dashboard/kibana.jsonc @@ -26,7 +26,7 @@ "uiActions", "urlForwarding", "presentationUtil", - "unifiedSearch", + "unifiedSearch" ], "optionalPlugins": [ "home", @@ -39,8 +39,7 @@ "serverless", "noDataPage", "observabilityAIAssistant", - "lens", - "cases" + "lens" ], "requiredBundles": [ "kibanaReact", diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/_dashboard_app_strings.ts b/src/platform/plugins/shared/dashboard/public/dashboard_app/_dashboard_app_strings.ts index d4a0597d8ca2a..16e865a1b6155 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/_dashboard_app_strings.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/_dashboard_app_strings.ts @@ -246,14 +246,6 @@ export const topNavStrings = { defaultMessage: 'Create a copy of your dashboard', }), }, - addToCase: { - label: i18n.translate('dashboard.topNave.addToCaseButtonAriaLabel', { - defaultMessage: 'add to case', - }), - description: i18n.translate('dashboard.topNave.addToCaseConfigDescription', { - defaultMessage: 'Add this dashboard to a case', - }), - }, }; export const getControlButtonTitle = () => diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/dashboard_router.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_app/dashboard_router.tsx index 2525c308c89b4..10d6bf6d32281 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/dashboard_router.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/dashboard_router.tsx @@ -10,7 +10,6 @@ import { AppMountParameters, CoreStart } from '@kbn/core/public'; import { createKbnUrlStateStorage, withNotifyOnErrors } from '@kbn/kibana-utils-plugin/public'; import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; -import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { Route, Routes } from '@kbn/shared-ux-router'; import { parse, ParsedQuery } from 'query-string'; import React from 'react'; @@ -145,31 +144,25 @@ export async function mountApp({ const app = ( - - - - - - - - - - - - - - + + + + + + + + + + + + ); diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/dashboard_add_to_case_modal.test.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/dashboard_add_to_case_modal.test.tsx deleted file mode 100644 index a1c46044a3225..0000000000000 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/dashboard_add_to_case_modal.test.tsx +++ /dev/null @@ -1,227 +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", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import React from 'react'; -import { render } from '@testing-library/react'; -import { AddToCaseModal } from './dashboard_add_to_case_modal'; -import { coreServices, cases, shareService } from '../../../services/kibana_services'; -import { DashboardContext } from '../../../dashboard_api/use_dashboard_api'; -import { DashboardInternalContext } from '../../../dashboard_api/use_dashboard_internal_api'; - -import { buildMockDashboardApi } from '../../../mocks'; - -jest.mock('../../../services/kibana_services', () => ({ - cases: { - ui: { - getCasesContext: jest.fn(() => ({ children }: any) => <>{children}), - }, - helpers: { - canUseCases: jest.fn(() => ({ - all: true, - create: true, - read: true, - update: true, - delete: true, - push: true, - connectors: true, - settings: true, - reopenCase: true, - createComment: true, - assign: true, - })), - }, - hooks: { - useCasesAddToExistingCaseModal: jest.fn(() => ({ - open: jest.fn(), - })), - }, - }, - shareService: { - url: { - locators: { - get: jest.fn(() => ({ - getRedirectUrl: jest.fn().mockImplementation(({ timeRange }) => { - return `/mock-redirect-url?from=${encodeURIComponent( - timeRange.from - )}&to=${encodeURIComponent(timeRange.to)}`; - }), - })), - }, - }, - }, - coreServices: { - notifications: { - toasts: { - addDanger: jest.fn(), - }, - }, - application: { - capabilities: { - dashboard_v2: { - save: true, - }, - }, - }, - }, - dataService: { - query: { - timefilter: { - timefilter: { - getTime: jest.fn(() => ({ from: 'now-15m', to: 'now' })), - getRefreshInterval: jest.fn(() => ({ pause: true, value: 0 })), - }, - }, - }, - }, -})); - -describe('AddToCaseModal', () => { - const { api: mockDashboardApi, internalApi } = buildMockDashboardApi({ - savedObjectId: 'mockSavedId', - overrides: { - timeRange: { from: 'now-15m', to: 'now' }, - }, - }); - - const mockDate = new Date('2025-06-17T15:37:53.937Z'); - beforeAll(() => { - jest.useFakeTimers().setSystemTime(mockDate); - }); - - afterAll(() => { - jest.useRealTimers(); - }); - - beforeEach(() => { - jest.clearAllMocks(); - // @ts-ignore next line - shareService.url.locators.get = jest.fn(() => ({ - getRedirectUrl: jest.fn().mockImplementation(({ timeRange }) => { - return `/mock-redirect-url?from=${encodeURIComponent( - timeRange.from - )}&to=${encodeURIComponent(timeRange.to)}`; - }), - })); - }); - - it('renders null if cases is not available', () => { - jest.mock('../../../services/kibana_services', () => ({ cases: null })); - - const { container } = render( - - - - - - ); - - expect(container.firstChild).toBeNull(); - }); - - it('renders the modal when isOpen is true and required props are provided', () => { - const open = jest.fn(); - const casesMock = cases!; - jest.spyOn(casesMock.hooks, 'useCasesAddToExistingCaseModal').mockReturnValue({ - close: jest.fn(), - open, - }); - render( - - - - - - ); - - expect(open).toBeCalledWith( - expect.objectContaining({ - getAttachments: expect.any(Function), - }) - ); - - // Optionally, you can invoke the function and test its return value - const callArgs = open.mock.calls[0][0]; - expect(callArgs.getAttachments()).toEqual([ - { - persistableStateAttachmentTypeId: '.page', - type: 'persistableState', - persistableStateAttachmentState: { - type: 'dashboard', - url: { - iconType: 'dashboardApp', - pathAndQuery: - '/mock-redirect-url?from=2025-06-17T15%3A22%3A53.937Z&to=2025-06-17T15%3A37%3A53.937Z', - label: 'My Dashboard', - actionLabel: 'Go to dashboard', - }, - screenContext: null, - }, - }, - ]); - }); - - it('does not render the modal when isOpen is false', () => { - const { container } = render( - - - - - - ); - - expect(container.firstChild).toBeNull(); - }); - - it('shows a danger toast when the dashboard locator is not defined', () => { - shareService.url.locators.get = jest.fn(() => undefined); - - render( - - - - - - ); - - expect(coreServices.notifications.toasts.addDanger).toHaveBeenCalledWith({ - title: expect.any(String), - 'data-test-subj': 'dashboardAddToCaseError', - }); - }); - - it('saves the time range in the URL as absolute time', () => { - const open = jest.fn(); - const casesMock = cases!; - jest.spyOn(casesMock.hooks, 'useCasesAddToExistingCaseModal').mockReturnValue({ - close: jest.fn(), - open, - }); - - render( - - - - - - ); - - expect(open).toBeCalledWith( - expect.objectContaining({ - getAttachments: expect.any(Function), - }) - ); - - const callArgs = open.mock.calls[0][0]; - const attachmentState = callArgs.getAttachments()[0].persistableStateAttachmentState; - - expect(attachmentState.url.pathAndQuery).toContain( - '/mock-redirect-url?from=2025-06-17T15%3A22%3A53.937Z&to=2025-06-17T15%3A37%3A53.937Z' - ); - }); -}); diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/dashboard_add_to_case_modal.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/dashboard_add_to_case_modal.tsx deleted file mode 100644 index 220b88588d3a9..0000000000000 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/cases/dashboard_add_to_case_modal.tsx +++ /dev/null @@ -1,160 +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", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import React, { useMemo, useEffect } from 'react'; -import { type TimeRange, getAbsoluteTimeRange } from '@kbn/data-plugin/common'; -import { i18n } from '@kbn/i18n'; -import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing'; -import type { - PageAttachmentPersistedState, - CaseAttachmentsWithoutOwner, -} from '@kbn/cases-plugin/public'; -import { type CasesPermissions } from '@kbn/cases-plugin/common'; -import { DASHBOARD_APP_LOCATOR } from '@kbn/deeplinks-analytics'; -import { useDashboardApi } from '../../../dashboard_api/use_dashboard_api'; -import { getDashboardTitle } from '../../_dashboard_app_strings'; -import { cases, shareService, coreServices } from '../../../services/kibana_services'; -import { DashboardLocatorParams } from '../../../../common/types'; - -export function AddToCaseModal({ isOpen }: { isOpen: boolean }) { - 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 = - casesPermissions.read && casesPermissions.update && casesPermissions.push; - const CasesContext = useMemo(() => { - if (!getCasesContext) { - return React.Fragment; - } - return getCasesContext(); - }, [getCasesContext]); - - const dashboardApi = useDashboardApi(); - - const [lastSavedId, title, viewMode, timeRange] = useBatchedPublishingSubjects( - dashboardApi.savedObjectId$, - dashboardApi.title$, - dashboardApi.viewMode$, - dashboardApi.timeRange$ - ); - - const dashboardTitle = useMemo(() => { - return getDashboardTitle(title, viewMode, !lastSavedId); - }, [title, viewMode, lastSavedId]); - - if (!cases) { - return null; - } - - return isOpen && hasCasesPermissions && lastSavedId ? ( - - - - ) : null; -} - -interface OpenAddToCaseModalProps { - savedObjectId: string; - dashboardTitle: string; - timeRange?: TimeRange; -} - -const OpenAddToCaseOpenModal = ({ - savedObjectId, - dashboardTitle, - timeRange, -}: OpenAddToCaseModalProps) => { - // Conditionally checked in wrapper to ensure cases is available - const useCasesAddToExistingCaseModal = cases?.hooks.useCasesAddToExistingCaseModal!; - const { url: urlService } = shareService; - const dashboardLocator = urlService.locators.get(DASHBOARD_APP_LOCATOR); - const casesModal = useCasesAddToExistingCaseModal(); - - useEffect(() => { - if (!dashboardLocator) { - coreServices.notifications.toasts.addDanger({ - title: i18n.translate('dashboard.topNav.cases.addToCaseModal.error.noDashboardLocator', { - defaultMessage: 'Error adding dashboard to case', - }), - 'data-test-subj': 'dashboardAddToCaseError', - }); - return; - } - const url = dashboardLocator.getRedirectUrl({ - dashboardId: savedObjectId, - timeRange: convertToAbsoluteTimeRange(timeRange), - }); - const persistableStateAttachmentState: PageAttachmentPersistedState = { - type: 'dashboard', - url: { - pathAndQuery: url, - label: dashboardTitle, - actionLabel: i18n.translate( - 'dashboard.topNav.cases.addToCaseModal.goToDashboardActionLabel', - { - defaultMessage: 'Go to dashboard', - } - ), - iconType: 'dashboardApp', - }, - screenContext: null, - }; - const attachments = { - persistableStateAttachmentState, - persistableStateAttachmentTypeId: '.page', - type: 'persistableState', - }; - casesModal.open({ - getAttachments: () => [attachments] as CaseAttachmentsWithoutOwner, - }); - }, [casesModal, dashboardLocator, savedObjectId, dashboardTitle, timeRange]); - - return <>; -}; - -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/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx index 75cffd8044c23..b7261cfaec13d 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx @@ -27,13 +27,11 @@ import { ShowShareModal } from './share/show_share_modal'; export const useDashboardMenuItems = ({ isLabsShown, setIsLabsShown, - setIsAddToCaseModalOpen, maybeRedirect, showResetChange, }: { isLabsShown: boolean; setIsLabsShown: Dispatch>; - setIsAddToCaseModalOpen: Dispatch>; maybeRedirect: (result?: SaveDashboardReturn) => void; showResetChange?: boolean; }) => { @@ -215,16 +213,6 @@ export const useDashboardMenuItems = ({ disableButton: disableTopNav, run: () => openSettingsFlyout(dashboardApi), }, - - addToCase: { - ...topNavStrings.addToCase, - id: 'add-to-case', - testId: 'dashboardAddToCaseButton', - disableButton: disableTopNav, - run: async () => { - setIsAddToCaseModalOpen(true); - }, - }, }; }, [ disableTopNav, @@ -240,7 +228,6 @@ export const useDashboardMenuItems = ({ quickSaveDashboard, resetChanges, isResetting, - setIsAddToCaseModalOpen, ]); const resetChangesMenuItem = useMemo(() => { @@ -287,7 +274,6 @@ export const useDashboardMenuItems = ({ ].filter(Boolean) as TopNavMenuData[]) : []; const duplicateMenuItem = showWriteControls ? [menuItems.interactiveSave] : []; - const addToCaseMenuItem = [menuItems.addToCase]; const editMenuItem = showWriteControls && !dashboardApi.isManaged ? [menuItems.edit] : []; const mayberesetChangesMenuItem = showResetChange ? [resetChangesMenuItem] : []; @@ -295,7 +281,6 @@ export const useDashboardMenuItems = ({ ...labsMenuItem, menuItems.fullScreen, ...duplicateMenuItem, - ...addToCaseMenuItem, ...mayberesetChangesMenuItem, ...shareMenuItem, ...editMenuItem, @@ -308,7 +293,6 @@ export const useDashboardMenuItems = ({ menuItems.interactiveSave, menuItems.edit, menuItems.fullScreen, - menuItems.addToCase, hasExportIntegration, dashboardApi.isManaged, showResetChange, diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx index a328e5f181833..8fd3157e284fc 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx @@ -9,6 +9,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import UseUnmount from 'react-use/lib/useUnmount'; + import { EuiBadge, EuiBreadcrumb, @@ -55,7 +56,7 @@ import { import { getDashboardCapabilities } from '../utils/get_dashboard_capabilities'; import { getFullEditPath } from '../utils/urls'; import { DashboardFavoriteButton } from './dashboard_favorite_button'; -import { AddToCaseModal } from '../dashboard_app/top_nav/cases/dashboard_add_to_case_modal'; + export interface InternalDashboardTopNavProps { customLeadingBreadCrumbs?: EuiBreadcrumb[]; embedSettings?: DashboardEmbedSettings; @@ -79,7 +80,6 @@ export function InternalDashboardTopNav({ }: InternalDashboardTopNavProps) { const [isChromeVisible, setIsChromeVisible] = useState(false); const [isLabsShown, setIsLabsShown] = useState(false); - const [isAddToCaseModalOpen, setIsAddToCaseModalOpen] = useState(false); const dashboardTitleRef = useRef(null); const isLabsEnabled = useMemo(() => coreServices.uiSettings.get(UI_SETTINGS.ENABLE_LABS_UI), []); @@ -266,7 +266,6 @@ export function InternalDashboardTopNav({ const { viewModeTopNavConfig, editModeTopNavConfig } = useDashboardMenuItems({ isLabsShown, setIsLabsShown, - setIsAddToCaseModalOpen, maybeRedirect, showResetChange, }); @@ -392,7 +391,6 @@ export function InternalDashboardTopNav({ {viewMode !== 'print' && isLabsEnabled && isLabsShown ? ( setIsLabsShown(false)} /> ) : null} - {viewMode === 'edit' ? : null} {showBorderBottom && } diff --git a/src/platform/plugins/shared/dashboard/public/plugin.tsx b/src/platform/plugins/shared/dashboard/public/plugin.tsx index 94bb7a0450458..e3d96f50316bd 100644 --- a/src/platform/plugins/shared/dashboard/public/plugin.tsx +++ b/src/platform/plugins/shared/dashboard/public/plugin.tsx @@ -32,7 +32,6 @@ import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public'; import type { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/public'; import { FieldFormatsStart } from '@kbn/field-formats-plugin/public/plugin'; import type { HomePublicPluginSetup } from '@kbn/home-plugin/public'; -import type { CasesPublicStart } from '@kbn/cases-plugin/public'; import { i18n } from '@kbn/i18n'; import type { Start as InspectorStartContract } from '@kbn/inspector-plugin/public'; import { replaceUrlHashQuery } from '@kbn/kibana-utils-plugin/common'; @@ -106,7 +105,7 @@ export interface DashboardStartDependencies { savedObjectsManagement: SavedObjectsManagementPluginStart; savedObjectsTaggingOss?: SavedObjectTaggingOssPluginStart; screenshotMode: ScreenshotModePluginStart; - share: SharePluginStart; + share?: SharePluginStart; spaces?: SpacesPluginStart; uiActions: UiActionsStart; unifiedSearch: UnifiedSearchPublicPluginStart; @@ -116,7 +115,6 @@ export interface DashboardStartDependencies { serverless?: ServerlessPluginStart; noDataPage?: NoDataPagePluginStart; lens?: LensPublicStart; - cases?: CasesPublicStart; observabilityAIAssistant?: ObservabilityAIAssistantPublicStart; } diff --git a/src/platform/plugins/shared/dashboard/public/services/kibana_services.ts b/src/platform/plugins/shared/dashboard/public/services/kibana_services.ts index 1f7a1d470d73e..9aed7fffb6497 100644 --- a/src/platform/plugins/shared/dashboard/public/services/kibana_services.ts +++ b/src/platform/plugins/shared/dashboard/public/services/kibana_services.ts @@ -11,7 +11,6 @@ import { BehaviorSubject } from 'rxjs'; import type { ContentManagementPublicStart } from '@kbn/content-management-plugin/public'; import type { CoreStart } from '@kbn/core/public'; -import type { CasesPublicStart } from '@kbn/cases-plugin/public'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public'; import type { EmbeddableStart } from '@kbn/embeddable-plugin/public'; @@ -46,12 +45,11 @@ export let presentationUtilService: PresentationUtilPluginStart; export let savedObjectsTaggingService: SavedObjectTaggingOssPluginStart | undefined; export let screenshotModeService: ScreenshotModePluginStart; export let serverlessService: ServerlessPluginStart | undefined; -export let shareService: SharePluginStart; +export let shareService: SharePluginStart | undefined; export let spacesService: SpacesApi | undefined; export let uiActionsService: UiActionsPublicStart; export let urlForwardingService: UrlForwardingStart; export let usageCollectionService: UsageCollectionStart | undefined; -export let cases: CasesPublicStart | undefined; const servicesReady$ = new BehaviorSubject(false); @@ -75,7 +73,6 @@ export const setKibanaServices = (kibanaCore: CoreStart, deps: DashboardStartDep uiActionsService = deps.uiActions; urlForwardingService = deps.urlForwarding; usageCollectionService = deps.usageCollection; - cases = deps.cases; servicesReady$.next(true); }; diff --git a/src/platform/plugins/shared/dashboard/tsconfig.json b/src/platform/plugins/shared/dashboard/tsconfig.json index cc9d7f08d4194..9eb807a7baf6e 100644 --- a/src/platform/plugins/shared/dashboard/tsconfig.json +++ b/src/platform/plugins/shared/dashboard/tsconfig.json @@ -85,8 +85,7 @@ "@kbn/esql-types", "@kbn/saved-objects-tagging-plugin", "@kbn/react-kibana-context-theme", - "@kbn/std", - "@kbn/cases-plugin" + "@kbn/std" ], "exclude": ["target/**/*"] } From 87dd13fe40013f687a4f38f3d032b9938839306e Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Wed, 25 Jun 2025 09:41:44 -0400 Subject: [PATCH 21/72] Update use_dashboard_menu_items.tsx --- .../public/dashboard_app/top_nav/use_dashboard_menu_items.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx index b7261cfaec13d..3e63534e817c3 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx @@ -116,6 +116,7 @@ export const useDashboardMenuItems = ({ /** * Register all of the top nav configs that can be used by dashboard. */ + const menuItems = useMemo(() => { return { fullScreen: { From 5bde52d5b0f8c399747571b1d196c7bb57185495 Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Wed, 25 Jun 2025 09:42:14 -0400 Subject: [PATCH 22/72] Update use_dashboard_menu_items.tsx --- .../public/dashboard_app/top_nav/use_dashboard_menu_items.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx index 3e63534e817c3..52d132b8d5582 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx @@ -116,7 +116,7 @@ export const useDashboardMenuItems = ({ /** * Register all of the top nav configs that can be used by dashboard. */ - + const menuItems = useMemo(() => { return { fullScreen: { From 07f312a3ce98952d56c1395160ea2518c8bb3c34 Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Wed, 25 Jun 2025 11:33:48 -0400 Subject: [PATCH 23/72] add attachment type to observability --- .../kbn-observability-schema/index.ts | 2 + .../page_attachment/constants.ts | 8 +++ .../page_attachment/latest.ts | 11 ++++ .../page_attachment/schema/latest.ts | 1 + .../page_attachment/schema/v1.ts | 40 +++++++++++ .../public/attachments/page/attachment.tsx | 64 ++++++++++++++++++ .../page/attachment_children.test.tsx | 30 +++++++++ .../attachments/page/attachment_children.tsx | 66 +++++++++++++++++++ .../attachments/page/go_to_action.test.tsx | 57 ++++++++++++++++ .../public/attachments/page/go_to_action.tsx | 39 +++++++++++ .../public/attachments/page/translations.tsx | 18 +++++ 11 files changed, 336 insertions(+) create mode 100644 x-pack/solutions/observability/packages/kbn-observability-schema/page_attachment/constants.ts create mode 100644 x-pack/solutions/observability/packages/kbn-observability-schema/page_attachment/latest.ts create mode 100644 x-pack/solutions/observability/packages/kbn-observability-schema/page_attachment/schema/latest.ts create mode 100644 x-pack/solutions/observability/packages/kbn-observability-schema/page_attachment/schema/v1.ts create mode 100644 x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment.tsx create mode 100644 x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment_children.test.tsx create mode 100644 x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment_children.tsx create mode 100644 x-pack/solutions/observability/plugins/observability/public/attachments/page/go_to_action.test.tsx create mode 100644 x-pack/solutions/observability/plugins/observability/public/attachments/page/go_to_action.tsx create mode 100644 x-pack/solutions/observability/plugins/observability/public/attachments/page/translations.tsx diff --git a/x-pack/solutions/observability/packages/kbn-observability-schema/index.ts b/x-pack/solutions/observability/packages/kbn-observability-schema/index.ts index 3095666dff351..0f9f44a833454 100644 --- a/x-pack/solutions/observability/packages/kbn-observability-schema/index.ts +++ b/x-pack/solutions/observability/packages/kbn-observability-schema/index.ts @@ -6,3 +6,5 @@ */ export * from './related_dashboards/latest'; +export * from './page_attachment/latest'; +export * from './page_attachment/constants'; diff --git a/x-pack/solutions/observability/packages/kbn-observability-schema/page_attachment/constants.ts b/x-pack/solutions/observability/packages/kbn-observability-schema/page_attachment/constants.ts new file mode 100644 index 0000000000000..6a14dc5355d9e --- /dev/null +++ b/x-pack/solutions/observability/packages/kbn-observability-schema/page_attachment/constants.ts @@ -0,0 +1,8 @@ +/* + * 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. + */ + +export const PAGE_ATTACHMENT_TYPE = '.page'; diff --git a/x-pack/solutions/observability/packages/kbn-observability-schema/page_attachment/latest.ts b/x-pack/solutions/observability/packages/kbn-observability-schema/page_attachment/latest.ts new file mode 100644 index 0000000000000..905ddd3da4178 --- /dev/null +++ b/x-pack/solutions/observability/packages/kbn-observability-schema/page_attachment/latest.ts @@ -0,0 +1,11 @@ +/* + * 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. + */ + +export { + type PageAttachmentPersistedState, + pageAttachmentPersistedStateSchema, +} from './schema/latest'; diff --git a/x-pack/solutions/observability/packages/kbn-observability-schema/page_attachment/schema/latest.ts b/x-pack/solutions/observability/packages/kbn-observability-schema/page_attachment/schema/latest.ts new file mode 100644 index 0000000000000..4b663d44837ef --- /dev/null +++ b/x-pack/solutions/observability/packages/kbn-observability-schema/page_attachment/schema/latest.ts @@ -0,0 +1 @@ +export { type PageAttachmentPersistedState, pageAttachmentPersistedStateSchema } from './v1'; diff --git a/x-pack/solutions/observability/packages/kbn-observability-schema/page_attachment/schema/v1.ts b/x-pack/solutions/observability/packages/kbn-observability-schema/page_attachment/schema/v1.ts new file mode 100644 index 0000000000000..f4967a619707e --- /dev/null +++ b/x-pack/solutions/observability/packages/kbn-observability-schema/page_attachment/schema/v1.ts @@ -0,0 +1,40 @@ +/* + * 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 { z } from '@kbn/zod'; + +export const pageAttachmentPersistedStateSchema = z.object({ + /** + * The type of page or page asset, e.g., 'dashboard', 'synthetics-test-run', 'slo-history', etc + */ + type: z.string(), + url: z.object({ + /** + * The URL to the page or page asset, excluding the base path. + */ + pathAndQuery: z.string(), + /** + * The label to render in the "Go to" action, example "View in Dashboard" for the asset + */ + actionLabel: z.string(), + /** + * The label to render in the attachment comment for the page or page asset + */ + label: z.string(), + /** + * The icon representing the page type, displayed in the comment and action + */ + iconType: z.string().optional(), + }), + /** + * Optional screen context for the page. A plain text description that + * can be provided to an LLM to generate a summary or perform analysis + */ + screenContext: z.array(z.object({ screenDescription: z.string() })).optional(), +}); + +export type PageAttachmentPersistedState = z.infer; diff --git a/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment.tsx b/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment.tsx new file mode 100644 index 0000000000000..24e11a1da7810 --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment.tsx @@ -0,0 +1,64 @@ +/* + * 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, { Suspense } from 'react'; +import { type PageAttachmentPersistedState, PAGE_ATTACHMENT_TYPE } from '@kbn/observability-schema'; +import { EuiLoadingSpinner } from '@elastic/eui'; +import { + type PersistableStateAttachmentType, + type PersistableStateAttachmentViewProps, + type AttachmentViewObject, + type AttachmentAction, + AttachmentActionType, +} from '@kbn/cases-plugin/public/client/attachment_framework/types'; +import * as i18n from './translations'; + +const AttachmentChildrenLazy = React.lazy(() => import('./attachment_children')); +const GoToAction = React.lazy(() => import('./go_to_action')); + +const getPageAttachmentViewObject = (): AttachmentViewObject< + PersistableStateAttachmentViewProps +> => { + return { + event: i18n.ADDED_PAGE, + timelineAvatar: 'link', + hideDefaultActions: false, + children: AttachmentChildrenLazy, + getActions: (props: PersistableStateAttachmentViewProps) => + getPageAttachmentActions(props.persistableStateAttachmentState), + }; +}; + +export const getPageAttachmentType = + (): PersistableStateAttachmentType => ({ + id: PAGE_ATTACHMENT_TYPE, + icon: 'link', + displayName: i18n.PAGE_LABEL, + getAttachmentViewObject: getPageAttachmentViewObject, + getAttachmentRemovalObject: () => ({ event: i18n.REMOVED_PAGE }), + }); + +const getPageAttachmentActions = (state: PageAttachmentPersistedState): AttachmentAction[] => [ + { + type: AttachmentActionType.CUSTOM as const, + render: () => getGoToAction({ state }), + isPrimary: true, + }, + { + type: AttachmentActionType.CUSTOM as const, + render: () => getGoToAction({ state }), + isPrimary: false, + }, +]; + +function getGoToAction({ state }: { state: PageAttachmentPersistedState }) { + return ( + }> + + + ); +} diff --git a/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment_children.test.tsx b/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment_children.test.tsx new file mode 100644 index 0000000000000..be456b4ad7666 --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment_children.test.tsx @@ -0,0 +1,30 @@ +/* + * 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 type { PageAttachmentPersistedState } from '@kbn/observability-schema'; +import { render, screen } from '@testing-library/react'; +import { PageAttachmentChildren } from './attachment_children'; + +describe('PageAttachmentChildren', () => { + const mockPersistableState: PageAttachmentPersistedState = { + type: 'mockType', + url: { + actionLabel: 'View in Dashboards', + pathAndQuery: '/test/path?query=1', + label: 'Sample Dashboard', + iconType: 'link', + }, + }; + + it('renders the link with correct label and href', () => { + render(); + + const link = screen.getByRole('link', { name: 'Sample Dashboard' }); + expect(link).toHaveAttribute('href', '/test/path?query=1'); + }); +}); 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 new file mode 100644 index 0000000000000..634aeb093959e --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment_children.tsx @@ -0,0 +1,66 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { EuiLink, EuiCallOut, EuiText, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; +import type { PageAttachmentPersistedState } from '@kbn/observability-schema'; + +interface AttachmentChildrenProps { + persistableStateAttachmentState: PageAttachmentPersistedState; +} + +export function PageAttachmentChildren({ + persistableStateAttachmentState, +}: AttachmentChildrenProps) { + const { url } = persistableStateAttachmentState; + + if (!url) { + return ( + + +

+ {i18n.translate('xpack.observability.caseView.pageAttachment.noUrlProvided', { + defaultMessage: 'This page attachment does not contain a valid URL.', + })} +

+
+
+ ); + } + const href = url.pathAndQuery || ''; + const label = url.label; + + return ( + <> + + {url.iconType && ( + + + + )} + + + {label} + + + + + ); +} + +PageAttachmentChildren.displayName = 'PageAttachmentChildren'; + +// Note: This is for lazy loading +// eslint-disable-next-line import/no-default-export +export default PageAttachmentChildren; diff --git a/x-pack/solutions/observability/plugins/observability/public/attachments/page/go_to_action.test.tsx b/x-pack/solutions/observability/plugins/observability/public/attachments/page/go_to_action.test.tsx new file mode 100644 index 0000000000000..24f681fe39ff6 --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability/public/attachments/page/go_to_action.test.tsx @@ -0,0 +1,57 @@ +/* + * 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, within } from '@testing-library/react'; +import GoToAction from './go_to_action'; + +describe('GoToAction', () => { + it('renders the button with correct props when state is valid', () => { + const state = { + type: 'discoverSession', + url: { + pathAndQuery: '/app/discover#/', + label: 'Discover session', + actionLabel: 'Go to Discover', + iconType: 'discoverApp', + }, + screenContext: null, + snapshot: { + imgData: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA...', + }, + }; + + render(); + const button = screen.getByTestId('cases-go-to-action'); + + expect(button).toBeInTheDocument(); + expect(within(button).getByText('Go to Discover')).toBeInTheDocument(); + expect(button).toHaveAttribute('href', '/app/discover#/'); + expect(button).toHaveAttribute('aria-label', 'Go to Discover'); + expect(button).toHaveAttribute('data-test-subj', 'cases-go-to-action'); + }); + + it('returns null when href or actionLabel is missing', () => { + const state = { + url: { + pathAndQuery: null, + label: 'Discover session', + actionLabel: 'actionLabel', + }, + iconType: 'discoverApp', + type: 'discoverSession', + screenContext: null, + snapshot: { + imgData: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA...', + }, + }; + + // @ts-ignore next line + render(); + expect(screen.queryByText('actionLabel')).not.toBeInTheDocument(); + }); +}); diff --git a/x-pack/solutions/observability/plugins/observability/public/attachments/page/go_to_action.tsx b/x-pack/solutions/observability/plugins/observability/public/attachments/page/go_to_action.tsx new file mode 100644 index 0000000000000..28114ad6fa70c --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability/public/attachments/page/go_to_action.tsx @@ -0,0 +1,39 @@ +/* + * 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 { EuiButtonEmpty, type IconType } from '@elastic/eui'; +import type { PageAttachmentPersistedState } from '@kbn/observability-schema'; + +export interface GoToActionProps { + state: PageAttachmentPersistedState; +} + +function GoToAction({ state }: GoToActionProps) { + const url = state?.url; + const href = url?.pathAndQuery; + + const actionLabel = url?.actionLabel; + const iconType = url?.iconType as IconType; + if (!href || !actionLabel) { + return null; + } + const buttonProps = { + iconType, + 'aria-label': actionLabel, + href, + 'data-test-subj': 'cases-go-to-action', + }; + + return {actionLabel}; +} + +GoToAction.displayName = 'PageAttachmentGoToAction'; + +// Note: This is for lazy loading +// eslint-disable-next-line import/no-default-export +export default GoToAction; diff --git a/x-pack/solutions/observability/plugins/observability/public/attachments/page/translations.tsx b/x-pack/solutions/observability/plugins/observability/public/attachments/page/translations.tsx new file mode 100644 index 0000000000000..af01ec5ccc818 --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability/public/attachments/page/translations.tsx @@ -0,0 +1,18 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const ADDED_PAGE = i18n.translate('xpack.observability.caseView.links.addedLink', { + defaultMessage: 'added a page', +}); +export const REMOVED_PAGE = i18n.translate('xpack.observability.caseView.links.removedLink', { + defaultMessage: 'removed page', +}); +export const PAGE_LABEL = i18n.translate('xpack.observability.caseView.links.pageLabel', { + defaultMessage: 'Page', +}); From ddf18389b63f6057f43df206f2cec5f309e3e73d Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Wed, 25 Jun 2025 11:55:00 -0400 Subject: [PATCH 24/72] remove attachment type from cases --- .../page/attachment.test.tsx | 104 ------------------ .../internal_attachments/page/attachment.tsx | 67 ----------- .../page/attachment_children.test.tsx | 31 ------ .../page/attachment_children.tsx | 64 ----------- .../page/go_to_action.test.tsx | 57 ---------- .../page/go_to_action.tsx | 39 ------- .../page/translations.tsx | 18 --- .../internal_attachments/page/types.ts | 37 ------- .../actions/__mocks__/action_wrapper.tsx | 0 .../actions/action_wrapper.test.tsx | 12 +- .../visualizations/actions/action_wrapper.tsx | 10 +- .../actions/add_to_existing_case.tsx | 0 .../convert_to_absolute_time_range.test.tsx | 0 .../actions/convert_to_absolute_time_range.ts | 0 .../visualizations/actions/index.ts | 0 .../actions/is_compatible.test.ts | 8 +- .../visualizations/actions/is_compatible.ts | 4 +- .../visualizations/actions/mocks.ts | 0 .../actions/open_modal.test.tsx | 8 +- .../visualizations/actions/open_modal.tsx | 4 +- .../visualizations/actions/register.ts | 0 .../visualizations/actions/translations.ts | 0 .../visualizations/actions/types.ts | 4 +- .../visualizations/actions/utils.test.ts | 0 .../visualizations/actions/utils.ts | 6 +- .../visualizations/attachment.test.tsx | 12 +- .../visualizations/attachment.tsx | 6 +- .../visualizations/index.mock.ts | 0 .../visualizations/lens_renderer.test.tsx | 4 +- .../visualizations/lens_renderer.tsx | 2 +- .../visualizations/open_lens_button.test.tsx | 4 +- .../visualizations/open_lens_button.tsx | 2 +- .../visualizations/translations.tsx | 0 .../visualizations/types.ts | 0 .../public/internal_attachments/index.ts | 5 +- 35 files changed, 44 insertions(+), 464 deletions(-) delete mode 100644 x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment.test.tsx delete mode 100644 x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment.tsx delete mode 100644 x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment_children.test.tsx delete mode 100644 x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment_children.tsx delete mode 100644 x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/go_to_action.test.tsx delete mode 100644 x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/go_to_action.tsx delete mode 100644 x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/translations.tsx delete mode 100644 x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/types.ts rename x-pack/platform/plugins/shared/cases/public/components/{internal_attachments => }/visualizations/actions/__mocks__/action_wrapper.tsx (100%) rename x-pack/platform/plugins/shared/cases/public/components/{internal_attachments => }/visualizations/actions/action_wrapper.test.tsx (91%) rename x-pack/platform/plugins/shared/cases/public/components/{internal_attachments => }/visualizations/actions/action_wrapper.tsx (87%) rename x-pack/platform/plugins/shared/cases/public/components/{internal_attachments => }/visualizations/actions/add_to_existing_case.tsx (100%) rename x-pack/platform/plugins/shared/cases/public/components/{internal_attachments => }/visualizations/actions/convert_to_absolute_time_range.test.tsx (100%) rename x-pack/platform/plugins/shared/cases/public/components/{internal_attachments => }/visualizations/actions/convert_to_absolute_time_range.ts (100%) rename x-pack/platform/plugins/shared/cases/public/components/{internal_attachments => }/visualizations/actions/index.ts (100%) rename x-pack/platform/plugins/shared/cases/public/components/{internal_attachments => }/visualizations/actions/is_compatible.test.ts (86%) rename x-pack/platform/plugins/shared/cases/public/components/{internal_attachments => }/visualizations/actions/is_compatible.ts (88%) rename x-pack/platform/plugins/shared/cases/public/components/{internal_attachments => }/visualizations/actions/mocks.ts (100%) rename x-pack/platform/plugins/shared/cases/public/components/{internal_attachments => }/visualizations/actions/open_modal.test.tsx (95%) rename x-pack/platform/plugins/shared/cases/public/components/{internal_attachments => }/visualizations/actions/open_modal.tsx (95%) rename x-pack/platform/plugins/shared/cases/public/components/{internal_attachments => }/visualizations/actions/register.ts (100%) rename x-pack/platform/plugins/shared/cases/public/components/{internal_attachments => }/visualizations/actions/translations.ts (100%) rename x-pack/platform/plugins/shared/cases/public/components/{internal_attachments => }/visualizations/actions/types.ts (84%) rename x-pack/platform/plugins/shared/cases/public/components/{internal_attachments => }/visualizations/actions/utils.test.ts (100%) rename x-pack/platform/plugins/shared/cases/public/components/{internal_attachments => }/visualizations/actions/utils.ts (84%) rename x-pack/platform/plugins/shared/cases/public/components/{internal_attachments => }/visualizations/attachment.test.tsx (91%) rename x-pack/platform/plugins/shared/cases/public/components/{internal_attachments => }/visualizations/attachment.tsx (92%) rename x-pack/platform/plugins/shared/cases/public/components/{internal_attachments => }/visualizations/index.mock.ts (100%) rename x-pack/platform/plugins/shared/cases/public/components/{internal_attachments => }/visualizations/lens_renderer.test.tsx (94%) rename x-pack/platform/plugins/shared/cases/public/components/{internal_attachments => }/visualizations/lens_renderer.tsx (96%) rename x-pack/platform/plugins/shared/cases/public/components/{internal_attachments => }/visualizations/open_lens_button.test.tsx (94%) rename x-pack/platform/plugins/shared/cases/public/components/{internal_attachments => }/visualizations/open_lens_button.tsx (96%) rename x-pack/platform/plugins/shared/cases/public/components/{internal_attachments => }/visualizations/translations.tsx (100%) rename x-pack/platform/plugins/shared/cases/public/components/{internal_attachments => }/visualizations/types.ts (100%) diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment.test.tsx b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment.test.tsx deleted file mode 100644 index a52998f968800..0000000000000 --- a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment.test.tsx +++ /dev/null @@ -1,104 +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 React, { Suspense } from 'react'; -import { screen, waitFor } from '@testing-library/react'; -import { PAGE_ATTACHMENT_TYPE } from '../../../../common/constants/links'; -import type { PersistableStateAttachmentViewProps } from '../../../client/attachment_framework/types'; -import { getPageAttachmentType } from './attachment'; -import { renderWithTestingProviders } from '../../../common/mock'; -import type { PageAttachmentPersistedState } from './types'; - -describe('getPageAttachmentType', () => { - const attachmentViewProps: PersistableStateAttachmentViewProps = { - persistableStateAttachmentTypeId: PAGE_ATTACHMENT_TYPE, - persistableStateAttachmentState: { - pathname: '/test/path', - type: PAGE_ATTACHMENT_TYPE, - url: { - pathAndQuery: '/test/path?query=1', - iconType: 'link', - actionLabel: 'View in Dashboards', - label: 'Test Link', - }, - snapshot: { - imgData: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA...', - }, - renderComponentId: 'PageAttachmentChildren', - screenContext: null, - }, - attachmentId: 'test', - caseData: { title: 'Test Case', id: 'case-id' }, - }; - - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('creates the attachment type correctly', () => { - const linkType = getPageAttachmentType(); - - expect(linkType).toStrictEqual({ - id: PAGE_ATTACHMENT_TYPE, - icon: 'link', - displayName: 'Page', - getAttachmentViewObject: expect.any(Function), - getAttachmentRemovalObject: expect.any(Function), - }); - }); - - describe('getAttachmentViewObject', () => { - it('renders the event correctly', () => { - const linkType = getPageAttachmentType(); - const event = linkType.getAttachmentViewObject(attachmentViewProps).event; - - expect(event).toBe('added a page'); - }); - - it('renders the timelineAvatar correctly', () => { - const linkType = getPageAttachmentType(); - const avatar = linkType.getAttachmentViewObject(attachmentViewProps).timelineAvatar; - - expect(avatar).toBe('link'); - }); - - it('does not hide the default actions', () => { - const linkType = getPageAttachmentType(); - const hideDefaultActions = - linkType.getAttachmentViewObject(attachmentViewProps).hideDefaultActions; - - expect(hideDefaultActions).toBe(false); - }); - - it('renders the children correctly', async () => { - const linkType = getPageAttachmentType(); - const Component = linkType.getAttachmentViewObject( - attachmentViewProps - // eslint-disable-next-line testing-library/no-node-access - ).children!; - - renderWithTestingProviders( - - - - ); - - await waitFor(() => { - expect(screen.getByText('Loading...')).toBeInTheDocument(); - }); - }); - }); - - describe('getAttachmentRemovalObject', () => { - it('renders the removal event correctly', () => { - const linkType = getPageAttachmentType(); - const event = linkType.getAttachmentRemovalObject?.(attachmentViewProps); - - expect(event).toEqual({ event: 'removed page' }); - }); - }); -}); diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment.tsx b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment.tsx deleted file mode 100644 index fc16fc1e7fe6d..0000000000000 --- a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment.tsx +++ /dev/null @@ -1,67 +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 React, { Suspense } from 'react'; -import { EuiLoadingSpinner } from '@elastic/eui'; -import { PAGE_ATTACHMENT_TYPE } from '../../../../common/constants/links'; -import * as i18n from './translations'; - -import { - type PersistableStateAttachmentType, - type PersistableStateAttachmentViewProps, - type AttachmentViewObject, - type AttachmentAction, - AttachmentActionType, -} from '../../../client/attachment_framework/types'; - -import type { PageAttachmentPersistedState } from './types'; - -const AttachmentChildrenLazy = React.lazy(() => import('./attachment_children')); -const GoToAction = React.lazy(() => import('./go_to_action')); - -const getPageAttachmentViewObject = (): AttachmentViewObject< - PersistableStateAttachmentViewProps -> => { - return { - event: i18n.ADDED_PAGE, - timelineAvatar: 'link', - hideDefaultActions: false, - children: AttachmentChildrenLazy, - getActions: (props: PersistableStateAttachmentViewProps) => - getPageAttachmentActions(props.persistableStateAttachmentState), - }; -}; - -export const getPageAttachmentType = - (): PersistableStateAttachmentType => ({ - id: PAGE_ATTACHMENT_TYPE, - icon: 'link', - displayName: i18n.PAGE_LABEL, - getAttachmentViewObject: getPageAttachmentViewObject, - getAttachmentRemovalObject: () => ({ event: i18n.REMOVED_PAGE }), - }); - -const getPageAttachmentActions = (state: PageAttachmentPersistedState): AttachmentAction[] => [ - { - type: AttachmentActionType.CUSTOM as const, - render: () => getGoToAction({ state }), - isPrimary: true, - }, - { - type: AttachmentActionType.CUSTOM as const, - render: () => getGoToAction({ state }), - isPrimary: false, - }, -]; - -function getGoToAction({ state }: { state: PageAttachmentPersistedState }) { - return ( - }> - - - ); -} diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment_children.test.tsx b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment_children.test.tsx deleted file mode 100644 index 69c17ab89ea04..0000000000000 --- a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment_children.test.tsx +++ /dev/null @@ -1,31 +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 React from 'react'; -import { render, screen } from '@testing-library/react'; -import { PageAttachmentChildren } from './attachment_children'; -import type { PageAttachmentPersistedState } from './types'; - -describe('PageAttachmentChildren', () => { - const mockPersistableState: PageAttachmentPersistedState = { - type: 'mockType', - url: { - actionLabel: 'View in Dashboards', - pathAndQuery: '/test/path?query=1', - label: 'Sample Dashboard', - iconType: 'link', - }, - screenContext: null, - }; - - it('renders the link with correct label and href', () => { - render(); - - const link = screen.getByRole('link', { name: 'Sample Dashboard' }); - expect(link).toHaveAttribute('href', '/test/path?query=1'); - }); -}); diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment_children.tsx b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment_children.tsx deleted file mode 100644 index e868e264619b4..0000000000000 --- a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/attachment_children.tsx +++ /dev/null @@ -1,64 +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 React from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiLink, EuiCallOut, EuiText, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; -import type { PageAttachmentPersistedState } from './types'; - -interface AttachmentChildrenProps { - persistableStateAttachmentState: PageAttachmentPersistedState; -} - -export const PageAttachmentChildren: React.FC = ({ - persistableStateAttachmentState, -}) => { - const { url } = persistableStateAttachmentState; - - if (!url) { - return ( - - -

- {i18n.translate('xpack.cases.caseView.pageAttachment.noUrlProvided', { - defaultMessage: 'This page attachment does not contain a valid URL.', - })} -

-
-
- ); - } - const href = url.pathAndQuery || ''; - const label = url.label; - - return ( - <> - - - - - - - {label} - - - - - ); -}; - -PageAttachmentChildren.displayName = 'PageAttachmentChildren'; - -// Note: This is for lazy loading -// eslint-disable-next-line import/no-default-export -export default PageAttachmentChildren; diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/go_to_action.test.tsx b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/go_to_action.test.tsx deleted file mode 100644 index 24f681fe39ff6..0000000000000 --- a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/go_to_action.test.tsx +++ /dev/null @@ -1,57 +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 React from 'react'; -import { render, screen, within } from '@testing-library/react'; -import GoToAction from './go_to_action'; - -describe('GoToAction', () => { - it('renders the button with correct props when state is valid', () => { - const state = { - type: 'discoverSession', - url: { - pathAndQuery: '/app/discover#/', - label: 'Discover session', - actionLabel: 'Go to Discover', - iconType: 'discoverApp', - }, - screenContext: null, - snapshot: { - imgData: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA...', - }, - }; - - render(); - const button = screen.getByTestId('cases-go-to-action'); - - expect(button).toBeInTheDocument(); - expect(within(button).getByText('Go to Discover')).toBeInTheDocument(); - expect(button).toHaveAttribute('href', '/app/discover#/'); - expect(button).toHaveAttribute('aria-label', 'Go to Discover'); - expect(button).toHaveAttribute('data-test-subj', 'cases-go-to-action'); - }); - - it('returns null when href or actionLabel is missing', () => { - const state = { - url: { - pathAndQuery: null, - label: 'Discover session', - actionLabel: 'actionLabel', - }, - iconType: 'discoverApp', - type: 'discoverSession', - screenContext: null, - snapshot: { - imgData: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA...', - }, - }; - - // @ts-ignore next line - render(); - expect(screen.queryByText('actionLabel')).not.toBeInTheDocument(); - }); -}); diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/go_to_action.tsx b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/go_to_action.tsx deleted file mode 100644 index a205ac2d2bcb6..0000000000000 --- a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/go_to_action.tsx +++ /dev/null @@ -1,39 +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 React from 'react'; -import { EuiButtonEmpty, type IconType } from '@elastic/eui'; -import type { PageAttachmentPersistedState } from './types'; - -export interface GoToDiscoverButtonProps { - state: PageAttachmentPersistedState; -} - -const GoToAction: React.FC = ({ state }) => { - const url = state?.url; - const href = url?.pathAndQuery; - - const actionLabel = url?.actionLabel; - const iconType = url?.iconType as IconType; - if (!href || !actionLabel) { - return null; - } - const buttonProps = { - iconType, - 'aria-label': actionLabel, - href, - 'data-test-subj': 'cases-go-to-action', - }; - - return {actionLabel}; -}; - -GoToAction.displayName = 'PageAttachmentGoToAction'; - -// Note: This is for lazy loading -// eslint-disable-next-line import/no-default-export -export default GoToAction; diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/translations.tsx b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/translations.tsx deleted file mode 100644 index 2ccf5c0a462d7..0000000000000 --- a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/translations.tsx +++ /dev/null @@ -1,18 +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 { i18n } from '@kbn/i18n'; - -export const ADDED_PAGE = i18n.translate('xpack.cases.caseView.links.addedLink', { - defaultMessage: 'added a page', -}); -export const REMOVED_PAGE = i18n.translate('xpack.cases.caseView.links.removedLink', { - defaultMessage: 'removed page', -}); -export const PAGE_LABEL = i18n.translate('xpack.cases.caseView.links.pageLabel', { - defaultMessage: 'Page', -}); diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/types.ts b/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/types.ts deleted file mode 100644 index 49b96c25c32dd..0000000000000 --- a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/page/types.ts +++ /dev/null @@ -1,37 +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 type { PersistableStateAttachmentState } from '../../../../common/types/domain'; - -export interface PageAttachmentPersistedState extends PersistableStateAttachmentState { - /** - * The type of page or page asset, e.g., 'dashboard', 'synthetics-test-run', 'slo-history', etc - */ - type: string; - url: { - /** - * The URL to the page or page asset, excluding the base path. - */ - pathAndQuery: string; - /** - * The icon representing the page type, displayed in the comment and action - */ - iconType: string; - /** - * The label to render in the "Go to" action, example "View in Dashboard" for the asset - */ - actionLabel: string; - /** - * The label to render in the comment for the page or page asset - */ - label: string; - }; - /** - * Optional screen context for the page. A plain text description that - * can be provided to an LLM to generate a summary or perform analysis - */ - screenContext: Array<{ screenDescription: string }> | null; -} diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/__mocks__/action_wrapper.tsx b/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/__mocks__/action_wrapper.tsx similarity index 100% rename from x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/__mocks__/action_wrapper.tsx rename to x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/__mocks__/action_wrapper.tsx diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/action_wrapper.test.tsx b/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/action_wrapper.test.tsx similarity index 91% rename from x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/action_wrapper.test.tsx rename to x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/action_wrapper.test.tsx index add585362ca3e..8437da2f28b9c 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/action_wrapper.test.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/action_wrapper.test.tsx @@ -7,19 +7,19 @@ import { render } from '@testing-library/react'; import React from 'react'; -import { SECURITY_SOLUTION_OWNER } from '../../../../../common'; -import { canUseCases } from '../../../../client/helpers/can_use_cases'; -import CasesProvider from '../../../cases_context'; +import { SECURITY_SOLUTION_OWNER } from '../../../../common'; +import { canUseCases } from '../../../client/helpers/can_use_cases'; +import CasesProvider from '../../cases_context'; import { ActionWrapper } from './action_wrapper'; import { getMockServices } from './mocks'; import type { CasesActionContextProps } from './types'; -jest.mock('../../../cases_context', () => +jest.mock('../../cases_context', () => jest.fn().mockImplementation(({ children, ...props }) =>
{children}
) ); -jest.mock('../../../../client/helpers/can_use_cases', () => { - const actual = jest.requireActual('../../../../client/helpers/can_use_cases'); +jest.mock('../../../client/helpers/can_use_cases', () => { + const actual = jest.requireActual('../../../client/helpers/can_use_cases'); return { ...actual, canUseCases: jest.fn(), diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/action_wrapper.tsx b/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/action_wrapper.tsx similarity index 87% rename from x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/action_wrapper.tsx rename to x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/action_wrapper.tsx index b2b0b79cfd2cb..e5f931492dabd 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/action_wrapper.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/action_wrapper.tsx @@ -10,12 +10,12 @@ import React from 'react'; import { Router } from '@kbn/shared-ux-router'; import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; -import { SECURITY_SOLUTION_OWNER } from '../../../../../common'; +import { SECURITY_SOLUTION_OWNER } from '../../../../common'; import type { CasesActionContextProps, Services } from './types'; -import { KibanaContextProvider, useKibana } from '../../../../common/lib/kibana'; -import CasesProvider from '../../../cases_context'; -import { getCaseOwnerByAppId } from '../../../../../common/utils/owner'; -import { canUseCases } from '../../../../client/helpers/can_use_cases'; +import { KibanaContextProvider, useKibana } from '../../../common/lib/kibana'; +import CasesProvider from '../../cases_context'; +import { getCaseOwnerByAppId } from '../../../../common/utils/owner'; +import { canUseCases } from '../../../client/helpers/can_use_cases'; export const DEFAULT_DARK_MODE = 'theme:darkMode' as const; diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/add_to_existing_case.tsx b/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/add_to_existing_case.tsx similarity index 100% rename from x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/add_to_existing_case.tsx rename to x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/add_to_existing_case.tsx diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/convert_to_absolute_time_range.test.tsx b/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/convert_to_absolute_time_range.test.tsx similarity index 100% rename from x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/convert_to_absolute_time_range.test.tsx rename to x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/convert_to_absolute_time_range.test.tsx diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/convert_to_absolute_time_range.ts b/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/convert_to_absolute_time_range.ts similarity index 100% rename from x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/convert_to_absolute_time_range.ts rename to x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/convert_to_absolute_time_range.ts diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/index.ts b/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/index.ts similarity index 100% rename from x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/index.ts rename to x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/index.ts diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/is_compatible.test.ts b/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/is_compatible.test.ts similarity index 86% rename from x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/is_compatible.test.ts rename to x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/is_compatible.test.ts index ded572d3d0491..40280083e3cbb 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/is_compatible.test.ts +++ b/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/is_compatible.test.ts @@ -8,15 +8,15 @@ import { BehaviorSubject } from 'rxjs'; import { coreMock } from '@kbn/core/public/mocks'; import { isCompatible } from './is_compatible'; -import { canUseCases } from '../../../../client/helpers/can_use_cases'; +import { canUseCases } from '../../../client/helpers/can_use_cases'; import { getMockLensApi } from './mocks'; -jest.mock('../../../../../common/utils/owner', () => ({ +jest.mock('../../../../common/utils/owner', () => ({ getCaseOwnerByAppId: () => 'securitySolution', })); -jest.mock('../../../../client/helpers/can_use_cases', () => { - const actual = jest.requireActual('../../../../client/helpers/can_use_cases'); +jest.mock('../../../client/helpers/can_use_cases', () => { + const actual = jest.requireActual('../../../client/helpers/can_use_cases'); return { ...actual, canUseCases: jest.fn(), diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/is_compatible.ts b/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/is_compatible.ts similarity index 88% rename from x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/is_compatible.ts rename to x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/is_compatible.ts index d31bb7390d904..90347cb8d4067 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/is_compatible.ts +++ b/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/is_compatible.ts @@ -8,8 +8,8 @@ import type { CoreStart } from '@kbn/core-lifecycle-browser'; import { isLensApi } from '@kbn/lens-plugin/public'; import { apiPublishesTimeRange, hasBlockingError } from '@kbn/presentation-publishing'; -import { canUseCases } from '../../../../client/helpers/can_use_cases'; -import { getCaseOwnerByAppId } from '../../../../../common/utils/owner'; +import { canUseCases } from '../../../client/helpers/can_use_cases'; +import { getCaseOwnerByAppId } from '../../../../common/utils/owner'; export function isCompatible( embeddable: unknown, diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/mocks.ts b/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/mocks.ts similarity index 100% rename from x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/mocks.ts rename to x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/mocks.ts diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/open_modal.test.tsx b/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/open_modal.test.tsx similarity index 95% rename from x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/open_modal.test.tsx rename to x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/open_modal.test.tsx index de8c24aa06b9a..41cf590c4edb3 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/open_modal.test.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/open_modal.test.tsx @@ -6,7 +6,7 @@ */ import { unmountComponentAtNode } from 'react-dom'; -import { useCasesAddToExistingCaseModal } from '../../../all_cases/selector_modal/use_cases_add_to_existing_case_modal'; +import { useCasesAddToExistingCaseModal } from '../../all_cases/selector_modal/use_cases_add_to_existing_case_modal'; import type { PropsWithChildren } from 'react'; import React from 'react'; import { @@ -16,7 +16,7 @@ import { mockLensAttributes, getMockServices, } from './mocks'; -import { useKibana } from '../../../../common/lib/kibana'; +import { useKibana } from '../../../common/lib/kibana'; import { waitFor } from '@testing-library/react'; import { openModal } from './open_modal'; import type { CasesActionContextProps } from './types'; @@ -24,7 +24,7 @@ import type { CasesActionContextProps } from './types'; const element = document.createElement('div'); document.body.appendChild(element); -jest.mock('../../../all_cases/selector_modal/use_cases_add_to_existing_case_modal', () => ({ +jest.mock('../../all_cases/selector_modal/use_cases_add_to_existing_case_modal', () => ({ useCasesAddToExistingCaseModal: jest.fn(), })); @@ -34,7 +34,7 @@ jest.mock('@kbn/kibana-react-plugin/public', () => ({ .mockImplementation(({ children }: PropsWithChildren) => <>{children}), })); -jest.mock('../../../../common/lib/kibana', () => { +jest.mock('../../../common/lib/kibana', () => { return { useKibana: jest.fn(), KibanaContextProvider: jest diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/open_modal.tsx b/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/open_modal.tsx similarity index 95% rename from x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/open_modal.tsx rename to x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/open_modal.tsx index 13f88047d71d5..1175a4c928006 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/open_modal.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/open_modal.tsx @@ -13,9 +13,9 @@ import { apiPublishesTimeRange, useStateFromPublishingSubject } from '@kbn/prese import { BehaviorSubject } from 'rxjs'; import { ActionWrapper } from './action_wrapper'; import type { CasesActionContextProps, Services } from './types'; -import type { CaseUI } from '../../../../../common'; +import type { CaseUI } from '../../../../common'; import { getLensCaseAttachment } from './utils'; -import { useCasesAddToExistingCaseModal } from '../../../all_cases/selector_modal/use_cases_add_to_existing_case_modal'; +import { useCasesAddToExistingCaseModal } from '../../all_cases/selector_modal/use_cases_add_to_existing_case_modal'; import { convertToAbsoluteTimeRange } from './convert_to_absolute_time_range'; interface Props { diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/register.ts b/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/register.ts similarity index 100% rename from x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/register.ts rename to x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/register.ts diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/translations.ts b/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/translations.ts similarity index 100% rename from x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/translations.ts rename to x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/translations.ts diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/types.ts b/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/types.ts similarity index 84% rename from x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/types.ts rename to x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/types.ts index fba01f3a32e14..b1ccf7bad921c 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/types.ts +++ b/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/types.ts @@ -9,8 +9,8 @@ import type { CoreStart } from '@kbn/core/public'; import type * as H from 'history'; import type { Storage } from '@kbn/kibana-utils-plugin/public'; -import type { CasesPublicStartDependencies } from '../../../../types'; -import type { CasesContextProps } from '../../../cases_context'; +import type { CasesPublicStartDependencies } from '../../../types'; +import type { CasesContextProps } from '../../cases_context'; export type CasesActionContextProps = Pick< CasesContextProps, diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/utils.test.ts b/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/utils.test.ts similarity index 100% rename from x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/utils.test.ts rename to x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/utils.test.ts diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/utils.ts b/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/utils.ts similarity index 84% rename from x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/utils.ts rename to x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/utils.ts index 7f0bab409712b..e51f1fb13bdee 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/actions/utils.ts +++ b/x-pack/platform/plugins/shared/cases/public/components/visualizations/actions/utils.ts @@ -5,9 +5,9 @@ * 2.0. */ import type { LensSavedObjectAttributes } from '@kbn/lens-plugin/public'; -import { LENS_ATTACHMENT_TYPE } from '../../../../../common/constants/visualizations'; -import type { PersistableStateAttachmentPayload } from '../../../../../common/types/domain'; -import { AttachmentType } from '../../../../../common/types/domain'; +import { LENS_ATTACHMENT_TYPE } from '../../../../common/constants/visualizations'; +import type { PersistableStateAttachmentPayload } from '../../../../common/types/domain'; +import { AttachmentType } from '../../../../common/types/domain'; import type { LensProps } from '../types'; type PersistableStateAttachmentWithoutOwner = Omit; diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/attachment.test.tsx b/x-pack/platform/plugins/shared/cases/public/components/visualizations/attachment.test.tsx similarity index 91% rename from x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/attachment.test.tsx rename to x-pack/platform/plugins/shared/cases/public/components/visualizations/attachment.test.tsx index 0b512c918565f..1805b1b88a555 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/attachment.test.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/visualizations/attachment.test.tsx @@ -7,14 +7,14 @@ import React, { Suspense } from 'react'; import { screen, waitFor } from '@testing-library/react'; -import { LENS_ATTACHMENT_TYPE } from '../../../../common'; -import type { PersistableStateAttachmentViewProps } from '../../../client/attachment_framework/types'; -import { AttachmentActionType } from '../../../client/attachment_framework/types'; +import { LENS_ATTACHMENT_TYPE } from '../../../common'; +import type { PersistableStateAttachmentViewProps } from '../../client/attachment_framework/types'; +import { AttachmentActionType } from '../../client/attachment_framework/types'; -import { basicCase } from '../../../containers/mock'; +import { basicCase } from '../../containers/mock'; import { getVisualizationAttachmentType } from './attachment'; -import { createStartServicesMock } from '../../../common/lib/kibana/kibana_react.mock'; -import { renderWithTestingProviders } from '../../../common/mock'; +import { createStartServicesMock } from '../../common/lib/kibana/kibana_react.mock'; +import { renderWithTestingProviders } from '../../common/mock'; describe('getVisualizationAttachmentType', () => { const mockEmbeddableComponent = jest diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/attachment.tsx b/x-pack/platform/plugins/shared/cases/public/components/visualizations/attachment.tsx similarity index 92% rename from x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/attachment.tsx rename to x-pack/platform/plugins/shared/cases/public/components/visualizations/attachment.tsx index c57b5f0ec89ae..013187afc5481 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/attachment.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/visualizations/attachment.tsx @@ -8,14 +8,14 @@ import React from 'react'; import deepEqual from 'fast-deep-equal'; -import { LENS_ATTACHMENT_TYPE } from '../../../../common/constants/visualizations'; +import { LENS_ATTACHMENT_TYPE } from '../../../common/constants/visualizations'; import * as i18n from './translations'; import type { PersistableStateAttachmentType, PersistableStateAttachmentViewProps, -} from '../../../client/attachment_framework/types'; -import { AttachmentActionType } from '../../../client/attachment_framework/types'; +} from '../../client/attachment_framework/types'; +import { AttachmentActionType } from '../../client/attachment_framework/types'; import type { LensProps } from './types'; import { OpenLensButton } from './open_lens_button'; import { LensRenderer } from './lens_renderer'; diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/index.mock.ts b/x-pack/platform/plugins/shared/cases/public/components/visualizations/index.mock.ts similarity index 100% rename from x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/index.mock.ts rename to x-pack/platform/plugins/shared/cases/public/components/visualizations/index.mock.ts diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/lens_renderer.test.tsx b/x-pack/platform/plugins/shared/cases/public/components/visualizations/lens_renderer.test.tsx similarity index 94% rename from x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/lens_renderer.test.tsx rename to x-pack/platform/plugins/shared/cases/public/components/visualizations/lens_renderer.test.tsx index fe115698010af..0a4b6f4f96bb9 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/lens_renderer.test.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/visualizations/lens_renderer.test.tsx @@ -10,8 +10,8 @@ import { screen } from '@testing-library/react'; import { LensRenderer } from './lens_renderer'; import { lensVisualization } from './index.mock'; -import { createStartServicesMock } from '../../../common/lib/kibana/kibana_react.mock'; -import { renderWithTestingProviders } from '../../../common/mock'; +import { createStartServicesMock } from '../../common/lib/kibana/kibana_react.mock'; +import { renderWithTestingProviders } from '../../common/mock'; describe('LensRenderer', () => { const mockEmbeddableComponent = jest diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/lens_renderer.tsx b/x-pack/platform/plugins/shared/cases/public/components/visualizations/lens_renderer.tsx similarity index 96% rename from x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/lens_renderer.tsx rename to x-pack/platform/plugins/shared/cases/public/components/visualizations/lens_renderer.tsx index f805fab8c116a..8596b8f8bd30f 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/lens_renderer.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/visualizations/lens_renderer.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { Global, css } from '@emotion/react'; import { EuiSpacer, useEuiTheme } from '@elastic/eui'; -import { useKibana } from '../../../common/lib/kibana'; +import { useKibana } from '../../common/lib/kibana'; import type { LensProps } from './types'; const LENS_VISUALIZATION_HEIGHT = 200; diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/open_lens_button.test.tsx b/x-pack/platform/plugins/shared/cases/public/components/visualizations/open_lens_button.test.tsx similarity index 94% rename from x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/open_lens_button.test.tsx rename to x-pack/platform/plugins/shared/cases/public/components/visualizations/open_lens_button.test.tsx index 442f0f2e0a926..8f8a2063ec8a1 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/open_lens_button.test.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/visualizations/open_lens_button.test.tsx @@ -12,8 +12,8 @@ import { screen } from '@testing-library/react'; import { OpenLensButton } from './open_lens_button'; import { lensVisualization } from './index.mock'; import userEvent from '@testing-library/user-event'; -import { createStartServicesMock } from '../../../common/lib/kibana/kibana_react.mock'; -import { renderWithTestingProviders } from '../../../common/mock'; +import { createStartServicesMock } from '../../common/lib/kibana/kibana_react.mock'; +import { renderWithTestingProviders } from '../../common/mock'; describe('OpenLensButton', () => { const props = { diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/open_lens_button.tsx b/x-pack/platform/plugins/shared/cases/public/components/visualizations/open_lens_button.tsx similarity index 96% rename from x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/open_lens_button.tsx rename to x-pack/platform/plugins/shared/cases/public/components/visualizations/open_lens_button.tsx index aa164ab4360fa..c6a49bb285a2f 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/open_lens_button.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/visualizations/open_lens_button.tsx @@ -8,7 +8,7 @@ import { isOfAggregateQueryType } from '@kbn/es-query'; import { EuiButtonEmpty } from '@elastic/eui'; import React, { useCallback } from 'react'; -import { useKibana } from '../../../common/lib/kibana'; +import { useKibana } from '../../common/lib/kibana'; import { OPEN_IN_VISUALIZATION } from './translations'; import type { LensProps } from './types'; diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/translations.tsx b/x-pack/platform/plugins/shared/cases/public/components/visualizations/translations.tsx similarity index 100% rename from x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/translations.tsx rename to x-pack/platform/plugins/shared/cases/public/components/visualizations/translations.tsx diff --git a/x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/types.ts b/x-pack/platform/plugins/shared/cases/public/components/visualizations/types.ts similarity index 100% rename from x-pack/platform/plugins/shared/cases/public/components/internal_attachments/visualizations/types.ts rename to x-pack/platform/plugins/shared/cases/public/components/visualizations/types.ts diff --git a/x-pack/platform/plugins/shared/cases/public/internal_attachments/index.ts b/x-pack/platform/plugins/shared/cases/public/internal_attachments/index.ts index 5c6eb53fde2ea..6d177fe5d7d84 100644 --- a/x-pack/platform/plugins/shared/cases/public/internal_attachments/index.ts +++ b/x-pack/platform/plugins/shared/cases/public/internal_attachments/index.ts @@ -7,10 +7,8 @@ import type { ExternalReferenceAttachmentTypeRegistry } from '../client/attachment_framework/external_reference_registry'; import type { PersistableStateAttachmentTypeRegistry } from '../client/attachment_framework/persistable_state_registry'; -import type { PersistableStateAttachmentType } from '../client/attachment_framework/types'; import { getFileType } from '../components/files/file_type'; -import { getVisualizationAttachmentType } from '../components/internal_attachments/visualizations/attachment'; -import { getPageAttachmentType } from '../components/internal_attachments/page/attachment'; +import { getVisualizationAttachmentType } from '../components/visualizations/attachment'; export const registerInternalAttachments = ( externalRefRegistry: ExternalReferenceAttachmentTypeRegistry, @@ -18,5 +16,4 @@ export const registerInternalAttachments = ( ) => { externalRefRegistry.register(getFileType()); persistableStateRegistry.register(getVisualizationAttachmentType()); - persistableStateRegistry.register(getPageAttachmentType() as PersistableStateAttachmentType); }; From 83bfd872dc849bd8369dc129d80170863f39c576 Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Wed, 25 Jun 2025 11:55:24 -0400 Subject: [PATCH 25/72] adjust license --- .../page_attachment/schema/latest.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/x-pack/solutions/observability/packages/kbn-observability-schema/page_attachment/schema/latest.ts b/x-pack/solutions/observability/packages/kbn-observability-schema/page_attachment/schema/latest.ts index 4b663d44837ef..79317a159bd28 100644 --- a/x-pack/solutions/observability/packages/kbn-observability-schema/page_attachment/schema/latest.ts +++ b/x-pack/solutions/observability/packages/kbn-observability-schema/page_attachment/schema/latest.ts @@ -1 +1,8 @@ +/* + * 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. + */ + export { type PageAttachmentPersistedState, pageAttachmentPersistedStateSchema } from './v1'; From 60067a9cd4ac298c23b3daee26749124e18fc3c5 Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Wed, 25 Jun 2025 12:14:35 -0400 Subject: [PATCH 26/72] remove additional references in cases --- .../plugins/shared/cases/common/constants/index.ts | 1 - .../plugins/shared/cases/common/constants/links.ts | 8 -------- x-pack/platform/plugins/shared/cases/common/index.ts | 1 - .../components/markdown_editor/plugins/lens/index.ts | 2 +- .../components/markdown_editor/plugins/lens/plugin.tsx | 2 +- .../plugins/lens/use_lens_open_visualization.tsx | 2 +- x-pack/platform/plugins/shared/cases/public/index.tsx | 1 - x-pack/platform/plugins/shared/cases/public/plugin.ts | 2 +- x-pack/platform/plugins/shared/cases/public/types.ts | 2 +- .../shared/cases/server/internal_attachments/index.ts | 9 +-------- 10 files changed, 6 insertions(+), 24 deletions(-) delete mode 100644 x-pack/platform/plugins/shared/cases/common/constants/links.ts diff --git a/x-pack/platform/plugins/shared/cases/common/constants/index.ts b/x-pack/platform/plugins/shared/cases/common/constants/index.ts index 1ee74467e271b..475b32644d449 100644 --- a/x-pack/platform/plugins/shared/cases/common/constants/index.ts +++ b/x-pack/platform/plugins/shared/cases/common/constants/index.ts @@ -11,7 +11,6 @@ export * from './owners'; export * from './files'; export * from './application'; export { LENS_ATTACHMENT_TYPE } from './visualizations'; -export { PAGE_ATTACHMENT_TYPE } from './links'; export const DEFAULT_DATE_FORMAT = 'dateFormat' as const; export const DEFAULT_DATE_FORMAT_TZ = 'dateFormat:tz' as const; diff --git a/x-pack/platform/plugins/shared/cases/common/constants/links.ts b/x-pack/platform/plugins/shared/cases/common/constants/links.ts deleted file mode 100644 index 6a14dc5355d9e..0000000000000 --- a/x-pack/platform/plugins/shared/cases/common/constants/links.ts +++ /dev/null @@ -1,8 +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. - */ - -export const PAGE_ATTACHMENT_TYPE = '.page'; diff --git a/x-pack/platform/plugins/shared/cases/common/index.ts b/x-pack/platform/plugins/shared/cases/common/index.ts index c5641df3e134e..37f0115f027fe 100644 --- a/x-pack/platform/plugins/shared/cases/common/index.ts +++ b/x-pack/platform/plugins/shared/cases/common/index.ts @@ -50,7 +50,6 @@ export { UPDATE_CASES_CAPABILITY, INTERNAL_BULK_GET_CASES_URL, LENS_ATTACHMENT_TYPE, - PAGE_ATTACHMENT_TYPE, INTERNAL_BULK_CREATE_ATTACHMENTS_URL, SAVED_OBJECT_TYPES, CASE_COMMENT_SAVED_OBJECT, diff --git a/x-pack/platform/plugins/shared/cases/public/components/markdown_editor/plugins/lens/index.ts b/x-pack/platform/plugins/shared/cases/public/components/markdown_editor/plugins/lens/index.ts index c150572f9a336..38f81fb6d525f 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/markdown_editor/plugins/lens/index.ts +++ b/x-pack/platform/plugins/shared/cases/public/components/markdown_editor/plugins/lens/index.ts @@ -8,6 +8,6 @@ import { plugin } from './plugin'; import { LensParser } from './parser'; import { VISUALIZATION } from './translations'; -import { LensRenderer } from '../../../internal_attachments/visualizations/lens_renderer'; +import { LensRenderer } from '../../../visualizations/lens_renderer'; export { plugin, LensParser as parser, LensRenderer as renderer, VISUALIZATION }; diff --git a/x-pack/platform/plugins/shared/cases/public/components/markdown_editor/plugins/lens/plugin.tsx b/x-pack/platform/plugins/shared/cases/public/components/markdown_editor/plugins/lens/plugin.tsx index 87e98d4ae9d58..59c652b07bfd0 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/markdown_editor/plugins/lens/plugin.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/markdown_editor/plugins/lens/plugin.tsx @@ -37,7 +37,7 @@ import { CommentEditorContext } from '../../context'; import { useLensDraftComment } from './use_lens_draft_comment'; import { VISUALIZATION } from './translations'; import { useIsMainApplication } from '../../../../common/hooks'; -import { convertToAbsoluteTimeRange } from '../../../internal_attachments/visualizations/actions/convert_to_absolute_time_range'; +import { convertToAbsoluteTimeRange } from '../../../visualizations/actions/convert_to_absolute_time_range'; const DEFAULT_TIMERANGE: TimeRange = { from: 'now-7d', diff --git a/x-pack/platform/plugins/shared/cases/public/components/markdown_editor/plugins/lens/use_lens_open_visualization.tsx b/x-pack/platform/plugins/shared/cases/public/components/markdown_editor/plugins/lens/use_lens_open_visualization.tsx index 519ca4800f5de..d4f6ddee3a657 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/markdown_editor/plugins/lens/use_lens_open_visualization.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/markdown_editor/plugins/lens/use_lens_open_visualization.tsx @@ -15,7 +15,7 @@ import { parseCommentString, getLensVisualizations, } from '../../../../../common/utils/markdown_plugins/utils'; -import { OPEN_IN_VISUALIZATION } from '../../../internal_attachments/visualizations/translations'; +import { OPEN_IN_VISUALIZATION } from '../../../visualizations/translations'; export const useLensOpenVisualization = ({ comment }: { comment: string }) => { const parsedComment = parseCommentString(comment); diff --git a/x-pack/platform/plugins/shared/cases/public/index.tsx b/x-pack/platform/plugins/shared/cases/public/index.tsx index 7ec924f26cfab..af6e71580f944 100644 --- a/x-pack/platform/plugins/shared/cases/public/index.tsx +++ b/x-pack/platform/plugins/shared/cases/public/index.tsx @@ -18,7 +18,6 @@ export type { CasesPublicStart, CasesPublicSetup } from './types'; export type { GetCreateCaseFlyoutProps } from './client/ui/get_create_case_flyout'; export type { GetAllCasesSelectorModalProps } from './client/ui/get_all_cases_selector_modal'; export type { GetRecentCasesProps } from './client/ui/get_recent_cases'; -export type { PageAttachmentPersistedState } from './components/internal_attachments/page/types'; export type { CaseAttachments, CaseAttachmentsWithoutOwner } from './types'; diff --git a/x-pack/platform/plugins/shared/cases/public/plugin.ts b/x-pack/platform/plugins/shared/cases/public/plugin.ts index 72f88a3cec420..67295a29437db 100644 --- a/x-pack/platform/plugins/shared/cases/public/plugin.ts +++ b/x-pack/platform/plugins/shared/cases/public/plugin.ts @@ -30,7 +30,7 @@ import { ExternalReferenceAttachmentTypeRegistry } from './client/attachment_fra import { PersistableStateAttachmentTypeRegistry } from './client/attachment_framework/persistable_state_registry'; import { registerCaseFileKinds } from './files'; import { registerInternalAttachments } from './internal_attachments'; -import { registerActions } from './components/internal_attachments/visualizations/actions'; +import { registerActions } from './components/visualizations/actions'; import type { CasesPublicSetup, CasesPublicStart, diff --git a/x-pack/platform/plugins/shared/cases/public/types.ts b/x-pack/platform/plugins/shared/cases/public/types.ts index 9c2f27859f98d..08e5787681a9e 100644 --- a/x-pack/platform/plugins/shared/cases/public/types.ts +++ b/x-pack/platform/plugins/shared/cases/public/types.ts @@ -185,6 +185,6 @@ export type SupportedCaseAttachment = export type CaseAttachments = SupportedCaseAttachment[]; export type CaseAttachmentWithoutOwner = DistributiveOmit; export type CaseAttachmentsWithoutOwner = CaseAttachmentWithoutOwner[]; -export type { LensProps } from './components/internal_attachments/visualizations/types'; +export type { LensProps } from './components/visualizations/types'; export type ServerError = IHttpFetchError; diff --git a/x-pack/platform/plugins/shared/cases/server/internal_attachments/index.ts b/x-pack/platform/plugins/shared/cases/server/internal_attachments/index.ts index aa25c0dec8f88..35b06455cd3dc 100644 --- a/x-pack/platform/plugins/shared/cases/server/internal_attachments/index.ts +++ b/x-pack/platform/plugins/shared/cases/server/internal_attachments/index.ts @@ -7,11 +7,7 @@ import { badRequest } from '@hapi/boom'; import { FileAttachmentMetadataRt } from '../../common/types/domain'; -import { - FILE_ATTACHMENT_TYPE, - PAGE_ATTACHMENT_TYPE, - LENS_ATTACHMENT_TYPE, -} from '../../common/constants'; +import { FILE_ATTACHMENT_TYPE, LENS_ATTACHMENT_TYPE } from '../../common/constants'; import { decodeWithExcessOrThrow } from '../common/runtime_types'; import type { ExternalReferenceAttachmentTypeRegistry } from '../attachment_framework/external_reference_registry'; @@ -23,9 +19,6 @@ export const registerInternalAttachments = ( ) => { externalRefRegistry.register({ id: FILE_ATTACHMENT_TYPE, schemaValidator }); persistableStateRegistry.register({ id: LENS_ATTACHMENT_TYPE }); - persistableStateRegistry.register({ - id: PAGE_ATTACHMENT_TYPE, - }); }; const schemaValidator = (data: unknown): void => { From ca0ddcbdfed5077e2c30e7522282d15cce7162dc Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Wed, 25 Jun 2025 13:05:59 -0400 Subject: [PATCH 27/72] adjust test --- .../public/attachments/page/go_to_action.test.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/x-pack/solutions/observability/plugins/observability/public/attachments/page/go_to_action.test.tsx b/x-pack/solutions/observability/plugins/observability/public/attachments/page/go_to_action.test.tsx index 24f681fe39ff6..4bbde8c233d6a 100644 --- a/x-pack/solutions/observability/plugins/observability/public/attachments/page/go_to_action.test.tsx +++ b/x-pack/solutions/observability/plugins/observability/public/attachments/page/go_to_action.test.tsx @@ -19,10 +19,6 @@ describe('GoToAction', () => { actionLabel: 'Go to Discover', iconType: 'discoverApp', }, - screenContext: null, - snapshot: { - imgData: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA...', - }, }; render(); From 905c6683351bda05f160f8bbb932f7067234e67c Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Wed, 25 Jun 2025 14:10:30 -0400 Subject: [PATCH 28/72] adjust tests --- .../attachments_framework/registered_persistable_state_basic.ts | 1 - .../attachments_framework/registered_persistable_state_trial.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/basic/attachments_framework/registered_persistable_state_basic.ts b/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/basic/attachments_framework/registered_persistable_state_basic.ts index b92020ae4cb5d..c636fadc2578e 100644 --- a/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/basic/attachments_framework/registered_persistable_state_basic.ts +++ b/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/basic/attachments_framework/registered_persistable_state_basic.ts @@ -34,7 +34,6 @@ export default ({ getService }: FtrProviderContext): void => { expect(types).to.eql({ '.lens': '78559fd806809ac3a1008942ead2a079864054f5', - '.page': '8e5143e79705c8cebe2d5d9949e22d52944629d0', '.test': 'ab2204830c67f5cf992c9aa2f7e3ead752cc60a1', }); }); diff --git a/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/trial/attachments_framework/registered_persistable_state_trial.ts b/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/trial/attachments_framework/registered_persistable_state_trial.ts index a618829690831..cdab3aceda608 100644 --- a/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/trial/attachments_framework/registered_persistable_state_trial.ts +++ b/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/trial/attachments_framework/registered_persistable_state_trial.ts @@ -34,7 +34,6 @@ export default ({ getService }: FtrProviderContext): void => { expect(types).to.eql({ '.lens': '78559fd806809ac3a1008942ead2a079864054f5', - '.page': '8e5143e79705c8cebe2d5d9949e22d52944629d0', '.test': 'ab2204830c67f5cf992c9aa2f7e3ead752cc60a1', aiopsChangePointChart: 'a1212d71947ec34487b374cecc47ab9941b5d91c', ml_anomaly_charts: '23e92e824af9db6e8b8bb1d63c222e04f57d2147', From 3a6f626dd7c2d00fa411b03cb26f9789287ce00b Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Thu, 26 Jun 2025 09:15:44 -0400 Subject: [PATCH 29/72] add "add to case" button to Synthetics monitor details --- .../plugins/shared/cases/public/index.tsx | 2 + .../attachments/page/attachment.test.tsx | 94 +++++++++ .../public/attachments/page/attachment.tsx | 40 ++-- .../attachments/page/attachment_children.tsx | 6 +- .../plugins/observability/public/plugin.ts | 5 + .../plugins/observability/server/plugin.ts | 7 + .../apps/locators/monitor_detail.test.ts | 78 ++++++++ .../public/apps/locators/monitor_detail.ts | 14 +- .../common/components/refresh_button.tsx | 15 +- .../components/monitor_details/actions.tsx | 51 +++++ .../add_to_case_action.test.tsx | 186 ++++++++++++++++++ .../monitor_details/add_to_case_action.tsx | 158 +++++++++++++++ .../monitor_summary/edit_monitor_link.tsx | 28 ++- .../monitor_details/route_config.tsx | 12 +- .../monitor_details/run_test_manually.tsx | 56 +++++- .../public/apps/synthetics/hooks/index.ts | 1 + .../hooks/use_monitor_detail_locator.test.ts | 90 +++++++++ .../hooks/use_monitor_detail_locator.ts | 23 ++- 18 files changed, 823 insertions(+), 43 deletions(-) create mode 100644 x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment.test.tsx create mode 100644 x-pack/solutions/observability/plugins/synthetics/public/apps/locators/monitor_detail.test.ts create mode 100644 x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/actions.tsx create mode 100644 x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/add_to_case_action.test.tsx create mode 100644 x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/add_to_case_action.tsx create mode 100644 x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/hooks/use_monitor_detail_locator.test.ts diff --git a/x-pack/platform/plugins/shared/cases/public/index.tsx b/x-pack/platform/plugins/shared/cases/public/index.tsx index af6e71580f944..b7f953dbfdef0 100644 --- a/x-pack/platform/plugins/shared/cases/public/index.tsx +++ b/x-pack/platform/plugins/shared/cases/public/index.tsx @@ -23,3 +23,5 @@ export type { CaseAttachments, CaseAttachmentsWithoutOwner } from './types'; export type { ICasesDeepLinkId } from './common/navigation'; export { getCasesDeepLinks, CasesDeepLinkId, generateCaseViewPath } from './common/navigation'; + +export { AttachmentActionType } from './client/attachment_framework/types'; diff --git a/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment.test.tsx b/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment.test.tsx new file mode 100644 index 0000000000000..56feabf4bb1fe --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment.test.tsx @@ -0,0 +1,94 @@ +/* + * 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, { Suspense } from 'react'; +import { screen, waitFor, render } from '@testing-library/react'; +import { type PageAttachmentPersistedState, PAGE_ATTACHMENT_TYPE } from '@kbn/observability-schema'; +import type { PersistableStateAttachmentViewProps } from '@kbn/cases-plugin/public/client/attachment_framework/types'; +import { getPageAttachmentType } from './attachment'; + +describe('getPageAttachmentType', () => { + const state: PageAttachmentPersistedState = { + type: 'exampleType', + url: { + pathAndQuery: '/test/path?query=1', + iconType: 'link', + actionLabel: 'View in Dashboards', + label: 'Test Link', + }, + }; + const attachmentViewProps: PersistableStateAttachmentViewProps = { + persistableStateAttachmentTypeId: PAGE_ATTACHMENT_TYPE, + persistableStateAttachmentState: state, + attachmentId: 'test', + caseData: { title: 'Test Case', id: 'case-id' }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('creates the attachment type correctly', () => { + const linkType = getPageAttachmentType(); + + expect(linkType).toStrictEqual({ + id: PAGE_ATTACHMENT_TYPE, + icon: 'link', + displayName: 'Page', + getAttachmentViewObject: expect.any(Function), + getAttachmentRemovalObject: expect.any(Function), + }); + }); + + describe('getAttachmentViewObject', () => { + it('renders the event correctly', () => { + const linkType = getPageAttachmentType(); + const event = linkType.getAttachmentViewObject(attachmentViewProps).event; + + expect(event).toBe('added a page'); + }); + + it('renders the timelineAvatar correctly', () => { + const linkType = getPageAttachmentType(); + const avatar = linkType.getAttachmentViewObject(attachmentViewProps).timelineAvatar; + + expect(avatar).toBe('link'); + }); + + it('does not hide the default actions', () => { + const linkType = getPageAttachmentType(); + const hideDefaultActions = + linkType.getAttachmentViewObject(attachmentViewProps).hideDefaultActions; + + expect(hideDefaultActions).toBe(false); + }); + + it('renders the children correctly', async () => { + const linkType = getPageAttachmentType(); + const Component = linkType.getAttachmentViewObject(attachmentViewProps).children!; + + render( + + + + ); + + await waitFor(() => { + expect(screen.getByText('Loading...')).toBeInTheDocument(); + }); + }); + }); + + describe('getAttachmentRemovalObject', () => { + it('renders the removal event correctly', () => { + const linkType = getPageAttachmentType(); + const event = linkType.getAttachmentRemovalObject?.(attachmentViewProps); + + expect(event).toEqual({ event: 'removed page' }); + }); + }); +}); diff --git a/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment.tsx b/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment.tsx index 24e11a1da7810..d45dd08fb6e8a 100644 --- a/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment.tsx +++ b/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment.tsx @@ -13,34 +13,34 @@ import { type PersistableStateAttachmentViewProps, type AttachmentViewObject, type AttachmentAction, - AttachmentActionType, } from '@kbn/cases-plugin/public/client/attachment_framework/types'; +import { AttachmentActionType } from '@kbn/cases-plugin/public'; import * as i18n from './translations'; const AttachmentChildrenLazy = React.lazy(() => import('./attachment_children')); const GoToAction = React.lazy(() => import('./go_to_action')); -const getPageAttachmentViewObject = (): AttachmentViewObject< - PersistableStateAttachmentViewProps -> => { - return { - event: i18n.ADDED_PAGE, - timelineAvatar: 'link', - hideDefaultActions: false, - children: AttachmentChildrenLazy, - getActions: (props: PersistableStateAttachmentViewProps) => - getPageAttachmentActions(props.persistableStateAttachmentState), +const getPageAttachmentViewObject = + (): AttachmentViewObject => { + return { + event: i18n.ADDED_PAGE, + timelineAvatar: 'link', + hideDefaultActions: false, + children: AttachmentChildrenLazy, + getActions: (props: PersistableStateAttachmentViewProps) => + getPageAttachmentActions( + props.persistableStateAttachmentState as PageAttachmentPersistedState + ), + }; }; -}; -export const getPageAttachmentType = - (): PersistableStateAttachmentType => ({ - id: PAGE_ATTACHMENT_TYPE, - icon: 'link', - displayName: i18n.PAGE_LABEL, - getAttachmentViewObject: getPageAttachmentViewObject, - getAttachmentRemovalObject: () => ({ event: i18n.REMOVED_PAGE }), - }); +export const getPageAttachmentType = (): PersistableStateAttachmentType => ({ + id: PAGE_ATTACHMENT_TYPE, + icon: 'link', + displayName: i18n.PAGE_LABEL, + getAttachmentViewObject: getPageAttachmentViewObject, + getAttachmentRemovalObject: () => ({ event: i18n.REMOVED_PAGE }), +}); const getPageAttachmentActions = (state: PageAttachmentPersistedState): AttachmentAction[] => [ { 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 634aeb093959e..602e2d939f3d6 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 @@ -8,16 +8,18 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiLink, EuiCallOut, EuiText, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; +import { type PersistableStateAttachmentViewProps } from '@kbn/cases-plugin/public/client/attachment_framework/types'; import type { PageAttachmentPersistedState } from '@kbn/observability-schema'; interface AttachmentChildrenProps { - persistableStateAttachmentState: PageAttachmentPersistedState; + persistableStateAttachmentState: PersistableStateAttachmentViewProps['persistableStateAttachmentState']; } export function PageAttachmentChildren({ persistableStateAttachmentState, }: AttachmentChildrenProps) { - const { url } = persistableStateAttachmentState; + const pageState = persistableStateAttachmentState as PageAttachmentPersistedState; + const { url } = pageState; if (!url) { return ( diff --git a/x-pack/solutions/observability/plugins/observability/public/plugin.ts b/x-pack/solutions/observability/plugins/observability/public/plugin.ts index 5ef2bc717498f..171de2de7504f 100644 --- a/x-pack/solutions/observability/plugins/observability/public/plugin.ts +++ b/x-pack/solutions/observability/plugins/observability/public/plugin.ts @@ -11,6 +11,7 @@ import type { ChartsPluginStart } from '@kbn/charts-plugin/public'; import type { CloudStart } from '@kbn/cloud-plugin/public'; import type { ContentManagementPublicStart } from '@kbn/content-management-plugin/public'; import type { IUiSettingsClient } from '@kbn/core/public'; +import type { CasesPublicSetup } from '@kbn/cases-plugin/public'; import { App, AppDeepLink, @@ -94,6 +95,7 @@ import { CaseDetailsLocatorDefinition, CasesOverviewLocatorDefinition, } from '../common/locators/cases'; +import { getPageAttachmentType } from './attachments/page/attachment'; export interface ConfigSchema { unsafe: { @@ -118,6 +120,7 @@ export interface ConfigSchema { } export type ObservabilityPublicSetup = ReturnType; export interface ObservabilityPublicPluginsSetup { + cases: CasesPublicSetup; data: DataPublicPluginSetup; fieldFormats: FieldFormatsSetup; observabilityShared: ObservabilitySharedPluginSetup; @@ -254,6 +257,8 @@ export class Plugin const logsLocator = pluginsSetup.share.url.locators.get(DISCOVER_APP_LOCATOR); + pluginsSetup.cases.attachmentFramework.registerPersistableState(getPageAttachmentType()); + const mount = async (params: AppMountParameters) => { // Load application bundle const { renderApp } = await import('./application'); diff --git a/x-pack/solutions/observability/plugins/observability/server/plugin.ts b/x-pack/solutions/observability/plugins/observability/server/plugin.ts index aea54082d5015..ec5534c347cff 100644 --- a/x-pack/solutions/observability/plugins/observability/server/plugin.ts +++ b/x-pack/solutions/observability/plugins/observability/server/plugin.ts @@ -6,6 +6,7 @@ */ import type { AlertingServerSetup, AlertingServerStart } from '@kbn/alerting-plugin/server'; +import type { CasesServerSetup } from '@kbn/cases-plugin/server'; import type { ContentManagementServerSetup } from '@kbn/content-management-plugin/server'; import type { DashboardPluginStart } from '@kbn/dashboard-plugin/server'; import { @@ -26,6 +27,7 @@ import { SpacesPluginSetup, SpacesPluginStart } from '@kbn/spaces-plugin/server' import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; import { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server'; import { PluginSetup as ESQLSetup } from '@kbn/esql/server'; +import { PAGE_ATTACHMENT_TYPE } from '@kbn/observability-schema'; import { ObservabilityConfig } from '.'; import { observabilityFeatureId } from '../common'; import { @@ -53,6 +55,7 @@ export type ObservabilityPluginSetup = ReturnType; interface PluginSetup { alerting: AlertingServerSetup; + cases: CasesServerSetup; features: FeaturesPluginSetup; guidedOnboarding?: GuidedOnboardingPluginSetup; ruleRegistry: RuleRegistryPluginSetupContract; @@ -98,6 +101,10 @@ export class ObservabilityPlugin plugins.features.registerKibanaFeature(getCasesFeatureV2(casesCapabilities, casesApiTags)); plugins.features.registerKibanaFeature(getCasesFeatureV3(casesCapabilities, casesApiTags)); + plugins.cases.attachmentFramework.registerPersistableState({ + id: PAGE_ATTACHMENT_TYPE, + }); + let annotationsApiPromise: Promise | undefined; core.uiSettings.register(uiSettings); diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/locators/monitor_detail.test.ts b/x-pack/solutions/observability/plugins/synthetics/public/apps/locators/monitor_detail.test.ts new file mode 100644 index 0000000000000..bdbb4a8244d49 --- /dev/null +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/locators/monitor_detail.test.ts @@ -0,0 +1,78 @@ +/* + * 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 { monitorDetailNavigatorParams } from './monitor_detail'; + +describe('monitorDetailNavigatorParams', () => { + const { getLocation } = monitorDetailNavigatorParams; + + it('generates the correct path with only configId', async () => { + const result = await getLocation({ configId: 'test-config-id' }); + expect(result).toEqual({ + app: 'synthetics', + path: '/monitor/test-config-id', + state: {}, + }); + }); + + it('includes locationId in the query parameters', async () => { + const result = await getLocation({ + configId: 'test-config-id', + locationId: 'test-location-id', + }); + expect(result).toEqual({ + app: 'synthetics', + path: '/monitor/test-config-id?locationId=test-location-id', + state: {}, + }); + }); + + it('includes spaceId in the query parameters', async () => { + const result = await getLocation({ configId: 'test-config-id', spaceId: 'test-space-id' }); + expect(result).toEqual({ + app: 'synthetics', + path: '/monitor/test-config-id?spaceId=test-space-id', + state: {}, + }); + }); + + it('includes timeRange in the query parameters', async () => { + const result = await getLocation({ + configId: 'test-config-id', + timeRange: { from: 'now-15m', to: 'now' }, + }); + expect(result).toEqual({ + app: 'synthetics', + path: '/monitor/test-config-id?dateRangeStart=now-15m&dateRangeEnd=now', + state: {}, + }); + }); + + it('includes tabId in the path', async () => { + const result = await getLocation({ configId: 'test-config-id', tabId: 'test-tab-id' }); + expect(result).toEqual({ + app: 'synthetics', + path: '/monitor/test-config-id/test-tab-id', + state: {}, + }); + }); + + it('combines all parameters correctly', async () => { + const result = await getLocation({ + configId: 'test-config-id', + locationId: 'test-location-id', + spaceId: 'test-space-id', + timeRange: { from: 'now-15m', to: 'now' }, + tabId: 'test-tab-id', + }); + expect(result).toEqual({ + app: 'synthetics', + path: '/monitor/test-config-id/test-tab-id?locationId=test-location-id&spaceId=test-space-id&dateRangeStart=now-15m&dateRangeEnd=now', + state: {}, + }); + }); +}); diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/locators/monitor_detail.ts b/x-pack/solutions/observability/plugins/synthetics/public/apps/locators/monitor_detail.ts index d79cce62fedf9..a5749f09e9b65 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/locators/monitor_detail.ts +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/locators/monitor_detail.ts @@ -6,25 +6,37 @@ */ import { syntheticsMonitorDetailLocatorID } from '@kbn/observability-plugin/common'; +import { TimeRange } from '@kbn/es-query'; async function navigate({ configId, locationId, spaceId, + timeRange, + tabId, }: { configId: string; locationId?: string; spaceId?: string; + timeRange?: TimeRange; + tabId?: string; }) { let queryParam = locationId ? `?locationId=${locationId}` : ''; + const tab = `${tabId ? `/${tabId}` : ''}`; if (spaceId) { queryParam += queryParam ? `&spaceId=${spaceId}` : `?spaceId=${spaceId}`; } + if (timeRange) { + queryParam += queryParam + ? `&dateRangeStart=${timeRange.from}&dateRangeEnd=${timeRange.to}` + : `?dateRangeStart=${timeRange.from}&dateRangeEnd=${timeRange.to}`; + } + return { app: 'synthetics', - path: `/monitor/${configId}${queryParam}`, + path: `/monitor/${configId}${tab}${queryParam}`, state: {}, }; } diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/common/components/refresh_button.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/common/components/refresh_button.tsx index aa9c1f57328f3..45a4fdcdb4e49 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/common/components/refresh_button.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/common/components/refresh_button.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiButton } from '@elastic/eui'; +import { EuiButton, EuiContextMenuItem } from '@elastic/eui'; import { useSyntheticsRefreshContext } from '../../../contexts'; export function RefreshButton() { @@ -23,6 +23,19 @@ export function RefreshButton() { ); } +export function RefreshContextItem() { + const { refreshApp } = useSyntheticsRefreshContext(); + return ( + refreshApp()} + > + {REFRESH_LABEL} + + ); +} + export const REFRESH_LABEL = i18n.translate('xpack.synthetics.overview.refresh', { defaultMessage: 'Refresh', }); 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 new file mode 100644 index 0000000000000..16d55b1b5e797 --- /dev/null +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/actions.tsx @@ -0,0 +1,51 @@ +/* + * 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, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiButton, EuiPopover, EuiContextMenuPanel } from '@elastic/eui'; +import { RunTestManuallyContextItem } from './run_test_manually'; +import { EditMonitorContextItem } from './monitor_summary/edit_monitor_link'; +import { RefreshContextItem } from '../common/components/refresh_button'; +import { AddToCaseContextItem } from './add_to_case_action'; + +export function Actions() { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const handleActionsClick = () => setIsPopoverOpen((value) => !value); + + const closePopover = () => setIsPopoverOpen(false); + return ( + + {i18n.translate('xpack.synthetics.monitorDetails.headerControl.actions', { + defaultMessage: 'Actions', + })} + + } + isOpen={isPopoverOpen} + closePopover={closePopover} + > + , + , + , + , + ]} + /> + + ); +} 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 new file mode 100644 index 0000000000000..4d5c06cfd2c12 --- /dev/null +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/add_to_case_action.test.tsx @@ -0,0 +1,186 @@ +/* + * 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 { AddToCaseContextItem } from './add_to_case_action'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { useSelectedMonitor } from './hooks/use_selected_monitor'; +import { useGetUrlParams } from '../../hooks'; + +jest.mock('@kbn/kibana-react-plugin/public', () => ({ + useKibana: jest.fn(), +})); + +jest.mock('./hooks/use_selected_monitor', () => ({ + useSelectedMonitor: jest.fn(), +})); + +jest.mock('../../hooks', () => ({ + useGetUrlParams: jest.fn(), + useMonitorDetailLocator: jest + .fn() + .mockImplementation( + ({ timeRange, locationId, configId, tabId }) => + `/${tabId}?${locationId ? `locationId=${locationId}&` : ''}dateRangeStart=${ + timeRange.from + }&dateRangeEnd=${timeRange.to}&configId=${configId}` + ), +})); + +describe('AddToCaseContextItem', () => { + const mockUseKibana = useKibana as jest.Mock; + const mockUseSelectedMonitor = useSelectedMonitor as jest.Mock; + const mockUseGetUrlParams = useGetUrlParams as jest.Mock; + const mockAddDanger = jest.fn(); + const mockOpen = jest.fn(); + + beforeAll(() => { + jest.useFakeTimers(); + jest.setSystemTime(new Date('2025-06-26T12:57:38.568Z')); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + beforeEach(() => { + jest.clearAllMocks(); + + mockUseKibana.mockReturnValue({ + services: { + cases: { + ui: { + getCasesContext: jest.fn(() => ({ children }: any) => <>{children}), + }, + helpers: { + canUseCases: jest.fn(() => ({ + read: true, + update: true, + push: true, + })), + }, + hooks: { + useCasesAddToExistingCaseModal: jest.fn(() => ({ + open: mockOpen, + })), + }, + }, + notifications: { + toasts: { + addDanger: mockAddDanger, + }, + }, + }, + }); + + mockUseSelectedMonitor.mockReturnValue({ + monitor: { config_id: 'test-config-id', name: 'Test Monitor' }, + }); + + mockUseGetUrlParams.mockReturnValue({ + dateRangeStart: 'now-15m', + dateRangeEnd: 'now', + locationId: 'test-location-id', + }); + }); + + it('renders the AddToCaseContextItem component', () => { + render(); + expect(screen.getByText('Add to case')).toBeInTheDocument(); + }); + + it('opens the case modal when clicked', () => { + mockUseKibana.mockReturnValueOnce({ + services: { + cases: { + ui: { + getCasesContext: jest.fn(() => ({ children }: any) => <>{children}), + }, + helpers: { + canUseCases: jest.fn(() => ({ + read: true, + update: true, + push: true, + })), + }, + hooks: { + useCasesAddToExistingCaseModal: jest.fn().mockReturnValue({ + open: mockOpen, + }), + }, + }, + notifications: { + toasts: { + addDanger: jest.fn(), + }, + }, + }, + }); + + 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', + }); + }); + + it('defines persistableStateAttachmentState correctly when case modal is opened', () => { + render(); + fireEvent.click(screen.getByText('Add to case')); + + const getAttachments = mockOpen.mock.calls[0][0].getAttachments; + const attachments = getAttachments(); + + expect(attachments).toHaveLength(1); + expect(attachments[0]).toEqual({ + persistableStateAttachmentState: { + type: 'dashboard', + url: { + pathAndQuery: + '/history?locationId=test-location-id&dateRangeStart=2025-06-26T12:42:38.568Z&dateRangeEnd=2025-06-26T12:57:38.568Z&configId=test-config-id', + label: 'Test Monitor', + actionLabel: 'Go to Monitor History', + iconType: 'uptimeApp', + }, + }, + persistableStateAttachmentTypeId: '.page', + type: 'persistableState', + }); + }); + + it('converts relative date ranges to absolute date ranges via the component', () => { + mockUseGetUrlParams.mockReturnValue({ + dateRangeStart: 'now-15m', + dateRangeEnd: 'now', + locationId: 'test-location-id', + }); + + render(); + fireEvent.click(screen.getByText('Add to case')); + + const getAttachments = mockOpen.mock.calls[0][0].getAttachments; + const attachments = getAttachments(); + + expect(attachments[0].persistableStateAttachmentState.url.pathAndQuery).toEqual( + '/history?locationId=test-location-id&dateRangeStart=2025-06-26T12:42:38.568Z&dateRangeEnd=2025-06-26T12:57:38.568Z&configId=test-config-id' + ); + }); +}); 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 new file mode 100644 index 0000000000000..027f742d72ee3 --- /dev/null +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/add_to_case_action.tsx @@ -0,0 +1,158 @@ +/* + * 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 } 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 { i18n } from '@kbn/i18n'; +import type { CaseAttachmentsWithoutOwner } from '@kbn/cases-plugin/public'; +import type { PageAttachmentPersistedState } from '@kbn/observability-schema'; +import { type CasesPermissions } from '@kbn/cases-plugin/common'; +import { 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 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 = + 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: { + cases: { + hooks: { useCasesAddToExistingCaseModal }, + }, + notifications, + }, + } = useKibana(); + const casesModal = useCasesAddToExistingCaseModal(); + const timeRange: TimeRange = { + from: dateRangeStart, + to: dateRangeEnd, + }; + + const redirectUrl = useMonitorDetailLocator({ + configId: monitor?.config_id ?? '', + timeRange: convertToAbsoluteTimeRange(timeRange), + locationId, + tabId: 'history', + }); + + const onClick = useCallback(() => { + if (!redirectUrl || !monitor?.name) { + notifications.toasts.addDanger({ + title: i18n.translate('xpack.synthetics.cases.addToCaseModal.error.noMonitorLocator', { + defaultMessage: 'Error adding monitor to case', + }), + 'data-test-subj': 'monitorAddToCaseError', + }); + return; + } + + casesModal.open({ + getAttachments: () => { + const url = redirectUrl; + const persistableStateAttachmentState: PageAttachmentPersistedState = { + type: 'dashboard', + url: { + pathAndQuery: url, + label: monitor.name, + actionLabel: i18n.translate( + 'xpack.synthetics.cases.addToCaseModal.goToDashboardActionLabel', + { + defaultMessage: 'Go to Monitor History', + } + ), + iconType: 'uptimeApp', + }, + }; + return [ + { + persistableStateAttachmentState, + persistableStateAttachmentTypeId: '.page', + type: 'persistableState', + }, + ] as CaseAttachmentsWithoutOwner; + }, + }); + }, [casesModal, notifications.toasts, monitor?.name, redirectUrl]); + + return ( + + {i18n.translate('xpack.synthetics.cases.addToCaseModal.buttonLabel', { + defaultMessage: 'Add to case', + })} + + ); +} + +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/public/apps/synthetics/components/monitor_details/monitor_summary/edit_monitor_link.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/edit_monitor_link.tsx index aba79c6621da0..3c9cc876eb24b 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/edit_monitor_link.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/edit_monitor_link.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { useParams } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; -import { EuiButton } from '@elastic/eui'; +import { EuiButton, EuiContextMenuItem } from '@elastic/eui'; import { useCanEditSynthetics } from '../../../../../hooks/use_capabilities'; import { useSyntheticsSettingsContext } from '../../../contexts'; @@ -45,6 +45,32 @@ export const EditMonitorLink = () => { ); }; +export const EditMonitorContextItem = () => { + const { basePath } = useSyntheticsSettingsContext(); + const { monitorId } = useParams<{ monitorId: string }>(); + const { spaceId } = useGetUrlParams(); + const canEditSynthetics = useCanEditSynthetics(); + const isLinkDisabled = !canEditSynthetics; + const linkProps = isLinkDisabled + ? { disabled: true } + : { + href: + `${basePath}/app/synthetics/edit-monitor/${monitorId}` + + (spaceId ? `?spaceId=${spaceId}` : ''), + }; + + return ( + + {EDIT_MONITOR} + + ); +}; + const EDIT_MONITOR = i18n.translate('xpack.synthetics.monitorSummary.editMonitor', { defaultMessage: 'Edit monitor', }); diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/route_config.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/route_config.tsx index b8d0db0f5a774..5c82bc164d0f5 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/route_config.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/route_config.tsx @@ -12,10 +12,8 @@ import { EuiIcon, EuiPageHeaderProps } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { MonitorDetailsAlerts } from './monitor_alerts/monitor_detail_alerts'; import { MonitorAlertsIcon } from './monitor_alerts/alerts_icon'; -import { RefreshButton } from '../common/components/refresh_button'; import { MonitorNotFoundPage } from './monitor_not_found_page'; import { MonitorDetailsPageTitle } from './monitor_details_page_title'; -import { RunTestManually } from './run_test_manually'; import { MonitorDetailsLastRun } from './monitor_details_last_run'; import { MonitorDetailsStatus } from './monitor_details_status'; import { MonitorDetailsLocation } from './monitor_details_location'; @@ -23,7 +21,7 @@ import { MonitorErrors } from './monitor_errors/monitor_errors'; import { MonitorErrorsIcon } from './monitor_errors/errors_icon'; import { MonitorHistory } from './monitor_history/monitor_history'; import { MonitorSummary } from './monitor_summary/monitor_summary'; -import { EditMonitorLink } from './monitor_summary/edit_monitor_link'; +import { Actions } from './actions'; import { MONITOR_ALERTS_ROUTE, MONITOR_ERRORS_ROUTE, @@ -126,17 +124,11 @@ const getMonitorSummaryHeader = ( const monitorId = match.params.monitorId; const rightSideItems = [ - , - , - , + , , , , ]; - if (selectedTab === 'alerts' || selectedTab === 'history' || selectedTab === 'errors') { - // remove first item refresh button - rightSideItems.shift(); - } return { pageTitle: , 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 e32918705634b..a278795611c74 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,7 +5,15 @@ * 2.0. */ -import { EuiButton, EuiToolTip } from '@elastic/eui'; +import { + EuiButton, + EuiToolTip, + EuiContextMenuItem, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiLoadingSpinner, +} from '@elastic/eui'; import React from 'react'; import { i18n } from '@kbn/i18n'; import { useDispatch, useSelector } from 'react-redux'; @@ -63,6 +71,52 @@ export const RunTestManually = () => { ); }; +export const RunTestManuallyContextItem = () => { + const dispatch = useDispatch(); + + const { monitor } = useSelectedMonitor(); + const testInProgress = useSelector(manualTestRunInProgressSelector(monitor?.config_id)); + + const canUsePublicLocations = useCanUsePublicLocations(monitor?.[ConfigKey.LOCATIONS]); + + const { space } = useKibanaSpace(); + + const content = !canUsePublicLocations + ? CANNOT_PERFORM_ACTION_PUBLIC_LOCATIONS + : testInProgress + ? TEST_SCHEDULED_LABEL + : TEST_NOW_ARIA_LABEL; + + return ( + + { + if (monitor) { + const spaceId = 'spaceId' in monitor ? (monitor.spaceId as string) : undefined; + dispatch( + manualTestMonitorAction.get({ + configId: monitor.config_id, + name: monitor.name, + ...(spaceId && spaceId !== space?.id ? { spaceId } : {}), + }) + ); + } + }} + > + + + {testInProgress ? : } + + {{RUN_TEST_LABEL}} + + + + ); +}; + const RUN_TEST_LABEL = i18n.translate('xpack.synthetics.monitorSummary.runTestManually', { defaultMessage: 'Run test manually', }); diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/hooks/index.ts b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/hooks/index.ts index ce0163ff74fb9..00042a442bdeb 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/hooks/index.ts +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/hooks/index.ts @@ -18,3 +18,4 @@ export * from './use_dimensions'; export * from './use_fleet_permissions'; export * from './use_monitor_enable_handler'; export * from './use_edit_monitor_locator'; +export * from './use_monitor_detail_locator'; diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/hooks/use_monitor_detail_locator.test.ts b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/hooks/use_monitor_detail_locator.test.ts new file mode 100644 index 0000000000000..10b924910afca --- /dev/null +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/hooks/use_monitor_detail_locator.test.ts @@ -0,0 +1,90 @@ +/* + * 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 { useMonitorDetailLocator } from './use_monitor_detail_locator'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { useKibanaSpace } from '../../../hooks/use_kibana_space'; + +jest.mock('@kbn/kibana-react-plugin/public', () => ({ + useKibana: jest.fn(), +})); + +jest.mock('../../../hooks/use_kibana_space', () => ({ + useKibanaSpace: jest.fn(), +})); + +const mockLocator = { + getRedirectUrl: jest.fn(), +}; + +describe('useMonitorDetailLocator', () => { + beforeEach(() => { + jest.clearAllMocks(); + (useKibana as jest.Mock).mockReturnValue({ + services: { + share: { + url: { + locators: { + get: jest.fn().mockReturnValue(mockLocator), + }, + }, + }, + }, + }); + (useKibanaSpace as jest.Mock).mockReturnValue({ space: { id: 'default' } }); + }); + + it('should generate the correct monitor URL', async () => { + const mockUrl = 'http://example.com/monitor'; + mockLocator.getRedirectUrl.mockReturnValue(mockUrl); + + const { result } = renderHook(() => + useMonitorDetailLocator({ + configId: 'test-config', + locationId: 'test-location', + timeRange: { from: 'now-15m', to: 'now' }, + tabId: 'overview', + }) + ); + + await waitFor(() => { + expect(mockLocator.getRedirectUrl).toHaveBeenCalledWith({ + configId: 'test-config', + locationId: 'test-location', + timeRange: { from: 'now-15m', to: 'now' }, + tabId: 'overview', + }); + }); + + expect(result.current).toBe(mockUrl); + }); + + it('should return undefined if locator is not available', async () => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + share: { + url: { + locators: { + get: jest.fn().mockReturnValue(undefined), + }, + }, + }, + }, + }); + + const { result } = renderHook(() => + useMonitorDetailLocator({ + configId: 'test-config', + }) + ); + + await waitFor(() => { + expect(result.current).toBeUndefined(); + }); + }); +}); 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 6758e14ce6d20..8f77b7e935ff1 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 @@ -8,19 +8,26 @@ import { useEffect, useState } from 'react'; import { syntheticsMonitorDetailLocatorID } from '@kbn/observability-plugin/common'; import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { TimeRange } from '@kbn/es-query'; import { getMonitorSpaceToAppend } from './use_edit_monitor_locator'; import { useKibanaSpace } from '../../../hooks/use_kibana_space'; import { ClientPluginsStart } from '../../../plugin'; +export interface MonitorDetailLocatorParams { + configId: string; + locationId?: string; + timeRange?: TimeRange; + spaces?: string[]; + tabId?: string; +} + export function useMonitorDetailLocator({ configId, locationId, spaces, -}: { - configId: string; - locationId?: string; - spaces?: string[]; -}) { + timeRange, + tabId, +}: MonitorDetailLocatorParams) { const { space } = useKibanaSpace(); const [monitorUrl, setMonitorUrl] = useState(undefined); const locator = useKibana().services?.share?.url.locators.get( @@ -29,15 +36,17 @@ export function useMonitorDetailLocator({ useEffect(() => { async function generateUrl() { - const url = await locator?.getUrl({ + const url = locator?.getRedirectUrl({ configId, locationId, + timeRange, + tabId, ...getMonitorSpaceToAppend(space, spaces), }); setMonitorUrl(url); } generateUrl(); - }, [locator, configId, locationId, spaces, space?.id, space]); + }, [locator, configId, locationId, spaces, space?.id, space, timeRange, tabId]); return monitorUrl; } From 15fa00f3394495abffd6ba30ff6eaf00b348588f Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Thu, 26 Jun 2025 09:25:16 -0400 Subject: [PATCH 30/72] adjust types --- .../cases/public/client/attachment_framework/types.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/x-pack/platform/plugins/shared/cases/public/client/attachment_framework/types.ts b/x-pack/platform/plugins/shared/cases/public/client/attachment_framework/types.ts index 5c60dd5d45018..cd946c885aaa3 100644 --- a/x-pack/platform/plugins/shared/cases/public/client/attachment_framework/types.ts +++ b/x-pack/platform/plugins/shared/cases/public/client/attachment_framework/types.ts @@ -57,11 +57,9 @@ export interface ExternalReferenceAttachmentViewProps extends CommonAttachmentVi externalReferenceMetadata: ExternalReferenceAttachmentPayload['externalReferenceMetadata']; } -export interface PersistableStateAttachmentViewProps< - Payload extends PersistableStateAttachmentPayload['persistableStateAttachmentState'] = PersistableStateAttachmentPayload['persistableStateAttachmentState'] -> extends CommonAttachmentViewProps { +export interface PersistableStateAttachmentViewProps extends CommonAttachmentViewProps { persistableStateAttachmentTypeId: PersistableStateAttachmentPayload['persistableStateAttachmentTypeId']; - persistableStateAttachmentState: Payload; + persistableStateAttachmentState: PersistableStateAttachmentPayload['persistableStateAttachmentState']; } export interface AttachmentType { @@ -73,9 +71,7 @@ export interface AttachmentType { } export type ExternalReferenceAttachmentType = AttachmentType; -export type PersistableStateAttachmentType< - Payload extends PersistableStateAttachmentPayload['persistableStateAttachmentState'] = PersistableStateAttachmentPayload['persistableStateAttachmentState'] -> = AttachmentType>; +export type PersistableStateAttachmentType = AttachmentType; export interface AttachmentFramework { registerExternalReference: ( From a86693066ea3a9fa9afa18539bbff422d2ec7839 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 26 Jun 2025 13:32:12 +0000 Subject: [PATCH 31/72] [CI] Auto-commit changed files from 'node scripts/styled_components_mapping' --- .../solutions/observability/plugins/synthetics/tsconfig.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/solutions/observability/plugins/synthetics/tsconfig.json b/x-pack/solutions/observability/plugins/synthetics/tsconfig.json index 8673013d8f812..b5ba7cffe2b61 100644 --- a/x-pack/solutions/observability/plugins/synthetics/tsconfig.json +++ b/x-pack/solutions/observability/plugins/synthetics/tsconfig.json @@ -115,7 +115,8 @@ "@kbn/fields-metadata-plugin", "@kbn/alerts-ui-shared", "@kbn/core-lifecycle-server", - "@kbn/shared-ux-error-boundary" + "@kbn/shared-ux-error-boundary", + "@kbn/observability-schema" ], "exclude": ["target/**/*"] } From 6f88fc227d64ebdea85e7787b139fe96082fa50d Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Thu, 26 Jun 2025 11:31:55 -0400 Subject: [PATCH 32/72] adjust security start plugin --- .../containers/user_profiles/api.test.ts | 31 +++++++++---------- .../public/containers/user_profiles/api.ts | 14 ++++----- .../use_bulk_get_user_profiles.test.ts | 2 +- .../use_bulk_get_user_profiles.ts | 4 +-- .../use_get_current_user_profile.test.ts | 2 +- .../use_get_current_user_profile.ts | 4 +-- .../contexts/synthetics_shared_context.tsx | 1 + .../plugins/synthetics/public/plugin.ts | 3 ++ 8 files changed, 32 insertions(+), 29 deletions(-) diff --git a/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/api.test.ts b/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/api.test.ts index ad547925f40af..656bc699fefff 100644 --- a/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/api.test.ts +++ b/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/api.test.ts @@ -5,7 +5,8 @@ * 2.0. */ -import type { UserProfileService } from '@kbn/core-user-profile-browser'; +import { securityMock } from '@kbn/security-plugin/public/mocks'; +import type { SecurityPluginStart } from '@kbn/security-plugin/public'; import { GENERAL_CASES_OWNER } from '../../../common/constants'; import { createStartServicesMock } from '../../common/lib/kibana/kibana_react.mock'; import { bulkGetUserProfiles, getCurrentUserProfile, suggestUserProfiles } from './api'; @@ -48,18 +49,17 @@ describe('User profiles API', () => { }); describe('bulkGetUserProfiles', () => { - let userProfileService: UserProfileService; + let security: SecurityPluginStart; beforeEach(() => { jest.clearAllMocks(); - userProfileService = { - bulkGet: jest.fn().mockResolvedValue(userProfiles), - } as unknown as UserProfileService; + security = securityMock.createStart(); + security.userProfiles.bulkGet = jest.fn().mockResolvedValue(userProfiles); }); it('returns the user profiles correctly', async () => { const res = await bulkGetUserProfiles({ - userProfile: userProfileService, + security, uids: userProfilesIds, }); @@ -68,7 +68,7 @@ describe('User profiles API', () => { it('should filter out empty user profiles', async () => { const res = await bulkGetUserProfiles({ - userProfile: userProfileService, + security, uids: [...userProfilesIds, ''], }); @@ -77,11 +77,11 @@ describe('User profiles API', () => { it('calls bulkGet correctly', async () => { await bulkGetUserProfiles({ - userProfile: userProfileService, + security, uids: userProfilesIds, }); - expect(userProfileService.bulkGet).toHaveBeenCalledWith({ + expect(security.userProfiles.bulkGet).toHaveBeenCalledWith({ dataPath: 'avatar', uids: new Set([ 'u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0', @@ -93,20 +93,19 @@ describe('User profiles API', () => { }); describe('getCurrentUserProfile', () => { - let userProfileService: UserProfileService; + let security: SecurityPluginStart; const currentProfile = userProfiles[0]; beforeEach(() => { jest.clearAllMocks(); - userProfileService = { - getCurrent: jest.fn().mockResolvedValue(currentProfile), - } as unknown as UserProfileService; + security = securityMock.createStart(); + security.userProfiles.getCurrent = jest.fn().mockResolvedValue(currentProfile); }); it('returns the current user profile correctly', async () => { const res = await getCurrentUserProfile({ - userProfile: userProfileService, + security, }); expect(res).toEqual(currentProfile); @@ -114,10 +113,10 @@ describe('User profiles API', () => { it('calls getCurrent correctly', async () => { await getCurrentUserProfile({ - userProfile: userProfileService, + security, }); - expect(userProfileService.getCurrent).toHaveBeenCalledWith({ + expect(security.userProfiles.getCurrent).toHaveBeenCalledWith({ dataPath: 'avatar', }); }); diff --git a/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/api.ts b/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/api.ts index 0044ba5aa3b2c..79bece053bb37 100644 --- a/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/api.ts +++ b/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/api.ts @@ -7,7 +7,7 @@ import type { HttpStart } from '@kbn/core/public'; import type { UserProfile } from '@kbn/security-plugin/common'; -import type { UserProfileService } from '@kbn/core-user-profile-browser'; +import type { SecurityPluginStart } from '@kbn/security-plugin/public'; import { isEmpty } from 'lodash'; import { INTERNAL_SUGGEST_USER_PROFILES_URL, DEFAULT_USER_SIZE } from '../../../common/constants'; @@ -35,12 +35,12 @@ export const suggestUserProfiles = async ({ }; export interface BulkGetUserProfilesArgs { - userProfile: UserProfileService; + security: SecurityPluginStart; uids: string[]; } export const bulkGetUserProfiles = async ({ - userProfile, + security, uids, }: BulkGetUserProfilesArgs): Promise => { const cleanUids: string[] = uids.filter((uid) => !isEmpty(uid)); @@ -48,15 +48,15 @@ export const bulkGetUserProfiles = async ({ return []; } - return userProfile.bulkGet({ uids: new Set(cleanUids), dataPath: 'avatar' }); + return security.userProfiles.bulkGet({ uids: new Set(cleanUids), dataPath: 'avatar' }); }; export interface GetCurrentUserProfileArgs { - userProfile: UserProfileService; + security: SecurityPluginStart; } export const getCurrentUserProfile = async ({ - userProfile, + security, }: GetCurrentUserProfileArgs): Promise => { - return userProfile.getCurrent({ dataPath: 'avatar' }); + return security.userProfiles.getCurrent({ dataPath: 'avatar' }); }; diff --git a/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/use_bulk_get_user_profiles.test.ts b/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/use_bulk_get_user_profiles.test.ts index 814ecd791da65..3cefde7eb8527 100644 --- a/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/use_bulk_get_user_profiles.test.ts +++ b/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/use_bulk_get_user_profiles.test.ts @@ -44,7 +44,7 @@ describe('useBulkGetUserProfiles', () => { await waitFor(() => expect(spyOnBulkGetUserProfiles).toBeCalledWith({ ...props, - userProfile: expect.anything(), + security: expect.anything(), }) ); }); diff --git a/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/use_bulk_get_user_profiles.ts b/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/use_bulk_get_user_profiles.ts index 29e7127159279..8b1b9580ca84f 100644 --- a/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/use_bulk_get_user_profiles.ts +++ b/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/use_bulk_get_user_profiles.ts @@ -21,14 +21,14 @@ const profilesToMap = (profiles: UserProfileWithAvatar[]): Map()); export const useBulkGetUserProfiles = ({ uids }: { uids: string[] }) => { - const { userProfile } = useKibana().services; + const { security } = useKibana().services; const toasts = useToasts(); return useQuery>( casesQueriesKeys.userProfilesList(uids), () => { - return bulkGetUserProfiles({ userProfile, uids }); + return bulkGetUserProfiles({ security, uids }); }, { select: profilesToMap, diff --git a/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/use_get_current_user_profile.test.ts b/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/use_get_current_user_profile.test.ts index 5facd95bd1d0e..9a3e0c2018af3 100644 --- a/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/use_get_current_user_profile.test.ts +++ b/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/use_get_current_user_profile.test.ts @@ -42,7 +42,7 @@ describe('useGetCurrentUserProfile', () => { }); expect(spyOnGetCurrentUserProfile).toBeCalledWith({ - userProfile: expect.anything(), + security: expect.anything(), }); }); diff --git a/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/use_get_current_user_profile.ts b/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/use_get_current_user_profile.ts index a82bd2acb4e99..e98f08ab486f9 100644 --- a/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/use_get_current_user_profile.ts +++ b/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/use_get_current_user_profile.ts @@ -15,14 +15,14 @@ import { casesQueriesKeys } from '../constants'; import { getCurrentUserProfile } from './api'; export const useGetCurrentUserProfile = () => { - const { userProfile } = useKibana().services; + const { security } = useKibana().services; const toasts = useToasts(); return useQuery( casesQueriesKeys.currentUser(), () => { - return getCurrentUserProfile({ userProfile }); + return getCurrentUserProfile({ security }); }, { retry: false, diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/contexts/synthetics_shared_context.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/contexts/synthetics_shared_context.tsx index cc0284550354b..c938fc8cc0b37 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/contexts/synthetics_shared_context.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/contexts/synthetics_shared_context.tsx @@ -55,6 +55,7 @@ export const SyntheticsSharedContext: React.FC< slo: startPlugins.slo, serverless: startPlugins.serverless, charts: startPlugins.charts, + security: startPlugins.security, }} > diff --git a/x-pack/solutions/observability/plugins/synthetics/public/plugin.ts b/x-pack/solutions/observability/plugins/synthetics/public/plugin.ts index cb3fc9b44ab39..9b8ebe2fc73a6 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/plugin.ts +++ b/x-pack/solutions/observability/plugins/synthetics/public/plugin.ts @@ -63,6 +63,7 @@ import { DashboardStart } from '@kbn/dashboard-plugin/public'; import { SLOPublicStart } from '@kbn/slo-plugin/public'; import type { ChartsPluginStart } from '@kbn/charts-plugin/public'; import { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/public'; +import { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/public'; import { EmbeddableEnhancedPluginStart } from '@kbn/embeddable-enhanced-plugin/public'; import { registerSyntheticsEmbeddables } from './apps/embeddables/register_embeddables'; import { kibanaService } from './utils/kibana_service'; @@ -84,6 +85,7 @@ export interface ClientPluginsSetup { observabilityShared: ObservabilitySharedPluginSetup; observabilityAIAssistant?: ObservabilityAIAssistantPublicSetup; share: SharePluginSetup; + security: SecurityPluginSetup; triggersActionsUi: TriggersAndActionsUIPublicPluginSetup; cloud?: CloudSetup; embeddable: EmbeddableSetup; @@ -104,6 +106,7 @@ export interface ClientPluginsStart { observabilityShared: ObservabilitySharedPluginStart; observabilityAIAssistant?: ObservabilityAIAssistantPublicStart; share: SharePluginStart; + security: SecurityPluginStart; triggersActionsUi: TriggersAndActionsUIPublicPluginStart; cases: CasesPublicStart; dataViews: DataViewsPublicPluginStart; From 3fa8b3eb30dbb5a5fb80917631d733d7ce5195a5 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 26 Jun 2025 15:52:36 +0000 Subject: [PATCH 33/72] [CI] Auto-commit changed files from 'node scripts/yarn_deduplicate' --- x-pack/platform/plugins/shared/cases/tsconfig.json | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/platform/plugins/shared/cases/tsconfig.json b/x-pack/platform/plugins/shared/cases/tsconfig.json index ddbac81d7fb87..e50d1d0123b5b 100644 --- a/x-pack/platform/plugins/shared/cases/tsconfig.json +++ b/x-pack/platform/plugins/shared/cases/tsconfig.json @@ -89,7 +89,6 @@ "@kbn/logging", "@kbn/core-elasticsearch-client-server-mocks", "@kbn/core-test-helpers-model-versions", - "@kbn/core-user-profile-browser", "@kbn/elastic-assistant-common", "@kbn/test", "@kbn/dev-utils", From 7b944e8581eb2ff2274216a2567277a97d2818b4 Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Thu, 26 Jun 2025 13:02:13 -0400 Subject: [PATCH 34/72] adjust import --- x-pack/platform/plugins/shared/cases/kibana.jsonc | 3 ++- .../observability/public/attachments/page/attachment.tsx | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/x-pack/platform/plugins/shared/cases/kibana.jsonc b/x-pack/platform/plugins/shared/cases/kibana.jsonc index 660bc792b8fc2..d7ba643a5ee12 100644 --- a/x-pack/platform/plugins/shared/cases/kibana.jsonc +++ b/x-pack/platform/plugins/shared/cases/kibana.jsonc @@ -49,7 +49,8 @@ "savedObjectsFinder" ], "extraPublicDirs": [ - "common" + "common", + "public/client/attachment_framework/types" ] } } \ No newline at end of file diff --git a/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment.tsx b/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment.tsx index d45dd08fb6e8a..72315da10dc42 100644 --- a/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment.tsx +++ b/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment.tsx @@ -13,8 +13,8 @@ import { type PersistableStateAttachmentViewProps, type AttachmentViewObject, type AttachmentAction, + AttachmentActionType, } from '@kbn/cases-plugin/public/client/attachment_framework/types'; -import { AttachmentActionType } from '@kbn/cases-plugin/public'; import * as i18n from './translations'; const AttachmentChildrenLazy = React.lazy(() => import('./attachment_children')); From 0478977ec3cd9e3ce22508210a9e307393427a27 Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Thu, 26 Jun 2025 13:03:50 -0400 Subject: [PATCH 35/72] Update assignees.test.tsx --- .../cases/public/components/case_form_fields/assignees.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/platform/plugins/shared/cases/public/components/case_form_fields/assignees.test.tsx b/x-pack/platform/plugins/shared/cases/public/components/case_form_fields/assignees.test.tsx index d7671b63e0820..ab96016784f68 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/case_form_fields/assignees.test.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/case_form_fields/assignees.test.tsx @@ -222,7 +222,7 @@ describe('Assignees', () => { }); expect(spyOnBulkGetUserProfiles).toHaveBeenCalledWith({ - userProfile: expect.anything(), + security: expect.anything(), uids: [userProfile.uid], }); From 041df8da416ec70e18ffd8042a886e81286c4f68 Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Thu, 26 Jun 2025 13:04:20 -0400 Subject: [PATCH 36/72] adjust import --- x-pack/platform/plugins/shared/cases/public/index.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/x-pack/platform/plugins/shared/cases/public/index.tsx b/x-pack/platform/plugins/shared/cases/public/index.tsx index b7f953dbfdef0..af6e71580f944 100644 --- a/x-pack/platform/plugins/shared/cases/public/index.tsx +++ b/x-pack/platform/plugins/shared/cases/public/index.tsx @@ -23,5 +23,3 @@ export type { CaseAttachments, CaseAttachmentsWithoutOwner } from './types'; export type { ICasesDeepLinkId } from './common/navigation'; export { getCasesDeepLinks, CasesDeepLinkId, generateCaseViewPath } from './common/navigation'; - -export { AttachmentActionType } from './client/attachment_framework/types'; From 6febbcff3b7a498378c426d652e03d8b491e6d0e Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Thu, 26 Jun 2025 15:09:09 -0400 Subject: [PATCH 37/72] adjust limit --- packages/kbn-optimizer/limits.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index cb6698ae7f4a1..f412b956ad227 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -105,7 +105,7 @@ pageLoadAssetSize: navigation: 37269 newsfeed: 42228 noDataPage: 5000 - observability: 118191 + observability: 160191 observabilityAIAssistant: 58230 observabilityAIAssistantApp: 27680 observabilityAiAssistantManagement: 19279 From 3ed6892837bd2ca1d80b2db75c23488efdadcf22 Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Fri, 27 Jun 2025 11:28:34 -0400 Subject: [PATCH 38/72] adjust tests --- .../attachments_framework/registered_persistable_state_basic.ts | 1 + .../attachments_framework/registered_persistable_state_trial.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/basic/attachments_framework/registered_persistable_state_basic.ts b/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/basic/attachments_framework/registered_persistable_state_basic.ts index c636fadc2578e..b92020ae4cb5d 100644 --- a/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/basic/attachments_framework/registered_persistable_state_basic.ts +++ b/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/basic/attachments_framework/registered_persistable_state_basic.ts @@ -34,6 +34,7 @@ export default ({ getService }: FtrProviderContext): void => { expect(types).to.eql({ '.lens': '78559fd806809ac3a1008942ead2a079864054f5', + '.page': '8e5143e79705c8cebe2d5d9949e22d52944629d0', '.test': 'ab2204830c67f5cf992c9aa2f7e3ead752cc60a1', }); }); diff --git a/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/trial/attachments_framework/registered_persistable_state_trial.ts b/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/trial/attachments_framework/registered_persistable_state_trial.ts index cdab3aceda608..a618829690831 100644 --- a/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/trial/attachments_framework/registered_persistable_state_trial.ts +++ b/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/trial/attachments_framework/registered_persistable_state_trial.ts @@ -34,6 +34,7 @@ export default ({ getService }: FtrProviderContext): void => { expect(types).to.eql({ '.lens': '78559fd806809ac3a1008942ead2a079864054f5', + '.page': '8e5143e79705c8cebe2d5d9949e22d52944629d0', '.test': 'ab2204830c67f5cf992c9aa2f7e3ead752cc60a1', aiopsChangePointChart: 'a1212d71947ec34487b374cecc47ab9941b5d91c', ml_anomaly_charts: '23e92e824af9db6e8b8bb1d63c222e04f57d2147', From 6949dfda282ae524f2fef44a490a1824abc472ca Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Fri, 27 Jun 2025 12:57:22 -0400 Subject: [PATCH 39/72] add feature flag --- .../observability_shared/common/config.ts | 22 +++++++++++++++ .../plugins/observability_shared/kibana.jsonc | 4 +-- .../observability_shared/public/index.ts | 6 ++-- .../observability_shared/public/plugin.ts | 7 ++++- .../observability_shared/server/config.ts | 16 +++++++++++ .../observability_shared/server/index.ts | 15 ++++++++++ .../observability_shared/server/plugin.ts | 28 +++++++++++++++++++ .../components/monitor_details/actions.tsx | 10 +++++-- 8 files changed, 100 insertions(+), 8 deletions(-) create mode 100644 x-pack/solutions/observability/plugins/observability_shared/common/config.ts create mode 100644 x-pack/solutions/observability/plugins/observability_shared/server/config.ts create mode 100644 x-pack/solutions/observability/plugins/observability_shared/server/index.ts create mode 100644 x-pack/solutions/observability/plugins/observability_shared/server/plugin.ts diff --git a/x-pack/solutions/observability/plugins/observability_shared/common/config.ts b/x-pack/solutions/observability/plugins/observability_shared/common/config.ts new file mode 100644 index 0000000000000..c4b6f0823a54f --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability_shared/common/config.ts @@ -0,0 +1,22 @@ +/* + * 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 { schema, TypeOf } from '@kbn/config-schema'; + +export const observabilitySharedConfigSchema = schema.object({ + unsafe: schema.maybe( + schema.object({ + investigativeExperienceEnabled: schema.maybe(schema.boolean({ defaultValue: true })), + }) + ), +}); + +export type ObservabilitySharedConfig = TypeOf; +export type ObservabilitySharedBrowserConfig = Pick< + TypeOf, + 'unsafe' +>; diff --git a/x-pack/solutions/observability/plugins/observability_shared/kibana.jsonc b/x-pack/solutions/observability/plugins/observability_shared/kibana.jsonc index 7b75319f71d1d..cd1e4dd6b4db5 100644 --- a/x-pack/solutions/observability/plugins/observability_shared/kibana.jsonc +++ b/x-pack/solutions/observability/plugins/observability_shared/kibana.jsonc @@ -9,10 +9,10 @@ "plugin": { "id": "observabilityShared", "browser": true, - "server": false, + "server": true, "configPath": [ "xpack", - "observability_shared" + "observabilityShared" ], "requiredPlugins": [ "cases", 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 f566c4dc7e1de..553fddb2b227a 100644 --- a/x-pack/solutions/observability/plugins/observability_shared/public/index.ts +++ b/x-pack/solutions/observability/plugins/observability_shared/public/index.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { PluginInitializerContext } from '@kbn/core/public'; import { ObservabilitySharedPlugin } from './plugin'; export type { ObservabilitySharedPlugin, @@ -12,8 +12,8 @@ export type { ObservabilitySharedPluginStart, ProfilingLocators, } from './plugin'; -export const plugin = () => { - return new ObservabilitySharedPlugin(); +export const plugin = (initializerContext: PluginInitializerContext) => { + return new ObservabilitySharedPlugin(initializerContext); }; export type { diff --git a/x-pack/solutions/observability/plugins/observability_shared/public/plugin.ts b/x-pack/solutions/observability/plugins/observability_shared/public/plugin.ts index f52e24fa14267..f630c3a1c52c5 100644 --- a/x-pack/solutions/observability/plugins/observability_shared/public/plugin.ts +++ b/x-pack/solutions/observability/plugins/observability_shared/public/plugin.ts @@ -9,6 +9,7 @@ import { CasesPublicStart } from '@kbn/cases-plugin/public'; import type { CoreSetup, CoreStart, Plugin } from '@kbn/core/public'; import type { EmbeddableStart } from '@kbn/embeddable-plugin/public'; import type { GuidedOnboardingPluginStart } from '@kbn/guided-onboarding-plugin/public'; +import type { PluginInitializerContext } from '@kbn/core/public'; import type { BrowserUrlService, SharePluginSetup, @@ -44,6 +45,7 @@ import { type MetricsExplorerLocator, type TransactionDetailsByTraceIdLocator, } from '../common'; +import { ObservabilitySharedBrowserConfig } from '../common/config'; import { updateGlobalNavigation } from './services/update_global_navigation'; import { DependencyOverviewLocator, @@ -89,12 +91,14 @@ interface ObservabilitySharedLocators { export class ObservabilitySharedPlugin implements Plugin { private readonly navigationRegistry = createNavigationRegistry(); private isSidebarEnabled$: BehaviorSubject; + private config?: ObservabilitySharedBrowserConfig; - constructor() { + constructor(private readonly initializerContext: PluginInitializerContext) { this.isSidebarEnabled$ = new BehaviorSubject(true); } public setup(coreSetup: CoreSetup, pluginsSetup: ObservabilitySharedSetup) { + this.config = this.initializerContext.config.get(); coreSetup.getStartServices().then(([coreStart]) => { coreStart.chrome .getChromeStyle$() @@ -130,6 +134,7 @@ export class ObservabilitySharedPlugin implements Plugin { registerSections: this.navigationRegistry.registerSections, }, updateGlobalNavigation, + config: this.config, }; } diff --git a/x-pack/solutions/observability/plugins/observability_shared/server/config.ts b/x-pack/solutions/observability/plugins/observability_shared/server/config.ts new file mode 100644 index 0000000000000..7ec3d55e97729 --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability_shared/server/config.ts @@ -0,0 +1,16 @@ +/* + * 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 type { PluginConfigDescriptor } from '@kbn/core/server'; +import { observabilitySharedConfigSchema } from '../common/config'; + +export const config: PluginConfigDescriptor = { + exposeToBrowser: { + unsafe: true, + }, + schema: observabilitySharedConfigSchema, +}; diff --git a/x-pack/solutions/observability/plugins/observability_shared/server/index.ts b/x-pack/solutions/observability/plugins/observability_shared/server/index.ts new file mode 100644 index 0000000000000..6a9a5aba136c9 --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability_shared/server/index.ts @@ -0,0 +1,15 @@ +/* + * 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 { PluginInitializerContext } from '@kbn/core/server'; + +export { config } from './config'; + +export async function plugin(initializerContext: PluginInitializerContext) { + const { ObservabilitySharedPlugin } = await import('./plugin'); + return new ObservabilitySharedPlugin(initializerContext); +} diff --git a/x-pack/solutions/observability/plugins/observability_shared/server/plugin.ts b/x-pack/solutions/observability/plugins/observability_shared/server/plugin.ts new file mode 100644 index 0000000000000..6ef45721f4660 --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability_shared/server/plugin.ts @@ -0,0 +1,28 @@ +/* + * 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 { PluginInitializerContext, Plugin as PluginType } from '@kbn/core/server'; + +import { ObservabilitySharedConfig } from '../common/config'; + +export class ObservabilitySharedPlugin implements PluginType { + private config?: ObservabilitySharedConfig; + + constructor(private readonly initContext: PluginInitializerContext) {} + + public setup() { + this.config = this.initContext.config.get(); + return {}; + } + + public start() { + return { + config: this.config, + }; + } + + public stop() {} +} 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 16d55b1b5e797..8a5e42fa2f9f8 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 @@ -7,14 +7,21 @@ import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButton, EuiPopover, EuiContextMenuPanel } from '@elastic/eui'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; import { RunTestManuallyContextItem } from './run_test_manually'; import { EditMonitorContextItem } from './monitor_summary/edit_monitor_link'; import { RefreshContextItem } from '../common/components/refresh_button'; import { AddToCaseContextItem } from './add_to_case_action'; +import { ClientPluginsStart } from '../../../../plugin'; export function Actions() { const [isPopoverOpen, setIsPopoverOpen] = useState(false); const handleActionsClick = () => setIsPopoverOpen((value) => !value); + const { + services: { observabilityShared }, + } = useKibana(); + const isAddToCaseEnabled = + observabilityShared.config?.unsafe?.investigativeExperienceEnabled || false; const closePopover = () => setIsPopoverOpen(false); return ( @@ -43,8 +50,7 @@ export function Actions() { , , , - , - ]} + ].concat(isAddToCaseEnabled ? [] : [])} /> ); From 552bc735cbc440dd0001eb01c346ab0a6229533c Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 27 Jun 2025 17:15:28 +0000 Subject: [PATCH 40/72] [CI] Auto-commit changed files from 'node scripts/styled_components_mapping' --- .../observability/plugins/observability_shared/tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/solutions/observability/plugins/observability_shared/tsconfig.json b/x-pack/solutions/observability/plugins/observability_shared/tsconfig.json index 17460eaf2cc73..312c877d29a1b 100644 --- a/x-pack/solutions/observability/plugins/observability_shared/tsconfig.json +++ b/x-pack/solutions/observability/plugins/observability_shared/tsconfig.json @@ -49,6 +49,7 @@ "@kbn/charts-theme", "@kbn/deeplinks-observability", "@kbn/home-sample-data-tab", + "@kbn/config-schema", ], "exclude": ["target/**/*", ".storybook/**/*.js"] } From aad339b143c6dd3412c7ce3fb68ac30d99baf088 Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Fri, 27 Jun 2025 13:53:51 -0400 Subject: [PATCH 41/72] add test --- .../monitor_details/actions.test.tsx | 185 ++++++++++++++++++ .../hooks/use_selected_monitor.tsx | 2 +- 2 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/actions.test.tsx 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 new file mode 100644 index 0000000000000..e73ace1c63bf0 --- /dev/null +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/actions.test.tsx @@ -0,0 +1,185 @@ +/* + * 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 { Actions } from './actions'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { useParams, useLocation } from 'react-router-dom'; +import { useDispatch, useSelector } from 'react-redux'; + +jest.mock('@kbn/kibana-react-plugin/public', () => ({ + useKibana: jest.fn(), +})); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: jest.fn(), + useLocation: jest.fn(), +})); + +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useDispatch: jest.fn(), + useSelector: jest.fn(), +})); + +describe('Actions Component', () => { + let mockDispatch: jest.Mock; + + beforeEach(() => { + mockDispatch = jest.fn(); + + (useDispatch as jest.Mock).mockReturnValue(mockDispatch); + (useSelector as jest.Mock).mockReturnValue([]); + (useParams as jest.Mock).mockReturnValue({ monitorId: 'test-monitor-id' }); + (useLocation as jest.Mock).mockReturnValue({ search: '?test=true' }); + }); + + it('renders all default action items', () => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + notifications: { + toasts: { + addDanger: jest.fn(), + }, + }, + observabilityShared: { + config: { + unsafe: { + investigativeExperienceEnabled: false, + }, + }, + }, + cases: { + ui: { + getCasesContext: jest.fn(() => ({ children }: { children: React.ReactNode }) => ( +
{children}
+ )), + }, + helpers: { + canUseCases: jest.fn(() => ({ + read: true, + update: true, + push: true, + })), + }, + hooks: { + useCasesAddToExistingCaseModal: jest.fn(() => ({ + open: jest.fn(), + })), + }, + }, + }, + }); + render(); + + // Open the popover + fireEvent.click(screen.getByTestId('o11yHeaderControlActionsButton')); + + expect(screen.getByText('Edit monitor')).toBeInTheDocument(); + expect(screen.getByText('Refresh')).toBeInTheDocument(); + expect(screen.getByText('Run test manually')).toBeInTheDocument(); + expect(screen.queryByText('Add to Case')).not.toBeInTheDocument(); + }); + + it('renders Add to Case item when investigativeExperienceEnabled is true and has cases permissions', () => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + notifications: { + toasts: { + addDanger: jest.fn(), + }, + }, + observabilityShared: { + config: { + unsafe: { + investigativeExperienceEnabled: true, + }, + }, + }, + cases: { + ui: { + getCasesContext: jest.fn(() => ({ children }: { children: React.ReactNode }) => ( +
{children}
+ )), + }, + helpers: { + canUseCases: jest.fn(() => ({ + read: true, + update: true, + push: true, + })), + }, + hooks: { + useCasesAddToExistingCaseModal: jest.fn(() => ({ + open: jest.fn(), + })), + }, + }, + }, + }); + + render(); + + // Open the popover + fireEvent.click(screen.getByTestId('o11yHeaderControlActionsButton')); + + expect(screen.getByText('Edit monitor')).toBeInTheDocument(); + expect(screen.getByText('Refresh')).toBeInTheDocument(); + expect(screen.getByText('Run test manually')).toBeInTheDocument(); + expect(screen.getByText('Add to case')).toBeInTheDocument(); + }); + + it('does not render Add to case item when permissions are not sufficient', () => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + notifications: { + toasts: { + addDanger: jest.fn(), + }, + }, + observabilityShared: { + config: { + unsafe: { + investigativeExperienceEnabled: true, + }, + }, + }, + cases: { + ui: { + getCasesContext: jest.fn(() => ({ children }: { children: React.ReactNode }) => ( +
{children}
+ )), + }, + helpers: { + canUseCases: jest.fn(() => ({ + read: false, + update: false, + push: false, + })), + }, + hooks: { + useCasesAddToExistingCaseModal: jest.fn(() => ({ + open: jest.fn(), + })), + }, + }, + }, + }); + + render(); + + // Open the popover + fireEvent.click(screen.getByTestId('o11yHeaderControlActionsButton')); + + expect(screen.getByText('Edit monitor')).toBeInTheDocument(); + expect(screen.getByText('Refresh')).toBeInTheDocument(); + expect(screen.getByText('Run test manually')).toBeInTheDocument(); + expect(screen.queryByText('Add to case')).not.toBeInTheDocument(); + }); +}); diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_selected_monitor.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_selected_monitor.tsx index a633a1760be99..fc3a71a4523cf 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_selected_monitor.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_selected_monitor.tsx @@ -55,7 +55,7 @@ export const useSelectedMonitor = ({ : null; const isMonitorMissing = - error?.body.statusCode === 404 && + error?.body?.statusCode === 404 && (error.getPayload as { monitorId: string })?.monitorId === monitorId; useEffect(() => { From 386d1bdd91342e6e0c03c4db9683d0421f8b91a2 Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Fri, 27 Jun 2025 14:15:27 -0400 Subject: [PATCH 42/72] adjust import --- x-pack/platform/plugins/shared/cases/public/index.tsx | 2 ++ .../observability/public/attachments/page/attachment.tsx | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/platform/plugins/shared/cases/public/index.tsx b/x-pack/platform/plugins/shared/cases/public/index.tsx index af6e71580f944..b7f953dbfdef0 100644 --- a/x-pack/platform/plugins/shared/cases/public/index.tsx +++ b/x-pack/platform/plugins/shared/cases/public/index.tsx @@ -23,3 +23,5 @@ export type { CaseAttachments, CaseAttachmentsWithoutOwner } from './types'; export type { ICasesDeepLinkId } from './common/navigation'; export { getCasesDeepLinks, CasesDeepLinkId, generateCaseViewPath } from './common/navigation'; + +export { AttachmentActionType } from './client/attachment_framework/types'; diff --git a/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment.tsx b/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment.tsx index 72315da10dc42..d45dd08fb6e8a 100644 --- a/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment.tsx +++ b/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment.tsx @@ -13,8 +13,8 @@ import { type PersistableStateAttachmentViewProps, type AttachmentViewObject, type AttachmentAction, - AttachmentActionType, } from '@kbn/cases-plugin/public/client/attachment_framework/types'; +import { AttachmentActionType } from '@kbn/cases-plugin/public'; import * as i18n from './translations'; const AttachmentChildrenLazy = React.lazy(() => import('./attachment_children')); From c31efa7b9cf2d4bdc280dc4df55fdce8a3846a0f Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Sun, 29 Jun 2025 21:18:00 -0400 Subject: [PATCH 43/72] adjust test --- .../test/plugin_functional/test_suites/core_plugins/rendering.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/platform/test/plugin_functional/test_suites/core_plugins/rendering.ts b/src/platform/test/plugin_functional/test_suites/core_plugins/rendering.ts index 746b7f7e8c110..b761453e5b7ed 100644 --- a/src/platform/test/plugin_functional/test_suites/core_plugins/rendering.ts +++ b/src/platform/test/plugin_functional/test_suites/core_plugins/rendering.ts @@ -379,6 +379,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) { 'xpack.observabilityAiAssistantManagement.logSourcesEnabled (boolean?)', 'xpack.observabilityAiAssistantManagement.spacesEnabled (boolean?)', 'xpack.observabilityAiAssistantManagement.visibilityEnabled (boolean?)', + 'xpack.observabilityShared.unsafe.investigativeExperienceEnabled (boolean?)', 'share.new_version.enabled (boolean?)', 'aiAssistantManagementSelection.preferredAIAssistantType (default?|never?|observability?)', /** From f5ef41638db13a49e8fcd98320997a5c80b23971 Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Tue, 1 Jul 2025 09:34:09 -0400 Subject: [PATCH 44/72] sanitize url --- .../public/attachments/page/attachment_children.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 602e2d939f3d6..f8a632a3c1677 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 @@ -8,6 +8,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiLink, EuiCallOut, EuiText, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; +import { escape } from 'lodash'; import { type PersistableStateAttachmentViewProps } from '@kbn/cases-plugin/public/client/attachment_framework/types'; import type { PageAttachmentPersistedState } from '@kbn/observability-schema'; @@ -40,7 +41,7 @@ export function PageAttachmentChildren({ ); } - const href = url.pathAndQuery || ''; + const href = escape(url.pathAndQuery || ''); const label = url.label; return ( From 567b4900f7c89cdbf021f3ac718df30a6164827c Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Mon, 7 Jul 2025 10:05:30 -0400 Subject: [PATCH 45/72] Update x-pack/solutions/observability/plugins/observability_shared/common/config.ts --- .../observability/plugins/observability_shared/common/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/solutions/observability/plugins/observability_shared/common/config.ts b/x-pack/solutions/observability/plugins/observability_shared/common/config.ts index c4b6f0823a54f..823ab705452ec 100644 --- a/x-pack/solutions/observability/plugins/observability_shared/common/config.ts +++ b/x-pack/solutions/observability/plugins/observability_shared/common/config.ts @@ -10,7 +10,7 @@ import { schema, TypeOf } from '@kbn/config-schema'; export const observabilitySharedConfigSchema = schema.object({ unsafe: schema.maybe( schema.object({ - investigativeExperienceEnabled: schema.maybe(schema.boolean({ defaultValue: true })), + investigativeExperienceEnabled: schema.maybe(schema.boolean({ defaultValue: false })), }) ), }); From 58b8d287b5b64dd5330608ab75e791b5e4d0215b Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Tue, 8 Jul 2025 08:29:58 -0400 Subject: [PATCH 46/72] Update x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/add_to_case_action.tsx Co-authored-by: Bena Kansara <69037875+benakansara@users.noreply.github.com> --- .../components/monitor_details/add_to_case_action.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 027f742d72ee3..f8eb888c3f85c 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 @@ -127,7 +127,7 @@ function AddToCaseButtonContent() { return ( From 65632391fdb8a06dbe98064cabdeab81c217de5c Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Tue, 8 Jul 2025 08:30:05 -0400 Subject: [PATCH 47/72] Update x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/add_to_case_action.tsx Co-authored-by: Bena Kansara <69037875+benakansara@users.noreply.github.com> --- .../components/monitor_details/add_to_case_action.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f8eb888c3f85c..89779fa3271e8 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 @@ -105,7 +105,7 @@ function AddToCaseButtonContent() { pathAndQuery: url, label: monitor.name, actionLabel: i18n.translate( - 'xpack.synthetics.cases.addToCaseModal.goToDashboardActionLabel', + 'xpack.synthetics.cases.addToCaseModal.goToMonitorHistoryActionLabel', { defaultMessage: 'Go to Monitor History', } From 11730cb7b6fbbd0338bd1a73ac6b6366773e59e7 Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Tue, 8 Jul 2025 08:30:52 -0400 Subject: [PATCH 48/72] Update x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/add_to_case_action.tsx --- .../components/monitor_details/add_to_case_action.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 89779fa3271e8..b33355d813543 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 @@ -100,7 +100,7 @@ function AddToCaseButtonContent() { getAttachments: () => { const url = redirectUrl; const persistableStateAttachmentState: PageAttachmentPersistedState = { - type: 'dashboard', + type: 'synthetics_monitor', url: { pathAndQuery: url, label: monitor.name, From f5c9d4639573d57270bffb44b0c85ee8608b318d Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Tue, 8 Jul 2025 22:11:29 -0400 Subject: [PATCH 49/72] adjust URL parsing --- .../attachments/page/attachment_children.tsx | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) 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 f8a632a3c1677..3dc311a2304b0 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 @@ -5,10 +5,9 @@ * 2.0. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiLink, EuiCallOut, EuiText, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; -import { escape } from 'lodash'; import { type PersistableStateAttachmentViewProps } from '@kbn/cases-plugin/public/client/attachment_framework/types'; import type { PageAttachmentPersistedState } from '@kbn/observability-schema'; @@ -21,8 +20,18 @@ export function PageAttachmentChildren({ }: AttachmentChildrenProps) { const pageState = persistableStateAttachmentState as PageAttachmentPersistedState; const { url } = pageState; + const label = url?.label; - if (!url) { + const href = useMemo(() => { + try { + const parsedUrl = new URL(url.pathAndQuery, window.location.origin); + return parsedUrl.href; + } catch (e) { + return ''; + } + }, [url?.pathAndQuery]); + + if (!url || !href || !label) { return ( ); } - const href = escape(url.pathAndQuery || ''); - const label = url.label; return ( <> From 9c437d10b5abd73b6fdceffb18c52ab89a2f9c1b Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Tue, 8 Jul 2025 22:13:50 -0400 Subject: [PATCH 50/72] adjust types --- .../monitor_details/add_to_case_action.tsx | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) 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 b33355d813543..8f66b36d073ec 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 @@ -21,8 +21,8 @@ export function AddToCaseContextItem() { const { services: { cases }, } = useKibana(); - const getCasesContext = cases.ui?.getCasesContext; - const canUseCases = cases.helpers?.canUseCases; + const getCasesContext = cases?.ui?.getCasesContext; + const canUseCases = cases?.helpers?.canUseCases; const casesPermissions: CasesPermissions = useMemo(() => { if (!canUseCases) { @@ -64,14 +64,10 @@ export function AddToCaseContextItem() { function AddToCaseButtonContent() { const { monitor } = useSelectedMonitor(); const { dateRangeEnd, dateRangeStart, locationId } = useGetUrlParams(); - const { - services: { - cases: { - hooks: { useCasesAddToExistingCaseModal }, - }, - notifications, - }, - } = useKibana(); + 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, @@ -98,11 +94,10 @@ function AddToCaseButtonContent() { casesModal.open({ getAttachments: () => { - const url = redirectUrl; const persistableStateAttachmentState: PageAttachmentPersistedState = { type: 'synthetics_monitor', url: { - pathAndQuery: url, + pathAndQuery: redirectUrl, label: monitor.name, actionLabel: i18n.translate( 'xpack.synthetics.cases.addToCaseModal.goToMonitorHistoryActionLabel', From 9d31b5db56943f0138d0224229747167301eba5f Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Wed, 9 Jul 2025 12:56:01 -0400 Subject: [PATCH 51/72] adjust tests --- .../public/attachments/page/attachment_children.test.tsx | 2 +- .../components/monitor_details/add_to_case_action.test.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment_children.test.tsx b/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment_children.test.tsx index be456b4ad7666..4c6e4c7891abc 100644 --- a/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment_children.test.tsx +++ b/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment_children.test.tsx @@ -25,6 +25,6 @@ describe('PageAttachmentChildren', () => { render(); const link = screen.getByRole('link', { name: 'Sample Dashboard' }); - expect(link).toHaveAttribute('href', '/test/path?query=1'); + expect(link).toHaveAttribute('href', 'http://localhost/test/path?query=1'); }); }); 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 4d5c06cfd2c12..0ef12eeb994af 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', () => { expect(attachments).toHaveLength(1); expect(attachments[0]).toEqual({ persistableStateAttachmentState: { - type: 'dashboard', + type: 'synthetics_monitor', url: { pathAndQuery: '/history?locationId=test-location-id&dateRangeStart=2025-06-26T12:42:38.568Z&dateRangeEnd=2025-06-26T12:57:38.568Z&configId=test-config-id', From 0cc84e4a8721a856122ad2e55537f4ce126160a9 Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Wed, 9 Jul 2025 21:21:28 -0400 Subject: [PATCH 52/72] adjust types --- .../plugins/observability/public/plugin.ts | 6 ++++-- .../plugins/observability/server/plugin.ts | 10 ++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/x-pack/solutions/observability/plugins/observability/public/plugin.ts b/x-pack/solutions/observability/plugins/observability/public/plugin.ts index 8a83e7aa3b4a3..15214ca158585 100644 --- a/x-pack/solutions/observability/plugins/observability/public/plugin.ts +++ b/x-pack/solutions/observability/plugins/observability/public/plugin.ts @@ -122,7 +122,7 @@ export interface ConfigSchema { } export type ObservabilityPublicSetup = ReturnType; export interface ObservabilityPublicPluginsSetup { - cases: CasesPublicSetup; + cases?: CasesPublicSetup; data: DataPublicPluginSetup; fieldFormats: FieldFormatsSetup; observabilityShared: ObservabilitySharedPluginSetup; @@ -264,7 +264,9 @@ export class Plugin const logsLocator = pluginsSetup.share.url.locators.get(DISCOVER_APP_LOCATOR); - pluginsSetup.cases.attachmentFramework.registerPersistableState(getPageAttachmentType()); + if (pluginsSetup.cases) { + pluginsSetup.cases.attachmentFramework.registerPersistableState(getPageAttachmentType()); + } const mount = async (params: AppMountParameters) => { // Load application bundle diff --git a/x-pack/solutions/observability/plugins/observability/server/plugin.ts b/x-pack/solutions/observability/plugins/observability/server/plugin.ts index 87985a56db644..2a0d96d39bc20 100644 --- a/x-pack/solutions/observability/plugins/observability/server/plugin.ts +++ b/x-pack/solutions/observability/plugins/observability/server/plugin.ts @@ -55,7 +55,7 @@ export type ObservabilityPluginSetup = ReturnType; interface PluginSetup { alerting: AlertingServerSetup; - cases: CasesServerSetup; + cases?: CasesServerSetup; features: FeaturesPluginSetup; guidedOnboarding?: GuidedOnboardingPluginSetup; ruleRegistry: RuleRegistryPluginSetupContract; @@ -101,9 +101,11 @@ export class ObservabilityPlugin plugins.features.registerKibanaFeature(getCasesFeatureV2(casesCapabilities, casesApiTags)); plugins.features.registerKibanaFeature(getCasesFeatureV3(casesCapabilities, casesApiTags)); - plugins.cases.attachmentFramework.registerPersistableState({ - id: PAGE_ATTACHMENT_TYPE, - }); + if (plugins.cases) { + plugins.cases.attachmentFramework.registerPersistableState({ + id: PAGE_ATTACHMENT_TYPE, + }); + } let annotationsApiPromise: Promise | undefined; From 35bb2db34fa3a38a01f4d5cd20db6d8024e5b9c9 Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Fri, 11 Jul 2025 11:24:54 -0400 Subject: [PATCH 53/72] handle external urls client side --- .../page/attachment_children.test.tsx | 64 ++++++++++++++++++- .../attachments/page/attachment_children.tsx | 56 +++++++++++++++- 2 files changed, 116 insertions(+), 4 deletions(-) diff --git a/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment_children.test.tsx b/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment_children.test.tsx index 4c6e4c7891abc..13ee1e5e293a6 100644 --- a/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment_children.test.tsx +++ b/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment_children.test.tsx @@ -9,8 +9,24 @@ import React from 'react'; import type { PageAttachmentPersistedState } from '@kbn/observability-schema'; import { render, screen } from '@testing-library/react'; import { PageAttachmentChildren } from './attachment_children'; +import { useKibana } from '../../utils/kibana_react'; + +jest.mock('../../utils/kibana_react', () => ({ + useKibana: jest.fn().mockReturnValue({ + services: { + application: { + navigateToUrl: jest.fn(), + }, + http: { + externalUrl: { isInternalUrl: jest.fn().mockReturnValue(true) }, + }, + notifications: { toasts: { addDanger: jest.fn() } }, + }, + }), +})); describe('PageAttachmentChildren', () => { + const mockNavigateToUrl = jest.fn(); const mockPersistableState: PageAttachmentPersistedState = { type: 'mockType', url: { @@ -21,10 +37,54 @@ describe('PageAttachmentChildren', () => { }, }; + beforeEach(() => { + jest.clearAllMocks(); + (useKibana as jest.Mock).mockReturnValue({ + services: { + application: { + navigateToUrl: mockNavigateToUrl, + }, + http: { + externalUrl: { isInternalUrl: jest.fn().mockReturnValue(true) }, + }, + notifications: { toasts: { addDanger: jest.fn() } }, + }, + }); + }); + it('renders the link with correct label and href', () => { render(); - const link = screen.getByRole('link', { name: 'Sample Dashboard' }); - expect(link).toHaveAttribute('href', 'http://localhost/test/path?query=1'); + const link = screen.getByRole('button', { name: 'Sample Dashboard' }); + link.click(); + expect(mockNavigateToUrl).toHaveBeenCalledWith('http://localhost/test/path?query=1'); + }); + + it('renders a callout when an external URL is detected', () => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + application: { + navigateToUrl: mockNavigateToUrl, + }, + http: { + externalUrl: { isInternalUrl: jest.fn().mockReturnValue(false) }, + }, + notifications: { toasts: { addDanger: jest.fn() } }, + }, + }); + const externalUrlState: PageAttachmentPersistedState = { + type: 'mockType', + url: { + actionLabel: 'View in Dashboards', + pathAndQuery: 'https://external.com/test/path?query=1', + label: 'External Dashboard', + iconType: 'link', + }, + }; + + render(); + + const callout = screen.getByText('External URL detected'); + expect(callout).toBeInTheDocument(); }); }); 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 3dc311a2304b0..a93d2e48d8d3e 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 @@ -5,11 +5,12 @@ * 2.0. */ -import React, { useMemo } from 'react'; +import React, { useMemo, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiLink, EuiCallOut, EuiText, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; import { type PersistableStateAttachmentViewProps } from '@kbn/cases-plugin/public/client/attachment_framework/types'; import type { PageAttachmentPersistedState } from '@kbn/observability-schema'; +import { useKibana } from '../../utils/kibana_react'; interface AttachmentChildrenProps { persistableStateAttachmentState: PersistableStateAttachmentViewProps['persistableStateAttachmentState']; @@ -19,6 +20,15 @@ export function PageAttachmentChildren({ persistableStateAttachmentState, }: AttachmentChildrenProps) { const pageState = persistableStateAttachmentState as PageAttachmentPersistedState; + const { + services: { + application, + http: { + externalUrl: { isInternalUrl }, + }, + notifications: { toasts }, + }, + } = useKibana(); const { url } = pageState; const label = url?.label; @@ -31,6 +41,48 @@ export function PageAttachmentChildren({ } }, [url?.pathAndQuery]); + const onClick = useCallback(() => { + if (!isInternalUrl(href)) { + toasts.addDanger({ + title: i18n.translate('xpack.observability.caseView.pageAttachment.externalUrlWarning', { + defaultMessage: 'External URL detected', + }), + text: i18n.translate('xpack.observability.caseView.pageAttachment.externalUrlWarningText', { + defaultMessage: 'External URLs are not supported. Please check the URL and try again.', + }), + }); + return; + } + + if (href) { + application.navigateToUrl(href); + } + }, [application, href, isInternalUrl, toasts]); + + if (!isInternalUrl(href)) { + return ( + + +

+ {i18n.translate('xpack.observability.caseView.pageAttachment.externalUrlWarningText', { + defaultMessage: + 'External URLs are not supported. Please check the URL and try again.', + })} +

+
+
+ ); + } + if (!url || !href || !label) { return ( )} - + {label} From dc1779fcdb62adec7e056548e357f70bdc06ec3f Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Mon, 14 Jul 2025 10:27:02 -0400 Subject: [PATCH 54/72] adjust test --- .../public/attachments/page/attachment.test.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment.test.tsx b/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment.test.tsx index 56feabf4bb1fe..a4f7ff48b9d0f 100644 --- a/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment.test.tsx +++ b/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment.test.tsx @@ -11,6 +11,20 @@ import { type PageAttachmentPersistedState, PAGE_ATTACHMENT_TYPE } from '@kbn/ob import type { PersistableStateAttachmentViewProps } from '@kbn/cases-plugin/public/client/attachment_framework/types'; import { getPageAttachmentType } from './attachment'; +jest.mock('../../utils/kibana_react', () => ({ + useKibana: jest.fn().mockReturnValue({ + services: { + application: { + navigateToUrl: jest.fn(), + }, + http: { + externalUrl: { isInternalUrl: jest.fn().mockReturnValue(true) }, + }, + notifications: { toasts: { addDanger: jest.fn() } }, + }, + }), +})); + describe('getPageAttachmentType', () => { const state: PageAttachmentPersistedState = { type: 'exampleType', From 973b6509b72a691551b50753e1081c66cc2c484c Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Tue, 15 Jul 2025 15:57:55 -0400 Subject: [PATCH 55/72] add page attachment schema package --- .github/CODEOWNERS | 1 + package.json | 1 + tsconfig.base.json | 2 ++ .../kbn-page-attachment-schema/README.md | 3 +++ .../kbn-page-attachment-schema/index.ts | 9 +++++++++ .../kbn-page-attachment-schema/jest.config.js | 12 +++++++++++ .../kbn-page-attachment-schema/kibana.jsonc | 9 +++++++++ .../kbn-page-attachment-schema/package.json | 7 +++++++ .../src}/constants.ts | 0 .../kbn-page-attachment-schema/src}/latest.ts | 0 .../src}/schema/latest.ts | 0 .../src}/schema/v1.ts | 0 .../kbn-page-attachment-schema/tsconfig.json | 20 +++++++++++++++++++ .../kbn-observability-schema/index.ts | 2 -- .../attachments/page/attachment.test.tsx | 5 ++++- .../public/attachments/page/attachment.tsx | 5 ++++- .../page/attachment_children.test.tsx | 2 +- .../attachments/page/attachment_children.tsx | 2 +- .../public/attachments/page/go_to_action.tsx | 2 +- .../plugins/observability/server/plugin.ts | 2 +- .../monitor_details/add_to_case_action.tsx | 2 +- yarn.lock | 4 ++++ 22 files changed, 81 insertions(+), 9 deletions(-) create mode 100644 x-pack/platform/packages/shared/kbn-page-attachment-schema/README.md create mode 100644 x-pack/platform/packages/shared/kbn-page-attachment-schema/index.ts create mode 100644 x-pack/platform/packages/shared/kbn-page-attachment-schema/jest.config.js create mode 100644 x-pack/platform/packages/shared/kbn-page-attachment-schema/kibana.jsonc create mode 100644 x-pack/platform/packages/shared/kbn-page-attachment-schema/package.json rename x-pack/{solutions/observability/packages/kbn-observability-schema/page_attachment => platform/packages/shared/kbn-page-attachment-schema/src}/constants.ts (100%) rename x-pack/{solutions/observability/packages/kbn-observability-schema/page_attachment => platform/packages/shared/kbn-page-attachment-schema/src}/latest.ts (100%) rename x-pack/{solutions/observability/packages/kbn-observability-schema/page_attachment => platform/packages/shared/kbn-page-attachment-schema/src}/schema/latest.ts (100%) rename x-pack/{solutions/observability/packages/kbn-observability-schema/page_attachment => platform/packages/shared/kbn-page-attachment-schema/src}/schema/v1.ts (100%) create mode 100644 x-pack/platform/packages/shared/kbn-page-attachment-schema/tsconfig.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 58a562c4f524c..ffd74b6b5b7df 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -877,6 +877,7 @@ x-pack/platform/packages/shared/kbn-profiler-cli @elastic/obs-knowledge-team x-pack/platform/packages/shared/kbn-sample-parser @elastic/streams-program-team x-pack/platform/packages/shared/kbn-search-index-documents @elastic/search-kibana x-pack/platform/packages/shared/kbn-slo-schema @elastic/obs-ux-management-team +x-pack/platform/packages/shared/kbn-page-attachment-schema @elastic/obs-ux-management-team x-pack/platform/packages/shared/kbn-streamlang @elastic/streams-program-team x-pack/platform/packages/shared/kbn-streams-schema @elastic/streams-program-team x-pack/platform/packages/shared/logs-overview @elastic/obs-ux-logs-team diff --git a/package.json b/package.json index 9f15671633619..5d5b8ed96b6b4 100644 --- a/package.json +++ b/package.json @@ -758,6 +758,7 @@ "@kbn/osquery-io-ts-types": "link:src/platform/packages/shared/kbn-osquery-io-ts-types", "@kbn/osquery-plugin": "link:x-pack/platform/plugins/shared/osquery", "@kbn/paertial-results-example-plugin": "link:examples/partial_results_example", + "@kbn/page-attachment-schema": "link:x-pack/platform/packages/shared/kbn-page-attachment-schema", "@kbn/painless-lab-plugin": "link:x-pack/platform/plugins/private/painless_lab", "@kbn/palettes": "link:src/platform/packages/shared/kbn-palettes", "@kbn/panel-loader": "link:src/platform/packages/private/kbn-panel-loader", diff --git a/tsconfig.base.json b/tsconfig.base.json index aa99a4a0c3024..92b99a28bab4e 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1462,6 +1462,8 @@ "@kbn/osquery-plugin/*": ["x-pack/platform/plugins/shared/osquery/*"], "@kbn/paertial-results-example-plugin": ["examples/partial_results_example"], "@kbn/paertial-results-example-plugin/*": ["examples/partial_results_example/*"], + "@kbn/page-attachment-schema": ["x-pack/platform/packages/shared/kbn-page-attachment-schema"], + "@kbn/page-attachment-schema/*": ["x-pack/platform/packages/shared/kbn-page-attachment-schema/*"], "@kbn/painless-lab-plugin": ["x-pack/platform/plugins/private/painless_lab"], "@kbn/painless-lab-plugin/*": ["x-pack/platform/plugins/private/painless_lab/*"], "@kbn/palettes": ["src/platform/packages/shared/kbn-palettes"], diff --git a/x-pack/platform/packages/shared/kbn-page-attachment-schema/README.md b/x-pack/platform/packages/shared/kbn-page-attachment-schema/README.md new file mode 100644 index 0000000000000..718324f6acd12 --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-page-attachment-schema/README.md @@ -0,0 +1,3 @@ +# @kbn/page-attachment-schema + +This shared package contains the zod schema definition for persisted state of the page attachment type used in cases. diff --git a/x-pack/platform/packages/shared/kbn-page-attachment-schema/index.ts b/x-pack/platform/packages/shared/kbn-page-attachment-schema/index.ts new file mode 100644 index 0000000000000..eb0e4bc8c2df3 --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-page-attachment-schema/index.ts @@ -0,0 +1,9 @@ +/* + * 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. + */ + +export * from './src/latest'; +export * from './src/constants'; diff --git a/x-pack/platform/packages/shared/kbn-page-attachment-schema/jest.config.js b/x-pack/platform/packages/shared/kbn-page-attachment-schema/jest.config.js new file mode 100644 index 0000000000000..b4b24dbed57bd --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-page-attachment-schema/jest.config.js @@ -0,0 +1,12 @@ +/* + * 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. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../..', + roots: ['/x-pack/platform/packages/shared/kbn-page-attachment-schema'], +}; diff --git a/x-pack/platform/packages/shared/kbn-page-attachment-schema/kibana.jsonc b/x-pack/platform/packages/shared/kbn-page-attachment-schema/kibana.jsonc new file mode 100644 index 0000000000000..ed1c254bbf588 --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-page-attachment-schema/kibana.jsonc @@ -0,0 +1,9 @@ +{ + "type": "shared-common", + "id": "@kbn/page-attachment-schema", + "owner": [ + "@elastic/obs-ux-management-team" + ], + "group": "platform", + "visibility": "shared" +} \ No newline at end of file diff --git a/x-pack/platform/packages/shared/kbn-page-attachment-schema/package.json b/x-pack/platform/packages/shared/kbn-page-attachment-schema/package.json new file mode 100644 index 0000000000000..124d6d7dac197 --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-page-attachment-schema/package.json @@ -0,0 +1,7 @@ +{ + "name": "@kbn/page-attachment-schema", + "description": "Page attachment zod schema definition and constants.", + "private": true, + "version": "1.0.0", + "license": "Elastic License 2.0" +} diff --git a/x-pack/solutions/observability/packages/kbn-observability-schema/page_attachment/constants.ts b/x-pack/platform/packages/shared/kbn-page-attachment-schema/src/constants.ts similarity index 100% rename from x-pack/solutions/observability/packages/kbn-observability-schema/page_attachment/constants.ts rename to x-pack/platform/packages/shared/kbn-page-attachment-schema/src/constants.ts diff --git a/x-pack/solutions/observability/packages/kbn-observability-schema/page_attachment/latest.ts b/x-pack/platform/packages/shared/kbn-page-attachment-schema/src/latest.ts similarity index 100% rename from x-pack/solutions/observability/packages/kbn-observability-schema/page_attachment/latest.ts rename to x-pack/platform/packages/shared/kbn-page-attachment-schema/src/latest.ts diff --git a/x-pack/solutions/observability/packages/kbn-observability-schema/page_attachment/schema/latest.ts b/x-pack/platform/packages/shared/kbn-page-attachment-schema/src/schema/latest.ts similarity index 100% rename from x-pack/solutions/observability/packages/kbn-observability-schema/page_attachment/schema/latest.ts rename to x-pack/platform/packages/shared/kbn-page-attachment-schema/src/schema/latest.ts diff --git a/x-pack/solutions/observability/packages/kbn-observability-schema/page_attachment/schema/v1.ts b/x-pack/platform/packages/shared/kbn-page-attachment-schema/src/schema/v1.ts similarity index 100% rename from x-pack/solutions/observability/packages/kbn-observability-schema/page_attachment/schema/v1.ts rename to x-pack/platform/packages/shared/kbn-page-attachment-schema/src/schema/v1.ts diff --git a/x-pack/platform/packages/shared/kbn-page-attachment-schema/tsconfig.json b/x-pack/platform/packages/shared/kbn-page-attachment-schema/tsconfig.json new file mode 100644 index 0000000000000..47c1c65ecc3aa --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-page-attachment-schema/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "**/*.ts" + ], + "kbn_references": [ + "@kbn/std", + "@kbn/io-ts-utils", + ], + "exclude": [ + "target/**/*", + ] +} diff --git a/x-pack/solutions/observability/packages/kbn-observability-schema/index.ts b/x-pack/solutions/observability/packages/kbn-observability-schema/index.ts index 0f9f44a833454..3095666dff351 100644 --- a/x-pack/solutions/observability/packages/kbn-observability-schema/index.ts +++ b/x-pack/solutions/observability/packages/kbn-observability-schema/index.ts @@ -6,5 +6,3 @@ */ export * from './related_dashboards/latest'; -export * from './page_attachment/latest'; -export * from './page_attachment/constants'; diff --git a/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment.test.tsx b/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment.test.tsx index a4f7ff48b9d0f..8d0d2874f8c33 100644 --- a/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment.test.tsx +++ b/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment.test.tsx @@ -7,7 +7,10 @@ import React, { Suspense } from 'react'; import { screen, waitFor, render } from '@testing-library/react'; -import { type PageAttachmentPersistedState, PAGE_ATTACHMENT_TYPE } from '@kbn/observability-schema'; +import { + type PageAttachmentPersistedState, + PAGE_ATTACHMENT_TYPE, +} from '@kbn/page-attachment-schema'; import type { PersistableStateAttachmentViewProps } from '@kbn/cases-plugin/public/client/attachment_framework/types'; import { getPageAttachmentType } from './attachment'; diff --git a/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment.tsx b/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment.tsx index d45dd08fb6e8a..1667f4d4797df 100644 --- a/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment.tsx +++ b/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment.tsx @@ -6,7 +6,10 @@ */ import React, { Suspense } from 'react'; -import { type PageAttachmentPersistedState, PAGE_ATTACHMENT_TYPE } from '@kbn/observability-schema'; +import { + type PageAttachmentPersistedState, + PAGE_ATTACHMENT_TYPE, +} from '@kbn/page-attachment-schema'; import { EuiLoadingSpinner } from '@elastic/eui'; import { type PersistableStateAttachmentType, diff --git a/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment_children.test.tsx b/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment_children.test.tsx index 13ee1e5e293a6..467e2eee75af7 100644 --- a/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment_children.test.tsx +++ b/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment_children.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import type { PageAttachmentPersistedState } from '@kbn/observability-schema'; +import type { PageAttachmentPersistedState } from '@kbn/page-attachment-schema'; import { render, screen } from '@testing-library/react'; import { PageAttachmentChildren } from './attachment_children'; import { useKibana } from '../../utils/kibana_react'; 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 a93d2e48d8d3e..704b0a09ac82f 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 @@ -9,7 +9,7 @@ import React, { useMemo, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiLink, EuiCallOut, EuiText, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; import { type PersistableStateAttachmentViewProps } from '@kbn/cases-plugin/public/client/attachment_framework/types'; -import type { PageAttachmentPersistedState } from '@kbn/observability-schema'; +import type { PageAttachmentPersistedState } from '@kbn/page-attachment-schema'; import { useKibana } from '../../utils/kibana_react'; interface AttachmentChildrenProps { diff --git a/x-pack/solutions/observability/plugins/observability/public/attachments/page/go_to_action.tsx b/x-pack/solutions/observability/plugins/observability/public/attachments/page/go_to_action.tsx index 28114ad6fa70c..14de00a07eb8d 100644 --- a/x-pack/solutions/observability/plugins/observability/public/attachments/page/go_to_action.tsx +++ b/x-pack/solutions/observability/plugins/observability/public/attachments/page/go_to_action.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { EuiButtonEmpty, type IconType } from '@elastic/eui'; -import type { PageAttachmentPersistedState } from '@kbn/observability-schema'; +import type { PageAttachmentPersistedState } from '@kbn/page-attachment-schema'; export interface GoToActionProps { state: PageAttachmentPersistedState; diff --git a/x-pack/solutions/observability/plugins/observability/server/plugin.ts b/x-pack/solutions/observability/plugins/observability/server/plugin.ts index 6c3acf69027e9..7c64855cbd5ca 100644 --- a/x-pack/solutions/observability/plugins/observability/server/plugin.ts +++ b/x-pack/solutions/observability/plugins/observability/server/plugin.ts @@ -27,7 +27,7 @@ import { SpacesPluginSetup, SpacesPluginStart } from '@kbn/spaces-plugin/server' import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; import { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server'; import { PluginSetup as ESQLSetup } from '@kbn/esql/server'; -import { PAGE_ATTACHMENT_TYPE } from '@kbn/observability-schema'; +import { PAGE_ATTACHMENT_TYPE } from '@kbn/page-attachment-schema'; import { ObservabilityConfig } from '.'; import { OBSERVABILITY_TIERED_FEATURES, observabilityFeatureId } from '../common'; import { 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 8f66b36d073ec..4371e658971a0 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 @@ -11,7 +11,7 @@ import { useKibana } from '@kbn/kibana-react-plugin/public'; import { type TimeRange, getAbsoluteTimeRange } from '@kbn/data-plugin/common'; import { i18n } from '@kbn/i18n'; import type { CaseAttachmentsWithoutOwner } from '@kbn/cases-plugin/public'; -import type { PageAttachmentPersistedState } from '@kbn/observability-schema'; +import type { PageAttachmentPersistedState } from '@kbn/page-attachment-schema'; import { type CasesPermissions } from '@kbn/cases-plugin/common'; import { ClientPluginsStart } from '../../../../plugin'; import { useSelectedMonitor } from './hooks/use_selected_monitor'; diff --git a/yarn.lock b/yarn.lock index a6ab3c72394c5..756227cf58a8e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6705,6 +6705,10 @@ version "0.0.0" uid "" +"@kbn/page-attachment-schema@link:x-pack/platform/packages/shared/kbn-page-attachment-schema": + version "0.0.0" + uid "" + "@kbn/painless-lab-plugin@link:x-pack/platform/plugins/private/painless_lab": version "0.0.0" uid "" From 5745c787cf5d7007a96662d2381cdda9a1317d44 Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Tue, 15 Jul 2025 16:45:00 -0400 Subject: [PATCH 56/72] add validation for page attachment type to prevent saving external urls --- .../api/internal/bulk_create_attachments.ts | 4 +- .../cases/server/routes/api/utils.test.ts | 62 ++++++++++++++++++- .../shared/cases/server/routes/api/utils.ts | 46 +++++++++++++- 3 files changed, 108 insertions(+), 4 deletions(-) diff --git a/x-pack/platform/plugins/shared/cases/server/routes/api/internal/bulk_create_attachments.ts b/x-pack/platform/plugins/shared/cases/server/routes/api/internal/bulk_create_attachments.ts index d585eee080162..71db38cd5d0e5 100644 --- a/x-pack/platform/plugins/shared/cases/server/routes/api/internal/bulk_create_attachments.ts +++ b/x-pack/platform/plugins/shared/cases/server/routes/api/internal/bulk_create_attachments.ts @@ -9,7 +9,7 @@ import { schema } from '@kbn/config-schema'; import { INTERNAL_BULK_CREATE_ATTACHMENTS_URL } from '../../../../common/constants'; import { createCaseError } from '../../../common/error'; import { createCasesRoute } from '../create_cases_route'; -import { escapeHatch } from '../utils'; +import { validAttachment } from '../utils'; import type { attachmentApiV1 } from '../../../../common/types/api'; import type { caseDomainV1 } from '../../../../common/types/domain'; import { DEFAULT_CASES_ROUTE_SECURITY } from '../constants'; @@ -22,7 +22,7 @@ export const bulkCreateAttachmentsRoute = createCasesRoute({ params: schema.object({ case_id: schema.string(), }), - body: schema.arrayOf(escapeHatch), + body: schema.arrayOf(validAttachment), }, routerOptions: { access: 'internal', diff --git a/x-pack/platform/plugins/shared/cases/server/routes/api/utils.test.ts b/x-pack/platform/plugins/shared/cases/server/routes/api/utils.test.ts index 68a6088309f7a..d03f8226062df 100644 --- a/x-pack/platform/plugins/shared/cases/server/routes/api/utils.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/routes/api/utils.test.ts @@ -7,8 +7,15 @@ import { isBoom, boomify } from '@hapi/boom'; import { loggingSystemMock } from '@kbn/core/server/mocks'; +import { PAGE_ATTACHMENT_TYPE } from '@kbn/page-attachment-schema'; import type { HTTPError } from '../../common/error'; -import { extractWarningValueFromWarningHeader, logDeprecatedEndpoint, wrapError } from './utils'; +import { + extractWarningValueFromWarningHeader, + logDeprecatedEndpoint, + wrapError, + validAttachment, + isPersistableStatePageAttachment, +} from './utils'; describe('Utils', () => { describe('wrapError', () => { @@ -83,4 +90,57 @@ describe('Utils', () => { ); }); }); + + describe('validAttachment', () => { + it('validates a valid attachment', () => { + const attachment = { + persistableStateAttachmentTypeId: PAGE_ATTACHMENT_TYPE, + persistableStateAttachmentState: { + url: { pathAndQuery: '/internal/safe-url' }, + }, + }; + + expect(validAttachment.validate(attachment)).toEqual(attachment); + }); + + it('throws an error for an external URL', () => { + const invalidAttachment = { + persistableStateAttachmentTypeId: PAGE_ATTACHMENT_TYPE, + persistableStateAttachmentState: { + url: { pathAndQuery: 'http://external-url.com' }, + }, + }; + + expect(() => validAttachment.validate(invalidAttachment)).toThrow( + 'External urls are not supported for page attachments. The provided url is: http://external-url.com' + ); + }); + }); + + describe('isPersistableStatePageAttachment', () => { + it('returns true for a valid persistable state page attachment', () => { + const attachment = { + persistableStateAttachmentTypeId: PAGE_ATTACHMENT_TYPE, + persistableStateAttachmentState: {}, + }; + + expect(isPersistableStatePageAttachment(attachment)).toBe(true); + }); + + it('returns false for an invalid attachment', () => { + const invalidAttachment = { + someOtherProperty: 'value', + }; + + expect(isPersistableStatePageAttachment(invalidAttachment)).toBe(false); + }); + + it('returns false for a null value', () => { + expect(isPersistableStatePageAttachment(null)).toBe(false); + }); + + it('returns false for a non-object value', () => { + expect(isPersistableStatePageAttachment('string')).toBe(false); + }); + }); }); diff --git a/x-pack/platform/plugins/shared/cases/server/routes/api/utils.ts b/x-pack/platform/plugins/shared/cases/server/routes/api/utils.ts index e363d37482a04..1260515d54dba 100644 --- a/x-pack/platform/plugins/shared/cases/server/routes/api/utils.ts +++ b/x-pack/platform/plugins/shared/cases/server/routes/api/utils.ts @@ -9,6 +9,12 @@ import type { Boom } from '@hapi/boom'; import { boomify, isBoom } from '@hapi/boom'; import { schema } from '@kbn/config-schema'; import type { CustomHttpResponseOptions, ResponseError, Headers, Logger } from '@kbn/core/server'; +import { isInternalURL } from '@kbn/std'; +import { + type PageAttachmentPersistedState, + PAGE_ATTACHMENT_TYPE, +} from '@kbn/page-attachment-schema'; +import type { AttachmentRequestAttributes } from '../../common/types/attachments'; import type { CaseError, HTTPError } from '../../common/error'; import { isCaseError, isHTTPError } from '../../common/error'; @@ -35,7 +41,45 @@ export function wrapError( }; } -export const escapeHatch = schema.object({}, { unknowns: 'allow' }); +export const escapeHatch = schema.object( + {}, + { + unknowns: 'allow', + } +); + +export const validAttachment = schema.object( + {}, + { + unknowns: 'allow', + validate: (value) => { + if ( + isPersistableStatePageAttachment(value) && + value.persistableStateAttachmentTypeId === PAGE_ATTACHMENT_TYPE + ) { + const persistedState = + value.persistableStateAttachmentState as PageAttachmentPersistedState; + const url = persistedState?.url?.pathAndQuery; + if (url && !isInternalURL(url)) { + return `External urls are not supported for page attachments. The provided url is: ${url}`; + } + } + }, + } +); + +/** + * Checks if the given value is a persistable state page attachment type. + */ +export const isPersistableStatePageAttachment = ( + value: unknown +): value is AttachmentRequestAttributes => { + return ( + typeof value === 'object' && + value !== null && + Boolean((value as AttachmentRequestAttributes).persistableStateAttachmentTypeId) + ); +}; /** * Creates a warning header with a message formatted according to RFC7234. From 6c1029eff2900df7a5de76a31ace048c1017dbd9 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 15 Jul 2025 20:56:57 +0000 Subject: [PATCH 57/72] [CI] Auto-commit changed files from 'node scripts/generate codeowners' --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ffd74b6b5b7df..2b28803bf9c92 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -873,11 +873,11 @@ x-pack/platform/packages/shared/kbn-inference-tracing-config @elastic/appex-ai-i x-pack/platform/packages/shared/kbn-key-value-metadata-table @elastic/obs-ux-infra_services-team @elastic/obs-ux-logs-team x-pack/platform/packages/shared/kbn-kibana-api-cli @elastic/appex-ai-infra x-pack/platform/packages/shared/kbn-langchain @elastic/security-generative-ai +x-pack/platform/packages/shared/kbn-page-attachment-schema @elastic/obs-ux-management-team x-pack/platform/packages/shared/kbn-profiler-cli @elastic/obs-knowledge-team x-pack/platform/packages/shared/kbn-sample-parser @elastic/streams-program-team x-pack/platform/packages/shared/kbn-search-index-documents @elastic/search-kibana x-pack/platform/packages/shared/kbn-slo-schema @elastic/obs-ux-management-team -x-pack/platform/packages/shared/kbn-page-attachment-schema @elastic/obs-ux-management-team x-pack/platform/packages/shared/kbn-streamlang @elastic/streams-program-team x-pack/platform/packages/shared/kbn-streams-schema @elastic/streams-program-team x-pack/platform/packages/shared/logs-overview @elastic/obs-ux-logs-team From 867bd02c7336d88b6bb486cd84cbf2246146aebb Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 15 Jul 2025 21:08:30 +0000 Subject: [PATCH 58/72] [CI] Auto-commit changed files from 'node scripts/notice' --- .../packages/shared/kbn-page-attachment-schema/tsconfig.json | 3 +-- x-pack/platform/plugins/shared/cases/tsconfig.json | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/platform/packages/shared/kbn-page-attachment-schema/tsconfig.json b/x-pack/platform/packages/shared/kbn-page-attachment-schema/tsconfig.json index 47c1c65ecc3aa..183ef8c332581 100644 --- a/x-pack/platform/packages/shared/kbn-page-attachment-schema/tsconfig.json +++ b/x-pack/platform/packages/shared/kbn-page-attachment-schema/tsconfig.json @@ -11,8 +11,7 @@ "**/*.ts" ], "kbn_references": [ - "@kbn/std", - "@kbn/io-ts-utils", + "@kbn/zod", ], "exclude": [ "target/**/*", diff --git a/x-pack/platform/plugins/shared/cases/tsconfig.json b/x-pack/platform/plugins/shared/cases/tsconfig.json index e596496134c54..a6d61fd96e735 100644 --- a/x-pack/platform/plugins/shared/cases/tsconfig.json +++ b/x-pack/platform/plugins/shared/cases/tsconfig.json @@ -93,7 +93,8 @@ "@kbn/elastic-assistant-common", "@kbn/test", "@kbn/dev-utils", - "@kbn/tooling-log" + "@kbn/tooling-log", + "@kbn/page-attachment-schema" ], "exclude": ["target/**/*"] } From 9c6946577ff34bcaaea39f4efe81d0abc935f61c Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 15 Jul 2025 21:21:16 +0000 Subject: [PATCH 59/72] [CI] Auto-commit changed files from 'node scripts/lint_ts_projects --fix' --- .../observability/plugins/observability/tsconfig.json | 3 ++- .../solutions/observability/plugins/synthetics/tsconfig.json | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/x-pack/solutions/observability/plugins/observability/tsconfig.json b/x-pack/solutions/observability/plugins/observability/tsconfig.json index 36a1f73a83b52..3f619edadd9d3 100644 --- a/x-pack/solutions/observability/plugins/observability/tsconfig.json +++ b/x-pack/solutions/observability/plugins/observability/tsconfig.json @@ -129,7 +129,8 @@ "@kbn/core-pricing-browser-mocks", "@kbn/esql", "@kbn/apm-synthtrace-client", - "@kbn/scout-oblt" + "@kbn/scout-oblt", + "@kbn/page-attachment-schema" ], "exclude": ["target/**/*"] } diff --git a/x-pack/solutions/observability/plugins/synthetics/tsconfig.json b/x-pack/solutions/observability/plugins/synthetics/tsconfig.json index d72dfc0f5088c..b779724acbe84 100644 --- a/x-pack/solutions/observability/plugins/synthetics/tsconfig.json +++ b/x-pack/solutions/observability/plugins/synthetics/tsconfig.json @@ -116,9 +116,9 @@ "@kbn/alerts-ui-shared", "@kbn/core-lifecycle-server", "@kbn/shared-ux-error-boundary", - "@kbn/observability-schema", "@kbn/field-formats-plugin", - "@kbn/core-ui-settings-browser" + "@kbn/core-ui-settings-browser", + "@kbn/page-attachment-schema" ], "exclude": ["target/**/*"] } From d064307f259c39c262d674848dfce7c88df2980f Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Tue, 15 Jul 2025 22:45:13 -0400 Subject: [PATCH 60/72] adjust types --- .../observability/plugins/observability/public/plugin.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/x-pack/solutions/observability/plugins/observability/public/plugin.ts b/x-pack/solutions/observability/plugins/observability/public/plugin.ts index c413cc4f1d890..fb3b4d3c5f350 100644 --- a/x-pack/solutions/observability/plugins/observability/public/plugin.ts +++ b/x-pack/solutions/observability/plugins/observability/public/plugin.ts @@ -16,7 +16,6 @@ import type { ChartsPluginStart } from '@kbn/charts-plugin/public'; import type { CloudStart } from '@kbn/cloud-plugin/public'; import type { ContentManagementPublicStart } from '@kbn/content-management-plugin/public'; import type { IUiSettingsClient } from '@kbn/core/public'; -import type { CasesPublicSetup } from '@kbn/cases-plugin/public'; import { App, AppDeepLink, @@ -127,7 +126,6 @@ export interface ConfigSchema { } export type ObservabilityPublicSetup = ReturnType; export interface ObservabilityPublicPluginsSetup { - cases?: CasesPublicSetup; data: DataPublicPluginSetup; fieldFormats: FieldFormatsSetup; observabilityShared: ObservabilitySharedPluginSetup; From 37a3f9da45ed7a878ac3c23ef868288c9ae8b8db Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Thu, 17 Jul 2025 09:08:23 -0400 Subject: [PATCH 61/72] adjust test --- .../public/attachments/page/attachment_children.test.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment_children.test.tsx b/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment_children.test.tsx index 467e2eee75af7..a7ad1c903f1fd 100644 --- a/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment_children.test.tsx +++ b/x-pack/solutions/observability/plugins/observability/public/attachments/page/attachment_children.test.tsx @@ -72,12 +72,13 @@ describe('PageAttachmentChildren', () => { notifications: { toasts: { addDanger: jest.fn() } }, }, }); + const label = 'External Dashboard'; const externalUrlState: PageAttachmentPersistedState = { type: 'mockType', url: { actionLabel: 'View in Dashboards', pathAndQuery: 'https://external.com/test/path?query=1', - label: 'External Dashboard', + label, iconType: 'link', }, }; @@ -86,5 +87,6 @@ describe('PageAttachmentChildren', () => { const callout = screen.getByText('External URL detected'); expect(callout).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: label })).not.toBeInTheDocument(); }); }); From 55603064e4feb4240d40cd098868f4719152e2d6 Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Thu, 17 Jul 2025 22:40:20 -0400 Subject: [PATCH 62/72] hide attachment registration behind feature flag --- .../plugins/observability/public/plugin.ts | 5 ++++- .../plugins/observability/server/plugin.ts | 11 ++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/x-pack/solutions/observability/plugins/observability/public/plugin.ts b/x-pack/solutions/observability/plugins/observability/public/plugin.ts index fb3b4d3c5f350..bb7819430ce0a 100644 --- a/x-pack/solutions/observability/plugins/observability/public/plugin.ts +++ b/x-pack/solutions/observability/plugins/observability/public/plugin.ts @@ -272,7 +272,10 @@ export class Plugin const logsLocator = pluginsSetup.share.url.locators.get(DISCOVER_APP_LOCATOR); - if (pluginsSetup.cases) { + if ( + pluginsSetup.cases && + pluginsSetup.observabilityShared.config.unsafe?.investigativeExperienceEnabled + ) { pluginsSetup.cases.attachmentFramework.registerPersistableState(getPageAttachmentType()); } diff --git a/x-pack/solutions/observability/plugins/observability/server/plugin.ts b/x-pack/solutions/observability/plugins/observability/server/plugin.ts index c6bb2a91bc75a..38b55ee5ca519 100644 --- a/x-pack/solutions/observability/plugins/observability/server/plugin.ts +++ b/x-pack/solutions/observability/plugins/observability/server/plugin.ts @@ -18,6 +18,10 @@ import { CoreSetup, CoreStart, Logger, Plugin, PluginInitializerContext } from ' import { DISCOVER_APP_LOCATOR, type DiscoverAppLocatorParams } from '@kbn/discover-plugin/common'; import { FeaturesPluginSetup } from '@kbn/features-plugin/server'; import type { GuidedOnboardingPluginSetup } from '@kbn/guided-onboarding-plugin/server'; +import type { + ObservabilitySharedPluginSetup, + ObservabilitySharedPluginStart, +} from '@kbn/observability-shared-plugin/server'; import { RuleRegistryPluginSetupContract, RuleRegistryPluginStartContract, @@ -66,6 +70,7 @@ interface PluginSetup { cloud?: CloudSetup; contentManagement: ContentManagementServerSetup; esql: ESQLSetup; + observabilityShared: ObservabilitySharedPluginSetup; } interface PluginStart { @@ -74,6 +79,7 @@ interface PluginStart { dataViews: DataViewsServerPluginStart; ruleRegistry: RuleRegistryPluginStartContract; dashboard: DashboardPluginStart; + observabilityShared: ObservabilitySharedPluginStart; } export class ObservabilityPlugin implements Plugin @@ -102,7 +108,10 @@ export class ObservabilityPlugin plugins.features.registerKibanaFeature(getCasesFeatureV2(casesCapabilities, casesApiTags)); plugins.features.registerKibanaFeature(getCasesFeatureV3(casesCapabilities, casesApiTags)); - if (plugins.cases) { + if ( + plugins.cases && + plugins.observabilityShared.config.unsafe?.investigativeExperienceEnabled + ) { plugins.cases.attachmentFramework.registerPersistableState({ id: PAGE_ATTACHMENT_TYPE, }); From 41167a9f8ca35c878d27b67ddc2a8f98dd0ff96b Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Thu, 17 Jul 2025 22:41:43 -0400 Subject: [PATCH 63/72] adjust test --- .../public/attachments/page/go_to_action.test.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/x-pack/solutions/observability/plugins/observability/public/attachments/page/go_to_action.test.tsx b/x-pack/solutions/observability/plugins/observability/public/attachments/page/go_to_action.test.tsx index 4bbde8c233d6a..a439c4fe3236a 100644 --- a/x-pack/solutions/observability/plugins/observability/public/attachments/page/go_to_action.test.tsx +++ b/x-pack/solutions/observability/plugins/observability/public/attachments/page/go_to_action.test.tsx @@ -41,9 +41,6 @@ describe('GoToAction', () => { iconType: 'discoverApp', type: 'discoverSession', screenContext: null, - snapshot: { - imgData: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA...', - }, }; // @ts-ignore next line From c9f0f43aa8f322f58e322e80d43f6d6fa581dd6b Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Thu, 17 Jul 2025 22:42:50 -0400 Subject: [PATCH 64/72] update translations --- .../observability/public/attachments/page/translations.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/solutions/observability/plugins/observability/public/attachments/page/translations.tsx b/x-pack/solutions/observability/plugins/observability/public/attachments/page/translations.tsx index af01ec5ccc818..b0a3b277ddefe 100644 --- a/x-pack/solutions/observability/plugins/observability/public/attachments/page/translations.tsx +++ b/x-pack/solutions/observability/plugins/observability/public/attachments/page/translations.tsx @@ -7,12 +7,12 @@ import { i18n } from '@kbn/i18n'; -export const ADDED_PAGE = i18n.translate('xpack.observability.caseView.links.addedLink', { +export const ADDED_PAGE = i18n.translate('xpack.observability.caseView.page.addedPage', { defaultMessage: 'added a page', }); -export const REMOVED_PAGE = i18n.translate('xpack.observability.caseView.links.removedLink', { +export const REMOVED_PAGE = i18n.translate('xpack.observability.caseView.page.removedPage', { defaultMessage: 'removed page', }); -export const PAGE_LABEL = i18n.translate('xpack.observability.caseView.links.pageLabel', { +export const PAGE_LABEL = i18n.translate('xpack.observability.caseView.page.pageLabel', { defaultMessage: 'Page', }); From c3378bf60216f8798c5d0b14ccd141778e965cc1 Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Thu, 17 Jul 2025 22:43:33 -0400 Subject: [PATCH 65/72] adjust config --- .../plugins/observability_shared/public/plugin.ts | 5 +++-- .../plugins/observability_shared/server/index.ts | 6 ++++++ .../plugins/observability_shared/server/plugin.ts | 13 +++++++++---- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/x-pack/solutions/observability/plugins/observability_shared/public/plugin.ts b/x-pack/solutions/observability/plugins/observability_shared/public/plugin.ts index 75b654c76d5ea..889986505f6d8 100644 --- a/x-pack/solutions/observability/plugins/observability_shared/public/plugin.ts +++ b/x-pack/solutions/observability/plugins/observability_shared/public/plugin.ts @@ -89,14 +89,14 @@ interface ObservabilitySharedLocators { export class ObservabilitySharedPlugin implements Plugin { private readonly navigationRegistry = createNavigationRegistry(); private isSidebarEnabled$: BehaviorSubject; - private config?: ObservabilitySharedBrowserConfig; + private config: ObservabilitySharedBrowserConfig; constructor(private readonly initializerContext: PluginInitializerContext) { this.isSidebarEnabled$ = new BehaviorSubject(true); + this.config = this.initializerContext.config.get(); } public setup(coreSetup: CoreSetup, pluginsSetup: ObservabilitySharedSetup) { - this.config = this.initializerContext.config.get(); coreSetup.getStartServices().then(([coreStart]) => { coreStart.chrome .getChromeStyle$() @@ -109,6 +109,7 @@ export class ObservabilitySharedPlugin implements Plugin { navigation: { registerSections: this.navigationRegistry.registerSections, }, + config: this.config, }; } diff --git a/x-pack/solutions/observability/plugins/observability_shared/server/index.ts b/x-pack/solutions/observability/plugins/observability_shared/server/index.ts index 6a9a5aba136c9..29c54d9572306 100644 --- a/x-pack/solutions/observability/plugins/observability_shared/server/index.ts +++ b/x-pack/solutions/observability/plugins/observability_shared/server/index.ts @@ -9,6 +9,12 @@ import { PluginInitializerContext } from '@kbn/core/server'; export { config } from './config'; +export type { + ObservabilitySharedPlugin, + ObservabilitySharedPluginSetup, + ObservabilitySharedPluginStart, +} from './plugin'; + export async function plugin(initializerContext: PluginInitializerContext) { const { ObservabilitySharedPlugin } = await import('./plugin'); return new ObservabilitySharedPlugin(initializerContext); diff --git a/x-pack/solutions/observability/plugins/observability_shared/server/plugin.ts b/x-pack/solutions/observability/plugins/observability_shared/server/plugin.ts index 6ef45721f4660..db6ff1861adec 100644 --- a/x-pack/solutions/observability/plugins/observability_shared/server/plugin.ts +++ b/x-pack/solutions/observability/plugins/observability_shared/server/plugin.ts @@ -8,14 +8,19 @@ import { PluginInitializerContext, Plugin as PluginType } from '@kbn/core/server import { ObservabilitySharedConfig } from '../common/config'; +export type ObservabilitySharedPluginSetup = ReturnType; +export type ObservabilitySharedPluginStart = ReturnType; export class ObservabilitySharedPlugin implements PluginType { - private config?: ObservabilitySharedConfig; + private config: ObservabilitySharedConfig; - constructor(private readonly initContext: PluginInitializerContext) {} + constructor(private readonly initContext: PluginInitializerContext) { + this.config = this.initContext.config.get(); + } public setup() { - this.config = this.initContext.config.get(); - return {}; + return { + config: this.config, + }; } public start() { From e8037be66fbe8458de1a7b1ab139432daacb648d Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Thu, 17 Jul 2025 22:50:45 -0400 Subject: [PATCH 66/72] add link to EuiIcon --- .../shared/kbn-page-attachment-schema/src/schema/v1.ts | 3 +++ 1 file changed, 3 insertions(+) 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 f4967a619707e..274a5def71068 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 @@ -28,6 +28,9 @@ export const pageAttachmentPersistedStateSchema = z.object({ /** * The icon representing the page type, displayed in the comment and action */ + /** + * The icon, rendered via {@link EuiIcon}, representing the page type, displayed in the comment and action. + */ iconType: z.string().optional(), }), /** From 2f40548aff56880bf3dc989d448164453aa0cbc1 Mon Sep 17 00:00:00 2001 From: Dominique Belcher Date: Thu, 17 Jul 2025 22:57:36 -0400 Subject: [PATCH 67/72] adjust comment --- .../shared/kbn-page-attachment-schema/src/schema/v1.ts | 3 --- 1 file changed, 3 deletions(-) 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 274a5def71068..1ae46596eda17 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 @@ -25,9 +25,6 @@ export const pageAttachmentPersistedStateSchema = z.object({ * The label to render in the attachment comment for the page or page asset */ label: z.string(), - /** - * The icon representing the page type, displayed in the comment and action - */ /** * The icon, rendered via {@link EuiIcon}, representing the page type, displayed in the comment and action. */ From b5d9da5c81222827786be4e70ac0c4682f6236ff Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Mon, 21 Jul 2025 11:41:40 -0400 Subject: [PATCH 68/72] Update x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/actions.tsx Co-authored-by: Bena Kansara <69037875+benakansara@users.noreply.github.com> --- .../apps/synthetics/components/monitor_details/actions.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 8a5e42fa2f9f8..4ef6d15c36149 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 @@ -29,7 +29,7 @@ export function Actions() { data-test-subj="monitorDetailsHeaderControlPopover" button={ Date: Mon, 21 Jul 2025 11:43:05 -0400 Subject: [PATCH 69/72] adjust tests --- .../attachments_framework/registered_persistable_state_basic.ts | 1 - .../attachments_framework/registered_persistable_state_trial.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/basic/attachments_framework/registered_persistable_state_basic.ts b/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/basic/attachments_framework/registered_persistable_state_basic.ts index 40bb8654db968..8c4e2e991117f 100644 --- a/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/basic/attachments_framework/registered_persistable_state_basic.ts +++ b/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/basic/attachments_framework/registered_persistable_state_basic.ts @@ -33,7 +33,6 @@ export default ({ getService }: FtrProviderContext): void => { expect(types).to.eql({ '.lens': '78559fd806809ac3a1008942ead2a079864054f5', - '.page': '8e5143e79705c8cebe2d5d9949e22d52944629d0', '.test': 'ab2204830c67f5cf992c9aa2f7e3ead752cc60a1', }); }); diff --git a/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/trial/attachments_framework/registered_persistable_state_trial.ts b/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/trial/attachments_framework/registered_persistable_state_trial.ts index 1a27847e59a51..9cb7ecd778eab 100644 --- a/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/trial/attachments_framework/registered_persistable_state_trial.ts +++ b/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/trial/attachments_framework/registered_persistable_state_trial.ts @@ -33,7 +33,6 @@ export default ({ getService }: FtrProviderContext): void => { expect(types).to.eql({ '.lens': '78559fd806809ac3a1008942ead2a079864054f5', - '.page': '8e5143e79705c8cebe2d5d9949e22d52944629d0', '.test': 'ab2204830c67f5cf992c9aa2f7e3ead752cc60a1', aiopsChangePointChart: 'a1212d71947ec34487b374cecc47ab9941b5d91c', ml_anomaly_charts: '23e92e824af9db6e8b8bb1d63c222e04f57d2147', From 8c045a8bc3345f5537e5606af7e63000a143cbc2 Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Mon, 21 Jul 2025 14:58:33 -0400 Subject: [PATCH 70/72] Update x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/actions.test.tsx --- .../apps/synthetics/components/monitor_details/actions.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e73ace1c63bf0..b05e46aa44edd 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 @@ -79,7 +79,7 @@ describe('Actions Component', () => { render(); // Open the popover - fireEvent.click(screen.getByTestId('o11yHeaderControlActionsButton')); + fireEvent.click(screen.getByTestId('monitorDetailsHeaderControlActionsButton')); expect(screen.getByText('Edit monitor')).toBeInTheDocument(); expect(screen.getByText('Refresh')).toBeInTheDocument(); From 8ef4b60309744b65518a30bdae4ae169529cb683 Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Mon, 21 Jul 2025 14:58:53 -0400 Subject: [PATCH 71/72] Update x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/actions.test.tsx --- .../apps/synthetics/components/monitor_details/actions.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 b05e46aa44edd..9917e5019acc2 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 @@ -127,7 +127,7 @@ describe('Actions Component', () => { render(); // Open the popover - fireEvent.click(screen.getByTestId('o11yHeaderControlActionsButton')); + fireEvent.click(screen.getByTestId('monitorDetailsHeaderControlActionsButton')); expect(screen.getByText('Edit monitor')).toBeInTheDocument(); expect(screen.getByText('Refresh')).toBeInTheDocument(); From dbb5783bbfc7a2587ddece554add9346ae498d3a Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Mon, 21 Jul 2025 14:59:16 -0400 Subject: [PATCH 72/72] Update x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/actions.test.tsx --- .../apps/synthetics/components/monitor_details/actions.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 9917e5019acc2..61eba82c7b962 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 @@ -175,7 +175,7 @@ describe('Actions Component', () => { render(); // Open the popover - fireEvent.click(screen.getByTestId('o11yHeaderControlActionsButton')); + fireEvent.click(screen.getByTestId('monitorDetailsHeaderControlActionsButton')); expect(screen.getByText('Edit monitor')).toBeInTheDocument(); expect(screen.getByText('Refresh')).toBeInTheDocument();