/*
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
import {
Button,
Dropdown,
Tag,
Typography,
} from '@douyinfe/semi-ui';
import {
timestamp2string,
showSuccess,
showError,
} from '../../../helpers';
import { IconMore } from '@douyinfe/semi-icons';
import {
FaPlay,
FaTrash,
FaServer,
FaMemory,
FaMicrochip,
FaCheckCircle,
FaSpinner,
FaClock,
FaExclamationCircle,
FaBan,
FaTerminal,
FaPlus,
FaCog,
FaInfoCircle,
FaLink,
FaStop,
FaHourglassHalf,
FaGlobe,
} from 'react-icons/fa';
import {t} from "i18next";
const normalizeStatus = (status) =>
typeof status === 'string' ? status.trim().toLowerCase() : '';
const STATUS_TAG_CONFIG = {
running: {
color: 'green',
label: t('运行中'),
icon: ,
},
deploying: {
color: 'blue',
label: t('部署中'),
icon: ,
},
pending: {
color: 'orange',
label: t('待部署'),
icon: ,
},
stopped: {
color: 'grey',
label: t('已停止'),
icon: ,
},
error: {
color: 'red',
label: t('错误'),
icon: ,
},
failed: {
color: 'red',
label: t('失败'),
icon: ,
},
destroyed: {
color: 'red',
label: t('已销毁'),
icon: ,
},
completed: {
color: 'green',
label: t('已完成'),
icon: ,
},
'deployment requested': {
color: 'blue',
label: t('部署请求中'),
icon: ,
},
'termination requested': {
color: 'orange',
label: t('终止请求中'),
icon: ,
},
};
const DEFAULT_STATUS_CONFIG = {
color: 'grey',
label: null,
icon: ,
};
const parsePercentValue = (value) => {
if (value === null || value === undefined) return null;
if (typeof value === 'string') {
const parsed = parseFloat(value.replace(/[^0-9.+-]/g, ''));
return Number.isFinite(parsed) ? parsed : null;
}
if (typeof value === 'number') {
return Number.isFinite(value) ? value : null;
}
return null;
};
const clampPercent = (value) => {
if (value === null || value === undefined) return null;
return Math.min(100, Math.max(0, Math.round(value)));
};
const formatRemainingMinutes = (minutes, t) => {
if (minutes === null || minutes === undefined) return null;
const numeric = Number(minutes);
if (!Number.isFinite(numeric)) return null;
const totalMinutes = Math.max(0, Math.round(numeric));
const days = Math.floor(totalMinutes / 1440);
const hours = Math.floor((totalMinutes % 1440) / 60);
const mins = totalMinutes % 60;
const parts = [];
if (days > 0) {
parts.push(`${days}${t('天')}`);
}
if (hours > 0) {
parts.push(`${hours}${t('小时')}`);
}
if (parts.length === 0 || mins > 0) {
parts.push(`${mins}${t('分钟')}`);
}
return parts.join(' ');
};
const getRemainingTheme = (percentRemaining) => {
if (percentRemaining === null) {
return {
iconColor: 'var(--semi-color-primary)',
tagColor: 'blue',
textColor: 'var(--semi-color-text-2)',
};
}
if (percentRemaining <= 10) {
return {
iconColor: '#ff5a5f',
tagColor: 'red',
textColor: '#ff5a5f',
};
}
if (percentRemaining <= 30) {
return {
iconColor: '#ffb400',
tagColor: 'orange',
textColor: '#ffb400',
};
}
return {
iconColor: '#2ecc71',
tagColor: 'green',
textColor: '#2ecc71',
};
};
const renderStatus = (status, t) => {
const normalizedStatus = normalizeStatus(status);
const config = STATUS_TAG_CONFIG[normalizedStatus] || DEFAULT_STATUS_CONFIG;
const statusText = typeof status === 'string' ? status : '';
const labelText = config.label ? t(config.label) : statusText || t('未知状态');
return (
{labelText}
);
};
// Container Name Cell Component - to properly handle React hooks
const ContainerNameCell = ({ text, record, t }) => {
const handleCopyId = () => {
navigator.clipboard.writeText(record.id);
showSuccess(t('ID已复制到剪贴板'));
};
return (
{text}
ID: {record.id}
);
};
// Render resource configuration
const renderResourceConfig = (resource, t) => {
if (!resource) return '-';
const { cpu, memory, gpu } = resource;
return (
{cpu && (
CPU: {cpu}
)}
{memory && (
内存: {memory}
)}
{gpu && (
GPU: {gpu}
)}
);
};
// Render instance count with status indicator
const renderInstanceCount = (count, record, t) => {
const normalizedStatus = normalizeStatus(record?.status);
const statusConfig = STATUS_TAG_CONFIG[normalizedStatus];
const countColor = statusConfig?.color ?? 'grey';
return (
{count || 0} {t('个实例')}
);
};
// Main function to get all deployment columns
export const getDeploymentsColumns = ({
t,
COLUMN_KEYS,
startDeployment,
restartDeployment,
deleteDeployment,
setEditingDeployment,
setShowEdit,
refresh,
activePage,
deployments,
// New handlers for enhanced operations
onViewLogs,
onExtendDuration,
onViewDetails,
onUpdateConfig,
onSyncToChannel,
}) => {
const columns = [
{
title: t('容器名称'),
dataIndex: 'container_name',
key: COLUMN_KEYS.container_name,
width: 300,
ellipsis: true,
render: (text, record) => (
),
},
{
title: t('状态'),
dataIndex: 'status',
key: COLUMN_KEYS.status,
width: 140,
render: (status) => (
{renderStatus(status, t)}
),
},
{
title: t('服务商'),
dataIndex: 'provider',
key: COLUMN_KEYS.provider,
width: 140,
render: (provider) =>
provider ? (
{provider}
) : (
{t('暂无')}
),
},
{
title: t('剩余时间'),
dataIndex: 'time_remaining',
key: COLUMN_KEYS.time_remaining,
width: 140,
render: (text, record) => {
const normalizedStatus = normalizeStatus(record?.status);
const percentUsedRaw = parsePercentValue(record?.completed_percent);
const percentUsed = clampPercent(percentUsedRaw);
const percentRemaining =
percentUsed === null ? null : clampPercent(100 - percentUsed);
const theme = getRemainingTheme(percentRemaining);
const statusDisplayMap = {
completed: t('已完成'),
destroyed: t('已销毁'),
failed: t('失败'),
error: t('失败'),
stopped: t('已停止'),
pending: t('待部署'),
deploying: t('部署中'),
'deployment requested': t('部署请求中'),
'termination requested': t('终止中'),
};
const statusOverride = statusDisplayMap[normalizedStatus];
const baseTimeDisplay =
text && String(text).trim() !== '' ? text : t('计算中');
const timeDisplay = baseTimeDisplay;
const humanReadable = formatRemainingMinutes(
record.compute_minutes_remaining,
t,
);
const showProgress = !statusOverride && normalizedStatus === 'running';
const showExtraInfo = Boolean(humanReadable || percentUsed !== null);
const showRemainingMeta =
record.compute_minutes_remaining !== undefined &&
record.compute_minutes_remaining !== null &&
percentRemaining !== null;
return (
{timeDisplay}
{showProgress && percentRemaining !== null ? (
{percentRemaining}%
) : statusOverride ? (
{statusOverride}
) : null}
{showExtraInfo && (
{humanReadable && (
{t('约')} {humanReadable}
)}
{percentUsed !== null && (
{t('已用')} {percentUsed}%
)}
)}
{showProgress && showRemainingMeta && (
{t('剩余')} {record.compute_minutes_remaining} {t('分钟')}
)}
);
},
},
{
title: t('硬件配置'),
dataIndex: 'hardware_info',
key: COLUMN_KEYS.hardware_info,
width: 220,
ellipsis: true,
render: (text, record) => (
{record.hardware_name}
x{record.hardware_quantity}
),
},
{
title: t('创建时间'),
dataIndex: 'created_at',
key: COLUMN_KEYS.created_at,
width: 150,
render: (text) => (
{timestamp2string(text)}
),
},
{
title: t('操作'),
key: COLUMN_KEYS.actions,
fixed: 'right',
width: 120,
render: (_, record) => {
const { status, id } = record;
const normalizedStatus = normalizeStatus(status);
const isEnded = normalizedStatus === 'completed' || normalizedStatus === 'destroyed';
const handleDelete = () => {
// Use enhanced confirmation dialog
onUpdateConfig?.(record, 'delete');
};
// Get primary action based on status
const getPrimaryAction = () => {
switch (normalizedStatus) {
case 'running':
return {
icon: ,
text: t('查看详情'),
onClick: () => onViewDetails?.(record),
type: 'secondary',
theme: 'borderless',
};
case 'failed':
case 'error':
return {
icon: ,
text: t('重试'),
onClick: () => startDeployment(id),
type: 'primary',
theme: 'solid',
};
case 'stopped':
return {
icon: ,
text: t('启动'),
onClick: () => startDeployment(id),
type: 'primary',
theme: 'solid',
};
case 'deployment requested':
case 'deploying':
return {
icon: ,
text: t('部署中'),
onClick: () => {},
type: 'secondary',
theme: 'light',
disabled: true,
};
case 'pending':
return {
icon: ,
text: t('待部署'),
onClick: () => {},
type: 'secondary',
theme: 'light',
disabled: true,
};
case 'termination requested':
return {
icon: ,
text: t('终止中'),
onClick: () => {},
type: 'secondary',
theme: 'light',
disabled: true,
};
case 'completed':
case 'destroyed':
default:
return {
icon: ,
text: t('已结束'),
onClick: () => {},
type: 'tertiary',
theme: 'borderless',
disabled: true,
};
}
};
const primaryAction = getPrimaryAction();
const primaryTheme = primaryAction.theme || 'solid';
const primaryType = primaryAction.type || 'primary';
if (isEnded) {
return (
);
}
// All actions dropdown with enhanced operations
const dropdownItems = [
onViewDetails?.(record)} icon={}>
{t('查看详情')}
,
];
if (!isEnded) {
dropdownItems.push(
onViewLogs?.(record)} icon={}>
{t('查看日志')}
,
);
}
const managementItems = [];
if (normalizedStatus === 'running') {
if (onSyncToChannel) {
managementItems.push(
onSyncToChannel(record)} icon={}>
{t('同步到渠道')}
,
);
}
}
if (normalizedStatus === 'failed' || normalizedStatus === 'error') {
managementItems.push(
startDeployment(id)} icon={}>
{t('重试')}
,
);
}
if (normalizedStatus === 'stopped') {
managementItems.push(
startDeployment(id)} icon={}>
{t('启动')}
,
);
}
if (managementItems.length > 0) {
dropdownItems.push();
dropdownItems.push(...managementItems);
}
const configItems = [];
if (!isEnded && (normalizedStatus === 'running' || normalizedStatus === 'deployment requested')) {
configItems.push(
onExtendDuration?.(record)} icon={}>
{t('延长时长')}
,
);
}
// if (!isEnded && normalizedStatus === 'running') {
// configItems.push(
// onUpdateConfig?.(record)} icon={}>
// {t('更新配置')}
// ,
// );
// }
if (configItems.length > 0) {
dropdownItems.push();
dropdownItems.push(...configItems);
}
if (!isEnded) {
dropdownItems.push();
dropdownItems.push(
}>
{t('销毁容器')}
,
);
}
const allActions = {dropdownItems};
const hasDropdown = dropdownItems.length > 0;
return (
{hasDropdown && (
}
className="px-1"
/>
)}
);
},
},
];
return columns;
};