增加代码编辑器表单组件
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 { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
import {
|
||||||
Package,
|
Package,
|
||||||
@ -11,7 +12,11 @@ import {
|
|||||||
GitBranch,
|
GitBranch,
|
||||||
Users,
|
Users,
|
||||||
Server,
|
Server,
|
||||||
CheckCircle2
|
CheckCircle2,
|
||||||
|
XCircle,
|
||||||
|
Clock,
|
||||||
|
TrendingUp,
|
||||||
|
History
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useToast } from '@/components/ui/use-toast';
|
import { useToast } from '@/components/ui/use-toast';
|
||||||
import { getDeployEnvironments, startDeployment } from './service';
|
import { getDeployEnvironments, startDeployment } from './service';
|
||||||
@ -26,6 +31,63 @@ const LoadingState = () => (
|
|||||||
</div>
|
</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 Dashboard: React.FC = () => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@ -257,45 +319,214 @@ const Dashboard: React.FC = () => {
|
|||||||
<div className="flex items-start gap-2 mb-3">
|
<div className="flex items-start gap-2 mb-3">
|
||||||
<Package className="h-4 w-4 text-muted-foreground shrink-0 mt-0.5" />
|
<Package className="h-4 w-4 text-muted-foreground shrink-0 mt-0.5" />
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<h4 className="font-semibold text-sm mb-1 truncate">{app.applicationName}</h4>
|
{app.applicationName ? (
|
||||||
<code className="text-xs text-muted-foreground truncate block">
|
<h4 className="font-semibold text-sm mb-1 truncate">{app.applicationName}</h4>
|
||||||
{app.applicationCode}
|
) : (
|
||||||
</code>
|
<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>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1.5 mb-3 text-xs text-muted-foreground">
|
<div className="space-y-1.5 mb-3 text-xs text-muted-foreground">
|
||||||
|
{/* 分支 */}
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<GitBranch className="h-3 w-3 shrink-0" />
|
<GitBranch className="h-3 w-3 shrink-0" />
|
||||||
<code className="truncate font-mono">{app.branch}</code>
|
{app.branch ? (
|
||||||
|
<code className="truncate font-mono">{app.branch}</code>
|
||||||
|
) : (
|
||||||
|
<Skeleton className="h-3 w-16" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 工作流 */}
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<Rocket className="h-3 w-3 shrink-0" />
|
<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>
|
</div>
|
||||||
|
|
||||||
{app.deploySystemName && (
|
{/* Jenkins */}
|
||||||
|
{app.deploySystemName ? (
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<Server className="h-3 w-3 shrink-0" />
|
<Server className="h-3 w-3 shrink-0" />
|
||||||
<span className="truncate">{app.deploySystemName}</span>
|
<span className="truncate">{app.deploySystemName}</span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => handleDeploy(app, currentEnv?.requiresApproval || false)}
|
onClick={() => handleDeploy(app, currentEnv?.requiresApproval || false)}
|
||||||
disabled={!currentEnv?.enabled || isDeploying}
|
disabled={!currentEnv?.enabled || isDeploying || app.isDeploying}
|
||||||
size="sm"
|
size="sm"
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-full text-xs h-8',
|
'w-full text-xs h-8',
|
||||||
currentEnv?.requiresApproval && 'bg-amber-600 hover:bg-amber-700'
|
currentEnv?.requiresApproval && 'bg-amber-600 hover:bg-amber-700'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isDeploying ? (
|
{(isDeploying || app.isDeploying) ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="h-3 w-3 mr-1 animate-spin" />
|
<Loader2 className="h-3 w-3 mr-1 animate-spin" />
|
||||||
处理中
|
部署中
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -1,11 +1,29 @@
|
|||||||
import type { BaseResponse } from '@/types/base';
|
|
||||||
|
|
||||||
export interface Approver {
|
export interface Approver {
|
||||||
userId: number;
|
userId: number;
|
||||||
username: string;
|
username: string;
|
||||||
realName: 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 {
|
export interface ApplicationConfig {
|
||||||
teamApplicationId: number;
|
teamApplicationId: number;
|
||||||
applicationId: number;
|
applicationId: number;
|
||||||
@ -19,10 +37,13 @@ export interface ApplicationConfig {
|
|||||||
workflowDefinitionId?: number;
|
workflowDefinitionId?: number;
|
||||||
workflowDefinitionName?: string;
|
workflowDefinitionName?: string;
|
||||||
workflowDefinitionKey?: string;
|
workflowDefinitionKey?: string;
|
||||||
|
deployStatistics?: DeployStatistics;
|
||||||
|
isDeploying?: boolean;
|
||||||
|
recentDeployRecords?: DeployRecord[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeployEnvironment {
|
export interface DeployEnvironment {
|
||||||
environmentId: number;
|
environmentId: number;
|
||||||
environmentCode: string;
|
environmentCode: string;
|
||||||
environmentName: string;
|
environmentName: string;
|
||||||
environmentDesc?: string;
|
environmentDesc?: string;
|
||||||
|
|||||||
@ -42,7 +42,6 @@ const EditDialog: React.FC<EditDialogProps> = ({
|
|||||||
if (open) {
|
if (open) {
|
||||||
if (record) {
|
if (record) {
|
||||||
setFormData({
|
setFormData({
|
||||||
tenantCode: record.tenantCode,
|
|
||||||
envCode: record.envCode,
|
envCode: record.envCode,
|
||||||
envName: record.envName,
|
envName: record.envName,
|
||||||
envDesc: record.envDesc,
|
envDesc: record.envDesc,
|
||||||
@ -58,10 +57,6 @@ const EditDialog: React.FC<EditDialogProps> = ({
|
|||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
// 验证
|
// 验证
|
||||||
if (!formData.tenantCode?.trim()) {
|
|
||||||
toast({ title: '提示', description: '请输入租户代码', variant: 'destructive' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!formData.envCode?.trim()) {
|
if (!formData.envCode?.trim()) {
|
||||||
toast({ title: '提示', description: '请输入环境编码', variant: 'destructive' });
|
toast({ title: '提示', description: '请输入环境编码', variant: 'destructive' });
|
||||||
return;
|
return;
|
||||||
@ -99,17 +94,6 @@ const EditDialog: React.FC<EditDialogProps> = ({
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogBody>
|
<DialogBody>
|
||||||
<div className="grid gap-4">
|
<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">
|
<div className="grid gap-2">
|
||||||
<Label htmlFor="envCode">环境编码 *</Label>
|
<Label htmlFor="envCode">环境编码 *</Label>
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@ -228,11 +228,28 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
|
|||||||
|
|
||||||
// 设置输入映射默认值(转换 UUID 为显示名称)
|
// 设置输入映射默认值(转换 UUID 为显示名称)
|
||||||
if (isConfigurableNode(nodeDefinition)) {
|
if (isConfigurableNode(nodeDefinition)) {
|
||||||
|
const savedInputMapping = nodeData.inputMapping || {};
|
||||||
const displayInputMapping = convertObjectToDisplayName(
|
const displayInputMapping = convertObjectToDisplayName(
|
||||||
nodeData.inputMapping || {},
|
savedInputMapping,
|
||||||
allNodes
|
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 改变时重置表单
|
// 只在 visible 或 node.id 改变时重置表单
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user