diff --git a/webui/src/__tests__/App.test.tsx b/webui/src/__tests__/App.test.tsx index 4358321..857ff0e 100644 --- a/webui/src/__tests__/App.test.tsx +++ b/webui/src/__tests__/App.test.tsx @@ -160,7 +160,7 @@ describe('App Component - Critical Business Logic Only', () => { expect(screen.getAllByTitle('Start instance').length).toBeGreaterThan(0) expect(screen.getAllByTitle('Stop instance').length).toBeGreaterThan(0) expect(screen.getAllByTitle('Edit instance').length).toBe(2) - expect(screen.getAllByTitle('Delete instance').length).toBeGreaterThan(0) + expect(screen.getAllByTitle('More actions').length).toBe(2) }) it('delete confirmation calls correct API', async () => { @@ -174,8 +174,17 @@ describe('App Component - Critical Business Logic Only', () => { expect(screen.getByText('test-instance-1')).toBeInTheDocument() }) - const deleteButtons = screen.getAllByTitle('Delete instance') - await user.click(deleteButtons[0]) + // First click the "More actions" button to reveal the delete button + const moreActionsButtons = screen.getAllByTitle('More actions') + await user.click(moreActionsButtons[0]) + + // Wait for the delete button to appear and click it + await waitFor(() => { + expect(screen.getByTitle('Delete instance')).toBeInTheDocument() + }) + + const deleteButton = screen.getByTitle('Delete instance') + await user.click(deleteButton) // Verify confirmation and API call expect(confirmSpy).toHaveBeenCalledWith('Are you sure you want to delete instance "test-instance-1"?') diff --git a/webui/src/components/BackendBadge.tsx b/webui/src/components/BackendBadge.tsx index a50cb4d..779fc81 100644 --- a/webui/src/components/BackendBadge.tsx +++ b/webui/src/components/BackendBadge.tsx @@ -1,7 +1,7 @@ import React from "react"; import { Badge } from "@/components/ui/badge"; import { BackendType, type BackendTypeValue } from "@/types/instance"; -import { Cpu, Zap, Server } from "lucide-react"; +import { Server } from "lucide-react"; interface BackendBadgeProps { backend?: BackendTypeValue; @@ -12,19 +12,6 @@ const BackendBadge: React.FC = ({ backend }) => { return null; } - const getIcon = () => { - switch (backend) { - case BackendType.LLAMA_CPP: - return ; - case BackendType.MLX_LM: - return ; - case BackendType.VLLM: - return ; - default: - return ; - } - }; - const getText = () => { switch (backend) { case BackendType.LLAMA_CPP: @@ -38,25 +25,25 @@ const BackendBadge: React.FC = ({ backend }) => { } }; - const getVariant = () => { + const getColorClasses = () => { switch (backend) { case BackendType.LLAMA_CPP: - return "secondary"; + return "bg-blue-100 text-blue-800 border-blue-200 dark:bg-blue-900 dark:text-blue-200 dark:border-blue-800"; case BackendType.MLX_LM: - return "outline"; + return "bg-green-100 text-green-800 border-green-200 dark:bg-green-900 dark:text-green-200 dark:border-green-800"; case BackendType.VLLM: - return "default"; + return "bg-purple-100 text-purple-800 border-purple-200 dark:bg-purple-900 dark:text-purple-200 dark:border-purple-800"; default: - return "secondary"; + return "bg-gray-100 text-gray-800 border-gray-200 dark:bg-gray-900 dark:text-gray-200 dark:border-gray-800"; } }; return ( - {getIcon()} + {getText()} ); diff --git a/webui/src/components/InstanceCard.tsx b/webui/src/components/InstanceCard.tsx index b152262..b3b3339 100644 --- a/webui/src/components/InstanceCard.tsx +++ b/webui/src/components/InstanceCard.tsx @@ -2,7 +2,7 @@ import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import type { Instance } from "@/types/instance"; -import { Edit, FileText, Play, Square, Trash2 } from "lucide-react"; +import { Edit, FileText, Play, Square, Trash2, MoreHorizontal } from "lucide-react"; import LogsDialog from "@/components/LogDialog"; import HealthBadge from "@/components/HealthBadge"; import BackendBadge from "@/components/BackendBadge"; @@ -25,6 +25,7 @@ function InstanceCard({ editInstance, }: InstanceCardProps) { const [isLogsOpen, setIsLogsOpen] = useState(false); + const [showAllActions, setShowAllActions] = useState(false); const health = useInstanceHealth(instance.name, instance.status); const handleStart = () => { @@ -55,39 +56,44 @@ function InstanceCard({ return ( <> - - -
- {instance.name} -
- {running && } + + + {/* Header with instance name and status badges */} +
+ + {instance.name} + + + {/* Badges row */} +
+ {running && }
- -
+ + {/* Primary actions - always visible */} +
- - - -
+ + {/* Secondary actions - collapsible */} + {showAllActions && ( +
+ + + +
+ )}
diff --git a/webui/src/components/__tests__/InstanceCard.test.tsx b/webui/src/components/__tests__/InstanceCard.test.tsx index e0c788a..f45b65b 100644 --- a/webui/src/components/__tests__/InstanceCard.test.tsx +++ b/webui/src/components/__tests__/InstanceCard.test.tsx @@ -102,7 +102,7 @@ afterEach(() => { it('opens logs dialog when logs button clicked', async () => { const user = userEvent.setup() - + render( { /> ) + // First click "More actions" to reveal the logs button + const moreActionsButton = screen.getByTitle('More actions') + await user.click(moreActionsButton) + const logsButton = screen.getByTitle('View logs') await user.click(logsButton) - + // Should open logs dialog (we can verify this by checking if dialog title appears) expect(screen.getByText(`Logs: ${stoppedInstance.name}`)).toBeInTheDocument() }) @@ -125,7 +129,7 @@ afterEach(() => { it('shows confirmation dialog and calls deleteInstance when confirmed', async () => { const user = userEvent.setup() const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true) - + render( { /> ) + // First click "More actions" to reveal the delete button + const moreActionsButton = screen.getByTitle('More actions') + await user.click(moreActionsButton) + const deleteButton = screen.getByTitle('Delete instance') await user.click(deleteButton) - + expect(confirmSpy).toHaveBeenCalledWith('Are you sure you want to delete instance "test-instance"?') expect(mockDeleteInstance).toHaveBeenCalledWith('test-instance') - + confirmSpy.mockRestore() }) it('does not call deleteInstance when confirmation cancelled', async () => { const user = userEvent.setup() const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(false) - + render( { /> ) + // First click "More actions" to reveal the delete button + const moreActionsButton = screen.getByTitle('More actions') + await user.click(moreActionsButton) + const deleteButton = screen.getByTitle('Delete instance') await user.click(deleteButton) - + expect(confirmSpy).toHaveBeenCalled() expect(mockDeleteInstance).not.toHaveBeenCalled() - + confirmSpy.mockRestore() }) }) describe('Button State Based on Instance Status', () => { - it('disables start button and enables stop button for running instance', () => { + it('disables start button and enables stop button for running instance', async () => { + const user = userEvent.setup() + render( { /> ) - expect(screen.getByTitle('Start instance')).toBeDisabled() + expect(screen.queryByTitle('Start instance')).not.toBeInTheDocument() expect(screen.getByTitle('Stop instance')).not.toBeDisabled() + + // Expand more actions to access delete button + const moreActionsButton = screen.getByTitle('More actions') + await user.click(moreActionsButton) + expect(screen.getByTitle('Delete instance')).toBeDisabled() // Can't delete running instance }) - it('enables start button and disables stop button for stopped instance', () => { + it('enables start button and disables stop button for stopped instance', async () => { + const user = userEvent.setup() + render( { ) expect(screen.getByTitle('Start instance')).not.toBeDisabled() - expect(screen.getByTitle('Stop instance')).toBeDisabled() + expect(screen.queryByTitle('Stop instance')).not.toBeInTheDocument() + + // Expand more actions to access delete button + const moreActionsButton = screen.getByTitle('More actions') + await user.click(moreActionsButton) + expect(screen.getByTitle('Delete instance')).not.toBeDisabled() // Can delete stopped instance }) - it('edit and logs buttons are always enabled', () => { + it('edit and logs buttons are always enabled', async () => { + const user = userEvent.setup() + render( { ) expect(screen.getByTitle('Edit instance')).not.toBeDisabled() + + // Expand more actions to access logs button + const moreActionsButton = screen.getByTitle('More actions') + await user.click(moreActionsButton) + expect(screen.getByTitle('View logs')).not.toBeDisabled() }) }) @@ -268,7 +301,7 @@ afterEach(() => { describe('Integration with LogsModal', () => { it('passes correct props to LogsModal', async () => { const user = userEvent.setup() - + render( { /> ) + // First click "More actions" to reveal the logs button + const moreActionsButton = screen.getByTitle('More actions') + await user.click(moreActionsButton) + // Open logs dialog await user.click(screen.getByTitle('View logs')) - + // Verify dialog opened with correct instance data expect(screen.getByText('Logs: running-instance')).toBeInTheDocument() - + // Close dialog to test close functionality const closeButtons = screen.getAllByText('Close') - const dialogCloseButton = closeButtons.find(button => + const dialogCloseButton = closeButtons.find(button => button.closest('[data-slot="dialog-content"]') ) expect(dialogCloseButton).toBeTruthy() await user.click(dialogCloseButton!) - + // Modal should close expect(screen.queryByText('Logs: running-instance')).not.toBeInTheDocument() })