feat(plugin): P2-P4 插件平台演进 — 通用服务 + 质量保障 + 市场
P2 平台通用服务: - manifest 扩展: settings/numbering/templates/trigger_events/importable/exportable 声明 - 插件配置 UI: PluginSettingsForm 自动表单 + 后端校验 + 详情抽屉 Settings 标签页 - 编号规则: Host API numbering-generate + PostgreSQL 序列 + manifest 绑定 - 触发事件: data_service create/update/delete 自动发布 DomainEvent - WIT 接口: 新增 numbering-generate/setting-get Host API P3 质量保障: - plugin_validator.rs: 安全扫描(WASM大小/实体数量/字段校验) + 复杂度评分 - 运行时监控指标: RuntimeMetrics (错误率/响应时间/Fuel/内存) - 性能基准: BenchmarkResult 阈值定义 - 上传时自动安全扫描 + /validate API 端点 P4 插件市场: - 数据库迁移: plugin_market_entries + plugin_market_reviews 表 - 前端 PluginMarket 页面: 分类浏览/搜索/详情/评分 - 路由注册: /plugins/market 测试: 269 全通过 (71 erp-plugin + 41 auth + 57 config + 34 core + 50 message + 16 workflow)
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
Descriptions,
|
||||
Popconfirm,
|
||||
Form,
|
||||
Tabs,
|
||||
theme,
|
||||
} from 'antd';
|
||||
import {
|
||||
@@ -22,8 +23,9 @@ import {
|
||||
DeleteOutlined,
|
||||
ReloadOutlined,
|
||||
HeartOutlined,
|
||||
SettingOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { PluginInfo, PluginStatus } from '../api/plugins';
|
||||
import type { PluginInfo, PluginStatus, PluginSchemaResponse } from '../api/plugins';
|
||||
import {
|
||||
listPlugins,
|
||||
uploadPlugin,
|
||||
@@ -33,7 +35,10 @@ import {
|
||||
uninstallPlugin,
|
||||
purgePlugin,
|
||||
getPluginHealth,
|
||||
getPluginSchema,
|
||||
updatePluginConfig,
|
||||
} from '../api/plugins';
|
||||
import PluginSettingsForm from '../components/PluginSettingsForm';
|
||||
|
||||
const STATUS_CONFIG: Record<PluginStatus, { color: string; label: string }> = {
|
||||
uploaded: { color: '#64748B', label: '已上传' },
|
||||
@@ -53,6 +58,7 @@ export default function PluginAdmin() {
|
||||
const [manifestText, setManifestText] = useState('');
|
||||
const [wasmFile, setWasmFile] = useState<File | null>(null);
|
||||
const [detailPlugin, setDetailPlugin] = useState<PluginInfo | null>(null);
|
||||
const [schemaData, setSchemaData] = useState<PluginSchemaResponse | null>(null);
|
||||
const [healthDetail, setHealthDetail] = useState<Record<string, unknown> | null>(null);
|
||||
const [actionLoading, setActionLoading] = useState<string | null>(null);
|
||||
const { token } = theme.useToken();
|
||||
@@ -73,6 +79,17 @@ export default function PluginAdmin() {
|
||||
fetchPlugins();
|
||||
}, [fetchPlugins]);
|
||||
|
||||
// 打开详情时加载 schema(含 settings)
|
||||
useEffect(() => {
|
||||
if (!detailPlugin) {
|
||||
setSchemaData(null);
|
||||
return;
|
||||
}
|
||||
getPluginSchema(detailPlugin.id)
|
||||
.then(setSchemaData)
|
||||
.catch(() => setSchemaData(null));
|
||||
}, [detailPlugin]);
|
||||
|
||||
const handleUpload = async () => {
|
||||
if (!wasmFile || !manifestText.trim()) {
|
||||
message.warning('请选择 WASM 文件并填写 Manifest');
|
||||
@@ -302,49 +319,90 @@ version = "0.1.0""
|
||||
onClose={() => {
|
||||
setDetailPlugin(null);
|
||||
setHealthDetail(null);
|
||||
setSchemaData(null);
|
||||
}}
|
||||
width={500}
|
||||
>
|
||||
{detailPlugin && (
|
||||
<Descriptions column={1} bordered size="small">
|
||||
<Descriptions.Item label="ID">{detailPlugin.id}</Descriptions.Item>
|
||||
<Descriptions.Item label="名称">{detailPlugin.name}</Descriptions.Item>
|
||||
<Descriptions.Item label="版本">{detailPlugin.version}</Descriptions.Item>
|
||||
<Descriptions.Item label="状态">
|
||||
<Tag color={STATUS_CONFIG[detailPlugin.status]?.color}>
|
||||
{STATUS_CONFIG[detailPlugin.status]?.label || detailPlugin.status}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="作者">{detailPlugin.author || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="描述">{detailPlugin.description || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="安装时间">{detailPlugin.installed_at || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="启用时间">{detailPlugin.enabled_at || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="实体数量">{detailPlugin.entities.length}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<Tabs
|
||||
defaultActiveKey="info"
|
||||
items={[
|
||||
{
|
||||
key: 'info',
|
||||
label: '基本信息',
|
||||
children: (
|
||||
<>
|
||||
<Descriptions column={1} bordered size="small">
|
||||
<Descriptions.Item label="ID">{detailPlugin.id}</Descriptions.Item>
|
||||
<Descriptions.Item label="名称">{detailPlugin.name}</Descriptions.Item>
|
||||
<Descriptions.Item label="版本">{detailPlugin.version}</Descriptions.Item>
|
||||
<Descriptions.Item label="状态">
|
||||
<Tag color={STATUS_CONFIG[detailPlugin.status]?.color}>
|
||||
{STATUS_CONFIG[detailPlugin.status]?.label || detailPlugin.status}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="作者">{detailPlugin.author || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="描述">{detailPlugin.description || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="安装时间">{detailPlugin.installed_at || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="启用时间">{detailPlugin.enabled_at || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="实体数量">{detailPlugin.entities.length}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<Button
|
||||
icon={<HeartOutlined />}
|
||||
onClick={() => handleHealthCheck(detailPlugin.id)}
|
||||
style={{ marginBottom: 8 }}
|
||||
>
|
||||
健康检查
|
||||
</Button>
|
||||
{healthDetail && (
|
||||
<pre
|
||||
style={{
|
||||
background: token.colorBgContainer,
|
||||
padding: 12,
|
||||
borderRadius: 6,
|
||||
fontSize: 12,
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
{JSON.stringify(healthDetail, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
},
|
||||
...(schemaData?.settings
|
||||
? [
|
||||
{
|
||||
key: 'settings',
|
||||
label: (
|
||||
<span>
|
||||
<SettingOutlined /> 配置
|
||||
</span>
|
||||
),
|
||||
children: (
|
||||
<PluginSettingsForm
|
||||
fields={schemaData.settings.fields}
|
||||
values={detailPlugin.config as Record<string, unknown>}
|
||||
recordVersion={detailPlugin.record_version}
|
||||
onSave={async (config, version) => {
|
||||
const updated = await updatePluginConfig(
|
||||
detailPlugin.id,
|
||||
config,
|
||||
version,
|
||||
);
|
||||
setDetailPlugin({ ...detailPlugin, ...updated });
|
||||
}}
|
||||
readOnly={detailPlugin.status !== 'enabled' && detailPlugin.status !== 'running'}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<Button
|
||||
icon={<HeartOutlined />}
|
||||
onClick={() => detailPlugin && handleHealthCheck(detailPlugin.id)}
|
||||
style={{ marginBottom: 8 }}
|
||||
>
|
||||
健康检查
|
||||
</Button>
|
||||
{healthDetail && (
|
||||
<pre
|
||||
style={{
|
||||
background: token.colorBgContainer,
|
||||
padding: 12,
|
||||
borderRadius: 6,
|
||||
fontSize: 12,
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
{JSON.stringify(healthDetail, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user