mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[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:
parent
b21ee115e9
commit
c96068c4ab
11 changed files with 195 additions and 85 deletions
|
@ -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>
|
||||
);
|
|
@ -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';
|
|
@ -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>
|
||||
);
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
};
|
|
@ -14,6 +14,7 @@ export const createGetMonitorStatusBarQuery = gql`
|
|||
monitorId: $monitorId
|
||||
) {
|
||||
timestamp
|
||||
millisFromNow
|
||||
monitor {
|
||||
status
|
||||
host
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -182,6 +182,9 @@ describe('ElasticsearchPingsAdapter class', () => {
|
|||
latest: {
|
||||
top_hits: {
|
||||
size: 1,
|
||||
sort: {
|
||||
'@timestamp': { order: 'desc' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue