Stored state lists its keys, each with a two-press delete beside Clear

This commit is contained in:
2026-10-15 00:04:03 +02:00
parent ddc281c81c
commit c9e38ede58
2 changed files with 55 additions and 3 deletions
+2 -2
View File
@@ -164,8 +164,8 @@ await asyncio.to_thread(self.store.delete, "token:device")
64 KiB and each package allows 256 keys; exceeding a limit raises `ValueError`.
- Calls block on the database: use `asyncio.to_thread` on the event loop.
Superusers can list keys and clear them under **Stored state** in the node
panel. Values are never returned. The equivalent API paths are:
Superusers can list keys, delete one, or clear them all under **Stored state**
in the node panel. Values are never returned. The equivalent API paths are:
| Method | Path |
|---|---|
+53 -1
View File
@@ -4,6 +4,7 @@ import {
CornerDownRight,
Maximize2,
Minimize2,
Trash2,
X,
} from "lucide-react"
import {
@@ -1236,12 +1237,15 @@ const SCAFFOLD_SHAPE = (() => {
/**
* What a connector keeps across restarts, such as a certificate it pinned on
* first contact. Clearing it is how a replaced device gets trusted again.
* first contact. Deleting a key is how one replaced device gets trusted again;
* clearing them all starts every device over.
*/
function StoredStateSection({ nodeType }: { nodeType: string }) {
const queryClient = useQueryClient()
const { showSuccessToast, showErrorToast } = useCustomToast()
const [confirming, setConfirming] = useState(false)
// The key whose delete was pressed once; a second press deletes it.
const [armed, setArmed] = useState<string | null>(null)
const queryKey = ["connector-state", nodeType]
const { data } = useQuery({
queryKey,
@@ -1256,6 +1260,15 @@ function StoredStateSection({ nodeType }: { nodeType: string }) {
},
onError: () => showErrorToast("The stored state could not be cleared."),
})
const forget = useMutation({
mutationFn: (key: string) =>
ConnectorsService.deleteConnectorState({ nodeType, key }),
onSuccess: (_, key) => {
showSuccessToast(`Deleted '${key}'`)
queryClient.invalidateQueries({ queryKey })
},
onError: () => showErrorToast("The key could not be deleted."),
})
if (!data) return null
return (
@@ -1308,6 +1321,45 @@ function StoredStateSection({ nodeType }: { nodeType: string }) {
</Button>
)}
</div>
{data.data.length > 0 ? (
<ul className="grid gap-1">
{data.data.map(({ key }) => {
// The icon says nothing of its own, so the label carries the
// confirm step.
const label =
armed === key ? `Confirm: delete ${key}` : `Delete ${key}`
return (
<li key={key} className="flex min-w-0 items-center gap-2">
<span className="min-w-0 flex-1 truncate font-mono text-sm">
{key}
</span>
<Button
variant={armed === key ? "destructive" : "ghost"}
size="icon-touch"
className={cn(
"shrink-0",
armed !== key && "text-muted-foreground",
)}
aria-label={label}
title={label}
disabled={forget.isPending}
onBlur={() => setArmed(null)}
onClick={() => {
if (armed !== key) {
setArmed(key)
return
}
setArmed(null)
forget.mutate(key)
}}
>
<Trash2 />
</Button>
</li>
)
})}
</ul>
) : null}
</div>
)
}