Stored state lists its keys, each with a two-press delete beside Clear
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WgBfkm4rCmtd1T4vHWjEPx
This commit is contained in:
@@ -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 |
|
||||
|---|---|
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user