mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
Co-authored-by: Shahzad <shahzad.muhammad@elastic.co>
This commit is contained in:
parent
160e56d9d5
commit
15e4eb0b9d
24 changed files with 8292 additions and 167 deletions
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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}*`);
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
Binary file not shown.
7762
x-pack/plugins/uptime/e2e/fixtures/es_archiver/browser/mappings.json
Normal file
7762
x-pack/plugins/uptime/e2e/fixtures/es_archiver/browser/mappings.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -6,3 +6,4 @@
|
|||
*/
|
||||
|
||||
export * from './uptime.journey';
|
||||
export * from './step_duration.journey';
|
||||
|
|
48
x-pack/plugins/uptime/e2e/journeys/step_duration.journey.ts
Normal file
48
x-pack/plugins/uptime/e2e/journeys/step_duration.journey.ts
Normal 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();
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
});
|
||||
|
|
27
x-pack/plugins/uptime/e2e/journeys/utils.ts
Normal file
27
x-pack/plugins/uptime/e2e/journeys/utils.ts
Normal 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 });
|
||||
}
|
|
@ -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 }),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -27,7 +27,11 @@ describe('<WaterfallMarkerIcon />', () => {
|
|||
type: 'step/end',
|
||||
step: {
|
||||
index: 0,
|
||||
status: 'succeeded',
|
||||
name: 'test-name',
|
||||
duration: {
|
||||
us: 9999,
|
||||
},
|
||||
},
|
||||
},
|
||||
monitor: {
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -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',
|
||||
});
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
`;
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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',
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue