[7.x] [Uptime] Monitor availability reporting (#67790) (#69668)

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Shahzad 2020-06-26 21:02:10 +02:00 committed by GitHub
parent 39b2d71ce7
commit 775cd0b42a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
70 changed files with 3195 additions and 1672 deletions

View file

@ -16568,7 +16568,6 @@
"xpack.uptime.locationMap.locations.missing.message": "重要な位置情報構成がありません。{codeBlock}フィールドを使用して、アップタイムチェック用に一意の地域を作成できます。",
"xpack.uptime.locationMap.locations.missing.message1": "詳細については、ドキュメンテーションを参照してください。",
"xpack.uptime.locationMap.locations.missing.title": "地理情報の欠測",
"xpack.uptime.locationMap.locations.tags.others": "{otherLoc}その他 ...",
"xpack.uptime.locationName.helpLinkAnnotation": "場所を追加",
"xpack.uptime.ml.durationChart.exploreInMlApp": "ML アプリで探索",
"xpack.uptime.ml.enableAnomalyDetectionPanel.anomalyDetectionTitle": "異常検知",
@ -16652,12 +16651,7 @@
"xpack.uptime.monitorStatusBar.locations.oneLocStatus": "{loc}場所での{status}",
"xpack.uptime.monitorStatusBar.locations.upStatus": "{loc}場所での{status}",
"xpack.uptime.monitorStatusBar.monitorUrlLinkAriaLabel": "監視 URL リンク",
"xpack.uptime.monitorStatusBar.sslCertificate.overview": "証明書概要",
"xpack.uptime.monitorStatusBar.sslCertificate.title": "証明書",
"xpack.uptime.monitorStatusBar.sslCertificateExpired.badgeContent": "{emphasizedText}が期限切れになりました",
"xpack.uptime.monitorStatusBar.sslCertificateExpired.label.ariaLabel": "{validityDate}に期限切れになりました",
"xpack.uptime.monitorStatusBar.sslCertificateExpiry.badgeContent": "{emphasizedText}が期限切れになります",
"xpack.uptime.monitorStatusBar.sslCertificateExpiry.label.ariaLabel": "{validityDate}に期限切れになります",
"xpack.uptime.monitorStatusBar.timestampFromNowTextAriaLabel": "最終確認からの経過時間",
"xpack.uptime.navigateToAlertingButton.content": "アラートを管理",
"xpack.uptime.navigateToAlertingUi": "Uptime を離れてアラート管理ページに移動します",

View file

@ -16574,7 +16574,6 @@
"xpack.uptime.locationMap.locations.missing.message": "重要的地理位置配置缺失。您可以使用 {codeBlock} 字段为您的运行时间检查创建独特的地理区域。",
"xpack.uptime.locationMap.locations.missing.message1": "在我们的文档中获取更多的信息。",
"xpack.uptime.locationMap.locations.missing.title": "地理信息缺失",
"xpack.uptime.locationMap.locations.tags.others": "{otherLoc} 其他......",
"xpack.uptime.locationName.helpLinkAnnotation": "添加位置",
"xpack.uptime.ml.durationChart.exploreInMlApp": "在 ML 应用中浏览",
"xpack.uptime.ml.enableAnomalyDetectionPanel.anomalyDetectionTitle": "异常检测",
@ -16658,12 +16657,7 @@
"xpack.uptime.monitorStatusBar.locations.oneLocStatus": "在 {loc} 位置{status}",
"xpack.uptime.monitorStatusBar.locations.upStatus": "在 {loc} 位置{status}",
"xpack.uptime.monitorStatusBar.monitorUrlLinkAriaLabel": "监测 URL 链接",
"xpack.uptime.monitorStatusBar.sslCertificate.overview": "证书概览",
"xpack.uptime.monitorStatusBar.sslCertificate.title": "证书",
"xpack.uptime.monitorStatusBar.sslCertificateExpired.badgeContent": "{emphasizedText}过期",
"xpack.uptime.monitorStatusBar.sslCertificateExpired.label.ariaLabel": "已于 {validityDate}过期",
"xpack.uptime.monitorStatusBar.sslCertificateExpiry.badgeContent": "{emphasizedText}过期",
"xpack.uptime.monitorStatusBar.sslCertificateExpiry.label.ariaLabel": "将于 {validityDate}过期",
"xpack.uptime.monitorStatusBar.timestampFromNowTextAriaLabel": "自上次检查以来经过的时间",
"xpack.uptime.navigateToAlertingButton.content": "管理告警",
"xpack.uptime.navigateToAlertingUi": "离开 Uptime 并前往“Alerting 管理”页面",

View file

@ -6,15 +6,19 @@
import * as t from 'io-ts';
export const LocationType = t.partial({
export const LocationType = t.type({
lat: t.string,
lon: t.string,
});
export const CheckGeoType = t.partial({
export const CheckGeoType = t.intersection([
t.type({
name: t.string,
}),
t.partial({
location: LocationType,
});
}),
]);
export const SummaryType = t.partial({
up: t.number,
@ -34,5 +38,6 @@ export const DateRangeType = t.type({
export type Summary = t.TypeOf<typeof SummaryType>;
export type Location = t.TypeOf<typeof LocationType>;
export type GeoPoint = t.TypeOf<typeof CheckGeoType>;
export type StatesIndexStatus = t.TypeOf<typeof StatesIndexStatusType>;
export type DateRange = t.TypeOf<typeof DateRangeType>;

View file

@ -7,17 +7,23 @@ import * as t from 'io-ts';
import { CheckGeoType, SummaryType } from '../common';
// IO type for validation
export const MonitorLocationType = t.partial({
export const MonitorLocationType = t.type({
up_history: t.number,
down_history: t.number,
timestamp: t.string,
summary: SummaryType,
geo: CheckGeoType,
timestamp: t.string,
});
// Typescript type for type checking
export type MonitorLocation = t.TypeOf<typeof MonitorLocationType>;
export const MonitorLocationsType = t.intersection([
t.type({ monitorId: t.string }),
t.type({
monitorId: t.string,
up_history: t.number,
down_history: t.number,
}),
t.partial({ locations: t.array(MonitorLocationType) }),
]);
export type MonitorLocations = t.TypeOf<typeof MonitorLocationsType>;

View file

@ -18,6 +18,10 @@ export const EXPIRES_SOON = i18n.translate('xpack.uptime.certs.expireSoon', {
defaultMessage: 'Expires soon',
});
export const EXPIRES = i18n.translate('xpack.uptime.certs.expires', {
defaultMessage: 'Expires',
});
export const SEARCH_CERTS = i18n.translate('xpack.uptime.certs.searchCerts', {
defaultMessage: 'Search certificates',
});

View file

@ -0,0 +1,11 @@
/*
* 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 URL_LABEL = i18n.translate('xpack.uptime.monitorList.table.url.name', {
defaultMessage: 'Url',
});

View file

@ -6,7 +6,7 @@
export * from './ml';
export * from './ping_list';
export * from './location_map';
export * from './monitor_status_details';
export * from './status_details/location_map';
export * from './status_details';
export * from './ping_histogram';
export * from './monitor_charts';

View file

@ -1,282 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LocationMap component doesnt shows warning if geo is provided 1`] = `
<EuiErrorBoundary>
<Styled(EuiFlexGroup)
gutterSize="none"
justifyContent="flexEnd"
wrap={true}
>
<Styled(EuiFlexItem)>
<LocationStatusTags
locations={
Array [
Object {
"geo": Object {
"location": Object {
"lat": "40.730610",
"lon": " -73.935242",
},
"name": "New York",
},
"summary": Object {
"down": 0,
"up": 4,
},
"timestamp": "2020-01-13T22:50:06.536Z",
},
Object {
"geo": Object {
"location": Object {
"lat": "52.487448",
"lon": " 13.394798",
},
"name": "Tokyo",
},
"summary": Object {
"down": 0,
"up": 4,
},
"timestamp": "2020-01-13T22:50:04.354Z",
},
]
}
/>
</Styled(EuiFlexItem)>
<EuiHideFor
sizes={
Array [
"xs",
]
}
>
<EuiFlexItem
grow={false}
>
<styled.div>
<EmbeddedMap
downPoints={Array []}
upPoints={
Array [
Object {
"lat": "40.730610",
"lon": " -73.935242",
},
Object {
"lat": "52.487448",
"lon": " 13.394798",
},
]
}
/>
</styled.div>
</EuiFlexItem>
</EuiHideFor>
</Styled(EuiFlexGroup)>
</EuiErrorBoundary>
`;
exports[`LocationMap component renders correctly against snapshot 1`] = `
<EuiErrorBoundary>
<Styled(EuiFlexGroup)
gutterSize="none"
justifyContent="flexEnd"
wrap={true}
>
<Styled(EuiFlexItem)>
<LocationStatusTags
locations={
Array [
Object {
"geo": Object {
"location": Object {
"lat": "40.730610",
"lon": " -73.935242",
},
"name": "New York",
},
"summary": Object {
"down": 0,
"up": 4,
},
"timestamp": "2020-01-13T22:50:06.536Z",
},
Object {
"geo": Object {
"location": Object {
"lat": "52.487448",
"lon": " 13.394798",
},
"name": "Tokyo",
},
"summary": Object {
"down": 0,
"up": 4,
},
"timestamp": "2020-01-13T22:50:04.354Z",
},
Object {
"geo": Object {
"name": "Unnamed-location",
},
"summary": Object {
"down": 0,
"up": 4,
},
"timestamp": "2020-01-13T22:50:02.753Z",
},
]
}
/>
</Styled(EuiFlexItem)>
<EuiHideFor
sizes={
Array [
"xs",
]
}
>
<EuiFlexItem
grow={false}
>
<LocationMissingWarning />
<styled.div>
<EmbeddedMap
downPoints={Array []}
upPoints={
Array [
Object {
"lat": "40.730610",
"lon": " -73.935242",
},
Object {
"lat": "52.487448",
"lon": " 13.394798",
},
]
}
/>
</styled.div>
</EuiFlexItem>
</EuiHideFor>
</Styled(EuiFlexGroup)>
</EuiErrorBoundary>
`;
exports[`LocationMap component renders named locations that have missing geo data 1`] = `
<EuiErrorBoundary>
<Styled(EuiFlexGroup)
gutterSize="none"
justifyContent="flexEnd"
wrap={true}
>
<Styled(EuiFlexItem)>
<LocationStatusTags
locations={
Array [
Object {
"geo": Object {
"location": undefined,
"name": "New York",
},
"summary": Object {
"down": 0,
"up": 4,
},
"timestamp": "2020-01-13T22:50:06.536Z",
},
]
}
/>
</Styled(EuiFlexItem)>
<EuiHideFor
sizes={
Array [
"xs",
]
}
>
<EuiFlexItem
grow={false}
>
<LocationMissingWarning />
<styled.div>
<EmbeddedMap
downPoints={Array []}
upPoints={Array []}
/>
</styled.div>
</EuiFlexItem>
</EuiHideFor>
</Styled(EuiFlexGroup)>
</EuiErrorBoundary>
`;
exports[`LocationMap component shows warning if geo information is missing 1`] = `
<EuiErrorBoundary>
<Styled(EuiFlexGroup)
gutterSize="none"
justifyContent="flexEnd"
wrap={true}
>
<Styled(EuiFlexItem)>
<LocationStatusTags
locations={
Array [
Object {
"geo": Object {
"location": Object {
"lat": "52.487448",
"lon": " 13.394798",
},
"name": "Tokyo",
},
"summary": Object {
"down": 0,
"up": 4,
},
"timestamp": "2020-01-13T22:50:04.354Z",
},
Object {
"geo": Object {
"name": "Unnamed-location",
},
"summary": Object {
"down": 0,
"up": 4,
},
"timestamp": "2020-01-13T22:50:02.753Z",
},
]
}
/>
</Styled(EuiFlexItem)>
<EuiHideFor
sizes={
Array [
"xs",
]
}
>
<EuiFlexItem
grow={false}
>
<LocationMissingWarning />
<styled.div>
<EmbeddedMap
downPoints={Array []}
upPoints={
Array [
Object {
"lat": "52.487448",
"lon": " 13.394798",
},
]
}
/>
</styled.div>
</EuiFlexItem>
</EuiHideFor>
</Styled(EuiFlexGroup)>
</EuiErrorBoundary>
`;

View file

@ -1,682 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LocationStatusTags component renders properly against props 1`] = `
<Fragment>
<styled.div>
<span>
<styled.div
key="0"
>
<EuiBadge
color="#bd271e"
>
<EuiText
size="m"
>
<styled.div>
Berlin
</styled.div>
</EuiText>
</EuiBadge>
<styled.span>
<EuiText
color="subdued"
>
1 Mon ago
</EuiText>
</styled.span>
</styled.div>
</span>
<span>
<styled.div
key="0"
>
<EuiBadge
color="#d3dae6"
>
<EuiText
size="m"
>
<styled.div>
Berlin
</styled.div>
</EuiText>
</EuiBadge>
<styled.span>
<EuiText
color="subdued"
>
1 Mon ago
</EuiText>
</styled.span>
</styled.div>
<styled.div
key="1"
>
<EuiBadge
color="#d3dae6"
>
<EuiText
size="m"
>
<styled.div>
Islamabad
</styled.div>
</EuiText>
</EuiBadge>
<styled.span>
<EuiText
color="subdued"
>
1 Mon ago
</EuiText>
</styled.span>
</styled.div>
</span>
</styled.div>
</Fragment>
`;
exports[`LocationStatusTags component renders when all locations are down 1`] = `
.c3 {
display: inline-block;
margin-left: 4px;
}
.c2 {
font-weight: 600;
}
.c1 {
margin-bottom: 5px;
white-space: nowrap;
}
.c0 {
max-height: 229px;
overflow: hidden;
margin-top: auto;
}
@media (max-width:1042px) {
.c1 {
display: inline-block;
margin-right: 16px;
}
}
<div
class="c0"
>
<span>
<div
class="c1"
>
<span
class="euiBadge euiBadge--iconLeft"
style="background-color:#bd271e;color:#fff"
>
<span
class="euiBadge__content"
>
<span
class="euiBadge__text"
>
<div
class="euiText euiText--medium"
>
<div
class="c2"
>
Islamabad
</div>
</div>
</span>
</span>
</span>
<span
class="c3"
>
<div
class="euiText euiText--medium"
>
<div
class="euiTextColor euiTextColor--subdued"
>
5s ago
</div>
</div>
</span>
</div>
<div
class="c1"
>
<span
class="euiBadge euiBadge--iconLeft"
style="background-color:#bd271e;color:#fff"
>
<span
class="euiBadge__content"
>
<span
class="euiBadge__text"
>
<div
class="euiText euiText--medium"
>
<div
class="c2"
>
Berlin
</div>
</div>
</span>
</span>
</span>
<span
class="c3"
>
<div
class="euiText euiText--medium"
>
<div
class="euiTextColor euiTextColor--subdued"
>
5m ago
</div>
</div>
</span>
</div>
</span>
<span />
</div>
`;
exports[`LocationStatusTags component renders when all locations are up 1`] = `
.c3 {
display: inline-block;
margin-left: 4px;
}
.c2 {
font-weight: 600;
}
.c1 {
margin-bottom: 5px;
white-space: nowrap;
}
.c0 {
max-height: 229px;
overflow: hidden;
margin-top: auto;
}
@media (max-width:1042px) {
.c1 {
display: inline-block;
margin-right: 16px;
}
}
<div
class="c0"
>
<span />
<span>
<div
class="c1"
>
<span
class="euiBadge euiBadge--iconLeft"
style="background-color:#d3dae6;color:#000"
>
<span
class="euiBadge__content"
>
<span
class="euiBadge__text"
>
<div
class="euiText euiText--medium"
>
<div
class="c2"
>
Berlin
</div>
</div>
</span>
</span>
</span>
<span
class="c3"
>
<div
class="euiText euiText--medium"
>
<div
class="euiTextColor euiTextColor--subdued"
>
5d ago
</div>
</div>
</span>
</div>
<div
class="c1"
>
<span
class="euiBadge euiBadge--iconLeft"
style="background-color:#d3dae6;color:#000"
>
<span
class="euiBadge__content"
>
<span
class="euiBadge__text"
>
<div
class="euiText euiText--medium"
>
<div
class="c2"
>
Islamabad
</div>
</div>
</span>
</span>
</span>
<span
class="c3"
>
<div
class="euiText euiText--medium"
>
<div
class="euiTextColor euiTextColor--subdued"
>
5s ago
</div>
</div>
</span>
</div>
</span>
</div>
`;
exports[`LocationStatusTags component renders when there are many location 1`] = `
Array [
.c3 {
display: inline-block;
margin-left: 4px;
}
.c2 {
font-weight: 600;
}
.c1 {
margin-bottom: 5px;
white-space: nowrap;
}
.c0 {
max-height: 229px;
overflow: hidden;
margin-top: auto;
}
@media (max-width:1042px) {
.c1 {
display: inline-block;
margin-right: 16px;
}
}
<div
class="c0"
>
<span>
<div
class="c1"
>
<span
class="euiBadge euiBadge--iconLeft"
style="background-color:#bd271e;color:#fff"
>
<span
class="euiBadge__content"
>
<span
class="euiBadge__text"
>
<div
class="euiText euiText--medium"
>
<div
class="c2"
>
Islamabad
</div>
</div>
</span>
</span>
</span>
<span
class="c3"
>
<div
class="euiText euiText--medium"
>
<div
class="euiTextColor euiTextColor--subdued"
>
5s ago
</div>
</div>
</span>
</div>
<div
class="c1"
>
<span
class="euiBadge euiBadge--iconLeft"
style="background-color:#bd271e;color:#fff"
>
<span
class="euiBadge__content"
>
<span
class="euiBadge__text"
>
<div
class="euiText euiText--medium"
>
<div
class="c2"
>
Berlin
</div>
</div>
</span>
</span>
</span>
<span
class="c3"
>
<div
class="euiText euiText--medium"
>
<div
class="euiTextColor euiTextColor--subdued"
>
5m ago
</div>
</div>
</span>
</div>
<div
class="c1"
>
<span
class="euiBadge euiBadge--iconLeft"
style="background-color:#bd271e;color:#fff"
>
<span
class="euiBadge__content"
>
<span
class="euiBadge__text"
>
<div
class="euiText euiText--medium"
>
<div
class="c2"
>
st-paul
</div>
</div>
</span>
</span>
</span>
<span
class="c3"
>
<div
class="euiText euiText--medium"
>
<div
class="euiTextColor euiTextColor--subdued"
>
5h ago
</div>
</div>
</span>
</div>
<div
class="c1"
>
<span
class="euiBadge euiBadge--iconLeft"
style="background-color:#bd271e;color:#fff"
>
<span
class="euiBadge__content"
>
<span
class="euiBadge__text"
>
<div
class="euiText euiText--medium"
>
<div
class="c2"
>
Tokyo
</div>
</div>
</span>
</span>
</span>
<span
class="c3"
>
<div
class="euiText euiText--medium"
>
<div
class="euiTextColor euiTextColor--subdued"
>
5d ago
</div>
</div>
</span>
</div>
<div
class="c1"
>
<span
class="euiBadge euiBadge--iconLeft"
style="background-color:#bd271e;color:#fff"
>
<span
class="euiBadge__content"
>
<span
class="euiBadge__text"
>
<div
class="euiText euiText--medium"
>
<div
class="c2"
>
New York
</div>
</div>
</span>
</span>
</span>
<span
class="c3"
>
<div
class="euiText euiText--medium"
>
<div
class="euiTextColor euiTextColor--subdued"
>
1 Mon ago
</div>
</div>
</span>
</div>
<div
class="c1"
>
<span
class="euiBadge euiBadge--iconLeft"
style="background-color:#bd271e;color:#fff"
>
<span
class="euiBadge__content"
>
<span
class="euiBadge__text"
>
<div
class="euiText euiText--medium"
>
<div
class="c2"
>
Toronto
</div>
</div>
</span>
</span>
</span>
<span
class="c3"
>
<div
class="euiText euiText--medium"
>
<div
class="euiTextColor euiTextColor--subdued"
>
5 Mon ago
</div>
</div>
</span>
</div>
<div
class="c1"
>
<span
class="euiBadge euiBadge--iconLeft"
style="background-color:#bd271e;color:#fff"
>
<span
class="euiBadge__content"
>
<span
class="euiBadge__text"
>
<div
class="euiText euiText--medium"
>
<div
class="c2"
>
Sydney
</div>
</div>
</span>
</span>
</span>
<span
class="c3"
>
<div
class="euiText euiText--medium"
>
<div
class="euiTextColor euiTextColor--subdued"
>
5 Yr ago
</div>
</div>
</span>
</div>
<div
class="c1"
>
<span
class="euiBadge euiBadge--iconLeft"
style="background-color:#bd271e;color:#fff"
>
<span
class="euiBadge__content"
>
<span
class="euiBadge__text"
>
<div
class="euiText euiText--medium"
>
<div
class="c2"
>
Paris
</div>
</div>
</span>
</span>
</span>
<span
class="c3"
>
<div
class="euiText euiText--medium"
>
<div
class="euiTextColor euiTextColor--subdued"
>
5 Yr ago
</div>
</div>
</span>
</div>
</span>
<span />
</div>,
.c0 {
padding-left: 18px;
}
@media (max-width:1042px) {
}
<div
class="c0"
>
<div
class="euiText euiText--medium"
>
<div
class="euiTextColor euiTextColor--subdued"
>
<h4>
1 Others ...
</h4>
</div>
</div>
</div>,
]
`;

View file

@ -1,95 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import styled from 'styled-components';
import { EuiFlexGroup, EuiFlexItem, EuiErrorBoundary, EuiHideFor } from '@elastic/eui';
import { LocationStatusTags } from './location_status_tags';
import { EmbeddedMap, LocationPoint } from './embeddables/embedded_map';
import { MonitorLocations, MonitorLocation } from '../../../../common/runtime_types';
import { UNNAMED_LOCATION } from '../../../../common/constants';
import { LocationMissingWarning } from './location_missing';
// These height/width values are used to make sure map is in center of panel
// And to make sure, it doesn't take too much space
const MapPanel = styled.div`
height: 240px;
width: 520px;
@media (min-width: 1300px) {
margin-right: 20px;
}
@media (max-width: 574px) {
height: 250px;
width: 100%;
margin-right: 0;
}
`;
const EuiFlexItemTags = styled(EuiFlexItem)`
padding-top: 5px;
@media (max-width: 1042px) {
flex-basis: 80% !important;
flex-grow: 0 !important;
order: 1;
}
`;
const FlexGroup = styled(EuiFlexGroup)`
@media (max-width: 850px) {
justify-content: center;
}
`;
interface LocationMapProps {
monitorLocations: MonitorLocations;
}
export const LocationMap = ({ monitorLocations }: LocationMapProps) => {
const upPoints: LocationPoint[] = [];
const downPoints: LocationPoint[] = [];
let isGeoInfoMissing = false;
if (monitorLocations?.locations) {
monitorLocations.locations.forEach((item: MonitorLocation) => {
if (item.geo?.name === UNNAMED_LOCATION || !item.geo?.location) {
isGeoInfoMissing = true;
} else if (
item.geo?.name !== UNNAMED_LOCATION &&
!!item.geo.location.lat &&
!!item.geo.location.lon
) {
// TypeScript doesn't infer that the above checks in this block's condition
// ensure that lat and lon are defined when we try to pass the location object directly,
// but if we destructure the values it does. Improvement to this block is welcome.
const { lat, lon } = item.geo.location;
if (item?.summary?.down === 0) {
upPoints.push({ lat, lon });
} else {
downPoints.push({ lat, lon });
}
}
});
}
return (
<EuiErrorBoundary>
<FlexGroup wrap={true} gutterSize="none" justifyContent="flexEnd">
<EuiFlexItemTags>
<LocationStatusTags locations={monitorLocations?.locations || []} />
</EuiFlexItemTags>
<EuiHideFor sizes={['xs']}>
<EuiFlexItem grow={false}>
{isGeoInfoMissing && <LocationMissingWarning />}
<MapPanel>
<EmbeddedMap upPoints={upPoints} downPoints={downPoints} />
</MapPanel>
</EuiFlexItem>
</EuiHideFor>
</FlexGroup>
</EuiErrorBoundary>
);
};

View file

@ -1,130 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useContext } from 'react';
import moment from 'moment';
import styled from 'styled-components';
import { EuiBadge, EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { UptimeThemeContext } from '../../../contexts';
import { MonitorLocation } from '../../../../common/runtime_types';
import { SHORT_TIMESPAN_LOCALE, SHORT_TS_LOCALE } from '../../../../common/constants';
const TimeStampSpan = styled.span`
display: inline-block;
margin-left: 4px;
`;
const TextStyle = styled.div`
font-weight: 600;
`;
const BadgeItem = styled.div`
margin-bottom: 5px;
white-space: nowrap;
@media (max-width: 1042px) {
display: inline-block;
margin-right: 16px;
}
`;
// Set height so that it remains within panel, enough height to display 7 locations tags
const TagContainer = styled.div`
max-height: 229px;
overflow: hidden;
margin-top: auto;
`;
const OtherLocationsDiv = styled.div`
padding-left: 18px;
`;
interface Props {
locations: MonitorLocation[];
}
interface StatusTag {
label: string;
timestamp: number;
}
export const LocationStatusTags = ({ locations }: Props) => {
const {
colors: { gray, danger },
} = useContext(UptimeThemeContext);
const upLocations: StatusTag[] = [];
const downLocations: StatusTag[] = [];
locations.forEach((item: any) => {
if (item.summary.down === 0) {
upLocations.push({ label: item.geo.name, timestamp: new Date(item.timestamp).valueOf() });
} else {
downLocations.push({ label: item.geo.name, timestamp: new Date(item.timestamp).valueOf() });
}
});
// Sort lexicographically by label
upLocations.sort((a, b) => {
return a.label > b.label ? 1 : b.label > a.label ? -1 : 0;
});
const tagLabel = (item: StatusTag, ind: number, color: string) => {
return (
<BadgeItem key={ind}>
<EuiBadge color={color}>
<EuiText size="m">
<TextStyle>{item.label}</TextStyle>
</EuiText>
</EuiBadge>
<TimeStampSpan>
<EuiText color="subdued">{moment(item.timestamp).fromNow()}</EuiText>
</TimeStampSpan>
</BadgeItem>
);
};
const prevLocal: string = moment.locale() ?? 'en';
const renderTags = () => {
const shortLocale = moment.locale(SHORT_TS_LOCALE) === SHORT_TS_LOCALE;
if (!shortLocale) {
moment.defineLocale(SHORT_TS_LOCALE, SHORT_TIMESPAN_LOCALE);
}
const tags = (
<TagContainer>
<span>{downLocations.map((item, ind) => tagLabel(item, ind, danger))}</span>
<span>{upLocations.map((item, ind) => tagLabel(item, ind, gray))}</span>
</TagContainer>
);
// Need to reset locale so it doesn't effect other parts of the app
moment.locale(prevLocal);
return tags;
};
return (
<>
{renderTags()}
{locations.length > 7 && (
<OtherLocationsDiv>
<EuiText color="subdued">
<h4>
<FormattedMessage
id="xpack.uptime.locationMap.locations.tags.others"
defaultMessage="{otherLoc} Others ..."
values={{
otherLoc: locations.length - 7,
}}
/>
</h4>
</EuiText>
</OtherLocationsDiv>
)}
</>
);
};

View file

@ -1,55 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`MonitorStatusBar component renders duration in ms, not us 1`] = `
<div
class="euiFlexGroup euiFlexGroup--directionColumn"
>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
>
<div
class="euiText euiText--medium"
>
<h2>
Up in 2 Locations
</h2>
</div>
</div>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
>
<div
class="euiText euiText--medium"
>
<a
aria-label="Monitor URL link"
class="euiLink euiLink--primary"
href="https://www.example.com/"
rel="noopener noreferrer"
target="_blank"
>
https://www.example.com/
</a>
</div>
</div>
<div
class="euiFlexItem"
>
<span
class="euiTextColor euiTextColor--subdued euiTitle euiTitle--xsmall"
>
<h1
data-test-subj="monitor-page-title"
>
id1
</h1>
</span>
</div>
<div
class="euiSpacer euiSpacer--l"
/>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
/>
</div>
`;

View file

@ -1,100 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import moment from 'moment';
import { i18n } from '@kbn/i18n';
import { Link } from 'react-router-dom';
import { EuiSpacer, EuiText, EuiBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { Tls } from '../../../../../common/runtime_types';
import { useCertStatus } from '../../../../hooks';
import { CERT_STATUS, CERTIFICATES_ROUTE } from '../../../../../common/constants';
interface Props {
/**
* TLS information coming from monitor in ES heartbeat index
*/
tls: Tls | null | undefined;
}
export const MonitorSSLCertificate = ({ tls }: Props) => {
const certStatus = useCertStatus(tls?.not_after);
const isExpiringSoon = certStatus === CERT_STATUS.EXPIRING_SOON;
const isExpired = certStatus === CERT_STATUS.EXPIRED;
const relativeDate = moment(tls?.not_after).fromNow();
return certStatus ? (
<>
<EuiText>
{i18n.translate('xpack.uptime.monitorStatusBar.sslCertificate.title', {
defaultMessage: 'Certificate:',
})}
</EuiText>
<EuiSpacer size="s" />
<EuiFlexGroup wrap>
<EuiFlexItem grow={false}>
<EuiText
className="eui-displayInline"
grow={false}
size="s"
aria-label={
isExpired
? i18n.translate(
'xpack.uptime.monitorStatusBar.sslCertificateExpired.label.ariaLabel',
{
defaultMessage: 'Expired {validityDate}',
values: { validityDate: relativeDate },
}
)
: i18n.translate(
'xpack.uptime.monitorStatusBar.sslCertificateExpiry.label.ariaLabel',
{
defaultMessage: 'Expires {validityDate}',
values: { validityDate: relativeDate },
}
)
}
>
{isExpired ? (
<FormattedMessage
id="xpack.uptime.monitorStatusBar.sslCertificateExpired.badgeContent"
defaultMessage="Expired {emphasizedText}"
values={{
emphasizedText: <EuiBadge color={'danger'}>{relativeDate}</EuiBadge>,
}}
/>
) : (
<FormattedMessage
id="xpack.uptime.monitorStatusBar.sslCertificateExpiry.badgeContent"
defaultMessage="Expires {emphasizedText}"
values={{
emphasizedText: (
<EuiBadge color={isExpiringSoon ? 'warning' : 'default'}>
{relativeDate}
</EuiBadge>
),
}}
/>
)}
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<Link to={CERTIFICATES_ROUTE} className="eui-displayInline">
<EuiText style={{ whiteSpace: 'nowrap' }}>
{i18n.translate('xpack.uptime.monitorStatusBar.sslCertificate.overview', {
defaultMessage: 'Certificate overview',
})}
</EuiText>
</Link>
</EuiFlexItem>
</EuiFlexGroup>
</>
) : null;
};

View file

@ -1,61 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import {
EuiLink,
EuiTitle,
EuiTextColor,
EuiSpacer,
EuiText,
EuiFlexGroup,
EuiFlexItem,
} from '@elastic/eui';
import { MonitorSSLCertificate } from './ssl_certificate';
import * as labels from './translations';
import { StatusByLocations } from './status_by_location';
import { Ping } from '../../../../../common/runtime_types';
import { MonitorLocations } from '../../../../../common/runtime_types';
interface MonitorStatusBarProps {
monitorId: string;
monitorStatus: Ping | null;
monitorLocations: MonitorLocations;
}
export const MonitorStatusBarComponent: React.FC<MonitorStatusBarProps> = ({
monitorId,
monitorStatus,
monitorLocations,
}) => {
const full = monitorStatus?.url?.full ?? '';
return (
<EuiFlexGroup direction="column" gutterSize="none" responsive={false}>
<EuiFlexItem grow={false}>
<StatusByLocations locations={monitorLocations?.locations ?? []} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText>
<EuiLink aria-label={labels.monitorUrlLinkAriaLabel} href={full} target="_blank">
{full}
</EuiLink>
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiTitle size="xs">
<EuiTextColor color="subdued">
<h1 data-test-subj="monitor-page-title">{monitorId}</h1>
</EuiTextColor>
</EuiTitle>
</EuiFlexItem>
<EuiSpacer />
<EuiFlexItem grow={false}>
<MonitorSSLCertificate tls={monitorStatus?.tls} />
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -1,50 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
export const healthStatusMessageAriaLabel = i18n.translate(
'xpack.uptime.monitorStatusBar.healthStatusMessageAriaLabel',
{
defaultMessage: 'Monitor status',
}
);
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 monitorUrlLinkAriaLabel = i18n.translate(
'xpack.uptime.monitorStatusBar.monitorUrlLinkAriaLabel',
{
defaultMessage: 'Monitor URL link',
}
);
export const durationTextAriaLabel = i18n.translate(
'xpack.uptime.monitorStatusBar.durationTextAriaLabel',
{
defaultMessage: 'Monitor duration in milliseconds',
}
);
export const timestampFromNowTextAriaLabel = i18n.translate(
'xpack.uptime.monitorStatusBar.timestampFromNowTextAriaLabel',
{
defaultMessage: 'Time since last check',
}
);
export const loadingMessage = i18n.translate('xpack.uptime.monitorStatusBar.loadingMessage', {
defaultMessage: 'Loading…',
});

View file

@ -0,0 +1,73 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`MonitorStatusBar component renders 1`] = `
Array [
<div>
<div
class="euiText euiText--medium"
>
<h2>
Up in 2 Locations
</h2>
</div>
</div>,
<div
class="euiSpacer euiSpacer--l"
/>,
.c0.c0.c0 {
width: 35%;
}
.c1.c1.c1 {
width: 65%;
overflow-wrap: anywhere;
}
<dl
class="euiDescriptionList euiDescriptionList--column euiDescriptionList--reverse euiDescriptionList--compressed"
style="max-width:450px"
>
<dt
class="euiDescriptionList__title c0"
>
Overall availability
</dt>
<dd
class="euiDescriptionList__description c1"
data-test-subj="uptimeOverallAvailability"
>
0.00 %
</dd>
<dt
class="euiDescriptionList__title c0"
>
Url
</dt>
<dd
class="euiDescriptionList__description c1"
>
<a
aria-label="Monitor URL link"
class="euiLink euiLink--primary"
href=""
rel="noopener noreferrer"
target="_blank"
>
<div
data-euiicon-type="popout"
/>
</a>
</dd>
<dt
class="euiDescriptionList__title c0"
>
Monitor ID
</dt>
<dd
class="euiDescriptionList__description c1"
data-test-subj="monitor-page-title"
/>
</dl>,
]
`;

View file

@ -2,61 +2,91 @@
exports[`SSL Certificate component renders 1`] = `
Array [
<div
class="euiText euiText--medium"
.c0.c0.c0 {
width: 35%;
}
<dt
class="euiDescriptionList__title c0"
>
Certificate:
</div>,
TLS Certificate
</dt>,
<div
class="euiSpacer euiSpacer--s"
/>,
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive euiFlexGroup--wrap"
>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
>
<div
aria-label="Expires in 2 months"
class="euiText euiText--small eui-displayInline euiText--constrainedWidth"
>
Expires
<span
class="euiBadge euiBadge--iconLeft"
style="background-color:#d3dae6;color:#000"
>
<span
class="euiBadge__content"
>
<span
class="euiBadge__text"
>
in 2 months
</span>
</span>
</span>
</div>
</div>
<div
class="euiFlexItem"
.c0.c0.c0 {
width: 65%;
overflow-wrap: anywhere;
}
.c1.c1.c1 {
margin: 0 0 0 4px;
display: inline-block;
color: inherit;
}
<dd
class="euiDescriptionList__description c0"
>
<a
class="eui-displayInline"
href="/certificates"
>
<div
class="euiText euiText--medium"
style="white-space:nowrap"
<span
class="euiToolTipAnchor"
>
Certificate overview
<div
class="euiText euiText--small"
>
<div
color="success"
data-euiicon-type="lock"
/>
<h4
class="c1"
>
Expires in 2 months
</h4>
</div>
</span>
</a>
</div>
</div>,
</dd>,
]
`;
exports[`SSL Certificate component renders null if invalid date 1`] = `null`;
exports[`SSL Certificate component renders null if invalid date 1`] = `
Array [
.c0.c0.c0 {
width: 35%;
}
<dt
class="euiDescriptionList__title c0"
>
TLS Certificate
</dt>,
<div
class="euiSpacer euiSpacer--s"
/>,
.c0.c0.c0 {
width: 65%;
overflow-wrap: anywhere;
}
<dd
class="euiDescriptionList__description c0"
>
<a
class="eui-displayInline"
href="/certificates"
>
<span>
--
</span>
</a>
</dd>,
]
`;
exports[`SSL Certificate component shallow renders 1`] = `
<ContextProvider

View file

@ -6,10 +6,11 @@
import moment from 'moment';
import React from 'react';
import { renderWithIntl } from 'test_utils/enzyme_helpers';
import { MonitorStatusBarComponent } from '../monitor_status_bar';
import { MonitorStatusBar } from '../status_bar';
import { Ping } from '../../../../../common/runtime_types';
import * as redux from 'react-redux';
import { renderWithRouter } from '../../../../lib';
import { createMemoryHistory } from 'history';
describe('MonitorStatusBar component', () => {
let monitorStatus: Ping;
@ -49,18 +50,21 @@ describe('MonitorStatusBar component', () => {
const spy = jest.spyOn(redux, 'useDispatch');
spy.mockReturnValue(jest.fn());
const spy1 = jest.spyOn(redux, 'useSelector');
spy1.mockReturnValue(true);
jest.spyOn(redux, 'useSelector').mockImplementation((fn, d) => {
if (fn.name === ' monitorStatusSelector') {
return monitorStatus;
} else {
return monitorLocations;
}
});
});
it('renders duration in ms, not us', () => {
const component = renderWithIntl(
<MonitorStatusBarComponent
monitorStatus={monitorStatus}
monitorId="id1"
monitorLocations={monitorLocations}
/>
);
it('renders', () => {
const history = createMemoryHistory({
initialEntries: ['/aWQx/'],
});
history.location.key = 'test';
const component = renderWithRouter(<MonitorStatusBar />, history);
expect(component).toMatchSnapshot();
});
});

View file

@ -6,9 +6,9 @@
import React from 'react';
import moment from 'moment';
import { EuiBadge } from '@elastic/eui';
import { EuiIcon } from '@elastic/eui';
import { Tls } from '../../../../../common/runtime_types';
import { MonitorSSLCertificate } from '../monitor_status_bar';
import { MonitorSSLCertificate } from '../status_bar';
import * as redux from 'react-redux';
import { mountWithRouter, renderWithRouter, shallowWithRouter } from '../../../../lib';
import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../../common/constants';
@ -58,14 +58,12 @@ describe('SSL Certificate component', () => {
};
const component = mountWithRouter(<MonitorSSLCertificate tls={monitorTls} />);
const badgeComponent = component.find(EuiBadge);
const lockIcon = component.find(EuiIcon);
expect(badgeComponent.props().color).toBe('warning');
expect(lockIcon.props().color).toBe('warning');
const badgeComponentText = component.find('.euiBadge__text');
expect(badgeComponentText.text()).toBe(moment(dateIn5Days).fromNow());
expect(badgeComponent.find('span.euiBadge--warning')).toBeTruthy();
const componentText = component.find('h4');
expect(componentText.text()).toBe('Expires soon ' + moment(dateIn5Days).fromNow());
});
it('does not render the expiration date with a warning state if expiry date is greater than a month', () => {
@ -75,12 +73,10 @@ describe('SSL Certificate component', () => {
};
const component = mountWithRouter(<MonitorSSLCertificate tls={monitorTls} />);
const badgeComponent = component.find(EuiBadge);
expect(badgeComponent.props().color).toBe('default');
const lockIcon = component.find(EuiIcon);
expect(lockIcon.props().color).toBe('success');
const badgeComponentText = component.find('.euiBadge__text');
expect(badgeComponentText.text()).toBe(moment(dateIn40Days).fromNow());
expect(badgeComponent.find('span.euiBadge--warning')).toHaveLength(0);
const componentText = component.find('h4');
expect(componentText.text()).toBe('Expires ' + moment(dateIn40Days).fromNow());
});
});

View file

@ -17,10 +17,16 @@ describe('StatusByLocation component', () => {
{
summary: { up: 4, down: 0 },
geo: { name: 'Berlin', location: { lat: '52.487448', lon: ' 13.394798' } },
up_history: 4,
down_history: 0,
timestamp: '2020-01-13T22:50:06.536Z',
},
{
summary: { up: 4, down: 0 },
geo: { name: 'st-paul', location: { lat: '52.487448', lon: ' 13.394798' } },
up_history: 4,
down_history: 0,
timestamp: '2020-01-13T22:50:06.536Z',
},
];
const component = shallowWithIntl(<StatusByLocations locations={monitorLocations} />);
@ -32,10 +38,16 @@ describe('StatusByLocation component', () => {
{
summary: { up: 4, down: 0 },
geo: { name: 'Berlin', location: { lat: '52.487448', lon: ' 13.394798' } },
up_history: 4,
down_history: 0,
timestamp: '2020-01-13T22:50:06.536Z',
},
{
summary: { up: 4, down: 0 },
geo: { name: 'st-paul', location: { lat: '52.487448', lon: ' 13.394798' } },
up_history: 4,
down_history: 0,
timestamp: '2020-01-13T22:50:06.536Z',
},
];
const component = renderWithIntl(<StatusByLocations locations={monitorLocations} />);
@ -47,6 +59,9 @@ describe('StatusByLocation component', () => {
{
summary: { up: 4, down: 0 },
geo: { name: 'Berlin', location: { lat: '52.487448', lon: ' 13.394798' } },
up_history: 4,
down_history: 0,
timestamp: '2020-01-13T22:50:06.536Z',
},
];
const component = renderWithIntl(<StatusByLocations locations={monitorLocations} />);
@ -58,6 +73,9 @@ describe('StatusByLocation component', () => {
{
summary: { up: 0, down: 4 },
geo: { name: 'Berlin', location: { lat: '52.487448', lon: ' 13.394798' } },
up_history: 4,
down_history: 0,
timestamp: '2020-01-13T22:50:06.536Z',
},
];
const component = renderWithIntl(<StatusByLocations locations={monitorLocations} />);
@ -69,10 +87,16 @@ describe('StatusByLocation component', () => {
{
summary: { up: 0, down: 4 },
geo: { name: 'Berlin', location: { lat: '52.487448', lon: ' 13.394798' } },
up_history: 4,
down_history: 0,
timestamp: '2020-01-13T22:50:06.536Z',
},
{
summary: { up: 0, down: 4 },
geo: { name: 'st-paul', location: { lat: '52.487448', lon: ' 13.394798' } },
up_history: 4,
down_history: 0,
timestamp: '2020-01-13T22:50:06.536Z',
},
];
const component = renderWithIntl(<StatusByLocations locations={monitorLocations} />);
@ -84,10 +108,16 @@ describe('StatusByLocation component', () => {
{
summary: { up: 0, down: 4 },
geo: { name: 'Berlin', location: { lat: '52.487448', lon: ' 13.394798' } },
up_history: 4,
down_history: 0,
timestamp: '2020-01-13T22:50:06.536Z',
},
{
summary: { up: 4, down: 0 },
geo: { name: 'st-paul', location: { lat: '52.487448', lon: ' 13.394798' } },
up_history: 4,
down_history: 0,
timestamp: '2020-01-13T22:50:06.536Z',
},
];
const component = renderWithIntl(<StatusByLocations locations={monitorLocations} />);

View file

@ -0,0 +1,381 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AvailabilityReporting component renders correctly against snapshot 1`] = `
Array [
@media (max-width:1042px) {
}
<div
class="euiSpacer euiSpacer--s"
/>,
.c0 {
white-space: nowrap;
display: inline-block;
}
@media (max-width:1042px) {
.c0 {
display: inline-block;
margin-right: 16px;
}
}
<div
class="euiBasicTable"
>
<div>
<table
class="euiTable euiTable--compressed"
>
<caption
class="euiScreenReaderOnly euiTableCaption"
/>
<thead>
<tr>
<th
class="euiTableHeaderCell"
data-test-subj="tableHeaderCell_label_0"
role="columnheader"
scope="col"
>
<div
class="euiTableCellContent"
>
<span
class="euiTableCellContent__text"
>
Location
</span>
</div>
</th>
<th
class="euiTableHeaderCell"
data-test-subj="tableHeaderCell_availability_1"
role="columnheader"
scope="col"
>
<div
class="euiTableCellContent euiTableCellContent--alignRight"
>
<span
class="euiTableCellContent__text"
>
Availability
</span>
</div>
</th>
<th
class="euiTableHeaderCell"
data-test-subj="tableHeaderCell_timestamp_2"
role="columnheader"
scope="col"
>
<div
class="euiTableCellContent euiTableCellContent--alignRight"
>
<span
class="euiTableCellContent__text"
>
Last check
</span>
</div>
</th>
</tr>
</thead>
<tbody>
<tr
class="euiTableRow"
>
<td
class="euiTableRowCell"
>
<div
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
>
Location
</div>
<div
class="euiTableCellContent euiTableCellContent--truncateText euiTableCellContent--overflowingContent"
>
<div
class="c0"
>
<span
class="euiBadge euiBadge--iconLeft"
style="background-color:#d3dae6;color:#000"
>
<span
class="euiBadge__content"
>
<span
class="euiBadge__text"
>
<div
class="euiText euiText--small"
>
<h4>
au-heartbeat
</h4>
</div>
</span>
</span>
</span>
</div>
</div>
</td>
<td
class="euiTableRowCell"
>
<div
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
>
Availability
</div>
<div
class="euiTableCellContent euiTableCellContent--alignRight euiTableCellContent--overflowingContent"
>
<span
class=""
>
100.00 %
</span>
</div>
</td>
<td
class="euiTableRowCell"
>
<div
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
>
Last check
</div>
<div
class="euiTableCellContent euiTableCellContent--alignRight"
>
<span
class="euiTableCellContent__text"
>
36m ago
</span>
</div>
</td>
</tr>
<tr
class="euiTableRow"
>
<td
class="euiTableRowCell"
>
<div
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
>
Location
</div>
<div
class="euiTableCellContent euiTableCellContent--truncateText euiTableCellContent--overflowingContent"
>
<div
class="c0"
>
<span
class="euiBadge euiBadge--iconLeft"
style="background-color:#d3dae6;color:#000"
>
<span
class="euiBadge__content"
>
<span
class="euiBadge__text"
>
<div
class="euiText euiText--small"
>
<h4>
nyc-heartbeat
</h4>
</div>
</span>
</span>
</span>
</div>
</div>
</td>
<td
class="euiTableRowCell"
>
<div
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
>
Availability
</div>
<div
class="euiTableCellContent euiTableCellContent--alignRight euiTableCellContent--overflowingContent"
>
<span
class=""
>
100.00 %
</span>
</div>
</td>
<td
class="euiTableRowCell"
>
<div
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
>
Last check
</div>
<div
class="euiTableCellContent euiTableCellContent--alignRight"
>
<span
class="euiTableCellContent__text"
>
36m ago
</span>
</div>
</td>
</tr>
<tr
class="euiTableRow"
>
<td
class="euiTableRowCell"
>
<div
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
>
Location
</div>
<div
class="euiTableCellContent euiTableCellContent--truncateText euiTableCellContent--overflowingContent"
>
<div
class="c0"
>
<span
class="euiBadge euiBadge--iconLeft"
style="background-color:#d3dae6;color:#000"
>
<span
class="euiBadge__content"
>
<span
class="euiBadge__text"
>
<div
class="euiText euiText--small"
>
<h4>
spa-heartbeat
</h4>
</div>
</span>
</span>
</span>
</div>
</div>
</td>
<td
class="euiTableRowCell"
>
<div
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
>
Availability
</div>
<div
class="euiTableCellContent euiTableCellContent--alignRight euiTableCellContent--overflowingContent"
>
<span
class=""
>
100.00 %
</span>
</div>
</td>
<td
class="euiTableRowCell"
>
<div
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
>
Last check
</div>
<div
class="euiTableCellContent euiTableCellContent--alignRight"
>
<span
class="euiTableCellContent__text"
>
36m ago
</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>,
]
`;
exports[`AvailabilityReporting component shallow renders correctly against snapshot 1`] = `
<Fragment>
<EuiSpacer
size="s"
/>
<EuiBasicTable
columns={
Array [
Object {
"field": "label",
"name": "Location",
"render": [Function],
"truncateText": true,
},
Object {
"align": "right",
"field": "availability",
"name": "Availability",
"render": [Function],
},
Object {
"align": "right",
"field": "timestamp",
"name": "Last check",
},
]
}
compressed={true}
items={
Array [
Object {
"availability": 100,
"color": "#d3dae6",
"label": "au-heartbeat",
"timestamp": "36m ago",
},
Object {
"availability": 100,
"color": "#d3dae6",
"label": "nyc-heartbeat",
"timestamp": "36m ago",
},
Object {
"availability": 100,
"color": "#d3dae6",
"label": "spa-heartbeat",
"timestamp": "36m ago",
},
]
}
noItemsMessage="No items found"
onChange={[Function]}
responsive={false}
tableLayout="fixed"
/>
</Fragment>
`;

View file

@ -0,0 +1,56 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TagLabel component renders correctly against snapshot 1`] = `
.c0 {
white-space: nowrap;
display: inline-block;
}
@media (max-width:1042px) {
.c0 {
display: inline-block;
margin-right: 16px;
}
}
<div
class="c0"
>
<span
class="euiBadge euiBadge--iconLeft"
style="background-color:#fff;color:#000"
>
<span
class="euiBadge__content"
>
<span
class="euiBadge__text"
>
<div
class="euiText euiText--small"
>
<h4>
US-East
</h4>
</div>
</span>
</span>
</span>
</div>
`;
exports[`TagLabel component shallow render correctly against snapshot 1`] = `
<styled.div>
<EuiBadge
color="#fff"
>
<EuiText
size="s"
>
<h4>
US-East
</h4>
</EuiText>
</EuiBadge>
</styled.div>
`;

View file

@ -0,0 +1,42 @@
/*
* 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 { renderWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers';
import { AvailabilityReporting } from '../availability_reporting';
import { StatusTag } from '../location_status_tags';
describe('AvailabilityReporting component', () => {
let allLocations: StatusTag[];
beforeEach(() => {
allLocations = [
{
label: 'au-heartbeat',
timestamp: '36m ago',
color: '#d3dae6',
availability: 100,
},
{
label: 'nyc-heartbeat',
timestamp: '36m ago',
color: '#d3dae6',
availability: 100,
},
{ label: 'spa-heartbeat', timestamp: '36m ago', color: '#d3dae6', availability: 100 },
];
});
it('shallow renders correctly against snapshot', () => {
const component = shallowWithIntl(<AvailabilityReporting allLocations={allLocations} />);
expect(component).toMatchSnapshot();
});
it('renders correctly against snapshot', () => {
const component = renderWithIntl(<AvailabilityReporting allLocations={allLocations} />);
expect(component).toMatchSnapshot();
});
});

View file

@ -7,7 +7,7 @@
import React from 'react';
import moment from 'moment';
import { renderWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers';
import { MonitorLocation } from '../../../../../common/runtime_types/monitor';
import { MonitorLocation } from '../../../../../../common/runtime_types/monitor';
import { LocationStatusTags } from '../index';
describe('LocationStatusTags component', () => {
@ -19,16 +19,22 @@ describe('LocationStatusTags component', () => {
summary: { up: 4, down: 0 },
geo: { name: 'Islamabad', location: { lat: '52.487448', lon: ' 13.394798' } },
timestamp: moment().subtract('5', 'w').toISOString(),
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(),
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(),
up_history: 4,
down_history: 0,
},
];
const component = shallowWithIntl(<LocationStatusTags locations={monitorLocations} />);
@ -41,41 +47,57 @@ describe('LocationStatusTags component', () => {
summary: { up: 0, down: 1 },
geo: { name: 'Islamabad', location: { lat: '52.487448', lon: ' 13.394798' } },
timestamp: moment().subtract('5', 's').toISOString(),
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(),
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(),
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(),
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(),
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(),
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(),
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(),
up_history: 4,
down_history: 0,
},
];
const component = renderWithIntl(<LocationStatusTags locations={monitorLocations} />);
@ -88,11 +110,15 @@ describe('LocationStatusTags component', () => {
summary: { up: 4, down: 0 },
geo: { name: 'Islamabad', location: { lat: '52.487448', lon: ' 13.394798' } },
timestamp: moment().subtract('5', 's').toISOString(),
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(),
up_history: 4,
down_history: 0,
},
];
const component = renderWithIntl(<LocationStatusTags locations={monitorLocations} />);
@ -105,11 +131,15 @@ describe('LocationStatusTags component', () => {
summary: { up: 0, down: 2 },
geo: { name: 'Islamabad', location: { lat: '52.487448', lon: ' 13.394798' } },
timestamp: moment().subtract('5', 's').toISOString(),
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(),
up_history: 4,
down_history: 0,
},
];
const component = renderWithIntl(<LocationStatusTags locations={monitorLocations} />);

View file

@ -0,0 +1,21 @@
/*
* 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 { renderWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers';
import { TagLabel } from '../tag_label';
describe('TagLabel component', () => {
it('shallow render correctly against snapshot', () => {
const component = shallowWithIntl(<TagLabel color={'#fff'} label={'US-East'} />);
expect(component).toMatchSnapshot();
});
it('renders correctly against snapshot', () => {
const component = renderWithIntl(<TagLabel color={'#fff'} label={'US-East'} />);
expect(component).toMatchSnapshot();
});
});

View file

@ -0,0 +1,87 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useState } from 'react';
import { EuiBasicTable, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { Pagination } from '@elastic/eui/src/components/basic_table/pagination_bar';
import { StatusTag } from './location_status_tags';
import { TagLabel } from './tag_label';
import { AvailabilityLabel, LastCheckLabel, LocationLabel } from '../translations';
interface Props {
allLocations: StatusTag[];
}
export const formatAvailabilityValue = (val: number) => {
const result = Math.round(val * 100) / 100;
return result.toFixed(2);
};
export const AvailabilityReporting: React.FC<Props> = ({ allLocations }) => {
const [pageIndex, setPageIndex] = useState(0);
const cols = [
{
field: 'label',
name: LocationLabel,
truncateText: true,
render: (val: string, item: StatusTag) => {
return <TagLabel color={item.color} label={item.label} />;
},
},
{
field: 'availability',
name: AvailabilityLabel,
align: 'right' as const,
render: (val: number) => {
return (
<span>
<FormattedMessage
id="xpack.uptime.availabilityLabelText"
defaultMessage="{value} %"
values={{ value: formatAvailabilityValue(val) }}
description="A percentage value, like 23.5%"
/>
</span>
);
},
},
{
name: LastCheckLabel,
field: 'timestamp',
align: 'right' as const,
},
];
const pageSize = 5;
const pagination: Pagination = {
pageIndex,
pageSize,
totalItemCount: allLocations.length,
hidePerPageOptions: true,
};
const onTableChange = ({ page }: any) => {
setPageIndex(page.index);
};
const paginationProps = allLocations.length > pageSize ? { pagination } : {};
return (
<>
<EuiSpacer size="s" />
<EuiBasicTable
responsive={false}
compressed={true}
columns={cols}
items={allLocations.slice(pageIndex * pageSize, pageIndex * pageSize + pageSize)}
onChange={onTableChange}
{...paginationProps}
/>
</>
);
};

View file

@ -0,0 +1,9 @@
/*
* 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.
*/
export { AvailabilityReporting } from './availability_reporting';
export { LocationStatusTags } from './location_status_tags';
export { TagLabel } from './tag_label';

View file

@ -0,0 +1,73 @@
/*
* 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 moment from 'moment';
import styled from 'styled-components';
import { UptimeThemeContext } from '../../../../contexts';
import { MonitorLocation } from '../../../../../common/runtime_types';
import { SHORT_TIMESPAN_LOCALE, SHORT_TS_LOCALE } from '../../../../../common/constants';
import { AvailabilityReporting } from '../index';
// Set height so that it remains within panel, enough height to display 7 locations tags
const TagContainer = styled.div`
max-height: 246px;
overflow: hidden;
`;
interface Props {
locations: MonitorLocation[];
}
export interface StatusTag {
label: string;
timestamp: string;
color: string;
availability: number;
}
export const LocationStatusTags = ({ locations }: Props) => {
const {
colors: { gray, danger },
} = useContext(UptimeThemeContext);
const allLocations: StatusTag[] = [];
const prevLocal: string = moment.locale() ?? 'en';
const shortLocale = moment.locale(SHORT_TS_LOCALE) === SHORT_TS_LOCALE;
if (!shortLocale) {
moment.defineLocale(SHORT_TS_LOCALE, SHORT_TIMESPAN_LOCALE);
}
locations.forEach((item: MonitorLocation) => {
allLocations.push({
label: item.geo.name!,
timestamp: moment(new Date(item.timestamp).valueOf()).fromNow(),
color: item.summary.down === 0 ? gray : danger,
availability: (item.up_history / (item.up_history + item.down_history)) * 100,
});
});
// Need to reset locale so it doesn't effect other parts of the app
moment.locale(prevLocal);
// Sort lexicographically by label
allLocations.sort((a, b) => {
return a.label > b.label ? 1 : b.label > a.label ? -1 : 0;
});
if (allLocations.length === 0) {
return null;
}
return (
<>
<TagContainer>
<AvailabilityReporting allLocations={allLocations} />
</TagContainer>
</>
);
};

View file

@ -0,0 +1,35 @@
/*
* 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, EuiText } from '@elastic/eui';
const BadgeItem = styled.div`
white-space: nowrap;
display: inline-block;
@media (max-width: 1042px) {
display: inline-block;
margin-right: 16px;
}
`;
interface Props {
color: string;
label: string;
}
export const TagLabel: React.FC<Props> = ({ color, label }) => {
return (
<BadgeItem>
<EuiBadge color={color}>
<EuiText size="s">
<h4>{label}</h4>
</EuiText>
</EuiBadge>
</BadgeItem>
);
};

View file

@ -4,9 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { MonitorStatusBarComponent } from './monitor_status_bar';
export { MonitorStatusDetailsComponent } from './status_details';
export { StatusByLocations } from './monitor_status_bar/status_by_location';
export { StatusByLocations } from './status_bar/status_by_location';
export { MonitorStatusDetails } from './status_details_container';
export { MonitorStatusBar } from './monitor_status_bar/status_bar_container';
export { MonitorStatusBar } from './status_bar/status_bar';
export { AvailabilityReporting } from './availability_reporting/availability_reporting';

View file

@ -0,0 +1,280 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LocationAvailability component doesnt shows warning if geo is provided 1`] = `
<EuiErrorBoundary>
<EuiFlexGroup
gutterSize="none"
responsive={false}
style={
Object {
"flexGrow": 0,
}
}
>
<EuiFlexItem>
<EuiTitle
size="s"
>
<h3>
Monitoring from
</h3>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<ToggleViewBtn
onChange={[Function]}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup
gutterSize="none"
justifyContent="flexEnd"
wrap={true}
>
<Styled(EuiFlexItem)
grow={true}
>
<LocationStatusTags
locations={
Array [
Object {
"down_history": 0,
"geo": Object {
"location": Object {
"lat": "40.730610",
"lon": " -73.935242",
},
"name": "New York",
},
"summary": Object {
"down": 0,
"up": 4,
},
"timestamp": "2020-01-13T22:50:06.536Z",
"up_history": 4,
},
Object {
"down_history": 0,
"geo": Object {
"location": Object {
"lat": "52.487448",
"lon": " 13.394798",
},
"name": "Tokyo",
},
"summary": Object {
"down": 0,
"up": 4,
},
"timestamp": "2020-01-13T22:50:04.354Z",
"up_history": 4,
},
]
}
/>
</Styled(EuiFlexItem)>
</EuiFlexGroup>
</EuiErrorBoundary>
`;
exports[`LocationAvailability component renders correctly against snapshot 1`] = `
<EuiErrorBoundary>
<EuiFlexGroup
gutterSize="none"
responsive={false}
style={
Object {
"flexGrow": 0,
}
}
>
<EuiFlexItem>
<EuiTitle
size="s"
>
<h3>
Monitoring from
</h3>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<ToggleViewBtn
onChange={[Function]}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup
gutterSize="none"
justifyContent="flexEnd"
wrap={true}
>
<Styled(EuiFlexItem)
grow={true}
>
<LocationStatusTags
locations={
Array [
Object {
"down_history": 0,
"geo": Object {
"location": Object {
"lat": "40.730610",
"lon": " -73.935242",
},
"name": "New York",
},
"summary": Object {
"down": 0,
"up": 4,
},
"timestamp": "2020-01-13T22:50:06.536Z",
"up_history": 4,
},
Object {
"down_history": 0,
"geo": Object {
"location": Object {
"lat": "52.487448",
"lon": " 13.394798",
},
"name": "Tokyo",
},
"summary": Object {
"down": 0,
"up": 4,
},
"timestamp": "2020-01-13T22:50:04.354Z",
"up_history": 4,
},
Object {
"down_history": 0,
"geo": Object {
"name": "Unnamed-location",
},
"summary": Object {
"down": 0,
"up": 4,
},
"timestamp": "2020-01-13T22:50:02.753Z",
"up_history": 4,
},
]
}
/>
</Styled(EuiFlexItem)>
</EuiFlexGroup>
</EuiErrorBoundary>
`;
exports[`LocationAvailability component renders named locations that have missing geo data 1`] = `
<EuiErrorBoundary>
<EuiFlexGroup
gutterSize="none"
responsive={false}
style={
Object {
"flexGrow": 0,
}
}
>
<EuiFlexItem>
<EuiTitle
size="s"
>
<h3>
Monitoring from
</h3>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<ToggleViewBtn
onChange={[Function]}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup
gutterSize="none"
justifyContent="flexEnd"
wrap={true}
>
<Styled(EuiFlexItem)
grow={true}
>
<LocationStatusTags
locations={
Array [
Object {
"down_history": 0,
"geo": Object {
"location": undefined,
"name": "New York",
},
"summary": Object {
"down": 0,
"up": 4,
},
"timestamp": "2020-01-13T22:50:06.536Z",
"up_history": 4,
},
]
}
/>
</Styled(EuiFlexItem)>
</EuiFlexGroup>
</EuiErrorBoundary>
`;
exports[`LocationAvailability component shows warning if geo information is missing 1`] = `
<EuiErrorBoundary>
<EuiFlexGroup
gutterSize="none"
responsive={false}
style={
Object {
"flexGrow": 0,
}
}
>
<EuiFlexItem>
<LocationMissingWarning />
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<ToggleViewBtn
onChange={[Function]}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup
gutterSize="none"
justifyContent="flexEnd"
wrap={true}
>
<EuiFlexItem
grow={false}
>
<LocationMap
downPoints={Array []}
upPoints={
Array [
Object {
"location": Object {
"lat": "52.487448",
"lon": " 13.394798",
},
"name": "Tokyo",
},
]
}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiErrorBoundary>
`;

View file

@ -6,59 +6,105 @@
import React from 'react';
import { shallowWithIntl } from 'test_utils/enzyme_helpers';
import { LocationMap } from '../location_map';
import { MonitorLocations } from '../../../../../common/runtime_types';
import { LocationMissingWarning } from '../location_missing';
import { LocationAvailability } from '../location_availability';
import { MonitorLocations } from '../../../../../../common/runtime_types';
import { LocationMissingWarning } from '../../location_map/location_missing';
class LocalStorageMock {
store: Record<string, string>;
constructor() {
this.store = { 'xpack.uptime.detailPage.selectedView': 'list' };
}
clear() {
this.store = {};
}
getItem(key: string) {
return this.store[key] || null;
}
setItem(key: string, value: string) {
this.store[key] = value.toString();
}
removeItem(key: string) {
delete this.store[key];
}
}
// Note For shallow test, we need absolute time strings
describe('LocationMap component', () => {
describe('LocationAvailability component', () => {
let monitorLocations: MonitorLocations;
beforeEach(() => {
// @ts-ignore replacing a call to localStorage we use for monitor list size
global.localStorage = new LocalStorageMock();
// @ts-ignore replacing a call to localStorage we use for monitor list size
global.localStorage.setItem('xpack.uptime.detailPage.selectedView', 'list');
monitorLocations = {
monitorId: 'wapo',
up_history: 12,
down_history: 0,
locations: [
{
summary: { up: 4, down: 0 },
geo: { name: 'New York', location: { lat: '40.730610', lon: ' -73.935242' } },
timestamp: '2020-01-13T22:50:06.536Z',
up_history: 4,
down_history: 0,
},
{
summary: { up: 4, down: 0 },
geo: { name: 'Tokyo', location: { lat: '52.487448', lon: ' 13.394798' } },
timestamp: '2020-01-13T22:50:04.354Z',
up_history: 4,
down_history: 0,
},
{
summary: { up: 4, down: 0 },
geo: { name: 'Unnamed-location' },
timestamp: '2020-01-13T22:50:02.753Z',
up_history: 4,
down_history: 0,
},
],
};
});
it('renders correctly against snapshot', () => {
const component = shallowWithIntl(<LocationMap monitorLocations={monitorLocations} />);
const component = shallowWithIntl(<LocationAvailability monitorLocations={monitorLocations} />);
expect(component).toMatchSnapshot();
});
it('shows warning if geo information is missing', () => {
// @ts-ignore replacing a call to localStorage we use for monitor list size
global.localStorage.setItem('xpack.uptime.detailPage.selectedView', 'map');
monitorLocations = {
monitorId: 'wapo',
up_history: 8,
down_history: 0,
locations: [
{
summary: { up: 4, down: 0 },
geo: { name: 'Tokyo', location: { lat: '52.487448', lon: ' 13.394798' } },
timestamp: '2020-01-13T22:50:04.354Z',
up_history: 4,
down_history: 0,
},
{
summary: { up: 4, down: 0 },
geo: { name: 'Unnamed-location' },
timestamp: '2020-01-13T22:50:02.753Z',
up_history: 4,
down_history: 0,
},
],
};
const component = shallowWithIntl(<LocationMap monitorLocations={monitorLocations} />);
const component = shallowWithIntl(<LocationAvailability monitorLocations={monitorLocations} />);
expect(component).toMatchSnapshot();
const warningComponent = component.find(LocationMissingWarning);
@ -68,20 +114,26 @@ describe('LocationMap component', () => {
it('doesnt shows warning if geo is provided', () => {
monitorLocations = {
monitorId: 'wapo',
up_history: 8,
down_history: 0,
locations: [
{
summary: { up: 4, down: 0 },
geo: { name: 'New York', location: { lat: '40.730610', lon: ' -73.935242' } },
timestamp: '2020-01-13T22:50:06.536Z',
up_history: 4,
down_history: 0,
},
{
summary: { up: 4, down: 0 },
geo: { name: 'Tokyo', location: { lat: '52.487448', lon: ' 13.394798' } },
timestamp: '2020-01-13T22:50:04.354Z',
up_history: 4,
down_history: 0,
},
],
};
const component = shallowWithIntl(<LocationMap monitorLocations={monitorLocations} />);
const component = shallowWithIntl(<LocationAvailability monitorLocations={monitorLocations} />);
expect(component).toMatchSnapshot();
const warningComponent = component.find(LocationMissingWarning);
@ -91,16 +143,20 @@ describe('LocationMap component', () => {
it('renders named locations that have missing geo data', () => {
monitorLocations = {
monitorId: 'wapo',
up_history: 4,
down_history: 0,
locations: [
{
summary: { up: 4, down: 0 },
geo: { name: 'New York', location: undefined },
timestamp: '2020-01-13T22:50:06.536Z',
up_history: 4,
down_history: 0,
},
],
};
const component = shallowWithIntl(<LocationMap monitorLocations={monitorLocations} />);
const component = shallowWithIntl(<LocationAvailability monitorLocations={monitorLocations} />);
expect(component).toMatchSnapshot();
});
});

View file

@ -0,0 +1,90 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useState } from 'react';
import styled from 'styled-components';
import { EuiFlexGroup, EuiFlexItem, EuiErrorBoundary, EuiTitle } from '@elastic/eui';
import { LocationStatusTags } from '../availability_reporting';
import { LocationPoint } from '../location_map/embeddables/embedded_map';
import { MonitorLocations, MonitorLocation } from '../../../../../common/runtime_types';
import { UNNAMED_LOCATION } from '../../../../../common/constants';
import { LocationMissingWarning } from '../location_map/location_missing';
import { useSelectedView } from './use_selected_view';
import { LocationMap } from '../location_map';
import { MonitoringFrom } from '../translations';
import { ToggleViewBtn } from './toggle_view_btn';
const EuiFlexItemTags = styled(EuiFlexItem)`
width: 350px;
@media (max-width: 1042px) {
width: 100%;
}
`;
interface LocationMapProps {
monitorLocations: MonitorLocations;
}
export const LocationAvailability = ({ monitorLocations }: LocationMapProps) => {
const upPoints: LocationPoint[] = [];
const downPoints: LocationPoint[] = [];
let isAnyGeoInfoMissing = false;
if (monitorLocations?.locations) {
monitorLocations.locations.forEach(({ geo, summary }: MonitorLocation) => {
if (geo?.name === UNNAMED_LOCATION || !geo?.location) {
isAnyGeoInfoMissing = true;
} else if (!!geo.location.lat && !!geo.location.lon) {
if (summary?.down === 0) {
upPoints.push(geo as LocationPoint);
} else {
downPoints.push(geo as LocationPoint);
}
}
});
}
const { selectedView: initialView } = useSelectedView();
const [selectedView, setSelectedView] = useState(initialView);
return (
<EuiErrorBoundary>
<EuiFlexGroup responsive={false} gutterSize={'none'} style={{ flexGrow: 0 }}>
{selectedView === 'list' && (
<EuiFlexItem>
<EuiTitle size="s">
<h3>{MonitoringFrom}</h3>
</EuiTitle>
</EuiFlexItem>
)}
{selectedView === 'map' && (
<EuiFlexItem>{isAnyGeoInfoMissing && <LocationMissingWarning />}</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<ToggleViewBtn
onChange={(val) => {
setSelectedView(val);
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup wrap={true} gutterSize="none" justifyContent="flexEnd">
{selectedView === 'list' && (
<EuiFlexItemTags grow={true}>
<LocationStatusTags locations={monitorLocations?.locations || []} />
</EuiFlexItemTags>
)}
{selectedView === 'map' && (
<EuiFlexItem grow={false}>
<LocationMap upPoints={upPoints} downPoints={downPoints} />
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiErrorBoundary>
);
};

View file

@ -0,0 +1,61 @@
/*
* 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 * as React from 'react';
import styled from 'styled-components';
import { EuiButtonGroup } from '@elastic/eui';
import { useSelectedView } from './use_selected_view';
import { ChangeToListView, ChangeToMapView } from '../translations';
const ToggleViewButtons = styled.span`
margin-left: auto;
`;
interface Props {
onChange: (val: string) => void;
}
export const ToggleViewBtn = ({ onChange }: Props) => {
const toggleButtons = [
{
id: `listBtn`,
label: ChangeToMapView,
name: 'listView',
iconType: 'list',
'data-test-subj': 'uptimeMonitorToggleListBtn',
'aria-label': ChangeToMapView,
},
{
id: `mapBtn`,
label: ChangeToListView,
name: 'mapView',
iconType: 'mapMarker',
'data-test-subj': 'uptimeMonitorToggleMapBtn',
'aria-label': ChangeToListView,
},
];
const { selectedView, setSelectedView } = useSelectedView();
const onChangeView = (optionId: string) => {
const currView = optionId === 'listBtn' ? 'list' : 'map';
setSelectedView(currView);
onChange(currView);
};
return (
<ToggleViewButtons>
<EuiButtonGroup
options={toggleButtons}
idToSelectedMap={{ listBtn: selectedView === 'list', mapBtn: selectedView === 'map' }}
onChange={(id) => onChangeView(id)}
type="multi"
isIconOnly
style={{ marginLeft: 'auto' }}
/>
</ToggleViewButtons>
);
};

View file

@ -0,0 +1,26 @@
/*
* 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 { useEffect, useState } from 'react';
const localKey = 'xpack.uptime.detailPage.selectedView';
interface Props {
selectedView: string;
setSelectedView: (val: string) => void;
}
export const useSelectedView = (): Props => {
const getSelectedView = localStorage.getItem(localKey) ?? 'list';
const [selectedView, setSelectedView] = useState(getSelectedView);
useEffect(() => {
localStorage.setItem(localKey, selectedView);
}, [selectedView]);
return { selectedView, setSelectedView };
};

View file

@ -0,0 +1,27 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LocationMap component renders correctly against snapshot 1`] = `
<styled.div>
<EmbeddedMap
downPoints={Array []}
upPoints={
Array [
Object {
"location": Object {
"lat": "40.730610",
"lon": " -73.935242",
},
"name": "New York",
},
Object {
"location": Object {
"lat": "52.487448",
"lon": " 13.394798",
},
"name": "Tokyo",
},
]
}
/>
</styled.div>
`;

View file

@ -4,6 +4,7 @@ exports[`LocationMissingWarning component renders correctly against snapshot 1`]
.c0 {
margin-left: auto;
margin-bottom: 3px;
margin-right: 5px;
}
<div

View file

@ -0,0 +1,33 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { shallowWithIntl } from 'test_utils/enzyme_helpers';
import { LocationMap } from '../location_map';
import { LocationPoint } from '../embeddables/embedded_map';
// Note For shallow test, we need absolute time strings
describe('LocationMap component', () => {
let upPoints: LocationPoint[];
beforeEach(() => {
upPoints = [
{
name: 'New York',
location: { lat: '40.730610', lon: ' -73.935242' },
},
{
name: 'Tokyo',
location: { lat: '52.487448', lon: ' 13.394798' },
},
];
});
it('renders correctly against snapshot', () => {
const component = shallowWithIntl(<LocationMap upPoints={upPoints} downPoints={[]} />);
expect(component).toMatchSnapshot();
});
});

View file

@ -14,6 +14,7 @@ export const mockDownPointsLayer = {
__featureCollection: {
features: [
{
id: 'Asia',
type: 'feature',
geometry: {
type: 'Point',
@ -21,6 +22,7 @@ export const mockDownPointsLayer = {
},
},
{
id: 'APJ',
type: 'feature',
geometry: {
type: 'Point',
@ -28,6 +30,7 @@ export const mockDownPointsLayer = {
},
},
{
id: 'Canada',
type: 'feature',
geometry: {
type: 'Point',
@ -79,6 +82,7 @@ export const mockUpPointsLayer = {
__featureCollection: {
features: [
{
id: 'US-EAST',
type: 'feature',
geometry: {
type: 'Point',
@ -86,6 +90,7 @@ export const mockUpPointsLayer = {
},
},
{
id: 'US-WEST',
type: 'feature',
geometry: {
type: 'Point',
@ -93,6 +98,7 @@ export const mockUpPointsLayer = {
},
},
{
id: 'Europe',
type: 'feature',
geometry: {
type: 'Point',

View file

@ -7,7 +7,7 @@
import { getLayerList } from '../map_config';
import { mockLayerList } from './__mocks__/mock';
import { LocationPoint } from '../embedded_map';
import { UptimeAppColors } from '../../../../../uptime_app';
import { UptimeAppColors } from '../../../../../../uptime_app';
jest.mock('uuid', () => {
return {
@ -22,14 +22,14 @@ describe('map_config', () => {
beforeEach(() => {
upPoints = [
{ lat: '52.487239', lon: '13.399262' },
{ lat: '55.487239', lon: '13.399262' },
{ lat: '54.487239', lon: '14.399262' },
{ name: 'US-EAST', location: { lat: '52.487239', lon: '13.399262' } },
{ location: { lat: '55.487239', lon: '13.399262' }, name: 'US-WEST' },
{ location: { lat: '54.487239', lon: '14.399262' }, name: 'Europe' },
];
downPoints = [
{ lat: '52.487239', lon: '13.399262' },
{ lat: '55.487239', lon: '13.399262' },
{ lat: '54.487239', lon: '14.399262' },
{ location: { lat: '52.487239', lon: '13.399262' }, name: 'Asia' },
{ location: { lat: '55.487239', lon: '13.399262' }, name: 'APJ' },
{ location: { lat: '54.487239', lon: '14.399262' }, name: 'Canada' },
];
colors = {
danger: '#BC261E',

View file

@ -7,28 +7,35 @@
import React, { useEffect, useState, useContext, useRef } from 'react';
import uuid from 'uuid';
import styled from 'styled-components';
import { createPortalNode, InPortal, OutPortal } from 'react-reverse-portal';
import {
MapEmbeddable,
MapEmbeddableInput,
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
} from '../../../../../../maps/public/embeddable';
} from '../../../../../../../maps/public/embeddable';
import * as i18n from './translations';
import { Location } from '../../../../../common/runtime_types';
import { GeoPoint } from '../../../../../../common/runtime_types';
import { getLayerList } from './map_config';
import { UptimeThemeContext, UptimeStartupPluginsContext } from '../../../../contexts';
import { UptimeThemeContext, UptimeStartupPluginsContext } from '../../../../../contexts';
import { MAP_SAVED_OBJECT_TYPE } from '../../../../../../../maps/public';
import { MapToolTipComponent } from './map_tool_tip';
import {
isErrorEmbeddable,
ViewMode,
ErrorEmbeddable,
} from '../../../../../../../../src/plugins/embeddable/public';
import { MAP_SAVED_OBJECT_TYPE } from '../../../../../../maps/public';
} from '../../../../../../../../../src/plugins/embeddable/public';
import {
RenderTooltipContentParams,
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
} from '../../../../../../../maps/public/classes/tooltips/tooltip_property';
export interface EmbeddedMapProps {
upPoints: LocationPoint[];
downPoints: LocationPoint[];
}
export type LocationPoint = Required<Location>;
export type LocationPoint = Required<GeoPoint>;
const EmbeddedPanel = styled.div`
z-index: auto;
@ -58,6 +65,8 @@ export const EmbeddedMap = React.memo(({ upPoints, downPoints }: EmbeddedMapProp
}
const factory: any = embeddablePlugin.getEmbeddableFactory(MAP_SAVED_OBJECT_TYPE);
const portalNode = React.useMemo(() => createPortalNode(), []);
const input: MapEmbeddableInput = {
id: uuid.v4(),
filters: [],
@ -77,12 +86,38 @@ export const EmbeddedMap = React.memo(({ upPoints, downPoints }: EmbeddedMapProp
zoom: 0,
},
disableInteractive: true,
disableTooltipControl: true,
hideToolbarOverlay: true,
hideLayerControl: true,
hideViewControl: true,
};
const renderTooltipContent = ({
addFilters,
closeTooltip,
features,
isLocked,
getLayerName,
loadFeatureProperties,
loadFeatureGeometry,
}: RenderTooltipContentParams) => {
const props = {
addFilters,
closeTooltip,
isLocked,
getLayerName,
loadFeatureProperties,
loadFeatureGeometry,
};
const relevantFeatures = features.filter(
(item: any) => item.layerId === 'up_points' || item.layerId === 'down_points'
);
if (relevantFeatures.length > 0) {
return <OutPortal {...props} node={portalNode} features={relevantFeatures} />;
}
closeTooltip();
return null;
};
useEffect(() => {
async function setupEmbeddable() {
if (!factory) {
@ -94,11 +129,13 @@ export const EmbeddedMap = React.memo(({ upPoints, downPoints }: EmbeddedMapProp
});
if (embeddableObject && !isErrorEmbeddable(embeddableObject)) {
embeddableObject.setLayerList(getLayerList(upPoints, downPoints, colors));
embeddableObject.setRenderTooltipContent(renderTooltipContent);
await embeddableObject.setLayerList(getLayerList(upPoints, downPoints, colors));
}
setEmbeddable(embeddableObject);
}
setupEmbeddable();
// we want this effect to execute exactly once after the component mounts
@ -126,6 +163,9 @@ export const EmbeddedMap = React.memo(({ upPoints, downPoints }: EmbeddedMapProp
className="embPanel__content"
ref={embeddableRoot}
/>
<InPortal node={portalNode}>
<MapToolTipComponent />
</InPortal>
</EmbeddedPanel>
);
});

View file

@ -6,7 +6,7 @@
import lowPolyLayerFeatures from './low_poly_layer.json';
import { LocationPoint } from './embedded_map';
import { UptimeAppColors } from '../../../../uptime_app';
import { UptimeAppColors } from '../../../../../uptime_app';
/**
* Returns `Source/Destination Point-to-point` Map LayerList configuration, with a source,
@ -70,9 +70,10 @@ export const getLowPolyLayer = () => {
export const getDownPointsLayer = (downPoints: LocationPoint[], dangerColor: string) => {
const features = downPoints?.map((point) => ({
type: 'feature',
id: point.name,
geometry: {
type: 'Point',
coordinates: [+point.lon, +point.lat],
coordinates: [+point.location.lon, +point.location.lat],
},
}));
return {
@ -122,9 +123,10 @@ export const getDownPointsLayer = (downPoints: LocationPoint[], dangerColor: str
export const getUpPointsLayer = (upPoints: LocationPoint[]) => {
const features = upPoints?.map((point) => ({
type: 'feature',
id: point.name,
geometry: {
type: 'Point',
coordinates: [+point.lon, +point.lat],
coordinates: [+point.location.lon, +point.location.lat],
},
}));
return {

View file

@ -0,0 +1,93 @@
/*
* 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 moment from 'moment';
import { i18n } from '@kbn/i18n';
import React, { useContext } from 'react';
import { useSelector } from 'react-redux';
import {
EuiDescriptionList,
EuiDescriptionListDescription,
EuiDescriptionListTitle,
EuiOutsideClickDetector,
EuiPopoverTitle,
} from '@elastic/eui';
import { TagLabel } from '../../availability_reporting';
import { UptimeThemeContext } from '../../../../../contexts';
import { AppState } from '../../../../../state';
import { monitorLocationsSelector } from '../../../../../state/selectors';
import { useMonitorId } from '../../../../../hooks';
import { MonitorLocation } from '../../../../../../common/runtime_types/monitor';
import { formatAvailabilityValue } from '../../availability_reporting/availability_reporting';
import { LastCheckLabel } from '../../translations';
import {
RenderTooltipContentParams,
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
} from '../../../../../../../maps/public/classes/tooltips/tooltip_property';
type MapToolTipProps = Partial<RenderTooltipContentParams>;
export const MapToolTipComponent = ({ closeTooltip, features = [] }: MapToolTipProps) => {
const { id: featureId, layerId } = features[0] ?? {};
const locationName = featureId?.toString();
const {
colors: { gray, danger },
} = useContext(UptimeThemeContext);
const monitorId = useMonitorId();
const monitorLocations = useSelector((state: AppState) =>
monitorLocationsSelector(state, monitorId)
);
if (!locationName || !monitorLocations?.locations) {
return null;
}
const {
timestamp,
up_history: ups,
down_history: downs,
}: MonitorLocation = monitorLocations.locations!.find(
({ geo }: MonitorLocation) => geo.name === locationName
)!;
const availability = (ups / (ups + downs)) * 100;
return (
<EuiOutsideClickDetector
onOutsideClick={() => {
if (closeTooltip != null) {
closeTooltip();
}
}}
>
<>
<EuiPopoverTitle>
{layerId === 'up_points' ? (
<TagLabel label={locationName} color={gray} />
) : (
<TagLabel label={locationName} color={danger} />
)}
</EuiPopoverTitle>
<EuiDescriptionList type="column" textStyle="reverse" compressed={true}>
<EuiDescriptionListTitle>Availability</EuiDescriptionListTitle>
<EuiDescriptionListDescription>
{i18n.translate('xpack.uptime.mapToolTip.AvailabilityStat.title', {
defaultMessage: '{value} %',
values: { value: formatAvailabilityValue(availability) },
description: 'A percentage value like 23.5%',
})}
</EuiDescriptionListDescription>
<EuiDescriptionListTitle>{LastCheckLabel}</EuiDescriptionListTitle>
<EuiDescriptionListDescription>
{moment(timestamp).fromNow()}
</EuiDescriptionListDescription>
</EuiDescriptionList>
</>
</EuiOutsideClickDetector>
);
};
export const MapToolTip = React.memo(MapToolTipComponent);

View file

@ -5,4 +5,4 @@
*/
export * from './location_map';
export * from './location_status_tags';
export * from '../availability_reporting/location_status_tags';

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 styled from 'styled-components';
import { EmbeddedMap, LocationPoint } from './embeddables/embedded_map';
// These height/width values are used to make sure map is in center of panel
// And to make sure, it doesn't take too much space
const MapPanel = styled.div`
height: 240px;
width: 520px;
margin-right: 65px;
@media (max-width: 574px) {
height: 250px;
width: 100%;
}
`;
interface Props {
upPoints: LocationPoint[];
downPoints: LocationPoint[];
}
export const LocationMap = ({ upPoints, downPoints }: Props) => {
return (
<MapPanel>
<EmbeddedMap upPoints={upPoints} downPoints={downPoints} />
</MapPanel>
);
};

View file

@ -16,11 +16,12 @@ import {
} from '@elastic/eui';
import styled from 'styled-components';
import { FormattedMessage } from '@kbn/i18n/react';
import { LocationLink } from '../../common/location_link';
import { LocationLink } from '../../../common/location_link';
const EuiPopoverRight = styled(EuiFlexItem)`
margin-left: auto;
margin-bottom: 3px;
margin-right: 5px;
`;
export const LocationMissingWarning = () => {

View file

@ -5,5 +5,4 @@
*/
export { MonitorSSLCertificate } from './ssl_certificate';
export { MonitorStatusBarComponent } from './status_bar';
export { MonitorStatusBar } from './status_bar_container';
export { MonitorStatusBar } from './status_bar';

View file

@ -0,0 +1,41 @@
/*
* 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 { Link } from 'react-router-dom';
import { EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { Tls } from '../../../../../common/runtime_types';
import { CERTIFICATES_ROUTE } from '../../../../../common/constants';
import { MonListDescription, MonListTitle } from './status_bar';
import { CertStatusColumn } from '../../../overview/monitor_list/cert_status_column';
interface Props {
/**
* TLS information coming from monitor in ES heartbeat index
*/
tls: Tls | undefined;
}
export const MonitorSSLCertificate = ({ tls }: Props) => {
return tls?.not_after ? (
<>
<MonListTitle>
<FormattedMessage
id="xpack.uptime.monitorStatusBar.sslCertificate.title"
defaultMessage="TLS Certificate"
/>
</MonListTitle>
<EuiSpacer size="s" />
<MonListDescription>
<Link to={CERTIFICATES_ROUTE} className="eui-displayInline">
<CertStatusColumn cert={tls} boldStyle={true} />
</Link>
</MonListDescription>
</>
) : null;
};

View file

@ -0,0 +1,82 @@
/*
* 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 {
EuiLink,
EuiIcon,
EuiSpacer,
EuiDescriptionList,
EuiDescriptionListTitle,
EuiDescriptionListDescription,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { MonitorSSLCertificate } from './ssl_certificate';
import * as labels from '../translations';
import { StatusByLocations } from './status_by_location';
import { useStatusBar } from './use_status_bar';
import { MonitorIDLabel, OverallAvailability } from '../translations';
import { URL_LABEL } from '../../../common/translations';
import { MonitorLocations } from '../../../../../common/runtime_types/monitor';
import { formatAvailabilityValue } from '../availability_reporting/availability_reporting';
export const MonListTitle = styled(EuiDescriptionListTitle)`
&&& {
width: 35%;
}
`;
export const MonListDescription = styled(EuiDescriptionListDescription)`
&&& {
width: 65%;
overflow-wrap: anywhere;
}
`;
export const MonitorStatusBar: React.FC = () => {
const { monitorId, monitorStatus, monitorLocations = {} } = useStatusBar();
const { locations, up_history: ups, down_history: downs } = monitorLocations as MonitorLocations;
const full = monitorStatus?.url?.full ?? '';
const availability = (ups === 0 && downs === 0) || !ups ? 0 : (ups / (ups + downs)) * 100;
return (
<>
<div>
<StatusByLocations locations={locations ?? []} />
</div>
<EuiSpacer />
<EuiDescriptionList
type="column"
compressed={true}
textStyle="reverse"
style={{ maxWidth: '450px' }}
>
<MonListTitle>{OverallAvailability}</MonListTitle>
<MonListDescription data-test-subj="uptimeOverallAvailability">
<FormattedMessage
id="xpack.uptime.availabilityLabelText"
defaultMessage="{value} %"
values={{ value: formatAvailabilityValue(availability) }}
description="A percentage value, like 23.5 %"
/>
</MonListDescription>
<MonListTitle>{URL_LABEL}</MonListTitle>
<MonListDescription>
<EuiLink aria-label={labels.monitorUrlLinkAriaLabel} href={full} target="_blank">
{full} <EuiIcon type={'popout'} size="s" />
</EuiLink>
</MonListDescription>
<MonListTitle>{MonitorIDLabel}</MonListTitle>
<MonListDescription data-test-subj="monitor-page-title">{monitorId}</MonListDescription>
<MonitorSSLCertificate tls={monitorStatus?.tls} />
</EuiDescriptionList>
</>
);
};

View file

@ -4,24 +4,33 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useContext, useEffect } from 'react';
import { useContext, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { monitorLocationsSelector, monitorStatusSelector } from '../../../../state/selectors';
import { MonitorStatusBarComponent } from './index';
import { getMonitorStatusAction } from '../../../../state/actions';
import { useGetUrlParams } from '../../../../hooks';
import { UptimeRefreshContext } from '../../../../contexts';
import { MonitorIdParam } from '../../../../../common/types';
import { useGetUrlParams, useMonitorId } from '../../../../hooks';
import { monitorLocationsSelector, monitorStatusSelector } from '../../../../state/selectors';
import { AppState } from '../../../../state';
import { getMonitorStatusAction } from '../../../../state/actions';
import { Ping } from '../../../../../common/runtime_types/ping';
import { MonitorLocations } from '../../../../../common/runtime_types/monitor';
export const MonitorStatusBar: React.FC<MonitorIdParam> = ({ monitorId }) => {
interface MonitorStatusBarProps {
monitorId: string;
monitorStatus: Ping | null;
monitorLocations?: MonitorLocations;
}
export const useStatusBar = (): MonitorStatusBarProps => {
const { lastRefresh } = useContext(UptimeRefreshContext);
const { dateRangeStart: dateStart, dateRangeEnd: dateEnd } = useGetUrlParams();
const dispatch = useDispatch();
const monitorId = useMonitorId();
const monitorStatus = useSelector(monitorStatusSelector);
const monitorLocations = useSelector((state: AppState) =>
monitorLocationsSelector(state, monitorId)
);
@ -30,11 +39,5 @@ export const MonitorStatusBar: React.FC<MonitorIdParam> = ({ monitorId }) => {
dispatch(getMonitorStatusAction({ dateStart, dateEnd, monitorId }));
}, [monitorId, dateStart, dateEnd, lastRefresh, dispatch]);
return (
<MonitorStatusBarComponent
monitorId={monitorId}
monitorStatus={monitorStatus}
monitorLocations={monitorLocations!}
/>
);
return { monitorStatus, monitorLocations, monitorId };
};

View file

@ -7,31 +7,24 @@
import React, { useContext, useEffect, useState } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
import styled from 'styled-components';
import { LocationMap } from '../location_map';
import { LocationAvailability } from './location_availability/location_availability';
import { UptimeRefreshContext } from '../../../contexts';
import { MonitorLocations } from '../../../../common/runtime_types';
import { MonitorStatusBar } from './monitor_status_bar';
import { MonitorStatusBar } from './status_bar';
interface MonitorStatusDetailsProps {
monitorId: string;
monitorLocations: MonitorLocations;
}
const WrapFlexItem = styled(EuiFlexItem)`
&&& {
@media (max-width: 768px) {
width: 100%;
}
@media (max-width: 1042px) {
flex-basis: 520px;
@media (max-width: 800px) {
flex-basis: 100%;
}
}
`;
export const MonitorStatusDetailsComponent = ({
monitorId,
monitorLocations,
}: MonitorStatusDetailsProps) => {
export const MonitorStatusDetailsComponent = ({ monitorLocations }: MonitorStatusDetailsProps) => {
const { refreshApp } = useContext(UptimeRefreshContext);
const [isTabActive] = useState(document.visibilityState);
@ -56,12 +49,12 @@ export const MonitorStatusDetailsComponent = ({
return (
<EuiPanel>
<EuiFlexGroup gutterSize="l" wrap responsive={true}>
<EuiFlexItem grow={true}>
<MonitorStatusBar monitorId={monitorId} />
<EuiFlexGroup gutterSize="l" wrap={true} responsive={true}>
<EuiFlexItem grow={1}>
<MonitorStatusBar />
</EuiFlexItem>
<WrapFlexItem grow={false}>
<LocationMap monitorLocations={monitorLocations} />
<WrapFlexItem grow={1}>
<LocationAvailability monitorLocations={monitorLocations} />
</WrapFlexItem>
</EuiFlexGroup>
</EuiPanel>

View file

@ -28,7 +28,5 @@ export const MonitorStatusDetails: React.FC<MonitorIdParam> = ({ monitorId }) =>
dispatch(getMonitorLocationsAction({ dateStart, dateEnd, monitorId }));
}, [monitorId, dateStart, dateEnd, lastRefresh, dispatch]);
return (
<MonitorStatusDetailsComponent monitorId={monitorId} monitorLocations={monitorLocations!} />
);
return <MonitorStatusDetailsComponent monitorLocations={monitorLocations!} />;
};

View file

@ -48,3 +48,56 @@ export const timestampFromNowTextAriaLabel = i18n.translate(
export const loadingMessage = i18n.translate('xpack.uptime.monitorStatusBar.loadingMessage', {
defaultMessage: 'Loading…',
});
export const MonitorIDLabel = i18n.translate('xpack.uptime.monitorStatusBar.monitor.id', {
defaultMessage: 'Monitor ID',
});
export const OverallAvailability = i18n.translate(
'xpack.uptime.monitorStatusBar.monitor.availability',
{
defaultMessage: 'Overall availability',
}
);
export const MonitoringFrom = i18n.translate(
'xpack.uptime.monitorStatusBar.monitor.monitoringFrom',
{
defaultMessage: 'Monitoring from',
}
);
export const ChangeToMapView = i18n.translate(
'xpack.uptime.monitorStatusBar.monitor.monitoringFrom.listToMap',
{
defaultMessage: 'Change to map view to check availability by location.',
}
);
export const ChangeToListView = i18n.translate(
'xpack.uptime.monitorStatusBar.monitor.monitoringFrom.MapToList',
{
defaultMessage: 'Change to list view to check availability by location.',
}
);
export const LocationLabel = i18n.translate(
'xpack.uptime.monitorStatusBar.monitor.availabilityReport.location',
{
defaultMessage: 'Location',
}
);
export const AvailabilityLabel = i18n.translate(
'xpack.uptime.monitorStatusBar.monitor.availabilityReport.availability',
{
defaultMessage: 'Availability',
}
);
export const LastCheckLabel = i18n.translate(
'xpack.uptime.monitorStatusBar.monitor.availabilityReport.lastCheck',
{
defaultMessage: 'Last check',
}
);

View file

@ -8,13 +8,14 @@ import React from 'react';
import moment from 'moment';
import styled from 'styled-components';
import { EuiIcon, EuiText, EuiToolTip } from '@elastic/eui';
import { Cert } from '../../../../common/runtime_types';
import { Cert, Tls } from '../../../../common/runtime_types';
import { useCertStatus } from '../../../hooks';
import { EXPIRED, EXPIRES_SOON } from '../../certificates/translations';
import { EXPIRED, EXPIRES, EXPIRES_SOON } from '../../certificates/translations';
import { CERT_STATUS } from '../../../../common/constants';
interface Props {
cert: Cert;
cert: Cert | Tls;
boldStyle?: boolean;
}
const Span = styled.span`
@ -22,7 +23,15 @@ const Span = styled.span`
vertical-align: middle;
`;
export const CertStatusColumn: React.FC<Props> = ({ cert }) => {
const H4Text = styled.h4`
&&& {
margin: 0 0 0 4px;
display: inline-block;
color: inherit;
}
`;
export const CertStatusColumn: React.FC<Props> = ({ cert, boldStyle = false }) => {
const certStatus = useCertStatus(cert?.not_after);
const relativeDate = moment(cert?.not_after).fromNow();
@ -32,9 +41,15 @@ export const CertStatusColumn: React.FC<Props> = ({ cert }) => {
<EuiToolTip content={moment(cert?.not_after).format('L LT')}>
<EuiText size="s">
<EuiIcon color={color} type="lock" size="s" />
{boldStyle ? (
<H4Text>
{text} {relativeDate}
</H4Text>
) : (
<Span>
{text} {relativeDate}
</Span>
)}
</EuiText>
</EuiToolTip>
);
@ -47,5 +62,5 @@ export const CertStatusColumn: React.FC<Props> = ({ cert }) => {
return <CertStatus color="danger" text={EXPIRED} />;
}
return certStatus ? <CertStatus color="success" text={'Expires'} /> : <span>--</span>;
return certStatus ? <CertStatus color="success" text={EXPIRES} /> : <span>--</span>;
};

View file

@ -30,6 +30,7 @@ import { MonitorListProps } from './monitor_list_container';
import { MonitorList } from '../../../state/reducers/monitor_list';
import { CertStatusColumn } from './cert_status_column';
import { MonitorListHeader } from './monitor_list_header';
import { URL_LABEL } from '../../common/translations';
interface Props extends MonitorListProps {
pageSize: number;
@ -106,7 +107,7 @@ export const MonitorListComponent: React.FC<Props> = ({
{
align: 'left' as const,
field: 'state.url.full',
name: labels.URL,
name: URL_LABEL,
render: (url: string, summary: MonitorSummary) => (
<TruncatedEuiLink href={url} target="_blank" color="text">
{url} <EuiIcon size="s" type="popout" color="subbdued" />

View file

@ -62,10 +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 URL = i18n.translate('xpack.uptime.monitorList.table.url.name', {
defaultMessage: 'Url',
});
export const UP = i18n.translate('xpack.uptime.monitorList.statusColumn.upLabel', {
defaultMessage: 'Up',
});

View file

@ -2,21 +2,25 @@
exports[`PageHeader shallow renders extra links: page_header_with_extra_links 1`] = `
Array [
.c0 {
white-space: nowrap;
}
@media only screen and (max-width:1024px) and (min-width:868px) {
.c0.c0.c0 .euiSuperDatePicker__flexWrapper {
.c1.c1.c1 .euiSuperDatePicker__flexWrapper {
width: 500px;
}
}
@media only screen and (max-width:880px) {
.c0.c0.c0 {
.c1.c1.c1 {
-webkit-box-flex: 1;
-webkit-flex-grow: 1;
-ms-flex-positive: 1;
flex-grow: 1;
}
.c0.c0.c0 .euiSuperDatePicker__flexWrapper {
.c1.c1.c1 .euiSuperDatePicker__flexWrapper {
width: calc(100% + 8px);
}
}
@ -28,7 +32,7 @@ Array [
class="euiFlexItem"
>
<h1
class="euiTitle euiTitle--medium"
class="c0 euiTitle euiTitle--medium"
>
TestingHeading
</h1>
@ -126,7 +130,7 @@ Array [
</div>
</div>
<div
class="euiFlexItem euiFlexItem--flexGrowZero c0"
class="euiFlexItem euiFlexItem--flexGrowZero c1"
style="flex-basis:485px"
>
<div
@ -238,21 +242,25 @@ Array [
exports[`PageHeader shallow renders with the date picker: page_header_with_date_picker 1`] = `
Array [
.c0 {
white-space: nowrap;
}
@media only screen and (max-width:1024px) and (min-width:868px) {
.c0.c0.c0 .euiSuperDatePicker__flexWrapper {
.c1.c1.c1 .euiSuperDatePicker__flexWrapper {
width: 500px;
}
}
@media only screen and (max-width:880px) {
.c0.c0.c0 {
.c1.c1.c1 {
-webkit-box-flex: 1;
-webkit-flex-grow: 1;
-ms-flex-positive: 1;
flex-grow: 1;
}
.c0.c0.c0 .euiSuperDatePicker__flexWrapper {
.c1.c1.c1 .euiSuperDatePicker__flexWrapper {
width: calc(100% + 8px);
}
}
@ -264,7 +272,7 @@ Array [
class="euiFlexItem"
>
<h1
class="euiTitle euiTitle--medium"
class="c0 euiTitle euiTitle--medium"
>
TestingHeading
</h1>
@ -273,7 +281,7 @@ Array [
class="euiFlexItem euiFlexItem--flexGrowZero"
/>
<div
class="euiFlexItem euiFlexItem--flexGrowZero c0"
class="euiFlexItem euiFlexItem--flexGrowZero c1"
style="flex-basis:485px"
>
<div
@ -385,6 +393,10 @@ Array [
exports[`PageHeader shallow renders without the date picker: page_header_no_date_picker 1`] = `
Array [
.c0 {
white-space: nowrap;
}
<div
class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--wrap"
>
@ -392,7 +404,7 @@ Array [
class="euiFlexItem"
>
<h1
class="euiTitle euiTitle--medium"
class="c0 euiTitle euiTitle--medium"
>
TestingHeading
</h1>

View file

@ -19,6 +19,7 @@ interface PageHeaderProps {
extraLinks?: boolean;
datePicker?: boolean;
}
const SETTINGS_LINK_TEXT = i18n.translate('xpack.uptime.page_header.settingsLink', {
defaultMessage: 'Settings',
});
@ -43,6 +44,10 @@ const StyledPicker = styled(EuiFlexItem)`
}
`;
const H1Text = styled.h1`
white-space: nowrap;
`;
export const PageHeader = React.memo(
({ headingText, extraLinks = false, datePicker = true }: PageHeaderProps) => {
const DatePickerComponent = () =>
@ -89,7 +94,7 @@ export const PageHeader = React.memo(
>
<EuiFlexItem grow={true}>
<EuiTitle>
<h1>{headingText}</h1>
<H1Text>{headingText}</H1Text>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>{extraLinkComponents}</EuiFlexItem>

View file

@ -32,7 +32,7 @@ export const getMonitorLocations: UMElasticsearchQueryFn<
bool: {
filter: [
{
match: {
term: {
'monitor.id': monitorId,
},
},
@ -70,6 +70,18 @@ export const getMonitorLocations: UMElasticsearchQueryFn<
_source: ['monitor', 'summary', 'observer', '@timestamp'],
},
},
down_history: {
sum: {
field: 'summary.down',
missing: 0,
},
},
up_history: {
sum: {
field: 'summary.up',
missing: 0,
},
},
},
},
},
@ -99,10 +111,17 @@ export const getMonitorLocations: UMElasticsearchQueryFn<
}
};
let totalUps = 0;
let totalDowns = 0;
const monLocs: MonitorLocation[] = [];
locations.forEach((loc: any) => {
const mostRecentLocation = loc.most_recent.hits.hits[0]._source;
locations.forEach(({ most_recent: mostRecent, up_history, down_history }: any) => {
const mostRecentLocation = mostRecent.hits.hits[0]._source;
totalUps += up_history.value;
totalDowns += down_history.value;
const location: MonitorLocation = {
up_history: up_history.value ?? 0,
down_history: down_history.value ?? 0,
summary: mostRecentLocation?.summary,
geo: getGeo(mostRecentLocation?.observer?.geo),
timestamp: mostRecentLocation['@timestamp'],
@ -113,5 +132,7 @@ export const getMonitorLocations: UMElasticsearchQueryFn<
return {
monitorId,
locations: monLocs,
up_history: totalUps,
down_history: totalDowns,
};
};

View file

@ -65,7 +65,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('detail page', async () => {
await uptimeService.navigation.goToMonitor(A11Y_TEST_MONITOR_ID);
await uptimeService.monitor.locationMapIsRendered();
await uptimeService.monitor.displayOverallAvailability('0.00 %');
await a11y.testAppSnapshot();
});

View file

@ -11,16 +11,14 @@ import { FtrProviderContext } from '../../ftr_provider_context';
export default ({ getPageObjects, getService }: FtrProviderContext) => {
const { uptime: uptimePage } = getPageObjects(['uptime']);
const uptime = getService('uptime');
const es = getService('legacyEs');
const monitor = () => uptime.monitor;
describe('Observer location', () => {
const start = moment().subtract('15', 'm').toISOString();
const end = moment().toISOString();
const MONITOR_ID = 'location-testing-id';
beforeEach(async () => {
const LessAvailMonitor = 'less-availability-monitor';
const addMonitorWithNoLocation = async () => {
/**
* This mogrify function will strip the documents of their location
* data (but preserve their location name), which is necessary for
@ -33,21 +31,40 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
}
return d;
};
await makeChecksWithStatus(
getService('legacyEs'),
MONITOR_ID,
5,
2,
10000,
{},
'up',
mogrifyNoLocation
);
await uptime.navigation.goToUptime();
await makeChecksWithStatus(es, MONITOR_ID, 5, 2, 10000, {}, 'up', mogrifyNoLocation);
};
const addLessAvailMonitor = async () => {
await makeChecksWithStatus(es, LessAvailMonitor, 5, 2, 10000, {}, 'up');
await makeChecksWithStatus(es, LessAvailMonitor, 5, 2, 10000, {}, 'down');
};
describe('Observer location', () => {
const start = moment().subtract('15', 'm').toISOString();
const end = moment().toISOString();
before(async () => {
await addMonitorWithNoLocation();
await addLessAvailMonitor();
await uptime.navigation.goToUptime();
await uptimePage.goToRoot(true);
});
beforeEach(async () => {
await addMonitorWithNoLocation();
await addLessAvailMonitor();
if (!(await uptime.navigation.isOnDetailsPage()))
await uptimePage.loadDataAndGoToMonitorPage(start, end, MONITOR_ID);
});
it('displays the overall availability', async () => {
await monitor().displayOverallAvailability('100.00 %');
});
it('can change the view to map', async () => {
await monitor().toggleToMapView();
});
it('renders the location panel and canvas', async () => {
await monitor().locationMapIsRendered();
});
@ -55,5 +72,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
it('renders the location missing popover when monitor has location name, but no geo data', async () => {
await monitor().locationMissingExists();
});
it('displays less monitor availability', async () => {
await uptime.navigation.goToHomeViaBreadCrumb();
await uptimePage.loadDataAndGoToMonitorPage(start, end, LessAvailMonitor);
await monitor().displayOverallAvailability('50.00 %');
});
});
};

View file

@ -58,13 +58,13 @@ export function UptimeCommonProvider({ getService }: FtrProviderContext) {
'[data-test-subj="xpack.uptime.filterBar.filterStatusUp"]'
);
if (await upFilter.elementHasClass('euiFilterButton-hasActiveFilters')) {
this.setStatusFilterUp();
await this.setStatusFilterUp();
}
const downFilter = await find.byCssSelector(
'[data-test-subj="xpack.uptime.filterBar.filterStatusDown"]'
);
if (await downFilter.elementHasClass('euiFilterButton-hasActiveFilters')) {
this.setStatusFilterDown();
await this.setStatusFilterDown();
}
},
async selectFilterItem(filterType: string, option: string) {

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import expect from '@kbn/expect/expect.js';
import { FtrProviderContext } from '../../ftr_provider_context';
export function UptimeMonitorProvider({ getService }: FtrProviderContext) {
@ -17,6 +18,13 @@ export function UptimeMonitorProvider({ getService }: FtrProviderContext) {
timeout: 3000,
});
},
async displayOverallAvailability(availabilityVal: string) {
return retry.tryForTime(60 * 1000, async () => {
await testSubjects.existOrFail('uptimeOverallAvailability');
const availability = await testSubjects.getVisibleText('uptimeOverallAvailability');
expect(availability).to.be(availabilityVal);
});
},
async locationMapIsRendered() {
return retry.tryForTime(15000, async () => {
await testSubjects.existOrFail('xpack.uptime.locationMap.embeddedPanel', {
@ -45,5 +53,8 @@ export function UptimeMonitorProvider({ getService }: FtrProviderContext) {
);
});
},
async toggleToMapView() {
await testSubjects.click('uptimeMonitorToggleMapBtn');
},
};
}

View file

@ -56,6 +56,7 @@ export function UptimeNavigationProvider({ getService, getPageObjects }: FtrProv
},
goToMonitor: async (monitorId: string) => {
// only go to monitor page if not already there
if (!(await testSubjects.exists('uptimeMonitorPage', { timeout: 0 }))) {
await testSubjects.click(`monitor-page-link-${monitorId}`);
await testSubjects.existOrFail('uptimeMonitorPage', {
@ -80,5 +81,13 @@ export function UptimeNavigationProvider({ getService, getPageObjects }: FtrProv
await PageObjects.timePicker.setAbsoluteRange(dateStart, dateEnd);
await this.goToMonitor(monitorId);
},
async isOnDetailsPage() {
return await testSubjects.exists('uptimeMonitorPage', { timeout: 0 });
},
async goToHomeViaBreadCrumb() {
await testSubjects.click('breadcrumb first');
},
};
}