[Uptime] Waterfall use different styling for number (#97216) (#100793)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

Co-authored-by: Shahzad <shahzad.muhammad@elastic.co>
This commit is contained in:
Kibana Machine 2021-05-27 12:56:16 -04:00 committed by GitHub
parent 39c2a52abc
commit 3f455e8ef6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 141 additions and 39 deletions

View file

@ -131,17 +131,18 @@ describe('WaterfallChartWrapper', () => {
});
it('opens flyout on sidebar click and closes on flyout close button', async () => {
const { getByText, getAllByText, getByTestId, queryByText, getByRole } = render(
const { getByText, getByTestId, queryByText, getByRole } = render(
<WaterfallChartWrapper total={mockNetworkItems.length} data={mockNetworkItems} />
);
expect(getByText(`1. ${mockNetworkItems[0].url}`)).toBeInTheDocument();
expect(getByText(`${mockNetworkItems[0].url}`)).toBeInTheDocument();
expect(getByText(`1.`)).toBeInTheDocument();
expect(queryByText('Content type')).not.toBeInTheDocument();
expect(queryByText(`${mockNetworkItems[0]?.mimeType}`)).not.toBeInTheDocument();
// open flyout
// selecter matches both button and accessible text. Button is the second element in the array;
const sidebarButton = getAllByText(/1./)[1];
// selector matches both button and accessible text. Button is the second element in the array;
const sidebarButton = getByTestId(`middleTruncatedTextButton1`);
fireEvent.click(sidebarButton);
// check for sample flyout items
@ -163,17 +164,18 @@ describe('WaterfallChartWrapper', () => {
});
it('opens flyout on sidebar click and closes on second sidebar click', async () => {
const { getByText, getAllByText, getByTestId, queryByText } = render(
const { getByText, getByTestId, queryByText } = render(
<WaterfallChartWrapper total={mockNetworkItems.length} data={mockNetworkItems} />
);
expect(getByText(`1. ${mockNetworkItems[0].url}`)).toBeInTheDocument();
expect(getByText(`${mockNetworkItems[0].url}`)).toBeInTheDocument();
expect(getByText(`1.`)).toBeInTheDocument();
expect(queryByText('Content type')).not.toBeInTheDocument();
expect(queryByText(`${mockNetworkItems[0]?.mimeType}`)).not.toBeInTheDocument();
// open flyout
// selecter matches both button and accessible text. Button is the second element in the array;
const sidebarButton = getAllByText(/1./)[1];
// selector matches both button and accessible text. Button is the second element in the array;
const sidebarButton = getByTestId(`middleTruncatedTextButton1`);
fireEvent.click(sidebarButton);
// check for sample flyout items and that the flyout is focused

View file

@ -81,6 +81,8 @@ export const WaterfallChartWrapper: React.FC<Props> = ({ data, total }) => {
);
}, [flyoutData, isFlyoutVisible, onFlyoutClose]);
const highestSideBarIndex = Math.max(...series.map((sr) => sr.x));
const renderSidebarItem: RenderItem<SidebarItem> = useCallback(
(item) => {
return (
@ -88,10 +90,11 @@ export const WaterfallChartWrapper: React.FC<Props> = ({ data, total }) => {
item={item}
renderFilterScreenReaderText={hasFilters && !onlyHighlighted}
onClick={onSidebarClick}
highestIndex={highestSideBarIndex}
/>
);
},
[hasFilters, onlyHighlighted, onSidebarClick]
[hasFilters, onlyHighlighted, onSidebarClick, highestSideBarIndex]
);
useTrackMetric({ app: 'uptime', metric: 'waterfall_chart_view', metricType: METRIC_TYPE.COUNT });

View file

@ -77,7 +77,7 @@ export const WaterfallFlyout = ({
return null;
}
const { url, details, certificates, requestHeaders, responseHeaders } = flyoutData;
const { x, url, details, certificates, requestHeaders, responseHeaders } = flyoutData;
trackMetric({ metric: 'waterfall_flyout', metricType: METRIC_TYPE.CLICK });
@ -93,7 +93,13 @@ export const WaterfallFlyout = ({
<EuiTitle size="s">
<h2 id="flyoutTitle">
<EuiFlexItem>
<MiddleTruncatedText text={url} url={url} ariaLabel={url} />
<MiddleTruncatedText
index={x + 1}
text={url}
url={url}
ariaLabel={url}
highestIndex={x + 1}
/>
</EuiFlexItem>
</h2>
</EuiTitle>

View file

@ -13,9 +13,10 @@ import { SidebarItem } from '../waterfall/types';
import { render } from '../../../../../lib/helper/rtl_helpers';
import { WaterfallSidebarItem } from './waterfall_sidebar_item';
import { SIDEBAR_FILTER_MATCHES_SCREENREADER_LABEL } from '../../waterfall/components/translations';
import { getChunks } from '../../waterfall/components/middle_truncated_text';
describe('waterfall filter', () => {
const url = 'http://www.elastic.co';
const url = 'http://www.elastic.co/observability/uptime';
const index = 0;
const offsetIndex = index + 1;
const item: SidebarItem = {
@ -25,26 +26,34 @@ describe('waterfall filter', () => {
offsetIndex,
};
it('renders sidbar item', () => {
const { getByText } = render(<WaterfallSidebarItem item={item} />);
it('renders sidebar item', () => {
const { getByText } = render(<WaterfallSidebarItem item={item} highestIndex={10} />);
expect(getByText(`${offsetIndex}. ${url}`));
const chunks = getChunks(url.replace('http://www.', ''));
expect(getByText(`${offsetIndex}. ${chunks.first}`));
expect(getByText(`${chunks.last}`));
});
it('render screen reader text when renderFilterScreenReaderText is true', () => {
const { getByLabelText } = render(
<WaterfallSidebarItem item={item} renderFilterScreenReaderText={true} />
<WaterfallSidebarItem item={item} renderFilterScreenReaderText={true} highestIndex={10} />
);
expect(
getByLabelText(`${SIDEBAR_FILTER_MATCHES_SCREENREADER_LABEL} ${offsetIndex}. ${url}`)
getByLabelText(`${SIDEBAR_FILTER_MATCHES_SCREENREADER_LABEL} ${url}`)
).toBeInTheDocument();
});
it('does not render screen reader text when renderFilterScreenReaderText is false', () => {
const onClick = jest.fn();
const { getByRole } = render(
<WaterfallSidebarItem item={item} renderFilterScreenReaderText={false} onClick={onClick} />
<WaterfallSidebarItem
item={item}
renderFilterScreenReaderText={false}
onClick={onClick}
highestIndex={10}
/>
);
const button = getByRole('button');
fireEvent.click(button);

View file

@ -17,10 +17,12 @@ interface SidebarItemProps {
item: SidebarItem;
renderFilterScreenReaderText?: boolean;
onClick?: OnSidebarClick;
highestIndex: number;
}
export const WaterfallSidebarItem = ({
item,
highestIndex,
renderFilterScreenReaderText,
onClick,
}: SidebarItemProps) => {
@ -42,7 +44,8 @@ export const WaterfallSidebarItem = ({
return is400 || is500 || isSpecific300;
};
const text = `${offsetIndex}. ${item.url}`;
const text = item.url;
const ariaLabel = `${
isHighlighted && renderFilterScreenReaderText
? `${SIDEBAR_FILTER_MATCHES_SCREENREADER_LABEL} `
@ -58,11 +61,13 @@ export const WaterfallSidebarItem = ({
<EuiFlexGroup>
<EuiFlexItem grow={false} style={{ minWidth: 0 }}>
<MiddleTruncatedText
index={offsetIndex}
text={text}
url={url}
ariaLabel={ariaLabel}
onClick={handleSidebarClick}
setButtonRef={setRef}
highestIndex={highestIndex}
/>
</EuiFlexItem>
</EuiFlexGroup>
@ -70,11 +75,13 @@ export const WaterfallSidebarItem = ({
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem grow={false} style={{ minWidth: 0 }}>
<MiddleTruncatedText
index={offsetIndex}
text={text}
url={url}
ariaLabel={ariaLabel}
onClick={handleSidebarClick}
setButtonRef={setRef}
highestIndex={highestIndex}
/>
</EuiFlexItem>
<EuiFlexItem component="span" grow={false}>

View file

@ -5,9 +5,10 @@
* 2.0.
*/
import { getChunks, MiddleTruncatedText } from './middle_truncated_text';
import { render, within, fireEvent, waitFor } from '@testing-library/react';
import React from 'react';
import { within, fireEvent, waitFor } from '@testing-library/react';
import { getChunks, MiddleTruncatedText } from './middle_truncated_text';
import { render } from '../../../../../lib/helper/rtl_helpers';
const longString =
'this-is-a-really-really-really-really-really-really-really-really-long-string.madeup.extension';
@ -28,7 +29,14 @@ describe('Component', () => {
const url = 'http://www.elastic.co';
it('renders truncated text and aria label', () => {
const { getByText, getByLabelText } = render(
<MiddleTruncatedText text={longString} ariaLabel={longString} url={url} />
<MiddleTruncatedText
index={1}
text={longString}
ariaLabel={longString}
url={url}
onClick={jest.fn()}
highestIndex={10}
/>
);
expect(getByText(first)).toBeInTheDocument();
@ -39,7 +47,13 @@ describe('Component', () => {
it('renders screen reader only text', () => {
const { getByTestId } = render(
<MiddleTruncatedText text={longString} ariaLabel={longString} url={url} />
<MiddleTruncatedText
index={1}
text={longString}
ariaLabel={longString}
url={url}
highestIndex={10}
/>
);
const { getByText } = within(getByTestId('middleTruncatedTextSROnly'));
@ -49,7 +63,13 @@ describe('Component', () => {
it('renders external link', () => {
const { getByText } = render(
<MiddleTruncatedText text={longString} ariaLabel={longString} url={url} />
<MiddleTruncatedText
index={1}
text={longString}
ariaLabel={longString}
url={url}
highestIndex={10}
/>
);
const link = getByText('Open resource in new tab').closest('a');
@ -61,13 +81,15 @@ describe('Component', () => {
const handleClick = jest.fn();
const { getByTestId } = render(
<MiddleTruncatedText
index={1}
text={longString}
ariaLabel={longString}
url={url}
onClick={handleClick}
highestIndex={10}
/>
);
const button = getByTestId('middleTruncatedTextButton');
const button = getByTestId('middleTruncatedTextButton1');
fireEvent.click(button);
await waitFor(() => {

View file

@ -6,12 +6,22 @@
*/
import React, { useMemo } from 'react';
import styled from 'styled-components';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiScreenReaderOnly, EuiToolTip, EuiButtonEmpty, EuiLink } from '@elastic/eui';
import {
EuiScreenReaderOnly,
EuiToolTip,
EuiButtonEmpty,
EuiLink,
EuiText,
EuiIcon,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FIXED_AXIS_HEIGHT } from './constants';
import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common';
interface Props {
index: number;
highestIndex: number;
ariaLabel: string;
text: string;
onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
@ -19,7 +29,7 @@ interface Props {
url: string;
}
const OuterContainer = styled.span`
const OuterContainer = euiStyled.span`
position: relative;
display: inline-flex;
align-items: center;
@ -28,13 +38,21 @@ const OuterContainer = styled.span`
}
`; // NOTE: min-width: 0 ensures flexbox and no-wrap children can co-exist
const InnerContainer = styled.span`
const InnerContainer = euiStyled.span`
overflow: hidden;
display: flex;
align-items: center;
`;
const FirstChunk = styled.span`
const IndexNumber = euiStyled(EuiText)`
font-family: ${(props) => props.theme.eui.euiCodeFontFamily};
margin-right: ${(props) => props.theme.eui.euiSizeXS};
line-height: ${FIXED_AXIS_HEIGHT}px;
text-align: right;
background-color: ${(props) => props.theme.eui.euiColorLightestShade};
`;
const FirstChunk = euiStyled.span`
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
@ -42,13 +60,13 @@ const FirstChunk = styled.span`
text-align: left;
`; // safari doesn't auto align text left in some cases
const LastChunk = styled.span`
const LastChunk = euiStyled.span`
flex-shrink: 0;
line-height: ${FIXED_AXIS_HEIGHT}px;
text-align: left;
`; // safari doesn't auto align text left in some cases
const StyledButton = styled(EuiButtonEmpty)`
const StyledButton = euiStyled(EuiButtonEmpty)`
&&& {
border: none;
@ -59,6 +77,10 @@ const StyledButton = styled(EuiButtonEmpty)`
}
`;
const SecureIcon = euiStyled(EuiIcon)`
margin-right: ${(props) => props.theme.eui.euiSizeXS};
`;
export const getChunks = (text: string = '') => {
const END_CHARS = 12;
const chars = text.split('');
@ -70,7 +92,18 @@ export const getChunks = (text: string = '') => {
// Helper component for adding middle text truncation, e.g.
// really-really-really-long....ompressed.js
// Can be used to accomodate content in sidebar item rendering.
export const MiddleTruncatedText = ({ ariaLabel, text, onClick, setButtonRef, url }: Props) => {
export const MiddleTruncatedText = ({
index,
ariaLabel,
text: fullText,
onClick,
setButtonRef,
url,
highestIndex,
}: Props) => {
const secureHttps = fullText.startsWith('https://');
const text = fullText.replace(/https:\/\/www.|http:\/\/www.|http:\/\/|https:\/\//, '');
const chunks = useMemo(() => {
return getChunks(text);
}, [text]);
@ -78,24 +111,44 @@ export const MiddleTruncatedText = ({ ariaLabel, text, onClick, setButtonRef, ur
return (
<OuterContainer aria-label={ariaLabel} data-test-subj="middleTruncatedTextContainer">
<EuiScreenReaderOnly>
<span data-test-subj="middleTruncatedTextSROnly">{text}</span>
<span data-test-subj="middleTruncatedTextSROnly">{fullText}</span>
</EuiScreenReaderOnly>
<EuiToolTip content={text} position="top" data-test-subj="middleTruncatedTextToolTip">
<EuiToolTip content={fullText} position="top" data-test-subj="middleTruncatedTextToolTip">
<>
{onClick ? (
<StyledButton
onClick={onClick}
data-test-subj="middleTruncatedTextButton"
data-test-subj={`middleTruncatedTextButton${index}`}
buttonRef={setButtonRef}
flush={'left'}
>
<InnerContainer>
<IndexNumber
color="subdued"
size="s"
style={{ minWidth: String(highestIndex).length + 1 + 'ch' }}
>
{index + '.'}
</IndexNumber>
{secureHttps && (
<SecureIcon
type="lock"
size="s"
color="secondary"
aria-label={i18n.translate('xpack.uptime.waterfallChart.sidebar.url.https', {
defaultMessage: 'https',
})}
/>
)}
<FirstChunk>{chunks.first}</FirstChunk>
<LastChunk>{chunks.last}</LastChunk>
</InnerContainer>
</StyledButton>
) : (
<InnerContainer aria-hidden={true}>
<FirstChunk>{chunks.first}</FirstChunk>
<FirstChunk>
{index}. {chunks.first}
</FirstChunk>
<LastChunk>{chunks.last}</LastChunk>
</InnerContainer>
)}

View file

@ -90,6 +90,7 @@ export const WaterfallChartSidebarWrapper = euiStyled(EuiFlexItem)`
export const WaterfallChartSidebarContainer = euiStyled.div<WaterfallChartSidebarContainer>`
height: ${(props) => `${props.height}px`};
overflow-y: hidden;
overflow-x: hidden;
`;
export const WaterfallChartSidebarContainerInnerPanel: StyledComponent<
@ -107,8 +108,7 @@ export const WaterfallChartSidebarContainerFlexGroup = euiStyled(EuiFlexGroup)`
// Ensures flex items honour no-wrap of children, rather than trying to extend to the full width of children.
export const WaterfallChartSidebarFlexItem = euiStyled(EuiFlexItem)`
min-width: 0;
padding-left: ${(props) => props.theme.eui.paddingSizes.m};
padding-right: ${(props) => props.theme.eui.paddingSizes.m};
padding-right: ${(props) => props.theme.eui.paddingSizes.s};
justify-content: space-around;
`;