From fda515654e7349e63dba879af6833d8b4476219b Mon Sep 17 00:00:00 2001 From: Qi Zhang Date: Fri, 22 May 2026 17:57:23 +0800 Subject: [PATCH 1/7] fix: clarify native resource creation scope Signed-off-by: Qi Zhang --- src/locales/en-US/resource.js | 2 ++ src/locales/zh-CN/resource.js | 2 ++ src/pages/ResourceCenter/components/HelmModals.js | 13 +++++++++++++ src/pages/ResourceCenter/index.js | 8 ++++++++ 4 files changed, 25 insertions(+) diff --git a/src/locales/en-US/resource.js b/src/locales/en-US/resource.js index b4730bfd6..0e47f7955 100644 --- a/src/locales/en-US/resource.js +++ b/src/locales/en-US/resource.js @@ -219,6 +219,7 @@ const resource = { 'resourceCenter.yaml.toolbar.createHint': 'Supports `.yaml` / `.yml`; you can continue reviewing and editing after import.', 'resourceCenter.yaml.toolbar.multiDoc': 'Multi-document supported', 'resourceCenter.yaml.toolbar.manualEdit': 'Manual editing preserved', + 'resourceCenter.yaml.nativeResourceNotice': 'This creates Kubernetes native resources. After creation, view and manage them in K8S Native Resources; they will not appear in Application Management automatically. To create applications or components for Application Management, use the Application Management entry.', 'resourceCenter.yaml.placeholder': 'apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: my-app\n...', 'resourceCenter.yaml.ok.create': 'Create', 'resourceCenter.yaml.ok.edit': 'Save', @@ -305,6 +306,7 @@ const resource = { 'resourceCenter.helm.modal.tabExternalHelper': 'Supports official, self-hosted repos, and OCI', 'resourceCenter.helm.modal.tabUpload': 'Upload Chart Package', 'resourceCenter.helm.modal.tabUploadHelper': 'Upload a .tgz and install the release directly', + 'resourceCenter.helm.modal.nativeResourceNotice': 'This installs a Helm release and manages it as Kubernetes native resources in K8S Native Resources. It will not appear in Application Management automatically. To create applications or components for Application Management, use the Application Management entry.', 'resourceCenter.helm.modal.externalNotice': 'Provide a chart address directly. Helm repo package URLs and OCI artifact addresses are both supported. Validation starts automatically when you go to the next step.', 'resourceCenter.helm.modal.externalNoticeShort': 'Provide a chart address directly. Helm repo package URLs and OCI artifact addresses are supported.', 'resourceCenter.helm.modal.externalSupport': 'Supports official Helm repos, self-hosted Helm repos, and OCI artifact sources.', diff --git a/src/locales/zh-CN/resource.js b/src/locales/zh-CN/resource.js index aea2b084d..f9db19145 100644 --- a/src/locales/zh-CN/resource.js +++ b/src/locales/zh-CN/resource.js @@ -219,6 +219,7 @@ const resource = { 'resourceCenter.yaml.toolbar.createHint': '支持 `.yaml` / `.yml`,导入后可继续校对与修改。', 'resourceCenter.yaml.toolbar.multiDoc': '支持多文档', 'resourceCenter.yaml.toolbar.manualEdit': '保留手动编辑', + 'resourceCenter.yaml.nativeResourceNotice': '这里创建的是 Kubernetes 原生资源,创建后在「K8S 原生资源」中查看和管理,不会自动出现在「应用管理」。如需应用管理中的应用或组件,请从应用管理入口创建。', 'resourceCenter.yaml.placeholder': 'apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: my-app\n...', 'resourceCenter.yaml.ok.create': '创建', 'resourceCenter.yaml.ok.edit': '保存', @@ -305,6 +306,7 @@ const resource = { 'resourceCenter.helm.modal.tabExternalHelper': '支持官方、自建 Repo 与 OCI', 'resourceCenter.helm.modal.tabUpload': '上传 Chart 包', 'resourceCenter.helm.modal.tabUploadHelper': '上传 .tgz 后直接安装 Release', + 'resourceCenter.helm.modal.nativeResourceNotice': '这里安装的是 Helm Release,会作为 Kubernetes 原生资源在「K8S 原生资源」中查看和管理,不会自动出现在「应用管理」。如需应用管理中的应用或组件,请从应用管理入口创建。', 'resourceCenter.helm.modal.externalNotice': '请直接填写 Chart 地址,支持 Helm 官方或自建 Helm Repo 中的 Chart 包地址,以及使用 OCI 格式的制品仓库。点击下一步后会自动检测并解析 Chart。', 'resourceCenter.helm.modal.externalNoticeShort': '请直接填写 Chart 地址,支持 Helm Repo 包地址和 OCI 制品地址。', 'resourceCenter.helm.modal.externalSupport': '支持 Helm 官方或自建 Helm Repo 仓库,以及使用 OCI 格式的制品仓库。', diff --git a/src/pages/ResourceCenter/components/HelmModals.js b/src/pages/ResourceCenter/components/HelmModals.js index 1f4770699..d2dcf4fad 100644 --- a/src/pages/ResourceCenter/components/HelmModals.js +++ b/src/pages/ResourceCenter/components/HelmModals.js @@ -69,6 +69,18 @@ class HelmModals extends PureComponent { ); } + renderHelmNativeResourceNotice() { + const helmModalMode = this.getStateValue('helmModalMode', 'install'); + if (helmModalMode !== 'install') { + return null; + } + return ( +
+ {t('resourceCenter.helm.modal.nativeResourceNotice', '这里安装的是 Helm Release,会作为 Kubernetes 原生资源在「K8S 原生资源」中查看和管理,不会自动出现在「应用管理」。如需应用管理中的应用或组件,请从应用管理入口创建。')} +
+ ); + } + renderHelmBrowse() { const helmRepos = this.getStateValue('helmRepos', []); const helmRepoLoading = this.getStateValue('helmRepoLoading', false); @@ -785,6 +797,7 @@ class HelmModals extends PureComponent { bodyStyle={{ padding: '16px 24px', minHeight: 560 }} > {this.renderHelmUpgradeAssistant()} + {this.renderHelmNativeResourceNotice()} {helmStep === 'source' && this.renderHelmSourceTabs()} {this.renderHelmStepContent()} diff --git a/src/pages/ResourceCenter/index.js b/src/pages/ResourceCenter/index.js index 15be89644..d215041cb 100644 --- a/src/pages/ResourceCenter/index.js +++ b/src/pages/ResourceCenter/index.js @@ -2216,6 +2216,14 @@ class ResourceCenter extends PureComponent { ) : null} + {yamlModalMode === 'create' && !yamlResultStep ? ( +
+ {formatMessage({ + id: 'resourceCenter.yaml.nativeResourceNotice', + defaultMessage: '这里创建的是 Kubernetes 原生资源,创建后在「K8S 原生资源」中查看和管理,不会自动出现在「应用管理」。如需应用管理中的应用或组件,请从应用管理入口创建。', + })} +
+ ) : null} {yamlResultStep ? this.renderYamlResultPanel() : ( Date: Fri, 22 May 2026 17:59:22 +0800 Subject: [PATCH 2/7] feat: show ingress resources in network tab Signed-off-by: Qi Zhang --- src/locales/en-US/resource.js | 18 +- src/locales/zh-CN/resource.js | 18 +- src/models/teamResources.js | 8 + src/pages/ResourceCenter/index.js | 37 +++- src/pages/ResourceCenter/tabs/NetworkTab.js | 178 +++++++++++++++----- 5 files changed, 200 insertions(+), 59 deletions(-) diff --git a/src/locales/en-US/resource.js b/src/locales/en-US/resource.js index 0e47f7955..ad929566f 100644 --- a/src/locales/en-US/resource.js +++ b/src/locales/en-US/resource.js @@ -126,13 +126,19 @@ const resource = { 'resourceCenter.tab.pod.emptyHint': 'If the list is empty, check whether workloads are created successfully and replicas are starting as expected.', 'resourceCenter.tab.network.title': 'Networking', - 'resourceCenter.tab.network.navDescription': 'Manage Service type, ports, and selectors', + 'resourceCenter.tab.network.navDescription': 'Manage Services, Ingresses, and traffic entry points', 'resourceCenter.tab.network.description': 'Manage traffic exposure in the team namespace and identify ports and entry points quickly.', 'resourceCenter.tab.network.listTitle': 'Network Resource List', - 'resourceCenter.tab.network.listDescription': 'Review service type, port exposure, and selector bindings, and edit YAML directly.', + 'resourceCenter.tab.network.listDescription': 'Review service type, port exposure, Ingress hosts, selector bindings, and edit YAML directly.', 'resourceCenter.tab.network.emptyTitle': 'No Network Resources Yet', - 'resourceCenter.tab.network.emptyDescription': 'After creating a Service with YAML, you can review ClusterIP, ports, and selectors here.', - 'resourceCenter.tab.network.emptyHint': 'If you need to expose traffic, create the workload first and then define the matching Service.', + 'resourceCenter.tab.network.emptyDescription': 'After creating a Service or Ingress with YAML, ports, hosts, and selectors appear here.', + 'resourceCenter.tab.network.emptyHint': 'If you need to expose traffic, create the workload first and then define the matching Service or Ingress.', + 'resourceCenter.tab.network.services': 'Services', + 'resourceCenter.tab.network.ingresses': 'Ingresses', + 'resourceCenter.tab.network.ingressClass': 'IngressClass', + 'resourceCenter.tab.network.hosts': 'Hosts', + 'resourceCenter.tab.network.tlsHosts': 'TLS Hosts', + 'resourceCenter.tab.network.backendServices': 'Backend Services', 'resourceCenter.tab.config.title': 'Configuration', 'resourceCenter.tab.config.navDescription': 'Unified view for ConfigMaps and Secrets', @@ -180,11 +186,11 @@ const resource = { 'resourceCenter.metrics.pod.errorHelper': 'Pods that need logs or events checked', 'resourceCenter.metrics.network.total': 'Network Objects', - 'resourceCenter.metrics.network.totalHelper': 'Service resources in the current team', + 'resourceCenter.metrics.network.totalHelper': 'Service and Ingress resources in the current team', 'resourceCenter.metrics.network.ports': 'Exposed Ports', 'resourceCenter.metrics.network.portsHelper': 'Total declared ports and protocols', 'resourceCenter.metrics.network.exposed': 'Externally Exposed', - 'resourceCenter.metrics.network.exposedHelper': 'Services that are not ClusterIP', + 'resourceCenter.metrics.network.exposedHelper': 'Services that are not ClusterIP plus Ingress entry points', 'resourceCenter.metrics.network.selectorless': 'No Selector', 'resourceCenter.metrics.network.selectorlessHelper': 'Requires manual confirmation of traffic binding', diff --git a/src/locales/zh-CN/resource.js b/src/locales/zh-CN/resource.js index f9db19145..15946306a 100644 --- a/src/locales/zh-CN/resource.js +++ b/src/locales/zh-CN/resource.js @@ -126,13 +126,19 @@ const resource = { 'resourceCenter.tab.pod.emptyHint': '如果这里为空,可以先检查工作负载是否创建完成或副本是否正常拉起。', 'resourceCenter.tab.network.title': '网络', - 'resourceCenter.tab.network.navDescription': 'Service 类型、端口与选择器管理', + 'resourceCenter.tab.network.navDescription': 'Service、Ingress 与流量入口管理', 'resourceCenter.tab.network.description': '管理团队命名空间下的网络暴露方式,快速识别端口与流量入口。', 'resourceCenter.tab.network.listTitle': '网络资源清单', - 'resourceCenter.tab.network.listDescription': '查看 Service 类型、端口暴露、Selector 绑定关系,并支持直接编辑 YAML。', + 'resourceCenter.tab.network.listDescription': '查看 Service 类型、端口暴露、Ingress 域名入口与 Selector 绑定关系,并支持直接编辑 YAML。', 'resourceCenter.tab.network.emptyTitle': '还没有网络资源', - 'resourceCenter.tab.network.emptyDescription': '通过 YAML 新建 Service 后,可以在这里集中查看 ClusterIP、端口和选择器信息。', - 'resourceCenter.tab.network.emptyHint': '如果要暴露服务,建议先创建工作负载,再补充对应的 Service 定义。', + 'resourceCenter.tab.network.emptyDescription': '通过 YAML 新建 Service 或 Ingress 后,可以在这里集中查看端口、域名入口和选择器信息。', + 'resourceCenter.tab.network.emptyHint': '如果要暴露服务,建议先创建工作负载,再补充对应的 Service 或 Ingress 定义。', + 'resourceCenter.tab.network.services': 'Services', + 'resourceCenter.tab.network.ingresses': 'Ingresses', + 'resourceCenter.tab.network.ingressClass': 'IngressClass', + 'resourceCenter.tab.network.hosts': 'Hosts', + 'resourceCenter.tab.network.tlsHosts': 'TLS Hosts', + 'resourceCenter.tab.network.backendServices': '后端服务', 'resourceCenter.tab.config.title': '配置', 'resourceCenter.tab.config.navDescription': 'ConfigMap 与 Secret 统一查看', @@ -180,11 +186,11 @@ const resource = { 'resourceCenter.metrics.pod.errorHelper': '需要重点查看日志或事件', 'resourceCenter.metrics.network.total': '网络对象数', - 'resourceCenter.metrics.network.totalHelper': '当前团队下的 Service 资源', + 'resourceCenter.metrics.network.totalHelper': '当前团队下的 Service 与 Ingress 资源', 'resourceCenter.metrics.network.ports': '暴露端口', 'resourceCenter.metrics.network.portsHelper': '已声明的端口与协议总数', 'resourceCenter.metrics.network.exposed': '外部暴露', - 'resourceCenter.metrics.network.exposedHelper': '非 ClusterIP 类型的 Service', + 'resourceCenter.metrics.network.exposedHelper': '非 ClusterIP Service 与 Ingress 入口', 'resourceCenter.metrics.network.selectorless': '无选择器', 'resourceCenter.metrics.network.selectorlessHelper': '需要人工确认流量绑定对象', diff --git a/src/models/teamResources.js b/src/models/teamResources.js index f1afa1634..9de51ec73 100644 --- a/src/models/teamResources.js +++ b/src/models/teamResources.js @@ -102,6 +102,14 @@ export default { const resources = [...configMaps, ...secrets]; yield put({ type: 'save', payload: { resources, total: resources.length } }); }, + *fetchNetworkResources({ payload }, { call, put }) { + const servicesRes = yield call(listNsResources, { ...payload, group: '', version: 'v1', resource: 'services' }); + const ingressesRes = yield call(listNsResources, { ...payload, group: 'networking.k8s.io', version: 'v1', resource: 'ingresses' }); + const services = (servicesRes && servicesRes.bean && servicesRes.bean.list) || []; + const ingresses = (ingressesRes && ingressesRes.bean && ingressesRes.bean.list) || []; + const resources = [...services, ...ingresses]; + yield put({ type: 'save', payload: { resources, total: resources.length } }); + }, *createResource({ payload, callback, handleError }, { call }) { const res = yield call(createNsResource, { ...payload, handleError }); if (res && callback) callback(res); diff --git a/src/pages/ResourceCenter/index.js b/src/pages/ResourceCenter/index.js index d215041cb..269d4f4ab 100644 --- a/src/pages/ResourceCenter/index.js +++ b/src/pages/ResourceCenter/index.js @@ -42,7 +42,7 @@ import HelmTab from './tabs/HelmTab'; helmReleaseHistory: teamResources.helmReleaseHistory, total: teamResources.total, currentEnterprise: enterprise.currentEnterprise, - resourceListLoading: loading.effects['teamResources/fetchResources'], + resourceListLoading: loading.effects['teamResources/fetchResources'] || loading.effects['teamResources/fetchNetworkResources'], configListLoading: loading.effects['teamResources/fetchConfigResources'], helmListLoading: loading.effects['teamResources/fetchHelmReleases'], resourceYamlLoading: loading.effects['teamResources/fetchResource'], @@ -374,6 +374,13 @@ class ResourceCenter extends PureComponent { }); return; } + if (tab === 'network') { + dispatch({ + type: 'teamResources/fetchNetworkResources', + payload: { team: teamName, region: regionName }, + }); + return; + } const resourceParams = tab === 'workload' ? { group: extra.group || workloadKindGroup || 'apps', version: 'v1', resource: extra.resource || workloadKind || 'deployments' } : TAB_RESOURCE_MAP[tab] || TAB_RESOURCE_MAP.workload; @@ -412,6 +419,13 @@ class ResourceCenter extends PureComponent { const kind = ((record && record.kind) || '').toLowerCase(); return { group: '', version: 'v1', resource: kind === 'secret' ? 'secrets' : 'configmaps' }; } + if (tab === 'network') { + const kind = ((record && record.kind) || '').toLowerCase(); + if (kind === 'ingress') { + return { group: 'networking.k8s.io', version: 'v1', resource: 'ingresses' }; + } + return { group: '', version: 'v1', resource: 'services' }; + } return this.getCurrentResourceParams(tab); }; @@ -651,9 +665,11 @@ class ResourceCenter extends PureComponent { } if (activeTab === 'network') { - const portCount = list.reduce((total, item) => total + ((item.ports || []).length || 0), 0); - const exposedCount = list.filter(item => item.type && item.type !== 'ClusterIP').length; - const selectorlessCount = list.filter(item => !item.selector || Object.keys(item.selector).length === 0).length; + const serviceList = list.filter(item => ((item.kind || 'Service').toLowerCase() === 'service')); + const ingressCount = list.filter(item => ((item.kind || '').toLowerCase() === 'ingress')).length; + const portCount = serviceList.reduce((total, item) => total + ((item.ports || []).length || 0), 0); + const exposedCount = serviceList.filter(item => item.type && item.type !== 'ClusterIP').length + ingressCount; + const selectorlessCount = serviceList.filter(item => !item.selector || Object.keys(item.selector).length === 0).length; return [ { label: formatMessage({ id: 'resourceCenter.metrics.network.total' }), value: list.length, helper: formatMessage({ id: 'resourceCenter.metrics.network.totalHelper' }), tone: 'default' }, { label: formatMessage({ id: 'resourceCenter.metrics.network.ports' }), value: portCount, helper: formatMessage({ id: 'resourceCenter.metrics.network.portsHelper' }), tone: 'running' }, @@ -827,6 +843,15 @@ class ResourceCenter extends PureComponent { })); }; + handleNetworkDetail = (record) => { + const kind = ((record && record.kind) || '').toLowerCase(); + if (kind === 'ingress') { + this.handleOpenResourceYaml(record, this.getRecordResourceParams(record, 'network')); + return; + } + this.jumpToServiceDetail(record); + }; + jumpToHelmDetail = (record, query = {}) => { const { dispatch } = this.props; const { teamName, regionName } = this.getParams(); @@ -2085,8 +2110,8 @@ class ResourceCenter extends PureComponent { onRefresh={() => this.fetchTabData(activeTab)} refreshLoading={tabLoading} onCreate={this.openCreateChooser} - onDetail={this.jumpToServiceDetail} - onEditYaml={record => this.handleOpenResourceYaml(record, { group: '', version: 'v1', resource: 'services' })} + onDetail={this.handleNetworkDetail} + onEditYaml={record => this.handleOpenResourceYaml(record, this.getRecordResourceParams(record, 'network'))} onDelete={this.handleDeleteResource} deletingName={deletingResourceName} yamlLoadingName={openingYamlName} diff --git a/src/pages/ResourceCenter/tabs/NetworkTab.js b/src/pages/ResourceCenter/tabs/NetworkTab.js index 17a179968..d7c5f4764 100644 --- a/src/pages/ResourceCenter/tabs/NetworkTab.js +++ b/src/pages/ResourceCenter/tabs/NetworkTab.js @@ -1,5 +1,5 @@ import React, { PureComponent } from 'react'; -import { Table, Tag, Popconfirm, Divider } from 'antd'; +import { Table, Tag, Popconfirm, Divider, Tabs } from 'antd'; import { formatMessage } from '@/utils/intl'; import ResourceToolbar from '../components/ResourceToolbar'; import AsyncTextAction from '../components/AsyncTextAction'; @@ -7,7 +7,40 @@ import styles from '../index.less'; import { getTablePagination, getTableScroll } from '../helpers'; import { formatBrowserLocalTime } from '../utils'; -const NETWORK_TABLE_SCROLL_X = 1250; +const { TabPane } = Tabs; + +const SERVICE_TABLE_SCROLL_X = 1250; +const INGRESS_TABLE_SCROLL_X = 1120; + +const isIngressResource = record => ((record && record.kind) || '').toLowerCase() === 'ingress'; + +const renderTagList = (values, className) => { + const list = (Array.isArray(values) ? values : []).filter(Boolean); + if (list.length === 0) { + return -; + } + return list.map(value => ( + {value} + )); +}; + +const renderServicePorts = (record) => { + const ports = Array.isArray(record.ports) ? record.ports : []; + if (ports.length === 0) { + return -; + } + return ports.map((port, index) => ( + {port.port}/{port.protocol || 'TCP'} + )); +}; + +const renderServiceSelector = (record) => { + return record.selector + ? Object.entries(record.selector).map(([key, selectorValue]) => ( + {key}={selectorValue} + )) + : -; +}; class NetworkTab extends PureComponent { render() { @@ -26,7 +59,30 @@ class NetworkTab extends PureComponent { emptyContent, } = this.props; - const columns = [ + const serviceData = (data || []).filter(record => !isIngressResource(record)); + const ingressData = (data || []).filter(isIngressResource); + + const renderActions = record => ( + + onEditYaml(record)}> + {formatMessage({ id: 'resourceCenter.common.edit' })} + + + {deletingName === record.name ? ( + + {formatMessage({ id: 'resourceCenter.common.delete' })} + + ) : ( + onDelete(record)}> + + {formatMessage({ id: 'resourceCenter.common.delete' })} + + + )} + + ); + + const serviceColumns = [ { title: formatMessage({ id: 'resourceCenter.common.name' }), dataIndex: 'name', @@ -43,22 +99,14 @@ class NetworkTab extends PureComponent { { title: formatMessage({ id: 'resourceCenter.detail.clusterIp', defaultMessage: 'Cluster IP' }), dataIndex: 'cluster_ip', key: 'cluster_ip', width: 150, render: value => {value || '-'} }, { title: formatMessage({ id: 'resourceCenter.common.ports' }), - dataIndex: 'ports', key: 'ports', width: 220, - render: ports => (Array.isArray(ports) ? ports : []).map((port, index) => ( - {port.port}/{port.protocol || 'TCP'} - )), + render: (_, record) => renderServicePorts(record), }, { title: formatMessage({ id: 'resourceCenter.common.selector' }), - dataIndex: 'selector', key: 'selector', - render: value => value - ? Object.entries(value).map(([key, selectorValue]) => ( - {key}={selectorValue} - )) - : -, + render: (_, record) => renderServiceSelector(record), }, { title: formatMessage({ id: 'resourceCenter.common.createdAt' }), @@ -72,26 +120,57 @@ class NetworkTab extends PureComponent { key: 'action', width: 140, fixed: 'right', - render: (_, record) => ( - - onEditYaml(record)}> - {formatMessage({ id: 'resourceCenter.common.edit' })} - - - {deletingName === record.name ? ( - - {formatMessage({ id: 'resourceCenter.common.delete' })} - - ) : ( - onDelete(record)}> - - {formatMessage({ id: 'resourceCenter.common.delete' })} - - - )} + render: (_, record) => renderActions(record), + }, + ]; + + const ingressColumns = [ + { + title: formatMessage({ id: 'resourceCenter.common.name' }), + dataIndex: 'name', + key: 'name', + width: 220, + fixed: 'left', + render: (text, record) => ( + onDetail(record)}> + {text} ), }, + { title: formatMessage({ id: 'resourceCenter.tab.network.ingressClass', defaultMessage: 'IngressClass' }), dataIndex: 'ingress_class', key: 'ingress_class', width: 160, render: value => value ? {value} : - }, + { + title: formatMessage({ id: 'resourceCenter.tab.network.hosts', defaultMessage: 'Hosts' }), + dataIndex: 'hosts', + key: 'hosts', + render: value => renderTagList(value, styles.tagInfo), + }, + { + title: formatMessage({ id: 'resourceCenter.tab.network.tlsHosts', defaultMessage: 'TLS Hosts' }), + dataIndex: 'tls_hosts', + key: 'tls_hosts', + render: value => renderTagList(value, styles.tagInfo), + }, + { + title: formatMessage({ id: 'resourceCenter.tab.network.backendServices', defaultMessage: 'Backend Services' }), + dataIndex: 'backend_services', + key: 'backend_services', + width: 220, + render: value => renderTagList(value, styles.tagPrimary), + }, + { + title: formatMessage({ id: 'resourceCenter.common.createdAt' }), + dataIndex: 'created_at', + key: 'created_at', + width: 180, + render: value => {formatBrowserLocalTime(value)}, + }, + { + title: formatMessage({ id: 'resourceCenter.common.operation' }), + key: 'action', + width: 140, + fixed: 'right', + render: (_, record) => renderActions(record), + }, ]; return ( @@ -105,17 +184,34 @@ class NetworkTab extends PureComponent { primaryActionLabel={formatMessage({ id: 'resourceCenter.common.createResource' })} onPrimaryAction={onCreate} /> - + + +
`Service-${record.name}`} + size="middle" + loading={refreshLoading} + scroll={getTableScroll(SERVICE_TABLE_SCROLL_X)} + pagination={getTablePagination(serviceData)} + locale={{ emptyText: emptyContent }} + /> + + +
`Ingress-${record.name}`} + size="middle" + loading={refreshLoading} + scroll={getTableScroll(INGRESS_TABLE_SCROLL_X)} + pagination={getTablePagination(ingressData)} + locale={{ emptyText: emptyContent }} + /> + + ); } From d304b422b1da6e14be4026c5ec5587b43553ff86 Mon Sep 17 00:00:00 2001 From: Qi Zhang Date: Mon, 25 May 2026 14:16:34 +0800 Subject: [PATCH 3/7] fix: fallback to installed plugins when market is unavailable Signed-off-by: Qi Zhang --- .../Extension/pluginCapacity/pluginTable.js | 77 +++++++++++++++---- 1 file changed, 63 insertions(+), 14 deletions(-) diff --git a/src/pages/Extension/pluginCapacity/pluginTable.js b/src/pages/Extension/pluginCapacity/pluginTable.js index 9a6789dbb..37f99c390 100644 --- a/src/pages/Extension/pluginCapacity/pluginTable.js +++ b/src/pages/Extension/pluginCapacity/pluginTable.js @@ -189,19 +189,59 @@ class Index extends PureComponent { loading: false }) } else { - this.setState({ - pluginList: [], - loading: false - }) + this.handleInstalledPluginList(eid); + } + }, + handleError: () => { + this.handleInstalledPluginList(eid); + } + }) + } + + normalizeInstalledPlugin = plugin => ({ + ...plugin, + plugin_id: plugin.plugin_id || plugin.name, + plugin_name: plugin.plugin_name || plugin.alias || plugin.display_name || plugin.name, + description: plugin.description || '', + installed: true, + installed_version: plugin.installed_version || plugin.version || '', + can_upgrade: false, + upgradeable: false, + latest_version: plugin.latest_version || '', + author: plugin.author || 'Rainbond 官方', + app_level: plugin.app_level || plugin.appLevel || 'enterprise' + }) + + handleInstalledPluginList = (eid, callback) => { + const { dispatch, regionName } = this.props; + dispatch({ + type: 'teamControl/fetchPluginUrl', + payload: { + enterprise_id: eid, + region_name: regionName, + }, + callback: res => { + const plugins = res && res.list && res.list.length > 0 + ? res.list.map(item => this.normalizeInstalledPlugin(item)) + : []; + this.setState({ + pluginList: plugins, + loading: false + }); + if (callback) { + callback(plugins); } }, handleError: () => { this.setState({ pluginList: [], loading: false - }) + }); + if (callback) { + callback([]); + } } - }) + }); } onJumpApp = (value, tab = 'upgrade') => { @@ -445,6 +485,18 @@ class Index extends PureComponent { }); } + completeInstallIfRunning = (pluginId, plugins) => { + const target = plugins.find(p => p.plugin_id === pluginId); + const status = target && `${target.status || ''}`.toUpperCase(); + if (target && status === 'RUNNING') { + this.stopInstallPolling(); + this.setState(prev => ({ + installingPlugins: { ...prev.installingPlugins, [pluginId]: false }, + installModalPhase: 'RUNNING', + })); + } + } + startInstallPolling = (pluginId) => { this.stopInstallPolling(); this.installPollTimer = setInterval(() => { @@ -461,14 +513,11 @@ class Index extends PureComponent { callback: res => { if (res && res.list && res.list.length > 0) { this.setState({ pluginList: res.list }); - const target = res.list.find(p => p.plugin_id === pluginId); - if (target && target.status === 'RUNNING') { - this.stopInstallPolling(); - this.setState(prev => ({ - installingPlugins: { ...prev.installingPlugins, [pluginId]: false }, - installModalPhase: 'RUNNING', - })); - } + this.completeInstallIfRunning(pluginId, res.list); + } else { + this.handleInstalledPluginList(eid, plugins => { + this.completeInstallIfRunning(pluginId, plugins); + }); } }, }); From fcf5d879a2bd03ab25d7dda1de6f2bf7f50655c5 Mon Sep 17 00:00:00 2001 From: Qi Zhang Date: Mon, 25 May 2026 15:55:44 +0800 Subject: [PATCH 4/7] fix: dedupe streamed event logs Signed-off-by: Qi Zhang --- src/components/LogProcress/index.js | 39 ++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/src/components/LogProcress/index.js b/src/components/LogProcress/index.js index 348dd8637..98c52f4c2 100644 --- a/src/components/LogProcress/index.js +++ b/src/components/LogProcress/index.js @@ -12,6 +12,7 @@ export default class Index extends PureComponent { this.state = {}; this.socketUrl = this.props.socketUrl; this.eventId = this.props.eventId; + this.socketLogKeys = new Set(); } escapeHtml = (str) => { @@ -23,6 +24,34 @@ export default class Index extends PureComponent { .replace(/"/g, '"') .replace(/'/g, '''); }; + getLogMessageText = data => { + if (!data || data.message === undefined || data.message === null) { + return ''; + } + if (typeof data.message === 'string') { + return data.message; + } + try { + return JSON.stringify(data.message); + } catch (e) { + return String(data.message); + } + }; + getLogKey = data => { + const time = data && data.time ? data.time : ''; + return `${time}-${this.getLogMessageText(data)}`; + }; + getHistoryLogKeys = () => { + const logs = this.props.list || []; + return logs.reduce((keys, item) => { + keys.add(this.getLogKey(item)); + return keys; + }, new Set()); + }; + getVisibleHistoryLogs = logs => logs.filter(item => { + const key = this.getLogKey(item); + return !this.socketLogKeys.has(key); + }); componentDidMount() { this.createTmpElement(); if (this.socketUrl) { @@ -44,6 +73,14 @@ export default class Index extends PureComponent { this.props.onFail && this.props.onFail(data); }, onMessage: data => { + const logKey = this.getLogKey(data); + if ( + this.socketLogKeys.has(logKey) || + this.getHistoryLogKeys().has(logKey) + ) { + return; + } + this.socketLogKeys.add(logKey); const ele = this.ele.cloneNode(); try { if (this.ref) { @@ -119,7 +156,7 @@ export default class Index extends PureComponent { this.ref = ref; }; render() { - const logs = this.props.list || []; + const logs = this.getVisibleHistoryLogs(this.props.list || []); return (
Date: Mon, 25 May 2026 17:31:33 +0800 Subject: [PATCH 5/7] fix: dedupe build log modal stream Signed-off-by: Qi Zhang --- .../Component/component/LogShow/index.js | 12 ++- .../Component/component/LogShow/logUtils.js | 63 +++++++++++++++ .../component/LogShow/logUtils.node.test.js | 78 +++++++++++++++++++ 3 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 src/pages/Component/component/LogShow/logUtils.js create mode 100644 src/pages/Component/component/LogShow/logUtils.node.test.js diff --git a/src/pages/Component/component/LogShow/index.js b/src/pages/Component/component/LogShow/index.js index 7917eb6d2..f9e62af84 100644 --- a/src/pages/Component/component/LogShow/index.js +++ b/src/pages/Component/component/LogShow/index.js @@ -7,6 +7,10 @@ import dateUtil from '../../../../utils/date-util'; import globalUtil from '../../../../utils/global'; import LogSocket from '../../../../utils/logSocket'; import styles from './index.less'; +import { + buildDuplicateLogBudget, + consumeDuplicateLogBudget +} from './logUtils'; @connect( ({ user }) => ({ @@ -26,6 +30,7 @@ class Index extends React.Component { dynamic: false }; this.state.dockerprogress = new Map(); + this.historyLogBudget = new Map(); } componentDidMount() { this.loadEventLog(); @@ -61,10 +66,12 @@ class Index extends React.Component { eventID: EventID }, callback: res => { + const historyLogs = (res && res.list) || []; + this.historyLogBudget = buildDuplicateLogBudget(historyLogs); if (res) { this.setState( { - logs: res.list + logs: historyLogs }, () => { if (showSocket) { @@ -80,6 +87,9 @@ class Index extends React.Component { } handleMessage = data => { const logs = this.state.logs || []; + if (consumeDuplicateLogBudget(this.historyLogBudget, data)) { + return; + } if (data.message.indexOf('id') !== -1) { try { const m = JSON.parse(data.message); diff --git a/src/pages/Component/component/LogShow/logUtils.js b/src/pages/Component/component/LogShow/logUtils.js new file mode 100644 index 000000000..dd154bc61 --- /dev/null +++ b/src/pages/Component/component/LogShow/logUtils.js @@ -0,0 +1,63 @@ +const normalizeLogMessage = log => { + if (!log || log.message === undefined || log.message === null) { + return ''; + } + + if (typeof log.message === 'string') { + return log.message; + } + + try { + return JSON.stringify(log.message); + } catch (e) { + return String(log.message); + } +}; + +const buildLogKey = log => { + if (!log) { + return ''; + } + + return [ + log.event_id || '', + log.step || '', + log.status || '', + log.level || '', + log.time || '', + normalizeLogMessage(log) + ].join('|'); +}; + +const buildDuplicateLogBudget = (logs = []) => + logs.reduce((budget, log) => { + const key = buildLogKey(log); + if (key) { + budget.set(key, (budget.get(key) || 0) + 1); + } + return budget; + }, new Map()); + +const consumeDuplicateLogBudget = (budget, log) => { + if (!budget) { + return false; + } + const key = buildLogKey(log); + const count = key ? budget.get(key) : 0; + if (!count) { + return false; + } + if (count === 1) { + budget.delete(key); + } else { + budget.set(key, count - 1); + } + return true; +}; + +module.exports = { + buildLogKey, + buildDuplicateLogBudget, + consumeDuplicateLogBudget, + normalizeLogMessage +}; diff --git a/src/pages/Component/component/LogShow/logUtils.node.test.js b/src/pages/Component/component/LogShow/logUtils.node.test.js new file mode 100644 index 000000000..f7643f4a3 --- /dev/null +++ b/src/pages/Component/component/LogShow/logUtils.node.test.js @@ -0,0 +1,78 @@ +const assert = require('assert'); +const { + buildLogKey, + buildDuplicateLogBudget, + consumeDuplicateLogBudget, + normalizeLogMessage +} = require('./logUtils'); + +const historyBudget = buildDuplicateLogBudget([ + { + event_id: 'event-a', + step: 'build-exector', + status: 'info', + level: 'info', + time: '2026-05-25T10:00:00Z', + message: 'Build app version from source code start' + }, + { + event_id: 'event-a', + step: 'build-exector', + status: 'info', + level: 'info', + time: '2026-05-25T10:00:01Z', + message: 'pull or clone code successfully, start code build' + } +]); + +assert.strictEqual( + consumeDuplicateLogBudget(historyBudget, { + event_id: 'event-a', + step: 'build-exector', + status: 'info', + level: 'info', + time: '2026-05-25T10:00:00Z', + message: 'Build app version from source code start' + }), + true, + 'socket logs already loaded from history should be skipped once' +); + +assert.strictEqual( + consumeDuplicateLogBudget(historyBudget, { + event_id: 'event-a', + step: 'build-exector', + status: 'info', + level: 'info', + time: '2026-05-25T10:00:02Z', + message: 'Build app version from source code start' + }), + false, + 'new repeated messages at a different time should still be shown' +); + +assert.strictEqual( + consumeDuplicateLogBudget(historyBudget, { + event_id: 'event-a', + step: 'build-exector', + status: 'info', + level: 'info', + time: '2026-05-25T10:00:04Z', + message: 'Starting CNB build' + }), + false, + 'new socket logs should be shown' +); + +assert.strictEqual( + normalizeLogMessage({ message: { id: 'layer-a', status: 'Downloading' } }), + '{"id":"layer-a","status":"Downloading"}', + 'object messages should use a stable key' +); + +assert.strictEqual( + buildLogKey({ event_id: 'event-a', time: 't1', message: 'same' }) === + buildLogKey({ event_id: 'event-a', time: 't2', message: 'same' }), + false, + 'time should keep separate log entries distinct' +); From 75e84fb09afbdf3161f04d56544f09d6e55e8d23 Mon Sep 17 00:00:00 2001 From: Qi Zhang Date: Mon, 25 May 2026 17:36:21 +0800 Subject: [PATCH 6/7] revert: restore build log handling Signed-off-by: Qi Zhang --- src/components/LogProcress/index.js | 39 +--------- .../Component/component/LogShow/index.js | 12 +-- .../Component/component/LogShow/logUtils.js | 63 --------------- .../component/LogShow/logUtils.node.test.js | 78 ------------------- 4 files changed, 2 insertions(+), 190 deletions(-) delete mode 100644 src/pages/Component/component/LogShow/logUtils.js delete mode 100644 src/pages/Component/component/LogShow/logUtils.node.test.js diff --git a/src/components/LogProcress/index.js b/src/components/LogProcress/index.js index 98c52f4c2..348dd8637 100644 --- a/src/components/LogProcress/index.js +++ b/src/components/LogProcress/index.js @@ -12,7 +12,6 @@ export default class Index extends PureComponent { this.state = {}; this.socketUrl = this.props.socketUrl; this.eventId = this.props.eventId; - this.socketLogKeys = new Set(); } escapeHtml = (str) => { @@ -24,34 +23,6 @@ export default class Index extends PureComponent { .replace(/"/g, '"') .replace(/'/g, '''); }; - getLogMessageText = data => { - if (!data || data.message === undefined || data.message === null) { - return ''; - } - if (typeof data.message === 'string') { - return data.message; - } - try { - return JSON.stringify(data.message); - } catch (e) { - return String(data.message); - } - }; - getLogKey = data => { - const time = data && data.time ? data.time : ''; - return `${time}-${this.getLogMessageText(data)}`; - }; - getHistoryLogKeys = () => { - const logs = this.props.list || []; - return logs.reduce((keys, item) => { - keys.add(this.getLogKey(item)); - return keys; - }, new Set()); - }; - getVisibleHistoryLogs = logs => logs.filter(item => { - const key = this.getLogKey(item); - return !this.socketLogKeys.has(key); - }); componentDidMount() { this.createTmpElement(); if (this.socketUrl) { @@ -73,14 +44,6 @@ export default class Index extends PureComponent { this.props.onFail && this.props.onFail(data); }, onMessage: data => { - const logKey = this.getLogKey(data); - if ( - this.socketLogKeys.has(logKey) || - this.getHistoryLogKeys().has(logKey) - ) { - return; - } - this.socketLogKeys.add(logKey); const ele = this.ele.cloneNode(); try { if (this.ref) { @@ -156,7 +119,7 @@ export default class Index extends PureComponent { this.ref = ref; }; render() { - const logs = this.getVisibleHistoryLogs(this.props.list || []); + const logs = this.props.list || []; return (
({ @@ -30,7 +26,6 @@ class Index extends React.Component { dynamic: false }; this.state.dockerprogress = new Map(); - this.historyLogBudget = new Map(); } componentDidMount() { this.loadEventLog(); @@ -66,12 +61,10 @@ class Index extends React.Component { eventID: EventID }, callback: res => { - const historyLogs = (res && res.list) || []; - this.historyLogBudget = buildDuplicateLogBudget(historyLogs); if (res) { this.setState( { - logs: historyLogs + logs: res.list }, () => { if (showSocket) { @@ -87,9 +80,6 @@ class Index extends React.Component { } handleMessage = data => { const logs = this.state.logs || []; - if (consumeDuplicateLogBudget(this.historyLogBudget, data)) { - return; - } if (data.message.indexOf('id') !== -1) { try { const m = JSON.parse(data.message); diff --git a/src/pages/Component/component/LogShow/logUtils.js b/src/pages/Component/component/LogShow/logUtils.js deleted file mode 100644 index dd154bc61..000000000 --- a/src/pages/Component/component/LogShow/logUtils.js +++ /dev/null @@ -1,63 +0,0 @@ -const normalizeLogMessage = log => { - if (!log || log.message === undefined || log.message === null) { - return ''; - } - - if (typeof log.message === 'string') { - return log.message; - } - - try { - return JSON.stringify(log.message); - } catch (e) { - return String(log.message); - } -}; - -const buildLogKey = log => { - if (!log) { - return ''; - } - - return [ - log.event_id || '', - log.step || '', - log.status || '', - log.level || '', - log.time || '', - normalizeLogMessage(log) - ].join('|'); -}; - -const buildDuplicateLogBudget = (logs = []) => - logs.reduce((budget, log) => { - const key = buildLogKey(log); - if (key) { - budget.set(key, (budget.get(key) || 0) + 1); - } - return budget; - }, new Map()); - -const consumeDuplicateLogBudget = (budget, log) => { - if (!budget) { - return false; - } - const key = buildLogKey(log); - const count = key ? budget.get(key) : 0; - if (!count) { - return false; - } - if (count === 1) { - budget.delete(key); - } else { - budget.set(key, count - 1); - } - return true; -}; - -module.exports = { - buildLogKey, - buildDuplicateLogBudget, - consumeDuplicateLogBudget, - normalizeLogMessage -}; diff --git a/src/pages/Component/component/LogShow/logUtils.node.test.js b/src/pages/Component/component/LogShow/logUtils.node.test.js deleted file mode 100644 index f7643f4a3..000000000 --- a/src/pages/Component/component/LogShow/logUtils.node.test.js +++ /dev/null @@ -1,78 +0,0 @@ -const assert = require('assert'); -const { - buildLogKey, - buildDuplicateLogBudget, - consumeDuplicateLogBudget, - normalizeLogMessage -} = require('./logUtils'); - -const historyBudget = buildDuplicateLogBudget([ - { - event_id: 'event-a', - step: 'build-exector', - status: 'info', - level: 'info', - time: '2026-05-25T10:00:00Z', - message: 'Build app version from source code start' - }, - { - event_id: 'event-a', - step: 'build-exector', - status: 'info', - level: 'info', - time: '2026-05-25T10:00:01Z', - message: 'pull or clone code successfully, start code build' - } -]); - -assert.strictEqual( - consumeDuplicateLogBudget(historyBudget, { - event_id: 'event-a', - step: 'build-exector', - status: 'info', - level: 'info', - time: '2026-05-25T10:00:00Z', - message: 'Build app version from source code start' - }), - true, - 'socket logs already loaded from history should be skipped once' -); - -assert.strictEqual( - consumeDuplicateLogBudget(historyBudget, { - event_id: 'event-a', - step: 'build-exector', - status: 'info', - level: 'info', - time: '2026-05-25T10:00:02Z', - message: 'Build app version from source code start' - }), - false, - 'new repeated messages at a different time should still be shown' -); - -assert.strictEqual( - consumeDuplicateLogBudget(historyBudget, { - event_id: 'event-a', - step: 'build-exector', - status: 'info', - level: 'info', - time: '2026-05-25T10:00:04Z', - message: 'Starting CNB build' - }), - false, - 'new socket logs should be shown' -); - -assert.strictEqual( - normalizeLogMessage({ message: { id: 'layer-a', status: 'Downloading' } }), - '{"id":"layer-a","status":"Downloading"}', - 'object messages should use a stable key' -); - -assert.strictEqual( - buildLogKey({ event_id: 'event-a', time: 't1', message: 'same' }) === - buildLogKey({ event_id: 'event-a', time: 't2', message: 'same' }), - false, - 'time should keep separate log entries distinct' -); From a5b74b79e8fb3f4d5cbc8233dc042c3598c132d0 Mon Sep 17 00:00:00 2001 From: Qi Zhang Date: Mon, 25 May 2026 18:23:44 +0800 Subject: [PATCH 7/7] fix: submit snapshot scaling rule fields Signed-off-by: Qi Zhang --- src/pages/Group/components/AppShareAppInfo.js | 12 +- .../Group/components/appShareFormHelpers.js | 108 ++++++++++++++++++ .../appShareFormHelpers.node.test.js | 47 ++++++++ src/pages/Group/components/appShareHelpers.js | 108 +----------------- 4 files changed, 167 insertions(+), 108 deletions(-) create mode 100644 src/pages/Group/components/appShareFormHelpers.js create mode 100644 src/pages/Group/components/appShareFormHelpers.node.test.js diff --git a/src/pages/Group/components/AppShareAppInfo.js b/src/pages/Group/components/AppShareAppInfo.js index d72e561e5..75726c852 100644 --- a/src/pages/Group/components/AppShareAppInfo.js +++ b/src/pages/Group/components/AppShareAppInfo.js @@ -131,7 +131,7 @@ class AppShareAppInfo extends PureComponent { const { getFieldDecorator, getFieldValue } = form; const pd16 = { padding: 16 }; if (app.extend_method_map) { - const steps = getFieldValue(`${ID}||step_node`); + const steps = getFieldValue(`extend||step_node||${ID}`); return (
@@ -152,7 +152,7 @@ class AppShareAppInfo extends PureComponent { })} style={pd16} > - {getFieldDecorator(`${ID}||min_node`, { + {getFieldDecorator(`extend||min_node||${ID}`, { initialValue: app.extend_method_map.min_node, rules: [ { @@ -181,7 +181,7 @@ class AppShareAppInfo extends PureComponent { })} style={pd16} > - {getFieldDecorator(`${ID}||max_node`, { + {getFieldDecorator(`extend||max_node||${ID}`, { initialValue: app.extend_method_map.max_node, rules: [ { @@ -210,7 +210,7 @@ class AppShareAppInfo extends PureComponent { })} style={pd16} > - {getFieldDecorator(`${ID}||step_node`, { + {getFieldDecorator(`extend||step_node||${ID}`, { initialValue: app.extend_method_map.step_node, rules: [ { @@ -239,7 +239,7 @@ class AppShareAppInfo extends PureComponent { })} style={pd16} > - {getFieldDecorator(`${ID}||init_memory`, { + {getFieldDecorator(`extend||init_memory||${ID}`, { initialValue: app.extend_method_map.init_memory || 0, rules: [ { @@ -272,7 +272,7 @@ class AppShareAppInfo extends PureComponent { })} style={pd16} > - {getFieldDecorator(`${ID}||container_cpu`, { + {getFieldDecorator(`extend||container_cpu||${ID}`, { initialValue: app.extend_method_map.container_cpu || 0, rules: [ { diff --git a/src/pages/Group/components/appShareFormHelpers.js b/src/pages/Group/components/appShareFormHelpers.js new file mode 100644 index 000000000..d247aa0c9 --- /dev/null +++ b/src/pages/Group/components/appShareFormHelpers.js @@ -0,0 +1,108 @@ +const cloneShareService = item => ({ + ...item, + extend_method_map: item.extend_method_map + ? { ...item.extend_method_map } + : item.extend_method_map, + dep_service_map_list: Array.isArray(item.dep_service_map_list) + ? item.dep_service_map_list.map(dep => ({ ...dep })) + : [], + service_connect_info_map_list: Array.isArray(item.service_connect_info_map_list) + ? item.service_connect_info_map_list.map(config => ({ ...config })) + : [], + service_env_map_list: Array.isArray(item.service_env_map_list) + ? item.service_env_map_list.map(config => ({ ...config })) + : [] +}); + +const collectShareServiceData = ({ + shareServiceList = [], + selectedShareKeys = [], + componentRefs = [] +}) => { + const shareServiceData = shareServiceList.map(cloneShareService); + let componentFormHasError = false; + + componentRefs.forEach(app => { + if (!app || !app.props || !app.props.form) { + return; + } + const apptab = app.props.tab; + let componentValues = null; + app.props.form.validateFields((errs, val) => { + if (errs) { + componentFormHasError = true; + return; + } + componentValues = val; + }); + if (componentFormHasError || !componentValues) { + return; + } + shareServiceData.forEach(option => { + const ID = option.service_id; + if (option.service_alias !== apptab) { + return; + } + Object.keys(componentValues).forEach(index => { + const indexarr = index.split('||'); + const firstInfo = indexarr && indexarr.length > 0 && indexarr[0]; + if (!firstInfo) { + return; + } + const isConnect = firstInfo === 'connect'; + const isEnv = firstInfo === 'env'; + + if (isConnect && indexarr[2] !== 'random') { + option.service_connect_info_map_list.forEach(serapp => { + if ( + serapp.attr_name === indexarr[1] && + String(ID) === String(indexarr[3]) + ) { + serapp[indexarr[2]] = componentValues[index]; + serapp.is_change = true; + } + }); + } + + if (isEnv) { + option.service_env_map_list.forEach(serapp => { + if ( + serapp.attr_name === indexarr[1] && + String(ID) === String(indexarr[2]) + ) { + serapp.attr_value = componentValues[index]; + serapp.is_change = true; + } + }); + } + + if ( + firstInfo === 'extend' && + option.extend_method_map && + String(ID) === String(indexarr[2]) + ) { + option.extend_method_map[indexarr[1]] = componentValues[index]; + } + }); + }); + }); + + const selectedShareServices = []; + selectedShareKeys.forEach(shareKey => { + shareServiceData.forEach(option => { + if (shareKey === option.service_share_uuid) { + selectedShareServices.push(option); + } + }); + }); + + return { + componentFormHasError, + shareServiceData, + selectedShareServices + }; +}; + +module.exports = { + collectShareServiceData +}; diff --git a/src/pages/Group/components/appShareFormHelpers.node.test.js b/src/pages/Group/components/appShareFormHelpers.node.test.js new file mode 100644 index 000000000..445dc165c --- /dev/null +++ b/src/pages/Group/components/appShareFormHelpers.node.test.js @@ -0,0 +1,47 @@ +const assert = require('assert'); +const { collectShareServiceData } = require('./appShareFormHelpers'); + +const componentRef = { + props: { + tab: 'web', + form: { + validateFields(callback) { + callback(null, { + 'extend||min_node||service-1': 2, + 'extend||max_node||service-1': 5, + 'extend||step_node||service-1': 2, + 'extend||init_memory||service-1': 1024, + 'extend||container_cpu||service-1': 500 + }); + } + } + } +}; + +const result = collectShareServiceData({ + shareServiceList: [ + { + service_id: 'service-1', + service_alias: 'web', + service_share_uuid: 'service-1+service-1', + extend_method_map: { + min_node: 1, + max_node: 3, + step_node: 1, + init_memory: 512, + container_cpu: 250 + } + } + ], + selectedShareKeys: ['service-1+service-1'], + componentRefs: [componentRef] +}); + +assert.strictEqual(result.componentFormHasError, false); +assert.deepStrictEqual(result.selectedShareServices[0].extend_method_map, { + min_node: 2, + max_node: 5, + step_node: 2, + init_memory: 1024, + container_cpu: 500 +}); diff --git a/src/pages/Group/components/appShareHelpers.js b/src/pages/Group/components/appShareHelpers.js index 9a2f85d1f..a6eb92f19 100644 --- a/src/pages/Group/components/appShareHelpers.js +++ b/src/pages/Group/components/appShareHelpers.js @@ -4,8 +4,13 @@ const { DEFAULT_SNAPSHOT_VERSION, buildNextSnapshotVersion } = require('./snapshotVersionHelpers'); +const { collectShareServiceData } = require('./appShareFormHelpers'); -export { DEFAULT_SNAPSHOT_VERSION, buildNextSnapshotVersion }; +export { + DEFAULT_SNAPSHOT_VERSION, + buildNextSnapshotVersion, + collectShareServiceData +}; export const appShareStateSelector = ({ user, @@ -32,104 +37,3 @@ export const validateShareVersion = value => { } return null; }; - -const cloneShareService = item => ({ - ...item, - extend_method_map: item.extend_method_map - ? { ...item.extend_method_map } - : item.extend_method_map, - dep_service_map_list: Array.isArray(item.dep_service_map_list) - ? item.dep_service_map_list.map(dep => ({ ...dep })) - : [], - service_connect_info_map_list: Array.isArray(item.service_connect_info_map_list) - ? item.service_connect_info_map_list.map(config => ({ ...config })) - : [], - service_env_map_list: Array.isArray(item.service_env_map_list) - ? item.service_env_map_list.map(config => ({ ...config })) - : [] -}); - -export const collectShareServiceData = ({ - shareServiceList = [], - selectedShareKeys = [], - componentRefs = [] -}) => { - const shareServiceData = shareServiceList.map(cloneShareService); - let componentFormHasError = false; - - componentRefs.forEach(app => { - if (!app || !app.props || !app.props.form) { - return; - } - const apptab = app.props.tab; - let componentValues = null; - app.props.form.validateFields((errs, val) => { - if (errs) { - componentFormHasError = true; - return; - } - componentValues = val; - }); - if (componentFormHasError || !componentValues) { - return; - } - shareServiceData.forEach(option => { - const ID = option.service_id; - if (option.service_alias !== apptab) { - return; - } - Object.keys(componentValues).forEach(index => { - const indexarr = index.split('||'); - const firstInfo = indexarr && indexarr.length > 0 && indexarr[0]; - if (!firstInfo) { - return; - } - const isConnect = firstInfo === 'connect'; - const isEnv = firstInfo === 'env'; - - if (isConnect && indexarr[2] !== 'random') { - option.service_connect_info_map_list.forEach(serapp => { - if ( - serapp.attr_name === indexarr[1] && - String(ID) === String(indexarr[3]) - ) { - serapp[indexarr[2]] = componentValues[index]; - serapp.is_change = true; - } - }); - } - - if (isEnv) { - option.service_env_map_list.forEach(serapp => { - if ( - serapp.attr_name === indexarr[1] && - String(ID) === String(indexarr[2]) - ) { - serapp.attr_value = componentValues[index]; - serapp.is_change = true; - } - }); - } - - if (firstInfo === 'extend' && option.extend_method_map) { - option.extend_method_map[indexarr[1]] = componentValues[index]; - } - }); - }); - }); - - const selectedShareServices = []; - selectedShareKeys.forEach(shareKey => { - shareServiceData.forEach(option => { - if (shareKey === option.service_share_uuid) { - selectedShareServices.push(option); - } - }); - }); - - return { - componentFormHasError, - shareServiceData, - selectedShareServices - }; -};