fix: open coder_app links in new tab when open_in is tab (#23000)

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
`<a>` 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 はな <kayla@tree.camp>
This commit is contained in:
Charlie Voiselle
2026-03-17 15:32:45 -04:00
committed by GitHub
co-authored by Kayla はな
parent 91ec0f1484
commit d3c9469e13
2 changed files with 37 additions and 2 deletions
@@ -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(
<AppLink app={app} workspace={MockWorkspace} agent={MockWorkspaceAgent} />,
);
};
// 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");
});
});
+12 -2
View File
@@ -135,7 +135,12 @@ export const AppLink: FC<AppLinkProps> = ({
const button = grouped ? (
<DropdownMenuItem asChild>
<a href={canClick ? link.href : undefined} onClick={link.onClick}>
<a
href={canClick ? link.href : undefined}
onClick={link.onClick}
target={app.open_in === "tab" ? "_blank" : undefined}
rel={app.open_in === "tab" ? "noreferrer" : undefined}
>
{icon}
{link.label}
{ShareIcon && <ShareIcon />}
@@ -143,7 +148,12 @@ export const AppLink: FC<AppLinkProps> = ({
</DropdownMenuItem>
) : (
<AgentButton asChild>
<a href={canClick ? link.href : undefined} onClick={link.onClick}>
<a
href={canClick ? link.href : undefined}
onClick={link.onClick}
target={app.open_in === "tab" ? "_blank" : undefined}
rel={app.open_in === "tab" ? "noreferrer" : undefined}
>
{icon}
{link.label}
{ShareIcon && <ShareIcon />}