[Uptime] Fix "last updated" field (#28720)

* Update MonitorStatusBar to not reference undefined value.

* Add sort to top_hits aggregation.

* Add default and loading state for status bar.

* Change bool check.

* Add aria-labels for monitor status bar.
This commit is contained in:
Justin Kambic 2019-01-23 15:10:36 -05:00 committed by GitHub
parent b21ee115e9
commit c96068c4ab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 195 additions and 85 deletions

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 { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
import React from 'react';
interface Props {
monitorId: string;
message?: string;
}
export const EmptyStatusBar = ({ message, monitorId }: Props) => (
<EuiPanel>
<EuiFlexGroup gutterSize="l">
<EuiFlexItem grow={false}>
{!message ? `No data found for monitor id ${monitorId}` : message}
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);

View file

@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { EmptyStatusBar } from './empty_status_bar';
export { SnapshotHistogram } from './snapshot_histogram';
export { StatusBar } from './status_bar';

View file

@ -0,0 +1,77 @@
/*
* 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, EuiPanel } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import moment from 'moment';
import React from 'react';
interface Props {
duration?: number;
url?: string;
status?: string;
timestamp?: string;
}
export const StatusBar = ({ timestamp, url, duration, status }: Props) => (
<EuiPanel>
<EuiFlexGroup gutterSize="l">
<EuiFlexItem grow={false}>
<EuiHealth
aria-label={i18n.translate('xpack.uptime.monitorStatusBar.healthStatusMessageAriaLabel', {
defaultMessage: 'Monitor status',
})}
color={status === 'up' ? 'success' : 'danger'}
style={{ lineHeight: 'inherit' }}
>
{status === 'up'
? i18n.translate('xpack.uptime.monitorStatusBar.healthStatusMessage.upLabel', {
defaultMessage: 'Up',
})
: i18n.translate('xpack.uptime.monitorStatusBar.healthStatusMessage.downLabel', {
defaultMessage: 'Down',
})}
</EuiHealth>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexItem grow={false}>
<EuiLink
aria-label={i18n.translate('xpack.uptime.monitorStatusBar.monitorUrlLinkAriaLabel', {
defaultMessage: 'Monitor URL link',
})}
href={url}
target="_blank"
>
{url}
</EuiLink>
</EuiFlexItem>
</EuiFlexItem>
<EuiFlexItem
aria-label={i18n.translate('xpack.uptime.monitorStatusBar.durationTextAriaLabel', {
defaultMessage: 'Monitor duration in milliseconds',
})}
grow={false}
>
<FormattedMessage
id="xpack.uptime.monitorStatusBar.healthStatus.durationInMillisecondsMessage"
// TODO: this should not be computed inline
values={{ duration: duration ? duration / 1000 : 0 }}
defaultMessage="{duration}ms"
description="The 'ms' is an abbreviation for 'milliseconds'."
/>
</EuiFlexItem>
<EuiFlexItem
aria-label={i18n.translate('xpack.uptime.monitorStatusBar.timestampFromNowTextAriaLabel', {
defaultMessage: 'Time since last check',
})}
grow={false}
>
{moment(timestamp).fromNow()}
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);

View file

@ -0,0 +1,24 @@
/*
* 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 { formatDuration } from '../format_duration';
describe('formatDuration', () => {
it('returns 0 for undefined', () => {
const result = formatDuration(undefined);
expect(result).toEqual(0);
});
it('returns 0 for NaN', () => {
const result = formatDuration(NaN);
expect(result).toEqual(0);
});
it('returns duration value in ms', () => {
const duration = 320000; // microseconds
expect(formatDuration(duration)).toEqual(320);
});
});

View file

@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export const formatDuration = (duration: number | undefined): number => {
if (duration === undefined) {
return 0;
}
// TODO: formatting should not be performed this way, remove bare number
return isNaN(duration) ? 0 : duration / 1000;
};

View file

@ -14,6 +14,7 @@ export const createGetMonitorStatusBarQuery = gql`
monitorId: $monitorId
) {
timestamp
millisFromNow
monitor {
status
host

View file

@ -4,19 +4,28 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiFlexGroup, EuiFlexItem, EuiHealth, EuiLink, EuiPanel } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import moment from 'moment';
import { ApolloError } from 'apollo-client';
import { get } from 'lodash';
import React from 'react';
import { Query } from 'react-apollo';
import { Ping } from 'x-pack/plugins/uptime/common/graphql/types';
import { UptimeCommonProps } from '../../../uptime_app';
import { StatusBar } from '../../functional';
import { EmptyStatusBar } from '../../functional/empty_status_bar';
import { formatDuration } from './format_duration';
import { createGetMonitorStatusBarQuery } from './get_monitor_status_bar';
interface MonitorStatusBarProps {
monitorId: string;
}
interface MonitorStatusBarQueryParams {
loading: boolean;
error?: ApolloError | any;
data?: { monitorStatus: Ping[] };
}
type Props = MonitorStatusBarProps & UptimeCommonProps;
export const MonitorStatusBar = ({
@ -31,11 +40,9 @@ export const MonitorStatusBar = ({
query={createGetMonitorStatusBarQuery}
variables={{ dateRangeStart, dateRangeEnd, monitorId }}
>
{({ loading, error, data }) => {
{({ loading, error, data }: MonitorStatusBarQueryParams) => {
if (loading) {
return i18n.translate('xpack.uptime.monitorStatusBar.loadingMessage', {
defaultMessage: 'Loading…',
});
return <EmptyStatusBar message="Fetching data" monitorId={monitorId} />;
}
if (error) {
return i18n.translate('xpack.uptime.monitorStatusBar.errorMessage', {
@ -43,82 +50,23 @@ export const MonitorStatusBar = ({
defaultMessage: 'Error {message}',
});
}
const { monitorStatus } = data;
if (!monitorStatus.length) {
return i18n.translate('xpack.uptime.monitorStatusBar.noDataMessage', {
values: { monitorId },
defaultMessage: 'No data found for monitor id {monitorId}',
});
const monitorStatus: Ping[] = get(data, 'monitorStatus');
if (!monitorStatus || !monitorStatus.length) {
return <EmptyStatusBar monitorId={monitorId} />;
}
const {
monitor: {
status,
timestamp,
ip,
duration: { us },
},
url: { full: fullURL },
} = monitorStatus[0];
const { monitor, timestamp, url } = monitorStatus[0];
const status = get(monitor, 'status', undefined);
const duration = parseInt(get(monitor, 'duration.us'), 10);
const full = get(url, 'full', undefined);
return (
<EuiPanel>
<EuiFlexGroup gutterSize="l">
<EuiFlexItem grow={false}>
<EuiFlexGroup>
<EuiFlexItem>
<FormattedMessage
id="xpack.uptime.monitorStatusBar.statusLabel"
defaultMessage="Status:"
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiHealth
color={status === 'up' ? 'success' : 'danger'}
style={{ lineHeight: 'inherit' }}
>
{status === 'up'
? i18n.translate(
'xpack.uptime.monitorStatusBar.healthStatusMessage.upLabel',
{ defaultMessage: 'Up' }
)
: i18n.translate(
'xpack.uptime.monitorStatusBar.healthStatusMessage.downLabel',
{ defaultMessage: 'Down' }
)}
</EuiHealth>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<FormattedMessage
id="xpack.uptime.monitorStatusBar.healthStatus.lastUpdateMessage"
values={{ timeFromNow: moment(timestamp).fromNow() }}
defaultMessage="Last update: {timeFromNow}"
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiLink href={fullURL} target="_blank">
{fullURL}
</EuiLink>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<FormattedMessage
id="xpack.uptime.monitorStatusBar.healthStatus.ipMessage"
// TODO: this should not be computed inline
values={{ ip }}
defaultMessage="IP: {ip}"
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<FormattedMessage
id="xpack.uptime.monitorStatusBar.healthStatus.durationInMillisecondsMessage"
// TODO: this should not be computed inline
values={{ duration: us / 1000 }}
defaultMessage="Duration: {duration} ms"
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
<StatusBar
duration={formatDuration(duration)}
status={status}
timestamp={timestamp}
url={full}
/>
);
}}
</Query>

View file

@ -27,7 +27,7 @@ import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import { Query } from 'react-apollo';
import { UptimeCommonProps } from '../../../uptime_app';
import { SnapshotHistogram } from '../../functional/snapshot_histogram';
import { SnapshotHistogram } from '../../functional';
import { getSnapshotQuery } from './get_snapshot';
interface SnapshotProps {

View file

@ -193,6 +193,8 @@ export const pingsSchema = gql`
type Ping {
"The timestamp of the ping's creation"
timestamp: String!
"Milliseconds from the timestamp to the current time"
millisFromNow: Int
"The agent that recorded the ping"
beat: Beat
docker: Docker

View file

@ -182,6 +182,9 @@ describe('ElasticsearchPingsAdapter class', () => {
latest: {
top_hits: {
size: 1,
sort: {
'@timestamp': { order: 'desc' },
},
},
},
},

View file

@ -5,6 +5,7 @@
*/
import { get } from 'lodash';
import moment from 'moment';
import { INDEX_NAMES } from '../../../../common/constants';
import { DocCount, HistogramSeries, Ping, PingResults } from '../../../../common/graphql/types';
import { DatabaseAdapter } from '../database';
@ -128,6 +129,9 @@ export class ElasticsearchPingsAdapter implements UMPingsAdapter {
latest: {
top_hits: {
size: 1,
sort: {
'@timestamp': { order: 'desc' },
},
},
},
},
@ -142,10 +146,16 @@ export class ElasticsearchPingsAdapter implements UMPingsAdapter {
} = await this.database.search(request, params);
// @ts-ignore TODO fix destructuring implicit any
return buckets.map(({ latest: { hits: { hits } } }) => ({
...hits[0]._source,
timestamp: hits[0]._source[`@timestamp`],
}));
return buckets.map(({ latest: { hits: { hits } } }) => {
const timestamp = hits[0]._source[`@timestamp`];
const momentTs = moment(timestamp);
const millisFromNow = moment().diff(momentTs);
return {
...hits[0]._source,
timestamp,
millisFromNow,
};
});
}
public async getPingHistogram(