mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
Waterfall layout and expansion fixes (#114889)
Fix according toggling behavior on trace waterfall. Previously clicking on any item would collapse the whole waterfall, now it just collapses the one you clicked on. Truncate spans so ones with very long names don't overflow. Make the left margin relative to the max level of depth so waterfalls with deep trees don't overflow.
This commit is contained in:
parent
26d42523c5
commit
db53a79cc4
8 changed files with 6019 additions and 2344 deletions
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { EuiAccordion, EuiAccordionProps } from '@elastic/eui';
|
||||
import { isEmpty } from 'lodash';
|
||||
import React, { useState } from 'react';
|
||||
import React, { Dispatch, SetStateAction, useState } from 'react';
|
||||
import { euiStyled } from '../../../../../../../../../../src/plugins/kibana_react/common';
|
||||
import { Margins } from '../../../../../shared/charts/Timeline';
|
||||
import { WaterfallItem } from './waterfall_item';
|
||||
|
@ -22,8 +22,8 @@ interface AccordionWaterfallProps {
|
|||
level: number;
|
||||
duration: IWaterfall['duration'];
|
||||
waterfallItemId?: string;
|
||||
setMaxLevel: Dispatch<SetStateAction<number>>;
|
||||
waterfall: IWaterfall;
|
||||
onToggleEntryTransaction?: () => void;
|
||||
timelineMargins: Margins;
|
||||
onClickWaterfallItem: (item: IWaterfallSpanOrTransaction) => void;
|
||||
}
|
||||
|
@ -97,12 +97,13 @@ export function AccordionWaterfall(props: AccordionWaterfallProps) {
|
|||
duration,
|
||||
waterfall,
|
||||
waterfallItemId,
|
||||
setMaxLevel,
|
||||
timelineMargins,
|
||||
onClickWaterfallItem,
|
||||
onToggleEntryTransaction,
|
||||
} = props;
|
||||
|
||||
const nextLevel = level + 1;
|
||||
setMaxLevel(nextLevel);
|
||||
|
||||
const children = waterfall.childrenByParentId[item.id] || [];
|
||||
const errorCount = waterfall.getErrorCount(item.id);
|
||||
|
@ -139,9 +140,6 @@ export function AccordionWaterfall(props: AccordionWaterfallProps) {
|
|||
forceState={isOpen ? 'open' : 'closed'}
|
||||
onToggle={() => {
|
||||
setIsOpen((isCurrentOpen) => !isCurrentOpen);
|
||||
if (onToggleEntryTransaction) {
|
||||
onToggleEntryTransaction();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children.map((child) => (
|
||||
|
|
|
@ -29,13 +29,6 @@ const Container = euiStyled.div`
|
|||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const TIMELINE_MARGINS = {
|
||||
top: 40,
|
||||
left: 100,
|
||||
right: 50,
|
||||
bottom: 0,
|
||||
};
|
||||
|
||||
const toggleFlyout = ({
|
||||
history,
|
||||
item,
|
||||
|
@ -72,6 +65,16 @@ export function Waterfall({ waterfall, waterfallItemId }: Props) {
|
|||
const agentMarks = getAgentMarks(waterfall.entryWaterfallTransaction?.doc);
|
||||
const errorMarks = getErrorMarks(waterfall.errorItems);
|
||||
|
||||
// Calculate the left margin relative to the deepest level, or 100px, whichever
|
||||
// is more.
|
||||
const [maxLevel, setMaxLevel] = useState(0);
|
||||
const timelineMargins = {
|
||||
top: 40,
|
||||
left: Math.max(100, maxLevel * 10),
|
||||
right: 50,
|
||||
bottom: 0,
|
||||
};
|
||||
|
||||
return (
|
||||
<HeightRetainer>
|
||||
<Container>
|
||||
|
@ -99,7 +102,7 @@ export function Waterfall({ waterfall, waterfallItemId }: Props) {
|
|||
marks={[...agentMarks, ...errorMarks]}
|
||||
xMax={duration}
|
||||
height={waterfallHeight}
|
||||
margins={TIMELINE_MARGINS}
|
||||
margins={timelineMargins}
|
||||
/>
|
||||
</div>
|
||||
<WaterfallItemsContainer>
|
||||
|
@ -110,16 +113,14 @@ export function Waterfall({ waterfall, waterfallItemId }: Props) {
|
|||
isOpen={isAccordionOpen}
|
||||
item={waterfall.entryWaterfallTransaction}
|
||||
level={0}
|
||||
setMaxLevel={setMaxLevel}
|
||||
waterfallItemId={waterfallItemId}
|
||||
duration={duration}
|
||||
waterfall={waterfall}
|
||||
timelineMargins={TIMELINE_MARGINS}
|
||||
timelineMargins={timelineMargins}
|
||||
onClickWaterfallItem={(item: IWaterfallItem) =>
|
||||
toggleFlyout({ history, item })
|
||||
}
|
||||
onToggleEntryTransaction={() =>
|
||||
setIsAccordionOpen((isOpen) => !isOpen)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</WaterfallItemsContainer>
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
} from '../../../../../../../common/elasticsearch_fieldnames';
|
||||
import { asDuration } from '../../../../../../../common/utils/formatters';
|
||||
import { Margins } from '../../../../../shared/charts/Timeline';
|
||||
import { TruncateWithTooltip } from '../../../../../shared/truncate_with_tooltip';
|
||||
import { SyncBadge } from './sync_badge';
|
||||
import { IWaterfallSpanOrTransaction } from './waterfall_helpers/waterfall_helpers';
|
||||
import { FailureBadge } from './failure_badge';
|
||||
|
@ -67,6 +68,7 @@ const ItemText = euiStyled.span`
|
|||
display: flex;
|
||||
align-items: center;
|
||||
height: ${({ theme }) => theme.eui.euiSizeL};
|
||||
max-width: 100%;
|
||||
|
||||
/* add margin to all direct descendants */
|
||||
& > * {
|
||||
|
@ -160,7 +162,11 @@ function NameLabel({ item }: { item: IWaterfallSpanOrTransaction }) {
|
|||
: '';
|
||||
name = `${item.doc.span.composite.count}${compositePrefix} ${name}`;
|
||||
}
|
||||
return <EuiText size="s">{name}</EuiText>;
|
||||
return (
|
||||
<EuiText style={{ overflow: 'hidden' }} size="s">
|
||||
<TruncateWithTooltip content={name} text={name} />
|
||||
</EuiText>
|
||||
);
|
||||
case 'transaction':
|
||||
return (
|
||||
<EuiTitle size="xxs">
|
||||
|
|
|
@ -1,56 +0,0 @@
|
|||
/*
|
||||
* 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, { ComponentType } from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { MockApmPluginContextWrapper } from '../../../../../context/apm_plugin/mock_apm_plugin_context';
|
||||
import { WaterfallContainer } from './index';
|
||||
import { getWaterfall } from './Waterfall/waterfall_helpers/waterfall_helpers';
|
||||
import {
|
||||
inferredSpans,
|
||||
simpleTrace,
|
||||
traceChildStartBeforeParent,
|
||||
traceWithErrors,
|
||||
urlParams,
|
||||
} from './waterfallContainer.stories.data';
|
||||
|
||||
export default {
|
||||
title: 'app/TransactionDetails/Waterfall',
|
||||
component: WaterfallContainer,
|
||||
decorators: [
|
||||
(Story: ComponentType) => (
|
||||
<MemoryRouter>
|
||||
<MockApmPluginContextWrapper>
|
||||
<Story />
|
||||
</MockApmPluginContextWrapper>
|
||||
</MemoryRouter>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export function Example() {
|
||||
const waterfall = getWaterfall(simpleTrace, '975c8d5bfd1dd20b');
|
||||
return <WaterfallContainer urlParams={urlParams} waterfall={waterfall} />;
|
||||
}
|
||||
|
||||
export function WithErrors() {
|
||||
const waterfall = getWaterfall(traceWithErrors, '975c8d5bfd1dd20b');
|
||||
return <WaterfallContainer urlParams={urlParams} waterfall={waterfall} />;
|
||||
}
|
||||
|
||||
export function ChildStartsBeforeParent() {
|
||||
const waterfall = getWaterfall(
|
||||
traceChildStartBeforeParent,
|
||||
'975c8d5bfd1dd20b'
|
||||
);
|
||||
return <WaterfallContainer urlParams={urlParams} waterfall={waterfall} />;
|
||||
}
|
||||
|
||||
export function InferredSpans() {
|
||||
const waterfall = getWaterfall(inferredSpans, 'f2387d37260d00bd');
|
||||
return <WaterfallContainer urlParams={urlParams} waterfall={waterfall} />;
|
||||
}
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* 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 { Meta, Story } from '@storybook/react';
|
||||
import React, { ComponentProps } from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { MockApmPluginContextWrapper } from '../../../../../context/apm_plugin/mock_apm_plugin_context';
|
||||
import { WaterfallContainer } from './index';
|
||||
import { getWaterfall } from './Waterfall/waterfall_helpers/waterfall_helpers';
|
||||
import {
|
||||
inferredSpans,
|
||||
manyChildrenWithSameLength,
|
||||
simpleTrace,
|
||||
traceChildStartBeforeParent,
|
||||
traceWithErrors,
|
||||
urlParams as testUrlParams,
|
||||
} from './waterfall_container.stories.data';
|
||||
|
||||
type Args = ComponentProps<typeof WaterfallContainer>;
|
||||
|
||||
const stories: Meta<Args> = {
|
||||
title: 'app/TransactionDetails/Waterfall',
|
||||
component: WaterfallContainer,
|
||||
decorators: [
|
||||
(StoryComponent) => (
|
||||
<MemoryRouter
|
||||
initialEntries={[
|
||||
'/services/{serviceName}/transactions/view?rangeFrom=now-15m&rangeTo=now&transactionName=testTransactionName',
|
||||
]}
|
||||
>
|
||||
<MockApmPluginContextWrapper>
|
||||
<StoryComponent />
|
||||
</MockApmPluginContextWrapper>
|
||||
</MemoryRouter>
|
||||
),
|
||||
],
|
||||
};
|
||||
export default stories;
|
||||
|
||||
export const Example: Story<Args> = ({ urlParams, waterfall }) => {
|
||||
return <WaterfallContainer urlParams={urlParams} waterfall={waterfall} />;
|
||||
};
|
||||
Example.args = {
|
||||
urlParams: testUrlParams,
|
||||
waterfall: getWaterfall(simpleTrace, '975c8d5bfd1dd20b'),
|
||||
};
|
||||
|
||||
export const WithErrors: Story<Args> = ({ urlParams, waterfall }) => {
|
||||
return <WaterfallContainer urlParams={urlParams} waterfall={waterfall} />;
|
||||
};
|
||||
WithErrors.args = {
|
||||
urlParams: testUrlParams,
|
||||
waterfall: getWaterfall(traceWithErrors, '975c8d5bfd1dd20b'),
|
||||
};
|
||||
|
||||
export const ChildStartsBeforeParent: Story<Args> = ({
|
||||
urlParams,
|
||||
waterfall,
|
||||
}) => {
|
||||
return <WaterfallContainer urlParams={urlParams} waterfall={waterfall} />;
|
||||
};
|
||||
ChildStartsBeforeParent.args = {
|
||||
urlParams: testUrlParams,
|
||||
waterfall: getWaterfall(traceChildStartBeforeParent, '975c8d5bfd1dd20b'),
|
||||
};
|
||||
|
||||
export const InferredSpans: Story<Args> = ({ urlParams, waterfall }) => {
|
||||
return <WaterfallContainer urlParams={urlParams} waterfall={waterfall} />;
|
||||
};
|
||||
InferredSpans.args = {
|
||||
urlParams: testUrlParams,
|
||||
waterfall: getWaterfall(inferredSpans, 'f2387d37260d00bd'),
|
||||
};
|
||||
|
||||
export const ManyChildrenWithSameLength: Story<Args> = ({
|
||||
urlParams,
|
||||
waterfall,
|
||||
}) => {
|
||||
return <WaterfallContainer urlParams={urlParams} waterfall={waterfall} />;
|
||||
};
|
||||
ManyChildrenWithSameLength.args = {
|
||||
urlParams: testUrlParams,
|
||||
waterfall: getWaterfall(manyChildrenWithSameLength, '9a7f717439921d39'),
|
||||
};
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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 { composeStories } from '@storybook/testing-react';
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { disableConsoleWarning } from '../../../../../utils/testHelpers';
|
||||
import * as stories from './waterfall_container.stories';
|
||||
|
||||
const { Example } = composeStories(stories);
|
||||
|
||||
describe('WaterfallContainer', () => {
|
||||
let consoleMock: jest.SpyInstance;
|
||||
|
||||
beforeAll(() => {
|
||||
consoleMock = disableConsoleWarning('Warning: componentWillReceiveProps');
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
consoleMock.mockRestore();
|
||||
});
|
||||
|
||||
it('renders', () => {
|
||||
expect(() => render(<Example />)).not.toThrowError();
|
||||
});
|
||||
|
||||
it('expands and contracts the accordion', () => {
|
||||
const { getAllByRole } = render(<Example />);
|
||||
const buttons = getAllByRole('button');
|
||||
const parentItem = buttons[2];
|
||||
const childItem = buttons[3];
|
||||
|
||||
parentItem.click();
|
||||
|
||||
expect(parentItem).toHaveAttribute('aria-expanded', 'false');
|
||||
expect(childItem).toHaveAttribute('aria-expanded', 'true');
|
||||
});
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue