From d3c9469e13deb1d5401f4d5c3f7e562d2e9d0aca Mon Sep 17 00:00:00 2001 From: Charlie Voiselle <464492+angrycub@users.noreply.github.com> Date: Tue, 17 Mar 2026 15:32:45 -0400 Subject: [PATCH] fix: open coder_app links in new tab when open_in is tab (#23000) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #18573 ## Changes When a `coder_app` resource sets `open_in = "tab"`, clicking the app link now opens in a new browser tab instead of navigating in the same tab. `target="_blank"` and `rel="noreferrer"` are set inline on the `` elements in `AppLink.tsx`, gated on `app.open_in === "tab"`. This follows the codebase convention of co-locating `target` and `rel` at the render site. `noreferrer` suppresses the Referer header to avoid leaking workspace IDs to destination servers and implies `noopener`. `noopener` prevents tabnabbing — without it, the opened page can redirect the Coder dashboard tab via `window.opener`. This is especially relevant for same-origin path-based apps, which would otherwise have full DOM access to the dashboard. > **Future enhancement**: template admins could opt into sending the referrer via a `coder_app` setting, enabling feedback pages built around workspace context. ## Tests A vitest case is added in `AppLink.test.tsx` (rather than a Storybook story, since the assertions are purely behavioral with no visual component): - **`sets target=_blank and rel=noopener noreferrer when open_in is tab`** — renders the app link with `open_in: "tab"` and asserts `target="_blank"` and `rel="noreferrer"` are present on the anchor. ## Slim-window behavior The `slim-window` test case and the `openAppInNewWindow()` comment in `apps.ts` have been split out into a follow-up PR for separate review, since the `window.open()` / `noopener` tradeoffs there deserve dedicated discussion. --------- Co-authored-by: Kayla はな --- .../resources/AppLink/AppLink.test.tsx | 25 +++++++++++++++++++ .../src/modules/resources/AppLink/AppLink.tsx | 14 +++++++++-- 2 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 site/src/modules/resources/AppLink/AppLink.test.tsx diff --git a/site/src/modules/resources/AppLink/AppLink.test.tsx b/site/src/modules/resources/AppLink/AppLink.test.tsx new file mode 100644 index 0000000000..44676b22c2 --- /dev/null +++ b/site/src/modules/resources/AppLink/AppLink.test.tsx @@ -0,0 +1,25 @@ +import { + MockWorkspace, + MockWorkspaceAgent, + MockWorkspaceApp, +} from "testHelpers/entities"; +import { renderWithAuth } from "testHelpers/renderHelpers"; +import { screen } from "@testing-library/react"; +import { AppLink } from "./AppLink"; + +const renderAppLink = (app: typeof MockWorkspaceApp) => { + return renderWithAuth( + , + ); +}; + +// Regression test for https://github.com/coder/coder/issues/18573: +// open_in="tab" was not opening links in a new tab. +describe("AppLink", () => { + it("sets target=_blank and rel=noreferrer when open_in is tab", async () => { + renderAppLink({ ...MockWorkspaceApp, open_in: "tab" }); + const link = await screen.findByRole("link"); + expect(link).toHaveAttribute("target", "_blank"); + expect(link).toHaveAttribute("rel", "noreferrer"); + }); +}); diff --git a/site/src/modules/resources/AppLink/AppLink.tsx b/site/src/modules/resources/AppLink/AppLink.tsx index 1c26704027..1b023522a3 100644 --- a/site/src/modules/resources/AppLink/AppLink.tsx +++ b/site/src/modules/resources/AppLink/AppLink.tsx @@ -135,7 +135,12 @@ export const AppLink: FC = ({ const button = grouped ? ( - + {icon} {link.label} {ShareIcon && } @@ -143,7 +148,12 @@ export const AppLink: FC = ({ ) : ( - + {icon} {link.label} {ShareIcon && }