| @@ -38,13 +38,8 @@ | |||
| margin-left: 16px; | |||
| font-size: @font-size-content; | |||
| line-height: 1.6; | |||
| white-space: pre-line; | |||
| word-break: break-all; | |||
| &--ellipsis { | |||
| .singleLine(); | |||
| } | |||
| &__text { | |||
| color: @text-color; | |||
| } | |||
| @@ -120,13 +120,10 @@ export function BasicInfoItemValue({ | |||
| } | |||
| return ( | |||
| <Typography.Text | |||
| className={classNames(myClassName, { | |||
| [`${myClassName}--ellipsis`]: ellipsis, | |||
| })} | |||
| ellipsis={{ tooltip: value }} | |||
| > | |||
| {component} | |||
| </Typography.Text> | |||
| <div className={myClassName}> | |||
| <Typography.Text ellipsis={ellipsis ? { tooltip: value } : false}> | |||
| {component} | |||
| </Typography.Text> | |||
| </div> | |||
| ); | |||
| } | |||
| @@ -33,10 +33,10 @@ | |||
| &__value { | |||
| flex: 1; | |||
| min-width: 0; | |||
| margin: 0 !important; | |||
| padding: 12px 20px 4px; | |||
| font-size: @font-size; | |||
| white-space: pre-line; | |||
| word-break: break-all; | |||
| & + & { | |||
| @@ -47,10 +47,6 @@ | |||
| padding-bottom: 12px; | |||
| } | |||
| &--ellipsis { | |||
| .singleLine(); | |||
| } | |||
| &__text { | |||
| color: @text-color; | |||
| } | |||
| @@ -1,3 +1,4 @@ | |||
| export enum PageEnum { | |||
| LOGIN = '/user/login', | |||
| Authorize = '/authorize', | |||
| } | |||
| @@ -162,7 +162,7 @@ export const useCheck = <T>(list: T[]) => { | |||
| const [selected, setSelected] = useState<T[]>([]); | |||
| const checked = useMemo(() => { | |||
| return selected.length === list.length; | |||
| return selected.length === list.length && selected.length > 0; | |||
| }, [selected, list]); | |||
| const indeterminate = useMemo(() => { | |||
| @@ -10,6 +10,7 @@ import { | |||
| getExpMetricsReq, | |||
| getExpTrainInfosReq, | |||
| } from '@/services/experiment'; | |||
| import { tableSorter } from '@/utils'; | |||
| import { to } from '@/utils/promise'; | |||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | |||
| import { useSearchParams } from '@umijs/max'; | |||
| @@ -103,7 +104,7 @@ function ExperimentComparison() { | |||
| }; | |||
| // 选择行 | |||
| const rowSelection: TableProps['rowSelection'] = { | |||
| const rowSelection: TableProps<TableData>['rowSelection'] = { | |||
| type: 'checkbox', | |||
| columnWidth: 48, | |||
| fixed: 'left', | |||
| @@ -126,8 +127,10 @@ function ExperimentComparison() { | |||
| } | |||
| }; | |||
| const columns: TableProps['columns'] = useMemo(() => { | |||
| const columns: TableProps<TableData>['columns'] = useMemo(() => { | |||
| const first: TableData | undefined = tableData[0]; | |||
| const metricsNames = first?.metrics_names ?? []; | |||
| const paramsNames = first?.params_names ?? []; | |||
| return [ | |||
| { | |||
| title: '基本信息', | |||
| @@ -175,7 +178,7 @@ function ExperimentComparison() { | |||
| { | |||
| title: `${config.title}参数`, | |||
| align: 'center', | |||
| children: first?.params_names.map((name) => ({ | |||
| children: paramsNames.map((name) => ({ | |||
| title: ( | |||
| <Tooltip title={name}> | |||
| <span>{name}</span> | |||
| @@ -187,14 +190,14 @@ function ExperimentComparison() { | |||
| align: 'center', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| sorter: (a, b) => a.params[name] - b.params[name], | |||
| sorter: (a, b) => tableSorter(a.params[name], b.params[name]), | |||
| showSorterTooltip: false, | |||
| })), | |||
| }, | |||
| { | |||
| title: `${config.title}指标`, | |||
| align: 'center', | |||
| children: first?.metrics_names.map((name) => ({ | |||
| children: metricsNames.map((name) => ({ | |||
| title: ( | |||
| <Tooltip title={name}> | |||
| <span>{name}</span> | |||
| @@ -206,7 +209,7 @@ function ExperimentComparison() { | |||
| align: 'center', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| sorter: (a, b) => a.metrics[name] - b.metrics[name], | |||
| sorter: (a, b) => tableSorter(a.metrics[name], b.metrics[name]), | |||
| showSorterTooltip: false, | |||
| })), | |||
| }, | |||
| @@ -1,6 +1,7 @@ | |||
| import SubAreaTitle from '@/components/SubAreaTitle'; | |||
| import { useCheck } from '@/hooks'; | |||
| import { getModelPageVersionsReq, getModelVersionsMetricsReq } from '@/services/dataset'; | |||
| import { tableSorter } from '@/utils'; | |||
| import { to } from '@/utils/promise'; | |||
| import tableCellRender from '@/utils/table'; | |||
| import { Checkbox, Table, Tooltip, type TablePaginationConfig, type TableProps } from 'antd'; | |||
| @@ -18,7 +19,7 @@ type TableData = { | |||
| metrics_names?: string[]; | |||
| metrics?: Record<string, number>; | |||
| params_names?: string[]; | |||
| params?: Record<string, string>; | |||
| params?: Record<string, number>; | |||
| }; | |||
| type ModelMetricsProps = { | |||
| @@ -113,14 +114,18 @@ function ModelMetrics({ resourceId, identifier, owner, version }: ModelMetricsPr | |||
| }; | |||
| // 分页切换 | |||
| const handleTableChange: TableProps['onChange'] = (pagination, _filters, _sorter, { action }) => { | |||
| const handleTableChange: TableProps<TableData>['onChange'] = ( | |||
| pagination, | |||
| _filters, | |||
| _sorter, | |||
| { action }, | |||
| ) => { | |||
| if (action === 'paginate') { | |||
| setPagination(pagination); | |||
| } | |||
| // console.log(pagination, filters, sorter, action); | |||
| }; | |||
| const rowSelection: TableProps['rowSelection'] = { | |||
| const rowSelection: TableProps<TableData>['rowSelection'] = { | |||
| type: 'checkbox', | |||
| fixed: 'left', | |||
| selectedRowKeys, | |||
| @@ -142,10 +147,12 @@ function ModelMetrics({ resourceId, identifier, owner, version }: ModelMetricsPr | |||
| }, [version, tableData]); | |||
| // 表头 | |||
| const columns: TableProps['columns'] = useMemo(() => { | |||
| const columns: TableProps<TableData>['columns'] = useMemo(() => { | |||
| const first: TableData | undefined = tableData.find( | |||
| (item) => item.metrics_names && item.metrics_names.length > 0, | |||
| ); | |||
| const metricsNames = first?.metrics_names ?? []; | |||
| const paramsNames = first?.params_names ?? []; | |||
| return [ | |||
| { | |||
| title: '基本信息', | |||
| @@ -165,7 +172,7 @@ function ModelMetrics({ resourceId, identifier, owner, version }: ModelMetricsPr | |||
| { | |||
| title: `训练参数`, | |||
| align: 'center', | |||
| children: first?.params_names?.map((name) => ({ | |||
| children: paramsNames.map((name) => ({ | |||
| title: ( | |||
| <Tooltip title={name}> | |||
| <span>{name}</span> | |||
| @@ -177,7 +184,7 @@ function ModelMetrics({ resourceId, identifier, owner, version }: ModelMetricsPr | |||
| align: 'center', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| sorter: (a, b) => a.params?.[name] ?? 0 - b.params?.[name] ?? 0, | |||
| sorter: (a, b) => tableSorter(a.params?.[name], b.params?.[name]), | |||
| showSorterTooltip: false, | |||
| })), | |||
| }, | |||
| @@ -188,12 +195,13 @@ function ModelMetrics({ resourceId, identifier, owner, version }: ModelMetricsPr | |||
| checked={metricsChecked} | |||
| indeterminate={metricsIndeterminate} | |||
| onChange={checkAllMetrics} | |||
| disabled={metricsNames.length === 0} | |||
| ></Checkbox>{' '} | |||
| <span>训练指标</span> | |||
| </div> | |||
| ), | |||
| align: 'center', | |||
| children: first?.metrics_names?.map((name) => ({ | |||
| children: metricsNames.map((name) => ({ | |||
| title: ( | |||
| <div> | |||
| <Checkbox | |||
| @@ -215,7 +223,7 @@ function ModelMetrics({ resourceId, identifier, owner, version }: ModelMetricsPr | |||
| align: 'center', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| sorter: (a, b) => a.metrics?.[name] ?? 0 - b.metrics?.[name] ?? 0, | |||
| sorter: (a, b) => tableSorter(a.metrics?.[name], b.metrics?.[name]), | |||
| showSorterTooltip: false, | |||
| })), | |||
| }, | |||
| @@ -18,6 +18,7 @@ import { | |||
| } from '@/services/modelDeployment'; | |||
| import themes from '@/styles/theme.less'; | |||
| import { formatDate } from '@/utils/date'; | |||
| import { openAntdModal } from '@/utils/modal'; | |||
| import { to } from '@/utils/promise'; | |||
| import SessionStorage from '@/utils/sessionStorage'; | |||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | |||
| @@ -37,6 +38,7 @@ import { type SearchProps } from 'antd/es/input'; | |||
| import classNames from 'classnames'; | |||
| import { useEffect, useState } from 'react'; | |||
| import ServiceRunStatusCell from '../components/ModelDeployStatusCell'; | |||
| import VersionCompareModal from '../components/VersionCompareModal'; | |||
| import { | |||
| CreateServiceVersionFrom, | |||
| ServiceData, | |||
| @@ -56,6 +58,7 @@ function ServiceInfo() { | |||
| const [inputText, setInputText] = useState(cacheState?.searchText); | |||
| const [tableData, setTableData] = useState<ServiceVersionData[]>([]); | |||
| const [total, setTotal] = useState(0); | |||
| const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]); | |||
| const [pagination, setPagination] = useState<TablePaginationConfig>( | |||
| cacheState?.pagination ?? { | |||
| current: 1, | |||
| @@ -208,11 +211,39 @@ function ServiceInfo() { | |||
| }; | |||
| // 分页切换 | |||
| const handleTableChange: TableProps['onChange'] = (pagination, _filters, _sorter, { action }) => { | |||
| const handleTableChange: TableProps<ServiceVersionData>['onChange'] = ( | |||
| pagination, | |||
| _filters, | |||
| _sorter, | |||
| { action }, | |||
| ) => { | |||
| if (action === 'paginate') { | |||
| setPagination(pagination); | |||
| } | |||
| // console.log(pagination, filters, sorter, action); | |||
| }; | |||
| // 版本对比 | |||
| const handleVersionCompare = () => { | |||
| if (selectedRowKeys.length !== 2) { | |||
| message.error('请选择两个版本进行对比'); | |||
| return; | |||
| } | |||
| openAntdModal(VersionCompareModal, { | |||
| version1: selectedRowKeys[0] as string, | |||
| version2: selectedRowKeys[1] as string, | |||
| }); | |||
| }; | |||
| // 选择行 | |||
| const rowSelection: TableProps<ServiceVersionData>['rowSelection'] = { | |||
| type: 'checkbox', | |||
| columnWidth: 48, | |||
| fixed: 'left', | |||
| selectedRowKeys, | |||
| onChange: (selectedRowKeys: React.Key[]) => { | |||
| setSelectedRowKeys(selectedRowKeys); | |||
| }, | |||
| }; | |||
| const columns: TableProps<ServiceVersionData>['columns'] = [ | |||
| @@ -379,13 +410,16 @@ function ServiceInfo() { | |||
| allowClear | |||
| ></Select> | |||
| <Button | |||
| style={{ marginRight: '20px', marginLeft: 'auto' }} | |||
| style={{ marginRight: '15px', marginLeft: 'auto' }} | |||
| type="default" | |||
| onClick={() => createServiceVersion(ServiceOperationType.Create)} | |||
| icon={<KFIcon type="icon-xinjian2" />} | |||
| > | |||
| 新增版本 | |||
| </Button> | |||
| <Button style={{ marginRight: '15px' }} type="default" onClick={handleVersionCompare}> | |||
| 版本对比 | |||
| </Button> | |||
| <Button | |||
| style={{ marginRight: 0 }} | |||
| type="default" | |||
| @@ -410,6 +444,7 @@ function ServiceInfo() { | |||
| showTotal: () => `共${total}条`, | |||
| }} | |||
| onChange={handleTableChange} | |||
| rowSelection={rowSelection} | |||
| rowKey="id" | |||
| /> | |||
| </div> | |||
| @@ -0,0 +1,117 @@ | |||
| @purple-color: #6516ff; | |||
| .title(@color, @background) { | |||
| width: 100%; | |||
| margin-bottom: 20px; | |||
| color: @color; | |||
| font-weight: 500; | |||
| font-size: @font-size; | |||
| line-height: 42px; | |||
| text-align: center; | |||
| background: @background; | |||
| .singleLine(); | |||
| } | |||
| .text() { | |||
| margin-bottom: 20px !important; | |||
| color: @text-color-secondary; | |||
| font-size: 13px; | |||
| word-break: break-all; | |||
| .singleLine(); | |||
| } | |||
| .version-container(@background) { | |||
| flex: 1; | |||
| min-width: 0; | |||
| background: @background; | |||
| border-radius: 4px; | |||
| } | |||
| .version-compare { | |||
| :global { | |||
| .ant-modal-content { | |||
| padding: 40px 40px 25px !important; | |||
| } | |||
| .ant-modal-header { | |||
| margin-bottom: 20px !important; | |||
| } | |||
| .kf-modal-title { | |||
| color: @text-color; | |||
| font-weight: 500; | |||
| font-size: 20px; | |||
| } | |||
| } | |||
| &__container { | |||
| display: flex; | |||
| flex-wrap: nowrap; | |||
| gap: 0 5px; | |||
| align-items: stretch; | |||
| height: 100%; | |||
| } | |||
| &__fields { | |||
| flex: none; | |||
| width: 117px; | |||
| padding: 0 15px; | |||
| background: white; | |||
| border: 1px solid .addAlpha(@primary-color, 0.2) []; | |||
| border-radius: 4px; | |||
| &__title { | |||
| margin-bottom: 20px; | |||
| color: @text-color; | |||
| font-size: @font-size; | |||
| line-height: 42px; | |||
| } | |||
| &__text { | |||
| .text(); | |||
| &--different { | |||
| color: @error-color; | |||
| } | |||
| } | |||
| } | |||
| &__left { | |||
| .version-container(.addAlpha(@primary-color, 0.04) []); | |||
| &__title { | |||
| .title(@primary-color, linear-gradient( | |||
| 159.9deg,rgba(138, 177, 255, 0.5) 0%, | |||
| rgba(22, 100, 255, 0.5) 100% | |||
| )); | |||
| } | |||
| &__text { | |||
| padding: 0 15px; | |||
| text-align: center; | |||
| .text(); | |||
| &--different { | |||
| color: @primary-color; | |||
| } | |||
| } | |||
| } | |||
| &__right { | |||
| .version-container(rgba(100, 30, 237, 0.04)); | |||
| &__title { | |||
| .title(@purple-color, linear-gradient( | |||
| 159.9deg, | |||
| rgba(193, 138, 255, 0.5) 0%, | |||
| rgba(146, 22, 255, 0.5) 100% | |||
| )); | |||
| } | |||
| &__text { | |||
| padding: 0 15px; | |||
| text-align: center; | |||
| .text(); | |||
| &--different { | |||
| color: @purple-color; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,193 @@ | |||
| import KFModal from '@/components/KFModal'; | |||
| import { ServiceRunStatus } from '@/enums'; | |||
| import { useComputingResource } from '@/hooks/resource'; | |||
| import { type ServiceVersionData } from '@/pages/ModelDeployment/types'; | |||
| import { getServiceVersionCompareReq } from '@/services/modelDeployment'; | |||
| import { to } from '@/utils/promise'; | |||
| import { Typography, type ModalProps } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { useEffect, useMemo, useState } from 'react'; | |||
| import { statusInfo } from '../ModelDeployStatusCell'; | |||
| import styles from './index.less'; | |||
| type CompareData = { | |||
| differences: Record<string, any>; | |||
| version1: ServiceVersionData; | |||
| version2: ServiceVersionData; | |||
| }; | |||
| type ServiceVersionDataKey = keyof ServiceVersionData; | |||
| type FiledType = { | |||
| key: ServiceVersionDataKey; | |||
| text: string; | |||
| format?: (data: any) => any; | |||
| }; | |||
| interface CreateMirrorModalProps extends Omit<ModalProps, 'onOk'> { | |||
| version1: string; | |||
| version2: string; | |||
| } | |||
| // 格式化环境变量 | |||
| const formatEnvText = (env: Record<string, string>) => { | |||
| if (!env || Object.keys(env).length === 0) { | |||
| return '--'; | |||
| } | |||
| return Object.entries(env) | |||
| .map(([key, value]) => `${key} = ${value}`) | |||
| .join(','); | |||
| }; | |||
| function VersionCompareModal({ version1, version2, ...rest }: CreateMirrorModalProps) { | |||
| const [compareData, setCompareData] = useState<CompareData | undefined>(undefined); | |||
| const getResourceDescription = useComputingResource()[2]; | |||
| const fields: FiledType[] = useMemo( | |||
| () => [ | |||
| { | |||
| key: 'service_name', | |||
| text: '服务名称', | |||
| }, | |||
| { | |||
| key: 'run_state', | |||
| text: '状态', | |||
| format: (data: any) => { | |||
| return data ? statusInfo[data as ServiceRunStatus].text : '--'; | |||
| }, | |||
| }, | |||
| { | |||
| key: 'image', | |||
| text: '镜像', | |||
| }, | |||
| { | |||
| key: 'code_config', | |||
| text: '代码配置', | |||
| format: (data: any) => { | |||
| return data?.show_value; | |||
| }, | |||
| }, | |||
| { | |||
| key: 'model', | |||
| text: '模型', | |||
| format: (data: any) => { | |||
| return data?.show_value; | |||
| }, | |||
| }, | |||
| { | |||
| key: 'resource', | |||
| text: '资源规格', | |||
| format: getResourceDescription, | |||
| }, | |||
| { | |||
| key: 'replicas', | |||
| text: '副本数', | |||
| }, | |||
| { | |||
| key: 'mount_path', | |||
| text: '挂载路径', | |||
| }, | |||
| { | |||
| key: 'url', | |||
| text: '服务URL', | |||
| }, | |||
| { | |||
| key: 'env_variables', | |||
| text: '环境变量', | |||
| format: formatEnvText, | |||
| }, | |||
| { | |||
| key: 'description', | |||
| text: '描述', | |||
| }, | |||
| ], | |||
| [getResourceDescription], | |||
| ); | |||
| useEffect(() => { | |||
| getServiceVersionCompare(); | |||
| }, []); | |||
| // 获取对比数据 | |||
| const getServiceVersionCompare = async () => { | |||
| const params = { | |||
| id1: version1, | |||
| id2: version2, | |||
| }; | |||
| const [res] = await to(getServiceVersionCompareReq(params)); | |||
| if (res && res.data) { | |||
| setCompareData(res.data); | |||
| } | |||
| }; | |||
| const { | |||
| version1: v1 = {} as ServiceVersionData, | |||
| version2: v2 = {} as ServiceVersionData, | |||
| differences = {}, | |||
| } = compareData || {}; | |||
| const isDifferent = (key: ServiceVersionDataKey) => { | |||
| const keys = Object.keys(differences); | |||
| return keys.includes(key); | |||
| }; | |||
| return ( | |||
| <KFModal | |||
| {...rest} | |||
| title="服务版本对比" | |||
| width={825} | |||
| footer={null} | |||
| className={styles['version-compare']} | |||
| > | |||
| <div className={styles['version-compare__container']}> | |||
| <div className={styles['version-compare__fields']}> | |||
| <div className={styles['version-compare__fields__title']}>基础版本号</div> | |||
| {fields.map(({ key, text }) => ( | |||
| <div | |||
| className={classNames(styles['version-compare__fields__text'], { | |||
| [styles['version-compare__fields__text--different']]: isDifferent(key), | |||
| })} | |||
| key={key} | |||
| > | |||
| {text} | |||
| </div> | |||
| ))} | |||
| </div> | |||
| <div className={styles['version-compare__left']}> | |||
| <div className={styles['version-compare__left__title']}>{v1.version}</div> | |||
| {fields.map(({ key, format }) => { | |||
| const text = format ? format(v1[key]) : v1[key]; | |||
| return ( | |||
| <div | |||
| key={key} | |||
| className={classNames(styles['version-compare__left__text'], { | |||
| [styles['version-compare__left__text--different']]: isDifferent(key), | |||
| })} | |||
| > | |||
| <Typography.Text ellipsis={{ tooltip: text }}>{text}</Typography.Text> | |||
| </div> | |||
| ); | |||
| })} | |||
| </div> | |||
| <div className={styles['version-compare__right']}> | |||
| <div className={styles['version-compare__right__title']}>{v2.version}</div> | |||
| {fields.map(({ key, format }) => { | |||
| const text = format ? format(v2[key]) : v2[key]; | |||
| return ( | |||
| <div | |||
| key={key} | |||
| className={classNames(styles['version-compare__right__text'], { | |||
| [styles['version-compare__right__text--different']]: isDifferent(key), | |||
| })} | |||
| > | |||
| <Typography.Text ellipsis={{ tooltip: text }}>{text}</Typography.Text> | |||
| </div> | |||
| ); | |||
| })} | |||
| </div> | |||
| </div> | |||
| </KFModal> | |||
| ); | |||
| } | |||
| export default VersionCompareModal; | |||
| @@ -104,3 +104,11 @@ export function getServiceVersionLogReq(params: any) { | |||
| params, | |||
| }); | |||
| } | |||
| // 获取服务版本对比 | |||
| export function getServiceVersionCompareReq(params: any) { | |||
| return request(`/api/mmp/service/serviceVersionCompare`, { | |||
| method: 'GET', | |||
| params, | |||
| }); | |||
| } | |||
| @@ -4,6 +4,7 @@ | |||
| * @Description: 工具类 | |||
| */ | |||
| import { PageEnum } from '@/enums/pagesEnums'; | |||
| import G6 from '@antv/g6'; | |||
| // 生成 8 位随机数 | |||
| @@ -218,3 +219,25 @@ export const getGitUrl = (url: string, branch: string): string => { | |||
| const gitUrl = url.replace(/\.git$/, ''); | |||
| return branch ? `${gitUrl}/tree/${branch}` : gitUrl; | |||
| }; | |||
| // 判断是否需要登录 | |||
| export const needAuth = (pathname: string) => { | |||
| return pathname !== PageEnum.LOGIN && pathname !== PageEnum.Authorize; | |||
| }; | |||
| // 表格排序 | |||
| export const tableSorter = (a: any, b: any) => { | |||
| if (b === null || b === undefined) { | |||
| return -1; | |||
| } | |||
| if (a === null || a === undefined) { | |||
| return 1; | |||
| } | |||
| if (typeof a === 'number' && typeof b === 'number') { | |||
| return a - b; | |||
| } | |||
| if (typeof a === 'string' && typeof b === 'string') { | |||
| return a.localeCompare(b); | |||
| } | |||
| return 0; | |||
| }; | |||