[Uptime] Show URL and metrics on sidebar and waterfall item tooltips (#99985)

* Add URL to metrics tooltip.

* Add screenreader label for URL container.

* Add metrics to URL sidebar tooltip.

* Rename vars.

* Delete unnecessary code.

* Undo rename.

* Extract component to dedicated file, add tests.

* Fix error in test.

* Add offset index to heading of waterfall chart tooltip.

* Format the waterfall tool tip header.

* Add horizontal rule and bold text for waterfall tooltip.

* Extract inline helper function to module-level for reuse.

* Reuse waterfall tooltip style.

* Style reusable tooltip content.

* Adapt existing chart tooltip to use tooltip content component for better consistency.

* Delete test code.

* Style EUI tooltip arrow.

* Revert whitespace change.

* Delete obsolete test.

* Implement and use common tooltip heading formatter function.

* Add tests for new formatter function.

* Fix a typo.

* Add a comment explaining a style hack.

* Add optional chaining to avoid breaking a test.

* Revert previous change, use RTL wrapper, rename describe block.

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Justin Kambic 2021-06-04 08:48:35 -04:00 committed by GitHub
parent aa8aa7f23d
commit 8f83090d74
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 180 additions and 21 deletions

View file

@ -7,6 +7,7 @@
import moment from 'moment';
import {
colourPalette,
formatTooltipHeading,
getConnectingTime,
getSeriesAndDomain,
getSidebarItems,
@ -729,3 +730,13 @@ describe('getSidebarItems', () => {
expect(actual[0].offsetIndex).toBe(1);
});
});
describe('formatTooltipHeading', () => {
it('puts index and URL text together', () => {
expect(formatTooltipHeading(1, 'http://www.elastic.co/')).toEqual('1. http://www.elastic.co/');
});
it('returns only the text if `index` is NaN', () => {
expect(formatTooltipHeading(NaN, 'http://www.elastic.co/')).toEqual('http://www.elastic.co/');
});
});

View file

@ -450,3 +450,6 @@ const MIME_TYPE_PALETTE = buildMimeTypePalette();
type ColourPalette = TimingColourPalette & MimeTypeColourPalette;
export const colourPalette: ColourPalette = { ...TIMING_PALETTE, ...MIME_TYPE_PALETTE };
export const formatTooltipHeading = (index: number, fullText: string): string =>
isNaN(index) ? fullText : `${index}. ${fullText}`;

View file

