Skip to content

feat: Support import/export for dashboard onClicks#2148

Merged
kodiakhq[bot] merged 4 commits into
mainfrom
drew/dashboard-linking-import
May 11, 2026
Merged

feat: Support import/export for dashboard onClicks#2148
kodiakhq[bot] merged 4 commits into
mainfrom
drew/dashboard-linking-import

Conversation

@pulpdrew
Copy link
Copy Markdown
Contributor

@pulpdrew pulpdrew commented Apr 22, 2026

Summary

This is the fourth in a series of PRs adding customizable on-click / linking behaviors to dashboard tables.

This feature is behind the NEXT_PUBLIC_IS_DASHBOARD_LINKING_ENABLED, which has been enabled in the preview environment and for local development.

Scope

In this PR: Dashboards with on-click behaviors that reference other dashboards or sources by ID can now be exported and imported.

Note: we do not currently support editing template-based mappings during import. Those are of course editable after importing.

Not included yet

Future PRs will add:

  • Passing templated filter values to the destination dashboard or search
  • Updates to the external API to support the new onClick fields
  • Updates to the MCP prompts to support generating dashboards with custom links
  • Updates to the import flow to support importing bundles of linked dashboards at once
  • Support for linking on other visualization types

Screenshots or video

Screen.Recording.2026-04-23.at.5.01.21.PM.mov

How to test locally or on Vercel

This can be tested in the preview environment

References

  • Linear Issue: Closes HDX-4091
  • Related PRs:

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 22, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hyperdx-oss Ready Ready Preview, Comment May 11, 2026 8:07pm

Request Review

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 22, 2026

🦋 Changeset detected

Latest commit: 9a2941c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@hyperdx/common-utils Patch
@hyperdx/app Patch
@hyperdx/api Patch
@hyperdx/otel-collector Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pulpdrew pulpdrew force-pushed the drew/dashboard-dashboard-linking branch 3 times, most recently from 8902ef4 to 7f746b7 Compare April 23, 2026 18:23
@pulpdrew pulpdrew force-pushed the drew/dashboard-linking-import branch from 50c116c to 15232c6 Compare April 23, 2026 20:56
@pulpdrew pulpdrew changed the base branch from drew/dashboard-dashboard-linking to main April 27, 2026 02:34
@pulpdrew pulpdrew force-pushed the drew/dashboard-linking-import branch from 15232c6 to 2e4ef50 Compare April 27, 2026 02:50
@pulpdrew pulpdrew marked this pull request as ready for review April 27, 2026 02:50
@github-actions github-actions Bot added the review/tier-3 Standard — full human review required label Apr 27, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 27, 2026

🟡 Tier 3 — Standard

Introduces new logic, modifies core functionality, or touches areas with non-trivial risk.

Why this tier:

  • Diff size: 477 production lines changed (Tier 2 max: < 250)
  • Cross-layer change: touches frontend (packages/app) + shared utils (packages/common-utils)

Review process: Full human review — logic, architecture, edge cases.
SLA: First-pass feedback within 1 business day.

Stats
  • Production files changed: 4
  • Production lines changed: 477 (+ 574 in test files, excluded from tier calculation)
  • Branch: drew/dashboard-linking-import
  • Author: pulpdrew

To override this classification, remove the review/tier-3 label and apply a different review/tier-* label. Manual overrides are preserved on subsequent pushes.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 27, 2026

PR Review

✅ No critical issues found.

Solid implementation with strong test coverage (unit tests for the export rewrite, E2E for the import flow including propagation and unmapped-onClick drop behavior). A few minor observations, none blocking:

  • ℹ️ convertOnClickTargetIdToName mutates config and deletes onClick — safe here because convertToTileTemplate operates on a structuredClone (packages/common-utils/src/core/utils.ts:507), but the mutation pattern is worth keeping in mind if this helper is ever reused elsewhere.
  • ℹ️ Name-based lookup uses toLowerCase() for matching (DBDashboardImportPage.tsx:264,293,305) — consistent with existing tile/filter source mapping, but two sources/dashboards differing only in case will collide. Acceptable as a known limitation matching existing behavior.
  • ℹ️ Silently dropping an onClick whose target was deleted at export time (utils.ts:493) is a reasonable choice but invisible to the user. Consider surfacing a warning in a future iteration if exporters get confused.
  • ℹ️ Propagation effect at DBDashboardImportPage.tsx:341- shares isUpdatingRef with the dashboard-propagation effect at line 339+ and now reacts to three watched arrays; the dirty-field guard keeps it correct, but the control flow is getting dense. Worth a small refactor in a follow-up if more mapping types get added.
  • ℹ️ useDashboards() now also fires on DBDashboardPage to support export (DBDashboardPage.tsx:1199). Fine for now but it's an extra full-list fetch per dashboard view.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 27, 2026

