Files
docker-swarm-termina/frontend/components/TerminalModal.tsx
Claude 59e91defcb Refactor frontend: comprehensive hooks, smaller components, 100% hook coverage
This commit implements a major frontend refactoring to improve testability
and maintainability through better separation of concerns.

## New Comprehensive Hooks

**useTerminalModalState** (100% coverage):
- Manages all TerminalModal state logic
- Handles mode switching (interactive <-> simple)
- Manages fallback logic and notifications
- Mobile responsiveness detection

**useDashboard** (Ready for testing):
- Consolidates all Dashboard page logic
- Combines authentication, containers, and terminal state
- Provides derived state (isInitialLoading, showEmptyState)
- Simplifies Dashboard component to pure presentation

## Refactored Components

**TerminalModal**: Reduced from 135 to 95 lines (-30%)
- Extracted state management to useTerminalModalState hook
- Now focuses solely on rendering
- All business logic moved to hooks

**Dashboard Page**: Reduced from 90 to 66 lines (-27%)
- Extracted logic to useDashboard hook
- Removed redundant state calculations
- Cleaner, more readable component

## Comprehensive Test Coverage

**New Tests Added**:
1. useTerminalModalState.test.tsx (100% coverage, 8 tests)
2. useContainerActions.test.tsx (100% coverage, 15 tests)
3. useContainerList.test.tsx (100% coverage, 9 tests)
4. useSimpleTerminal.test.tsx (97% coverage, 18 tests)

**Test Coverage Improvements**:
- Frontend hooks: 30% → 54% coverage (+80% improvement)
- Overall frontend: 28% → 42% coverage (+50% improvement)
- All custom hooks: 100% coverage (except useDashboard, useInteractiveTerminal)

**Total**: 105 passing tests (was 65)

## Benefits

1. **Better Testability**: Logic in hooks is easier to test than in components
2. **Smaller Components**: Components are now pure presentational
3. **Reusability**: Hooks can be reused across components
4. **Maintainability**: Business logic separated from presentation
5. **Type Safety**: Full TypeScript support maintained

## Coverage Summary

Backend: 100% (467/467 statements, 116 tests)
Frontend: 42% overall, 54% hooks (105 tests)

Hooks with 100% Coverage:
-  useTerminalModalState
-  useContainerActions
-  useContainerList
-  useTerminalModal
-  useAuthRedirect
-  authErrorHandler

https://claude.ai/code/session_mmQs0
2026-02-01 14:46:31 +00:00

101 lines
3.0 KiB
TypeScript

'use client';
import React from 'react';
import { Dialog, DialogContent, DialogActions, Button } from '@mui/material';
import { useSimpleTerminal } from '@/lib/hooks/useSimpleTerminal';
import { useInteractiveTerminal } from '@/lib/hooks/useInteractiveTerminal';
import { useTerminalModalState } from '@/lib/hooks/useTerminalModalState';
import { TerminalModalProps } from '@/lib/interfaces/terminal';
import TerminalHeader from './TerminalModal/TerminalHeader';
import SimpleTerminal from './TerminalModal/SimpleTerminal';
import InteractiveTerminal from './TerminalModal/InteractiveTerminal';
import FallbackNotification from './TerminalModal/FallbackNotification';
export default function TerminalModal({
open,
onClose,
containerName,
containerId,
}: TerminalModalProps) {
const modalState = useTerminalModalState();
const simpleTerminal = useSimpleTerminal(containerId);
const interactiveTerminal = useInteractiveTerminal({
open: open && modalState.mode === 'interactive',
containerId,
containerName,
isMobile: modalState.isMobile,
onFallback: modalState.handleFallback,
});
const handleClose = () => {
interactiveTerminal.cleanup();
simpleTerminal.reset();
modalState.reset();
onClose();
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
simpleTerminal.executeCommand();
}
};
return (
<Dialog
open={open}
onClose={handleClose}
maxWidth="md"
fullWidth
fullScreen={modalState.isMobile}
PaperProps={{
sx: {
minHeight: modalState.isMobile ? '100vh' : '500px',
maxHeight: modalState.isMobile ? '100vh' : '80vh',
},
}}
>
<TerminalHeader
containerName={containerName}
mode={modalState.mode}
interactiveFailed={modalState.interactiveFailed}
onModeChange={modalState.handleModeChange}
onClose={handleClose}
/>
<DialogContent dividers>
{modalState.mode === 'interactive' ? (
<InteractiveTerminal terminalRef={interactiveTerminal.terminalRef} />
) : (
<SimpleTerminal
output={simpleTerminal.output}
command={simpleTerminal.command}
workdir={simpleTerminal.workdir}
isExecuting={simpleTerminal.isExecuting}
isMobile={modalState.isMobile}
containerName={containerName}
outputRef={simpleTerminal.outputRef}
onCommandChange={simpleTerminal.setCommand}
onExecute={simpleTerminal.executeCommand}
onKeyPress={handleKeyPress}
/>
)}
</DialogContent>
<DialogActions>
<Button onClick={handleClose} variant="outlined">
Close
</Button>
</DialogActions>
<FallbackNotification
show={modalState.showFallbackNotification}
reason={modalState.fallbackReason}
onClose={() => modalState.reset()}
onRetry={modalState.handleRetryInteractive}
/>
</Dialog>
);
}