feat: duplicate deployment

This commit is contained in:
jialin
2026-02-04 14:58:48 +08:00
parent 5ad9ac277c
commit 05c05419fc
25 changed files with 366 additions and 220 deletions
+14 -14
View File
@@ -86,14 +86,14 @@ export default [
redirect: '/models/deployments' redirect: '/models/deployments'
}, },
{ {
name: 'routes', name: 'modelCatalog',
path: '/models/routes', path: '/models/catalog',
key: 'routes', key: 'modelsCatalog',
icon: 'icon-captive_portal', icon: 'icon-layers',
selectedIcon: 'icon-captive_portal', selectedIcon: 'icon-layers-filled',
defaultIcon: 'icon-captive_portal', defaultIcon: 'icon-layers',
access: 'canSeeAdmin', access: 'canSeeAdmin',
component: './model-routes/index' component: './llmodels/catalog'
}, },
{ {
name: 'deployment', name: 'deployment',
@@ -106,14 +106,14 @@ export default [
component: './llmodels/index' component: './llmodels/index'
}, },
{ {
name: 'modelCatalog', name: 'routes',
path: '/models/catalog', path: '/models/routes',
key: 'modelsCatalog', key: 'routes',
icon: 'icon-layers', icon: 'icon-captive_portal',
selectedIcon: 'icon-layers-filled', selectedIcon: 'icon-captive_portal',
defaultIcon: 'icon-layers', defaultIcon: 'icon-captive_portal',
access: 'canSeeAdmin', access: 'canSeeAdmin',
component: './llmodels/catalog' component: './model-routes/index'
}, },
{ {
name: 'providers', name: 'providers',
+8 -2
View File
@@ -3,7 +3,7 @@ import { getAtomStorage } from '@/atoms/utils';
import VersionInfo, { modalConfig } from '@/components/version-info'; import VersionInfo, { modalConfig } from '@/components/version-info';
import externalLinks from '@/constants/external-links'; import externalLinks from '@/constants/external-links';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Button, Divider, Modal } from 'antd'; import { Button, Divider, Modal, Typography } from 'antd';
import { createStyles } from 'antd-style'; import { createStyles } from 'antd-style';
import styled from 'styled-components'; import styled from 'styled-components';
@@ -54,7 +54,13 @@ const Footer: React.FC = () => {
<CompanyWrapper> <CompanyWrapper>
<span>&copy;</span> <span>&copy;</span>
<span> {new Date().getFullYear()}</span> <span> {new Date().getFullYear()}</span>
<span> {intl.formatMessage({ id: 'settings.company' })}</span> <Typography.Link
href="https://gpustack.ai"
target="_blank"
rel="noopener noreferrer"
>
{intl.formatMessage({ id: 'settings.company' })}
</Typography.Link>
</CompanyWrapper> </CompanyWrapper>
<Divider orientation="vertical" /> <Divider orientation="vertical" />
<Button <Button
+1
View File
@@ -8,6 +8,7 @@ import { useEffect, useRef, useState } from 'react';
* generic hook to query data list * generic hook to query data list
* @template ListItem * @template ListItem
* @param Params - query params type, e.g. { page: number; perPage: number }
* @param option.fetchList: (params, extra) => Promise<{ items: ListItem[] }> * @param option.fetchList: (params, extra) => Promise<{ items: ListItem[] }>
* @returns loading, dataList, fetchData, cancelRequest * @returns loading, dataList, fetchData, cancelRequest
*/ */
+1 -1
View File
@@ -205,7 +205,7 @@ export default {
'common.form.password': 'Password', 'common.form.password': 'Password',
'common.form.username': 'Username', 'common.form.username': 'Username',
'common.login.rember': 'Remember me', 'common.login.rember': 'Remember me',
'settings.company': 'SEAL', 'settings.company': 'GPUStack.ai',
'common.button.help': 'Help', 'common.button.help': 'Help',
'common.button.feedback': 'Feedback', 'common.button.feedback': 'Feedback',
'common.button.docs': 'Documentation', 'common.button.docs': 'Documentation',
+1
View File
@@ -2,6 +2,7 @@ export default {
'models.button.deploy': 'Deploy Model', 'models.button.deploy': 'Deploy Model',
'models.title': 'Models', 'models.title': 'Models',
'models.title.edit': 'Edit Model', 'models.title.edit': 'Edit Model',
'models.title.duplicate': 'Duplicate Model',
'models.table.models': 'models', 'models.table.models': 'models',
'models.table.name': 'Model Name', 'models.table.name': 'Model Name',
'models.form.source': 'Source', 'models.form.source': 'Source',
+1 -1
View File
@@ -205,7 +205,7 @@ export default {
'common.form.password': 'パスワード', 'common.form.password': 'パスワード',
'common.form.username': 'ユーザー名', 'common.form.username': 'ユーザー名',
'common.login.rember': 'ログイン状態を保持', 'common.login.rember': 'ログイン状態を保持',
'settings.company': 'SEAL', 'settings.company': 'GPUStack.ai',
'common.button.help': 'ヘルプ', 'common.button.help': 'ヘルプ',
'common.button.feedback': 'フィードバック', 'common.button.feedback': 'フィードバック',
'common.button.docs': 'ドキュメント', 'common.button.docs': 'ドキュメント',
+1
View File
@@ -2,6 +2,7 @@ export default {
'models.button.deploy': 'モデルをデプロイ', 'models.button.deploy': 'モデルをデプロイ',
'models.title': 'モデル', 'models.title': 'モデル',
'models.title.edit': 'モデルを編集', 'models.title.edit': 'モデルを編集',
'models.title.duplicate': 'モデルを複製',
'models.table.models': 'モデル', 'models.table.models': 'モデル',
'models.table.name': 'モデル名', 'models.table.name': 'モデル名',
'models.form.source': 'ソース', 'models.form.source': 'ソース',
+1 -1
View File
@@ -203,7 +203,7 @@ export default {
'common.form.password': 'Пароль', 'common.form.password': 'Пароль',
'common.form.username': 'Имя пользователя', 'common.form.username': 'Имя пользователя',
'common.login.rember': 'Запомнить меня', 'common.login.rember': 'Запомнить меня',
'settings.company': 'SEAL', 'settings.company': 'GPUStack.ai',
'common.button.help': 'Помощь', 'common.button.help': 'Помощь',
'common.button.feedback': 'Обратная связь', 'common.button.feedback': 'Обратная связь',
'common.button.docs': 'Документация', 'common.button.docs': 'Документация',
+1
View File
@@ -2,6 +2,7 @@ export default {
'models.button.deploy': 'Развернуть модель', 'models.button.deploy': 'Развернуть модель',
'models.title': 'Модели', 'models.title': 'Модели',
'models.title.edit': 'Редактировать модель', 'models.title.edit': 'Редактировать модель',
'models.title.duplicate': 'Дублировать модель',
'models.table.models': 'Модели', 'models.table.models': 'Модели',
'models.table.name': 'Название модели', 'models.table.name': 'Название модели',
'models.form.source': 'Источник', 'models.form.source': 'Источник',
+1
View File
@@ -2,6 +2,7 @@ export default {
'models.button.deploy': '部署模型', 'models.button.deploy': '部署模型',
'models.title': '模型', 'models.title': '模型',
'models.title.edit': '编辑模型', 'models.title.edit': '编辑模型',
'models.title.duplicate': '复制模型',
'models.table.models': '模型', 'models.table.models': '模型',
'models.table.name': '模型名称', 'models.table.name': '模型名称',
'models.form.source': '来源', 'models.form.source': '来源',
@@ -27,7 +27,12 @@ import _ from 'lodash';
import React, { useCallback, useEffect, useMemo } from 'react'; import React, { useCallback, useEffect, useMemo } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { MODEL_INSTANCE_API } from '../apis'; import { MODEL_INSTANCE_API } from '../apis';
import { InstanceStatusMap, InstanceStatusMapValue, status } from '../config'; import {
InstanceStatusMap,
InstanceStatusMapValue,
modelCategoriesMap,
status
} from '../config';
import { backendOptionsMap } from '../config/backend-parameters'; import { backendOptionsMap } from '../config/backend-parameters';
import { generateSource } from '../config/button-actions'; import { generateSource } from '../config/button-actions';
import { import {
@@ -431,12 +436,18 @@ const InstanceItem: React.FC<InstanceItemProps> = ({
const intl = useIntl(); const intl = useIntl();
const actionItems = useMemo(() => { const actionItems = useMemo(() => {
return _.filter(childActionList, (action: any) => { return _.filter(childActionList, (action: any) => {
if (action.key === 'benchmark') {
return (
action.status.includes(instanceData.state) &&
modelData?.categories?.includes(modelCategoriesMap.llm)
);
}
if (action.status && action.status.length > 0) { if (action.status && action.status.length > 0) {
return action.status.includes(instanceData.state); return action.status.includes(instanceData.state);
} }
return true; return true;
}); });
}, [instanceData.state]); }, [instanceData.state, modelData]);
const createFileName = (name: string) => { const createFileName = (name: string) => {
const timestamp = dayjs().format('YYYY-MM-DD_HH-mm-ss'); const timestamp = dayjs().format('YYYY-MM-DD_HH-mm-ss');
+84 -116
View File
@@ -15,6 +15,9 @@ import useExpandedRowKeys from '@/hooks/use-expanded-row-keys';
import useTableRowSelection from '@/hooks/use-table-row-selection'; import useTableRowSelection from '@/hooks/use-table-row-selection';
import PageBox from '@/pages/_components/page-box'; import PageBox from '@/pages/_components/page-box';
import useNoResourceResult from '@/pages/llmodels/hooks/use-no-resource-result'; import useNoResourceResult from '@/pages/llmodels/hooks/use-no-resource-result';
import { TargetStatusValueMap } from '@/pages/model-routes/config';
import useOpenPlayground from '@/pages/model-routes/hooks/use-open-playground';
import useQueryTargets from '@/pages/model-routes/services/use-query-targets';
import { handleBatchRequest } from '@/utils'; import { handleBatchRequest } from '@/utils';
import { DownOutlined, SearchOutlined, SyncOutlined } from '@ant-design/icons'; import { DownOutlined, SearchOutlined, SyncOutlined } from '@ant-design/icons';
import { useIntl, useNavigate, useSearchParams } from '@umijs/max'; import { useIntl, useNavigate, useSearchParams } from '@umijs/max';
@@ -54,11 +57,10 @@ import {
ModelInstanceListItem, ModelInstanceListItem,
SourceType SourceType
} from '../config/types'; } from '../config/types';
import useEditDeployment from '../hooks/use-edit-deployment';
import useFilterStatus from '../hooks/use-filter-status'; import useFilterStatus from '../hooks/use-filter-status';
import useFormInitialValues from '../hooks/use-form-initial-values'; import useFormInitialValues from '../hooks/use-form-initial-values';
import useModelsColumns from '../hooks/use-models-columns'; import useModelsColumns from '../hooks/use-models-columns';
import AccessControlModal from './access-control-modal';
import APIAccessInfoModal from './api-access-info';
import DeployModal from './deploy-modal'; import DeployModal from './deploy-modal';
import Instances from './instances'; import Instances from './instances';
import UpdateModelModal from './update-modal'; import UpdateModelModal from './update-modal';
@@ -140,13 +142,12 @@ const Models: React.FC<ModelsProps> = ({
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const page = searchParams.get('page'); const page = searchParams.get('page');
const { saveScrollHeight, restoreScrollHeight } = useBodyScroll(); const { saveScrollHeight, restoreScrollHeight } = useBodyScroll();
const [updateFormInitials, setUpdateFormInitials] = useState<{ const {
data: any; openEditModalStatus,
isGGUF: boolean; openEditModal,
}>({ openDuplicateModal,
data: {}, closeEditModal
isGGUF: false } = useEditDeployment();
});
const [expandAtom, setExpandAtom] = useAtom(modelsExpandKeysAtom); const [expandAtom, setExpandAtom] = useAtom(modelsExpandKeysAtom);
const [modelsSession, setModelsSession] = useAtom(modelsSessionAtom); const [modelsSession, setModelsSession] = useAtom(modelsSessionAtom);
const intl = useIntl(); const intl = useIntl();
@@ -159,17 +160,18 @@ const Models: React.FC<ModelsProps> = ({
removeExpandedRowKey, removeExpandedRowKey,
expandedRowKeys expandedRowKeys
} = useExpandedRowKeys(expandAtom); } = useExpandedRowKeys(expandAtom);
const { handleOpenPlayGround } = useOpenPlayground();
const { labelRender, optionRender, handleStatusChange, statusOptions } = const { labelRender, optionRender, handleStatusChange, statusOptions } =
useFilterStatus({ useFilterStatus({
onStatusChange: onStatusChange onStatusChange: onStatusChange
}); });
const { fetchData: fetchTargets, dataList: targetList } = useQueryTargets();
useEffect(() => {
fetchTargets({});
}, []);
const [apiAccessInfo, setAPIAccessInfo] = useState<any>({
show: false,
data: {}
});
const [openLogModal, setOpenLogModal] = useState(false); const [openLogModal, setOpenLogModal] = useState(false);
const [openAddModal, setOpenAddModal] = useState(false);
const [openDeployModal, setOpenDeployModal] = useState<{ const [openDeployModal, setOpenDeployModal] = useState<{
show: boolean; show: boolean;
width: number | string; width: number | string;
@@ -183,7 +185,6 @@ const Models: React.FC<ModelsProps> = ({
isGGUF: false, isGGUF: false,
source: modelSourceMap.huggingface_value as SourceType source: modelSourceMap.huggingface_value as SourceType
}); });
const currentData = useRef<ListItem>({} as ListItem);
const [currentInstance, setCurrentInstance] = useState<{ const [currentInstance, setCurrentInstance] = useState<{
url: string; url: string;
status: string; status: string;
@@ -194,17 +195,6 @@ const Models: React.FC<ModelsProps> = ({
url: '', url: '',
status: '' status: ''
}); });
const [openAccessControlModal, setOpenAccessControlModal] = useState<{
open: boolean;
currentData: ListItem | null;
title: string;
action: PageActionType;
}>({
open: false,
currentData: null,
title: '',
action: PageAction.CREATE
});
const modalRef = useRef<any>(null); const modalRef = useRef<any>(null);
useEffect(() => { useEffect(() => {
@@ -223,10 +213,6 @@ const Models: React.FC<ModelsProps> = ({
}; };
}, []); }, []);
const setCurrentData = (data: ListItem) => {
currentData.current = data;
};
const handleOnSort = (order: TableOrder | Array<TableOrder>) => { const handleOnSort = (order: TableOrder | Array<TableOrder>) => {
onTableSort?.(order); onTableSort?.(order);
}; };
@@ -253,16 +239,29 @@ const Models: React.FC<ModelsProps> = ({
}; };
const handleModalOk = async (data: FormData) => { const handleModalOk = async (data: FormData) => {
const currentData = openEditModalStatus.currentData;
try { try {
await updateModel({ if (currentData.realAction === PageAction.COPY) {
data, const modelData = await createModel({
id: currentData.current?.id as number data
}); });
setOpenAddModal(false);
message.success(intl.formatMessage({ id: 'common.message.success' })); if (data.replicas > 0) {
if (data.replicas > currentData.current?.replicas) { updateExpandedRowKeys([modelData.id, ...expandedRowKeys]);
updateExpandedRowKeys([currentData.current?.id, ...expandedRowKeys]); }
} }
if (currentData.realAction === PageAction.EDIT) {
await updateModel({
data,
id: currentData.row.id as number
});
if (data.replicas > currentData.row.replicas) {
updateExpandedRowKeys([currentData.row.id, ...expandedRowKeys]);
}
}
closeEditModal();
message.success(intl.formatMessage({ id: 'common.message.success' }));
setTimeout(() => { setTimeout(() => {
handleSearch(); handleSearch();
}, 150); }, 150);
@@ -271,7 +270,7 @@ const Models: React.FC<ModelsProps> = ({
}; };
const handleModalCancel = useCallback(() => { const handleModalCancel = useCallback(() => {
setOpenAddModal(false); closeEditModal();
restoreScrollHeight(); restoreScrollHeight();
}, []); }, []);
@@ -282,29 +281,28 @@ const Models: React.FC<ModelsProps> = ({
}); });
}; };
const handleCreateModel = useCallback( const refreshListStatus = (modelData: ListItem) => {
async (data: FormData) => { setTimeout(() => {
try { updateExpandedRowKeys([modelData.id, ...expandedRowKeys]);
console.log('data:', data, openDeployModal); }, 300);
message.success(intl.formatMessage({ id: 'common.message.success' }));
setTimeout(() => {
handleSearch?.();
}, 150);
};
const modelData = await createModel({ const handleCreateModel = async (data: FormData) => {
data try {
}); const modelData = await createModel({
setOpenDeployModal({ data
...openDeployModal, });
show: false setOpenDeployModal({
}); ...openDeployModal,
setTimeout(() => { show: false
updateExpandedRowKeys([modelData.id, ...expandedRowKeys]); });
}, 300); refreshListStatus(modelData);
message.success(intl.formatMessage({ id: 'common.message.success' })); } catch (error) {}
setTimeout(() => { };
handleSearch?.();
}, 150);
} catch (error) {}
},
[openDeployModal]
);
const handleLogModalCancel = useCallback(() => { const handleLogModalCancel = useCallback(() => {
setOpenLogModal(false); setOpenLogModal(false);
@@ -400,28 +398,23 @@ const Models: React.FC<ModelsProps> = ({
return `${MODELS_API}/${params.id}/instances`; return `${MODELS_API}/${params.id}/instances`;
}, []); }, []);
const handleEdit = async (row: ListItem) => { const handleEdit = async (row: ListItem, realAction: PageActionType) => {
const initialValues = generateFormValues(row, []); const initialValues = generateFormValues(row, []);
setUpdateFormInitials({ if (realAction === PageAction.EDIT) {
data: initialValues, openEditModal(initialValues, row);
isGGUF: false } else if (realAction === PageAction.COPY) {
}); openDuplicateModal(initialValues, row);
setCurrentData(row); }
setOpenAddModal(true);
saveScrollHeight(); saveScrollHeight();
}; };
const handleViewAPIInfo = useCallback((row: ListItem) => {
setAPIAccessInfo({
show: true,
data: row
});
}, []);
const handleSelect = useMemoizedFn(async (val: any, row: ListItem) => { const handleSelect = useMemoizedFn(async (val: any, row: ListItem) => {
try { try {
if (val === 'edit') { if (val === 'edit') {
handleEdit(row); handleEdit(row, PageAction.EDIT);
}
if (val === 'copy') {
handleEdit(row, PageAction.COPY);
} }
if (val === 'delete') { if (val === 'delete') {
handleDelete(row); handleDelete(row);
@@ -433,10 +426,6 @@ const Models: React.FC<ModelsProps> = ({
onStart?.(); onStart?.();
} }
if (val === 'api') {
handleViewAPIInfo(row);
}
if (val === 'stop') { if (val === 'stop') {
modalRef.current?.show({ modalRef.current?.show({
content: 'models.instances', content: 'models.instances',
@@ -450,13 +439,16 @@ const Models: React.FC<ModelsProps> = ({
} }
}); });
} }
if (val === 'chat') {
const targetRoute = targetList.find(
(target) =>
target.model_id === row.id &&
target.state === TargetStatusValueMap.Active
);
if (val === 'accessControl') { handleOpenPlayGround({
setOpenAccessControlModal({ categories: row.categories || [],
title: intl.formatMessage({ id: 'models.button.accessSettings' }), name: targetRoute?.route_name || ''
action: PageAction.EDIT,
currentData: row,
open: true
}); });
} }
} catch (error) { } catch (error) {
@@ -551,7 +543,8 @@ const Models: React.FC<ModelsProps> = ({
return { return {
handleSelect, handleSelect,
clusterList, clusterList,
sortOrder sortOrder,
targetList: targetList
}; };
}, [handleSelect, clusterList, sortOrder]); }, [handleSelect, clusterList, sortOrder]);
@@ -565,14 +558,6 @@ const Models: React.FC<ModelsProps> = ({
} }
}); });
const handleCancelAccessControl = () => {
setOpenAccessControlModal({
...openAccessControlModal,
currentData: null,
open: false
});
};
const { noResourceResult } = useNoResourceResult({ const { noResourceResult } = useNoResourceResult({
loadend: loadend, loadend: loadend,
loading: loading, loading: loading,
@@ -735,10 +720,10 @@ const Models: React.FC<ModelsProps> = ({
></SealTable> ></SealTable>
</PageBox> </PageBox>
<UpdateModelModal <UpdateModelModal
open={openAddModal} open={openEditModalStatus.open}
action={PageAction.EDIT} action={openEditModalStatus.action}
title={intl.formatMessage({ id: 'models.title.edit' })} title={openEditModalStatus.title}
updateFormInitials={updateFormInitials} currentData={openEditModalStatus.currentData}
clusterList={clusterList} clusterList={clusterList}
onCancel={handleModalCancel} onCancel={handleModalCancel}
onOk={handleModalOk} onOk={handleModalOk}
@@ -765,23 +750,6 @@ const Models: React.FC<ModelsProps> = ({
onCancel={handleLogModalCancel} onCancel={handleLogModalCancel}
></ViewLogsModal> ></ViewLogsModal>
<DeleteModal ref={modalRef}></DeleteModal> <DeleteModal ref={modalRef}></DeleteModal>
<APIAccessInfoModal
open={apiAccessInfo.show}
data={apiAccessInfo.data}
onClose={() => {
setAPIAccessInfo({
...apiAccessInfo,
show: false
});
}}
></APIAccessInfoModal>
<AccessControlModal
onCancel={handleCancelAccessControl}
title={openAccessControlModal.title}
open={openAccessControlModal.open}
currentData={openAccessControlModal.currentData}
action={openAccessControlModal.action}
></AccessControlModal>
</> </>
); );
}; };
+18 -33
View File
@@ -1,10 +1,10 @@
import ModalFooter from '@/components/modal-footer'; import ModalFooter from '@/components/modal-footer';
import GSDrawer from '@/components/scroller-modal/gs-drawer'; import GSDrawer from '@/components/scroller-modal/gs-drawer';
import { PageAction } from '@/config';
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Button } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
import React, { useEffect, useMemo, useRef } from 'react'; import React, { useEffect, useRef } from 'react';
import ColumnWrapper from '../../_components/column-wrapper'; import ColumnWrapper from '../../_components/column-wrapper';
import { import {
DeployFormKeyMap, DeployFormKeyMap,
@@ -22,9 +22,10 @@ type AddModalProps = {
title: string; title: string;
action: PageActionType; action: PageActionType;
open: boolean; open: boolean;
updateFormInitials: { currentData: {
data: FormData; data: FormData;
isGGUF: boolean; isGGUF: boolean;
realAction?: PageActionType;
}; };
clusterList: Global.BaseOption< clusterList: Global.BaseOption<
number, number,
@@ -48,7 +49,7 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
onOk, onOk,
onCancel, onCancel,
clusterList, clusterList,
updateFormInitials: { isGGUF, data: formData } currentData: { isGGUF, data: formData, realAction }
} = props || {}; } = props || {};
const intl = useIntl(); const intl = useIntl();
const { const {
@@ -198,14 +199,6 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
onCancel?.(); onCancel?.();
}; };
const showExtraButton = useMemo(() => {
return (
warningStatus.show &&
warningStatus.type !== 'success' &&
!warningStatus.isDefault
);
}, [warningStatus.show, warningStatus.type, warningStatus.isDefault]);
useEffect(() => { useEffect(() => {
const initGPUSelector = async () => { const initGPUSelector = async () => {
const gpuOptions = await formRef.current?.getGPUOptionList({ const gpuOptions = await formRef.current?.getGPUOptionList({
@@ -256,31 +249,23 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
}} }}
footer={ footer={
<> <>
<CompatibilityAlert {realAction === PageAction.EDIT && (
showClose={false} <CompatibilityAlert
onClose={() => { showClose={false}
setWarningStatus({ onClose={() => {
show: false, setWarningStatus({
message: '' show: false,
}); message: ''
}} });
warningStatus={warningStatus} }}
contentStyle={{ paddingInline: 0 }} warningStatus={warningStatus}
></CompatibilityAlert> contentStyle={{ paddingInline: 0 }}
></CompatibilityAlert>
)}
<ModalFooter <ModalFooter
style={ModalFooterStyle} style={ModalFooterStyle}
onCancel={onCancel} onCancel={onCancel}
onOk={handleSumit} onOk={handleSumit}
showOkBtn={!showExtraButton}
extra={
showExtraButton && (
<Button type="primary" onClick={handleSubmitAnyway}>
{intl.formatMessage({
id: 'models.form.submit.anyway'
})}
</Button>
)
}
></ModalFooter> ></ModalFooter>
</> </>
} }
+10 -5
View File
@@ -39,16 +39,21 @@ export const ActionList: ActionItem[] = [
key: 'edit', key: 'edit',
icon: icons.EditOutlined icon: icons.EditOutlined
}, },
{
label: 'models.openinplayground',
key: 'chat',
icon: icons.ExperimentOutlined
},
{
key: 'copy',
label: 'common.button.duplicate',
icon: icons.CopyOutlined
},
{ {
label: 'common.button.start', label: 'common.button.start',
key: 'start', key: 'start',
icon: icons.Play icon: icons.Play
}, },
{
label: 'models.table.button.apiAccessInfo',
key: 'api',
icon: icons.ApiOutlined
},
{ {
label: 'common.button.stop', label: 'common.button.stop',
key: 'stop', key: 'stop',
@@ -0,0 +1,82 @@
import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import { useIntl } from '@umijs/max';
import { useState } from 'react';
import { ListItem } from '../config/types';
const useEditDeployment = () => {
const intl = useIntl();
const [openModalStatus, setOpenModalStatus] = useState<{
open: boolean;
action: PageActionType;
currentData: {
isGGUF: boolean;
data: any; // for form data
row: ListItem;
realAction?: PageActionType; // added action here for copy action
};
title: string;
}>({
open: false,
action: PageAction.EDIT,
currentData: {
isGGUF: false,
data: {},
row: {} as ListItem
},
title: ''
});
const openEditModal = (formData: any, row: ListItem) => {
setOpenModalStatus({
open: true,
action: PageAction.EDIT,
currentData: {
isGGUF: false,
data: formData,
row: row
},
title: intl.formatMessage({ id: 'models.title.edit' })
});
};
const openDuplicateModal = (formData: any, row: ListItem) => {
setOpenModalStatus({
open: true,
action: PageAction.EDIT,
currentData: {
isGGUF: false,
data: {
...formData,
name: `${formData.name}-copy`
},
row: row,
realAction: PageAction.COPY
},
title: intl.formatMessage({ id: 'models.title.duplicate' })
});
};
const closeModal = () => {
setOpenModalStatus({
open: false,
action: PageAction.EDIT,
currentData: {
isGGUF: false,
data: {},
row: {} as ListItem
},
title: ''
});
};
return {
openEditModalStatus: openModalStatus,
setOpenEditModalStatus: setOpenModalStatus,
openEditModal: openEditModal,
openDuplicateModal: openDuplicateModal,
closeEditModal: closeModal
};
};
export default useEditDeployment;
+31 -21
View File
@@ -3,8 +3,10 @@ import AutoTooltip from '@/components/auto-tooltip';
import DropdownButtons from '@/components/drop-down-buttons'; import DropdownButtons from '@/components/drop-down-buttons';
import { SealColumnProps } from '@/components/seal-table/types'; import { SealColumnProps } from '@/components/seal-table/types';
import { OPENAI_COMPATIBLE, tableSorter } from '@/config/settings'; import { OPENAI_COMPATIBLE, tableSorter } from '@/config/settings';
import { TargetStatusValueMap } from '@/pages/model-routes/config';
import { QuestionCircleOutlined } from '@ant-design/icons'; import { QuestionCircleOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { useMemoizedFn } from 'ahooks';
import { Tooltip } from 'antd'; import { Tooltip } from 'antd';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import _ from 'lodash'; import _ from 'lodash';
@@ -13,24 +15,6 @@ import ModelTag from '../../_components/model-tag';
import { ActionList, generateSource } from '../config/button-actions'; import { ActionList, generateSource } from '../config/button-actions';
import { ListItem } from '../config/types'; import { ListItem } from '../config/types';
const setModelActionList = (record: any) => {
return _.filter(ActionList, (action: any) => {
if (action.key === 'chat' || action.key === 'api') {
return record.ready_replicas > 0;
}
if (action.key === 'start') {
return record.replicas === 0;
}
if (action.key === 'stop') {
return record.replicas > 0;
}
return true;
});
};
interface ModelsColumnsHookProps { interface ModelsColumnsHookProps {
handleSelect: (val: string, record: ListItem) => void; handleSelect: (val: string, record: ListItem) => void;
sortOrder: string[]; sortOrder: string[];
@@ -43,10 +27,36 @@ interface ModelsColumnsHookProps {
const useModelsColumns = ({ const useModelsColumns = ({
handleSelect, handleSelect,
clusterList, clusterList,
sortOrder sortOrder,
}: ModelsColumnsHookProps): SealColumnProps[] => { targetList
}: ModelsColumnsHookProps & { targetList: any[] }): SealColumnProps[] => {
const intl = useIntl(); const intl = useIntl();
const setModelActionList = useMemoizedFn((record: any) => {
return _.filter(ActionList, (action: any) => {
if (action.key === 'chat') {
return (
record.ready_replicas > 0 &&
targetList?.find(
(target) =>
target.model_id === record.id &&
target.state === TargetStatusValueMap.Active
)
);
}
if (action.key === 'start') {
return record.replicas === 0;
}
if (action.key === 'stop') {
return record.replicas > 0;
}
return true;
});
});
return useMemo(() => { return useMemo(() => {
return [ return [
{ {
@@ -137,7 +147,7 @@ const useModelsColumns = ({
key: 'operation', key: 'operation',
dataIndex: 'operation', dataIndex: 'operation',
span: 3, span: 3,
render: (text, record) => ( render: (text: any, record: ListItem) => (
<DropdownButtons <DropdownButtons
items={setModelActionList(record)} items={setModelActionList(record)}
onSelect={(val) => handleSelect(val, record)} onSelect={(val) => handleSelect(val, record)}
+12 -11
View File
@@ -36,17 +36,18 @@ export async function deleteModelRoute(id: number) {
}); });
} }
export async function queryRouteTargets(params: { id: number }, options?: any) { export async function queryRouteTargets(
return request<Global.PageResponse<RouteTarget>>( params: { id?: number },
`${MODEL_ROUTE_TARGETS}?route_id=${params.id}`, options?: any
{ ) {
method: 'GET', return request<Global.PageResponse<RouteTarget>>(`${MODEL_ROUTE_TARGETS}`, {
params: { method: 'GET',
page: -1 params: {
}, route_id: params.id,
cancelToken: options?.token page: -1
} },
); cancelToken: options?.token
});
} }
export async function deleteModelRouteTarget(id: number) { export async function deleteModelRouteTarget(id: number) {
+5
View File
@@ -29,6 +29,11 @@ export const rowActionList = [
key: 'chat', key: 'chat',
icon: icons.ExperimentOutlined icon: icons.ExperimentOutlined
}, },
{
label: 'models.table.button.apiAccessInfo',
key: 'api',
icon: icons.ApiOutlined
},
{ {
label: 'models.button.accessSettings', label: 'models.button.accessSettings',
key: 'accessControl', key: 'accessControl',
+2 -5
View File
@@ -38,13 +38,10 @@ export interface RouteTarget {
id: number; id: number;
created_at: string; created_at: string;
updated_at: string; updated_at: string;
deleted_at: string; weight: number;
provider_model_name: string;
weight: number | null;
model_id: number; model_id: number;
provider_id: number;
name: string; name: string;
route_name: string;
route_id: number; route_id: number;
state: string; state: string;
fallback_status_codes: string[];
} }
+11 -2
View File
@@ -13,10 +13,19 @@ const Basic = () => {
const { getRuleMessage } = useAppUtils(); const { getRuleMessage } = useAppUtils();
return ( return (
<> <>
<Form.Item name="name" data-field="name"> <Form.Item
name="name"
data-field="name"
rules={[
{
required: true,
message: getRuleMessage('input', 'common.table.name')
}
]}
>
<SealInput.Input <SealInput.Input
required required
label={intl.formatMessage({ id: 'models.table.name' })} label={intl.formatMessage({ id: 'common.table.name' })}
/> />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
@@ -18,7 +18,7 @@ const useAccessColumns = (
const filterActions = (record: RouteItem) => { const filterActions = (record: RouteItem) => {
return rowActionList.filter((action) => { return rowActionList.filter((action) => {
if (action.key === 'chat') { if (action.key === 'chat' || action.key === 'api') {
return record.ready_targets > 0; return record.ready_targets > 0;
} }
return true; return true;
@@ -0,0 +1,30 @@
import { useState } from 'react';
const useViewApIInfo = () => {
const [apiAccessInfo, setAPIAccessInfo] = useState<any>({
show: false,
data: {}
});
const openViewAPIInfo = (row: any) => {
setAPIAccessInfo({
show: true,
data: row
});
};
const closeViewAPIInfo = () => {
setAPIAccessInfo({
show: false,
data: {}
});
};
return {
apiAccessInfo,
openViewAPIInfo,
closeViewAPIInfo
};
};
export default useViewApIInfo;
+10
View File
@@ -10,6 +10,7 @@ import { TABLE_SORT_DIRECTIONS } from '@/config/settings';
import useExpandedRowKeys from '@/hooks/use-expanded-row-keys'; import useExpandedRowKeys from '@/hooks/use-expanded-row-keys';
import useTableFetch from '@/hooks/use-table-fetch'; import useTableFetch from '@/hooks/use-table-fetch';
import useWatchList from '@/hooks/use-watch-list'; import useWatchList from '@/hooks/use-watch-list';
import APIAccessInfoModal from '@/pages/llmodels/components/api-access-info';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { useMemoizedFn } from 'ahooks'; import { useMemoizedFn } from 'ahooks';
import { message } from 'antd'; import { message } from 'antd';
@@ -39,6 +40,7 @@ import useCreateRoute from './hooks/use-create-route';
import useOpenPlayground from './hooks/use-open-playground'; import useOpenPlayground from './hooks/use-open-playground';
import useRoutesColumns from './hooks/use-routes-columns'; import useRoutesColumns from './hooks/use-routes-columns';
import useTargetSourceModels from './hooks/use-target-source-models'; import useTargetSourceModels from './hooks/use-target-source-models';
import useViewApIInfo from './hooks/use-view-api-info';
const ModelRoutes: React.FC = () => { const ModelRoutes: React.FC = () => {
const { const {
@@ -75,6 +77,7 @@ const ModelRoutes: React.FC = () => {
} = useAccessControl(); } = useAccessControl();
const { sourceModels, fetchSourceModels } = useTargetSourceModels(); const { sourceModels, fetchSourceModels } = useTargetSourceModels();
const { handleOpenPlayGround } = useOpenPlayground(); const { handleOpenPlayGround } = useOpenPlayground();
const { apiAccessInfo, openViewAPIInfo, closeViewAPIInfo } = useViewApIInfo();
const [modelList, setModelsList] = useState<Global.BaseOption<number>[]>([]); const [modelList, setModelsList] = useState<Global.BaseOption<number>[]>([]);
useEffect(() => { useEffect(() => {
@@ -149,6 +152,8 @@ const ModelRoutes: React.FC = () => {
); );
} else if (val === 'chat') { } else if (val === 'chat') {
handleOpenPlayGround(row); handleOpenPlayGround(row);
} else if (val === 'api') {
openViewAPIInfo(row);
} }
}); });
@@ -316,6 +321,11 @@ const ModelRoutes: React.FC = () => {
currentData={openAccessControlModalStatus.currentData} currentData={openAccessControlModalStatus.currentData}
action={openAccessControlModalStatus.action} action={openAccessControlModalStatus.action}
></AccessControlModal> ></AccessControlModal>
<APIAccessInfoModal
open={apiAccessInfo.show}
data={apiAccessInfo.data}
onClose={closeViewAPIInfo}
></APIAccessInfoModal>
<DeleteModal ref={modalRef}></DeleteModal> <DeleteModal ref={modalRef}></DeleteModal>
</> </>
); );
@@ -0,0 +1,23 @@
import { useQueryDataList } from '@/hooks/use-query-data-list';
import { queryRouteTargets } from '../apis';
import { RouteTarget as ListItem } from '../config/types';
export const useQueryRouteTargets = (optons?: {
getLabel?: (item: ListItem) => string;
getValue?: (item: ListItem) => any;
}) => {
const { dataList, loading, fetchData, cancelRequest } =
useQueryDataList<ListItem>({
key: 'routeTargets',
fetchList: queryRouteTargets
});
return {
dataList,
loading,
fetchData,
cancelRequest
};
};
export default useQueryRouteTargets;
+4 -5
View File
@@ -179,14 +179,13 @@ const generateEnvArgs = (params: any) => {
// concat the args, the args is a key-value // concat the args, the args is a key-value
const generateExtraArgs = (params: any) => { const generateExtraArgs = (params: any) => {
const args = params.registrationInfo?.args || {}; const args = params.registrationInfo?.args || [];
const argsList = Object.entries(args); if (args.length === 0) {
if (argsList.length === 0) {
return ''; return '';
} }
let argsStr = ''; let argsStr = '';
argsList.forEach(([key, value]) => { args.forEach((item: string[]) => {
argsStr += `${key} ${_.isBoolean(value) ? value : value || ''} \\\n `; argsStr += `${item[0]} ${_.isBoolean(item[1]) ? item[1] : item[1] || ''} \\\n `;
}); });
return argsStr; return argsStr;
}; };