From 90111bc97b8e288cdcdbcc87ff3d308ac150a061 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A0=81=E5=86=9C=E5=BE=90?= <10495708+xu-zhonglin@user.noreply.gitee.com> Date: Fri, 10 Apr 2026 18:52:05 +0800 Subject: [PATCH] feat: vm style change --- src/components/CreateComponentModal/index.js | 17 +- src/components/GlobalRouter/index.js | 128 +- src/components/GlobalRouter/index.less | 77 + .../ImageVirtualMachineForm/index.js | 1701 +++++++++-------- .../ImageVirtualMachineForm/index.less | 288 ++- src/components/VMAssetCatalogModal/index.js | 263 +-- src/components/VMAssetCatalogModal/index.less | 150 +- src/locales/en-US/enterprise.js | 4 +- src/locales/en-US/team.js | 18 +- src/locales/zh-CN/enterprise.js | 2 +- src/locales/zh-CN/team.js | 18 +- 11 files changed, 1536 insertions(+), 1130 deletions(-) diff --git a/src/components/CreateComponentModal/index.js b/src/components/CreateComponentModal/index.js index 17f48145a..0f0583888 100644 --- a/src/components/CreateComponentModal/index.js +++ b/src/components/CreateComponentModal/index.js @@ -108,6 +108,7 @@ const CreateComponentModal = ({ visible, onCancel, dispatch, currentEnterprise, const [currentDatabaseType, setCurrentDatabaseType] = useState(null); const [showDatabaseForm, setShowDatabaseForm] = useState(false); const [virtualMachineImages, setVirtualMachineImages] = useState([]); + const [vmAssetCatalogVisible, setVmAssetCatalogVisible] = useState(false); // 插件相关状态 const [availablePlugins, setAvailablePlugins] = useState([]); @@ -1398,6 +1399,7 @@ const CreateComponentModal = ({ visible, onCancel, dispatch, currentEnterprise, setLocalMarketPage(1); setLocalMarketActiveTab('all'); setCurrentFormType(''); + setVmAssetCatalogVisible(false); setHasInitialized(false); // 重置初始化标志 } }, [visible, initialView, hasInitialized]); @@ -1524,6 +1526,10 @@ const CreateComponentModal = ({ visible, onCancel, dispatch, currentEnterprise, const handleBack = () => { if (currentView === 'form') { + if (currentFormType === 'vm' && vmAssetCatalogVisible) { + setVmAssetCatalogVisible(false); + return; + } setCurrentFormType(''); popViewHistory(); } else if (currentView === 'plugin') { @@ -2021,6 +2027,7 @@ const CreateComponentModal = ({ visible, onCancel, dispatch, currentEnterprise, case 'thirdList': return selectedOauthService ? formatMessage({ id: 'componentOverview.body.CreateComponentModal.repo' }, { name: selectedOauthService.name }) : formatMessage({ id: 'componentOverview.body.CreateComponentModal.source_repo' }); case 'form': + if (currentFormType === 'vm' && vmAssetCatalogVisible) return formatMessage({ id: 'Vm.assetCatalog.title' }); if (currentFormType === 'docker') return formatMessage({ id: 'componentOverview.body.CreateComponentModal.container' }); if (currentFormType === 'docker-compose') return formatMessage({ id: 'componentOverview.body.CreateComponentModal.docker_compose' }); if (currentFormType === 'code-custom') return formatMessage({ id: 'componentOverview.body.CreateComponentModal.source_code' }); @@ -2076,6 +2083,9 @@ const CreateComponentModal = ({ visible, onCancel, dispatch, currentEnterprise, const showSaaSPrice = PluginUtils.isInstallPlugin(pluginsList, 'rainbond-bill'); // 表单视图需要显示提交按钮 if (currentView === 'form') { + if (currentFormType === 'vm' && vmAssetCatalogVisible) { + return null; + } return (
- {/* 底部:收起按钮 */} + {/* 底部:升级提示和收起按钮 */}
+ {this.renderUpgradeEntry()} {this.renderCollapseButton()}
diff --git a/src/components/GlobalRouter/index.less b/src/components/GlobalRouter/index.less index 5ab4cb8e1..ec37c9dc3 100644 --- a/src/components/GlobalRouter/index.less +++ b/src/components/GlobalRouter/index.less @@ -89,6 +89,79 @@ // ==================== 底部区域 ==================== .menuFooter { flex-shrink: 0; + display: flex; + flex-direction: column; +} + +.upgradeCard { + display: flex; + align-items: center; + margin: 8px 12px 0; + padding: 10px 12px; + border-radius: 10px; + border: 1px solid fade(@success-color, 24%); + background: linear-gradient(180deg, fade(@success-color, 12%) 0%, fade(@success-color, 4%) 100%); + cursor: pointer; + transition: background @transition-duration @transition-timing, + box-shadow @transition-duration @transition-timing, + border-color @transition-duration @transition-timing; + + &:hover { + background: linear-gradient(180deg, fade(@success-color, 16%) 0%, fade(@success-color, 8%) 100%); + border-color: fade(@success-color, 36%); + box-shadow: 0 8px 20px fade(@success-color, 14%); + } +} + +.upgradeCardIcon { + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + width: 32px; + height: 32px; + border-radius: 8px; + background: fade(@success-color, 14%); + color: @success-color; + font-size: 16px; +} + +.upgradeCardContent { + min-width: 0; + margin-left: 8px; +} + +.upgradeCardTitle { + color: @heading-color; + font-size: 14px; + font-weight: 600; + line-height: 18px; +} + +.upgradeShortcut { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + margin: 12px 8px 0; + border-radius: 8px; + color: @success-color; + cursor: pointer; + transition: background @transition-duration @transition-timing, + color @transition-duration @transition-timing; + + &:hover { + background: fade(@success-color, 12%); + color: @success-color; + } +} + +.upgradeStatusIcon { + display: block; + width: 18px; + height: 18px; + color: currentColor; } // ==================== 视图切换器 ==================== @@ -492,6 +565,10 @@ background: #d9d9d9; } } + + .upgradeCard { + display: none; + } } // ==================== 折叠状态下的视图切换器 ==================== diff --git a/src/components/ImageVirtualMachineForm/index.js b/src/components/ImageVirtualMachineForm/index.js index 076cc1739..7bdd95378 100644 --- a/src/components/ImageVirtualMachineForm/index.js +++ b/src/components/ImageVirtualMachineForm/index.js @@ -1,51 +1,69 @@ /* eslint-disable react/jsx-indent */ /* eslint-disable no-nested-ternary */ -import { Alert, Button, Form, Input, Select, Radio, Upload, Icon, Tooltip, notification, Switch, Progress, message } from 'antd'; +import { + Alert, + Button, + Form, + Input, + Select, + Radio, + Upload, + Icon, + Tooltip, + notification, + Switch, + message +} from 'antd'; import { connect } from 'dva'; import React, { Fragment, PureComponent } from 'react'; import { formatMessage } from '@/utils/intl'; -import AddGroup from '../../components/AddOrEditGroup'; -import cookie from '../../utils/cookie'; -import anolisOS from '../../../public/images/anolis.png'; -import centOS from '../../../public/images/centos.png'; -import deepinOS from '../../../public/images/deepin.png'; -import ubuntuOS from '../../../public/images/ubuntu.png'; import { pinyin } from 'pinyin-pro'; import globalUtil from '../../utils/global'; import role from '@/utils/newRole'; import handleAPIError from '../../utils/error'; -import ChunkUploader from '../../utils/ChunkUploader'; import VMAssetCatalogModal from '../VMAssetCatalogModal'; import styles from './index.less'; - +import anolisOS from '../../../public/images/anolis.png'; +import centOS from '../../../public/images/centos.png'; +import deepinOS from '../../../public/images/deepin.png'; +import ubuntuOS from '../../../public/images/ubuntu.png'; const { Option } = Select; -const { TextArea } = Input; -const formItemLayout = { - labelCol: { - span: 7 +const PUBLIC_VM_OPTIONS = [ + { + key: 'centos7.9', + vm_url: 'https://mirrors.aliyun.com/centos/7.9.2009/isos/x86_64/CentOS-7-x86_64-Minimal-2009.iso', + image_name: 'centos7.9', + icon: centOS }, - wrapperCol: { - span: 15 - } -}; -const formItemLayouts = { - labelCol: { - span: 7 + { + key: 'anolisos7.9', + vm_url: 'https://mirrors.aliyun.com/anolis/7.9/isos/GA/x86_64/AnolisOS-7.9-Minimal-x86_64-dvd.iso', + image_name: 'anolisos7.9', + icon: anolisOS }, - wrapperCol: { - span: 15 + { + key: 'deepin20.9', + vm_url: 'https://mirrors.aliyun.com/deepin-cd/20.9/deepin-desktop-community-20.9-amd64.iso', + image_name: 'deepin20.9', + icon: deepinOS + }, + { + key: 'ubuntu23.10', + vm_url: 'https://mirrors.aliyun.com/ubuntu-releases/mantic/ubuntu-23.10-live-server-amd64.iso', + image_name: 'ubuntu23.10', + icon: ubuntuOS } -}; +]; @connect( ({ global, loading, user, teamControl }) => ({ currUser: user.currentUser, groups: global.groups, rainbondInfo: global.rainbondInfo, - createAppByDockerrunLoading: - loading.effects['createApp/createAppByVirtualMachine'], + createAppByVirtualMachineLoading: + loading.effects['createApp/createAppByVirtualMachine'], currentTeamPermissionsInfo: teamControl.currentTeamPermissionsInfo }), null, @@ -56,21 +74,13 @@ const formItemLayouts = { export default class Index extends PureComponent { constructor(props) { super(props); + const defaultPublicVm = PUBLIC_VM_OPTIONS[0]; this.state = { - showUsernameAndPass: false, - addGroup: false, - language: cookie.get('language') === 'zh-CN' ? true : false, radioKey: 'public', - assetCatalogVisible: false, - uploadMode: 'normal', fileList: [], - percents: false, vmShow: false, existFileList: [], - chunkUploadProgress: 0, - isChunkUploading: false, - currentFile: null, - chunkUploader: null, + showAdvanced: false, vmCapabilities: { chunk_upload_supported: false, gpu_supported: false, @@ -80,84 +90,113 @@ export default class Index extends PureComponent { usb_resources: [], networks: [] }, - PublicVm: [ - { vm_url: 'https://mirrors.aliyun.com/centos/7.9.2009/isos/x86_64/CentOS-7-x86_64-Minimal-2009.iso', image_name: 'centos7.9' }, - { vm_url: 'https://mirrors.aliyun.com/anolis/7.9/isos/GA/x86_64/AnolisOS-7.9-Minimal-x86_64-dvd.iso', image_name: 'anolisos7.9' }, - { vm_url: 'https://mirrors.aliyun.com/deepin-cd/20.9/deepin-desktop-community-20.9-amd64.iso', image_name: 'deepin20.9' }, - { vm_url: 'https://mirrors.aliyun.com/ubuntu-releases/mantic/ubuntu-23.10-live-server-amd64.iso', image_name: 'ubuntu23.10' }, - ], - selectName: 'centos7.9', - selectUrl: 'https://mirrors.aliyun.com/centos/7.9.2009/isos/x86_64/CentOS-7-x86_64-Minimal-2009.iso', + publicVmOptions: PUBLIC_VM_OPTIONS, + selectedPublicVm: defaultPublicVm, comNames: [], creatComPermission: {} }; this.appliedTemplateVersionId = null; } + componentWillMount() { this.loop = false; } + componentWillUnmount() { this.loop = false; } + componentDidMount() { this.fetchPipePipeline(); this.fetchVMCapabilities(); this.handleJarWarUpload(); - const { handleType, groupId } = this.props; - const group_id = globalUtil.getAppID() - if(group_id){ + const fixedGroupId = this.getFixedGroupId(); + if (fixedGroupId) { this.setState({ - creatComPermission: role.queryPermissionsInfo(this.props.currentTeamPermissionsInfo?.team, 'app_overview', `app_${globalUtil.getAppID() || group_id}`) - }) - } - if (handleType && handleType === 'Service') { - this.fetchComponentNames(Number(groupId)); + creatComPermission: role.queryPermissionsInfo( + this.props.currentTeamPermissionsInfo?.team, + 'app_overview', + `app_${fixedGroupId}` + ) + }); + this.fetchComponentNames(fixedGroupId); } if (this.props.templatePreset) { this.applyTemplatePreset(this.props.templatePreset); } } + componentDidUpdate(prevProps) { if ( this.props.templatePreset && (!prevProps.templatePreset || - prevProps.templatePreset.template_version_id !== this.props.templatePreset.template_version_id) + prevProps.templatePreset.template_version_id !== + this.props.templatePreset.template_version_id) ) { this.applyTemplatePreset(this.props.templatePreset); } } + + getFixedGroupId = () => { + const { handleType, groupId, data = {} } = this.props; + const currentAppId = globalUtil.getAppID(); + if (handleType === 'Service' && groupId) { + return Number(groupId); + } + if (currentAppId) { + return Number(currentAppId); + } + if (data.group_id) { + return Number(data.group_id); + } + return undefined; + }; + + getCurrentGroupName = () => { + const { groups = [] } = this.props; + const fixedGroupId = this.getFixedGroupId(); + if (!fixedGroupId) { + return ''; + } + const target = (groups || []).find( + item => Number(item.group_id) === Number(fixedGroupId) + ); + return target ? target.group_name : `${fixedGroupId}`; + }; + handleJarWarUpload = () => { - const { dispatch } = this.props - const teamName = globalUtil.getCurrTeamName() - const regionName = globalUtil.getCurrRegionName() - //获取上传事件 + const { dispatch } = this.props; dispatch({ - type: "createApp/createJarWarServices", + type: 'createApp/createJarWarServices', payload: { - region: regionName, - team_name: teamName, - component_id: '', + region: globalUtil.getCurrRegionName(), + team_name: globalUtil.getCurrTeamName(), + component_id: '' }, - callback: (res) => { + callback: res => { if (res && res.status_code === 200) { - this.setState({ - record: res.bean, - event_id: res.bean.event_id, - region_name: res.bean && res.bean.region, - team_name: res.bean && res.bean.team_name - }, () => { - if (res.bean.region !== '') { - this.loop = true; - this.handleJarWarUploadStatus(); + this.setState( + { + record: res.bean, + event_id: res.bean.event_id, + region_name: res.bean && res.bean.region, + team_name: res.bean && res.bean.team_name + }, + () => { + if (res.bean.region !== '') { + this.loop = true; + this.handleJarWarUploadStatus(); + } } - }) + ); } }, handleError: err => { handleAPIError(err); } }); - } + }; + fetchVMCapabilities = () => { const { dispatch } = this.props; dispatch({ @@ -167,54 +206,32 @@ export default class Index extends PureComponent { }, callback: res => { const capabilities = (res && res.bean) || this.state.vmCapabilities; - this.setState(prevState => { - const nextState = { - vmCapabilities: capabilities - }; - if (!capabilities.chunk_upload_supported && prevState.uploadMode === 'chunk') { - nextState.uploadMode = 'normal'; - } - if ( - capabilities.chunk_upload_supported && - prevState.uploadMode === 'normal' && - prevState.fileList.length === 0 && - prevState.existFileList.length === 0 && - !prevState.currentFile - ) { - nextState.uploadMode = 'chunk'; - } - return nextState; + this.setState({ + vmCapabilities: capabilities }); }, handleError: err => { handleAPIError(err); } }); - } - //查询上传状态 + }; + handleJarWarUploadStatus = () => { - const { - dispatch - } = this.props; - const { event_id } = this.state + const { dispatch } = this.props; + const { event_id } = this.state; dispatch({ type: 'createApp/createJarWarUploadStatus', payload: { region: globalUtil.getCurrRegionName(), team_name: globalUtil.getCurrTeamName(), - event_id: event_id + event_id }, callback: data => { - if (data) { - if (data.bean.package_name && data.bean.package_name.length > 0) { - this.setState({ - existFileList: data.bean.package_name - }); - // notification.success({ - // message: formatMessage({id:'notification.success.upload_file'}) - // }) - this.loop = false - } + if (data && data.bean.package_name && data.bean.package_name.length > 0) { + this.setState({ + existFileList: data.bean.package_name + }); + this.loop = false; } if (this.loop) { setTimeout(() => { @@ -227,78 +244,67 @@ export default class Index extends PureComponent { } }); }; - //删除上传文件 + handleJarWarUploadDelete = () => { - const { event_id } = this.state - const { dispatch, form } = this.props + const { event_id } = this.state; + const { dispatch, form } = this.props; dispatch({ - type: "createApp/deleteJarWarUploadStatus", + type: 'createApp/deleteJarWarUploadStatus', payload: { team_name: globalUtil.getCurrTeamName(), event_id }, - callback: (data) => { - if (data.bean.res == 'ok') { + callback: data => { + if (data.bean.res === 'ok') { this.setState({ existFileList: [], - fileList: [], - currentFile: null, - chunkUploader: null, - chunkUploadProgress: 0, - isChunkUploading: false + fileList: [] }); form.setFieldsValue({ packageTarFile: [] }); notification.success({ message: formatMessage({ id: 'notification.success.delete_file' }) - }) - this.handleJarWarUpload() + }); + this.handleJarWarUpload(); } }, handleError: err => { handleAPIError(err); } }); - } - onAddGroup = () => { - this.setState({ addGroup: true }); - }; - cancelAddGroup = () => { - this.setState({ addGroup: false }); }; - handleAddGroup = groupId => { - const { setFieldsValue } = this.props.form; - setFieldsValue({ group_id: groupId }); - role.refreshPermissionsInfo(groupId, false, this.callbcak) - this.cancelAddGroup(); - }; - callbcak=(val)=>{ - this.setState({ creatComPermission: val }) - } + handleSubmit = e => { e.preventDefault(); - const { event_id, radioKey, uploadMode, existFileList, isChunkUploading } = this.state + const { event_id, radioKey, existFileList, selectedPublicVm } = this.state; const { form, onSubmit, archInfo } = this.props; + const fixedGroupId = this.getFixedGroupId(); + form.validateFields((err, fieldsValue) => { if (!err && onSubmit) { - if (radioKey === 'upload' && uploadMode === 'chunk' && isChunkUploading) { - message.warning(formatMessage({ id: 'teamAdd.create.upload.waitForCompletion' })); - return; - } if (radioKey === 'upload' && existFileList.length === 0) { - message.warning(formatMessage({ id: 'teamAdd.create.upload.finishBeforeSubmit' })); + message.warning( + formatMessage({ id: 'teamAdd.create.upload.finishBeforeSubmit' }) + ); return; } - if (archInfo && archInfo.length != 2 && archInfo.length != 0) { - fieldsValue.arch = archInfo[0] + + if (archInfo && archInfo.length !== 2 && archInfo.length !== 0) { + fieldsValue.arch = archInfo[0]; + } + + if (fixedGroupId) { + fieldsValue.group_id = fixedGroupId; } - if (radioKey == 'public') { - fieldsValue.vm_url = this.state.selectUrl - fieldsValue.image_name = this.state.selectName + + if (radioKey === 'public') { + fieldsValue.vm_url = selectedPublicVm.vm_url; + fieldsValue.image_name = selectedPublicVm.image_name; fieldsValue.source_type = 'public'; - fieldsValue.asset_id = this.findAssetByName(this.state.selectName)?.id || ''; + fieldsValue.asset_id = + this.findAssetByName(selectedPublicVm.image_name)?.id || ''; fieldsValue.template_id = ''; fieldsValue.template_version_id = ''; - } else if (radioKey === 'address') { + } else if (radioKey === 'url') { fieldsValue.source_type = 'url'; fieldsValue.asset_id = ''; fieldsValue.template_id = ''; @@ -310,15 +316,28 @@ export default class Index extends PureComponent { fieldsValue.template_version_id = ''; } else { const selectedAsset = this.findAssetByName(fieldsValue.image_name); - if (selectedAsset && selectedAsset.status !== 'ready' && selectedAsset.status !== 'partial') { + if ( + selectedAsset && + selectedAsset.status !== 'ready' && + selectedAsset.status !== 'partial' + ) { message.warning(formatMessage({ id: 'Vm.assetCatalog.useDisabled' })); return; } fieldsValue.source_type = 'existing'; - fieldsValue.asset_id = selectedAsset ? selectedAsset.id : fieldsValue.asset_id || ''; - fieldsValue.template_id = selectedAsset && selectedAsset.template_id ? selectedAsset.template_id : fieldsValue.template_id || ''; - fieldsValue.template_version_id = selectedAsset && selectedAsset.template_version_id ? selectedAsset.template_version_id : fieldsValue.template_version_id || ''; + fieldsValue.asset_id = selectedAsset + ? selectedAsset.id + : fieldsValue.asset_id || ''; + fieldsValue.template_id = + selectedAsset && selectedAsset.template_id + ? selectedAsset.template_id + : fieldsValue.template_id || ''; + fieldsValue.template_version_id = + selectedAsset && selectedAsset.template_version_id + ? selectedAsset.template_version_id + : fieldsValue.template_version_id || ''; } + if (!fieldsValue.gpu_enabled) { fieldsValue.gpu_resources = []; } @@ -329,76 +348,104 @@ export default class Index extends PureComponent { fieldsValue.network_name = ''; fieldsValue.fixed_ip = ''; } - onSubmit(fieldsValue, radioKey == 'upload' ? event_id : ''); + + if (!fieldsValue.group_id) { + fieldsValue.group_name = + fieldsValue.group_name || fieldsValue.service_cname; + fieldsValue.k8s_app = + fieldsValue.k8s_app || + this.generateEnglishName(fieldsValue.group_name || fieldsValue.service_cname); + } + + onSubmit(fieldsValue, radioKey === 'upload' ? event_id : ''); } }); }; - handleValiateNameSpace = (_, value, callback) => { + + handleValidateK8sName = (_, value, callback) => { if (!value) { - return callback(new Error(formatMessage({ id: 'placeholder.k8s_component_name' }))); - } - if (value && value.length <= 32) { - const Reg = /^[a-z]([-a-z0-9]*[a-z0-9])?$/; - if (!Reg.test(value)) { - return callback( - new Error(formatMessage({ id: 'placeholder.nameSpaceReg' })) - ); - } - callback(); + return callback( + new Error(formatMessage({ id: 'placeholder.k8s_component_name' })) + ); } if (value.length > 16) { return callback(new Error(formatMessage({ id: 'placeholder.max16' }))); } + const reg = /^[a-z]([-a-z0-9]*[a-z0-9])?$/; + if (!reg.test(value)) { + return callback(new Error(formatMessage({ id: 'placeholder.nameSpaceReg' }))); + } + callback(); }; - handleChangeImageSource = (key) => { + + handleChangeImageSource = e => { + const radioKey = e.target.value; + const { form } = this.props; this.setState({ - radioKey: key.target.value + radioKey }); - if (this.props.form) { - this.props.form.setFieldsValue({ - asset_id: '', - template_id: '', - template_version_id: '' - }); - } - } - applyTemplatePreset = (preset) => { + form.setFieldsValue({ + imagefrom: radioKey, + asset_id: '', + template_id: '', + template_version_id: '' + }); + }; + + applyTemplatePreset = preset => { const { form } = this.props; if (!preset || !form || this.appliedTemplateVersionId === preset.template_version_id) { return; } - const runtimeSnapshot = preset && preset.extra && preset.extra.runtime_snapshot ? preset.extra.runtime_snapshot : {}; + const runtimeSnapshot = + preset && preset.extra && preset.extra.runtime_snapshot + ? preset.extra.runtime_snapshot + : {}; this.appliedTemplateVersionId = preset.template_version_id; - this.setState({ - radioKey: 'ok' - }); - form.setFieldsValue({ - imagefrom: 'ok', - image_name: preset.name, - asset_id: preset.id, - template_id: preset.template_id, - template_version_id: preset.template_version_id, - boot_mode: runtimeSnapshot.boot_mode || undefined, - gpu_enabled: !!runtimeSnapshot.gpu_enabled, - gpu_resources: runtimeSnapshot.gpu_resources || [], - usb_enabled: !!runtimeSnapshot.usb_enabled, - usb_resources: runtimeSnapshot.usb_resources || [], - network_mode: runtimeSnapshot.network_mode || 'random', - network_name: runtimeSnapshot.network_name || undefined, - fixed_ip: runtimeSnapshot.fixed_ip || undefined - }); + this.setState( + { + radioKey: 'existing' + }, + () => { + form.setFieldsValue({ + imagefrom: 'existing', + image_name: preset.name, + asset_id: preset.id, + template_id: preset.template_id, + template_version_id: preset.template_version_id, + boot_mode: runtimeSnapshot.boot_mode || undefined, + gpu_enabled: !!runtimeSnapshot.gpu_enabled, + gpu_resources: runtimeSnapshot.gpu_resources || [], + usb_enabled: !!runtimeSnapshot.usb_enabled, + usb_resources: runtimeSnapshot.usb_resources || [], + network_mode: runtimeSnapshot.network_mode || 'random', + network_name: runtimeSnapshot.network_name || undefined, + fixed_ip: runtimeSnapshot.fixed_ip || undefined + }); + } + ); }; + openAssetCatalog = () => { - this.setState({ assetCatalogVisible: true }); + const { onOpenAssetCatalog } = this.props; + if (onOpenAssetCatalog) { + onOpenAssetCatalog(); + } }; + closeAssetCatalog = () => { - this.setState({ assetCatalogVisible: false }); + const { onCloseAssetCatalog } = this.props; + if (onCloseAssetCatalog) { + onCloseAssetCatalog(); + } }; - findAssetByName = (name) => { + + findAssetByName = name => { const { virtualMachineImage = [] } = this.props; return (virtualMachineImage || []).find(item => item.name === name); }; - getAssetSourceLabel = (sourceType) => { + + getAssetSourceLabel = sourceType => { const sourceMap = { public: 'Vm.createVm.public', url: 'Vm.createVm.add', @@ -407,39 +454,51 @@ export default class Index extends PureComponent { clone: 'Vm.createVm.clone', vm_template: 'Vm.template.center.entry' }; - return formatMessage({ id: sourceMap[sourceType] || 'Vm.assetCatalog.sourceUnknown' }); + return formatMessage({ + id: sourceMap[sourceType] || 'Vm.assetCatalog.sourceUnknown' + }); }; - renderAssetOptionLabel = (asset) => { - return `${asset.name} / ${this.getAssetSourceLabel(asset.source_type)} / ${asset.arch || '-'} / ${asset.format || '-'} / ${asset.status || '-'}`; + + renderAssetOptionLabel = asset => { + return `${asset.name}`; }; - handleUseAsset = (asset) => { + + handleUseAsset = asset => { const { form } = this.props; if (!asset || asset.status !== 'ready') { message.warning(formatMessage({ id: 'Vm.assetCatalog.useDisabled' })); return; } - const runtimeSnapshot = asset && asset.extra && asset.extra.runtime_snapshot ? asset.extra.runtime_snapshot : {}; - this.setState({ - radioKey: 'ok', - assetCatalogVisible: false - }); - form.setFieldsValue({ - imagefrom: 'ok', - image_name: asset.name, - asset_id: asset.id, - template_id: asset.template_id || undefined, - template_version_id: asset.template_version_id || undefined, - boot_mode: runtimeSnapshot.boot_mode || undefined, - gpu_enabled: !!runtimeSnapshot.gpu_enabled, - gpu_resources: runtimeSnapshot.gpu_resources || [], - usb_enabled: !!runtimeSnapshot.usb_enabled, - usb_resources: runtimeSnapshot.usb_resources || [], - network_mode: runtimeSnapshot.network_mode || 'random', - network_name: runtimeSnapshot.network_name || undefined, - fixed_ip: runtimeSnapshot.fixed_ip || undefined - }); + const runtimeSnapshot = + asset && asset.extra && asset.extra.runtime_snapshot + ? asset.extra.runtime_snapshot + : {}; + this.setState( + { + radioKey: 'existing' + }, + () => { + form.setFieldsValue({ + imagefrom: 'existing', + image_name: asset.name, + asset_id: asset.id, + template_id: asset.template_id || undefined, + template_version_id: asset.template_version_id || undefined, + boot_mode: runtimeSnapshot.boot_mode || undefined, + gpu_enabled: !!runtimeSnapshot.gpu_enabled, + gpu_resources: runtimeSnapshot.gpu_resources || [], + usb_enabled: !!runtimeSnapshot.usb_enabled, + usb_resources: runtimeSnapshot.usb_resources || [], + network_mode: runtimeSnapshot.network_mode || 'random', + network_name: runtimeSnapshot.network_name || undefined, + fixed_ip: runtimeSnapshot.fixed_ip || undefined + }); + this.closeAssetCatalog(); + } + ); }; - handleDeleteAsset = (asset) => { + + handleDeleteAsset = asset => { const { dispatch, onRefreshAssets } = this.props; return new Promise((resolve, reject) => { dispatch({ @@ -464,13 +523,7 @@ export default class Index extends PureComponent { }); }); }; - onUploadModeChange = (e) => { - if (e.target.value === 'chunk' && !this.state.vmCapabilities.chunk_upload_supported) { - message.warning(formatMessage({ id: 'teamAdd.create.upload.chunkUnsupported' })); - return; - } - this.setState({ uploadMode: e.target.value }); - }; + validateRuntimeResources = (enabledField, messageId) => (_, value, callback) => { if (!this.props.form.getFieldValue(enabledField)) { callback(); @@ -482,124 +535,7 @@ export default class Index extends PureComponent { } callback(new Error(formatMessage({ id: messageId }))); }; - handleChunkFileSelect = (file) => { - const { event_id, record, vmCapabilities } = this.state; - if (!vmCapabilities.chunk_upload_supported) { - message.warning(formatMessage({ id: 'teamAdd.create.upload.chunkUnsupported' })); - return false; - } - if (!event_id || !record || !record.upload_url) { - message.error(formatMessage({ id: 'teamAdd.create.upload.uploaderInitFailed' })); - return false; - } - const allowedTypes = ['.img', '.qcow2', '.iso', '.tar', '.gz', '.xz']; - const fileExt = file.name.substring(file.name.lastIndexOf('.')).toLowerCase(); - if (!allowedTypes.includes(fileExt)) { - message.error(formatMessage({ id: 'Vm.createVm.package' })); - return false; - } - this.setState({ currentFile: file }); - const uploader = new ChunkUploader(file, event_id, { - uploadUrl: record.upload_url, - chunkSize: 5 * 1024 * 1024, - concurrency: 5 - }); - this.setState({ chunkUploader: uploader }); - return false; - }; - handleStartChunkUpload = async () => { - const { chunkUploader, currentFile, vmCapabilities } = this.state; - const { form } = this.props; - if (!vmCapabilities.chunk_upload_supported) { - message.warning(formatMessage({ id: 'teamAdd.create.upload.chunkUnsupported' })); - return; - } - if (!currentFile) { - message.warning(formatMessage({ id: 'teamAdd.create.upload.selectFileFirst' })); - return; - } - if (!chunkUploader) { - message.error(formatMessage({ id: 'teamAdd.create.upload.uploaderInitFailed' })); - return; - } - this.setState({ isChunkUploading: true, chunkUploadProgress: 0 }); - try { - await chunkUploader.upload((progress) => { - this.setState({ chunkUploadProgress: progress }); - }); - notification.success({ - message: formatMessage({ id: 'notification.success.upload_file' }) - }); - const virtualFileList = [{ - uid: '-1', - name: currentFile.name, - status: 'done', - response: { msg: 'success' } - }]; - this.setState({ - isChunkUploading: false, - chunkUploadProgress: 100, - existFileList: [currentFile.name], - fileList: virtualFileList - }); - form.setFieldsValue({ - packageTarFile: virtualFileList - }); - this.handleJarWarUploadStatus(); - } catch (error) { - message.error(formatMessage({ id: 'teamAdd.create.upload.uploadFailed' }) + ': ' + (error.message || 'Unknown error')); - this.setState({ isChunkUploading: false }); - } - }; - handlePauseChunkUpload = () => { - const { chunkUploader } = this.state; - if (chunkUploader) { - chunkUploader.pause(); - this.setState({ isChunkUploading: false }); - message.info(formatMessage({ id: 'teamAdd.create.upload.pauseSuccess' })); - } - }; - handleResumeChunkUpload = async () => { - const { chunkUploader } = this.state; - if (!chunkUploader) { - message.error(formatMessage({ id: 'teamAdd.create.upload.noUploadTask' })); - return; - } - this.setState({ isChunkUploading: true }); - try { - await chunkUploader.resume((progress) => { - this.setState({ chunkUploadProgress: progress }); - }); - notification.success({ - message: formatMessage({ id: 'teamAdd.create.upload.resumeSuccess' }) - }); - this.setState({ - isChunkUploading: false, - chunkUploadProgress: 100 - }); - this.handleJarWarUploadStatus(); - } catch (error) { - message.error(formatMessage({ id: 'teamAdd.create.upload.resumeFailed' }) + ': ' + (error.message || 'Unknown error')); - this.setState({ isChunkUploading: false }); - } - }; - handleCancelChunkUpload = async () => { - const { chunkUploader } = this.state; - const { form } = this.props; - if (chunkUploader) { - await chunkUploader.cancel(); - this.setState({ - isChunkUploading: false, - chunkUploadProgress: 0, - currentFile: null, - chunkUploader: null, - fileList: [] - }); - form.setFieldsValue({ packageTarFile: [] }); - message.info(formatMessage({ id: 'teamAdd.create.upload.cancelSuccess' })); - } - }; - //上传 + onChangeUpload = info => { let { fileList } = info; fileList = fileList.filter(file => { @@ -608,26 +544,14 @@ export default class Index extends PureComponent { } return true; }); - if (info && info.event && info.event.percent) { - this.setState({ - percents: info.event.percent - }); - } - - const { status } = info.file; - if (status === 'done') { - this.setState({ - percents: false - }); - } this.setState({ fileList }); }; - //删除 + onRemove = () => { this.setState({ fileList: [] }); }; - // 获取插件列表 - fetchPipePipeline = (eid) => { + + fetchPipePipeline = () => { const { dispatch, currUser } = this.props; dispatch({ type: 'teamControl/fetchPluginUrl', @@ -637,30 +561,33 @@ export default class Index extends PureComponent { }, callback: res => { if (res && res.list) { - res.list.map(item => { - if (item.name == "rainbond-vm") { + res.list.forEach(item => { + if (item.name === 'rainbond-vm') { this.setState({ - vmShow: true, - }) + vmShow: true + }); } - } - ) + }); } } - }) - } - PublicVmSelect = (item) => { + }); + }; + + handleSelectPublicVm = item => { this.setState({ - selectName: item.image_name, - selectUrl: item.vm_url - }) - } - // 获取当前选取的app的所有组件的英文名称 - fetchComponentNames = (group_id) => { + selectedPublicVm: item + }); + }; + + fetchComponentNames = group_id => { const { dispatch } = this.props; this.setState({ - creatComPermission: role.queryPermissionsInfo(this.props.currentTeamPermissionsInfo?.team, 'app_overview', `app_${group_id}`) - }) + creatComPermission: role.queryPermissionsInfo( + this.props.currentTeamPermissionsInfo?.team, + 'app_overview', + `app_${group_id}` + ) + }); dispatch({ type: 'appControl/getComponentNames', payload: { @@ -670,531 +597,613 @@ export default class Index extends PureComponent { callback: res => { if (res && res.bean) { this.setState({ - comNames: res.bean.component_names && res.bean.component_names.length > 0 ? res.bean.component_names : [] - }) + comNames: + res.bean.component_names && res.bean.component_names.length > 0 + ? res.bean.component_names + : [] + }); } } - }); + }); }; - // 生成英文名 - generateEnglishName = (name) => { - if(name != undefined){ - const { appNames } = this.props; - const pinyinName = pinyin(name, {toneType: 'none'}).replace(/\s/g, ''); - const cleanedPinyinName = pinyinName.toLowerCase(); - if (appNames && appNames.length > 0) { - const isExist = appNames.some(item => item === cleanedPinyinName); - if (isExist) { - const random = Math.floor(Math.random() * 10000); - return `${cleanedPinyinName}${random}`; - } - return cleanedPinyinName; + + generateEnglishName = name => { + if (name === undefined || name === null || name === '') { + return ''; + } + const { comNames } = this.state; + const pinyinName = pinyin(name, { toneType: 'none' }).replace(/\s/g, ''); + const cleanedPinyinName = pinyinName.toLowerCase(); + if (comNames && comNames.length > 0) { + const isExist = comNames.some(item => item === cleanedPinyinName); + if (isExist) { + const random = Math.floor(Math.random() * 10000); + return `${cleanedPinyinName}${random}`; } - return cleanedPinyinName; } - return '' - } - render() { - const { - groups, - createAppByDockerrunLoading, - form, - groupId, - handleType, - ButtonGroupState, - showSubmitBtn = true, - showCreateGroup = true, - archInfo, - virtualMachineImage, - rainbondInfo - } = this.props; + return cleanedPinyinName; + }; + + renderFileList = () => { + const { existFileList } = this.state; + if (existFileList.length === 0) { + return ( +
+ {formatMessage({ id: 'teamAdd.create.null_data' })} +
+ ); + } + return ( +
+ {existFileList.map(item => ( +
+
+ +
+
{item}
+
+ {formatMessage({ id: 'Vm.createVm.uploadSuccessHint' })} +
+
+
+ +
+ ))} +
+ ); + }; + + renderPublicVmCards = () => { + const { publicVmOptions, selectedPublicVm } = this.state; + return ( +
+ {publicVmOptions.map(item => { + const active = selectedPublicVm.image_name === item.image_name; + return ( +
this.handleSelectPublicVm(item)} + > +
+ {item.image_name} +
+
{item.image_name}
+
+ ); + })} +
+ ); + }; + + renderSourceFields = () => { + const { form, virtualMachineImage = [] } = this.props; const { getFieldDecorator } = form; - const myheaders = {}; - const data = this.props.data || {}; - const isService = handleType && handleType === 'Service'; - const host = rainbondInfo.document?.enable ? rainbondInfo.document.value.platform_url : 'https://www.rainbond.com' - const { language, radioKey, fileList, vmShow, existFileList, PublicVm, selectName, uploadMode, vmCapabilities, isChunkUploading, chunkUploadProgress, currentFile, creatComPermission: { - isCreate - } } = this.state; - const is_language = language ? formItemLayout : formItemLayouts; - let arch = 'amd64' - let archLegnth = archInfo.length - if (archLegnth == 2) { - arch = 'amd64' - } else if (archInfo.length == 1) { - arch = archInfo && archInfo[0] + const { radioKey } = this.state; + + if (radioKey === 'public') { + return this.renderPublicVmCards(); + } + + if (radioKey === 'url') { + return ( + + + {getFieldDecorator('vm_url', { + rules: [{ required: true, message: formatMessage({ id: 'Vm.createVm.InputInstall' }) }] + })( + + )} + + + {getFieldDecorator('image_name', { + rules: [{ required: true, message: formatMessage({ id: 'Vm.createVm.inputName' }) }] + })( + + )} + + + ); + } + + if (radioKey === 'upload') { + return ( + + + {getFieldDecorator('packageTarFile', { initialValue: [] })( +
+ + + +
+ )} +
+ + {this.renderFileList()} + + + {getFieldDecorator('image_name', { + rules: [{ required: true, message: formatMessage({ id: 'Vm.createVm.inputName' }) }] + })( + + )} + +
+ ); } - const group_id = globalUtil.getAppID() - const initialGroupId = isService - ? Number(groupId) - : data.group_id || (group_id ? Number(group_id) : undefined) + return ( -
- {getFieldDecorator('asset_id', { - initialValue: '' - })()} - {getFieldDecorator('template_id', { - initialValue: '' - })()} - {getFieldDecorator('template_version_id', { - initialValue: '' - })()} - {this.props.templatePreset && this.props.templatePreset.status === 'partial' && ( - - - + + {getFieldDecorator('image_name', { + rules: [{ required: true, message: formatMessage({ id: 'Vm.createVm.selectImg' }) }] + })( + )} - - {getFieldDecorator('group_id', { - initialValue: initialGroupId, - rules: [{ required: true, message: formatMessage({ id: 'placeholder.select' }) }] + + + ); + }; + + renderRuntimeFields = (archLength, arch, form) => { + const { getFieldDecorator } = form; + const { vmCapabilities } = this.state; + return ( + + {archLength === 2 ? ( + + {getFieldDecorator('arch', { + initialValue: arch, + rules: [{ required: true, message: formatMessage({ id: 'placeholder.code_version' }) }] })( - + + amd64 + arm64 + )} - - {getFieldDecorator('service_cname', { - initialValue: data.service_cname || '', + ) : null} + + {vmCapabilities.gpu_supported ? ( + +
+
+
+ {formatMessage({ id: 'Vm.createVm.gpu' })} +
+
+ {getFieldDecorator('gpu_enabled', { + valuePropName: 'checked', + initialValue: false + })()} +
+
+ ) : null} + {vmCapabilities.gpu_supported && form.getFieldValue('gpu_enabled') ? ( + + {getFieldDecorator('gpu_resources', { + initialValue: [], rules: [ - { required: true, message: formatMessage({ id: 'placeholder.service_cname' }) }, { - max: 24, - message: formatMessage({ id: 'placeholder.max24' }) + validator: this.validateRuntimeResources( + 'gpu_enabled', + 'Vm.createVm.gpuResourcesRequired' + ) } ] - })()} + })( + + )} + ) : null} - - {getFieldDecorator('k8s_component_name', { - initialValue: this.generateEnglishName(form.getFieldValue('service_cname')), + {vmCapabilities.usb_supported ? ( + +
+
+
+ {formatMessage({ id: 'Vm.createVm.usb' })} +
+
+ {getFieldDecorator('usb_enabled', { + valuePropName: 'checked', + initialValue: false + })()} +
+
+ ) : null} + {vmCapabilities.usb_supported && form.getFieldValue('usb_enabled') ? ( + + {getFieldDecorator('usb_resources', { + initialValue: [], rules: [ - { required: true, validator: this.handleValiateNameSpace } + { + validator: this.validateRuntimeResources( + 'usb_enabled', + 'Vm.createVm.usbResourcesRequired' + ) + } ] - })()} - - - {getFieldDecorator('imagefrom', { - initialValue: 'public', - rules: [{ required: true, message: formatMessage({ id: 'placeholder.code_version' }) }] })( -
- - {formatMessage({ id: 'Vm.createVm.public' })} - {formatMessage({ id: 'Vm.createVm.add' })} - {formatMessage({ id: 'Vm.createVm.upload' })} - {virtualMachineImage && virtualMachineImage.length > 0 && {formatMessage({ id: 'Vm.createVm.have' })}} - - {virtualMachineImage && virtualMachineImage.length > 0 && ( -
- - -
- )} -
+ )}
- {radioKey != 'ok' ? ( - <> - {radioKey == 'public' && - - {getFieldDecorator('url', { - })( -
- {PublicVm && PublicVm.map((item, index) => { - return ( -
this.PublicVmSelect(item)}> -
-
- {item.image_name} -
- {item.image_name == 'centos7.9' && } - {item.image_name == 'anolisos7.9' && } - {item.image_name == 'deepin20.9' && } - {item.image_name == 'ubuntu23.10' && } -
-
- ) - }) - } -
- )} -
+ ) : null} - } - {radioKey == 'address' && - - {getFieldDecorator('vm_url', { - rules: [ - { required: true } - ] - })()} - - } - {radioKey == 'upload' && - <> - - - {formatMessage({ id: 'teamAdd.create.upload.mode.normal' })} - - {formatMessage({ id: 'teamAdd.create.upload.mode.chunk' })} - - - - - {getFieldDecorator('packageTarFile', { - rules: [ - ] - })( - uploadMode === 'normal' ? ( - - - - ) : ( - - - - - {currentFile && ( -
-
- - {currentFile.name} - - ({(currentFile.size / 1024 / 1024).toFixed(2)} MB) - -
-
- -
-
- {!isChunkUploading && chunkUploadProgress === 0 && ( - - )} - {isChunkUploading && ( - - )} - {!isChunkUploading && chunkUploadProgress > 0 && chunkUploadProgress < 100 && ( - - )} - -
-
- )} -
- ) - )} -
- -
-
- {existFileList.length > 0 ? - (existFileList.map((item) => { - return ( -
- - - {item} - -
- ) - })) : ( -
- {formatMessage({ id: 'teamAdd.create.null_data' })} -
- )} -
- {existFileList.length > 0 && -
- -
- } -
-
- - } - {radioKey != 'public' && - - {getFieldDecorator('image_name', { - rules: [ - { required: true } - ] - })()} - - } - - ) : ( - - {getFieldDecorator('image_name', { - rules: [ - { required: true, } - ] - })()} - + {formatMessage({ id: 'Vm.createVm.networkFixed' })} + + )} - {archLegnth == 2 && - - {getFieldDecorator('arch', { - initialValue: arch, - rules: [{ required: true, message: formatMessage({ id: 'placeholder.code_version' }) }] - })( - - amd64 - arm64 - - )} - } +
- {vmCapabilities.gpu_supported && ( - - {getFieldDecorator('gpu_enabled', { - valuePropName: 'checked', - initialValue: false - })( - - )} - - )} - {vmCapabilities.gpu_supported && form.getFieldValue('gpu_enabled') && ( - - {getFieldDecorator('gpu_resources', { - initialValue: [], + {form.getFieldValue('network_mode') === 'fixed' ? ( + + + {getFieldDecorator('network_name', { rules: [ { - validator: this.validateRuntimeResources('gpu_enabled', 'Vm.createVm.gpuResourcesRequired') + required: true, + message: formatMessage({ id: 'Vm.createVm.networkNamePlaceholder' }) } ] })( )} - )} - - {vmCapabilities.usb_supported && ( - - {getFieldDecorator('usb_enabled', { - valuePropName: 'checked', - initialValue: false - })( - - )} - - )} - {vmCapabilities.usb_supported && form.getFieldValue('usb_enabled') && ( - - {getFieldDecorator('usb_resources', { - initialValue: [], + + {getFieldDecorator('fixed_ip', { rules: [ { - validator: this.validateRuntimeResources('usb_enabled', 'Vm.createVm.usbResourcesRequired') + required: true, + message: formatMessage({ id: 'Vm.createVm.fixedIPPlaceholder' }) } ] })( - + )} - )} + + ) : null} +
+ ); + }; - - {getFieldDecorator('network_mode', { - initialValue: 'random' - })( - - {formatMessage({ id: 'Vm.createVm.networkRandom' })} - - {formatMessage({ id: 'Vm.createVm.networkFixed' })} - - - )} - + renderSubmitButton = fixedGroupId => { + const { + handleType, + ButtonGroupState, + rainbondInfo, + createAppByVirtualMachineLoading + } = this.props; + const { + vmShow, + creatComPermission: { isCreate } + } = this.state; + const host = rainbondInfo.document?.enable + ? rainbondInfo.document.value.platform_url + : 'https://www.rainbond.com'; - {form.getFieldValue('network_mode') === 'fixed' && ( + if (handleType && ButtonGroupState) { + return this.props.handleServiceBotton( + + + , + false + ); + } + + return ( + - - {getFieldDecorator('network_name', { - rules: [ - { required: true, message: formatMessage({ id: 'Vm.createVm.networkNamePlaceholder' }) } - ] - })( - - )} - - - {getFieldDecorator('fixed_ip', { - rules: [ - { required: true, message: formatMessage({ id: 'Vm.createVm.fixedIPPlaceholder' }) } - ] - })( - - )} - + {formatMessage({ id: 'Vm.createVm.unInstall' })} + + {formatMessage({ id: 'Vm.createVm.doc' })} + - )} + ) + } + key={`${vmShow}-${fixedGroupId || 'new'}`} + > + + + ); + }; - {showSubmitBtn ? ( + render() { + const { + form, + showSubmitBtn = true, + archInfo = [], + virtualMachineImage = [], + showAssetCatalog = false + } = this.props; + const { getFieldDecorator } = form; + const { + radioKey, + showAdvanced + } = this.state; + const fixedGroupId = this.getFixedGroupId(); + let arch = 'amd64'; + const archLength = archInfo.length; + if (archLength === 1) { + arch = archInfo[0]; + } + + return ( + +
+
+ + {getFieldDecorator('group_id', { + initialValue: fixedGroupId + })()} + {getFieldDecorator('asset_id', { + initialValue: '' + })()} + {getFieldDecorator('template_id', { + initialValue: '' + })()} + {getFieldDecorator('template_version_id', { + initialValue: '' + })()} + + {this.props.templatePreset && + this.props.templatePreset.status === 'partial' ? ( + + ) : null} - {isService && ButtonGroupState - ? this.props.handleServiceBotton( - - - - , false - ) - : !handleType && ( - {formatMessage({ id: 'Vm.createVm.unInstall' })}{formatMessage({id:'Vm.createVm.doc'})}} key={vmShow}> + label={ +
+ + {formatMessage({ id: 'teamAdd.create.form.service_cname' })} + + {virtualMachineImage && virtualMachineImage.length > 0 ? ( - - )} + ) : null} +
+ } + > + {getFieldDecorator('service_cname', { + initialValue: '', + rules: [ + { + required: true, + message: formatMessage({ id: 'placeholder.service_cname' }) + }, + { + max: 24, + message: formatMessage({ id: 'placeholder.max24' }) + } + ] + })()} +
+ + {getFieldDecorator('k8s_component_name', { + initialValue: this.generateEnglishName( + form.getFieldValue('service_cname') + ), + rules: [{ required: true, validator: this.handleValidateK8sName }] + })()} + + + + {getFieldDecorator('imagefrom', { + initialValue: radioKey, + rules: [ + { + required: true, + message: formatMessage({ id: 'placeholder.code_version' }) + } + ] + })( + + + {formatMessage({ id: 'Vm.createVm.public' })} + + + {formatMessage({ id: 'Vm.createVm.add' })} + + + {formatMessage({ id: 'Vm.createVm.upload' })} + + {virtualMachineImage && virtualMachineImage.length > 0 ? ( + + {formatMessage({ id: 'Vm.createVm.have' })} + + ) : null} + + )} + {this.renderSourceFields()} + + {this.renderRuntimeFields(archLength, arch, form)} + + {!fixedGroupId ? ( +
+ +
+ ) : null} + + {!fixedGroupId && showAdvanced ? ( +
+
+ + {getFieldDecorator('group_name', { + initialValue: form.getFieldValue('service_cname') || '', + rules: [ + { + required: true, + message: formatMessage({ id: 'popover.newApp.appName.placeholder' }) + }, + { + max: 24, + message: formatMessage({ id: 'placeholder.max24' }) + } + ] + })( + + )} + + + {getFieldDecorator('k8s_app', { + initialValue: this.generateEnglishName( + form.getFieldValue('group_name') || + form.getFieldValue('service_cname') || + '' + ), + rules: [{ required: true, validator: this.handleValidateK8sName }] + })()} + +
+ ) : null} + + {showSubmitBtn ? ( + + {this.renderSubmitButton(fixedGroupId)} + + ) : null} + +
+ + {showAssetCatalog ? ( + ) : null} - - {this.state.addGroup && ( - - )} - +
); } diff --git a/src/components/ImageVirtualMachineForm/index.less b/src/components/ImageVirtualMachineForm/index.less index 824782c77..be0567d0e 100644 --- a/src/components/ImageVirtualMachineForm/index.less +++ b/src/components/ImageVirtualMachineForm/index.less @@ -1,73 +1,241 @@ -.yaml_container { - :global { - .ant-upload { - width: 200px; - } - .ant-form-item-children{ - display: flex; - } - .ant-upload-list-item-info{ - margin-right: 12px; - } +@import '~antd/lib/style/themes/default.less'; + +.vmForm { + padding: 4px 2px 12px; + + :global { + .ant-form-item { + margin-bottom: 20px; } - .update{ - width: 300px; + + .ant-form-item-label>label { + font-size: 14px; + font-weight: 600; + color: @text-color; + } + + .ant-radio-group { display: flex; - max-height: 200px; - overflow: hidden; - padding: 16px 24px; - border: 1px solid #e3e3e3; - position: relative; - .delete{ - position: absolute; - top: -5%; - right: 2%; - } - .fileName{ - width: 250px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - line-height: 24px; - } + flex-wrap: wrap; + gap: 12px; + } + + .ant-select, + .ant-input { + width: 100%; + } + + .ant-upload { + display: inline-block; } + } } -.file{ - width: 250px; - white-space: nowrap; + +.noticeAlert { + margin-bottom: 20px; +} + +.fieldLabelRow { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + width: 100%; +} + +.fieldLabelText { + color: @text-color; + font-size: 14px; + font-weight: 600; +} + +.assetCatalogTrigger { + padding-right: 0 !important; +} + +.inlineGrid { + display: grid; + grid-template-columns: 1fr; +} + +.publicVmGrid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; +} + +.publicVmCard { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 8px; + padding: 12px 0; + border-radius: 12px; + cursor: pointer; + transition: all 0.2s ease; + border: 1px solid @border-color-base; + margin-bottom: 24px; + + &:hover { + transform: translateY(-1px); + } +} + +.publicVmCardActive { + color: @primary-color; +} + +.publicVmCardIconWrap { + display: flex; + align-items: center; + justify-content: center; + width: 44px; + height: 44px; + background: #f6f8fb; + border-radius: 12px; +} + +.publicVmCardIcon { + width: 28px; + height: 28px; + object-fit: contain; +} + +.publicVmCardName { + color: @heading-color; + font-size: 14px; + font-weight: 600; + line-height: 22px; + text-align: center; +} + +.uploadPanel { + padding: 6px 0 0; + background: transparent; +} + +.fileIcon { + color: @primary-color; +} + +.fileList { + display: flex; + flex-direction: column; + gap: 10px; +} + +.fileCard { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 8px 0; +} + +.fileMain { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; + flex: 1; +} + +.fileMeta { + min-width: 0; + flex: 1; +} + +.fileName { overflow: hidden; + color: @heading-color; + font-size: 13px; + font-weight: 500; + line-height: 20px; text-overflow: ellipsis; - padding: 0 24px; - border: 1px solid #d9d9d9; + white-space: nowrap; +} + +.fileHint { + margin-top: 2px; + color: @text-color-secondary; + font-size: 12px; + line-height: 18px; +} + +.fileDelete { + padding: 0 !important; +} + +.emptyState { + padding: 12px 0; + color: @text-color-secondary; + font-size: 12px; + line-height: 20px; + text-align: left; } -.public{ + +.switchPanel { display: flex; - margin-bottom: 24px; - &>div{ - cursor: pointer; - margin: 0 4px; - width: 100px; - height: 30px; - .publicItemName{ + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 4px 0; +} + +.switchPanelMeta { + flex: 1; +} + +.switchPanelTitle { + color: @heading-color; + font-size: 13px; + font-weight: 600; + line-height: 20px; +} + +.switchPanelDesc { + margin-top: 4px; + color: @text-color-secondary; + font-size: 12px; + line-height: 18px; +} + +.advancedToggle { + margin-top: 8px; + margin-bottom: 12px; + + :global { + .ant-btn { + padding-left: 0; + color: @primary-color; font-weight: 500; - line-height: 30px; - font-size: 12px; - text-align: center; - color: #000; - border: 1px solid #e3e3e3; - background-color: #fff; - border-radius: 5px; - padding: 4px; - img{ - width: 30px; - height: 30px; - } } } - .active{ - .publicItemName{ - color: #1890ff; - border: 1px solid #1890ff; - } +} + +.advancedPanel { + margin-bottom: 20px; + padding: 4px 2px 0; +} + +.advancedDivider { + margin-bottom: 18px; + border-top: 1px solid fade(@border-color-base, 92%); +} + +.submitRow { + margin-bottom: 0 !important; + text-align: center; +} + +@media (max-width: 640px) { + .inlineGrid { + grid-template-columns: 1fr; + } + + .fileCard { + align-items: flex-start; + flex-direction: column; } } \ No newline at end of file diff --git a/src/components/VMAssetCatalogModal/index.js b/src/components/VMAssetCatalogModal/index.js index 6894c5719..f4169a50e 100644 --- a/src/components/VMAssetCatalogModal/index.js +++ b/src/components/VMAssetCatalogModal/index.js @@ -1,4 +1,4 @@ -import { Empty, Input, Modal, Popconfirm, Table, Tag, Tooltip } from 'antd'; +import { Empty, Input, Popconfirm, Tooltip, Button, List, Tag } from 'antd'; import React, { Fragment, PureComponent } from 'react'; import { formatMessage } from '@/utils/intl'; import styles from './index.less'; @@ -8,12 +8,11 @@ class VMAssetCatalogModal extends PureComponent { super(props); this.state = { keyword: '', - detailAsset: null, deleteLoadingId: null }; } - getSourceLabel = (sourceType) => { + getSourceLabel = sourceType => { const sourceMap = { public: 'Vm.createVm.public', url: 'Vm.createVm.add', @@ -25,22 +24,7 @@ class VMAssetCatalogModal extends PureComponent { return formatMessage({ id: sourceMap[sourceType] || 'Vm.assetCatalog.sourceUnknown' }); }; - formatBytes = (size) => { - const value = Number(size || 0); - if (!value) { - return '-'; - } - const units = ['B', 'KB', 'MB', 'GB', 'TB']; - let current = value; - let unitIndex = 0; - while (current >= 1024 && unitIndex < units.length - 1) { - current /= 1024; - unitIndex += 1; - } - return `${current.toFixed(current >= 10 || unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`; - }; - - handleDelete = async (asset) => { + handleDelete = async asset => { const { onDelete } = this.props; if (!onDelete) { return; @@ -67,196 +51,123 @@ class VMAssetCatalogModal extends PureComponent { }); }; - renderDetailLine = (label, value) => ( -
-
{label}
-
{value === 0 ? '0' : value || '-'}
-
- ); - render() { - const { visible, onCancel, onUseAsset, assets = [] } = this.props; - const { keyword, detailAsset, deleteLoadingId } = this.state; + const { assets = [], onUseAsset } = this.props; + const { keyword, deleteLoadingId } = this.state; const filteredAssets = this.getFilteredAssets(); - const columns = [ - { - title: formatMessage({ id: 'Vm.assetCatalog.name' }), - dataIndex: 'name', - key: 'name', - width: 160, - render: value => ( - - {value} - - ) - }, - { - title: formatMessage({ id: 'Vm.assetCatalog.source' }), - dataIndex: 'source_type', - key: 'source_type', - width: 110, - render: value => {this.getSourceLabel(value)} - }, - { - title: formatMessage({ id: 'Vm.assetCatalog.archFormat' }), - key: 'arch_format', - width: 130, - render: (_, record) => ( - - {`${record.arch || '-'} / ${record.format || '-'}`} - - ) - }, - { - title: formatMessage({ id: 'Vm.assetCatalog.size' }), - dataIndex: 'size_bytes', - key: 'size_bytes', - width: 100, - render: value => this.formatBytes(value) - }, - { - title: formatMessage({ id: 'Vm.assetCatalog.status' }), - dataIndex: 'status', - key: 'status', - width: 110, - render: value => {value || formatMessage({ id: 'Vm.assetCatalog.statusUnknown' })} - }, - { - title: formatMessage({ id: 'Vm.assetCatalog.diskCount' }), - dataIndex: 'disk_count', - key: 'disk_count', - width: 90, - render: value => value || '-' - }, - { - title: formatMessage({ id: 'Vm.assetCatalog.references' }), - dataIndex: 'reference_count', - key: 'reference_count', - width: 90, - }, - { - title: formatMessage({ id: 'Vm.assetCatalog.createdAt' }), - dataIndex: 'create_time', - key: 'create_time', - width: 160, - render: value => {value || '-'} - }, - { - title: formatMessage({ id: 'Vm.assetCatalog.actions' }), - key: 'actions', - width: 220, - render: (_, record) => ( + const renderAssetItem = asset => { + const canUse = asset.status === 'ready'; + const metaItems = [ + { + label: formatMessage({ id: 'Vm.assetCatalog.source' }), + value: this.getSourceLabel(asset.source_type) + }, + { + label: formatMessage({ id: 'Vm.assetCatalog.status' }), + value: asset.status || formatMessage({ id: 'Vm.assetCatalog.statusUnknown' }) + }, + { + label: formatMessage({ id: 'Vm.assetCatalog.references' }), + value: asset.reference_count || 0 + }, + { + label: formatMessage({ id: 'Vm.assetCatalog.createdAt' }), + value: asset.create_time || '-' + } + ]; + + return ( + +
+
+ + {asset.name || '-'} + +
+
+ {metaItems.map(item => ( +
+
{item.label}:
+
+ {item.label === formatMessage({ id: 'Vm.assetCatalog.status' }) ? ( + + {item.value} + + ) : ( + item.value + )} +
+
+ ))} +
+
- - { - if (record.status !== 'ready') { - e.preventDefault(); - return; - } - onUseAsset && onUseAsset(record); - }} + + - this.setState({ detailAsset: record })}> - {formatMessage({ id: 'Vm.assetCatalog.detail' })} - this.handleDelete(record)} + onConfirm={() => this.handleDelete(asset)} okText={formatMessage({ id: 'button.confirm' })} cancelText={formatMessage({ id: 'button.cancel' })} - disabled={record.reference_count > 0} + disabled={asset.reference_count > 0} > 0 ? formatMessage({ id: 'Vm.assetCatalog.deleteDisabled' }) : ''} + title={ + asset.reference_count > 0 + ? formatMessage({ id: 'Vm.assetCatalog.deleteDisabled' }) + : '' + } > - 0 ? '#bfbfbf' : undefined }} - onClick={e => { - if (record.reference_count > 0 || deleteLoadingId === record.id) { - e.preventDefault(); - } - }} +
- ) - } - ]; - const tableScrollX = columns.reduce((total, column) => total + (Number(column.width) || 0), 0); +
+ ); + }; return ( - +
this.setState({ keyword: e.target.value })} />
-
- + + emptyText: ( +
+ +
+ ) }} /> - - - this.setState({ detailAsset: null })} - destroyOnClose - > - {detailAsset && ( -
- {this.renderDetailLine(formatMessage({ id: 'Vm.assetCatalog.name' }), detailAsset.name)} - {this.renderDetailLine(formatMessage({ id: 'Vm.assetCatalog.diskCount' }), detailAsset.disk_count)} - {this.renderDetailLine(formatMessage({ id: 'Vm.assetCatalog.source' }), - this.getSourceLabel(detailAsset.source_type))} - {this.renderDetailLine(formatMessage({ id: 'Vm.assetCatalog.sourceUri' }), detailAsset.source_uri)} - {this.renderDetailLine(formatMessage({ id: 'Vm.assetCatalog.status' }), detailAsset.status)} - {this.renderDetailLine(formatMessage({ id: 'Vm.assetCatalog.archFormat' }), - `${detailAsset.arch || '-'} / ${detailAsset.format || '-'}`)} - {this.renderDetailLine(formatMessage({ id: 'Vm.assetCatalog.size' }), - this.formatBytes(detailAsset.size_bytes))} - {this.renderDetailLine(formatMessage({ id: 'Vm.assetCatalog.bootMode' }), detailAsset.boot_mode)} - {this.renderDetailLine(formatMessage({ id: 'Vm.assetCatalog.references' }), detailAsset.reference_count)} - {detailAsset.source_asset && this.renderDetailLine( - formatMessage({ id: 'Vm.assetCatalog.sourceAsset' }), - detailAsset.source_asset.name - )} -
- )} -
+ ); } diff --git a/src/components/VMAssetCatalogModal/index.less b/src/components/VMAssetCatalogModal/index.less index 433f53aeb..25d23f8b9 100644 --- a/src/components/VMAssetCatalogModal/index.less +++ b/src/components/VMAssetCatalogModal/index.less @@ -1,68 +1,142 @@ @import '~antd/lib/style/themes/default.less'; -.assetCatalogModal { - :global { - .ant-modal { - max-width: calc(100vw - 32px); - } - - .ant-modal-body { - overflow-x: hidden; - } - } +.catalogSurface { + padding: 4px 0 0; } .toolbar { display: flex; - justify-content: space-between; - margin-bottom: 16px; + justify-content: flex-end; + margin-bottom: 24px; } -.tableWrap { - width: 100%; - min-width: 0; - overflow-x: auto; +.searchInput { + width: 300px; +} + +.listWrap { + max-height: 520px; + overflow-y: auto; :global { - .ant-spin-nested-loading, - .ant-spin-container, - .ant-table, - .ant-table-content, - .ant-table-scroll, - .ant-table-body { - min-width: 0; + .ant-list-items { + display: flex; + flex-direction: column; + gap: 12px; + padding-right: 4px; } + } +} - .ant-table-body { - overflow-x: auto !important; - } +.assetItem { + display: flex !important; + align-items: stretch; + justify-content: space-between; + gap: 16px; + padding: 14px 16px !important; + background: #fff; + border: 1px solid fade(@border-color-base, 92%); + border-radius: 12px; + transition: border-color 0.2s ease; + border-bottom: 1px solid #e8e8e8 !important; + + &:hover { + border-color: fade(@primary-color, 24%); } } +.assetMain { + display: flex; + flex-direction: column; + gap: 10px; + min-width: 0; + flex: 1; +} + +.assetNameWrap { + min-width: 0; +} + .nameText { display: inline-block; max-width: 100%; overflow: hidden; + color: @heading-color; + font-size: 14px; + font-weight: 600; + line-height: 20px; text-overflow: ellipsis; white-space: nowrap; - font-weight: 500; } -.textWrap { - display: inline-block; - max-width: 100%; - white-space: normal; - word-break: break-word; - line-height: 20px; +.assetMetaGrid { + display: flex; + flex-wrap: wrap; + gap: 6px 12px; +} + +.assetMetaItem { + min-width: 0; + display: flex; + align-items: center; + gap: 4px; +} + +.assetMetaLabel { + color: @text-color-secondary; + font-size: 12px; + line-height: 18px; + white-space: nowrap; +} + +.assetMetaValue { + overflow: hidden; + color: @heading-color; + font-size: 12px; + font-weight: 500; + line-height: 18px; + text-overflow: ellipsis; + white-space: nowrap; } .actionCell { display: flex; - flex-wrap: wrap; - gap: 8px 12px; - white-space: normal; + flex-direction: column; + gap: 6px; + justify-content: flex-end; + align-self: flex-end; + + > * { + width: 100%; + } +} + +.actionButton { + display: block; + width: 100%; + min-width: 100%; + max-width: 100%; +} + +.emptyWrap { + padding: 48px 0; } -.actionLink { - margin-left: 0 !important; +@media (max-width: 768px) { + .searchInput { + width: 100%; + } + + .assetItem { + flex-direction: column; + } + + .assetMetaGrid { + grid-template-columns: 1fr; + } + + .actionCell { + flex-direction: row; + width: 100%; + } } diff --git a/src/locales/en-US/enterprise.js b/src/locales/en-US/enterprise.js index 2c995abf0..54ec3f7bb 100644 --- a/src/locales/en-US/enterprise.js +++ b/src/locales/en-US/enterprise.js @@ -141,7 +141,7 @@ const enterpriseOverview = { 'enterpriseOverview.overview.InstallStep.demo': 'Demo', 'enterpriseOverview.overview.InstallStep.start': 'Start now', // Version update popup window - 'enterpriseOverview.overview.UpdateVersion.title': 'There are new version updates', + 'enterpriseOverview.overview.UpdateVersion.title': 'New version update', 'enterpriseOverview.overview.UpdateVersion.tip': 'Want to update a new version?', } @@ -1571,4 +1571,4 @@ const monitarEnterprise = { 'monitarEnterprise.error.desc.scan': 'The installed patrol plug-in was not detected', } -export default Object.assign({}, enterpriseOverview, applicationMarket, enterpriseTeamManagement, enterpriseColony, enterpriseUser, enterpriseSetting, otherEnterprise, LogEnterprise, extensionEnterprise, platformUpgrade, auditEnterprise, containerLog, monitarEnterprise); \ No newline at end of file +export default Object.assign({}, enterpriseOverview, applicationMarket, enterpriseTeamManagement, enterpriseColony, enterpriseUser, enterpriseSetting, otherEnterprise, LogEnterprise, extensionEnterprise, platformUpgrade, auditEnterprise, containerLog, monitarEnterprise); diff --git a/src/locales/en-US/team.js b/src/locales/en-US/team.js index ab8925739..48392fed9 100644 --- a/src/locales/en-US/team.js +++ b/src/locales/en-US/team.js @@ -723,6 +723,19 @@ const Vm = { 'Vm.createVm.docker': 'Build from container', 'Vm.createVm.creatCom': 'Create components from a virtual machine', 'Vm.createVm.creatApp': 'Create an application using a VM image', + 'Vm.createVm.createInApp': 'Create a VM component inside the current app', + 'Vm.createVm.modalDesc': 'Support public images, direct download links, file uploads, and existing assets in one unified VM creation flow.', + 'Vm.createVm.currentApp': 'Current app: {name}', + 'Vm.createVm.basicInfo': 'Basic Information', + 'Vm.createVm.basicInfoDesc': 'Define the component identity first, then continue with runtime and network settings.', + 'Vm.createVm.sourceDesc': 'Choose the VM source and the form will reveal the fields required for that source type.', + 'Vm.createVm.publicTemplate': 'Official public template', + 'Vm.createVm.assetCount': '{count} assets are available. Open the asset catalog to inspect details and status.', + 'Vm.createVm.runtimeConfig': 'Runtime Configuration', + 'Vm.createVm.runtimeConfigDesc': 'Choose arch, GPU, USB, and network settings based on current cluster capabilities.', + 'Vm.createVm.gpuDesc': 'Enable GPU acceleration for the virtual machine and select the GPU resources below.', + 'Vm.createVm.usbDesc': 'Enable USB passthrough for the virtual machine and bind the USB resources below.', + 'Vm.createVm.uploadSuccessHint': 'This uploaded file will be used as the image source for the current creation flow.', 'Vm.createVm.specification': 'Specification', 'Vm.createVm.distribution': 'Specifies the amount of memory allocated to this virtual machine. The memory size must be a multiple of 4 MB.', 'Vm.createVm.memory': 'Internal memory', @@ -777,6 +790,7 @@ const Vm = { 'Vm.createVm.cloneSourcePlaceholder': 'Please select an existing image to clone', 'Vm.assetCatalog.manage': 'Asset Catalog', 'Vm.assetCatalog.title': 'Image Asset Catalog', + 'Vm.assetCatalog.total': 'Showing {count} items out of {total} assets', 'Vm.assetCatalog.name': 'Asset Name', 'Vm.assetCatalog.source': 'Source', 'Vm.assetCatalog.sourceUnknown': 'Unknown', @@ -787,14 +801,14 @@ const Vm = { 'Vm.assetCatalog.references': 'References', 'Vm.assetCatalog.createdAt': 'Created At', 'Vm.assetCatalog.actions': 'Actions', - 'Vm.assetCatalog.useAsset': 'Create Component', + 'Vm.assetCatalog.useAsset': 'Select', 'Vm.assetCatalog.clone': 'Quick Copy', 'Vm.assetCatalog.cloneSuccess': 'Image asset copied successfully', 'Vm.assetCatalog.delete': 'Delete', 'Vm.assetCatalog.deleteConfirm': 'Delete this image asset?', 'Vm.assetCatalog.deleteDisabled': 'This image asset is still referenced by virtual machines', 'Vm.assetCatalog.useDisabled': 'This image asset is not ready yet and cannot be used to create a virtual machine', - 'Vm.assetCatalog.detail': 'Details', + 'Vm.assetCatalog.detail': 'Detail', 'Vm.assetCatalog.detailTitle': 'Image Asset Detail', 'Vm.assetCatalog.searchPlaceholder': 'Search by name, source or status', 'Vm.assetCatalog.empty': 'No image assets', diff --git a/src/locales/zh-CN/enterprise.js b/src/locales/zh-CN/enterprise.js index 2cbce6606..e1237c6f8 100644 --- a/src/locales/zh-CN/enterprise.js +++ b/src/locales/zh-CN/enterprise.js @@ -142,7 +142,7 @@ const enterpriseOverview = { 'enterpriseOverview.overview.InstallStep.demo': '查看演示示例', 'enterpriseOverview.overview.InstallStep.start': '马上开始', // 版本更新弹窗 - 'enterpriseOverview.overview.UpdateVersion.title': '有新版本更新', + 'enterpriseOverview.overview.UpdateVersion.title': '新版本更新', 'enterpriseOverview.overview.UpdateVersion.tip': '要去更新新版本吗?', } diff --git a/src/locales/zh-CN/team.js b/src/locales/zh-CN/team.js index 29b1371f7..b5136ab28 100644 --- a/src/locales/zh-CN/team.js +++ b/src/locales/zh-CN/team.js @@ -733,6 +733,19 @@ const Vm = { 'Vm.createVm.docker': '从容器构建', 'Vm.createVm.creatCom': '从虚拟机创建组件', 'Vm.createVm.creatApp': '通过虚拟机镜像创建应用。', + 'Vm.createVm.createInApp': '在当前应用内创建虚拟机组件', + 'Vm.createVm.modalDesc': '统一支持公共镜像、链接下载、文件上传和已有资产四种来源,并保持与镜像容器一致的创建体验。', + 'Vm.createVm.currentApp': '当前应用:{name}', + 'Vm.createVm.basicInfo': '基础信息', + 'Vm.createVm.basicInfoDesc': '先确定组件名称,创建后可继续补充运行配置和连接方式。', + 'Vm.createVm.sourceDesc': '选择虚拟机来源后,平台会按来源类型自动组织所需字段。', + 'Vm.createVm.publicTemplate': '官方公共模板', + 'Vm.createVm.assetCount': '共 {count} 个可用资产,可通过资产目录查看详情与状态。', + 'Vm.createVm.runtimeConfig': '运行配置', + 'Vm.createVm.runtimeConfigDesc': '按集群能力选择架构、GPU、USB 与网络参数,不需要的能力可以保持关闭。', + 'Vm.createVm.gpuDesc': '为虚拟机开启 GPU 加速,并在下方选择可用的 GPU 资源。', + 'Vm.createVm.usbDesc': '为虚拟机开启 USB 透传,并绑定可用的 USB 资源。', + 'Vm.createVm.uploadSuccessHint': '上传完成后将作为当前创建使用的镜像文件。', 'Vm.createVm.specification': '规格', 'Vm.createVm.distribution': '指定分配给此虚拟机的内存量。内存大小必须为 4 MB的倍数。', 'Vm.createVm.memory': '内存', @@ -787,6 +800,7 @@ const Vm = { 'Vm.createVm.cloneSourcePlaceholder': '请选择要复制的已有镜像', 'Vm.assetCatalog.manage': '镜像资产目录', 'Vm.assetCatalog.title': '镜像资产目录', + 'Vm.assetCatalog.total': '当前显示 {count} 项,共 {total} 项资产', 'Vm.assetCatalog.name': '资产名称', 'Vm.assetCatalog.source': '来源', 'Vm.assetCatalog.sourceUnknown': '未知来源', @@ -797,14 +811,14 @@ const Vm = { 'Vm.assetCatalog.references': '引用数', 'Vm.assetCatalog.createdAt': '创建时间', 'Vm.assetCatalog.actions': '操作', - 'Vm.assetCatalog.useAsset': '创建组件', + 'Vm.assetCatalog.useAsset': '选择', 'Vm.assetCatalog.clone': '快速复制', 'Vm.assetCatalog.cloneSuccess': '镜像资产复制成功', 'Vm.assetCatalog.delete': '删除', 'Vm.assetCatalog.deleteConfirm': '确认删除该镜像资产吗?', 'Vm.assetCatalog.deleteDisabled': '该镜像资产仍被虚拟机引用,暂不支持删除', 'Vm.assetCatalog.useDisabled': '镜像资产尚未就绪,暂时不能用于创建虚拟机', - 'Vm.assetCatalog.detail': '查看详情', + 'Vm.assetCatalog.detail': '详情', 'Vm.assetCatalog.detailTitle': '镜像资产详情', 'Vm.assetCatalog.searchPlaceholder': '按名称、来源或状态搜索', 'Vm.assetCatalog.empty': '暂无镜像资产',