From 8ce65cbe3d951baea82a55f6c1940637dab1e51d Mon Sep 17 00:00:00 2001 From: ningyv <1793599591@qq.com> Date: Mon, 10 Feb 2025 17:03:28 +0800 Subject: [PATCH] feat: Deployment Progress Popup Development --- .../components/postcat/api/Codebox/index.tsx | 11 +- frontend/packages/common/src/hooks/http.ts | 40 ++++- .../packages/core/src/const/system/const.tsx | 3 +- frontend/packages/core/src/index.css | 6 + .../core/src/pages/guide/AIModelGuide.tsx | 12 +- .../core/src/pages/system/SystemList.tsx | 17 +- .../ServiceDeployMentFooter.tsx | 93 ++++++++++ .../serviceDeployment/ServiceDeployment.tsx | 167 +++++++++++++++--- 8 files changed, 295 insertions(+), 54 deletions(-) create mode 100644 frontend/packages/core/src/pages/system/serviceDeployment/ServiceDeployMentFooter.tsx diff --git a/frontend/packages/common/src/components/postcat/api/Codebox/index.tsx b/frontend/packages/common/src/components/postcat/api/Codebox/index.tsx index 7cf4ec9c..73e2b399 100644 --- a/frontend/packages/common/src/components/postcat/api/Codebox/index.tsx +++ b/frontend/packages/common/src/components/postcat/api/Codebox/index.tsx @@ -26,7 +26,8 @@ interface CodeboxProps { language?: codeBoxLanguagesType extraContent?: React.ReactNode sx?: Record - 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(``) @@ -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 => { diff --git a/frontend/packages/common/src/hooks/http.ts b/frontend/packages/common/src/hooks/http.ts index 6688fdad..b02b4e10 100644 --- a/frontend/packages/common/src/hooks/http.ts +++ b/frontend/packages/common/src/hooks/http.ts @@ -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 | 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) => { // 全局错误处理 diff --git a/frontend/packages/core/src/const/system/const.tsx b/frontend/packages/core/src/const/system/const.tsx index a3090477..bfd95cf1 100644 --- a/frontend/packages/core/src/const/system/const.tsx +++ b/frontend/packages/core/src/const/system/const.tsx @@ -111,8 +111,7 @@ export const SYSTEM_TABLE_COLUMNS: PageProColumns[] = [ { title: '状态', width: 140, - dataIndex: 'update_time', - // dataIndex: 'state', + dataIndex: 'state', ellipsis: true }, { diff --git a/frontend/packages/core/src/index.css b/frontend/packages/core/src/index.css index 38b3f917..fe0145b5 100644 --- a/frontend/packages/core/src/index.css +++ b/frontend/packages/core/src/index.css @@ -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{ diff --git a/frontend/packages/core/src/pages/guide/AIModelGuide.tsx b/frontend/packages/core/src/pages/guide/AIModelGuide.tsx index 53c2288b..7e88505f 100644 --- a/frontend/packages/core/src/pages/guide/AIModelGuide.tsx +++ b/frontend/packages/core/src/pages/guide/AIModelGuide.tsx @@ -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>('model/local/deploy', { + fetchData>('model/local/deploy/start', { method: 'POST', - eoBody: finalValue + eoBody: { + model: value.modelID, + team: value?.team + } }) .then((response) => { const { code, msg } = response diff --git a/frontend/packages/core/src/pages/system/SystemList.tsx b/frontend/packages/core/src/pages/system/SystemList.tsx index 5c08f906..683b16d0 100644 --- a/frontend/packages/core/src/pages/system/SystemList.tsx +++ b/frontend/packages/core/src/pages/system/SystemList.tsx @@ -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: , - onOk: () => { - console.log('ok') + footer: () => { + return }, 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) => ( .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) } }} diff --git a/frontend/packages/core/src/pages/system/serviceDeployment/ServiceDeployMentFooter.tsx b/frontend/packages/core/src/pages/system/serviceDeployment/ServiceDeployMentFooter.tsx new file mode 100644 index 00000000..b121329b --- /dev/null +++ b/frontend/packages/core/src/pages/system/serviceDeployment/ServiceDeployMentFooter.tsx @@ -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>('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>('model/local', { + method: 'DELETE', + eoBody: { recordId: record.id } + }) + .then((response: BasicResponse) => { + 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' ? ( +
+ + +
+ ) : ( +
+ + +
+ )} + + ) +} diff --git a/frontend/packages/core/src/pages/system/serviceDeployment/ServiceDeployment.tsx b/frontend/packages/core/src/pages/system/serviceDeployment/ServiceDeployment.tsx index 7f75a6e9..485a12cb 100644 --- a/frontend/packages/core/src/pages/system/serviceDeployment/ServiceDeployment.tsx +++ b/frontend/packages/core/src/pages/system/serviceDeployment/ServiceDeployment.tsx @@ -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 }) => ( - - step {index} status: {status} -
- } - > - {dot} - -) +const getIcon = (status: string) => { + switch (status) { + case 'completed': + return + case 'inProgress': + return + case 'pending': + return + case 'error': + return + 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 ( -
- {/* */} - -
+ <> +
+ + {stepItem.map((item, index) => ( + + ))} + +
+ + ) + } + ]} + > + ) }