From 9e0634f449fc9ef4dc63381dd806366bbd986fab Mon Sep 17 00:00:00 2001 From: jialin Date: Wed, 19 Feb 2025 15:16:14 +0800 Subject: [PATCH] refactor: models list table columns from props --- .../seal-table/components/header.tsx | 41 ++- .../seal-table/components/row-prefix.tsx | 64 +++++ .../seal-table/components/row-skeleton.tsx | 13 - .../seal-table/components/table-body.tsx | 3 +- .../seal-table/components/table-row.tsx | 137 ++++----- .../seal-table/components/table-skeleton.tsx | 32 +++ src/components/seal-table/index.tsx | 72 +++-- src/components/seal-table/types.ts | 8 +- .../llmodels/components/instance-item.tsx | 84 +++--- src/pages/llmodels/components/table-list.tsx | 259 +++++++++--------- src/pages/llmodels/config/types.ts | 2 + .../components/container-install.tsx | 2 +- 12 files changed, 372 insertions(+), 345 deletions(-) create mode 100644 src/components/seal-table/components/row-prefix.tsx delete mode 100644 src/components/seal-table/components/row-skeleton.tsx create mode 100644 src/components/seal-table/components/table-skeleton.tsx diff --git a/src/components/seal-table/components/header.tsx b/src/components/seal-table/components/header.tsx index ef146156..ff951561 100644 --- a/src/components/seal-table/components/header.tsx +++ b/src/components/seal-table/components/header.tsx @@ -4,16 +4,16 @@ import { SealColumnProps } from '../types'; import TableHeader from './table-header'; interface HeaderProps { - children: React.ReactNode[]; + columns: SealColumnProps[]; onSort?: (dataIndex: string, order: any) => void; } const Header: React.FC = (props) => { const { onSort } = props; + return ( - {React.Children.map(props.children, (child, i) => { - const { props: columnProps } = child as any; + {props.columns?.map((columnProps, i) => { const { title, dataIndex, @@ -24,25 +24,22 @@ const Header: React.FC = (props) => { sorter, defaultSortOrder } = columnProps as SealColumnProps; - if (React.isValidElement(child)) { - return ( - - - - ); - } - return null; + return ( + + + + ); })} ); diff --git a/src/components/seal-table/components/row-prefix.tsx b/src/components/seal-table/components/row-prefix.tsx new file mode 100644 index 00000000..2372a2fd --- /dev/null +++ b/src/components/seal-table/components/row-prefix.tsx @@ -0,0 +1,64 @@ +import { DownOutlined, RightOutlined } from '@ant-design/icons'; +import { Button, Checkbox } from 'antd'; +import _ from 'lodash'; +import React from 'react'; + +interface RowPrefixProps { + expandable?: boolean | React.ReactNode; + enableSelection?: boolean; + expanded?: boolean; + checked?: boolean; + handleRowExpand?: () => void; + handleSelectChange?: (e: any) => void; +} + +const RowPrefix: React.FC = (props) => { + const { + expandable, + enableSelection, + expanded, + checked, + handleRowExpand, + handleSelectChange + } = props; + + if (expandable && enableSelection) { + return ( +
+ + {_.isBoolean(expandable) ? ( + + ) : ( + expandable + )} + + +
+ ); + } + if (expandable) { + return ( +
+ {_.isBoolean(expandable) ? ( + + ) : ( + expandable + )} +
+ ); + } + if (enableSelection) { + return ( +
+ {} +
+ ); + } + return null; +}; + +export default RowPrefix; diff --git a/src/components/seal-table/components/row-skeleton.tsx b/src/components/seal-table/components/row-skeleton.tsx deleted file mode 100644 index 2940bbad..00000000 --- a/src/components/seal-table/components/row-skeleton.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Checkbox } from 'antd'; -import '../styles/skeleton.less'; - -const RowSkeleton = () => { - return ( -
-
- -
- ); -}; - -export default RowSkeleton; diff --git a/src/components/seal-table/components/table-body.tsx b/src/components/seal-table/components/table-body.tsx index 6f0ac0bc..6da6f5da 100644 --- a/src/components/seal-table/components/table-body.tsx +++ b/src/components/seal-table/components/table-body.tsx @@ -1,10 +1,11 @@ import { Empty } from 'antd'; import React from 'react'; +import { SealColumnProps } from '../types'; import TableRow from './table-row'; interface TableBodyProps { dataSource: any[]; - columns: React.ReactNode[]; + columns: SealColumnProps[]; rowKey: string; rowSelection?: any; expandable?: any; diff --git a/src/components/seal-table/components/table-row.tsx b/src/components/seal-table/components/table-row.tsx index 564e65b2..4862ca2d 100644 --- a/src/components/seal-table/components/table-row.tsx +++ b/src/components/seal-table/components/table-row.tsx @@ -2,14 +2,21 @@ import useSetChunkRequest, { createAxiosToken } from '@/hooks/use-chunk-request'; import useUpdateChunkedList from '@/hooks/use-update-chunk-list'; -import { DownOutlined, RightOutlined } from '@ant-design/icons'; -import { Button, Checkbox, Col, Empty, Row, Spin } from 'antd'; +import { Col, Empty, Row, Spin } from 'antd'; import classNames from 'classnames'; import _ from 'lodash'; -import React, { useEffect, useRef, useState } from 'react'; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState +} from 'react'; import RowContext from '../row-context'; import TableContext from '../table-context'; import { RowContextProps, SealTableProps } from '../types'; +import RowPrefix from './row-prefix'; +import TableColumn from './seal-column'; const TableRow: React.FC< RowContextProps & @@ -38,7 +45,7 @@ const TableRow: React.FC< }>(TableContext); const { setChunkRequest } = useSetChunkRequest(); const [expanded, setExpanded] = useState(false); - const [checked, setChecked] = useState(false); + // const [checked, setChecked] = useState(false); const [childrenData, setChildrenData] = useState([]); const [loading, setLoading] = useState(false); const [firstLoad, setFirstLoad] = useState(true); @@ -56,17 +63,6 @@ const TableRow: React.FC< // callback: (list) => renderChildren?.(list) }); - useEffect(() => { - if (rowSelection) { - const { selectedRowKeys } = rowSelection; - if (selectedRowKeys.includes(record[rowKey])) { - setChecked(true); - } else { - setChecked(false); - } - } - }, [rowSelection]); - useEffect(() => { return () => { if (pollTimer.current) { @@ -77,7 +73,23 @@ const TableRow: React.FC< }; }, []); + const checked = useMemo(() => { + return rowSelection?.selectedRowKeys?.includes(record[rowKey]); + }, [rowSelection?.selectedRowKeys, record, rowKey]); + const renderChildrenData = () => { + if (childrenData.length === 0) { + return ( + + ); + } return renderChildren?.(childrenData, record); }; @@ -92,7 +104,7 @@ const TableRow: React.FC< } }; - const handleLoadChildren = async () => { + const handleLoadChildren = useCallback(async () => { try { axiosToken.current?.cancel?.(); axiosToken.current = createAxiosToken(); @@ -109,7 +121,7 @@ const TableRow: React.FC< } finally { setFirstLoad(false); } - }; + }, [record, loadChildren]); const filterUpdateChildrenHandler = () => { if (!expandedRowKeys?.includes(record[rowKey])) { @@ -224,51 +236,6 @@ const TableRow: React.FC< }; }, [updateChild, tableContext.allChildren]); - const renderRowPrefix = () => { - if (expandable && rowSelection) { - return ( -
- - {_.isBoolean(expandable) ? ( - - ) : ( - expandable - )} - - -
- ); - } - if (expandable) { - return ( -
- {_.isBoolean(expandable) ? ( - - ) : ( - expandable - )} -
- ); - } - if (rowSelection) { - return ( -
- { - - } -
- ); - } - return null; - }; - return (
@@ -277,40 +244,30 @@ const TableRow: React.FC< 'row-wrapper-selected': checked })} > - {renderRowPrefix()} + - {React.Children.map(columns, (child) => { - const { props: columnProps } = child as any; - if (React.isValidElement(child)) { - return ( - - {child} - - ); - } - return ; + {columns?.map((columnProps) => { + return ( + + + + ); })}
{expanded && (
- - {childrenData.length ? ( - renderChildrenData() - ) : ( - - )} - + {renderChildrenData()}
)} diff --git a/src/components/seal-table/components/table-skeleton.tsx b/src/components/seal-table/components/table-skeleton.tsx new file mode 100644 index 00000000..ef270179 --- /dev/null +++ b/src/components/seal-table/components/table-skeleton.tsx @@ -0,0 +1,32 @@ +import { Checkbox } from 'antd'; +import React from 'react'; +import styled from 'styled-components'; +import '../styles/skeleton.less'; + +const SkeletonItem = () => { + return ( +
+
+ +
+ ); +}; + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + gap: 20px; +`; + +const TableSkeleton = () => { + const dataSource = Array.from({ length: 5 }); + return ( + + {dataSource.map((item, index) => ( + + ))} + + ); +}; + +export default React.memo(TableSkeleton); diff --git a/src/components/seal-table/index.tsx b/src/components/seal-table/index.tsx index a4709d9f..a7d6f679 100644 --- a/src/components/seal-table/index.tsx +++ b/src/components/seal-table/index.tsx @@ -1,16 +1,17 @@ import { Pagination, Spin, type PaginationProps } from 'antd'; import _ from 'lodash'; -import React, { useEffect, useState } from 'react'; +import React, { useMemo } from 'react'; import Header from './components/header'; import HeaderPrefix from './components/header-prefix'; import TableBody from './components/table-body'; import './styles/index.less'; -import { SealTableProps } from './types'; +import { SealColumnProps, SealTableProps } from './types'; const SealTable: React.FC = ( props ) => { const { + columns, children, rowKey, childParentKey, @@ -29,33 +30,46 @@ const SealTable: React.FC = ( loadChildren, loadChildrenAPI } = props; - const [selectState, setSelectState] = useState({ - selectAll: false, - indeterminate: false - }); - useEffect(() => { - if (rowSelection) { - const { selectedRowKeys } = rowSelection; - const selectedKeys = new Set(rowSelection.selectedRowKeys); - const allRowKeys = props.dataSource.map((record) => record[rowKey]); - if (!selectedRowKeys?.length) { - setSelectState({ - selectAll: false, - indeterminate: false - }); - } else if (allRowKeys.every((key) => selectedKeys.has(key))) { - setSelectState({ - selectAll: true, - indeterminate: false - }); - } else { - setSelectState({ - selectAll: false, - indeterminate: true - }); - } + const parsedColumns = useMemo(() => { + if (columns) return columns; + + return React.Children.toArray(children) + .filter(React.isValidElement) + .map((child) => { + const column = child as React.ReactElement; + const { title, dataIndex, key, render, ...restProps } = column.props; + + return { + title, + dataIndex, + key: key || dataIndex, + render, + ...restProps + }; + }); + }, [columns, children]); + + const selectState = useMemo(() => { + const selectedRowKeys = rowSelection?.selectedRowKeys || []; + const selectedKeys = new Set(selectedRowKeys); + const allRowKeys = props.dataSource.map((record) => record[rowKey]); + if (!selectedRowKeys?.length) { + return { + selectAll: false, + indeterminate: false + }; + } + if (allRowKeys.every((key) => selectedKeys.has(key))) { + return { + selectAll: true, + indeterminate: false + }; } + return { + selectAll: false, + indeterminate: true + }; }, [rowSelection?.selectedRowKeys, props.dataSource, rowKey]); const selectAllRows = () => { @@ -103,12 +117,12 @@ const SealTable: React.FC = ( expandable={expandable} enableSelection={rowSelection?.enableSelection} > -
{children}
+
React.ReactNode; dataIndex: string; + key?: string; width?: number; span: number; align?: 'left' | 'center' | 'right'; @@ -18,6 +19,7 @@ export interface SealColumnProps { }; valueType?: 'text' | 'number' | 'date' | 'datetime' | 'time'; sortOrder?: 'ascend' | 'descend' | null; + [key: string]: any; } export interface TableHeaderProps { @@ -26,7 +28,7 @@ export interface TableHeaderProps { sortOrder?: 'ascend' | 'descend' | null; dataIndex: string; onSort?: (dataIndex: string, order: 'ascend' | 'descend') => void; - children?: React.ReactNode; + children?: React.ReactElement; title: React.ReactNode; style?: React.CSSProperties; firstCell?: boolean; @@ -42,10 +44,11 @@ export interface RowSelectionProps { onChange: (selectedRowKeys: React.Key[], selectedRows: any[]) => void; } export interface SealTableProps { + columns?: SealColumnProps[]; childParentKey?: string; expandedRowKeys?: React.Key[]; rowSelection?: RowSelectionProps; - children: React.ReactNode[]; + children?: React.ReactElement[]; empty?: React.ReactNode; expandable?: React.ReactNode; dataSource: any[]; @@ -65,7 +68,6 @@ export interface SealTableProps { export interface RowContextProps { record: Record; - columns: React.ReactNode[]; pollingChildren?: boolean; rowIndex: number; } diff --git a/src/pages/llmodels/components/instance-item.tsx b/src/pages/llmodels/components/instance-item.tsx index 5115ec9d..05fb1a4f 100644 --- a/src/pages/llmodels/components/instance-item.tsx +++ b/src/pages/llmodels/components/instance-item.tsx @@ -18,7 +18,7 @@ import { useIntl } from '@umijs/max'; import { Button, Col, Divider, Row, Space, Tag, Tooltip } from 'antd'; import dayjs from 'dayjs'; import _ from 'lodash'; -import React, { useCallback } from 'react'; +import React from 'react'; import { InstanceStatusMap, InstanceStatusMapValue, status } from '../config'; import { ModelInstanceListItem } from '../config/types'; import '../style/instance-item.less'; @@ -35,6 +35,38 @@ interface InstanceItemProps { ) => void; } +const childActionList = [ + { + label: 'common.button.viewlog', + key: 'viewlog', + status: [ + InstanceStatusMap.Initializing, + InstanceStatusMap.Running, + InstanceStatusMap.Error, + InstanceStatusMap.Starting, + InstanceStatusMap.Downloading + ], + icon: + }, + { + label: 'common.button.delrecreate', + key: 'delete', + props: { + danger: true + }, + icon: + } +]; + +const setChildActionList = (item: ModelInstanceListItem) => { + return _.filter(childActionList, (action: any) => { + if (action.key === 'viewlog') { + return action.status.includes(item.state); + } + return true; + }); +}; + const InstanceItem: React.FC = ({ list, workerList, @@ -43,56 +75,6 @@ const InstanceItem: React.FC = ({ }) => { const intl = useIntl(); - const distributeCols = [ - { - title: 'Worker', - key: 'worker_name' - }, - { - title: 'IP', - key: 'worker_ip', - render: ({ row }: { row: ModelInstanceListItem }) => { - return row.port ? `${row.worker_ip}:${row.port}` : row.worker_ip; - } - }, - { - title: intl.formatMessage({ id: 'models.table.gpuindex' }), - key: 'gpu_index' - } - ]; - - const childActionList = [ - { - label: 'common.button.viewlog', - key: 'viewlog', - status: [ - InstanceStatusMap.Initializing, - InstanceStatusMap.Running, - InstanceStatusMap.Error, - InstanceStatusMap.Starting, - InstanceStatusMap.Downloading - ], - icon: - }, - { - label: 'common.button.delrecreate', - key: 'delete', - props: { - danger: true - }, - icon: - } - ]; - - const setChildActionList = useCallback((item: ModelInstanceListItem) => { - return _.filter(childActionList, (action: any) => { - if (action.key === 'viewlog') { - return action.status.includes(item.state); - } - return true; - }); - }, []); - const renderWorkerInfo = (item: ModelInstanceListItem) => { let workerIp = '-'; if (item.worker_ip) { diff --git a/src/pages/llmodels/components/table-list.tsx b/src/pages/llmodels/components/table-list.tsx index 55e71263..9aa30fa4 100644 --- a/src/pages/llmodels/components/table-list.tsx +++ b/src/pages/llmodels/components/table-list.tsx @@ -6,7 +6,7 @@ import IconFont from '@/components/icon-font'; import { PageSize } from '@/components/logs-viewer/config'; import PageTools from '@/components/page-tools'; import SealTable from '@/components/seal-table'; -import SealColumn from '@/components/seal-table/components/seal-column'; +import { SealColumnProps } from '@/components/seal-table/types'; import { PageAction } from '@/config'; import HotKeys from '@/config/hotkeys'; import useBodyScroll from '@/hooks/use-body-scroll'; @@ -32,7 +32,7 @@ import { Button, Dropdown, Input, Select, Space, Tooltip, message } from 'antd'; import dayjs from 'dayjs'; import { useAtom } from 'jotai'; import _ from 'lodash'; -import { memo, useCallback, useEffect, useRef, useState } from 'react'; +import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { MODELS_API, @@ -113,6 +113,39 @@ const ActionList = [ } ]; +const generateSource = (record: ListItem) => { + if (record.source === modelSourceMap.modelscope_value) { + return `${modelSourceMap.modelScope}/${record.model_scope_model_id}`; + } + if (record.source === modelSourceMap.huggingface_value) { + return `${modelSourceMap.huggingface}/${record.huggingface_repo_id}`; + } + if (record.source === modelSourceMap.local_path_value) { + return `${record.local_path}`; + } + if (record.source === modelSourceMap.ollama_library_value) { + return `${modelSourceMap.ollama_library}/${record.ollama_library_model_name}`; + } + return ''; +}; + +const setActionList = (record: ListItem) => { + return _.filter(ActionList, (action: any) => { + if (action.key === 'chat') { + return record.ready_replicas > 0; + } + if (action.key === 'start') { + return record.replicas === 0; + } + + if (action.key === 'stop') { + return record.replicas > 0; + } + + return true; + }); +}; + const Models: React.FC = ({ handleNameChange, handleSearch, @@ -270,28 +303,11 @@ const Models: React.FC = ({ } ]; - const setActionList = useCallback((record: ListItem) => { - return _.filter(ActionList, (action: any) => { - if (action.key === 'chat') { - return record.ready_replicas > 0; - } - if (action.key === 'start') { - return record.replicas === 0; - } - - if (action.key === 'stop') { - return record.replicas > 0; - } - - return true; - }); - }, []); - const handleOnSort = (dataIndex: string, order: any) => { setSortOrder(order); }; - const handleOnCell = async (record: any, dataIndex: string) => { + const handleOnCell = useCallback(async (record: any, dataIndex: string) => { const params = { id: record.id, data: _.omit(record, [ @@ -304,7 +320,7 @@ const Models: React.FC = ({ }; await updateModel(params); message.success(intl.formatMessage({ id: 'common.message.success' })); - }; + }, []); const handleStartModel = async (row: ListItem) => { try { @@ -502,7 +518,7 @@ const Models: React.FC = ({ [deleteModelInstance] ); - const getModelInstances = async (row: any, options?: any) => { + const getModelInstances = useCallback(async (row: any, options?: any) => { try { const params = { id: row.id, @@ -516,11 +532,11 @@ const Models: React.FC = ({ } catch (error) { return []; } - }; + }, []); - const generateChildrenRequestAPI = (params: any) => { + const generateChildrenRequestAPI = useCallback((params: any) => { return `${MODELS_API}/${params.id}/instances`; - }; + }, []); const handleEdit = (row: ListItem) => { setCurrentData(row); @@ -588,22 +604,6 @@ const Models: React.FC = ({ [workerList] ); - const generateSource = useCallback((record: ListItem) => { - if (record.source === modelSourceMap.modelscope_value) { - return `${modelSourceMap.modelScope}/${record.model_scope_model_id}`; - } - if (record.source === modelSourceMap.huggingface_value) { - return `${modelSourceMap.huggingface}/${record.huggingface_repo_id}`; - } - if (record.source === modelSourceMap.local_path_value) { - return `${record.local_path}`; - } - if (record.source === modelSourceMap.ollama_library_value) { - return `${modelSourceMap.ollama_library}/${record.ollama_library_model_name}`; - } - return ''; - }, []); - const handleClickDropdown = (item: any) => { if (item.key === 'huggingface') { setOpenDeployModal({ @@ -667,6 +667,87 @@ const Models: React.FC = ({ }); }; + const columns: SealColumnProps[] = useMemo( + () => [ + { + title: intl.formatMessage({ id: 'common.table.name' }), + dataIndex: 'name', + key: 'name', + width: 400, + span: 5, + render: (text: string, record: ListItem) => ( + + + {text} + + + + ) + }, + { + title: intl.formatMessage({ id: 'models.form.source' }), + dataIndex: 'source', + key: 'source', + span: 6, + render: (text: string, record: ListItem) => ( + + {generateSource(record)} + + ) + }, + { + title: ( + + {intl.formatMessage({ id: 'models.form.replicas' })} + + + ), + dataIndex: 'replicas', + key: 'replicas', + align: 'center', + span: 4, + editable: { + valueType: 'number', + title: intl.formatMessage({ id: 'models.table.replicas.edit' }) + }, + render: (text: number, record: ListItem) => ( + + {record.ready_replicas} / {record.replicas} + + ) + }, + { + title: intl.formatMessage({ id: 'common.table.createTime' }), + dataIndex: 'created_at', + key: 'created_at', + defaultSortOrder: 'descend', + sortOrder, + sorter: false, + span: 5, + render: (text: number) => ( + + {dayjs(text).format('YYYY-MM-DD HH:mm:ss')} + + ) + }, + { + title: intl.formatMessage({ id: 'common.table.operation' }), + key: 'operation', + dataIndex: 'operation', + span: 4, + render: (text, record) => ( + handleSelect(val, record)} + /> + ) + } + ], + [sortOrder, intl] + ); + return ( <> = ({ } > = ({ hideOnSinglePage: queryParams.perPage === 10, onChange: handlePageChange }} - > - { - return ( - - - {text} - - - - ); - }} - /> - { - return ( - - {generateSource(record)} - - ); - }} - /> - - {intl.formatMessage({ id: 'models.form.replicas' })} - - - } - dataIndex="replicas" - key="replicas" - align="center" - span={4} - editable={{ - valueType: 'number', - title: intl.formatMessage({ id: 'models.table.replicas.edit' }) - }} - render={(text, record: ListItem) => { - return ( - - {record.ready_replicas} / {record.replicas} - - ); - }} - /> - { - return ( - - {dayjs(text).format('YYYY-MM-DD HH:mm:ss')} - - ); - }} - /> - { - return ( - handleSelect(val, record)} - > - ); - }} - /> - + > = (props) => { token: '${mytoken}', workerip: '${myworkerip}' }); - }, [versionInfo, activeKey, props.token, origin]); + }, [versionInfo, activeKey, props.token]); return (