mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-03 03:10:50 +08:00
feat: enhance logging functionality with incremental loading, improved error handling, and UI updates for better user experience
This commit is contained in:
@@ -146,7 +146,7 @@
|
|||||||
"excluded_models_label": "排除的模型 (可选):",
|
"excluded_models_label": "排除的模型 (可选):",
|
||||||
"excluded_models_placeholder": "用逗号或换行分隔,例如: gemini-1.5-pro, gemini-1.5-flash",
|
"excluded_models_placeholder": "用逗号或换行分隔,例如: gemini-1.5-pro, gemini-1.5-flash",
|
||||||
"excluded_models_hint": "留空表示不过滤;保存时会自动去重并忽略空白。",
|
"excluded_models_hint": "留空表示不过滤;保存时会自动去重并忽略空白。",
|
||||||
"excluded_models_count": "排除 {count} 个模型",
|
"excluded_models_count": "排除 {{count}} 个模型",
|
||||||
"codex_title": "Codex API 配置",
|
"codex_title": "Codex API 配置",
|
||||||
"codex_add_button": "添加配置",
|
"codex_add_button": "添加配置",
|
||||||
"codex_empty_title": "暂无Codex配置",
|
"codex_empty_title": "暂无Codex配置",
|
||||||
@@ -219,7 +219,7 @@
|
|||||||
"openai_models_search_placeholder": "按名称、别名或描述筛选",
|
"openai_models_search_placeholder": "按名称、别名或描述筛选",
|
||||||
"openai_models_search_empty": "没有匹配的模型,请更换关键字试试。",
|
"openai_models_search_empty": "没有匹配的模型,请更换关键字试试。",
|
||||||
"openai_models_fetch_invalid_url": "请先填写有效的 Base URL",
|
"openai_models_fetch_invalid_url": "请先填写有效的 Base URL",
|
||||||
"openai_models_fetch_added": "已添加 {count} 个新模型",
|
"openai_models_fetch_added": "已添加 {{count}} 个新模型",
|
||||||
"openai_edit_modal_title": "编辑OpenAI兼容提供商",
|
"openai_edit_modal_title": "编辑OpenAI兼容提供商",
|
||||||
"openai_edit_modal_name_label": "提供商名称:",
|
"openai_edit_modal_name_label": "提供商名称:",
|
||||||
"openai_edit_modal_url_label": "Base URL:",
|
"openai_edit_modal_url_label": "Base URL:",
|
||||||
@@ -253,19 +253,19 @@
|
|||||||
"delete_button": "删除",
|
"delete_button": "删除",
|
||||||
"delete_confirm": "确定要删除文件",
|
"delete_confirm": "确定要删除文件",
|
||||||
"delete_all_confirm": "确定要删除所有认证文件吗?此操作不可恢复!",
|
"delete_all_confirm": "确定要删除所有认证文件吗?此操作不可恢复!",
|
||||||
"delete_filtered_confirm": "确定要删除筛选出的 {type} 认证文件吗?此操作不可恢复!",
|
"delete_filtered_confirm": "确定要删除筛选出的 {{type}} 认证文件吗?此操作不可恢复!",
|
||||||
"upload_error_json": "只能上传JSON文件",
|
"upload_error_json": "只能上传JSON文件",
|
||||||
"upload_success": "文件上传成功",
|
"upload_success": "文件上传成功",
|
||||||
"download_success": "文件下载成功",
|
"download_success": "文件下载成功",
|
||||||
"delete_success": "文件删除成功",
|
"delete_success": "文件删除成功",
|
||||||
"delete_all_success": "成功删除",
|
"delete_all_success": "成功删除",
|
||||||
"delete_filtered_success": "成功删除 {count} 个 {type} 认证文件",
|
"delete_filtered_success": "成功删除 {{count}} 个 {{type}} 认证文件",
|
||||||
"delete_filtered_partial": "{type} 认证文件删除完成,成功 {success} 个,失败 {failed} 个",
|
"delete_filtered_partial": "{{type}} 认证文件删除完成,成功 {{success}} 个,失败 {{failed}} 个",
|
||||||
"delete_filtered_none": "当前筛选类型 ({type}) 下没有可删除的认证文件",
|
"delete_filtered_none": "当前筛选类型 ({{type}}) 下没有可删除的认证文件",
|
||||||
"files_count": "个文件",
|
"files_count": "个文件",
|
||||||
"pagination_prev": "上一页",
|
"pagination_prev": "上一页",
|
||||||
"pagination_next": "下一页",
|
"pagination_next": "下一页",
|
||||||
"pagination_info": "第 {current} / {total} 页 · 共 {count} 个文件",
|
"pagination_info": "第 {{current}} / {{total}} 页 · 共 {{count}} 个文件",
|
||||||
"search_label": "搜索配置文件",
|
"search_label": "搜索配置文件",
|
||||||
"search_placeholder": "输入名称、类型或提供方关键字",
|
"search_placeholder": "输入名称、类型或提供方关键字",
|
||||||
"page_size_label": "单页数量",
|
"page_size_label": "单页数量",
|
||||||
@@ -318,7 +318,7 @@
|
|||||||
"description": "按提供商分列展示,点击卡片编辑或删除;支持 * 通配符,范围跟随上方的配置文件过滤标签。",
|
"description": "按提供商分列展示,点击卡片编辑或删除;支持 * 通配符,范围跟随上方的配置文件过滤标签。",
|
||||||
"add": "新增排除",
|
"add": "新增排除",
|
||||||
"add_title": "新增提供商排除列表",
|
"add_title": "新增提供商排除列表",
|
||||||
"edit_title": "编辑 {provider} 的排除列表",
|
"edit_title": "编辑 {{provider}} 的排除列表",
|
||||||
"refresh": "刷新",
|
"refresh": "刷新",
|
||||||
"refreshing": "刷新中...",
|
"refreshing": "刷新中...",
|
||||||
"provider_label": "提供商",
|
"provider_label": "提供商",
|
||||||
@@ -333,19 +333,19 @@
|
|||||||
"save_success": "排除列表已更新",
|
"save_success": "排除列表已更新",
|
||||||
"save_failed": "更新排除列表失败",
|
"save_failed": "更新排除列表失败",
|
||||||
"delete": "删除提供商",
|
"delete": "删除提供商",
|
||||||
"delete_confirm": "确定要删除 {provider} 的排除列表吗?",
|
"delete_confirm": "确定要删除 {{provider}} 的排除列表吗?",
|
||||||
"delete_success": "已删除该提供商的排除列表",
|
"delete_success": "已删除该提供商的排除列表",
|
||||||
"delete_failed": "删除排除列表失败",
|
"delete_failed": "删除排除列表失败",
|
||||||
"deleting": "正在删除...",
|
"deleting": "正在删除...",
|
||||||
"no_models": "未配置排除模型",
|
"no_models": "未配置排除模型",
|
||||||
"model_count": "排除 {count} 个模型",
|
"model_count": "排除 {{count}} 个模型",
|
||||||
"list_empty_all": "暂无任何提供商的排除列表,点击“新增排除”创建。",
|
"list_empty_all": "暂无任何提供商的排除列表,点击“新增排除”创建。",
|
||||||
"list_empty_filtered": "当前筛选下没有排除项,点击“新增排除”添加。",
|
"list_empty_filtered": "当前筛选下没有排除项,点击“新增排除”添加。",
|
||||||
"disconnected": "请先连接服务器以查看排除列表",
|
"disconnected": "请先连接服务器以查看排除列表",
|
||||||
"load_failed": "加载排除列表失败",
|
"load_failed": "加载排除列表失败",
|
||||||
"provider_required": "请先填写提供商名称",
|
"provider_required": "请先填写提供商名称",
|
||||||
"scope_all": "当前范围:全局(显示所有提供商)",
|
"scope_all": "当前范围:全局(显示所有提供商)",
|
||||||
"scope_provider": "当前范围:{provider}"
|
"scope_provider": "当前范围:{{provider}}"
|
||||||
},
|
},
|
||||||
"auth_login": {
|
"auth_login": {
|
||||||
"codex_oauth_title": "Codex OAuth",
|
"codex_oauth_title": "Codex OAuth",
|
||||||
@@ -562,7 +562,7 @@
|
|||||||
"models_loading": "正在加载可用模型...",
|
"models_loading": "正在加载可用模型...",
|
||||||
"models_empty": "未从 /v1/models 获取到模型数据",
|
"models_empty": "未从 /v1/models 获取到模型数据",
|
||||||
"models_error": "获取模型列表失败",
|
"models_error": "获取模型列表失败",
|
||||||
"models_count": "可用模型 {count} 个",
|
"models_count": "可用模型 {{count}} 个",
|
||||||
"version_check_title": "版本检查",
|
"version_check_title": "版本检查",
|
||||||
"version_check_desc": "调用 /latest-version 接口比对服务器版本,提示是否有可用更新。",
|
"version_check_desc": "调用 /latest-version 接口比对服务器版本,提示是否有可用更新。",
|
||||||
"version_current_label": "当前版本",
|
"version_current_label": "当前版本",
|
||||||
@@ -570,7 +570,7 @@
|
|||||||
"version_check_button": "检查更新",
|
"version_check_button": "检查更新",
|
||||||
"version_check_idle": "点击检查更新",
|
"version_check_idle": "点击检查更新",
|
||||||
"version_checking": "正在检查最新版本...",
|
"version_checking": "正在检查最新版本...",
|
||||||
"version_update_available": "有新版本可用:{version}",
|
"version_update_available": "有新版本可用:{{version}}",
|
||||||
"version_is_latest": "当前已是最新版本",
|
"version_is_latest": "当前已是最新版本",
|
||||||
"version_check_error": "检查更新失败",
|
"version_check_error": "检查更新失败",
|
||||||
"version_current_missing": "未获取到服务器版本号,暂无法比对",
|
"version_current_missing": "未获取到服务器版本号,暂无法比对",
|
||||||
@@ -595,7 +595,7 @@
|
|||||||
"gemini_key_deleted": "Gemini密钥删除成功",
|
"gemini_key_deleted": "Gemini密钥删除成功",
|
||||||
"gemini_multi_input_required": "请先输入至少一个Gemini密钥",
|
"gemini_multi_input_required": "请先输入至少一个Gemini密钥",
|
||||||
"gemini_multi_failed": "Gemini密钥批量添加失败",
|
"gemini_multi_failed": "Gemini密钥批量添加失败",
|
||||||
"gemini_multi_summary": "Gemini批量添加完成:成功 {success},跳过 {skipped},失败 {failed}",
|
"gemini_multi_summary": "Gemini批量添加完成:成功 {{success}},跳过 {{skipped}},失败 {{failed}}",
|
||||||
"codex_config_added": "Codex配置添加成功",
|
"codex_config_added": "Codex配置添加成功",
|
||||||
"codex_config_updated": "Codex配置更新成功",
|
"codex_config_updated": "Codex配置更新成功",
|
||||||
"codex_config_deleted": "Codex配置删除成功",
|
"codex_config_deleted": "Codex配置删除成功",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState, useRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Card } from '@/components/ui/Card';
|
import { Card } from '@/components/ui/Card';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
@@ -12,46 +12,78 @@ interface ErrorLogItem {
|
|||||||
modified?: number;
|
modified?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 限制显示的最大日志行数,防止渲染过多导致卡死
|
||||||
|
const MAX_DISPLAY_LINES = 500;
|
||||||
|
|
||||||
export function LogsPage() {
|
export function LogsPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { showNotification } = useNotificationStore();
|
const { showNotification } = useNotificationStore();
|
||||||
const connectionStatus = useAuthStore((state) => state.connectionStatus);
|
const connectionStatus = useAuthStore((state) => state.connectionStatus);
|
||||||
|
|
||||||
const [logs, setLogs] = useState<string>('');
|
const [logLines, setLogLines] = useState<string[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [autoRefresh, setAutoRefresh] = useState(false);
|
const [autoRefresh, setAutoRefresh] = useState(false);
|
||||||
const [intervalId, setIntervalId] = useState<number | null>(null);
|
|
||||||
const [errorLogs, setErrorLogs] = useState<ErrorLogItem[]>([]);
|
const [errorLogs, setErrorLogs] = useState<ErrorLogItem[]>([]);
|
||||||
const [loadingErrors, setLoadingErrors] = useState(false);
|
const [loadingErrors, setLoadingErrors] = useState(false);
|
||||||
|
|
||||||
|
// 保存最新时间戳用于增量获取
|
||||||
|
const latestTimestampRef = useRef<number>(0);
|
||||||
|
|
||||||
const disableControls = connectionStatus !== 'connected';
|
const disableControls = connectionStatus !== 'connected';
|
||||||
|
|
||||||
const loadLogs = async () => {
|
const loadLogs = async (incremental = false) => {
|
||||||
if (connectionStatus !== 'connected') {
|
if (connectionStatus !== 'connected') {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!incremental) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
}
|
||||||
setError('');
|
setError('');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await logsApi.fetchLogs({ limit: 500 });
|
const params = incremental && latestTimestampRef.current > 0
|
||||||
const text = Array.isArray(data) ? data.join('\n') : data?.logs || data || '';
|
? { after: latestTimestampRef.current }
|
||||||
setLogs(text);
|
: {};
|
||||||
|
const data = await logsApi.fetchLogs(params);
|
||||||
|
|
||||||
|
// 更新时间戳
|
||||||
|
if (data['latest-timestamp']) {
|
||||||
|
latestTimestampRef.current = data['latest-timestamp'];
|
||||||
|
}
|
||||||
|
|
||||||
|
const newLines = Array.isArray(data.lines) ? data.lines : [];
|
||||||
|
|
||||||
|
if (incremental && newLines.length > 0) {
|
||||||
|
// 增量更新:追加新日志并限制总行数
|
||||||
|
setLogLines(prev => {
|
||||||
|
const combined = [...prev, ...newLines];
|
||||||
|
return combined.slice(-MAX_DISPLAY_LINES);
|
||||||
|
});
|
||||||
|
} else if (!incremental) {
|
||||||
|
// 全量加载:只取最后 MAX_DISPLAY_LINES 行
|
||||||
|
setLogLines(newLines.slice(-MAX_DISPLAY_LINES));
|
||||||
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Failed to load logs:', err);
|
console.error('Failed to load logs:', err);
|
||||||
|
if (!incremental) {
|
||||||
setError(err?.message || t('logs.load_error'));
|
setError(err?.message || t('logs.load_error'));
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
if (!incremental) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearLogs = async () => {
|
const clearLogs = async () => {
|
||||||
if (!window.confirm(t('logs.clear_confirm'))) return;
|
if (!window.confirm(t('logs.clear_confirm'))) return;
|
||||||
try {
|
try {
|
||||||
await logsApi.clearLogs();
|
await logsApi.clearLogs();
|
||||||
setLogs('');
|
setLogLines([]);
|
||||||
|
latestTimestampRef.current = 0;
|
||||||
showNotification(t('logs.clear_success'), 'success');
|
showNotification(t('logs.clear_success'), 'success');
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
showNotification(`${t('notification.delete_failed')}: ${err?.message || ''}`, 'error');
|
showNotification(`${t('notification.delete_failed')}: ${err?.message || ''}`, 'error');
|
||||||
@@ -59,7 +91,8 @@ export function LogsPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const downloadLogs = () => {
|
const downloadLogs = () => {
|
||||||
const blob = new Blob([logs], { type: 'text/plain' });
|
const text = logLines.join('\n');
|
||||||
|
const blob = new Blob([text], { type: 'text/plain' });
|
||||||
const url = window.URL.createObjectURL(blob);
|
const url = window.URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = url;
|
a.href = url;
|
||||||
@@ -78,13 +111,15 @@ export function LogsPage() {
|
|||||||
setLoadingErrors(true);
|
setLoadingErrors(true);
|
||||||
try {
|
try {
|
||||||
const res = await logsApi.fetchErrorLogs();
|
const res = await logsApi.fetchErrorLogs();
|
||||||
const list: ErrorLogItem[] = Array.isArray(res)
|
// API 返回 { files: [...] }
|
||||||
? res
|
const files = (res as any)?.files;
|
||||||
: Object.entries(res || {}).map(([name, meta]) => ({
|
const list: ErrorLogItem[] = Array.isArray(files)
|
||||||
name,
|
? files.map((f: any) => ({
|
||||||
size: (meta as any)?.size,
|
name: f.name,
|
||||||
modified: (meta as any)?.modified
|
size: f.size,
|
||||||
}));
|
modified: f.modified
|
||||||
|
}))
|
||||||
|
: [];
|
||||||
setErrorLogs(list);
|
setErrorLogs(list);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Failed to load error logs:', err);
|
console.error('Failed to load error logs:', err);
|
||||||
@@ -113,23 +148,25 @@ export function LogsPage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (connectionStatus === 'connected') {
|
if (connectionStatus === 'connected') {
|
||||||
loadLogs();
|
latestTimestampRef.current = 0;
|
||||||
|
loadLogs(false);
|
||||||
loadErrorLogs();
|
loadErrorLogs();
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [connectionStatus]);
|
}, [connectionStatus]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (autoRefresh) {
|
if (!autoRefresh || connectionStatus !== 'connected') {
|
||||||
const id = window.setInterval(loadLogs, 8000);
|
return;
|
||||||
setIntervalId(id);
|
}
|
||||||
|
const id = window.setInterval(() => {
|
||||||
|
loadLogs(true);
|
||||||
|
}, 8000);
|
||||||
return () => window.clearInterval(id);
|
return () => window.clearInterval(id);
|
||||||
}
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
if (intervalId) {
|
}, [autoRefresh, connectionStatus]);
|
||||||
window.clearInterval(intervalId);
|
|
||||||
setIntervalId(null);
|
const logsText = logLines.join('\n');
|
||||||
}
|
|
||||||
}, [autoRefresh]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="stack">
|
<div className="stack">
|
||||||
@@ -137,13 +174,13 @@ export function LogsPage() {
|
|||||||
title={t('logs.title')}
|
title={t('logs.title')}
|
||||||
extra={
|
extra={
|
||||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||||||
<Button variant="secondary" size="sm" onClick={loadLogs} disabled={loading}>
|
<Button variant="secondary" size="sm" onClick={() => loadLogs(false)} disabled={loading}>
|
||||||
{t('logs.refresh_button')}
|
{t('logs.refresh_button')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="secondary" size="sm" onClick={() => setAutoRefresh((v) => !v)}>
|
<Button variant="secondary" size="sm" onClick={() => setAutoRefresh((v) => !v)}>
|
||||||
{t('logs.auto_refresh')}: {autoRefresh ? t('common.yes') : t('common.no')}
|
{t('logs.auto_refresh')}: {autoRefresh ? t('common.yes') : t('common.no')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="secondary" size="sm" onClick={downloadLogs} disabled={!logs}>
|
<Button variant="secondary" size="sm" onClick={downloadLogs} disabled={logLines.length === 0}>
|
||||||
{t('logs.download_button')}
|
{t('logs.download_button')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="danger" size="sm" onClick={clearLogs} disabled={disableControls}>
|
<Button variant="danger" size="sm" onClick={clearLogs} disabled={disableControls}>
|
||||||
@@ -155,8 +192,8 @@ export function LogsPage() {
|
|||||||
{error && <div className="error-box">{error}</div>}
|
{error && <div className="error-box">{error}</div>}
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="hint">{t('logs.loading')}</div>
|
<div className="hint">{t('logs.loading')}</div>
|
||||||
) : logs ? (
|
) : logsText ? (
|
||||||
<pre className="log-viewer">{logs}</pre>
|
<pre className="log-viewer">{logsText}</pre>
|
||||||
) : (
|
) : (
|
||||||
<EmptyState title={t('logs.empty_title')} description={t('logs.empty_desc')} />
|
<EmptyState title={t('logs.empty_title')} description={t('logs.empty_desc')} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -12,10 +12,8 @@ export function SystemPage() {
|
|||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
const { showNotification } = useNotificationStore();
|
const { showNotification } = useNotificationStore();
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
const { config, fetchConfig } = useConfigStore((state) => ({
|
const config = useConfigStore((state) => state.config);
|
||||||
config: state.config,
|
const fetchConfig = useConfigStore((state) => state.fetchConfig);
|
||||||
fetchConfig: state.fetchConfig
|
|
||||||
}));
|
|
||||||
|
|
||||||
const [models, setModels] = useState<ModelInfo[]>([]);
|
const [models, setModels] = useState<ModelInfo[]>([]);
|
||||||
const [loadingModels, setLoadingModels] = useState(false);
|
const [loadingModels, setLoadingModels] = useState(false);
|
||||||
@@ -70,8 +68,7 @@ export function SystemPage() {
|
|||||||
}
|
}
|
||||||
}, [config?.apiKeys]);
|
}, [config?.apiKeys]);
|
||||||
|
|
||||||
const fetchModels = useCallback(
|
const fetchModels = async ({ forceRefreshKeys = false }: { forceRefreshKeys?: boolean } = {}) => {
|
||||||
async ({ forceRefreshKeys = false }: { forceRefreshKeys?: boolean } = {}) => {
|
|
||||||
if (auth.connectionStatus !== 'connected') {
|
if (auth.connectionStatus !== 'connected') {
|
||||||
setModelStatus({
|
setModelStatus({
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
@@ -111,9 +108,7 @@ export function SystemPage() {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoadingModels(false);
|
setLoadingModels(false);
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
[auth.apiBase, auth.connectionStatus, resolveApiKeysForModels, showNotification, t]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchConfig().catch(() => {
|
fetchConfig().catch(() => {
|
||||||
@@ -123,7 +118,8 @@ export function SystemPage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchModels();
|
fetchModels();
|
||||||
}, [fetchModels]);
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [auth.connectionStatus, auth.apiBase]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="stack">
|
<div className="stack">
|
||||||
|
|||||||
@@ -5,12 +5,18 @@
|
|||||||
import { apiClient } from './client';
|
import { apiClient } from './client';
|
||||||
|
|
||||||
export interface LogsQuery {
|
export interface LogsQuery {
|
||||||
after?: string | number;
|
after?: number;
|
||||||
limit?: number;
|
}
|
||||||
|
|
||||||
|
export interface LogsResponse {
|
||||||
|
lines: string[];
|
||||||
|
'line-count': number;
|
||||||
|
'latest-timestamp': number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const logsApi = {
|
export const logsApi = {
|
||||||
fetchLogs: (params: LogsQuery = {}) => apiClient.get('/logs', { params }),
|
fetchLogs: (params: LogsQuery = {}): Promise<LogsResponse> =>
|
||||||
|
apiClient.get('/logs', { params }),
|
||||||
|
|
||||||
clearLogs: () => apiClient.delete('/logs'),
|
clearLogs: () => apiClient.delete('/logs'),
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user