feat: add Docker container management functionality
- Implement DockerHandler to handle API requests for listing Docker containers. - Create DockerService to interact with Docker API and retrieve container information. - Add routes for Docker container management in the API. - Introduce frontend API integration for Docker container listing. - Enhance ProxyHostForm to allow quick selection of Docker containers. - Update Docker-related tests to ensure functionality and error handling. - Modify Docker Compose files to enable Docker socket access for local and remote environments. - Add TypeScript configurations for improved build processes.
This commit is contained in:
26
frontend/src/api/docker.ts
Normal file
26
frontend/src/api/docker.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import client from './client'
|
||||
|
||||
export interface DockerPort {
|
||||
private_port: number
|
||||
public_port: number
|
||||
type: string
|
||||
}
|
||||
|
||||
export interface DockerContainer {
|
||||
id: string
|
||||
names: string[]
|
||||
image: string
|
||||
state: string
|
||||
status: string
|
||||
network: string
|
||||
ip: string
|
||||
ports: DockerPort[]
|
||||
}
|
||||
|
||||
export const dockerApi = {
|
||||
listContainers: async (host?: string): Promise<DockerContainer[]> => {
|
||||
const params = host ? { host } : undefined
|
||||
const response = await client.get<DockerContainer[]>('/docker/containers', { params })
|
||||
return response.data
|
||||
},
|
||||
}
|
||||
@@ -11,7 +11,7 @@ export interface ImportPreview {
|
||||
session: ImportSession;
|
||||
preview: {
|
||||
hosts: Array<{ domain_names: string; [key: string]: unknown }>;
|
||||
conflicts: Record<string, string>;
|
||||
conflicts: string[];
|
||||
errors: string[];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from 'react'
|
||||
import type { ProxyHost } from '../api/proxyHosts'
|
||||
import { useRemoteServers } from '../hooks/useRemoteServers'
|
||||
import { useDocker } from '../hooks/useDocker'
|
||||
|
||||
interface ProxyHostFormProps {
|
||||
host?: ProxyHost
|
||||
@@ -25,6 +26,9 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor
|
||||
})
|
||||
|
||||
const { servers: remoteServers } = useRemoteServers()
|
||||
const [dockerHost, setDockerHost] = useState('')
|
||||
const [showDockerHost, setShowDockerHost] = useState(false)
|
||||
const { containers: dockerContainers, isLoading: dockerLoading, error: dockerError } = useDocker(dockerHost)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
@@ -54,6 +58,23 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor
|
||||
}
|
||||
}
|
||||
|
||||
const handleContainerSelect = (containerId: string) => {
|
||||
const container = dockerContainers.find(c => c.id === containerId)
|
||||
if (container) {
|
||||
// Prefer internal IP if available, otherwise use container name
|
||||
const host = container.ip || container.names[0]
|
||||
// Use the first exposed port if available, otherwise default to 80
|
||||
const port = container.ports && container.ports.length > 0 ? container.ports[0].private_port : 80
|
||||
|
||||
setFormData({
|
||||
...formData,
|
||||
forward_host: host,
|
||||
forward_port: port,
|
||||
forward_scheme: 'http',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
||||
<div className="bg-dark-card rounded-lg border border-gray-800 max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
||||
@@ -85,26 +106,75 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Remote Server Quick Select */}
|
||||
{remoteServers.length > 0 && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* Remote Server Quick Select */}
|
||||
{remoteServers.length > 0 && (
|
||||
<div>
|
||||
<label htmlFor="quick-select-server" className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Quick Select: Remote Server
|
||||
</label>
|
||||
<select
|
||||
id="quick-select-server"
|
||||
onChange={e => handleServerSelect(e.target.value)}
|
||||
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">-- Select a server --</option>
|
||||
{remoteServers.map(server => (
|
||||
<option key={server.uuid} value={server.uuid}>
|
||||
{server.name} ({server.host}:{server.port})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Docker Container Quick Select */}
|
||||
<div>
|
||||
<label htmlFor="quick-select" className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Quick Select from Remote Servers
|
||||
</label>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<label htmlFor="quick-select-docker" className="block text-sm font-medium text-gray-300">
|
||||
Quick Select: Container
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowDockerHost(!showDockerHost)}
|
||||
className="text-xs text-blue-400 hover:text-blue-300"
|
||||
>
|
||||
{showDockerHost ? 'Hide Remote' : 'Remote Docker?'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showDockerHost && (
|
||||
<input
|
||||
type="text"
|
||||
placeholder="tcp://100.x.y.z:2375"
|
||||
value={dockerHost}
|
||||
onChange={(e) => setDockerHost(e.target.value)}
|
||||
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white text-sm mb-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
)}
|
||||
|
||||
<select
|
||||
id="quick-select"
|
||||
onChange={e => handleServerSelect(e.target.value)}
|
||||
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
id="quick-select-docker"
|
||||
onChange={e => handleContainerSelect(e.target.value)}
|
||||
disabled={dockerLoading}
|
||||
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50"
|
||||
>
|
||||
<option value="">-- Select a server --</option>
|
||||
{remoteServers.map(server => (
|
||||
<option key={server.uuid} value={server.uuid}>
|
||||
{server.name} ({server.host}:{server.port})
|
||||
<option value="">
|
||||
{dockerLoading ? 'Loading containers...' : '-- Select a container --'}
|
||||
</option>
|
||||
{dockerContainers.map(container => (
|
||||
<option key={container.id} value={container.id}>
|
||||
{container.names[0]} ({container.image})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{dockerError && (
|
||||
<p className="text-xs text-red-400 mt-1">
|
||||
Failed to connect: {(dockerError as Error).message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Forward Details */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
|
||||
@@ -4,7 +4,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import ProxyHostForm from '../ProxyHostForm'
|
||||
import { mockRemoteServers } from '../../test/mockData'
|
||||
|
||||
// Mock the hook
|
||||
// Mock the hooks
|
||||
vi.mock('../../hooks/useRemoteServers', () => ({
|
||||
useRemoteServers: vi.fn(() => ({
|
||||
servers: mockRemoteServers,
|
||||
@@ -16,6 +16,26 @@ vi.mock('../../hooks/useRemoteServers', () => ({
|
||||
})),
|
||||
}))
|
||||
|
||||
vi.mock('../../hooks/useDocker', () => ({
|
||||
useDocker: vi.fn(() => ({
|
||||
containers: [
|
||||
{
|
||||
id: 'container-123',
|
||||
names: ['my-app'],
|
||||
image: 'nginx:latest',
|
||||
state: 'running',
|
||||
status: 'Up 2 hours',
|
||||
network: 'bridge',
|
||||
ip: '172.17.0.2',
|
||||
ports: [{ private_port: 80, public_port: 8080, type: 'tcp' }]
|
||||
}
|
||||
],
|
||||
isLoading: false,
|
||||
error: null,
|
||||
refetch: vi.fn(),
|
||||
})),
|
||||
}))
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
@@ -65,6 +85,7 @@ describe('ProxyHostForm', () => {
|
||||
block_exploits: true,
|
||||
websocket_support: false,
|
||||
enabled: true,
|
||||
locations: [],
|
||||
created_at: '2025-11-18T10:00:00Z',
|
||||
updated_at: '2025-11-18T10:00:00Z',
|
||||
}
|
||||
@@ -159,13 +180,29 @@ describe('ProxyHostForm', () => {
|
||||
expect(screen.getByText(/Local Docker Registry/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
const select = screen.getByRole('combobox', { name: /quick select/i })
|
||||
const select = screen.getByLabelText('Quick Select: Remote Server')
|
||||
fireEvent.change(select, { target: { value: mockRemoteServers[0].uuid } })
|
||||
|
||||
expect(screen.getByDisplayValue(mockRemoteServers[0].host)).toBeInTheDocument()
|
||||
expect(screen.getByDisplayValue(mockRemoteServers[0].port)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('populates fields when a docker container is selected', async () => {
|
||||
renderWithClient(
|
||||
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText('Quick Select: Container')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
const select = screen.getByLabelText('Quick Select: Container')
|
||||
fireEvent.change(select, { target: { value: 'container-123' } })
|
||||
|
||||
expect(screen.getByDisplayValue('172.17.0.2')).toBeInTheDocument() // IP
|
||||
expect(screen.getByDisplayValue('80')).toBeInTheDocument() // Port
|
||||
})
|
||||
|
||||
it('displays error message on submission failure', async () => {
|
||||
const mockErrorSubmit = vi.fn(() => Promise.reject(new Error('Submission failed')))
|
||||
renderWithClient(
|
||||
@@ -199,4 +236,19 @@ describe('ProxyHostForm', () => {
|
||||
|
||||
expect(advancedInput).toHaveValue('header_up X-Test "True"')
|
||||
})
|
||||
|
||||
it('allows entering a remote docker host', async () => {
|
||||
renderWithClient(
|
||||
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
|
||||
)
|
||||
|
||||
const toggle = screen.getByText('Remote Docker?')
|
||||
fireEvent.click(toggle)
|
||||
|
||||
const input = screen.getByPlaceholderText('tcp://100.x.y.z:2375')
|
||||
expect(input).toBeInTheDocument()
|
||||
|
||||
fireEvent.change(input, { target: { value: 'tcp://remote:2375' } })
|
||||
expect(input).toHaveValue('tcp://remote:2375')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -49,22 +49,26 @@ describe('useImport', () => {
|
||||
|
||||
it('uploads content and creates session', async () => {
|
||||
const mockSession = {
|
||||
uuid: 'session-1',
|
||||
filename: 'Caddyfile',
|
||||
state: 'reviewing',
|
||||
id: 'session-1',
|
||||
state: 'reviewing' as const,
|
||||
created_at: '2025-01-18T10:00:00Z',
|
||||
updated_at: '2025-01-18T10:00:00Z',
|
||||
}
|
||||
|
||||
const mockPreview = {
|
||||
hosts: [{ domain: 'test.com' }],
|
||||
const mockPreviewData = {
|
||||
hosts: [{ domain_names: 'test.com' }],
|
||||
conflicts: [],
|
||||
errors: [],
|
||||
}
|
||||
|
||||
vi.mocked(api.uploadCaddyfile).mockResolvedValue({ session: mockSession })
|
||||
const mockResponse = {
|
||||
session: mockSession,
|
||||
preview: mockPreviewData,
|
||||
}
|
||||
|
||||
vi.mocked(api.uploadCaddyfile).mockResolvedValue(mockResponse)
|
||||
vi.mocked(api.getImportStatus).mockResolvedValue({ has_pending: true, session: mockSession })
|
||||
vi.mocked(api.getImportPreview).mockResolvedValue(mockPreview)
|
||||
vi.mocked(api.getImportPreview).mockResolvedValue(mockResponse)
|
||||
|
||||
const { result } = renderHook(() => useImport(), { wrapper: createWrapper() })
|
||||
|
||||
@@ -103,20 +107,24 @@ describe('useImport', () => {
|
||||
|
||||
it('commits import with resolutions', async () => {
|
||||
const mockSession = {
|
||||
uuid: 'session-2',
|
||||
filename: 'Caddyfile',
|
||||
state: 'reviewing',
|
||||
id: 'session-2',
|
||||
state: 'reviewing' as const,
|
||||
created_at: '2025-01-18T10:00:00Z',
|
||||
updated_at: '2025-01-18T10:00:00Z',
|
||||
}
|
||||
|
||||
const mockResponse = {
|
||||
session: mockSession,
|
||||
preview: { hosts: [], conflicts: [], errors: [] },
|
||||
}
|
||||
|
||||
let isCommitted = false
|
||||
vi.mocked(api.uploadCaddyfile).mockResolvedValue({ session: mockSession })
|
||||
vi.mocked(api.uploadCaddyfile).mockResolvedValue(mockResponse)
|
||||
vi.mocked(api.getImportStatus).mockImplementation(async () => {
|
||||
if (isCommitted) return { has_pending: false }
|
||||
return { has_pending: true, session: mockSession }
|
||||
})
|
||||
vi.mocked(api.getImportPreview).mockResolvedValue({ hosts: [], conflicts: [], errors: [] })
|
||||
vi.mocked(api.getImportPreview).mockResolvedValue(mockResponse)
|
||||
vi.mocked(api.commitImport).mockImplementation(async () => {
|
||||
isCommitted = true
|
||||
})
|
||||
@@ -144,20 +152,24 @@ describe('useImport', () => {
|
||||
|
||||
it('cancels active import session', async () => {
|
||||
const mockSession = {
|
||||
uuid: 'session-3',
|
||||
filename: 'Caddyfile',
|
||||
state: 'reviewing',
|
||||
id: 'session-3',
|
||||
state: 'reviewing' as const,
|
||||
created_at: '2025-01-18T10:00:00Z',
|
||||
updated_at: '2025-01-18T10:00:00Z',
|
||||
}
|
||||
|
||||
const mockResponse = {
|
||||
session: mockSession,
|
||||
preview: { hosts: [], conflicts: [], errors: [] },
|
||||
}
|
||||
|
||||
let isCancelled = false
|
||||
vi.mocked(api.uploadCaddyfile).mockResolvedValue({ session: mockSession })
|
||||
vi.mocked(api.uploadCaddyfile).mockResolvedValue(mockResponse)
|
||||
vi.mocked(api.getImportStatus).mockImplementation(async () => {
|
||||
if (isCancelled) return { has_pending: false }
|
||||
return { has_pending: true, session: mockSession }
|
||||
})
|
||||
vi.mocked(api.getImportPreview).mockResolvedValue({ hosts: [], conflicts: [], errors: [] })
|
||||
vi.mocked(api.getImportPreview).mockResolvedValue(mockResponse)
|
||||
vi.mocked(api.cancelImport).mockImplementation(async () => {
|
||||
isCancelled = true
|
||||
})
|
||||
@@ -184,16 +196,20 @@ describe('useImport', () => {
|
||||
|
||||
it('handles commit errors', async () => {
|
||||
const mockSession = {
|
||||
uuid: 'session-4',
|
||||
filename: 'Caddyfile',
|
||||
state: 'reviewing',
|
||||
id: 'session-4',
|
||||
state: 'reviewing' as const,
|
||||
created_at: '2025-01-18T10:00:00Z',
|
||||
updated_at: '2025-01-18T10:00:00Z',
|
||||
}
|
||||
|
||||
vi.mocked(api.uploadCaddyfile).mockResolvedValue({ session: mockSession })
|
||||
const mockResponse = {
|
||||
session: mockSession,
|
||||
preview: { hosts: [], conflicts: [], errors: [] },
|
||||
}
|
||||
|
||||
vi.mocked(api.uploadCaddyfile).mockResolvedValue(mockResponse)
|
||||
vi.mocked(api.getImportStatus).mockResolvedValue({ has_pending: true, session: mockSession })
|
||||
vi.mocked(api.getImportPreview).mockResolvedValue({ hosts: [], conflicts: [], errors: [] })
|
||||
vi.mocked(api.getImportPreview).mockResolvedValue(mockResponse)
|
||||
|
||||
const mockError = new Error('Commit failed')
|
||||
vi.mocked(api.commitImport).mockRejectedValue(mockError)
|
||||
|
||||
@@ -13,6 +13,25 @@ vi.mock('../../api/proxyHosts', () => ({
|
||||
deleteProxyHost: vi.fn(),
|
||||
}))
|
||||
|
||||
const createMockHost = (overrides: Partial<api.ProxyHost> = {}): api.ProxyHost => ({
|
||||
uuid: '1',
|
||||
domain_names: 'test.com',
|
||||
forward_scheme: 'http',
|
||||
forward_host: 'localhost',
|
||||
forward_port: 8080,
|
||||
ssl_forced: false,
|
||||
http2_support: false,
|
||||
hsts_enabled: false,
|
||||
hsts_subdomains: false,
|
||||
block_exploits: false,
|
||||
websocket_support: false,
|
||||
locations: [],
|
||||
enabled: true,
|
||||
created_at: '2025-01-01T00:00:00Z',
|
||||
updated_at: '2025-01-01T00:00:00Z',
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const createWrapper = () => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
@@ -37,8 +56,8 @@ describe('useProxyHosts', () => {
|
||||
|
||||
it('loads proxy hosts on mount', async () => {
|
||||
const mockHosts = [
|
||||
{ uuid: '1', domain_names: 'test.com', enabled: true, forward_host: 'localhost', forward_port: 8080 },
|
||||
{ uuid: '2', domain_names: 'app.com', enabled: true, forward_host: 'localhost', forward_port: 3000 },
|
||||
createMockHost({ uuid: '1', domain_names: 'test.com', enabled: true, forward_host: 'localhost', forward_port: 8080 }),
|
||||
createMockHost({ uuid: '2', domain_names: 'app.com', enabled: true, forward_host: 'localhost', forward_port: 3000 }),
|
||||
]
|
||||
|
||||
vi.mocked(api.getProxyHosts).mockResolvedValue(mockHosts)
|
||||
@@ -74,7 +93,7 @@ describe('useProxyHosts', () => {
|
||||
it('creates a new proxy host', async () => {
|
||||
vi.mocked(api.getProxyHosts).mockResolvedValue([])
|
||||
const newHost = { domain_names: 'new.com', forward_host: 'localhost', forward_port: 9000 }
|
||||
const createdHost = { uuid: '3', ...newHost, enabled: true }
|
||||
const createdHost = createMockHost({ uuid: '3', ...newHost, enabled: true })
|
||||
|
||||
vi.mocked(api.createProxyHost).mockImplementation(async () => {
|
||||
vi.mocked(api.getProxyHosts).mockResolvedValue([createdHost])
|
||||
@@ -98,12 +117,11 @@ describe('useProxyHosts', () => {
|
||||
})
|
||||
|
||||
it('updates an existing proxy host', async () => {
|
||||
const existingHost = { uuid: '1', domain_names: 'test.com', enabled: true, forward_host: 'localhost', forward_port: 8080 }
|
||||
const existingHost = createMockHost({ uuid: '1', domain_names: 'test.com', enabled: true, forward_host: 'localhost', forward_port: 8080 })
|
||||
let hosts = [existingHost]
|
||||
vi.mocked(api.getProxyHosts).mockImplementation(() => Promise.resolve(hosts))
|
||||
|
||||
const updatedHost = { ...existingHost, domain_names: 'updated.com' }
|
||||
vi.mocked(api.updateProxyHost).mockImplementation(async (uuid, data) => {
|
||||
vi.mocked(api.updateProxyHost).mockImplementation(async (_, data) => {
|
||||
hosts = [{ ...existingHost, ...data }]
|
||||
return hosts[0]
|
||||
})
|
||||
@@ -126,8 +144,8 @@ describe('useProxyHosts', () => {
|
||||
|
||||
it('deletes a proxy host', async () => {
|
||||
const hosts = [
|
||||
{ uuid: '1', domain_names: 'test.com', enabled: true, forward_host: 'localhost', forward_port: 8080 },
|
||||
{ uuid: '2', domain_names: 'app.com', enabled: true, forward_host: 'localhost', forward_port: 3000 },
|
||||
createMockHost({ uuid: '1', domain_names: 'test.com', enabled: true, forward_host: 'localhost', forward_port: 8080 }),
|
||||
createMockHost({ uuid: '2', domain_names: 'app.com', enabled: true, forward_host: 'localhost', forward_port: 3000 }),
|
||||
]
|
||||
vi.mocked(api.getProxyHosts).mockResolvedValue(hosts)
|
||||
vi.mocked(api.deleteProxyHost).mockImplementation(async (uuid) => {
|
||||
@@ -167,7 +185,7 @@ describe('useProxyHosts', () => {
|
||||
})
|
||||
|
||||
it('handles update errors', async () => {
|
||||
const host = { uuid: '1', domain_names: 'test.com', enabled: true, forward_host: 'localhost', forward_port: 8080 }
|
||||
const host = createMockHost({ uuid: '1', domain_names: 'test.com', enabled: true, forward_host: 'localhost', forward_port: 8080 })
|
||||
vi.mocked(api.getProxyHosts).mockResolvedValue([host])
|
||||
const mockError = new Error('Failed to update')
|
||||
vi.mocked(api.updateProxyHost).mockRejectedValue(mockError)
|
||||
@@ -182,7 +200,7 @@ describe('useProxyHosts', () => {
|
||||
})
|
||||
|
||||
it('handles delete errors', async () => {
|
||||
const host = { uuid: '1', domain_names: 'test.com', enabled: true, forward_host: 'localhost', forward_port: 8080 }
|
||||
const host = createMockHost({ uuid: '1', domain_names: 'test.com', enabled: true, forward_host: 'localhost', forward_port: 8080 })
|
||||
vi.mocked(api.getProxyHosts).mockResolvedValue([host])
|
||||
const mockError = new Error('Failed to delete')
|
||||
vi.mocked(api.deleteProxyHost).mockRejectedValue(mockError)
|
||||
|
||||
@@ -14,6 +14,19 @@ vi.mock('../../api/remoteServers', () => ({
|
||||
testRemoteServerConnection: vi.fn(),
|
||||
}))
|
||||
|
||||
const createMockServer = (overrides: Partial<api.RemoteServer> = {}): api.RemoteServer => ({
|
||||
uuid: '1',
|
||||
name: 'Server 1',
|
||||
provider: 'generic',
|
||||
host: 'localhost',
|
||||
port: 8080,
|
||||
enabled: true,
|
||||
reachable: true,
|
||||
created_at: '2025-01-01T00:00:00Z',
|
||||
updated_at: '2025-01-01T00:00:00Z',
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const createWrapper = () => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
@@ -38,8 +51,8 @@ describe('useRemoteServers', () => {
|
||||
|
||||
it('loads all remote servers on mount', async () => {
|
||||
const mockServers = [
|
||||
{ uuid: '1', name: 'Server 1', host: 'localhost', port: 8080, enabled: true },
|
||||
{ uuid: '2', name: 'Server 2', host: '192.168.1.100', port: 3000, enabled: false },
|
||||
createMockServer({ uuid: '1', name: 'Server 1', host: 'localhost', port: 8080, enabled: true }),
|
||||
createMockServer({ uuid: '2', name: 'Server 2', host: '192.168.1.100', port: 3000, enabled: false }),
|
||||
]
|
||||
|
||||
vi.mocked(api.getRemoteServers).mockResolvedValue(mockServers)
|
||||
@@ -75,7 +88,7 @@ describe('useRemoteServers', () => {
|
||||
it('creates a new remote server', async () => {
|
||||
vi.mocked(api.getRemoteServers).mockResolvedValue([])
|
||||
const newServer = { name: 'New Server', host: 'new.local', port: 5000, provider: 'generic' }
|
||||
const createdServer = { uuid: '4', ...newServer, enabled: true }
|
||||
const createdServer = createMockServer({ uuid: '4', ...newServer, enabled: true })
|
||||
|
||||
vi.mocked(api.createRemoteServer).mockImplementation(async () => {
|
||||
vi.mocked(api.getRemoteServers).mockResolvedValue([createdServer])
|
||||
@@ -99,12 +112,11 @@ describe('useRemoteServers', () => {
|
||||
})
|
||||
|
||||
it('updates an existing remote server', async () => {
|
||||
const existingServer = { uuid: '1', name: 'Server 1', host: 'localhost', port: 8080, enabled: true }
|
||||
const existingServer = createMockServer({ uuid: '1', name: 'Server 1', host: 'localhost', port: 8080, enabled: true })
|
||||
let servers = [existingServer]
|
||||
vi.mocked(api.getRemoteServers).mockImplementation(() => Promise.resolve(servers))
|
||||
|
||||
const updatedServer = { ...existingServer, name: 'Updated Server' }
|
||||
vi.mocked(api.updateRemoteServer).mockImplementation(async (uuid, data) => {
|
||||
vi.mocked(api.updateRemoteServer).mockImplementation(async (_, data) => {
|
||||
servers = [{ ...existingServer, ...data }]
|
||||
return servers[0]
|
||||
})
|
||||
@@ -127,8 +139,8 @@ describe('useRemoteServers', () => {
|
||||
|
||||
it('deletes a remote server', async () => {
|
||||
const servers = [
|
||||
{ uuid: '1', name: 'Server 1', host: 'localhost', port: 8080, enabled: true },
|
||||
{ uuid: '2', name: 'Server 2', host: '192.168.1.100', port: 3000, enabled: false },
|
||||
createMockServer({ uuid: '1', name: 'Server 1', host: 'localhost', port: 8080, enabled: true }),
|
||||
createMockServer({ uuid: '2', name: 'Server 2', host: '192.168.1.100', port: 3000, enabled: false }),
|
||||
]
|
||||
vi.mocked(api.getRemoteServers).mockResolvedValue(servers)
|
||||
vi.mocked(api.deleteRemoteServer).mockImplementation(async (uuid) => {
|
||||
@@ -185,7 +197,7 @@ describe('useRemoteServers', () => {
|
||||
})
|
||||
|
||||
it('handles update errors', async () => {
|
||||
const server = { uuid: '1', name: 'Server 1', host: 'localhost', port: 8080, enabled: true }
|
||||
const server = createMockServer({ uuid: '1', name: 'Server 1', host: 'localhost', port: 8080, enabled: true })
|
||||
vi.mocked(api.getRemoteServers).mockResolvedValue([server])
|
||||
const mockError = new Error('Failed to update')
|
||||
vi.mocked(api.updateRemoteServer).mockRejectedValue(mockError)
|
||||
@@ -196,11 +208,11 @@ describe('useRemoteServers', () => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
await expect(result.current.updateServer('1', { name: 'Updated' })).rejects.toThrow('Failed to update')
|
||||
await expect(result.current.updateServer('1', { name: 'Updated Server' })).rejects.toThrow('Failed to update')
|
||||
})
|
||||
|
||||
it('handles delete errors', async () => {
|
||||
const server = { uuid: '1', name: 'Server 1', host: 'localhost', port: 8080, enabled: true }
|
||||
const server = createMockServer({ uuid: '1', name: 'Server 1', host: 'localhost', port: 8080, enabled: true })
|
||||
vi.mocked(api.getRemoteServers).mockResolvedValue([server])
|
||||
const mockError = new Error('Failed to delete')
|
||||
vi.mocked(api.deleteRemoteServer).mockRejectedValue(mockError)
|
||||
|
||||
22
frontend/src/hooks/useDocker.ts
Normal file
22
frontend/src/hooks/useDocker.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { dockerApi } from '../api/docker'
|
||||
|
||||
export function useDocker(host?: string) {
|
||||
const {
|
||||
data: containers = [],
|
||||
isLoading,
|
||||
error,
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: ['docker-containers', host],
|
||||
queryFn: () => dockerApi.listContainers(host),
|
||||
retry: 1, // Don't retry too much if docker is not available
|
||||
})
|
||||
|
||||
return {
|
||||
containers,
|
||||
isLoading,
|
||||
error,
|
||||
refetch,
|
||||
}
|
||||
}
|
||||
@@ -134,7 +134,7 @@ api.example.com {
|
||||
{showReview && preview && preview.preview && (
|
||||
<ImportReviewTable
|
||||
hosts={preview.preview.hosts}
|
||||
conflicts={Object.keys(preview.preview.conflicts)}
|
||||
conflicts={preview.preview.conflicts}
|
||||
errors={preview.preview.errors}
|
||||
onCommit={handleCommit}
|
||||
onCancel={() => setShowReview(false)}
|
||||
|
||||
@@ -8,14 +8,13 @@ export const mockProxyHosts: ProxyHost[] = [
|
||||
forward_scheme: 'http',
|
||||
forward_host: 'localhost',
|
||||
forward_port: 3000,
|
||||
access_list_id: undefined,
|
||||
certificate_id: undefined,
|
||||
ssl_forced: false,
|
||||
http2_support: true,
|
||||
hsts_enabled: false,
|
||||
hsts_subdomains: false,
|
||||
block_exploits: true,
|
||||
websocket_support: true,
|
||||
locations: [],
|
||||
advanced_config: undefined,
|
||||
enabled: true,
|
||||
created_at: '2025-11-18T10:00:00Z',
|
||||
@@ -27,14 +26,13 @@ export const mockProxyHosts: ProxyHost[] = [
|
||||
forward_scheme: 'http',
|
||||
forward_host: '192.168.1.100',
|
||||
forward_port: 8080,
|
||||
access_list_id: undefined,
|
||||
certificate_id: undefined,
|
||||
ssl_forced: false,
|
||||
http2_support: true,
|
||||
hsts_enabled: false,
|
||||
hsts_subdomains: false,
|
||||
block_exploits: true,
|
||||
websocket_support: false,
|
||||
locations: [],
|
||||
advanced_config: undefined,
|
||||
enabled: true,
|
||||
created_at: '2025-11-18T10:00:00Z',
|
||||
|
||||
Reference in New Issue
Block a user