Browse Source

feat: 完成服务对比

pull/138/head
cp3hnu 1 year ago
parent
commit
5d99e0481c
12 changed files with 413 additions and 37 deletions
  1. +0
    -5
      react-ui/src/components/BasicInfo/index.less
  2. +5
    -8
      react-ui/src/components/BasicInfo/index.tsx
  3. +1
    -5
      react-ui/src/components/BasicTableInfo/index.less
  4. +1
    -0
      react-ui/src/enums/pagesEnums.ts
  5. +1
    -1
      react-ui/src/hooks/index.ts
  6. +9
    -6
      react-ui/src/pages/Experiment/Comparison/index.tsx
  7. +17
    -9
      react-ui/src/pages/Model/components/ModelMetrics/index.tsx
  8. +38
    -3
      react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx
  9. +117
    -0
      react-ui/src/pages/ModelDeployment/components/VersionCompareModal/index.less
  10. +193
    -0
      react-ui/src/pages/ModelDeployment/components/VersionCompareModal/index.tsx
  11. +8
    -0
      react-ui/src/services/modelDeployment/index.ts
  12. +23
    -0
      react-ui/src/utils/index.ts

+ 0
- 5
react-ui/src/components/BasicInfo/index.less View File

@@ -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;
}


+ 5
- 8
react-ui/src/components/BasicInfo/index.tsx View File

@@ -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>
);
}

+ 1
- 5
react-ui/src/components/BasicTableInfo/index.less View File

@@ -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
- 0
react-ui/src/enums/pagesEnums.ts View File

@@ -1,3 +1,4 @@
export enum PageEnum {
LOGIN = '/user/login',
Authorize = '/authorize',
}

+ 1
- 1
react-ui/src/hooks/index.ts View File

@@ -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(() => {


+ 9
- 6
react-ui/src/pages/Experiment/Comparison/index.tsx View File

@@ -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,
})),
},


+ 17
- 9
react-ui/src/pages/Model/components/ModelMetrics/index.tsx View File

@@ -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,
})),
},


+ 38
- 3
react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx View File

@@ -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>


+ 117
- 0
react-ui/src/pages/ModelDeployment/components/VersionCompareModal/index.less View File

@@ -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;
}
}
}
}

+ 193
- 0
react-ui/src/pages/ModelDeployment/components/VersionCompareModal/index.tsx View File

@@ -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;

+ 8
- 0
react-ui/src/services/modelDeployment/index.ts View File

@@ -104,3 +104,11 @@ export function getServiceVersionLogReq(params: any) {
params,
});
}

// 获取服务版本对比
export function getServiceVersionCompareReq(params: any) {
return request(`/api/mmp/service/serviceVersionCompare`, {
method: 'GET',
params,
});
}

+ 23
- 0
react-ui/src/utils/index.ts View File

@@ -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;
};

Loading…
Cancel
Save