diff --git a/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts b/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts index 5ed71acaf7739..0a4d6310927c4 100644 --- a/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts +++ b/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts @@ -143,7 +143,7 @@ export const PingType = t.intersection([ response: t.partial({ body: HttpResponseBodyType, bytes: t.number, - redirects: t.string, + redirects: t.array(t.string), status_code: t.number, }), version: t.string, diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/expanded_row.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/expanded_row.test.tsx.snap index 004de391a51a4..11bdf134bd0e8 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/expanded_row.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/expanded_row.test.tsx.snap @@ -1,7 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`PingListExpandedRow doesn't render list items if the body field is undefined 1`] = ` - + + + + +
+ { }); } return ( - + + {ping?.http?.response?.redirects && ( + + + + )} diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/ping_list.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/ping_list.tsx index 576810bba24fd..09782c1b76edb 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/ping_list.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/ping_list.tsx @@ -237,6 +237,7 @@ export const PingListComponent = (props: Props) => { render: (item: Ping) => { return ( toggleDetails(item, expandedRows, setExpandedRows)} disabled={!item.error && !(item.http?.response?.body?.bytes ?? 0 > 0)} aria-label={ diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/ping_redirects.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/ping_redirects.tsx new file mode 100644 index 0000000000000..b3e59615cbce5 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/ping_redirects.tsx @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +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/ping'; + +const ListGroup = styled(EuiListGroup)` + &&& { + a { + padding-left: 0; + } + } +`; + +interface Props { + monitorStatus: Ping | null; + showTitle?: boolean; +} + +export const PingRedirects: React.FC = ({ 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: 'subdued', + iconType: 'popout', + iconSize: 's', + alwaysShow: true, + 'aria-label': i18n.translate('xpack.uptime.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: 'subdued', + iconType: 'popout', + iconSize: 's', + 'aria-label': i18n.translate('xpack.uptime.monitorList.redirects.openWindow', { + defaultMessage: 'Link will open in new window.', + }), + alwaysShow: true, + }, + }); + }); + + const Panel = showTitle ? EuiPanel : 'div'; + + return list ? ( + + {showTitle && ( + +

+ {i18n.translate('xpack.uptime.monitorList.redirects.title', { + defaultMessage: 'Redirects', + })} +

+
+ )} + + { + + {i18n.translate('xpack.uptime.monitorList.redirects.description', { + defaultMessage: 'Heartbeat followed {number} redirects while executing ping.', + values: { + number: list?.length ?? 0, + }, + })} + + } + + +
+ ) : null; +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/monitor_redirects.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/monitor_redirects.tsx new file mode 100644 index 0000000000000..5129db9c2135b --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/monitor_redirects.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiPopover } from '@elastic/eui'; +import styled from 'styled-components'; +import { Ping } from '../../../../../common/runtime_types'; +import { PingRedirects } from '../../ping_list/ping_redirects'; +import { MonListDescription, MonListTitle } from './status_bar'; + +interface Props { + monitorStatus: Ping | null; +} + +const RedirectBtn = styled.span` + cursor: pointer; +`; + +export const MonitorRedirects: React.FC = ({ monitorStatus }) => { + const list = monitorStatus?.http?.response?.redirects; + + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const button = ( + + setIsPopoverOpen(!isPopoverOpen)} + data-test-subj="uptimeMonitorRedirectInfo" + > + {i18n.translate('xpack.uptime.monitorList.redirects.title.number', { + defaultMessage: '{number}', + values: { + number: list?.length ?? 0, + }, + })} + + + ); + + return list ? ( + <> + Redirects + setIsPopoverOpen(false)} + > + + + + ) : null; +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/status_bar.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/status_bar.tsx index afcc8fae7a8ac..4ea383567d71c 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/status_bar.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/status_bar.tsx @@ -23,6 +23,7 @@ import { MonitorIDLabel, OverallAvailability } from '../translations'; import { URL_LABEL } from '../../../common/translations'; import { MonitorLocations } from '../../../../../common/runtime_types/monitor'; import { formatAvailabilityValue } from '../availability_reporting/availability_reporting'; +import { MonitorRedirects } from './monitor_redirects'; export const MonListTitle = styled(EuiDescriptionListTitle)` &&& { @@ -76,6 +77,7 @@ export const MonitorStatusBar: React.FC = () => { {MonitorIDLabel} {monitorId} +
); diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts index 01384ec145236..669033fc6524a 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts @@ -32,7 +32,7 @@ describe('getLatestMonitor', () => { }, }, size: 1, - _source: ['url', 'monitor', 'observer', '@timestamp', 'tls.*'], + _source: ['url', 'monitor', 'observer', '@timestamp', 'tls.*', 'http'], sort: { '@timestamp': { order: 'desc' }, }, diff --git a/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts b/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts index a58208fc2bb96..3b4aeaf92c508 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts @@ -45,7 +45,7 @@ export const getLatestMonitor: UMElasticsearchQueryFn { }); it('can navigate to cert page', async () => { - await uptimeService.cert.isUptimeDataMissing(); + await uptimeService.common.waitUntilDataIsLoaded(); await uptimeService.cert.hasViewCertButton(); await uptimeService.navigation.goToCertificates(); }); diff --git a/x-pack/test/functional/apps/uptime/index.ts b/x-pack/test/functional/apps/uptime/index.ts index 6b2b61cba2b64..261f685eeb9cc 100644 --- a/x-pack/test/functional/apps/uptime/index.ts +++ b/x-pack/test/functional/apps/uptime/index.ts @@ -56,6 +56,10 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => { loadTestFile(require.resolve('./certificates')); }); + describe('with generated data but no data reset', () => { + loadTestFile(require.resolve('./ping_redirects')); + }); + describe('with real-world data', () => { before(async () => { await esArchiver.unload(ARCHIVE); diff --git a/x-pack/test/functional/apps/uptime/locations.ts b/x-pack/test/functional/apps/uptime/locations.ts index 8aefca6a70195..6bfa19c6ef578 100644 --- a/x-pack/test/functional/apps/uptime/locations.ts +++ b/x-pack/test/functional/apps/uptime/locations.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import moment from 'moment'; import { makeChecksWithStatus } from '../../../api_integration/apis/uptime/rest/helper/make_checks'; import { FtrProviderContext } from '../../ftr_provider_context'; @@ -40,8 +39,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }; describe('Observer location', () => { - const start = moment().subtract('15', 'm').toISOString(); - const end = moment().toISOString(); + const start = '~ 15 minutes ago'; + const end = 'now'; before(async () => { await addMonitorWithNoLocation(); diff --git a/x-pack/test/functional/apps/uptime/ping_redirects.ts b/x-pack/test/functional/apps/uptime/ping_redirects.ts new file mode 100644 index 0000000000000..b87e8c1748c82 --- /dev/null +++ b/x-pack/test/functional/apps/uptime/ping_redirects.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { makeChecksWithStatus } from '../../../api_integration/apis/uptime/rest/helper/make_checks'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const { uptime: uptimePage, header } = getPageObjects(['uptime', 'header']); + const uptime = getService('uptime'); + const esArchiver = getService('esArchiver'); + + const archive = 'uptime/blank'; + + const monitor = () => uptime.monitor; + + describe('Ping redirects', () => { + const start = '~ 15 minutes ago'; + const end = 'now'; + + const MONITOR_ID = 'redirect-testing-id'; + + before(async () => { + await esArchiver.loadIfNeeded(archive); + }); + + after('unload', async () => { + await esArchiver.unload(archive); + }); + + beforeEach(async () => { + await makeChecksWithStatus( + getService('legacyEs'), + MONITOR_ID, + 5, + 2, + 10000, + { + http: { + rtt: { total: { us: 157784 } }, + response: { + status_code: 200, + redirects: ['http://localhost:3000/first', 'https://www.washingtonpost.com/'], + body: { + bytes: 642102, + hash: '597a8cfb33ff8e09bff16283306553c3895282aaf5386e1843d466d44979e28a', + }, + }, + }, + }, + 'up' + ); + await delay(1000); + }); + + it('loads and goes to details page', async () => { + await uptime.navigation.goToUptime(); + await uptimePage.loadDataAndGoToMonitorPage(start, end, MONITOR_ID); + }); + + it('display redirect info in detail panel', async () => { + await header.waitUntilLoadingHasFinished(); + await monitor().hasRedirectInfo(); + }); + + it('displays redirects in ping list expand row', async () => { + await monitor().hasRedirectInfoInPingList(); + }); + }); +}; diff --git a/x-pack/test/functional/page_objects/uptime_page.ts b/x-pack/test/functional/page_objects/uptime_page.ts index 074a2d598be8a..8102d8b95680e 100644 --- a/x-pack/test/functional/page_objects/uptime_page.ts +++ b/x-pack/test/functional/page_objects/uptime_page.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../ftr_provider_context'; export function UptimePageProvider({ getPageObjects, getService }: FtrProviderContext) { - const pageObjects = getPageObjects(['common', 'timePicker']); + const pageObjects = getPageObjects(['common', 'timePicker', 'header']); const { common: commonService, monitor, navigation } = getService('uptime'); const retry = getService('retry'); @@ -42,6 +42,7 @@ export function UptimePageProvider({ getPageObjects, getService }: FtrProviderCo } public async loadDataAndGoToMonitorPage(dateStart: string, dateEnd: string, monitorId: string) { + await pageObjects.header.waitUntilLoadingHasFinished(); await this.setDateRange(dateStart, dateEnd); await navigation.goToMonitor(monitorId); } diff --git a/x-pack/test/functional/services/uptime/certificates.ts b/x-pack/test/functional/services/uptime/certificates.ts index 06de9be5af7e9..ab43604786282 100644 --- a/x-pack/test/functional/services/uptime/certificates.ts +++ b/x-pack/test/functional/services/uptime/certificates.ts @@ -24,14 +24,6 @@ export function UptimeCertProvider({ getService, getPageObjects }: FtrProviderCo }; return { - async isUptimeDataMissing() { - return retry.tryForTime(60 * 1000, async () => { - if (await testSubjects.exists('data-missing', { timeout: 0 })) { - await refreshApp(); - } - await testSubjects.missingOrFail('data-missing'); - }); - }, async hasViewCertButton() { return retry.tryForTime(15000, async () => { await testSubjects.existOrFail('uptimeCertificatesLink'); diff --git a/x-pack/test/functional/services/uptime/common.ts b/x-pack/test/functional/services/uptime/common.ts index 5f544b5e46010..13c9ead89d09d 100644 --- a/x-pack/test/functional/services/uptime/common.ts +++ b/x-pack/test/functional/services/uptime/common.ts @@ -91,5 +91,13 @@ export function UptimeCommonProvider({ getService }: FtrProviderContext) { 5000 ); }, + async waitUntilDataIsLoaded() { + return retry.tryForTime(60 * 1000, async () => { + if (await testSubjects.exists('data-missing')) { + await testSubjects.click('superDatePickerApplyTimeButton'); + } + await testSubjects.missingOrFail('data-missing'); + }); + }, }; } diff --git a/x-pack/test/functional/services/uptime/monitor.ts b/x-pack/test/functional/services/uptime/monitor.ts index 593950fbb7619..c45454e731696 100644 --- a/x-pack/test/functional/services/uptime/monitor.ts +++ b/x-pack/test/functional/services/uptime/monitor.ts @@ -7,11 +7,13 @@ import expect from '@kbn/expect/expect.js'; import { FtrProviderContext } from '../../ftr_provider_context'; -export function UptimeMonitorProvider({ getService }: FtrProviderContext) { +export function UptimeMonitorProvider({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const retry = getService('retry'); const find = getService('find'); + const PageObjects = getPageObjects(['header']); + return { async locationMissingExists() { return await testSubjects.existOrFail('xpack.uptime.locationMap.locationMissing', { @@ -56,5 +58,29 @@ export function UptimeMonitorProvider({ getService }: FtrProviderContext) { async toggleToMapView() { await testSubjects.click('uptimeMonitorToggleMapBtn'); }, + async hasRedirectInfo() { + return retry.tryForTime(30000, async () => { + await testSubjects.existOrFail('uptimeMonitorRedirectInfo'); + }); + }, + async expandPingRow() { + return retry.tryForTime( + 60 * 3000, + async () => { + await testSubjects.existOrFail('uptimePingListExpandBtn', { timeout: 5000 }); + await testSubjects.click('uptimePingListExpandBtn'); + }, + async () => { + await testSubjects.click('superDatePickerApplyTimeButton'); + await PageObjects.header.waitUntilLoadingHasFinished(); + } + ); + }, + async hasRedirectInfoInPingList() { + await this.expandPingRow(); + return retry.tryForTime(60 * 1000, async () => { + await testSubjects.existOrFail('uptimeMonitorPingListRedirectInfo'); + }); + }, }; }