feat: Deployment Progress Popup Development

This commit is contained in:
ningyv
2025-02-10 17:03:28 +08:00
parent 0b2928eb3c
commit 8ce65cbe3d
8 changed files with 295 additions and 54 deletions
@@ -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> => {
+32 -8
View File
@@ -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
},
{
+6
View File
@@ -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>
</>
)
}