mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
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:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 },
|
||||
])}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user