feat: add app iframe controls (#18421)

Add a home and "open in new tab" button.  Other controls are not 
possible due to cross-origin restrictions.

Closes #18178

---------

Co-authored-by: BrunoQuaresma <bruno_nonato_quaresma@hotmail.com>
This commit is contained in:
Asher
2025-06-19 09:22:36 -08:00
committed by GitHub
parent b49e62faad
commit 63b5f0b998
2 changed files with 93 additions and 38 deletions
+71 -17
View File
@@ -1,7 +1,16 @@
import type { WorkspaceApp } from "api/typesGenerated";
import { Button } from "components/Button/Button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "components/DropdownMenu/DropdownMenu";
import { EllipsisVertical, ExternalLinkIcon, HouseIcon } from "lucide-react";
import { useAppLink } from "modules/apps/useAppLink";
import type { Task } from "modules/tasks/tasks";
import type { FC } from "react";
import { type FC, useRef } from "react";
import { Link as RouterLink } from "react-router-dom";
import { cn } from "utils/cn";
type TaskAppIFrameProps = {
@@ -31,24 +40,69 @@ export const TaskAppIFrame: FC<TaskAppIFrameProps> = ({
workspace: task.workspace,
});
let href = link.href;
try {
const url = new URL(link.href);
if (pathname) {
url.pathname = pathname;
const appHref = (): string => {
try {
const url = new URL(link.href, location.href);
if (pathname) {
url.pathname = pathname;
}
return url.toString();
} catch (err) {
console.warn(`Failed to parse URL ${link.href} for app ${app.id}`, err);
return link.href;
}
href = url.toString();
} catch (err) {
console.warn(`Failed to parse URL ${link.href} for app ${app.id}`, err);
}
};
const frameRef = useRef<HTMLIFrameElement>(null);
const frameSrc = appHref();
return (
<iframe
src={href}
title={link.label}
loading="eager"
className={cn([active ? "block" : "hidden", "w-full h-full border-0"])}
allow="clipboard-read; clipboard-write"
/>
<div className={cn([active ? "flex" : "hidden", "w-full h-full flex-col"])}>
<div className="bg-surface-tertiary flex items-center p-2 py-1 gap-1">
<Button
size="icon"
variant="subtle"
onClick={(e) => {
e.preventDefault();
if (frameRef.current?.contentWindow) {
frameRef.current.contentWindow.location.href = appHref();
}
}}
>
<HouseIcon />
<span className="sr-only">Home</span>
</Button>
{/* Possibly we will put a URL bar here, but for now we cannot due to
* cross-origin restrictions in iframes. */}
<div className="w-full"></div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="icon" variant="subtle" aria-label="More options">
<EllipsisVertical aria-hidden="true" />
<span className="sr-only">More options</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem asChild>
<RouterLink to={frameSrc} target="_blank">
<ExternalLinkIcon />
Open app in new tab
</RouterLink>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<iframe
ref={frameRef}
src={frameSrc}
title={link.label}
loading="eager"
className={"w-full h-full border-0"}
allow="clipboard-read; clipboard-write"
/>
</div>
);
};
+22 -21
View File
@@ -57,19 +57,21 @@ export const TaskApps: FC<TaskAppsProps> = ({ task }) => {
return (
<main className="flex-1 flex flex-col">
<div className="border-0 border-b border-border border-solid w-full p-1 flex gap-2">
{embeddedApps.map((app) => (
<TaskAppButton
key={app.id}
task={task}
app={app}
active={app.id === activeAppId}
onClick={(e) => {
e.preventDefault();
setActiveAppId(app.id);
}}
/>
))}
<div className="w-full flex items-center border-0 border-b border-border border-solid">
<div className="p-2 pb-0 flex gap-2 items-center">
{embeddedApps.map((app) => (
<TaskAppTab
key={app.id}
task={task}
app={app}
active={app.id === activeAppId}
onClick={(e) => {
e.preventDefault();
setActiveAppId(app.id);
}}
/>
))}
</div>
{externalApps.length > 0 && (
<div className="ml-auto">
@@ -122,19 +124,14 @@ export const TaskApps: FC<TaskAppsProps> = ({ task }) => {
);
};
type TaskAppButtonProps = {
type TaskAppTabProps = {
task: Task;
app: WorkspaceApp;
active: boolean;
onClick: (e: React.MouseEvent<HTMLAnchorElement>) => void;
};
const TaskAppButton: FC<TaskAppButtonProps> = ({
task,
app,
active,
onClick,
}) => {
const TaskAppTab: FC<TaskAppTabProps> = ({ task, app, active, onClick }) => {
const agent = task.workspace.latest_build.resources
.flatMap((r) => r.agents)
.filter((a) => !!a)
@@ -156,7 +153,11 @@ const TaskAppButton: FC<TaskAppButtonProps> = ({
key={app.id}
asChild
className={cn([
{ "text-content-primary": active },
"px-3",
{
"text-content-primary bg-surface-tertiary rounded-sm rounded-b-none":
active,
},
{ "opacity-75 hover:opacity-100": !active },
])}
>