[Uptime] Monitor details page left side title (#53529)

* 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

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Shahzad 2020-01-09 18:23:44 +01:00 committed by GitHub
parent c2362d4807
commit 31a0bfd540
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
85 changed files with 1202 additions and 977 deletions

View file

@ -3,7 +3,7 @@
## Purpose
The purpose of this plugin is to provide users of Heartbeat more visibility of what's happening
in their infrasturcture. It's primarily built using React and Apollo's GraphQL tools.
in their infrastructure. It's primarily built using React and Apollo's GraphQL tools.
## Layout
@ -44,7 +44,7 @@ From `~/kibana/x-pack`, run `node scripts/jest.js`.
### Functional tests
In one shell, from **~/kibana/x-pack**:
`node scripts/functional_tests-server.js`
`node scripts/functional_tests_server.js`
In another shell, from **~kibana/x-pack**:
`node ../scripts/functional_test_runner.js --grep="{TEST_NAME}"`.

View file

@ -352,25 +352,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "getMonitorPageTitle",
"description": "",
"args": [
{
"name": "monitorId",
"description": "",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
},
"defaultValue": null
}
],
"type": { "kind": "OBJECT", "name": "MonitorPageTitle", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "getMonitorStates",
"description": "Fetches the current state of Uptime monitors for the given parameters.",
@ -2549,45 +2530,6 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "MonitorPageTitle",
"description": "",
"fields": [
{
"name": "id",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "url",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "name",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "MonitorSummaryResult",

View file

@ -32,7 +32,6 @@ export interface Query {
getFilterBar?: FilterBar | null;
getMonitorPageTitle?: MonitorPageTitle | null;
/** Fetches the current state of Uptime monitors for the given parameters. */
getMonitorStates?: MonitorSummaryResult | null;
/** Fetches details about the uptime index. */
@ -484,13 +483,6 @@ export interface FilterBar {
urls?: string[] | null;
}
export interface MonitorPageTitle {
id: string;
url?: string | null;
name?: string | null;
}
/** The primary object returned for monitor states. */
export interface MonitorSummaryResult {
/** Used to go to the next page of results */
@ -736,24 +728,12 @@ export interface GetMonitorChartsDataQueryArgs {
location?: string | null;
}
export interface GetLatestMonitorsQueryArgs {
/** The lower limit of the date range. */
dateRangeStart: string;
/** The upper limit of the date range. */
dateRangeEnd: string;
/** Optional: a specific monitor ID filter. */
monitorId?: string | null;
/** Optional: a specific instance location filter. */
location?: string | null;
}
export interface GetFilterBarQueryArgs {
dateRangeStart: string;
dateRangeEnd: string;
}
export interface GetMonitorPageTitleQueryArgs {
monitorId: string;
}
export interface GetMonitorStatesQueryArgs {
dateRangeStart: string;

View file

@ -6,5 +6,4 @@
export * from './common';
export * from './snapshot';
export * from './monitor/monitor_details';
export * from './monitor/monitor_locations';
export * from './monitor';

View file

@ -3,6 +3,7 @@
* 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';
// IO type for validation

View file

@ -4,5 +4,5 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { MonitorSSLCertificate } from './monitor_ssl_certificate';
export { MonitorStatusBar, MonitorStatusBarComponent } from './monitor_status_bar';
export * from './details';
export * from './locations';

View file

@ -1,29 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EmptyStatusBar component renders a default message when no message provided 1`] = `
<EuiPanel>
<EuiFlexGroup
gutterSize="l"
>
<EuiFlexItem
grow={false}
>
No data found for monitor id mon_id
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
`;
exports[`EmptyStatusBar component renders a message when provided 1`] = `
<EuiPanel>
<EuiFlexGroup
gutterSize="l"
>
<EuiFlexItem
grow={false}
>
foobarbaz
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
`;

View file

@ -5,22 +5,14 @@ Array [
<div
class="euiSpacer euiSpacer--s"
/>,
.c0 {
margin-left: 20px;
}
<div
class="c0"
<div
aria-label="SSL certificate expires"
class="euiText euiText--small euiText--constrainedWidth"
>
<div
aria-label="SSL certificate expires"
class="euiText euiText--small euiText--constrainedWidth"
class="euiTextColor euiTextColor--subdued"
>
<div
class="euiTextColor euiTextColor--subdued"
>
SSL certificate expires in 2 months
</div>
SSL certificate expires in 2 months
</div>
</div>,
]

View file

@ -2,46 +2,24 @@
exports[`MonitorStatusBar component renders duration in ms, not us 1`] = `
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive euiFlexGroup--wrap"
class="euiFlexGroup euiFlexGroup--directionColumn euiFlexGroup--responsive"
>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
>
<div
aria-label="Monitor status"
class="euiHealth"
style="line-height:inherit"
class="euiText euiText--medium"
>
<div
class="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow"
>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
>
<svg
aria-hidden="true"
class="euiIcon euiIcon--medium euiIcon--success euiIcon-isLoading"
focusable="false"
height="16"
role="img"
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
/>
</div>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
>
Up
</div>
</div>
<h2>
Up in 2 Locations
</h2>
</div>
</div>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
class="euiText euiText--medium"
>
<a
aria-label="Monitor URL link"
@ -55,16 +33,23 @@ exports[`MonitorStatusBar component renders duration in ms, not us 1`] = `
</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
<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

@ -227,54 +227,24 @@ exports[`UptimeDatePicker component validates props with shallow render 1`] = `
commonlyUsedRanges={
Array [
Object {
"end": "now/d",
"end": "now",
"label": "Today",
"start": "now/d",
},
Object {
"end": "now/w",
"label": "This week",
"end": "now",
"label": "Week to date",
"start": "now/w",
},
Object {
"end": "now",
"label": "Last 15 minutes",
"start": "now-15m",
"label": "Month to date",
"start": "now/M",
},
Object {
"end": "now",
"label": "Last 30 minutes",
"start": "now-30m",
},
Object {
"end": "now",
"label": "Last 1 hour",
"start": "now-1h",
},
Object {
"end": "now",
"label": "Last 24 hours",
"start": "now-24h",
},
Object {
"end": "now",
"label": "Last 7 days",
"start": "now-7d",
},
Object {
"end": "now",
"label": "Last 30 days",
"start": "now-30d",
},
Object {
"end": "now",
"label": "Last 90 days",
"start": "now-90d",
},
Object {
"end": "now",
"label": "Last 2 year",
"start": "now-1y",
"label": "Year to date",
"start": "now/y",
},
]
}

View file

@ -1,21 +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 { shallowWithIntl } from 'test_utils/enzyme_helpers';
import { EmptyStatusBar } from '../empty_status_bar';
describe('EmptyStatusBar component', () => {
it('renders a message when provided', () => {
const component = shallowWithIntl(<EmptyStatusBar message="foobarbaz" monitorId="mon_id" />);
expect(component).toMatchSnapshot();
});
it('renders a default message when no message provided', () => {
const component = shallowWithIntl(<EmptyStatusBar monitorId="mon_id" />);
expect(component).toMatchSnapshot();
});
});

View file

@ -8,7 +8,7 @@ import React from 'react';
import moment from 'moment';
import { renderWithIntl } from 'test_utils/enzyme_helpers';
import { PingTls } from '../../../../common/graphql/types';
import { MonitorSSLCertificate } from '../monitor_status_bar';
import { MonitorSSLCertificate } from '../monitor_status_details/monitor_status_bar';
describe('MonitorStatusBar component', () => {
let monitorTls: PingTls;

View file

@ -8,34 +8,59 @@ import moment from 'moment';
import React from 'react';
import { renderWithIntl } from 'test_utils/enzyme_helpers';
import { Ping } from '../../../../common/graphql/types';
import { MonitorStatusBarComponent } from '../monitor_status_bar';
import { MonitorStatusBarComponent } from '../monitor_status_details/monitor_status_bar';
describe('MonitorStatusBar component', () => {
let monitorStatus: Ping[];
let monitorStatus: Ping;
let monitorLocations: any;
let dateStart: string;
let dateEnd: string;
beforeEach(() => {
monitorStatus = [
{
id: 'id1',
timestamp: moment(new Date())
.subtract(15, 'm')
.toString(),
monitor: {
duration: {
us: 1234567,
},
status: 'up',
},
url: {
full: 'https://www.example.com/',
monitorStatus = {
id: 'id1',
timestamp: moment(new Date())
.subtract(15, 'm')
.toString(),
monitor: {
duration: {
us: 1234567,
},
status: 'up',
},
];
url: {
full: 'https://www.example.com/',
},
};
monitorLocations = {
monitorId: 'secure-avc',
locations: [
{
summary: { up: 4, down: 0 },
geo: { name: 'Berlin', location: { lat: '52.487448', lon: ' 13.394798' } },
},
{
summary: { up: 4, down: 0 },
geo: { name: 'st-paul', location: { lat: '52.487448', lon: ' 13.394798' } },
},
],
};
dateStart = moment('01-01-2010').toString();
dateEnd = moment('10-10-2010').toString();
});
it('renders duration in ms, not us', () => {
const component = renderWithIntl(
<MonitorStatusBarComponent loading={false} data={{ monitorStatus }} monitorId="foo" />
<MonitorStatusBarComponent
monitorStatus={monitorStatus}
monitorId="id1"
dateStart={dateStart}
dateEnd={dateEnd}
monitorLocations={monitorLocations}
loadMonitorStatus={jest.fn()}
/>
);
expect(component).toMatchSnapshot();
});

View file

@ -6,37 +6,16 @@
import { shallowWithIntl, renderWithIntl } from 'test_utils/enzyme_helpers';
import React from 'react';
import { UptimeDatePicker, CommonlyUsedRange } from '../uptime_date_picker';
import { UptimeDatePicker } from '../uptime_date_picker';
describe('UptimeDatePicker component', () => {
let commonlyUsedRange: CommonlyUsedRange[];
beforeEach(() => {
commonlyUsedRange = [
{ from: 'now/d', to: 'now/d', display: 'Today' },
{ from: 'now/w', to: 'now/w', display: 'This week' },
{ from: 'now-15m', to: 'now', display: 'Last 15 minutes' },
{ from: 'now-30m', to: 'now', display: 'Last 30 minutes' },
{ from: 'now-1h', to: 'now', display: 'Last 1 hour' },
{ from: 'now-24h', to: 'now', display: 'Last 24 hours' },
{ from: 'now-7d', to: 'now', display: 'Last 7 days' },
{ from: 'now-30d', to: 'now', display: 'Last 30 days' },
{ from: 'now-90d', to: 'now', display: 'Last 90 days' },
{ from: 'now-1y', to: 'now', display: 'Last 2 year' },
];
});
it('validates props with shallow render', () => {
const component = shallowWithIntl(
<UptimeDatePicker commonlyUsedRanges={commonlyUsedRange} refreshApp={jest.fn()} />
);
const component = shallowWithIntl(<UptimeDatePicker refreshApp={jest.fn()} />);
expect(component).toMatchSnapshot();
});
it('renders properly with mock data', () => {
const component = renderWithIntl(
<UptimeDatePicker commonlyUsedRanges={commonlyUsedRange} refreshApp={jest.fn()} />
);
const component = renderWithIntl(<UptimeDatePicker refreshApp={jest.fn()} />);
expect(component).toMatchSnapshot();
});

View file

@ -20,7 +20,7 @@ exports[`DataMissing component renders basePath and headingMessage 1`] = `
values={
Object {
"configureHeartbeatLink": <ForwardRef
href="foo/app/kibana#/home/tutorial/uptimeMonitors"
href="/app/kibana#/home/tutorial/uptimeMonitors"
target="_blank"
>
<FormattedMessage

View file

@ -2,7 +2,6 @@
exports[`EmptyState component does not render empty state with appropriate base path and no docs 1`] = `
<EmptyStateComponent
basePath="foo"
data={
Object {
"statesIndexStatus": Object {
@ -118,7 +117,6 @@ exports[`EmptyState component does not render empty state with appropriate base
loading={false}
>
<DataMissing
basePath="foo"
headingMessage="No uptime data found"
>
<EuiFlexGroup
@ -153,7 +151,7 @@ exports[`EmptyState component does not render empty state with appropriate base
values={
Object {
"configureHeartbeatLink": <ForwardRef
href="foo/app/kibana#/home/tutorial/uptimeMonitors"
href="/app/kibana#/home/tutorial/uptimeMonitors"
target="_blank"
>
<FormattedMessage
@ -249,7 +247,7 @@ exports[`EmptyState component does not render empty state with appropriate base
values={
Object {
"configureHeartbeatLink": <ForwardRef
href="foo/app/kibana#/home/tutorial/uptimeMonitors"
href="/app/kibana#/home/tutorial/uptimeMonitors"
target="_blank"
>
<FormattedMessage
@ -262,12 +260,12 @@ exports[`EmptyState component does not render empty state with appropriate base
}
>
<EuiLink
href="foo/app/kibana#/home/tutorial/uptimeMonitors"
href="/app/kibana#/home/tutorial/uptimeMonitors"
target="_blank"
>
<a
className="euiLink euiLink--primary"
href="foo/app/kibana#/home/tutorial/uptimeMonitors"
href="/app/kibana#/home/tutorial/uptimeMonitors"
rel="noopener noreferrer"
target="_blank"
>
@ -301,7 +299,6 @@ exports[`EmptyState component does not render empty state with appropriate base
exports[`EmptyState component doesn't render child components when count is falsey 1`] = `
<EmptyStateComponent
basePath=""
intl={
Object {
"defaultFormats": Object {},
@ -470,7 +467,6 @@ exports[`EmptyState component doesn't render child components when count is fals
exports[`EmptyState component notifies when index does not exist 1`] = `
<EmptyStateComponent
basePath="foo"
data={
Object {
"statesIndexStatus": Object {
@ -586,7 +582,6 @@ exports[`EmptyState component notifies when index does not exist 1`] = `
loading={false}
>
<DataMissing
basePath="foo"
headingMessage="Uptime index not found"
>
<EuiFlexGroup
@ -621,7 +616,7 @@ exports[`EmptyState component notifies when index does not exist 1`] = `
values={
Object {
"configureHeartbeatLink": <ForwardRef
href="foo/app/kibana#/home/tutorial/uptimeMonitors"
href="/app/kibana#/home/tutorial/uptimeMonitors"
target="_blank"
>
<FormattedMessage
@ -717,7 +712,7 @@ exports[`EmptyState component notifies when index does not exist 1`] = `
values={
Object {
"configureHeartbeatLink": <ForwardRef
href="foo/app/kibana#/home/tutorial/uptimeMonitors"
href="/app/kibana#/home/tutorial/uptimeMonitors"
target="_blank"
>
<FormattedMessage
@ -730,12 +725,12 @@ exports[`EmptyState component notifies when index does not exist 1`] = `
}
>
<EuiLink
href="foo/app/kibana#/home/tutorial/uptimeMonitors"
href="/app/kibana#/home/tutorial/uptimeMonitors"
target="_blank"
>
<a
className="euiLink euiLink--primary"
href="foo/app/kibana#/home/tutorial/uptimeMonitors"
href="/app/kibana#/home/tutorial/uptimeMonitors"
rel="noopener noreferrer"
target="_blank"
>
@ -783,7 +778,6 @@ exports[`EmptyState component renders child components when count is truthy 1`]
exports[`EmptyState component renders error message when an error occurs 1`] = `
<EmptyStateComponent
basePath=""
errors={
Array [
Object {
@ -1043,7 +1037,6 @@ exports[`EmptyState component renders error message when an error occurs 1`] = `
exports[`EmptyState component renders loading state if no errors or doc count 1`] = `
<EmptyStateComponent
basePath=""
intl={
Object {
"defaultFormats": Object {},

View file

@ -10,7 +10,7 @@ import { DataMissing } from '../data_missing';
describe('DataMissing component', () => {
it('renders basePath and headingMessage', () => {
const component = shallowWithIntl(<DataMissing basePath="foo" headingMessage="bar" />);
const component = shallowWithIntl(<DataMissing headingMessage="bar" />);
expect(component).toMatchSnapshot();
});
});

View file

@ -24,7 +24,7 @@ describe('EmptyState component', () => {
it('renders child components when count is truthy', () => {
const component = shallowWithIntl(
<EmptyStateComponent basePath="" data={{ statesIndexStatus }} loading={false}>
<EmptyStateComponent data={{ statesIndexStatus }} loading={false}>
<div>Foo</div>
<div>Bar</div>
<div>Baz</div>
@ -35,7 +35,7 @@ describe('EmptyState component', () => {
it(`doesn't render child components when count is falsey`, () => {
const component = mountWithIntl(
<EmptyStateComponent basePath="" data={undefined} loading={false}>
<EmptyStateComponent data={undefined} loading={false}>
<div>Shouldn&apos;t be rendered</div>
</EmptyStateComponent>
);
@ -57,7 +57,7 @@ describe('EmptyState component', () => {
},
];
const component = mountWithIntl(
<EmptyStateComponent basePath="" data={undefined} errors={errors} loading={false}>
<EmptyStateComponent data={undefined} errors={errors} loading={false}>
<div>Shouldn&apos;t appear...</div>
</EmptyStateComponent>
);
@ -66,7 +66,7 @@ describe('EmptyState component', () => {
it('renders loading state if no errors or doc count', () => {
const component = mountWithIntl(
<EmptyStateComponent basePath="" loading={true}>
<EmptyStateComponent loading={true}>
<div>Should appear even while loading...</div>
</EmptyStateComponent>
);
@ -81,7 +81,7 @@ describe('EmptyState component', () => {
indexExists: true,
};
const component = mountWithIntl(
<EmptyStateComponent basePath="foo" data={{ statesIndexStatus }} loading={false}>
<EmptyStateComponent data={{ statesIndexStatus }} loading={false}>
<div>If this is in the snapshot the test should fail</div>
</EmptyStateComponent>
);
@ -91,7 +91,7 @@ describe('EmptyState component', () => {
it('notifies when index does not exist', () => {
statesIndexStatus.indexExists = false;
const component = mountWithIntl(
<EmptyStateComponent basePath="foo" data={{ statesIndexStatus }} loading={false}>
<EmptyStateComponent data={{ statesIndexStatus }} loading={false}>
<div>This text should not render</div>
</EmptyStateComponent>
);

View file

@ -14,48 +14,51 @@ import {
EuiLink,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import React, { useContext } from 'react';
import { UptimeSettingsContext } from '../../../contexts';
interface DataMissingProps {
basePath: string;
headingMessage: string;
}
export const DataMissing = ({ basePath, headingMessage }: DataMissingProps) => (
<EuiFlexGroup justifyContent="center">
<EuiFlexItem grow={false}>
<EuiSpacer size="xs" />
<EuiPanel>
<EuiEmptyPrompt
iconType="uptimeApp"
title={
<EuiTitle size="l">
<h3>{headingMessage}</h3>
</EuiTitle>
}
body={
<p>
<FormattedMessage
id="xpack.uptime.emptyState.configureHeartbeatToGetStartedMessage"
defaultMessage="{configureHeartbeatLink} to start logging uptime data."
values={{
configureHeartbeatLink: (
<EuiLink
target="_blank"
href={`${basePath}/app/kibana#/home/tutorial/uptimeMonitors`}
>
<FormattedMessage
id="xpack.uptime.emptyState.configureHeartbeatLinkText"
defaultMessage="Configure Heartbeat"
/>
</EuiLink>
),
}}
/>
</p>
}
/>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
);
export const DataMissing = ({ headingMessage }: DataMissingProps) => {
const { basePath } = useContext(UptimeSettingsContext);
return (
<EuiFlexGroup justifyContent="center">
<EuiFlexItem grow={false}>
<EuiSpacer size="xs" />
<EuiPanel>
<EuiEmptyPrompt
iconType="uptimeApp"
title={
<EuiTitle size="l">
<h3>{headingMessage}</h3>
</EuiTitle>
}
body={
<p>
<FormattedMessage
id="xpack.uptime.emptyState.configureHeartbeatToGetStartedMessage"
defaultMessage="{configureHeartbeatLink} to start logging uptime data."
values={{
configureHeartbeatLink: (
<EuiLink
target="_blank"
href={`${basePath}/app/kibana#/home/tutorial/uptimeMonitors`}
>
<FormattedMessage
id="xpack.uptime.emptyState.configureHeartbeatLinkText"
defaultMessage="Configure Heartbeat"
/>
</EuiLink>
),
}}
/>
</p>
}
/>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -18,13 +18,12 @@ interface EmptyStateQueryResult {
}
interface EmptyStateProps {
basePath: string;
children: JSX.Element[] | JSX.Element;
}
type Props = UptimeGraphQLQueryProps<EmptyStateQueryResult> & EmptyStateProps;
export const EmptyStateComponent = ({ basePath, children, data, errors }: Props) => {
export const EmptyStateComponent = ({ children, data, errors }: Props) => {
if (errors) {
return <EmptyStateError errors={errors} />;
}
@ -33,7 +32,6 @@ export const EmptyStateComponent = ({ basePath, children, data, errors }: Props)
if (!indexExists) {
return (
<DataMissing
basePath={basePath}
headingMessage={i18n.translate('xpack.uptime.emptyState.noIndexTitle', {
defaultMessage: 'Uptime index not found',
})}
@ -42,7 +40,6 @@ export const EmptyStateComponent = ({ basePath, children, data, errors }: Props)
} else if (indexExists && docCount && docCount.count === 0) {
return (
<DataMissing
basePath={basePath}
headingMessage={i18n.translate('xpack.uptime.emptyState.noDataMessage', {
defaultMessage: 'No uptime data found',
})}

View file

@ -1,31 +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 { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
interface Props {
monitorId: string;
message?: string;
}
export const EmptyStatusBar = ({ message, monitorId }: Props) => (
<EuiPanel>
<EuiFlexGroup gutterSize="l">
<EuiFlexItem grow={false}>
{!message
? i18n.translate('xpack.uptime.emptyStatusBar.defaultMessage', {
defaultMessage: 'No data found for monitor id {monitorId}',
description:
'This is the default message we display in a status bar when there is no data available for an uptime monitor.',
values: { monitorId },
})
: message}
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);

View file

@ -6,14 +6,12 @@
export { DonutChart } from './charts/donut_chart';
export { EmptyState } from './empty_state';
export { EmptyStatusBar } from './empty_status_bar';
export { MonitorStatusBar } from './monitor_status_details';
export { FilterGroup } from './filter_group';
export { IntegrationLink } from './integration_link';
export { KueryBar } from './kuery_bar';
export { MonitorCharts } from './monitor_charts';
export { MonitorList } from './monitor_list';
export { MonitorPageTitle } from './monitor_page_title';
export { MonitorStatusBar } from './monitor_status_bar';
export { OverviewPageParsingErrorCallout } from './overview_page_parsing_error_callout';
export { PingList } from './ping_list';
export { Snapshot } from './snapshot';

View file

@ -7,6 +7,7 @@
import { getLayerList } from '../map_config';
import { mockLayerList } from './__mocks__/mock';
import { LocationPoint } from '../embedded_map';
import { UptimeAppColors } from '../../../../../uptime_app';
jest.mock('uuid', () => {
return {
@ -17,6 +18,7 @@ jest.mock('uuid', () => {
describe('map_config', () => {
let upPoints: LocationPoint[];
let downPoints: LocationPoint[];
let colors: Pick<UptimeAppColors, 'gray' | 'danger'>;
beforeEach(() => {
upPoints = [
@ -29,11 +31,15 @@ describe('map_config', () => {
{ lat: '55.487239', lon: '13.399262' },
{ lat: '54.487239', lon: '14.399262' },
];
colors = {
danger: '#BC261E',
gray: '#000',
};
});
describe('#getLayerList', () => {
test('it returns the low poly layer', () => {
const layerList = getLayerList(upPoints, downPoints, { danger: '#BC261E', gray: '#000' });
const layerList = getLayerList(upPoints, downPoints, colors);
expect(layerList).toStrictEqual(mockLayerList);
});
});

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useEffect, useState, useContext } from 'react';
import React, { useEffect, useState, useContext, useRef } from 'react';
import uuid from 'uuid';
import styled from 'styled-components';
@ -48,19 +48,31 @@ const EmbeddedPanel = styled.div`
export const EmbeddedMap = ({ upPoints, downPoints }: EmbeddedMapProps) => {
const { colors } = useContext(UptimeSettingsContext);
const [embeddable, setEmbeddable] = useState<MapEmbeddable>();
const embeddableRoot: React.RefObject<HTMLDivElement> = React.createRef();
const embeddableRoot: React.RefObject<HTMLDivElement> = useRef<HTMLDivElement>(null);
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 },
query: {
query: '',
language: 'kuery',
},
refreshConfig: {
value: 0,
pause: false,
},
viewMode: 'view',
isLayerTOCOpen: false,
hideFilterActions: true,
mapCenter: { lon: 11, lat: 20, zoom: 0 },
// Zoom Lat/Lon values are set to make sure map is in center in the panel
// It wil also omit Greenland/Antarctica etc
mapCenter: {
lon: 11,
lat: 20,
zoom: 0,
},
disableInteractive: true,
disableTooltipControl: true,
hideToolbarOverlay: true,
@ -80,16 +92,19 @@ export const EmbeddedMap = ({ upPoints, downPoints }: EmbeddedMapProps) => {
setEmbeddable(embeddableObject);
}
setupEmbeddable();
// we want this effect to execute exactly once after the component mounts
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// update map layers based on points
useEffect(() => {
if (embeddable) {
embeddable.setLayerList(getLayerList(upPoints, downPoints, colors));
}
}, [upPoints, downPoints, embeddable, colors]);
// We can only render after embeddable has already initialized
useEffect(() => {
if (embeddableRoot.current && embeddable) {
embeddable.render(embeddableRoot.current);

View file

@ -6,6 +6,7 @@
import lowPolyLayerFeatures from './low_poly_layer.json';
import { LocationPoint } from './embedded_map';
import { UptimeAppColors } from '../../../../uptime_app';
/**
* Returns `Source/Destination Point-to-point` Map LayerList configuration, with a source,
@ -15,7 +16,7 @@ import { LocationPoint } from './embedded_map';
export const getLayerList = (
upPoints: LocationPoint[],
downPoints: LocationPoint[],
{ gray, danger }: { gray: string; danger: string }
{ gray, danger }: Pick<UptimeAppColors, 'gray' | 'danger'>
) => {
return [getLowPolyLayer(), getDownPointsLayer(downPoints, danger), getUpPointsLayer(upPoints)];
};

View file

@ -11,10 +11,12 @@ import { LocationStatusTags } from './location_status_tags';
import { EmbeddedMap, LocationPoint } from './embeddables/embedded_map';
import { MonitorLocations } from '../../../../common/runtime_types';
// 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: 10px;
margin-right: 20px;
`;
interface LocationMapProps {

View file

@ -1,38 +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 { EuiTextColor, EuiTitle } from '@elastic/eui';
import { EuiLoadingSpinner } from '@elastic/eui';
import React from 'react';
import { MonitorPageTitle as TitleType } from '../../../common/graphql/types';
import { UptimeGraphQLQueryProps, withUptimeGraphQL } from '../higher_order';
import { monitorPageTitleQuery } from '../../queries';
interface MonitorPageTitleQueryResult {
monitorPageTitle?: TitleType;
}
interface MonitorPageTitleProps {
monitorId: string;
}
type Props = MonitorPageTitleProps & UptimeGraphQLQueryProps<MonitorPageTitleQueryResult>;
export const MonitorPageTitleComponent = ({ data }: Props) =>
data && data.monitorPageTitle ? (
<EuiTitle size="xxs">
<EuiTextColor color="subdued">
<h1 data-test-subj="monitor-page-title">{data.monitorPageTitle.id}</h1>
</EuiTextColor>
</EuiTitle>
) : (
<EuiLoadingSpinner size="xl" />
);
export const MonitorPageTitle = withUptimeGraphQL<
MonitorPageTitleQueryResult,
MonitorPageTitleProps
>(MonitorPageTitleComponent, monitorPageTitleQuery);

View file

@ -1,80 +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 { EuiFlexGroup, EuiFlexItem, EuiHealth, EuiLink } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { get } from 'lodash';
import moment from 'moment';
import React from 'react';
import { Ping } from '../../../../common/graphql/types';
import { UptimeGraphQLQueryProps, withUptimeGraphQL } from '../../higher_order';
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[];
}
interface MonitorStatusBarProps {
monitorId: string;
}
type Props = MonitorStatusBarProps & UptimeGraphQLQueryProps<MonitorStatusBarQueryResult>;
export const MonitorStatusBarComponent = ({ data, monitorId }: Props) => {
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 (
<>
<EuiFlexGroup gutterSize="l" wrap>
<EuiFlexItem grow={false}>
<EuiHealth
aria-label={labels.healthStatusMessageAriaLabel}
color={status === 'up' ? 'success' : 'danger'}
style={{ lineHeight: 'inherit' }}
>
{status === 'up' ? labels.upLabel : labels.downLabel}
</EuiHealth>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexItem grow={false}>
<EuiLink aria-label={labels.monitorUrlLinkAriaLabel} href={full} target="_blank">
{full}
</EuiLink>
</EuiFlexItem>
</EuiFlexItem>
{!!duration && (
<EuiFlexItem aria-label={labels.durationTextAriaLabel} grow={false}>
<FormattedMessage
id="xpack.uptime.monitorStatusBar.healthStatus.durationInMillisecondsMessage"
values={{ duration: convertMicrosecondsToMilliseconds(duration) }}
defaultMessage="{duration}ms"
description="The 'ms' is an abbreviation for 'milliseconds'."
/>
</EuiFlexItem>
)}
<EuiFlexItem aria-label={labels.timestampFromNowTextAriaLabel} grow={true}>
{moment(new Date(timestamp).valueOf()).fromNow()}
</EuiFlexItem>
</EuiFlexGroup>
<MonitorSSLCertificate tls={tls} />
</>
);
}
return <EmptyStatusBar message={labels.loadingMessage} monitorId={monitorId} />;
};
export const MonitorStatusBar = withUptimeGraphQL<
MonitorStatusBarQueryResult,
MonitorStatusBarProps
>(MonitorStatusBarComponent, monitorStatusBarQuery);

View file

@ -0,0 +1,51 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`StatusByLocation component renders all locations are down 1`] = `
<div
class="euiText euiText--medium"
>
<h2>
Down in 2 Locations
</h2>
</div>
`;
exports[`StatusByLocation component renders when down in some locations 1`] = `
<div
class="euiText euiText--medium"
>
<h2>
Down in 1/2 Locations
</h2>
</div>
`;
exports[`StatusByLocation component renders when only one location and it is down 1`] = `
<div
class="euiText euiText--medium"
>
<h2>
Down in 1 Location
</h2>
</div>
`;
exports[`StatusByLocation component renders when only one location and it is up 1`] = `
<div
class="euiText euiText--medium"
>
<h2>
Up in 1 Location
</h2>
</div>
`;
exports[`StatusByLocation component renders when up in all locations 1`] = `
<div
class="euiText euiText--medium"
>
<h2>
Up in 2 Locations
</h2>
</div>
`;

View file

@ -0,0 +1,81 @@
/*
* 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 } from 'test_utils/enzyme_helpers';
import { MonitorLocation } from '../../../../../common/runtime_types';
import { StatusByLocations } from '../';
describe('StatusByLocation component', () => {
let monitorLocations: MonitorLocation[];
it('renders when up in all locations', () => {
monitorLocations = [
{
summary: { up: 4, down: 0 },
geo: { name: 'Berlin', location: { lat: '52.487448', lon: ' 13.394798' } },
},
{
summary: { up: 4, down: 0 },
geo: { name: 'st-paul', location: { lat: '52.487448', lon: ' 13.394798' } },
},
];
const component = renderWithIntl(<StatusByLocations locations={monitorLocations} />);
expect(component).toMatchSnapshot();
});
it('renders when only one location and it is up', () => {
monitorLocations = [
{
summary: { up: 4, down: 0 },
geo: { name: 'Berlin', location: { lat: '52.487448', lon: ' 13.394798' } },
},
];
const component = renderWithIntl(<StatusByLocations locations={monitorLocations} />);
expect(component).toMatchSnapshot();
});
it('renders when only one location and it is down', () => {
monitorLocations = [
{
summary: { up: 0, down: 4 },
geo: { name: 'Berlin', location: { lat: '52.487448', lon: ' 13.394798' } },
},
];
const component = renderWithIntl(<StatusByLocations locations={monitorLocations} />);
expect(component).toMatchSnapshot();
});
it('renders all locations are down', () => {
monitorLocations = [
{
summary: { up: 0, down: 4 },
geo: { name: 'Berlin', location: { lat: '52.487448', lon: ' 13.394798' } },
},
{
summary: { up: 0, down: 4 },
geo: { name: 'st-paul', location: { lat: '52.487448', lon: ' 13.394798' } },
},
];
const component = renderWithIntl(<StatusByLocations locations={monitorLocations} />);
expect(component).toMatchSnapshot();
});
it('renders when down in some locations', () => {
monitorLocations = [
{
summary: { up: 0, down: 4 },
geo: { name: 'Berlin', location: { lat: '52.487448', lon: ' 13.394798' } },
},
{
summary: { up: 4, down: 0 },
geo: { name: 'st-paul', location: { lat: '52.487448', lon: ' 13.394798' } },
},
];
const component = renderWithIntl(<StatusByLocations locations={monitorLocations} />);
expect(component).toMatchSnapshot();
});
});

View file

@ -5,12 +5,12 @@
*/
import { connect } from 'react-redux';
import { AppState } from '../../../state';
import { getMonitorLocations } from '../../../state/selectors';
import { selectMonitorLocations } 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),
monitorLocations: selectMonitorLocations(state, monitorId),
});
const mapDispatchToProps = (dispatch: any, ownProps: any) => ({
@ -32,3 +32,5 @@ export const MonitorStatusDetails = connect(
)(MonitorStatusDetailsComponent);
export * from './monitor_status_details';
export { MonitorStatusBar } from './monitor_status_bar';
export { StatusByLocations } from './monitor_status_bar/status_by_location';

View file

@ -0,0 +1,50 @@
/*
* 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 { Dispatch } from 'redux';
import {
StateProps,
DispatchProps,
MonitorStatusBarComponent,
MonitorStatusBarProps,
} from './monitor_status_bar';
import { selectMonitorStatus, selectMonitorLocations } from '../../../../state/selectors';
import { AppState } from '../../../../state';
import { getMonitorStatus, getSelectedMonitor } from '../../../../state/actions';
const mapStateToProps = (state: AppState, ownProps: MonitorStatusBarProps) => ({
monitorStatus: selectMonitorStatus(state),
monitorLocations: selectMonitorLocations(state, ownProps.monitorId),
});
const mapDispatchToProps = (dispatch: Dispatch<any>, ownProps: MonitorStatusBarProps) => ({
loadMonitorStatus: () => {
const { dateStart, dateEnd, monitorId } = ownProps;
dispatch(
getMonitorStatus({
monitorId,
dateStart,
dateEnd,
})
);
dispatch(
getSelectedMonitor({
monitorId,
})
);
},
});
// @ts-ignore TODO: Investigate typescript issues here
export const MonitorStatusBar = connect<StateProps, DispatchProps, MonitorStatusBarProps>(
// @ts-ignore TODO: Investigate typescript issues here
mapStateToProps,
mapDispatchToProps
)(MonitorStatusBarComponent);
export { MonitorSSLCertificate } from './monitor_ssl_certificate';
export * from './monitor_status_bar';

View file

@ -10,9 +10,8 @@ import moment from 'moment';
import { EuiSpacer, EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import styled from 'styled-components';
import { PingTls } from '../../../../common/graphql/types';
import { PingTls } from '../../../../../common/graphql/types';
interface Props {
/**
@ -21,10 +20,6 @@ interface Props {
tls: PingTls | null | undefined;
}
const TextContainer = styled.div`
margin-left: 20px;
`;
export const MonitorSSLCertificate = ({ tls }: Props) => {
const certificateValidity: string | undefined = get(
tls,
@ -37,27 +32,22 @@ export const MonitorSSLCertificate = ({ tls }: Props) => {
return validExpiryDate && certificateValidity ? (
<>
<EuiSpacer size="s" />
<TextContainer>
<EuiText
color="subdued"
grow={false}
size="s"
aria-label={i18n.translate(
'xpack.uptime.monitorStatusBar.sslCertificateExpiry.ariaLabel',
{
defaultMessage: 'SSL certificate expires',
}
)}
>
<FormattedMessage
id="xpack.uptime.monitorStatusBar.sslCertificateExpiry.content"
defaultMessage="SSL certificate expires {certificateValidity}"
values={{
certificateValidity: moment(new Date(certificateValidity).valueOf()).fromNow(),
}}
/>
</EuiText>
</TextContainer>
<EuiText
color="subdued"
grow={false}
size="s"
aria-label={i18n.translate('xpack.uptime.monitorStatusBar.sslCertificateExpiry.ariaLabel', {
defaultMessage: 'SSL certificate expires',
})}
>
<FormattedMessage
id="xpack.uptime.monitorStatusBar.sslCertificateExpiry.content"
defaultMessage="SSL certificate expires {certificateValidity}"
values={{
certificateValidity: moment(new Date(certificateValidity).valueOf()).fromNow(),
}}
/>
</EuiText>
</>
) : null;
};

View file

@ -0,0 +1,79 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
EuiLink,
EuiTitle,
EuiTextColor,
EuiSpacer,
EuiText,
EuiFlexGroup,
EuiFlexItem,
} from '@elastic/eui';
import React, { useEffect } from 'react';
import { MonitorSSLCertificate } from './monitor_ssl_certificate';
import * as labels from './translations';
import { StatusByLocations } from './status_by_location';
import { Ping } from '../../../../../common/graphql/types';
import { MonitorLocations } from '../../../../../common/runtime_types';
export interface StateProps {
monitorStatus: Ping;
monitorLocations: MonitorLocations;
}
export interface DispatchProps {
loadMonitorStatus: () => void;
}
export interface MonitorStatusBarProps {
monitorId: string;
dateStart: string;
dateEnd: string;
}
type Props = MonitorStatusBarProps & StateProps & DispatchProps;
export const MonitorStatusBarComponent: React.FC<Props> = ({
dateStart,
dateEnd,
monitorId,
loadMonitorStatus,
monitorStatus,
monitorLocations,
}) => {
useEffect(() => {
loadMonitorStatus();
}, [dateStart, dateEnd, loadMonitorStatus]);
const full = monitorStatus?.url?.full ?? '';
return (
<EuiFlexGroup direction="column" gutterSize="none">
<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

@ -0,0 +1,70 @@
/*
* 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 { EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { MonitorLocation } from '../../../../../common/runtime_types';
interface StatusByLocationsProps {
locations: MonitorLocation[];
}
export const StatusByLocations = ({ locations }: StatusByLocationsProps) => {
const upLocations: string[] = [];
const downLocations: string[] = [];
if (locations)
locations.forEach((item: any) => {
if (item.summary.down === 0) {
upLocations.push(item.geo.name);
} else {
downLocations.push(item.geo.name);
}
});
let statusMessage = '';
let status = '';
if (downLocations.length === 0) {
// for Messaging like 'Up in 1 Location' or 'Up in 2 Locations'
statusMessage = `${locations.length}`;
status = 'Up';
} else if (downLocations.length > 0) {
// for Messaging like 'Down in 1/2 Locations'
status = 'Down';
statusMessage = `${downLocations.length}/${locations.length}`;
if (downLocations.length === locations.length) {
// for Messaging like 'Down in 2 Locations'
statusMessage = `${locations.length}`;
}
}
return (
<EuiText>
<h2>
{locations.length <= 1 ? (
<FormattedMessage
id="xpack.uptime.monitorStatusBar.locations.oneLocStatus"
values={{
status,
loc: statusMessage,
}}
defaultMessage="{status} in {loc} Location"
/>
) : (
<FormattedMessage
id="xpack.uptime.monitorStatusBar.locations.upStatus"
values={{
status,
loc: statusMessage,
}}
defaultMessage="{status} in {loc} Locations"
/>
)}
</h2>
</EuiText>
);
};

View file

@ -7,7 +7,7 @@
import React, { useEffect } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
import { LocationMap } from '../location_map';
import { MonitorStatusBar } from '../monitor_status_bar';
import { MonitorStatusBar } from './monitor_status_bar';
interface MonitorStatusBarProps {
monitorId: string;
@ -34,7 +34,12 @@ export const MonitorStatusDetailsComponent = ({
<EuiPanel>
<EuiFlexGroup gutterSize="l" wrap>
<EuiFlexItem grow={true}>
<MonitorStatusBar monitorId={monitorId} variables={variables} />
<MonitorStatusBar
monitorId={monitorId}
variables={variables}
dateStart={dateStart}
dateEnd={dateEnd}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<LocationMap monitorLocations={monitorLocations} />

View file

@ -43,7 +43,7 @@ interface StoreProps {
/**
* Contains functions that will dispatch actions used
* for this component's lifecyclel
* for this component's life cycle
*/
interface DispatchProps {
loadSnapshotCount: typeof fetchSnapshotCount;

View file

@ -5,9 +5,10 @@
*/
import { EuiSuperDatePicker } from '@elastic/eui';
import React from 'react';
import React, { useContext } from 'react';
import { useUrlParams } from '../../hooks';
import { CLIENT_DEFAULTS } from '../../../common/constants';
import { UptimeSettingsContext } from '../../contexts';
// TODO: when EUI exports types for this, this should be replaced
interface SuperDateRangePickerRangeChangedEvent {
@ -26,16 +27,14 @@ export interface CommonlyUsedRange {
display: string;
}
interface Props {
interface UptimeDatePickerProps {
refreshApp: () => void;
commonlyUsedRanges?: CommonlyUsedRange[];
}
type UptimeDatePickerProps = Props;
export const UptimeDatePicker = ({ refreshApp, commonlyUsedRanges }: UptimeDatePickerProps) => {
export const UptimeDatePicker = ({ refreshApp }: UptimeDatePickerProps) => {
const [getUrlParams, updateUrl] = useUrlParams();
const { autorefreshInterval, autorefreshIsPaused, dateRangeStart, dateRangeEnd } = getUrlParams();
const { commonlyUsedRanges } = useContext(UptimeSettingsContext);
const euiCommonlyUsedRanges = commonlyUsedRanges
? commonlyUsedRanges.map(

View file

@ -9,6 +9,7 @@ import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import { createContext } from 'react';
import { UptimeAppColors } from '../uptime_app';
import { CONTEXT_DEFAULTS } from '../../common/constants';
import { CommonlyUsedRange } from '../components/functional/uptime_date_picker';
export interface UMSettingsContextValues {
absoluteStartDate: number;
@ -23,7 +24,7 @@ export interface UMSettingsContextValues {
isInfraAvailable: boolean;
isLogsAvailable: boolean;
refreshApp: () => void;
setHeadingText: (text: string) => void;
commonlyUsedRanges?: CommonlyUsedRange[];
}
const {
@ -64,9 +65,6 @@ const defaultContext: UMSettingsContextValues = {
refreshApp: () => {
throw new Error('App refresh was not initialized, set it when you invoke the context');
},
setHeadingText: () => {
throw new Error('setHeadingText was not initialized on UMSettingsContext.');
},
};
export const UptimeSettingsContext = createContext(defaultContext);

View file

@ -7,3 +7,4 @@
export { MonitorPage } from './monitor';
export { OverviewPage } from './overview';
export { NotFoundPage } from './not_found';
export { PageHeader } from './page_header';

View file

@ -5,60 +5,31 @@
*/
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, PingList } from '../components/functional';
import React, { Fragment, useContext, useState } from 'react';
import { useParams } from 'react-router-dom';
import { MonitorCharts, PingList } from '../components/functional';
import { UMUpdateBreadcrumbs } from '../lib/lib';
import { UptimeSettingsContext } from '../contexts';
import { useUptimeTelemetry, useUrlParams, UptimePage } 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';
import { PageHeader } from './page_header';
interface MonitorPageProps {
match: { params: { monitorId: string } };
// this is the query function provided by Apollo's Client API
query: <T, TVariables = OperationVariables>(
options: QueryOptions<TVariables>
) => Promise<ApolloQueryResult<T>>;
setBreadcrumbs: UMUpdateBreadcrumbs;
}
export const MonitorPage = ({ query, setBreadcrumbs, match }: MonitorPageProps) => {
export const MonitorPage = ({ setBreadcrumbs }: MonitorPageProps) => {
// decode 64 base string, it was decoded to make it a valid url, since monitor id can be a url
const monitorId = atob(match.params.monitorId);
let { monitorId } = useParams();
monitorId = atob(monitorId || '');
const [pingListPageCount, setPingListPageCount] = useState<number>(10);
const { colors, refreshApp, setHeadingText } = useContext(UptimeSettingsContext);
const { colors, refreshApp } = useContext(UptimeSettingsContext);
const [getUrlParams, updateUrlParams] = useUrlParams();
const { absoluteDateRangeStart, absoluteDateRangeEnd, ...params } = getUrlParams();
const { dateRangeStart, dateRangeEnd, selectedPingStatus } = params;
useEffect(() => {
query({
query: gql`
query MonitorPageTitle($monitorId: String!) {
monitorPageTitle: getMonitorPageTitle(monitorId: $monitorId) {
id
url
name
}
}
`,
variables: { monitorId },
}).then((result: any) => {
const { name, url, id } = result.data.monitorPageTitle;
const heading: string = name || url || id;
document.title = getTitle(name);
setBreadcrumbs(getMonitorPageBreadcrumb(heading, stringifyUrlParams(params)));
if (setHeadingText) {
setHeadingText(heading);
}
});
}, [monitorId, params, query, setBreadcrumbs, setHeadingText]);
const [selectedLocation, setSelectedLocation] = useState(undefined);
const sharedVariables = {
@ -75,7 +46,7 @@ export const MonitorPage = ({ query, setBreadcrumbs, match }: MonitorPageProps)
return (
<Fragment>
<MonitorPageTitle monitorId={monitorId} variables={{ monitorId }} />
<PageHeader setBreadcrumbs={setBreadcrumbs} />
<EuiSpacer size="s" />
<MonitorStatusDetails
monitorId={monitorId}

View file

@ -5,10 +5,8 @@
*/
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { Fragment, useContext, useEffect, useState } from 'react';
import React, { Fragment, useContext, useState } from 'react';
import styled from 'styled-components';
import { getOverviewPageBreadcrumbs } from '../breadcrumbs';
import {
EmptyState,
FilterGroup,
@ -24,15 +22,10 @@ import { stringifyUrlParams } from '../lib/helper/stringify_url_params';
import { useTrackPageview } from '../../../infra/public';
import { combineFiltersAndUserSearch, stringifyKueries, toStaticIndexPattern } from '../lib/helper';
import { AutocompleteProviderRegister, esKuery } from '../../../../../../src/plugins/data/public';
import { PageHeader } from './page_header';
interface OverviewPageProps {
basePath: string;
autocomplete: Pick<AutocompleteProviderRegister, 'getProvider'>;
history: any;
location: {
pathname: string;
search: string;
};
setBreadcrumbs: UMUpdateBreadcrumbs;
}
@ -52,8 +45,8 @@ const EuiFlexItemStyled = styled(EuiFlexItem)`
}
`;
export const OverviewPage = ({ basePath, autocomplete, setBreadcrumbs }: Props) => {
const { colors, setHeadingText } = useContext(UptimeSettingsContext);
export const OverviewPage = ({ autocomplete, setBreadcrumbs }: Props) => {
const { colors } = useContext(UptimeSettingsContext);
const [getUrlParams, updateUrl] = useUrlParams();
const { absoluteDateRangeStart, absoluteDateRangeEnd, ...params } = getUrlParams();
const {
@ -68,18 +61,6 @@ export const OverviewPage = ({ basePath, autocomplete, setBreadcrumbs }: Props)
useUptimeTelemetry(UptimePage.Overview);
useIndexPattern(setIndexPattern);
useEffect(() => {
setBreadcrumbs(getOverviewPageBreadcrumbs());
if (setHeadingText) {
setHeadingText(
i18n.translate('xpack.uptime.overviewPage.headerText', {
defaultMessage: 'Overview',
description: `The text that will be displayed in the app's heading when the Overview page loads.`,
})
);
}
}, [basePath, setBreadcrumbs, setHeadingText]);
useTrackPageview({ app: 'uptime', path: 'overview' });
useTrackPageview({ app: 'uptime', path: 'overview', delay: 15000 });
@ -121,7 +102,8 @@ export const OverviewPage = ({ basePath, autocomplete, setBreadcrumbs }: Props)
return (
<Fragment>
<EmptyState basePath={basePath} implementsCustomErrorState={true} variables={{}}>
<PageHeader setBreadcrumbs={setBreadcrumbs} />
<EmptyState implementsCustomErrorState={true} variables={{}}>
<EuiFlexGroup gutterSize="xs" wrap responsive>
<EuiFlexItem grow={1} style={{ flexBasis: 500 }}>
<KueryBar autocomplete={autocomplete} />

View file

@ -0,0 +1,85 @@
/*
* 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 { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import React, { useEffect, useState, useContext } from 'react';
import { connect } from 'react-redux';
import { useRouteMatch, useParams } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
import { UptimeDatePicker } from '../components/functional/uptime_date_picker';
import { AppState } from '../state';
import { selectSelectedMonitor } from '../state/selectors';
import { getMonitorPageBreadcrumb, getOverviewPageBreadcrumbs } from '../breadcrumbs';
import { stringifyUrlParams } from '../lib/helper/stringify_url_params';
import { UptimeSettingsContext } from '../contexts';
import { getTitle } from '../lib/helper/get_title';
import { UMUpdateBreadcrumbs } from '../lib/lib';
import { MONITOR_ROUTE } from '../routes';
interface PageHeaderProps {
monitorStatus?: any;
setBreadcrumbs: UMUpdateBreadcrumbs;
}
export const PageHeaderComponent = ({ monitorStatus, setBreadcrumbs }: PageHeaderProps) => {
const monitorPage = useRouteMatch({
path: MONITOR_ROUTE,
});
const { refreshApp } = useContext(UptimeSettingsContext);
const { absoluteDateRangeStart, absoluteDateRangeEnd, ...params } = useParams();
const headingText = i18n.translate('xpack.uptime.overviewPage.headerText', {
defaultMessage: 'Overview',
description: `The text that will be displayed in the app's heading when the Overview page loads.`,
});
const [headerText, setHeaderText] = useState(headingText);
useEffect(() => {
if (monitorPage) {
setHeaderText(monitorStatus?.url?.full);
if (monitorStatus?.monitor) {
const { name, id } = monitorStatus.monitor;
document.title = getTitle(name || id);
}
} else {
document.title = getTitle();
}
}, [monitorStatus, monitorPage, setHeaderText]);
useEffect(() => {
if (monitorPage) {
if (headerText) {
setBreadcrumbs(getMonitorPageBreadcrumb(headerText, stringifyUrlParams(params)));
}
} else {
setBreadcrumbs(getOverviewPageBreadcrumbs());
}
}, [headerText, setBreadcrumbs, params, monitorPage]);
return (
<>
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween" gutterSize="s">
<EuiFlexItem>
<EuiTitle>
<h1>{headerText}</h1>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<UptimeDatePicker refreshApp={refreshApp} />
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
</>
);
};
const mapStateToProps = (state: AppState) => ({
monitorStatus: selectSelectedMonitor(state),
});
export const PageHeader = connect(mapStateToProps, null)(PageHeaderComponent);

View file

@ -7,6 +7,4 @@
export { docCountQuery, docCountQueryString } from './doc_count_query';
export { filterBarQuery, filterBarQueryString } from './filter_bar_query';
export { monitorChartsQuery, monitorChartsQueryString } from './monitor_charts_query';
export { monitorPageTitleQuery } from './monitor_page_title_query';
export { monitorStatusBarQuery, monitorStatusBarQueryString } from './monitor_status_bar_query';
export { pingsQuery, pingsQueryString } from './pings_query';

View file

@ -1,20 +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 gql from 'graphql-tag';
export const monitorPageTitleQueryString = `
query MonitorPageTitle($monitorId: String!) {
monitorPageTitle: getMonitorPageTitle(monitorId: $monitorId) {
id
url
name
}
}`;
export const monitorPageTitleQuery = gql`
${monitorPageTitleQueryString}
`;

View file

@ -1,41 +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 gql from 'graphql-tag';
export const monitorStatusBarQueryString = `
query MonitorStatus($dateRangeStart: String!, $dateRangeEnd: String!, $monitorId: String, $location: String) {
monitorStatus: getLatestMonitors(
dateRangeStart: $dateRangeStart
dateRangeEnd: $dateRangeEnd
monitorId: $monitorId
location: $location
) {
timestamp
monitor {
status
duration {
us
}
}
observer {
geo {
name
}
}
tls {
certificate_not_valid_after
}
url {
full
}
}
}
`;
export const monitorStatusBarQuery = gql`
${monitorStatusBarQueryString}
`;

View file

@ -0,0 +1,32 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FC } from 'react';
import { Route, Switch } from 'react-router-dom';
import { MonitorPage, OverviewPage, NotFoundPage } from './pages';
import { AutocompleteProviderRegister } from '../../../../../src/plugins/data/public';
import { UMUpdateBreadcrumbs } from './lib/lib';
export const MONITOR_ROUTE = '/monitor/:monitorId/:location?';
export const OVERVIEW_ROUTE = '/';
interface RouterProps {
autocomplete: Pick<AutocompleteProviderRegister, 'getProvider'>;
basePath: string;
setBreadcrumbs: UMUpdateBreadcrumbs;
}
export const PageRouter: FC<RouterProps> = ({ autocomplete, basePath, setBreadcrumbs }) => (
<Switch>
<Route path={MONITOR_ROUTE}>
<MonitorPage setBreadcrumbs={setBreadcrumbs} />
</Route>
<Route path={OVERVIEW_ROUTE}>
<OverviewPage autocomplete={autocomplete} setBreadcrumbs={setBreadcrumbs} />
</Route>
<Route component={NotFoundPage} />
</Switch>
);

View file

@ -6,3 +6,4 @@
export * from './snapshot';
export * from './ui';
export * from './monitor_status';

View file

@ -0,0 +1,15 @@
/*
* 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 { createAction } from 'redux-actions';
import { QueryParams } from './types';
export const getSelectedMonitor = createAction<{ monitorId: string }>('GET_SELECTED_MONITOR');
export const getSelectedMonitorSuccess = createAction<QueryParams>('GET_SELECTED_MONITOR_SUCCESS');
export const getSelectedMonitorFail = createAction<QueryParams>('GET_SELECTED_MONITOR_FAIL');
export const getMonitorStatus = createAction<QueryParams>('GET_MONITOR_STATUS');
export const getMonitorStatusSuccess = createAction<QueryParams>('GET_MONITOR_STATUS_SUCCESS');
export const getMonitorStatusFail = createAction<QueryParams>('GET_MONITOR_STATUS_FAIL');

View file

@ -5,10 +5,12 @@
*/
export interface QueryParams {
monitorId: string;
dateStart: string;
dateEnd: string;
filters?: string;
statusFilter?: string;
location?: string;
}
export interface MonitorDetailsActionPayload {

View file

@ -6,3 +6,4 @@
export * from './monitor';
export * from './snapshot';
export * from './monitor_status';

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 { getApiPath } from '../../lib/helper';
import { QueryParams } from '../actions/types';
import { Ping } from '../../../common/graphql/types';
export interface APIParams {
basePath: string;
monitorId: string;
}
export const fetchSelectedMonitor = async ({ basePath, monitorId }: APIParams): Promise<Ping> => {
const url = getApiPath(`/api/uptime/monitor/selected`, basePath);
const params = {
monitorId,
};
const urlParams = new URLSearchParams(params).toString();
const response = await fetch(`${url}?${urlParams}`);
if (!response.ok) {
throw new Error(response.statusText);
}
const responseData = await response.json();
return responseData;
};
export const fetchMonitorStatus = async ({
basePath,
monitorId,
dateStart,
dateEnd,
}: QueryParams & APIParams): Promise<Ping> => {
const url = getApiPath(`/api/uptime/monitor/status`, basePath);
const params = {
monitorId,
dateStart,
dateEnd,
};
const urlParams = new URLSearchParams(params).toString();
const response = await fetch(`${url}?${urlParams}`);
if (!response.ok) {
throw new Error(response.statusText);
}
const responseData = await response.json();
return responseData;
};

View file

@ -7,8 +7,10 @@
import { fork } from 'redux-saga/effects';
import { fetchMonitorDetailsEffect } from './monitor';
import { fetchSnapshotCountSaga } from './snapshot';
import { fetchMonitorStatusEffect } from './monitor_status';
export function* rootEffect() {
yield fork(fetchMonitorDetailsEffect);
yield fork(fetchSnapshotCountSaga);
yield fork(fetchMonitorStatusEffect);
}

View file

@ -0,0 +1,53 @@
/*
* 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 { call, put, takeLatest, select } from 'redux-saga/effects';
import { Action } from 'redux-actions';
import {
getSelectedMonitor,
getSelectedMonitorSuccess,
getSelectedMonitorFail,
getMonitorStatus,
getMonitorStatusSuccess,
getMonitorStatusFail,
} from '../actions/monitor_status';
import { fetchSelectedMonitor, fetchMonitorStatus } from '../api';
import { getBasePath } from '../selectors';
function* selectedMonitorEffect(action: Action<any>) {
const { monitorId } = action.payload;
try {
const basePath = yield select(getBasePath);
const response = yield call(fetchSelectedMonitor, {
monitorId,
basePath,
});
yield put({ type: getSelectedMonitorSuccess, payload: response });
} catch (error) {
yield put({ type: getSelectedMonitorFail, payload: error.message });
}
}
function* monitorStatusEffect(action: Action<any>) {
const { monitorId, dateStart, dateEnd } = action.payload;
try {
const basePath = yield select(getBasePath);
const response = yield call(fetchMonitorStatus, {
monitorId,
basePath,
dateStart,
dateEnd,
});
yield put({ type: getMonitorStatusSuccess, payload: response });
} catch (error) {
yield put({ type: getMonitorStatusFail, payload: error.message });
}
}
export function* fetchMonitorStatusEffect() {
yield takeLatest(getMonitorStatus, monitorStatusEffect);
yield takeLatest(getSelectedMonitor, selectedMonitorEffect);
}

View file

@ -8,10 +8,11 @@ import { combineReducers } from 'redux';
import { monitorReducer } from './monitor';
import { snapshotReducer } from './snapshot';
import { uiReducer } from './ui';
import { monitorStatusReducer } from './monitor_status';
export const rootReducer = combineReducers({
monitor: monitorReducer,
snapshot: snapshotReducer,
// @ts-ignore for now TODO: refactor to use redux-action
ui: uiReducer,
monitorStatus: monitorStatusReducer,
});

View file

@ -19,10 +19,10 @@ import { MonitorLocations } from '../../../common/runtime_types';
type MonitorLocationsList = Map<string, MonitorLocations>;
export interface MonitorState {
monitorDetailsList: MonitorDetailsState[];
monitorLocationsList: MonitorLocationsList;
loading: boolean;
errors: any[];
monitorDetailsList: MonitorDetailsState[];
monitorLocationsList: MonitorLocationsList;
}
const initialState: MonitorState = {

View file

@ -0,0 +1,67 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { handleActions, Action } from 'redux-actions';
import {
getSelectedMonitor,
getSelectedMonitorSuccess,
getSelectedMonitorFail,
getMonitorStatus,
getMonitorStatusSuccess,
getMonitorStatusFail,
} from '../actions';
import { Ping } from '../../../common/graphql/types';
import { QueryParams } from '../actions/types';
export interface MonitorStatusState {
status: Ping | null;
monitor: Ping | null;
loading: boolean;
}
const initialState: MonitorStatusState = {
status: null,
monitor: null,
loading: false,
};
type MonitorStatusPayload = QueryParams & Ping;
export const monitorStatusReducer = handleActions<MonitorStatusState, MonitorStatusPayload>(
{
[String(getSelectedMonitor)]: (state, action: Action<QueryParams>) => ({
...state,
loading: true,
}),
[String(getSelectedMonitorSuccess)]: (state, action: Action<Ping>) => ({
...state,
loading: false,
monitor: { ...action.payload } as Ping,
}),
[String(getSelectedMonitorFail)]: (state, action: Action<any>) => ({
...state,
loading: false,
}),
[String(getMonitorStatus)]: (state, action: Action<QueryParams>) => ({
...state,
loading: true,
}),
[String(getMonitorStatusSuccess)]: (state, action: Action<Ping>) => ({
...state,
loading: false,
status: { ...action.payload } as Ping,
}),
[String(getMonitorStatusFail)]: (state, action: Action<any>) => ({
...state,
loading: false,
}),
},
initialState
);

View file

@ -24,10 +24,11 @@ describe('state selectors', () => {
errors: [],
loading: false,
},
ui: {
basePath: 'yyz',
integrationsPopoverOpen: null,
lastRefresh: 125,
ui: { basePath: 'yyz', integrationsPopoverOpen: null, lastRefresh: 125 },
monitorStatus: {
status: null,
monitor: null,
loading: false,
},
};

View file

@ -17,6 +17,14 @@ export const getMonitorDetails = (state: AppState, summary: any) => {
return state.monitor.monitorDetailsList[summary.monitor_id];
};
export const getMonitorLocations = (state: AppState, monitorId: string) => {
export const selectMonitorLocations = (state: AppState, monitorId: string) => {
return state.monitor.monitorLocationsList?.get(monitorId);
};
export const selectSelectedMonitor = (state: AppState) => {
return state.monitorStatus.monitor;
};
export const selectMonitorStatus = (state: AppState) => {
return state.monitorStatus.status;
};

View file

@ -5,25 +5,25 @@
*/
import DateMath from '@elastic/datemath';
import { EuiFlexGroup, EuiFlexItem, EuiPage, EuiSpacer, EuiTitle } from '@elastic/eui';
import { EuiPage } from '@elastic/eui';
import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json';
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import { i18n } from '@kbn/i18n';
import React, { useEffect, useState } from 'react';
import { ApolloProvider } from 'react-apollo';
import { Provider as ReduxProvider } from 'react-redux';
import { BrowserRouter as Router, Route, RouteComponentProps, Switch } from 'react-router-dom';
import { BrowserRouter as Router, Route, RouteComponentProps } from 'react-router-dom';
import { I18nStart, ChromeBreadcrumb, LegacyCoreStart } from 'src/core/public';
import { PluginsStart } from 'ui/new_platform/new_platform';
import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public';
import { UMGraphQLClient, UMUpdateBreadcrumbs, UMUpdateBadge } from './lib/lib';
import { MonitorPage, OverviewPage, NotFoundPage } from './pages';
import { UptimeRefreshContext, UptimeSettingsContext, UMSettingsContextValues } from './contexts';
import { UptimeDatePicker, CommonlyUsedRange } from './components/functional/uptime_date_picker';
import { CommonlyUsedRange } from './components/functional/uptime_date_picker';
import { useUrlParams } from './hooks';
import { getTitle } from './lib/helper/get_title';
import { store } from './state';
import { setBasePath, triggerAppRefresh } from './state/actions';
import { PageRouter } from './routes';
export interface UptimeAppColors {
danger: string;
@ -93,7 +93,6 @@ const Application = (props: UptimeAppProps) => {
};
}
const [lastRefresh, setLastRefresh] = useState<number>(Date.now());
const [headingText, setHeadingText] = useState<string | undefined>(undefined);
useEffect(() => {
renderGlobalHelpControls();
@ -146,7 +145,7 @@ const Application = (props: UptimeAppProps) => {
isInfraAvailable,
isLogsAvailable,
refreshApp,
setHeadingText,
commonlyUsedRanges,
};
};
@ -166,49 +165,11 @@ const Application = (props: UptimeAppProps) => {
<UptimeSettingsContext.Provider value={initializeSettingsContextValues()}>
<EuiPage className="app-wrapper-panel " data-test-subj="uptimeApp">
<main>
<EuiFlexGroup
alignItems="center"
justifyContent="spaceBetween"
gutterSize="s"
>
<EuiFlexItem>
<EuiTitle>
<h1>{headingText}</h1>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<UptimeDatePicker
refreshApp={refreshApp}
commonlyUsedRanges={commonlyUsedRanges}
{...rootRouteProps}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
<Switch>
<Route
path="/monitor/:monitorId/:location?"
render={routerProps => (
<MonitorPage
query={client.query}
setBreadcrumbs={setBreadcrumbs}
{...routerProps}
/>
)}
/>
<Route
path="/"
render={routerProps => (
<OverviewPage
autocomplete={plugins.data.autocomplete}
basePath={basePath}
setBreadcrumbs={setBreadcrumbs}
{...routerProps}
/>
)}
/>
<Route component={NotFoundPage} />
</Switch>
<PageRouter
autocomplete={plugins.data.autocomplete}
basePath={basePath}
setBreadcrumbs={setBreadcrumbs}
/>
</main>
</EuiPage>
</UptimeSettingsContext.Provider>

View file

@ -9,12 +9,8 @@ import { UMResolver } from '../../../common/graphql/resolver_types';
import {
FilterBar,
GetFilterBarQueryArgs,
GetLatestMonitorsQueryArgs,
GetMonitorChartsDataQueryArgs,
GetMonitorPageTitleQueryArgs,
MonitorChart,
MonitorPageTitle,
Ping,
GetSnapshotHistogramQueryArgs,
} from '../../../common/graphql/types';
import { UMServerLibs } from '../../lib/lib';
@ -23,13 +19,6 @@ import { HistogramResult } from '../../../common/domain_types';
export type UMMonitorsResolver = UMResolver<any | Promise<any>, any, UMGqlRange, UMContext>;
export type UMLatestMonitorsResolver = UMResolver<
Ping[] | Promise<Ping[]>,
any,
GetLatestMonitorsQueryArgs,
UMContext
>;
export type UMGetMonitorChartsResolver = UMResolver<
any | Promise<any>,
any,
@ -44,13 +33,6 @@ export type UMGetFilterBarResolver = UMResolver<
UMContext
>;
export type UMGetMontiorPageTitleResolver = UMResolver<
MonitorPageTitle | Promise<MonitorPageTitle | null> | null,
any,
GetMonitorPageTitleQueryArgs,
UMContext
>;
export type UMGetSnapshotHistogram = UMResolver<
HistogramResult | Promise<HistogramResult>,
any,
@ -64,9 +46,7 @@ export const createMonitorsResolvers: CreateUMGraphQLResolvers = (
Query: {
getSnapshotHistogram: UMGetSnapshotHistogram;
getMonitorChartsData: UMGetMonitorChartsResolver;
getLatestMonitors: UMLatestMonitorsResolver;
getFilterBar: UMGetFilterBarResolver;
getMonitorPageTitle: UMGetMontiorPageTitleResolver;
};
} => ({
Query: {
@ -97,19 +77,6 @@ export const createMonitorsResolvers: CreateUMGraphQLResolvers = (
location,
});
},
async getLatestMonitors(
_resolver,
{ dateRangeStart, dateRangeEnd, monitorId, location },
{ APICaller }
): Promise<Ping[]> {
return await libs.pings.getLatestMonitorDocs({
callES: APICaller,
dateRangeStart,
dateRangeEnd,
monitorId,
location,
});
},
async getFilterBar(
_resolver,
{ dateRangeStart, dateRangeEnd },
@ -121,12 +88,5 @@ export const createMonitorsResolvers: CreateUMGraphQLResolvers = (
dateRangeEnd,
});
},
async getMonitorPageTitle(
_resolver: any,
{ monitorId },
{ APICaller }
): Promise<MonitorPageTitle | null> {
return await libs.monitors.getMonitorPageTitle({ callES: APICaller, monitorId });
},
},
});

View file

@ -114,12 +114,6 @@ export const monitorsSchema = gql`
interval: UnsignedInteger!
}
type MonitorPageTitle {
id: String!
url: String
name: String
}
extend type Query {
getMonitors(
dateRangeStart: String!
@ -156,7 +150,5 @@ export const monitorsSchema = gql`
): [Ping!]!
getFilterBar(dateRangeStart: String!, dateRangeEnd: String!): FilterBar
getMonitorPageTitle(monitorId: String!): MonitorPageTitle
}
`;

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { MonitorChart, MonitorPageTitle } from '../../../../common/graphql/types';
import { MonitorChart } from '../../../../common/graphql/types';
import { UMElasticsearchQueryFn } from '../framework';
import { MonitorDetails, MonitorLocations } from '../../../../common/runtime_types';
@ -31,11 +31,6 @@ export interface GetMonitorDetailsParams {
dateEnd: string;
}
export interface GetMonitorPageTitleParams {
/** @member monitorId the ID to query */
monitorId: string;
}
/**
* Fetch data for the monitor page title.
*/
@ -57,7 +52,6 @@ export interface UMMonitorsAdapter {
/**
* Fetch data for the monitor page title.
*/
getMonitorPageTitle: UMElasticsearchQueryFn<{ monitorId: string }, MonitorPageTitle | null>;
getMonitorDetails: UMElasticsearchQueryFn<GetMonitorDetailsParams, MonitorDetails>;
getMonitorLocations: UMElasticsearchQueryFn<GetMonitorLocationsParams, MonitorLocations>;
}

View file

@ -6,7 +6,7 @@
import { get } from 'lodash';
import { INDEX_NAMES } from '../../../../common/constants';
import { MonitorChart, Ping, LocationDurationLine } from '../../../../common/graphql/types';
import { MonitorChart, LocationDurationLine } from '../../../../common/graphql/types';
import { getHistogramIntervalFormatted } from '../../helper';
import { MonitorError, MonitorLocation } from '../../../../common/runtime_types';
import { UMMonitorsAdapter } from './adapter_types';
@ -195,42 +195,6 @@ export const elasticsearchMonitorsAdapter: UMMonitorsAdapter = {
}, {});
},
getMonitorPageTitle: async ({ callES, monitorId }) => {
const params = {
index: INDEX_NAMES.HEARTBEAT,
body: {
query: {
bool: {
filter: {
term: {
'monitor.id': monitorId,
},
},
},
},
sort: [
{
'@timestamp': {
order: 'desc',
},
},
],
size: 1,
},
};
const result = await callES('search', params);
const pageTitle: Ping | null = get(result, 'hits.hits[0]._source', null);
if (pageTitle === null) {
return null;
}
return {
id: get(pageTitle, 'monitor.id', null) || monitorId,
url: get(pageTitle, 'url.full', null),
name: get(pageTitle, 'monitor.name', null),
};
},
getMonitorDetails: async ({ callES, monitorId, dateStart, dateEnd }) => {
const queryFilters: any = [
{
@ -289,11 +253,6 @@ export const elasticsearchMonitorsAdapter: UMMonitorsAdapter = {
};
},
/**
* Fetch data for the monitor page title.
* @param request Kibana server request
*
*/
getMonitorLocations: async ({ callES, monitorId, dateStart, dateEnd }) => {
const params = {
index: INDEX_NAMES.HEARTBEAT,

View file

@ -411,7 +411,7 @@ describe('ElasticsearchPingsAdapter class', () => {
});
});
describe('getLatestMonitorDocs', () => {
describe('getLatestMonitorStatus', () => {
let expectedGetLatestSearchParams: any;
beforeEach(() => {
expectedGetLatestSearchParams = {
@ -429,7 +429,7 @@ describe('ElasticsearchPingsAdapter class', () => {
},
},
{
term: { 'monitor.id': 'testmonitor' },
term: { 'monitor.id': 'testMonitor' },
},
],
},
@ -467,7 +467,7 @@ describe('ElasticsearchPingsAdapter class', () => {
_source: {
'@timestamp': 123456,
monitor: {
id: 'testmonitor',
id: 'testMonitor',
},
},
},
@ -483,17 +483,16 @@ describe('ElasticsearchPingsAdapter class', () => {
it('returns data in expected shape', async () => {
const mockEsClient = jest.fn(async (_request: any, _params: any) => mockEsSearchResult);
const result = await adapter.getLatestMonitorDocs({
const result = await adapter.getLatestMonitorStatus({
callES: mockEsClient,
dateRangeStart: 'now-1h',
dateRangeEnd: 'now',
monitorId: 'testmonitor',
dateStart: 'now-1h',
dateEnd: 'now',
monitorId: 'testMonitor',
});
expect(result).toHaveLength(1);
expect(result[0].timestamp).toBe(123456);
expect(result[0].monitor).not.toBeFalsy();
expect(result.timestamp).toBe(123456);
expect(result.monitor).not.toBeFalsy();
// @ts-ignore monitor will be defined
expect(result[0].monitor.id).toBe('testmonitor');
expect(result.monitor.id).toBe('testMonitor');
expect(mockEsClient).toHaveBeenCalledWith('search', expectedGetLatestSearchParams);
});
});

View file

@ -33,16 +33,13 @@ export interface GetAllParams {
export interface GetLatestMonitorDocsParams {
/** @member dateRangeStart timestamp bounds */
dateRangeStart: string;
dateStart?: string;
/** @member dateRangeEnd timestamp bounds */
dateRangeEnd: string;
dateEnd?: string;
/** @member monitorId optional limit to monitorId */
monitorId?: string | null;
/** @member location optional location value for use in filtering*/
location?: string | null;
}
export interface GetPingHistogramParams {
@ -64,7 +61,10 @@ export interface GetPingHistogramParams {
export interface UMPingsAdapter {
getAll: UMElasticsearchQueryFn<GetAllParams, PingResults>;
getLatestMonitorDocs: UMElasticsearchQueryFn<GetLatestMonitorDocsParams, Ping[]>;
// Get the monitor meta info regardless of timestamp
getMonitor: UMElasticsearchQueryFn<GetLatestMonitorDocsParams, Ping>;
getLatestMonitorStatus: UMElasticsearchQueryFn<GetLatestMonitorDocsParams, Ping>;
getPingHistogram: UMElasticsearchQueryFn<GetPingHistogramParams, HistogramResult>;

View file

@ -88,7 +88,10 @@ export const elasticsearchPingsAdapter: UMPingsAdapter = {
return results;
},
getLatestMonitorDocs: async ({ callES, dateRangeStart, dateRangeEnd, monitorId, location }) => {
// Get The monitor latest state sorted by timestamp with date range
getLatestMonitorStatus: async ({ callES, dateStart, dateEnd, monitorId }) => {
// TODO: Write tests for this function
const params = {
index: INDEX_NAMES.HEARTBEAT,
body: {
@ -98,13 +101,12 @@ export const elasticsearchPingsAdapter: UMPingsAdapter = {
{
range: {
'@timestamp': {
gte: dateRangeStart,
lte: dateRangeEnd,
gte: dateStart,
lte: dateEnd,
},
},
},
...(monitorId ? [{ term: { 'monitor.id': monitorId } }] : []),
...(location ? [{ term: { 'observer.geo.name': location } }] : []),
],
},
},
@ -131,21 +133,45 @@ export const elasticsearchPingsAdapter: UMPingsAdapter = {
};
const result = await callES('search', params);
const buckets: any[] = get(result, 'aggregations.by_id.buckets', []);
const ping: any = result.aggregations.by_id.buckets?.[0]?.latest.hits?.hits?.[0] ?? {};
return buckets.map(
({
latest: {
hits: { hits },
return {
...ping?._source,
timestamp: ping?._source?.['@timestamp'],
};
},
// Get the monitor meta info regardless of timestamp
getMonitor: async ({ callES, monitorId }) => {
const params = {
index: INDEX_NAMES.HEARTBEAT,
body: {
size: 1,
_source: ['url', 'monitor', 'observer'],
query: {
bool: {
filter: [
{
term: {
'monitor.id': monitorId,
},
},
],
},
},
}) => {
const timestamp = hits[0]._source[`@timestamp`];
return {
...hits[0]._source,
timestamp,
};
}
);
sort: [
{
'@timestamp': {
order: 'desc',
},
},
],
},
};
const result = await callES('search', params);
return result.hits.hits[0]?._source;
},
getPingHistogram: async ({

View file

@ -9,7 +9,12 @@ import { createGetIndexPatternRoute } from './index_pattern';
import { createLogMonitorPageRoute, createLogOverviewPageRoute } from './telemetry';
import { createGetSnapshotCount } from './snapshot';
import { UMRestApiRouteFactory } from './types';
import { createGetMonitorDetailsRoute, createGetMonitorLocationsRoute } from './monitors';
import {
createGetMonitorRoute,
createGetMonitorDetailsRoute,
createGetMonitorLocationsRoute,
createGetStatusBarRoute,
} from './monitors';
export * from './types';
export { createRouteWithAuth } from './create_route_with_auth';
@ -17,8 +22,10 @@ export { uptimeRouteWrapper } from './uptime_route_wrapper';
export const restApiRoutes: UMRestApiRouteFactory[] = [
createGetAllRoute,
createGetIndexPatternRoute,
createGetMonitorRoute,
createGetMonitorDetailsRoute,
createGetMonitorLocationsRoute,
createGetStatusBarRoute,
createGetSnapshotCount,
createLogMonitorPageRoute,
createLogOverviewPageRoute,

View file

@ -6,3 +6,4 @@
export { createGetMonitorDetailsRoute } from './monitors_details';
export { createGetMonitorLocationsRoute } from './monitor_locations';
export { createGetMonitorRoute, createGetStatusBarRoute } from './status';

View file

@ -0,0 +1,60 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { schema } from '@kbn/config-schema';
import { UMServerLibs } from '../../lib/lib';
import { UMRestApiRouteFactory } from '../types';
export const createGetMonitorRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({
method: 'GET',
path: '/api/uptime/monitor/selected',
validate: {
query: schema.object({
monitorId: schema.string(),
}),
},
options: {
tags: ['access:uptime'],
},
handler: async ({ callES }, _context, request, response): Promise<any> => {
const { monitorId } = request.query;
return response.ok({
body: {
...(await libs.pings.getMonitor({ callES, monitorId })),
},
});
},
});
export const createGetStatusBarRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({
method: 'GET',
path: '/api/uptime/monitor/status',
validate: {
query: schema.object({
monitorId: schema.string(),
dateStart: schema.string(),
dateEnd: schema.string(),
}),
},
options: {
tags: ['access:uptime'],
},
handler: async ({ callES }, _context, request, response): Promise<any> => {
const { monitorId, dateStart, dateEnd } = request.query;
const result = await libs.pings.getLatestMonitorStatus({
callES,
monitorId,
dateStart,
dateEnd,
});
return response.ok({
body: {
...result,
},
});
},
});

View file

@ -11741,7 +11741,6 @@
"xpack.uptime.emptyState.loadingMessage": "読み込み中…",
"xpack.uptime.emptyState.noDataTitle": "利用可能なアップタイムデータがありません",
"xpack.uptime.emptyStateError.title": "エラー",
"xpack.uptime.emptyStatusBar.defaultMessage": "監視 ID {monitorId} のデータが見つかりません",
"xpack.uptime.errorMessage": "エラー: {message}",
"xpack.uptime.featureCatalogueDescription": "エンドポイントヘルスチェックとアップタイム監視を行います。",
"xpack.uptime.featureRegistry.uptimeFeatureName": "アップタイム",
@ -11781,7 +11780,6 @@
"xpack.uptime.monitorList.statusColumn.upLabel": "アップ",
"xpack.uptime.monitorList.statusColumnLabel": "ステータス",
"xpack.uptime.monitorStatusBar.durationTextAriaLabel": "ミリ秒単位の監視時間",
"xpack.uptime.monitorStatusBar.healthStatus.durationInMillisecondsMessage": "{duration}ms",
"xpack.uptime.monitorStatusBar.healthStatusMessage.downLabel": "ダウン",
"xpack.uptime.monitorStatusBar.healthStatusMessage.upLabel": "アップ",
"xpack.uptime.monitorStatusBar.healthStatusMessageAriaLabel": "監視ステータス",

View file

@ -11830,7 +11830,6 @@
"xpack.uptime.emptyState.loadingMessage": "正在加载……",
"xpack.uptime.emptyState.noDataTitle": "没有可用的运行时间数据",
"xpack.uptime.emptyStateError.title": "错误",
"xpack.uptime.emptyStatusBar.defaultMessage": "未找到监测 ID {monitorId} 的数据",
"xpack.uptime.errorMessage": "错误:{message}",
"xpack.uptime.featureCatalogueDescription": "执行终端节点运行状况检查和运行时间监测。",
"xpack.uptime.featureRegistry.uptimeFeatureName": "运行时间",
@ -11870,7 +11869,6 @@
"xpack.uptime.monitorList.statusColumn.upLabel": "运行",
"xpack.uptime.monitorList.statusColumnLabel": "状态",
"xpack.uptime.monitorStatusBar.durationTextAriaLabel": "监测持续时间(毫秒)",
"xpack.uptime.monitorStatusBar.healthStatus.durationInMillisecondsMessage": "{duration}ms",
"xpack.uptime.monitorStatusBar.healthStatusMessage.downLabel": "关闭",
"xpack.uptime.monitorStatusBar.healthStatusMessage.upLabel": "运行",
"xpack.uptime.monitorStatusBar.healthStatusMessageAriaLabel": "检测状态",

View file

@ -1,7 +0,0 @@
{
"monitorPageTitle": {
"id": "0002-up",
"url": "http://localhost:5678/pattern?r=200x1",
"name": ""
}
}

View file

@ -1,20 +0,0 @@
[
{
"timestamp": "2019-09-11T03:40:34.371Z",
"monitor": {
"status": "up",
"duration": {
"us": 24627
}
},
"observer": {
"geo": {
"name": "mpls"
}
},
"tls": null,
"url": {
"full": "http://localhost:5678/pattern?r=200x1"
}
}
]

View file

@ -10,6 +10,7 @@ import { join } from 'path';
import { cloneDeep } from 'lodash';
const fixturesDir = join(__dirname, '..', 'fixtures');
const restFixturesDir = join(__dirname, '../../rest/', 'fixtures');
const excludeFieldsFrom = (from: any, excluder?: (d: any) => any): any => {
const clone = cloneDeep(from);
@ -20,7 +21,11 @@ const excludeFieldsFrom = (from: any, excluder?: (d: any) => any): any => {
};
export const expectFixtureEql = <T>(data: T, fixtureName: string, excluder?: (d: T) => void) => {
const fixturePath = join(fixturesDir, `${fixtureName}.json`);
let fixturePath = join(fixturesDir, `${fixtureName}.json`);
if (!fs.existsSync(fixturePath)) {
fixturePath = join(restFixturesDir, `${fixtureName}.json`);
}
const dataExcluded = excludeFieldsFrom(data, excluder);
expect(dataExcluded).not.to.be(undefined);
if (process.env.UPDATE_UPTIME_FIXTURES) {

View file

@ -13,9 +13,7 @@ export default function({ loadTestFile }) {
loadTestFile(require.resolve('./doc_count'));
loadTestFile(require.resolve('./filter_bar'));
loadTestFile(require.resolve('./monitor_charts'));
loadTestFile(require.resolve('./monitor_page_title'));
loadTestFile(require.resolve('./monitor_states'));
loadTestFile(require.resolve('./monitor_status_bar'));
loadTestFile(require.resolve('./ping_list'));
loadTestFile(require.resolve('./snapshot_histogram'));
});

View file

@ -1,37 +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 { monitorPageTitleQueryString } from '../../../../../legacy/plugins/uptime/public/queries/monitor_page_title_query';
import { FtrProviderContext } from '../../../ftr_provider_context';
import { expectFixtureEql } from './helpers/expect_fixture_eql';
export default function({ getService }: FtrProviderContext) {
describe('monitor_page_title', () => {
before('load heartbeat data', () => getService('esArchiver').load('uptime/full_heartbeat'));
after('unload heartbeat index', () => getService('esArchiver').unload('uptime/full_heartbeat'));
const supertest = getService('supertest');
it('will fetch a title for a given monitorId', async () => {
const getMonitorTitleQuery = {
operationName: 'MonitorPageTitle',
query: monitorPageTitleQueryString,
variables: {
monitorId: '0002-up',
},
};
const {
body: { data },
} = await supertest
.post('/api/uptime/graphql')
.set('kbn-xsrf', 'foo')
.send({ ...getMonitorTitleQuery });
expectFixtureEql(data, 'monitor_page_title');
});
});
}

View file

@ -1,58 +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 { monitorStatusBarQueryString } from '../../../../../legacy/plugins/uptime/public/queries';
import { expectFixtureEql } from './helpers/expect_fixture_eql';
export default function({ getService }) {
describe('monitorStatusBar query', () => {
before('load heartbeat data', () => getService('esArchiver').load('uptime/full_heartbeat'));
after('unload heartbeat index', () => getService('esArchiver').unload('uptime/full_heartbeat'));
const supertest = getService('supertest');
it('returns the status for all monitors with no ID filtering', async () => {
const getMonitorStatusBarQuery = {
operationName: 'MonitorStatus',
query: monitorStatusBarQueryString,
variables: {
dateRangeStart: '2019-01-28T17:40:08.078Z',
dateRangeEnd: '2025-01-28T19:00:16.078Z',
},
};
const {
body: {
data: { monitorStatus: responseData },
},
} = await supertest
.post('/api/uptime/graphql')
.set('kbn-xsrf', 'foo')
.send({ ...getMonitorStatusBarQuery });
expectFixtureEql(responseData, 'monitor_status_all', res =>
res.forEach(i => delete i.millisFromNow)
);
});
it('returns the status for only the given monitor', async () => {
const getMonitorStatusBarQuery = {
operationName: 'MonitorStatus',
query: monitorStatusBarQueryString,
variables: {
dateRangeStart: '2019-01-28T17:40:08.078Z',
dateRangeEnd: '2025-01-28T19:00:16.078Z',
monitorId: '0002-up',
},
};
const res = await supertest
.post('/api/uptime/graphql')
.set('kbn-xsrf', 'foo')
.send({ ...getMonitorStatusBarQuery });
expectFixtureEql(res.body.data.monitorStatus, 'monitor_status_by_id');
});
});
}

View file

@ -0,0 +1,89 @@
{
"@timestamp": "2019-09-11T03:40:34.371Z",
"agent": {
"ephemeral_id": "412a92a8-2142-4b1a-a7a2-1afd32e12f85",
"hostname": "avc-x1x",
"id": "04e1d082-65bc-4929-8d65-d0768a2621c4",
"type": "heartbeat",
"version": "8.0.0"
},
"ecs": {
"version": "1.1.0"
},
"event": {
"dataset": "uptime"
},
"host": {
"name": "avc-x1x"
},
"http": {
"response": {
"body": {
"bytes": 3,
"hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf"
},
"status_code": 200
},
"rtt": {
"content": {
"us": 57
},
"response_header": {
"us": 262
},
"total": {
"us": 20331
},
"validate": {
"us": 319
},
"write_request": {
"us": 82
}
}
},
"monitor": {
"check_group": "d76f0762-d445-11e9-88e3-3e80641b9c71",
"duration": {
"us": 24627
},
"id": "0002-up",
"ip": "127.0.0.1",
"name": "",
"status": "up",
"type": "http"
},
"observer": {
"geo": {
"location": "37.926868, -78.024902",
"name": "mpls"
},
"hostname": "avc-x1x"
},
"resolve": {
"ip": "127.0.0.1",
"rtt": {
"us": 4218
}
},
"summary": {
"down": 0,
"up": 1
},
"tcp": {
"rtt": {
"connect": {
"us": 103
}
}
},
"timestamp": "2019-09-11T03:40:34.371Z",
"url": {
"domain": "localhost",
"full": "http://localhost:5678/pattern?r=200x1",
"path": "/pattern",
"port": 5678,
"query": "r=200x1",
"scheme": "http"
}
}

View file

@ -0,0 +1,28 @@
{
"monitor": {
"check_group": "d76f0762-d445-11e9-88e3-3e80641b9c71",
"duration": {
"us": 24627
},
"id": "0002-up",
"ip": "127.0.0.1",
"name": "",
"status": "up",
"type": "http"
},
"observer": {
"geo": {
"location": "37.926868, -78.024902",
"name": "mpls"
},
"hostname": "avc-x1x"
},
"url": {
"domain": "localhost",
"full": "http://localhost:5678/pattern?r=200x1",
"path": "/pattern",
"port": 5678,
"query": "r=200x1",
"scheme": "http"
}
}

View file

@ -12,5 +12,7 @@ export default function({ getService, loadTestFile }: FtrProviderContext) {
before('load heartbeat data', () => esArchiver.load('uptime/blank'));
after('unload', () => esArchiver.unload('uptime/blank'));
loadTestFile(require.resolve('./snapshot'));
loadTestFile(require.resolve('./monitor_latest_status'));
loadTestFile(require.resolve('./selected_monitor'));
});
}

View file

@ -0,0 +1,25 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { expectFixtureEql } from '../graphql/helpers/expect_fixture_eql';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function({ getService }: FtrProviderContext) {
describe('get monitor latest status API', () => {
const dateStart = '2018-01-28T17:40:08.078Z';
const dateEnd = '2025-01-28T19:00:16.078Z';
const monitorId = '0002-up';
const supertest = getService('supertest');
it('returns the status for only the given monitor', async () => {
const apiResponse = await supertest.get(
`/api/uptime/monitor/status?monitorId=${monitorId}&dateStart=${dateStart}&dateEnd=${dateEnd}`
);
expectFixtureEql(apiResponse.body, 'monitor_latest_status');
});
});
}

View file

@ -0,0 +1,23 @@
/*
* 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 { expectFixtureEql } from '../graphql/helpers/expect_fixture_eql';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function({ getService }: FtrProviderContext) {
describe('get selected monitor by ID', () => {
const monitorId = '0002-up';
const supertest = getService('supertest');
it('returns the monitor for give ID', async () => {
const apiResponse = await supertest.get(
`/api/uptime/monitor/selected?monitorId=${monitorId}`
);
expectFixtureEql(apiResponse.body, 'selected_monitor');
});
});
}