fix(preview): say why an outside site shows blank, with Open tab beside it
YouTube, Google and login pages refuse framing and the browser draws a broken-page icon; the hint lived only in the empty state. A page not on this machine now gets a strip under the address bar with Open tab. The refused- address toast says what is allowed in plain words.
This commit is contained in:
@@ -7,7 +7,9 @@
|
||||
* `normalizePreviewUrl` refuses the dashboard's origin and anything but
|
||||
* http(s), and the sandbox leaves out top navigation so a page cannot move
|
||||
* the dashboard away. A site that forbids framing (most login pages) shows
|
||||
* blank; that is the browser's rule, so the way out is the Open button. The
|
||||
* blank or as the browser's broken-page icon; that is the browser's rule and
|
||||
* cannot be read from here, so any page not on this machine gets a strip
|
||||
* saying so with the Open button beside it. The
|
||||
* last address is remembered per workspace in localStorage (a viewer
|
||||
* convenience, never the server's).
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
@@ -16,7 +18,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ExternalLink, Globe, RotateCw } from "lucide-react";
|
||||
import { normalizePreviewUrl } from "../../lib/openInPreview";
|
||||
import { isLoopbackUrl, normalizePreviewUrl } from "../../lib/openInPreview";
|
||||
import { isSubmitEnter } from "../../lib/keys";
|
||||
import { toast } from "../../lib/toast";
|
||||
|
||||
@@ -80,6 +82,7 @@ export function UrlPreview({
|
||||
}
|
||||
}
|
||||
|
||||
const openTab = () => url && window.open(url, "_blank", "noopener,noreferrer");
|
||||
const portList = Object.entries(ports ?? {}).filter(([, p]) => Number.isInteger(p) && p > 0);
|
||||
const btn =
|
||||
"flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-fg-muted transition-colors hover:bg-surface-4 hover:text-fg-primary disabled:opacity-40 disabled:hover:bg-transparent";
|
||||
@@ -115,13 +118,31 @@ export function UrlPreview({
|
||||
type="button"
|
||||
className={btn}
|
||||
disabled={!url}
|
||||
onClick={() => url && window.open(url, "_blank", "noopener,noreferrer")}
|
||||
onClick={openTab}
|
||||
title={t("preview.openTab")}
|
||||
aria-label={t("preview.openTab")}
|
||||
>
|
||||
<ExternalLink className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
{url && !isLoopbackUrl(url) && (
|
||||
// An outside site: most refuse to be framed, and a refusal cannot be
|
||||
// detected from here — so say it, with the way out beside it.
|
||||
<div
|
||||
data-testid="url-preview-blocked-hint"
|
||||
className="flex shrink-0 items-center gap-2 border-b border-border bg-surface-2 px-2 py-1.5 text-[11px] text-fg-secondary"
|
||||
>
|
||||
<span className="min-w-0 flex-1">{t("preview.blockedHint")}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={openTab}
|
||||
className="flex shrink-0 items-center gap-1 rounded-md px-1.5 py-0.5 font-medium text-accent hover:bg-surface-4"
|
||||
>
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
{t("preview.openTabShort")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{url ? (
|
||||
<iframe
|
||||
key={`${url}#${reload}`}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
* @file UrlPreview.test.tsx
|
||||
* @description The Preview tab: port chips before a page is open, the
|
||||
* address bar opening a page in a sandboxed frame without top navigation,
|
||||
* a link's request opening its page, and the dashboard's own origin refused.
|
||||
* a link's request opening its page, the dashboard's own origin refused, and
|
||||
* the framing-refusal strip shown for an outside site only.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
@@ -36,6 +37,17 @@ describe("UrlPreview", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("warns that an outside site may refuse to be framed, never for a local page", () => {
|
||||
render(<UrlPreview laneId={4} ports={{}} request={null} />);
|
||||
const input = screen.getByRole("textbox");
|
||||
fireEvent.change(input, { target: { value: "localhost:3000" } });
|
||||
fireEvent.keyDown(input, { key: "Enter" });
|
||||
expect(screen.queryByTestId("url-preview-blocked-hint")).toBeNull();
|
||||
fireEvent.change(input, { target: { value: "https://youtube.com" } });
|
||||
fireEvent.keyDown(input, { key: "Enter" });
|
||||
expect(screen.getByTestId("url-preview-blocked-hint")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("opens a link's request", () => {
|
||||
render(
|
||||
<UrlPreview laneId={3} ports={{}} request={{ url: "http://localhost:5173/", nonce: 1 }} />
|
||||
|
||||
Reference in New Issue
Block a user