[Uptime] Added step duraion in step list (#116266) (#120755)

Co-authored-by: Shahzad <shahzad.muhammad@elastic.co>
This commit is contained in:
Kibana Machine 2021-12-08 09:06:54 -05:00 committed by GitHub
parent 160e56d9d5
commit 15e4eb0b9d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 8292 additions and 167 deletions

View file

@ -26,7 +26,7 @@ export interface ExploratoryEmbeddableProps {
showCalculationMethod?: boolean;
axisTitlesVisibility?: XYState['axisTitlesVisibilitySettings'];
legendIsVisible?: boolean;
dataTypesIndexPatterns?: Record<AppDataType, string>;
dataTypesIndexPatterns?: Partial<Record<AppDataType, string>>;
reportConfigMap?: ReportConfigMap;
}

View file

@ -19,11 +19,16 @@ const buildOrCondition = (values: string[]) => {
}
return `(${values.join(' or ')})`;
};
function addSlashes(str: string) {
return (str + '').replace(/[\\"']/g, '\\$&').replace(/\u0000/g, '\\0');
}
export const urlFiltersToKueryString = (urlFilters: UrlFilter[]): string => {
let kueryString = '';
urlFilters.forEach(({ field, values, notValues, wildcards, notWildcards }) => {
const valuesT = values?.map((val) => `"${val}"`);
const notValuesT = notValues?.map((val) => `"${val}"`);
const valuesT = values?.map((val) => `"${addSlashes(val)}"`);
const notValuesT = notValues?.map((val) => `"${addSlashes(val)}"`);
const wildcardsT = wildcards?.map((val) => `*${val}*`);
const notWildcardsT = notWildcards?.map((val) => `*${val}*`);

View file

@ -44,8 +44,12 @@ export const JourneyStepType = t.intersection([
text: t.string,
}),
step: t.type({
status: t.string,
index: t.number,
name: t.string,
duration: t.type({
us: t.number,
}),
}),
isFullScreenshot: t.boolean,
isScreenshotRef: t.boolean,

File diff suppressed because it is too large Load diff

View file

@ -6,3 +6,4 @@
*/
export * from './uptime.journey';
export * from './step_duration.journey';

View file

@ -0,0 +1,48 @@
/*
* 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 { journey, step, expect, before } from '@elastic/synthetics';
import { loginToKibana, waitForLoadingToFinish } from './utils';
journey('StepsDuration', async ({ page, params }) => {
before(async () => {
await waitForLoadingToFinish({ page });
});
const queryParams = new URLSearchParams({
dateRangeStart: '2021-11-21T22:06:06.502Z',
dateRangeEnd: '2021-11-21T22:10:08.203Z',
}).toString();
const baseUrl = `${params.kibanaUrl}/app/uptime`;
step('Go to uptime', async () => {
await page.goto(`${baseUrl}?${queryParams}`, {
waitUntil: 'networkidle',
});
await loginToKibana({ page });
});
step('Go to monitor details', async () => {
await page.click('text=Dismiss');
await page.click('button:has-text("test-monitor - inline")');
expect(page.url()).toBe(`${baseUrl}/monitor/dGVzdC1tb25pdG9yLWlubGluZQ==/?${queryParams}`);
});
step('Go to journey details', async () => {
await page.click('text=18 seconds');
expect(page.url()).toBe(`${baseUrl}/journey/9f217c22-4b17-11ec-b976-aa665a54da40/steps`);
});
step('Check for monitor duration', async () => {
await page.hover('text=8.9 sec');
await page.waitForSelector('text=Explore');
expect(await page.$('text=Explore')).toBeTruthy();
await page.waitForSelector('text=area chart');
expect(await page.$('text=area chart')).toBeTruthy();
});
});

View file

@ -5,22 +5,17 @@
* 2.0.
*/
import { journey, step } from '@elastic/synthetics';
import { journey, step, before } from '@elastic/synthetics';
import { waitForLoadingToFinish } from './utils';
export const byTestId = (testId: string) => {
return `[data-test-subj=${testId}]`;
};
journey('uptime', ({ page, params }) => {
async function waitForLoadingToFinish() {
let isLoadingVisible = true;
while (isLoadingVisible) {
const loading = await page.$(byTestId('kbnLoadingMessage'));
isLoadingVisible = loading !== null;
await page.waitForTimeout(5 * 1000);
}
}
before(async () => {
await waitForLoadingToFinish({ page });
});
step('Go to Kibana', async () => {
await page.goto(`${params.kibanaUrl}/app/uptime?dateRangeStart=now-5y&dateRangeEnd=now`, {
@ -38,7 +33,6 @@ journey('uptime', ({ page, params }) => {
});
step('dismiss synthetics notice', async () => {
await waitForLoadingToFinish();
await page.click('[data-test-subj=uptimeDismissSyntheticsCallout]', {
timeout: 60 * 1000,
});

View file

@ -0,0 +1,27 @@
/*
* 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 { Page } from '@elastic/synthetics';
import { byTestId } from './uptime.journey';
export async function waitForLoadingToFinish({ page }: { page: Page }) {
while (true) {
if ((await page.$(byTestId('kbnLoadingMessage'))) === null) break;
await page.waitForTimeout(5 * 1000);
}
}
export async function loginToKibana({ page }: { page: Page }) {
await page.fill('[data-test-subj=loginUsername]', 'elastic', {
timeout: 60 * 1000,
});
await page.fill('[data-test-subj=loginPassword]', 'changeme');
await page.click('[data-test-subj=loginSubmit]');
await waitForLoadingToFinish({ page });
}

View file

@ -5,13 +5,29 @@
* 2.0.
*/
import { FtrConfigProviderContext } from '@kbn/test';
import yargs from 'yargs';
import { playwrightRunTests } from './playwright_start';
const { argv } = yargs(process.argv.slice(2))
.option('headless', {
default: true,
type: 'boolean',
description: 'Start in headless mode',
})
.option('grep', {
default: undefined,
type: 'string',
description: 'run only journeys with a name or tags that matches the glob',
})
.help();
const { headless, grep } = argv;
async function runE2ETests({ readConfigFile }: FtrConfigProviderContext) {
const kibanaConfig = await readConfigFile(require.resolve('./config.ts'));
return {
...kibanaConfig.getAll(),
testRunner: playwrightRunTests(),
testRunner: playwrightRunTests({ headless, match: grep }),
};
}

View file

@ -13,22 +13,32 @@ import { esArchiverLoad, esArchiverUnload } from './tasks/es_archiver';
import './journeys';
export function playwrightRunTests() {
export function playwrightRunTests({ headless, match }: { headless: boolean; match?: string }) {
return async ({ getService }: any) => {
const result = await playwrightStart(getService);
const result = await playwrightStart(getService, headless, match);
if (result && result.uptime.status !== 'succeeded') {
if (
result?.uptime &&
result.uptime.status !== 'succeeded' &&
result.StepsDuration &&
result.StepsDuration.status !== 'succeeded'
) {
throw new Error('Tests failed');
}
};
}
async function playwrightStart(getService: any) {
async function playwrightStart(getService: any, headless = true, match?: string) {
console.log('Loading esArchiver...');
await esArchiverLoad('full_heartbeat');
const esArchiver = getService('esArchiver');
esArchiverLoad('full_heartbeat');
esArchiverLoad('browser');
const config = getService('config');
await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote');
const kibanaUrl = Url.format({
protocol: config.get('servers.kibana.protocol'),
hostname: config.get('servers.kibana.hostname'),
@ -37,11 +47,13 @@ async function playwrightStart(getService: any) {
const res = await playwrightRun({
params: { kibanaUrl },
playwrightOptions: { headless: true, chromiumSandbox: false, timeout: 60 * 1000 },
playwrightOptions: { headless, chromiumSandbox: false, timeout: 60 * 1000 },
match: match === 'undefined' ? '' : match,
});
console.log('Removing esArchiver...');
await esArchiverUnload('full_heartbeat');
esArchiverUnload('full_heartbeat');
esArchiverUnload('browser');
return res;
}

View file

@ -27,7 +27,11 @@ describe('<WaterfallMarkerIcon />', () => {
type: 'step/end',
step: {
index: 0,
status: 'succeeded',
name: 'test-name',
duration: {
us: 9999,
},
},
},
monitor: {

View file

@ -17,16 +17,22 @@ describe('<WaterfallMarkerTrend />', () => {
jest.spyOn(moment.prototype, 'diff').mockImplementation(mockDiff);
const timestamp = '2021-12-03T14:35:41.072Z';
let activeStep: JourneyStep | undefined;
beforeEach(() => {
activeStep = {
'@timestamp': '123',
'@timestamp': timestamp,
_id: 'id',
synthetics: {
type: 'step/end',
step: {
index: 0,
status: 'succeeded',
name: 'test-name',
duration: {
us: 9999,
},
},
},
monitor: {
@ -38,7 +44,8 @@ describe('<WaterfallMarkerTrend />', () => {
},
},
};
mockDiff.mockReturnValue(0);
// value diff in milliseconds
mockDiff.mockReturnValue(10 * 1000);
});
const BASE_PATH = 'xyz';
@ -63,14 +70,20 @@ describe('<WaterfallMarkerTrend />', () => {
expect(heading.innerHTML).toEqual('test title');
expect(getByLabelText('append title').innerHTML.indexOf(BASE_PATH)).not.toBe(-1);
expect(getByText('kpi-over-time'));
expect(getByLabelText('attributes').innerHTML.indexOf('0s')).not.toBe(-1);
expect(getByLabelText('attributes').innerHTML.indexOf('0h')).toBe(-1);
expect(getByLabelText('attributes').innerHTML.indexOf('0m')).toBe(-1);
expect(getByLabelText('attributes').innerHTML.indexOf('0d')).toBe(-1);
const attributesText = getByLabelText('attributes').innerHTML;
expect(attributesText.includes('"2021-12-03T14:35:41.072Z"')).toBeTruthy();
const attributes = JSON.parse(attributesText);
expect(
moment(attributes[0].time.from)
.add(10 * 1000 * 48, 'millisecond')
.toISOString()
).toBe(timestamp);
});
it('handles days', () => {
mockDiff.mockReturnValue(10);
it('handles timespan difference', () => {
const oneMinDiff = 60 * 1000;
mockDiff.mockReturnValue(oneMinDiff);
const { getByLabelText } = render(
<TestWrapper activeStep={activeStep} basePath={BASE_PATH}>
<WaterfallMarkerTrend title="test title" field="field" />
@ -79,45 +92,29 @@ describe('<WaterfallMarkerTrend />', () => {
const attributesText = getByLabelText('attributes').innerHTML;
expect(attributesText.indexOf('480s')).toBe(-1);
expect(attributesText.indexOf('480h')).toBe(-1);
expect(attributesText.indexOf('480m')).toBe(-1);
expect(attributesText.indexOf('480d')).not.toBe(-1);
});
it('handles hours', () => {
mockDiff.mockReturnValueOnce(0);
mockDiff.mockReturnValue(10);
const { getByLabelText } = render(
<TestWrapper activeStep={activeStep} basePath={BASE_PATH}>
<WaterfallMarkerTrend title="test title" field="field" />
</TestWrapper>
expect(attributesText).toBe(
JSON.stringify([
{
name: 'test title(test-name)',
selectedMetricField: 'field',
time: { to: '2021-12-03T14:35:41.072Z', from: '2021-12-03T13:47:41.072Z' },
seriesType: 'area',
dataType: 'synthetics',
reportDefinitions: {
'monitor.name': [null],
'synthetics.step.name.keyword': ['test-name'],
},
operationType: 'last_value',
},
])
);
const attributesText = getByLabelText('attributes').innerHTML;
expect(attributesText.indexOf('480s')).toBe(-1);
expect(attributesText.indexOf('480h')).not.toBe(-1);
expect(attributesText.indexOf('480m')).toBe(-1);
expect(attributesText.indexOf('480d')).toBe(-1);
});
it('handles minutes', () => {
mockDiff.mockReturnValueOnce(0);
mockDiff.mockReturnValueOnce(0);
mockDiff.mockReturnValue(10);
const { getByLabelText } = render(
<TestWrapper activeStep={activeStep} basePath={BASE_PATH}>
<WaterfallMarkerTrend title="test title" field="field" />
</TestWrapper>
);
const attributesText = getByLabelText('attributes').innerHTML;
expect(attributesText.indexOf('480s')).toBe(-1);
expect(attributesText.indexOf('480h')).toBe(-1);
expect(attributesText.indexOf('480m')).not.toBe(-1);
expect(attributesText.indexOf('480d')).toBe(-1);
const attributes = JSON.parse(attributesText);
expect(
moment(attributes[0].time.from)
.add(oneMinDiff * 48, 'millisecond')
.toISOString()
).toBe(timestamp);
});
it('returns null for missing active step', () => {

View file

@ -6,101 +6,15 @@
*/
import React from 'react';
import { EuiButton } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import moment from 'moment';
import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public';
import { useUptimeStartPlugins } from '../../../../../contexts/uptime_startup_plugins_context';
import { AllSeries, createExploratoryViewUrl } from '../../../../../../../observability/public';
import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common';
import { useWaterfallContext } from '../context/waterfall_chart';
import { JourneyStep } from '../../../../../../common/runtime_types';
const getLast48Intervals = (activeStep: JourneyStep) => {
const { lt, gte } = activeStep.monitor.timespan!;
const inDays = moment(lt).diff(moment(gte), 'days');
if (inDays > 0) {
return { to: 'now', from: `now-${inDays * 48}d` };
}
const inHours = moment(lt).diff(moment(gte), 'hours');
if (inHours > 0) {
return { to: 'now', from: `now-${inHours * 48}h` };
}
const inMinutes = moment(lt).diff(moment(gte), 'minutes');
if (inMinutes > 0) {
return { to: 'now', from: `now-${inMinutes * 48}m` };
}
const inSeconds = moment(lt).diff(moment(gte), 'seconds');
return { to: 'now', from: `now-${inSeconds * 48}s` };
};
import { StepFieldTrend } from '../../../../synthetics/check_steps/step_field_trend';
export function WaterfallMarkerTrend({ title, field }: { title: string; field: string }) {
const { observability } = useUptimeStartPlugins();
const EmbeddableExpView = observability!.ExploratoryViewEmbeddable;
const basePath = useKibana().services.http?.basePath?.get();
const { activeStep } = useWaterfallContext();
if (!activeStep) {
return null;
}
const allSeries: AllSeries = [
{
name: `${title}(${activeStep.synthetics.step?.name!})`,
selectedMetricField: field,
time: getLast48Intervals(activeStep),
seriesType: 'area',
dataType: 'synthetics',
reportDefinitions: {
'monitor.name': [activeStep.monitor.name!],
'synthetics.step.name.keyword': [activeStep.synthetics.step?.name!],
},
operationType: 'last_value',
},
];
const href = createExploratoryViewUrl(
{
reportType: 'kpi-over-time',
allSeries,
},
basePath
);
return (
<Wrapper>
<EmbeddableExpView
title={title}
appendTitle={
<EuiButton iconType={'visArea'} href={href} target="_blank" size="s">
{EXPLORE_LABEL}
</EuiButton>
}
reportType={'kpi-over-time'}
attributes={allSeries}
axisTitlesVisibility={{ x: false, yLeft: false, yRight: false }}
legendIsVisible={false}
/>
</Wrapper>
);
return <StepFieldTrend field={field} title={title} step={activeStep} />;
}
export const EXPLORE_LABEL = i18n.translate('xpack.uptime.synthetics.markers.explore', {
defaultMessage: 'Explore',
});
const Wrapper = euiStyled.div`
height: 200px;
width: 400px;
&&& {
.expExpressionRenderer__expression {
padding-bottom: 0 !important;
}
}
`;

View file

@ -0,0 +1,71 @@
/*
* 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 React from 'react';
import { EuiButtonEmpty, EuiPopover } from '@elastic/eui';
import { useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { JourneyStep } from '../../../../common/runtime_types';
import { StepFieldTrend } from './step_field_trend';
import { microToSec } from '../../../lib/formatting';
interface Props {
step: JourneyStep;
durationPopoverOpenIndex: number | null;
setDurationPopoverOpenIndex: (val: number | null) => void;
}
export const StepDuration = ({
step,
durationPopoverOpenIndex,
setDurationPopoverOpenIndex,
}: Props) => {
const component = useMemo(
() => (
<StepFieldTrend
step={step}
field={'synthetics.step.duration.us'}
title={STEP_DURATION_TREND}
/>
),
[step]
);
if (step.synthetics.step?.status === 'skipped') {
return <span>--</span>;
}
const button = (
<EuiButtonEmpty
onMouseEnter={() => setDurationPopoverOpenIndex(step.synthetics.step?.index ?? null)}
iconType="visArea"
>
{i18n.translate('xpack.uptime.synthetics.step.duration', {
defaultMessage: '{value} seconds',
values: {
value: microToSec(step.synthetics.step?.duration.us!, 1),
},
})}
</EuiButtonEmpty>
);
return (
<EuiPopover
isOpen={durationPopoverOpenIndex === step.synthetics.step?.index}
button={button}
closePopover={() => setDurationPopoverOpenIndex(null)}
zIndex={100}
ownFocus={false}
>
{component}
</EuiPopover>
);
};
const STEP_DURATION_TREND = i18n.translate('xpack.uptime.synthetics.step.durationTrend', {
defaultMessage: 'Step duration trend',
});

View file

@ -0,0 +1,89 @@
/*
* 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 { getLast48Intervals, StepFieldTrend } from './step_field_trend';
import { render } from '../../../lib/helper/rtl_helpers';
import { JourneyStep } from '../../../../common/runtime_types';
const step: JourneyStep = {
_id: 'docID',
monitor: {
check_group: 'check_group',
duration: {
us: 123,
},
id: 'id',
status: 'up',
type: 'browser',
timespan: {
gte: '2021-12-01T12:54:28.098Z',
lt: '2021-12-01T12:55:28.098Z',
},
},
synthetics: {
step: {
index: 4,
status: 'succeeded',
name: 'STEP_NAME',
duration: {
us: 9999,
},
},
type: 'step/end',
},
'@timestamp': '2021-12-03T15:23:41.072Z',
};
describe('StepFieldTrend', () => {
it('it renders embeddable', async () => {
const { findByText } = render(
<StepFieldTrend step={step} field="step.duration.us" title="Step duration" />
);
expect(await findByText('Embeddable exploratory view')).toBeInTheDocument();
});
});
describe('getLast48Intervals', () => {
it('it returns expected values', () => {
// 48 minutes difference
expect(getLast48Intervals(step)).toEqual({
from: '2021-12-03T14:35:41.072Z',
to: '2021-12-03T15:23:41.072Z',
});
step.monitor.timespan = {
gte: '2021-12-01T12:55:38.098Z',
lt: '2021-12-01T12:55:48.098Z',
};
// 8 minutes difference
expect(getLast48Intervals(step)).toEqual({
from: '2021-12-03T15:15:41.072Z',
to: '2021-12-03T15:23:41.072Z',
});
step.monitor.timespan = {
gte: '2021-12-01T12:54:28.098Z',
lt: '2021-12-01T13:55:28.098Z',
};
// 48h difference
expect(getLast48Intervals(step)).toEqual({
from: '2021-12-01T14:35:41.072Z',
to: '2021-12-03T15:23:41.072Z',
});
step.monitor.timespan = {
gte: '2021-12-01T12:54:28.098Z',
lt: '2021-12-02T12:55:28.098Z',
};
// 48d difference
expect(getLast48Intervals(step)).toEqual({
from: '2021-10-16T14:35:41.072Z',
to: '2021-12-03T15:23:41.072Z',
});
});
});

View file

@ -0,0 +1,108 @@
/*
* 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 { EuiButton } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import moment from 'moment';
import { useSelector } from 'react-redux';
import { useUptimeStartPlugins } from '../../../contexts/uptime_startup_plugins_context';
import { JourneyStep } from '../../../../common/runtime_types';
import { AllSeries, createExploratoryViewUrl } from '../../../../../observability/public';
import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { selectDynamicSettings } from '../../../state/selectors';
export const getLast48Intervals = (activeStep: JourneyStep) => {
const timestamp = activeStep['@timestamp'];
const { lt, gte } = activeStep.monitor.timespan!;
const difference = moment(lt).diff(moment(gte), 'millisecond') * 48;
return {
to: timestamp,
from: moment(timestamp).subtract(difference, 'millisecond').toISOString(),
};
};
export function StepFieldTrend({
title,
field,
step: activeStep,
}: {
title: string;
field: string;
step: JourneyStep;
}) {
const { observability } = useUptimeStartPlugins();
const indexSettings = useSelector(selectDynamicSettings);
const EmbeddableExpView = observability!.ExploratoryViewEmbeddable;
const basePath = useKibana().services.http?.basePath?.get();
const allSeries: AllSeries = [
{
name: `${title}(${activeStep.synthetics.step?.name!})`,
selectedMetricField: field,
time: getLast48Intervals(activeStep),
seriesType: 'area',
dataType: 'synthetics',
reportDefinitions: {
'monitor.name': [activeStep.monitor.name!],
'synthetics.step.name.keyword': [activeStep.synthetics.step?.name!],
},
operationType: 'last_value',
},
];
const href = createExploratoryViewUrl(
{
reportType: 'kpi-over-time',
allSeries,
},
basePath
);
return (
<Wrapper>
<EmbeddableExpView
title={title}
appendTitle={
<EuiButton iconType={'visArea'} href={href} target="_blank" size="s">
{EXPLORE_LABEL}
</EuiButton>
}
reportType={'kpi-over-time'}
attributes={allSeries}
axisTitlesVisibility={{ x: false, yLeft: false, yRight: false }}
legendIsVisible={false}
dataTypesIndexPatterns={
indexSettings.settings?.heartbeatIndices
? {
synthetics: indexSettings.settings?.heartbeatIndices,
}
: undefined
}
/>
</Wrapper>
);
}
export const EXPLORE_LABEL = i18n.translate('xpack.uptime.synthetics.markers.explore', {
defaultMessage: 'Explore',
});
const Wrapper = euiStyled.div`
height: 200px;
width: 400px;
&&& {
.expExpressionRenderer__expression {
padding-bottom: 0 !important;
}
}
`;

View file

@ -35,6 +35,10 @@ describe('StepList component', () => {
step: {
name: 'load page',
index: 1,
status: 'succeeded',
duration: {
us: 9999,
},
},
},
},
@ -58,6 +62,10 @@ describe('StepList component', () => {
step: {
name: 'go to login',
index: 2,
status: 'succeeded',
duration: {
us: 9999,
},
},
},
},

View file

@ -7,7 +7,7 @@
import { EuiBasicTable, EuiBasicTableColumn, EuiButtonIcon, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { MouseEvent } from 'react';
import React, { MouseEvent, useState } from 'react';
import styled from 'styled-components';
import { JourneyStep } from '../../../../common/runtime_types';
import { STATUS_LABEL } from '../../monitor/ping_list/translations';
@ -17,6 +17,7 @@ import { StepDetailLink } from '../../common/step_detail_link';
import { VIEW_PERFORMANCE } from '../../monitor/synthetics/translations';
import { StepImage } from './step_image';
import { useExpandedRow } from './use_expanded_row';
import { StepDuration } from './step_duration';
export const SpanWithMargin = styled.span`
margin-right: 16px;
@ -79,6 +80,8 @@ export const StepsList = ({ data, error, loading }: Props) => {
const { expandedRows, toggleExpand } = useExpandedRow({ steps, allSteps: data, loading });
const [durationPopoverOpenIndex, setDurationPopoverOpenIndex] = useState<number | null>(null);
const columns: Array<EuiBasicTableColumn<JourneyStep>> = [
{
field: 'synthetics.payload.status',
@ -93,6 +96,18 @@ export const StepsList = ({ data, error, loading }: Props) => {
name: STEP_NAME_LABEL,
render: (_timestamp: string, item) => <StepImage step={item} />,
},
{
name: 'Step duration',
render: (item: JourneyStep) => {
return (
<StepDuration
step={item}
durationPopoverOpenIndex={durationPopoverOpenIndex}
setDurationPopoverOpenIndex={setDurationPopoverOpenIndex}
/>
);
},
},
{
align: 'left',
field: 'timestamp',
@ -106,6 +121,7 @@ export const StepsList = ({ data, error, loading }: Props) => {
</StepDetailLink>
),
},
{
align: 'right',
width: '24px',
@ -133,7 +149,13 @@ export const StepsList = ({ data, error, loading }: Props) => {
const targetElem = evt.target as HTMLElement;
// we dont want to capture image click event
if (targetElem.tagName !== 'IMG' && targetElem.tagName !== 'BUTTON') {
if (
targetElem.tagName !== 'IMG' &&
targetElem.tagName !== 'BUTTON' &&
targetElem.tagName !== 'CANVAS' &&
!targetElem.classList.contains('euiButtonEmpty__text') &&
!targetElem.classList.contains('euiIcon')
) {
toggleExpand({ journeyStep: item });
}
},

View file

@ -51,6 +51,10 @@ describe('useExpandedROw', () => {
step: {
name: 'load page',
index: 1,
status: 'succeeded',
duration: {
us: 9999,
},
},
},
},
@ -73,6 +77,10 @@ describe('useExpandedROw', () => {
step: {
name: 'go to login',
index: 2,
status: 'succeeded',
duration: {
us: 9999,
},
},
},
},

View file

@ -28,7 +28,11 @@ describe('ExecutedStep', () => {
synthetics: {
step: {
index: 4,
status: 'succeeded',
name: 'STEP_NAME',
duration: {
us: 9999,
},
},
type: 'step/end',
},
@ -43,7 +47,11 @@ describe('ExecutedStep', () => {
},
step: {
index: 3,
status: 'succeeded',
name: 'STEP_NAME',
duration: {
us: 9999,
},
},
type: 'step/end',
};

View file

@ -14,7 +14,10 @@ export function milliToSec(ms: number) {
return ms / ONE_SECOND_AS_MILLI;
}
export function microToSec(micro: number) {
export function microToSec(micro: number, fixedNumber?: number) {
if (fixedNumber) {
return (micro / ONE_SECOND_AS_MICROS).toFixed(fixedNumber);
}
return (micro / ONE_SECOND_AS_MICROS).toFixed(0);
}

View file

@ -124,6 +124,7 @@ const mockCore: () => Partial<CoreStart> = () => {
// @ts-ignore
PageTemplate: EuiPageTemplate,
},
ExploratoryViewEmbeddable: () => <div>Embeddable exploratory view</div>,
},
};
@ -143,7 +144,10 @@ export function MockKibanaProvider<ExtraCore>({
return (
<KibanaContextProvider services={{ ...coreOptions }} {...kibanaProps}>
<UptimeRefreshContextProvider>
<UptimeStartupPluginsContextProvider data={(coreOptions as any).data}>
<UptimeStartupPluginsContextProvider
data={(coreOptions as any).data}
observability={(coreOptions as any).observability}
>
<EuiThemeProvider darkMode={false}>
<I18nProvider>{children}</I18nProvider>
</EuiThemeProvider>

View file

@ -33,9 +33,19 @@ const { argv } = yargs(process.argv.slice(2))
type: 'string',
description: 'Path to the Kibana install directory',
})
.option('headless', {
default: true,
type: 'boolean',
description: 'Start in headless mode',
})
.option('grep', {
default: undefined,
type: 'string',
description: 'run only journeys with a name or tags that matches the glob',
})
.help();
const { server, runner, open, kibanaInstallDir } = argv;
const { server, runner, open, kibanaInstallDir, headless, grep } = argv;
const e2eDir = path.join(__dirname, '../e2e');
@ -49,12 +59,22 @@ if (server) {
const config = './playwright_run.ts';
function executeRunner() {
childProcess.execSync(
`node ../../../scripts/${ftrScript} --config ${config} --kibana-install-dir '${kibanaInstallDir}'`,
{
cwd: e2eDir,
stdio: 'inherit',
}
);
if (runner) {
childProcess.execSync(
`node ../../../scripts/${ftrScript} --config ${config} --kibana-install-dir '${kibanaInstallDir}' --headless ${headless} --grep ${grep}`,
{
cwd: e2eDir,
stdio: 'inherit',
}
);
} else {
childProcess.execSync(
`node ../../../scripts/${ftrScript} --config ${config} --kibana-install-dir '${kibanaInstallDir}' `,
{
cwd: e2eDir,
stdio: 'inherit',
}
);
}
}
executeRunner();