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:
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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'),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user