refactor: gpu service API alignment

This commit is contained in:
jialin
2026-05-25 22:42:02 +08:00
committed by jialin
parent 81c7e2ee61
commit 88ab927755
90 changed files with 3247 additions and 2023 deletions
+72 -345
View File
@@ -1,14 +1,8 @@
import { currentClusterAtom } from '@/atoms/gpuservice';
import { getCurrentOrgNamespace } from '@/atoms/user';
import { PageAction } from '@/config';
import { PaginationKey, TABLE_SORT_DIRECTIONS } from '@/config/settings';
import useTableFetch from '@/hooks/use-table-fetch';
import { ProviderValueMap } from '@/pages/cluster-management/config';
import { useQueryClusterList } from '@/pages/cluster-management/services/use-query-cluster-list';
import { handleBatchRequest } from '@/utils';
import { PlusOutlined } from '@ant-design/icons';
import {
BaseSelect,
DeleteModal,
DropdownButtons,
FilterBar,
@@ -17,26 +11,13 @@ import {
} from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { useMemoizedFn } from 'ahooks';
import {
Button,
ConfigProvider,
Divider,
Flex,
message,
Modal,
Space,
Table
} from 'antd';
import { useAtom } from 'jotai';
import { Button, ConfigProvider, message, Modal, Space, Table } from 'antd';
import _ from 'lodash';
import { useCallback, useEffect, useMemo } from 'react';
import { PageContainerInner } from '../../_components/page-box';
import PageBox from '../../_components/page-box';
import {
deleteGPUServiceInstance,
GPU_SERVICE_INSTANCES_API,
queryGPUServiceInstances,
startGPUServiceInstance,
stopGPUServiceInstance
queryGPUServiceInstances
} from './apis';
import AddModal from './components/add-modal';
import ViewEventsModal from './components/view-events-modal';
@@ -52,72 +33,7 @@ import useUpdateInstance from './services/use-update-instance';
const GPUService: React.FC = () => {
const intl = useIntl();
const [modal, modalContextHolder] = Modal.useModal();
const [currentCluster, setCurrentCluster] = useAtom(currentClusterAtom);
const clusterID = currentCluster?.id;
// In admin "All" view there's no Org context, so the helper falls
// back to the selected cluster's owner Org name.
const namespace = getCurrentOrgNamespace(currentCluster?.owner_principal_id);
const deleteInstance = useCallback(
(id: number) => deleteGPUServiceInstance({ namespace, clusterID, id }),
[namespace, clusterID]
);
const startInstance = useCallback(
(row: ListItem) =>
startGPUServiceInstance({
namespace: row.metadata?.namespace || namespace,
clusterID,
name: row.metadata?.name as string
}),
[namespace, clusterID]
);
const stopInstance = useCallback(
(row: ListItem) =>
stopGPUServiceInstance({
namespace: row.metadata?.namespace || namespace,
clusterID,
name: row.metadata?.name as string
}),
[namespace, clusterID]
);
const fetchInstances = useMemoizedFn(
async (
params: any,
options?: any
): Promise<Global.PageResponse<ListItem>> => {
const effectiveClusterID = params.cluster_id ?? clusterID;
if (!effectiveClusterID) {
return {
items: [],
pagination: {
total: 0,
totalPage: 0,
page: 1,
perPage: params.perPage || 10
}
} as Global.PageResponse<ListItem>;
}
const res = await queryGPUServiceInstances(
{ ...params, namespace, clusterID: effectiveClusterID },
options
);
const total = res?.items?.length ?? 0;
const perPage = params.perPage || 10;
return {
items: res?.items ?? [],
pagination: {
total,
totalPage: Math.ceil(total / perPage),
page: params.page || 1,
perPage
}
};
}
);
const [, modalContextHolder] = Modal.useModal();
const {
dataSource,
@@ -130,16 +46,14 @@ const GPUService: React.FC = () => {
fetchData,
handlePageChange,
handleTableChange,
handleQueryChange,
handleSearch,
handleNameChange
} = useTableFetch<ListItem>({
key: PaginationKey.Instances,
fetchAPI: fetchInstances,
deleteAPI: deleteInstance,
watch: false,
polling: true,
API: GPU_SERVICE_INSTANCES_API({ namespace, clusterID }),
fetchAPI: queryGPUServiceInstances,
deleteAPI: deleteGPUServiceInstance,
watch: true,
API: GPU_SERVICE_INSTANCES_API,
contentForDelete: intl.formatMessage({ id: 'gpuservice.instance' })
});
@@ -160,55 +74,11 @@ const GPUService: React.FC = () => {
closeViewEventsModal,
openViewEventsModalStatus
} = useViewEvents();
const {
fetchClusterList,
cancelRequest: cancelClusterRequest,
clusterList
} = useQueryClusterList();
const k8sClusterList = useMemo(
() =>
clusterList.filter(
(item) => item.provider === ProviderValueMap.Kubernetes
),
[clusterList]
);
useEffect(() => {
fetchClusterList({ page: -1 }).then((clusters) => {
const k8sClusters = clusters.filter(
(item: any) => item.provider === ProviderValueMap.Kubernetes
);
if (k8sClusters.length === 0) {
return;
}
const storedCluster = k8sClusters.find(
(item: any) => item.id === currentCluster?.id
);
const targetCluster = storedCluster ?? k8sClusters[0];
if (!storedCluster) {
setCurrentCluster({
...targetCluster,
label: targetCluster.name,
value: targetCluster.id
});
}
handleQueryChange({
cluster_id: targetCluster.id,
page: 1
});
});
return () => {
cancelClusterRequest();
};
}, []);
const handleModalOk = async (data: FormData) => {
try {
if (openInstanceModalStatus.realAction === PageAction.CREATE) {
await deleteInstance(
openInstanceModalStatus.currentData!.metadata?.name as any
);
await deleteGPUServiceInstance(openInstanceModalStatus.currentData!.id);
await new Promise((resolve) => {
setTimeout(resolve, 500);
});
@@ -226,161 +96,34 @@ const GPUService: React.FC = () => {
closeInstanceModal();
message.success(intl.formatMessage({ id: 'common.message.success' }));
} catch (error) {
message.error(intl.formatMessage({ id: 'common.message.fail' }));
// ignore
}
};
const handleRecreate = useMemoizedFn((row: ListItem) => {
modalRef.current?.show({
title: intl.formatMessage({
id: 'gpuservice.instance.recreate.confirm.title'
}),
content: 'gpuservice.instance.recreate.confirm.content',
okText: 'common.button.recreate',
operation: 'gpuservice.instance.recreate.confirm.content',
name: row.metadata?.name,
onOk: async () => {
try {
await deleteInstance(row.metadata?.name as any);
await new Promise((resolve) => {
setTimeout(resolve, 500);
});
await createInstance({
data: {
metadata: { name: row.metadata.name, namespace },
spec: row.spec
} as FormData
});
message.success(intl.formatMessage({ id: 'common.message.success' }));
handleSearch();
} catch (error) {
message.error(intl.formatMessage({ id: 'common.message.fail' }));
}
}
});
});
const handleStart = useMemoizedFn(async (row: ListItem) => {
modalRef.current?.show({
title: intl.formatMessage({ id: 'common.title.start.confirm' }),
content: 'gpuservice.instance',
okText: 'common.button.start',
operation: 'common.start.single.confirm',
name: row.metadata?.name,
async onOk() {
try {
await startInstance(row);
message.success(intl.formatMessage({ id: 'common.message.success' }));
fetchData();
} catch (error) {
// ignore
}
}
});
});
const handleStop = useMemoizedFn(async (row: ListItem) => {
modalRef.current?.show({
title: intl.formatMessage({ id: 'common.title.stop.confirm' }),
content: 'gpuservice.instance',
okText: 'common.button.stop',
operation: 'common.stop.single.confirm',
name: row.metadata?.name,
async onOk() {
try {
await stopInstance(row);
message.success(intl.formatMessage({ id: 'common.message.success' }));
fetchData();
} catch (error) {
// ignore
}
}
});
});
const handleStartBatch = useMemoizedFn(() => {
modalRef.current?.show({
title: intl.formatMessage({ id: 'common.title.start.confirm' }),
content: 'gpuservice.instance',
okText: 'common.button.start',
operation: 'common.start.confirm',
selection: true,
async onOk() {
const res = await handleBatchRequest(
rowSelection.selectedRows as ListItem[],
startInstance
);
fetchData();
return res;
}
});
});
const handleStopBatch = useMemoizedFn(() => {
modalRef.current?.show({
title: intl.formatMessage({ id: 'common.title.stop.confirm' }),
content: 'gpuservice.instance',
okText: 'common.button.stop',
operation: 'common.stop.confirm',
selection: true,
async onOk() {
const res = await handleBatchRequest(
rowSelection.selectedRows as ListItem[],
stopInstance
);
fetchData();
return res;
}
});
});
const handleSelect = useMemoizedFn((val: string, row: ListItem) => {
if (val === 'view') {
openViewInstanceModal(row);
} else if (val === 'edit') {
openEditInstanceModal(row);
} else if (val === 'delete') {
handleDelete({
...row,
name: row.metadata?.name,
id: row.metadata?.name as any
});
handleDelete({ ...row, name: row.name });
} else if (val === 'recreate') {
// Keep handleRecreate above for the old confirm-modal flow. The current
// UX opens the editable form and recreates after submit.
openRecreateInstanceModal(row);
} else if (val === 'viewlog') {
openViewLogsModal(row);
} else if (val === 'viewevent') {
openViewEventsModal(row);
} else if (val === 'start') {
handleStart(row);
} else if (val === 'stop') {
handleStop(row);
}
// start / stop are disabled until backend endpoints exist
});
const handleBatchActionSelect = useMemoizedFn((val: string) => {
if (val === 'start') {
handleStartBatch();
} else if (val === 'stop') {
handleStopBatch();
} else if (val === 'delete') {
if (val === 'delete') {
handleDeleteBatch();
}
// start / stop are disabled until backend endpoints exist
});
const handleClusterChange = (value: number) => {
const cluster = k8sClusterList.find((item) => item.value === value);
if (cluster) {
setCurrentCluster(cluster);
}
handleQueryChange({
cluster_id: value,
page: 1
});
};
const renderEmpty = (type?: string) => {
if (type !== 'Table') return;
return (
@@ -389,7 +132,7 @@ const GPUService: React.FC = () => {
loadend={dataSource.loadend}
dataSource={dataSource.dataList}
image={<IconFont type="icon-cloud-outlined" />}
filters={_.pick(queryParams, ['search', 'manufacturer'])}
filters={_.pick(queryParams, ['search'])}
noFoundText={intl.formatMessage({
id: 'noresult.gpuservice.instance.nofound'
})}
@@ -411,80 +154,64 @@ const GPUService: React.FC = () => {
});
return (
<PageContainerInner
leftContent={
<Flex align="center">
<span>{intl.formatMessage({ id: 'menu.gpuService.instances' })}</span>
<Divider
orientation="vertical"
style={{
marginLeft: 16
<>
<PageBox>
<FilterBar
marginBottom={22}
marginTop={30}
showSelect={false}
handleSearch={handleSearch}
handleInputChange={handleNameChange}
rowSelection={rowSelection}
widths={{ input: 300 }}
right={
<Space size={16}>
<Button
icon={<PlusOutlined />}
type="primary"
onClick={openCreateInstanceModal}
>
{intl.formatMessage({ id: 'gpuservice.instance.add' })}
</Button>
<DropdownButtons
items={batchActionList}
extra={
rowSelection.selectedRowKeys.length > 0 && (
<span>({rowSelection.selectedRowKeys.length})</span>
)
}
size="large"
showText={true}
disabled={!rowSelection.selectedRowKeys.length}
onSelect={handleBatchActionSelect}
/>
</Space>
}
/>
<ConfigProvider renderEmpty={renderEmpty}>
<Table
columns={columns}
dataSource={dataSource.dataList}
rowSelection={rowSelection}
loading={{
spinning: dataSource.loading,
size: 'middle'
}}
sortDirections={TABLE_SORT_DIRECTIONS}
showSorterTooltip={false}
rowKey={(record) => record.id}
onChange={handleTableChange}
pagination={{
showSizeChanger: true,
pageSize: queryParams.perPage,
current: queryParams.page,
total: dataSource.total,
hideOnSinglePage: queryParams.perPage === 10,
onChange: handlePageChange
}}
/>
<BaseSelect
variant="borderless"
value={queryParams.cluster_id}
options={k8sClusterList}
onChange={handleClusterChange}
style={{ minWidth: 120, fontWeight: 500 }}
></BaseSelect>
</Flex>
}
>
<FilterBar
marginBottom={22}
marginTop={30}
showSelect={false}
selectOptions={k8sClusterList}
select={{ showSearch: true }}
selectHolder={intl.formatMessage({
id: 'gpuservice.instance.filter.cluster'
})}
handleSearch={handleSearch}
handleSelectChange={handleClusterChange}
handleInputChange={handleNameChange}
rowSelection={rowSelection}
widths={{ input: 300 }}
right={
<Space size={16}>
<Button
icon={<PlusOutlined />}
type="primary"
onClick={openCreateInstanceModal}
>
{intl.formatMessage({ id: 'gpuservice.instance.add' })}
</Button>
<DropdownButtons
items={batchActionList}
extra={
rowSelection.selectedRowKeys.length > 0 && (
<span>({rowSelection.selectedRowKeys.length})</span>
)
}
size="large"
showText={true}
disabled={!rowSelection.selectedRowKeys.length}
onSelect={handleBatchActionSelect}
/>
</Space>
}
/>
<ConfigProvider renderEmpty={renderEmpty}>
<Table
columns={columns}
dataSource={dataSource.dataList}
rowSelection={rowSelection}
loading={{
spinning: dataSource.loading,
size: 'middle'
}}
sortDirections={TABLE_SORT_DIRECTIONS}
showSorterTooltip={false}
rowKey={(record) => record.metadata.name}
onChange={handleTableChange}
pagination={false}
/>
</ConfigProvider>
</ConfigProvider>
</PageBox>
<AddModal
open={openInstanceModalStatus.open}
action={openInstanceModalStatus.action}
@@ -510,7 +237,7 @@ const GPUService: React.FC = () => {
/>
<DeleteModal ref={modalRef} />
{modalContextHolder}
</PageContainerInner>
</>
);
};