diff --git a/react-ui/src/components/BasicInfo/index.less b/react-ui/src/components/BasicInfo/index.less index ffeb3d3d..e4570868 100644 --- a/react-ui/src/components/BasicInfo/index.less +++ b/react-ui/src/components/BasicInfo/index.less @@ -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; } diff --git a/react-ui/src/components/BasicInfo/index.tsx b/react-ui/src/components/BasicInfo/index.tsx index 60cd5b57..bd07db34 100644 --- a/react-ui/src/components/BasicInfo/index.tsx +++ b/react-ui/src/components/BasicInfo/index.tsx @@ -120,13 +120,10 @@ export function BasicInfoItemValue({ } return ( - - {component} - +
+ + {component} + +
); } diff --git a/react-ui/src/components/BasicTableInfo/index.less b/react-ui/src/components/BasicTableInfo/index.less index cc3d0984..314b05ca 100644 --- a/react-ui/src/components/BasicTableInfo/index.less +++ b/react-ui/src/components/BasicTableInfo/index.less @@ -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; } diff --git a/react-ui/src/enums/pagesEnums.ts b/react-ui/src/enums/pagesEnums.ts index 756d3325..d11eb8b5 100644 --- a/react-ui/src/enums/pagesEnums.ts +++ b/react-ui/src/enums/pagesEnums.ts @@ -1,3 +1,4 @@ export enum PageEnum { LOGIN = '/user/login', + Authorize = '/authorize', } diff --git a/react-ui/src/hooks/index.ts b/react-ui/src/hooks/index.ts index 6f5bdbbc..f5ef64af 100644 --- a/react-ui/src/hooks/index.ts +++ b/react-ui/src/hooks/index.ts @@ -162,7 +162,7 @@ export const useCheck = (list: T[]) => { const [selected, setSelected] = useState([]); const checked = useMemo(() => { - return selected.length === list.length; + return selected.length === list.length && selected.length > 0; }, [selected, list]); const indeterminate = useMemo(() => { diff --git a/react-ui/src/pages/Experiment/Comparison/index.tsx b/react-ui/src/pages/Experiment/Comparison/index.tsx index 1667a064..1edfd0a3 100644 --- a/react-ui/src/pages/Experiment/Comparison/index.tsx +++ b/react-ui/src/pages/Experiment/Comparison/index.tsx @@ -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['rowSelection'] = { type: 'checkbox', columnWidth: 48, fixed: 'left', @@ -126,8 +127,10 @@ function ExperimentComparison() { } }; - const columns: TableProps['columns'] = useMemo(() => { + const columns: TableProps['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: ( {name} @@ -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: ( {name} @@ -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, })), }, diff --git a/react-ui/src/pages/Model/components/ModelMetrics/index.tsx b/react-ui/src/pages/Model/components/ModelMetrics/index.tsx index 7ffd7fbb..d1f8d2d6 100644 --- a/react-ui/src/pages/Model/components/ModelMetrics/index.tsx +++ b/react-ui/src/pages/Model/components/ModelMetrics/index.tsx @@ -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; params_names?: string[]; - params?: Record; + params?: Record; }; type ModelMetricsProps = { @@ -113,14 +114,18 @@ function ModelMetrics({ resourceId, identifier, owner, version }: ModelMetricsPr }; // 分页切换 - const handleTableChange: TableProps['onChange'] = (pagination, _filters, _sorter, { action }) => { + const handleTableChange: TableProps['onChange'] = ( + pagination, + _filters, + _sorter, + { action }, + ) => { if (action === 'paginate') { setPagination(pagination); } - // console.log(pagination, filters, sorter, action); }; - const rowSelection: TableProps['rowSelection'] = { + const rowSelection: TableProps['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['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: ( {name} @@ -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} >{' '} 训练指标 ), align: 'center', - children: first?.metrics_names?.map((name) => ({ + children: metricsNames.map((name) => ({ title: (
a.metrics?.[name] ?? 0 - b.metrics?.[name] ?? 0, + sorter: (a, b) => tableSorter(a.metrics?.[name], b.metrics?.[name]), showSorterTooltip: false, })), }, diff --git a/react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx b/react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx index 42b80e87..07d40de5 100644 --- a/react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx +++ b/react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx @@ -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([]); const [total, setTotal] = useState(0); + const [selectedRowKeys, setSelectedRowKeys] = useState([]); const [pagination, setPagination] = useState( cacheState?.pagination ?? { current: 1, @@ -208,11 +211,39 @@ function ServiceInfo() { }; // 分页切换 - const handleTableChange: TableProps['onChange'] = (pagination, _filters, _sorter, { action }) => { + const handleTableChange: TableProps['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['rowSelection'] = { + type: 'checkbox', + columnWidth: 48, + fixed: 'left', + selectedRowKeys, + onChange: (selectedRowKeys: React.Key[]) => { + setSelectedRowKeys(selectedRowKeys); + }, }; const columns: TableProps['columns'] = [ @@ -379,13 +410,16 @@ function ServiceInfo() { allowClear > +
diff --git a/react-ui/src/pages/ModelDeployment/components/VersionCompareModal/index.less b/react-ui/src/pages/ModelDeployment/components/VersionCompareModal/index.less new file mode 100644 index 00000000..f0d5449e --- /dev/null +++ b/react-ui/src/pages/ModelDeployment/components/VersionCompareModal/index.less @@ -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; + } + } + } +} diff --git a/react-ui/src/pages/ModelDeployment/components/VersionCompareModal/index.tsx b/react-ui/src/pages/ModelDeployment/components/VersionCompareModal/index.tsx new file mode 100644 index 00000000..be28ba3b --- /dev/null +++ b/react-ui/src/pages/ModelDeployment/components/VersionCompareModal/index.tsx @@ -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; + version1: ServiceVersionData; + version2: ServiceVersionData; +}; + +type ServiceVersionDataKey = keyof ServiceVersionData; + +type FiledType = { + key: ServiceVersionDataKey; + text: string; + format?: (data: any) => any; +}; + +interface CreateMirrorModalProps extends Omit { + version1: string; + version2: string; +} + +// 格式化环境变量 +const formatEnvText = (env: Record) => { + 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(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 ( + +
+
+
基础版本号
+ {fields.map(({ key, text }) => ( +
+ {text} +
+ ))} +
+
+
{v1.version}
+ {fields.map(({ key, format }) => { + const text = format ? format(v1[key]) : v1[key]; + return ( +
+ {text} +
+ ); + })} +
+
+
{v2.version}
+ {fields.map(({ key, format }) => { + const text = format ? format(v2[key]) : v2[key]; + return ( +
+ {text} +
+ ); + })} +
+
+
+ ); +} + +export default VersionCompareModal; diff --git a/react-ui/src/services/modelDeployment/index.ts b/react-ui/src/services/modelDeployment/index.ts index eccf841e..9a33687f 100644 --- a/react-ui/src/services/modelDeployment/index.ts +++ b/react-ui/src/services/modelDeployment/index.ts @@ -104,3 +104,11 @@ export function getServiceVersionLogReq(params: any) { params, }); } + +// 获取服务版本对比 +export function getServiceVersionCompareReq(params: any) { + return request(`/api/mmp/service/serviceVersionCompare`, { + method: 'GET', + params, + }); +} diff --git a/react-ui/src/utils/index.ts b/react-ui/src/utils/index.ts index 0af6f5aa..67ca10d2 100644 --- a/react-ui/src/utils/index.ts +++ b/react-ui/src/utils/index.ts @@ -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; +};