[APM] Implement nest level expand/collapse toggle for each span row (#75259)
* returning an waterfallTransaction * fixing style * fixing unit test * fixing style * addressing PR comment * addressing PR comment Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
c3b6745e3d
commit
75232a74f3
|
@ -40,7 +40,6 @@ const Container = styled.div<IContainerStyleProps>`
|
|||
padding-bottom: ${px(units.plus)};
|
||||
margin-right: ${(props) => px(props.timelineMargins.right)};
|
||||
margin-left: ${(props) => px(props.timelineMargins.left)};
|
||||
border-top: 1px solid ${({ theme }) => theme.eui.euiColorLightShade};
|
||||
background-color: ${({ isSelected, theme }) =>
|
||||
isSelected ? theme.eui.euiColorLightestShade : 'initial'};
|
||||
cursor: pointer;
|
||||
|
@ -191,7 +190,10 @@ export function WaterfallItem({
|
|||
type={item.docType}
|
||||
timelineMargins={timelineMargins}
|
||||
isSelected={isSelected}
|
||||
onClick={onClick}
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
onClick();
|
||||
}}
|
||||
>
|
||||
<ItemBar // using inline styles instead of props to avoid generating a css class for each item
|
||||
style={{ left: `${left}%`, width: `${width}%` }}
|
||||
|
|
|
@ -0,0 +1,170 @@
|
|||
/*
|
||||
* 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 { EuiAccordion, EuiAccordionProps } from '@elastic/eui';
|
||||
import { Location } from 'history';
|
||||
import { isEmpty } from 'lodash';
|
||||
import React, { useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Margins } from '../../../../../shared/charts/Timeline';
|
||||
import { WaterfallItem } from './WaterfallItem';
|
||||
import {
|
||||
IWaterfall,
|
||||
IWaterfallItem,
|
||||
} from './waterfall_helpers/waterfall_helpers';
|
||||
|
||||
interface AccordionWaterfallProps {
|
||||
isOpen: boolean;
|
||||
item: IWaterfallItem;
|
||||
level: number;
|
||||
serviceColors: IWaterfall['serviceColors'];
|
||||
duration: IWaterfall['duration'];
|
||||
waterfallItemId?: string;
|
||||
location: Location;
|
||||
errorsPerTransaction: IWaterfall['errorsPerTransaction'];
|
||||
childrenByParentId: Record<string, IWaterfallItem[]>;
|
||||
onToggleEntryTransaction?: (
|
||||
nextState: EuiAccordionProps['forceState']
|
||||
) => void;
|
||||
timelineMargins: Margins;
|
||||
onClickWaterfallItem: (item: IWaterfallItem) => void;
|
||||
}
|
||||
|
||||
const StyledAccordion = styled(EuiAccordion).withConfig({
|
||||
shouldForwardProp: (prop) =>
|
||||
!['childrenCount', 'marginLeftLevel', 'hasError'].includes(prop),
|
||||
})<
|
||||
EuiAccordionProps & {
|
||||
childrenCount: number;
|
||||
marginLeftLevel: number;
|
||||
hasError: boolean;
|
||||
}
|
||||
>`
|
||||
.euiAccordion {
|
||||
border-top: 1px solid ${({ theme }) => theme.eui.euiColorLightShade};
|
||||
}
|
||||
.euiIEFlexWrapFix {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
}
|
||||
.euiAccordion__childWrapper {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.euiAccordion__padding--l {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.euiAccordion__iconWrapper {
|
||||
display: flex;
|
||||
position: relative;
|
||||
&:after {
|
||||
content: ${(props) => `'${props.childrenCount}'`};
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
top: -1px;
|
||||
z-index: 1;
|
||||
font-size: ${({ theme }) => theme.eui.euiFontSizeXS};
|
||||
}
|
||||
}
|
||||
|
||||
${(props) => {
|
||||
const borderLeft = props.hasError
|
||||
? `2px solid ${props.theme.eui.euiColorDanger};`
|
||||
: `1px solid ${props.theme.eui.euiColorLightShade};`;
|
||||
return `.button_${props.id} {
|
||||
margin-left: ${props.marginLeftLevel}px;
|
||||
border-left: ${borderLeft}
|
||||
&:hover {
|
||||
background-color: ${props.theme.eui.euiColorLightestShade};
|
||||
}
|
||||
}`;
|
||||
//
|
||||
}}
|
||||
`;
|
||||
|
||||
const WaterfallItemContainer = styled.div`
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
`;
|
||||
|
||||
export function AccordionWaterfall(props: AccordionWaterfallProps) {
|
||||
const [isOpen, setIsOpen] = useState(props.isOpen);
|
||||
|
||||
const {
|
||||
item,
|
||||
level,
|
||||
serviceColors,
|
||||
duration,
|
||||
childrenByParentId,
|
||||
waterfallItemId,
|
||||
location,
|
||||
errorsPerTransaction,
|
||||
timelineMargins,
|
||||
onClickWaterfallItem,
|
||||
} = props;
|
||||
|
||||
const nextLevel = level + 1;
|
||||
|
||||
const errorCount =
|
||||
item.docType === 'transaction'
|
||||
? errorsPerTransaction[item.doc.transaction.id]
|
||||
: 0;
|
||||
|
||||
const children = childrenByParentId[item.id] || [];
|
||||
|
||||
// To indent the items creating the parent/child tree
|
||||
const marginLeftLevel = 8 * level;
|
||||
|
||||
return (
|
||||
<StyledAccordion
|
||||
buttonClassName={`button_${item.id}`}
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
hasError={errorCount > 0}
|
||||
marginLeftLevel={marginLeftLevel}
|
||||
childrenCount={children.length}
|
||||
buttonContent={
|
||||
<WaterfallItemContainer>
|
||||
<WaterfallItem
|
||||
key={item.id}
|
||||
timelineMargins={timelineMargins}
|
||||
color={serviceColors[item.doc.service.name]}
|
||||
item={item}
|
||||
totalDuration={duration}
|
||||
isSelected={item.id === waterfallItemId}
|
||||
errorCount={errorCount}
|
||||
onClick={() => {
|
||||
onClickWaterfallItem(item);
|
||||
}}
|
||||
/>
|
||||
</WaterfallItemContainer>
|
||||
}
|
||||
arrowDisplay={isEmpty(children) ? 'none' : 'left'}
|
||||
initialIsOpen={true}
|
||||
forceState={isOpen ? 'open' : 'closed'}
|
||||
onToggle={() => setIsOpen((isCurrentOpen) => !isCurrentOpen)}
|
||||
>
|
||||
{children.map((child) => (
|
||||
<AccordionWaterfall
|
||||
key={child.id}
|
||||
isOpen={isOpen}
|
||||
item={child}
|
||||
level={nextLevel}
|
||||
serviceColors={serviceColors}
|
||||
waterfallItemId={waterfallItemId}
|
||||
location={location}
|
||||
errorsPerTransaction={errorsPerTransaction}
|
||||
duration={duration}
|
||||
childrenByParentId={childrenByParentId}
|
||||
timelineMargins={timelineMargins}
|
||||
onClickWaterfallItem={onClickWaterfallItem}
|
||||
/>
|
||||
))}
|
||||
</StyledAccordion>
|
||||
);
|
||||
}
|
|
@ -4,21 +4,22 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiCallOut } from '@elastic/eui';
|
||||
import { EuiButtonEmpty, EuiCallOut } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Location } from 'history';
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
// @ts-ignore
|
||||
import { StickyContainer } from 'react-sticky';
|
||||
import styled from 'styled-components';
|
||||
import { px } from '../../../../../../style/variables';
|
||||
import { history } from '../../../../../../utils/history';
|
||||
import { Timeline } from '../../../../../shared/charts/Timeline';
|
||||
import { HeightRetainer } from '../../../../../shared/HeightRetainer';
|
||||
import { fromQuery, toQuery } from '../../../../../shared/Links/url_helpers';
|
||||
import { getAgentMarks } from '../Marks/get_agent_marks';
|
||||
import { getErrorMarks } from '../Marks/get_error_marks';
|
||||
import { AccordionWaterfall } from './accordion_waterfall';
|
||||
import { WaterfallFlyout } from './WaterfallFlyout';
|
||||
import { WaterfallItem } from './WaterfallItem';
|
||||
import {
|
||||
IWaterfall,
|
||||
IWaterfallItem,
|
||||
|
@ -32,7 +33,7 @@ const Container = styled.div`
|
|||
|
||||
const TIMELINE_MARGINS = {
|
||||
top: 40,
|
||||
left: 50,
|
||||
left: 100,
|
||||
right: 50,
|
||||
bottom: 0,
|
||||
};
|
||||
|
@ -58,6 +59,7 @@ const WaterfallItemsContainer = styled.div<{
|
|||
paddingTop: number;
|
||||
}>`
|
||||
padding-top: ${(props) => px(props.paddingTop)};
|
||||
border-bottom: 1px solid ${({ theme }) => theme.eui.euiColorMediumShade};
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
|
@ -66,72 +68,91 @@ interface Props {
|
|||
location: Location;
|
||||
exceedsMax: boolean;
|
||||
}
|
||||
|
||||
export function Waterfall({
|
||||
waterfall,
|
||||
exceedsMax,
|
||||
waterfallItemId,
|
||||
location,
|
||||
}: Props) {
|
||||
const [isAccordionOpen, setIsAccordionOpen] = useState(true);
|
||||
const itemContainerHeight = 58; // TODO: This is a nasty way to calculate the height of the svg element. A better approach should be found
|
||||
const waterfallHeight = itemContainerHeight * waterfall.items.length;
|
||||
|
||||
const { serviceColors, duration } = waterfall;
|
||||
|
||||
const agentMarks = getAgentMarks(waterfall.entryTransaction);
|
||||
const agentMarks = getAgentMarks(waterfall.entryWaterfallTransaction?.doc);
|
||||
const errorMarks = getErrorMarks(waterfall.errorItems, serviceColors);
|
||||
|
||||
function renderWaterfallItem(item: IWaterfallItem) {
|
||||
const errorCount =
|
||||
item.docType === 'transaction'
|
||||
? waterfall.errorsPerTransaction[item.doc.transaction.id]
|
||||
: 0;
|
||||
|
||||
function renderItems(
|
||||
childrenByParentId: Record<string | number, IWaterfallItem[]>
|
||||
) {
|
||||
const { entryWaterfallTransaction } = waterfall;
|
||||
if (!entryWaterfallTransaction) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<WaterfallItem
|
||||
key={item.id}
|
||||
<AccordionWaterfall
|
||||
// used to recreate the entire tree when `isAccordionOpen` changes, collapsing or expanding all elements.
|
||||
key={`accordion_state_${isAccordionOpen}`}
|
||||
isOpen={isAccordionOpen}
|
||||
item={entryWaterfallTransaction}
|
||||
level={0}
|
||||
serviceColors={serviceColors}
|
||||
waterfallItemId={waterfallItemId}
|
||||
location={location}
|
||||
errorsPerTransaction={waterfall.errorsPerTransaction}
|
||||
duration={duration}
|
||||
childrenByParentId={childrenByParentId}
|
||||
timelineMargins={TIMELINE_MARGINS}
|
||||
color={serviceColors[item.doc.service.name]}
|
||||
item={item}
|
||||
totalDuration={duration}
|
||||
isSelected={item.id === waterfallItemId}
|
||||
errorCount={errorCount}
|
||||
onClick={() => toggleFlyout({ item, location })}
|
||||
onClickWaterfallItem={(item: IWaterfallItem) =>
|
||||
toggleFlyout({ item, location })
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{exceedsMax && (
|
||||
<EuiCallOut
|
||||
color="warning"
|
||||
size="s"
|
||||
iconType="alert"
|
||||
title={i18n.translate('xpack.apm.waterfall.exceedsMax', {
|
||||
defaultMessage:
|
||||
'Number of items in this trace exceed what is displayed',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
<StickyContainer>
|
||||
<Timeline
|
||||
marks={[...agentMarks, ...errorMarks]}
|
||||
xMax={duration}
|
||||
height={waterfallHeight}
|
||||
margins={TIMELINE_MARGINS}
|
||||
/>
|
||||
<WaterfallItemsContainer paddingTop={TIMELINE_MARGINS.top}>
|
||||
{waterfall.items.map(renderWaterfallItem)}
|
||||
</WaterfallItemsContainer>
|
||||
</StickyContainer>
|
||||
<HeightRetainer>
|
||||
<Container>
|
||||
{exceedsMax && (
|
||||
<EuiCallOut
|
||||
color="warning"
|
||||
size="s"
|
||||
iconType="alert"
|
||||
title={i18n.translate('xpack.apm.waterfall.exceedsMax', {
|
||||
defaultMessage:
|
||||
'Number of items in this trace exceed what is displayed',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
<StickyContainer>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<EuiButtonEmpty
|
||||
style={{ zIndex: 3, position: 'absolute' }}
|
||||
iconType={isAccordionOpen ? 'arrowDown' : 'arrowRight'}
|
||||
onClick={() => {
|
||||
setIsAccordionOpen((isOpen) => !isOpen);
|
||||
}}
|
||||
/>
|
||||
<Timeline
|
||||
marks={[...agentMarks, ...errorMarks]}
|
||||
xMax={duration}
|
||||
height={waterfallHeight}
|
||||
margins={TIMELINE_MARGINS}
|
||||
/>
|
||||
</div>
|
||||
<WaterfallItemsContainer paddingTop={TIMELINE_MARGINS.top}>
|
||||
{renderItems(waterfall.childrenByParentId)}
|
||||
</WaterfallItemsContainer>
|
||||
</StickyContainer>
|
||||
|
||||
<WaterfallFlyout
|
||||
waterfallItemId={waterfallItemId}
|
||||
waterfall={waterfall}
|
||||
location={location}
|
||||
toggleFlyout={toggleFlyout}
|
||||
/>
|
||||
</Container>
|
||||
<WaterfallFlyout
|
||||
waterfallItemId={waterfallItemId}
|
||||
waterfall={waterfall}
|
||||
location={location}
|
||||
toggleFlyout={toggleFlyout}
|
||||
/>
|
||||
</Container>
|
||||
</HeightRetainer>
|
||||
);
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -28,7 +28,7 @@ interface IWaterfallGroup {
|
|||
const ROOT_ID = 'root';
|
||||
|
||||
export interface IWaterfall {
|
||||
entryTransaction?: Transaction;
|
||||
entryWaterfallTransaction?: IWaterfallTransaction;
|
||||
rootTransaction?: Transaction;
|
||||
|
||||
/**
|
||||
|
@ -36,6 +36,7 @@ export interface IWaterfall {
|
|||
*/
|
||||
duration: number;
|
||||
items: IWaterfallItem[];
|
||||
childrenByParentId: Record<string | number, IWaterfallItem[]>;
|
||||
errorsPerTransaction: TraceAPIResponse['errorsPerTransaction'];
|
||||
errorsCount: number;
|
||||
serviceColors: IServiceColors;
|
||||
|
@ -329,6 +330,7 @@ export function getWaterfall(
|
|||
errorsCount: sum(Object.values(errorsPerTransaction)),
|
||||
serviceColors: {},
|
||||
errorItems: [],
|
||||
childrenByParentId: {},
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -357,10 +359,8 @@ export function getWaterfall(
|
|||
const duration = getWaterfallDuration(items);
|
||||
const serviceColors = getServiceColors(items);
|
||||
|
||||
const entryTransaction = entryWaterfallTransaction?.doc;
|
||||
|
||||
return {
|
||||
entryTransaction,
|
||||
entryWaterfallTransaction,
|
||||
rootTransaction,
|
||||
duration,
|
||||
items,
|
||||
|
@ -368,5 +368,6 @@ export function getWaterfall(
|
|||
errorsCount: errorItems.length,
|
||||
serviceColors,
|
||||
errorItems,
|
||||
childrenByParentId: getChildrenGroupedByParentId(items),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -8,8 +8,8 @@ import { Location } from 'history';
|
|||
import React from 'react';
|
||||
import { IUrlParams } from '../../../../../context/UrlParamsContext/types';
|
||||
import { ServiceLegends } from './ServiceLegends';
|
||||
import { Waterfall } from './Waterfall';
|
||||
import { IWaterfall } from './Waterfall/waterfall_helpers/waterfall_helpers';
|
||||
import { Waterfall } from './Waterfall';
|
||||
|
||||
interface Props {
|
||||
urlParams: IUrlParams;
|
||||
|
|
|
@ -64,8 +64,8 @@ export function WaterfallWithSummmary({
|
|||
});
|
||||
};
|
||||
|
||||
const { entryTransaction } = waterfall;
|
||||
if (!entryTransaction) {
|
||||
const { entryWaterfallTransaction } = waterfall;
|
||||
if (!entryWaterfallTransaction) {
|
||||
const content = isLoading ? (
|
||||
<LoadingStatePrompt />
|
||||
) : (
|
||||
|
@ -84,6 +84,8 @@ export function WaterfallWithSummmary({
|
|||
return <EuiPanel paddingSize="m">{content}</EuiPanel>;
|
||||
}
|
||||
|
||||
const entryTransaction = entryWaterfallTransaction.doc;
|
||||
|
||||
return (
|
||||
<EuiPanel paddingSize="m">
|
||||
<EuiFlexGroup>
|
||||
|
|
Loading…
Reference in a new issue