mirror of
https://github.com/coder/coder.git
synced 2026-06-05 05:58:20 +00:00
aa689cbb39
**Demo:** <img width="1624" height="967" alt="Screenshot 2025-10-21 at 10 45 24" src="https://github.com/user-attachments/assets/b0ae724f-055a-4b13-b2a6-f11f4432de9b" /> Closes https://github.com/coder/internal/issues/1077
154 lines
4.6 KiB
TypeScript
154 lines
4.6 KiB
TypeScript
import { Button } from "components/Button/Button";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from "components/DropdownMenu/DropdownMenu";
|
|
import { Spinner } from "components/Spinner/Spinner";
|
|
import { useProxy } from "contexts/ProxyContext";
|
|
import { EllipsisVertical, ExternalLinkIcon, HouseIcon } from "lucide-react";
|
|
import { useAppLink } from "modules/apps/useAppLink";
|
|
import type { Task, WorkspaceAppWithAgent } from "modules/tasks/tasks";
|
|
import { type FC, type HTMLProps, useRef } from "react";
|
|
import { Link as RouterLink } from "react-router";
|
|
import { cn } from "utils/cn";
|
|
import { TaskWildcardWarning } from "./TaskWildcardWarning";
|
|
|
|
type TaskAppIFrameProps = {
|
|
task: Task;
|
|
app: WorkspaceAppWithAgent;
|
|
active: boolean;
|
|
};
|
|
|
|
export const TaskAppIFrame: FC<TaskAppIFrameProps> = ({
|
|
task,
|
|
app,
|
|
active,
|
|
}) => {
|
|
const link = useAppLink(app, {
|
|
agent: app.agent,
|
|
workspace: task.workspace,
|
|
});
|
|
const proxy = useProxy();
|
|
const frameRef = useRef<HTMLIFrameElement>(null);
|
|
const shouldDisplayWildcardWarning =
|
|
app.subdomain && !proxy.proxy?.preferredWildcardHostname;
|
|
|
|
if (shouldDisplayWildcardWarning) {
|
|
return (
|
|
<div className="h-full flex items-center justify-center pb-4">
|
|
<TaskWildcardWarning />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className={cn([active ? "flex" : "hidden", "w-full h-full flex-col"])}>
|
|
{app.slug === "preview" && (
|
|
<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 = link.href;
|
|
}
|
|
}}
|
|
>
|
|
<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={link.href} target="_blank">
|
|
<ExternalLinkIcon />
|
|
Open app in new tab
|
|
</RouterLink>
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
)}
|
|
|
|
{app.health === "healthy" || app.health === "disabled" ? (
|
|
<TaskIframe ref={frameRef} src={link.href} title={link.label} />
|
|
) : app.health === "unhealthy" ? (
|
|
<div className="w-full h-full flex flex-col items-center justify-center p-4">
|
|
<h3 className="m-0 font-medium text-content-primary text-base text-center">
|
|
App "{app.display_name}" is unhealthy
|
|
</h3>
|
|
<div className="text-content-secondary text-sm">
|
|
<span className="block text-center">
|
|
Here are some troubleshooting steps you can take:
|
|
</span>
|
|
<ul className="m-0 pt-4 flex flex-col gap-4">
|
|
{app.healthcheck && (
|
|
<li>
|
|
<span className="block font-medium text-content-primary mb-1">
|
|
Verify healthcheck
|
|
</span>
|
|
Try running the following inside your workspace:{" "}
|
|
<code className="font-mono text-content-primary select-all">
|
|
curl -v "{app.healthcheck.url}"
|
|
</code>
|
|
</li>
|
|
)}
|
|
<li>
|
|
<span className="block font-medium text-content-primary mb-1">
|
|
Check logs
|
|
</span>
|
|
See{" "}
|
|
<code className="font-mono text-content-primary select-all">
|
|
/tmp/coder-agent.log
|
|
</code>{" "}
|
|
inside your workspace "{task.workspace.name}" for more
|
|
information.
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
) : app.health === "initializing" ? (
|
|
<div className="w-full h-full flex items-center justify-center">
|
|
<Spinner loading />
|
|
</div>
|
|
) : (
|
|
<div className="w-full h-full flex flex-col items-center justify-center">
|
|
<h3 className="m-0 font-medium text-content-primary text-base">
|
|
Error
|
|
</h3>
|
|
<span className="text-content-secondary text-sm">
|
|
The app is in an unknown health state.
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
type TaskIframeProps = HTMLProps<HTMLIFrameElement>;
|
|
|
|
export const TaskIframe: FC<TaskIframeProps> = ({ className, ...props }) => {
|
|
return (
|
|
<iframe
|
|
loading="eager"
|
|
className={cn("w-full h-full border-0", className)}
|
|
allow="clipboard-read; clipboard-write"
|
|
{...props}
|
|
/>
|
|
);
|
|
};
|