E2E Test Results

All tests passed • 174 passed • 3 skipped • 1220s

Status Count
✅ Passed 174
❌ Failed 0
⚠️ Flaky 3
⏭️ Skipped 3

Tests ran across 4 shards in parallel.

View full report →

@pulpdrew pulpdrew requested a review from teeohhem April 27, 2026 04:15
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 11, 2026

Deep Review

🔴 P0/P1 -- must fix

  • packages/common-utils/src/core/utils.ts:466 -- convertToDashboardTemplate gained a fourth dashboards parameter with default []; any pre-PR three-arg caller of this public @hyperdx/common-utils export now silently drops every dashboard-type onClick on export (the dashboards.find(...) lookup returns undefined and the code path is delete config.onClick), a behavior change with no compile error.
    • Fix: Make dashboards required so callers fail to compile until they audit their export path, or split the change into two functions (one with onClick rewriting, one without).
    • api-contract
  • packages/app/src/DBDashboardImportPage.tsx:562 -- findSource(...)!.id and findConnection(...)!.id use non-null assertions on Source | undefined / Connection | undefined, but MappingFormStateSchema allows empty-string mappings, so submitting with any unselected tile-source or RawSQL-connection row throws a TypeError swallowed by the generic "Something went wrong" toast with no per-row indication.
    • Fix: Add .refine(s => s.length > 0, ...) to each required mapping element in MappingFormStateSchema (or guard before assertion) so the form surfaces a row-level required error instead of crashing.
    • correctness, kieran-typescript
  • packages/app/src/DBDashboardImportPage.tsx:289 -- The onClick search-source auto-init sources.find(s => s.name.toLowerCase() === targetName) (and the propagation cascade) does not filter by source kind, so a metric or session source whose name matches the template's onClick target name lands in onClickSourceMappings, while the rendered dropdown filters with isLogSource(s) || isTraceSource(s) and silently hides the value -- the user sees an empty cell but the invalid id persists into the saved dashboard.
    • Fix: Apply the same isLogSource(s) || isTraceSource(s) filter inside the auto-init find and inside the propagation keysForOnClicksWithMatchingSource write loop so non-log/non-trace ids can never enter onClickSourceMappings.
    • adversarial
  • packages/app/src/DBDashboardImportPage.tsx:305 -- dashboards.find(d => d.name.toLowerCase() === targetName) (and the matching sources.find at line 295) takes the first match by name; if the destination workspace has two dashboards or sources with the same name, the onClick is silently bound to whichever the API returns first, and any user with rename rights can quietly redirect imported onClicks by creating a colliding name.
    • Fix: On import, detect duplicate name matches in the destination workspace and surface a per-row warning ("Ops matches 2 dashboards -- pick one explicitly"); on export, detect duplicate names in the source workspace and either fail loudly or emit a disambiguated target.
    • adversarial, testing
  • packages/app/src/DBDashboardImportPage.tsx:537 -- applyOnClick's "drop if not mapped" branch (if (!mappedId) return { ...config, onClick: undefined }) is unreachable when auto-init populated onClickSourceMappings[idx] with an out-of-options id (see the cross-kind cascade above), so the documented "leave blank to drop" behavior fails for any onClick whose template name matches a non-log/non-trace source in the destination workspace.
    • Fix: At submit time, validate that mappedId is a member of the same option set the user could have selected (log/trace for search, existing dashboards for dashboard); otherwise drop the onClick.
    • adversarial
  • packages/app/src/DBDashboardImportPage.tsx:261 -- Nine as SavedChartConfig casts on tile.config (lines 261, 270, 290, 301, 402, 447, 455, 490, 501) silence the checker across the whole config surface; DashboardTemplate.tiles[i].config is typed via TileTemplateSchema (the *WithoutAlert union), and if SavedChartConfigSchema ever adds a required field, every read here will silently degrade.
    • Fix: Export a TileTemplateConfig type alias from @hyperdx/common-utils/types and use it directly, or unify TileTemplateSchema.config with SavedChartConfigSchema via .extend so the casts become unnecessary.
    • kieran-typescript

