From 743a6934ca89269c0a8136f57e96937120359c15 Mon Sep 17 00:00:00 2001 From: Nut He <18328704+hetao92@users.noreply.github.com> Date: Thu, 24 Feb 2022 19:37:36 +0800 Subject: [PATCH] feat: init task log feat: add log read mod: change key mod: code review --- app/app.less | 1 + app/config/locale/en-US.json | 3 +- app/config/locale/zh-CN.json | 3 +- app/config/service.ts | 14 ++ app/interfaces/import.ts | 9 +- app/pages/Console/OutputBox/index.tsx | 2 - app/pages/Console/index.tsx | 2 +- .../SchemaConfig/TagConfig/index.tsx | 4 +- app/pages/Import/TaskCreate/index.tsx | 4 +- .../TaskList/TaskItem/LogModal/index.less | 62 +++++++++ .../TaskList/TaskItem/LogModal/index.tsx | 131 ++++++++++++++++++ app/pages/Import/TaskList/TaskItem/index.tsx | 15 +- app/pages/Import/TaskList/index.tsx | 52 ++++++- app/stores/import.ts | 42 +++++- 14 files changed, 313 insertions(+), 31 deletions(-) create mode 100644 app/pages/Import/TaskList/TaskItem/LogModal/index.less create mode 100644 app/pages/Import/TaskList/TaskItem/LogModal/index.tsx diff --git a/app/app.less b/app/app.less index 40982178..09b557b7 100644 --- a/app/app.less +++ b/app/app.less @@ -56,6 +56,7 @@ border: none; white-space: nowrap; color: @darkBlue; + &.ant-radio-button-wrapper-checked { color: #fff; } diff --git a/app/config/locale/en-US.json b/app/config/locale/en-US.json index f77ebfe4..9415e904 100644 --- a/app/config/locale/en-US.json +++ b/app/config/locale/en-US.json @@ -247,9 +247,10 @@ "createTask": "New Import", "uploadTemp": "Import Template", "downloadConfig": "Download Config", + "downloadLog": "Download Log", "viewLogs": "View Logs", "details": "Details", - "lines": "Lines", + "task": "import task", "taskList": "Task List", "taskName": "Task Name", "vertices": "Map Vertices", diff --git a/app/config/locale/zh-CN.json b/app/config/locale/zh-CN.json index aef52601..47d5cbcc 100644 --- a/app/config/locale/zh-CN.json +++ b/app/config/locale/zh-CN.json @@ -247,9 +247,10 @@ "createTask": "创建导入任务", "uploadTemp": "导入模板", "downloadConfig": "下载配置文件", + "downloadLog": "下载日志", "viewLogs": "查看日志", "details": "详情", - "lines": "行", + "task": "导入任务", "taskList": "任务列表", "taskName": "任务名称", "vertices": "关联点", diff --git a/app/config/service.ts b/app/config/service.ts index efb50873..63ec07a1 100644 --- a/app/config/service.ts +++ b/app/config/service.ts @@ -13,6 +13,7 @@ const importData = post('/api-nebula/task/import'); const handleImportAction = post('/api-nebula/task/import/action'); const getLog = get('/api/import/log'); +const getErrLog = get('/api/import/err_log'); const finishImport = post('/api/import/finish'); const getImportWokingDir = get('/api/import/working_dir'); @@ -31,6 +32,15 @@ const uploadFiles = (params?, config?) => 'Content-Type': 'multipart/form-data', }, }); + + +const getTaskLogs = (params?, config?) => { + const { id, ...others } = params; + return get(`/api/import/task_log_paths/${id}`)(others, config); +}; + +const getTaskConfigUrl = (id: number) => `/api-nebula/task/import/config/${id}`; +const getTaskLogUrl = (path: string) => `/api-nebula/task/import/log?pathName=${encodeURI(path)}`; export default { execNGQL, batchExecNGQL, @@ -40,10 +50,14 @@ export default { finishImport, handleImportAction, getLog, + getErrLog, getImportWokingDir, getUploadDir, getTaskDir, deteleFile, getFiles, uploadFiles, + getTaskConfigUrl, + getTaskLogs, + getTaskLogUrl }; diff --git a/app/interfaces/import.ts b/app/interfaces/import.ts index 30e4f69e..61391df1 100644 --- a/app/interfaces/import.ts +++ b/app/interfaces/import.ts @@ -8,8 +8,11 @@ export enum ITaskStatus { } export interface ITaskStats { - totalLine: number; - totalCount: number; + totalBatches: number; + totalBytes: number; + totalImportedBytes: number; + totalLatency: number; + totalReqTime: number; numFailed: number; numReadFailed: number; } @@ -23,7 +26,7 @@ export interface ITaskItem { user: string; taskStatus: ITaskStatus; taskMessage: string; - statsQuery: ITaskStats; + stats: ITaskStats; } export interface IVerticesConfig { diff --git a/app/pages/Console/OutputBox/index.tsx b/app/pages/Console/OutputBox/index.tsx index ee48d2b9..f7e65d28 100644 --- a/app/pages/Console/OutputBox/index.tsx +++ b/app/pages/Console/OutputBox/index.tsx @@ -169,9 +169,7 @@ const OutputBox = (props: IProps) => { const link = document.createElement('a'); link.href = url; link.download = `result.csv`; - document.body.appendChild(link); link.click(); - document.body.removeChild(link); }; const handleExplore = () => { diff --git a/app/pages/Console/index.tsx b/app/pages/Console/index.tsx index 6b351bab..9ef50897 100644 --- a/app/pages/Console/index.tsx +++ b/app/pages/Console/index.tsx @@ -2,7 +2,7 @@ import { Button, Select, Tooltip, message } from 'antd'; import React, { useEffect, useRef, useState } from 'react'; import intl from 'react-intl-universal'; import { observer } from 'mobx-react-lite'; -import { trackPageView, trackEvent } from '@app/utils/stat'; +import { trackEvent, trackPageView } from '@app/utils/stat'; import { useStore } from '@app/stores'; import Instruction from '@app/components/Instruction'; import Icon from '@app/components/Icon'; diff --git a/app/pages/Import/TaskCreate/SchemaConfig/TagConfig/index.tsx b/app/pages/Import/TaskCreate/SchemaConfig/TagConfig/index.tsx index f0d4c7d2..16eade44 100644 --- a/app/pages/Import/TaskCreate/SchemaConfig/TagConfig/index.tsx +++ b/app/pages/Import/TaskCreate/SchemaConfig/TagConfig/index.tsx @@ -17,11 +17,11 @@ interface IProps { const VerticesConfig = (props: IProps) => { const { tag, tagIndex, configIndex, file } = props; const { dataImport, schema } = useStore(); - const { asyncUpdateTagConfig, updateTagPropMapping } = dataImport; + const { updateTagConfig, updateTagPropMapping } = dataImport; const { tags } = schema; const handleTagChange = (configIndex: number, tagIndex: number, value: string) => { - asyncUpdateTagConfig({ configIndex, tagIndex, tag: value }); + updateTagConfig({ configIndex, tagIndex, tag: value }); }; const handlePropChange = (index, field, value) => { diff --git a/app/pages/Import/TaskCreate/index.tsx b/app/pages/Import/TaskCreate/index.tsx index 38767aee..a185847d 100644 --- a/app/pages/Import/TaskCreate/index.tsx +++ b/app/pages/Import/TaskCreate/index.tsx @@ -24,7 +24,7 @@ const formItemLayout = { }; const TaskCreate = () => { const { dataImport, schema, global } = useStore(); - const { taskDir, asyncGetTaskDir, basicConfig, verticesConfig, edgesConfig, updateBasicConfig, importTask } = dataImport; + const { taskDir, getTaskDir, basicConfig, verticesConfig, edgesConfig, updateBasicConfig, importTask } = dataImport; const { spaces, spaceVidType, getSpaces, updateSpaceInfo, currentSpace } = schema; const { host, username } = global; const { batchSize } = basicConfig; @@ -125,7 +125,7 @@ const TaskCreate = () => { }; useEffect(() => { - asyncGetTaskDir(); + getTaskDir(); getSpaces(); if(currentSpace) { updateSpaceInfo(currentSpace); diff --git a/app/pages/Import/TaskList/TaskItem/LogModal/index.less b/app/pages/Import/TaskList/TaskItem/LogModal/index.less new file mode 100644 index 00000000..4a3d09f7 --- /dev/null +++ b/app/pages/Import/TaskList/TaskItem/LogModal/index.less @@ -0,0 +1,62 @@ +@import '~@app/common.less'; +.log-modal { + height: 100vh; + .ant-modal { + height: 80%; + .ant-modal-content { + height: 100%; + } + } + .import-modal-title { + display: flex; + align-items: center; + } + .ant-modal-header { + border-bottom: none; + padding-right: 80px; + padding-top: 15px; + .ant-modal-title { + display: flex; + align-items: center; + justify-content: space-between; + } + .ant-modal-close { + top: 5px; + } + } + .ant-modal-body { + display: flex; + height: 91%; + } + .log-container { + width: 100%; + height: 100%; + overflow: auto; + padding: 10px 20px 120px; + font-size: 18px; + text-align: left; + background: #333; + color: #fff; + word-break: break-all; + } +} +.log-tab { + max-height: 65vh; + .ant-tabs-nav { + width: 200px; + .ant-tabs-tab { + background-color: @lightGray; + color: @darkBlue; + } + .ant-tabs-tab-active { + background-color: #0091FF; + color: #fff; + .ant-tabs-tab-btn { + color: #fff; + } + } + } + .ant-tabs-content-holder > .ant-tabs-content { + display: none + } +} \ No newline at end of file diff --git a/app/pages/Import/TaskList/TaskItem/LogModal/index.tsx b/app/pages/Import/TaskList/TaskItem/LogModal/index.tsx new file mode 100644 index 00000000..c7bc7448 --- /dev/null +++ b/app/pages/Import/TaskList/TaskItem/LogModal/index.tsx @@ -0,0 +1,131 @@ +import { Button, Modal, Tabs } from 'antd'; +import _ from 'lodash'; +import React, { useEffect, useRef, useState } from 'react'; +import intl from 'react-intl-universal'; +import Icon from '@app/components/Icon'; +import './index.less'; +import { useStore } from '@app/stores'; +import { ITaskStatus } from '@app/interfaces/import'; + +const { TabPane } = Tabs; + +interface ILogDimension { + space: string; + id: number; + status: ITaskStatus; +} + +interface ILog { + name: string; + path: string; +} +interface IProps { + logDimension: ILogDimension; + visible: boolean; + onCancel: () => void; +} +const LogModal = (props: IProps) => { + const { visible, onCancel, logDimension: { space, id, status } } = props; + const { dataImport: { getLogs, downloadTaskLog, getImportLogDetail, getErrLogDetail } } = useStore(); + const logRef = useRef(null); + const timer = useRef(null); + const offset = useRef(0); + const _status = useRef(status); + const [logs, setLogs] = useState([]); + const [currentLog, setCurrentLog] = useState(null); + const handleTabChange = (key: string) => { + setCurrentLog(logs.filter(item => item.name === key)[0]); + }; + + const getAllLogs = async() => { + const { code, data } = await getLogs(id); + if(code === 0) { + setLogs(data); + setCurrentLog(data[0]); + } + }; + + const handleLogDownload = () => { + if(currentLog) { + downloadTaskLog(currentLog.path); + } + }; + + const readLog = async() => { + const getLogDetail = currentLog!.name === 'import.log' ? getImportLogDetail : getErrLogDetail; + const res = await getLogDetail({ + offset: offset.current, + taskId: id, + path: currentLog!.path + }); + handleLogData(res); + }; + + const handleLogData = (res) => { + const { data } = res; + if(!logRef.current) { + timer.current = setTimeout(readLog, 2000); + return; + } + if (data) { + logRef.current.innerHTML += data.join('
') + '
'; + logRef.current.scrollTop = logRef.current.scrollHeight; + offset.current += data.length; + timer.current = setTimeout(readLog, 2000); + } else if (_status.current === ITaskStatus.StatusProcessing) { + timer.current = setTimeout(readLog, 2000); + } else { + offset.current = 0; + } + }; + + useEffect(() => { + getAllLogs(); + return () => { + clearTimeout(timer.current); + }; + }, []); + + useEffect(() => { + _status.current = status; + }, [status]); + useEffect(() => { + clearTimeout(timer.current); + if(logRef.current) { + logRef.current.innerHTML = ''; + } + offset.current = 0; + if(currentLog) { + readLog(); + } + }, [currentLog]); + return ( + +
+ {`${space} ${intl.get('import.task')} - ${intl.get('common.log')}`} + {status === ITaskStatus.StatusProcessing &&
+ + } + width="80%" + visible={visible} + onCancel={onCancel} + wrapClassName="log-modal" + destroyOnClose={true} + footer={false} + > + + {logs.map(log => ( + + ))} + +
+ + ); +}; + +export default LogModal; diff --git a/app/pages/Import/TaskList/TaskItem/index.tsx b/app/pages/Import/TaskList/TaskItem/index.tsx index da3107e8..5e8f2de0 100644 --- a/app/pages/Import/TaskList/TaskItem/index.tsx +++ b/app/pages/Import/TaskList/TaskItem/index.tsx @@ -6,12 +6,14 @@ import './index.less'; import { ITaskItem, ITaskStatus } from '@app/interfaces/import'; import dayjs from 'dayjs'; import { floor } from 'lodash'; +import { getFileSize } from '@app/utils/file'; import Icon from '@app/components/Icon'; interface IProps { data: ITaskItem; handleStop: (id: number) => void; handleDelete: (id: number) => void; handleDownload: (id: number) => void; + onViewLog: (id: number, space: string, taskStatus: ITaskStatus) => void; } @@ -39,12 +41,13 @@ const TaskItem = (props: IProps) => { space, taskID, name, - statsQuery: { totalCount, totalLine, numFailed, numReadFailed }, + stats: { totalImportedBytes, totalBytes, numFailed, numReadFailed }, taskStatus, taskMessage, updatedTime, createdTime }, + onViewLog, handleDownload, handleStop, handleDelete } = props; @@ -99,20 +102,20 @@ const TaskItem = (props: IProps) => {
- {taskStatus !== ITaskStatus.StatusFinished && `${totalCount} ${intl.get('import.lines')} / `} - {totalLine}{' '}{intl.get('import.lines')} + {taskStatus !== ITaskStatus.StatusFinished && `${getFileSize(totalImportedBytes)} / `} + {getFileSize(totalBytes)}{' '} {dayjs.duration(dayjs.unix(updatedTime).diff(dayjs.unix(createdTime))).format('HH:mm:ss')}
- - + {/* */} + {taskStatus === ITaskStatus.StatusProcessing && { const timer = useRef(null); const { dataImport } = useStore(); const history = useHistory(); - const { taskList, asyncGetTaskList, stopTask, deleteTask, downloadTaskConfig } = dataImport; + const { taskList, getTaskList, stopTask, deleteTask, downloadTaskConfig } = dataImport; + const [modalVisible, setVisible] = useState(false); + const [logDimension, setLogDimension] = useState({} as ILogDimension); const handleTaskStop = useCallback(async(id: number) => { clearTimeout(timer.current); const { code } = await stopTask(id); if(code === 0) { message.success(intl.get('import.stopImportingSuccess')); - await asyncGetTaskList(); + getTaskList(); } }, []); const handleTaskDelete = useCallback(async(id: number) => { @@ -32,12 +40,21 @@ const TaskList = () => { const { code } = await deleteTask(id); if(code === 0) { message.success(intl.get('import.deleteSuccess')); - await asyncGetTaskList(); + getTaskList(); } }, []); + + const handleLogView = (id: number, space: string, taskStatus: ITaskStatus) => { + setLogDimension({ + space, + id, + status: taskStatus + }); + setVisible(true); + }; useEffect(() => { isMounted = true; - asyncGetTaskList(); + getTaskList(); trackPageView('/import/tasks'); return () => { isMounted = false; @@ -46,12 +63,28 @@ const TaskList = () => { }, []); useEffect(() => { const needRefresh = taskList.filter(item => item.taskStatus === ITaskStatus.StatusProcessing).length > 0; + if(logDimension.id !== undefined && logDimension.status === ITaskStatus.StatusProcessing) { + const status = taskList.filter(item => item.taskID === logDimension.id)[0].taskStatus; + if(status !== ITaskStatus.StatusProcessing) { + setLogDimension({ + id: logDimension.id, + space: logDimension.space, + status + }); + } + } if(needRefresh && isMounted) { - timer.current = setTimeout(asyncGetTaskList, 2000); + timer.current = setTimeout(getTaskList, 2000); } else { clearTimeout(timer.current); } }, [taskList]); + + useEffect(() => { + if(modalVisible === false) { + setLogDimension({} as ILogDimension); + } + }, [modalVisible]); return (
@@ -69,12 +102,17 @@ const TaskList = () => {

{intl.get('import.taskList')} ({taskList.length})

{taskList.map(item => ( ))} + {modalVisible && setVisible(false)} + visible={modalVisible} />}
); }; diff --git a/app/stores/import.ts b/app/stores/import.ts index b97f6203..9c6037fe 100644 --- a/app/stores/import.ts +++ b/app/stores/import.ts @@ -32,7 +32,7 @@ export class ImportStore { Object.keys(payload).forEach(key => Object.prototype.hasOwnProperty.call(this, key) && (this[key] = payload[key])); }; - asyncGetTaskList = async() => { + getTaskList = async() => { const { code, data } = await service.handleImportAction({ taskAction: 'actionQueryAll', }); @@ -43,7 +43,7 @@ export class ImportStore { } }; - asyncGetTaskDir = async() => { + getTaskDir = async() => { const { code, data } = (await service.getTaskDir()) as any; if (code === 0) { const { taskDir } = data; @@ -52,6 +52,11 @@ export class ImportStore { }); } }; + + getLogs = async(id: number) => { + const { code, data } = (await service.getTaskLogs({ id })) as any; + return { code, data }; + } importTask = async(config, name) => { const { code, data } = (await service.importData({ @@ -85,14 +90,39 @@ export class ImportStore { downloadTaskConfig = async(taskID: number) => { const link = document.createElement('a'); - link.href = `/api-nebula/task/import/config/${taskID}`; + link.href = service.getTaskConfigUrl(taskID); link.download = `config.yml`; - document.body.appendChild(link); link.click(); - document.body.removeChild(link); } - asyncUpdateTagConfig = async(payload: { + downloadTaskLog = async(path: string) => { + const link = document.createElement('a'); + link.href = service.getTaskLogUrl(path); + link.download = `log.yml`; + link.click(); + } + + getImportLogDetail = async(params: { + offset: number; + limit?: number; + taskId: string | number; + path: string; + }) => { + const res = await service.getLog(params); + return res; + } + + getErrLogDetail = async(params: { + offset: number; + limit?: number; + taskId: string | number; + path: string; + }) => { + const res = await service.getErrLog(params); + return res; + } + + updateTagConfig = async(payload: { tag: string; tagIndex: number; configIndex: number;