| @@ -0,0 +1,3 @@ | |||
| .ant-table .ant-table-cell .kf-table-col-title { | |||
| margin-bottom: 0; | |||
| } | |||
| @@ -0,0 +1,32 @@ | |||
| /* | |||
| * @Author: 赵伟 | |||
| * @Date: 2025-03-11 10:52:23 | |||
| * @Description: 用于内容可变的表格类标题 | |||
| */ | |||
| import { Typography } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import './index.less'; | |||
| type TableColTitleProps = { | |||
| /** 标题 */ | |||
| title: string; | |||
| /** 自定义类名 */ | |||
| className?: string; | |||
| /** 自定义样式 */ | |||
| style?: React.CSSProperties; | |||
| }; | |||
| function TableColTitle({ title, className, style }: TableColTitleProps) { | |||
| return ( | |||
| <Typography.Paragraph | |||
| ellipsis={{ tooltip: title }} | |||
| className={classNames('kf-table-col-title', className)} | |||
| style={style} | |||
| > | |||
| {title} | |||
| </Typography.Paragraph> | |||
| ); | |||
| } | |||
| export default TableColTitle; | |||
| @@ -69,35 +69,30 @@ function ExperimentHistory({ fileUrl, isClassification }: ExperimentHistoryProps | |||
| dataIndex: 'accuracy', | |||
| key: 'accuracy', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| }, | |||
| { | |||
| title: '耗时', | |||
| dataIndex: 'duration', | |||
| key: 'duration', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| }, | |||
| { | |||
| title: '训练损失', | |||
| dataIndex: 'train_loss', | |||
| key: 'train_loss', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| }, | |||
| { | |||
| title: '特征处理', | |||
| dataIndex: 'feature', | |||
| key: 'feature', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| }, | |||
| { | |||
| title: '算法', | |||
| dataIndex: 'althorithm', | |||
| key: 'althorithm', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| }, | |||
| { | |||
| title: '状态', | |||
| @@ -26,7 +26,7 @@ | |||
| .startTime { | |||
| .singleLine(); | |||
| width: calc(20% + 10px); | |||
| width: 200px; | |||
| } | |||
| .status { | |||
| @@ -8,7 +8,7 @@ import { elapsedTime, formatDate } from '@/utils/date'; | |||
| import { to } from '@/utils/promise'; | |||
| import { modalConfirm } from '@/utils/ui'; | |||
| import { DoubleRightOutlined } from '@ant-design/icons'; | |||
| import { App, Button, Checkbox, ConfigProvider, Tooltip } from 'antd'; | |||
| import { App, Button, Checkbox, ConfigProvider, Typography } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { useEffect, useMemo } from 'react'; | |||
| import { ExperimentListType, experimentListConfig } from '../ExperimentList/config'; | |||
| @@ -159,9 +159,9 @@ function ExperimentInstanceComponent({ | |||
| {elapsedTime(item.create_time, item.finish_time)} | |||
| </div> | |||
| <div className={styles.startTime}> | |||
| <Tooltip title={formatDate(item.create_time)}> | |||
| <span>{formatDate(item.create_time)}</span> | |||
| </Tooltip> | |||
| <Typography.Text ellipsis={{ tooltip: formatDate(item.create_time) }}> | |||
| {formatDate(item.create_time)} | |||
| </Typography.Text> | |||
| </div> | |||
| <div className={styles.statusBox}> | |||
| <img | |||
| @@ -261,16 +261,13 @@ function ExperimentList({ type }: ExperimentListProps) { | |||
| dataIndex: config.descProperty, | |||
| key: 'ml_description', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| }, | |||
| { | |||
| title: '创建时间', | |||
| dataIndex: 'update_time', | |||
| key: 'update_time', | |||
| width: '20%', | |||
| render: tableCellRender(true, TableCellValueType.Date), | |||
| ellipsis: { showTitle: false }, | |||
| width: 200, | |||
| render: tableCellRender(false, TableCellValueType.Date), | |||
| }, | |||
| { | |||
| title: '最近五次运行状态', | |||
| @@ -58,7 +58,7 @@ function ResourceVersion({ resourceType, info }: ResourceVersionProps) { | |||
| title: '文件大小', | |||
| dataIndex: 'file_size', | |||
| key: 'file_size', | |||
| render: tableCellRender(), | |||
| render: tableCellRender(false), | |||
| }, | |||
| { | |||
| title: '更新时间', | |||
| @@ -99,7 +99,13 @@ function ResourceVersion({ resourceType, info }: ResourceVersionProps) { | |||
| </Button> | |||
| </Flex> | |||
| </Flex> | |||
| <Table columns={columns} dataSource={fileList} pagination={false} rowKey="url" /> | |||
| <Table | |||
| columns={columns} | |||
| dataSource={fileList} | |||
| pagination={false} | |||
| rowKey="url" | |||
| tableLayout="fixed" | |||
| /> | |||
| </div> | |||
| ); | |||
| } | |||
| @@ -34,13 +34,13 @@ | |||
| border-left: none !important; | |||
| } | |||
| } | |||
| .ant-table-tbody-virtual::after { | |||
| border-bottom: none !important; | |||
| } | |||
| .ant-table-footer { | |||
| padding: 0; | |||
| border: none !important; | |||
| } | |||
| .ant-table-column-title { | |||
| min-width: 0; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -4,6 +4,7 @@ | |||
| * @Description: 实验对比 | |||
| */ | |||
| import TableColTitle from '@/components/TableColTitle'; | |||
| import { | |||
| getExpEvaluateInfosReq, | |||
| getExpMetricsReq, | |||
| @@ -13,7 +14,7 @@ import { tableSorter } from '@/utils'; | |||
| import { to } from '@/utils/promise'; | |||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | |||
| import { useSearchParams } from '@umijs/max'; | |||
| import { App, Button, Table, TablePaginationConfig, TableProps, Tooltip } from 'antd'; | |||
| import { App, Button, Table, TablePaginationConfig, TableProps } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { useEffect, useMemo, useState } from 'react'; | |||
| import ExperimentStatusCell from '../components/ExperimentStatusCell'; | |||
| @@ -154,7 +155,6 @@ function ExperimentComparison() { | |||
| fixed: 'left', | |||
| align: 'center', | |||
| render: tableCellRender(true, TableCellValueType.Array), | |||
| ellipsis: { showTitle: false }, | |||
| }, | |||
| ], | |||
| }, | |||
| @@ -162,17 +162,12 @@ function ExperimentComparison() { | |||
| title: `${config.title}参数`, | |||
| align: 'center', | |||
| children: paramsNames.map((name) => ({ | |||
| title: ( | |||
| <Tooltip title={name}> | |||
| <span>{name}</span> | |||
| </Tooltip> | |||
| ), | |||
| title: <TableColTitle title={name} />, | |||
| dataIndex: ['params', name], | |||
| key: name, | |||
| width: 120, | |||
| width: 150, | |||
| align: 'center', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| sorter: (a, b) => tableSorter(a.params?.[name], b.params?.[name]), | |||
| showSorterTooltip: false, | |||
| })), | |||
| @@ -181,17 +176,12 @@ function ExperimentComparison() { | |||
| title: `${config.title}指标`, | |||
| align: 'center', | |||
| children: metricsNames.map((name) => ({ | |||
| title: ( | |||
| <Tooltip title={name}> | |||
| <span>{name}</span> | |||
| </Tooltip> | |||
| ), | |||
| title: <TableColTitle title={name} />, | |||
| dataIndex: ['metrics', name], | |||
| key: name, | |||
| width: 120, | |||
| width: 150, | |||
| align: 'center', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| sorter: (a, b) => tableSorter(a.metrics?.[name], b.metrics?.[name]), | |||
| showSorterTooltip: false, | |||
| })), | |||
| @@ -13,7 +13,7 @@ import { elapsedTime, formatDate } from '@/utils/date'; | |||
| import { to } from '@/utils/promise'; | |||
| import { modalConfirm } from '@/utils/ui'; | |||
| import { DoubleRightOutlined } from '@ant-design/icons'; | |||
| import { App, Button, Checkbox, ConfigProvider, Tooltip } from 'antd'; | |||
| import { App, Button, Checkbox, ConfigProvider, Typography } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { useEffect, useMemo } from 'react'; | |||
| import TensorBoardStatusCell from '../TensorBoardStatus'; | |||
| @@ -186,9 +186,9 @@ function ExperimentInstanceComponent({ | |||
| <div className={styles.description}> | |||
| <div style={{ width: '50%' }}>{elapsedTime(item.create_time, item.finish_time)}</div> | |||
| <div style={{ width: '50%' }} className={styles.startTime}> | |||
| <Tooltip title={formatDate(item.create_time)}> | |||
| <span>{formatDate(item.create_time)}</span> | |||
| </Tooltip> | |||
| <Typography.Text ellipsis={{ tooltip: formatDate(item.create_time) }}> | |||
| {formatDate(item.create_time)} | |||
| </Typography.Text> | |||
| </div> | |||
| </div> | |||
| <div className={styles.statusBox}> | |||
| @@ -383,7 +383,7 @@ function Experiment() { | |||
| title: '实验名称', | |||
| dataIndex: 'name', | |||
| key: 'name', | |||
| render: tableCellRender(), | |||
| render: tableCellRender(false), | |||
| width: '16%', | |||
| }, | |||
| { | |||
| @@ -400,7 +400,6 @@ function Experiment() { | |||
| dataIndex: 'description', | |||
| key: 'description', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| }, | |||
| { | |||
| title: '最近五次运行状态', | |||
| @@ -1,14 +1,15 @@ | |||
| import InfoGroup from '@/components/InfoGroup'; | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import TableColTitle from '@/components/TableColTitle'; | |||
| import TrialStatusCell from '@/pages/HyperParameter/components/TrialStatusCell'; | |||
| import { HyperParameterFile, HyperParameterTrial } from '@/pages/HyperParameter/types'; | |||
| import { getExpMetricsReq } from '@/services/hyperParameter'; | |||
| import { downLoadZip } from '@/utils/downloadfile'; | |||
| import { to } from '@/utils/promise'; | |||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | |||
| import { App, Button, Table, Tooltip, Tree, type TableProps, type TreeDataNode } from 'antd'; | |||
| import { App, Button, Table, Tree, type TableProps, type TreeDataNode } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { useState } from 'react'; | |||
| import { useEffect, useState } from 'react'; | |||
| import styles from './index.less'; | |||
| const { DirectoryTree } = Tree; | |||
| @@ -22,13 +23,16 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { | |||
| const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]); | |||
| const { message } = App.useApp(); | |||
| const [tableData, setTableData] = useState<HyperParameterTrial[]>([]); | |||
| const [loading, setLoading] = useState(true); | |||
| const [loading, setLoading] = useState(false); | |||
| // 防止 Tabs 卡顿 | |||
| setTimeout(() => { | |||
| setTableData(trialList); | |||
| setLoading(false); | |||
| }, 100); | |||
| useEffect(() => { | |||
| setLoading(true); | |||
| setTimeout(() => { | |||
| setTableData(trialList); | |||
| setLoading(false); | |||
| }, 500); | |||
| }, []); | |||
| // 计算 column | |||
| const first: HyperParameterTrial | undefined = trialList[0]; | |||
| @@ -43,6 +47,7 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { | |||
| dataIndex: 'index', | |||
| key: 'index', | |||
| width: 100, | |||
| fixed: 'left', | |||
| render: (_text, record, index: number) => { | |||
| return ( | |||
| <div className={styles['cell-index']}> | |||
| @@ -53,28 +58,36 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { | |||
| }, | |||
| }, | |||
| { | |||
| title: '运行次数', | |||
| dataIndex: 'training_iteration', | |||
| key: 'training_iteration', | |||
| width: 120, | |||
| render: tableCellRender(false), | |||
| }, | |||
| { | |||
| title: '平均时长(秒)', | |||
| dataIndex: 'time_avg', | |||
| key: 'time_avg', | |||
| width: 150, | |||
| render: tableCellRender(false, TableCellValueType.Custom, { | |||
| format: (value = 0) => Number(value).toFixed(2), | |||
| }), | |||
| ellipsis: { showTitle: false }, | |||
| }, | |||
| { | |||
| title: '状态', | |||
| dataIndex: 'status', | |||
| key: 'status', | |||
| width: 120, | |||
| render: TrialStatusCell, | |||
| title: '基本信息', | |||
| align: 'center', | |||
| children: [ | |||
| { | |||
| title: '运行次数', | |||
| dataIndex: 'training_iteration', | |||
| key: 'training_iteration', | |||
| width: 120, | |||
| fixed: 'left', | |||
| render: tableCellRender(false), | |||
| }, | |||
| { | |||
| title: '平均时长(秒)', | |||
| dataIndex: 'time_avg', | |||
| key: 'time_avg', | |||
| width: 150, | |||
| fixed: 'left', | |||
| render: tableCellRender(false, TableCellValueType.Custom, { | |||
| format: (value = 0) => Number(value).toFixed(2), | |||
| }), | |||
| }, | |||
| { | |||
| title: '状态', | |||
| dataIndex: 'status', | |||
| key: 'status', | |||
| width: 120, | |||
| fixed: 'left', | |||
| render: TrialStatusCell, | |||
| }, | |||
| ], | |||
| }, | |||
| ]; | |||
| @@ -85,18 +98,12 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { | |||
| key: 'config', | |||
| align: 'center', | |||
| children: paramsNames.map((name) => ({ | |||
| title: ( | |||
| <Tooltip title={name}> | |||
| <span>{name}</span> | |||
| </Tooltip> | |||
| ), | |||
| title: <TableColTitle title={name} />, | |||
| dataIndex: ['config', name], | |||
| key: name, | |||
| width: 120, | |||
| align: 'center', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| showSorterTooltip: false, | |||
| })), | |||
| }); | |||
| } | |||
| @@ -108,18 +115,12 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { | |||
| key: 'metrics', | |||
| align: 'center', | |||
| children: metricNames.map((name) => ({ | |||
| title: ( | |||
| <Tooltip title={name}> | |||
| <span>{name}</span> | |||
| </Tooltip> | |||
| ), | |||
| title: <TableColTitle title={name} />, | |||
| dataIndex: ['metric_analysis', name], | |||
| key: name, | |||
| width: 120, | |||
| align: 'center', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| showSorterTooltip: false, | |||
| })), | |||
| }); | |||
| } | |||
| @@ -1,10 +1,11 @@ | |||
| import TableColTitle from '@/components/TableColTitle'; | |||
| import { | |||
| getReqParamName, | |||
| type FormParameter, | |||
| } from '@/pages/HyperParameter/components/CreateForm/utils'; | |||
| import { HyperParameterData } from '@/pages/HyperParameter/types'; | |||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | |||
| import { Table, Tooltip, type TableProps } from 'antd'; | |||
| import { Table, type TableProps } from 'antd'; | |||
| import { useMemo } from 'react'; | |||
| import styles from './index.less'; | |||
| @@ -43,16 +44,14 @@ function ParameterInfo({ info }: ParameterInfoProps) { | |||
| dataIndex: 'name', | |||
| key: 'type', | |||
| width: '40%', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| render: tableCellRender('auto'), | |||
| }, | |||
| { | |||
| title: '参数类型', | |||
| dataIndex: 'type', | |||
| key: 'type', | |||
| width: '20%', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| render: tableCellRender(false), | |||
| }, | |||
| { | |||
| title: '取值范围', | |||
| @@ -64,7 +63,6 @@ function ParameterInfo({ info }: ParameterInfoProps) { | |||
| return JSON.stringify(value); | |||
| }, | |||
| }), | |||
| ellipsis: { showTitle: false }, | |||
| }, | |||
| ]; | |||
| @@ -72,16 +70,11 @@ function ParameterInfo({ info }: ParameterInfoProps) { | |||
| runParameters.length > 0 | |||
| ? parameters.map(({ name }) => { | |||
| return { | |||
| title: ( | |||
| <Tooltip title={name}> | |||
| <span>{name}</span> | |||
| </Tooltip> | |||
| ), | |||
| title: <TableColTitle title={name} />, | |||
| dataIndex: name, | |||
| key: name, | |||
| width: 150, | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| }; | |||
| }) | |||
| : []; | |||
| @@ -89,7 +82,14 @@ function ParameterInfo({ info }: ParameterInfoProps) { | |||
| return ( | |||
| <div className={styles['parameter-info']}> | |||
| <div className={styles['parameter-info__title']}>超参数</div> | |||
| <Table dataSource={parameters} columns={columns} rowKey="name" bordered pagination={false} /> | |||
| <Table | |||
| dataSource={parameters} | |||
| columns={columns} | |||
| rowKey="name" | |||
| bordered | |||
| pagination={false} | |||
| tableLayout="fixed" | |||
| /> | |||
| <div className={styles['parameter-info__title']}>手动运行超参数</div> | |||
| <Table | |||
| dataSource={runParameters} | |||
| @@ -98,6 +98,7 @@ function ParameterInfo({ info }: ParameterInfoProps) { | |||
| bordered | |||
| pagination={false} | |||
| scroll={{ x: '100%' }} | |||
| tableLayout="fixed" | |||
| /> | |||
| </div> | |||
| ); | |||
| @@ -192,7 +192,6 @@ function MirrorList() { | |||
| key: 'description', | |||
| width: '35%', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| }, | |||
| { | |||
| title: '创建时间', | |||
| @@ -21,6 +21,9 @@ | |||
| border-left: none !important; | |||
| } | |||
| } | |||
| .ant-table-column-title { | |||
| min-width: 0; | |||
| } | |||
| } | |||
| } | |||
| @@ -1,10 +1,11 @@ | |||
| import SubAreaTitle from '@/components/SubAreaTitle'; | |||
| import TableColTitle from '@/components/TableColTitle'; | |||
| 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'; | |||
| import { Checkbox, Flex, Table, type TablePaginationConfig, type TableProps } from 'antd'; | |||
| import { useEffect, useMemo, useState } from 'react'; | |||
| import MetricsChart, { MetricsChatData } from '../MetricsChart'; | |||
| import styles from './index.less'; | |||
| @@ -174,17 +175,12 @@ function ModelMetrics({ resourceId, identifier, owner, version }: ModelMetricsPr | |||
| title: `训练参数`, | |||
| align: 'center', | |||
| children: paramsNames.map((name) => ({ | |||
| title: ( | |||
| <Tooltip title={name}> | |||
| <span>{name}</span> | |||
| </Tooltip> | |||
| ), | |||
| title: <TableColTitle title={name} />, | |||
| dataIndex: ['params', name], | |||
| key: name, | |||
| width: 120, | |||
| width: 150, | |||
| align: 'center', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| sorter: (a, b) => tableSorter(a.params?.[name], b.params?.[name]), | |||
| showSorterTooltip: false, | |||
| })), | |||
| @@ -197,14 +193,14 @@ function ModelMetrics({ resourceId, identifier, owner, version }: ModelMetricsPr | |||
| indeterminate={metricsIndeterminate} | |||
| onChange={checkAllMetrics} | |||
| disabled={metricsNames.length === 0} | |||
| ></Checkbox>{' '} | |||
| <span>训练指标</span> | |||
| ></Checkbox> | |||
| <span style={{ marginLeft: 4 }}>训练指标</span> | |||
| </div> | |||
| ), | |||
| align: 'center', | |||
| children: metricsNames.map((name) => ({ | |||
| title: ( | |||
| <div> | |||
| <Flex align="center"> | |||
| <Checkbox | |||
| checked={isSingleMetricsChecked(name)} | |||
| onChange={(e) => { | |||
| @@ -212,18 +208,15 @@ function ModelMetrics({ resourceId, identifier, owner, version }: ModelMetricsPr | |||
| checkSingleMetrics(name); | |||
| }} | |||
| onClick={(e) => e.stopPropagation()} | |||
| ></Checkbox>{' '} | |||
| <Tooltip title={name}> | |||
| <span>{name}</span> | |||
| </Tooltip> | |||
| </div> | |||
| ></Checkbox> | |||
| <TableColTitle style={{ marginLeft: 4 }} title={name} /> | |||
| </Flex> | |||
| ), | |||
| dataIndex: ['metrics', name], | |||
| key: name, | |||
| width: 120, | |||
| width: 150, | |||
| align: 'center', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| sorter: (a, b) => tableSorter(a.metrics?.[name], b.metrics?.[name]), | |||
| showSorterTooltip: false, | |||
| })), | |||
| @@ -253,6 +246,8 @@ function ModelMetrics({ resourceId, identifier, owner, version }: ModelMetricsPr | |||
| }} | |||
| onChange={handleTableChange} | |||
| rowKey="name" | |||
| tableLayout="fixed" | |||
| scroll={{ x: '100%' }} | |||
| /> | |||
| </div> | |||
| <div className={styles['model-metrics__chart']}> | |||
| @@ -285,7 +285,6 @@ function ServiceInfo() { | |||
| key: 'model', | |||
| width: '20%', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| }, | |||
| { | |||
| title: '状态', | |||
| @@ -300,7 +299,6 @@ function ServiceInfo() { | |||
| key: 'image', | |||
| width: '20%', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| }, | |||
| { | |||
| title: '副本数量', | |||
| @@ -317,7 +315,6 @@ function ServiceInfo() { | |||
| render: tableCellRender(true, TableCellValueType.Custom, { | |||
| format: getResourceDescription, | |||
| }), | |||
| ellipsis: { showTitle: false }, | |||
| }, | |||
| { | |||
| title: '操作', | |||
| @@ -152,6 +152,7 @@ const Pipeline = () => { | |||
| title: '流水线名称', | |||
| dataIndex: 'name', | |||
| key: 'name', | |||
| width: '50%', | |||
| render: tableCellRender(false, TableCellValueType.Link, { | |||
| onClick: gotoDetail, | |||
| }), | |||
| @@ -160,19 +161,21 @@ const Pipeline = () => { | |||
| title: '流水线描述', | |||
| dataIndex: 'description', | |||
| key: 'description', | |||
| width: '50%', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| }, | |||
| { | |||
| title: '创建时间', | |||
| dataIndex: 'create_time', | |||
| key: 'create_time', | |||
| width: 180, | |||
| render: tableCellRender(false, TableCellValueType.Date), | |||
| }, | |||
| { | |||
| title: '修改时间', | |||
| dataIndex: 'update_time', | |||
| key: 'update_time', | |||
| width: 180, | |||
| render: tableCellRender(false, TableCellValueType.Date), | |||
| }, | |||
| { | |||
| @@ -4,7 +4,7 @@ import * as BasicInfoStories from './BasicInfo.stories'; | |||
| // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export | |||
| const meta = { | |||
| title: 'Components/BasicTableInfo 表格基本信息', | |||
| title: 'Components/BasicTableInfo 基本信息表格版', | |||
| component: BasicTableInfo, | |||
| parameters: { | |||
| // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout | |||
| @@ -31,7 +31,7 @@ export const RightArrow = () => <svg | |||
| <path d="m11.1 7.35-5.5 5.5a.5.5 0 0 1-.7-.7L10.04 7 4.9 1.85a.5.5 0 1 1 .7-.7l5.5 5.5c.2.2.2.5 0 .7Z" /> | |||
| </svg> | |||
| <Meta title="Configure your project" /> | |||
| <Meta title="Documentation/Storybook" /> | |||
| <div className="sb-container"> | |||
| <div className='sb-section-title'> | |||
| @@ -1,53 +0,0 @@ | |||
| import type { Meta, StoryObj } from '@storybook/react'; | |||
| import { fn } from '@storybook/test'; | |||
| import { Button } from './Button'; | |||
| // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export | |||
| const meta = { | |||
| title: 'Example/Button', | |||
| component: Button, | |||
| parameters: { | |||
| // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout | |||
| layout: 'centered', | |||
| }, | |||
| // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs | |||
| tags: ['autodocs'], | |||
| // More on argTypes: https://storybook.js.org/docs/api/argtypes | |||
| argTypes: { | |||
| backgroundColor: { control: 'color' }, | |||
| }, | |||
| // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args | |||
| args: { onClick: fn() }, | |||
| } satisfies Meta<typeof Button>; | |||
| export default meta; | |||
| type Story = StoryObj<typeof meta>; | |||
| // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args | |||
| export const Primary: Story = { | |||
| args: { | |||
| primary: true, | |||
| label: 'Button', | |||
| }, | |||
| }; | |||
| export const Secondary: Story = { | |||
| args: { | |||
| label: 'Button', | |||
| }, | |||
| }; | |||
| export const Large: Story = { | |||
| args: { | |||
| size: 'large', | |||
| label: 'Button', | |||
| }, | |||
| }; | |||
| export const Small: Story = { | |||
| args: { | |||
| size: 'small', | |||
| label: 'Button', | |||
| }, | |||
| }; | |||
| @@ -1,37 +0,0 @@ | |||
| import React from 'react'; | |||
| import './button.css'; | |||
| export interface ButtonProps { | |||
| /** Is this the principal call to action on the page? */ | |||
| primary?: boolean; | |||
| /** What background color to use */ | |||
| backgroundColor?: string; | |||
| /** How large should the button be? */ | |||
| size?: 'small' | 'medium' | 'large'; | |||
| /** Button contents */ | |||
| label: string; | |||
| /** Optional click handler */ | |||
| onClick?: () => void; | |||
| } | |||
| /** Primary UI component for user interaction */ | |||
| export const Button = ({ | |||
| primary = false, | |||
| size = 'medium', | |||
| backgroundColor, | |||
| label, | |||
| ...props | |||
| }: ButtonProps) => { | |||
| const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary'; | |||
| return ( | |||
| <button | |||
| type="button" | |||
| className={['storybook-button', `storybook-button--${size}`, mode].join(' ')} | |||
| style={{ backgroundColor }} | |||
| {...props} | |||
| > | |||
| {label} | |||
| </button> | |||
| ); | |||
| }; | |||
| @@ -1,33 +0,0 @@ | |||
| import type { Meta, StoryObj } from '@storybook/react'; | |||
| import { fn } from '@storybook/test'; | |||
| import { Header } from './Header'; | |||
| const meta = { | |||
| title: 'Example/Header', | |||
| component: Header, | |||
| // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs | |||
| tags: ['autodocs'], | |||
| parameters: { | |||
| // More on how to position stories at: https://storybook.js.org/docs/configure/story-layout | |||
| layout: 'fullscreen', | |||
| }, | |||
| args: { | |||
| onLogin: fn(), | |||
| onLogout: fn(), | |||
| onCreateAccount: fn(), | |||
| }, | |||
| } satisfies Meta<typeof Header>; | |||
| export default meta; | |||
| type Story = StoryObj<typeof meta>; | |||
| export const LoggedIn: Story = { | |||
| args: { | |||
| user: { | |||
| name: 'Jane Doe', | |||
| }, | |||
| }, | |||
| }; | |||
| export const LoggedOut: Story = {}; | |||
| @@ -1,56 +0,0 @@ | |||
| import React from 'react'; | |||
| import { Button } from './Button'; | |||
| import './header.css'; | |||
| type User = { | |||
| name: string; | |||
| }; | |||
| export interface HeaderProps { | |||
| user?: User; | |||
| onLogin?: () => void; | |||
| onLogout?: () => void; | |||
| onCreateAccount?: () => void; | |||
| } | |||
| export const Header = ({ user, onLogin, onLogout, onCreateAccount }: HeaderProps) => ( | |||
| <header> | |||
| <div className="storybook-header"> | |||
| <div> | |||
| <svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"> | |||
| <g fill="none" fillRule="evenodd"> | |||
| <path | |||
| d="M10 0h12a10 10 0 0110 10v12a10 10 0 01-10 10H10A10 10 0 010 22V10A10 10 0 0110 0z" | |||
| fill="#FFF" | |||
| /> | |||
| <path | |||
| d="M5.3 10.6l10.4 6v11.1l-10.4-6v-11zm11.4-6.2l9.7 5.5-9.7 5.6V4.4z" | |||
| fill="#555AB9" | |||
| /> | |||
| <path | |||
| d="M27.2 10.6v11.2l-10.5 6V16.5l10.5-6zM15.7 4.4v11L6 10l9.7-5.5z" | |||
| fill="#91BAF8" | |||
| /> | |||
| </g> | |||
| </svg> | |||
| <h1>Acme</h1> | |||
| </div> | |||
| <div> | |||
| {user ? ( | |||
| <> | |||
| <span className="welcome"> | |||
| Welcome, <b>{user.name}</b>! | |||
| </span> | |||
| <Button size="small" onClick={onLogout} label="Log out" /> | |||
| </> | |||
| ) : ( | |||
| <> | |||
| <Button size="small" onClick={onLogin} label="Log in" /> | |||
| <Button primary size="small" onClick={onCreateAccount} label="Sign up" /> | |||
| </> | |||
| )} | |||
| </div> | |||
| </div> | |||
| </header> | |||
| ); | |||
| @@ -1,32 +0,0 @@ | |||
| import type { Meta, StoryObj } from '@storybook/react'; | |||
| import { expect, userEvent, within } from '@storybook/test'; | |||
| import { Page } from './Page'; | |||
| const meta = { | |||
| title: 'Example/Page', | |||
| component: Page, | |||
| parameters: { | |||
| // More on how to position stories at: https://storybook.js.org/docs/configure/story-layout | |||
| layout: 'fullscreen', | |||
| }, | |||
| } satisfies Meta<typeof Page>; | |||
| export default meta; | |||
| type Story = StoryObj<typeof meta>; | |||
| export const LoggedOut: Story = {}; | |||
| // More on component testing: https://storybook.js.org/docs/writing-tests/component-testing | |||
| export const LoggedIn: Story = { | |||
| play: async ({ canvasElement }) => { | |||
| const canvas = within(canvasElement); | |||
| const loginButton = canvas.getByRole('button', { name: /Log in/i }); | |||
| await expect(loginButton).toBeInTheDocument(); | |||
| await userEvent.click(loginButton); | |||
| await expect(loginButton).not.toBeInTheDocument(); | |||
| const logoutButton = canvas.getByRole('button', { name: /Log out/i }); | |||
| await expect(logoutButton).toBeInTheDocument(); | |||
| }, | |||
| }; | |||
| @@ -1,73 +0,0 @@ | |||
| import React from 'react'; | |||
| import { Header } from './Header'; | |||
| import './page.css'; | |||
| type User = { | |||
| name: string; | |||
| }; | |||
| export const Page: React.FC = () => { | |||
| const [user, setUser] = React.useState<User>(); | |||
| return ( | |||
| <article> | |||
| <Header | |||
| user={user} | |||
| onLogin={() => setUser({ name: 'Jane Doe' })} | |||
| onLogout={() => setUser(undefined)} | |||
| onCreateAccount={() => setUser({ name: 'Jane Doe' })} | |||
| /> | |||
| <section className="storybook-page"> | |||
| <h2>Pages in Storybook</h2> | |||
| <p> | |||
| We recommend building UIs with a{' '} | |||
| <a href="https://componentdriven.org" target="_blank" rel="noopener noreferrer"> | |||
| <strong>component-driven</strong> | |||
| </a>{' '} | |||
| process starting with atomic components and ending with pages. | |||
| </p> | |||
| <p> | |||
| Render pages with mock data. This makes it easy to build and review page states without | |||
| needing to navigate to them in your app. Here are some handy patterns for managing page | |||
| data in Storybook: | |||
| </p> | |||
| <ul> | |||
| <li> | |||
| Use a higher-level connected component. Storybook helps you compose such data from the | |||
| "args" of child component stories | |||
| </li> | |||
| <li> | |||
| Assemble data in the page component from your services. You can mock these services out | |||
| using Storybook. | |||
| </li> | |||
| </ul> | |||
| <p> | |||
| Get a guided tutorial on component-driven development at{' '} | |||
| <a href="https://storybook.js.org/tutorials/" target="_blank" rel="noopener noreferrer"> | |||
| Storybook tutorials | |||
| </a> | |||
| . Read more in the{' '} | |||
| <a href="https://storybook.js.org/docs" target="_blank" rel="noopener noreferrer"> | |||
| docs | |||
| </a> | |||
| . | |||
| </p> | |||
| <div className="tip-wrapper"> | |||
| <span className="tip">Tip</span> Adjust the width of the canvas with the{' '} | |||
| <svg width="10" height="10" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg"> | |||
| <g fill="none" fillRule="evenodd"> | |||
| <path | |||
| d="M1.5 5.2h4.8c.3 0 .5.2.5.4v5.1c-.1.2-.3.3-.4.3H1.4a.5.5 0 01-.5-.4V5.7c0-.3.2-.5.5-.5zm0-2.1h6.9c.3 0 .5.2.5.4v7a.5.5 0 01-1 0V4H1.5a.5.5 0 010-1zm0-2.1h9c.3 0 .5.2.5.4v9.1a.5.5 0 01-1 0V2H1.5a.5.5 0 010-1zm4.3 5.2H2V10h3.8V6.2z" | |||
| id="a" | |||
| fill="#999" | |||
| /> | |||
| </g> | |||
| </svg> | |||
| Viewports addon in the toolbar | |||
| </div> | |||
| </section> | |||
| </article> | |||
| ); | |||
| }; | |||
| @@ -1,30 +0,0 @@ | |||
| .storybook-button { | |||
| display: inline-block; | |||
| cursor: pointer; | |||
| border: 0; | |||
| border-radius: 3em; | |||
| font-weight: 700; | |||
| line-height: 1; | |||
| font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; | |||
| } | |||
| .storybook-button--primary { | |||
| background-color: #555ab9; | |||
| color: white; | |||
| } | |||
| .storybook-button--secondary { | |||
| box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset; | |||
| background-color: transparent; | |||
| color: #333; | |||
| } | |||
| .storybook-button--small { | |||
| padding: 10px 16px; | |||
| font-size: 12px; | |||
| } | |||
| .storybook-button--medium { | |||
| padding: 11px 20px; | |||
| font-size: 14px; | |||
| } | |||
| .storybook-button--large { | |||
| padding: 12px 24px; | |||
| font-size: 16px; | |||
| } | |||
| @@ -1,32 +0,0 @@ | |||
| .storybook-header { | |||
| display: flex; | |||
| justify-content: space-between; | |||
| align-items: center; | |||
| border-bottom: 1px solid rgba(0, 0, 0, 0.1); | |||
| padding: 15px 20px; | |||
| font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; | |||
| } | |||
| .storybook-header svg { | |||
| display: inline-block; | |||
| vertical-align: top; | |||
| } | |||
| .storybook-header h1 { | |||
| display: inline-block; | |||
| vertical-align: top; | |||
| margin: 6px 0 6px 10px; | |||
| font-weight: 700; | |||
| font-size: 20px; | |||
| line-height: 1; | |||
| } | |||
| .storybook-header button + button { | |||
| margin-left: 10px; | |||
| } | |||
| .storybook-header .welcome { | |||
| margin-right: 10px; | |||
| color: #333; | |||
| font-size: 14px; | |||
| } | |||
| @@ -1,68 +0,0 @@ | |||
| .storybook-page { | |||
| margin: 0 auto; | |||
| padding: 48px 20px; | |||
| max-width: 600px; | |||
| color: #333; | |||
| font-size: 14px; | |||
| line-height: 24px; | |||
| font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; | |||
| } | |||
| .storybook-page h2 { | |||
| display: inline-block; | |||
| vertical-align: top; | |||
| margin: 0 0 4px; | |||
| font-weight: 700; | |||
| font-size: 32px; | |||
| line-height: 1; | |||
| } | |||
| .storybook-page p { | |||
| margin: 1em 0; | |||
| } | |||
| .storybook-page a { | |||
| color: inherit; | |||
| } | |||
| .storybook-page ul { | |||
| margin: 1em 0; | |||
| padding-left: 30px; | |||
| } | |||
| .storybook-page li { | |||
| margin-bottom: 8px; | |||
| } | |||
| .storybook-page .tip { | |||
| display: inline-block; | |||
| vertical-align: top; | |||
| margin-right: 10px; | |||
| border-radius: 1em; | |||
| background: #e7fdd8; | |||
| padding: 4px 12px; | |||
| color: #357a14; | |||
| font-weight: 700; | |||
| font-size: 11px; | |||
| line-height: 12px; | |||
| } | |||
| .storybook-page .tip-wrapper { | |||
| margin-top: 40px; | |||
| margin-bottom: 40px; | |||
| font-size: 13px; | |||
| line-height: 20px; | |||
| } | |||
| .storybook-page .tip-wrapper svg { | |||
| display: inline-block; | |||
| vertical-align: top; | |||
| margin-top: 3px; | |||
| margin-right: 4px; | |||
| width: 12px; | |||
| height: 12px; | |||
| } | |||
| .storybook-page .tip-wrapper svg path { | |||
| fill: #1ea7fd; | |||
| } | |||
| @@ -6,7 +6,7 @@ | |||
| import { isEmpty } from '@/utils'; | |||
| import { formatDate } from '@/utils/date'; | |||
| import { Tooltip } from 'antd'; | |||
| import { Tooltip, TooltipProps, Typography } from 'antd'; | |||
| import dayjs from 'dayjs'; | |||
| export enum TableCellValueType { | |||
| @@ -65,7 +65,7 @@ function formatArray(property?: string): TableCellFormatter { | |||
| } | |||
| function tableCellRender<T>( | |||
| ellipsis: boolean = false, | |||
| ellipsis: boolean | TooltipProps | 'auto' = false, | |||
| type: TableCellValueType = TableCellValueType.Text, | |||
| options?: TableCellValueOptions<T>, | |||
| ) { | |||
| @@ -92,41 +92,83 @@ function tableCellRender<T>( | |||
| break; | |||
| } | |||
| if (ellipsis && text) { | |||
| if (ellipsis === 'auto' && text) { | |||
| return renderCell(type, text, 'auto', record, options?.onClick); | |||
| } else if (ellipsis && text) { | |||
| const tooltipProps = typeof ellipsis === 'object' ? ellipsis : {}; | |||
| const { overlayStyle, ...rest } = tooltipProps; | |||
| return ( | |||
| <Tooltip title={text} placement="topLeft" overlayStyle={{ maxWidth: '400px' }}> | |||
| {renderCell(text, type === TableCellValueType.Link, record, options?.onClick)} | |||
| <Tooltip {...rest} overlayStyle={{ maxWidth: 400, ...overlayStyle }} title={text}> | |||
| {renderCell(type, text, true, record, options?.onClick)} | |||
| </Tooltip> | |||
| ); | |||
| } else { | |||
| return renderCell(text, type === TableCellValueType.Link, record, options?.onClick); | |||
| return renderCell(type, text, false, record, options?.onClick); | |||
| } | |||
| }; | |||
| } | |||
| function renderCell<T>( | |||
| type: TableCellValueType, | |||
| text: any | undefined | null, | |||
| isLink: boolean, | |||
| ellipsis: boolean | 'auto', | |||
| record: T, | |||
| onClick?: (record: T, e: React.MouseEvent) => void, | |||
| ) { | |||
| return isLink ? renderLink(text, record, onClick) : renderText(text); | |||
| } | |||
| function renderText(text: any | undefined | null) { | |||
| return <span>{!isEmpty(text) ? text : '--'}</span>; | |||
| return type === TableCellValueType.Link | |||
| ? renderLink(text, ellipsis, record, onClick) | |||
| : renderText(text, ellipsis); | |||
| } | |||
| function renderLink<T>( | |||
| text: any | undefined | null, | |||
| ellipsis: boolean | 'auto', | |||
| record: T, | |||
| onClick?: (record: T, e: React.MouseEvent) => void, | |||
| ) { | |||
| return ( | |||
| <a className="kf-table-row-link" onClick={(e) => onClick?.(record, e)}> | |||
| {text} | |||
| {renderText(text, ellipsis)} | |||
| </a> | |||
| ); | |||
| } | |||
| function renderText(text: any | undefined | null, ellipsis: boolean | 'auto') { | |||
| if (ellipsis === 'auto') { | |||
| return ( | |||
| <Typography.Paragraph | |||
| style={{ marginBottom: 0 }} | |||
| ellipsis={{ | |||
| tooltip: { | |||
| title: text, | |||
| destroyTooltipOnHide: true, | |||
| overlayStyle: { maxWidth: 400 }, | |||
| }, | |||
| }} | |||
| > | |||
| {!isEmpty(text) ? text : '--'} | |||
| </Typography.Paragraph> | |||
| ); | |||
| } | |||
| return ( | |||
| <span | |||
| style={ | |||
| ellipsis | |||
| ? { | |||
| whiteSpace: 'nowrap', | |||
| overflow: 'hidden', | |||
| textOverflow: 'ellipsis', | |||
| wordBreak: 'break-all', | |||
| display: 'inline-block', | |||
| maxWidth: '100%', | |||
| } | |||
| : undefined | |||
| } | |||
| > | |||
| {!isEmpty(text) ? text : '--'} | |||
| </span> | |||
| ); | |||
| } | |||
| export default tableCellRender; | |||