/** * 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 = ({ visible, onCancel }) => { const customService = useService(CustomService); const { nodeId: sidebarNodeId, setNodeId } = useContext(SidebarContext); const [isRunning, setRunning] = useState(false); const [values, setValues] = useState>({}); const [errors, setErrors] = useState(); 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(); }; // 当测试运行面板打开时,自动关闭右侧节点编辑侧栏,避免两个 SideSheet 重叠 useEffect(() => { if (visible) { setNodeId(undefined); } }, [visible]); useEffect(() => { if (sidebarNodeId) { onCancel(); } }, [sidebarNodeId]); const renderRunning = (
{I18n.t('Running...')}
); const renderStatus = (
{result?.ok === true && {I18n.t('Success')}} {(errors?.length || result?.ok === false) && ( {I18n.t('Failed')} )}
); const renderForm = (
{I18n.t('Input Form')}
{I18n.t('JSON Mode')}
setInputJSONMode(checked)} size="small" />
{renderStatus} {errors?.map((e) => (
{e}
))} {inputJSONMode ? ( ) : ( )} {/* 展示后端返回的执行信息 */}
); const renderButton = ( ); return (
{I18n.t('Test Run')}
{isRunning ? renderRunning : renderForm}
{renderButton}
); };