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:
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 <密钥></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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user