1.33 日志通用查询
This commit is contained in:
parent
5804d6e2e6
commit
3469c8ccb1
@ -1,26 +1,18 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import {
|
import {
|
||||||
GitBranch,
|
|
||||||
Rocket,
|
Rocket,
|
||||||
CheckCircle2,
|
Activity,
|
||||||
XCircle,
|
|
||||||
TrendingUp,
|
|
||||||
Clock,
|
|
||||||
History,
|
|
||||||
Loader2,
|
|
||||||
User,
|
|
||||||
FileText,
|
|
||||||
Hash,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { formatDuration, formatTime, getStatusIcon, getStatusText } from '../utils/dashboardUtils';
|
|
||||||
import { getLanguageIcon } from '../utils/languageIcons';
|
import { getLanguageIcon } from '../utils/languageIcons';
|
||||||
import type { ApplicationConfig, DeployEnvironment, DeployRecord } from '../types';
|
import type { ApplicationConfig, DeployEnvironment, DeployRecord } from '../types';
|
||||||
import { DeployFlowGraphModal } from './DeployFlowGraphModal';
|
import { DeployFlowGraphModal } from './DeployFlowGraphModal';
|
||||||
import DeploymentFormModal from './DeploymentFormModal';
|
import DeploymentFormModal from './DeploymentFormModal';
|
||||||
|
import { LogViewerDialog } from './LogViewerDialog';
|
||||||
|
import { DeployTabContent } from './DeployTabContent';
|
||||||
|
import { RuntimeTabContent } from './RuntimeTabContent';
|
||||||
|
|
||||||
interface ApplicationCardProps {
|
interface ApplicationCardProps {
|
||||||
app: ApplicationConfig;
|
app: ApplicationConfig;
|
||||||
@ -40,8 +32,8 @@ export const ApplicationCard: React.FC<ApplicationCardProps> = ({
|
|||||||
const [selectedDeployRecordId, setSelectedDeployRecordId] = useState<number | null>(null);
|
const [selectedDeployRecordId, setSelectedDeployRecordId] = useState<number | null>(null);
|
||||||
const [flowModalOpen, setFlowModalOpen] = useState(false);
|
const [flowModalOpen, setFlowModalOpen] = useState(false);
|
||||||
const [deployDialogOpen, setDeployDialogOpen] = useState(false);
|
const [deployDialogOpen, setDeployDialogOpen] = useState(false);
|
||||||
|
const [logDialogOpen, setLogDialogOpen] = useState(false);
|
||||||
|
|
||||||
// 获取语言图标配置
|
|
||||||
const languageConfig = getLanguageIcon(app.language);
|
const languageConfig = getLanguageIcon(app.language);
|
||||||
|
|
||||||
const handleDeployRecordClick = (record: DeployRecord) => {
|
const handleDeployRecordClick = (record: DeployRecord) => {
|
||||||
@ -50,9 +42,9 @@ export const ApplicationCard: React.FC<ApplicationCardProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col p-3 rounded-lg border hover:bg-accent/50 transition-colors">
|
<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 min-h-[54px]">
|
<div className="flex items-start gap-2 mb-3">
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
@ -86,284 +78,52 @@ export const ApplicationCard: React.FC<ApplicationCardProps> = ({
|
|||||||
) : (
|
) : (
|
||||||
<Skeleton className="h-3 w-20" />
|
<Skeleton className="h-3 w-20" />
|
||||||
)}
|
)}
|
||||||
{/* 应用描述 - 固定单行显示,超出省略 */}
|
</div>
|
||||||
<div className="mt-1 h-[14px]">
|
</div>
|
||||||
|
|
||||||
|
{/* 应用描述 */}
|
||||||
|
<div className="mb-3 min-h-[14px]">
|
||||||
{app.applicationDesc ? (
|
{app.applicationDesc ? (
|
||||||
<p className="text-[10px] text-muted-foreground truncate">
|
<p className="text-xs text-muted-foreground truncate">
|
||||||
{app.applicationDesc}
|
{app.applicationDesc}
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<Skeleton className="h-3 w-full" />
|
<Skeleton className="h-3 w-full" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Git/工作流/系统信息 */}
|
{/* Tab切换 */}
|
||||||
<div className="space-y-1.5 mb-3 text-xs text-muted-foreground">
|
<Tabs defaultValue="deploy" className="flex-1">
|
||||||
{/* 分支 */}
|
<TabsList className="grid w-full grid-cols-2 mb-3">
|
||||||
<div className="flex items-center gap-1.5">
|
<TabsTrigger value="deploy" className="text-xs">
|
||||||
<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="flex items-center gap-1 text-[10px]">
|
|
||||||
<Clock className="h-3 w-3 shrink-0" />
|
|
||||||
<Skeleton className="h-3 w-32" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 最近部署记录 - 固定显示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" />
|
<Rocket className="h-3 w-3 mr-1" />
|
||||||
{environment.requiresApproval ? '申请部署' : '立即部署'}
|
部署
|
||||||
</>
|
</TabsTrigger>
|
||||||
)}
|
<TabsTrigger value="runtime" className="text-xs">
|
||||||
</Button>
|
<Activity className="h-3 w-3 mr-1" />
|
||||||
</div>
|
运行时
|
||||||
</TooltipTrigger>
|
</TabsTrigger>
|
||||||
{!environment.canDeploy && (
|
</TabsList>
|
||||||
<TooltipContent>
|
|
||||||
<p>您没有部署权限</p>
|
{/* 部署Tab */}
|
||||||
</TooltipContent>
|
<TabsContent value="deploy" className="mt-0 h-[380px]">
|
||||||
)}
|
<DeployTabContent
|
||||||
</Tooltip>
|
app={app}
|
||||||
</TooltipProvider>
|
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
|
<DeploymentFormModal
|
||||||
@ -373,7 +133,6 @@ export const ApplicationCard: React.FC<ApplicationCardProps> = ({
|
|||||||
environment={environment}
|
environment={environment}
|
||||||
teamId={teamId}
|
teamId={teamId}
|
||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
// 部署成功后,触发父组件的 onDeploy 回调(用于刷新数据)
|
|
||||||
onDeploy(app, '');
|
onDeploy(app, '');
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -384,7 +143,14 @@ export const ApplicationCard: React.FC<ApplicationCardProps> = ({
|
|||||||
deployRecordId={selectedDeployRecordId}
|
deployRecordId={selectedDeployRecordId}
|
||||||
onOpenChange={setFlowModalOpen}
|
onOpenChange={setFlowModalOpen}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* 日志查看对话框 */}
|
||||||
|
<LogViewerDialog
|
||||||
|
open={logDialogOpen}
|
||||||
|
onOpenChange={setLogDialogOpen}
|
||||||
|
app={app}
|
||||||
|
environment={environment}
|
||||||
|
/>
|
||||||
</div>
|
</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 BuildType = 'JENKINS' | 'NATIVE';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行时类型
|
||||||
|
*/
|
||||||
|
export type RuntimeType = 'K8S' | 'DOCKER' | 'SERVER';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行时状态(预留)
|
||||||
|
*/
|
||||||
|
export type RuntimeStatus = 'RUNNING' | 'STOPPED' | 'DEPLOYING' | 'UNKNOWN';
|
||||||
|
|
||||||
export interface ApplicationConfig {
|
export interface ApplicationConfig {
|
||||||
teamApplicationId: number;
|
teamApplicationId: number;
|
||||||
applicationId: number;
|
applicationId: number;
|
||||||
@ -72,6 +82,16 @@ export interface ApplicationConfig {
|
|||||||
deployStatistics?: DeployStatistics;
|
deployStatistics?: DeployStatistics;
|
||||||
isDeploying?: boolean;
|
isDeploying?: boolean;
|
||||||
recentDeployRecords?: DeployRecord[];
|
recentDeployRecords?: DeployRecord[];
|
||||||
|
// 运行时配置
|
||||||
|
runtimeType?: RuntimeType;
|
||||||
|
runtimeStatus?: RuntimeStatus; // 预留
|
||||||
|
k8sSystemName?: string;
|
||||||
|
k8sNamespaceName?: string;
|
||||||
|
k8sDeploymentName?: string;
|
||||||
|
dockerServerName?: string;
|
||||||
|
dockerContainerName?: string;
|
||||||
|
serverName?: string;
|
||||||
|
logQueryCommand?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NotificationConfig {
|
export interface NotificationConfig {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user