[Synthetics] Changed embeddable view when only one monitor in one location is selected (#218402)

This PR closes #208981 by adding a new action to the Monitor card to
view only that monitor in the dashboard.



https://github.com/user-attachments/assets/f500d220-b57f-4c43-a632-b2383e33988e

---------

Co-authored-by: Shahzad <shahzad31comp@gmail.com>
This commit is contained in:
Francesco Fagnani 2025-04-17 14:03:40 +02:00 committed by GitHub
parent 431116a33a
commit ec939b6718
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 217 additions and 65 deletions

View file

@ -51,7 +51,7 @@ export const getMonitorsEmbeddableFactory = (
deserializeState: (state) => {
return state.rawState as OverviewEmbeddableState;
},
buildEmbeddable: async (state, buildApi, uuid, parentApi) => {
buildEmbeddable: async (state, buildApi) => {
const [coreStart, pluginStart] = await getStartServices();
const titleManager = initializeTitleManager(state);

View file

@ -5,17 +5,28 @@
* 2.0.
*/
import React, { useEffect, useRef } from 'react';
import React, { useCallback, useEffect, useRef } from 'react';
import { Subject } from 'rxjs';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { areFiltersEmpty } from '../common/utils';
import { getOverviewStore } from './redux_store';
import { ShowSelectedFilters } from '../common/show_selected_filters';
import { setOverviewPageStateAction } from '../../synthetics/state';
import {
selectOverviewTrends,
setFlyoutConfig,
setOverviewPageStateAction,
trendStatsBatch,
} from '../../synthetics/state';
import { MonitorFilters } from './types';
import { EmbeddablePanelWrapper } from '../../synthetics/components/common/components/embeddable_panel_wrapper';
import { SyntheticsEmbeddableContext } from '../synthetics_embeddable_context';
import { OverviewGrid } from '../../synthetics/components/monitors_page/overview/overview/overview_grid';
import { useMonitorsSortedByStatus } from '../../synthetics/hooks/use_monitors_sorted_by_status';
import { MetricItem } from '../../synthetics/components/monitors_page/overview/overview/metric_item/metric_item';
import { FlyoutParamProps } from '../../synthetics/components/monitors_page/overview/overview/types';
import { MaybeMonitorDetailsFlyout } from '../../synthetics/components/monitors_page/overview/overview/monitor_detail_flyout';
import { useOverviewStatus } from '../../synthetics/components/monitors_page/hooks/use_overview_status';
import { OverviewLoader } from '../../synthetics/components/monitors_page/overview/overview/overview_loader';
export const StatusGridComponent = ({
reload$,
@ -27,19 +38,83 @@ export const StatusGridComponent = ({
const overviewStore = useRef(getOverviewStore());
const hasFilters = !areFiltersEmpty(filters);
const singleMonitor =
filters && filters.locations.length === 1 && filters.monitorIds.length === 1;
return (
const monitorOverviewListComponent = (
<SyntheticsEmbeddableContext reload$={reload$} reduxStore={overviewStore.current}>
<MonitorsOverviewList filters={filters} singleMonitor={singleMonitor} />
</SyntheticsEmbeddableContext>
);
return singleMonitor ? (
monitorOverviewListComponent
) : (
<EmbeddablePanelWrapper
titleAppend={hasFilters ? <ShowSelectedFilters filters={filters ?? {}} /> : null}
>
<SyntheticsEmbeddableContext reload$={reload$} reduxStore={overviewStore.current}>
<MonitorsOverviewList filters={filters} />
</SyntheticsEmbeddableContext>
{monitorOverviewListComponent}
</EmbeddablePanelWrapper>
);
};
const MonitorsOverviewList = ({ filters }: { filters: MonitorFilters }) => {
const SingleMonitorView = () => {
const trendData = useSelector(selectOverviewTrends);
const dispatch = useDispatch();
const setFlyoutConfigCallback = useCallback(
(params: FlyoutParamProps) => {
dispatch(setFlyoutConfig(params));
},
[dispatch]
);
const { loaded } = useOverviewStatus({
scopeStatusByLocation: true,
});
const monitorsSortedByStatus = useMonitorsSortedByStatus();
if (loaded && monitorsSortedByStatus.length !== 1) {
throw new Error(
'One and only one monitor should always be returned by useMonitorsSortedByStatus in this component, this should never happen'
);
}
const monitor = monitorsSortedByStatus.length === 1 ? monitorsSortedByStatus[0] : undefined;
useEffect(() => {
if (monitor && !trendData[monitor.configId + monitor.locationId]) {
dispatch(
trendStatsBatch.get([
{
configId: monitor.configId,
locationId: monitor.locationId,
schedule: monitor.schedule,
},
])
);
}
}, [dispatch, monitor, trendData]);
const style = { height: '100%' };
if (!monitor) return <OverviewLoader rows={1} columns={1} style={style} />;
return (
<>
<MetricItem monitor={monitor} onClick={setFlyoutConfigCallback} style={style} />
<MaybeMonitorDetailsFlyout setFlyoutConfigCallback={setFlyoutConfigCallback} />
</>
);
};
const MonitorsOverviewList = ({
filters,
singleMonitor,
}: {
filters: MonitorFilters;
singleMonitor?: boolean;
}) => {
const dispatch = useDispatch();
useEffect(() => {
if (!filters) return;
@ -54,5 +129,9 @@ const MonitorsOverviewList = ({ filters }: { filters: MonitorFilters }) => {
);
}, [dispatch, filters]);
if (singleMonitor) {
return <SingleMonitorView />;
}
return <OverviewGrid />;
};

View file

@ -28,25 +28,28 @@ import {
const SavedObjectSaveModalDashboard = withSuspense(LazySavedObjectSaveModalDashboard);
export const AddToDashboard = ({
export const useAddToDashboard = ({
type,
asButton = false,
embeddableInput = {},
objectType = i18n.translate('xpack.synthetics.item.actions.addToDashboard.objectTypeLabel', {
defaultMessage: 'Status Overview',
}),
documentTitle = i18n.translate('xpack.synthetics.item.actions.addToDashboard.attachmentTitle', {
defaultMessage: 'Status Overview',
}),
}: {
type: typeof SYNTHETICS_STATS_OVERVIEW_EMBEDDABLE | typeof SYNTHETICS_MONITORS_EMBEDDABLE;
asButton?: boolean;
embeddableInput?: Record<string, unknown>;
objectType?: string;
documentTitle?: string;
}) => {
const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);
const [isDashboardAttachmentReady, setDashboardAttachmentReady] = React.useState(false);
const closePopover = () => {
setIsPopoverOpen(false);
};
const { embeddable } = useKibana<ClientPluginsStart>().services;
const handleAttachToDashboardSave: SaveModalDashboardProps['onSave'] = useCallback(
({ dashboardId, newTitle, newDescription }) => {
({ dashboardId }) => {
const stateTransfer = embeddable.getStateTransfer();
const embeddableInput = {};
const state = {
input: embeddableInput,
@ -60,8 +63,42 @@ export const AddToDashboard = ({
path,
});
},
[embeddable, type]
[embeddable, type, embeddableInput]
);
const MaybeSavedObjectSaveModalDashboard = isDashboardAttachmentReady ? (
<SavedObjectSaveModalDashboard
objectType={objectType}
documentInfo={{
title: documentTitle,
}}
canSaveByReference={false}
onClose={() => {
setDashboardAttachmentReady(false);
}}
onSave={handleAttachToDashboardSave}
/>
) : null;
return { setDashboardAttachmentReady, MaybeSavedObjectSaveModalDashboard };
};
export const AddToDashboard = ({
type,
asButton = false,
}: {
type: typeof SYNTHETICS_STATS_OVERVIEW_EMBEDDABLE | typeof SYNTHETICS_MONITORS_EMBEDDABLE;
asButton?: boolean;
}) => {
const { setDashboardAttachmentReady, MaybeSavedObjectSaveModalDashboard } = useAddToDashboard({
type,
});
const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);
const closePopover = () => {
setIsPopoverOpen(false);
};
const isSyntheticsApp = window.location.pathname.includes('/app/synthetics');
if (!isSyntheticsApp) {
@ -123,26 +160,7 @@ export const AddToDashboard = ({
/>
</EuiPopover>
)}
{isDashboardAttachmentReady ? (
<SavedObjectSaveModalDashboard
objectType={i18n.translate(
'xpack.synthetics.item.actions.addToDashboard.objectTypeLabel',
{
defaultMessage: 'Status Overview',
}
)}
documentInfo={{
title: i18n.translate('xpack.synthetics.item.actions.addToDashboard.attachmentTitle', {
defaultMessage: 'Status Overview',
}),
}}
canSaveByReference={false}
onClose={() => {
setDashboardAttachmentReady(false);
}}
onSave={handleAttachToDashboardSave}
/>
) : null}
{MaybeSavedObjectSaveModalDashboard}
</>
);
};

View file

@ -20,6 +20,7 @@ import { FETCH_STATUS } from '@kbn/observability-shared-plugin/public';
import { useDispatch, useSelector } from 'react-redux';
import styled from 'styled-components';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { SYNTHETICS_MONITORS_EMBEDDABLE } from '../../../../../embeddables/constants';
import { useCreateSLO } from '../../hooks/use_create_slo';
import { TEST_SCHEDULED_LABEL } from '../../../monitor_add_edit/form/run_test_btn';
import { useCanUsePublicLocById } from '../../hooks/use_can_use_public_loc_id';
@ -36,6 +37,7 @@ import { setFlyoutConfig } from '../../../../state/overview/actions';
import { useEditMonitorLocator } from '../../../../hooks/use_edit_monitor_locator';
import { useMonitorDetailLocator } from '../../../../hooks/use_monitor_detail_locator';
import { NoPermissionsTooltip } from '../../../common/components/permissions';
import { useAddToDashboard } from '../../../common/components/add_to_dashboard';
type PopoverPosition = 'relative' | 'default';
@ -178,6 +180,23 @@ export function ActionsPopover({
},
};
const { MaybeSavedObjectSaveModalDashboard, setDashboardAttachmentReady } = useAddToDashboard({
type: SYNTHETICS_MONITORS_EMBEDDABLE,
embeddableInput: {
filters: {
monitorIds: [{ label: monitor.name, value: monitor.configId }],
tags: [],
locations: [{ label: monitor.locationLabel, value: monitor.locationId }],
monitorTypes: [],
projects: [],
},
},
documentTitle: `${monitor.name} - ${monitor.locationLabel}`,
objectType: i18n.translate('xpack.synthetics.overview.actions.addToDashboard.objectTypeLabel', {
defaultMessage: 'Monitor Overview',
}),
});
const alertLoading = alertStatus(monitor.configId) === FETCH_STATUS.LOADING;
let popoverItems: EuiContextMenuPanelItemDescriptor[] = [
{
@ -291,6 +310,14 @@ export function ActionsPopover({
}
},
},
{
name: addMonitorToDashboardLabel,
icon: 'dashboardApp',
onClick: () => {
setIsPopoverOpen(false);
setDashboardAttachmentReady(true);
},
},
];
if (isInspectView) popoverItems = popoverItems.filter((i) => i !== quickInspectPopoverItem);
@ -331,6 +358,7 @@ export function ActionsPopover({
</EuiPopover>
</Container>
{CreateSLOFlyout}
{MaybeSavedObjectSaveModalDashboard}
</>
);
}
@ -421,6 +449,13 @@ const enableMonitorAlertLabel = i18n.translate(
}
);
const addMonitorToDashboardLabel = i18n.translate(
'xpack.synthetics.overview.actions.addToDashboard',
{
defaultMessage: 'Add to dashboard',
}
);
const enabledSuccessLabel = (name: string) =>
i18n.translate('xpack.synthetics.overview.actions.enabledSuccessLabel', {
defaultMessage: 'Monitor "{name}" enabled successfully',

View file

@ -38,6 +38,7 @@ import { LocationsStatus, useStatusByLocation } from '../../../../hooks/use_stat
import {
getMonitorAction,
selectMonitorUpsertStatus,
selectOverviewState,
selectServiceLocationsState,
selectSyntheticsMonitor,
selectSyntheticsMonitorError,
@ -52,6 +53,7 @@ import { MonitorEnabled } from '../../management/monitor_list_table/monitor_enab
import { ConfigKey, EncryptedSyntheticsMonitor, OverviewStatusMetaData } from '../types';
import { ActionsPopover } from './actions_popover';
import { FlyoutParamProps } from './types';
import { quietFetchOverviewStatusAction } from '../../../../state/overview_status';
interface Props {
configId: string;
@ -209,7 +211,7 @@ function DetailedFlyoutHeader({
export function LoadingState() {
return (
<EuiFlexGroup alignItems="center" justifyContent="center" style={{ height: '100%' }}>
<EuiFlexGroup alignItems="center" justifyContent="center" css={{ height: '100%' }}>
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="xl" />
</EuiFlexItem>
@ -370,6 +372,34 @@ export function MonitorDetailFlyout(props: Props) {
);
}
export const MaybeMonitorDetailsFlyout = ({
setFlyoutConfigCallback,
}: {
setFlyoutConfigCallback: (params: FlyoutParamProps) => void;
}) => {
const dispatch = useDispatch();
const { flyoutConfig, pageState } = useSelector(selectOverviewState);
const hideFlyout = useCallback(() => dispatch(setFlyoutConfig(null)), [dispatch]);
const forceRefreshCallback = useCallback(
() => dispatch(quietFetchOverviewStatusAction.get({ pageState })),
[dispatch, pageState]
);
return flyoutConfig?.configId && flyoutConfig?.location ? (
<MonitorDetailFlyout
configId={flyoutConfig.configId}
id={flyoutConfig.id}
location={flyoutConfig.location}
locationId={flyoutConfig.locationId}
spaceId={flyoutConfig.spaceId}
onClose={hideFlyout}
onEnabledChange={forceRefreshCallback}
onLocationChange={setFlyoutConfigCallback}
/>
) : null;
};
const DURATION_HEADER_TEXT = i18n.translate('xpack.synthetics.monitorList.durationHeaderText', {
defaultMessage: 'Duration',
});

View file

@ -21,7 +21,6 @@ import {
import { MetricItem } from './metric_item/metric_item';
import { ShowAllSpaces } from '../../common/show_all_spaces';
import { OverviewStatusMetaData } from '../../../../../../../common/runtime_types';
import { quietFetchOverviewStatusAction } from '../../../../state/overview_status';
import type { TrendRequest } from '../../../../../../../common/types';
import { SYNTHETICS_MONITORS_EMBEDDABLE } from '../../../../../embeddables/constants';
import { AddToDashboard } from '../../../common/components/add_to_dashboard';
@ -39,9 +38,9 @@ import { OverviewLoader } from './overview_loader';
import { OverviewPaginationInfo } from './overview_pagination_info';
import { SortFields } from './sort_fields';
import { NoMonitorsFound } from '../../common/no_monitors_found';
import { MonitorDetailFlyout } from './monitor_detail_flyout';
import { useSyntheticsRefreshContext } from '../../../../contexts';
import { FlyoutParamProps } from './types';
import { MaybeMonitorDetailsFlyout } from './monitor_detail_flyout';
const ITEM_HEIGHT = 172;
const ROW_COUNT = 4;
@ -61,7 +60,6 @@ export const OverviewGrid = memo(() => {
const monitorsSortedByStatus: OverviewStatusMetaData[] = useMonitorsSortedByStatus();
const {
flyoutConfig,
pageState,
groupBy: { field: groupField },
} = useSelector(selectOverviewState);
@ -77,12 +75,7 @@ export const OverviewGrid = memo(() => {
(params: FlyoutParamProps) => dispatch(setFlyoutConfig(params)),
[dispatch]
);
const hideFlyout = useCallback(() => dispatch(setFlyoutConfig(null)), [dispatch]);
const { lastRefresh } = useSyntheticsRefreshContext();
const forceRefreshCallback = useCallback(
() => dispatch(quietFetchOverviewStatusAction.get({ pageState })),
[dispatch, pageState]
);
useEffect(() => {
if (monitorsSortedByStatus.length) {
@ -186,7 +179,7 @@ export const OverviewGrid = memo(() => {
<EuiFlexGroup
data-test-subj={`overview-grid-row-${listIndex}`}
gutterSize="m"
style={{ ...style }}
css={{ ...style }}
>
{listData[listIndex].map((_, idx) => (
<EuiFlexItem
@ -252,18 +245,7 @@ export const OverviewGrid = memo(() => {
</EuiFlexGroup>
</>
)}
{flyoutConfig?.configId && flyoutConfig?.location && (
<MonitorDetailFlyout
configId={flyoutConfig.configId}
id={flyoutConfig.id}
location={flyoutConfig.location}
locationId={flyoutConfig.locationId}
spaceId={flyoutConfig.spaceId}
onClose={hideFlyout}
onEnabledChange={forceRefreshCallback}
onLocationChange={setFlyoutConfigCallback}
/>
)}
<MaybeMonitorDetailsFlyout setFlyoutConfigCallback={setFlyoutConfigCallback} />
</>
);
});

View file

@ -6,15 +6,23 @@
*/
import React from 'react';
import { EuiFlexGrid, EuiFlexItem } from '@elastic/eui';
import { EuiFlexGrid, EuiFlexItem, EuiFlexGridProps } from '@elastic/eui';
import { OverviewGridItemLoader } from './overview_grid_item_loader';
export const OverviewLoader = ({ rows }: { rows?: number }) => {
export const OverviewLoader = ({
rows,
columns,
style,
}: {
rows?: number;
columns?: EuiFlexGridProps['columns'];
style?: { height: string };
}) => {
const ROWS = rows ?? 4;
const COLUMNS = 4;
const COLUMNS = columns ?? 4;
const loaders = Array(ROWS * COLUMNS).fill(null);
return (
<EuiFlexGrid gutterSize="m" columns={COLUMNS}>
<EuiFlexGrid gutterSize="m" columns={COLUMNS} css={style}>
{loaders.map((_, i) => (
<EuiFlexItem key={i}>
<OverviewGridItemLoader />