You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by ju...@apache.org on 2021/04/19 02:44:57 UTC

[apisix-dashboard] branch master updated: fix: show correct health checker (#1784)

This is an automated email from the ASF dual-hosted git repository.

juzhiyuan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-dashboard.git


The following commit(s) were added to refs/heads/master by this push:
     new 5e46f60  fix: show correct health checker (#1784)
5e46f60 is described below

commit 5e46f6072113fc2d41807256a6786ed7c84d6674
Author: 琚致远 <ju...@apache.org>
AuthorDate: Mon Apr 19 10:44:52 2021 +0800

    fix: show correct health checker (#1784)
    
    Co-authored-by: litesun <7s...@gmail.com>
---
 ...an-skip-upstream-when-select-service-id.spec.js |  23 ++--
 .../route/create-route-with-upstream.spec.js       |  13 ++-
 .../service/edit-service-with-upstream.spec.js     |   7 +-
 web/src/components/Upstream/UpstreamForm.tsx       | 129 +++++++++++----------
 .../components/Upstream/components/PassHost.tsx    |  10 +-
 .../Upstream/components/UpstreamSelector.tsx       |  10 +-
 .../Upstream/components/active-check/Host.tsx      |   5 -
 .../Upstream/components/active-check/HttpPath.tsx  |   7 --
 .../Upstream/components/active-check/Port.tsx      |   4 +-
 web/src/components/Upstream/locales/en-US.ts       |   7 +-
 web/src/components/Upstream/locales/zh-CN.ts       |   7 +-
 web/src/components/Upstream/service.ts             |  91 ++++++++++++++-
 web/src/components/Upstream/typings.d.ts           |   3 +-
 web/src/pages/Route/Create.tsx                     |   1 +
 web/src/pages/Route/components/Step1/MetaView.tsx  |  63 ++++++----
 .../Route/components/Step2/RequestRewriteView.tsx  |   8 +-
 web/src/pages/Route/locales/en-US.ts               |   2 +
 web/src/pages/Route/locales/zh-CN.ts               |   2 +
 web/src/pages/Route/service.ts                     |   7 --
 web/src/pages/Route/transform.ts                   |  17 ++-
 web/src/pages/Route/typing.d.ts                    |  30 +----
 web/src/pages/Service/Create.tsx                   |  14 ++-
 web/src/pages/Service/components/Step1.tsx         |   4 +-
 web/src/pages/Service/service.ts                   |   7 --
 web/src/pages/Upstream/Create.tsx                  |  17 +--
 web/src/pages/Upstream/components/Step1.tsx        |  13 +--
 web/src/pages/Upstream/locales/zh-CN.ts            |   2 +-
 web/src/pages/Upstream/service.ts                  |   4 +-
 web/src/pages/Upstream/transform.ts                |  83 -------------
 29 files changed, 297 insertions(+), 293 deletions(-)

diff --git a/web/cypress/integration/route/can-skip-upstream-when-select-service-id.spec.js b/web/cypress/integration/route/can-skip-upstream-when-select-service-id.spec.js
index d62cd87..aeb5118 100644
--- a/web/cypress/integration/route/can-skip-upstream-when-select-service-id.spec.js
+++ b/web/cypress/integration/route/can-skip-upstream-when-select-service-id.spec.js
@@ -61,7 +61,7 @@ context('Can select service_id skip upstream in route', () => {
     cy.get(this.domSelector.name).type(this.data.routeName);
     cy.contains('Next').click();
     cy.get(this.domSelector.upstreamSelector).click();
-    cy.contains('None').should('not.exist');
+    cy.get('.ant-select-item-option-disabled > .ant-select-item-option-content').contains('None');
 
     cy.contains('Previous').click();
     cy.wait(500);
@@ -91,9 +91,12 @@ context('Can select service_id skip upstream in route', () => {
     cy.contains(this.data.routeName).siblings().contains('Configure').click();
     cy.get(this.domSelector.serviceSelector).click();
     cy.contains('None').click();
+    cy.get(this.domSelector.notification).should('contain', 'Please check the configuration of binding service');
+    cy.get(this.domSelector.notificationCloseIcon).click();
+
     cy.contains('Next').click();
-    cy.get(this.domSelector.upstream_id).click();
-    cy.contains('None').should('not.exist');
+    cy.wait(500);
+    cy.get('[data-cy=upstream_selector]').click();
     cy.contains(this.data.upstreamName).click();
     cy.contains('Next').click();
     cy.contains('Next').click();
@@ -101,13 +104,7 @@ context('Can select service_id skip upstream in route', () => {
     cy.contains(this.data.submitSuccess);
   });
 
-  it('should delete upstream, service and route', function () {
-    cy.visit('/');
-    cy.contains('Service').click();
-    cy.contains(this.data.serviceName).siblings().contains('Delete').click();
-    cy.contains('button', 'Confirm').click();
-    cy.get(this.domSelector.notification).should('contain', this.data.deleteServiceSuccess);
-
+  it('should delete route, service and upstream', function () {
     cy.visit('/');
     cy.contains('Route').click();
     cy.contains(this.data.routeName).siblings().contains('More').click();
@@ -115,7 +112,13 @@ context('Can select service_id skip upstream in route', () => {
     cy.get(this.domSelector.deleteAlert).should('be.visible').within(() => {
       cy.contains('OK').click();
     });
+
     cy.get(this.domSelector.notification).should('contain', this.data.deleteRouteSuccess);
+    cy.visit('/');
+    cy.contains('Service').click();
+    cy.contains(this.data.serviceName).siblings().contains('Delete').click();
+    cy.contains('button', 'Confirm').click();
+    cy.get(this.domSelector.notification).should('contain', this.data.deleteServiceSuccess);
 
     cy.visit('/');
     cy.contains('Upstream').click();
diff --git a/web/cypress/integration/route/create-route-with-upstream.spec.js b/web/cypress/integration/route/create-route-with-upstream.spec.js
index 2a976b8..8a64a05 100644
--- a/web/cypress/integration/route/create-route-with-upstream.spec.js
+++ b/web/cypress/integration/route/create-route-with-upstream.spec.js
@@ -54,8 +54,7 @@ context('Create Route with Upstream', () => {
     cy.get(this.domSelector.input).should('be.disabled');
     // should enable Upstream input boxes after selecting Custom mode
     cy.get(this.domSelector.upstreamSelector).click();
-    cy.contains('Custom').click();
-    cy.get(this.domSelector.input).should('not.be.disabled');
+    cy.contains('.ant-select-item-option-content', 'Custom').click();
 
     cy.get(this.domSelector.nodes_0_host).clear().type(this.data.ip1);
     cy.get(this.domSelector.nodes_0_port).type(this.data.port);
@@ -77,7 +76,9 @@ context('Create Route with Upstream', () => {
     cy.contains(this.data.routeName).siblings().contains('Configure').click();
 
     cy.get(this.domSelector.name).should('value', this.data.routeName);
-    cy.contains('Next').click({ force: true });
+    cy.contains('Next').click({
+      force: true
+    });
 
     // check if the changes have been saved
     cy.get(this.domSelector.nodes_0_host).should('value', this.data.ip1);
@@ -87,7 +88,7 @@ context('Create Route with Upstream', () => {
     cy.get(this.domSelector.input).should('be.disabled');
 
     cy.contains(this.data.upstreamName).click();
-    cy.contains('Custom').click();
+    cy.contains('.ant-select-item-option-content', 'Custom').click();
     cy.get(this.domSelector.input).should('not.be.disabled');
 
     cy.get(this.domSelector.nodes_0_host).clear().type(this.data.ip2);
@@ -107,7 +108,9 @@ context('Create Route with Upstream', () => {
     cy.contains(this.data.routeName).siblings().contains('Configure').click();
     // ensure it has already changed to edit page
     cy.get(this.domSelector.name).should('value', this.data.routeName);
-    cy.contains('Next').click({ force: true });
+    cy.contains('Next').click({
+      force: true
+    });
     cy.get(this.domSelector.nodes_0_host).should('value', this.data.ip2);
   });
 
diff --git a/web/cypress/integration/service/edit-service-with-upstream.spec.js b/web/cypress/integration/service/edit-service-with-upstream.spec.js
index 66e9c2f..150a9e4 100644
--- a/web/cypress/integration/service/edit-service-with-upstream.spec.js
+++ b/web/cypress/integration/service/edit-service-with-upstream.spec.js
@@ -63,11 +63,14 @@ context('Edit Service with Upstream', () => {
     cy.contains('Search').click();
     cy.contains(this.data.serviceName).siblings().contains('Configure').click();
 
-    cy.get(this.domSelector.nodes_0_host).click({ force: true }).should('value', this.data.ip1);
+    cy.wait(500);
+    cy.get(this.domSelector.nodes_0_host).click({
+      force: true
+    }).should('value', this.data.ip1);
     cy.get(this.domSelector.input).should('be.disabled');
 
     cy.get(this.domSelector.upstreamSelector).click();
-    cy.contains('Custom').click();
+    cy.contains('.ant-select-item-option-content', 'Custom').click();
     cy.get(this.domSelector.nodes_0_host).should('not.be.disabled').clear().type(this.data.ip2);
     cy.get(this.domSelector.nodes_0_port).type(this.data.port);
     cy.get(this.domSelector.nodes_0_weight).type(this.data.weight);
diff --git a/web/src/components/Upstream/UpstreamForm.tsx b/web/src/components/Upstream/UpstreamForm.tsx
index 2cf0151..c8a606b 100644
--- a/web/src/components/Upstream/UpstreamForm.tsx
+++ b/web/src/components/Upstream/UpstreamForm.tsx
@@ -14,13 +14,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import { Divider, Form, Switch } from 'antd';
+import { Divider, Form, notification, Switch } from 'antd';
 import React, { useState, forwardRef, useImperativeHandle, useEffect } from 'react';
 import { useIntl } from 'umi';
 import type { FormInstance } from 'antd/es/form';
 
 import PanelSection from '@/components/PanelSection';
-import { transformRequest } from '@/pages/Upstream/transform';
 import PassiveCheck from './components/passive-check';
 import ActiveCheck from './components/active-check'
 import Nodes from './components/Nodes'
@@ -31,7 +30,7 @@ import UpstreamSelector from './components/UpstreamSelector';
 import Retries from './components/Retries';
 import PassHost from './components/PassHost';
 import TLSComponent from './components/TLS';
-import { transformUpstreamDataFromRequest } from './service';
+import { convertToRequestData } from './service';
 
 type Upstream = {
   name?: string;
@@ -46,14 +45,18 @@ type Props = {
   // FIXME: use proper typing
   ref?: any;
   required?: boolean;
+  neverReadonly?: boolean
 };
 
+/**
+ * UpstreamForm is used to reuse Upstream Form UI,
+ * before using this component, we need to execute the following command:
+ * form.setFieldsValue(convertToFormData(VALUE_FROM_API))
+*/
 const UpstreamForm: React.FC<Props> = forwardRef(
-  ({ form, disabled, list = [], showSelector, required = true }, ref) => {
+  ({ form, disabled = false, list = [], showSelector = false, required = true, neverReadonly = false }, ref) => {
     const { formatMessage } = useIntl();
-    const [readonly, setReadonly] = useState(
-      Boolean(form.getFieldValue('upstream_id')) || disabled,
-    );
+    const [readonly, setReadonly] = useState(false);
     const [hiddenForm, setHiddenForm] = useState(false);
 
     const timeoutFields = [
@@ -75,39 +78,58 @@ const UpstreamForm: React.FC<Props> = forwardRef(
     ];
 
     useImperativeHandle(ref, () => ({
-      getData: () => transformRequest(form.getFieldsValue()),
+      getData: () => convertToRequestData(form.getFieldsValue()),
     }));
 
-    useEffect(() => {
-      const formData = transformRequest(form.getFieldsValue()) || {};
-      const { upstream_id } = form.getFieldsValue();
+    const resetForm = (upstream_id: string) => {
+      if (upstream_id === undefined) {
+        return
+      }
 
+      if (!neverReadonly) {
+        setReadonly(!["Custom", "None"].includes(upstream_id) || disabled);
+      }
+
+      /**
+       * upstream_id === None <==> required === false
+       * No need to bind Upstream object.
+       * When creating Route and binds with a Service, no need to configure Upstream in Route.
+      */
       if (upstream_id === 'None') {
         setHiddenForm(true);
-        if (required) {
-          requestAnimationFrame(() => {
-            form.resetFields();
-            setHiddenForm(false);
-          });
-        }
-      } else {
-        if (upstream_id) {
-          requestAnimationFrame(() => {
-            const targetData = list.find((item) => item.id === upstream_id) as UpstreamComponent.ResponseData
-            if (targetData) {
-              form.setFieldsValue(transformUpstreamDataFromRequest(targetData));
-            }
-          });
-        }
-        if (!required && !Object.keys(formData).length) {
-          requestAnimationFrame(() => {
-            form.setFieldsValue({ upstream_id: 'None' });
-            setHiddenForm(true);
-          });
-        }
+        form.resetFields()
+        form.setFieldsValue({ upstream_id: 'None' })
+        return
+      }
+
+      setHiddenForm(false)
+
+      // NOTE: Use Ant Design's form object to set data automatically
+      if (upstream_id === "Custom") {
+        return
+      }
+
+      // NOTE: Set data from Upstream List (Upstream Selector)
+      if (list.length === 0) {
+        return
+      }
+      form.resetFields()
+      const targetData = list.find((item) => item.id === upstream_id) as UpstreamComponent.ResponseData
+      if (targetData) {
+        form.setFieldsValue(targetData);
       }
-      setReadonly(Boolean(upstream_id) || disabled);
-    }, [list]);
+    }
+
+    /**
+     * upstream_id
+     * - None: No need to bind Upstream to a resource (e.g Service).
+     * - Custom: Users could input values on UpstreamForm
+     * - Upstream ID from API
+    */
+    useEffect(() => {
+      const upstream_id = form.getFieldValue('upstream_id');
+      resetForm(upstream_id)
+    }, [form.getFieldValue('upstream_id'), list]);
 
     const ActiveHealthCheck = () => (
       <React.Fragment>
@@ -192,19 +214,26 @@ const UpstreamForm: React.FC<Props> = forwardRef(
             }
           </Form.Item>
           <Divider orientation="left" plain />
-          <Form.Item label={formatMessage({ id: 'page.upstream.step.healthyCheck.passive' })} name={['custom', 'checks', 'passive']} valuePropName="checked">
+          <Form.Item label={formatMessage({ id: 'page.upstream.step.healthyCheck.passive' })} name={['custom', 'checks', 'passive']} valuePropName="checked" tooltip={formatMessage({ id: 'component.upstream.other.health-check.passive-only' })}>
             <Switch disabled={readonly} />
           </Form.Item>
-          <Form.Item shouldUpdate noStyle>
+          <Form.Item shouldUpdate={(prev, next) => prev.custom?.checks?.passive !== next.custom?.checks?.passive} noStyle>
             {
               () => {
                 const passive = form.getFieldValue(['custom', 'checks', 'passive'])
+                const active = form.getFieldValue(['custom', 'checks', 'active'])
                 if (passive) {
                   /*
                   * When enable passive check, we should enable active check, too.
                   * When we use form.setFieldsValue to enable active check, error throws.
                   * We choose to alert users first, and need users to enable active check manually.
                   */
+                  if (!active) {
+                    notification.warn({
+                      message: formatMessage({ id: 'component.upstream.other.health-check.invalid' }),
+                      description: formatMessage({ id: 'component.upstream.other.health-check.passive-only' })
+                    })
+                  }
                   return <PassiveHealthCheck />
                 }
                 return null
@@ -225,32 +254,8 @@ const UpstreamForm: React.FC<Props> = forwardRef(
             list={list}
             disabled={disabled}
             required={required}
-            shouldUpdate={(prev, next) => {
-              setReadonly(Boolean(next.upstream_id));
-              if (prev.upstream_id !== next.upstream_id) {
-                const id = next.upstream_id;
-                if (id) {
-                  const targetData = list.find((item) => item.id === id) as UpstreamComponent.ResponseData
-                  if (targetData) {
-                    form.setFieldsValue(transformUpstreamDataFromRequest(targetData));
-                  }
-                  form.setFieldsValue({
-                    upstream_id: id,
-                  });
-                }
-              }
-              return prev.upstream_id !== next.upstream_id;
-            }}
-            onChange={(upstream_id) => {
-              setReadonly(Boolean(upstream_id));
-              setHiddenForm(Boolean(upstream_id === 'None'));
-              const targetData = list.find((item) => item.id === upstream_id) as UpstreamComponent.ResponseData
-              if (targetData) {
-                form.setFieldsValue(transformUpstreamDataFromRequest(targetData));
-              }
-              if (upstream_id === '') {
-                form.resetFields();
-              }
+            onChange={(nextUpstreamId) => {
+              resetForm(nextUpstreamId);
             }}
           />
         )}
diff --git a/web/src/components/Upstream/components/PassHost.tsx b/web/src/components/Upstream/components/PassHost.tsx
index a2f7e45..72b5821 100644
--- a/web/src/components/Upstream/components/PassHost.tsx
+++ b/web/src/components/Upstream/components/PassHost.tsx
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 import React from 'react'
-import { Form, Input, Select } from 'antd'
+import { Form, Input, notification, Select } from 'antd'
 import { useIntl } from 'umi'
 import type { FormInstance } from 'antd/lib/form'
 
@@ -79,6 +79,14 @@ const Component: React.FC<Props> = ({ form, readonly }) => {
               </Form.Item>
             );
           }
+
+          if (form.getFieldValue('pass_host') === 'node' && (form.getFieldValue('nodes') || []).length !== 1) {
+            notification.warning({
+              message: formatMessage({id: 'component.upstream.other.pass_host-with-multiple-nodes.title'}),
+              description: formatMessage({id: 'component.upstream.other.pass_host-with-multiple-nodes'})
+            })
+            form.setFieldsValue({pass_host: 'pass'})
+          }
           return null;
         }}
       </Form.Item>
diff --git a/web/src/components/Upstream/components/UpstreamSelector.tsx b/web/src/components/Upstream/components/UpstreamSelector.tsx
index 1f8b225..34f7fec 100644
--- a/web/src/components/Upstream/components/UpstreamSelector.tsx
+++ b/web/src/components/Upstream/components/UpstreamSelector.tsx
@@ -27,18 +27,16 @@ type Props = {
   list?: Upstream[];
   disabled?: boolean;
   required?: boolean;
-  shouldUpdate: (prev: any, next: any) => void;
   onChange: (id: string) => void
 }
 
-const Component: React.FC<Props> = ({ shouldUpdate, onChange, list = [], disabled, required }) => {
+const UpstreamSelector: React.FC<Props> = ({ onChange, list = [], disabled, required }) => {
   const { formatMessage } = useIntl()
 
   return (
     <Form.Item
       label={formatMessage({ id: 'page.upstream.step.select.upstream' })}
       name="upstream_id"
-      shouldUpdate={shouldUpdate as any}
     >
       <Select
         showSearch
@@ -49,11 +47,11 @@ const Component: React.FC<Props> = ({ shouldUpdate, onChange, list = [], disable
           item?.children.toLowerCase().includes(input.toLowerCase())
         }
       >
-        {Boolean(!required) && <Select.Option value={'None'}>None</Select.Option>}
+        <Select.Option value="None" disabled={required}>{formatMessage({id: 'component.upstream.other.none'})}</Select.Option>
         {[
           {
             name: formatMessage({ id: 'page.upstream.step.select.upstream.select.option' }),
-            id: '',
+            id: 'Custom',
           },
           ...list,
         ].map((item) => (
@@ -66,4 +64,4 @@ const Component: React.FC<Props> = ({ shouldUpdate, onChange, list = [], disable
   )
 }
 
-export default Component
+export default UpstreamSelector
diff --git a/web/src/components/Upstream/components/active-check/Host.tsx b/web/src/components/Upstream/components/active-check/Host.tsx
index f21a76b..148d60a 100644
--- a/web/src/components/Upstream/components/active-check/Host.tsx
+++ b/web/src/components/Upstream/components/active-check/Host.tsx
@@ -27,7 +27,6 @@ const Component: React.FC<Props> = ({ readonly }) => {
   return (
     <Form.Item
       label={formatMessage({ id: 'component.upstream.fields.checks.active.host' })}
-      required
       tooltip={formatMessage({ id: 'component.upstream.fields.checks.active.host.tooltip' })}
       style={{ marginBottom: 0 }}
     >
@@ -35,10 +34,6 @@ const Component: React.FC<Props> = ({ readonly }) => {
         name={['checks', 'active', 'host']}
         rules={[
           {
-            required: true,
-            message: formatMessage({ id: 'component.upstream.fields.checks.active.host.required' }),
-          },
-          {
             pattern: new RegExp(
               /^\\*?[0-9a-zA-Z-._]+$/,
               'g',
diff --git a/web/src/components/Upstream/components/active-check/HttpPath.tsx b/web/src/components/Upstream/components/active-check/HttpPath.tsx
index db5c2b6..b99457a 100644
--- a/web/src/components/Upstream/components/active-check/HttpPath.tsx
+++ b/web/src/components/Upstream/components/active-check/HttpPath.tsx
@@ -28,7 +28,6 @@ const Component: React.FC<Props> = ({ readonly }) => {
   return (
     <Form.Item
       label={formatMessage({ id: 'component.upstream.fields.checks.active.http_path' })}
-      required
       tooltip={formatMessage({
         id: 'component.upstream.fields.checks.active.http_path.tooltip',
       })}
@@ -36,12 +35,6 @@ const Component: React.FC<Props> = ({ readonly }) => {
       <Form.Item
         name={['checks', 'active', 'http_path']}
         noStyle
-        rules={[
-          {
-            required: true,
-            message: formatMessage({ id: 'component.upstream.fields.checks.active.http_path.placeholder' }),
-          },
-        ]}
         initialValue="/"
       >
         <Input
diff --git a/web/src/components/Upstream/components/active-check/Port.tsx b/web/src/components/Upstream/components/active-check/Port.tsx
index 8ca857b..030a6c7 100644
--- a/web/src/components/Upstream/components/active-check/Port.tsx
+++ b/web/src/components/Upstream/components/active-check/Port.tsx
@@ -25,8 +25,8 @@ type Props = {
 const Component: React.FC<Props> = ({ readonly }) => {
   const { formatMessage } = useIntl()
   return (
-    <Form.Item label={formatMessage({ id: 'component.upstream.fields.checks.active.port' })} required>
-      <Form.Item name={['checks', 'active', 'port']} noStyle initialValue={80} rules={[{ required: true, message: formatMessage({ id: "component.upstream.fields.checks.active.port.required" }) }]}>
+    <Form.Item label={formatMessage({ id: 'component.upstream.fields.checks.active.port' })}>
+      <Form.Item name={['checks', 'active', 'port']} noStyle initialValue={80}>
         <InputNumber
           placeholder={formatMessage({
             id: 'component.upstream.fields.checks.active.port',
diff --git a/web/src/components/Upstream/locales/en-US.ts b/web/src/components/Upstream/locales/en-US.ts
index 5387087..9f94442 100644
--- a/web/src/components/Upstream/locales/en-US.ts
+++ b/web/src/components/Upstream/locales/en-US.ts
@@ -52,7 +52,6 @@ export default {
   'component.upstream.fields.checks.active.host.scope': 'Only letters, numbers and . are supported',
 
   'component.upstream.fields.checks.active.port': 'Port',
-  'component.upstream.fields.checks.active.port.required': 'Please enter the port',
 
   'component.upstream.fields.checks.active.http_path': 'HTTP Path',
   'component.upstream.fields.checks.active.http_path.tooltip': 'The path that should be used when issuing the HTTP GET request to the target. The default value is /.',
@@ -90,4 +89,10 @@ export default {
 
   'component.upstream.fields.checks.passive.unhealthy.timeouts': 'Timeouts',
   'component.upstream.fields.checks.passive.unhealthy.timeouts.tooltip': 'Number of timeouts in proxied traffic to consider a target unhealthy, as observed by passive health checks.',
+
+  'component.upstream.other.none': 'None',
+  'component.upstream.other.pass_host-with-multiple-nodes.title': 'Please check the target node configuration',
+  'component.upstream.other.pass_host-with-multiple-nodes': 'When using a host name or IP in the target node list, make sure there is only one target node',
+  'component.upstream.other.health-check.passive-only': 'When passive health check is enabled, active health check needs to be enabled at the same time.',
+  'component.upstream.other.health-check.invalid': 'Please check the health check configuration',
 }
diff --git a/web/src/components/Upstream/locales/zh-CN.ts b/web/src/components/Upstream/locales/zh-CN.ts
index 5f93dcd..809a60a 100644
--- a/web/src/components/Upstream/locales/zh-CN.ts
+++ b/web/src/components/Upstream/locales/zh-CN.ts
@@ -52,7 +52,6 @@ export default {
   'component.upstream.fields.checks.active.host.scope': '仅支持字母、数字和 . ',
 
   'component.upstream.fields.checks.active.port': '端口',
-  'component.upstream.fields.checks.active.port.required': '请输入端口',
 
   'component.upstream.fields.checks.active.http_path': '请求路径',
   'component.upstream.fields.checks.active.http_path.tooltip': '向目标节点发出 HTTP GET 请求时应使用的路径。',
@@ -95,4 +94,10 @@ export default {
 
   'component.upstream.fields.checks.passive.unhealthy.timeouts': '超时时间',
   'component.upstream.fields.checks.passive.unhealthy.timeouts.tooltip': '根据被动健康检查的观察,在代理中认为目标不健康的超时次数。',
+
+  'component.upstream.other.none': '不选择(仅在绑定服务时可用)',
+  'component.upstream.other.pass_host-with-multiple-nodes.title': '请检查目标节点配置',
+  'component.upstream.other.pass_host-with-multiple-nodes': '当使用目标节点列表中的主机名或者 IP 时,请确认只有一个目标节点',
+  'component.upstream.other.health-check.passive-only': '启用被动健康检查时,需要同时启用主动健康检查。',
+  'component.upstream.other.health-check.invalid': '请检查健康检查配置',
 }
diff --git a/web/src/components/Upstream/service.ts b/web/src/components/Upstream/service.ts
index 2642100..098ed22 100644
--- a/web/src/components/Upstream/service.ts
+++ b/web/src/components/Upstream/service.ts
@@ -14,15 +14,23 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import cloneDeep from 'lodash/cloneDeep'
+import { notification } from 'antd';
+import { isNil, omitBy, omit, pick, cloneDeep } from 'lodash';
+import { formatMessage, request } from 'umi';
 
 /**
  * Because we have some `custom` field in Upstream Form, like custom.tls/custom.checks.active etc,
  * we need to transform data that doesn't have `custom` field to data contains `custom` field
 */
-export const transformUpstreamDataFromRequest = (originData: UpstreamComponent.ResponseData) => {
+export const convertToFormData = (originData: UpstreamComponent.ResponseData) => {
+  if (originData === undefined) {
+    // NOTE: When binding Service without Upstream configuration (None), originData === undefined
+    return undefined
+  }
+
   const data = cloneDeep(originData)
   data.custom = {}
+  data.upstream_id = "Custom"
 
   if (data.checks) {
     data.custom.checks = {}
@@ -40,5 +48,84 @@ export const transformUpstreamDataFromRequest = (originData: UpstreamComponent.R
     data.custom.tls = "enable"
   }
 
+  if (data.id) {
+    data.upstream_id = data.id;
+  }
+
   return data
 }
+
+/**
+ * Transform Upstream Form data from custom data to API needed data
+*/
+export const convertToRequestData = (
+  formData: UpstreamModule.RequestBody,
+): UpstreamModule.RequestBody | undefined | { upstream_id: string } => {
+  let data = omitBy(formData, isNil) as UpstreamModule.RequestBody;
+  data = omit(data, 'custom');
+
+  const {
+    type,
+    hash_on,
+    key,
+    k8s_deployment_info,
+    nodes,
+    pass_host,
+    upstream_host,
+    upstream_id = "Custom",
+    checks
+  } = data;
+
+  if (!["Custom", "None"].includes(upstream_id)) {
+    return { upstream_id };
+  }
+
+  data = omit(data, "upstream_id") as any
+
+  if (nodes && k8s_deployment_info) {
+    return undefined;
+  }
+
+  if (!nodes && !k8s_deployment_info) {
+    return undefined;
+  }
+
+  if (type === 'chash') {
+    if (!hash_on) {
+      return undefined;
+    }
+
+    if (hash_on !== 'consumer' && !key) {
+      return undefined;
+    }
+  }
+
+  if (pass_host === 'rewrite' && !upstream_host) {
+    return undefined;
+  }
+
+  if (checks?.passive && !checks.active) {
+    notification.error({
+      message: formatMessage({id: 'component.upstream.other.health-check.invalid'}),
+      description: formatMessage({id: 'component.upstream.other.health-check.passive-only'})
+    })
+    return undefined
+  }
+
+  if (nodes) {
+    // NOTE: https://github.com/ant-design/ant-design/issues/27396
+    data.nodes = data.nodes?.map((item) => {
+      return pick(item, ['host', 'port', 'weight']);
+    });
+    return data;
+  }
+
+  return undefined;
+};
+
+export const fetchUpstreamList = () => {
+  return request<Res<ResListData<UpstreamComponent.ResponseData>>>('/upstreams').then(({ data }) => ({
+    data: data.rows.map(row => convertToFormData(row)),
+    total: data.total_size,
+  }));
+};
diff --git a/web/src/components/Upstream/typings.d.ts b/web/src/components/Upstream/typings.d.ts
index bbe22d3..e7b2382 100644
--- a/web/src/components/Upstream/typings.d.ts
+++ b/web/src/components/Upstream/typings.d.ts
@@ -42,7 +42,7 @@ declare namespace UpstreamComponent {
   }
 
   type ResponseData = {
-    nodes: Node[];
+    nodes?: Node[];
     retries?: number;
     timeout?: Timeout;
     tls?: TLS;
@@ -61,6 +61,7 @@ declare namespace UpstreamComponent {
     desc?: string;
     service_name?: string;
     id?: string;
+    upstream_id?: string;
     // NOTE: custom field
     custom?: Record<string, any>;
   }
diff --git a/web/src/pages/Route/Create.tsx b/web/src/pages/Route/Create.tsx
index 8c478d1..8da9546 100644
--- a/web/src/pages/Route/Create.tsx
+++ b/web/src/pages/Route/Create.tsx
@@ -102,6 +102,7 @@ const Page: React.FC<Props> = (props) => {
       return (
         <Step1
           form={form1}
+          upstreamForm={form2}
           advancedMatchingRules={advancedMatchingRules}
           onChange={({ action, data }) => {
             if (action === 'redirectOptionChange') {
diff --git a/web/src/pages/Route/components/Step1/MetaView.tsx b/web/src/pages/Route/components/Step1/MetaView.tsx
index b377f05..4cbd64a 100644
--- a/web/src/pages/Route/components/Step1/MetaView.tsx
+++ b/web/src/pages/Route/components/Step1/MetaView.tsx
@@ -16,7 +16,7 @@
  */
 import React, { useEffect, useState } from 'react';
 import Form from 'antd/es/form';
-import { Input, Switch, Select, Button, Tag, AutoComplete, Row, Col } from 'antd';
+import { Input, Switch, Select, Button, Tag, AutoComplete, Row, Col, notification } from 'antd';
 import { useIntl } from 'umi';
 
 import PanelSection from '@/components/PanelSection';
@@ -24,7 +24,7 @@ import { FORM_ITEM_WITHOUT_LABEL } from '@/pages/Route/constants';
 import LabelsDrawer from '@/components/LabelsfDrawer';
 import { fetchLabelList, fetchServiceList } from '../../service';
 
-const MetaView: React.FC<RouteModule.Step1PassProps> = ({ disabled, form, isEdit, onChange = () => { } }) => {
+const MetaView: React.FC<RouteModule.Step1PassProps> = ({ disabled, form, isEdit, upstreamForm, onChange = () => { } }) => {
   const { formatMessage } = useIntl();
   const [visible, setVisible] = useState(false);
   const [labelList, setLabelList] = useState<LabelList>({});
@@ -285,27 +285,44 @@ const MetaView: React.FC<RouteModule.Step1PassProps> = ({ disabled, form, isEdit
   )
 
   const ServiceSelector: React.FC = () => (
-    <Form.Item label={formatMessage({ id: 'page.route.service' })} tooltip={formatMessage({id: 'page.route.fields.service_id.tooltip'})}>
-      <Row>
-        <Col span={5}>
-          <Form.Item noStyle name="service_id">
-            <Select disabled={disabled}>
-              {/* TODO: value === '' means  no service_id select, need to find a better way */}
-              <Select.Option value="" key={Math.random().toString(36).substring(7)}>
-                {formatMessage({ id: "page.route.service.none" })}
-              </Select.Option>
-              {serviceList.map((item) => {
-                return (
-                  <Select.Option value={item.id} key={item.id}>
-                    {item.name}
-                  </Select.Option>
-                );
-              })}
-            </Select>
-          </Form.Item>
-        </Col>
-      </Row>
-    </Form.Item>
+    <React.Fragment>
+      <Form.Item label={formatMessage({ id: 'page.route.service' })} tooltip={formatMessage({ id: 'page.route.fields.service_id.tooltip' })}>
+        <Row>
+          <Col span={5}>
+            <Form.Item noStyle name="service_id">
+              <Select disabled={disabled}>
+                {/* TODO: value === '' means  no service_id select, need to find a better way */}
+                <Select.Option value="" key={Math.random().toString(36).substring(7)}>
+                  {formatMessage({ id: "page.route.service.none" })}
+                </Select.Option>
+                {serviceList.map((item) => {
+                  return (
+                    <Select.Option value={item.id} key={item.id}>
+                      {item.name}
+                    </Select.Option>
+                  );
+                })}
+              </Select>
+            </Form.Item>
+          </Col>
+        </Row>
+      </Form.Item>
+      <Form.Item noStyle shouldUpdate={(prev, next) => {
+        if (next.service_id === '') {
+          const upstream_id = upstreamForm?.getFieldValue('upstream_id')
+          if (upstream_id === 'None') {
+            notification.warning({
+              message: formatMessage({ id: 'page.route.fields.service_id.invalid' }),
+              description: formatMessage({ id: 'page.route.fields.service_id.without-upstream' })
+            })
+            form.setFieldsValue({ service_id: prev.service_id })
+          }
+        }
+        return prev.service_id !== next.service_id
+      }}>
+        {() => null}
+      </Form.Item>
+    </React.Fragment>
   )
 
   return (
diff --git a/web/src/pages/Route/components/Step2/RequestRewriteView.tsx b/web/src/pages/Route/components/Step2/RequestRewriteView.tsx
index 2e6a458..f3aa1ad 100644
--- a/web/src/pages/Route/components/Step2/RequestRewriteView.tsx
+++ b/web/src/pages/Route/components/Step2/RequestRewriteView.tsx
@@ -15,9 +15,9 @@
  * limitations under the License.
  */
 import React, { useEffect, useState } from 'react';
-import UpstreamForm from '@/components/Upstream';
 
-import { fetchUpstreamList } from '../../service';
+import UpstreamForm from '@/components/Upstream';
+import { fetchUpstreamList } from '@/components/Upstream/service';
 
 const RequestRewriteView: React.FC<RouteModule.Step2PassProps> = ({
   form,
@@ -25,9 +25,9 @@ const RequestRewriteView: React.FC<RouteModule.Step2PassProps> = ({
   disabled,
   hasServiceId = false,
 }) => {
-  const [list, setList] = useState<UpstreamModule.RequestBody[]>([]);
+  const [list, setList] = useState<UpstreamComponent.ResponseData[]>([]);
   useEffect(() => {
-    fetchUpstreamList().then(({ data }) => setList(data));
+    fetchUpstreamList().then(({ data }) => setList(data as UpstreamComponent.ResponseData[]));
   }, []);
   return (
     <UpstreamForm
diff --git a/web/src/pages/Route/locales/en-US.ts b/web/src/pages/Route/locales/en-US.ts
index 06de908..2609800 100644
--- a/web/src/pages/Route/locales/en-US.ts
+++ b/web/src/pages/Route/locales/en-US.ts
@@ -171,6 +171,8 @@ export default {
 
   'page.route.advanced-match.operator.sample.IN': 'Please enter an array, e.g ["1", "2"]',
   'page.route.advanced-match.operator.sample.~~': 'Please enter a regular expression, e.g [a-z]+',
+  'page.route.fields.service_id.invalid': 'Please check the configuration of binding service',
+  'page.route.fields.service_id.without-upstream': 'If you do not bind the service, you must set the Upstream (Step 2)',
   'page.route.advanced-match.tooltip': 'It supports route matching through request headers, request parameters and cookies, and can be applied to scenarios such as grayscale publishing and blue-green testing.',
 
   'page.route.fields.custom.redirectOption.tooltip': 'This is related to redirect plugin',
diff --git a/web/src/pages/Route/locales/zh-CN.ts b/web/src/pages/Route/locales/zh-CN.ts
index 8228f60..3c134bc 100644
--- a/web/src/pages/Route/locales/zh-CN.ts
+++ b/web/src/pages/Route/locales/zh-CN.ts
@@ -170,6 +170,8 @@ export default {
 
   'page.route.advanced-match.operator.sample.IN': '请输入数组,示例:["1", "2"]',
   'page.route.advanced-match.operator.sample.~~': '请输入正则表达式,示例:[a-z]+',
+  'page.route.fields.service_id.invalid': '请检查路由绑定的服务',
+  'page.route.fields.service_id.without-upstream': '如果不绑定服务,则必须设置上游服务(步骤 2)',
   'page.route.advanced-match.tooltip': '支持通过请求头,请求参数、Cookie 进行路由匹配,可应用于灰度发布,蓝绿测试等场景。',
 
   'page.route.fields.custom.redirectOption.tooltip': '在此配置 redirect 插件',
diff --git a/web/src/pages/Route/service.ts b/web/src/pages/Route/service.ts
index f2b291f..99144cd 100644
--- a/web/src/pages/Route/service.ts
+++ b/web/src/pages/Route/service.ts
@@ -72,13 +72,6 @@ export const checkUniqueName = (name = '', exclude = '') =>
     ),
   });
 
-export const fetchUpstreamList = () => {
-  return request<Res<ResListData<UpstreamModule.RequestBody>>>('/upstreams').then(({ data }) => ({
-    data: data.rows,
-    total: data.total_size,
-  }));
-};
-
 export const fetchUpstreamItem = (sid: string) => {
   return request(`/upstreams/${sid}`).then(({ nodes, timeout, id }) => {
     return {
diff --git a/web/src/pages/Route/transform.ts b/web/src/pages/Route/transform.ts
index b7f62b0..4b03caa 100644
--- a/web/src/pages/Route/transform.ts
+++ b/web/src/pages/Route/transform.ts
@@ -22,6 +22,7 @@ import {
   URI_REWRITE_TYPE,
   HOST_REWRITE_TYPE
 } from '@/pages/Route/constants';
+import { convertToFormData } from '@/components/Upstream/service';
 
 export const transformProxyRewrite2Plugin = (data: RouteModule.ProxyRewrite): RouteModule.ProxyRewrite => {
   let omitFieldsList: string[] = ['kvHeaders'];
@@ -178,11 +179,16 @@ export const transformStepData = ({
     unset(data.plugins, ['proxy-rewrite']);
   }
 
-  if (Object.keys(redirect).length === 0 || redirect.http_to_https) {
+  if ((Object.keys(redirect).length === 0 || redirect.http_to_https) && form2Data) {
+    /**
+     * Due to convertToRequestData under the Upstream component,
+     * if upstream_id === Custom or None, it will be omitted.
+     * So upstream_id here mush be a valid Upstream ID from API.
+    */
     if (form2Data.upstream_id) {
-      data.upstream_id = form2Data.upstream_id;
+      data.upstream_id = form2Data.upstream_id
     } else {
-      data.upstream = form2Data;
+      data.upstream = form2Data
     }
 
     if (redirect.http_to_https) {
@@ -207,7 +213,6 @@ export const transformStepData = ({
       'hostRewriteType',
       'proxyRewrite',
       service_id.length === 0 ? 'service_id' : '',
-      form2Data.upstream_id === 'None' ? 'upstream_id' : '',
       !Object.keys(data.plugins || {}).length ? 'plugins' : '',
       !Object.keys(data.script || {}).length ? 'script' : '',
       form1Data.hosts.filter(Boolean).length === 0 ? 'hosts' : '',
@@ -328,10 +333,10 @@ export const transformRouteData = (data: RouteModule.Body) => {
   const advancedMatchingRules: RouteModule.MatchingRule[] = transformVarsToRules(vars);
 
   if (upstream && Object.keys(upstream).length) {
-    upstream.upstream_id = '';
+    upstream.upstream_id = 'Custom';
   }
 
-  const form2Data: RouteModule.Form2Data = upstream || { upstream_id };
+  const form2Data: UpstreamComponent.ResponseData = convertToFormData(upstream) || { upstream_id: upstream_id || 'None' };
 
   const { plugins, script, plugin_config_id = '' } = data;
 
diff --git a/web/src/pages/Route/typing.d.ts b/web/src/pages/Route/typing.d.ts
index ea27938..0355c2c 100644
--- a/web/src/pages/Route/typing.d.ts
+++ b/web/src/pages/Route/typing.d.ts
@@ -73,19 +73,8 @@ declare namespace RouteModule {
     host?: string;
     hosts: string[];
     remote_addrs: string[];
+    upstream: UpstreamComponent.ResponseData;
     vars: [string, Operator, string | any[]][];
-    upstream: {
-      upstream_id?: string;
-      type: 'roundrobin' | 'chash' | 'ewma';
-      hash_on?: string;
-      key?: string;
-      nodes: Record<string, number>;
-      timeout: {
-        connect: number;
-        send: number;
-        read: number;
-      };
-    };
     upstream_path?: {
       type?: string;
       from?: string;
@@ -110,6 +99,7 @@ declare namespace RouteModule {
 
   type Step1PassProps = {
     form: FormInstance;
+    upstreamForm?: FormInstance;
     advancedMatchingRules: MatchingRule[];
     disabled?: boolean;
     isEdit?: boolean;
@@ -161,23 +151,9 @@ declare namespace RouteModule {
     hasServiceId: boolean;
   };
 
-  type Form2Data = {
-    type: 'roundrobin' | 'chash' | 'ewma';
-    hash_on?: string;
-    key?: string;
-    upstreamPath?: string;
-    upstream_id?: string;
-    timeout: {
-      connect: number;
-      send: number;
-      read: number;
-    };
-    nodes: Record<string, number>;
-  };
-
   type RequestData = {
     form1Data: Form1Data;
-    form2Data: Form2Data;
+    form2Data: UpstreamComponent.ResponseData;
     step3Data: Step3Data;
     advancedMatchingRules: MatchingRule[];
   };
diff --git a/web/src/pages/Service/Create.tsx b/web/src/pages/Service/Create.tsx
index 4d1dbc2..ab977a9 100644
--- a/web/src/pages/Service/Create.tsx
+++ b/web/src/pages/Service/Create.tsx
@@ -22,6 +22,7 @@ import { omit } from 'lodash';
 
 import ActionBar from '@/components/ActionBar';
 import PluginPage from '@/components/Plugin';
+import { convertToFormData } from '@/components/Upstream/service';
 import Preview from './components/Preview';
 import Step1 from './components/Step1';
 import { create, update, fetchItem } from './service';
@@ -48,11 +49,11 @@ const Page: React.FC = (props) => {
     const { serviceId } = (props as any).match.params;
     if (serviceId) {
       fetchItem(serviceId).then(({ data }) => {
-        if (data.upstream_id && data.upstream_id !== '') {
-          upstreamForm.setFieldsValue({ upstream_id: data.upstream_id });
+        if (data.upstream_id) {
+          upstreamForm.setFieldsValue({ upstream_id: data.upstream_id })
         }
         if (data.upstream) {
-          upstreamForm.setFieldsValue(data.upstream);
+          upstreamForm.setFieldsValue(convertToFormData(data.upstream))
         }
         form.setFieldsValue(omit(data, ['upstream_id', 'upstream', 'plugins']));
         setPlugins(data.plugins || {});
@@ -67,6 +68,9 @@ const Page: React.FC = (props) => {
     };
 
     const upstreamFormData = upstreamRef.current?.getData();
+    if (!upstreamFormData) {
+      return
+    }
     if (!upstreamFormData.upstream_id) {
       data.upstream = upstreamFormData;
     } else {
@@ -78,8 +82,8 @@ const Page: React.FC = (props) => {
       .then(() => {
         notification.success({
           message: `${serviceId
-              ? formatMessage({ id: 'component.global.edit' })
-              : formatMessage({ id: 'component.global.create' })
+            ? formatMessage({ id: 'component.global.edit' })
+            : formatMessage({ id: 'component.global.create' })
             } ${formatMessage({ id: 'menu.service' })} ${formatMessage({
               id: 'component.status.success',
             })}`,
diff --git a/web/src/pages/Service/components/Step1.tsx b/web/src/pages/Service/components/Step1.tsx
index 12b38a0..3817456 100644
--- a/web/src/pages/Service/components/Step1.tsx
+++ b/web/src/pages/Service/components/Step1.tsx
@@ -19,7 +19,7 @@ import { Form, Input } from 'antd';
 import { useIntl } from 'umi';
 
 import UpstreamForm from '@/components/Upstream';
-import { fetchUpstreamList } from '../service';
+import { fetchUpstreamList } from '@/components/Upstream/service';
 
 const FORM_LAYOUT = {
   labelCol: {
@@ -37,7 +37,7 @@ const Step1: React.FC<ServiceModule.Step1PassProps> = ({
   disabled,
 }) => {
   const { formatMessage } = useIntl();
-  const [list, setList] = useState<UpstreamModule.RequestBody[]>([]);
+  const [list, setList] = useState<UpstreamComponent.ResponseData[]>([]);
   useEffect(() => {
     fetchUpstreamList().then(({ data }) => setList(data));
   }, []);
diff --git a/web/src/pages/Service/service.ts b/web/src/pages/Service/service.ts
index bda706e..659c3ef 100644
--- a/web/src/pages/Service/service.ts
+++ b/web/src/pages/Service/service.ts
@@ -28,13 +28,6 @@ export const fetchList = ({ current = 1, pageSize = 10, ...res }) =>
     total: data.total_size,
   }));
 
-export const fetchUpstreamList = () => {
-  return request<Res<ResListData<UpstreamModule.RequestBody>>>('/upstreams').then(({ data }) => ({
-    data: data.rows,
-    total: data.total_size,
-  }));
-};
-
 export const create = (data: ServiceModule.Entity) =>
   request('/services', {
     method: 'POST',
diff --git a/web/src/pages/Upstream/Create.tsx b/web/src/pages/Upstream/Create.tsx
index 9ab672e..b276d74 100644
--- a/web/src/pages/Upstream/Create.tsx
+++ b/web/src/pages/Upstream/Create.tsx
@@ -16,12 +16,10 @@
  */
 import React, { useState, useEffect, useRef } from 'react';
 import { PageContainer } from '@ant-design/pro-layout';
-import { Card, Steps, notification, Form, Button } from 'antd';
+import { Card, Steps, notification, Form } from 'antd';
 import { history, useIntl } from 'umi';
-import { QuestionCircleOutlined } from '@ant-design/icons';
 
 import ActionBar from '@/components/ActionBar';
-import { transformUpstreamDataFromRequest } from '@/components/Upstream/service';
 
 import Step1 from './components/Step1';
 import { fetchOne, create, update } from './service';
@@ -36,8 +34,8 @@ const Page: React.FC = (props) => {
     const { id } = (props as any).match.params;
 
     if (id) {
-      fetchOne(id).then(({ data }) => {
-        form1.setFieldsValue(transformUpstreamDataFromRequest(data));
+      fetchOne(id).then(data => {
+        form1.setFieldsValue(data);
       });
     }
   }, []);
@@ -81,13 +79,6 @@ const Page: React.FC = (props) => {
         title={(props as any).match.params.id
           ? formatMessage({ id: 'page.upstream.configure' })
           : formatMessage({ id: 'page.upstream.create' })}
-
-        extra={
-          // TODO: support Document modal
-          <Button type="default" disabled>
-            <QuestionCircleOutlined />
-            {formatMessage({ id: 'component.document' })}
-          </Button>}
       >
         <Card bordered={false}>
           <Steps current={step - 1} style={{ marginBottom: 30 }}>
@@ -95,7 +86,7 @@ const Page: React.FC = (props) => {
             <Steps.Step title={formatMessage({ id: 'page.upstream.create.preview' })} />
           </Steps>
 
-          {step === 1 && <Step1 form={form1} upstreamRef={upstreamRef} />}
+          {step === 1 && <Step1 form={form1} upstreamRef={upstreamRef} neverReadonly />}
           {step === 2 && <Step1 form={form1} upstreamRef={upstreamRef} disabled />}
         </Card>
       </PageContainer>
diff --git a/web/src/pages/Upstream/components/Step1.tsx b/web/src/pages/Upstream/components/Step1.tsx
index 40f3c31..226e71c 100644
--- a/web/src/pages/Upstream/components/Step1.tsx
+++ b/web/src/pages/Upstream/components/Step1.tsx
@@ -14,27 +14,22 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import React, { useEffect, useState } from 'react';
+import React from 'react';
 import { Form, Input } from 'antd';
 import type { FormInstance } from 'antd/lib/form';
 import { useIntl } from 'umi';
 
 import UpstreamForm from '@/components/Upstream';
-import { fetchList } from '../service';
 
 type Props = {
   form: FormInstance;
   disabled?: boolean;
   upstreamRef?: React.MutableRefObject<any>;
+  neverReadonly?: boolean;
 };
 
-const Step1: React.FC<Props> = ({ form, disabled, upstreamRef }) => {
+const Step1: React.FC<Props> = ({ form, disabled, upstreamRef, neverReadonly }) => {
   const { formatMessage } = useIntl();
-  const [list, setList] = useState<UpstreamModule.RequestBody[]>([]);
-
-  useEffect(() => {
-    fetchList({}).then(({ data }) => setList(data));
-  }, []);
 
   return (
     <>
@@ -56,7 +51,7 @@ const Step1: React.FC<Props> = ({ form, disabled, upstreamRef }) => {
           />
         </Form.Item>
       </Form>
-      <UpstreamForm ref={upstreamRef} form={form} disabled={disabled} list={list} />
+      <UpstreamForm ref={upstreamRef} form={form} disabled={disabled} neverReadonly={neverReadonly} />
     </>
   );
 };
diff --git a/web/src/pages/Upstream/locales/zh-CN.ts b/web/src/pages/Upstream/locales/zh-CN.ts
index 73611a2..eb0c19a 100644
--- a/web/src/pages/Upstream/locales/zh-CN.ts
+++ b/web/src/pages/Upstream/locales/zh-CN.ts
@@ -34,7 +34,7 @@ export default {
   'page.upstream.step.type': '负载均衡算法',
   'page.upstream.step.pass-host': 'Host 请求头',
   'page.upstream.step.pass-host.pass': '保持与客户端请求一致的主机名',
-  'page.upstream.step.pass-host.node': '使用上游节点列表中的主机名或 IP',
+  'page.upstream.step.pass-host.node': '使用目标节点列表中的主机名或 IP',
   'page.upstream.step.pass-host.rewrite': '自定义 Host 请求头(即将废弃)',
   'page.upstream.step.pass-host.upstream_host': '自定义主机名',
   'page.upstream.step.connect.timeout': '连接超时',
diff --git a/web/src/pages/Upstream/service.ts b/web/src/pages/Upstream/service.ts
index 3712f34..9dee862 100644
--- a/web/src/pages/Upstream/service.ts
+++ b/web/src/pages/Upstream/service.ts
@@ -16,6 +16,8 @@
  */
 import { request } from 'umi';
 
+import { convertToFormData } from '@/components/Upstream/service';
+
 export const fetchList = ({ current = 1, pageSize = 10, ...res }) => {
   return request<Res<ResListData<UpstreamModule.RequestBody>>>('/upstreams', {
     params: {
@@ -29,7 +31,7 @@ export const fetchList = ({ current = 1, pageSize = 10, ...res }) => {
   }));
 };
 
-export const fetchOne = (id: string) => request<Res<any>>(`/upstreams/${id}`);
+export const fetchOne = (id: string) => request<Res<any>>(`/upstreams/${id}`).then(({data}) => convertToFormData(data));
 
 export const create = (data: UpstreamModule.RequestBody) =>
   request('/upstreams', {
diff --git a/web/src/pages/Upstream/transform.ts b/web/src/pages/Upstream/transform.ts
deleted file mode 100644
index 0b520f2..0000000
--- a/web/src/pages/Upstream/transform.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import { pickBy, identity, omit, pick } from 'lodash';
-
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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.
- */
-export const transformRequest = (
-  formData: UpstreamModule.RequestBody,
-): UpstreamModule.RequestBody | undefined | { upstream_id: string } => {
-  let data = pickBy(formData, identity) as UpstreamModule.RequestBody;
-
-  data = omit(data, 'custom')
-
-  const {
-    type,
-    hash_on,
-    key,
-    k8s_deployment_info,
-    nodes,
-    pass_host,
-    upstream_host,
-    upstream_id,
-  } = data;
-
-  data.checks = pickBy(data.checks || {}, identity);
-  if (data.checks.active) {
-    data.checks.active = pickBy(
-      data.checks.active,
-      identity,
-    ) as UpstreamModule.HealthCheck['active'];
-  }
-
-  if (upstream_id) {
-    return { upstream_id };
-  }
-
-  if (Object.keys(data.checks).length === 0) {
-    data = omit(data, 'checks');
-  }
-  if (nodes && k8s_deployment_info) {
-    return undefined;
-  }
-
-  if (!nodes && !k8s_deployment_info) {
-    return undefined;
-  }
-
-  if (type === 'chash') {
-    if (!hash_on) {
-      return undefined;
-    }
-
-    if (hash_on !== 'consumer' && !key) {
-      return undefined;
-    }
-  }
-
-  if (pass_host === 'rewrite' && !upstream_host) {
-    return undefined;
-  }
-
-  if (nodes) {
-    // NOTE: https://github.com/ant-design/ant-design/issues/27396
-    data.nodes = data.nodes?.map((item) => {
-      return pick(item, ['host', 'port', 'weight']);
-    });
-    return data;
-  }
-
-  return undefined;
-};