mirror of
https://github.com/APIParkLab/APIPark.git
synced 2026-06-14 20:41:15 +08:00
feat: Deployment Progress Popup Development
This commit is contained in:
@@ -26,7 +26,8 @@ interface CodeboxProps {
|
||||
language?: codeBoxLanguagesType
|
||||
extraContent?: React.ReactNode
|
||||
sx?: Record<string, unknown>
|
||||
editorTheme?: 'vs' | 'vs-dark' | 'hc-black'
|
||||
editorTheme?: 'vs' | 'vs-dark' | 'hc-black',
|
||||
autoScrollToEnd?: boolean
|
||||
}
|
||||
|
||||
export const Codebox = memo((props: CodeboxProps) => {
|
||||
@@ -41,7 +42,8 @@ export const Codebox = memo((props: CodeboxProps) => {
|
||||
readOnly = false,
|
||||
language = 'plaintext',
|
||||
extraContent,
|
||||
editorTheme = 'vs'
|
||||
editorTheme = 'vs',
|
||||
autoScrollToEnd = false
|
||||
} = props
|
||||
|
||||
const [code, setCode] = useState<string>(``)
|
||||
@@ -120,6 +122,11 @@ export const Codebox = memo((props: CodeboxProps) => {
|
||||
|
||||
const editorDidMount = (editor: MonacoEditor.IStandaloneCodeEditor): void => {
|
||||
editorRef.current = editor
|
||||
autoScrollToEnd && editor.onDidChangeModelContent(() => {
|
||||
const model = editor.getModel()
|
||||
const lineCount = model.getLineCount()
|
||||
editor.revealLine(lineCount)
|
||||
})
|
||||
}
|
||||
|
||||
const formatCode = async (): Promise<void> => {
|
||||
|
||||
@@ -129,11 +129,13 @@ const DEFAULT_HEADERS = {
|
||||
namespace: 'default'
|
||||
}
|
||||
|
||||
type EoRequest = RequestInit & {
|
||||
export type EoRequest = RequestInit & {
|
||||
eoParams?: { [k: string]: unknown }
|
||||
eoTransformKeys?: string[]
|
||||
eoApiPrefix?: string
|
||||
eoBody?: { [k: string]: unknown } | Array<unknown> | string
|
||||
isStream?: boolean
|
||||
handleStream?: (line: any) => void
|
||||
}
|
||||
|
||||
type EoHeaders = Headers | { [k: string]: string }
|
||||
@@ -186,14 +188,36 @@ export function useFetch() {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
|
||||
// 如果响应体为JSON且指定了转换键,则转换响应数据
|
||||
if (options?.eoApiPrefix||isJsonHttp(response.headers)) {
|
||||
const data = await response.json()
|
||||
const newData = (await pluginEventHub.emit('httpResponse', { data, continue: true })) as Response
|
||||
return shouldTransformKeys ? (keysToCamel(newData, options.eoTransformKeys as string[]) as T) : data
|
||||
}
|
||||
if (options?.isStream) {
|
||||
const reader = response.body?.getReader()
|
||||
const decoder = new TextDecoder('utf-8')
|
||||
let buffer = ''
|
||||
if (reader) {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
const lines = buffer.split('\n')
|
||||
buffer = lines.pop() || ''
|
||||
for (const line of lines) {
|
||||
options?.handleStream?.(line)
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
if (buffer) {
|
||||
options?.handleStream?.(buffer)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 如果响应体为JSON且指定了转换键,则转换响应数据
|
||||
if (options?.eoApiPrefix || isJsonHttp(response.headers)) {
|
||||
const data = await response.json()
|
||||
const newData = (await pluginEventHub.emit('httpResponse', { data, continue: true })) as Response
|
||||
return shouldTransformKeys ? (keysToCamel(newData, options.eoTransformKeys as string[]) as T) : data
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
// 全局错误处理
|
||||
|
||||
@@ -111,8 +111,7 @@ export const SYSTEM_TABLE_COLUMNS: PageProColumns<SystemTableListItem>[] = [
|
||||
{
|
||||
title: '状态',
|
||||
width: 140,
|
||||
dataIndex: 'update_time',
|
||||
// dataIndex: 'state',
|
||||
dataIndex: 'state',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
|
||||
@@ -746,6 +746,12 @@ p{
|
||||
padding:16px 20px !important
|
||||
}
|
||||
}
|
||||
.custom-steps .ant-steps-icon span {
|
||||
width: auto !important;
|
||||
}
|
||||
.custom-steps .ant-steps-item-content {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
|
||||
.ant-modal-body .pr-PAGE_INSIDE_X{
|
||||
|
||||
@@ -92,14 +92,12 @@ export const AIModelGuide = () => {
|
||||
*/
|
||||
const deployLocalModel = (value: { modelID: string; team?: number }) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const finalValue = {
|
||||
model: value.modelID,
|
||||
team: value?.team
|
||||
}
|
||||
console.log(finalValue)
|
||||
fetchData<BasicResponse<null>>('model/local/deploy', {
|
||||
fetchData<BasicResponse<null>>('model/local/deploy/start', {
|
||||
method: 'POST',
|
||||
eoBody: finalValue
|
||||
eoBody: {
|
||||
model: value.modelID,
|
||||
team: value?.team
|
||||
}
|
||||
})
|
||||
.then((response) => {
|
||||
const { code, msg } = response
|
||||
|
||||
@@ -15,6 +15,7 @@ import { SERVICE_KIND_OPTIONS, SYSTEM_TABLE_COLUMNS } from '../../const/system/c
|
||||
import { SystemConfigHandle, SystemTableListItem } from '../../const/system/type.ts'
|
||||
import SystemConfig from './SystemConfig.tsx'
|
||||
import { ServiceDeployment } from './serviceDeployment/ServiceDeployment.tsx'
|
||||
import { LogsFooter } from './serviceDeployment/ServiceDeployMentFooter.tsx'
|
||||
|
||||
const SystemList: FC = () => {
|
||||
const navigate = useNavigate()
|
||||
@@ -130,13 +131,11 @@ const SystemList: FC = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
const openLogsModal = (record: any) => {
|
||||
console.log('record', record)
|
||||
|
||||
modal.confirm({
|
||||
const modalInstance = modal.confirm({
|
||||
title: $t('部署过程'),
|
||||
content: <ServiceDeployment record={record} />,
|
||||
onOk: () => {
|
||||
console.log('ok')
|
||||
footer: () => {
|
||||
return <LogsFooter record={record} modalInstance={modalInstance} />
|
||||
},
|
||||
width: 600,
|
||||
okText: $t('确认'),
|
||||
@@ -161,13 +160,13 @@ const SystemList: FC = () => {
|
||||
;(x.valueEnum as any)[option.value] = { text: $t(option.label) }
|
||||
})
|
||||
}
|
||||
if ((x.dataIndex as string) === 'update_time') {
|
||||
if ((x.dataIndex as string) === 'state') {
|
||||
x.render = (text: any, record: any) => (
|
||||
<span
|
||||
className={`text-[13px] ${record.can_delete ? '[&>.ant-typography]:text-[#2196f3]' : ''}`}
|
||||
className={`text-[13px] ${record.state === 'deploying' ? '[&>.ant-typography]:text-[#2196f3]' : record.state === 'error' ? '[&>.ant-typography]:text-[#ff4d4f]' : ''}`}
|
||||
onClick={(e) => {
|
||||
if (record.can_delete) {
|
||||
e?.stopPropagation();
|
||||
if (['deploying', 'error'].includes(record.state)) {
|
||||
e?.stopPropagation()
|
||||
openLogsModal(record)
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { App, Button } from 'antd'
|
||||
import { $t } from '@common/locales/index.ts'
|
||||
import { useFetch } from '@common/hooks/http.ts'
|
||||
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
|
||||
|
||||
export const LogsFooter = (props: any) => {
|
||||
const { record, modalInstance } = props
|
||||
const { message, modal } = App.useApp()
|
||||
const { fetchData } = useFetch()
|
||||
const stopDeploy = () => {
|
||||
modal.confirm({
|
||||
title: $t('停止部署'),
|
||||
content: $t('确定停止部署吗?'),
|
||||
onOk: () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fetchData<BasicResponse<any>>('model/local/cancel_deploy', {
|
||||
method: 'POST',
|
||||
eoBody: { recordId: record.id }
|
||||
})
|
||||
.then((response) => {
|
||||
const { code, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
resolve(true)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
reject(false)
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
resolve(true)
|
||||
modalInstance.destroy()
|
||||
})
|
||||
})
|
||||
},
|
||||
width: 600,
|
||||
okText: $t('确认'),
|
||||
cancelText: $t('取消'),
|
||||
closable: true,
|
||||
icon: <></>
|
||||
})
|
||||
}
|
||||
const deleteService = () => {
|
||||
modal.confirm({
|
||||
title: $t('删除服务'),
|
||||
content: $t('确定删除服务吗?'),
|
||||
onOk: () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fetchData<BasicResponse<any>>('model/local', {
|
||||
method: 'DELETE',
|
||||
eoBody: { recordId: record.id }
|
||||
})
|
||||
.then((response: BasicResponse<any>) => {
|
||||
const { code, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
resolve(true)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
reject(false)
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
resolve(true)
|
||||
modalInstance.destroy()
|
||||
})
|
||||
})
|
||||
},
|
||||
width: 600,
|
||||
okText: $t('确认'),
|
||||
cancelText: $t('取消'),
|
||||
closable: true,
|
||||
icon: <></>
|
||||
})
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{record.state === 'error' ? (
|
||||
<div className="flex justify-end items-center">
|
||||
<Button onClick={() => { modalInstance.destroy() }}>取消</Button>
|
||||
<Button onClick={deleteService} type="primary" danger>
|
||||
删除服务
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex justify-end items-center">
|
||||
<Button onClick={stopDeploy} type="primary" danger>
|
||||
停止
|
||||
</Button>
|
||||
<Button type="primary" onClick={() => { modalInstance.destroy() }}>继续等待</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,44 +1,159 @@
|
||||
import { SystemTableListItem } from '@core/const/system/type'
|
||||
import type { StepsProps } from 'antd'
|
||||
import { Popover, Steps } from 'antd'
|
||||
import { CheckCircleOutlined, LoadingOutlined } from '@ant-design/icons'
|
||||
import { Steps } from 'antd'
|
||||
import { CheckCircleOutlined, LoadingOutlined, ClockCircleOutlined, CloseCircleOutlined } from '@ant-design/icons'
|
||||
import { Codebox } from '@common/components/postcat/api/Codebox'
|
||||
import { Collapse } from 'antd'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useFetch } from '@common/hooks/http'
|
||||
|
||||
const customDot: StepsProps['progressDot'] = (dot, { status, index }) => (
|
||||
<Popover
|
||||
content={
|
||||
<span>
|
||||
step {index} status: {status}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
{dot}
|
||||
</Popover>
|
||||
)
|
||||
const getIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return <CheckCircleOutlined style={{ color: 'green', fontSize: '40px' }} />
|
||||
case 'inProgress':
|
||||
return <LoadingOutlined style={{ color: '#2196f3', fontSize: '40px' }} />
|
||||
case 'pending':
|
||||
return <ClockCircleOutlined style={{ color: 'gray', fontSize: '40px' }} />
|
||||
case 'error':
|
||||
return <CloseCircleOutlined style={{ color: 'red', fontSize: '40px' }} />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export const ServiceDeployment = (props: { record: SystemTableListItem }) => {
|
||||
const { record } = props
|
||||
console.log('record', record)
|
||||
|
||||
const items = [
|
||||
const [stepItem, setStepItem] = useState<
|
||||
{
|
||||
title: string
|
||||
description?: string
|
||||
status?: string
|
||||
}[]
|
||||
>([
|
||||
{
|
||||
title: 'Download',
|
||||
description: '4.7 GB / 4.7 GB'
|
||||
status: 'pending'
|
||||
},
|
||||
{
|
||||
title: 'Deploy',
|
||||
status: 'pending'
|
||||
},
|
||||
{
|
||||
title: 'Initializing',
|
||||
status: 'pending'
|
||||
}
|
||||
]
|
||||
])
|
||||
|
||||
const [scriptStr, setScriptStr] = useState('')
|
||||
const [step, setStep] = useState(0)
|
||||
const [collapseText] = useState('Progress log')
|
||||
const { fetchData } = useFetch()
|
||||
|
||||
useEffect(() => {
|
||||
setStepItem((prevItems) =>
|
||||
prevItems.map((item, index) => {
|
||||
return { ...item, status: index < step ? 'completed' : item.status }
|
||||
})
|
||||
)
|
||||
}, [step])
|
||||
|
||||
useEffect(() => {
|
||||
fetchData(
|
||||
'http://localhost:3000/stream',
|
||||
// 'model/local/deploy',
|
||||
{
|
||||
method: 'POST',
|
||||
eoBody: { recordId: record.id },
|
||||
custom: true,
|
||||
isStream: true,
|
||||
handleStream: (chunk) => {
|
||||
const parsedChunk = JSON.parse(chunk)
|
||||
// 下载中
|
||||
if (parsedChunk?.data?.state.includes('download')) {
|
||||
setStepItem((prevItems) =>
|
||||
prevItems.map((item) => {
|
||||
return item.title === 'Download'
|
||||
? {
|
||||
...item,
|
||||
description: `${parsedChunk?.data?.info?.current} / ${parsedChunk?.data?.info?.total}`,
|
||||
status: 'inProgress'
|
||||
}
|
||||
: item
|
||||
})
|
||||
)
|
||||
setStep(0)
|
||||
// 部署中
|
||||
} else if (parsedChunk?.data?.state.includes('deploy')) {
|
||||
setStepItem((prevItems) =>
|
||||
prevItems.map((item) => {
|
||||
return { ...item, status: item.title === 'Deploy' ? 'inProgress' : item.status }
|
||||
})
|
||||
)
|
||||
setStep(1)
|
||||
// 初始化中
|
||||
} else if (parsedChunk?.data?.state.includes('initializing')) {
|
||||
setStepItem((prevItems) =>
|
||||
prevItems.map((item) => {
|
||||
return { ...item, status: item.title === 'Initializing' ? 'inProgress' : item.status }
|
||||
})
|
||||
)
|
||||
setStep(2)
|
||||
// 完成
|
||||
} else if (parsedChunk?.data?.state.includes('finish')) {
|
||||
setStepItem((prevItems) =>
|
||||
prevItems.map((item) => {
|
||||
return { ...item, status: item.title === 'Initializing' ? 'completed' : item.status }
|
||||
})
|
||||
)
|
||||
setStep(4)
|
||||
}
|
||||
setScriptStr(parsedChunk?.data?.message || '')
|
||||
}
|
||||
}
|
||||
)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="flex justify-center items-center">
|
||||
{/* <Steps items={items} /> */}
|
||||
<Steps
|
||||
current={0}
|
||||
labelPlacement="vertical"
|
||||
items={items}
|
||||
/>
|
||||
</div>
|
||||
<>
|
||||
<div className="flex justify-center items-center mb-[20px] mt-[20px] custom-steps">
|
||||
<Steps labelPlacement="vertical">
|
||||
{stepItem.map((item, index) => (
|
||||
<Steps.Step
|
||||
key={index}
|
||||
title={item.title}
|
||||
icon={getIcon(item.status || '')}
|
||||
description={item.description}
|
||||
/>
|
||||
))}
|
||||
</Steps>
|
||||
</div>
|
||||
<Collapse
|
||||
expandIconPosition="end"
|
||||
defaultActiveKey={['1']}
|
||||
className="[&_.ant-collapse-content-box]:p-[0px]"
|
||||
items={[
|
||||
{
|
||||
label: collapseText,
|
||||
key: '1',
|
||||
children: (
|
||||
<Codebox
|
||||
editorTheme="vs-dark"
|
||||
readOnly={true}
|
||||
autoScrollToEnd={true}
|
||||
options={{
|
||||
wordWrap: 'off'
|
||||
}}
|
||||
width="100%"
|
||||
value={scriptStr}
|
||||
height="200px"
|
||||
language="json"
|
||||
enableToolbar={false}
|
||||
/>
|
||||
)
|
||||
}
|
||||
]}
|
||||
></Collapse>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user