import { inject, observer } from 'mobx-react';
import { ModalAction } from 'containers/Action';
import Notify from 'components/Notify';
import globalRouterStore from 'stores/neutron/router';
import globalNeutronStore from 'stores/neutron/neutron';
import globalNetworkStore, { NetworkStore } from 'stores/neutron/network';
import { nameTypeValidate, ipValidate } from 'utils/validate';
import { has } from 'lodash';
import { networkStatus } from 'resources/neutron/network';
const { isIpCidr, isIPv6Cidr, getFirstIpFromCidr } = ipValidate;
const { nameValidateWithoutChinese } = nameTypeValidate;
const DEFAULT_CIDR = {
ipv4: '192.168.0.0/24',
ipv6: '1001:1001::/64',
};
const DEFAULT_DNS = {
ipv4: '1.1.1.1',
ipv6: '1001:1001::2',
};
const IP_VERSION_OPTIONS = [
{ label: t('IPv4'), value: 'ipv4' },
{ label: t('IPv6'), value: 'ipv6' },
];
export class NetworkQuickStart extends ModalAction {
static id = 'network-quick-start';
static title = t('Quick Create');
static policy = ['create_network', 'create_subnet', 'create_router'];
static allowed = () => Promise.resolve(true);
constructor(props) {
super(props);
this.state = {
ipVersion: 'ipv4',
createRouter: false,
openExternalNetwork: false,
isSubmitting: false,
};
}
init() {
this.routerStore = globalRouterStore;
this.networkStore = new NetworkStore();
}
componentDidMount() {
this.setSubmitFormCallback();
}
componentWillUnmount() {
this.setSubmitFormCallback(null);
}
setSubmitFormCallback = (callback = this.submitForm) => {
if (this.props.setSubmitFormCallback) {
this.props.setSubmitFormCallback(callback);
}
};
get name() {
return t('Quick Start Network');
}
getModalSize() {
return 'large';
}
get labelCol() {
return { span: 8 };
}
get wrapperCol() {
return { span: 14 };
}
get modalProps() {
return {
width: '80%',
style: { top: 20 },
bodyStyle: {
maxHeight: 'calc(100vh - 200px)',
overflow: 'auto',
padding: '24px',
},
className: 'quick-start-network-form',
};
}
get isIpv4() {
const { ipVersion = 'ipv4' } = this.state;
return ipVersion === 'ipv4';
}
get defaultValue() {
const ipVersion = this.state.ipVersion || 'ipv4';
const defaultCidr = DEFAULT_CIDR[ipVersion];
const defaultGatewayIp = getFirstIpFromCidr(defaultCidr);
const defaultDns = DEFAULT_DNS[ipVersion];
const gatewayIp =
ipVersion === 'ipv6' && !defaultGatewayIp
? getFirstIpFromCidr(defaultCidr)
: defaultGatewayIp;
const defaultValue = {
networkName: 'network-1',
subnetName: 'subnet-1',
subnetCidr: defaultCidr,
ipVersion,
gatewayIp,
dns: defaultDns,
createRouter: false,
routerName: 'router-1',
openExternalNetwork: false,
};
if (ipVersion === 'ipv6') {
defaultValue.ipv6AddressMode = 'slaac';
defaultValue.ipv6RaMode = 'slaac';
}
return defaultValue;
}
get isSubmitting() {
return this.state.isSubmitting;
}
validateCidr = (rule, value) => {
if (!value) {
return Promise.resolve();
}
const { ipVersion = 'ipv4' } = this.state;
const isValid = ipVersion === 'ipv4' ? isIpCidr(value) : isIPv6Cidr(value);
if (isValid) {
return Promise.resolve();
}
return Promise.reject(new Error(t('Invalid: ') + t('CIDR')));
};
validateIpv6 = (rule, value) => {
if (!value || value.trim() === '') {
return Promise.resolve();
}
if (ipValidate.isIpv6(value)) {
return Promise.resolve();
}
return Promise.reject(new Error(t('Invalid: Please input a valid IPv6.')));
};
handleIpVersionChange = (value) => {
this.setState({ ipVersion: value });
if (this.formRef?.current) {
const defaultCidr = DEFAULT_CIDR[value];
const defaultGatewayIp = getFirstIpFromCidr(defaultCidr);
const defaultDns = DEFAULT_DNS[value];
this.formRef.current.setFieldsValue({
subnetCidr: defaultCidr,
gatewayIp: defaultGatewayIp,
dns: defaultDns,
});
}
};
handleGatewayIpChange = (e) => {
this.setState({
gatewayIp: e?.target?.value,
});
};
handleCidrChange = (e) => {
const cidr = e?.target?.value;
this.updateGatewayFromCidr(cidr);
};
handleBooleanStateChange = (field) => (e) => {
this.setState({
[field]: e?.target?.checked ?? e,
});
};
updateGatewayFromCidr = (cidr) => {
if (cidr && this.formRef?.current) {
const firstIp = getFirstIpFromCidr(cidr);
if (firstIp) {
this.formRef.current.setFieldsValue({
gatewayIp: firstIp,
});
}
const ipVersion = this.state.ipVersion || 'ipv4';
const defaultDns = DEFAULT_DNS[ipVersion];
if (defaultDns) {
this.formRef.current.setFieldsValue({
dns: defaultDns,
});
}
}
};
onValuesChange = (changedFields) => {
if (has(changedFields, 'createRouter')) {
this.setState({
createRouter: changedFields.createRouter,
});
}
if (has(changedFields, 'openExternalNetwork')) {
this.setState({
openExternalNetwork: changedFields.openExternalNetwork,
});
}
if (has(changedFields, 'subnetCidr')) {
this.updateGatewayFromCidr(changedFields.subnetCidr);
}
};
get formItems() {
const {
ipVersion = 'ipv4',
createRouter,
openExternalNetwork,
} = this.state;
const isIpv4 = ipVersion === 'ipv4';
const baseItems = [
{
name: 'networkName',
label: t('Network Name'),
type: 'input',
required: true,
validator: nameValidateWithoutChinese,
},
{
name: 'subnetName',
label: t('Subnet Name'),
type: 'input',
required: true,
},
{
name: 'ipVersion',
label: t('IP Version'),
type: 'select',
options: IP_VERSION_OPTIONS,
onChange: this.handleIpVersionChange,
required: true,
},
{
name: 'subnetCidr',
label: t('Subnet CIDR'),
type: 'input',
required: true,
placeholder: DEFAULT_CIDR[ipVersion],
validator: this.validateCidr,
onChange: this.handleCidrChange,
tip: isIpv4
? t(
'It is recommended that you use the private network address 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16'
)
: t('e.g. 2001:Db8::/48'),
},
{
name: 'gatewayIp',
label: t('Gateway IP'),
type: isIpv4 ? 'ip-input' : 'input',
onChange: this.handleGatewayIpChange,
tip: t(
'Auto-populated with first IP from CIDR. You can modify if needed.'
),
validator: isIpv4 ? null : this.validateIpv6,
},
{
name: 'dns',
label: t('DNS'),
type: 'textarea',
extra: t('One entry per line (e.g. {ip})', {
ip: DEFAULT_DNS[ipVersion],
}),
placeholder: DEFAULT_DNS[ipVersion],
},
{
name: 'createRouter',
label: t('Create and Attach Router'),
type: 'check',
onChange: this.handleBooleanStateChange('createRouter'),
},
];
if (createRouter) {
baseItems.push(
{
name: 'routerName',
label: t('Router Name'),
type: 'input',
required: true,
validator: nameValidateWithoutChinese,
},
{
name: 'openExternalNetwork',
label: t('Options'),
type: 'check',
content: t('External Gateway'),
tip: t(
'Enable external gateway to allow the router to connect to external networks for internet access.'
),
onChange: this.handleBooleanStateChange('openExternalNetwork'),
}
);
if (openExternalNetwork) {
baseItems.push({
name: 'externalNetwork',
label: t('External Gateway'),
type: 'select-table',
backendPageStore: this.networkStore,
extraParams: { 'router:external': true },
required: true,
labelCol: { span: 8 },
wrapperCol: { span: 14 },
filterParams: [
{
label: t('Name'),
name: 'name',
},
],
columns: [
{
title: t('Name'),
dataIndex: 'name',
},
{
title: t('Status'),
dataIndex: 'status',
valueMap: networkStatus,
},
{
title: t('Created At'),
dataIndex: 'created_at',
valueRender: 'sinceTime',
},
],
});
}
}
return baseItems;
}
submitForm = () => {
this.onClickSubmit(
(success, hasError) => {
if (success && !hasError && this.props.onCancel) {
this.props.onCancel();
}
},
null,
this.props.containerProps || {}
);
};
onOk = async (values, containerProps, callback) => {
this.values = values;
this.setState({ isSubmitting: true });
try {
await this.onSubmit(values, containerProps);
const networkName = values.networkName || 'unknown';
Notify.success(
t('Network {name} was successfully created.', {
name: networkName,
})
);
this.handleSuccess(callback);
} catch (err) {
const networkName = values.networkName || 'unknown';
Notify.error(
err?.message ||
t('Unable to create network {name}.', {
name: networkName,
})
);
this.handleError(callback);
} finally {
this.setState({ isSubmitting: false });
}
};
handleSuccess = (callback) => {
if (callback && typeof callback === 'function') {
callback(true, false);
}
};
handleError = (callback) => {
if (callback && typeof callback === 'function') {
callback(false, true);
}
};
createNetworkAndSubnet = async (networkData, subnetData) => {
globalNetworkStore.updateCreateWithSubnet(true);
const result = await globalNetworkStore.createAndMore(networkData, {
...subnetData,
create_subnet: true,
});
const { network, subnet } = result || {};
globalNetworkStore.lastCreatedItem = network;
globalNetworkStore.lastCreatedSubnet = subnet;
return { network, subnet };
};
createAndConnectRouter = async (network, subnet, routerConfig) => {
const { routerName, openExternalNetwork, externalNetwork } = routerConfig;
const routerData = {
name: routerName || 'router-1',
project_id: globalNeutronStore.currentProjectId,
};
if (openExternalNetwork && externalNetwork?.selectedRows?.[0]) {
routerData.external_gateway_info = {
network_id: externalNetwork.selectedRows[0].id,
};
}
const routerResult = await this.routerStore.create(routerData);
const router = routerResult.router || routerResult;
const subnetId = subnet.subnet ? subnet.subnet.id : subnet.id;
await this.routerStore.connectSubnet({
id: router.id,
subnetId,
networkId: network.id,
});
return router;
};
prepareNetworkData = (values) => {
const { networkName } = values;
const { currentProjectId } = globalNeutronStore;
if (!currentProjectId) {
throw new Error('Current project ID is not available');
}
return {
name: networkName,
project_id: currentProjectId,
};
};
prepareSubnetData = (values) => {
const { subnetName, subnetCidr, ipVersion, gatewayIp, dns } = values;
const isIpv6 = ipVersion === 'ipv6';
const subnetData = {
subnet_name: subnetName,
cidr: subnetCidr,
ip_version: ipVersion || 'ipv4',
enable_dhcp: true,
dns,
};
if (isIpv6 || gatewayIp) {
subnetData.gateway_ip = gatewayIp;
}
if (isIpv6) {
subnetData.ipv6_address_mode = 'slaac';
subnetData.ipv6_ra_mode = 'slaac';
}
return subnetData;
};
prepareRouterData = (values) => {
const { routerName, openExternalNetwork, externalNetwork } = values;
return {
routerName,
openExternalNetwork,
externalNetwork,
};
};
onSubmit = async (values) => {
const { createRouter } = values;
const networkData = this.prepareNetworkData(values);
const subnetData = this.prepareSubnetData(values);
const { network, subnet } = await this.createNetworkAndSubnet(
networkData,
subnetData
);
if (createRouter && network && subnet) {
try {
const routerConfig = this.prepareRouterData(values);
await this.createAndConnectRouter(network, subnet, routerConfig);
const routerName = values.routerName || 'router-1';
Notify.success(
t('Router {name} was created and subnet successfully connected.', {
name: routerName,
})
);
} catch (routerError) {
const routerName = values.routerName || 'router-1';
Notify.error(
t('Router {name} creation failed: {error}', {
name: routerName,
error: routerError.message || 'Unknown error',
})
);
}
}
};
}
export default inject('rootStore')(observer(NetworkQuickStart));