Skip to content

Commit ac4feaf

Browse files
committed
[frontend] 增加应用编排回退到历史版本功能
1 parent debfc62 commit ac4feaf

34 files changed

+396
-152
lines changed

app-engine/frontend/src/components/timeLine/index.tsx

Lines changed: 184 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -4,69 +4,213 @@
44
* Licensed under the MIT License. See License.txt in the project root for license information.
55
*--------------------------------------------------------------------------------------------*/
66

7-
import React, { useEffect, useState } from 'react';
8-
import { Drawer, Timeline, Empty } from 'antd';
7+
import React, { useContext, useEffect, useRef, useState } from 'react';
8+
import { Button, Drawer, Timeline } from 'antd';
99
import { useParams } from 'react-router-dom';
1010
import { CloseOutlined } from '@ant-design/icons';
11-
import { getVersion } from '@/shared/http/aipp';
12-
import { useTranslation } from "react-i18next";
13-
import tagImg from '@/assets/images/ai/tag.png';
11+
import { Message } from '@/shared/utils/message';
12+
import { getAppInfo, getVersion, resetApp } from '@/shared/http/aipp';
13+
import { useTranslation } from 'react-i18next';
14+
import { useAppDispatch } from '@/store/hook';
15+
import { setIsReadOnly } from '@/store/common/common';
16+
import { RenderContext } from '@/pages/aippIndex/context';
17+
18+
const PAGE_SIZE = 10;
1419

