-
Notifications
You must be signed in to change notification settings - Fork 449
feat(app): make View Trace action more prominent with a one-time nudge #2815
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
elizabetdev
wants to merge
11
commits into
main
Choose a base branch
from
elizabet/prominent-view-trace-action
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
2f26416
feat(app): make View Trace action more prominent with a one-time nudge
elizabetdev 8b7a7af
fix(app): stop the View Trace callout from fighting the panel Esc hotkey
elizabetdev c106af5
refactor(app): extract ViewTraceCalloutButton and cover it with tests
elizabetdev e82ea75
fix(app): use placement-neutral copy for the View Trace callout
elizabetdev 116929a
docs(app): refresh View Trace changeset to match final behavior
elizabetdev 2bfb0f2
Merge branch 'main' into elizabet/prominent-view-trace-action
elizabetdev 23f1294
docs(app): document intentional no-outside-dismiss on View Trace callout
elizabetdev eaf7f92
Merge branch 'main' into elizabet/prominent-view-trace-action
elizabetdev 98cd9b8
Merge branch 'main' into elizabet/prominent-view-trace-action
elizabetdev 0f1f9ad
Merge branch 'main' into elizabet/prominent-view-trace-action
kodiakhq[bot] 0a6d4bc
fix(app): avoid unsafe type assertion in e2e base-test; fix knip stry…
elizabetdev File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| --- | ||
| '@hyperdx/app': patch | ||
| --- | ||
|
|
||
| Make the log side panel "View Trace" action more noticeable: it now uses an | ||
| outlined (secondary) button with the trace source icon, larger compact size, and | ||
| is right-aligned so it stands out from the dimmed metadata row instead of | ||
| blending in as subtle inline text. The first time a log with a correlated trace | ||
| is opened, a one-time popover points users to the button; it is dismissed by an | ||
| explicit acknowledgement ("Got it" or clicking View Trace) and then never shows | ||
| again (persisted per browser). It deliberately does not intercept Escape, which | ||
| keeps its normal side-panel behavior. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| --- | ||
| name: data-source-icons | ||
| description: Use the correct Tabler icon for each HyperDX data source kind (Log, Trace, Session, Metric, PromQL). Use whenever adding or changing an icon that represents a source kind or a signal type — source selectors, side panels, cross-source actions like "View Trace", tabs, badges, breadcrumbs, or anywhere a log/trace/session/metric is depicted. | ||
| --- | ||
|
|
||
| # Data source icons | ||
|
|
||
| There is **one canonical icon per source kind**. The source of truth is | ||
| `SOURCE_KIND_ICONS` in | ||
| `packages/app/src/components/sourceSelectUtils.tsx`. Never invent a different | ||
| icon for a source kind (e.g. don't use `IconTimeline` for a trace) — always | ||
| match the table below so icons stay consistent across the app. | ||
|
|
||
| ## Canonical mapping | ||
|
|
||
| | Source kind (`SourceKind`) | Tabler icon | Meaning | | ||
| | -------------------------- | ---------------- | ----------------------------- | | ||
| | `Log` | `IconLogs` | Logs | | ||
| | `Trace` | `IconConnection` | Traces / spans | | ||
| | `Session` | `IconDeviceLaptop` | Session replay / client sessions | | ||
| | `Metric` | `IconChartLine` | Metrics | | ||
| | `Promql` | `IconChartLine` | PromQL metrics (same as Metric) | | ||
|
|
||
| All imported from `@tabler/icons-react`. | ||
|
|
||
| ## Rules | ||
|
|
||
| 1. **Prefer the shared map.** When you need a source-kind icon in a context that | ||
| already has (or can accept) a `SourceKind`, use `SOURCE_KIND_ICONS[kind]` | ||
| from `sourceSelectUtils.tsx` rather than hardcoding an icon component. | ||
| 2. **If you must hardcode** (e.g. a fixed "View Trace" action that always points | ||
| at a trace), import the exact icon from the table above — for a trace that is | ||
| `IconConnection`, not `IconTimeline`/`IconRoute`/etc. | ||
| 3. **Sizing**: the shared map uses `size={16}`. Match the surrounding UI; small | ||
| inline buttons/badges commonly use `14`. | ||
| 4. **Adding a new source kind**: update `SOURCE_KIND_ICONS` first, then update | ||
| this table so the two never drift. | ||
|
|
||
| ## Examples | ||
|
|
||
| Cross-source "View Trace" action (fixed target → hardcode the trace icon): | ||
|
|
||
| ```tsx | ||
| import { IconConnection } from '@tabler/icons-react'; | ||
|
|
||
| <Button leftSection={<IconConnection size={14} />}>View Trace</Button>; | ||
| ``` | ||
|
|
||
| Dynamic, kind-driven icon (prefer the shared map): | ||
|
|
||
| ```tsx | ||
| import { SOURCE_KIND_ICONS } from '@/components/sourceSelectUtils'; | ||
|
|
||
| <span>{SOURCE_KIND_ICONS[source.kind]}</span>; | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| import { useCallback } from 'react'; | ||
| import { Button, Popover, Stack, Text } from '@mantine/core'; | ||
| import { IconArrowRight, IconConnection } from '@tabler/icons-react'; | ||
|
|
||
| import { useLocalStorage } from '@/utils'; | ||
|
|
||
| import { VIEW_TRACE_CALLOUT_DISMISSED_KEY } from './viewTraceCallout'; | ||
|
|
||
| type ViewTraceCalloutButtonProps = { | ||
| /** True while the correlated trace hasn't resolved; the button is disabled. */ | ||
| disabled: boolean; | ||
| /** Navigate to the correlated trace. Only reachable when not disabled. */ | ||
| onView: () => void; | ||
| }; | ||
|
|
||
| /** | ||
| * The prominent "View trace" action shown in a log side panel, wrapped in a | ||
| * one-time nudge popover. | ||
| * | ||
| * The callout is dismissed only by an explicit acknowledgement — clicking | ||
| * "Got it" or the button itself — which persists under its own localStorage key | ||
| * (not user settings). It deliberately does not close on stray clicks, drawer | ||
| * resizes, or Escape: Escape stays owned by the panel-level hotkey so the | ||
| * callout never fights it for the keypress. Because nothing transient is | ||
| * stored, the nudge simply reappears the next time the panel opens on an | ||
| * eligible log until it is acknowledged. | ||
| */ | ||
| export function ViewTraceCalloutButton({ | ||
| disabled, | ||
| onView, | ||
| }: ViewTraceCalloutButtonProps) { | ||
| const [dismissed, setDismissed] = useLocalStorage( | ||
| VIEW_TRACE_CALLOUT_DISMISSED_KEY, | ||
| false, | ||
| ); | ||
| const dismiss = useCallback(() => setDismissed(true), [setDismissed]); | ||
|
|
||
| return ( | ||
| <Popover | ||
| width={260} | ||
| position="bottom-end" | ||
| withArrow | ||
| shadow="md" | ||
| trapFocus={false} | ||
| // Intentional: an outside click neither dismisses the callout nor is it | ||
| // treated as an accidental dismissal. This is a one-time, one-line hint | ||
| // that only ever covers a small strip below the button; it is dismissed | ||
| // deliberately via "Got it" or by clicking View Trace, and does not | ||
| // reappear once acknowledged. We accept that the covered controls are | ||
| // briefly non-interactive so a stray click can't burn the message before | ||
| // it is read. (Reviewed: PR #2815 — kept by design.) | ||
| closeOnClickOutside={false} | ||
| closeOnEscape={false} | ||
| opened={!disabled && !dismissed} | ||
| > | ||
| <Popover.Target> | ||
| <Button | ||
| data-testid="side-panel-view-trace" | ||
| variant="secondary" | ||
| size="compact-sm" | ||
| ml="auto" | ||
| leftSection={<IconConnection size={14} />} | ||
| rightSection={<IconArrowRight size={14} />} | ||
| onClick={() => { | ||
| dismiss(); | ||
| onView(); | ||
| }} | ||
| disabled={disabled} | ||
| > | ||
| View Trace | ||
| </Button> | ||
| </Popover.Target> | ||
| <Popover.Dropdown | ||
| data-testid="view-trace-callout" | ||
| role="status" | ||
| aria-live="polite" | ||
| > | ||
| <Stack gap="xs"> | ||
| <Text size="sm" fw={600}> | ||
| Jump to this log's full trace | ||
| </Text> | ||
| <Text size="xs" c="dimmed"> | ||
| Open the correlated trace in one click. | ||
| </Text> | ||
| <Button | ||
| variant="primary" | ||
| size="compact-xs" | ||
| ml="auto" | ||
| onClick={dismiss} | ||
| > | ||
| Got it | ||
| </Button> | ||
| </Stack> | ||
| </Popover.Dropdown> | ||
| </Popover> | ||
| ); | ||
| } | ||
70 changes: 70 additions & 0 deletions
70
packages/app/src/components/__tests__/ViewTraceCalloutButton.test.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| import { fireEvent, screen } from '@testing-library/react'; | ||
|
|
||
| import { VIEW_TRACE_CALLOUT_DISMISSED_KEY } from '@/components/viewTraceCallout'; | ||
| import { ViewTraceCalloutButton } from '@/components/ViewTraceCalloutButton'; | ||
|
|
||
| describe('ViewTraceCalloutButton', () => { | ||
| beforeEach(() => { | ||
| window.localStorage.clear(); | ||
| }); | ||
|
|
||
| it('shows the one-time callout when enabled and not yet dismissed', () => { | ||
| renderWithMantine( | ||
| <ViewTraceCalloutButton disabled={false} onView={jest.fn()} />, | ||
| ); | ||
|
|
||
| expect(screen.getByTestId('side-panel-view-trace')).toBeEnabled(); | ||
| expect(screen.getByTestId('view-trace-callout')).toBeInTheDocument(); | ||
| expect(screen.getByText('Got it')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('does not open the callout while the trace is unresolved (disabled)', () => { | ||
| renderWithMantine(<ViewTraceCalloutButton disabled onView={jest.fn()} />); | ||
|
|
||
| expect(screen.getByTestId('side-panel-view-trace')).toBeDisabled(); | ||
| expect(screen.queryByTestId('view-trace-callout')).not.toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('does not show the callout once it has been dismissed previously', () => { | ||
| window.localStorage.setItem( | ||
| VIEW_TRACE_CALLOUT_DISMISSED_KEY, | ||
| JSON.stringify(true), | ||
| ); | ||
|
|
||
| renderWithMantine( | ||
| <ViewTraceCalloutButton disabled={false} onView={jest.fn()} />, | ||
| ); | ||
|
|
||
| // Button still available, but the nudge no longer appears. | ||
| expect(screen.getByTestId('side-panel-view-trace')).toBeInTheDocument(); | ||
| expect(screen.queryByTestId('view-trace-callout')).not.toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('persists dismissal and navigates when the View Trace button is clicked', () => { | ||
| const onView = jest.fn(); | ||
| renderWithMantine( | ||
| <ViewTraceCalloutButton disabled={false} onView={onView} />, | ||
| ); | ||
|
|
||
| fireEvent.click(screen.getByTestId('side-panel-view-trace')); | ||
|
|
||
| expect(onView).toHaveBeenCalledTimes(1); | ||
| expect(window.localStorage.getItem(VIEW_TRACE_CALLOUT_DISMISSED_KEY)).toBe( | ||
| JSON.stringify(true), | ||
| ); | ||
| }); | ||
|
|
||
| it('persists dismissal without navigating when "Got it" is clicked', () => { | ||
| const onView = jest.fn(); | ||
| renderWithMantine( | ||
| <ViewTraceCalloutButton disabled={false} onView={onView} />, | ||
| ); | ||
|
|
||
| fireEvent.click(screen.getByText('Got it')); | ||
|
|
||
| expect(onView).not.toHaveBeenCalled(); | ||
| expect(window.localStorage.getItem(VIEW_TRACE_CALLOUT_DISMISSED_KEY)).toBe( | ||
| JSON.stringify(true), | ||
| ); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| /** | ||
| * localStorage key for the one-time "View trace" button callout dismissal. | ||
| * | ||
| * Kept in this dependency-free module so it can be shared by the React | ||
| * component and the Playwright fixture (which seeds it) without either copy | ||
| * drifting from the other. | ||
| */ | ||
| export const VIEW_TRACE_CALLOUT_DISMISSED_KEY = | ||
| 'hdx-view-trace-callout-dismissed'; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.