🟡 P2 -- recommended

  • packages/app/src/DBDashboardPage.tsx:2172 -- The Export menu item guards on !sources || !dashboard but not !allDashboards; if useDashboards() is still loading or errored, allDashboards ?? [] flows into convertToDashboardTemplate and every dashboard-type onClick is silently dropped from the downloaded JSON.
    • Fix: Add !allDashboards to the guard so the menu shows "Export Failed" until the dashboards query resolves.
    • correctness
  • packages/common-utils/src/core/utils.ts:472 -- DashboardTemplate.version stays at '0.1.0' despite a semantic change in what id-mode target.id carries (was a workspace id, now a human name); any pre-PR exported template re-imported into a post-PR instance hits sources.find(s => s.name === <raw-uuid>), fails to match, and the onClick is silently dropped with no migration path.
    • Fix: Bump DashboardTemplate.version to 0.2.0, branch the import on version, and either reject older templates with a clear error or map the legacy id through the old code path.
    • api-contract, adversarial
  • packages/common-utils/src/types.ts:680 -- The export rewrites target.id to a name but keeps mode: 'id', so OnClickTargetSchema's discriminated union now means id-or-name-depending-on-context; any downstream consumer (external API mirrors, IaC, third-party tooling) that reads mode === 'id' as a true id will be misled.
    • Fix: Introduce a third target mode (e.g. { mode: 'name', name: string }) and have the exporter emit it, so the encoding is self-describing.
    • adversarial
  • packages/common-utils/src/core/utils.ts:484 -- convertOnClickTargetIdToName silently drops onClicks whose referenced source/dashboard is missing, but the export site at DBDashboardPage.tsx:2180 shows no notification beyond a successful download; users exporting a dashboard whose onClick target was deleted weeks ago lose data without knowing.
    • Fix: Have convertToDashboardTemplate return { template, droppedOnClicks: string[] } and surface the count via a Mantine notification at the export site.
    • adversarial
  • packages/app/src/DBDashboardImportPage.tsx:341 -- The combined propagation effect's "find first changed array" walk advances only one of three prev*Refs per render; when the reset effect synchronously calls setValue for all five mapping arrays (initial data load), the unadvanced refs trigger phantom second-pass propagation on the next render, producing N extra renders and re-running the cascade with stale inputSourceName semantics.
    • Fix: At the start of the effect, snapshot all three current arrays and advance their refs before any branching; or restructure the effect so each watched array has its own focused effect, eliminating the first-changed waterfall.
    • correctness, julik-frontend-races, maintainability
  • packages/app/src/DBDashboardImportPage.tsx:341 -- isUpdatingRef provides no mutual exclusion across the three propagation effects: React fires them sequentially in one flush, and each effect sets/clears the ref synchronously before the next runs, so effects 2 and 3 always see false while effect 1's writes are still propagating through the form state.
    • Fix: Either reset isUpdatingRef in a queueMicrotask after all effects in the flush complete, or replace the boolean lock with a per-render counter (or drop it and rely on the per-array prev*Ref to provide idempotency).
    • julik-frontend-races
  • packages/common-utils/src/core/utils.ts:484 -- convertOnClickTargetIdToName is defined as a closure inside convertToDashboardTemplate, capturing sources and dashboards; the new unit-test block has to construct a full Dashboard object for each case to drive it, which is why no test covers whereTemplate/filters carry-over or empty-string name.
    • Fix: Lift it to a free function rewriteOnClickIdToName(config, sources, dashboards) so tests can exercise its branches directly.
    • maintainability
  • packages/app/src/DBDashboardImportPage.tsx:260 -- The case-insensitive find(item => item.name.toLowerCase() === candidate.toLowerCase())?.id || '' pattern is duplicated five times in the reset effect (tile-source, connection, filter-source, onClick-source, onClick-dashboard) with subtle differences (|| '' vs ?? ''); one site drifting would be hard to spot.
    • Fix: Extract a matchId(items, name) helper and call it five times; tests then need to cover one place, not five.
    • maintainability
  • packages/app/src/DBDashboardImportPage.tsx:537 -- The import-side applyOnClick (template-passthrough / id→workspace-id map / drop-on-empty) has no unit test; only @full-stack-tagged E2E tests exercise it, so if the e2e suite is skipped on a fast CI gate the three branches are uncovered.
    • Fix: Extract applyOnClickMapping(inputOnClick, searchId, dashboardId): SavedChartConfig as a pure helper and unit-test it directly alongside the convertToDashboardTemplate tests.
    • testing
  • packages/api/src/utils/zod.ts:403 -- externalDashboardTileSchema (and the MCP tile schemas in packages/api/src/mcp/tools/dashboards/schemas.ts) carry no onClick field, so hyperdx_get_dashboard returns tiles with onClick silently stripped and hyperdx_save_dashboard rejects onClicks at validation; an agent reading and writing back a dashboard erases every onClick on every tile. (Pre-existing relative to the OnClick schema, but this PR newly establishes onClick as a first-class round-trippable concept and amplifies the parity gap.)
    • Fix: Mirror OnClickSchema into externalDashboardTileSchema and the mcpTileSchema variants, plus map it through convertTileToExternalChart and translateExternalChartToTileConfig.
    • agent-native
