'use client' import { useEffect, useState } from 'react' import { Database, Search, ChevronRight, Key, AlertCircle, RefreshCw, Edit3, Check } from 'lucide-react' interface NS { id: string; title: string } interface KVKey { name: string; expiration?: number; metadata?: any } export default function KVPage() { const [namespaces, setNamespaces] = useState([]) const [selectedNs, setSelectedNs] = useState(null) const [keys, setKeys] = useState([]) const [selectedKey, setSelectedKey] = useState(null) const [value, setValue] = useState('') const [editValue, setEditValue] = useState('') const [editing, setEditing] = useState(false) const [search, setSearch] = useState('') const [loading, setLoading] = useState(true) const [loadingKeys, setLoadingKeys] = useState(false) const [loadingValue, setLoadingValue] = useState(false) const [saving, setSaving] = useState(false) const [live, setLive] = useState(false) const [saved, setSaved] = useState(false) useEffect(() => { fetch('/api/kv').then(r => r.json()).then(d => { setNamespaces(d.namespaces || []) setLive(d.live ?? false) setLoading(false) }).catch(() => setLoading(false)) }, []) const selectNs = async (ns: NS) => { setSelectedNs(ns); setSelectedKey(null); setValue('') setLoadingKeys(true) try { const r = await fetch(`/api/kv?ns=${ns.id}`) const d = await r.json() setKeys(d.keys || []) } finally { setLoadingKeys(false) } } const selectKey = async (key: string) => { if (!selectedNs) return setSelectedKey(key); setLoadingValue(true) try { const r = await fetch(`/api/kv?ns=${selectedNs.id}&key=${encodeURIComponent(key)}`) const d = await r.json() setValue(d.value || '') setEditValue(d.value || '') } finally { setLoadingValue(false) } } const saveValue = async () => { if (!selectedNs || !selectedKey) return setSaving(true) try { await fetch('/api/kv', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ nsId: selectedNs.id, key: selectedKey, value: editValue }), }) setValue(editValue) setSaved(true) setTimeout(() => setSaved(false), 2000) setEditing(false) } finally { setSaving(false) } } const filteredNs = namespaces.filter(ns => !search || ns.title.toLowerCase().includes(search.toLowerCase())) const filteredKeys = keys.filter(k => !search || k.name.toLowerCase().includes(search.toLowerCase())) return (

KV Browser

{namespaces.length} namespaces · Cloudflare Workers KV {live ? '● live' : '○ set CF token'}

{!live && (
Set CLOUDFLARE_API_TOKEN in Vercel
)}
{/* Namespace list */}
setSearch(e.target.value)} placeholder="Search…" style={{ width: '100%', background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 6, padding: '7px 10px 7px 28px', color: '#fff', fontSize: 12, outline: 'none', boxSizing: 'border-box' }} />
{loading ?
Loading…
: filteredNs.map(ns => (
selectNs(ns)} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '10px 14px', cursor: 'pointer', background: selectedNs?.id === ns.id ? 'rgba(245,166,35,0.12)' : 'transparent', borderLeft: `3px solid ${selectedNs?.id === ns.id ? '#F5A623' : 'transparent'}`, transition: 'all .1s', }}> {ns.title}
))} {!loading && filteredNs.length === 0 &&
No namespaces
}
{/* Right panel */}
{!selectedNs ? (
← Select a namespace to browse keys
) : ( <> {/* Keys */}
{selectedNs.title} ({keys.length} keys) {loadingKeys && }
{filteredKeys.map(k => (
selectKey(k.name)} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '8px 14px', cursor: 'pointer', background: selectedKey === k.name ? 'rgba(245,166,35,0.08)' : 'transparent', borderLeft: `2px solid ${selectedKey === k.name ? '#F5A623' : 'transparent'}`, }}> {k.name}
))} {filteredKeys.length === 0 && !loadingKeys &&
No keys
}
{/* Value viewer */} {selectedKey && (
{selectedKey}
{!editing ? ( ) : ( <> )}
{loadingValue ? (
Loading value…
) : (