Compare commits

...

5 Commits

9 changed files with 160 additions and 60 deletions

View File

@@ -60,6 +60,21 @@
}
}
// The Badge is already pill-rounded, so an 18x18 box renders a circle for a
// single digit and a capsule for more.
.eventsBadge {
// l3 background (!important beats the Badge's [data-color] rule).
--badge-background: var(--l3-background) !important;
--badge-padding: 0 5px;
// Static count, not interactive — cancel the Badge's hover background change.
--badge-hover-background: var(--badge-background) !important;
margin-left: 6px;
min-width: 18px;
height: 18px;
vertical-align: middle;
}
.tabsScroll {
flex: 1;
min-height: 0;

View File

@@ -1,4 +1,5 @@
import { useCallback, useMemo } from 'react';
import { Badge } from '@signozhq/ui/badge';
import {
TabsContent,
TabsList,
@@ -281,6 +282,8 @@ function SpanDetailsContent({
// .map((key) => ({ key, value: allAttrs[key] }));
// }, [selectedSpan]);
const eventsCount = selectedSpan.events?.length || 0;
return (
<div className={styles.body}>
<div className={styles.detailsSection}>
@@ -397,7 +400,10 @@ function SpanDetailsContent({
<Bookmark size={14} /> Overview
</TabsTrigger>
<TabsTrigger value="events" variant="secondary">
<ScrollText size={14} /> Events ({selectedSpan.events?.length || 0})
<ScrollText size={14} /> Events
<Badge color="secondary" className={styles.eventsBadge}>
{eventsCount}
</Badge>
</TabsTrigger>
<TabsTrigger value="logs" variant="secondary">
<List size={14} /> Logs

View File

@@ -19,6 +19,7 @@
.backBtn {
flex-shrink: 0;
border: 1px solid var(--l1-border);
}
.traceIdSection {
@@ -26,6 +27,11 @@
align-items: center;
gap: 8px;
flex-shrink: 0;
// Tabular figures so the trace ID's digits line up at a fixed width.
:global(.key-value-label__value) {
font-variant-numeric: tabular-nums;
}
}
.filterSection {

View File

@@ -133,7 +133,7 @@ function TraceDetailsHeader({
<Button
variant="solid"
color="secondary"
size="md"
size="icon"
className={styles.backBtn}
onClick={handlePreviousBtnClick}
aria-label="Back"

View File

@@ -1,10 +1,8 @@
import { useCallback, useRef, useState } from 'react';
import { useHistory, useLocation } from 'react-router-dom';
import { useCopyToClipboard } from 'react-use';
import { ChevronsRight, Copy, Search, X } from '@signozhq/icons';
import { ArrowRightFromLine, Search, X } from '@signozhq/icons';
import { Switch } from '@signozhq/ui/switch';
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import { toast } from '@signozhq/ui/sonner';
import { Button } from '@signozhq/ui/button';
import {
TooltipRoot,
@@ -21,6 +19,7 @@ import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { uniqBy } from 'lodash-es';
import NozButton from 'pages/TraceDetailsV3/TraceDetailsHeader/NozButton';
import CopyButton from 'periscope/components/CopyButton/CopyButton';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import {
@@ -89,7 +88,6 @@ function Filters({
onExpand: () => void;
onCollapse: () => void;
}): JSX.Element {
const [, setCopy] = useCopyToClipboard();
const [filters, setFilters] = useState<TagFilter>(
BASE_FILTER_QUERY.filters || { items: [], op: 'AND' },
);
@@ -301,20 +299,7 @@ function Filters({
<div className={styles.pillPopover}>
<div className={styles.pillPopoverHeader}>
<Typography.Text>Search query</Typography.Text>
<Button
variant="ghost"
size="icon"
color="secondary"
onClick={(): void => {
setCopy(expression);
toast.success('Copied to clipboard', {
richColors: false,
position: 'top-right',
});
}}
>
<Copy size={12} />
</Button>
<CopyButton value={expression} size={12} />
</div>
<div className={styles.pillPopoverExpression}>{expression}</div>
</div>
@@ -421,7 +406,7 @@ function Filters({
color="secondary"
onClick={onCollapse}
>
<ChevronsRight size={14} />
<ArrowRightFromLine size={14} />
</Button>
</TooltipTrigger>
<TooltipContent>Collapse filters</TooltipContent>

View File

@@ -0,0 +1,52 @@
// Square copy button whose icon cross-fades between copy and check.
.copyButton {
flex-shrink: 0;
}
// Both icons occupy the same box; only one is visible at a time.
.iconStack {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
}
.icon {
position: absolute;
inset: 0;
transition:
opacity 300ms ease,
filter 300ms ease,
transform 300ms ease;
}
// Idle state: copy visible, check blurred/rotated/faded out.
.copyIcon {
opacity: 1;
filter: blur(0);
transform: rotate(0deg);
}
.checkIcon {
opacity: 0;
filter: blur(4px);
transform: rotate(-90deg);
// Green checkmark to signal a successful copy; eases in a touch slower.
color: var(--bg-forest-500);
transition-duration: 500ms;
}
// Copied state: copy fades/blurs/rotates out, check animates in.
.iconStack[data-copied='true'] {
.copyIcon {
opacity: 0;
filter: blur(4px);
transform: rotate(90deg);
}
.checkIcon {
opacity: 1;
filter: blur(0);
transform: rotate(0deg);
}
}

View File

@@ -0,0 +1,71 @@
import { CSSProperties, useCallback } from 'react';
import { Check, Copy } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import cx from 'classnames';
import { useCopyToClipboard } from 'hooks/useCopyToClipboard';
import styles from './CopyButton.module.scss';
export interface CopyButtonProps {
/** Text written to the clipboard on click. */
value: string;
/** Icon size in px. Default 14. */
size?: number;
/** Accessible label for the idle (not-yet-copied) state. Default "Copy". */
ariaLabel?: string;
/** Extra class merged onto the button. */
className?: string;
/** Fired after a successful copy (e.g. to show a toast). */
onCopy?: () => void;
testId?: string;
}
/**
* Square, icon-only copy button. Shows a copy icon that cross-fades to a
* checkmark (blur + rotate + fade) on copy, reverting after 2s. The checkmark
* uses the hover-state icon colour.
*/
function CopyButton({
value,
size = 14,
ariaLabel = 'Copy',
className,
onCopy,
testId,
}: CopyButtonProps): JSX.Element {
const { copyToClipboard, isCopied } = useCopyToClipboard();
const handleClick = useCallback((): void => {
copyToClipboard(value);
onCopy?.();
}, [copyToClipboard, value, onCopy]);
const stackStyle: CSSProperties = { width: size, height: size };
return (
<Button
variant="ghost"
color="secondary"
size="icon"
className={cx(styles.copyButton, className)}
onClick={handleClick}
aria-label={isCopied ? 'Copied' : ariaLabel}
testId={testId}
>
<span className={styles.iconStack} style={stackStyle} data-copied={isCopied}>
<Copy size={size} className={cx(styles.icon, styles.copyIcon)} />
<Check size={size} className={cx(styles.icon, styles.checkIcon)} />
</span>
</Button>
);
}
CopyButton.defaultProps = {
size: 14,
ariaLabel: 'Copy',
className: undefined,
onCopy: undefined,
testId: undefined,
};
export default CopyButton;

View File

@@ -22,23 +22,6 @@
--dropdown-menu-content-z-index: 1000;
}
&__copy-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 4px 8px;
border: 1px solid var(--l2-border);
border-radius: 4px;
background: transparent;
color: var(--l2-foreground);
cursor: pointer;
&:hover {
color: var(--l1-foreground);
background: var(--l3-background);
}
}
// Shared content container — no scroll, each view handles its own
&__content {
flex: 1;

View File

@@ -1,13 +1,11 @@
import { useMemo, useState } from 'react';
import { useCopyToClipboard } from 'react-use';
import { ChevronDown, Copy } from '@signozhq/icons';
import { ChevronDown } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { DropdownMenuSimple as Dropdown } from '@signozhq/ui/dropdown-menu';
import { toast } from '@signozhq/ui/sonner';
import logEvent from 'api/common/logEvent';
import CopyButton from 'periscope/components/CopyButton/CopyButton';
import { JsonView } from 'periscope/components/JsonView';
import { PrettyView } from 'periscope/components/PrettyView';
import { PrettyViewProps } from 'periscope/components/PrettyView';
import { PrettyView, PrettyViewProps } from 'periscope/components/PrettyView';
import './DataViewer.styles.scss';
@@ -33,7 +31,6 @@ function DataViewer({
prettyViewProps,
}: DataViewerProps): JSX.Element {
const [viewMode, setViewMode] = useState<ViewMode>('pretty');
const [, setCopy] = useCopyToClipboard();
const jsonString = useMemo(() => JSON.stringify(data, null, 2), [data]);
@@ -51,14 +48,6 @@ function DataViewer({
}
};
const handleCopy = (): void => {
const text = JSON.stringify(data, null, 2);
setCopy(text);
toast.success('Copied to clipboard', {
position: 'top-right',
});
};
const currentLabel =
VIEW_MODE_OPTIONS.find((opt) => opt.value === viewMode)?.label ?? 'Pretty';
@@ -94,14 +83,7 @@ function DataViewer({
{currentLabel}
</Button>
</Dropdown>
<button
type="button"
className="data-viewer__copy-btn"
onClick={handleCopy}
aria-label="Copy JSON"
>
<Copy size={14} />
</button>
<CopyButton value={jsonString} ariaLabel="Copy JSON" />
</div>
<div className="data-viewer__content">