feat: 新增日志中心、功能优化与代码重构

- 新增 log_datasource.go / log_proxy.go / user_api_token.go 后端控制器
- 新增 log_datasource.go model
- 新增 monitor/log-center.ts API 模块
- 新增 LogCenter 前端组件及页面
- 重构 deployment 相关组件 (HPDA/UpdateImageModal/bundle-yaml 等)
- 新增 ResetPasswordDialog / code-snippet-block / copy-button 等 UI 组件
- 重构多个 k8s 相关组件 (TopologyView/ResourceVisualEditor/PodFileBrowser 等)
- 更新多个 API 和工具函数 (auth.ts/monitor/index.ts/request.ts 等)
- 更新静态资源 (index.html + 新 JS/CSS bundle)
- 删除废弃静态资源文件
This commit is contained in:
Logan committed 2026-06-26 23:18:05 +08:00
1 parent 5ca0e6d83a
commit 8d46e8f352
197 files changed
+5014 -815

No files matched your search

@@ -2,9 +2,9 @@
// 作者: Logan.Li
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { AlertTriangle, Loader2, Minus, Plus, Trash2 } from 'lucide-react';
import { ModalShell } from './ModalShell';
import { UpdateImageModal } from './UpdateImageModal';
import type { ActionModal } from './types';
export interface ActionModalsProps {
@@ -42,26 +42,14 @@ export function ActionModals({
<>
{/* ============ 弹窗:调整镜像版本 ============ */}
{modal?.kind === 'image' && (
<ModalShell title="调整镜像版本" onClose={onClose}>
<div className="space-y-3">
<div>
<Label className="text-slate-400 text-sm">当前镜像</Label>
<p className="text-xs text-slate-500 font-mono mt-1 bg-slate-800 rounded px-3 py-2">{deployment.image || '-'}</p>
</div>
<div>
<Label className="text-slate-400 text-sm">新镜像地址</Label>
<Input value={newImage} onChange={e => setNewImage(e.target.value)}
placeholder="harbor.example.com/app:v2.0"
className="mt-1 bg-slate-800 border-slate-700 text-slate-200 text-sm font-mono" />
</div>
<div className="flex justify-end gap-2 pt-2">
<Button variant="outline" className="border-slate-700 text-slate-400" onClick={onClose}>取消</Button>
<Button className="bg-blue-600 hover:bg-blue-700" disabled={actionLoading === 'image' || !newImage.trim()} onClick={onUpdateImage}>
{actionLoading === 'image' ? <Loader2 className="h-4 w-4 animate-spin" /> : '更新镜像'}
</Button>
</div>
</div>
</ModalShell>
<UpdateImageModal
deployment={deployment}
newImage={newImage}
setNewImage={setNewImage}
actionLoading={actionLoading}
onClose={onClose}
onUpdateImage={onUpdateImage}
/>
)}
{/* ============ 弹窗:伸缩 ============ */}
@@ -1,49 +1,135 @@
// CICDWebhookContent.tsx — CI/CD Webhook 弹窗内容
// 作者: Logan.Li
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { useToast } from '@/hooks/use-toast';
import { useUserStore } from '@/store';
// CICDWebhookContent.tsx — CI/CD Webhook 弹窗内容 // Logan.Li
import { useEffect, useMemo, useState } from 'react';
import { Link } from 'react-router-dom';
import { Loader2 } from 'lucide-react';
import { CodeSnippetBlock } from '@/components/ui/code-snippet-block';
import { userApi } from '@/api';
import { getPublicApiBase } from '@/utils/request';
import { getSessionApiAccessKey } from '@/utils/storage';
export interface CICDWebhookContentProps {
clusterCode: string;
namespace: string;
deploymentName: string;
/** 当前 Deployment 镜像 // Logan.Li */
image?: string;
/** 主容器名(可选,多容器时建议填写)// Logan.Li */
containerName?: string;
}
export function CICDWebhookContent({ clusterCode, namespace, deploymentName }: CICDWebhookContentProps) {
const { toast } = useToast();
const token = useUserStore(( s: any) => s.token) || localStorage.getItem('token') || '';
const webhookUrl = `${window.location.origin}/api/v1/clusters/${clusterCode}/namespaces/${namespace}/deployments/${deploymentName}/update-image`;
const curlCmd = `curl -X POST '${webhookUrl}' \\
-H 'Authorization: Bearer ${token ? token.substring(0, 16) + '...' : 'YOUR_TOKEN'}' \\
-H 'Content-Type: application/json' \\
-d '{"image": "harbor.example.com/app:v1.0", "container": "your-container-name"}'`;
export function CICDWebhookContent({
clusterCode,
namespace,
deploymentName,
image = '',
containerName = '',
}: CICDWebhookContentProps) {
const apiBase = getPublicApiBase();
const [keyLoading, setKeyLoading] = useState(true);
const [hasKey, setHasKey] = useState(false);
const [keyExpired, setKeyExpired] = useState(false);
const [accessKey, setAccessKey] = useState('');
const copy = (text: string) => {
navigator.clipboard.writeText(text).then(() => toast({ title: '已复制' })).catch(() => {});
};
useEffect(() => {
let cancelled = false;
(async () => {
setKeyLoading(true);
const cached = getSessionApiAccessKey();
if (cached) setAccessKey(cached);
try {
const meta = await userApi.getApiAccessKey();
if (!cancelled) {
setHasKey(!!meta?.has_token);
setKeyExpired(!!meta?.expired);
}
} catch {
if (!cancelled) setHasKey(!!cached);
} finally {
if (!cancelled) setKeyLoading(false);
}
})();
return () => { cancelled = true; };
}, []);
const updateUrl = `${apiBase}/kubernetes/${clusterCode}/namespace/${namespace}/deployments/${deploymentName}/update-image`;
const restartUrl = `${apiBase}/kubernetes/${clusterCode}/namespace/${namespace}/deployments/${deploymentName}/restart`;
const updateBody = useMemo(() => {
const body: Record<string, string> = { image: image || '<新镜像地址>' };
if (containerName) body.container_name = containerName;
return JSON.stringify(body);
}, [image, containerName]);
const updateCurl = accessKey
? `curl -X POST '${updateUrl}' \\
-H 'X-AIOps-Access-Key: ${accessKey}' \\
-H 'Content-Type: application/json' \\
-d '${updateBody}'`
: `curl -X POST '${updateUrl}' \\
-H 'X-AIOps-Access-Key: <个人中心生成的访问密钥>' \\
-H 'Content-Type: application/json' \\
-d '${updateBody}'`;
const restartCurl = accessKey
? `curl -X POST '${restartUrl}' \\
-H 'X-AIOps-Access-Key: ${accessKey}'`
: `curl -X POST '${restartUrl}' \\
-H 'X-AIOps-Access-Key: <个人中心生成的访问密钥>'`;
const canCopy = !!accessKey && !keyExpired;
return (
<div className="space-y-4">
<div>
<Label className="text-slate-400 text-sm">Webhook URL</Label>
<div className="flex items-center gap-2 mt-1">
<code className="flex-1 text-xs text-slate-300 bg-slate-800 rounded px-3 py-2 break-all font-mono">{webhookUrl}</code>
<Button variant="outline" size="sm" className="border-slate-700 text-slate-400 shrink-0" onClick={() => copy(webhookUrl)}>复制</Button>
</div>
<div className="rounded-md bg-[var(--dg-bg-tertiary)]/50 px-3 py-2 text-xs text-[var(--dg-text-muted)] leading-relaxed">
{keyLoading ? (
<span className="inline-flex items-center gap-1.5">
<Loader2 className="h-3 w-3 animate-spin" />正在读取 API 密钥…
</span>
) : keyExpired ? (
<>
访问凭证已过期,请前往
<Link to="/profile" className="text-[var(--dg-blue)] hover:underline mx-1">个人中心</Link>
调整有效期或轮换凭证后再复制 curl。
</>
) : accessKey ? (
<>已填入本机保存的访问凭证,复制 curl 后可直接在终端执行触发更新。</>
) : hasKey ? (
<>
已检测到访问凭证,但本机未缓存明文。请前往
<Link to="/profile" className="text-[var(--dg-blue)] hover:underline mx-1">个人中心</Link>
轮换凭证一次,之后 curl 将自动填入。
</>
) : (
<>
请先在
<Link to="/profile" className="text-[var(--dg-blue)] hover:underline mx-1">个人中心</Link>
签发 CI/CD 访问凭证,curl 将自动填入。
</>
)}
</div>
<div>
<Label className="text-slate-400 text-sm">curl 示例</Label>
<div className="relative mt-1">
<pre className="text-xs text-slate-300 bg-slate-800 rounded p-3 overflow-x-auto font-mono whitespace-pre-wrap">{curlCmd}</pre>
<Button variant="outline" size="sm" className="absolute top-2 right-2 border-slate-700 text-slate-400" onClick={() => copy(curlCmd)}>复制</Button>
</div>
</div>
<div className="text-xs text-slate-500 space-y-1">
<p>• 使用 Bearer Token 认证(JWT 登录令牌)</p>
<p>• <code className="text-slate-400">image</code> 必填,指定新镜像地址</p>
<p>• <code className="text-slate-400">container</code> 可选,不填则更新第一个容器</p>
<CodeSnippetBlock
label="更新镜像"
code={updateCurl}
copyLabel="curl 已复制,可直接粘贴到终端执行"
copyDisabled={!canCopy}
copyDisabledTitle="请先在个人中心签发访问凭证"
/>
<CodeSnippetBlock
label="滚动重启"
code={restartCurl}
copyLabel="curl 已复制,可直接粘贴到终端执行"
copyDisabled={!canCopy}
copyDisabledTitle="请先在个人中心签发访问凭证"
/>
<div className="text-xs text-[var(--dg-text-muted)] space-y-1">
<p>• 示例镜像:<code className="text-[var(--dg-text-secondary)]">{image || '—'}</code>
{containerName ? <> · 容器 <code className="text-[var(--dg-text-secondary)]">{containerName}</code></> : null}
</p>
<p>• 使用长期 API 密钥,不会随登录 JWT 过期</p>
<p>• 修改 <code className="text-[var(--dg-text-secondary)]">image</code> 字段后执行 curl 即可触发滚动更新</p>
<p>• 也可用 <code className="text-[var(--dg-text-secondary)]">Authorization: Bearer &lt;密钥&gt;</code></p>
</div>
</div>
);
@@ -12,11 +12,13 @@ export interface EditModeContainerProps {
namespace: string;
deploymentName: string;
manifest: any | null;
onSaved: () => void;
onSaved: (updated?: any) => void;
onCancel: () => void;
/** 新建模式:名称可编辑,保存走 apply // Logan.Li */
isCreate?: boolean;
}
export function EditModeContainer({ clusterCode, namespace, deploymentName, manifest, onSaved, onCancel }: EditModeContainerProps) {
export function EditModeContainer({ clusterCode, namespace, deploymentName, manifest, onSaved, onCancel, isCreate = false }: EditModeContainerProps) {
type EditTab = 'basic' | 'containers' | 'storage' | 'advanced' | 'service';
const [activeTab, setActiveTab] = useState<EditTab>('basic');
const [editManifest, setEditManifest] = useState<any>(manifest);
@@ -27,7 +29,7 @@ export function EditModeContainer({ clusterCode, namespace, deploymentName, mani
const handleSaved = (updated?: any) => {
if (updated) setEditManifest(updated);
onSaved();
onSaved(updated);
};
const tabs: Array<{ key: EditTab; label: string; icon: typeof Settings }> = [
@@ -45,6 +47,7 @@ export function EditModeContainer({ clusterCode, namespace, deploymentName, mani
manifest: editManifest,
onSaved: handleSaved,
onCancel,
isCreate,
};
return (
@@ -1,5 +1,4 @@
// ModalShell.tsx — 通用 Modal 外壳
// 作者: Logan.Li
// ModalShell.tsx — 通用 Modal 外壳(固定标题 + 内容区滚动)// Logan.Li
import { Button } from '@/components/ui/button';
import { X } from 'lucide-react';
@@ -7,17 +6,25 @@ export interface ModalShellProps {
title: string;
children: React.ReactNode;
onClose: () => void;
/** lg:宽弹窗;xl:镜像地址等长文本场景 // Logan.Li */
size?: 'default' | 'lg' | 'xl';
}
export function ModalShell({ title, children, onClose }: ModalShellProps) {
export function ModalShell({ title, children, onClose, size = 'default' }: ModalShellProps) {
const widthClass =
size === 'xl' ? 'w-[780px]' : size === 'lg' ? 'w-[680px]' : 'w-[460px]';
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60" onClick={onClose}>
<div className="bg-slate-900 border border-slate-700 rounded-xl p-6 w-[460px] max-w-[90vw] shadow-2xl" onClick={e => e.stopPropagation()}>
<div className="flex items-center justify-between mb-4">
<h2 className="text-base font-bold text-slate-100">{title}</h2>
<Button variant="ghost" size="icon" className="h-7 w-7 text-slate-400" onClick={onClose}><X className="h-4 w-4" /></Button>
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4" onClick={onClose}>
<div
className={`bg-slate-900 border border-slate-700 rounded-xl ${widthClass} max-w-[92vw] max-h-[85vh] flex flex-col shadow-2xl`}
onClick={e => e.stopPropagation()}
>
<div className="flex items-center justify-between gap-3 px-6 pt-6 pb-4 shrink-0">
<h2 className="text-base font-bold text-slate-100 truncate min-w-0" title={title}>{title}</h2>
<Button variant="ghost" size="icon" className="h-7 w-7 shrink-0 text-slate-400" onClick={onClose}><X className="h-4 w-4" /></Button>
</div>
{children}
<div className="flex-1 min-h-0 overflow-y-auto px-6 pb-6">{children}</div>
</div>
</div>
);
@@ -63,13 +63,13 @@ export function PodLogsModal({ clusterCode, namespace, podName, containers, onCl
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60" onClick={onClose}>
<div className="bg-slate-900 border border-slate-700 rounded-xl w-[90vw] max-w-4xl max-h-[80vh] flex flex-col" onClick={e => e.stopPropagation()}>
<div className="flex items-center justify-between p-3 border-b border-slate-700">
<div className="flex items-center gap-2">
<FileText className="h-4 w-4 text-blue-400" />
<span className="text-sm font-semibold text-slate-200">{podName}</span>
{container && <Badge variant="outline" className="border-slate-700 text-slate-400 text-[10px]">{container}</Badge>}
<div className="flex items-center justify-between p-3 border-b border-slate-700 gap-3 shrink-0">
<div className="flex items-center gap-2 min-w-0 flex-1">
<FileText className="h-4 w-4 shrink-0 text-blue-400" />
<span className="text-sm font-semibold text-slate-200 truncate" title={podName}>{podName}</span>
{container && <Badge variant="outline" className="border-slate-700 text-slate-400 text-[10px] shrink-0">{container}</Badge>}
</div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-2 shrink-0 flex-wrap justify-end">
{containers.length > 0 && (
<select value={container} onChange={e => setContainer(e.target.value)}
className="bg-slate-800 border border-slate-700 rounded text-xs text-slate-300 px-2 py-1">
@@ -0,0 +1,154 @@
// UpdateImageModal.tsx — 调整镜像版本弹窗(地址 + Tag 分栏)// Logan.Li
import { useEffect, useMemo, useState } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { ArrowRight, Loader2 } from 'lucide-react';
import { joinImageRef, parseImageRef } from '../image-ref-utils';
import { ModalShell } from './ModalShell';
export interface UpdateImageModalProps {
deployment: any;
newImage: string;
setNewImage: (value: string) => void;
actionLoading: string;
onClose: () => void;
onUpdateImage: () => void;
}
export function UpdateImageModal({
deployment,
newImage,
setNewImage,
actionLoading,
onClose,
onUpdateImage,
}: UpdateImageModalProps) {
const currentImage = deployment?.image || '';
const current = useMemo(() => parseImageRef(currentImage), [currentImage]);
const [newRepo, setNewRepo] = useState('');
const [newTag, setNewTag] = useState('');
useEffect(() => {
const parsed = parseImageRef(newImage || currentImage);
setNewRepo(parsed.repository);
setNewTag(parsed.tag);
}, [newImage, currentImage]);
const previewImage = useMemo(() => joinImageRef(newRepo, newTag), [newRepo, newTag]);
const tagChanged = newTag.trim() !== current.tag;
const repoChanged = newRepo.trim() !== current.repository;
const hasChanges = previewImage !== currentImage;
const syncImage = (repo: string, tag: string) => {
setNewImage(joinImageRef(repo, tag));
};
return (
<ModalShell title="调整镜像版本" onClose={onClose} size="xl">
<div className="space-y-5">
{/* 当前镜像:只读展示完整地址与 Tag // Logan.Li */}
<div className="rounded-lg border border-slate-700/80 bg-slate-800/40 p-4 space-y-3">
<p className="text-xs font-medium text-slate-400">当前镜像</p>
<div className="grid grid-cols-[1fr_180px] gap-3">
<div>
<Label className="text-slate-500 text-xs">镜像地址</Label>
<p
className="mt-1.5 text-sm text-slate-200 font-mono break-all leading-relaxed bg-slate-900/60 rounded-md px-3 py-2.5 min-h-[42px]"
title={current.repository || '-'}
>
{current.repository || '-'}
</p>
</div>
<div>
<Label className="text-slate-500 text-xs">当前 Tag</Label>
<p
className="mt-1.5 text-sm text-cyan-300 font-mono break-all leading-relaxed bg-slate-900/60 rounded-md px-3 py-2.5 min-h-[42px]"
title={current.tag || '-'}
>
{current.tag || '-'}
</p>
</div>
</div>
<p className="text-[11px] text-slate-500 font-mono break-all" title={currentImage}>
完整引用:<span className="text-slate-400">{currentImage || '-'}</span>
</p>
</div>
<div className="flex items-center justify-center text-slate-600">
<ArrowRight className="h-4 w-4" />
</div>
{/* 新镜像:地址与 Tag 分栏编辑 // Logan.Li */}
<div className="rounded-lg border border-blue-500/30 bg-blue-500/5 p-4 space-y-3">
<p className="text-xs font-medium text-blue-300/90">新镜像</p>
<div className="grid grid-cols-[1fr_180px] gap-3">
<div>
<Label className="text-slate-400 text-xs">镜像地址</Label>
<Input
value={newRepo}
onChange={e => {
const v = e.target.value;
setNewRepo(v);
syncImage(v, newTag);
}}
placeholder="harbor.example.com/project/app"
className="mt-1.5 h-10 bg-slate-800 border-slate-700 text-slate-200 text-sm font-mono"
/>
</div>
<div>
<Label className="text-slate-400 text-xs">新版本 Tag</Label>
<Input
value={newTag}
onChange={e => {
const v = e.target.value;
setNewTag(v);
syncImage(newRepo, v);
}}
placeholder="v2.0.0"
className="mt-1.5 h-10 bg-slate-800 border-slate-700 text-cyan-200 text-sm font-mono"
/>
</div>
</div>
<div className="flex flex-wrap items-center gap-2 text-xs">
<span className="text-slate-500">Tag 变更:</span>
<code className="px-2 py-0.5 rounded bg-slate-800 text-slate-400 font-mono">{current.tag}</code>
<ArrowRight className="h-3 w-3 text-slate-600" />
<code
className={`px-2 py-0.5 rounded font-mono ${
tagChanged ? 'bg-cyan-500/15 text-cyan-300' : 'bg-slate-800 text-slate-400'
}`}
>
{newTag.trim() || '-'}
</code>
{!tagChanged && !repoChanged && (
<span className="text-slate-600">(未修改)</span>
)}
{repoChanged && !tagChanged && (
<span className="text-amber-400/80">地址已变更</span>
)}
</div>
<p className="text-[11px] text-slate-500 font-mono break-all">
更新预览:<span className="text-slate-300">{previewImage || '-'}</span>
</p>
</div>
<div className="flex justify-end gap-2 pt-1">
<Button variant="outline" className="border-slate-700 text-slate-400" onClick={onClose}>
取消
</Button>
<Button
className="bg-blue-600 hover:bg-blue-700"
disabled={actionLoading === 'image' || !previewImage.trim() || !hasChanges}
onClick={onUpdateImage}
>
{actionLoading === 'image' ? <Loader2 className="h-4 w-4 animate-spin" /> : '更新镜像'}
</Button>
</div>
</div>
</ModalShell>
);
}