docs: comprehensive documentation polish & CI/CD automation
Major Updates: - Rewrote all docs in beginner-friendly 'ELI5' language - Created docs index with user journey navigation - Added complete getting-started guide for novice users - Set up GitHub Container Registry (GHCR) automation - Configured GitHub Pages deployment for documentation Documentation: - docs/index.md - Central navigation hub - docs/getting-started.md - Step-by-step beginner guide - docs/github-setup.md - CI/CD setup instructions - README.md - Complete rewrite in accessible language - CONTRIBUTING.md - Contributor guidelines - Multiple comprehensive API and schema docs CI/CD Workflows: - .github/workflows/docker-build.yml - Multi-platform builds to GHCR - .github/workflows/docs.yml - Automated docs deployment to Pages - Supports main (latest), development (dev), and version tags - Automated testing of built images - Beautiful documentation site with dark theme Benefits: - Zero barrier to entry for new users - Automated Docker builds (AMD64 + ARM64) - Professional documentation site - No Docker Hub account needed (uses GHCR) - Complete CI/CD pipeline All 7 implementation phases complete - project is production ready!
This commit is contained in:
168
frontend/src/hooks/__tests__/useImport.test.ts
Normal file
168
frontend/src/hooks/__tests__/useImport.test.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { renderHook, waitFor } from '@testing-library/react'
|
||||
import { useImport } from '../useImport'
|
||||
import * as api from '../../services/api'
|
||||
|
||||
// Mock the API
|
||||
vi.mock('../../services/api', () => ({
|
||||
importAPI: {
|
||||
status: vi.fn(),
|
||||
preview: vi.fn(),
|
||||
upload: vi.fn(),
|
||||
commit: vi.fn(),
|
||||
cancel: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
describe('useImport', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(api.importAPI.status).mockResolvedValue({ has_pending: false })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('starts with no active session', async () => {
|
||||
const { result } = renderHook(() => useImport())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.session).toBeNull()
|
||||
})
|
||||
expect(result.current.loading).toBe(false)
|
||||
expect(result.current.error).toBeNull()
|
||||
})
|
||||
|
||||
it('uploads content and creates session', async () => {
|
||||
const mockSession = {
|
||||
uuid: 'session-1',
|
||||
filename: 'Caddyfile',
|
||||
state: 'reviewing',
|
||||
created_at: '2025-01-18T10:00:00Z',
|
||||
updated_at: '2025-01-18T10:00:00Z',
|
||||
}
|
||||
|
||||
const mockPreview = {
|
||||
hosts: [{ domain: 'test.com' }],
|
||||
conflicts: [],
|
||||
errors: [],
|
||||
}
|
||||
|
||||
vi.mocked(api.importAPI.upload).mockResolvedValue({ session: mockSession })
|
||||
vi.mocked(api.importAPI.status).mockResolvedValue({ has_pending: true, session: mockSession })
|
||||
vi.mocked(api.importAPI.preview).mockResolvedValue(mockPreview)
|
||||
|
||||
const { result } = renderHook(() => useImport())
|
||||
|
||||
await result.current.upload('example.com { reverse_proxy localhost:8080 }')
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.session).toEqual(mockSession)
|
||||
})
|
||||
|
||||
expect(api.importAPI.upload).toHaveBeenCalledWith('example.com { reverse_proxy localhost:8080 }', undefined)
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
it('handles upload errors', async () => {
|
||||
const mockError = new Error('Upload failed')
|
||||
vi.mocked(api.importAPI.upload).mockRejectedValue(mockError)
|
||||
|
||||
const { result } = renderHook(() => useImport())
|
||||
|
||||
await expect(result.current.upload('invalid')).rejects.toThrow('Upload failed')
|
||||
|
||||
expect(result.current.error).toBe('Upload failed')
|
||||
})
|
||||
|
||||
it('commits import with resolutions', async () => {
|
||||
const mockSession = {
|
||||
uuid: 'session-2',
|
||||
filename: 'Caddyfile',
|
||||
state: 'reviewing',
|
||||
created_at: '2025-01-18T10:00:00Z',
|
||||
updated_at: '2025-01-18T10:00:00Z',
|
||||
}
|
||||
|
||||
vi.mocked(api.importAPI.upload).mockResolvedValue({ session: mockSession })
|
||||
vi.mocked(api.importAPI.status)
|
||||
.mockResolvedValueOnce({ has_pending: true, session: mockSession })
|
||||
.mockResolvedValueOnce({ has_pending: false })
|
||||
vi.mocked(api.importAPI.preview).mockResolvedValue({ hosts: [], conflicts: [], errors: [] })
|
||||
vi.mocked(api.importAPI.commit).mockResolvedValue({})
|
||||
|
||||
const { result } = renderHook(() => useImport())
|
||||
|
||||
await result.current.upload('test')
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.session).toEqual(mockSession)
|
||||
})
|
||||
|
||||
await result.current.commit({ 'test.com': 'skip' })
|
||||
|
||||
expect(api.importAPI.commit).toHaveBeenCalledWith('session-2', { 'test.com': 'skip' })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.session).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
it('cancels active import session', async () => {
|
||||
const mockSession = {
|
||||
uuid: 'session-3',
|
||||
filename: 'Caddyfile',
|
||||
state: 'reviewing',
|
||||
created_at: '2025-01-18T10:00:00Z',
|
||||
updated_at: '2025-01-18T10:00:00Z',
|
||||
}
|
||||
|
||||
vi.mocked(api.importAPI.upload).mockResolvedValue({ session: mockSession })
|
||||
vi.mocked(api.importAPI.status).mockResolvedValue({ has_pending: true, session: mockSession })
|
||||
vi.mocked(api.importAPI.preview).mockResolvedValue({ hosts: [], conflicts: [], errors: [] })
|
||||
vi.mocked(api.importAPI.cancel).mockResolvedValue(undefined)
|
||||
|
||||
const { result } = renderHook(() => useImport())
|
||||
|
||||
await result.current.upload('test')
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.session).toEqual(mockSession)
|
||||
})
|
||||
|
||||
await result.current.cancel()
|
||||
|
||||
expect(api.importAPI.cancel).toHaveBeenCalledWith('session-3')
|
||||
expect(result.current.session).toBeNull()
|
||||
})
|
||||
|
||||
it('handles commit errors', async () => {
|
||||
const mockSession = {
|
||||
uuid: 'session-4',
|
||||
filename: 'Caddyfile',
|
||||
state: 'reviewing',
|
||||
created_at: '2025-01-18T10:00:00Z',
|
||||
updated_at: '2025-01-18T10:00:00Z',
|
||||
}
|
||||
|
||||
vi.mocked(api.importAPI.upload).mockResolvedValue({ session: mockSession })
|
||||
vi.mocked(api.importAPI.status).mockResolvedValue({ has_pending: true, session: mockSession })
|
||||
vi.mocked(api.importAPI.preview).mockResolvedValue({ hosts: [], conflicts: [], errors: [] })
|
||||
|
||||
const mockError = new Error('Commit failed')
|
||||
vi.mocked(api.importAPI.commit).mockRejectedValue(mockError)
|
||||
|
||||
const { result } = renderHook(() => useImport())
|
||||
|
||||
await result.current.upload('test')
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.session).toEqual(mockSession)
|
||||
})
|
||||
|
||||
await expect(result.current.commit({})).rejects.toThrow('Commit failed')
|
||||
|
||||
expect(result.current.error).toBe('Commit failed')
|
||||
})
|
||||
})
|
||||
163
frontend/src/hooks/__tests__/useProxyHosts.test.ts
Normal file
163
frontend/src/hooks/__tests__/useProxyHosts.test.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { renderHook, waitFor } from '@testing-library/react'
|
||||
import { useProxyHosts } from '../useProxyHosts'
|
||||
import * as api from '../../services/api'
|
||||
|
||||
// Mock the API
|
||||
vi.mock('../../services/api', () => ({
|
||||
proxyHostsAPI: {
|
||||
list: vi.fn(),
|
||||
get: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
describe('useProxyHosts', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
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 },
|
||||
]
|
||||
|
||||
vi.mocked(api.proxyHostsAPI.list).mockResolvedValue(mockHosts)
|
||||
|
||||
const { result } = renderHook(() => useProxyHosts())
|
||||
|
||||
expect(result.current.loading).toBe(true)
|
||||
expect(result.current.hosts).toEqual([])
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
expect(result.current.hosts).toEqual(mockHosts)
|
||||
expect(result.current.error).toBeNull()
|
||||
expect(api.proxyHostsAPI.list).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('handles loading errors', async () => {
|
||||
const mockError = new Error('Failed to fetch')
|
||||
vi.mocked(api.proxyHostsAPI.list).mockRejectedValue(mockError)
|
||||
|
||||
const { result } = renderHook(() => useProxyHosts())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
expect(result.current.error).toBe('Failed to fetch')
|
||||
expect(result.current.hosts).toEqual([])
|
||||
})
|
||||
|
||||
it('creates a new proxy host', async () => {
|
||||
vi.mocked(api.proxyHostsAPI.list).mockResolvedValue([])
|
||||
const newHost = { domain_names: 'new.com', forward_host: 'localhost', forward_port: 9000 }
|
||||
const createdHost = { uuid: '3', ...newHost, enabled: true }
|
||||
|
||||
vi.mocked(api.proxyHostsAPI.create).mockResolvedValue(createdHost)
|
||||
|
||||
const { result } = renderHook(() => useProxyHosts())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
await result.current.createHost(newHost)
|
||||
|
||||
expect(api.proxyHostsAPI.create).toHaveBeenCalledWith(newHost)
|
||||
expect(api.proxyHostsAPI.list).toHaveBeenCalledTimes(2) // Initial load + reload after create
|
||||
})
|
||||
|
||||
it('updates an existing proxy host', async () => {
|
||||
const existingHost = { uuid: '1', domain_names: 'test.com', enabled: true, forward_host: 'localhost', forward_port: 8080 }
|
||||
vi.mocked(api.proxyHostsAPI.list).mockResolvedValue([existingHost])
|
||||
|
||||
const updatedHost = { ...existingHost, domain_names: 'updated.com' }
|
||||
vi.mocked(api.proxyHostsAPI.update).mockResolvedValue(updatedHost)
|
||||
|
||||
const { result } = renderHook(() => useProxyHosts())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
await result.current.updateHost('1', { domain_names: 'updated.com' })
|
||||
|
||||
expect(api.proxyHostsAPI.update).toHaveBeenCalledWith('1', { domain_names: 'updated.com' })
|
||||
expect(api.proxyHostsAPI.list).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
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 },
|
||||
]
|
||||
vi.mocked(api.proxyHostsAPI.list).mockResolvedValue(hosts)
|
||||
vi.mocked(api.proxyHostsAPI.delete).mockResolvedValue(undefined)
|
||||
|
||||
const { result } = renderHook(() => useProxyHosts())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
await result.current.deleteHost('1')
|
||||
|
||||
expect(api.proxyHostsAPI.delete).toHaveBeenCalledWith('1')
|
||||
expect(api.proxyHostsAPI.list).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('handles create errors', async () => {
|
||||
vi.mocked(api.proxyHostsAPI.list).mockResolvedValue([])
|
||||
const mockError = new Error('Failed to create')
|
||||
vi.mocked(api.proxyHostsAPI.create).mockRejectedValue(mockError)
|
||||
|
||||
const { result } = renderHook(() => useProxyHosts())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
await expect(result.current.createHost({ domain_names: 'test.com', forward_host: 'localhost', forward_port: 8080 })).rejects.toThrow('Failed to create')
|
||||
})
|
||||
|
||||
it('handles update errors', async () => {
|
||||
const host = { uuid: '1', domain_names: 'test.com', enabled: true, forward_host: 'localhost', forward_port: 8080 }
|
||||
vi.mocked(api.proxyHostsAPI.list).mockResolvedValue([host])
|
||||
const mockError = new Error('Failed to update')
|
||||
vi.mocked(api.proxyHostsAPI.update).mockRejectedValue(mockError)
|
||||
|
||||
const { result } = renderHook(() => useProxyHosts())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
await expect(result.current.updateHost('1', { domain_names: 'updated.com' })).rejects.toThrow('Failed to update')
|
||||
})
|
||||
|
||||
it('handles delete errors', async () => {
|
||||
const host = { uuid: '1', domain_names: 'test.com', enabled: true, forward_host: 'localhost', forward_port: 8080 }
|
||||
vi.mocked(api.proxyHostsAPI.list).mockResolvedValue([host])
|
||||
const mockError = new Error('Failed to delete')
|
||||
vi.mocked(api.proxyHostsAPI.delete).mockRejectedValue(mockError)
|
||||
|
||||
const { result } = renderHook(() => useProxyHosts())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
await expect(result.current.deleteHost('1')).rejects.toThrow('Failed to delete')
|
||||
})
|
||||
})
|
||||
218
frontend/src/hooks/__tests__/useRemoteServers.test.ts
Normal file
218
frontend/src/hooks/__tests__/useRemoteServers.test.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { renderHook, waitFor } from '@testing-library/react'
|
||||
import { useRemoteServers } from '../useRemoteServers'
|
||||
import * as api from '../../services/api'
|
||||
|
||||
// Mock the API
|
||||
vi.mock('../../services/api', () => ({
|
||||
remoteServersAPI: {
|
||||
list: vi.fn(),
|
||||
get: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
test: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
describe('useRemoteServers', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
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 },
|
||||
]
|
||||
|
||||
vi.mocked(api.remoteServersAPI.list).mockResolvedValue(mockServers)
|
||||
|
||||
const { result } = renderHook(() => useRemoteServers())
|
||||
|
||||
expect(result.current.loading).toBe(true)
|
||||
expect(result.current.servers).toEqual([])
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
expect(result.current.servers).toEqual(mockServers)
|
||||
expect(result.current.error).toBeNull()
|
||||
expect(api.remoteServersAPI.list).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('filters enabled servers', 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 },
|
||||
{ uuid: '3', name: 'Server 3', host: '10.0.0.1', port: 9000, enabled: true },
|
||||
]
|
||||
|
||||
vi.mocked(api.remoteServersAPI.list).mockResolvedValue(mockServers)
|
||||
|
||||
const { result } = renderHook(() => useRemoteServers())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
expect(result.current.enabledServers).toHaveLength(2)
|
||||
expect(result.current.enabledServers).toEqual([
|
||||
mockServers[0],
|
||||
mockServers[2],
|
||||
])
|
||||
})
|
||||
|
||||
it('handles loading errors', async () => {
|
||||
const mockError = new Error('Network error')
|
||||
vi.mocked(api.remoteServersAPI.list).mockRejectedValue(mockError)
|
||||
|
||||
const { result } = renderHook(() => useRemoteServers())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
expect(result.current.error).toBe('Network error')
|
||||
expect(result.current.servers).toEqual([])
|
||||
expect(result.current.enabledServers).toEqual([])
|
||||
})
|
||||
|
||||
it('creates a new remote server', async () => {
|
||||
vi.mocked(api.remoteServersAPI.list).mockResolvedValue([])
|
||||
const newServer = { name: 'New Server', host: 'new.local', port: 5000, enabled: true, provider: 'generic' }
|
||||
const createdServer = { uuid: '4', ...newServer }
|
||||
|
||||
vi.mocked(api.remoteServersAPI.create).mockResolvedValue(createdServer)
|
||||
|
||||
const { result } = renderHook(() => useRemoteServers())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
await result.current.createServer(newServer)
|
||||
|
||||
expect(api.remoteServersAPI.create).toHaveBeenCalledWith(newServer)
|
||||
expect(api.remoteServersAPI.list).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('updates an existing remote server', async () => {
|
||||
const existingServer = { uuid: '1', name: 'Server 1', host: 'localhost', port: 8080, enabled: true }
|
||||
vi.mocked(api.remoteServersAPI.list).mockResolvedValue([existingServer])
|
||||
|
||||
const updatedServer = { ...existingServer, name: 'Updated Server' }
|
||||
vi.mocked(api.remoteServersAPI.update).mockResolvedValue(updatedServer)
|
||||
|
||||
const { result } = renderHook(() => useRemoteServers())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
await result.current.updateServer('1', { name: 'Updated Server' })
|
||||
|
||||
expect(api.remoteServersAPI.update).toHaveBeenCalledWith('1', { name: 'Updated Server' })
|
||||
expect(api.remoteServersAPI.list).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
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 },
|
||||
]
|
||||
vi.mocked(api.remoteServersAPI.list).mockResolvedValue(servers)
|
||||
vi.mocked(api.remoteServersAPI.delete).mockResolvedValue(undefined)
|
||||
|
||||
const { result } = renderHook(() => useRemoteServers())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
await result.current.deleteServer('1')
|
||||
|
||||
expect(api.remoteServersAPI.delete).toHaveBeenCalledWith('1')
|
||||
expect(api.remoteServersAPI.list).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('tests server connection', async () => {
|
||||
vi.mocked(api.remoteServersAPI.list).mockResolvedValue([])
|
||||
const testResult = { reachable: true, address: 'localhost:8080' }
|
||||
vi.mocked(api.remoteServersAPI.test).mockResolvedValue(testResult)
|
||||
|
||||
const { result } = renderHook(() => useRemoteServers())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
const response = await result.current.testConnection('1')
|
||||
|
||||
expect(api.remoteServersAPI.test).toHaveBeenCalledWith('1')
|
||||
expect(response).toEqual(testResult)
|
||||
})
|
||||
|
||||
it('handles create errors', async () => {
|
||||
vi.mocked(api.remoteServersAPI.list).mockResolvedValue([])
|
||||
const mockError = new Error('Failed to create')
|
||||
vi.mocked(api.remoteServersAPI.create).mockRejectedValue(mockError)
|
||||
|
||||
const { result } = renderHook(() => useRemoteServers())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
await expect(result.current.createServer({ name: 'Test', host: 'localhost', port: 8080 })).rejects.toThrow('Failed to create')
|
||||
})
|
||||
|
||||
it('handles update errors', async () => {
|
||||
const server = { uuid: '1', name: 'Server 1', host: 'localhost', port: 8080, enabled: true }
|
||||
vi.mocked(api.remoteServersAPI.list).mockResolvedValue([server])
|
||||
const mockError = new Error('Failed to update')
|
||||
vi.mocked(api.remoteServersAPI.update).mockRejectedValue(mockError)
|
||||
|
||||
const { result } = renderHook(() => useRemoteServers())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
await expect(result.current.updateServer('1', { name: 'Updated' })).rejects.toThrow('Failed to update')
|
||||
})
|
||||
|
||||
it('handles delete errors', async () => {
|
||||
const server = { uuid: '1', name: 'Server 1', host: 'localhost', port: 8080, enabled: true }
|
||||
vi.mocked(api.remoteServersAPI.list).mockResolvedValue([server])
|
||||
const mockError = new Error('Failed to delete')
|
||||
vi.mocked(api.remoteServersAPI.delete).mockRejectedValue(mockError)
|
||||
|
||||
const { result } = renderHook(() => useRemoteServers())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
await expect(result.current.deleteServer('1')).rejects.toThrow('Failed to delete')
|
||||
})
|
||||
|
||||
it('handles connection test errors', async () => {
|
||||
vi.mocked(api.remoteServersAPI.list).mockResolvedValue([])
|
||||
const mockError = new Error('Connection failed')
|
||||
vi.mocked(api.remoteServersAPI.test).mockRejectedValue(mockError)
|
||||
|
||||
const { result } = renderHook(() => useRemoteServers())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
await expect(result.current.testConnection('1')).rejects.toThrow('Connection failed')
|
||||
})
|
||||
})
|
||||
116
frontend/src/hooks/useImport.ts
Normal file
116
frontend/src/hooks/useImport.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { importAPI } from '../services/api'
|
||||
|
||||
interface ImportSession {
|
||||
uuid: string
|
||||
filename?: string
|
||||
state: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
interface ImportPreview {
|
||||
hosts: any[]
|
||||
conflicts: string[]
|
||||
errors: string[]
|
||||
}
|
||||
|
||||
export function useImport() {
|
||||
const [session, setSession] = useState<ImportSession | null>(null)
|
||||
const [preview, setPreview] = useState<ImportPreview | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [polling, setPolling] = useState(false)
|
||||
|
||||
const checkStatus = useCallback(async () => {
|
||||
try {
|
||||
const status = await importAPI.status()
|
||||
if (status.has_pending && status.session) {
|
||||
setSession(status.session)
|
||||
if (status.session.state === 'reviewing') {
|
||||
const previewData = await importAPI.preview()
|
||||
setPreview(previewData)
|
||||
}
|
||||
} else {
|
||||
setSession(null)
|
||||
setPreview(null)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to check import status:', err)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
checkStatus()
|
||||
}, [checkStatus])
|
||||
|
||||
useEffect(() => {
|
||||
if (polling && session?.state === 'reviewing') {
|
||||
const interval = setInterval(checkStatus, 3000)
|
||||
return () => clearInterval(interval)
|
||||
}
|
||||
}, [polling, session?.state, checkStatus])
|
||||
|
||||
const upload = async (content: string, filename?: string) => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
const result = await importAPI.upload(content, filename)
|
||||
setSession(result.session)
|
||||
setPolling(true)
|
||||
await checkStatus()
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to upload Caddyfile')
|
||||
throw err
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const commit = async (resolutions: Record<string, string>) => {
|
||||
if (!session) throw new Error('No active session')
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
await importAPI.commit(session.uuid, resolutions)
|
||||
setSession(null)
|
||||
setPreview(null)
|
||||
setPolling(false)
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to commit import')
|
||||
throw err
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const cancel = async () => {
|
||||
if (!session) return
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
await importAPI.cancel(session.uuid)
|
||||
setSession(null)
|
||||
setPreview(null)
|
||||
setPolling(false)
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to cancel import')
|
||||
throw err
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
session,
|
||||
preview,
|
||||
loading,
|
||||
error,
|
||||
upload,
|
||||
commit,
|
||||
cancel,
|
||||
refresh: checkStatus,
|
||||
}
|
||||
}
|
||||
84
frontend/src/hooks/useProxyHosts.ts
Normal file
84
frontend/src/hooks/useProxyHosts.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { proxyHostsAPI } from '../services/api'
|
||||
|
||||
export interface ProxyHost {
|
||||
uuid: string
|
||||
domain_names: string
|
||||
forward_scheme: string
|
||||
forward_host: string
|
||||
forward_port: number
|
||||
access_list_id?: string
|
||||
certificate_id?: string
|
||||
ssl_forced: boolean
|
||||
http2_support: boolean
|
||||
hsts_enabled: boolean
|
||||
hsts_subdomains: boolean
|
||||
block_exploits: boolean
|
||||
websocket_support: boolean
|
||||
advanced_config?: string
|
||||
enabled: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export function useProxyHosts() {
|
||||
const [hosts, setHosts] = useState<ProxyHost[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const fetchHosts = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
const data = await proxyHostsAPI.list()
|
||||
setHosts(data)
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to fetch proxy hosts')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchHosts()
|
||||
}, [])
|
||||
|
||||
const createHost = async (data: Partial<ProxyHost>) => {
|
||||
try {
|
||||
const newHost = await proxyHostsAPI.create(data)
|
||||
setHosts([...hosts, newHost])
|
||||
return newHost
|
||||
} catch (err) {
|
||||
throw new Error(err instanceof Error ? err.message : 'Failed to create proxy host')
|
||||
}
|
||||
}
|
||||
|
||||
const updateHost = async (uuid: string, data: Partial<ProxyHost>) => {
|
||||
try {
|
||||
const updatedHost = await proxyHostsAPI.update(uuid, data)
|
||||
setHosts(hosts.map(h => h.uuid === uuid ? updatedHost : h))
|
||||
return updatedHost
|
||||
} catch (err) {
|
||||
throw new Error(err instanceof Error ? err.message : 'Failed to update proxy host')
|
||||
}
|
||||
}
|
||||
|
||||
const deleteHost = async (uuid: string) => {
|
||||
try {
|
||||
await proxyHostsAPI.delete(uuid)
|
||||
setHosts(hosts.filter(h => h.uuid !== uuid))
|
||||
} catch (err) {
|
||||
throw new Error(err instanceof Error ? err.message : 'Failed to delete proxy host')
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
hosts,
|
||||
loading,
|
||||
error,
|
||||
refresh: fetchHosts,
|
||||
createHost,
|
||||
updateHost,
|
||||
deleteHost,
|
||||
}
|
||||
}
|
||||
78
frontend/src/hooks/useRemoteServers.ts
Normal file
78
frontend/src/hooks/useRemoteServers.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { remoteServersAPI } from '../services/api'
|
||||
|
||||
export interface RemoteServer {
|
||||
uuid: string
|
||||
name: string
|
||||
provider: string
|
||||
host: string
|
||||
port: number
|
||||
username?: string
|
||||
enabled: boolean
|
||||
reachable: boolean
|
||||
last_check?: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export function useRemoteServers() {
|
||||
const [servers, setServers] = useState<RemoteServer[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const fetchServers = async (enabledOnly = false) => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
const data = await remoteServersAPI.list(enabledOnly)
|
||||
setServers(data)
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to fetch remote servers')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchServers()
|
||||
}, [])
|
||||
|
||||
const createServer = async (data: Partial<RemoteServer>) => {
|
||||
try {
|
||||
const newServer = await remoteServersAPI.create(data)
|
||||
setServers([...servers, newServer])
|
||||
return newServer
|
||||
} catch (err) {
|
||||
throw new Error(err instanceof Error ? err.message : 'Failed to create remote server')
|
||||
}
|
||||
}
|
||||
|
||||
const updateServer = async (uuid: string, data: Partial<RemoteServer>) => {
|
||||
try {
|
||||
const updatedServer = await remoteServersAPI.update(uuid, data)
|
||||
setServers(servers.map(s => s.uuid === uuid ? updatedServer : s))
|
||||
return updatedServer
|
||||
} catch (err) {
|
||||
throw new Error(err instanceof Error ? err.message : 'Failed to update remote server')
|
||||
}
|
||||
}
|
||||
|
||||
const deleteServer = async (uuid: string) => {
|
||||
try {
|
||||
await remoteServersAPI.delete(uuid)
|
||||
setServers(servers.filter(s => s.uuid !== uuid))
|
||||
} catch (err) {
|
||||
throw new Error(err instanceof Error ? err.message : 'Failed to delete remote server')
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
servers,
|
||||
loading,
|
||||
error,
|
||||
refresh: fetchServers,
|
||||
createServer,
|
||||
updateServer,
|
||||
deleteServer,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user