diff --git a/e2e/nodes.spec.ts b/e2e/nodes.spec.ts index 80a1cfe6..4839b617 100644 --- a/e2e/nodes.spec.ts +++ b/e2e/nodes.spec.ts @@ -46,8 +46,8 @@ test.describe('Node management', () => { await page.locator('#node-api-url').fill('http://localhost:6379'); // api_token is required to enable the submit button; use a dummy value since we're testing URL validation await page.locator('#node-api-token').fill('dummy-token'); - // Use .last() to target the dialog submit button, not the trigger - await page.getByRole('button', { name: /add node/i }).last().click(); + // Scope to the dialog so we target the submit button, not the trigger + await page.getByRole('dialog').getByRole('button', { name: /add node/i }).click(); await expect(page.getByText(/loopback|localhost/i)).toBeVisible({ timeout: 5_000 }); }); @@ -59,7 +59,7 @@ test.describe('Node management', () => { await page.locator('#node-api-url').fill('not-a-url-at-all'); // api_token is required to enable the submit button; use a dummy value since we're testing URL validation await page.locator('#node-api-token').fill('dummy-token'); - await page.getByRole('button', { name: /add node/i }).last().click(); + await page.getByRole('dialog').getByRole('button', { name: /add node/i }).click(); await expect(page.getByText(/valid url|invalid url/i)).toBeVisible({ timeout: 5_000 }); }); diff --git a/frontend/src/components/NodeManager.tsx b/frontend/src/components/NodeManager.tsx index e3fd9577..d2344501 100644 --- a/frontend/src/components/NodeManager.tsx +++ b/frontend/src/components/NodeManager.tsx @@ -4,8 +4,7 @@ import type { Node, NodeMode } from '@/context/NodeContext'; import { apiFetch } from '@/lib/api'; import { copyToClipboard } from '@/lib/clipboard'; import { toast } from '@/components/ui/toast-store'; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogTrigger } from './ui/dialog'; -import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from './ui/alert-dialog'; +import { Modal, ModalHeader, ModalBody, ModalFooter, ConfirmModal } from './ui/modal'; import { Button } from './ui/button'; import { Input } from './ui/input'; import { Label } from './ui/label'; @@ -232,11 +231,12 @@ export function NodeManager() { throw new Error(err.error || 'Failed to delete node'); } toast.success(`Node "${deletingNode.name}" deleted`); - setDeleteOpen(false); - setDeletingNode(null); await refreshNodes(); } catch (error) { toast.error((error as Error).message || 'Failed to delete node'); + } finally { + setDeleteOpen(false); + setDeletingNode(null); } }; @@ -422,28 +422,40 @@ export function NodeManager() {
{/* Actions */}
- { + setFormData(defaultFormData); + setCreateOpen(true); + }} + > + + Add node + + { setCreateOpen(open); - // Reset form to defaults every time the dialog opens if (open) setFormData(defaultFormData); }} + size="lg" > - - - - Add node - - - - - Add {formData.type === 'local' ? 'Local' : 'Remote'} Node - + + {renderFormFields()} - - + + setCreateOpen(false)}>Cancel + } + primary={ Add node - - - + } + /> +
@@ -723,12 +735,10 @@ export function NodeManager() {
)} - {/* Edit Dialog */} - - - - Edit Node - + {/* Edit Modal */} + + + {renderFormFields()} {formData.type === 'remote' && formData.mode === 'pilot_agent' && editingNodeId !== null && (
@@ -749,23 +759,28 @@ export function NodeManager() {
)} - - +
+ { setEditOpen(false); setEditingNodeId(null); }}>Cancel + } + primary={ - -
-
+ } + /> + - {/* Pilot enrollment dialog (create + regenerate flows both open this) */} - { if (!open) { @@ -775,13 +790,16 @@ export function NodeManager() { setFormData(defaultFormData); } }} + size="xl" > - - - Enroll the pilot agent - + + {activeEnrollment && ( -
+ <>

Run this command on {activeEnrollment.nodeName} to start the pilot agent. The token below is valid for 15 minutes and can only be used once.

@@ -797,10 +815,13 @@ export function NodeManager() { {enrollmentCopied ? 'Copied' : 'Copy command'}
- + )} - +
+ { setActiveEnrollment(null); setEnrollmentCopied(false); @@ -811,25 +832,24 @@ export function NodeManager() { > Done - -
-
+ } + /> + {/* Delete Confirmation */} - - - - Delete Node - - Are you sure you want to remove {deletingNode?.name}? This will only remove the node from Sencho - it will not affect the remote instance or any running containers. - - - - Cancel - Delete - - - + +

+ Removes {deletingNode?.name} from this console. The remote instance and its containers are not affected. +

+
); } diff --git a/frontend/src/components/ui/modal.tsx b/frontend/src/components/ui/modal.tsx index 8928184d..dee9ab42 100644 --- a/frontend/src/components/ui/modal.tsx +++ b/frontend/src/components/ui/modal.tsx @@ -141,7 +141,7 @@ function ConfirmDestructiveHeader(props: ModalHeaderBaseProps) { } export function ModalBody({ className, ...props }: React.HTMLAttributes) { - return
; + return
; } interface ModalFooterProps {