mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-03 03:10:50 +08:00
feat(settings): move request logging toggle behind hidden entry
This commit is contained in:
@@ -2,6 +2,8 @@ import { ReactNode, SVGProps, useCallback, useEffect, useLayoutEffect, useRef, u
|
|||||||
import { NavLink, Outlet } from 'react-router-dom';
|
import { NavLink, Outlet } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
|
import { Modal } from '@/components/ui/Modal';
|
||||||
|
import { ToggleSwitch } from '@/components/ui/ToggleSwitch';
|
||||||
import {
|
import {
|
||||||
IconBot,
|
IconBot,
|
||||||
IconChartLine,
|
IconChartLine,
|
||||||
@@ -16,7 +18,7 @@ import {
|
|||||||
} from '@/components/ui/icons';
|
} from '@/components/ui/icons';
|
||||||
import { INLINE_LOGO_JPEG } from '@/assets/logoInline';
|
import { INLINE_LOGO_JPEG } from '@/assets/logoInline';
|
||||||
import { useAuthStore, useConfigStore, useLanguageStore, useNotificationStore, useThemeStore } from '@/stores';
|
import { useAuthStore, useConfigStore, useLanguageStore, useNotificationStore, useThemeStore } from '@/stores';
|
||||||
import { versionApi } from '@/services/api';
|
import { configApi, versionApi } from '@/services/api';
|
||||||
|
|
||||||
const sidebarIcons: Record<string, ReactNode> = {
|
const sidebarIcons: Record<string, ReactNode> = {
|
||||||
dashboard: <IconLayoutDashboard size={18} />,
|
dashboard: <IconLayoutDashboard size={18} />,
|
||||||
@@ -148,6 +150,7 @@ export function MainLayout() {
|
|||||||
const config = useConfigStore((state) => state.config);
|
const config = useConfigStore((state) => state.config);
|
||||||
const fetchConfig = useConfigStore((state) => state.fetchConfig);
|
const fetchConfig = useConfigStore((state) => state.fetchConfig);
|
||||||
const clearCache = useConfigStore((state) => state.clearCache);
|
const clearCache = useConfigStore((state) => state.clearCache);
|
||||||
|
const updateConfigValue = useConfigStore((state) => state.updateConfigValue);
|
||||||
|
|
||||||
const theme = useThemeStore((state) => state.theme);
|
const theme = useThemeStore((state) => state.theme);
|
||||||
const toggleTheme = useThemeStore((state) => state.toggleTheme);
|
const toggleTheme = useThemeStore((state) => state.toggleTheme);
|
||||||
@@ -157,11 +160,20 @@ export function MainLayout() {
|
|||||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||||
const [checkingVersion, setCheckingVersion] = useState(false);
|
const [checkingVersion, setCheckingVersion] = useState(false);
|
||||||
const [brandExpanded, setBrandExpanded] = useState(true);
|
const [brandExpanded, setBrandExpanded] = useState(true);
|
||||||
|
const [requestLogModalOpen, setRequestLogModalOpen] = useState(false);
|
||||||
|
const [requestLogDraft, setRequestLogDraft] = useState(false);
|
||||||
|
const [requestLogTouched, setRequestLogTouched] = useState(false);
|
||||||
|
const [requestLogSaving, setRequestLogSaving] = useState(false);
|
||||||
const brandCollapseTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const brandCollapseTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const headerRef = useRef<HTMLElement | null>(null);
|
const headerRef = useRef<HTMLElement | null>(null);
|
||||||
|
const versionTapCount = useRef(0);
|
||||||
|
const versionTapTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
const fullBrandName = 'CLI Proxy API Management Center';
|
const fullBrandName = 'CLI Proxy API Management Center';
|
||||||
const abbrBrandName = t('title.abbr');
|
const abbrBrandName = t('title.abbr');
|
||||||
|
const requestLogEnabled = config?.requestLog ?? false;
|
||||||
|
const requestLogDirty = requestLogDraft !== requestLogEnabled;
|
||||||
|
const canEditRequestLog = connectionStatus === 'connected' && Boolean(config);
|
||||||
|
|
||||||
// 将顶栏高度写入 CSS 变量,确保侧栏/内容区计算一致,防止滚动时抖动
|
// 将顶栏高度写入 CSS 变量,确保侧栏/内容区计算一致,防止滚动时抖动
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
@@ -203,6 +215,20 @@ export function MainLayout() {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (requestLogModalOpen && !requestLogTouched) {
|
||||||
|
setRequestLogDraft(requestLogEnabled);
|
||||||
|
}
|
||||||
|
}, [requestLogModalOpen, requestLogTouched, requestLogEnabled]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (versionTapTimer.current) {
|
||||||
|
clearTimeout(versionTapTimer.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleBrandClick = useCallback(() => {
|
const handleBrandClick = useCallback(() => {
|
||||||
if (!brandExpanded) {
|
if (!brandExpanded) {
|
||||||
setBrandExpanded(true);
|
setBrandExpanded(true);
|
||||||
@@ -216,6 +242,60 @@ export function MainLayout() {
|
|||||||
}
|
}
|
||||||
}, [brandExpanded]);
|
}, [brandExpanded]);
|
||||||
|
|
||||||
|
const openRequestLogModal = useCallback(() => {
|
||||||
|
setRequestLogTouched(false);
|
||||||
|
setRequestLogDraft(requestLogEnabled);
|
||||||
|
setRequestLogModalOpen(true);
|
||||||
|
}, [requestLogEnabled]);
|
||||||
|
|
||||||
|
const handleRequestLogClose = useCallback(() => {
|
||||||
|
setRequestLogModalOpen(false);
|
||||||
|
setRequestLogTouched(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleVersionTap = useCallback(() => {
|
||||||
|
versionTapCount.current += 1;
|
||||||
|
if (versionTapTimer.current) {
|
||||||
|
clearTimeout(versionTapTimer.current);
|
||||||
|
}
|
||||||
|
versionTapTimer.current = setTimeout(() => {
|
||||||
|
versionTapCount.current = 0;
|
||||||
|
}, 1500);
|
||||||
|
|
||||||
|
if (versionTapCount.current >= 7) {
|
||||||
|
versionTapCount.current = 0;
|
||||||
|
if (versionTapTimer.current) {
|
||||||
|
clearTimeout(versionTapTimer.current);
|
||||||
|
versionTapTimer.current = null;
|
||||||
|
}
|
||||||
|
openRequestLogModal();
|
||||||
|
}
|
||||||
|
}, [openRequestLogModal]);
|
||||||
|
|
||||||
|
const handleRequestLogSave = async () => {
|
||||||
|
if (!canEditRequestLog) return;
|
||||||
|
if (!requestLogDirty) {
|
||||||
|
setRequestLogModalOpen(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const previous = requestLogEnabled;
|
||||||
|
setRequestLogSaving(true);
|
||||||
|
updateConfigValue('request-log', requestLogDraft);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await configApi.updateRequestLog(requestLogDraft);
|
||||||
|
clearCache('request-log');
|
||||||
|
showNotification(t('notification.request_log_updated'), 'success');
|
||||||
|
setRequestLogModalOpen(false);
|
||||||
|
} catch (error: any) {
|
||||||
|
updateConfigValue('request-log', previous);
|
||||||
|
showNotification(`${t('notification.update_failed')}: ${error?.message || ''}`, 'error');
|
||||||
|
} finally {
|
||||||
|
setRequestLogSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchConfig().catch(() => {
|
fetchConfig().catch(() => {
|
||||||
// ignore initial failure; login flow会提示
|
// ignore initial failure; login flow会提示
|
||||||
@@ -369,7 +449,7 @@ export function MainLayout() {
|
|||||||
<span>
|
<span>
|
||||||
{t('footer.api_version')}: {serverVersion || t('system_info.version_unknown')}
|
{t('footer.api_version')}: {serverVersion || t('system_info.version_unknown')}
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span onClick={handleVersionTap}>
|
||||||
{t('footer.version')}: {__APP_VERSION__ || t('system_info.version_unknown')}
|
{t('footer.version')}: {__APP_VERSION__ || t('system_info.version_unknown')}
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
@@ -379,6 +459,40 @@ export function MainLayout() {
|
|||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
open={requestLogModalOpen}
|
||||||
|
onClose={handleRequestLogClose}
|
||||||
|
title={t('basic_settings.request_log_title')}
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button variant="secondary" onClick={handleRequestLogClose} disabled={requestLogSaving}>
|
||||||
|
{t('common.cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleRequestLogSave}
|
||||||
|
loading={requestLogSaving}
|
||||||
|
disabled={!canEditRequestLog || !requestLogDirty}
|
||||||
|
>
|
||||||
|
{t('common.save')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="request-log-modal">
|
||||||
|
<div className="status-badge warning">{t('basic_settings.request_log_warning')}</div>
|
||||||
|
<ToggleSwitch
|
||||||
|
label={t('basic_settings.request_log_enable')}
|
||||||
|
labelPosition="left"
|
||||||
|
checked={requestLogDraft}
|
||||||
|
disabled={!canEditRequestLog || requestLogSaving}
|
||||||
|
onChange={(value) => {
|
||||||
|
setRequestLogDraft(value);
|
||||||
|
setRequestLogTouched(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,15 +5,26 @@ interface ToggleSwitchProps {
|
|||||||
onChange: (value: boolean) => void;
|
onChange: (value: boolean) => void;
|
||||||
label?: ReactNode;
|
label?: ReactNode;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
labelPosition?: 'left' | 'right';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ToggleSwitch({ checked, onChange, label, disabled = false }: ToggleSwitchProps) {
|
export function ToggleSwitch({
|
||||||
|
checked,
|
||||||
|
onChange,
|
||||||
|
label,
|
||||||
|
disabled = false,
|
||||||
|
labelPosition = 'right'
|
||||||
|
}: ToggleSwitchProps) {
|
||||||
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
onChange(event.target.checked);
|
onChange(event.target.checked);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const className = ['switch', labelPosition === 'left' ? 'switch-label-left' : '']
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label className="switch">
|
<label className={className}>
|
||||||
<input type="checkbox" checked={checked} onChange={handleChange} disabled={disabled} />
|
<input type="checkbox" checked={checked} onChange={handleChange} disabled={disabled} />
|
||||||
<span className="track">
|
<span className="track">
|
||||||
<span className="thumb" />
|
<span className="thumb" />
|
||||||
|
|||||||
@@ -133,7 +133,9 @@
|
|||||||
"usage_statistics_enable": "Enable usage statistics",
|
"usage_statistics_enable": "Enable usage statistics",
|
||||||
"logging_title": "Logging",
|
"logging_title": "Logging",
|
||||||
"logging_to_file_enable": "Enable logging to file",
|
"logging_to_file_enable": "Enable logging to file",
|
||||||
|
"request_log_title": "Request Logging",
|
||||||
"request_log_enable": "Enable request logging",
|
"request_log_enable": "Enable request logging",
|
||||||
|
"request_log_warning": "Keep this off unless you need detailed troubleshooting.",
|
||||||
"ws_auth_title": "WebSocket Authentication",
|
"ws_auth_title": "WebSocket Authentication",
|
||||||
"ws_auth_enable": "Require auth for /ws/*"
|
"ws_auth_enable": "Require auth for /ws/*"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -133,7 +133,9 @@
|
|||||||
"usage_statistics_enable": "启用使用统计",
|
"usage_statistics_enable": "启用使用统计",
|
||||||
"logging_title": "日志记录",
|
"logging_title": "日志记录",
|
||||||
"logging_to_file_enable": "启用日志记录到文件",
|
"logging_to_file_enable": "启用日志记录到文件",
|
||||||
|
"request_log_title": "请求日志",
|
||||||
"request_log_enable": "启用请求日志",
|
"request_log_enable": "启用请求日志",
|
||||||
|
"request_log_warning": "仅在需要排查问题时开启,日常请保持关闭。",
|
||||||
"ws_auth_title": "WebSocket 鉴权",
|
"ws_auth_title": "WebSocket 鉴权",
|
||||||
"ws_auth_enable": "启用 /ws/* 鉴权"
|
"ws_auth_enable": "启用 /ws/* 鉴权"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -288,12 +288,6 @@ export function DashboardPage() {
|
|||||||
{config.loggingToFile ? t('common.yes') : t('common.no')}
|
{config.loggingToFile ? t('common.yes') : t('common.no')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.configItem}>
|
|
||||||
<span className={styles.configLabel}>{t('basic_settings.request_log_enable')}</span>
|
|
||||||
<span className={`${styles.configValue} ${config.requestLog ? styles.enabled : styles.disabled}`}>
|
|
||||||
{config.requestLog ? t('common.yes') : t('common.no')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className={styles.configItem}>
|
<div className={styles.configItem}>
|
||||||
<span className={styles.configLabel}>{t('basic_settings.retry_count_label')}</span>
|
<span className={styles.configLabel}>{t('basic_settings.retry_count_label')}</span>
|
||||||
<span className={styles.configValue}>{config.requestRetry ?? 0}</span>
|
<span className={styles.configValue}>{config.requestRetry ?? 0}</span>
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ type PendingKey =
|
|||||||
| 'switchProject'
|
| 'switchProject'
|
||||||
| 'switchPreview'
|
| 'switchPreview'
|
||||||
| 'usage'
|
| 'usage'
|
||||||
| 'requestLog'
|
|
||||||
| 'loggingToFile'
|
| 'loggingToFile'
|
||||||
| 'wsAuth';
|
| 'wsAuth';
|
||||||
|
|
||||||
@@ -70,7 +69,7 @@ export function SettingsPage() {
|
|||||||
|
|
||||||
const toggleSetting = async (
|
const toggleSetting = async (
|
||||||
section: PendingKey,
|
section: PendingKey,
|
||||||
rawKey: 'debug' | 'usage-statistics-enabled' | 'request-log' | 'logging-to-file' | 'ws-auth',
|
rawKey: 'debug' | 'usage-statistics-enabled' | 'logging-to-file' | 'ws-auth',
|
||||||
value: boolean,
|
value: boolean,
|
||||||
updater: (val: boolean) => Promise<any>,
|
updater: (val: boolean) => Promise<any>,
|
||||||
successMessage: string
|
successMessage: string
|
||||||
@@ -81,8 +80,6 @@ export function SettingsPage() {
|
|||||||
return config?.debug ?? false;
|
return config?.debug ?? false;
|
||||||
case 'usage-statistics-enabled':
|
case 'usage-statistics-enabled':
|
||||||
return config?.usageStatisticsEnabled ?? false;
|
return config?.usageStatisticsEnabled ?? false;
|
||||||
case 'request-log':
|
|
||||||
return config?.requestLog ?? false;
|
|
||||||
case 'logging-to-file':
|
case 'logging-to-file':
|
||||||
return config?.loggingToFile ?? false;
|
return config?.loggingToFile ?? false;
|
||||||
case 'ws-auth':
|
case 'ws-auth':
|
||||||
@@ -200,21 +197,6 @@ export function SettingsPage() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ToggleSwitch
|
|
||||||
label={t('basic_settings.request_log_enable')}
|
|
||||||
checked={config?.requestLog ?? false}
|
|
||||||
disabled={disableControls || pending.requestLog || loading}
|
|
||||||
onChange={(value) =>
|
|
||||||
toggleSetting(
|
|
||||||
'requestLog',
|
|
||||||
'request-log',
|
|
||||||
value,
|
|
||||||
configApi.updateRequestLog,
|
|
||||||
t('notification.request_log_updated')
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
label={t('basic_settings.logging_to_file_enable')}
|
label={t('basic_settings.logging_to_file_enable')}
|
||||||
checked={config?.loggingToFile ?? false}
|
checked={config?.loggingToFile ?? false}
|
||||||
|
|||||||
@@ -308,6 +308,12 @@ textarea {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.switch-label-left {
|
||||||
|
.label {
|
||||||
|
order: -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.pill {
|
.pill {
|
||||||
padding: 4px 10px;
|
padding: 4px 10px;
|
||||||
border-radius: $radius-full;
|
border-radius: $radius-full;
|
||||||
@@ -410,6 +416,17 @@ textarea {
|
|||||||
background: var(--bg-primary);
|
background: var(--bg-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.request-log-modal {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: $spacing-md;
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
border: 1px dashed var(--border-color);
|
border: 1px dashed var(--border-color);
|
||||||
border-radius: $radius-lg;
|
border-radius: $radius-lg;
|
||||||
|
|||||||
Reference in New Issue
Block a user