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
@@ -1,10 +1,13 @@
import { request } from '@umijs/max';
import { FormData, ListItem } from '../types';
import { FormData, ListItem } from '../config/types';
export const GPU_SERVICE_PUBLIC_KEY_API = '/gpu-instance-ssh-public-keys/data';
export const GPU_SERVICE_PUBLIC_KEY_API = '/gpu-instance-ssh-public-keys';
export async function queryGPUServicePublicKeys(params: {}, options?: any) {
return request<ListItem>(GPU_SERVICE_PUBLIC_KEY_API, {
export async function queryGPUServicePublicKeys(
params: Global.SearchParams,
options?: any
) {
return request<Global.PageResponse<ListItem>>(GPU_SERVICE_PUBLIC_KEY_API, {
method: 'GET',
params,
cancelToken: options?.token
@@ -18,8 +21,11 @@ export async function createGPUServicePublicKey(params: { data: FormData }) {
});
}
export async function updateGPUServicePublicKey(params: { data: FormData }) {
return request<ListItem>(`${GPU_SERVICE_PUBLIC_KEY_API}`, {
export async function updateGPUServicePublicKey(params: {
id: number;
data: Omit<FormData, 'name'>;
}) {
return request<ListItem>(`${GPU_SERVICE_PUBLIC_KEY_API}/${params.id}`, {
method: 'PUT',
data: params.data
});
@@ -0,0 +1,70 @@
import { PageActionType } from '@/config/types';
import { ModalFooter } from '@gpustack/core-ui';
import { useRef } from 'react';
import FormDrawer from '../../../_components/form-drawer';
import { FormData, ListItem } from '../config/types';
import GPUServicePublicKeyForm from '../forms';
type AddPublicKeyModalProps = {
title: string;
action: PageActionType;
open: boolean;
onOk: (values: FormData) => void;
data?: ListItem | null;
onCancel: () => void;
};
const AddPublicKeyModal: React.FC<AddPublicKeyModalProps> = ({
title,
action,
open,
onOk,
data,
onCancel
}) => {
const form = useRef<any>(null);
const handleSubmit = () => {
form.current?.submit();
};
const handleCancel = () => {
form.current?.resetFields();
onCancel();
};
const onFinish = async (values: FormData) => {
onOk({ ...values });
};
return (
<FormDrawer
title={title}
open={open}
onCancel={handleCancel}
onSubmit={handleSubmit}
width={600}
footer={
<ModalFooter
onOk={handleSubmit}
onCancel={handleCancel}
style={{
padding: '16px 24px 8px',
display: 'flex',
justifyContent: 'flex-end'
}}
/>
}
>
<GPUServicePublicKeyForm
ref={form}
action={action}
currentData={data}
onFinish={onFinish}
open={open}
/>
</FormDrawer>
);
};
export default AddPublicKeyModal;
@@ -0,0 +1,23 @@
export interface FormData {
name: string;
owner_principal_id?: number | null;
displayName?: string | null;
description?: string | null;
spec: {
data: string;
};
}
export interface ListItem {
id: number;
created_at: string;
updated_at: string;
deleted_at?: string | null;
owner_principal_id?: number | null;
name?: string;
displayName?: string | null;
description?: string | null;
spec: {
data: string;
};
}
@@ -0,0 +1,69 @@
import { PageAction, validateLabelNameRegxFor63 } from '@/config';
import { Input as CInput, Textarea, useAppUtils } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Form } from 'antd';
import OwnerPrincipalIdField from '../../../_components/owner-principal-id-field';
import { FormData } from '../config/types';
const Basic = ({ action }: { action: string }) => {
const intl = useIntl();
const { getRuleMessage } = useAppUtils();
return (
<>
<OwnerPrincipalIdField name="owner_principal_id" />
<Form.Item<FormData>
name="name"
rules={[
{
required: true,
message: getRuleMessage('input', 'common.table.name')
},
{
pattern: validateLabelNameRegxFor63,
message: intl.formatMessage({ id: 'gpuservice.form.rule.name' })
}
]}
>
<CInput.Input
disabled={action === PageAction.EDIT}
label={intl.formatMessage({ id: 'common.table.name' })}
required
/>
</Form.Item>
<Form.Item<FormData> name="displayName">
<CInput.Input
label={intl.formatMessage({ id: 'common.table.displayName' })}
/>
</Form.Item>
<Form.Item<FormData> name="description">
<Textarea
scaleSize={true}
label={intl.formatMessage({ id: 'common.table.description' })}
/>
</Form.Item>
<Form.Item<FormData>
name={['spec', 'data']}
rules={[
{
required: true,
message: getRuleMessage('input', 'gpuservice.publicKey.label')
}
]}
>
<Textarea
required
alwaysFocus
label={intl.formatMessage({ id: 'gpuservice.publicKey.label' })}
placeholder={intl.formatMessage({
id: 'gpuservice.publicKey.placeholder'
})}
trim={false}
autoSize={{ minRows: 6, maxRows: 12 }}
/>
</Form.Item>
</>
);
};
export default Basic;
@@ -0,0 +1,62 @@
import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import { Form } from 'antd';
import { forwardRef, useEffect, useImperativeHandle } from 'react';
import { FormData, ListItem } from '../config/types';
import Basic from './basic';
interface PublicKeyFormProps {
ref?: any;
open: boolean;
action: PageActionType;
currentData?: ListItem | null;
onFinish: (values: FormData) => Promise<void>;
}
const GPUServicePublicKeyForm: React.FC<PublicKeyFormProps> = forwardRef(
(props, ref) => {
const { action, currentData, open, onFinish } = props;
const [form] = Form.useForm<FormData>();
useEffect(() => {
if (!open) {
form.resetFields();
return;
}
if (action === PageAction.EDIT && currentData) {
form.setFieldsValue({
name: currentData.name as string,
displayName: currentData.displayName,
description: currentData.description,
spec: {
data: currentData.spec?.data ?? ''
}
});
}
}, [action, currentData, form, open]);
useImperativeHandle(ref, () => ({
submit: () => {
form.submit();
},
resetFields: () => {
form.resetFields();
}
}));
return (
<Form
name="gpuServicePublicKeyForm"
form={form}
onFinish={onFinish}
preserve={false}
initialValues={{}}
>
<Basic action={action} />
</Form>
);
}
);
export default GPUServicePublicKeyForm;
@@ -0,0 +1,52 @@
import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import useBodyScroll from '@/hooks/use-body-scroll';
import { useState } from 'react';
import { ListItem } from '../config/types';
const useCreatePublicKeyModal = () => {
const { saveScrollHeight, restoreScrollHeight } = useBodyScroll();
const [openModalStatus, setOpenModalStatus] = useState<{
open: boolean;
action: PageActionType;
currentData?: ListItem | null;
title: string;
}>({
open: false,
action: PageAction.CREATE,
currentData: null,
title: ''
});
const openModal = (
action: PageActionType,
title: string,
row?: ListItem | null
) => {
setOpenModalStatus({
open: true,
title,
action,
currentData: row ?? null
});
saveScrollHeight();
};
const closeModal = () => {
setOpenModalStatus({
open: false,
action: PageAction.CREATE,
currentData: null,
title: ''
});
restoreScrollHeight();
};
return {
openPublicKeyModalStatus: openModalStatus,
openPublicKeyModal: openModal,
closePublicKeyModal: closeModal
};
};
export default useCreatePublicKeyModal;
@@ -0,0 +1,95 @@
import { AutoTooltip, DropdownButtons, icons } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import type { ColumnsType } from 'antd/lib/table';
import dayjs from 'dayjs';
import { useMemo } from 'react';
import { ListItem } from '../config/types';
const rowActionList = [
{
label: 'common.button.edit',
key: 'edit',
locale: true,
icon: icons.EditOutlined
},
{
label: 'common.button.delete',
key: 'delete',
locale: true,
icon: icons.DeleteOutlined,
props: {
danger: true
}
}
];
interface ColumnsHookProps {
handleSelect: (val: string, record: ListItem) => void;
sortOrder: string[];
}
const usePublicKeyColumns = ({
handleSelect,
sortOrder
}: ColumnsHookProps): ColumnsType<ListItem> => {
const intl = useIntl();
return useMemo(() => {
return [
{
title: intl.formatMessage({ id: 'common.table.name' }),
dataIndex: 'name',
key: 'name',
sorter: false,
ellipsis: {
showTitle: false
},
render: (text: string, record: ListItem) => (
<AutoTooltip ghost style={{ maxWidth: 360 }}>
<span className="text-primary">{record.displayName || text}</span>
</AutoTooltip>
)
},
{
title: intl.formatMessage({ id: 'common.table.description' }),
dataIndex: 'description',
key: 'description',
sorter: false,
ellipsis: {
showTitle: false
},
render: (text: string) => (
<AutoTooltip ghost style={{ maxWidth: 360 }}>
{text || '-'}
</AutoTooltip>
)
},
{
title: intl.formatMessage({ id: 'common.table.createTime' }),
dataIndex: 'created_at',
key: 'created_at',
sorter: false,
ellipsis: {
showTitle: false
},
render: (text: string) => (
<AutoTooltip ghost>
{text ? dayjs(text).format('YYYY-MM-DD HH:mm:ss') : '-'}
</AutoTooltip>
)
},
{
title: intl.formatMessage({ id: 'common.table.operation' }),
key: 'operation',
dataIndex: 'operation',
render: (_text, record) => (
<DropdownButtons
items={rowActionList}
onSelect={(val) => handleSelect(val, record)}
/>
)
}
];
}, [handleSelect, sortOrder, intl]);
};
export default usePublicKeyColumns;
+154 -73
View File
@@ -1,94 +1,175 @@
import { Input as CInput, Textarea, useAppUtils } from '@gpustack/core-ui';
import { PageAction } from '@/config';
import { TABLE_SORT_DIRECTIONS } from '@/config/settings';
import useTableFetch from '@/hooks/use-table-fetch';
import { DeleteModal, FilterBar, IconFont, NoResult } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Button, Form, message } from 'antd';
import React, { useEffect } from 'react';
import { useMemoizedFn } from 'ahooks';
import { ConfigProvider, message, Table } from 'antd';
import _ from 'lodash';
import PageBox from '../../_components/page-box';
import useGetSshkey from './services/use-get-sshkey';
import {
deleteGPUServicePublicKey,
GPU_SERVICE_PUBLIC_KEY_API,
queryGPUServicePublicKeys
} from './apis';
import AddPublicKeyModal from './components/add-public-key-modal';
import { FormData, ListItem } from './config/types';
import useCreatePublicKeyModal from './hooks/use-create-public-key-modal';
import usePublicKeyColumns from './hooks/use-public-key-columns';
import useCreateSshkey from './services/use-create-sshkey';
import useUpdateSshkey from './services/use-update-sshkey';
import { FormData } from './types';
const GPUServicePublicKeys: React.FC = () => {
const intl = useIntl();
const [form] = Form.useForm<FormData>();
const { getRuleMessage } = useAppUtils();
const { fetchData: fetchSshkey } = useGetSshkey();
const { fetchData: updateSshkey, loading: updating } = useUpdateSshkey();
useEffect(() => {
fetchSshkey({}).then((res) => {
const data = res?.spec?.data || '';
form.setFieldsValue({
name: res?.name,
spec: {
data
}
});
});
}, []);
const {
dataSource,
rowSelection,
queryParams,
sortOrder,
modalRef,
handleDelete,
handleDeleteBatch,
fetchData,
handlePageChange,
handleTableChange,
handleSearch,
handleNameChange
} = useTableFetch<ListItem>({
key: 'PublicKeys',
fetchAPI: queryGPUServicePublicKeys,
deleteAPI: deleteGPUServicePublicKey,
watch: false,
polling: false,
API: GPU_SERVICE_PUBLIC_KEY_API,
contentForDelete: intl.formatMessage({ id: 'gpuservice.publicKey' })
});
const handleSave = async () => {
form.submit();
const { fetchData: createSshkey } = useCreateSshkey();
const { fetchData: updateSshkey } = useUpdateSshkey();
const { openPublicKeyModalStatus, openPublicKeyModal, closePublicKeyModal } =
useCreatePublicKeyModal();
const handleAdd = () => {
openPublicKeyModal(
PageAction.CREATE,
intl.formatMessage({ id: 'gpuservice.publicKey.add' })
);
};
const onFinish = async (values: FormData) => {
const handleEdit = (row: ListItem) => {
openPublicKeyModal(
PageAction.EDIT,
intl.formatMessage({ id: 'gpuservice.publicKey.edit' }),
row
);
};
const handleModalOk = async (data: FormData) => {
try {
await updateSshkey({
data: values
});
if (openPublicKeyModalStatus.action === PageAction.EDIT) {
await updateSshkey({
id: openPublicKeyModalStatus.currentData!.id,
data: data
});
} else {
await createSshkey({ data });
}
fetchData();
closePublicKeyModal();
message.success(intl.formatMessage({ id: 'common.message.success' }));
} catch (error) {
// ignore
// it's handled in interceptor
}
};
const handleSelect = useMemoizedFn((val: string, row: ListItem) => {
if (val === 'edit') {
handleEdit(row);
} else if (val === 'delete') {
handleDelete({ ...row, name: row.name as string });
}
});
const renderEmpty = (type?: string) => {
if (type !== 'Table') return;
return (
<NoResult
loading={dataSource.loading}
loadend={dataSource.loadend}
dataSource={dataSource.dataList}
image={<IconFont type="icon-storage-outlined" />}
filters={_.pick(queryParams, ['search'])}
noFoundText={intl.formatMessage({
id: 'noresult.gpuservice.sshkey.nofound'
})}
title={intl.formatMessage({ id: 'noresult.gpuservice.sshkey.title' })}
subTitle={intl.formatMessage({
id: 'noresult.gpuservice.sshkey.subTitle'
})}
onClick={handleAdd}
buttonText={intl.formatMessage({ id: 'noresult.button.add' })}
/>
);
};
const columns = usePublicKeyColumns({
handleSelect,
sortOrder
});
return (
<PageBox>
<Form
style={{ width: 700 }}
name="gpuServicePublicKeyForm"
form={form}
onFinish={onFinish}
initialValues={{
name: 'SSHPublicKey',
spec: {
data: ''
}
}}
>
<Form.Item<FormData> name="name" hidden>
<CInput.Input />
</Form.Item>
<Form.Item<FormData>
name={['spec', 'data']}
rules={[
{
required: true,
message: getRuleMessage('input', 'gpuservice.publicKey.label')
}
]}
>
<Textarea
required
label={intl.formatMessage({ id: 'gpuservice.publicKey.label' })}
placeholder={intl.formatMessage({
id: 'gpuservice.publicKey.placeholder'
})}
trim={false}
alwaysFocus
autoSize={{ minRows: 8, maxRows: 16 }}
style={{ width: 700, minHeight: 186 }}
<>
<PageBox>
<FilterBar
marginBottom={22}
marginTop={30}
showSelect={false}
inputHolder={intl.formatMessage({
id: 'gpuservice.publicKey.filter.name'
})}
buttonText={intl.formatMessage({ id: 'gpuservice.publicKey.add' })}
handleSearch={handleSearch}
handleDeleteByBatch={handleDeleteBatch}
handleClickPrimary={handleAdd}
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.id}
onChange={handleTableChange}
pagination={{
showSizeChanger: true,
pageSize: queryParams.perPage,
current: queryParams.page,
total: dataSource.total,
hideOnSinglePage: queryParams.perPage === 10,
onChange: handlePageChange
}}
/>
</Form.Item>
<Button
type="primary"
loading={updating}
style={{ width: 120, marginTop: 24 }}
onClick={handleSave}
>
{intl.formatMessage({ id: 'common.button.save' })}
</Button>
</Form>
</PageBox>
</ConfigProvider>
</PageBox>
<AddPublicKeyModal
open={openPublicKeyModalStatus.open}
action={openPublicKeyModalStatus.action}
title={openPublicKeyModalStatus.title}
data={openPublicKeyModalStatus.currentData}
onCancel={closePublicKeyModal}
onOk={handleModalOk}
/>
<DeleteModal ref={modalRef} />
</>
);
};
@@ -1,7 +1,7 @@
import { useQueryData } from '@gpustack/core-ui';
import { useCallback } from 'react';
import { createGPUServicePublicKey } from '../apis';
import { FormData, ListItem } from '../types';
import { FormData, ListItem } from '../config/types';
interface CreateSshkeyParams {
data: FormData;
@@ -1,24 +0,0 @@
import { useQueryData } from '@gpustack/core-ui';
import { useCallback } from 'react';
import { queryGPUServicePublicKeys } from '../apis';
import { ListItem } from '../types';
export default function useGetSshkey() {
const fetchDetail = useCallback(
(params: {}, options?: any) => queryGPUServicePublicKeys(params, options),
[]
);
const { detailData, loading, cancelRequest, fetchData } =
useQueryData<ListItem>({
fetchDetail,
key: 'sshkey'
});
return {
detailData,
loading,
cancelRequest,
fetchData
};
}
@@ -0,0 +1,27 @@
import { useQueryData } from '@gpustack/core-ui';
import { useCallback } from 'react';
import { queryGPUServicePublicKeys } from '../apis';
import { ListItem } from '../config/types';
export default function useQueryPublicKeys() {
const fetchDetail = useCallback(
(params: Global.SearchParams = { page: 1, perPage: 100 }, options?: any) =>
queryGPUServicePublicKeys(params, options),
[]
);
const { detailData, loading, cancelRequest, fetchData } = useQueryData<
Global.PageResponse<ListItem>,
Global.SearchParams
>({
fetchDetail,
key: 'publicKeys'
});
return {
detailData,
loading,
cancelRequest,
fetchData
};
}
@@ -0,0 +1,29 @@
import { useQueryDataList } from '@gpustack/core-ui';
import { useCallback } from 'react';
import { queryGPUServicePublicKeys } from '../apis';
import { ListItem } from '../config/types';
export default function useQuerySshkeys() {
const fetchList = useCallback(
(params: Global.SearchParams = { page: 1, perPage: 100 }, options?: any) =>
queryGPUServicePublicKeys(params, options),
[]
);
const { dataList, loading, cancelRequest, fetchData } = useQueryDataList<
ListItem,
Global.SearchParams
>({
key: 'sshkeyOptions',
fetchList,
getLabel: (item) => item?.name as string,
getValue: (item) => item?.name as string
});
return {
sshkeyOptions: dataList as Array<{ label: string; value: string }>,
loading,
cancelRequest,
fetchData
};
}
@@ -1,16 +1,17 @@
import { useQueryData } from '@gpustack/core-ui';
import { useCallback } from 'react';
import { updateGPUServicePublicKey } from '../apis';
import { FormData, ListItem } from '../types';
import { FormData, ListItem } from '../config/types';
interface UpdateSshkeyParams {
data: FormData;
id: number;
data: Omit<FormData, 'name'>;
}
export default function useUpdateSshkey() {
const fetchDetail = useCallback(
(params: UpdateSshkeyParams) =>
updateGPUServicePublicKey({ data: params.data }),
updateGPUServicePublicKey({ id: params.id, data: params.data }),
[]
);
@@ -1,13 +0,0 @@
export interface FormData {
name: string;
description: string;
spec: {
data: string;
};
}
export interface ListItem extends FormData {
id: number;
created_at?: string;
updated_at?: string;
}