feat: support edit port forwarding with port range

1. Refactor edit port forwarding
2. Support edit port forwarding with port range
3. Update the instance name of the port forwarding
4. Update the display of the port forwarding porotols
5. Fix the display of the port forwarding popup in the fip list page

Change-Id: Iab97a60275f94d79cfb95db42154351665d0025e
This commit is contained in:
Jingwei.Zhang 2022-08-18 11:14:09 +08:00
parent f560965348
commit c6f4c9117e
8 changed files with 212 additions and 280 deletions

View File

@ -27,7 +27,6 @@
"A container with the same name already exists": "A container with the same name already exists",
"A dynamic scheduling algorithm that estimates the server load based on the number of currently active connections. The system allocates new connection requests to the server with the least number of current connections. Commonly used for long connection services, such as database connections and other services.": "A dynamic scheduling algorithm that estimates the server load based on the number of currently active connections. The system allocates new connection requests to the server with the least number of current connections. Commonly used for long connection services, such as database connections and other services.",
"A host aggregate can be associated with at most one AZ. Once the association is established, the AZ cannot be disassociated.": "A host aggregate can be associated with at most one AZ. Once the association is established, the AZ cannot be disassociated.",
"A port forwarding has been created for this port of this FIP, please choose another port.": "A port forwarding has been created for this port of this FIP, please choose another port.",
"A public container will allow anyone to use the objects in your container through a public URL.": "A public container will allow anyone to use the objects in your container through a public URL.",
"A snapshot is an image which preserves the disk state of a running instance, which can be used to start a new instance.": "A snapshot is an image which preserves the disk state of a running instance, which can be used to start a new instance.",
"A template is a YAML file that contains configuration information, please enter the correct format.": "A template is a YAML file that contains configuration information, please enter the correct format.",
@ -2334,7 +2333,6 @@
"The password must not be the same as the previous two": "The password must not be the same as the previous two",
"The password must not be the same as the previous {num}": "The password must not be the same as the previous {num}",
"The port created here will be automatically deleted when detach. If you need a reusable port, please go to the Virtual Adapter page to create and attach the port to instance.": "The port created here will be automatically deleted when detach. If you need a reusable port, please go to the Virtual Adapter page to create and attach the port to instance.",
"The port of this fip is in use, Please change another port.": "The port of this fip is in use, Please change another port.",
"The private key content format is: with \"-----BEGIN RSA PRIVATE KEY-----\" as the beginning,\"-----END RSA PRIVATE KEY-----\" as the end, 64 characters per line, the last line does not exceed 64 characters, and there cannot be blank lines.": "The private key content format is: with \"-----BEGIN RSA PRIVATE KEY-----\" as the beginning,\"-----END RSA PRIVATE KEY-----\" as the end, 64 characters per line, the last line does not exceed 64 characters, and there cannot be blank lines.",
"The private key of the certificate, the extension of the private key is \"key\", you can directly enter the content of the private key file or upload a private key that conforms to the format document.": "The private key of the certificate, the extension of the private key is \"key\", you can directly enter the content of the private key file or upload a private key that conforms to the format document.",
"The resource class of the scheduled node needs to correspond to the resource class name of the flavor used by the ironic instance (for example, the resource class name of the scheduling node is baremetal.with-GPU, and the custom resource class name of the flavor is CUSTOM_BAREMETAL_WITH_GPU=1).": "The resource class of the scheduled node needs to correspond to the resource class name of the flavor used by the ironic instance (for example, the resource class name of the scheduling node is baremetal.with-GPU, and the custom resource class name of the flavor is CUSTOM_BAREMETAL_WITH_GPU=1).",
@ -2412,6 +2410,7 @@
"Tuvalu": "Tuvalu",
"Two-way authentication": "Two-way authentication",
"Type": "Type",
"UDP": "UDP",
"UDPLite": "UDPLite",
"UNHEALTHY": "UNHEALTHY",
"UNKNOWN": "UNKNOWN",

View File

