diff --git a/Dockerfile.embed b/Dockerfile.embed index 8652c0c..cd93052 100644 --- a/Dockerfile.embed +++ b/Dockerfile.embed @@ -36,7 +36,7 @@ WORKDIR /app # 使用国内 Alpine 镜像源加速 RUN sed -i 's|dl-cdn.alpinelinux.org|mirrors.tuna.tsinghua.edu.cn|g' /etc/apk/repositories -RUN apk add --no-cache git ca-certificates tzdata +RUN apk add --no-cache git ca-certificates tzdata gcc musl-dev ENV TZ=Asia/Shanghai ENV GOPROXY=https://goproxy.cn,direct @@ -54,7 +54,8 @@ RUN rm -rf static && mkdir -p static COPY --from=frontend-builder /app/dist/ static/ # 构建后端二进制(前端已嵌入,注入版本号) -RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ +# local 模式需 CGO(SQLite + redka/mattn-sqlite3)// Logan.Li +RUN CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build \ -ldflags="-w -s -X main.Version=${VERSION} -X main.BuildTime=${BUILD_TIME}" \ -o aiops . diff --git a/deploy/kubernetes/deployment.yaml b/deploy/kubernetes/deployment.yaml index 202439a..77fbe93 100644 --- a/deploy/kubernetes/deployment.yaml +++ b/deploy/kubernetes/deployment.yaml @@ -17,6 +17,7 @@ metadata: app.kubernetes.io/name: aiops app.kubernetes.io/component: app spec: + # MODE=local 使用 SQLite,必须保持 1 副本(多副本数据不共享)// Logan.Li replicas: 1 strategy: type: RollingUpdate diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e7d0fca..58463ae 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -30,6 +30,7 @@ const KubernetesNodes = lazy(() => import('@/components/kubernetes/KubernetesNod const PodMetricsPage = lazy(() => import('@/components/kubernetes/PodMetricsPage').then(m => ({ default: m.default }))); const EventsPage = lazy(() => import('@/components/kubernetes/EventsPage').then(m => ({ default: m.default }))); const CapacityPage = lazy(() => import('@/components/kubernetes/CapacityPage').then(m => ({ default: m.default }))); +const K8sShortcutRedirect = lazy(() => import('@/components/kubernetes/K8sShortcutRedirect').then(m => ({ default: m.default }))); const AlertManager = lazy(() => import('@/components/monitor/AlertManager').then(m => ({ default: m.AlertManager }))); const ApprovalFlowManager = lazy(() => import('@/components/approval/ApprovalFlow').then(m => ({ default: m.ApprovalFlowManager }))); const ApprovalResourcePage = lazy(() => import('@/pages/ApprovalResourcePage').then(m => ({ default: m.ApprovalResourcePage }))); @@ -154,6 +155,7 @@ function App() { {/* Quick Access Routes (redirect to first cluster) */} } /> + } /> } /> } /> } /> diff --git a/frontend/src/components/kubernetes/DeploymentView.tsx b/frontend/src/components/kubernetes/DeploymentView.tsx index cb67b56..9285f5e 100644 --- a/frontend/src/components/kubernetes/DeploymentView.tsx +++ b/frontend/src/components/kubernetes/DeploymentView.tsx @@ -104,17 +104,20 @@ export function DeploymentView(props: DeploymentViewProps) { setTabLoading('events'); kubernetesApi.getEvents(clusterCode, namespace, undefined).then((res: any) => { const allEvents = Array.isArray(res) ? res : (res?.list || res?.items || res?.data || []); + const depName = deployment?.name || ''; const depEvents = allEvents.filter((e: any) => { - const involved = e.involvedObject || e.involved_object || {}; - const objName = involved.name || ''; - return objName === deployment?.name || objName.startsWith(deployment?.name + '-'); + const obj = String(e.object || ''); + if (!depName || !obj) return false; + if (obj.endsWith(`/${depName}`)) return true; + const name = obj.split('/').pop() || ''; + return name === depName || name.startsWith(`${depName}-`); }).map((e: any) => ({ type: e.type || 'Normal', reason: e.reason || '', message: e.message || '', - source: (e.source?.component || e.sourceComponent || '-'), - firstTimestamp: e.firstTimestamp || e.first_timestamp || '', - lastTimestamp: e.lastTimestamp || e.last_timestamp || '', + source: e.object || e.source?.component || e.sourceComponent || '-', + firstTimestamp: e.first_time || e.firstTimestamp || e.first_timestamp || '', + lastTimestamp: e.last_time || e.lastTimestamp || e.last_timestamp || '', count: e.count || 1, })); setEvents(depEvents); diff --git a/frontend/src/components/kubernetes/EventsPage.tsx b/frontend/src/components/kubernetes/EventsPage.tsx index a838bbe..d11129d 100644 --- a/frontend/src/components/kubernetes/EventsPage.tsx +++ b/frontend/src/components/kubernetes/EventsPage.tsx @@ -85,6 +85,14 @@ export default function EventsPage() { // Filter events by selected reason/kind from aggregation click const displayEvents = useMemo(() => { let filtered = events; + const hours = parseInt(timeWindow, 10); + if (hours > 0) { + const since = Date.now() - hours * 3600 * 1000; + filtered = filtered.filter((e) => { + const ts = e.last_time || e.lastTime || e.last_timestamp; + return !ts || new Date(ts).getTime() >= since; + }); + } if (selectedReason) { filtered = filtered.filter(e => e.reason === selectedReason); } @@ -92,7 +100,7 @@ export default function EventsPage() { filtered = filtered.filter(e => (e.object || '').startsWith(selectedKind + '/')); } return filtered; - }, [events, selectedReason, selectedKind]); + }, [events, selectedReason, selectedKind, timeWindow]); const warningCount = useMemo(() => events.filter(e => e.type === 'Warning').length, [events]); const normalCount = useMemo(() => events.filter(e => e.type === 'Normal').length, [events]); diff --git a/frontend/src/components/kubernetes/K8sShortcutRedirect.tsx b/frontend/src/components/kubernetes/K8sShortcutRedirect.tsx new file mode 100644 index 0000000..4d4371c --- /dev/null +++ b/frontend/src/components/kubernetes/K8sShortcutRedirect.tsx @@ -0,0 +1,35 @@ +// K8sShortcutRedirect — 侧边栏快捷入口跳转到首个集群 // Logan.Li +import { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Loader2 } from 'lucide-react'; +import { kubernetesApi } from '@/api'; + +interface Props { + /** 集群下路径,如 events、namespace/default/deployments */ + target: string; +} + +export default function K8sShortcutRedirect({ target }: Props) { + const navigate = useNavigate(); + + useEffect(() => { + kubernetesApi.getClusters() + .then((res: any) => { + const list = res?.list || []; + if (list.length > 0) { + const code = list[0].code || String(list[0].id); + navigate(`/kubernetes/${code}/${target}`, { replace: true }); + } else { + navigate('/kubernetes', { replace: true }); + } + }) + .catch(() => navigate('/kubernetes', { replace: true })); + }, [navigate, target]); + + return ( +
+ + 正在跳转... +
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/kubernetes/NamespaceResourcePage.tsx b/frontend/src/components/kubernetes/NamespaceResourcePage.tsx index 106b3ab..017b16d 100644 --- a/frontend/src/components/kubernetes/NamespaceResourcePage.tsx +++ b/frontend/src/components/kubernetes/NamespaceResourcePage.tsx @@ -16,7 +16,6 @@ import { Download, FileCode, Pencil, Terminal, HardDrive, } from 'lucide-react'; import { YamlViewer } from './YamlViewer'; -import { ResourceVisualEditor } from './ResourceVisualEditor'; import { PodExecTerminal } from './PodExecTerminal'; import { PodFileBrowser } from './PodFileBrowser'; import { kubernetesApi } from '@/api'; @@ -106,10 +105,8 @@ const RESOURCE_CONFIG: Record{s.cluster_ip || '-'} {s.ports || '-'} -
- - -
+
), @@ -126,10 +123,8 @@ const RESOURCE_CONFIG: Record{hosts.length > 0 ? hosts.join(', ') : '-'} {i.tls ? 'HTTPS' : 'HTTP'} -
- - -
+
); @@ -144,12 +139,8 @@ const RESOURCE_CONFIG: Record{c.namespace} {c.key_count || '-'} -
- - -
+
), @@ -163,12 +154,8 @@ const RESOURCE_CONFIG: Record{s.namespace} {s.type || '-'} -
- - -
+
), @@ -211,10 +198,8 @@ const RESOURCE_CONFIG: Record{p.size || '-'} {p.storage_class || '-'} -
- - -
+
), @@ -353,7 +338,7 @@ export default function NamespaceResourcePage() { const [loading, setLoading] = useState(false); const [search, setSearch] = useState(''); const [yamlItem, setYamlItem] = useState<{resource: string; name: string; editMode?: boolean} | null>(null); - const [visualEditItem, setVisualEditItem] = useState<{resource: string; name: string} | null>(null); + const [terminalPod, setTerminalPod] = useState(null); const [fileBrowserPod, setFileBrowserPod] = useState(null); @@ -420,34 +405,11 @@ export default function NamespaceResourcePage() { }; const handleYaml = (name: string) => setYamlItem({ resource: type, name }); - const handleEdit = (name: string) => { - // 使用可视化编辑器的资源类型 - if (['configmaps', 'secrets', 'services', 'ingresses', 'pvcs'].includes(type)) { - setVisualEditItem({ resource: type, name }); - } else { - setYamlItem({ resource: type, name, editMode: true }); - } - }; + // 网络/配置/存储统一走 YamlViewer 编辑(清理只读字段 + CodeMirror)// Logan.Li + const handleEdit = (name: string) => setYamlItem({ resource: type, name, editMode: true }); const handleExportCSV = () => kubernetesApi.exportAsCSV(`${type}-${clusterCode}-${namespace}.csv`, filtered); const handleExportJSON = () => kubernetesApi.exportAsJSON(`${type}-${clusterCode}-${namespace}.json`, filtered); - const sidebarResources = useMemo(() => [ - { type: 'deployments' as ResourceType, label: 'Deployments', icon: Boxes }, - { type: 'statefulsets' as ResourceType, label: 'StatefulSets', icon: Boxes }, - { type: 'daemonsets' as ResourceType, label: 'DaemonSets', icon: Boxes }, - { type: 'pods' as ResourceType, label: 'Pods', icon: Container }, - { type: 'cronjobs' as ResourceType, label: 'CronJobs', icon: Clock }, - { type: 'services' as ResourceType, label: 'Services', icon: Network }, - { type: 'ingresses' as ResourceType, label: 'Ingresses', icon: Network }, - { type: 'configmaps' as ResourceType, label: 'ConfigMaps', icon: FileText }, - { type: 'secrets' as ResourceType, label: 'Secrets', icon: Shield }, - { type: 'hpas' as ResourceType, label: 'HPAs', icon: Activity }, - { type: 'pvcs' as ResourceType, label: '存储卷', icon: Boxes }, - { type: 'serviceaccounts' as ResourceType, label: 'ServiceAccounts', icon: Shield }, - { type: 'roles' as ResourceType, label: 'Roles', icon: Shield }, - { type: 'rolebindings' as ResourceType, label: 'RoleBindings', icon: Shield }, - ], []); - if (!config) return
未知资源类型: {type}
; return ( @@ -464,16 +426,7 @@ export default function NamespaceResourcePage() { autoEdit={yamlItem.editMode === true} /> )} - {visualEditItem && ( - setVisualEditItem(null)} - onSaved={loadData} - /> - )} +
集群 {clusterCode && <>{clusterCode}} @@ -505,18 +458,12 @@ export default function NamespaceResourcePage() {
-
- {sidebarResources.map((r) => ( - - {r.label} - - ))} -
-
+
+
setSearch(e.target.value)} className="pl-9 h-9 bg-slate-800/50 border-slate-700 text-slate-200" />
+
diff --git a/frontend/src/components/kubernetes/YamlViewer.tsx b/frontend/src/components/kubernetes/YamlViewer.tsx index 2d40019..48547da 100644 --- a/frontend/src/components/kubernetes/YamlViewer.tsx +++ b/frontend/src/components/kubernetes/YamlViewer.tsx @@ -183,12 +183,12 @@ export function YamlViewer({ clusterCode, namespace, resource, name, onClose, on // 弹窗模式(原有行为) return (
-
e.stopPropagation()}> +
e.stopPropagation()}> {/* Header */}
-

{resource}/{name}

-

{namespace} · {clusterCode}

+

{editing ? 'YAML 编辑' : 'YAML 查看'} — {resource}/{name}

+

{namespace} · {clusterCode}{editing ? ' · 已清理只读字段' : ''}

{/* Body */} -
+
{loading ? (
) : ( editing && setEditYaml(val)} @@ -249,7 +249,7 @@ export function YamlViewer({ clusterCode, namespace, resource, name, onClose, on closeBrackets: true, indentOnInput: true, }} - style={{ fontSize: '12px' }} + style={{ fontSize: '13px', height: '100%' }} /> )}
diff --git a/frontend/src/components/kubernetes/deployment-view/EventsTab.tsx b/frontend/src/components/kubernetes/deployment-view/EventsTab.tsx index 82a7dd9..dc269ac 100644 --- a/frontend/src/components/kubernetes/deployment-view/EventsTab.tsx +++ b/frontend/src/components/kubernetes/deployment-view/EventsTab.tsx @@ -36,7 +36,7 @@ export function EventsTab({ events, loading }: { events: EventItem[]; loading: s {e.reason} - {e.message} + {e.message} {e.source} {e.count} {formatK8sTime(e.lastTimestamp)} diff --git a/frontend/src/components/layout/Breadcrumb.tsx b/frontend/src/components/layout/Breadcrumb.tsx index e244ec6..888ddc7 100644 --- a/frontend/src/components/layout/Breadcrumb.tsx +++ b/frontend/src/components/layout/Breadcrumb.tsx @@ -27,9 +27,8 @@ const ROUTE_NAME_MAP: Record = { 'secrets': 'Secrets', 'hpas': 'HPA', 'pvcs': '存储', - 'metrics': '资源用量', + 'events': '事件', - 'capacity': '容量分析', 'topology': '架构拓扑', 'namespace': '命名空间', 'workload': '工作负载', diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx index 67f1e8c..e3c2d5f 100644 --- a/frontend/src/components/layout/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar.tsx @@ -98,9 +98,7 @@ const menuItems: MenuItem[] = [ ], }, { id: 'k8s-storage', name: '存储', icon: Database, path: '/workloads/pvcs', permission: 'k8s:workloads' }, - { id: 'k8s-metrics', name: '资源用量', icon: Activity, path: '/workloads/metrics', permission: 'k8s:workloads' }, { id: 'k8s-events', name: '事件', icon: Bell, path: '/workloads/events', permission: 'k8s:workloads' }, - { id: 'k8s-capacity', name: '容量分析', icon: BarChart3, path: '/workloads/capacity', permission: 'k8s:workloads' }, { id: 'k8s-topology', name: '架构拓扑', icon: Network, path: '/topology', permission: 'k8s:topology' }, ], }, diff --git a/run.sh b/run.sh index 17b3569..1872214 100755 --- a/run.sh +++ b/run.sh @@ -1060,6 +1060,9 @@ cmd_kubernetes() { scale) check_kubernetes_deps local replicas=${2:-1} + if [ "$replicas" -gt 1 ]; then + warning "MODE=local 时 SQLite 数据不共享,多副本会导致数据分裂,建议 replicas=1 或切换 MODE=release" + fi info "扩缩容到 $replicas 副本..." kubectl -n "$KUBERNETES_NAMESPACE" scale deployment/aiops --replicas="$replicas" ;;