Studio: Change API Keys settings to API Access (#5268)

* chore: change API Keys settings to API Access

* chore: replace key with plug svg

* chore: replace API access with developer and related SVG

* chore: rename API keys menu to developer

* fix: focus active settings tab on open

* fix: prevent settings autofocus on open

* Revert "fix: prevent settings autofocus on open"

This reverts commit 4b64d73eda.
This commit is contained in:
Lee Jackson 2026-05-04 14:56:38 +01:00 committed by GitHub
commit 5533bdb8b6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 44 additions and 24 deletions

View file

@ -37,12 +37,12 @@ import {
Delete02Icon,
Download03Icon,
GemIcon,
Key01Icon,
Search01Icon,
PowerIcon,
PencilEdit02Icon,
LayoutAlignLeftIcon,
Settings02Icon,
SourceCodeSquareIcon,
ZapIcon,
} from "@hugeicons/core-free-icons";
import {
@ -548,8 +548,8 @@ export function AppSidebar() {
<DropdownMenuItem
onSelect={() => useSettingsDialogStore.getState().openDialog("api-keys")}
>
<HugeiconsIcon icon={Key01Icon} strokeWidth={1.75} className="size-[18px]" />
<span>API Keys</span>
<HugeiconsIcon icon={SourceCodeSquareIcon} strokeWidth={1.75} className="size-[18px]" />
<span>Developer</span>
<span className="ml-auto rounded-[6px] border border-emerald-500/25 bg-emerald-500/10 px-1.5 py-0.5 text-[10px] leading-none font-semibold text-emerald-700 dark:text-emerald-300">
New
</span>

View file

@ -15,7 +15,7 @@ export interface ApiKey {
export async function fetchApiKeys(): Promise<ApiKey[]> {
const res = await authFetch("/api/auth/api-keys");
if (!res.ok) throw new Error("Failed to load API keys");
if (!res.ok) throw new Error("Failed to load API access");
const data = (await res.json()) as { api_keys: ApiKey[] };
return data.api_keys.filter((k) => k.is_active);
}
@ -29,7 +29,7 @@ export async function createApiKey(
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name, expires_in_days: expiresInDays }),
});
if (!res.ok) throw new Error("Failed to create API key");
if (!res.ok) throw new Error("Failed to create access token");
return res.json();
}
@ -37,5 +37,5 @@ export async function revokeApiKey(keyId: number): Promise<void> {
const res = await authFetch(`/api/auth/api-keys/${keyId}`, {
method: "DELETE",
});
if (!res.ok) throw new Error("Failed to revoke API key");
if (!res.ok) throw new Error("Failed to revoke access token");
}

View file

