refactor: model instance item
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
import ModalFooter from '@/components/modal-footer';
|
||||
import GSDrawer from '@/components/scroller-modal/gs-drawer';
|
||||
import { ProviderValueMap } from '@/pages/cluster-management/config';
|
||||
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 { 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 = {
|
||||
title: string;
|
||||
open: boolean;
|
||||
source: string;
|
||||
width?: string | number;
|
||||
hasLinuxWorker?: boolean;
|
||||
workerOptions: any[];
|
||||
workersList?: any[];
|
||||
onOk: (values: FormData) => void;
|
||||
onCancel: () => void;
|
||||
};
|
||||
|
||||
const ColWrapper = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
max-width: 33.33%;
|
||||
`;
|
||||
|
||||
const FormWrapper = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
maxwidth: 100%;
|
||||
`;
|
||||
|
||||
const DownloadModel: React.FC<AddModalProps> = (props) => {
|
||||
const {
|
||||
title,
|
||||
open,
|
||||
onOk,
|
||||
onCancel,
|
||||
hasLinuxWorker,
|
||||
source,
|
||||
width = 600,
|
||||
workerOptions,
|
||||
workersList
|
||||
} = props || {};
|
||||
const SEARCH_SOURCE = [
|
||||
modelSourceMap.huggingface_value,
|
||||
modelSourceMap.modelscope_value
|
||||
];
|
||||
|
||||
const form = useRef<any>({});
|
||||
const intl = useIntl();
|
||||
const [selectedModel, setSelectedModel] = useState<any>({});
|
||||
const [collapsed, setCollapsed] = useState<boolean>(false);
|
||||
const [isGGUF, setIsGGUF] = useState<boolean>(false);
|
||||
const [fileName, setFileName] = useState<string>('');
|
||||
const modelFileRef = useRef<any>(null);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
|
||||
const generateModelInfo = () => {
|
||||
if (source === modelSourceMap.huggingface_value) {
|
||||
const huggingFaceModel = {
|
||||
huggingface_repo_id: selectedModel.name,
|
||||
huggingface_filename: fileName || null
|
||||
};
|
||||
return huggingFaceModel;
|
||||
}
|
||||
|
||||
if (source === modelSourceMap.modelscope_value) {
|
||||
const modelScopeModel = {
|
||||
model_scope_model_id: selectedModel.name,
|
||||
model_scope_file_path: fileName || null
|
||||
};
|
||||
return modelScopeModel;
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
const handleOnSelectModel = (item: any) => {
|
||||
setSelectedModel(item);
|
||||
setFileName('');
|
||||
};
|
||||
|
||||
const handleOk = async (values: any) => {
|
||||
setLoading(true);
|
||||
await onOk({
|
||||
...values,
|
||||
source: source,
|
||||
...generateModelInfo()
|
||||
});
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handleSumit = () => {
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
form.current?.form?.submit?.();
|
||||
};
|
||||
|
||||
const debounceFetchModelFiles = debounce(() => {
|
||||
modelFileRef.current?.fetchModelFiles?.();
|
||||
}, 300);
|
||||
|
||||
const handleSetIsGGUF = (flag: boolean) => {
|
||||
setIsGGUF(flag);
|
||||
if (flag) {
|
||||
debounceFetchModelFiles();
|
||||
}
|
||||
};
|
||||
const handleSelectModelFile = useCallback((item: any) => {
|
||||
setFileName(item.fakeName);
|
||||
}, []);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
onCancel?.();
|
||||
}, [onCancel]);
|
||||
|
||||
const initDefaultWorker = () => {
|
||||
if (!workerOptions || workerOptions.length === 0) {
|
||||
form.current?.form?.setFieldValue('worker_id', []);
|
||||
return;
|
||||
}
|
||||
const getWorkerId = (worker: any) => [
|
||||
worker?.value ?? '',
|
||||
worker?.children?.[0]?.value ?? ''
|
||||
];
|
||||
|
||||
const customWorker = workerOptions.find(
|
||||
(item) => item.provider === ProviderValueMap.Docker
|
||||
);
|
||||
|
||||
const worker_id = getWorkerId(customWorker || workerOptions[0]);
|
||||
form.current?.form?.setFieldValue('worker_id', worker_id);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setIsGGUF(false);
|
||||
}
|
||||
if (open) {
|
||||
initDefaultWorker();
|
||||
}
|
||||
|
||||
return () => {
|
||||
setSelectedModel({});
|
||||
};
|
||||
}, [open, source]);
|
||||
|
||||
return (
|
||||
<GSDrawer
|
||||
title={title}
|
||||
open={open}
|
||||
onClose={handleCancel}
|
||||
destroyOnHidden={true}
|
||||
closeIcon={false}
|
||||
mask={{
|
||||
closable: false
|
||||
}}
|
||||
keyboard={false}
|
||||
zIndex={2000}
|
||||
styles={{
|
||||
wrapper: { width: width }
|
||||
}}
|
||||
footer={false}
|
||||
>
|
||||
<div style={{ display: 'flex', height: '100%' }}>
|
||||
{SEARCH_SOURCE.includes(props.source) && (
|
||||
<>
|
||||
<ColWrapper>
|
||||
<SearchModel
|
||||
hasLinuxWorker={hasLinuxWorker}
|
||||
modelSource={props.source}
|
||||
onSelectModel={handleOnSelectModel}
|
||||
isDownload={true}
|
||||
></SearchModel>
|
||||
<Separator></Separator>
|
||||
</ColWrapper>
|
||||
<ColWrapper>
|
||||
<ColumnWrapper styles={{ container: { padding: 0 } }}>
|
||||
<ModelCard
|
||||
selectedModel={selectedModel}
|
||||
onCollapse={setCollapsed}
|
||||
collapsed={collapsed}
|
||||
modelSource={props.source}
|
||||
setIsGGUF={handleSetIsGGUF}
|
||||
></ModelCard>
|
||||
{isGGUF && (
|
||||
<HFModelFile
|
||||
ref={modelFileRef}
|
||||
selectedModel={selectedModel}
|
||||
modelSource={props.source}
|
||||
onSelectFile={handleSelectModelFile}
|
||||
collapsed={collapsed}
|
||||
isDownload={true}
|
||||
></HFModelFile>
|
||||
)}
|
||||
</ColumnWrapper>
|
||||
<Separator></Separator>
|
||||
</ColWrapper>
|
||||
</>
|
||||
)}
|
||||
<FormWrapper>
|
||||
<ColumnWrapper
|
||||
styles={{
|
||||
container: { paddingBlock: 0 }
|
||||
}}
|
||||
footer={
|
||||
<>
|
||||
<ModalFooter
|
||||
onCancel={handleCancel}
|
||||
onOk={handleSumit}
|
||||
okBtnProps={{
|
||||
loading: loading
|
||||
}}
|
||||
style={{
|
||||
padding: '16px 24px 8px',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end'
|
||||
}}
|
||||
></ModalFooter>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<>
|
||||
{SEARCH_SOURCE.includes(source) && (
|
||||
<TitleWrapper>
|
||||
{intl.formatMessage({
|
||||
id: 'resources.modelfiles.selecttarget'
|
||||
})}
|
||||
<span style={{ display: 'flex', height: 24 }}></span>
|
||||
</TitleWrapper>
|
||||
)}
|
||||
<TargetForm
|
||||
ref={form}
|
||||
onOk={handleOk}
|
||||
source={source}
|
||||
selectedModel={selectedModel}
|
||||
fileName={fileName}
|
||||
workersList={workersList}
|
||||
workerOptions={workerOptions}
|
||||
></TargetForm>
|
||||
</>
|
||||
</ColumnWrapper>
|
||||
</FormWrapper>
|
||||
</div>
|
||||
</GSDrawer>
|
||||
);
|
||||
};
|
||||
|
||||
export default DownloadModel;
|
||||
@@ -0,0 +1,246 @@
|
||||
import SealCascader from '@/components/seal-form/seal-cascader';
|
||||
import SealInput from '@/components/seal-form/seal-input';
|
||||
import SealSelect from '@/components/seal-form/seal-select';
|
||||
import TooltipList from '@/components/tooltip-list';
|
||||
import useAppUtils from '@/hooks/use-app-utils';
|
||||
import { ModelFileFormData as FormData } from '@/pages/resources/config/types';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Form } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import minimatch from 'minimatch';
|
||||
import React, {
|
||||
forwardRef,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useMemo
|
||||
} from 'react';
|
||||
import { localPathTipsList, modelSourceMap, sourceOptions } from '../../config';
|
||||
import { useGenerateWorkersModelFileOptions } from '../../hooks';
|
||||
|
||||
type EmptyObject = Record<never, never>;
|
||||
|
||||
type CascaderOption<T extends object = EmptyObject> = {
|
||||
label: string;
|
||||
value: string | number;
|
||||
parent?: boolean;
|
||||
disabled?: boolean;
|
||||
index?: number;
|
||||
children?: CascaderOption<T>[];
|
||||
} & Partial<T>;
|
||||
|
||||
interface TargetFormProps {
|
||||
ref?: any;
|
||||
source: string;
|
||||
workerOptions: CascaderOption<{ state: string }>[];
|
||||
workersList?: Global.BaseOption<
|
||||
number,
|
||||
{ state: string; labels: Record<string, string>; cluster_id: number }
|
||||
>[];
|
||||
selectedModel?: Record<string, any>;
|
||||
fileName?: string;
|
||||
onOk: (values: any) => void;
|
||||
}
|
||||
|
||||
const TargetForm: React.FC<TargetFormProps> = forwardRef((props, ref) => {
|
||||
const {
|
||||
modelFileOptions,
|
||||
getModelFileList,
|
||||
generateWorkersModelFileOptions
|
||||
} = useGenerateWorkersModelFileOptions();
|
||||
const { onOk, source, workerOptions, workersList, selectedModel, fileName } =
|
||||
props;
|
||||
const { getRuleMessage } = useAppUtils();
|
||||
const intl = useIntl();
|
||||
const [form] = Form.useForm();
|
||||
const localPath = Form.useWatch('local_path', form);
|
||||
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
try {
|
||||
if (workersList && workersList?.length > 0) {
|
||||
const modelFiles = await getModelFileList();
|
||||
generateWorkersModelFileOptions(modelFiles, workersList || []);
|
||||
}
|
||||
} catch (error) {}
|
||||
};
|
||||
init();
|
||||
}, [workersList]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
form
|
||||
}));
|
||||
|
||||
const handleOk = (values: any) => {
|
||||
const data = _.pickBy(values, (val: string) => val);
|
||||
onOk({
|
||||
...data,
|
||||
worker_id: data.worker_id?.[1]
|
||||
});
|
||||
};
|
||||
|
||||
const handleOnLocalPathBlur = (e: any) => {
|
||||
let { value } = e.target;
|
||||
|
||||
// remove all the backslashes and slashes at the end of the string
|
||||
value = value.replace(/(\\|\/)+$/, '');
|
||||
form.setFieldsValue({
|
||||
local_path: value
|
||||
});
|
||||
};
|
||||
|
||||
const renderOptionNode = (props: { data: any }) => {
|
||||
const { data } = props;
|
||||
const currentWorker = modelFileOptions.find(
|
||||
(item) => item.value === data.value
|
||||
);
|
||||
|
||||
const isExisting = currentWorker?.children?.some((child) => {
|
||||
const isSameFile =
|
||||
child.fileName === (fileName || localPath || '') ||
|
||||
minimatch(child.fileName || '', fileName || '');
|
||||
|
||||
return child.repoId === (selectedModel?.name || '') && isSameFile;
|
||||
});
|
||||
|
||||
const localeId = localPath
|
||||
? 'resources.modelfiles.form.added'
|
||||
: 'resources.modelfiles.form.exsting';
|
||||
|
||||
return (
|
||||
<span>
|
||||
{data.label}
|
||||
{isExisting && (
|
||||
<span className="text-tertiary m-l-4">
|
||||
[{intl.formatMessage({ id: localeId })}]
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const renderLocalPathFields = () => {
|
||||
return (
|
||||
<>
|
||||
<Form.Item<FormData>
|
||||
name="local_path"
|
||||
key="local_path"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: getRuleMessage('input', 'models.form.filePath')
|
||||
}
|
||||
]}
|
||||
>
|
||||
<SealInput.Input
|
||||
required
|
||||
label={intl.formatMessage({ id: 'models.form.filePath' })}
|
||||
onBlur={handleOnLocalPathBlur}
|
||||
description={<TooltipList list={localPathTipsList}></TooltipList>}
|
||||
></SealInput.Input>
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const renderFieldsBySource = useMemo(() => {
|
||||
if (props.source === modelSourceMap.local_path_value) {
|
||||
return renderLocalPathFields();
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [props.source, intl]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={handleOk}
|
||||
preserve={false}
|
||||
clearOnDestroy={true}
|
||||
initialValues={{
|
||||
source: source
|
||||
}}
|
||||
>
|
||||
<Form.Item<FormData>
|
||||
name="source"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: getRuleMessage('select', 'models.form.source')
|
||||
}
|
||||
]}
|
||||
>
|
||||
{
|
||||
<SealSelect
|
||||
disabled
|
||||
label={intl.formatMessage({
|
||||
id: 'models.form.source'
|
||||
})}
|
||||
options={sourceOptions}
|
||||
required
|
||||
></SealSelect>
|
||||
}
|
||||
</Form.Item>
|
||||
{renderFieldsBySource}
|
||||
<Form.Item
|
||||
name="worker_id"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: getRuleMessage('select', 'resources.worker')
|
||||
}
|
||||
]}
|
||||
>
|
||||
<SealCascader
|
||||
required
|
||||
showSearch
|
||||
expandTrigger="hover"
|
||||
multiple={false}
|
||||
classNames={{
|
||||
popup: {
|
||||
root: 'cascader-popup-wrapper gpu-selector'
|
||||
}
|
||||
}}
|
||||
maxTagCount={1}
|
||||
label={intl.formatMessage({ id: 'resources.worker' })}
|
||||
options={workerOptions}
|
||||
showCheckedStrategy="SHOW_CHILD"
|
||||
optionNode={renderOptionNode}
|
||||
getPopupContainer={(triggerNode) => triggerNode.parentNode}
|
||||
></SealCascader>
|
||||
</Form.Item>
|
||||
{source !== modelSourceMap.local_path_value && (
|
||||
<Form.Item<FormData>
|
||||
name="local_dir"
|
||||
rules={[
|
||||
{
|
||||
required: false,
|
||||
message: getRuleMessage(
|
||||
'input',
|
||||
'resources.modelfiles.form.localdir'
|
||||
)
|
||||
}
|
||||
]}
|
||||
>
|
||||
<SealInput.Input
|
||||
description={
|
||||
<span
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: intl.formatMessage({
|
||||
id: 'resources.modelfiles.form.localdir.tips'
|
||||
})
|
||||
}}
|
||||
></span>
|
||||
}
|
||||
label={intl.formatMessage({
|
||||
id: 'resources.modelfiles.form.localdir'
|
||||
})}
|
||||
></SealInput.Input>
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default TargetForm;
|
||||
@@ -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: <IconFont type="icon-logs" />
|
||||
},
|
||||
{
|
||||
label: 'common.button.downloadLog',
|
||||
key: 'download',
|
||||
status: [
|
||||
InstanceStatusMap.Initializing,
|
||||
InstanceStatusMap.Running,
|
||||
InstanceStatusMap.Error,
|
||||
InstanceStatusMap.Starting,
|
||||
InstanceStatusMap.Downloading
|
||||
],
|
||||
icon: <DownloadOutlined />
|
||||
},
|
||||
{
|
||||
label: 'models.table.instance.benchmark',
|
||||
key: 'benchmark',
|
||||
status: [InstanceStatusMap.Running],
|
||||
icon: <IconFont type="icon-speed" />
|
||||
},
|
||||
{
|
||||
label: 'common.button.delrecreate',
|
||||
key: 'delete',
|
||||
props: {
|
||||
danger: true
|
||||
},
|
||||
icon: <DeleteOutlined />
|
||||
}
|
||||
];
|
||||
|
||||
interface ActionsCellProps {
|
||||
record: ModelInstanceListItem;
|
||||
modelData: ListItem;
|
||||
onSelect: (val: string, record: ModelInstanceListItem) => void;
|
||||
}
|
||||
|
||||
const ActionsCell: React.FC<ActionsCellProps> = ({
|
||||
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 (
|
||||
<div
|
||||
style={{
|
||||
width: 280,
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const downloadNotification = (
|
||||
data: HandlerOptions & {
|
||||
filename: string;
|
||||
duration?: number;
|
||||
chunkRequestRef: any;
|
||||
}
|
||||
) => {
|
||||
api.open({
|
||||
duration: data.duration,
|
||||
message: renderMessage(data.filename),
|
||||
key: data.filename,
|
||||
closeIcon: (
|
||||
<span>{intl.formatMessage({ id: 'common.button.cancel' })}</span>
|
||||
),
|
||||
description: <Progress percent={data.percent} size="small"></Progress>,
|
||||
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}
|
||||
<DropdownButtons
|
||||
items={actionItems}
|
||||
onSelect={handleOnSelect}
|
||||
></DropdownButtons>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActionsCell;
|
||||
@@ -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<CPUOffloadingCellProps> = ({ 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 (
|
||||
<Tooltip
|
||||
styles={{
|
||||
container: {
|
||||
paddingInline: 12
|
||||
}
|
||||
}}
|
||||
title={<InfoColumn fieldList={fieldList} data={offloadData}></InfoColumn>}
|
||||
>
|
||||
<span>
|
||||
<ThemeTag
|
||||
opacity={0.75}
|
||||
color="cyan"
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
maxWidth: '100%',
|
||||
minWidth: 50,
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
borderRadius: 12
|
||||
}}
|
||||
>
|
||||
<InfoCircleOutlined className="m-r-5" />
|
||||
{intl.formatMessage({
|
||||
id: 'models.table.cpuoffload'
|
||||
})}
|
||||
</ThemeTag>
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default CPUOffloadingCell;
|
||||
@@ -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 (
|
||||
<GPUIndexWrapper>
|
||||
{_.chunk(gpuIndexes, 8).map((item: number[], index: number) => {
|
||||
return <span key={index}>{item.join(',')}</span>;
|
||||
})}
|
||||
</GPUIndexWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
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)}
|
||||
<span>(main)</span>
|
||||
</>
|
||||
) : (
|
||||
renderGpuIndexs(list)
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'models.table.vram.allocated',
|
||||
locale: true,
|
||||
key: 'vram',
|
||||
render: ({ rowIndex, row, dataList }) => {
|
||||
return convertFileSize(row.vram, 1);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const calcTotalVram = (vram: Record<string, number>) => {
|
||||
return _.sum(_.values(vram));
|
||||
};
|
||||
|
||||
const DistributedServerList: React.FC<DistributeInfoCellProps> = ({
|
||||
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 (
|
||||
<div>
|
||||
<SimpleTabel
|
||||
rowKey="worker_name"
|
||||
columns={distributeCols}
|
||||
dataSource={[...mainWorker, ...list]}
|
||||
></SimpleTabel>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<TooltipOverlayScroller
|
||||
toolTipProps={{
|
||||
styles: {
|
||||
container: {
|
||||
width: 'max-content',
|
||||
maxWidth: '520px',
|
||||
minWidth: '400px'
|
||||
}
|
||||
}
|
||||
}}
|
||||
title={
|
||||
<DistributedServerList
|
||||
record={record}
|
||||
workerList={workerList}
|
||||
></DistributedServerList>
|
||||
}
|
||||
>
|
||||
<span>
|
||||
<ThemeTag
|
||||
opacity={0.75}
|
||||
color="processing"
|
||||
style={{
|
||||
marginRight: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
maxWidth: '100%',
|
||||
minWidth: 50,
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
borderRadius: 12
|
||||
}}
|
||||
>
|
||||
<InfoCircleOutlined className="m-r-5" />
|
||||
{intl.formatMessage({
|
||||
id: 'models.table.acrossworker'
|
||||
})}
|
||||
</ThemeTag>
|
||||
</span>
|
||||
</TooltipOverlayScroller>
|
||||
);
|
||||
};
|
||||
|
||||
export default DistributeInfoCell;
|
||||
@@ -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 (
|
||||
<StatusTag
|
||||
download={{
|
||||
percent: row.download_progress
|
||||
}}
|
||||
statusValue={{
|
||||
status: row.download_progress
|
||||
? status[InstanceStatusMap.Running]
|
||||
: status[InstanceStatusMap.Initializing],
|
||||
text: row.download_progress,
|
||||
message: ''
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
];
|
||||
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 (
|
||||
<div>
|
||||
{severList.length > 0 && (
|
||||
<SimpleTabel
|
||||
columns={downloadList}
|
||||
dataSource={[...mainWorker, ...list]}
|
||||
rowKey="worker_name"
|
||||
theme="light"
|
||||
></SimpleTabel>
|
||||
)}
|
||||
|
||||
{draftModelList.length > 0 && (
|
||||
<SimpleTabel
|
||||
columns={draftModelDownloadList}
|
||||
dataSource={[...draftModelList]}
|
||||
rowKey="worker_name"
|
||||
theme="light"
|
||||
></SimpleTabel>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const DownloadingStatus: React.FC<DownloadingStatusProps> = (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 (
|
||||
<Tooltip
|
||||
arrow={true}
|
||||
styles={{
|
||||
container: {
|
||||
width: 360,
|
||||
backgroundColor: 'var(--color-spotlight-bg)'
|
||||
}
|
||||
}}
|
||||
classNames={{
|
||||
root: 'light-downloading-tooltip'
|
||||
}}
|
||||
title={
|
||||
<DownloadingTips
|
||||
severList={severList}
|
||||
workerList={workerList}
|
||||
record={record}
|
||||
></DownloadingTips>
|
||||
}
|
||||
>
|
||||
<Progress
|
||||
showInfo={false}
|
||||
type="circle"
|
||||
size={16}
|
||||
strokeColor="var(--ant-color-success)"
|
||||
percent={
|
||||
_.find(severList, (item: any) => item.download_progress < 100)
|
||||
?.download_progress ||
|
||||
record.draft_model_download_progress ||
|
||||
0
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default DownloadingStatus;
|
||||
@@ -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<InstanceStatusProps> = ({
|
||||
record,
|
||||
onSelect
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
if (!record.state) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<StatusTag
|
||||
download={
|
||||
record.state === InstanceStatusMap.Downloading
|
||||
? { percent: record.download_progress }
|
||||
: undefined
|
||||
}
|
||||
extra={
|
||||
record.state === InstanceStatusMap.Error && record.worker_id ? (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
style={{ paddingLeft: 0 }}
|
||||
onClick={() => onSelect('viewlog', record)}
|
||||
>
|
||||
{intl.formatMessage({ id: 'models.list.more.logs' })}
|
||||
</Button>
|
||||
) : 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;
|
||||
@@ -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<string, number>) => {
|
||||
return _.sum(_.values(vram));
|
||||
};
|
||||
|
||||
const WorkerInfoContent: React.FC<NameCellProps> = ({ record, modelData }) => {
|
||||
const intl = useIntl();
|
||||
let workerIp = '-';
|
||||
if (record.worker_ip) {
|
||||
workerIp = record.port
|
||||
? `${record.worker_ip}:${record.port}`
|
||||
: record.worker_ip;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<div>{record.worker_name}</div>
|
||||
<div className="flex-center">
|
||||
<HddFilled className="m-r-5" />
|
||||
{workerIp}
|
||||
</div>
|
||||
<div className="flex-center">
|
||||
<IconFont type="icon-filled-gpu" className="m-r-5" />
|
||||
{intl.formatMessage({ id: 'models.table.gpuindex' })}: [
|
||||
{_.join(
|
||||
record.gpu_indexes?.sort?.((a, b) => a - b),
|
||||
','
|
||||
)}
|
||||
]
|
||||
</div>
|
||||
<div className="flex-center">
|
||||
<ThunderboltFilled className="m-r-5" />
|
||||
{intl.formatMessage({ id: 'models.form.backend' })}:{' '}
|
||||
{record?.backend || modelData?.backend || ''}
|
||||
{record.backend_version || modelData?.backend_version
|
||||
? `(${record.backend_version || modelData?.backend_version})`
|
||||
: ''}
|
||||
</div>
|
||||
<div className="flex-center">
|
||||
<PieChartFilled className="m-r-5" />
|
||||
{intl.formatMessage({ id: 'models.table.vram.allocated' })}:{' '}
|
||||
{convertFileSize(
|
||||
record.computed_resource_claim?.vram
|
||||
? calcTotalVram(record.computed_resource_claim?.vram)
|
||||
: 0,
|
||||
1
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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}
|
||||
styles={{
|
||||
container: {
|
||||
width: 'max-content',
|
||||
maxWidth: '400px'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="server-info">
|
||||
<InfoCircleOutlined />
|
||||
</span>
|
||||
</Tooltip>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const NameCell: React.FC<NameCellProps> = ({
|
||||
record,
|
||||
modelData,
|
||||
defaultOpenId
|
||||
}) => {
|
||||
return (
|
||||
<span className="flex-center instance-name">
|
||||
<AutoTooltip title={record.name} ghost>
|
||||
<span className="m-r-5">{record.name}</span>
|
||||
</AutoTooltip>
|
||||
{!!record.worker_id && (
|
||||
<WorkerInfo
|
||||
title={
|
||||
<WorkerInfoContent
|
||||
record={record}
|
||||
modelData={modelData}
|
||||
></WorkerInfoContent>
|
||||
}
|
||||
defaultOpen={defaultOpenId === record.name}
|
||||
></WorkerInfo>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default NameCell;
|
||||
@@ -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 (
|
||||
<StatusTag
|
||||
download={{
|
||||
percent: row.download_progress
|
||||
}}
|
||||
statusValue={{
|
||||
status: row.download_progress
|
||||
? status[InstanceStatusMap.Running]
|
||||
: status[InstanceStatusMap.Initializing],
|
||||
text: row.download_progress,
|
||||
message: ''
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
];
|
||||
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<string, number>) => {
|
||||
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 (
|
||||
<span className="server-info-wrapper">
|
||||
<Tooltip
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
title={props.title}
|
||||
styles={{
|
||||
container: {
|
||||
width: 'max-content',
|
||||
maxWidth: '400px'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="server-info">
|
||||
<InfoCircleOutlined />
|
||||
</span>
|
||||
</Tooltip>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<div>
|
||||
{severList.length > 0 && (
|
||||
<SimpleTabel
|
||||
columns={downloadList}
|
||||
dataSource={[...mainWorker, ...list]}
|
||||
rowKey="worker_name"
|
||||
theme="light"
|
||||
></SimpleTabel>
|
||||
)}
|
||||
|
||||
{draftModelList.length > 0 && (
|
||||
<SimpleTabel
|
||||
columns={draftModelDownloadList}
|
||||
dataSource={[...draftModelList]}
|
||||
rowKey="worker_name"
|
||||
theme="light"
|
||||
></SimpleTabel>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<Tooltip
|
||||
arrow={true}
|
||||
styles={{
|
||||
container: {
|
||||
width: 360,
|
||||
backgroundColor: 'var(--color-spotlight-bg)'
|
||||
}
|
||||
}}
|
||||
classNames={{
|
||||
root: 'light-downloading-tooltip'
|
||||
}}
|
||||
title={
|
||||
<RenderRayactorDownloading
|
||||
severList={severList}
|
||||
workerList={workerList}
|
||||
instanceData={instanceData}
|
||||
></RenderRayactorDownloading>
|
||||
}
|
||||
>
|
||||
<Progress
|
||||
showInfo={false}
|
||||
type="circle"
|
||||
size={16}
|
||||
strokeColor="var(--ant-color-success)"
|
||||
percent={
|
||||
_.find(severList, (item: any) => item.download_progress < 100)
|
||||
?.download_progress ||
|
||||
instanceData.draft_model_download_progress ||
|
||||
0
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const InstanceStatusTag = (
|
||||
props: Pick<InstanceItemProps, 'instanceData' | 'handleChildSelect'>
|
||||
) => {
|
||||
const intl = useIntl();
|
||||
const { instanceData, handleChildSelect } = props;
|
||||
if (!instanceData.state) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<StatusTag
|
||||
download={
|
||||
instanceData.state === InstanceStatusMap.Downloading
|
||||
? { percent: instanceData.download_progress }
|
||||
: undefined
|
||||
}
|
||||
extra={
|
||||
instanceData.state === InstanceStatusMap.Error &&
|
||||
instanceData.worker_id ? (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
style={{ paddingLeft: 0 }}
|
||||
onClick={() => handleChildSelect('viewlog', instanceData)}
|
||||
>
|
||||
{intl.formatMessage({ id: 'models.list.more.logs' })}
|
||||
</Button>
|
||||
) : 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: <IconFont type="icon-logs" />
|
||||
},
|
||||
{
|
||||
label: 'common.button.downloadLog',
|
||||
key: 'download',
|
||||
status: [
|
||||
InstanceStatusMap.Initializing,
|
||||
InstanceStatusMap.Running,
|
||||
InstanceStatusMap.Error,
|
||||
InstanceStatusMap.Starting,
|
||||
InstanceStatusMap.Downloading
|
||||
],
|
||||
icon: <DownloadOutlined />
|
||||
},
|
||||
{
|
||||
label: 'models.table.instance.benchmark',
|
||||
key: 'benchmark',
|
||||
status: [InstanceStatusMap.Running],
|
||||
icon: <IconFont type="icon-speed" />
|
||||
},
|
||||
{
|
||||
label: 'common.button.delrecreate',
|
||||
key: 'delete',
|
||||
props: {
|
||||
danger: true
|
||||
},
|
||||
icon: <DeleteOutlined />
|
||||
}
|
||||
];
|
||||
|
||||
const renderGpuIndexs = (gpuIndexes: number[]) => {
|
||||
return (
|
||||
<GPUIndexWrapper>
|
||||
{_.chunk(gpuIndexes, 8).map((item: number[], index: number) => {
|
||||
return <span key={index}>{item.join(',')}</span>;
|
||||
})}
|
||||
</GPUIndexWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
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)}
|
||||
<span>(main)</span>
|
||||
</>
|
||||
) : (
|
||||
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 (
|
||||
<div
|
||||
style={{
|
||||
width: 280,
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const InstanceItem: React.FC<InstanceItemProps> = ({
|
||||
instanceData,
|
||||
workerList,
|
||||
@@ -430,350 +28,74 @@ const InstanceItem: React.FC<InstanceItemProps> = ({
|
||||
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: (
|
||||
<span>{intl.formatMessage({ id: 'common.button.cancel' })}</span>
|
||||
),
|
||||
description: <Progress percent={data.percent} size="small"></Progress>,
|
||||
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 (
|
||||
<div>
|
||||
<div>{instanceData.worker_name}</div>
|
||||
<div className="flex-center">
|
||||
<HddFilled className="m-r-5" />
|
||||
{workerIp}
|
||||
</div>
|
||||
<div className="flex-center">
|
||||
<IconFont type="icon-filled-gpu" className="m-r-5" />
|
||||
{intl.formatMessage({ id: 'models.table.gpuindex' })}: [
|
||||
{_.join(
|
||||
instanceData.gpu_indexes?.sort?.((a, b) => a - b),
|
||||
','
|
||||
)}
|
||||
]
|
||||
</div>
|
||||
<div className="flex-center">
|
||||
<ThunderboltFilled className="m-r-5" />
|
||||
{intl.formatMessage({ id: 'models.form.backend' })}:{' '}
|
||||
{instanceData?.backend || modelData?.backend || ''}
|
||||
{instanceData.backend_version || modelData?.backend_version
|
||||
? `(${instanceData.backend_version || modelData?.backend_version})`
|
||||
: ''}
|
||||
</div>
|
||||
<div className="flex-center">
|
||||
<PieChartFilled className="m-r-5" />
|
||||
{intl.formatMessage({ id: 'models.table.vram.allocated' })}:{' '}
|
||||
{convertFileSize(
|
||||
instanceData.computed_resource_claim?.vram
|
||||
? calcTotalVram(instanceData.computed_resource_claim?.vram)
|
||||
: 0,
|
||||
1
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}, [
|
||||
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 (
|
||||
<div>
|
||||
<SimpleTabel
|
||||
rowKey="worker_name"
|
||||
columns={distributeCols}
|
||||
dataSource={[...mainWorker, ...list]}
|
||||
></SimpleTabel>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderDistributionInfo = (distributed_servers: DistributedServers) => {
|
||||
const severList: DistributedServerItem[] =
|
||||
distributed_servers?.subordinate_workers || [];
|
||||
|
||||
if (!severList.length) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<TooltipOverlayScroller
|
||||
toolTipProps={{
|
||||
styles: {
|
||||
container: {
|
||||
width: 'max-content',
|
||||
maxWidth: '520px',
|
||||
minWidth: '400px'
|
||||
}
|
||||
}
|
||||
}}
|
||||
title={renderDistributedServer(severList)}
|
||||
>
|
||||
<span>
|
||||
<ThemeTag
|
||||
opacity={0.75}
|
||||
color="processing"
|
||||
style={{
|
||||
marginRight: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
maxWidth: '100%',
|
||||
minWidth: 50,
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
borderRadius: 12
|
||||
}}
|
||||
>
|
||||
<InfoCircleOutlined className="m-r-5" />
|
||||
{intl.formatMessage({
|
||||
id: 'models.table.acrossworker'
|
||||
})}
|
||||
</ThemeTag>
|
||||
</span>
|
||||
</TooltipOverlayScroller>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<Tooltip
|
||||
styles={{
|
||||
container: {
|
||||
paddingInline: 12
|
||||
}
|
||||
}}
|
||||
title={
|
||||
<InfoColumn fieldList={fieldList} data={offloadData}></InfoColumn>
|
||||
}
|
||||
>
|
||||
<span>
|
||||
<ThemeTag
|
||||
opacity={0.75}
|
||||
color="cyan"
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
maxWidth: '100%',
|
||||
minWidth: 50,
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
borderRadius: 12
|
||||
}}
|
||||
>
|
||||
<InfoCircleOutlined className="m-r-5" />
|
||||
{intl.formatMessage({
|
||||
id: 'models.table.cpuoffload'
|
||||
})}
|
||||
</ThemeTag>
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
}, [
|
||||
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}
|
||||
<div style={{ borderRadius: 'var(--ant-table-header-border-radius)' }}>
|
||||
<RowChildren>
|
||||
<Row style={{ width: '100%' }} align="middle">
|
||||
<Col
|
||||
span={6}
|
||||
<div style={{ borderRadius: 'var(--ant-table-header-border-radius)' }}>
|
||||
<RowChildren>
|
||||
<Row style={{ width: '100%' }} align="middle">
|
||||
<Col
|
||||
span={6}
|
||||
style={{
|
||||
paddingInline: 'var(--ant-table-cell-padding-inline)'
|
||||
}}
|
||||
>
|
||||
<NameCell
|
||||
record={instanceData}
|
||||
modelData={modelData}
|
||||
defaultOpenId={defaultOpenId}
|
||||
></NameCell>
|
||||
</Col>
|
||||
<Col span={7}>
|
||||
<span
|
||||
style={{
|
||||
paddingInline: 'var(--ant-table-cell-padding-inline)'
|
||||
paddingLeft: '58px',
|
||||
flexWrap: 'wrap',
|
||||
gap: '8px'
|
||||
}}
|
||||
className="flex align-center"
|
||||
>
|
||||
<span className="flex-center instance-name">
|
||||
<AutoTooltip title={instanceData.name} ghost>
|
||||
<span className="m-r-5">{instanceData.name}</span>
|
||||
</AutoTooltip>
|
||||
{!!instanceData.worker_id && (
|
||||
<WorkerInfo
|
||||
title={renderWorkerInfo}
|
||||
defaultOpen={defaultOpenId === instanceData.name}
|
||||
></WorkerInfo>
|
||||
)}
|
||||
</span>
|
||||
</Col>
|
||||
<Col span={7}>
|
||||
<span
|
||||
style={{
|
||||
paddingLeft: '58px',
|
||||
flexWrap: 'wrap',
|
||||
gap: '8px'
|
||||
}}
|
||||
className="flex align-center"
|
||||
>
|
||||
{renderOffloadInfo}
|
||||
{renderDistributionInfo(
|
||||
instanceData.distributed_servers || ({} as DistributedServers)
|
||||
)}
|
||||
</span>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<span
|
||||
style={{ paddingLeft: '40px', gap: 4 }}
|
||||
className="flex-center"
|
||||
>
|
||||
<InstanceStatusTag
|
||||
instanceData={instanceData}
|
||||
handleChildSelect={handleChildSelect}
|
||||
/>
|
||||
<RenderWorkerDownloading
|
||||
backend={modelData?.backend}
|
||||
distributed_servers={instanceData.distributed_servers}
|
||||
workerList={workerList}
|
||||
instanceData={instanceData}
|
||||
></RenderWorkerDownloading>
|
||||
</span>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<span style={{ paddingLeft: 43 }} className="flex">
|
||||
<AutoTooltip ghost>
|
||||
{dayjs(instanceData.created_at).format('YYYY-MM-DD HH:mm:ss')}
|
||||
</AutoTooltip>
|
||||
</span>
|
||||
</Col>
|
||||
<Col span={3}>
|
||||
<div style={{ paddingLeft: 36 }}>
|
||||
<DropdownButtons
|
||||
items={actionItems}
|
||||
onSelect={handleOnSelect}
|
||||
></DropdownButtons>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</RowChildren>
|
||||
</div>
|
||||
</>
|
||||
<CPUOffloadingCell record={instanceData}></CPUOffloadingCell>
|
||||
<DistributeInfoCell
|
||||
record={instanceData}
|
||||
workerList={workerList}
|
||||
></DistributeInfoCell>
|
||||
</span>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<span
|
||||
style={{ paddingLeft: '40px', gap: 4 }}
|
||||
className="flex-center"
|
||||
>
|
||||
<InstanceStatusCell
|
||||
record={instanceData}
|
||||
onSelect={handleChildSelect}
|
||||
/>
|
||||
<DownloadingStatusCell
|
||||
backend={modelData?.backend}
|
||||
distributed_servers={instanceData.distributed_servers}
|
||||
workerList={workerList}
|
||||
record={instanceData}
|
||||
></DownloadingStatusCell>
|
||||
</span>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<span style={{ paddingLeft: 43 }} className="flex">
|
||||
<AutoTooltip ghost>
|
||||
{dayjs(instanceData.created_at).format('YYYY-MM-DD HH:mm:ss')}
|
||||
</AutoTooltip>
|
||||
</span>
|
||||
</Col>
|
||||
<Col span={3}>
|
||||
<div style={{ paddingLeft: 36 }}>
|
||||
<ActionsCell
|
||||
record={instanceData}
|
||||
modelData={modelData}
|
||||
onSelect={handleChildSelect}
|
||||
></ActionsCell>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</RowChildren>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default InstanceItem;
|
||||
|
||||
Reference in New Issue
Block a user