@ -8,17 +8,19 @@
import React, { useMemo } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiButtonEmpty,
EuiScreenReaderOnly,
EuiToolTip,
EuiButtonEmpty,
EuiLink,
EuiText,
EuiIcon,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { WaterfallTooltipContent } from './waterfall_tooltip_content';
import { WaterfallTooltipResponsiveMaxWidth } from './styles';
import { FIXED_AXIS_HEIGHT } from './constants';
import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common';
import { formatTooltipHeading } from '../../step_detail/waterfall/data_formatting';
interface Props {
index: number;
@ -116,7 +118,9 @@ export const MiddleTruncatedText = ({
</EuiScreenReaderOnly>
<WaterfallTooltipResponsiveMaxWidth
as={EuiToolTip}
content={`${index}. ${fullText}`}
content={
<WaterfallTooltipContent {...{ text: formatTooltipHeading(index, fullText), url }} />
}
data-test-subj="middleTruncatedTextToolTip"
delay="long"
position="top"

View file

@ -153,6 +153,9 @@ export const WaterfallChartTooltip = euiStyled(WaterfallTooltipResponsiveMaxWidt
border-radius: ${(props) => props.theme.eui.euiBorderRadius};
color: ${(props) => props.theme.eui.euiColorLightestShade};
padding: ${(props) => props.theme.eui.paddingSizes.s};
.euiToolTip__arrow {
background-color: ${(props) => props.theme.eui.euiColorDarkestShade};
}
`;
export const NetworkRequestsTotalStyle = euiStyled(EuiText)`

View file

@ -18,11 +18,12 @@ import {
TickFormatter,
TooltipInfo,
} from '@elastic/charts';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { BAR_HEIGHT } from './constants';
import { useChartTheme } from '../../../../../hooks/use_chart_theme';
import { WaterfallChartChartContainer, WaterfallChartTooltip } from './styles';
import { useWaterfallContext, WaterfallData } from '..';
import { WaterfallTooltipContent } from './waterfall_tooltip_content';
import { formatTooltipHeading } from '../../step_detail/waterfall/data_formatting';
const getChartHeight = (data: WaterfallData): number => {
// We get the last item x(number of bars) and adds 1 to cater for 0 index
@ -32,23 +33,25 @@ const getChartHeight = (data: WaterfallData): number => {
};
const Tooltip = (tooltipInfo: TooltipInfo) => {
const { data, renderTooltipItem } = useWaterfallContext();
const relevantItems = data.filter((item) => {
return (
item.x === tooltipInfo.header?.value && item.config.showTooltip && item.config.tooltipProps
);
});
return relevantItems.length ? (
<WaterfallChartTooltip>
<EuiFlexGroup direction="column" gutterSize="none">
{relevantItems.map((item, index) => {
return (
<EuiFlexItem key={index}>{renderTooltipItem(item.config.tooltipProps)}</EuiFlexItem>
);
})}
</EuiFlexGroup>
</WaterfallChartTooltip>
) : null;
const { data, sidebarItems } = useWaterfallContext();
return useMemo(() => {
const sidebarItem = sidebarItems?.find((item) => item.index === tooltipInfo.header?.value);
const relevantItems = data.filter((item) => {
return (
item.x === tooltipInfo.header?.value && item.config.showTooltip && item.config.tooltipProps
);
});
return relevantItems.length ? (
<WaterfallChartTooltip>
{sidebarItem && (
<WaterfallTooltipContent
text={formatTooltipHeading(sidebarItem.index + 1, sidebarItem.url)}
url={sidebarItem.url}
/>
)}
</WaterfallChartTooltip>
) : null;
}, [data, sidebarItems, tooltipInfo.header?.value]);
};
interface Props {
@ -82,7 +85,12 @@ export const WaterfallBarChart = ({
<Settings
showLegend={false}
rotation={90}
tooltip={{ customTooltip: Tooltip }}
tooltip={{
// this is done to prevent the waterfall tooltip from rendering behind Kibana's
// stacked header when the user highlights an item at the top of the chart
boundary: document.getElementById('app-fixed-viewport') ?? undefined,
customTooltip: Tooltip,
}}
theme={theme}
onProjectionClick={handleProjectionClick}
onElementClick={handleElementClick}

View file

@ -0,0 +1,84 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { render } from '../../../../../lib/helper/rtl_helpers';
import { WaterfallTooltipContent } from './waterfall_tooltip_content';
jest.mock('../context/waterfall_chart', () => ({
useWaterfallContext: jest.fn().mockReturnValue({
data: [
{
x: 0,
config: {
url: 'https://www.elastic.co',
tooltipProps: {
colour: '#000000',
value: 'test-val',
},
showTooltip: true,
},
},
{
x: 0,
config: {
url: 'https://www.elastic.co/with/missing/tooltip.props',
showTooltip: true,
},
},
{
x: 1,
config: {
url: 'https://www.elastic.co/someresource.path',
tooltipProps: {
colour: '#010000',
value: 'test-val-missing',
},
showTooltip: true,
},
},
],
renderTooltipItem: (props: any) => (
<div aria-label="tooltip item">
<div>{props.colour}</div>
<div>{props.value}</div>
</div>
),
sidebarItems: [
{
isHighlighted: true,
index: 0,
offsetIndex: 1,
url: 'https://www.elastic.co',
status: 200,
method: 'GET',
},
],
}),
}));
describe('WaterfallTooltipContent', () => {
it('renders tooltip', () => {
const { getByText, queryByText } = render(
<WaterfallTooltipContent text="1. https://www.elastic.co" url="https://www.elastic.co" />
);
expect(getByText('#000000')).toBeInTheDocument();
expect(getByText('test-val')).toBeInTheDocument();
expect(getByText('1. https://www.elastic.co')).toBeInTheDocument();
expect(queryByText('#010000')).toBeNull();
expect(queryByText('test-val-missing')).toBeNull();
});
it(`doesn't render metric if tooltip props missing`, () => {
const { getAllByLabelText, getByText } = render(
<WaterfallTooltipContent text="1. https://www.elastic.co" url="https://www.elastic.co" />
);
const metricElements = getAllByLabelText('tooltip item');
expect(metricElements).toHaveLength(1);
expect(getByText('test-val')).toBeInTheDocument();
});
});

View file

@ -0,0 +1,46 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiText } from '@elastic/eui';
import { useWaterfallContext } from '../context/waterfall_chart';
import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common';
interface Props {
text: string;
url: string;
}
const StyledText = euiStyled(EuiText)`
font-weight: bold;
`;
const StyledHorizontalRule = euiStyled(EuiHorizontalRule)`
background-color: ${(props) => props.theme.eui.euiColorDarkShade};
`;
export const WaterfallTooltipContent: React.FC<Props> = ({ text, url }) => {
const { data, renderTooltipItem, sidebarItems } = useWaterfallContext();
const tooltipMetrics = data.filter(
(datum) =>
datum.x === sidebarItems?.find((sidebarItem) => sidebarItem.url === url)?.index &&
datum.config.tooltipProps &&
datum.config.showTooltip
);
return (
<>
<StyledText>{text}</StyledText>
<StyledHorizontalRule margin="none" />
<EuiFlexGroup direction="column" gutterSize="none">
{tooltipMetrics.map((item, idx) => (
<EuiFlexItem key={idx}>{renderTooltipItem(item.config.tooltipProps)}</EuiFlexItem>
))}
</EuiFlexGroup>
</>
);
};