[Uptime] Details page refactor for browser monitor (#84425)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Shahzad 2020-12-07 14:35:19 +01:00 committed by GitHub
parent 08680882ce
commit e476baf276
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
73 changed files with 1349 additions and 1143 deletions

View file

@ -27,6 +27,8 @@ export {
METRIC_TYPE,
} from './hooks/use_track_metric';
export { useFetcher } from './hooks/use_fetcher';
export * from './typings';
export { useChartTheme } from './hooks/use_chart_theme';

View file

@ -20558,8 +20558,6 @@
"xpack.uptime.featureRegistry.uptimeFeatureName": "アップタイム",
"xpack.uptime.filterBar.ariaLabel": "概要ページのインプットフィルター基準",
"xpack.uptime.filterBar.filterAllLabel": "すべて",
"xpack.uptime.filterBar.filterDownLabel": "ダウン",
"xpack.uptime.filterBar.filterUpLabel": "アップ",
"xpack.uptime.filterBar.options.location.name": "場所",
"xpack.uptime.filterBar.options.portLabel": "ポート",
"xpack.uptime.filterBar.options.schemeLabel": "スキーム",
@ -20660,8 +20658,6 @@
"xpack.uptime.monitorList.tlsColumnLabel": "TLS証明書",
"xpack.uptime.monitorList.viewCertificateTitle": "証明書ステータス",
"xpack.uptime.monitorStatusBar.durationTextAriaLabel": "ミリ秒単位の監視時間",
"xpack.uptime.monitorStatusBar.healthStatusMessage.downLabel": "ダウン",
"xpack.uptime.monitorStatusBar.healthStatusMessage.upLabel": "アップ",
"xpack.uptime.monitorStatusBar.healthStatusMessageAriaLabel": "監視ステータス",
"xpack.uptime.monitorStatusBar.loadingMessage": "読み込み中…",
"xpack.uptime.monitorStatusBar.locations.oneLocStatus": "{loc}場所での{status}",
@ -20714,17 +20710,10 @@
"xpack.uptime.pingList.expandedRow.truncated": "初めの {contentBytes} バイトを表示中。",
"xpack.uptime.pingList.expandRow": "拡張",
"xpack.uptime.pingList.ipAddressColumnLabel": "IP",
"xpack.uptime.pingList.locationLabel": "場所",
"xpack.uptime.pingList.locationNameColumnLabel": "場所",
"xpack.uptime.pingList.recencyMessage": "最終確認 {fromNow}",
"xpack.uptime.pingList.responseCodeColumnLabel": "応答コード",
"xpack.uptime.pingList.statusColumnHealthDownLabel": "ダウン",
"xpack.uptime.pingList.statusColumnHealthUpLabel": "アップ",
"xpack.uptime.pingList.statusColumnLabel": "ステータス",
"xpack.uptime.pingList.statusLabel": "ステータス",
"xpack.uptime.pingList.statusOptions.allStatusOptionLabel": "すべて",
"xpack.uptime.pingList.statusOptions.downStatusOptionLabel": "ダウン",
"xpack.uptime.pingList.statusOptions.upStatusOptionLabel": "アップ",
"xpack.uptime.pluginDescription": "アップタイム監視",
"xpack.uptime.settings.blank.error": "空白にすることはできません。",
"xpack.uptime.settings.blankNumberField.error": "数値でなければなりません。",
@ -20737,17 +20726,13 @@
"xpack.uptime.settings.saveSuccess": "設定が保存されました。",
"xpack.uptime.settingsBreadcrumbText": "設定",
"xpack.uptime.snapshot.donutChart.ariaLabel": "現在のステータスを表す円グラフ、{total}個中{down}個のモニターがダウンしています。",
"xpack.uptime.snapshot.donutChart.legend.downRowLabel": "ダウン",
"xpack.uptime.snapshot.donutChart.legend.upRowLabel": "アップ",
"xpack.uptime.snapshot.monitor": "監視",
"xpack.uptime.snapshot.monitors": "監視",
"xpack.uptime.snapshot.noDataDescription": "選択した時間範囲に ping はありません。",
"xpack.uptime.snapshot.noDataTitle": "利用可能な ping データがありません",
"xpack.uptime.snapshot.pingsOverTimeTitle": "一定時間のピング",
"xpack.uptime.snapshotHistogram.description": "{startTime} から {endTime} までの期間のアップタイムステータスを表示する棒グラフです。",
"xpack.uptime.snapshotHistogram.series.downLabel": "ダウン",
"xpack.uptime.snapshotHistogram.series.pings": "モニター接続確認",
"xpack.uptime.snapshotHistogram.series.upLabel": "アップ",
"xpack.uptime.snapshotHistogram.xAxisId": "ピングX軸",
"xpack.uptime.snapshotHistogram.yAxis.title": "ピング",
"xpack.uptime.snapshotHistogram.yAxisId": "ピングY軸",

View file

@ -20578,8 +20578,6 @@
"xpack.uptime.featureRegistry.uptimeFeatureName": "运行时间",
"xpack.uptime.filterBar.ariaLabel": "概览页面的输入筛选条件",
"xpack.uptime.filterBar.filterAllLabel": "全部",
"xpack.uptime.filterBar.filterDownLabel": "关闭",
"xpack.uptime.filterBar.filterUpLabel": "运行",
"xpack.uptime.filterBar.options.location.name": "位置",
"xpack.uptime.filterBar.options.portLabel": "端口",
"xpack.uptime.filterBar.options.schemeLabel": "方案",
@ -20680,8 +20678,6 @@
"xpack.uptime.monitorList.tlsColumnLabel": "TLS 证书",
"xpack.uptime.monitorList.viewCertificateTitle": "证书状态",
"xpack.uptime.monitorStatusBar.durationTextAriaLabel": "监测持续时间(毫秒)",
"xpack.uptime.monitorStatusBar.healthStatusMessage.downLabel": "关闭",
"xpack.uptime.monitorStatusBar.healthStatusMessage.upLabel": "运行",
"xpack.uptime.monitorStatusBar.healthStatusMessageAriaLabel": "检测状态",
"xpack.uptime.monitorStatusBar.loadingMessage": "正在加载……",
"xpack.uptime.monitorStatusBar.locations.oneLocStatus": "在 {loc} 位置处于 {status}",
@ -20734,17 +20730,10 @@
"xpack.uptime.pingList.expandedRow.truncated": "显示前 {contentBytes} 字节。",
"xpack.uptime.pingList.expandRow": "展开",
"xpack.uptime.pingList.ipAddressColumnLabel": "IP",
"xpack.uptime.pingList.locationLabel": "位置",
"xpack.uptime.pingList.locationNameColumnLabel": "位置",
"xpack.uptime.pingList.recencyMessage": "{fromNow}已检查",
"xpack.uptime.pingList.responseCodeColumnLabel": "响应代码",
"xpack.uptime.pingList.statusColumnHealthDownLabel": "关闭",
"xpack.uptime.pingList.statusColumnHealthUpLabel": "运行",
"xpack.uptime.pingList.statusColumnLabel": "状态",
"xpack.uptime.pingList.statusLabel": "状态",
"xpack.uptime.pingList.statusOptions.allStatusOptionLabel": "全部",
"xpack.uptime.pingList.statusOptions.downStatusOptionLabel": "关闭",
"xpack.uptime.pingList.statusOptions.upStatusOptionLabel": "运行",
"xpack.uptime.pluginDescription": "运行时间监测",
"xpack.uptime.settings.blank.error": "不能为空。",
"xpack.uptime.settings.blankNumberField.error": "必须为数字。",
@ -20757,17 +20746,13 @@
"xpack.uptime.settings.saveSuccess": "设置已保存!",
"xpack.uptime.settingsBreadcrumbText": "设置",
"xpack.uptime.snapshot.donutChart.ariaLabel": "显示当前状态的饼图。{total} 个监测中有 {down} 个已关闭。",
"xpack.uptime.snapshot.donutChart.legend.downRowLabel": "关闭",
"xpack.uptime.snapshot.donutChart.legend.upRowLabel": "运行",
"xpack.uptime.snapshot.monitor": "监测",
"xpack.uptime.snapshot.monitors": "监测",
"xpack.uptime.snapshot.noDataDescription": "选定的时间范围中没有 ping。",
"xpack.uptime.snapshot.noDataTitle": "没有可用的 ping 数据",
"xpack.uptime.snapshot.pingsOverTimeTitle": "时移 Ping 数",
"xpack.uptime.snapshotHistogram.description": "显示从 {startTime} 到 {endTime} 的运行时间时移状态的条形图。",
"xpack.uptime.snapshotHistogram.series.downLabel": "关闭",
"xpack.uptime.snapshotHistogram.series.pings": "监测 Ping",
"xpack.uptime.snapshotHistogram.series.upLabel": "运行",
"xpack.uptime.snapshotHistogram.xAxisId": "Ping X 轴",
"xpack.uptime.snapshotHistogram.yAxis.title": "Ping",
"xpack.uptime.snapshotHistogram.yAxisId": "Ping Y 轴",

View file

@ -38,6 +38,5 @@ export const CLIENT_DEFAULTS = {
MONITOR_LIST_SORT_DIRECTION: 'asc',
MONITOR_LIST_SORT_FIELD: 'monitor_id',
SEARCH: '',
SELECTED_PING_LIST_STATUS: '',
STATUS_FILTER: '',
};

View file

@ -15,6 +15,16 @@ export const CERTIFICATES_ROUTE = '/certificates';
export enum STATUS {
UP = 'up',
DOWN = 'down',
COMPLETE = 'complete',
FAILED = 'failed',
SKIPPED = 'skipped',
}
export enum MONITOR_TYPES {
HTTP = 'http',
TCP = 'tcp',
ICMP = 'icmp',
BROWSER = 'browser',
}
export const ML_JOB_ID = 'high_latency_by_geo';

View file

@ -232,6 +232,7 @@ export const PingType = t.intersection([
full: t.string,
port: t.number,
scheme: t.string,
path: t.string,
}),
service: t.partial({
name: t.string,
@ -280,7 +281,6 @@ export const makePing = (f: {
export const PingsResponseType = t.type({
total: t.number,
locations: t.array(t.string),
pings: t.array(PingType),
});
@ -293,7 +293,7 @@ export const GetPingsParamsType = t.intersection([
t.partial({
index: t.number,
size: t.number,
location: t.string,
locations: t.string,
monitorId: t.string,
sort: t.string,
status: t.string,

View file

@ -6,11 +6,6 @@ exports[`LocationLink component renders a help link when location not present 1`
target="_blank"
>
Add location
 
<EuiIcon
size="s"
type="popout"
/>
</EuiLink>
`;

View file

@ -491,7 +491,7 @@ exports[`DonutChart component renders a donut chart 1`] = `
<span
class="euiFlexItem euiFlexItem--flexGrowZero c1"
>
Down
Up
</span>
<span
class="euiFlexItem c2"
@ -532,7 +532,7 @@ exports[`DonutChart component renders a donut chart 1`] = `
<span
class="euiFlexItem euiFlexItem--flexGrowZero c1"
>
Up
Down
</span>
<span
class="euiFlexItem c2"
@ -644,7 +644,7 @@ exports[`DonutChart component renders a green check when all monitors are up 1`]
<span
class="euiFlexItem euiFlexItem--flexGrowZero c2"
>
Down
Up
</span>
<span
class="euiFlexItem c3"
@ -685,7 +685,7 @@ exports[`DonutChart component renders a green check when all monitors are up 1`]
<span
class="euiFlexItem euiFlexItem--flexGrowZero c2"
>
Up
Down
</span>
<span
class="euiFlexItem c3"

View file

@ -6,7 +6,7 @@ exports[`DonutChartLegend applies valid props as expected 1`] = `
color="#bd271e"
content={23}
data-test-subj="xpack.uptime.snapshot.donutChart.down"
message="Down"
message="Up"
/>
<EuiSpacer
size="m"
@ -15,7 +15,7 @@ exports[`DonutChartLegend applies valid props as expected 1`] = `
color="#d3dae6"
content={45}
data-test-subj="xpack.uptime.snapshot.donutChart.up"
message="Up"
message="Down"
/>
</styled.div>
`;

View file

@ -4,12 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import { EuiSpacer } from '@elastic/eui';
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';
const LegendContainer = styled.div`
max-width: 150px;
@ -34,18 +34,14 @@ export const DonutChartLegend = ({ down, up }: Props) => {
<DonutChartLegendRow
color={danger}
content={down}
message={i18n.translate('xpack.uptime.snapshot.donutChart.legend.downRowLabel', {
defaultMessage: 'Down',
})}
message={STATUS_UP_LABEL}
data-test-subj={'xpack.uptime.snapshot.donutChart.down'}
/>
<EuiSpacer size="m" />
<DonutChartLegendRow
color={gray}
content={up}
message={i18n.translate('xpack.uptime.snapshot.donutChart.legend.upRowLabel', {
defaultMessage: 'Up',
})}
message={STATUS_DOWN_LABEL}
data-test-subj={'xpack.uptime.snapshot.donutChart.up'}
/>
</LegendContainer>

View file

@ -28,6 +28,7 @@ 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';
export interface PingHistogramComponentProps {
/**
@ -84,14 +85,6 @@ export const PingHistogramComponent: React.FC<PingHistogramComponentProps> = ({
} else {
const { histogram, minInterval } = data;
const downSpecId = i18n.translate('xpack.uptime.snapshotHistogram.series.downLabel', {
defaultMessage: 'Down',
});
const upMonitorsId = i18n.translate('xpack.uptime.snapshotHistogram.series.upLabel', {
defaultMessage: 'Up',
});
const onBrushEnd: BrushEndListener = ({ x }) => {
if (!x) {
return;
@ -113,8 +106,8 @@ export const PingHistogramComponent: React.FC<PingHistogramComponentProps> = ({
histogram.forEach(({ x, upCount, downCount }) => {
barData.push(
{ x, y: downCount ?? 0, type: downSpecId },
{ x, y: upCount ?? 0, type: upMonitorsId }
{ x, y: downCount ?? 0, type: STATUS_DOWN_LABEL },
{ x, y: upCount ?? 0, type: STATUS_UP_LABEL }
);
});
@ -168,7 +161,7 @@ export const PingHistogramComponent: React.FC<PingHistogramComponentProps> = ({
<BarSeries
color={[danger, gray]}
data={barData}
id={downSpecId}
id={STATUS_DOWN_LABEL}
name={i18n.translate('xpack.uptime.snapshotHistogram.series.pings', {
defaultMessage: 'Monitor Pings',
})}

View file

@ -6,7 +6,7 @@
import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiIcon, EuiLink, EuiText } from '@elastic/eui';
import { EuiLink, EuiText } from '@elastic/eui';
interface LocationLinkProps {
location?: string | null;
@ -32,8 +32,6 @@ export const LocationLink = ({ location, textSize }: LocationLinkProps) => {
description:
'Text that instructs the user to navigate to our docs to add a geographic location to their data',
})}
&nbsp;
<EuiIcon size="s" type="popout" />
</EuiLink>
);
};

View file

@ -9,3 +9,25 @@ import { i18n } from '@kbn/i18n';
export const URL_LABEL = i18n.translate('xpack.uptime.monitorList.table.url.name', {
defaultMessage: 'Url',
});
export const STATUS_UP_LABEL = i18n.translate('xpack.uptime.monitorList.statusColumn.upLabel', {
defaultMessage: 'Up',
});
export const STATUS_DOWN_LABEL = i18n.translate('xpack.uptime.monitorList.statusColumn.downLabel', {
defaultMessage: 'Down',
});
export const STATUS_COMPLETE_LABEL = i18n.translate(
'xpack.uptime.monitorList.statusColumn.completeLabel',
{
defaultMessage: 'Complete',
}
);
export const STATUS_FAILED_LABEL = i18n.translate(
'xpack.uptime.monitorList.statusColumn.failedLabel',
{
defaultMessage: 'Failed',
}
);

View file

@ -2,92 +2,7 @@
exports[`PingList component renders sorted list without errors 1`] = `
<EuiPanel>
<EuiTitle
size="s"
>
<h4>
<FormattedMessage
defaultMessage="History"
id="xpack.uptime.pingList.checkHistoryTitle"
values={Object {}}
/>
</h4>
</EuiTitle>
<EuiSpacer
size="s"
/>
<EuiFlexGroup
justifyContent="spaceBetween"
>
<EuiFlexItem
grow={false}
>
<EuiFormRow
aria-label="Status"
describedByIds={Array []}
display="row"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
label="Status"
labelType="label"
>
<EuiSelect
aria-label="Status"
data-test-subj="xpack.uptime.pingList.statusSelect"
onChange={[Function]}
options={
Array [
Object {
"data-test-subj": "xpack.uptime.pingList.statusOptions.all",
"text": "All",
"value": "",
},
Object {
"data-test-subj": "xpack.uptime.pingList.statusOptions.up",
"text": "Up",
"value": "up",
},
Object {
"data-test-subj": "xpack.uptime.pingList.statusOptions.down",
"text": "Down",
"value": "down",
},
]
}
value=""
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem>
<EuiFormRow
aria-label="Location"
describedByIds={Array []}
display="row"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
label="Location"
labelType="label"
>
<EuiSelect
aria-label="Location"
data-test-subj="xpack.uptime.pingList.locationSelect"
onChange={[Function]}
options={
Array [
Object {
"data-test-subj": "xpack.uptime.pingList.locationOptions.all",
"text": "All",
"value": "",
},
]
}
value=""
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
<PingListHeader />
<EuiSpacer
size="s"
/>
@ -112,24 +27,16 @@ exports[`PingList component renders sorted list without errors 1`] = `
"name": "IP",
},
Object {
"align": "right",
"align": "center",
"field": "monitor.duration.us",
"name": "Duration",
"render": [Function],
},
Object {
"align": "right",
"field": "error.type",
"name": "Error type",
"render": [Function],
},
Object {
"align": "right",
"field": "http.response.status_code",
"name": <styled.span>
Response code
</styled.span>,
"name": "Error",
"render": [Function],
"width": "30%",
},
Object {
"align": "right",
@ -181,148 +88,6 @@ exports[`PingList component renders sorted list without errors 1`] = `
},
"timestamp": "2019-01-28T17:47:09.075Z",
},
Object {
"docId": "fejjio21",
"monitor": Object {
"duration": Object {
"us": 1452,
},
"id": "auto-tcp-0X81440A68E839814D",
"ip": "127.0.0.1",
"name": "",
"status": "up",
"type": "tcp",
},
"timestamp": "2019-01-28T17:47:06.077Z",
},
Object {
"docId": "fewzio21",
"error": Object {
"message": "dial tcp 127.0.0.1:9200: connect: connection refused",
"type": "io",
},
"monitor": Object {
"duration": Object {
"us": 1094,
},
"id": "auto-tcp-0X81440A68E839814E",
"ip": "127.0.0.1",
"name": "",
"status": "down",
"type": "tcp",
},
"timestamp": "2019-01-28T17:47:07.075Z",
},
Object {
"docId": "fewpi321",
"error": Object {
"message": "Get http://localhost:12349/: dial tcp 127.0.0.1:12349: connect: connection refused",
"type": "io",
},
"monitor": Object {
"duration": Object {
"us": 1597,
},
"id": "auto-http-0X3675F89EF061209G",
"ip": "127.0.0.1",
"name": "",
"status": "down",
"type": "http",
},
"timestamp": "2019-01-28T17:47:07.074Z",
},
Object {
"docId": "0ewjio21",
"error": Object {
"message": "dial tcp 127.0.0.1:9200: connect: connection refused",
"type": "io",
},
"monitor": Object {
"duration": Object {
"us": 1699,
},
"id": "auto-tcp-0X81440A68E839814H",
"ip": "127.0.0.1",
"name": "",
"status": "down",
"type": "tcp",
},
"timestamp": "2019-01-28T17:47:18.080Z",
},
Object {
"docId": "3ewjio21",
"error": Object {
"message": "dial tcp 127.0.0.1:9200: connect: connection refused",
"type": "io",
},
"monitor": Object {
"duration": Object {
"us": 5384,
},
"id": "auto-tcp-0X81440A68E839814I",
"ip": "127.0.0.1",
"name": "",
"status": "down",
"type": "tcp",
},
"timestamp": "2019-01-28T17:47:19.076Z",
},
Object {
"docId": "fewjip21",
"error": Object {
"message": "Get http://localhost:12349/: dial tcp 127.0.0.1:12349: connect: connection refused",
"type": "io",
},
"monitor": Object {
"duration": Object {
"us": 5397,
},
"id": "auto-http-0X3675F89EF061209J",
"ip": "127.0.0.1",
"name": "",
"status": "down",
"type": "http",
},
"timestamp": "2019-01-28T17:47:19.076Z",
},
Object {
"docId": "fewjio21",
"http": Object {
"response": Object {
"status_code": 200,
},
},
"monitor": Object {
"duration": Object {
"us": 127511,
},
"id": "auto-tcp-0X81440A68E839814C",
"ip": "172.217.7.4",
"name": "",
"status": "up",
"type": "http",
},
"timestamp": "2019-01-28T17:47:19.077Z",
},
Object {
"docId": "fewjik81",
"http": Object {
"response": Object {
"status_code": 200,
},
},
"monitor": Object {
"duration": Object {
"us": 287543,
},
"id": "auto-http-0X131221E73F825974",
"ip": "192.30.253.112",
"name": "",
"status": "up",
"type": "http",
},
"timestamp": "2019-01-28T17:47:19.077Z",
},
]
}
loading={false}
@ -339,11 +104,11 @@ exports[`PingList component renders sorted list without errors 1`] = `
50,
100,
],
"totalItemCount": 10,
"totalItemCount": 9231,
}
}
responsive={true}
tableLayout="fixed"
tableLayout="auto"
/>
</EuiPanel>
`;

View file

@ -6,17 +6,21 @@
import React from 'react';
import { shallowWithIntl } from '@kbn/test/jest';
import { PingListComponent, rowShouldExpand, toggleDetails } from '../ping_list';
import { PingList } from '../ping_list';
import { Ping, PingsResponse } from '../../../../../common/runtime_types';
import { ExpandedRowMap } from '../../../overview/monitor_list/types';
import { rowShouldExpand, toggleDetails } from '../columns/expand_row';
import * as pingListHook from '../use_pings';
import { mockReduxHooks } from '../../../../lib/helper/test_helpers';
mockReduxHooks();
describe('PingList component', () => {
let response: PingsResponse;
beforeEach(() => {
beforeAll(() => {
response = {
total: 9231,
locations: ['nyc'],
pings: [
{
docId: 'fewjio21',
@ -50,147 +54,19 @@ describe('PingList component', () => {
type: 'tcp',
},
},
{
docId: 'fejjio21',
timestamp: '2019-01-28T17:47:06.077Z',
monitor: {
duration: { us: 1452 },
id: 'auto-tcp-0X81440A68E839814D',
ip: '127.0.0.1',
name: '',
status: 'up',
type: 'tcp',
},
},
{
docId: 'fewzio21',
timestamp: '2019-01-28T17:47:07.075Z',
error: {
message: 'dial tcp 127.0.0.1:9200: connect: connection refused',
type: 'io',
},
monitor: {
duration: { us: 1094 },
id: 'auto-tcp-0X81440A68E839814E',
ip: '127.0.0.1',
name: '',
status: 'down',
type: 'tcp',
},
},
{
docId: 'fewpi321',
timestamp: '2019-01-28T17:47:07.074Z',
error: {
message:
'Get http://localhost:12349/: dial tcp 127.0.0.1:12349: connect: connection refused',
type: 'io',
},
monitor: {
duration: { us: 1597 },
id: 'auto-http-0X3675F89EF061209G',
ip: '127.0.0.1',
name: '',
status: 'down',
type: 'http',
},
},
{
docId: '0ewjio21',
timestamp: '2019-01-28T17:47:18.080Z',
error: {
message: 'dial tcp 127.0.0.1:9200: connect: connection refused',
type: 'io',
},
monitor: {
duration: { us: 1699 },
id: 'auto-tcp-0X81440A68E839814H',
ip: '127.0.0.1',
name: '',
status: 'down',
type: 'tcp',
},
},
{
docId: '3ewjio21',
timestamp: '2019-01-28T17:47:19.076Z',
error: {
message: 'dial tcp 127.0.0.1:9200: connect: connection refused',
type: 'io',
},
monitor: {
duration: { us: 5384 },
id: 'auto-tcp-0X81440A68E839814I',
ip: '127.0.0.1',
name: '',
status: 'down',
type: 'tcp',
},
},
{
docId: 'fewjip21',
timestamp: '2019-01-28T17:47:19.076Z',
error: {
message:
'Get http://localhost:12349/: dial tcp 127.0.0.1:12349: connect: connection refused',
type: 'io',
},
monitor: {
duration: { us: 5397 },
id: 'auto-http-0X3675F89EF061209J',
ip: '127.0.0.1',
name: '',
status: 'down',
type: 'http',
},
},
{
docId: 'fewjio21',
timestamp: '2019-01-28T17:47:19.077Z',
http: { response: { status_code: 200 } },
monitor: {
duration: { us: 127511 },
id: 'auto-tcp-0X81440A68E839814C',
ip: '172.217.7.4',
name: '',
status: 'up',
type: 'http',
},
},
{
docId: 'fewjik81',
timestamp: '2019-01-28T17:47:19.077Z',
http: { response: { status_code: 200 } },
monitor: {
duration: { us: 287543 },
id: 'auto-http-0X131221E73F825974',
ip: '192.30.253.112',
name: '',
status: 'up',
type: 'http',
},
},
],
};
jest.spyOn(pingListHook, 'usePingsList').mockReturnValue({
...response,
error: undefined,
loading: false,
failedSteps: { steps: [], checkGroup: '1-f-4d-4f' },
});
});
it('renders sorted list without errors', () => {
const component = shallowWithIntl(
<PingListComponent
dateRange={{
from: 'now-15m',
to: 'now',
}}
getPings={jest.fn()}
pruneJourneysCallback={jest.fn()}
lastRefresh={123}
loading={false}
locations={[]}
monitorId="foo"
pings={response.pings}
total={10}
/>
);
const component = shallowWithIntl(<PingList />);
expect(component).toMatchSnapshot();
});

View file

@ -0,0 +1,182 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Ping Timestamp component render without errors 1`] = `
.c0 {
position: relative;
}
.c0 figure.euiImage div.stepArrowsFullScreen {
display: none;
}
.c0 figure.euiImage-isFullScreen div.stepArrowsFullScreen {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
}
.c0 div.stepArrows {
display: none;
}
.c0:hover div.stepArrows {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
}
.c1 {
width: 120px;
text-align: center;
border: 1px solid #d3dae6;
}
<div
class="c0"
>
<div
class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
class="euiFlexItem"
>
<div
class="euiText euiText--medium c1"
>
<strong>
No image available
</strong>
</div>
</div>
<div
class="euiFlexItem"
>
<div
class="stepArrowsFullScreen"
style="position:absolute;bottom:32px;width:100%"
/>
<a
class="euiLink euiLink--primary eui-textNoWrap"
href="/step/details"
rel="noreferrer"
>
Nov 26, 2020 10:28:56 AM
</a>
<div
class="euiSpacer euiSpacer--s"
/>
</div>
</div>
<div
class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive stepArrows"
style="position:absolute;bottom:0;left:30px"
>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
>
<button
aria-label="Next"
class="euiButtonIcon euiButtonIcon--subdued"
disabled=""
type="button"
>
<span
aria-hidden="true"
class="euiButtonIcon__icon"
data-euiicon-type="arrowLeft"
/>
</button>
</div>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
>
<button
aria-label="Next"
class="euiButtonIcon euiButtonIcon--subdued"
type="button"
>
<span
aria-hidden="true"
class="euiButtonIcon__icon"
data-euiicon-type="arrowRight"
/>
</button>
</div>
</div>
</div>
`;
exports[`Ping Timestamp component shallow render without errors 1`] = `
<styled.div>
<EuiFlexGroup
alignItems="center"
gutterSize="s"
>
<EuiFlexItem>
<NoImageAvailable />
</EuiFlexItem>
<EuiFlexItem>
<div
className="stepArrowsFullScreen"
style={
Object {
"bottom": 32,
"position": "absolute",
"width": "100%",
}
}
/>
<EuiLink
className="eui-textNoWrap"
href="/step/details"
>
Nov 26, 2020 10:28:56 AM
</EuiLink>
<EuiSpacer
size="s"
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup
alignItems="center"
className="stepArrows"
gutterSize="s"
style={
Object {
"bottom": 0,
"left": 30,
"position": "absolute",
}
}
>
<EuiFlexItem
grow={false}
>
<EuiButtonIcon
aria-label="Next"
color="subdued"
disabled={true}
iconType="arrowLeft"
onClick={[Function]}
size="s"
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiButtonIcon
aria-label="Next"
color="subdued"
disabled={false}
iconType="arrowRight"
onClick={[Function]}
size="s"
/>
</EuiFlexItem>
</EuiFlexGroup>
</styled.div>
`;

View file

@ -0,0 +1,67 @@
/*
* 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 { shallowWithIntl, renderWithIntl } from '@kbn/test/jest';
import { PingTimestamp } from '../ping_timestamp';
import { mockReduxHooks } from '../../../../../lib/helper/test_helpers';
import { Ping } from '../../../../../../common/runtime_types/ping';
import { EuiThemeProvider } from '../../../../../../../observability/public';
mockReduxHooks();
describe('Ping Timestamp component', () => {
let response: Ping;
beforeAll(() => {
response = {
ecs: { version: '1.6.0' },
agent: {
ephemeral_id: '52ce1110-464f-4d74-b94c-3c051bf12589',
id: '3ebcd3c2-f5c3-499e-8d86-80f98e5f4c08',
name: 'docker-desktop',
type: 'heartbeat',
version: '7.10.0',
hostname: 'docker-desktop',
},
monitor: {
status: 'up',
check_group: 'f58a484f-2ffb-11eb-9b35-025000000001',
duration: { us: 1528598 },
id: 'basic addition and completion of single task',
name: 'basic addition and completion of single task',
type: 'browser',
timespan: { lt: '2020-11-26T15:29:56.820Z', gte: '2020-11-26T15:28:56.820Z' },
},
url: {
full: 'file:///opt/elastic-synthetics/examples/todos/app/index.html',
scheme: 'file',
domain: '',
path: '/opt/elastic-synthetics/examples/todos/app/index.html',
},
synthetics: { type: 'heartbeat/summary' },
summary: { up: 1, down: 0 },
timestamp: '2020-11-26T15:28:56.896Z',
docId: '0WErBXYB0mvWTKLO-yQm',
};
});
it('shallow render without errors', () => {
const component = shallowWithIntl(
<PingTimestamp ping={response} timestamp={response.timestamp} />
);
expect(component).toMatchSnapshot();
});
it('render without errors', () => {
const component = renderWithIntl(
<EuiThemeProvider darkMode={false}>
<PingTimestamp ping={response} timestamp={response.timestamp} />
</EuiThemeProvider>
);
expect(component).toMatchSnapshot();
});
});

View file

@ -0,0 +1,60 @@
/*
* 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 { EuiButtonIcon } from '@elastic/eui';
import { Ping } from '../../../../../common/runtime_types/ping';
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;
const isBrowserMonitor = item.monitor.type === 'browser';
return errorPresent || httpBodyPresent || isBrowserMonitor;
}
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)}
disabled={!rowShouldExpand(item)}
aria-label={
expandedRows[item.docId]
? i18n.translate('xpack.uptime.pingList.collapseRow', {
defaultMessage: 'Collapse',
})
: i18n.translate('xpack.uptime.pingList.expandRow', { defaultMessage: 'Expand' })
}
iconType={expandedRows[item.docId] ? 'arrowUp' : 'arrowDown'}
/>
);
};

View file

@ -0,0 +1,28 @@
/*
* 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 { Ping, SyntheticsJourneyApiResponse } from '../../../../../common/runtime_types/ping';
interface Props {
ping: Ping;
failedSteps?: SyntheticsJourneyApiResponse;
}
export const FailedStep = ({ ping, failedSteps }: Props) => {
const thisFailedStep = failedSteps?.steps?.find(
(fs) => fs.monitor.check_group === ping.monitor.check_group
);
if (!thisFailedStep) {
return <>--</>;
}
return (
<div>
{thisFailedStep.synthetics?.step?.index}. {thisFailedStep.synthetics?.step?.name}
</div>
);
};

View file

@ -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;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import styled from 'styled-components';
import { Ping } from '../../../../../common/runtime_types/ping';
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>
);
};

View file

@ -0,0 +1,67 @@
/*
* 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, { useContext } from 'react';
import { i18n } from '@kbn/i18n';
import moment from 'moment';
import { EuiBadge, EuiSpacer, EuiText } from '@elastic/eui';
import { Ping } from '../../../../../common/runtime_types/ping';
import { MONITOR_TYPES, STATUS } from '../../../../../common/constants';
import { UptimeThemeContext } from '../../../../contexts';
import {
STATUS_COMPLETE_LABEL,
STATUS_DOWN_LABEL,
STATUS_FAILED_LABEL,
STATUS_UP_LABEL,
} from '../../../common/translations';
interface Props {
pingStatus: string;
item: Ping;
}
const getPingStatusLabel = (status: string, ping: Ping) => {
if (ping.monitor.type === MONITOR_TYPES.BROWSER) {
return status === 'up' ? STATUS_COMPLETE_LABEL : STATUS_FAILED_LABEL;
}
return status === 'up' ? STATUS_UP_LABEL : STATUS_DOWN_LABEL;
};
export const PingStatusColumn = ({ pingStatus, item }: Props) => {
const {
colors: { dangerBehindText },
} = useContext(UptimeThemeContext);
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.uptime.pingList.ping-${item.docId}`}>
<EuiBadge
className="eui-textCenter"
color={pingStatus === STATUS.UP ? 'secondary' : dangerBehindText}
>
{getPingStatusLabel(pingStatus, item)}
</EuiBadge>
<EuiSpacer size="xs" />
<EuiText size="xs" color="subdued">
{i18n.translate('xpack.uptime.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>
);
};

View file

@ -0,0 +1,213 @@
/*
* 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, { useContext, useEffect, useState } from 'react';
import {
EuiButtonIcon,
EuiFlexGroup,
EuiFlexItem,
EuiImage,
EuiLink,
EuiSpacer,
EuiText,
} from '@elastic/eui';
import useIntersection from 'react-use/lib/useIntersection';
import moment from 'moment';
import styled from 'styled-components';
import { FormattedMessage } from '@kbn/i18n/react';
import { Ping } from '../../../../../common/runtime_types/ping';
import { getShortTimeStamp } from '../../../overview/monitor_list/columns/monitor_status_column';
import { euiStyled, useFetcher } from '../../../../../../observability/public';
import { getJourneyScreenshot } from '../../../../state/api/journey';
import { UptimeSettingsContext } from '../../../../contexts';
const StepImage = styled(EuiImage)`
&&& {
display: flex;
figcaption {
white-space: nowrap;
align-self: center;
margin-left: 8px;
margin-top: 8px;
}
}
`;
const StepDiv = styled.div`
figure.euiImage {
div.stepArrowsFullScreen {
display: none;
}
}
figure.euiImage-isFullScreen {
div.stepArrowsFullScreen {
display: flex;
}
}
position: relative;
div.stepArrows {
display: none;
}
:hover {
div.stepArrows {
display: flex;
}
}
`;
interface Props {
timestamp: string;
ping: Ping;
}
export const PingTimestamp = ({ timestamp, ping }: Props) => {
const [stepNo, setStepNo] = useState(1);
const [stepImages, setStepImages] = useState<string[]>([]);
const intersectionRef = React.useRef(null);
const { basePath } = useContext(UptimeSettingsContext);
const imgPath = basePath + `/api/uptime/journey/screenshot/${ping.monitor.check_group}/${stepNo}`;
const intersection = useIntersection(intersectionRef, {
root: null,
rootMargin: '0px',
threshold: 1,
});
const { data } = useFetcher(() => {
if (intersection && intersection.intersectionRatio === 1 && !stepImages[stepNo - 1])
return getJourneyScreenshot(imgPath);
}, [intersection?.intersectionRatio, stepNo]);
useEffect(() => {
if (data) {
setStepImages((prevState) => [...prevState, data?.src]);
}
}, [data]);
const imgSrc = stepImages[stepNo] || data?.src;
const ImageCaption = (
<>
<div
className="stepArrowsFullScreen"
style={{ position: 'absolute', bottom: 32, width: '100%' }}
>
{imgSrc && (
<EuiFlexGroup gutterSize="s" alignItems="center" justifyContent="center">
<EuiFlexItem grow={false}>
<EuiButtonIcon
disabled={stepNo === 1}
size="m"
onClick={() => {
setStepNo(stepNo - 1);
}}
iconType="arrowLeft"
aria-label="Next"
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText>
Step:{stepNo} {data?.stepName}
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
disabled={stepNo === data?.maxSteps}
size="m"
onClick={() => {
setStepNo(stepNo + 1);
}}
iconType="arrowRight"
aria-label="Next"
/>
</EuiFlexItem>
</EuiFlexGroup>
)}
</div>
<EuiLink className="eui-textNoWrap" href={'/step/details'}>
{getShortTimeStamp(moment(timestamp))}
</EuiLink>
<EuiSpacer size="s" />
</>
);
return (
<StepDiv ref={intersectionRef}>
{imgSrc ? (
<StepImage
allowFullScreen={true}
size="s"
hasShadow
caption={ImageCaption}
alt="No image available"
url={imgSrc}
/>
) : (
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem>
<NoImageAvailable />
</EuiFlexItem>
<EuiFlexItem> {ImageCaption}</EuiFlexItem>
</EuiFlexGroup>
)}
<EuiFlexGroup
className="stepArrows"
gutterSize="s"
alignItems="center"
style={{ position: 'absolute', bottom: 0, left: 30 }}
>
<EuiFlexItem grow={false}>
<EuiButtonIcon
disabled={stepNo === 1}
color="subdued"
size="s"
onClick={() => {
setStepNo(stepNo - 1);
}}
iconType="arrowLeft"
aria-label="Next"
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
disabled={stepNo === data?.maxSteps}
color="subdued"
size="s"
onClick={() => {
setStepNo(stepNo + 1);
}}
iconType="arrowRight"
aria-label="Next"
/>
</EuiFlexItem>
</EuiFlexGroup>
</StepDiv>
);
};
const BorderedText = euiStyled(EuiText)`
width: 120px;
text-align: center;
border: 1px solid ${(props) => props.theme.eui.euiColorLightShade};
`;
export const NoImageAvailable = () => {
return (
<BorderedText>
<strong>
<FormattedMessage
id="xpack.uptime.synthetics.screenshot.noImageMessage"
defaultMessage="No image available"
/>
</strong>
</BorderedText>
);
};

View file

@ -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;
* you may not use this file except in compliance with the Elastic License.
*/
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>
<EuiBadge>{statusCode}</EuiBadge>
</SpanWithMargin>
);
};

View file

@ -4,5 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { PingListComponent } from './ping_list';
export { PingList } from './ping_list';

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiIcon, EuiLink, EuiText } from '@elastic/eui';
import { EuiLink, EuiText } from '@elastic/eui';
import React from 'react';
import { i18n } from '@kbn/i18n';
@ -24,7 +24,5 @@ export const LocationName = ({ location }: LocationNameProps) =>
description:
'Text that instructs the user to navigate to our docs to add a geographic location to their data',
})}
&nbsp;
<EuiIcon size="s" type="popout" />
</EuiLink>
);

View file

@ -4,192 +4,56 @@
* you may not use this file except in compliance with the Elastic License.
*/
import {
EuiBadge,
EuiBasicTable,
EuiFlexGroup,
EuiFlexItem,
EuiHealth,
EuiPanel,
EuiSelect,
EuiSpacer,
EuiText,
EuiTitle,
EuiFormRow,
EuiButtonIcon,
} from '@elastic/eui';
import { EuiBasicTable, EuiPanel, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import moment from 'moment';
import React, { useCallback, useContext, useState, useEffect } from 'react';
import React, { useCallback, useState, useEffect } from 'react';
import styled from 'styled-components';
import { useDispatch, useSelector } from 'react-redux';
import { Ping, GetPingsParams, DateRange } from '../../../../common/runtime_types';
import { useDispatch } from 'react-redux';
import { Ping } from '../../../../common/runtime_types';
import { convertMicrosecondsToMilliseconds as microsToMillis } from '../../../lib/helper';
import { LocationName } from './location_name';
import { Pagination } from '../../overview/monitor_list';
import { PingListExpandedRowComponent } from './expanded_row';
// import { PingListProps } from './ping_list_container';
import { pruneJourneyState } from '../../../state/actions/journey';
import { selectPingList } from '../../../state/selectors';
import { UptimeRefreshContext, UptimeSettingsContext } from '../../../contexts';
import { getPings as getPingsAction } from '../../../state/actions';
import { PingStatusColumn } from './columns/ping_status';
import * as I18LABELS from './translations';
import { MONITOR_TYPES } from '../../../../common/constants';
import { ResponseCodeColumn } from './columns/response_code';
import { ERROR_LABEL, LOCATION_LABEL, RES_CODE_LABEL, TIMESTAMP_LABEL } from './translations';
import { ExpandRowColumn } from './columns/expand_row';
import { PingErrorCol } from './columns/ping_error';
import { PingTimestamp } from './columns/ping_timestamp';
import { FailedStep } from './columns/failed_step';
import { usePingsList } from './use_pings';
import { PingListHeader } from './ping_list_header';
export interface PingListProps {
monitorId: string;
}
export const SpanWithMargin = styled.span`
margin-right: 16px;
`;
export const PingList = (props: PingListProps) => {
const {
error,
loading,
pingList: { locations, pings, total },
} = useSelector(selectPingList);
const DEFAULT_PAGE_SIZE = 10;
const { lastRefresh } = useContext(UptimeRefreshContext);
const { dateRangeStart: drs, dateRangeEnd: dre } = useContext(UptimeSettingsContext);
export const PingList = () => {
const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
const [pageIndex, setPageIndex] = useState(0);
const dispatch = useDispatch();
const getPingsCallback = useCallback(
(params: GetPingsParams) => dispatch(getPingsAction(params)),
[dispatch]
);
const pruneJourneysCallback = useCallback(
(checkGroups: string[]) => dispatch(pruneJourneyState(checkGroups)),
[dispatch]
);
return (
<PingListComponent
dateRange={{
from: drs,
to: dre,
}}
error={error}
getPings={getPingsCallback}
pruneJourneysCallback={pruneJourneysCallback}
lastRefresh={lastRefresh}
loading={loading}
locations={locations}
pings={pings}
total={total}
{...props}
/>
);
};
export const AllLocationOption = {
'data-test-subj': 'xpack.uptime.pingList.locationOptions.all',
text: 'All',
value: '',
};
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} />,
const { error, loading, pings, total, failedSteps } = usePingsList({
pageSize,
pageIndex,
});
};
const SpanWithMargin = styled.span`
margin-right: 16px;
`;
interface Props extends PingListProps {
dateRange: DateRange;
error?: Error;
getPings: (props: GetPingsParams) => void;
pruneJourneysCallback: (checkGroups: string[]) => void;
lastRefresh: number;
loading: boolean;
locations: string[];
pings: Ping[];
total: number;
}
const DEFAULT_PAGE_SIZE = 10;
const statusOptions = [
{
'data-test-subj': 'xpack.uptime.pingList.statusOptions.all',
text: i18n.translate('xpack.uptime.pingList.statusOptions.allStatusOptionLabel', {
defaultMessage: 'All',
}),
value: '',
},
{
'data-test-subj': 'xpack.uptime.pingList.statusOptions.up',
text: i18n.translate('xpack.uptime.pingList.statusOptions.upStatusOptionLabel', {
defaultMessage: 'Up',
}),
value: 'up',
},
{
'data-test-subj': 'xpack.uptime.pingList.statusOptions.down',
text: i18n.translate('xpack.uptime.pingList.statusOptions.downStatusOptionLabel', {
defaultMessage: 'Down',
}),
value: 'down',
},
];
export function rowShouldExpand(item: Ping) {
const errorPresent = !!item.error;
const httpBodyPresent = item.http?.response?.body?.bytes ?? 0 > 0;
const isBrowserMonitor = item.monitor.type === 'browser';
return errorPresent || httpBodyPresent || isBrowserMonitor;
}
export const PingListComponent = (props: Props) => {
const [selectedLocation, setSelectedLocation] = useState<string>('');
const [status, setStatus] = useState<string>('');
const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
const [pageIndex, setPageIndex] = useState(0);
const {
dateRange: { from, to },
error,
getPings,
pruneJourneysCallback,
lastRefresh,
loading,
locations,
monitorId,
pings,
total,
} = props;
useEffect(() => {
getPings({
dateRange: {
from,
to,
},
location: selectedLocation,
monitorId,
index: pageIndex,
size: pageSize,
status: status !== 'all' ? status : '',
});
}, [from, to, getPings, monitorId, lastRefresh, selectedLocation, pageIndex, pageSize, status]);
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) {
@ -203,73 +67,62 @@ export const PingListComponent = (props: Props) => {
const expandedCheckGroups = pings
.filter((p: Ping) => Object.keys(expandedRows).some((f) => p.docId === f))
.map(({ monitor: { check_group: cg } }) => cg);
const expandedCheckGroupsStr = JSON.stringify(expandedCheckGroups);
useEffect(() => {
pruneJourneysCallback(JSON.parse(expandedCheckGroupsStr));
}, [pruneJourneysCallback, expandedCheckGroupsStr]);
const locationOptions = !locations
? [AllLocationOption]
: [AllLocationOption].concat(
locations.map((name) => ({
text: name,
'data-test-subj': `xpack.uptime.pingList.locationOptions.${name}`,
value: name,
}))
);
const hasStatus = pings.reduce(
(hasHttpStatus: boolean, currentPing) =>
hasHttpStatus || !!currentPing.http?.response?.status_code,
false
);
const monitorType = pings?.[0]?.monitor.type;
const columns: any[] = [
{
field: 'monitor.status',
name: i18n.translate('xpack.uptime.pingList.statusColumnLabel', {
defaultMessage: 'Status',
}),
name: I18LABELS.STATUS_LABEL,
render: (pingStatus: string, item: Ping) => (
<div data-test-subj={`xpack.uptime.pingList.ping-${item.docId}`}>
<EuiHealth color={pingStatus === 'up' ? 'success' : 'danger'}>
{pingStatus === 'up'
? i18n.translate('xpack.uptime.pingList.statusColumnHealthUpLabel', {
defaultMessage: 'Up',
})
: i18n.translate('xpack.uptime.pingList.statusColumnHealthDownLabel', {
defaultMessage: 'Down',
})}
</EuiHealth>
<EuiText size="xs" color="subdued">
{i18n.translate('xpack.uptime.pingList.recencyMessage', {
values: { fromNow: moment(item.timestamp).fromNow() },
defaultMessage: 'Checked {fromNow}',
description:
'A string used to inform our users how long ago Heartbeat pinged the selected host.',
})}
</EuiText>
</div>
<PingStatusColumn pingStatus={pingStatus} item={item} />
),
},
{
align: 'left',
field: 'observer.geo.name',
name: i18n.translate('xpack.uptime.pingList.locationNameColumnLabel', {
defaultMessage: 'Location',
}),
name: LOCATION_LABEL,
render: (location: string) => <LocationName location={location} />,
},
...(monitorType === MONITOR_TYPES.BROWSER
? [
{
align: 'left',
field: 'timestamp',
name: TIMESTAMP_LABEL,
render: (timestamp: string, item: Ping) => (
<PingTimestamp timestamp={timestamp} ping={item} />
),
},
]
: []),
// ip column not needed for browser type
...(monitorType !== MONITOR_TYPES.BROWSER
? [
{
align: 'right',
dataType: 'number',
field: 'monitor.ip',
name: i18n.translate('xpack.uptime.pingList.ipAddressColumnLabel', {
defaultMessage: 'IP',
}),
},
]
: []),
{
align: 'right',
dataType: 'number',
field: 'monitor.ip',
name: i18n.translate('xpack.uptime.pingList.ipAddressColumnLabel', {
defaultMessage: 'IP',
}),
},
{
align: 'right',
align: 'center',
field: 'monitor.duration.us',
name: i18n.translate('xpack.uptime.pingList.durationMsColumnLabel', {
defaultMessage: 'Duration',
@ -281,31 +134,33 @@ export const PingListComponent = (props: Props) => {
}),
},
{
align: hasStatus ? 'right' : 'center',
field: 'error.type',
name: i18n.translate('xpack.uptime.pingList.errorTypeColumnLabel', {
defaultMessage: 'Error type',
}),
render: (errorType: string) => errorType ?? '-',
name: ERROR_LABEL,
width: '30%',
render: (errorType: string, item: Ping) => <PingErrorCol ping={item} errorType={errorType} />,
},
...(monitorType === MONITOR_TYPES.BROWSER
? [
{
field: 'monitor.status',
align: 'left',
name: i18n.translate('xpack.uptime.pingList.columns.failedStep', {
defaultMessage: 'Failed step',
}),
render: (timestamp: string, item: Ping) => (
<FailedStep ping={item} failedSteps={failedSteps} />
),
},
]
: []),
// Only add this column is there is any status present in list
...(hasStatus
? [
{
field: 'http.response.status_code',
align: 'right',
name: (
<SpanWithMargin>
{i18n.translate('xpack.uptime.pingList.responseCodeColumnLabel', {
defaultMessage: 'Response code',
})}
</SpanWithMargin>
),
render: (statusCode: string) => (
<SpanWithMargin>
<EuiBadge>{statusCode}</EuiBadge>
</SpanWithMargin>
),
name: <SpanWithMargin>{RES_CODE_LABEL}</SpanWithMargin>,
render: (statusCode: string) => <ResponseCodeColumn statusCode={statusCode} />,
},
]
: []),
@ -313,23 +168,13 @@ export const PingListComponent = (props: Props) => {
align: 'right',
width: '24px',
isExpander: true,
render: (item: Ping) => {
return (
<EuiButtonIcon
data-test-subj="uptimePingListExpandBtn"
onClick={() => toggleDetails(item, expandedRows, setExpandedRows)}
disabled={!rowShouldExpand(item)}
aria-label={
expandedRows[item.docId]
? i18n.translate('xpack.uptime.pingList.collapseRow', {
defaultMessage: 'Collapse',
})
: i18n.translate('xpack.uptime.pingList.expandRow', { defaultMessage: 'Expand' })
}
iconType={expandedRows[item.docId] ? 'arrowUp' : 'arrowDown'}
/>
);
},
render: (item: Ping) => (
<ExpandRowColumn
item={item}
expandedRows={expandedRows}
setExpandedRows={setExpandedRows}
/>
),
},
];
@ -338,63 +183,12 @@ export const PingListComponent = (props: Props) => {
pageIndex,
pageSize,
pageSizeOptions: [10, 25, 50, 100],
/**
* we're not currently supporting pagination in this component
* so the first page is the only page
*/
totalItemCount: total,
};
return (
<EuiPanel>
<EuiTitle size="s">
<h4>
<FormattedMessage id="xpack.uptime.pingList.checkHistoryTitle" defaultMessage="History" />
</h4>
</EuiTitle>
<EuiSpacer size="s" />
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiFormRow
label="Status"
aria-label={i18n.translate('xpack.uptime.pingList.statusLabel', {
defaultMessage: 'Status',
})}
>
<EuiSelect
options={statusOptions}
aria-label={i18n.translate('xpack.uptime.pingList.statusLabel', {
defaultMessage: 'Status',
})}
data-test-subj="xpack.uptime.pingList.statusSelect"
value={status}
onChange={(selected) => {
setStatus(selected.target.value);
}}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem>
<EuiFormRow
label="Location"
aria-label={i18n.translate('xpack.uptime.pingList.locationLabel', {
defaultMessage: 'Location',
})}
>
<EuiSelect
options={locationOptions}
value={selectedLocation}
aria-label={i18n.translate('xpack.uptime.pingList.locationLabel', {
defaultMessage: 'Location',
})}
data-test-subj="xpack.uptime.pingList.locationSelect"
onChange={(selected) => {
setSelectedLocation(selected.target.value);
}}
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
<PingListHeader />
<EuiSpacer size="s" />
<EuiBasicTable
loading={loading}
@ -410,6 +204,7 @@ export const PingListComponent = (props: Props) => {
setPageSize(criteria.page!.size);
setPageIndex(criteria.page!.index);
}}
tableLayout={'auto'}
/>
</EuiPanel>
);

View file

@ -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;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
import { StatusFilter } from '../../overview/monitor_list/status_filter';
import { FilterGroup } from '../../overview/filter_group';
export const PingListHeader = () => {
return (
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiTitle size="s">
<h4>
<FormattedMessage
id="xpack.uptime.pingList.checkHistoryTitle"
defaultMessage="History"
/>
</h4>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<StatusFilter />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<FilterGroup />
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -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;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
export const STATUS_LABEL = i18n.translate('xpack.uptime.pingList.statusColumnLabel', {
defaultMessage: 'Status',
});
export const RES_CODE_LABEL = i18n.translate('xpack.uptime.pingList.responseCodeColumnLabel', {
defaultMessage: 'Response code',
});
export const ERROR_TYPE_LABEL = i18n.translate('xpack.uptime.pingList.errorTypeColumnLabel', {
defaultMessage: 'Error type',
});
export const ERROR_LABEL = i18n.translate('xpack.uptime.pingList.errorColumnLabel', {
defaultMessage: 'Error',
});
export const LOCATION_LABEL = i18n.translate('xpack.uptime.pingList.locationNameColumnLabel', {
defaultMessage: 'Location',
});
export const TIMESTAMP_LABEL = i18n.translate('xpack.uptime.pingList.timestampColumnLabel', {
defaultMessage: 'Timestamp',
});

View file

@ -0,0 +1,79 @@
/*
* 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 { useDispatch, useSelector } from 'react-redux';
import { useCallback, useContext, useEffect } from 'react';
import { selectPingList } from '../../../state/selectors';
import { GetPingsParams, Ping } from '../../../../common/runtime_types/ping';
import { getPings as getPingsAction } from '../../../state/actions';
import { useGetUrlParams, useMonitorId } from '../../../hooks';
import { UptimeRefreshContext, UptimeSettingsContext } from '../../../contexts';
import { useFetcher } from '../../../../../observability/public';
import { fetchJourneysFailedSteps } from '../../../state/api/journey';
import { useSelectedFilters } from '../../../hooks/use_selected_filters';
import { MONITOR_TYPES } from '../../../../common/constants';
interface Props {
pageSize: number;
pageIndex: number;
}
export const usePingsList = ({ pageSize, pageIndex }: Props) => {
const {
error,
loading,
pingList: { pings, total },
} = useSelector(selectPingList);
const { lastRefresh } = useContext(UptimeRefreshContext);
const { dateRangeStart: from, dateRangeEnd: to } = useContext(UptimeSettingsContext);
const { statusFilter } = useGetUrlParams();
const { selectedLocations } = useSelectedFilters();
const dispatch = useDispatch();
const monitorId = useMonitorId();
const getPings = useCallback((params: GetPingsParams) => dispatch(getPingsAction(params)), [
dispatch,
]);
useEffect(() => {
getPings({
monitorId,
dateRange: {
from,
to,
},
locations: JSON.stringify(selectedLocations),
index: pageIndex,
size: pageSize,
status: statusFilter !== 'all' ? statusFilter : '',
});
}, [
from,
to,
getPings,
monitorId,
lastRefresh,
pageIndex,
pageSize,
statusFilter,
selectedLocations,
]);
const { data } = useFetcher(() => {
if (pings?.length > 0 && pings.find((ping) => ping.monitor.type === MONITOR_TYPES.BROWSER))
return fetchJourneysFailedSteps({
checkGroups: pings.map((ping: Ping) => ping.monitor.check_group!),
});
}, [pings]);
return { error, loading, pings, total, failedSteps: data };
};

View file

@ -13,17 +13,17 @@ Array [
class="euiSpacer euiSpacer--l"
/>,
.c0.c0.c0 {
width: 35%;
width: 30%;
max-width: 250px;
}
.c1.c1.c1 {
width: 65%;
width: 70%;
overflow-wrap: anywhere;
}
<dl
class="euiDescriptionList euiDescriptionList--column euiDescriptionList--reverse euiDescriptionList--compressed"
style="max-width:450px"
>
<dt
class="euiDescriptionList__title c0"
@ -51,10 +51,6 @@ Array [
rel="noopener noreferrer"
target="_blank"
>
<span
data-euiicon-type="popout"
/>
<span
aria-label="External link"
class="euiLink__externalIcon"

View file

@ -3,7 +3,8 @@
exports[`SSL Certificate component renders 1`] = `
Array [
.c0.c0.c0 {
width: 35%;
width: 30%;
max-width: 250px;
}
<dt
@ -15,7 +16,7 @@ Array [
class="euiSpacer euiSpacer--s"
/>,
.c0.c0.c0 {
width: 65%;
width: 70%;
overflow-wrap: anywhere;
}
@ -57,7 +58,8 @@ Array [
exports[`SSL Certificate component renders null if invalid date 1`] = `
Array [
.c0.c0.c0 {
width: 35%;
width: 30%;
max-width: 250px;
}
<dt
@ -69,7 +71,7 @@ Array [
class="euiSpacer euiSpacer--s"
/>,
.c0.c0.c0 {
width: 65%;
width: 70%;
overflow-wrap: anywhere;
}

View file

@ -105,7 +105,7 @@ Array [
>
<span
class="euiBadge euiBadge--iconLeft"
style="background-color:#d3dae6;color:#000"
style="background-color:#6dccb1;color:#000"
>
<span
class="euiBadge__content"
@ -113,13 +113,7 @@ Array [
<span
class="euiBadge__text"
>
<span
class="euiTextColor euiTextColor--default"
>
<h4>
au-heartbeat
</h4>
</span>
au-heartbeat
</span>
</span>
</span>
@ -182,7 +176,7 @@ Array [
>
<span
class="euiBadge euiBadge--iconLeft"
style="background-color:#d3dae6;color:#000"
style="background-color:#ff7e62;color:#000"
>
<span
class="euiBadge__content"
@ -190,13 +184,7 @@ Array [
<span
class="euiBadge__text"
>
<span
class="euiTextColor euiTextColor--ghost"
>
<h4>
nyc-heartbeat
</h4>
</span>
nyc-heartbeat
</span>
</span>
</span>
@ -259,7 +247,7 @@ Array [
>
<span
class="euiBadge euiBadge--iconLeft"
style="background-color:#d3dae6;color:#000"
style="background-color:#ff7e62;color:#000"
>
<span
class="euiBadge__content"
@ -267,13 +255,7 @@ Array [
<span
class="euiBadge__text"
>
<span
class="euiTextColor euiTextColor--ghost"
>
<h4>
spa-heartbeat
</h4>
</span>
spa-heartbeat
</span>
</span>
</span>

View file

@ -11,21 +11,21 @@ exports[`LocationStatusTags component renders properly against props 1`] = `
"color": "#d3dae6",
"label": "Berlin",
"status": "up",
"timestamp": "1 Mon ago",
"timestamp": "Sept 4, 2020 9:31:38 AM",
},
Object {
"availability": 100,
"color": "#bd271e",
"label": "Berlin",
"status": "down",
"timestamp": "1 Mon ago",
"timestamp": "Sept 4, 2020 9:31:38 AM",
},
Object {
"availability": 100,
"color": "#d3dae6",
"label": "Islamabad",
"status": "up",
"timestamp": "1 Mon ago",
"timestamp": "Sept 4, 2020 9:31:38 AM",
},
]
}
@ -142,7 +142,7 @@ exports[`LocationStatusTags component renders when all locations are down 1`] =
>
<span
class="euiBadge euiBadge--iconLeft"
style="background-color:#bd271e;color:#fff"
style="background-color:#ff7e62;color:#000"
>
<span
class="euiBadge__content"
@ -150,13 +150,7 @@ exports[`LocationStatusTags component renders when all locations are down 1`] =
<span
class="euiBadge__text"
>
<span
class="euiTextColor euiTextColor--ghost"
>
<h4>
Berlin
</h4>
</span>
Berlin
</span>
</span>
</span>
@ -195,7 +189,7 @@ exports[`LocationStatusTags component renders when all locations are down 1`] =
<span
class="euiTableCellContent__text"
>
5m ago
Sept 4, 2020 9:31:38 AM
</span>
</div>
</td>
@ -219,7 +213,7 @@ exports[`LocationStatusTags component renders when all locations are down 1`] =
>
<span
class="euiBadge euiBadge--iconLeft"
style="background-color:#bd271e;color:#fff"
style="background-color:#ff7e62;color:#000"
>
<span
class="euiBadge__content"
@ -227,13 +221,7 @@ exports[`LocationStatusTags component renders when all locations are down 1`] =
<span
class="euiBadge__text"
>
<span
class="euiTextColor euiTextColor--ghost"
>
<h4>
Islamabad
</h4>
</span>
Islamabad
</span>
</span>
</span>
@ -272,7 +260,7 @@ exports[`LocationStatusTags component renders when all locations are down 1`] =
<span
class="euiTableCellContent__text"
>
5s ago
Sept 4, 2020 9:31:38 AM
</span>
</div>
</td>
@ -392,7 +380,7 @@ exports[`LocationStatusTags component renders when all locations are up 1`] = `
>
<span
class="euiBadge euiBadge--iconLeft"
style="background-color:#d3dae6;color:#000"
style="background-color:#6dccb1;color:#000"
>
<span
class="euiBadge__content"
@ -400,13 +388,7 @@ exports[`LocationStatusTags component renders when all locations are up 1`] = `
<span
class="euiBadge__text"
>
<span
class="euiTextColor euiTextColor--default"
>
<h4>
Berlin
</h4>
</span>
Berlin
</span>
</span>
</span>
@ -445,7 +427,7 @@ exports[`LocationStatusTags component renders when all locations are up 1`] = `
<span
class="euiTableCellContent__text"
>
5d ago
Sept 4, 2020 9:31:38 AM
</span>
</div>
</td>
@ -469,7 +451,7 @@ exports[`LocationStatusTags component renders when all locations are up 1`] = `
>
<span
class="euiBadge euiBadge--iconLeft"
style="background-color:#d3dae6;color:#000"
style="background-color:#6dccb1;color:#000"
>
<span
class="euiBadge__content"
@ -477,13 +459,7 @@ exports[`LocationStatusTags component renders when all locations are up 1`] = `
<span
class="euiBadge__text"
>
<span
class="euiTextColor euiTextColor--default"
>
<h4>
Islamabad
</h4>
</span>
Islamabad
</span>
</span>
</span>
@ -522,7 +498,7 @@ exports[`LocationStatusTags component renders when all locations are up 1`] = `
<span
class="euiTableCellContent__text"
>
5s ago
Sept 4, 2020 9:31:38 AM
</span>
</div>
</td>
@ -642,7 +618,7 @@ exports[`LocationStatusTags component renders when there are many location 1`] =
>
<span
class="euiBadge euiBadge--iconLeft"
style="background-color:#bd271e;color:#fff"
style="background-color:#ff7e62;color:#000"
>
<span
class="euiBadge__content"
@ -650,13 +626,7 @@ exports[`LocationStatusTags component renders when there are many location 1`] =
<span
class="euiBadge__text"
>
<span
class="euiTextColor euiTextColor--ghost"
>
<h4>
Berlin
</h4>
</span>
Berlin
</span>
</span>
</span>
@ -695,7 +665,7 @@ exports[`LocationStatusTags component renders when there are many location 1`] =
<span
class="euiTableCellContent__text"
>
5m ago
Sept 4, 2020 9:31:38 AM
</span>
</div>
</td>
@ -719,7 +689,7 @@ exports[`LocationStatusTags component renders when there are many location 1`] =
>
<span
class="euiBadge euiBadge--iconLeft"
style="background-color:#bd271e;color:#fff"
style="background-color:#ff7e62;color:#000"
>
<span
class="euiBadge__content"
@ -727,13 +697,7 @@ exports[`LocationStatusTags component renders when there are many location 1`] =
<span
class="euiBadge__text"
>
<span
class="euiTextColor euiTextColor--ghost"
>
<h4>
Islamabad
</h4>
</span>
Islamabad
</span>
</span>
</span>
@ -772,7 +736,7 @@ exports[`LocationStatusTags component renders when there are many location 1`] =
<span
class="euiTableCellContent__text"
>
5s ago
Sept 4, 2020 9:31:38 AM
</span>
</div>
</td>
@ -796,7 +760,7 @@ exports[`LocationStatusTags component renders when there are many location 1`] =
>
<span
class="euiBadge euiBadge--iconLeft"
style="background-color:#bd271e;color:#fff"
style="background-color:#ff7e62;color:#000"
>
<span
class="euiBadge__content"
@ -804,13 +768,7 @@ exports[`LocationStatusTags component renders when there are many location 1`] =
<span
class="euiBadge__text"
>
<span
class="euiTextColor euiTextColor--ghost"
>
<h4>
New York
</h4>
</span>
New York
</span>
</span>
</span>
@ -849,7 +807,7 @@ exports[`LocationStatusTags component renders when there are many location 1`] =
<span
class="euiTableCellContent__text"
>
1 Mon ago
Sept 4, 2020 9:31:38 AM
</span>
</div>
</td>
@ -873,7 +831,7 @@ exports[`LocationStatusTags component renders when there are many location 1`] =
>
<span
class="euiBadge euiBadge--iconLeft"
style="background-color:#bd271e;color:#fff"
style="background-color:#ff7e62;color:#000"
>
<span
class="euiBadge__content"
@ -881,13 +839,7 @@ exports[`LocationStatusTags component renders when there are many location 1`] =
<span
class="euiBadge__text"
>
<span
class="euiTextColor euiTextColor--ghost"
>
<h4>
Paris
</h4>
</span>
Paris
</span>
</span>
</span>
@ -926,7 +878,7 @@ exports[`LocationStatusTags component renders when there are many location 1`] =
<span
class="euiTableCellContent__text"
>
5 Yr ago
Sept 4, 2020 9:31:38 AM
</span>
</div>
</td>
@ -950,7 +902,7 @@ exports[`LocationStatusTags component renders when there are many location 1`] =
>
<span
class="euiBadge euiBadge--iconLeft"
style="background-color:#bd271e;color:#fff"
style="background-color:#ff7e62;color:#000"
>
<span
class="euiBadge__content"
@ -958,13 +910,7 @@ exports[`LocationStatusTags component renders when there are many location 1`] =
<span
class="euiBadge__text"
>
<span
class="euiTextColor euiTextColor--ghost"
>
<h4>
Sydney
</h4>
</span>
Sydney
</span>
</span>
</span>
@ -1003,7 +949,7 @@ exports[`LocationStatusTags component renders when there are many location 1`] =
<span
class="euiTableCellContent__text"
>
5 Yr ago
Sept 4, 2020 9:31:38 AM
</span>
</div>
</td>

View file

@ -18,7 +18,7 @@ exports[`TagLabel component renders correctly against snapshot 1`] = `
>
<span
class="euiBadge euiBadge--iconLeft"
style="background-color:#fff;color:#000"
style="background-color:#ff7e62;color:#000"
>
<span
class="euiBadge__content"
@ -26,13 +26,7 @@ exports[`TagLabel component renders correctly against snapshot 1`] = `
<span
class="euiBadge__text"
>
<span
class="euiTextColor euiTextColor--ghost"
>
<h4>
US-East
</h4>
</span>
US-East
</span>
</span>
</span>
@ -42,15 +36,9 @@ exports[`TagLabel component renders correctly against snapshot 1`] = `
exports[`TagLabel component shallow render correctly against snapshot 1`] = `
<styled.div>
<EuiBadge
color="#fff"
color="secondary"
>
<EuiTextColor
color="default"
>
<h4>
US-East
</h4>
</EuiTextColor>
US-East
</EuiBadge>
</styled.div>
`;

View file

@ -5,10 +5,12 @@
*/
import React from 'react';
import moment from 'moment';
import { renderWithIntl, shallowWithIntl } from '@kbn/test/jest';
import { MonitorLocation } from '../../../../../../common/runtime_types/monitor';
import { LocationStatusTags } from '../index';
import { mockMoment } from '../../../../../lib/helper/test_helpers';
mockMoment();
jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => {
return {
@ -24,21 +26,21 @@ describe('LocationStatusTags component', () => {
{
summary: { up: 4, down: 0 },
geo: { name: 'Islamabad', location: { lat: '52.487448', lon: ' 13.394798' } },
timestamp: moment().subtract('5', 'w').toISOString(),
timestamp: 'Oct 26, 2020 7:49:20 AM',
up_history: 4,
down_history: 0,
},
{
summary: { up: 4, down: 0 },
geo: { name: 'Berlin', location: { lat: '52.487448', lon: ' 13.394798' } },
timestamp: moment().subtract('5', 'w').toISOString(),
timestamp: 'Oct 26, 2020 7:49:20 AM',
up_history: 4,
down_history: 0,
},
{
summary: { up: 0, down: 2 },
geo: { name: 'Berlin', location: { lat: '52.487448', lon: ' 13.394798' } },
timestamp: moment().subtract('5', 'w').toISOString(),
timestamp: 'Oct 26, 2020 7:49:20 AM',
up_history: 4,
down_history: 0,
},
@ -52,56 +54,56 @@ describe('LocationStatusTags component', () => {
{
summary: { up: 0, down: 1 },
geo: { name: 'Islamabad', location: { lat: '52.487448', lon: ' 13.394798' } },
timestamp: moment().subtract('5', 's').toISOString(),
timestamp: 'Oct 26, 2020 7:49:20 AM',
up_history: 4,
down_history: 0,
},
{
summary: { up: 0, down: 1 },
geo: { name: 'Berlin', location: { lat: '52.487448', lon: ' 13.394798' } },
timestamp: moment().subtract('5', 'm').toISOString(),
timestamp: 'Oct 26, 2020 7:49:20 AM',
up_history: 4,
down_history: 0,
},
{
summary: { up: 0, down: 1 },
geo: { name: 'st-paul', location: { lat: '52.487448', lon: ' 13.394798' } },
timestamp: moment().subtract('5', 'h').toISOString(),
timestamp: 'Oct 26, 2020 7:49:20 AM',
up_history: 4,
down_history: 0,
},
{
summary: { up: 0, down: 1 },
geo: { name: 'Tokyo', location: { lat: '52.487448', lon: ' 13.394798' } },
timestamp: moment().subtract('5', 'd').toISOString(),
timestamp: 'Oct 26, 2020 7:49:20 AM',
up_history: 4,
down_history: 0,
},
{
summary: { up: 0, down: 1 },
geo: { name: 'New York', location: { lat: '52.487448', lon: ' 13.394798' } },
timestamp: moment().subtract('5', 'w').toISOString(),
timestamp: 'Oct 26, 2020 7:49:20 AM',
up_history: 4,
down_history: 0,
},
{
summary: { up: 0, down: 1 },
geo: { name: 'Toronto', location: { lat: '52.487448', lon: ' 13.394798' } },
timestamp: moment().subtract('5', 'M').toISOString(),
timestamp: 'Oct 26, 2020 7:49:20 AM',
up_history: 4,
down_history: 0,
},
{
summary: { up: 0, down: 1 },
geo: { name: 'Sydney', location: { lat: '52.487448', lon: ' 13.394798' } },
timestamp: moment().subtract('5', 'y').toISOString(),
timestamp: 'Oct 26, 2020 7:49:20 AM',
up_history: 4,
down_history: 0,
},
{
summary: { up: 0, down: 1 },
geo: { name: 'Paris', location: { lat: '52.487448', lon: ' 13.394798' } },
timestamp: moment().subtract('5', 'y').toISOString(),
timestamp: 'Oct 26, 2020 7:49:20 AM',
up_history: 4,
down_history: 0,
},
@ -115,14 +117,14 @@ describe('LocationStatusTags component', () => {
{
summary: { up: 4, down: 0 },
geo: { name: 'Islamabad', location: { lat: '52.487448', lon: ' 13.394798' } },
timestamp: moment().subtract('5', 's').toISOString(),
timestamp: 'Oct 26, 2020 7:49:20 AM',
up_history: 4,
down_history: 0,
},
{
summary: { up: 4, down: 0 },
geo: { name: 'Berlin', location: { lat: '52.487448', lon: ' 13.394798' } },
timestamp: moment().subtract('5', 'd').toISOString(),
timestamp: 'Oct 26, 2020 7:49:20 AM',
up_history: 4,
down_history: 0,
},
@ -136,14 +138,14 @@ describe('LocationStatusTags component', () => {
{
summary: { up: 0, down: 2 },
geo: { name: 'Islamabad', location: { lat: '52.487448', lon: ' 13.394798' } },
timestamp: moment().subtract('5', 's').toISOString(),
timestamp: 'Oct 26, 2020 7:49:20 AM',
up_history: 4,
down_history: 0,
},
{
summary: { up: 0, down: 2 },
geo: { name: 'Berlin', location: { lat: '52.487448', lon: ' 13.394798' } },
timestamp: moment().subtract('5', 'm').toISOString(),
timestamp: 'Oct 26, 2020 7:49:20 AM',
up_history: 4,
down_history: 0,
},

View file

@ -11,6 +11,7 @@ import { UptimeThemeContext } from '../../../../contexts';
import { MonitorLocation } from '../../../../../common/runtime_types';
import { SHORT_TIMESPAN_LOCALE, SHORT_TS_LOCALE } from '../../../../../common/constants';
import { AvailabilityReporting } from '../index';
import { getShortTimeStamp } from '../../../overview/monitor_list/columns/monitor_status_column';
// Set height so that it remains within panel, enough height to display 7 locations tags
const TagContainer = styled.div`
@ -46,7 +47,7 @@ export const LocationStatusTags = ({ locations }: Props) => {
locations.forEach((item: MonitorLocation) => {
allLocations.push({
label: item.geo.name!,
timestamp: moment(new Date(item.timestamp).valueOf()).fromNow(),
timestamp: getShortTimeStamp(moment(new Date(item.timestamp).valueOf())),
color: item.summary.down === 0 ? gray : danger,
availability: (item.up_history / (item.up_history + item.down_history)) * 100,
status: item.summary.down === 0 ? 'up' : 'down',

View file

@ -6,8 +6,9 @@
import React from 'react';
import styled from 'styled-components';
import { EuiBadge, EuiTextColor } from '@elastic/eui';
import { EuiBadge } from '@elastic/eui';
import { StatusTag } from './location_status_tags';
import { STATUS } from '../../../../../common/constants';
const BadgeItem = styled.div`
white-space: nowrap;
@ -21,11 +22,7 @@ const BadgeItem = styled.div`
export const TagLabel: React.FC<StatusTag> = ({ color, label, status }) => {
return (
<BadgeItem>
<EuiBadge color={color}>
<EuiTextColor color={status === 'down' ? 'ghost' : 'default'}>
<h4>{label}</h4>
</EuiTextColor>
</EuiBadge>
<EuiBadge color={status === STATUS.DOWN ? 'danger' : 'secondary'}>{label}</EuiBadge>
</BadgeItem>
);
};

View file

@ -8,7 +8,6 @@ import React from 'react';
import styled from 'styled-components';
import {
EuiLink,
EuiIcon,
EuiSpacer,
EuiDescriptionList,
EuiDescriptionListTitle,
@ -27,13 +26,14 @@ import { MonitorRedirects } from './monitor_redirects';
export const MonListTitle = styled(EuiDescriptionListTitle)`
&&& {
width: 35%;
width: 30%;
max-width: 250px;
}
`;
export const MonListDescription = styled(EuiDescriptionListDescription)`
&&& {
width: 65%;
width: 70%;
overflow-wrap: anywhere;
}
`;
@ -53,12 +53,7 @@ export const MonitorStatusBar: React.FC = () => {
<StatusByLocations locations={locations ?? []} />
</div>
<EuiSpacer />
<EuiDescriptionList
type="column"
compressed={true}
textStyle="reverse"
style={{ maxWidth: '450px' }}
>
<EuiDescriptionList type="column" compressed={true} textStyle="reverse">
<MonListTitle>{OverallAvailability}</MonListTitle>
<MonListDescription data-test-subj="uptimeOverallAvailability">
<FormattedMessage
@ -70,8 +65,8 @@ export const MonitorStatusBar: React.FC = () => {
</MonListDescription>
<MonListTitle>{URL_LABEL}</MonListTitle>
<MonListDescription>
<EuiLink aria-label={labels.monitorUrlLinkAriaLabel} href={full} target="_blank">
{full} <EuiIcon type={'popout'} size="s" />
<EuiLink aria-label={labels.monitorUrlLinkAriaLabel} href={full} target="_blank" external>
{full}
</EuiLink>
</MonListDescription>
<MonListTitle>{MonitorIDLabel}</MonListTitle>

View file

@ -13,17 +13,6 @@ export const healthStatusMessageAriaLabel = i18n.translate(
}
);
export const upLabel = i18n.translate('xpack.uptime.monitorStatusBar.healthStatusMessage.upLabel', {
defaultMessage: 'Up',
});
export const downLabel = i18n.translate(
'xpack.uptime.monitorStatusBar.healthStatusMessage.downLabel',
{
defaultMessage: 'Down',
}
);
export const typeLabel = i18n.translate('xpack.uptime.monitorStatusBar.type.label', {
defaultMessage: 'Type',
});

View file

@ -7,10 +7,13 @@
import React, { useState } from 'react';
import { EuiFilterGroup } from '@elastic/eui';
import styled from 'styled-components';
import { useRouteMatch } from 'react-router-dom';
import { FilterPopoverProps, FilterPopover } from './filter_popover';
import { OverviewFilters } from '../../../../common/runtime_types/overview_filters';
import { filterLabels } from './translations';
import { useFilterUpdate } from '../../../hooks/use_filter_update';
import { MONITOR_ROUTE } from '../../../../common/constants';
import { useSelectedFilters } from '../../../hooks/use_selected_filters';
interface PresentationalComponentProps {
loading: boolean;
@ -32,15 +35,16 @@ export const FilterGroupComponent: React.FC<PresentationalComponentProps> = ({
values: string[];
}>({ fieldName: '', values: [] });
const { selectedLocations, selectedPorts, selectedSchemes, selectedTags } = useFilterUpdate(
updatedFieldValues.fieldName,
updatedFieldValues.values
);
useFilterUpdate(updatedFieldValues.fieldName, updatedFieldValues.values);
const { selectedLocations, selectedPorts, selectedSchemes, selectedTags } = useSelectedFilters();
const onFilterFieldChange = (fieldName: string, values: string[]) => {
setUpdatedFieldValues({ fieldName, values });
};
const isMonitorPage = useRouteMatch(MONITOR_ROUTE);
const filterPopoverProps: FilterPopoverProps[] = [
{
loading,
@ -51,36 +55,41 @@ export const FilterGroupComponent: React.FC<PresentationalComponentProps> = ({
selectedItems: selectedLocations,
title: filterLabels.LOCATION,
},
{
loading,
onFilterFieldChange,
fieldName: 'url.port',
id: 'port',
disabled: ports.length === 0,
items: ports.map((p: number) => p.toString()),
selectedItems: selectedPorts,
title: filterLabels.PORT,
},
{
loading,
onFilterFieldChange,
fieldName: 'monitor.type',
id: 'scheme',
disabled: schemes.length === 0,
items: schemes,
selectedItems: selectedSchemes,
title: filterLabels.SCHEME,
},
{
loading,
onFilterFieldChange,
fieldName: 'tags',
id: 'tags',
disabled: tags.length === 0,
items: tags,
selectedItems: selectedTags,
title: filterLabels.TAGS,
},
// on monitor page we only display location filter in ping list
...(!isMonitorPage
? [
{
loading,
onFilterFieldChange,
fieldName: 'url.port',
id: 'port',
disabled: ports.length === 0,
items: ports.map((p: number) => p.toString()),
selectedItems: selectedPorts,
title: filterLabels.PORT,
},
{
loading,
onFilterFieldChange,
fieldName: 'monitor.type',
id: 'scheme',
disabled: schemes.length === 0,
items: schemes,
selectedItems: selectedSchemes,
title: filterLabels.SCHEME,
},
{
loading,
onFilterFieldChange,
fieldName: 'tags',
id: 'tags',
disabled: tags.length === 0,
items: tags,
selectedItems: selectedTags,
title: filterLabels.TAGS,
},
]
: []),
];
return (

View file

@ -18,9 +18,9 @@ import {
SHORT_TS_LOCALE,
} from '../../../../../common/constants';
import * as labels from '../translations';
import { UptimeThemeContext } from '../../../../contexts';
import { euiStyled } from '../../../../../../observability/public';
import { STATUS_DOWN_LABEL, STATUS_UP_LABEL } from '../../../common/translations';
interface MonitorListStatusColumnProps {
status: string;
@ -37,9 +37,9 @@ const StatusColumnFlexG = styled(EuiFlexGroup)`
export const getHealthMessage = (status: string): string | null => {
switch (status) {
case STATUS.UP:
return labels.UP;
return STATUS_UP_LABEL;
case STATUS.DOWN:
return labels.DOWN;
return STATUS_DOWN_LABEL;
default:
return null;
}

View file

@ -18,7 +18,7 @@ exports[`MostRecentError component renders properly with mock data 1`] = `
>
<a
data-test-subj="monitor-page-link-bad-ssl"
href="/monitor/YmFkLXNzbA==/?selectedPingStatus=down"
href="/monitor/YmFkLXNzbA==/?statusFilter=down"
>
Get https://expired.badssl.com: x509: certificate has expired or is not yet valid
</a>

View file

@ -35,7 +35,7 @@ interface MostRecentErrorProps {
export const MostRecentError = ({ error, monitorId, timestamp }: MostRecentErrorProps) => {
const { absoluteDateRangeStart, absoluteDateRangeEnd, ...params } = useGetUrlParams();
params.selectedPingStatus = 'down';
params.statusFilter = 'down';
const linkParameters = stringifyUrlParams(params, true);
const timestampStr = timestamp ? moment(new Date(timestamp).valueOf()).fromNow() : '';

View file

@ -9,6 +9,7 @@ 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';
export const StatusFilter: React.FC = () => {
const { statusFilter } = useGetUrlParams();
@ -28,18 +29,14 @@ export const StatusFilter: React.FC = () => {
isActive={statusFilter === ''}
/>
<FilterStatusButton
content={i18n.translate('xpack.uptime.filterBar.filterUpLabel', {
defaultMessage: 'Up',
})}
content={STATUS_UP_LABEL}
dataTestSubj="xpack.uptime.filterBar.filterStatusUp"
value="up"
withNext={true}
isActive={statusFilter === 'up'}
/>
<FilterStatusButton
content={i18n.translate('xpack.uptime.filterBar.filterDownLabel', {
defaultMessage: 'Down',
})}
content={STATUS_DOWN_LABEL}
dataTestSubj="xpack.uptime.filterBar.filterStatusDown"
value="down"
withNext={false}

View file

@ -62,14 +62,6 @@ export const NO_DATA_MESSAGE = i18n.translate('xpack.uptime.monitorList.noItemMe
description: 'This message is shown if the monitors table is rendered but has no items.',
});
export const UP = i18n.translate('xpack.uptime.monitorList.statusColumn.upLabel', {
defaultMessage: 'Up',
});
export const DOWN = i18n.translate('xpack.uptime.monitorList.statusColumn.downLabel', {
defaultMessage: 'Down',
});
export const RESPONSE_ANOMALY_SCORE = i18n.translate(
'xpack.uptime.monitorList.anomalyColumn.label',
{

View file

@ -209,7 +209,7 @@ exports[`useUrlParams deletes keys that do not have truthy values 1`] = `
}
>
<div>
{"absoluteDateRangeStart":20,"absoluteDateRangeEnd":20,"autorefreshInterval":60000,"autorefreshIsPaused":false,"dateRangeStart":"now-12","dateRangeEnd":"now","filters":"","search":"","selectedPingStatus":"","statusFilter":"","pagination":"foo","focusConnectorField":false}
{"pagination":"foo","absoluteDateRangeStart":20,"absoluteDateRangeEnd":20,"autorefreshInterval":60000,"autorefreshIsPaused":false,"dateRangeStart":"now-12","dateRangeEnd":"now","filters":"","search":"","statusFilter":"","focusConnectorField":false}
</div>
<button
id="setUrlParams"
@ -433,7 +433,7 @@ exports[`useUrlParams gets the expected values using the context 1`] = `
hook={[Function]}
>
<div>
{"absoluteDateRangeStart":20,"absoluteDateRangeEnd":20,"autorefreshInterval":60000,"autorefreshIsPaused":false,"dateRangeStart":"now-15m","dateRangeEnd":"now","filters":"","search":"","selectedPingStatus":"","statusFilter":"","focusConnectorField":false}
{"absoluteDateRangeStart":20,"absoluteDateRangeEnd":20,"autorefreshInterval":60000,"autorefreshIsPaused":false,"dateRangeStart":"now-15m","dateRangeEnd":"now","filters":"","search":"","statusFilter":"","focusConnectorField":false}
</div>
<button
id="setUrlParams"

View file

@ -27,6 +27,7 @@ export const makeBaseBreadcrumb = (
// values in dateRangeStart are better for a URL.
delete crumbParams.absoluteDateRangeStart;
delete crumbParams.absoluteDateRangeEnd;
delete crumbParams.statusFilter;
const query = stringifyUrlParams(crumbParams, true);
href += query === EMPTY_QUERY ? '' : query;
}

View file

@ -7,24 +7,11 @@
import { useEffect } from 'react';
import { useUrlParams } from './use_url_params';
/**
* Handle an added or removed value to filter against for an uptime field.
* @param fieldName the name of the field to filter against
* @param values the list of values to use when filter a field
*/
interface SelectedFilters {
selectedTags: string[];
selectedPorts: string[];
selectedSchemes: string[];
selectedLocations: string[];
selectedFilters: Map<string, string[]>;
}
export const useFilterUpdate = (
fieldName?: string,
values?: string[],
shouldUpdateUrl: boolean = true
): SelectedFilters => {
) => {
const [getUrlParams, updateUrl] = useUrlParams();
const { filters: currentFilters } = getUrlParams();
@ -62,12 +49,4 @@ export const useFilterUpdate = (
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fieldName, values]);
return {
selectedTags: filterKueries.get('tags') || [],
selectedPorts: filterKueries.get('url.port') || [],
selectedSchemes: filterKueries.get('monitor.type') || [],
selectedLocations: filterKueries.get('observer.geo.name') || [],
selectedFilters: filterKueries,
};
};

View file

@ -0,0 +1,28 @@
/*
* 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 { useMemo } from 'react';
import { useGetUrlParams } from './use_url_params';
export const useSelectedFilters = () => {
const { filters } = useGetUrlParams();
return useMemo(() => {
let selectedFilters: Map<string, string[]>;
try {
selectedFilters = new Map<string, string[]>(JSON.parse(filters));
} catch {
selectedFilters = new Map<string, string[]>();
}
return {
selectedTags: selectedFilters.get('tags') || [],
selectedPorts: selectedFilters.get('url.port') || [],
selectedSchemes: selectedFilters.get('monitor.type') || [],
selectedLocations: selectedFilters.get('observer.geo.name') || [],
};
}, [filters]);
};

View file

@ -66,7 +66,6 @@ export const mockStore = {
loading: false,
pingList: {
total: 0,
locations: [],
pings: [],
},
},

View file

@ -16,11 +16,10 @@ describe('stringifyUrlParams', () => {
filters: 'monitor.id: bar',
focusConnectorField: true,
search: 'monitor.id: foo',
selectedPingStatus: 'down',
statusFilter: 'up',
});
expect(result).toMatchInlineSnapshot(
`"?autorefreshInterval=50000&autorefreshIsPaused=false&dateRangeStart=now-15m&dateRangeEnd=now&filters=monitor.id%3A%20bar&focusConnectorField=true&search=monitor.id%3A%20foo&selectedPingStatus=down&statusFilter=up"`
`"?autorefreshInterval=50000&autorefreshIsPaused=false&dateRangeStart=now-15m&dateRangeEnd=now&filters=monitor.id%3A%20bar&focusConnectorField=true&search=monitor.id%3A%20foo&statusFilter=up"`
);
});
@ -34,7 +33,6 @@ describe('stringifyUrlParams', () => {
filters: 'monitor.id: bar',
focusConnectorField: false,
search: undefined,
selectedPingStatus: undefined,
statusFilter: '',
pagination: undefined,
},
@ -46,6 +44,5 @@ describe('stringifyUrlParams', () => {
expect(result.includes('pagination')).toBeFalsy();
expect(result.includes('search')).toBeFalsy();
expect(result.includes('selectedPingStatus')).toBeFalsy();
});
});

View file

@ -8,6 +8,7 @@
import moment from 'moment';
import { Moment } from 'moment-timezone';
import * as redux from 'react-redux';
export function mockMoment() {
// avoid timezone issues
@ -20,3 +21,9 @@ export function mockMoment() {
return `15 minutes ago`;
});
}
export function mockReduxHooks(response?: any) {
jest.spyOn(redux, 'useDispatch').mockReturnValue(jest.fn());
jest.spyOn(redux, 'useSelector').mockReturnValue(response);
}

View file

@ -12,7 +12,6 @@ Object {
"focusConnectorField": false,
"pagination": undefined,
"search": "",
"selectedPingStatus": "",
"statusFilter": "",
}
`;
@ -29,7 +28,6 @@ Object {
"focusConnectorField": false,
"pagination": undefined,
"search": "",
"selectedPingStatus": "",
"statusFilter": "",
}
`;
@ -46,7 +44,6 @@ Object {
"focusConnectorField": false,
"pagination": undefined,
"search": "monitor.status: down",
"selectedPingStatus": "up",
"statusFilter": "",
}
`;
@ -63,7 +60,6 @@ Object {
"focusConnectorField": false,
"pagination": undefined,
"search": "",
"selectedPingStatus": "",
"statusFilter": "",
}
`;
@ -80,7 +76,6 @@ Object {
"focusConnectorField": false,
"pagination": undefined,
"search": "",
"selectedPingStatus": "",
"statusFilter": "",
}
`;

View file

@ -46,7 +46,6 @@ describe('getSupportedUrlParams', () => {
DATE_RANGE_END,
FILTERS,
SEARCH,
SELECTED_PING_LIST_STATUS,
STATUS_FILTER,
} = CLIENT_DEFAULTS;
const result = getSupportedUrlParams({});
@ -62,7 +61,6 @@ describe('getSupportedUrlParams', () => {
focusConnectorField: false,
pagination: undefined,
search: SEARCH,
selectedPingStatus: SELECTED_PING_LIST_STATUS,
statusFilter: STATUS_FILTER,
});
});

View file

@ -19,7 +19,6 @@ export interface UptimeUrlParams {
pagination?: string;
filters: string;
search: string;
selectedPingStatus: string;
statusFilter: string;
focusConnectorField?: boolean;
}
@ -32,7 +31,6 @@ const {
DATE_RANGE_START,
DATE_RANGE_END,
SEARCH,
SELECTED_PING_LIST_STATUS,
FILTERS,
STATUS_FILTER,
} = CLIENT_DEFAULTS;
@ -73,13 +71,13 @@ export const getSupportedUrlParams = (params: {
dateRangeEnd,
filters,
search,
selectedPingStatus,
statusFilter,
pagination,
focusConnectorField,
} = filteredParams;
return {
pagination,
absoluteDateRangeStart: parseAbsoluteDate(
dateRangeStart || DATE_RANGE_START,
ABSOLUTE_DATE_RANGE_START
@ -95,10 +93,7 @@ export const getSupportedUrlParams = (params: {
dateRangeEnd: dateRangeEnd || DATE_RANGE_END,
filters: filters || FILTERS,
search: search || SEARCH,
selectedPingStatus:
selectedPingStatus === undefined ? SELECTED_PING_LIST_STATUS : selectedPingStatus,
statusFilter: statusFilter || STATUS_FILTER,
pagination,
focusConnectorField: !!focusConnectorField,
};
};

View file

@ -82,7 +82,7 @@ export const MonitorPage: React.FC = () => {
<EuiSpacer size="s" />
<MonitorCharts monitorId={monitorId} />
<EuiSpacer size="s" />
<PingList monitorId={monitorId} />
<PingList />
</>
);
};

View file

@ -4,9 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { HttpFetchError } from 'src/core/public';
import { fetchSnapshotCount } from '../snapshot';
import { apiService } from '../utils';
import { HttpFetchError } from 'src/core/public';
describe('snapshot API', () => {
let fetchMock: jest.SpyInstance<Partial<unknown>>;
@ -15,8 +15,9 @@ describe('snapshot API', () => {
beforeEach(() => {
apiService.http = {
get: jest.fn(),
fetch: jest.fn(),
} as any;
fetchMock = jest.spyOn(apiService.http, 'get');
fetchMock = jest.spyOn(apiService.http, 'fetch');
mockResponse = { up: 3, down: 12, total: 15 };
});
@ -31,7 +32,9 @@ describe('snapshot API', () => {
dateRangeEnd: 'now',
filters: 'monitor.id:"auto-http-0X21EE76EAC459873F"',
});
expect(fetchMock).toHaveBeenCalledWith('/api/uptime/snapshot/count', {
expect(fetchMock).toHaveBeenCalledWith({
asResponse: false,
path: '/api/uptime/snapshot/count',
query: {
dateRangeEnd: 'now',
dateRangeStart: 'now-15m',

View file

@ -20,3 +20,40 @@ export async function fetchJourneySteps(
SyntheticsJourneyApiResponseType
)) as SyntheticsJourneyApiResponse;
}
export async function fetchJourneysFailedSteps({
checkGroups,
}: {
checkGroups: string[];
}): Promise<SyntheticsJourneyApiResponse> {
return (await apiService.get(
`/api/uptime/journeys/failed_steps`,
{ checkGroups },
SyntheticsJourneyApiResponseType
)) as SyntheticsJourneyApiResponse;
}
export async function getJourneyScreenshot(imgSrc: string) {
try {
const imgRequest = new Request(imgSrc);
const response = await fetch(imgRequest);
if (response.status !== 200) {
return null;
}
const imgBlob = await response.blob();
const stepName = response.headers.get('caption-name');
const maxSteps = response.headers.get('max-steps');
return {
stepName,
maxSteps: Number(maxSteps ?? 0),
src: URL.createObjectURL(imgBlob),
};
} catch (e) {
return null;
}
}

View file

@ -59,8 +59,8 @@ class ApiService {
return ApiService.instance;
}
public async get(apiUrl: string, params?: HttpFetchQuery, decodeType?: any) {
const response = await this._http!.get(apiUrl, { query: params });
public async get(apiUrl: string, params?: HttpFetchQuery, decodeType?: any, asResponse = false) {
const response = await this._http!.fetch({ path: apiUrl, query: params, asResponse });
if (decodeType) {
const decoded = decodeType.decode(response);

View file

@ -17,7 +17,6 @@ export interface PingListState {
const initialState: PingListState = {
pingList: {
total: 0,
locations: [],
pings: [],
},
loading: false,
@ -36,6 +35,7 @@ export const pingListReducer = handleActions<PingListState, PingListPayload>(
...state,
pingList: { ...action.payload },
loading: false,
error: undefined,
}),
[String(getPingsFail)]: (state, action: Action<Error>) => ({

View file

@ -65,7 +65,6 @@ describe('state selectors', () => {
loading: false,
pingList: {
total: 0,
locations: [],
pings: [],
},
},

View file

@ -127,15 +127,6 @@ describe('getAll', () => {
Array [
Object {
"body": Object {
"aggs": Object {
"locations": Object {
"terms": Object {
"field": "observer.geo.name",
"missing": "N/A",
"size": 1000,
},
},
},
"query": Object {
"bool": Object {
"filter": Array [
@ -205,15 +196,6 @@ describe('getAll', () => {
Array [
Object {
"body": Object {
"aggs": Object {
"locations": Object {
"terms": Object {
"field": "observer.geo.name",
"missing": "N/A",
"size": 1000,
},
},
},
"query": Object {
"bool": Object {
"filter": Array [
@ -283,15 +265,6 @@ describe('getAll', () => {
Array [
Object {
"body": Object {
"aggs": Object {
"locations": Object {
"terms": Object {
"field": "observer.geo.name",
"missing": "N/A",
"size": 1000,
},
},
},
"query": Object {
"bool": Object {
"filter": Array [
@ -361,15 +334,6 @@ describe('getAll', () => {
Array [
Object {
"body": Object {
"aggs": Object {
"locations": Object {
"terms": Object {
"field": "observer.geo.name",
"missing": "N/A",
"size": 1000,
},
},
},
"query": Object {
"bool": Object {
"filter": Array [
@ -444,15 +408,6 @@ describe('getAll', () => {
Array [
Object {
"body": Object {
"aggs": Object {
"locations": Object {
"terms": Object {
"field": "observer.geo.name",
"missing": "N/A",
"size": 1000,
},
},
},
"query": Object {
"bool": Object {
"filter": Array [

View file

@ -0,0 +1,56 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { UMElasticsearchQueryFn } from '../adapters/framework';
import { Ping } from '../../../common/runtime_types';
interface GetJourneyStepsParams {
checkGroups: string[];
}
export const getJourneyFailedSteps: UMElasticsearchQueryFn<GetJourneyStepsParams, Ping> = async ({
uptimeEsClient,
checkGroups,
}) => {
const params = {
query: {
bool: {
filter: [
{
terms: {
'synthetics.type': ['step/end'],
},
},
{
exists: {
field: 'synthetics.error',
},
},
{
terms: {
'monitor.check_group': checkGroups,
},
},
],
},
},
sort: [{ 'synthetics.step.index': { order: 'asc' } }, { '@timestamp': { order: 'asc' } }],
_source: {
excludes: ['synthetics.blob'],
},
size: 500,
};
const { body: result } = await uptimeEsClient.search({ body: params });
return (result.hits.hits.map((h) => {
const source = h._source as Ping & { '@timestamp': string };
return {
...source,
timestamp: source['@timestamp'],
};
}) as unknown) as Ping;
};

View file

@ -5,7 +5,7 @@
*/
import { UMElasticsearchQueryFn } from '../adapters/framework';
import { ESSearchBody } from '../../../../../typings/elasticsearch';
import { Ping } from '../../../common/runtime_types/ping';
interface GetJourneyScreenshotParams {
checkGroup: string;
@ -16,7 +16,9 @@ export const getJourneyScreenshot: UMElasticsearchQueryFn<
GetJourneyScreenshotParams,
any
> = async ({ uptimeEsClient, checkGroup, stepIndex }) => {
const params: ESSearchBody = {
const params = {
track_total_hits: true,
size: 0,
query: {
bool: {
filter: [
@ -30,19 +32,38 @@ export const getJourneyScreenshot: UMElasticsearchQueryFn<
'synthetics.type': 'step/screenshot',
},
},
{
term: {
'synthetics.step.index': stepIndex,
},
},
],
},
},
_source: ['synthetics.blob'],
aggs: {
step: {
filter: {
term: {
'synthetics.step.index': stepIndex,
},
},
aggs: {
image: {
top_hits: {
size: 1,
_source: ['synthetics.blob', 'synthetics.step.name'],
},
},
},
},
},
};
const { body: result } = await uptimeEsClient.search({ body: params });
if (!Array.isArray(result?.hits?.hits) || result.hits.hits.length < 1) {
if (result?.hits?.total.value < 1) {
return null;
}
return result.hits.hits.map(({ _source }: any) => _source?.synthetics?.blob ?? null)[0];
const stepHit = result?.aggregations?.step.image.hits.hits[0]._source as Ping;
return {
blob: stepHit.synthetics?.blob ?? null,
stepName: stepHit?.synthetics?.step?.name ?? '',
totalSteps: result?.hits?.total.value,
};
};

View file

@ -59,7 +59,7 @@ export const getPings: UMElasticsearchQueryFn<GetPingsParams, PingsResponse> = a
status,
sort,
size: sizeParam,
location,
locations,
}) => {
const size = sizeParam ?? DEFAULT_PAGE_SIZE;
@ -77,27 +77,17 @@ export const getPings: UMElasticsearchQueryFn<GetPingsParams, PingsResponse> = a
},
},
sort: [{ '@timestamp': { order: (sort ?? 'desc') as 'asc' | 'desc' } }],
aggs: {
locations: {
terms: {
field: 'observer.geo.name',
missing: 'N/A',
size: 1000,
},
},
},
...(location ? { post_filter: { term: { 'observer.geo.name': location } } } : {}),
...((locations ?? []).length > 0
? { post_filter: { terms: { 'observer.geo.name': locations } } }
: {}),
};
const {
body: {
hits: { hits, total },
aggregations: aggs,
},
} = await uptimeEsClient.search({ body: searchBody });
const locations = aggs?.locations ?? { buckets: [{ key: 'N/A', doc_count: 0 }] };
const pings: Ping[] = hits.map((doc: any) => {
const { _id, _source } = doc;
// Calculate here the length of the content string in bytes, this is easier than in client JS, where
@ -113,7 +103,6 @@ export const getPings: UMElasticsearchQueryFn<GetPingsParams, PingsResponse> = a
return {
total: total.value,
locations: locations.buckets.map((bucket) => bucket.key as string),
pings,
};
};

View file

@ -20,6 +20,7 @@ import { getSnapshotCount } from './get_snapshot_counts';
import { getIndexStatus } from './get_index_status';
import { getJourneySteps } from './get_journey_steps';
import { getJourneyScreenshot } from './get_journey_screenshot';
import { getJourneyFailedSteps } from './get_journey_failed_steps';
export const requests = {
getCerts,
@ -37,6 +38,7 @@ export const requests = {
getSnapshotCount,
getIndexStatus,
getJourneySteps,
getJourneyFailedSteps,
getJourneyScreenshot,
};

View file

@ -24,6 +24,7 @@ import {
} from './monitors';
import { createGetMonitorDurationRoute } from './monitors/monitors_durations';
import { createGetIndexPatternRoute, createGetIndexStatusRoute } from './index_state';
import { createJourneyFailedStepsRoute } from './pings/journeys';
export * from './types';
export { createRouteWithAuth } from './create_route_with_auth';
@ -47,4 +48,5 @@ export const restApiRoutes: UMRestApiRouteFactory[] = [
createGetMonitorDurationRoute,
createJourneyRoute,
createJourneyScreenshotRoute,
createJourneyFailedStepsRoute,
];

View file

@ -5,12 +5,9 @@
*/
import { schema } from '@kbn/config-schema';
import { isLeft } from 'fp-ts/lib/Either';
import { PathReporter } from 'io-ts/lib/PathReporter';
import { UMServerLibs } from '../../lib/lib';
import { UMRestApiRouteFactory } from '../types';
import { API_URLS } from '../../../common/constants';
import { GetPingsParamsType } from '../../../common/runtime_types';
export const createGetPingsRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({
method: 'GET',
@ -19,7 +16,7 @@ export const createGetPingsRoute: UMRestApiRouteFactory = (libs: UMServerLibs) =
query: schema.object({
from: schema.string(),
to: schema.string(),
location: schema.maybe(schema.string()),
locations: schema.maybe(schema.string()),
monitorId: schema.maybe(schema.string()),
index: schema.maybe(schema.number()),
size: schema.maybe(schema.number()),
@ -28,17 +25,17 @@ export const createGetPingsRoute: UMRestApiRouteFactory = (libs: UMServerLibs) =
}),
},
handler: async ({ uptimeEsClient }, _context, request, response): Promise<any> => {
const { from, to, ...optional } = request.query;
const params = GetPingsParamsType.decode({ dateRange: { from, to }, ...optional });
if (isLeft(params)) {
// eslint-disable-next-line no-console
console.error(new Error(PathReporter.report(params).join(';')));
return response.badRequest({ body: { message: 'Received invalid request parameters.' } });
}
const { from, to, index, monitorId, status, sort, size, locations } = request.query;
const result = await libs.requests.getPings({
uptimeEsClient,
...params.right,
dateRange: { from, to },
index,
monitorId,
status,
sort,
size,
locations: locations ? JSON.parse(locations) : [],
});
return response.ok({

View file

@ -29,10 +29,12 @@ export const createJourneyScreenshotRoute: UMRestApiRouteFactory = (libs: UMServ
return response.notFound();
}
return response.ok({
body: Buffer.from(result, 'base64'),
body: Buffer.from(result.blob, 'base64'),
headers: {
'content-type': 'image/png',
'cache-control': 'max-age=600',
'caption-name': result.stepName,
'max-steps': result.totalSteps,
},
});
},

View file

@ -31,3 +31,27 @@ export const createJourneyRoute: UMRestApiRouteFactory = (libs: UMServerLibs) =>
});
},
});
export const createJourneyFailedStepsRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({
method: 'GET',
path: '/api/uptime/journeys/failed_steps',
validate: {
query: schema.object({
checkGroups: schema.arrayOf(schema.string()),
}),
},
handler: async ({ uptimeEsClient }, _context, request, response) => {
const { checkGroups } = request.query;
const result = await libs.requests.getJourneyFailedSteps({
uptimeEsClient,
checkGroups,
});
return response.ok({
body: {
checkGroups,
steps: result,
},
});
},
});

View file

@ -30,10 +30,9 @@ export default function ({ getService }: FtrProviderContext) {
const apiResponse = await supertest.get(`/api/uptime/pings?from=${from}&to=${to}&size=10`);
const { total, locations, pings } = decodePingsResponseData(apiResponse.body);
const { total, pings } = decodePingsResponseData(apiResponse.body);
expect(total).to.be(2000);
expect(locations).to.eql(['mpls']);
expect(pings).length(10);
expect(pings.map(({ monitor: { id } }) => id)).to.eql([
'0074-up',
@ -58,10 +57,9 @@ export default function ({ getService }: FtrProviderContext) {
`/api/uptime/pings?from=${from}&to=${to}&size=${size}`
);
const { total, locations, pings } = decodePingsResponseData(apiResponse.body);
const { total, pings } = decodePingsResponseData(apiResponse.body);
expect(total).to.be(2000);
expect(locations).to.eql(['mpls']);
expect(pings).length(50);
expect(pings.map(({ monitor: { id } }) => id)).to.eql([
'0074-up',
@ -127,10 +125,9 @@ export default function ({ getService }: FtrProviderContext) {
`/api/uptime/pings?from=${from}&to=${to}&monitorId=${monitorId}&size=${size}`
);
const { total, locations, pings } = decodePingsResponseData(apiResponse.body);
const { total, pings } = decodePingsResponseData(apiResponse.body);
expect(total).to.be(20);
expect(locations).to.eql(['mpls']);
pings.forEach(({ monitor: { id } }) => expect(id).to.eql('0001-up'));
expect(pings.map(({ timestamp }) => timestamp)).to.eql([
'2019-09-11T03:40:34.371Z',
@ -162,10 +159,9 @@ export default function ({ getService }: FtrProviderContext) {
`/api/uptime/pings?from=${from}&to=${to}&monitorId=${monitorId}&size=${size}&sort=${sort}`
);
const { total, locations, pings } = decodePingsResponseData(apiResponse.body);
const { total, pings } = decodePingsResponseData(apiResponse.body);
expect(total).to.be(20);
expect(locations).to.eql(['mpls']);
expect(pings.map(({ timestamp }) => timestamp)).to.eql([
'2019-09-11T03:31:04.380Z',
'2019-09-11T03:31:34.366Z',

View file

@ -32,23 +32,27 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await uptime.loadDataAndGoToMonitorPage(dateStart, dateEnd, monitorId);
});
it('should select the ping list location filter', async () => {
await uptimeService.common.selectFilterItem('location', 'mpls');
});
it('should set the status filter', async () => {
await uptimeService.common.setStatusFilterUp();
});
it('displays ping data as expected', async () => {
await uptime.checkPingListInteractions(
[
'XZtoHm0B0I9WX_CznN-6',
'7ZtoHm0B0I9WX_CzJ96M',
'pptnHm0B0I9WX_Czst5X',
'I5tnHm0B0I9WX_CzPd46',
'y5tmHm0B0I9WX_Czx93x',
'XZtmHm0B0I9WX_CzUt3H',
'-JtlHm0B0I9WX_Cz3dyX',
'k5tlHm0B0I9WX_CzaNxm',
'NZtkHm0B0I9WX_Cz89w9',
'zJtkHm0B0I9WX_CzftsN',
],
'mpls',
'up'
);
await uptime.checkPingListInteractions([
'XZtoHm0B0I9WX_CznN-6',
'7ZtoHm0B0I9WX_CzJ96M',
'pptnHm0B0I9WX_Czst5X',
'I5tnHm0B0I9WX_CzPd46',
'y5tmHm0B0I9WX_Czx93x',
'XZtmHm0B0I9WX_CzUt3H',
'-JtlHm0B0I9WX_Cz3dyX',
'k5tlHm0B0I9WX_CzaNxm',
'NZtkHm0B0I9WX_Cz89w9',
'zJtkHm0B0I9WX_CzftsN',
]);
});
});
});

View file

@ -109,17 +109,7 @@ export function UptimePageProvider({ getPageObjects, getService }: FtrProviderCo
return commonService.clickPageSizeSelectPopoverItem(size);
}
public async checkPingListInteractions(
timestamps: string[],
location?: string,
status?: string
): Promise<void> {
if (location) {
await monitor.setPingListLocation(location);
}
if (status) {
await monitor.setPingListStatus(status);
}
public async checkPingListInteractions(timestamps: string[]): Promise<void> {
return monitor.checkForPingListTimestamps(timestamps);
}