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
This commit is contained in:
jialin
2026-07-16 13:09:18 +08:00
committed by jialin
parent 938aef6c26
commit 67e2d914f4
10 changed files with 255 additions and 212 deletions
@@ -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<number>[];
}
interface TargetItemProps {
interface TargetItemProps extends SharedGrid {
onSelect: (val: any, record: any) => void;
data: any;
sourceModels: any[];
modelList?: Global.BaseOption<number>[];
}
// 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<TargetItemProps> = ({
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<TargetItemProps> = ({
</span>
);
};
const sourceNode = renderProviderSource();
const weightNode =
data.fallback_status_codes && data.fallback_status_codes?.length > 0 ? (
<>
{data.weight > 0 && <span style={{ marginInline: 8 }}>/</span>}
<span>{intl.formatMessage({ id: 'routes.table.label.fallback' })}</span>
</>
) : (
<AutoTooltip ghost>
{intl.formatMessage({ id: 'routes.form.target.weight' })}:{' '}
{data.weight || 0}
</AutoTooltip>
);
const statusNode = (
<StatusTag
statusValue={{
status: TargetStatus[data.state],
text: TargetStatusLabelMap[data.state],
message: ''
}}
/>
);
return (
<div style={{ borderRadius: 'var(--ant-table-header-border-radius)' }}>
<RowChildren>
<Row
gutter={16}
style={{ width: '100%', color: 'var(--ant-color-text-secondary)' }}
<ExpandedRowGrid
gridTemplate={gridTemplate}
prefixWidth={prefixWidth}
style={{ color: 'var(--ant-color-text-secondary)' }}
>
<ExpandedRowGrid.Cell span={1} style={{ height: '100%', gap: 4 }}>
<AutoTooltip ghost>{data.name}</AutoTooltip>
{!!data.overridden_model_name && !!data.model_id && (
<FilesTag color="purple" variant="outlined">
<span style={{ opacity: 1 }}>LoRA</span>
</FilesTag>
)}
</ExpandedRowGrid.Cell>
{splitMiddle ? (
// Enterprise: org / targets / quota → one track each.
<>
<ExpandedRowGrid.Cell span={1}>{sourceNode}</ExpandedRowGrid.Cell>
<ExpandedRowGrid.Cell span={middleSpan - 2}>
{weightNode}
</ExpandedRowGrid.Cell>
<ExpandedRowGrid.Cell span={1}>{statusNode}</ExpandedRowGrid.Cell>
</>
) : (
// Only `targets` exists: share the one track via a 5:2:3 sub-grid.
// A raw grid div (not <Cell>) because it needs `display: grid`.
<div
style={{
gridColumn: `span ${middleSpan}`,
minWidth: 0,
display: 'grid',
gridTemplateColumns: 'minmax(0, 5fr) minmax(0, 2fr) minmax(0, 3fr)',
alignItems: 'center'
}}
>
<Col span={5}>
<CellContent
style={{
gap: 4,
paddingInline: 'var(--ant-table-cell-padding-inline)'
}}
>
<AutoTooltip ghost>{data.name}</AutoTooltip>
{!!data.overridden_model_name && !!data.model_id && (
<FilesTag color="purple" variant="outlined">
<span style={{ opacity: 1 }}>LoRA</span>
</FilesTag>
)}
</CellContent>
</Col>
<Col span={5} style={{ paddingLeft: 64 }}>
<CellContent>{renderProviderSource()}</CellContent>
</Col>
<Col span={2}>
<CellContent>
{data.fallback_status_codes &&
data.fallback_status_codes?.length > 0 ? (
<>
{data.weight > 0 && (
<span style={{ marginInline: 8 }}>/</span>
)}
<span>
{intl.formatMessage({
id: 'routes.table.label.fallback'
})}
</span>
</>
) : (
<AutoTooltip ghost>
{intl.formatMessage({ id: 'routes.form.target.weight' })}:{' '}
{data.weight || 0}
</AutoTooltip>
)}
</CellContent>
</Col>
<Col span={3}>
<CellContent>
<AutoTooltip ghost>
<StatusTag
statusValue={{
status: TargetStatus[data.state],
text: TargetStatusLabelMap[data.state],
message: ''
}}
/>
</AutoTooltip>
</CellContent>
</Col>
<Col span={5}>
<CellContent style={{ paddingLeft: 45 }}>
<AutoTooltip ghost minWidth={20}>
{dayjs(data.created_at).format('YYYY-MM-DD HH:mm:ss')}
</AutoTooltip>
</CellContent>
</Col>
<Col span={4}>
<CellContent
style={{
paddingLeft: 38
}}
>
<DropdownButtons
items={childActionList}
onSelect={(val) => onSelect(val, data)}
></DropdownButtons>
</CellContent>
</Col>
</Row>
</RowChildren>
</div>
<div style={subCellStyle}>{sourceNode}</div>
<div style={subCellStyle}>{weightNode}</div>
<div style={subCellStyle}>{statusNode}</div>
</div>
)}
<ExpandedRowGrid.Cell span={1} style={{ height: '100%' }}>
<AutoTooltip ghost minWidth={20}>
{dayjs(data.created_at).format('YYYY-MM-DD HH:mm:ss')}
</AutoTooltip>
</ExpandedRowGrid.Cell>
<ExpandedRowGrid.Cell span={1} style={{ height: '100%' }}>
<DropdownButtons
items={childActionList}
onSelect={(val) => onSelect(val, data)}
></DropdownButtons>
</ExpandedRowGrid.Cell>
</ExpandedRowGrid>
);
};
@@ -172,7 +188,10 @@ const RouteTargets: React.FC<ProviderModelProps> = ({
dataList,
onSelect,
modelList,
sourceModels
sourceModels,
gridTemplate,
prefixWidth,
columns
}) => {
return (
<div>
@@ -183,6 +202,9 @@ const RouteTargets: React.FC<ProviderModelProps> = ({
onSelect={onSelect}
sourceModels={sourceModels}
modelList={modelList}
gridTemplate={gridTemplate}
prefixWidth={prefixWidth}
columns={columns}
></RouteItem>
))}
</div>
@@ -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) => (
<AutoTooltip ghost minWidth={20}>
{dayjs(value).format('YYYY-MM-DD HH:mm:ss')}
+3
View File
@@ -291,6 +291,9 @@ const ModelRoutes: React.FC = () => {
dataList={list}
onSelect={onChildSelect}
sourceModels={sourceModels}
gridTemplate={options.gridTemplate}
prefixWidth={options.prefixWidth}
columns={options.columns}
/>
);
};