diff --git a/web/src/features/Admin/ProxyHostManagement/Components/ProxyConfigEditorModal.tsx b/web/src/features/Admin/ProxyHostManagement/Components/ProxyConfigEditorModal.tsx
index 97d0d84..cddc74d 100644
--- a/web/src/features/Admin/ProxyHostManagement/Components/ProxyConfigEditorModal.tsx
+++ b/web/src/features/Admin/ProxyHostManagement/Components/ProxyConfigEditorModal.tsx
@@ -1,101 +1,48 @@
-import { RefreshCw, RotateCcw, Save, Eye } from 'lucide-react'
+import { RotateCcw, Save } from 'lucide-react'
+
import useTranslationStore from '../../../../language/useTranslationStore'
-import { ConfirmDialog } from '../../../../shared/Modal/Components/ConfirmDialog'
import { Modal } from '../../../../shared/Modal'
+import { ConfirmDialog } from '../../../../shared/Modal/Components/ConfirmDialog'
import { uiClassNames } from '../../../../shared/Styles/uiClassNames'
-import type { ProxyHttpSettings } from '../../../../shared/Types/proxy-runtime.types'
+import CaddyConfigCodeBlock from './CaddyConfigCodeBlock'
import useProxyConfigEditorLogic from '../Hooks/useProxyConfigEditorLogic'
import type { ProxyConfigEditorModalProps } from '../Types/proxy-config-editor.types'
-const FIELDS: ReadonlyArray<{
- key: keyof ProxyHttpSettings
- labelKey: string
- min: number
- max: number
- unitKey: string
-}> = [
- {
- key: 'clientMaxBodySizeBytes',
- labelKey: 'fieldClientMaxBodySizeBytes',
- min: 1024,
- max: 1073741824,
- unitKey: 'bytes',
- },
- {
- key: 'proxyConnectTimeoutSeconds',
- labelKey: 'fieldProxyConnectTimeoutSeconds',
- min: 1,
- max: 60,
- unitKey: 'seconds',
- },
- {
- key: 'proxyReadTimeoutSeconds',
- labelKey: 'fieldProxyReadTimeoutSeconds',
- min: 1,
- max: 3600,
- unitKey: 'seconds',
- },
- {
- key: 'proxySendTimeoutSeconds',
- labelKey: 'fieldProxySendTimeoutSeconds',
- min: 1,
- max: 3600,
- unitKey: 'seconds',
- },
-]
-
-function ReadOnlySource({ source }: { readonly source: string | undefined }) {
- const { t } = useTranslationStore()
- return source ? (
-
- {source}
-
- ) : (
- {t('admin.proxyHosts.config.unavailable')}
- )
-}
+const FIELDS = [
+ ['clientMaxBodySizeBytes', 'fieldClientMaxBodySizeBytes', 1024, 1073741824, 'bytes'],
+ ['proxyConnectTimeoutSeconds', 'fieldProxyConnectTimeoutSeconds', 1, 60, 'seconds'],
+ ['proxyReadTimeoutSeconds', 'fieldProxyReadTimeoutSeconds', 1, 3600, 'seconds'],
+ ['proxySendTimeoutSeconds', 'fieldProxySendTimeoutSeconds', 1, 3600, 'seconds'],
+] as const
-export default function ProxyConfigEditorModal({
- canEdit,
- onOpenChange,
- open,
- proxyHost,
-}: ProxyConfigEditorModalProps) {
+/** Renders the per-host proxy settings editor and active Caddy configuration. */
+export default function ProxyConfigEditorModal(props: ProxyConfigEditorModalProps) {
const { t } = useTranslationStore()
- const { handler, state } = useProxyConfigEditorLogic({ canEdit, onOpenChange, open, proxyHost })
- const busy = state.isSaving || state.isResetting || state.isPreviewing || state.isRefreshing
- const source =
- state.activeTab === 'active'
- ? state.data?.active?.config
- : state.activeTab === 'defaults'
- ? state.data?.defaults?.config
- : state.activeTab === 'preview'
- ? state.preview?.config
- : undefined
+ const { state, handler } = useProxyConfigEditorLogic(props)
+ const busy = state.isRefreshing || state.isSaving || state.isResetting
+ const source = state.data?.active?.config
return (
<>
- {canEdit ? (
+ {props.canEdit ? (
<>
{t('admin.proxyHosts.config.resetButton')}
-
- {state.isReloadConfirmationOpen ? (
-
- ) : null}
{state.isResetConfirmationOpen ? (
('edit')
const [draft, setDraft] = useState<{
settings: ProxyHttpSettings
- baseline: ProxyHttpSettings
baseRevision: string
} | null>(null)
- const [preview, setPreview] = useState(null)
- const [previewError, setPreviewError] = useState(null)
const [isResetConfirmationOpen, setResetConfirmationOpen] = useState(false)
- const [isReloadConfirmationOpen, setReloadConfirmationOpen] = useState(false)
const queryKey = proxyHostManagementQueryKeys.hostConfigEditor(proxyHost.id)
const query = useQuery({
queryKey,
@@ -53,22 +48,15 @@ export default function useProxyConfigEditorLogic({
const data = query.data as ProxyHostConfigEditorData | undefined
const settings = draft?.settings ?? data?.settings ?? EMPTY_SETTINGS
const baseRevision = draft?.baseRevision ?? data?.baseRevision ?? null
- const isDirty =
- draft !== null && JSON.stringify(draft.settings) !== JSON.stringify(draft.baseline)
- const clearErrors = useCallback(() => {
- setPreviewError(null)
- }, [])
const setSetting = useCallback(
(key: keyof ProxyHttpSettings, value: number | undefined) => {
if (!canEdit || !data || baseRevision === null) return
const next = { ...settings }
if (value === undefined) delete next[key]
else next[key] = value
- setDraft({ settings: next, baseline: draft?.baseline ?? data.settings, baseRevision })
- setPreview(null)
- clearErrors()
+ setDraft({ settings: next, baseRevision })
},
- [baseRevision, canEdit, clearErrors, data, draft, settings],
+ [baseRevision, canEdit, data, settings],
)
const invalidate = useCallback(async () => {
await Promise.all([
@@ -92,7 +80,6 @@ export default function useProxyConfigEditorLogic({
}
await invalidate()
setDraft(null)
- setPreview(null)
toast[result.runtimeStatus === 'pending' ? 'warning' : 'success'](result.message)
onOpenChange(false)
},
@@ -100,21 +87,6 @@ export default function useProxyConfigEditorLogic({
toast.error('admin.proxyHosts.config.errors.saveFailed')
},
})
- const previewMutation = useMutation({
- mutationFn: (value: ProxyHttpSettings) =>
- previewProxyHostConfigEditorHandler({
- data: { proxyHostId: proxyHost.id, settings: value },
- }),
- onSuccess: (result) => {
- setPreview(result)
- setActiveTab('preview')
- setPreviewError(null)
- },
- onError: () => {
- setPreview(null)
- setPreviewError('admin.proxyHosts.config.errors.previewFailed')
- },
- })
const resetMutation = useMutation({
mutationFn: () =>
resetProxyHostConfigEditorHandler({
@@ -127,7 +99,6 @@ export default function useProxyConfigEditorLogic({
}
await invalidate()
setDraft(null)
- setPreview(null)
setResetConfirmationOpen(false)
toast[result.runtimeStatus === 'pending' ? 'warning' : 'success'](result.message)
onOpenChange(false)
@@ -140,15 +111,6 @@ export default function useProxyConfigEditorLogic({
if (canEdit && baseRevision !== null && !saveMutation.isPending && !resetMutation.isPending)
saveMutation.mutate(settings)
}, [baseRevision, canEdit, resetMutation, saveMutation, settings])
- const previewSettings = useCallback(() => {
- if (
- canEdit &&
- baseRevision !== null &&
- !previewMutation.isPending &&
- !saveMutation.isPending
- )
- previewMutation.mutate(settings)
- }, [baseRevision, canEdit, previewMutation, saveMutation, settings])
const reset = useCallback(() => {
if (
canEdit &&
@@ -156,24 +118,9 @@ export default function useProxyConfigEditorLogic({
!saveMutation.isPending &&
!resetMutation.isPending
) {
- clearErrors()
setResetConfirmationOpen(true)
}
- }, [baseRevision, canEdit, clearErrors, resetMutation, saveMutation])
- const reload = useCallback(async () => {
- const result = await query.refetch()
- if (result.isError || !result.data) {
- toast.error('admin.proxyHosts.config.errors.loadFailed')
- return
- }
- setDraft(null)
- setPreview(null)
- setReloadConfirmationOpen(false)
- }, [query, toast])
- const refresh = useCallback(() => {
- if (isDirty) setReloadConfirmationOpen(true)
- else void reload()
- }, [isDirty, reload])
+ }, [baseRevision, canEdit, resetMutation, saveMutation])
const state: ProxyConfigEditorState = {
activeTab,
settings,
@@ -181,28 +128,19 @@ export default function useProxyConfigEditorLogic({
data,
isError: query.isError,
isLoading: query.isPending,
- isPreviewing: previewMutation.isPending,
isRefreshing: query.isFetching,
- isReloadConfirmationOpen,
isResetConfirmationOpen,
isResetting: resetMutation.isPending,
isSaving: saveMutation.isPending,
- preview,
- previewError,
- isDirty,
}
const handler: ProxyConfigEditorHandlers = {
- confirmReload: reload,
confirmReset: async () => {
await resetMutation.mutateAsync().catch(() => undefined)
},
- preview: previewSettings,
- refresh,
reset,
save,
setActiveTab,
setSetting,
- setReloadConfirmationOpen,
setResetConfirmationOpen,
}
return { state, handler }
diff --git a/web/src/features/Admin/ProxyHostManagement/Types/proxy-config-editor.types.ts b/web/src/features/Admin/ProxyHostManagement/Types/proxy-config-editor.types.ts
index 0754e75..2e2aa52 100644
--- a/web/src/features/Admin/ProxyHostManagement/Types/proxy-config-editor.types.ts
+++ b/web/src/features/Admin/ProxyHostManagement/Types/proxy-config-editor.types.ts
@@ -1,12 +1,11 @@
import type {
- ProxyConfigSource,
ProxyHostConfigEditorData,
ProxyConfigEditorData,
ProxyHttpSettings,
} from '../../../../shared/Types/proxy-runtime.types'
import type { ProxyHostSummary } from '../../../../shared/Types/proxy-hosts.types'
-export type ProxyConfigEditorTab = 'edit' | 'active' | 'defaults' | 'preview'
+export type ProxyConfigEditorTab = 'edit' | 'active'
export interface ProxyConfigEditorModalProps {
readonly proxyHost: ProxyHostSummary
@@ -30,27 +29,18 @@ export interface ProxyConfigEditorState {
readonly data: ProxyHostConfigEditorData | undefined
readonly isError: boolean
readonly isLoading: boolean
- readonly isPreviewing: boolean
readonly isRefreshing: boolean
- readonly isReloadConfirmationOpen: boolean
readonly isResetConfirmationOpen: boolean
readonly isResetting: boolean
readonly isSaving: boolean
- readonly preview: ProxyConfigSource | null
- readonly previewError: string | null
- readonly isDirty: boolean
}
export interface ProxyConfigEditorHandlers {
- readonly confirmReload: () => Promise
readonly confirmReset: () => Promise
- readonly preview: () => void
- readonly refresh: () => void
readonly reset: () => void
readonly save: () => void
readonly setActiveTab: (tab: ProxyConfigEditorTab) => void
readonly setSetting: (key: keyof ProxyHttpSettings, value: number | undefined) => void
- readonly setReloadConfirmationOpen: (open: boolean) => void
readonly setResetConfirmationOpen: (open: boolean) => void
}
diff --git a/web/src/tests/proxy-hosts-ui.test.tsx b/web/src/tests/proxy-hosts-ui.test.tsx
index 9507316..d66fdae 100644
--- a/web/src/tests/proxy-hosts-ui.test.tsx
+++ b/web/src/tests/proxy-hosts-ui.test.tsx
@@ -1449,4 +1449,60 @@ describe('Caddy proxy host configuration editor', () => {
expect(document.body.textContent).not.toContain('Advanced')
expect(document.body.textContent).not.toContain('Nginx')
})
+
+ test('uses the shared formatted syntax block and removes preview-only host actions', async () => {
+ await renderPage([
+ PERMISSIONS.PROXY_HOSTS_VIEW,
+ PERMISSIONS.PROXY_HOSTS_UPDATE,
+ PERMISSIONS.PROXY_HOSTS_APPLY,
+ ])
+ await waitFor(() => getRows().length === 2)
+ await openMenu(getButton('Open actions for app.example.com'))
+ await click(getMenuItem('Config'))
+ await waitFor(() => document.querySelectorAll('input[type="number"]').length === 4)
+
+ const dialog = document.querySelector('[role="dialog"]')!
+ expect(dialog.textContent).not.toContain('Preview')
+ expect(dialog.textContent).not.toContain('Generated defaults')
+ expect(dialog.textContent).not.toContain('Reload')
+
+ await click(getButton('Active config'))
+ const codeBlock = dialog.querySelector('pre[aria-label="Active config"]')
+ expect(codeBlock).not.toBeNull()
+ expect(codeBlock?.textContent).toContain('\n "http"')
+ expect(codeBlock?.querySelector('[data-token="key"]')).not.toBeNull()
+ expect(codeBlock?.querySelector('[data-token="number"]')).not.toBeNull()
+ })
+
+ test('confirms host reset, preserves host identity, and reports success', async () => {
+ await renderPage([
+ PERMISSIONS.PROXY_HOSTS_VIEW,
+ PERMISSIONS.PROXY_HOSTS_UPDATE,
+ PERMISSIONS.PROXY_HOSTS_APPLY,
+ ])
+ await waitFor(() => getRows().length === 2)
+ await openMenu(getButton('Open actions for app.example.com'))
+ await click(getMenuItem('Config'))
+ await waitFor(() => document.querySelector('input[type="number"]') !== null)
+
+ await click(getButton('Restore defaults'))
+ await waitFor(
+ () => document.body.textContent?.includes('Restore this host’s defaults?') ?? false,
+ )
+ expect(resetProxyHostConfigEditorHandlerMock).not.toHaveBeenCalled()
+ await click(getLastButton('Cancel'))
+ await waitFor(() => !document.body.textContent?.includes('Restore this host’s defaults?'))
+ expect(resetProxyHostConfigEditorHandlerMock).not.toHaveBeenCalled()
+
+ await click(getButton('Restore defaults'))
+ await click(getButton('Restore and apply'))
+ await waitFor(() => resetProxyHostConfigEditorHandlerMock.mock.calls.length === 1)
+ expect(resetProxyHostConfigEditorHandlerMock).toHaveBeenCalledWith({
+ data: {
+ proxyHostId: enabledHost.id,
+ baseRevision: editorBaseRevision,
+ },
+ })
+ await waitForToast('success')
+ })
})