Difficulty: Beginner–Intermediate · You'll need: React, TanStack Query · Size: ~20 lines
What's going on
Deleting a container from the inspect drawer works — and then immediately reports a failure.
Steps to reproduce
- Open a container's inspect drawer
- Click Delete, confirm
- A success toast appears: "Container deleted"
- The drawer stays open and replaces its contents with:
Inspect failed
docker inspect 924f6917da91: Error response from daemon:
No such container: 924f6917da91607ff636bc1de91a0306596bf71825c4705f2f3f2ff07d53bad7
The deletion succeeded. The error is the dashboard inspecting a container it just removed.
Why it matters
The operator sees a success toast and a red failure banner for the same action, seconds apart. The natural reading is that something went half-wrong — so they retry, or go and check by hand. For the one irreversible operation in the product, that is the worst possible moment to be ambiguous about what happened.
It also sends two doomed requests to the agent (retry: 1 is set in apps/web/src/app/providers.tsx), each one a full container.inspect round trip over the WebSocket to run docker inspect on a container that no longer exists.
Root cause
Two defects, both real, either of which alone produces the symptom.
1. The drawer is never closed after a successful delete
apps/web/src/components/ContainerInspectDrawer.tsx:145-156 fires the action and nothing else:
onClick={() => onAction?.(container, 'remove')}
onAction is useContainerCommands.run. The drawer's open/closed state lives in the page:
// apps/web/src/features/containers/ContainersPage.tsx
const [inspecting, setInspecting] = useState<ContainerRow | null>(null)
and setInspecting(null) is only reachable from the drawer's onClose. So after a successful delete the drawer stays mounted with inspecting still pointing at the dead container.
2. The inspect query is never evicted from the cache
apps/web/src/hooks/useContainerAction.ts invalidates only the list:
onSuccess: async (_result, variables) => {
await queryClient.invalidateQueries({
queryKey: containersQueryKey(variables.hostId),
})
}
containerInspectQueryKey(hostId, containerId) is untouched. With the drawer still mounted, useContainerInspect stays enabled, its staleTime: 5_000 lapses, and it refetches — hitting a container that is gone.
The stale entry also outlives the drawer: it sits in the cache keyed by container id until garbage-collected.
The fix
Defect 2 is unambiguous. In useContainerAction, when the action was remove, also drop the inspect entry:
if (variables.action === 'remove') {
queryClient.removeQueries({
queryKey: containerInspectQueryKey(variables.hostId, variables.containerId),
})
}
removeQueries, not invalidateQueries — invalidating would refetch the deleted container, which is the bug.
Defect 1 needs a decision. The drawer must close, but only on a successful remove:
- not on cancel — the user dismissed the confirmation and expects the drawer as they left it
- not on failure — that is exactly when they need the drawer's error visible
- not on start/stop/restart — the point of those is watching the state change in place
onAction currently returns void, so the drawer cannot tell success from cancel from failure. Options:
- A. Have
useContainerCommands.run return the outcome (a boolean, or the ContainerActionResult), widen the onAction prop to Promise<...> | void, and let the drawer await it and close on success. Keeps the decision next to the button.
- B. Give the drawer an
onRemoved callback that the page wires to setInspecting(null). More explicit, but each page must remember to pass it — three pages today.
A is probably right, for the same reason the delete confirmation lives in useContainerCommands rather than in each component: one shared path, no per-surface bookkeeping. But argue for whichever you pick.
Watch out for
- The row menu path. Delete is also in
ContainerTable's dropdown, where no drawer is open. Whatever you change must not break that.
ContainerLogsDrawer. Deleting a container while its logs are streaming has the same shape of problem — worth checking, and a fine follow-up issue if it is broken too.
- Not optimistic. The row must still disappear only after the server confirms. Do not "fix" this by hiding things before the result arrives.
How to verify
npm run docker:infra && npm run dev:server && npm run dev:web
docker run -d --name test-delete nginx
Then check all four paths:
- Open the drawer, delete, confirm → drawer closes, success toast, row gone, no error banner
- Open the drawer, delete, cancel → drawer stays open, unchanged
- Delete from the row menu with no drawer open → still works
- Force a failure (delete a running container without ticking force) → drawer stays open and shows Docker's message
Watch the network tab in case 1: there must be no GET /containers/:id/inspect after the delete returns.
Done when
Related
Difficulty: Beginner–Intermediate · You'll need: React, TanStack Query · Size: ~20 lines
What's going on
Deleting a container from the inspect drawer works — and then immediately reports a failure.
Steps to reproduce
The deletion succeeded. The error is the dashboard inspecting a container it just removed.
Why it matters
The operator sees a success toast and a red failure banner for the same action, seconds apart. The natural reading is that something went half-wrong — so they retry, or go and check by hand. For the one irreversible operation in the product, that is the worst possible moment to be ambiguous about what happened.
It also sends two doomed requests to the agent (
retry: 1is set inapps/web/src/app/providers.tsx), each one a fullcontainer.inspectround trip over the WebSocket to rundocker inspecton a container that no longer exists.Root cause
Two defects, both real, either of which alone produces the symptom.
1. The drawer is never closed after a successful delete
apps/web/src/components/ContainerInspectDrawer.tsx:145-156fires the action and nothing else:onActionisuseContainerCommands.run. The drawer's open/closed state lives in the page:and
setInspecting(null)is only reachable from the drawer'sonClose. So after a successful delete the drawer stays mounted withinspectingstill pointing at the dead container.2. The inspect query is never evicted from the cache
apps/web/src/hooks/useContainerAction.tsinvalidates only the list:containerInspectQueryKey(hostId, containerId)is untouched. With the drawer still mounted,useContainerInspectstaysenabled, itsstaleTime: 5_000lapses, and it refetches — hitting a container that is gone.The stale entry also outlives the drawer: it sits in the cache keyed by container id until garbage-collected.
The fix
Defect 2 is unambiguous. In
useContainerAction, when the action wasremove, also drop the inspect entry:removeQueries, notinvalidateQueries— invalidating would refetch the deleted container, which is the bug.Defect 1 needs a decision. The drawer must close, but only on a successful remove:
onActioncurrently returnsvoid, so the drawer cannot tell success from cancel from failure. Options:useContainerCommands.runreturn the outcome (a boolean, or theContainerActionResult), widen theonActionprop toPromise<...> | void, and let the drawerawaitit and close on success. Keeps the decision next to the button.onRemovedcallback that the page wires tosetInspecting(null). More explicit, but each page must remember to pass it — three pages today.A is probably right, for the same reason the delete confirmation lives in
useContainerCommandsrather than in each component: one shared path, no per-surface bookkeeping. But argue for whichever you pick.Watch out for
ContainerTable's dropdown, where no drawer is open. Whatever you change must not break that.ContainerLogsDrawer. Deleting a container while its logs are streaming has the same shape of problem — worth checking, and a fine follow-up issue if it is broken too.How to verify
Then check all four paths:
Watch the network tab in case 1: there must be no
GET /containers/:id/inspectafter the delete returns.Done when
Related
localhost, even for remote hosts #157 is a separate bug in the same table