feat: enhance import handling with overwrite support and detailed conflict resolution
feat: add subroute handler extraction for improved Caddyfile parsing test: add tests for subroute handler extraction functionality fix: update UI to display staging certificate status and improve dashboard metrics docs: clarify staging certificate deletion process in ACME documentation
This commit is contained in:
@@ -49,7 +49,16 @@ export default function CertificateList() {
|
||||
<tr key={cert.id || cert.domain} className="hover:bg-gray-800/50 transition-colors">
|
||||
<td className="px-6 py-4 font-medium text-white">{cert.name || '-'}</td>
|
||||
<td className="px-6 py-4 font-medium text-white">{cert.domain}</td>
|
||||
<td className="px-6 py-4">{cert.issuer}</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{cert.issuer}</span>
|
||||
{cert.issuer?.toLowerCase().includes('staging') && (
|
||||
<span className="px-2 py-0.5 text-xs font-medium bg-yellow-500/10 text-yellow-400 border border-yellow-500/20 rounded">
|
||||
STAGING
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
{new Date(cert.expires_at).toLocaleDateString()}
|
||||
</td>
|
||||
@@ -60,7 +69,7 @@ export default function CertificateList() {
|
||||
{cert.id && (cert.provider === 'custom' || cert.issuer?.includes('staging')) && (
|
||||
<button
|
||||
onClick={() => {
|
||||
const message = cert.provider === 'custom'
|
||||
const message = cert.provider === 'custom'
|
||||
? 'Are you sure you want to delete this certificate?'
|
||||
: 'Delete this staging certificate? It will be regenerated on next request.'
|
||||
if (confirm(message)) {
|
||||
|
||||
@@ -136,7 +136,7 @@ export default function ImportReviewTable({ hosts, conflicts, conflictDetails, e
|
||||
const hasConflict = conflicts.includes(domain)
|
||||
const isExpanded = expandedRows.has(domain)
|
||||
const details = conflictDetails?.[domain]
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr key={`${domain}-${idx}`} className="hover:bg-gray-900/50">
|
||||
@@ -186,7 +186,7 @@ export default function ImportReviewTable({ hosts, conflicts, conflictDetails, e
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
{hasConflict && isExpanded && details && (
|
||||
<tr key={`${domain}-details`} className="bg-gray-900/30">
|
||||
<td colSpan={3} className="px-6 py-4">
|
||||
@@ -298,19 +298,19 @@ export default function ImportReviewTable({ hosts, conflicts, conflictDetails, e
|
||||
}
|
||||
|
||||
function getRecommendation(details: ConflictDetail): string {
|
||||
const hasTargetChange =
|
||||
const hasTargetChange =
|
||||
details.imported.forward_host !== details.existing.forward_host ||
|
||||
details.imported.forward_port !== details.existing.forward_port ||
|
||||
details.imported.forward_scheme !== details.existing.forward_scheme
|
||||
|
||||
const hasConfigChange =
|
||||
|
||||
const hasConfigChange =
|
||||
details.imported.ssl_forced !== details.existing.ssl_forced ||
|
||||
details.imported.websocket !== details.existing.websocket
|
||||
|
||||
if (hasTargetChange) {
|
||||
return 'The imported configuration points to a different backend server. Choose "Replace" if you want to update the target, or "Keep Existing" if the current setup is correct.'
|
||||
}
|
||||
|
||||
|
||||
if (hasConfigChange) {
|
||||
return 'The imported configuration has different SSL or WebSocket settings. Choose "Replace" to update these settings, or "Keep Existing" to maintain current configuration.'
|
||||
}
|
||||
|
||||
@@ -121,4 +121,140 @@ describe('ImportReviewTable', () => {
|
||||
expect(screen.getByRole('combobox')).toBeInTheDocument()
|
||||
expect(screen.queryByText('No conflict')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('expands and collapses conflict details', () => {
|
||||
const conflicts = ['test.example.com']
|
||||
const conflictDetails = {
|
||||
'test.example.com': {
|
||||
existing: {
|
||||
forward_scheme: 'http',
|
||||
forward_host: '192.168.1.1',
|
||||
forward_port: 8080,
|
||||
ssl_forced: true,
|
||||
websocket: true,
|
||||
enabled: true,
|
||||
},
|
||||
imported: {
|
||||
forward_scheme: 'http',
|
||||
forward_host: '192.168.1.2',
|
||||
forward_port: 9090,
|
||||
ssl_forced: false,
|
||||
websocket: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
render(
|
||||
<ImportReviewTable
|
||||
hosts={mockImportPreview.hosts}
|
||||
conflicts={conflicts}
|
||||
conflictDetails={conflictDetails}
|
||||
errors={[]}
|
||||
onCommit={mockOnCommit}
|
||||
onCancel={mockOnCancel}
|
||||
/>
|
||||
)
|
||||
|
||||
// Initially collapsed
|
||||
expect(screen.queryByText('Current Configuration')).not.toBeInTheDocument()
|
||||
|
||||
// Find and click expand button (it's the ▶ button)
|
||||
const expandButton = screen.getByText('▶')
|
||||
fireEvent.click(expandButton)
|
||||
|
||||
// Now should show details
|
||||
expect(screen.getByText('Current Configuration')).toBeInTheDocument()
|
||||
expect(screen.getByText('Imported Configuration')).toBeInTheDocument()
|
||||
expect(screen.getByText('http://192.168.1.1:8080')).toBeInTheDocument()
|
||||
expect(screen.getByText('http://192.168.1.2:9090')).toBeInTheDocument()
|
||||
|
||||
// Click collapse button
|
||||
const collapseButton = screen.getByText('▼')
|
||||
fireEvent.click(collapseButton)
|
||||
|
||||
// Details should be hidden again
|
||||
expect(screen.queryByText('Current Configuration')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows recommendation based on configuration differences', () => {
|
||||
const conflicts = ['test.example.com']
|
||||
const conflictDetails = {
|
||||
'test.example.com': {
|
||||
existing: {
|
||||
forward_scheme: 'http',
|
||||
forward_host: '192.168.1.1',
|
||||
forward_port: 8080,
|
||||
ssl_forced: true,
|
||||
websocket: false,
|
||||
enabled: true,
|
||||
},
|
||||
imported: {
|
||||
forward_scheme: 'http',
|
||||
forward_host: '192.168.1.1',
|
||||
forward_port: 8080,
|
||||
ssl_forced: false,
|
||||
websocket: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
render(
|
||||
<ImportReviewTable
|
||||
hosts={mockImportPreview.hosts}
|
||||
conflicts={conflicts}
|
||||
conflictDetails={conflictDetails}
|
||||
errors={[]}
|
||||
onCommit={mockOnCommit}
|
||||
onCancel={mockOnCancel}
|
||||
/>
|
||||
)
|
||||
|
||||
// Expand to see recommendation
|
||||
const expandButton = screen.getByText('▶')
|
||||
fireEvent.click(expandButton)
|
||||
|
||||
// Should show recommendation about config changes (SSL differs)
|
||||
expect(screen.getByText(/different SSL or WebSocket settings/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('highlights configuration differences', () => {
|
||||
const conflicts = ['test.example.com']
|
||||
const conflictDetails = {
|
||||
'test.example.com': {
|
||||
existing: {
|
||||
forward_scheme: 'http',
|
||||
forward_host: '192.168.1.1',
|
||||
forward_port: 8080,
|
||||
ssl_forced: true,
|
||||
websocket: true,
|
||||
enabled: true,
|
||||
},
|
||||
imported: {
|
||||
forward_scheme: 'https',
|
||||
forward_host: '192.168.1.2',
|
||||
forward_port: 9090,
|
||||
ssl_forced: false,
|
||||
websocket: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
render(
|
||||
<ImportReviewTable
|
||||
hosts={mockImportPreview.hosts}
|
||||
conflicts={conflicts}
|
||||
conflictDetails={conflictDetails}
|
||||
errors={[]}
|
||||
onCommit={mockOnCommit}
|
||||
onCancel={mockOnCancel}
|
||||
/>
|
||||
)
|
||||
|
||||
const expandButton = screen.getByText('▶')
|
||||
fireEvent.click(expandButton)
|
||||
|
||||
// Check for differences being displayed
|
||||
expect(screen.getByText('https://192.168.1.2:9090')).toBeInTheDocument()
|
||||
expect(screen.getByText('http://192.168.1.1:8080')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user