diff --git a/src/layouts/admin-menu.jsx b/src/layouts/admin-menu.jsx index 5d271977..44926208 100644 --- a/src/layouts/admin-menu.jsx +++ b/src/layouts/admin-menu.jsx @@ -293,6 +293,13 @@ const renderMenu = (t) => { level: 2, routePath: '/network/networks-admin/detail/:id', }, + { + path: /^\/network\/networks-admin\/detail\/.[^/]+\/subnet\/.[^/]+$/, + name: t('Subnet Detail'), + key: 'subnetDetailAdmin', + level: 2, + routePath: '/network/networks-admin/detail/:networkId/subnet/:id', + }, ], }, { @@ -308,6 +315,28 @@ const renderMenu = (t) => { level: 2, routePath: '/network/port-admin/detail/:id', }, + { + path: /^\/network\/networks-admin\/detail\/.[^/]+\/port\/.[^/]+$/, + name: t('Port Detail'), + key: 'networkPortDetailAdmin', + level: 2, + routePath: '/network/networks-admin/detail/:networkId/port/:id', + }, + { + path: /^\/network\/networks-admin\/detail\/.[^/]+\/subnet\/.[^/]+\/port\/.[^/]+$/, + name: t('Port Detail'), + key: 'subnetPortDetailAdmin', + level: 2, + routePath: + '/network/networks-admin/detail/:networkId/subnet/:subnetId/port/:id', + }, + { + path: /^\/network\/instance-admin\/detail\/.[^/]+\/port\/.[^/]+$/, + name: t('Port Detail'), + key: 'instancePortDetailAdmin', + level: 2, + routePath: '/network/instance-admin/detail/:instanceId/port/:id', + }, ], }, { diff --git a/src/layouts/menu.jsx b/src/layouts/menu.jsx index 91ad82c2..5c6e8cdb 100644 --- a/src/layouts/menu.jsx +++ b/src/layouts/menu.jsx @@ -259,6 +259,13 @@ const renderMenu = (t) => { level: 2, routePath: '/network/networks/detail/:id', }, + { + path: /^\/network\/networks\/detail\/.[^/]+\/subnet\/.[^/]+$/, + name: t('Subnet Detail'), + key: 'subnetDetail', + level: 2, + routePath: '/network/networks/detail/:networkId/subnet/:id', + }, ], }, { @@ -274,6 +281,28 @@ const renderMenu = (t) => { level: 2, routePath: '/network/port/detail/:id', }, + { + path: /^\/network\/networks\/detail\/.[^/]+\/port\/.[^/]+$/, + name: t('Port Detail'), + key: 'networkPortDetail', + level: 2, + routePath: '/network/networks/detail/:networkId/port/:id', + }, + { + path: /^\/network\/networks\/detail\/.[^/]+\/subnet\/.[^/]+\/port\/.[^/]+$/, + name: t('Port Detail'), + key: 'subnetPortDetail', + level: 2, + routePath: + '/network/networks/detail/:networkId/subnet/:subnetId/port/:id', + }, + { + path: /^\/network\/instance\/detail\/.[^/]+\/port\/.[^/]+$/, + name: t('Port Detail'), + key: 'instancePortDetail', + level: 2, + routePath: '/network/instance/detail/:instanceId/port/:id', + }, ], }, { diff --git a/src/locales/en.json b/src/locales/en.json index ad7cdb30..5a1990fd 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -587,6 +587,7 @@ "DNS": "DNS", "DNS Assignment": "DNS Assignment", "DNS Name": "DNS Name", + "DNS Nameservers": "DNS Nameservers", "DPD Action": "DPD Action", "DPD Interval (sec)": "DPD Interval (sec)", "DPD actions controls the use of Dead Peer Detection Protocol.": "DPD actions controls the use of Dead Peer Detection Protocol.", @@ -841,6 +842,7 @@ "Enable Admin State": "Enable Admin State", "Enable Compute Host": "Enable Compute Host", "Enable Compute Service": "Enable Compute Service", + "Enable DHCP": "Enable DHCP", "Enable Domain": "Enable Domain", "Enable Floating IP": "Enable Floating IP", "Enable HealthMonitor": "Enable HealthMonitor", @@ -1089,6 +1091,7 @@ "IP Address": "IP Address", "IP Distribution Mode": "IP Distribution Mode", "IP Protocol": "IP Protocol", + "IP Usage": "IP Usage", "IP Version": "IP Version", "IP address allocation polls, one enter per line(e.g. 192.168.1.2,192.168.1.200)": "IP address allocation polls, one enter per line(e.g. 192.168.1.2,192.168.1.200)", "IP address allocation polls, one enter per line(e.g. {ip})": "IP address allocation polls, one enter per line(e.g. {ip})", @@ -1781,6 +1784,7 @@ "Pool Protocol": "Pool Protocol", "Pools": "Pools", "Port": "Port", + "Port Count": "Port Count", "Port Detail": "Port Detail", "Port Forwarding": "Port Forwarding", "Port Forwardings": "Port Forwardings", @@ -2217,6 +2221,7 @@ "Sub Users": "Sub Users", "Subnet": "Subnet", "Subnet Count": "Subnet Count", + "Subnet Detail": "Subnet Detail", "Subnet ID": "Subnet ID", "Subnet Name": "Subnet Name", "Subnets": "Subnets", @@ -2858,6 +2863,7 @@ "static routers": "static routers", "stop instance": "stop instance", "storage backend": "storage backend", + "subnet": "subnet", "subnets": "subnets", "suspend instance": "suspend instance", "the Republic of Abkhazia": "the Republic of Abkhazia", diff --git a/src/locales/zh.json b/src/locales/zh.json index 0e03e6c7..1e2d7f33 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -587,6 +587,7 @@ "DNS": "DNS", "DNS Assignment": "DNS指派", "DNS Name": "DNS名称", + "DNS Nameservers": "DNS服务器", "DPD Action": "DPD动作", "DPD Interval (sec)": "DPD最大延迟(秒)", "DPD actions controls the use of Dead Peer Detection Protocol.": "DPD动作控制对失效对端协议的处理方式。", @@ -841,6 +842,7 @@ "Enable Admin State": "启用管理状态", "Enable Compute Host": "启用计算节点", "Enable Compute Service": "启用计算服务", + "Enable DHCP": "DHCP 已启用", "Enable Domain": "启用域", "Enable Floating IP": "使用浮动IP", "Enable HealthMonitor": "启用健康检查", @@ -1089,6 +1091,7 @@ "IP Address": "IP地址", "IP Distribution Mode": "IP分配模式", "IP Protocol": "IP协议", + "IP Usage": "IP使用情况", "IP Version": "IP版本", "IP address allocation polls, one enter per line(e.g. 192.168.1.2,192.168.1.200)": "IP地址分配池,每行一条(例如: 192.168.1.2,192.168.1.200)", "IP address allocation polls, one enter per line(e.g. {ip})": "IP地址分配池,每行一条(例如: {ip})", @@ -1781,6 +1784,7 @@ "Pool Protocol": "资源池协议", "Pools": "", "Port": "端口", + "Port Count": "端口数量", "Port Detail": "端口详情", "Port Forwarding": "端口转发", "Port Forwardings": "端口转发", @@ -2217,6 +2221,7 @@ "Sub Users": "组内用户列表", "Subnet": "子网", "Subnet Count": "子网数量", + "Subnet Detail": "子网详情", "Subnet ID": "子网ID", "Subnet Name": "子网名称", "Subnets": "子网", @@ -2858,6 +2863,7 @@ "static routers": "静态路由", "stop instance": "关闭云主机", "storage backend": "存储后端", + "subnet": "子网", "subnets": "子网", "suspend instance": "挂起云主机", "the Republic of Abkhazia": "阿布哈兹", diff --git a/src/pages/network/containers/Network/Detail/index.jsx b/src/pages/network/containers/Network/Detail/index.jsx index ed0e1dbe..3961b04b 100644 --- a/src/pages/network/containers/Network/Detail/index.jsx +++ b/src/pages/network/containers/Network/Detail/index.jsx @@ -16,9 +16,9 @@ import { inject, observer } from 'mobx-react'; import Base from 'containers/TabDetail'; import { NetworkStore } from 'stores/neutron/network'; import { networkStatus } from 'resources/neutron/network'; -import Port from 'src/pages/network/containers/Port'; +import Port from 'pages/network/containers/Port'; import globalRootStore from 'stores/root'; -import Subnets from './Subnets'; +import Subnet from 'pages/network/containers/Subnet'; import Detail from './Detail'; import actionConfigs from '../actions'; @@ -140,7 +140,7 @@ export class NetworkDetail extends Base { { title: t('Subnets'), key: 'subnets', - component: Subnets, + component: Subnet, }, { title: t('Ports'), diff --git a/src/pages/network/containers/Port/Detail/index.jsx b/src/pages/network/containers/Port/Detail/index.jsx index 53dd0c48..da902583 100644 --- a/src/pages/network/containers/Port/Detail/index.jsx +++ b/src/pages/network/containers/Port/Detail/index.jsx @@ -32,9 +32,43 @@ export class PortDetail extends Base { } get listUrl() { + const { networkId, subnetId, instanceId } = this.params; + if (this.isSubnetPortDetail) { + return this.getRoutePath( + 'subnetDetail', + { id: subnetId, networkId }, + { tab: 'ports' } + ); + } + if (this.isNetworkPortDetail) { + return this.getRoutePath( + 'networkDetail', + { id: networkId }, + { tab: 'ports' } + ); + } + if (this.isInstancePortDetail) { + return this.getRoutePath( + 'instanceDetail', + { id: instanceId }, + { tab: 'interface' } + ); + } return this.getRoutePath('port'); } + get isSubnetPortDetail() { + return this.path.includes('subnet'); + } + + get isNetworkPortDetail() { + return this.path.includes('networks') && !this.isSubnetPortDetail; + } + + get isInstancePortDetail() { + return this.path.includes('instance'); + } + get actionConfigs() { if (this.isAdminPage) { return actionConfigs.adminActions; diff --git a/src/pages/network/containers/Port/index.jsx b/src/pages/network/containers/Port/index.jsx index 858314e8..8c2faa29 100644 --- a/src/pages/network/containers/Port/index.jsx +++ b/src/pages/network/containers/Port/index.jsx @@ -38,20 +38,29 @@ export class Port extends Base { return ( this.inDetailPage && (this.path.includes('networks/detail') || - this.path.includes('networks-admin/detail')) + this.path.includes('networks-admin/detail')) && + !this.isSubnetDetail ); } + get isSubnetDetail() { + return this.inDetailPage && this.path.includes('subnet'); + } + + get isRecycleBinDetail() { + return this.inDetailPage && this.path.includes('recycle-bin'); + } + get isFilterByBackend() { - return true; + return !this.isSubnetDetail; } get isSortByBackend() { - return true; + return this.isFilterByBackend; } get defaultSortKey() { - return 'status'; + return this.isFilterByBackend ? 'status' : ''; } updateFetchParamsByPage = (params) => { @@ -65,6 +74,15 @@ export class Port extends Base { return newParams; }; + updateFetchParams = (params) => { + const { id, networkId, ...rest } = params; + return { + network_id: networkId, + subnetId: id, + ...rest, + }; + }; + get policy() { return 'get_port'; } @@ -77,10 +95,6 @@ export class Port extends Base { return true; } - get isRecycleBinDetail() { - return this.inDetailPage && this.path.includes('recycle-bin'); - } - get actionConfigs() { if (this.isRecycleBinDetail) { return emptyActionConfig; @@ -141,12 +155,44 @@ export class Port extends Base { ); }; + getPortDetailRoute = () => { + if (this.isSubnetDetail) { + return { + routeName: this.getRouteName('subnetPortDetail'), + routeParamsFunc: (data) => ({ + networkId: data.network_id, + subnetId: data.subnet_id, + id: data.id, + }), + }; + } + if (this.isNetworkDetail) { + return { + routeName: this.getRouteName('networkPortDetail'), + routeParamsFunc: (data) => ({ + networkId: data.network_id, + id: data.id, + }), + }; + } + if (this.isInstanceDetail) { + return { + routeName: this.getRouteName('instancePortDetail'), + routeParamsFunc: (data) => ({ + instanceId: data.device_id, + id: data.id, + }), + }; + } + return { routeName: this.getRouteName('portDetail') }; + }; + getColumns = () => { const columns = [ { title: t('ID/Name'), dataIndex: 'name', - routeName: this.getRouteName('portDetail'), + ...this.getPortDetailRoute(), }, { title: t('Project ID/Name'), @@ -261,7 +307,7 @@ export class Port extends Base { { label: t('DHCP Agent'), key: 'network:dhcp' }, { label: t('Others'), - key: 'network:local_ip,network:routed,network:distributed', + key: 'network:local_ip,network:routed,network:distributed,compute:kuryr', }, { label: t('Unbounded'), @@ -269,7 +315,18 @@ export class Port extends Base { }, ], }; - ret.push(deviceOwner); + if (this.isSubnetDetail) { + deviceOwner.filterFunc = (value, filter) => { + if (filter === 'none') { + return !value; + } + return value && filter.includes(value); + }; + } + if (!this.isInstanceDetail) { + ret.push(deviceOwner); + } + return ret; } } diff --git a/src/pages/network/containers/Subnet/Detail/Detail.jsx b/src/pages/network/containers/Subnet/Detail/Detail.jsx new file mode 100644 index 00000000..b64cb88b --- /dev/null +++ b/src/pages/network/containers/Subnet/Detail/Detail.jsx @@ -0,0 +1,140 @@ +// Copyright 2022 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import React from 'react'; +import { inject, observer } from 'mobx-react'; +import Base from 'containers/BaseDetail'; + +export class BaseDetail extends Base { + get leftCards() { + const cards = [this.networkCard, this.baseInfoCard]; + if (this.canAddNetworkIPUsageInfo) { + cards.push(this.ipUsageCard); + } + return cards; + } + + get canAddNetworkIPUsageInfo() { + return ( + this.isAdminPage || this.currentProjectId === this.detailData.project_id + ); + } + + get networkCard() { + const options = [ + { + label: t('Network Name'), + dataIndex: 'network.name', + }, + { + label: t('Network ID'), + dataIndex: 'network.id', + render: (value) => { + const link = this.getLinkRender('networkDetail', value, { + id: value, + }); + return link; + }, + }, + ]; + return { + title: t('Network Info'), + options, + }; + } + + get baseInfoCard() { + const options = [ + { + label: t('Gateway IP'), + dataIndex: 'gateway_ip', + }, + { + label: t('Allocation Pools'), + dataIndex: 'allocation_pools', + render: (value) => { + const items = (value || []).map((it) => { + const { start, end } = it; + return ( +
+ {start} - {end} +
+ ); + }); + return <>{items}; + }, + }, + { + label: t('Enable DHCP'), + dataIndex: 'enable_dhcp', + valueRender: 'yesNo', + }, + { + label: t('Host Routes'), + dataIndex: 'host_routes', + render: (value) => { + if (!value.length) { + return '-'; + } + const lines = value.map((it) => { + const { destination, nexthop } = it; + return ( +
+ {destination},{nexthop} +
+ ); + }); + return <>{lines}; + }, + }, + { + label: t('DNS Nameservers'), + dataIndex: 'dns_nameservers', + render: (value) => { + if (!value.length) { + return '-'; + } + const lines = value.map((it) =>
{it}
); + return <>{lines}; + }, + }, + ]; + return { + title: t('Base Info'), + options, + }; + } + + get ipUsageCard() { + if (!this.canAddNetworkIPUsageInfo) { + return null; + } + const options = [ + { + label: t('Total IPs'), + dataIndex: 'total_ips', + }, + { + label: t('Used IPs'), + dataIndex: 'used_ips', + }, + ]; + return { + title: t('IP Usage'), + options, + }; + } +} + +export default inject('rootStore')(observer(BaseDetail)); diff --git a/src/pages/network/containers/Subnet/Detail/index.jsx b/src/pages/network/containers/Subnet/Detail/index.jsx new file mode 100644 index 00000000..0142e6e3 --- /dev/null +++ b/src/pages/network/containers/Subnet/Detail/index.jsx @@ -0,0 +1,108 @@ +// Copyright 2022 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { inject, observer } from 'mobx-react'; +import Base from 'containers/TabDetail'; +import { SubnetStore } from 'stores/neutron/subnet'; +import Port from 'pages/network/containers/Port'; +import Detail from './Detail'; +import actionConfigs from '../actions'; + +export class SubnetDetail extends Base { + get name() { + return t('subnet'); + } + + get policy() { + return 'get_subnet'; + } + + get listUrl() { + const { networkId } = this.params; + return this.getRoutePath( + 'networkDetail', + { id: networkId }, + { tab: 'subnets' } + ); + } + + get actionConfigs() { + return actionConfigs; + } + + updateFetchParams = (params) => { + return { + ...params, + inDetail: true, + }; + }; + + get detailInfos() { + const ret = [ + { + title: t('Name'), + dataIndex: 'name', + }, + { + title: t('Project ID'), + dataIndex: 'project_id', + }, + { + title: t('CIDR'), + dataIndex: 'cidr', + }, + { + title: t('IP Version'), + dataIndex: 'ip_version', + }, + { + title: t('Created At'), + dataIndex: 'created_at', + valueRender: 'toLocalTime', + }, + { + title: t('Update At'), + dataIndex: 'updated_at', + valueRender: 'toLocalTime', + }, + { + title: t('Description'), + dataIndex: 'description', + }, + ]; + return ret; + } + + get tabs() { + const tabs = [ + { + title: t('Detail'), + key: 'detail', + component: Detail, + }, + { + title: t('Ports'), + key: 'ports', + component: Port, + }, + ]; + return tabs; + } + + init() { + this.store = new SubnetStore(); + } +} + +export default inject('rootStore')(observer(SubnetDetail)); diff --git a/src/pages/network/containers/Network/Detail/subnetActions/DeleteSubnet.jsx b/src/pages/network/containers/Subnet/actions/Delete.jsx similarity index 95% rename from src/pages/network/containers/Network/Detail/subnetActions/DeleteSubnet.jsx rename to src/pages/network/containers/Subnet/actions/Delete.jsx index 6d6e2d3d..8eb8ba33 100644 --- a/src/pages/network/containers/Network/Detail/subnetActions/DeleteSubnet.jsx +++ b/src/pages/network/containers/Subnet/actions/Delete.jsx @@ -15,7 +15,7 @@ import { ConfirmAction } from 'containers/Action'; import globalSubnetStore from 'stores/neutron/subnet'; -export default class DeleteAction extends ConfirmAction { +export default class Delete extends ConfirmAction { get id() { return 'delete'; } diff --git a/src/pages/network/containers/Network/Detail/subnetActions/EditSubnet.jsx b/src/pages/network/containers/Subnet/actions/Edit.jsx similarity index 98% rename from src/pages/network/containers/Network/Detail/subnetActions/EditSubnet.jsx rename to src/pages/network/containers/Subnet/actions/Edit.jsx index caa0285f..159d1c87 100644 --- a/src/pages/network/containers/Network/Detail/subnetActions/EditSubnet.jsx +++ b/src/pages/network/containers/Subnet/actions/Edit.jsx @@ -16,7 +16,7 @@ import { inject, observer } from 'mobx-react'; import { ModalAction } from 'containers/Action'; import { ipValidate } from 'utils/validate'; import globalSubnetStore from 'stores/neutron/subnet'; -import networkUtil from '../../actions/networkUtil'; +import networkUtil from '../../Network/actions/networkUtil'; const { checkAllocation_pools, diff --git a/src/pages/network/containers/Network/Detail/subnetActions/index.jsx b/src/pages/network/containers/Subnet/actions/index.jsx similarity index 72% rename from src/pages/network/containers/Network/Detail/subnetActions/index.jsx rename to src/pages/network/containers/Subnet/actions/index.jsx index e04e041d..3d1d6293 100644 --- a/src/pages/network/containers/Network/Detail/subnetActions/index.jsx +++ b/src/pages/network/containers/Subnet/actions/index.jsx @@ -12,21 +12,21 @@ // See the License for the specific language governing permissions and // limitations under the License. -import CreateSubnet from '../../actions/CreateSubnet'; -import DeleteAction from './DeleteSubnet'; -import EditSubnet from './EditSubnet'; +import Create from '../../Network/actions/CreateSubnet'; +import Delete from './Delete'; +import Edit from './Edit'; const actionConfigs = { rowActions: { - firstAction: EditSubnet, + firstAction: Edit, moreActions: [ { - action: DeleteAction, + action: Delete, }, ], }, - batchActions: [DeleteAction], - primaryActions: [CreateSubnet], + batchActions: [Delete], + primaryActions: [Create], }; export default actionConfigs; diff --git a/src/pages/network/containers/Network/Detail/Subnets.jsx b/src/pages/network/containers/Subnet/index.jsx similarity index 60% rename from src/pages/network/containers/Network/Detail/Subnets.jsx rename to src/pages/network/containers/Subnet/index.jsx index d6cbd275..25f977ce 100644 --- a/src/pages/network/containers/Network/Detail/Subnets.jsx +++ b/src/pages/network/containers/Subnet/index.jsx @@ -12,54 +12,18 @@ // See the License for the specific language governing permissions and // limitations under the License. +import React from 'react'; import { observer, inject } from 'mobx-react'; import Base from 'containers/List'; import { SubnetStore } from 'stores/neutron/subnet'; -import { toJS } from 'mobx'; -import globalRootStore from 'stores/root'; -import actionConfigs from './subnetActions'; +import actionConfigs from './actions'; // import { networkStatus } from 'resources/network'; export class Subnets extends Base { init() { this.store = new SubnetStore(); - const { detail: { subnet_ip_availability = [] } = {} } = this.props; - this.subnet_ip_availability = subnet_ip_availability; } - getDataSource = () => { - const { data, filters = {}, timeFilter = {} } = this.list; - const { id, tab, ...rest } = filters; - const newFilters = rest; - let items = []; - if (this.isFilterByBackend) { - items = toJS(data); - } else { - items = (toJS(data) || []).filter((it) => - this.filterData(it, toJS(newFilters), toJS(timeFilter)) - ); - this.updateList({ total: items.length }); - } - const hasTransData = items.some((item) => - this.itemInTransitionFunction(item) - ); - if (hasTransData) { - this.setRefreshDataTimerTransition(); - } else { - this.setRefreshDataTimerAuto(); - } - const ret = items.map((item) => { - const usageDetail = this.subnet_ip_availability.find( - (i) => i.subnet_id === item.id - ); - return { - ...usageDetail, - ...item, - }; - }); - return ret; - }; - get policy() { return 'get_subnet'; } @@ -77,16 +41,16 @@ export class Subnets extends Base { } updateFetchParams = () => { - const { id } = this.props.match.params; return { - network_id: id, + network_id: this.id, + network: this.props.detail, + all_projects: this.isAdminPage, }; }; get canAddNetworkIPUsageInfo() { return ( - this.isAdminPage || - globalRootStore.user.project.id === this.props.detail.project_id + this.isAdminPage || this.currentProjectId === this.props.detail.project_id ); } @@ -96,6 +60,11 @@ export class Subnets extends Base { title: t('Name'), dataIndex: 'name', stringify: (name, record) => name || record.id, + routeName: this.getRouteName('subnetDetail'), + routeParamsFunc: (data) => ({ + networkId: data.network_id, + id: data.id, + }), }, { title: t('CIDR'), @@ -113,11 +82,25 @@ export class Subnets extends Base { isHideable: true, }, { - // title: t('Status'), - // dataIndex: 'status', - // render: value => networkStatus[value] || value, - // isHideable: true, - // }, { + title: t('Port Count'), + dataIndex: 'subnetPorts', + isHideable: true, + stringify: (value) => (value || []).length, + render: (value, record) => { + const count = (value || []).length; + if (!count) { + return '-'; + } + const link = this.getLinkRender( + 'subnetDetail', + count, + { id: record.id, networkId: record.network_id }, + { tab: 'ports' } + ); + return <>{link}; + }, + }, + { title: t('Created At'), dataIndex: 'created_at', valueRender: 'toLocalTime', @@ -126,7 +109,7 @@ export class Subnets extends Base { ]; if (this.canAddNetworkIPUsageInfo) { ret.splice( - 4, + 5, 0, { title: t('Total IPs'), diff --git a/src/pages/network/routes/index.js b/src/pages/network/routes/index.js index ec3b37d7..bda54af5 100644 --- a/src/pages/network/routes/index.js +++ b/src/pages/network/routes/index.js @@ -17,6 +17,7 @@ import E404 from 'pages/base/containers/404'; import Network from '../containers/Network'; import AdminNetwork from '../containers/Network/Network'; import NetworkDetail from '../containers/Network/Detail'; +import SubnetDetail from '../containers/Subnet/Detail'; import Router from '../containers/Router'; import FloatingIp from '../containers/FloatingIp'; import FloatingIpDetail from '../containers/FloatingIp/Detail'; @@ -58,6 +59,46 @@ export default [ component: NetworkDetail, exact: true, }, + { + path: `${PATH}/networks/detail/:networkId/subnet/:id`, + component: SubnetDetail, + exact: true, + }, + { + path: `${PATH}/networks-admin/detail/:networkId/subnet/:id`, + component: SubnetDetail, + exact: true, + }, + { + path: `${PATH}/networks/detail/:networkId/port/:id`, + component: PortDetail, + exact: true, + }, + { + path: `${PATH}/networks-admin/detail/:networkId/port/:id`, + component: PortDetail, + exact: true, + }, + { + path: `${PATH}/networks/detail/:networkId/subnet/:subnetId/port/:id`, + component: PortDetail, + exact: true, + }, + { + path: `${PATH}/networks-admin/detail/:networkId/subnet/:subnetId/port/:id`, + component: PortDetail, + exact: true, + }, + { + path: `${PATH}/instance/detail/:instanceId/port/:id`, + component: PortDetail, + exact: true, + }, + { + path: `${PATH}/instance-admin/detail/:instanceId/port/:id`, + component: PortDetail, + exact: true, + }, { path: `${PATH}/router`, component: Router, exact: true }, { path: `${PATH}/router-admin`, component: Router, exact: true }, { diff --git a/src/stores/neutron/port-extension.js b/src/stores/neutron/port-extension.js index d14ae12e..f92c211f 100644 --- a/src/stores/neutron/port-extension.js +++ b/src/stores/neutron/port-extension.js @@ -59,6 +59,24 @@ export class PortStore extends Base { }; } + get paramsFunc() { + return (params, all_projects) => { + const { current, device_owner, subnetId, networkId, ...rest } = params; + const newParams = { ...rest }; + if (device_owner && isString(device_owner)) { + if (device_owner === 'none') { + newParams.device_owner = ['']; + } else { + newParams.device_owner = device_owner.split(','); + } + } + if (!all_projects) { + newParams.tenant_id = this.currentProjectId; + } + return newParams; + }; + } + @observable fixed_ips = new List(); @@ -150,6 +168,37 @@ export class PortStore extends Base { item.itemInList = itemContrib; return item; } + + async listDidFetch(items, allProjects, filters) { + if (!items.length) { + return items; + } + const { subnetId } = filters; + if (!subnetId) { + return items; + } + const newItems = []; + items.forEach((it) => { + const { fixed_ips = [] } = it; + const newFixedIps = fixed_ips.filter((ip) => ip.subnet_id === subnetId); + if (newFixedIps.length) { + const ipv4 = it.ipv4.filter((ip) => + newFixedIps.some((newIp) => newIp.ip_address === ip) + ); + const ipv6 = it.ipv6.filter((ip) => + newFixedIps.some((newIp) => newIp.ip_address === ip) + ); + newItems.push({ + ...it, + fixed_ips: newFixedIps, + ipv4, + ipv6, + subnet_id: subnetId, + }); + } + }); + return newItems; + } } const globalPortStore = new PortStore(); diff --git a/src/stores/neutron/subnet.js b/src/stores/neutron/subnet.js index cf340880..36935ac6 100644 --- a/src/stores/neutron/subnet.js +++ b/src/stores/neutron/subnet.js @@ -25,6 +25,17 @@ export class SubnetStore extends Base { return false; } + get portClient() { + return client.neutron.ports; + } + + get paramsFunc() { + return (params) => { + const { network, ...rest } = params; + return rest; + }; + } + @action async update({ id }, values) { const { @@ -47,6 +58,62 @@ export class SubnetStore extends Base { }; return this.submitting(this.client.update(id, { subnet: data })); } + + async listDidFetch(items, allProjects, filters) { + if (!items.length) { + return items; + } + const { + network: { id: networkId, subnet_ip_availability: ipUsage = [] } = {}, + } = filters; + const portParams = { + network_id: networkId, + }; + if (!allProjects) { + portParams.tenant_id = this.currentProjectId; + } + const { ports = [] } = await this.portClient.list(portParams); + return items.map((it) => { + const ipInfo = ipUsage.find((u) => u.subnet_id === it.id); + const subnetPorts = ports.filter((port) => { + return port.fixed_ips.find((ip) => ip.subnet_id === it.id); + }); + const { total_ips, used_ips } = ipInfo || {}; + return { + ...it, + total_ips, + used_ips, + subnetPorts, + }; + }); + } + + async detailDidFetch(item, allProjects, filters) { + const { inDetail = false } = filters; + if (!inDetail) { + return item; + } + const { network_id, id } = item; + const networkParams = { + id: network_id, + isAdminPage: allProjects, + currentProjectId: this.currentProjectId, + }; + const { NetworkStore } = require('stores/neutron/network'); + const network = + await new NetworkStore().fetchDetailWithAvailabilityAndUsage( + networkParams + ); + const { subnet_ip_availability = [] } = network; + const ipAvailability = subnet_ip_availability.find( + (it) => it.subnet_id === id + ); + return { + ...item, + network, + ...ipAvailability, + }; + } } const globalSubnetStore = new SubnetStore();