feat(pending-banner): add Discard button to cancel pending restart without applying

- DELETE /api/config/pending endpoint calls _clear_pending_restart()
- cellAPI.cancelPending() calls the new endpoint
- PendingRestartBanner shows a "Discard" button alongside "Apply Now";
  clicking it drops the pending state without restarting any containers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-22 12:07:39 -04:00
parent b5462f84e0
commit 16609da529
3 changed files with 42 additions and 11 deletions
+7
View File
@@ -629,6 +629,13 @@ def get_pending_config():
}) })
@app.route('/api/config/pending', methods=['DELETE'])
def cancel_pending_config():
"""Discard pending configuration changes without restarting any containers."""
_clear_pending_restart()
return jsonify({'message': 'Pending changes discarded'})
@app.route('/api/config/apply', methods=['POST']) @app.route('/api/config/apply', methods=['POST'])
def apply_pending_config(): def apply_pending_config():
"""Apply pending configuration by restarting containers via docker compose up -d.""" """Apply pending configuration by restarting containers via docker compose up -d."""
+34 -11
View File
@@ -35,9 +35,10 @@ import Vault from './pages/Vault';
import ContainerDashboard from './components/ContainerDashboard'; import ContainerDashboard from './components/ContainerDashboard';
import CellNetwork from './pages/CellNetwork'; import CellNetwork from './pages/CellNetwork';
function PendingRestartBanner({ pending, onApply }) { function PendingRestartBanner({ pending, onApply, onCancel }) {
const [confirming, setConfirming] = useState(false); const [confirming, setConfirming] = useState(false);
const [applying, setApplying] = useState(false); const [applying, setApplying] = useState(false);
const [cancelling, setCancelling] = useState(false);
const handleApply = async () => { const handleApply = async () => {
setApplying(true); setApplying(true);
@@ -49,6 +50,15 @@ function PendingRestartBanner({ pending, onApply }) {
} }
}; };
const handleCancel = async () => {
setCancelling(true);
try {
await onCancel();
} finally {
setCancelling(false);
}
};
return ( return (
<> <>
<div className="mb-6 bg-warning-50 border border-warning-300 rounded-lg p-4"> <div className="mb-6 bg-warning-50 border border-warning-300 rounded-lg p-4">
@@ -66,14 +76,23 @@ function PendingRestartBanner({ pending, onApply }) {
)} )}
</div> </div>
</div> </div>
<button <div className="ml-4 flex-shrink-0 flex items-center gap-2">
onClick={() => setConfirming(true)} <button
disabled={applying} onClick={handleCancel}
className="ml-4 flex-shrink-0 flex items-center gap-1.5 px-3 py-1.5 bg-warning-600 hover:bg-warning-700 disabled:opacity-50 text-white text-sm font-medium rounded-md transition-colors" disabled={applying || cancelling}
> className="flex items-center gap-1.5 px-3 py-1.5 bg-white hover:bg-gray-50 disabled:opacity-50 text-warning-700 text-sm font-medium rounded-md border border-warning-300 transition-colors"
<RefreshCw className={`h-4 w-4 ${applying ? 'animate-spin' : ''}`} /> >
{applying ? 'Restarting…' : 'Apply Now'} {cancelling ? 'Discarding…' : 'Discard'}
</button> </button>
<button
onClick={() => setConfirming(true)}
disabled={applying || cancelling}
className="flex items-center gap-1.5 px-3 py-1.5 bg-warning-600 hover:bg-warning-700 disabled:opacity-50 text-white text-sm font-medium rounded-md transition-colors"
>
<RefreshCw className={`h-4 w-4 ${applying ? 'animate-spin' : ''}`} />
{applying ? 'Restarting…' : 'Apply Now'}
</button>
</div>
</div> </div>
</div> </div>
@@ -147,7 +166,11 @@ function App() {
const handleApply = useCallback(async () => { const handleApply = useCallback(async () => {
await cellAPI.applyPending(); await cellAPI.applyPending();
// Optimistically clear the banner; containers are restarting setPending({ needs_restart: false, changes: [] });
}, []);
const handleCancel = useCallback(async () => {
await cellAPI.cancelPending();
setPending({ needs_restart: false, changes: [] }); setPending({ needs_restart: false, changes: [] });
}, []); }, []);
@@ -209,7 +232,7 @@ function App() {
)} )}
{isOnline && pending.needs_restart && ( {isOnline && pending.needs_restart && (
<PendingRestartBanner pending={pending} onApply={handleApply} /> <PendingRestartBanner pending={pending} onApply={handleApply} onCancel={handleCancel} />
)} )}
<Routes> <Routes>
+1
View File
@@ -44,6 +44,7 @@ export const cellAPI = {
exportConfig: (format = 'json') => api.get('/api/config/export', { params: { format } }), exportConfig: (format = 'json') => api.get('/api/config/export', { params: { format } }),
importConfig: (config, format = 'json') => api.post('/api/config/import', { config, format }), importConfig: (config, format = 'json') => api.post('/api/config/import', { config, format }),
getPending: () => api.get('/api/config/pending'), getPending: () => api.get('/api/config/pending'),
cancelPending: () => api.delete('/api/config/pending'),
applyPending: () => api.post('/api/config/apply'), applyPending: () => api.post('/api/config/apply'),
}; };