From 6528bb255b18413eca73d2a6bb9d0a2f07d2c6ae Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Wed, 26 Jun 2024 13:58:43 -0400 Subject: [PATCH 01/39] TEMP. --- package.json | 4 +- .../common/constants/synthetics/rest_api.ts | 1 + .../monitor_management/monitor_types.ts | 1 + .../overview/overview/metric_item.tsx | 200 +++++++++++++++--- .../overview/overview/overview_grid.tsx | 181 +++++++++++++--- .../overview/overview/overview_grid_item.tsx | 38 ++-- .../monitors_page/overview/overview_page.tsx | 13 +- .../hooks/use_last_50_duration_chart.ts | 3 + .../synthetics/hooks/use_last_x_checks.ts | 4 +- .../hooks/use_monitors_sorted_by_status.tsx | 9 +- .../hooks/use_status_by_location_overview.ts | 1 + .../apps/synthetics/state/overview/actions.ts | 19 ++ .../apps/synthetics/state/overview/api.ts | 7 + .../apps/synthetics/state/overview/effects.ts | 14 +- .../apps/synthetics/state/overview/index.ts | 14 ++ .../apps/synthetics/state/overview/models.ts | 4 + .../synthetics/state/overview/selectors.ts | 4 + .../apps/synthetics/state/root_effect.ts | 3 +- .../public/apps/synthetics/synthetics_app.tsx | 125 +++++------ .../public/apps/synthetics/utils/on_render.ts | 20 ++ .../synthetics/server/routes/index.ts | 2 + .../routes/overview_trends/fetch_trends.ts | 141 ++++++++++++ .../routes/overview_trends/overview_trends.ts | 73 +++++++ .../normalizers/common_fields.ts | 1 + yarn.lock | 15 +- 25 files changed, 747 insertions(+), 150 deletions(-) create mode 100644 x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/on_render.ts create mode 100644 x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/fetch_trends.ts create mode 100644 x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/overview_trends.ts diff --git a/package.json b/package.json index a64ea5684a838..38c65ed12fcac 100644 --- a/package.json +++ b/package.json @@ -1152,6 +1152,7 @@ "react-use": "^15.3.8", "react-virtualized": "^9.22.5", "react-window": "^1.8.10", + "react-window-infinite-loader": "^1.0.9", "reduce-reducers": "^1.0.4", "redux": "^4.2.1", "redux-actions": "^2.6.5", @@ -1757,7 +1758,8 @@ "xmlbuilder": "13.0.2", "yargs": "^15.4.1", "yarn-deduplicate": "^6.0.2", - "zod-to-json-schema": "^3.22.3" + "zod-to-json-schema": "^3.22.3", + "@types/react-window-infinite-loader": "^1.0.9" }, "packageManager": "yarn@1.22.21" } \ No newline at end of file diff --git a/x-pack/plugins/observability_solution/synthetics/common/constants/synthetics/rest_api.ts b/x-pack/plugins/observability_solution/synthetics/common/constants/synthetics/rest_api.ts index 0003360c707b6..23ac30a5ff7b1 100644 --- a/x-pack/plugins/observability_solution/synthetics/common/constants/synthetics/rest_api.ts +++ b/x-pack/plugins/observability_solution/synthetics/common/constants/synthetics/rest_api.ts @@ -26,6 +26,7 @@ export enum SYNTHETICS_API_URLS { SYNTHETICS_OVERVIEW = '/internal/synthetics/overview', PINGS = '/internal/synthetics/pings', PING_STATUSES = '/internal/synthetics/ping_statuses', + OVERVIEW_TRENDS = '/internal/synthetics/overview_trends', OVERVIEW_STATUS = `/internal/synthetics/overview_status`, INDEX_SIZE = `/internal/synthetics/index_size`, AGENT_POLICIES = `/internal/synthetics/agent_policies`, diff --git a/x-pack/plugins/observability_solution/synthetics/common/runtime_types/monitor_management/monitor_types.ts b/x-pack/plugins/observability_solution/synthetics/common/runtime_types/monitor_management/monitor_types.ts index 808e7ced0bdbb..178fbabf52c75 100644 --- a/x-pack/plugins/observability_solution/synthetics/common/runtime_types/monitor_management/monitor_types.ts +++ b/x-pack/plugins/observability_solution/synthetics/common/runtime_types/monitor_management/monitor_types.ts @@ -388,6 +388,7 @@ export const MonitorOverviewItemCodec = t.intersection([ schedule: t.string, }), t.partial({ + status: t.string, projectId: t.string, }), ]); diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx index 2a48ff0d2b991..92a4687ade752 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx @@ -5,10 +5,10 @@ * 2.0. */ import { i18n } from '@kbn/i18n'; -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { css } from '@emotion/react'; import { Chart, Settings, Metric, MetricTrendShape } from '@elastic/charts'; -import { EuiPanel, EuiIconTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiPanel, EuiIconTip, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { DARK_THEME } from '@elastic/charts'; import { useTheme } from '@kbn/observability-shared-plugin/public'; import { useDispatch, useSelector } from 'react-redux'; @@ -47,36 +47,38 @@ export const getColor = ( export const MetricItem = ({ monitor, - medianDuration, - maxDuration, - minDuration, - avgDuration, - data, + // medianDuration, + // maxDuration, + // minDuration, + // avgDuration, + // data, onClick, + style, }: { monitor: MonitorOverviewItem; - data: Array<{ x: number; y: number }>; - medianDuration: number; - avgDuration: number; - minDuration: number; - maxDuration: number; + // data: Array<{ x: number; y: number }>; + // medianDuration: number; + // avgDuration: number; + // minDuration: number; + // maxDuration: number; + style?: React.CSSProperties; onClick: (params: { id: string; configId: string; location: string; locationId: string }) => void; }) => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); const isErrorPopoverOpen = useSelector(selectErrorPopoverState); const locationName = useLocationName(monitor); - const { status, timestamp, ping, configIdByLocation } = useStatusByLocationOverview({ - configId: monitor.configId, - locationId: monitor.location.id, - }); + // const { status, timestamp, ping, configIdByLocation } = useStatusByLocationOverview({ + // configId: monitor.configId, + // locationId: monitor.location.id, + // }); const theme = useTheme(); const testInProgress = useSelector(manualTestRunInProgressSelector(monitor.configId)); const dispatch = useDispatch(); - return ( -
+ const contents = useMemo(() => { + return ( - + {/* + */} + + ), + valueFormatter: (d: number) => formatDuration(d), + color: getColor(theme, monitor.isEnabled, monitor.status), + }, + ], + ]} + /> + +
+ +
+ {/* {configIdByLocation && ( + + )} */} +
+ ); + }, [ + dispatch, + isErrorPopoverOpen, + isPopoverOpen, + locationName, + monitor, + onClick, + testInProgress, + theme, + ]); + + return ( +
+ {contents} +
+ ); + return ( +
+ { + if (isErrorPopoverOpen) { + dispatch(toggleErrorPopoverOpen(null)); + } + }} + css={css` + height: 100%; + overflow: hidden; + position: relative; + + & .cardItemActions_hover { + pointer-events: none; + opacity: 0; + &:focus-within { + pointer-events: auto; + opacity: 1; + } + } + &:hover .cardItemActions_hover { + pointer-events: auto; + opacity: 1; + } + `} + title={monitor.name} + // title={moment(timestamp).format('LLL')} + > + + { + if (testInProgress) { + dispatch(toggleTestNowFlyoutAction(monitor.configId)); + dispatch(toggleErrorPopoverOpen(null)); + } else { + dispatch(hideTestNowFlyoutAction()); + dispatch(toggleErrorPopoverOpen(null)); + } + if (!testInProgress && locationName) { + onClick({ + configId: monitor.configId, + id: monitor.id, + location: locationName, + locationId: monitor.location.id, + }); + } + }} + // TODO connect to charts.theme service see src/plugins/charts/public/services/theme/README.md + baseTheme={DARK_THEME} + locale={i18n.getLocale()} + /> + + + {i18n.translate('xpack.synthetics.overview.duration.label', { + defaultMessage: 'Duration', + })} + {/* + + */} ), valueFormatter: (d: number) => formatDuration(d), - color: getColor(theme, monitor.isEnabled, status), + color: getColor(theme, monitor.isEnabled, monitor.status), }, ], ]} @@ -189,7 +338,7 @@ export const MetricItem = ({ locationId={monitor.location.id} />
- {configIdByLocation && ( + {/* {configIdByLocation && ( - )} + )} */} +
); }; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx index 53f14deb0dab5..5289d8ab1125a 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx @@ -4,9 +4,11 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useState, useRef, memo, useCallback } from 'react'; +import React, { useState, useRef, memo, useCallback, useMemo, useEffect } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { i18n } from '@kbn/i18n'; +import InfiniteLoader from 'react-window-infinite-loader'; +import { FixedSizeGrid as ReactWindowGrid, FixedSizeList } from 'react-window'; import { EuiFlexGroup, EuiFlexItem, @@ -14,6 +16,8 @@ import { EuiSpacer, EuiButtonEmpty, EuiText, + EuiAutoSizer, + EuiAutoSize, } from '@elastic/eui'; import { selectOverviewStatus } from '../../../../state/overview_status'; import { useInfiniteScroll } from './use_infinite_scroll'; @@ -22,7 +26,9 @@ import { GroupFields } from './grid_by_group/group_fields'; import { quietFetchOverviewAction, selectOverviewState, + selectOverviewTrends, setFlyoutConfig, + trendStatsBatch, } from '../../../../state/overview'; import { useMonitorsSortedByStatus } from '../../../../hooks/use_monitors_sorted_by_status'; import { OverviewLoader } from './overview_loader'; @@ -31,9 +37,15 @@ import { FlyoutParamProps, OverviewGridItem } from './overview_grid_item'; import { SortFields } from './sort_fields'; import { NoMonitorsFound } from '../../common/no_monitors_found'; import { MonitorDetailFlyout } from './monitor_detail_flyout'; +import { useAbsoluteDate, useStatusByLocationOverview } from '../../../../hooks'; +import { useSyntheticsRefreshContext } from '../../../../contexts'; -export const OverviewGrid = memo(() => { +const ITEM_HEIGHT = 172; + +export const OverviewGrid = memo(({ monitorsSortedByStatus }: { monitorsSortedByStatus: any }) => { const { status } = useSelector(selectOverviewStatus); + const trends = useSelector(selectOverviewTrends); + console.log('trends', trends); const { data: { monitors }, @@ -44,28 +56,47 @@ export const OverviewGrid = memo(() => { } = useSelector(selectOverviewState); const { perPage } = pageState; const [page, setPage] = useState(1); + const [stats, setStats] = useState>({}); + const [vpage, setvpage] = useState([]); const dispatch = useDispatch(); const intersectionRef = useRef(null); - const { monitorsSortedByStatus } = useMonitorsSortedByStatus(); const setFlyoutConfigCallback = useCallback( (params: FlyoutParamProps) => dispatch(setFlyoutConfig(params)), [dispatch] ); const hideFlyout = useCallback(() => dispatch(setFlyoutConfig(null)), [dispatch]); + const { lastRefresh } = useSyntheticsRefreshContext(); const forceRefreshCallback = useCallback( () => dispatch(quietFetchOverviewAction.get(pageState)), [dispatch, pageState] ); - const { currentMonitors } = useInfiniteScroll({ intersectionRef, monitorsSortedByStatus }); + const listHeight = Math.min(ITEM_HEIGHT * (monitorsSortedByStatus.length / 4), 800); + const listRef = React.createRef(); + const infiniteLoaderRef = useRef<{ resetloadMoreItemsCache: () => void }>(null); + useEffect(() => { + if (infiniteLoaderRef.current) { + infiniteLoaderRef.current.resetloadMoreItemsCache(); + } + }, [lastRefresh]); + console.log('monitors sorted by status', monitorsSortedByStatus); + console.log('vpage', vpage); + console.log('map', JSON.stringify(vpage.flatMap((x) => x).map(({ configId }) => configId))); + // const { currentMonitors } = useInfiniteScroll({ intersectionRef, monitorsSortedByStatus }); + // const listHeight = React.useMemo( + // () => Math.min(ITEM_HEIGHT * (currentMonitors.length / 4), 800), + // [currentMonitors.length] + // ); + // const itemCount = React.useMemo(() => currentMonitors.length, [currentMonitors.length]); // Display no monitors found when down, up, or disabled filter produces no results if (status && !monitorsSortedByStatus.length && loaded) { return ; } + // return
hi
; return ( <> { - <> +
{groupField === 'none' ? ( - loaded && currentMonitors.length ? ( - - {currentMonitors.map((monitor) => ( - + {({ width }: EuiAutoSize) => ( + vpage[idx] !== undefined} + itemCount={monitorsSortedByStatus.length / 4} + loadMoreItems={(start: number, stop: number) => { + console.log('load more itesm', start, stop); + const newRows = []; + for (let i = start; i < stop; i++) { + newRows.push(monitorsSortedByStatus.slice(i * 4, i * 4 + 4)); + } + const fetchStatsActionPayload = []; + for (const newRow of newRows) { + for (const item of newRow) { + console.log('request for ', item.configId, item.location.id, item); + fetchStatsActionPayload.push({ + configId: item.configId, + locationId: item.location.id, + }); + } + } + dispatch(trendStatsBatch.get(fetchStatsActionPayload)); + setvpage([...vpage, ...newRows]); + }} + minimumBatchSize={8} + threshold={4} > - - - ))} - + {({ onItemsRendered, ref }) => ( + + {(props) => { + // console.log('props', props); + return ( + + {props.data + .slice(props.index * 4, props.index * 4 + 4) + .map((item: any, idx: number) => ( + + + + ))} + + ); + return
hi!
; + }} +
+ )} + + // + // {({ style, ...rest }) => ( + // + // )} + // + )} + ) : ( + // + // {currentMonitors.map((monitor) => ( + // + // + // + // ))} + // + //
react window here plz
) ) : ( @@ -117,30 +226,34 @@ export const OverviewGrid = memo(() => { /> )} - +
{groupField === 'none' && ( - {currentMonitors.length === monitors.length && ( + {monitorsSortedByStatus.length === monitors.length && ( {SHOWING_ALL_MONITORS_LABEL} )} - {currentMonitors.length === monitors.length && currentMonitors.length > perPage && ( - - window.scrollTo(0, 0)} - iconType="sortUp" - iconSide="right" - size="xs" - > - {SCROLL_TO_TOP_LABEL} - - - )} + {monitorsSortedByStatus.length === monitors.length && + monitorsSortedByStatus.length > perPage && ( + + { + window.scrollTo({ top: 0, left: 0, behavior: 'smooth' }); + listRef.current.scrollToItem(0); + }} + iconType="sortUp" + iconSide="right" + size="xs" + > + {SCROLL_TO_TOP_LABEL} + + + )} )} {flyoutConfig?.configId && flyoutConfig?.location && ( diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid_item.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid_item.tsx index fefef3c973eb5..1789f8e72d067 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid_item.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid_item.tsx @@ -8,6 +8,8 @@ import React from 'react'; import { MetricItem } from './metric_item'; import { useLast50DurationChart, useStatusByLocationOverview } from '../../../../hooks'; import { MonitorOverviewItem } from '../../../../../../../common/runtime_types'; +import { useSelector } from 'react-redux'; +import { selectOverviewTrends } from '../../../../state'; export interface FlyoutParamProps { id: string; @@ -19,29 +21,35 @@ export interface FlyoutParamProps { export const OverviewGridItem = ({ monitor, onClick, + style, }: { monitor: MonitorOverviewItem; onClick: (params: FlyoutParamProps) => void; + style?: React.CSSProperties; }) => { - const { timestamp } = useStatusByLocationOverview({ - configId: monitor.configId, - locationId: monitor.location.id, - }); + const d = useSelector(selectOverviewTrends); + const trendData = d[monitor.configId + monitor.location.id]; + console.log('trend data for monitor', monitor.name, trendData); + // const { timestamp } = useStatusByLocationOverview({ + // configId: monitor.configId, + // locationId: monitor.location.id, + // }); - const { data, medianDuration, maxDuration, avgDuration, minDuration } = useLast50DurationChart({ - locationId: monitor.location?.id, - monitorId: monitor.id, - timestamp, - schedule: monitor.schedule, - }); + // const { data, medianDuration, maxDuration, avgDuration, minDuration } = useLast50DurationChart({ + // locationId: monitor.location?.id, + // monitorId: monitor.id, + // timestamp, + // schedule: monitor.schedule, + // }); return ( ); diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview_page.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview_page.tsx index e16633fed2444..94c1ec39915c9 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview_page.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview_page.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useEffect } from 'react'; +import React, { Profiler, useEffect } from 'react'; import { EuiFlexGroup, EuiSpacer, EuiFlexItem } from '@elastic/eui'; import { useDispatch, useSelector } from 'react-redux'; import { useTrackPageview } from '@kbn/observability-shared-plugin/public'; @@ -33,6 +33,8 @@ import { SearchField } from '../common/search_field'; import { NoMonitorsFound } from '../common/no_monitors_found'; import { OverviewErrors } from './overview/overview_errors/overview_errors'; import { AlertingCallout } from '../../common/alerting_callout/alerting_callout'; +import { onRender } from '../../../utils/on_render'; +import { useMonitorsSortedByStatus } from '../../../hooks/use_monitors_sorted_by_status'; export const OverviewPage: React.FC = () => { useTrackPageview({ app: 'synthetics', path: 'overview' }); @@ -59,6 +61,7 @@ export const OverviewPage: React.FC = () => { data: { monitors }, pageState, } = useSelector(selectOverviewState); + console.log('page state', pageState); const { loading: monitorsLoading, @@ -84,6 +87,8 @@ export const OverviewPage: React.FC = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [dispatch, lastRefresh]); + const { monitorsSortedByStatus } = useMonitorsSortedByStatus(); + const hasNoMonitors = !search && !enablementLoading && monitorsLoaded && absoluteTotal === 0; if (hasNoMonitors && !monitorsLoading && isEnabled) { @@ -97,7 +102,7 @@ export const OverviewPage: React.FC = () => { const noMonitorFound = monitorsLoaded && overviewLoaded && monitors?.length === 0; return ( - <> + @@ -126,11 +131,11 @@ export const OverviewPage: React.FC = () => { - + ) : ( )} - + ); }; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_last_50_duration_chart.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_last_50_duration_chart.ts index f08e025a48540..2280bf62b5c5b 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_last_50_duration_chart.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_last_50_duration_chart.ts @@ -10,6 +10,7 @@ import { useLastXChecks } from './use_last_x_checks'; const fields = ['monitor.duration.us']; +// can we make this an ES query instead? export function useLast50DurationChart({ monitorId, locationId, @@ -21,6 +22,7 @@ export function useLast50DurationChart({ locationId: string; schedule: string; }) { + return null; const { hits, loading } = useLastXChecks<{ 'monitor.duration.us': number[] | undefined; }>({ @@ -31,6 +33,7 @@ export function useLast50DurationChart({ timestamp, schedule, }); + return null; const { data, median, min, max, avg } = useMemo(() => { if (loading) { return { diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_last_x_checks.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_last_x_checks.ts index 7b155f53272dd..047343db9979c 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_last_x_checks.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_last_x_checks.ts @@ -33,7 +33,7 @@ export const getTimeRangeFilter = (schedule: string) => { }, }; }; - +let hits = 0; export function useLastXChecks({ monitorId, locationId, @@ -80,7 +80,9 @@ export function useLastXChecks({ fields, }, }); + console.log('query', params); + return null; const { data } = useReduxEsSearch(params, [lastRefresh], { name: `zGetLastXChecks/${monitorId}/${locationId}`, isRequestReady: locationsLoaded && Boolean(timestamp), // don't run query until locations are loaded diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_monitors_sorted_by_status.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_monitors_sorted_by_status.tsx index 73f9644749351..38aba0bdba7c2 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_monitors_sorted_by_status.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_monitors_sorted_by_status.tsx @@ -24,6 +24,7 @@ export function useMonitorsSortedByStatus() { const downMonitors = useRef | null>(null); const monitorsSortedByStatus = useMemo(() => { + console.log('has status', !!status, status); if (!status) { return { down: [], @@ -50,16 +51,16 @@ export function useMonitorsSortedByStatus() { monitors.forEach((monitor) => { if (!monitor.isEnabled) { - orderedDisabledMonitors.push(monitor); + orderedDisabledMonitors.push({ ...monitor, status: 'disabled' }); } else if ( monitor.configId in downMonitorMap && downMonitorMap[monitor.configId].includes(monitor.location.id) ) { - orderedDownMonitors.push(monitor); + orderedDownMonitors.push({ ...monitor, status: 'down' }); } else if (pendingConfigs?.[`${monitor.configId}-${monitor.location.id}`]) { - orderedPendingMonitors.push(monitor); + orderedPendingMonitors.push({ ...monitor, status: 'pending' }); } else { - orderedUpMonitors.push(monitor); + orderedUpMonitors.push({ ...monitor, status: 'up' }); } }); downMonitors.current = downMonitorMap; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_status_by_location_overview.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_status_by_location_overview.ts index f7c5b57984a4b..a60abd6e97a5f 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_status_by_location_overview.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_status_by_location_overview.ts @@ -9,6 +9,7 @@ import { useSelector } from 'react-redux'; import { OverviewStatusState } from '../../../../common/runtime_types'; import { selectOverviewStatus } from '../state/overview_status'; +// switch this to a component that contains this logic? Unnecessary splintering export function useStatusByLocationOverview({ configId, locationId, diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/actions.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/actions.ts index 73e24e6ece6c9..7b0572b2352ae 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/actions.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/actions.ts @@ -33,3 +33,22 @@ export const quietFetchOverviewAction = createAsyncAction< MonitorOverviewPageState, MonitorOverviewResult >('quietFetchOverviewAction'); + +export const trendStatsBatch = createAsyncAction< + Array<{ configId: string; locationId: string }>, + Record, + // { + // by_id: { + // buckets: Array<{ + // by_location: { + // buckets: Array<{ + // median: { values: { '50.0': number } }; + // stats: { count: number; min: number; max: number; avg: number; sum: number }; + // last_50: { hits: { hits: }} + // }>; + // }; + // }>; + // }; + // }, + any +>('batchTrendStats'); diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/api.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/api.ts index 7244efc4dc655..240f80becd67e 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/api.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/api.ts @@ -57,3 +57,10 @@ export const fetchOverviewStatus = async ( const params = toStatusOverviewQueryArgs(pageState); return apiService.get(SYNTHETICS_API_URLS.OVERVIEW_STATUS, params, OverviewStatusCodec); }; + +export const fetchOverviewTrendStats = async ( + filters: Array<{ configId: string; locationId: string }> +): Promise => { + console.log('in api', filters); + return apiService.post(SYNTHETICS_API_URLS.OVERVIEW_TRENDS, filters); +}; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.ts index 71821c46665c7..09b3761229138 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.ts @@ -5,10 +5,10 @@ * 2.0. */ -import { debounce } from 'redux-saga/effects'; +import { debounce, takeEvery, takeLeading } from 'redux-saga/effects'; import { fetchEffectFactory } from '../utils/fetch_effect'; -import { fetchMonitorOverviewAction, quietFetchOverviewAction } from './actions'; -import { fetchMonitorOverview } from './api'; +import { fetchMonitorOverviewAction, quietFetchOverviewAction, trendStatsBatch } from './actions'; +import { fetchMonitorOverview, fetchOverviewTrendStats as trendsApi } from './api'; export function* fetchMonitorOverviewEffect() { yield debounce( @@ -21,3 +21,11 @@ export function* fetchMonitorOverviewEffect() { ) ); } + +export function* fetchOverviewTrendStats() { + console.log('in effect'); + yield takeLeading( + trendStatsBatch.get, + fetchEffectFactory(trendsApi, trendStatsBatch.success, trendStatsBatch.fail) + ); +} diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/index.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/index.ts index 3f8038ffc84d3..d9c89959fc3ac 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/index.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/index.ts @@ -15,6 +15,7 @@ import { setOverviewGroupByAction, setOverviewPageStateAction, toggleErrorPopoverOpen, + trendStatsBatch, } from './actions'; import { enableMonitorAlertAction } from '../monitor_list/actions'; import { ConfigKey } from '../../components/monitor_add_edit/types'; @@ -30,6 +31,7 @@ const initialState: MonitorOverviewState = { sortOrder: 'asc', sortField: 'status', }, + trendStats: {}, groupBy: { field: 'none', order: 'asc' }, flyoutConfig: null, loading: false, @@ -89,6 +91,18 @@ export const monitorOverviewReducer = createReducer(initialState, (builder) => { }) .addCase(toggleErrorPopoverOpen, (state, action) => { state.isErrorPopoverOpen = action.payload; + }) + .addCase(trendStatsBatch.get, (state, action) => { + for (const { configId, locationId } of action.payload) { + if (!state.trendStats[configId + locationId]) { + state.trendStats[configId + locationId] = null; + } + } + }) + .addCase(trendStatsBatch.success, (state, action) => { + for (const key of Object.keys(action.payload)) { + state.trendStats[key] = action.payload[key]; + } }); }); diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/models.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/models.ts index 8706ca519d49f..d64ced353a137 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/models.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/models.ts @@ -32,6 +32,10 @@ export interface MonitorOverviewState { isErrorPopoverOpen?: string | null; error: IHttpSerializedFetchError | null; groupBy: GroupByState; + trendStats: Record< + string, + null | { data: any[]; median: number; avg: number; min: number; max: number } + >; } export interface GroupByState { diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/selectors.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/selectors.ts index 677b7cc8208d9..98286a3da118f 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/selectors.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/selectors.ts @@ -16,3 +16,7 @@ export const selectErrorPopoverState = createSelector( selectOverviewState, (state) => state.isErrorPopoverOpen ); +export const selectOverviewTrends = createSelector( + selectOverviewState, + ({ trendStats }) => trendStats +); diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/root_effect.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/root_effect.ts index 62d671d8e98fd..a8005953da915 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/root_effect.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/root_effect.ts @@ -33,7 +33,7 @@ import { upsertMonitorEffect, fetchMonitorFiltersEffect, } from './monitor_list'; -import { fetchMonitorOverviewEffect } from './overview'; +import { fetchMonitorOverviewEffect, fetchOverviewTrendStats } from './overview'; import { fetchServiceLocationsEffect } from './service_locations'; import { browserJourneyEffects, fetchJourneyStepsEffect } from './browser_journey'; import { fetchPingStatusesEffect } from './ping_status'; @@ -71,5 +71,6 @@ export const rootEffect = function* root(): Generator { fork(getCertsListEffect), fork(getDefaultAlertingEffect), fork(enableDefaultAlertingSilentlyEffect), + fork(fetchOverviewTrendStats), ]); }; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/synthetics_app.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/synthetics_app.tsx index 93c45442695ad..6095b4b4a877f 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/synthetics_app.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/synthetics_app.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useEffect } from 'react'; +import React, { Profiler, useEffect } from 'react'; import { Provider as ReduxProvider } from 'react-redux'; import { APP_WRAPPER_CLASS } from '@kbn/core/public'; @@ -31,6 +31,7 @@ import { import { SyntheticsDataViewContextProvider } from './contexts/synthetics_data_view_context'; import { PageRouter } from './routes'; import { setBasePath, storage, store } from './state'; +import { onRender } from './utils/on_render'; const Application = (props: SyntheticsAppProps) => { const { @@ -69,66 +70,68 @@ const Application = (props: SyntheticsAppProps) => { store.dispatch(setBasePath(basePath)); return ( - - - - - - - - - - - -
- - - - - - - -
-
-
-
-
-
-
-
-
-
-
-
+ {}}> + + + + + + + + + + + +
+ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
); }; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/on_render.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/on_render.ts new file mode 100644 index 0000000000000..df8f0a005bde4 --- /dev/null +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/on_render.ts @@ -0,0 +1,20 @@ +let renderCount = 0; +export function onRender( + id: any, + phase: any, + actualDuration: any, + baseDuration: any, + startTime: any, + commitTime: any, + interactions: any +) { + console.log('onRender', `count: ${renderCount++}`, { + id, + phase, + actualDuration, + baseDuration, + startTime, + commitTime, + interactions, + }); +} diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/index.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/index.ts index c97abe44a6c5a..ba3bc0b443fb9 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/index.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/index.ts @@ -60,6 +60,7 @@ import { getAllSyntheticsMonitorRoute } from './monitor_cruds/get_monitors_list' import { getLocationMonitors } from './settings/private_locations/get_location_monitors'; import { addSyntheticsParamsRoute } from './settings/params/add_param'; import { deleteSyntheticsParamsRoute } from './settings/params/delete_param'; +import { createOverviewTrendsRoute } from './overview_trends/overview_trends'; export const syntheticsAppRestApiRoutes: SyntheticsRestApiRouteFactory[] = [ addSyntheticsProjectMonitorRoute, @@ -101,6 +102,7 @@ export const syntheticsAppRestApiRoutes: SyntheticsRestApiRouteFactory[] = [ getConnectorTypesRoute, createGetDynamicSettingsRoute, createPostDynamicSettingsRoute, + createOverviewTrendsRoute, ]; export const syntheticsAppPublicRestApiRoutes: SyntheticsRestApiRouteFactory[] = [ diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/fetch_trends.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/fetch_trends.ts new file mode 100644 index 0000000000000..c22058e7d25da --- /dev/null +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/fetch_trends.ts @@ -0,0 +1,141 @@ +/* + * 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 { UptimeEsClient } from '../../lib'; + +export function fetchTrends(configId: string, locationIds: string[], esClient: UptimeEsClient) { + const query = { + size: 0, + query: { + bool: { + filter: [ + { + bool: { + filter: [ + { + exists: { + field: 'summary', + }, + }, + { + bool: { + should: [ + { + bool: { + should: [ + { + match: { + 'summary.final_attempt': true, + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + must_not: { + bool: { + should: [ + { + exists: { + field: 'summary.final_attempt', + }, + }, + ], + minimum_should_match: 1, + }, + }, + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + { + bool: { + must_not: { + exists: { + field: 'run_once', + }, + }, + }, + }, + { + terms: { + 'observer.name': locationIds, + }, + }, + { + term: { + config_id: configId, + }, + }, + { + range: { + '@timestamp': { + gte: 'now-9h', + lte: 'now', + }, + }, + }, + ], + }, + }, + aggs: { + by_id: { + terms: { + field: 'config_id', + }, + aggs: { + by_location: { + terms: { + field: 'observer.name', + }, + aggs: { + last_50: { + top_hits: { + size: 50, + sort: [ + { + '@timestamp': { + order: 'desc', + }, + }, + ], + }, + }, + stats: { + stats: { + field: 'monitor.duration.us', + }, + }, + median: { + percentiles: { + field: 'monitor.duration.us', + percents: [50], + }, + }, + }, + }, + }, + }, + }, + _source: false, + sort: [ + { + '@timestamp': 'desc', + }, + ], + fields: ['monitor.duration.us'], + }; + console.log(JSON.stringify(query)); + return esClient.search({ body: query }); +} diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/overview_trends.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/overview_trends.ts new file mode 100644 index 0000000000000..671e0639733bf --- /dev/null +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/overview_trends.ts @@ -0,0 +1,73 @@ +/* + * 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 } from '@kbn/config-schema'; +import { SYNTHETICS_API_URLS } from '../../../common/constants'; +import { SyntheticsRestApiRouteFactory } from '../types'; +import { fetchTrends } from './fetch_trends'; + +export const createOverviewTrendsRoute: SyntheticsRestApiRouteFactory = () => ({ + method: 'POST', + path: SYNTHETICS_API_URLS.OVERVIEW_TRENDS, + validate: { + body: schema.arrayOf( + schema.object({ + configId: schema.string(), + locationId: schema.string(), + }) + ), + }, + handler: async (routeContext): Promise => { + const esClient = routeContext.uptimeEsClient; + const body = routeContext.request.body as Array<{ configId: string; locationId: string }>; + const configs = body.reduce((acc: Record, { configId, locationId }) => { + if (!acc[configId]) { + acc[configId] = [locationId]; + } else { + acc[configId].push(locationId); + } + return acc; + }, {}); + const results = await Promise.all( + Object.keys(configs).map((key) => fetchTrends(key, configs[key], esClient)) + ); + let main = {}; + for (const res of results) { + const aggregations = res.body.aggregations as any; + console.log('aggregations', aggregations); + aggregations.by_id.buckets.map(({ key, by_location }: { key: string; by_location: any }) => { + const ret: Record = {}; + for (const location of by_location.buckets) { + console.log('creating location bucket for ', key, location); + ret[key + location.key] = { + data: location.last_50.hits.hits, + ...location.stats, + median: location.median.values['50.0'], + }; + } + console.log('ret', ret); + main = { ...main, ...ret }; + }); + // console.log('formatted', formatted); + // console.log('keys', Object.keys(formatted)); + } + return routeContext.response.ok({ body: main }); + // const res = await fetchTrends(Array.from(configIds), Array.from(locationIds), esClient); + // const aggregations = res.body.aggregations as any; + // const ret: Record = {}; + // aggregations.by_id.buckets.map(({ key, by_location }: { key: string; by_location: any }) => { + // for (const location of by_location.buckets) { + // ret[key + location.key] = { + // data: location.last_50, + // ...location.stats, + // median: location.median.values['50.0'], + // }; + // } + // }); + // return routeContext.response.ok({ body: ret }); + }, +}); diff --git a/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/project_monitor/normalizers/common_fields.ts b/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/project_monitor/normalizers/common_fields.ts index 4019151dc44f6..efbd43f155285 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/project_monitor/normalizers/common_fields.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/project_monitor/normalizers/common_fields.ts @@ -146,6 +146,7 @@ export const getMonitorSchedule = ( schedule: number | string | MonitorFields['schedule'], defaultValue?: MonitorFields['schedule'] ) => { + console.log('schedule', schedule); if (!schedule && defaultValue) { return defaultValue; } diff --git a/yarn.lock b/yarn.lock index 1d099d2e2980e..573492b848b6a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10940,7 +10940,15 @@ "@types/prop-types" "*" "@types/react" "*" -"@types/react-window@^1.8.8": +"@types/react-window-infinite-loader@^1.0.9": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@types/react-window-infinite-loader/-/react-window-infinite-loader-1.0.9.tgz#9b24d4e60f20397ff853c6857f7fe0645becbeb9" + integrity sha512-gEInTjQwURCnDOFyIEK2+fWB5gTjqwx30O62QfxA9stE5aiB6EWkGj4UMhc0axq7/FV++Gs/TGW8FtgEx0S6Tw== + dependencies: + "@types/react" "*" + "@types/react-window" "*" + +"@types/react-window@*", "@types/react-window@^1.8.8": version "1.8.8" resolved "https://registry.yarnpkg.com/@types/react-window/-/react-window-1.8.8.tgz#c20645414d142364fbe735818e1c1e0a145696e3" integrity sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q== @@ -26876,6 +26884,11 @@ react-virtualized@^9.22.5: prop-types "^15.7.2" react-lifecycles-compat "^3.0.4" +react-window-infinite-loader@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/react-window-infinite-loader/-/react-window-infinite-loader-1.0.9.tgz#d861c03d5cbc550e2f185371af820fd22d46c099" + integrity sha512-5Hg89IdU4Vrp0RT8kZYKeTIxWZYhNkVXeI1HbKo01Vm/Z7qztDvXljwx16sMzsa9yapRJQW3ODZfMUw38SOWHw== + react-window@^1.8.10: version "1.8.10" resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.10.tgz#9e6b08548316814b443f7002b1cf8fd3a1bdde03" From 607f8f92e64b9a103b6b4d07389b28c95c017d43 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Wed, 26 Jun 2024 15:22:49 -0400 Subject: [PATCH 02/39] TEMP. --- .../overview/overview/metric_item.tsx | 178 +++--------------- .../overview/overview/overview_grid.tsx | 4 +- .../overview/overview/overview_grid_item.tsx | 3 - .../routes/overview_trends/fetch_trends.ts | 1 + .../routes/overview_trends/overview_trends.ts | 4 +- 5 files changed, 29 insertions(+), 161 deletions(-) diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx index 92a4687ade752..54ef968f02202 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx @@ -13,7 +13,11 @@ import { DARK_THEME } from '@elastic/charts'; import { useTheme } from '@kbn/observability-shared-plugin/public'; import { useDispatch, useSelector } from 'react-redux'; import moment from 'moment'; -import { selectErrorPopoverState, toggleErrorPopoverOpen } from '../../../../state'; +import { + selectErrorPopoverState, + selectOverviewTrends, + toggleErrorPopoverOpen, +} from '../../../../state'; import { useLocationName, useStatusByLocationOverview } from '../../../../hooks'; import { formatDuration } from '../../../../utils/formatting'; import { MonitorOverviewItem } from '../../../../../../../common/runtime_types'; @@ -64,6 +68,10 @@ export const MetricItem = ({ style?: React.CSSProperties; onClick: (params: { id: string; configId: string; location: string; locationId: string }) => void; }) => { + const d = useSelector(selectOverviewTrends); + console.log('trends', d); + const trendData = d[monitor.configId + monitor.location.id]; + console.log('trend data for monitor', monitor.name, monitor.location.id, monitor, trendData); const [isPopoverOpen, setIsPopoverOpen] = useState(false); const isErrorPopoverOpen = useSelector(selectErrorPopoverState); const locationName = useLocationName(monitor); @@ -77,150 +85,12 @@ export const MetricItem = ({ const dispatch = useDispatch(); - const contents = useMemo(() => { - return ( - { - if (isErrorPopoverOpen) { - dispatch(toggleErrorPopoverOpen(null)); - } - }} - css={css` - height: 100%; - overflow: hidden; - position: relative; - - & .cardItemActions_hover { - pointer-events: none; - opacity: 0; - &:focus-within { - pointer-events: auto; - opacity: 1; - } - } - &:hover .cardItemActions_hover { - pointer-events: auto; - opacity: 1; - } - `} - title={monitor.name} - // title={moment(timestamp).format('LLL')} - > - - { - if (testInProgress) { - dispatch(toggleTestNowFlyoutAction(monitor.configId)); - dispatch(toggleErrorPopoverOpen(null)); - } else { - dispatch(hideTestNowFlyoutAction()); - dispatch(toggleErrorPopoverOpen(null)); - } - if (!testInProgress && locationName) { - onClick({ - configId: monitor.configId, - id: monitor.id, - location: locationName, - locationId: monitor.location.id, - }); - } - }} - // TODO connect to charts.theme service see src/plugins/charts/public/services/theme/README.md - baseTheme={DARK_THEME} - locale={i18n.getLocale()} - /> - - - {i18n.translate('xpack.synthetics.overview.duration.label', { - defaultMessage: 'Duration', - })} - - {/* - - */} - - ), - valueFormatter: (d: number) => formatDuration(d), - color: getColor(theme, monitor.isEnabled, monitor.status), - }, - ], - ]} - /> - -
- -
- {/* {configIdByLocation && ( - - )} */} -
- ); - }, [ - dispatch, - isErrorPopoverOpen, - isPopoverOpen, - locationName, - monitor, - onClick, - testInProgress, - theme, - ]); - - return ( -
- {contents} -
- ); + // return ( + //
+ // {contents} + //
+ // ); + if (!trendData) return null; return (
- {/* + - */} + ), - valueFormatter: (d: number) => formatDuration(d), + valueFormatter: (x: number) => formatDuration(x), color: getColor(theme, monitor.isEnabled, monitor.status), }, ], diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx index 5289d8ab1125a..c8b802a521b51 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx @@ -148,8 +148,8 @@ export const OverviewGrid = memo(({ monitorsSortedByStatus }: { monitorsSortedBy dispatch(trendStatsBatch.get(fetchStatsActionPayload)); setvpage([...vpage, ...newRows]); }} - minimumBatchSize={8} - threshold={4} + minimumBatchSize={16} + threshold={10} > {({ onItemsRendered, ref }) => ( void; style?: React.CSSProperties; }) => { - const d = useSelector(selectOverviewTrends); - const trendData = d[monitor.configId + monitor.location.id]; - console.log('trend data for monitor', monitor.name, trendData); // const { timestamp } = useStatusByLocationOverview({ // configId: monitor.configId, // locationId: monitor.location.id, diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/fetch_trends.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/fetch_trends.ts index c22058e7d25da..7e48d3002a813 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/fetch_trends.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/fetch_trends.ts @@ -110,6 +110,7 @@ export function fetchTrends(configId: string, locationIds: string[], esClient: U }, }, ], + _source: ['monitor.duration.us'], }, }, stats: { diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/overview_trends.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/overview_trends.ts index 671e0639733bf..d4c33d645e29e 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/overview_trends.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/overview_trends.ts @@ -44,7 +44,9 @@ export const createOverviewTrendsRoute: SyntheticsRestApiRouteFactory = () => ({ for (const location of by_location.buckets) { console.log('creating location bucket for ', key, location); ret[key + location.key] = { - data: location.last_50.hits.hits, + data: location.last_50.hits.hits + .reverse() + .map((hit, x) => ({ x, y: hit._source.monitor.duration.us })), ...location.stats, median: location.median.values['50.0'], }; From 9895747cdcbd591170b750cc89c130e2a4ca6f3d Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Wed, 26 Jun 2024 16:44:11 -0400 Subject: [PATCH 03/39] PoC. --- .../overview/overview/metric_item.tsx | 12 +-- .../overview/overview/overview_grid.tsx | 4 +- .../overview/overview/overview_grid_item.tsx | 11 -- .../routes/overview_trends/overview_trends.ts | 101 +++++++++--------- 4 files changed, 60 insertions(+), 68 deletions(-) diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx index 54ef968f02202..e5051d1bcc64e 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx @@ -75,10 +75,10 @@ export const MetricItem = ({ const [isPopoverOpen, setIsPopoverOpen] = useState(false); const isErrorPopoverOpen = useSelector(selectErrorPopoverState); const locationName = useLocationName(monitor); - // const { status, timestamp, ping, configIdByLocation } = useStatusByLocationOverview({ - // configId: monitor.configId, - // locationId: monitor.location.id, - // }); + const { status, timestamp, ping, configIdByLocation } = useStatusByLocationOverview({ + configId: monitor.configId, + locationId: monitor.location.id, + }); const theme = useTheme(); const testInProgress = useSelector(manualTestRunInProgressSelector(monitor.configId)); @@ -206,7 +206,7 @@ export const MetricItem = ({ locationId={monitor.location.id} />
- {/* {configIdByLocation && ( + {configIdByLocation && ( - )} */} + )} diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx index c8b802a521b51..bc54a68b1cc9c 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx @@ -128,7 +128,7 @@ export const OverviewGrid = memo(({ monitorsSortedByStatus }: { monitorsSortedBy vpage[idx] !== undefined} - itemCount={monitorsSortedByStatus.length / 4} + itemCount={monitorsSortedByStatus.length + 1 / 4} loadMoreItems={(start: number, stop: number) => { console.log('load more itesm', start, stop); const newRows = []; @@ -162,7 +162,6 @@ export const OverviewGrid = memo(({ monitorsSortedByStatus }: { monitorsSortedBy ref={listRef} > {(props) => { - // console.log('props', props); return ( {props.data @@ -177,7 +176,6 @@ export const OverviewGrid = memo(({ monitorsSortedByStatus }: { monitorsSortedBy ))} ); - return
hi!
; }} )} diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid_item.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid_item.tsx index a16b685989380..abf871c659dee 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid_item.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid_item.tsx @@ -27,17 +27,6 @@ export const OverviewGridItem = ({ onClick: (params: FlyoutParamProps) => void; style?: React.CSSProperties; }) => { - // const { timestamp } = useStatusByLocationOverview({ - // configId: monitor.configId, - // locationId: monitor.location.id, - // }); - - // const { data, medianDuration, maxDuration, avgDuration, minDuration } = useLast50DurationChart({ - // locationId: monitor.location?.id, - // monitorId: monitor.id, - // timestamp, - // schedule: monitor.schedule, - // }); return ( ({ ), }, handler: async (routeContext): Promise => { - const esClient = routeContext.uptimeEsClient; - const body = routeContext.request.body as Array<{ configId: string; locationId: string }>; - const configs = body.reduce((acc: Record, { configId, locationId }) => { - if (!acc[configId]) { - acc[configId] = [locationId]; - } else { - acc[configId].push(locationId); - } - return acc; - }, {}); - const results = await Promise.all( - Object.keys(configs).map((key) => fetchTrends(key, configs[key], esClient)) - ); - let main = {}; - for (const res of results) { - const aggregations = res.body.aggregations as any; - console.log('aggregations', aggregations); - aggregations.by_id.buckets.map(({ key, by_location }: { key: string; by_location: any }) => { - const ret: Record = {}; - for (const location of by_location.buckets) { - console.log('creating location bucket for ', key, location); - ret[key + location.key] = { - data: location.last_50.hits.hits - .reverse() - .map((hit, x) => ({ x, y: hit._source.monitor.duration.us })), - ...location.stats, - median: location.median.values['50.0'], - }; + return withSpan('fetch trends', async () => { + const esClient = routeContext.uptimeEsClient; + const body = routeContext.request.body as Array<{ configId: string; locationId: string }>; + const configs = body.reduce((acc: Record, { configId, locationId }) => { + if (!acc[configId]) { + acc[configId] = [locationId]; + } else { + acc[configId].push(locationId); } - console.log('ret', ret); - main = { ...main, ...ret }; - }); - // console.log('formatted', formatted); - // console.log('keys', Object.keys(formatted)); - } - return routeContext.response.ok({ body: main }); - // const res = await fetchTrends(Array.from(configIds), Array.from(locationIds), esClient); - // const aggregations = res.body.aggregations as any; - // const ret: Record = {}; - // aggregations.by_id.buckets.map(({ key, by_location }: { key: string; by_location: any }) => { - // for (const location of by_location.buckets) { - // ret[key + location.key] = { - // data: location.last_50, - // ...location.stats, - // median: location.median.values['50.0'], - // }; - // } - // }); - // return routeContext.response.ok({ body: ret }); + return acc; + }, {}); + const results = await Promise.all( + Object.keys(configs).map((key) => fetchTrends(key, configs[key], esClient)) + ); + let main = {}; + for (const res of results) { + const aggregations = res.body.aggregations as any; + console.log('aggregations', aggregations); + aggregations.by_id.buckets.map( + ({ key, by_location }: { key: string; by_location: any }) => { + const ret: Record = {}; + for (const location of by_location.buckets) { + console.log('creating location bucket for ', key, location); + ret[key + location.key] = { + data: location.last_50.hits.hits + .reverse() + .map((hit, x) => ({ x, y: hit._source.monitor.duration.us })), + ...location.stats, + median: location.median.values['50.0'], + }; + } + console.log('ret', ret); + main = { ...main, ...ret }; + } + ); + // console.log('formatted', formatted); + // console.log('keys', Object.keys(formatted)); + } + return routeContext.response.ok({ body: main }); + // const res = await fetchTrends(Array.from(configIds), Array.from(locationIds), esClient); + // const aggregations = res.body.aggregations as any; + // const ret: Record = {}; + // aggregations.by_id.buckets.map(({ key, by_location }: { key: string; by_location: any }) => { + // for (const location of by_location.buckets) { + // ret[key + location.key] = { + // data: location.last_50, + // ...location.stats, + // median: location.median.values['50.0'], + // }; + // } + // }); + // return routeContext.response.ok({ body: ret }); + }); }, }); From f0f3cc95b52032ef9132701159dbbc97eec18242 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Thu, 27 Jun 2024 12:19:31 -0400 Subject: [PATCH 04/39] Clean up. --- .../overview/overview/overview_grid.tsx | 74 +++---------------- .../apps/synthetics/state/overview/actions.ts | 17 +---- .../apps/synthetics/state/overview/effects.ts | 37 +++++++++- .../apps/synthetics/state/root_effect.ts | 7 +- .../routes/overview_trends/fetch_trends.ts | 9 ++- .../routes/overview_trends/overview_trends.ts | 60 ++++++--------- 6 files changed, 82 insertions(+), 122 deletions(-) diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx index bc54a68b1cc9c..3474c5c3786ef 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx @@ -4,15 +4,14 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useState, useRef, memo, useCallback, useMemo, useEffect } from 'react'; +import React, { useState, useRef, memo, useCallback, useEffect } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { i18n } from '@kbn/i18n'; import InfiniteLoader from 'react-window-infinite-loader'; -import { FixedSizeGrid as ReactWindowGrid, FixedSizeList } from 'react-window'; +import { FixedSizeList } from 'react-window'; import { EuiFlexGroup, EuiFlexItem, - EuiFlexGrid, EuiSpacer, EuiButtonEmpty, EuiText, @@ -20,32 +19,27 @@ import { EuiAutoSize, } from '@elastic/eui'; import { selectOverviewStatus } from '../../../../state/overview_status'; -import { useInfiniteScroll } from './use_infinite_scroll'; import { GridItemsByGroup } from './grid_by_group/grid_items_by_group'; import { GroupFields } from './grid_by_group/group_fields'; import { quietFetchOverviewAction, + refreshOverviewTrends, selectOverviewState, - selectOverviewTrends, setFlyoutConfig, trendStatsBatch, } from '../../../../state/overview'; -import { useMonitorsSortedByStatus } from '../../../../hooks/use_monitors_sorted_by_status'; import { OverviewLoader } from './overview_loader'; import { OverviewPaginationInfo } from './overview_pagination_info'; import { FlyoutParamProps, OverviewGridItem } from './overview_grid_item'; import { SortFields } from './sort_fields'; import { NoMonitorsFound } from '../../common/no_monitors_found'; import { MonitorDetailFlyout } from './monitor_detail_flyout'; -import { useAbsoluteDate, useStatusByLocationOverview } from '../../../../hooks'; import { useSyntheticsRefreshContext } from '../../../../contexts'; const ITEM_HEIGHT = 172; export const OverviewGrid = memo(({ monitorsSortedByStatus }: { monitorsSortedByStatus: any }) => { const { status } = useSelector(selectOverviewStatus); - const trends = useSelector(selectOverviewTrends); - console.log('trends', trends); const { data: { monitors }, @@ -56,7 +50,6 @@ export const OverviewGrid = memo(({ monitorsSortedByStatus }: { monitorsSortedBy } = useSelector(selectOverviewState); const { perPage } = pageState; const [page, setPage] = useState(1); - const [stats, setStats] = useState>({}); const [vpage, setvpage] = useState([]); const dispatch = useDispatch(); @@ -74,29 +67,17 @@ export const OverviewGrid = memo(({ monitorsSortedByStatus }: { monitorsSortedBy ); const listHeight = Math.min(ITEM_HEIGHT * (monitorsSortedByStatus.length / 4), 800); - const listRef = React.createRef(); - const infiniteLoaderRef = useRef<{ resetloadMoreItemsCache: () => void }>(null); + const listRef: React.LegacyRef> | undefined = React.createRef(); + const infiniteLoaderRef: React.LegacyRef = React.createRef(); useEffect(() => { - if (infiniteLoaderRef.current) { - infiniteLoaderRef.current.resetloadMoreItemsCache(); - } - }, [lastRefresh]); - console.log('monitors sorted by status', monitorsSortedByStatus); - console.log('vpage', vpage); - console.log('map', JSON.stringify(vpage.flatMap((x) => x).map(({ configId }) => configId))); - // const { currentMonitors } = useInfiniteScroll({ intersectionRef, monitorsSortedByStatus }); + dispatch(refreshOverviewTrends.get()); + }, [dispatch, lastRefresh]); - // const listHeight = React.useMemo( - // () => Math.min(ITEM_HEIGHT * (currentMonitors.length / 4), 800), - // [currentMonitors.length] - // ); - // const itemCount = React.useMemo(() => currentMonitors.length, [currentMonitors.length]); // Display no monitors found when down, up, or disabled filter produces no results if (status && !monitorsSortedByStatus.length && loaded) { return ; } - // return
hi
; return ( <> vpage[idx] !== undefined} itemCount={monitorsSortedByStatus.length + 1 / 4} loadMoreItems={(start: number, stop: number) => { - console.log('load more itesm', start, stop); const newRows = []; for (let i = start; i < stop; i++) { newRows.push(monitorsSortedByStatus.slice(i * 4, i * 4 + 4)); @@ -138,7 +118,6 @@ export const OverviewGrid = memo(({ monitorsSortedByStatus }: { monitorsSortedBy const fetchStatsActionPayload = []; for (const newRow of newRows) { for (const item of newRow) { - console.log('request for ', item.configId, item.location.id, item); fetchStatsActionPayload.push({ configId: item.configId, locationId: item.location.id, @@ -148,8 +127,8 @@ export const OverviewGrid = memo(({ monitorsSortedByStatus }: { monitorsSortedBy dispatch(trendStatsBatch.get(fetchStatsActionPayload)); setvpage([...vpage, ...newRows]); }} - minimumBatchSize={16} - threshold={10} + minimumBatchSize={20} + threshold={30} > {({ onItemsRendered, ref }) => ( @@ -180,40 +159,9 @@ export const OverviewGrid = memo(({ monitorsSortedByStatus }: { monitorsSortedBy )}
- // - // {({ style, ...rest }) => ( - // - // )} - // )} ) : ( - // - // {currentMonitors.map((monitor) => ( - // - // - // - // ))} - // - //
react window here plz
) ) : ( @@ -242,7 +190,7 @@ export const OverviewGrid = memo(({ monitorsSortedByStatus }: { monitorsSortedBy data-test-subj="syntheticsOverviewGridButton" onClick={() => { window.scrollTo({ top: 0, left: 0, behavior: 'smooth' }); - listRef.current.scrollToItem(0); + listRef.current?.scrollToItem(0); }} iconType="sortUp" iconSide="right" diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/actions.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/actions.ts index 7b0572b2352ae..f5ef6e295da4f 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/actions.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/actions.ts @@ -34,21 +34,12 @@ export const quietFetchOverviewAction = createAsyncAction< MonitorOverviewResult >('quietFetchOverviewAction'); +export const refreshOverviewTrends = createAsyncAction, any>( + 'refreshOverviewTrendStats' +); + export const trendStatsBatch = createAsyncAction< Array<{ configId: string; locationId: string }>, Record, - // { - // by_id: { - // buckets: Array<{ - // by_location: { - // buckets: Array<{ - // median: { values: { '50.0': number } }; - // stats: { count: number; min: number; max: number; avg: number; sum: number }; - // last_50: { hits: { hits: }} - // }>; - // }; - // }>; - // }; - // }, any >('batchTrendStats'); diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.ts index 09b3761229138..9ab6e1670dd28 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.ts @@ -5,9 +5,15 @@ * 2.0. */ -import { debounce, takeEvery, takeLeading } from 'redux-saga/effects'; +import { debounce, call, takeLeading, put, select } from 'redux-saga/effects'; +import { selectOverviewTrends } from './selectors'; import { fetchEffectFactory } from '../utils/fetch_effect'; -import { fetchMonitorOverviewAction, quietFetchOverviewAction, trendStatsBatch } from './actions'; +import { + fetchMonitorOverviewAction, + quietFetchOverviewAction, + refreshOverviewTrends, + trendStatsBatch, +} from './actions'; import { fetchMonitorOverview, fetchOverviewTrendStats as trendsApi } from './api'; export function* fetchMonitorOverviewEffect() { @@ -23,9 +29,34 @@ export function* fetchMonitorOverviewEffect() { } export function* fetchOverviewTrendStats() { - console.log('in effect'); yield takeLeading( trendStatsBatch.get, fetchEffectFactory(trendsApi, trendStatsBatch.success, trendStatsBatch.fail) ); } + +// writing refresh logic +export function* refreshOverviewTrendStats() { + yield takeLeading(refreshOverviewTrends.get, function* (): Generator< + unknown, + void, + Record + > { + const trends: Record = yield select(selectOverviewTrends); + let all = {}; + const keys = Object.keys(trends); + do { + const res = yield call( + trendsApi, + keys.splice(0, keys.length < 10 ? keys.length : 10).map((key: string) => ({ + configId: trends[key].configId, + locationId: trends[key].locationId, + })) + ); + all = { ...all, ...res }; + } while (keys.length); + if (Object.keys(all).length) { + yield put(trendStatsBatch.success(all)); + } + }); +} diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/root_effect.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/root_effect.ts index a8005953da915..424c6fa70eed6 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/root_effect.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/root_effect.ts @@ -33,7 +33,11 @@ import { upsertMonitorEffect, fetchMonitorFiltersEffect, } from './monitor_list'; -import { fetchMonitorOverviewEffect, fetchOverviewTrendStats } from './overview'; +import { + fetchMonitorOverviewEffect, + fetchOverviewTrendStats, + refreshOverviewTrendStats, +} from './overview'; import { fetchServiceLocationsEffect } from './service_locations'; import { browserJourneyEffects, fetchJourneyStepsEffect } from './browser_journey'; import { fetchPingStatusesEffect } from './ping_status'; @@ -72,5 +76,6 @@ export const rootEffect = function* root(): Generator { fork(getDefaultAlertingEffect), fork(enableDefaultAlertingSilentlyEffect), fork(fetchOverviewTrendStats), + fork(refreshOverviewTrendStats), ]); }; diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/fetch_trends.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/fetch_trends.ts index 7e48d3002a813..6cd22d2f512e0 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/fetch_trends.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/fetch_trends.ts @@ -5,10 +5,11 @@ * 2.0. */ +import { SearchRequest } from '@elastic/elasticsearch/lib/api/types'; import { UptimeEsClient } from '../../lib'; export function fetchTrends(configId: string, locationIds: string[], esClient: UptimeEsClient) { - const query = { + const query: SearchRequest = { size: 0, query: { bool: { @@ -90,12 +91,12 @@ export function fetchTrends(configId: string, locationIds: string[], esClient: U }, }, aggs: { - by_id: { + byId: { terms: { field: 'config_id', }, aggs: { - by_location: { + byLocation: { terms: { field: 'observer.name', }, @@ -137,6 +138,6 @@ export function fetchTrends(configId: string, locationIds: string[], esClient: U ], fields: ['monitor.duration.us'], }; - console.log(JSON.stringify(query)); + return esClient.search({ body: query }); } diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/overview_trends.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/overview_trends.ts index 028b3569fa5af..1397b53fc7c33 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/overview_trends.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/overview_trends.ts @@ -15,12 +15,14 @@ export const createOverviewTrendsRoute: SyntheticsRestApiRouteFactory = () => ({ method: 'POST', path: SYNTHETICS_API_URLS.OVERVIEW_TRENDS, validate: { - body: schema.arrayOf( - schema.object({ - configId: schema.string(), - locationId: schema.string(), - }) - ), + body: schema.object({ + body: schema.arrayOf( + schema.object({ + configId: schema.string(), + locationId: schema.string(), + }) + ), + }), }, handler: async (routeContext): Promise => { return withSpan('fetch trends', async () => { @@ -40,41 +42,23 @@ export const createOverviewTrendsRoute: SyntheticsRestApiRouteFactory = () => ({ let main = {}; for (const res of results) { const aggregations = res.body.aggregations as any; - console.log('aggregations', aggregations); - aggregations.by_id.buckets.map( - ({ key, by_location }: { key: string; by_location: any }) => { - const ret: Record = {}; - for (const location of by_location.buckets) { - console.log('creating location bucket for ', key, location); - ret[key + location.key] = { - data: location.last_50.hits.hits - .reverse() - .map((hit, x) => ({ x, y: hit._source.monitor.duration.us })), - ...location.stats, - median: location.median.values['50.0'], - }; - } - console.log('ret', ret); - main = { ...main, ...ret }; + aggregations.byId.buckets.map(({ key, byLocation }: { key: string; byLocation: any }) => { + const ret: Record = {}; + for (const location of byLocation.buckets) { + ret[key + location.key] = { + configId: key, + locationId: location.key, + data: location.last_50.hits.hits + .reverse() + .map((hit: any, x: number) => ({ x, y: hit._source.monitor.duration.us })), + ...location.stats, + median: location.median.values['50.0'], + }; } - ); - // console.log('formatted', formatted); - // console.log('keys', Object.keys(formatted)); + main = { ...main, ...ret }; + }); } return routeContext.response.ok({ body: main }); - // const res = await fetchTrends(Array.from(configIds), Array.from(locationIds), esClient); - // const aggregations = res.body.aggregations as any; - // const ret: Record = {}; - // aggregations.by_id.buckets.map(({ key, by_location }: { key: string; by_location: any }) => { - // for (const location of by_location.buckets) { - // ret[key + location.key] = { - // data: location.last_50, - // ...location.stats, - // median: location.median.values['50.0'], - // }; - // } - // }); - // return routeContext.response.ok({ body: ret }); }); }, }); From ecac143103c320843e547a4c27d2ca8080c91024 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Thu, 27 Jun 2024 12:24:19 -0400 Subject: [PATCH 05/39] Fixup. --- .../server/routes/overview_trends/overview_trends.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/overview_trends.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/overview_trends.ts index 1397b53fc7c33..98544bb159ca5 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/overview_trends.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/overview_trends.ts @@ -15,14 +15,7 @@ export const createOverviewTrendsRoute: SyntheticsRestApiRouteFactory = () => ({ method: 'POST', path: SYNTHETICS_API_URLS.OVERVIEW_TRENDS, validate: { - body: schema.object({ - body: schema.arrayOf( - schema.object({ - configId: schema.string(), - locationId: schema.string(), - }) - ), - }), + body: schema.any(), }, handler: async (routeContext): Promise => { return withSpan('fetch trends', async () => { From c9468f1809e895b76bb138f2d71bd023c6236a53 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Thu, 27 Jun 2024 12:50:59 -0400 Subject: [PATCH 06/39] Cleanup. --- package.json | 4 +- .../grid_by_group/grid_group_item.tsx | 23 +++++++--- .../grid_by_group/grid_items_by_group.tsx | 2 +- .../overview/overview/metric_item.tsx | 28 +++---------- .../overview/overview/overview_grid.tsx | 5 ++- .../overview/overview/overview_grid_item.tsx | 42 ------------------- .../monitors_page/overview/overview/types.ts | 13 ++++++ 7 files changed, 42 insertions(+), 75 deletions(-) delete mode 100644 x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid_item.tsx create mode 100644 x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/types.ts diff --git a/package.json b/package.json index 38c65ed12fcac..16ef66cf2a789 100644 --- a/package.json +++ b/package.json @@ -1542,6 +1542,7 @@ "@types/react-test-renderer": "^17.0.2", "@types/react-virtualized": "^9.21.22", "@types/react-window": "^1.8.8", + "@types/react-window-infinite-loader": "^1.0.9", "@types/redux-actions": "^2.6.1", "@types/resolve": "^1.20.1", "@types/seedrandom": ">=2.0.0 <4.0.0", @@ -1758,8 +1759,7 @@ "xmlbuilder": "13.0.2", "yargs": "^15.4.1", "yarn-deduplicate": "^6.0.2", - "zod-to-json-schema": "^3.22.3", - "@types/react-window-infinite-loader": "^1.0.9" + "zod-to-json-schema": "^3.22.3" }, "packageManager": "yarn@1.22.21" } \ No newline at end of file diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/grid_by_group/grid_group_item.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/grid_by_group/grid_group_item.tsx index 8f92887da6ba8..7346eb85e631a 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/grid_by_group/grid_group_item.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/grid_by_group/grid_group_item.tsx @@ -17,13 +17,15 @@ import { EuiTablePagination, } from '@elastic/eui'; import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; import { useSelector } from 'react-redux'; import { useKey } from 'react-use'; import { OverviewLoader } from '../overview_loader'; import { useFilteredGroupMonitors } from './use_filtered_group_monitors'; import { MonitorOverviewItem } from '../../types'; -import { FlyoutParamProps, OverviewGridItem } from '../overview_grid_item'; import { selectOverviewStatus } from '../../../../../state/overview_status'; +import { MetricItem } from '../metric_item'; +import { FlyoutParamProps } from '../types'; const PER_ROW = 4; const DEFAULT_ROW_SIZE = 2; @@ -103,7 +105,10 @@ export const GroupGridItem = ({ isDisabled={groupMonitors.length === 0} className="fullScreenButton" iconType="fullScreen" - aria-label="Full screen" + aria-label={i18n.translate( + 'xpack.synthetics.groupGridItem.euiButtonIcon.fullScreenLabel', + { defaultMessage: 'Full screen' } + )} onClick={() => { if (fullScreenGroup) { setFullScreenGroup(''); @@ -117,7 +122,12 @@ export const GroupGridItem = ({ - {groupMonitors.length} Monitors + + {groupMonitors.length}{' '} + {i18n.translate('xpack.synthetics.groupGridItem.monitorsBadgeLabel', { + defaultMessage: 'Monitors', + })} + } @@ -136,7 +146,7 @@ export const GroupGridItem = ({ key={`${monitor.id}-${monitor.location?.id}`} data-test-subj="syntheticsOverviewGridItem" > - + ))} @@ -145,7 +155,10 @@ export const GroupGridItem = ({ )} ; - // medianDuration: number; - // avgDuration: number; - // minDuration: number; - // maxDuration: number; style?: React.CSSProperties; onClick: (params: { id: string; configId: string; location: string; locationId: string }) => void; }) => { - const d = useSelector(selectOverviewTrends); - console.log('trends', d); - const trendData = d[monitor.configId + monitor.location.id]; - console.log('trend data for monitor', monitor.name, monitor.location.id, monitor, trendData); + const trendData = useSelector(selectOverviewTrends)[monitor.configId + monitor.location.id]; const [isPopoverOpen, setIsPopoverOpen] = useState(false); const isErrorPopoverOpen = useSelector(selectErrorPopoverState); const locationName = useLocationName(monitor); @@ -85,12 +72,8 @@ export const MetricItem = ({ const dispatch = useDispatch(); - // return ( - //
- // {contents} - //
- // ); if (!trendData) return null; + return (
( - diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid_item.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid_item.tsx deleted file mode 100644 index abf871c659dee..0000000000000 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid_item.tsx +++ /dev/null @@ -1,42 +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 { MetricItem } from './metric_item'; -import { useLast50DurationChart, useStatusByLocationOverview } from '../../../../hooks'; -import { MonitorOverviewItem } from '../../../../../../../common/runtime_types'; -import { useSelector } from 'react-redux'; -import { selectOverviewTrends } from '../../../../state'; - -export interface FlyoutParamProps { - id: string; - configId: string; - location: string; - locationId: string; -} - -export const OverviewGridItem = ({ - monitor, - onClick, - style, -}: { - monitor: MonitorOverviewItem; - onClick: (params: FlyoutParamProps) => void; - style?: React.CSSProperties; -}) => { - return ( - - ); -}; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/types.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/types.ts new file mode 100644 index 0000000000000..a2e7d8581e657 --- /dev/null +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/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 FlyoutParamProps { + id: string; + configId: string; + location: string; + locationId: string; +} From 7859e3cbfd82aa1e6814501f057d6f8ba172b50d Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Thu, 27 Jun 2024 12:57:25 -0400 Subject: [PATCH 07/39] Cleanup. --- .../monitors_page/overview/overview_page.tsx | 8 +- .../hooks/use_last_50_duration_chart.test.ts | 186 ---------------- .../hooks/use_last_50_duration_chart.ts | 90 -------- .../hooks/use_last_x_checks.test.tsx | 203 ------------------ .../synthetics/hooks/use_last_x_checks.ts | 99 --------- .../hooks/use_monitors_sorted_by_status.tsx | 9 +- .../hooks/use_status_by_location_overview.ts | 1 - .../apps/synthetics/state/overview/api.ts | 1 - .../public/apps/synthetics/synthetics_app.tsx | 125 ++++++----- .../public/apps/synthetics/utils/on_render.ts | 20 -- .../normalizers/common_fields.ts | 1 - 11 files changed, 68 insertions(+), 675 deletions(-) delete mode 100644 x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_last_50_duration_chart.test.ts delete mode 100644 x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_last_50_duration_chart.ts delete mode 100644 x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_last_x_checks.test.tsx delete mode 100644 x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_last_x_checks.ts delete mode 100644 x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/on_render.ts diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview_page.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview_page.tsx index 94c1ec39915c9..1e464444528de 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview_page.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview_page.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { Profiler, useEffect } from 'react'; +import React, { useEffect } from 'react'; import { EuiFlexGroup, EuiSpacer, EuiFlexItem } from '@elastic/eui'; import { useDispatch, useSelector } from 'react-redux'; import { useTrackPageview } from '@kbn/observability-shared-plugin/public'; @@ -33,7 +33,6 @@ import { SearchField } from '../common/search_field'; import { NoMonitorsFound } from '../common/no_monitors_found'; import { OverviewErrors } from './overview/overview_errors/overview_errors'; import { AlertingCallout } from '../../common/alerting_callout/alerting_callout'; -import { onRender } from '../../../utils/on_render'; import { useMonitorsSortedByStatus } from '../../../hooks/use_monitors_sorted_by_status'; export const OverviewPage: React.FC = () => { @@ -61,7 +60,6 @@ export const OverviewPage: React.FC = () => { data: { monitors }, pageState, } = useSelector(selectOverviewState); - console.log('page state', pageState); const { loading: monitorsLoading, @@ -102,7 +100,7 @@ export const OverviewPage: React.FC = () => { const noMonitorFound = monitorsLoaded && overviewLoaded && monitors?.length === 0; return ( - + <> @@ -136,6 +134,6 @@ export const OverviewPage: React.FC = () => { ) : ( )} - + ); }; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_last_50_duration_chart.test.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_last_50_duration_chart.test.ts deleted file mode 100644 index 4c149ce74667f..0000000000000 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_last_50_duration_chart.test.ts +++ /dev/null @@ -1,186 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { renderHook } from '@testing-library/react-hooks'; -import * as hooks from './use_last_x_checks'; -import { useLast50DurationChart } from './use_last_50_duration_chart'; -import { WrappedHelper } from '../utils/testing'; - -describe('useLast50DurationChart', () => { - const getMockHits = (): Array<{ 'monitor.duration.us': number[] | undefined }> => { - const hits = []; - for (let i = 0; i < 10; i++) { - hits.push({ - 'monitor.duration.us': [i], - }); - } - return hits; - }; - - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('returns expected results', () => { - jest.spyOn(hooks, 'useLastXChecks').mockReturnValue({ hits: getMockHits(), loading: false }); - - const { result } = renderHook( - () => useLast50DurationChart({ monitorId: 'mock-id', locationId: 'loc', schedule: '1' }), - { wrapper: WrappedHelper } - ); - expect(result.current).toEqual({ - medianDuration: 5, - maxDuration: 9, - minDuration: 0, - avgDuration: 4.5, - data: [ - { - x: 0, - y: 9, - }, - { - x: 1, - y: 8, - }, - { - x: 2, - y: 7, - }, - { - x: 3, - y: 6, - }, - { - x: 4, - y: 5, - }, - { - x: 5, - y: 4, - }, - { - x: 6, - y: 3, - }, - { - x: 7, - y: 2, - }, - { - x: 8, - y: 1, - }, - { - x: 9, - y: 0, - }, - ], - loading: false, - }); - }); - - it('handles undefined monitor duration', () => { - const hitsWithAnUndefinedDuration = [...getMockHits()]; - hitsWithAnUndefinedDuration[1] = { 'monitor.duration.us': undefined }; - - jest - .spyOn(hooks, 'useLastXChecks') - .mockReturnValue({ hits: hitsWithAnUndefinedDuration, loading: false }); - const { result } = renderHook( - () => useLast50DurationChart({ monitorId: 'mock-id', locationId: 'loc', schedule: '10' }), - { wrapper: WrappedHelper } - ); - - const data = [ - { - x: 0, - y: 9, - }, - { - x: 1, - y: 8, - }, - { - x: 2, - y: 7, - }, - { - x: 3, - y: 6, - }, - { - x: 4, - y: 5, - }, - { - x: 5, - y: 4, - }, - { - x: 6, - y: 3, - }, - { - x: 7, - y: 2, - }, - { - x: 9, - y: 0, - }, - ]; - - expect(result.current).toEqual({ - medianDuration: [...data].sort((a, b) => a.y - b.y)[Math.floor(data.length / 2)].y, - maxDuration: 9, - minDuration: 0, - avgDuration: 4.4, - data, - loading: false, - }); - }); - - it('passes proper params to useLastXChecks', () => { - const hitsWithAnUndefinedDuration = [...getMockHits()]; - hitsWithAnUndefinedDuration[1] = { 'monitor.duration.us': undefined }; - const monitorId = 'mock-id'; - const locationId = 'loc'; - - const spy = jest - .spyOn(hooks, 'useLastXChecks') - .mockReturnValue({ hits: hitsWithAnUndefinedDuration, loading: false }); - renderHook(() => useLast50DurationChart({ monitorId, locationId, schedule: '120' }), { - wrapper: WrappedHelper, - }); - - expect(spy).toBeCalledTimes(1); - expect(spy).toBeCalledWith({ - monitorId, - locationId, - fields: ['monitor.duration.us'], - size: 50, - schedule: '120', - }); - }); - - it('returns loading properly', () => { - const loading = true; - - jest.spyOn(hooks, 'useLastXChecks').mockReturnValue({ hits: getMockHits(), loading }); - const { result } = renderHook( - () => useLast50DurationChart({ monitorId: 'mock-id', locationId: 'loc', schedule: '3' }), - { wrapper: WrappedHelper } - ); - renderHook( - () => useLast50DurationChart({ monitorId: 'test-id', locationId: 'loc', schedule: '5' }), - { - wrapper: WrappedHelper, - } - ); - expect(result.current.loading).toEqual(loading); - }); -}); diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_last_50_duration_chart.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_last_50_duration_chart.ts deleted file mode 100644 index 2280bf62b5c5b..0000000000000 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_last_50_duration_chart.ts +++ /dev/null @@ -1,90 +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 { useMemo } from 'react'; -import { useLastXChecks } from './use_last_x_checks'; - -const fields = ['monitor.duration.us']; - -// can we make this an ES query instead? -export function useLast50DurationChart({ - monitorId, - locationId, - timestamp, - schedule, -}: { - monitorId: string; - timestamp?: string; - locationId: string; - schedule: string; -}) { - return null; - const { hits, loading } = useLastXChecks<{ - 'monitor.duration.us': number[] | undefined; - }>({ - monitorId, - locationId, - fields, - size: 50, - timestamp, - schedule, - }); - return null; - const { data, median, min, max, avg } = useMemo(() => { - if (loading) { - return { - data: [], - median: 0, - avg: 0, - min: 0, - max: 0, - }; - } - - // calculate min, max, average duration and median - - const coords = hits - .reverse() // results are returned in desc order by timestamp. Reverse to ensure the data is in asc order by timestamp - .map((hit, index) => { - const duration = hit?.['monitor.duration.us']?.[0]; - if (duration === undefined) { - return null; - } - return { - x: index, - y: duration, - }; - }) - .filter((item) => item !== null); - - const sortedByDuration = [...hits].sort( - (a, b) => (a?.['monitor.duration.us']?.[0] || 0) - (b?.['monitor.duration.us']?.[0] || 0) - ); - - return { - data: coords as Array<{ x: number; y: number }>, - median: sortedByDuration[Math.floor(hits.length / 2)]?.['monitor.duration.us']?.[0] || 0, - avg: - sortedByDuration.reduce((acc, curr) => acc + (curr?.['monitor.duration.us']?.[0] || 0), 0) / - hits.length, - min: sortedByDuration[0]?.['monitor.duration.us']?.[0] || 0, - max: sortedByDuration[sortedByDuration.length - 1]?.['monitor.duration.us']?.[0] || 0, - }; - }, [hits, loading]); - - return useMemo( - () => ({ - data, - medianDuration: median, - avgDuration: avg, - minDuration: min, - maxDuration: max, - loading, - }), - [data, median, avg, min, max, loading] - ); -} diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_last_x_checks.test.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_last_x_checks.test.tsx deleted file mode 100644 index 8c16902357273..0000000000000 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_last_x_checks.test.tsx +++ /dev/null @@ -1,203 +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 { renderHook } from '@testing-library/react-hooks'; -import { getTimeRangeFilter, useLastXChecks } from './use_last_x_checks'; -import { WrappedHelper } from '../utils/testing'; -import * as searchHooks from './use_redux_es_search'; -import { SYNTHETICS_INDEX_PATTERN } from '../../../../common/constants'; - -describe('useLastXChecks', () => { - const getMockHits = (): Array<{ fields: { 'monitor.duration.us': number[] | undefined } }> => { - const hits = []; - for (let i = 0; i < 10; i++) { - hits.push({ - fields: { - 'monitor.duration.us': [i], - }, - }); - } - return hits; - }; - - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('returns expected results', () => { - jest.spyOn(searchHooks, 'useReduxEsSearch').mockReturnValue({ - data: { hits: { hits: getMockHits() } }, - } as any); - - const { result } = renderHook( - () => - useLastXChecks({ - monitorId: 'mock-id', - locationId: 'loc', - size: 30, - fields: ['monitor.duration.us'], - schedule: '10', - }), - { wrapper: WrappedHelper } - ); - expect(result.current).toEqual({ - hits: [ - { - 'monitor.duration.us': [0], - }, - { - 'monitor.duration.us': [1], - }, - { - 'monitor.duration.us': [2], - }, - { - 'monitor.duration.us': [3], - }, - { - 'monitor.duration.us': [4], - }, - { - 'monitor.duration.us': [5], - }, - { - 'monitor.duration.us': [6], - }, - { - 'monitor.duration.us': [7], - }, - { - 'monitor.duration.us': [8], - }, - { - 'monitor.duration.us': [9], - }, - ], - loading: false, - }); - }); - - it('passes proper params', () => { - const spy = jest.spyOn(searchHooks, 'useReduxEsSearch').mockReturnValue({ - data: { hits: { hits: getMockHits() } }, - } as any); - - const fields = ['monitor.summary']; - const size = 30; - - renderHook( - () => - useLastXChecks({ - monitorId: 'mock-id', - locationId: 'loc', - size, - fields, - schedule: '120', - }), - { wrapper: WrappedHelper } - ); - expect(spy).toBeCalledTimes(1); - expect(spy).toBeCalledWith( - expect.objectContaining({ body: expect.objectContaining({ fields, size }) }), - expect.anything(), - expect.anything() - ); - }); - - it('returns loading properly', () => { - jest.spyOn(searchHooks, 'useReduxEsSearch').mockReturnValue({ - data: { hits: { hits: getMockHits() } }, - } as any); - - const { result } = renderHook( - () => - useLastXChecks({ - monitorId: 'mock-id', - locationId: 'loc', - size: 30, - fields: ['monitor.duration.us'], - schedule: '240', - }), - { wrapper: WrappedHelper } - ); - expect(result.current.loading).toEqual(false); - }); - - it('returns loading true when there is no data', () => { - jest.spyOn(searchHooks, 'useReduxEsSearch').mockReturnValue({ - data: undefined, - } as any); - - const { result } = renderHook( - () => - useLastXChecks({ - monitorId: 'mock-id', - locationId: 'loc', - size: 30, - fields: ['monitor.duration.us'], - schedule: '1', - }), - { wrapper: WrappedHelper } - ); - expect(result.current.loading).toEqual(true); - }); - - it('calls useEsSearch with correct index', () => { - const spy = jest.spyOn(searchHooks, 'useReduxEsSearch').mockReturnValue({ - data: { hits: { hits: getMockHits() } }, - } as any); - - const WrapperWithState = ({ children }: { children: React.ReactElement }) => { - return ( - - {children} - - ); - }; - - renderHook( - () => - useLastXChecks({ - monitorId: 'mock-id', - locationId: 'loc', - size: 30, - fields: ['monitor.duration.us'], - schedule: '3', - }), - { wrapper: WrapperWithState } - ); - expect(spy).toBeCalledWith( - expect.objectContaining({ index: SYNTHETICS_INDEX_PATTERN }), - expect.anything(), - expect.anything() - ); - }); -}); - -describe('getTimeRangeFilter', () => { - it.each([ - [1, 'now-1h'], - [3, 'now-3h'], - [5, 'now-5h'], - [10, 'now-9h'], - [60, 'now-50h'], - [120, 'now-100h'], - [240, 'now-200h'], - ])('returns expected filter', (val, res) => { - const filter = getTimeRangeFilter(String(val)); - expect(filter).toEqual({ - range: { - '@timestamp': { - gte: res, - lte: 'now', - }, - }, - }); - }); -}); diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_last_x_checks.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_last_x_checks.ts deleted file mode 100644 index 047343db9979c..0000000000000 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_last_x_checks.ts +++ /dev/null @@ -1,99 +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 { useMemo } from 'react'; -import { useSelector } from 'react-redux'; -import { createEsParams } from '@kbn/observability-shared-plugin/public'; -import { useReduxEsSearch } from './use_redux_es_search'; -import { Ping } from '../../../../common/runtime_types'; - -import { - EXCLUDE_RUN_ONCE_FILTER, - getLocationFilter, - FINAL_SUMMARY_FILTER, -} from '../../../../common/constants/client_defaults'; -import { selectServiceLocationsState } from '../state'; -import { useSyntheticsRefreshContext } from '../contexts/synthetics_refresh_context'; -import { SYNTHETICS_INDEX_PATTERN, UNNAMED_LOCATION } from '../../../../common/constants'; - -export const getTimeRangeFilter = (schedule: string) => { - const inMinutes = Number(schedule); - const fiftyChecksInMinutes = inMinutes * 50; - const hours = Math.ceil(fiftyChecksInMinutes / 60); - return { - range: { - '@timestamp': { - gte: `now-${hours}h`, - lte: 'now', - }, - }, - }; -}; -let hits = 0; -export function useLastXChecks({ - monitorId, - locationId, - fields = ['*'], - size = 50, - timestamp, - schedule, -}: { - monitorId: string; - schedule: string; - locationId: string; - timestamp?: string; - fields?: string[]; - size?: number; -}) { - const { lastRefresh } = useSyntheticsRefreshContext(); - const { locationsLoaded, locations } = useSelector(selectServiceLocationsState); - - const params = createEsParams({ - index: SYNTHETICS_INDEX_PATTERN, - body: { - size, - query: { - bool: { - filter: [ - FINAL_SUMMARY_FILTER, - EXCLUDE_RUN_ONCE_FILTER, - getTimeRangeFilter(schedule), - { - term: { - 'monitor.id': monitorId, - }, - }, - ], - ...getLocationFilter({ - locationId, - locationName: - locations.find((location) => location.id === locationId)?.label || UNNAMED_LOCATION, - }), - }, - }, - _source: false, - sort: [{ '@timestamp': 'desc' }], - fields, - }, - }); - console.log('query', params); - - return null; - const { data } = useReduxEsSearch(params, [lastRefresh], { - name: `zGetLastXChecks/${monitorId}/${locationId}`, - isRequestReady: locationsLoaded && Boolean(timestamp), // don't run query until locations are loaded - }); - - const dataAsJSON = JSON.stringify(data?.hits?.hits); - - return useMemo(() => { - return { - hits: (data?.hits?.hits.map((hit) => hit.fields) as Fields[]) || [], - loading: !data, - }; - }, [dataAsJSON]); // eslint-disable-line react-hooks/exhaustive-deps -} diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_monitors_sorted_by_status.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_monitors_sorted_by_status.tsx index 38aba0bdba7c2..73f9644749351 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_monitors_sorted_by_status.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_monitors_sorted_by_status.tsx @@ -24,7 +24,6 @@ export function useMonitorsSortedByStatus() { const downMonitors = useRef | null>(null); const monitorsSortedByStatus = useMemo(() => { - console.log('has status', !!status, status); if (!status) { return { down: [], @@ -51,16 +50,16 @@ export function useMonitorsSortedByStatus() { monitors.forEach((monitor) => { if (!monitor.isEnabled) { - orderedDisabledMonitors.push({ ...monitor, status: 'disabled' }); + orderedDisabledMonitors.push(monitor); } else if ( monitor.configId in downMonitorMap && downMonitorMap[monitor.configId].includes(monitor.location.id) ) { - orderedDownMonitors.push({ ...monitor, status: 'down' }); + orderedDownMonitors.push(monitor); } else if (pendingConfigs?.[`${monitor.configId}-${monitor.location.id}`]) { - orderedPendingMonitors.push({ ...monitor, status: 'pending' }); + orderedPendingMonitors.push(monitor); } else { - orderedUpMonitors.push({ ...monitor, status: 'up' }); + orderedUpMonitors.push(monitor); } }); downMonitors.current = downMonitorMap; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_status_by_location_overview.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_status_by_location_overview.ts index a60abd6e97a5f..f7c5b57984a4b 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_status_by_location_overview.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_status_by_location_overview.ts @@ -9,7 +9,6 @@ import { useSelector } from 'react-redux'; import { OverviewStatusState } from '../../../../common/runtime_types'; import { selectOverviewStatus } from '../state/overview_status'; -// switch this to a component that contains this logic? Unnecessary splintering export function useStatusByLocationOverview({ configId, locationId, diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/api.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/api.ts index 240f80becd67e..7a3307fe6e88c 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/api.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/api.ts @@ -61,6 +61,5 @@ export const fetchOverviewStatus = async ( export const fetchOverviewTrendStats = async ( filters: Array<{ configId: string; locationId: string }> ): Promise => { - console.log('in api', filters); return apiService.post(SYNTHETICS_API_URLS.OVERVIEW_TRENDS, filters); }; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/synthetics_app.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/synthetics_app.tsx index 6095b4b4a877f..93c45442695ad 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/synthetics_app.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/synthetics_app.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { Profiler, useEffect } from 'react'; +import React, { useEffect } from 'react'; import { Provider as ReduxProvider } from 'react-redux'; import { APP_WRAPPER_CLASS } from '@kbn/core/public'; @@ -31,7 +31,6 @@ import { import { SyntheticsDataViewContextProvider } from './contexts/synthetics_data_view_context'; import { PageRouter } from './routes'; import { setBasePath, storage, store } from './state'; -import { onRender } from './utils/on_render'; const Application = (props: SyntheticsAppProps) => { const { @@ -70,68 +69,66 @@ const Application = (props: SyntheticsAppProps) => { store.dispatch(setBasePath(basePath)); return ( - {}}> - - - - - - - - - - - -
- - - - - - - -
-
-
-
-
-
-
-
-
-
-
-
-
+ + + + + + + + + + + +
+ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
); }; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/on_render.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/on_render.ts deleted file mode 100644 index df8f0a005bde4..0000000000000 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/on_render.ts +++ /dev/null @@ -1,20 +0,0 @@ -let renderCount = 0; -export function onRender( - id: any, - phase: any, - actualDuration: any, - baseDuration: any, - startTime: any, - commitTime: any, - interactions: any -) { - console.log('onRender', `count: ${renderCount++}`, { - id, - phase, - actualDuration, - baseDuration, - startTime, - commitTime, - interactions, - }); -} diff --git a/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/project_monitor/normalizers/common_fields.ts b/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/project_monitor/normalizers/common_fields.ts index efbd43f155285..4019151dc44f6 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/project_monitor/normalizers/common_fields.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/project_monitor/normalizers/common_fields.ts @@ -146,7 +146,6 @@ export const getMonitorSchedule = ( schedule: number | string | MonitorFields['schedule'], defaultValue?: MonitorFields['schedule'] ) => { - console.log('schedule', schedule); if (!schedule && defaultValue) { return defaultValue; } From 541a53932414b6eeef1e7f8c0315966aa3c39f7c Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Thu, 27 Jun 2024 13:04:00 -0400 Subject: [PATCH 08/39] Cleanup. --- .../synthetics/public/apps/synthetics/hooks/index.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/index.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/index.ts index 2e5d9e2f76d66..ce0163ff74fb9 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/index.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/index.ts @@ -10,8 +10,6 @@ export * from './use_url_params'; export * from './use_breadcrumbs'; export * from './use_enablement'; export * from './use_locations'; -export * from './use_last_x_checks'; -export * from './use_last_50_duration_chart'; export * from './use_location_name'; export * from './use_status_by_location'; export * from './use_status_by_location_overview'; From 5f7f0e84ecf419688b56a0d6141bf759b84ead4c Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Mon, 1 Jul 2024 12:29:07 -0400 Subject: [PATCH 09/39] Cleanup types. --- .../overview/overview/overview_grid.tsx | 335 +++++++++--------- .../hooks/use_monitors_sorted_by_status.tsx | 5 +- 2 files changed, 173 insertions(+), 167 deletions(-) diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx index 0f34d7cfa90af..d0d8a9f4f8539 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx @@ -36,187 +36,190 @@ import { MonitorDetailFlyout } from './monitor_detail_flyout'; import { useSyntheticsRefreshContext } from '../../../../contexts'; import { MetricItem } from './metric_item'; import { FlyoutParamProps } from './types'; +import { MonitorOverviewItem } from '../types'; const ITEM_HEIGHT = 172; -export const OverviewGrid = memo(({ monitorsSortedByStatus }: { monitorsSortedByStatus: any }) => { - const { status } = useSelector(selectOverviewStatus); +export const OverviewGrid = memo( + ({ monitorsSortedByStatus }: { monitorsSortedByStatus: MonitorOverviewItem[] }) => { + const { status } = useSelector(selectOverviewStatus); - const { - data: { monitors }, - flyoutConfig, - loaded, - pageState, - groupBy: { field: groupField }, - } = useSelector(selectOverviewState); - const { perPage } = pageState; - const [page, setPage] = useState(1); - const [vpage, setvpage] = useState([]); + const { + data: { monitors }, + flyoutConfig, + loaded, + pageState, + groupBy: { field: groupField }, + } = useSelector(selectOverviewState); + const { perPage } = pageState; + const [page, setPage] = useState(1); + const [vpage, setvpage] = useState([]); - const dispatch = useDispatch(); - const intersectionRef = useRef(null); + const dispatch = useDispatch(); + const intersectionRef = useRef(null); - const setFlyoutConfigCallback = useCallback( - (params: FlyoutParamProps) => dispatch(setFlyoutConfig(params)), - [dispatch] - ); - const hideFlyout = useCallback(() => dispatch(setFlyoutConfig(null)), [dispatch]); - const { lastRefresh } = useSyntheticsRefreshContext(); - const forceRefreshCallback = useCallback( - () => dispatch(quietFetchOverviewAction.get(pageState)), - [dispatch, pageState] - ); + const setFlyoutConfigCallback = useCallback( + (params: FlyoutParamProps) => dispatch(setFlyoutConfig(params)), + [dispatch] + ); + const hideFlyout = useCallback(() => dispatch(setFlyoutConfig(null)), [dispatch]); + const { lastRefresh } = useSyntheticsRefreshContext(); + const forceRefreshCallback = useCallback( + () => dispatch(quietFetchOverviewAction.get(pageState)), + [dispatch, pageState] + ); - const listHeight = Math.min(ITEM_HEIGHT * (monitorsSortedByStatus.length / 4), 800); - const listRef: React.LegacyRef> | undefined = React.createRef(); - const infiniteLoaderRef: React.LegacyRef = React.createRef(); - useEffect(() => { - dispatch(refreshOverviewTrends.get()); - }, [dispatch, lastRefresh]); + const listHeight = Math.min(ITEM_HEIGHT * (monitorsSortedByStatus.length / 4), 800); + const listRef: React.LegacyRef> | undefined = React.createRef(); + const infiniteLoaderRef: React.LegacyRef = React.createRef(); + useEffect(() => { + dispatch(refreshOverviewTrends.get()); + }, [dispatch, lastRefresh]); - // Display no monitors found when down, up, or disabled filter produces no results - if (status && !monitorsSortedByStatus.length && loaded) { - return ; - } + // Display no monitors found when down, up, or disabled filter produces no results + if (status && !monitorsSortedByStatus.length && loaded) { + return ; + } - return ( - <> - - - - - - setPage(1)} /> - - - - - - -
- {groupField === 'none' ? ( - loaded && monitorsSortedByStatus.length ? ( - - {({ width }: EuiAutoSize) => ( - vpage[idx] !== undefined} - itemCount={monitorsSortedByStatus.length + 1 / 4} - loadMoreItems={(start: number, stop: number) => { - const newRows = []; - for (let i = start; i < stop; i++) { - newRows.push(monitorsSortedByStatus.slice(i * 4, i * 4 + 4)); - } - const fetchStatsActionPayload = []; - for (const newRow of newRows) { - for (const item of newRow) { - fetchStatsActionPayload.push({ - configId: item.configId, - locationId: item.location.id, - }); + return ( + <> + + + + + + setPage(1)} /> + + + + + + +
+ {groupField === 'none' ? ( + loaded && monitorsSortedByStatus.length ? ( + + {({ width }: EuiAutoSize) => ( + vpage[idx] !== undefined} + itemCount={monitorsSortedByStatus.length + 1 / 4} + loadMoreItems={(start: number, stop: number) => { + const newRows = []; + for (let i = start; i < stop; i++) { + newRows.push(monitorsSortedByStatus.slice(i * 4, i * 4 + 4)); } - } - dispatch(trendStatsBatch.get(fetchStatsActionPayload)); - setvpage([...vpage, ...newRows]); - }} - minimumBatchSize={20} - threshold={30} - > - {({ onItemsRendered, ref }) => ( - - {(props) => { - return ( - - {props.data - .slice(props.index * 4, props.index * 4 + 4) - .map((item: any, idx: number) => ( - - - - ))} - - ); - }} - - )} - - )} - + const fetchStatsActionPayload = []; + for (const newRow of newRows) { + for (const item of newRow) { + fetchStatsActionPayload.push({ + configId: item.configId, + locationId: item.location.id, + }); + } + } + dispatch(trendStatsBatch.get(fetchStatsActionPayload)); + setvpage([...vpage, ...newRows]); + }} + minimumBatchSize={20} + threshold={30} + > + {({ onItemsRendered, ref }) => ( + + {(props) => { + return ( + + {props.data + .slice(props.index * 4, props.index * 4 + 4) + .map((item: any, idx: number) => ( + + + + ))} + + ); + }} + + )} + + )} + + ) : ( + + ) ) : ( - - ) - ) : ( - - )} - -
-
- -
- {groupField === 'none' && ( - - {monitorsSortedByStatus.length === monitors.length && ( - - {SHOWING_ALL_MONITORS_LABEL} - + )} - {monitorsSortedByStatus.length === monitors.length && - monitorsSortedByStatus.length > perPage && ( + +
+
+ +
+ {groupField === 'none' && ( + + {monitorsSortedByStatus.length === monitors.length && ( - { - window.scrollTo({ top: 0, left: 0, behavior: 'smooth' }); - listRef.current?.scrollToItem(0); - }} - iconType="sortUp" - iconSide="right" - size="xs" - > - {SCROLL_TO_TOP_LABEL} - + {SHOWING_ALL_MONITORS_LABEL} )} - - )} - {flyoutConfig?.configId && flyoutConfig?.location && ( - - )} - - ); -}); + {monitorsSortedByStatus.length === monitors.length && + monitorsSortedByStatus.length > perPage && ( + + { + window.scrollTo({ top: 0, left: 0, behavior: 'smooth' }); + listRef.current?.scrollToItem(0); + }} + iconType="sortUp" + iconSide="right" + size="xs" + > + {SCROLL_TO_TOP_LABEL} + + + )} + + )} + {flyoutConfig?.configId && flyoutConfig?.location && ( + + )} + + ); + } +); const SHOWING_ALL_MONITORS_LABEL = i18n.translate( 'xpack.synthetics.overview.grid.showingAllMonitors.label', diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_monitors_sorted_by_status.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_monitors_sorted_by_status.tsx index 73f9644749351..c0a59c31a8760 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_monitors_sorted_by_status.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_monitors_sorted_by_status.tsx @@ -12,7 +12,10 @@ import { MonitorOverviewItem } from '../../../../common/runtime_types'; import { selectOverviewState } from '../state/overview'; import { useGetUrlParams } from './use_url_params'; -export function useMonitorsSortedByStatus() { +export function useMonitorsSortedByStatus(): { + monitorsSortedByStatus: MonitorOverviewItem[]; + downMonitors: Record | null; +} { const { statusFilter } = useGetUrlParams(); const { status } = useSelector(selectOverviewStatus); From 5b56fe6e721c941b15c53a20f0c14a6aeb842abe Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Tue, 2 Jul 2024 11:38:57 -0400 Subject: [PATCH 10/39] Fix color generation code. --- .../components/monitors_page/overview/overview/metric_item.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx index 0aa2f3ec6213f..a7c688c353ea1 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx @@ -173,7 +173,7 @@ export const MetricItem = ({ ), valueFormatter: (x: number) => formatDuration(x), - color: getColor(theme, monitor.isEnabled, monitor.status), + color: getColor(theme, monitor.isEnabled, status), }, ], ]} From 31f1bcbef1f377cd511cbbb0d0f5c63769be48fe Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Wed, 3 Jul 2024 12:26:51 -0400 Subject: [PATCH 11/39] TEMP --- .../overview/overview/metric_item.tsx | 156 +++++++++--------- .../overview/overview/overview_grid.tsx | 93 +++++++---- .../apps/synthetics/state/overview/api.ts | 4 +- .../apps/synthetics/state/overview/effects.ts | 11 +- 4 files changed, 149 insertions(+), 115 deletions(-) diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx index a7c688c353ea1..070442ecc695a 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx @@ -72,8 +72,6 @@ export const MetricItem = ({ const dispatch = useDispatch(); - if (!trendData) return null; - return (
- - { - if (testInProgress) { - dispatch(toggleTestNowFlyoutAction(monitor.configId)); - dispatch(toggleErrorPopoverOpen(null)); - } else { - dispatch(hideTestNowFlyoutAction()); - dispatch(toggleErrorPopoverOpen(null)); - } - if (!testInProgress && locationName) { - onClick({ - configId: monitor.configId, - id: monitor.id, - location: locationName, - locationId: monitor.location.id, - }); - } - }} - // TODO connect to charts.theme service see src/plugins/charts/public/services/theme/README.md - baseTheme={DARK_THEME} - locale={i18n.getLocale()} - /> - - - {i18n.translate('xpack.synthetics.overview.duration.label', { - defaultMessage: 'Duration', - })} - - - {monitor.name}
} + {!!trendData && ( + + { + if (testInProgress) { + dispatch(toggleTestNowFlyoutAction(monitor.configId)); + dispatch(toggleErrorPopoverOpen(null)); + } else { + dispatch(hideTestNowFlyoutAction()); + dispatch(toggleErrorPopoverOpen(null)); + } + if (!testInProgress && locationName) { + onClick({ + configId: monitor.configId, + id: monitor.id, + location: locationName, + locationId: monitor.location.id, + }); + } + }} + // TODO connect to charts.theme service see src/plugins/charts/public/services/theme/README.md + baseTheme={DARK_THEME} + locale={i18n.getLocale()} + /> + + + {i18n.translate('xpack.synthetics.overview.duration.label', { + defaultMessage: 'Duration', })} - content={i18n.translate( - 'xpack.synthetics.overview.duration.description.values', - { - defaultMessage: 'Avg: {avg}, Min: {min}, Max: {max}', - values: { - avg: formatDuration(trendData.avg, { noSpace: true }), - min: formatDuration(trendData.min, { noSpace: true }), - max: formatDuration(trendData.max, { noSpace: true }), - }, - } - )} - position="top" - /> - - - ), - valueFormatter: (x: number) => formatDuration(x), - color: getColor(theme, monitor.isEnabled, status), - }, - ], - ]} - /> - +
+ + + + + ), + valueFormatter: (x: number) => formatDuration(x), + color: getColor(theme, monitor.isEnabled, status), + }, + ], + ]} + /> +
+ )}
([]); + const [maxItem, setMaxItem] = useState(0); + console.log('vpage', vpage); + console.log('load data up to row', maxItem); + + // offload the fetching of trend data to a new effect and the list will work better const dispatch = useDispatch(); const intersectionRef = useRef(null); @@ -70,6 +75,23 @@ export const OverviewGrid = memo( ); const listHeight = Math.min(ITEM_HEIGHT * (monitorsSortedByStatus.length / 4), 800); + console.log('list height', listHeight); + let items = []; /* + // feasible technical way to match entities with rules, for rule types we have + // - today, there are many ways to configure alerts and are tied to data streams + // - APM, on the other hand, can be entity-based, like a namespace or service, and we can have a strucured association between the entity and the alert + - when we get to a point that a user is looking through alist of entities, they will eventually be devfining alerts based on the entity + - when we're at that point we can see the alerts defined for a given entity. At that point we can allow people to define how well the alert is working to send information about prbolemsn with the given entity and allow them to configure their alerts properly + // enabling ml/alerts/slo by default, create poc that integrates these with an onboarding flow + // nginx, apache, problem child, lmd, dga + */ + const listItems: any = []; + let ind = 0; + do { + items = monitorsSortedByStatus.slice(ind, ind + 4); + ind += 4; + if (items.length) listItems.push(items); + } while (items.length); const listRef: React.LegacyRef> | undefined = React.createRef(); const infiniteLoaderRef: React.LegacyRef = React.createRef(); useEffect(() => { @@ -111,51 +133,54 @@ export const OverviewGrid = memo( {({ width }: EuiAutoSize) => ( vpage[idx] !== undefined} - itemCount={monitorsSortedByStatus.length + 1 / 4} - loadMoreItems={(start: number, stop: number) => { - const newRows = []; - for (let i = start; i < stop; i++) { - newRows.push(monitorsSortedByStatus.slice(i * 4, i * 4 + 4)); - } - const fetchStatsActionPayload = []; - for (const newRow of newRows) { - for (const item of newRow) { - fetchStatsActionPayload.push({ - configId: item.configId, - locationId: item.location.id, - }); - } - } - dispatch(trendStatsBatch.get(fetchStatsActionPayload)); - setvpage([...vpage, ...newRows]); + isItemLoaded={(idx: number) => { + return true; + console.log('isitemloaded', idx); + return vpage[idx] !== undefined; + }} + itemCount={listItems.length} + loadMoreItems={(_start: number, stop: number) => { + setMaxItem(stop); + // console.log('load more items', start, stop); + // const newRows = []; + // const slice = listItems.slice(start, stop); + // console.log('slice for', start, stop, slice, monitorsSortedByStatus); + // const mapped = slice + // .flatMap((x: any) => x) + // .map(({ configId, location }: any) => ({ + // configId, + // locationId: location.id, + // })); + // console.log('mapped', mapped); + // console.log('dispatching for ', start, stop); + // dispatch(trendStatsBatch.get(mapped)); + // setvpage([...vpage, ...slice]); }} - minimumBatchSize={20} - threshold={30} + minimumBatchSize={8} + threshold={8} > {({ onItemsRendered, ref }) => ( {(props) => { + console.log('the props', props); return ( - {props.data - .slice(props.index * 4, props.index * 4 + 4) - .map((item: any, idx: number) => ( - - - - ))} + {props.data[props.index].map((item: any, idx: number) => ( + + + + ))} ); }} @@ -170,7 +195,7 @@ export const OverviewGrid = memo( ) : ( )} diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/api.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/api.ts index 7a3307fe6e88c..3ffc4e30bc6c7 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/api.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/api.ts @@ -61,5 +61,7 @@ export const fetchOverviewStatus = async ( export const fetchOverviewTrendStats = async ( filters: Array<{ configId: string; locationId: string }> ): Promise => { - return apiService.post(SYNTHETICS_API_URLS.OVERVIEW_TRENDS, filters); + if (!filters.length) return []; + const res = apiService.post(SYNTHETICS_API_URLS.OVERVIEW_TRENDS, filters); + return res; }; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.ts index 9ab6e1670dd28..1dae315509e56 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.ts @@ -48,10 +48,13 @@ export function* refreshOverviewTrendStats() { do { const res = yield call( trendsApi, - keys.splice(0, keys.length < 10 ? keys.length : 10).map((key: string) => ({ - configId: trends[key].configId, - locationId: trends[key].locationId, - })) + keys + .splice(0, keys.length < 10 ? keys.length : 10) + .filter((key: string) => trends[key] !== null) + .map((key: string) => ({ + configId: trends[key].configId, + locationId: trends[key].locationId, + })) ); all = { ...all, ...res }; } while (keys.length); From 3368b3a08f1f79740db3942470e7f7b1bc87636f Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Mon, 8 Jul 2024 11:02:31 -0400 Subject: [PATCH 12/39] TEMP --- .../overview/overview/metric_item.tsx | 160 +++++++++--------- .../overview/overview/overview_grid.tsx | 44 +++-- .../apps/synthetics/state/overview/effects.ts | 21 ++- 3 files changed, 127 insertions(+), 98 deletions(-) diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx index 070442ecc695a..d6bf3d94826a1 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx @@ -59,6 +59,7 @@ export const MetricItem = ({ onClick: (params: { id: string; configId: string; location: string; locationId: string }) => void; }) => { const trendData = useSelector(selectOverviewTrends)[monitor.configId + monitor.location.id]; + console.log('trend data for monitor', monitor.name, trendData, monitor.configId); const [isPopoverOpen, setIsPopoverOpen] = useState(false); const isErrorPopoverOpen = useSelector(selectErrorPopoverState); const locationName = useLocationName(monitor); @@ -102,87 +103,86 @@ export const MetricItem = ({ `} title={moment(timestamp).format('LLL')} > - {!trendData &&
{monitor.name}
} - {!!trendData && ( - - { - if (testInProgress) { - dispatch(toggleTestNowFlyoutAction(monitor.configId)); - dispatch(toggleErrorPopoverOpen(null)); - } else { - dispatch(hideTestNowFlyoutAction()); - dispatch(toggleErrorPopoverOpen(null)); - } - if (!testInProgress && locationName) { - onClick({ - configId: monitor.configId, - id: monitor.id, - location: locationName, - locationId: monitor.location.id, - }); - } - }} - // TODO connect to charts.theme service see src/plugins/charts/public/services/theme/README.md - baseTheme={DARK_THEME} - locale={i18n.getLocale()} - /> - - - {i18n.translate('xpack.synthetics.overview.duration.label', { - defaultMessage: 'Duration', + {/* {!trendData &&
{monitor.name}
} */} + {/* {!!trendData && ( */} + + { + if (testInProgress) { + dispatch(toggleTestNowFlyoutAction(monitor.configId)); + dispatch(toggleErrorPopoverOpen(null)); + } else { + dispatch(hideTestNowFlyoutAction()); + dispatch(toggleErrorPopoverOpen(null)); + } + if (!testInProgress && locationName) { + onClick({ + configId: monitor.configId, + id: monitor.id, + location: locationName, + locationId: monitor.location.id, + }); + } + }} + // TODO connect to charts.theme service see src/plugins/charts/public/services/theme/README.md + baseTheme={DARK_THEME} + locale={i18n.getLocale()} + /> + + + {i18n.translate('xpack.synthetics.overview.duration.label', { + defaultMessage: 'Duration', + })} + + + - - - - - ), - valueFormatter: (x: number) => formatDuration(x), - color: getColor(theme, monitor.isEnabled, status), - }, - ], - ]} - /> - - )} + content={i18n.translate( + 'xpack.synthetics.overview.duration.description.values', + { + defaultMessage: 'Avg: {avg}, Min: {min}, Max: {max}', + values: { + avg: formatDuration(trendData.avg, { noSpace: true }), + min: formatDuration(trendData.min, { noSpace: true }), + max: formatDuration(trendData.max, { noSpace: true }), + }, + } + )} + position="top" + /> +
+ + ) : ( +
Loading metrics
+ ), + valueFormatter: (x: number) => formatDuration(x), + color: getColor(theme, monitor.isEnabled, status), + }, + ], + ]} + /> +
+ {/* )} */}
{ + if (monitorsSortedByStatus.length && maxItem) { + const batch = []; + const slice = monitorsSortedByStatus.slice(0, (maxItem + 1) * 4); + for (const item of slice) { + if (trendData[item.configId + item.location.id] === undefined) { + batch.push({ configId: item.configId, locationId: item.location.id }); + } + } + console.log('dispatching for batch', maxItem, batch); + if (batch.length) dispatch(trendStatsBatch.get(batch)); + } + }, [dispatch, maxItem, monitorsSortedByStatus, trendData]); + const listHeight = Math.min(ITEM_HEIGHT * (monitorsSortedByStatus.length / 4), 800); console.log('list height', listHeight); - let items = []; /* - // feasible technical way to match entities with rules, for rule types we have - // - today, there are many ways to configure alerts and are tied to data streams - // - APM, on the other hand, can be entity-based, like a namespace or service, and we can have a strucured association between the entity and the alert - - when we get to a point that a user is looking through alist of entities, they will eventually be devfining alerts based on the entity - - when we're at that point we can see the alerts defined for a given entity. At that point we can allow people to define how well the alert is working to send information about prbolemsn with the given entity and allow them to configure their alerts properly - // enabling ml/alerts/slo by default, create poc that integrates these with an onboarding flow - // nginx, apache, problem child, lmd, dga - */ + let items = []; const listItems: any = []; let ind = 0; + console.log('page state', pageState, monitorsSortedByStatus); do { items = monitorsSortedByStatus.slice(ind, ind + 4); ind += 4; @@ -134,14 +143,19 @@ export const OverviewGrid = memo( { - return true; console.log('isitemloaded', idx); - return vpage[idx] !== undefined; + const row = listItems[idx]; + for (const monitor of row) { + if (!trendData[monitor.configId + monitor.location.id]) { + return false; + } + } + return true; }} itemCount={listItems.length} loadMoreItems={(_start: number, stop: number) => { - setMaxItem(stop); - // console.log('load more items', start, stop); + setMaxItem(Math.max(maxItem, stop)); + console.log('load more items', stop); // const newRows = []; // const slice = listItems.slice(start, stop); // console.log('slice for', start, stop, slice, monitorsSortedByStatus); @@ -156,8 +170,8 @@ export const OverviewGrid = memo( // dispatch(trendStatsBatch.get(mapped)); // setvpage([...vpage, ...slice]); }} - minimumBatchSize={8} - threshold={8} + minimumBatchSize={20} + threshold={12} > {({ onItemsRendered, ref }) => ( + ): Generator> { + try { + const chunkSize = 40; + for (let i = action.payload.length; i > 0; i -= chunkSize) { + const chunk = action.payload.slice(Math.max(i - chunkSize, 0), i); + if (chunk.length > 0) { + const res = yield call(trendsApi, chunk); + yield put(trendStatsBatch.success(res)); + } + } + } catch (e: any) { + yield put(trendStatsBatch.fail(e)); + } + } ); } From e3f2a5b18f429e18de57c72dd30a502e55f5aa7f Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Wed, 10 Jul 2024 13:42:46 -0400 Subject: [PATCH 13/39] Cleanup. --- .../overview/overview/overview_grid.tsx | 68 +++++++------------ 1 file changed, 24 insertions(+), 44 deletions(-) diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx index a226f031ca098..8990f2c5487de 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useState, useRef, memo, useCallback, useEffect } from 'react'; +import React, { useState, useRef, memo, useCallback, useEffect, useMemo } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { i18n } from '@kbn/i18n'; import InfiniteLoader from 'react-window-infinite-loader'; @@ -40,6 +40,7 @@ import { FlyoutParamProps } from './types'; import { MonitorOverviewItem } from '../types'; const ITEM_HEIGHT = 172; +const ROW_COUNT = 4; export const OverviewGrid = memo( ({ monitorsSortedByStatus }: { monitorsSortedByStatus: MonitorOverviewItem[] }) => { @@ -54,10 +55,7 @@ export const OverviewGrid = memo( } = useSelector(selectOverviewState); const { perPage } = pageState; const [page, setPage] = useState(1); - const [vpage, setvpage] = useState([]); const [maxItem, setMaxItem] = useState(0); - console.log('vpage', vpage); - console.log('load data up to row', maxItem); const trendData = useSelector(selectOverviewTrends); // offload the fetching of trend data to a new effect and the list will work better @@ -79,28 +77,25 @@ export const OverviewGrid = memo( useEffect(() => { if (monitorsSortedByStatus.length && maxItem) { const batch = []; - const slice = monitorsSortedByStatus.slice(0, (maxItem + 1) * 4); + const slice = monitorsSortedByStatus.slice(0, (maxItem + 1) * ROW_COUNT); for (const item of slice) { if (trendData[item.configId + item.location.id] === undefined) { batch.push({ configId: item.configId, locationId: item.location.id }); } } - console.log('dispatching for batch', maxItem, batch); if (batch.length) dispatch(trendStatsBatch.get(batch)); } }, [dispatch, maxItem, monitorsSortedByStatus, trendData]); - const listHeight = Math.min(ITEM_HEIGHT * (monitorsSortedByStatus.length / 4), 800); - console.log('list height', listHeight); - let items = []; - const listItems: any = []; - let ind = 0; - console.log('page state', pageState, monitorsSortedByStatus); - do { - items = monitorsSortedByStatus.slice(ind, ind + 4); - ind += 4; - if (items.length) listItems.push(items); - } while (items.length); + const listHeight = Math.min(ITEM_HEIGHT * (monitorsSortedByStatus.length / ROW_COUNT), 800); + const listItems: Array> = useMemo(() => { + const acc = []; + for (let i = 0; i < monitorsSortedByStatus.length; i += ROW_COUNT) { + acc.push(monitorsSortedByStatus.slice(i, i + ROW_COUNT)); + } + return acc; + }, [monitorsSortedByStatus]); + const listRef: React.LegacyRef> | undefined = React.createRef(); const infiniteLoaderRef: React.LegacyRef = React.createRef(); useEffect(() => { @@ -143,37 +138,18 @@ export const OverviewGrid = memo( { - console.log('isitemloaded', idx); - const row = listItems[idx]; - for (const monitor of row) { - if (!trendData[monitor.configId + monitor.location.id]) { - return false; - } - } - return true; + return listItems[idx].every( + (m: any) => !!trendData[m.configId + m.location.id] + ); }} itemCount={listItems.length} loadMoreItems={(_start: number, stop: number) => { setMaxItem(Math.max(maxItem, stop)); - console.log('load more items', stop); - // const newRows = []; - // const slice = listItems.slice(start, stop); - // console.log('slice for', start, stop, slice, monitorsSortedByStatus); - // const mapped = slice - // .flatMap((x: any) => x) - // .map(({ configId, location }: any) => ({ - // configId, - // locationId: location.id, - // })); - // console.log('mapped', mapped); - // console.log('dispatching for ', start, stop); - // dispatch(trendStatsBatch.get(mapped)); - // setvpage([...vpage, ...slice]); }} minimumBatchSize={20} threshold={12} > - {({ onItemsRendered, ref }) => ( + {({ onItemsRendered }) => ( {(props) => { - console.log('the props', props); return ( - + {props.data[props.index].map((item: any, idx: number) => ( - + ))} + {props.data[props.index].length % ROW_COUNT !== 0 && + // Adds empty items to fill out row + Array.from({ + length: ROW_COUNT - props.data[props.index].length, + }).map((_, idx) => )} ); }} From c1dd4e74f5193a26578fcfe6da0320cb8c425436 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Wed, 10 Jul 2024 16:00:12 -0400 Subject: [PATCH 14/39] Cleanup. --- .../grid_by_group/grid_group_item.tsx | 2 +- .../overview/overview/metric_item.tsx | 19 +- .../overview/overview/overview_grid.tsx | 332 +++++++++--------- .../monitors_page/overview/overview_page.tsx | 5 +- .../apps/synthetics/state/overview/api.ts | 25 +- .../apps/synthetics/state/overview/effects.ts | 22 +- 6 files changed, 213 insertions(+), 192 deletions(-) diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/grid_by_group/grid_group_item.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/grid_by_group/grid_group_item.tsx index 7346eb85e631a..e5d8502776bb2 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/grid_by_group/grid_group_item.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/grid_by_group/grid_group_item.tsx @@ -123,7 +123,7 @@ export const GroupGridItem = ({ - {groupMonitors.length}{' '} + {groupMonitors.length}  {i18n.translate('xpack.synthetics.groupGridItem.monitorsBadgeLabel', { defaultMessage: 'Monitors', })} diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx index d6bf3d94826a1..73c079d0b7469 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx @@ -6,6 +6,7 @@ */ import { i18n } from '@kbn/i18n'; import React, { useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; import { css } from '@emotion/react'; import { Chart, Settings, Metric, MetricTrendShape } from '@elastic/charts'; import { EuiPanel, EuiIconTip, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; @@ -29,6 +30,8 @@ import { } from '../../../../state/manual_test_runs'; import { MetricItemIcon } from './metric_item_icon'; +const METRIC_ITEM_HEIGHT = 160; + export const getColor = ( theme: ReturnType, isEnabled: boolean, @@ -59,7 +62,6 @@ export const MetricItem = ({ onClick: (params: { id: string; configId: string; location: string; locationId: string }) => void; }) => { const trendData = useSelector(selectOverviewTrends)[monitor.configId + monitor.location.id]; - console.log('trend data for monitor', monitor.name, trendData, monitor.configId); const [isPopoverOpen, setIsPopoverOpen] = useState(false); const isErrorPopoverOpen = useSelector(selectErrorPopoverState); const locationName = useLocationName(monitor); @@ -74,7 +76,10 @@ export const MetricItem = ({ const dispatch = useDispatch(); return ( -
+
- {/* {!trendData &&
{monitor.name}
} */} - {/* {!!trendData && ( */} { @@ -173,7 +176,12 @@ export const MetricItem = ({ ) : ( -
Loading metrics
+
+ +
), valueFormatter: (x: number) => formatDuration(x), color: getColor(theme, monitor.isEnabled, status), @@ -182,7 +190,6 @@ export const MetricItem = ({ ]} />
- {/* )} */}
{ - const { status } = useSelector(selectOverviewStatus); +interface ListItem { + configId: string; + location: { id: string }; +} - const { - data: { monitors }, - flyoutConfig, - loaded, - pageState, - groupBy: { field: groupField }, - } = useSelector(selectOverviewState); - const { perPage } = pageState; - const [page, setPage] = useState(1); - const [maxItem, setMaxItem] = useState(0); +export const OverviewGrid = memo(() => { + const { status } = useSelector(selectOverviewStatus); + const monitorsSortedByStatus: MonitorOverviewItem[] = + useMonitorsSortedByStatus().monitorsSortedByStatus; - const trendData = useSelector(selectOverviewTrends); - // offload the fetching of trend data to a new effect and the list will work better + const { + data: { monitors }, + flyoutConfig, + loaded, + pageState, + groupBy: { field: groupField }, + } = useSelector(selectOverviewState); + const trendData = useSelector(selectOverviewTrends); + const { perPage } = pageState; - const dispatch = useDispatch(); - const intersectionRef = useRef(null); + const [page, setPage] = useState(1); + const [maxItem, setMaxItem] = useState(0); - const setFlyoutConfigCallback = useCallback( - (params: FlyoutParamProps) => dispatch(setFlyoutConfig(params)), - [dispatch] - ); - const hideFlyout = useCallback(() => dispatch(setFlyoutConfig(null)), [dispatch]); - const { lastRefresh } = useSyntheticsRefreshContext(); - const forceRefreshCallback = useCallback( - () => dispatch(quietFetchOverviewAction.get(pageState)), - [dispatch, pageState] - ); + const dispatch = useDispatch(); - useEffect(() => { - if (monitorsSortedByStatus.length && maxItem) { - const batch = []; - const slice = monitorsSortedByStatus.slice(0, (maxItem + 1) * ROW_COUNT); - for (const item of slice) { - if (trendData[item.configId + item.location.id] === undefined) { - batch.push({ configId: item.configId, locationId: item.location.id }); - } - } - if (batch.length) dispatch(trendStatsBatch.get(batch)); - } - }, [dispatch, maxItem, monitorsSortedByStatus, trendData]); + const setFlyoutConfigCallback = useCallback( + (params: FlyoutParamProps) => dispatch(setFlyoutConfig(params)), + [dispatch] + ); + const hideFlyout = useCallback(() => dispatch(setFlyoutConfig(null)), [dispatch]); + const { lastRefresh } = useSyntheticsRefreshContext(); + const forceRefreshCallback = useCallback( + () => dispatch(quietFetchOverviewAction.get(pageState)), + [dispatch, pageState] + ); - const listHeight = Math.min(ITEM_HEIGHT * (monitorsSortedByStatus.length / ROW_COUNT), 800); - const listItems: Array> = useMemo(() => { - const acc = []; - for (let i = 0; i < monitorsSortedByStatus.length; i += ROW_COUNT) { - acc.push(monitorsSortedByStatus.slice(i, i + ROW_COUNT)); + useEffect(() => { + if (monitorsSortedByStatus.length && maxItem) { + const batch = []; + const slice = monitorsSortedByStatus.slice(0, (maxItem + 1) * ROW_COUNT); + for (const item of slice) { + if (trendData[item.configId + item.location.id] === undefined) { + batch.push({ configId: item.configId, locationId: item.location.id }); + } } - return acc; - }, [monitorsSortedByStatus]); + if (batch.length) dispatch(trendStatsBatch.get(batch)); + } + }, [dispatch, maxItem, monitorsSortedByStatus, trendData]); - const listRef: React.LegacyRef> | undefined = React.createRef(); - const infiniteLoaderRef: React.LegacyRef = React.createRef(); - useEffect(() => { - dispatch(refreshOverviewTrends.get()); - }, [dispatch, lastRefresh]); + const listHeight = Math.min( + ITEM_HEIGHT * Math.ceil(monitorsSortedByStatus.length / ROW_COUNT), + MAX_LIST_HEIGHT + ); - // Display no monitors found when down, up, or disabled filter produces no results - if (status && !monitorsSortedByStatus.length && loaded) { - return ; + const listItems: ListItem[][] = useMemo(() => { + const acc = []; + for (let i = 0; i < monitorsSortedByStatus.length; i += ROW_COUNT) { + acc.push(monitorsSortedByStatus.slice(i, i + ROW_COUNT)); } + return acc; + }, [monitorsSortedByStatus]); - return ( - <> - - - - - - setPage(1)} /> - - - - - - -
- {groupField === 'none' ? ( - loaded && monitorsSortedByStatus.length ? ( - - {({ width }: EuiAutoSize) => ( - { - return listItems[idx].every( - (m: any) => !!trendData[m.configId + m.location.id] - ); - }} - itemCount={listItems.length} - loadMoreItems={(_start: number, stop: number) => { - setMaxItem(Math.max(maxItem, stop)); - }} - minimumBatchSize={20} - threshold={12} - > - {({ onItemsRendered }) => ( - - {(props) => { - return ( - - {props.data[props.index].map((item: any, idx: number) => ( - - - - ))} - {props.data[props.index].length % ROW_COUNT !== 0 && - // Adds empty items to fill out row - Array.from({ - length: ROW_COUNT - props.data[props.index].length, - }).map((_, idx) => )} - - ); - }} - - )} - - )} - - ) : ( - - ) + const listRef: React.LegacyRef> | undefined = React.createRef(); + useEffect(() => { + dispatch(refreshOverviewTrends.get()); + }, [dispatch, lastRefresh]); + + // Display no monitors found when down, up, or disabled filter produces no results + if (status && !monitorsSortedByStatus.length && loaded) { + return ; + } + + return ( + <> + + + + + + setPage(1)} /> + + + + + + +
+ {groupField === 'none' ? ( + loaded && monitorsSortedByStatus.length ? ( + + {({ width }: EuiAutoSize) => ( + + listItems[idx].every((m) => !!trendData[m.configId + m.location.id]) + } + itemCount={listItems.length} + loadMoreItems={(_start: number, stop: number) => + setMaxItem(Math.max(maxItem, stop)) + } + minimumBatchSize={MIN_BATCH_SIZE} + threshold={LIST_THRESHOLD} + > + {({ onItemsRendered }) => ( + + {(props: React.PropsWithChildren>) => ( + + {props.data[props.index].map((_, idx) => ( + + + + ))} + {props.data[props.index].length % ROW_COUNT !== 0 && + // Adds empty items to fill out row + Array.from({ + length: ROW_COUNT - props.data[props.index].length, + }).map((_, idx) => )} + + )} + + )} + + )} + ) : ( - - )} - -
-
- -
- {groupField === 'none' && ( + + ) + ) : ( + + )} + +
+ {groupField === 'none' && ( + <> + {monitorsSortedByStatus.length === monitors.length && ( @@ -223,22 +229,22 @@ export const OverviewGrid = memo( )} - )} - {flyoutConfig?.configId && flyoutConfig?.location && ( - - )} - - ); - } -); + + )} + {flyoutConfig?.configId && flyoutConfig?.location && ( + + )} + + ); +}); const SHOWING_ALL_MONITORS_LABEL = i18n.translate( 'xpack.synthetics.overview.grid.showingAllMonitors.label', diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview_page.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview_page.tsx index 1e464444528de..e16633fed2444 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview_page.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview_page.tsx @@ -33,7 +33,6 @@ import { SearchField } from '../common/search_field'; import { NoMonitorsFound } from '../common/no_monitors_found'; import { OverviewErrors } from './overview/overview_errors/overview_errors'; import { AlertingCallout } from '../../common/alerting_callout/alerting_callout'; -import { useMonitorsSortedByStatus } from '../../../hooks/use_monitors_sorted_by_status'; export const OverviewPage: React.FC = () => { useTrackPageview({ app: 'synthetics', path: 'overview' }); @@ -85,8 +84,6 @@ export const OverviewPage: React.FC = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [dispatch, lastRefresh]); - const { monitorsSortedByStatus } = useMonitorsSortedByStatus(); - const hasNoMonitors = !search && !enablementLoading && monitorsLoaded && absoluteTotal === 0; if (hasNoMonitors && !monitorsLoading && isEnabled) { @@ -129,7 +126,7 @@ export const OverviewPage: React.FC = () => { - + ) : ( diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/api.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/api.ts index 3ffc4e30bc6c7..50deb8d7ed1d1 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/api.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/api.ts @@ -57,11 +57,22 @@ export const fetchOverviewStatus = async ( const params = toStatusOverviewQueryArgs(pageState); return apiService.get(SYNTHETICS_API_URLS.OVERVIEW_STATUS, params, OverviewStatusCodec); }; - +interface TrendDatum { + x: number; + y: number; +} +interface OverviewTrend { + configId: string; + locationId: string; + data: TrendDatum[]; + count: number; + min: number; + max: number; + avg: number; + sum: number; + median: number; +} export const fetchOverviewTrendStats = async ( - filters: Array<{ configId: string; locationId: string }> -): Promise => { - if (!filters.length) return []; - const res = apiService.post(SYNTHETICS_API_URLS.OVERVIEW_TRENDS, filters); - return res; -}; + monitors: Array<{ configId: string; locationId: string }> +): Promise> => + monitors.length ? apiService.post(SYNTHETICS_API_URLS.OVERVIEW_TRENDS, monitors) : {}; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.ts index 375a69de8414a..90d0867ea2c82 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.ts @@ -39,8 +39,8 @@ export function* fetchOverviewTrendStats() { for (let i = action.payload.length; i > 0; i -= chunkSize) { const chunk = action.payload.slice(Math.max(i - chunkSize, 0), i); if (chunk.length > 0) { - const res = yield call(trendsApi, chunk); - yield put(trendStatsBatch.success(res)); + const trendStats = yield call(trendsApi, chunk); + yield put(trendStatsBatch.success(trendStats)); } } } catch (e: any) { @@ -57,24 +57,24 @@ export function* refreshOverviewTrendStats() { void, Record > { - const trends: Record = yield select(selectOverviewTrends); - let all = {}; - const keys = Object.keys(trends); + const existingTrends: Record = yield select(selectOverviewTrends); + let acc = {}; + const keys = Object.keys(existingTrends); do { const res = yield call( trendsApi, keys .splice(0, keys.length < 10 ? keys.length : 10) - .filter((key: string) => trends[key] !== null) + .filter((key: string) => existingTrends[key] !== null) .map((key: string) => ({ - configId: trends[key].configId, - locationId: trends[key].locationId, + configId: existingTrends[key].configId, + locationId: existingTrends[key].locationId, })) ); - all = { ...all, ...res }; + acc = { ...acc, ...res }; } while (keys.length); - if (Object.keys(all).length) { - yield put(trendStatsBatch.success(all)); + if (Object.keys(acc).length) { + yield put(trendStatsBatch.success(acc)); } }); } From 00de2f1389af5077731c7d9f340925ffc05212c9 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Thu, 11 Jul 2024 12:33:53 -0400 Subject: [PATCH 15/39] Update unit tests. --- .../overview/overview/metric_item.tsx | 2 +- .../overview/overview/overview_grid.test.tsx | 111 +++++++++++++++--- .../overview/overview/overview_grid.tsx | 6 +- .../apps/synthetics/state/overview/effects.ts | 1 - 4 files changed, 98 insertions(+), 22 deletions(-) diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx index 73c079d0b7469..1459cafc76f7b 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx @@ -77,7 +77,7 @@ export const MetricItem = ({ return (
{ const locationIdToName: Record = { @@ -63,19 +65,88 @@ describe('Overview Grid', () => { return hits; }; + const useSelectorMockImplementation = (selector: any) => { + const monitors = getMockData(); + if (selector.name === 'selectOverviewState') { + const overviewState: MonitorOverviewState = { + flyoutConfig: null, + data: { + monitors, + total: monitors.length, + allMonitorIds: monitors.map((monitor) => monitor.configId), + }, + pageState: { + perPage: 20, + sortOrder: 'desc', + sortField: 'status', + }, + loading: false, + loaded: true, + error: null, + groupBy: { + field: 'none', + order: 'asc', + }, + trendStats: {}, + }; + return overviewState; + } else if (selector.name === 'selectOverviewStatus') { + return { + status: { + allConfigs: monitors.reduce((acc: Record, cur) => { + acc[`${cur.configId}`] = { + configId: cur.configId, + monitorQueryId: cur.id, + location: locationIdToName[cur.location.id], + status: 'down', + }; + return acc; + }, {}), + allIds: monitors.map((monitor) => monitor.configId), + allMonitorsCount: monitors.length, + disabledCount: 0, + disabledMonitorQueryIds: [], + disabledMonitorsCount: 0, + down: 0, + downConfigs: {}, + enabledMonitorQueryIds: monitors.map((monitor) => monitor.configId), + pending: 0, + pendingConfigs: {}, + projectMonitorsCount: 0, + up: monitors.length, + upConfigs: monitors.map((monitor) => monitor.configId), + }, + }; + } + const overviewState: Record< + string, + null | { data: any[]; median: number; avg: number; min: number; max: number } + > = {}; + monitors.forEach((monitor) => { + const key = `${monitor.configId}${monitor.location.id}`; + if (!overviewState[key]) { + overviewState[key] = { + data: getMockChart(), + avg: 30000, + min: 0, + max: 50000, + median: 15000, + }; + } + }); + return overviewState; + }; + const perPage = 20; it('renders correctly', async () => { - jest.spyOn(hooks, 'useLast50DurationChart').mockReturnValue({ - data: getMockChart(), - avgDuration: 30000, - minDuration: 0, - maxDuration: 50000, - medianDuration: 15000, - loading: false, + jest.spyOn(useMonitorsSortedByStatus, 'useMonitorsSortedByStatus').mockReturnValue({ + monitorsSortedByStatus: getMockData(), + downMonitors: {}, }); + jest.spyOn(reactRedux, 'useSelector').mockImplementation(useSelectorMockImplementation); - const { getByText, getAllByTestId, queryByText } = render(, { + const { getByText, getByTestId } = render(, { state: { overview: { pageState: { @@ -125,20 +196,22 @@ describe('Overview Grid', () => { expect(getByText('Showing')).toBeInTheDocument(); expect(getByText('40')).toBeInTheDocument(); expect(getByText('Monitors')).toBeInTheDocument(); - expect(queryByText('Showing all monitors')).not.toBeInTheDocument(); - expect(getAllByTestId('syntheticsOverviewGridItem').length).toEqual(perPage); + getMockData() + // since uses react-window, it only renders the visible items, + // thus, monitors that would be significantly below the fold will not render without scrolling + .slice(0, 27) + .forEach((monitor) => { + expect(getByTestId(`${monitor.name}-${monitor.location.id}-metric-item`)); + }); }); }); it('displays showing all monitors label when reaching the end of the list', async () => { - jest.spyOn(hooks, 'useLast50DurationChart').mockReturnValue({ - data: getMockChart(), - avgDuration: 30000, - minDuration: 0, - maxDuration: 50000, - medianDuration: 15000, - loading: false, + jest.spyOn(useMonitorsSortedByStatus, 'useMonitorsSortedByStatus').mockReturnValue({ + monitorsSortedByStatus: getMockData(), + downMonitors: {}, }); + jest.spyOn(reactRedux, 'useSelector').mockImplementation(useSelectorMockImplementation); const { getByText } = render(, { state: { diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx index 04ed0ccfbb5b3..f93da542df155 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx @@ -169,7 +169,11 @@ export const OverviewGrid = memo(() => { ref={listRef} > {(props: React.PropsWithChildren>) => ( - + {props.data[props.index].map((_, idx) => ( Date: Thu, 11 Jul 2024 12:48:23 -0400 Subject: [PATCH 16/39] Fix types. --- .../synthetics/utils/testing/__mocks__/synthetics_store.mock.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_store.mock.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_store.mock.ts index abfc08919b33a..897be8c4ad970 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_store.mock.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_store.mock.ts @@ -100,6 +100,7 @@ export const mockState: SyntheticsAppState = { sortOrder: 'asc', sortField: 'name.keyword', }, + trendStats: {}, data: { total: 0, allMonitorIds: [], From 0bb07c930f84a1f8cf0c1aa7cc90351e79c0e2f5 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Thu, 11 Jul 2024 13:09:59 -0400 Subject: [PATCH 17/39] Reduce reliance on `any`. --- .../overview/overview/metric_item.tsx | 2 +- .../overview/overview/overview_grid.tsx | 2 +- .../apps/synthetics/state/overview/actions.ts | 7 +++--- .../apps/synthetics/state/overview/api.ts | 20 +++------------- .../apps/synthetics/state/overview/effects.ts | 16 ++++++------- .../apps/synthetics/state/overview/models.ts | 24 +++++++++++++++---- 6 files changed, 36 insertions(+), 35 deletions(-) diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx index 1459cafc76f7b..52ad1cd3e8656 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx @@ -183,7 +183,7 @@ export const MetricItem = ({ />
), - valueFormatter: (x: number) => formatDuration(x), + valueFormatter: (d: number) => formatDuration(d), color: getColor(theme, monitor.isEnabled, status), }, ], diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx index f93da542df155..0c941a5c86892 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx @@ -108,7 +108,7 @@ export const OverviewGrid = memo(() => { return acc; }, [monitorsSortedByStatus]); - const listRef: React.LegacyRef> | undefined = React.createRef(); + const listRef: React.LegacyRef> | undefined = React.createRef(); useEffect(() => { dispatch(refreshOverviewTrends.get()); }, [dispatch, lastRefresh]); diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/actions.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/actions.ts index f5ef6e295da4f..381a073d5a431 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/actions.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/actions.ts @@ -7,10 +7,11 @@ import { createAction } from '@reduxjs/toolkit'; import { createAsyncAction } from '../utils/actions'; -import { +import type { MonitorOverviewFlyoutConfig, MonitorOverviewPageState, MonitorOverviewState, + TrendTable, } from './models'; import { MonitorOverviewResult } from '../../../../../common/runtime_types'; @@ -34,12 +35,12 @@ export const quietFetchOverviewAction = createAsyncAction< MonitorOverviewResult >('quietFetchOverviewAction'); -export const refreshOverviewTrends = createAsyncAction, any>( +export const refreshOverviewTrends = createAsyncAction( 'refreshOverviewTrendStats' ); export const trendStatsBatch = createAsyncAction< Array<{ configId: string; locationId: string }>, - Record, + TrendTable, any >('batchTrendStats'); diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/api.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/api.ts index 50deb8d7ed1d1..4adec7dc34cd4 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/api.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/api.ts @@ -14,7 +14,7 @@ import { OverviewStatusCodec, } from '../../../../../common/runtime_types'; import { apiService } from '../../../../utils/api_service'; -import { MonitorOverviewPageState } from './models'; +import type { MonitorOverviewPageState, TrendTable } from './models'; function toMonitorOverviewQueryArgs( pageState: MonitorOverviewPageState @@ -57,22 +57,8 @@ export const fetchOverviewStatus = async ( const params = toStatusOverviewQueryArgs(pageState); return apiService.get(SYNTHETICS_API_URLS.OVERVIEW_STATUS, params, OverviewStatusCodec); }; -interface TrendDatum { - x: number; - y: number; -} -interface OverviewTrend { - configId: string; - locationId: string; - data: TrendDatum[]; - count: number; - min: number; - max: number; - avg: number; - sum: number; - median: number; -} + export const fetchOverviewTrendStats = async ( monitors: Array<{ configId: string; locationId: string }> -): Promise> => +): Promise => monitors.length ? apiService.post(SYNTHETICS_API_URLS.OVERVIEW_TRENDS, monitors) : {}; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.ts index 6fc989bb67c18..ef67dbc4d905a 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.ts @@ -15,6 +15,7 @@ import { trendStatsBatch, } from './actions'; import { fetchMonitorOverview, fetchOverviewTrendStats as trendsApi } from './api'; +import type { TrendTable } from './models'; export function* fetchMonitorOverviewEffect() { yield debounce( @@ -33,7 +34,7 @@ export function* fetchOverviewTrendStats() { trendStatsBatch.get, function* ( action: ReturnType - ): Generator> { + ): Generator { try { const chunkSize = 40; for (let i = action.payload.length; i > 0; i -= chunkSize) { @@ -51,12 +52,8 @@ export function* fetchOverviewTrendStats() { } export function* refreshOverviewTrendStats() { - yield takeLeading(refreshOverviewTrends.get, function* (): Generator< - unknown, - void, - Record - > { - const existingTrends: Record = yield select(selectOverviewTrends); + yield takeLeading(refreshOverviewTrends.get, function* (): Generator { + const existingTrends: TrendTable = yield select(selectOverviewTrends); let acc = {}; const keys = Object.keys(existingTrends); do { @@ -66,8 +63,9 @@ export function* refreshOverviewTrendStats() { .splice(0, keys.length < 10 ? keys.length : 10) .filter((key: string) => existingTrends[key] !== null) .map((key: string) => ({ - configId: existingTrends[key].configId, - locationId: existingTrends[key].locationId, + // assertion in filter above ensures this is not null + configId: existingTrends[key]!.configId, + locationId: existingTrends[key]!.locationId, })) ); acc = { ...acc, ...res }; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/models.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/models.ts index d64ced353a137..7e761d80e4d73 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/models.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/models.ts @@ -32,13 +32,29 @@ export interface MonitorOverviewState { isErrorPopoverOpen?: string | null; error: IHttpSerializedFetchError | null; groupBy: GroupByState; - trendStats: Record< - string, - null | { data: any[]; median: number; avg: number; min: number; max: number } - >; + trendStats: TrendTable; } export interface GroupByState { field: ConfigKey.TAGS | ConfigKey.PROJECT_ID | ConfigKey.MONITOR_TYPE | 'locationId' | 'none'; order: 'asc' | 'desc'; } + +export interface TrendDatum { + x: number; + y: number; +} + +export interface OverviewTrend { + configId: string; + locationId: string; + data: TrendDatum[]; + count: number; + min: number; + max: number; + avg: number; + sum: number; + median: number; +} + +export type TrendTable = Record; From e9e06124f3287637f69a13da3604eb896aad972a Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 11 Jul 2024 17:29:02 +0000 Subject: [PATCH 18/39] [CI] Auto-commit changed files from 'node scripts/lint_ts_projects --fix' --- x-pack/plugins/observability_solution/synthetics/tsconfig.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/observability_solution/synthetics/tsconfig.json b/x-pack/plugins/observability_solution/synthetics/tsconfig.json index 95ed03ba36a11..492737d099fed 100644 --- a/x-pack/plugins/observability_solution/synthetics/tsconfig.json +++ b/x-pack/plugins/observability_solution/synthetics/tsconfig.json @@ -90,7 +90,8 @@ "@kbn/react-kibana-mount", "@kbn/react-kibana-context-render", "@kbn/react-kibana-context-theme", - "@kbn/search-types" + "@kbn/search-types", + "@kbn/apm-utils" ], "exclude": ["target/**/*"] } From 32905dcf01231fb50a73d63ea889d457709276ab Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Mon, 26 Aug 2024 17:06:34 -0400 Subject: [PATCH 19/39] Fix types. --- .../monitors_page/overview/overview/metric_item.tsx | 9 --------- .../monitors_page/overview/overview/overview_grid.tsx | 1 - .../utils/testing/__mocks__/synthetics_store.mock.ts | 2 ++ 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx index 9fa4f5305f5c2..0cc655f6337ff 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx @@ -56,20 +56,11 @@ export const getColor = ( export const MetricItem = ({ monitor, - stats, - data, onClick, style, }: { monitor: MonitorOverviewItem; style?: React.CSSProperties; - data: Array<{ x: number; y: number }>; - stats: { - medianDuration: number; - avgDuration: number; - minDuration: number; - maxDuration: number; - }; onClick: (params: { id: string; configId: string; location: string; locationId: string }) => void; }) => { const trendData = useSelector(selectOverviewTrends)[monitor.configId + monitor.location.id]; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx index 78489c2e7b7bf..72f561510748d 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx @@ -63,7 +63,6 @@ export const OverviewGrid = memo(() => { data: { monitors }, flyoutConfig, loaded, - loading, pageState, groupBy: { field: groupField }, } = useSelector(selectOverviewState); diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_store.mock.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_store.mock.ts index 897be8c4ad970..a3f9e5fbae3ba 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_store.mock.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_store.mock.ts @@ -100,6 +100,8 @@ export const mockState: SyntheticsAppState = { sortOrder: 'asc', sortField: 'name.keyword', }, + trendsLoading: false, + trendsPendingStack: [], trendStats: {}, data: { total: 0, From 865a3d84a29309f5a68082bb1a5237a3d56daffd Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Wed, 28 Aug 2024 13:05:12 -0400 Subject: [PATCH 20/39] Simplify. --- package.json | 1 + .../apps/synthetics/state/overview/actions.ts | 12 +- .../apps/synthetics/state/overview/api.ts | 6 +- .../synthetics/state/overview/effects.test.ts | 187 ++++++++++++++++++ .../apps/synthetics/state/overview/effects.ts | 109 ++++------ .../apps/synthetics/state/overview/index.ts | 12 -- .../apps/synthetics/state/overview/models.ts | 7 +- .../synthetics/state/overview/selectors.ts | 8 - .../apps/synthetics/state/root_effect.ts | 2 - .../__mocks__/synthetics_store.mock.ts | 2 - .../routes/overview_trends/overview_trends.ts | 3 +- yarn.lock | 5 + 12 files changed, 243 insertions(+), 111 deletions(-) create mode 100644 x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.test.ts diff --git a/package.json b/package.json index caee7fba882a0..05e2b1b2947da 100644 --- a/package.json +++ b/package.json @@ -1202,6 +1202,7 @@ "redux-actions": "^2.6.5", "redux-devtools-extension": "^2.13.8", "redux-saga": "^1.1.3", + "redux-saga-testing": "^2.0.2", "redux-thunk": "^2.4.2", "redux-thunks": "^1.0.0", "reflect-metadata": "^0.2.1", diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/actions.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/actions.ts index 380ca0e79ed0e..ed62690f79608 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/actions.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/actions.ts @@ -11,6 +11,7 @@ import type { MonitorOverviewFlyoutConfig, MonitorOverviewPageState, MonitorOverviewState, + TrendKey, TrendTable, } from './models'; import { MonitorOverviewResult } from '../../../../../common/runtime_types'; @@ -39,13 +40,4 @@ export const refreshOverviewTrends = createAsyncAction( 'refreshOverviewTrendStats' ); -export const trendStatsBatch = createAsyncAction< - Array<{ configId: string; locationId: string }>, - TrendTable, - any ->('batchTrendStats'); - -export const trendStatsInFlight = createAction('trendStatsInFlight'); - -export const stackTrendStats = - createAction>('enqueueTrendStats'); +export const trendStatsBatch = createAsyncAction('batchTrendStats'); diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/api.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/api.ts index e7d743d3c7b36..275f0a22e0471 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/api.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/api.ts @@ -14,7 +14,7 @@ import { OverviewStatusCodec, } from '../../../../../common/runtime_types'; import { apiService } from '../../../../utils/api_service'; -import type { MonitorOverviewPageState, TrendTable } from './models'; +import type { MonitorOverviewPageState, TrendKey, TrendTable } from './models'; function toMonitorOverviewQueryArgs( pageState: MonitorOverviewPageState @@ -59,7 +59,5 @@ export const fetchOverviewStatus = async ( return apiService.get(SYNTHETICS_API_URLS.OVERVIEW_STATUS, params, OverviewStatusCodec); }; -export const fetchOverviewTrendStats = async ( - monitors: Array<{ configId: string; locationId: string }> -): Promise => +export const fetchOverviewTrendStats = async (monitors: TrendKey[]): Promise => monitors.length ? apiService.post(SYNTHETICS_API_URLS.OVERVIEW_TRENDS, monitors) : {}; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.test.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.test.ts new file mode 100644 index 0000000000000..8b063a81f04d0 --- /dev/null +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.test.ts @@ -0,0 +1,187 @@ +/* + * 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 sagaHelper from 'redux-saga-testing'; +import { call, put, select } from 'redux-saga/effects'; +import { TRENDS_CHUNK_SIZE, fetchTrendEffect, refreshTrends } from './effects'; +import { trendStatsBatch } from './actions'; +import { fetchOverviewTrendStats as trendsApi } from './api'; +import { TrendKey, TrendTable, selectOverviewTrends } from '.'; + +const generateTrendRequests = () => { + const ar: TrendKey[] = []; + for (let i = 0; i < 30; i++) ar.push({ configId: `configId${i}`, locationId: 'location' }); + return ar; +}; + +const responseReducer = ( + acc: Record, + curr: { configId: string; locationId: string } +) => ({ ...acc, [curr.configId + curr.locationId]: null }); + +describe('overview effects', () => { + describe('fetchTrendEffect', () => { + const trendRequests = generateTrendRequests(); + const firstChunk = trendRequests.slice( + trendRequests.length - TRENDS_CHUNK_SIZE, + trendRequests.length + ); + const secondChunk = trendRequests.slice(0, 10); + const firstChunkResponse = firstChunk.reduce(responseReducer, {}); + const secondChunkResponse = secondChunk.reduce(responseReducer, {}); + + const it = sagaHelper( + fetchTrendEffect(trendStatsBatch.get(trendRequests)) as IterableIterator + ); + + it('calls the `trendsApi` with the first chunk of trend requests', (callResult) => { + expect(callResult).toEqual(call(trendsApi, firstChunk)); + return firstChunkResponse; + }); + + it('sends trends stats success action', (putResult) => { + expect(putResult).toEqual(put(trendStatsBatch.success(firstChunkResponse))); + }); + + it('calls the api for the second chunk', (callResult) => { + expect(callResult).toEqual(call(trendsApi, secondChunk)); + return secondChunkResponse; + }); + + it('sends trends stats success action', (putResult) => { + expect(putResult).toEqual(put(trendStatsBatch.success(secondChunkResponse))); + }); + + it('terminates', (result) => { + expect(result).toBeUndefined(); + }); + }); + describe('refreshTrends with no data', () => { + const it = sagaHelper(refreshTrends() as IterableIterator); + + it('selects the trends in the table', (selectResult) => { + expect(selectResult).toEqual(select(selectOverviewTrends)); + return { monitor1: null, monitor2: null, monitor3: null }; + }); + + it('skips the API if the data is null', (result) => { + expect(result).toBeUndefined(); + }); + }); + + describe('refreshTrends with data', () => { + const it = sagaHelper(refreshTrends() as IterableIterator); + const table: TrendTable = { + monitor1: { + configId: 'monitor1', + locationId: 'location', + data: [{ x: 0, y: 1 }], + count: 1, + median: 1, + min: 0, + max: 0, + avg: 0, + sum: 0, + }, + monitor2: null, + monitor3: { + configId: 'monitor3', + locationId: 'location', + data: [{ x: 0, y: 1 }], + count: 1, + median: 1, + min: 0, + max: 0, + avg: 0, + sum: 0, + }, + }; + + it('selects the trends in the table', (selectResult) => { + expect(selectResult).toEqual(select(selectOverviewTrends)); + + return table; + }); + + it('calls the api for the first chunk', (callResult) => { + expect(callResult).toEqual( + call(trendsApi, [ + { configId: 'monitor1', locationId: 'location' }, + { configId: 'monitor3', locationId: 'location' }, + ]) + ); + + return { + monitor1: { + configId: 'monitor1', + locationId: 'location', + data: [ + { x: 0, y: 1 }, + { x: 1, y: 2 }, + ], + count: 2, + median: 2, + min: 1, + max: 1, + avg: 1, + sum: 1, + }, + monitor2: { + configId: 'monitor2', + locationId: 'location', + data: [ + { x: 0, y: 1 }, + { x: 1, y: 2 }, + ], + count: 2, + median: 2, + min: 1, + max: 1, + avg: 1, + sum: 1, + }, + }; + }); + + it('sends trends stats success action', (putResult) => { + expect(putResult).toEqual( + put( + trendStatsBatch.success({ + monitor1: { + configId: 'monitor1', + locationId: 'location', + data: [ + { x: 0, y: 1 }, + { x: 1, y: 2 }, + ], + count: 2, + median: 2, + min: 1, + max: 1, + avg: 1, + sum: 1, + }, + monitor2: { + configId: 'monitor2', + locationId: 'location', + data: [ + { x: 0, y: 1 }, + { x: 1, y: 2 }, + ], + count: 2, + median: 2, + min: 1, + max: 1, + avg: 1, + sum: 1, + }, + }) + ) + ); + }); + }); +}); diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.ts index 6706f877b2eca..048b5fdecc465 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.ts @@ -6,15 +6,13 @@ */ import { debounce, call, takeLeading, takeEvery, put, select } from 'redux-saga/effects'; -import { selectOverviewTrends, selectTrendState } from './selectors'; +import { selectOverviewTrends } from './selectors'; import { fetchEffectFactory } from '../utils/fetch_effect'; import { fetchMonitorOverviewAction, quietFetchOverviewAction, refreshOverviewTrends, - stackTrendStats, trendStatsBatch, - trendStatsInFlight, } from './actions'; import { fetchMonitorOverview, fetchOverviewTrendStats as trendsApi } from './api'; import type { TrendTable } from './models'; @@ -31,77 +29,48 @@ export function* fetchMonitorOverviewEffect() { ); } -const CHUNK_SIZE = 40; -export function* fetchOverviewTrendStats() { - yield takeEvery( - trendStatsBatch.get, - function* ( - action: ReturnType - ): Generator { - try { - // track the loading state in the reducer - // if in flight, push this to a FILO queue which we update on success/fail of the fetch effect here. This will need a new action. Then, grab the newest chunk from the queue and fetch it. - const { trendsLoading } = yield select(selectTrendState); - console.log('trends loading value', trendsLoading); - for (let i = action.payload.length; i > 0; i -= CHUNK_SIZE) { - const chunk = action.payload.slice(Math.max(i - CHUNK_SIZE, 0), i); - if (!trendsLoading && chunk.length > 0) { - yield put(trendStatsInFlight(true)); - const trendStats = yield call(trendsApi, chunk); - yield put(trendStatsBatch.success(trendStats)); - } else if (chunk.length > 0) { - console.log('data loading, pushing to stack'); - // push to queue - yield put(stackTrendStats(chunk)); - } - } - } catch (e: any) { - yield put(trendStatsBatch.fail(e)); +export const TRENDS_CHUNK_SIZE = 20; + +export function* fetchTrendEffect( + action: ReturnType +): Generator { + try { + // batch requests LIFO as the user scrolls + for (let i = action.payload.length; i > 0; i -= TRENDS_CHUNK_SIZE) { + const chunk = action.payload.slice(Math.max(i - TRENDS_CHUNK_SIZE, 0), i); + if (chunk.length > 0) { + const trendStats = yield call(trendsApi, chunk); + yield put(trendStatsBatch.success(trendStats)); } } - ); + } catch (e: any) { + yield put(trendStatsBatch.fail(e)); + } } - -export function* popPendingTrendRequests() { - yield takeEvery([trendStatsBatch.success, trendStatsBatch.fail, stackTrendStats], function* () { - const { trendsPendingStack } = yield select(selectTrendState); - if (trendsPendingStack.length) { - console.log('trends stack', trendsPendingStack); - - // using splice in this way removes the last CHUNK_SIZE elements from the array - const batch = trendsPendingStack.splice(-CHUNK_SIZE, CHUNK_SIZE); - - console.log('trends pending stack', trendsPendingStack); - console.log('batch', batch); - - yield put(stackTrendStats(trendsPendingStack)); - yield put(trendStatsBatch.get(batch)); - } - }); +export function* fetchOverviewTrendStats() { + yield takeEvery(trendStatsBatch.get, fetchTrendEffect); } - -export function* refreshOverviewTrendStats() { - yield takeLeading(refreshOverviewTrends.get, function* (): Generator { - const existingTrends: TrendTable = yield select(selectOverviewTrends); - let acc = {}; - const keys = Object.keys(existingTrends); - do { - console.log('doing a refresh', new Date()); - const res = yield call( - trendsApi, - keys - .splice(0, keys.length < 10 ? keys.length : 10) - .filter((key: string) => existingTrends[key] !== null) - .map((key: string) => ({ - configId: existingTrends[key]!.configId, - locationId: existingTrends[key]!.locationId, - })) - ); - console.log('received data', new Date()); +export function* refreshTrends(): Generator { + const existingTrends: TrendTable = yield select(selectOverviewTrends); + let acc = {}; + const keys = Object.keys(existingTrends); + do { + const chunk = keys + .splice(0, keys.length < 10 ? keys.length : 10) + .filter((key: string) => existingTrends[key] !== null) + .map((key: string) => ({ + configId: existingTrends[key]!.configId, + locationId: existingTrends[key]!.locationId, + })); + if (chunk.length) { + const res = yield call(trendsApi, chunk); acc = { ...acc, ...res }; - } while (keys.length); - if (Object.keys(acc).length) { - yield put(trendStatsBatch.success(acc)); } - }); + } while (keys.length); + if (Object.keys(acc).length) { + yield put(trendStatsBatch.success(acc)); + } +} +export function* refreshOverviewTrendStats() { + yield takeLeading(refreshOverviewTrends.get, refreshTrends); } diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/index.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/index.ts index 3959f8cc0b8c6..74ad8bb7a2a57 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/index.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/index.ts @@ -15,7 +15,6 @@ import { setFlyoutConfig, setOverviewGroupByAction, setOverviewPageStateAction, - stackTrendStats, toggleErrorPopoverOpen, trendStatsBatch, } from './actions'; @@ -34,8 +33,6 @@ const initialState: MonitorOverviewState = { sortField: 'status', }, trendStats: {}, - trendsPendingStack: [], - trendsLoading: false, groupBy: { field: 'none', order: 'asc' }, flyoutConfig: null, loading: false, @@ -106,19 +103,10 @@ export const monitorOverviewReducer = createReducer(initialState, (builder) => { } } }) - .addCase(trendStatsBatch.fail, (state, action) => { - console.error('received error', action.payload); - state.trendsLoading = false; - }) .addCase(trendStatsBatch.success, (state, action) => { - console.log('received trend data', trendStatsBatch.success); - state.trendsLoading = false; for (const key of Object.keys(action.payload)) { state.trendStats[key] = action.payload[key]; } - }) - .addCase(stackTrendStats, (state, action) => { - state.trendsPendingStack.push(...action.payload); }); }); diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/models.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/models.ts index 534a1534da088..f848edf883988 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/models.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/models.ts @@ -33,8 +33,6 @@ export interface MonitorOverviewState { error: IHttpSerializedFetchError | null; groupBy: GroupByState; trendStats: TrendTable; - trendsLoading: boolean; - trendsPendingStack: Array<{ configId: string; locationId: string }>; } export interface GroupByState { @@ -42,6 +40,11 @@ export interface GroupByState { order: 'asc' | 'desc'; } +export interface TrendKey { + configId: string; + locationId: string; +} + export interface TrendDatum { x: number; y: number; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/selectors.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/selectors.ts index 7412246916bc3..98286a3da118f 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/selectors.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/selectors.ts @@ -16,14 +16,6 @@ export const selectErrorPopoverState = createSelector( selectOverviewState, (state) => state.isErrorPopoverOpen ); -export const selectTrendState = createSelector( - selectOverviewState, - ({ trendsLoading, trendStats, trendsPendingStack }) => ({ - trendStats, - trendsLoading, - trendsPendingStack, - }) -); export const selectOverviewTrends = createSelector( selectOverviewState, ({ trendStats }) => trendStats diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/root_effect.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/root_effect.ts index a5fa56851a077..424c6fa70eed6 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/root_effect.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/root_effect.ts @@ -37,7 +37,6 @@ import { fetchMonitorOverviewEffect, fetchOverviewTrendStats, refreshOverviewTrendStats, - popPendingTrendRequests, } from './overview'; import { fetchServiceLocationsEffect } from './service_locations'; import { browserJourneyEffects, fetchJourneyStepsEffect } from './browser_journey'; @@ -78,6 +77,5 @@ export const rootEffect = function* root(): Generator { fork(enableDefaultAlertingSilentlyEffect), fork(fetchOverviewTrendStats), fork(refreshOverviewTrendStats), - fork(popPendingTrendRequests), ]); }; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_store.mock.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_store.mock.ts index a3f9e5fbae3ba..897be8c4ad970 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_store.mock.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_store.mock.ts @@ -100,8 +100,6 @@ export const mockState: SyntheticsAppState = { sortOrder: 'asc', sortField: 'name.keyword', }, - trendsLoading: false, - trendsPendingStack: [], trendStats: {}, data: { total: 0, diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/overview_trends.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/overview_trends.ts index 5252af2434ab9..3fe8f1854b720 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/overview_trends.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/overview_trends.ts @@ -10,6 +10,7 @@ import { schema } from '@kbn/config-schema'; import { SYNTHETICS_API_URLS } from '../../../common/constants'; import { SyntheticsRestApiRouteFactory } from '../types'; import { fetchTrends } from './fetch_trends'; +import { TrendKey } from '../../../public/apps/synthetics/state'; export const createOverviewTrendsRoute: SyntheticsRestApiRouteFactory = () => ({ method: 'POST', @@ -20,7 +21,7 @@ export const createOverviewTrendsRoute: SyntheticsRestApiRouteFactory = () => ({ handler: async (routeContext): Promise => { return withSpan('fetch trends', async () => { const esClient = routeContext.syntheticsEsClient; - const body = routeContext.request.body as Array<{ configId: string; locationId: string }>; + const body = routeContext.request.body as TrendKey[]; const configs = body.reduce((acc: Record, { configId, locationId }) => { if (!acc[configId]) { acc[configId] = [locationId]; diff --git a/yarn.lock b/yarn.lock index f8f4215d029af..06b520a3b3182 100644 --- a/yarn.lock +++ b/yarn.lock @@ -27115,6 +27115,11 @@ redux-devtools-extension@^2.13.8: resolved "https://registry.yarnpkg.com/redux-devtools-extension/-/redux-devtools-extension-2.13.8.tgz#37b982688626e5e4993ff87220c9bbb7cd2d96e1" integrity sha512-8qlpooP2QqPtZHQZRhx3x3OP5skEV1py/zUdMY28WNAocbafxdG2tRD1MWE7sp8obGMNYuLWanhhQ7EQvT1FBg== +redux-saga-testing@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/redux-saga-testing/-/redux-saga-testing-2.0.2.tgz#a542b771a6b6584397198f35d47d07bae8dbfc9c" + integrity sha512-8IVPTaEw0Typ9TGCAsktFrrU+I5ACbmwPmzW0DQjUgZvja0k1jP2ILnUn+fZDF1gT8c0L/Ubj//bsej5X3OkaQ== + redux-saga@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/redux-saga/-/redux-saga-1.1.3.tgz#9f3e6aebd3c994bbc0f6901a625f9a42b51d1112" From 3f36547067194c5cc97f433bc84745abed3aa0dc Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Wed, 28 Aug 2024 13:52:12 -0400 Subject: [PATCH 21/39] Clean up. --- .../monitors_page/overview/overview/overview_grid.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx index 72f561510748d..3fdcf425b22a4 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx @@ -24,6 +24,7 @@ import { useOverviewStatus } from '../../hooks/use_overview_status'; import { GridItemsByGroup } from './grid_by_group/grid_items_by_group'; import { GroupFields } from './grid_by_group/group_fields'; import { + TrendKey, fetchMonitorOverviewAction, quietFetchOverviewAction, refreshOverviewTrends, @@ -92,9 +93,9 @@ export const OverviewGrid = memo(() => { useEffect(() => { if (monitorsSortedByStatus.length && maxItem) { - const batch = []; - const slice = monitorsSortedByStatus.slice(0, (maxItem + 1) * ROW_COUNT); - for (const item of slice) { + const batch: TrendKey[] = []; + const chunk = monitorsSortedByStatus.slice(0, (maxItem + 1) * ROW_COUNT); + for (const item of chunk) { if (trendData[item.configId + item.location.id] === undefined) { batch.push({ configId: item.configId, locationId: item.location.id }); } @@ -109,7 +110,7 @@ export const OverviewGrid = memo(() => { ); const listItems: ListItem[][] = useMemo(() => { - const acc = []; + const acc: ListItem[][] = []; for (let i = 0; i < monitorsSortedByStatus.length; i += ROW_COUNT) { acc.push(monitorsSortedByStatus.slice(i, i + ROW_COUNT)); } From ffbca471711e3a4206ede0202898ab8aaf522ba6 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Wed, 28 Aug 2024 13:52:36 -0400 Subject: [PATCH 22/39] Add unit tests to cover batching cycle. --- .../synthetics/state/overview/effects.test.ts | 183 +++++++++++------- 1 file changed, 114 insertions(+), 69 deletions(-) diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.test.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.test.ts index 8b063a81f04d0..b9765505c2976 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.test.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.test.ts @@ -10,7 +10,7 @@ import { call, put, select } from 'redux-saga/effects'; import { TRENDS_CHUNK_SIZE, fetchTrendEffect, refreshTrends } from './effects'; import { trendStatsBatch } from './actions'; import { fetchOverviewTrendStats as trendsApi } from './api'; -import { TrendKey, TrendTable, selectOverviewTrends } from '.'; +import { OverviewTrend, TrendKey, TrendTable, selectOverviewTrends } from '.'; const generateTrendRequests = () => { const ar: TrendKey[] = []; @@ -18,10 +18,10 @@ const generateTrendRequests = () => { return ar; }; -const responseReducer = ( - acc: Record, - curr: { configId: string; locationId: string } -) => ({ ...acc, [curr.configId + curr.locationId]: null }); +const responseReducer = (acc: Record, curr: TrendKey) => ({ + ...acc, + [curr.configId + curr.locationId]: null, +}); describe('overview effects', () => { describe('fetchTrendEffect', () => { @@ -60,6 +60,7 @@ describe('overview effects', () => { expect(result).toBeUndefined(); }); }); + describe('refreshTrends with no data', () => { const it = sagaHelper(refreshTrends() as IterableIterator); @@ -101,6 +102,37 @@ describe('overview effects', () => { }, }; + const apiResponse: TrendTable = { + monitor1: { + configId: 'monitor1', + locationId: 'location', + data: [ + { x: 0, y: 1 }, + { x: 1, y: 2 }, + ], + count: 2, + median: 2, + min: 1, + max: 1, + avg: 1, + sum: 1, + }, + monitor2: { + configId: 'monitor2', + locationId: 'location', + data: [ + { x: 0, y: 1 }, + { x: 1, y: 2 }, + ], + count: 2, + median: 2, + min: 1, + max: 1, + avg: 1, + sum: 1, + }, + }; + it('selects the trends in the table', (selectResult) => { expect(selectResult).toEqual(select(selectOverviewTrends)); @@ -115,73 +147,86 @@ describe('overview effects', () => { ]) ); - return { - monitor1: { - configId: 'monitor1', - locationId: 'location', - data: [ - { x: 0, y: 1 }, - { x: 1, y: 2 }, - ], - count: 2, - median: 2, - min: 1, - max: 1, - avg: 1, - sum: 1, - }, - monitor2: { - configId: 'monitor2', - locationId: 'location', - data: [ - { x: 0, y: 1 }, - { x: 1, y: 2 }, - ], - count: 2, - median: 2, - min: 1, - max: 1, - avg: 1, - sum: 1, - }, - }; + return apiResponse; }); it('sends trends stats success action', (putResult) => { - expect(putResult).toEqual( - put( - trendStatsBatch.success({ - monitor1: { - configId: 'monitor1', - locationId: 'location', - data: [ - { x: 0, y: 1 }, - { x: 1, y: 2 }, - ], - count: 2, - median: 2, - min: 1, - max: 1, - avg: 1, - sum: 1, - }, - monitor2: { - configId: 'monitor2', - locationId: 'location', - data: [ - { x: 0, y: 1 }, - { x: 1, y: 2 }, - ], - count: 2, - median: 2, - min: 1, - max: 1, - avg: 1, - sum: 1, - }, - }) - ) - ); + expect(putResult).toEqual(put(trendStatsBatch.success(apiResponse))); + }); + }); + + describe('refreshTrends with multiple pages', () => { + const it = sagaHelper(refreshTrends() as IterableIterator); + function generateTable() { + const table: TrendTable = {}; + for (let i = 0; i < 30; i++) { + table[`monitor${i}location`] = { + configId: `monitor${i}`, + locationId: 'location', + data: [{ x: 0, y: 1 }], + count: 1, + median: 1, + min: 0, + max: 0, + avg: 0, + sum: 0, + }; + } + return table; + } + + const testTable = generateTable(); + + const computedTrendsReducer = (acc: Record, curr: TrendKey) => ({ + ...acc, + [curr.configId + curr.locationId]: testTable[curr.configId + curr.locationId] ?? null, + }); + + const getComputedApiCall = (start: number, end: number) => { + return Object.keys(testTable) + .slice(start, end) + .map((k) => ({ + configId: testTable[k]!.configId, + locationId: testTable[k]!.locationId, + })); + }; + + it('selects the trends in the table', (selectResult) => { + expect(selectResult).toEqual(select(selectOverviewTrends)); + return testTable; + }); + + const firstApiCall = getComputedApiCall(0, 10); + const firstSuccessAction = firstApiCall.reduce(computedTrendsReducer, {}); + it('calls the api for the first chunk', (callResult) => { + expect(callResult).toEqual(call(trendsApi, firstApiCall)); + + return firstSuccessAction; + }); + + const secondApiCall = getComputedApiCall(10, 20); + const secondSuccessAction = secondApiCall.reduce(computedTrendsReducer, {}); + it('calls the api for the second chunk', (callResult) => { + expect(callResult).toEqual(call(trendsApi, secondApiCall)); + + return secondSuccessAction; + }); + + const thirdApiCall = getComputedApiCall(20, 30); + const thirdSuccessAction = thirdApiCall.reduce(computedTrendsReducer, {}); + it('calls the api for the third chunk', (callResult) => { + expect(callResult).toEqual(call(trendsApi, thirdApiCall)); + + return thirdSuccessAction; + }); + + const batchSuccessPayload = { + ...firstSuccessAction, + ...secondSuccessAction, + ...thirdSuccessAction, + }; + it('sends trend stats success action for the second chunk', (putResult) => { + expect(putResult).toEqual(put(trendStatsBatch.success(batchSuccessPayload))); }); }); }); From 86146dba75af108047a3f3271ccdfbe0c0600987 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Thu, 29 Aug 2024 10:58:25 -0400 Subject: [PATCH 23/39] Add `msearch` helper to Synthetics ES client. --- .../synthetics/server/lib.test.ts | 68 +++++++++++++++++++ .../synthetics/server/lib.ts | 33 ++++++++- 2 files changed, 100 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/observability_solution/synthetics/server/lib.test.ts b/x-pack/plugins/observability_solution/synthetics/server/lib.test.ts index 2de1a2b530734..bfb19213c4f43 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/lib.test.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/lib.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { MsearchResponse } from '@elastic/elasticsearch/lib/api/types'; import { SyntheticsEsClient } from './lib'; import { savedObjectsClientMock, uiSettingsServiceMock } from '@kbn/core/server/mocks'; import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks'; @@ -22,6 +23,73 @@ describe('SyntheticsEsClient', () => { jest.clearAllMocks(); }); + describe('msearch', () => { + it('should call baseESClient.msearch with correct parameters', async () => { + esClient.msearch.mockResolvedValueOnce({ + body: { + responses: [ + { aggregations: { aggName: { value: 'str' } } }, + { aggregations: { aggName: { value: 'str' } } }, + ], + }, + } as unknown as MsearchResponse); + + const mockSearchParams = [ + { + query: { + match_all: {}, + }, + }, + { + query: { + match_all: {}, + }, + }, + ]; + + const result = await syntheticsEsClient.msearch(mockSearchParams); + + expect(esClient.msearch).toHaveBeenCalledWith( + { + searches: [ + { + index: 'synthetics-*', + ignore_unavailable: true, + }, + mockSearchParams[0], + { + index: 'synthetics-*', + ignore_unavailable: true, + }, + mockSearchParams[1], + ], + }, + { meta: true } + ); + + expect(result).toMatchInlineSnapshot(` + Object { + "responses": Array [ + Object { + "aggregations": Object { + "aggName": Object { + "value": "str", + }, + }, + }, + Object { + "aggregations": Object { + "aggName": Object { + "value": "str", + }, + }, + }, + ], + } + `); + }); + }); + describe('search', () => { it('should call baseESClient.search with correct parameters', async () => { const mockSearchParams = { diff --git a/x-pack/plugins/observability_solution/synthetics/server/lib.ts b/x-pack/plugins/observability_solution/synthetics/server/lib.ts index e4877f09f66a9..12f08d679c89e 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/lib.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/lib.ts @@ -5,6 +5,10 @@ * 2.0. */ +import { + MsearchMultisearchBody, + MsearchMultisearchHeader, +} from '@elastic/elasticsearch/lib/api/types'; import { ElasticsearchClient, SavedObjectsClientContract, @@ -13,7 +17,7 @@ import { } from '@kbn/core/server'; import chalk from 'chalk'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import type { ESSearchResponse } from '@kbn/es-types'; +import type { ESSearchResponse, InferSearchResponseOf } from '@kbn/es-types'; import { RequestStatus } from '@kbn/inspector-plugin/common'; import { InspectResponse } from '@kbn/observability-plugin/typings/common'; import { enableInspectEsQueries } from '@kbn/observability-plugin/common'; @@ -116,6 +120,33 @@ export class SyntheticsEsClient { return res; } + + async msearch< + TDocument = unknown, + TSearchRequest extends estypes.SearchRequest = estypes.SearchRequest + >( + requests: MsearchMultisearchBody[] + ): Promise<{ responses: Array> }> { + const searches: Array = []; + for (const request of requests) { + searches.push({ index: SYNTHETICS_INDEX_PATTERN, ignore_unavailable: true }); + searches.push(request); + } + + const results = await this.baseESClient.msearch( + { + searches, + }, + { meta: true } + ); + + return { + responses: results.body.responses as unknown as Array< + InferSearchResponseOf + >, + }; + } + async count(params: TParams): Promise { let res: any; let esError: any; From 9aa1c0ca07b8ad01ab0e8a7cd5b09df587d78a54 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Thu, 29 Aug 2024 11:56:37 -0400 Subject: [PATCH 24/39] Move trend types to common for re-use on server. --- .../synthetics/common/types/index.ts | 1 + .../synthetics/common/types/overview.ts | 30 +++++++++++++++++++ .../overview/overview/overview_grid.tsx | 2 +- .../apps/synthetics/state/overview/actions.ts | 3 +- .../apps/synthetics/state/overview/api.ts | 3 +- .../synthetics/state/overview/effects.test.ts | 3 +- .../apps/synthetics/state/overview/effects.ts | 4 +-- .../apps/synthetics/state/overview/models.ts | 26 ++-------------- 8 files changed, 41 insertions(+), 31 deletions(-) create mode 100644 x-pack/plugins/observability_solution/synthetics/common/types/overview.ts diff --git a/x-pack/plugins/observability_solution/synthetics/common/types/index.ts b/x-pack/plugins/observability_solution/synthetics/common/types/index.ts index be369427c47e7..6544898d54a64 100644 --- a/x-pack/plugins/observability_solution/synthetics/common/types/index.ts +++ b/x-pack/plugins/observability_solution/synthetics/common/types/index.ts @@ -8,3 +8,4 @@ export * from './synthetics_monitor'; export * from './monitor_validation'; export * from './default_alerts'; +export * from './overview'; diff --git a/x-pack/plugins/observability_solution/synthetics/common/types/overview.ts b/x-pack/plugins/observability_solution/synthetics/common/types/overview.ts new file mode 100644 index 0000000000000..38887a6eabe78 --- /dev/null +++ b/x-pack/plugins/observability_solution/synthetics/common/types/overview.ts @@ -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. + */ + +export interface TrendKey { + configId: string; + locationId: string; +} + +export interface TrendDatum { + x: number; + y: number; +} + +export interface OverviewTrend { + configId: string; + locationId: string; + data: TrendDatum[]; + count: number; + min: number; + max: number; + avg: number; + sum: number; + median: number; +} + +export type TrendTable = Record; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx index 3fdcf425b22a4..42d16fe0a1222 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx @@ -18,13 +18,13 @@ import { EuiAutoSizer, EuiAutoSize, } from '@elastic/eui'; +import type { TrendKey } from '../../../../../../../common/types'; import { SYNTHETICS_MONITORS_EMBEDDABLE } from '../../../../../embeddables/constants'; import { AddToDashboard } from '../../../common/components/add_to_dashboard'; import { useOverviewStatus } from '../../hooks/use_overview_status'; import { GridItemsByGroup } from './grid_by_group/grid_items_by_group'; import { GroupFields } from './grid_by_group/group_fields'; import { - TrendKey, fetchMonitorOverviewAction, quietFetchOverviewAction, refreshOverviewTrends, diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/actions.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/actions.ts index ed62690f79608..4f8e40325b3c2 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/actions.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/actions.ts @@ -5,14 +5,13 @@ * 2.0. */ import { createAction } from '@reduxjs/toolkit'; +import { TrendKey, TrendTable } from '../../../../../common/types'; import { createAsyncAction } from '../utils/actions'; import type { MonitorOverviewFlyoutConfig, MonitorOverviewPageState, MonitorOverviewState, - TrendKey, - TrendTable, } from './models'; import { MonitorOverviewResult } from '../../../../../common/runtime_types'; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/api.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/api.ts index 275f0a22e0471..546ebf99cef4c 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/api.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/api.ts @@ -13,8 +13,9 @@ import { OverviewStatus, OverviewStatusCodec, } from '../../../../../common/runtime_types'; +import type { TrendKey, TrendTable } from '../../../../../common/types'; import { apiService } from '../../../../utils/api_service'; -import type { MonitorOverviewPageState, TrendKey, TrendTable } from './models'; +import type { MonitorOverviewPageState } from './models'; function toMonitorOverviewQueryArgs( pageState: MonitorOverviewPageState diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.test.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.test.ts index b9765505c2976..3d13c1abca9a2 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.test.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.test.ts @@ -7,10 +7,11 @@ import sagaHelper from 'redux-saga-testing'; import { call, put, select } from 'redux-saga/effects'; +import { OverviewTrend, TrendKey, TrendTable } from '../../../../../common/types'; import { TRENDS_CHUNK_SIZE, fetchTrendEffect, refreshTrends } from './effects'; import { trendStatsBatch } from './actions'; import { fetchOverviewTrendStats as trendsApi } from './api'; -import { OverviewTrend, TrendKey, TrendTable, selectOverviewTrends } from '.'; +import { selectOverviewTrends } from '.'; const generateTrendRequests = () => { const ar: TrendKey[] = []; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.ts index 048b5fdecc465..f30e18067d8da 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.ts @@ -6,8 +6,9 @@ */ import { debounce, call, takeLeading, takeEvery, put, select } from 'redux-saga/effects'; -import { selectOverviewTrends } from './selectors'; +import type { TrendTable } from '../../../../../common/types'; import { fetchEffectFactory } from '../utils/fetch_effect'; +import { selectOverviewTrends } from './selectors'; import { fetchMonitorOverviewAction, quietFetchOverviewAction, @@ -15,7 +16,6 @@ import { trendStatsBatch, } from './actions'; import { fetchMonitorOverview, fetchOverviewTrendStats as trendsApi } from './api'; -import type { TrendTable } from './models'; export function* fetchMonitorOverviewEffect() { yield debounce( diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/models.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/models.ts index f848edf883988..0dbc2100c2fee 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/models.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/models.ts @@ -4,6 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + +import type { TrendTable } from '../../../../../common/types'; import type { MonitorListSortField } from '../../../../../common/runtime_types/monitor_management/sort_field'; import { ConfigKey, MonitorOverviewResult } from '../../../../../common/runtime_types'; @@ -39,27 +41,3 @@ export interface GroupByState { field: ConfigKey.TAGS | ConfigKey.PROJECT_ID | ConfigKey.MONITOR_TYPE | 'locationId' | 'none'; order: 'asc' | 'desc'; } - -export interface TrendKey { - configId: string; - locationId: string; -} - -export interface TrendDatum { - x: number; - y: number; -} - -export interface OverviewTrend { - configId: string; - locationId: string; - data: TrendDatum[]; - count: number; - min: number; - max: number; - avg: number; - sum: number; - median: number; -} - -export type TrendTable = Record; From a27f99cab8b8e6351047487b2cdc45836951cfe4 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Thu, 29 Aug 2024 11:58:59 -0400 Subject: [PATCH 25/39] Clean up. --- .../apps/synthetics/state/overview/effects.ts | 2 +- .../routes/overview_trends/fetch_trends.ts | 214 +++++++++--------- .../routes/overview_trends/overview_trends.ts | 118 ++++++---- 3 files changed, 184 insertions(+), 150 deletions(-) diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.ts index f30e18067d8da..46f2a34aeea30 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.ts @@ -29,7 +29,7 @@ export function* fetchMonitorOverviewEffect() { ); } -export const TRENDS_CHUNK_SIZE = 20; +export const TRENDS_CHUNK_SIZE = 50; export function* fetchTrendEffect( action: ReturnType diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/fetch_trends.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/fetch_trends.ts index b5fc635384f80..095b964434cd2 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/fetch_trends.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/fetch_trends.ts @@ -5,139 +5,137 @@ * 2.0. */ -import { SearchRequest } from '@elastic/elasticsearch/lib/api/types'; -import { SyntheticsEsClient } from '../../lib'; +import { MsearchMultisearchBody } from '@elastic/elasticsearch/lib/api/types'; -export function fetchTrends(configId: string, locationIds: string[], esClient: SyntheticsEsClient) { - const query: SearchRequest = { - size: 0, - query: { - bool: { - filter: [ - { - bool: { - filter: [ - { - exists: { - field: 'summary', - }, +export const getFetchTrendsQuery = ( + configId: string, + locationIds: string[] +): MsearchMultisearchBody => ({ + size: 0, + query: { + bool: { + filter: [ + { + bool: { + filter: [ + { + exists: { + field: 'summary', }, - { - bool: { - should: [ - { - bool: { - should: [ - { - match: { - 'summary.final_attempt': true, - }, + }, + { + bool: { + should: [ + { + bool: { + should: [ + { + match: { + 'summary.final_attempt': true, }, - ], - minimum_should_match: 1, - }, + }, + ], + minimum_should_match: 1, }, - { - bool: { - must_not: { - bool: { - should: [ - { - exists: { - field: 'summary.final_attempt', - }, + }, + { + bool: { + must_not: { + bool: { + should: [ + { + exists: { + field: 'summary.final_attempt', }, - ], - minimum_should_match: 1, - }, + }, + ], + minimum_should_match: 1, }, }, }, - ], - minimum_should_match: 1, - }, + }, + ], + minimum_should_match: 1, }, - ], - }, + }, + ], }, - { - bool: { - must_not: { - exists: { - field: 'run_once', - }, + }, + { + bool: { + must_not: { + exists: { + field: 'run_once', }, }, }, - { - terms: { - 'observer.name': locationIds, - }, + }, + { + terms: { + 'observer.name': locationIds, }, - { - term: { - config_id: configId, - }, + }, + { + term: { + config_id: configId, }, - { - range: { - '@timestamp': { - gte: 'now-9h', - lte: 'now', - }, + }, + { + range: { + '@timestamp': { + gte: 'now-9h', + lte: 'now', }, }, - ], - }, - }, - aggs: { - byId: { - terms: { - field: 'config_id', }, - aggs: { - byLocation: { - terms: { - field: 'observer.name', - }, - aggs: { - last_50: { - top_hits: { - size: 50, - sort: [ - { - '@timestamp': { - order: 'desc', - }, + ], + }, + }, + aggs: { + byId: { + terms: { + field: 'config_id', + }, + aggs: { + byLocation: { + terms: { + field: 'observer.name', + }, + aggs: { + last_50: { + top_hits: { + size: 50, + sort: [ + { + '@timestamp': { + order: 'desc', }, - ], - _source: ['monitor.duration.us'], - }, + }, + ], + _source: ['monitor.duration.us'], }, + }, + stats: { stats: { - stats: { - field: 'monitor.duration.us', - }, + field: 'monitor.duration.us', }, - median: { - percentiles: { - field: 'monitor.duration.us', - percents: [50], - }, + }, + median: { + percentiles: { + field: 'monitor.duration.us', + percents: [50], }, }, }, }, }, }, - _source: false, - sort: [ - { - '@timestamp': 'desc', - }, - ], - fields: ['monitor.duration.us'], - }; - - return esClient.search({ body: query }); -} + }, + _source: false, + sort: [ + { + '@timestamp': 'desc', + }, + ], + fields: ['monitor.duration.us'], +}); diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/overview_trends.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/overview_trends.ts index 3fe8f1854b720..88479cb77e9aa 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/overview_trends.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/overview_trends.ts @@ -5,54 +5,90 @@ * 2.0. */ -import { withSpan } from '@kbn/apm-utils'; import { schema } from '@kbn/config-schema'; import { SYNTHETICS_API_URLS } from '../../../common/constants'; -import { SyntheticsRestApiRouteFactory } from '../types'; -import { fetchTrends } from './fetch_trends'; -import { TrendKey } from '../../../public/apps/synthetics/state'; +import { TrendKey } from '../../../common/types'; +import { getFetchTrendsQuery } from './fetch_trends'; -export const createOverviewTrendsRoute: SyntheticsRestApiRouteFactory = () => ({ +interface TrendAggs { + aggregations: { + byId: { + buckets: Array<{ + key: string; + byLocation: { + buckets: Array<{ + key: string; + last_50: { + hits: { + hits: Array<{ + _source: { + monitor: { + duration: { + us: number; + }; + }; + }; + }>; + }; + }; + stats: {}; + median: { + values: { + '50.0': number; + }; + }; + }>; + }; + }>; + }; + }; +} + +export const createOverviewTrendsRoute = () => ({ method: 'POST', path: SYNTHETICS_API_URLS.OVERVIEW_TRENDS, validate: { - body: schema.any(), + body: schema.arrayOf( + schema.object({ + configId: schema.string(), + locationId: schema.string(), + }) + ), }, - handler: async (routeContext): Promise => { - return withSpan('fetch trends', async () => { - const esClient = routeContext.syntheticsEsClient; - const body = routeContext.request.body as TrendKey[]; - const configs = body.reduce((acc: Record, { configId, locationId }) => { - if (!acc[configId]) { - acc[configId] = [locationId]; - } else { - acc[configId].push(locationId); - } - return acc; - }, {}); - const results = await Promise.all( - Object.keys(configs).map((key) => fetchTrends(key, configs[key], esClient)) - ); - let main = {}; - for (const res of results) { - const aggregations = res.body.aggregations as any; - aggregations.byId.buckets.map(({ key, byLocation }: { key: string; byLocation: any }) => { - const ret: Record = {}; - for (const location of byLocation.buckets) { - ret[key + location.key] = { - configId: key, - locationId: location.key, - data: location.last_50.hits.hits - .reverse() - .map((hit: any, x: number) => ({ x, y: hit._source.monitor.duration.us })), - ...location.stats, - median: location.median.values['50.0'], - }; - } - main = { ...main, ...ret }; - }); + handler: async (routeContext) => { + const esClient = routeContext.syntheticsEsClient; + const body = routeContext.request.body as TrendKey[]; + const configs = body.reduce((acc: Record, { configId, locationId }) => { + if (!acc[configId]) { + acc[configId] = [locationId]; + } else { + acc[configId].push(locationId); } - return routeContext.response.ok({ body: main }); - }); + return acc; + }, {}); + + const requests = Object.keys(configs).map((key) => getFetchTrendsQuery(key, configs[key])); + const results = await esClient.msearch(requests); + + let main = {}; + for (const res of results.responses as unknown as TrendAggs[]) { + const aggregations = res.aggregations; + aggregations.byId.buckets.map(({ key, byLocation }) => { + const ret: Record = {}; + for (const location of byLocation.buckets) { + ret[key + location.key] = { + configId: key, + locationId: location.key, + data: location.last_50.hits.hits + .reverse() + .map((hit: any, x: number) => ({ x, y: hit._source.monitor.duration.us })), + ...location.stats, + median: location.median.values['50.0'], + }; + } + main = { ...main, ...ret }; + }); + } + return routeContext.response.ok({ body: main }); }, }); From 6f976f050713181ae0f493ffa17d1885eb5b16d0 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Thu, 29 Aug 2024 12:34:19 -0400 Subject: [PATCH 26/39] Delete unneeded filters from query. --- .../routes/overview_trends/fetch_trends.ts | 35 ------------------- .../routes/overview_trends/overview_trends.ts | 2 +- 2 files changed, 1 insertion(+), 36 deletions(-) diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/fetch_trends.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/fetch_trends.ts index 095b964434cd2..c4a4fcb733140 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/fetch_trends.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/fetch_trends.ts @@ -23,41 +23,6 @@ export const getFetchTrendsQuery = ( field: 'summary', }, }, - { - bool: { - should: [ - { - bool: { - should: [ - { - match: { - 'summary.final_attempt': true, - }, - }, - ], - minimum_should_match: 1, - }, - }, - { - bool: { - must_not: { - bool: { - should: [ - { - exists: { - field: 'summary.final_attempt', - }, - }, - ], - minimum_should_match: 1, - }, - }, - }, - }, - ], - minimum_should_match: 1, - }, - }, ], }, }, diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/overview_trends.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/overview_trends.ts index 88479cb77e9aa..0871fc732d518 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/overview_trends.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/overview_trends.ts @@ -66,7 +66,7 @@ export const createOverviewTrendsRoute = () => ({ } return acc; }, {}); - + const requests = Object.keys(configs).map((key) => getFetchTrendsQuery(key, configs[key])); const results = await esClient.msearch(requests); From 60bc9edf8f61ebe3e01dbb32a60c19d9552b9093 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Thu, 29 Aug 2024 12:57:25 -0400 Subject: [PATCH 27/39] Prefer ``. --- .../overview/overview/metric_item.tsx | 42 ++++--------------- 1 file changed, 9 insertions(+), 33 deletions(-) diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx index 0cc655f6337ff..243f333a424d6 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx @@ -31,6 +31,7 @@ import { toggleTestNowFlyoutAction, } from '../../../../state/manual_test_runs'; import { MetricItemIcon } from './metric_item_icon'; +import { MetricItemExtra } from './metric_item/metric_item_extra'; const METRIC_ITEM_HEIGHT = 160; @@ -144,39 +145,14 @@ export const MetricItem = ({ trendShape: MetricTrendShape.Area, trend: trendData?.data ?? [], extra: trendData ? ( - - - {i18n.translate('xpack.synthetics.overview.duration.label', { - defaultMessage: 'Duration', - })} - - - - - + ) : (
Date: Fri, 30 Aug 2024 14:18:46 -0400 Subject: [PATCH 28/39] Clean up and fix types. --- .../monitors_page/overview/overview/metric_item.tsx | 2 +- .../server/routes/overview_trends/overview_trends.ts | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx index 243f333a424d6..57c50bc54a5bf 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx @@ -9,7 +9,7 @@ import React, { useState } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { css } from '@emotion/react'; import { Chart, Settings, Metric, MetricTrendShape } from '@elastic/charts'; -import { EuiFlexGroup, EuiFlexItem, EuiIconTip, EuiPanel, EuiSpacer } from '@elastic/eui'; +import { EuiPanel, EuiSpacer } from '@elastic/eui'; import { DARK_THEME } from '@elastic/charts'; import { useTheme } from '@kbn/observability-shared-plugin/public'; import moment from 'moment'; diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/overview_trends.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/overview_trends.ts index 0871fc732d518..889397ec75368 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/overview_trends.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/overview_trends.ts @@ -5,10 +5,11 @@ * 2.0. */ -import { schema } from '@kbn/config-schema'; +import { ObjectType, schema } from '@kbn/config-schema'; import { SYNTHETICS_API_URLS } from '../../../common/constants'; import { TrendKey } from '../../../common/types'; import { getFetchTrendsQuery } from './fetch_trends'; +import { SyntheticsRestApiRouteFactory } from '../types'; interface TrendAggs { aggregations: { @@ -44,7 +45,7 @@ interface TrendAggs { }; } -export const createOverviewTrendsRoute = () => ({ +export const createOverviewTrendsRoute: SyntheticsRestApiRouteFactory = () => ({ method: 'POST', path: SYNTHETICS_API_URLS.OVERVIEW_TRENDS, validate: { @@ -53,7 +54,7 @@ export const createOverviewTrendsRoute = () => ({ configId: schema.string(), locationId: schema.string(), }) - ), + ) as unknown as ObjectType, }, handler: async (routeContext) => { const esClient = routeContext.syntheticsEsClient; From 9a16f16220ab8341b81b86b8a88d873025181a84 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Fri, 30 Aug 2024 14:19:03 -0400 Subject: [PATCH 29/39] Fix new tests for larger chunk size. --- .../public/apps/synthetics/state/overview/effects.test.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.test.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.test.ts index 3d13c1abca9a2..4edd5c915cad6 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.test.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.test.ts @@ -13,9 +13,12 @@ import { trendStatsBatch } from './actions'; import { fetchOverviewTrendStats as trendsApi } from './api'; import { selectOverviewTrends } from '.'; +const TEST_TRENDS_LENGTH = 80; + const generateTrendRequests = () => { const ar: TrendKey[] = []; - for (let i = 0; i < 30; i++) ar.push({ configId: `configId${i}`, locationId: 'location' }); + for (let i = 0; i < TEST_TRENDS_LENGTH; i++) + ar.push({ configId: `configId${i}`, locationId: 'location' }); return ar; }; @@ -31,7 +34,7 @@ describe('overview effects', () => { trendRequests.length - TRENDS_CHUNK_SIZE, trendRequests.length ); - const secondChunk = trendRequests.slice(0, 10); + const secondChunk = trendRequests.slice(0, TEST_TRENDS_LENGTH - TRENDS_CHUNK_SIZE); const firstChunkResponse = firstChunk.reduce(responseReducer, {}); const secondChunkResponse = secondChunk.reduce(responseReducer, {}); From 51e5160c1157e9699d7ee2978fb9167c66b8e1be Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Fri, 30 Aug 2024 14:39:20 -0400 Subject: [PATCH 30/39] Simplify. --- .../overview/overview/overview_grid.tsx | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx index 42d16fe0a1222..9dfaa7c78511d 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx @@ -181,24 +181,28 @@ export const OverviewGrid = memo(() => { itemData={listItems} ref={listRef} > - {(props: React.PropsWithChildren>) => ( + {({ + index: listIndex, + style, + data: listData, + }: React.PropsWithChildren>) => ( - {props.data[props.index].map((_, idx) => ( - + {listData[listIndex].map((_, idx) => ( + ))} - {props.data[props.index].length % ROW_COUNT !== 0 && + {listData[listIndex].length % ROW_COUNT !== 0 && // Adds empty items to fill out row Array.from({ - length: ROW_COUNT - props.data[props.index].length, + length: ROW_COUNT - listData[listIndex].length, }).map((_, idx) => )} )} From 80f8f888ae99e1a80657833755ba70356a34d7d3 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Fri, 30 Aug 2024 15:11:36 -0400 Subject: [PATCH 31/39] Simplify. --- .../public/apps/synthetics/state/overview/effects.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.ts index 46f2a34aeea30..5463bf6b46782 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.ts @@ -47,16 +47,18 @@ export function* fetchTrendEffect( yield put(trendStatsBatch.fail(e)); } } + export function* fetchOverviewTrendStats() { yield takeEvery(trendStatsBatch.get, fetchTrendEffect); } + export function* refreshTrends(): Generator { const existingTrends: TrendTable = yield select(selectOverviewTrends); let acc = {}; const keys = Object.keys(existingTrends); - do { + while (keys.length) { const chunk = keys - .splice(0, keys.length < 10 ? keys.length : 10) + .splice(0, keys.length < 10 ? keys.length : 40) .filter((key: string) => existingTrends[key] !== null) .map((key: string) => ({ configId: existingTrends[key]!.configId, @@ -66,11 +68,12 @@ export function* refreshTrends(): Generator { const res = yield call(trendsApi, chunk); acc = { ...acc, ...res }; } - } while (keys.length); + } if (Object.keys(acc).length) { yield put(trendStatsBatch.success(acc)); } } + export function* refreshOverviewTrendStats() { yield takeLeading(refreshOverviewTrends.get, refreshTrends); } From d5c836d902376c42b30e17c506ad2e8c515048b3 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Tue, 3 Sep 2024 13:26:57 -0400 Subject: [PATCH 32/39] Prefer histogram over top_hits for building duration chart. --- .../synthetics/common/types/overview.ts | 2 + .../overview/overview/overview_grid.tsx | 10 +++- .../apps/synthetics/state/overview/actions.ts | 6 +- .../apps/synthetics/state/overview/api.ts | 4 +- .../apps/synthetics/state/overview/effects.ts | 18 +++++- .../routes/overview_trends/fetch_trends.ts | 24 ++++---- .../routes/overview_trends/overview_trends.ts | 57 +++++++++++-------- 7 files changed, 75 insertions(+), 46 deletions(-) diff --git a/x-pack/plugins/observability_solution/synthetics/common/types/overview.ts b/x-pack/plugins/observability_solution/synthetics/common/types/overview.ts index 38887a6eabe78..4982f0f408fcf 100644 --- a/x-pack/plugins/observability_solution/synthetics/common/types/overview.ts +++ b/x-pack/plugins/observability_solution/synthetics/common/types/overview.ts @@ -10,6 +10,8 @@ export interface TrendKey { locationId: string; } +export type TrendRequest = TrendKey & { schedule: string }; + export interface TrendDatum { x: number; y: number; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx index 9dfaa7c78511d..97ad610935e66 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx @@ -18,7 +18,7 @@ import { EuiAutoSizer, EuiAutoSize, } from '@elastic/eui'; -import type { TrendKey } from '../../../../../../../common/types'; +import type { TrendRequest } from '../../../../../../../common/types'; import { SYNTHETICS_MONITORS_EMBEDDABLE } from '../../../../../embeddables/constants'; import { AddToDashboard } from '../../../common/components/add_to_dashboard'; import { useOverviewStatus } from '../../hooks/use_overview_status'; @@ -93,11 +93,15 @@ export const OverviewGrid = memo(() => { useEffect(() => { if (monitorsSortedByStatus.length && maxItem) { - const batch: TrendKey[] = []; + const batch: TrendRequest[] = []; const chunk = monitorsSortedByStatus.slice(0, (maxItem + 1) * ROW_COUNT); for (const item of chunk) { if (trendData[item.configId + item.location.id] === undefined) { - batch.push({ configId: item.configId, locationId: item.location.id }); + batch.push({ + configId: item.configId, + locationId: item.location.id, + schedule: item.schedule, + }); } } if (batch.length) dispatch(trendStatsBatch.get(batch)); diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/actions.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/actions.ts index 4f8e40325b3c2..b5098aaa7cbf6 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/actions.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/actions.ts @@ -5,7 +5,7 @@ * 2.0. */ import { createAction } from '@reduxjs/toolkit'; -import { TrendKey, TrendTable } from '../../../../../common/types'; +import { TrendRequest, TrendTable } from '../../../../../common/types'; import { createAsyncAction } from '../utils/actions'; import type { @@ -39,4 +39,6 @@ export const refreshOverviewTrends = createAsyncAction( 'refreshOverviewTrendStats' ); -export const trendStatsBatch = createAsyncAction('batchTrendStats'); +export const trendStatsBatch = createAsyncAction( + 'batchTrendStats' +); diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/api.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/api.ts index 546ebf99cef4c..2a8e782013651 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/api.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/api.ts @@ -13,7 +13,7 @@ import { OverviewStatus, OverviewStatusCodec, } from '../../../../../common/runtime_types'; -import type { TrendKey, TrendTable } from '../../../../../common/types'; +import type { TrendRequest, TrendTable } from '../../../../../common/types'; import { apiService } from '../../../../utils/api_service'; import type { MonitorOverviewPageState } from './models'; @@ -60,5 +60,5 @@ export const fetchOverviewStatus = async ( return apiService.get(SYNTHETICS_API_URLS.OVERVIEW_STATUS, params, OverviewStatusCodec); }; -export const fetchOverviewTrendStats = async (monitors: TrendKey[]): Promise => +export const fetchOverviewTrendStats = async (monitors: TrendRequest[]): Promise => monitors.length ? apiService.post(SYNTHETICS_API_URLS.OVERVIEW_TRENDS, monitors) : {}; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.ts index 5463bf6b46782..930c729bde4f7 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.ts @@ -8,7 +8,7 @@ import { debounce, call, takeLeading, takeEvery, put, select } from 'redux-saga/effects'; import type { TrendTable } from '../../../../../common/types'; import { fetchEffectFactory } from '../utils/fetch_effect'; -import { selectOverviewTrends } from './selectors'; +import { selectOverviewState, selectOverviewTrends } from './selectors'; import { fetchMonitorOverviewAction, quietFetchOverviewAction, @@ -16,6 +16,7 @@ import { trendStatsBatch, } from './actions'; import { fetchMonitorOverview, fetchOverviewTrendStats as trendsApi } from './api'; +import { MonitorOverviewState } from '.'; export function* fetchMonitorOverviewEffect() { yield debounce( @@ -52,17 +53,28 @@ export function* fetchOverviewTrendStats() { yield takeEvery(trendStatsBatch.get, fetchTrendEffect); } -export function* refreshTrends(): Generator { +export function* refreshTrends(): Generator { const existingTrends: TrendTable = yield select(selectOverviewTrends); + const overviewState: MonitorOverviewState = yield select(selectOverviewState); + let acc = {}; const keys = Object.keys(existingTrends); while (keys.length) { const chunk = keys .splice(0, keys.length < 10 ? keys.length : 40) - .filter((key: string) => existingTrends[key] !== null) + .filter( + (key: string) => + existingTrends[key] !== null && + overviewState.data.monitors.some( + ({ configId }) => configId === existingTrends[key]!.configId + ) + ) .map((key: string) => ({ configId: existingTrends[key]!.configId, locationId: existingTrends[key]!.locationId, + schedule: overviewState.data.monitors.find( + ({ configId }) => configId === existingTrends[key]!.configId + )!.schedule, })); if (chunk.length) { const res = yield call(trendsApi, chunk); diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/fetch_trends.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/fetch_trends.ts index c4a4fcb733140..29f6bd78f2f44 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/fetch_trends.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/fetch_trends.ts @@ -9,7 +9,8 @@ import { MsearchMultisearchBody } from '@elastic/elasticsearch/lib/api/types'; export const getFetchTrendsQuery = ( configId: string, - locationIds: string[] + locationIds: string[], + interval: number ): MsearchMultisearchBody => ({ size: 0, query: { @@ -48,7 +49,7 @@ export const getFetchTrendsQuery = ( { range: { '@timestamp': { - gte: 'now-9h', + gte: `now-${interval}m`, lte: 'now', }, }, @@ -68,16 +69,17 @@ export const getFetchTrendsQuery = ( }, aggs: { last_50: { - top_hits: { - size: 50, - sort: [ - { - '@timestamp': { - order: 'desc', - }, + histogram: { + field: '@timestamp', + interval: interval * 1000, + min_doc_count: 1, + }, + aggs: { + max: { + avg: { + field: 'monitor.duration.us', }, - ], - _source: ['monitor.duration.us'], + }, }, }, stats: { diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/overview_trends.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/overview_trends.ts index 889397ec75368..6dfcb1ee4593b 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/overview_trends.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/overview_trends.ts @@ -7,7 +7,7 @@ import { ObjectType, schema } from '@kbn/config-schema'; import { SYNTHETICS_API_URLS } from '../../../common/constants'; -import { TrendKey } from '../../../common/types'; +import { TrendRequest } from '../../../common/types'; import { getFetchTrendsQuery } from './fetch_trends'; import { SyntheticsRestApiRouteFactory } from '../types'; @@ -20,17 +20,9 @@ interface TrendAggs { buckets: Array<{ key: string; last_50: { - hits: { - hits: Array<{ - _source: { - monitor: { - duration: { - us: number; - }; - }; - }; - }>; - }; + buckets: Array<{ + max: { value: number }; + }>; }; stats: {}; median: { @@ -45,6 +37,9 @@ interface TrendAggs { }; } +export const getTimeRangeFilter = (schedule: string, numChecks = 50) => + Number(schedule) * numChecks; + export const createOverviewTrendsRoute: SyntheticsRestApiRouteFactory = () => ({ method: 'POST', path: SYNTHETICS_API_URLS.OVERVIEW_TRENDS, @@ -53,22 +48,31 @@ export const createOverviewTrendsRoute: SyntheticsRestApiRouteFactory = () => ({ schema.object({ configId: schema.string(), locationId: schema.string(), + schedule: schema.string(), }) ) as unknown as ObjectType, }, handler: async (routeContext) => { const esClient = routeContext.syntheticsEsClient; - const body = routeContext.request.body as TrendKey[]; - const configs = body.reduce((acc: Record, { configId, locationId }) => { - if (!acc[configId]) { - acc[configId] = [locationId]; - } else { - acc[configId].push(locationId); - } - return acc; - }, {}); + const body = routeContext.request.body as TrendRequest[]; + const configs = body.reduce( + ( + acc: Record, + { configId, locationId, schedule } + ) => { + if (!acc[configId]) { + acc[configId] = { locations: [locationId], interval: getTimeRangeFilter(schedule) }; + } else { + acc[configId].locations.push(locationId); + } + return acc; + }, + {} + ); - const requests = Object.keys(configs).map((key) => getFetchTrendsQuery(key, configs[key])); + const requests = Object.keys(configs).map((key) => + getFetchTrendsQuery(key, configs[key].locations, configs[key].interval) + ); const results = await esClient.msearch(requests); let main = {}; @@ -80,9 +84,12 @@ export const createOverviewTrendsRoute: SyntheticsRestApiRouteFactory = () => ({ ret[key + location.key] = { configId: key, locationId: location.key, - data: location.last_50.hits.hits - .reverse() - .map((hit: any, x: number) => ({ x, y: hit._source.monitor.duration.us })), + data: location.last_50.buckets.map( + (durationBucket: { max: { value: number } }, x: number) => ({ + x, + y: durationBucket.max.value, + }) + ), ...location.stats, median: location.median.values['50.0'], }; From dea9c2e0fe1fa8344b63a1c35763f2b0ce2d13c2 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Tue, 3 Sep 2024 13:27:04 -0400 Subject: [PATCH 33/39] Remove overly-complex tests. --- .../synthetics/state/overview/effects.test.ts | 104 ++++-------------- 1 file changed, 23 insertions(+), 81 deletions(-) diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.test.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.test.ts index 4edd5c915cad6..ceef406ebbd1b 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.test.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/effects.test.ts @@ -7,18 +7,18 @@ import sagaHelper from 'redux-saga-testing'; import { call, put, select } from 'redux-saga/effects'; -import { OverviewTrend, TrendKey, TrendTable } from '../../../../../common/types'; +import { TrendKey, TrendRequest, TrendTable } from '../../../../../common/types'; import { TRENDS_CHUNK_SIZE, fetchTrendEffect, refreshTrends } from './effects'; import { trendStatsBatch } from './actions'; import { fetchOverviewTrendStats as trendsApi } from './api'; -import { selectOverviewTrends } from '.'; +import { selectOverviewState, selectOverviewTrends } from '.'; const TEST_TRENDS_LENGTH = 80; const generateTrendRequests = () => { - const ar: TrendKey[] = []; + const ar: TrendRequest[] = []; for (let i = 0; i < TEST_TRENDS_LENGTH; i++) - ar.push({ configId: `configId${i}`, locationId: 'location' }); + ar.push({ configId: `configId${i}`, locationId: 'location', schedule: '3' }); return ar; }; @@ -73,6 +73,11 @@ describe('overview effects', () => { return { monitor1: null, monitor2: null, monitor3: null }; }); + it('selects the overview state', (selectResult) => { + expect(selectResult).toEqual(select(selectOverviewState)); + return { data: { monitors: [] } }; + }); + it('skips the API if the data is null', (result) => { expect(result).toBeUndefined(); }); @@ -143,11 +148,23 @@ describe('overview effects', () => { return table; }); + it('selects the overview state', (selectResults) => { + expect(selectResults).toEqual(select(selectOverviewState)); + return { + data: { + monitors: [ + { configId: 'monitor1', schedule: '3' }, + { configId: 'monitor3', schedule: '3' }, + ], + }, + }; + }); + it('calls the api for the first chunk', (callResult) => { expect(callResult).toEqual( call(trendsApi, [ - { configId: 'monitor1', locationId: 'location' }, - { configId: 'monitor3', locationId: 'location' }, + { configId: 'monitor1', locationId: 'location', schedule: '3' }, + { configId: 'monitor3', locationId: 'location', schedule: '3' }, ]) ); @@ -158,79 +175,4 @@ describe('overview effects', () => { expect(putResult).toEqual(put(trendStatsBatch.success(apiResponse))); }); }); - - describe('refreshTrends with multiple pages', () => { - const it = sagaHelper(refreshTrends() as IterableIterator); - function generateTable() { - const table: TrendTable = {}; - for (let i = 0; i < 30; i++) { - table[`monitor${i}location`] = { - configId: `monitor${i}`, - locationId: 'location', - data: [{ x: 0, y: 1 }], - count: 1, - median: 1, - min: 0, - max: 0, - avg: 0, - sum: 0, - }; - } - return table; - } - - const testTable = generateTable(); - - const computedTrendsReducer = (acc: Record, curr: TrendKey) => ({ - ...acc, - [curr.configId + curr.locationId]: testTable[curr.configId + curr.locationId] ?? null, - }); - - const getComputedApiCall = (start: number, end: number) => { - return Object.keys(testTable) - .slice(start, end) - .map((k) => ({ - configId: testTable[k]!.configId, - locationId: testTable[k]!.locationId, - })); - }; - - it('selects the trends in the table', (selectResult) => { - expect(selectResult).toEqual(select(selectOverviewTrends)); - return testTable; - }); - - const firstApiCall = getComputedApiCall(0, 10); - const firstSuccessAction = firstApiCall.reduce(computedTrendsReducer, {}); - it('calls the api for the first chunk', (callResult) => { - expect(callResult).toEqual(call(trendsApi, firstApiCall)); - - return firstSuccessAction; - }); - - const secondApiCall = getComputedApiCall(10, 20); - const secondSuccessAction = secondApiCall.reduce(computedTrendsReducer, {}); - it('calls the api for the second chunk', (callResult) => { - expect(callResult).toEqual(call(trendsApi, secondApiCall)); - - return secondSuccessAction; - }); - - const thirdApiCall = getComputedApiCall(20, 30); - const thirdSuccessAction = thirdApiCall.reduce(computedTrendsReducer, {}); - it('calls the api for the third chunk', (callResult) => { - expect(callResult).toEqual(call(trendsApi, thirdApiCall)); - - return thirdSuccessAction; - }); - - const batchSuccessPayload = { - ...firstSuccessAction, - ...secondSuccessAction, - ...thirdSuccessAction, - }; - it('sends trend stats success action for the second chunk', (putResult) => { - expect(putResult).toEqual(put(trendStatsBatch.success(batchSuccessPayload))); - }); - }); }); From 57b031fbba016d5db3dbaf91503e9c581c276d74 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Tue, 3 Sep 2024 13:28:39 -0400 Subject: [PATCH 34/39] Rename a function. --- .../server/routes/overview_trends/overview_trends.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/overview_trends.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/overview_trends.ts index 6dfcb1ee4593b..0c0361ee0b27b 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/overview_trends.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/overview_trends.ts @@ -37,7 +37,7 @@ interface TrendAggs { }; } -export const getTimeRangeFilter = (schedule: string, numChecks = 50) => +export const getIntervalForCheckCount = (schedule: string, numChecks = 50) => Number(schedule) * numChecks; export const createOverviewTrendsRoute: SyntheticsRestApiRouteFactory = () => ({ @@ -61,7 +61,7 @@ export const createOverviewTrendsRoute: SyntheticsRestApiRouteFactory = () => ({ { configId, locationId, schedule } ) => { if (!acc[configId]) { - acc[configId] = { locations: [locationId], interval: getTimeRangeFilter(schedule) }; + acc[configId] = { locations: [locationId], interval: getIntervalForCheckCount(schedule) }; } else { acc[configId].locations.push(locationId); } From bb312426e6224e806d36d9397cbe3181a948689f Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 3 Sep 2024 17:40:21 +0000 Subject: [PATCH 35/39] [CI] Auto-commit changed files from 'node scripts/yarn_deduplicate' --- x-pack/plugins/observability_solution/synthetics/tsconfig.json | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugins/observability_solution/synthetics/tsconfig.json b/x-pack/plugins/observability_solution/synthetics/tsconfig.json index cc042f965a5aa..8446b7850d3a9 100644 --- a/x-pack/plugins/observability_solution/synthetics/tsconfig.json +++ b/x-pack/plugins/observability_solution/synthetics/tsconfig.json @@ -90,7 +90,6 @@ "@kbn/react-kibana-context-render", "@kbn/react-kibana-context-theme", "@kbn/search-types", - "@kbn/apm-utils", "@kbn/core-lifecycle-browser", "@kbn/ui-actions-browser", "@kbn/presentation-publishing", From d20fab4a5b8007a2cb6593cd826bf139cba14c4a Mon Sep 17 00:00:00 2001 From: shahzad31 Date: Wed, 4 Sep 2024 14:21:52 +0200 Subject: [PATCH 36/39] update types --- .../synthetics/server/lib.ts | 4 +- .../routes/overview_trends/fetch_trends.ts | 142 ++++++++---------- .../routes/overview_trends/overview_trends.ts | 54 ++----- 3 files changed, 78 insertions(+), 122 deletions(-) diff --git a/x-pack/plugins/observability_solution/synthetics/server/lib.ts b/x-pack/plugins/observability_solution/synthetics/server/lib.ts index 12f08d679c89e..63d511a2d2063 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/lib.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/lib.ts @@ -122,8 +122,8 @@ export class SyntheticsEsClient { } async msearch< - TDocument = unknown, - TSearchRequest extends estypes.SearchRequest = estypes.SearchRequest + TSearchRequest extends estypes.SearchRequest = estypes.SearchRequest, + TDocument = unknown >( requests: MsearchMultisearchBody[] ): Promise<{ responses: Array> }> { diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/fetch_trends.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/fetch_trends.ts index 29f6bd78f2f44..b57c634c1bddd 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/fetch_trends.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/fetch_trends.ts @@ -5,104 +5,88 @@ * 2.0. */ -import { MsearchMultisearchBody } from '@elastic/elasticsearch/lib/api/types'; +import { EXCLUDE_RUN_ONCE_FILTER, SUMMARY_FILTER } from '../../../common/constants/client_defaults'; +import { createEsParams } from '../../lib'; -export const getFetchTrendsQuery = ( - configId: string, - locationIds: string[], - interval: number -): MsearchMultisearchBody => ({ - size: 0, - query: { - bool: { - filter: [ - { - bool: { - filter: [ - { - exists: { - field: 'summary', - }, +export const getFetchTrendsQuery = (configId: string, locationIds: string[], interval: number) => + createEsParams({ + body: { + size: 0, + query: { + bool: { + filter: [ + SUMMARY_FILTER, + EXCLUDE_RUN_ONCE_FILTER, + { + terms: { + 'observer.name': locationIds, }, - ], - }, - }, - { - bool: { - must_not: { - exists: { - field: 'run_once', + }, + { + term: { + config_id: configId, }, }, - }, - }, - { - terms: { - 'observer.name': locationIds, - }, - }, - { - term: { - config_id: configId, - }, - }, - { - range: { - '@timestamp': { - gte: `now-${interval}m`, - lte: 'now', + { + range: { + '@timestamp': { + gte: `now-${interval}m`, + lte: 'now', + }, + }, }, - }, + ], }, - ], - }, - }, - aggs: { - byId: { - terms: { - field: 'config_id', }, aggs: { - byLocation: { + byId: { terms: { - field: 'observer.name', + field: 'config_id', }, aggs: { - last_50: { - histogram: { - field: '@timestamp', - interval: interval * 1000, - min_doc_count: 1, + byLocation: { + terms: { + field: 'observer.name', }, aggs: { - max: { - avg: { + last50: { + histogram: { + field: '@timestamp', + interval: interval * 1000, + min_doc_count: 1, + }, + aggs: { + max: { + avg: { + field: 'monitor.duration.us', + }, + }, + }, + }, + stats: { + stats: { field: 'monitor.duration.us', }, }, - }, - }, - stats: { - stats: { - field: 'monitor.duration.us', - }, - }, - median: { - percentiles: { - field: 'monitor.duration.us', - percents: [50], + median: { + percentiles: { + field: 'monitor.duration.us', + percents: [50], + }, + }, }, }, }, }, }, + _source: false, + sort: [ + { + '@timestamp': 'desc', + }, + ], + fields: ['monitor.duration.us'], }, - }, - _source: false, - sort: [ - { - '@timestamp': 'desc', - }, - ], - fields: ['monitor.duration.us'], -}); + }); + +export type TrendsQuery = ReturnType; diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/overview_trends.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/overview_trends.ts index 0c0361ee0b27b..80fdae3eddecc 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/overview_trends.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/overview_trends.ts @@ -8,35 +8,9 @@ import { ObjectType, schema } from '@kbn/config-schema'; import { SYNTHETICS_API_URLS } from '../../../common/constants'; import { TrendRequest } from '../../../common/types'; -import { getFetchTrendsQuery } from './fetch_trends'; +import { getFetchTrendsQuery, TrendsQuery } from './fetch_trends'; import { SyntheticsRestApiRouteFactory } from '../types'; -interface TrendAggs { - aggregations: { - byId: { - buckets: Array<{ - key: string; - byLocation: { - buckets: Array<{ - key: string; - last_50: { - buckets: Array<{ - max: { value: number }; - }>; - }; - stats: {}; - median: { - values: { - '50.0': number; - }; - }; - }>; - }; - }>; - }; - }; -} - export const getIntervalForCheckCount = (schedule: string, numChecks = 50) => Number(schedule) * numChecks; @@ -55,6 +29,7 @@ export const createOverviewTrendsRoute: SyntheticsRestApiRouteFactory = () => ({ handler: async (routeContext) => { const esClient = routeContext.syntheticsEsClient; const body = routeContext.request.body as TrendRequest[]; + const configs = body.reduce( ( acc: Record, @@ -70,26 +45,23 @@ export const createOverviewTrendsRoute: SyntheticsRestApiRouteFactory = () => ({ {} ); - const requests = Object.keys(configs).map((key) => - getFetchTrendsQuery(key, configs[key].locations, configs[key].interval) + const requests = Object.keys(configs).map( + (key) => getFetchTrendsQuery(key, configs[key].locations, configs[key].interval).body ); - const results = await esClient.msearch(requests); + const results = await esClient.msearch(requests); let main = {}; - for (const res of results.responses as unknown as TrendAggs[]) { - const aggregations = res.aggregations; - aggregations.byId.buckets.map(({ key, byLocation }) => { + for (const res of results.responses) { + res.aggregations?.byId.buckets.map(({ key, byLocation }) => { const ret: Record = {}; for (const location of byLocation.buckets) { - ret[key + location.key] = { + ret[String(key) + String(location.key)] = { configId: key, locationId: location.key, - data: location.last_50.buckets.map( - (durationBucket: { max: { value: number } }, x: number) => ({ - x, - y: durationBucket.max.value, - }) - ), + data: location.last50.buckets.map((durationBucket, x) => ({ + x, + y: durationBucket.max.value, + })), ...location.stats, median: location.median.values['50.0'], }; @@ -97,6 +69,6 @@ export const createOverviewTrendsRoute: SyntheticsRestApiRouteFactory = () => ({ main = { ...main, ...ret }; }); } - return routeContext.response.ok({ body: main }); + return main; }, }); From 8613f37896ef89e4497a4924b0f730c5de035d56 Mon Sep 17 00:00:00 2001 From: shahzad31 Date: Wed, 4 Sep 2024 14:25:20 +0200 Subject: [PATCH 37/39] just one more type --- .../server/routes/overview_trends/overview_trends.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/overview_trends.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/overview_trends.ts index 80fdae3eddecc..e3d7589160c02 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/overview_trends.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/overview_trends/overview_trends.ts @@ -7,7 +7,7 @@ import { ObjectType, schema } from '@kbn/config-schema'; import { SYNTHETICS_API_URLS } from '../../../common/constants'; -import { TrendRequest } from '../../../common/types'; +import { TrendRequest, TrendTable } from '../../../common/types'; import { getFetchTrendsQuery, TrendsQuery } from './fetch_trends'; import { SyntheticsRestApiRouteFactory } from '../types'; @@ -26,7 +26,7 @@ export const createOverviewTrendsRoute: SyntheticsRestApiRouteFactory = () => ({ }) ) as unknown as ObjectType, }, - handler: async (routeContext) => { + handler: async (routeContext): Promise => { const esClient = routeContext.syntheticsEsClient; const body = routeContext.request.body as TrendRequest[]; From 20551af61c33c68ec798651360d4ce92ecd32f5e Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Wed, 4 Sep 2024 10:38:49 -0400 Subject: [PATCH 38/39] Make overview footer only display when user scrolls to end of list. --- .../overview/overview/overview_grid.tsx | 100 ++++++++++-------- 1 file changed, 53 insertions(+), 47 deletions(-) diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx index 97ad610935e66..6633a95c26771 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx @@ -72,6 +72,7 @@ export const OverviewGrid = memo(() => { const [page, setPage] = useState(1); const [maxItem, setMaxItem] = useState(0); + const [currentIndex, setCurrentIndex] = useState(0); const dispatch = useDispatch(); @@ -189,27 +190,30 @@ export const OverviewGrid = memo(() => { index: listIndex, style, data: listData, - }: React.PropsWithChildren>) => ( - - {listData[listIndex].map((_, idx) => ( - - - - ))} - {listData[listIndex].length % ROW_COUNT !== 0 && - // Adds empty items to fill out row - Array.from({ - length: ROW_COUNT - listData[listIndex].length, - }).map((_, idx) => )} - - )} + }: React.PropsWithChildren>) => { + setCurrentIndex(listIndex); + return ( + + {listData[listIndex].map((_, idx) => ( + + + + ))} + {listData[listIndex].length % ROW_COUNT !== 0 && + // Adds empty items to fill out row + Array.from({ + length: ROW_COUNT - listData[listIndex].length, + }).map((_, idx) => )} + + ); + }} )} @@ -227,35 +231,37 @@ export const OverviewGrid = memo(() => { )}
- {groupField === 'none' && ( - <> - - - {monitorsSortedByStatus.length === monitors.length && ( - - {SHOWING_ALL_MONITORS_LABEL} - - )} - {monitorsSortedByStatus.length === monitors.length && - monitorsSortedByStatus.length > perPage && ( + {groupField === 'none' && + // display this footer when user scrolls to end of list + currentIndex * ROW_COUNT + ROW_COUNT >= monitorsSortedByStatus.length && ( + <> + + + {monitorsSortedByStatus.length === monitors.length && ( - { - window.scrollTo({ top: 0, left: 0, behavior: 'smooth' }); - listRef.current?.scrollToItem(0); - }} - iconType="sortUp" - iconSide="right" - size="xs" - > - {SCROLL_TO_TOP_LABEL} - + {SHOWING_ALL_MONITORS_LABEL} )} - - - )} + {monitorsSortedByStatus.length === monitors.length && + monitorsSortedByStatus.length > perPage && ( + + { + window.scrollTo({ top: 0, left: 0, behavior: 'smooth' }); + listRef.current?.scrollToItem(0); + }} + iconType="sortUp" + iconSide="right" + size="xs" + > + {SCROLL_TO_TOP_LABEL} + + + )} + + + )} {flyoutConfig?.configId && flyoutConfig?.location && ( Date: Wed, 4 Sep 2024 10:48:47 -0400 Subject: [PATCH 39/39] Only render footer when we want it to show. --- .../components/monitors_page/overview/overview/overview_grid.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx index 6633a95c26771..76e52685736b1 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx @@ -232,6 +232,7 @@ export const OverviewGrid = memo(() => {
{groupField === 'none' && + loaded && // display this footer when user scrolls to end of list currentIndex * ROW_COUNT + ROW_COUNT >= monitorsSortedByStatus.length && ( <>