1520
const TimeLineFc = (props) => {
16-
const { open, setOpen, type = '' } = props;
21+
const { open, setOpen, type = '', updateAippCallBack } = props;
1722
const [timeList, setTimeList] = useState([]);
1823
const { tenantId, appId } = useParams();
1924
const { t } = useTranslation();
25+
const [selectedAppId, setSelectedAppId] = useState(appId);
26+
const dispatch = useAppDispatch();
27+
const [page, setPage] = useState(1);
28+
const [loading, setLoading] = useState(false);
29+
const scrollRef = useRef();
30+
const hasMoreRef = useRef<any>(true);
31+
const currentAppInfo = useRef<any>(null);
32+
const { renderRef, elsaReadOnlyRef } = useContext(RenderContext);
2033

21-
useEffect(() => {
22-
open && getVersion(tenantId, appId, type).then(res => {
34+
const fetchData = async (currentPage: number) => {
35+
if (!open || loading || !hasMoreRef.current) return;
36+
setLoading(true);
37+
try {
38+
const res = await getVersion(tenantId, appId, type, PAGE_SIZE * (currentPage - 1), PAGE_SIZE);
2339
if (res.code === 0) {
24-
setTimeList(res.data);
40+
const newItems = res.data.results || [];
41+
setTimeList((prev) => {
42+
const newIds = new Set(prev.map((item) => item.id));
43+
const filteredNewItems = newItems.filter((item) => !newIds.has(item.id));
44+
return [...prev, ...filteredNewItems];
45+
});
46+
if (newItems.length < PAGE_SIZE) {
47+
hasMoreRef.current = false;
48+
} else {
49+
setPage(currentPage + 1);
50+
}
2551
}
26-
})
52+
} finally {
53+
setLoading(false);
54+
}
55+
};
56+
57+
const getCurrentApp = async (tenantId: string, appId: string) => {
58+
const res: any = await getAppInfo(tenantId, appId);
59+
if (res.code === 0) {
60+
currentAppInfo.current = res.data;
61+
}
62+
};
63+
64+
useEffect(() => {
65+
console.log(open)
66+
if (open) {
67+
dispatch(setIsReadOnly(true));
68+
setTimeList([]);
69+
setPage(1);
70+
hasMoreRef.current = true;
71+
window.agent?.readOnly();
72+
73+
Promise.all([
74+
getCurrentApp(tenantId, appId), // 刷新当前应用数据
75+
fetchData(1) // 加载历史版本
76+
]).catch(console.error);
77+
}
2778
}, [open]);
28-
const descProcess = (str) => {
29-
if (!str || str === 'null') {
30-
return '';
79+
80+
useEffect(() => {
81+
const handleScroll = () => {
82+
const container = scrollRef.current;
83+
if (!container) return;
84+
const { scrollTop, scrollHeight, clientHeight } = container;
85+
if (scrollTop + clientHeight >= scrollHeight - 50) {
86+
fetchData(page);
87+
}
88+
};
89+
90+
const container = scrollRef.current;
91+
container?.addEventListener('scroll', handleScroll);
92+
return () => container?.removeEventListener('scroll', handleScroll);
93+
}, [page, open, hasMoreRef.current]);
94+
95+
96+
const descProcess = (str) => (!str || str === 'null' ? '' : str);
97+
98+
const handleRecover = () => {
99+
if (appId !== selectedAppId) {
100+
resetApp(tenantId, appId, selectedAppId, {
101+
'Content-Type': 'application/json'
102+
}).then(res => {
103+
if (res.code === 0) {
104+
Message({ type: 'success', content: t('resetSucceed') });
105+
currentAppInfo.current = res.data;
106+
handleClose();
107+
}
108+
});
31109
}
32-
return str;
110+
};
111+
112+
const handleItemClick = (timeItem) => {
113+
setSelectedAppId(timeItem.id);
114+
updateAippCallBack(
115+
{
116+
flowGraph: timeItem.flowGraph,
117+
configFormProperties: timeItem.configFormProperties
118+
}
119+
);
120+
renderRef.current = false;
121+
elsaReadOnlyRef.current = true;
122+
};
123+
124+
const handleClose = () => {
125+
dispatch(setIsReadOnly(false));
126+
setSelectedAppId(appId);
127+
setTimeList([]);
128+
setPage(1);
129+
hasMoreRef.current = true;
130+
updateAippCallBack(currentAppInfo.current);
131+
renderRef.current = false;
132+
elsaReadOnlyRef.current = false;
133+
setOpen(false);
33134
}
34-
return <>
135+
136+
useEffect(() => {
137+
return () => {
138+
// 组件卸载时自动重置
139+
dispatch(setIsReadOnly(false));
140+
};
141+
}, [dispatch]);
142+
143+
return (
35144
<Drawer
36145
title={t('publishHistory')}
37146
placement='right'
38147
width='420px'
39148
closeIcon={false}
40-
onClose={() => setOpen(false)}
149+
onClose={handleClose}
41150
open={open}
42-
footer={null}
43-
extra={
44-
<CloseOutlined onClick={() => setOpen(false)} />
45-
}>
46-
<div>
47-
<div style={{ marginBottom: '18px', display: 'flex', alignItems: 'center' }}>
48-
<img src={tagImg} />
49-
<span style={{ marginLeft: '12px' }}>{t('cannotRevertVersion')}</span>
151+
mask={false}
152+
footer={
153+
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '12px', padding: '16px 0' }}>
154+
<Button onClick={handleClose}>{t('exit')}</Button>
155+
<Button type="primary" onClick={handleRecover} disabled={appId === selectedAppId}>{t('recover')}</Button>
50156
</div>
51-
{timeList.length > 0 ?
157+
}
158+
extra={<CloseOutlined onClick={handleClose} />}
159+
>
160+
<div ref={scrollRef} style={{ maxHeight: '750px', overflowY: 'auto', paddingRight: '8px' }}>
161+
{timeList.length > 0 ? (
52162
<Timeline>
53-
{ timeList.map(timeItem => (
54-
<Timeline.Item color='#000000'>
55-
<div className="time-line-inner" style={{ color: 'rgb(77, 77, 77)' }}>
56-
<div style={{ fontWeight: '700' }}>{timeItem.appVersion}</div>
57-
<div style={{ margin: '8px 0' }}>{descProcess(timeItem.publishedDescription)}</div>
58-
<div>{timeItem.publishedBy}</div>
59-
<div>{timeItem.publishedAt}</div>
60-
</div>
61-
</Timeline.Item>
62-
)) }
63-
</Timeline> :
64-
<div style={{ marginTop: '300px' }}><Empty description={t('noData')} /></div>
65-
}
163+
<Timeline.Item
164+
color={appId === selectedAppId ? 'blue' : '#000000'}
165+
key={appId}
166+
>
167+
<div
168+
className="time-line-inner"
169+
style={{
170+
color: appId === selectedAppId ? '#1677ff' : 'rgb(77, 77, 77)',
171+
backgroundColor: appId === selectedAppId ? '#e6f7ff' : 'transparent',
172+
borderRadius: '4px',
173+
padding: '8px',
174+
cursor: 'pointer',
175+
}}
176+
onClick={() => handleItemClick(currentAppInfo.current)}
177+
>
178+
<div style={{ fontWeight: '700' }}>{t('currentDraft')}</div>
179+
</div>
180+
</Timeline.Item>
181+
{timeList.map(timeItem => {
182+
const isSelected = timeItem.id === selectedAppId;
183+
return (
184+
<Timeline.Item
185+
color={isSelected ? 'blue' : '#000000'}
186+
key={timeItem.id}
187+
>
188+
<div
189+
className="time-line-inner"
190+
style={{
191+
color: isSelected ? '#1677ff' : 'rgb(77, 77, 77)',
192+
backgroundColor: isSelected ? '#e6f7ff' : 'transparent',
193+
borderRadius: '4px',
194+
padding: '8px',
195+
cursor: 'pointer',
196+
}}
197+
onClick={() => handleItemClick(timeItem)}
198+
>
199+
<div style={{ fontWeight: '700' }}>{timeItem.version}</div>
200+
<div style={{ margin: '8px 0' }}>{descProcess(timeItem.publishedDescription)}</div>
201+
<div>{timeItem.updateBy}</div>
202+
<div>{timeItem.updateAt}</div>
203+
</div>
204+
</Timeline.Item>
205+
);
206+
})}
207+
{loading && <div style={{ textAlign: 'center', padding: '12px' }}>{t('loading')}...</div>}
208+
{!hasMoreRef.current && <div style={{ textAlign: 'center', color: '#888' }}>{t('noMore')}</div>}
209+
</Timeline>
210+
) : null}
66211
</div>
67212
</Drawer>
68-
</>
213+
);
69214
};
70215

71-
72216
export default TimeLineFc;

app-engine/frontend/src/locale/en_US.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@
223223
"gotIt": "Got It",
224224
"successReleased": "Application released",
225225
"versionTip": "Invalid version format",
226-
"releaseTip": "The version to be released will overwrite the historical version and cannot be rolled back",
226+
"releaseTip": "The version to be released will overwrite the historical version",
227227
"releaseApplication": "Release Application",
228228
"releaseTip2": "Release the application only after it passes the debugging",
229229
"versionName": "Version",
@@ -952,5 +952,11 @@
952952
"zoomOut": "Zoom Out",
953953
"zoomIn": "Zoom In",
954954
"formItemFieldTypeCannotBeEmpty": "Field type is required",
955-
"addParallelTask": "Add Parallel Tasks"
955+
"addParallelTask": "Add Parallel Tasks",
956+
"recover": "recover",
957+
"exit": "exit",
958+
"resetSucceed": "Reset Succeed",
959+
"currentDraft": "Current Draft",
960+
"loading": "Loading",
961+
"noMore": "You've reached the end"
956962
}

app-engine/frontend/src/locale/zh_CN.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@
306306
"successReleased": "发布应用成功",
307307
"successReleased2": "发布工具流成功",
308308
"releaseApplication": "发布应用",
309-
"releaseTip": "新版本将覆盖历史版本,并不可回退",
309+
"releaseTip": "新版本将覆盖历史版本",
310310
"releaseTip2": "请调试应用,确认无误后发布",
311311
"releaseToolFlow": "发布工具流",
312312
"versionName": "版本名称",
@@ -952,5 +952,11 @@
952952
"zoomOut": "缩小",
953953
"zoomIn": "放大",
954954
"formItemFieldTypeCannotBeEmpty": "表单项类型不能为空",
955-
"addParallelTask": "添加并行任务"
955+
"addParallelTask": "添加并行任务",
956+
"recover": "恢复",
957+
"exit": "退出",
958+
"resetSucceed": "恢复应用成功",
959+
"currentDraft": "当前草稿",
960+
"loading": "加载中",
961+
"noMore": "已显示全部"
956962
}

