From 1ad2fec073ebc5d5d40a46af217ac6e910a3bc02 Mon Sep 17 00:00:00 2001 From: nntrivi2001 Date: Mon, 2 Nov 2026 15:10:22 +0700 Subject: [PATCH] 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. --- client/src/components/lanes/UrlPreview.tsx | 27 ++++++++++++++++--- .../lanes/__tests__/UrlPreview.test.tsx | 14 +++++++++- client/src/i18n/locales/en/lanes.json | 4 ++- client/src/i18n/locales/vi/lanes.json | 4 ++- docs/LANES.md | 2 +- 5 files changed, 44 insertions(+), 7 deletions(-) diff --git a/client/src/components/lanes/UrlPreview.tsx b/client/src/components/lanes/UrlPreview.tsx index ddae9917..5f756cc3 100644 --- a/client/src/components/lanes/UrlPreview.tsx +++ b/client/src/components/lanes/UrlPreview.tsx @@ -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ĩ @@ -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")} > + {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. +
+ {t("preview.blockedHint")} + +
+ )} {url ? (