From 947799511ce57a2665827a9c251e3f3b8ba1f908 Mon Sep 17 00:00:00 2001 From: "HomePC\\Kevin" Date: Sun, 13 Sep 2026 16:44:29 +0200 Subject: [PATCH 1/2] fix(ui): align host configuration with global editor --- .../Components/ProxyConfigEditorModal.tsx | 201 ++++-------------- .../Hooks/useProxyConfigEditorLogic.ts | 69 +----- .../Types/proxy-config-editor.types.ts | 12 +- web/src/tests/proxy-hosts-ui.test.tsx | 56 +++++ 4 files changed, 107 insertions(+), 231 deletions(-) diff --git a/web/src/features/Admin/ProxyHostManagement/Components/ProxyConfigEditorModal.tsx b/web/src/features/Admin/ProxyHostManagement/Components/ProxyConfigEditorModal.tsx index 97d0d84..2d49051 100644 --- a/web/src/features/Admin/ProxyHostManagement/Components/ProxyConfigEditorModal.tsx +++ b/web/src/features/Admin/ProxyHostManagement/Components/ProxyConfigEditorModal.tsx @@ -1,101 +1,47 @@ -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) { +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 ? ( <> - - - - {state.preview ? ( + {(['edit', 'active'] as const).map((tab) => ( - ) : null} - + ))} {state.activeTab === 'edit' ? (
{t('admin.proxyHosts.config.settings')} - {FIELDS.map((field) => ( -
+ ) : state.activeTab === 'active' && source ? ( + ) : ( - - )} - {state.previewError ? ( -

- {t(state.previewError, { - defaultValue: t('admin.proxyHosts.config.errors.previewFailed'), - })} +

+ {t('admin.proxyHosts.config.unavailable')}

- ) : null} + )} )}
- {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 +47,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 +79,6 @@ export default function useProxyConfigEditorLogic({ } await invalidate() setDraft(null) - setPreview(null) toast[result.runtimeStatus === 'pending' ? 'warning' : 'success'](result.message) onOpenChange(false) }, @@ -100,21 +86,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 +98,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 +110,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 +117,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 +127,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') + }) }) From 4c954b68d8b81bef5a3cb2273cc14cf57e6d943c Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 14:25:22 +0000 Subject: [PATCH 2/2] docs(web): document proxy config editor and state hook --- .../ProxyHostManagement/Components/ProxyConfigEditorModal.tsx | 1 + .../Admin/ProxyHostManagement/Hooks/useProxyConfigEditorLogic.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/web/src/features/Admin/ProxyHostManagement/Components/ProxyConfigEditorModal.tsx b/web/src/features/Admin/ProxyHostManagement/Components/ProxyConfigEditorModal.tsx index 2d49051..cddc74d 100644 --- a/web/src/features/Admin/ProxyHostManagement/Components/ProxyConfigEditorModal.tsx +++ b/web/src/features/Admin/ProxyHostManagement/Components/ProxyConfigEditorModal.tsx @@ -15,6 +15,7 @@ const FIELDS = [ ['proxySendTimeoutSeconds', 'fieldProxySendTimeoutSeconds', 1, 3600, 'seconds'], ] as const +/** Renders the per-host proxy settings editor and active Caddy configuration. */ export default function ProxyConfigEditorModal(props: ProxyConfigEditorModalProps) { const { t } = useTranslationStore() const { state, handler } = useProxyConfigEditorLogic(props) diff --git a/web/src/features/Admin/ProxyHostManagement/Hooks/useProxyConfigEditorLogic.ts b/web/src/features/Admin/ProxyHostManagement/Hooks/useProxyConfigEditorLogic.ts index 0339aa2..e124609 100644 --- a/web/src/features/Admin/ProxyHostManagement/Hooks/useProxyConfigEditorLogic.ts +++ b/web/src/features/Admin/ProxyHostManagement/Hooks/useProxyConfigEditorLogic.ts @@ -22,6 +22,7 @@ import type { const EMPTY_SETTINGS: ProxyHttpSettings = {} +/** Manages the per-host proxy configuration query, mutations, and local draft state. */ export default function useProxyConfigEditorLogic({ canEdit, onOpenChange,