chore: show existing when add file

This commit is contained in:
jialin
2025-10-23 11:52:49 +08:00
parent cc0580301e
commit a815e71bfe
25 changed files with 256 additions and 97 deletions
+1
View File
@@ -55,6 +55,7 @@
"lodash": "^4.17.21",
"mammoth": "^1.8.0",
"marked": "^14.1.0",
"minimatch": "^10.0.3",
"ml-dataset-iris": "^1.2.1",
"ml-pca": "^4.1.1",
"monaco-editor": "^0.53.0",
+22
View File
@@ -126,6 +126,9 @@ dependencies:
marked:
specifier: ^14.1.0
version: 14.1.4
minimatch:
specifier: ^10.0.3
version: 10.0.3
ml-dataset-iris:
specifier: ^1.2.1
version: 1.2.1
@@ -2647,6 +2650,18 @@ packages:
- supports-color
dev: false
/@isaacs/balanced-match@4.0.1:
resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==, tarball: https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz}
engines: {node: 20 || >=22}
dev: false
/@isaacs/brace-expansion@5.0.0:
resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==, tarball: https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz}
engines: {node: 20 || >=22}
dependencies:
'@isaacs/balanced-match': 4.0.1
dev: false
/@isaacs/cliui@8.0.2:
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==, tarball: https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz}
engines: {node: '>=12'}
@@ -11096,6 +11111,13 @@ packages:
resolution: {integrity: sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==, tarball: https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz}
dev: false
/minimatch@10.0.3:
resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==, tarball: https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz}
engines: {node: 20 || >=22}
dependencies:
'@isaacs/brace-expansion': 5.0.0
dev: false
/minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==, tarball: https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz}
dependencies:
+3 -1
View File
@@ -20,8 +20,9 @@ export const OverlayScroller: React.FC<
maxHeight?: number;
style?: React.CSSProperties;
children: React.ReactNode;
onScroll?: (e: React.UIEvent<HTMLDivElement, UIEvent>) => void;
}
> = ({ children, maxHeight, scrollbars, oppositeTheme, style }) => {
> = ({ children, maxHeight, scrollbars, oppositeTheme, style, onScroll }) => {
const scroller = React.useRef<any>(null);
const { initialize } = useOverlayScroller({
options: {
@@ -43,6 +44,7 @@ export const OverlayScroller: React.FC<
className="overlay-scroller-wrapper"
$maxHeight={maxHeight || 200}
hidden={false}
onWheel={onScroll}
style={{
paddingInlineStart: 8,
paddingInlineEnd: 8,
+7 -7
View File
@@ -1,11 +1,11 @@
import { PageActionType, StatusType } from './types';
import { StatusType } from './types';
export const PageAction: Record<string, PageActionType> = {
CREATE: 'create',
UPDATE: 'update',
VIEW: 'view',
EDIT: 'edit'
};
export enum PageAction {
CREATE = 'create',
UPDATE = 'update',
VIEW = 'view',
EDIT = 'edit'
}
export const StatusColorMap: Record<
StatusType,
+2 -1
View File
@@ -94,5 +94,6 @@ export default {
'resource.register.maos.support': 'Apple Silicon (M series), macOS 14+',
'resource.register.windows.support': 'win 10, win 11',
'resources.model.instance': 'Model Instance',
'resources.worker.download.privatekey': 'Download Private Key'
'resources.worker.download.privatekey': 'Download Private Key',
'resources.modelfiles.form.exsting': 'Existing'
};
+3 -1
View File
@@ -95,7 +95,8 @@ export default {
'resource.register.windows.support': 'win 10, win 11',
'resources.model.instance': 'Model Instance',
'resources.worker.download.privatekey': 'Download Private Key',
'resources.worker': 'Worker'
'resources.worker': 'Worker',
'resources.modelfiles.form.exsting': 'Existing'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
@@ -114,4 +115,5 @@ export default {
// 13. 'resources.model.instance': 'Model Instance',
// 14. 'resources.worker.download.privatekey': 'Download Private Key',
// 15. 'resources.worker': 'Worker',
// 16. 'resources.modelfiles.form.exsting': 'Existing'
// ========== End of To-Do List ==========
+2 -1
View File
@@ -93,7 +93,8 @@ export default {
'resource.register.windows.support': 'Windows 10, Windows 11',
'resources.model.instance': 'Модель экземпляра',
'resources.worker.download.privatekey': 'Скачать приватный ключ',
'resources.worker': 'Worker'
'resources.worker': 'Worker',
'resources.modelfiles.form.exsting': 'Существующий'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
+2 -1
View File
@@ -92,5 +92,6 @@ export default {
'resource.register.maos.support': 'M 芯片,macOS 14+',
'resource.register.windows.support': 'win 10, win 11',
'resources.model.instance': '模型实例',
'resources.worker.download.privatekey': '下载私钥'
'resources.worker.download.privatekey': '下载私钥',
'resources.modelfiles.form.exsting': '已存在'
};
+2 -2
View File
@@ -6,8 +6,8 @@ export default {
'users.table.username': '用户名',
'users.table.role': '角色',
'users.table.status': '状态',
'users.status.active': '激活',
'users.status.inactive': '用',
'users.status.active': '启用',
'users.status.inactive': '用',
'users.form.active': '激活账户',
'users.form.active.description': '启用或禁用此用户账户',
'users.form.fullname': '全名',
+2 -2
View File
@@ -139,7 +139,7 @@ const SelectPanel: React.FC<SelectPanelProps> = ({
</Button>
</Header>
<SelectedList
maxHeight={height - 40}
maxHeight={height - 50}
selectedList={options.filter((item) =>
selectedKeys.includes(item.key)
)}
@@ -194,7 +194,7 @@ const SelectPanel: React.FC<SelectPanelProps> = ({
</Header>
{showOptions.length > 0 ? (
<List
maxHeight={height - 40}
maxHeight={height - 50}
dataList={showOptions}
selectedKeys={selectedKeys}
onSelectChange={handleSelectChange}
@@ -547,6 +547,11 @@ const AddModal: React.FC<AddModalProps> = (props) => {
>
<FormWrapper>
<ColumnWrapper
styles={{
container: {
paddingTop: 0
}
}}
paddingBottom={
warningStatus.show
? Array.isArray(warningStatus.message)
+3 -3
View File
@@ -8,13 +8,13 @@ import '../style/gpu-card.less';
const CardWrapper = styled.div`
display: flex;
gap: 10px;
gap: 8px;
flex-direction: column;
align-items: flex-start;
justify-content: center;
width: 100%;
border-bottom: 1px solid var(--ant-color-split);
padding-bottom: 10px;
padding-bottom: 5px;
`;
const Header = styled.div`
@@ -25,7 +25,7 @@ const Description = styled.div`
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 10px;
gap: 8px;
color: var(--ant-color-text-tertiary);
`;
+16 -4
View File
@@ -10,6 +10,7 @@ import React, { useMemo } from 'react';
import styled from 'styled-components';
import { modelCategories, modelCategoriesMap, modelSourceMap } from '../config';
import { categoryToPathMap } from '../config/button-actions';
import { categoryConfig } from './model-tag';
const MyModelsStatusValueMap = {
Inactive: 'Inactive',
@@ -198,10 +199,21 @@ const ModelItem: React.FC<{
<div className="content">
<div className="footer">
<div className="extra-info">
<ThemeTag className="tag-item" color="blue">
{_.find(modelCategories, { value: model.categories?.[0] })
?.label || model.categories?.[0]}
</ThemeTag>
{model.categories?.length > 0 &&
model.categories.map((sItem: string) => {
return (
<ThemeTag
icon={categoryConfig[sItem]?.icon}
key={sItem}
className="tag-item"
color={categoryConfig[sItem]?.color || 'blue'}
opacity={0.7}
>
{_.find(modelCategories, { value: sItem })?.label ||
sItem}
</ThemeTag>
);
})}
{maxToken > 0 && (
<ThemeTag className="tag-item" color="purple">
{maxToken}K context
+1 -1
View File
@@ -7,7 +7,7 @@ import {
import { Tag } from 'antd';
import { modelCategoriesMap } from '../config';
const categoryConfig = {
export const categoryConfig = {
[modelCategoriesMap.reranker]: {
icon: <IconFont type="icon-rank1" />,
color: 'cyan',
+8 -3
View File
@@ -22,6 +22,7 @@ type AddModalProps = {
width?: string | number;
hasLinuxWorker?: boolean;
workerOptions: any[];
workersList?: any[];
onOk: (values: FormData) => void;
onCancel: () => void;
};
@@ -47,7 +48,8 @@ const DownloadModel: React.FC<AddModalProps> = (props) => {
hasLinuxWorker,
source,
width = 600,
workerOptions
workerOptions,
workersList
} = props || {};
const SEARCH_SOURCE = [
modelSourceMap.huggingface_value,
@@ -66,7 +68,7 @@ const DownloadModel: React.FC<AddModalProps> = (props) => {
if (source === modelSourceMap.huggingface_value) {
const huggingFaceModel = {
huggingface_repo_id: selectedModel.name,
huggingface_filename: null
huggingface_filename: fileName || null
};
return huggingFaceModel;
}
@@ -74,7 +76,7 @@ const DownloadModel: React.FC<AddModalProps> = (props) => {
if (source === modelSourceMap.modelscope_value) {
const modelScopeModel = {
model_scope_model_id: selectedModel.name,
model_scope_file_path: null
model_scope_file_path: fileName || null
};
return modelScopeModel;
}
@@ -238,6 +240,9 @@ const DownloadModel: React.FC<AddModalProps> = (props) => {
ref={form}
onOk={handleOk}
source={source}
selectedModel={selectedModel}
fileName={fileName}
workersList={workersList}
workerOptions={workerOptions}
></TargetForm>
</>
+72 -7
View File
@@ -7,22 +7,64 @@ import { ModelFileFormData as FormData } from '@/pages/resources/config/types';
import { useIntl } from '@umijs/max';
import { Form } from 'antd';
import _ from 'lodash';
import React, { forwardRef, useImperativeHandle, useMemo } from 'react';
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: any[];
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 { onOk, source, workerOptions } = props;
const {
modelFileOptions,
getModelFileList,
generateWorkersModelFileOptions
} = useGenerateWorkersModelFileOptions();
const { onOk, source, workerOptions, workersList, selectedModel, fileName } =
props;
const { getRuleMessage } = useAppUtils();
const intl = useIntl();
const [form] = Form.useForm();
useEffect(() => {
const init = async () => {
try {
if (workersList && workersList?.length > 0) {
const modelFiles = await getModelFileList();
generateWorkersModelFileOptions(modelFiles, workersList || []);
}
} catch (error) {}
};
init();
}, [workersList]);
useImperativeHandle(ref, () => ({
form
}));
@@ -33,10 +75,6 @@ const TargetForm: React.FC<TargetFormProps> = forwardRef((props, ref) => {
...data,
worker_id: data.worker_id?.[1]
});
console.log('Form Data: ', {
...data,
worker_id: data.worker_id?.[1]
});
};
const handleOnLocalPathBlur = (e: any) => {
@@ -49,6 +87,32 @@ const TargetForm: React.FC<TargetFormProps> = forwardRef((props, ref) => {
});
};
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 || '') ||
minimatch(child.fileName || '', fileName || '');
return child.repoId === selectedModel?.name && isSameFile;
});
return (
<span>
{data.label}
{isExisting && (
<span className="text-tertiary m-l-4">
({intl.formatMessage({ id: 'resources.modelfiles.form.exsting' })})
</span>
)}
</span>
);
};
const renderLocalPathFields = () => {
return (
<>
@@ -136,6 +200,7 @@ const TargetForm: React.FC<TargetFormProps> = forwardRef((props, ref) => {
label={intl.formatMessage({ id: 'resources.worker' })}
options={workerOptions}
showCheckedStrategy="SHOW_CHILD"
optionNode={renderOptionNode}
getPopupContainer={(triggerNode) => triggerNode.parentNode}
></SealCascader>
</Form.Item>
+10 -10
View File
@@ -1,3 +1,4 @@
import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import CollapsePanel from '@/pages/_components/collapse-panel';
import { useIntl } from '@umijs/max';
@@ -122,22 +123,21 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
const SegmentedTop = useMemo(() => {
if (
[
modelSourceMap.huggingface_value,
modelSourceMap.modelscope_value
].includes(source) &&
formKey === deployFormKeyMap.deployment
modelSourceMap.local_path_value === source ||
action === PageAction.EDIT ||
formKey === deployFormKeyMap.catalog
) {
return {
top: 50,
offsetTop: 146
top: 0,
offsetTop: 96
};
}
return {
top: 0,
offsetTop: 96
top: 50,
offsetTop: 146
};
}, [source, formKey]);
}, [source, formKey, action]);
const handleSumit = () => {
form.submit();
@@ -98,7 +98,6 @@ const LocalPathForm: React.FC = () => {
<SealInput.Input
allowClear
required
onBlur={handleOnBlur}
onFocus={handleOnFocus}
label={intl.formatMessage({ id: 'models.form.filePath' })}
description={<TooltipList list={localPathTipsList}></TooltipList>}
@@ -94,6 +94,14 @@ const ScheduleTypeForm: React.FC = () => {
const handleOnStepReplica = (value: number | null) => {
if (value === null) {
form.setFieldValue(['gpu_selector', 'gpus_per_replica'], 1);
return;
}
const isPowerOfTwo = (n: number) => (n & (n - 1)) === 0 && n !== 0; // check power of two
if (!isPowerOfTwo(value)) {
const newValue = Math.pow(2, Math.round(Math.log2(value)));
form.setFieldValue(['gpu_selector', 'gpus_per_replica'], newValue);
onValuesChange?.({}, form.getFieldsValue());
}
};
+28 -12
View File
@@ -25,12 +25,27 @@ export type MessageStatus = {
evaluateResult?: EvaluateResult;
};
export interface ModelFileOption {
label: string;
value: number;
labels?: Record<string, string>;
parent: boolean;
repoId?: string;
fileName?: string;
[key: string]: any;
children?: ModelFileOption[];
}
export type WarningStausOptions = {
lockAfterUpdate?: boolean;
override?: boolean;
};
export const useGenerateModelFileOptions = () => {
export const useGenerateWorkersModelFileOptions = () => {
const [modelFileOptions, setModelFileOptions] = useState<ModelFileOption[]>(
[]
);
const getModelFileList = async () => {
try {
const res = await queryModelFilesList({ page: 1, perPage: 100 });
@@ -42,7 +57,7 @@ export const useGenerateModelFileOptions = () => {
}
};
const generateModelFileOptions = (list: any[], workerList: any[]) => {
const generateWorkersModelFileOptions = (list: any[], workerList: any[]) => {
const workerFields = new Set(['name', 'id', 'ip', 'status']);
const workersMap = new Map<number, WorkerListItem>();
@@ -54,7 +69,7 @@ export const useGenerateModelFileOptions = () => {
const result = Array.from(workersMap.values()).map((worker) => ({
label: worker.name,
value: worker.name,
value: worker.id,
labels: worker.labels,
parent: true,
children: list
@@ -63,18 +78,16 @@ export const useGenerateModelFileOptions = () => {
item.worker_id === worker.id && !!item.resolved_paths?.length
)
.map((item) => {
const resolved_paths =
Array.isArray(item.resolved_paths) && item.resolved_paths.length
? item.resolved_paths[0].split('/')
: [];
const label =
resolved_paths.length > 0 ? resolved_paths.pop() : 'Unknown File';
return {
label: item.resolved_paths[0] || '',
value: item.resolved_paths[0] || '',
worker_labels: worker.labels,
worker_name: worker.name,
parent: false,
repoId: item.huggingface_repo_id || item.model_scope_model_id || '',
fileName:
item.huggingface_filename ||
item.model_scope_file_path ||
item.local_path ||
'',
...item
};
}),
@@ -95,12 +108,15 @@ export const useGenerateModelFileOptions = () => {
return acc;
}, []);
setModelFileOptions(result);
return childrenList;
};
return {
getModelFileList,
generateModelFileOptions
modelFileOptions,
generateWorkersModelFileOptions
};
};
@@ -15,7 +15,7 @@ import { GPUListItem, ListItem } from '../config/types';
type EmptyObject = Record<never, never>;
type CascaderOption<T extends object = EmptyObject> = {
export type CascaderOption<T extends object = EmptyObject> = {
label: string;
value: string | number;
parent?: boolean;
@@ -210,7 +210,10 @@ export const useGenerateWorkerOptions = () => {
cluster_id: item.cluster_id,
state: item.state,
label: item.name,
value: item.id
value: item.id,
id: item.id,
labels: item.labels || {},
name: item.name
}))
);
setClusterList(
@@ -349,6 +349,7 @@ const ModelFiles = () => {
width={downloadModalStatus.width}
hasLinuxWorker={downloadModalStatus.hasLinuxWorker}
workerOptions={readyWorkers}
workersList={workersList}
></DownloadModal>
<DeployModal
deploymentType="modelFiles"
@@ -161,6 +161,7 @@ const getModelInfo = (record: ListItem) => {
filename: record.huggingface_filename || record.huggingface_repo_id
};
}
if (record.source === modelSourceMap.modelscope_value) {
return {
source: `${source}/${record.model_scope_model_id}`,
@@ -169,14 +170,7 @@ const getModelInfo = (record: ListItem) => {
filename: record.model_scope_file_path || record.model_scope_model_id
};
}
if (record.source === modelSourceMap.ollama_library_value) {
return {
source: `${source}/${record.ollama_library_model_name}`,
repo_id: record.ollama_library_model_name,
title: record.ollama_library_model_name,
filename: record.ollama_library_model_name
};
}
return {
source: `${source}${record.local_path}`,
repo_id: record.local_path,
@@ -275,7 +269,7 @@ const useFilesColumns = (props: {
},
render: (text: string, record: ListItem) => {
const modelInfo = getModelInfo(record);
const { repo_id, source } = modelInfo;
const { source } = modelInfo;
return (
<TextWrapper style={{ paddingRight: 8 }}>
<AutoTooltip ghost title={source}>
+19 -15
View File
@@ -6,7 +6,7 @@ import SealSelect from '@/components/seal-form/seal-select';
import SealSwitch from '@/components/seal-form/seal-switch';
import { PageAction, PasswordReg } from '@/config';
import { PageActionType } from '@/config/types';
import { useIntl } from '@umijs/max';
import { useIntl, useModel } from '@umijs/max';
import { Form, Select } from 'antd';
import { useEffect } from 'react';
import { UserRoles, UserRolesOptions } from '../config';
@@ -28,6 +28,7 @@ const AddModal: React.FC<AddModalProps> = ({
data,
onCancel
}) => {
const { initialState } = useModel('@@initialState') || {};
const [form] = Form.useForm();
const intl = useIntl();
@@ -125,20 +126,23 @@ const AddModal: React.FC<AddModalProps> = ({
</SealSelect>
</Form.Item>
</div>
<div style={{ flex: 1 }}>
<Form.Item<FormData>
name="is_active"
rules={[{ required: false }]}
valuePropName="checked"
>
<SealSwitch
label={intl.formatMessage({ id: 'users.form.active' })}
description={intl.formatMessage({
id: 'users.form.active.description'
})}
/>
</Form.Item>
</div>
{(data?.id !== initialState?.currentUser?.id ||
action === PageAction.CREATE) && (
<div style={{ flex: 1 }}>
<Form.Item<FormData>
name="is_active"
rules={[{ required: false }]}
valuePropName="checked"
>
<SealSwitch
label={intl.formatMessage({ id: 'users.form.active' })}
description={intl.formatMessage({
id: 'users.form.active.description'
})}
/>
</Form.Item>
</div>
)}
</div>
<Form.Item<FormData>
+31 -14
View File
@@ -109,20 +109,37 @@ const useUsersColumns = ({
render: (text: string, record: ListItem) => {
return (
<>
<Tag
style={{
marginRight: 0,
paddingInline: 12,
borderRadius: 12
}}
color={record.is_active ? 'success' : 'default'}
>
{intl.formatMessage({
id: record.is_active
? 'users.status.active'
: 'users.status.inactive'
})}
</Tag>
{record.is_active ? (
<Tag
style={{
marginRight: 0,
paddingInline: 12,
borderRadius: 12,
background: 'unset',
borderColor: 'var(--ant-color-success)'
}}
color="success"
>
{intl.formatMessage({
id: 'users.status.active'
})}
</Tag>
) : (
<Tag
style={{
marginRight: 0,
paddingInline: 12,
borderRadius: 12,
background: 'unset',
color: 'var(--ant-color-text-description)'
}}
color="default"
>
{intl.formatMessage({
id: 'users.status.inactive'
})}
</Tag>
)}
</>
);
}