@ -27,7 +27,6 @@
"A container with the same name already exists": "已存在同名容器",
"A dynamic scheduling algorithm that estimates the server load based on the number of currently active connections. The system allocates new connection requests to the server with the least number of current connections. Commonly used for long connection services, such as database connections and other services.": "通过当前活跃的连接数来估计服务器负载情况的一种动态调度算法,系统把新的连接请求分配给当前连接数目最少的服务器。常用于长连接服务,例如数据库连接等服务。",
"A host aggregate can be associated with at most one AZ. Once the association is established, the AZ cannot be disassociated.": "一个主机集合最多可以与一个AZ建立关联一旦建立了关联无法再取消关联AZ。",
"A port forwarding has been created for this port of this FIP, please choose another port.": "该端口已经创建了端口转发,请使用另一个端口。",
"A public container will allow anyone to use the objects in your container through a public URL.": "一个公有容器会允许任何人通过公共 URL 去使用您容器里面的对象。",
"A snapshot is an image which preserves the disk state of a running instance, which can be used to start a new instance.": "云主机当前状态的磁盘数据保存,创建镜像文件,以备将来启动新的云主机使用。",
"A template is a YAML file that contains configuration information, please enter the correct format.": "模板是包含配置信息的YAML文件 请输入正确的格式。",
@ -2250,7 +2249,7 @@
"System Roles": "系统角色",
"System Running Time": "",
"System is error, please try again later.": "系统出错,请稍后再试。",
"TCP": "",
"TCP": "TCP",
"TCP Connections": "TCP连接数",
"TLS Disabled": "TLS禁用",
"Tag is no longer than 60 characters": "标签名长度不超过60个字符",
@ -2334,7 +2333,6 @@
"The password must not be the same as the previous two": "用户新密码不能与重置前的密码一致",
"The password must not be the same as the previous {num}": "用户新密码不能与前{num}次密码相同",
"The port created here will be automatically deleted when detach. If you need a reusable port, please go to the Virtual Adapter page to create and attach the port to instance.": "此处创建的网卡会在卸载的时候被自动删除,如果需要可复用的网卡,请前往虚拟网卡页面创建再从虚拟网卡页面绑定云主机。",
"The port of this fip is in use, Please change another port.": "FIP的此端口已经在使用中请更换另一个端口。",
"The private key content format is: with \"-----BEGIN RSA PRIVATE KEY-----\" as the beginning,\"-----END RSA PRIVATE KEY-----\" as the end, 64 characters per line, the last line does not exceed 64 characters, and there cannot be blank lines.": "私钥内容格式为:以“-----BEGIN RSA PRIVATE KEY-----”,以“-----END RSA PRIVATE KEY-----”作为结尾每行64字符最后一行不超过64字符不能有空行。",
"The private key of the certificate, the extension of the private key is \"key\", you can directly enter the content of the private key file or upload a private key that conforms to the format document.": "证书的私钥私钥扩展名为”key”您可直接输入私钥文件内容或上传符合格式的私钥文件。",
"The resource class of the scheduled node needs to correspond to the resource class name of the flavor used by the ironic instance (for example, the resource class name of the scheduling node is baremetal.with-GPU, and the custom resource class name of the flavor is CUSTOM_BAREMETAL_WITH_GPU=1).": "被调度节点的资源类需要与裸机实例使用的云主机类型的资源类名称对应(比如:调度节点的资源类名称为 baremetal.with-GPU云主机类型的资源类名称为CUSTOM_BAREMETAL_WITH_GPU=1 )。",
@ -2412,6 +2410,7 @@
"Tuvalu": "图瓦卢",
"Two-way authentication": "双向认证",
"Type": "类型",
"UDP": "UDP",
"UDPLite": "",
"UNHEALTHY": "不健康",
"UNKNOWN": "未知",

View File