🔵 P3 nitpicks (8)
  • packages/app/src/DBDashboardImportPage.tsx:278 -- The reset effect's setValue('filterSourceMappings', filterSourceMappings) can pass undefined when input.filters is absent, since input.filters?.map(...) short-circuits.
    • Fix: Use setValue('filterSourceMappings', filterSourceMappings ?? []) to keep the field aligned with the [] default.
  • packages/common-utils/src/core/utils.ts:498 -- mode: 'id' as const is redundant inside an object literal whose surrounding OnClickTarget type already pins the discriminant.
    • Fix: Drop as const.
  • packages/common-utils/src/types.ts:710 -- OnClickSearchById and OnClickDashboardById are exported but only consumed inside DBDashboardImportPage.tsx; the type guards alone are the meaningful public surface.
    • Fix: Make the type aliases internal (drop the export) -- the guard return types remain inferable.
    • maintainability, kieran-typescript
  • .changeset/three-shrimps-complain.md:1 -- Bumps @hyperdx/common-utils as patch despite this PR adding two new exported type guards and two new exported type aliases; recent comparable PRs use minor for new exports.
    • Fix: Change @hyperdx/common-utils: patch to minor (the package is private so impact is low, but it aligns with observed convention).
    • api-contract, project-standards
  • packages/app/src/DBDashboardImportPage.tsx:419 -- Propagation setValue calls pass shouldValidate: true but not shouldDirty: true; the isDirty guard ends up keying on "value differs from '' default" rather than "user touched it", so after first propagation the field is dirty regardless of user intent.
    • Fix: Track user-vs-auto separately (e.g., a userTouched set updated only by the SelectControlled onChange) or set the field through setValue(..., { shouldDirty: false }) for propagation writes.
  • packages/app/src/DBDashboardImportPage.tsx:1-863 -- File grew to 863 lines (was 618 on main, already over the 300-line guidance in CLAUDE.md); the new onClick logic adds ~245 lines to an already-oversize component.
    • Fix: Extract the mapping table and propagation effects into a <MappingTable> / useMappingPropagation() module.
    • project-standards
  • packages/app/tests/e2e/page-objects/DashboardImportPage.ts:121 -- selectMapping's placeholder ternary ('Select a connection' vs 'Select a dashboard' vs 'Select a source') couples the page object to UI copy strings; adding a future mapping type that reuses 'Select a source' would silently fall through.
    • Fix: Add a data-testid per mapping type on the Select and target it from the page object instead.
  • packages/app/tests/e2e/features/dashboard-template-import.spec.ts:31 -- writeTempTemplate writes to os.tmpdir() with no cleanup hook; harmless but sets a bad precedent for future authors copying the helper.
    • Fix: Track written paths in a tempFiles: string[] and fs.unlink them in afterEach.

