Skip to content

Commit 6626d1b

Browse files
authored
Merge pull request #343 from HuolalaTech/main
v2.3.1
2 parents e2f0fe4 + cc992bd commit 6626d1b

File tree

15 files changed

+76
-157
lines changed

15 files changed

+76
-157
lines changed

.github/assets/dashboard-en.png

51.1 KB
Loading

.github/assets/dashboard.png

35.3 KB
Loading
File renamed without changes.

src/apis/index.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { default as request } from './request';
2+
import demo from './demo.json?url';
23

34
export const getSpyRoom = (group: string = '') => {
45
return request.get<I.SpyRoomList>(`/room/list`, {
@@ -52,3 +53,15 @@ export const requestLogin = (data: { password: string }) => {
5253
data,
5354
});
5455
};
56+
57+
export const requestGetLogFileContent = async (url: string) => {
58+
// for OSpy demo
59+
if (url === 'demo') {
60+
return await (await fetch(demo)).json();
61+
}
62+
// for files not served by PageSpy, like S3 resource
63+
if (!url.includes(request.defaultPrefix)) {
64+
return await (await fetch(url)).json();
65+
}
66+
return request.get<any>(url);
67+
};

src/apis/request.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,14 @@ class ApiRequest {
4949
const qs = new URLSearchParams(removeUndefinedValues(params))
5050
.toString()
5151
.replace(/^./, '?$&');
52-
const url = prefix + path + qs;
52+
let url = prefix + path + qs;
53+
54+
try {
55+
// if path is a complete URL, use it directly
56+
url = new URL(path).toString();
57+
} catch (e) {
58+
// do nothing
59+
}
5360

5461
const headers: Record<string, string> = {};
5562
const token = getAuthToken();

src/components/LogReplayer/index.tsx

Lines changed: 4 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { HarborDataItem, useReplayStore } from '@/store/replay';
88
import { RRWebPlayer } from './RRWebPlayer';
99
import { PluginPanel } from './PluginPanel';
1010
import '@huolala-tech/react-json-view/dist/style.css';
11-
import request from '@/apis/request';
1211
import {
1312
ReactNode,
1413
RefCallback,
@@ -26,14 +25,14 @@ import { ErrorDetailDrawer } from '@/components/ErrorDetailDrawer';
2625
import { Meta } from './Meta';
2726
import { debug } from '@/utils/debug';
2827
import { useTranslation } from 'react-i18next';
28+
import { requestGetLogFileContent } from '@/apis';
2929

3030
interface Props {
3131
url: string;
32-
fileId?: string;
3332
backSlot?: ReactNode;
3433
}
3534

36-
export const LogReplayer = ({ url, fileId, backSlot = null }: Props) => {
35+
export const LogReplayer = ({ url, backSlot = null }: Props) => {
3736
useEffect(() => {
3837
return () => {
3938
if (url.startsWith('blob://')) {
@@ -55,58 +54,9 @@ export const LogReplayer = ({ url, fileId, backSlot = null }: Props) => {
5554
async () => {
5655
let res: any;
5756
try {
58-
let fetchUrl = url;
59-
const headers: Record<string, string> = {};
60-
61-
// 处理URL可能包含的fileId
62-
let extractedFileId = fileId;
63-
if (
64-
!extractedFileId &&
65-
fetchUrl.includes('/api/v1/log/download?fileId=')
66-
) {
67-
try {
68-
const urlObj = new URL(fetchUrl, window.location.origin);
69-
const fileIdParam = urlObj.searchParams.get('fileId');
70-
if (fileIdParam) {
71-
extractedFileId = fileIdParam;
72-
}
73-
} catch (e) {
74-
console.error('URL解析错误:', e);
75-
}
76-
}
77-
78-
// 添加认证令牌到所有API请求
79-
if (
80-
extractedFileId ||
81-
(fetchUrl && fetchUrl.includes(request.defaultPrefix))
82-
) {
83-
const token = localStorage.getItem('page-spy-auth-token');
84-
if (token) {
85-
headers['Authorization'] = `Bearer ${token}`;
86-
} else {
87-
throw new Error(t('auth.login_required') || 'Login required');
88-
}
89-
}
90-
91-
// 请求日志数据
92-
const response = await fetch(fetchUrl, { headers });
93-
94-
// 检查是否成功
95-
if (!response.ok) {
96-
if (response.status === 401) {
97-
throw new Error(t('auth.login_required') || 'Login required');
98-
}
99-
throw new Error(
100-
`${t('replay.fetch-error') || 'Error fetching log'}: ${
101-
response.statusText
102-
}`,
103-
);
104-
}
105-
106-
// 解析JSON数据
107-
res = await response.json();
57+
res = await requestGetLogFileContent(url);
10858
} catch (e: any) {
109-
throw new Error(e.message || t('replay.invalid-source'));
59+
throw new Error(t('replay.invalid-source')!);
11060
}
11161
// source not found, for example, the file be cleared in the server
11262
if (res?.success === false) {

src/pages/LogList/index.tsx

Lines changed: 16 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { deleteSpyLog, getSpyLogs } from '@/apis';
1+
import { deleteSpyLog, getSpyLogs, requestGetLogFileContent } from '@/apis';
22
import { useRequest } from 'ahooks';
33
import {
44
Typography,
@@ -29,7 +29,6 @@ import {
2929
ClearOutlined,
3030
ClockCircleOutlined,
3131
CloseCircleOutlined,
32-
CopyOutlined,
3332
DeleteOutlined,
3433
DownloadOutlined,
3534
InfoCircleOutlined,
@@ -419,10 +418,7 @@ const LogList = () => {
419418
return (
420419
<Space size="small">
421420
<Link
422-
to={{
423-
pathname: '/replay',
424-
search: `?fileId=${row.fileId}`,
425-
}}
421+
to={{ pathname: '/replay', search: `?url=${logUrl}` }}
426422
target="_blank"
427423
>
428424
<Button
@@ -438,59 +434,21 @@ const LogList = () => {
438434
type="text"
439435
size="small"
440436
icon={<DownloadOutlined />}
441-
onClick={() => {
442-
// 使用认证令牌构建下载链接
443-
const token = localStorage.getItem(
444-
'page-spy-auth-token',
445-
);
446-
if (!token) {
447-
message.error(t('auth.login_required'));
448-
return;
449-
}
450-
451-
// 创建一个临时的a元素来处理下载(带Authorization头)
452-
const xhr = new XMLHttpRequest();
453-
xhr.open('GET', logUrl, true);
454-
xhr.setRequestHeader(
455-
'Authorization',
456-
`Bearer ${token}`,
457-
);
458-
xhr.responseType = 'blob';
459-
460-
xhr.onload = function () {
461-
if (xhr.status === 200) {
462-
const blob = xhr.response;
463-
const url = window.URL.createObjectURL(blob);
464-
const a = document.createElement('a');
465-
a.style.display = 'none';
466-
a.href = url;
467-
// 从服务器响应中获取文件名或使用默认文件名
468-
const contentDisposition = xhr.getResponseHeader(
469-
'Content-Disposition',
470-
);
471-
let filename = 'log-file.json';
472-
if (contentDisposition) {
473-
const filenameMatch =
474-
contentDisposition.match(/filename=(.+)/);
475-
if (filenameMatch && filenameMatch[1]) {
476-
filename = filenameMatch[1];
477-
}
478-
}
479-
a.download = filename;
480-
document.body.appendChild(a);
481-
a.click();
482-
window.URL.revokeObjectURL(url);
483-
document.body.removeChild(a);
484-
} else {
485-
message.error(t('common.download_failed'));
486-
}
487-
};
488-
489-
xhr.onerror = function () {
490-
message.error(t('common.download_failed'));
491-
};
437+
onClick={async () => {
438+
const json = await requestGetLogFileContent(logUrl);
439+
const blob = new Blob([JSON.stringify(json)], {
440+
type: 'application/json',
441+
});
442+
const url = URL.createObjectURL(blob);
492443

493-
xhr.send();
444+
const a = document.createElement('a');
445+
a.style.display = 'none';
446+
a.href = url;
447+
a.download = row.name;
448+
document.body.appendChild(a);
449+
a.click();
450+
URL.revokeObjectURL(url);
451+
document.body.removeChild(a);
494452
}}
495453
>
496454
{t('replay.download-file')}

src/pages/MainDocs/md/deploy-guide.en.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ To ensure data security and ease of use, PageSpy offers a variety of complete, o
1212

1313
- [Deploy with Node](./deploy-with-node)
1414
- [Deploy with Docker](./deploy-with-docker)
15-
- [Install on Baota](./deploy-with-bt)
15+
- [Install on Baota](./deploy-with-baota)

src/pages/MainDocs/md/deploy-guide.ja.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ import ApiImg from '@/assets/image/screenshot/page-spy-api.png';
1212

1313
- [Node.js でデプロイ](./deploy-with-node)
1414
- [Docker でデプロイ](./deploy-with-docker)
15-
- [宝塔パネルにインストール](./deploy-with-bt)
15+
- [宝塔パネルにインストール](./deploy-with-baota)

src/pages/MainDocs/md/deploy-guide.ko.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ import ApiImg from '@/assets/image/screenshot/page-spy-api.png';
1212

1313
- [Node.js로 배포](./deploy-with-node)
1414
- [Docker로 배포](./deploy-with-docker)
15-
- [바오타 패널에 설치](./deploy-with-bt)
15+
- [바오타 패널에 설치](./deploy-with-baota)

src/pages/MainDocs/md/deploy-guide.zh.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ import ApiImg from '@/assets/image/screenshot/page-spy-api.png';
1212

1313
- [使用 Node 部署](./deploy-with-node)
1414
- [使用 Docker 部署](./deploy-with-docker)
15-
- [在宝塔面板安装](./deploy-with-bt)
15+
- [在宝塔面板安装](./deploy-with-baota)

src/pages/OSpy/components/Replayer/index.tsx

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,15 @@ import { Button, Flex, Space } from 'antd';
55
import { useTranslation } from 'react-i18next';
66
import { ArrowLeftOutlined } from '@ant-design/icons';
77
import { useSize } from 'ahooks';
8-
import { Link, useLocation, useNavigate } from 'react-router-dom';
9-
import demo from './demo.json?url';
8+
import { Link, useNavigate } from 'react-router-dom';
109
import { SelectLogButton } from '@/components/SelectLogButton';
10+
import { useUrlParam } from '@/utils/useUrlParam';
1111

1212
export const Replayer = () => {
1313
const { t } = useTranslation();
1414
const size = useSize(document.body);
1515
const navigate = useNavigate();
16-
const { search } = useLocation();
17-
18-
const replayUrl = useMemo(() => {
19-
const url = new URLSearchParams(search).get('url');
20-
if (url === 'demo') return demo;
21-
return url || '';
22-
}, [search]);
16+
const replayUrl = useUrlParam();
2317

2418
const backSlot = useMemo(() => {
2519
return (

src/pages/Replay/index.tsx

Lines changed: 3 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,36 +6,13 @@ import { Space, Button, message } from 'antd';
66
import { useMemo } from 'react';
77
import { Link, useNavigate } from 'react-router-dom';
88
import { useTranslation } from 'react-i18next';
9-
import request from '@/apis/request';
9+
import { useUrlParam } from '@/utils/useUrlParam';
1010

1111
const Replay = () => {
12-
const { url, fileId } = useSearch();
12+
const url = useUrlParam();
1313
const { t } = useTranslation();
1414
const navigate = useNavigate();
1515

16-
// 处理日志下载URL
17-
const logUrl = useMemo(() => {
18-
if (url) {
19-
// 如果url是API请求,提取fileId并使用更安全的方式
20-
if (url.includes('/api/v1/log/download?fileId=')) {
21-
try {
22-
const urlObj = new URL(url, window.location.origin);
23-
const extractedFileId = urlObj.searchParams.get('fileId');
24-
if (extractedFileId) {
25-
return `${request.defaultPrefix}/log/download?fileId=${extractedFileId}`;
26-
}
27-
} catch (e) {
28-
console.error('URL解析错误:', e);
29-
}
30-
}
31-
return url;
32-
}
33-
if (fileId) {
34-
return `${request.defaultPrefix}/log/download?fileId=${fileId}`;
35-
}
36-
return '';
37-
}, [url, fileId]);
38-
3916
const backSlot = useMemo(() => {
4017
return (
4118
<Space>
@@ -51,7 +28,7 @@ const Replay = () => {
5128
);
5229
}, [navigate, t]);
5330

54-
return <LogReplayer url={logUrl} fileId={fileId} backSlot={backSlot} />;
31+
return <LogReplayer url={url} backSlot={backSlot} />;
5532
};
5633

5734
export default Replay;

src/utils/AuthContext.tsx

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { useTranslation } from 'react-i18next';
1111
import { requestAuthStatus, requestLogin } from '@/apis';
1212
import { AUTH_FAILED_EVENT, TOKEN_KEY } from '@/apis/request';
1313
import { useEventListener } from './useEventListener';
14+
import { isClient, isDoc } from './constants';
1415

1516
// 认证上下文定义
1617
interface AuthContextType {
@@ -26,10 +27,15 @@ export const AuthContext = createContext<AuthContextType>(
2627

2728
export const AuthProvider = ({ children }: PropsWithChildren<unknown>) => {
2829
const { t } = useTranslation();
29-
const [loading, setLoading] = useState<boolean>(true);
30+
const [loading, setLoading] = useState<boolean>(false);
3031

31-
// 为 true 时存在两种可能:1. 无需密码;2. 认证通过
32-
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
32+
// 为 true 时存在几种可能
33+
// 1. 无需密码
34+
// 2. 认证通过
35+
// 3. 构建文档 (yarn build:doc)
36+
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(
37+
isDoc ? true : false,
38+
);
3339
useEventListener(AUTH_FAILED_EVENT, () => setIsAuthenticated(false));
3440

3541
const setToken = (token: string) => {
@@ -81,8 +87,10 @@ export const AuthProvider = ({ children }: PropsWithChildren<unknown>) => {
8187
};
8288

8389
// 初始化时验证令牌
84-
useEffect(() => {
85-
checkStatus();
90+
useMemo(() => {
91+
if (isClient) {
92+
checkStatus();
93+
}
8694
}, []);
8795

8896
const contextValue = useMemo<AuthContextType>(

src/utils/useUrlParam.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { useMemo } from 'react';
2+
import { useLocation } from 'react-router-dom';
3+
4+
export const useUrlParam = () => {
5+
// DON'T USE useSearch OR URLSearchParams, it decodes the url value automatically
6+
const { search } = useLocation();
7+
const replayUrl = useMemo(() => {
8+
const url = search.split('?url=')?.[1];
9+
return url || '';
10+
}, [search]);
11+
return replayUrl;
12+
};

0 commit comments

Comments
 (0)