Files
gpustack-ui/src/pages/gpu-service/instances/index.tsx
T
gitlawrandjialin e7a376db70 refactor: adapt org-namespace lookup to principal name rename
Tracks the gpustack identity-consolidation effort: the unified
``principals`` table's URL-safe identifier column was renamed
``slug`` → ``name`` (with the legacy display ``name`` → ``display_name``).
The enterprise plugin's persisted org caches (``organizationList``,
``allOrganizations`` in localStorage) now write ``name`` instead of
``slug`` on each Organization row.

``getCurrentOrgNamespace`` reads those caches to compose the k8s
namespace (``gpustack-{name}``) for GPU-instance / storage CRDs.
Updated to read ``item.name`` so namespace resolution stays in sync
with what the enterprise plugin writes — otherwise every gpu-service
write would fall through to ``gpustack-default`` even when the user
has an Org context.

Stale ``slug`` references in surrounding comments also retitled to
``name`` to avoid divergence between code and prose. The namespace
format itself (``gpustack-{...}``) is unchanged — only the column it
sources is.
2026-05-21 12:15:39 +08:00

392 lines
11 KiB
TypeScript

import { currentClusterAtom } from '@/atoms/gpuservice';
import { getCurrentOrgNamespace } from '@/atoms/user';
import { PageAction } from '@/config';
import { PaginationKey, TABLE_SORT_DIRECTIONS } from '@/config/settings';
import type { PageActionType } from '@/config/types';
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 {
BaseSelect,
DeleteModal,
FilterBar,
IconFont,
NoResult
} from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { useMemoizedFn } from 'ahooks';
import { ConfigProvider, Divider, Flex, message, Modal, Table } from 'antd';
import { useAtom } from 'jotai';
import _ from 'lodash';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { PageContainerInner } from '../../_components/page-box';
import {
deleteGPUServiceInstance,
GPU_SERVICE_INSTANCES_API,
queryGPUServiceInstances
} from './apis';
import AddModal from './components/add-modal';
import ViewEventsModal from './components/view-events-modal';
import ViewLogsModal from './components/view-logs-modal';
import { FormData, ListItem } from './config/types';
import useInstancesColumns from './hooks/use-instances-columns';
import useViewEvents from './hooks/use-view-events';
import useViewLogs from './hooks/use-view-logs';
import useCreateInstance from './services/use-create-instance';
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 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 {
dataSource,
rowSelection,
queryParams,
sortOrder,
modalRef,
handleDelete,
handleDeleteBatch,
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 }),
contentForDelete: intl.formatMessage({ id: 'gpuservice.instance' })
});
const { fetchData: createInstance } = useCreateInstance();
const { fetchData: updateInstance } = useUpdateInstance();
const { openViewLogsModal, closeViewLogsModal, openViewLogsModalStatus } =
useViewLogs();
const {
openViewEventsModal,
closeViewEventsModal,
openViewEventsModalStatus
} = useViewEvents();
const {
fetchClusterList,
cancelRequest: cancelClusterRequest,
clusterList
} = useQueryClusterList();
const k8sClusterList = useMemo(
() =>
clusterList.filter(
(item) => item.provider === ProviderValueMap.Kubernetes
),
[clusterList]
);
const [openAddModalStatus, setOpenAddModalStatus] = useState<{
action: PageActionType;
open: boolean;
title: string;
currentData?: ListItem | null;
}>({
action: PageAction.CREATE,
title: '',
open: false,
currentData: null
});
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 handleAddInstance = () => {
setOpenAddModalStatus({
action: PageAction.CREATE,
title: intl.formatMessage({ id: 'gpuservice.instance.add' }),
open: true,
currentData: null
});
};
const handleEditInstance = (row: ListItem) => {
setOpenAddModalStatus({
action: PageAction.EDIT,
title: intl.formatMessage({ id: 'gpuservice.instance.edit' }),
open: true,
currentData: row
});
};
const closeModal = () => {
setOpenAddModalStatus({
action: PageAction.CREATE,
title: '',
open: false,
currentData: null
});
};
const handleModalOk = async (data: FormData) => {
try {
if (openAddModalStatus.action === PageAction.EDIT) {
await updateInstance({
id: openAddModalStatus.currentData!.id,
data
});
} else {
await createInstance({ data });
}
fetchData();
closeModal();
message.success(intl.formatMessage({ id: 'common.message.success' }));
} catch (error) {
message.error(intl.formatMessage({ id: 'common.message.fail' }));
}
};
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 handleSelect = useMemoizedFn((val: string, row: ListItem) => {
if (val === 'edit') {
handleEditInstance(row);
} else if (val === 'delete') {
handleDelete({
...row,
name: row.metadata?.name,
id: row.metadata?.name as any
});
} else if (val === 'recreate') {
handleRecreate(row);
} else if (val === 'viewlog') {
openViewLogsModal(row);
} else if (val === 'viewevent') {
openViewEventsModal(row);
}
});
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 (
<NoResult
loading={dataSource.loading}
loadend={dataSource.loadend}
dataSource={dataSource.dataList}
image={<IconFont type="icon-cloud-outlined" />}
filters={_.pick(queryParams, ['search', 'manufacturer'])}
noFoundText={intl.formatMessage({
id: 'noresult.gpuservice.instance.nofound'
})}
title={intl.formatMessage({
id: 'noresult.gpuservice.instance.title'
})}
subTitle={intl.formatMessage({
id: 'noresult.gpuservice.instance.subTitle'
})}
onClick={handleAddInstance}
buttonText={intl.formatMessage({ id: 'noresult.button.add' })}
/>
);
};
const columns = useInstancesColumns({
handleSelect,
sortOrder
});
return (
<PageContainerInner
leftContent={
<Flex align="center">
<span>{intl.formatMessage({ id: 'menu.gpuService.instances' })}</span>
<Divider
orientation="vertical"
style={{
marginLeft: 16
}}
/>
<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'
})}
buttonText={intl.formatMessage({ id: 'gpuservice.instance.add' })}
handleSearch={handleSearch}
handleSelectChange={handleClusterChange}
handleDeleteByBatch={handleDeleteBatch}
handleClickPrimary={handleAddInstance}
handleInputChange={handleNameChange}
rowSelection={rowSelection}
widths={{ input: 300 }}
/>
<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>
<AddModal
open={openAddModalStatus.open}
action={openAddModalStatus.action}
title={openAddModalStatus.title}
data={openAddModalStatus.currentData}
onCancel={closeModal}
onOk={handleModalOk}
/>
<ViewLogsModal
open={openViewLogsModalStatus.open}
url={openViewLogsModalStatus.url}
tail={openViewLogsModalStatus.tail}
onCancel={closeViewLogsModal}
/>
<ViewEventsModal
open={openViewEventsModalStatus.open}
name={openViewEventsModalStatus.name}
namespace={openViewEventsModalStatus.namespace}
clusterID={openViewEventsModalStatus.clusterID}
onCancel={closeViewEventsModal}
/>
<DeleteModal ref={modalRef} />
{modalContextHolder}
</PageContainerInner>
);
};
export default GPUService;