1.33 日志通用查询

This commit is contained in:
dengqichen 2025-12-16 15:27:12 +08:00
parent 5804d6e2e6
commit 3469c8ccb1
8 changed files with 933 additions and 290 deletions

View File

@ -1,26 +1,18 @@
import React, { useState } from 'react';
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
GitBranch,
Rocket,
CheckCircle2,
XCircle,
TrendingUp,
Clock,
History,
Loader2,
User,
FileText,
Hash,
Activity,
} from "lucide-react";
import { formatDuration, formatTime, getStatusIcon, getStatusText } from '../utils/dashboardUtils';
import { getLanguageIcon } from '../utils/languageIcons';
import type { ApplicationConfig, DeployEnvironment, DeployRecord } from '../types';
import { DeployFlowGraphModal } from './DeployFlowGraphModal';
import DeploymentFormModal from './DeploymentFormModal';
import { LogViewerDialog } from './LogViewerDialog';
import { DeployTabContent } from './DeployTabContent';
import { RuntimeTabContent } from './RuntimeTabContent';
interface ApplicationCardProps {
app: ApplicationConfig;
@ -40,8 +32,8 @@ export const ApplicationCard: React.FC<ApplicationCardProps> = ({
const [selectedDeployRecordId, setSelectedDeployRecordId] = useState<number | null>(null);
const [flowModalOpen, setFlowModalOpen] = useState(false);
const [deployDialogOpen, setDeployDialogOpen] = useState(false);
const [logDialogOpen, setLogDialogOpen] = useState(false);
// 获取语言图标配置
const languageConfig = getLanguageIcon(app.language);
const handleDeployRecordClick = (record: DeployRecord) => {
@ -50,9 +42,9 @@ export const ApplicationCard: React.FC<ApplicationCardProps> = ({
};
return (
<div className="flex flex-col p-3 rounded-lg border hover:bg-accent/50 transition-colors">
{/* 应用基本信息 - 固定高度确保一致性 */}
<div className="flex items-start gap-2 mb-3 min-h-[54px]">
<div className="flex flex-col p-4 rounded-lg border hover:bg-accent/50 transition-colors">
{/* 顶部:应用信息 */}
<div className="flex items-start gap-2 mb-3">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
@ -86,284 +78,52 @@ export const ApplicationCard: React.FC<ApplicationCardProps> = ({
) : (
<Skeleton className="h-3 w-20" />
)}
{/* 应用描述 - 固定单行显示,超出省略 */}
<div className="mt-1 h-[14px]">
{app.applicationDesc ? (
<p className="text-[10px] text-muted-foreground truncate">
{app.applicationDesc}
</p>
) : (
<Skeleton className="h-3 w-full" />
)}
</div>
</div>
</div>
{/* Git/工作流/系统信息 */}
<div className="space-y-1.5 mb-3 text-xs text-muted-foreground">
{/* 分支 */}
<div className="flex items-center gap-1.5">
<GitBranch className="h-3 w-3 shrink-0" />
{app.sourceBranch ? (
<code className="truncate font-mono">{app.sourceBranch}</code>
) : (
<span className="text-amber-600 italic font-medium"></span>
)}
</div>
</div>
{/* 部署统计信息 */}
<div className="mb-3 pt-2 border-t border-dashed">
<div className="grid grid-cols-4 gap-1 mb-2 text-xs">
{/* 总次数 */}
<div className="text-center">
{app.deployStatistics ? (
<>
<div className="flex items-center justify-center gap-0.5">
<TrendingUp className="h-3 w-3 text-muted-foreground" />
<span className="font-semibold">{app.deployStatistics.totalCount ?? 0}</span>
</div>
<div className="text-[10px] text-muted-foreground"></div>
</>
) : (
<>
<Skeleton className="h-4 w-8 mx-auto mb-1" />
<Skeleton className="h-3 w-12 mx-auto" />
</>
)}
</div>
{/* 成功次数 */}
<div className="text-center">
{app.deployStatistics ? (
<>
<div className="flex items-center justify-center gap-0.5">
<CheckCircle2 className="h-3 w-3 text-green-600" />
<span className="font-semibold text-green-600">{app.deployStatistics.successCount ?? 0}</span>
</div>
<div className="text-[10px] text-muted-foreground"></div>
</>
) : (
<>
<Skeleton className="h-4 w-8 mx-auto mb-1" />
<Skeleton className="h-3 w-12 mx-auto" />
</>
)}
</div>
{/* 失败次数 */}
<div className="text-center">
{app.deployStatistics ? (
<>
<div className="flex items-center justify-center gap-0.5">
<XCircle className="h-3 w-3 text-red-600" />
<span className="font-semibold text-red-600">{app.deployStatistics.failedCount ?? 0}</span>
</div>
<div className="text-[10px] text-muted-foreground"></div>
</>
) : (
<>
<Skeleton className="h-4 w-8 mx-auto mb-1" />
<Skeleton className="h-3 w-12 mx-auto" />
</>
)}
</div>
{/* 运行中次数 */}
<div className="text-center">
{app.deployStatistics ? (
<>
<div className="flex items-center justify-center gap-0.5">
<Loader2 className="h-3 w-3 text-blue-600" />
<span className="font-semibold text-blue-600">{app.deployStatistics.runningCount ?? 0}</span>
</div>
<div className="text-[10px] text-muted-foreground"></div>
</>
) : (
<>
<Skeleton className="h-4 w-8 mx-auto mb-1" />
<Skeleton className="h-3 w-12 mx-auto" />
</>
)}
</div>
</div>
{/* 最近部署信息 */}
{app.deployStatistics ? (
<div className="flex items-center gap-1 text-[10px] text-muted-foreground flex-wrap">
<Clock className="h-3 w-3 shrink-0" />
{app.deployStatistics.lastDeployTime ? (
<>
<span>: {formatTime(app.deployStatistics.lastDeployTime)}</span>
{app.deployStatistics.lastDeployBy ? (
<span>by {app.deployStatistics.lastDeployBy}</span>
) : (
<Skeleton className="h-3 w-16" />
)}
{app.deployStatistics.latestStatus ? (() => {
const { icon: StatusIcon, color } = getStatusIcon(app.deployStatistics.latestStatus);
return (
<span className={cn("flex items-center gap-0.5", color)}>
<StatusIcon className={cn("h-3 w-3", app.deployStatistics.latestStatus === 'RUNNING' && "animate-spin")} />
{getStatusText(app.deployStatistics.latestStatus)}
</span>
);
})() : (
<Skeleton className="h-3 w-12" />
)}
</>
) : (
<Skeleton className="h-3 w-32" />
)}
</div>
{/* 应用描述 */}
<div className="mb-3 min-h-[14px]">
{app.applicationDesc ? (
<p className="text-xs text-muted-foreground truncate">
{app.applicationDesc}
</p>
) : (
<div className="flex items-center gap-1 text-[10px]">
<Clock className="h-3 w-3 shrink-0" />
<Skeleton className="h-3 w-32" />
</div>
<Skeleton className="h-3 w-full" />
)}
{/* 最近部署记录 - 固定显示2条槽位 */}
<div className="mt-2 space-y-2">
<div className="flex items-center gap-1 text-[10px] font-medium text-muted-foreground mb-1">
<History className="h-3 w-3" />
<span></span>
</div>
{/* 始终显示2条记录槽位确保卡片高度一致 */}
{Array.from({ length: 2 }).map((_, index) => {
const record = app.recentDeployRecords?.[index];
if (record) {
// 显示实际记录 - 固定高度确保一致性
const { icon: StatusIcon, color } = getStatusIcon(record.status);
return (
<div key={record.id} className="p-1.5 rounded-md bg-muted/30 border border-muted/50 h-[68px] flex flex-col justify-between overflow-hidden">
{/* 第一行:状态、编号、部署人 */}
<div className="flex items-center gap-2 flex-wrap min-h-[18px]">
<div className="flex items-center gap-1">
<StatusIcon className={cn("h-3.5 w-3.5 shrink-0", color, record.status === 'RUNNING' && "animate-spin")} />
<span className={cn("text-[10px] font-semibold", color)}>{getStatusText(record.status)}</span>
</div>
<button
onClick={() => handleDeployRecordClick(record)}
className="flex items-center gap-1 px-1.5 py-0.5 rounded bg-background/50 hover:bg-background/80 transition-colors cursor-pointer"
title="点击查看工作流详情"
>
<Hash className="h-2.5 w-2.5 text-muted-foreground" />
<span className="text-[10px] text-muted-foreground font-mono hover:text-primary transition-colors">
{record.id}
</span>
</button>
{record.deployBy && (
<div className="flex items-center gap-1">
<User className="h-2.5 w-2.5 text-muted-foreground" />
<span className="text-[10px] text-muted-foreground">{record.deployBy}</span>
</div>
)}
</div>
{/* 第二行:时间信息(一行显示) */}
<div className="flex items-center gap-1.5 text-[10px] text-muted-foreground flex-nowrap overflow-hidden min-h-[16px]">
{(record.startTime || record.endTime || record.duration) ? (
<>
<Clock className="h-2.5 w-2.5 shrink-0" />
<div className="flex items-center gap-1 flex-nowrap min-w-0">
{record.startTime && (
<span className="whitespace-nowrap">{formatTime(record.startTime)}</span>
)}
{record.endTime && (
<>
<span className="px-0.5 shrink-0"></span>
<span className="whitespace-nowrap">{formatTime(record.endTime)}</span>
</>
)}
{record.duration && (
<>
<span className="px-0.5 text-muted-foreground/50 shrink-0"></span>
<span className="font-medium whitespace-nowrap">{formatDuration(record.duration)}</span>
</>
)}
</div>
</>
) : (
<span className="text-muted-foreground/50">-</span>
)}
</div>
{/* 第三行:备注(固定高度,超出截断) */}
<div className="flex items-center gap-1 text-[10px] text-muted-foreground pt-0.5 border-t border-muted/30 min-h-[18px]">
{record.deployRemark ? (
<>
<FileText className="h-2.5 w-2.5 shrink-0" />
<span className="truncate">{record.deployRemark}</span>
</>
) : (
<span className="text-muted-foreground/50">-</span>
)}
</div>
</div>
);
} else {
// 显示骨架屏占位 - 固定高度与实际记录一致
return (
<div key={`skeleton-${index}`} className="p-1.5 rounded-md bg-muted/30 border border-muted/50 h-[68px] flex flex-col justify-between">
{/* 第一行:状态、编号、部署人 */}
<div className="flex items-center gap-2 min-h-[18px]">
<Skeleton className="h-3.5 w-16" />
<Skeleton className="h-4 w-12" />
<Skeleton className="h-3.5 w-12 ml-auto" />
</div>
{/* 第二行:时间信息 */}
<div className="min-h-[16px]">
<Skeleton className="h-3 w-32" />
</div>
{/* 第三行:备注 */}
<div className="min-h-[18px] pt-0.5 border-t border-muted/30">
<Skeleton className="h-3 w-24" />
</div>
</div>
);
}
})}
</div>
</div>
{/* 部署按钮 */}
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="w-full">
<Button
onClick={() => setDeployDialogOpen(true)}
disabled={!environment.enabled || !environment.canDeploy || isDeploying || app.isDeploying}
size="sm"
className={cn(
'w-full text-xs h-8',
environment.requiresApproval && 'bg-amber-600 hover:bg-amber-700'
)}
>
{(isDeploying || app.isDeploying) ? (
<>
<Loader2 className="h-3 w-3 mr-1 animate-spin" />
</>
) : (
<>
<Rocket className="h-3 w-3 mr-1" />
{environment.requiresApproval ? '申请部署' : '立即部署'}
</>
)}
</Button>
</div>
</TooltipTrigger>
{!environment.canDeploy && (
<TooltipContent>
<p></p>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
{/* Tab切换 */}
<Tabs defaultValue="deploy" className="flex-1">
<TabsList className="grid w-full grid-cols-2 mb-3">
<TabsTrigger value="deploy" className="text-xs">
<Rocket className="h-3 w-3 mr-1" />
</TabsTrigger>
<TabsTrigger value="runtime" className="text-xs">
<Activity className="h-3 w-3 mr-1" />
</TabsTrigger>
</TabsList>
{/* 部署Tab */}
<TabsContent value="deploy" className="mt-0 h-[380px]">
<DeployTabContent
app={app}
environment={environment}
isDeploying={isDeploying}
onDeployClick={() => setDeployDialogOpen(true)}
onDeployRecordClick={handleDeployRecordClick}
/>
</TabsContent>
{/* 运行时Tab */}
<TabsContent value="runtime" className="mt-0 h-[380px]">
<RuntimeTabContent
app={app}
onLogClick={() => setLogDialogOpen(true)}
/>
</TabsContent>
</Tabs>
{/* 部署表单对话框 */}
<DeploymentFormModal
@ -373,7 +133,6 @@ export const ApplicationCard: React.FC<ApplicationCardProps> = ({
environment={environment}
teamId={teamId}
onSuccess={() => {
// 部署成功后,触发父组件的 onDeploy 回调(用于刷新数据)
onDeploy(app, '');
}}
/>
@ -384,7 +143,14 @@ export const ApplicationCard: React.FC<ApplicationCardProps> = ({
deployRecordId={selectedDeployRecordId}
onOpenChange={setFlowModalOpen}
/>
{/* 日志查看对话框 */}
<LogViewerDialog
open={logDialogOpen}
onOpenChange={setLogDialogOpen}
app={app}
environment={environment}
/>
</div>
);
};

View File

@ -0,0 +1,306 @@
import React from 'react';
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
import {
GitBranch,
Rocket,
CheckCircle2,
XCircle,
TrendingUp,
Clock,
History,
Loader2,
User,
FileText,
Hash,
Hammer,
} from "lucide-react";
import { formatDuration, formatTime, getStatusIcon, getStatusText } from '../utils/dashboardUtils';
import type { ApplicationConfig, DeployEnvironment, DeployRecord } from '../types';
interface DeployTabContentProps {
app: ApplicationConfig;
environment: DeployEnvironment;
isDeploying: boolean;
onDeployClick: () => void;
onDeployRecordClick: (record: DeployRecord) => void;
}
export const DeployTabContent: React.FC<DeployTabContentProps> = ({
app,
environment,
isDeploying,
onDeployClick,
onDeployRecordClick,
}) => {
const getBuildBadge = () => {
if (!app.buildType) return null;
switch (app.buildType) {
case 'JENKINS':
return (
<Badge variant="outline" className="bg-blue-50 text-blue-700 border-blue-200">
<Hammer className="h-3 w-3 mr-1" />
Jenkins
</Badge>
);
case 'NATIVE':
return (
<Badge variant="outline" className="bg-green-50 text-green-700 border-green-200">
<Hammer className="h-3 w-3 mr-1" />
</Badge>
);
default:
return null;
}
};
return (
<div className="flex flex-col h-full">
{/* 内容区 */}
<div className="flex-1 space-y-3 overflow-y-auto">
{/* 构建和分支信息 */}
<div className="space-y-2">
{/* 构建类型 */}
<div className="min-h-[24px] flex items-center">
{getBuildBadge()}
</div>
{/* 分支信息 */}
<div className="min-h-[20px] flex items-center">
{app.sourceBranch ? (
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<GitBranch className="h-3 w-3" />
<code className="font-mono">{app.sourceBranch}</code>
</div>
) : (
<span className="text-xs text-muted-foreground/50">-</span>
)}
</div>
</div>
{/* 部署统计 */}
<div className="pt-3 border-t">
<div className="grid grid-cols-4 gap-2 mb-2 text-xs">
<div className="text-center">
{app.deployStatistics ? (
<>
<div className="flex items-center justify-center gap-0.5">
<TrendingUp className="h-3 w-3 text-muted-foreground" />
<span className="font-semibold">{app.deployStatistics.totalCount ?? 0}</span>
</div>
<div className="text-[10px] text-muted-foreground"></div>
</>
) : (
<>
<Skeleton className="h-4 w-8 mx-auto mb-1" />
<Skeleton className="h-3 w-12 mx-auto" />
</>
)}
</div>
<div className="text-center">
{app.deployStatistics ? (
<>
<div className="flex items-center justify-center gap-0.5">
<CheckCircle2 className="h-3 w-3 text-green-600" />
<span className="font-semibold text-green-600">{app.deployStatistics.successCount ?? 0}</span>
</div>
<div className="text-[10px] text-muted-foreground"></div>
</>
) : (
<>
<Skeleton className="h-4 w-8 mx-auto mb-1" />
<Skeleton className="h-3 w-12 mx-auto" />
</>
)}
</div>
<div className="text-center">
{app.deployStatistics ? (
<>
<div className="flex items-center justify-center gap-0.5">
<XCircle className="h-3 w-3 text-red-600" />
<span className="font-semibold text-red-600">{app.deployStatistics.failedCount ?? 0}</span>
</div>
<div className="text-[10px] text-muted-foreground"></div>
</>
) : (
<>
<Skeleton className="h-4 w-8 mx-auto mb-1" />
<Skeleton className="h-3 w-12 mx-auto" />
</>
)}
</div>
<div className="text-center">
{app.deployStatistics ? (
<>
<div className="flex items-center justify-center gap-0.5">
<Loader2 className="h-3 w-3 text-blue-600" />
<span className="font-semibold text-blue-600">{app.deployStatistics.runningCount ?? 0}</span>
</div>
<div className="text-[10px] text-muted-foreground"></div>
</>
) : (
<>
<Skeleton className="h-4 w-8 mx-auto mb-1" />
<Skeleton className="h-3 w-12 mx-auto" />
</>
)}
</div>
</div>
{app.deployStatistics ? (
<div className="flex items-center gap-1 text-[10px] text-muted-foreground flex-wrap">
<Clock className="h-3 w-3 shrink-0" />
{app.deployStatistics.lastDeployTime ? (
<>
<span>: {formatTime(app.deployStatistics.lastDeployTime)}</span>
{app.deployStatistics.lastDeployBy && (
<span>by {app.deployStatistics.lastDeployBy}</span>
)}
{app.deployStatistics.latestStatus && (() => {
const { icon: StatusIcon, color } = getStatusIcon(app.deployStatistics.latestStatus);
return (
<span className={cn("flex items-center gap-0.5", color)}>
<StatusIcon className={cn("h-3 w-3", app.deployStatistics.latestStatus === 'RUNNING' && "animate-spin")} />
{getStatusText(app.deployStatistics.latestStatus)}
</span>
);
})()}
</>
) : (
<Skeleton className="h-3 w-32" />
)}
</div>
) : (
<div className="flex items-center gap-1 text-[10px]">
<Clock className="h-3 w-3 shrink-0" />
<Skeleton className="h-3 w-32" />
</div>
)}
{/* 最近部署记录 */}
<div className="mt-3 space-y-2">
<div className="flex items-center gap-1 text-[10px] font-medium text-muted-foreground">
<History className="h-3 w-3" />
<span></span>
</div>
{Array.from({ length: 2 }).map((_, index) => {
const record = app.recentDeployRecords?.[index];
if (record) {
const { icon: StatusIcon, color } = getStatusIcon(record.status);
return (
<div key={record.id} className="p-2 rounded-md bg-muted/30 border border-muted/50 min-h-[68px] flex flex-col justify-between">
<div className="flex items-center gap-2 flex-wrap min-h-[18px]">
<div className="flex items-center gap-1">
<StatusIcon className={cn("h-3.5 w-3.5 shrink-0", color, record.status === 'RUNNING' && "animate-spin")} />
<span className={cn("text-[10px] font-semibold", color)}>{getStatusText(record.status)}</span>
</div>
<button
onClick={() => onDeployRecordClick(record)}
className="flex items-center gap-1 px-1.5 py-0.5 rounded bg-background/50 hover:bg-background/80 transition-colors"
>
<Hash className="h-2.5 w-2.5 text-muted-foreground" />
<span className="text-[10px] text-muted-foreground font-mono hover:text-primary transition-colors">
{record.id}
</span>
</button>
{record.deployBy && (
<div className="flex items-center gap-1">
<User className="h-2.5 w-2.5 text-muted-foreground" />
<span className="text-[10px] text-muted-foreground">{record.deployBy}</span>
</div>
)}
</div>
<div className="flex items-center gap-1.5 text-[10px] text-muted-foreground flex-nowrap overflow-hidden min-h-[16px]">
{(record.startTime || record.endTime || record.duration) ? (
<>
<Clock className="h-2.5 w-2.5 shrink-0" />
<div className="flex items-center gap-1 flex-nowrap min-w-0">
{record.startTime && (
<span className="whitespace-nowrap">{formatTime(record.startTime)}</span>
)}
{record.endTime && (
<>
<span className="px-0.5 shrink-0"></span>
<span className="whitespace-nowrap">{formatTime(record.endTime)}</span>
</>
)}
{record.duration && (
<>
<span className="px-0.5 text-muted-foreground/50 shrink-0"></span>
<span className="font-medium whitespace-nowrap">{formatDuration(record.duration)}</span>
</>
)}
</div>
</>
) : (
<span className="text-muted-foreground/50">-</span>
)}
</div>
<div className="flex items-center gap-1 text-[10px] text-muted-foreground pt-1 border-t border-muted/30 min-h-[18px]">
{record.deployRemark ? (
<>
<FileText className="h-2.5 w-2.5 shrink-0" />
<span className="truncate">{record.deployRemark}</span>
</>
) : (
<span className="text-muted-foreground/50">-</span>
)}
</div>
</div>
);
} else {
return (
<div key={`skeleton-${index}`} className="p-2 rounded-md bg-muted/30 border border-muted/50 min-h-[68px] flex flex-col justify-between">
<div className="flex items-center gap-2 min-h-[18px]">
<Skeleton className="h-3.5 w-16" />
<Skeleton className="h-4 w-12" />
<Skeleton className="h-3.5 w-12 ml-auto" />
</div>
<div className="min-h-[16px]">
<Skeleton className="h-3 w-32" />
</div>
<div className="min-h-[18px] pt-1 border-t border-muted/30">
<Skeleton className="h-3 w-24" />
</div>
</div>
);
}
})}
</div>
</div>
</div>
{/* 操作区 - 固定在底部 */}
<div className="pt-3 border-t mt-3">
<Button
onClick={onDeployClick}
disabled={!environment.enabled || !environment.canDeploy || isDeploying || app.isDeploying}
size="sm"
className="w-full"
>
{(isDeploying || app.isDeploying) ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
</>
) : (
<>
<Rocket className="h-4 w-4 mr-2" />
{environment.requiresApproval ? '申请部署' : '立即部署'}
</>
)}
</Button>
</div>
</div>
);
};

View File

@ -0,0 +1,98 @@
import React from 'react';
import { Badge } from "@/components/ui/badge";
import { Container } from "lucide-react";
interface DockerRuntimeStatusProps {
dockerServerName?: string;
dockerContainerName?: string;
}
export const DockerRuntimeStatus: React.FC<DockerRuntimeStatusProps> = ({
dockerServerName,
dockerContainerName,
}) => {
// MOCK数据 - 后续替换为真实API
const mockStatus = {
status: 'running',
uptime: '2h 15m',
cpu: { used: 0.5, total: 2, percentage: 25 },
memory: { used: 512, total: 1024, percentage: 50 },
};
return (
<div className="space-y-3">
{/* 运行时类型 */}
<Badge variant="outline" className="bg-orange-50 text-orange-700 border-orange-200">
<Container className="h-3 w-3 mr-1" />
Docker
</Badge>
{/* 运行状态 */}
<div className="p-3 rounded-lg border bg-muted/30">
<div className="flex items-center gap-2 mb-3">
<div className="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
<span className="text-sm font-medium"></span>
</div>
<div className="space-y-2 text-xs">
<div className="flex items-center justify-between">
<span className="text-muted-foreground"></span>
<span className="font-medium">Running</span>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground"></span>
<span className="font-medium">{mockStatus.uptime}</span>
</div>
<div className="space-y-1">
<div className="flex items-center justify-between">
<span className="text-muted-foreground">CPU</span>
<span className="font-medium">{mockStatus.cpu.percentage}%</span>
</div>
<div className="w-full bg-muted rounded-full h-1.5">
<div
className="bg-blue-500 h-1.5 rounded-full transition-all"
style={{ width: `${mockStatus.cpu.percentage}%` }}
/>
</div>
<div className="text-[10px] text-muted-foreground text-right">
{mockStatus.cpu.used}/{mockStatus.cpu.total} cores
</div>
</div>
<div className="space-y-1">
<div className="flex items-center justify-between">
<span className="text-muted-foreground"></span>
<span className="font-medium">{mockStatus.memory.percentage}%</span>
</div>
<div className="w-full bg-muted rounded-full h-1.5">
<div
className="bg-green-500 h-1.5 rounded-full transition-all"
style={{ width: `${mockStatus.memory.percentage}%` }}
/>
</div>
<div className="text-[10px] text-muted-foreground text-right">
{mockStatus.memory.used}MB/{mockStatus.memory.total}MB
</div>
</div>
</div>
</div>
{/* 配置详情 */}
<div className="p-3 rounded-lg border bg-muted/30">
<div className="text-xs font-medium text-muted-foreground mb-2"></div>
<div className="space-y-2 text-xs">
<div className="flex items-center justify-between">
<span className="text-muted-foreground"></span>
<span className="font-medium">{dockerServerName || '-'}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground"></span>
<span className="font-medium">{dockerContainerName || '-'}</span>
</div>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,101 @@
import React from 'react';
import { Badge } from "@/components/ui/badge";
import { Box } from "lucide-react";
interface K8sRuntimeStatusProps {
k8sSystemName?: string;
k8sNamespaceName?: string;
k8sDeploymentName?: string;
}
export const K8sRuntimeStatus: React.FC<K8sRuntimeStatusProps> = ({
k8sSystemName,
k8sNamespaceName,
k8sDeploymentName,
}) => {
// MOCK数据 - 后续替换为真实API
const mockStatus = {
status: 'running',
podCount: { ready: 3, total: 3 },
cpu: { used: 0.9, total: 2, percentage: 45 },
memory: { used: 1.2, total: 2, percentage: 60 },
};
return (
<div className="space-y-3">
{/* 运行时类型 */}
<Badge variant="outline" className="bg-purple-50 text-purple-700 border-purple-200">
<Box className="h-3 w-3 mr-1" />
Kubernetes
</Badge>
{/* 运行状态 */}
<div className="p-3 rounded-lg border bg-muted/30">
<div className="flex items-center gap-2 mb-3">
<div className="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
<span className="text-sm font-medium"></span>
</div>
<div className="space-y-2 text-xs">
<div className="flex items-center justify-between">
<span className="text-muted-foreground">Pod</span>
<span className="font-medium">
{mockStatus.podCount.ready}/{mockStatus.podCount.total} Running
</span>
</div>
<div className="space-y-1">
<div className="flex items-center justify-between">
<span className="text-muted-foreground">CPU</span>
<span className="font-medium">{mockStatus.cpu.percentage}%</span>
</div>
<div className="w-full bg-muted rounded-full h-1.5">
<div
className="bg-blue-500 h-1.5 rounded-full transition-all"
style={{ width: `${mockStatus.cpu.percentage}%` }}
/>
</div>
<div className="text-[10px] text-muted-foreground text-right">
{mockStatus.cpu.used}/{mockStatus.cpu.total} cores
</div>
</div>
<div className="space-y-1">
<div className="flex items-center justify-between">
<span className="text-muted-foreground"></span>
<span className="font-medium">{mockStatus.memory.percentage}%</span>
</div>
<div className="w-full bg-muted rounded-full h-1.5">
<div
className="bg-green-500 h-1.5 rounded-full transition-all"
style={{ width: `${mockStatus.memory.percentage}%` }}
/>
</div>
<div className="text-[10px] text-muted-foreground text-right">
{mockStatus.memory.used}GB/{mockStatus.memory.total}GB
</div>
</div>
</div>
</div>
{/* 配置详情 */}
<div className="p-3 rounded-lg border bg-muted/30">
<div className="text-xs font-medium text-muted-foreground mb-2"></div>
<div className="space-y-2 text-xs">
<div className="flex items-center justify-between">
<span className="text-muted-foreground"></span>
<span className="font-medium">{k8sSystemName || '-'}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground"></span>
<span className="font-medium">{k8sNamespaceName || '-'}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground"></span>
<span className="font-medium">{k8sDeploymentName || '-'}</span>
</div>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,153 @@
import React, { useState } from 'react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Badge } from "@/components/ui/badge";
import { Box, Container, Server } from "lucide-react";
import LogViewer from '@/components/LogViewer';
import type { ApplicationConfig, DeployEnvironment } from '../types';
interface LogViewerDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
app: ApplicationConfig;
environment: DeployEnvironment;
}
export const LogViewerDialog: React.FC<LogViewerDialogProps> = ({
open,
onOpenChange,
app,
environment,
}) => {
const [loading, setLoading] = useState(false);
const [logContent, setLogContent] = useState<string>('');
const getRuntimeIcon = () => {
switch (app.runtimeType) {
case 'K8S':
return { icon: Box, color: 'text-purple-600', bg: 'bg-purple-100', label: 'Kubernetes' };
case 'DOCKER':
return { icon: Container, color: 'text-orange-600', bg: 'bg-orange-100', label: 'Docker' };
case 'SERVER':
return { icon: Server, color: 'text-gray-600', bg: 'bg-gray-100', label: '服务器' };
default:
return { icon: Server, color: 'text-gray-600', bg: 'bg-gray-100', label: '未知' };
}
};
const runtimeConfig = getRuntimeIcon();
const RuntimeIcon = runtimeConfig.icon;
const getRuntimeInfo = () => {
switch (app.runtimeType) {
case 'K8S':
return (
<div className="space-y-1 text-sm">
<div className="flex items-center gap-2">
<span className="text-muted-foreground">:</span>
<span className="font-medium">{app.k8sSystemName || '-'}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-muted-foreground">:</span>
<span className="font-medium">{app.k8sNamespaceName || '-'}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-muted-foreground">:</span>
<span className="font-medium">{app.k8sDeploymentName || '-'}</span>
</div>
</div>
);
case 'DOCKER':
return (
<div className="space-y-1 text-sm">
<div className="flex items-center gap-2">
<span className="text-muted-foreground">:</span>
<span className="font-medium">{app.dockerServerName || '-'}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-muted-foreground">:</span>
<span className="font-medium">{app.dockerContainerName || '-'}</span>
</div>
</div>
);
case 'SERVER':
return (
<div className="space-y-1 text-sm">
<div className="flex items-center gap-2">
<span className="text-muted-foreground">:</span>
<span className="font-medium">{app.serverName || '-'}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-muted-foreground">:</span>
<code className="font-mono text-xs bg-muted px-2 py-1 rounded">
{app.logQueryCommand || '-'}
</code>
</div>
</div>
);
default:
return <div className="text-sm text-muted-foreground"></div>;
}
};
const handleRefreshLogs = async () => {
setLoading(true);
// TODO: 调用实际的日志API
setTimeout(() => {
setLogContent(
'[2024-12-16 10:30:45] INFO Application started successfully\n' +
'[2024-12-16 10:30:46] INFO Server listening on port 8080\n' +
'[2024-12-16 10:31:00] DEBUG Database connection established\n' +
'[2024-12-16 10:31:15] INFO Processing request: GET /api/users\n' +
'[2024-12-16 10:31:16] WARN Slow query detected: 1.2s\n'
);
setLoading(false);
}, 1000);
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl max-h-[80vh] flex flex-col">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<RuntimeIcon className={runtimeConfig.color} />
{app.applicationName} -
</DialogTitle>
<DialogDescription>
: {environment.environmentName}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 flex-1 flex flex-col min-h-0">
<div className="p-4 rounded-lg border bg-muted/30">
<div className="flex items-center gap-2 mb-3">
<Badge variant="outline" className={runtimeConfig.bg}>
<RuntimeIcon className={`h-3 w-3 mr-1 ${runtimeConfig.color}`} />
{runtimeConfig.label}
</Badge>
</div>
{getRuntimeInfo()}
</div>
<div className="flex-1 min-h-0">
<LogViewer
content={logContent}
loading={loading}
onRefresh={handleRefreshLogs}
onDownload={() => {}}
height="100%"
theme="vs-dark"
showLineNumbers={true}
autoScroll={true}
/>
</div>
</div>
</DialogContent>
</Dialog>
);
};

View File

@ -0,0 +1,105 @@
import React from 'react';
import { Button } from "@/components/ui/button";
import { ScrollText, AlertCircle } from "lucide-react";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import type { ApplicationConfig } from '../types';
import { K8sRuntimeStatus } from './K8sRuntimeStatus';
import { DockerRuntimeStatus } from './DockerRuntimeStatus';
import { ServerRuntimeStatus } from './ServerRuntimeStatus';
interface RuntimeTabContentProps {
app: ApplicationConfig;
onLogClick: () => void;
}
export const RuntimeTabContent: React.FC<RuntimeTabContentProps> = ({
app,
onLogClick,
}) => {
const hasRuntimeConfig = !!app.runtimeType;
const renderRuntimeStatus = () => {
if (!hasRuntimeConfig) {
// 未配置运行时 - 显示占位内容
return (
<div className="p-8 text-center text-sm text-muted-foreground border rounded-lg bg-muted/30">
</div>
);
}
switch (app.runtimeType) {
case 'K8S':
return (
<K8sRuntimeStatus
k8sSystemName={app.k8sSystemName}
k8sNamespaceName={app.k8sNamespaceName}
k8sDeploymentName={app.k8sDeploymentName}
/>
);
case 'DOCKER':
return (
<DockerRuntimeStatus
dockerServerName={app.dockerServerName}
dockerContainerName={app.dockerContainerName}
/>
);
case 'SERVER':
return (
<ServerRuntimeStatus
serverName={app.serverName}
logQueryCommand={app.logQueryCommand}
/>
);
default:
return (
<div className="p-8 text-center text-sm text-muted-foreground border rounded-lg bg-muted/30">
</div>
);
}
};
return (
<div className="flex flex-col h-full">
{/* 内容区 */}
<div className="flex-1 space-y-3 overflow-y-auto">
{renderRuntimeStatus()}
</div>
{/* 操作区 - 固定在底部 */}
<div className="pt-3 border-t mt-3">
{hasRuntimeConfig ? (
<Button
onClick={onLogClick}
size="sm"
className="w-full"
>
<ScrollText className="h-4 w-4 mr-2" />
</Button>
) : (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="w-full">
<Button
disabled
size="sm"
className="w-full"
>
<AlertCircle className="h-4 w-4 mr-2" />
</Button>
</div>
</TooltipTrigger>
<TooltipContent>
<p></p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
</div>
);
};

View File

@ -0,0 +1,94 @@
import React from 'react';
import { Badge } from "@/components/ui/badge";
import { Server } from "lucide-react";
interface ServerRuntimeStatusProps {
serverName?: string;
logQueryCommand?: string;
}
export const ServerRuntimeStatus: React.FC<ServerRuntimeStatusProps> = ({
serverName,
logQueryCommand,
}) => {
// MOCK数据 - 后续替换为真实API
const mockStatus = {
status: 'running',
pid: 12345,
uptime: '2h 15m',
cpu: { percentage: 15 },
memory: { used: 256 },
};
return (
<div className="space-y-3">
{/* 运行时类型 */}
<Badge variant="outline" className="bg-gray-50 text-gray-700 border-gray-200">
<Server className="h-3 w-3 mr-1" />
</Badge>
{/* 运行状态 */}
<div className="p-3 rounded-lg border bg-muted/30">
<div className="flex items-center gap-2 mb-3">
<div className="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
<span className="text-sm font-medium"></span>
</div>
<div className="space-y-2 text-xs">
<div className="flex items-center justify-between">
<span className="text-muted-foreground"></span>
<span className="font-medium">Running</span>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground">PID</span>
<span className="font-medium font-mono">{mockStatus.pid}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground"></span>
<span className="font-medium">{mockStatus.uptime}</span>
</div>
<div className="space-y-1">
<div className="flex items-center justify-between">
<span className="text-muted-foreground">CPU</span>
<span className="font-medium">{mockStatus.cpu.percentage}%</span>
</div>
<div className="w-full bg-muted rounded-full h-1.5">
<div
className="bg-blue-500 h-1.5 rounded-full transition-all"
style={{ width: `${mockStatus.cpu.percentage}%` }}
/>
</div>
</div>
<div className="space-y-1">
<div className="flex items-center justify-between">
<span className="text-muted-foreground"></span>
<span className="font-medium">{mockStatus.memory.used}MB</span>
</div>
</div>
</div>
</div>
{/* 配置详情 */}
<div className="p-3 rounded-lg border bg-muted/30">
<div className="text-xs font-medium text-muted-foreground mb-2"></div>
<div className="space-y-2 text-xs">
<div className="flex items-center justify-between">
<span className="text-muted-foreground"></span>
<span className="font-medium">{serverName || '-'}</span>
</div>
<div className="flex flex-col gap-1">
<span className="text-muted-foreground"></span>
<code className="font-mono text-[10px] bg-muted px-2 py-1 rounded break-all">
{logQueryCommand || '-'}
</code>
</div>
</div>
</div>
</div>
);
};

View File

@ -42,6 +42,16 @@ export interface DeployRecord {
export type BuildType = 'JENKINS' | 'NATIVE';
/**
*
*/
export type RuntimeType = 'K8S' | 'DOCKER' | 'SERVER';
/**
*
*/
export type RuntimeStatus = 'RUNNING' | 'STOPPED' | 'DEPLOYING' | 'UNKNOWN';
export interface ApplicationConfig {
teamApplicationId: number;
applicationId: number;
@ -72,6 +82,16 @@ export interface ApplicationConfig {
deployStatistics?: DeployStatistics;
isDeploying?: boolean;
recentDeployRecords?: DeployRecord[];
// 运行时配置
runtimeType?: RuntimeType;
runtimeStatus?: RuntimeStatus; // 预留
k8sSystemName?: string;
k8sNamespaceName?: string;
k8sDeploymentName?: string;
dockerServerName?: string;
dockerContainerName?: string;
serverName?: string;
logQueryCommand?: string;
}
export interface NotificationConfig {