You've already forked DataMate
init datamate
This commit is contained in:
66
frontend/src/pages/SettingsPage/Settings.tsx
Normal file
66
frontend/src/pages/SettingsPage/Settings.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { useState } from "react";
|
||||
import { Tabs } from "antd";
|
||||
import {
|
||||
SettingOutlined,
|
||||
DatabaseOutlined,
|
||||
ApiOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import DevelopmentInProgress from "@/components/DevelopmentInProgress";
|
||||
import WebhookConfig from "./components/WebhookConfig";
|
||||
import EnvironmentAccess from "./components/EnvironmentAccess";
|
||||
import SystemConfig from "./components/SystemConfig";
|
||||
|
||||
export default function SettingsPage() {
|
||||
return <DevelopmentInProgress />;
|
||||
const [activeTab, setActiveTab] = useState("system");
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-gray-900">系统设置</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Settings Tabs */}
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={setActiveTab}
|
||||
className="space-y-6"
|
||||
items={[
|
||||
{
|
||||
key: "system",
|
||||
label: (
|
||||
<span>
|
||||
<SettingOutlined className="mr-1" />
|
||||
系统设置
|
||||
</span>
|
||||
),
|
||||
children: <SystemConfig />,
|
||||
},
|
||||
{
|
||||
key: "environment",
|
||||
label: (
|
||||
<span>
|
||||
<DatabaseOutlined className="mr-1" />
|
||||
环境接入
|
||||
</span>
|
||||
),
|
||||
children: <EnvironmentAccess />,
|
||||
},
|
||||
{
|
||||
key: "webhook",
|
||||
label: (
|
||||
<span>
|
||||
<ApiOutlined className="mr-1" />
|
||||
Webhook
|
||||
</span>
|
||||
),
|
||||
children: <WebhookConfig />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
365
frontend/src/pages/SettingsPage/components/EnvironmentAccess.tsx
Normal file
365
frontend/src/pages/SettingsPage/components/EnvironmentAccess.tsx
Normal file
@@ -0,0 +1,365 @@
|
||||
import { Card, Button, Form, Input, Modal, Select, Badge } from "antd";
|
||||
import {
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
ReloadOutlined,
|
||||
ExperimentOutlined,
|
||||
EyeOutlined,
|
||||
EyeInvisibleOutlined,
|
||||
CopyOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { useState } from "react";
|
||||
|
||||
interface VectorDBConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
type: "pinecone" | "weaviate" | "qdrant" | "milvus" | "chroma";
|
||||
url: string;
|
||||
apiKey: string;
|
||||
dimension: number;
|
||||
metric: string;
|
||||
status: "connected" | "disconnected" | "error";
|
||||
}
|
||||
|
||||
interface ModelConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
provider: "openai" | "anthropic" | "google" | "azure" | "local";
|
||||
model: string;
|
||||
apiKey: string;
|
||||
endpoint?: string;
|
||||
status: "active" | "inactive";
|
||||
usage: number;
|
||||
}
|
||||
|
||||
export default function EnvironmentAccess() {
|
||||
const [form] = Form.useForm();
|
||||
const [showApiKey, setShowApiKey] = useState<{ [key: string]: boolean }>({});
|
||||
const [showVectorDBDialog, setShowVectorDBDialog] = useState(false);
|
||||
const [showModelDialog, setShowModelDialog] = useState(false);
|
||||
|
||||
const [vectorDBs, setVectorDBs] = useState<VectorDBConfig[]>([
|
||||
{
|
||||
id: "1",
|
||||
name: "Pinecone Production",
|
||||
type: "pinecone",
|
||||
url: "https://your-index.svc.us-east1-gcp.pinecone.io",
|
||||
apiKey: "pc-****-****-****",
|
||||
dimension: 1536,
|
||||
metric: "cosine",
|
||||
status: "connected",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "Weaviate Local",
|
||||
type: "weaviate",
|
||||
url: "http://localhost:8080",
|
||||
apiKey: "",
|
||||
dimension: 768,
|
||||
metric: "cosine",
|
||||
status: "disconnected",
|
||||
},
|
||||
]);
|
||||
const [providerOptions] = useState([
|
||||
{ value: "openai", label: "OpenAI" },
|
||||
{ value: "anthropic", label: "Anthropic" },
|
||||
{ value: "google", label: "Google" },
|
||||
{ value: "azure", label: "Azure" },
|
||||
{ value: "local", label: "本地部署" },
|
||||
]);
|
||||
const [models, setModels] = useState<ModelConfig[]>([
|
||||
{
|
||||
id: "1",
|
||||
name: "GPT-4 Turbo",
|
||||
provider: "openai",
|
||||
model: "gpt-4-turbo-preview",
|
||||
apiKey: "sk-****-****-****",
|
||||
status: "active",
|
||||
usage: 85,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "Claude 3 Sonnet",
|
||||
provider: "anthropic",
|
||||
model: "claude-3-sonnet-20240229",
|
||||
apiKey: "sk-ant-****-****",
|
||||
status: "active",
|
||||
usage: 42,
|
||||
},
|
||||
]);
|
||||
|
||||
const [dbOptions] = useState([
|
||||
{ value: "pinecone", label: "Pinecone" },
|
||||
{ value: "weaviate", label: "Weaviate" },
|
||||
{ value: "qdrant", label: "Qdrant" },
|
||||
{ value: "milvus", label: "Milvus" },
|
||||
{ value: "chroma", label: "Chroma" },
|
||||
]);
|
||||
const [metricOptions] = useState([
|
||||
{ value: "cosine", label: "Cosine" },
|
||||
{ value: "euclidean", label: "Euclidean" },
|
||||
{ value: "dotproduct", label: "Dot Product" },
|
||||
]);
|
||||
|
||||
const [newVectorDB, setNewVectorDB] = useState({
|
||||
name: "",
|
||||
type: "pinecone",
|
||||
url: "",
|
||||
apiKey: "",
|
||||
dimension: 1536,
|
||||
metric: "cosine",
|
||||
});
|
||||
const [newModel, setNewModel] = useState({
|
||||
name: "",
|
||||
provider: "openai",
|
||||
model: "",
|
||||
apiKey: "",
|
||||
endpoint: "",
|
||||
});
|
||||
|
||||
const handleAddVectorDB = () => {
|
||||
setNewVectorDB({
|
||||
name: "",
|
||||
type: "pinecone",
|
||||
url: "",
|
||||
apiKey: "",
|
||||
dimension: 1536,
|
||||
metric: "cosine",
|
||||
});
|
||||
setShowVectorDBDialog(true);
|
||||
};
|
||||
|
||||
const handleAddModel = () => {
|
||||
setNewModel({
|
||||
name: "",
|
||||
provider: "openai",
|
||||
model: "",
|
||||
apiKey: generateApiKey(),
|
||||
endpoint: "",
|
||||
});
|
||||
setShowModelDialog(true);
|
||||
};
|
||||
const toggleApiKeyVisibility = (id: string) => {
|
||||
setShowApiKey((prev) => ({ ...prev, [id]: !prev[id] }));
|
||||
};
|
||||
|
||||
const generateApiKey = () => {
|
||||
const chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let result = "sk-";
|
||||
for (let i = 0; i < 48; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<Card>
|
||||
<div className="flex items-top justify-between">
|
||||
<h2 className="text-base font-medium mb-4">向量数据库</h2>
|
||||
<Button type="link" onClick={handleAddVectorDB}>
|
||||
添加向量库
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
{vectorDBs.map((db) => (
|
||||
<Card key={db.id} className="border rounded-lg p-4 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">{db.name}</span>
|
||||
<Badge
|
||||
status={
|
||||
db.status === "connected"
|
||||
? "success"
|
||||
: db.status === "error"
|
||||
? "error"
|
||||
: "default"
|
||||
}
|
||||
text={
|
||||
db.status === "connected"
|
||||
? "已连接"
|
||||
: db.status === "error"
|
||||
? "异常"
|
||||
: "未连接"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button icon={<ExperimentOutlined />} size="small" />
|
||||
<Button icon={<EditOutlined />} size="small" />
|
||||
<Button icon={<DeleteOutlined />} size="small" danger />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 space-y-1">
|
||||
<p>类型: {db.type}</p>
|
||||
<p>地址: {db.url}</p>
|
||||
<p>
|
||||
维度: {db.dimension} | 距离度量: {db.metric}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<div className="flex items-top justify-between">
|
||||
<h2 className="text-base font-medium mb-4">模型接入</h2>
|
||||
<Button type="link" onClick={handleAddModel}>
|
||||
添加模型
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
{models.map((model) => (
|
||||
<Card key={model.id} className="border rounded-lg p-4 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">{model.name}</span>
|
||||
<Badge
|
||||
status={model.status === "active" ? "success" : "default"}
|
||||
text={model.status === "active" ? "启用" : "禁用"}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
icon={
|
||||
showApiKey[model.id] ? (
|
||||
<EyeInvisibleOutlined />
|
||||
) : (
|
||||
<EyeOutlined />
|
||||
)
|
||||
}
|
||||
size="small"
|
||||
onClick={() => toggleApiKeyVisibility(model.id)}
|
||||
/>
|
||||
<Button icon={<ReloadOutlined />} size="small" />
|
||||
<Button icon={<EditOutlined />} size="small" />
|
||||
<Button icon={<DeleteOutlined />} size="small" danger />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 space-y-1">
|
||||
<p>提供商: {model.provider}</p>
|
||||
<p>模型: {model.model}</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>API Key:</span>
|
||||
<code className="bg-gray-100 px-2 py-1 rounded text-xs">
|
||||
{showApiKey[model.id] ? model.apiKey : "sk-****-****-****"}
|
||||
</code>
|
||||
<Button
|
||||
icon={<CopyOutlined />}
|
||||
size="small"
|
||||
onClick={() => navigator.clipboard.writeText(model.apiKey)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>使用率:</span>
|
||||
<div className="flex-1 bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className="bg-blue-500 h-2 rounded-full"
|
||||
style={{ width: `${model.usage}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs">{model.usage}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
{/* VectorDB Modal */}
|
||||
<Modal
|
||||
open={showVectorDBDialog}
|
||||
onCancel={() => setShowVectorDBDialog(false)}
|
||||
title="添加向量数据库"
|
||||
footer={[
|
||||
<Button key="cancel" onClick={() => setShowVectorDBDialog(false)}>
|
||||
取消
|
||||
</Button>,
|
||||
<Button
|
||||
key="ok"
|
||||
type="primary"
|
||||
onClick={() => setShowVectorDBDialog(false)}
|
||||
>
|
||||
添加数据库
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Form layout="vertical">
|
||||
<Form.Item name="name" label="数据库名称">
|
||||
<Input placeholder="输入数据库名称" />
|
||||
</Form.Item>
|
||||
<Form.Item name="type" label="数据库类型">
|
||||
<Select options={dbOptions}></Select>
|
||||
</Form.Item>
|
||||
<Form.Item name="url" label="连接地址">
|
||||
<Input placeholder="https://your-index.svc.region.pinecone.io" />
|
||||
</Form.Item>
|
||||
<Form.Item name="apiKey" label="API Key">
|
||||
<Input type="password" placeholder="输入API密钥" />
|
||||
</Form.Item>
|
||||
<Form.Item name="dimension" label="向量维度">
|
||||
<Input type="number" />
|
||||
</Form.Item>
|
||||
<Form.Item name="metric" label="距离度量">
|
||||
<Select options={metricOptions}></Select>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* Model Modal */}
|
||||
<Modal
|
||||
open={showModelDialog}
|
||||
onCancel={() => setShowModelDialog(false)}
|
||||
title="添加AI模型"
|
||||
footer={[
|
||||
<Button key="cancel" onClick={() => setShowModelDialog(false)}>
|
||||
取消
|
||||
</Button>,
|
||||
<Button
|
||||
key="ok"
|
||||
type="primary"
|
||||
onClick={() => setShowModelDialog(false)}
|
||||
>
|
||||
添加模型
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
onValuesChange={(changedValues) => {
|
||||
setNewModel({ ...newModel, ...changedValues });
|
||||
}}
|
||||
layout="vertical"
|
||||
>
|
||||
<Form.Item name="name" label="模型名称">
|
||||
<Input placeholder="输入模型名称" />
|
||||
</Form.Item>
|
||||
<Form.Item name="provider" label="服务提供商">
|
||||
<Select options={providerOptions}></Select>
|
||||
</Form.Item>
|
||||
<Form.Item name="model" label="模型标识">
|
||||
<Input placeholder="gpt-4-turbo-preview" />
|
||||
</Form.Item>
|
||||
<Form.Item name="apiKey" label="API Key">
|
||||
<Input
|
||||
placeholder="输入或生成API密钥"
|
||||
addonAfter={
|
||||
<ReloadOutlined
|
||||
onClick={() => {
|
||||
form.setFieldsValue({ apiKey: generateApiKey() });
|
||||
setNewModel({ ...newModel, apiKey: generateApiKey() });
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
{newModel.provider === "local" && (
|
||||
<Form.Item name="endpoint" label="自定义端点">
|
||||
<Input placeholder="http://localhost:8000/v1" />
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
86
frontend/src/pages/SettingsPage/components/SystemConfig.tsx
Normal file
86
frontend/src/pages/SettingsPage/components/SystemConfig.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import { Card, Divider, Input, Select, Switch, Button, Form, App } from "antd";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function SystemConfig() {
|
||||
const { message } = App.useApp();
|
||||
// System Settings State
|
||||
const [systemConfig, setSystemConfig] = useState({
|
||||
siteName: "ML Dataset Tool",
|
||||
maxFileSize: "100",
|
||||
autoBackup: true,
|
||||
logLevel: "info",
|
||||
sessionTimeout: "30",
|
||||
enableNotifications: true,
|
||||
});
|
||||
|
||||
const logLevelOptions = [
|
||||
{ value: "debug", label: "Debug" },
|
||||
{ value: "info", label: "Info" },
|
||||
{ value: "warn", label: "Warning" },
|
||||
{ value: "error", label: "Error" },
|
||||
];
|
||||
|
||||
const handleSaveSystemSettings = () => {
|
||||
// Save system settings logic
|
||||
console.log("Saving system settings:", systemConfig);
|
||||
message.success("系统设置已保存");
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Form
|
||||
onValuesChange={(changedValues) => {
|
||||
setSystemConfig((prevConfig) => ({
|
||||
...prevConfig,
|
||||
...changedValues,
|
||||
}));
|
||||
}}
|
||||
layout="vertical"
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<Form.Item name="siteName" label="站点名称">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="maxFileSize" label="最大文件大小 (MB)">
|
||||
<Input type="number" />
|
||||
</Form.Item>
|
||||
<Form.Item name="logLevel" label="日志级别">
|
||||
<Select options={logLevelOptions}></Select>
|
||||
</Form.Item>
|
||||
<Form.Item name="sessionTimeout" label="会话超时 (分钟)">
|
||||
<Input type="number" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
<Divider />
|
||||
<div className="space-y-4">
|
||||
<h4 className="font-medium">功能开关</h4>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<span>自动备份</span>
|
||||
<p className="text-sm text-gray-500">定期自动备份系统数据</p>
|
||||
</div>
|
||||
<Form.Item name="autoBackup" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<span>启用通知</span>
|
||||
<p className="text-sm text-gray-500">接收系统通知和提醒</p>
|
||||
</div>
|
||||
<Form.Item name="enableNotifications" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end mt-6">
|
||||
<Button type="primary" onClick={handleSaveSystemSettings}>
|
||||
保存设置
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
313
frontend/src/pages/SettingsPage/components/WebhookConfig.tsx
Normal file
313
frontend/src/pages/SettingsPage/components/WebhookConfig.tsx
Normal file
@@ -0,0 +1,313 @@
|
||||
import { Button, Card, Checkbox, Form, Input, Modal, Badge } from "antd";
|
||||
import {
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
KeyOutlined,
|
||||
ReloadOutlined,
|
||||
ExperimentOutlined,
|
||||
ThunderboltOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { useState } from "react";
|
||||
|
||||
interface WebhookEvent {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
category: string;
|
||||
}
|
||||
|
||||
interface WebhookConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
events: string[];
|
||||
status: "active" | "inactive";
|
||||
secret: string;
|
||||
retryCount: number;
|
||||
}
|
||||
|
||||
const availableEvents: WebhookEvent[] = [
|
||||
{
|
||||
id: "project_created",
|
||||
name: "项目创建",
|
||||
description: "新项目被创建时触发",
|
||||
category: "项目管理",
|
||||
},
|
||||
{
|
||||
id: "project_updated",
|
||||
name: "项目更新",
|
||||
description: "项目信息被修改时触发",
|
||||
category: "项目管理",
|
||||
},
|
||||
{
|
||||
id: "project_deleted",
|
||||
name: "项目删除",
|
||||
description: "项目被删除时触发",
|
||||
category: "项目管理",
|
||||
},
|
||||
{
|
||||
id: "task_created",
|
||||
name: "任务创建",
|
||||
description: "新任务被创建时触发",
|
||||
category: "任务管理",
|
||||
},
|
||||
{
|
||||
id: "task_updated",
|
||||
name: "任务更新",
|
||||
description: "任务状态或内容被更新时触发",
|
||||
category: "任务管理",
|
||||
},
|
||||
{
|
||||
id: "task_completed",
|
||||
name: "任务完成",
|
||||
description: "任务被标记为完成时触发",
|
||||
category: "任务管理",
|
||||
},
|
||||
{
|
||||
id: "annotation_created",
|
||||
name: "标注创建",
|
||||
description: "新标注被创建时触发",
|
||||
category: "标注管理",
|
||||
},
|
||||
{
|
||||
id: "annotation_updated",
|
||||
name: "标注更新",
|
||||
description: "标注被修改时触发",
|
||||
category: "标注管理",
|
||||
},
|
||||
{
|
||||
id: "annotation_deleted",
|
||||
name: "标注删除",
|
||||
description: "标注被删除时触发",
|
||||
category: "标注管理",
|
||||
},
|
||||
{
|
||||
id: "model_trained",
|
||||
name: "模型训练完成",
|
||||
description: "模型训练任务完成时触发",
|
||||
category: "模型管理",
|
||||
},
|
||||
{
|
||||
id: "prediction_created",
|
||||
name: "预测生成",
|
||||
description: "新预测结果生成时触发",
|
||||
category: "预测管理",
|
||||
},
|
||||
];
|
||||
|
||||
export default function WebhookConfig() {
|
||||
const [newWebhook, setNewWebhook] = useState({
|
||||
name: "",
|
||||
url: "",
|
||||
events: [] as string[],
|
||||
secret: "",
|
||||
retryCount: 3,
|
||||
});
|
||||
const [showWebhookDialog, setShowWebhookDialog] = useState(false);
|
||||
// Webhook State
|
||||
const [webhooks, setWebhooks] = useState<WebhookConfig[]>([
|
||||
{
|
||||
id: "1",
|
||||
name: "数据同步Webhook",
|
||||
url: "https://webhook.example.com/data-sync",
|
||||
events: ["task_created", "task_completed", "annotation_created"],
|
||||
status: "active",
|
||||
secret: "wh_secret_123456",
|
||||
retryCount: 3,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "任务通知Webhook",
|
||||
url: "https://webhook.example.com/task-notify",
|
||||
events: ["task_started", "task_completed", "task_failed"],
|
||||
status: "inactive",
|
||||
secret: "wh_secret_789012",
|
||||
retryCount: 5,
|
||||
},
|
||||
]);
|
||||
|
||||
const handleAddWebhook = () => {
|
||||
setNewWebhook({
|
||||
name: "",
|
||||
url: "",
|
||||
events: [],
|
||||
secret: generateApiKey(),
|
||||
retryCount: 3,
|
||||
});
|
||||
setShowWebhookDialog(true);
|
||||
};
|
||||
|
||||
const generateApiKey = () => {
|
||||
const chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let result = "sk-";
|
||||
for (let i = 0; i < 48; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-medium">Webhook 配置</h3>
|
||||
</div>
|
||||
<Button onClick={handleAddWebhook}>新增Webhook</Button>
|
||||
</div>
|
||||
<div className="grid gap-4">
|
||||
{webhooks.map((webhook) => (
|
||||
<Card key={webhook.id}>
|
||||
<div className="flex items-start justify-between p-6">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="font-medium">{webhook.name}</span>
|
||||
<Badge
|
||||
status={webhook.status === "active" ? "success" : "default"}
|
||||
text={webhook.status === "active" ? "启用" : "禁用"}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-gray-600 flex items-center gap-2">
|
||||
<ThunderboltOutlined />
|
||||
{webhook.url}
|
||||
</p>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-sm text-gray-500">事件:</span>
|
||||
{webhook.events.map((event) => {
|
||||
const eventInfo = availableEvents.find(
|
||||
(e) => e.id === event
|
||||
);
|
||||
return (
|
||||
<Badge
|
||||
key={event}
|
||||
status="default"
|
||||
text={eventInfo?.name || event}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm text-gray-500">
|
||||
<span className="flex items-center gap-1">
|
||||
<KeyOutlined />
|
||||
Secret: {webhook.secret.substring(0, 12)}...
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<ReloadOutlined />
|
||||
重试: {webhook.retryCount}次
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button icon={<ExperimentOutlined />} size="small" />
|
||||
<Button icon={<EditOutlined />} size="small" />
|
||||
<Button icon={<DeleteOutlined />} size="small" danger />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
<Modal
|
||||
open={showWebhookDialog}
|
||||
onCancel={() => setShowWebhookDialog(false)}
|
||||
title="新增 Webhook"
|
||||
footer={[
|
||||
<Button key="cancel" onClick={() => setShowWebhookDialog(false)}>
|
||||
取消
|
||||
</Button>,
|
||||
<Button
|
||||
key="ok"
|
||||
type="primary"
|
||||
onClick={() => setShowWebhookDialog(false)}
|
||||
>
|
||||
创建Webhook
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Form
|
||||
layout="vertical"
|
||||
initialValues={newWebhook}
|
||||
onValuesChange={(changedValues) => {
|
||||
setNewWebhook({ ...newWebhook, ...changedValues });
|
||||
}}
|
||||
>
|
||||
<Form.Item name="name" label="Webhook名称">
|
||||
<Input placeholder="输入Webhook名称" />
|
||||
</Form.Item>
|
||||
<Form.Item name="retryCount" label="重试次数">
|
||||
<Input type="number" />
|
||||
</Form.Item>
|
||||
<Form.Item name="url" label="Webhook URL">
|
||||
<Input placeholder="https://your-domain.com/webhook" />
|
||||
</Form.Item>
|
||||
<Form.Item name="secret" label="Secret Key">
|
||||
<Input
|
||||
placeholder="用于验证Webhook请求的密钥"
|
||||
addonAfter={
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={() =>
|
||||
setNewWebhook({ ...newWebhook, secret: generateApiKey() })
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="选择事件">
|
||||
<div className="max-h-48 overflow-y-auto border rounded-lg p-3 space-y-3">
|
||||
{Object.entries(
|
||||
availableEvents.reduce((acc, event) => {
|
||||
if (!acc[event.category]) acc[event.category] = [];
|
||||
acc[event.category].push(event);
|
||||
return acc;
|
||||
}, {} as Record<string, WebhookEvent[]>)
|
||||
).map(([category, events]) => (
|
||||
<div key={category} className="space-y-2">
|
||||
<h4 className="font-medium text-sm text-gray-700">
|
||||
{category}
|
||||
</h4>
|
||||
<div className="space-y-2 pl-4">
|
||||
{events.map((event) => (
|
||||
<div
|
||||
key={event.id}
|
||||
className="flex items-start space-x-2"
|
||||
>
|
||||
<Checkbox
|
||||
checked={newWebhook.events.includes(event.id)}
|
||||
onChange={(e) => {
|
||||
const checked = e.target.checked;
|
||||
if (checked) {
|
||||
setNewWebhook({
|
||||
...newWebhook,
|
||||
events: [...newWebhook.events, event.id],
|
||||
});
|
||||
} else {
|
||||
setNewWebhook({
|
||||
...newWebhook,
|
||||
events: newWebhook.events.filter(
|
||||
(ev) => ev !== event.id
|
||||
),
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="text-sm font-medium">
|
||||
{event.name}
|
||||
</span>
|
||||
</Checkbox>
|
||||
<span className="text-xs text-gray-500">
|
||||
{event.description}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user