Skip to content

Commit 63b5f0b

Browse files
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>
1 parent b49e62f commit 63b5f0b

File tree

2 files changed

+93
-38
lines changed

2 files changed

+93
-38
lines changed

site/src/pages/TaskPage/TaskAppIframe.tsx

Lines changed: 71 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
import type { WorkspaceApp } from "api/typesGenerated";
2+
import { Button } from "components/Button/Button";
3+
import {
4+
DropdownMenu,
5+
DropdownMenuContent,
6+
DropdownMenuItem,
7+
DropdownMenuTrigger,
8+
} from "components/DropdownMenu/DropdownMenu";
9+
import { EllipsisVertical, ExternalLinkIcon, HouseIcon } from "lucide-react";
210
import { useAppLink } from "modules/apps/useAppLink";
311
import type { Task } from "modules/tasks/tasks";
4-
import type { FC } from "react";
12+
import { type FC, useRef } from "react";
13+
import { Link as RouterLink } from "react-router-dom";
514
import { cn } from "utils/cn";
615

716
type TaskAppIFrameProps = {
@@ -31,24 +40,69 @@ export const TaskAppIFrame: FC<TaskAppIFrameProps> = ({
3140
workspace: task.workspace,
3241
});
3342

34-
let href = link.href;
35-
try {
36-
const url = new URL(link.href);
37-
if (pathname) {
38-
url.pathname = pathname;
43+
const appHref = (): string => {
44+
try {
45+
const url = new URL(link.href, location.href);
46+
if (pathname) {
47+
url.pathname = pathname;
48+
}
49+
return url.toString();
50+
} catch (err) {
51+
console.warn(`Failed to parse URL ${link.href} for app ${app.id}`, err);
52+
return link.href;
3953
}
40-
href = url.toString();
41-
} catch (err) {
42-
console.warn(`Failed to parse URL ${link.href} for app ${app.id}`, err);
43-
}
54+
};
55+
56+
const frameRef = useRef<HTMLIFrameElement>(null);
57+
const frameSrc = appHref();
4458

4559
return (
46-
<iframe
47-
src={href}
48-
title={link.label}
49-
loading="eager"
50-
className={cn([active ? "block" : "hidden", "w-full h-full border-0"])}
51-
allow="clipboard-read; clipboard-write"
52-
/>
60+
<div className={cn([active ? "flex" : "hidden", "w-full h-full flex-col"])}>
61+
<div className="bg-surface-tertiary flex items-center p-2 py-1 gap-1">
62+
<Button
63+
size="icon"
64+
variant="subtle"
65+
onClick={(e) => {
66+
e.preventDefault();
67+
if (frameRef.current?.contentWindow) {
68+
frameRef.current.contentWindow.location.href = appHref();
69+
}
70+
}}
71+
>
72+
<HouseIcon />
73+
<span className="sr-only">Home</span>
74+
</Button>
75+
76+
{/* Possibly we will put a URL bar here, but for now we cannot due to
77+
* cross-origin restrictions in iframes. */}
78+
<div className="w-full"></div>
79+
80+
<DropdownMenu>
81+
<DropdownMenuTrigger asChild>
82+
<Button size="icon" variant="subtle" aria-label="More options">
83+
<EllipsisVertical aria-hidden="true" />
84+
<span className="sr-only">More options</span>
85+
</Button>
86+
</DropdownMenuTrigger>
87+
<DropdownMenuContent align="end">
88+
<DropdownMenuItem asChild>
89+
<RouterLink to={frameSrc} target="_blank">
90+
<ExternalLinkIcon />
91+
Open app in new tab
92+
</RouterLink>
93+
</DropdownMenuItem>
94+
</DropdownMenuContent>
95+
</DropdownMenu>
96+
</div>
97+
98+
<iframe
99+
ref={frameRef}
100+
src={frameSrc}
101+
title={link.label}
102+
loading="eager"
103+
className={"w-full h-full border-0"}
104+
allow="clipboard-read; clipboard-write"
105+
/>
106+
</div>
53107
);
54108
};

site/src/pages/TaskPage/TaskApps.tsx

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -57,19 +57,21 @@ export const TaskApps: FC<TaskAppsProps> = ({ task }) => {
5757

5858
return (
5959
<main className="flex-1 flex flex-col">
60-
<div className="border-0 border-b border-border border-solid w-full p-1 flex gap-2">
61-
{embeddedApps.map((app) => (
62-
<TaskAppButton
63-
key={app.id}
64-
task={task}
65-
app={app}
66-
active={app.id === activeAppId}
67-
onClick={(e) => {
68-
e.preventDefault();
69-
setActiveAppId(app.id);
70-
}}
71-
/>
72-
))}
60+
<div className="w-full flex items-center border-0 border-b border-border border-solid">
61+
<div className="p-2 pb-0 flex gap-2 items-center">
62+
{embeddedApps.map((app) => (
63+
<TaskAppTab
64+
key={app.id}
65+
task={task}
66+
app={app}
67+
active={app.id === activeAppId}
68+
onClick={(e) => {
69+
e.preventDefault();
70+
setActiveAppId(app.id);
71+
}}
72+
/>
73+
))}
74+
</div>
7375

7476
{externalApps.length > 0 && (
7577
<div className="ml-auto">
@@ -122,19 +124,14 @@ export const TaskApps: FC<TaskAppsProps> = ({ task }) => {
122124
);
123125
};
124126

125-
type TaskAppButtonProps = {
127+
type TaskAppTabProps = {
126128
task: Task;
127129
app: WorkspaceApp;
128130
active: boolean;
129131
onClick: (e: React.MouseEvent<HTMLAnchorElement>) => void;
130132
};
131133

132-
const TaskAppButton: FC<TaskAppButtonProps> = ({
133-
task,
134-
app,
135-
active,
136-
onClick,
137-
}) => {
134+
const TaskAppTab: FC<TaskAppTabProps> = ({ task, app, active, onClick }) => {
138135
const agent = task.workspace.latest_build.resources
139136
.flatMap((r) => r.agents)
140137
.filter((a) => !!a)
@@ -156,7 +153,11 @@ const TaskAppButton: FC<TaskAppButtonProps> = ({
156153
key={app.id}
157154
asChild
158155
className={cn([
159-
{ "text-content-primary": active },
156+
"px-3",
157+
{
158+
"text-content-primary bg-surface-tertiary rounded-sm rounded-b-none":
159+
active,
160+
},
160161
{ "opacity-75 hover:opacity-100": !active },
161162
])}
162163
>

0 commit comments

Comments
 (0)