| @@ -0,0 +1,19 @@ | |||
| .kf-label-value { | |||
| display: flex; | |||
| align-items: flex-start; | |||
| font-size: 16px; | |||
| line-height: 1.6; | |||
| &__label { | |||
| flex: none; | |||
| width: 80px; | |||
| color: @text-color-secondary; | |||
| } | |||
| &__value { | |||
| flex: 1; | |||
| color: @text-color; | |||
| white-space: pre-line; | |||
| word-break: break-all; | |||
| } | |||
| } | |||
| @@ -0,0 +1,20 @@ | |||
| import classNames from 'classnames'; | |||
| import './index.less'; | |||
| type labelValueProps = { | |||
| label: string; | |||
| value?: any; | |||
| className?: string; | |||
| style?: React.CSSProperties; | |||
| }; | |||
| function LabelValue({ label, value, className, style }: labelValueProps) { | |||
| return ( | |||
| <div className={classNames('kf-label-value', className)} style={style}> | |||
| <div className="kf-label-value__label">{label}</div> | |||
| <div className="kf-label-value__value">{value ?? '--'}</div> | |||
| </div> | |||
| ); | |||
| } | |||
| export default LabelValue; | |||
| @@ -1,4 +1,4 @@ | |||
| .modal-title { | |||
| .kf-modal-title { | |||
| display: flex; | |||
| align-items: center; | |||
| color: @primary-color; | |||
| @@ -6,7 +6,7 @@ | |||
| import classNames from 'classnames'; | |||
| import React from 'react'; | |||
| import styles from './index.less'; | |||
| import './index.less'; | |||
| type ModalTitleProps = { | |||
| title: React.ReactNode; | |||
| @@ -17,8 +17,8 @@ type ModalTitleProps = { | |||
| function ModalTitle({ title, image, style, className }: ModalTitleProps) { | |||
| return ( | |||
| <div className={classNames(styles['modal-title'], className)} style={style}> | |||
| {image && <img className={styles['modal-title__image']} src={image} alt="" />} | |||
| <div className={classNames('kf-modal-title', className)} style={style}> | |||
| {image && <img className={'kf-modal-title__image'} src={image} alt="" />} | |||
| {title} | |||
| </div> | |||
| ); | |||
| @@ -1,3 +1,4 @@ | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import ModelEvolution from '@/pages/Model/components/ModelEvolution'; | |||
| import { to } from '@/utils/promise'; | |||
| import { useParams, useSearchParams } from '@umijs/max'; | |||
| @@ -7,21 +8,21 @@ import { ResourceData, ResourceType, resourceConfig } from '../../config'; | |||
| import ResourceVersion from '../ResourceVersion'; | |||
| import styles from './index.less'; | |||
| export enum ResourceInfoTabKeys { | |||
| Introduction = 'introduction', | |||
| Version = 'version', | |||
| Evolution = 'evolution', | |||
| } | |||
| type ResourceIntroProps = { | |||
| resourceType: ResourceType; | |||
| }; | |||
| enum TabKeys { | |||
| Introduction = '1', | |||
| Version = '2', | |||
| Evolution = '3', | |||
| } | |||
| const ResourceIntro = ({ resourceType }: ResourceIntroProps) => { | |||
| const [info, setInfo] = useState<ResourceData>({} as ResourceData); | |||
| const locationParams = useParams(); | |||
| const [searchParams] = useSearchParams(); | |||
| const defaultTab = searchParams.get('tab') || '1'; | |||
| const defaultTab = searchParams.get('tab') || ResourceInfoTabKeys.Introduction; | |||
| let versionParam = searchParams.get('version'); | |||
| const [versionList, setVersionList] = useState([]); | |||
| const [version, setVersion] = useState<string | undefined>(undefined); | |||
| @@ -74,8 +75,9 @@ const ResourceIntro = ({ resourceType }: ResourceIntroProps) => { | |||
| const items = [ | |||
| { | |||
| key: TabKeys.Introduction, | |||
| key: ResourceInfoTabKeys.Introduction, | |||
| label: `${typeName}简介`, | |||
| icon: <KFIcon type="icon-moxingjianjie" />, | |||
| children: ( | |||
| <> | |||
| <div className={styles['resource-intro__title']}>简介</div> | |||
| @@ -84,8 +86,9 @@ const ResourceIntro = ({ resourceType }: ResourceIntroProps) => { | |||
| ), | |||
| }, | |||
| { | |||
| key: TabKeys.Version, | |||
| key: ResourceInfoTabKeys.Version, | |||
| label: `${typeName}文件/版本`, | |||
| icon: <KFIcon type="icon-moxingwenjian" />, | |||
| children: ( | |||
| <ResourceVersion | |||
| resourceType={resourceType} | |||
| @@ -94,7 +97,7 @@ const ResourceIntro = ({ resourceType }: ResourceIntroProps) => { | |||
| isPublic={info.available_range === 1} | |||
| versionList={versionList} | |||
| version={version} | |||
| isActive={activeTab === TabKeys.Version} | |||
| isActive={activeTab === ResourceInfoTabKeys.Version} | |||
| getVersionList={getVersionList} | |||
| onVersionChange={handleVersionChange} | |||
| ></ResourceVersion> | |||
| @@ -104,15 +107,16 @@ const ResourceIntro = ({ resourceType }: ResourceIntroProps) => { | |||
| if (resourceType === ResourceType.Model) { | |||
| items.push({ | |||
| key: TabKeys.Evolution, | |||
| key: ResourceInfoTabKeys.Evolution, | |||
| label: `模型演化`, | |||
| icon: <KFIcon type="icon-moxingyanhua1" />, | |||
| children: ( | |||
| <ModelEvolution | |||
| resourceId={resourceId} | |||
| resourceName={info.name} | |||
| versionList={versionList} | |||
| version={version} | |||
| isActive={activeTab === TabKeys.Evolution} | |||
| isActive={activeTab === ResourceInfoTabKeys.Evolution} | |||
| onVersionChange={handleVersionChange} | |||
| ></ModelEvolution> | |||
| ), | |||
| @@ -27,14 +27,12 @@ type Log = { | |||
| const scrollToBottom = (smooth: boolean = true) => { | |||
| const element = document.getElementsByClassName('ant-tabs-content-holder')?.[0]; | |||
| if (element) { | |||
| if (smooth) { | |||
| element.scrollTo({ | |||
| top: element.scrollHeight, | |||
| behavior: 'smooth', | |||
| }); | |||
| } else { | |||
| element.scrollTo({ top: element.scrollHeight }); | |||
| } | |||
| const optons: ScrollToOptions = { | |||
| top: element.scrollHeight, | |||
| behavior: smooth ? 'smooth' : 'instant', | |||
| }; | |||
| element.scrollTo(optons); | |||
| } | |||
| }; | |||
| @@ -153,13 +151,9 @@ function LogGroup({ | |||
| )} | |||
| <div className={styles['log-group__more-button']}> | |||
| {showMoreBtn && ( | |||
| <Button | |||
| type="text" | |||
| style={{ color: 'white' }} | |||
| icon={<DoubleRightOutlined rotate={90} />} | |||
| onClick={loadMore} | |||
| > | |||
| <Button type="text" style={{ color: 'white' }} onClick={loadMore}> | |||
| 更多 | |||
| <DoubleRightOutlined rotate={90} /> | |||
| </Button> | |||
| )} | |||
| </div> | |||
| @@ -5,6 +5,7 @@ import themes from '@/styles/theme.less'; | |||
| import { to } from '@/utils/promise'; | |||
| import G6, { G6GraphEvent, Graph, Item } from '@antv/g6'; | |||
| // @ts-ignore | |||
| import { ResourceInfoTabKeys } from '@/pages/Dataset/components/ResourceIntro'; | |||
| import { Flex, Select } from 'antd'; | |||
| import { useEffect, useRef, useState } from 'react'; | |||
| import GraphLegand from '../GraphLegand'; | |||
| @@ -178,7 +179,7 @@ function ModelEvolution({ | |||
| case NodeType.children: | |||
| case NodeType.parent: { | |||
| const { current_model_id, version } = model as ModelDepsData; | |||
| url = `${origin}/dataset/model/${current_model_id}?tab=3&version=${version}`; | |||
| url = `${origin}/dataset/model/${current_model_id}?tab=${ResourceInfoTabKeys.Evolution}&version=${version}`; | |||
| break; | |||
| } | |||
| case NodeType.project: { | |||
| @@ -189,7 +190,7 @@ function ModelEvolution({ | |||
| case NodeType.trainDataset: | |||
| case NodeType.testDataset: { | |||
| const { dataset_id, dataset_version } = model as TrainDataset; | |||
| url = `${origin}/dataset/dataset/${dataset_id}?tab=2&version=${dataset_version}`; | |||
| url = `${origin}/dataset/dataset/${dataset_id}?tab=${ResourceInfoTabKeys.Version}&version=${dataset_version}`; | |||
| break; | |||
| } | |||
| default: | |||
| @@ -9,37 +9,30 @@ | |||
| padding: 30px 30px 0; | |||
| background-color: white; | |||
| border-radius: 10px; | |||
| } | |||
| &__basic { | |||
| &__item { | |||
| display: flex; | |||
| align-items: flex-start; | |||
| font-size: 16px; | |||
| line-height: 1.6; | |||
| &__tabs { | |||
| flex: 1; | |||
| min-height: 0; | |||
| margin-top: 20px; | |||
| padding-bottom: 10px; | |||
| .label { | |||
| flex: none; | |||
| width: 80px; | |||
| color: @text-color-secondary; | |||
| } | |||
| :global { | |||
| .ant-tabs { | |||
| height: 100%; | |||
| .ant-tabs-nav { | |||
| margin-bottom: 10px; | |||
| } | |||
| .ant-tabs-content { | |||
| height: 100%; | |||
| .value { | |||
| flex: 1; | |||
| color: @text-color; | |||
| white-space: pre-line; | |||
| word-break: break-all; | |||
| .ant-tabs-tabpane { | |||
| height: 100%; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| &__guide { | |||
| flex: 1; | |||
| margin-top: 10px; | |||
| padding: 10px; | |||
| overflow-y: auto; | |||
| color: white; | |||
| white-space: pre-wrap; | |||
| background-color: rgba(0, 0, 0, 0.85); | |||
| } | |||
| } | |||
| @@ -6,16 +6,13 @@ | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import PageTitle from '@/components/PageTitle'; | |||
| import SubAreaTitle from '@/components/SubAreaTitle'; | |||
| import { useComputingResource } from '@/hooks/resource'; | |||
| import { useSessionStorage } from '@/hooks/sessionStorage'; | |||
| import { getModelDeploymentDocsReq } from '@/services/modelDeployment'; | |||
| import { formatDate } from '@/utils/date'; | |||
| import { to } from '@/utils/promise'; | |||
| import { modelDeploymentInfoKey } from '@/utils/sessionStorage'; | |||
| import { Col, Row, Tabs, type TabsProps } from 'antd'; | |||
| import { pick } from 'lodash'; | |||
| import { useEffect, useState } from 'react'; | |||
| import ModelDeploymentStatusCell from '../components/ModelDeployStatusCell'; | |||
| import { Tabs, type TabsProps } from 'antd'; | |||
| import { useState } from 'react'; | |||
| import BasicInfo from '../components/BasicInfo'; | |||
| import ServerLog from '../components/ServerLog'; | |||
| import UserGuide from '../components/UserGuide'; | |||
| import { ModelDeploymentData } from '../types'; | |||
| import styles from './index.less'; | |||
| @@ -25,24 +22,6 @@ export enum ModelDeploymentTabKey { | |||
| Log = 'Log', | |||
| } | |||
| const tabItems = [ | |||
| { | |||
| key: ModelDeploymentTabKey.Predict, | |||
| label: '预测', | |||
| icon: <KFIcon type="icon-yuce" />, | |||
| }, | |||
| { | |||
| key: ModelDeploymentTabKey.Guide, | |||
| label: '调用指南', | |||
| icon: <KFIcon type="icon-tiaoyongzhinan" />, | |||
| }, | |||
| { | |||
| key: ModelDeploymentTabKey.Log, | |||
| label: '服务日志', | |||
| icon: <KFIcon type="icon-fuwurizhi" />, | |||
| }, | |||
| ]; | |||
| function ModelDeploymentInfo() { | |||
| const [activeTab, setActiveTab] = useState<string>(ModelDeploymentTabKey.Predict); | |||
| const [modelDeployementInfo] = useSessionStorage<ModelDeploymentData | undefined>( | |||
| @@ -50,38 +29,32 @@ function ModelDeploymentInfo() { | |||
| true, | |||
| undefined, | |||
| ); | |||
| const getResourceDescription = useComputingResource()[2]; | |||
| const [docs, setDocs] = useState(''); | |||
| useEffect(() => { | |||
| getModelDeploymentDocs(); | |||
| }, [modelDeployementInfo]); | |||
| // 获取模型部署文档 | |||
| const getModelDeploymentDocs = async () => { | |||
| const params = pick(modelDeployementInfo, ['service_id', 'service_ins_id']); | |||
| const [res] = await to(getModelDeploymentDocsReq(params)); | |||
| if (res && res.data && res.data.docs) { | |||
| setDocs(JSON.stringify(res.data.docs, null, 2)); | |||
| } | |||
| }; | |||
| const tabItems = [ | |||
| { | |||
| key: ModelDeploymentTabKey.Predict, | |||
| label: '预测', | |||
| icon: <KFIcon type="icon-yuce" />, | |||
| }, | |||
| { | |||
| key: ModelDeploymentTabKey.Guide, | |||
| label: '调用指南', | |||
| icon: <KFIcon type="icon-tiaoyongzhinan" />, | |||
| children: <UserGuide info={modelDeployementInfo}></UserGuide>, | |||
| }, | |||
| { | |||
| key: ModelDeploymentTabKey.Log, | |||
| label: '服务日志', | |||
| icon: <KFIcon type="icon-fuwurizhi" />, | |||
| children: <ServerLog info={modelDeployementInfo}></ServerLog>, | |||
| }, | |||
| ]; | |||
| // 切换 Tab,重置数据 | |||
| const hanleTabChange: TabsProps['onChange'] = (value) => { | |||
| setActiveTab(value); | |||
| }; | |||
| // 格式化环境变量 | |||
| const formatEnvText = () => { | |||
| if (!modelDeployementInfo?.env) { | |||
| return '--'; | |||
| } | |||
| const env = modelDeployementInfo.env; | |||
| return Object.entries(env) | |||
| .map(([key, value]) => `${key}: ${value}`) | |||
| .join('\n'); | |||
| }; | |||
| return ( | |||
| <div className={styles['model-deployment-info']}> | |||
| <PageTitle title="服务详情"></PageTitle> | |||
| @@ -91,125 +64,10 @@ function ModelDeploymentInfo() { | |||
| image={require('@/assets/img/mirror-basic.png')} | |||
| style={{ marginBottom: '26px' }} | |||
| ></SubAreaTitle> | |||
| <div className={styles['model-deployment-info__basic']}> | |||
| <Row gutter={40} style={{ marginBottom: '20px' }}> | |||
| <Col span={10}> | |||
| <div className={styles['model-deployment-info__basic__item']}> | |||
| <div className={styles['label']}>服务名称:</div> | |||
| <div className={styles['value']}>{modelDeployementInfo?.service_name ?? '--'}</div> | |||
| </div> | |||
| </Col> | |||
| <Col span={10}> | |||
| <div className={styles['model-deployment-info__basic__item']}> | |||
| <div className={styles['label']}>镜 像:</div> | |||
| <div className={styles['value']}>{modelDeployementInfo?.image ?? '--'}</div> | |||
| </div> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={40} style={{ marginBottom: '20px' }}> | |||
| <Col span={10}> | |||
| <div className={styles['model-deployment-info__basic__item']}> | |||
| <div className={styles['label']}>状 态:</div> | |||
| <div className={styles['value']}> | |||
| {ModelDeploymentStatusCell(modelDeployementInfo?.status)} | |||
| </div> | |||
| </div> | |||
| </Col> | |||
| <Col span={10}> | |||
| <div className={styles['model-deployment-info__basic__item']}> | |||
| <div className={styles['label']}>模 型:</div> | |||
| <div className={styles['value']}> | |||
| {modelDeployementInfo?.model?.show_value ?? '--'} | |||
| </div> | |||
| </div> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={40} style={{ marginBottom: '20px' }}> | |||
| <Col span={10}> | |||
| <div className={styles['model-deployment-info__basic__item']}> | |||
| <div className={styles['label']}>创建人:</div> | |||
| <div className={styles['value']}>{modelDeployementInfo?.created_by ?? '--'}</div> | |||
| </div> | |||
| </Col> | |||
| <Col span={10}> | |||
| <div className={styles['model-deployment-info__basic__item']}> | |||
| <div className={styles['label']}>挂载路径:</div> | |||
| <div className={styles['value']}>{modelDeployementInfo?.model_path ?? '--'}</div> | |||
| </div> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={40} style={{ marginBottom: '20px' }}> | |||
| <Col span={10}> | |||
| <div className={styles['model-deployment-info__basic__item']}> | |||
| <div className={styles['label']}>API URL:</div> | |||
| <div className={styles['value']}>{modelDeployementInfo?.url ?? '--'}</div> | |||
| </div> | |||
| </Col> | |||
| <Col span={10}> | |||
| <div className={styles['model-deployment-info__basic__item']}> | |||
| <div className={styles['label']}>副本数量:</div> | |||
| <div className={styles['value']}>{modelDeployementInfo?.replicas ?? '--'}</div> | |||
| </div> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={40} style={{ marginBottom: '20px' }}> | |||
| <Col span={10}> | |||
| <div className={styles['model-deployment-info__basic__item']}> | |||
| <div className={styles['label']}>创建时间:</div> | |||
| <div className={styles['value']}> | |||
| {modelDeployementInfo?.create_time | |||
| ? formatDate(modelDeployementInfo.create_time) | |||
| : '--'} | |||
| </div> | |||
| </div> | |||
| </Col> | |||
| <Col span={10}> | |||
| <div className={styles['model-deployment-info__basic__item']}> | |||
| <div className={styles['label']}>更新时间:</div> | |||
| <div className={styles['value']}> | |||
| {modelDeployementInfo?.update_time | |||
| ? formatDate(modelDeployementInfo.update_time) | |||
| : '--'} | |||
| </div> | |||
| </div> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={40} style={{ marginBottom: '20px' }}> | |||
| <Col span={10}> | |||
| <div className={styles['model-deployment-info__basic__item']}> | |||
| <div className={styles['label']}>环境变量:</div> | |||
| <div className={styles['value']}>{formatEnvText()}</div> | |||
| </div> | |||
| </Col> | |||
| <Col span={10}> | |||
| <div className={styles['model-deployment-info__basic__item']}> | |||
| <div className={styles['label']}>资源规格:</div> | |||
| <div className={styles['value']}> | |||
| {modelDeployementInfo?.resource | |||
| ? getResourceDescription(modelDeployementInfo.resource) | |||
| : '--'} | |||
| </div> | |||
| </div> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={40}> | |||
| <Col span={18}> | |||
| <div className={styles['model-deployment-info__basic__item']}> | |||
| <div className={styles['label']}>描 述:</div> | |||
| <div className={styles['value']}>{modelDeployementInfo?.description ?? '--'}</div> | |||
| </div> | |||
| </Col> | |||
| </Row> | |||
| <BasicInfo info={modelDeployementInfo} /> | |||
| <div className={styles['model-deployment-info__content__tabs']}> | |||
| <Tabs activeKey={activeTab} items={tabItems} onChange={hanleTabChange} /> | |||
| </div> | |||
| <Tabs | |||
| activeKey={activeTab} | |||
| style={{ marginTop: '20px' }} | |||
| items={tabItems} | |||
| onChange={hanleTabChange} | |||
| /> | |||
| {activeTab === ModelDeploymentTabKey.Guide && ( | |||
| <div className={styles['model-deployment-info__guide']}>{docs}</div> | |||
| )} | |||
| </div> | |||
| </div> | |||
| ); | |||
| @@ -0,0 +1,91 @@ | |||
| import LabelValue from '@/components/LabelValue'; | |||
| import { useComputingResource } from '@/hooks/resource'; | |||
| import { ModelDeploymentData } from '@/pages/ModelDeployment/types'; | |||
| import { formatDate } from '@/utils/date'; | |||
| import { Col, Row } from 'antd'; | |||
| import ModelDeploymentStatusCell from '../ModelDeployStatusCell'; | |||
| type BasicInfoProps = { | |||
| info?: ModelDeploymentData; | |||
| }; | |||
| function BasicInfo({ info }: BasicInfoProps) { | |||
| const getResourceDescription = useComputingResource()[2]; | |||
| // 格式化环境变量 | |||
| const formatEnvText = () => { | |||
| if (!info?.env) { | |||
| return '--'; | |||
| } | |||
| const env = info.env; | |||
| return Object.entries(env) | |||
| .map(([key, value]) => `${key}: ${value}`) | |||
| .join('\n'); | |||
| }; | |||
| return ( | |||
| <div> | |||
| <Row gutter={40} style={{ marginBottom: '20px' }}> | |||
| <Col span={10}> | |||
| <LabelValue label="服务名称:" value={info?.service_name}></LabelValue> | |||
| </Col> | |||
| <Col span={10}> | |||
| <LabelValue label="镜 像:" value={info?.image}></LabelValue> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={40} style={{ marginBottom: '20px' }}> | |||
| <Col span={10}> | |||
| <LabelValue | |||
| label="状 态:" | |||
| value={ModelDeploymentStatusCell(info?.status)} | |||
| ></LabelValue> | |||
| </Col> | |||
| <Col span={10}> | |||
| <LabelValue label="模 型:" value={info?.model?.show_value}></LabelValue> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={40} style={{ marginBottom: '20px' }}> | |||
| <Col span={10}> | |||
| <LabelValue label="创建人:" value={info?.created_by}></LabelValue> | |||
| </Col> | |||
| <Col span={10}> | |||
| <LabelValue label="挂载路径:" value={info?.model_path}></LabelValue> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={40} style={{ marginBottom: '20px' }}> | |||
| <Col span={10}> | |||
| <LabelValue label="API URL:" value={info?.url}></LabelValue> | |||
| </Col> | |||
| <Col span={10}> | |||
| <LabelValue label="副本数量:" value={info?.replicas}></LabelValue> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={40} style={{ marginBottom: '20px' }}> | |||
| <Col span={10}> | |||
| <LabelValue label="创建时间:" value={formatDate(info?.create_time)}></LabelValue> | |||
| </Col> | |||
| <Col span={10}> | |||
| <LabelValue label="更新时间:" value={formatDate(info?.update_time)}></LabelValue> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={40} style={{ marginBottom: '20px' }}> | |||
| <Col span={10}> | |||
| <LabelValue label="环境变量:" value={formatEnvText()}></LabelValue> | |||
| </Col> | |||
| <Col span={10}> | |||
| <LabelValue | |||
| label="资源规格:" | |||
| value={info?.resource ? getResourceDescription(info.resource) : '--'} | |||
| ></LabelValue> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={40}> | |||
| <Col span={18}> | |||
| <LabelValue label="描 述:" value={info?.description}></LabelValue> | |||
| </Col> | |||
| </Row> | |||
| </div> | |||
| ); | |||
| } | |||
| export default BasicInfo; | |||
| @@ -0,0 +1,16 @@ | |||
| .server-log { | |||
| height: 100%; | |||
| &__data { | |||
| height: calc(100% - 42px); | |||
| margin-top: 10px; | |||
| padding: 10px; | |||
| overflow-y: auto; | |||
| color: white; | |||
| white-space: pre-wrap; | |||
| background-color: rgba(0, 0, 0, 0.85); | |||
| &__more { | |||
| padding: 10px 0; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,147 @@ | |||
| import { ModelDeploymentData } from '@/pages/ModelDeployment/types'; | |||
| import { getModelDeploymentLogReq } from '@/services/modelDeployment'; | |||
| import { to } from '@/utils/promise'; | |||
| import { DoubleRightOutlined } from '@ant-design/icons'; | |||
| import { Button, DatePicker, type TimeRangePickerProps } from 'antd'; | |||
| import dayjs from 'dayjs'; | |||
| import { pick } from 'lodash'; | |||
| import { useEffect, useState } from 'react'; | |||
| import styles from './index.less'; | |||
| const { RangePicker } = DatePicker; | |||
| // 滚动到底部 | |||
| // const scrollToBottom = (smooth: boolean = true) => { | |||
| // const element = document.getElementById('server-log'); | |||
| // if (element) { | |||
| // const optons: ScrollToOptions = { | |||
| // top: element.scrollHeight, | |||
| // behavior: smooth ? 'smooth' : 'instant', | |||
| // }; | |||
| // element.scrollTo(optons); | |||
| // } | |||
| // }; | |||
| type LogData = { | |||
| log_content: string; | |||
| end_time: string; | |||
| start_time: string; | |||
| }; | |||
| type ServerLogProps = { | |||
| info?: ModelDeploymentData; | |||
| }; | |||
| function ServerLog({ info }: ServerLogProps) { | |||
| const [dateRange, setDateRange] = useState<NonNullable<TimeRangePickerProps['value']>>([ | |||
| dayjs().add(-1, 'h'), | |||
| dayjs(), | |||
| ]); | |||
| const [logTime, setLogTime] = useState<[string, string]>([ | |||
| `${dateRange[0]!.valueOf() * Math.pow(10, 6)}`, | |||
| `${dateRange[1]!.valueOf() * Math.pow(10, 6)}`, | |||
| ]); | |||
| const [logData, setLogData] = useState<LogData[]>([]); | |||
| const [hasMore, setHasMore] = useState(false); | |||
| const rangePresets: TimeRangePickerProps['presets'] = [ | |||
| { label: '最近 1 小时', value: [dayjs().add(-1, 'h'), dayjs()] }, | |||
| { label: '最近 2 小时', value: [dayjs().add(-2, 'h'), dayjs()] }, | |||
| { label: '最近 3 小时', value: [dayjs().add(-3, 'h'), dayjs()] }, | |||
| { label: '最近 1 天', value: [dayjs().add(-1, 'd'), dayjs()] }, | |||
| { label: '最近 2 天', value: [dayjs().add(-2, 'd'), dayjs()] }, | |||
| { label: '最近 7 天', value: [dayjs().add(-7, 'd'), dayjs()] }, | |||
| { label: '最近 14 天', value: [dayjs().add(-14, 'd'), dayjs()] }, | |||
| { label: '最近 30 天', value: [dayjs().add(-30, 'd'), dayjs()] }, | |||
| ]; | |||
| useEffect(() => { | |||
| getModelDeploymentLog(); | |||
| }, [info, logTime]); | |||
| // 获取模型部署日志 | |||
| const getModelDeploymentLog = async () => { | |||
| if (info && logTime && logTime.length === 2) { | |||
| const params = { | |||
| start_time: logTime[0], | |||
| end_time: logTime[1], | |||
| ...pick(info, ['service_id', 'service_ins_id']), | |||
| }; | |||
| const [res] = await to(getModelDeploymentLogReq(params)); | |||
| if (res && res.data) { | |||
| setLogData((prev) => [...prev, res.data]); | |||
| setHasMore(!!res.data.log_content); | |||
| // setTimeout(() => { | |||
| // scrollToBottom(); | |||
| // }, 100); | |||
| } | |||
| } | |||
| }; | |||
| // 搜索 | |||
| const handleSearch = () => { | |||
| setLogData([]); | |||
| setHasMore(false); | |||
| setLogTime([ | |||
| `${dateRange[0]!.valueOf() * Math.pow(10, 6)}`, | |||
| `${dateRange[1]!.valueOf() * Math.pow(10, 6)}`, | |||
| ]); | |||
| }; | |||
| // 加载更多日志 | |||
| const loadMoreLog = () => { | |||
| const lastLog = logData[logData.length - 1]; | |||
| setLogTime([lastLog.start_time, lastLog.end_time]); | |||
| }; | |||
| // 禁止选择今天之后和之前31天的日期 | |||
| const disabledDate: TimeRangePickerProps['disabledDate'] = (currentDate) => { | |||
| return ( | |||
| Date.now() - currentDate.valueOf() < 0 || | |||
| Date.now() - currentDate.valueOf() > 31 * 24 * 60 * 60 * 1000 | |||
| ); | |||
| }; | |||
| // 处理日期变化 | |||
| const handleRangeChange: TimeRangePickerProps['onChange'] = (dates) => { | |||
| if (dates) { | |||
| setDateRange(dates); | |||
| } | |||
| }; | |||
| return ( | |||
| <div className={styles['server-log']}> | |||
| <div> | |||
| <RangePicker | |||
| presets={rangePresets} | |||
| showTime | |||
| value={dateRange} | |||
| format="YYYY-MM-DD HH:mm:ss" | |||
| onChange={handleRangeChange} | |||
| allowClear={false} | |||
| disabledDate={disabledDate} | |||
| /> | |||
| <Button type="default" style={{ marginLeft: '20px' }} onClick={handleSearch}> | |||
| 查询 | |||
| </Button> | |||
| </div> | |||
| {logData.length > 0 && ( | |||
| <div className={styles['server-log__data']} id="server-log"> | |||
| <div>{logData.map((v) => v.log_content).join('') || '暂无日志'}</div> | |||
| {hasMore && ( | |||
| <Button | |||
| type="text" | |||
| className={styles['server-log__data__more']} | |||
| style={{ color: 'white' }} | |||
| onClick={loadMoreLog} | |||
| > | |||
| 更多 | |||
| <DoubleRightOutlined rotate={90} /> | |||
| </Button> | |||
| )} | |||
| </div> | |||
| )} | |||
| </div> | |||
| ); | |||
| } | |||
| export default ServerLog; | |||
| @@ -0,0 +1,8 @@ | |||
| .user-guide { | |||
| height: 100%; | |||
| padding: 10px; | |||
| overflow-y: auto; | |||
| color: white; | |||
| white-space: pre-wrap; | |||
| background-color: rgba(0, 0, 0, 0.85); | |||
| } | |||
| @@ -0,0 +1,32 @@ | |||
| import { ModelDeploymentData } from '@/pages/ModelDeployment/types'; | |||
| import { getModelDeploymentDocsReq } from '@/services/modelDeployment'; | |||
| import { to } from '@/utils/promise'; | |||
| import { pick } from 'lodash'; | |||
| import { useEffect, useState } from 'react'; | |||
| import styles from './index.less'; | |||
| type UserGuideProps = { | |||
| info?: ModelDeploymentData; | |||
| }; | |||
| function UserGuide({ info }: UserGuideProps) { | |||
| const [docs, setDocs] = useState(''); | |||
| useEffect(() => { | |||
| getModelDeploymentDocs(); | |||
| }, [info]); | |||
| // 获取模型部署文档 | |||
| const getModelDeploymentDocs = async () => { | |||
| if (info) { | |||
| const params = pick(info, ['service_id', 'service_ins_id']); | |||
| const [res] = await to(getModelDeploymentDocsReq(params)); | |||
| if (res && res.data && res.data.docs) { | |||
| setDocs(JSON.stringify(res.data.docs, null, 2)); | |||
| } | |||
| } | |||
| }; | |||
| return <div className={styles['user-guide']}>{docs}</div>; | |||
| } | |||
| export default UserGuide; | |||
| @@ -67,3 +67,11 @@ export function getModelDeploymentDocsReq(data: any) { | |||
| data, | |||
| }); | |||
| } | |||
| // 获取模型部署日志 | |||
| export function getModelDeploymentLogReq(data: any) { | |||
| return request(`/api/v1/model/getAppLog`, { | |||
| method: 'POST', | |||
| data, | |||
| }); | |||
| } | |||
| @@ -71,7 +71,13 @@ export const gotoLoginPage = (toHome: boolean = true) => { | |||
| } | |||
| }; | |||
| // 上传文件校验 | |||
| /** | |||
| * 验证文件上传 | |||
| * | |||
| * @param {UploadFile[]} files - The array of uploaded files. | |||
| * @param {boolean} [required=true] - Flag indicating if files are required. | |||
| * @return {boolean} Returns true if all files are valid, false otherwise. | |||
| */ | |||
| export const validateUploadFiles = (files: UploadFile[], required: boolean = true): boolean => { | |||
| if (required && files.length === 0) { | |||
| message.error('请上传文件'); | |||
| @@ -95,3 +101,18 @@ export const validateUploadFiles = (files: UploadFile[], required: boolean = tru | |||
| }); | |||
| return !hasError; | |||
| }; | |||
| /** | |||
| * 滚动到底部 | |||
| * | |||
| * @param {boolean} smooth - Determines if the scroll should be smooth | |||
| */ | |||
| export const scrollToBottom = (element: HTMLElement | null, smooth: boolean = true) => { | |||
| if (element) { | |||
| const optons: ScrollToOptions = { | |||
| top: element.scrollHeight, | |||
| behavior: smooth ? 'smooth' : 'instant', | |||
| }; | |||
| element.scrollTo(optons); | |||
| } | |||
| }; | |||