diff --git a/frontend/src/container/TraceWaterfall/SpanLineActionButtons/SpanLineActionButtons.styles.scss b/frontend/src/container/TraceWaterfall/SpanLineActionButtons/SpanLineActionButtons.styles.scss
new file mode 100644
index 0000000000..6cfa385f6f
--- /dev/null
+++ b/frontend/src/container/TraceWaterfall/SpanLineActionButtons/SpanLineActionButtons.styles.scss
@@ -0,0 +1,42 @@
+.span-line-action-buttons {
+ display: flex;
+ position: absolute;
+ transform: translate(-50%, -50%);
+ top: 50%;
+ right: 0;
+ cursor: pointer;
+ border-radius: 2px;
+ border: 1px solid var(--bg-slate-400);
+ background: var(--bg-ink-400);
+
+ .ant-btn-default {
+ border: none;
+ box-shadow: none;
+ padding: 9px;
+ justify-content: center;
+ align-items: center;
+ display: flex;
+
+ &.active-tab {
+ background-color: var(--bg-slate-400);
+ }
+ }
+
+ .copy-span-btn {
+ border-color: var(--bg-slate-400) !important;
+ }
+}
+
+.lightMode {
+ .span-line-action-buttons {
+ border: 1px solid var(--bg-vanilla-400);
+ background: var(--bg-vanilla-400);
+
+ .ant-btn-default {
+ }
+
+ .copy-span-btn {
+ border-color: var(--bg-vanilla-400) !important;
+ }
+ }
+}
diff --git a/frontend/src/container/TraceWaterfall/SpanLineActionButtons/index.tsx b/frontend/src/container/TraceWaterfall/SpanLineActionButtons/index.tsx
new file mode 100644
index 0000000000..f8646cd186
--- /dev/null
+++ b/frontend/src/container/TraceWaterfall/SpanLineActionButtons/index.tsx
@@ -0,0 +1,34 @@
+import './SpanLineActionButtons.styles.scss';
+
+import { LinkOutlined } from '@ant-design/icons';
+import { Button, Tooltip } from 'antd';
+import { useCopySpanLink } from 'hooks/trace/useCopySpanLink';
+import { Span } from 'types/api/trace/getTraceV2';
+
+export interface SpanLineActionButtonsProps {
+ span: Span;
+ customClassName?: string;
+}
+export default function SpanLineActionButtons({
+ span,
+ customClassName = '',
+}: SpanLineActionButtonsProps): JSX.Element {
+ const { onSpanCopy } = useCopySpanLink(span);
+
+ return (
+
+
+ }
+ onClick={onSpanCopy}
+ className="copy-span-btn"
+ />
+
+
+ );
+}
+
+SpanLineActionButtons.defaultProps = {
+ customClassName: '',
+};
diff --git a/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/Success.tsx b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/Success.tsx
index b3a113665f..9e432d8b25 100644
--- a/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/Success.tsx
+++ b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/Success.tsx
@@ -9,6 +9,7 @@ import cx from 'classnames';
import { TableV3 } from 'components/TableV3/TableV3';
import { themeColors } from 'constants/theme';
import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils';
+import SpanLineActionButtons from 'container/TraceWaterfall/SpanLineActionButtons';
import { IInterestedSpan } from 'container/TraceWaterfall/TraceWaterfall';
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
import {
@@ -25,7 +26,9 @@ import {
useEffect,
useMemo,
useRef,
+ useState,
} from 'react';
+import { useHistory, useLocation } from 'react-router-dom';
import { Span } from 'types/api/trace/getTraceV2';
import { toFixed } from 'utils/toFixed';
@@ -166,20 +169,38 @@ function SpanDuration({
const leftOffset = ((span.timestamp - traceMetadata.startTime) * 1e2) / spread;
const width = (span.durationNano * 1e2) / (spread * 1e6);
+ const { search } = useLocation();
+ const history = useHistory();
+ const searchParams = new URLSearchParams(search);
+
let color = generateColor(span.serviceName, themeColors.traceDetailColors);
if (span.hasError) {
color = `var(--bg-cherry-500)`;
}
+ const [hasActionButtons, setHasActionButtons] = useState(false);
+
+ const handleMouseEnter = (): void => {
+ setHasActionButtons(true);
+ };
+
+ const handleMouseLeave = (): void => {
+ setHasActionButtons(false);
+ };
+
return (
{
setSelectedSpan(span);
+ searchParams.set('spanId', span.spanId);
+ history.replace({ search: searchParams.toString() });
}}
>
+ {hasActionButtons &&
}
} => {
+ const urlQuery = useUrlQuery();
+ const { pathname } = useLocation();
+ const [, setCopy] = useCopyToClipboard();
+ const { notifications } = useNotifications();
+
+ const onSpanCopy: MouseEventHandler = useCallback(
+ (event) => {
+ if (!span) return;
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ urlQuery.delete('spanId');
+
+ if (span.spanId) {
+ urlQuery.set('spanId', span?.spanId);
+ }
+
+ const link = `${window.location.origin}${pathname}?${urlQuery.toString()}`;
+
+ setCopy(link);
+ notifications.success({
+ message: 'Copied to clipboard',
+ });
+ },
+ [span, urlQuery, pathname, setCopy, notifications],
+ );
+
+ return {
+ onSpanCopy,
+ };
+};