app-engine/frontend/src/pages/addFlow/components/addflow-header.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import timeImg from '@/assets/images/ai/time.png';
3737
const AddHeader = (props) => {
3838
const dispatch = useAppDispatch();
3939
const { t } = useTranslation();
40-
const { handleDebugClick, workFlow, types, saveTime } = props;
40+
const { handleDebugClick, workFlow, types, saveTime, updateAippCallBack } = props;
4141
const { appInfo, setFlowInfo } = useContext(FlowContext);
4242
const [open, setOpen] = useState(false);
4343
const [imgPath, setImgPath] = useState('');
@@ -162,7 +162,12 @@ const AddHeader = (props) => {
162162
appInfo={appInfo}
163163
/>
164164
{/* 工具流发布历史信息弹窗 */}
165-
<TimeLineDrawer open={open} setOpen={setOpen} type='waterflow' />
165+
<TimeLineDrawer
166+
open={open}
167+
setOpen={setOpen}
168+
updateAippCallBack ={updateAippCallBack }
169+
workflow={workFlow} type='waterflow'
170+
/>
166171
</div>
167172
)}</>
168173
};

app-engine/frontend/src/pages/addFlow/components/elsa-stage.tsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { getAddFlowConfig, getEvaluateConfig } from '@/shared/http/appBuilder';
2222
import { useAppDispatch, useAppSelector } from '@/store/hook';
2323
import { setAppInfo, setValidateInfo } from '@/store/appInfo/appInfo';
2424
import { setTestStatus, setTestTime } from '@/store/flowTest/flowTest';
25-
import { FlowContext } from '../../aippIndex/context';
25+
import { FlowContext, RenderContext } from '../../aippIndex/context';
2626
import CreateTestSet from '../../appDetail/evaluate/testSet/createTestset/createTestSet';
2727
import AddSearch from '../../configForm/configUi/components/add-search';
2828
import { configMap } from '../config';
@@ -68,6 +68,7 @@ const Stage = (props) => {
6868
const [groupId, setGroupId] = useState("");
6969
const { CONFIGS } = configMap[process.env.NODE_ENV];
7070
const { type, appInfo, setFlowInfo } = useContext(FlowContext);
71+
const { renderRef, elsaReadOnlyRef } = useContext(RenderContext);
7172
const testStatus = useAppSelector((state) => state.flowTestStore.testStatus);
7273
const appValidateInfo = useAppSelector((state) => state.appStore.validateInfo);
7374
const choseNodeId = useAppSelector((state) => state.appStore.choseNodeId);
@@ -78,7 +79,6 @@ const Stage = (props) => {
7879
const pluginCallback = useRef<any>();
7980
const formCallback = useRef<any>();
8081
const currentApp = useRef<any>();
81-
const render = useRef<any>(false);
8282
const currentChange = useRef<any>(false);
8383
const modalRef = useRef<any>();
8484
const openModalRef = useRef<any>();
@@ -90,16 +90,16 @@ const Stage = (props) => {
9090
const connectKnowledgeEvent = useRef<any>();
9191
const dispatch = useAppDispatch();
9292
useEffect(() => {
93-
if (appInfo.name && !render.current) {
94-
render.current = true;
93+
if (appInfo.name && !renderRef.current) {
94+
renderRef.current = true;
9595
currentApp.current = JSON.parse(JSON.stringify(appInfo));
9696
window.agent = null;
97-
setElsaData();
97+
setElsaData(elsaReadOnlyRef.current);
9898
}
9999
}, [appInfo]);
100100
useEffect(() => {
101101
return () => {
102-
render.current = false;
102+
renderRef.current = false;
103103
window.agent = null;
104104
dispatch(setTestStatus(null));
105105
}
@@ -112,7 +112,7 @@ const Stage = (props) => {
112112
}
113113
const realAppId = getQueryString('appId');
114114
// 编辑工作流
115-
function setElsaData() {
115+
function setElsaData(readOnly: boolean) {
116116
let graphData = appInfo.flowGraph?.appearance || {};
117117
const stageDom = document.getElementById('stage');
118118
let data = JSON.parse(JSON.stringify(graphData));
@@ -224,6 +224,9 @@ const Stage = (props) => {
224224
setShowTools(true);
225225
setModalTypes('parallel');
226226
});
227+
if (readOnly) {
228+
agent.readOnly();
229+
}
227230
}).catch(() => {
228231
setSpinning && setSpinning(false);
229232
});

0 commit comments

Comments
 (0)