chore: list models query

This commit is contained in:
jialin
2024-08-09 10:26:46 +08:00
parent ab8483fc42
commit 8490e32ef1
14 changed files with 669 additions and 114 deletions
+3 -42
View File
@@ -5,15 +5,13 @@ import SealSelect from '@/components/seal-form/seal-select';
import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import { convertFileSize } from '@/utils';
import { SearchOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Form, Input, Modal } from 'antd';
import { Form, Modal } from 'antd';
import _ from 'lodash';
import { memo, useCallback, useEffect, useState } from 'react';
import { queryHuggingfaceModelFiles, queryHuggingfaceModels } from '../apis';
import { modelSourceMap } from '../config';
import { FormData, ListItem } from '../config/types';
import SearchInput from './search-input';
type AddModalProps = {
title: string;
@@ -50,7 +48,6 @@ const AddModal: React.FC<AddModalProps> = (props) => {
const [fileOptions, setFileOptions] = useState<
{ label: string; value: string }[]
>([]);
const [ollamaTags, setOllamaTags] = useState<string[]>([]);
const initFormValue = () => {
if (action === PageAction.CREATE && open) {
@@ -154,24 +151,11 @@ const AddModal: React.FC<AddModalProps> = (props) => {
}
]}
>
<SealAutoComplete
<SealInput.Input
label={intl.formatMessage({ id: 'models.form.repoid' })}
required
showSearch
onBlur={handleRepoOnBlur}
onSelect={handleRepoOnBlur}
onSearch={debounceSearch}
options={repoOptions}
addAfter={
<span style={{ position: 'relative', top: '2px' }}>
<SearchOutlined></SearchOutlined>
</span>
}
disabled={true}
description={intl.formatMessage({ id: 'models.form.repoid.desc' })}
>
<Input></Input>
</SealAutoComplete>
></SealInput.Input>
</Form.Item>
<Form.Item<FormData>
name="huggingface_filename"
@@ -246,14 +230,6 @@ const AddModal: React.FC<AddModalProps> = (props) => {
}
]}
>
{/* <SealAutoComplete
filterOption
disabled={action === PageAction.EDIT}
label={intl.formatMessage({ id: 'model.form.ollama.model' })}
placeholder={intl.formatMessage({ id: 'model.form.ollamaholder' })}
required
options={ollamaModelOptions}
></SealAutoComplete> */}
<SealInput.Input
disabled={action === PageAction.EDIT}
label={intl.formatMessage({ id: 'model.form.ollama.model' })}
@@ -261,12 +237,6 @@ const AddModal: React.FC<AddModalProps> = (props) => {
required
></SealInput.Input>
</Form.Item>
<Form.Item name="tag">
<SealInput.Input
label="Tag"
placeholder={`${_.join(ollamaTags, ',')} or another...`}
></SealInput.Input>
</Form.Item>
</>
);
};
@@ -286,7 +256,6 @@ const AddModal: React.FC<AddModalProps> = (props) => {
const handleOnSelectModel = useCallback((item: any) => {
const repo = item.name;
setOllamaTags(_.map(item.tags, (tag: string) => _.toLower(tag)));
if (form.getFieldValue('source') === modelSourceMap.huggingface_value) {
form.setFieldValue('huggingface_repo_id', repo);
handleFetchModelFiles(repo);
@@ -320,13 +289,6 @@ const AddModal: React.FC<AddModalProps> = (props) => {
<ModalFooter onCancel={onCancel} onOk={handleSumit}></ModalFooter>
}
>
{action === PageAction.CREATE && (
<SearchInput
modelSource={modelSource}
onSourceChange={handleSourceChange}
onSelectModel={handleOnSelectModel}
></SearchInput>
)}
<Form name="addModalForm" form={form} onFinish={onOk} preserve={false}>
<Form.Item<FormData>
name="name"
@@ -362,7 +324,6 @@ const AddModal: React.FC<AddModalProps> = (props) => {
)
}
]}
noStyle={action === PageAction.CREATE}
>
{action === PageAction.EDIT && (
<SealSelect
@@ -0,0 +1,395 @@
import ModalFooter from '@/components/modal-footer';
import SealInput from '@/components/seal-form/seal-input';
import SealSelect from '@/components/seal-form/seal-select';
import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import { convertFileSize } from '@/utils';
import { useIntl } from '@umijs/max';
import { Divider, Drawer, Form } from 'antd';
import _ from 'lodash';
import { memo, useCallback, useEffect, useState } from 'react';
import { queryHuggingfaceModelFiles, queryHuggingfaceModels } from '../apis';
import { modelSourceMap } from '../config';
import { FormData, ListItem } from '../config/types';
import HFModelFile from './hf-model-file';
import SearchModel from './search-model';
type AddModalProps = {
title: string;
action: PageActionType;
open: boolean;
data?: ListItem;
source: string;
onOk: (values: FormData) => void;
onCancel: () => void;
};
const sourceOptions = [
{
label: 'Hugging Face',
value: modelSourceMap.huggingface_value,
key: 'huggingface'
},
{
label: 'Ollama Library',
value: modelSourceMap.ollama_library_value,
key: 'ollama_library'
}
];
const AddModal: React.FC<AddModalProps> = (props) => {
console.log('addmodel====');
const { title, action, open, source, onOk, onCancel } = props || {};
const [form] = Form.useForm();
const intl = useIntl();
const modelSource = Form.useWatch('source', form);
const huggingfaceRepoId = Form.useWatch('huggingface_repo_id', form);
const [loading, setLoading] = useState(false);
const [repoOptions, setRepoOptions] = useState<
{ label: string; value: string }[]
>([]);
const [fileOptions, setFileOptions] = useState<
{ label: string; value: string }[]
>([]);
const initFormValue = () => {
form.setFieldsValue({
source: props.source,
replicas: 1
});
};
useEffect(() => {
initFormValue();
console.log('source========', props.source);
}, [open]);
const fileNamLabel = (item: any) => {
return (
<span>
{item.path}
<span
style={{ color: 'var(--ant-color-text-tertiary)', marginLeft: '4px' }}
>
({convertFileSize(item.size)})
</span>
</span>
);
};
const handleFetchModelFiles = async (repo: string) => {
try {
setLoading(true);
const res = await queryHuggingfaceModelFiles({ repo });
const list = _.filter(res, (file: any) => {
return _.endsWith(file.path, '.gguf');
}).map((item: any) => {
return {
label: fileNamLabel(item),
value: item.path,
size: item.size
};
});
setFileOptions(list);
setLoading(false);
} catch (error) {
setFileOptions([]);
setLoading(false);
}
};
const handleRepoOnBlur = (e: any) => {
const repo = form.getFieldValue('huggingface_repo_id');
handleFetchModelFiles(repo);
};
const handleSelectModelFile = useCallback((item: any) => {
form.setFieldValue('huggingface_filename', item.path);
}, []);
const handleOnSearchRepo = async (text: string) => {
try {
const params = {
search: {
query: text,
tags: ['gguf']
}
};
const models = await queryHuggingfaceModels(params);
const list = _.map(models || [], (item: any) => {
return {
...item,
value: item.name,
label: item.name
};
});
setRepoOptions(list);
} catch (error) {
setRepoOptions([]);
}
};
const debounceSearch = _.debounce((text: string) => {
handleOnSearchRepo(text);
}, 300);
const renderHuggingfaceFields = () => {
return (
<>
<Form.Item<FormData>
name="huggingface_repo_id"
rules={[
{
required: true,
message: intl.formatMessage(
{
id: 'common.form.rule.input'
},
{ name: intl.formatMessage({ id: 'models.form.repoid' }) }
)
}
]}
>
<SealInput.Input
label={intl.formatMessage({ id: 'models.form.repoid' })}
required
disabled={true}
></SealInput.Input>
</Form.Item>
<Form.Item<FormData>
name="huggingface_filename"
rules={[
{
required: true,
message: intl.formatMessage(
{
id: 'common.form.rule.input'
},
{ name: intl.formatMessage({ id: 'models.form.filename' }) }
)
}
]}
>
<SealInput.Input
label={intl.formatMessage({ id: 'models.form.filename' })}
required
disabled={true}
></SealInput.Input>
</Form.Item>
</>
);
};
const renderS3Fields = () => {
return (
<>
<Form.Item<FormData>
name="s3_address"
rules={[
{
required: true,
message: intl.formatMessage(
{
id: 'common.form.rule.input'
},
{ name: intl.formatMessage({ id: 'models.form.s3address' }) }
)
}
]}
>
<SealInput.Input
label={intl.formatMessage({
id: 'models.form.s3address'
})}
required
></SealInput.Input>
</Form.Item>
</>
);
};
const renderOllamaModelFields = () => {
return (
<>
<Form.Item<FormData>
name="ollama_library_model_name"
rules={[
{
required: true,
message: intl.formatMessage(
{
id: 'common.form.rule.input'
},
{ name: intl.formatMessage({ id: 'models.table.name' }) }
)
}
]}
>
<SealInput.Input
disabled={action === PageAction.EDIT}
label={intl.formatMessage({ id: 'model.form.ollama.model' })}
placeholder={intl.formatMessage({ id: 'model.form.ollamaholder' })}
required
></SealInput.Input>
</Form.Item>
</>
);
};
const renderFieldsBySource = () => {
switch (props.source) {
case modelSourceMap.huggingface_value:
return renderHuggingfaceFields();
case modelSourceMap.ollama_library_value:
return renderOllamaModelFields();
case modelSourceMap.s3_value:
return renderS3Fields();
default:
return null;
}
};
const handleOnSelectModel = useCallback((item: any) => {
const repo = item.name;
if (form.getFieldValue('source') === modelSourceMap.huggingface_value) {
form.setFieldValue('huggingface_repo_id', repo);
} else {
form.setFieldValue('ollama_library_model_name', repo);
}
}, []);
const handleSumit = () => {
form.submit();
};
return (
<Drawer
title={title}
open={open}
onClose={onCancel}
destroyOnClose={true}
closeIcon={true}
maskClosable={false}
keyboard={false}
styles={{
body: {
height: 'calc(100vh - 110px)'
}
}}
width="90%"
footer={
<ModalFooter
onCancel={onCancel}
onOk={handleSumit}
style={{
display: 'flex',
justifyContent: 'flex-end'
}}
></ModalFooter>
}
>
<div style={{ display: 'flex' }}>
<div style={{ flex: 1 }}>
<SearchModel
modelSource={modelSource}
onSelectModel={handleOnSelectModel}
></SearchModel>
</div>
<Divider type="vertical"></Divider>
<div style={{ flex: 1 }}>
<HFModelFile
repo={huggingfaceRepoId}
onSelectFile={handleSelectModelFile}
></HFModelFile>
</div>
<Divider type="vertical"></Divider>
<div style={{ width: 450 }}>
<h3 className="flex-between font-size-14">
<span>Configuration</span>
</h3>
<Form name="deployModel" form={form} onFinish={onOk} preserve={false}>
<Form.Item<FormData>
name="name"
rules={[
{
required: true,
message: intl.formatMessage(
{
id: 'common.form.rule.input'
},
{ name: intl.formatMessage({ id: 'common.table.name' }) }
)
}
]}
>
<SealInput.Input
label={intl.formatMessage({
id: 'common.table.name'
})}
required
></SealInput.Input>
</Form.Item>
<Form.Item<FormData>
name="source"
rules={[
{
required: true,
message: intl.formatMessage(
{
id: 'common.form.rule.select'
},
{ name: intl.formatMessage({ id: 'models.form.source' }) }
)
}
]}
>
{
<SealSelect
disabled={true}
label={intl.formatMessage({
id: 'models.form.source'
})}
options={sourceOptions}
required
></SealSelect>
}
</Form.Item>
{renderFieldsBySource()}
<Form.Item<FormData>
name="replicas"
rules={[
{
required: true,
message: intl.formatMessage(
{
id: 'common.form.rule.input'
},
{ name: intl.formatMessage({ id: 'models.form.replicas' }) }
)
}
]}
>
<SealInput.Number
style={{ width: '100%' }}
label={intl.formatMessage({
id: 'models.form.replicas'
})}
required
min={0}
></SealInput.Number>
</Form.Item>
<Form.Item<FormData> name="description">
<SealInput.TextArea
label={intl.formatMessage({
id: 'common.table.description'
})}
></SealInput.TextArea>
</Form.Item>
</Form>
</div>
</div>
</Drawer>
);
};
export default memo(AddModal);
@@ -0,0 +1,101 @@
import { convertFileSize } from '@/utils';
import { SearchOutlined } from '@ant-design/icons';
import { Button, Col, Empty, Row, Space, Spin } from 'antd';
import classNames from 'classnames';
import _ from 'lodash';
import { useEffect, useState } from 'react';
import { queryHuggingfaceModelFiles } from '../apis';
import '../style/hf-model-file.less';
interface HFModelFileProps {
repo: string;
onSelectFile?: (file: any) => void;
}
const HFModelFile: React.FC<HFModelFileProps> = (props) => {
const [dataSource, setDataSource] = useState<any>({
fileList: [],
loading: false
});
const [current, setCurrent] = useState<string>('');
const handleSelectModelFile = (item: any) => {
props.onSelectFile?.(item);
setCurrent(item.path);
};
const handleFetchModelFiles = async () => {
if (!props.repo) {
setDataSource({ fileList: [], loading: false });
return;
}
setDataSource({ ...dataSource, loading: true });
setCurrent('');
try {
const res = await queryHuggingfaceModelFiles({ repo: props.repo });
const list = _.filter(res, (file: any) => {
return _.endsWith(file.path, '.gguf');
});
setDataSource({ fileList: list, loading: false });
handleSelectModelFile(list[0]);
} catch (error) {
setDataSource({ fileList: [], loading: false });
handleSelectModelFile({});
}
};
useEffect(() => {
handleFetchModelFiles();
}, [props.repo]);
return (
<div>
<h3 className="flex-between font-size-14">
<span>Available Files({dataSource.fileList.length || 0})</span>
</h3>
<div>
<Spin spinning={dataSource.loading} style={{ minHeight: 100 }}></Spin>
{dataSource.fileList.length ? (
<Row gutter={[10, 10]}>
{_.map(dataSource.fileList, (item: any) => {
return (
<Col span={24} key={item.path}>
<div
className={classNames('hf-model-file', {
active: item.path === current
})}
onClick={() => handleSelectModelFile(item)}
>
<div className="title">{item.path}</div>
<Space className="tags">
<span className="tag-item">
{convertFileSize(item.size)}
</span>
</Space>
<div className="btn">
<Button size="middle">Install</Button>
</div>
</div>
</Col>
);
})}
</Row>
) : (
!dataSource.loading && (
<Empty
imageStyle={{ height: 'auto', marginTop: '20px' }}
image={
<SearchOutlined
className="font-size-16"
style={{ color: 'var(--ant-color-text-tertiary)' }}
></SearchOutlined>
}
description="No models found"
/>
)
)}
</div>
</div>
);
};
export default HFModelFile;
@@ -1,9 +1,8 @@
import IconFont from '@/components/icon-font';
import RadioButtons from '@/components/radio-buttons';
import { SearchOutlined } from '@ant-design/icons';
import { Input } from 'antd';
import _ from 'lodash';
import React, { useRef, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { queryHuggingfaceModels } from '../apis';
import { modelSourceMap, ollamaModelOptions } from '../config';
import SearchStyle from '../style/search-result.less';
@@ -11,7 +10,7 @@ import SearchResult from './search-result';
interface SearchInputProps {
modelSource: string;
onSourceChange: (source: string) => void;
onSourceChange?: (source: string) => void;
onSelectModel: (model: any) => void;
}
@@ -37,6 +36,12 @@ const SearchInput: React.FC<SearchInputProps> = (props) => {
const [loading, setLoading] = useState(false);
const cacheRepoOptions = useRef<any[]>([]);
const axiosTokenRef = useRef<any>(null);
const [current, setCurrent] = useState<string>('');
const handleOnSelectModel = (item: any) => {
onSelectModel(item);
setCurrent(item.id);
};
const handleOnSearchRepo = async (text: string) => {
axiosTokenRef.current?.abort?.();
@@ -67,8 +72,10 @@ const SearchInput: React.FC<SearchInputProps> = (props) => {
).reverse();
cacheRepoOptions.current = sortedList;
setRepoOptions(sortedList);
handleOnSelectModel(sortedList[0]);
} catch (error) {
setRepoOptions([]);
handleOnSelectModel({});
cacheRepoOptions.current = [];
} finally {
setLoading(false);
@@ -81,7 +88,6 @@ const SearchInput: React.FC<SearchInputProps> = (props) => {
};
const handleOnFocus = () => {
setShowSearch(true);
if (
!repoOptions.length &&
!cacheRepoOptions.current.length &&
@@ -95,17 +101,6 @@ const SearchInput: React.FC<SearchInputProps> = (props) => {
}
};
const handleOnSelectModel = (item: any) => {
onSelectModel(item);
setShowSearch(false);
};
const handleOnBlur = () => {
setTimeout(() => {
setShowSearch(false);
}, 200);
};
const handleFilterModels = (e: any) => {
const text = e.target.value;
const list = _.filter(cacheRepoOptions.current, (item: any) => {
@@ -121,26 +116,30 @@ const SearchInput: React.FC<SearchInputProps> = (props) => {
const handleSourceChange = (source: string) => {
axiosTokenRef.current?.abort?.();
onSourceChange(source);
onSourceChange?.(source);
setRepoOptions([]);
cacheRepoOptions.current = [];
};
useEffect(() => {
handleOnFocus();
return () => {
axiosTokenRef.current?.abort?.();
};
}, [modelSource]);
return (
<>
<div style={{ flex: 1 }}>
<div className={SearchStyle['search-bar']}>
<RadioButtons
{/* <RadioButtons
options={sourceList}
value={modelSource}
onChange={handleSourceChange}
></RadioButtons>
></RadioButtons> */}
<Input
onPressEnter={handlerSearchModels}
onChange={debounceFilter}
allowClear
onFocus={handleOnFocus}
onBlur={() => handleOnBlur()}
className="m-l-20"
placeholder={
modelSource === 'huggingface'
? 'Search models from hugging face '
@@ -156,17 +155,16 @@ const SearchInput: React.FC<SearchInputProps> = (props) => {
}
></Input>
</div>
{showSearch && (
{
<SearchResult
loading={loading}
resultList={repoOptions}
current=""
current={current}
source={modelSource}
onSelect={handleOnSelectModel}
></SearchResult>
)}
<div style={{ height: '81px' }}></div>
</>
}
</div>
);
};
@@ -26,7 +26,7 @@ const SearchResult: React.FC<SearchResultProps> = (props) => {
{resultList.length ? (
<Row gutter={[10, 10]}>
{resultList.map((item, index) => (
<Col span={12} key={item.name}>
<Col span={24} key={item.name}>
<div onClick={(e) => handleSelect(e, item)}>
<HFModelItem
source={source}
+63 -25
View File
@@ -5,7 +5,6 @@ import PageTools from '@/components/page-tools';
import SealTable from '@/components/seal-table';
import SealColumn from '@/components/seal-table/components/seal-column';
import { PageAction } from '@/config';
import type { PageActionType } from '@/config/types';
import useExpandedRowKeys from '@/hooks/use-expanded-row-keys';
import useTableRowSelection from '@/hooks/use-table-row-selection';
import useTableSort from '@/hooks/use-table-sort';
@@ -13,14 +12,14 @@ import ViewCodeModal from '@/pages/playground/components/view-code-modal';
import { handleBatchRequest } from '@/utils';
import {
DeleteOutlined,
DownOutlined,
EditOutlined,
PlusOutlined,
SyncOutlined,
WechatWorkOutlined
} from '@ant-design/icons';
import { PageContainer } from '@ant-design/pro-components';
import { Access, useAccess, useIntl, useNavigate } from '@umijs/max';
import { Button, Input, Space, Tag, message } from 'antd';
import { Button, Dropdown, Input, Space, Tag, message } from 'antd';
import dayjs from 'dayjs';
import _ from 'lodash';
import { useCallback, useRef, useState } from 'react';
@@ -36,6 +35,7 @@ import {
import { modelSourceMap } from '../config';
import { FormData, ListItem, ModelInstanceListItem } from '../config/types';
import AddModal from './add-modal';
import DeployModal from './deploy-modal';
import InstanceItem from './instance-item';
import ViewLogsModal from './view-logs-modal';
@@ -80,13 +80,37 @@ const Models: React.FC<ModelsProps> = ({
const [openViewCodeModal, setOpenViewCodeModal] = useState(false);
const [openLogModal, setOpenLogModal] = useState(false);
const [openAddModal, setOpenAddModal] = useState(false);
const [action, setAction] = useState<PageActionType>(PageAction.CREATE);
const [openDeployModal, setOpenDeployModal] = useState(false);
const [title, setTitle] = useState<string>('');
const [currentData, setCurrentData] = useState<ListItem | undefined>(
undefined
);
const [currentInstanceUrl, setCurrentInstanceUrl] = useState<string>('');
const modalRef = useRef<any>(null);
const sourceRef = useRef<any>(null);
const sourceOptions = [
{
label: 'Hugging Face',
value: modelSourceMap.huggingface_value,
key: 'huggingface',
icon: <IconFont type="icon-huggingface"></IconFont>,
onClick: () => {
sourceRef.current = modelSourceMap.huggingface_value;
setOpenDeployModal(true);
}
},
{
label: 'Ollama Library',
value: modelSourceMap.ollama_library_value,
key: 'ollama_library',
icon: <IconFont type="icon-ollama"></IconFont>,
onClick: () => {
sourceRef.current = modelSourceMap.ollama_library_value;
setOpenDeployModal(true);
}
}
];
const ActionList = [
{
@@ -133,7 +157,6 @@ const Models: React.FC<ModelsProps> = ({
const handleAddModal = () => {
setOpenAddModal(true);
setAction(PageAction.CREATE);
setTitle(intl.formatMessage({ id: 'models.button.deploy' }));
};
@@ -141,21 +164,12 @@ const Models: React.FC<ModelsProps> = ({
async (data: FormData) => {
try {
console.log('data:', data);
if (data.source === modelSourceMap.ollama_library_value) {
data.ollama_library_model_name = `${data.ollama_library_model_name}:${data.tag}`;
}
if (action === PageAction.CREATE) {
await createModel({ data });
}
if (action === PageAction.EDIT) {
await updateModel({ data, id: currentData?.id as number });
}
await updateModel({ data, id: currentData?.id as number });
setOpenAddModal(false);
message.success(intl.formatMessage({ id: 'common.message.success' }));
} catch (error) {}
},
[action, currentData]
[currentData]
);
const handleModalCancel = useCallback(() => {
@@ -163,6 +177,20 @@ const Models: React.FC<ModelsProps> = ({
setOpenAddModal(false);
}, []);
const handleDeployModalCancel = useCallback(() => {
setOpenDeployModal(false);
}, []);
const handleCreateModel = useCallback(async (data: FormData) => {
try {
console.log('data:', data);
await createModel({ data });
setOpenDeployModal(false);
message.success(intl.formatMessage({ id: 'common.message.success' }));
} catch (error) {}
}, []);
const handleLogModalCancel = useCallback(() => {
setOpenLogModal(false);
}, []);
@@ -230,7 +258,6 @@ const Models: React.FC<ModelsProps> = ({
const handleEdit = (row: ListItem) => {
setCurrentData(row);
setOpenAddModal(true);
setAction(PageAction.EDIT);
setTitle(intl.formatMessage({ id: 'models.title.edit' }));
};
@@ -314,13 +341,15 @@ const Models: React.FC<ModelsProps> = ({
}
right={
<Space size={20}>
<Button
icon={<PlusOutlined></PlusOutlined>}
type="primary"
onClick={handleAddModal}
>
{intl?.formatMessage?.({ id: 'models.button.deploy' })}
</Button>
<Dropdown menu={{ items: sourceOptions }}>
<Button
icon={<DownOutlined></DownOutlined>}
type="primary"
iconPosition="end"
>
{intl?.formatMessage?.({ id: 'models.button.deploy' })}
</Button>
</Dropdown>
<Access accessible={access.canDelete}>
<Button
icon={<DeleteOutlined />}
@@ -440,12 +469,21 @@ const Models: React.FC<ModelsProps> = ({
</PageContainer>
<AddModal
open={openAddModal}
action={action}
action={PageAction.EDIT}
title={title}
data={currentData}
onCancel={handleModalCancel}
onOk={handleModalOk}
></AddModal>
<DeployModal
open={openDeployModal}
action={PageAction.CREATE}
title="Deploy Model"
data={currentData}
source={sourceRef.current}
onCancel={handleDeployModalCancel}
onOk={handleCreateModel}
></DeployModal>
<ViewLogsModal
url={currentInstanceUrl}
open={openLogModal}