Skip to content

Commit fadbf77

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

File tree

2 files changed

+115
-39
lines changed

2 files changed

+115
-39
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-ful"])}>
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: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export const TaskApps: FC<TaskAppsProps> = ({ task }) => {
3838
const [activeAppId, setActiveAppId] = useState<string>(() => {
3939
const appId = embeddedApps[0]?.id;
4040
if (!appId) {
41-
throw new Error("No apps found in task");
41+
throw new Error("No active app found in task");
4242
}
4343
return appId;
4444
});
@@ -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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy