[Uptime] Uptime to APM integration (#34892)

* Add integrations popover.

* Add some more functionality, code is WIP/mocked.

* Trying some things WIP.

* Import settings values from context.

* Remove obsolete comment.

* Add links.

* Rename component. Clean up placeholder text and add translations.

* Minor tweaks. Rename component file.

* Fix import for renamed file.

* Add domain to api query result fixtures.

* Change integration to utilize EuiTable's actions API.

* Add translation for new column heading.

* Update busted snapshot.

* Add snapshot test for new component.

* Refactor integration links to dedicated component.

* Remove obsolete index export.

* Update monitor list test snapshot.

* Default monitor list to empty array instead of undefined.

* Extract URL construction to helper function.

* Make entire link text clickable for APM integration.

* Update broken test snapshot.

* Fix type and update test snapshot.
This commit is contained in:
Justin Kambic 2019-05-01 11:30:09 -04:00 committed by GitHub
parent a343899f75
commit 5fdb23c31d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 418 additions and 117 deletions

View file

@ -0,0 +1,29 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`IntegrationLink component renders without errors 1`] = `
<EuiLink
aria-label="foo"
color="subdued"
href="/app/foo?kuery=localhost"
type="button"
>
<EuiFlexGroup>
<EuiFlexItem
grow={false}
>
<EuiToolTip
content="info for bar"
delay="regular"
position="top"
>
<EuiIcon
type="apmApp"
/>
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem>
click for bar
</EuiFlexItem>
</EuiFlexGroup>
</EuiLink>
`;

View file

@ -39,6 +39,11 @@ exports[`MonitorList component renders a monitor list without errors 1`] = `
"name": "URL",
"render": [Function],
},
Object {
"field": "ping.url.full",
"name": "URL",
"render": [Function],
},
Object {
"align": "right",
"field": "upSeries",
@ -46,6 +51,12 @@ exports[`MonitorList component renders a monitor list without errors 1`] = `
"render": [Function],
"width": "180px",
},
Object {
"align": "right",
"field": "ping",
"name": "Integrations",
"render": [Function],
},
]
}
executeQueryOptions={Object {}}

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 React from 'react';
import { IntegrationLink } from '../integration_link';
import { shallowWithIntl } from 'test_utils/enzyme_helpers';
describe('IntegrationLink component', () => {
it('renders without errors', () => {
const component = shallowWithIntl(
<IntegrationLink
ariaLabel="foo"
href="/app/foo?kuery=localhost"
iconType="apmApp"
message="click for bar"
tooltipContent="info for bar"
/>
);
expect(component).toMatchSnapshot();
});
});

View file

