Files
memind_adm/src/admin/pages/SystemTestsPage.tsx
T

422 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useCallback, useEffect, useMemo, useState } from 'react';
import {
createSystemTestAccount,
deleteSystemTestAccount,
listSkillCatalog,
listSystemTestAccounts,
runSystemSkillValidation,
} from '../../api/client';
import type { AdminSystemTestAccount, AdminSystemTestReport, SkillDefinition } from '../../types';
import { formatTime } from '../utils/format';
const DEFAULT_SKILL = 'service-integration-smoke';
function statusLabel(status: 'passed' | 'warning' | 'failed') {
if (status === 'passed') return '通过';
if (status === 'warning') return '待处理';
return '失败';
}
function AddAccountModal({
onClose,
onSave,
}: {
onClose: () => void;
onSave: (payload: { label: string; username: string; password: string }) => Promise<void>;
}) {
const [label, setLabel] = useState('');
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault();
const normalizedUsername = username.trim();
const normalizedLabel = label.trim() || normalizedUsername;
if (!normalizedUsername || !password) return;
setSaving(true);
setError(null);
try {
await onSave({
label: normalizedLabel,
username: normalizedUsername,
password,
});
} catch (err) {
setError(err instanceof Error ? err.message : '保存测试账号失败');
} finally {
setSaving(false);
}
};
return (
<div className="modal-backdrop" onClick={(event) => event.target === event.currentTarget && onClose()}>
<div className="modal-box" style={{ maxWidth: 520 }}>
<div className="modal-head">
<div>
<h3></h3>
<p className="muted"></p>
</div>
<button type="button" className="modal-close" onClick={onClose}>×</button>
</div>
{error && <p className="banner banner-error">{error}</p>}
<form className="admin-form plan-form" onSubmit={handleSubmit}>
<label className="plan-form-row">
<span></span>
<input
type="text"
value={label}
onChange={(event) => setLabel(event.target.value)}
placeholder="例如 John 测试号"
/>
</label>
<label className="plan-form-row">
<span></span>
<input
type="text"
value={username}
onChange={(event) => setUsername(event.target.value)}
placeholder="例如 john"
autoComplete="username"
required
/>
</label>
<label className="plan-form-row">
<span></span>
<input
type="password"
value={password}
onChange={(event) => setPassword(event.target.value)}
placeholder="输入该账号密码"
autoComplete="current-password"
required
/>
</label>
<div className="plan-form-actions">
<button type="button" className="admin-btn-secondary" onClick={onClose} disabled={saving}></button>
<button type="submit" className="send-btn" disabled={saving || !username.trim() || !password}>
{saving ? '保存中…' : '保存账号'}
</button>
</div>
</form>
</div>
</div>
);
}
export function SystemTestsPage() {
const [catalog, setCatalog] = useState<SkillDefinition[]>([]);
const [skillName, setSkillName] = useState(DEFAULT_SKILL);
const [accounts, setAccounts] = useState<AdminSystemTestAccount[]>([]);
const [selectedAccountId, setSelectedAccountId] = useState('');
const [loading, setLoading] = useState(true);
const [running, setRunning] = useState(false);
const [accountsLoading, setAccountsLoading] = useState(true);
const [accountBusyId, setAccountBusyId] = useState<string | null>(null);
const [showAddAccount, setShowAddAccount] = useState(false);
const [error, setError] = useState<string | null>(null);
const [report, setReport] = useState<AdminSystemTestReport | null>(null);
const loadCatalog = useCallback(async () => {
setLoading(true);
setError(null);
try {
const nextCatalog = await listSkillCatalog();
setCatalog(nextCatalog);
if (nextCatalog.some((item) => item.name === DEFAULT_SKILL)) {
setSkillName(DEFAULT_SKILL);
} else if (nextCatalog[0]?.name) {
setSkillName(nextCatalog[0].name);
}
} catch (err) {
setError(err instanceof Error ? err.message : '加载 skill 列表失败');
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
void loadCatalog();
}, [loadCatalog]);
const loadAccounts = useCallback(async () => {
setAccountsLoading(true);
try {
const nextAccounts = await listSystemTestAccounts();
setAccounts(nextAccounts);
setSelectedAccountId((current) =>
nextAccounts.some((item) => item.id === current) ? current : (nextAccounts[0]?.id ?? ''),
);
} catch (err) {
setError(err instanceof Error ? err.message : '加载测试账号失败');
} finally {
setAccountsLoading(false);
}
}, []);
useEffect(() => {
void loadAccounts();
}, [loadAccounts]);
const selectedSkill = useMemo(
() => catalog.find((item) => item.name === skillName) ?? null,
[catalog, skillName],
);
const selectedAccount = useMemo(
() => accounts.find((item) => item.id === selectedAccountId) ?? null,
[accounts, selectedAccountId],
);
const handleSaveAccount = async (payload: { label: string; username: string; password: string }) => {
const account = await createSystemTestAccount(payload);
setAccounts((current) => [account, ...current]);
setSelectedAccountId(account.id);
setShowAddAccount(false);
};
const handleRemoveAccount = async () => {
if (!selectedAccount) return;
setAccountBusyId(selectedAccount.id);
setError(null);
try {
await deleteSystemTestAccount(selectedAccount.id);
const nextAccounts = accounts.filter((item) => item.id !== selectedAccount.id);
setAccounts(nextAccounts);
setSelectedAccountId(nextAccounts[0]?.id ?? '');
} catch (err) {
setError(err instanceof Error ? err.message : '删除测试账号失败');
} finally {
setAccountBusyId(null);
}
};
const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault();
if (!selectedAccount) {
setError('请先选择一个测试账号,或先添加账号');
return;
}
setRunning(true);
setError(null);
try {
const nextReport = await runSystemSkillValidation({
accountId: selectedAccount.id,
username: '',
password: '',
skillName,
});
setReport(nextReport);
} catch (err) {
setError(err instanceof Error ? err.message : '系统测试执行失败');
} finally {
setRunning(false);
}
};
return (
<div className="admin-page">
<div className="admin-page-head">
<h2></h2>
<p className="muted"> skill </p>
</div>
{error && <p className="banner banner-error">{error}</p>}
<section className="admin-card">
<div className="admin-card-head">
<div>
<h2></h2>
<p className="muted">使 service-integration-smoke skill </p>
</div>
</div>
{loading ? (
<p className="muted"> skill </p>
) : (
<form className="admin-form" onSubmit={handleSubmit}>
<label>
<span> Skill</span>
<select value={skillName} onChange={(e) => setSkillName(e.target.value)} className="admin-select">
{catalog.map((item) => (
<option key={item.name} value={item.name}>
{item.label}
</option>
))}
</select>
</label>
<div className="form-span-all system-test-account-panel">
<label className="system-test-account-select">
<span></span>
<select
value={selectedAccountId}
onChange={(event) => setSelectedAccountId(event.target.value)}
className="admin-select"
disabled={accounts.length === 0 || accountsLoading}
>
{accountsLoading ? (
<option value=""></option>
) : accounts.length === 0 ? (
<option value=""></option>
) : (
accounts.map((account) => (
<option key={account.id} value={account.id}>
{account.label} ({account.username})
</option>
))
)}
</select>
</label>
<div className="system-test-account-actions">
<button type="button" className="admin-btn-secondary" onClick={() => setShowAddAccount(true)}>
+
</button>
<button
type="button"
className="admin-btn-secondary"
onClick={() => void handleRemoveAccount()}
disabled={!selectedAccount || accountBusyId === selectedAccount?.id}
>
{accountBusyId === selectedAccount?.id ? '删除中…' : '删除当前账号'}
</button>
<button type="button" className="admin-btn-secondary" onClick={() => void loadAccounts()} disabled={accountsLoading}>
{accountsLoading ? '刷新中…' : '刷新账号'}
</button>
</div>
{selectedAccount ? (
<div className="system-test-account-summary muted">
使: <strong>{selectedAccount.label}</strong> · @{selectedAccount.username} · {selectedAccount.passwordMasked}
</div>
) : (
<div className="system-test-account-summary muted">
</div>
)}
</div>
{selectedSkill && (
<p className="muted">
skill: <strong>{selectedSkill.label}</strong> · {selectedSkill.description}
</p>
)}
<button type="submit" className="send-btn" disabled={running || loading}>
{running ? '验证执行中…' : '开始验证'}
</button>
</form>
)}
</section>
{showAddAccount && (
<AddAccountModal
onClose={() => setShowAddAccount(false)}
onSave={handleSaveAccount}
/>
)}
{report && (
<>
<section className="admin-card">
<div className="admin-card-head">
<div>
<h2></h2>
<p className="muted">
@{report.account.username} · skill {report.selectedSkill} · Portal {report.portalBaseUrl}
</p>
</div>
</div>
<div className="admin-stat-grid">
<div className="admin-stat-card">
<div className="admin-stat-label"></div>
<div className="admin-stat-value">{report.ok ? '通过' : '有问题'}</div>
<div className="muted">
{formatTime(report.startedAt)} / {formatTime(report.finishedAt)}
</div>
</div>
<div className="admin-stat-card">
<div className="admin-stat-label"></div>
<div className="admin-stat-value">{report.summary.passed}</div>
</div>
<div className="admin-stat-card">
<div className="admin-stat-label"></div>
<div className="admin-stat-value">{report.summary.warnings}</div>
</div>
<div className="admin-stat-card">
<div className="admin-stat-label"></div>
<div className="admin-stat-value">{report.summary.failed}</div>
</div>
</div>
</section>
<section className="admin-card">
<div className="admin-card-head">
<h2></h2>
</div>
<div className="admin-table-wrap">
<table className="admin-table">
<thead>
<tr>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{report.steps.map((step) => (
<tr key={step.key}>
<td>{step.label}</td>
<td>{statusLabel(step.status)}</td>
<td>
<div>{step.message}</div>
{step.details && (
<details style={{ marginTop: 8 }}>
<summary className="muted"></summary>
<pre style={{ whiteSpace: 'pre-wrap', marginTop: 8 }}>
{JSON.stringify(step.details, null, 2)}
</pre>
</details>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
<section className="admin-card">
<div className="admin-card-head">
<h2></h2>
</div>
{report.issues.length === 0 ? (
<p className="muted"></p>
) : (
<div className="admin-table-wrap">
<table className="admin-table">
<thead>
<tr>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{report.issues.map((issue, index) => (
<tr key={`${issue.stepKey}-${index}`}>
<td>{issue.severity === 'error' ? '错误' : '提醒'}</td>
<td>{issue.stepKey}</td>
<td>
<div>{issue.title}</div>
<div className="muted" style={{ marginTop: 6 }}>{issue.detail}</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</section>
</>
)}
</div>
);
}