Reviewers (10): correctness, testing, maintainability, project-standards, agent-native, learnings, api-contract, kieran-typescript, julik-frontend-races, adversarial.

Testing gaps:

  • No test exercises the cross-kind contamination path (a metric source named identically to the template's onClick target).
  • No test covers a pre-PR exported template (raw-id target.id) being re-imported -- the entire migration scenario is untested.
  • No unit test for the import-side applyOnClick (template passthrough / id→workspace-id / drop-on-empty branches).
  • No unit test for isOnClickSearchById / isOnClickDashboardById covering onClick === undefined, wrong-type input, and mode: 'template'.
  • No test exercises the multi-array simultaneous-change render (all three mapping arrays differing in one batched commit) that drives the phantom-propagation finding.
  • No test for the isDirty user-override guard (auto-propagate, user overrides, second propagation should leave the override alone).
  • No round-trip test through the external REST or MCP API surface that asserts onClick is preserved (it currently isn't, anywhere outside the UI).

@pulpdrew
Copy link
Copy Markdown
Contributor Author

packages/app/src/DBDashboardImportPage.tsx:309 -- the init useEffect writes all five mapping arrays via setValue after sources/connections/dashboards resolve, but every prevXxxRef was seeded from the initial ''-filled defaults, so the propagation effect treats the auto-init writes as a user edit and the first-changed-index cascade clobbers any tile/onClick that should have mapped to a different source.

This is fine, as the propagation will only run on initial load of a file, at which point the user has not made selections

packages/common-utils/src/core/utils.ts:473 -- exports still report version: '0.1.0' even though the semantics of onClick.target.id flipped from a workspace ID to a human-readable name, so any pre-PR template re-imported post-PR silently fails the name lookup and drops every id-mode onClick.

This feature is still toggled off, so no user can run into the semantic change - these are backwards compatible changes and no version bump on the format is necessary.

packages/api/src/mcp/tools/dashboards/schemas.ts:301 -- every variant of mcpTilesParam (mcpLineTileSchema, mcpBarTileSchema, mcpTableTileSchema, etc.) omits onClick, so an agent cannot create or update a dashboard with the new linking behavior even though SavedChartConfigSchema accepts it.
packages/api/src/routers/external-api/v2/utils/dashboards.ts:111 -- convertToExternalTileChartConfig and translateExternalChartToTileConfig build tile configs without an onClick field; v2 GETs hide existing onClicks and v2 PATCHes silently erase them on every touched tile.

There are followup tickets for the external API and MCP updates (silly bot, read the PR description won't you??)

packages/app/src/DBDashboardImportPage.tsx:571 -- source!.id and connection!.id non-null assertions in onSubmit evaluate to undefined at runtime when useSources/useConnections has refetched and the previously-mapped record no longer exists, sending { source: undefined } to the server and surfacing only a generic "Something went wrong" toast.

The sources changing during the actual import is not an edge case I am worried about, sources are not deleted frequently.

Copy link
Copy Markdown
Contributor

@teeohhem teeohhem left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm!

@kodiakhq kodiakhq Bot merged commit 401dff5 into main May 11, 2026
19 checks passed
@kodiakhq kodiakhq Bot deleted the drew/dashboard-linking-import branch May 11, 2026 20:13
kodiakhq Bot pushed a commit that referenced this pull request May 28, 2026
…tile row click (#2321)

## Summary

When a dashboard table tile is configured with an `onClick` action ([#2140](#2140), [#2141](#2141), [#2146](#2146), [#2148](#2148)), the row click destination is now discoverable before the user commits to the click.

**What's new for users:**

- **Hover hint.** After ~250ms of hover, a small card above the row reads `Search HyperDX Logs` or `Open dashboard "API Latency Drilldown"` (or generic `Open in search` / `Open dashboard` when the target is templated or the named source / dashboard no longer exists).
- **Native browser behaviors.** The cell wrapper is now a real `<a href>`. Cmd-click opens the destination in a new tab, middle-click opens it in a new tab, right-click shows the browser context menu with "Open in New Tab" and "Copy Link Address", and the destination URL appears in the status bar on hover.
- **Keyboard navigation.** Tab focuses the first clickable cell with a visible focus ring; Enter activates it.

### Screenshots

Search action: hovering a row reveals the resolved source name.

| Light | Dark |
|---|---|
| ![Search onClick hover hint, light theme](https://raw.githubusercontent.com/hyperdxio/hyperdx/assets/pr-2321/onclick-search-hover.png) | ![Search onClick hover hint, dark theme](https://raw.githubusercontent.com/hyperdxio/hyperdx/assets/pr-2321/onclick-search-hover-dark.png) |

Dashboard action: hovering a row reveals the resolved dashboard name.

| Light | Dark |
|---|---|
| ![Dashboard onClick hover hint, light theme](https://raw.githubusercontent.com/hyperdxio/hyperdx/assets/pr-2321/onclick-dashboard-hover.png) | ![Dashboard onClick hover hint, dark theme](https://raw.githubusercontent.com/hyperdxio/hyperdx/assets/pr-2321/onclick-dashboard-hover-dark.png) |

**How the implementation changes:**

- `packages/app/src/HDXMultiSeriesTableChart.tsx`: cell wrapper goes from `<div role="link" tabIndex={0}>` plus manual `onClick` / `onAuxClick` / `onMouseDown` / `onKeyDown` handlers to a Next.js `<Link href>` with `prefetch={false}`. The HoverCard wraps the whole `<tr>` at the row body level so the hint position stays stable as the cursor moves across cells; cell-level mounting would flicker per column. Rows whose templates fail to resolve render as a real `<button type="button">` (not `<a href="#">`) so cmd-click / middle-click / right-click "Open in New Tab" can't silently open a meaningless new tab against a `#` fragment.
- `packages/app/src/components/DBTableChart.tsx`: drops the parent `onRowClick` cmd/middle-click branching; threads new `getRowAction` (or legacy `getRowSearchLink`) into Table.
- `packages/common-utils/src/core/linkUrlBuilder.ts`: new `describeOnClick({ onClick, sourceNamesById, dashboardNamesById }): string` helper returns the one-line hint, with an exhaustiveness check on the OnClick discriminator.
- `packages/app/src/hooks/useOnClickLinkBuilder.ts`: also builds `Map<id, name>` for sources and dashboards, computes the row-independent description once, and returns a per-row resolver of shape `{ url, description, onClickError? }`. Per-row results are memoized internally (WeakMap) so cells sharing a row don't rerun handlebars rendering. When a row's templates fail to resolve, `url` is `null` and `onClickError` fires the existing Mantine "Link error" toast on click.
- Focus ring uses the shared `styles/focus.module.scss` `focusRing` style.
- Cell wrappers carry `data-testid="dashboard-table-row-action"` on both branches; the e2e page object resolves by testid so future inline column links don't steal the click.
- The legacy `getRowSearchLink` drilldown path (used outside dashboards, e.g. the services dashboard) renders as a plain `<Link href>` without a HoverCard so behavior there is unchanged.

## Test plan

- [x] `make ci-lint` clean across all 3 workspaces.
- [x] `make ci-unit` clean. New `HDXMultiSeriesTableChart` component test covers both success and failure branches end to end: anchor / button shape, `onClickError` wiring, row-level HoverCard hint visibility, legacy `getRowSearchLink` fallback.
- [x] `describeOnClick` unit tests cover all 6 OnClickSchema discriminator + name-lookup combinations.
- [x] `useOnClickLinkBuilder` hook tests cover: no onClick configured returns null, resolved source name in description, resolved dashboard name in description, row resolution failure encodes as `url: null` + click handler fires notification, WeakMap caching shares results across cells of the same row.
- [x] `knip` clean (no new unused exports).
- [x] UI verified in the dev stack (screenshots above). Status bar shows the destination URL on the success branch; failure branch (a `<button>`) shows no status bar URL and clicking fires the existing red `Link error` toast. Right-click on a success anchor shows the native context menu; cmd-click and middle-click open in a new tab.
- [x] Both light and dark theme verified.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

automerge review/tier-3 Standard — full human review required

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants