From 572af171522122b99b398291efa7a34a1b91307f Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 17 Mar 2021 09:29:24 -0400 Subject: [PATCH] [Uptime] Synthetic check steps list view (#90978) (#94716) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Shahzad --- .../translations/translations/ja-JP.json | 6 - .../translations/translations/zh-CN.json | 6 - x-pack/plugins/uptime/common/constants/ui.ts | 2 + .../uptime/common/runtime_types/ping/ping.ts | 2 + .../components/common/step_detail_link.tsx | 12 +- .../columns/ping_timestamp/nav_buttons.tsx | 8 +- .../ping_timestamp/ping_timestamp.test.tsx | 12 +- .../columns/ping_timestamp/ping_timestamp.tsx | 45 ++- .../step_image_caption.test.tsx | 5 +- .../ping_timestamp/step_image_caption.tsx | 33 ++- .../ping_timestamp/step_image_popover.tsx | 3 +- .../monitor/ping_list/expanded_row.test.tsx | 37 ++- .../monitor/ping_list/expanded_row.tsx | 19 -- .../monitor/ping_list/ping_list.tsx | 57 +++- .../synthetics/browser_expanded_row.test.tsx | 203 -------------- .../synthetics/browser_expanded_row.tsx | 65 ----- .../synthetics/executed_journey.test.tsx | 265 ------------------ .../monitor/synthetics/executed_journey.tsx | 88 ------ .../monitor/synthetics/executed_step.tsx | 110 -------- .../monitor/synthetics/status_badge.test.tsx | 42 --- .../step_detail/step_detail_container.tsx | 2 +- .../step_detail/use_monitor_breadcrumb.tsx | 33 ++- .../use_monitor_breadcrumbs.test.tsx | 84 +++++- .../columns/monitor_status_column.tsx | 2 +- .../step_expanded_row/screenshot_link.tsx | 47 ++++ .../step_expanded_row/step_screenshots.tsx | 86 ++++++ .../synthetics/check_steps/step_image.tsx | 28 ++ .../synthetics/check_steps/step_list.test.tsx | 144 ++++++++++ .../synthetics/check_steps/steps_list.tsx | 175 ++++++++++++ .../synthetics/check_steps/use_check_steps.ts | 29 ++ .../check_steps/use_expanded_row.test.tsx | 152 ++++++++++ .../check_steps/use_expanded_row.tsx | 88 ++++++ .../synthetics/code_block_accordion.tsx | 4 +- .../synthetics/console_event.test.tsx | 0 .../synthetics/console_event.tsx | 4 +- .../console_output_event_list.test.tsx | 0 .../synthetics/console_output_event_list.tsx | 4 +- .../synthetics/empty_journey.test.tsx | 0 .../synthetics/empty_journey.tsx | 0 .../synthetics/executed_step.test.tsx | 53 ++-- .../components/synthetics/executed_step.tsx | 118 ++++++++ .../synthetics/status_badge.test.tsx | 33 +++ .../{monitor => }/synthetics/status_badge.tsx | 20 +- .../step_screenshot_display.test.tsx | 2 +- .../synthetics/step_screenshot_display.tsx | 89 ++---- .../components/synthetics/translations.ts | 20 ++ .../uptime/public/hooks/use_telemetry.ts | 1 + x-pack/plugins/uptime/public/pages/index.ts | 2 +- .../pages/synthetics/checks_navigation.tsx | 60 ++++ .../{ => synthetics}/step_detail_page.tsx | 6 +- .../pages/synthetics/synthetics_checks.tsx | 44 +++ x-pack/plugins/uptime/public/routes.tsx | 9 + .../uptime/public/state/api/journey.ts | 17 ++ .../lib/requests/get_journey_details.ts | 6 +- .../lib/requests/get_journey_screenshot.ts | 4 +- .../lib/requests/get_journey_steps.test.ts | 4 +- .../server/lib/requests/get_journey_steps.ts | 2 +- .../lib/requests/get_last_successful_step.ts | 77 +++++ .../uptime/server/lib/requests/index.ts | 2 + .../plugins/uptime/server/rest_api/index.ts | 2 + .../rest_api/pings/journey_screenshots.ts | 5 +- .../uptime/server/rest_api/pings/journeys.ts | 23 +- .../synthetics/last_successful_step.ts | 33 +++ 63 files changed, 1514 insertions(+), 1020 deletions(-) delete mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/browser_expanded_row.test.tsx delete mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/browser_expanded_row.tsx delete mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/executed_journey.test.tsx delete mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/executed_journey.tsx delete mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.tsx delete mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/status_badge.test.tsx create mode 100644 x-pack/plugins/uptime/public/components/synthetics/check_steps/step_expanded_row/screenshot_link.tsx create mode 100644 x-pack/plugins/uptime/public/components/synthetics/check_steps/step_expanded_row/step_screenshots.tsx create mode 100644 x-pack/plugins/uptime/public/components/synthetics/check_steps/step_image.tsx create mode 100644 x-pack/plugins/uptime/public/components/synthetics/check_steps/step_list.test.tsx create mode 100644 x-pack/plugins/uptime/public/components/synthetics/check_steps/steps_list.tsx create mode 100644 x-pack/plugins/uptime/public/components/synthetics/check_steps/use_check_steps.ts create mode 100644 x-pack/plugins/uptime/public/components/synthetics/check_steps/use_expanded_row.test.tsx create mode 100644 x-pack/plugins/uptime/public/components/synthetics/check_steps/use_expanded_row.tsx rename x-pack/plugins/uptime/public/components/{monitor => }/synthetics/code_block_accordion.tsx (87%) rename x-pack/plugins/uptime/public/components/{monitor => }/synthetics/console_event.test.tsx (100%) rename x-pack/plugins/uptime/public/components/{monitor => }/synthetics/console_event.tsx (89%) rename x-pack/plugins/uptime/public/components/{monitor => }/synthetics/console_output_event_list.test.tsx (100%) rename x-pack/plugins/uptime/public/components/{monitor => }/synthetics/console_output_event_list.tsx (92%) rename x-pack/plugins/uptime/public/components/{monitor => }/synthetics/empty_journey.test.tsx (100%) rename x-pack/plugins/uptime/public/components/{monitor => }/synthetics/empty_journey.tsx (100%) rename x-pack/plugins/uptime/public/components/{monitor => }/synthetics/executed_step.test.tsx (54%) create mode 100644 x-pack/plugins/uptime/public/components/synthetics/executed_step.tsx create mode 100644 x-pack/plugins/uptime/public/components/synthetics/status_badge.test.tsx rename x-pack/plugins/uptime/public/components/{monitor => }/synthetics/status_badge.tsx (66%) rename x-pack/plugins/uptime/public/components/{monitor => }/synthetics/step_screenshot_display.test.tsx (96%) rename x-pack/plugins/uptime/public/components/{monitor => }/synthetics/step_screenshot_display.tsx (52%) create mode 100644 x-pack/plugins/uptime/public/components/synthetics/translations.ts create mode 100644 x-pack/plugins/uptime/public/pages/synthetics/checks_navigation.tsx rename x-pack/plugins/uptime/public/pages/{ => synthetics}/step_detail_page.tsx (75%) create mode 100644 x-pack/plugins/uptime/public/pages/synthetics/synthetics_checks.tsx create mode 100644 x-pack/plugins/uptime/server/lib/requests/get_last_successful_step.ts create mode 100644 x-pack/plugins/uptime/server/rest_api/synthetics/last_successful_step.ts diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 9095ca8eb7997..db6fc739c1307 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -23251,12 +23251,8 @@ "xpack.uptime.synthetics.emptyJourney.message.footer": "表示する詳細情報はありません。", "xpack.uptime.synthetics.emptyJourney.message.heading": "ステップが含まれていませんでした。", "xpack.uptime.synthetics.emptyJourney.title": "ステップがありません。", - "xpack.uptime.synthetics.executedJourney.heading": "概要情報", "xpack.uptime.synthetics.executedStep.errorHeading": "エラー", - "xpack.uptime.synthetics.executedStep.scriptHeading": "スクリプトのステップ", "xpack.uptime.synthetics.executedStep.stackTrace": "スタックトレース", - "xpack.uptime.synthetics.executedStep.stepName": "{stepNumber}. {stepName}", - "xpack.uptime.synthetics.experimentalCallout.title": "実験的機能", "xpack.uptime.synthetics.imageLoadingSpinner.ariaLabel": "画像を示すアニメーションスピナーを読み込んでいます", "xpack.uptime.synthetics.journey.allFailedMessage": "{total}ステップ - すべて失敗またはスキップされました", "xpack.uptime.synthetics.journey.allSucceededMessage": "{total}ステップ - すべて成功しました", @@ -23267,8 +23263,6 @@ "xpack.uptime.synthetics.screenshot.noImageMessage": "画像がありません", "xpack.uptime.synthetics.screenshotDisplay.altText": "名前「{stepName}」のステップのスクリーンショット", "xpack.uptime.synthetics.screenshotDisplay.altTextWithoutName": "スクリーンショット", - "xpack.uptime.synthetics.screenshotDisplay.thumbnailAltText": "名前「{stepName}」のステップのサムネイルスクリーンショット", - "xpack.uptime.synthetics.screenshotDisplay.thumbnailAltTextWithoutName": "サムネイルスクリーンショット", "xpack.uptime.synthetics.statusBadge.failedMessage": "失敗", "xpack.uptime.synthetics.statusBadge.skippedMessage": "スキップ", "xpack.uptime.synthetics.statusBadge.succeededMessage": "成功", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 3f924631e835e..75b318c669674 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -23607,12 +23607,8 @@ "xpack.uptime.synthetics.emptyJourney.message.footer": "没有更多可显示的信息。", "xpack.uptime.synthetics.emptyJourney.message.heading": "此过程不包含任何步骤。", "xpack.uptime.synthetics.emptyJourney.title": "没有此过程的任何步骤", - "xpack.uptime.synthetics.executedJourney.heading": "摘要信息", "xpack.uptime.synthetics.executedStep.errorHeading": "错误", - "xpack.uptime.synthetics.executedStep.scriptHeading": "步骤脚本", "xpack.uptime.synthetics.executedStep.stackTrace": "堆栈跟踪", - "xpack.uptime.synthetics.executedStep.stepName": "{stepNumber}:{stepName}", - "xpack.uptime.synthetics.experimentalCallout.title": "实验功能", "xpack.uptime.synthetics.imageLoadingSpinner.ariaLabel": "表示图像正在加载的动画旋转图标", "xpack.uptime.synthetics.journey.allFailedMessage": "{total} 个步骤 - 全部失败或跳过", "xpack.uptime.synthetics.journey.allSucceededMessage": "{total} 个步骤 - 全部成功", @@ -23623,8 +23619,6 @@ "xpack.uptime.synthetics.screenshot.noImageMessage": "没有可用图像", "xpack.uptime.synthetics.screenshotDisplay.altText": "名称为“{stepName}”的步骤的屏幕截图", "xpack.uptime.synthetics.screenshotDisplay.altTextWithoutName": "屏幕截图", - "xpack.uptime.synthetics.screenshotDisplay.thumbnailAltText": "名称为“{stepName}”的步骤的缩略屏幕截图", - "xpack.uptime.synthetics.screenshotDisplay.thumbnailAltTextWithoutName": "缩略屏幕截图", "xpack.uptime.synthetics.statusBadge.failedMessage": "失败", "xpack.uptime.synthetics.statusBadge.skippedMessage": "已跳过", "xpack.uptime.synthetics.statusBadge.succeededMessage": "成功", diff --git a/x-pack/plugins/uptime/common/constants/ui.ts b/x-pack/plugins/uptime/common/constants/ui.ts index 880bc0f92ddf6..dcaf4bb310ad7 100644 --- a/x-pack/plugins/uptime/common/constants/ui.ts +++ b/x-pack/plugins/uptime/common/constants/ui.ts @@ -15,6 +15,8 @@ export const CERTIFICATES_ROUTE = '/certificates'; export const STEP_DETAIL_ROUTE = '/journey/:checkGroupId/step/:stepIndex'; +export const SYNTHETIC_CHECK_STEPS_ROUTE = '/journey/:checkGroupId/steps'; + export enum STATUS { UP = 'up', DOWN = 'down', diff --git a/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts b/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts index 8991d52f6a920..77b9473f2912e 100644 --- a/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts +++ b/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts @@ -216,6 +216,7 @@ export const PingType = t.intersection([ type: t.string, url: t.string, end: t.number, + text: t.string, }), }), tags: t.array(t.string), @@ -251,6 +252,7 @@ export const SyntheticsJourneyApiResponseType = t.intersection([ t.intersection([ t.type({ timestamp: t.string, + journey: PingType, }), t.partial({ next: t.type({ diff --git a/x-pack/plugins/uptime/public/components/common/step_detail_link.tsx b/x-pack/plugins/uptime/public/components/common/step_detail_link.tsx index 313dd18e67c11..fa6d0b4c3f8bb 100644 --- a/x-pack/plugins/uptime/public/components/common/step_detail_link.tsx +++ b/x-pack/plugins/uptime/public/components/common/step_detail_link.tsx @@ -6,7 +6,7 @@ */ import React, { FC } from 'react'; -import { ReactRouterEuiButton } from './react_router_helpers'; +import { ReactRouterEuiButtonEmpty } from './react_router_helpers'; interface StepDetailLinkProps { /** @@ -23,14 +23,8 @@ export const StepDetailLink: FC = ({ children, checkGroupId const to = `/journey/${checkGroupId}/step/${stepIndex}`; return ( - + {children} - + ); }; diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/nav_buttons.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/nav_buttons.tsx index 390a133b1819b..3b0aad721be8a 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/nav_buttons.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/nav_buttons.tsx @@ -6,7 +6,7 @@ */ import { EuiButtonIcon, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; -import React from 'react'; +import React, { MouseEvent } from 'react'; import { nextAriaLabel, prevAriaLabel } from './translations'; export interface NavButtonsProps { @@ -34,8 +34,9 @@ export const NavButtons: React.FC = ({ disabled={stepNumber === 1} color="subdued" size="s" - onClick={() => { + onClick={(evt: MouseEvent) => { setStepNumber(stepNumber - 1); + evt.stopPropagation(); }} iconType="arrowLeft" aria-label={prevAriaLabel} @@ -46,8 +47,9 @@ export const NavButtons: React.FC = ({ disabled={stepNumber === maxSteps} color="subdued" size="s" - onClick={() => { + onClick={(evt: MouseEvent) => { setStepNumber(stepNumber + 1); + evt.stopPropagation(); }} iconType="arrowRight" aria-label={nextAriaLabel} diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.test.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.test.tsx index 2a1989cafa434..d628b2d8388f9 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.test.tsx @@ -12,6 +12,8 @@ import { mockReduxHooks } from '../../../../../lib/helper/test_helpers'; import { render } from '../../../../../lib/helper/rtl_helpers'; import { Ping } from '../../../../../../common/runtime_types/ping'; import * as observabilityPublic from '../../../../../../../observability/public'; +import { getShortTimeStamp } from '../../../../overview/monitor_list/columns/monitor_status_column'; +import moment from 'moment'; mockReduxHooks(); @@ -68,7 +70,7 @@ describe('Ping Timestamp component', () => { .spyOn(observabilityPublic, 'useFetcher') .mockReturnValue({ status: fetchStatus, data: null, refetch: () => null }); const { getByTestId } = render( - + ); expect(getByTestId('pingTimestampSpinner')).toBeInTheDocument(); } @@ -79,7 +81,7 @@ describe('Ping Timestamp component', () => { .spyOn(observabilityPublic, 'useFetcher') .mockReturnValue({ status: FETCH_STATUS.SUCCESS, data: null, refetch: () => null }); const { getByTestId } = render( - + ); expect(getByTestId('pingTimestampNoImageAvailable')).toBeInTheDocument(); }); @@ -91,7 +93,9 @@ describe('Ping Timestamp component', () => { data: { src }, refetch: () => null, }); - const { container } = render(); + const { container } = render( + + ); expect(container.querySelector('img')?.src).toBe(src); }); @@ -103,7 +107,7 @@ describe('Ping Timestamp component', () => { refetch: () => null, }); const { getByAltText, getAllByText, queryByAltText } = render( - + ); const caption = getAllByText('Nov 26, 2020 10:28:56 AM'); fireEvent.mouseEnter(caption[0]); diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.tsx index cfb92dd31190e..16553e9de8604 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.tsx @@ -8,18 +8,15 @@ import React, { useContext, useEffect, useState } from 'react'; import useIntersection from 'react-use/lib/useIntersection'; import styled from 'styled-components'; -import moment from 'moment'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { Ping } from '../../../../../../common/runtime_types/ping'; import { useFetcher, FETCH_STATUS } from '../../../../../../../observability/public'; import { getJourneyScreenshot } from '../../../../../state/api/journey'; import { UptimeSettingsContext } from '../../../../../contexts'; -import { NavButtons } from './nav_buttons'; import { NoImageDisplay } from './no_image_display'; import { StepImageCaption } from './step_image_caption'; import { StepImagePopover } from './step_image_popover'; import { formatCaptionContent } from './translations'; -import { getShortTimeStamp } from '../../../../overview/monitor_list/columns/monitor_status_column'; const StepDiv = styled.div` figure.euiImage { @@ -27,25 +24,16 @@ const StepDiv = styled.div` display: none; } } - - position: relative; - div.stepArrows { - display: none; - } - :hover { - div.stepArrows { - display: flex; - } - } `; interface Props { - timestamp: string; + label?: string; ping: Ping; + initialStepNo?: number; } -export const PingTimestamp = ({ timestamp, ping }: Props) => { - const [stepNumber, setStepNumber] = useState(1); +export const PingTimestamp = ({ label, ping, initialStepNo = 1 }: Props) => { + const [stepNumber, setStepNumber] = useState(initialStepNo); const [isImagePopoverOpen, setIsImagePopoverOpen] = useState(false); const [stepImages, setStepImages] = useState([]); @@ -77,6 +65,8 @@ export const PingTimestamp = ({ timestamp, ping }: Props) => { const captionContent = formatCaptionContent(stepNumber, data?.maxSteps); + const [numberOfCaptions, setNumberOfCaptions] = useState(0); + const ImageCaption = ( { maxSteps={data?.maxSteps} setStepNumber={setStepNumber} stepNumber={stepNumber} - timestamp={timestamp} isLoading={status === FETCH_STATUS.LOADING || status === FETCH_STATUS.PENDING} + label={label} + onVisible={(val) => setNumberOfCaptions((prevVal) => (val ? prevVal + 1 : prevVal - 1))} /> ); + useEffect(() => { + // This is a hack to get state if image is in full screen, we should refactor + // it once eui image exposes it's full screen state + // we are checking if number of captions are 2, that means + // image is in full screen mode since caption is also rendered on + // full screen image + // we dont want to change image displayed in thumbnail + if (numberOfCaptions === 1 && stepNumber !== initialStepNo) { + setStepNumber(initialStepNo); + } + }, [numberOfCaptions, initialStepNo, stepNumber]); + return ( @@ -111,16 +114,10 @@ export const PingTimestamp = ({ timestamp, ping }: Props) => { isPending={status === FETCH_STATUS.PENDING} /> )} - - {getShortTimeStamp(moment(timestamp))} + {label} ); diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.test.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.test.tsx index a33e587093279..5c2c4d3669e79 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.test.tsx @@ -9,6 +9,8 @@ import { fireEvent, waitFor } from '@testing-library/react'; import React from 'react'; import { render } from '../../../../../lib/helper/rtl_helpers'; import { StepImageCaption, StepImageCaptionProps } from './step_image_caption'; +import { getShortTimeStamp } from '../../../../overview/monitor_list/columns/monitor_status_column'; +import moment from 'moment'; describe('StepImageCaption', () => { let defaultProps: StepImageCaptionProps; @@ -20,7 +22,8 @@ describe('StepImageCaption', () => { maxSteps: 3, setStepNumber: jest.fn(), stepNumber: 2, - timestamp: '2020-11-26T15:28:56.896Z', + label: getShortTimeStamp(moment('2020-11-26T15:28:56.896Z')), + onVisible: jest.fn(), isLoading: false, }; }); diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.tsx index fe9709a02b684..80d41ccc23dc8 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.tsx @@ -5,11 +5,9 @@ * 2.0. */ +import React, { MouseEvent, useEffect } from 'react'; import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -import React from 'react'; -import moment from 'moment'; import { nextAriaLabel, prevAriaLabel } from './translations'; -import { getShortTimeStamp } from '../../../../overview/monitor_list/columns/monitor_status_column'; import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; export interface StepImageCaptionProps { @@ -18,7 +16,8 @@ export interface StepImageCaptionProps { maxSteps?: number; setStepNumber: React.Dispatch>; stepNumber: number; - timestamp: string; + label?: string; + onVisible: (val: boolean) => void; isLoading: boolean; } @@ -35,19 +34,34 @@ export const StepImageCaption: React.FC = ({ maxSteps, setStepNumber, stepNumber, - timestamp, isLoading, + label, + onVisible, }) => { + useEffect(() => { + onVisible(true); + return () => { + onVisible(false); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + return ( - + { + // we don't want this to be captured by row click which leads to step list page + evt.stopPropagation(); + }} + >
{imgSrc && ( { + onClick={(evt: MouseEvent) => { setStepNumber(stepNumber - 1); + evt.preventDefault(); }} iconType="arrowLeft" aria-label={prevAriaLabel} @@ -62,8 +76,9 @@ export const StepImageCaption: React.FC = ({ { + onClick={(evt: MouseEvent) => { setStepNumber(stepNumber + 1); + evt.stopPropagation(); }} iconType="arrowRight" iconSide="right" @@ -75,7 +90,7 @@ export const StepImageCaption: React.FC = ({ )} - {getShortTimeStamp(moment(timestamp))} + {label}
); diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_popover.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_popover.tsx index 4fc8db515a5d6..d3dce3a2505b2 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_popover.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_popover.tsx @@ -38,7 +38,7 @@ export const StepImagePopover: React.FC = ({ isImagePopoverOpen, }) => ( = ({ /> } isOpen={isImagePopoverOpen} + closePopover={() => {}} > { > - - - + color="primary" + > + + The Title", + "hash": "testhash", + } + } + /> + + + , + "title": "Response Body", + }, + ] + } + /> + `); diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/expanded_row.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/expanded_row.tsx index 2599b8ed9fdca..df0d273d3bc3a 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/expanded_row.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/expanded_row.tsx @@ -21,7 +21,6 @@ import { i18n } from '@kbn/i18n'; import { Ping, HttpResponseBody } from '../../../../common/runtime_types'; import { DocLinkForBody } from './doc_link_body'; import { PingRedirects } from './ping_redirects'; -import { BrowserExpandedRow } from '../synthetics/browser_expanded_row'; import { PingHeaders } from './headers'; interface Props { @@ -57,24 +56,6 @@ const BodyExcerpt = ({ content }: { content: string }) => export const PingListExpandedRowComponent = ({ ping }: Props) => { const listItems = []; - if (ping.monitor.type === 'browser') { - return ( - - - - - - - - - ); - } - // Show the error block if (ping.error) { listItems.push({ diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/ping_list.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/ping_list.tsx index 18bc5f5ec3ecb..65644ce493906 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/ping_list.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/ping_list.tsx @@ -7,8 +7,10 @@ import { EuiBasicTable, EuiPanel, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { useCallback, useState, useEffect } from 'react'; +import React, { useCallback, useState, useEffect, MouseEvent } from 'react'; import styled from 'styled-components'; +import { useHistory } from 'react-router-dom'; +import moment from 'moment'; import { useDispatch } from 'react-redux'; import { Ping } from '../../../../common/runtime_types'; import { convertMicrosecondsToMilliseconds as microsToMillis } from '../../../lib/helper'; @@ -27,6 +29,7 @@ import { FailedStep } from './columns/failed_step'; import { usePingsList } from './use_pings'; import { PingListHeader } from './ping_list_header'; import { clearPings } from '../../../state/actions'; +import { getShortTimeStamp } from '../../overview/monitor_list/columns/monitor_status_column'; export const SpanWithMargin = styled.span` margin-right: 16px; @@ -69,6 +72,8 @@ export const PingList = () => { const dispatch = useDispatch(); + const history = useHistory(); + const pruneJourneysCallback = useCallback( (checkGroups: string[]) => dispatch(pruneJourneyState(checkGroups)), [dispatch] @@ -140,7 +145,7 @@ export const PingList = () => { field: 'timestamp', name: TIMESTAMP_LABEL, render: (timestamp: string, item: Ping) => ( - + ), }, ] @@ -197,20 +202,43 @@ export const PingList = () => { }, ] : []), - { - align: 'right', - width: '24px', - isExpander: true, - render: (item: Ping) => ( - - ), - }, + ...(monitorType !== MONITOR_TYPES.BROWSER + ? [ + { + align: 'right', + width: '24px', + isExpander: true, + render: (item: Ping) => ( + + ), + }, + ] + : []), ]; + const getRowProps = (item: Ping) => { + if (monitorType !== MONITOR_TYPES.BROWSER) { + return {}; + } + const { monitor } = item; + return { + height: '85px', + 'data-test-subj': `row-${monitor.check_group}`, + onClick: (evt: MouseEvent) => { + const targetElem = evt.target as HTMLElement; + + // we dont want to capture image click event + if (targetElem.tagName !== 'IMG' && targetElem.tagName !== 'path') { + history.push(`/journey/${monitor.check_group}/steps`); + } + }, + }; + }; + const pagination: Pagination = { initialPageSize: DEFAULT_PAGE_SIZE, pageIndex, @@ -247,6 +275,7 @@ export const PingList = () => { setPageIndex(criteria.page!.index); }} tableLayout={'auto'} + rowProps={getRowProps} /> ); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/browser_expanded_row.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/browser_expanded_row.test.tsx deleted file mode 100644 index 396d51e3002b2..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/browser_expanded_row.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 { shallowWithIntl } from '@kbn/test/jest'; -import React from 'react'; -import { BrowserExpandedRowComponent } from './browser_expanded_row'; -import { Ping } from '../../../../common/runtime_types'; - -describe('BrowserExpandedRowComponent', () => { - let defStep: Ping; - beforeEach(() => { - defStep = { - docId: 'doc-id', - timestamp: '123', - monitor: { - duration: { - us: 100, - }, - id: 'mon-id', - status: 'up', - type: 'browser', - }, - }; - }); - - it('returns empty step state when no journey', () => { - expect(shallowWithIntl()).toMatchInlineSnapshot( - `` - ); - }); - - it('returns empty step state when journey has no steps', () => { - expect( - shallowWithIntl( - - ) - ).toMatchInlineSnapshot(``); - }); - - it('displays loading spinner when loading', () => { - expect( - shallowWithIntl( - - ) - ).toMatchInlineSnapshot(` -
- -
- `); - }); - - it('renders executed journey when step/end is present', () => { - expect( - shallowWithIntl( - - ) - ).toMatchInlineSnapshot(` - - `); - }); - - it('handles case where synth type is somehow missing', () => { - expect( - shallowWithIntl( - - ) - ).toMatchInlineSnapshot(`""`); - }); - - it('renders console output step list when only console steps are present', () => { - expect( - shallowWithIntl( - - ) - ).toMatchInlineSnapshot(` - - `); - }); - - it('renders null when only unsupported steps are present', () => { - expect( - shallowWithIntl( - - ) - ).toMatchInlineSnapshot(`""`); - }); -}); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/browser_expanded_row.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/browser_expanded_row.tsx deleted file mode 100644 index 2ceaa2d1b68ef..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/browser_expanded_row.tsx +++ /dev/null @@ -1,65 +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 { EuiLoadingSpinner } from '@elastic/eui'; -import React, { useEffect, FC } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { Ping } from '../../../../common/runtime_types'; -import { getJourneySteps } from '../../../state/actions/journey'; -import { JourneyState } from '../../../state/reducers/journey'; -import { journeySelector } from '../../../state/selectors'; -import { EmptyJourney } from './empty_journey'; -import { ExecutedJourney } from './executed_journey'; -import { ConsoleOutputEventList } from './console_output_event_list'; - -interface BrowserExpandedRowProps { - checkGroup?: string; -} - -export const BrowserExpandedRow: React.FC = ({ checkGroup }) => { - const dispatch = useDispatch(); - useEffect(() => { - if (checkGroup) { - dispatch(getJourneySteps({ checkGroup })); - } - }, [dispatch, checkGroup]); - - const journeys = useSelector(journeySelector); - const journey = journeys[checkGroup ?? '']; - - return ; -}; - -type ComponentProps = BrowserExpandedRowProps & { - journey?: JourneyState; -}; - -const stepEnd = (step: Ping) => step.synthetics?.type === 'step/end'; -const stepConsole = (step: Ping) => - ['stderr', 'cmd/status'].indexOf(step.synthetics?.type ?? '') !== -1; - -export const BrowserExpandedRowComponent: FC = ({ checkGroup, journey }) => { - if (!!journey && journey.loading) { - return ( -
- -
- ); - } - - if (!journey || journey.steps.length === 0) { - return ; - } - - if (journey.steps.some(stepEnd)) return ; - - if (journey.steps.some(stepConsole)) return ; - - // TODO: should not happen, this means that the journey has no step/end and no console logs, but some other steps; filmstrip, screenshot, etc. - // we should probably create an error prompt letting the user know this step is not supported yet - return null; -}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_journey.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_journey.test.tsx deleted file mode 100644 index 2fbc19d245826..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_journey.test.tsx +++ /dev/null @@ -1,265 +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 { shallowWithIntl } from '@kbn/test/jest'; -import React from 'react'; -import { ExecutedJourney } from './executed_journey'; -import { Ping } from '../../../../common/runtime_types'; - -const MONITOR_BOILERPLATE = { - id: 'MON_ID', - duration: { - us: 10, - }, - status: 'down', - type: 'browser', -}; - -describe('ExecutedJourney component', () => { - let steps: Ping[]; - - beforeEach(() => { - steps = [ - { - docId: '1', - timestamp: '123', - monitor: MONITOR_BOILERPLATE, - synthetics: { - payload: { - status: 'failed', - }, - type: 'step/end', - }, - }, - { - docId: '2', - timestamp: '124', - monitor: MONITOR_BOILERPLATE, - synthetics: { - payload: { - status: 'failed', - }, - type: 'step/end', - }, - }, - ]; - }); - - it('creates expected message for all failed', () => { - const wrapper = shallowWithIntl( - - ); - expect(wrapper.find('EuiText')).toMatchInlineSnapshot(` - -

- -

-

- 2 Steps - all failed or skipped -

-
- `); - }); - - it('creates expected message for all succeeded', () => { - steps[0].synthetics!.payload!.status = 'succeeded'; - steps[1].synthetics!.payload!.status = 'succeeded'; - const wrapper = shallowWithIntl( - - ); - expect(wrapper.find('EuiText')).toMatchInlineSnapshot(` - -

- -

-

- 2 Steps - all succeeded -

-
- `); - }); - - it('creates appropriate message for mixed results', () => { - steps[0].synthetics!.payload!.status = 'succeeded'; - const wrapper = shallowWithIntl( - - ); - expect(wrapper.find('EuiText')).toMatchInlineSnapshot(` - -

- -

-

- 2 Steps - 1 succeeded -

-
- `); - }); - - it('tallies skipped steps', () => { - steps[0].synthetics!.payload!.status = 'succeeded'; - steps[1].synthetics!.payload!.status = 'skipped'; - const wrapper = shallowWithIntl( - - ); - expect(wrapper.find('EuiText')).toMatchInlineSnapshot(` - -

- -

-

- 2 Steps - 1 succeeded -

-
- `); - }); - - it('uses appropriate count when non-step/end steps are included', () => { - steps[0].synthetics!.payload!.status = 'succeeded'; - steps.push({ - docId: '3', - timestamp: '125', - monitor: MONITOR_BOILERPLATE, - synthetics: { - type: 'stderr', - error: { - message: `there was an error, that's all we know`, - stack: 'your.error.happened.here', - }, - }, - }); - const wrapper = shallowWithIntl( - - ); - expect(wrapper.find('EuiText')).toMatchInlineSnapshot(` - -

- -

-

- 2 Steps - 1 succeeded -

-
- `); - }); - - it('renders a component per step', () => { - expect( - shallowWithIntl( - - ).find('EuiFlexGroup') - ).toMatchInlineSnapshot(` - - - - - - `); - }); -}); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_journey.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_journey.tsx deleted file mode 100644 index 1ded7f065d8ab..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_journey.tsx +++ /dev/null @@ -1,88 +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 { EuiFlexGroup, EuiSpacer, EuiText } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React, { FC } from 'react'; -import { Ping } from '../../../../common/runtime_types'; -import { JourneyState } from '../../../state/reducers/journey'; -import { ExecutedStep } from './executed_step'; - -interface StepStatusCount { - failed: number; - skipped: number; - succeeded: number; -} - -function statusMessage(count: StepStatusCount) { - const total = count.succeeded + count.failed + count.skipped; - if (count.failed + count.skipped === total) { - return i18n.translate('xpack.uptime.synthetics.journey.allFailedMessage', { - defaultMessage: '{total} Steps - all failed or skipped', - values: { total }, - }); - } else if (count.succeeded === total) { - return i18n.translate('xpack.uptime.synthetics.journey.allSucceededMessage', { - defaultMessage: '{total} Steps - all succeeded', - values: { total }, - }); - } - return i18n.translate('xpack.uptime.synthetics.journey.partialSuccessMessage', { - defaultMessage: '{total} Steps - {succeeded} succeeded', - values: { succeeded: count.succeeded, total }, - }); -} - -function reduceStepStatus(prev: StepStatusCount, cur: Ping): StepStatusCount { - if (cur.synthetics?.payload?.status === 'succeeded') { - prev.succeeded += 1; - return prev; - } else if (cur.synthetics?.payload?.status === 'skipped') { - prev.skipped += 1; - return prev; - } - prev.failed += 1; - return prev; -} - -function isStepEnd(step: Ping) { - return step.synthetics?.type === 'step/end'; -} - -interface ExecutedJourneyProps { - journey: JourneyState; -} - -export const ExecutedJourney: FC = ({ journey }) => { - return ( -
- -

- -

-

- {statusMessage( - journey.steps - .filter(isStepEnd) - .reduce(reduceStepStatus, { failed: 0, skipped: 0, succeeded: 0 }) - )} -

-
- - - {journey.steps.filter(isStepEnd).map((step, index) => ( - - ))} - - -
- ); -}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.tsx deleted file mode 100644 index 991aa8fefba0a..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.tsx +++ /dev/null @@ -1,110 +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 { EuiFlexItem, EuiFlexGroup, EuiSpacer, EuiText } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React, { FC } from 'react'; -import { i18n } from '@kbn/i18n'; -import { CodeBlockAccordion } from './code_block_accordion'; -import { StepScreenshotDisplay } from './step_screenshot_display'; -import { StatusBadge } from './status_badge'; -import { Ping } from '../../../../common/runtime_types'; -import { StepDetailLink } from '../../common/step_detail_link'; -import { VIEW_PERFORMANCE } from './translations'; - -const CODE_BLOCK_OVERFLOW_HEIGHT = 360; - -interface ExecutedStepProps { - step: Ping; - index: number; - checkGroup: string; -} - -export const ExecutedStep: FC = ({ step, index, checkGroup }) => { - return ( - <> -
- - - - - - - - -
- -
-
-
- -
- - - - - - {step.synthetics?.step?.index && ( - - - {VIEW_PERFORMANCE} - - - - )} - - {step.synthetics?.payload?.source} - - - {step.synthetics?.error?.message} - - - {step.synthetics?.error?.stack} - - - -
-
- - ); -}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/status_badge.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/status_badge.test.tsx deleted file mode 100644 index 304787e96818f..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/status_badge.test.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 { shallowWithIntl } from '@kbn/test/jest'; -import React from 'react'; -import { StatusBadge } from './status_badge'; - -describe('StatusBadge', () => { - it('displays success message', () => { - expect(shallowWithIntl()).toMatchInlineSnapshot(` - - Succeeded - - `); - }); - - it('displays failed message', () => { - expect(shallowWithIntl()).toMatchInlineSnapshot(` - - Failed - - `); - }); - - it('displays skipped message', () => { - expect(shallowWithIntl()).toMatchInlineSnapshot(` - - Skipped - - `); - }); -}); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/step_detail_container.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/step_detail_container.tsx index 346af9d31a28b..ef0d001ac905e 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/step_detail_container.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/step_detail_container.tsx @@ -48,7 +48,7 @@ export const StepDetailContainer: React.FC = ({ checkGroup, stepIndex }) }; }, [stepIndex, journey]); - useMonitorBreadcrumb({ journey, activeStep }); + useMonitorBreadcrumb({ details: journey?.details, activeStep, performanceBreakDownView: true }); const handleNextStep = useCallback(() => { history.push(`/journey/${checkGroup}/step/${stepIndex + 1}`); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/use_monitor_breadcrumb.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/use_monitor_breadcrumb.tsx index c51b85f76d605..8b85f05130d0b 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/use_monitor_breadcrumb.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/use_monitor_breadcrumb.tsx @@ -6,20 +6,25 @@ */ import moment from 'moment'; +import { i18n } from '@kbn/i18n'; import { useBreadcrumbs } from '../../../../hooks/use_breadcrumbs'; -import { useKibana, useUiSetting$ } from '../../../../../../../../src/plugins/kibana_react/public'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { JourneyState } from '../../../../state/reducers/journey'; import { Ping } from '../../../../../common/runtime_types/ping'; import { PLUGIN } from '../../../../../common/constants/plugin'; +import { getShortTimeStamp } from '../../../overview/monitor_list/columns/monitor_status_column'; interface Props { - journey: JourneyState; + details: JourneyState['details']; activeStep?: Ping; + performanceBreakDownView?: boolean; } -export const useMonitorBreadcrumb = ({ journey, activeStep }: Props) => { - const [dateFormat] = useUiSetting$('dateFormat'); - +export const useMonitorBreadcrumb = ({ + details, + activeStep, + performanceBreakDownView = false, +}: Props) => { const kibana = useKibana(); const appPath = kibana.services.application?.getUrlForApp(PLUGIN.ID) ?? ''; @@ -32,8 +37,22 @@ export const useMonitorBreadcrumb = ({ journey, activeStep }: Props) => { }, ] : []), - ...(journey?.details?.timestamp - ? [{ text: moment(journey?.details?.timestamp).format(dateFormat) }] + ...(details?.journey?.monitor?.check_group + ? [ + { + text: getShortTimeStamp(moment(details?.timestamp)), + href: `${appPath}/journey/${details.journey.monitor.check_group}/steps`, + }, + ] + : []), + ...(performanceBreakDownView + ? [ + { + text: i18n.translate('xpack.uptime.synthetics.performanceBreakDown.label', { + defaultMessage: 'Performance breakdown', + }), + }, + ] : []), ]); }; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/use_monitor_breadcrumbs.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/use_monitor_breadcrumbs.test.tsx index ac79d7f4c2a8a..4aed073424788 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/use_monitor_breadcrumbs.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/use_monitor_breadcrumbs.test.tsx @@ -17,7 +17,7 @@ import { JourneyState } from '../../../../state/reducers/journey'; import { chromeServiceMock, uiSettingsServiceMock } from 'src/core/public/mocks'; describe('useMonitorBreadcrumbs', () => { - it('sets the given breadcrumbs', () => { + it('sets the given breadcrumbs for steps list view', () => { let breadcrumbObj: ChromeBreadcrumb[] = []; const getBreadcrumbs = () => { return breadcrumbObj; @@ -43,8 +43,13 @@ describe('useMonitorBreadcrumbs', () => { const Component = () => { useMonitorBreadcrumb({ - activeStep: { monitor: { id: 'test-monitor' } } as Ping, - journey: { details: { timestamp: '2021-01-04T11:25:19.104Z' } } as JourneyState, + activeStep: { monitor: { id: 'test-monitor', check_group: 'fake-test-group' } } as Ping, + details: { + timestamp: '2021-01-04T11:25:19.104Z', + journey: { + monitor: { id: 'test-monitor', check_group: 'fake-test-group' }, + }, + } as JourneyState['details'], }); return <>Step Water Fall; }; @@ -69,7 +74,78 @@ describe('useMonitorBreadcrumbs', () => { "text": "test-monitor", }, Object { - "text": "Jan 4, 2021 @ 06:25:19.104", + "href": "/app/uptime/journey/fake-test-group/steps", + "onClick": [Function], + "text": "Jan 4, 2021 6:25:19 AM", + }, + ] + `); + }); + + it('sets the given breadcrumbs for performance breakdown page', () => { + let breadcrumbObj: ChromeBreadcrumb[] = []; + const getBreadcrumbs = () => { + return breadcrumbObj; + }; + + const core = { + chrome: { + ...chromeServiceMock.createStartContract(), + setBreadcrumbs: (newBreadcrumbs: ChromeBreadcrumb[]) => { + breadcrumbObj = newBreadcrumbs; + }, + }, + uiSettings: { + ...uiSettingsServiceMock.createSetupContract(), + get(key: string, defaultOverride?: any): any { + return `MMM D, YYYY @ HH:mm:ss.SSS` || defaultOverride; + }, + get$(key: string, defaultOverride?: any): any { + return of(`MMM D, YYYY @ HH:mm:ss.SSS`) || of(defaultOverride); + }, + }, + }; + + const Component = () => { + useMonitorBreadcrumb({ + activeStep: { monitor: { id: 'test-monitor', check_group: 'fake-test-group' } } as Ping, + details: { + timestamp: '2021-01-04T11:25:19.104Z', + journey: { + monitor: { id: 'test-monitor', check_group: 'fake-test-group' }, + }, + } as JourneyState['details'], + performanceBreakDownView: true, + }); + return <>Step Water Fall; + }; + + render( + + + , + { core } + ); + + expect(getBreadcrumbs()).toMatchInlineSnapshot(` + Array [ + Object { + "href": "/app/uptime", + "onClick": [Function], + "text": "Uptime", + }, + Object { + "href": "/app/uptime/monitor/dGVzdC1tb25pdG9y", + "onClick": [Function], + "text": "test-monitor", + }, + Object { + "href": "/app/uptime/journey/fake-test-group/steps", + "onClick": [Function], + "text": "Jan 4, 2021 6:25:19 AM", + }, + Object { + "text": "Performance breakdown", }, ] `); diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/monitor_status_column.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/monitor_status_column.tsx index c6476a5bf2e53..f5581f75b3759 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/monitor_status_column.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/monitor_status_column.tsx @@ -67,7 +67,7 @@ export const getShortTimeStamp = (timeStamp: moment.Moment, relative = false) => moment.locale(prevLocale); return shortTimestamp; } else { - if (moment().diff(timeStamp, 'd') > 1) { + if (moment().diff(timeStamp, 'd') >= 1) { return timeStamp.format('ll LTS'); } return timeStamp.format('LTS'); diff --git a/x-pack/plugins/uptime/public/components/synthetics/check_steps/step_expanded_row/screenshot_link.tsx b/x-pack/plugins/uptime/public/components/synthetics/check_steps/step_expanded_row/screenshot_link.tsx new file mode 100644 index 0000000000000..16068e0d72b46 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/synthetics/check_steps/step_expanded_row/screenshot_link.tsx @@ -0,0 +1,47 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n/react'; +import { ReactRouterEuiLink } from '../../../common/react_router_helpers'; +import { Ping } from '../../../../../common/runtime_types/ping'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; + +const LabelLink = euiStyled.div` + margin-bottom: ${(props) => props.theme.eui.paddingSizes.xs}; + font-size: ${({ theme }) => theme.eui.euiFontSizeS}; +`; + +interface Props { + lastSuccessfulStep: Ping; +} + +export const ScreenshotLink = ({ lastSuccessfulStep }: Props) => { + return ( + + + + + + + ), + }} + /> + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/synthetics/check_steps/step_expanded_row/step_screenshots.tsx b/x-pack/plugins/uptime/public/components/synthetics/check_steps/step_expanded_row/step_screenshots.tsx new file mode 100644 index 0000000000000..eb7bc95751557 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/synthetics/check_steps/step_expanded_row/step_screenshots.tsx @@ -0,0 +1,86 @@ +/* + * 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 moment from 'moment'; +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { StepScreenshotDisplay } from '../../step_screenshot_display'; +import { Ping } from '../../../../../common/runtime_types/ping'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; +import { useFetcher } from '../../../../../../observability/public'; +import { fetchLastSuccessfulStep } from '../../../../state/api/journey'; +import { ScreenshotLink } from './screenshot_link'; +import { getShortTimeStamp } from '../../../overview/monitor_list/columns/monitor_status_column'; + +const Label = euiStyled.div` + margin-bottom: ${(props) => props.theme.eui.paddingSizes.xs}; + font-size: ${({ theme }) => theme.eui.euiFontSizeS}; + color: ${({ theme }) => theme.eui.euiColorDarkShade}; +`; + +interface Props { + step: Ping; +} + +export const StepScreenshots = ({ step }: Props) => { + const isSucceeded = step.synthetics?.payload?.status === 'succeeded'; + + const { data: lastSuccessfulStep } = useFetcher(() => { + if (!isSucceeded) { + return fetchLastSuccessfulStep({ + timestamp: step.timestamp, + monitorId: step.monitor.id, + stepIndex: step.synthetics?.step?.index!, + }); + } + }, [step.docId, step.timestamp]); + + return ( + + + + + + + + {!isSucceeded && lastSuccessfulStep?.monitor && ( + + + + + + + )} + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/synthetics/check_steps/step_image.tsx b/x-pack/plugins/uptime/public/components/synthetics/check_steps/step_image.tsx new file mode 100644 index 0000000000000..69a5ef91a5925 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/synthetics/check_steps/step_image.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { Ping } from '../../../../common/runtime_types/ping'; +import { PingTimestamp } from '../../monitor/ping_list/columns/ping_timestamp'; + +interface Props { + step: Ping; +} + +export const StepImage = ({ step }: Props) => { + return ( + + + + + + {step.synthetics?.step?.name} + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/synthetics/check_steps/step_list.test.tsx b/x-pack/plugins/uptime/public/components/synthetics/check_steps/step_list.test.tsx new file mode 100644 index 0000000000000..959bf0f644580 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/synthetics/check_steps/step_list.test.tsx @@ -0,0 +1,144 @@ +/* + * 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 { Ping } from '../../../../common/runtime_types/ping'; +import { StepsList } from './steps_list'; +import { render } from '../../../lib/helper/rtl_helpers'; + +describe('StepList component', () => { + let steps: Ping[]; + + beforeEach(() => { + steps = [ + { + docId: '1', + timestamp: '123', + monitor: { + id: 'MON_ID', + duration: { + us: 10, + }, + status: 'down', + type: 'browser', + check_group: 'fake-group', + }, + synthetics: { + payload: { + status: 'failed', + }, + type: 'step/end', + step: { + name: 'load page', + index: 1, + }, + }, + }, + { + docId: '2', + timestamp: '124', + monitor: { + id: 'MON_ID', + duration: { + us: 10, + }, + status: 'down', + type: 'browser', + check_group: 'fake-group-1', + }, + synthetics: { + payload: { + status: 'failed', + }, + type: 'step/end', + step: { + name: 'go to login', + index: 2, + }, + }, + }, + ]; + }); + + it('creates expected message for all failed', () => { + const { getByText } = render(); + expect(getByText('2 Steps - all failed or skipped')); + }); + + it('renders a link to the step detail view', () => { + const { getByTitle, getByTestId } = render(); + expect(getByTestId('step-detail-link')).toHaveAttribute('href', '/journey/fake-group/step/1'); + expect(getByTitle(`Failed`)); + }); + + it.each([ + ['succeeded', 'Succeeded'], + ['failed', 'Failed'], + ['skipped', 'Skipped'], + ])('supplies status badge correct status', (status, expectedStatus) => { + const step = steps[0]; + step.synthetics!.payload!.status = status; + const { getByText } = render(); + expect(getByText(expectedStatus)); + }); + + it('creates expected message for all succeeded', () => { + steps[0].synthetics!.payload!.status = 'succeeded'; + steps[1].synthetics!.payload!.status = 'succeeded'; + + const { getByText } = render(); + expect(getByText('2 Steps - all succeeded')); + }); + + it('creates appropriate message for mixed results', () => { + steps[0].synthetics!.payload!.status = 'succeeded'; + + const { getByText } = render(); + expect(getByText('2 Steps - 1 succeeded')); + }); + + it('tallies skipped steps', () => { + steps[0].synthetics!.payload!.status = 'succeeded'; + steps[1].synthetics!.payload!.status = 'skipped'; + + const { getByText } = render(); + expect(getByText('2 Steps - 1 succeeded')); + }); + + it('uses appropriate count when non-step/end steps are included', () => { + steps[0].synthetics!.payload!.status = 'succeeded'; + steps.push({ + docId: '3', + timestamp: '125', + monitor: { + id: 'MON_ID', + duration: { + us: 10, + }, + status: 'down', + type: 'browser', + check_group: 'fake-group-2', + }, + synthetics: { + type: 'stderr', + error: { + message: `there was an error, that's all we know`, + stack: 'your.error.happened.here', + }, + }, + }); + + const { getByText } = render(); + expect(getByText('2 Steps - 1 succeeded')); + }); + + it('renders a row per step', () => { + const { getByTestId } = render(); + expect(getByTestId('row-fake-group')); + expect(getByTestId('row-fake-group-1')); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/synthetics/check_steps/steps_list.tsx b/x-pack/plugins/uptime/public/components/synthetics/check_steps/steps_list.tsx new file mode 100644 index 0000000000000..47bf3ae0a1784 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/synthetics/check_steps/steps_list.tsx @@ -0,0 +1,175 @@ +/* + * 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 { EuiBasicTable, EuiButtonIcon, EuiPanel, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { MouseEvent } from 'react'; +import styled from 'styled-components'; +import { Ping } from '../../../../common/runtime_types'; +import { STATUS_LABEL } from '../../monitor/ping_list/translations'; +import { COLLAPSE_LABEL, EXPAND_LABEL, STEP_NAME_LABEL } from '../translations'; +import { StatusBadge } from '../status_badge'; +import { StepDetailLink } from '../../common/step_detail_link'; +import { VIEW_PERFORMANCE } from '../../monitor/synthetics/translations'; +import { StepImage } from './step_image'; +import { useExpandedRow } from './use_expanded_row'; + +export const SpanWithMargin = styled.span` + margin-right: 16px; +`; + +interface Props { + data: Ping[]; + error?: Error; + loading: boolean; +} + +interface StepStatusCount { + failed: number; + skipped: number; + succeeded: number; +} + +function isStepEnd(step: Ping) { + return step.synthetics?.type === 'step/end'; +} + +function statusMessage(count: StepStatusCount, loading?: boolean) { + if (loading) { + return i18n.translate('xpack.uptime.synthetics.journey.loadingSteps', { + defaultMessage: 'Loading steps ...', + }); + } + const total = count.succeeded + count.failed + count.skipped; + if (count.failed + count.skipped === total) { + return i18n.translate('xpack.uptime.synthetics.journey.allFailedMessage', { + defaultMessage: '{total} Steps - all failed or skipped', + values: { total }, + }); + } else if (count.succeeded === total) { + return i18n.translate('xpack.uptime.synthetics.journey.allSucceededMessage', { + defaultMessage: '{total} Steps - all succeeded', + values: { total }, + }); + } + return i18n.translate('xpack.uptime.synthetics.journey.partialSuccessMessage', { + defaultMessage: '{total} Steps - {succeeded} succeeded', + values: { succeeded: count.succeeded, total }, + }); +} + +function reduceStepStatus(prev: StepStatusCount, cur: Ping): StepStatusCount { + if (cur.synthetics?.payload?.status === 'succeeded') { + prev.succeeded += 1; + return prev; + } else if (cur.synthetics?.payload?.status === 'skipped') { + prev.skipped += 1; + return prev; + } + prev.failed += 1; + return prev; +} + +export const StepsList = ({ data, error, loading }: Props) => { + const steps = data.filter(isStepEnd); + + const { expandedRows, toggleExpand } = useExpandedRow({ steps, allPings: data, loading }); + + const columns: any[] = [ + { + field: 'synthetics.payload.status', + name: STATUS_LABEL, + render: (pingStatus: string, item: Ping) => ( + + ), + }, + { + align: 'left', + field: 'timestamp', + name: STEP_NAME_LABEL, + render: (timestamp: string, item: Ping) => , + }, + { + align: 'left', + field: 'timestamp', + name: '', + render: (val: string, item: Ping) => ( + + {VIEW_PERFORMANCE} + + ), + }, + { + align: 'right', + width: '24px', + isExpander: true, + render: (ping: Ping) => { + return ( + toggleExpand({ ping })} + aria-label={expandedRows[ping.docId] ? COLLAPSE_LABEL : EXPAND_LABEL} + iconType={expandedRows[ping.docId] ? 'arrowUp' : 'arrowDown'} + /> + ); + }, + }, + ]; + + const getRowProps = (item: Ping) => { + const { monitor } = item; + + return { + height: '85px', + 'data-test-subj': `row-${monitor.check_group}`, + onClick: (evt: MouseEvent) => { + const targetElem = evt.target as HTMLElement; + + // we dont want to capture image click event + if (targetElem.tagName !== 'IMG' && targetElem.tagName !== 'BUTTON') { + toggleExpand({ ping: item }); + } + }, + }; + }; + + return ( + + +

+ {statusMessage( + steps.reduce(reduceStepStatus, { failed: 0, skipped: 0, succeeded: 0 }), + loading + )} +

+
+ +
+ ); +}; diff --git a/x-pack/plugins/uptime/public/components/synthetics/check_steps/use_check_steps.ts b/x-pack/plugins/uptime/public/components/synthetics/check_steps/use_check_steps.ts new file mode 100644 index 0000000000000..da40b900fdcc2 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/synthetics/check_steps/use_check_steps.ts @@ -0,0 +1,29 @@ +/* + * 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 { useParams } from 'react-router-dom'; +import { FETCH_STATUS, useFetcher } from '../../../../../observability/public'; +import { fetchJourneySteps } from '../../../state/api/journey'; +import { JourneyState } from '../../../state/reducers/journey'; + +export const useCheckSteps = (): JourneyState => { + const { checkGroupId } = useParams<{ checkGroupId: string }>(); + + const { data, status, error } = useFetcher(() => { + return fetchJourneySteps({ + checkGroup: checkGroupId, + }); + }, [checkGroupId]); + + return { + error, + checkGroup: checkGroupId, + steps: data?.steps ?? [], + details: data?.details, + loading: status === FETCH_STATUS.LOADING || status === FETCH_STATUS.PENDING, + }; +}; diff --git a/x-pack/plugins/uptime/public/components/synthetics/check_steps/use_expanded_row.test.tsx b/x-pack/plugins/uptime/public/components/synthetics/check_steps/use_expanded_row.test.tsx new file mode 100644 index 0000000000000..d94122a7311ca --- /dev/null +++ b/x-pack/plugins/uptime/public/components/synthetics/check_steps/use_expanded_row.test.tsx @@ -0,0 +1,152 @@ +/* + * 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 { Route } from 'react-router-dom'; +import { fireEvent, screen } from '@testing-library/dom'; +import { EuiButtonIcon } from '@elastic/eui'; +import { createMemoryHistory } from 'history'; + +import { useExpandedRow } from './use_expanded_row'; +import { render } from '../../../lib/helper/rtl_helpers'; +import { Ping } from '../../../../common/runtime_types/ping'; +import { SYNTHETIC_CHECK_STEPS_ROUTE } from '../../../../common/constants'; +import { COLLAPSE_LABEL, EXPAND_LABEL } from '../translations'; +import { act } from 'react-dom/test-utils'; + +describe('useExpandedROw', () => { + let expandedRowsObj = {}; + const TEST_ID = 'uptimeStepListExpandBtn'; + + const history = createMemoryHistory({ + initialEntries: ['/journey/fake-group/steps'], + }); + const steps: Ping[] = [ + { + docId: '1', + timestamp: '123', + monitor: { + id: 'MON_ID', + duration: { + us: 10, + }, + status: 'down', + type: 'browser', + check_group: 'fake-group', + }, + synthetics: { + payload: { + status: 'failed', + }, + type: 'step/end', + step: { + name: 'load page', + index: 1, + }, + }, + }, + { + docId: '2', + timestamp: '124', + monitor: { + id: 'MON_ID', + duration: { + us: 10, + }, + status: 'down', + type: 'browser', + check_group: 'fake-group', + }, + synthetics: { + payload: { + status: 'failed', + }, + type: 'step/end', + step: { + name: 'go to login', + index: 2, + }, + }, + }, + ]; + + const Component = () => { + const { expandedRows, toggleExpand } = useExpandedRow({ + steps, + allPings: steps, + loading: false, + }); + + expandedRowsObj = expandedRows; + + return ( + + Step list + {steps.map((ping, index) => ( + toggleExpand({ ping })} + aria-label={expandedRows[ping.docId] ? COLLAPSE_LABEL : EXPAND_LABEL} + iconType={expandedRows[ping.docId] ? 'arrowUp' : 'arrowDown'} + /> + ))} + + ); + }; + + it('it toggles rows on expand click', async () => { + render(, { + history, + }); + + fireEvent.click(await screen.findByTestId(TEST_ID + '1')); + + expect(Object.keys(expandedRowsObj)).toStrictEqual(['1']); + + expect(JSON.stringify(expandedRowsObj)).toContain('fake-group'); + + await act(async () => { + fireEvent.click(await screen.findByTestId(TEST_ID + '1')); + }); + + expect(Object.keys(expandedRowsObj)).toStrictEqual([]); + }); + + it('it can expand both rows at same time', async () => { + render(, { + history, + }); + + // let's expand both rows + fireEvent.click(await screen.findByTestId(TEST_ID + '1')); + fireEvent.click(await screen.findByTestId(TEST_ID + '0')); + + expect(Object.keys(expandedRowsObj)).toStrictEqual(['0', '1']); + }); + + it('it updates already expanded rows on new check group monitor', async () => { + render(, { + history, + }); + + // let's expand both rows + fireEvent.click(await screen.findByTestId(TEST_ID + '1')); + fireEvent.click(await screen.findByTestId(TEST_ID + '0')); + + const newFakeGroup = 'new-fake-group-1'; + + steps[0].monitor.check_group = newFakeGroup; + steps[1].monitor.check_group = newFakeGroup; + + act(() => { + history.push(`/journey/${newFakeGroup}/steps`); + }); + + expect(JSON.stringify(expandedRowsObj)).toContain(newFakeGroup); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/synthetics/check_steps/use_expanded_row.tsx b/x-pack/plugins/uptime/public/components/synthetics/check_steps/use_expanded_row.tsx new file mode 100644 index 0000000000000..bb56b237dfbd2 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/synthetics/check_steps/use_expanded_row.tsx @@ -0,0 +1,88 @@ +/* + * 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, { useEffect, useState, useCallback } from 'react'; +import { useParams } from 'react-router-dom'; +import { ExecutedStep } from '../executed_step'; +import { Ping } from '../../../../common/runtime_types/ping'; + +interface HookProps { + loading: boolean; + allPings: Ping[]; + steps: Ping[]; +} + +type ExpandRowType = Record; + +export const useExpandedRow = ({ loading, steps, allPings }: HookProps) => { + const [expandedRows, setExpandedRows] = useState({}); + // eui table uses index from 0, synthetics uses 1 + + const { checkGroupId } = useParams<{ checkGroupId: string }>(); + + const getBrowserConsole = useCallback( + (index: number) => { + return allPings.find( + (stepF) => + stepF.synthetics?.type === 'journey/browserconsole' && + stepF.synthetics?.step?.index! === index + )?.synthetics?.payload?.text; + }, + [allPings] + ); + + useEffect(() => { + const expandedRowsN: ExpandRowType = {}; + for (const expandedRowKeyStr in expandedRows) { + if (expandedRows.hasOwnProperty(expandedRowKeyStr)) { + const expandedRowKey = Number(expandedRowKeyStr); + + const step = steps.find((stepF) => stepF.synthetics?.step?.index !== expandedRowKey)!; + + expandedRowsN[expandedRowKey] = ( + + ); + } + } + + setExpandedRows(expandedRowsN); + + // we only want to update when checkGroupId changes + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [checkGroupId, loading]); + + const toggleExpand = ({ ping }: { ping: Ping }) => { + // eui table uses index from 0, synthetics uses 1 + const stepIndex = ping.synthetics?.step?.index! - 1; + + // If already expanded, collapse + if (expandedRows[stepIndex]) { + delete expandedRows[stepIndex]; + setExpandedRows({ ...expandedRows }); + } else { + // Otherwise expand this row + setExpandedRows({ + ...expandedRows, + [stepIndex]: ( + + ), + }); + } + }; + + return { expandedRows, toggleExpand }; +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/code_block_accordion.tsx b/x-pack/plugins/uptime/public/components/synthetics/code_block_accordion.tsx similarity index 87% rename from x-pack/plugins/uptime/public/components/monitor/synthetics/code_block_accordion.tsx rename to x-pack/plugins/uptime/public/components/synthetics/code_block_accordion.tsx index 18aeb7a236ca8..225ba1041c263 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/code_block_accordion.tsx +++ b/x-pack/plugins/uptime/public/components/synthetics/code_block_accordion.tsx @@ -13,6 +13,7 @@ interface Props { id?: string; language: 'html' | 'javascript'; overflowHeight: number; + initialIsOpen?: boolean; } /** @@ -25,9 +26,10 @@ export const CodeBlockAccordion: FC = ({ id, language, overflowHeight, + initialIsOpen = false, }) => { return children && id ? ( - + {children} diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/console_event.test.tsx b/x-pack/plugins/uptime/public/components/synthetics/console_event.test.tsx similarity index 100% rename from x-pack/plugins/uptime/public/components/monitor/synthetics/console_event.test.tsx rename to x-pack/plugins/uptime/public/components/synthetics/console_event.test.tsx diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/console_event.tsx b/x-pack/plugins/uptime/public/components/synthetics/console_event.tsx similarity index 89% rename from x-pack/plugins/uptime/public/components/monitor/synthetics/console_event.tsx rename to x-pack/plugins/uptime/public/components/synthetics/console_event.tsx index dc7b6ce9ea123..19672f953607b 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/console_event.tsx +++ b/x-pack/plugins/uptime/public/components/synthetics/console_event.tsx @@ -7,8 +7,8 @@ import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; import React, { useContext, FC } from 'react'; -import { Ping } from '../../../../common/runtime_types'; -import { UptimeThemeContext } from '../../../contexts'; +import { UptimeThemeContext } from '../../contexts'; +import { Ping } from '../../../common/runtime_types/ping'; interface Props { event: Ping; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/console_output_event_list.test.tsx b/x-pack/plugins/uptime/public/components/synthetics/console_output_event_list.test.tsx similarity index 100% rename from x-pack/plugins/uptime/public/components/monitor/synthetics/console_output_event_list.test.tsx rename to x-pack/plugins/uptime/public/components/synthetics/console_output_event_list.test.tsx diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/console_output_event_list.tsx b/x-pack/plugins/uptime/public/components/synthetics/console_output_event_list.tsx similarity index 92% rename from x-pack/plugins/uptime/public/components/monitor/synthetics/console_output_event_list.tsx rename to x-pack/plugins/uptime/public/components/synthetics/console_output_event_list.tsx index df1f6aeb3623b..df4314e5ccf1c 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/console_output_event_list.tsx +++ b/x-pack/plugins/uptime/public/components/synthetics/console_output_event_list.tsx @@ -8,9 +8,9 @@ import { EuiCodeBlock, EuiSpacer, EuiTitle } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { FC } from 'react'; -import { Ping } from '../../../../common/runtime_types'; -import { JourneyState } from '../../../state/reducers/journey'; import { ConsoleEvent } from './console_event'; +import { Ping } from '../../../common/runtime_types/ping'; +import { JourneyState } from '../../state/reducers/journey'; interface Props { journey: JourneyState; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/empty_journey.test.tsx b/x-pack/plugins/uptime/public/components/synthetics/empty_journey.test.tsx similarity index 100% rename from x-pack/plugins/uptime/public/components/monitor/synthetics/empty_journey.test.tsx rename to x-pack/plugins/uptime/public/components/synthetics/empty_journey.test.tsx diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/empty_journey.tsx b/x-pack/plugins/uptime/public/components/synthetics/empty_journey.tsx similarity index 100% rename from x-pack/plugins/uptime/public/components/monitor/synthetics/empty_journey.tsx rename to x-pack/plugins/uptime/public/components/synthetics/empty_journey.tsx diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.test.tsx b/x-pack/plugins/uptime/public/components/synthetics/executed_step.test.tsx similarity index 54% rename from x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.test.tsx rename to x-pack/plugins/uptime/public/components/synthetics/executed_step.test.tsx index 225ccb884ad00..24b52e09adbf9 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.test.tsx +++ b/x-pack/plugins/uptime/public/components/synthetics/executed_step.test.tsx @@ -7,8 +7,8 @@ import React from 'react'; import { ExecutedStep } from './executed_step'; -import { Ping } from '../../../../common/runtime_types'; -import { render } from '../../../lib/helper/rtl_helpers'; +import { render } from '../../lib/helper/rtl_helpers'; +import { Ping } from '../../../common/runtime_types/ping'; describe('ExecutedStep', () => { let step: Ping; @@ -34,33 +34,6 @@ describe('ExecutedStep', () => { }; }); - it('renders correct step heading', () => { - const { getByText } = render(); - - expect(getByText(`${step?.synthetics?.step?.index}. ${step?.synthetics?.step?.name}`)); - }); - - it('renders a link to the step detail view', () => { - const { getByRole, getByText } = render( - - ); - expect(getByRole('link')).toHaveAttribute('href', '/journey/fake-group/step/4'); - expect(getByText('4. STEP_NAME')); - }); - - it.each([ - ['succeeded', 'Succeeded'], - ['failed', 'Failed'], - ['skipped', 'Skipped'], - ['somegarbage', '4.'], - ])('supplies status badge correct status', (status, expectedStatus) => { - step.synthetics = { - payload: { status }, - }; - const { getByText } = render(); - expect(getByText(expectedStatus)); - }); - it('renders accordion for step', () => { step.synthetics = { payload: { @@ -72,10 +45,9 @@ describe('ExecutedStep', () => { }, }; - const { getByText } = render(); + const { getByText } = render(); - expect(getByText('4. STEP_NAME')); - expect(getByText('Step script')); + expect(getByText('Script executed at this step')); expect(getByText(`const someVar = "the var"`)); }); @@ -87,11 +59,22 @@ describe('ExecutedStep', () => { }, }; - const { getByText } = render(); + const { getByText } = render(); - expect(getByText('4.')); - expect(getByText('Error')); + expect(getByText('Error message')); expect(getByText('There was an error executing the step.')); expect(getByText('some.stack.trace.string')); }); + + it('renders accordions for console output', () => { + const browserConsole = + "Refused to execute script from because its MIME type ('image/gif') is not executable"; + + const { getByText } = render( + + ); + + expect(getByText('Console output')); + expect(getByText(browserConsole)); + }); }); diff --git a/x-pack/plugins/uptime/public/components/synthetics/executed_step.tsx b/x-pack/plugins/uptime/public/components/synthetics/executed_step.tsx new file mode 100644 index 0000000000000..a77b3dfe3ba21 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/synthetics/executed_step.tsx @@ -0,0 +1,118 @@ +/* + * 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 { EuiLoadingSpinner, EuiSpacer, EuiText } from '@elastic/eui'; +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { CodeBlockAccordion } from './code_block_accordion'; +import { Ping } from '../../../common/runtime_types/ping'; +import { euiStyled } from '../../../../../../src/plugins/kibana_react/common'; +import { StepScreenshots } from './check_steps/step_expanded_row/step_screenshots'; + +const CODE_BLOCK_OVERFLOW_HEIGHT = 360; + +interface ExecutedStepProps { + step: Ping; + index: number; + loading: boolean; + browserConsole?: string; +} + +const Label = euiStyled.div` + margin-bottom: ${(props) => props.theme.eui.paddingSizes.xs}; + font-size: ${({ theme }) => theme.eui.euiFontSizeS}; + color: ${({ theme }) => theme.eui.euiColorDarkShade}; +`; + +const Message = euiStyled.div` + font-weight: bold; + font-size:${({ theme }) => theme.eui.euiFontSizeM}; + margin-bottom: ${(props) => props.theme.eui.paddingSizes.m}; +`; + +const ExpandedRow = euiStyled.div` + padding: '8px'; + max-width: 1000px; + width: 100%; +`; + +export const ExecutedStep: FC = ({ + loading, + step, + index, + browserConsole = '', +}) => { + const isSucceeded = step.synthetics?.payload?.status === 'succeeded'; + + return ( + + {loading ? ( + + ) : ( + <> + + {step.synthetics?.error?.message && ( + + + {step.synthetics?.error?.message} + + )} + + + {step.synthetics?.payload?.source} + + + + <> + {browserConsole} + + + + + + + {step.synthetics?.error?.stack} + + + )} + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/synthetics/status_badge.test.tsx b/x-pack/plugins/uptime/public/components/synthetics/status_badge.test.tsx new file mode 100644 index 0000000000000..500c680b91bf6 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/synthetics/status_badge.test.tsx @@ -0,0 +1,33 @@ +/* + * 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 { StatusBadge } from './status_badge'; +import { render } from '../../lib/helper/rtl_helpers'; + +describe('StatusBadge', () => { + it('displays success message', () => { + const { getByText } = render(); + + expect(getByText('1.')); + expect(getByText('Succeeded')); + }); + + it('displays failed message', () => { + const { getByText } = render(); + + expect(getByText('2.')); + expect(getByText('Failed')); + }); + + it('displays skipped message', () => { + const { getByText } = render(); + + expect(getByText('3.')); + expect(getByText('Skipped')); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/status_badge.tsx b/x-pack/plugins/uptime/public/components/synthetics/status_badge.tsx similarity index 66% rename from x-pack/plugins/uptime/public/components/monitor/synthetics/status_badge.tsx rename to x-pack/plugins/uptime/public/components/synthetics/status_badge.tsx index 0cf9e5477d0db..b4c4e310abe6b 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/status_badge.tsx +++ b/x-pack/plugins/uptime/public/components/synthetics/status_badge.tsx @@ -5,14 +5,15 @@ * 2.0. */ -import { EuiBadge } from '@elastic/eui'; +import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useContext, FC } from 'react'; -import { UptimeAppColors } from '../../../apps/uptime_app'; -import { UptimeThemeContext } from '../../../contexts'; +import { UptimeAppColors } from '../../apps/uptime_app'; +import { UptimeThemeContext } from '../../contexts'; interface StatusBadgeProps { status?: string; + stepNo: number; } export function colorFromStatus(color: UptimeAppColors, status?: string) { @@ -45,9 +46,18 @@ export function textFromStatus(status?: string) { } } -export const StatusBadge: FC = ({ status }) => { +export const StatusBadge: FC = ({ status, stepNo }) => { const theme = useContext(UptimeThemeContext); return ( - {textFromStatus(status)} + + + + {stepNo}. + + + + {textFromStatus(status)} + + ); }; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_screenshot_display.test.tsx b/x-pack/plugins/uptime/public/components/synthetics/step_screenshot_display.test.tsx similarity index 96% rename from x-pack/plugins/uptime/public/components/monitor/synthetics/step_screenshot_display.test.tsx rename to x-pack/plugins/uptime/public/components/synthetics/step_screenshot_display.test.tsx index 29dca39c34bf2..52d2eacaf0e52 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_screenshot_display.test.tsx +++ b/x-pack/plugins/uptime/public/components/synthetics/step_screenshot_display.test.tsx @@ -5,9 +5,9 @@ * 2.0. */ -import { render } from '../../../lib/helper/rtl_helpers'; import React from 'react'; import { StepScreenshotDisplay } from './step_screenshot_display'; +import { render } from '../../lib/helper/rtl_helpers'; jest.mock('react-use/lib/useIntersection', () => () => ({ isIntersecting: true, diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_screenshot_display.tsx b/x-pack/plugins/uptime/public/components/synthetics/step_screenshot_display.tsx similarity index 52% rename from x-pack/plugins/uptime/public/components/monitor/synthetics/step_screenshot_display.tsx rename to x-pack/plugins/uptime/public/components/synthetics/step_screenshot_display.tsx index 654193de72a9c..78c65b7d40803 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_screenshot_display.tsx +++ b/x-pack/plugins/uptime/public/components/synthetics/step_screenshot_display.tsx @@ -5,33 +5,32 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiImage, EuiPopover, EuiText } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiImage, EuiText } from '@elastic/eui'; import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useContext, useEffect, useRef, useState, FC } from 'react'; import useIntersection from 'react-use/lib/useIntersection'; -import { UptimeSettingsContext, UptimeThemeContext } from '../../../contexts'; +import { UptimeSettingsContext, UptimeThemeContext } from '../../contexts'; interface StepScreenshotDisplayProps { screenshotExists?: boolean; checkGroup?: string; stepIndex?: number; stepName?: string; + lazyLoad?: boolean; } -const THUMBNAIL_WIDTH = 320; -const THUMBNAIL_HEIGHT = 180; -const POPOVER_IMG_WIDTH = 640; -const POPOVER_IMG_HEIGHT = 360; +const IMAGE_WIDTH = 640; +const IMAGE_HEIGHT = 360; const StepImage = styled(EuiImage)` &&& { figcaption { display: none; } - width: ${THUMBNAIL_WIDTH}, - height: ${THUMBNAIL_HEIGHT}, + width: ${IMAGE_WIDTH}, + height: ${IMAGE_HEIGHT}, objectFit: 'cover', objectPosition: 'center top', } @@ -42,6 +41,7 @@ export const StepScreenshotDisplay: FC = ({ screenshotExists, stepIndex, stepName, + lazyLoad = true, }) => { const containerRef = useRef(null); const { @@ -50,8 +50,6 @@ export const StepScreenshotDisplay: FC = ({ const { basePath } = useContext(UptimeSettingsContext); - const [isImagePopoverOpen, setIsImagePopoverOpen] = useState(false); - const intersection = useIntersection(containerRef, { root: null, rootMargin: '0px', @@ -69,57 +67,26 @@ export const StepScreenshotDisplay: FC = ({ let content: JSX.Element | null = null; const imgSrc = basePath + `/api/uptime/journey/screenshot/${checkGroup}/${stepIndex}`; - if (hasIntersected && screenshotExists) { + if ((hasIntersected || !lazyLoad) && screenshotExists) { content = ( - <> - setIsImagePopoverOpen(true)} - onMouseLeave={() => setIsImagePopoverOpen(false)} - /> - } - closePopover={() => setIsImagePopoverOpen(false)} - isOpen={isImagePopoverOpen} - > - - - + ); } else if (screenshotExists === false) { content = ( @@ -148,7 +115,7 @@ export const StepScreenshotDisplay: FC = ({ return (
{content}
diff --git a/x-pack/plugins/uptime/public/components/synthetics/translations.ts b/x-pack/plugins/uptime/public/components/synthetics/translations.ts new file mode 100644 index 0000000000000..743118574b325 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/synthetics/translations.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const STEP_NAME_LABEL = i18n.translate('xpack.uptime.stepList.stepName', { + defaultMessage: 'Step name', +}); + +export const COLLAPSE_LABEL = i18n.translate('xpack.uptime.stepList.collapseRow', { + defaultMessage: 'Collapse', +}); + +export const EXPAND_LABEL = i18n.translate('xpack.uptime.stepList.expandRow', { + defaultMessage: 'Expand', +}); diff --git a/x-pack/plugins/uptime/public/hooks/use_telemetry.ts b/x-pack/plugins/uptime/public/hooks/use_telemetry.ts index da0f109747758..b9ec9cc5e5516 100644 --- a/x-pack/plugins/uptime/public/hooks/use_telemetry.ts +++ b/x-pack/plugins/uptime/public/hooks/use_telemetry.ts @@ -16,6 +16,7 @@ export enum UptimePage { Settings = 'Settings', Certificates = 'Certificates', StepDetail = 'StepDetail', + SyntheticCheckStepsPage = 'SyntheticCheckStepsPage', NotFound = '__not-found__', } diff --git a/x-pack/plugins/uptime/public/pages/index.ts b/x-pack/plugins/uptime/public/pages/index.ts index 828942bc1eb1e..5624f61c3abb5 100644 --- a/x-pack/plugins/uptime/public/pages/index.ts +++ b/x-pack/plugins/uptime/public/pages/index.ts @@ -6,6 +6,6 @@ */ export { MonitorPage } from './monitor'; -export { StepDetailPage } from './step_detail_page'; +export { StepDetailPage } from './synthetics/step_detail_page'; export { SettingsPage } from './settings'; export { NotFoundPage } from './not_found'; diff --git a/x-pack/plugins/uptime/public/pages/synthetics/checks_navigation.tsx b/x-pack/plugins/uptime/public/pages/synthetics/checks_navigation.tsx new file mode 100644 index 0000000000000..291019d93c398 --- /dev/null +++ b/x-pack/plugins/uptime/public/pages/synthetics/checks_navigation.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useHistory } from 'react-router-dom'; +import moment from 'moment'; +import { SyntheticsJourneyApiResponse } from '../../../common/runtime_types/ping'; +import { getShortTimeStamp } from '../../components/overview/monitor_list/columns/monitor_status_column'; + +interface Props { + timestamp: string; + details: SyntheticsJourneyApiResponse['details']; +} + +export const ChecksNavigation = ({ timestamp, details }: Props) => { + const history = useHistory(); + + return ( + + + { + history.push(`/journey/${details?.previous?.checkGroup}/steps`); + }} + > + + + + + {getShortTimeStamp(moment(timestamp))} + + + { + history.push(`/journey/${details?.next?.checkGroup}/steps`); + }} + > + + + + + ); +}; diff --git a/x-pack/plugins/uptime/public/pages/step_detail_page.tsx b/x-pack/plugins/uptime/public/pages/synthetics/step_detail_page.tsx similarity index 75% rename from x-pack/plugins/uptime/public/pages/step_detail_page.tsx rename to x-pack/plugins/uptime/public/pages/synthetics/step_detail_page.tsx index aa81ddd0eae3d..de38d2d663523 100644 --- a/x-pack/plugins/uptime/public/pages/step_detail_page.tsx +++ b/x-pack/plugins/uptime/public/pages/synthetics/step_detail_page.tsx @@ -7,9 +7,9 @@ import React from 'react'; import { useParams } from 'react-router-dom'; -import { useTrackPageview } from '../../../observability/public'; -import { useInitApp } from '../hooks/use_init_app'; -import { StepDetailContainer } from '../components/monitor/synthetics/step_detail/step_detail_container'; +import { useTrackPageview } from '../../../../observability/public'; +import { useInitApp } from '../../hooks/use_init_app'; +import { StepDetailContainer } from '../../components/monitor/synthetics/step_detail/step_detail_container'; export const StepDetailPage: React.FC = () => { useInitApp(); diff --git a/x-pack/plugins/uptime/public/pages/synthetics/synthetics_checks.tsx b/x-pack/plugins/uptime/public/pages/synthetics/synthetics_checks.tsx new file mode 100644 index 0000000000000..edfd7ae24f91b --- /dev/null +++ b/x-pack/plugins/uptime/public/pages/synthetics/synthetics_checks.tsx @@ -0,0 +1,44 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { useTrackPageview } from '../../../../observability/public'; +import { useInitApp } from '../../hooks/use_init_app'; +import { StepsList } from '../../components/synthetics/check_steps/steps_list'; +import { useCheckSteps } from '../../components/synthetics/check_steps/use_check_steps'; +import { ChecksNavigation } from './checks_navigation'; +import { useMonitorBreadcrumb } from '../../components/monitor/synthetics/step_detail/use_monitor_breadcrumb'; +import { EmptyJourney } from '../../components/synthetics/empty_journey'; + +export const SyntheticsCheckSteps: React.FC = () => { + useInitApp(); + useTrackPageview({ app: 'uptime', path: 'syntheticCheckSteps' }); + useTrackPageview({ app: 'uptime', path: 'syntheticCheckSteps', delay: 15000 }); + + const { error, loading, steps, details, checkGroup } = useCheckSteps(); + + useMonitorBreadcrumb({ details, activeStep: details?.journey }); + + return ( + <> + + + +

{details?.journey?.monitor.name || details?.journey?.monitor.id}

+
+
+ + {details && } + +
+ + + {(!steps || steps.length === 0) && !loading && } + + ); +}; diff --git a/x-pack/plugins/uptime/public/routes.tsx b/x-pack/plugins/uptime/public/routes.tsx index 82aa09c3293e6..dcfb21955f219 100644 --- a/x-pack/plugins/uptime/public/routes.tsx +++ b/x-pack/plugins/uptime/public/routes.tsx @@ -15,10 +15,12 @@ import { OVERVIEW_ROUTE, SETTINGS_ROUTE, STEP_DETAIL_ROUTE, + SYNTHETIC_CHECK_STEPS_ROUTE, } from '../common/constants'; import { MonitorPage, StepDetailPage, NotFoundPage, SettingsPage } from './pages'; import { CertificatesPage } from './pages/certificates'; import { UptimePage, useUptimeTelemetry } from './hooks'; +import { SyntheticsCheckSteps } from './pages/synthetics/synthetics_checks'; interface RouteProps { path: string; @@ -71,6 +73,13 @@ const Routes: RouteProps[] = [ dataTestSubj: 'uptimeStepDetailPage', telemetryId: UptimePage.StepDetail, }, + { + title: baseTitle, + path: SYNTHETIC_CHECK_STEPS_ROUTE, + component: SyntheticsCheckSteps, + dataTestSubj: 'uptimeSyntheticCheckStepsPage', + telemetryId: UptimePage.SyntheticCheckStepsPage, + }, { title: baseTitle, path: OVERVIEW_ROUTE, diff --git a/x-pack/plugins/uptime/public/state/api/journey.ts b/x-pack/plugins/uptime/public/state/api/journey.ts index 5c4c7c7149792..63796a66d1c5c 100644 --- a/x-pack/plugins/uptime/public/state/api/journey.ts +++ b/x-pack/plugins/uptime/public/state/api/journey.ts @@ -8,6 +8,7 @@ import { apiService } from './utils'; import { FetchJourneyStepsParams } from '../actions/journey'; import { + Ping, SyntheticsJourneyApiResponse, SyntheticsJourneyApiResponseType, } from '../../../common/runtime_types'; @@ -34,6 +35,22 @@ export async function fetchJourneysFailedSteps({ )) as SyntheticsJourneyApiResponse; } +export async function fetchLastSuccessfulStep({ + monitorId, + timestamp, + stepIndex, +}: { + monitorId: string; + timestamp: string; + stepIndex: number; +}): Promise { + return (await apiService.get(`/api/uptime/synthetics/step/success/`, { + monitorId, + timestamp, + stepIndex, + })) as Ping; +} + export async function getJourneyScreenshot(imgSrc: string) { try { const imgRequest = new Request(imgSrc); diff --git a/x-pack/plugins/uptime/server/lib/requests/get_journey_details.ts b/x-pack/plugins/uptime/server/lib/requests/get_journey_details.ts index e0edcc4576378..de37688b155f5 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_journey_details.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_journey_details.ts @@ -27,13 +27,12 @@ export const getJourneyDetails: UMElasticsearchQueryFn< }, { term: { - 'synthetics.type': 'journey/end', + 'synthetics.type': 'journey/start', }, }, ], }, }, - _source: ['@timestamp', 'monitor.id'], size: 1, }; @@ -53,7 +52,7 @@ export const getJourneyDetails: UMElasticsearchQueryFn< }, { term: { - 'synthetics.type': 'journey/end', + 'synthetics.type': 'journey/start', }, }, ], @@ -109,6 +108,7 @@ export const getJourneyDetails: UMElasticsearchQueryFn< nextJourneyResult?.hits?.hits.length > 0 ? nextJourneyResult?.hits?.hits[0] : null; return { timestamp: thisJourneySource['@timestamp'], + journey: thisJourneySource, previous: previousJourney ? { checkGroup: previousJourney._source.monitor.check_group, diff --git a/x-pack/plugins/uptime/server/lib/requests/get_journey_screenshot.ts b/x-pack/plugins/uptime/server/lib/requests/get_journey_screenshot.ts index 9cb5e1eedb6b0..faa260eb9abd4 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_journey_screenshot.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_journey_screenshot.ts @@ -60,10 +60,10 @@ export const getJourneyScreenshot: UMElasticsearchQueryFn< return null; } - const stepHit = result?.aggregations?.step.image.hits.hits[0]._source as Ping; + const stepHit = result?.aggregations?.step.image.hits.hits[0]?._source as Ping; return { - blob: stepHit.synthetics?.blob ?? null, + blob: stepHit?.synthetics?.blob ?? null, stepName: stepHit?.synthetics?.step?.name ?? '', totalSteps: result?.hits?.total.value, }; diff --git a/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.test.ts b/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.test.ts index 1034318257f66..af7752b05997e 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.test.ts @@ -14,9 +14,9 @@ describe('getJourneySteps request module', () => { expect(formatSyntheticEvents()).toMatchInlineSnapshot(` Array [ "step/end", - "stderr", "cmd/status", "step/screenshot", + "journey/browserconsole", ] `); }); @@ -121,9 +121,9 @@ describe('getJourneySteps request module', () => { "terms": Object { "synthetics.type": Array [ "step/end", - "stderr", "cmd/status", "step/screenshot", + "journey/browserconsole", ], }, } diff --git a/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts b/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts index 3055f169fc495..43d17cb938159 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts @@ -13,7 +13,7 @@ export interface GetJourneyStepsParams { syntheticEventTypes?: string | string[]; } -const defaultEventTypes = ['step/end', 'stderr', 'cmd/status', 'step/screenshot']; +const defaultEventTypes = ['step/end', 'cmd/status', 'step/screenshot', 'journey/browserconsole']; export const formatSyntheticEvents = (eventTypes?: string | string[]) => { if (!eventTypes) { diff --git a/x-pack/plugins/uptime/server/lib/requests/get_last_successful_step.ts b/x-pack/plugins/uptime/server/lib/requests/get_last_successful_step.ts new file mode 100644 index 0000000000000..82958167341c0 --- /dev/null +++ b/x-pack/plugins/uptime/server/lib/requests/get_last_successful_step.ts @@ -0,0 +1,77 @@ +/* + * 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 { UMElasticsearchQueryFn } from '../adapters/framework'; +import { Ping } from '../../../common/runtime_types/ping'; + +export interface GetStepScreenshotParams { + monitorId: string; + timestamp: string; + stepIndex: number; +} + +export const getStepLastSuccessfulStep: UMElasticsearchQueryFn< + GetStepScreenshotParams, + any +> = async ({ uptimeEsClient, monitorId, stepIndex, timestamp }) => { + const lastSuccessCheckParams = { + size: 1, + sort: [ + { + '@timestamp': { + order: 'desc', + }, + }, + ], + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + lte: timestamp, + }, + }, + }, + { + term: { + 'monitor.id': monitorId, + }, + }, + { + term: { + 'synthetics.type': 'step/end', + }, + }, + { + term: { + 'synthetics.step.status': 'succeeded', + }, + }, + { + term: { + 'synthetics.step.index': stepIndex, + }, + }, + ], + }, + }, + }; + + const { body: result } = await uptimeEsClient.search({ body: lastSuccessCheckParams }); + + if (result?.hits?.total.value < 1) { + return null; + } + + const step = result?.hits.hits[0]._source as Ping & { '@timestamp': string }; + + return { + ...step, + timestamp: step['@timestamp'], + }; +}; diff --git a/x-pack/plugins/uptime/server/lib/requests/index.ts b/x-pack/plugins/uptime/server/lib/requests/index.ts index 9e665fb8bbdb0..24109245c2902 100644 --- a/x-pack/plugins/uptime/server/lib/requests/index.ts +++ b/x-pack/plugins/uptime/server/lib/requests/index.ts @@ -24,6 +24,7 @@ import { getJourneyScreenshot } from './get_journey_screenshot'; import { getJourneyDetails } from './get_journey_details'; import { getNetworkEvents } from './get_network_events'; import { getJourneyFailedSteps } from './get_journey_failed_steps'; +import { getStepLastSuccessfulStep } from './get_last_successful_step'; export const requests = { getCerts, @@ -42,6 +43,7 @@ export const requests = { getIndexStatus, getJourneySteps, getJourneyFailedSteps, + getStepLastSuccessfulStep, getJourneyScreenshot, getJourneyDetails, getNetworkEvents, diff --git a/x-pack/plugins/uptime/server/rest_api/index.ts b/x-pack/plugins/uptime/server/rest_api/index.ts index 41556d3c8d513..91b5597321ed0 100644 --- a/x-pack/plugins/uptime/server/rest_api/index.ts +++ b/x-pack/plugins/uptime/server/rest_api/index.ts @@ -27,6 +27,7 @@ import { createGetMonitorDurationRoute } from './monitors/monitors_durations'; import { createGetIndexPatternRoute, createGetIndexStatusRoute } from './index_state'; import { createNetworkEventsRoute } from './network_events'; import { createJourneyFailedStepsRoute } from './pings/journeys'; +import { createLastSuccessfulStepRoute } from './synthetics/last_successful_step'; export * from './types'; export { createRouteWithAuth } from './create_route_with_auth'; @@ -52,4 +53,5 @@ export const restApiRoutes: UMRestApiRouteFactory[] = [ createJourneyScreenshotRoute, createNetworkEventsRoute, createJourneyFailedStepsRoute, + createLastSuccessfulStepRoute, ]; diff --git a/x-pack/plugins/uptime/server/rest_api/pings/journey_screenshots.ts b/x-pack/plugins/uptime/server/rest_api/pings/journey_screenshots.ts index cda078da01539..2b056498d7f10 100644 --- a/x-pack/plugins/uptime/server/rest_api/pings/journey_screenshots.ts +++ b/x-pack/plugins/uptime/server/rest_api/pings/journey_screenshots.ts @@ -18,6 +18,9 @@ export const createJourneyScreenshotRoute: UMRestApiRouteFactory = (libs: UMServ stepIndex: schema.number(), _debug: schema.maybe(schema.boolean()), }), + query: schema.object({ + _debug: schema.maybe(schema.boolean()), + }), }, handler: async ({ uptimeEsClient, request, response }) => { const { checkGroup, stepIndex } = request.params; @@ -28,7 +31,7 @@ export const createJourneyScreenshotRoute: UMRestApiRouteFactory = (libs: UMServ stepIndex, }); - if (result === null) { + if (result === null || !result.blob) { return response.notFound(); } return response.ok({ diff --git a/x-pack/plugins/uptime/server/rest_api/pings/journeys.ts b/x-pack/plugins/uptime/server/rest_api/pings/journeys.ts index def373e88ae16..9b5bffc380c27 100644 --- a/x-pack/plugins/uptime/server/rest_api/pings/journeys.ts +++ b/x-pack/plugins/uptime/server/rest_api/pings/journeys.ts @@ -15,7 +15,6 @@ export const createJourneyRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => validate: { params: schema.object({ checkGroup: schema.string(), - _debug: schema.maybe(schema.boolean()), }), query: schema.object({ // provides a filter for the types of synthetic events to include @@ -23,21 +22,24 @@ export const createJourneyRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => syntheticEventTypes: schema.maybe( schema.oneOf([schema.arrayOf(schema.string()), schema.string()]) ), + _debug: schema.maybe(schema.boolean()), }), }, handler: async ({ uptimeEsClient, request }): Promise => { const { checkGroup } = request.params; const { syntheticEventTypes } = request.query; - const result = await libs.requests.getJourneySteps({ - uptimeEsClient, - checkGroup, - syntheticEventTypes, - }); - const details = await libs.requests.getJourneyDetails({ - uptimeEsClient, - checkGroup, - }); + const [result, details] = await Promise.all([ + await libs.requests.getJourneySteps({ + uptimeEsClient, + checkGroup, + syntheticEventTypes, + }), + await libs.requests.getJourneyDetails({ + uptimeEsClient, + checkGroup, + }), + ]); return { checkGroup, @@ -53,6 +55,7 @@ export const createJourneyFailedStepsRoute: UMRestApiRouteFactory = (libs: UMSer validate: { query: schema.object({ checkGroups: schema.arrayOf(schema.string()), + _debug: schema.maybe(schema.boolean()), }), }, handler: async ({ uptimeEsClient, request }): Promise => { diff --git a/x-pack/plugins/uptime/server/rest_api/synthetics/last_successful_step.ts b/x-pack/plugins/uptime/server/rest_api/synthetics/last_successful_step.ts new file mode 100644 index 0000000000000..a1523fae9d4a1 --- /dev/null +++ b/x-pack/plugins/uptime/server/rest_api/synthetics/last_successful_step.ts @@ -0,0 +1,33 @@ +/* + * 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 { UMServerLibs } from '../../lib/lib'; +import { UMRestApiRouteFactory } from '../types'; + +export const createLastSuccessfulStepRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ + method: 'GET', + path: '/api/uptime/synthetics/step/success/', + validate: { + query: schema.object({ + monitorId: schema.string(), + stepIndex: schema.number(), + timestamp: schema.string(), + _debug: schema.maybe(schema.boolean()), + }), + }, + handler: async ({ uptimeEsClient, request, response }) => { + const { timestamp, monitorId, stepIndex } = request.query; + + return await libs.requests.getStepLastSuccessfulStep({ + uptimeEsClient, + monitorId, + stepIndex, + timestamp, + }); + }, +});