/* 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 && (
); }, }, ]; return columns; };