mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-10-30 14:49:14 +00:00
687 lines
20 KiB
TypeScript
687 lines
20 KiB
TypeScript
import {createRef, Fragment, useCallback, useEffect, useState} from 'react';
|
|
import {browserHistory} from 'react-router';
|
|
import styled from '@emotion/styled';
|
|
import type {Location} from 'history';
|
|
|
|
import GuideAnchor from 'sentry/components/assistant/guideAnchor';
|
|
import Count from 'sentry/components/count';
|
|
import * as DividerHandlerManager from 'sentry/components/events/interfaces/spans/dividerHandlerManager';
|
|
import * as ScrollbarManager from 'sentry/components/events/interfaces/spans/scrollbarManager';
|
|
import {MeasurementMarker} from 'sentry/components/events/interfaces/spans/styles';
|
|
import type {
|
|
SpanBoundsType,
|
|
SpanGeneratedBoundsType,
|
|
VerticalMark,
|
|
} from 'sentry/components/events/interfaces/spans/utils';
|
|
import {
|
|
getMeasurementBounds,
|
|
transactionTargetHash,
|
|
} from 'sentry/components/events/interfaces/spans/utils';
|
|
import ProjectBadge from 'sentry/components/idBadge/projectBadge';
|
|
import Link from 'sentry/components/links/link';
|
|
import {ROW_HEIGHT, SpanBarType} from 'sentry/components/performance/waterfall/constants';
|
|
import {
|
|
Row,
|
|
RowCell,
|
|
RowCellContainer,
|
|
RowReplayTimeIndicators,
|
|
} from 'sentry/components/performance/waterfall/row';
|
|
import {DurationPill, RowRectangle} from 'sentry/components/performance/waterfall/rowBar';
|
|
import {
|
|
DividerContainer,
|
|
DividerLine,
|
|
DividerLineGhostContainer,
|
|
ErrorBadge,
|
|
} from 'sentry/components/performance/waterfall/rowDivider';
|
|
import {
|
|
RowTitle,
|
|
RowTitleContainer,
|
|
RowTitleContent,
|
|
} from 'sentry/components/performance/waterfall/rowTitle';
|
|
import {
|
|
ConnectorBar,
|
|
TOGGLE_BORDER_BOX,
|
|
TreeConnector,
|
|
TreeToggle,
|
|
TreeToggleContainer,
|
|
TreeToggleIcon,
|
|
} from 'sentry/components/performance/waterfall/treeConnector';
|
|
import {
|
|
getDurationDisplay,
|
|
getHumanDuration,
|
|
} from 'sentry/components/performance/waterfall/utils';
|
|
import {generateIssueEventTarget} from 'sentry/components/quickTrace/utils';
|
|
import {Tooltip} from 'sentry/components/tooltip';
|
|
import type {Organization} from 'sentry/types';
|
|
import {defined} from 'sentry/utils';
|
|
import toPercent from 'sentry/utils/number/toPercent';
|
|
import type {
|
|
TraceError,
|
|
TraceFullDetailed,
|
|
} from 'sentry/utils/performance/quickTrace/types';
|
|
import {
|
|
isTraceError,
|
|
isTraceRoot,
|
|
isTraceTransaction,
|
|
} from 'sentry/utils/performance/quickTrace/utils';
|
|
import Projects from 'sentry/utils/projects';
|
|
|
|
import {ProjectBadgeContainer} from './styles';
|
|
import TransactionDetail from './transactionDetail';
|
|
import type {TraceInfo, TraceRoot, TreeDepth} from './types';
|
|
import {shortenErrorTitle} from './utils';
|
|
|
|
const MARGIN_LEFT = 0;
|
|
|
|
type Props = {
|
|
addContentSpanBarRef: (instance: HTMLDivElement | null) => void;
|
|
continuingDepths: TreeDepth[];
|
|
generateBounds: (bounds: SpanBoundsType) => SpanGeneratedBoundsType;
|
|
hasGuideAnchor: boolean;
|
|
index: number;
|
|
isExpanded: boolean;
|
|
isLast: boolean;
|
|
isOrphan: boolean;
|
|
isVisible: boolean;
|
|
location: Location;
|
|
onWheel: (deltaX: number) => void;
|
|
organization: Organization;
|
|
removeContentSpanBarRef: (instance: HTMLDivElement | null) => void;
|
|
toggleExpandedState: () => void;
|
|
traceInfo: TraceInfo;
|
|
transaction: TraceRoot | TraceFullDetailed | TraceError;
|
|
barColor?: string;
|
|
isOrphanError?: boolean;
|
|
measurements?: Map<number, VerticalMark>;
|
|
numOfOrphanErrors?: number;
|
|
onlyOrphanErrors?: boolean;
|
|
};
|
|
|
|
function TransactionBar(props: Props) {
|
|
const [showDetail, setShowDetail] = useState(false);
|
|
const transactionRowDOMRef = createRef<HTMLDivElement>();
|
|
const transactionTitleRef = createRef<HTMLDivElement>();
|
|
let spanContentRef: HTMLDivElement | null = null;
|
|
|
|
const handleWheel = useCallback(
|
|
(event: WheelEvent) => {
|
|
// https://stackoverflow.com/q/57358640
|
|
// https://github.com/facebook/react/issues/14856
|
|
if (Math.abs(event.deltaY) > Math.abs(event.deltaX)) {
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
if (Math.abs(event.deltaY) === Math.abs(event.deltaX)) {
|
|
return;
|
|
}
|
|
|
|
const {onWheel} = props;
|
|
onWheel(event.deltaX);
|
|
},
|
|
[props]
|
|
);
|
|
|
|
const scrollIntoView = useCallback(() => {
|
|
const element = transactionRowDOMRef.current;
|
|
if (!element) {
|
|
return;
|
|
}
|
|
const boundingRect = element.getBoundingClientRect();
|
|
const offset = boundingRect.top + window.scrollY;
|
|
setShowDetail(true);
|
|
window.scrollTo(0, offset);
|
|
}, [transactionRowDOMRef]);
|
|
|
|
useEffect(() => {
|
|
const {location, transaction} = props;
|
|
const transactionTitleRefCurrentCopy = transactionTitleRef.current;
|
|
|
|
if (
|
|
'event_id' in transaction &&
|
|
transactionTargetHash(transaction.event_id) === location.hash
|
|
) {
|
|
scrollIntoView();
|
|
}
|
|
|
|
if (transactionTitleRefCurrentCopy) {
|
|
transactionTitleRefCurrentCopy.addEventListener('wheel', handleWheel, {
|
|
passive: false,
|
|
});
|
|
}
|
|
|
|
return () => {
|
|
if (transactionTitleRefCurrentCopy) {
|
|
transactionTitleRefCurrentCopy.removeEventListener('wheel', handleWheel);
|
|
}
|
|
};
|
|
}, [handleWheel, props, scrollIntoView, transactionTitleRef]);
|
|
|
|
const handleRowCellClick = () => {
|
|
const {transaction, organization} = props;
|
|
|
|
if (isTraceError(transaction)) {
|
|
browserHistory.push(generateIssueEventTarget(transaction, organization));
|
|
}
|
|
|
|
if (isTraceTransaction<TraceFullDetailed>(transaction)) {
|
|
setShowDetail(prev => !prev);
|
|
}
|
|
};
|
|
|
|
const getCurrentOffset = () => {
|
|
const {transaction} = props;
|
|
const {generation} = transaction;
|
|
|
|
return getOffset(generation);
|
|
};
|
|
|
|
const renderMeasurements = () => {
|
|
const {measurements, generateBounds} = props;
|
|
if (!measurements) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<Fragment>
|
|
{Array.from(measurements.values()).map(verticalMark => {
|
|
const mark = Object.values(verticalMark.marks)[0];
|
|
const {timestamp} = mark;
|
|
const bounds = getMeasurementBounds(timestamp, generateBounds);
|
|
|
|
const shouldDisplay = defined(bounds.left) && defined(bounds.width);
|
|
|
|
if (!shouldDisplay || !bounds.isSpanVisibleInView) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<MeasurementMarker
|
|
key={String(timestamp)}
|
|
style={{
|
|
left: `clamp(0%, ${toPercent(bounds.left || 0)}, calc(100% - 1px))`,
|
|
}}
|
|
failedThreshold={verticalMark.failedThreshold}
|
|
/>
|
|
);
|
|
})}
|
|
</Fragment>
|
|
);
|
|
};
|
|
|
|
const renderConnector = (hasToggle: boolean) => {
|
|
const {continuingDepths, isExpanded, isOrphan, isLast, transaction} = props;
|
|
|
|
const {generation = 0} = transaction;
|
|
const eventId =
|
|
isTraceTransaction<TraceFullDetailed>(transaction) || isTraceError(transaction)
|
|
? transaction.event_id
|
|
: transaction.traceSlug;
|
|
|
|
if (generation === 0) {
|
|
if (hasToggle) {
|
|
return (
|
|
<ConnectorBar
|
|
style={{right: '15px', height: '10px', bottom: '-5px', top: 'auto'}}
|
|
orphanBranch={false}
|
|
/>
|
|
);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
const connectorBars: Array<React.ReactNode> = continuingDepths.map(
|
|
({depth, isOrphanDepth}) => {
|
|
if (generation - depth <= 1) {
|
|
// If the difference is less than or equal to 1, then it means that the continued
|
|
// bar is from its direct parent. In this case, do not render a connector bar
|
|
// because the tree connector below will suffice.
|
|
return null;
|
|
}
|
|
|
|
const left = -1 * getOffset(generation - depth - 1) - 2;
|
|
|
|
return (
|
|
<ConnectorBar
|
|
style={{left}}
|
|
key={`${eventId}-${depth}`}
|
|
orphanBranch={isOrphanDepth}
|
|
/>
|
|
);
|
|
}
|
|
);
|
|
|
|
if (hasToggle && isExpanded) {
|
|
connectorBars.push(
|
|
<ConnectorBar
|
|
style={{
|
|
right: '15px',
|
|
height: '10px',
|
|
bottom: isLast ? `-${ROW_HEIGHT / 2 + 1}px` : '0',
|
|
top: 'auto',
|
|
}}
|
|
key={`${eventId}-last`}
|
|
orphanBranch={false}
|
|
/>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<TreeConnector isLast={isLast} hasToggler={hasToggle} orphanBranch={isOrphan}>
|
|
{connectorBars}
|
|
</TreeConnector>
|
|
);
|
|
};
|
|
|
|
const renderToggle = (errored: boolean) => {
|
|
const {isExpanded, transaction, toggleExpandedState, numOfOrphanErrors} = props;
|
|
const left = getCurrentOffset();
|
|
|
|
const hasOrphanErrors = numOfOrphanErrors && numOfOrphanErrors > 0;
|
|
const childrenLength =
|
|
(!isTraceError(transaction) && transaction.children?.length) || 0;
|
|
const generation = transaction.generation || 0;
|
|
if (childrenLength <= 0 && !hasOrphanErrors) {
|
|
return (
|
|
<TreeToggleContainer style={{left: `${left}px`}}>
|
|
{renderConnector(false)}
|
|
</TreeToggleContainer>
|
|
);
|
|
}
|
|
|
|
const isRoot = generation === 0;
|
|
|
|
return (
|
|
<TreeToggleContainer style={{left: `${left}px`}} hasToggler>
|
|
{renderConnector(true)}
|
|
<TreeToggle
|
|
disabled={isRoot}
|
|
isExpanded={isExpanded}
|
|
errored={errored}
|
|
onClick={event => {
|
|
event.stopPropagation();
|
|
|
|
if (isRoot) {
|
|
return;
|
|
}
|
|
|
|
toggleExpandedState();
|
|
}}
|
|
>
|
|
<Count value={childrenLength + (numOfOrphanErrors ?? 0)} />
|
|
{!isRoot && (
|
|
<div>
|
|
<TreeToggleIcon direction={isExpanded ? 'up' : 'down'} />
|
|
</div>
|
|
)}
|
|
</TreeToggle>
|
|
</TreeToggleContainer>
|
|
);
|
|
};
|
|
|
|
const renderTitle = (_: ScrollbarManager.ScrollbarManagerChildrenProps) => {
|
|
const {organization, transaction, addContentSpanBarRef, removeContentSpanBarRef} =
|
|
props;
|
|
const left = getCurrentOffset();
|
|
const errored = isTraceTransaction<TraceFullDetailed>(transaction)
|
|
? transaction.errors &&
|
|
transaction.errors.length + transaction.performance_issues.length > 0
|
|
: false;
|
|
|
|
const projectBadge = (isTraceTransaction<TraceFullDetailed>(transaction) ||
|
|
isTraceError(transaction)) && (
|
|
<Projects orgId={organization.slug} slugs={[transaction.project_slug]}>
|
|
{({projects}) => {
|
|
const project = projects.find(p => p.slug === transaction.project_slug);
|
|
return (
|
|
<ProjectBadgeContainer>
|
|
<Tooltip title={transaction.project_slug}>
|
|
<ProjectBadge
|
|
project={project ? project : {slug: transaction.project_slug}}
|
|
avatarSize={16}
|
|
hideName
|
|
/>
|
|
</Tooltip>
|
|
</ProjectBadgeContainer>
|
|
);
|
|
}}
|
|
</Projects>
|
|
);
|
|
|
|
const content = isTraceError(transaction) ? (
|
|
<Fragment>
|
|
{projectBadge}
|
|
<RowTitleContent errored>
|
|
<ErrorLink to={generateIssueEventTarget(transaction, organization)}>
|
|
<strong>{'Unknown \u2014 '}</strong>
|
|
{shortenErrorTitle(transaction.title)}
|
|
</ErrorLink>
|
|
</RowTitleContent>
|
|
</Fragment>
|
|
) : isTraceTransaction<TraceFullDetailed>(transaction) ? (
|
|
<Fragment>
|
|
{projectBadge}
|
|
<RowTitleContent errored={errored}>
|
|
<strong>
|
|
{transaction['transaction.op']}
|
|
{' \u2014 '}
|
|
</strong>
|
|
{transaction.transaction}
|
|
</RowTitleContent>
|
|
</Fragment>
|
|
) : (
|
|
<RowTitleContent errored={false}>
|
|
<strong>{'Trace \u2014 '}</strong>
|
|
{transaction.traceSlug}
|
|
</RowTitleContent>
|
|
);
|
|
|
|
return (
|
|
<RowTitleContainer
|
|
ref={ref => {
|
|
if (!ref) {
|
|
removeContentSpanBarRef(spanContentRef);
|
|
return;
|
|
}
|
|
|
|
addContentSpanBarRef(ref);
|
|
spanContentRef = ref;
|
|
}}
|
|
>
|
|
{renderToggle(errored)}
|
|
<RowTitle
|
|
style={{
|
|
left: `${left}px`,
|
|
width: '100%',
|
|
}}
|
|
>
|
|
{content}
|
|
</RowTitle>
|
|
</RowTitleContainer>
|
|
);
|
|
};
|
|
|
|
const renderDivider = (
|
|
dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps
|
|
) => {
|
|
if (showDetail) {
|
|
// Mock component to preserve layout spacing
|
|
return (
|
|
<DividerLine
|
|
showDetail
|
|
style={{
|
|
position: 'absolute',
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const {addDividerLineRef} = dividerHandlerChildrenProps;
|
|
|
|
return (
|
|
<DividerLine
|
|
ref={addDividerLineRef()}
|
|
style={{
|
|
position: 'absolute',
|
|
}}
|
|
onMouseEnter={() => {
|
|
dividerHandlerChildrenProps.setHover(true);
|
|
}}
|
|
onMouseLeave={() => {
|
|
dividerHandlerChildrenProps.setHover(false);
|
|
}}
|
|
onMouseOver={() => {
|
|
dividerHandlerChildrenProps.setHover(true);
|
|
}}
|
|
onMouseDown={dividerHandlerChildrenProps.onDragStart}
|
|
onClick={event => {
|
|
// we prevent the propagation of the clicks from this component to prevent
|
|
// the span detail from being opened.
|
|
event.stopPropagation();
|
|
}}
|
|
/>
|
|
);
|
|
};
|
|
|
|
const renderGhostDivider = (
|
|
dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps
|
|
) => {
|
|
const {dividerPosition, addGhostDividerLineRef} = dividerHandlerChildrenProps;
|
|
|
|
return (
|
|
<DividerLineGhostContainer
|
|
style={{
|
|
width: `calc(${toPercent(dividerPosition)} + 0.5px)`,
|
|
display: 'none',
|
|
}}
|
|
>
|
|
<DividerLine
|
|
ref={addGhostDividerLineRef()}
|
|
style={{
|
|
right: 0,
|
|
}}
|
|
className="hovering"
|
|
onClick={event => {
|
|
// the ghost divider line should not be interactive.
|
|
// we prevent the propagation of the clicks from this component to prevent
|
|
// the span detail from being opened.
|
|
event.stopPropagation();
|
|
}}
|
|
/>
|
|
</DividerLineGhostContainer>
|
|
);
|
|
};
|
|
|
|
const renderErrorBadge = () => {
|
|
const {transaction} = props;
|
|
|
|
if (
|
|
isTraceRoot(transaction) ||
|
|
isTraceError(transaction) ||
|
|
!(transaction.errors.length + transaction.performance_issues.length)
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
return <ErrorBadge />;
|
|
};
|
|
|
|
const renderRectangle = () => {
|
|
const {transaction, traceInfo, barColor} = props;
|
|
|
|
// Use 1 as the difference in the case that startTimestamp === endTimestamp
|
|
const delta = Math.abs(traceInfo.endTimestamp - traceInfo.startTimestamp) || 1;
|
|
const start_timestamp = isTraceError(transaction)
|
|
? transaction.timestamp
|
|
: transaction.start_timestamp;
|
|
|
|
if (!(start_timestamp && transaction.timestamp)) {
|
|
return null;
|
|
}
|
|
|
|
const startPosition = Math.abs(start_timestamp - traceInfo.startTimestamp);
|
|
const startPercentage = startPosition / delta;
|
|
const duration = Math.abs(transaction.timestamp - start_timestamp);
|
|
const widthPercentage = duration / delta;
|
|
|
|
return (
|
|
<StyledRowRectangle
|
|
style={{
|
|
backgroundColor: barColor,
|
|
left: `min(${toPercent(startPercentage || 0)}, calc(100% - 1px))`,
|
|
width: toPercent(widthPercentage || 0),
|
|
}}
|
|
>
|
|
{renderPerformanceIssues()}
|
|
{isTraceError(transaction) ? (
|
|
<ErrorBadge />
|
|
) : (
|
|
<Fragment>
|
|
{renderErrorBadge()}
|
|
<DurationPill
|
|
durationDisplay={getDurationDisplay({
|
|
left: startPercentage,
|
|
width: widthPercentage,
|
|
})}
|
|
showDetail={showDetail}
|
|
>
|
|
{getHumanDuration(duration)}
|
|
</DurationPill>
|
|
</Fragment>
|
|
)}
|
|
</StyledRowRectangle>
|
|
);
|
|
};
|
|
|
|
const renderPerformanceIssues = () => {
|
|
const {transaction, barColor} = props;
|
|
if (isTraceError(transaction) || isTraceRoot(transaction)) {
|
|
return null;
|
|
}
|
|
|
|
const rows: React.ReactElement[] = [];
|
|
// Use 1 as the difference in the case that startTimestamp === endTimestamp
|
|
const delta = Math.abs(transaction.timestamp - transaction.start_timestamp) || 1;
|
|
for (let i = 0; i < transaction.performance_issues.length; i++) {
|
|
const issue = transaction.performance_issues[i];
|
|
const startPosition = Math.abs(issue.start - transaction.start_timestamp);
|
|
const startPercentage = startPosition / delta;
|
|
const duration = Math.abs(issue.end - issue.start);
|
|
const widthPercentage = duration / delta;
|
|
rows.push(
|
|
<RowRectangle
|
|
style={{
|
|
backgroundColor: barColor,
|
|
left: `min(${toPercent(startPercentage || 0)}, calc(100% - 1px))`,
|
|
width: toPercent(widthPercentage || 0),
|
|
}}
|
|
spanBarType={SpanBarType.AFFECTED}
|
|
/>
|
|
);
|
|
}
|
|
return rows;
|
|
};
|
|
|
|
const renderHeader = ({
|
|
dividerHandlerChildrenProps,
|
|
scrollbarManagerChildrenProps,
|
|
}: {
|
|
dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps;
|
|
scrollbarManagerChildrenProps: ScrollbarManager.ScrollbarManagerChildrenProps;
|
|
}) => {
|
|
const {hasGuideAnchor, index, transaction, onlyOrphanErrors = false} = props;
|
|
const {dividerPosition} = dividerHandlerChildrenProps;
|
|
const hideDurationRectangle = isTraceRoot(transaction) && onlyOrphanErrors;
|
|
|
|
return (
|
|
<RowCellContainer showDetail={showDetail}>
|
|
<RowCell
|
|
data-test-id="transaction-row-title"
|
|
data-type="span-row-cell"
|
|
style={{
|
|
width: `calc(${toPercent(dividerPosition)} - 0.5px)`,
|
|
paddingTop: 0,
|
|
}}
|
|
showDetail={showDetail}
|
|
onClick={handleRowCellClick}
|
|
ref={transactionTitleRef}
|
|
>
|
|
<GuideAnchor target="trace_view_guide_row" disabled={!hasGuideAnchor}>
|
|
{renderTitle(scrollbarManagerChildrenProps)}
|
|
</GuideAnchor>
|
|
</RowCell>
|
|
<DividerContainer>{renderDivider(dividerHandlerChildrenProps)}</DividerContainer>
|
|
<RowCell
|
|
data-test-id="transaction-row-duration"
|
|
data-type="span-row-cell"
|
|
showStriping={index % 2 !== 0}
|
|
style={{
|
|
width: `calc(${toPercent(1 - dividerPosition)} - 0.5px)`,
|
|
paddingTop: 0,
|
|
overflow: 'visible',
|
|
}}
|
|
showDetail={showDetail}
|
|
onClick={handleRowCellClick}
|
|
>
|
|
<RowReplayTimeIndicators />
|
|
<GuideAnchor target="trace_view_guide_row_details" disabled={!hasGuideAnchor}>
|
|
{!hideDurationRectangle && renderRectangle()}
|
|
{renderMeasurements()}
|
|
</GuideAnchor>
|
|
</RowCell>
|
|
{!showDetail && renderGhostDivider(dividerHandlerChildrenProps)}
|
|
</RowCellContainer>
|
|
);
|
|
};
|
|
|
|
const renderDetail = () => {
|
|
const {location, organization, isVisible, transaction} = props;
|
|
|
|
if (isTraceError(transaction) || isTraceRoot(transaction)) {
|
|
return null;
|
|
}
|
|
|
|
if (!isVisible || !showDetail) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<TransactionDetail
|
|
location={location}
|
|
organization={organization}
|
|
transaction={transaction}
|
|
scrollIntoView={scrollIntoView}
|
|
/>
|
|
);
|
|
};
|
|
|
|
const {isVisible, transaction} = props;
|
|
|
|
return (
|
|
<StyledRow
|
|
ref={transactionRowDOMRef}
|
|
visible={isVisible}
|
|
showBorder={showDetail}
|
|
cursor={isTraceTransaction<TraceFullDetailed>(transaction) ? 'pointer' : 'default'}
|
|
>
|
|
<ScrollbarManager.Consumer>
|
|
{scrollbarManagerChildrenProps => (
|
|
<DividerHandlerManager.Consumer>
|
|
{dividerHandlerChildrenProps =>
|
|
renderHeader({
|
|
dividerHandlerChildrenProps,
|
|
scrollbarManagerChildrenProps,
|
|
})
|
|
}
|
|
</DividerHandlerManager.Consumer>
|
|
)}
|
|
</ScrollbarManager.Consumer>
|
|
{renderDetail()}
|
|
</StyledRow>
|
|
);
|
|
}
|
|
|
|
function getOffset(generation) {
|
|
return generation * (TOGGLE_BORDER_BOX / 2) + MARGIN_LEFT;
|
|
}
|
|
|
|
export default TransactionBar;
|
|
|
|
const StyledRow = styled(Row)`
|
|
&,
|
|
${RowCellContainer} {
|
|
overflow: visible;
|
|
}
|
|
`;
|
|
|
|
const ErrorLink = styled(Link)`
|
|
color: ${p => p.theme.error};
|
|
`;
|
|
|
|
const StyledRowRectangle = styled(RowRectangle)`
|
|
display: flex;
|
|
align-items: center;
|
|
`;
|