mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-03 05:45:04 +03:00
feat(dashboard): inline show/hide toggle for API keys on API Manager page (#4505)
Rebuilt onto release/v3.8.33 (squash-base-stale). Integrated into release/v3.8.33.
This commit is contained in:
committed by
GitHub
parent
bb16f88524
commit
840413faa2
@@ -16,6 +16,8 @@ import {
|
||||
computeApiKeyCounts,
|
||||
formatUsdCost,
|
||||
toLocalDateTimeInputValue,
|
||||
maskKey,
|
||||
toggleKeyVisibility,
|
||||
} from "./apiManagerPageUtils";
|
||||
import type { KeyStatus, KeyType } from "./apiManagerPageUtils";
|
||||
import { readActiveOnlyPreference, writeActiveOnlyPreference } from "./apiManagerPageStorage";
|
||||
@@ -220,6 +222,10 @@ export default function ApiManagerPageClient() {
|
||||
const [usageStats, setUsageStats] = useState<Record<string, KeyUsageStats>>({});
|
||||
const [sessionCounts, setSessionCounts] = useState<Record<string, number>>({});
|
||||
const [allowKeyReveal, setAllowKeyReveal] = useState(false);
|
||||
// Per-row API key visibility toggle (eye / eye-off). Keys default to masked.
|
||||
// Map id -> fully revealed key string fetched on demand from /api/keys/{id}/reveal.
|
||||
const [revealedKeys, setRevealedKeys] = useState<Map<string, string>>(new Map());
|
||||
const [visibleKeys, setVisibleKeys] = useState<Set<string>>(new Set());
|
||||
const createKeyNameFieldRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
@@ -570,6 +576,14 @@ export default function ApiManagerPageClient() {
|
||||
const res = await fetch(`/api/keys/${encodeURIComponent(id)}`, { method: "DELETE" });
|
||||
if (res.ok) {
|
||||
setKeys((prev) => prev.filter((k) => k.id !== id));
|
||||
// Clean up any cached reveal/visibility state for this key.
|
||||
setRevealedKeys((prev) => {
|
||||
if (!prev.has(id)) return prev;
|
||||
const next = new Map(prev);
|
||||
next.delete(id);
|
||||
return next;
|
||||
});
|
||||
setVisibleKeys((prev) => (prev.has(id) ? toggleKeyVisibility(prev, id) : prev));
|
||||
} else {
|
||||
const data = await res.json();
|
||||
setPageError(data.error || t("failedDeleteKey"));
|
||||
@@ -624,6 +638,12 @@ export default function ApiManagerPageClient() {
|
||||
|
||||
const data = await res.json();
|
||||
if (typeof data?.key === "string") {
|
||||
// Cache the revealed value so a subsequent show-toggle does not refetch.
|
||||
setRevealedKeys((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.set(keyId, data.key);
|
||||
return next;
|
||||
});
|
||||
await copy(data.key, `existing_key_${keyId}`);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -631,6 +651,39 @@ export default function ApiManagerPageClient() {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle the visibility of one key inline (eye / eye-off button).
|
||||
* Lazy-fetches the full key from /api/keys/{id}/reveal on the FIRST show,
|
||||
* then caches it in `revealedKeys` so re-toggling is instant. Hiding only
|
||||
* flips the visibility set — the cached reveal stays so a re-show is free.
|
||||
*/
|
||||
const handleToggleKeyVisibility = async (keyId: string) => {
|
||||
if (!keyId) return;
|
||||
const isCurrentlyVisible = visibleKeys.has(keyId);
|
||||
|
||||
if (!isCurrentlyVisible && !revealedKeys.has(keyId)) {
|
||||
try {
|
||||
const res = await fetch(`/api/keys/${encodeURIComponent(keyId)}/reveal`);
|
||||
if (!res.ok) {
|
||||
console.log("Error revealing key:", await res.text());
|
||||
return;
|
||||
}
|
||||
const data = await res.json();
|
||||
if (typeof data?.key !== "string") return;
|
||||
setRevealedKeys((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.set(keyId, data.key);
|
||||
return next;
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("Error revealing key:", error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setVisibleKeys((prev) => toggleKeyVisibility(prev, keyId));
|
||||
};
|
||||
|
||||
const handleUpdatePermissions = async (
|
||||
name: string,
|
||||
allowedModels: string[],
|
||||
@@ -931,18 +984,35 @@ export default function ApiManagerPageClient() {
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-span-3 flex items-center gap-1.5">
|
||||
<code className="text-sm text-text-muted font-mono truncate">{key.key}</code>
|
||||
<code className="text-sm text-text-muted font-mono truncate">
|
||||
{visibleKeys.has(key.id)
|
||||
? (revealedKeys.get(key.id) ?? key.key)
|
||||
: maskKey(key.key)}
|
||||
</code>
|
||||
{allowKeyReveal ? (
|
||||
<button
|
||||
onClick={() => handleCopyExistingKey(key.id)}
|
||||
className="p-1 text-text-muted/60 hover:text-primary transition-colors shrink-0"
|
||||
title={tc("copy")}
|
||||
aria-label={tc("copy")}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]">
|
||||
{copied === `existing_key_${key.id}` ? "check" : "content_copy"}
|
||||
</span>
|
||||
</button>
|
||||
<>
|
||||
<button
|
||||
onClick={() => handleToggleKeyVisibility(key.id)}
|
||||
className="p-1 text-text-muted/60 hover:text-primary transition-colors shrink-0"
|
||||
title={visibleKeys.has(key.id) ? t("hideKey") : t("showKey")}
|
||||
aria-label={visibleKeys.has(key.id) ? t("hideKey") : t("showKey")}
|
||||
aria-pressed={visibleKeys.has(key.id)}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]">
|
||||
{visibleKeys.has(key.id) ? "visibility_off" : "visibility"}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleCopyExistingKey(key.id)}
|
||||
className="p-1 text-text-muted/60 hover:text-primary transition-colors shrink-0"
|
||||
title={tc("copy")}
|
||||
aria-label={tc("copy")}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]">
|
||||
{copied === `existing_key_${key.id}` ? "check" : "content_copy"}
|
||||
</span>
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<span
|
||||
className="p-1 text-text-muted/40 opacity-0 group-hover:opacity-100 transition-all shrink-0 cursor-help"
|
||||
|
||||
@@ -103,3 +103,24 @@ export function formatUsdCost(value: number, locale: string): string {
|
||||
maximumFractionDigits: amount > 0 && amount < 1 ? 4 : 2,
|
||||
}).format(amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mask a fully revealed API key for the at-rest display: keep the first 8 chars
|
||||
* (provider prefix + a few entropy bits, e.g. `sk-or-12...`), append an ellipsis.
|
||||
* Returns "" for empty/missing input so the UI can render an empty `<code>` cleanly.
|
||||
*/
|
||||
export function maskKey(fullKey: string | null | undefined): string {
|
||||
if (!fullKey) return "";
|
||||
return fullKey.length > 8 ? `${fullKey.slice(0, 8)}...` : fullKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Immutable Set toggle helper for the "which keys are currently revealed" state.
|
||||
* Returns a NEW Set so React state setters always see a fresh reference.
|
||||
*/
|
||||
export function toggleKeyVisibility(prev: Set<string>, keyId: string): Set<string> {
|
||||
const next = new Set(prev);
|
||||
if (next.has(keyId)) next.delete(keyId);
|
||||
else next.add(keyId);
|
||||
return next;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user