From 33fe1809970693b77f29e458c143a1af3587918a Mon Sep 17 00:00:00 2001 From: jialin Date: Thu, 19 Mar 2026 20:05:18 +0800 Subject: [PATCH] refactor: model instance item --- src/config/global.d.ts | 4 +- src/hooks/use-actions.ts | 10 + .../{ => components}/download/index.tsx | 16 +- .../{ => components}/download/target-form.tsx | 4 +- .../instance-cells/actions-cell.tsx | 154 ++++ .../instance-cells/cpu-offloading-cell.tsx | 80 ++ .../instance-cells/distribute-info-cell.tsx | 184 ++++ .../downloading-status-cell.tsx | 179 ++++ .../instance-cells/instance-status-cell.tsx | 61 ++ .../components/instance-cells/name-cell.tsx | 133 +++ .../components/instance/instance-item.tsx | 824 ++---------------- .../instance-view/cells/actions-cell.tsx | 0 src/pages/llmodels/instance-view/index.tsx | 108 ++- .../instance-view/use-instance-columns.tsx | 63 ++ .../resources/components/model-files.tsx | 2 +- 15 files changed, 1057 insertions(+), 765 deletions(-) create mode 100644 src/hooks/use-actions.ts rename src/pages/llmodels/{ => components}/download/index.tsx (93%) rename src/pages/llmodels/{ => components}/download/target-form.tsx (98%) create mode 100644 src/pages/llmodels/components/instance-cells/actions-cell.tsx create mode 100644 src/pages/llmodels/components/instance-cells/cpu-offloading-cell.tsx create mode 100644 src/pages/llmodels/components/instance-cells/distribute-info-cell.tsx create mode 100644 src/pages/llmodels/components/instance-cells/downloading-status-cell.tsx create mode 100644 src/pages/llmodels/components/instance-cells/instance-status-cell.tsx create mode 100644 src/pages/llmodels/components/instance-cells/name-cell.tsx create mode 100644 src/pages/llmodels/instance-view/cells/actions-cell.tsx create mode 100644 src/pages/llmodels/instance-view/use-instance-columns.tsx diff --git a/src/config/global.d.ts b/src/config/global.d.ts index 80d88c96..b1d17d96 100644 --- a/src/config/global.d.ts +++ b/src/config/global.d.ts @@ -69,7 +69,7 @@ declare namespace Global { onCancel: () => void; } - interface ActionItem { + interface ActionItem { label: string; key: string; icon: React.ReactNode; @@ -77,6 +77,8 @@ declare namespace Global { props?: { danger?: boolean; }; + visible?: (record: T) => boolean; + disabled?: (record: T) => boolean; } } diff --git a/src/hooks/use-actions.ts b/src/hooks/use-actions.ts new file mode 100644 index 00000000..7ea80d5b --- /dev/null +++ b/src/hooks/use-actions.ts @@ -0,0 +1,10 @@ +export default function useActions(actions: Global.ActionItem[], ctx: T) { + return actions + .filter((action) => { + return action.visible ? action.visible(ctx) : true; + }) + .map((action) => ({ + ...action, + disabled: action.disabled?.(ctx) + })); +} diff --git a/src/pages/llmodels/download/index.tsx b/src/pages/llmodels/components/download/index.tsx similarity index 93% rename from src/pages/llmodels/download/index.tsx rename to src/pages/llmodels/components/download/index.tsx index 14329de9..308a5499 100644 --- a/src/pages/llmodels/download/index.tsx +++ b/src/pages/llmodels/components/download/index.tsx @@ -5,14 +5,14 @@ import { useIntl } from '@umijs/max'; import { debounce } from 'lodash'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import styled from 'styled-components'; -import ColumnWrapper from '../../_components/column-wrapper'; -import HFModelFile from '../components/model-source/hf-model-file'; -import ModelCard from '../components/model-source/model-card'; -import SearchModel from '../components/model-source/search-model'; -import Separator from '../components/separator'; -import TitleWrapper from '../components/title-wrapper'; -import { modelSourceMap } from '../config'; -import { FormData } from '../config/types'; +import ColumnWrapper from '../../../_components/column-wrapper'; +import { modelSourceMap } from '../../config'; +import { FormData } from '../../config/types'; +import HFModelFile from '../model-source/hf-model-file'; +import ModelCard from '../model-source/model-card'; +import SearchModel from '../model-source/search-model'; +import Separator from '../separator'; +import TitleWrapper from '../title-wrapper'; import TargetForm from './target-form'; type AddModalProps = { diff --git a/src/pages/llmodels/download/target-form.tsx b/src/pages/llmodels/components/download/target-form.tsx similarity index 98% rename from src/pages/llmodels/download/target-form.tsx rename to src/pages/llmodels/components/download/target-form.tsx index b2256d43..3c7cd3f8 100644 --- a/src/pages/llmodels/download/target-form.tsx +++ b/src/pages/llmodels/components/download/target-form.tsx @@ -14,8 +14,8 @@ import React, { useImperativeHandle, useMemo } from 'react'; -import { localPathTipsList, modelSourceMap, sourceOptions } from '../config'; -import { useGenerateWorkersModelFileOptions } from '../hooks'; +import { localPathTipsList, modelSourceMap, sourceOptions } from '../../config'; +import { useGenerateWorkersModelFileOptions } from '../../hooks'; type EmptyObject = Record; diff --git a/src/pages/llmodels/components/instance-cells/actions-cell.tsx b/src/pages/llmodels/components/instance-cells/actions-cell.tsx new file mode 100644 index 00000000..db644f93 --- /dev/null +++ b/src/pages/llmodels/components/instance-cells/actions-cell.tsx @@ -0,0 +1,154 @@ +import DropdownButtons from '@/components/drop-down-buttons'; +import IconFont from '@/components/icon-font'; +import { HandlerOptions } from '@/hooks/use-chunk-fetch'; +import useDownloadStream from '@/hooks/use-download-stream'; +import { useBenchmarkTargetInstance } from '@/pages/llmodels/hooks/use-run-benchmark'; +import { DeleteOutlined, DownloadOutlined } from '@ant-design/icons'; +import { useIntl } from '@umijs/max'; +import { Progress, notification } from 'antd'; +import dayjs from 'dayjs'; +import { MODEL_INSTANCE_API } from '../../apis'; +import { InstanceStatusMap, modelCategoriesMap } from '../../config'; +import { ListItem, ModelInstanceListItem } from '../../config/types'; + +const childActionList = [ + { + label: 'common.button.viewlog', + key: 'viewlog', + status: [ + InstanceStatusMap.Initializing, + InstanceStatusMap.Running, + InstanceStatusMap.Error, + InstanceStatusMap.Starting, + InstanceStatusMap.Downloading + ], + icon: + }, + { + label: 'common.button.downloadLog', + key: 'download', + status: [ + InstanceStatusMap.Initializing, + InstanceStatusMap.Running, + InstanceStatusMap.Error, + InstanceStatusMap.Starting, + InstanceStatusMap.Downloading + ], + icon: + }, + { + label: 'models.table.instance.benchmark', + key: 'benchmark', + status: [InstanceStatusMap.Running], + icon: + }, + { + label: 'common.button.delrecreate', + key: 'delete', + props: { + danger: true + }, + icon: + } +]; + +interface ActionsCellProps { + record: ModelInstanceListItem; + modelData: ListItem; + onSelect: (val: string, record: ModelInstanceListItem) => void; +} + +const ActionsCell: React.FC = ({ + record, + modelData, + onSelect +}) => { + const { runBenchmarkOnInstance } = useBenchmarkTargetInstance(); + const [api, contextHolder] = notification.useNotification({ + stack: { threshold: 1 } + }); + const { downloadStream } = useDownloadStream(); + const intl = useIntl(); + + const createFileName = (name: string) => { + const timestamp = dayjs().format('YYYY-MM-DD_HH-mm-ss'); + const fileName = `${name}_${timestamp}.txt`; + return fileName; + }; + + const renderMessage = (title: string) => { + return ( +
+ {title} +
+ ); + }; + + const downloadNotification = ( + data: HandlerOptions & { + filename: string; + duration?: number; + chunkRequestRef: any; + } + ) => { + api.open({ + duration: data.duration, + message: renderMessage(data.filename), + key: data.filename, + closeIcon: ( + {intl.formatMessage({ id: 'common.button.cancel' })} + ), + description: , + onClose() { + data.chunkRequestRef?.current?.abort(); + notification.destroy?.(data.filename); + } + }); + }; + + const handleOnSelect = (val: string) => { + if (val === 'benchmark') { + runBenchmarkOnInstance(record); + } else if (val === 'download') { + downloadStream({ + url: `${MODEL_INSTANCE_API}/${record.id}/logs`, + filename: createFileName(record.name), + downloadNotification + }); + } else { + onSelect(val, record); + } + }; + + const actionItems = childActionList.filter((action: any) => { + if (action.key === 'benchmark') { + return ( + action.status.includes(record.state) && + modelData?.categories?.includes(modelCategoriesMap.llm) + ); + } + if (action.status && action.status.length > 0) { + return action.status.includes(record.state); + } + return true; + }); + + return ( + <> + {contextHolder} + + + ); +}; + +export default ActionsCell; diff --git a/src/pages/llmodels/components/instance-cells/cpu-offloading-cell.tsx b/src/pages/llmodels/components/instance-cells/cpu-offloading-cell.tsx new file mode 100644 index 00000000..e81514c0 --- /dev/null +++ b/src/pages/llmodels/components/instance-cells/cpu-offloading-cell.tsx @@ -0,0 +1,80 @@ +import InfoColumn from '@/components/simple-table/info-column'; +import ThemeTag from '@/components/tags-wrapper/theme-tag'; +import { InfoCircleOutlined } from '@ant-design/icons'; +import { useIntl } from '@umijs/max'; +import { Tooltip } from 'antd'; +import _ from 'lodash'; +import React from 'react'; +import { ModelInstanceListItem } from '../../config/types'; + +const fieldList = [ + { + label: 'CPU', + key: 'cpuoffload', + locale: false + }, + { + label: 'GPU', + key: 'gpuoffload', + locale: false + } +]; + +interface CPUOffloadingCellProps { + record: ModelInstanceListItem; +} +const CPUOffloadingCell: React.FC = ({ record }) => { + const intl = useIntl(); + const { total_layers, offload_layers } = + record?.computed_resource_claim || {}; + + if (total_layers === offload_layers || !total_layers) { + return null; + } + + const offloadData = { + cpuoffload: `${ + _.subtract(total_layers, offload_layers) || 0 + } ${intl.formatMessage({ + id: 'models.table.layers' + })}`, + gpuoffload: `${offload_layers} ${intl.formatMessage({ + id: 'models.table.layers' + })}` + }; + + return ( + } + > + + + + {intl.formatMessage({ + id: 'models.table.cpuoffload' + })} + + + + ); +}; + +export default CPUOffloadingCell; diff --git a/src/pages/llmodels/components/instance-cells/distribute-info-cell.tsx b/src/pages/llmodels/components/instance-cells/distribute-info-cell.tsx new file mode 100644 index 00000000..8360e491 --- /dev/null +++ b/src/pages/llmodels/components/instance-cells/distribute-info-cell.tsx @@ -0,0 +1,184 @@ +import { TooltipOverlayScroller } from '@/components/overlay-scroller'; +import SimpleTabel, { ColumnProps } from '@/components/simple-table'; +import ThemeTag from '@/components/tags-wrapper/theme-tag'; +import { ListItem as WorkerListItem } from '@/pages/resources/config/types'; +import { convertFileSize } from '@/utils'; +import { InfoCircleOutlined } from '@ant-design/icons'; +import { useIntl } from '@umijs/max'; +import _ from 'lodash'; +import React from 'react'; +import styled from 'styled-components'; +import { + DistributedServerItem, + DistributedServers, + ModelInstanceListItem +} from '../../config/types'; + +const GPUIndexWrapper = styled.span` + display: flex; + flex-direction: column; + gap: 2px; +`; + +interface DistributeInfoCellProps { + record: ModelInstanceListItem; + workerList: WorkerListItem[]; +} + +const renderGpuIndexs = (gpuIndexes: number[]) => { + return ( + + {_.chunk(gpuIndexes, 8).map((item: number[], index: number) => { + return {item.join(',')}; + })} + + ); +}; + +const distributeCols: ColumnProps[] = [ + { + title: 'Worker', + key: 'worker_name', + style: { + wordBreak: 'break-word' + } + }, + { + title: 'IP', + key: 'worker_ip', + render: ({ row }) => { + return row.port ? `${row.worker_ip}:${row.port}` : row.worker_ip; + } + }, + { + title: 'models.table.gpuindex', + locale: true, + key: 'gpu_index', + render: ({ row }) => { + const list = row.gpu_index?.sort((a: number, b: number) => a - b) || []; + return row.is_main ? ( + <> + {renderGpuIndexs(list)} + (main) + + ) : ( + renderGpuIndexs(list) + ); + } + }, + { + title: 'models.table.vram.allocated', + locale: true, + key: 'vram', + render: ({ rowIndex, row, dataList }) => { + return convertFileSize(row.vram, 1); + } + } +]; + +const calcTotalVram = (vram: Record) => { + return _.sum(_.values(vram)); +}; + +const DistributedServerList: React.FC = ({ + record, + workerList +}) => { + const severList: DistributedServerItem[] = + record?.distributed_servers?.subordinate_workers || []; + + const list = _.map(severList, (item: any) => { + const data = _.find(workerList, { id: item.worker_id }); + return { + worker_name: data?.name, + worker_ip: data?.ip, + port: '', + is_main: false, + vram: calcTotalVram(item.computed_resource_claim?.vram || {}), + gpu_index: _.keys(item.computed_resource_claim?.vram) + .map((i: string) => Number(i)) + .sort((a: number, b: number) => a - b) + }; + }); + + const mainWorker = [ + { + worker_name: `${record.worker_name}`, + worker_ip: `${record.worker_ip}`, + port: '', + vram: calcTotalVram(record.computed_resource_claim?.vram || {}), + is_main: true, + gpu_index: record.gpu_indexes?.sort((a: number, b: number) => a - b) + } + ]; + + return ( +
+ +
+ ); +}; + +const DistributeInfoCell: React.FC<{ + record: ModelInstanceListItem; + workerList: WorkerListItem[]; +}> = ({ record, workerList }) => { + const intl = useIntl(); + const distributed_servers: DistributedServers | undefined = + record?.distributed_servers; + + const severList: DistributedServerItem[] = + distributed_servers?.subordinate_workers || []; + + if (!severList.length) { + return null; + } + return ( + + } + > + + + + {intl.formatMessage({ + id: 'models.table.acrossworker' + })} + + + + ); +}; + +export default DistributeInfoCell; diff --git a/src/pages/llmodels/components/instance-cells/downloading-status-cell.tsx b/src/pages/llmodels/components/instance-cells/downloading-status-cell.tsx new file mode 100644 index 00000000..92223f71 --- /dev/null +++ b/src/pages/llmodels/components/instance-cells/downloading-status-cell.tsx @@ -0,0 +1,179 @@ +import SimpleTabel, { ColumnProps } from '@/components/simple-table'; +import StatusTag from '@/components/status-tag'; +import { ListItem as WorkerListItem } from '@/pages/resources/config/types'; +import { Progress, Tooltip } from 'antd'; +import _ from 'lodash'; +import { InstanceStatusMap, status } from '../../config'; +import { generateSource } from '../../config/button-actions'; +import { + DistributedServerItem, + DistributedServers, + ModelInstanceListItem +} from '../../config/types'; +import { backendOptionsMap } from '../../constants/backend-parameters'; + +interface DownloadingStatusProps { + distributed_servers?: DistributedServers; + workerList: WorkerListItem[]; + record: ModelInstanceListItem; + backend?: string; +} + +const statusColumn: ColumnProps[] = [ + { + title: 'models.table.download.progress', + locale: true, + key: 'download_progress', + render: ({ row }) => { + return ( + + ); + } + } +]; +const downloadList: ColumnProps[] = [ + { + title: 'resources.worker', + locale: true, + key: 'worker_name', + width: 280 + }, + ...statusColumn +]; + +const draftModelDownloadList: ColumnProps[] = [ + { + title: 'models.form.draftModel', + locale: true, + key: 'draft_model', + style: { + wordBreak: 'break-word' + }, + width: 280 + }, + ...statusColumn +]; + +const DownloadingTips = (props: { + severList: any[]; + record: ModelInstanceListItem; + workerList: WorkerListItem[]; +}) => { + const { severList, record, workerList } = props; + if (!severList.length && !record.draft_model_download_progress) { + return null; + } + const list = _.map(severList, (item: any) => { + const data = _.find(workerList, { id: item.worker_id }); + return { + worker_name: data?.name, + worker_ip: data?.ip, + download_progress: _.round(item.download_progress, 2) + }; + }); + + const mainWorker = [ + { + worker_name: `${record.worker_name}`, + worker_ip: `${record.worker_ip}`, + download_progress: _.round(record.download_progress, 2) + } + ]; + + const draftModelList = []; + if (record.draft_model_download_progress > 0) { + draftModelList.push({ + draft_model: generateSource(record.draft_model_source), + download_progress: _.round(record.draft_model_download_progress, 2) + }); + } + + return ( +
+ {severList.length > 0 && ( + + )} + + {draftModelList.length > 0 && ( + + )} +
+ ); +}; + +const DownloadingStatus: React.FC = (props) => { + const { distributed_servers, workerList, record, backend } = props; + + const severList: DistributedServerItem[] = + distributed_servers?.subordinate_workers || []; + + const isWorkerNotDownloading = + record.state !== InstanceStatusMap.Downloading || + !severList.length || + backend === backendOptionsMap.llamaBox; + + const isDraftModeNotDownloading = + !record.draft_model_download_progress || + record.draft_model_download_progress >= 100; + + if (isWorkerNotDownloading && isDraftModeNotDownloading) { + return null; + } + return ( + + } + > + item.download_progress < 100) + ?.download_progress || + record.draft_model_download_progress || + 0 + } + /> + + ); +}; + +export default DownloadingStatus; diff --git a/src/pages/llmodels/components/instance-cells/instance-status-cell.tsx b/src/pages/llmodels/components/instance-cells/instance-status-cell.tsx new file mode 100644 index 00000000..e7cd653c --- /dev/null +++ b/src/pages/llmodels/components/instance-cells/instance-status-cell.tsx @@ -0,0 +1,61 @@ +import StatusTag from '@/components/status-tag'; +import { useIntl } from '@umijs/max'; +import { Button } from 'antd'; +import React from 'react'; +import { + InstanceStatusMap, + InstanceStatusMapValue, + status +} from '../../config'; +import { ModelInstanceListItem } from '../../config/types'; + +interface InstanceStatusProps { + record: ModelInstanceListItem; + onSelect: (val: string, record: ModelInstanceListItem) => void; +} + +const InstanceStatusTag: React.FC = ({ + record, + onSelect +}) => { + const intl = useIntl(); + if (!record.state) { + return null; + } + return ( + onSelect('viewlog', record)} + > + {intl.formatMessage({ id: 'models.list.more.logs' })} + + ) : null + } + statusValue={{ + status: + record.state === InstanceStatusMap.Downloading && + record.download_progress === 100 + ? status[InstanceStatusMap.Running] + : status[record.state], + text: InstanceStatusMapValue[record.state], + message: + record.state === InstanceStatusMap.Downloading && + record.download_progress === 100 + ? '' + : record.state_message + }} + /> + ); +}; + +export default InstanceStatusTag; diff --git a/src/pages/llmodels/components/instance-cells/name-cell.tsx b/src/pages/llmodels/components/instance-cells/name-cell.tsx new file mode 100644 index 00000000..9a5523b0 --- /dev/null +++ b/src/pages/llmodels/components/instance-cells/name-cell.tsx @@ -0,0 +1,133 @@ +import AutoTooltip from '@/components/auto-tooltip'; +import IconFont from '@/components/icon-font'; +import { convertFileSize } from '@/utils'; +import { + HddFilled, + InfoCircleOutlined, + PieChartFilled, + ThunderboltFilled +} from '@ant-design/icons'; +import { useIntl } from '@umijs/max'; +import { Tooltip } from 'antd'; +import _ from 'lodash'; +import React, { useEffect } from 'react'; +import { ModelInstanceListItem } from '../../config/types'; +import '../../style/instance-item.less'; + +interface NameCellProps { + record: ModelInstanceListItem; + modelData: any; + defaultOpenId?: string; +} + +const calcTotalVram = (vram: Record) => { + return _.sum(_.values(vram)); +}; + +const WorkerInfoContent: React.FC = ({ record, modelData }) => { + const intl = useIntl(); + let workerIp = '-'; + if (record.worker_ip) { + workerIp = record.port + ? `${record.worker_ip}:${record.port}` + : record.worker_ip; + } + return ( +
+
{record.worker_name}
+
+ + {workerIp} +
+
+ + {intl.formatMessage({ id: 'models.table.gpuindex' })}: [ + {_.join( + record.gpu_indexes?.sort?.((a, b) => a - b), + ',' + )} + ] +
+
+ + {intl.formatMessage({ id: 'models.form.backend' })}:{' '} + {record?.backend || modelData?.backend || ''} + {record.backend_version || modelData?.backend_version + ? `(${record.backend_version || modelData?.backend_version})` + : ''} +
+
+ + {intl.formatMessage({ id: 'models.table.vram.allocated' })}:{' '} + {convertFileSize( + record.computed_resource_claim?.vram + ? calcTotalVram(record.computed_resource_claim?.vram) + : 0, + 1 + )} +
+
+ ); +}; + +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 ( + + + + + + + + ); +}; + +const NameCell: React.FC = ({ + record, + modelData, + defaultOpenId +}) => { + return ( + + + {record.name} + + {!!record.worker_id && ( + + } + defaultOpen={defaultOpenId === record.name} + > + )} + + ); +}; + +export default NameCell; diff --git a/src/pages/llmodels/components/instance/instance-item.tsx b/src/pages/llmodels/components/instance/instance-item.tsx index b8bf2e58..b7fd5848 100644 --- a/src/pages/llmodels/components/instance/instance-item.tsx +++ b/src/pages/llmodels/components/instance/instance-item.tsx @@ -1,48 +1,17 @@ -import { systemConfigAtom } from '@/atoms/system'; import AutoTooltip from '@/components/auto-tooltip'; -import DropdownButtons from '@/components/drop-down-buttons'; -import IconFont from '@/components/icon-font'; -import { TooltipOverlayScroller } from '@/components/overlay-scroller'; import RowChildren from '@/components/seal-table/components/row-children'; -import SimpleTabel, { ColumnProps } from '@/components/simple-table'; -import InfoColumn from '@/components/simple-table/info-column'; -import StatusTag from '@/components/status-tag'; -import ThemeTag from '@/components/tags-wrapper/theme-tag'; -import { HandlerOptions } from '@/hooks/use-chunk-fetch'; -import useDownloadStream from '@/hooks/use-download-stream'; -import { useBenchmarkTargetInstance } from '@/pages/llmodels/hooks/use-run-benchmark'; import { ListItem as WorkerListItem } from '@/pages/resources/config/types'; -import { convertFileSize } from '@/utils'; -import { - DeleteOutlined, - DownloadOutlined, - HddFilled, - InfoCircleOutlined, - PieChartFilled, - ThunderboltFilled -} from '@ant-design/icons'; -import { useIntl } from '@umijs/max'; -import { Button, Col, Progress, Row, Tooltip, notification } from 'antd'; +import { Col, Row } from 'antd'; import dayjs from 'dayjs'; -import { useAtomValue } from 'jotai'; -import _ from 'lodash'; -import React, { useCallback, useEffect, useMemo } from 'react'; -import styled from 'styled-components'; -import { MODEL_INSTANCE_API } from '../../apis'; -import { - InstanceStatusMap, - InstanceStatusMapValue, - modelCategoriesMap, - status -} from '../../config'; -import { generateSource } from '../../config/button-actions'; -import { - DistributedServerItem, - DistributedServers, - ModelInstanceListItem -} from '../../config/types'; -import { backendOptionsMap } from '../../constants/backend-parameters'; +import React from 'react'; +import { ModelInstanceListItem } from '../../config/types'; import '../../style/instance-item.less'; +import ActionsCell from '../instance-cells/actions-cell'; +import CPUOffloadingCell from '../instance-cells/cpu-offloading-cell'; +import DistributeInfoCell from '../instance-cells/distribute-info-cell'; +import DownloadingStatusCell from '../instance-cells/downloading-status-cell'; +import InstanceStatusCell from '../instance-cells/instance-status-cell'; +import NameCell from '../instance-cells/name-cell'; interface InstanceItemProps { instanceData: ModelInstanceListItem; @@ -52,377 +21,6 @@ interface InstanceItemProps { handleChildSelect: (val: string, item: ModelInstanceListItem) => void; } -const fieldList = [ - { - label: 'CPU', - key: 'cpuoffload', - locale: false - }, - { - label: 'GPU', - key: 'gpuoffload', - locale: false - } -]; - -const statusColumn: ColumnProps[] = [ - { - title: 'models.table.download.progress', - locale: true, - key: 'download_progress', - render: ({ row }) => { - return ( - - ); - } - } -]; -const downloadList: ColumnProps[] = [ - { - title: 'resources.worker', - locale: true, - key: 'worker_name', - width: 280 - }, - ...statusColumn -]; - -const draftModelDownloadList: ColumnProps[] = [ - { - title: 'models.form.draftModel', - locale: true, - key: 'draft_model', - style: { - wordBreak: 'break-word' - }, - width: 280 - }, - ...statusColumn -]; - -const calcTotalVram = (vram: Record) => { - return _.sum(_.values(vram)); -}; - -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 ( - - - - - - - - ); -}; - -const GPUIndexWrapper = styled.span` - display: flex; - flex-direction: column; - gap: 2px; -`; -const RenderRayactorDownloading = (props: { - severList: any[]; - instanceData: any; - workerList: WorkerListItem[]; -}) => { - const { severList, instanceData, workerList } = props; - if (!severList.length && !instanceData.draft_model_download_progress) { - return null; - } - const list = _.map(severList, (item: any) => { - const data = _.find(workerList, { id: item.worker_id }); - return { - worker_name: data?.name, - worker_ip: data?.ip, - download_progress: _.round(item.download_progress, 2) - }; - }); - - const mainWorker = [ - { - worker_name: `${instanceData.worker_name}`, - worker_ip: `${instanceData.worker_ip}`, - download_progress: _.round(instanceData.download_progress, 2) - } - ]; - - const draftModelList = []; - if (instanceData.draft_model_download_progress > 0) { - draftModelList.push({ - draft_model: generateSource(instanceData.draft_model_source), - download_progress: _.round(instanceData.draft_model_download_progress, 2) - }); - } - - return ( -
- {severList.length > 0 && ( - - )} - - {draftModelList.length > 0 && ( - - )} -
- ); -}; - -const RenderWorkerDownloading = (props: { - distributed_servers?: DistributedServers; - workerList: WorkerListItem[]; - instanceData: ModelInstanceListItem; - backend?: string; -}) => { - const { distributed_servers, workerList, instanceData, backend } = props; - - const severList: DistributedServerItem[] = - distributed_servers?.subordinate_workers || []; - - const isWorkerNotDownloading = - instanceData.state !== InstanceStatusMap.Downloading || - !severList.length || - backend === backendOptionsMap.llamaBox; - - const isDraftModeNotDownloading = - !instanceData.draft_model_download_progress || - instanceData.draft_model_download_progress >= 100; - - if (isWorkerNotDownloading && isDraftModeNotDownloading) { - return null; - } - return ( - - } - > - item.download_progress < 100) - ?.download_progress || - instanceData.draft_model_download_progress || - 0 - } - /> - - ); -}; - -const InstanceStatusTag = ( - props: Pick -) => { - const intl = useIntl(); - const { instanceData, handleChildSelect } = props; - if (!instanceData.state) { - return null; - } - return ( - <> - handleChildSelect('viewlog', instanceData)} - > - {intl.formatMessage({ id: 'models.list.more.logs' })} - - ) : null - } - statusValue={{ - status: - instanceData.state === InstanceStatusMap.Downloading && - instanceData.download_progress === 100 - ? status[InstanceStatusMap.Running] - : status[instanceData.state], - text: InstanceStatusMapValue[instanceData.state], - message: - instanceData.state === InstanceStatusMap.Downloading && - instanceData.download_progress === 100 - ? '' - : instanceData.state_message - }} - /> - - ); -}; - -const childActionList = [ - { - label: 'common.button.viewlog', - key: 'viewlog', - status: [ - InstanceStatusMap.Initializing, - InstanceStatusMap.Running, - InstanceStatusMap.Error, - InstanceStatusMap.Starting, - InstanceStatusMap.Downloading - ], - icon: - }, - { - label: 'common.button.downloadLog', - key: 'download', - status: [ - InstanceStatusMap.Initializing, - InstanceStatusMap.Running, - InstanceStatusMap.Error, - InstanceStatusMap.Starting, - InstanceStatusMap.Downloading - ], - icon: - }, - { - label: 'models.table.instance.benchmark', - key: 'benchmark', - status: [InstanceStatusMap.Running], - icon: - }, - { - label: 'common.button.delrecreate', - key: 'delete', - props: { - danger: true - }, - icon: - } -]; - -const renderGpuIndexs = (gpuIndexes: number[]) => { - return ( - - {_.chunk(gpuIndexes, 8).map((item: number[], index: number) => { - return {item.join(',')}; - })} - - ); -}; - -const distributeCols: ColumnProps[] = [ - { - title: 'Worker', - key: 'worker_name', - style: { - wordBreak: 'break-word' - } - }, - { - title: 'IP', - key: 'worker_ip', - render: ({ row }) => { - return row.port ? `${row.worker_ip}:${row.port}` : row.worker_ip; - } - }, - { - title: 'models.table.gpuindex', - locale: true, - key: 'gpu_index', - render: ({ row }) => { - const list = row.gpu_index?.sort((a: number, b: number) => a - b) || []; - return row.is_main ? ( - <> - {renderGpuIndexs(list)} - (main) - - ) : ( - renderGpuIndexs(list) - ); - } - }, - { - title: 'models.table.vram.allocated', - locale: true, - key: 'vram', - render: ({ rowIndex, row, dataList }) => { - return convertFileSize(row.vram, 1); - } - } -]; - -const renderMessage = (title: string) => { - return ( -
- {title} -
- ); -}; - const InstanceItem: React.FC = ({ instanceData, workerList, @@ -430,350 +28,74 @@ const InstanceItem: React.FC = ({ defaultOpenId, handleChildSelect }) => { - const systemConfig = useAtomValue(systemConfigAtom); - const { runBenchmarkOnInstance } = useBenchmarkTargetInstance(); - const [api, contextHolder] = notification.useNotification({ - stack: { threshold: 1 } - }); - const { downloadStream } = useDownloadStream(); - const intl = useIntl(); - const actionItems = useMemo(() => { - return _.filter(childActionList, (action: any) => { - if (action.key === 'benchmark') { - return ( - action.status.includes(instanceData.state) && - modelData?.categories?.includes(modelCategoriesMap.llm) - ); - } - if (action.status && action.status.length > 0) { - return action.status.includes(instanceData.state); - } - return true; - }); - }, [instanceData.state, modelData, systemConfig.showMonitoring]); - - const createFileName = (name: string) => { - const timestamp = dayjs().format('YYYY-MM-DD_HH-mm-ss'); - const fileName = `${name}_${timestamp}.txt`; - return fileName; - }; - - const downloadNotification = useCallback( - ( - data: HandlerOptions & { - filename: string; - duration?: number; - chunkRequestRef: any; - } - ) => { - api.open({ - duration: data.duration, - message: renderMessage(data.filename), - key: data.filename, - closeIcon: ( - {intl.formatMessage({ id: 'common.button.cancel' })} - ), - description: , - onClose() { - data.chunkRequestRef?.current?.abort(); - notification.destroy?.(data.filename); - } - }); - }, - [] - ); - - const renderWorkerInfo = useMemo(() => { - let workerIp = '-'; - if (instanceData.worker_ip) { - workerIp = instanceData.port - ? `${instanceData.worker_ip}:${instanceData.port}` - : instanceData.worker_ip; - } - return ( -
-
{instanceData.worker_name}
-
- - {workerIp} -
-
- - {intl.formatMessage({ id: 'models.table.gpuindex' })}: [ - {_.join( - instanceData.gpu_indexes?.sort?.((a, b) => a - b), - ',' - )} - ] -
-
- - {intl.formatMessage({ id: 'models.form.backend' })}:{' '} - {instanceData?.backend || modelData?.backend || ''} - {instanceData.backend_version || modelData?.backend_version - ? `(${instanceData.backend_version || modelData?.backend_version})` - : ''} -
-
- - {intl.formatMessage({ id: 'models.table.vram.allocated' })}:{' '} - {convertFileSize( - instanceData.computed_resource_claim?.vram - ? calcTotalVram(instanceData.computed_resource_claim?.vram) - : 0, - 1 - )} -
-
- ); - }, [ - instanceData.worker_name, - instanceData.worker_ip, - instanceData.port, - instanceData.gpu_indexes, - instanceData?.backend, - instanceData?.backend_version, - modelData?.backend, - modelData?.backend_version, - intl - ]); - - const renderDistributedServer = (severList: any[]) => { - const list = _.map(severList, (item: any) => { - const data = _.find(workerList, { id: item.worker_id }); - return { - worker_name: data?.name, - worker_ip: data?.ip, - port: '', - is_main: false, - vram: calcTotalVram(item.computed_resource_claim?.vram || {}), - gpu_index: _.keys(item.computed_resource_claim?.vram) - .map((i: string) => Number(i)) - .sort((a: number, b: number) => a - b) - }; - }); - - const mainWorker = [ - { - worker_name: `${instanceData.worker_name}`, - worker_ip: `${instanceData.worker_ip}`, - port: '', - vram: calcTotalVram(instanceData.computed_resource_claim?.vram || {}), - is_main: true, - gpu_index: instanceData.gpu_indexes?.sort( - (a: number, b: number) => a - b - ) - } - ]; - - return ( -
- -
- ); - }; - - const renderDistributionInfo = (distributed_servers: DistributedServers) => { - const severList: DistributedServerItem[] = - distributed_servers?.subordinate_workers || []; - - if (!severList.length) { - return null; - } - return ( - - - - - {intl.formatMessage({ - id: 'models.table.acrossworker' - })} - - - - ); - }; - - const renderOffloadInfo = useMemo(() => { - const total_layers = instanceData.computed_resource_claim?.total_layers; - const offload_layers = instanceData.computed_resource_claim?.offload_layers; - - if (total_layers === offload_layers || !total_layers) { - return null; - } - - const offloadData = { - cpuoffload: `${ - _.subtract( - instanceData.computed_resource_claim?.total_layers, - instanceData.computed_resource_claim?.offload_layers - ) || 0 - } ${intl.formatMessage({ - id: 'models.table.layers' - })}`, - gpuoffload: `${instanceData.computed_resource_claim?.offload_layers} ${intl.formatMessage( - { - id: 'models.table.layers' - } - )}` - }; - return ( - - } - > - - - - {intl.formatMessage({ - id: 'models.table.cpuoffload' - })} - - - - ); - }, [ - instanceData.computed_resource_claim?.total_layers, - instanceData.computed_resource_claim?.offload_layers - ]); - - const handleOnSelect = (val: string) => { - if (val === 'benchmark') { - runBenchmarkOnInstance(instanceData); - } else if (val === 'download') { - downloadStream({ - url: `${MODEL_INSTANCE_API}/${instanceData.id}/logs`, - filename: createFileName(instanceData.name), - downloadNotification - }); - } else { - handleChildSelect(val, instanceData); - } - }; - return ( - <> - {contextHolder} -
- - - + + + + + + + - - - {instanceData.name} - - {!!instanceData.worker_id && ( - - )} - - - - - {renderOffloadInfo} - {renderDistributionInfo( - instanceData.distributed_servers || ({} as DistributedServers) - )} - - - - - - - - - - - - {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/instance-view/cells/actions-cell.tsx b/src/pages/llmodels/instance-view/cells/actions-cell.tsx new file mode 100644 index 00000000..e69de29b diff --git a/src/pages/llmodels/instance-view/index.tsx b/src/pages/llmodels/instance-view/index.tsx index d3f9ebfd..dec5d73e 100644 --- a/src/pages/llmodels/instance-view/index.tsx +++ b/src/pages/llmodels/instance-view/index.tsx @@ -1,7 +1,111 @@ -import React from 'react'; +import DeleteModal from '@/components/delete-modal'; +import IconFont from '@/components/icon-font'; +import { FilterBar } from '@/components/page-tools'; +import { PaginationKey, TABLE_SORT_DIRECTIONS } from '@/config/settings'; +import useTableFetch from '@/hooks/use-table-fetch'; +import NoResult from '@/pages/_components/no-result'; +import PageBox from '@/pages/_components/page-box'; +import { useIntl } from '@umijs/max'; +import { useMemoizedFn } from 'ahooks'; +import { ConfigProvider, Table } from 'antd'; +import _ from 'lodash'; +import { + deleteModelInstance, + MODEL_INSTANCE_API, + queryModelsInstances +} from '../apis'; +import { ModelInstanceListItem as ListItem } from '../config/types'; +import useInstanceColumns from './use-instance-columns'; const InstanceView: React.FC = () => { - return
InstanceView
; + const { + dataSource, + rowSelection, + queryParams, + modalRef, + handleTableChange, + handleDelete, + handleDeleteBatch, + fetchData, + handlePageChange, + handleSearch, + handleNameChange + } = useTableFetch({ + key: PaginationKey.Providers, + fetchAPI: queryModelsInstances, + deleteAPI: deleteModelInstance, + watch: false, + API: MODEL_INSTANCE_API, + contentForDelete: 'menu.models.providers' + }); + const intl = useIntl(); + + const handleSelect = useMemoizedFn((val: any, row: ListItem) => {}); + + const renderEmpty = (type?: string) => { + if (type !== 'Table') return; + return ( + } + filters={_.omit(queryParams, ['sort_by'])} + noFoundText={intl.formatMessage({ + id: 'noresult.providers.nofound' + })} + title={intl.formatMessage({ id: 'noresult.providers.title' })} + subTitle={intl.formatMessage({ + id: 'noresult.providers.subTitle' + })} + buttonText={intl.formatMessage({ id: 'noresult.button.add' })} + > + ); + }; + + const columns = useInstanceColumns(handleSelect); + + return ( + <> + + + +
+
+
+ + + ); }; export default InstanceView; diff --git a/src/pages/llmodels/instance-view/use-instance-columns.tsx b/src/pages/llmodels/instance-view/use-instance-columns.tsx new file mode 100644 index 00000000..8c670345 --- /dev/null +++ b/src/pages/llmodels/instance-view/use-instance-columns.tsx @@ -0,0 +1,63 @@ +// columns.ts +import DropdownButtons from '@/components/drop-down-buttons'; +import { tableSorter } from '@/config/settings'; +import { useIntl } from '@umijs/max'; +import { ColumnsType } from 'antd/lib/table'; +import { useMemo } from 'react'; +import { ModelInstanceListItem as ListItem } from '../config/types'; + +const useProviderColumns = ( + handleSelect: (val: string, record: ListItem) => void, + onCellClick?: (record: ListItem, dataIndex: string) => void +): ColumnsType => { + const intl = useIntl(); + + return useMemo(() => { + return [ + { + title: intl.formatMessage({ id: 'common.table.name' }), + dataIndex: 'name', + sorter: tableSorter(1), + minWidth: 160, + span: 5, + render: () => '--' + }, + { + title: intl.formatMessage({ id: 'providers.table.providerName' }), + dataIndex: ['config', 'type'], + sorter: tableSorter(2), + span: 4, + minWidth: 160, + render: () => '--' + }, + { + title: intl.formatMessage({ id: 'providers.table.models' }), + dataIndex: 'models', + span: 3, + minWidth: 200, + render: () => '--' + }, + { + title: intl.formatMessage({ id: 'common.table.createTime' }), + dataIndex: 'created_at', + sorter: tableSorter(6), + span: 3, + render: () => '--' + }, + { + title: intl.formatMessage({ id: 'common.table.operation' }), + dataIndex: 'operations', + span: 3, + minWidth: 120, + render: (value: string, record: ListItem) => ( + handleSelect(val, record)} + > + ) + } + ]; + }, [handleSelect, onCellClick]); +}; + +export default useProviderColumns; diff --git a/src/pages/resources/components/model-files.tsx b/src/pages/resources/components/model-files.tsx index 868f9e73..5d839ec3 100644 --- a/src/pages/resources/components/model-files.tsx +++ b/src/pages/resources/components/model-files.tsx @@ -11,6 +11,7 @@ import NoResult from '@/pages/_components/no-result'; import PageBox from '@/pages/_components/page-box'; import { createModel } from '@/pages/llmodels/apis'; import DeployModal from '@/pages/llmodels/components/deployment/deploy-modal'; +import DownloadModal from '@/pages/llmodels/components/download'; import { modelSourceMap } from '@/pages/llmodels/config'; import { modalConfig, @@ -18,7 +19,6 @@ import { } from '@/pages/llmodels/config/button-actions'; import { SourceType } from '@/pages/llmodels/config/types'; import { backendOptionsMap } from '@/pages/llmodels/constants/backend-parameters'; -import DownloadModal from '@/pages/llmodels/download'; import useCheckBackend from '@/pages/llmodels/hooks/use-check-backend'; import { useGenerateWorkerOptions } from '@/pages/llmodels/hooks/use-form-initial-values'; import useRecognizeAudio from '@/pages/llmodels/hooks/use-recognize-audio';