Skip to content

Commit 89c67d4

Browse files
BrunoQuaresmacode-asher
authored andcommitted
feat: add app iframe controls
1 parent 9cbe02e commit 89c67d4

File tree

2 files changed

+114
-38
lines changed

2 files changed

+114
-38
lines changed

site/src/pages/TaskPage/TaskAppIframe.tsx

Lines changed: 92 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,21 @@
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 {
10+
EllipsisVertical,
11+
ExternalLinkIcon,
12+
HouseIcon,
13+
RotateCwIcon,
14+
} from "lucide-react";
15+
import { openAppInNewWindow } from "modules/apps/apps";
216
import { useAppLink } from "modules/apps/useAppLink";
317
import type { Task } from "modules/tasks/tasks";
4-
import type { FC } from "react";
18+
import { type FC, useRef } from "react";
519
import { cn } from "utils/cn";
620

721
type TaskAppIFrameProps = {
@@ -31,24 +45,85 @@ export const TaskAppIFrame: FC<TaskAppIFrameProps> = ({
3145
workspace: task.workspace,
3246
});
3347

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

4564
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-
/>
65+
<div className={cn([active ? "block" : "hidden", "w-full h-full"])}>
66+
<div className="bg-surface-tertiary flex items-center p-2 py-1 gap-1">
67+
<Button
68+
size="icon"
69+
variant="subtle"
70+
onClick={(e) => {
71+
e.preventDefault();
72+
if (frameRef.current?.contentWindow) {
73+
frameRef.current.contentWindow.location.reload();
74+
}
75+
}}
76+
>
77+
<RotateCwIcon />
78+
<span className="sr-only">Refresh</span>
79+
</Button>
80+
81+
<Button
82+
size="icon"
83+
variant="subtle"
84+
onClick={(e) => {
85+
e.preventDefault();
86+
if (frameRef.current?.contentWindow) {
87+
frameRef.current.contentWindow.location.href = appHref();
88+
}
89+
}}
90+
>
91+
<HouseIcon />
92+
<span className="sr-only">Home</span>
93+
</Button>
94+
95+
{/* Possibly we will put a URL bar here, but for now we cannot due to
96+
* cross-origin restrictions in iframes. */}
97+
<div className="w-full"></div>
98+
99+
<DropdownMenu>
100+
<DropdownMenuTrigger asChild>
101+
<Button size="icon" variant="subtle" aria-label="More options">
102+
<EllipsisVertical aria-hidden="true" />
103+
<span className="sr-only">More options</span>
104+
</Button>
105+
</DropdownMenuTrigger>
106+
<DropdownMenuContent align="end">
107+
<DropdownMenuItem
108+
onClick={() => {
109+
openAppInNewWindow(frameSrc);
110+
}}
111+
>
112+
<ExternalLinkIcon />
113+
Open app in new tab
114+
</DropdownMenuItem>
115+
</DropdownMenuContent>
116+
</DropdownMenu>
117+
</div>
118+
119+
<iframe
120+
ref={frameRef}
121+
src={frameSrc}
122+
title={link.label}
123+
loading="eager"
124+
className={"w-full h-full border-0"}
125+
allow="clipboard-read; clipboard-write"
126+
/>
127+
</div>
53128
);
54129
};

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)