[Synthetics] Support synthetics dedupe strategy in Uptime app (#101678)
* Add new runtime types for parsing on client/server. * Add more runtime types. * Remove dead code. * Mark parameter as unused. * Improve typing for failed journey request function. * Add new API functions, improve typing in a few others. * Modify API calls to work with new screenshot_ref data. * Fix untested refactor error. * Add required fields to runtime type. * Update typing in failed steps component. * Adapt client to work with old screenshots as well as new screenshot_ref. * Refactor composite code to reusable hook. * Implement screenshot blocks endpoint. * Define runtime types for full-size screenshots. * Delete dedicated screenshot and ref queries. * Optimize screenshot endpoint by combining queries. * Handle parsing error. * Clean up screenshot/ref typings. * Remove dead types. DRY a type out. * Simplify types. * Improve typing in step screenshot components. * Prefer PNG to JPG for canvas composite op. * Simplify and clean up some code. * Remove reliance on `Ping` type, clean up types. * Add a comment. * Add a comment. * Fix typing for `FailedStep` component. * Standardize loading spinner sizes. * Add comments to composite code. * Remove unnecessary optional chaining. * Reformat error string. * Remove unneeded key from request return object. * Add a comment to a return object explaining very large cache value. * Make type annotation more accurate. * Resolve some type and test errors. * Clean up remaining type errors. * Move type definitions to simplify imports. * Simplify `PingTimestamp` interface. * Refactor failing unit test to use RTL and actually test things. * Add tests for new helper functions. * Add a comment. * Test `PingTimestamp` for screenshot ref data. * Test `StepImageCaption` for ref data. * Improve typing for step list column definitions. * Harden a test. * Extract code to avoid repeated declarations. * Create centralized mock for `useCompositeImage`. * Add test for ref to `StepScreenshotDisplay`. * Add tests for `getJourneyDetails`. * Extract search results wrapper to helper lib. * Add tests for `getJourneyFailedSteps`. * Add support for aggs to result helper wrapper. * Write tests for `getJourneyScreenshot` and simplify type checking. * Write tests for `getJourneyScreenshotBlocks`. * Simplify prop types for `FailedStep`. * Remove unused type. * Fix regression in step navigating for new style screenshots. * Implement PR feedback. * Implement PR feedback. * Implement PR feedback. * Reduce limit of screenshot block queries from 10k to 1k. * Remove redundant field selection from ES query. * Implement PR feedback. * Fix regression that caused "Last successful step" to not show an image. * Delete unused props from `Ping` runtime type. * More precise naming. * Naming improvements. Add `useCallback` to prevent callback re-declaration. * Prefer explicit props to `{...spread}` syntax. * Remove redundant type checking. * Delete obsolete unit tests. * Fix a regression. * Add effect to `useEffect`.
This commit is contained in:
parent
39ba747728
commit
0c8d5e8f89
|
@ -1,31 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
|
||||
// IO type for validation
|
||||
export const PingErrorType = t.intersection([
|
||||
t.partial({
|
||||
code: t.string,
|
||||
id: t.string,
|
||||
stack_trace: t.string,
|
||||
type: t.string,
|
||||
}),
|
||||
t.type({
|
||||
// this is _always_ on the error field
|
||||
message: t.string,
|
||||
}),
|
||||
]);
|
||||
|
||||
// Typescript type for type checking
|
||||
export type PingError = t.TypeOf<typeof PingErrorType>;
|
||||
|
||||
export const MonitorDetailsType = t.intersection([
|
||||
t.type({ monitorId: t.string }),
|
||||
t.partial({ error: PingErrorType, timestamp: t.string, alerts: t.unknown }),
|
||||
]);
|
||||
export type MonitorDetails = t.TypeOf<typeof MonitorDetailsType>;
|
|
@ -5,6 +5,5 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './details';
|
||||
export * from './locations';
|
||||
export * from './state';
|
||||
|
|
|
@ -7,3 +7,4 @@
|
|||
|
||||
export * from './histogram';
|
||||
export * from './ping';
|
||||
export * from './synthetics';
|
||||
|
|
|
@ -7,7 +7,29 @@
|
|||
|
||||
import * as t from 'io-ts';
|
||||
import { DateRangeType } from '../common';
|
||||
import { PingErrorType } from '../monitor';
|
||||
|
||||
// IO type for validation
|
||||
export const PingErrorType = t.intersection([
|
||||
t.partial({
|
||||
code: t.string,
|
||||
id: t.string,
|
||||
stack_trace: t.string,
|
||||
type: t.string,
|
||||
}),
|
||||
t.type({
|
||||
// this is _always_ on the error field
|
||||
message: t.string,
|
||||
}),
|
||||
]);
|
||||
|
||||
// Typescript type for type checking
|
||||
export type PingError = t.TypeOf<typeof PingErrorType>;
|
||||
|
||||
export const MonitorDetailsType = t.intersection([
|
||||
t.type({ monitorId: t.string }),
|
||||
t.partial({ error: PingErrorType, timestamp: t.string, alerts: t.unknown }),
|
||||
]);
|
||||
export type MonitorDetails = t.TypeOf<typeof MonitorDetailsType>;
|
||||
|
||||
export const HttpResponseBodyType = t.partial({
|
||||
bytes: t.number,
|
||||
|
@ -193,10 +215,6 @@ export const PingType = t.intersection([
|
|||
name: t.string,
|
||||
}),
|
||||
type: t.string,
|
||||
// ui-related field
|
||||
screenshotLoading: t.boolean,
|
||||
// ui-related field
|
||||
screenshotExists: t.boolean,
|
||||
blob: t.string,
|
||||
blob_mime: t.string,
|
||||
payload: t.partial({
|
||||
|
@ -242,36 +260,6 @@ export const PingType = t.intersection([
|
|||
}),
|
||||
]);
|
||||
|
||||
export const SyntheticsJourneyApiResponseType = t.intersection([
|
||||
t.type({
|
||||
checkGroup: t.string,
|
||||
steps: t.array(PingType),
|
||||
}),
|
||||
t.partial({
|
||||
details: t.union([
|
||||
t.intersection([
|
||||
t.type({
|
||||
timestamp: t.string,
|
||||
journey: PingType,
|
||||
}),
|
||||
t.partial({
|
||||
next: t.type({
|
||||
timestamp: t.string,
|
||||
checkGroup: t.string,
|
||||
}),
|
||||
previous: t.type({
|
||||
timestamp: t.string,
|
||||
checkGroup: t.string,
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
t.null,
|
||||
]),
|
||||
}),
|
||||
]);
|
||||
|
||||
export type SyntheticsJourneyApiResponse = t.TypeOf<typeof SyntheticsJourneyApiResponseType>;
|
||||
|
||||
export type Ping = t.TypeOf<typeof PingType>;
|
||||
|
||||
// Convenience function for tests etc that makes an empty ping
|
||||
|
|
|
@ -0,0 +1,150 @@
|
|||
/*
|
||||
* 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 {
|
||||
isRefResult,
|
||||
isFullScreenshot,
|
||||
isScreenshotRef,
|
||||
isScreenshotImageBlob,
|
||||
RefResult,
|
||||
FullScreenshot,
|
||||
ScreenshotImageBlob,
|
||||
ScreenshotRefImageData,
|
||||
} from './synthetics';
|
||||
|
||||
describe('synthetics runtime types', () => {
|
||||
let refResult: RefResult;
|
||||
let fullScreenshot: FullScreenshot;
|
||||
let screenshotImageBlob: ScreenshotImageBlob;
|
||||
let screenshotRef: ScreenshotRefImageData;
|
||||
|
||||
beforeEach(() => {
|
||||
refResult = {
|
||||
'@timestamp': '123',
|
||||
monitor: {
|
||||
check_group: 'check-group',
|
||||
},
|
||||
screenshot_ref: {
|
||||
width: 1200,
|
||||
height: 900,
|
||||
blocks: [
|
||||
{
|
||||
hash: 'hash1',
|
||||
top: 0,
|
||||
left: 0,
|
||||
height: 120,
|
||||
width: 90,
|
||||
},
|
||||
{
|
||||
hash: 'hash2',
|
||||
top: 0,
|
||||
left: 90,
|
||||
height: 120,
|
||||
width: 90,
|
||||
},
|
||||
],
|
||||
},
|
||||
synthetics: {
|
||||
package_version: 'v1',
|
||||
step: {
|
||||
name: 'step name',
|
||||
index: 0,
|
||||
},
|
||||
type: 'step/screenshot_ref',
|
||||
},
|
||||
};
|
||||
|
||||
fullScreenshot = {
|
||||
synthetics: {
|
||||
blob: 'image data',
|
||||
blob_mime: 'image/jpeg',
|
||||
step: {
|
||||
name: 'step name',
|
||||
},
|
||||
type: 'step/screenshot',
|
||||
},
|
||||
};
|
||||
|
||||
screenshotImageBlob = {
|
||||
stepName: null,
|
||||
maxSteps: 1,
|
||||
src: 'image data',
|
||||
};
|
||||
|
||||
screenshotRef = {
|
||||
stepName: null,
|
||||
maxSteps: 1,
|
||||
ref: {
|
||||
screenshotRef: refResult,
|
||||
blocks: [
|
||||
{
|
||||
id: 'hash1',
|
||||
synthetics: {
|
||||
blob: 'image data',
|
||||
blob_mime: 'image/jpeg',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'hash2',
|
||||
synthetics: {
|
||||
blob: 'image data',
|
||||
blob_mime: 'image/jpeg',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe('isRefResult', () => {
|
||||
it('identifies refs correctly', () => {
|
||||
expect(isRefResult(refResult)).toBe(true);
|
||||
});
|
||||
|
||||
it('fails objects that do not correspond to the type', () => {
|
||||
expect(isRefResult(fullScreenshot)).toBe(false);
|
||||
expect(isRefResult(screenshotRef)).toBe(false);
|
||||
expect(isRefResult(screenshotImageBlob)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isScreenshot', () => {
|
||||
it('identifies screenshot objects correctly', () => {
|
||||
expect(isFullScreenshot(fullScreenshot)).toBe(true);
|
||||
});
|
||||
|
||||
it('fails objects that do not correspond to the type', () => {
|
||||
expect(isFullScreenshot(refResult)).toBe(false);
|
||||
expect(isFullScreenshot(screenshotRef)).toBe(false);
|
||||
expect(isFullScreenshot(screenshotImageBlob)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isScreenshotImageBlob', () => {
|
||||
it('identifies screenshot image blob objects correctly', () => {
|
||||
expect(isScreenshotImageBlob(screenshotImageBlob)).toBe(true);
|
||||
});
|
||||
|
||||
it('fails objects that do not correspond to the type', () => {
|
||||
expect(isScreenshotImageBlob(refResult)).toBe(false);
|
||||
expect(isScreenshotImageBlob(screenshotRef)).toBe(false);
|
||||
expect(isScreenshotImageBlob(fullScreenshot)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isScreenshotRef', () => {
|
||||
it('identifies screenshot ref objects correctly', () => {
|
||||
expect(isScreenshotRef(screenshotRef)).toBe(true);
|
||||
});
|
||||
|
||||
it('fails objects that do not correspond to the type', () => {
|
||||
expect(isScreenshotRef(refResult)).toBe(false);
|
||||
expect(isScreenshotRef(fullScreenshot)).toBe(false);
|
||||
expect(isScreenshotRef(screenshotImageBlob)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
207
x-pack/plugins/uptime/common/runtime_types/ping/synthetics.ts
Normal file
207
x-pack/plugins/uptime/common/runtime_types/ping/synthetics.ts
Normal file
|
@ -0,0 +1,207 @@
|
|||
/*
|
||||
* 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 { isRight } from 'fp-ts/lib/Either';
|
||||
import * as t from 'io-ts';
|
||||
|
||||
/**
|
||||
* This type has some overlap with the Ping type, but it helps avoid runtime type
|
||||
* check failures and removes a lot of unnecessary fields that our Synthetics UI code
|
||||
* does not care about.
|
||||
*/
|
||||
export const JourneyStepType = t.intersection([
|
||||
t.partial({
|
||||
monitor: t.partial({
|
||||
duration: t.type({
|
||||
us: t.number,
|
||||
}),
|
||||
name: t.string,
|
||||
status: t.string,
|
||||
type: t.string,
|
||||
}),
|
||||
synthetics: t.partial({
|
||||
error: t.partial({
|
||||
message: t.string,
|
||||
stack: t.string,
|
||||
}),
|
||||
payload: t.partial({
|
||||
message: t.string,
|
||||
source: t.string,
|
||||
status: t.string,
|
||||
text: t.string,
|
||||
}),
|
||||
step: t.type({
|
||||
index: t.number,
|
||||
name: t.string,
|
||||
}),
|
||||
isFullScreenshot: t.boolean,
|
||||
isScreenshotRef: t.boolean,
|
||||
}),
|
||||
}),
|
||||
t.type({
|
||||
_id: t.string,
|
||||
'@timestamp': t.string,
|
||||
monitor: t.type({
|
||||
id: t.string,
|
||||
check_group: t.string,
|
||||
}),
|
||||
synthetics: t.type({
|
||||
type: t.string,
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
|
||||
export type JourneyStep = t.TypeOf<typeof JourneyStepType>;
|
||||
|
||||
export const FailedStepsApiResponseType = t.type({
|
||||
checkGroups: t.array(t.string),
|
||||
steps: t.array(JourneyStepType),
|
||||
});
|
||||
|
||||
export type FailedStepsApiResponse = t.TypeOf<typeof FailedStepsApiResponseType>;
|
||||
|
||||
/**
|
||||
* The individual screenshot blocks Synthetics uses to reduce disk footprint.
|
||||
*/
|
||||
export const ScreenshotBlockType = t.type({
|
||||
hash: t.string,
|
||||
top: t.number,
|
||||
left: t.number,
|
||||
height: t.number,
|
||||
width: t.number,
|
||||
});
|
||||
|
||||
/**
|
||||
* The old style of screenshot document that contains a full screenshot blob.
|
||||
*/
|
||||
export const FullScreenshotType = t.type({
|
||||
synthetics: t.intersection([
|
||||
t.partial({
|
||||
blob: t.string,
|
||||
}),
|
||||
t.type({
|
||||
blob: t.string,
|
||||
blob_mime: t.string,
|
||||
step: t.type({
|
||||
name: t.string,
|
||||
}),
|
||||
type: t.literal('step/screenshot'),
|
||||
}),
|
||||
]),
|
||||
});
|
||||
|
||||
export type FullScreenshot = t.TypeOf<typeof FullScreenshotType>;
|
||||
|
||||
export function isFullScreenshot(data: unknown): data is FullScreenshot {
|
||||
return isRight(FullScreenshotType.decode(data));
|
||||
}
|
||||
|
||||
/**
|
||||
* The ref used by synthetics to organize the blocks needed to recompose a
|
||||
* fragmented image.
|
||||
*/
|
||||
export const RefResultType = t.type({
|
||||
'@timestamp': t.string,
|
||||
monitor: t.type({
|
||||
check_group: t.string,
|
||||
}),
|
||||
screenshot_ref: t.type({
|
||||
width: t.number,
|
||||
height: t.number,
|
||||
blocks: t.array(ScreenshotBlockType),
|
||||
}),
|
||||
synthetics: t.type({
|
||||
package_version: t.string,
|
||||
step: t.type({
|
||||
name: t.string,
|
||||
index: t.number,
|
||||
}),
|
||||
type: t.literal('step/screenshot_ref'),
|
||||
}),
|
||||
});
|
||||
|
||||
export type RefResult = t.TypeOf<typeof RefResultType>;
|
||||
|
||||
export function isRefResult(data: unknown): data is RefResult {
|
||||
return isRight(RefResultType.decode(data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the result of querying for the legacy-style full screenshot blob.
|
||||
*/
|
||||
export const ScreenshotImageBlobType = t.type({
|
||||
stepName: t.union([t.null, t.string]),
|
||||
maxSteps: t.number,
|
||||
src: t.string,
|
||||
});
|
||||
|
||||
export type ScreenshotImageBlob = t.TypeOf<typeof ScreenshotImageBlobType>;
|
||||
|
||||
export function isScreenshotImageBlob(data: unknown): data is ScreenshotImageBlob {
|
||||
return isRight(ScreenshotImageBlobType.decode(data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the block blobs stored by hash. These documents are used to recompose synthetics images.
|
||||
*/
|
||||
export const ScreenshotBlockDocType = t.type({
|
||||
id: t.string,
|
||||
synthetics: t.type({
|
||||
blob: t.string,
|
||||
blob_mime: t.string,
|
||||
}),
|
||||
});
|
||||
|
||||
export type ScreenshotBlockDoc = t.TypeOf<typeof ScreenshotBlockDocType>;
|
||||
|
||||
/**
|
||||
* Contains the fields requried by the Synthetics UI when utilizing screenshot refs.
|
||||
*/
|
||||
export const ScreenshotRefImageDataType = t.type({
|
||||
stepName: t.union([t.null, t.string]),
|
||||
maxSteps: t.number,
|
||||
ref: t.type({
|
||||
screenshotRef: RefResultType,
|
||||
blocks: t.array(ScreenshotBlockDocType),
|
||||
}),
|
||||
});
|
||||
|
||||
export type ScreenshotRefImageData = t.TypeOf<typeof ScreenshotRefImageDataType>;
|
||||
|
||||
export function isScreenshotRef(data: unknown): data is ScreenshotRefImageData {
|
||||
return isRight(ScreenshotRefImageDataType.decode(data));
|
||||
}
|
||||
|
||||
export const SyntheticsJourneyApiResponseType = t.intersection([
|
||||
t.type({
|
||||
checkGroup: t.string,
|
||||
steps: t.array(JourneyStepType),
|
||||
}),
|
||||
t.partial({
|
||||
details: t.union([
|
||||
t.intersection([
|
||||
t.type({
|
||||
timestamp: t.string,
|
||||
journey: JourneyStepType,
|
||||
}),
|
||||
t.partial({
|
||||
next: t.type({
|
||||
timestamp: t.string,
|
||||
checkGroup: t.string,
|
||||
}),
|
||||
previous: t.type({
|
||||
timestamp: t.string,
|
||||
checkGroup: t.string,
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
t.null,
|
||||
]),
|
||||
}),
|
||||
]);
|
||||
|
||||
export type SyntheticsJourneyApiResponse = t.TypeOf<typeof SyntheticsJourneyApiResponseType>;
|
|
@ -6,16 +6,16 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Ping, SyntheticsJourneyApiResponse } from '../../../../../common/runtime_types/ping';
|
||||
import { FailedStepsApiResponse } from '../../../../../common/runtime_types/ping/synthetics';
|
||||
|
||||
interface Props {
|
||||
ping: Ping;
|
||||
failedSteps?: SyntheticsJourneyApiResponse;
|
||||
checkGroup?: string;
|
||||
failedSteps?: FailedStepsApiResponse;
|
||||
}
|
||||
|
||||
export const FailedStep = ({ ping, failedSteps }: Props) => {
|
||||
export const FailedStep = ({ checkGroup, failedSteps }: Props) => {
|
||||
const thisFailedStep = failedSteps?.steps?.find(
|
||||
(fs) => fs.monitor.check_group === ping.monitor.check_group
|
||||
(fs) => !!checkGroup && fs.monitor.check_group === checkGroup
|
||||
);
|
||||
|
||||
if (!thisFailedStep) {
|
||||
|
|
|
@ -1,89 +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 { fireEvent, waitFor } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { render } from '../../../../../lib/helper/rtl_helpers';
|
||||
import { NavButtons, NavButtonsProps } from './nav_buttons';
|
||||
|
||||
describe('NavButtons', () => {
|
||||
let defaultProps: NavButtonsProps;
|
||||
|
||||
beforeEach(() => {
|
||||
defaultProps = {
|
||||
maxSteps: 3,
|
||||
stepNumber: 2,
|
||||
setStepNumber: jest.fn(),
|
||||
setIsImagePopoverOpen: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
it('labels prev and next buttons', () => {
|
||||
const { getByLabelText } = render(<NavButtons {...defaultProps} />);
|
||||
|
||||
expect(getByLabelText('Previous step'));
|
||||
expect(getByLabelText('Next step'));
|
||||
});
|
||||
|
||||
it('increments step number on next click', async () => {
|
||||
const { getByLabelText } = render(<NavButtons {...defaultProps} />);
|
||||
|
||||
const nextButton = getByLabelText('Next step');
|
||||
|
||||
fireEvent.click(nextButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(defaultProps.setStepNumber).toHaveBeenCalledTimes(1);
|
||||
expect(defaultProps.setStepNumber).toHaveBeenCalledWith(3);
|
||||
});
|
||||
});
|
||||
|
||||
it('decrements step number on prev click', async () => {
|
||||
const { getByLabelText } = render(<NavButtons {...defaultProps} />);
|
||||
|
||||
const nextButton = getByLabelText('Previous step');
|
||||
|
||||
fireEvent.click(nextButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(defaultProps.setStepNumber).toHaveBeenCalledTimes(1);
|
||||
expect(defaultProps.setStepNumber).toHaveBeenCalledWith(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('disables `next` button on final step', () => {
|
||||
defaultProps.stepNumber = 3;
|
||||
|
||||
const { getByLabelText } = render(<NavButtons {...defaultProps} />);
|
||||
|
||||
// getByLabelText('Next step');
|
||||
expect(getByLabelText('Next step')).toHaveAttribute('disabled');
|
||||
expect(getByLabelText('Previous step')).not.toHaveAttribute('disabled');
|
||||
});
|
||||
|
||||
it('disables `prev` button on final step', () => {
|
||||
defaultProps.stepNumber = 1;
|
||||
|
||||
const { getByLabelText } = render(<NavButtons {...defaultProps} />);
|
||||
|
||||
expect(getByLabelText('Next step')).not.toHaveAttribute('disabled');
|
||||
expect(getByLabelText('Previous step')).toHaveAttribute('disabled');
|
||||
});
|
||||
|
||||
it('opens popover when mouse enters', async () => {
|
||||
const { getByLabelText } = render(<NavButtons {...defaultProps} />);
|
||||
|
||||
const nextButton = getByLabelText('Next step');
|
||||
|
||||
fireEvent.mouseEnter(nextButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(defaultProps.setIsImagePopoverOpen).toHaveBeenCalledTimes(1);
|
||||
expect(defaultProps.setIsImagePopoverOpen).toHaveBeenCalledWith(true);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,59 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiButtonIcon, EuiFlexItem, EuiFlexGroup } from '@elastic/eui';
|
||||
import React, { MouseEvent } from 'react';
|
||||
import { nextAriaLabel, prevAriaLabel } from './translations';
|
||||
|
||||
export interface NavButtonsProps {
|
||||
maxSteps?: number;
|
||||
setIsImagePopoverOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setStepNumber: React.Dispatch<React.SetStateAction<number>>;
|
||||
stepNumber: number;
|
||||
}
|
||||
|
||||
export const NavButtons: React.FC<NavButtonsProps> = ({
|
||||
maxSteps,
|
||||
setIsImagePopoverOpen,
|
||||
setStepNumber,
|
||||
stepNumber,
|
||||
}) => (
|
||||
<EuiFlexGroup
|
||||
className="stepArrows"
|
||||
gutterSize="s"
|
||||
alignItems="center"
|
||||
onMouseEnter={() => setIsImagePopoverOpen(true)}
|
||||
style={{ position: 'absolute', bottom: 0, left: 30 }}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
disabled={stepNumber === 1}
|
||||
color="subdued"
|
||||
size="s"
|
||||
onClick={(evt: MouseEvent<HTMLButtonElement>) => {
|
||||
setStepNumber(stepNumber - 1);
|
||||
evt.stopPropagation();
|
||||
}}
|
||||
iconType="arrowLeft"
|
||||
aria-label={prevAriaLabel}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
disabled={stepNumber === maxSteps}
|
||||
color="subdued"
|
||||
size="s"
|
||||
onClick={(evt: MouseEvent<HTMLButtonElement>) => {
|
||||
setStepNumber(stepNumber + 1);
|
||||
evt.stopPropagation();
|
||||
}}
|
||||
iconType="arrowRight"
|
||||
aria-label={nextAriaLabel}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
|
@ -27,7 +27,7 @@ export const NoImageDisplay: React.FC<NoImageDisplayProps> = ({
|
|||
{isLoading || isPending ? (
|
||||
<EuiLoadingSpinner
|
||||
aria-label={imageLoadingSpinnerAriaLabel}
|
||||
size="xl"
|
||||
size="l"
|
||||
data-test-subj="pingTimestampSpinner"
|
||||
/>
|
||||
) : (
|
||||
|
|
|
@ -10,10 +10,11 @@ import { fireEvent, waitFor } from '@testing-library/react';
|
|||
import { PingTimestamp } from './ping_timestamp';
|
||||
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';
|
||||
import '../../../../../lib/__mocks__/use_composite_image.mock';
|
||||
import { mockRef } from '../../../../../lib/__mocks__/screenshot_ref.mock';
|
||||
|
||||
mockReduxHooks();
|
||||
|
||||
|
@ -27,40 +28,13 @@ jest.mock('../../../../../../../observability/public', () => {
|
|||
});
|
||||
|
||||
describe('Ping Timestamp component', () => {
|
||||
let response: Ping;
|
||||
let checkGroup: string;
|
||||
let timestamp: string;
|
||||
const { FETCH_STATUS } = observabilityPublic;
|
||||
|
||||
beforeAll(() => {
|
||||
response = {
|
||||
ecs: { version: '1.6.0' },
|
||||
agent: {
|
||||
ephemeral_id: '52ce1110-464f-4d74-b94c-3c051bf12589',
|
||||
id: '3ebcd3c2-f5c3-499e-8d86-80f98e5f4c08',
|
||||
name: 'docker-desktop',
|
||||
type: 'heartbeat',
|
||||
version: '7.10.0',
|
||||
hostname: 'docker-desktop',
|
||||
},
|
||||
monitor: {
|
||||
status: 'up',
|
||||
check_group: 'f58a484f-2ffb-11eb-9b35-025000000001',
|
||||
duration: { us: 1528598 },
|
||||
id: 'basic addition and completion of single task',
|
||||
name: 'basic addition and completion of single task',
|
||||
type: 'browser',
|
||||
timespan: { lt: '2020-11-26T15:29:56.820Z', gte: '2020-11-26T15:28:56.820Z' },
|
||||
},
|
||||
url: {
|
||||
full: 'file:///opt/elastic-synthetics/examples/todos/app/index.html',
|
||||
scheme: 'file',
|
||||
domain: '',
|
||||
path: '/opt/elastic-synthetics/examples/todos/app/index.html',
|
||||
},
|
||||
synthetics: { type: 'heartbeat/summary' },
|
||||
summary: { up: 1, down: 0 },
|
||||
timestamp: '2020-11-26T15:28:56.896Z',
|
||||
docId: '0WErBXYB0mvWTKLO-yQm',
|
||||
};
|
||||
checkGroup = 'f58a484f-2ffb-11eb-9b35-025000000001';
|
||||
timestamp = '2020-11-26T15:28:56.896Z';
|
||||
});
|
||||
|
||||
it.each([[FETCH_STATUS.PENDING], [FETCH_STATUS.LOADING]])(
|
||||
|
@ -70,7 +44,7 @@ describe('Ping Timestamp component', () => {
|
|||
.spyOn(observabilityPublic, 'useFetcher')
|
||||
.mockReturnValue({ status: fetchStatus, data: null, refetch: () => null });
|
||||
const { getByTestId } = render(
|
||||
<PingTimestamp ping={response} label={getShortTimeStamp(moment(response.timestamp))} />
|
||||
<PingTimestamp checkGroup={checkGroup} label={getShortTimeStamp(moment(timestamp))} />
|
||||
);
|
||||
expect(getByTestId('pingTimestampSpinner')).toBeInTheDocument();
|
||||
}
|
||||
|
@ -81,7 +55,7 @@ describe('Ping Timestamp component', () => {
|
|||
.spyOn(observabilityPublic, 'useFetcher')
|
||||
.mockReturnValue({ status: FETCH_STATUS.SUCCESS, data: null, refetch: () => null });
|
||||
const { getByTestId } = render(
|
||||
<PingTimestamp ping={response} label={getShortTimeStamp(moment(response.timestamp))} />
|
||||
<PingTimestamp checkGroup={checkGroup} label={getShortTimeStamp(moment(timestamp))} />
|
||||
);
|
||||
expect(getByTestId('pingTimestampNoImageAvailable')).toBeInTheDocument();
|
||||
});
|
||||
|
@ -90,11 +64,11 @@ describe('Ping Timestamp component', () => {
|
|||
const src = 'http://sample.com/sampleImageSrc.png';
|
||||
jest.spyOn(observabilityPublic, 'useFetcher').mockReturnValue({
|
||||
status: FETCH_STATUS.SUCCESS,
|
||||
data: { src },
|
||||
data: { maxSteps: 2, stepName: 'test', src },
|
||||
refetch: () => null,
|
||||
});
|
||||
const { container } = render(
|
||||
<PingTimestamp ping={response} label={getShortTimeStamp(moment(response.timestamp))} />
|
||||
<PingTimestamp checkGroup={checkGroup} label={getShortTimeStamp(moment(timestamp))} />
|
||||
);
|
||||
expect(container.querySelector('img')?.src).toBe(src);
|
||||
});
|
||||
|
@ -103,12 +77,13 @@ describe('Ping Timestamp component', () => {
|
|||
const src = 'http://sample.com/sampleImageSrc.png';
|
||||
jest.spyOn(observabilityPublic, 'useFetcher').mockReturnValue({
|
||||
status: FETCH_STATUS.SUCCESS,
|
||||
data: { src },
|
||||
data: { maxSteps: 1, stepName: null, src },
|
||||
refetch: () => null,
|
||||
});
|
||||
const { getByAltText, getAllByText, queryByAltText } = render(
|
||||
<PingTimestamp ping={response} label={getShortTimeStamp(moment(response.timestamp))} />
|
||||
<PingTimestamp checkGroup={checkGroup} label={getShortTimeStamp(moment(timestamp))} />
|
||||
);
|
||||
|
||||
const caption = getAllByText('Nov 26, 2020 10:28:56 AM');
|
||||
fireEvent.mouseEnter(caption[0]);
|
||||
|
||||
|
@ -120,4 +95,29 @@ describe('Ping Timestamp component', () => {
|
|||
|
||||
await waitFor(() => expect(queryByAltText(altText)).toBeNull());
|
||||
});
|
||||
|
||||
it('handles screenshot ref data', async () => {
|
||||
jest.spyOn(observabilityPublic, 'useFetcher').mockReturnValue({
|
||||
status: FETCH_STATUS.SUCCESS,
|
||||
data: mockRef,
|
||||
refetch: () => null,
|
||||
});
|
||||
|
||||
const { getByAltText, getByText, getByRole, getAllByText, queryByAltText } = render(
|
||||
<PingTimestamp checkGroup={checkGroup} label={getShortTimeStamp(moment(timestamp))} />
|
||||
);
|
||||
|
||||
await waitFor(() => getByRole('img'));
|
||||
const caption = getAllByText('Nov 26, 2020 10:28:56 AM');
|
||||
fireEvent.mouseEnter(caption[0]);
|
||||
|
||||
const altText = `A larger version of the screenshot for this journey step's thumbnail.`;
|
||||
|
||||
await waitFor(() => getByAltText(altText));
|
||||
|
||||
fireEvent.mouseLeave(caption[0]);
|
||||
|
||||
await waitFor(() => expect(queryByAltText(altText)).toBeNull());
|
||||
expect(getByText('Step: 1 of 1'));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,7 +9,11 @@ import React, { useContext, useEffect, useState } from 'react';
|
|||
import useIntersection from 'react-use/lib/useIntersection';
|
||||
import styled from 'styled-components';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { Ping } from '../../../../../../common/runtime_types/ping';
|
||||
import {
|
||||
isScreenshotImageBlob,
|
||||
isScreenshotRef,
|
||||
ScreenshotRefImageData,
|
||||
} from '../../../../../../common/runtime_types/ping';
|
||||
import { useFetcher, FETCH_STATUS } from '../../../../../../../observability/public';
|
||||
import { getJourneyScreenshot } from '../../../../../state/api/journey';
|
||||
import { UptimeSettingsContext } from '../../../../../contexts';
|
||||
|
@ -27,12 +31,12 @@ const StepDiv = styled.div`
|
|||
`;
|
||||
|
||||
interface Props {
|
||||
checkGroup?: string;
|
||||
label?: string;
|
||||
ping: Ping;
|
||||
initialStepNo?: number;
|
||||
}
|
||||
|
||||
export const PingTimestamp = ({ label, ping, initialStepNo = 1 }: Props) => {
|
||||
export const PingTimestamp = ({ label, checkGroup, initialStepNo = 1 }: Props) => {
|
||||
const [stepNumber, setStepNumber] = useState(initialStepNo);
|
||||
const [isImagePopoverOpen, setIsImagePopoverOpen] = useState(false);
|
||||
|
||||
|
@ -42,7 +46,7 @@ export const PingTimestamp = ({ label, ping, initialStepNo = 1 }: Props) => {
|
|||
|
||||
const { basePath } = useContext(UptimeSettingsContext);
|
||||
|
||||
const imgPath = `${basePath}/api/uptime/journey/screenshot/${ping.monitor.check_group}/${stepNumber}`;
|
||||
const imgPath = `${basePath}/api/uptime/journey/screenshot/${checkGroup}/${stepNumber}`;
|
||||
|
||||
const intersection = useIntersection(intersectionRef, {
|
||||
root: null,
|
||||
|
@ -55,13 +59,19 @@ export const PingTimestamp = ({ label, ping, initialStepNo = 1 }: Props) => {
|
|||
return getJourneyScreenshot(imgPath);
|
||||
}, [intersection?.intersectionRatio, stepNumber]);
|
||||
|
||||
const [screenshotRef, setScreenshotRef] = useState<ScreenshotRefImageData | undefined>(undefined);
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
if (isScreenshotRef(data)) {
|
||||
setScreenshotRef(data);
|
||||
} else if (isScreenshotImageBlob(data)) {
|
||||
setStepImages((prevState) => [...prevState, data?.src]);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const imgSrc = stepImages?.[stepNumber - 1] ?? data?.src;
|
||||
let imgSrc;
|
||||
if (isScreenshotImageBlob(data)) {
|
||||
imgSrc = stepImages?.[stepNumber - 1] ?? data.src;
|
||||
}
|
||||
|
||||
const captionContent = formatCaptionContent(stepNumber, data?.maxSteps);
|
||||
|
||||
|
@ -71,6 +81,7 @@ export const PingTimestamp = ({ label, ping, initialStepNo = 1 }: Props) => {
|
|||
<StepImageCaption
|
||||
captionContent={captionContent}
|
||||
imgSrc={imgSrc}
|
||||
imgRef={screenshotRef}
|
||||
maxSteps={data?.maxSteps}
|
||||
setStepNumber={setStepNumber}
|
||||
stepNumber={stepNumber}
|
||||
|
@ -100,14 +111,16 @@ export const PingTimestamp = ({ label, ping, initialStepNo = 1 }: Props) => {
|
|||
onMouseLeave={() => setIsImagePopoverOpen(false)}
|
||||
ref={intersectionRef}
|
||||
>
|
||||
{imgSrc ? (
|
||||
{(imgSrc || screenshotRef) && (
|
||||
<StepImagePopover
|
||||
captionContent={captionContent}
|
||||
imageCaption={ImageCaption}
|
||||
imgSrc={imgSrc}
|
||||
imgRef={screenshotRef}
|
||||
isImagePopoverOpen={isImagePopoverOpen}
|
||||
/>
|
||||
) : (
|
||||
)}
|
||||
{!imgSrc && !screenshotRef && (
|
||||
<NoImageDisplay
|
||||
imageCaption={ImageCaption}
|
||||
isLoading={status === FETCH_STATUS.LOADING}
|
||||
|
|
|
@ -11,6 +11,7 @@ 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';
|
||||
import { mockRef } from '../../../../../lib/__mocks__/screenshot_ref.mock';
|
||||
|
||||
describe('StepImageCaption', () => {
|
||||
let defaultProps: StepImageCaptionProps;
|
||||
|
@ -91,4 +92,12 @@ describe('StepImageCaption', () => {
|
|||
|
||||
getByText('test caption content');
|
||||
});
|
||||
|
||||
it('renders caption content for screenshot ref data', async () => {
|
||||
const { getByText } = render(
|
||||
<StepImageCaption {...{ ...defaultProps, imgRef: mockRef, imgSrc: undefined }} />
|
||||
);
|
||||
|
||||
getByText('test caption content');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,10 +9,12 @@ import React, { MouseEvent, useEffect } from 'react';
|
|||
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
|
||||
import { nextAriaLabel, prevAriaLabel } from './translations';
|
||||
import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common';
|
||||
import { ScreenshotRefImageData } from '../../../../../../common/runtime_types';
|
||||
|
||||
export interface StepImageCaptionProps {
|
||||
captionContent: string;
|
||||
imgSrc?: string;
|
||||
imgRef?: ScreenshotRefImageData;
|
||||
maxSteps?: number;
|
||||
setStepNumber: React.Dispatch<React.SetStateAction<number>>;
|
||||
stepNumber: number;
|
||||
|
@ -30,6 +32,7 @@ const ImageCaption = euiStyled.div`
|
|||
|
||||
export const StepImageCaption: React.FC<StepImageCaptionProps> = ({
|
||||
captionContent,
|
||||
imgRef,
|
||||
imgSrc,
|
||||
maxSteps,
|
||||
setStepNumber,
|
||||
|
@ -54,7 +57,7 @@ export const StepImageCaption: React.FC<StepImageCaptionProps> = ({
|
|||
}}
|
||||
>
|
||||
<div className="stepArrowsFullScreen">
|
||||
{imgSrc && (
|
||||
{(imgSrc || imgRef) && (
|
||||
<EuiFlexGroup alignItems="center" justifyContent="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
|
|
|
@ -5,10 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiImage, EuiPopover } from '@elastic/eui';
|
||||
import { EuiImage, EuiLoadingSpinner, EuiPopover } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { ScreenshotRefImageData } from '../../../../../../common/runtime_types/ping/synthetics';
|
||||
import { fullSizeImageAlt } from './translations';
|
||||
import { useCompositeImage } from '../../../../../hooks/use_composite_image';
|
||||
|
||||
const POPOVER_IMG_HEIGHT = 360;
|
||||
const POPOVER_IMG_WIDTH = 640;
|
||||
|
@ -24,40 +26,137 @@ const StepImage = styled(EuiImage)`
|
|||
}
|
||||
}
|
||||
`;
|
||||
|
||||
interface ScreenshotImageProps {
|
||||
captionContent: string;
|
||||
imageCaption: JSX.Element;
|
||||
}
|
||||
|
||||
const DefaultImage: React.FC<ScreenshotImageProps & { imageData?: string }> = ({
|
||||
captionContent,
|
||||
imageCaption,
|
||||
imageData,
|
||||
}) =>
|
||||
imageData ? (
|
||||
<StepImage
|
||||
allowFullScreen={true}
|
||||
alt={captionContent}
|
||||
caption={imageCaption}
|
||||
data-test-subj="pingTimestampImage"
|
||||
hasShadow
|
||||
url={imageData}
|
||||
size="s"
|
||||
className="syntheticsStepImage"
|
||||
/>
|
||||
) : (
|
||||
<EuiLoadingSpinner size="l" />
|
||||
);
|
||||
|
||||
/**
|
||||
* This component provides an intermediate step for composite images. It causes a loading spinner to appear
|
||||
* while the image is being re-assembled, then calls the default image component and provides a data URL for the image.
|
||||
*/
|
||||
const RecomposedScreenshotImage: React.FC<
|
||||
ScreenshotImageProps & {
|
||||
imgRef: ScreenshotRefImageData;
|
||||
setImageData: React.Dispatch<string | undefined>;
|
||||
imageData: string | undefined;
|
||||
}
|
||||
> = ({ captionContent, imageCaption, imageData, imgRef, setImageData }) => {
|
||||
// initially an undefined URL value is passed to the image display, and a loading spinner is rendered.
|
||||
// `useCompositeImage` will call `setUrl` when the image is composited, and the updated `url` will display.
|
||||
useCompositeImage(imgRef, setImageData, imageData);
|
||||
|
||||
return (
|
||||
<DefaultImage
|
||||
captionContent={captionContent}
|
||||
imageCaption={imageCaption}
|
||||
imageData={imageData}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export interface StepImagePopoverProps {
|
||||
captionContent: string;
|
||||
imageCaption: JSX.Element;
|
||||
imgSrc: string;
|
||||
imgSrc?: string;
|
||||
imgRef?: ScreenshotRefImageData;
|
||||
isImagePopoverOpen: boolean;
|
||||
}
|
||||
|
||||
const StepImageComponent: React.FC<
|
||||
Omit<StepImagePopoverProps, 'isImagePopoverOpen'> & {
|
||||
setImageData: React.Dispatch<string | undefined>;
|
||||
imageData: string | undefined;
|
||||
}
|
||||
> = ({ captionContent, imageCaption, imageData, imgRef, imgSrc, setImageData }) => {
|
||||
if (imgSrc) {
|
||||
return (
|
||||
<DefaultImage
|
||||
captionContent={captionContent}
|
||||
imageCaption={imageCaption}
|
||||
imageData={imageData}
|
||||
/>
|
||||
);
|
||||
} else if (imgRef) {
|
||||
return (
|
||||
<RecomposedScreenshotImage
|
||||
captionContent={captionContent}
|
||||
imageCaption={imageCaption}
|
||||
imageData={imageData}
|
||||
imgRef={imgRef}
|
||||
setImageData={setImageData}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const StepImagePopover: React.FC<StepImagePopoverProps> = ({
|
||||
captionContent,
|
||||
imageCaption,
|
||||
imgRef,
|
||||
imgSrc,
|
||||
isImagePopoverOpen,
|
||||
}) => (
|
||||
<EuiPopover
|
||||
anchorPosition="leftDown"
|
||||
button={
|
||||
<StepImage
|
||||
allowFullScreen={true}
|
||||
alt={captionContent}
|
||||
caption={imageCaption}
|
||||
data-test-subj="pingTimestampImage"
|
||||
hasShadow
|
||||
url={imgSrc}
|
||||
size="s"
|
||||
className="syntheticsStepImage"
|
||||
/>
|
||||
}) => {
|
||||
const [imageData, setImageData] = React.useState<string | undefined>(imgSrc || undefined);
|
||||
|
||||
React.useEffect(() => {
|
||||
// for legacy screenshots, when a new image arrives, we must overwrite it
|
||||
if (imgSrc && imgSrc !== imageData) {
|
||||
setImageData(imgSrc);
|
||||
}
|
||||
isOpen={isImagePopoverOpen}
|
||||
closePopover={() => {}}
|
||||
>
|
||||
<EuiImage
|
||||
alt={fullSizeImageAlt}
|
||||
url={imgSrc}
|
||||
style={{ height: POPOVER_IMG_HEIGHT, width: POPOVER_IMG_WIDTH, objectFit: 'contain' }}
|
||||
/>
|
||||
</EuiPopover>
|
||||
);
|
||||
}, [imgSrc, imageData]);
|
||||
|
||||
const setImageDataCallback = React.useCallback(
|
||||
(newImageData: string | undefined) => setImageData(newImageData),
|
||||
[setImageData]
|
||||
);
|
||||
return (
|
||||
<EuiPopover
|
||||
anchorPosition="leftDown"
|
||||
button={
|
||||
<StepImageComponent
|
||||
captionContent={captionContent}
|
||||
imageCaption={imageCaption}
|
||||
imgRef={imgRef}
|
||||
imgSrc={imgSrc}
|
||||
setImageData={setImageDataCallback}
|
||||
imageData={imageData}
|
||||
/>
|
||||
}
|
||||
isOpen={isImagePopoverOpen}
|
||||
closePopover={() => {}}
|
||||
>
|
||||
{imageData ? (
|
||||
<EuiImage
|
||||
alt={fullSizeImageAlt}
|
||||
url={imageData}
|
||||
style={{ height: POPOVER_IMG_HEIGHT, width: POPOVER_IMG_WIDTH, objectFit: 'contain' }}
|
||||
/>
|
||||
) : (
|
||||
<EuiLoadingSpinner size="l" />
|
||||
)}
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -62,7 +62,7 @@ describe('PingList component', () => {
|
|||
...response,
|
||||
error: undefined,
|
||||
loading: false,
|
||||
failedSteps: { steps: [], checkGroup: '1-f-4d-4f' },
|
||||
failedSteps: { steps: [], checkGroups: ['1-f-4d-4f'] },
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -72,7 +72,7 @@ describe('PingList component', () => {
|
|||
total: 0,
|
||||
error: undefined,
|
||||
loading: true,
|
||||
failedSteps: { steps: [], checkGroup: '1-f-4d-4f' },
|
||||
failedSteps: { steps: [], checkGroups: ['1-f-4d-4f'] },
|
||||
});
|
||||
const { getByText } = render(<PingList />);
|
||||
expect(getByText('Loading history...')).toBeInTheDocument();
|
||||
|
@ -84,7 +84,7 @@ describe('PingList component', () => {
|
|||
total: 0,
|
||||
error: undefined,
|
||||
loading: false,
|
||||
failedSteps: { steps: [], checkGroup: '1-f-4d-4f' },
|
||||
failedSteps: { steps: [], checkGroups: ['1-f-4d-4f'] },
|
||||
});
|
||||
const { getByText } = render(<PingList />);
|
||||
expect(getByText('No history found')).toBeInTheDocument();
|
||||
|
|
|
@ -145,7 +145,10 @@ export const PingList = () => {
|
|||
field: 'timestamp',
|
||||
name: TIMESTAMP_LABEL,
|
||||
render: (timestamp: string, item: Ping) => (
|
||||
<PingTimestamp label={getShortTimeStamp(moment(timestamp))} ping={item} />
|
||||
<PingTimestamp
|
||||
checkGroup={item.monitor.check_group}
|
||||
label={getShortTimeStamp(moment(timestamp))}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
@ -185,8 +188,8 @@ export const PingList = () => {
|
|||
name: i18n.translate('xpack.uptime.pingList.columns.failedStep', {
|
||||
defaultMessage: 'Failed step',
|
||||
}),
|
||||
render: (timestamp: string, item: Ping) => (
|
||||
<FailedStep ping={item} failedSteps={failedSteps} />
|
||||
render: (_timestamp: string, item: Ping) => (
|
||||
<FailedStep checkGroup={item.monitor?.check_group} failedSteps={failedSteps} />
|
||||
),
|
||||
},
|
||||
]
|
||||
|
|
|
@ -10,13 +10,19 @@ import { i18n } from '@kbn/i18n';
|
|||
import { useBreadcrumbs } from '../../../../hooks/use_breadcrumbs';
|
||||
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 ActiveStep {
|
||||
monitor: {
|
||||
id: string;
|
||||
name?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface Props {
|
||||
details: JourneyState['details'];
|
||||
activeStep?: Ping;
|
||||
activeStep?: ActiveStep;
|
||||
performanceBreakDownView?: boolean;
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
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 { JourneyStep } from '../../../../../common/runtime_types/ping/synthetics';
|
||||
import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common';
|
||||
|
||||
const LabelLink = euiStyled.div`
|
||||
|
@ -17,7 +17,7 @@ const LabelLink = euiStyled.div`
|
|||
`;
|
||||
|
||||
interface Props {
|
||||
lastSuccessfulStep: Ping;
|
||||
lastSuccessfulStep: JourneyStep;
|
||||
}
|
||||
|
||||
export const ScreenshotLink = ({ lastSuccessfulStep }: Props) => {
|
||||
|
|
|
@ -10,7 +10,7 @@ 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 { JourneyStep } from '../../../../../common/runtime_types/ping/synthetics';
|
||||
import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common';
|
||||
import { useFetcher } from '../../../../../../observability/public';
|
||||
import { fetchLastSuccessfulStep } from '../../../../state/api/journey';
|
||||
|
@ -24,21 +24,22 @@ const Label = euiStyled.div`
|
|||
`;
|
||||
|
||||
interface Props {
|
||||
step: Ping;
|
||||
step: JourneyStep;
|
||||
}
|
||||
|
||||
export const StepScreenshots = ({ step }: Props) => {
|
||||
const isSucceeded = step.synthetics?.payload?.status === 'succeeded';
|
||||
|
||||
const { data: lastSuccessfulStep } = useFetcher(() => {
|
||||
const { data } = useFetcher(() => {
|
||||
if (!isSucceeded) {
|
||||
return fetchLastSuccessfulStep({
|
||||
timestamp: step.timestamp,
|
||||
timestamp: step['@timestamp'],
|
||||
monitorId: step.monitor.id,
|
||||
stepIndex: step.synthetics?.step?.index!,
|
||||
});
|
||||
}
|
||||
}, [step.docId, step.timestamp]);
|
||||
}, [step._id, step['@timestamp']]);
|
||||
const lastSuccessfulStep: JourneyStep | undefined = data;
|
||||
|
||||
return (
|
||||
<EuiFlexGroup>
|
||||
|
@ -59,26 +60,28 @@ export const StepScreenshots = ({ step }: Props) => {
|
|||
</Label>
|
||||
<StepScreenshotDisplay
|
||||
checkGroup={step.monitor.check_group}
|
||||
screenshotExists={step.synthetics?.screenshotExists}
|
||||
isScreenshotRef={!!step.synthetics?.isScreenshotRef}
|
||||
isScreenshotBlob={!!step.synthetics?.isFullScreenshot}
|
||||
stepIndex={step.synthetics?.step?.index}
|
||||
stepName={step.synthetics?.step?.name}
|
||||
lazyLoad={false}
|
||||
/>
|
||||
<EuiSpacer size="xs" />
|
||||
<Label>{getShortTimeStamp(moment(step.timestamp))}</Label>
|
||||
<Label>{getShortTimeStamp(moment(step['@timestamp']))}</Label>
|
||||
</EuiFlexItem>
|
||||
{!isSucceeded && lastSuccessfulStep?.monitor && (
|
||||
<EuiFlexItem>
|
||||
<ScreenshotLink lastSuccessfulStep={lastSuccessfulStep} />
|
||||
<StepScreenshotDisplay
|
||||
checkGroup={lastSuccessfulStep.monitor.check_group}
|
||||
screenshotExists={true}
|
||||
isScreenshotRef={!!lastSuccessfulStep.synthetics?.isScreenshotRef}
|
||||
isScreenshotBlob={!!lastSuccessfulStep.synthetics?.isFullScreenshot}
|
||||
stepIndex={lastSuccessfulStep.synthetics?.step?.index}
|
||||
stepName={lastSuccessfulStep.synthetics?.step?.name}
|
||||
lazyLoad={false}
|
||||
/>
|
||||
<EuiSpacer size="xs" />
|
||||
<Label>{getShortTimeStamp(moment(lastSuccessfulStep.timestamp))}</Label>
|
||||
<Label>{getShortTimeStamp(moment(lastSuccessfulStep['@timestamp']))}</Label>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -7,18 +7,21 @@
|
|||
|
||||
import React from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
|
||||
import { Ping } from '../../../../common/runtime_types/ping';
|
||||
import { JourneyStep } from '../../../../common/runtime_types/ping/synthetics';
|
||||
import { PingTimestamp } from '../../monitor/ping_list/columns/ping_timestamp';
|
||||
|
||||
interface Props {
|
||||
step: Ping;
|
||||
step: JourneyStep;
|
||||
}
|
||||
|
||||
export const StepImage = ({ step }: Props) => {
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<PingTimestamp ping={step} initialStepNo={step.synthetics?.step?.index} />
|
||||
<PingTimestamp
|
||||
checkGroup={step.monitor.check_group}
|
||||
initialStepNo={step.synthetics?.step?.index}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText>{step.synthetics?.step?.name}</EuiText>
|
||||
|
|
|
@ -6,18 +6,18 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Ping } from '../../../../common/runtime_types/ping';
|
||||
import { JourneyStep } from '../../../../common/runtime_types/ping';
|
||||
import { StepsList } from './steps_list';
|
||||
import { render } from '../../../lib/helper/rtl_helpers';
|
||||
|
||||
describe('StepList component', () => {
|
||||
let steps: Ping[];
|
||||
let steps: JourneyStep[];
|
||||
|
||||
beforeEach(() => {
|
||||
steps = [
|
||||
{
|
||||
docId: '1',
|
||||
timestamp: '123',
|
||||
_id: '1',
|
||||
'@timestamp': '123',
|
||||
monitor: {
|
||||
id: 'MON_ID',
|
||||
duration: {
|
||||
|
@ -39,8 +39,8 @@ describe('StepList component', () => {
|
|||
},
|
||||
},
|
||||
{
|
||||
docId: '2',
|
||||
timestamp: '124',
|
||||
_id: '2',
|
||||
'@timestamp': '124',
|
||||
monitor: {
|
||||
id: 'MON_ID',
|
||||
duration: {
|
||||
|
@ -112,8 +112,8 @@ describe('StepList component', () => {
|
|||
it('uses appropriate count when non-step/end steps are included', () => {
|
||||
steps[0].synthetics!.payload!.status = 'succeeded';
|
||||
steps.push({
|
||||
docId: '3',
|
||||
timestamp: '125',
|
||||
_id: '3',
|
||||
'@timestamp': '125',
|
||||
monitor: {
|
||||
id: 'MON_ID',
|
||||
duration: {
|
|
@ -5,11 +5,17 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiBasicTable, EuiButtonIcon, EuiPanel, EuiTitle } from '@elastic/eui';
|
||||
import {
|
||||
EuiBasicTable,
|
||||
EuiBasicTableColumn,
|
||||
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 { JourneyStep } 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';
|
||||
|
@ -23,7 +29,7 @@ export const SpanWithMargin = styled.span`
|
|||
`;
|
||||
|
||||
interface Props {
|
||||
data: Ping[];
|
||||
data: JourneyStep[];
|
||||
error?: Error;
|
||||
loading: boolean;
|
||||
}
|
||||
|
@ -34,7 +40,7 @@ interface StepStatusCount {
|
|||
succeeded: number;
|
||||
}
|
||||
|
||||
function isStepEnd(step: Ping) {
|
||||
function isStepEnd(step: JourneyStep) {
|
||||
return step.synthetics?.type === 'step/end';
|
||||
}
|
||||
|
||||
|
@ -62,7 +68,7 @@ function statusMessage(count: StepStatusCount, loading?: boolean) {
|
|||
});
|
||||
}
|
||||
|
||||
function reduceStepStatus(prev: StepStatusCount, cur: Ping): StepStatusCount {
|
||||
function reduceStepStatus(prev: StepStatusCount, cur: JourneyStep): StepStatusCount {
|
||||
if (cur.synthetics?.payload?.status === 'succeeded') {
|
||||
prev.succeeded += 1;
|
||||
return prev;
|
||||
|
@ -75,15 +81,15 @@ function reduceStepStatus(prev: StepStatusCount, cur: Ping): StepStatusCount {
|
|||
}
|
||||
|
||||
export const StepsList = ({ data, error, loading }: Props) => {
|
||||
const steps = data.filter(isStepEnd);
|
||||
const steps: JourneyStep[] = data.filter(isStepEnd);
|
||||
|
||||
const { expandedRows, toggleExpand } = useExpandedRow({ steps, allPings: data, loading });
|
||||
const { expandedRows, toggleExpand } = useExpandedRow({ steps, allSteps: data, loading });
|
||||
|
||||
const columns: any[] = [
|
||||
const columns: Array<EuiBasicTableColumn<JourneyStep>> = [
|
||||
{
|
||||
field: 'synthetics.payload.status',
|
||||
name: STATUS_LABEL,
|
||||
render: (pingStatus: string, item: Ping) => (
|
||||
render: (pingStatus: string, item) => (
|
||||
<StatusBadge status={pingStatus} stepNo={item.synthetics?.step?.index!} />
|
||||
),
|
||||
},
|
||||
|
@ -91,13 +97,13 @@ export const StepsList = ({ data, error, loading }: Props) => {
|
|||
align: 'left',
|
||||
field: 'timestamp',
|
||||
name: STEP_NAME_LABEL,
|
||||
render: (timestamp: string, item: Ping) => <StepImage step={item} />,
|
||||
render: (_timestamp: string, item) => <StepImage step={item} />,
|
||||
},
|
||||
{
|
||||
align: 'left',
|
||||
field: 'timestamp',
|
||||
name: '',
|
||||
render: (val: string, item: Ping) => (
|
||||
render: (_val: string, item) => (
|
||||
<StepDetailLink
|
||||
checkGroupId={item.monitor.check_group!}
|
||||
stepIndex={item.synthetics?.step?.index!}
|
||||
|
@ -110,20 +116,20 @@ export const StepsList = ({ data, error, loading }: Props) => {
|
|||
align: 'right',
|
||||
width: '24px',
|
||||
isExpander: true,
|
||||
render: (ping: Ping) => {
|
||||
render: (journeyStep: JourneyStep) => {
|
||||
return (
|
||||
<EuiButtonIcon
|
||||
data-test-subj="uptimeStepListExpandBtn"
|
||||
onClick={() => toggleExpand({ ping })}
|
||||
aria-label={expandedRows[ping.docId] ? COLLAPSE_LABEL : EXPAND_LABEL}
|
||||
iconType={expandedRows[ping.docId] ? 'arrowUp' : 'arrowDown'}
|
||||
onClick={() => toggleExpand({ journeyStep })}
|
||||
aria-label={expandedRows[journeyStep._id] ? COLLAPSE_LABEL : EXPAND_LABEL}
|
||||
iconType={expandedRows[journeyStep._id] ? 'arrowUp' : 'arrowDown'}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const getRowProps = (item: Ping) => {
|
||||
const getRowProps = (item: JourneyStep) => {
|
||||
const { monitor } = item;
|
||||
|
||||
return {
|
||||
|
@ -134,7 +140,7 @@ export const StepsList = ({ data, error, loading }: Props) => {
|
|||
|
||||
// we dont want to capture image click event
|
||||
if (targetElem.tagName !== 'IMG' && targetElem.tagName !== 'BUTTON') {
|
||||
toggleExpand({ ping: item });
|
||||
toggleExpand({ journeyStep: item });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -13,7 +13,7 @@ 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 { JourneyStep } from '../../../../common/runtime_types';
|
||||
import { SYNTHETIC_CHECK_STEPS_ROUTE } from '../../../../common/constants';
|
||||
import { COLLAPSE_LABEL, EXPAND_LABEL } from '../translations';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
@ -25,17 +25,16 @@ describe('useExpandedROw', () => {
|
|||
const history = createMemoryHistory({
|
||||
initialEntries: ['/journey/fake-group/steps'],
|
||||
});
|
||||
const steps: Ping[] = [
|
||||
const steps: JourneyStep[] = [
|
||||
{
|
||||
docId: '1',
|
||||
timestamp: '123',
|
||||
_id: '1',
|
||||
'@timestamp': '123',
|
||||
monitor: {
|
||||
id: 'MON_ID',
|
||||
duration: {
|
||||
us: 10,
|
||||
},
|
||||
status: 'down',
|
||||
type: 'browser',
|
||||
check_group: 'fake-group',
|
||||
},
|
||||
synthetics: {
|
||||
|
@ -50,15 +49,14 @@ describe('useExpandedROw', () => {
|
|||
},
|
||||
},
|
||||
{
|
||||
docId: '2',
|
||||
timestamp: '124',
|
||||
_id: '2',
|
||||
'@timestamp': '124',
|
||||
monitor: {
|
||||
id: 'MON_ID',
|
||||
duration: {
|
||||
us: 10,
|
||||
},
|
||||
status: 'down',
|
||||
type: 'browser',
|
||||
check_group: 'fake-group',
|
||||
},
|
||||
synthetics: {
|
||||
|
@ -77,7 +75,7 @@ describe('useExpandedROw', () => {
|
|||
const Component = () => {
|
||||
const { expandedRows, toggleExpand } = useExpandedRow({
|
||||
steps,
|
||||
allPings: steps,
|
||||
allSteps: steps,
|
||||
loading: false,
|
||||
});
|
||||
|
||||
|
@ -86,13 +84,13 @@ describe('useExpandedROw', () => {
|
|||
return (
|
||||
<Route path={SYNTHETIC_CHECK_STEPS_ROUTE}>
|
||||
Step list
|
||||
{steps.map((ping, index) => (
|
||||
{steps.map((journeyStep, index) => (
|
||||
<EuiButtonIcon
|
||||
key={index}
|
||||
data-test-subj={TEST_ID + index}
|
||||
onClick={() => toggleExpand({ ping })}
|
||||
aria-label={expandedRows[ping.docId] ? COLLAPSE_LABEL : EXPAND_LABEL}
|
||||
iconType={expandedRows[ping.docId] ? 'arrowUp' : 'arrowDown'}
|
||||
onClick={() => toggleExpand({ journeyStep })}
|
||||
aria-label={expandedRows[journeyStep._id] ? COLLAPSE_LABEL : EXPAND_LABEL}
|
||||
iconType={expandedRows[journeyStep._id] ? 'arrowUp' : 'arrowDown'}
|
||||
/>
|
||||
))}
|
||||
</Route>
|
||||
|
|
|
@ -8,17 +8,17 @@
|
|||
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';
|
||||
import { JourneyStep } from '../../../../common/runtime_types/ping';
|
||||
|
||||
interface HookProps {
|
||||
loading: boolean;
|
||||
allPings: Ping[];
|
||||
steps: Ping[];
|
||||
allSteps: JourneyStep[];
|
||||
steps: JourneyStep[];
|
||||
}
|
||||
|
||||
type ExpandRowType = Record<string, JSX.Element>;
|
||||
|
||||
export const useExpandedRow = ({ loading, steps, allPings }: HookProps) => {
|
||||
export const useExpandedRow = ({ loading, steps, allSteps }: HookProps) => {
|
||||
const [expandedRows, setExpandedRows] = useState<ExpandRowType>({});
|
||||
// eui table uses index from 0, synthetics uses 1
|
||||
|
||||
|
@ -26,13 +26,13 @@ export const useExpandedRow = ({ loading, steps, allPings }: HookProps) => {
|
|||
|
||||
const getBrowserConsole = useCallback(
|
||||
(index: number) => {
|
||||
return allPings.find(
|
||||
return allSteps.find(
|
||||
(stepF) =>
|
||||
stepF.synthetics?.type === 'journey/browserconsole' &&
|
||||
stepF.synthetics?.step?.index! === index
|
||||
)?.synthetics?.payload?.text;
|
||||
},
|
||||
[allPings]
|
||||
[allSteps]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -60,9 +60,9 @@ export const useExpandedRow = ({ loading, steps, allPings }: HookProps) => {
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [checkGroupId, loading]);
|
||||
|
||||
const toggleExpand = ({ ping }: { ping: Ping }) => {
|
||||
const toggleExpand = ({ journeyStep }: { journeyStep: JourneyStep }) => {
|
||||
// eui table uses index from 0, synthetics uses 1
|
||||
const stepIndex = ping.synthetics?.step?.index! - 1;
|
||||
const stepIndex = journeyStep.synthetics?.step?.index! - 1;
|
||||
|
||||
// If already expanded, collapse
|
||||
if (expandedRows[stepIndex]) {
|
||||
|
@ -74,9 +74,9 @@ export const useExpandedRow = ({ loading, steps, allPings }: HookProps) => {
|
|||
...expandedRows,
|
||||
[stepIndex]: (
|
||||
<ExecutedStep
|
||||
step={ping}
|
||||
step={journeyStep}
|
||||
browserConsole={getBrowserConsole(stepIndex)}
|
||||
index={ping.synthetics?.step?.index!}
|
||||
index={journeyStep.synthetics?.step?.index!}
|
||||
loading={loading}
|
||||
/>
|
||||
),
|
||||
|
|
|
@ -15,9 +15,10 @@ describe('ConsoleEvent component', () => {
|
|||
shallowWithIntl(
|
||||
<ConsoleEvent
|
||||
event={{
|
||||
timestamp: '123',
|
||||
docId: '1',
|
||||
'@timestamp': '123',
|
||||
_id: '1',
|
||||
monitor: {
|
||||
check_group: 'check_group',
|
||||
id: 'MONITOR_ID',
|
||||
duration: {
|
||||
us: 123,
|
||||
|
@ -63,9 +64,10 @@ describe('ConsoleEvent component', () => {
|
|||
shallowWithIntl(
|
||||
<ConsoleEvent
|
||||
event={{
|
||||
timestamp: '123',
|
||||
docId: '1',
|
||||
'@timestamp': '123',
|
||||
_id: '1',
|
||||
monitor: {
|
||||
check_group: 'check_group',
|
||||
id: 'MONITOR_ID',
|
||||
duration: {
|
||||
us: 123,
|
||||
|
|
|
@ -8,10 +8,10 @@
|
|||
import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui';
|
||||
import React, { useContext, FC } from 'react';
|
||||
import { UptimeThemeContext } from '../../contexts';
|
||||
import { Ping } from '../../../common/runtime_types/ping';
|
||||
import { JourneyStep } from '../../../common/runtime_types/ping';
|
||||
|
||||
interface Props {
|
||||
event: Ping;
|
||||
event: JourneyStep;
|
||||
}
|
||||
|
||||
export const ConsoleEvent: FC<Props> = ({ event }) => {
|
||||
|
@ -28,7 +28,7 @@ export const ConsoleEvent: FC<Props> = ({ event }) => {
|
|||
|
||||
return (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>{event.timestamp}</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>{event['@timestamp']}</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} style={{ color: typeColor }}>
|
||||
{event.synthetics?.type}
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -5,147 +5,101 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { shallowWithIntl } from '@kbn/test/jest';
|
||||
import React from 'react';
|
||||
import { JourneyStep } from '../../../common/runtime_types/ping/synthetics';
|
||||
import { render } from '../../lib/helper/rtl_helpers';
|
||||
import { ConsoleOutputEventList } from './console_output_event_list';
|
||||
|
||||
describe('ConsoleOutputEventList component', () => {
|
||||
let steps: JourneyStep[];
|
||||
|
||||
beforeEach(() => {
|
||||
steps = [
|
||||
{
|
||||
'@timestamp': '123',
|
||||
_id: '1',
|
||||
monitor: {
|
||||
check_group: 'check_group',
|
||||
id: 'MON_ID',
|
||||
duration: {
|
||||
us: 10,
|
||||
},
|
||||
status: 'down',
|
||||
type: 'browser',
|
||||
},
|
||||
synthetics: {
|
||||
type: 'stderr',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@timestamp': '124',
|
||||
_id: '2',
|
||||
monitor: {
|
||||
check_group: 'check_group',
|
||||
id: 'MON_ID',
|
||||
duration: {
|
||||
us: 10,
|
||||
},
|
||||
status: 'down',
|
||||
type: 'browser',
|
||||
},
|
||||
synthetics: {
|
||||
type: 'cmd/status',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@timestamp': '124',
|
||||
_id: '2',
|
||||
monitor: {
|
||||
check_group: 'check_group',
|
||||
id: 'MON_ID',
|
||||
duration: {
|
||||
us: 10,
|
||||
},
|
||||
status: 'down',
|
||||
type: 'browser',
|
||||
},
|
||||
synthetics: {
|
||||
type: 'step/end',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@timestamp': '125',
|
||||
_id: '3',
|
||||
monitor: {
|
||||
check_group: 'check_group',
|
||||
id: 'MON_ID',
|
||||
duration: {
|
||||
us: 10,
|
||||
},
|
||||
status: 'down',
|
||||
type: 'browser',
|
||||
},
|
||||
synthetics: {
|
||||
type: 'stdout',
|
||||
},
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
it('renders a component per console event', () => {
|
||||
expect(
|
||||
shallowWithIntl(
|
||||
<ConsoleOutputEventList
|
||||
journey={{
|
||||
checkGroup: 'check_group',
|
||||
loading: false,
|
||||
// 4 steps, three console, one step/end
|
||||
steps: [
|
||||
{
|
||||
timestamp: '123',
|
||||
docId: '1',
|
||||
monitor: {
|
||||
id: 'MON_ID',
|
||||
duration: {
|
||||
us: 10,
|
||||
},
|
||||
status: 'down',
|
||||
type: 'browser',
|
||||
},
|
||||
synthetics: {
|
||||
type: 'stderr',
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamp: '124',
|
||||
docId: '2',
|
||||
monitor: {
|
||||
id: 'MON_ID',
|
||||
duration: {
|
||||
us: 10,
|
||||
},
|
||||
status: 'down',
|
||||
type: 'browser',
|
||||
},
|
||||
synthetics: {
|
||||
type: 'cmd/status',
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamp: '124',
|
||||
docId: '2',
|
||||
monitor: {
|
||||
id: 'MON_ID',
|
||||
duration: {
|
||||
us: 10,
|
||||
},
|
||||
status: 'down',
|
||||
type: 'browser',
|
||||
},
|
||||
synthetics: {
|
||||
type: 'step/end',
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamp: '125',
|
||||
docId: '3',
|
||||
monitor: {
|
||||
id: 'MON_ID',
|
||||
duration: {
|
||||
us: 10,
|
||||
},
|
||||
status: 'down',
|
||||
type: 'browser',
|
||||
},
|
||||
synthetics: {
|
||||
type: 'stdout',
|
||||
},
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
).find('EuiCodeBlock')
|
||||
).toMatchInlineSnapshot(`
|
||||
<EuiCodeBlock>
|
||||
<ConsoleEvent
|
||||
event={
|
||||
Object {
|
||||
"docId": "1",
|
||||
"monitor": Object {
|
||||
"duration": Object {
|
||||
"us": 10,
|
||||
},
|
||||
"id": "MON_ID",
|
||||
"status": "down",
|
||||
"type": "browser",
|
||||
},
|
||||
"synthetics": Object {
|
||||
"type": "stderr",
|
||||
},
|
||||
"timestamp": "123",
|
||||
}
|
||||
}
|
||||
key="1_console-event-row"
|
||||
/>
|
||||
<ConsoleEvent
|
||||
event={
|
||||
Object {
|
||||
"docId": "2",
|
||||
"monitor": Object {
|
||||
"duration": Object {
|
||||
"us": 10,
|
||||
},
|
||||
"id": "MON_ID",
|
||||
"status": "down",
|
||||
"type": "browser",
|
||||
},
|
||||
"synthetics": Object {
|
||||
"type": "cmd/status",
|
||||
},
|
||||
"timestamp": "124",
|
||||
}
|
||||
}
|
||||
key="2_console-event-row"
|
||||
/>
|
||||
<ConsoleEvent
|
||||
event={
|
||||
Object {
|
||||
"docId": "3",
|
||||
"monitor": Object {
|
||||
"duration": Object {
|
||||
"us": 10,
|
||||
},
|
||||
"id": "MON_ID",
|
||||
"status": "down",
|
||||
"type": "browser",
|
||||
},
|
||||
"synthetics": Object {
|
||||
"type": "stdout",
|
||||
},
|
||||
"timestamp": "125",
|
||||
}
|
||||
}
|
||||
key="3_console-event-row"
|
||||
/>
|
||||
</EuiCodeBlock>
|
||||
`);
|
||||
const { getByRole, getByText, queryByText } = render(
|
||||
<ConsoleOutputEventList
|
||||
journey={{
|
||||
checkGroup: 'check_group',
|
||||
loading: false,
|
||||
// 4 steps, three console, one step/end
|
||||
steps,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
expect(getByRole('heading').innerHTML).toBe('No steps ran');
|
||||
steps
|
||||
.filter((step) => step.synthetics.type !== 'step/end')
|
||||
.forEach((step) => {
|
||||
expect(getByText(step['@timestamp']));
|
||||
expect(getByText(step.synthetics.type));
|
||||
});
|
||||
expect(queryByText('step/end')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,17 +9,17 @@ import { EuiCodeBlock, EuiSpacer, EuiTitle } from '@elastic/eui';
|
|||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React, { FC } from 'react';
|
||||
import { ConsoleEvent } from './console_event';
|
||||
import { Ping } from '../../../common/runtime_types/ping';
|
||||
import { JourneyStep } from '../../../common/runtime_types/ping';
|
||||
import { JourneyState } from '../../state/reducers/journey';
|
||||
|
||||
interface Props {
|
||||
journey: JourneyState;
|
||||
}
|
||||
|
||||
const isConsoleStep = (step: Ping) =>
|
||||
step.synthetics?.type === 'stderr' ||
|
||||
step.synthetics?.type === 'stdout' ||
|
||||
step.synthetics?.type === 'cmd/status';
|
||||
const CONSOLE_STEP_TYPES = ['stderr', 'stdout', 'cmd/status'];
|
||||
|
||||
const isConsoleStep = (step: JourneyStep) =>
|
||||
CONSOLE_STEP_TYPES.some((type) => type === step.synthetics.type);
|
||||
|
||||
export const ConsoleOutputEventList: FC<Props> = ({ journey }) => (
|
||||
<div>
|
||||
|
@ -41,7 +41,7 @@ export const ConsoleOutputEventList: FC<Props> = ({ journey }) => (
|
|||
<EuiSpacer />
|
||||
<EuiCodeBlock>
|
||||
{journey.steps.filter(isConsoleStep).map((consoleEvent) => (
|
||||
<ConsoleEvent event={consoleEvent} key={consoleEvent.docId + '_console-event-row'} />
|
||||
<ConsoleEvent event={consoleEvent} key={consoleEvent._id + '_console-event-row'} />
|
||||
))}
|
||||
</EuiCodeBlock>
|
||||
</div>
|
||||
|
|
|
@ -8,15 +8,16 @@
|
|||
import React from 'react';
|
||||
import { ExecutedStep } from './executed_step';
|
||||
import { render } from '../../lib/helper/rtl_helpers';
|
||||
import { Ping } from '../../../common/runtime_types/ping';
|
||||
import { JourneyStep } from '../../../common/runtime_types/ping';
|
||||
|
||||
describe('ExecutedStep', () => {
|
||||
let step: Ping;
|
||||
let step: JourneyStep;
|
||||
|
||||
beforeEach(() => {
|
||||
step = {
|
||||
docId: 'docID',
|
||||
_id: 'docID',
|
||||
monitor: {
|
||||
check_group: 'check_group',
|
||||
duration: {
|
||||
us: 123,
|
||||
},
|
||||
|
@ -29,8 +30,9 @@ describe('ExecutedStep', () => {
|
|||
index: 4,
|
||||
name: 'STEP_NAME',
|
||||
},
|
||||
type: 'step/end',
|
||||
},
|
||||
timestamp: 'timestamp',
|
||||
'@timestamp': 'timestamp',
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -43,6 +45,7 @@ describe('ExecutedStep', () => {
|
|||
index: 3,
|
||||
name: 'STEP_NAME',
|
||||
},
|
||||
type: 'step/end',
|
||||
};
|
||||
|
||||
const { getByText } = render(<ExecutedStep index={3} step={step} loading={false} />);
|
||||
|
@ -57,6 +60,7 @@ describe('ExecutedStep', () => {
|
|||
message: 'There was an error executing the step.',
|
||||
stack: 'some.stack.trace.string',
|
||||
},
|
||||
type: 'an error type',
|
||||
};
|
||||
|
||||
const { getByText } = render(<ExecutedStep index={3} step={step} loading={false} />);
|
||||
|
|
|
@ -9,14 +9,14 @@ 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 { JourneyStep } 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;
|
||||
step: JourneyStep;
|
||||
index: number;
|
||||
loading: boolean;
|
||||
browserConsole?: string;
|
||||
|
|
|
@ -8,6 +8,18 @@
|
|||
import React from 'react';
|
||||
import { StepScreenshotDisplay } from './step_screenshot_display';
|
||||
import { render } from '../../lib/helper/rtl_helpers';
|
||||
import * as observabilityPublic from '../../../../observability/public';
|
||||
import '../../lib/__mocks__/use_composite_image.mock';
|
||||
import { mockRef } from '../../lib/__mocks__/screenshot_ref.mock';
|
||||
|
||||
jest.mock('../../../../observability/public', () => {
|
||||
const originalModule = jest.requireActual('../../../../observability/public');
|
||||
|
||||
return {
|
||||
...originalModule,
|
||||
useFetcher: jest.fn().mockReturnValue({ data: null, status: 'success' }),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('react-use/lib/useIntersection', () => () => ({
|
||||
isIntersecting: true,
|
||||
|
@ -18,7 +30,8 @@ describe('StepScreenshotDisplayProps', () => {
|
|||
const { getByAltText } = render(
|
||||
<StepScreenshotDisplay
|
||||
checkGroup="check_group"
|
||||
screenshotExists={true}
|
||||
isScreenshotBlob={true}
|
||||
isScreenshotRef={false}
|
||||
stepIndex={1}
|
||||
stepName="STEP_NAME"
|
||||
/>
|
||||
|
@ -29,7 +42,12 @@ describe('StepScreenshotDisplayProps', () => {
|
|||
|
||||
it('uses alternative text when step name not available', () => {
|
||||
const { getByAltText } = render(
|
||||
<StepScreenshotDisplay checkGroup="check_group" screenshotExists={true} stepIndex={1} />
|
||||
<StepScreenshotDisplay
|
||||
checkGroup="check_group"
|
||||
isScreenshotBlob={true}
|
||||
isScreenshotRef={false}
|
||||
stepIndex={1}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(getByAltText('Screenshot')).toBeInTheDocument();
|
||||
|
@ -39,11 +57,32 @@ describe('StepScreenshotDisplayProps', () => {
|
|||
const { getByTestId } = render(
|
||||
<StepScreenshotDisplay
|
||||
checkGroup="check_group"
|
||||
isScreenshotBlob={false}
|
||||
isScreenshotRef={false}
|
||||
stepIndex={1}
|
||||
stepName="STEP_NAME"
|
||||
screenshotExists={false}
|
||||
/>
|
||||
);
|
||||
expect(getByTestId('stepScreenshotImageUnavailable')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays screenshot thumbnail for ref', () => {
|
||||
jest.spyOn(observabilityPublic, 'useFetcher').mockReturnValue({
|
||||
status: observabilityPublic.FETCH_STATUS.SUCCESS,
|
||||
data: { ...mockRef },
|
||||
refetch: () => null,
|
||||
});
|
||||
|
||||
const { getByAltText } = render(
|
||||
<StepScreenshotDisplay
|
||||
checkGroup="check_group"
|
||||
isScreenshotBlob={false}
|
||||
isScreenshotRef={true}
|
||||
stepIndex={1}
|
||||
stepName="STEP_NAME"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(getByAltText('Screenshot for step with name "STEP_NAME"')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,16 +5,31 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiImage, EuiText } from '@elastic/eui';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
EuiImage,
|
||||
EuiLoadingSpinner,
|
||||
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 {
|
||||
isScreenshotRef as isAScreenshotRef,
|
||||
ScreenshotRefImageData,
|
||||
} from '../../../common/runtime_types';
|
||||
import { UptimeSettingsContext, UptimeThemeContext } from '../../contexts';
|
||||
import { useFetcher } from '../../../../observability/public';
|
||||
import { getJourneyScreenshot } from '../../state/api/journey';
|
||||
import { useCompositeImage } from '../../hooks';
|
||||
|
||||
interface StepScreenshotDisplayProps {
|
||||
screenshotExists?: boolean;
|
||||
isScreenshotBlob: boolean;
|
||||
isScreenshotRef: boolean;
|
||||
checkGroup?: string;
|
||||
stepIndex?: number;
|
||||
stepName?: string;
|
||||
|
@ -36,9 +51,56 @@ const StepImage = styled(EuiImage)`
|
|||
}
|
||||
`;
|
||||
|
||||
const BaseStepImage = ({
|
||||
stepIndex,
|
||||
stepName,
|
||||
url,
|
||||
}: Pick<StepScreenshotDisplayProps, 'stepIndex' | 'stepName'> & { url?: string }) => {
|
||||
if (!url) return <EuiLoadingSpinner size="l" />;
|
||||
return (
|
||||
<StepImage
|
||||
allowFullScreen={true}
|
||||
alt={
|
||||
stepName
|
||||
? i18n.translate('xpack.uptime.synthetics.screenshotDisplay.altText', {
|
||||
defaultMessage: 'Screenshot for step with name "{stepName}"',
|
||||
values: {
|
||||
stepName,
|
||||
},
|
||||
})
|
||||
: i18n.translate('xpack.uptime.synthetics.screenshotDisplay.altTextWithoutName', {
|
||||
defaultMessage: 'Screenshot',
|
||||
})
|
||||
}
|
||||
caption={`Step:${stepIndex} ${stepName}`}
|
||||
hasShadow
|
||||
url={url}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type ComposedStepImageProps = Pick<StepScreenshotDisplayProps, 'stepIndex' | 'stepName'> & {
|
||||
url: string | undefined;
|
||||
imgRef: ScreenshotRefImageData;
|
||||
setUrl: React.Dispatch<string | undefined>;
|
||||
};
|
||||
|
||||
const ComposedStepImage = ({
|
||||
stepIndex,
|
||||
stepName,
|
||||
url,
|
||||
imgRef,
|
||||
setUrl,
|
||||
}: ComposedStepImageProps) => {
|
||||
useCompositeImage(imgRef, setUrl, url);
|
||||
if (!url) return <EuiLoadingSpinner size="l" />;
|
||||
return <BaseStepImage stepIndex={stepIndex} stepName={stepName} url={url} />;
|
||||
};
|
||||
|
||||
export const StepScreenshotDisplay: FC<StepScreenshotDisplayProps> = ({
|
||||
checkGroup,
|
||||
screenshotExists,
|
||||
isScreenshotBlob: isScreenshotBlob,
|
||||
isScreenshotRef,
|
||||
stepIndex,
|
||||
stepName,
|
||||
lazyLoad = true,
|
||||
|
@ -64,60 +126,59 @@ export const StepScreenshotDisplay: FC<StepScreenshotDisplayProps> = ({
|
|||
}
|
||||
}, [hasIntersected, isIntersecting, setHasIntersected]);
|
||||
|
||||
let content: JSX.Element | null = null;
|
||||
const imgSrc = basePath + `/api/uptime/journey/screenshot/${checkGroup}/${stepIndex}`;
|
||||
|
||||
if ((hasIntersected || !lazyLoad) && screenshotExists) {
|
||||
content = (
|
||||
<StepImage
|
||||
allowFullScreen={true}
|
||||
alt={
|
||||
stepName
|
||||
? i18n.translate('xpack.uptime.synthetics.screenshotDisplay.altText', {
|
||||
defaultMessage: 'Screenshot for step with name "{stepName}"',
|
||||
values: {
|
||||
stepName,
|
||||
},
|
||||
})
|
||||
: i18n.translate('xpack.uptime.synthetics.screenshotDisplay.altTextWithoutName', {
|
||||
defaultMessage: 'Screenshot',
|
||||
})
|
||||
}
|
||||
caption={`Step:${stepIndex} ${stepName}`}
|
||||
hasShadow
|
||||
url={imgSrc}
|
||||
/>
|
||||
);
|
||||
} else if (screenshotExists === false) {
|
||||
content = (
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
direction="column"
|
||||
style={{ paddingTop: '32px' }}
|
||||
data-test-subj="stepScreenshotImageUnavailable"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon color="subdued" size="xxl" type="image" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.uptime.synthetics.screenshot.noImageMessage"
|
||||
defaultMessage="No image available"
|
||||
/>
|
||||
</strong>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
// When loading a legacy screenshot, set `url` to full-size screenshot path.
|
||||
// Otherwise, we first need to composite the image.
|
||||
const [url, setUrl] = useState<string | undefined>(isScreenshotBlob ? imgSrc : undefined);
|
||||
|
||||
// when the image is a composite, we need to fetch the data since we cannot specify a blob URL
|
||||
const { data: screenshotRef } = useFetcher(() => {
|
||||
if (isScreenshotRef) {
|
||||
return getJourneyScreenshot(imgSrc);
|
||||
}
|
||||
}, [basePath, checkGroup, stepIndex, isScreenshotRef]);
|
||||
|
||||
const shouldRenderImage = hasIntersected || !lazyLoad;
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={{ backgroundColor: pageBackground, height: IMAGE_HEIGHT, width: IMAGE_WIDTH }}
|
||||
>
|
||||
{content}
|
||||
{shouldRenderImage && isScreenshotBlob && (
|
||||
<BaseStepImage stepName={stepName} stepIndex={stepIndex} url={url} />
|
||||
)}
|
||||
{shouldRenderImage && isScreenshotRef && isAScreenshotRef(screenshotRef) && (
|
||||
<ComposedStepImage
|
||||
imgRef={screenshotRef}
|
||||
stepName={stepName}
|
||||
stepIndex={stepIndex}
|
||||
setUrl={setUrl}
|
||||
url={url}
|
||||
/>
|
||||
)}
|
||||
{!isScreenshotBlob && !isScreenshotRef && (
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
direction="column"
|
||||
style={{ paddingTop: '32px' }}
|
||||
data-test-subj="stepScreenshotImageUnavailable"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon color="subdued" size="xxl" type="image" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.uptime.synthetics.screenshot.noImageMessage"
|
||||
defaultMessage="No image available"
|
||||
/>
|
||||
</strong>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -5,9 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './use_monitor';
|
||||
export * from './use_url_params';
|
||||
export * from './use_telemetry';
|
||||
export * from './use_composite_image';
|
||||
export * from './update_kuery_string';
|
||||
export * from './use_cert_status';
|
||||
export * from './use_monitor';
|
||||
export * from './use_search_text';
|
||||
export * from './use_cert_status';
|
||||
export * from './use_telemetry';
|
||||
export * from './use_url_params';
|
||||
|
|
55
x-pack/plugins/uptime/public/hooks/use_composite_image.ts
Normal file
55
x-pack/plugins/uptime/public/hooks/use_composite_image.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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 { composeScreenshotRef } from '../lib/helper/compose_screenshot_images';
|
||||
import { ScreenshotRefImageData } from '../../common/runtime_types/ping/synthetics';
|
||||
|
||||
/**
|
||||
* Checks if two refs are the same. If the ref is unchanged, there's no need
|
||||
* to run the expensive draw procedure.
|
||||
*/
|
||||
function isNewRef(a: ScreenshotRefImageData, b: ScreenshotRefImageData): boolean {
|
||||
if (typeof a === 'undefined' || typeof b === 'undefined') return false;
|
||||
const stepA = a.ref.screenshotRef.synthetics.step;
|
||||
const stepB = b.ref.screenshotRef.synthetics.step;
|
||||
return stepA.index !== stepB.index || stepA.name !== stepB.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assembles the data for a composite image and returns the composite to a callback.
|
||||
* @param imgRef the data and dimensions for the composite image.
|
||||
* @param onComposeImageSuccess sends the composited image to this callback.
|
||||
* @param imageData this is the composited image value, if it is truthy the function will skip the compositing process
|
||||
*/
|
||||
export const useCompositeImage = (
|
||||
imgRef: ScreenshotRefImageData,
|
||||
onComposeImageSuccess: React.Dispatch<string | undefined>,
|
||||
imageData?: string
|
||||
): void => {
|
||||
const [curRef, setCurRef] = React.useState<ScreenshotRefImageData>(imgRef);
|
||||
|
||||
React.useEffect(() => {
|
||||
const canvas = document.createElement('canvas');
|
||||
|
||||
async function compose() {
|
||||
await composeScreenshotRef(imgRef, canvas);
|
||||
const imgData = canvas.toDataURL('image/png', 1.0);
|
||||
onComposeImageSuccess(imgData);
|
||||
}
|
||||
|
||||
// if the URL is truthy it means it's already been composed, so there
|
||||
// is no need to call the function
|
||||
if (typeof imageData === 'undefined' || isNewRef(imgRef, curRef)) {
|
||||
compose();
|
||||
setCurRef(imgRef);
|
||||
}
|
||||
return () => {
|
||||
canvas.parentElement?.removeChild(canvas);
|
||||
};
|
||||
}, [imgRef, onComposeImageSuccess, curRef, imageData]);
|
||||
};
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ScreenshotRefImageData } from '../../../common/runtime_types';
|
||||
|
||||
export const mockRef: ScreenshotRefImageData = {
|
||||
maxSteps: 1,
|
||||
stepName: 'load homepage',
|
||||
ref: {
|
||||
screenshotRef: {
|
||||
'@timestamp': '2021-06-08T19:42:30.257Z',
|
||||
synthetics: {
|
||||
package_version: '1.0.0-beta.2',
|
||||
step: { name: 'load homepage', index: 1 },
|
||||
type: 'step/screenshot_ref',
|
||||
},
|
||||
screenshot_ref: {
|
||||
blocks: [
|
||||
{
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: 160,
|
||||
hash: 'd518801fc523cf02727cd520f556c4113b3098c7',
|
||||
height: 90,
|
||||
},
|
||||
{
|
||||
top: 0,
|
||||
left: 160,
|
||||
width: 160,
|
||||
hash: 'fa90345d5d7b05b1601e9ee645e663bc358869e0',
|
||||
height: 90,
|
||||
},
|
||||
],
|
||||
width: 1280,
|
||||
height: 720,
|
||||
},
|
||||
monitor: { check_group: 'a567cc7a-c891-11eb-bdf9-3e22fb19bf97' },
|
||||
},
|
||||
blocks: [
|
||||
{
|
||||
id: 'd518801fc523cf02727cd520f556c4113b3098c7',
|
||||
synthetics: {
|
||||
blob:
|
||||
'/9j/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCABaAKADASIAAhEBAxEB/8QAHAABAAIDAQEBAAAAAAAAAAAAAAMFBAYHAggB/8QANRAAAQMDAwEHAwMCBwEAAAAAAQACAwQFEQYSITEHE0FRUpLRFBVhIjKBcaEIFiMkMzSxQv/EABkBAQADAQEAAAAAAAAAAAAAAAADBQYEAv/EACYRAQABBAEEAgEFAAAAAAAAAAABAgMEESEFEjFBBjJxE0JRYYH/2gAMAwEAAhEDEQA/APqlERAREQEREBERAREQEREBERAREQEREBERAREQeT4J5qn1XdDZbFU1rWB74wA1p6FxIAz+OVp+ldV3O6UdxfVd0BEWCNzG4ILieOvgAVFk3qcezVeq8Uxt12cK7dtzdp8ROnRTKxmA57QT+cL2CCucOc55LnuLnHkknJKvNNV7xUtpZHF0bwdmT+0jlZPA+WUZWRFmujtiqdRO98+t/lLe6fVbomqJ3ptqIi2KvEWHc7hR2uhlrbjVQUlJCN0k9RII2MGcZLjwOSsprg5oc0gtIyCPFB6REQERQPqIWVDIHSxtnkBc2MuAc4DqQOpwgnREQEREBERAREQcf7QdZXKK+z0FvlENPTkMcCxru8djnOQeOcYUuj7hLdLZWCop6SlxKwsmYxsDJpMEbT0Bdg54Vjr3SlD9ZLfqieSOmG01EDB+qQ8AbT4Z4z/JXP7ncZK90bdoipYRsgp2fsjb+PM+ZPJVhl2MXNwpxu37RqZ9wydzqGX03LquV1zMb3FO+Jj1uPUOiTQvik2yxuY/w3DCptS6vpNHPj3MFXdjhzaUOwImnxkPgSOjeviVV6Wv9dBWU9C+eV9LK7ug3hzoi7gOZnOCCc+S0+5dnepX3CUQNhuj3ykOlhqWOcXE9XBxDh+SVkek/D8TCyv1ci53RHNMTxz/AH/Ol3f+TXc3H1i0TFXifevw+hdI32n1LYqa50zSxsoLXMccljgcFufHnx8Rha7fdeVrNUVWn9KacqL/AHGgZHLXFtVHTRUweMsaXv6vI52gdPHri27ONOP0xpWnoKh7X1Jc6WYt6B7vAf0GB/C5ZZxNQ9rGvaCr1rPpupqaqCrgiLKYtqonRABzTOxxO3G0hp4wtBXFMVzFPh2WJrm3TNz7a5Zvadq+HVnYRrV30VTbrjQYpK6hqMF8EokYcZHDmkEEOHBC3683nUlufRw2LSv3mldTse6o+4xU+1/ILNrhk8AHP5/C5h2g2C22zsj7SbrR6kfqCquggNZUGSEhskbmNAxE0NacEZGPAK8u94rLn2jzabrtVT6atFHaqeriFM6KGWse4nc4SSNd+luACGrylbNYu0enqbRqWpv1tqrNXacy65Ub3tlcxvd941zHN4eHN6dP/CcO3641hcaamraLs5qzQVO18T5btTxy907kPdGTwcHO3OfBc/7N5NLSdoPaxZ6/UUN1tldTULTU11wZI6pibTvEx70EAhhftJH7cDyXrU9c7s40/DcND9okt5ZDLDDTWCsqIa36lrpGs7qJw/1G4BJGM4DcILnUOpNUUfb2Y7TpWruQjsUjI6YXKGFs0f1LP9wNzsDB/RtOHc+S3aW70b9caTprtZDBqGst087HmVrzRYEZli3Dh3JAyODtVDe7lR2j/ERbai51MNJT1WmpqeKWeRrGOkFSx5bkkc4HRZF8qIartu0LPTSxzQy2uveySNwc17T3RBBHBB80Eg7RrrdrtdabRmkKu90lsqH0c9a+thpY3Ts/cyPfkuxnrwP7Z6BbppqihppqymdSVEkTXyU7nNeYXEAlhc0kEg8ZBwccLk3YfqGzWSxXux3i40FuuluvNY2ogqZmxOIdKXNeA4jLSCMEccLrtNPFVU8U9NKyaGVofHJG4Oa9pGQQRwQR4oMhERAREQEREFTqa2/drJV0WQHSt/ST0DgcjP4yAuDXCiqbfVPp6yF8MzDgtcP7jzH5X0aenkoZqaGYDv4mSY8HNB/9U1q9NvhUdS6VTnTFcTqqOP8AHF9A2apuF7pqlsJ+lp373SPGG7hyAD4nOFu2ndL1lJeGVddIwd2S4BhyXk8fwOVu0bGsADWhoHAwMBe8LlyrVOTcprr/AG+EuB06jDt9m9zve36qe96bsV/MRvtmttyMWRGayljm2Z8twOFcIpFkpqXTVipbZNbKWy22C2zHMlJHSRtif0/cwDB6DqPBL1pmw30Q/fLJbLj3IIj+spI5u7B6hu4HH8K5RBUf5cseAPs1uwIjB/1Wf8Z6s6ftPl0WLbNGaXtVa2ttmnLLRVjc7Z6egijkGfJzWgrYUQVN70/Zr/FEy+Wm33KOIl0baymZMGE9SA4HBU1NabdS/Smlt9JCaSPuafu4Wt7iPgbGYH6W8DgccKwRBRXjSWnL1VCqvGn7RcKkANEtXRRyvAHhuc0lWtNBFS08UFNEyGGJoZHHG0NaxoGAABwAB4LIRAREQEREBERAREQEREBERAREQEREBERAREQEREBERBHul9DPcfhN0voZ7j8KREEe6X0M9x+E3S+hnuPwpEQR7pfQz3H4TdL6Ge4/CkRBHul9DPcfhN0voZ7j8KREEe6X0M9x+E3S+hnuPwpEQR7pfQz3H4TdL6Ge4/CkRBHul9DPcfhN0voZ7j8KREEe6X0M9x+E3S+hnuPwpEQR7pfQz3H4TdL6Ge4/CkRBHuk9DPcfhVV1vcdtexssL3ucM4jOcf1zhW/ktF1OT92k5/8AgL3RT3Ty4c/IqsW+6jy//9k=',
|
||||
blob_mime: 'image/jpeg',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'fa90345d5d7b05b1601e9ee645e663bc358869e0',
|
||||
synthetics: {
|
||||
blob:
|
||||
'/9j/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCABaAKADASIAAhEBAxEB/8QAHAABAAIDAQEBAAAAAAAAAAAAAAYIAwUHBAIB/8QANBAAAQQCAQICCAUDBQAAAAAAAQACAwQFEQYSIQcxExQWQVFSVKEiMmGS0RVTcQhDgZHh/8QAGQEBAAIDAAAAAAAAAAAAAAAAAAIEAwUG/8QAHxEBAAEEAwADAAAAAAAAAAAAAAEEExVRAgORBRIh/9oADAMBAAIRAxEAPwC1KIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIg+JHBkbnnyaCVyXj3jlhsqMRYs4PP43GZax6rUyViuz1Z8vUWhhe150dtI8vcfcCV1iyCa8oAJJYQAP8KvPg54WZPJ8D4qOY5TKwY/HWn3IuPzVG1xHK2aQtMjiOtwO+rR9zu3ZB3qDL46xYNeDIVJJ+p7TGyZpdtmuoaB3sbG/htZaORpZBshoW61psbul5gla/pPwOj2KrvjvD7J2uD+Kliph5q3KLuYvMozzxGKWWqXMcWxF2vwvBkGx2dvz7LyYvi+Vyd/MTcI4vk+K1TxWTG2GW63qvrVw76Q0bHU73el/nuHcLvPMPByvAYKCT1yfMOstimrSMfFEYI+t4eQ7YOj5AH9dLfR5fGyQVp48hUdDaf6OCQTtLZX7I6WHenHYI0Pgq18A47IefeGcuN4PmcEyhQt1crbs0XRMfY9Vc3rcfftx7Pdrq6gB+XQxcaxvI4cB4acXs8Sz0M/HuStluXDVJr9BmkeHscPNmn93a6Rrz7hBZ0ZKi6+aDbtY3gOo1xK30gHx6d70tJxbnGA5PPlYsPejlfjLD69jbgO7fN7e/dnf83kuI8B48/HZmjjuQcCy9/lsOdfcm5AA6GH0ZcSJvWR+duv9o9j/lfVHhLK2L8WMDZ4rlIn3LslmnNjKbWelqGRjmRQyHTTot2Yt+QIHdBYmhfqZGEy0LVezED0l8Ege3fw2CvWuJ/6fqeTo5TkLLGDFTGlkDYsg/EnFS2ntBBa6vst/DvXU0Dvvz327YgIiINXyPM0ePYS5lstO2ChUjMksh9w/wAe8k6AHvJChHG/FrGZfOY7GW8LnsN/VQX46xk6gjit6G9NcHHRI7gH3a+IC93jfxm5y/wxzeGxfe9Mxj4mF3SJHMka/oJ/Xp0N9t6Wm4/zPOcizGCx0PAsnj44AHZG3lq3oIqpa0dq52esk9hrXbX66DoozGMMDLAyNMwPk9C2T07el0nl0A711fp5rKclRF4UTcrC6R1CuZW+kI+PTvaq6cTySpxanxB3FM66zQ5ay9JdZVLqz4DKSHscO7vPZ0NADZI8ls8txzJY3xYns4XjN+/JazrbbxkcUHRsBO3Tw32OBYwe6N3l27HyQd04/wA1wfIM5l8RjLrZL2LeGWGbA3sebe/4gPIkeRW2gy+MsVp7MGRpy16+/TSMma5sevPqIOh5e9V6yvDMnFl/F3HYXj89bJZWBk2Mvw1AyGSH8JmhZMAA1z9kFuxsg78lro+NXLo5Ba4pw3L8fxrOITY+3WnpGF124QekMjHeRw+fWz/yNhZMZzFCOZ/9UoBkLGySu9YZpjXflc477A7GifNZ7eSo1Kgt27laCq7XTNLK1rDvy04nXdV1wHhnUfybhbbnFZPU5eJj+o+kqPDHXOkdptjXpQSezu4IHwGo9W4ryUcO8Np8zispNjqFe5Xs1H4g3pK0rpn+jc+q/RILOkA67AA+8ILcIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiCOe1Nb6eb7J7U1vp5vsoj5u7r9PkrFuHMZSo3HiW+1Nb6eb7J7U1vp5vsoiiW4MpUbjxLvamt9PN9k9qa30832URRLcGUqNx4l3tTW+nm+ye1Nb6eb7KIoluDKVG48S72prfTzfZPamt9PN9lEUS3BlKjceJd7U1vp5vsntTW/sTfZRFEtwZSo3HiW+1Nf+xN/0Fuq05sQtliawscNj8X/AIucKccYJOJi3+qhz4REfi98fW9nfz+vNteqX5GfuP8ACdUvyM/cf4WRFiblj6pfkZ+4/wAJ1S/Iz9x/hZEQY+qX5GfuP8J1S/Iz9x/hZEQf/9k=',
|
||||
blob_mime: 'image/jpeg',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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 { ScreenshotRefImageData } from '../../../common/runtime_types/ping/synthetics';
|
||||
import * as composeScreenshotImages from '../../hooks/use_composite_image';
|
||||
|
||||
jest
|
||||
.spyOn(composeScreenshotImages, 'useCompositeImage')
|
||||
.mockImplementation(
|
||||
(
|
||||
_imgRef: ScreenshotRefImageData,
|
||||
callback: React.Dispatch<string | undefined>,
|
||||
url?: string
|
||||
) => {
|
||||
if (!url) {
|
||||
callback('img src');
|
||||
}
|
||||
}
|
||||
);
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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 { ScreenshotRefImageData } from '../../../common/runtime_types';
|
||||
|
||||
/**
|
||||
* Draws image fragments on a canvas.
|
||||
* @param data Contains overall image size, fragment dimensions, and the blobs of image data to render.
|
||||
* @param canvas A canvas to use for the rendering.
|
||||
* @returns A promise that will resolve when the final draw operation completes.
|
||||
*/
|
||||
export async function composeScreenshotRef(
|
||||
data: ScreenshotRefImageData,
|
||||
canvas: HTMLCanvasElement
|
||||
) {
|
||||
const {
|
||||
ref: { screenshotRef, blocks },
|
||||
} = data;
|
||||
|
||||
canvas.width = screenshotRef.screenshot_ref.width;
|
||||
canvas.height = screenshotRef.screenshot_ref.height;
|
||||
|
||||
const ctx = canvas.getContext('2d', { alpha: false });
|
||||
|
||||
/**
|
||||
* We need to treat each operation as an async task, otherwise we will race between drawing image
|
||||
* chunks and extracting the final data URL from the canvas; without this, the image could be blank or incomplete.
|
||||
*/
|
||||
const drawOperations: Array<Promise<void>> = [];
|
||||
|
||||
for (const block of screenshotRef.screenshot_ref.blocks) {
|
||||
drawOperations.push(
|
||||
new Promise<void>((resolve, reject) => {
|
||||
const img = new Image();
|
||||
const { top, left, width, height, hash } = block;
|
||||
const blob = blocks.find((b) => b.id === hash);
|
||||
if (!blob) {
|
||||
reject(Error(`Error processing image. Expected image data with hash ${hash} is missing`));
|
||||
} else {
|
||||
img.onload = () => {
|
||||
ctx?.drawImage(img, left, top, width, height);
|
||||
resolve();
|
||||
};
|
||||
img.src = `data:image/jpg;base64,${blob.synthetics.blob}`;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// once all `draw` operations finish, caller can extract img string
|
||||
return Promise.all(drawOperations);
|
||||
}
|
|
@ -8,7 +8,12 @@
|
|||
import { apiService } from './utils';
|
||||
import { FetchJourneyStepsParams } from '../actions/journey';
|
||||
import {
|
||||
Ping,
|
||||
FailedStepsApiResponse,
|
||||
FailedStepsApiResponseType,
|
||||
JourneyStep,
|
||||
JourneyStepType,
|
||||
ScreenshotImageBlob,
|
||||
ScreenshotRefImageData,
|
||||
SyntheticsJourneyApiResponse,
|
||||
SyntheticsJourneyApiResponseType,
|
||||
} from '../../../common/runtime_types';
|
||||
|
@ -16,23 +21,23 @@ import {
|
|||
export async function fetchJourneySteps(
|
||||
params: FetchJourneyStepsParams
|
||||
): Promise<SyntheticsJourneyApiResponse> {
|
||||
return (await apiService.get(
|
||||
return apiService.get(
|
||||
`/api/uptime/journey/${params.checkGroup}`,
|
||||
{ syntheticEventTypes: params.syntheticEventTypes },
|
||||
SyntheticsJourneyApiResponseType
|
||||
)) as SyntheticsJourneyApiResponse;
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchJourneysFailedSteps({
|
||||
checkGroups,
|
||||
}: {
|
||||
checkGroups: string[];
|
||||
}): Promise<SyntheticsJourneyApiResponse> {
|
||||
return (await apiService.get(
|
||||
}): Promise<FailedStepsApiResponse> {
|
||||
return apiService.get(
|
||||
`/api/uptime/journeys/failed_steps`,
|
||||
{ checkGroups },
|
||||
SyntheticsJourneyApiResponseType
|
||||
)) as SyntheticsJourneyApiResponse;
|
||||
FailedStepsApiResponseType
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchLastSuccessfulStep({
|
||||
|
@ -43,15 +48,21 @@ export async function fetchLastSuccessfulStep({
|
|||
monitorId: string;
|
||||
timestamp: string;
|
||||
stepIndex: number;
|
||||
}): Promise<Ping> {
|
||||
return (await apiService.get(`/api/uptime/synthetics/step/success/`, {
|
||||
monitorId,
|
||||
timestamp,
|
||||
stepIndex,
|
||||
})) as Ping;
|
||||
}): Promise<JourneyStep> {
|
||||
return await apiService.get(
|
||||
`/api/uptime/synthetics/step/success/`,
|
||||
{
|
||||
monitorId,
|
||||
timestamp,
|
||||
stepIndex,
|
||||
},
|
||||
JourneyStepType
|
||||
);
|
||||
}
|
||||
|
||||
export async function getJourneyScreenshot(imgSrc: string) {
|
||||
export async function getJourneyScreenshot(
|
||||
imgSrc: string
|
||||
): Promise<ScreenshotImageBlob | ScreenshotRefImageData | null> {
|
||||
try {
|
||||
const imgRequest = new Request(imgSrc);
|
||||
|
||||
|
@ -61,16 +72,22 @@ export async function getJourneyScreenshot(imgSrc: string) {
|
|||
return null;
|
||||
}
|
||||
|
||||
const imgBlob = await response.blob();
|
||||
|
||||
const contentType = response.headers.get('content-type');
|
||||
const stepName = response.headers.get('caption-name');
|
||||
const maxSteps = response.headers.get('max-steps');
|
||||
|
||||
return {
|
||||
stepName,
|
||||
maxSteps: Number(maxSteps ?? 0),
|
||||
src: URL.createObjectURL(imgBlob),
|
||||
};
|
||||
const maxSteps = Number(response.headers.get('max-steps') ?? 0);
|
||||
if (contentType?.indexOf('application/json') !== -1) {
|
||||
return {
|
||||
stepName,
|
||||
maxSteps,
|
||||
ref: await response.json(),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
stepName,
|
||||
maxSteps,
|
||||
src: URL.createObjectURL(await response.blob()),
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { handleActions, Action } from 'redux-actions';
|
||||
import { Ping, SyntheticsJourneyApiResponse } from '../../../common/runtime_types';
|
||||
import { JourneyStep, SyntheticsJourneyApiResponse } from '../../../common/runtime_types';
|
||||
import { pruneJourneyState } from '../actions/journey';
|
||||
import {
|
||||
FetchJourneyStepsParams,
|
||||
|
@ -18,7 +18,7 @@ import {
|
|||
|
||||
export interface JourneyState {
|
||||
checkGroup: string;
|
||||
steps: Ping[];
|
||||
steps: JourneyStep[];
|
||||
details?: SyntheticsJourneyApiResponse['details'];
|
||||
loading: boolean;
|
||||
error?: Error;
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* 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 { getJourneyDetails } from './get_journey_details';
|
||||
import { mockSearchResult } from './helper';
|
||||
|
||||
describe('getJourneyDetails', () => {
|
||||
let mockData: unknown;
|
||||
|
||||
beforeEach(() => {
|
||||
mockData = {
|
||||
_id: 'uTjtNHoBu1okBtVwOgMb',
|
||||
_source: {
|
||||
'@timestamp': '2021-06-22T18:13:19.013Z',
|
||||
synthetics: {
|
||||
package_version: '1.0.0-beta.2',
|
||||
journey: {
|
||||
name: 'inline',
|
||||
id: 'inline',
|
||||
},
|
||||
payload: {
|
||||
source:
|
||||
'async ({ page, browser, params }) => {\n scriptFn.apply(null, [core_1.step, page, browser, params]);\n }',
|
||||
params: {},
|
||||
},
|
||||
index: 0,
|
||||
type: 'journey/start',
|
||||
},
|
||||
monitor: {
|
||||
name: 'My Monitor - inline',
|
||||
timespan: {
|
||||
lt: '2021-06-22T18:14:19.013Z',
|
||||
gte: '2021-06-22T18:13:19.013Z',
|
||||
},
|
||||
check_group: '85946468-d385-11eb-8848-acde48001122',
|
||||
id: 'my-browser-monitor-inline',
|
||||
type: 'browser',
|
||||
status: 'up',
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it('formats ref detail data', async () => {
|
||||
expect(
|
||||
await getJourneyDetails({
|
||||
uptimeEsClient: mockSearchResult(mockData),
|
||||
checkGroup: '85946468-d385-11eb-8848-acde48001122',
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"journey": Object {
|
||||
"@timestamp": "2021-06-22T18:13:19.013Z",
|
||||
"_id": "uTjtNHoBu1okBtVwOgMb",
|
||||
"monitor": Object {
|
||||
"check_group": "85946468-d385-11eb-8848-acde48001122",
|
||||
"id": "my-browser-monitor-inline",
|
||||
"name": "My Monitor - inline",
|
||||
"status": "up",
|
||||
"timespan": Object {
|
||||
"gte": "2021-06-22T18:13:19.013Z",
|
||||
"lt": "2021-06-22T18:14:19.013Z",
|
||||
},
|
||||
"type": "browser",
|
||||
},
|
||||
"synthetics": Object {
|
||||
"index": 0,
|
||||
"journey": Object {
|
||||
"id": "inline",
|
||||
"name": "inline",
|
||||
},
|
||||
"package_version": "1.0.0-beta.2",
|
||||
"payload": Object {
|
||||
"params": Object {},
|
||||
"source": "async ({ page, browser, params }) => {
|
||||
scriptFn.apply(null, [core_1.step, page, browser, params]);
|
||||
}",
|
||||
},
|
||||
"type": "journey/start",
|
||||
},
|
||||
},
|
||||
"next": Object {
|
||||
"checkGroup": "85946468-d385-11eb-8848-acde48001122",
|
||||
"timestamp": "2021-06-22T18:13:19.013Z",
|
||||
},
|
||||
"previous": Object {
|
||||
"checkGroup": "85946468-d385-11eb-8848-acde48001122",
|
||||
"timestamp": "2021-06-22T18:13:19.013Z",
|
||||
},
|
||||
"timestamp": "2021-06-22T18:13:19.013Z",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns null for 0 hits', async () => {
|
||||
expect(
|
||||
await getJourneyDetails({ uptimeEsClient: mockSearchResult([]), checkGroup: 'check_group' })
|
||||
).toBe(null);
|
||||
});
|
||||
});
|
|
@ -7,7 +7,10 @@
|
|||
|
||||
import { QueryDslQueryContainer } from '@elastic/elasticsearch/api/types';
|
||||
import { UMElasticsearchQueryFn } from '../adapters/framework';
|
||||
import { SyntheticsJourneyApiResponse } from '../../../common/runtime_types';
|
||||
import {
|
||||
JourneyStep,
|
||||
SyntheticsJourneyApiResponse,
|
||||
} from '../../../common/runtime_types/ping/synthetics';
|
||||
|
||||
export interface GetJourneyDetails {
|
||||
checkGroup: string;
|
||||
|
@ -39,8 +42,9 @@ export const getJourneyDetails: UMElasticsearchQueryFn<
|
|||
|
||||
const { body: thisJourney } = await uptimeEsClient.search({ body: baseParams });
|
||||
|
||||
if (thisJourney?.hits?.hits.length > 0) {
|
||||
const thisJourneySource: any = thisJourney.hits.hits[0]._source;
|
||||
if (thisJourney.hits.hits.length > 0) {
|
||||
const { _id, _source } = thisJourney.hits.hits[0];
|
||||
const thisJourneySource = Object.assign({ _id }, _source) as JourneyStep;
|
||||
|
||||
const baseSiblingParams = {
|
||||
query: {
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { getJourneyFailedSteps } from './get_journey_failed_steps';
|
||||
import { mockSearchResult } from './helper';
|
||||
|
||||
describe('getJourneyFailedSteps', () => {
|
||||
let mockData: unknown;
|
||||
|
||||
beforeEach(() => {
|
||||
mockData = {
|
||||
_id: 'uTjtNHoBu1okBtVwOgMb',
|
||||
_source: {
|
||||
'@timestamp': '2021-06-22T18:13:19.013Z',
|
||||
synthetics: {
|
||||
package_version: '1.0.0-beta.2',
|
||||
journey: {
|
||||
name: 'inline',
|
||||
id: 'inline',
|
||||
},
|
||||
payload: {
|
||||
source:
|
||||
'async ({ page, browser, params }) => {\n scriptFn.apply(null, [core_1.step, page, browser, params]);\n }',
|
||||
params: {},
|
||||
},
|
||||
index: 0,
|
||||
type: 'journey/start',
|
||||
},
|
||||
monitor: {
|
||||
name: 'My Monitor - inline',
|
||||
timespan: {
|
||||
lt: '2021-06-22T18:14:19.013Z',
|
||||
gte: '2021-06-22T18:13:19.013Z',
|
||||
},
|
||||
check_group: '85946468-d385-11eb-8848-acde48001122',
|
||||
id: 'my-browser-monitor-inline',
|
||||
type: 'browser',
|
||||
status: 'up',
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it('formats failed steps', async () => {
|
||||
expect(
|
||||
await getJourneyFailedSteps({
|
||||
uptimeEsClient: mockSearchResult(mockData),
|
||||
checkGroups: ['chg1', 'chg2'],
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"@timestamp": "2021-06-22T18:13:19.013Z",
|
||||
"_id": "uTjtNHoBu1okBtVwOgMb",
|
||||
"monitor": Object {
|
||||
"check_group": "85946468-d385-11eb-8848-acde48001122",
|
||||
"id": "my-browser-monitor-inline",
|
||||
"name": "My Monitor - inline",
|
||||
"status": "up",
|
||||
"timespan": Object {
|
||||
"gte": "2021-06-22T18:13:19.013Z",
|
||||
"lt": "2021-06-22T18:14:19.013Z",
|
||||
},
|
||||
"type": "browser",
|
||||
},
|
||||
"synthetics": Object {
|
||||
"index": 0,
|
||||
"journey": Object {
|
||||
"id": "inline",
|
||||
"name": "inline",
|
||||
},
|
||||
"package_version": "1.0.0-beta.2",
|
||||
"payload": Object {
|
||||
"params": Object {},
|
||||
"source": "async ({ page, browser, params }) => {
|
||||
scriptFn.apply(null, [core_1.step, page, browser, params]);
|
||||
}",
|
||||
},
|
||||
"type": "journey/start",
|
||||
},
|
||||
"timestamp": "2021-06-22T18:13:19.013Z",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -6,19 +6,18 @@
|
|||
*/
|
||||
|
||||
import { QueryDslQueryContainer } from '@elastic/elasticsearch/api/types';
|
||||
import { SearchHit } from '../../../../../../src/core/types/elasticsearch';
|
||||
import { asMutableArray } from '../../../common/utils/as_mutable_array';
|
||||
import { UMElasticsearchQueryFn } from '../adapters/framework';
|
||||
import { Ping } from '../../../common/runtime_types';
|
||||
import { JourneyStep } from '../../../common/runtime_types/ping/synthetics';
|
||||
|
||||
export interface GetJourneyStepsParams {
|
||||
checkGroups: string[];
|
||||
}
|
||||
|
||||
export const getJourneyFailedSteps: UMElasticsearchQueryFn<GetJourneyStepsParams, Ping> = async ({
|
||||
uptimeEsClient,
|
||||
checkGroups,
|
||||
}) => {
|
||||
export const getJourneyFailedSteps: UMElasticsearchQueryFn<
|
||||
GetJourneyStepsParams,
|
||||
JourneyStep[]
|
||||
> = async ({ uptimeEsClient, checkGroups }) => {
|
||||
const params = {
|
||||
query: {
|
||||
bool: {
|
||||
|
@ -53,11 +52,11 @@ export const getJourneyFailedSteps: UMElasticsearchQueryFn<GetJourneyStepsParams
|
|||
|
||||
const { body: result } = await uptimeEsClient.search({ body: params });
|
||||
|
||||
return ((result.hits.hits as Array<SearchHit<Ping>>).map((h) => {
|
||||
const source = h._source as Ping & { '@timestamp': string };
|
||||
return result.hits.hits.map(({ _id, _source }) => {
|
||||
const step = Object.assign({ _id }, _source) as JourneyStep;
|
||||
return {
|
||||
...source,
|
||||
timestamp: source['@timestamp'],
|
||||
...step,
|
||||
timestamp: step['@timestamp'],
|
||||
};
|
||||
}) as unknown) as Ping;
|
||||
});
|
||||
};
|
||||
|
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* 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 { getJourneyScreenshot } from './get_journey_screenshot';
|
||||
import { mockSearchResult } from './helper';
|
||||
|
||||
describe('getJourneyScreenshot', () => {
|
||||
it('returns screenshot data', async () => {
|
||||
const screenshotResult = {
|
||||
_id: 'id',
|
||||
_source: {
|
||||
synthetics: {
|
||||
blob_mime: 'image/jpeg',
|
||||
blob: 'image data',
|
||||
step: {
|
||||
name: 'load homepage',
|
||||
},
|
||||
type: 'step/screenshot',
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(
|
||||
await getJourneyScreenshot({
|
||||
uptimeEsClient: mockSearchResult([], {
|
||||
// @ts-expect-error incomplete search result
|
||||
step: { image: { hits: { hits: [screenshotResult] } } },
|
||||
}),
|
||||
checkGroup: 'checkGroup',
|
||||
stepIndex: 0,
|
||||
})
|
||||
).toEqual({
|
||||
synthetics: {
|
||||
blob: 'image data',
|
||||
blob_mime: 'image/jpeg',
|
||||
step: {
|
||||
name: 'load homepage',
|
||||
},
|
||||
type: 'step/screenshot',
|
||||
},
|
||||
totalSteps: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns ref data', async () => {
|
||||
const screenshotRefResult = {
|
||||
_id: 'id',
|
||||
_source: {
|
||||
'@timestamp': '123',
|
||||
monitor: {
|
||||
check_group: 'check_group',
|
||||
},
|
||||
screenshot_ref: {
|
||||
width: 10,
|
||||
height: 20,
|
||||
blocks: [
|
||||
{
|
||||
hash: 'hash1',
|
||||
top: 0,
|
||||
left: 0,
|
||||
height: 2,
|
||||
width: 4,
|
||||
},
|
||||
{
|
||||
hash: 'hash2',
|
||||
top: 0,
|
||||
left: 2,
|
||||
height: 2,
|
||||
width: 4,
|
||||
},
|
||||
],
|
||||
},
|
||||
synthetics: {
|
||||
package_version: 'v1.0.0',
|
||||
step: {
|
||||
name: 'name',
|
||||
index: 0,
|
||||
},
|
||||
type: 'step/screenshot_ref',
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(
|
||||
await getJourneyScreenshot({
|
||||
uptimeEsClient: mockSearchResult([], {
|
||||
// @ts-expect-error incomplete search result
|
||||
step: { image: { hits: { hits: [screenshotRefResult] } } },
|
||||
}),
|
||||
checkGroup: 'checkGroup',
|
||||
stepIndex: 0,
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"@timestamp": "123",
|
||||
"monitor": Object {
|
||||
"check_group": "check_group",
|
||||
},
|
||||
"screenshot_ref": Object {
|
||||
"blocks": Array [
|
||||
Object {
|
||||
"hash": "hash1",
|
||||
"height": 2,
|
||||
"left": 0,
|
||||
"top": 0,
|
||||
"width": 4,
|
||||
},
|
||||
Object {
|
||||
"hash": "hash2",
|
||||
"height": 2,
|
||||
"left": 2,
|
||||
"top": 0,
|
||||
"width": 4,
|
||||
},
|
||||
],
|
||||
"height": 20,
|
||||
"width": 10,
|
||||
},
|
||||
"synthetics": Object {
|
||||
"package_version": "v1.0.0",
|
||||
"step": Object {
|
||||
"index": 0,
|
||||
"name": "name",
|
||||
},
|
||||
"type": "step/screenshot_ref",
|
||||
},
|
||||
"totalSteps": 0,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -6,30 +6,22 @@
|
|||
*/
|
||||
|
||||
import { QueryDslQueryContainer } from '@elastic/elasticsearch/api/types';
|
||||
import { UMElasticsearchQueryFn } from '../adapters/framework';
|
||||
import { Ping } from '../../../common/runtime_types/ping';
|
||||
import { UMElasticsearchQueryFn } from '../adapters';
|
||||
import { RefResult, FullScreenshot } from '../../../common/runtime_types/ping/synthetics';
|
||||
|
||||
export interface GetJourneyScreenshotParams {
|
||||
checkGroup: string;
|
||||
stepIndex: number;
|
||||
interface ResultType {
|
||||
_source: RefResult | FullScreenshot;
|
||||
}
|
||||
|
||||
export interface GetJourneyScreenshotResults {
|
||||
blob: string | null;
|
||||
mimeType: string | null;
|
||||
stepName: string;
|
||||
totalSteps: number;
|
||||
}
|
||||
export type ScreenshotReturnTypesUnion =
|
||||
| ((FullScreenshot | RefResult) & { totalSteps: number })
|
||||
| null;
|
||||
|
||||
export const getJourneyScreenshot: UMElasticsearchQueryFn<
|
||||
GetJourneyScreenshotParams,
|
||||
any
|
||||
> = async ({
|
||||
uptimeEsClient,
|
||||
checkGroup,
|
||||
stepIndex,
|
||||
}): Promise<GetJourneyScreenshotResults | null> => {
|
||||
const params = {
|
||||
{ checkGroup: string; stepIndex: number },
|
||||
ScreenshotReturnTypesUnion
|
||||
> = async ({ checkGroup, stepIndex, uptimeEsClient }) => {
|
||||
const body = {
|
||||
track_total_hits: true,
|
||||
size: 0,
|
||||
query: {
|
||||
|
@ -41,8 +33,8 @@ export const getJourneyScreenshot: UMElasticsearchQueryFn<
|
|||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'synthetics.type': 'step/screenshot',
|
||||
terms: {
|
||||
'synthetics.type': ['step/screenshot', 'step/screenshot_ref'],
|
||||
},
|
||||
},
|
||||
] as QueryDslQueryContainer[],
|
||||
|
@ -59,25 +51,22 @@ export const getJourneyScreenshot: UMElasticsearchQueryFn<
|
|||
image: {
|
||||
top_hits: {
|
||||
size: 1,
|
||||
_source: ['synthetics.blob', 'synthetics.blob_mime', 'synthetics.step.name'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const { body: result } = await uptimeEsClient.search({ body: params });
|
||||
|
||||
if (result?.hits?.total.value < 1) {
|
||||
return null;
|
||||
}
|
||||
const result = await uptimeEsClient.search({ body });
|
||||
|
||||
const stepHit = result?.aggregations?.step.image.hits.hits[0]?._source as Ping;
|
||||
const screenshotsOrRefs =
|
||||
(result.body.aggregations?.step.image.hits.hits as ResultType[]) ?? null;
|
||||
|
||||
if (screenshotsOrRefs.length === 0) return null;
|
||||
|
||||
return {
|
||||
blob: stepHit?.synthetics?.blob ?? null,
|
||||
mimeType: stepHit?.synthetics?.blob_mime ?? null,
|
||||
stepName: stepHit?.synthetics?.step?.name ?? '',
|
||||
totalSteps: result?.hits?.total.value,
|
||||
...screenshotsOrRefs[0]._source,
|
||||
totalSteps: result.body.hits.total.value,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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 { getJourneyScreenshotBlocks } from './get_journey_screenshot_blocks';
|
||||
import { mockSearchResult } from './helper';
|
||||
|
||||
describe('getJourneyScreenshotBlocks', () => {
|
||||
it('returns formatted blocks', async () => {
|
||||
expect(
|
||||
await getJourneyScreenshotBlocks({
|
||||
uptimeEsClient: mockSearchResult([
|
||||
{
|
||||
_id: 'hash1',
|
||||
_source: {
|
||||
synthetics: {
|
||||
blob: 'image data',
|
||||
blob_mime: 'image/jpeg',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
_id: 'hash2',
|
||||
_source: {
|
||||
synthetics: {
|
||||
blob: 'image data',
|
||||
blob_mime: 'image/jpeg',
|
||||
},
|
||||
},
|
||||
},
|
||||
]),
|
||||
blockIds: ['hash1', 'hash2'],
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"id": "hash1",
|
||||
"synthetics": Object {
|
||||
"blob": "image data",
|
||||
"blob_mime": "image/jpeg",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "hash2",
|
||||
"synthetics": Object {
|
||||
"blob": "image data",
|
||||
"blob_mime": "image/jpeg",
|
||||
},
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ScreenshotBlockDoc } from '../../../common/runtime_types/ping/synthetics';
|
||||
import { UMElasticsearchQueryFn } from '../adapters/framework';
|
||||
|
||||
interface ScreenshotBlockResultType {
|
||||
_id: string;
|
||||
_source: {
|
||||
synthetics: {
|
||||
blob: string;
|
||||
blob_mime: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export const getJourneyScreenshotBlocks: UMElasticsearchQueryFn<
|
||||
{ blockIds: string[] },
|
||||
ScreenshotBlockDoc[]
|
||||
> = async ({ blockIds, uptimeEsClient }) => {
|
||||
const body = {
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
ids: {
|
||||
values: blockIds,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
size: 1000,
|
||||
};
|
||||
|
||||
const fetchScreenshotBlocksResult = await uptimeEsClient.search({ body });
|
||||
|
||||
return (fetchScreenshotBlocksResult.body.hits.hits as ScreenshotBlockResultType[]).map(
|
||||
({ _id, _source }) => ({
|
||||
id: _id,
|
||||
synthetics: {
|
||||
blob: _source.synthetics.blob,
|
||||
blob_mime: _source.synthetics.blob_mime,
|
||||
},
|
||||
})
|
||||
);
|
||||
};
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { JourneyStep } from '../../../common/runtime_types/ping/synthetics';
|
||||
import { getJourneySteps, formatSyntheticEvents } from './get_journey_steps';
|
||||
import { getUptimeESMockClient } from './helper';
|
||||
|
||||
|
@ -13,10 +14,11 @@ describe('getJourneySteps request module', () => {
|
|||
it('returns default steps if none are provided', () => {
|
||||
expect(formatSyntheticEvents()).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"step/end",
|
||||
"cmd/status",
|
||||
"step/screenshot",
|
||||
"journey/browserconsole",
|
||||
"step/end",
|
||||
"step/screenshot",
|
||||
"step/screenshot_ref",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
@ -107,8 +109,8 @@ describe('getJourneySteps request module', () => {
|
|||
it('formats ES result', async () => {
|
||||
const { esClient: mockEsClient, uptimeEsClient } = getUptimeESMockClient();
|
||||
|
||||
mockEsClient.search.mockResolvedValueOnce(data as any);
|
||||
const result: any = await getJourneySteps({
|
||||
mockEsClient.search.mockResolvedValueOnce(data);
|
||||
const result: JourneyStep[] = await getJourneySteps({
|
||||
uptimeEsClient,
|
||||
checkGroup: '2bf952dc-64b5-11eb-8b3b-42010a84000d',
|
||||
});
|
||||
|
@ -120,10 +122,11 @@ describe('getJourneySteps request module', () => {
|
|||
Object {
|
||||
"terms": Object {
|
||||
"synthetics.type": Array [
|
||||
"step/end",
|
||||
"cmd/status",
|
||||
"step/screenshot",
|
||||
"journey/browserconsole",
|
||||
"step/end",
|
||||
"step/screenshot",
|
||||
"step/screenshot_ref",
|
||||
],
|
||||
},
|
||||
}
|
||||
|
@ -156,10 +159,12 @@ describe('getJourneySteps request module', () => {
|
|||
|
||||
expect(result).toHaveLength(2);
|
||||
// `getJourneySteps` is responsible for formatting these fields, so we need to check them
|
||||
result.forEach((step: any) => {
|
||||
expect(['2021-02-01T17:45:19.001Z', '2021-02-01T17:45:49.944Z']).toContain(step.timestamp);
|
||||
expect(['o6myXncBFt2V8m6r6z-r', 'IjqzXncBn2sjqrYxYoCG']).toContain(step.docId);
|
||||
expect(step.synthetics.screenshotExists).toBeDefined();
|
||||
result.forEach((step: JourneyStep) => {
|
||||
expect(['2021-02-01T17:45:19.001Z', '2021-02-01T17:45:49.944Z']).toContain(
|
||||
step['@timestamp']
|
||||
);
|
||||
expect(['o6myXncBFt2V8m6r6z-r', 'IjqzXncBn2sjqrYxYoCG']).toContain(step._id);
|
||||
expect(step.synthetics.isFullScreenshot).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -168,9 +173,9 @@ describe('getJourneySteps request module', () => {
|
|||
|
||||
data.body.hits.hits[0]._source.synthetics.type = 'step/screenshot';
|
||||
data.body.hits.hits[0]._source.synthetics.step.index = 2;
|
||||
mockEsClient.search.mockResolvedValueOnce(data as any);
|
||||
mockEsClient.search.mockResolvedValueOnce(data);
|
||||
|
||||
const result: any = await getJourneySteps({
|
||||
const result: JourneyStep[] = await getJourneySteps({
|
||||
uptimeEsClient,
|
||||
checkGroup: '2bf952dc-64b5-11eb-8b3b-42010a84000d',
|
||||
syntheticEventTypes: ['stderr', 'step/end'],
|
||||
|
@ -191,7 +196,7 @@ describe('getJourneySteps request module', () => {
|
|||
`);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].synthetics.screenshotExists).toBe(true);
|
||||
expect(result[0].synthetics.isFullScreenshot).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,17 +6,22 @@
|
|||
*/
|
||||
|
||||
import { QueryDslQueryContainer } from '@elastic/elasticsearch/api/types';
|
||||
import { SearchHit } from 'src/core/types/elasticsearch/search';
|
||||
import { asMutableArray } from '../../../common/utils/as_mutable_array';
|
||||
import { UMElasticsearchQueryFn } from '../adapters/framework';
|
||||
import { Ping } from '../../../common/runtime_types';
|
||||
import { JourneyStep } from '../../../common/runtime_types/ping/synthetics';
|
||||
|
||||
export interface GetJourneyStepsParams {
|
||||
checkGroup: string;
|
||||
syntheticEventTypes?: string | string[];
|
||||
}
|
||||
|
||||
const defaultEventTypes = ['step/end', 'cmd/status', 'step/screenshot', 'journey/browserconsole'];
|
||||
const defaultEventTypes = [
|
||||
'cmd/status',
|
||||
'journey/browserconsole',
|
||||
'step/end',
|
||||
'step/screenshot',
|
||||
'step/screenshot_ref',
|
||||
];
|
||||
|
||||
export const formatSyntheticEvents = (eventTypes?: string | string[]) => {
|
||||
if (!eventTypes) {
|
||||
|
@ -26,11 +31,12 @@ export const formatSyntheticEvents = (eventTypes?: string | string[]) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const getJourneySteps: UMElasticsearchQueryFn<GetJourneyStepsParams, Ping> = async ({
|
||||
uptimeEsClient,
|
||||
checkGroup,
|
||||
syntheticEventTypes,
|
||||
}) => {
|
||||
type ResultType = JourneyStep & { '@timestamp': string };
|
||||
|
||||
export const getJourneySteps: UMElasticsearchQueryFn<
|
||||
GetJourneyStepsParams,
|
||||
JourneyStep[]
|
||||
> = async ({ uptimeEsClient, checkGroup, syntheticEventTypes }) => {
|
||||
const params = {
|
||||
query: {
|
||||
bool: {
|
||||
|
@ -53,28 +59,43 @@ export const getJourneySteps: UMElasticsearchQueryFn<GetJourneyStepsParams, Ping
|
|||
{ '@timestamp': { order: 'asc' } },
|
||||
] as const),
|
||||
_source: {
|
||||
excludes: ['synthetics.blob'],
|
||||
excludes: ['synthetics.blob', 'screenshot_ref'],
|
||||
},
|
||||
size: 500,
|
||||
};
|
||||
const { body: result } = await uptimeEsClient.search({ body: params });
|
||||
|
||||
const screenshotIndexes: number[] = (result.hits.hits as Array<SearchHit<Ping>>)
|
||||
.filter((h) => h._source?.synthetics?.type === 'step/screenshot')
|
||||
.map((h) => h._source?.synthetics?.step?.index as number);
|
||||
const steps = result.hits.hits.map(
|
||||
({ _id, _source }) => Object.assign({ _id }, _source) as ResultType
|
||||
);
|
||||
|
||||
return ((result.hits.hits as Array<SearchHit<Ping>>)
|
||||
.filter((h) => h._source?.synthetics?.type !== 'step/screenshot')
|
||||
.map((h) => {
|
||||
const source = h._source as Ping & { '@timestamp': string };
|
||||
return {
|
||||
...source,
|
||||
timestamp: source['@timestamp'],
|
||||
docId: h._id,
|
||||
synthetics: {
|
||||
...source.synthetics,
|
||||
screenshotExists: screenshotIndexes.some((i) => i === source.synthetics?.step?.index),
|
||||
},
|
||||
};
|
||||
}) as unknown) as Ping;
|
||||
const screenshotIndexList: number[] = [];
|
||||
const refIndexList: number[] = [];
|
||||
const stepsWithoutImages: ResultType[] = [];
|
||||
|
||||
/**
|
||||
* Store screenshot indexes, we use these to determine if a step has a screenshot below.
|
||||
* Store steps that are not screenshots, we return these to the client.
|
||||
*/
|
||||
for (const step of steps) {
|
||||
const { synthetics } = step;
|
||||
if (synthetics.type === 'step/screenshot' && synthetics?.step?.index) {
|
||||
screenshotIndexList.push(synthetics.step.index);
|
||||
} else if (synthetics.type === 'step/screenshot_ref' && synthetics?.step?.index) {
|
||||
refIndexList.push(synthetics.step.index);
|
||||
} else {
|
||||
stepsWithoutImages.push(step);
|
||||
}
|
||||
}
|
||||
|
||||
return stepsWithoutImages.map(({ _id, ...rest }) => ({
|
||||
_id,
|
||||
...rest,
|
||||
timestamp: rest['@timestamp'],
|
||||
synthetics: {
|
||||
...rest.synthetics,
|
||||
isFullScreenshot: screenshotIndexList.some((i) => i === rest?.synthetics?.step?.index),
|
||||
isScreenshotRef: refIndexList.some((i) => i === rest?.synthetics?.step?.index),
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { estypes } from '@elastic/elasticsearch';
|
||||
import { UMElasticsearchQueryFn } from '../adapters/framework';
|
||||
import { Ping } from '../../../common/runtime_types/ping';
|
||||
import { JourneyStep } from '../../../common/runtime_types/ping';
|
||||
|
||||
export interface GetStepScreenshotParams {
|
||||
monitorId: string;
|
||||
|
@ -17,7 +17,7 @@ export interface GetStepScreenshotParams {
|
|||
|
||||
export const getStepLastSuccessfulStep: UMElasticsearchQueryFn<
|
||||
GetStepScreenshotParams,
|
||||
any
|
||||
JourneyStep | null
|
||||
> = async ({ uptimeEsClient, monitorId, stepIndex, timestamp }) => {
|
||||
const lastSuccessCheckParams: estypes.SearchRequest['body'] = {
|
||||
size: 1,
|
||||
|
@ -65,11 +65,11 @@ export const getStepLastSuccessfulStep: UMElasticsearchQueryFn<
|
|||
|
||||
const { body: result } = await uptimeEsClient.search({ body: lastSuccessCheckParams });
|
||||
|
||||
if (result?.hits?.total.value < 1) {
|
||||
if (result.hits.total.value < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const step = result?.hits.hits[0]._source as Ping & { '@timestamp': string };
|
||||
const step = result.hits.hits[0]._source as JourneyStep & { '@timestamp': string };
|
||||
|
||||
return {
|
||||
...step,
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { SearchResponse } from '@elastic/elasticsearch/api/types';
|
||||
import { AggregationsAggregate, SearchResponse } from '@elastic/elasticsearch/api/types';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { ElasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks';
|
||||
import {
|
||||
|
@ -81,3 +81,34 @@ export const getUptimeESMockClient = (
|
|||
}),
|
||||
};
|
||||
};
|
||||
|
||||
export function mockSearchResult(
|
||||
data: unknown,
|
||||
aggregations: Record<string, AggregationsAggregate> = {}
|
||||
): UptimeESClient {
|
||||
const { esClient: mockEsClient, uptimeEsClient } = getUptimeESMockClient();
|
||||
|
||||
// @ts-expect-error incomplete search response
|
||||
mockEsClient.search.mockResolvedValue({
|
||||
body: {
|
||||
took: 18,
|
||||
timed_out: false,
|
||||
_shards: {
|
||||
total: 1,
|
||||
successful: 1,
|
||||
skipped: 0,
|
||||
failed: 0,
|
||||
},
|
||||
hits: {
|
||||
hits: Array.isArray(data) ? data : [data],
|
||||
max_score: 0.0,
|
||||
total: {
|
||||
value: Array.isArray(data) ? data.length : 0,
|
||||
relation: 'gte',
|
||||
},
|
||||
},
|
||||
aggregations,
|
||||
},
|
||||
});
|
||||
return uptimeEsClient;
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ 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';
|
||||
import { getJourneyScreenshotBlocks } from './get_journey_screenshot_blocks';
|
||||
|
||||
export const requests = {
|
||||
getCerts,
|
||||
|
@ -45,6 +46,7 @@ export const requests = {
|
|||
getJourneyFailedSteps,
|
||||
getStepLastSuccessfulStep,
|
||||
getJourneyScreenshot,
|
||||
getJourneyScreenshotBlocks,
|
||||
getJourneyDetails,
|
||||
getNetworkEvents,
|
||||
};
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
createGetPingsRoute,
|
||||
createJourneyRoute,
|
||||
createJourneyScreenshotRoute,
|
||||
createJourneyScreenshotBlockRoute,
|
||||
} from './pings';
|
||||
import { createGetDynamicSettingsRoute, createPostDynamicSettingsRoute } from './dynamic_settings';
|
||||
import { createLogPageViewRoute } from './telemetry';
|
||||
|
@ -51,6 +52,7 @@ export const restApiRoutes: UMRestApiRouteFactory[] = [
|
|||
createGetMonitorDurationRoute,
|
||||
createJourneyRoute,
|
||||
createJourneyScreenshotRoute,
|
||||
createJourneyScreenshotBlockRoute,
|
||||
createNetworkEventsRoute,
|
||||
createJourneyFailedStepsRoute,
|
||||
createLastSuccessfulStepRoute,
|
||||
|
|
|
@ -9,3 +9,4 @@ export { createGetPingsRoute } from './get_pings';
|
|||
export { createGetPingHistogramRoute } from './get_ping_histogram';
|
||||
export { createJourneyRoute } from './journeys';
|
||||
export { createJourneyScreenshotRoute } from './journey_screenshots';
|
||||
export { createJourneyScreenshotBlockRoute } from './journey_screenshot_blocks';
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 * as t from 'io-ts';
|
||||
import { isRight } from 'fp-ts/lib/Either';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { UMServerLibs } from '../../lib/lib';
|
||||
import { UMRestApiRouteFactory } from '../types';
|
||||
import { ScreenshotBlockDoc } from '../../../common/runtime_types/ping/synthetics';
|
||||
|
||||
export const createJourneyScreenshotBlockRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({
|
||||
method: 'GET',
|
||||
path: '/api/uptime/journey/screenshot/block',
|
||||
validate: {
|
||||
query: schema.object({
|
||||
hash: schema.oneOf([schema.string(), schema.arrayOf(schema.string())]),
|
||||
_inspect: schema.maybe(schema.boolean()),
|
||||
}),
|
||||
},
|
||||
handler: async ({ request, response, uptimeEsClient }) => {
|
||||
const { hash } = request.query;
|
||||
|
||||
const decoded = t.union([t.string, t.array(t.string)]).decode(hash);
|
||||
if (!isRight(decoded)) {
|
||||
return response.badRequest();
|
||||
}
|
||||
const { right: data } = decoded;
|
||||
let result: ScreenshotBlockDoc[];
|
||||
try {
|
||||
result = await libs.requests.getJourneyScreenshotBlocks({
|
||||
blockIds: Array.isArray(data) ? data : [data],
|
||||
uptimeEsClient,
|
||||
});
|
||||
} catch (e: unknown) {
|
||||
return response.custom({ statusCode: 500, body: { message: e } });
|
||||
}
|
||||
if (result.length === 0) {
|
||||
return response.notFound();
|
||||
}
|
||||
return response.ok({
|
||||
body: result,
|
||||
headers: {
|
||||
// we can cache these blocks with extreme prejudice as they are inherently unchanging
|
||||
// when queried by ID, since the ID is the hash of the data
|
||||
'Cache-Control': 'max-age=604800',
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
|
@ -6,9 +6,23 @@
|
|||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import {
|
||||
isRefResult,
|
||||
isFullScreenshot,
|
||||
ScreenshotBlockDoc,
|
||||
} from '../../../common/runtime_types/ping/synthetics';
|
||||
import { UMServerLibs } from '../../lib/lib';
|
||||
import { ScreenshotReturnTypesUnion } from '../../lib/requests/get_journey_screenshot';
|
||||
import { UMRestApiRouteFactory } from '../types';
|
||||
|
||||
function getSharedHeaders(stepName: string, totalSteps: number) {
|
||||
return {
|
||||
'cache-control': 'max-age=600',
|
||||
'caption-name': stepName,
|
||||
'max-steps': String(totalSteps),
|
||||
};
|
||||
}
|
||||
|
||||
export const createJourneyScreenshotRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({
|
||||
method: 'GET',
|
||||
path: '/api/uptime/journey/screenshot/{checkGroup}/{stepIndex}',
|
||||
|
@ -25,23 +39,49 @@ export const createJourneyScreenshotRoute: UMRestApiRouteFactory = (libs: UMServ
|
|||
handler: async ({ uptimeEsClient, request, response }) => {
|
||||
const { checkGroup, stepIndex } = request.params;
|
||||
|
||||
const result = await libs.requests.getJourneyScreenshot({
|
||||
uptimeEsClient,
|
||||
checkGroup,
|
||||
stepIndex,
|
||||
});
|
||||
|
||||
if (result === null || !result.blob) {
|
||||
return response.notFound();
|
||||
let result: ScreenshotReturnTypesUnion | null = null;
|
||||
try {
|
||||
result = await libs.requests.getJourneyScreenshot({
|
||||
uptimeEsClient,
|
||||
checkGroup,
|
||||
stepIndex,
|
||||
});
|
||||
} catch (e) {
|
||||
return response.customError({ body: { message: e }, statusCode: 500 });
|
||||
}
|
||||
return response.ok({
|
||||
body: Buffer.from(result.blob, 'base64'),
|
||||
headers: {
|
||||
'content-type': result.mimeType || 'image/png', // falls back to 'image/png' for earlier versions of synthetics
|
||||
'cache-control': 'max-age=600',
|
||||
'caption-name': result.stepName,
|
||||
'max-steps': result.totalSteps,
|
||||
},
|
||||
});
|
||||
|
||||
if (isFullScreenshot(result)) {
|
||||
if (!result.synthetics.blob) {
|
||||
return response.notFound();
|
||||
}
|
||||
|
||||
return response.ok({
|
||||
body: Buffer.from(result.synthetics.blob, 'base64'),
|
||||
headers: {
|
||||
'content-type': result.synthetics.blob_mime || 'image/png', // falls back to 'image/png' for earlier versions of synthetics
|
||||
...getSharedHeaders(result.synthetics.step.name, result.totalSteps),
|
||||
},
|
||||
});
|
||||
} else if (isRefResult(result)) {
|
||||
const blockIds = result.screenshot_ref.blocks.map(({ hash }) => hash);
|
||||
let blocks: ScreenshotBlockDoc[];
|
||||
try {
|
||||
blocks = await libs.requests.getJourneyScreenshotBlocks({
|
||||
uptimeEsClient,
|
||||
blockIds,
|
||||
});
|
||||
} catch (e: unknown) {
|
||||
return response.custom({ statusCode: 500, body: { message: e } });
|
||||
}
|
||||
return response.ok({
|
||||
body: {
|
||||
screenshotRef: result,
|
||||
blocks,
|
||||
},
|
||||
headers: getSharedHeaders(result.synthetics.step.name, result.totalSteps ?? 0),
|
||||
});
|
||||
}
|
||||
|
||||
return response.notFound();
|
||||
},
|
||||
});
|
||||
|
|
|
@ -25,27 +25,31 @@ export const createJourneyRoute: UMRestApiRouteFactory = (libs: UMServerLibs) =>
|
|||
_inspect: schema.maybe(schema.boolean()),
|
||||
}),
|
||||
},
|
||||
handler: async ({ uptimeEsClient, request }): Promise<any> => {
|
||||
handler: async ({ uptimeEsClient, request, response }): Promise<any> => {
|
||||
const { checkGroup } = request.params;
|
||||
const { syntheticEventTypes } = request.query;
|
||||
|
||||
const [result, details] = await Promise.all([
|
||||
await libs.requests.getJourneySteps({
|
||||
uptimeEsClient,
|
||||
checkGroup,
|
||||
syntheticEventTypes,
|
||||
}),
|
||||
await libs.requests.getJourneyDetails({
|
||||
uptimeEsClient,
|
||||
checkGroup,
|
||||
}),
|
||||
]);
|
||||
try {
|
||||
const [result, details] = await Promise.all([
|
||||
await libs.requests.getJourneySteps({
|
||||
uptimeEsClient,
|
||||
checkGroup,
|
||||
syntheticEventTypes,
|
||||
}),
|
||||
await libs.requests.getJourneyDetails({
|
||||
uptimeEsClient,
|
||||
checkGroup,
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
checkGroup,
|
||||
steps: result,
|
||||
details,
|
||||
};
|
||||
return {
|
||||
checkGroup,
|
||||
steps: result,
|
||||
details,
|
||||
};
|
||||
} catch (e: unknown) {
|
||||
return response.custom({ statusCode: 500, body: { message: e } });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -58,16 +62,19 @@ export const createJourneyFailedStepsRoute: UMRestApiRouteFactory = (libs: UMSer
|
|||
_inspect: schema.maybe(schema.boolean()),
|
||||
}),
|
||||
},
|
||||
handler: async ({ uptimeEsClient, request }): Promise<any> => {
|
||||
handler: async ({ uptimeEsClient, request, response }): Promise<any> => {
|
||||
const { checkGroups } = request.query;
|
||||
const result = await libs.requests.getJourneyFailedSteps({
|
||||
uptimeEsClient,
|
||||
checkGroups,
|
||||
});
|
||||
|
||||
return {
|
||||
checkGroups,
|
||||
steps: result,
|
||||
};
|
||||
try {
|
||||
const result = await libs.requests.getJourneyFailedSteps({
|
||||
uptimeEsClient,
|
||||
checkGroups,
|
||||
});
|
||||
return {
|
||||
checkGroups,
|
||||
steps: result,
|
||||
};
|
||||
} catch (e) {
|
||||
return response.customError({ statusCode: 500, body: e });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -6,6 +6,11 @@
|
|||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import {
|
||||
isRefResult,
|
||||
isFullScreenshot,
|
||||
JourneyStep,
|
||||
} from '../../../common/runtime_types/ping/synthetics';
|
||||
import { UMServerLibs } from '../../lib/lib';
|
||||
import { UMRestApiRouteFactory } from '../types';
|
||||
|
||||
|
@ -23,11 +28,36 @@ export const createLastSuccessfulStepRoute: UMRestApiRouteFactory = (libs: UMSer
|
|||
handler: async ({ uptimeEsClient, request, response }) => {
|
||||
const { timestamp, monitorId, stepIndex } = request.query;
|
||||
|
||||
return await libs.requests.getStepLastSuccessfulStep({
|
||||
const step: JourneyStep | null = await libs.requests.getStepLastSuccessfulStep({
|
||||
uptimeEsClient,
|
||||
monitorId,
|
||||
stepIndex,
|
||||
timestamp,
|
||||
});
|
||||
|
||||
if (step === null) {
|
||||
return response.notFound();
|
||||
}
|
||||
|
||||
if (!step.synthetics?.step?.index) {
|
||||
return response.ok({ body: step });
|
||||
}
|
||||
|
||||
const screenshot = await libs.requests.getJourneyScreenshot({
|
||||
uptimeEsClient,
|
||||
checkGroup: step.monitor.check_group,
|
||||
stepIndex: step.synthetics.step.index,
|
||||
});
|
||||
|
||||
if (screenshot === null) {
|
||||
return response.ok({ body: step });
|
||||
}
|
||||
|
||||
step.synthetics.isScreenshotRef = isRefResult(screenshot);
|
||||
step.synthetics.isFullScreenshot = isFullScreenshot(screenshot);
|
||||
|
||||
return response.ok({
|
||||
body: step,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue