update
This commit is contained in:
1 parent
0690285147
commit
5618cd2738
154 files changed
+5021
-3161
No files matched your search
+53
-1
@@ -1,4 +1,4 @@
|
||||
import { Suspense, lazy } from 'react';
|
||||
import { Suspense, lazy, useEffect, useRef } from 'react';
|
||||
import { BrowserRouter, Routes, Route, Navigate, useLocation } from 'react-router-dom';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ThemeProvider } from '@/hooks/useTheme';
|
||||
@@ -10,6 +10,7 @@ import { PageTransition } from '@/components/layout/PageTransition';
|
||||
import { ErrorBoundary } from '@/components/layout/ErrorBoundary';
|
||||
import { Toaster } from '@/components/ui/sonner';
|
||||
import i18n from '@/i18n';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
// Lazy load page components for code splitting
|
||||
const Dashboard = lazy(() => import('@/components/dashboard/Dashboard').then(m => ({ default: m.Dashboard })));
|
||||
@@ -104,12 +105,63 @@ const queryClient = new QueryClient({
|
||||
},
|
||||
});
|
||||
|
||||
function AssetVersionGuard() {
|
||||
const currentVersionRef = useRef<string>('');
|
||||
const notifiedRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
const readVersion = async () => {
|
||||
try {
|
||||
const resp = await fetch(`/version?t=${Date.now()}`, { cache: 'no-store' });
|
||||
if (!resp.ok) return;
|
||||
const data = await resp.json();
|
||||
const nextVersion = [data.version, data.git_commit, data.build_time].filter(Boolean).join(':');
|
||||
if (!nextVersion) return;
|
||||
if (!currentVersionRef.current) {
|
||||
currentVersionRef.current = nextVersion;
|
||||
return;
|
||||
}
|
||||
if (currentVersionRef.current !== nextVersion && !notifiedRef.current) {
|
||||
notifiedRef.current = true;
|
||||
toast.info(i18n.t('common.appUpdated'), {
|
||||
description: i18n.t('common.appUpdatedDesc'),
|
||||
action: {
|
||||
label: i18n.t('common.reloadPage'),
|
||||
onClick: () => window.location.reload(),
|
||||
},
|
||||
duration: Infinity,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// 版本检查失败不影响业务页面,后续定时器会重试。 // Logan.Li
|
||||
}
|
||||
};
|
||||
|
||||
void readVersion();
|
||||
const timer = window.setInterval(readVersion, 60_000);
|
||||
const handleFocus = () => void readVersion();
|
||||
const handleVisibility = () => {
|
||||
if (document.visibilityState === 'visible') void readVersion();
|
||||
};
|
||||
window.addEventListener('focus', handleFocus);
|
||||
document.addEventListener('visibilitychange', handleVisibility);
|
||||
return () => {
|
||||
window.clearInterval(timer);
|
||||
window.removeEventListener('focus', handleFocus);
|
||||
document.removeEventListener('visibilitychange', handleVisibility);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ConfirmProvider>
|
||||
<Toaster position="top-right" richColors closeButton />
|
||||
<AssetVersionGuard />
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
|
||||
@@ -2,44 +2,47 @@
|
||||
import { http } from '@/utils/request';
|
||||
import { useUserStore } from '@/store';
|
||||
|
||||
// 会话与消息类型(与后端 model.AgentCLISession/Message 对应) // Logan.Li
|
||||
// 会话与消息类型(与后端 model.AIRun/AIRunMessage 对应) // Logan.Li
|
||||
export interface AgentCliSession {
|
||||
id: number;
|
||||
run_id: string;
|
||||
parent_run_id: string;
|
||||
user_id: number;
|
||||
feature: string;
|
||||
mode: string; // host | kubernetes
|
||||
server_id: number;
|
||||
server_name: string;
|
||||
server_host: string;
|
||||
cluster_id: number;
|
||||
cluster_name: string;
|
||||
namespace: string;
|
||||
pod_name: string;
|
||||
container: string;
|
||||
subject_type: string;
|
||||
subject_id: string;
|
||||
subject_name: string;
|
||||
subject_scope: string;
|
||||
title: string;
|
||||
status: string; // running/done/aborted/error
|
||||
provider: string;
|
||||
model: string;
|
||||
safety_mode: string;
|
||||
context_pack_id: string;
|
||||
prompt_tokens: number;
|
||||
completion_tokens: number;
|
||||
rounds: number;
|
||||
total_tokens: number;
|
||||
duration: number;
|
||||
error?: string;
|
||||
started_at: string;
|
||||
finished_at?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface AgentCliMessage {
|
||||
id: number;
|
||||
session_id: number;
|
||||
run_id: string;
|
||||
round: number;
|
||||
seq: number;
|
||||
type: string; // thought/tool_call/tool_result/done/error/user
|
||||
role: string;
|
||||
content: string;
|
||||
tool_name: string;
|
||||
tool_args: string;
|
||||
tool_output: string;
|
||||
risk: string;
|
||||
success: boolean;
|
||||
duration: string;
|
||||
tool_call_id: string;
|
||||
span_id: string;
|
||||
tokens: number;
|
||||
created_at: string;
|
||||
}
|
||||
@@ -96,4 +99,4 @@ export function buildAgentCliWsUrl(params: AgentCliWsParams | number | string):
|
||||
}
|
||||
|
||||
return `${base}?mode=host&id=${params.serverId}&token=${token}`;
|
||||
}
|
||||
}
|
||||
@@ -3,18 +3,6 @@
|
||||
import { http, type ListResponse } from '@/utils/request';
|
||||
|
||||
export const deployApi = {
|
||||
// Git 仓库认证
|
||||
getRepos: () => http.get<ListResponse>('/deploy/repos'),
|
||||
getRepo: (id: number | string) => http.get(`/deploy/repos/${id}`),
|
||||
createRepo: ( data: Record<string, unknown>) => http.post('/deploy/repos', data),
|
||||
updateRepo: (id: number | string, data: Record<string, unknown>) => http.put(`/deploy/repos/${id}`, data),
|
||||
deleteRepo: (id: number | string) => http.delete(`/deploy/repos/${id}`),
|
||||
// 镜像仓库认证
|
||||
getRegistries: () => http.get<ListResponse>('/deploy/registries'),
|
||||
getRegistry: (id: number | string) => http.get(`/deploy/registries/${id}`),
|
||||
createRegistry: ( data: Record<string, unknown>) => http.post('/deploy/registries', data),
|
||||
updateRegistry: (id: number | string, data: Record<string, unknown>) => http.put(`/deploy/registries/${id}`, data),
|
||||
deleteRegistry: (id: number | string) => http.delete(`/deploy/registries/${id}`),
|
||||
// 环境管理
|
||||
getEnvironments: () => http.get<ListResponse>('/deploy/environments'),
|
||||
createEnvironment: ( data: Record<string, unknown>) => http.post('/deploy/environments', data),
|
||||
@@ -45,4 +33,4 @@ export const deployApi = {
|
||||
uninstallTool: (id: number | string) => http.post(`/deploy/tools/${id}/uninstall`),
|
||||
checkTool: (id: number | string) => http.post(`/deploy/tools/${id}/check`),
|
||||
getToolVersions: (id: number | string) => http.get(`/deploy/tools/${id}/versions`),
|
||||
};
|
||||
};
|
||||
@@ -54,17 +54,17 @@ export function messagesToChatItems(msgs: AgentCliMessage[]): ChatItem[] {
|
||||
items.push({ id: nextId(), kind: 'thought', round: m.round, content: m.content });
|
||||
break;
|
||||
case 'status':
|
||||
items.push({ id: nextId(), kind: 'status', round: m.round, phase: m.duration || undefined, content: m.content, toolName: m.tool_name, risk: m.risk });
|
||||
items.push({ id: nextId(), kind: 'status', round: m.round, content: m.content, toolName: m.tool_name });
|
||||
break;
|
||||
case 'tool_call': {
|
||||
let command = '';
|
||||
try {
|
||||
const args = JSON.parse(m.tool_args || '{}') as Record<string, unknown>;
|
||||
command = String(args.command || '');
|
||||
const args = JSON.parse(m.content || '{}') as Record<string, unknown>;
|
||||
command = String(args.command || (args.action ? `${args.action} ${args.name || ''}` : ''));
|
||||
} catch { /* 忽略解析失败 */ }
|
||||
items.push({
|
||||
id: nextId(), kind: 'tool', round: m.round,
|
||||
toolName: m.tool_name, command, risk: m.risk, pending: true,
|
||||
toolName: m.tool_name, command, pending: true,
|
||||
});
|
||||
break;
|
||||
}
|
||||
@@ -73,9 +73,8 @@ export function messagesToChatItems(msgs: AgentCliMessage[]): ChatItem[] {
|
||||
if (items[i].kind === 'tool' && items[i].pending && items[i].toolName === m.tool_name) {
|
||||
items[i] = {
|
||||
...items[i],
|
||||
output: m.content || m.tool_output,
|
||||
success: m.success,
|
||||
duration: m.duration,
|
||||
output: m.content,
|
||||
success: true,
|
||||
pending: false,
|
||||
};
|
||||
break;
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Plus, Key, Globe, Container } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { credentialApi, deployApi } from '@/api';
|
||||
import type { JsonData } from '@/types/common';
|
||||
|
||||
interface Repo {
|
||||
id: number; name: string; url: string; credential_id: number | null;
|
||||
}
|
||||
interface Registry {
|
||||
id: number; name: string; registry_url: string; namespace: string; credential_id: number | null;
|
||||
}
|
||||
interface Credential {
|
||||
id: number; name: string; secret_type: string; summary: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onOpenChange: (v: boolean) => void;
|
||||
type: 'repo' | 'registry';
|
||||
editId: number | null;
|
||||
repos: Repo[];
|
||||
registries: Registry[];
|
||||
onSave: () => void;
|
||||
}
|
||||
|
||||
export default function CredentialFormDialog({ open, onOpenChange, type, editId, repos, registries, onSave }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const isRepo = type === 'repo';
|
||||
const [name, setName] = useState('');
|
||||
const [desc, setDesc] = useState('');
|
||||
const [url, setUrl] = useState('');
|
||||
const [namespace, setNamespace] = useState('');
|
||||
const [credentialId, setCredentialId] = useState<number | string>('');
|
||||
const [credentials, setCredentials] = useState<Credential[]>([]);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
// Docker 仓库 → docker 类型;Git 仓库 → api 或 docker 类型
|
||||
const filteredCredentials = credentials.filter(c =>
|
||||
isRepo ? true : c.secret_type === 'docker'
|
||||
);
|
||||
|
||||
const loadCredentials = async () => {
|
||||
try {
|
||||
const res: JsonData = await credentialApi.list({ page_size: 100 });
|
||||
setCredentials(res?.list || []);
|
||||
} catch { /* silent */ }
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadCredentials();
|
||||
if (editId) {
|
||||
if (isRepo) {
|
||||
const r = repos.find(x => x.id === editId);
|
||||
if (r) { setName(r.name); setDesc(''); setUrl(r.url); setCredentialId(r.credential_id || ''); }
|
||||
} else {
|
||||
const r = registries.find(x => x.id === editId);
|
||||
if (r) { setName(r.name); setDesc(''); setUrl(r.registry_url); setNamespace(r.namespace || ''); setCredentialId(r.credential_id || ''); }
|
||||
}
|
||||
} else {
|
||||
setName(''); setDesc(''); setUrl(''); setNamespace(''); setCredentialId('');
|
||||
}
|
||||
}, [editId, open, isRepo]);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!name.trim() || !url.trim()) { toast.error(t('deploy.form.nameAddrRequired')); return; }
|
||||
setSaving(true);
|
||||
try {
|
||||
const cid = credentialId ? Number(credentialId) : null;
|
||||
const payload = isRepo
|
||||
? { name: name.trim(), description: desc.trim(), url: url.trim(), credential_id: cid }
|
||||
: { name: name.trim(), description: desc.trim(), registry_url: url.trim(), namespace: namespace.trim(), credential_id: cid };
|
||||
if (editId) {
|
||||
if (isRepo) await deployApi.updateRepo(editId, payload);
|
||||
else await deployApi.updateRegistry(editId, payload);
|
||||
toast.success(t('deploy.toast.updated'));
|
||||
} else {
|
||||
if (isRepo) await deployApi.createRepo(payload);
|
||||
else await deployApi.createRegistry(payload);
|
||||
toast.success(t('deploy.toast.created'));
|
||||
}
|
||||
onOpenChange(false);
|
||||
onSave();
|
||||
} catch { toast.error(t('deploy.toast.saveFailed')); }
|
||||
finally { setSaving(false); }
|
||||
};
|
||||
|
||||
const CredIcon = isRepo ? Globe : Container;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[480px] bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
|
||||
<CredIcon className="h-5 w-5 text-[var(--dg-blue)]" />
|
||||
{editId ? t('common.edit') : t('common.add')} {isRepo ? t('deploy.credForm.gitRepo') : t('deploy.credForm.imageRegistry')}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-2">
|
||||
<div className="space-y-1"><Label>{t('deploy.form.nameStar')}</Label>
|
||||
<Input value={name} onChange={e => setName(e.target.value)} placeholder={isRepo ? t('deploy.credForm.repoNamePlaceholder') : t('deploy.credForm.registryNamePlaceholder')} className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" /></div>
|
||||
<div className="space-y-1"><Label>{isRepo ? t('deploy.form.repoUrlStar') : 'Registry URL *'}</Label>
|
||||
<Input value={url} onChange={e => setUrl(e.target.value)} placeholder={isRepo ? 'http://10.10.10.72:9000/group/repo.git' : 'http://10.10.10.72'} className="font-mono text-sm bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" /></div>
|
||||
{!isRepo && <div className="space-y-1"><Label>{t('deploy.form.defaultNamespace')}</Label>
|
||||
<Input value={namespace} onChange={e => setNamespace(e.target.value)} placeholder={t('deploy.credForm.namespacePlaceholder')} className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" /></div>}
|
||||
|
||||
{/* 关联凭证 */}
|
||||
<div className="space-y-1">
|
||||
<Label>{t('deploy.cred.linkedCred')} <span className="text-xs text-[var(--dg-text-secondary)] opacity-70">{t('deploy.credForm.credHint')}</span></Label>
|
||||
<Select value={String(credentialId)} onValueChange={v => setCredentialId(v === '__none' ? '' : v)}>
|
||||
<SelectTrigger className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
|
||||
<SelectValue placeholder={isRepo ? t('deploy.cred.selectApiCred') : t('deploy.cred.selectDockerPwd')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__none">{t('deploy.cred.notLinkedCred')}</SelectItem>
|
||||
{filteredCredentials.map(c => (
|
||||
<SelectItem key={c.id} value={String(c.id)}>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<Key className="h-3 w-3 opacity-60" />
|
||||
{c.name} {c.summary && <span className="text-[10px] opacity-50">({c.summary})</span>}
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{filteredCredentials.length === 0 && (
|
||||
<p className="text-xs dg-text-yellow flex items-center gap-1 mt-1">
|
||||
<Plus className="h-3 w-3" />
|
||||
{t('deploy.credForm.noCredHint', { type: isRepo ? '' : t('deploy.credForm.dockerPwd') })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>{t('common.cancel')}</Button>
|
||||
<Button onClick={handleSave} disabled={saving}>{saving ? t('deploy.pipeline.saving') : t('common.save')}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Plus, Pencil, Trash2, RefreshCw, GitBranch, Container, Globe, Settings2 } from 'lucide-react';
|
||||
import { Plus, Pencil, Trash2, RefreshCw, Globe, Settings2 } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -12,24 +12,12 @@ import { useSearchParams } from 'react-router-dom';
|
||||
import BuildNodesManager from './BuildNodesManager';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { toast } from 'sonner';
|
||||
import { useConfirm } from '@/components/ui/confirm-dialog';
|
||||
import { deployApi } from '@/api';
|
||||
import CredentialFormDialog from './CredentialFormDialog';
|
||||
import ToolConfigTab from './ToolConfigTab';
|
||||
import type { JsonData } from '@/types/common';
|
||||
|
||||
interface Repo {
|
||||
id: number; name: string; description: string; url: string;
|
||||
auth_type: string; credential_id: number | null; enabled: boolean;
|
||||
created_by: string; updated_at: string;
|
||||
}
|
||||
interface Registry {
|
||||
id: number; name: string; description: string; registry_url: string; namespace: string;
|
||||
auth_type: string; credential_id: number | null; enabled: boolean;
|
||||
created_by: string; updated_at: string;
|
||||
}
|
||||
interface Environment {
|
||||
id: number; name: string; display_name: string; type: string; color: string;
|
||||
sort_order: number; description: string; enabled: boolean;
|
||||
@@ -74,7 +62,10 @@ function EnvFormDialog({ open, onClose, onSaved, editData }: { open: boolean; on
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogContent>
|
||||
<DialogHeader><DialogTitle>{editData ? t('deploy.env.editEnv') : t('deploy.env.addEnv')}</DialogTitle></DialogHeader>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editData ? t('deploy.env.editEnv') : t('deploy.env.addEnv')}</DialogTitle>
|
||||
<DialogDescription>{t('deploy.label.envManage')}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1"><Label>{t('deploy.env.envLabel')}</Label><Input value={name} onChange={e => setName(e.target.value)} placeholder="production" /></div>
|
||||
<div className="space-y-1"><Label>{t('deploy.table.displayName')}</Label><Input value={displayName} onChange={e => setDisplayName(e.target.value)} placeholder={t('deploy.env.prodEnv')} /></div>
|
||||
@@ -102,46 +93,42 @@ function EnvFormDialog({ open, onClose, onSaved, editData }: { open: boolean; on
|
||||
);
|
||||
}
|
||||
|
||||
const CONFIG_TABS = ['repos', 'registries', 'environments', 'tools', 'build-nodes'] as const;
|
||||
const CONFIG_TABS = ['environments', 'build-nodes', 'tools'] as const;
|
||||
|
||||
export default function DeployCredentials() {
|
||||
const { t } = useTranslation();
|
||||
const _confirm = useConfirm();
|
||||
const confirm = useConfirm();
|
||||
const [searchParams] = useSearchParams();
|
||||
const tabFromUrl = searchParams.get('tab');
|
||||
const [repos, setRepos] = useState<Repo[]>([]);
|
||||
const [registries, setRegistries] = useState<Registry[]>([]);
|
||||
const [environments, setEnvironments] = useState<Environment[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [editorOpen, setEditorOpen] = useState(false);
|
||||
const [editType, setEditType] = useState<'repo' | 'registry'>('repo');
|
||||
const [editingId, setEditingId] = useState<number | null>(null);
|
||||
const [envDialogOpen, setEnvDialogOpen] = useState(false);
|
||||
const [editEnv, setEditEnv] = useState<Environment | null>(null);
|
||||
const [activeTab, setActiveTab] = useState(
|
||||
tabFromUrl && (CONFIG_TABS as readonly string[]).includes(tabFromUrl) ? tabFromUrl : 'repos',
|
||||
tabFromUrl && (CONFIG_TABS as readonly string[]).includes(tabFromUrl) ? tabFromUrl : 'environments',
|
||||
);
|
||||
|
||||
const fetchAll = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [rR, rReg, rEnv] = await Promise.all([deployApi.getRepos(), deployApi.getRegistries(), deployApi.getEnvironments()]);
|
||||
setRepos((rR as JsonData)?.list || []);
|
||||
setRegistries((rReg as JsonData)?.list || []);
|
||||
setEnvironments((rEnv as JsonData)?.list || []);
|
||||
const rEnv = await deployApi.getEnvironments();
|
||||
setEnvironments((rEnv as { list?: Environment[] })?.list || []);
|
||||
} catch { toast.error(t('deploy.toast.getDataFailed')); }
|
||||
finally { setLoading(false); }
|
||||
};
|
||||
|
||||
useEffect(() => { fetchAll(); }, []);
|
||||
|
||||
const handleDelete = async (type: 'repo' | 'registry' | 'env', item: JsonData) => {
|
||||
const ok = await _confirm({ title: t('common.delete'), description: t('deploy.toast.deleteConfirm', { name: item.name || item.display_name }), confirmText: t('common.delete'), variant: 'destructive' });
|
||||
const handleDeleteEnv = async (item: Environment) => {
|
||||
const ok = await confirm({
|
||||
title: t('common.delete'),
|
||||
description: t('deploy.toast.deleteConfirm', { name: item.display_name || item.name }),
|
||||
confirmText: t('common.delete'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
if (!ok) return;
|
||||
try {
|
||||
if (type === 'repo') await deployApi.deleteRepo(item.id);
|
||||
else if (type === 'registry') await deployApi.deleteRegistry(item.id);
|
||||
else await deployApi.deleteEnvironment(item.id);
|
||||
await deployApi.deleteEnvironment(item.id);
|
||||
toast.success(t('deploy.toast.deleted'));
|
||||
fetchAll();
|
||||
} catch { toast.error(t('deploy.toast.deleteFailed')); }
|
||||
@@ -150,8 +137,6 @@ export default function DeployCredentials() {
|
||||
const envTypeLabel: Record<string, string> = { production: t('deploy.env.production'), staging: t('deploy.env.staging'), testing: t('deploy.env.testing'), development: t('deploy.env.development') };
|
||||
|
||||
const pageTabs = [
|
||||
{ id: 'repos', label: t('deploy.credForm.gitRepo'), badge: repos.length || undefined },
|
||||
{ id: 'registries', label: t('deploy.credForm.imageRegistry'), badge: registries.length || undefined },
|
||||
{ id: 'environments', label: t('deploy.label.envManage'), badge: environments.length || undefined },
|
||||
{ id: 'build-nodes', label: t('deploy.deployNav.buildNodes') },
|
||||
{ id: 'tools', label: t('deploy.tool.title') },
|
||||
@@ -168,109 +153,25 @@ export default function DeployCredentials() {
|
||||
activeTab={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
actions={activeTab === 'build-nodes' ? undefined : (
|
||||
<>
|
||||
<Button variant="outline" size="sm" className="dg-btn-secondary" onClick={fetchAll}>
|
||||
<RefreshCw className="w-4 h-4 mr-1" />{t('common.refresh')}
|
||||
</Button>
|
||||
<Button size="sm" className="dg-btn-primary" onClick={() => { setEditType('repo'); setEditingId(null); setEditorOpen(true); }}>
|
||||
<Plus className="w-4 h-4 mr-1" />{t('deploy.credForm.newRepo')}
|
||||
</Button>
|
||||
<Button size="sm" variant="secondary" onClick={() => { setEditType('registry'); setEditingId(null); setEditorOpen(true); }}>
|
||||
<Plus className="w-4 h-4 mr-1" />{t('deploy.credForm.newRegistry')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
contentClassName="space-y-4"
|
||||
>
|
||||
{activeTab === 'repos' && (
|
||||
repos.length === 0 ? (
|
||||
<PageEmpty icon={<GitBranch className="h-10 w-10" />} title={t('deploy.credForm.noRepo')} />
|
||||
) : (
|
||||
<ContentPanel noPadding>
|
||||
<div className="overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-[var(--dg-bg-tertiary)] border-b border-[var(--dg-border)]">
|
||||
<tr>
|
||||
<th className="px-3 py-2 text-left font-medium text-[var(--dg-text-secondary)]">{t('deploy.table.name')}</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-[var(--dg-text-secondary)] hidden md:table-cell">{t('deploy.table.repoUrl')}</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-[var(--dg-text-secondary)]">{t('deploy.cred.authMethod')}</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-[var(--dg-text-secondary)]">{t('deploy.table.status')}</th>
|
||||
<th className="px-3 py-2 text-right font-medium text-[var(--dg-text-secondary)]">{t('common.actions')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{repos.map(r => (
|
||||
<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.name}</td>
|
||||
<td className="px-3 py-2 text-[var(--dg-text-secondary)] hidden md:table-cell font-mono text-xs max-w-[300px] truncate">{r.url}</td>
|
||||
<td className="px-3 py-2"><Badge variant="outline" className="text-xs">{r.auth_type}</Badge></td>
|
||||
<td className="px-3 py-2">
|
||||
<Badge className={r.enabled ? 'bg-emerald-500/20 dg-text-green border-emerald-500/30' : 'bg-[var(--dg-bg-active)]/20 dg-text-secondary border-[var(--dg-border)]'}>{r.enabled ? t('common.enable') : t('common.disable')}</Badge>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right">
|
||||
<div className="flex justify-end gap-1">
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => { setEditType('repo'); setEditingId(r.id); setEditorOpen(true); }}><Pencil className="w-3.5 h-3.5" /></Button>
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-red-500" onClick={() => handleDelete('repo', r)}><Trash2 className="w-3.5 h-3.5" /></Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ContentPanel>
|
||||
)
|
||||
)}
|
||||
|
||||
{activeTab === 'registries' && (
|
||||
registries.length === 0 ? (
|
||||
<PageEmpty icon={<Container className="h-10 w-10" />} title={t('deploy.credForm.noRegistry')} />
|
||||
) : (
|
||||
<ContentPanel noPadding>
|
||||
<div className="overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-[var(--dg-bg-tertiary)] border-b border-[var(--dg-border)]">
|
||||
<tr>
|
||||
<th className="px-3 py-2 text-left font-medium text-[var(--dg-text-secondary)]">{t('deploy.table.name')}</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-[var(--dg-text-secondary)] hidden md:table-cell">Registry URL</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-[var(--dg-text-secondary)] hidden sm:table-cell">{t('deploy.credForm.namespace')}</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-[var(--dg-text-secondary)]">{t('deploy.cred.authMethod')}</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-[var(--dg-text-secondary)]">{t('deploy.table.status')}</th>
|
||||
<th className="px-3 py-2 text-right font-medium text-[var(--dg-text-secondary)]">{t('common.actions')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{registries.map(r => (
|
||||
<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.name}</td>
|
||||
<td className="px-3 py-2 text-[var(--dg-text-secondary)] hidden md:table-cell font-mono text-xs max-w-[250px] truncate">{r.registry_url}</td>
|
||||
<td className="px-3 py-2 text-[var(--dg-text-secondary)] hidden sm:table-cell">{r.namespace || '-'}</td>
|
||||
<td className="px-3 py-2"><Badge variant="outline" className="text-xs">{r.auth_type}</Badge></td>
|
||||
<td className="px-3 py-2">
|
||||
<Badge className={r.enabled ? 'bg-emerald-500/20 dg-text-green border-emerald-500/30' : 'bg-[var(--dg-bg-active)]/20 dg-text-secondary border-[var(--dg-border)]'}>{r.enabled ? t('common.enable') : t('common.disable')}</Badge>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right">
|
||||
<div className="flex justify-end gap-1">
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => { setEditType('registry'); setEditingId(r.id); setEditorOpen(true); }}><Pencil className="w-3.5 h-3.5" /></Button>
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-red-500" onClick={() => handleDelete('registry', r)}><Trash2 className="w-3.5 h-3.5" /></Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ContentPanel>
|
||||
)
|
||||
)}
|
||||
|
||||
{activeTab === 'environments' && (
|
||||
<>
|
||||
<div className="flex items-center justify-end mb-3">
|
||||
activeTab === 'environments' ? (
|
||||
<>
|
||||
<Button variant="outline" size="sm" className="dg-btn-secondary" onClick={fetchAll}>
|
||||
<RefreshCw className="w-4 h-4 mr-1" />{t('common.refresh')}
|
||||
</Button>
|
||||
<Button size="sm" className="dg-btn-primary" onClick={() => { setEditEnv(null); setEnvDialogOpen(true); }}>
|
||||
<Plus className="w-4 h-4 mr-1" />新增环境
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<Button variant="outline" size="sm" className="dg-btn-secondary" onClick={fetchAll}>
|
||||
<RefreshCw className="w-4 h-4 mr-1" />{t('common.refresh')}
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
contentClassName="space-y-4"
|
||||
>
|
||||
{activeTab === 'environments' && (
|
||||
<>
|
||||
{environments.length === 0 ? (
|
||||
<PageEmpty
|
||||
icon={<Globe className="h-10 w-10" />}
|
||||
@@ -316,7 +217,7 @@ export default function DeployCredentials() {
|
||||
<td className="px-3 py-2 text-right">
|
||||
<div className="flex justify-end gap-1">
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => { setEditEnv(env); setEnvDialogOpen(true); }}><Pencil className="w-3.5 h-3.5" /></Button>
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-red-500" onClick={() => handleDelete('env', env)}><Trash2 className="w-3.5 h-3.5" /></Button>
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-red-500" onClick={() => handleDeleteEnv(env)}><Trash2 className="w-3.5 h-3.5" /></Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -333,11 +234,6 @@ export default function DeployCredentials() {
|
||||
|
||||
{activeTab === 'tools' && <ToolConfigTab />}
|
||||
|
||||
<CredentialFormDialog
|
||||
open={editorOpen} onOpenChange={setEditorOpen} type={editType} editId={editingId}
|
||||
repos={repos} registries={registries} onSave={fetchAll}
|
||||
/>
|
||||
|
||||
<EnvFormDialog open={envDialogOpen} onClose={() => setEnvDialogOpen(false)} onSaved={fetchAll} editData={editEnv} />
|
||||
</PageShell>
|
||||
);
|
||||
|
||||
@@ -14,8 +14,6 @@ import {
|
||||
Trash2,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Container,
|
||||
Globe,
|
||||
History,
|
||||
Server,
|
||||
Network,
|
||||
@@ -54,8 +52,6 @@ export function PipelineCard({
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const stages = parseStages(pipeline.stages);
|
||||
const repo = pipeline.repo as Record<string, unknown> | undefined;
|
||||
const registry = pipeline.registry as Record<string, unknown> | undefined;
|
||||
const environment = pipeline.environment as Record<string, unknown> | undefined;
|
||||
const code = (pipeline.code || pipeline.id) as string;
|
||||
const deployTarget = String(pipeline.deploy_target || 'ssh');
|
||||
@@ -151,8 +147,6 @@ export function PipelineCard({
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
{repo && <span className="flex items-center gap-1"><Globe className="h-3 w-3" />{String(repo.name)}</span>}
|
||||
{registry && <span className="flex items-center gap-1"><Container className="h-3 w-3" />{String(registry.name)}</span>}
|
||||
{pipeline.last_run_at && (
|
||||
<span><Clock className="h-3 w-3 inline mr-0.5" />{new Date(pipeline.last_run_at as string).toLocaleString('zh-CN')}</span>
|
||||
)}
|
||||
@@ -260,4 +254,4 @@ export function PipelineCard({
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,15 @@ interface State {
|
||||
errorInfo: ErrorInfo | null;
|
||||
}
|
||||
|
||||
function isChunkLoadError(error: Error | null) {
|
||||
// 发版后旧入口引用已删除 chunk 时,浏览器会抛动态 import 加载失败。 // Logan.Li
|
||||
const message = error?.message || '';
|
||||
return message.includes('Failed to fetch dynamically imported module') ||
|
||||
message.includes('Importing a module script failed') ||
|
||||
message.includes('ChunkLoadError') ||
|
||||
message.includes('Loading chunk');
|
||||
}
|
||||
|
||||
/**
|
||||
* ErrorBoundary - 全局错误边界组件
|
||||
*
|
||||
@@ -38,6 +47,10 @@ export class ErrorBoundary extends Component<Props, State> {
|
||||
}
|
||||
|
||||
handleRetry = () => {
|
||||
if (isChunkLoadError(this.state.error)) {
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
this.setState({ hasError: false, error: null, errorInfo: null });
|
||||
};
|
||||
|
||||
@@ -60,6 +73,7 @@ export class ErrorBoundary extends Component<Props, State> {
|
||||
if (this.props.fallback) {
|
||||
return this.props.fallback;
|
||||
}
|
||||
const chunkLoadError = isChunkLoadError(this.state.error);
|
||||
|
||||
return (
|
||||
<div className="min-h-[60vh] flex items-center justify-center p-6">
|
||||
@@ -75,7 +89,7 @@ export class ErrorBoundary extends Component<Props, State> {
|
||||
{i18n.t('common.ebPageError')}
|
||||
</h2>
|
||||
<p className="text-sm text-[var(--dg-text-muted)] mt-2">
|
||||
{i18n.t('common.ebDescription')}
|
||||
{chunkLoadError ? i18n.t('common.ebChunkDescription') : i18n.t('common.ebDescription')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -116,7 +130,7 @@ export class ErrorBoundary extends Component<Props, State> {
|
||||
onClick={this.handleRetry}
|
||||
>
|
||||
<RotateCcw className="h-4 w-4 mr-1.5" />
|
||||
{i18n.t('common.reload')}
|
||||
{chunkLoadError ? i18n.t('common.reloadPage') : i18n.t('common.reload')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
|
||||
@@ -283,6 +283,13 @@ export function CloudServerImport({ open, onClose, initialAccountId }: CloudServ
|
||||
r.code?.toLowerCase().includes(regionSearch.toLowerCase())
|
||||
);
|
||||
|
||||
const getImportHostname = (server: any) => {
|
||||
// 阿里云 HostName 常是 iZ 开头的系统名,导入预览优先展示控制台实例名称。 // Logan.Li
|
||||
if (server.instance_name && server.instance_name !== server.instance_id) return server.instance_name;
|
||||
if (server.hostname && server.hostname !== server.instance_id) return server.hostname;
|
||||
return server.private_ip || server.public_ip || server.instance_name || server.instance_id || '-';
|
||||
};
|
||||
|
||||
const steps = [
|
||||
{ id: 1, title: t('server.cloudProvider'), icon: Cloud },
|
||||
{ id: 2, title: t('server.cloudAccount'), icon: Key },
|
||||
@@ -620,7 +627,7 @@ export function CloudServerImport({ open, onClose, initialAccountId }: CloudServ
|
||||
<td className="px-3 py-2">
|
||||
<Checkbox checked={selectedServers.includes(server.instance_id)} />
|
||||
</td>
|
||||
<td className="px-3 py-2 text-sm text-[var(--dg-text-primary)]">{server.instance_name || server.hostname || '-'}</td>
|
||||
<td className="px-3 py-2 text-sm text-[var(--dg-text-primary)]">{getImportHostname(server)}</td>
|
||||
<td className="px-3 py-2 text-sm text-[var(--dg-text-secondary)]">
|
||||
{server.private_ip || server.public_ip || '-'}
|
||||
</td>
|
||||
|
||||
@@ -33,6 +33,7 @@ import type { JsonData } from '@/types/common';
|
||||
// 表单数据结构
|
||||
export interface ServerFormData {
|
||||
hostname: string;
|
||||
alias: string;
|
||||
ip: string;
|
||||
public_ip: string;
|
||||
port: string;
|
||||
@@ -55,6 +56,7 @@ export interface ServerFormData {
|
||||
|
||||
export const EMPTY_FORM: ServerFormData = {
|
||||
hostname: '',
|
||||
alias: '',
|
||||
ip: '',
|
||||
public_ip: '',
|
||||
port: '22',
|
||||
@@ -183,6 +185,17 @@ export function ServerForm({
|
||||
<div className="space-y-4 py-2">
|
||||
{/* 基础信息 */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-[var(--dg-text-secondary)]">
|
||||
{t('server.formAlias')}
|
||||
</Label>
|
||||
<Input
|
||||
placeholder={t('server.aliasPlaceholder')}
|
||||
value={formData.alias}
|
||||
onChange={(e) => setFormData({ ...formData, alias: e.target.value })}
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-[var(--dg-text-secondary)]">
|
||||
{t('server.formHostname')} <span className="dg-text-red">*</span>
|
||||
|
||||
@@ -122,10 +122,13 @@ export function ServerManager() {
|
||||
const servers = data?.list ?? [];
|
||||
const total = data?.total ?? 0;
|
||||
|
||||
const getServerDisplayName = (server: JsonData) =>
|
||||
server.alias || server.cloud_instance_name || server.hostname || server.ip || "";
|
||||
|
||||
const openTerminal = (server: JsonData) => {
|
||||
setTerminalServer({
|
||||
id: server.id,
|
||||
name: server.hostname,
|
||||
name: getServerDisplayName(server),
|
||||
ip: server.ip,
|
||||
hasAuth: server.has_auth ?? true,
|
||||
});
|
||||
@@ -133,12 +136,12 @@ export function ServerManager() {
|
||||
};
|
||||
|
||||
const openRdp = (server: JsonData) => {
|
||||
setRdpServer({ id: server.id, name: server.hostname, ip: server.ip });
|
||||
setRdpServer({ id: server.id, name: getServerDisplayName(server), ip: server.ip });
|
||||
setRdpOpen(true);
|
||||
};
|
||||
|
||||
const openFileBrowser = (server: JsonData) => {
|
||||
setFileBrowserServer({ id: server.id, name: server.hostname });
|
||||
setFileBrowserServer({ id: server.id, name: getServerDisplayName(server) });
|
||||
setFileBrowserOpen(true);
|
||||
};
|
||||
|
||||
@@ -171,9 +174,13 @@ export function ServerManager() {
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
const server = servers.find((s) => String(s.id) === String(id));
|
||||
const name = server ? getServerDisplayName(server) : id;
|
||||
const ok = await confirm({
|
||||
title: t("server.deleteTitle"),
|
||||
description: `${server?.hostname || id}`,
|
||||
description: t("server.deleteConfirm", {
|
||||
name,
|
||||
ip: server?.ip || "-",
|
||||
}),
|
||||
confirmText: t("common.delete"),
|
||||
variant: "destructive",
|
||||
});
|
||||
@@ -192,7 +199,9 @@ export function ServerManager() {
|
||||
if (selectedServers.length === 0) return;
|
||||
const ok = await confirm({
|
||||
title: t("server.batchDeleteTitle"),
|
||||
description: `${selectedServers.length}`,
|
||||
description: t("server.batchDeleteConfirm", {
|
||||
count: selectedServers.length,
|
||||
}),
|
||||
confirmText: t("server.batchDelete"),
|
||||
variant: "destructive",
|
||||
});
|
||||
@@ -224,6 +233,7 @@ export function ServerManager() {
|
||||
setEditingId(server.id);
|
||||
setFormData({
|
||||
hostname: server.hostname || "",
|
||||
alias: server.alias || "",
|
||||
ip: server.ip || "",
|
||||
public_ip: server.public_ip || "",
|
||||
port: String(server.port || 22),
|
||||
@@ -256,6 +266,7 @@ export function ServerManager() {
|
||||
setFormSaving(true);
|
||||
const payload: Record<string, unknown> = {
|
||||
hostname: formData.hostname,
|
||||
alias: formData.alias,
|
||||
ip: formData.ip,
|
||||
public_ip: formData.public_ip || undefined,
|
||||
port: Number(formData.port) || 22,
|
||||
@@ -306,6 +317,7 @@ export function ServerManager() {
|
||||
return;
|
||||
}
|
||||
const headers = [
|
||||
t("server.csv.name"),
|
||||
t("server.csv.hostname"),
|
||||
t("server.csv.ip"),
|
||||
t("server.csv.publicIp"),
|
||||
@@ -321,6 +333,7 @@ export function ServerManager() {
|
||||
const memGB = s.memory ? Math.round(Number(s.memory) / 1024) : 0;
|
||||
const tags = getTags(s).join(";");
|
||||
return [
|
||||
getServerDisplayName(s),
|
||||
s.hostname || "",
|
||||
s.ip || "",
|
||||
s.public_ip || "",
|
||||
@@ -482,7 +495,7 @@ export function ServerManager() {
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">
|
||||
{t("server.hostname")}
|
||||
{t("server.assetName")}
|
||||
</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">
|
||||
{t("server.ipAddress")}
|
||||
|
||||
@@ -52,6 +52,8 @@ export function ServerTableRow({
|
||||
const tags = getTags(server);
|
||||
const memGB = server.memory ? Math.round(server.memory / 1024) : 0;
|
||||
const isRdp = isRdpServer(server);
|
||||
const displayName = server.alias || server.cloud_instance_name || server.hostname || server.ip;
|
||||
const secondaryName = server.hostname && server.hostname !== displayName ? server.hostname : server.os;
|
||||
|
||||
return (
|
||||
<TableRow className="border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/50">
|
||||
@@ -66,9 +68,12 @@ export function ServerTableRow({
|
||||
<ServerIcon className="h-4 w-4 text-[var(--dg-text-muted)]" />
|
||||
<div>
|
||||
<p className="font-medium text-[var(--dg-text-primary)]">
|
||||
{server.hostname}
|
||||
{displayName}
|
||||
</p>
|
||||
<p className="text-xs text-[var(--dg-text-muted)]">
|
||||
{secondaryName}
|
||||
{server.cloud_instance_id ? ` · ${server.cloud_instance_id}` : ""}
|
||||
</p>
|
||||
<p className="text-xs text-[var(--dg-text-muted)]">{server.os}</p>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
"analyzing": "Analyzing",
|
||||
"annotation": "Annotation",
|
||||
"appName": "AIOps Platform",
|
||||
"appUpdated": "System updated",
|
||||
"appUpdatedDesc": "A new frontend version is available. Refresh the page before continuing.",
|
||||
"apply": "Apply",
|
||||
"applyFilter": "Apply Filter",
|
||||
"approve": "Approve",
|
||||
@@ -144,6 +146,7 @@
|
||||
},
|
||||
"ebCaughtError": "[ErrorBoundary] Caught error:",
|
||||
"ebCopyError": "Copy Error",
|
||||
"ebChunkDescription": "Page assets may have been updated or temporarily unavailable. Please refresh to load the latest version.",
|
||||
"ebDescription": "Sorry, this page encountered an unexpected error. You can try reloading or returning to home.",
|
||||
"ebErrorInfo": "[ErrorBoundary] Error info:",
|
||||
"ebGoHome": "Back to Home",
|
||||
@@ -759,8 +762,9 @@
|
||||
"csvImport": "CSV Import",
|
||||
"csvImportHint": "hostname + IP only",
|
||||
"addServer": "Add Server",
|
||||
"searchPlaceholder": "Search hostname or IP...",
|
||||
"searchPlaceholder": "Search alias, cloud name, hostname or IP...",
|
||||
"allStatus": "All Status",
|
||||
"assetName": "Asset Name",
|
||||
"hostname": "Hostname",
|
||||
"ipAddress": "IP Address",
|
||||
"publicIp": "Public IP",
|
||||
@@ -774,8 +778,10 @@
|
||||
"items": "items",
|
||||
"totalItems": "{{count}} total",
|
||||
"deleteTitle": "Delete Server",
|
||||
"deleteConfirm": "Delete server \"{{name}}\" ({{ip}})? This action cannot be undone.",
|
||||
"deleted": "Server deleted",
|
||||
"batchDeleteTitle": "Batch Delete Servers",
|
||||
"batchDeleteConfirm": "Delete the selected {{count}} servers? This action cannot be undone.",
|
||||
"updated": "Server updated",
|
||||
"created": "Server created",
|
||||
"saveFailed": "Save failed, please retry",
|
||||
@@ -785,6 +791,7 @@
|
||||
"deleteFailed": "Delete failed",
|
||||
"batchDeleteFailed": "Batch delete failed",
|
||||
"csv": {
|
||||
"name": "Asset Name",
|
||||
"hostname": "Hostname",
|
||||
"ip": "IP Address",
|
||||
"publicIp": "Public IP",
|
||||
@@ -1076,6 +1083,8 @@
|
||||
"editServer": "Edit Server",
|
||||
"editServerDesc": "Modify server asset information",
|
||||
"addServerDesc": "Add a new server asset to CMDB",
|
||||
"formAlias": "Alias",
|
||||
"aliasPlaceholder": "e.g. prod-gateway, order-svc-01",
|
||||
"formHostname": "Hostname",
|
||||
"formIpAddress": "IP Address",
|
||||
"osType": "OS Type",
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
"analyzing": "分析中",
|
||||
"annotation": "注解",
|
||||
"appName": "AIOps平台",
|
||||
"appUpdated": "系统已更新",
|
||||
"appUpdatedDesc": "检测到前端资源已有新版本,建议刷新页面后继续操作。",
|
||||
"apply": "应用",
|
||||
"applyFilter": "应用筛选",
|
||||
"approve": "通过",
|
||||
@@ -144,6 +146,7 @@
|
||||
},
|
||||
"ebCaughtError": "[ErrorBoundary] 捕获到错误:",
|
||||
"ebCopyError": "复制错误",
|
||||
"ebChunkDescription": "页面资源可能已更新或短暂不可用,请刷新页面重新加载最新资源。",
|
||||
"ebDescription": "抱歉,该页面遇到了意外错误。您可以尝试重新加载或返回首页。",
|
||||
"ebErrorInfo": "[ErrorBoundary] 错误信息:",
|
||||
"ebGoHome": "返回首页",
|
||||
@@ -759,8 +762,9 @@
|
||||
"csvImport": "CSV导入",
|
||||
"csvImportHint": "hostname + IP 即可",
|
||||
"addServer": "添加服务器",
|
||||
"searchPlaceholder": "搜索主机名或IP...",
|
||||
"searchPlaceholder": "搜索别名、云实例名、主机名或IP...",
|
||||
"allStatus": "全部状态",
|
||||
"assetName": "资产名称",
|
||||
"hostname": "主机名",
|
||||
"ipAddress": "IP地址",
|
||||
"publicIp": "公网IP",
|
||||
@@ -774,8 +778,10 @@
|
||||
"items": "条",
|
||||
"totalItems": "共 {{count}} 条",
|
||||
"deleteTitle": "删除服务器",
|
||||
"deleteConfirm": "确认删除服务器「{{name}}」({{ip}})?此操作不可撤销。",
|
||||
"deleted": "服务器已删除",
|
||||
"batchDeleteTitle": "批量删除服务器",
|
||||
"batchDeleteConfirm": "确认删除选中的 {{count}} 台服务器?此操作不可撤销。",
|
||||
"updated": "服务器已更新",
|
||||
"created": "服务器已创建",
|
||||
"saveFailed": "保存失败,请重试",
|
||||
@@ -785,6 +791,7 @@
|
||||
"deleteFailed": "删除失败",
|
||||
"batchDeleteFailed": "批量删除失败",
|
||||
"csv": {
|
||||
"name": "资产名称",
|
||||
"hostname": "主机名",
|
||||
"ip": "IP地址",
|
||||
"publicIp": "公网IP",
|
||||
@@ -1076,6 +1083,8 @@
|
||||
"editServer": "编辑服务器",
|
||||
"editServerDesc": "修改服务器资产信息",
|
||||
"addServerDesc": "录入新的服务器资产到 CMDB",
|
||||
"formAlias": "别名",
|
||||
"aliasPlaceholder": "如:生产网关、订单服务-01",
|
||||
"formHostname": "主机名",
|
||||
"formIpAddress": "IP地址",
|
||||
"osType": "系统类型",
|
||||
|
||||
@@ -9,6 +9,11 @@ import App from './App.tsx'
|
||||
// 首屏前应用主题,避免闪烁 // Logan.Li
|
||||
applyThemeToDocument(getInitialTheme())
|
||||
|
||||
window.addEventListener('vite:preloadError', () => {
|
||||
// 任一左侧子菜单懒加载资源在发版后失效时,统一整页刷新获取最新 index/chunk。 // Logan.Li
|
||||
window.location.reload()
|
||||
})
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
|
||||
@@ -31,7 +31,7 @@ export interface PipelineStage {
|
||||
command: string;
|
||||
artifact_path: string;
|
||||
image: string;
|
||||
registry_id: number;
|
||||
registry_credential_id: number;
|
||||
dockerfile: string;
|
||||
env_vars: Record<string, string>;
|
||||
timeout: number;
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
export interface AssetServer {
|
||||
id: number;
|
||||
hostname: string;
|
||||
alias: string;
|
||||
cloud_instance_name: string;
|
||||
cloud_instance_id: string;
|
||||
ip: string;
|
||||
port: number; // SSH 端口,默认 22
|
||||
os: string;
|
||||
@@ -36,6 +39,9 @@ export interface AssetServer {
|
||||
export interface AssetServerListItem {
|
||||
id: number;
|
||||
hostname: string;
|
||||
alias: string;
|
||||
cloud_instance_name: string;
|
||||
cloud_instance_id: string;
|
||||
ip: string;
|
||||
os: string;
|
||||
status: string;
|
||||
|
||||
Reference in new issue
Block a user