feat(flows): 新增流程编辑器基础功能与相关组件
feat(backend): 添加流程模型与服务支持 feat(frontend): 实现流程编辑器UI与交互 feat(assets): 添加流程节点图标资源 feat(plugins): 实现上下文菜单和运行时插件 feat(components): 新增基础节点和侧边栏组件 feat(routes): 添加流程相关路由配置 feat(models): 创建流程和运行日志数据模型 feat(services): 实现流程服务层逻辑 feat(migration): 添加流程相关数据库迁移 feat(config): 更新前端配置支持流程编辑器 feat(utils): 增强axios错误处理和工具函数
This commit is contained in:
211
frontend/src/flows/components/testrun/testrun-panel/index.tsx
Normal file
211
frontend/src/flows/components/testrun/testrun-panel/index.tsx
Normal file
@ -0,0 +1,211 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { FC, useContext, useEffect, useState } from 'react';
|
||||
|
||||
import classnames from 'classnames';
|
||||
import { useService, I18n } from '@flowgram.ai/free-layout-editor';
|
||||
import { Button, SideSheet, Switch, Tag } from '@douyinfe/semi-ui';
|
||||
import { IconClose, IconPlay, IconSpin } from '@douyinfe/semi-icons';
|
||||
|
||||
import { TestRunJsonInput } from '../testrun-json-input';
|
||||
import { TestRunForm } from '../testrun-form';
|
||||
import { NodeStatusGroup } from '../node-status-bar/group';
|
||||
// 改为使用后端运行服务
|
||||
import { CustomService } from '../../../services';
|
||||
import { SidebarContext } from '../../../context';
|
||||
import { IconCancel } from '../../../assets/icon-cancel';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
interface TestRunSidePanelProps {
|
||||
visible: boolean;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export const TestRunSidePanel: FC<TestRunSidePanelProps> = ({ visible, onCancel }) => {
|
||||
const customService = useService(CustomService);
|
||||
const { nodeId: sidebarNodeId, setNodeId } = useContext(SidebarContext);
|
||||
|
||||
const [isRunning, setRunning] = useState(false);
|
||||
const [values, setValues] = useState<Record<string, unknown>>({});
|
||||
const [errors, setErrors] = useState<string[]>();
|
||||
const [result, setResult] = useState<
|
||||
| {
|
||||
ok?: boolean;
|
||||
ctx: any;
|
||||
logs: string[];
|
||||
}
|
||||
| undefined
|
||||
>();
|
||||
|
||||
// en - Use localStorage to persist the JSON mode state
|
||||
const [inputJSONMode, _setInputJSONMode] = useState(() => {
|
||||
const savedMode = localStorage.getItem('testrun-input-json-mode');
|
||||
return savedMode ? JSON.parse(savedMode) : false;
|
||||
});
|
||||
|
||||
const setInputJSONMode = (checked: boolean) => {
|
||||
_setInputJSONMode(checked);
|
||||
localStorage.setItem('testrun-input-json-mode', JSON.stringify(checked));
|
||||
};
|
||||
|
||||
const extractErrorMsg = (logs: string[] | undefined): string | undefined => {
|
||||
if (!logs || logs.length === 0) return undefined;
|
||||
const patterns = [/failed/i, /error/i, /panic/i];
|
||||
for (const line of logs) {
|
||||
if (patterns.some((p) => p.test(line))) return line;
|
||||
}
|
||||
return logs[logs.length - 1];
|
||||
};
|
||||
|
||||
const onTestRun = async () => {
|
||||
if (isRunning) {
|
||||
// 后端运行不可取消,这里直接忽略重复点击
|
||||
return;
|
||||
}
|
||||
setResult(undefined);
|
||||
setErrors(undefined);
|
||||
setRunning(true);
|
||||
try {
|
||||
// 运行前保存(静默),确保后端 YAML 与编辑器一致;若保存失败则不继续运行
|
||||
const saved = await customService.save({ silent: true });
|
||||
if (!saved) {
|
||||
setErrors([I18n.t('Save failed, cannot run')]);
|
||||
return;
|
||||
}
|
||||
const runRes = await customService.run(values);
|
||||
if (runRes) {
|
||||
// 若后端返回 ok=false,则视为失败并展示失败信息与日志
|
||||
if ((runRes as any).ok === false) {
|
||||
setResult(runRes as any);
|
||||
const err = extractErrorMsg((runRes as any).logs) || I18n.t('Run failed');
|
||||
setErrors([err]);
|
||||
} else {
|
||||
setResult(runRes as any);
|
||||
}
|
||||
} else {
|
||||
setErrors([I18n.t('Run failed')]);
|
||||
}
|
||||
} catch (e: any) {
|
||||
setErrors([e?.message || I18n.t('Run failed')]);
|
||||
} finally {
|
||||
setRunning(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onClose = async () => {
|
||||
setValues({});
|
||||
setRunning(false);
|
||||
onCancel();
|
||||
};
|
||||
|
||||
// sidebar effect
|
||||
useEffect(() => {
|
||||
setNodeId(undefined);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (sidebarNodeId) {
|
||||
onCancel();
|
||||
}
|
||||
}, [sidebarNodeId]);
|
||||
|
||||
const renderRunning = (
|
||||
<div className={styles['testrun-panel-running']}>
|
||||
<IconSpin spin size="large" />
|
||||
<div className={styles.text}>{I18n.t('Running...')}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderStatus = (
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
{result?.ok === true && <Tag color="green">{I18n.t('Success')}</Tag>}
|
||||
{(errors?.length || result?.ok === false) && (
|
||||
<Tag color="red">{I18n.t('Failed')}</Tag>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderForm = (
|
||||
<div className={styles['testrun-panel-form']}>
|
||||
<div className={styles['testrun-panel-input']}>
|
||||
<div className={styles.title}>{I18n.t('Input Form')}</div>
|
||||
<div>{I18n.t('JSON Mode')}</div>
|
||||
<Switch
|
||||
checked={inputJSONMode}
|
||||
onChange={(checked: boolean) => setInputJSONMode(checked)}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
{renderStatus}
|
||||
{errors?.map((e) => (
|
||||
<div className={styles.error} key={e}>
|
||||
{e}
|
||||
</div>
|
||||
))}
|
||||
{inputJSONMode ? (
|
||||
<TestRunJsonInput values={values} setValues={setValues} />
|
||||
) : (
|
||||
<TestRunForm values={values} setValues={setValues} />
|
||||
)}
|
||||
{/* 展示后端返回的执行信息 */}
|
||||
<NodeStatusGroup title={I18n.t('Context')} data={result?.ctx} optional disableCollapse />
|
||||
<NodeStatusGroup title={I18n.t('Logs')} data={result?.logs} optional disableCollapse />
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderButton = (
|
||||
<Button
|
||||
onClick={onTestRun}
|
||||
icon={isRunning ? <IconCancel /> : <IconPlay size="small" />}
|
||||
className={classnames(styles.button, {
|
||||
[styles.running]: isRunning,
|
||||
[styles.default]: !isRunning,
|
||||
})}
|
||||
>
|
||||
{isRunning ? I18n.t('Running...') : I18n.t('Test Run')}
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<SideSheet
|
||||
title={I18n.t('Test Run')}
|
||||
visible={visible}
|
||||
mask={false}
|
||||
motion={false}
|
||||
onCancel={onClose}
|
||||
width={400}
|
||||
headerStyle={{
|
||||
display: 'none',
|
||||
}}
|
||||
bodyStyle={{
|
||||
padding: 0,
|
||||
}}
|
||||
style={{
|
||||
background: 'none',
|
||||
boxShadow: 'none',
|
||||
}}
|
||||
>
|
||||
<div className={styles['testrun-panel-container']}>
|
||||
<div className={styles['testrun-panel-header']}>
|
||||
<div className={styles['testrun-panel-title']}>{I18n.t('Test Run')}</div>
|
||||
<Button
|
||||
className={styles['testrun-panel-title']}
|
||||
type="tertiary"
|
||||
icon={<IconClose />}
|
||||
size="small"
|
||||
theme="borderless"
|
||||
onClick={onClose}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles['testrun-panel-content']}>
|
||||
{isRunning ? renderRunning : renderForm}
|
||||
</div>
|
||||
<div className={styles['testrun-panel-footer']}>{renderButton}</div>
|
||||
</div>
|
||||
</SideSheet>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user