style: model list buttons

This commit is contained in:
jialin
2025-03-12 10:43:59 +08:00
parent a7cfa05504
commit 5edbc1a7b5
24 changed files with 460 additions and 157 deletions
+1 -1
View File
@@ -89,7 +89,7 @@ export default [
name: 'apikeys', name: 'apikeys',
path: '/api-keys', path: '/api-keys',
key: 'apikeys', key: 'apikeys',
icon: 'LockOutlined', icon: 'KeyOutlined',
component: './api-keys' component: './api-keys'
}, },
{ {
+54 -16
View File
@@ -10,6 +10,11 @@ interface DropdownButtonsProps {
items: MenuProps['items']; items: MenuProps['items'];
size?: 'small' | 'middle' | 'large'; size?: 'small' | 'middle' | 'large';
trigger?: Trigger[]; trigger?: Trigger[];
showText?: boolean;
disabled?: boolean;
variant?: 'filled' | 'outlined';
color?: string;
extra?: React.ReactNode;
onSelect: (val: any, item?: any) => void; onSelect: (val: any, item?: any) => void;
} }
@@ -17,9 +22,16 @@ const DropdownButtons: React.FC<DropdownButtonsProps> = ({
items, items,
size = 'middle', size = 'middle',
trigger = ['hover'], trigger = ['hover'],
showText,
disabled,
variant,
color,
extra,
onSelect onSelect
}) => { }) => {
const headItem = _.head(items);
const intl = useIntl(); const intl = useIntl();
const handleMenuClick = (item: any) => { const handleMenuClick = (item: any) => {
const selectItem = _.find(items, { key: item.key }); const selectItem = _.find(items, { key: item.key });
onSelect(item.key, selectItem); onSelect(item.key, selectItem);
@@ -37,19 +49,20 @@ const DropdownButtons: React.FC<DropdownButtonsProps> = ({
return ( return (
<> <>
{items?.length === 1 ? ( {items?.length === 1 ? (
<Tooltip title={intl.formatMessage({ id: _.head(items)?.label })}> <Tooltip title={intl.formatMessage({ id: headItem?.label })}>
<Button <Button
className={classNames('dropdown-button', size)} className={classNames('dropdown-button', size)}
{..._.head(items)} icon={headItem?.icon}
icon={_.get(items, '0.icon')}
size={size} size={size}
{..._.get(items, '0.props')} {...headItem?.props}
onClick={handleButtonClick} onClick={handleButtonClick}
></Button> ></Button>
</Tooltip> </Tooltip>
) : ( ) : (
<Dropdown.Button <Dropdown.Button
disabled={disabled}
trigger={trigger} trigger={trigger}
type="primary"
dropdownRender={(menus: any) => { dropdownRender={(menus: any) => {
return ( return (
<div <div
@@ -67,9 +80,10 @@ const DropdownButtons: React.FC<DropdownButtonsProps> = ({
<Button <Button
{...item.props} {...item.props}
type="text" type="text"
size="middle" size={size}
icon={item.icon} icon={item.icon}
key={item.key} key={item.key}
disabled={item.disabled}
onClick={() => handleMenuClick(item)} onClick={() => handleMenuClick(item)}
style={{ width: '100%', justifyContent: 'flex-start' }} style={{ width: '100%', justifyContent: 'flex-start' }}
> >
@@ -81,21 +95,45 @@ const DropdownButtons: React.FC<DropdownButtonsProps> = ({
); );
}} }}
buttonsRender={([leftButton, rightButton]) => [ buttonsRender={([leftButton, rightButton]) => [
<Tooltip <>
title={intl.formatMessage({ id: _.head(items)?.label })} {showText ? (
key="leftButton" <Button
> {...headItem?.props}
<Button disabled={headItem?.disabled || disabled}
className={classNames('dropdown-button', size)} className={classNames('dropdown-button', size)}
onClick={handleButtonClick} onClick={handleButtonClick}
size={size} size={size}
icon={_.head(items)?.icon} icon={headItem?.icon}
></Button> variant={variant}
</Tooltip>, color={color}
>
{intl.formatMessage({
id: headItem?.label
})}
{extra}
</Button>
) : (
<Tooltip
title={intl.formatMessage({ id: headItem?.label })}
key="leftButton"
>
<Button
{...headItem?.props}
className={classNames('dropdown-button', size)}
onClick={handleButtonClick}
size={size}
icon={headItem?.icon}
disabled={headItem?.disabled}
></Button>
</Tooltip>
)}
</>,
<Button <Button
icon={<MoreOutlined />} icon={<MoreOutlined />}
size={size} size={size}
key="menu" key="menu"
variant={variant}
color="default"
className={classNames('dropdown-button', size)} className={classNames('dropdown-button', size)}
></Button> ></Button>
]} ]}
+8 -3
View File
@@ -3,15 +3,20 @@ import React from 'react';
import styles from './styles/wrapper.less'; import styles from './styles/wrapper.less';
const Wrapper: React.FC<{ const Wrapper: React.FC<{
label?: string; label?: React.ReactNode;
description?: React.ReactNode; description?: React.ReactNode;
labelExtra?: React.ReactNode;
children: React.ReactNode; children: React.ReactNode;
}> = ({ children, label, description, ...rest }) => { }> = ({ children, label, description, labelExtra, ...rest }) => {
return ( return (
<div className={styles['wrapper']}> <div className={styles['wrapper']}>
{label && ( {label && (
<span className="label"> <span className="label">
<LabelInfo label={label} description={description}></LabelInfo> <LabelInfo
label={label}
description={description}
labelExtra={labelExtra}
></LabelInfo>
</span> </span>
)} )}
{React.isValidElement(children) {React.isValidElement(children)
+12 -3
View File
@@ -8,17 +8,26 @@ import ListItem from './list-item';
interface ListInputProps { interface ListInputProps {
dataList: string[]; dataList: string[];
label: string; label: React.ReactNode;
description?: React.ReactNode; description?: React.ReactNode;
btnText?: string; btnText?: string;
options?: Global.HintOptions[]; options?: Global.HintOptions[];
placeholder?: string; placeholder?: string;
labelExtra?: React.ReactNode;
onChange: (data: string[]) => void; onChange: (data: string[]) => void;
} }
const ListInput: React.FC<ListInputProps> = (props) => { const ListInput: React.FC<ListInputProps> = (props) => {
const intl = useIntl(); const intl = useIntl();
const { dataList, label, description, onChange, btnText, options } = props; const {
dataList,
label,
description,
onChange,
btnText,
options,
labelExtra
} = props;
const [list, setList] = React.useState<{ value: string; uid: number }[]>([]); const [list, setList] = React.useState<{ value: string; uid: number }[]>([]);
const countRef = React.useRef(0); const countRef = React.useRef(0);
const buttonRef = React.useRef<HTMLButtonElement>(null); const buttonRef = React.useRef<HTMLButtonElement>(null);
@@ -71,7 +80,7 @@ const ListInput: React.FC<ListInputProps> = (props) => {
}, [dataList]); }, [dataList]);
return ( return (
<Wrapper label={label} description={description}> <Wrapper label={label} description={description} labelExtra={labelExtra}>
<> <>
{_.map(list, (item: any, index: number) => { {_.map(list, (item: any, index: number) => {
return ( return (
@@ -7,9 +7,10 @@ interface NoteInfoProps {
required?: boolean; required?: boolean;
label: React.ReactNode; label: React.ReactNode;
description?: React.ReactNode; description?: React.ReactNode;
labelExtra?: React.ReactNode;
} }
const NoteInfo: React.FC<NoteInfoProps> = (props) => { const NoteInfo: React.FC<NoteInfoProps> = (props) => {
const { required, description, label } = props || {}; const { required, description, label, labelExtra } = props || {};
if (!label) return null; if (!label) return null;
return ( return (
@@ -43,6 +44,7 @@ const NoteInfo: React.FC<NoteInfoProps> = (props) => {
</span> </span>
</> </>
)} )}
{labelExtra}
</span> </span>
); );
}; };
@@ -17,6 +17,7 @@ interface WrapperProps {
variant?: string; variant?: string;
hasPrefix?: boolean; hasPrefix?: boolean;
classList?: string; classList?: string;
labelExtra?: React.ReactNode;
onClick?: () => void; onClick?: () => void;
} }
@@ -35,6 +36,7 @@ const Wrapper: React.FC<WrapperProps> = ({
hasPrefix, hasPrefix,
noWrapperStyle, noWrapperStyle,
classList, classList,
labelExtra,
onClick onClick
}) => { }) => {
return ( return (
@@ -74,6 +76,7 @@ const Wrapper: React.FC<WrapperProps> = ({
label={label} label={label}
required={required} required={required}
description={description} description={description}
labelExtra={labelExtra}
></LabelInfo> ></LabelInfo>
</label> </label>
{extra && <div className={wrapperStyle.extra}>{extra}</div>} {extra && <div className={wrapperStyle.extra}>{extra}</div>}
+2
View File
@@ -20,6 +20,7 @@ const SealInput: React.FC<InputProps & SealFormItemProps> = (props) => {
checkStatus, checkStatus,
trim = true, trim = true,
loading, loading,
labelExtra,
...rest ...rest
} = props; } = props;
const [isFocus, setIsFocus] = useState(false); const [isFocus, setIsFocus] = useState(false);
@@ -74,6 +75,7 @@ const SealInput: React.FC<InputProps & SealFormItemProps> = (props) => {
<Wrapper <Wrapper
status={checkStatus || status} status={checkStatus || status}
label={label} label={label}
labelExtra={labelExtra}
isFocus={isFocus} isFocus={isFocus}
required={required} required={required}
description={description} description={description}
+1
View File
@@ -9,6 +9,7 @@ export interface SealFormItemProps {
addAfter?: React.ReactNode; addAfter?: React.ReactNode;
allowNull?: boolean; allowNull?: boolean;
loading?: React.ReactNode; loading?: React.ReactNode;
labelExtra?: React.ReactNode;
trim?: boolean; trim?: boolean;
checkStatus?: 'success' | 'error' | 'warning' | ''; checkStatus?: 'success' | 'error' | 'warning' | '';
} }
@@ -53,12 +53,12 @@ const TableRow: React.FC<
childrenDataRef.current = childrenData; childrenDataRef.current = childrenData;
const axiosToken = useRef<any>(null); const axiosToken = useRef<any>(null);
const [updateChild, setUpdateChild] = useState(true); const [updateChild, setUpdateChild] = useState(true);
const [currentExpand, setCurrentExpand] = useState(false);
const { updateChunkedList, cacheDataListRef } = useUpdateChunkedList({ const { updateChunkedList, cacheDataListRef } = useUpdateChunkedList({
dataList: childrenData, dataList: childrenData,
limit: 100, limit: 100,
setDataList: setChildrenData setDataList: setChildrenData
// callback: (list) => renderChildren?.(list)
}); });
useEffect(() => { useEffect(() => {
@@ -92,7 +92,10 @@ const TableRow: React.FC<
></Empty> ></Empty>
); );
} }
return renderChildren?.(childrenData, record); return renderChildren?.(childrenData, {
parent: record,
currentExpanded: currentExpand
});
}; };
const handlePolling = async () => { const handlePolling = async () => {
@@ -163,6 +166,7 @@ const TableRow: React.FC<
const handleRowExpand = async () => { const handleRowExpand = async () => {
onExpand?.(!expanded, record, record[rowKey]); onExpand?.(!expanded, record, record[rowKey]);
setCurrentExpand(!expanded);
if (pollTimer.current) { if (pollTimer.current) {
clearInterval(pollTimer.current); clearInterval(pollTimer.current);
@@ -2,8 +2,7 @@
position: relative; position: relative;
display: flex; display: flex;
align-items: center; align-items: center;
min-height: 54px; height: 54px;
padding-block: 5px;
border-radius: var(--border-radius-mdium); border-radius: var(--border-radius-mdium);
transition: all 0.2s ease; transition: all 0.2s ease;
+4 -1
View File
@@ -59,7 +59,10 @@ export interface SealTableProps {
onSort?: (dataIndex: string, order: 'ascend' | 'descend') => void; onSort?: (dataIndex: string, order: 'ascend' | 'descend') => void;
onExpand?: (expanded: boolean, record: any, rowKey: any) => void; onExpand?: (expanded: boolean, record: any, rowKey: any) => void;
onExpandAll?: (expanded: boolean) => void; onExpandAll?: (expanded: boolean) => void;
renderChildren?: (data: any, parent?: any) => React.ReactNode; renderChildren?: (
data: any,
options: { parent?: any; [key: string]: any }
) => React.ReactNode;
loadChildren?: (record: any, options?: any) => Promise<any[]>; loadChildren?: (record: any, options?: any) => Promise<any[]>;
loadChildrenAPI?: (record: any) => string; loadChildrenAPI?: (record: any) => string;
contentRendered?: () => void; contentRendered?: () => void;
+1 -1
View File
@@ -14,7 +14,7 @@ const TableHeader = ({ columns }: TableHeaderProps) => {
{columns.map((column: any, index: number) => { {columns.map((column: any, index: number) => {
return ( return (
<th key={index}> <th key={index}>
<span className="cell-span"> <span className="cell-span cell-header">
{column.locale {column.locale
? intl.formatMessage({ id: column.title }) ? intl.formatMessage({ id: column.title })
: column.title} : column.title}
+9 -1
View File
@@ -16,7 +16,7 @@
} }
tr:last-child td { tr:last-child td {
border-bottom: none; // border-bottom: none;
} }
} }
@@ -25,9 +25,17 @@
font-weight: 600; font-weight: 600;
} }
td {
color: var(--color-white-quaternary);
}
.cell-span { .cell-span {
display: flex; display: flex;
padding: 4px 6px; padding: 4px 6px;
min-height: 32px; min-height: 32px;
} }
.cell-header {
font-weight: var(--font-weight-bold);
}
} }
+24 -2
View File
@@ -6,11 +6,31 @@ import TableHeader from './header';
import './index.less'; import './index.less';
import TableRow from './row'; import TableRow from './row';
interface ColumnProps { export interface ColumnProps {
title: string; title: string;
key: string; key: string;
render?: (data: { dataIndex: string; row: any }) => any; render?: (data: {
dataIndex: string;
dataList?: any[];
row: any;
rowIndex?: number;
colIndex?: number;
}) => any;
locale?: boolean; locale?: boolean;
colSpan?: (params: {
row: any;
rowIndex: number;
colIndex: number;
dataIndex: string;
dataList: any[];
}) => number;
rowSpan?: (params: {
row: any;
rowIndex: number;
colIndex: number;
dataIndex: string;
dataList: any[];
}) => number;
} }
interface SimpleTableProps { interface SimpleTableProps {
@@ -42,7 +62,9 @@ const SimpleTabel: React.FC<SimpleTableProps> = (props) => {
return ( return (
<TableRow <TableRow
row={item} row={item}
rowIndex={index}
columns={columns} columns={columns}
dataList={dataSource}
key={rowKey ? item[rowKey] : index} key={rowKey ? item[rowKey] : index}
></TableRow> ></TableRow>
); );
+73 -10
View File
@@ -1,25 +1,88 @@
import React from 'react'; import React, { useMemo } from 'react';
interface TableRowProps { interface TableRowProps {
row: any; row: any;
columns: any; columns: any;
rowIndex: number;
dataList: any[];
} }
const TableRow = ({ row, columns }: TableRowProps) => {
interface TableCellProps {
row: any;
column: any;
rowIndex: number;
colIndex: number;
dataList: any[];
}
const TableCell: React.FC<TableCellProps> = (props: TableCellProps) => {
const { row, column, rowIndex, colIndex, dataList } = props;
const renderContent = useMemo(() => {
return column.render
? column.render({
dataIndex: column.key,
dataList: dataList,
row: row,
rowIndex,
colIndex: colIndex
})
: row[column.key];
}, [column, row, rowIndex, colIndex, dataList]);
if (renderContent === null && (column.colSpan || column.rowSpan)) {
return null;
}
return (
<td
key={colIndex}
rowSpan={column.rowSpan?.({
row,
rowIndex,
colIndex: colIndex,
dataIndex: column.key,
dataList: dataList
})}
colSpan={column.colSpan?.({
row,
rowIndex,
colIndex: colIndex,
dataIndex: column.key,
dataList: dataList
})}
>
<span className="cell-span">
{column.render
? column.render({
dataIndex: column.key,
dataList: dataList,
row: row,
rowIndex,
colIndex: colIndex
})
: row[column.key]}
</span>
</td>
);
};
const TableRow = ({ row, columns, rowIndex, dataList }: TableRowProps) => {
return ( return (
<tr> <tr>
{columns.map((column: any, index: number) => { {columns.map((column: any, index: number) => {
return ( return (
<td key={index}> <TableCell
<span className="cell-span"> key={index}
{column.render rowIndex={rowIndex}
? column.render({ dataIndex: column.key, row: row }) colIndex={index}
: row[column.key]} dataList={dataList}
</span> row={row}
</td> column={column}
></TableCell>
); );
})} })}
</tr> </tr>
); );
}; };
export default React.memo(TableRow); export default TableRow;
+7
View File
@@ -3,13 +3,16 @@ import { useCallback, useState } from 'react';
export default function useExpandedRowKeys(defaultKeys: React.Key[] = []) { export default function useExpandedRowKeys(defaultKeys: React.Key[] = []) {
const [expandedRowKeys, setExpandedRowKeys] = const [expandedRowKeys, setExpandedRowKeys] =
useState<React.Key[]>(defaultKeys); useState<React.Key[]>(defaultKeys);
const [currentExpand, setCurrentExpand] = useState<React.Key | null>(null);
const handleExpandChange = useCallback( const handleExpandChange = useCallback(
(expanded: boolean, record: any, rowKey: any) => { (expanded: boolean, record: any, rowKey: any) => {
if (expanded) { if (expanded) {
setExpandedRowKeys((keys) => [...keys, rowKey]); setExpandedRowKeys((keys) => [...keys, rowKey]);
setCurrentExpand(rowKey);
} else { } else {
setExpandedRowKeys((keys) => keys.filter((key) => key !== rowKey)); setExpandedRowKeys((keys) => keys.filter((key) => key !== rowKey));
setCurrentExpand(null);
} }
}, },
[] []
@@ -19,6 +22,7 @@ export default function useExpandedRowKeys(defaultKeys: React.Key[] = []) {
(expanded: boolean, keys: React.Key[] = []) => { (expanded: boolean, keys: React.Key[] = []) => {
if (!expanded) { if (!expanded) {
setExpandedRowKeys([]); setExpandedRowKeys([]);
setCurrentExpand(null);
} }
if (expanded) { if (expanded) {
setExpandedRowKeys((prevKeys) => [...new Set([...prevKeys, ...keys])]); setExpandedRowKeys((prevKeys) => [...new Set([...prevKeys, ...keys])]);
@@ -41,10 +45,13 @@ export default function useExpandedRowKeys(defaultKeys: React.Key[] = []) {
const clearExpandedRowKeys = () => { const clearExpandedRowKeys = () => {
setExpandedRowKeys([]); setExpandedRowKeys([]);
setCurrentExpand(null);
}; };
return { return {
expandedRowKeys, expandedRowKeys,
currentExpand,
setCurrentExpand,
clearExpandedRowKeys, clearExpandedRowKeys,
updateExpandedRowKeys, updateExpandedRowKeys,
removeExpandedRowKey, removeExpandedRowKey,
+3 -1
View File
@@ -108,5 +108,7 @@ export default {
'models.table.list.getStart': 'models.table.list.getStart':
'<span style="margin-right: 5px;font-size: 13px;">Get started with</span> <span style="font-size: 14px;font-weight: 700">DeepSeek-R1-Distill-Qwen-1.5B</span>', '<span style="margin-right: 5px;font-size: 13px;">Get started with</span> <span style="font-size: 14px;font-weight: 700">DeepSeek-R1-Distill-Qwen-1.5B</span>',
'models.table.llamaAcrossworker': 'Llama-box Across Workers', 'models.table.llamaAcrossworker': 'Llama-box Across Workers',
'models.table.vllmAcrossworker': 'vLLM Across Workers' 'models.table.vllmAcrossworker': 'vLLM Across Workers',
'models.form.releases': 'Releases',
'models.form.moreparameters': 'More Parameters'
}; };
+4 -1
View File
@@ -109,5 +109,8 @@ export default {
'models.table.llamaAcrossworker': 'models.table.llamaAcrossworker':
'TODO: Translate key "models.table.llamaAcrossworker"', 'TODO: Translate key "models.table.llamaAcrossworker"',
'models.table.vllmAcrossworker': 'models.table.vllmAcrossworker':
'TODO: Translate key "models.table.vllmAcrossworker"' 'TODO: Translate key "models.table.vllmAcrossworker"',
'models.form.releases': 'TODO: Translate key "models.form.releases"',
'models.form.moreparameters':
'TODO: Translate key "models.form.moreparameters"'
}; };
+3 -1
View File
@@ -104,5 +104,7 @@ export default {
'models.table.list.getStart': 'models.table.list.getStart':
'<span style="margin-right: 5px;font-size: 13px;">一键部署</span><span style="font-size: 14px;font-weight: 700">DeepSeek-R1-Distill-Qwen-1.5B</span><span style="margin-left: 5px;font-size: 13px;">立即使用!</span>', '<span style="margin-right: 5px;font-size: 13px;">一键部署</span><span style="font-size: 14px;font-weight: 700">DeepSeek-R1-Distill-Qwen-1.5B</span><span style="margin-left: 5px;font-size: 13px;">立即使用!</span>',
'models.table.llamaAcrossworker': 'Llama-box 跨节点', 'models.table.llamaAcrossworker': 'Llama-box 跨节点',
'models.table.vllmAcrossworker': 'vLLM 跨节点' 'models.table.vllmAcrossworker': 'vLLM 跨节点',
'models.form.releases': '版本',
'models.form.moreparameters': '更多参数'
}; };
@@ -101,12 +101,14 @@ const AdvanceConfig: React.FC<AdvanceConfigProps> = (props) => {
if (backend === backendOptionsMap.llamaBox) { if (backend === backendOptionsMap.llamaBox) {
return { return {
backend: 'llama-box', backend: 'llama-box',
releases: 'https://github.com/gpustack/llama-box/releases',
link: 'https://github.com/gpustack/llama-box?tab=readme-ov-file#usage' link: 'https://github.com/gpustack/llama-box?tab=readme-ov-file#usage'
}; };
} }
if (backend === backendOptionsMap.vllm) { if (backend === backendOptionsMap.vllm) {
return { return {
backend: 'vLLM', backend: 'vLLM',
releases: 'https://github.com/vllm-project/vllm/releases',
link: 'https://docs.vllm.ai/en/stable/serving/openai_compatible_server.html#command-line-arguments-for-the-server' link: 'https://docs.vllm.ai/en/stable/serving/openai_compatible_server.html#command-line-arguments-for-the-server'
}; };
} }
@@ -327,6 +329,25 @@ const AdvanceConfig: React.FC<AdvanceConfigProps> = (props) => {
description={intl.formatMessage({ description={intl.formatMessage({
id: 'models.form.backendVersion.tips' id: 'models.form.backendVersion.tips'
})} })}
labelExtra={
backendParamsTips?.releases && (
<span
style={{
marginLeft: 5
}}
>
(
<Typography.Link
style={{ lineHeight: 1 }}
href={backendParamsTips?.releases}
target="_blank"
>
{intl.formatMessage({ id: 'models.form.releases' })}
</Typography.Link>
)
</span>
)
}
></SealInput.Input> ></SealInput.Input>
</Form.Item> </Form.Item>
@@ -346,19 +367,20 @@ const AdvanceConfig: React.FC<AdvanceConfigProps> = (props) => {
dataList={form.getFieldValue('backend_parameters') || []} dataList={form.getFieldValue('backend_parameters') || []}
onChange={handleBackendParametersChange} onChange={handleBackendParametersChange}
options={paramsConfig} options={paramsConfig}
description={ labelExtra={
backendParamsTips && ( backendParamsTips?.link && (
<span> <span style={{ marginLeft: 2 }}>
{intl.formatMessage( (
{ id: 'models.form.backend_parameters.vllm.tips' },
{ backend: backendParamsTips.backend || '' }
)}{' '}
<Typography.Link <Typography.Link
href={backendParamsTips.link} style={{ lineHeight: 1 }}
href={backendParamsTips?.link}
target="_blank" target="_blank"
> >
{intl.formatMessage({ id: 'common.text.here' })} {intl.formatMessage({
id: 'models.form.moreparameters'
})}
</Typography.Link> </Typography.Link>
)
</span> </span>
) )
} }
+99 -69
View File
@@ -2,7 +2,7 @@ import AutoTooltip from '@/components/auto-tooltip';
import DropdownButtons from '@/components/drop-down-buttons'; import DropdownButtons from '@/components/drop-down-buttons';
import IconFont from '@/components/icon-font'; import IconFont from '@/components/icon-font';
import RowChildren from '@/components/seal-table/components/row-children'; import RowChildren from '@/components/seal-table/components/row-children';
import SimpleTabel from '@/components/simple-table'; import SimpleTabel, { ColumnProps } from '@/components/simple-table';
import StatusTag from '@/components/status-tag'; import StatusTag from '@/components/status-tag';
import { HandlerOptions } from '@/hooks/use-chunk-fetch'; import { HandlerOptions } from '@/hooks/use-chunk-fetch';
import useDownloadStream from '@/hooks/use-download-stream'; import useDownloadStream from '@/hooks/use-download-stream';
@@ -12,7 +12,8 @@ import {
DeleteOutlined, DeleteOutlined,
DownloadOutlined, DownloadOutlined,
HddFilled, HddFilled,
InfoCircleOutlined InfoCircleOutlined,
ThunderboltFilled
} from '@ant-design/icons'; } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { import {
@@ -27,17 +28,48 @@ import {
} from 'antd'; } from 'antd';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import _ from 'lodash'; import _ from 'lodash';
import React, { useCallback, useMemo } from 'react'; import React, { useCallback, useEffect, useMemo } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { MODEL_INSTANCE_API } from '../apis'; import { MODEL_INSTANCE_API } from '../apis';
import { InstanceStatusMap, InstanceStatusMapValue, status } from '../config'; import { InstanceStatusMap, InstanceStatusMapValue, status } from '../config';
import { ModelInstanceListItem } from '../config/types'; import { ModelInstanceListItem } from '../config/types';
import '../style/instance-item.less'; import '../style/instance-item.less';
const WorkerInfo = (props: {
title: React.ReactNode;
defaultOpen: boolean;
}) => {
const [open, setOpen] = React.useState(props.defaultOpen);
useEffect(() => {
if (props.defaultOpen) {
setTimeout(() => {
setOpen(false);
}, 1000);
}
}, [props.defaultOpen]);
return (
<span className="server-info-wrapper">
<Tooltip
open={open}
onOpenChange={setOpen}
title={props.title}
overlayInnerStyle={{
width: 'max-content',
maxWidth: '400px'
}}
>
<span className="server-info">
<InfoCircleOutlined />
</span>
</Tooltip>
</span>
);
};
interface InstanceItemProps { interface InstanceItemProps {
instanceData: ModelInstanceListItem; instanceData: ModelInstanceListItem;
workerList: WorkerListItem[]; workerList: WorkerListItem[];
modelData?: any; modelData?: any;
defaultOpenId: string;
handleChildSelect: (val: string, item: ModelInstanceListItem) => void; handleChildSelect: (val: string, item: ModelInstanceListItem) => void;
} }
@@ -76,7 +108,7 @@ const childActionList = [
} }
]; ];
const distributeCols = [ const distributeCols: ColumnProps[] = [
{ {
title: 'Worker', title: 'Worker',
key: 'worker_name' key: 'worker_name'
@@ -84,7 +116,7 @@ const distributeCols = [
{ {
title: 'IP', title: 'IP',
key: 'worker_ip', key: 'worker_ip',
render: ({ row }: { row: ModelInstanceListItem }) => { render: ({ row }) => {
return row.port ? `${row.worker_ip}:${row.port}` : row.worker_ip; return row.port ? `${row.worker_ip}:${row.port}` : row.worker_ip;
} }
}, },
@@ -94,9 +126,21 @@ const distributeCols = [
key: 'gpu_index' key: 'gpu_index'
}, },
{ {
title: 'resources.table.memory', title: 'resources.table.vram',
locale: true, locale: true,
key: 'ram' key: 'vram',
rowSpan: ({ row, rowIndex, colIndex, dataIndex, dataList }) => {
return rowIndex === 0 ? dataList.length : 0;
},
render: ({ rowIndex, dataList }) => {
if (rowIndex === 0) {
return convertFileSize(
_.sumBy(dataList, (item: any) => item.vram),
2
);
}
return null;
}
} }
]; ];
@@ -153,6 +197,7 @@ const InstanceItem: React.FC<InstanceItemProps> = ({
instanceData, instanceData,
workerList, workerList,
modelData, modelData,
defaultOpenId,
handleChildSelect handleChildSelect
}) => { }) => {
const [api, contextHolder] = notification.useNotification({ const [api, contextHolder] = notification.useNotification({
@@ -224,55 +269,32 @@ const InstanceItem: React.FC<InstanceItemProps> = ({
? `${instanceData.worker_ip}:${instanceData.port}` ? `${instanceData.worker_ip}:${instanceData.port}`
: instanceData.worker_ip; : instanceData.worker_ip;
} }
let backend = modelData?.backend || '';
if (modelData.backend_version) {
backend += ` (${modelData.backend_version})`;
}
const vrams = instanceData.computed_resource_claim?.vram || {};
return ( return (
<div> <div>
<div style={{ marginBottom: 5 }}> <div>{instanceData.worker_name}</div>
<div className="flex-center">
<HddFilled className="m-r-5" /> <HddFilled className="m-r-5" />
{instanceData.worker_name} {workerIp}
</div> </div>
<div className="flex m-b-6 gap-6"> <div className="flex-center">
<InfoItem label="IP" value={workerIp} width={180}></InfoItem> <IconFont type="icon-filled-gpu" className="m-r-5" />
<InfoItem {intl.formatMessage({ id: 'models.table.gpuindex' })}: [
width={90} {_.join(instanceData.gpu_indexes?.sort?.(), ',')}]
label={intl.formatMessage({ id: 'models.form.backend' })}
value={backend}
></InfoItem>
</div> </div>
<div className="flex gap-6"> <div className="flex-center">
<InfoItem <ThunderboltFilled className="m-r-5" />
width={180} {intl.formatMessage({ id: 'models.form.backend' })}:{' '}
label={intl.formatMessage({ id: 'models.table.gpuindex' })} {modelData?.backend || ''}
value={Object.keys(vrams) {modelData.backend_version ? `(${modelData.backend_version})` : ''}
?.sort?.()
.map((index) => {
return (
<span className="flex-1" key={index}>
<span className="index">
[{index}] {''}
</span>
{convertFileSize(vrams?.[index], 0)}
</span>
);
})}
></InfoItem>
<InfoItem
width={90}
label={intl.formatMessage({ id: 'resources.table.memory' })}
value={convertFileSize(
instanceData.computed_resource_claim?.ram,
0
)}
></InfoItem>
</div> </div>
</div> </div>
); );
}, [modelData, instanceData, intl]); }, [modelData, instanceData, intl]);
const calcTotalVram = (vram: Record<string, number>) => {
return _.sum(_.values(vram));
};
const renderDistributedServer = useCallback( const renderDistributedServer = useCallback(
(severList: any[]) => { (severList: any[]) => {
const list = _.map(severList, (item: any) => { const list = _.map(severList, (item: any) => {
@@ -281,20 +303,35 @@ const InstanceItem: React.FC<InstanceItemProps> = ({
worker_name: data?.name, worker_name: data?.name,
worker_ip: data?.ip, worker_ip: data?.ip,
port: '', port: '',
ram: convertFileSize(item.computed_resource_claim?.ram, 0), vram: calcTotalVram(item.computed_resource_claim?.vram || {}),
gpu_index: displayGPUs(item.computed_resource_claim?.vram || {}) gpu_index: _.keys(item.computed_resource_claim?.vram).join(',')
}; };
}); });
// const list = [
// {
// worker_name: 'worker1',
// worker_ip: '192.168.50.23',
// port: '',
// vram: 21555525632,
// gpu_index: '0,1'
// },
// {
// worker_name: 'worker2',
// worker_ip: '192.168.50.25',
// port: '',
// vram: 21555525632,
// gpu_index: '2,3'
// }
// ];
const mainWorker = [ const mainWorker = [
{ {
worker_name: `${instanceData.worker_name} (main)`, worker_name: `${instanceData.worker_name}`,
worker_ip: `${instanceData.worker_ip}`, worker_ip: `${instanceData.worker_ip}`,
port: '', port: '',
ram: convertFileSize(instanceData.computed_resource_claim?.ram, 0), vram: calcTotalVram(instanceData.computed_resource_claim?.vram || {}),
gpu_index: displayGPUs( gpu_index: `${instanceData.gpu_indexes?.join?.(',')}(main)`
instanceData.computed_resource_claim?.vram || {}
)
} }
]; ];
@@ -443,18 +480,12 @@ const InstanceItem: React.FC<InstanceItemProps> = ({
<AutoTooltip title={instanceData.name} ghost> <AutoTooltip title={instanceData.name} ghost>
<span className="m-r-5">{instanceData.name}</span> <span className="m-r-5">{instanceData.name}</span>
</AutoTooltip> </AutoTooltip>
<Tooltip {!!instanceData.worker_id && (
title={renderWorkerInfo} <WorkerInfo
overlayInnerStyle={{ title={renderWorkerInfo}
width: 'max-content', defaultOpen={defaultOpenId === instanceData.name}
maxWidth: '400px' ></WorkerInfo>
}} )}
>
<span className="server-info">
<InfoCircleOutlined className="m-r-2" />{' '}
{intl.formatMessage({ id: 'common.button.moreInfo' })}
</span>
</Tooltip>
</span> </span>
</Col> </Col>
<Col span={6}> <Col span={6}>
@@ -468,10 +499,9 @@ const InstanceItem: React.FC<InstanceItemProps> = ({
> >
{renderOffloadInfo} {renderOffloadInfo}
{renderDistributionInfo( {renderDistributionInfo(
instanceData.distributed_servers?.rpc_servers || [] instanceData.distributed_servers?.rpc_servers ||
)} instanceData.distributed_servers?.ray_actors ||
{renderDistributionInfo( []
instanceData.distributed_servers?.ray_actors || []
)} )}
</span> </span>
</Col> </Col>
+21 -1
View File
@@ -1,7 +1,7 @@
import { ListItem as WorkerListItem } from '@/pages/resources/config/types'; import { ListItem as WorkerListItem } from '@/pages/resources/config/types';
import { Space } from 'antd'; import { Space } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
import React from 'react'; import React, { useEffect, useMemo } from 'react';
import { ModelInstanceListItem } from '../config/types'; import { ModelInstanceListItem } from '../config/types';
import '../style/instance-item.less'; import '../style/instance-item.less';
import InstanceItem from './instance-item'; import InstanceItem from './instance-item';
@@ -10,6 +10,7 @@ interface InstanceItemProps {
list: ModelInstanceListItem[]; list: ModelInstanceListItem[];
workerList: WorkerListItem[]; workerList: WorkerListItem[];
modelData?: any; modelData?: any;
currentExpanded?: string;
handleChildSelect: (val: string, item: ModelInstanceListItem) => void; handleChildSelect: (val: string, item: ModelInstanceListItem) => void;
} }
@@ -17,8 +18,26 @@ const Instances: React.FC<InstanceItemProps> = ({
list, list,
workerList, workerList,
modelData, modelData,
currentExpanded,
handleChildSelect handleChildSelect
}) => { }) => {
const [firstLoad, setFirstLoad] = React.useState(true);
const defaultOpenId = useMemo(() => {
if (!currentExpanded) {
return '';
}
const current = _.find(
list,
(item: ModelInstanceListItem) => item.worker_id
);
return current ? current.name : '';
}, [currentExpanded, list]);
useEffect(() => {
setFirstLoad(false);
}, []);
return ( return (
<Space size={16} direction="vertical" style={{ width: '100%' }}> <Space size={16} direction="vertical" style={{ width: '100%' }}>
{_.map(list, (item: ModelInstanceListItem, index: number) => { {_.map(list, (item: ModelInstanceListItem, index: number) => {
@@ -28,6 +47,7 @@ const Instances: React.FC<InstanceItemProps> = ({
modelData={modelData} modelData={modelData}
workerList={workerList} workerList={workerList}
instanceData={item} instanceData={item}
defaultOpenId={firstLoad ? defaultOpenId : ''}
handleChildSelect={handleChildSelect} handleChildSelect={handleChildSelect}
></InstanceItem> ></InstanceItem>
); );
+58 -19
View File
@@ -32,7 +32,7 @@ import {
SyncOutlined SyncOutlined
} from '@ant-design/icons'; } from '@ant-design/icons';
import { PageContainer } from '@ant-design/pro-components'; import { PageContainer } from '@ant-design/pro-components';
import { Access, useAccess, useIntl, useNavigate } from '@umijs/max'; import { useAccess, useIntl, useNavigate } from '@umijs/max';
import { import {
Button, Button,
Dropdown, Dropdown,
@@ -68,7 +68,6 @@ import {
} from '../config'; } from '../config';
import { FormData, ListItem, ModelInstanceListItem } from '../config/types'; import { FormData, ListItem, ModelInstanceListItem } from '../config/types';
import { useGenerateFormEditInitialValues } from '../hooks'; import { useGenerateFormEditInitialValues } from '../hooks';
import DeployDropdown from './deploy-dropdown';
import DeployModal from './deploy-modal'; import DeployModal from './deploy-modal';
import Instances from './instances'; import Instances from './instances';
import ModelTag from './model-tag'; import ModelTag from './model-tag';
@@ -120,7 +119,7 @@ const ActionList = [
{ {
label: 'common.button.start', label: 'common.button.start',
key: 'start', key: 'start',
icon: <IconFont type="icon-playcircle"></IconFont> icon: <IconFont type="icon-outline-play"></IconFont>
}, },
{ {
label: 'common.button.delete', label: 'common.button.delete',
@@ -132,6 +131,27 @@ const ActionList = [
} }
]; ];
const ButtonList = [
{
label: 'common.button.start',
key: 'start',
icon: <IconFont type="icon-outline-play"></IconFont>
},
{
label: 'common.button.stop',
key: 'stop',
icon: <IconFont type="icon-stop1"></IconFont>
},
{
label: 'common.button.delete',
key: 'delete',
icon: <DeleteOutlined />,
props: {
danger: true
}
}
];
const generateSource = (record: ListItem) => { const generateSource = (record: ListItem) => {
if (record.source === modelSourceMap.modelscope_value) { if (record.source === modelSourceMap.modelscope_value) {
return `${modelSourceMap.modelScope}/${record.model_scope_model_id}`; return `${modelSourceMap.modelScope}/${record.model_scope_model_id}`;
@@ -327,6 +347,12 @@ const Models: React.FC<ModelsProps> = ({
}, []); }, []);
const sourceOptions = [ const sourceOptions = [
{
label: intl.formatMessage({ id: 'menu.models.modelCatalog' }),
value: 'catalog',
key: 'catalog',
icon: <IconFont type="icon-catalog"></IconFont>
},
{ {
label: 'Hugging Face', label: 'Hugging Face',
value: modelSourceMap.huggingface_value, value: modelSourceMap.huggingface_value,
@@ -345,12 +371,6 @@ const Models: React.FC<ModelsProps> = ({
key: 'modelscope', key: 'modelscope',
icon: <IconFont type="icon-tu2"></IconFont> icon: <IconFont type="icon-tu2"></IconFont>
}, },
{
label: intl.formatMessage({ id: 'menu.models.modelCatalog' }),
value: 'catalog',
key: 'catalog',
icon: <IconFont type="icon-catalog"></IconFont>
},
{ {
label: intl.formatMessage({ id: 'models.form.localPath' }), label: intl.formatMessage({ id: 'models.form.localPath' }),
value: modelSourceMap.local_path_value, value: modelSourceMap.local_path_value,
@@ -659,11 +679,12 @@ const Models: React.FC<ModelsProps> = ({
); );
const renderChildren = useCallback( const renderChildren = useCallback(
(list: any, parent?: any) => { (list: any, options: { parent?: any; [key: string]: any }) => {
return ( return (
<Instances <Instances
list={list} list={list}
modelData={parent} currentExpanded={options.currentExpanded}
modelData={options.parent}
workerList={workerList} workerList={workerList}
handleChildSelect={handleChildSelect} handleChildSelect={handleChildSelect}
></Instances> ></Instances>
@@ -739,6 +760,18 @@ const Models: React.FC<ModelsProps> = ({
}); });
}; };
const handleActionSelect = (val: any) => {
if (val === 'delete') {
handleDeleteBatch();
}
if (val === 'start') {
handleStartBatch();
}
if (val === 'stop') {
handleStopBatch();
}
};
const columns: SealColumnProps[] = useMemo(() => { const columns: SealColumnProps[] = useMemo(() => {
return [ return [
{ {
@@ -939,12 +972,6 @@ const Models: React.FC<ModelsProps> = ({
onClick: handleClickDropdown onClick: handleClickDropdown
}} }}
trigger={['hover']} trigger={['hover']}
dropdownRender={() => (
<DeployDropdown
items={sourceOptions}
onSelect={handleClickDropdown}
></DeployDropdown>
)}
placement="bottomRight" placement="bottomRight"
> >
<Button <Button
@@ -955,7 +982,19 @@ const Models: React.FC<ModelsProps> = ({
{intl?.formatMessage?.({ id: 'models.button.deploy' })} {intl?.formatMessage?.({ id: 'models.button.deploy' })}
</Button> </Button>
</Dropdown> </Dropdown>
<Button <DropdownButtons
items={ButtonList}
extra={
rowSelection.selectedRowKeys.length > 0 && (
<span>({rowSelection.selectedRowKeys.length})</span>
)
}
size="large"
showText={true}
disabled={!rowSelection.selectedRowKeys.length}
onSelect={handleActionSelect}
/>
{/* <Button
icon={<IconFont type="icon-outline-play"></IconFont>} icon={<IconFont type="icon-outline-play"></IconFont>}
onClick={handleStartBatch} onClick={handleStartBatch}
disabled={!rowSelection.selectedRows.length} disabled={!rowSelection.selectedRows.length}
@@ -993,7 +1032,7 @@ const Models: React.FC<ModelsProps> = ({
)} )}
</span> </span>
</Button> </Button>
</Access> </Access> */}
</Space> </Space>
} }
></PageTools> ></PageTools>
+32 -13
View File
@@ -1,6 +1,7 @@
import AutoTooltip from '@/components/auto-tooltip'; import AutoTooltip from '@/components/auto-tooltip';
import PageTools from '@/components/page-tools'; import PageTools from '@/components/page-tools';
import ProgressBar from '@/components/progress-bar'; import ProgressBar from '@/components/progress-bar';
import InfoColumn from '@/components/simple-table/info-column';
import useTableSort from '@/hooks/use-table-sort'; import useTableSort from '@/hooks/use-table-sort';
import { convertFileSize } from '@/utils'; import { convertFileSize } from '@/utils';
import { SyncOutlined } from '@ant-design/icons'; import { SyncOutlined } from '@ant-design/icons';
@@ -12,6 +13,33 @@ import { queryGpuDevicesList } from '../apis';
import { GPUDeviceItem } from '../config/types'; import { GPUDeviceItem } from '../config/types';
const { Column } = Table; const { Column } = Table;
const fieldList = [
{
label: 'resources.table.total',
key: 'total',
locale: true,
render: (val: any) => {
return convertFileSize(val, 0);
}
},
{
label: 'resources.table.used',
key: 'used',
locale: true,
render: (val: any) => {
return convertFileSize(val, 0);
}
},
{
label: 'resources.table.allocated',
key: 'allocated',
locale: true,
render: (val: any) => {
return convertFileSize(val, 0);
}
}
];
const GPUList: React.FC = () => { const GPUList: React.FC = () => {
console.log('GPUList======'); console.log('GPUList======');
const intl = useIntl(); const intl = useIntl();
@@ -228,19 +256,10 @@ const GPUList: React.FC = () => {
) * 100 ) * 100
} }
label={ label={
<span className="flex-column"> <InfoColumn
<span> fieldList={fieldList}
{intl.formatMessage({ id: 'resources.table.total' })}:{' '} data={record.memory}
{convertFileSize(record.memory?.total, 0)} ></InfoColumn>
</span>
<span>
{intl.formatMessage({ id: 'resources.table.used' })}:{' '}
{convertFileSize(
record.memory?.used || record.memory?.allocated,
0
)}
</span>
</span>
} }
></ProgressBar> ></ProgressBar>
); );