diff --git a/src/components/FormItem/index.jsx b/src/components/FormItem/index.jsx index 160e6d9f..5421551c 100644 --- a/src/components/FormItem/index.jsx +++ b/src/components/FormItem/index.jsx @@ -258,7 +258,8 @@ export default class FormItem extends React.Component { } getSelectTableValidator = (rule, value) => { - if (!value || value.selectedRowKeys.length === 0) { + const { selectedRowKeys = [] } = value || {}; + if (selectedRowKeys.length === 0) { return Promise.reject( new Error(rule.placeholder || `${t('Please select')}${rule.label}!`) ); diff --git a/src/locales/en.json b/src/locales/en.json index e6478e61..e290f648 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1254,6 +1254,7 @@ "Neutron Agent Detail": "Neutron Agent Detail", "Neutron Agents": "Neutron Agents", "Neutron Service": "Neutron Service", + "New": "New", "New Availability Zone": "New Availability Zone", "New Caledonia": "New Caledonia", "New Zealand": "New Zealand", @@ -1451,7 +1452,6 @@ "Please select your Domain!": "Please select your Domain!", "Please select your Region!": "Please select your Region!", "Please select {name} first": "Please select {name} first", - "Please select!": "Please select!", "Please set CPU && Ram first.": "Please set CPU && Ram first.", "Please set MUNA": "Please set MUNA", "Please upload files smaller than { size }G on the page. It is recommended to upload files over { size }G using API.": "Please upload files smaller than { size }G on the page. It is recommended to upload files over { size }G using API.", @@ -1482,6 +1482,7 @@ "Port Security Enabled": "Port Security Enabled", "Port Type": "Port Type", "Ports": "Ports", + "Ports provide extra communication channels to your instances. You can select ports instead of networks or a mix of both (The port executes its own security group rules by default).": "Ports provide extra communication channels to your instances. You can select ports instead of networks or a mix of both (The port executes its own security group rules by default).", "Portugal": "Portugal", "Power Off": "Power Off", "Power On": "Power On", diff --git a/src/locales/zh.json b/src/locales/zh.json index f83e01ab..4cea53a3 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -1254,6 +1254,7 @@ "Neutron Agent Detail": "网络服务详情", "Neutron Agents": "网络服务", "Neutron Service": "网络服务", + "New": "新建", "New Availability Zone": "新可用域", "New Caledonia": "新喀里多尼亚", "New Zealand": "新西兰", @@ -1451,7 +1452,6 @@ "Please select your Domain!": "请选择Domain!", "Please select your Region!": "请选择Region!", "Please select {name} first": "请先选择{name}", - "Please select!": "请选择!", "Please set CPU && Ram first.": "请先设置CPU、内存。", "Please set MUNA": "请设置NUMA节点", "Please upload files smaller than { size }G on the page. It is recommended to upload files over { size }G using API.": "页面请上传小于{ size }G的文件,超过{ size }G的文件建议使用API上传。", @@ -1482,6 +1482,7 @@ "Port Security Enabled": "启用端口安全", "Port Type": "端口方式", "Ports": "端口", + "Ports provide extra communication channels to your instances. You can select ports instead of networks or a mix of both (The port executes its own security group rules by default).": "端口为您的云主机提供了额外的通信渠道。您可以选择已创建的端口而非网络或者二者都选(端口默认执行本身的安全组规则)。", "Portugal": "葡萄牙", "Power Off": "关机", "Power On": "开机", diff --git a/src/pages/compute/containers/Instance/actions/StepCreate/ConfirmStep/index.jsx b/src/pages/compute/containers/Instance/actions/StepCreate/ConfirmStep/index.jsx index 6651cdc4..80d130f7 100644 --- a/src/pages/compute/containers/Instance/actions/StepCreate/ConfirmStep/index.jsx +++ b/src/pages/compute/containers/Instance/actions/StepCreate/ConfirmStep/index.jsx @@ -74,7 +74,7 @@ export class ConfirmStep extends Base { getVirtualLANs() { const { context } = this.props; - const { networks } = context; + const { networks = [] } = context; const values = networks.map((it) => { const { networkOption, subnetOption, ipTypeOption, ip } = it.value; const subnet = @@ -86,19 +86,42 @@ export class ConfirmStep extends Base { return ( {values.map((i) => ( - {i} + + {i} + + ))} + + ); + } + + getPorts() { + const { context } = this.props; + const { ports: { selectedRows = [] } = {} } = context; + const values = selectedRows.map((it) => it.name || it.id); + return ( + + {values.map((i) => ( + + {i} + ))} ); - // return values.join(
); } getSecurityGroups() { const { context } = this.props; const { securityGroup: { selectedRows = [] } = {} } = context; const values = selectedRows.map((it) => it.name); - return values; - // return values.join(
); + return ( + + {values.map((i) => ( + + {i} + + ))} + + ); } getLoginType() { @@ -215,10 +238,15 @@ export class ConfirmStep extends Base { }, items: [ { - label: t('Virtual LAN'), + label: `${t('Virtual LAN')}(${t('New')})`, value: this.getVirtualLANs(), span: 1, }, + { + label: `${t('Virtual LAN')}(${t('Created')})`, + value: this.getPorts(), + span: 1, + }, { label: t('Security Group'), value: this.getSecurityGroups(), diff --git a/src/pages/compute/containers/Instance/actions/StepCreate/NetworkStep/index.jsx b/src/pages/compute/containers/Instance/actions/StepCreate/NetworkStep/index.jsx index 1e64d72b..b8451b7e 100644 --- a/src/pages/compute/containers/Instance/actions/StepCreate/NetworkStep/index.jsx +++ b/src/pages/compute/containers/Instance/actions/StepCreate/NetworkStep/index.jsx @@ -15,12 +15,11 @@ import React from 'react'; import { inject, observer } from 'mobx-react'; import { Link } from 'react-router-dom'; -import { Button } from 'antd'; -import { FormOutlined } from '@ant-design/icons'; import { isEmpty, isArray } from 'lodash'; import { NetworkStore } from 'stores/neutron/network'; import { SubnetStore } from 'stores/neutron/subnet'; import { SecurityGroupStore } from 'stores/neutron/security-group'; +import { VirtualAdapterStore } from 'stores/neutron/virtual-adapter'; import { ipValidate } from 'utils/validate'; import Base from 'components/Form'; import NetworkSelect from 'components/FormItem/NetworkSelect'; @@ -29,6 +28,7 @@ import { securityGroupColumns, securityGroupFilter, } from 'resources/security-group'; +import { portColumns, portFilters } from 'resources/port'; // import EditYamlModal from 'components/Modals/EditYaml'; const { isIPv4, isIpv6 } = ipValidate; @@ -38,6 +38,7 @@ export class NetworkStep extends Base { this.networkStore = new NetworkStore(); this.subnetStore = new SubnetStore(); this.securityGroupStore = new SecurityGroupStore(); + this.portStore = new VirtualAdapterStore(); this.subnetMap = {}; } @@ -153,16 +154,43 @@ export class NetworkStep extends Base { }); }; + checkNetworkAndPort = ({ getFieldValue }) => ({ + validator() { + const networkSelect = getFieldValue('networkSelect'); + const ports = getFieldValue('ports'); + const { selectedRowKeys: networkSelected = [] } = networkSelect || {}; + const { selectedRowKeys: portsSelected = [] } = ports || {}; + if (networkSelected.length === 0 && portsSelected === 0) { + return Promise.reject(t('Please select')); + } + return Promise.resolve(); + }, + }); + + onPortChange = (value) => { + const { selectedRows = [] } = value || {}; + this.updateContext({ + portSelectRows: selectedRows, + }); + }; + get nameForStateUpdate() { - return ['networkSelect', 'networks']; + return ['networkSelect', 'networks', 'ports']; } get formItems() { - const { networkSelectRows = [], subnets, initValue = [] } = this.state; + const { + networkSelectRows = [], + subnets, + initValue = [], + ports = [], + } = this.state; const showNetworks = networkSelectRows.length > 0; const showSecurityGroups = networkSelectRows.length && networkSelectRows.every((it) => it.port_security_enabled); + const networkRequired = ports.length === 0; + const portRequired = networkSelectRows.length === 0; return [ { name: 'networkSelect', @@ -172,7 +200,9 @@ export class NetworkStep extends Base { onChange: this.onNetworkChange, showExternal: true, isMulti: true, - required: true, + required: networkRequired, + otherRule: this.checkNetworkAndPort, + dependencies: ['ports'], header: (
{t( @@ -208,20 +238,29 @@ export class NetworkStep extends Base { }, }, { - name: 'ipv6', - label: 'IPv6', - type: 'label', - hidden: true, - content: ( - - {t('The selected VPC/ subnet does not have IPv6 enabled.')}{' '} - {' '} - - ), + name: 'divider1', + type: 'divider', }, { + name: 'ports', + type: 'select-table', + // type: 'input', + label: t('Ports'), + extraParams: { project_id: this.currentProjectId, status: 'DOWN' }, + backendPageStore: this.portStore, + isMulti: true, + header: t( + 'Ports provide extra communication channels to your instances. You can select ports instead of networks or a mix of both (The port executes its own security group rules by default).' + ), + filterParams: portFilters, + columns: portColumns, + dependencies: ['networkSelect'], + otherRule: this.checkNetworkAndPort, + required: portRequired, + onChange: this.onPortChange, + }, + { + name: 'divider2', type: 'divider', }, { diff --git a/src/pages/compute/containers/Instance/actions/StepCreate/index.jsx b/src/pages/compute/containers/Instance/actions/StepCreate/index.jsx index 6ca053df..601633dd 100644 --- a/src/pages/compute/containers/Instance/actions/StepCreate/index.jsx +++ b/src/pages/compute/containers/Instance/actions/StepCreate/index.jsx @@ -409,16 +409,43 @@ export class StepCreate extends StepAction { }; } + getNetworkData(values) { + const { networks = [], ports = {} } = values; + let hasIp = false; + const data = []; + networks.forEach((it) => { + const net = { + uuid: it.value.network, + }; + if (it.value.ipType === 1 && it.value.ip) { + net.fixed_ip = it.value.ip; + hasIp = true; + } + data.push(net); + }); + const { selectedRowKeys = [] } = ports || {}; + selectedRowKeys.forEach((it) => { + const net = { + port: it, + }; + data.push(net); + }); + return { + data, + hasIp, + }; + } + getSubmitData(values) { if (this.status === 'error') { return null; } const { volumes, imageRef } = this.getVolumeAndImageData(values); + const { data: networks, hasIp } = this.getNetworkData(values); const { availableZone, keypair, loginType, - networks, password, physicalNode, physicalNodeType, @@ -429,7 +456,10 @@ export class StepCreate extends StepAction { name, count = 1, } = values; - let hasIp = false; + if (hasIp && count > 1) { + this.ipBatchError = true; + return null; + } const { selectedRows: securityGroupSelectedRows = [] } = securityGroup || {}; const server = { @@ -440,21 +470,8 @@ export class StepCreate extends StepAction { flavorRef: flavor.selectedRowKeys[0], availability_zone: availableZone.value, block_device_mapping_v2: volumes, - networks: networks.map((it) => { - const net = { - uuid: it.value.network, - }; - if (it.value.ipType === 1 && it.value.ip) { - net.fixed_ip = it.value.ip; - hasIp = true; - } - return net; - }), + networks, }; - if (hasIp && count > 1) { - this.ipBatchError = true; - return Promise.reject(); - } if (imageRef) { server.imageRef = imageRef; } @@ -501,6 +518,10 @@ export class StepCreate extends StepAction { const { data } = this.state; this.values = data; const submitData = this.getSubmitData(data); + if (!submitData) { + Notify.errorWithDetail(null, this.errorText); + return; + } this.onSubmit(submitData).then( () => { this.routing.push(this.listUrl); diff --git a/src/resources/port.jsx b/src/resources/port.jsx index fd0463bd..e09c9bc8 100644 --- a/src/resources/port.jsx +++ b/src/resources/port.jsx @@ -174,3 +174,54 @@ export function getPortFormItem(device_owner) { }, ]; } + +export const portColumns = [ + { + title: t('ID/Name'), + dataIndex: 'name', + sorter: false, + render: (value, record) => ( +
+
{record.id}
+
{value || '-'}
+
+ ), + }, + { + title: t('Owned Network'), + dataIndex: 'network_name', + isLink: true, + idKey: 'network_id', + sorter: false, + }, + { + title: t('IPv4 Address'), + dataIndex: 'ipv4', + render: (value) => value.map((it) =>
{it}
), + sorter: false, + }, + { + title: t('IPv6 Address'), + dataIndex: 'ipv6', + render: (value) => value.map((it) =>
{it}
), + sorter: false, + }, + { + title: t('Mac Address'), + dataIndex: 'mac_address', + sorter: false, + }, + { + title: t('Status'), + dataIndex: 'status', + render: (value) => portStatus[value] || value, + sorter: false, + }, +]; + +export const portFilters = [ + { + label: t('Name'), + name: 'name', + }, +];