From ddac7500d4a163fad7fe0083ccce192e7bf9d2f9 Mon Sep 17 00:00:00 2001 From: Panagiota Mitsopoulou Date: Sat, 11 Apr 2026 22:28:28 +0300 Subject: [PATCH] feat(synthetics): add CCS remote monitor support to overview and detail pages Server: skip space_id filter when CCS enabled, add second-pass collapsed query to detect remote monitors with no local saved object, populate remote field on OverviewStatusMetaData. UI: add MonitorRemoteBadge, MonitorRemoteCallout, and deep-link utility. Show remote badges on overview cards and compact table, restrict actions for remote monitors, display remote callout on monitor details page. --- .../components/monitor_remote_badge.tsx | 50 ++++ .../components/monitor_remote_callout.tsx | 67 +++++ .../hooks/use_monitor_remote_info.ts | 28 +++ .../monitor_details_page_title.tsx | 6 + .../monitor_summary/monitor_summary.tsx | 9 + .../overview/overview/actions_popover.tsx | 236 +++++++++--------- .../hooks/use_monitors_table_columns.tsx | 14 +- .../overview/metric_item/metric_item_body.tsx | 17 +- .../synthetics/utils/remote_monitor_urls.ts | 37 +++ 9 files changed, 344 insertions(+), 120 deletions(-) create mode 100644 x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/common/components/monitor_remote_badge.tsx create mode 100644 x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/common/components/monitor_remote_callout.tsx create mode 100644 x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_monitor_remote_info.ts create mode 100644 x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/utils/remote_monitor_urls.ts diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/common/components/monitor_remote_badge.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/common/components/monitor_remote_badge.tsx new file mode 100644 index 0000000000000..b13c4d5501d7e --- /dev/null +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/common/components/monitor_remote_badge.tsx @@ -0,0 +1,50 @@ +/* + * 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 { EuiBadge, EuiFlexItem, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import type { MouseEvent } from 'react'; +import React from 'react'; +import type { RemoteMonitorInfo } from '../../../../../../common/runtime_types/remote'; +import { useKibanaSpace } from '../../../../../hooks/use_kibana_space'; +import { createRemoteMonitorDetailsUrl } from '../../../utils/remote_monitor_urls'; + +export function MonitorRemoteBadge({ + remote, + configId, +}: { + remote?: RemoteMonitorInfo; + configId: string; +}) { + const { space } = useKibanaSpace(); + + if (!remote) { + return null; + } + + const detailsUrl = createRemoteMonitorDetailsUrl(remote, configId, space?.id); + + return ( + + + { + e.stopPropagation(); + }} + > + {i18n.translate('xpack.synthetics.remoteBadge.label', { + defaultMessage: 'Remote', + })} + + + + ); +} diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/common/components/monitor_remote_callout.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/common/components/monitor_remote_callout.tsx new file mode 100644 index 0000000000000..e526169b209dc --- /dev/null +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/common/components/monitor_remote_callout.tsx @@ -0,0 +1,67 @@ +/* + * 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 { EuiButton, EuiCallOut, EuiLink } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import React from 'react'; +import type { RemoteMonitorInfo } from '../../../../../../common/runtime_types/remote'; +import { useKibanaSpace } from '../../../../../hooks/use_kibana_space'; +import { createRemoteMonitorDetailsUrl } from '../../../utils/remote_monitor_urls'; + +export function MonitorRemoteCallout({ + remote, + configId, +}: { + remote: RemoteMonitorInfo; + configId: string; +}) { + const { space } = useKibanaSpace(); + const detailsUrl = createRemoteMonitorDetailsUrl(remote, configId, space?.id); + + return ( + +

+ {remote.remoteName}, + kibanaUrl: ( + + {remote.kibanaUrl} + + ), + }} + /> +

+ {detailsUrl && ( + + {i18n.translate('xpack.synthetics.monitorDetails.remoteCallout.viewButton', { + defaultMessage: 'View on remote instance', + })} + + )} +
+ ); +} diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_monitor_remote_info.ts b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_monitor_remote_info.ts new file mode 100644 index 0000000000000..235e0fb0cb078 --- /dev/null +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_monitor_remote_info.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 { useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import type { RemoteMonitorInfo } from '../../../../../../common/runtime_types/remote'; +import { selectOverviewStatus } from '../../../state/overview_status'; + +/** + * Looks up remote monitor info from the overview status state for a given configId. + * Returns the RemoteMonitorInfo if the monitor was fetched from a remote cluster, + * or undefined if it is a local monitor. + */ +export function useMonitorRemoteInfo(configId: string): RemoteMonitorInfo | undefined { + const { allConfigs } = useSelector(selectOverviewStatus); + + return useMemo(() => { + if (!allConfigs) { + return undefined; + } + const match = allConfigs.find((config) => config.configId === configId); + return match?.remote; + }, [allConfigs, configId]); +} diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_details_page_title.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_details_page_title.tsx index b03ca571fff2d..5f48735eba299 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_details_page_title.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_details_page_title.tsx @@ -7,17 +7,23 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { useParams } from 'react-router-dom'; import { MonitorSelector } from './monitor_selector/monitor_selector'; import { useSelectedMonitor } from './hooks/use_selected_monitor'; +import { useMonitorRemoteInfo } from './hooks/use_monitor_remote_info'; +import { MonitorRemoteBadge } from '../common/components/monitor_remote_badge'; export const MonitorDetailsPageTitle = () => { + const { monitorId } = useParams<{ monitorId: string }>(); const { monitor } = useSelectedMonitor(); + const remoteInfo = useMonitorRemoteInfo(monitorId); return ( {monitor?.name} + {remoteInfo && } diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_summary.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_summary.tsx index e902473c4e182..d0d7928c69ed2 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_summary.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_summary.tsx @@ -19,11 +19,13 @@ import { i18n } from '@kbn/i18n'; import { useParams } from 'react-router-dom'; import { LoadWhenInView } from '@kbn/observability-shared-plugin/public'; import { MonitorMWsCallout } from '../../common/mws_callout/monitor_mws_callout'; +import { MonitorRemoteCallout } from '../../common/components/monitor_remote_callout'; import { MissingIntegrationCallout } from '../../monitor_add_edit/steps/missing_integration_callout'; import { SummaryPanel } from './summary_panel'; import { useMonitorDetailsPage } from '../use_monitor_details_page'; import { useMonitorRangeFrom } from '../hooks/use_monitor_range_from'; +import { useMonitorRemoteInfo } from '../hooks/use_monitor_remote_info'; import { MonitorAlerts } from './monitor_alerts'; import { MonitorStatusPanel } from '../monitor_status/monitor_status_panel'; import { MonitorDurationTrend } from './duration_trend'; @@ -37,6 +39,7 @@ import { useMonitorAttachmentConfig } from '../hooks/use_monitor_attachment_conf export const MonitorSummary = () => { const { monitorId: configId } = useParams<{ monitorId: string }>(); const { from, to } = useMonitorRangeFrom(); + const remoteInfo = useMonitorRemoteInfo(configId); const dateLabel = from === 'now-30d/d' ? LAST_30_DAYS_LABEL : TO_DATE_LABEL; const isMediumDevice = useIsWithinBreakpoints(['xs', 's', 'm', 'l']); @@ -52,6 +55,12 @@ export const MonitorSummary = () => { return ( <> + {remoteInfo && ( + <> + + + + )} diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/actions_popover.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/actions_popover.tsx index f6099d2b38e30..a0a297a36f5d4 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/actions_popover.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/actions_popover.tsx @@ -203,6 +203,8 @@ export function ActionsPopover({ }); const alertLoading = alertStatus(monitor.configId) === FETCH_STATUS.LOADING; + const isRemoteMonitor = !!monitor.remote; + let popoverItems: EuiContextMenuPanelItemDescriptor[] = [ { name: actionsMenuGoToMonitorName, @@ -211,121 +213,127 @@ export function ActionsPopover({ 'data-test-subj': 'actionsPopoverGoToMonitor', }, quickInspectPopoverItem, - { - name: testInProgress ? ( - - {runTestManually} - - ) : ( - - {runTestManually} - - ), - icon: 'flask', - disabled: testInProgress || !canUsePublicLocations || !isServiceAllowed, - onClick: () => { - dispatch(manualTestMonitorAction.get({ configId: monitor.configId, name: monitor.name })); - dispatch(setFlyoutConfig(null)); - setIsPopoverOpen(false); - }, - }, - { - name: ( - - {actionsMenuEditMonitorName} - - ), - icon: 'pencil', - disabled: !canEditSynthetics || !isServiceAllowed, - href: editUrl, - 'data-test-subj': 'editMonitorLink', - }, - { - name: ( - - {actionsMenuCloneMonitorName} - - ), - icon: 'copy', - disabled: !canEditSynthetics || !isServiceAllowed, - href: http?.basePath.prepend(`synthetics/add-monitor?cloneId=${monitor.configId}`), - 'data-test-subj': 'cloneMonitorLink', - }, - { - name: ( - - {CREATE_SLO} - - ), - icon: 'chartGauge', - disabled: !canEditSynthetics || !isServiceAllowed, - onClick: () => { - setIsPopoverOpen(false); - setIsSLOFlyoutOpen(true); - }, - 'data-test-subj': 'createSLOBtn', - }, - { - name: ( - - {enableLabel} - - ), - icon: 'contrast', - disabled: !canEditSynthetics || !canUsePublicLocations, - onClick: () => { - if (status !== FETCH_STATUS.LOADING) { - updateMonitorEnabledState(!monitor.isEnabled); - } - }, - }, - { - name: ( - - {monitor.isStatusAlertEnabled ? disableAlertLabel : enableMonitorAlertLabel} - - ), - disabled: !canEditSynthetics || !canUsePublicLocations || !isServiceAllowed, - icon: alertLoading ? ( - - ) : monitor.isStatusAlertEnabled ? ( - 'bellSlash' - ) : ( - 'bell' - ), - onClick: () => { - if (!alertLoading) { - updateAlertEnabledState({ - monitor: { - [ConfigKey.ALERT_CONFIG]: toggleStatusAlert({ - status: { - enabled: monitor.isStatusAlertEnabled, - }, - }), + ...(!isRemoteMonitor + ? ([ + { + name: testInProgress ? ( + + {runTestManually} + + ) : ( + + {runTestManually} + + ), + icon: 'flask', + disabled: testInProgress || !canUsePublicLocations || !isServiceAllowed, + onClick: () => { + dispatch( + manualTestMonitorAction.get({ configId: monitor.configId, name: monitor.name }) + ); + dispatch(setFlyoutConfig(null)); + setIsPopoverOpen(false); }, - configId: monitor.configId, - name: monitor.name, - }); - } - }, - }, - { - name: addMonitorToDashboardLabel, - icon: 'dashboardApp', - onClick: () => { - setIsPopoverOpen(false); - setDashboardAttachmentReady(true); - }, - }, + }, + { + name: ( + + {actionsMenuEditMonitorName} + + ), + icon: 'pencil', + disabled: !canEditSynthetics || !isServiceAllowed, + href: editUrl, + 'data-test-subj': 'editMonitorLink', + }, + { + name: ( + + {actionsMenuCloneMonitorName} + + ), + icon: 'copy', + disabled: !canEditSynthetics || !isServiceAllowed, + href: http?.basePath.prepend(`synthetics/add-monitor?cloneId=${monitor.configId}`), + 'data-test-subj': 'cloneMonitorLink', + }, + { + name: ( + + {CREATE_SLO} + + ), + icon: 'chartGauge', + disabled: !canEditSynthetics || !isServiceAllowed, + onClick: () => { + setIsPopoverOpen(false); + setIsSLOFlyoutOpen(true); + }, + 'data-test-subj': 'createSLOBtn', + }, + { + name: ( + + {enableLabel} + + ), + icon: 'contrast', + disabled: !canEditSynthetics || !canUsePublicLocations, + onClick: () => { + if (status !== FETCH_STATUS.LOADING) { + updateMonitorEnabledState(!monitor.isEnabled); + } + }, + }, + { + name: ( + + {monitor.isStatusAlertEnabled ? disableAlertLabel : enableMonitorAlertLabel} + + ), + disabled: !canEditSynthetics || !canUsePublicLocations || !isServiceAllowed, + icon: alertLoading ? ( + + ) : monitor.isStatusAlertEnabled ? ( + 'bellSlash' + ) : ( + 'bell' + ), + onClick: () => { + if (!alertLoading) { + updateAlertEnabledState({ + monitor: { + [ConfigKey.ALERT_CONFIG]: toggleStatusAlert({ + status: { + enabled: monitor.isStatusAlertEnabled, + }, + }), + }, + configId: monitor.configId, + name: monitor.name, + }); + } + }, + }, + { + name: addMonitorToDashboardLabel, + icon: 'dashboardApp', + onClick: () => { + setIsPopoverOpen(false); + setDashboardAttachmentReady(true); + }, + }, + ] as EuiContextMenuPanelItemDescriptor[]) + : []), ]; if (isInspectView) popoverItems = popoverItems.filter((i) => i !== quickInspectPopoverItem); diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/compact_view/hooks/use_monitors_table_columns.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/compact_view/hooks/use_monitors_table_columns.tsx index c7756ad377074..c7da8453db709 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/compact_view/hooks/use_monitors_table_columns.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/compact_view/hooks/use_monitors_table_columns.tsx @@ -19,6 +19,7 @@ import { MonitorBarSeries } from '../components/monitor_bar_series'; import { useMonitorHistogram } from '../../../../hooks/use_monitor_histogram'; import type { OverviewStatusMetaData } from '../../../../../../../../../common/runtime_types'; import { MonitorTypeBadge } from '../../../../../common/components/monitor_type_badge'; +import { MonitorRemoteBadge } from '../../../../../common/components/monitor_remote_badge'; import { getFilterForTypeMessage } from '../../../../management/monitor_list_table/labels'; import type { FlyoutParamProps } from '../../types'; import { MonitorsActions } from '../components/monitors_actions'; @@ -104,9 +105,16 @@ export const useMonitorsTableColumns = ({ className="clickCellContent" > - openFlyout(monitor)}> - {name} - + + + openFlyout(monitor)}> + {name} + + + {monitor.remote && ( + + )} + ); + + const badges = ( + <> + {typeBadge} + {monitor.remote && } + + ); + if (tags.length === 0) { return ( <> - {typeBadge} + + {badges} + ); } @@ -42,7 +53,7 @@ export const MetricItemBody = ({ monitor }: { monitor: OverviewStatusMetaData }) {(tags ?? []).length > 0 && ( {typeBadge}} + prependChildren={badges} color="default" tags={tags} disableExpand={true} diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/utils/remote_monitor_urls.ts b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/utils/remote_monitor_urls.ts new file mode 100644 index 0000000000000..7d03b3e2f2746 --- /dev/null +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/utils/remote_monitor_urls.ts @@ -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 type { RemoteMonitorInfo } from '../../../../common/runtime_types/remote'; + +/** + * Builds a base URL for a remote monitor details page on the remote Kibana instance. + */ +function createBaseRemoteMonitorDetailsUrl( + remote: RemoteMonitorInfo, + configId: string, + spaceId: string = 'default' +): URL | undefined { + if (!remote || remote.kibanaUrl === '') { + return undefined; + } + + const spacePath = spaceId !== 'default' ? `/s/${spaceId}` : ''; + const detailsPath = `/app/synthetics/monitor/${configId}`; + + return new URL(`${spacePath}${detailsPath}`, remote.kibanaUrl); +} + +/** + * Creates a URL string for viewing a remote monitor's details page on the remote Kibana instance. + */ +export function createRemoteMonitorDetailsUrl( + remote: RemoteMonitorInfo, + configId: string, + spaceId: string = 'default' +): string | undefined { + return createBaseRemoteMonitorDetailsUrl(remote, configId, spaceId)?.toString(); +}