@ -440,8 +440,11 @@ describe('MonitorList component', () => {
it('renders a monitor list without errors', () => {
const component = shallowWithIntl(
<MonitorListComponent
basePath="foo"
dangerColor="red"
data={{ monitorStatus: { monitors } }}
dateRangeStart="now-15m"
dateRangeEnd="now"
loading={false}
/>
);

View file

@ -9,6 +9,7 @@ export { EmptyStatusBar } from './empty_status_bar';
export { ErrorList } from './error_list';
export { FilterBar } from './filter_bar';
export { FilterBarLoading } from './filter_bar_loading';
export { IntegrationLink } from './integration_link';
export { MonitorCharts } from './monitor_charts';
export { MonitorList } from './monitor_list';
export { MonitorPageTitle } from './monitor_page_title';

View file

@ -0,0 +1,35 @@
/*
* 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, EuiIcon, EuiLink, EuiToolTip } from '@elastic/eui';
import React from 'react';
interface IntegrationLinkProps {
ariaLabel: string;
href: string;
iconType: 'apmApp' | 'infraApp' | 'loggingApp';
message: string;
tooltipContent: string;
}
export const IntegrationLink = ({
ariaLabel,
href,
iconType,
message,
tooltipContent,
}: IntegrationLinkProps) => (
<EuiLink aria-label={ariaLabel} color="subdued" href={href}>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiToolTip content={tooltipContent} position="top">
<EuiIcon type={iconType} />
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem>{message}</EuiFlexItem>
</EuiFlexGroup>
</EuiLink>
);

View file

@ -27,10 +27,11 @@ import { get } from 'lodash';
import moment from 'moment';
import React from 'react';
import { Link } from 'react-router-dom';
import { LatestMonitor, MonitorSeriesPoint } from '../../../common/graphql/types';
import { LatestMonitor, MonitorSeriesPoint, Ping } from '../../../common/graphql/types';
import { UptimeGraphQLQueryProps, withUptimeGraphQL } from '../higher_order';
import { monitorListQuery } from '../../queries';
import { MonitorSparkline } from './monitor_sparkline';
import { MonitorListActionsPopover } from './monitor_list_actions_popover';
interface MonitorListQueryResult {
// TODO: clean up this ugly result data shape, there should be no nesting
@ -40,7 +41,10 @@ interface MonitorListQueryResult {
}
interface MonitorListProps {
basePath: string;
dangerColor: string;
dateRangeStart: string;
dateRangeEnd: string;
linkParameters?: string;
}
@ -53,99 +57,139 @@ const monitorListPagination = {
pageSizeOptions: [5, 10, 20, 50],
};
export const MonitorListComponent = ({ dangerColor, data, linkParameters, loading }: Props) => (
<EuiPanel paddingSize="s">
<EuiTitle size="xs">
<h5>
<FormattedMessage
id="xpack.uptime.monitorList.monitoringStatusTitle"
defaultMessage="Monitor status"
/>
</h5>
</EuiTitle>
<EuiSpacer size="s" />
<EuiInMemoryTable
columns={[
{
field: 'ping.monitor.status',
width: '150px',
name: i18n.translate('xpack.uptime.monitorList.statusColumnLabel', {
defaultMessage: 'Status',
}),
render: (status: string, monitor: LatestMonitor) => (
<div>
<EuiHealth
color={status === 'up' ? 'success' : 'danger'}
style={{ display: 'block' }}
>
{status === 'up'
? i18n.translate('xpack.uptime.monitorList.statusColumn.upLabel', {
defaultMessage: 'Up',
})
: i18n.translate('xpack.uptime.monitorList.statusColumn.downLabel', {
defaultMessage: 'Down',
})}
</EuiHealth>
<EuiText size="xs" color="subdued">
{moment(get(monitor, 'ping.monitor.timestamp', undefined)).fromNow()}
</EuiText>
</div>
),
},
{
field: 'ping.monitor.id',
name: i18n.translate('xpack.uptime.monitorList.idColumnLabel', {
defaultMessage: 'ID',
}),
render: (id: string, monitor: LatestMonitor) => (
<EuiLink>
<Link
data-test-subj={`monitor-page-link-${id}`}
to={`/monitor/${id}${linkParameters}`}
>
{monitor.ping && monitor.ping.monitor && monitor.ping.monitor.name
? monitor.ping.monitor.name
: id}
</Link>
</EuiLink>
),
},
{
field: 'ping.url.full',
name: i18n.translate('xpack.uptime.monitorList.urlColumnLabel', {
defaultMessage: 'URL',
}),
render: (url: string, monitor: LatestMonitor) => (
<div>
<EuiLink href={url} target="_blank" color="text">
{url} <EuiIcon size="s" type="popout" color="subdued" />
</EuiLink>
{monitor.ping && monitor.ping.monitor && monitor.ping.monitor.ip ? (
export const MonitorListComponent = ({
basePath,
dangerColor,
dateRangeStart,
dateRangeEnd,
data,
linkParameters,
loading,
}: Props) => {
return (
<EuiPanel paddingSize="s">
<EuiTitle size="xs">
<h5>
<FormattedMessage
id="xpack.uptime.monitorList.monitoringStatusTitle"
defaultMessage="Monitor status"
/>
</h5>
</EuiTitle>
<EuiSpacer size="s" />
<EuiInMemoryTable
columns={[
{
field: 'ping.monitor.status',
width: '150px',
name: i18n.translate('xpack.uptime.monitorList.statusColumnLabel', {
defaultMessage: 'Status',
}),
render: (status: string, monitor: LatestMonitor) => (
<div>
<EuiHealth
color={status === 'up' ? 'success' : 'danger'}
style={{ display: 'block' }}
>
{status === 'up'
? i18n.translate('xpack.uptime.monitorList.statusColumn.upLabel', {
defaultMessage: 'Up',
})
: i18n.translate('xpack.uptime.monitorList.statusColumn.downLabel', {
defaultMessage: 'Down',
})}
</EuiHealth>
<EuiText size="xs" color="subdued">
{monitor.ping.monitor.ip}
{moment(get(monitor, 'ping.monitor.timestamp', undefined)).fromNow()}
</EuiText>
) : null}
</div>
),
},
{
field: 'upSeries',
width: '180px',
align: 'right',
name: i18n.translate('xpack.uptime.monitorList.monitorHistoryColumnLabel', {
defaultMessage: 'Downtime history',
}),
render: (downSeries: MonitorSeriesPoint, monitor: LatestMonitor) => (
<MonitorSparkline dangerColor={dangerColor} monitor={monitor} />
),
},
]}
loading={loading}
items={(data && data.monitorStatus && data.monitorStatus.monitors) || undefined}
pagination={monitorListPagination}
/>
</EuiPanel>
);
</div>
),
},
{
field: 'ping.monitor.id',
name: i18n.translate('xpack.uptime.monitorList.idColumnLabel', {
defaultMessage: 'ID',
}),
render: (id: string, monitor: LatestMonitor) => (
<EuiLink>
<Link
data-test-subj={`monitor-page-link-${id}`}
to={`/monitor/${id}${linkParameters}`}
>
{monitor.ping && monitor.ping.monitor && monitor.ping.monitor.name
? monitor.ping.monitor.name
: id}
</Link>
</EuiLink>
),
},
{
field: 'ping.url.full',
name: i18n.translate('xpack.uptime.monitorList.urlColumnLabel', {
defaultMessage: 'URL',
}),
render: (url: string, monitor: LatestMonitor) => (
<div>
<EuiLink href={url} target="_blank" color="text">
{url} <EuiIcon size="s" type="popout" color="subdued" />
</EuiLink>
</div>
),
},
{
field: 'ping.url.full',
name: i18n.translate('xpack.uptime.monitorList.urlColumnLabel', {
defaultMessage: 'URL',
}),
render: (url: string, monitor: LatestMonitor) => (
<div>
<EuiLink href={url} target="_blank" color="text">
{url} <EuiIcon size="s" type="popout" color="subdued" />
</EuiLink>
{monitor.ping && monitor.ping.monitor && monitor.ping.monitor.ip ? (
<EuiText size="xs" color="subdued">
{monitor.ping.monitor.ip}
</EuiText>
) : null}
</div>
),
},
{
field: 'upSeries',
width: '180px',
align: 'right',
name: i18n.translate('xpack.uptime.monitorList.monitorHistoryColumnLabel', {
defaultMessage: 'Downtime history',
}),
render: (downSeries: MonitorSeriesPoint, monitor: LatestMonitor) => (
<MonitorSparkline dangerColor={dangerColor} monitor={monitor} />
),
},
{
align: 'right',
field: 'ping',
name: i18n.translate('xpack.uptime.monitorList.observabilityIntegrationsColumnLabel', {
defaultMessage: 'Integrations',
description:
'The heading column of some action buttons that will take users to other Obsevability apps',
}),
render: (ping: Ping, monitor: LatestMonitor) => (
<MonitorListActionsPopover
basePath={basePath}
dateRangeStart={dateRangeStart}
dateRangeEnd={dateRangeEnd}
monitor={monitor}
/>
),
},
]}
loading={loading}
items={(data && data.monitorStatus && data.monitorStatus.monitors) || []}
pagination={monitorListPagination}
/>
</EuiPanel>
);
};
export const MonitorList = withUptimeGraphQL<MonitorListQueryResult, MonitorListProps>(
MonitorListComponent,

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 { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiPopover } from '@elastic/eui';
import React, { useState } from 'react';
import { i18n } from '@kbn/i18n';
import { get } from 'lodash';
import { LatestMonitor } from '../../../common/graphql/types';
import { IntegrationLink } from './integration_link';
import { getApmHref } from '../../lib/helper';
interface MonitorListActionsPopoverProps {
basePath: string;
dateRangeStart: string;
dateRangeEnd: string;
monitor: LatestMonitor;
}
export const MonitorListActionsPopover = ({
basePath,
dateRangeStart,
dateRangeEnd,
monitor,
monitor: { ping },
}: MonitorListActionsPopoverProps) => {
const popoverId = `${monitor.id.key}_popover`;
const [popoverIsVisible, setPopoverIsVisible] = useState<boolean>(false);
const domain = get(ping, 'url.domain', '');
return (
<EuiPopover
button={
<EuiButtonIcon
aria-label={i18n.translate(
'xpack.uptime.monitorList.observabilityIntegrationsColumn.popoverIconButton.ariaLabel',
{
defaultMessage: 'Opens integrations popover for monitor with url {monitorUrl}',
description:
'A message explaining that this button opens a popover with links to other apps for a given monitor',
values: { monitorUrl: monitor.id.url },
}
)}
color="subdued"
iconType="boxesHorizontal"
onClick={() => setPopoverIsVisible(true)}
/>
}
closePopover={() => setPopoverIsVisible(false)}
id={popoverId}
isOpen={popoverIsVisible}
>
<EuiFlexGroup>
<EuiFlexItem>
<IntegrationLink
ariaLabel={i18n.translate('xpack.uptime.apmIntegrationAction.description', {
defaultMessage: 'Search APM for this monitor',
description:
'This value is shown to users when they hover over an icon that will take them to the APM app.',
})}
href={getApmHref(monitor, basePath, dateRangeStart, dateRangeEnd)}
iconType="apmApp"
message={i18n.translate('xpack.uptime.apmIntegrationAction.text', {
defaultMessage: 'Check APM for domain',
description:
'A message explaining that when the user clicks the associated link, it will navigate to the APM app and search for the selected domain',
})}
tooltipContent={i18n.translate(
'xpack.uptime.monitorList.observabilityIntegrationsColumn.apmIntegrationLink.tooltip',
{
defaultMessage: 'Click here to check APM for the domain "{domain}".',
description:
'A messsage shown in a tooltip explaining that the nested anchor tag will navigate to the APM app and search for the given URL domain.',
values: {
domain,
},
}
)}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPopover>
);
};

View file

@ -5,10 +5,7 @@
*/
import qs from 'querystring';
import {
UptimeUrlParams,
getSupportedUrlParams,
} from '../lib/helper/url_params/get_supported_url_params';
import { UptimeUrlParams, getSupportedUrlParams } from '../lib/helper';
interface Location {
pathname: string;

View file

@ -5,3 +5,5 @@
*/
export { convertMicrosecondsToMilliseconds } from './convert_measurements';
export { getApmHref } from './observability_integration';
export { UptimeUrlParams, getSupportedUrlParams } from './url_params';

View file

@ -0,0 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`getApmHref creates href with base path when present 1`] = `"/foo/app/apm#/services?kuery=url.domain:%20%22www.elastic.co%22&rangeFrom=now-15m&rangeTo=now"`;
exports[`getApmHref does not add a base path or extra slash when base path is undefined 1`] = `"/app/apm#/services?kuery=url.domain:%20%22www.elastic.co%22&rangeFrom=now-15m&rangeTo=now"`;

View file

@ -0,0 +1,36 @@
/*
* 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 { getApmHref } from '../get_apm_href';
import { LatestMonitor } from '../../../../../common/graphql/types';
describe('getApmHref', () => {
let monitor: LatestMonitor;
beforeEach(() => {
monitor = {
id: {
key: 'monitorId',
},
ping: {
timestamp: 'foo',
url: {
domain: 'www.elastic.co',
},
},
};
});
it('creates href with base path when present', () => {
const result = getApmHref(monitor, 'foo', 'now-15m', 'now');
expect(result).toMatchSnapshot();
});
it('does not add a base path or extra slash when base path is undefined', () => {
const result = getApmHref(monitor, '', 'now-15m', 'now');
expect(result).toMatchSnapshot();
});
});

View file

@ -0,0 +1,18 @@
/*
* 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 { get } from 'lodash';
import { LatestMonitor } from '../../../../common/graphql/types';
export const getApmHref = (
monitor: LatestMonitor,
basePath: string,
dateRangeStart: string,
dateRangeEnd: string
) =>
`${basePath && basePath.length ? `/${basePath}` : ''}/app/apm#/services?kuery=${encodeURI(
`url.domain: "${get(monitor, 'ping.url.domain')}"`
)}&rangeFrom=${dateRangeStart}&rangeTo=${dateRangeEnd}`;

View file

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

View file

@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { getSupportedUrlParams } from './get_supported_url_params';
export { UptimeUrlParams, getSupportedUrlParams } from './get_supported_url_params';

View file

@ -80,7 +80,10 @@ export const OverviewPage = ({ basePath, setBreadcrumbs, history, location }: Pr
<Snapshot colors={colors} variables={sharedProps} />
<EuiSpacer size="s" />
<MonitorList
basePath={basePath}
dangerColor={colors.danger}
dateRangeStart={dateRangeStart}
dateRangeEnd={dateRangeEnd}
linkParameters={linkParameters}
variables={sharedProps}
/>

View file

@ -30,6 +30,7 @@ export const monitorListQueryString = `
status
}
url {
domain
full
}
}

View file

@ -12,7 +12,7 @@
"name": "",
"status": "up"
},
"url": { "full": "https://www.google.com/" }
"url": { "domain": "www.google.com", "full": "https://www.google.com/" }
},
"upSeries": [
{ "x": 1548697620000, "y": 74 },
@ -54,7 +54,7 @@
"name": "",
"status": "down"
},
"url": { "full": "http://localhost:12349/" }
"url": { "domain": "localhost", "full": "http://localhost:12349/" }
},
"upSeries": [
{ "x": 1548697620000, "y": null },
@ -96,7 +96,7 @@
"name": "",
"status": "up"
},
"url": { "full": "http://www.google.com/" }
"url": { "domain": "www.google.com", "full": "http://www.google.com/" }
},
"upSeries": [
{ "x": 1548697620000, "y": 58 },
@ -138,7 +138,7 @@
"name": "",
"status": "up"
},
"url": { "full": "https://www.github.com/" }
"url": { "domain": "www.github.com", "full": "https://www.github.com/" }
},
"upSeries": [
{ "x": 1548697620000, "y": 69 },
@ -180,7 +180,7 @@
"name": "",
"status": "down"
},
"url": { "full": "http://www.example.com/" }
"url": { "domain": "www.example.com", "full": "http://www.example.com/" }
},
"upSeries": [
{ "x": 1548697620000, "y": null },
@ -222,7 +222,7 @@
"name": "",
"status": "up"
},
"url": { "full": "https://www.wikipedia.org/" }
"url": { "domain": "www.wikipedia.org", "full": "https://www.wikipedia.org/" }
},
"upSeries": [
{ "x": 1548697620000, "y": 5 },
@ -264,7 +264,7 @@
"name": "",
"status": "up"
},
"url": { "full": "http://www.reddit.com/" }
"url": { "domain": "www.reddit.com", "full": "http://www.reddit.com/" }
},
"upSeries": [
{ "x": 1548697620000, "y": 79 },
@ -306,7 +306,7 @@
"name": "",
"status": "up"
},
"url": { "full": "https://www.elastic.co" }
"url": { "domain": "www.elastic.co", "full": "https://www.elastic.co" }
},
"upSeries": [
{ "x": 1548697620000, "y": 79 },
@ -348,7 +348,7 @@
"name": "",
"status": "up"
},
"url": { "full": "https://news.google.com/" }
"url": { "domain": "news.google.com", "full": "https://news.google.com/" }
},
"upSeries": [
{ "x": 1548697620000, "y": 5 },
@ -390,7 +390,7 @@
"name": "",
"status": "up"
},
"url": { "full": "tcp://localhost:9200" }
"url": { "domain": "localhost", "full": "tcp://localhost:9200" }
},
"upSeries": [
{ "x": 1548697620000, "y": 1 },

View file

@ -12,7 +12,7 @@
"name": "",
"status": "down"
},
"url": { "full": "http://localhost:12349/" }
"url": { "domain": "localhost", "full": "http://localhost:12349/" }
},
"upSeries": [
{ "x": 1548697620000, "y": null },
@ -54,7 +54,7 @@
"name": "",
"status": "down"
},
"url": { "full": "http://www.example.com/" }
"url": { "domain": "www.example.com", "full": "http://www.example.com/" }
},
"upSeries": [
{ "x": 1548697620000, "y": null },

View file

@ -12,7 +12,7 @@
"name": "",
"status": "up"
},
"url": { "full": "https://www.google.com/" }
"url": { "domain": "www.google.com", "full": "https://www.google.com/" }
},
"upSeries": [
{ "x": 1548697620000, "y": 74 },
@ -54,7 +54,7 @@
"name": "",
"status": "up"
},
"url": { "full": "http://www.google.com/" }
"url": { "domain": "www.google.com", "full": "http://www.google.com/" }
},
"upSeries": [
{ "x": 1548697620000, "y": 58 },
@ -96,7 +96,7 @@
"name": "",
"status": "up"
},
"url": { "full": "https://www.github.com/" }
"url": { "domain": "www.github.com", "full": "https://www.github.com/" }
},
"upSeries": [
{ "x": 1548697620000, "y": 69 },
@ -138,7 +138,7 @@
"name": "",
"status": "up"
},
"url": { "full": "https://www.wikipedia.org/" }
"url": { "domain": "www.wikipedia.org", "full": "https://www.wikipedia.org/" }
},
"upSeries": [
{ "x": 1548697620000, "y": 5 },
@ -180,7 +180,7 @@
"name": "",
"status": "up"
},
"url": { "full": "http://www.reddit.com/" }
"url": { "domain": "www.reddit.com", "full": "http://www.reddit.com/" }
},
"upSeries": [
{ "x": 1548697620000, "y": 79 },
@ -222,7 +222,7 @@
"name": "",
"status": "up"
},
"url": { "full": "https://www.elastic.co" }
"url": { "domain": "www.elastic.co", "full": "https://www.elastic.co" }
},
"upSeries": [
{ "x": 1548697620000, "y": 79 },
@ -264,7 +264,7 @@
"name": "",
"status": "up"
},
"url": { "full": "https://news.google.com/" }
"url": { "domain": "news.google.com", "full": "https://news.google.com/" }
},
"upSeries": [
{ "x": 1548697620000, "y": 5 },
@ -306,7 +306,7 @@
"name": "",
"status": "up"
},
"url": { "full": "tcp://localhost:9200" }
"url": { "domain": "localhost", "full": "tcp://localhost:9200" }
},
"upSeries": [
{ "x": 1548697620000, "y": 1 },