Feature/issue 44550 new details panel and location map (#50518)

* update

* added an embeddable maps

* update map config

* added options to disable zoom, hide tool tips, widgets/overlays in embeddable maps

* added options to disable zoom, hide tool tips, widgets/overlays in embeddable maps

* added bool option to hide header

* revert panel changes

* update panel

* update map

* added disable interactive

* update uptime embeddable

* update redux state and removed widget over lay hiding

* refactor widget overlay prop

* update layout

* update rest API

* remove maps code

* update components

* update up/down points on map

* update snaps

* fixed type

* update request

* update request to include rnage

* fix tests

* utilize newly added setLayers method

* remove unused code

* refactor code
This commit is contained in:
Shahzad 2019-12-13 16:02:00 +01:00 committed by GitHub
parent 6ca913874c
commit d79631adaa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 4080 additions and 126 deletions

View file

@ -0,0 +1,27 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as t from 'io-ts';
export const LocationType = t.partial({
lat: t.string,
lon: t.string,
});
export const CheckGeoType = t.partial({
name: t.string,
location: LocationType,
});
export const SummaryType = t.partial({
up: t.number,
down: t.number,
geo: CheckGeoType,
});
export type Summary = t.TypeOf<typeof SummaryType>;
export type CheckGeo = t.TypeOf<typeof CheckGeoType>;
export type Location = t.TypeOf<typeof LocationType>;

View file

@ -4,5 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
export * from './common';
export * from './snapshot';
export * from './monitor/monitor_details';
export * from './monitor/monitor_locations';

View file

@ -0,0 +1,22 @@
/*
* 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 t from 'io-ts';
import { CheckGeoType, SummaryType } from '../common';
// IO type for validation
export const MonitorLocationType = t.partial({
summary: SummaryType,
geo: CheckGeoType,
});
// Typescript type for type checking
export type MonitorLocation = t.TypeOf<typeof MonitorLocationType>;
export const MonitorLocationsType = t.intersection([
t.type({ monitorId: t.string }),
t.partial({ locations: t.array(MonitorLocationType) }),
]);
export type MonitorLocations = t.TypeOf<typeof MonitorLocationsType>;

View file

@ -7,6 +7,7 @@
import chrome from 'ui/chrome';
import { npStart } from 'ui/new_platform';
import { Plugin } from './plugin';
import 'uiExports/embeddableFactories';
new Plugin(
{ opaqueId: Symbol('uptime'), env: {} as any, config: { get: () => ({} as any) } },

View file

@ -2,71 +2,67 @@
exports[`MonitorStatusBar component renders duration in ms, not us 1`] = `
<div
class="euiPanel euiPanel--paddingMedium"
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive euiFlexGroup--wrap"
>
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive euiFlexGroup--wrap"
class="euiFlexItem euiFlexItem--flexGrowZero"
>
<div
aria-label="Monitor status"
class="euiHealth"
style="line-height:inherit"
>
<div
class="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow"
>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
>
<svg
class="euiIcon euiIcon--medium euiIcon--success euiIcon-isLoading"
focusable="false"
height="16"
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
/>
</div>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
>
Up
</div>
</div>
</div>
</div>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
>
<div
aria-label="Monitor status"
class="euiHealth"
style="line-height:inherit"
<a
aria-label="Monitor URL link"
class="euiLink euiLink--primary"
href="https://www.example.com/"
rel="noopener noreferrer"
target="_blank"
>
<div
class="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow"
>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
>
<svg
class="euiIcon euiIcon--medium euiIcon--success euiIcon-isLoading"
focusable="false"
height="16"
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
/>
</div>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
>
Up
</div>
</div>
</div>
</div>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
>
<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
aria-label="Monitor duration in milliseconds"
class="euiFlexItem euiFlexItem--flexGrowZero"
>
1234ms
</div>
<div
aria-label="Time since last check"
class="euiFlexItem"
>
15 minutes ago
https://www.example.com/
</a>
</div>
</div>
<div
aria-label="Monitor duration in milliseconds"
class="euiFlexItem euiFlexItem--flexGrowZero"
>
1234ms
</div>
<div
aria-label="Time since last check"
class="euiFlexItem"
>
15 minutes ago
</div>
</div>
`;

View file

@ -0,0 +1,185 @@
/*
* 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 lowPolyLayerFeatures from '../low_poly_layer.json';
export const mockDownPointsLayer = {
id: 'down_points',
label: 'Down Locations',
sourceDescriptor: {
type: 'GEOJSON_FILE',
__featureCollection: {
features: [
{
type: 'feature',
geometry: {
type: 'Point',
coordinates: [13.399262, 52.487239],
},
},
{
type: 'feature',
geometry: {
type: 'Point',
coordinates: [13.399262, 55.487239],
},
},
{
type: 'feature',
geometry: {
type: 'Point',
coordinates: [14.399262, 54.487239],
},
},
],
type: 'FeatureCollection',
},
},
visible: true,
style: {
type: 'VECTOR',
properties: {
fillColor: {
type: 'STATIC',
options: {
color: '#BC261E',
},
},
lineColor: {
type: 'STATIC',
options: {
color: '#fff',
},
},
lineWidth: {
type: 'STATIC',
options: {
size: 2,
},
},
iconSize: {
type: 'STATIC',
options: {
size: 6,
},
},
},
},
type: 'VECTOR',
};
export const mockUpPointsLayer = {
id: 'up_points',
label: 'Up Locations',
sourceDescriptor: {
type: 'GEOJSON_FILE',
__featureCollection: {
features: [
{
type: 'feature',
geometry: {
type: 'Point',
coordinates: [13.399262, 52.487239],
},
},
{
type: 'feature',
geometry: {
type: 'Point',
coordinates: [13.399262, 55.487239],
},
},
{
type: 'feature',
geometry: {
type: 'Point',
coordinates: [14.399262, 54.487239],
},
},
],
type: 'FeatureCollection',
},
},
visible: true,
style: {
type: 'VECTOR',
properties: {
fillColor: {
type: 'STATIC',
options: {
color: '#98A2B2',
},
},
lineColor: {
type: 'STATIC',
options: {
color: '#fff',
},
},
lineWidth: {
type: 'STATIC',
options: {
size: 2,
},
},
iconSize: {
type: 'STATIC',
options: {
size: 6,
},
},
},
},
type: 'VECTOR',
};
export const mockLayerList = [
{
id: 'low_poly_layer',
label: 'World countries',
minZoom: 0,
maxZoom: 24,
alpha: 1,
sourceDescriptor: {
id: 'b7486535-171b-4d3b-bb2e-33c1a0a2854c',
type: 'GEOJSON_FILE',
__featureCollection: lowPolyLayerFeatures,
},
visible: true,
style: {
type: 'VECTOR',
properties: {
fillColor: {
type: 'STATIC',
options: {
color: '#cad3e4',
},
},
lineColor: {
type: 'STATIC',
options: {
color: '#fff',
},
},
lineWidth: {
type: 'STATIC',
options: {
size: 0,
},
},
iconSize: {
type: 'STATIC',
options: {
size: 6,
},
},
},
},
type: 'VECTOR',
},
mockDownPointsLayer,
mockUpPointsLayer,
];

View file

@ -0,0 +1,102 @@
/*
* 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, { useEffect, useState } from 'react';
import uuid from 'uuid';
import styled from 'styled-components';
import { start } from '../../../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public/legacy';
import * as i18n from './translations';
// @ts-ignore
import { MAP_SAVED_OBJECT_TYPE } from '../../../../../../maps/common/constants';
import { MapEmbeddable } from './types';
import { getLayerList } from './map_config';
export interface EmbeddedMapProps {
upPoints: LocationPoint[];
downPoints: LocationPoint[];
}
export interface LocationPoint {
lat: string;
lon: string;
}
const EmbeddedPanel = styled.div`
z-index: auto;
flex: 1;
display: flex;
flex-direction: column;
height: 100%;
position: relative;
.embPanel__content {
display: flex;
flex: 1 1 100%;
z-index: 1;
min-height: 0; // Absolute must for Firefox to scroll contents
}
&&& .mapboxgl-canvas {
animation: none !important;
}
`;
export const EmbeddedMap = ({ upPoints, downPoints }: EmbeddedMapProps) => {
const [embeddable, setEmbeddable] = useState<MapEmbeddable>();
useEffect(() => {
async function setupEmbeddable() {
const mapState = {
layerList: getLayerList(upPoints, downPoints),
title: i18n.MAP_TITLE,
};
// @ts-ignore
const embeddableObject = await factory.createFromState(mapState, input, undefined);
setEmbeddable(embeddableObject);
}
setupEmbeddable();
}, []);
useEffect(() => {
if (embeddable) {
embeddable.setLayerList(getLayerList(upPoints, downPoints));
}
}, [upPoints, downPoints]);
useEffect(() => {
if (embeddableRoot.current && embeddable) {
embeddable.render(embeddableRoot.current);
}
}, [embeddable]);
const factory = start.getEmbeddableFactory(MAP_SAVED_OBJECT_TYPE);
const input = {
id: uuid.v4(),
filters: [],
hidePanelTitles: true,
query: { query: '', language: 'kuery' },
refreshConfig: { value: 0, pause: false },
viewMode: 'view',
isLayerTOCOpen: false,
hideFilterActions: true,
mapCenter: { lon: 11, lat: 47, zoom: 0 },
disableInteractive: true,
disableTooltipControl: true,
hideToolbarOverlay: true,
};
const embeddableRoot: React.RefObject<HTMLDivElement> = React.createRef();
return (
<EmbeddedPanel>
<div className="embPanel__content" ref={embeddableRoot} />
</EmbeddedPanel>
);
};
EmbeddedMap.displayName = 'EmbeddedMap';

View file

@ -0,0 +1,40 @@
/*
* 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 { getLayerList } from './map_config';
import { mockLayerList } from './__mocks__/mock';
import { LocationPoint } from './embedded_map';
jest.mock('uuid', () => {
return {
v4: jest.fn(() => 'uuid.v4()'),
};
});
describe('map_config', () => {
let upPoints: LocationPoint[];
let downPoints: LocationPoint[];
beforeEach(() => {
upPoints = [
{ lat: '52.487239', lon: '13.399262' },
{ lat: '55.487239', lon: '13.399262' },
{ lat: '54.487239', lon: '14.399262' },
];
downPoints = [
{ lat: '52.487239', lon: '13.399262' },
{ lat: '55.487239', lon: '13.399262' },
{ lat: '54.487239', lon: '14.399262' },
];
});
describe('#getLayerList', () => {
test('it returns the low poly layer', () => {
const layerList = getLayerList(upPoints, downPoints);
expect(layerList).toStrictEqual(mockLayerList);
});
});
});

View file

@ -0,0 +1,167 @@
/*
* 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 lowPolyLayerFeatures from './low_poly_layer.json';
import { LocationPoint } from './embedded_map';
/**
* Returns `Source/Destination Point-to-point` Map LayerList configuration, with a source,
* destination, and line layer for each of the provided indexPatterns
*
*/
export const getLayerList = (upPoints: LocationPoint[], downPoints: LocationPoint[]) => {
return [getLowPolyLayer(), getDownPointsLayer(downPoints), getUpPointsLayer(upPoints)];
};
export const getLowPolyLayer = () => {
return {
id: 'low_poly_layer',
label: 'World countries',
minZoom: 0,
maxZoom: 24,
alpha: 1,
sourceDescriptor: {
id: 'b7486535-171b-4d3b-bb2e-33c1a0a2854c',
type: 'GEOJSON_FILE',
__featureCollection: lowPolyLayerFeatures,
},
visible: true,
style: {
type: 'VECTOR',
properties: {
fillColor: {
type: 'STATIC',
options: {
color: '#cad3e4',
},
},
lineColor: {
type: 'STATIC',
options: {
color: '#fff',
},
},
lineWidth: {
type: 'STATIC',
options: {
size: 0,
},
},
iconSize: {
type: 'STATIC',
options: {
size: 6,
},
},
},
},
type: 'VECTOR',
};
};
export const getDownPointsLayer = (downPoints: LocationPoint[]) => {
const features = downPoints?.map(point => ({
type: 'feature',
geometry: {
type: 'Point',
coordinates: [+point.lon, +point.lat],
},
}));
return {
id: 'down_points',
label: 'Down Locations',
sourceDescriptor: {
type: 'GEOJSON_FILE',
__featureCollection: {
features,
type: 'FeatureCollection',
},
},
visible: true,
style: {
type: 'VECTOR',
properties: {
fillColor: {
type: 'STATIC',
options: {
color: '#BC261E',
},
},
lineColor: {
type: 'STATIC',
options: {
color: '#fff',
},
},
lineWidth: {
type: 'STATIC',
options: {
size: 2,
},
},
iconSize: {
type: 'STATIC',
options: {
size: 6,
},
},
},
},
type: 'VECTOR',
};
};
export const getUpPointsLayer = (upPoints: LocationPoint[]) => {
const features = upPoints?.map(point => ({
type: 'feature',
geometry: {
type: 'Point',
coordinates: [+point.lon, +point.lat],
},
}));
return {
id: 'up_points',
label: 'Up Locations',
sourceDescriptor: {
type: 'GEOJSON_FILE',
__featureCollection: {
features,
type: 'FeatureCollection',
},
},
visible: true,
style: {
type: 'VECTOR',
properties: {
fillColor: {
type: 'STATIC',
options: {
color: '#98A2B2',
},
},
lineColor: {
type: 'STATIC',
options: {
color: '#fff',
},
},
lineWidth: {
type: 'STATIC',
options: {
size: 2,
},
},
iconSize: {
type: 'STATIC',
options: {
size: 6,
},
},
},
},
type: 'VECTOR',
};
};

View file

@ -0,0 +1,14 @@
/*
* 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 MAP_TITLE = i18n.translate(
'xpack.uptime.components.embeddables.embeddedMap.embeddablePanelTitle',
{
defaultMessage: 'Monitor Observer Location Map',
}
);

View file

@ -0,0 +1,31 @@
/*
* 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 { Query } from 'src/plugins/data/common';
import { TimeRange } from 'src/plugins/data/public';
import {
EmbeddableInput,
EmbeddableOutput,
IEmbeddable,
} from '../../../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public';
import { esFilters } from '../../../../../../../../../src/plugins/data/public';
export interface MapEmbeddableInput extends EmbeddableInput {
filters: esFilters.Filter[];
query: Query;
refreshConfig: {
isPaused: boolean;
interval: number;
};
timeRange?: TimeRange;
}
export interface CustomProps {
setLayerList: Function;
}
export type MapEmbeddable = IEmbeddable<MapEmbeddableInput, EmbeddableOutput> & CustomProps;

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 * from './location_map';

View file

@ -0,0 +1,38 @@
/*
* 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';
const MapPanel = styled.div`
height: 400px;
width: 520px;
`;
interface LocationMapProps {
monitorLocations: any;
}
export const LocationMap = ({ monitorLocations }: LocationMapProps) => {
const upPoints: LocationPoint[] = [];
const downPoints: LocationPoint[] = [];
if (monitorLocations?.locations) {
monitorLocations.locations.forEach((item: any) => {
if (item.summary.down === 0) {
upPoints.push(item.geo.location);
} else {
downPoints.push(item.geo.location);
}
});
}
return (
<MapPanel>
<EmbeddedMap upPoints={upPoints} downPoints={downPoints} />
</MapPanel>
);
};

View file

@ -4,8 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiFlexGroup, EuiFlexItem, EuiHealth, EuiLink, EuiPanel } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { EuiFlexGroup, EuiFlexItem, EuiHealth, EuiLink } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { get } from 'lodash';
import moment from 'moment';
@ -16,6 +15,7 @@ import { monitorStatusBarQuery } from '../../../queries';
import { EmptyStatusBar } from '../empty_status_bar';
import { convertMicrosecondsToMilliseconds } from '../../../lib/helper';
import { MonitorSSLCertificate } from './monitor_ssl_certificate';
import * as labels from './translations';
interface MonitorStatusBarQueryResult {
monitorStatus?: Ping[];
@ -28,58 +28,33 @@ interface MonitorStatusBarProps {
type Props = MonitorStatusBarProps & UptimeGraphQLQueryProps<MonitorStatusBarQueryResult>;
export const MonitorStatusBarComponent = ({ data, monitorId }: Props) => {
if (data && data.monitorStatus && data.monitorStatus.length) {
if (data?.monitorStatus?.length) {
const { monitor, timestamp, tls } = data.monitorStatus[0];
const duration: number | undefined = get(monitor, 'duration.us', undefined);
const status = get<'up' | 'down'>(monitor, 'status', 'down');
const full = get<string>(data.monitorStatus[0], 'url.full');
return (
<EuiPanel>
<>
<EuiFlexGroup gutterSize="l" wrap>
<EuiFlexItem grow={false}>
<EuiHealth
aria-label={i18n.translate(
'xpack.uptime.monitorStatusBar.healthStatusMessageAriaLabel',
{
defaultMessage: 'Monitor status',
}
)}
aria-label={labels.healthStatusMessageAriaLabel}
color={status === 'up' ? 'success' : 'danger'}
style={{ lineHeight: 'inherit' }}
>
{status === 'up'
? i18n.translate('xpack.uptime.monitorStatusBar.healthStatusMessage.upLabel', {
defaultMessage: 'Up',
})
: i18n.translate('xpack.uptime.monitorStatusBar.healthStatusMessage.downLabel', {
defaultMessage: 'Down',
})}
{status === 'up' ? labels.upLabel : labels.downLabel}
</EuiHealth>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexItem grow={false}>
<EuiLink
aria-label={i18n.translate(
'xpack.uptime.monitorStatusBar.monitorUrlLinkAriaLabel',
{
defaultMessage: 'Monitor URL link',
}
)}
href={full}
target="_blank"
>
<EuiLink aria-label={labels.monitorUrlLinkAriaLabel} href={full} target="_blank">
{full}
</EuiLink>
</EuiFlexItem>
</EuiFlexItem>
{!!duration && (
<EuiFlexItem
aria-label={i18n.translate('xpack.uptime.monitorStatusBar.durationTextAriaLabel', {
defaultMessage: 'Monitor duration in milliseconds',
})}
grow={false}
>
<EuiFlexItem aria-label={labels.durationTextAriaLabel} grow={false}>
<FormattedMessage
id="xpack.uptime.monitorStatusBar.healthStatus.durationInMillisecondsMessage"
values={{ duration: convertMicrosecondsToMilliseconds(duration) }}
@ -88,30 +63,15 @@ export const MonitorStatusBarComponent = ({ data, monitorId }: Props) => {
/>
</EuiFlexItem>
)}
<EuiFlexItem
aria-label={i18n.translate(
'xpack.uptime.monitorStatusBar.timestampFromNowTextAriaLabel',
{
defaultMessage: 'Time since last check',
}
)}
grow={true}
>
<EuiFlexItem aria-label={labels.timestampFromNowTextAriaLabel} grow={true}>
{moment(new Date(timestamp).valueOf()).fromNow()}
</EuiFlexItem>
</EuiFlexGroup>
<MonitorSSLCertificate tls={tls} />
</EuiPanel>
</>
);
}
return (
<EmptyStatusBar
message={i18n.translate('xpack.uptime.monitorStatusBar.loadingMessage', {
defaultMessage: 'Loading…',
})}
monitorId={monitorId}
/>
);
return <EmptyStatusBar message={labels.loadingMessage} monitorId={monitorId} />;
};
export const MonitorStatusBar = withUptimeGraphQL<

View file

@ -0,0 +1,49 @@
/*
* 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,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 { connect } from 'react-redux';
import { AppState } from '../../../state';
import { getMonitorLocations } from '../../../state/selectors';
import { fetchMonitorLocations } from '../../../state/actions/monitor';
import { MonitorStatusDetailsComponent } from './monitor_status_details';
const mapStateToProps = (state: AppState, { monitorId }: any) => ({
monitorLocations: getMonitorLocations(state, monitorId),
});
const mapDispatchToProps = (dispatch: any, ownProps: any) => ({
loadMonitorLocations: () => {
const { dateStart, dateEnd, monitorId } = ownProps;
dispatch(
fetchMonitorLocations({
monitorId,
dateStart,
dateEnd,
})
);
},
});
export const MonitorStatusDetails = connect(
mapStateToProps,
mapDispatchToProps
)(MonitorStatusDetailsComponent);
export * from './monitor_status_details';

View file

@ -0,0 +1,45 @@
/*
* 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, { useEffect } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
import { LocationMap } from '../location_map';
import { MonitorStatusBar } from '../monitor_status_bar';
interface MonitorStatusBarProps {
monitorId: string;
variables: any;
loadMonitorLocations: any;
monitorLocations: any;
dateStart: any;
dateEnd: any;
}
export const MonitorStatusDetailsComponent = ({
monitorId,
variables,
loadMonitorLocations,
monitorLocations,
dateStart,
dateEnd,
}: MonitorStatusBarProps) => {
useEffect(() => {
loadMonitorLocations(monitorId);
}, [monitorId, dateStart, dateEnd]);
return (
<EuiPanel>
<EuiFlexGroup gutterSize="l" wrap>
<EuiFlexItem grow={true}>
<MonitorStatusBar monitorId={monitorId} variables={variables} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<LocationMap monitorLocations={monitorLocations} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
};

View file

@ -0,0 +1,49 @@
/*
* 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

@ -4,26 +4,19 @@
* you may not use this file except in compliance with the Elastic License.
*/
import {
// @ts-ignore No typings for EuiSpacer
EuiSpacer,
} from '@elastic/eui';
import { EuiSpacer } from '@elastic/eui';
import { ApolloQueryResult, OperationVariables, QueryOptions } from 'apollo-client';
import gql from 'graphql-tag';
import React, { Fragment, useContext, useEffect, useState } from 'react';
import { getMonitorPageBreadcrumb } from '../breadcrumbs';
import {
MonitorCharts,
MonitorPageTitle,
MonitorStatusBar,
PingList,
} from '../components/functional';
import { MonitorCharts, MonitorPageTitle, PingList } from '../components/functional';
import { UMUpdateBreadcrumbs } from '../lib/lib';
import { UptimeSettingsContext } from '../contexts';
import { useUrlParams } from '../hooks';
import { stringifyUrlParams } from '../lib/helper/stringify_url_params';
import { useTrackPageview } from '../../../infra/public';
import { getTitle } from '../lib/helper/get_title';
import { MonitorStatusDetails } from '../components/functional/monitor_status_details';
interface MonitorPageProps {
logMonitorPageLoad: () => void;
@ -92,7 +85,12 @@ export const MonitorPage = ({
<Fragment>
<MonitorPageTitle monitorId={monitorId} variables={{ monitorId }} />
<EuiSpacer size="s" />
<MonitorStatusBar monitorId={monitorId} variables={sharedVariables} />
<MonitorStatusDetails
monitorId={monitorId}
variables={sharedVariables}
dateStart={absoluteDateRangeStart}
dateEnd={absoluteDateRangeEnd}
/>
<EuiSpacer size="s" />
<MonitorCharts
{...colors}

View file

@ -4,10 +4,17 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { MonitorLocations } from '../../../common/runtime_types';
import { QueryParams } from './types';
export const FETCH_MONITOR_DETAILS = 'FETCH_MONITOR_DETAILS';
export const FETCH_MONITOR_DETAILS_SUCCESS = 'FETCH_MONITOR_DETAILS_SUCCESS';
export const FETCH_MONITOR_DETAILS_FAIL = 'FETCH_MONITOR_DETAILS_FAIL';
export const FETCH_MONITOR_LOCATIONS = 'FETCH_MONITOR_LOCATIONS';
export const FETCH_MONITOR_LOCATIONS_SUCCESS = 'FETCH_MONITOR_LOCATIONS_SUCCESS';
export const FETCH_MONITOR_LOCATIONS_FAIL = 'FETCH_MONITOR_LOCATIONS_FAIL';
export interface MonitorDetailsState {
monitorId: string;
error: Error;
@ -28,6 +35,25 @@ interface GetMonitorDetailsFailAction {
payload: any;
}
export interface MonitorLocationsPayload extends QueryParams {
monitorId: string;
}
interface GetMonitorLocationsAction {
type: typeof FETCH_MONITOR_LOCATIONS;
payload: MonitorLocationsPayload;
}
interface GetMonitorLocationsSuccessAction {
type: typeof FETCH_MONITOR_LOCATIONS_SUCCESS;
payload: MonitorLocations;
}
interface GetMonitorLocationsFailAction {
type: typeof FETCH_MONITOR_LOCATIONS_FAIL;
payload: any;
}
export function fetchMonitorDetails(monitorId: string): GetMonitorDetailsAction {
return {
type: FETCH_MONITOR_DETAILS,
@ -51,7 +77,33 @@ export function fetchMonitorDetailsFail(error: any): GetMonitorDetailsFailAction
};
}
export function fetchMonitorLocations(payload: MonitorLocationsPayload): GetMonitorLocationsAction {
return {
type: FETCH_MONITOR_LOCATIONS,
payload,
};
}
export function fetchMonitorLocationsSuccess(
monitorLocationsState: MonitorLocations
): GetMonitorLocationsSuccessAction {
return {
type: FETCH_MONITOR_LOCATIONS_SUCCESS,
payload: monitorLocationsState,
};
}
export function fetchMonitorLocationsFail(error: any): GetMonitorLocationsFailAction {
return {
type: FETCH_MONITOR_LOCATIONS_FAIL,
payload: error,
};
}
export type MonitorActionTypes =
| GetMonitorDetailsAction
| GetMonitorDetailsSuccessAction
| GetMonitorDetailsFailAction;
| GetMonitorDetailsFailAction
| GetMonitorLocationsAction
| GetMonitorLocationsSuccessAction
| GetMonitorLocationsFailAction;

View file

@ -0,0 +1,12 @@
/*
* 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 interface QueryParams {
dateStart: string;
dateEnd: string;
filters?: string;
statusFilter?: string;
}

View file

@ -6,7 +6,13 @@
import { ThrowReporter } from 'io-ts/lib/ThrowReporter';
import { getApiPath } from '../../lib/helper';
import { MonitorDetailsType, MonitorDetails } from '../../../common/runtime_types';
import {
MonitorDetailsType,
MonitorDetails,
MonitorLocations,
MonitorLocationsType,
} from '../../../common/runtime_types';
import { QueryParams } from '../actions/types';
interface ApiRequest {
monitorId: string;
@ -27,3 +33,30 @@ export const fetchMonitorDetails = async ({
return data;
});
};
type ApiParams = QueryParams & ApiRequest;
export const fetchMonitorLocations = async ({
monitorId,
basePath,
dateStart,
dateEnd,
}: ApiParams): Promise<MonitorLocations> => {
const url = getApiPath(`/api/uptime/monitor/locations`, basePath);
const params = {
dateStart,
dateEnd,
monitorId,
};
const urlParams = new URLSearchParams(params).toString();
const response = await fetch(`${url}?${urlParams}`);
if (!response.ok) {
throw new Error(response.statusText);
}
return response.json().then(data => {
ThrowReporter.report(MonitorLocationsType.decode(data));
return data;
});
};

View file

@ -10,8 +10,11 @@ import {
FETCH_MONITOR_DETAILS,
FETCH_MONITOR_DETAILS_SUCCESS,
FETCH_MONITOR_DETAILS_FAIL,
FETCH_MONITOR_LOCATIONS,
FETCH_MONITOR_LOCATIONS_SUCCESS,
FETCH_MONITOR_LOCATIONS_FAIL,
} from '../actions/monitor';
import { fetchMonitorDetails } from '../api';
import { fetchMonitorDetails, fetchMonitorLocations } from '../api';
import { getBasePath } from '../selectors';
function* monitorDetailsEffect(action: Action<any>) {
@ -25,6 +28,18 @@ function* monitorDetailsEffect(action: Action<any>) {
}
}
function* monitorLocationsEffect(action: Action<any>) {
const payload = action.payload;
try {
const basePath = yield select(getBasePath);
const response = yield call(fetchMonitorLocations, { basePath, ...payload });
yield put({ type: FETCH_MONITOR_LOCATIONS_SUCCESS, payload: response });
} catch (error) {
yield put({ type: FETCH_MONITOR_LOCATIONS_FAIL, payload: error.message });
}
}
export function* fetchMonitorDetailsEffect() {
yield takeLatest(FETCH_MONITOR_DETAILS, monitorDetailsEffect);
yield takeLatest(FETCH_MONITOR_LOCATIONS, monitorLocationsEffect);
}

View file

@ -10,16 +10,24 @@ import {
FETCH_MONITOR_DETAILS,
FETCH_MONITOR_DETAILS_SUCCESS,
FETCH_MONITOR_DETAILS_FAIL,
FETCH_MONITOR_LOCATIONS,
FETCH_MONITOR_LOCATIONS_SUCCESS,
FETCH_MONITOR_LOCATIONS_FAIL,
} from '../actions/monitor';
import { MonitorLocations } from '../../../common/runtime_types';
type MonitorLocationsList = Map<string, MonitorLocations>;
export interface MonitorState {
monitorDetailsList: MonitorDetailsState[];
monitorLocationsList: MonitorLocationsList;
loading: boolean;
errors: any[];
}
const initialState: MonitorState = {
monitorDetailsList: [],
monitorLocationsList: new Map(),
loading: false,
errors: [],
};
@ -42,10 +50,27 @@ export function monitorReducer(state = initialState, action: MonitorActionTypes)
loading: false,
};
case FETCH_MONITOR_DETAILS_FAIL:
const error = action.payload;
return {
...state,
errors: [...state.errors, error],
errors: [...state.errors, action.payload],
};
case FETCH_MONITOR_LOCATIONS:
return {
...state,
loading: true,
};
case FETCH_MONITOR_LOCATIONS_SUCCESS:
const monLocations = state.monitorLocationsList;
monLocations.set(action.payload.monitorId, action.payload);
return {
...state,
monitorLocationsList: monLocations,
loading: false,
};
case FETCH_MONITOR_LOCATIONS_FAIL:
return {
...state,
errors: [...state.errors, action.payload],
};
default:
return state;

View file

@ -11,6 +11,7 @@ describe('state selectors', () => {
const state: AppState = {
monitor: {
monitorDetailsList: [],
monitorLocationsList: new Map(),
loading: false,
errors: [],
},

View file

@ -14,3 +14,7 @@ export const isIntegrationsPopupOpen = ({ ui: { integrationsPopoverOpen } }: App
export const getMonitorDetails = (state: AppState, summary: any) => {
return state.monitor.monitorDetailsList[summary.monitor_id];
};
export const getMonitorLocations = (state: AppState, monitorId: string) => {
return state.monitor.monitorLocationsList?.get(monitorId);
};

View file

@ -17,4 +17,10 @@ export interface UMMonitorsAdapter {
getFilterBar(request: any, dateRangeStart: string, dateRangeEnd: string): Promise<any>;
getMonitorPageTitle(request: any, monitorId: string): Promise<MonitorPageTitle | null>;
getMonitorDetails(request: any, monitorId: string): Promise<any>;
getMonitorLocations(
request: any,
monitorId: string,
dateStart: string,
dateEnd: string
): Promise<any>;
}

View file

@ -16,7 +16,12 @@ import {
import { getHistogramIntervalFormatted } from '../../helper';
import { DatabaseAdapter } from '../database';
import { UMMonitorsAdapter } from './adapter_types';
import { MonitorDetails, MonitorError } from '../../../../common/runtime_types';
import {
MonitorDetails,
MonitorError,
MonitorLocations,
MonitorLocation,
} from '../../../../common/runtime_types';
const formatStatusBuckets = (time: any, buckets: any, docCount: any) => {
let up = null;
@ -273,6 +278,11 @@ export class ElasticsearchMonitorsAdapter implements UMMonitorsAdapter {
};
}
/**
* Fetch data for the monitor page title.
* @param request Kibana server request
* @param monitorId the ID to query
*/
public async getMonitorDetails(request: any, monitorId: string): Promise<MonitorDetails> {
const params = {
index: INDEX_NAMES.HEARTBEAT,
@ -320,4 +330,100 @@ export class ElasticsearchMonitorsAdapter implements UMMonitorsAdapter {
timestamp: errorTimeStamp,
};
}
/**
* Fetch data for the monitor page title.
* @param request Kibana server request
* @param monitorId the ID to query
*/
public async getMonitorLocations(
request: any,
monitorId: string,
dateStart: string,
dateEnd: string
): Promise<MonitorLocations> {
const params = {
index: INDEX_NAMES.HEARTBEAT,
body: {
size: 0,
query: {
bool: {
filter: [
{
match: {
'monitor.id': monitorId,
},
},
{
exists: {
field: 'summary',
},
},
{
range: {
'@timestamp': {
gte: dateStart,
lte: dateEnd,
},
},
},
],
},
},
aggs: {
location: {
terms: {
field: 'observer.geo.name',
missing: '__location_missing__',
},
aggs: {
most_recent: {
top_hits: {
size: 1,
sort: {
'@timestamp': {
order: 'desc',
},
},
_source: ['monitor', 'summary', 'observer'],
},
},
},
},
},
},
};
const result = await this.database.search(request, params);
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],
},
};
};
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),
};
monLocs.push(location);
}
});
return {
monitorId,
locations: monLocs,
};
}
}

View file

@ -9,7 +9,7 @@ import { createGetIndexPatternRoute } from './index_pattern';
import { createLogMonitorPageRoute, createLogOverviewPageRoute } from './telemetry';
import { createGetSnapshotCount } from './snapshot';
import { UMRestApiRouteCreator } from './types';
import { createGetMonitorDetailsRoute } from './monitors';
import { createGetMonitorDetailsRoute, createGetMonitorLocationsRoute } from './monitors';
export * from './types';
export { createRouteWithAuth } from './create_route_with_auth';
@ -17,6 +17,7 @@ export const restApiRoutes: UMRestApiRouteCreator[] = [
createGetAllRoute,
createGetIndexPatternRoute,
createGetMonitorDetailsRoute,
createGetMonitorLocationsRoute,
createGetSnapshotCount,
createLogMonitorPageRoute,
createLogOverviewPageRoute,

View file

@ -5,3 +5,4 @@
*/
export { createGetMonitorDetailsRoute } from './monitors_details';
export { createGetMonitorLocationsRoute } from './monitor_locations';

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 { schema } from '@kbn/config-schema';
import { UMServerLibs } from '../../lib/lib';
import { UMRestApiRouteCreator } from '../types';
export const createGetMonitorLocationsRoute: UMRestApiRouteCreator = (libs: UMServerLibs) => ({
method: 'GET',
path: '/api/uptime/monitor/locations',
validate: {
query: schema.object({
monitorId: schema.string(),
dateStart: schema.string(),
dateEnd: schema.string(),
}),
},
options: {
tags: ['access:uptime'],
},
handler: async (_context, request, response): Promise<any> => {
const { monitorId, dateStart, dateEnd } = request.query;
return response.ok({
body: {
...(await libs.monitors.getMonitorLocations(request, monitorId, dateStart, dateEnd)),
},
});
},
});