|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import { useState, useEffect, useMemo } from 'react'; |
|
|
import { useTranslation } from 'react-i18next'; |
|
|
import { API, showError, showSuccess } from '../../helpers'; |
|
|
import { ITEMS_PER_PAGE } from '../../constants'; |
|
|
import { useTableCompactMode } from '../common/useTableCompactMode'; |
|
|
|
|
|
export const useDeploymentsData = () => { |
|
|
const { t } = useTranslation(); |
|
|
const [compactMode, setCompactMode] = useTableCompactMode('deployments'); |
|
|
|
|
|
|
|
|
const [deployments, setDeployments] = useState([]); |
|
|
const [loading, setLoading] = useState(true); |
|
|
const [activePage, setActivePage] = useState(1); |
|
|
const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE); |
|
|
const [searching, setSearching] = useState(false); |
|
|
const [deploymentCount, setDeploymentCount] = useState(0); |
|
|
|
|
|
|
|
|
const [showEdit, setShowEdit] = useState(false); |
|
|
const [editingDeployment, setEditingDeployment] = useState({ |
|
|
id: undefined, |
|
|
}); |
|
|
|
|
|
|
|
|
const [selectedKeys, setSelectedKeys] = useState([]); |
|
|
const rowSelection = { |
|
|
getCheckboxProps: (record) => ({ |
|
|
name: record.deployment_name, |
|
|
}), |
|
|
selectedRowKeys: selectedKeys.map((deployment) => deployment.id), |
|
|
onChange: (selectedRowKeys, selectedRows) => { |
|
|
setSelectedKeys(selectedRows); |
|
|
}, |
|
|
}; |
|
|
|
|
|
|
|
|
const formInitValues = { |
|
|
searchKeyword: '', |
|
|
searchStatus: '', |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const extractItems = (payload) => { |
|
|
const items = payload?.items || payload || []; |
|
|
return Array.isArray(items) ? items : []; |
|
|
}; |
|
|
|
|
|
|
|
|
const [formApi, setFormApi] = useState(null); |
|
|
|
|
|
|
|
|
const getFormValues = () => formApi?.getValues() || formInitValues; |
|
|
|
|
|
|
|
|
const closeEdit = () => { |
|
|
setShowEdit(false); |
|
|
setTimeout(() => { |
|
|
setEditingDeployment({ id: undefined }); |
|
|
}, 500); |
|
|
}; |
|
|
|
|
|
|
|
|
const setDeploymentFormat = (deployments) => { |
|
|
for (let i = 0; i < deployments.length; i++) { |
|
|
deployments[i].key = deployments[i].id; |
|
|
} |
|
|
setDeployments(deployments); |
|
|
}; |
|
|
|
|
|
|
|
|
const [activeStatusKey, setActiveStatusKey] = useState('all'); |
|
|
const [statusCounts, setStatusCounts] = useState({}); |
|
|
|
|
|
|
|
|
const COLUMN_KEYS = useMemo( |
|
|
() => ({ |
|
|
id: 'id', |
|
|
status: 'status', |
|
|
provider: 'provider', |
|
|
container_name: 'container_name', |
|
|
time_remaining: 'time_remaining', |
|
|
hardware_info: 'hardware_info', |
|
|
created_at: 'created_at', |
|
|
actions: 'actions', |
|
|
|
|
|
deployment_name: 'deployment_name', |
|
|
model_name: 'model_name', |
|
|
instance_count: 'instance_count', |
|
|
resource_config: 'resource_config', |
|
|
updated_at: 'updated_at', |
|
|
}), |
|
|
[], |
|
|
); |
|
|
|
|
|
const ensureRequiredColumns = (columns = {}) => { |
|
|
const normalized = { |
|
|
...columns, |
|
|
[COLUMN_KEYS.container_name]: true, |
|
|
[COLUMN_KEYS.actions]: true, |
|
|
}; |
|
|
|
|
|
if (normalized[COLUMN_KEYS.provider] === undefined) { |
|
|
normalized[COLUMN_KEYS.provider] = true; |
|
|
} |
|
|
|
|
|
return normalized; |
|
|
}; |
|
|
|
|
|
const [visibleColumns, setVisibleColumnsState] = useState(() => { |
|
|
const saved = localStorage.getItem('deployments_visible_columns'); |
|
|
if (saved) { |
|
|
try { |
|
|
const parsed = JSON.parse(saved); |
|
|
return ensureRequiredColumns(parsed); |
|
|
} catch (e) { |
|
|
console.error('Failed to parse saved column visibility:', e); |
|
|
} |
|
|
} |
|
|
return ensureRequiredColumns({ |
|
|
[COLUMN_KEYS.container_name]: true, |
|
|
[COLUMN_KEYS.status]: true, |
|
|
[COLUMN_KEYS.provider]: true, |
|
|
[COLUMN_KEYS.time_remaining]: true, |
|
|
[COLUMN_KEYS.hardware_info]: true, |
|
|
[COLUMN_KEYS.created_at]: true, |
|
|
[COLUMN_KEYS.actions]: true, |
|
|
|
|
|
[COLUMN_KEYS.deployment_name]: false, |
|
|
[COLUMN_KEYS.model_name]: false, |
|
|
[COLUMN_KEYS.instance_count]: false, |
|
|
[COLUMN_KEYS.resource_config]: false, |
|
|
[COLUMN_KEYS.updated_at]: false, |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
const [showColumnSelector, setShowColumnSelector] = useState(false); |
|
|
|
|
|
|
|
|
const saveColumnVisibility = (newVisibleColumns) => { |
|
|
const normalized = ensureRequiredColumns(newVisibleColumns); |
|
|
localStorage.setItem('deployments_visible_columns', JSON.stringify(normalized)); |
|
|
setVisibleColumnsState(normalized); |
|
|
}; |
|
|
|
|
|
|
|
|
const loadDeployments = async ( |
|
|
page = 1, |
|
|
size = pageSize, |
|
|
statusKey = activeStatusKey, |
|
|
) => { |
|
|
setLoading(true); |
|
|
try { |
|
|
let url = `/api/deployments/?p=${page}&page_size=${size}`; |
|
|
if (statusKey && statusKey !== 'all') { |
|
|
url = `/api/deployments/search?status=${statusKey}&p=${page}&page_size=${size}`; |
|
|
} |
|
|
|
|
|
const res = await API.get(url); |
|
|
const { success, message, data } = res.data; |
|
|
if (success) { |
|
|
const newPageData = extractItems(data); |
|
|
setActivePage(data.page || page); |
|
|
setDeploymentCount(data.total || newPageData.length); |
|
|
setDeploymentFormat(newPageData); |
|
|
|
|
|
if (data.status_counts) { |
|
|
const sumAll = Object.values(data.status_counts).reduce( |
|
|
(acc, v) => acc + v, |
|
|
0, |
|
|
); |
|
|
setStatusCounts({ ...data.status_counts, all: sumAll }); |
|
|
} |
|
|
} else { |
|
|
showError(message); |
|
|
setDeployments([]); |
|
|
} |
|
|
} catch (error) { |
|
|
console.error(error); |
|
|
showError(t('获取部署列表失败')); |
|
|
setDeployments([]); |
|
|
} |
|
|
setLoading(false); |
|
|
}; |
|
|
|
|
|
|
|
|
const searchDeployments = async (searchTerms) => { |
|
|
setSearching(true); |
|
|
try { |
|
|
const { searchKeyword, searchStatus } = searchTerms; |
|
|
const params = new URLSearchParams({ |
|
|
p: '1', |
|
|
page_size: pageSize.toString(), |
|
|
}); |
|
|
|
|
|
if (searchKeyword?.trim()) { |
|
|
params.append('keyword', searchKeyword.trim()); |
|
|
} |
|
|
if (searchStatus && searchStatus !== 'all') { |
|
|
params.append('status', searchStatus); |
|
|
} |
|
|
|
|
|
const res = await API.get(`/api/deployments/search?${params}`); |
|
|
const { success, message, data } = res.data; |
|
|
|
|
|
if (success) { |
|
|
const items = extractItems(data); |
|
|
setActivePage(1); |
|
|
setDeploymentCount(data.total || items.length); |
|
|
setDeploymentFormat(items); |
|
|
} else { |
|
|
showError(message); |
|
|
setDeployments([]); |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('Search error:', error); |
|
|
showError(t('搜索失败')); |
|
|
setDeployments([]); |
|
|
} |
|
|
setSearching(false); |
|
|
}; |
|
|
|
|
|
|
|
|
const refresh = async (page = activePage) => { |
|
|
await loadDeployments(page, pageSize); |
|
|
}; |
|
|
|
|
|
|
|
|
const handlePageChange = (page) => { |
|
|
setActivePage(page); |
|
|
if (!searching) { |
|
|
loadDeployments(page, pageSize); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
const handlePageSizeChange = (size) => { |
|
|
setPageSize(size); |
|
|
setActivePage(1); |
|
|
if (!searching) { |
|
|
loadDeployments(1, size); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
const handleTabChange = (statusKey) => { |
|
|
setActiveStatusKey(statusKey); |
|
|
setActivePage(1); |
|
|
loadDeployments(1, pageSize, statusKey); |
|
|
}; |
|
|
|
|
|
|
|
|
const startDeployment = async (deploymentId) => { |
|
|
try { |
|
|
const res = await API.post(`/api/deployments/${deploymentId}/start`); |
|
|
if (res.data.success) { |
|
|
showSuccess(t('部署启动成功')); |
|
|
await refresh(); |
|
|
} else { |
|
|
showError(res.data.message); |
|
|
} |
|
|
} catch (error) { |
|
|
console.error(error); |
|
|
showError(t('启动部署失败')); |
|
|
} |
|
|
}; |
|
|
|
|
|
const restartDeployment = async (deploymentId) => { |
|
|
try { |
|
|
const res = await API.post(`/api/deployments/${deploymentId}/restart`); |
|
|
if (res.data.success) { |
|
|
showSuccess(t('部署重启成功')); |
|
|
await refresh(); |
|
|
} else { |
|
|
showError(res.data.message); |
|
|
} |
|
|
} catch (error) { |
|
|
console.error(error); |
|
|
showError(t('重启部署失败')); |
|
|
} |
|
|
}; |
|
|
|
|
|
const deleteDeployment = async (deploymentId) => { |
|
|
try { |
|
|
const res = await API.delete(`/api/deployments/${deploymentId}`); |
|
|
if (res.data.success) { |
|
|
showSuccess(t('部署删除成功')); |
|
|
await refresh(); |
|
|
} else { |
|
|
showError(res.data.message); |
|
|
} |
|
|
} catch (error) { |
|
|
console.error(error); |
|
|
showError(t('删除部署失败')); |
|
|
} |
|
|
}; |
|
|
|
|
|
const syncDeploymentToChannel = async (deployment) => { |
|
|
if (!deployment?.id) { |
|
|
showError(t('同步渠道失败:缺少部署信息')); |
|
|
return; |
|
|
} |
|
|
|
|
|
try { |
|
|
const containersResp = await API.get(`/api/deployments/${deployment.id}/containers`); |
|
|
if (!containersResp.data?.success) { |
|
|
showError(containersResp.data?.message || t('获取容器信息失败')); |
|
|
return; |
|
|
} |
|
|
|
|
|
const containers = containersResp.data?.data?.containers || []; |
|
|
const activeContainer = containers.find((ctr) => ctr?.public_url); |
|
|
|
|
|
if (!activeContainer?.public_url) { |
|
|
showError(t('未找到可用的容器访问地址')); |
|
|
return; |
|
|
} |
|
|
|
|
|
const rawUrl = String(activeContainer.public_url).trim(); |
|
|
const baseUrl = rawUrl.replace(/\/+$/, ''); |
|
|
if (!baseUrl) { |
|
|
showError(t('容器访问地址无效')); |
|
|
return; |
|
|
} |
|
|
|
|
|
const baseName = deployment.container_name || deployment.deployment_name || deployment.name || deployment.id; |
|
|
const safeName = String(baseName || 'ionet').slice(0, 60); |
|
|
const channelName = `[IO.NET] ${safeName}`; |
|
|
|
|
|
let randomKey; |
|
|
try { |
|
|
randomKey = (typeof crypto !== 'undefined' && crypto.randomUUID) |
|
|
? `ionet-${crypto.randomUUID().replace(/-/g, '')}` |
|
|
: null; |
|
|
} catch (err) { |
|
|
randomKey = null; |
|
|
} |
|
|
if (!randomKey) { |
|
|
randomKey = `ionet-${Math.random().toString(36).slice(2)}${Math.random().toString(36).slice(2)}`; |
|
|
} |
|
|
|
|
|
const otherInfo = { |
|
|
source: 'ionet', |
|
|
deployment_id: deployment.id, |
|
|
deployment_name: safeName, |
|
|
container_id: activeContainer.container_id || null, |
|
|
public_url: baseUrl, |
|
|
}; |
|
|
|
|
|
const payload = { |
|
|
mode: 'single', |
|
|
channel: { |
|
|
name: channelName, |
|
|
type: 4, |
|
|
key: randomKey, |
|
|
base_url: baseUrl, |
|
|
group: 'default', |
|
|
tag: 'ionet', |
|
|
remark: `[IO.NET] Auto-synced from deployment ${deployment.id}`, |
|
|
other_info: JSON.stringify(otherInfo), |
|
|
}, |
|
|
}; |
|
|
|
|
|
const createResp = await API.post('/api/channel/', payload); |
|
|
if (createResp.data?.success) { |
|
|
showSuccess(t('已同步到渠道')); |
|
|
} else { |
|
|
showError(createResp.data?.message || t('同步渠道失败')); |
|
|
} |
|
|
} catch (error) { |
|
|
console.error(error); |
|
|
showError(t('同步渠道失败')); |
|
|
} |
|
|
}; |
|
|
|
|
|
const updateDeploymentName = async (deploymentId, newName) => { |
|
|
try { |
|
|
const res = await API.put(`/api/deployments/${deploymentId}/name`, { name: newName }); |
|
|
if (res.data.success) { |
|
|
showSuccess(t('部署名称更新成功')); |
|
|
await refresh(); |
|
|
return true; |
|
|
} else { |
|
|
showError(res.data.message); |
|
|
return false; |
|
|
} |
|
|
} catch (error) { |
|
|
console.error(error); |
|
|
showError(t('更新部署名称失败')); |
|
|
return false; |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
const batchDeleteDeployments = async () => { |
|
|
if (selectedKeys.length === 0) return; |
|
|
|
|
|
try { |
|
|
const ids = selectedKeys.map(deployment => deployment.id); |
|
|
const res = await API.post('/api/deployments/batch_delete', { ids }); |
|
|
if (res.data.success) { |
|
|
showSuccess(t('批量删除成功')); |
|
|
setSelectedKeys([]); |
|
|
await refresh(); |
|
|
} else { |
|
|
showError(res.data.message); |
|
|
} |
|
|
} catch (error) { |
|
|
console.error(error); |
|
|
showError(t('批量删除失败')); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
const handleRow = (record) => ({ |
|
|
onClick: () => { |
|
|
|
|
|
}, |
|
|
}); |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
loadDeployments(); |
|
|
}, []); |
|
|
|
|
|
return { |
|
|
|
|
|
deployments, |
|
|
loading, |
|
|
searching, |
|
|
activePage, |
|
|
pageSize, |
|
|
deploymentCount, |
|
|
statusCounts, |
|
|
activeStatusKey, |
|
|
compactMode, |
|
|
setCompactMode, |
|
|
|
|
|
|
|
|
selectedKeys, |
|
|
setSelectedKeys, |
|
|
rowSelection, |
|
|
|
|
|
|
|
|
showEdit, |
|
|
setShowEdit, |
|
|
editingDeployment, |
|
|
setEditingDeployment, |
|
|
closeEdit, |
|
|
|
|
|
|
|
|
visibleColumns, |
|
|
setVisibleColumns: saveColumnVisibility, |
|
|
showColumnSelector, |
|
|
setShowColumnSelector, |
|
|
COLUMN_KEYS, |
|
|
|
|
|
|
|
|
formInitValues, |
|
|
formApi, |
|
|
setFormApi, |
|
|
getFormValues, |
|
|
|
|
|
|
|
|
loadDeployments, |
|
|
searchDeployments, |
|
|
refresh, |
|
|
handlePageChange, |
|
|
handlePageSizeChange, |
|
|
handleTabChange, |
|
|
handleRow, |
|
|
|
|
|
|
|
|
startDeployment, |
|
|
restartDeployment, |
|
|
deleteDeployment, |
|
|
updateDeploymentName, |
|
|
syncDeploymentToChannel, |
|
|
|
|
|
|
|
|
batchDeleteDeployments, |
|
|
|
|
|
|
|
|
t, |
|
|
}; |
|
|
}; |
|
|
|