From 67e2d914f45485b0f47a72cd79c92d8490ab8ebd Mon Sep 17 00:00:00 2001 From: jialin Date: Thu, 16 Jul 2026 12:02:37 +0800 Subject: [PATCH] refactor(tables): share parent column grid in expandable child rows - llmodels/model-routes/cluster child rows use ExpandedRowGrid + Cell (auto-flow spans, no parent-key/grid-line math) - define --seal-table-row-min-height token in global.less - routes created_at column: span -> fixed width 180 --- src/global.less | 1 + src/pages/cluster-management/clusters.tsx | 3 + .../components/pool-rows.tsx | 85 +++++--- .../hooks/use-pools-columns.tsx | 18 -- .../components/instance/instance-item.tsx | 137 ++++++------ .../components/instance/instances.tsx | 9 + src/pages/llmodels/components/table-list.tsx | 3 + .../model-routes/components/route-targets.tsx | 206 ++++++++++-------- .../model-routes/hooks/use-routes-columns.tsx | 2 +- src/pages/model-routes/index.tsx | 3 + 10 files changed, 255 insertions(+), 212 deletions(-) diff --git a/src/global.less b/src/global.less index efb721a1..8c8864da 100644 --- a/src/global.less +++ b/src/global.less @@ -91,6 +91,7 @@ html { // ======== container ============ --color-border-container: #ededed; --color-text-table-header: #71717a; + --seal-table-row-min-height: 68px; } html[data-theme='realDark'] { diff --git a/src/pages/cluster-management/clusters.tsx b/src/pages/cluster-management/clusters.tsx index 01772a99..704cfb38 100644 --- a/src/pages/cluster-management/clusters.tsx +++ b/src/pages/cluster-management/clusters.tsx @@ -358,6 +358,9 @@ const Clusters: React.FC = () => { dataList={list} provider={options.parent?.provider} clusterId={options.parent?.id} + gridTemplate={options.gridTemplate} + prefixWidth={options.prefixWidth} + columns={options.columns} /> ); }; diff --git a/src/pages/cluster-management/components/pool-rows.tsx b/src/pages/cluster-management/components/pool-rows.tsx index 7ec8379f..3c755a95 100644 --- a/src/pages/cluster-management/components/pool-rows.tsx +++ b/src/pages/cluster-management/components/pool-rows.tsx @@ -2,13 +2,14 @@ import { PageAction } from '@/config'; import { PageActionType } from '@/config/types'; import { CellContent, + type ChildGridOptions, DeleteModal, - RowChildren, + ExpandedRowGrid, TableRowProvider } from '@gpustack/core-ui'; import { useIntl } from '@umijs/max'; import { useMemoizedFn } from 'ahooks'; -import { Col, message, Row } from 'antd'; +import { message } from 'antd'; import _ from 'lodash'; import React, { useRef, useState } from 'react'; import { deleteWorkerPool, updateWorkerPool } from '../apis'; @@ -16,7 +17,10 @@ import { ProviderType } from '../config'; import { NodePoolFormData, NodePoolListItem } from '../config/types'; import usePoolsColumns from '../hooks/use-pools-columns'; import AddPool from './add-pool'; -interface PoolRowsProps { +interface PoolRowsProps extends Pick< + ChildGridOptions, + 'gridTemplate' | 'prefixWidth' | 'columns' +> { dataList: NodePoolListItem[]; provider: ProviderType; clusterId: number | string; @@ -25,9 +29,35 @@ interface PoolRowsProps { const PoolRows: React.FC = ({ dataList, provider, - clusterId + clusterId, + gridTemplate, + prefixWidth = 0, + columns: parentColumns }) => { const intl = useIntl(); + + // The child row shares the parent's column grid; cells flow left-to-right and + // only declare a span, keyed on the pool column's OWN dataIndex — never on a + // parent cluster column key. Parent layout: name (1) | provider…state middle + // region | created_at (1) | operations (1). The three middle pool columns + // cover that region: `replicas`→state (last, 1 track), `image_name`→ + // models+workers (2 tracks), `instance_type` absorbs the rest (plugins + + // provider + gpus). + const columnCount = parentColumns?.length ?? 0; + const middleSpan = Math.max(columnCount - 3, 1); + const spanFor = (dataIndex: string): number => { + switch (dataIndex) { + case 'instance_type': + return Math.max(middleSpan - 3, 1); + case 'image_name': + return 2; + case 'replicas': + return 1; + default: + // name / created_at / operations align 1:1 with their parent column. + return 1; + } + }; const modalRef = useRef(null); const [addPoolStatus, setAddPoolStatus] = useState<{ open: boolean; @@ -62,8 +92,10 @@ const PoolRows: React.FC = ({ } }; - const handleOnCell = async (row: NodePoolListItem, dataIndex: string) => { - console.log('handleOncell===', row, dataIndex); + const handleOnCell = async ( + row: NodePoolListItem, + _data: { dataIndex: string; newValue: any; oldValue: any } + ) => { try { await updateWorkerPool({ data: row, @@ -118,31 +150,26 @@ const PoolRows: React.FC = ({ <> {dataList?.map((data: NodePoolListItem) => { return ( -
- - - - {columns.map((col: Record) => { - return ( - - - - ); - })} - - - -
+ + {columns.map((col: Record) => ( + + + + ))} + + ); })} ( {text} @@ -60,9 +57,6 @@ const usePoolsColumns = ( showTitle: false }, span: 4, - style: { - paddingLeft: 62 - }, render: (text: string, record: ListItem) => ( ( ( {dayjs(text).format('YYYY-MM-DD HH:mm:ss')} @@ -179,9 +164,6 @@ const usePoolsColumns = ( key: 'operations', dataIndex: 'operations', span: 3, - style: { - paddingLeft: 36 - }, render: (text: string, record: ListItem) => ( void; } @@ -24,79 +29,67 @@ const InstanceItem: React.FC = ({ workerList, modelData, defaultOpenId, + gridTemplate, + prefixWidth = 0, + columns, handleChildSelect }) => { + // The child row shares the parent's column grid; cells flow left-to-right and + // only declare a span, so there is no dependency on parent column keys. + // Parent layout: name (1) | middle plugin/source region | replicas, + // created_at, operation (last 3). The middle absorbs whatever columns sit + // between name and replicas (cluster_id, source, any plugin column). + const columnCount = columns?.length ?? 0; + const middleSpan = Math.max(columnCount - 4, 1); + return ( -
- - - - - - - - - - - - - - - - - - - - - {dayjs(instanceData.created_at).format('YYYY-MM-DD HH:mm:ss')} - - - - -
- -
- -
-
-
+ + + + + + + + + + + + + + + {dayjs(instanceData.created_at).format('YYYY-MM-DD HH:mm:ss')} + + + + + + ); }; export default InstanceItem; diff --git a/src/pages/llmodels/components/instance/instances.tsx b/src/pages/llmodels/components/instance/instances.tsx index 4e31b3ef..d28544e1 100644 --- a/src/pages/llmodels/components/instance/instances.tsx +++ b/src/pages/llmodels/components/instance/instances.tsx @@ -17,6 +17,9 @@ interface InstanceItemProps { workerList: WorkerListItem[]; modelData?: any; currentExpanded?: string; + gridTemplate?: string; + prefixWidth?: number; + columns?: any[]; handleChildSelect: (val: string, item: ModelInstanceListItem) => void; } @@ -25,6 +28,9 @@ const Instances: React.FC = ({ workerList, modelData, currentExpanded, + gridTemplate, + prefixWidth, + columns, handleChildSelect }) => { const [firstLoad, setFirstLoad] = React.useState(true); @@ -55,6 +61,9 @@ const Instances: React.FC = ({ instanceData={item} defaultOpenId={firstLoad ? defaultOpenId : ''} handleChildSelect={handleChildSelect} + gridTemplate={gridTemplate} + prefixWidth={prefixWidth} + columns={columns} > ); })} diff --git a/src/pages/llmodels/components/table-list.tsx b/src/pages/llmodels/components/table-list.tsx index 4e978fb4..c702b86d 100644 --- a/src/pages/llmodels/components/table-list.tsx +++ b/src/pages/llmodels/components/table-list.tsx @@ -441,6 +441,9 @@ const Models: React.FC = ({ modelData={options.parent} workerList={workerList} handleChildSelect={handleChildSelect} + gridTemplate={options.gridTemplate} + prefixWidth={options.prefixWidth} + columns={options.columns} > ); }, diff --git a/src/pages/model-routes/components/route-targets.tsx b/src/pages/model-routes/components/route-targets.tsx index 19ba9e89..9797ff82 100644 --- a/src/pages/model-routes/components/route-targets.tsx +++ b/src/pages/model-routes/components/route-targets.tsx @@ -2,24 +2,19 @@ import ProviderLogo from '@/pages/maas-provider/components/provider-logo'; import { DeleteOutlined } from '@ant-design/icons'; import { AutoTooltip, + ChildGridOptions, DropdownButtons, - RowChildren, + ExpandedRowGrid, StatusTag } from '@gpustack/core-ui'; import { useIntl } from '@umijs/max'; -import { Col, Row, Tag } from 'antd'; +import { Tag } from 'antd'; import dayjs from 'dayjs'; import React from 'react'; import styled from 'styled-components'; import { TargetStatus, TargetStatusLabelMap } from '../config'; import { RouteTarget } from '../config/types'; -const CellContent = styled.div` - display: flex; - align-items: center; - height: 100%; -`; - const FilesTag = styled(Tag)` cursor: pointer; display: flex; @@ -29,20 +24,33 @@ const FilesTag = styled(Tag)` border-radius: 12px; `; -interface ProviderModelProps { +type SharedGrid = Pick< + ChildGridOptions, + 'gridTemplate' | 'prefixWidth' | 'columns' +>; + +interface ProviderModelProps extends SharedGrid { dataList: RouteTarget[]; onSelect: (val: any, record: any) => void; sourceModels: any[]; modelList?: Global.BaseOption[]; } -interface TargetItemProps { +interface TargetItemProps extends SharedGrid { onSelect: (val: any, record: any) => void; data: any; sourceModels: any[]; modelList?: Global.BaseOption[]; } +// Sub-column inside the merged `targets` cell. +const subCellStyle: React.CSSProperties = { + minWidth: 0, + display: 'flex', + alignItems: 'center', + paddingInline: 'var(--ant-table-cell-padding-inline)' +}; + export const childActionList = [ { key: 'delete', @@ -58,10 +66,24 @@ const RouteItem: React.FC = ({ onSelect, data, sourceModels, - modelList + modelList, + gridTemplate, + prefixWidth = 0, + columns }) => { const intl = useIntl(); + // The child row shares the parent's column grid. Cells flow left-to-right, + // so each cell only declares how many parent columns it spans. Parent layout + // is always: name (1) | middle plugin region | created_at (1) | operations (1). + // Enterprise inserts plugin columns (org, quota) into the middle region. + const columnCount = columns?.length ?? 0; + const middleSpan = Math.max(columnCount - 3, 1); + // With ≥3 middle tracks (enterprise: org / targets / quota) give source, + // weight and status their own tracks; source pins to the first middle track, + // status to the last, weight absorbs whatever is between. + const splitMiddle = middleSpan >= 3; + const renderProviderSource = () => { const model = sourceModels.find((item: any) => { if (data.model_id) { @@ -83,88 +105,82 @@ const RouteItem: React.FC = ({ ); }; + + const sourceNode = renderProviderSource(); + const weightNode = + data.fallback_status_codes && data.fallback_status_codes?.length > 0 ? ( + <> + {data.weight > 0 && /} + {intl.formatMessage({ id: 'routes.table.label.fallback' })} + + ) : ( + + {intl.formatMessage({ id: 'routes.form.target.weight' })}:{' '} + {data.weight || 0} + + ); + const statusNode = ( + + ); + return ( -
- - + + {data.name} + {!!data.overridden_model_name && !!data.model_id && ( + + LoRA + + )} + + {splitMiddle ? ( + // Enterprise: org / targets / quota → one track each. + <> + {sourceNode} + + {weightNode} + + {statusNode} + + ) : ( + // Only `targets` exists: share the one track via a 5:2:3 sub-grid. + // A raw grid div (not ) because it needs `display: grid`. +
- - - {data.name} - {!!data.overridden_model_name && !!data.model_id && ( - - LoRA - - )} - - - - {renderProviderSource()} - - - - {data.fallback_status_codes && - data.fallback_status_codes?.length > 0 ? ( - <> - {data.weight > 0 && ( - / - )} - - {intl.formatMessage({ - id: 'routes.table.label.fallback' - })} - - - ) : ( - - {intl.formatMessage({ id: 'routes.form.target.weight' })}:{' '} - {data.weight || 0} - - )} - - - - - - - - - - - - - {dayjs(data.created_at).format('YYYY-MM-DD HH:mm:ss')} - - - - - - onSelect(val, data)} - > - - - - -
+
{sourceNode}
+
{weightNode}
+
{statusNode}
+
+ )} + + + {dayjs(data.created_at).format('YYYY-MM-DD HH:mm:ss')} + + + + onSelect(val, data)} + > + + ); }; @@ -172,7 +188,10 @@ const RouteTargets: React.FC = ({ dataList, onSelect, modelList, - sourceModels + sourceModels, + gridTemplate, + prefixWidth, + columns }) => { return (
@@ -183,6 +202,9 @@ const RouteTargets: React.FC = ({ onSelect={onSelect} sourceModels={sourceModels} modelList={modelList} + gridTemplate={gridTemplate} + prefixWidth={prefixWidth} + columns={columns} > ))}
diff --git a/src/pages/model-routes/hooks/use-routes-columns.tsx b/src/pages/model-routes/hooks/use-routes-columns.tsx index b261dab7..1d61fbf4 100644 --- a/src/pages/model-routes/hooks/use-routes-columns.tsx +++ b/src/pages/model-routes/hooks/use-routes-columns.tsx @@ -232,7 +232,7 @@ const useAccessColumns = ({ title: intl.formatMessage({ id: 'common.table.createTime' }), dataIndex: 'created_at', sorter: tableSorter(6), - span: createTimeSpan, + width: 180, render: (value: string) => ( {dayjs(value).format('YYYY-MM-DD HH:mm:ss')} diff --git a/src/pages/model-routes/index.tsx b/src/pages/model-routes/index.tsx index 7ee5f73e..f66d035a 100644 --- a/src/pages/model-routes/index.tsx +++ b/src/pages/model-routes/index.tsx @@ -291,6 +291,9 @@ const ModelRoutes: React.FC = () => { dataList={list} onSelect={onChildSelect} sourceModels={sourceModels} + gridTemplate={options.gridTemplate} + prefixWidth={options.prefixWidth} + columns={options.columns} /> ); };