Unverified Commit c9595c69 authored by zhu-mingye's avatar zhu-mingye Committed by GitHub

optimization checkpoints page (#735)

parent e8a87da6
...@@ -10,11 +10,13 @@ import { ...@@ -10,11 +10,13 @@ import {
import {parseByteStr, parseMilliSecondStr, parseSecondStr} from "@/components/Common/function"; import {parseByteStr, parseMilliSecondStr, parseSecondStr} from "@/components/Common/function";
import ProTable, {ActionType, ProColumns} from "@ant-design/pro-table"; import ProTable, {ActionType, ProColumns} from "@ant-design/pro-table";
import {useRef} from "react"; import {useRef} from "react";
import {CheckPointsDetailInfo, SavePointInfo} from "@/pages/DevOps/data"; import {CheckPointsDetailInfo} from "@/pages/DevOps/data";
import {CODE, queryData} from "@/components/Common/crud"; import {CODE, queryData} from "@/components/Common/crud";
import {selectSavePointRestartTask} from "@/pages/DevOps/service"; import {selectSavePointRestartTask} from "@/pages/DevOps/service";
import {JOB_LIFE_CYCLE} from "@/components/Common/JobLifeCycle"; import {JOB_LIFE_CYCLE} from "@/components/Common/JobLifeCycle";
import {history, useLocation} from 'umi'; import {history} from 'umi';
import {SavePointTableListItem} from "@/components/Studio/StudioRightTool/StudioSavePoint/data";
import moment from "moment";
const {TabPane} = Tabs; const {TabPane} = Tabs;
...@@ -22,133 +24,156 @@ const CheckPoints = (props: any) => { ...@@ -22,133 +24,156 @@ const CheckPoints = (props: any) => {
const {job} = props; const {job} = props;
const actionRef = useRef<ActionType>(); const actionRef = useRef<ActionType>();
const getOverview = () => {
const JsonParseObject = (item : any ) =>{
return JSON.parse(JSON.stringify(item))
}
const getOverview = (checkpoints: any) => {
let counts = JsonParseObject(checkpoints.counts)
let latest = JsonParseObject(checkpoints.latest)
return ( return (
<> <>
{JSON.stringify(job?.jobHistory?.checkpoints).includes("errors") ? <Empty image={Empty.PRESENTED_IMAGE_SIMPLE}/> :
<Descriptions bordered size="small" column={1}> <Descriptions bordered size="small" column={1}>
<Descriptions.Item label="CheckPoint Counts"> <Descriptions.Item label="CheckPoint Counts">
<Tag color="blue" title={"Total"}> <Tag color="blue" title={"Total"} >
<RocketOutlined/> Total: {job?.jobHistory?.checkpoints['counts']['total']} <RocketOutlined/> Total: {counts.total}
</Tag> </Tag>
<Tag color="red" title={"Failed"}> <Tag color="red" title={"Failed"}>
<CloseCircleOutlined/> Failed: {job?.jobHistory?.checkpoints['counts']['failed']} <CloseCircleOutlined/> Failed: {counts.failed}
</Tag> </Tag>
<Tag color="cyan" title={"Restored"}> <Tag color="cyan" title={"Restored"}>
<ExclamationCircleOutlined/> Restored: {job?.jobHistory?.checkpoints['counts']['restored']} <ExclamationCircleOutlined/> Restored: {counts.restored}
</Tag> </Tag>
<Tag color="green" title={"Completed"}> <Tag color="green" title={"Completed"}>
<CheckCircleOutlined/> Completed: {job?.jobHistory?.checkpoints['counts']['completed']} <CheckCircleOutlined/> Completed: {counts.completed}
</Tag> </Tag>
<Tag color="orange" title={"In Progress"}> <Tag color="orange" title={"In Progress"}>
<SyncOutlined spin/> In Progress: {job?.jobHistory?.checkpoints['counts']['in_progress']} <SyncOutlined spin/> In Progress: {counts.in_progress}
</Tag> </Tag>
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="Latest Completed CheckPoint"> <Descriptions.Item label="Latest Completed CheckPoint">
<Tag color="green" title={"Latest Completed CheckPoint"}> <Tag color="green" title={"Latest Completed CheckPoint"}>
{job?.jobHistory?.checkpoints['latest']['completed'] === null ? 'None' : {latest.completed === null ? 'None' :
job?.jobHistory?.checkpoints['latest']['completed']['external_path'] JsonParseObject(latest.completed).external_path
} }
</Tag> </Tag>
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="Latest Failed CheckPoint"> <Descriptions.Item label="Latest Failed CheckPoint">
{latest.failed === null ?
<Tag color="red" title={"Latest Failed CheckPoint"}> <Tag color="red" title={"Latest Failed CheckPoint"}>
{job?.jobHistory?.checkpoints['latest']['failed'] === null ? 'None' : {'None'}
job?.jobHistory?.checkpoints['latest']['failed']['external_path'] </Tag> :
} <>
<Tag color="red" title={"Latest Failed CheckPoint"}>
{"id: " + JsonParseObject(latest.failed).id}
</Tag>
<Tag color="red" title={"Latest Failed CheckPoint"}>
{ "Fail Time: " + moment(JsonParseObject(latest.failed).failure_timestamp).format('YYYY-MM-DD HH:mm:ss')}
</Tag> </Tag>
<Tag color="red" title={"Latest Failed CheckPoint"}>
{"Cause: " + JsonParseObject(latest.failed).failure_message}
</Tag>
</>
}
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="Latest Restored"> <Descriptions.Item label="Latest Restored">
<Tag color="cyan" title={"Latest Restored"}> <Tag color="cyan" title={"Latest Restored"}>
{job?.jobHistory?.checkpoints['latest']['restored'] === null ? 'None' : {latest.restored === null ? 'None' :
job?.jobHistory?.checkpoints['latest']['restored']['external_path']} JsonParseObject(latest.restored).external_path}
</Tag> </Tag>
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="Latest Savepoint"> <Descriptions.Item label="Latest Savepoint">
<Tag color="purple" title={"Latest Savepoint"}> <Tag color="purple" title={"Latest Savepoint"}>
{job?.jobHistory?.checkpoints['latest']['savepoint'] === null ? 'None' : {latest.savepoint === null ? 'None' :
job?.jobHistory?.checkpoints['latest']['savepoint']['external_path'] JsonParseObject(latest.savepoint).external_path
} }
</Tag> </Tag>
</Descriptions.Item> </Descriptions.Item>
</Descriptions> </Descriptions>
}
</> </>
) )
} }
const getSummary = () => {
const getSummary = (checkpoints : any) => {
let end_to_end_duration = JsonParseObject(JsonParseObject(checkpoints.summary)).end_to_end_duration
let state_size = JsonParseObject(JsonParseObject(checkpoints.summary)).state_size
let processed_data = JsonParseObject(JsonParseObject(checkpoints.summary)).processed_data
let persisted_data = JsonParseObject(JsonParseObject(checkpoints.summary)).persisted_data
let alignment_buffered = JsonParseObject(JsonParseObject(checkpoints.summary)).alignment_buffered
return ( return (
<> <>
{JSON.stringify(job?.jobHistory?.checkpoints).includes("errors") ? <Empty image={Empty.PRESENTED_IMAGE_SIMPLE}/> :
<Descriptions bordered size="small" column={1}> <Descriptions bordered size="small" column={1}>
<Descriptions.Item label="End to End Duration"> <Descriptions.Item label="End to End Duration">
<Tag color="blue" title={"Max"}> <Tag color="blue" title={"Max"}>
<RocketOutlined/> Max: {parseSecondStr(job?.jobHistory?.checkpoints['summary']['end_to_end_duration']['max'])} <RocketOutlined/> Max: {parseSecondStr(end_to_end_duration.max)}
</Tag> </Tag>
<Tag color="green" title={"Min"}> <Tag color="green" title={"Min"}>
<RocketOutlined/> Min: {parseSecondStr(job?.jobHistory?.checkpoints['summary']['end_to_end_duration']['min'])} <RocketOutlined/> Min: {parseSecondStr(end_to_end_duration.min)}
</Tag> </Tag>
<Tag color="orange" title={"Avg"}> <Tag color="orange" title={"Avg"}>
<RocketOutlined/> Avg: {parseSecondStr(job?.jobHistory?.checkpoints['summary']['end_to_end_duration']['avg'])} <RocketOutlined/> Avg: {parseSecondStr(end_to_end_duration.avg)}
</Tag> </Tag>
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="Checkpointed Data Size"> <Descriptions.Item label="Checkpointed Data Size">
<Tag color="blue" title={"Max"}> <Tag color="blue" title={"Max"}>
<RocketOutlined/> Max: {parseByteStr(job?.jobHistory?.checkpoints['summary']['state_size']['max'])} <RocketOutlined/> Max: {parseByteStr(state_size.max)}
</Tag> </Tag>
<Tag color="green" title={"Min"}> <Tag color="green" title={"Min"}>
<RocketOutlined/> Min: {parseByteStr(job?.jobHistory?.checkpoints['summary']['state_size']['min'])} <RocketOutlined/> Min: {parseByteStr(state_size.min)}
</Tag> </Tag>
<Tag color="orange" title={"Avg"}> <Tag color="orange" title={"Avg"}>
<RocketOutlined/> Avg: {parseByteStr(job?.jobHistory?.checkpoints['summary']['state_size']['avg'])} <RocketOutlined/> Avg: {parseByteStr(state_size.avg)}
</Tag> </Tag>
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="Processed (persisted) in-flight data"> <Descriptions.Item label="Processed (persisted) in-flight data">
<Tag color="blue" title={"Max"}> <Tag color="blue" title={"Max"}>
<RocketOutlined/> Max: {job?.jobHistory?.checkpoints['summary']['processed_data']['max']} <RocketOutlined/> Max: {processed_data.max}
</Tag> </Tag>
<Tag color="green" title={"Min"}> <Tag color="green" title={"Min"}>
<RocketOutlined/> Min: {job?.jobHistory?.checkpoints['summary']['processed_data']['min']} <RocketOutlined/> Min: {processed_data.min}
</Tag> </Tag>
<Tag color="orange" title={"Avg"}> <Tag color="orange" title={"Avg"}>
<RocketOutlined/> Avg: {job?.jobHistory?.checkpoints['summary']['processed_data']['avg']} <RocketOutlined/> Avg: {processed_data.avg}
</Tag> </Tag>
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="Persisted data"> <Descriptions.Item label="Persisted data">
<Tag color="blue" title={"Max"}> <Tag color="blue" title={"Max"}>
<RocketOutlined/> Max: {job?.jobHistory?.checkpoints['summary']['persisted_data']['max']} <RocketOutlined/> Max: {persisted_data.max}
</Tag> </Tag>
<Tag color="green" title={"Min"}> <Tag color="green" title={"Min"}>
<RocketOutlined/> Min: {job?.jobHistory?.checkpoints['summary']['persisted_data']['min']} <RocketOutlined/> Min: {persisted_data.min}
</Tag> </Tag>
<Tag color="orange" title={"Avg"}> <Tag color="orange" title={"Avg"}>
<RocketOutlined/> Avg: {job?.jobHistory?.checkpoints['summary']['persisted_data']['avg']} <RocketOutlined/> Avg: {persisted_data.avg}
</Tag> </Tag>
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="Alignment Buffered"> <Descriptions.Item label="Alignment Buffered">
<Tag color="blue" title={"Max"}> <Tag color="blue" title={"Max"}>
<RocketOutlined/> Max: {job?.jobHistory?.checkpoints['summary']['alignment_buffered']['max']} <RocketOutlined/> Max: {alignment_buffered.max}
</Tag> </Tag>
<Tag color="green" title={"Min"}> <Tag color="green" title={"Min"}>
<RocketOutlined/> Min: {job?.jobHistory?.checkpoints['summary']['alignment_buffered']['min']} <RocketOutlined/> Min: {alignment_buffered.min}
</Tag> </Tag>
<Tag color="orange" title={"Avg"}> <Tag color="orange" title={"Avg"}>
<RocketOutlined/> Avg: {job?.jobHistory?.checkpoints['summary']['alignment_buffered']['avg']} <RocketOutlined/> Avg: {alignment_buffered.avg}
</Tag> </Tag>
</Descriptions.Item> </Descriptions.Item>
</Descriptions> </Descriptions>
}
</> </>
) )
} }
...@@ -161,8 +186,6 @@ const CheckPoints = (props: any) => { ...@@ -161,8 +186,6 @@ const CheckPoints = (props: any) => {
okText: '确认', okText: '确认',
cancelText: '取消', cancelText: '取消',
onOk: async () => { onOk: async () => {
// TODO: handleRecoveryCheckPoint
// await handleRecoveryCheckPoint('api/task/recoveryCheckPoint', [row]);
const res = selectSavePointRestartTask(job?.instance?.taskId, job?.instance?.step == JOB_LIFE_CYCLE.ONLINE, row.external_path); const res = selectSavePointRestartTask(job?.instance?.taskId, job?.instance?.step == JOB_LIFE_CYCLE.ONLINE, row.external_path);
res.then((result) => { res.then((result) => {
if (result.code == CODE.SUCCESS) { if (result.code == CODE.SUCCESS) {
...@@ -176,10 +199,10 @@ const CheckPoints = (props: any) => { ...@@ -176,10 +199,10 @@ const CheckPoints = (props: any) => {
}); });
} }
const getHistory = () => { const getHistory = (checkpoints : any) => {
const checkPointsList: CheckPointsDetailInfo[] = []; const checkPointsList: CheckPointsDetailInfo[] = [];
job?.jobHistory?.checkpoints['history']?.forEach((entity: CheckPointsDetailInfo) => { checkpoints?.history?.forEach((entity: CheckPointsDetailInfo) => {
return checkPointsList.push({ return checkPointsList.push({
jobID: job?.id, jobID: job?.id,
historyID: job?.jobHistory.id, historyID: job?.jobHistory.id,
...@@ -256,7 +279,7 @@ const CheckPoints = (props: any) => { ...@@ -256,7 +279,7 @@ const CheckPoints = (props: any) => {
render: (dom, entity) => { render: (dom, entity) => {
return <> return <>
{entity.status === 'COMPLETED' ? {entity.status === 'COMPLETED' ?
<Button title="暂不可用" onClick={() => recoveryCheckPoint(entity)}>此处恢复</Button> : undefined} <Button onClick={() => recoveryCheckPoint(entity)}>此处恢复</Button> : undefined}
</> </>
}, },
}, },
...@@ -287,69 +310,68 @@ const CheckPoints = (props: any) => { ...@@ -287,69 +310,68 @@ const CheckPoints = (props: any) => {
} }
const getConfigraution = () => { const getConfiguration = (checkpointsConfig : any) => {
let checkpointsConfigInfo = JsonParseObject(checkpointsConfig)
return ( return (
<> <>
{JSON.stringify(job?.jobHistory?.checkpointsConfig).includes("errors") ? <Empty image={Empty.PRESENTED_IMAGE_SIMPLE}/> :
<Descriptions bordered size="small" column={1}> <Descriptions bordered size="small" column={1}>
<Descriptions.Item label="Checkpointing Mode"> <Descriptions.Item label="Checkpointing Mode">
<Tag color="blue" title={"Checkpointing Mode"}> <Tag color="blue" title={"Checkpointing Mode"}>
{job?.jobHistory?.checkpointsConfig['mode'].toUpperCase()} {checkpointsConfigInfo.mode.toUpperCase()}
</Tag> </Tag>
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="Checkpoint Storage"> <Descriptions.Item label="Checkpoint Storage">
<Tag color="blue" title={"Checkpoint Storage"}> <Tag color="blue" title={"Checkpoint Storage"}>
{job?.jobHistory?.checkpointsConfig['checkpoint_storage'] ? job?.jobHistory?.checkpointsConfig['checkpoint_storage'] : 'Disabled'} {checkpointsConfigInfo.checkpoint_storage ? checkpointsConfigInfo.checkpoint_storage : 'Disabled'}
</Tag> </Tag>
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="State Backend"> <Descriptions.Item label="State Backend">
<Tag color="blue" title={"State Backend"}> <Tag color="blue" title={"State Backend"}>
{job?.jobHistory?.checkpointsConfig['state_backend'] ? job?.jobHistory?.checkpointsConfig['state_backend'] : 'Disabled'} {checkpointsConfigInfo.state_backend ? checkpointsConfigInfo.state_backend : 'Disabled'}
</Tag> </Tag>
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="Interval"> <Descriptions.Item label="Interval">
<Tag color="blue" title={"Interval"}> <Tag color="blue" title={"Interval"}>
{job?.jobHistory?.checkpointsConfig['interval']} {checkpointsConfigInfo.interval }
</Tag> </Tag>
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="Timeout"> <Descriptions.Item label="Timeout">
<Tag color="blue" title={"Timeout"}> <Tag color="blue" title={"Timeout"}>
{(job?.jobHistory?.checkpointsConfig['timeout'])} {checkpointsConfigInfo.timeout}
</Tag> </Tag>
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="Minimum Pause Between Checkpoints"> <Descriptions.Item label="Minimum Pause Between Checkpoints">
<Tag color="blue" title={"Minimum Pause Between Checkpoints"}> <Tag color="blue" title={"Minimum Pause Between Checkpoints"}>
{(job?.jobHistory?.checkpointsConfig['min_pause'])} {checkpointsConfigInfo.min_pause}
</Tag> </Tag>
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="Maximum Concurrent Checkpoints"> <Descriptions.Item label="Maximum Concurrent Checkpoints">
<Tag color="blue" title={"Maximum Concurrent Checkpoints"}> <Tag color="blue" title={"Maximum Concurrent Checkpoints"}>
{job?.jobHistory?.checkpointsConfig['max_concurrent']} {checkpointsConfigInfo.max_concurrent}
</Tag> </Tag>
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="Unaligned Checkpoints "> <Descriptions.Item label="Unaligned Checkpoints ">
<Tag color="blue" title={"Unaligned Checkpoints"}> <Tag color="blue" title={"Unaligned Checkpoints"}>
{job?.jobHistory?.checkpointsConfig['unaligned_checkpoints'] ? 'Enabled' : 'Disabled'} {checkpointsConfigInfo.unaligned_checkpoints ? 'Enabled' : 'Disabled'}
</Tag> </Tag>
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="Persist Checkpoints Externally Enabled"> <Descriptions.Item label="Persist Checkpoints Externally Enabled">
<Tag color="blue" title={"Persist Checkpoints Externally Enabled"}> <Tag color="blue" title={"Persist Checkpoints Externally Enabled"}>
{job?.jobHistory?.checkpointsConfig['externalization']['enabled'] ? 'Enabled' : 'Disabled'} {JsonParseObject(checkpointsConfigInfo.externalization).enabled ? 'Enabled' : 'Disabled'}
</Tag> </Tag>
</Descriptions.Item> </Descriptions.Item>
{job?.jobHistory?.checkpointsConfig['externalization']['enabled'] && ( {JsonParseObject(checkpointsConfigInfo.externalization).enabled && (
<Descriptions.Item label="Delete On Cancellation"> <Descriptions.Item label="Delete On Cancellation">
<Tag color="blue" title={"Delete On Cancellation"}> <Tag color="blue" title={"Delete On Cancellation"}>
{job?.jobHistory?.checkpointsConfig['externalization']['delete_on_cancellation'] ? 'Enabled' : 'Disabled'} { JsonParseObject(checkpointsConfigInfo.externalization).delete_on_cancellation ? 'Enabled' : 'Disabled'}
</Tag> </Tag>
</Descriptions.Item> </Descriptions.Item>
)} )}
...@@ -357,11 +379,10 @@ const CheckPoints = (props: any) => { ...@@ -357,11 +379,10 @@ const CheckPoints = (props: any) => {
<Descriptions.Item label="Tolerable Failed Checkpoints"> <Descriptions.Item label="Tolerable Failed Checkpoints">
<Tag color="blue" title={"Tolerable Failed Checkpoints"}> <Tag color="blue" title={"Tolerable Failed Checkpoints"}>
{job?.jobHistory?.checkpointsConfig['tolerable_failed_checkpoints']} {checkpointsConfigInfo.tolerable_failed_checkpoints}
</Tag> </Tag>
</Descriptions.Item> </Descriptions.Item>
</Descriptions> </Descriptions>
}
</> </>
) )
} }
...@@ -370,7 +391,7 @@ const CheckPoints = (props: any) => { ...@@ -370,7 +391,7 @@ const CheckPoints = (props: any) => {
function getSavePoint() { function getSavePoint() {
const url = '/api/savepoints'; const url = '/api/savepoints';
const columns: ProColumns<SavePointInfo>[] = [ const columns: ProColumns<SavePointTableListItem>[] = [
{ {
title: 'ID', title: 'ID',
align: 'center', align: 'center',
...@@ -410,7 +431,7 @@ const CheckPoints = (props: any) => { ...@@ -410,7 +431,7 @@ const CheckPoints = (props: any) => {
return ( return (
<> <>
<ProTable<SavePointInfo> <ProTable<SavePointTableListItem>
columns={columns} columns={columns}
style={{width: '100%'}} style={{width: '100%'}}
request={(params, sorter, filter) => queryData(url, {taskId: job?.instance.taskId, ...params, sorter, filter})} request={(params, sorter, filter) => queryData(url, {taskId: job?.instance.taskId, ...params, sorter, filter})}
...@@ -431,19 +452,27 @@ const CheckPoints = (props: any) => { ...@@ -431,19 +452,27 @@ const CheckPoints = (props: any) => {
border: "1px solid #f0f0f0", border: "1px solid #f0f0f0",
}}> }}>
<TabPane tab={<span>&nbsp; Overview &nbsp;</span>} key="overview"> <TabPane tab={<span>&nbsp; Overview &nbsp;</span>} key="overview">
{getOverview()} { !JSON.stringify(job?.jobHistory?.checkpoints).includes("errors") ?
getOverview(JsonParseObject(job?.jobHistory?.checkpoints)) :
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE}/>}
</TabPane> </TabPane>
<TabPane tab={<span>&nbsp; History &nbsp;</span>} key="history"> <TabPane tab={<span>&nbsp; History &nbsp;</span>} key="history">
{getHistory()} {getHistory(JsonParseObject(job?.jobHistory?.checkpoints))}
</TabPane> </TabPane>
<TabPane tab={<span>&nbsp; Summary &nbsp;</span>} key="summary"> <TabPane tab={<span>&nbsp; Summary &nbsp;</span>} key="summary">
{getSummary()} { !JSON.stringify(job?.jobHistory?.checkpoints).includes("errors") ?
getSummary(JsonParseObject(job?.jobHistory?.checkpoints)) :
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE}/>
}
</TabPane> </TabPane>
<TabPane tab={<span>&nbsp; Configraution &nbsp;</span>} key="configraution"> <TabPane tab={<span>&nbsp; Configuration &nbsp;</span>} key="configuration">
{getConfigraution()} { !JSON.stringify(job?.jobHistory?.checkpointsConfig).includes("errors") ?
getConfiguration(JsonParseObject(job?.jobHistory?.checkpointsConfig)) :
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE}/>
}
</TabPane> </TabPane>
<TabPane tab={<span>&nbsp; SavePoint &nbsp;</span>} key="savepoint"> <TabPane tab={<span>&nbsp; SavePoint &nbsp;</span>} key="savepoint">
......
...@@ -132,12 +132,3 @@ export type CheckPointsDetailInfo = { ...@@ -132,12 +132,3 @@ export type CheckPointsDetailInfo = {
trigger_timestamp: number, trigger_timestamp: number,
} }
export type SavePointInfo = {
id: number,
taskId: number,
name: string,
type: string,
path: string,
createTime: Date,
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment