add debuger
This commit is contained in:
@@ -16,6 +16,7 @@
|
|||||||
"@radix-ui/react-popover": "^1.1.14",
|
"@radix-ui/react-popover": "^1.1.14",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@radix-ui/react-switch": "^1.2.5",
|
"@radix-ui/react-switch": "^1.2.5",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
"@radix-ui/react-tooltip": "^1.2.7",
|
"@radix-ui/react-tooltip": "^1.2.7",
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
|
|||||||
102
ui/pnpm-lock.yaml
generated
102
ui/pnpm-lock.yaml
generated
@@ -26,6 +26,9 @@ importers:
|
|||||||
'@radix-ui/react-switch':
|
'@radix-ui/react-switch':
|
||||||
specifier: ^1.2.5
|
specifier: ^1.2.5
|
||||||
version: 1.2.5(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 1.2.5(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
'@radix-ui/react-tabs':
|
||||||
|
specifier: ^1.1.13
|
||||||
|
version: 1.1.13(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
'@radix-ui/react-tooltip':
|
'@radix-ui/react-tooltip':
|
||||||
specifier: ^1.2.7
|
specifier: ^1.2.7
|
||||||
version: 1.2.8(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 1.2.8(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
@@ -517,6 +520,19 @@ packages:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-collection@1.1.7':
|
||||||
|
resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-compose-refs@1.1.2':
|
'@radix-ui/react-compose-refs@1.1.2':
|
||||||
resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==}
|
resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -548,6 +564,15 @@ packages:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-direction@1.1.1':
|
||||||
|
resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-dismissable-layer@1.1.10':
|
'@radix-ui/react-dismissable-layer@1.1.10':
|
||||||
resolution: {integrity: sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==}
|
resolution: {integrity: sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -709,6 +734,19 @@ packages:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-roving-focus@1.1.11':
|
||||||
|
resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-slot@1.2.3':
|
'@radix-ui/react-slot@1.2.3':
|
||||||
resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==}
|
resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -731,6 +769,19 @@ packages:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-tabs@1.1.13':
|
||||||
|
resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-tooltip@1.2.8':
|
'@radix-ui/react-tooltip@1.2.8':
|
||||||
resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==}
|
resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -2333,6 +2384,18 @@ snapshots:
|
|||||||
'@types/react': 19.1.8
|
'@types/react': 19.1.8
|
||||||
'@types/react-dom': 19.1.6(@types/react@19.1.8)
|
'@types/react-dom': 19.1.6(@types/react@19.1.8)
|
||||||
|
|
||||||
|
'@radix-ui/react-collection@1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0)
|
||||||
|
'@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.0)
|
||||||
|
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
'@radix-ui/react-slot': 1.2.3(@types/react@19.1.8)(react@19.1.0)
|
||||||
|
react: 19.1.0
|
||||||
|
react-dom: 19.1.0(react@19.1.0)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.1.8
|
||||||
|
'@types/react-dom': 19.1.6(@types/react@19.1.8)
|
||||||
|
|
||||||
'@radix-ui/react-compose-refs@1.1.2(@types/react@19.1.8)(react@19.1.0)':
|
'@radix-ui/react-compose-refs@1.1.2(@types/react@19.1.8)(react@19.1.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
@@ -2367,6 +2430,12 @@ snapshots:
|
|||||||
'@types/react': 19.1.8
|
'@types/react': 19.1.8
|
||||||
'@types/react-dom': 19.1.6(@types/react@19.1.8)
|
'@types/react-dom': 19.1.6(@types/react@19.1.8)
|
||||||
|
|
||||||
|
'@radix-ui/react-direction@1.1.1(@types/react@19.1.8)(react@19.1.0)':
|
||||||
|
dependencies:
|
||||||
|
react: 19.1.0
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.1.8
|
||||||
|
|
||||||
'@radix-ui/react-dismissable-layer@1.1.10(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
'@radix-ui/react-dismissable-layer@1.1.10(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.2
|
'@radix-ui/primitive': 1.1.2
|
||||||
@@ -2524,6 +2593,23 @@ snapshots:
|
|||||||
'@types/react': 19.1.8
|
'@types/react': 19.1.8
|
||||||
'@types/react-dom': 19.1.6(@types/react@19.1.8)
|
'@types/react-dom': 19.1.6(@types/react@19.1.8)
|
||||||
|
|
||||||
|
'@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/primitive': 1.1.3
|
||||||
|
'@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0)
|
||||||
|
'@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.0)
|
||||||
|
'@radix-ui/react-direction': 1.1.1(@types/react@19.1.8)(react@19.1.0)
|
||||||
|
'@radix-ui/react-id': 1.1.1(@types/react@19.1.8)(react@19.1.0)
|
||||||
|
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.8)(react@19.1.0)
|
||||||
|
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.8)(react@19.1.0)
|
||||||
|
react: 19.1.0
|
||||||
|
react-dom: 19.1.0(react@19.1.0)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.1.8
|
||||||
|
'@types/react-dom': 19.1.6(@types/react@19.1.8)
|
||||||
|
|
||||||
'@radix-ui/react-slot@1.2.3(@types/react@19.1.8)(react@19.1.0)':
|
'@radix-ui/react-slot@1.2.3(@types/react@19.1.8)(react@19.1.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0)
|
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0)
|
||||||
@@ -2546,6 +2632,22 @@ snapshots:
|
|||||||
'@types/react': 19.1.8
|
'@types/react': 19.1.8
|
||||||
'@types/react-dom': 19.1.6(@types/react@19.1.8)
|
'@types/react-dom': 19.1.6(@types/react@19.1.8)
|
||||||
|
|
||||||
|
'@radix-ui/react-tabs@1.1.13(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/primitive': 1.1.3
|
||||||
|
'@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.0)
|
||||||
|
'@radix-ui/react-direction': 1.1.1(@types/react@19.1.8)(react@19.1.0)
|
||||||
|
'@radix-ui/react-id': 1.1.1(@types/react@19.1.8)(react@19.1.0)
|
||||||
|
'@radix-ui/react-presence': 1.1.5(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
'@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.8)(react@19.1.0)
|
||||||
|
react: 19.1.0
|
||||||
|
react-dom: 19.1.0(react@19.1.0)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.1.8
|
||||||
|
'@types/react-dom': 19.1.6(@types/react@19.1.8)
|
||||||
|
|
||||||
'@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
'@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.3
|
'@radix-ui/primitive': 1.1.3
|
||||||
|
|||||||
496
ui/src/components/DebugPage.tsx
Normal file
496
ui/src/components/DebugPage.tsx
Normal file
@@ -0,0 +1,496 @@
|
|||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { ArrowLeft, Send, Copy, Square, History, Maximize } from 'lucide-react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import MonacoEditor from '@monaco-editor/react';
|
||||||
|
import { RequestHistoryDrawer } from './RequestHistoryDrawer';
|
||||||
|
import { requestHistoryDB } from '@/lib/db';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
|
|
||||||
|
export function DebugPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
const [requestData, setRequestData] = useState({
|
||||||
|
url: '',
|
||||||
|
method: 'POST',
|
||||||
|
headers: '{}',
|
||||||
|
body: '{}'
|
||||||
|
});
|
||||||
|
const [responseData, setResponseData] = useState({
|
||||||
|
status: 0,
|
||||||
|
responseTime: 0,
|
||||||
|
body: '',
|
||||||
|
headers: '{}'
|
||||||
|
});
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isHistoryDrawerOpen, setIsHistoryDrawerOpen] = useState(false);
|
||||||
|
const [fullscreenEditor, setFullscreenEditor] = useState<'headers' | 'body' | null>(null);
|
||||||
|
const headersEditorRef = useRef<any>(null);
|
||||||
|
const bodyEditorRef = useRef<any>(null);
|
||||||
|
|
||||||
|
// 切换全屏模式
|
||||||
|
const toggleFullscreen = (editorType: 'headers' | 'body') => {
|
||||||
|
const isEnteringFullscreen = fullscreenEditor !== editorType;
|
||||||
|
setFullscreenEditor(isEnteringFullscreen ? editorType : null);
|
||||||
|
|
||||||
|
// 延迟触发Monaco编辑器的重新布局,等待DOM更新完成
|
||||||
|
setTimeout(() => {
|
||||||
|
if (headersEditorRef.current) {
|
||||||
|
headersEditorRef.current.layout();
|
||||||
|
}
|
||||||
|
if (bodyEditorRef.current) {
|
||||||
|
bodyEditorRef.current.layout();
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 从URL参数中解析日志数据
|
||||||
|
useEffect(() => {
|
||||||
|
const params = new URLSearchParams(location.search);
|
||||||
|
const logDataParam = params.get('logData');
|
||||||
|
|
||||||
|
if (logDataParam) {
|
||||||
|
try {
|
||||||
|
const parsedData = JSON.parse(decodeURIComponent(logDataParam));
|
||||||
|
|
||||||
|
// 解析URL - 支持多种字段名
|
||||||
|
const url = parsedData.url || parsedData.requestUrl || parsedData.endpoint || '';
|
||||||
|
|
||||||
|
// 解析Method - 支持多种字段名和大小写
|
||||||
|
const method = (parsedData.method || parsedData.requestMethod || 'POST').toUpperCase();
|
||||||
|
|
||||||
|
// 解析Headers - 支持多种格式
|
||||||
|
let headers: Record<string, string> = {};
|
||||||
|
if (parsedData.headers) {
|
||||||
|
if (typeof parsedData.headers === 'string') {
|
||||||
|
try {
|
||||||
|
headers = JSON.parse(parsedData.headers);
|
||||||
|
} catch {
|
||||||
|
// 如果是字符串格式,尝试解析为键值对
|
||||||
|
const headerLines = parsedData.headers.split('\n');
|
||||||
|
headerLines.forEach((line: string) => {
|
||||||
|
const [key, ...values] = line.split(':');
|
||||||
|
if (key && values.length > 0) {
|
||||||
|
headers[key.trim()] = values.join(':').trim();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
headers = parsedData.headers;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析Body - 支持多种格式和嵌套结构
|
||||||
|
let body: Record<string, unknown> = {};
|
||||||
|
let bodyData = null;
|
||||||
|
|
||||||
|
// 支持多种字段名和嵌套结构
|
||||||
|
if (parsedData.body) {
|
||||||
|
bodyData = parsedData.body;
|
||||||
|
} else if (parsedData.request && parsedData.request.body) {
|
||||||
|
bodyData = parsedData.request.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bodyData) {
|
||||||
|
if (typeof bodyData === 'string') {
|
||||||
|
try {
|
||||||
|
// 尝试解析为JSON对象
|
||||||
|
const parsed = JSON.parse(bodyData);
|
||||||
|
body = parsed;
|
||||||
|
} catch {
|
||||||
|
// 如果不是JSON,检查是否是纯文本
|
||||||
|
const trimmed = bodyData.trim();
|
||||||
|
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
||||||
|
// 看起来像JSON但解析失败,作为字符串保存
|
||||||
|
body = { raw: bodyData };
|
||||||
|
} else {
|
||||||
|
// 普通文本,直接保存
|
||||||
|
body = { content: bodyData };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (typeof bodyData === 'object') {
|
||||||
|
// 已经是对象,直接使用
|
||||||
|
body = bodyData;
|
||||||
|
} else {
|
||||||
|
// 其他类型,转换为字符串
|
||||||
|
body = { content: String(bodyData) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预填充请求表单
|
||||||
|
setRequestData({
|
||||||
|
url,
|
||||||
|
method,
|
||||||
|
headers: JSON.stringify(headers, null, 2),
|
||||||
|
body: JSON.stringify(body, null, 2)
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Log data parsed successfully:', { url, method, headers, body });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to parse log data:', error);
|
||||||
|
console.error('Raw log data:', logDataParam);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [location.search]);
|
||||||
|
|
||||||
|
// 发送请求
|
||||||
|
const sendRequest = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
const headers = JSON.parse(requestData.headers);
|
||||||
|
const body = JSON.parse(requestData.body);
|
||||||
|
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
const response = await fetch(requestData.url, {
|
||||||
|
method: requestData.method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...headers
|
||||||
|
},
|
||||||
|
body: requestData.method !== 'GET' ? JSON.stringify(body) : undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
const endTime = Date.now();
|
||||||
|
const responseTime = endTime - startTime;
|
||||||
|
|
||||||
|
const responseHeaders: Record<string, string> = {};
|
||||||
|
response.headers.forEach((value, key) => {
|
||||||
|
responseHeaders[key] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseText = await response.text();
|
||||||
|
let responseBody = responseText;
|
||||||
|
|
||||||
|
// 尝试解析JSON响应
|
||||||
|
try {
|
||||||
|
const jsonResponse = JSON.parse(responseText);
|
||||||
|
responseBody = JSON.stringify(jsonResponse, null, 2);
|
||||||
|
} catch {
|
||||||
|
// 如果不是JSON,保持原样
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseHeadersString = JSON.stringify(responseHeaders, null, 2);
|
||||||
|
|
||||||
|
setResponseData({
|
||||||
|
status: response.status,
|
||||||
|
responseTime,
|
||||||
|
body: responseBody,
|
||||||
|
headers: responseHeadersString
|
||||||
|
});
|
||||||
|
|
||||||
|
// 保存到IndexedDB
|
||||||
|
await requestHistoryDB.saveRequest({
|
||||||
|
url: requestData.url,
|
||||||
|
method: requestData.method,
|
||||||
|
headers: requestData.headers,
|
||||||
|
body: requestData.body,
|
||||||
|
status: response.status,
|
||||||
|
responseTime,
|
||||||
|
responseBody,
|
||||||
|
responseHeaders: responseHeadersString
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Request failed:', error);
|
||||||
|
setResponseData({
|
||||||
|
status: 0,
|
||||||
|
responseTime: 0,
|
||||||
|
body: `请求失败: ${error instanceof Error ? error.message : '未知错误'}`,
|
||||||
|
headers: '{}'
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 从历史记录中选择请求
|
||||||
|
const handleSelectRequest = (request: import('@/lib/db').RequestHistoryItem) => {
|
||||||
|
setRequestData({
|
||||||
|
url: request.url,
|
||||||
|
method: request.method,
|
||||||
|
headers: request.headers,
|
||||||
|
body: request.body
|
||||||
|
});
|
||||||
|
|
||||||
|
setResponseData({
|
||||||
|
status: request.status,
|
||||||
|
responseTime: request.responseTime,
|
||||||
|
body: request.responseBody,
|
||||||
|
headers: request.responseHeaders
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 复制cURL命令
|
||||||
|
const copyCurl = () => {
|
||||||
|
try {
|
||||||
|
const headers = JSON.parse(requestData.headers);
|
||||||
|
const body = JSON.parse(requestData.body);
|
||||||
|
|
||||||
|
let curlCommand = `curl -X ${requestData.method} "${requestData.url}"`;
|
||||||
|
|
||||||
|
// 添加headers
|
||||||
|
Object.entries(headers).forEach(([key, value]) => {
|
||||||
|
curlCommand += ` \\\n -H "${key}: ${value}"`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加body
|
||||||
|
if (requestData.method !== 'GET' && Object.keys(body).length > 0) {
|
||||||
|
curlCommand += ` \\\n -d '${JSON.stringify(body)}'`;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(curlCommand);
|
||||||
|
alert('cURL命令已复制到剪贴板');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to copy cURL:', error);
|
||||||
|
alert('复制cURL命令失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-screen bg-gray-50 font-sans">
|
||||||
|
{/* 头部 */}
|
||||||
|
<header className="flex h-16 items-center justify-between border-b bg-white px-6">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Button variant="ghost" size="sm" onClick={() => navigate('/dashboard')}>
|
||||||
|
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||||
|
返回
|
||||||
|
</Button>
|
||||||
|
<h1 className="text-xl font-semibold text-gray-800">HTTP 调试器</h1>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button variant="outline" onClick={() => setIsHistoryDrawerOpen(true)}>
|
||||||
|
<History className="h-4 w-4 mr-2" />
|
||||||
|
历史记录
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" onClick={copyCurl}>
|
||||||
|
<Copy className="h-4 w-4 mr-2" />
|
||||||
|
复制 cURL
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* 主要内容 */}
|
||||||
|
<main className="flex h-[calc(100vh-4rem)] flex-col gap-4 p-4 overflow-hidden">
|
||||||
|
{/* 上部分:请求参数配置 - 上中下布局 */}
|
||||||
|
<div className="h-1/2 flex flex-col gap-4">
|
||||||
|
<div className="bg-white rounded-lg border p-4 flex-1 flex flex-col">
|
||||||
|
<h3 className="font-medium mb-4">请求参数配置</h3>
|
||||||
|
<div className="flex flex-col gap-4 flex-1">
|
||||||
|
{/* 上:Method、URL和发送请求按钮配置 */}
|
||||||
|
<div className="flex gap-4 items-end">
|
||||||
|
<div className="w-32">
|
||||||
|
<label className="block text-sm font-medium mb-1">Method</label>
|
||||||
|
<select
|
||||||
|
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
|
||||||
|
value={requestData.method}
|
||||||
|
onChange={(e) => setRequestData(prev => ({ ...prev, method: e.target.value }))}
|
||||||
|
>
|
||||||
|
<option value="GET">GET</option>
|
||||||
|
<option value="POST">POST</option>
|
||||||
|
<option value="PUT">PUT</option>
|
||||||
|
<option value="DELETE">DELETE</option>
|
||||||
|
<option value="PATCH">PATCH</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<label className="block text-sm font-medium mb-1">URL</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
|
||||||
|
value={requestData.url}
|
||||||
|
onChange={(e) => setRequestData(prev => ({ ...prev, url: e.target.value }))}
|
||||||
|
placeholder="https://api.example.com/endpoint"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant={isLoading ? "destructive" : "default"}
|
||||||
|
onClick={isLoading ? () => {} : sendRequest}
|
||||||
|
disabled={isLoading || !requestData.url.trim()}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<Square className="h-4 w-4 mr-2" />
|
||||||
|
请求中...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Send className="h-4 w-4 mr-2" />
|
||||||
|
发送请求
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Headers和Body配置 - 使用tab布局 */}
|
||||||
|
<div className="flex-1">
|
||||||
|
<Tabs defaultValue="headers" className="h-full flex flex-col">
|
||||||
|
<TabsList className="grid w-full grid-cols-2">
|
||||||
|
<TabsTrigger value="headers">Headers</TabsTrigger>
|
||||||
|
<TabsTrigger value="body">Body</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="headers" className="flex-1 mt-2">
|
||||||
|
<div
|
||||||
|
className={`${fullscreenEditor === 'headers' ? '' : 'h-full'} flex flex-col ${
|
||||||
|
fullscreenEditor === 'headers' ? 'fixed bg-white w-[100vw] h-[100vh] z-[9999] top-0 left-0 p-4' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<label className="block text-sm font-medium">Headers (JSON)</label>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => toggleFullscreen('headers')}
|
||||||
|
>
|
||||||
|
<Maximize className="h-4 w-4 mr-1" />
|
||||||
|
{fullscreenEditor === 'headers' ? '退出全屏' : '全屏'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
id="fullscreen-headers"
|
||||||
|
className={`${fullscreenEditor === 'headers' ? 'h-full' : 'flex-1'} border border-gray-300 rounded-md overflow-hidden relative`}
|
||||||
|
>
|
||||||
|
<MonacoEditor
|
||||||
|
height="100%"
|
||||||
|
language="json"
|
||||||
|
value={requestData.headers}
|
||||||
|
onChange={(value) => setRequestData(prev => ({ ...prev, headers: value || '{}' }))}
|
||||||
|
onMount={(editor) => {
|
||||||
|
headersEditorRef.current = editor;
|
||||||
|
}}
|
||||||
|
options={{
|
||||||
|
minimap: { enabled: fullscreenEditor === 'headers' },
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
fontSize: 14,
|
||||||
|
lineNumbers: 'on',
|
||||||
|
wordWrap: 'on',
|
||||||
|
automaticLayout: true,
|
||||||
|
formatOnPaste: true,
|
||||||
|
formatOnType: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="body" className="flex-1 mt-2">
|
||||||
|
<div
|
||||||
|
className={`${fullscreenEditor === 'body' ? '' : 'h-full'} flex flex-col ${
|
||||||
|
fullscreenEditor === 'body' ? 'fixed bg-white w-[100vw] h-[100vh] z-[9999] top-0 left-0 p-4' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<label className="block text-sm font-medium">Body (JSON)</label>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => toggleFullscreen('body')}
|
||||||
|
>
|
||||||
|
<Maximize className="h-4 w-4 mr-1" />
|
||||||
|
{fullscreenEditor === 'body' ? '退出全屏' : '全屏'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
id="fullscreen-body"
|
||||||
|
className={`${fullscreenEditor === 'body' ? 'h-full' : 'flex-1'} border border-gray-300 rounded-md overflow-hidden relative`}
|
||||||
|
>
|
||||||
|
<MonacoEditor
|
||||||
|
height="100%"
|
||||||
|
language="json"
|
||||||
|
value={requestData.body}
|
||||||
|
onChange={(value) => setRequestData(prev => ({ ...prev, body: value || '{}' }))}
|
||||||
|
onMount={(editor) => {
|
||||||
|
bodyEditorRef.current = editor;
|
||||||
|
}}
|
||||||
|
options={{
|
||||||
|
minimap: { enabled: fullscreenEditor === 'body' },
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
fontSize: 14,
|
||||||
|
lineNumbers: 'on',
|
||||||
|
wordWrap: 'on',
|
||||||
|
automaticLayout: true,
|
||||||
|
formatOnPaste: true,
|
||||||
|
formatOnType: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 下部分:响应信息查看 */}
|
||||||
|
<div className="h-1/2 flex flex-col gap-4">
|
||||||
|
<div className="flex-1 bg-white rounded-lg border p-4 flex flex-col">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="font-medium">响应信息</h3>
|
||||||
|
{responseData.status > 0 && (
|
||||||
|
<div className="flex items-center gap-4 text-sm">
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
状态码: <span className={`font-mono px-2 py-1 rounded ${
|
||||||
|
responseData.status >= 200 && responseData.status < 300
|
||||||
|
? 'bg-green-100 text-green-800'
|
||||||
|
: responseData.status >= 400
|
||||||
|
? 'bg-red-100 text-red-800'
|
||||||
|
: 'bg-yellow-100 text-yellow-800'
|
||||||
|
}`}>
|
||||||
|
{responseData.status}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
响应时间: <span className="font-mono">{responseData.responseTime}ms</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{responseData.body ? (
|
||||||
|
<div className="flex-1">
|
||||||
|
<Tabs defaultValue="body" className="h-full flex flex-col">
|
||||||
|
<TabsList className="grid w-full grid-cols-2">
|
||||||
|
<TabsTrigger value="body">响应体</TabsTrigger>
|
||||||
|
<TabsTrigger value="headers">响应头</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="body" className="flex-1 mt-2">
|
||||||
|
<div className="bg-gray-50 border rounded-md p-3 h-full overflow-auto">
|
||||||
|
<pre className="text-sm whitespace-pre-wrap">
|
||||||
|
{responseData.body}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="headers" className="flex-1 mt-2">
|
||||||
|
<div className="bg-gray-50 border rounded-md p-3 h-full overflow-auto">
|
||||||
|
<pre className="text-sm">
|
||||||
|
{responseData.headers}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex-1 flex items-center justify-center text-gray-500">
|
||||||
|
{isLoading ? '发送请求中...' : '发送请求后将在此显示响应'}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{/* 请求历史抽屉 */}
|
||||||
|
<RequestHistoryDrawer
|
||||||
|
isOpen={isHistoryDrawerOpen}
|
||||||
|
onClose={() => setIsHistoryDrawerOpen(false)}
|
||||||
|
onSelectRequest={handleSelectRequest}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import Editor from '@monaco-editor/react';
|
import Editor from '@monaco-editor/react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { api } from '@/lib/api';
|
import { api } from '@/lib/api';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { X, RefreshCw, Download, Trash2, ArrowLeft, File, Layers } from 'lucide-react';
|
import { X, RefreshCw, Download, Trash2, ArrowLeft, File, Layers, Bug } from 'lucide-react';
|
||||||
|
|
||||||
interface LogViewerProps {
|
interface LogViewerProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -17,6 +18,7 @@ interface LogEntry {
|
|||||||
message: string; // 现在这个字段直接包含原始JSON字符串
|
message: string; // 现在这个字段直接包含原始JSON字符串
|
||||||
source?: string;
|
source?: string;
|
||||||
reqId?: string;
|
reqId?: string;
|
||||||
|
[key: string]: any; // 允许动态属性,如msg、url、body等
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LogFile {
|
interface LogFile {
|
||||||
@@ -50,6 +52,7 @@ interface GroupedLogsResponse {
|
|||||||
|
|
||||||
export function LogViewer({ open, onOpenChange, showToast }: LogViewerProps) {
|
export function LogViewer({ open, onOpenChange, showToast }: LogViewerProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
const [logs, setLogs] = useState<string[]>([]);
|
const [logs, setLogs] = useState<string[]>([]);
|
||||||
const [logFiles, setLogFiles] = useState<LogFile[]>([]);
|
const [logFiles, setLogFiles] = useState<LogFile[]>([]);
|
||||||
const [selectedFile, setSelectedFile] = useState<LogFile | null>(null);
|
const [selectedFile, setSelectedFile] = useState<LogFile | null>(null);
|
||||||
@@ -63,6 +66,7 @@ export function LogViewer({ open, onOpenChange, showToast }: LogViewerProps) {
|
|||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const refreshInterval = useRef<NodeJS.Timeout | null>(null);
|
const refreshInterval = useRef<NodeJS.Timeout | null>(null);
|
||||||
const workerRef = useRef<Worker | null>(null);
|
const workerRef = useRef<Worker | null>(null);
|
||||||
|
const editorRef = useRef<any>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
@@ -507,6 +511,183 @@ export function LogViewer({ open, onOpenChange, showToast }: LogViewerProps) {
|
|||||||
return logs.join('\n');
|
return logs.join('\n');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 解析日志行,获取final request的行号
|
||||||
|
const getFinalRequestLines = () => {
|
||||||
|
const lines: number[] = [];
|
||||||
|
|
||||||
|
if (groupByReqId && groupedLogs && selectedReqId && groupedLogs.groups[selectedReqId]) {
|
||||||
|
// 分组模式下,检查选中的请求日志
|
||||||
|
const requestLogs = groupedLogs.groups[selectedReqId];
|
||||||
|
requestLogs.forEach((log, index) => {
|
||||||
|
try {
|
||||||
|
// @ts-ignore
|
||||||
|
log = JSON.parse(log)
|
||||||
|
// 检查日志的msg字段是否等于"final request"
|
||||||
|
if (log.msg === "final request") {
|
||||||
|
lines.push(index + 1); // 行号从1开始
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 解析失败,跳过
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 非分组模式下,检查原始日志
|
||||||
|
logs.forEach((logLine, index) => {
|
||||||
|
try {
|
||||||
|
const log = JSON.parse(logLine);
|
||||||
|
// 检查日志的msg字段是否等于"final request"
|
||||||
|
if (log.msg === "final request") {
|
||||||
|
lines.push(index + 1); // 行号从1开始
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 解析失败,跳过
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理调试按钮点击
|
||||||
|
const handleDebugClick = (lineNumber: number) => {
|
||||||
|
console.log('handleDebugClick called with lineNumber:', lineNumber);
|
||||||
|
console.log('Current state:', { groupByReqId, selectedReqId, logsLength: logs.length });
|
||||||
|
|
||||||
|
let logData = null;
|
||||||
|
|
||||||
|
if (groupByReqId && groupedLogs && selectedReqId && groupedLogs.groups[selectedReqId]) {
|
||||||
|
// 分组模式下获取日志数据
|
||||||
|
const requestLogs = groupedLogs.groups[selectedReqId];
|
||||||
|
console.log('Group mode - requestLogs length:', requestLogs.length);
|
||||||
|
logData = requestLogs[lineNumber - 1]; // 行号转换为数组索引
|
||||||
|
console.log('Group mode - logData:', logData);
|
||||||
|
} else {
|
||||||
|
// 非分组模式下获取日志数据
|
||||||
|
console.log('Non-group mode - logs length:', logs.length);
|
||||||
|
try {
|
||||||
|
const logLine = logs[lineNumber - 1];
|
||||||
|
console.log('Log line:', logLine);
|
||||||
|
logData = JSON.parse(logLine);
|
||||||
|
console.log('Parsed logData:', logData);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to parse log data:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logData) {
|
||||||
|
console.log('Navigating to debug page with logData:', logData);
|
||||||
|
// 导航到调试页面,并传递日志数据作为URL参数
|
||||||
|
const logDataParam = encodeURIComponent(JSON.stringify(logData));
|
||||||
|
console.log('Encoded logDataParam length:', logDataParam.length);
|
||||||
|
navigate(`/debug?logData=${logDataParam}`);
|
||||||
|
} else {
|
||||||
|
console.error('No log data found for line:', lineNumber);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 配置Monaco Editor
|
||||||
|
const configureEditor = (editor: any) => {
|
||||||
|
editorRef.current = editor;
|
||||||
|
|
||||||
|
// 启用glyph margin
|
||||||
|
editor.updateOptions({
|
||||||
|
glyphMargin: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 存储当前的装饰ID
|
||||||
|
let currentDecorations: string[] = [];
|
||||||
|
|
||||||
|
// 添加glyph margin装饰
|
||||||
|
const updateDecorations = () => {
|
||||||
|
const finalRequestLines = getFinalRequestLines();
|
||||||
|
const decorations = finalRequestLines.map(lineNumber => ({
|
||||||
|
range: {
|
||||||
|
startLineNumber: lineNumber,
|
||||||
|
startColumn: 1,
|
||||||
|
endLineNumber: lineNumber,
|
||||||
|
endColumn: 1
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
glyphMarginClassName: 'debug-button-glyph',
|
||||||
|
glyphMarginHoverMessage: { value: '点击调试此请求' }
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 使用deltaDecorations正确更新装饰,清理旧的装饰
|
||||||
|
currentDecorations = editor.deltaDecorations(currentDecorations, decorations);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始更新装饰
|
||||||
|
updateDecorations();
|
||||||
|
|
||||||
|
// 监听glyph margin点击 - 使用正确的事件监听方式
|
||||||
|
editor.onMouseDown((e: any) => {
|
||||||
|
console.log('Mouse down event:', e.target);
|
||||||
|
console.log('Event details:', {
|
||||||
|
type: e.target.type,
|
||||||
|
hasDetail: !!e.target.detail,
|
||||||
|
glyphMarginLane: e.target.detail?.glyphMarginLane,
|
||||||
|
offsetX: e.target.detail?.offsetX,
|
||||||
|
glyphMarginLeft: e.target.detail?.glyphMarginLeft,
|
||||||
|
glyphMarginWidth: e.target.detail?.glyphMarginWidth
|
||||||
|
});
|
||||||
|
|
||||||
|
// 检查是否点击在glyph margin区域
|
||||||
|
const isGlyphMarginClick = e.target.detail &&
|
||||||
|
e.target.detail.glyphMarginLane !== undefined &&
|
||||||
|
e.target.detail.offsetX !== undefined &&
|
||||||
|
e.target.detail.offsetX <= e.target.detail.glyphMarginLeft + e.target.detail.glyphMarginWidth;
|
||||||
|
|
||||||
|
console.log('Is glyph margin click:', isGlyphMarginClick);
|
||||||
|
|
||||||
|
if (e.target.position && isGlyphMarginClick) {
|
||||||
|
const finalRequestLines = getFinalRequestLines();
|
||||||
|
console.log('Final request lines:', finalRequestLines);
|
||||||
|
console.log('Clicked line number:', e.target.position.lineNumber);
|
||||||
|
if (finalRequestLines.includes(e.target.position.lineNumber)) {
|
||||||
|
console.log('Opening debug page for line:', e.target.position.lineNumber);
|
||||||
|
handleDebugClick(e.target.position.lineNumber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 尝试使用 onGlyphMarginClick 如果可用
|
||||||
|
if (typeof editor.onGlyphMarginClick === 'function') {
|
||||||
|
editor.onGlyphMarginClick((e: any) => {
|
||||||
|
console.log('Glyph margin click event:', e);
|
||||||
|
const finalRequestLines = getFinalRequestLines();
|
||||||
|
if (finalRequestLines.includes(e.target.position.lineNumber)) {
|
||||||
|
console.log('Opening debug page for line (glyph):', e.target.position.lineNumber);
|
||||||
|
handleDebugClick(e.target.position.lineNumber);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加鼠标移动事件来检测悬停在调试按钮上
|
||||||
|
editor.onMouseMove((e: any) => {
|
||||||
|
if (e.target.position && (e.target.type === 4 || e.target.type === 'glyph-margin')) {
|
||||||
|
const finalRequestLines = getFinalRequestLines();
|
||||||
|
if (finalRequestLines.includes(e.target.position.lineNumber)) {
|
||||||
|
// 可以在这里添加悬停效果
|
||||||
|
editor.updateOptions({
|
||||||
|
glyphMargin: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 当日志变化时更新装饰
|
||||||
|
const interval = setInterval(updateDecorations, 1000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(interval);
|
||||||
|
// 清理装饰
|
||||||
|
if (editorRef.current) {
|
||||||
|
editorRef.current.deltaDecorations(currentDecorations, []);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
if (!isVisible && !open) {
|
if (!isVisible && !open) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -668,23 +849,27 @@ export function LogViewer({ open, onOpenChange, showToast }: LogViewerProps) {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
// 显示日志内容
|
// 显示日志内容
|
||||||
<Editor
|
<div className="relative h-full">
|
||||||
height="100%"
|
<Editor
|
||||||
defaultLanguage="json"
|
height="100%"
|
||||||
value={formatLogsForEditor()}
|
defaultLanguage="json"
|
||||||
theme="vs"
|
value={formatLogsForEditor()}
|
||||||
options={{
|
theme="vs"
|
||||||
minimap: { enabled: true },
|
options={{
|
||||||
fontSize: 14,
|
minimap: { enabled: true },
|
||||||
scrollBeyondLastLine: false,
|
fontSize: 14,
|
||||||
automaticLayout: true,
|
scrollBeyondLastLine: false,
|
||||||
wordWrap: 'on',
|
automaticLayout: true,
|
||||||
readOnly: true,
|
wordWrap: 'on',
|
||||||
lineNumbers: 'on',
|
readOnly: true,
|
||||||
folding: true,
|
lineNumbers: 'on',
|
||||||
renderWhitespace: 'all',
|
folding: true,
|
||||||
}}
|
renderWhitespace: 'all',
|
||||||
/>
|
glyphMargin: true,
|
||||||
|
}}
|
||||||
|
onMount={configureEditor}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
169
ui/src/components/RequestHistoryDrawer.tsx
Normal file
169
ui/src/components/RequestHistoryDrawer.tsx
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { History, Trash2, Clock, X } from 'lucide-react';
|
||||||
|
import { requestHistoryDB, type RequestHistoryItem } from '@/lib/db';
|
||||||
|
|
||||||
|
interface RequestHistoryDrawerProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSelectRequest: (request: RequestHistoryItem) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RequestHistoryDrawer({ isOpen, onClose, onSelectRequest }: RequestHistoryDrawerProps) {
|
||||||
|
const [requests, setRequests] = useState<RequestHistoryItem[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
loadRequests();
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
const loadRequests = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const history = await requestHistoryDB.getRequests();
|
||||||
|
setRequests(history);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load request history:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (id: string, event: React.MouseEvent) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
try {
|
||||||
|
await requestHistoryDB.deleteRequest(id);
|
||||||
|
setRequests(prev => prev.filter(req => req.id !== id));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to delete request:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClearAll = async () => {
|
||||||
|
if (window.confirm('确定要清空所有请求历史吗?')) {
|
||||||
|
try {
|
||||||
|
await requestHistoryDB.clearAllRequests();
|
||||||
|
setRequests([]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to clear request history:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTime = (timestamp: string) => {
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
const now = new Date();
|
||||||
|
const diff = now.getTime() - date.getTime();
|
||||||
|
const minutes = Math.floor(diff / 60000);
|
||||||
|
|
||||||
|
if (minutes < 1) return '刚刚';
|
||||||
|
if (minutes < 60) return `${minutes}分钟前`;
|
||||||
|
if (minutes < 1440) return `${Math.floor(minutes / 60)}小时前`;
|
||||||
|
return date.toLocaleDateString();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50">
|
||||||
|
{/* 遮罩层 */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 bg-black bg-opacity-50"
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 抽屉 */}
|
||||||
|
<div className="absolute right-0 top-0 h-full w-96 bg-white shadow-xl flex flex-col">
|
||||||
|
{/* 头部 */}
|
||||||
|
<div className="flex items-center justify-between p-4 border-b">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<History className="h-5 w-5" />
|
||||||
|
<h2 className="text-lg font-semibold">请求历史</h2>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleClearAll}
|
||||||
|
disabled={requests.length === 0}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 mr-1" />
|
||||||
|
清空
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="sm" onClick={onClose}>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 内容 */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-4">
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex items-center justify-center h-32 text-gray-500">
|
||||||
|
加载中...
|
||||||
|
</div>
|
||||||
|
) : requests.length > 0 ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{requests.map((item) => (
|
||||||
|
<div
|
||||||
|
key={item.id}
|
||||||
|
className="p-3 bg-gray-50 rounded-lg border cursor-pointer hover:bg-gray-100 transition-colors"
|
||||||
|
onClick={() => {
|
||||||
|
onSelectRequest(item);
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-mono text-xs bg-gray-200 px-2 py-1 rounded">
|
||||||
|
{item.method}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-medium truncate flex-1">
|
||||||
|
{item.url}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={(e) => handleDelete(item.id, e)}
|
||||||
|
className="h-6 w-6 p-0"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between text-xs text-gray-500">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className={`font-mono px-1 rounded ${
|
||||||
|
item.status >= 200 && item.status < 300
|
||||||
|
? 'bg-green-100 text-green-800'
|
||||||
|
: item.status >= 400
|
||||||
|
? 'bg-red-100 text-red-800'
|
||||||
|
: 'bg-yellow-100 text-yellow-800'
|
||||||
|
}`}>
|
||||||
|
{item.status}
|
||||||
|
</span>
|
||||||
|
<span>{item.responseTime}ms</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Clock className="h-3 w-3" />
|
||||||
|
<span>{formatTime(item.timestamp)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center text-gray-500 py-8">
|
||||||
|
<History className="h-12 w-12 mx-auto mb-4 text-gray-300" />
|
||||||
|
<p>暂无请求历史</p>
|
||||||
|
<p className="text-sm mt-2">发送请求后会在此显示历史记录</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
53
ui/src/components/ui/tabs.tsx
Normal file
53
ui/src/components/ui/tabs.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Tabs = TabsPrimitive.Root
|
||||||
|
|
||||||
|
const TabsList = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.List>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.List
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TabsList.displayName = TabsPrimitive.List.displayName
|
||||||
|
|
||||||
|
const TabsTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
||||||
|
|
||||||
|
const TabsContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TabsContent.displayName = TabsPrimitive.Content.displayName
|
||||||
|
|
||||||
|
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||||
@@ -154,3 +154,24 @@
|
|||||||
scrollbar-color: oklch(0.708 0 0) oklch(0.269 0 0);
|
scrollbar-color: oklch(0.708 0 0) oklch(0.269 0 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Monaco Editor 调试按钮样式 */
|
||||||
|
.debug-button-glyph {
|
||||||
|
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="%23056bfe" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20v-9"/><path d="M14 7a4 4 0 0 1 4 4v3a6 6 0 0 1-12 0v-3a4 4 0 0 1 4-4z"/><path d="M14.12 3.88 16 2"/><path d="M21 21a4 4 0 0 0-3.81-4"/><path d="M21 5a4 4 0 0 1-3.55 3.97"/><path d="M22 13h-4"/><path d="M3 21a4 4 0 0 1 3.81-4"/><path d="M3 5a4 4 0 0 0 3.55 3.97"/><path d="M6 13H2"/><path d="m8 2 1.88 1.88"/><path d="M9 7.13V6a3 3 0 1 1 6 0v1.13"/></svg>') center center no-repeat;
|
||||||
|
background-size: 14px 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.8;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-button-glyph:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保调试按钮在glyph margin中可见 */
|
||||||
|
.monaco-editor .margin-view-overlays .debug-button-glyph {
|
||||||
|
display: block !important;
|
||||||
|
width: 16px !important;
|
||||||
|
height: 16px !important;
|
||||||
|
margin: 2px 0;
|
||||||
|
}
|
||||||
|
|||||||
106
ui/src/lib/db.ts
Normal file
106
ui/src/lib/db.ts
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
export interface RequestHistoryItem {
|
||||||
|
id: string;
|
||||||
|
url: string;
|
||||||
|
method: string;
|
||||||
|
headers: string;
|
||||||
|
body: string;
|
||||||
|
timestamp: string;
|
||||||
|
status: number;
|
||||||
|
responseTime: number;
|
||||||
|
responseBody: string;
|
||||||
|
responseHeaders: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class RequestHistoryDB {
|
||||||
|
private readonly DB_NAME = 'RequestHistoryDB';
|
||||||
|
private readonly STORE_NAME = 'requests';
|
||||||
|
private readonly VERSION = 1;
|
||||||
|
|
||||||
|
async openDB(): Promise<IDBDatabase> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const request = indexedDB.open(this.DB_NAME, this.VERSION);
|
||||||
|
|
||||||
|
request.onerror = () => reject(request.error);
|
||||||
|
request.onsuccess = () => resolve(request.result);
|
||||||
|
|
||||||
|
request.onupgradeneeded = (event) => {
|
||||||
|
const db = (event.target as IDBOpenDBRequest).result;
|
||||||
|
|
||||||
|
if (!db.objectStoreNames.contains(this.STORE_NAME)) {
|
||||||
|
const store = db.createObjectStore(this.STORE_NAME, { keyPath: 'id' });
|
||||||
|
store.createIndex('timestamp', 'timestamp', { unique: false });
|
||||||
|
store.createIndex('url', 'url', { unique: false });
|
||||||
|
store.createIndex('method', 'method', { unique: false });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveRequest(request: Omit<RequestHistoryItem, 'id' | 'timestamp'>): Promise<void> {
|
||||||
|
const db = await this.openDB();
|
||||||
|
const item: RequestHistoryItem = {
|
||||||
|
...request,
|
||||||
|
id: Date.now().toString(),
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = db.transaction([this.STORE_NAME], 'readwrite');
|
||||||
|
const store = transaction.objectStore(this.STORE_NAME);
|
||||||
|
const request = store.add(item);
|
||||||
|
|
||||||
|
request.onsuccess = () => resolve();
|
||||||
|
request.onerror = () => reject(request.error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRequests(limit: number = 50): Promise<RequestHistoryItem[]> {
|
||||||
|
const db = await this.openDB();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = db.transaction([this.STORE_NAME], 'readonly');
|
||||||
|
const store = transaction.objectStore(this.STORE_NAME);
|
||||||
|
const index = store.index('timestamp');
|
||||||
|
const request = index.openCursor(null, 'prev');
|
||||||
|
|
||||||
|
const results: RequestHistoryItem[] = [];
|
||||||
|
|
||||||
|
request.onsuccess = (event) => {
|
||||||
|
const cursor = (event.target as IDBRequest).result;
|
||||||
|
if (cursor && results.length < limit) {
|
||||||
|
results.push(cursor.value);
|
||||||
|
cursor.continue();
|
||||||
|
} else {
|
||||||
|
resolve(results);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onerror = () => reject(request.error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteRequest(id: string): Promise<void> {
|
||||||
|
const db = await this.openDB();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = db.transaction([this.STORE_NAME], 'readwrite');
|
||||||
|
const store = transaction.objectStore(this.STORE_NAME);
|
||||||
|
const request = store.delete(id);
|
||||||
|
|
||||||
|
request.onsuccess = () => resolve();
|
||||||
|
request.onerror = () => reject(request.error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearAllRequests(): Promise<void> {
|
||||||
|
const db = await this.openDB();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = db.transaction([this.STORE_NAME], 'readwrite');
|
||||||
|
const store = transaction.objectStore(this.STORE_NAME);
|
||||||
|
const request = store.clear();
|
||||||
|
|
||||||
|
request.onsuccess = () => resolve();
|
||||||
|
request.onerror = () => reject(request.error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const requestHistoryDB = new RequestHistoryDB();
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { createMemoryRouter, Navigate } from 'react-router-dom';
|
import { createMemoryRouter, Navigate } from 'react-router-dom';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import { Login } from '@/components/Login';
|
import { Login } from '@/components/Login';
|
||||||
|
import { DebugPage } from '@/components/DebugPage';
|
||||||
import ProtectedRoute from '@/components/ProtectedRoute';
|
import ProtectedRoute from '@/components/ProtectedRoute';
|
||||||
import PublicRoute from '@/components/PublicRoute';
|
import PublicRoute from '@/components/PublicRoute';
|
||||||
|
|
||||||
@@ -17,6 +18,10 @@ export const router = createMemoryRouter([
|
|||||||
path: '/dashboard',
|
path: '/dashboard',
|
||||||
element: <ProtectedRoute><App /></ProtectedRoute>,
|
element: <ProtectedRoute><App /></ProtectedRoute>,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/debug',
|
||||||
|
element: <ProtectedRoute><DebugPage /></ProtectedRoute>,
|
||||||
|
},
|
||||||
], {
|
], {
|
||||||
initialEntries: ['/dashboard']
|
initialEntries: ['/dashboard']
|
||||||
});
|
});
|
||||||
@@ -1 +1 @@
|
|||||||
{"root":["./src/app.tsx","./src/i18n.ts","./src/main.tsx","./src/routes.tsx","./src/types.ts","./src/vite-env.d.ts","./src/components/configprovider.tsx","./src/components/jsoneditor.tsx","./src/components/logviewer.tsx","./src/components/login.tsx","./src/components/protectedroute.tsx","./src/components/providerlist.tsx","./src/components/providers.tsx","./src/components/publicroute.tsx","./src/components/router.tsx","./src/components/settingsdialog.tsx","./src/components/statuslineconfigdialog.tsx","./src/components/statuslineimportexport.tsx","./src/components/transformerlist.tsx","./src/components/transformers.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/color-picker.tsx","./src/components/ui/combo-input.tsx","./src/components/ui/combobox.tsx","./src/components/ui/command.tsx","./src/components/ui/dialog.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/multi-combobox.tsx","./src/components/ui/popover.tsx","./src/components/ui/switch.tsx","./src/components/ui/toast.tsx","./src/lib/api.ts","./src/lib/utils.ts","./src/utils/statusline.ts"],"version":"5.8.3"}
|
{"root":["./src/app.tsx","./src/i18n.ts","./src/main.tsx","./src/routes.tsx","./src/types.ts","./src/vite-env.d.ts","./src/components/configprovider.tsx","./src/components/debugpage.tsx","./src/components/jsoneditor.tsx","./src/components/logviewer.tsx","./src/components/login.tsx","./src/components/protectedroute.tsx","./src/components/providerlist.tsx","./src/components/providers.tsx","./src/components/publicroute.tsx","./src/components/requesthistorydrawer.tsx","./src/components/router.tsx","./src/components/settingsdialog.tsx","./src/components/statuslineconfigdialog.tsx","./src/components/statuslineimportexport.tsx","./src/components/transformerlist.tsx","./src/components/transformers.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/color-picker.tsx","./src/components/ui/combo-input.tsx","./src/components/ui/combobox.tsx","./src/components/ui/command.tsx","./src/components/ui/dialog.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/multi-combobox.tsx","./src/components/ui/popover.tsx","./src/components/ui/switch.tsx","./src/components/ui/tabs.tsx","./src/components/ui/toast.tsx","./src/lib/api.ts","./src/lib/db.ts","./src/lib/utils.ts","./src/utils/statusline.ts"],"version":"5.8.3"}
|
||||||
Reference in New Issue
Block a user