1.33 日志通用查询
This commit is contained in:
parent
5804d6e2e6
commit
3469c8ccb1
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
306
frontend/src/pages/Dashboard/components/DeployTabContent.tsx
Normal file
306
frontend/src/pages/Dashboard/components/DeployTabContent.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
101
frontend/src/pages/Dashboard/components/K8sRuntimeStatus.tsx
Normal file
101
frontend/src/pages/Dashboard/components/K8sRuntimeStatus.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
153
frontend/src/pages/Dashboard/components/LogViewerDialog.tsx
Normal file
153
frontend/src/pages/Dashboard/components/LogViewerDialog.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
105
frontend/src/pages/Dashboard/components/RuntimeTabContent.tsx
Normal file
105
frontend/src/pages/Dashboard/components/RuntimeTabContent.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user