feat: disconnect a custom endpoint (DELETE /api/custom-provider + Disconnect in its editor)
Connecting was one-way: POST could only replace the base URL, and the only escape was hand-editing settings.json. DELETE now mirrors POST — drop the stored section, delete the reserved key, unregister the provider from piModels() — and the connect editor grows a Disconnect beside Connect, following the stored-key rows' Delete pattern (quiet text, one click, no confirm, only once there is an endpoint to remove).
This commit is contained in:
@@ -5,6 +5,8 @@
|
|||||||
* fast), store the key, persist { baseUrl, models }, re-sync the
|
* fast), store the key, persist { baseUrl, models }, re-sync the
|
||||||
* provider into piModels(). Discovery lives here because connecting
|
* provider into piModels(). Discovery lives here because connecting
|
||||||
* and discovering are one user action.
|
* and discovering are one user action.
|
||||||
|
* DELETE → disconnect: drop the stored config + the stored key and unregister
|
||||||
|
* the provider (the mirror image of POST; idempotent).
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
deleteKey,
|
deleteKey,
|
||||||
@@ -56,3 +58,15 @@ export async function POST(request: Request) {
|
|||||||
syncCustomProvider(next.customProvider);
|
syncCustomProvider(next.customProvider);
|
||||||
return Response.json({ ok: true, models: next.customProvider?.models ?? [] });
|
return Response.json({ ok: true, models: next.customProvider?.models ?? [] });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Disconnect: forget the endpoint AND its key, then unregister the provider.
|
||||||
|
* Deleting the section (rather than blanking baseUrl) keeps settings.json the
|
||||||
|
* same shape as a never-connected install. Idempotent by construction. */
|
||||||
|
export async function DELETE() {
|
||||||
|
const record = readSettingsRecord() ?? { version: 1 };
|
||||||
|
const { customProvider: _dropped, ...rest } = record;
|
||||||
|
const next = writeSettingsRecord(rest);
|
||||||
|
deleteKey(CUSTOM_PROVIDER.envVar);
|
||||||
|
syncCustomProvider(next.customProvider);
|
||||||
|
return Response.json({ ok: true });
|
||||||
|
}
|
||||||
|
|||||||
@@ -444,6 +444,15 @@ export function KeysSection({
|
|||||||
notifySettingsSaved("keys");
|
notifySettingsSaved("keys");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Forget the custom endpoint (config + key server-side), same rhythm as
|
||||||
|
* deleteKey: fire, re-read, broadcast so the catalog narrows everywhere. */
|
||||||
|
async function disconnectCustom(): Promise<void> {
|
||||||
|
await fetch("/api/custom-provider", { method: "DELETE" }).catch(() => undefined);
|
||||||
|
setAddingCustom(false);
|
||||||
|
await reload();
|
||||||
|
notifySettingsSaved("keys");
|
||||||
|
}
|
||||||
|
|
||||||
if (loading || statuses === null) {
|
if (loading || statuses === null) {
|
||||||
return <p className="px-6 py-5 text-xs text-muted-foreground/70">Loading keys…</p>;
|
return <p className="px-6 py-5 text-xs text-muted-foreground/70">Loading keys…</p>;
|
||||||
}
|
}
|
||||||
@@ -496,6 +505,7 @@ export function KeysSection({
|
|||||||
await reload();
|
await reload();
|
||||||
notifySettingsSaved("keys");
|
notifySettingsSaved("keys");
|
||||||
}}
|
}}
|
||||||
|
onDelete={disconnectCustom}
|
||||||
onCancel={() => setAddingCustom(false)}
|
onCancel={() => setAddingCustom(false)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -660,11 +670,13 @@ function CustomEndpointEditor({
|
|||||||
keyPresent,
|
keyPresent,
|
||||||
onSaved,
|
onSaved,
|
||||||
onCancel,
|
onCancel,
|
||||||
|
onDelete,
|
||||||
}: {
|
}: {
|
||||||
baseUrl0: string;
|
baseUrl0: string;
|
||||||
keyPresent: boolean;
|
keyPresent: boolean;
|
||||||
onSaved: () => void | Promise<void>;
|
onSaved: () => void | Promise<void>;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
|
onDelete?: () => void | Promise<void>;
|
||||||
}) {
|
}) {
|
||||||
const [baseUrl, setBaseUrl] = useState(baseUrl0);
|
const [baseUrl, setBaseUrl] = useState(baseUrl0);
|
||||||
const [key, setKey] = useState("");
|
const [key, setKey] = useState("");
|
||||||
@@ -742,6 +754,20 @@ function CustomEndpointEditor({
|
|||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
|
{/* Disconnect rides beside Connect, exactly the stored-key rows' Delete:
|
||||||
|
quiet text, one click, no confirm — and only once there IS an
|
||||||
|
endpoint (a fresh add has nothing to delete). */}
|
||||||
|
{onDelete && baseUrl0.trim() && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => void onDelete()}
|
||||||
|
disabled={busy}
|
||||||
|
data-testid="custom-delete"
|
||||||
|
className="shrink-0 text-xs text-muted-foreground/70 underline-offset-2 hover:text-red-500 hover:underline"
|
||||||
|
>
|
||||||
|
Disconnect
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{needsKey && (
|
{needsKey && (
|
||||||
<p className="text-[11px] text-muted-foreground">
|
<p className="text-[11px] text-muted-foreground">
|
||||||
|
|||||||
Reference in New Issue
Block a user