增加代码编辑器表单组件
This commit is contained in:
parent
c23c93b753
commit
efc92ed8e2
@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
Package,
|
||||
@ -11,7 +12,11 @@ import {
|
||||
GitBranch,
|
||||
Users,
|
||||
Server,
|
||||
CheckCircle2
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
Clock,
|
||||
TrendingUp,
|
||||
History
|
||||
} from "lucide-react";
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
import { getDeployEnvironments, startDeployment } from './service';
|
||||
@ -26,6 +31,63 @@ const LoadingState = () => (
|
||||
</div>
|
||||
);
|
||||
|
||||
// 格式化持续时间
|
||||
const formatDuration = (ms?: number) => {
|
||||
if (!ms) return '-';
|
||||
const seconds = Math.floor(ms / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
if (minutes > 0) {
|
||||
return `${minutes}分${seconds % 60}秒`;
|
||||
}
|
||||
return `${seconds}秒`;
|
||||
};
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (timeStr?: string) => {
|
||||
if (!timeStr) return '-';
|
||||
try {
|
||||
const date = new Date(timeStr);
|
||||
return date.toLocaleString('zh-CN', {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
} catch {
|
||||
return timeStr;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取状态图标和颜色
|
||||
const getStatusIcon = (status?: string) => {
|
||||
switch (status) {
|
||||
case 'SUCCESS':
|
||||
return { icon: CheckCircle2, color: 'text-green-600' };
|
||||
case 'FAILED':
|
||||
return { icon: XCircle, color: 'text-red-600' };
|
||||
case 'RUNNING':
|
||||
return { icon: Loader2, color: 'text-blue-600' };
|
||||
default:
|
||||
return { icon: Clock, color: 'text-gray-400' };
|
||||
}
|
||||
};
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status?: string) => {
|
||||
switch (status) {
|
||||
case 'SUCCESS':
|
||||
return '成功';
|
||||
case 'FAILED':
|
||||
return '失败';
|
||||
case 'RUNNING':
|
||||
return '运行中';
|
||||
case 'CANCELLED':
|
||||
return '已取消';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
};
|
||||
|
||||
const Dashboard: React.FC = () => {
|
||||
const { toast } = useToast();
|
||||
const [loading, setLoading] = useState(true);
|
||||
@ -257,45 +319,214 @@ const Dashboard: React.FC = () => {
|
||||
<div className="flex items-start gap-2 mb-3">
|
||||
<Package className="h-4 w-4 text-muted-foreground shrink-0 mt-0.5" />
|
||||
<div className="flex-1 min-w-0">
|
||||
{app.applicationName ? (
|
||||
<h4 className="font-semibold text-sm mb-1 truncate">{app.applicationName}</h4>
|
||||
) : (
|
||||
<Skeleton className="h-4 w-24 mb-1" />
|
||||
)}
|
||||
{app.applicationCode ? (
|
||||
<code className="text-xs text-muted-foreground truncate block">
|
||||
{app.applicationCode}
|
||||
</code>
|
||||
) : (
|
||||
<Skeleton className="h-3 w-20" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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.branch ? (
|
||||
<code className="truncate font-mono">{app.branch}</code>
|
||||
) : (
|
||||
<Skeleton className="h-3 w-16" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 工作流 */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Rocket className="h-3 w-3 shrink-0" />
|
||||
<span className="truncate">{app.workflowDefinitionName || '未配置'}</span>
|
||||
{app.workflowDefinitionName ? (
|
||||
<span className="truncate">{app.workflowDefinitionName}</span>
|
||||
) : (
|
||||
<Skeleton className="h-3 w-20" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{app.deploySystemName && (
|
||||
{/* Jenkins */}
|
||||
{app.deploySystemName ? (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Server className="h-3 w-3 shrink-0" />
|
||||
<span className="truncate">{app.deploySystemName}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Server className="h-3 w-3 shrink-0" />
|
||||
<Skeleton className="h-3 w-24" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 部署统计信息 */}
|
||||
<div className="mb-3 pt-2 border-t border-dashed">
|
||||
<div className="grid grid-cols-3 gap-1.5 mb-2 text-xs">
|
||||
{/* 总次数 */}
|
||||
<div className="text-center">
|
||||
{app.deployStatistics ? (
|
||||
<>
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<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-1">
|
||||
<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-1">
|
||||
<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>
|
||||
|
||||
{/* 最近部署信息 */}
|
||||
{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>
|
||||
)}
|
||||
|
||||
{/* 最近部署记录 */}
|
||||
{app.recentDeployRecords && app.recentDeployRecords.length > 0 ? (
|
||||
<div className="mt-2 space-y-1">
|
||||
<div className="flex items-center gap-1 text-[10px] font-medium text-muted-foreground">
|
||||
<History className="h-3 w-3" />
|
||||
<span>最近记录</span>
|
||||
</div>
|
||||
{app.recentDeployRecords.slice(0, 2).map((record) => {
|
||||
const { icon: StatusIcon, color } = getStatusIcon(record.status);
|
||||
return (
|
||||
<div key={record.id} className="flex items-center justify-between text-[10px]">
|
||||
<div className="flex items-center gap-1 flex-1 min-w-0">
|
||||
<StatusIcon className={cn("h-3 w-3 shrink-0", color, record.status === 'RUNNING' && "animate-spin")} />
|
||||
{record.startTime ? (
|
||||
<>
|
||||
<span className="truncate">{formatTime(record.startTime)}</span>
|
||||
{record.deployRemark && (
|
||||
<span className="text-muted-foreground truncate">- {record.deployRemark}</span>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Skeleton className="h-3 w-20" />
|
||||
)}
|
||||
</div>
|
||||
{record.duration ? (
|
||||
<span className="text-muted-foreground shrink-0 ml-1">
|
||||
{formatDuration(record.duration)}
|
||||
</span>
|
||||
) : (
|
||||
<Skeleton className="h-3 w-12 shrink-0" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-2 space-y-1">
|
||||
<div className="flex items-center gap-1 text-[10px] font-medium text-muted-foreground">
|
||||
<History className="h-3 w-3" />
|
||||
<span>最近记录</span>
|
||||
</div>
|
||||
{/* 显示2条骨架记录 */}
|
||||
{[1, 2].map((i) => (
|
||||
<div key={i} className="flex items-center justify-between text-[10px]">
|
||||
<Skeleton className="h-3 w-24" />
|
||||
<Skeleton className="h-3 w-12 shrink-0" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={() => handleDeploy(app, currentEnv?.requiresApproval || false)}
|
||||
disabled={!currentEnv?.enabled || isDeploying}
|
||||
disabled={!currentEnv?.enabled || isDeploying || app.isDeploying}
|
||||
size="sm"
|
||||
className={cn(
|
||||
'w-full text-xs h-8',
|
||||
currentEnv?.requiresApproval && 'bg-amber-600 hover:bg-amber-700'
|
||||
)}
|
||||
>
|
||||
{isDeploying ? (
|
||||
{(isDeploying || app.isDeploying) ? (
|
||||
<>
|
||||
<Loader2 className="h-3 w-3 mr-1 animate-spin" />
|
||||
处理中
|
||||
部署中
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@ -1,11 +1,29 @@
|
||||
import type { BaseResponse } from '@/types/base';
|
||||
|
||||
export interface Approver {
|
||||
userId: number;
|
||||
username: string;
|
||||
realName: string;
|
||||
}
|
||||
|
||||
export interface DeployStatistics {
|
||||
totalCount: number;
|
||||
successCount: number;
|
||||
failedCount: number;
|
||||
runningCount: number;
|
||||
lastDeployTime?: string;
|
||||
lastDeployBy?: string;
|
||||
latestStatus?: 'SUCCESS' | 'FAILED' | 'RUNNING' | 'CANCELLED';
|
||||
}
|
||||
|
||||
export interface DeployRecord {
|
||||
id: number;
|
||||
status: 'SUCCESS' | 'FAILED' | 'RUNNING' | 'CANCELLED';
|
||||
deployBy: string;
|
||||
startTime: string;
|
||||
endTime?: string;
|
||||
deployRemark?: string;
|
||||
duration?: number; // 持续时间(毫秒)
|
||||
}
|
||||
|
||||
export interface ApplicationConfig {
|
||||
teamApplicationId: number;
|
||||
applicationId: number;
|
||||
@ -19,6 +37,9 @@ export interface ApplicationConfig {
|
||||
workflowDefinitionId?: number;
|
||||
workflowDefinitionName?: string;
|
||||
workflowDefinitionKey?: string;
|
||||
deployStatistics?: DeployStatistics;
|
||||
isDeploying?: boolean;
|
||||
recentDeployRecords?: DeployRecord[];
|
||||
}
|
||||
|
||||
export interface DeployEnvironment {
|
||||
|
||||
@ -42,7 +42,6 @@ const EditDialog: React.FC<EditDialogProps> = ({
|
||||
if (open) {
|
||||
if (record) {
|
||||
setFormData({
|
||||
tenantCode: record.tenantCode,
|
||||
envCode: record.envCode,
|
||||
envName: record.envName,
|
||||
envDesc: record.envDesc,
|
||||
@ -58,10 +57,6 @@ const EditDialog: React.FC<EditDialogProps> = ({
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
// 验证
|
||||
if (!formData.tenantCode?.trim()) {
|
||||
toast({ title: '提示', description: '请输入租户代码', variant: 'destructive' });
|
||||
return;
|
||||
}
|
||||
if (!formData.envCode?.trim()) {
|
||||
toast({ title: '提示', description: '请输入环境编码', variant: 'destructive' });
|
||||
return;
|
||||
@ -99,17 +94,6 @@ const EditDialog: React.FC<EditDialogProps> = ({
|
||||
</DialogHeader>
|
||||
<DialogBody>
|
||||
<div className="grid gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="tenantCode">租户代码 *</Label>
|
||||
<Input
|
||||
id="tenantCode"
|
||||
value={formData.tenantCode || ''}
|
||||
onChange={(e) => setFormData({ ...formData, tenantCode: e.target.value })}
|
||||
placeholder="请输入租户代码"
|
||||
disabled={!!record}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="envCode">环境编码 *</Label>
|
||||
<Input
|
||||
|
||||
@ -228,11 +228,28 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
|
||||
|
||||
// 设置输入映射默认值(转换 UUID 为显示名称)
|
||||
if (isConfigurableNode(nodeDefinition)) {
|
||||
const savedInputMapping = nodeData.inputMapping || {};
|
||||
const displayInputMapping = convertObjectToDisplayName(
|
||||
nodeData.inputMapping || {},
|
||||
savedInputMapping,
|
||||
allNodes
|
||||
);
|
||||
inputForm.reset(displayInputMapping);
|
||||
|
||||
// ✅ 从 schema 中提取 default 值并合并
|
||||
const schemaDefaults: Record<string, any> = {};
|
||||
if (nodeDefinition.inputMappingSchema?.properties) {
|
||||
Object.keys(nodeDefinition.inputMappingSchema.properties).forEach(key => {
|
||||
const prop = nodeDefinition.inputMappingSchema!.properties![key];
|
||||
// 如果该字段没有保存的值,使用 schema 中的 default
|
||||
if (!(key in displayInputMapping) && 'default' in prop) {
|
||||
schemaDefaults[key] = prop.default;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
inputForm.reset({
|
||||
...schemaDefaults,
|
||||
...displayInputMapping,
|
||||
});
|
||||
}
|
||||
}
|
||||
// 只在 visible 或 node.id 改变时重置表单
|
||||
|
||||
Loading…
Reference in New Issue
Block a user