@ -15,7 +15,7 @@
import React from 'react';
import { inject, observer } from 'mobx-react';
import { ModalAction } from 'containers/Action';
import { isNull, isObject } from 'lodash';
import { isNull, isObject, isString } from 'lodash';
import { getCanReachSubnetIdsWithRouterIdInComponent } from 'resources/neutron/router';
import { PortStore } from 'stores/neutron/port-extension';
import {
@ -23,10 +23,15 @@ import {
getPortsAndReasons,
getPortsForPortFormItem,
} from 'resources/neutron/port';
import { getInterfaceWithReason } from 'resources/neutron/floatingip';
import {
getInterfaceWithReason,
portForwardingProtocols,
getPortForwardingName,
} from 'resources/neutron/floatingip';
import globalPortForwardingStore from 'stores/neutron/port-forwarding';
import { enablePFW } from 'resources/neutron/neutron';
import { regex } from 'utils/validate';
import { getOptions } from 'utils/index';
const { portRangeRegex } = regex;
@ -60,6 +65,10 @@ export class CreatePortForwarding extends ModalAction {
this.getPorts();
this.getRangeSupport();
this.getFipAlreadyUsedPorts();
this.getExtraInfo();
}
getExtraInfo() {
getCanReachSubnetIdsWithRouterIdInComponent.call(this, (router) => {
const { item } = this;
return (
@ -69,9 +78,13 @@ export class CreatePortForwarding extends ModalAction {
});
}
get fipId() {
return this.item.id;
}
async getFipAlreadyUsedPorts() {
const detail = await globalPortForwardingStore.fetchList({
fipId: this.item.id,
fipId: this.fipId,
});
this.setState({
alreadyUsedPorts: detail || [],
@ -79,7 +92,10 @@ export class CreatePortForwarding extends ModalAction {
}
get instanceName() {
return this.item.floating_ip_address || this.values.name;
return getPortForwardingName(
this.submitData || this.values,
this.item.floating_ip_address
);
}
static get modalSize() {
@ -130,6 +146,7 @@ export class CreatePortForwarding extends ModalAction {
}
data.internal_ip_address = fixedIPAddressSelectedRows[0].fixed_ip_address;
data.internal_port_id = selectedRows[0].id;
this.submitData = data;
return data;
}
@ -172,7 +189,7 @@ export class CreatePortForwarding extends ModalAction {
try {
await globalPortForwardingStore.fetchListByPage({
limit: 1,
fipId: this.item.id,
fipId: this.fipId,
external_port_range: '80:81',
});
this.setState({
@ -210,43 +227,48 @@ export class CreatePortForwarding extends ModalAction {
this.formRef.current.resetFields(['fixed_ip_address', 'internal_port']);
};
checkPortUsedBase = (pf, type, port, protocol) => {
const {
external_port,
internal_port,
external_port_range,
internal_port_range,
} = pf;
const range =
type === 'external' ? external_port_range : internal_port_range;
if (range) {
const [start, end] = this.getRangeFromString(range);
return port >= start && port <= end && pf.protocol === protocol;
}
const pfPort = type === 'external' ? external_port : internal_port;
return port === pfPort && pf.protocol === protocol;
};
checkPortUsedInternal = (baseCheck, pf) => {
if (!baseCheck) {
return false;
}
const formData = this.formRef.current.getFieldsValue([
'virtual_adapter',
'fixed_ip_address',
]);
const internalIpAddress =
formData.fixed_ip_address.selectedRows[0].fixed_ip_address;
const internalPortId = formData.virtual_adapter.selectedRows[0].id;
return (
pf.internal_port_id === internalPortId &&
pf.internal_ip_address === internalIpAddress
);
};
checkPortUsed = (val, type) => {
const { alreadyUsedPorts: usedPorts, protocol } = this.state;
const port = parseInt(val, 10);
const checkInternal = (baseCheck, pf) => {
if (!baseCheck) {
return false;
}
const formData = this.formRef.current.getFieldsValue([
'virtual_adapter',
'fixed_ip_address',
]);
const internalIpAddress =
formData.fixed_ip_address.selectedRows[0].fixed_ip_address;
const internalPortId = formData.virtual_adapter.selectedRows[0].id;
return (
pf.internal_port_id === internalPortId &&
pf.internal_ip_address === internalIpAddress
);
};
return usedPorts.find((pf) => {
const {
external_port,
internal_port,
external_port_range,
internal_port_range,
} = pf;
const range =
type === 'external' ? external_port_range : internal_port_range;
const pfPort = type === 'external' ? external_port : internal_port;
if (range) {
const [start, end] = this.getRangeFromString(range);
const baseCheck =
port >= start && port <= end && pf.protocol === protocol;
return type === 'external' ? baseCheck : checkInternal(baseCheck, pf);
}
const baseCheck = port === pfPort && pf.protocol === protocol;
return type === 'external' ? baseCheck : checkInternal(baseCheck, pf);
const baseCheck = this.checkPortUsedBase(pf, type, port, protocol);
return type === 'external'
? baseCheck
: this.checkPortUsedInternal(baseCheck, pf);
});
};
@ -389,13 +411,18 @@ export class CreatePortForwarding extends ModalAction {
};
validateExternalPort = (rule, val) => {
const value =
val === undefined ? '' : isString(val) ? val : val.toString() || '';
const { internal_port: internalPort } = this.formRef.current.getFieldsValue(
['internal_port']
);
if (!portRangeRegex.test(val)) {
if (!portRangeRegex.test(value)) {
return Promise.resolve(true);
}
const result = this.checkExternalPortInput(val, internalPort || '');
const result = this.checkExternalPortInput(
value,
(internalPort || '').toString() || ''
);
if (result) {
return Promise.reject(result);
}
@ -442,13 +469,18 @@ export class CreatePortForwarding extends ModalAction {
};
validateInternalPort = (_, val) => {
if (!portRangeRegex.test(val)) {
const value =
val === undefined ? '' : isString(val) ? val : val.toString() || '';
if (!portRangeRegex.test(value)) {
return Promise.resolve(true);
}
const { external_port: externalPort } = this.formRef.current.getFieldsValue(
['external_port']
);
const result = this.checkInternalPortInput(externalPort || '', val);
const result = this.checkInternalPortInput(
(externalPort || '').toString() || '',
value
);
if (result) {
return Promise.reject(result);
}
@ -544,16 +576,7 @@ export class CreatePortForwarding extends ModalAction {
name: 'protocol',
label: t('Protocol'),
type: 'select',
options: [
{
label: 'TCP',
value: 'tcp',
},
{
label: 'UDP',
value: 'udp',
},
],
options: getOptions(portForwardingProtocols),
required: true,
},
{

View File

@ -14,6 +14,7 @@
import { ConfirmAction } from 'containers/Action';
import globalPortForwardingStore from 'stores/neutron/port-forwarding';
import { getPortForwardingName } from 'resources/neutron/floatingip';
export default class Delete extends ConfirmAction {
get id() {
@ -39,13 +40,7 @@ export default class Delete extends ConfirmAction {
policy = 'delete_floatingip_port_forwarding';
getItemName = (data) => {
const {
floating_ip_address,
external_port,
internal_ip_address,
internal_port,
} = data;
return `${floating_ip_address}:${external_port}-->${internal_ip_address}:${internal_port}`;
return getPortForwardingName(data, data.floating_ip_address);
};
onSubmit = (data) => {

View File

@ -14,17 +14,16 @@
import { inject, observer } from 'mobx-react';
import globalPortForwardingStore from 'stores/neutron/port-forwarding';
import globalPortStore, { PortStore } from 'stores/neutron/port-extension';
import globalPortStore from 'stores/neutron/port-extension';
import { getCanReachSubnetIdsWithRouterIdInComponent } from 'resources/neutron/router';
import { getInterfaceWithReason } from 'resources/neutron/floatingip';
import {
getPortFormItem,
getPortsAndReasons,
getPortsForPortFormItem,
} from 'resources/neutron/port';
import { ModalAction } from 'containers/Action';
getInterfaceWithReason,
getPortForwardingName,
} from 'resources/neutron/floatingip';
import { getPortsAndReasons } from 'resources/neutron/port';
import { CreatePortForwarding as Base } from './Create';
export class Edit extends ModalAction {
export class Edit extends Base {
static id = 'edit';
static title = t('Edit');
@ -33,8 +32,20 @@ export class Edit extends ModalAction {
return t('Edit Port Forwarding');
}
init() {
this.portStore = new PortStore();
get instanceName() {
const { floating_ip_address: fip } = this.item;
return getPortForwardingName(this.item, fip);
}
get tips() {
return '';
}
get fipId() {
return this.item.fip.id;
}
getExtraInfo() {
getCanReachSubnetIdsWithRouterIdInComponent
.call(this, (router) => {
const {
@ -48,32 +59,6 @@ export class Edit extends ModalAction {
.then(() => {
this.getInitialPortFixedIPs();
});
this.getFipAlreadyUsedPorts();
this.getPorts();
this.state = {
alreadyUsedPorts: [],
instanceFixedIPs: [],
portFixedIPs: [],
canReachSubnetIdsWithRouterId: [],
routerIdWithExternalNetworkInfo: [],
};
}
get portDeviceOwner() {
return ['compute:nova', ''];
}
getPorts() {
getPortsForPortFormItem.call(this, this.portDeviceOwner);
}
async getFipAlreadyUsedPorts() {
const detail = await globalPortForwardingStore.fetchList({
fipId: this.item.fip.id,
});
this.setState({
alreadyUsedPorts: detail || [],
});
}
async getInitialPortFixedIPs() {
@ -93,20 +78,10 @@ export class Edit extends ModalAction {
selectedRowKeys: tmp.map((i) => i.id),
selectedRows: tmp,
};
this.formRef.current &&
this.formRef.current.setFields([
{
name: 'virtual_adapter',
value: {
selectedRowKeys: [portDetail.id],
selectedRows: [portDetail],
},
},
// {
// name: 'fixed_ip_address',
// value: fixed_ip_address,
// },
]);
this.updateFormValue('virtual_adapter', {
selectedRowKeys: [portDetail.id],
selectedRows: [portDetail],
});
return fixed_ip_address;
})
.then((fixed_ip_address) => {
@ -116,26 +91,6 @@ export class Edit extends ModalAction {
});
}
get instanceName() {
const {
floating_ip_address,
external_port,
internal_ip_address,
internal_port,
} = this.item;
return `${floating_ip_address}:${external_port} => ${internal_ip_address}:${internal_port}`;
}
static get modalSize() {
return 'large';
}
getModalSize() {
return 'large';
}
portsDisableFunc = (i) => i.fixed_ips.length === 0;
get defaultValue() {
const {
floating_ip_address,
@ -187,21 +142,37 @@ export class Edit extends ModalAction {
static allowed = () => true;
onSubmit = (values) => {
getSubmitData(values) {
const {
floatingIp,
virtual_adapter: { selectedRows = [] } = {},
fixed_ip_address: { selectedRows: fixedIPAddressSelectedRows = [] } = {},
external_port,
internal_port,
...rest
} = values;
const data = {
...rest,
};
if (external_port.toString().includes(':')) {
data.external_port_range = external_port;
} else {
data.external_port = external_port;
}
if (internal_port.toString().includes(':')) {
data.internal_port_range = internal_port;
} else {
data.internal_port = internal_port;
}
data.internal_ip_address = fixedIPAddressSelectedRows[0].fixed_ip_address;
data.internal_port_id = selectedRows[0].id;
return data;
}
onSubmit = (data) => {
return globalPortForwardingStore.edit(
{
fipId: this.item.fip.id,
fipId: this.fipId,
id: this.item.id,
},
data
@ -209,130 +180,61 @@ export class Edit extends ModalAction {
};
get formItems() {
const { fixed_ip_address = { selectedRows: [] } } = this.state;
const ret = [
{
name: 'floatingIp',
label: t('Floating Ip'),
type: 'label',
iconType: 'floatingIp',
},
{
name: 'protocol',
label: t('Protocol'),
type: 'select',
options: [
{
label: 'TCP',
value: 'tcp',
},
{
label: 'UDP',
value: 'udp',
},
],
required: true,
},
{
name: 'external_port',
label: t('External Port'),
type: 'input-number',
min: 1,
max: 65535,
required: true,
validator: (_, val) => {
if (!val) {
return Promise.reject(
new Error(`${t('Please input')} ${t('External Port')}`)
);
}
const { alreadyUsedPorts } = this.state;
const { external_port: initialExternalPort } = this.item;
const flag = alreadyUsedPorts.some(
(pf) =>
pf.external_port !== initialExternalPort &&
pf.external_port === val
);
if (flag) {
return Promise.reject(
new Error(
t('The port of this fip is in use, Please change another port.')
)
);
}
return Promise.resolve(true);
},
},
{
name: 'internal_port',
label: t('Internal Port'),
type: 'input-number',
hidden: fixed_ip_address.selectedRows.length === 0,
min: 1,
max: 65535,
required: true,
validator: (_, val) => {
if (!val) {
return Promise.reject(
new Error(`${t('Please input')} ${t('Internal Port')}`)
);
}
const formData = this.formRef.current.getFieldsValue([
'virtual_adapter',
'fixed_ip_address',
]);
const selectedFixedIPAddress = (formData.fixed_ip_address &&
formData.fixed_ip_address.selectedRows) || [''];
const internal_ip_address =
selectedFixedIPAddress[0].fixed_ip_address;
const internal_port_id = formData.virtual_adapter.selectedRows[0].id;
const { internal_port } = this.item;
const { alreadyUsedPorts } = this.state;
// check whether the port has been used
const flag = alreadyUsedPorts.some(
(pf) =>
pf.internal_port !== internal_port &&
pf.internal_port === val &&
pf.internal_port_id === internal_port_id &&
pf.internal_ip_address === internal_ip_address
);
if (flag) {
return Promise.reject(
new Error(
t(
'A port forwarding has been created for this port of this FIP, please choose another port.'
)
)
);
}
return Promise.resolve(true);
},
},
];
const extraColumn = getPortFormItem.call(this);
const portIndex = extraColumn.findIndex(
(i) => i.name === 'virtual_adapter'
);
extraColumn[portIndex].label = t('Target Port');
const fixedIPAddressIndex = extraColumn.findIndex(
(i) => i.name === 'fixed_ip_address'
);
extraColumn[fixedIPAddressIndex].label = t('Target IP Address');
extraColumn[fixedIPAddressIndex].onChange = (e) => {
this.setState(
{
fixed_ip_address: e,
},
() => {
this.formRef.current.resetFields(['internal_port']);
}
);
const items = super.formItems;
if (this.supportRange) {
return items;
}
const externalPortItem = items.find((it) => it.name === 'external_port');
const internalPortItem = items.find((it) => it.name === 'internal_port');
externalPortItem.label = t('External Port');
internalPortItem.label = t('Internal Port');
const inputConfig = {
type: 'input-int',
min: 1,
max: 65535,
extra: t('Enter an integer value between 1 and 65535.'),
};
ret.splice(3, 0, ...extraColumn);
return ret;
Object.assign(externalPortItem, inputConfig);
Object.assign(internalPortItem, inputConfig);
return items;
}
checkPortUsedBase = (pf, type, port, protocol) => {
const {
external_port,
internal_port,
external_port_range,
internal_port_range,
} = pf;
const range =
type === 'external' ? external_port_range : internal_port_range;
if (range) {
const [start, end] = this.getRangeFromString(range);
return port >= start && port <= end && pf.protocol === protocol;
}
const pfPort = type === 'external' ? external_port : internal_port;
return (
this.item.id !== pf.id && port === pfPort && pf.protocol === protocol
);
};
checkPortUsedInternal = (baseCheck, pf) => {
if (!baseCheck) {
return false;
}
const formData = this.formRef.current.getFieldsValue([
'virtual_adapter',
'fixed_ip_address',
]);
const internalIpAddress =
formData.fixed_ip_address.selectedRows[0].fixed_ip_address;
const internalPortId = formData.virtual_adapter.selectedRows[0].id;
return (
this.item.id !== pf.id &&
pf.internal_port_id === internalPortId &&
pf.internal_ip_address === internalIpAddress
);
};
}
export default inject('rootStore')(observer(Edit));

View File

@ -15,6 +15,8 @@
import { observer, inject } from 'mobx-react';
import Base from 'containers/List';
import { PortForwardingStore } from 'stores/neutron/port-forwarding';
import { getOptions } from 'utils/index';
import { portForwardingProtocols } from 'resources/neutron/floatingip';
import actionConfigs from './actions';
export class PortForwarding extends Base {
@ -93,6 +95,7 @@ export class PortForwarding extends Base {
title: t('Protocol'),
dataIndex: 'protocol',
isHideable: true,
render: (value) => portForwardingProtocols[value] || value,
},
{
title: t('Description'),
@ -107,16 +110,7 @@ export class PortForwarding extends Base {
{
label: t('Protocol'),
name: 'protocol',
options: [
{
label: 'TCP',
key: 'tcp',
},
{
label: 'UDP',
key: 'udp',
},
],
options: getOptions(portForwardingProtocols),
},
{
label: t('External Port'),

View File

@ -18,6 +18,7 @@ import Base from 'containers/List';
import {
floatingIpStatus,
transitionStatuses,
getPortForwardingName,
} from 'resources/neutron/floatingip';
import { FloatingIpStore } from 'stores/neutron/floatingIp';
import { emptyActionConfig } from 'utils/constants';
@ -155,17 +156,7 @@ export class FloatingIps extends Base {
return '';
}
const { floating_ip_address: fip } = record;
const {
protocol,
external_port,
external_port_range,
internal_ip_address,
internal_port,
internal_port_range,
} = detail;
return `${protocol}: ${fip}:${
external_port || external_port_range
} => ${internal_ip_address}:${internal_port || internal_port_range}`;
return getPortForwardingName(rest, fip);
}
get portForwardingResourceName() {
@ -183,7 +174,10 @@ export class FloatingIps extends Base {
return null;
}
const pageSize = 10;
const zeroLength = length > pageSize ? pageSize - (length % pageSize) : 0;
let zeroLength = 0;
if (length > pageSize && length % pageSize) {
zeroLength = pageSize - (length % pageSize);
}
const zeroData = Array.from({ length: zeroLength }, (i) => ({
key: `zero-${i}`,
}));

View File

@ -17,6 +17,7 @@ import globalNetworkStore from 'stores/neutron/network';
import { ipValidate } from 'utils/validate';
import globalFloatingIpsStore from 'stores/neutron/floatingIp';
import { enablePFW } from 'resources/neutron/neutron';
import { isEmpty } from 'lodash';
const { isIPv4 } = ipValidate;
@ -344,3 +345,28 @@ export const getFIPFormItemForAssociate = (self) => {
],
};
};
export const portForwardingProtocols = {
tcp: t('TCP'),
udp: t('UDP'),
};
export const getPortForwardingName = (portForwarding = {}, fip = '') => {
if (isEmpty(portForwarding)) {
return '';
}
const {
protocol,
external_port,
external_port_range,
internal_ip_address,
internal_port,
internal_port_range,
} = portForwarding;
const protocolLabel = portForwardingProtocols[protocol] || protocol;
const name = `${
external_port || external_port_range
} => ${internal_ip_address}:${internal_port || internal_port_range}`;
const longName = fip ? `${fip}:${name}` : name;
return `${protocolLabel}: ${longName}`;
};