Unverified Commit 90a82dd0 authored by aiwenmo's avatar aiwenmo Committed by GitHub

[Feature-1027][admin,web] Add data development task information log details button (#1029)

Co-authored-by: 's avatarwenmo <32723967+wenmo@users.noreply.github.com>
parent 53b55dbe
...@@ -204,7 +204,7 @@ ...@@ -204,7 +204,7 @@
</dependencies> </dependencies>
<build> <build>
<finalName>${project.artifactId}</finalName> <finalName>${project.artifactId}-${project.version}</finalName>
<plugins> <plugins>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
......
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
</includes> </includes>
</fileSet> </fileSet>
<fileSet> <fileSet>
<directory>${project.parent.basedir}/dlink-admin/target/dlink-admin/lib</directory> <directory>${project.parent.basedir}/dlink-admin/target/dlink-admin-${project.version}/lib</directory>
<outputDirectory>lib</outputDirectory> <outputDirectory>lib</outputDirectory>
<includes> <includes>
<include>*.jar</include> <include>*.jar</include>
......
...@@ -19,12 +19,11 @@ ...@@ -19,12 +19,11 @@
import MonacoEditor from "react-monaco-editor"; import MonacoEditor from "react-monaco-editor";
import * as _monaco from "monaco-editor";
export type CodeShowFormProps = { export type CodeShowFormProps = {
height?: string; height?: string;
width?: string; width?: string;
language: string; language?: string;
theme?: string; theme?: string;
options?: any; options?: any;
code: string; code: string;
...@@ -40,8 +39,8 @@ const CodeShow = (props: CodeShowFormProps) => { ...@@ -40,8 +39,8 @@ const CodeShow = (props: CodeShowFormProps) => {
options = { options = {
selectOnLineNumbers: true, selectOnLineNumbers: true,
renderSideBySide: false, renderSideBySide: false,
autoIndent:'None', autoIndent: 'None',
readOnly:true , readOnly: true,
}, },
code, code,
} = props; } = props;
......
...@@ -26,18 +26,15 @@ import ProList from '@ant-design/pro-list'; ...@@ -26,18 +26,15 @@ import ProList from '@ant-design/pro-list';
import {handleRemove, queryData} from "@/components/Common/crud"; import {handleRemove, queryData} from "@/components/Common/crud";
import ProDescriptions from '@ant-design/pro-descriptions'; import ProDescriptions from '@ant-design/pro-descriptions';
import React, {useState} from "react"; import React, {useState} from "react";
import {ModalForm,} from '@ant-design/pro-form';
import styles from "./index.less";
import {Scrollbars} from 'react-custom-scrollbars';
import StudioPreview from "../StudioPreview"; import StudioPreview from "../StudioPreview";
import {getJobData} from "@/pages/DataStudio/service"; import {getJobData} from "@/pages/DataStudio/service";
import {HistoryItem} from "@/components/Studio/StudioConsole/StudioHistory/data"; import {HistoryItem} from "@/components/Studio/StudioConsole/StudioHistory/data";
import CodeShow from "@/components/Common/CodeShow"; import CodeShow from "@/components/Common/CodeShow";
const { Title, Paragraph, Text, Link } = Typography; const {Title, Paragraph, Text, Link} = Typography;
type HistoryConfig={ type HistoryConfig = {
useSession: boolean; useSession: boolean;
session: string; session: string;
useRemote: boolean; useRemote: boolean;
...@@ -62,41 +59,44 @@ type HistoryConfig={ ...@@ -62,41 +59,44 @@ type HistoryConfig={
const url = '/api/history'; const url = '/api/history';
const StudioHistory = (props: any) => { const StudioHistory = (props: any) => {
const {current,refs,dispatch} = props; const {current, refs, dispatch} = props;
const [modalVisit, setModalVisit] = useState(false); const [modalVisit, setModalVisit] = useState(false);
const [row, setRow] = useState<HistoryItem>(); const [row, setRow] = useState<HistoryItem>();
const [config,setConfig] = useState<HistoryConfig>(); const [config, setConfig] = useState<HistoryConfig>();
const [type,setType] = useState<number>(); const [type, setType] = useState<number>();
const [result,setResult] = useState<{}>(); const [result, setResult] = useState<{}>();
const showDetail=(row:HistoryItem,type:number)=>{ const showDetail = (row: HistoryItem, type: number) => {
setRow(row); setRow(row);
setModalVisit(true); setModalVisit(true);
setType(type); setType(type);
setConfig(JSON.parse(row.configJson)); setConfig(JSON.parse(row.configJson));
if(type===3){ if (type === 3) {
// showJobData(row.jobId,dispatch) // showJobData(row.jobId,dispatch)
const res = getJobData(row.jobId); const res = getJobData(row.jobId);
res.then((resd)=>{ res.then((resd) => {
setResult(resd.datas); setResult(resd.datas);
}); });
} }
}; };
const removeHistory=(row:HistoryItem)=>{ const removeHistory = (row: HistoryItem) => {
Modal.confirm({ Modal.confirm({
title: '删除执行记录', title: '删除执行记录',
content: '确定删除该执行记录吗?', content: '确定删除该执行记录吗?',
okText: '确认', okText: '确认',
cancelText: '取消', cancelText: '取消',
onOk:async () => { onOk: async () => {
await handleRemove(url,[row]); await handleRemove(url, [row]);
// refs.current?.reloadAndRest?.();
refs.history?.current?.reload(); refs.history?.current?.reload();
} }
}); });
}; };
const handleCancel = () => {
setModalVisit(false);
};
return ( return (
<> <>
<ProList<HistoryItem> <ProList<HistoryItem>
...@@ -111,7 +111,7 @@ const StudioHistory = (props: any) => { ...@@ -111,7 +111,7 @@ const StudioHistory = (props: any) => {
}} }}
rowKey="id" rowKey="id"
headerTitle="执行历史" headerTitle="执行历史"
request={(params, sorter, filter) => queryData(url,{...params, sorter:{id:'descend'}, filter})} request={(params, sorter, filter) => queryData(url, {...params, sorter: {id: 'descend'}, filter})}
pagination={{ pagination={{
pageSize: 5, pageSize: 5,
}} }}
...@@ -124,7 +124,7 @@ const StudioHistory = (props: any) => { ...@@ -124,7 +124,7 @@ const StudioHistory = (props: any) => {
return ( return (
<Space size={0}> <Space size={0}>
<Tag color="blue" key={row.jobId}> <Tag color="blue" key={row.jobId}>
<FireOutlined /> {row.jobId} <FireOutlined/> {row.jobId}
</Tag> </Tag>
</Space> </Space>
); );
...@@ -132,7 +132,7 @@ const StudioHistory = (props: any) => { ...@@ -132,7 +132,7 @@ const StudioHistory = (props: any) => {
}, },
description: { description: {
search: false, search: false,
render:(_, row)=>{ render: (_, row) => {
return (<Paragraph> return (<Paragraph>
<blockquote> <blockquote>
<Link href={`http://${row.jobManagerAddress}`} target="_blank"> <Link href={`http://${row.jobManagerAddress}`} target="_blank">
...@@ -148,37 +148,37 @@ const StudioHistory = (props: any) => { ...@@ -148,37 +148,37 @@ const StudioHistory = (props: any) => {
render: (_, row) => { render: (_, row) => {
return ( return (
<Space size={0}> <Space size={0}>
{row.jobName?( {row.jobName ? (
<Tag color="gray" key={row.jobName}> <Tag color="gray" key={row.jobName}>
{row.jobName} {row.jobName}
</Tag> </Tag>
):''} ) : ''}
{row.session?( {row.session ? (
<Tag color="orange" key={row.session}> <Tag color="orange" key={row.session}>
<MessageOutlined /> {row.session} <MessageOutlined/> {row.session}
</Tag> </Tag>
):''} ) : ''}
{row.clusterAlias?( {row.clusterAlias ? (
<Tag color="green" key={row.clusterAlias}> <Tag color="green" key={row.clusterAlias}>
<ClusterOutlined /> {row.clusterAlias} <ClusterOutlined/> {row.clusterAlias}
</Tag> </Tag>
):(<Tag color="green" key={row.clusterAlias}> ) : (<Tag color="green" key={row.clusterAlias}>
<ClusterOutlined /> 本地环境 <ClusterOutlined/> 本地环境
</Tag>)} </Tag>)}
{row.type?( {row.type ? (
<Tag color="blue" key={row.type}> <Tag color="blue" key={row.type}>
<RocketOutlined /> {row.type} <RocketOutlined/> {row.type}
</Tag> </Tag>
):''} ) : ''}
{(row.status==2) ? {(row.status == 2) ?
(<><Badge status="success"/><Text type="success">SUCCESS</Text></>): (<><Badge status="success"/><Text type="success">SUCCESS</Text></>) :
(row.status==1) ? (row.status == 1) ?
<><Badge status="success"/><Text type="secondary">RUNNING</Text></> : <><Badge status="success"/><Text type="secondary">RUNNING</Text></> :
(row.status==3) ? (row.status == 3) ?
<><Badge status="error"/><Text type="danger">FAILED</Text></> : <><Badge status="error"/><Text type="danger">FAILED</Text></> :
(row.status==4) ? (row.status == 4) ?
<><Badge status="error"/><Text type="warning">CANCEL</Text></> : <><Badge status="error"/><Text type="warning">CANCEL</Text></> :
(row.status==0) ? (row.status == 0) ?
<><Badge status="error"/><Text type="warning">INITIALIZE</Text></> : <><Badge status="error"/><Text type="warning">INITIALIZE</Text></> :
<><Badge status="success"/><Text type="danger">UNKNOWEN</Text></>} <><Badge status="success"/><Text type="danger">UNKNOWEN</Text></>}
</Space> </Space>
...@@ -188,19 +188,29 @@ const StudioHistory = (props: any) => { ...@@ -188,19 +188,29 @@ const StudioHistory = (props: any) => {
}, },
actions: { actions: {
render: (text, row) => [ render: (text, row) => [
<a key="config" onClick={()=>{showDetail(row,1)}}> <a key="config" onClick={() => {
showDetail(row, 1)
}}>
执行配置 执行配置
</a>, </a>,
<a key="statement" onClick={()=>{showDetail(row,2)}}> <a key="statement" onClick={() => {
showDetail(row, 2)
}}>
FlinkSql语句 FlinkSql语句
</a>, </a>,
<a key="result" onClick={()=>{showDetail(row,3)}}> <a key="result" onClick={() => {
showDetail(row, 3)
}}>
预览数据 预览数据
</a>, </a>,
<a key="error" onClick={()=>{showDetail(row,4)}}> <a key="error" onClick={() => {
showDetail(row, 4)
}}>
异常信息 异常信息
</a>, </a>,
<a key="delete" onClick={()=>{removeHistory(row)}}> <a key="delete" onClick={() => {
removeHistory(row)
}}>
删除 删除
</a>, </a>,
], ],
...@@ -259,40 +269,35 @@ const StudioHistory = (props: any) => { ...@@ -259,40 +269,35 @@ const StudioHistory = (props: any) => {
}} }}
options={{ options={{
search: false, search: false,
setting:false setting: false
}} }}
/> />
<ModalForm <Modal
width={'80%'}
visible={modalVisit} visible={modalVisit}
onFinish={async () => { destroyOnClose
}} centered
onVisibleChange={setModalVisit} footer={false}
submitter={{ onCancel={handleCancel}
submitButtonProps: {
style: {
display: 'none',
},
},
}}
> >
{type==1 && ( {type == 1 && (
<ProDescriptions <ProDescriptions
column={2} column={2}
title='执行配置' title='执行配置'
> >
<ProDescriptions.Item span={2} label="JobId" > <ProDescriptions.Item span={2} label="JobId">
<Tag color="blue" key={row.jobId}> <Tag color="blue" key={row.jobId}>
<FireOutlined /> {row.jobId} <FireOutlined/> {row.jobId}
</Tag> </Tag>
</ProDescriptions.Item> </ProDescriptions.Item>
<ProDescriptions.Item label="共享会话" > <ProDescriptions.Item label="共享会话">
{config.useSession?'启用':'禁用'} {config.useSession ? '启用' : '禁用'}
</ProDescriptions.Item> </ProDescriptions.Item>
<ProDescriptions.Item label="会话 Key"> <ProDescriptions.Item label="会话 Key">
{config.session} {config.session}
</ProDescriptions.Item> </ProDescriptions.Item>
<ProDescriptions.Item label="执行方式" > <ProDescriptions.Item label="执行方式">
{config.useRemote?'远程':'本地'} {config.useRemote ? '远程' : '本地'}
</ProDescriptions.Item> </ProDescriptions.Item>
<ProDescriptions.Item label="任务类型"> <ProDescriptions.Item label="任务类型">
{config.type} {config.type}
...@@ -303,17 +308,17 @@ const StudioHistory = (props: any) => { ...@@ -303,17 +308,17 @@ const StudioHistory = (props: any) => {
<ProDescriptions.Item label="集群配置ID"> <ProDescriptions.Item label="集群配置ID">
{config.clusterConfigurationId} {config.clusterConfigurationId}
</ProDescriptions.Item> </ProDescriptions.Item>
<ProDescriptions.Item label="预览结果" > <ProDescriptions.Item label="预览结果">
{config.useResult?'启用':'禁用'} {config.useResult ? '启用' : '禁用'}
</ProDescriptions.Item> </ProDescriptions.Item>
<ProDescriptions.Item label="打印流" > <ProDescriptions.Item label="打印流">
{config.useChangeLog?'启用':'禁用'} {config.useChangeLog ? '启用' : '禁用'}
</ProDescriptions.Item> </ProDescriptions.Item>
<ProDescriptions.Item label="最大行数"> <ProDescriptions.Item label="最大行数">
{config.maxRowNum} {config.maxRowNum}
</ProDescriptions.Item> </ProDescriptions.Item>
<ProDescriptions.Item label="自动停止" > <ProDescriptions.Item label="自动停止">
{config.useAutoCancel?'启用':'禁用'} {config.useAutoCancel ? '启用' : '禁用'}
</ProDescriptions.Item> </ProDescriptions.Item>
<ProDescriptions.Item span={2} label="JobManagerAddress"> <ProDescriptions.Item span={2} label="JobManagerAddress">
{row.jobManagerAddress} {row.jobManagerAddress}
...@@ -325,10 +330,10 @@ const StudioHistory = (props: any) => { ...@@ -325,10 +330,10 @@ const StudioHistory = (props: any) => {
{config.jobName} {config.jobName}
</ProDescriptions.Item> </ProDescriptions.Item>
<ProDescriptions.Item label="片段机制"> <ProDescriptions.Item label="片段机制">
{config.useSqlFragment?'启用':'禁用'} {config.useSqlFragment ? '启用' : '禁用'}
</ProDescriptions.Item> </ProDescriptions.Item>
<ProDescriptions.Item label="语句集"> <ProDescriptions.Item label="语句集">
{config.useStatementSet?'启用':'禁用'} {config.useStatementSet ? '启用' : '禁用'}
</ProDescriptions.Item> </ProDescriptions.Item>
<ProDescriptions.Item label="并行度"> <ProDescriptions.Item label="并行度">
{config.parallelism} {config.parallelism}
...@@ -344,59 +349,57 @@ const StudioHistory = (props: any) => { ...@@ -344,59 +349,57 @@ const StudioHistory = (props: any) => {
</ProDescriptions.Item> </ProDescriptions.Item>
</ProDescriptions> </ProDescriptions>
)} )}
{type==2 && ( {type == 2 && (
<ProDescriptions <ProDescriptions
column={1} column={1}
title='FlinkSql 语句' title='FlinkSql 语句'
> >
<ProDescriptions.Item label="JobId" > <ProDescriptions.Item label="JobId">
<Tag color="blue" key={row.jobId}> <Tag color="blue" key={row.jobId}>
<FireOutlined /> {row.jobId} <FireOutlined/> {row.jobId}
</Tag> </Tag>
</ProDescriptions.Item> </ProDescriptions.Item>
<ProDescriptions.Item> <ProDescriptions.Item>
<CodeShow width={"100%"} height={"500px"} language={"sql"} code={row.statement} theme={"vs-dark"}/> <CodeShow height={"80vh"} language={"sql"} code={row.statement} theme={"vs-dark"}/>
</ProDescriptions.Item> </ProDescriptions.Item>
</ProDescriptions> </ProDescriptions>
)} )}
{type==3 && ( {type == 3 && (
<ProDescriptions <ProDescriptions
column={2} column={2}
title='数据预览' title='数据预览'
> >
<ProDescriptions.Item span={2} label="JobId" > <ProDescriptions.Item span={2} label="JobId">
<Tag color="blue" key={row.jobId}> <Tag color="blue" key={row.jobId}>
<FireOutlined /> {row.jobId} <FireOutlined/> {row.jobId}
</Tag> </Tag>
</ProDescriptions.Item> </ProDescriptions.Item>
<ProDescriptions.Item span={2} > <ProDescriptions.Item span={2}>
<StudioPreview result={result} style={{width: '100%'}}/> <StudioPreview result={result} style={{width: '100%'}}/>
</ProDescriptions.Item> </ProDescriptions.Item>
</ProDescriptions> </ProDescriptions>
)} )}
{type==4 && ( {type == 4 && (
<ProDescriptions <ProDescriptions
column={1} column={1}
title='异常信息' title='异常信息'
> >
<ProDescriptions.Item label="JobId" > <ProDescriptions.Item label="JobId">
<Tag color="blue" key={row.jobId}> <Tag color="blue" key={row.jobId}>
<FireOutlined /> {row.jobId} <FireOutlined/> {row.jobId}
</Tag> </Tag>
</ProDescriptions.Item> </ProDescriptions.Item>
<ProDescriptions.Item> <ProDescriptions.Item>
<Scrollbars style={{height: '400px',width:'100%'}}> <CodeShow height={"80vh"} language={"java"} code={row.error} theme={"vs-dark"}/>
<pre className={styles.code}>{row.error}</pre>
</Scrollbars>
</ProDescriptions.Item> </ProDescriptions.Item>
</ProDescriptions> </ProDescriptions>
)} )}
</ModalForm> </Modal>
</> </>
); );
}; };
export default connect(({Studio}: {Studio: StateType}) => ({ export default connect(({Studio}: { Studio: StateType }) => ({
current: Studio.current, current: Studio.current,
refs: Studio.refs, refs: Studio.refs,
}))(StudioHistory); }))(StudioHistory);
...@@ -18,23 +18,39 @@ ...@@ -18,23 +18,39 @@
*/ */
import {Typography, Divider, Badge, Empty,Tag} from "antd"; import {Badge, Button, Divider, Empty, Modal, Tag, Typography} from "antd";
import {StateType} from "@/pages/DataStudio/model"; import {StateType} from "@/pages/DataStudio/model";
import {connect} from "umi"; import {connect} from "umi";
import {FireOutlined, ScheduleOutlined} from '@ant-design/icons'; import {FireOutlined, ZoomInOutlined} from '@ant-design/icons';
import StudioSqlConfig from "@/components/Studio/StudioRightTool/StudioSqlConfig"; import {isSql} from "@/components/Studio/conf";
import {DIALECT, isSql} from "@/components/Studio/conf"; import {useState} from "react";
import CodeShow from "@/components/Common/CodeShow";
const { Title, Paragraph, Text, Link } = Typography; const {Title, Paragraph, Text, Link} = Typography;
const StudioMsg = (props:any) => { const StudioMsg = (props: any) => {
const {current} = props; const {current} = props;
const [sqlModalVisit, setSqlModalVisit] = useState(false);
const [errorModalVisit, setErrorModalVisit] = useState(false);
const handleOpenSqlModal = () => {
setSqlModalVisit(true);
};
const handleOpenErrorModal = () => {
setErrorModalVisit(true);
};
const handleCancel = () => {
setSqlModalVisit(false);
setErrorModalVisit(false);
};
const renderCommonSqlContent = () => { const renderCommonSqlContent = () => {
return (<> return (<>
<Paragraph> <Paragraph>
<blockquote> <Divider type="vertical"/>{current.console.result.startTime} <blockquote><Divider type="vertical"/>{current.console.result.startTime}
<Divider type="vertical"/>{current.console.result.endTime} <Divider type="vertical"/>{current.console.result.endTime}
<Divider type="vertical"/> <Divider type="vertical"/>
{!(current.console.result.success) ? <><Badge status="error"/><Text type="danger">Error</Text></> : {!(current.console.result.success) ? <><Badge status="error"/><Text type="danger">Error</Text></> :
...@@ -55,17 +71,34 @@ const StudioMsg = (props:any) => { ...@@ -55,17 +71,34 @@ const StudioMsg = (props:any) => {
</Link> <Divider type="vertical"/>{current.console.result.startTime} </Link> <Divider type="vertical"/>{current.console.result.startTime}
<Divider type="vertical"/>{current.console.result.endTime} <Divider type="vertical"/>{current.console.result.endTime}
<Divider type="vertical"/> <Divider type="vertical"/>
{!(current.console.result.status==='SUCCESS') ? <><Badge status="error"/><Text type="danger">Error</Text></> : {!(current.console.result.status === 'SUCCESS') ? <><Badge status="error"/><Text
type="danger">Error</Text></> :
<><Badge status="success"/><Text type="success">Success</Text></>} <><Badge status="success"/><Text type="success">Success</Text></>}
<Divider type="vertical"/> <Divider type="vertical"/>
{current.console.result.jobConfig?.jobName&&<Text code>{current.console.result.jobConfig?.jobName}</Text>} {current.console.result.jobConfig?.jobName && <Text code>{current.console.result.jobConfig?.jobName}</Text>}
{current.console.result.jobId&& {current.console.result.jobId &&
(<> (<>
<Divider type="vertical"/> <Divider type="vertical"/>
<Tag color="blue" key={current.console.result.jobId}> <Tag color="blue" key={current.console.result.jobId}>
<FireOutlined /> {current.console.result.jobId} <FireOutlined/> {current.console.result.jobId}
</Tag> </Tag>
</>)} </>)}
<Button
type="text"
icon={<ZoomInOutlined/>}
onClick={handleOpenSqlModal}
>
SQL
</Button>
{current.console.result.error ?
<Button
type="text"
icon={<ZoomInOutlined/>}
onClick={handleOpenErrorModal}
>
Error
</Button> : undefined
}
</blockquote> </blockquote>
{current.console.result.statement && (<pre style={{height: '100px'}}>{current.console.result.statement}</pre>)} {current.console.result.statement && (<pre style={{height: '100px'}}>{current.console.result.statement}</pre>)}
{current.console.result.error && (<pre style={{height: '100px'}}>{current.console.result.error}</pre>)} {current.console.result.error && (<pre style={{height: '100px'}}>{current.console.result.error}</pre>)}
...@@ -75,14 +108,38 @@ const StudioMsg = (props:any) => { ...@@ -75,14 +108,38 @@ const StudioMsg = (props:any) => {
return ( return (
<Typography> <>
{current?.task&&current.console.result.startTime?(isSql(current.task.dialect) ? renderCommonSqlContent(): <Typography>
renderFlinkSqlContent() ):<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} /> {current?.task && current.console.result.startTime ? (isSql(current.task.dialect) ? renderCommonSqlContent() :
} renderFlinkSqlContent()) : <Empty image={Empty.PRESENTED_IMAGE_SIMPLE}/>
</Typography> }
</Typography>
<Modal
width={'100%'}
visible={sqlModalVisit}
destroyOnClose
centered
footer={false}
onCancel={handleCancel}
>
{current.console.result.statement &&
(<CodeShow height={"80vh"} language={"sql"} code={current.console.result.statement} theme={"vs-dark"}/>)}
</Modal>
<Modal
width={'100%'}
visible={errorModalVisit}
destroyOnClose
centered
footer={false}
onCancel={handleCancel}
>
{current.console.result.error &&
(<CodeShow height={"80vh"} language={"java"} code={current.console.result.error} theme={"vs-dark"}/>)}
</Modal>
</>
); );
}; };
export default connect(({ Studio }: { Studio: StateType }) => ({ export default connect(({Studio}: { Studio: StateType }) => ({
current: Studio.current, current: Studio.current,
}))(StudioMsg); }))(StudioMsg);
...@@ -1630,6 +1630,24 @@ export default (): React.ReactNode => { ...@@ -1630,6 +1630,24 @@ export default (): React.ReactNode => {
<li> <li>
<Link>优化部署文档</Link> <Link>优化部署文档</Link>
</li> </li>
<li>
<Link>修复 yarn-application 任务分隔符错误</Link>
</li>
<li>
<Link>升级 Flink 1.15 版本为 1.15.2</Link>
</li>
<li>
<Link>优化 SqlServer 字段类型查询</Link>
</li>
<li>
<Link>修复重命名作业后保存作业失败</Link>
</li>
<li>
<Link>修复提交历史的第二次弹框时无内容</Link>
</li>
<li>
<Link>新增数据开发任务信息日志详情按钮</Link>
</li>
</ul> </ul>
</Paragraph> </Paragraph>
</Timeline.Item> </Timeline.Item>
......
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