import { useEffect, useState } from 'react'; import { ExternalLink, Loader2, TriangleAlert } from 'lucide-react'; import { useClient } from 'hooks/useClient'; import { Button } from '@/components/ui/button'; // Gitea's own web interface, rendered in place. // // ── Why an iframe and not panels ── // // Gitea has a complete web interface. The version of this that shipped before was seventeen files // reimplementing a fraction of it, and every one of those files was a promise to keep up with a project // that moves faster than this plugin will. // // ── The one thing this component must do ── // // Get a cookie before the frame loads. Everything inside the iframe — stylesheets, scripts, avatars — is // fetched by the browser with no Authorization header, so the frame's URL is served above the platform's // gate and authenticates with a path-scoped cookie instead. That cookie is minted by a normal // authenticated call, which is what this does first. // // Load the iframe before that call returns and every request in it is unauthenticated, which reads as // Gitea being down. const FRAME_SRC = '/api/gitea/_web/'; export const GiteaFrame = () => { const { post } = useClient(); const [state, setState] = useState<'opening' | 'ready' | 'failed'>('opening'); const [error, setError] = useState(null); useEffect(() => { let cancelled = false; // Not in a dependency array: `useClient()` returns a new object every render, so depending on `post` // would re-mint on every keystroke elsewhere in the app. Mount-only is what is meant. void (async () => { try { await post('/gitea/session', {}); if (!cancelled) setState('ready'); } catch (err) { if (cancelled) return; setError(err instanceof Error ? err.message : 'could not open a session'); setState('failed'); } })(); return () => { cancelled = true; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); if (state === 'opening') { return (
Opening Gitea…
); } if (state === 'failed') { return (

Could not open Gitea

{/* The message, not a generic one: the interesting failure here is almost always "the sidecar is not running" or "no instance is configured", and those want different answers. */}

{error}

); } return (
{/* The escape hatch, and it is not an admission of defeat: the whole point of proxying an app that already has a UI is that its own UI stays available. Anything the frame handles badly, this opens properly. */}