[Uptime] Details page map handle geo information missing (#54483)

* update API

* update query

* hide layer control and added loc tags

* update test

* remove unused comment

* update API

* remove capitalization

* style fix

* update types

* added location status number on details page

* useref instead of createRef

* update interface

* update import

* removed redundant file

* fix header for empty data

* refactor for most recent check

* remove redundant code

* remone unused translation

* update status bar

* update styling

* update snaps

* added API tests

* fix types

* fixing integration tests and a typo

* remove unused translations

* update tests

* fixed PR feedback

* update feedback

* update messaging

* update snap

* added timestamp in front of tags

* update missing

* update locs

* update geo info missing

* use formatted message

* update snaps

* updated types

* update test

* fix test

* update tests

* update more skipped tests

* update test

* update warning message

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Shahzad 2020-01-14 21:27:40 +01:00 committed by GitHub
parent 2ac0c91c7a
commit f547b76312
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 707 additions and 38 deletions

View file

@ -0,0 +1,7 @@
/*
* 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 const UNNAMED_LOCATION = 'Unnamed-location';

View file

@ -11,3 +11,4 @@ export { INDEX_NAMES } from './index_names';
export * from './capabilities';
export { PLUGIN } from './plugin';
export { QUERY, STATES } from './query';
export * from './constants';

View file

@ -0,0 +1,203 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LocationMap component doesnt shows warning if geo is provided 1`] = `
<EuiErrorBoundary>
<EuiFlexGroup>
<EuiFlexItem
grow={false}
>
<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",
},
]
}
/>
</EuiFlexItem>
<EuiFlexItem
grow={true}
>
<styled.div>
<EmbeddedMap
downPoints={Array []}
upPoints={
Array [
Object {
"lat": "40.730610",
"lon": " -73.935242",
},
Object {
"lat": "52.487448",
"lon": " 13.394798",
},
]
}
/>
</styled.div>
</EuiFlexItem>
</EuiFlexGroup>
</EuiErrorBoundary>
`;
exports[`LocationMap component renders correctly against snapshot 1`] = `
<EuiErrorBoundary>
<EuiFlexGroup>
<EuiFlexItem
grow={false}
>
<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",
},
]
}
/>
</EuiFlexItem>
<EuiFlexItem
grow={true}
>
<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>
</EuiFlexGroup>
</EuiErrorBoundary>
`;
exports[`LocationMap component shows warning if geo information is missing 1`] = `
<EuiErrorBoundary>
<EuiFlexGroup>
<EuiFlexItem
grow={false}
>
<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",
},
]
}
/>
</EuiFlexItem>
<EuiFlexItem
grow={true}
>
<LocationMissingWarning />
<styled.div>
<EmbeddedMap
downPoints={Array []}
upPoints={
Array [
Object {
"lat": "52.487448",
"lon": " 13.394798",
},
]
}
/>
</styled.div>
</EuiFlexItem>
</EuiFlexGroup>
</EuiErrorBoundary>
`;

View file

@ -0,0 +1,123 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LocationMissingWarning component renders correctly against snapshot 1`] = `
<div
class="euiFlexGroup euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
style="margin-left:auto;margin-right:20px"
>
<div
class="euiPopover euiPopover--anchorDownCenter"
id="popover"
>
<div
class="euiPopover__anchor"
>
<button
class="euiButton euiButton--warning euiButton--small"
type="button"
>
<span
class="euiButton__content"
>
<svg
aria-hidden="true"
class="euiIcon euiIcon--medium euiIcon-isLoading euiButton__icon"
focusable="false"
height="16"
role="img"
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
/>
<span
class="euiButton__text"
>
Geo Information Missing
</span>
</span>
</button>
</div>
</div>
</div>
</div>
`;
exports[`LocationMissingWarning component shallow render correctly against snapshot 1`] = `
<EuiFlexGroup
gutterSize="none"
>
<EuiFlexItem
grow={false}
style={
Object {
"marginLeft": "auto",
"marginRight": 20,
}
}
>
<EuiPopover
anchorPosition="downCenter"
button={
<EuiButton
color="warning"
iconType="alert"
onClick={[Function]}
size="s"
>
Geo Information Missing
</EuiButton>
}
closePopover={[Function]}
display="inlineBlock"
hasArrow={true}
id="popover"
isOpen={false}
ownFocus={false}
panelPaddingSize="m"
>
<EuiText
style={
Object {
"width": "350px",
}
}
>
<FormattedMessage
defaultMessage="Important geo location configuration is missing. You can use the {codeBlock} field to create distinctive geographic regions for your uptime checks."
id="xpack.uptime.locationMap.locations.missing.message"
values={
Object {
"codeBlock": <EuiCode>
observer.geo.??
</EuiCode>,
}
}
/>
</EuiText>
<EuiSpacer
size="xs"
/>
<EuiText
style={
Object {
"width": "350px",
}
}
>
<FormattedMessage
defaultMessage="Get more information in our documentation."
id="xpack.uptime.locationMap.locations.missing.message1"
values={Object {}}
/>
<EuiSpacer
size="xs"
/>
<LocationLink />
</EuiText>
</EuiPopover>
</EuiFlexItem>
</EuiFlexGroup>
`;

View file

@ -1,5 +1,82 @@
// 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;

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 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';
// Note For shallow test, we need absolute time strings
describe('LocationMap component', () => {
let monitorLocations: MonitorLocations;
beforeEach(() => {
monitorLocations = {
monitorId: 'wapo',
locations: [
{
summary: { up: 4, down: 0 },
geo: { name: 'New York', location: { lat: '40.730610', lon: ' -73.935242' } },
timestamp: '2020-01-13T22:50:06.536Z',
},
{
summary: { up: 4, down: 0 },
geo: { name: 'Tokyo', location: { lat: '52.487448', lon: ' 13.394798' } },
timestamp: '2020-01-13T22:50:04.354Z',
},
{
summary: { up: 4, down: 0 },
geo: { name: 'Unnamed-location' },
timestamp: '2020-01-13T22:50:02.753Z',
},
],
};
});
it('renders correctly against snapshot', () => {
const component = shallowWithIntl(<LocationMap monitorLocations={monitorLocations} />);
expect(component).toMatchSnapshot();
});
it('shows warning if geo information is missing', () => {
monitorLocations = {
monitorId: 'wapo',
locations: [
{
summary: { up: 4, down: 0 },
geo: { name: 'Tokyo', location: { lat: '52.487448', lon: ' 13.394798' } },
timestamp: '2020-01-13T22:50:04.354Z',
},
{
summary: { up: 4, down: 0 },
geo: { name: 'Unnamed-location' },
timestamp: '2020-01-13T22:50:02.753Z',
},
],
};
const component = shallowWithIntl(<LocationMap monitorLocations={monitorLocations} />);
expect(component).toMatchSnapshot();
const warningComponent = component.find(LocationMissingWarning);
expect(warningComponent).toHaveLength(1);
});
it('doesnt shows warning if geo is provided', () => {
monitorLocations = {
monitorId: 'wapo',
locations: [
{
summary: { up: 4, down: 0 },
geo: { name: 'New York', location: { lat: '40.730610', lon: ' -73.935242' } },
timestamp: '2020-01-13T22:50:06.536Z',
},
{
summary: { up: 4, down: 0 },
geo: { name: 'Tokyo', location: { lat: '52.487448', lon: ' 13.394798' } },
timestamp: '2020-01-13T22:50:04.354Z',
},
],
};
const component = shallowWithIntl(<LocationMap monitorLocations={monitorLocations} />);
expect(component).toMatchSnapshot();
const warningComponent = component.find(LocationMissingWarning);
expect(warningComponent).toHaveLength(0);
});
});

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 { LocationMissingWarning } from '../location_missing';
describe('LocationMissingWarning component', () => {
it('shallow render correctly against snapshot', () => {
const component = shallowWithIntl(<LocationMissingWarning />);
expect(component).toMatchSnapshot();
});
it('renders correctly against snapshot', () => {
const component = renderWithIntl(<LocationMissingWarning />);
expect(component).toMatchSnapshot();
});
});

View file

@ -6,13 +6,41 @@
import React from 'react';
import moment from 'moment';
import { renderWithIntl } from 'test_utils/enzyme_helpers';
import { renderWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers';
import { MonitorLocation } from '../../../../../common/runtime_types/monitor';
import { LocationStatusTags } from '../';
describe('LocationStatusTags component', () => {
let monitorLocations: MonitorLocation[];
it('renders properly against props', () => {
monitorLocations = [
{
summary: { up: 4, down: 0 },
geo: { name: 'Islamabad', location: { lat: '52.487448', lon: ' 13.394798' } },
timestamp: moment()
.subtract('5', 'w')
.toISOString(),
},
{
summary: { up: 4, down: 0 },
geo: { name: 'Berlin', location: { lat: '52.487448', lon: ' 13.394798' } },
timestamp: moment()
.subtract('5', 'w')
.toISOString(),
},
{
summary: { up: 0, down: 2 },
geo: { name: 'Berlin', location: { lat: '52.487448', lon: ' 13.394798' } },
timestamp: moment()
.subtract('5', 'w')
.toISOString(),
},
];
const component = shallowWithIntl(<LocationStatusTags locations={monitorLocations} />);
expect(component).toMatchSnapshot();
});
it('renders when there are many location', () => {
monitorLocations = [
{

View file

@ -6,10 +6,12 @@
import React from 'react';
import styled from 'styled-components';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiErrorBoundary } from '@elastic/eui';
import { LocationStatusTags } from './location_status_tags';
import { EmbeddedMap, LocationPoint } from './embeddables/embedded_map';
import { MonitorLocations } 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
@ -27,25 +29,34 @@ export const LocationMap = ({ monitorLocations }: LocationMapProps) => {
const upPoints: LocationPoint[] = [];
const downPoints: LocationPoint[] = [];
let isGeoInfoMissing = false;
if (monitorLocations?.locations) {
monitorLocations.locations.forEach((item: any) => {
if (item.summary.down === 0) {
upPoints.push(item.geo.location);
} else {
downPoints.push(item.geo.location);
if (item.geo?.name !== UNNAMED_LOCATION) {
if (item.summary.down === 0) {
upPoints.push(item.geo.location);
} else {
downPoints.push(item.geo.location);
}
} else if (item.geo?.name === UNNAMED_LOCATION) {
isGeoInfoMissing = true;
}
});
}
return (
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<LocationStatusTags locations={monitorLocations?.locations || []} />
</EuiFlexItem>
<EuiFlexItem grow={true}>
<MapPanel>
<EmbeddedMap upPoints={upPoints} downPoints={downPoints} />
</MapPanel>
</EuiFlexItem>
</EuiFlexGroup>
<EuiErrorBoundary>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<LocationStatusTags locations={monitorLocations?.locations || []} />
</EuiFlexItem>
<EuiFlexItem grow={true}>
{isGeoInfoMissing && <LocationMissingWarning />}
<MapPanel>
<EmbeddedMap upPoints={upPoints} downPoints={downPoints} />
</MapPanel>
</EuiFlexItem>
</EuiFlexGroup>
</EuiErrorBoundary>
);
};

View file

@ -0,0 +1,64 @@
/*
* 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 {
EuiButton,
EuiFlexGroup,
EuiFlexItem,
EuiPopover,
EuiSpacer,
EuiText,
EuiCode,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { LocationLink } from '../monitor_list/monitor_list_drawer';
export const LocationMissingWarning = () => {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const togglePopover = () => {
setIsPopoverOpen(!isPopoverOpen);
};
const button = (
<EuiButton iconType="alert" size="s" color="warning" onClick={togglePopover}>
Geo Information Missing
</EuiButton>
);
return (
<EuiFlexGroup gutterSize="none">
<EuiFlexItem grow={false} style={{ marginLeft: 'auto', marginRight: 20 }}>
<EuiPopover
id="popover"
button={button}
isOpen={isPopoverOpen}
closePopover={togglePopover}
>
<EuiText style={{ width: '350px' }}>
<FormattedMessage
id="xpack.uptime.locationMap.locations.missing.message"
defaultMessage="Important geo location configuration is missing.
You can use the {codeBlock} field to create distinctive geographic regions for
your uptime checks."
values={{ codeBlock: <EuiCode>observer.geo.??</EuiCode> }}
/>
</EuiText>
<EuiSpacer size="xs" />
<EuiText style={{ width: '350px' }}>
<FormattedMessage
id="xpack.uptime.locationMap.locations.missing.message1"
defaultMessage="Get more information in our documentation."
/>
<EuiSpacer size="xs" />
<LocationLink />
</EuiText>
</EuiPopover>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -25,6 +25,7 @@ const BadgeItem = styled.div`
margin-bottom: 5px;
`;
// Set height so that it remains within panel, enough height to display 7 locations tags
const TagContainer = styled.div`
padding: 10px;
max-height: 229px;
@ -65,7 +66,7 @@ export const LocationStatusTags = ({ locations }: Props) => {
return a.timestamp < b.timestamp ? 1 : b.timestamp < a.timestamp ? -1 : 0;
});
moment.locale('en', {
moment.updateLocale('en', {
relativeTime: {
future: 'in %s',
past: '%s ago',

View file

@ -14,6 +14,25 @@ exports[`MonitorStatusList component renders checks 1`] = `
locationNames={Set {}}
status="up"
/>
<EuiSpacer
size="s"
/>
<EuiCallOut
color="warning"
>
<FormattedMessage
defaultMessage="Some heartbeat instances do not have a location defined. {link} to your heartbeat configuration."
id="xpack.uptime.monitorList.drawer.missingLocation"
values={
Object {
"link": <LocationLink />,
}
}
/>
</EuiCallOut>
<EuiSpacer
size="s"
/>
</Fragment>
`;
@ -31,5 +50,24 @@ exports[`MonitorStatusList component renders null in place of child status with
locationNames={Set {}}
status="up"
/>
<EuiSpacer
size="s"
/>
<EuiCallOut
color="warning"
>
<FormattedMessage
defaultMessage="Some heartbeat instances do not have a location defined. {link} to your heartbeat configuration."
id="xpack.uptime.monitorList.drawer.missingLocation"
values={
Object {
"link": <LocationLink />,
}
}
/>
</EuiCallOut>
<EuiSpacer
size="s"
/>
</Fragment>
`;

View file

@ -11,6 +11,7 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { Check } from '../../../../../common/graphql/types';
import { LocationLink } from './location_link';
import { MonitorStatusRow } from './monitor_status_row';
import { UNNAMED_LOCATION } from '../../../../../common/constants';
interface MonitorStatusListProps {
/**
@ -21,7 +22,6 @@ interface MonitorStatusListProps {
export const UP = 'up';
export const DOWN = 'down';
export const UNNAMED_LOCATION = 'unnamed-location';
export const MonitorStatusList = ({ checks }: MonitorStatusListProps) => {
const upChecks: Set<string> = new Set();

View file

@ -8,7 +8,8 @@ import React, { useContext } from 'react';
import { EuiHealth, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { UptimeSettingsContext } from '../../../../contexts';
import { UNNAMED_LOCATION, UP } from './monitor_status_list';
import { UP } from './monitor_status_list';
import { UNNAMED_LOCATION } from '../../../../../common/constants';
interface MonitorStatusRowProps {
/**

View file

@ -5,7 +5,7 @@
*/
import { get } from 'lodash';
import { INDEX_NAMES } from '../../../../common/constants';
import { INDEX_NAMES, UNNAMED_LOCATION } from '../../../../common/constants';
import { MonitorChart, LocationDurationLine } from '../../../../common/graphql/types';
import { getHistogramIntervalFormatted } from '../../helper';
import { MonitorError, MonitorLocation } from '../../../../common/runtime_types';
@ -347,28 +347,32 @@ export const elasticsearchMonitorsAdapter: UMMonitorsAdapter = {
const locations = result?.aggregations?.location?.buckets ?? [];
const getGeo = (locGeo: any) => {
const { name, location } = locGeo;
const latLon = location.trim().split(',');
return {
name,
location: {
lat: latLon[0],
lon: latLon[1],
},
};
if (locGeo) {
const { name, location } = locGeo;
const latLon = location.trim().split(',');
return {
name,
location: {
lat: latLon[0],
lon: latLon[1],
},
};
} else {
return {
name: UNNAMED_LOCATION,
};
}
};
const monLocs: MonitorLocation[] = [];
locations.forEach((loc: any) => {
if (loc?.key !== '__location_missing__') {
const mostRecentLocation = loc.most_recent.hits.hits[0]._source;
const location: MonitorLocation = {
summary: mostRecentLocation?.summary,
geo: getGeo(mostRecentLocation?.observer?.geo),
timestamp: mostRecentLocation['@timestamp'],
};
monLocs.push(location);
}
const mostRecentLocation = loc.most_recent.hits.hits[0]._source;
const location: MonitorLocation = {
summary: mostRecentLocation?.summary,
geo: getGeo(mostRecentLocation?.observer?.geo),
timestamp: mostRecentLocation['@timestamp'],
};
monLocs.push(location);
});
return {