@ -92,7 +92,7 @@ export function ApiKeyRow({
className="text-destructive focus:text-destructive"
>
<HugeiconsIcon icon={Delete02Icon} className="size-3.5 mr-2" />
Revoke key
Revoke token
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

View file

@ -34,7 +34,7 @@ export function CreateKeyForm({
onCreated(result.key);
setName("");
} catch (err) {
onError(err instanceof Error ? err.message : "Couldn't create key.");
onError(err instanceof Error ? err.message : "Couldn't create access token.");
} finally {
setLoading(false);
}
@ -49,9 +49,9 @@ export function CreateKeyForm({
<Input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Key name (e.g. production)"
placeholder="Token name (e.g. production)"
className="h-8 min-w-[180px] flex-1 text-sm"
aria-label="New key name"
aria-label="New access token name"
/>
<div className="inline-flex items-center rounded-md border border-border bg-background p-0.5">
{EXPIRY_PRESETS.map((p) => {
@ -75,7 +75,7 @@ export function CreateKeyForm({
})}
</div>
<Button type="submit" size="sm" disabled={loading || !name.trim()}>
{loading ? "Creating…" : "Create key"}
{loading ? "Creating…" : "Create token"}
</Button>
</div>
</form>

View file

@ -32,7 +32,7 @@ export function KeyRevealCard({
className="size-3.5 text-emerald-600 dark:text-emerald-500"
/>
<span className="text-xs font-medium text-emerald-700 dark:text-emerald-500">
New key created
New access token created
</span>
</div>
<button
@ -43,7 +43,7 @@ export function KeyRevealCard({
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background",
copied && "border-emerald-500/40 bg-emerald-500/10",
)}
aria-label={copied ? "Key copied" : "Copy key"}
aria-label={copied ? "Access token copied" : "Copy access token"}
>
<code className="min-w-0 flex-1 break-all text-left text-foreground">
{rawKey}

View file

@ -10,15 +10,16 @@ import {
import { cn } from "@/lib/utils";
import {
Cancel01Icon,
Key01Icon,
Message01Icon,
PaintBrush02Icon,
Settings02Icon,
SourceCodeSquareIcon,
SparklesIcon,
UserIcon,
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { motion, useReducedMotion } from "motion/react";
import { useEffect, useRef } from "react";
import { useSettingsDialogStore, type SettingsTab } from "./stores/settings-dialog-store";
import { AboutTab } from "./tabs/about-tab";
import { ApiKeysTab } from "./tabs/api-keys-tab";
@ -39,7 +40,7 @@ const TABS: TabDef[] = [
{ id: "profile", label: "Profile", icon: UserIcon },
{ id: "appearance", label: "Appearance", icon: PaintBrush02Icon },
{ id: "chat", label: "Chat", icon: Message01Icon },
{ id: "api-keys", label: "API Keys", icon: Key01Icon, badge: "New" },
{ id: "api-keys", label: "Developer", icon: SourceCodeSquareIcon, badge: "New" },
{ id: "about", label: "Help", icon: SparklesIcon },
];
@ -66,6 +67,22 @@ export function SettingsDialog() {
const setActiveTab = useSettingsDialogStore((s) => s.setActiveTab);
const closeDialog = useSettingsDialogStore((s) => s.closeDialog);
const reduced = useReducedMotion();
const tabButtonRefs = useRef<Record<SettingsTab, HTMLButtonElement | null>>({
general: null,
profile: null,
appearance: null,
chat: null,
"api-keys": null,
about: null,
});
useEffect(() => {
if (!open) return;
const frame = window.requestAnimationFrame(() => {
tabButtonRefs.current[activeTab]?.focus({ preventScroll: true });
});
return () => window.cancelAnimationFrame(frame);
}, [open, activeTab]);
return (
<Dialog open={open} onOpenChange={(o) => !o && closeDialog()}>
@ -91,6 +108,9 @@ export function SettingsDialog() {
return (
<button
key={tab.id}
ref={(node) => {
tabButtonRefs.current[tab.id] = node;
}}
type="button"
onClick={() => setActiveTab(tab.id)}
className={cn(

View file

@ -36,7 +36,7 @@ export function ApiKeysTab() {
try {
setKeys(await fetchApiKeys());
} catch (e) {
setError(e instanceof Error ? e.message : "Couldn't load API keys.");
setError(e instanceof Error ? e.message : "Couldn't load API access.");
} finally {
setLoading(false);
}
@ -54,7 +54,7 @@ export function ApiKeysTab() {
await load();
setRevokeTarget(null);
} catch (e) {
setError(e instanceof Error ? e.message : "Couldn't revoke key.");
setError(e instanceof Error ? e.message : "Couldn't revoke access token.");
} finally {
setRevoking(false);
}
@ -63,7 +63,7 @@ export function ApiKeysTab() {
return (
<div className="flex flex-col gap-6">
<header className="flex flex-col gap-1">
<h1 className="text-lg font-semibold font-heading">API Keys</h1>
<h1 className="text-lg font-semibold font-heading">Developer</h1>
<p className="text-xs text-muted-foreground">
Access Unsloth programmatically via the OpenAI-compatible API.{" "}
<a
@ -112,7 +112,7 @@ export function ApiKeysTab() {
</AnimatePresence>
<section className="flex flex-col">
<h2 className="mb-2 text-sm font-semibold text-foreground">Your keys</h2>
<h2 className="mb-2 text-sm font-semibold text-foreground">Access tokens</h2>
{error ? (
<div className="rounded-md border border-destructive/20 bg-destructive/5 p-3 text-xs text-destructive">
{error}
@ -128,7 +128,7 @@ export function ApiKeysTab() {
</div>
) : keys.length === 0 ? (
<p className="py-6 text-center text-xs text-muted-foreground">
No API keys yet.
No API access yet.
</p>
) : (
<div className="flex flex-col">
@ -144,9 +144,9 @@ export function ApiKeysTab() {
<Dialog open={revokeTarget !== null} onOpenChange={(o) => !o && setRevokeTarget(null)}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>Revoke key {revokeTarget?.name}?</DialogTitle>
<DialogTitle>Revoke access token {revokeTarget?.name}?</DialogTitle>
<DialogDescription>
Applications using this key will immediately lose access. This cannot be undone.
Applications using this token will immediately lose access. This cannot be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter>

View file

@ -199,7 +199,7 @@ export function GeneralTab() {
<SettingsRow
destructive
label="Reset all local preferences"
description="Clears theme, tokens, sidebar state, and presets. Chats and API keys are not affected."
description="Clears theme, tokens, sidebar state, and presets. Chats and API access are not affected."
>
<Button
variant="outline"
@ -218,7 +218,7 @@ export function GeneralTab() {
<DialogTitle>Reset all local preferences?</DialogTitle>
<DialogDescription>
This clears your theme, tokens, and stored settings, then reloads
Studio. Chats and API keys are not affected.
Studio. Chats and API access are not affected.
</DialogDescription>
</DialogHeader>
<DialogFooter>