feat(alert): A2 告警引擎分布式安全 — Redis 分布式锁(SetNX)
- Cache 接口新增 SetNX 方法(go-redis 原子 / redka check-then-set 单副本安全) - AlertEngine 改造为支持分布式锁: tryAcquireLock/releaseLock/GetEngineStatus - 多副本部署时仅 Leader 执行告警检查, 60s TTL 防死锁, fail-open 容错 - 新增 GET /alerts/engine/status 监控端点(is_leader/instance_id/lock_holder) - 修复 GrafanaDashboard.tsx Panel.options 类型不匹配导致构建中断 - 同步未提交的前端类型安全迁移(140+ 文件) - ROADMAP/TDD_PLAN/PROJECT_EVALUATION 三文档同步更新 验证: go build 零错误, Docker 部署 healthy, 引擎状态端点正常, 锁竞争测试通过(非 Leader 跳过检查), 5/5 核心 API 回归通过
This commit is contained in:
1 parent
5ca0e6d83a
commit
c68aef4444
150 files changed
+1038
-690
No files matched your search
@@ -44,6 +44,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/u
|
||||
import { toast } from 'sonner';
|
||||
import { pipelineApi, deployApi } from '@/api';
|
||||
import { useConfirm } from '@/components/ui/confirm-dialog';
|
||||
import type { JsonData } from '@/types/common';
|
||||
|
||||
// ---- types ----
|
||||
interface Task {
|
||||
@@ -58,7 +59,7 @@ interface LogEntry {
|
||||
}
|
||||
|
||||
const stageTypeLabels: Record<string, string> = { source: '拉取代码', build: '构建', deploy: '部署', test: '测试', custom: '自定义' };
|
||||
const taskIcons: Record<string, any> = { GitBranch, Wrench, Server, FlaskConical, Zap, Terminal, FileCode, Container };
|
||||
const taskIcons: JsonData = { GitBranch, Wrench, Server, FlaskConical, Zap, Terminal, FileCode, Container };
|
||||
|
||||
function uid() { return `tsk-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`; }
|
||||
function fmtDuration(s: number | undefined): string {
|
||||
@@ -119,7 +120,7 @@ function TaskEditor({ task, onUpdate }: { task: Task | null; onUpdate: (t: Task)
|
||||
}
|
||||
|
||||
// ====== Log Dialog ======
|
||||
function LogDialog({ open, onClose, run, logs }: { open: boolean; onClose: () => void; run: any; logs: LogEntry[] }) {
|
||||
function LogDialog({ open, onClose, run, logs }: { open: boolean; onClose: () => void; run?: JsonData; logs: LogEntry[] }) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-3xl max-h-[80vh] overflow-y-auto">
|
||||
@@ -160,16 +161,16 @@ function LogDialog({ open, onClose, run, logs }: { open: boolean; onClose: () =>
|
||||
export default function PipelineEditor() {
|
||||
const { code } = useParams<{ code: string }>();
|
||||
const pipelineCode = code || '';
|
||||
const [pipeline, setPipeline] = useState<any>(null);
|
||||
const [pipeline, setPipeline] = useState<JsonData>(null);
|
||||
const [stages, setStages] = useState<Stage[]>([]);
|
||||
const [selectedTaskId, setSelectedTaskId] = useState<string | null>(null);
|
||||
const [expandedStages, setExpandedStages] = useState<Record<string, boolean>>({});
|
||||
const [activeTab, setActiveTab] = useState('flow');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [runs, setRuns] = useState<any[]>([]);
|
||||
const [environments, setEnvironments] = useState<any[]>([]);
|
||||
const [members, setMembers] = useState<any[]>([]);
|
||||
const [runs, setRuns] = useState<JsonData[]>([]);
|
||||
const [environments, setEnvironments] = useState<JsonData[]>([]);
|
||||
const [members, setMembers] = useState<JsonData[]>([]);
|
||||
const [loadingMembers, setLoadingMembers] = useState(false);
|
||||
|
||||
// trigger config
|
||||
@@ -186,7 +187,7 @@ export default function PipelineEditor() {
|
||||
|
||||
// log dialog
|
||||
const [logOpen, setLogOpen] = useState(false);
|
||||
const [logRun, setLogRun] = useState<any>(null);
|
||||
const [logRun, setLogRun] = useState<JsonData>(null);
|
||||
const [logs, setLogs] = useState<LogEntry[]>([]);
|
||||
|
||||
const _confirm = useConfirm();
|
||||
@@ -194,7 +195,7 @@ export default function PipelineEditor() {
|
||||
const loadEnvironments = useCallback(async () => {
|
||||
try {
|
||||
const res = await deployApi.getEnvironments();
|
||||
setEnvironments(Array.isArray(res) ? res : (res as Record<string, any>)?.list || []);
|
||||
setEnvironments(Array.isArray(res) ? res : (res as JsonData)?.list || []);
|
||||
} catch { /* silent */ }
|
||||
}, []);
|
||||
|
||||
@@ -202,7 +203,7 @@ export default function PipelineEditor() {
|
||||
setLoadingMembers(true);
|
||||
try {
|
||||
const res = await pipelineApi.getMembers(pipelineCode);
|
||||
setMembers(Array.isArray(res) ? res : (res as Record<string, any>)?.data || []);
|
||||
setMembers(Array.isArray(res) ? res : (res as JsonData)?.data || []);
|
||||
} catch { setMembers([]); }
|
||||
finally { setLoadingMembers(false); }
|
||||
}, [pipelineCode]);
|
||||
@@ -210,7 +211,7 @@ export default function PipelineEditor() {
|
||||
const loadRuns = useCallback(async () => {
|
||||
try {
|
||||
const res = await pipelineApi.getRuns(pipelineCode);
|
||||
setRuns(Array.isArray(res) ? res : (res as Record<string, any>)?.list || []);
|
||||
setRuns(Array.isArray(res) ? res : (res as JsonData)?.list || []);
|
||||
} catch { setRuns([]); }
|
||||
}, [pipelineCode]);
|
||||
|
||||
@@ -218,7 +219,7 @@ export default function PipelineEditor() {
|
||||
(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const p = await pipelineApi.getPipelineDetail(pipelineCode) as Record<string, any>;
|
||||
const p = await pipelineApi.getPipelineDetail(pipelineCode) as JsonData;
|
||||
// http.get 已自动解包 json.data,p 即为 pipeline 对象
|
||||
if (!p || !p.id) { setPipeline(null); setLoading(false); return; }
|
||||
setPipeline(p);
|
||||
@@ -294,11 +295,11 @@ export default function PipelineEditor() {
|
||||
} catch { toast.error('停止失败'); }
|
||||
};
|
||||
|
||||
const handleViewLog = async ( run: any) => {
|
||||
const handleViewLog = async (run: JsonData) => {
|
||||
setLogRun(run);
|
||||
setLogOpen(true);
|
||||
try {
|
||||
const data = await pipelineApi.getRunLog(pipelineCode, run.id) as Record<string, any>;
|
||||
const data = await pipelineApi.getRunLog(pipelineCode, run.id) as JsonData;
|
||||
setLogs(data?.logs || []);
|
||||
} catch { setLogs([]); }
|
||||
};
|
||||
@@ -482,7 +483,7 @@ export default function PipelineEditor() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{runs.map(( r: any) => (
|
||||
{runs.map((r: JsonData) => (
|
||||
<tr key={r.id} className="border-b border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/50">
|
||||
<td className="px-3 py-2 font-medium text-[var(--dg-text-primary)]">#{r.build_number || r.id}</td>
|
||||
<td className="px-3 py-2">
|
||||
@@ -552,8 +553,8 @@ export default function PipelineEditor() {
|
||||
|
||||
{/* Members list — split direct users vs group assignments */}
|
||||
{(() => {
|
||||
const directUsers = members.filter(( m: any) => m.username);
|
||||
const groupAssignments = members.filter(( m: any) => m.group_id);
|
||||
const directUsers = members.filter((m: JsonData) => m.username);
|
||||
const groupAssignments = members.filter((m: JsonData) => m.group_id);
|
||||
return (
|
||||
<>
|
||||
{/* Direct users */}
|
||||
@@ -568,7 +569,7 @@ export default function PipelineEditor() {
|
||||
<div className="py-4 text-center text-[var(--dg-text-secondary)] text-xs">暂无直接用户</div>
|
||||
) : (
|
||||
<div className="divide-y divide-[var(--dg-border)]">
|
||||
{directUsers.map(( m: any) => (
|
||||
{directUsers.map((m: JsonData) => (
|
||||
<div key={m.id} className="flex items-center justify-between px-4 py-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-blue-500/20 flex items-center justify-center"><Users className="w-4 h-4 text-blue-400" /></div>
|
||||
@@ -597,7 +598,7 @@ export default function PipelineEditor() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-[var(--dg-border)]">
|
||||
{groupAssignments.map(( m: any) => (
|
||||
{groupAssignments.map((m: JsonData) => (
|
||||
<div key={m.id} className="flex items-center justify-between px-4 py-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-purple-500/20 flex items-center justify-center"><Shield className="w-4 h-4 text-purple-400" /></div>
|
||||
|
||||
Reference in new issue
Block a user