make the connection settings reachable at /gitea/connection

The form appeared only when nothing was configured, so once a token was saved
there was no way back to it — a member who mistyped theirs, or anyone rotating
one, had nowhere to go. On a single-user machine that is an annoyance; on a
company one it is the ordinary case, because every member has a token of their
own to set.

Uses the route pair the platform already generates for every plugin, so this
needed no platform change. A plug icon beside the open-in-new-tab button, as a
real Link rather than a button, because it is navigation and should be
cmd-clickable.

Saving returns to the frame — otherwise you are left looking at the form you
just submitted, which reads as nothing having happened.

The two halves were already right: the panel shows only the token field when
isOwner is false, and getResolvedServiceCredentials gives a member the owner's
URL with their OWN secret, never the owner's.
This commit is contained in:
officer
2026-08-19 09:58:30 +00:00
parent fe67de0281
commit c31f95155f
2 changed files with 42 additions and 15 deletions
+25 -11
View File
@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react';
import { ExternalLink, Loader2, TriangleAlert } from 'lucide-react';
import { Link, useParams } from 'react-router';
import { ExternalLink, Loader2, Plug, TriangleAlert } from 'lucide-react';
import { useClient } from 'hooks/useClient';
import { Button } from '@/components/ui/button';
import { GiteaConnection, type ConnectionState } from './GiteaConnection';
@@ -25,6 +26,11 @@ import { GiteaConnection, type ConnectionState } from './GiteaConnection';
const FRAME_SRC = '/api/gitea/_web/';
export const GiteaFrame = () => {
// `/gitea/connection` renders the settings instead of the frame. The plugin route pair gives this for
// free — `<route>` and `<route>/:section` are generated for every plugin — and it is the only way to
// change a token once one is set: the form used to appear ONLY when nothing was configured, so a
// member who mistyped theirs, or anyone rotating one, had nowhere to go.
const { section } = useParams();
const { get, post } = useClient();
const [state, setState] = useState<'opening' | 'ready' | 'failed'>('opening');
const [connection, setConnection] = useState<ConnectionState | null>(null);
@@ -83,7 +89,8 @@ export const GiteaFrame = () => {
);
}
if (connection && !connection.configured) {
// Unconfigured goes straight to the form; `/gitea/connection` opens it deliberately.
if (connection && (!connection.configured || section === 'connection')) {
return <GiteaConnection state={connection} onSaved={() => setAttempt((n) => n + 1)} />;
}
@@ -92,15 +99,22 @@ export const GiteaFrame = () => {
{/* 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. */}
<Button
size="sm"
variant="outline"
className="absolute right-3 top-3 z-10 opacity-60 hover:opacity-100"
onClick={() => window.open(FRAME_SRC, '_blank', 'noopener')}
title="Open Gitea in a new tab"
>
<ExternalLink className="h-3.5 w-3.5" />
</Button>
<div className="absolute right-3 top-3 z-10 flex gap-1.5 opacity-60 transition-opacity hover:opacity-100">
{/* A real link, not a button: it is navigation, and it should be cmd-clickable like any other. */}
<Button size="sm" variant="outline" asChild title="Connection settings">
<Link to="/gitea/connection">
<Plug className="h-3.5 w-3.5" />
</Link>
</Button>
<Button
size="sm"
variant="outline"
onClick={() => window.open(FRAME_SRC, '_blank', 'noopener')}
title="Open Gitea in a new tab"
>
<ExternalLink className="h-3.5 w-3.5" />
</Button>
</div>
<iframe
src={FRAME_SRC}
title="Gitea"