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:
parent
820b5c2e20
commit
5533bdb8b6
8 changed files with 44 additions and 24 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue