diff --git a/docs/reference/connector-contract.md b/docs/reference/connector-contract.md index 95aff46..3e75e41 100644 --- a/docs/reference/connector-contract.md +++ b/docs/reference/connector-contract.md @@ -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 | |---|---| diff --git a/frontend/src/components/Flow/NodePanel.tsx b/frontend/src/components/Flow/NodePanel.tsx index 237cf30..b5cdffc 100644 --- a/frontend/src/components/Flow/NodePanel.tsx +++ b/frontend/src/components/Flow/NodePanel.tsx @@ -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(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 }) { )} + {data.data.length > 0 ? ( + + ) : null} ) }