mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Synthetics] Run now and Test Now mode (#148160)
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Fixes https://github.com/elastic/kibana/issues/147677
This commit is contained in:
parent
f1ede739a3
commit
bc78a13d8f
97 changed files with 3847 additions and 259 deletions
|
@ -14,12 +14,15 @@ const SYNTHETICS_RUNNER = Symbol.for('SYNTHETICS_RUNNER');
|
|||
// @ts-ignore
|
||||
export const runner: Runner = global[SYNTHETICS_RUNNER];
|
||||
|
||||
export const recordVideo = (page: Page) => {
|
||||
export const recordVideo = (page: Page, postfix = '') => {
|
||||
after(async () => {
|
||||
try {
|
||||
const videoFilePath = await page.video()?.path();
|
||||
const pathToVideo = videoFilePath?.replace('.journeys/videos/', '').replace('.webm', '');
|
||||
const newVideoPath = videoFilePath?.replace(pathToVideo!, runner.currentJourney!.name);
|
||||
const newVideoPath = videoFilePath?.replace(
|
||||
pathToVideo!,
|
||||
runner.currentJourney!.name + `-${postfix}`
|
||||
);
|
||||
fs.renameSync(videoFilePath!, newVideoPath!);
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
|
|
|
@ -139,7 +139,9 @@ export class TestReporter implements Reporter {
|
|||
fs.unlinkSync('.journeys/videos/' + journeyName + '.webm');
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(e);
|
||||
console.log(
|
||||
'Failed to delete video file for path ' + '.journeys/videos/' + journeyName + '.webm'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ import { expect, Page } from '@elastic/synthetics';
|
|||
|
||||
export async function waitForLoadingToFinish({ page }: { page: Page }) {
|
||||
while (true) {
|
||||
if ((await page.$(byTestId('kbnLoadingMessage'))) === null) break;
|
||||
if (!(await page.isVisible(byTestId('kbnLoadingMessage'), { timeout: 5000 }))) break;
|
||||
await page.waitForTimeout(5 * 1000);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,6 +55,10 @@ export function useFetcher<TReturn>(
|
|||
|
||||
const promise = fn({ signal });
|
||||
if (!promise) {
|
||||
setResult((prevResult) => ({
|
||||
...prevResult,
|
||||
status: FETCH_STATUS.NOT_INITIATED,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -74,6 +78,7 @@ export function useFetcher<TReturn>(
|
|||
if (!signal.aborted) {
|
||||
setResult({
|
||||
data,
|
||||
loading: false,
|
||||
status: FETCH_STATUS.SUCCESS,
|
||||
error: undefined,
|
||||
} as FetcherResult<InferResponseType<TReturn>>);
|
||||
|
|
|
@ -12,6 +12,7 @@ export const OverviewStatusMetaDataCodec = t.interface({
|
|||
monitorQueryId: t.string,
|
||||
configId: t.string,
|
||||
location: t.string,
|
||||
timestamp: t.string,
|
||||
status: t.string,
|
||||
ping: PingType,
|
||||
});
|
||||
|
|
|
@ -6,7 +6,14 @@
|
|||
*/
|
||||
|
||||
import type { SimpleSavedObject } from '@kbn/core/public';
|
||||
import { EncryptedSyntheticsMonitor, SyntheticsMonitor } from '../runtime_types';
|
||||
import {
|
||||
EncryptedSyntheticsMonitor,
|
||||
Locations,
|
||||
MonitorFields,
|
||||
ServiceLocationErrors,
|
||||
SyntheticsMonitor,
|
||||
SyntheticsMonitorSchedule,
|
||||
} from '../runtime_types';
|
||||
|
||||
export interface MonitorIdParam {
|
||||
monitorId: string;
|
||||
|
@ -24,3 +31,12 @@ export interface SyntheticsServiceAllowed {
|
|||
serviceAllowed: boolean;
|
||||
signupUrl: string;
|
||||
}
|
||||
|
||||
export interface TestNowResponse {
|
||||
schedule: SyntheticsMonitorSchedule;
|
||||
locations: Locations;
|
||||
errors?: ServiceLocationErrors;
|
||||
testRunId: string;
|
||||
configId: string;
|
||||
monitor: MonitorFields;
|
||||
}
|
||||
|
|
|
@ -1,32 +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 fs from 'fs';
|
||||
import Runner from '@elastic/synthetics/src/core/runner';
|
||||
import { after, Page } from '@elastic/synthetics';
|
||||
|
||||
const SYNTHETICS_RUNNER = Symbol.for('SYNTHETICS_RUNNER');
|
||||
|
||||
// @ts-ignore
|
||||
export const runner: Runner = global[SYNTHETICS_RUNNER];
|
||||
|
||||
export const recordVideo = (page: Page, postfix = '') => {
|
||||
after(async () => {
|
||||
try {
|
||||
const videoFilePath = await page.video()?.path();
|
||||
const pathToVideo = videoFilePath?.replace('.journeys/videos/', '').replace('.webm', '');
|
||||
const newVideoPath = videoFilePath?.replace(
|
||||
pathToVideo!,
|
||||
runner.currentJourney!.name + `-${postfix}`
|
||||
);
|
||||
fs.renameSync(videoFilePath!, newVideoPath!);
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Error while renaming video file', e);
|
||||
}
|
||||
});
|
||||
};
|
|
@ -153,6 +153,7 @@ const createMonitorJourney = ({
|
|||
|
||||
step('Go to monitor management', async () => {
|
||||
await syntheticsApp.navigateToMonitorManagement(true);
|
||||
await syntheticsApp.enableMonitorManagement();
|
||||
});
|
||||
|
||||
step('Ensure all monitors are deleted', async () => {
|
||||
|
@ -178,7 +179,6 @@ const createMonitorJourney = ({
|
|||
});
|
||||
|
||||
step(`view ${monitorName} details in Monitor Management UI`, async () => {
|
||||
await syntheticsApp.navigateToMonitorManagement();
|
||||
const hasFailure = await syntheticsApp.findMonitorConfiguration(monitorListDetails);
|
||||
expect(hasFailure).toBeFalsy();
|
||||
});
|
||||
|
@ -191,6 +191,8 @@ const createMonitorJourney = ({
|
|||
monitorType
|
||||
);
|
||||
expect(hasFailure).toBeFalsy();
|
||||
await page.click('text=Update monitor');
|
||||
await page.waitForSelector('text=Monitor updated successfully.');
|
||||
});
|
||||
|
||||
step('cannot save monitor with the same name', async () => {
|
||||
|
@ -198,12 +200,10 @@ const createMonitorJourney = ({
|
|||
await syntheticsApp.createMonitor({ monitorConfig, monitorType });
|
||||
await page.waitForSelector('text=Monitor name already exists');
|
||||
await syntheticsApp.clickByTestSubj('syntheticsMonitorConfigSubmitButton');
|
||||
const success = page.locator('text=Monitor added successfully.');
|
||||
expect(await success.count()).toBe(0);
|
||||
await page.waitForSelector('text=Cancel');
|
||||
});
|
||||
|
||||
step('delete monitor', async () => {
|
||||
await syntheticsApp.navigateToMonitorManagement();
|
||||
await syntheticsApp.findByText('Monitor');
|
||||
const isSuccessful = await syntheticsApp.deleteMonitors();
|
||||
expect(isSuccessful).toBeTruthy();
|
||||
|
|
|
@ -29,9 +29,7 @@ journey(`DefaultStatusAlert`, async ({ page, params }) => {
|
|||
let downCheckTime = new Date(Date.now()).toISOString();
|
||||
|
||||
before(async () => {
|
||||
await services.cleaUpRules();
|
||||
await services.cleaUpAlerts();
|
||||
await services.cleanTestMonitors();
|
||||
await services.cleaUp();
|
||||
await services.enableMonitorManagedViaApi();
|
||||
await services.addTestMonitor('Test Monitor', {
|
||||
type: 'http',
|
||||
|
@ -45,9 +43,7 @@ journey(`DefaultStatusAlert`, async ({ page, params }) => {
|
|||
});
|
||||
|
||||
after(async () => {
|
||||
await services.cleaUpRules();
|
||||
await services.cleaUpAlerts();
|
||||
await services.cleanTestMonitors();
|
||||
await services.cleaUp();
|
||||
});
|
||||
|
||||
step('Go to monitors page', async () => {
|
||||
|
@ -86,7 +82,7 @@ journey(`DefaultStatusAlert`, async ({ page, params }) => {
|
|||
step('set the monitor status as down', async () => {
|
||||
downCheckTime = new Date(Date.now()).toISOString();
|
||||
await services.addTestSummaryDocument({
|
||||
isDown: true,
|
||||
docType: 'summaryDown',
|
||||
timestamp: downCheckTime,
|
||||
});
|
||||
await page.waitForTimeout(5 * 1000);
|
||||
|
@ -135,7 +131,7 @@ journey(`DefaultStatusAlert`, async ({ page, params }) => {
|
|||
});
|
||||
|
||||
step('set the status down again to generate another alert', async () => {
|
||||
await services.addTestSummaryDocument({ isDown: true });
|
||||
await services.addTestSummaryDocument({ docType: 'summaryDown' });
|
||||
|
||||
await retry.tryForTime(2 * 60 * 1000, async () => {
|
||||
await page.click(byTestId('querySubmitButton'));
|
||||
|
@ -161,7 +157,7 @@ journey(`DefaultStatusAlert`, async ({ page, params }) => {
|
|||
await services.addTestSummaryDocument({
|
||||
timestamp: downCheckTime,
|
||||
monitorId,
|
||||
isDown: true,
|
||||
docType: 'summaryDown',
|
||||
name,
|
||||
});
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { journey, step, expect, Page } from '@elastic/synthetics';
|
||||
import { assertText, byTestId } from '@kbn/observability-plugin/e2e/utils';
|
||||
import { RetryService } from '@kbn/ftr-common-functional-services';
|
||||
import { recordVideo } from '../../helpers/record_video';
|
||||
import { recordVideo } from '@kbn/observability-plugin/e2e/record_video';
|
||||
import { syntheticsAppPageProvider } from '../../page_objects/synthetics/synthetics_app';
|
||||
|
||||
let page1: Page;
|
||||
|
|
|
@ -5,17 +5,16 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { before, expect, journey, step } from '@elastic/synthetics';
|
||||
import { expect, journey, step } from '@elastic/synthetics';
|
||||
import { recordVideo } from '@kbn/observability-plugin/e2e/record_video';
|
||||
import { syntheticsAppPageProvider } from '../../page_objects/synthetics/synthetics_app';
|
||||
|
||||
journey('Test Monitor Detail Flyout', async ({ page, params }) => {
|
||||
journey('TestMonitorDetailFlyout', async ({ page, params }) => {
|
||||
recordVideo(page);
|
||||
|
||||
const syntheticsApp = syntheticsAppPageProvider({ page, kibanaUrl: params.kibanaUrl });
|
||||
const monitorName = 'test-flyout-http-monitor';
|
||||
|
||||
before(async () => {
|
||||
await syntheticsApp.waitForLoadingToFinish();
|
||||
});
|
||||
|
||||
step('Go to monitor-management', async () => {
|
||||
await syntheticsApp.navigateToAddMonitor();
|
||||
});
|
||||
|
|
|
@ -18,4 +18,5 @@ export * from './alerting_default.journey';
|
|||
export * from './global_parameters.journey';
|
||||
export * from './detail_flyout';
|
||||
export * from './alert_rules/default_status_alert.journey';
|
||||
export * from './test_now_mode.journey';
|
||||
export * from './data_retention.journey';
|
||||
|
|
|
@ -0,0 +1,319 @@
|
|||
/*
|
||||
* 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 { DocOverrides } from './sample_docs';
|
||||
|
||||
export const journeySummary = ({ name, timestamp, monitorId, testRunId }: DocOverrides = {}) => ({
|
||||
summary: {
|
||||
up: 1,
|
||||
down: 0,
|
||||
},
|
||||
test_run_id: testRunId ?? '07e339f4-4d56-4cdb-b314-96faacaee645',
|
||||
agent: {
|
||||
name: 'job-88fe737c53c39aea-lp69x',
|
||||
id: '049c1703-b4bd-45fa-8715-61812add68c0',
|
||||
type: 'heartbeat',
|
||||
ephemeral_id: 'e5a562a7-0fff-4684-bb5d-93abf7a754be',
|
||||
version: '8.6.0',
|
||||
},
|
||||
synthetics: {
|
||||
journey: {
|
||||
name: 'inline',
|
||||
id: 'inline',
|
||||
tags: null,
|
||||
},
|
||||
type: 'heartbeat/summary',
|
||||
},
|
||||
monitor: {
|
||||
duration: {
|
||||
us: 2218379,
|
||||
},
|
||||
origin: 'ui',
|
||||
name: name ?? 'https://www.google.com',
|
||||
check_group: testRunId ?? '6e7ce5d2-8756-11ed-98ca-6a95015d8678',
|
||||
id: monitorId ?? '07e339f4-4d56-4cdb-b314-96faacaee645',
|
||||
timespan: {
|
||||
lt: '2022-12-29T09:04:45.789Z',
|
||||
gte: '2022-12-29T08:54:45.789Z',
|
||||
},
|
||||
type: 'browser',
|
||||
status: 'up',
|
||||
},
|
||||
url: {
|
||||
path: '/',
|
||||
scheme: 'https',
|
||||
port: 443,
|
||||
domain: 'www.google.com',
|
||||
full: 'https://www.google.com/',
|
||||
},
|
||||
observer: {
|
||||
geo: {
|
||||
name: 'US Central QA',
|
||||
location: '41.8780, 93.0977',
|
||||
},
|
||||
name: 'US Central QA',
|
||||
},
|
||||
'@timestamp': timestamp ?? '2022-12-29T08:54:44.502Z',
|
||||
ecs: {
|
||||
version: '8.0.0',
|
||||
},
|
||||
config_id: '07e339f4-4d56-4cdb-b314-96faacaee645',
|
||||
data_stream: {
|
||||
namespace: 'default',
|
||||
type: 'synthetics',
|
||||
dataset: 'browser',
|
||||
},
|
||||
run_once: true,
|
||||
state: {
|
||||
duration_ms: 0,
|
||||
checks: 1,
|
||||
ends: null,
|
||||
started_at: '2022-12-29T08:54:45.845773057Z',
|
||||
id: 'default-1855d174b55-0',
|
||||
up: 1,
|
||||
flap_history: [],
|
||||
down: 0,
|
||||
status: 'up',
|
||||
},
|
||||
event: {
|
||||
agent_id_status: 'auth_metadata_missing',
|
||||
ingested: '2022-12-29T08:54:47Z',
|
||||
type: 'heartbeat/summary',
|
||||
dataset: 'browser',
|
||||
},
|
||||
});
|
||||
|
||||
export const journeyStart = ({ name, timestamp, monitorId, testRunId }: DocOverrides = {}) => ({
|
||||
test_run_id: testRunId ?? '07e339f4-4d56-4cdb-b314-96faacaee645',
|
||||
agent: {
|
||||
name: 'job-88fe737c53c39aea-lp69x',
|
||||
id: '049c1703-b4bd-45fa-8715-61812add68c0',
|
||||
ephemeral_id: 'e5a562a7-0fff-4684-bb5d-93abf7a754be',
|
||||
type: 'heartbeat',
|
||||
version: '8.6.0',
|
||||
},
|
||||
package: {
|
||||
name: '@elastic/synthetics',
|
||||
version: '1.0.0-beta.38',
|
||||
},
|
||||
os: {
|
||||
platform: 'linux',
|
||||
},
|
||||
synthetics: {
|
||||
package_version: '1.0.0-beta.38',
|
||||
journey: {
|
||||
name: 'inline',
|
||||
id: 'inline',
|
||||
},
|
||||
payload: {
|
||||
source:
|
||||
'({ page, context, browser, params, request }) => {\n scriptFn.apply(null, [\n core_1.step,\n page,\n context,\n browser,\n params,\n expect_1.expect,\n request,\n ]);\n }',
|
||||
params: {},
|
||||
},
|
||||
index: 0,
|
||||
type: 'journey/start',
|
||||
},
|
||||
monitor: {
|
||||
origin: 'ui',
|
||||
name: name ?? 'https://www.google.com',
|
||||
check_group: testRunId ?? '6e7ce5d2-8756-11ed-98ca-6a95015d8678',
|
||||
id: monitorId ?? '07e339f4-4d56-4cdb-b314-96faacaee645',
|
||||
timespan: {
|
||||
lt: '2022-12-29T09:04:42.284Z',
|
||||
gte: '2022-12-29T08:54:42.284Z',
|
||||
},
|
||||
type: 'browser',
|
||||
},
|
||||
observer: {
|
||||
geo: {
|
||||
name: 'Test private location',
|
||||
location: '41.8780, 93.0977',
|
||||
},
|
||||
name: 'Test private location',
|
||||
},
|
||||
'@timestamp': timestamp ?? '2022-12-29T08:54:42.284Z',
|
||||
ecs: {
|
||||
version: '8.0.0',
|
||||
},
|
||||
config_id: monitorId ?? '07e339f4-4d56-4cdb-b314-96faacaee645',
|
||||
data_stream: {
|
||||
namespace: 'default',
|
||||
type: 'synthetics',
|
||||
dataset: 'browser',
|
||||
},
|
||||
run_once: true,
|
||||
event: {
|
||||
agent_id_status: 'auth_metadata_missing',
|
||||
ingested: '2022-12-29T08:54:43Z',
|
||||
type: 'journey/start',
|
||||
dataset: 'browser',
|
||||
},
|
||||
});
|
||||
|
||||
export const step1 = ({ name, timestamp, monitorId, testRunId }: DocOverrides = {}) => ({
|
||||
test_run_id: testRunId ?? 'c16b1614-7f48-4791-8f46-9ccf3a896e20',
|
||||
agent: {
|
||||
name: 'job-76905d93798e6fff-z6nsb',
|
||||
id: '3dcc8177-b2ca-4b52-8e79-66332f6dbc72',
|
||||
type: 'heartbeat',
|
||||
ephemeral_id: '9b6b4f26-2a70-417a-8732-22ae95a406fa',
|
||||
version: '8.6.0',
|
||||
},
|
||||
package: {
|
||||
name: '@elastic/synthetics',
|
||||
version: '1.0.0-beta.38',
|
||||
},
|
||||
os: {
|
||||
platform: 'linux',
|
||||
},
|
||||
synthetics: {
|
||||
package_version: '1.0.0-beta.38',
|
||||
journey: {
|
||||
name: 'inline',
|
||||
id: 'inline',
|
||||
},
|
||||
payload: {
|
||||
source: "async () => {\n await page.goto('https://www.google.com');\n}",
|
||||
url: 'https://www.google.com/',
|
||||
status: 'succeeded',
|
||||
},
|
||||
index: 7,
|
||||
step: {
|
||||
duration: {
|
||||
us: 1419377,
|
||||
},
|
||||
name: 'Go to https://www.google.com',
|
||||
index: 1,
|
||||
status: 'succeeded',
|
||||
},
|
||||
type: 'step/end',
|
||||
},
|
||||
monitor: {
|
||||
origin: 'ui',
|
||||
name: name ?? 'https://www.google.com',
|
||||
check_group: testRunId ?? 'e81de0da-875e-11ed-8f2a-7eb894226a99',
|
||||
id: monitorId ?? 'c16b1614-7f48-4791-8f46-9ccf3a896e20',
|
||||
timespan: {
|
||||
lt: '2022-12-29T10:05:23.730Z',
|
||||
gte: '2022-12-29T09:55:23.730Z',
|
||||
},
|
||||
type: 'browser',
|
||||
},
|
||||
url: {
|
||||
path: '/',
|
||||
scheme: 'https',
|
||||
port: 443,
|
||||
domain: 'www.google.com',
|
||||
full: 'https://www.google.com/',
|
||||
},
|
||||
observer: {
|
||||
geo: {
|
||||
name: 'US Central QA',
|
||||
location: '41.8780, 93.0977',
|
||||
},
|
||||
name: 'US Central QA',
|
||||
},
|
||||
'@timestamp': timestamp ?? '2022-12-29T09:55:23.729Z',
|
||||
ecs: {
|
||||
version: '8.0.0',
|
||||
},
|
||||
config_id: monitorId ?? 'c16b1614-7f48-4791-8f46-9ccf3a896e20',
|
||||
data_stream: {
|
||||
namespace: 'default',
|
||||
type: 'synthetics',
|
||||
dataset: 'browser',
|
||||
},
|
||||
run_once: true,
|
||||
event: {
|
||||
agent_id_status: 'auth_metadata_missing',
|
||||
ingested: '2022-12-29T09:55:24Z',
|
||||
type: 'step/end',
|
||||
dataset: 'browser',
|
||||
},
|
||||
});
|
||||
|
||||
export const step2 = ({ name, timestamp, monitorId, testRunId }: DocOverrides = {}) => ({
|
||||
test_run_id: testRunId ?? 'c16b1614-7f48-4791-8f46-9ccf3a896e20',
|
||||
agent: {
|
||||
name: 'job-76905d93798e6fff-z6nsb',
|
||||
id: '3dcc8177-b2ca-4b52-8e79-66332f6dbc72',
|
||||
ephemeral_id: '9b6b4f26-2a70-417a-8732-22ae95a406fa',
|
||||
type: 'heartbeat',
|
||||
version: '8.6.0',
|
||||
},
|
||||
package: {
|
||||
name: '@elastic/synthetics',
|
||||
version: '1.0.0-beta.38',
|
||||
},
|
||||
os: {
|
||||
platform: 'linux',
|
||||
},
|
||||
synthetics: {
|
||||
package_version: '1.0.0-beta.38',
|
||||
journey: {
|
||||
name: 'inline',
|
||||
id: 'inline',
|
||||
},
|
||||
payload: {
|
||||
source: "async () => {\n await page.goto('https://www.google.com');\n}",
|
||||
url: 'https://www.google.com/',
|
||||
status: 'succeeded',
|
||||
},
|
||||
index: 15,
|
||||
step: {
|
||||
duration: {
|
||||
us: 788024,
|
||||
},
|
||||
name: 'Go to step 2',
|
||||
index: 2,
|
||||
status: 'succeeded',
|
||||
},
|
||||
type: 'step/end',
|
||||
},
|
||||
monitor: {
|
||||
origin: 'ui',
|
||||
name: name ?? 'https://www.google.com',
|
||||
check_group: testRunId ?? 'e81de0da-875e-11ed-8f2a-7eb894226a99',
|
||||
id: monitorId ?? 'c16b1614-7f48-4791-8f46-9ccf3a896e20',
|
||||
timespan: {
|
||||
lt: '2022-12-29T10:05:24.550Z',
|
||||
gte: '2022-12-29T09:55:24.550Z',
|
||||
},
|
||||
type: 'browser',
|
||||
},
|
||||
url: {
|
||||
path: '/',
|
||||
scheme: 'https',
|
||||
port: 443,
|
||||
domain: 'www.google.com',
|
||||
full: 'https://www.google.com/',
|
||||
},
|
||||
observer: {
|
||||
geo: {
|
||||
name: 'US Central QA',
|
||||
location: '41.8780, 93.0977',
|
||||
},
|
||||
name: 'US Central QA',
|
||||
},
|
||||
'@timestamp': timestamp ?? '2022-12-29T09:55:24.520Z',
|
||||
ecs: {
|
||||
version: '8.0.0',
|
||||
},
|
||||
config_id: monitorId ?? 'c16b1614-7f48-4791-8f46-9ccf3a896e20',
|
||||
data_stream: {
|
||||
namespace: 'default',
|
||||
type: 'synthetics',
|
||||
dataset: 'browser',
|
||||
},
|
||||
run_once: true,
|
||||
event: {
|
||||
agent_id_status: 'auth_metadata_missing',
|
||||
ingested: '2022-12-29T09:55:24Z',
|
||||
type: 'step/end',
|
||||
dataset: 'browser',
|
||||
},
|
||||
});
|
|
@ -0,0 +1,344 @@
|
|||
/*
|
||||
* 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 uuid from 'uuid';
|
||||
|
||||
export interface DocOverrides {
|
||||
timestamp?: string;
|
||||
monitorId?: string;
|
||||
name?: string;
|
||||
testRunId?: string;
|
||||
}
|
||||
|
||||
export const getUpHit = ({ name, timestamp, monitorId, testRunId }: DocOverrides = {}) => ({
|
||||
summary: {
|
||||
up: 1,
|
||||
down: 0,
|
||||
},
|
||||
tcp: {
|
||||
rtt: {
|
||||
connect: {
|
||||
us: 22245,
|
||||
},
|
||||
},
|
||||
},
|
||||
agent: {
|
||||
name: 'docker-fleet-server',
|
||||
id: 'dd39a87d-a1e5-45a1-8dd9-e78d6a1391c6',
|
||||
type: 'heartbeat',
|
||||
ephemeral_id: '264bb432-93f6-4aa6-a14d-266c53b9e7c7',
|
||||
version: '8.7.0',
|
||||
},
|
||||
resolve: {
|
||||
rtt: {
|
||||
us: 3101,
|
||||
},
|
||||
ip: '142.250.181.196',
|
||||
},
|
||||
elastic_agent: {
|
||||
id: 'dd39a87d-a1e5-45a1-8dd9-e78d6a1391c6',
|
||||
version: '8.7.0',
|
||||
snapshot: true,
|
||||
},
|
||||
monitor: {
|
||||
duration: {
|
||||
us: 155239,
|
||||
},
|
||||
ip: '142.250.181.196',
|
||||
origin: 'ui',
|
||||
name: name ?? 'Test Monitor',
|
||||
timespan: {
|
||||
lt: '2022-12-18T09:55:04.211Z',
|
||||
gte: '2022-12-18T09:52:04.211Z',
|
||||
},
|
||||
fleet_managed: true,
|
||||
id: monitorId ?? 'b9d9e146-746f-427f-bbf5-6e786b5b4e73',
|
||||
check_group: 'a039fd21-7eb9-11ed-8949-0242ac120006',
|
||||
type: 'http',
|
||||
status: 'up',
|
||||
},
|
||||
url: {
|
||||
scheme: 'https',
|
||||
port: 443,
|
||||
domain: 'www.google.com',
|
||||
full: 'https://www.google.com',
|
||||
},
|
||||
observer: {
|
||||
geo: {
|
||||
name: 'Test private location',
|
||||
},
|
||||
name: 'Test private location',
|
||||
},
|
||||
'@timestamp': timestamp ?? '2022-12-18T09:52:04.056Z',
|
||||
ecs: {
|
||||
version: '8.0.0',
|
||||
},
|
||||
config_id: monitorId ?? 'b9d9e146-746f-427f-bbf5-6e786b5b4e73',
|
||||
data_stream: {
|
||||
namespace: 'default',
|
||||
type: 'synthetics',
|
||||
dataset: 'http',
|
||||
},
|
||||
tls: {
|
||||
cipher: 'TLS-AES-128-GCM-SHA256',
|
||||
certificate_not_valid_before: '2022-11-28T08:19:01.000Z',
|
||||
established: true,
|
||||
server: {
|
||||
x509: {
|
||||
not_after: '2023-02-20T08:19:00.000Z',
|
||||
subject: {
|
||||
distinguished_name: 'CN=www.google.com',
|
||||
common_name: 'www.google.com',
|
||||
},
|
||||
not_before: '2022-11-28T08:19:01.000Z',
|
||||
public_key_curve: 'P-256',
|
||||
public_key_algorithm: 'ECDSA',
|
||||
signature_algorithm: 'SHA256-RSA',
|
||||
serial_number: '173037077033925240295268439311466214245',
|
||||
issuer: {
|
||||
distinguished_name: 'CN=GTS CA 1C3,O=Google Trust Services LLC,C=US',
|
||||
common_name: 'GTS CA 1C3',
|
||||
},
|
||||
},
|
||||
hash: {
|
||||
sha1: 'ea1b44061b864526c45619230b3299117d11bf4e',
|
||||
sha256: 'a5686448de09cc82b9cdad1e96357f919552ab14244da7948dd412ec0fc37d2b',
|
||||
},
|
||||
},
|
||||
rtt: {
|
||||
handshake: {
|
||||
us: 35023,
|
||||
},
|
||||
},
|
||||
version: '1.3',
|
||||
certificate_not_valid_after: '2023-02-20T08:19:00.000Z',
|
||||
version_protocol: 'tls',
|
||||
},
|
||||
state: {
|
||||
duration_ms: 0,
|
||||
checks: 1,
|
||||
ends: null,
|
||||
started_at: '2022-12-18T09:52:10.30502451Z',
|
||||
up: 1,
|
||||
id: 'Test private location-18524a5e641-0',
|
||||
down: 0,
|
||||
flap_history: [],
|
||||
status: 'up',
|
||||
},
|
||||
event: {
|
||||
agent_id_status: 'verified',
|
||||
ingested: '2022-12-18T09:52:11Z',
|
||||
dataset: 'http',
|
||||
},
|
||||
...(testRunId && { test_run_id: testRunId }),
|
||||
http: {
|
||||
rtt: {
|
||||
response_header: {
|
||||
us: 144758,
|
||||
},
|
||||
total: {
|
||||
us: 149191,
|
||||
},
|
||||
write_request: {
|
||||
us: 48,
|
||||
},
|
||||
content: {
|
||||
us: 401,
|
||||
},
|
||||
validate: {
|
||||
us: 145160,
|
||||
},
|
||||
},
|
||||
response: {
|
||||
headers: {
|
||||
Server: 'gws',
|
||||
P3p: 'CP="This is not a P3P policy! See g.co/p3phelp for more info."',
|
||||
Date: 'Thu, 29 Dec 2022 08:17:09 GMT',
|
||||
'X-Frame-Options': 'SAMEORIGIN',
|
||||
'Accept-Ranges': 'none',
|
||||
'Cache-Control': 'private, max-age=0',
|
||||
'X-Xss-Protection': '0',
|
||||
'Cross-Origin-Opener-Policy-Report-Only': 'same-origin-allow-popups; report-to="gws"',
|
||||
Vary: 'Accept-Encoding',
|
||||
Expires: '-1',
|
||||
'Content-Type': 'text/html; charset=ISO-8859-1',
|
||||
},
|
||||
status_code: 200,
|
||||
mime_type: 'text/html; charset=utf-8',
|
||||
body: {
|
||||
bytes: 13963,
|
||||
hash: 'a4c2cf7dead9fb9329fc3727fc152b6a12072410926430491d02a0c6dc3a70ff',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const firstDownHit = ({ name, timestamp, monitorId }: DocOverrides = {}) => ({
|
||||
summary: {
|
||||
up: 0,
|
||||
down: 1,
|
||||
},
|
||||
tcp: {
|
||||
rtt: {
|
||||
connect: {
|
||||
us: 20482,
|
||||
},
|
||||
},
|
||||
},
|
||||
agent: {
|
||||
name: 'docker-fleet-server',
|
||||
id: 'dd39a87d-a1e5-45a1-8dd9-e78d6a1391c6',
|
||||
type: 'heartbeat',
|
||||
ephemeral_id: '264bb432-93f6-4aa6-a14d-266c53b9e7c7',
|
||||
version: '8.7.0',
|
||||
},
|
||||
resolve: {
|
||||
rtt: {
|
||||
us: 3234,
|
||||
},
|
||||
ip: '142.250.181.196',
|
||||
},
|
||||
elastic_agent: {
|
||||
id: 'dd39a87d-a1e5-45a1-8dd9-e78d6a1391c6',
|
||||
version: '8.7.0',
|
||||
snapshot: true,
|
||||
},
|
||||
monitor: {
|
||||
duration: {
|
||||
us: 152459,
|
||||
},
|
||||
origin: 'ui',
|
||||
ip: '142.250.181.196',
|
||||
name: name ?? 'Test Monitor',
|
||||
fleet_managed: true,
|
||||
check_group: uuid.v4(),
|
||||
timespan: {
|
||||
lt: '2022-12-18T09:52:50.128Z',
|
||||
gte: '2022-12-18T09:49:50.128Z',
|
||||
},
|
||||
id: monitorId ?? 'b9d9e146-746f-427f-bbf5-6e786b5b4e73',
|
||||
type: 'http',
|
||||
status: 'down',
|
||||
},
|
||||
error: {
|
||||
message: 'received status code 200 expecting [500]',
|
||||
type: 'validate',
|
||||
},
|
||||
url: {
|
||||
scheme: 'https',
|
||||
port: 443,
|
||||
domain: 'www.google.com',
|
||||
full: 'https://www.google.com',
|
||||
},
|
||||
observer: {
|
||||
geo: {
|
||||
name: 'Test private location',
|
||||
},
|
||||
name: 'Test private location',
|
||||
},
|
||||
'@timestamp': timestamp ?? '2022-12-18T09:49:49.976Z',
|
||||
ecs: {
|
||||
version: '8.0.0',
|
||||
},
|
||||
config_id: monitorId ?? 'b9d9e146-746f-427f-bbf5-6e786b5b4e73',
|
||||
data_stream: {
|
||||
namespace: 'default',
|
||||
type: 'synthetics',
|
||||
dataset: 'http',
|
||||
},
|
||||
tls: {
|
||||
established: true,
|
||||
cipher: 'TLS-AES-128-GCM-SHA256',
|
||||
certificate_not_valid_before: '2022-11-28T08:19:01.000Z',
|
||||
server: {
|
||||
x509: {
|
||||
not_after: '2023-02-20T08:19:00.000Z',
|
||||
subject: {
|
||||
distinguished_name: 'CN=www.google.com',
|
||||
common_name: 'www.google.com',
|
||||
},
|
||||
not_before: '2022-11-28T08:19:01.000Z',
|
||||
public_key_algorithm: 'ECDSA',
|
||||
public_key_curve: 'P-256',
|
||||
signature_algorithm: 'SHA256-RSA',
|
||||
serial_number: '173037077033925240295268439311466214245',
|
||||
issuer: {
|
||||
distinguished_name: 'CN=GTS CA 1C3,O=Google Trust Services LLC,C=US',
|
||||
common_name: 'GTS CA 1C3',
|
||||
},
|
||||
},
|
||||
hash: {
|
||||
sha1: 'ea1b44061b864526c45619230b3299117d11bf4e',
|
||||
sha256: 'a5686448de09cc82b9cdad1e96357f919552ab14244da7948dd412ec0fc37d2b',
|
||||
},
|
||||
},
|
||||
rtt: {
|
||||
handshake: {
|
||||
us: 28468,
|
||||
},
|
||||
},
|
||||
version: '1.3',
|
||||
certificate_not_valid_after: '2023-02-20T08:19:00.000Z',
|
||||
version_protocol: 'tls',
|
||||
},
|
||||
state: {
|
||||
duration_ms: 0,
|
||||
checks: 1,
|
||||
ends: null,
|
||||
started_at: '2022-12-18T09:49:56.007551998Z',
|
||||
id: 'Test private location-18524a3d9a7-0',
|
||||
up: 0,
|
||||
down: 1,
|
||||
flap_history: [],
|
||||
status: 'down',
|
||||
},
|
||||
event: {
|
||||
agent_id_status: 'verified',
|
||||
ingested: '2022-12-18T09:49:57Z',
|
||||
dataset: 'http',
|
||||
},
|
||||
http: {
|
||||
rtt: {
|
||||
response_header: {
|
||||
us: 144758,
|
||||
},
|
||||
total: {
|
||||
us: 149191,
|
||||
},
|
||||
write_request: {
|
||||
us: 48,
|
||||
},
|
||||
content: {
|
||||
us: 401,
|
||||
},
|
||||
validate: {
|
||||
us: 145160,
|
||||
},
|
||||
},
|
||||
response: {
|
||||
headers: {
|
||||
Server: 'gws',
|
||||
P3p: 'CP="This is not a P3P policy! See g.co/p3phelp for more info."',
|
||||
Date: 'Thu, 29 Dec 2022 08:17:09 GMT',
|
||||
'X-Frame-Options': 'SAMEORIGIN',
|
||||
'Accept-Ranges': 'none',
|
||||
'Cache-Control': 'private, max-age=0',
|
||||
'X-Xss-Protection': '0',
|
||||
'Cross-Origin-Opener-Policy-Report-Only': 'same-origin-allow-popups; report-to="gws"',
|
||||
Vary: 'Accept-Encoding',
|
||||
Expires: '-1',
|
||||
'Content-Type': 'text/html; charset=ISO-8859-1',
|
||||
},
|
||||
status_code: 200,
|
||||
mime_type: 'text/html; charset=utf-8',
|
||||
body: {
|
||||
bytes: 13963,
|
||||
hash: 'a4c2cf7dead9fb9329fc3727fc152b6a12072410926430491d02a0c6dc3a70ff',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
|
@ -10,7 +10,8 @@ import type { Client } from '@elastic/elasticsearch';
|
|||
import { KbnClient, uriencode } from '@kbn/test';
|
||||
import pMap from 'p-map';
|
||||
import { SYNTHETICS_API_URLS } from '../../../../common/constants';
|
||||
import { firstDownHit, firstUpHit } from '../alert_rules/sample_docs/sample_docs';
|
||||
import { journeyStart, journeySummary, step1, step2 } from './data/browser_docs';
|
||||
import { firstDownHit, getUpHit } from './data/sample_docs';
|
||||
|
||||
export class SyntheticsServices {
|
||||
kibanaUrl: string;
|
||||
|
@ -98,22 +99,80 @@ export class SyntheticsServices {
|
|||
}
|
||||
|
||||
async addTestSummaryDocument({
|
||||
isDown = false,
|
||||
docType = 'summaryUp',
|
||||
timestamp = new Date(Date.now()).toISOString(),
|
||||
monitorId,
|
||||
name,
|
||||
}: { monitorId?: string; isDown?: boolean; timestamp?: string; name?: string } = {}) {
|
||||
testRunId,
|
||||
stepIndex = 1,
|
||||
}: {
|
||||
monitorId?: string;
|
||||
docType?: 'summaryUp' | 'summaryDown' | 'journeyStart' | 'journeyEnd' | 'stepEnd';
|
||||
timestamp?: string;
|
||||
name?: string;
|
||||
testRunId?: string;
|
||||
stepIndex?: number;
|
||||
} = {}) {
|
||||
const getService = this.params.getService;
|
||||
const es: Client = getService('es');
|
||||
|
||||
let document = {
|
||||
'@timestamp': timestamp,
|
||||
};
|
||||
|
||||
let index = 'synthetics-http-default';
|
||||
|
||||
switch (docType) {
|
||||
case 'stepEnd':
|
||||
index = 'synthetics-browser-default';
|
||||
|
||||
const stepDoc =
|
||||
stepIndex === 1
|
||||
? step1({ timestamp, monitorId, name, testRunId })
|
||||
: step2({ timestamp, monitorId, name, testRunId });
|
||||
|
||||
document = { ...stepDoc, ...document };
|
||||
break;
|
||||
case 'journeyEnd':
|
||||
index = 'synthetics-browser-default';
|
||||
document = { ...journeySummary({ timestamp, monitorId, name, testRunId }), ...document };
|
||||
break;
|
||||
case 'journeyStart':
|
||||
index = 'synthetics-browser-default';
|
||||
document = { ...journeyStart({ timestamp, monitorId, name, testRunId }), ...document };
|
||||
break;
|
||||
case 'summaryDown':
|
||||
document = { ...firstDownHit({ timestamp, monitorId, name, testRunId }), ...document };
|
||||
break;
|
||||
case 'summaryUp':
|
||||
document = { ...getUpHit({ timestamp, monitorId, name, testRunId }), ...document };
|
||||
break;
|
||||
default:
|
||||
document = { ...getUpHit({ timestamp, monitorId, name, testRunId }), ...document };
|
||||
}
|
||||
|
||||
await es.index({
|
||||
index: 'synthetics-http-default',
|
||||
document: {
|
||||
...(isDown ? firstDownHit({ timestamp, monitorId, name }) : firstUpHit),
|
||||
'@timestamp': timestamp,
|
||||
},
|
||||
index,
|
||||
document,
|
||||
});
|
||||
}
|
||||
|
||||
async cleaUp(things: Array<'monitors' | 'alerts' | 'rules'> = ['monitors', 'alerts', 'rules']) {
|
||||
const promises = [];
|
||||
if (things.includes('monitors')) {
|
||||
promises.push(this.cleanTestMonitors());
|
||||
}
|
||||
if (things.includes('alerts')) {
|
||||
promises.push(this.cleaUpAlerts());
|
||||
}
|
||||
|
||||
if (things.includes('rules')) {
|
||||
promises.push(this.cleaUpRules());
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
async cleaUpAlerts() {
|
||||
const getService = this.params.getService;
|
||||
const es: Client = getService('es');
|
||||
|
|
|
@ -0,0 +1,164 @@
|
|||
/*
|
||||
* 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, before, after, expect } from '@elastic/synthetics';
|
||||
import { RetryService } from '@kbn/ftr-common-functional-services';
|
||||
import { recordVideo } from '@kbn/observability-plugin/e2e/record_video';
|
||||
import { byTestId } from '@kbn/observability-plugin/e2e/utils';
|
||||
import { syntheticsAppPageProvider } from '../../page_objects/synthetics/synthetics_app';
|
||||
import { SyntheticsServices } from './services/synthetics_services';
|
||||
|
||||
journey(`TestNowMode`, async ({ page, params }) => {
|
||||
page.setDefaultTimeout(60 * 1000);
|
||||
recordVideo(page);
|
||||
const syntheticsApp = syntheticsAppPageProvider({ page, kibanaUrl: params.kibanaUrl });
|
||||
|
||||
const services = new SyntheticsServices(params);
|
||||
|
||||
const getService = params.getService;
|
||||
const retry: RetryService = getService('retry');
|
||||
|
||||
const firstCheckTime = new Date(Date.now()).toISOString();
|
||||
|
||||
let testRunId: string | undefined;
|
||||
|
||||
before(async () => {
|
||||
page.on('request', (evt) => {
|
||||
if (
|
||||
evt.resourceType() === 'fetch' &&
|
||||
(evt.url().includes('service/monitors/trigger/') ||
|
||||
evt.url().includes('uptime/service/monitors/run_once'))
|
||||
) {
|
||||
evt
|
||||
.response()
|
||||
?.then((res) => res?.json())
|
||||
.then((res) => {
|
||||
if (res.testRunId) {
|
||||
testRunId = res.testRunId;
|
||||
} else {
|
||||
try {
|
||||
testRunId = evt.url().split('/run_once/').pop();
|
||||
} catch (e) {
|
||||
// eee
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await services.cleaUp();
|
||||
await services.enableMonitorManagedViaApi();
|
||||
await services.addTestMonitor('Test Monitor', {
|
||||
type: 'http',
|
||||
urls: 'https://www.google.com',
|
||||
custom_heartbeat_id: 'b9d9e146-746f-427f-bbf5-6e786b5b4e73',
|
||||
locations: [
|
||||
{ id: 'Test private location', label: 'Test private location', isServiceManaged: true },
|
||||
],
|
||||
});
|
||||
await services.addTestSummaryDocument({ timestamp: firstCheckTime });
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await services.cleaUp();
|
||||
});
|
||||
|
||||
step('Go to monitors page', async () => {
|
||||
await syntheticsApp.navigateToOverview(true);
|
||||
});
|
||||
|
||||
step('Opens flyout when run manually', async () => {
|
||||
await page.hover('text=Test Monitor');
|
||||
await page.click('[aria-label="Open actions menu"]');
|
||||
await page.click('text=Run test manually');
|
||||
await page.waitForSelector('text=Test results');
|
||||
});
|
||||
|
||||
step('Displays results when successful', async () => {
|
||||
await retry.tryForTime(30 * 1000, async () => {
|
||||
expect(testRunId?.length).toBe(36);
|
||||
});
|
||||
await services.addTestSummaryDocument({ testRunId });
|
||||
await page.waitForSelector('text=Completed');
|
||||
await page.waitForSelector('text=Took 155 ms');
|
||||
await page.waitForSelector('text=Test private location');
|
||||
});
|
||||
|
||||
step('Displays data in expanded row', async () => {
|
||||
await page.click(byTestId('uptimePingListExpandBtn'));
|
||||
await page.waitForSelector('text=Body size is 13KB.');
|
||||
await page.click('text=Response headers');
|
||||
await page.waitForSelector('text=Accept-Ranges');
|
||||
await page.waitForSelector('text=Cache-Control');
|
||||
await page.waitForSelector('text=private, max-age=0');
|
||||
await page.click(byTestId('euiFlyoutCloseButton'));
|
||||
});
|
||||
|
||||
step('Add a browser monitor', async () => {
|
||||
await services.addTestMonitor('Browser Monitor', {
|
||||
type: 'browser',
|
||||
'source.inline.script':
|
||||
"step('Go to https://www.google.com', async () => {\n await page.goto('https://www.google.com');\n});\n\nstep('Go to https://www.google.com', async () => {\n await page.goto('https://www.google.com');\n});",
|
||||
urls: 'https://www.google.com',
|
||||
locations: [
|
||||
{ id: 'Test private location', label: 'Test private location', isServiceManaged: true },
|
||||
],
|
||||
});
|
||||
|
||||
await page.click(byTestId('syntheticsMonitorManagementTab'));
|
||||
await page.click(byTestId('syntheticsMonitorOverviewTab'));
|
||||
|
||||
await page.hover('text=Browser Monitor');
|
||||
await page.click('[aria-label="Open actions menu"]');
|
||||
await page.click('text=Edit monitor');
|
||||
});
|
||||
|
||||
step('Opens flyout when in edit mode', async () => {
|
||||
testRunId = '';
|
||||
await page.click(byTestId('syntheticsRunTestBtn'));
|
||||
await page.waitForSelector('text=Test results');
|
||||
await page.waitForSelector('text=0 steps completed');
|
||||
await page.waitForSelector('text=PENDING');
|
||||
});
|
||||
|
||||
step('Displays results when successful in edit mode', async () => {
|
||||
await retry.tryForTime(30 * 1000, async () => {
|
||||
expect(testRunId?.length).toBe(36);
|
||||
});
|
||||
await page.waitForTimeout(1000);
|
||||
await services.addTestSummaryDocument({ testRunId, docType: 'journeyStart' });
|
||||
await page.waitForTimeout(1000);
|
||||
await page.waitForSelector('text=Test private location');
|
||||
await page.waitForSelector('text=IN PROGRESS');
|
||||
});
|
||||
|
||||
step('Verifies that first step is populated', async () => {
|
||||
await services.addTestSummaryDocument({ testRunId, docType: 'stepEnd', stepIndex: 1 });
|
||||
|
||||
await page.waitForSelector('text=1 step completed');
|
||||
await page.waitForSelector('text=Go to https://www.google.com');
|
||||
await page.waitForSelector('text=1.4 s');
|
||||
await page.waitForSelector('text=Complete');
|
||||
});
|
||||
|
||||
step('Verifies that second step is populated', async () => {
|
||||
await services.addTestSummaryDocument({ testRunId, docType: 'stepEnd', stepIndex: 2 });
|
||||
await retry.tryForTime(90 * 1000, async () => {
|
||||
await page.waitForSelector('text=2 steps completed');
|
||||
await page.waitForSelector('text="Go to step 2"');
|
||||
await page.waitForSelector('text=788 ms');
|
||||
await page.waitForSelector('text=IN PROGRESS');
|
||||
});
|
||||
});
|
||||
|
||||
step('Complete it with summary document', async () => {
|
||||
await services.addTestSummaryDocument({ testRunId, docType: 'journeyEnd' });
|
||||
|
||||
await page.waitForSelector('text=COMPLETED');
|
||||
await page.waitForSelector('text=took 2 s');
|
||||
});
|
||||
});
|
|
@ -59,12 +59,18 @@ journey('StatusFlyoutInAlertingApp', async ({ page, params }) => {
|
|||
|
||||
await waitForLoadingToFinish({ page });
|
||||
|
||||
await page.click(byTestId('"xpack.synthetics.alerts.monitorStatus.filterBar"'));
|
||||
|
||||
await assertText({ page, text: 'browser' });
|
||||
await assertText({ page, text: 'http' });
|
||||
|
||||
await retry.tryForTime(30 * 1000, async () => {
|
||||
const suggestionItem = await page.$(byTestId('autoCompleteSuggestionText'));
|
||||
expect(await suggestionItem?.textContent()).toBe('"browser" ');
|
||||
await page.click('text=browser');
|
||||
|
||||
const element = await page.waitForSelector(
|
||||
byTestId('"xpack.synthetics.alerts.monitorStatus.filterBar"')
|
||||
);
|
||||
|
||||
expect((await element?.textContent())?.trim()).toBe('monitor.type : "browser"');
|
||||
});
|
||||
|
||||
await page.click(byTestId('euiFlyoutCloseButton'));
|
||||
|
|
|
@ -151,6 +151,7 @@ Object.keys(configuration).forEach((type) => {
|
|||
});
|
||||
|
||||
journey('Monitor Management breadcrumbs', async ({ page, params }: { page: Page; params: any }) => {
|
||||
recordVideo(page);
|
||||
const uptime = monitorManagementPageProvider({ page, kibanaUrl: params.kibanaUrl });
|
||||
const defaultMonitorDetails = {
|
||||
name: `Sample monitor ${uuid.v4()}`,
|
||||
|
@ -212,6 +213,7 @@ journey('Monitor Management breadcrumbs', async ({ page, params }: { page: Page;
|
|||
journey(
|
||||
'MonitorManagement-case-insensitive sort',
|
||||
async ({ page, params }: { page: Page; params: any }) => {
|
||||
recordVideo(page);
|
||||
const uptime = monitorManagementPageProvider({ page, kibanaUrl: params.kibanaUrl });
|
||||
|
||||
const sortedMonitors = [
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
import { journey, step, expect } from '@elastic/synthetics';
|
||||
import { byTestId, TIMEOUT_60_SEC } from '@kbn/observability-plugin/e2e/utils';
|
||||
import { recordVideo } from '../../../helpers/record_video';
|
||||
import { recordVideo } from '@kbn/observability-plugin/e2e/record_video';
|
||||
import { monitorManagementPageProvider } from '../../../page_objects/uptime/monitor_management';
|
||||
|
||||
journey('ManagePrivateLocation', async ({ page, params: { kibanaUrl } }) => {
|
||||
|
@ -14,8 +14,6 @@ journey('ManagePrivateLocation', async ({ page, params: { kibanaUrl } }) => {
|
|||
|
||||
const uptime = monitorManagementPageProvider({ page, kibanaUrl });
|
||||
|
||||
recordVideo(page);
|
||||
|
||||
step('Go to monitor-management', async () => {
|
||||
await uptime.navigateToMonitorManagement();
|
||||
});
|
||||
|
|
|
@ -71,9 +71,15 @@ export function syntheticsAppPageProvider({ page, kibanaUrl }: { page: Page; kib
|
|||
},
|
||||
|
||||
async navigateToAddMonitor() {
|
||||
await page.goto(addMonitor, {
|
||||
waitUntil: 'networkidle',
|
||||
});
|
||||
if (await page.isVisible('text=select a different monitor type', { timeout: 0 })) {
|
||||
await page.click('text=select a different monitor type');
|
||||
} else if (await page.isVisible('text=Create monitor', { timeout: 0 })) {
|
||||
await page.click('text=Create monitor');
|
||||
} else {
|
||||
await page.goto(addMonitor, {
|
||||
waitUntil: 'networkidle',
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
async ensureIsOnMonitorConfigPage() {
|
||||
|
@ -89,7 +95,9 @@ export function syntheticsAppPageProvider({ page, kibanaUrl }: { page: Page; kib
|
|||
async deleteMonitors() {
|
||||
let isSuccessful: boolean = false;
|
||||
while (true) {
|
||||
if ((await page.$(this.byTestId('euiCollapsedItemActionsButton'))) === null) {
|
||||
if (
|
||||
!(await page.isVisible(this.byTestId('euiCollapsedItemActionsButton'), { timeout: 0 }))
|
||||
) {
|
||||
isSuccessful = true;
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiBasicTableColumn,
|
||||
EuiButtonEmpty,
|
||||
EuiCallOut,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiHighlight,
|
||||
EuiLink,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
formatDate,
|
||||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { EuiInMemoryTable } from '@elastic/eui';
|
||||
import moment from 'moment';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { useFetcher } from '@kbn/observability-plugin/public';
|
||||
import { useStdErrorLogs } from './use_std_error_logs';
|
||||
import { SYNTHETICS_INDEX_PATTERN } from '../../../../../../common/constants';
|
||||
import { Ping } from '../../../../../../common/runtime_types';
|
||||
import { ClientPluginsStart } from '../../../../../plugin';
|
||||
|
||||
export const StdErrorLogs = ({
|
||||
monitorId,
|
||||
checkGroup,
|
||||
timestamp,
|
||||
title,
|
||||
summaryMessage,
|
||||
hideTitle = false,
|
||||
}: {
|
||||
monitorId?: string;
|
||||
checkGroup?: string;
|
||||
timestamp?: string;
|
||||
title?: string;
|
||||
summaryMessage?: string;
|
||||
hideTitle?: boolean;
|
||||
}) => {
|
||||
const columns = [
|
||||
{
|
||||
field: '@timestamp',
|
||||
name: TIMESTAMP_LABEL,
|
||||
sortable: true,
|
||||
render: (date: string) => formatDate(date, 'dateTime'),
|
||||
},
|
||||
{
|
||||
field: 'synthetics.payload.message',
|
||||
name: 'Message',
|
||||
render: (message: string) => (
|
||||
<EuiHighlight
|
||||
search={message.includes('SyntaxError:') ? 'SyntaxError:' : 'ReferenceError:'}
|
||||
>
|
||||
{message}
|
||||
</EuiHighlight>
|
||||
),
|
||||
},
|
||||
] as Array<EuiBasicTableColumn<Ping>>;
|
||||
|
||||
const { items, loading } = useStdErrorLogs({ monitorId, checkGroup });
|
||||
|
||||
const { discover, observability } = useKibana<ClientPluginsStart>().services;
|
||||
|
||||
const { data: discoverLink } = useFetcher(async () => {
|
||||
const dataView = await observability.getAppDataView('synthetics', SYNTHETICS_INDEX_PATTERN);
|
||||
return discover.locator?.getUrl({
|
||||
query: { language: 'kuery', query: `monitor.check_group: ${checkGroup}` },
|
||||
indexPatternId: dataView?.id,
|
||||
columns: ['synthetics.payload.message', 'error.message'],
|
||||
timeRange: timestamp
|
||||
? {
|
||||
from: moment(timestamp).subtract(10, 'minutes').toISOString(),
|
||||
to: moment(timestamp).add(5, 'minutes').toISOString(),
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
}, [checkGroup, timestamp]);
|
||||
|
||||
const search = {
|
||||
box: {
|
||||
incremental: true,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{!hideTitle && (
|
||||
<>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="s">
|
||||
<h3>{title ?? TEST_RUN_LOGS_LABEL}</h3>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiLink>
|
||||
<EuiButtonEmpty
|
||||
href={discoverLink}
|
||||
iconType="discoverApp"
|
||||
isDisabled={!discoverLink}
|
||||
>
|
||||
{VIEW_IN_DISCOVER_LABEL}
|
||||
</EuiButtonEmpty>
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiCallOut title={ERROR_SUMMARY_LABEL} color="danger" iconType="alert">
|
||||
<p>{summaryMessage}</p>
|
||||
</EuiCallOut>
|
||||
</>
|
||||
)}
|
||||
|
||||
<EuiSpacer />
|
||||
|
||||
<EuiInMemoryTable
|
||||
items={items}
|
||||
columns={columns}
|
||||
tableLayout="auto"
|
||||
loading={loading}
|
||||
search={search}
|
||||
itemId="id"
|
||||
executeQueryOptions={{
|
||||
defaultFields: ['@timestamp', 'synthetics.payload.message'],
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const TIMESTAMP_LABEL = i18n.translate('xpack.synthetics.monitorList.timestamp', {
|
||||
defaultMessage: 'Timestamp',
|
||||
});
|
||||
|
||||
export const ERROR_SUMMARY_LABEL = i18n.translate('xpack.synthetics.monitorList.errorSummary', {
|
||||
defaultMessage: 'Error summary',
|
||||
});
|
||||
|
||||
export const VIEW_IN_DISCOVER_LABEL = i18n.translate(
|
||||
'xpack.synthetics.monitorList.viewInDiscover',
|
||||
{
|
||||
defaultMessage: 'View in discover',
|
||||
}
|
||||
);
|
||||
|
||||
export const TEST_RUN_LOGS_LABEL = i18n.translate('xpack.synthetics.monitorList.testRunLogs', {
|
||||
defaultMessage: 'Test run logs',
|
||||
});
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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 { createEsParams, useEsSearch } from '@kbn/observability-plugin/public';
|
||||
import { Ping } from '../../../../../../common/runtime_types';
|
||||
import { SYNTHETICS_INDEX_PATTERN } from '../../../../../../common/constants';
|
||||
|
||||
export const useStdErrorLogs = ({
|
||||
monitorId,
|
||||
checkGroup,
|
||||
}: {
|
||||
monitorId?: string;
|
||||
checkGroup?: string;
|
||||
}) => {
|
||||
const { data, loading } = useEsSearch(
|
||||
createEsParams({
|
||||
index: !monitorId && !checkGroup ? '' : SYNTHETICS_INDEX_PATTERN,
|
||||
body: {
|
||||
size: 1000,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
term: {
|
||||
'synthetics.type': 'stderr',
|
||||
},
|
||||
},
|
||||
...(monitorId
|
||||
? [
|
||||
{
|
||||
term: {
|
||||
'monitor.id': monitorId,
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(checkGroup
|
||||
? [
|
||||
{
|
||||
term: {
|
||||
'monitor.check_group': checkGroup,
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
[],
|
||||
{ name: 'getStdErrLogs' }
|
||||
);
|
||||
|
||||
return {
|
||||
items: data?.hits.hits.map((hit) => ({ ...(hit._source as Ping), id: hit._id })) ?? [],
|
||||
loading,
|
||||
};
|
||||
};
|
|
@ -9,6 +9,7 @@ import React from 'react';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiAccordion, EuiDescribedFormGroup, EuiPanel, EuiSpacer } from '@elastic/eui';
|
||||
import { useFormContext, FieldError } from 'react-hook-form';
|
||||
import styled from 'styled-components';
|
||||
import { FORM_CONFIG } from '../form/form_config';
|
||||
import { Field } from '../form/field';
|
||||
import { ConfigKey, FormMonitorType } from '../types';
|
||||
|
@ -31,11 +32,14 @@ export const AdvancedConfig = () => {
|
|||
<EuiSpacer />
|
||||
{FORM_CONFIG[type].advanced?.map((configGroup) => {
|
||||
return (
|
||||
<EuiDescribedFormGroup
|
||||
<DescripedFormGroup
|
||||
description={configGroup.description}
|
||||
title={<h4>{configGroup.title}</h4>}
|
||||
fullWidth
|
||||
key={configGroup.title}
|
||||
descriptionFlexItemProps={{ style: { minWidth: 200 } }}
|
||||
fieldFlexItemProps={{ style: { minWidth: 500 } }}
|
||||
style={{ flexWrap: 'wrap' }}
|
||||
>
|
||||
{configGroup.components.map((field) => {
|
||||
return (
|
||||
|
@ -46,10 +50,16 @@ export const AdvancedConfig = () => {
|
|||
/>
|
||||
);
|
||||
})}
|
||||
</EuiDescribedFormGroup>
|
||||
</DescripedFormGroup>
|
||||
);
|
||||
})}
|
||||
</EuiAccordion>
|
||||
</EuiPanel>
|
||||
) : null;
|
||||
};
|
||||
|
||||
const DescripedFormGroup = styled(EuiDescribedFormGroup)`
|
||||
> div.euiFlexGroup {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
* 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, { useState } from 'react';
|
||||
import { EuiButton, EuiToolTip } from '@elastic/eui';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { useFetcher } from '@kbn/observability-plugin/public';
|
||||
import { TestNowModeFlyout, TestRun } from '../../test_now_mode/test_now_mode_flyout';
|
||||
import { format } from './formatter';
|
||||
import {
|
||||
Locations,
|
||||
MonitorFields as MonitorFieldsType,
|
||||
} from '../../../../../../common/runtime_types';
|
||||
import { runOnceMonitor } from '../../../state/manual_test_runs/api';
|
||||
|
||||
export const RunTestButton = () => {
|
||||
const { watch, formState, getValues } = useFormContext();
|
||||
|
||||
const [inProgress, setInProgress] = useState(false);
|
||||
const [testRun, setTestRun] = useState<TestRun>();
|
||||
|
||||
const handleTestNow = () => {
|
||||
const config = getValues() as MonitorFieldsType;
|
||||
if (config) {
|
||||
setInProgress(true);
|
||||
setTestRun({
|
||||
id: uuidv4(),
|
||||
monitor: format(config) as MonitorFieldsType,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const {
|
||||
data,
|
||||
loading: isPushing,
|
||||
error: serviceError,
|
||||
} = useFetcher(() => {
|
||||
// in case of test now mode outside of form add/edit, we don't need to trigger since it's already triggered
|
||||
if (testRun?.id) {
|
||||
return runOnceMonitor({
|
||||
monitor: testRun.monitor,
|
||||
id: testRun.id,
|
||||
});
|
||||
}
|
||||
}, [testRun?.id]);
|
||||
|
||||
const locations = watch('locations') as Locations;
|
||||
|
||||
const { tooltipContent, isDisabled } = useTooltipContent(
|
||||
locations,
|
||||
formState.isValid,
|
||||
inProgress
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiToolTip key={tooltipContent} content={tooltipContent}>
|
||||
<EuiButton
|
||||
data-test-subj="syntheticsRunTestBtn"
|
||||
color="success"
|
||||
disabled={isDisabled}
|
||||
aria-label={TEST_NOW_ARIA_LABEL}
|
||||
iconType="play"
|
||||
onClick={() => {
|
||||
handleTestNow();
|
||||
}}
|
||||
>
|
||||
{RUN_TEST}
|
||||
</EuiButton>
|
||||
</EuiToolTip>
|
||||
{testRun && (
|
||||
<TestNowModeFlyout
|
||||
serviceError={serviceError}
|
||||
errors={data?.errors ?? []}
|
||||
isPushing={Boolean(isPushing)}
|
||||
testRun={testRun}
|
||||
inProgress={inProgress}
|
||||
onClose={() => {
|
||||
setTestRun(undefined);
|
||||
setInProgress(false);
|
||||
}}
|
||||
onDone={() => {
|
||||
setInProgress(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const useTooltipContent = (
|
||||
locations: Locations,
|
||||
isValid: boolean,
|
||||
isTestRunInProgress?: boolean
|
||||
) => {
|
||||
const isAnyPublicLocationSelected = locations?.some((loc) => loc.isServiceManaged);
|
||||
const isOnlyPrivateLocations = (locations?.length ?? 0) > 0 && !isAnyPublicLocationSelected;
|
||||
|
||||
let tooltipContent =
|
||||
isOnlyPrivateLocations || (isValid && !isAnyPublicLocationSelected)
|
||||
? PRIVATE_AVAILABLE_LABEL
|
||||
: TEST_NOW_DESCRIPTION;
|
||||
|
||||
tooltipContent = isTestRunInProgress ? TEST_SCHEDULED_LABEL : tooltipContent;
|
||||
|
||||
const isDisabled = !isValid || isTestRunInProgress || !isAnyPublicLocationSelected;
|
||||
|
||||
return { tooltipContent, isDisabled };
|
||||
};
|
||||
|
||||
const TEST_NOW_DESCRIPTION = i18n.translate('xpack.synthetics.testRun.description', {
|
||||
defaultMessage: 'Test your monitor and verify the results before saving',
|
||||
});
|
||||
|
||||
const TEST_SCHEDULED_LABEL = i18n.translate('xpack.synthetics.monitorList.testNow.scheduled', {
|
||||
defaultMessage: 'Test is already scheduled',
|
||||
});
|
||||
|
||||
const PRIVATE_AVAILABLE_LABEL = i18n.translate(
|
||||
'xpack.synthetics.monitorList.testNow.available.private',
|
||||
{
|
||||
defaultMessage: `You can't currently test monitors running on private locations on demand.`,
|
||||
}
|
||||
);
|
||||
|
||||
const TEST_NOW_ARIA_LABEL = i18n.translate('xpack.synthetics.monitorList.testNow.AriaLabel', {
|
||||
defaultMessage: 'Click to run test now',
|
||||
});
|
||||
|
||||
const RUN_TEST = i18n.translate('xpack.synthetics.monitorList.runTest.label', {
|
||||
defaultMessage: 'Run test',
|
||||
});
|
|
@ -5,32 +5,26 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Redirect, useParams, useHistory, useRouteMatch } from 'react-router-dom';
|
||||
import React, { useState } from 'react';
|
||||
import { Redirect, useParams, useHistory } from 'react-router-dom';
|
||||
import { EuiButton, EuiLink, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { useFetcher, FETCH_STATUS } from '@kbn/observability-plugin/public';
|
||||
import { FETCH_STATUS } from '@kbn/observability-plugin/public';
|
||||
import { RunTestButton } from './run_test_btn';
|
||||
import { useMonitorSave } from '../hooks/use_monitor_save';
|
||||
import { DeleteMonitor } from '../../monitors_page/management/monitor_list_table/delete_monitor';
|
||||
import { SyntheticsMonitor } from '../types';
|
||||
import { ConfigKey, SourceType, SyntheticsMonitor } from '../types';
|
||||
import { format } from './formatter';
|
||||
import {
|
||||
createMonitorAPI,
|
||||
getMonitorAPI,
|
||||
updateMonitorAPI,
|
||||
} from '../../../state/monitor_management/api';
|
||||
import { kibanaService } from '../../../../../utils/kibana_service';
|
||||
|
||||
import { MONITORS_ROUTE, MONITOR_EDIT_ROUTE } from '../../../../../../common/constants';
|
||||
import { MONITORS_ROUTE } from '../../../../../../common/constants';
|
||||
|
||||
export const ActionBar = () => {
|
||||
const { monitorId } = useParams<{ monitorId: string }>();
|
||||
const history = useHistory();
|
||||
const editRouteMatch = useRouteMatch({ path: MONITOR_EDIT_ROUTE });
|
||||
const isEdit = editRouteMatch?.isExact;
|
||||
const {
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
formState: { errors, defaultValues },
|
||||
} = useFormContext();
|
||||
|
||||
const [monitorPendingDeletion, setMonitorPendingDeletion] = useState<SyntheticsMonitor | null>(
|
||||
|
@ -39,44 +33,7 @@ export const ActionBar = () => {
|
|||
|
||||
const [monitorData, setMonitorData] = useState<SyntheticsMonitor | undefined>(undefined);
|
||||
|
||||
const { data: monitorObject } = useFetcher(() => {
|
||||
if (isEdit) {
|
||||
return getMonitorAPI({ id: monitorId });
|
||||
}
|
||||
return undefined;
|
||||
}, []);
|
||||
|
||||
const { data, status } = useFetcher(() => {
|
||||
if (!monitorData) {
|
||||
return null;
|
||||
}
|
||||
if (isEdit) {
|
||||
return updateMonitorAPI({
|
||||
id: monitorId,
|
||||
monitor: monitorData,
|
||||
});
|
||||
} else {
|
||||
return createMonitorAPI({
|
||||
monitor: monitorData,
|
||||
});
|
||||
}
|
||||
}, [monitorData]);
|
||||
|
||||
const loading = status === FETCH_STATUS.LOADING;
|
||||
|
||||
useEffect(() => {
|
||||
if (status === FETCH_STATUS.FAILURE) {
|
||||
kibanaService.toasts.addDanger({
|
||||
title: MONITOR_FAILURE_LABEL,
|
||||
toastLifeTimeMs: 3000,
|
||||
});
|
||||
} else if (status === FETCH_STATUS.SUCCESS && !loading) {
|
||||
kibanaService.toasts.addSuccess({
|
||||
title: monitorId ? MONITOR_UPDATED_SUCCESS_LABEL : MONITOR_SUCCESS_LABEL,
|
||||
toastLifeTimeMs: 3000,
|
||||
});
|
||||
}
|
||||
}, [data, status, monitorId, loading]);
|
||||
const { status, loading, isEdit } = useMonitorSave({ monitorData });
|
||||
|
||||
const formSubmitter = (formData: Record<string, any>) => {
|
||||
if (!Object.keys(errors).length) {
|
||||
|
@ -90,12 +47,12 @@ export const ActionBar = () => {
|
|||
<>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={true}>
|
||||
{isEdit && monitorObject && (
|
||||
{isEdit && defaultValues && (
|
||||
<div>
|
||||
<EuiButton
|
||||
color="danger"
|
||||
onClick={() => {
|
||||
setMonitorPendingDeletion(monitorObject?.attributes);
|
||||
setMonitorPendingDeletion(defaultValues as SyntheticsMonitor);
|
||||
}}
|
||||
>
|
||||
{DELETE_MONITOR_LABEL}
|
||||
|
@ -103,9 +60,13 @@ export const ActionBar = () => {
|
|||
</div>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLink href={history.createHref({ pathname: MONITORS_ROUTE })}>{CANCEL_LABEL}</EuiLink>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<RunTestButton />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill
|
||||
|
@ -118,13 +79,15 @@ export const ActionBar = () => {
|
|||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{monitorPendingDeletion && monitorObject && (
|
||||
{monitorPendingDeletion && (
|
||||
<DeleteMonitor
|
||||
fields={monitorObject?.attributes}
|
||||
configId={monitorId}
|
||||
name={defaultValues?.[ConfigKey.NAME] ?? ''}
|
||||
reloadPage={() => {
|
||||
history.push(MONITORS_ROUTE);
|
||||
}}
|
||||
setMonitorPendingDeletion={setMonitorPendingDeletion}
|
||||
isProjectMonitor={defaultValues?.[ConfigKey.MONITOR_SOURCE_TYPE] === SourceType.PROJECT}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
@ -155,24 +118,3 @@ const UPDATE_MONITOR_LABEL = i18n.translate(
|
|||
defaultMessage: 'Update monitor',
|
||||
}
|
||||
);
|
||||
|
||||
const MONITOR_SUCCESS_LABEL = i18n.translate(
|
||||
'xpack.synthetics.monitorManagement.monitorAddedSuccessMessage',
|
||||
{
|
||||
defaultMessage: 'Monitor added successfully.',
|
||||
}
|
||||
);
|
||||
|
||||
const MONITOR_UPDATED_SUCCESS_LABEL = i18n.translate(
|
||||
'xpack.synthetics.monitorManagement.monitorEditedSuccessMessage',
|
||||
{
|
||||
defaultMessage: 'Monitor updated successfully.',
|
||||
}
|
||||
);
|
||||
|
||||
const MONITOR_FAILURE_LABEL = i18n.translate(
|
||||
'xpack.synthetics.monitorManagement.monitorFailureMessage',
|
||||
{
|
||||
defaultMessage: 'Monitor was unable to be saved. Please try again later.',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* 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 { FETCH_STATUS, useFetcher } from '@kbn/observability-plugin/public';
|
||||
import { useParams, useRouteMatch } from 'react-router-dom';
|
||||
import { useEffect } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { MONITOR_EDIT_ROUTE } from '../../../../../../common/constants';
|
||||
import { SyntheticsMonitor } from '../../../../../../common/runtime_types';
|
||||
import { createMonitorAPI, updateMonitorAPI } from '../../../state/monitor_management/api';
|
||||
import { kibanaService } from '../../../../../utils/kibana_service';
|
||||
|
||||
export const useMonitorSave = ({ monitorData }: { monitorData?: SyntheticsMonitor }) => {
|
||||
const { monitorId } = useParams<{ monitorId: string }>();
|
||||
|
||||
const editRouteMatch = useRouteMatch({ path: MONITOR_EDIT_ROUTE });
|
||||
const isEdit = editRouteMatch?.isExact;
|
||||
|
||||
const { data, status, loading } = useFetcher(() => {
|
||||
if (monitorData) {
|
||||
if (isEdit) {
|
||||
return updateMonitorAPI({
|
||||
id: monitorId,
|
||||
monitor: monitorData,
|
||||
});
|
||||
} else {
|
||||
return createMonitorAPI({
|
||||
monitor: monitorData,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [monitorData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (status === FETCH_STATUS.FAILURE) {
|
||||
kibanaService.toasts.addDanger({
|
||||
title: MONITOR_FAILURE_LABEL,
|
||||
toastLifeTimeMs: 3000,
|
||||
});
|
||||
} else if (status === FETCH_STATUS.SUCCESS && !loading) {
|
||||
kibanaService.toasts.addSuccess({
|
||||
title: monitorId ? MONITOR_UPDATED_SUCCESS_LABEL : MONITOR_SUCCESS_LABEL,
|
||||
toastLifeTimeMs: 3000,
|
||||
});
|
||||
}
|
||||
}, [data, status, monitorId, loading]);
|
||||
|
||||
return { status, loading, isEdit };
|
||||
};
|
||||
|
||||
const MONITOR_SUCCESS_LABEL = i18n.translate(
|
||||
'xpack.synthetics.monitorManagement.monitorAddedSuccessMessage',
|
||||
{
|
||||
defaultMessage: 'Monitor added successfully.',
|
||||
}
|
||||
);
|
||||
|
||||
const MONITOR_UPDATED_SUCCESS_LABEL = i18n.translate(
|
||||
'xpack.synthetics.monitorManagement.monitorEditedSuccessMessage',
|
||||
{
|
||||
defaultMessage: 'Monitor updated successfully.',
|
||||
}
|
||||
);
|
||||
|
||||
const MONITOR_FAILURE_LABEL = i18n.translate(
|
||||
'xpack.synthetics.monitorManagement.monitorFailureMessage',
|
||||
{
|
||||
defaultMessage: 'Monitor was unable to be saved. Please try again later.',
|
||||
}
|
||||
);
|
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import { EuiLoadingSpinner } from '@elastic/eui';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTrackPageview } from '@kbn/observability-plugin/public';
|
||||
|
||||
|
@ -21,7 +20,7 @@ import { useMonitorAddEditBreadcrumbs } from './use_breadcrumbs';
|
|||
|
||||
const MonitorAddPage = () => {
|
||||
useTrackPageview({ app: 'synthetics', path: 'add-monitor' });
|
||||
const { space, loading, error } = useKibanaSpace();
|
||||
const { space } = useKibanaSpace();
|
||||
useTrackPageview({ app: 'synthetics', path: 'add-monitor', delay: 15000 });
|
||||
useMonitorAddEditBreadcrumbs();
|
||||
const dispatch = useDispatch();
|
||||
|
@ -30,12 +29,10 @@ const MonitorAddPage = () => {
|
|||
dispatch(getServiceLocations());
|
||||
}, [dispatch]);
|
||||
|
||||
return !loading && !error ? (
|
||||
return (
|
||||
<MonitorForm space={space?.id}>
|
||||
<MonitorSteps stepMap={ADD_MONITOR_STEPS} />
|
||||
</MonitorForm>
|
||||
) : (
|
||||
<EuiLoadingSpinner />
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -6,10 +6,10 @@
|
|||
*/
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import { EuiLoadingSpinner } from '@elastic/eui';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTrackPageview, useFetcher } from '@kbn/observability-plugin/public';
|
||||
import { LoadingState } from '../monitors_page/overview/overview/monitor_detail_flyout';
|
||||
import { ConfigKey } from '../../../../../common/runtime_types';
|
||||
import { getServiceLocations } from '../../state';
|
||||
import { ServiceAllowedWrapper } from '../common/wrappers/service_allowed_wrapper';
|
||||
|
@ -44,7 +44,7 @@ const MonitorEditPage: React.FC = () => {
|
|||
/>
|
||||
</MonitorForm>
|
||||
) : (
|
||||
<EuiLoadingSpinner />
|
||||
<LoadingState />
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -15,11 +15,11 @@ interface Props {
|
|||
|
||||
export const Step = ({ description, children }: Props) => {
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup gutterSize="s" wrap>
|
||||
<EuiFlexItem style={{ minWidth: 200 }}>
|
||||
<EuiText>{description}</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>{children}</EuiFlexItem>
|
||||
<EuiFlexItem style={{ minWidth: 500 }}>{children}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -12,29 +12,23 @@ import { toMountPoint } from '@kbn/kibana-react-plugin/public';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import {
|
||||
ConfigKey,
|
||||
EncryptedSyntheticsSavedMonitor,
|
||||
SourceType,
|
||||
SyntheticsMonitor,
|
||||
} from '../../../../../../../common/runtime_types';
|
||||
import { fetchDeleteMonitor } from '../../../../state';
|
||||
import { kibanaService } from '../../../../../../utils/kibana_service';
|
||||
import * as labels from './labels';
|
||||
|
||||
export const DeleteMonitor = ({
|
||||
fields,
|
||||
name,
|
||||
reloadPage,
|
||||
configId,
|
||||
isProjectMonitor,
|
||||
setMonitorPendingDeletion,
|
||||
}: {
|
||||
fields: SyntheticsMonitor | EncryptedSyntheticsSavedMonitor;
|
||||
configId: string;
|
||||
name: string;
|
||||
isProjectMonitor: boolean;
|
||||
reloadPage: () => void;
|
||||
setMonitorPendingDeletion: (val: null) => void;
|
||||
}) => {
|
||||
const configId = fields[ConfigKey.CONFIG_ID];
|
||||
const name = fields[ConfigKey.NAME];
|
||||
const isProjectMonitor = fields[ConfigKey.MONITOR_SOURCE_TYPE] === SourceType.PROJECT;
|
||||
|
||||
const [isDeleting, setIsDeleting] = useState<boolean>(false);
|
||||
|
||||
const handleConfirmDelete = () => {
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
ConfigKey,
|
||||
EncryptedSyntheticsSavedMonitor,
|
||||
OverviewStatusState,
|
||||
SourceType,
|
||||
} from '../../../../../../../common/runtime_types';
|
||||
import { useMonitorListColumns } from './columns';
|
||||
import * as labels from './labels';
|
||||
|
@ -128,9 +129,13 @@ export const MonitorList = ({
|
|||
</EuiPanel>
|
||||
{monitorPendingDeletion && (
|
||||
<DeleteMonitor
|
||||
fields={monitorPendingDeletion}
|
||||
reloadPage={reloadPage}
|
||||
configId={monitorPendingDeletion[ConfigKey.CONFIG_ID]}
|
||||
name={monitorPendingDeletion[ConfigKey.NAME] ?? ''}
|
||||
setMonitorPendingDeletion={setMonitorPendingDeletion}
|
||||
isProjectMonitor={
|
||||
monitorPendingDeletion[ConfigKey.MONITOR_SOURCE_TYPE] === SourceType.PROJECT
|
||||
}
|
||||
reloadPage={reloadPage}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -15,8 +15,12 @@ import {
|
|||
EuiLoadingSpinner,
|
||||
} from '@elastic/eui';
|
||||
import { FETCH_STATUS } from '@kbn/observability-plugin/public';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
manualTestMonitorAction,
|
||||
manualTestRunInProgressSelector,
|
||||
} from '../../../../state/manual_test_runs';
|
||||
import { toggleStatusAlert } from '../../../../../../../common/runtime_types/monitor_management/alert_config';
|
||||
import { useSelectedMonitor } from '../../../monitor_details/hooks/use_selected_monitor';
|
||||
import { useMonitorAlertEnable } from '../../../../hooks/use_monitor_alert_enable';
|
||||
|
@ -124,6 +128,8 @@ export function ActionsPopover({
|
|||
monitor.isEnabled ? disableMonitorLabel : enableMonitorLabel
|
||||
);
|
||||
|
||||
const testInProgress = useSelector(manualTestRunInProgressSelector(monitor.configId));
|
||||
|
||||
useEffect(() => {
|
||||
if (status === FETCH_STATUS.LOADING) {
|
||||
setEnableLabel(loadingLabel(monitor.isEnabled));
|
||||
|
@ -140,7 +146,12 @@ export function ActionsPopover({
|
|||
onClick: () => {
|
||||
if (locationName) {
|
||||
dispatch(
|
||||
setFlyoutConfig({ configId: monitor.configId, location: locationName, id: monitor.id })
|
||||
setFlyoutConfig({
|
||||
configId: monitor.configId,
|
||||
location: locationName,
|
||||
id: monitor.id,
|
||||
locationId: monitor.location.id,
|
||||
})
|
||||
);
|
||||
setIsPopoverOpen(false);
|
||||
}
|
||||
|
@ -156,12 +167,16 @@ export function ActionsPopover({
|
|||
href: detailUrl,
|
||||
},
|
||||
quickInspectPopoverItem,
|
||||
// not rendering this for now because the manual test flyout is
|
||||
// still in the design phase
|
||||
// {
|
||||
// name: 'Run test manually',
|
||||
// icon: 'beaker',
|
||||
// },
|
||||
{
|
||||
name: runTestManually,
|
||||
icon: 'beaker',
|
||||
disabled: testInProgress,
|
||||
onClick: () => {
|
||||
dispatch(manualTestMonitorAction.get(monitor.configId));
|
||||
dispatch(setFlyoutConfig(null));
|
||||
setIsPopoverOpen(false);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: actionsMenuEditMonitorName,
|
||||
icon: 'pencil',
|
||||
|
@ -212,6 +227,7 @@ export function ActionsPopover({
|
|||
size={iconSize}
|
||||
display="empty"
|
||||
onClick={() => setIsPopoverOpen((b: boolean) => !b)}
|
||||
title={openActionsMenuAria}
|
||||
/>
|
||||
</IconPanel>
|
||||
}
|
||||
|
@ -240,6 +256,10 @@ const quickInspectName = i18n.translate('xpack.synthetics.overview.actions.quick
|
|||
defaultMessage: 'Quick inspect',
|
||||
});
|
||||
|
||||
const runTestManually = i18n.translate('xpack.synthetics.overview.actions.runTestManually.title', {
|
||||
defaultMessage: 'Run test manually',
|
||||
});
|
||||
|
||||
const openActionsMenuAria = i18n.translate(
|
||||
'xpack.synthetics.overview.actions.openPopover.ariaLabel',
|
||||
{
|
||||
|
|
|
@ -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 * as React from 'react';
|
||||
import { EuiLoadingSpinner, EuiToolTip } from '@elastic/eui';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { manualTestRunSelector } from '../../../../state/manual_test_runs';
|
||||
|
||||
export const ManualTestRunProgress = ({ configId }: { configId: string }) => {
|
||||
const testNowRun = useSelector(manualTestRunSelector(configId));
|
||||
|
||||
const inProgress = testNowRun?.status === 'in-progress' || testNowRun?.status === 'loading';
|
||||
|
||||
return inProgress ? (
|
||||
<EuiToolTip position="top" content="Test is in progress">
|
||||
<EuiLoadingSpinner />
|
||||
</EuiToolTip>
|
||||
) : null;
|
||||
};
|
|
@ -10,11 +10,19 @@ import { Chart, Settings, Metric, MetricTrendShape } from '@elastic/charts';
|
|||
import { EuiPanel } from '@elastic/eui';
|
||||
import { DARK_THEME } from '@elastic/charts';
|
||||
import { useTheme } from '@kbn/observability-plugin/public';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import moment from 'moment';
|
||||
import { ManualTestRunProgress } from './manual_run_progress';
|
||||
import { useLocationName, useStatusByLocationOverview } from '../../../../hooks';
|
||||
import { formatDuration } from '../../../../utils/formatting';
|
||||
import { MonitorOverviewItem } from '../../../../../../../common/runtime_types';
|
||||
import { ActionsPopover } from './actions_popover';
|
||||
import { OverviewGridItemLoader } from './overview_grid_item_loader';
|
||||
import {
|
||||
hideTestNowFlyoutAction,
|
||||
manualTestRunInProgressSelector,
|
||||
toggleTestNowFlyoutAction,
|
||||
} from '../../../../state/manual_test_runs';
|
||||
|
||||
export const getColor = (
|
||||
theme: ReturnType<typeof useTheme>,
|
||||
|
@ -47,14 +55,18 @@ export const MetricItem = ({
|
|||
data: Array<{ x: number; y: number }>;
|
||||
averageDuration: number;
|
||||
loaded: boolean;
|
||||
onClick: (params: { id: string; configId: string; location: string }) => void;
|
||||
onClick: (params: { id: string; configId: string; location: string; locationId: string }) => void;
|
||||
}) => {
|
||||
const [isMouseOver, setIsMouseOver] = useState(false);
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
const locationName = useLocationName({ locationId: monitor.location?.id });
|
||||
const status = useStatusByLocationOverview(monitor.id, locationName);
|
||||
const { status, timestamp } = useStatusByLocationOverview(monitor.id, locationName);
|
||||
const theme = useTheme();
|
||||
|
||||
const testInProgress = useSelector(manualTestRunInProgressSelector(monitor.configId));
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
return (
|
||||
<div data-test-subj={`${monitor.name}-metric-item`} style={{ height: '160px' }}>
|
||||
{loaded ? (
|
||||
|
@ -74,14 +86,25 @@ export const MetricItem = ({
|
|||
height: '100%',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
title={moment(timestamp).format('LLL')}
|
||||
>
|
||||
<Chart>
|
||||
<Settings
|
||||
onElementClick={() =>
|
||||
monitor.configId &&
|
||||
locationName &&
|
||||
onClick({ configId: monitor.configId, id: monitor.id, location: locationName })
|
||||
}
|
||||
onElementClick={() => {
|
||||
if (testInProgress) {
|
||||
dispatch(toggleTestNowFlyoutAction(monitor.configId));
|
||||
} else {
|
||||
dispatch(hideTestNowFlyoutAction());
|
||||
}
|
||||
if (!testInProgress && locationName) {
|
||||
onClick({
|
||||
configId: monitor.configId,
|
||||
id: monitor.id,
|
||||
location: locationName,
|
||||
locationId: monitor.location?.id,
|
||||
});
|
||||
}
|
||||
}}
|
||||
baseTheme={DARK_THEME}
|
||||
/>
|
||||
<Metric
|
||||
|
@ -89,6 +112,7 @@ export const MetricItem = ({
|
|||
data={[
|
||||
[
|
||||
{
|
||||
icon: () => <ManualTestRunProgress configId={monitor.configId} />,
|
||||
title: monitor.name,
|
||||
subtitle: locationName,
|
||||
value: averageDuration,
|
||||
|
|
|
@ -53,6 +53,7 @@ describe('Monitor Detail Flyout', () => {
|
|||
configId="test-id"
|
||||
id="test-id"
|
||||
location="US East"
|
||||
locationId="us-east"
|
||||
onClose={onCloseMock}
|
||||
onEnabledChange={jest.fn()}
|
||||
onLocationChange={jest.fn()}
|
||||
|
@ -76,6 +77,7 @@ describe('Monitor Detail Flyout', () => {
|
|||
configId="test-id"
|
||||
id="test-id"
|
||||
location="US East"
|
||||
locationId="us-east"
|
||||
onClose={jest.fn()}
|
||||
onEnabledChange={jest.fn()}
|
||||
onLocationChange={jest.fn()}
|
||||
|
@ -95,6 +97,7 @@ describe('Monitor Detail Flyout', () => {
|
|||
configId="test-id"
|
||||
id="test-id"
|
||||
location="US East"
|
||||
locationId="us-east"
|
||||
onClose={jest.fn()}
|
||||
onEnabledChange={jest.fn()}
|
||||
onLocationChange={jest.fn()}
|
||||
|
@ -130,6 +133,7 @@ describe('Monitor Detail Flyout', () => {
|
|||
configId="test-id"
|
||||
id="test-id"
|
||||
location="US East"
|
||||
locationId="us-east"
|
||||
onClose={jest.fn()}
|
||||
onEnabledChange={jest.fn()}
|
||||
onLocationChange={jest.fn()}
|
||||
|
|
|
@ -49,7 +49,7 @@ import { ClientPluginsStart } from '../../../../../../plugin';
|
|||
import { useStatusByLocation } from '../../../../hooks/use_status_by_location';
|
||||
import { MonitorEnabled } from '../../management/monitor_list_table/monitor_enabled';
|
||||
import { ActionsPopover } from './actions_popover';
|
||||
import { selectOverviewState } from '../../../../state';
|
||||
import { selectOverviewState, selectServiceLocationsState } from '../../../../state';
|
||||
import { useMonitorDetail } from '../../../../hooks/use_monitor_detail';
|
||||
import {
|
||||
ConfigKey,
|
||||
|
@ -65,9 +65,15 @@ interface Props {
|
|||
configId: string;
|
||||
id: string;
|
||||
location: string;
|
||||
locationId: string;
|
||||
onClose: () => void;
|
||||
onEnabledChange: () => void;
|
||||
onLocationChange: (params: { configId: string; id: string; location: string }) => void;
|
||||
onLocationChange: (params: {
|
||||
configId: string;
|
||||
id: string;
|
||||
location: string;
|
||||
locationId: string;
|
||||
}) => void;
|
||||
currentDurationChartFrom?: string;
|
||||
currentDurationChartTo?: string;
|
||||
previousDurationChartFrom?: string;
|
||||
|
@ -174,11 +180,13 @@ function LocationSelect({
|
|||
configId: string;
|
||||
monitor: EncryptedSyntheticsMonitor;
|
||||
onEnabledChange: () => void;
|
||||
setCurrentLocation: (location: string) => void;
|
||||
setCurrentLocation: (location: string, locationId: string) => void;
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const status = locations.find((l) => l.observer?.geo?.name === currentLocation)?.monitor?.status;
|
||||
|
||||
const { locations: allLocations } = useSelector(selectServiceLocationsState);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup wrap={true} responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
|
@ -213,13 +221,14 @@ function LocationSelect({
|
|||
id: 0,
|
||||
title: GO_TO_LOCATIONS_LABEL,
|
||||
items: locations.map((l) => {
|
||||
const loc = allLocations.find((ll) => ll.label === l.observer?.geo?.name);
|
||||
return {
|
||||
name: l.observer?.geo?.name,
|
||||
icon: <EuiHealth color={!!l.summary?.down ? 'danger' : 'success'} />,
|
||||
disabled: !l.observer?.geo?.name || l.observer.geo.name === currentLocation,
|
||||
onClick: () => {
|
||||
if (l.observer?.geo?.name && currentLocation !== l.observer.geo.name)
|
||||
setCurrentLocation(l.observer?.geo?.name);
|
||||
setCurrentLocation(l.observer?.geo?.name, loc?.id!);
|
||||
},
|
||||
};
|
||||
}),
|
||||
|
@ -256,7 +265,7 @@ export function LoadingState() {
|
|||
}
|
||||
|
||||
export function MonitorDetailFlyout(props: Props) {
|
||||
const { id, configId, onLocationChange } = props;
|
||||
const { id, configId, onLocationChange, locationId } = props;
|
||||
const {
|
||||
data: { monitors },
|
||||
} = useSelector(selectOverviewState);
|
||||
|
@ -267,12 +276,14 @@ export function MonitorDetailFlyout(props: Props) {
|
|||
}, [id, monitors]);
|
||||
|
||||
const setLocation = useCallback(
|
||||
(location: string) => onLocationChange({ id, configId, location }),
|
||||
(location: string, locationIdT: string) =>
|
||||
onLocationChange({ id, configId, location, locationId: locationIdT }),
|
||||
[id, configId, onLocationChange]
|
||||
);
|
||||
|
||||
const detailLink = useMonitorDetailLocator({
|
||||
configId: id,
|
||||
locationId,
|
||||
});
|
||||
|
||||
const {
|
||||
|
|
|
@ -58,8 +58,17 @@ export const OverviewGrid = memo(() => {
|
|||
const dispatch = useDispatch();
|
||||
|
||||
const setFlyoutConfigCallback = useCallback(
|
||||
({ configId, id, location }: { configId: string; id: string; location: string }) =>
|
||||
dispatch(setFlyoutConfig({ configId, id, location })),
|
||||
({
|
||||
configId,
|
||||
id,
|
||||
location,
|
||||
locationId,
|
||||
}: {
|
||||
configId: string;
|
||||
id: string;
|
||||
location: string;
|
||||
locationId: string;
|
||||
}) => dispatch(setFlyoutConfig({ configId, id, location, locationId })),
|
||||
[dispatch]
|
||||
);
|
||||
const hideFlyout = useCallback(() => dispatch(setFlyoutConfig(null)), [dispatch]);
|
||||
|
@ -159,6 +168,7 @@ export const OverviewGrid = memo(() => {
|
|||
configId={flyoutConfig.configId}
|
||||
id={flyoutConfig.id}
|
||||
location={flyoutConfig.location}
|
||||
locationId={flyoutConfig.locationId}
|
||||
onClose={hideFlyout}
|
||||
onEnabledChange={forceRefreshCallback}
|
||||
onLocationChange={setFlyoutConfigCallback}
|
||||
|
|
|
@ -14,7 +14,7 @@ export const OverviewGridItem = ({
|
|||
onClick,
|
||||
}: {
|
||||
monitor: MonitorOverviewItem;
|
||||
onClick: (params: { id: string; configId: string; location: string }) => void;
|
||||
onClick: (params: { id: string; configId: string; location: string; locationId: string }) => void;
|
||||
}) => {
|
||||
const { data, loading, averageDuration } = useLast50DurationChart({
|
||||
locationId: monitor.location?.id,
|
||||
|
|
|
@ -14,7 +14,7 @@ describe('useLocationMonitors', () => {
|
|||
it('returns expected results', () => {
|
||||
const { result } = renderHook(() => useLocationMonitors(), { wrapper: WrappedHelper });
|
||||
|
||||
expect(result.current).toStrictEqual({ locationMonitors: [], loading: true });
|
||||
expect(result.current).toStrictEqual({ locationMonitors: [], loading: false });
|
||||
expect(defaultCore.savedObjects.client.find).toHaveBeenCalledWith({
|
||||
aggs: {
|
||||
locations: {
|
||||
|
|
|
@ -0,0 +1,141 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import * as React from 'react';
|
||||
import { EuiAccordion, EuiText, EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import styled from 'styled-components';
|
||||
import { BrowserStepsList } from '../../common/monitor_test_result/browser_steps_list';
|
||||
import {
|
||||
CheckGroupResult,
|
||||
useBrowserRunOnceMonitors,
|
||||
} from '../hooks/use_browser_run_once_monitors';
|
||||
import { TestResultHeader } from '../test_result_header';
|
||||
import { StdErrorLogs } from '../../common/components/stderr_logs';
|
||||
|
||||
interface Props {
|
||||
testRunId: string;
|
||||
expectPings: number;
|
||||
onDone: (testRunId: string) => void;
|
||||
}
|
||||
export const BrowserTestRunResult = ({ expectPings, onDone, testRunId }: Props) => {
|
||||
const { summariesLoading, expectedSummariesLoaded, stepLoadingInProgress, checkGroupResults } =
|
||||
useBrowserRunOnceMonitors({
|
||||
testRunId,
|
||||
expectSummaryDocs: expectPings,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (expectedSummariesLoaded) {
|
||||
onDone(testRunId);
|
||||
}
|
||||
}, [onDone, expectedSummariesLoaded, testRunId]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{checkGroupResults.map((checkGroupResult) => {
|
||||
const { checkGroupId, journeyStarted, summaryDoc, stepsLoading, steps, completedSteps } =
|
||||
checkGroupResult;
|
||||
const isStepsLoading = !summariesLoading && journeyStarted && summaryDoc && stepsLoading;
|
||||
const isStepsLoadingFailed =
|
||||
summaryDoc && !summariesLoading && !stepLoadingInProgress && steps.length === 0;
|
||||
|
||||
return (
|
||||
<AccordionWrapper
|
||||
key={'accordion-' + checkGroupId}
|
||||
id={'accordion-' + checkGroupId}
|
||||
element="fieldset"
|
||||
className="euiAccordionForm"
|
||||
buttonClassName="euiAccordionForm__button"
|
||||
buttonContent={getButtonContent(checkGroupResult)}
|
||||
paddingSize="s"
|
||||
data-test-subj="expandResults"
|
||||
initialIsOpen={checkGroupResults.length === 1}
|
||||
>
|
||||
{isStepsLoading && (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="xs">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText>{LOADING_STEPS}</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiLoadingSpinner size="s" />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
{isStepsLoadingFailed && (
|
||||
<EuiText color="danger">{summaryDoc?.error?.message ?? FAILED_TO_RUN}</EuiText>
|
||||
)}
|
||||
|
||||
{isStepsLoadingFailed &&
|
||||
summaryDoc?.error?.message?.includes('journey did not finish executing') && (
|
||||
<StdErrorLogs checkGroup={summaryDoc.monitor.check_group} hideTitle={true} />
|
||||
)}
|
||||
|
||||
{completedSteps > 0 && (
|
||||
<BrowserStepsList
|
||||
steps={steps}
|
||||
loading={Boolean(stepLoadingInProgress)}
|
||||
error={undefined}
|
||||
showStepNumber={true}
|
||||
compressed={true}
|
||||
/>
|
||||
)}
|
||||
</AccordionWrapper>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const AccordionWrapper = styled(EuiAccordion)`
|
||||
.euiAccordion__buttonContent {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
function getButtonContent({
|
||||
journeyDoc,
|
||||
summaryDoc,
|
||||
checkGroupId,
|
||||
journeyStarted,
|
||||
completedSteps,
|
||||
}: CheckGroupResult) {
|
||||
return (
|
||||
<div>
|
||||
<TestResultHeader
|
||||
title={journeyDoc?.observer?.geo?.name}
|
||||
summaryDocs={summaryDoc ? [summaryDoc] : []}
|
||||
checkGroupId={checkGroupId}
|
||||
journeyStarted={journeyStarted}
|
||||
isCompleted={Boolean(summaryDoc)}
|
||||
configId={journeyDoc?.config_id}
|
||||
/>
|
||||
<EuiText size="s">
|
||||
<p>
|
||||
<EuiText color="subdued">
|
||||
{i18n.translate('xpack.synthetics.monitorManagement.stepCompleted', {
|
||||
defaultMessage:
|
||||
'{stepCount, number} {stepCount, plural, one {step} other {steps}} completed',
|
||||
values: {
|
||||
stepCount: completedSteps ?? 0,
|
||||
},
|
||||
})}
|
||||
</EuiText>
|
||||
</p>
|
||||
</EuiText>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const FAILED_TO_RUN = i18n.translate('xpack.synthetics.monitorManagement.failedRun', {
|
||||
defaultMessage: 'Failed to run steps',
|
||||
});
|
||||
|
||||
const LOADING_STEPS = i18n.translate('xpack.synthetics.monitorManagement.loadingSteps', {
|
||||
defaultMessage: 'Loading steps...',
|
||||
});
|
|
@ -0,0 +1,271 @@
|
|||
/*
|
||||
* 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 { useEffect, useState, useRef } from 'react';
|
||||
import { createEsParams, useEsSearch, useFetcher } from '@kbn/observability-plugin/public';
|
||||
import { useTickTick } from './use_tick_tick';
|
||||
import { isStepEnd } from '../../common/monitor_test_result/browser_steps_list';
|
||||
import { SYNTHETICS_INDEX_PATTERN } from '../../../../../../common/constants';
|
||||
import { JourneyStep } from '../../../../../../common/runtime_types';
|
||||
import { fetchBrowserJourney } from '../../../state';
|
||||
|
||||
export interface CheckGroupResult {
|
||||
checkGroupId: string;
|
||||
journeyStarted: boolean;
|
||||
journeyDoc?: JourneyStep;
|
||||
summaryDoc?: JourneyStep;
|
||||
steps: JourneyStep[];
|
||||
stepsLoading: boolean;
|
||||
completedSteps: number;
|
||||
}
|
||||
|
||||
export const useBrowserEsResults = ({
|
||||
testRunId,
|
||||
lastRefresh,
|
||||
}: {
|
||||
testRunId: string;
|
||||
lastRefresh: number;
|
||||
}) => {
|
||||
return useEsSearch(
|
||||
createEsParams({
|
||||
index: SYNTHETICS_INDEX_PATTERN,
|
||||
body: {
|
||||
sort: [
|
||||
{
|
||||
'@timestamp': 'desc',
|
||||
},
|
||||
],
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
terms: {
|
||||
'synthetics.type': ['heartbeat/summary', 'journey/start'],
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
term: {
|
||||
test_run_id: testRunId,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
size: 1000,
|
||||
}),
|
||||
[testRunId, lastRefresh],
|
||||
{ name: 'TestRunData' }
|
||||
);
|
||||
};
|
||||
|
||||
export const useBrowserRunOnceMonitors = ({
|
||||
testRunId,
|
||||
skipDetails = false,
|
||||
refresh = true,
|
||||
expectSummaryDocs,
|
||||
}: {
|
||||
testRunId: string;
|
||||
refresh?: boolean;
|
||||
skipDetails?: boolean;
|
||||
expectSummaryDocs: number;
|
||||
}) => {
|
||||
const { refreshTimer, lastRefresh } = useTickTick(5 * 1000, refresh);
|
||||
|
||||
const [checkGroupResults, setCheckGroupResults] = useState<CheckGroupResult[]>(() => {
|
||||
return new Array(expectSummaryDocs)
|
||||
.fill({
|
||||
checkGroupId: '',
|
||||
journeyStarted: false,
|
||||
steps: [],
|
||||
stepsLoading: false,
|
||||
completedSteps: 0,
|
||||
} as CheckGroupResult)
|
||||
.map((emptyCheckGroup, index) => ({
|
||||
...emptyCheckGroup,
|
||||
checkGroupId: `placeholder-check-group-${index}`,
|
||||
}));
|
||||
});
|
||||
|
||||
const lastUpdated = useRef<{ checksum: string; time: number }>({
|
||||
checksum: '',
|
||||
time: Date.now(),
|
||||
});
|
||||
|
||||
const { data, loading: summariesLoading } = useBrowserEsResults({
|
||||
testRunId,
|
||||
lastRefresh,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const hits = data?.hits.hits;
|
||||
|
||||
if (hits && hits.length > 0) {
|
||||
const allDocs = (hits ?? []).map(({ _source }) => _source as JourneyStep);
|
||||
const checkGroupsById = allDocs
|
||||
.filter(
|
||||
(doc) =>
|
||||
doc.synthetics?.type === 'journey/start' || doc.synthetics?.type === 'heartbeat/summary'
|
||||
)
|
||||
.reduce(
|
||||
(acc, cur) => ({
|
||||
...acc,
|
||||
[cur.monitor.check_group]: {
|
||||
checkGroupId: cur.monitor.check_group,
|
||||
journeyStarted: true,
|
||||
journeyDoc: cur,
|
||||
summaryDoc: null,
|
||||
steps: [],
|
||||
stepsLoading: false,
|
||||
completedSteps: 0,
|
||||
},
|
||||
}),
|
||||
{}
|
||||
) as Record<string, CheckGroupResult>;
|
||||
|
||||
allDocs.forEach((step) => {
|
||||
if (step.synthetics?.type === 'heartbeat/summary') {
|
||||
checkGroupsById[step.monitor.check_group].summaryDoc = step;
|
||||
}
|
||||
});
|
||||
|
||||
const checkGroups = Object.values(checkGroupsById);
|
||||
const finishedCheckGroups = checkGroups.filter((group) => !!group.summaryDoc);
|
||||
|
||||
if (finishedCheckGroups.length >= expectSummaryDocs) {
|
||||
clearInterval(refreshTimer);
|
||||
}
|
||||
|
||||
replaceCheckGroupResults(checkGroups);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [expectSummaryDocs, data, refreshTimer]);
|
||||
|
||||
// Loading steps for browser runs
|
||||
const checkGroupIds = checkGroupResults.map(({ checkGroupId }) => checkGroupId);
|
||||
const checkGroupCheckSum = checkGroupIds.reduce((acc, cur) => acc + cur, '');
|
||||
const { loading: stepLoadingInProgress } = useFetcher(() => {
|
||||
if (checkGroupIds.length && !skipDetails) {
|
||||
setCheckGroupResults((prevState) => {
|
||||
return prevState.map((result) => ({ ...result, stepsLoading: true }));
|
||||
});
|
||||
|
||||
return Promise.all(
|
||||
checkGroupIds.map((id) => {
|
||||
return fetchBrowserJourney({
|
||||
checkGroup: id,
|
||||
})
|
||||
.then((stepsData) => {
|
||||
updateCheckGroupResult(stepsData.checkGroup, {
|
||||
steps: stepsData.steps,
|
||||
completedSteps: stepsData.steps.filter(isStepEnd).length,
|
||||
});
|
||||
|
||||
return stepsData;
|
||||
})
|
||||
.finally(() => {
|
||||
updateCheckGroupResult(id, {
|
||||
stepsLoading: false,
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.resolve(null);
|
||||
}, [checkGroupCheckSum, setCheckGroupResults, lastRefresh]);
|
||||
|
||||
// Whenever a new found document is fetched, update lastUpdated
|
||||
useEffect(() => {
|
||||
const currentChecksum = getCheckGroupChecksum(checkGroupResults);
|
||||
if (checkGroupCheckSum !== lastUpdated.current.checksum) {
|
||||
// Mutating lastUpdated
|
||||
lastUpdated.current.checksum = currentChecksum;
|
||||
lastUpdated.current.time = Date.now();
|
||||
}
|
||||
}, [checkGroupResults, checkGroupCheckSum]);
|
||||
|
||||
const updateCheckGroupResult = (id: string, result: Partial<CheckGroupResult>) => {
|
||||
setCheckGroupResults((prevState) => {
|
||||
return prevState.map((r) => {
|
||||
if (id !== r.checkGroupId) {
|
||||
return r;
|
||||
}
|
||||
|
||||
return mergeCheckGroups(r, result);
|
||||
}) as CheckGroupResult[];
|
||||
});
|
||||
};
|
||||
|
||||
const replaceCheckGroupResults = (curCheckGroups: CheckGroupResult[]) => {
|
||||
const emptyCheckGroups = checkGroupResults.filter((group) =>
|
||||
group.checkGroupId.startsWith('placeholder-check-group')
|
||||
);
|
||||
|
||||
// Padding the collection with placeholders so that rows could be shown on UI with loading state
|
||||
const paddedCheckGroups =
|
||||
curCheckGroups.length < expectSummaryDocs
|
||||
? [
|
||||
...curCheckGroups,
|
||||
...emptyCheckGroups.slice(-1 * (expectSummaryDocs - curCheckGroups.length)),
|
||||
]
|
||||
: curCheckGroups;
|
||||
|
||||
setCheckGroupResults((prevCheckGroups) => {
|
||||
const newIds = paddedCheckGroups.map(({ checkGroupId }) => checkGroupId);
|
||||
const newById: Record<string, CheckGroupResult> = paddedCheckGroups.reduce(
|
||||
(acc, cur) => ({ ...acc, [cur.checkGroupId]: cur }),
|
||||
{}
|
||||
);
|
||||
const oldById: Record<string, CheckGroupResult> = prevCheckGroups.reduce(
|
||||
(acc, cur) => ({ ...acc, [cur.checkGroupId]: cur }),
|
||||
{}
|
||||
);
|
||||
|
||||
return newIds.map((id) => mergeCheckGroups(oldById[id], newById[id]));
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
data,
|
||||
summariesLoading,
|
||||
stepLoadingInProgress,
|
||||
expectedSummariesLoaded:
|
||||
checkGroupResults.filter(({ summaryDoc }) => !!summaryDoc).length >= expectSummaryDocs,
|
||||
checkGroupResults,
|
||||
lastUpdated: lastUpdated.current.time,
|
||||
};
|
||||
};
|
||||
|
||||
function mergeCheckGroups(prev: CheckGroupResult, curr: Partial<CheckGroupResult>) {
|
||||
// Once completed steps has been determined and shown, don't lower the number on UI due to re-fetch
|
||||
const completedSteps = curr.completedSteps
|
||||
? Math.max(prev?.completedSteps ?? 0, curr.completedSteps ?? 0)
|
||||
: prev?.completedSteps ?? 0;
|
||||
|
||||
let steps = curr.steps ?? [];
|
||||
if (steps.length === 0 && (prev?.steps ?? []).length > 0) {
|
||||
steps = prev.steps;
|
||||
}
|
||||
|
||||
return {
|
||||
...(prev ?? {}),
|
||||
...curr,
|
||||
steps,
|
||||
completedSteps,
|
||||
};
|
||||
}
|
||||
|
||||
function getCheckGroupChecksum(checkGroupResults: CheckGroupResult[]) {
|
||||
return checkGroupResults.reduce((acc, cur) => {
|
||||
return (
|
||||
acc + cur?.journeyDoc?._id ??
|
||||
'' + cur?.summaryDoc?._id ??
|
||||
'' + (cur?.steps ?? []).reduce((stepAcc, { _id }) => stepAcc + _id, '')
|
||||
);
|
||||
}, '');
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* 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 { useEffect, useMemo, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { kibanaService } from '../../../../../utils/kibana_service';
|
||||
import { Locations, ServiceLocationErrors } from '../../monitor_add_edit/types';
|
||||
|
||||
export function useRunOnceErrors({
|
||||
testRunId,
|
||||
serviceError,
|
||||
errors,
|
||||
locations,
|
||||
}: {
|
||||
testRunId: string;
|
||||
serviceError?: Error;
|
||||
errors: ServiceLocationErrors;
|
||||
locations: Locations;
|
||||
}) {
|
||||
const [locationErrors, setLocationErrors] = useState<ServiceLocationErrors>([]);
|
||||
const [runOnceServiceError, setRunOnceServiceError] = useState<Error | undefined | null>(null);
|
||||
const publicLocations = useMemo(
|
||||
() => (locations ?? []).filter((loc) => loc.isServiceManaged),
|
||||
[locations]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setLocationErrors([]);
|
||||
setRunOnceServiceError(null);
|
||||
}, [testRunId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (locationErrors.length || errors.length) {
|
||||
setLocationErrors(errors);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [errors]);
|
||||
|
||||
useEffect(() => {
|
||||
if (runOnceServiceError?.message !== serviceError?.message) {
|
||||
setRunOnceServiceError(serviceError);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [serviceError]);
|
||||
|
||||
const locationsById: Record<string, Locations[number]> = useMemo(
|
||||
() => (publicLocations as Locations).reduce((acc, cur) => ({ ...acc, [cur.id]: cur }), {}),
|
||||
[publicLocations]
|
||||
);
|
||||
|
||||
const expectPings =
|
||||
publicLocations.length - (locationErrors ?? []).filter(({ locationId }) => !!locationId).length;
|
||||
|
||||
const hasBlockingError =
|
||||
!!runOnceServiceError ||
|
||||
(locationErrors?.length && locationErrors?.length === publicLocations.length);
|
||||
|
||||
const errorMessages = useMemo(() => {
|
||||
if (hasBlockingError) {
|
||||
return [{ name: 'Error', message: PushErrorService, title: PushErrorLabel }];
|
||||
} else if (locationErrors?.length > 0) {
|
||||
// If only some of the locations were unsuccessful
|
||||
return locationErrors
|
||||
.map(({ locationId }) => locationsById[locationId])
|
||||
.filter((location) => !!location)
|
||||
.map((location) => ({
|
||||
name: 'Error',
|
||||
message: getLocationTestErrorLabel(location.label),
|
||||
title: RunErrorLabel,
|
||||
}));
|
||||
}
|
||||
|
||||
return [];
|
||||
}, [locationsById, locationErrors, hasBlockingError]);
|
||||
|
||||
useEffect(() => {
|
||||
errorMessages.forEach(
|
||||
({ name, message, title }: { name: string; message: string; title: string }) => {
|
||||
kibanaService.toasts.addError({ name, message }, { title });
|
||||
}
|
||||
);
|
||||
}, [errorMessages]);
|
||||
|
||||
return {
|
||||
expectPings,
|
||||
hasBlockingError,
|
||||
blockingErrorMessage: hasBlockingError ? PushErrorService : null,
|
||||
errorMessages,
|
||||
};
|
||||
}
|
||||
|
||||
const PushErrorLabel = i18n.translate('xpack.synthetics.testRun.pushErrorLabel', {
|
||||
defaultMessage: 'Push error',
|
||||
});
|
||||
|
||||
const RunErrorLabel = i18n.translate('xpack.synthetics.testRun.runErrorLabel', {
|
||||
defaultMessage: 'Error running test',
|
||||
});
|
||||
|
||||
const getLocationTestErrorLabel = (locationName: string) =>
|
||||
i18n.translate('xpack.synthetics.testRun.runErrorLocation', {
|
||||
defaultMessage: 'Failed to run monitor on location {locationName}.',
|
||||
values: { locationName },
|
||||
});
|
||||
|
||||
const PushErrorService = i18n.translate('xpack.synthetics.testRun.pushError', {
|
||||
defaultMessage: 'Failed to push the monitor to service.',
|
||||
});
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { createEsParams, useEsSearch } from '@kbn/observability-plugin/public';
|
||||
import { SUMMARY_FILTER } from '../../../../../../common/constants/client_defaults';
|
||||
import { Ping } from '../../../../../../common/runtime_types';
|
||||
import { SYNTHETICS_INDEX_PATTERN } from '../../../../../../common/constants';
|
||||
import { useTickTick } from './use_tick_tick';
|
||||
|
||||
export const useSimpleRunOnceMonitors = ({
|
||||
expectSummaryDocs,
|
||||
testRunId,
|
||||
}: {
|
||||
expectSummaryDocs: number;
|
||||
testRunId: string;
|
||||
}) => {
|
||||
const { refreshTimer, lastRefresh } = useTickTick(2 * 1000, false);
|
||||
|
||||
const { data, loading } = useEsSearch(
|
||||
createEsParams({
|
||||
index: SYNTHETICS_INDEX_PATTERN,
|
||||
body: {
|
||||
sort: [
|
||||
{
|
||||
'@timestamp': 'desc',
|
||||
},
|
||||
],
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
SUMMARY_FILTER,
|
||||
{
|
||||
term: {
|
||||
test_run_id: testRunId,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
size: 1000,
|
||||
}),
|
||||
[testRunId, lastRefresh],
|
||||
{ name: 'TestRunData' }
|
||||
);
|
||||
|
||||
const lastUpdated = useRef<{ checksum: string; time: number }>({
|
||||
checksum: '',
|
||||
time: Date.now(),
|
||||
});
|
||||
|
||||
return useMemo(() => {
|
||||
const docs = data?.hits.hits ?? [];
|
||||
|
||||
// Whenever a new found document is fetched, update lastUpdated
|
||||
const docsChecksum = docs
|
||||
.map(({ _id }: { _id: string }) => _id)
|
||||
.reduce((acc, cur) => acc + cur, '');
|
||||
if (docsChecksum !== lastUpdated.current.checksum) {
|
||||
// Mutating lastUpdated
|
||||
lastUpdated.current.checksum = docsChecksum;
|
||||
lastUpdated.current.time = Date.now();
|
||||
}
|
||||
|
||||
if (docs.length > 0) {
|
||||
if (docs.length >= expectSummaryDocs) {
|
||||
clearInterval(refreshTimer);
|
||||
}
|
||||
|
||||
return {
|
||||
data,
|
||||
loading,
|
||||
summaryDocs: docs.map((doc) => ({
|
||||
...(doc._source as Ping),
|
||||
timestamp: (doc._source as Record<string, string>)?.['@timestamp'],
|
||||
docId: doc._id,
|
||||
})),
|
||||
lastUpdated: lastUpdated.current.time,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
data,
|
||||
loading,
|
||||
summaryDocs: null,
|
||||
lastUpdated: lastUpdated.current.time,
|
||||
};
|
||||
}, [expectSummaryDocs, data, loading, refreshTimer]);
|
||||
};
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 { useDispatch, useSelector } from 'react-redux';
|
||||
import { useRouteMatch } from 'react-router-dom';
|
||||
import { useEffect } from 'react';
|
||||
import { OVERVIEW_ROUTE } from '../../../../../../common/constants';
|
||||
import { hideTestNowFlyoutAction, testNowRunsSelector } from '../../../state/manual_test_runs';
|
||||
|
||||
export const useTestFlyoutOpen = () => {
|
||||
const testNowRuns = useSelector(testNowRunsSelector);
|
||||
|
||||
const isOverview = useRouteMatch({
|
||||
path: [OVERVIEW_ROUTE],
|
||||
});
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const flyoutTestOpen = Object.values(testNowRuns).find((value) => {
|
||||
return value.isTestNowFlyoutOpen;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOverview?.isExact && flyoutTestOpen) {
|
||||
dispatch(hideTestNowFlyoutAction());
|
||||
}
|
||||
});
|
||||
|
||||
return flyoutTestOpen;
|
||||
};
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 { useEffect, useState, useContext } from 'react';
|
||||
import { SyntheticsRefreshContext } from '../../../contexts';
|
||||
|
||||
export function useTickTick(interval?: number, refresh = true) {
|
||||
const { refreshApp } = useContext(SyntheticsRefreshContext);
|
||||
|
||||
const [nextTick, setNextTick] = useState(Date.now());
|
||||
|
||||
const [tickTick] = useState<NodeJS.Timer>(() =>
|
||||
setInterval(() => {
|
||||
if (refresh) {
|
||||
refreshApp();
|
||||
}
|
||||
setNextTick(Date.now());
|
||||
}, interval ?? 5 * 1000)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearInterval(tickTick);
|
||||
};
|
||||
}, [tickTick]);
|
||||
|
||||
return { refreshTimer: tickTick, lastRefresh: nextTick };
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* 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, { Fragment, useEffect } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useBrowserRunOnceMonitors } from '../hooks/use_browser_run_once_monitors';
|
||||
|
||||
interface Props {
|
||||
testRunId: string;
|
||||
expectPings: number;
|
||||
onDone: (testRunId: string) => void;
|
||||
onProgress: (message: string) => void;
|
||||
}
|
||||
export const BrowserTestRunResult = ({ expectPings, onDone, testRunId, onProgress }: Props) => {
|
||||
const { summariesLoading, expectedSummariesLoaded, stepLoadingInProgress, checkGroupResults } =
|
||||
useBrowserRunOnceMonitors({
|
||||
testRunId,
|
||||
expectSummaryDocs: expectPings,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (expectedSummariesLoaded) {
|
||||
onDone(testRunId);
|
||||
}
|
||||
}, [onDone, expectedSummariesLoaded, testRunId]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{checkGroupResults.map((checkGroupResult) => {
|
||||
const { checkGroupId, journeyStarted, summaryDoc, stepsLoading, steps, completedSteps } =
|
||||
checkGroupResult;
|
||||
const isStepsLoading = !summariesLoading && journeyStarted && summaryDoc && stepsLoading;
|
||||
const isStepsLoadingFailed =
|
||||
summaryDoc && !summariesLoading && !stepLoadingInProgress && steps.length === 0;
|
||||
|
||||
if (completedSteps > 0) {
|
||||
onProgress(
|
||||
i18n.translate('xpack.synthetics.monitorManagement.stepCompleted', {
|
||||
defaultMessage:
|
||||
'{stepCount, number} {stepCount, plural, one {step} other {steps}} completed',
|
||||
values: {
|
||||
stepCount: completedSteps ?? 0,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (isStepsLoading) {
|
||||
onProgress(LOADING_STEPS);
|
||||
}
|
||||
|
||||
if (isStepsLoadingFailed) {
|
||||
// TODO: Add error toast
|
||||
onProgress(summaryDoc?.error?.message ?? FAILED_TO_RUN);
|
||||
// <EuiText color="danger">{summaryDoc?.error?.message ?? FAILED_TO_RUN}</EuiText>
|
||||
}
|
||||
if (
|
||||
isStepsLoadingFailed &&
|
||||
summaryDoc?.error?.message?.includes('journey did not finish executing')
|
||||
) {
|
||||
// TODO: Add error toast
|
||||
// <StdErrorLogs checkGroup={summaryDoc.monitor.check_group} hideTitle={true} />;
|
||||
}
|
||||
|
||||
return <Fragment key={'accordion-' + checkGroupId} />;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const FAILED_TO_RUN = i18n.translate('xpack.synthetics.monitorManagement.failedRun', {
|
||||
defaultMessage: 'Failed to run steps',
|
||||
});
|
||||
|
||||
const LOADING_STEPS = i18n.translate('xpack.synthetics.monitorManagement.loadingSteps', {
|
||||
defaultMessage: 'Loading steps...',
|
||||
});
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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, { Fragment } from 'react';
|
||||
import { useRunOnceErrors } from '../hooks/use_run_once_errors';
|
||||
import { ManualTestRun } from '../../../state/manual_test_runs';
|
||||
import { BrowserTestRunResult } from './browser_test_results';
|
||||
import { SimpleTestResults } from './simple_test_results';
|
||||
import { Locations } from '../../../../../../common/runtime_types';
|
||||
|
||||
export function ManualTestRunMode({
|
||||
manualTestRun,
|
||||
onDone,
|
||||
}: {
|
||||
manualTestRun: ManualTestRun;
|
||||
onDone: (testRunId: string) => void;
|
||||
}) {
|
||||
const { expectPings } = useRunOnceErrors({
|
||||
testRunId: manualTestRun.testRunId!,
|
||||
locations: (manualTestRun.monitor!.locations ?? []) as Locations,
|
||||
errors: manualTestRun.errors ?? [],
|
||||
});
|
||||
|
||||
if (!manualTestRun.testRunId || !manualTestRun.monitor) return null;
|
||||
|
||||
const isBrowserMonitor = manualTestRun.monitor.type === 'browser';
|
||||
|
||||
return (
|
||||
<Fragment key={manualTestRun.testRunId}>
|
||||
{isBrowserMonitor ? (
|
||||
<BrowserTestRunResult
|
||||
expectPings={expectPings}
|
||||
onDone={onDone}
|
||||
testRunId={manualTestRun.testRunId}
|
||||
onProgress={() => {}}
|
||||
/>
|
||||
) : (
|
||||
<SimpleTestResults
|
||||
expectPings={expectPings}
|
||||
onDone={onDone}
|
||||
testRunId={manualTestRun.testRunId}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React, { useEffect } from 'react';
|
||||
import { useSimpleRunOnceMonitors } from '../hooks/use_simple_run_once_monitors';
|
||||
|
||||
interface Props {
|
||||
testRunId: string;
|
||||
expectPings: number;
|
||||
onDone: (testRunId: string) => void;
|
||||
}
|
||||
export function SimpleTestResults({ testRunId, expectPings, onDone }: Props) {
|
||||
const { summaryDocs } = useSimpleRunOnceMonitors({
|
||||
testRunId,
|
||||
expectSummaryDocs: expectPings,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (summaryDocs) {
|
||||
if (summaryDocs.length >= expectPings) {
|
||||
onDone(testRunId);
|
||||
}
|
||||
}
|
||||
}, [testRunId, expectPings, summaryDocs, onDone]);
|
||||
|
||||
return <></>;
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { EuiButtonIcon } from '@elastic/eui';
|
||||
import { Ping } from '../../../../../../../../common/runtime_types';
|
||||
import { PingListExpandedRowComponent } from '../expanded_row';
|
||||
|
||||
export const toggleDetails = (
|
||||
ping: Ping,
|
||||
expandedRows: Record<string, JSX.Element>,
|
||||
setExpandedRows: (update: Record<string, JSX.Element>) => any
|
||||
) => {
|
||||
// If already expanded, collapse
|
||||
if (expandedRows[ping.docId]) {
|
||||
delete expandedRows[ping.docId];
|
||||
setExpandedRows({ ...expandedRows });
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise expand this row
|
||||
setExpandedRows({
|
||||
...expandedRows,
|
||||
[ping.docId]: <PingListExpandedRowComponent ping={ping} />,
|
||||
});
|
||||
};
|
||||
|
||||
export function rowShouldExpand(item: Ping) {
|
||||
const errorPresent = !!item.error;
|
||||
const httpBodyPresent = item.http?.response?.body?.bytes ?? 0 > 0;
|
||||
return (
|
||||
errorPresent ||
|
||||
httpBodyPresent ||
|
||||
item?.http?.response?.headers ||
|
||||
item?.http?.response?.redirects
|
||||
);
|
||||
}
|
||||
|
||||
interface Props {
|
||||
item: Ping;
|
||||
expandedRows: Record<string, JSX.Element>;
|
||||
setExpandedRows: (val: Record<string, JSX.Element>) => void;
|
||||
}
|
||||
export const ExpandRowColumn = ({ item, expandedRows, setExpandedRows }: Props) => {
|
||||
return (
|
||||
<EuiButtonIcon
|
||||
data-test-subj="uptimePingListExpandBtn"
|
||||
onClick={() => toggleDetails(item, expandedRows, setExpandedRows)}
|
||||
isDisabled={!rowShouldExpand(item)}
|
||||
aria-label={
|
||||
expandedRows[item.docId]
|
||||
? i18n.translate('xpack.synthetics.pingList.collapseRow', {
|
||||
defaultMessage: 'Collapse',
|
||||
})
|
||||
: i18n.translate('xpack.synthetics.pingList.expandRow', { defaultMessage: 'Expand' })
|
||||
}
|
||||
iconType={expandedRows[item.docId] ? 'arrowUp' : 'arrowDown'}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Ping } from '../../../../../../../../common/runtime_types';
|
||||
|
||||
const StyledSpan = styled.span`
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
errorType: string;
|
||||
ping: Ping;
|
||||
}
|
||||
|
||||
export const PingErrorCol = ({ errorType, ping }: Props) => {
|
||||
if (!errorType) {
|
||||
return <>--</>;
|
||||
}
|
||||
return (
|
||||
<StyledSpan>
|
||||
{errorType}:{ping.error?.message}
|
||||
</StyledSpan>
|
||||
);
|
||||
};
|
|
@ -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 { i18n } from '@kbn/i18n';
|
||||
import moment from 'moment';
|
||||
import { EuiBadge, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import { STATUS } from '../../../../../../../../common/constants';
|
||||
import { Ping } from '../../../../../../../../common/runtime_types';
|
||||
import {
|
||||
STATUS_DOWN_LABEL,
|
||||
STATUS_UP_LABEL,
|
||||
} from '../../../../../../../../common/translations/translations';
|
||||
|
||||
interface Props {
|
||||
pingStatus: string;
|
||||
item: Ping;
|
||||
}
|
||||
|
||||
const getPingStatusLabel = (status: string) => {
|
||||
return status === 'up' ? STATUS_UP_LABEL : STATUS_DOWN_LABEL;
|
||||
};
|
||||
|
||||
export const PingStatusColumn = ({ pingStatus, item }: Props) => {
|
||||
const timeStamp = moment(item.timestamp);
|
||||
|
||||
let checkedTime = '';
|
||||
|
||||
if (moment().diff(timeStamp, 'd') > 1) {
|
||||
checkedTime = timeStamp.format('ll LTS');
|
||||
} else {
|
||||
checkedTime = timeStamp.format('LTS');
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-test-subj={`xpack.synthetics.pingList.ping-${item.docId}`}>
|
||||
<EuiBadge className="eui-textCenter" color={pingStatus === STATUS.UP ? 'success' : 'danger'}>
|
||||
{getPingStatusLabel(pingStatus)}
|
||||
</EuiBadge>
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiText size="xs" color="subdued">
|
||||
{i18n.translate('xpack.synthetics.pingList.recencyMessage', {
|
||||
values: { fromNow: checkedTime },
|
||||
defaultMessage: 'Checked {fromNow}',
|
||||
description:
|
||||
'A string used to inform our users how long ago Heartbeat pinged the selected host.',
|
||||
})}
|
||||
</EuiText>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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 styled from 'styled-components';
|
||||
import { EuiBadge } from '@elastic/eui';
|
||||
|
||||
const SpanWithMargin = styled.span`
|
||||
margin-right: 16px;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
statusCode: string;
|
||||
}
|
||||
export const ResponseCodeColumn = ({ statusCode }: Props) => {
|
||||
return (
|
||||
<SpanWithMargin>
|
||||
{statusCode ? <EuiBadge data-test-subj="pingResponseCode">{statusCode}</EuiBadge> : '--'}
|
||||
</SpanWithMargin>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// @ts-ignore formatNumber
|
||||
import { formatNumber } from '@elastic/eui/lib/services/format';
|
||||
import {
|
||||
EuiCallOut,
|
||||
EuiCodeBlock,
|
||||
EuiDescriptionList,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { HttpResponseBody, Ping } from '../../../../../../../common/runtime_types';
|
||||
import { PingRedirects } from './ping_redirects';
|
||||
import { PingHeaders } from './headers';
|
||||
|
||||
interface Props {
|
||||
ping: Ping;
|
||||
}
|
||||
|
||||
const BodyDescription = ({ body }: { body: HttpResponseBody }) => {
|
||||
const contentBytes = body.content_bytes || 0;
|
||||
const bodyBytes = body.bytes || 0;
|
||||
|
||||
const truncatedText =
|
||||
contentBytes > 0 && contentBytes < bodyBytes
|
||||
? i18n.translate('xpack.synthetics.pingList.expandedRow.truncated', {
|
||||
defaultMessage: 'Showing first {contentBytes} bytes.',
|
||||
values: { contentBytes },
|
||||
})
|
||||
: null;
|
||||
const bodySizeText =
|
||||
bodyBytes > 0
|
||||
? i18n.translate('xpack.synthetics.pingList.expandedRow.bodySize', {
|
||||
defaultMessage: 'Body size is {bodyBytes}.',
|
||||
values: { bodyBytes: formatNumber(bodyBytes, '0b') },
|
||||
})
|
||||
: null;
|
||||
const combinedText = [truncatedText, bodySizeText].filter((s) => s).join(' ');
|
||||
|
||||
return <EuiText>{combinedText}</EuiText>;
|
||||
};
|
||||
|
||||
const BodyExcerpt = ({ content }: { content: string }) =>
|
||||
content ? <EuiCodeBlock overflowHeight={250}>{content}</EuiCodeBlock> : null;
|
||||
|
||||
export const PingListExpandedRowComponent = ({ ping }: Props) => {
|
||||
const listItems = [];
|
||||
|
||||
// Show the error block
|
||||
if (ping.error) {
|
||||
listItems.push({
|
||||
title: i18n.translate('xpack.synthetics.pingList.expandedRow.error', {
|
||||
defaultMessage: 'Error',
|
||||
}),
|
||||
description: <EuiText>{ping.error.message}</EuiText>,
|
||||
});
|
||||
}
|
||||
|
||||
// Show the body, if present
|
||||
if (ping.http?.response?.body) {
|
||||
const body = ping.http.response.body;
|
||||
|
||||
listItems.push({
|
||||
title: i18n.translate('xpack.synthetics.pingList.expandedRow.response_body', {
|
||||
defaultMessage: 'Response Body',
|
||||
}),
|
||||
description: (
|
||||
<>
|
||||
<BodyDescription body={body} />
|
||||
<EuiSpacer size={'s'} />
|
||||
{body.content ? (
|
||||
<BodyExcerpt content={body.content || ''} />
|
||||
) : (
|
||||
<EuiText>
|
||||
<FormattedMessage
|
||||
id="xpack.synthetics.testResults.expandedRow.response_body.notRecorded"
|
||||
defaultMessage='Body not recorded. Set index response body option to "On Always" in advanced options of monitor configuration to record body.'
|
||||
/>
|
||||
</EuiText>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
});
|
||||
}
|
||||
return (
|
||||
<EuiFlexGroup direction="column">
|
||||
{ping?.http?.response?.redirects && (
|
||||
<EuiFlexItem>
|
||||
<PingRedirects monitorStatus={ping} showTitle={true} />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{ping?.http?.response?.headers && (
|
||||
<EuiFlexItem>
|
||||
<PingHeaders headers={ping?.http?.response?.headers} />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem>
|
||||
<EuiCallOut color={ping?.error ? 'danger' : 'primary'}>
|
||||
<EuiDescriptionList listItems={listItems} />
|
||||
</EuiCallOut>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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 { EuiAccordion, EuiDescriptionList, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { PingHeaders as HeadersProp } from '../../../../../../../common/runtime_types';
|
||||
|
||||
interface Props {
|
||||
headers: HeadersProp;
|
||||
}
|
||||
|
||||
export const PingHeaders = ({ headers }: Props) => {
|
||||
const headersList = Object.keys(headers)
|
||||
.sort()
|
||||
.map((header) => ({
|
||||
title: header,
|
||||
description: headers[header],
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiAccordion
|
||||
id="responseHeaderAccord"
|
||||
buttonContent={
|
||||
<EuiTitle size="xs">
|
||||
<h3>
|
||||
{i18n.translate('xpack.synthetics.pingList.headers.title', {
|
||||
defaultMessage: 'Response headers',
|
||||
})}
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiDescriptionList
|
||||
titleProps={{ style: { width: '30%', paddingLeft: 30 } }}
|
||||
compressed={true}
|
||||
type="responsiveColumn"
|
||||
listItems={headersList}
|
||||
/>
|
||||
</EuiAccordion>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { EuiBasicTable } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import styled from 'styled-components';
|
||||
import { formatDuration } from '../../../../utils/formatting';
|
||||
import { Ping } from '../../../../../../../common/runtime_types';
|
||||
import * as I18LABELS from './translations';
|
||||
import { PingStatusColumn } from './columns/ping_status';
|
||||
import { ERROR_LABEL, LOCATION_LABEL, RES_CODE_LABEL } from './translations';
|
||||
import { PingErrorCol } from './columns/ping_error';
|
||||
import { ResponseCodeColumn } from './columns/response_code';
|
||||
import { ExpandRowColumn } from './columns/expand_row';
|
||||
|
||||
interface Props {
|
||||
loading?: boolean;
|
||||
pings: Ping[];
|
||||
error?: Error;
|
||||
onChange?: (criteria: any) => void;
|
||||
}
|
||||
|
||||
export function PingListTable({ loading, error, pings, onChange }: Props) {
|
||||
const [expandedRows, setExpandedRows] = useState<Record<string, JSX.Element>>({});
|
||||
|
||||
const expandedIdsToRemove = JSON.stringify(
|
||||
Object.keys(expandedRows).filter((e) => !pings.some(({ docId }) => docId === e))
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const parsed = JSON.parse(expandedIdsToRemove);
|
||||
if (parsed.length) {
|
||||
parsed.forEach((docId: string) => {
|
||||
delete expandedRows[docId];
|
||||
});
|
||||
setExpandedRows(expandedRows);
|
||||
}
|
||||
}, [expandedIdsToRemove, expandedRows]);
|
||||
|
||||
const hasStatus = pings.reduce(
|
||||
(hasHttpStatus: boolean, currentPing) =>
|
||||
hasHttpStatus || !!currentPing.http?.response?.status_code,
|
||||
false
|
||||
);
|
||||
|
||||
const hasError = pings.reduce(
|
||||
(errorType: boolean, currentPing) => errorType || !!currentPing.error?.type,
|
||||
false
|
||||
);
|
||||
|
||||
const columns: any[] = [
|
||||
{
|
||||
field: 'monitor.status',
|
||||
name: I18LABELS.STATUS_LABEL,
|
||||
render: (pingStatus: string, item: Ping) => (
|
||||
<PingStatusColumn pingStatus={pingStatus} item={item} />
|
||||
),
|
||||
},
|
||||
{
|
||||
align: 'left',
|
||||
field: 'observer.geo.name',
|
||||
name: LOCATION_LABEL,
|
||||
},
|
||||
{
|
||||
align: 'right',
|
||||
field: 'monitor.ip',
|
||||
name: i18n.translate('xpack.synthetics.pingList.ipAddressColumnLabel', {
|
||||
defaultMessage: 'IP',
|
||||
}),
|
||||
},
|
||||
{
|
||||
align: 'center',
|
||||
field: 'monitor.duration.us',
|
||||
name: i18n.translate('xpack.synthetics.pingList.durationMsColumnLabel', {
|
||||
defaultMessage: 'Duration',
|
||||
}),
|
||||
render: (duration: number | null) =>
|
||||
duration ? (
|
||||
formatDuration(duration)
|
||||
) : (
|
||||
<span data-test-subj="ping-list-duration-unavailable-tool-tip">{'--'}</span>
|
||||
),
|
||||
},
|
||||
...(hasError
|
||||
? [
|
||||
{
|
||||
field: 'error.type',
|
||||
name: ERROR_LABEL,
|
||||
width: '30%',
|
||||
render: (errorType: string, item: Ping) => (
|
||||
<PingErrorCol ping={item} errorType={errorType} />
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
// Only add this column is there is any status present in list
|
||||
...(hasStatus
|
||||
? [
|
||||
{
|
||||
field: 'http.response.status_code',
|
||||
align: 'right',
|
||||
name: <SpanWithMargin>{RES_CODE_LABEL}</SpanWithMargin>,
|
||||
render: (statusCode: string) => <ResponseCodeColumn statusCode={statusCode} />,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
align: 'right',
|
||||
width: '24px',
|
||||
isExpander: true,
|
||||
render: (item: Ping) => (
|
||||
<ExpandRowColumn
|
||||
item={item}
|
||||
expandedRows={expandedRows}
|
||||
setExpandedRows={setExpandedRows}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<EuiBasicTable
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
error={error?.message}
|
||||
isExpandable={true}
|
||||
hasActions={true}
|
||||
items={pings}
|
||||
itemId="docId"
|
||||
itemIdToExpandedRowMap={expandedRows}
|
||||
noItemsMessage={
|
||||
loading
|
||||
? i18n.translate('xpack.synthetics.pingList.pingsLoadingMesssage', {
|
||||
defaultMessage: 'Loading history...',
|
||||
})
|
||||
: i18n.translate('xpack.synthetics.pingList.pingsUnavailableMessage', {
|
||||
defaultMessage: 'No history found',
|
||||
})
|
||||
}
|
||||
tableLayout={'auto'}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const SpanWithMargin = styled.span`
|
||||
margin-right: 16px;
|
||||
`;
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import styled from 'styled-components';
|
||||
import { EuiListGroup, EuiListGroupItemProps, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import { Ping } from '../../../../../../../common/runtime_types';
|
||||
|
||||
const ListGroup = styled(EuiListGroup)`
|
||||
&&& {
|
||||
a {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
monitorStatus: Ping | null;
|
||||
showTitle?: boolean;
|
||||
}
|
||||
|
||||
export const PingRedirects: React.FC<Props> = ({ monitorStatus, showTitle }) => {
|
||||
const monitorUrl = monitorStatus?.url?.full;
|
||||
|
||||
const list = monitorStatus?.http?.response?.redirects;
|
||||
|
||||
const listOfRedirects: EuiListGroupItemProps[] = [
|
||||
{
|
||||
label: monitorUrl,
|
||||
href: monitorUrl,
|
||||
iconType: 'globe',
|
||||
size: 's',
|
||||
target: '_blank',
|
||||
extraAction: {
|
||||
color: 'text',
|
||||
iconType: 'popout',
|
||||
iconSize: 's',
|
||||
alwaysShow: true,
|
||||
'aria-label': i18n.translate('xpack.synthetics.monitorList.redirects.openWindow', {
|
||||
defaultMessage: 'Link will open in new window.',
|
||||
}),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
(list ?? []).forEach((url: string) => {
|
||||
listOfRedirects.push({
|
||||
label: url,
|
||||
href: url,
|
||||
iconType: 'sortDown',
|
||||
size: 's',
|
||||
target: '_blank',
|
||||
extraAction: {
|
||||
color: 'text',
|
||||
iconType: 'popout',
|
||||
iconSize: 's',
|
||||
'aria-label': i18n.translate('xpack.synthetics.monitorList.redirects.openWindow', {
|
||||
defaultMessage: 'Link will open in new window.',
|
||||
}),
|
||||
alwaysShow: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const Panel = showTitle ? EuiPanel : 'div';
|
||||
|
||||
return list ? (
|
||||
<Panel data-test-subj="uptimeMonitorPingListRedirectInfo">
|
||||
{showTitle && (
|
||||
<EuiText size="xs">
|
||||
<h3>
|
||||
{i18n.translate('xpack.synthetics.monitorList.redirects.title', {
|
||||
defaultMessage: 'Redirects',
|
||||
})}
|
||||
</h3>
|
||||
</EuiText>
|
||||
)}
|
||||
<EuiSpacer size="xs" />
|
||||
{
|
||||
<EuiText>
|
||||
{i18n.translate('xpack.synthetics.monitorList.redirects.description', {
|
||||
defaultMessage: 'Heartbeat followed {number} redirects while executing ping.',
|
||||
values: {
|
||||
number: list?.length ?? 0,
|
||||
},
|
||||
})}
|
||||
</EuiText>
|
||||
}
|
||||
<EuiSpacer size="s" />
|
||||
<ListGroup gutterSize={'none'} listItems={listOfRedirects} />
|
||||
</Panel>
|
||||
) : null;
|
||||
};
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const STATUS_LABEL = i18n.translate('xpack.synthetics.pingList.statusColumnLabel', {
|
||||
defaultMessage: 'Status',
|
||||
});
|
||||
|
||||
export const RES_CODE_LABEL = i18n.translate('xpack.synthetics.pingList.responseCodeColumnLabel', {
|
||||
defaultMessage: 'Response code',
|
||||
});
|
||||
export const ERROR_TYPE_LABEL = i18n.translate('xpack.synthetics.pingList.errorTypeColumnLabel', {
|
||||
defaultMessage: 'Error type',
|
||||
});
|
||||
export const ERROR_LABEL = i18n.translate('xpack.synthetics.pingList.errorColumnLabel', {
|
||||
defaultMessage: 'Error',
|
||||
});
|
||||
|
||||
export const LOCATION_LABEL = i18n.translate('xpack.synthetics.pingList.locationNameColumnLabel', {
|
||||
defaultMessage: 'Location',
|
||||
});
|
||||
|
||||
export const TIMESTAMP_LABEL = i18n.translate('xpack.synthetics.pingList.timestampColumnLabel', {
|
||||
defaultMessage: 'Timestamp',
|
||||
});
|
|
@ -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 React, { useEffect, useState } from 'react';
|
||||
import { Ping } from '../../../../../../common/runtime_types';
|
||||
import { useSimpleRunOnceMonitors } from '../hooks/use_simple_run_once_monitors';
|
||||
import { TestResultHeader } from '../test_result_header';
|
||||
import { PingListTable } from './ping_list/ping_list_table';
|
||||
|
||||
interface Props {
|
||||
testRunId: string;
|
||||
expectPings: number;
|
||||
onDone: (testRunId: string) => void;
|
||||
}
|
||||
export function SimpleTestResults({ testRunId, expectPings, onDone }: Props) {
|
||||
const [summaryDocsCache, setSummaryDocsCache] = useState<Ping[]>([]);
|
||||
const { summaryDocs, loading } = useSimpleRunOnceMonitors({
|
||||
testRunId,
|
||||
expectSummaryDocs: expectPings,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (summaryDocs) {
|
||||
setSummaryDocsCache((prevState: Ping[]) => {
|
||||
const prevById: Record<string, Ping> = prevState.reduce(
|
||||
(acc, cur) => ({ ...acc, [cur.docId]: cur }),
|
||||
{}
|
||||
);
|
||||
return summaryDocs.map((updatedDoc) => ({
|
||||
...updatedDoc,
|
||||
...(prevById[updatedDoc.docId] ?? {}),
|
||||
}));
|
||||
});
|
||||
|
||||
if (summaryDocs.length >= expectPings) {
|
||||
onDone(testRunId);
|
||||
}
|
||||
}
|
||||
}, [testRunId, expectPings, summaryDocs, onDone]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TestResultHeader
|
||||
summaryDocs={summaryDocsCache}
|
||||
isCompleted={Boolean(summaryDocs && summaryDocs.length >= expectPings)}
|
||||
/>
|
||||
{summaryDocs && <PingListTable pings={summaryDocsCache} loading={loading} />}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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 React, { useEffect } from 'react';
|
||||
import { EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer } from '@elastic/eui';
|
||||
import { useRunOnceErrors } from './hooks/use_run_once_errors';
|
||||
import { BrowserTestRunResult } from './browser/browser_test_results';
|
||||
import { TestRun } from './test_now_mode_flyout';
|
||||
import { SimpleTestResults } from './simple/simple_test_results';
|
||||
import { Locations, ServiceLocationErrors } from '../../../../../common/runtime_types';
|
||||
|
||||
export function TestNowMode({
|
||||
testRun,
|
||||
onDone,
|
||||
isPushing,
|
||||
serviceError,
|
||||
errors,
|
||||
}: {
|
||||
serviceError?: Error;
|
||||
errors: ServiceLocationErrors;
|
||||
isPushing: boolean;
|
||||
testRun: TestRun;
|
||||
onDone: (testRunId: string) => void;
|
||||
}) {
|
||||
const { hasBlockingError, blockingErrorMessage, expectPings } = useRunOnceErrors({
|
||||
testRunId: testRun.id,
|
||||
serviceError,
|
||||
errors: errors ?? [],
|
||||
locations: (testRun.monitor.locations ?? []) as Locations,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPushing && (!testRun.id || hasBlockingError)) {
|
||||
onDone(testRun.id);
|
||||
}
|
||||
// we don't need onDone as a dependency since it's a function
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [testRun.id, hasBlockingError, isPushing]);
|
||||
|
||||
const isBrowserMonitor = testRun.monitor.type === 'browser';
|
||||
|
||||
return (
|
||||
<EuiPanel color="subdued" hasBorder={true}>
|
||||
{(hasBlockingError && !isPushing && (
|
||||
<EuiCallOut title={blockingErrorMessage} color="danger" iconType="alert" />
|
||||
)) ||
|
||||
null}
|
||||
|
||||
{testRun && !hasBlockingError && !isPushing && (
|
||||
<EuiFlexGroup direction="column" gutterSize="xs">
|
||||
<EuiFlexItem key={testRun.id}>
|
||||
{isBrowserMonitor ? (
|
||||
<BrowserTestRunResult
|
||||
expectPings={expectPings}
|
||||
onDone={onDone}
|
||||
testRunId={testRun.id}
|
||||
/>
|
||||
) : (
|
||||
<SimpleTestResults expectPings={expectPings} onDone={onDone} testRunId={testRun.id} />
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
<EuiSpacer size="xs" />
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiCallOut,
|
||||
EuiErrorBoundary,
|
||||
EuiFlyout,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutFooter,
|
||||
EuiFlyoutHeader,
|
||||
EuiLoadingSpinner,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { LoadingState } from '../monitors_page/overview/overview/monitor_detail_flyout';
|
||||
import { MonitorFields, ServiceLocationErrors } from '../../../../../common/runtime_types';
|
||||
import { TestNowMode } from './test_now_mode';
|
||||
|
||||
export interface TestRun {
|
||||
id: string;
|
||||
monitor: MonitorFields;
|
||||
}
|
||||
|
||||
export function TestNowModeFlyout({
|
||||
testRun,
|
||||
onClose,
|
||||
onDone,
|
||||
inProgress,
|
||||
isPushing,
|
||||
errors,
|
||||
serviceError,
|
||||
}: {
|
||||
serviceError?: Error;
|
||||
errors: ServiceLocationErrors;
|
||||
testRun?: TestRun;
|
||||
inProgress: boolean;
|
||||
isPushing: boolean;
|
||||
onClose: () => void;
|
||||
onDone: (testRunId: string) => void;
|
||||
}) {
|
||||
const flyout = (
|
||||
<EuiFlyout
|
||||
type="push"
|
||||
size="m"
|
||||
paddingSize="m"
|
||||
maxWidth="44%"
|
||||
aria-labelledby={TEST_RESULT}
|
||||
onClose={onClose}
|
||||
>
|
||||
<EuiFlyoutHeader>
|
||||
<EuiTitle size="m">
|
||||
<h2>{TEST_RESULTS}</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<EuiErrorBoundary>
|
||||
{isPushing && (
|
||||
<EuiCallOut color="primary">
|
||||
{PushingLabel} <EuiLoadingSpinner />
|
||||
</EuiCallOut>
|
||||
)}
|
||||
{testRun ? (
|
||||
<TestNowMode
|
||||
isPushing={isPushing}
|
||||
errors={errors}
|
||||
serviceError={serviceError}
|
||||
testRun={testRun}
|
||||
onDone={onDone}
|
||||
/>
|
||||
) : (
|
||||
!isPushing && <LoadingState />
|
||||
)}
|
||||
</EuiErrorBoundary>
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiButtonEmpty iconType="cross" onClick={onClose} flush="left">
|
||||
{CLOSE_LABEL}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
);
|
||||
|
||||
return <>{(testRun || inProgress) && <EuiErrorBoundary>{flyout}</EuiErrorBoundary>}</>;
|
||||
}
|
||||
|
||||
const TEST_RESULT = i18n.translate('xpack.synthetics.monitorManagement.testResult', {
|
||||
defaultMessage: 'Test result',
|
||||
});
|
||||
|
||||
const TEST_RESULTS = i18n.translate('xpack.synthetics.monitorManagement.testResults', {
|
||||
defaultMessage: 'Test results',
|
||||
});
|
||||
|
||||
const CLOSE_LABEL = i18n.translate('xpack.synthetics.monitorManagement.closeButtonLabel', {
|
||||
defaultMessage: 'Close',
|
||||
});
|
||||
|
||||
const PushingLabel = i18n.translate('xpack.synthetics.testRun.pushing.description', {
|
||||
defaultMessage: 'Pushing the monitor to service...',
|
||||
});
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* 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, { useCallback } from 'react';
|
||||
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useTestFlyoutOpen } from './hooks/use_test_flyout_open';
|
||||
import { TestNowModeFlyout } from './test_now_mode_flyout';
|
||||
import { ManualTestRunMode } from './manual_test_run_mode/manual_test_run_mode';
|
||||
import { useSyntheticsRefreshContext } from '../../contexts';
|
||||
import {
|
||||
manualTestRunUpdateAction,
|
||||
testNowRunsSelector,
|
||||
TestRunStatus,
|
||||
} from '../../state/manual_test_runs';
|
||||
import { MonitorFields } from '../../../../../common/runtime_types';
|
||||
|
||||
export interface TestRun {
|
||||
id: string;
|
||||
monitor: MonitorFields;
|
||||
runOnceMode: boolean;
|
||||
}
|
||||
|
||||
export function TestNowModeFlyoutContainer() {
|
||||
const dispatch = useDispatch();
|
||||
const testNowRuns = useSelector(testNowRunsSelector);
|
||||
const { refreshApp } = useSyntheticsRefreshContext();
|
||||
|
||||
const flyoutOpenTestRun = useTestFlyoutOpen();
|
||||
|
||||
const onDone = useCallback(
|
||||
(testRunId) => {
|
||||
dispatch(
|
||||
manualTestRunUpdateAction({
|
||||
testRunId,
|
||||
status: TestRunStatus.COMPLETED,
|
||||
})
|
||||
);
|
||||
refreshApp();
|
||||
},
|
||||
[dispatch, refreshApp]
|
||||
);
|
||||
|
||||
const handleFlyoutClose = useCallback(
|
||||
(testRunId) => {
|
||||
dispatch(
|
||||
manualTestRunUpdateAction({
|
||||
testRunId,
|
||||
isTestNowFlyoutOpen: false,
|
||||
})
|
||||
);
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const flyout = flyoutOpenTestRun ? (
|
||||
<TestNowModeFlyout
|
||||
testRun={
|
||||
flyoutOpenTestRun?.testRunId && flyoutOpenTestRun?.monitor
|
||||
? { id: flyoutOpenTestRun.testRunId, monitor: flyoutOpenTestRun.monitor }
|
||||
: undefined
|
||||
}
|
||||
inProgress={
|
||||
flyoutOpenTestRun.status === 'in-progress' || flyoutOpenTestRun.status === 'loading'
|
||||
}
|
||||
onClose={() => handleFlyoutClose(flyoutOpenTestRun.testRunId)}
|
||||
onDone={onDone}
|
||||
isPushing={flyoutOpenTestRun.status === 'loading'}
|
||||
errors={flyoutOpenTestRun.errors ?? []}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{Object.values(testNowRuns)
|
||||
.filter((val) => val.testRunId)
|
||||
.map((manualTestRun) => (
|
||||
<ManualTestRunMode
|
||||
key={manualTestRun.testRunId}
|
||||
manualTestRun={manualTestRun}
|
||||
onDone={onDone}
|
||||
/>
|
||||
))}
|
||||
{flyout}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiBadge,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLink,
|
||||
EuiLoadingSpinner,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import * as React from 'react';
|
||||
import { useSyntheticsSettingsContext } from '../../contexts';
|
||||
import { JourneyStep, Ping } from '../../../../../common/runtime_types';
|
||||
import { formatDuration } from '../../utils/formatting';
|
||||
|
||||
interface Props {
|
||||
checkGroupId?: string;
|
||||
summaryDocs?: Ping[] | JourneyStep[] | null;
|
||||
journeyStarted?: boolean;
|
||||
title?: string;
|
||||
configId?: string;
|
||||
isCompleted: boolean;
|
||||
}
|
||||
|
||||
export function TestResultHeader({
|
||||
checkGroupId,
|
||||
title,
|
||||
summaryDocs,
|
||||
journeyStarted,
|
||||
isCompleted,
|
||||
configId,
|
||||
}: Props) {
|
||||
const { basePath } = useSyntheticsSettingsContext();
|
||||
let duration = 0;
|
||||
if (summaryDocs && summaryDocs.length > 0) {
|
||||
summaryDocs.forEach((sDoc) => {
|
||||
duration += sDoc.monitor.duration?.us ?? 0;
|
||||
});
|
||||
}
|
||||
|
||||
const summaryDoc = summaryDocs?.[0] as Ping;
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="xs">
|
||||
<h3>{title ?? TEST_RESULT}</h3>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={true}>
|
||||
{isCompleted ? (
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBadge color={summaryDoc?.summary?.down! > 0 ? 'danger' : 'success'}>
|
||||
{summaryDoc?.summary?.down! > 0 ? FAILED_LABEL : COMPLETED_LABEL}
|
||||
</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="xs" color="subdued">
|
||||
{i18n.translate('xpack.synthetics.monitorManagement.timeTaken', {
|
||||
defaultMessage: 'Took {timeTaken}',
|
||||
values: { timeTaken: formatDuration(duration) },
|
||||
})}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
) : (
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBadge style={{ width: 100 }} color={journeyStarted ? 'primary' : 'warning'}>
|
||||
{journeyStarted ? IN_PROGRESS_LABEL : PENDING_LABEL}
|
||||
</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiLoadingSpinner />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
{checkGroupId && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLink
|
||||
href={`${basePath}/app/synthetics/monitor/${configId}/test-run/${checkGroupId}`}
|
||||
target="_blank"
|
||||
>
|
||||
{VIEW_DETAILS}
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
export const PENDING_LABEL = i18n.translate('xpack.synthetics.monitorManagement.pending', {
|
||||
defaultMessage: 'PENDING',
|
||||
});
|
||||
|
||||
const TEST_RESULT = i18n.translate('xpack.synthetics.monitorManagement.testResult', {
|
||||
defaultMessage: 'Test result',
|
||||
});
|
||||
|
||||
const COMPLETED_LABEL = i18n.translate('xpack.synthetics.monitorManagement.completed', {
|
||||
defaultMessage: 'COMPLETED',
|
||||
});
|
||||
|
||||
const FAILED_LABEL = i18n.translate('xpack.synthetics.monitorManagement.failed', {
|
||||
defaultMessage: 'FAILED',
|
||||
});
|
||||
|
||||
export const IN_PROGRESS_LABEL = i18n.translate('xpack.synthetics.monitorManagement.inProgress', {
|
||||
defaultMessage: 'IN PROGRESS',
|
||||
});
|
||||
|
||||
const VIEW_DETAILS = i18n.translate('xpack.synthetics.monitorManagement.viewTestRunDetails', {
|
||||
defaultMessage: 'View test result details',
|
||||
});
|
|
@ -11,9 +11,10 @@ import { selectOverviewStatus } from '../state/overview';
|
|||
export function useStatusByLocationOverview(configId: string, locationName?: string) {
|
||||
const { status } = useSelector(selectOverviewStatus);
|
||||
if (!locationName || !status) {
|
||||
return 'unknown';
|
||||
return { status: 'unknown' };
|
||||
}
|
||||
const allConfigs = status.allConfigs;
|
||||
const config = allConfigs[`${configId}-${locationName}`];
|
||||
|
||||
return allConfigs[`${configId}-${locationName}`]?.status || 'unknown';
|
||||
return { status: config?.status || 'unknown', timestamp: config?.timestamp };
|
||||
}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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 { createAction } from '@reduxjs/toolkit';
|
||||
import { ManualTestRun } from '.';
|
||||
import { TestNowResponse } from '../../../../../common/types';
|
||||
import { createAsyncAction } from '../utils/actions';
|
||||
|
||||
export const toggleTestNowFlyoutAction = createAction<string>('TOGGLE TEST NOW FLYOUT ACTION');
|
||||
export const hideTestNowFlyoutAction = createAction('HIDE ALL TEST NOW FLYOUT ACTION');
|
||||
|
||||
export const manualTestMonitorAction = createAsyncAction<string, TestNowResponse | undefined>(
|
||||
'TEST_NOW_MONITOR_ACTION'
|
||||
);
|
||||
|
||||
export const manualTestRunUpdateAction = createAction<
|
||||
Partial<ManualTestRun> & { testRunId: string }
|
||||
>('manualTestRunUpdateAction');
|
||||
|
||||
export const clearTestNowMonitorAction = createAction<string>('CLEAR_TEST_NOW_MONITOR_ACTION');
|
|
@ -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 { ServiceLocationErrors, SyntheticsMonitor } from '../../../../../common/runtime_types';
|
||||
import { TestNowResponse } from '../../../../../common/types';
|
||||
import { apiService } from '../../../../utils/api_service';
|
||||
import { API_URLS } from '../../../../../common/constants';
|
||||
|
||||
export const triggerTestNowMonitor = async (
|
||||
configId: string
|
||||
): Promise<TestNowResponse | undefined> => {
|
||||
return await apiService.get(API_URLS.TRIGGER_MONITOR + `/${configId}`);
|
||||
};
|
||||
|
||||
export const runOnceMonitor = async ({
|
||||
monitor,
|
||||
id,
|
||||
}: {
|
||||
monitor: SyntheticsMonitor;
|
||||
id: string;
|
||||
}): Promise<{ errors: ServiceLocationErrors }> => {
|
||||
return await apiService.post(API_URLS.RUN_ONCE_MONITOR + `/${id}`, monitor);
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { takeEvery } from 'redux-saga/effects';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { fetchEffectFactory } from '../utils/fetch_effect';
|
||||
import { manualTestMonitorAction } from './actions';
|
||||
import { triggerTestNowMonitor } from './api';
|
||||
|
||||
export function* fetchManualTestRunsEffect() {
|
||||
yield takeEvery(
|
||||
manualTestMonitorAction.get,
|
||||
fetchEffectFactory(
|
||||
triggerTestNowMonitor,
|
||||
manualTestMonitorAction.success,
|
||||
manualTestMonitorAction.fail,
|
||||
'',
|
||||
FAILED_TEST
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const FAILED_TEST = i18n.translate('xpack.synthetics.runTest.failure', {
|
||||
defaultMessage: 'Failed to run test manually',
|
||||
});
|
|
@ -0,0 +1,169 @@
|
|||
/*
|
||||
* 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 { createReducer, PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
import { WritableDraft } from 'immer/dist/types/types-external';
|
||||
import { IHttpFetchError } from '@kbn/core-http-browser';
|
||||
|
||||
import { TestNowResponse } from '../../../../../common/types';
|
||||
import {
|
||||
clearTestNowMonitorAction,
|
||||
hideTestNowFlyoutAction,
|
||||
manualTestMonitorAction,
|
||||
manualTestRunUpdateAction,
|
||||
toggleTestNowFlyoutAction,
|
||||
} from './actions';
|
||||
import {
|
||||
Locations,
|
||||
ScheduleUnit,
|
||||
ServiceLocationErrors,
|
||||
SyntheticsMonitorSchedule,
|
||||
} from '../../../../../common/runtime_types';
|
||||
|
||||
export enum TestRunStatus {
|
||||
LOADING = 'loading',
|
||||
IN_PROGRESS = 'in-progress',
|
||||
COMPLETED = 'completed',
|
||||
}
|
||||
|
||||
export interface ManualTestRun {
|
||||
configId: string;
|
||||
testRunId?: string;
|
||||
status: TestRunStatus;
|
||||
schedule: SyntheticsMonitorSchedule;
|
||||
locations: Locations;
|
||||
errors?: ServiceLocationErrors;
|
||||
fetchError?: { name: string; message: string };
|
||||
isTestNowFlyoutOpen: boolean;
|
||||
monitor?: TestNowResponse['monitor'];
|
||||
}
|
||||
|
||||
export interface ManualTestRunsState {
|
||||
[configId: string]: ManualTestRun;
|
||||
}
|
||||
|
||||
const initialState: ManualTestRunsState = {};
|
||||
|
||||
export const manualTestRunsReducer = createReducer(initialState, (builder) => {
|
||||
builder
|
||||
.addCase(
|
||||
String(manualTestMonitorAction.get),
|
||||
(state: WritableDraft<ManualTestRunsState>, action: PayloadAction<string>) => {
|
||||
state = Object.values(state).reduce((acc, curr) => {
|
||||
acc[curr.configId] = {
|
||||
...curr,
|
||||
isTestNowFlyoutOpen: false,
|
||||
};
|
||||
|
||||
return acc;
|
||||
}, state);
|
||||
|
||||
state[action.payload] = {
|
||||
configId: action.payload,
|
||||
status: TestRunStatus.LOADING,
|
||||
schedule: { unit: ScheduleUnit.MINUTES, number: '3' },
|
||||
locations: [],
|
||||
isTestNowFlyoutOpen: true,
|
||||
};
|
||||
}
|
||||
)
|
||||
.addCase(
|
||||
String(manualTestMonitorAction.success),
|
||||
(state: WritableDraft<ManualTestRunsState>, { payload }: PayloadAction<TestNowResponse>) => {
|
||||
state[payload.configId] = {
|
||||
configId: payload.configId,
|
||||
testRunId: payload.testRunId,
|
||||
status: TestRunStatus.IN_PROGRESS,
|
||||
errors: payload.errors,
|
||||
schedule: payload.schedule,
|
||||
locations: payload.locations,
|
||||
isTestNowFlyoutOpen: true,
|
||||
monitor: payload.monitor,
|
||||
};
|
||||
}
|
||||
)
|
||||
.addCase(
|
||||
String(manualTestMonitorAction.fail),
|
||||
(state: WritableDraft<ManualTestRunsState>, action: PayloadAction<TestNowResponse>) => {
|
||||
const fetchError = action.payload as unknown as IHttpFetchError;
|
||||
if (fetchError?.request.url) {
|
||||
const { name, message } = fetchError;
|
||||
|
||||
const [, errorMonitor] =
|
||||
Object.entries(state).find(
|
||||
([key]) => fetchError.request.url.indexOf(key) > -1 ?? false
|
||||
) ?? [];
|
||||
|
||||
if (errorMonitor) {
|
||||
state[errorMonitor.configId] = {
|
||||
...state[errorMonitor.configId],
|
||||
status: TestRunStatus.COMPLETED,
|
||||
errors: undefined,
|
||||
fetchError: { name, message },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (action.payload.configId) {
|
||||
state[action.payload.configId] = {
|
||||
...state[action.payload.configId],
|
||||
status: TestRunStatus.COMPLETED,
|
||||
errors: action.payload.errors,
|
||||
fetchError: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
)
|
||||
.addCase(manualTestRunUpdateAction, (state: WritableDraft<ManualTestRunsState>, action) => {
|
||||
const { testRunId, ...rest } = action.payload;
|
||||
const configId = Object.keys(state).find((key) => state[key].testRunId === testRunId);
|
||||
if (configId) {
|
||||
state[configId] = {
|
||||
...state[configId],
|
||||
...rest,
|
||||
};
|
||||
}
|
||||
})
|
||||
.addCase(toggleTestNowFlyoutAction, (state: WritableDraft<ManualTestRunsState>, action) => {
|
||||
state = Object.values(state).reduce((acc, curr) => {
|
||||
acc[curr.configId] = {
|
||||
...curr,
|
||||
isTestNowFlyoutOpen: false,
|
||||
};
|
||||
|
||||
return acc;
|
||||
}, state);
|
||||
|
||||
state[action.payload] = {
|
||||
...state[action.payload],
|
||||
isTestNowFlyoutOpen: !state[action.payload].isTestNowFlyoutOpen,
|
||||
};
|
||||
})
|
||||
.addCase(hideTestNowFlyoutAction, (state: WritableDraft<ManualTestRunsState>) => {
|
||||
state = Object.values(state).reduce((acc, curr) => {
|
||||
acc[curr.configId] = {
|
||||
...curr,
|
||||
isTestNowFlyoutOpen: false,
|
||||
};
|
||||
|
||||
return acc;
|
||||
}, state);
|
||||
return state;
|
||||
})
|
||||
.addCase(
|
||||
String(clearTestNowMonitorAction),
|
||||
(state: WritableDraft<ManualTestRunsState>, action: PayloadAction<string>) => {
|
||||
delete state[action.payload];
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
export * from './actions';
|
||||
export * from './selectors';
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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 type { SyntheticsAppState } from '../root_reducer';
|
||||
|
||||
export const testNowRunsSelector = ({ manualTestRuns }: SyntheticsAppState) => manualTestRuns;
|
||||
|
||||
export const manualTestRunSelector =
|
||||
(configId?: string) =>
|
||||
({ manualTestRuns }: SyntheticsAppState) =>
|
||||
configId ? manualTestRuns[configId] : undefined;
|
||||
|
||||
export const manualTestRunInProgressSelector =
|
||||
(configId: string) =>
|
||||
({ manualTestRuns }: SyntheticsAppState) => {
|
||||
return (
|
||||
manualTestRuns[configId]?.status === 'in-progress' ||
|
||||
manualTestRuns[configId]?.status === 'loading'
|
||||
);
|
||||
};
|
|
@ -22,6 +22,7 @@ export type MonitorOverviewFlyoutConfig = {
|
|||
configId: string;
|
||||
id: string;
|
||||
location: string;
|
||||
locationId: string;
|
||||
} | null;
|
||||
|
||||
export interface MonitorOverviewState {
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { all, fork } from 'redux-saga/effects';
|
||||
import { fetchManualTestRunsEffect } from './manual_test_runs/effects';
|
||||
import { enableDefaultAlertingEffect, updateDefaultAlertingEffect } from './alert_rules/effects';
|
||||
import { executeEsQueryEffect } from './elasticsearch';
|
||||
import {
|
||||
|
@ -53,5 +54,6 @@ export const rootEffect = function* root(): Generator {
|
|||
fork(updateDefaultAlertingEffect),
|
||||
fork(executeEsQueryEffect),
|
||||
fork(fetchJourneyStepsEffect),
|
||||
fork(fetchManualTestRunsEffect),
|
||||
]);
|
||||
};
|
||||
|
|
|
@ -9,6 +9,7 @@ import { combineReducers } from '@reduxjs/toolkit';
|
|||
|
||||
import { browserJourneyReducer } from './browser_journey';
|
||||
import { defaultAlertingReducer, DefaultAlertingState } from './alert_rules';
|
||||
import { manualTestRunsReducer, ManualTestRunsState } from './manual_test_runs';
|
||||
import {
|
||||
dynamicSettingsReducer,
|
||||
DynamicSettingsState,
|
||||
|
@ -38,10 +39,11 @@ export interface SyntheticsAppState {
|
|||
overview: MonitorOverviewState;
|
||||
networkEvents: NetworkEventsState;
|
||||
agentPolicies: AgentPoliciesState;
|
||||
manualTestRuns: ManualTestRunsState;
|
||||
monitorDetails: MonitorDetailsState;
|
||||
browserJourney: BrowserJourneyState;
|
||||
dynamicSettings: DynamicSettingsState;
|
||||
defaultAlerting: DefaultAlertingState;
|
||||
dynamicSettings: DynamicSettingsState;
|
||||
serviceLocations: ServiceLocationsState;
|
||||
syntheticsEnablement: SyntheticsEnablementState;
|
||||
}
|
||||
|
@ -58,6 +60,7 @@ export const rootReducer = combineReducers<SyntheticsAppState>({
|
|||
agentPolicies: agentPoliciesReducer,
|
||||
monitorDetails: monitorDetailsReducer,
|
||||
browserJourney: browserJourneyReducer,
|
||||
manualTestRuns: manualTestRunsReducer,
|
||||
defaultAlerting: defaultAlertingReducer,
|
||||
dynamicSettings: dynamicSettingsReducer,
|
||||
serviceLocations: serviceLocationsReducer,
|
||||
|
|
|
@ -91,7 +91,7 @@ export function fetchEffectFactory<T, R, S, F>(
|
|||
|
||||
if (typeof onSuccess === 'function') {
|
||||
onSuccess?.(response as R);
|
||||
} else if (typeof onSuccess === 'string') {
|
||||
} else if (onSuccess && typeof onSuccess === 'string') {
|
||||
kibanaService.core.notifications.toasts.addSuccess(onSuccess);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ import { PageRouter } from './routes';
|
|||
import { store, storage, setBasePath } from './state';
|
||||
import { kibanaService } from '../../utils/kibana_service';
|
||||
import { ActionMenu } from './components/common/header/action_menu';
|
||||
import { TestNowModeFlyoutContainer } from './components/test_now_mode/test_now_mode_flyout_container';
|
||||
|
||||
const Application = (props: SyntheticsAppProps) => {
|
||||
const {
|
||||
|
@ -110,6 +111,7 @@ const Application = (props: SyntheticsAppProps) => {
|
|||
<InspectorContextProvider>
|
||||
<PageRouter />
|
||||
<ActionMenu appMountParameters={appMountParameters} />
|
||||
<TestNowModeFlyoutContainer />
|
||||
</InspectorContextProvider>
|
||||
</RedirectAppLinks>
|
||||
</div>
|
||||
|
|
|
@ -134,6 +134,7 @@ export const mockState: SyntheticsAppState = {
|
|||
loading: {},
|
||||
error: {},
|
||||
},
|
||||
manualTestRuns: {},
|
||||
};
|
||||
|
||||
function getBrowserJourneyMockSlice() {
|
||||
|
|
|
@ -10,7 +10,10 @@ import { renderWithIntl } from '@kbn/test-jest-helpers';
|
|||
|
||||
import { DonutChartLegend } from './donut_chart_legend';
|
||||
|
||||
import { STATUS_DOWN_LABEL, STATUS_UP_LABEL } from '../translations';
|
||||
import {
|
||||
STATUS_DOWN_LABEL,
|
||||
STATUS_UP_LABEL,
|
||||
} from '../../../../../common/translations/translations';
|
||||
|
||||
describe('DonutChartLegend', () => {
|
||||
it('applies valid props as expected', () => {
|
||||
|
|
|
@ -10,7 +10,10 @@ import React, { useContext } from 'react';
|
|||
import styled from 'styled-components';
|
||||
import { DonutChartLegendRow } from './donut_chart_legend_row';
|
||||
import { UptimeThemeContext } from '../../../contexts';
|
||||
import { STATUS_DOWN_LABEL, STATUS_UP_LABEL } from '../translations';
|
||||
import {
|
||||
STATUS_DOWN_LABEL,
|
||||
STATUS_UP_LABEL,
|
||||
} from '../../../../../common/translations/translations';
|
||||
|
||||
const LegendContainer = styled.div`
|
||||
max-width: 150px;
|
||||
|
|
|
@ -32,7 +32,7 @@ import { UptimeThemeContext } from '../../../contexts';
|
|||
import { MONITOR_CHART_HEIGHT } from '../../monitor';
|
||||
import { monitorStatusSelector } from '../../../state/selectors';
|
||||
import { microToMilli, microToSec } from '../../../lib/formatting';
|
||||
import { MS_LABEL, SECONDS_LABEL } from '../translations';
|
||||
import { MS_LABEL, SECONDS_LABEL } from '../../../../../common/translations/translations';
|
||||
|
||||
interface DurationChartProps {
|
||||
/**
|
||||
|
|
|
@ -9,7 +9,7 @@ import React from 'react';
|
|||
import { LineSeries, CurveType, Fit, ScaleType } from '@elastic/charts';
|
||||
import { LocationDurationLine } from '../../../../../common/types';
|
||||
import { microToMilli, microToSec } from '../../../lib/formatting';
|
||||
import { MS_LABEL, SEC_LABEL } from '../translations';
|
||||
import { MS_LABEL, SEC_LABEL } from '../../../../../common/translations/translations';
|
||||
|
||||
interface Props {
|
||||
monitorType: string;
|
||||
|
|
|
@ -30,7 +30,10 @@ import { HistogramResult } from '../../../../../common/runtime_types';
|
|||
import { useUrlParams } from '../../../hooks';
|
||||
import { ChartEmptyState } from './chart_empty_state';
|
||||
import { getDateRangeFromChartElement } from './utils';
|
||||
import { STATUS_DOWN_LABEL, STATUS_UP_LABEL } from '../translations';
|
||||
import {
|
||||
STATUS_DOWN_LABEL,
|
||||
STATUS_UP_LABEL,
|
||||
} from '../../../../../common/translations/translations';
|
||||
|
||||
export interface PingHistogramComponentProps {
|
||||
/**
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
STATUS_DOWN_LABEL,
|
||||
STATUS_FAILED_LABEL,
|
||||
STATUS_UP_LABEL,
|
||||
} from '../../../common/translations';
|
||||
} from '../../../../../../common/translations/translations';
|
||||
|
||||
interface Props {
|
||||
pingStatus: string;
|
||||
|
|
|
@ -21,7 +21,11 @@ import * as labels from '../translations';
|
|||
import { StatusByLocations } from './status_by_location';
|
||||
import { useStatusBar } from './use_status_bar';
|
||||
import { MonitorIDLabel, OverallAvailability } from '../translations';
|
||||
import { PROJECT_LABEL, TAGS_LABEL, URL_LABEL } from '../../../common/translations';
|
||||
import {
|
||||
PROJECT_LABEL,
|
||||
TAGS_LABEL,
|
||||
URL_LABEL,
|
||||
} from '../../../../../../common/translations/translations';
|
||||
import { MonitorLocations } from '../../../../../../common/runtime_types/monitor';
|
||||
import { formatAvailabilityValue } from '../availability_reporting/availability_reporting';
|
||||
import { MonitorRedirects } from './monitor_redirects';
|
||||
|
|
|
@ -11,10 +11,12 @@ import { useLocationMonitors } from './use_location_monitors';
|
|||
import { defaultCore, WrappedHelper } from '../../../../../apps/synthetics/utils/testing';
|
||||
|
||||
describe('useLocationMonitors', () => {
|
||||
it('returns expected results', () => {
|
||||
const { result } = renderHook(() => useLocationMonitors(), { wrapper: WrappedHelper });
|
||||
it('returns expected results', async () => {
|
||||
const { result, waitFor } = renderHook(() => useLocationMonitors(), { wrapper: WrappedHelper });
|
||||
|
||||
expect(result.current).toStrictEqual({ locationMonitors: [], loading: true });
|
||||
await waitFor(() => result.current.loading === false);
|
||||
|
||||
expect(result.current).toStrictEqual({ locationMonitors: [], loading: false });
|
||||
expect(defaultCore.savedObjects.client.find).toHaveBeenCalledWith({
|
||||
aggs: {
|
||||
locations: {
|
||||
|
|
|
@ -18,7 +18,7 @@ import { EuiTableSortingType } from '@elastic/eui/src/components/basic_table/tab
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useCallback, useContext, useMemo } from 'react';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { PROJECT_LABEL } from '../../common/translations';
|
||||
import { PROJECT_LABEL } from '../../../../../common/translations/translations';
|
||||
import {
|
||||
CommonFields,
|
||||
ConfigKey,
|
||||
|
|
|
@ -31,7 +31,10 @@ import {
|
|||
SHORT_TS_LOCALE,
|
||||
} from '../../../../../../common/constants';
|
||||
|
||||
import { STATUS_DOWN_LABEL, STATUS_UP_LABEL } from '../../../common/translations';
|
||||
import {
|
||||
STATUS_DOWN_LABEL,
|
||||
STATUS_UP_LABEL,
|
||||
} from '../../../../../../common/translations/translations';
|
||||
import { MonitorProgress } from './progress/monitor_progress';
|
||||
import { refreshedMonitorSelector } from '../../../../state/reducers/monitor_list';
|
||||
import { testNowRunSelector } from '../../../../state/reducers/test_now_runs';
|
||||
|
|
|
@ -30,7 +30,7 @@ import { MonitorListProps } from './monitor_list_container';
|
|||
import { MonitorList } from '../../../state/reducers/monitor_list';
|
||||
import { CertStatusColumn } from './columns/cert_status_column';
|
||||
import { MonitorListHeader } from './monitor_list_header';
|
||||
import { TAGS_LABEL, URL_LABEL } from '../../common/translations';
|
||||
import { TAGS_LABEL, URL_LABEL } from '../../../../../common/translations/translations';
|
||||
import { EnableMonitorAlert } from './columns/enable_alert';
|
||||
import { STATUS_ALERT_COLUMN, TEST_NOW_COLUMN } from './translations';
|
||||
import { MonitorNameColumn } from './columns/monitor_name_col';
|
||||
|
|
|
@ -10,7 +10,10 @@ import { i18n } from '@kbn/i18n';
|
|||
import { EuiFilterGroup } from '@elastic/eui';
|
||||
import { FilterStatusButton } from './filter_status_button';
|
||||
import { useGetUrlParams } from '../../../hooks';
|
||||
import { STATUS_DOWN_LABEL, STATUS_UP_LABEL } from '../../common/translations';
|
||||
import {
|
||||
STATUS_DOWN_LABEL,
|
||||
STATUS_UP_LABEL,
|
||||
} from '../../../../../common/translations/translations';
|
||||
|
||||
export const StatusFilter: React.FC = () => {
|
||||
const { statusFilter } = useGetUrlParams();
|
||||
|
|
|
@ -6,12 +6,12 @@
|
|||
*/
|
||||
|
||||
import { createAction } from 'redux-actions';
|
||||
import { TestNowResponse } from '../../../../common/types';
|
||||
import {
|
||||
FetchMonitorStatesQueryArgs,
|
||||
MonitorSummariesResult,
|
||||
} from '../../../../common/runtime_types';
|
||||
import { createAsyncAction } from './utils';
|
||||
import { TestNowResponse } from '../api';
|
||||
|
||||
export const getMonitorList = createAction<FetchMonitorStatesQueryArgs>('GET_MONITOR_LIST');
|
||||
export const getMonitorListSuccess = createAction<MonitorSummariesResult>(
|
||||
|
|
|
@ -18,12 +18,11 @@ import {
|
|||
ServiceLocationsApiResponseCodec,
|
||||
ServiceLocationErrors,
|
||||
ThrottlingOptions,
|
||||
Locations,
|
||||
SyntheticsMonitorSchedule,
|
||||
} from '../../../../common/runtime_types';
|
||||
import {
|
||||
DecryptedSyntheticsMonitorSavedObject,
|
||||
SyntheticsServiceAllowed,
|
||||
TestNowResponse,
|
||||
} from '../../../../common/types';
|
||||
import { apiService } from './utils';
|
||||
|
||||
|
@ -87,14 +86,6 @@ export const runOnceMonitor = async ({
|
|||
return await apiService.post(API_URLS.RUN_ONCE_MONITOR + `/${id}`, monitor);
|
||||
};
|
||||
|
||||
export interface TestNowResponse {
|
||||
schedule: SyntheticsMonitorSchedule;
|
||||
locations: Locations;
|
||||
errors?: ServiceLocationErrors;
|
||||
testRunId: string;
|
||||
monitorId: string;
|
||||
}
|
||||
|
||||
export const triggerTestNowMonitor = async (
|
||||
configId: string
|
||||
): Promise<TestNowResponse | undefined> => {
|
||||
|
|
|
@ -7,8 +7,9 @@
|
|||
|
||||
import { takeEvery } from 'redux-saga/effects';
|
||||
import type { IHttpFetchError } from '@kbn/core-http-browser';
|
||||
import { TestNowResponse } from '../../../../common/types';
|
||||
import { testNowMonitorAction } from '../actions';
|
||||
import { type TestNowResponse, triggerTestNowMonitor } from '../api';
|
||||
import { triggerTestNowMonitor } from '../api';
|
||||
import { fetchEffectFactory } from './fetch_effect';
|
||||
|
||||
export function* fetchTestNowMonitorEffect() {
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { handleActions, type Action } from 'redux-actions';
|
||||
import type { IHttpFetchError, ResponseErrorBody } from '@kbn/core-http-browser';
|
||||
import { TestNowResponse } from '../../../../common/types';
|
||||
import {
|
||||
getMonitorList,
|
||||
getMonitorListSuccess,
|
||||
|
@ -17,7 +18,6 @@ import {
|
|||
} from '../actions';
|
||||
import type { MonitorSummariesResult } from '../../../../common/runtime_types';
|
||||
import type { AppState } from '..';
|
||||
import type { TestNowResponse } from '../api';
|
||||
|
||||
export interface MonitorList {
|
||||
loading: boolean;
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import { createReducer, type PayloadAction } from '@reduxjs/toolkit';
|
||||
import type { WritableDraft } from 'immer/dist/types/types-external';
|
||||
import type { IHttpFetchError } from '@kbn/core-http-browser';
|
||||
import { TestNowResponse } from '../../../../common/types';
|
||||
import {
|
||||
type Locations,
|
||||
ScheduleUnit,
|
||||
|
@ -15,7 +16,6 @@ import {
|
|||
type SyntheticsMonitorSchedule,
|
||||
} from '../../../../common/runtime_types';
|
||||
import { clearTestNowMonitorAction, testNowMonitorAction } from '../actions';
|
||||
import type { TestNowResponse } from '../api';
|
||||
import type { AppState } from '..';
|
||||
|
||||
export enum TestRunStats {
|
||||
|
@ -58,8 +58,8 @@ export const testNowRunsReducer = createReducer(initialState, (builder) => {
|
|||
String(testNowMonitorAction.success),
|
||||
(state: WritableDraft<TestNowRunsState>, { payload }: PayloadAction<TestNowResponse>) => ({
|
||||
...state,
|
||||
[payload.monitorId]: {
|
||||
monitorId: payload.monitorId,
|
||||
[payload.configId]: {
|
||||
monitorId: payload.configId,
|
||||
testRunId: payload.testRunId,
|
||||
status: TestRunStats.IN_PROGRESS,
|
||||
errors: payload.errors,
|
||||
|
@ -93,11 +93,11 @@ export const testNowRunsReducer = createReducer(initialState, (builder) => {
|
|||
}
|
||||
}
|
||||
|
||||
if (action.payload.monitorId) {
|
||||
if (action.payload.configId) {
|
||||
return {
|
||||
...state,
|
||||
[action.payload.monitorId]: {
|
||||
...state[action.payload.monitorId],
|
||||
[action.payload.configId]: {
|
||||
...state[action.payload.configId],
|
||||
status: TestRunStats.COMPLETED,
|
||||
errors: action.payload.errors,
|
||||
fetchError: undefined,
|
||||
|
|
|
@ -42,7 +42,7 @@ export const getJourneyDetails: UMElasticsearchQueryFn<
|
|||
|
||||
const { body: thisJourney } = await uptimeEsClient.search(
|
||||
{ body: baseParams },
|
||||
'getJourneyDetails'
|
||||
'getJourneyDetailsCurrentJourney'
|
||||
);
|
||||
|
||||
if (thisJourney.hits.hits.length > 0) {
|
||||
|
@ -108,8 +108,14 @@ export const getJourneyDetails: UMElasticsearchQueryFn<
|
|||
sort: [{ '@timestamp': { order: 'asc' as const } }],
|
||||
};
|
||||
|
||||
const { body: previousJourneyResult } = await uptimeEsClient.search({ body: previousParams });
|
||||
const { body: nextJourneyResult } = await uptimeEsClient.search({ body: nextParams });
|
||||
const { body: previousJourneyResult } = await uptimeEsClient.search(
|
||||
{ body: previousParams },
|
||||
'getJourneyDetailsNextJourney'
|
||||
);
|
||||
const { body: nextJourneyResult } = await uptimeEsClient.search(
|
||||
{ body: nextParams },
|
||||
'getJourneyDetailsPreviousJourney'
|
||||
);
|
||||
const previousJourney: any =
|
||||
previousJourneyResult?.hits?.hits.length > 0 ? previousJourneyResult?.hits?.hits[0] : null;
|
||||
const nextJourney: any =
|
||||
|
|
|
@ -110,7 +110,7 @@ export async function queryMonitorStatus(
|
|||
response.body.aggregations?.id.buckets.forEach(
|
||||
({ location, key: queryId }: { location: any; key: string }) => {
|
||||
location.buckets.forEach(({ status }: { key: string; status: any }) => {
|
||||
const ping = status.hits.hits[0]._source as Ping;
|
||||
const ping = status.hits.hits[0]._source as Ping & { '@timestamp': string };
|
||||
|
||||
const locationName = ping.observer?.geo?.name!;
|
||||
|
||||
|
@ -132,6 +132,7 @@ export async function queryMonitorStatus(
|
|||
monitorQueryId,
|
||||
location: locationName,
|
||||
status: 'up',
|
||||
timestamp: ping['@timestamp'],
|
||||
};
|
||||
} else if (downCount > 0) {
|
||||
down += 1;
|
||||
|
@ -141,6 +142,7 @@ export async function queryMonitorStatus(
|
|||
monitorQueryId,
|
||||
location: locationName,
|
||||
status: 'down',
|
||||
timestamp: ping['@timestamp'],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
|
|
@ -184,6 +184,7 @@ describe('current status route', () => {
|
|||
location: 'Asia/Pacific - Japan',
|
||||
status: 'up',
|
||||
ping: expect.any(Object),
|
||||
timestamp: expect.any(String),
|
||||
},
|
||||
'id2-Asia/Pacific - Japan': {
|
||||
configId: 'id2',
|
||||
|
@ -191,6 +192,7 @@ describe('current status route', () => {
|
|||
location: 'Asia/Pacific - Japan',
|
||||
status: 'up',
|
||||
ping: expect.any(Object),
|
||||
timestamp: expect.any(String),
|
||||
},
|
||||
},
|
||||
downConfigs: {
|
||||
|
@ -200,6 +202,7 @@ describe('current status route', () => {
|
|||
location: 'Europe - Germany',
|
||||
status: 'down',
|
||||
ping: expect.any(Object),
|
||||
timestamp: expect.any(String),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -334,6 +337,7 @@ describe('current status route', () => {
|
|||
location: 'Asia/Pacific - Japan',
|
||||
status: 'up',
|
||||
ping: expect.any(Object),
|
||||
timestamp: expect.any(String),
|
||||
},
|
||||
'id2-Asia/Pacific - Japan': {
|
||||
configId: 'id2',
|
||||
|
@ -341,6 +345,7 @@ describe('current status route', () => {
|
|||
location: 'Asia/Pacific - Japan',
|
||||
status: 'up',
|
||||
ping: expect.any(Object),
|
||||
timestamp: expect.any(String),
|
||||
},
|
||||
},
|
||||
downConfigs: {
|
||||
|
@ -350,6 +355,7 @@ describe('current status route', () => {
|
|||
location: 'Europe - Germany',
|
||||
status: 'down',
|
||||
ping: expect.any(Object),
|
||||
timestamp: expect.any(String),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -44,6 +44,7 @@ export const runOnceSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () =
|
|||
monitorId,
|
||||
heartbeatId: monitorId,
|
||||
runOnce: true,
|
||||
testRunId: monitorId,
|
||||
params: paramsBySpace[spaceId],
|
||||
}),
|
||||
]);
|
||||
|
|
|
@ -6,10 +6,10 @@
|
|||
*/
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { TestNowResponse } from '../../../common/types';
|
||||
import {
|
||||
ConfigKey,
|
||||
MonitorFields,
|
||||
SyntheticsMonitor,
|
||||
SyntheticsMonitorWithSecrets,
|
||||
} from '../../../common/runtime_types';
|
||||
import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes/types';
|
||||
|
@ -26,18 +26,8 @@ export const testNowMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
|
|||
monitorId: schema.string({ minLength: 1, maxLength: 1024 }),
|
||||
}),
|
||||
},
|
||||
handler: async ({
|
||||
request,
|
||||
savedObjectsClient,
|
||||
server,
|
||||
syntheticsMonitorClient,
|
||||
}): Promise<any> => {
|
||||
handler: async ({ request, server, syntheticsMonitorClient }): Promise<any> => {
|
||||
const { monitorId } = request.params;
|
||||
const monitor = await savedObjectsClient.get<SyntheticsMonitor>(
|
||||
syntheticsMonitorType,
|
||||
monitorId
|
||||
);
|
||||
|
||||
const encryptedClient = server.encryptedSavedObjects.getClient();
|
||||
|
||||
const monitorWithSecrets =
|
||||
|
@ -47,7 +37,8 @@ export const testNowMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
|
|||
);
|
||||
const normalizedMonitor = normalizeSecrets(monitorWithSecrets);
|
||||
|
||||
const { [ConfigKey.SCHEDULE]: schedule, [ConfigKey.LOCATIONS]: locations } = monitor.attributes;
|
||||
const { [ConfigKey.SCHEDULE]: schedule, [ConfigKey.LOCATIONS]: locations } =
|
||||
monitorWithSecrets.attributes;
|
||||
|
||||
const { syntheticsService } = syntheticsMonitorClient;
|
||||
|
||||
|
@ -69,9 +60,22 @@ export const testNowMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
|
|||
]);
|
||||
|
||||
if (errors && errors?.length > 0) {
|
||||
return { errors, testRunId, monitorId, schedule, locations };
|
||||
return {
|
||||
errors,
|
||||
testRunId,
|
||||
schedule,
|
||||
locations,
|
||||
configId: monitorId,
|
||||
monitor: normalizedMonitor.attributes,
|
||||
} as TestNowResponse;
|
||||
}
|
||||
|
||||
return { testRunId, monitorId, schedule, locations };
|
||||
return {
|
||||
testRunId,
|
||||
schedule,
|
||||
locations,
|
||||
configId: monitorId,
|
||||
monitor: normalizedMonitor.attributes,
|
||||
} as TestNowResponse;
|
||||
},
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue