// columns.ts import { systemConfigAtom } from '@/atoms/system'; import { OPENAI_COMPATIBLE, tableSorter } from '@/config/settings'; import { TargetStatusValueMap } from '@/pages/model-routes/config'; import { usePluginListColumns } from '@/plugins/list-extra-columns'; import { QuestionCircleOutlined } from '@ant-design/icons'; import { AutoTooltip, DropdownButtons, GrafanaIcon, icons, type TableColumnProps } from '@gpustack/core-ui'; import { useIntl } from '@umijs/max'; import { useMemoizedFn } from 'ahooks'; import { Flex, Tooltip } from 'antd'; import dayjs from 'dayjs'; import { useAtomValue } from 'jotai'; import _ from 'lodash'; import { useMemo } from 'react'; import ModelTag from '../../_components/model-tag'; import { generateSource } from '../config/button-actions'; import { ListItem } from '../config/types'; interface ActionItem { label: string; key: string; icon: React.ReactNode; props?: { danger?: boolean; }; } const Dot = ({ color }: { color: string }) => { return ( ); }; const ActionList: ActionItem[] = [ { label: 'common.button.edit', key: 'edit', icon: icons.EditOutlined }, { label: 'models.openinplayground', key: 'chat', icon: icons.ExperimentOutlined }, { label: 'common.button.start', key: 'start', icon: icons.Play }, { label: 'common.button.stop', key: 'stop', icon: icons.Stop }, { label: 'resources.metrics.details', key: 'metrics', icon: ( ) }, { key: 'copy', label: 'common.button.clone', icon: icons.CopyOutlined }, { label: 'common.button.delete', key: 'delete', props: { danger: true }, icon: icons.DeleteOutlined } ]; interface ModelsColumnsHookProps { handleSelect: (val: string, record: ListItem) => void; sortOrder: string[]; clusterList: Global.BaseOption< number, { provider: string; state: string | number } >[]; } const useModelsColumns = ({ handleSelect, clusterList, sortOrder, targetList }: ModelsColumnsHookProps & { targetList: any[] }): TableColumnProps[] => { const intl = useIntl(); const systemConfig = useAtomValue(systemConfigAtom); const pluginCols = usePluginListColumns('llmodels'); const setModelActionList = useMemoizedFn((record: any) => { return _.filter(ActionList, (action: any) => { if (action.key === 'chat') { return ( record.ready_replicas > 0 && targetList?.find( (target) => target.model_id === record.id && target.state === TargetStatusValueMap.Active ) ); } if (action.key === 'start') { return record.replicas === 0; } if (action.key === 'stop') { return record.replicas > 0; } if (action.key === 'metrics') { return systemConfig?.showMonitoring; } return true; }); }); const getColor = (record: ListItem) => { if (!record.replicas && !record.ready_replicas) { return 'var(--ant-color-fill-secondary)'; } if (record.replicas > 0 && !record.ready_replicas) { return 'var(--ant-color-warning)'; } if (record.ready_replicas > 0 && record.replicas > 0) { return 'var(--ant-color-success)'; } return 'var(--ant-color-warning)'; }; return useMemo(() => { // Two prebuilt span maps for the 24-unit SealTable grid: one for // the default layout, one for when a plugin contributes an extra // column (currently always the 4-span Organization cell). Width // absorbed comes from the widest non-name columns (`source`, // `replicas`, `created_at`). See the matching map in // `use-cluster-columns.tsx` for rationale. const SPANS_DEFAULT = { source: 5, replicas: 4, createTime: 4 }; const SPANS_WITH_PLUGIN = { source: 3, replicas: 3, createTime: 3 }; const spans = pluginCols.length > 0 ? SPANS_WITH_PLUGIN : SPANS_DEFAULT; const pluginRendered = pluginCols.map((c) => ({ title: intl.formatMessage({ id: c.titleId }), dataIndex: c.key, key: c.key, span: c.span ?? 4, render: (_text: any, record: ListItem) => c.render(record) })); return [ { title: intl.formatMessage({ id: 'common.table.name' }), dataIndex: 'name', key: 'name', sorter: tableSorter(1), span: 5, render: (text: string, record: ListItem) => ( {text} } > {text} ) }, ...pluginRendered, { title: intl.formatMessage({ id: 'clusters.title' }), dataIndex: 'cluster_id', key: 'cluster_id', sorter: tableSorter(2), span: 3, render: (text: string, record: ListItem) => ( { clusterList.find((item) => item.value === record.cluster_id) ?.label } ) }, { title: intl.formatMessage({ id: 'models.form.source' }), dataIndex: 'source', key: 'source', sorter: tableSorter(3), span: spans.source, render: (text: string, record: ListItem) => ( {generateSource(record)} ) }, { title: ( {intl.formatMessage({ id: 'models.form.replicas' })} ), dataIndex: 'replicas', key: 'replicas', align: 'left', sorter: tableSorter(4), span: spans.replicas, 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', sorter: tableSorter(5), span: spans.createTime, render: (text: number) => ( {dayjs(text).format('YYYY-MM-DD HH:mm:ss')} ) }, { title: intl.formatMessage({ id: 'common.table.operation' }), key: 'operation', dataIndex: 'operation', span: 3, render: (text: any, record: ListItem) => ( handleSelect(val, record)} /> ) } ]; }, [ sortOrder, clusterList, intl, handleSelect, setModelActionList, pluginCols ]); }; export default useModelsColumns;