YANG QIA 562b29aebb
fix(logger): list log files by application name (#4325)
* fix(logger): list log files by application name

* fix: tips
2024-05-13 11:36:12 +08:00

227 lines
6.8 KiB
TypeScript

/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { useAPIClient, useRequest } from '@nocobase/client';
import React, { useCallback, useMemo } from 'react';
import { Tree, Card, Alert, Typography, Input, Button, theme, Empty } from 'antd';
import type { DataNode } from 'antd/lib/tree';
import { FolderOutlined, FileOutlined } from '@ant-design/icons';
import { useLoggerTranslation } from './locale';
import { useMemoizedFn } from 'ahooks';
const { Paragraph, Text } = Typography;
type Log = string | LogDir;
type LogDir = {
name: string;
files: Log[];
};
const Tips = React.memo(() => {
const { t } = useLoggerTranslation();
return (
<Typography>
<Paragraph>
<Text code>request_*.log</Text> - {t('API request and response logs')}
</Paragraph>
<Paragraph>
<Text code>system_*.log</Text> -{' '}
{t('Application, database, plugins and other system logs, the error level logs will be sent to')}{' '}
<Text code>system_error_*.log</Text>
</Paragraph>
<Paragraph>
<Text code>sql_*.log</Text> - {t('SQL execution logs, printed by Sequelize when the db logging is enabled')}
</Paragraph>
</Typography>
);
});
Tips.displayName = 'Tips';
export const LogsDownloader = React.memo((props) => {
const { token } = theme.useToken();
const { t: lang } = useLoggerTranslation();
const t = useMemoizedFn(lang);
const api = useAPIClient();
const [expandedKeys, setExpandedKeys] = React.useState<React.Key[]>(['0']);
const [searchValue, setSearchValue] = React.useState('');
const [autoExpandParent, setAutoExpandParent] = React.useState(true);
const [checkedKeys, setCheckedKeys] = React.useState<string[]>([]);
const { data } = useRequest(() =>
api
.resource('logger')
.list()
.then((res) => res.data?.data),
);
const data2tree = useCallback(
(data: Log[], parent: string): DataNode[] =>
data.map((log: Log, index: number) => {
const key = `${parent}-${index}`;
if (typeof log === 'string') {
return {
title: log,
key,
icon: <FileOutlined />,
};
}
return {
title: log.name,
key,
icon: <FolderOutlined />,
children: data2tree(log.files, key),
};
}),
[],
);
const defaultTree: DataNode[] = useMemo(() => {
const files = data || [];
return [
{
title: t('All'),
key: '0',
children: data2tree(files as Log[], '0'),
},
];
}, [data, data2tree, t]);
const onExpand = (newExpandedKeys: React.Key[]) => {
setExpandedKeys(newExpandedKeys);
setAutoExpandParent(false);
};
const onSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.target;
const search = (data: DataNode[]) => {
return data.reduce((acc: DataNode[], node: DataNode) => {
if ((node.title as string)?.includes(value)) {
acc.push(node);
}
if (node.children) {
return [...acc, ...search(node.children)];
}
return acc;
}, []);
};
const newExpandedKeys = search(defaultTree).map((node: DataNode) => node.key);
setExpandedKeys(newExpandedKeys);
setSearchValue(value);
setAutoExpandParent(true);
setCheckedKeys([]);
};
const tree = React.useMemo(() => {
if (!searchValue) {
return defaultTree;
}
const match = (data: DataNode[]): DataNode[] => {
const matched = [];
for (const node of data) {
const nodeTitle = node.title as string;
const index = nodeTitle.indexOf(searchValue);
const beforeStr = nodeTitle.substring(0, index);
const afterStr = nodeTitle.substring(index + searchValue.length);
const title =
index > -1 ? (
<span>
{beforeStr}
<span style={{ color: token.colorPrimary }}>{searchValue}</span>
{afterStr}
</span>
) : (
<span>{nodeTitle}</span>
);
if (index > -1) {
matched.push({ ...node, title });
} else if (node.children) {
const children = match(node.children);
if (children.length) {
matched.push({ ...node, title, children });
}
}
}
return matched;
};
return match(defaultTree);
}, [searchValue, defaultTree, token.colorPrimary]);
const Download = () => {
const getValues = (data: DataNode[], parent: string) => {
return data.reduce((acc: string[], node: DataNode) => {
let title = node.title as string;
title = node.key === '0' ? title : `${parent}/${title}`;
if (node.children) {
return [...acc, ...getValues(node.children, node.key === '0' ? '' : title)];
} else if (checkedKeys.includes(node.key as string) && node.key !== '0') {
acc.push(title);
}
return acc;
}, []);
};
const files = getValues(defaultTree, '');
if (!files.length) {
return;
}
api
.request({
url: 'logger:download',
method: 'post',
responseType: 'blob',
data: {
files,
},
})
.then((res) => {
const url = window.URL.createObjectURL(new Blob([res.data], { type: 'application/gzip' }));
const link = document.createElement('a');
link.setAttribute('href', url);
link.setAttribute('download', 'logs.tar.gz');
link.click();
link.remove();
})
.catch((err) => {
console.log(err);
});
};
return (
<Card style={{ minHeight: '700px' }}>
<Alert message={''} description={<Tips />} type="info" showIcon />
<Input.Search style={{ marginTop: 16, width: '450px' }} placeholder={t('Search')} onChange={onSearch} />
<div
style={{
maxHeight: '400px',
width: '450px',
overflow: 'auto',
border: '1px solid',
marginTop: '6px',
marginBottom: '10px',
borderColor: token.colorBorder,
}}
>
{tree.length ? (
<Tree
checkable
showIcon
showLine
checkedKeys={checkedKeys}
expandedKeys={expandedKeys}
autoExpandParent={autoExpandParent}
onExpand={onExpand}
onCheck={(keys: any) => setCheckedKeys(keys)}
treeData={tree}
/>
) : (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
)}
</div>
<Button type="primary" onClick={Download}>
{t('Download')} (.tar.gz)
</Button>
</Card>
);
});
LogsDownloader.displayName = 'LogsDownloader';