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

sunyi 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 6c3f35c  feat: user can skip upstream when select service_id (#1302)
6c3f35c is described below

commit 6c3f35ce7f0c6812004546dcf8483432e8bcb65b
Author: litesun <su...@apache.org>
AuthorDate: Fri Jan 22 23:51:59 2021 +0800

    feat: user can skip upstream when select service_id (#1302)
    
    * feat: user can skip upstream when select service_id
    
    * feat: update code
    
    * feat: update upstream
    
    * fix: manual input
    
    * feat: update CreateStep4
    
    * fix: Select Upstream show empty string
    
    * merge LiteSun/feat-route into test-route
    
    * feat: auto fill DEFAULT_UPSTREAM
    
    * test:  create route can skip upstream
    
    * feat: update upstreamForm
    
    * feat: update testcase
    
    * feat: update upstream testcase
    
    Co-authored-by: guoqqqi <979918...@qq.com>
---
 .../route/create-route-can-skip-upstream.spec.js   | 132 ++++++++++
 .../upstream/create_and_delete_upstream.spec.js    |  10 +-
 web/src/components/Upstream/UpstreamForm.tsx       | 276 +++++++++++----------
 web/src/pages/Route/Create.tsx                     |   7 +-
 .../Route/components/CreateStep4/CreateStep4.tsx   |   2 +-
 .../Route/components/Step2/RequestRewriteView.tsx  |   2 +
 web/src/pages/Route/transform.ts                   |   5 +
 web/src/pages/Route/typing.d.ts                    |   2 +
 web/src/pages/Service/components/Step1.tsx         |   1 +
 web/src/pages/Upstream/locales/en-US.ts            |   4 +-
 10 files changed, 300 insertions(+), 141 deletions(-)

diff --git 
a/web/cypress/integration/route/create-route-can-skip-upstream.spec.js 
b/web/cypress/integration/route/create-route-can-skip-upstream.spec.js
new file mode 100644
index 0000000..af78885
--- /dev/null
+++ b/web/cypress/integration/route/create-route-can-skip-upstream.spec.js
@@ -0,0 +1,132 @@
+/*
+ * 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.
+ */
+/* eslint-disable no-undef */
+
+context('Can select service_id skip upstream in route', () => {
+  const data = {
+    upstreamName: 'test_upstream',
+    serviceName: 'test_service',
+    routeName: 'test_route',
+    ip: '127.0.0.1',
+  }
+  const domSelector = {
+    name: '#name',
+    nodes_0_host: '#nodes_0_host',
+    upstream_id: '#upstream_id',
+    input: ':input',
+    customUpstream: '[title=Custom]',
+    titleName: '[title=Name]',
+    testService: '[title=test_service]',
+    notification: '.ant-notification-notice-message',
+  };
+
+  beforeEach(() => {
+    cy.login();
+  });
+
+  it('should create test upstream and service', () => {
+    cy.visit('/');
+    cy.contains('Upstream').click();
+    cy.contains('Create').click();
+
+    cy.get(domSelector.name).type(data.upstreamName);
+    cy.get(domSelector.nodes_0_host).type(data.ip);
+    cy.contains('Next').click();
+    cy.contains('Submit').click();
+    cy.get(domSelector.notification).should('contain', 'Create Upstream 
Successfully');
+
+    cy.visit('/');
+    cy.contains('Service').click();
+    cy.contains('Create').click();
+    cy.get(domSelector.name).type(data.serviceName);
+    cy.get(domSelector.customUpstream).click();
+    cy.contains(data.upstreamName).click();
+    cy.contains('Next').click();
+    cy.contains('Next').click();
+    cy.contains('Submit').click();
+    cy.get(domSelector.notification).should('contain', 'Create Service 
Successfully');
+  });
+
+  it('should skip upstream module after service is selected when creating 
route', () => {
+    cy.visit('/');
+    cy.contains('Route').click();
+    cy.contains('Create').click();
+
+    // The None option doesn't exist when service isn't selected
+    cy.get(domSelector.name).type(data.routeName);
+    cy.contains('Next').click();
+    cy.get(domSelector.customUpstream).click();
+    cy.contains('None').should('not.exist');
+
+    cy.contains('Previous').click();
+    cy.contains('None').click();
+    cy.contains(data.serviceName).click();
+    cy.contains('Next').click();
+
+    // make sure upstream data can be saved
+    cy.get(domSelector.customUpstream).click();
+    cy.contains(data.upstreamName).click();
+    cy.get(domSelector.input).should('be.disabled');
+
+    cy.contains(data.upstreamName).click();
+    cy.contains('None').click();
+    cy.contains('Next').click();
+    cy.contains('Next').click();
+    cy.contains('Submit').click();
+    cy.contains('Goto List').click();
+  });
+
+  it('should skip Upstream module after service is selected when editing 
route', () => {
+    cy.visit('/');
+    cy.contains('Route').click();
+
+    cy.get(domSelector.titleName).type(data.routeName);
+    cy.contains('Search').click();
+    cy.contains(data.routeName).siblings().contains('Edit').click();
+    cy.get(domSelector.testService).click();
+    cy.contains('None').click();
+    cy.contains('Next').click();
+    cy.get(domSelector.upstream_id).click();
+    cy.contains('None').should('not.exist');
+    cy.contains(data.upstreamName).click();
+    cy.contains('Next').click();
+    cy.contains('Next').click();
+    cy.contains('Submit').click();
+    cy.contains('Submit Successfully');
+  });
+
+  it('should delete upstream, service and route', () => {
+    cy.visit('/');
+    cy.contains('Upstream').click();
+    cy.contains(data.upstreamName).siblings().contains('Delete').click();
+    cy.contains('button', 'Confirm').click();
+    cy.get(domSelector.notification).should('contain', 'Delete Upstream 
Successfully');
+
+    cy.visit('/');
+    cy.contains('Service').click();
+    cy.contains(data.serviceName).siblings().contains('Delete').click();
+    cy.contains('button', 'Confirm').click();
+    cy.get(domSelector.notification).should('contain', 'Delete Service 
Successfully');
+
+    cy.visit('/');
+    cy.contains('Route').click();
+    cy.contains(data.routeName).siblings().contains('Delete').click();
+    cy.contains('button', 'Confirm').click();
+    cy.get(domSelector.notification).should('contain', 'Delete Route 
Successfully');
+  });
+});
+
diff --git 
a/web/cypress/integration/upstream/create_and_delete_upstream.spec.js 
b/web/cypress/integration/upstream/create_and_delete_upstream.spec.js
index b445f0c..b671741 100644
--- a/web/cypress/integration/upstream/create_and_delete_upstream.spec.js
+++ b/web/cypress/integration/upstream/create_and_delete_upstream.spec.js
@@ -45,8 +45,8 @@ context('Create and Delete Upstream', () => {
     cy.get('#nodes_0_port').clear().type('7000');
     cy.contains('Next').click();
     cy.contains('Submit').click();
-    cy.get(domSelectors.notification).should('contain', 'Create upstream 
successfully');
-    cy.contains('Create upstream successfully');
+    cy.get(domSelectors.notification).should('contain', 'Create Upstream 
Successfully');
+    cy.contains('Create Upstream Successfully');
     cy.wait(sleepTime * 5);
     cy.url().should('contains', 'upstream/list');
   });
@@ -57,7 +57,7 @@ context('Create and Delete Upstream', () => {
     cy.wait(sleepTime * 5);
     cy.contains(name).siblings().contains('Delete').click();
     cy.contains('button', 'Confirm').click();
-    cy.get(domSelectors.notification).should('contain', 'Delete successfully');
+    cy.get(domSelectors.notification).should('contain', 'Delete Upstream 
Successfully');
   });
 
   it('should create chash upstream', () => {
@@ -101,7 +101,7 @@ context('Create and Delete Upstream', () => {
     // next to finish
     cy.contains('Next').click();
     cy.contains('Submit').click();
-    cy.get(domSelectors.notification).should('contain', 'Create upstream 
successfully');
+    cy.get(domSelectors.notification).should('contain', 'Create Upstream 
Successfully');
     cy.wait(sleepTime * 5);
     cy.url().should('contains', 'upstream/list');
   });
@@ -112,6 +112,6 @@ context('Create and Delete Upstream', () => {
     cy.wait(sleepTime * 5);
     cy.contains(name).siblings().contains('Delete').click();
     cy.contains('button', 'Confirm').click();
-    cy.get(domSelectors.notification).should('contain', 'Delete successfully');
+    cy.get(domSelectors.notification).should('contain', 'Delete Upstream 
Successfully');
   });
 });
diff --git a/web/src/components/Upstream/UpstreamForm.tsx 
b/web/src/components/Upstream/UpstreamForm.tsx
index 3e7cff7..36b8bf4 100644
--- a/web/src/components/Upstream/UpstreamForm.tsx
+++ b/web/src/components/Upstream/UpstreamForm.tsx
@@ -18,10 +18,11 @@ import { MinusCircleOutlined, PlusOutlined } from 
'@ant-design/icons';
 import { Button, Col, Divider, Form, Input, InputNumber, Row, Select, 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 '@api7-dashboard/ui';
 import { transformRequest } from '@/pages/Upstream/transform';
-import type { FormInstance } from 'antd/es/form';
+import { DEFAULT_UPSTREAM } from './constant'
 
 enum Type {
   roundrobin = 'roundrobin',
@@ -61,6 +62,7 @@ type Props = {
   showSelector?: boolean;
   // FIXME: use proper typing
   ref?: any;
+  required: boolean,
 };
 
 const removeBtnStyle = {
@@ -70,11 +72,12 @@ const removeBtnStyle = {
 };
 
 const UpstreamForm: React.FC<Props> = forwardRef(
-  ({ form, disabled, list = [], showSelector }, ref) => {
+  ({ form, disabled, list = [], showSelector, required = true }, ref) => {
     const { formatMessage } = useIntl();
     const [readonly, setReadonly] = useState(
       Boolean(form.getFieldValue('upstream_id')) || disabled,
     );
+    const [hidenForm, setHidenForm] = useState(false);
 
     const timeoutFields = [
       {
@@ -96,13 +99,32 @@ const UpstreamForm: React.FC<Props> = forwardRef(
     }));
 
     useEffect(() => {
-      const id = form.getFieldValue('upstream_id');
-      if (id) {
-        setReadonly(true);
-        requestAnimationFrame(() => {
-          form.setFieldsValue(list.find((item) => item.id === id));
-        });
+      const formData = transformRequest(form.getFieldsValue()) || {};
+      const { upstream_id } = form.getFieldsValue();
+
+      if (upstream_id === 'None') {
+        setHidenForm(true);
+        if (required) {
+          requestAnimationFrame(() => {
+            form.resetFields();
+            form.setFieldsValue(DEFAULT_UPSTREAM);
+            setHidenForm(false);
+          });
+        }
+      } else {
+        if (upstream_id) {
+          requestAnimationFrame(() => {
+            form.setFieldsValue(list.find((item) => item.id === upstream_id));
+          });
+        }
+        if (!required && !Object.keys(formData).length) {
+          requestAnimationFrame(() => {
+            form.setFieldsValue({ upstream_id: 'None' });
+            setHidenForm(true);
+          });
+        }
       }
+      setReadonly(Boolean(upstream_id) || disabled);
     }, [list]);
 
     const CHash = () => (
@@ -608,26 +630,20 @@ const UpstreamForm: React.FC<Props> = forwardRef(
           <Form.Item
             label={formatMessage({ id: 'page.upstream.step.select.upstream' })}
             name="upstream_id"
-            shouldUpdate={(prev, next) => {
-              setReadonly(Boolean(next.upstream_id));
-              if (prev.upstream_id !== next.upstream_id) {
-                const id = next.upstream_id;
-                if (id) {
-                  form.setFieldsValue(list.find((item) => item.id === id));
-                  form.setFieldsValue({
-                    upstream_id: id,
-                  });
-                }
-              }
-              return prev.upstream_id !== next.upstream_id;
-            }}
           >
             <Select
               disabled={disabled}
-              onChange={(id) => {
-                form.setFieldsValue(list.find((item) => item.id === id));
+              onChange={(upstream_id) => {
+                setReadonly(Boolean(upstream_id));
+                setHidenForm(Boolean(upstream_id === 'None'));
+                form.setFieldsValue(list.find((item) => item.id === 
upstream_id));
+                if (upstream_id === '') {
+                  form.resetFields();
+                  form.setFieldsValue(DEFAULT_UPSTREAM);
+                }
               }}
             >
+              {Boolean(!required) && <Select.Option value={'None'} 
>None</Select.Option>}
               {[
                 {
                   name: formatMessage({ id: 
'page.upstream.step.select.upstream.select.option' }),
@@ -643,116 +659,118 @@ const UpstreamForm: React.FC<Props> = forwardRef(
           </Form.Item>
         )}
 
-        <Form.Item
-          label={formatMessage({ id: 'page.upstream.step.type' })}
-          name="type"
-          rules={[{ required: true }]}
-        >
-          <Select disabled={readonly}>
-            {Object.entries(Type).map(([label, value]) => {
-              return (
-                <Select.Option value={value} key={value}>
-                  {label}
-                </Select.Option>
-              );
-            })}
-          </Select>
-        </Form.Item>
-        <Form.Item shouldUpdate noStyle>
-          {() => {
-            if (form.getFieldValue('type') === 'chash') {
-              return <CHash />;
-            }
-            return null;
-          }}
-        </Form.Item>
-        {NodeList()}
-        <Form.Item
-          label={formatMessage({ id: 'page.upstream.step.pass-host' })}
-          name="pass_host"
-          extra={formatMessage({ id: 'page.upstream.step.pass-host.tips' })}
-        >
-          <Select disabled={readonly}>
-            <Select.Option value="pass">
-              {formatMessage({ id: 'page.upstream.step.pass-host.pass' })}
-            </Select.Option>
-            <Select.Option value="node">
-              {formatMessage({ id: 'page.upstream.step.pass-host.node' })}
-            </Select.Option>
-            <Select.Option value="rewrite">
-              {formatMessage({ id: 'page.upstream.step.pass-host.rewrite' })}
-            </Select.Option>
-          </Select>
-        </Form.Item>
-        <Form.Item
-          noStyle
-          shouldUpdate={(prev, next) => {
-            return prev.pass_host !== next.pass_host;
-          }}
-        >
-          {() => {
-            if (form.getFieldValue('pass_host') === 'rewrite') {
-              return (
-                <Form.Item
-                  label={formatMessage({ id: 
'page.upstream.step.pass-host.upstream_host' })}
-                  name="upstream_host"
-                >
-                  <Input disabled={readonly} />
-                </Form.Item>
-              );
-            }
-            return null;
-          }}
-        </Form.Item>
-
-        {timeoutFields.map(({ label, name }) => (
-          <Form.Item label={label} required key={label}>
-            <Form.Item
-              name={name}
-              noStyle
-              rules={[
-                {
-                  required: true,
-                  message: formatMessage({ id: 
`page.upstream.step.input.${name[1]}.timeout` }),
-                },
-              ]}
-            >
-              <InputNumber disabled={readonly} />
-            </Form.Item>
-            <TimeUnit />
+        {!hidenForm && (<>
+          <Form.Item
+            label={formatMessage({ id: 'page.upstream.step.type' })}
+            name="type"
+            rules={[{ required: true }]}
+          >
+            <Select disabled={readonly}>
+              {Object.entries(Type).map(([label, value]) => {
+                return (
+                  <Select.Option value={value} key={value}>
+                    {label}
+                  </Select.Option>
+                );
+              })}
+            </Select>
+          </Form.Item>
+          <Form.Item shouldUpdate noStyle>
+            {() => {
+              if (form.getFieldValue('type') === 'chash') {
+                return <CHash />;
+              }
+              return null;
+            }}
+          </Form.Item>
+          {NodeList()}
+          <Form.Item
+            label={formatMessage({ id: 'page.upstream.step.pass-host' })}
+            name="pass_host"
+            extra={formatMessage({ id: 'page.upstream.step.pass-host.tips' })}
+          >
+            <Select disabled={readonly}>
+              <Select.Option value="pass">
+                {formatMessage({ id: 'page.upstream.step.pass-host.pass' })}
+              </Select.Option>
+              <Select.Option value="node">
+                {formatMessage({ id: 'page.upstream.step.pass-host.node' })}
+              </Select.Option>
+              <Select.Option value="rewrite">
+                {formatMessage({ id: 'page.upstream.step.pass-host.rewrite' })}
+              </Select.Option>
+            </Select>
+          </Form.Item>
+          <Form.Item
+            noStyle
+            shouldUpdate={(prev, next) => {
+              return prev.pass_host !== next.pass_host;
+            }}
+          >
+            {() => {
+              if (form.getFieldValue('pass_host') === 'rewrite') {
+                return (
+                  <Form.Item
+                    label={formatMessage({ id: 
'page.upstream.step.pass-host.upstream_host' })}
+                    name="upstream_host"
+                  >
+                    <Input disabled={readonly} />
+                  </Form.Item>
+                );
+              }
+              return null;
+            }}
           </Form.Item>
-        ))}
 
-        <PanelSection
-          title={formatMessage({ id: 
'page.upstream.step.healthyCheck.healthy.check' })}
-        >
-          {[
-            {
-              label: formatMessage({ id: 
'page.upstream.step.healthyCheck.active' }),
-              name: ['checks', 'active'],
-              component: <ActiveHealthCheck />,
-            },
-            {
-              label: formatMessage({ id: 
'page.upstream.step.healthyCheck.passive' }),
-              name: ['checks', 'passive'],
-              component: <InActiveHealthCheck />,
-            },
-          ].map(({ label, name, component }) => (
-            <div key={label}>
-              <Form.Item label={label} name={name} valuePropName="checked" 
key={label}>
-                <Switch disabled={readonly} />
-              </Form.Item>
-              <Form.Item shouldUpdate noStyle>
-                {() => {
-                  if (form.getFieldValue(name)) {
-                    return component;
-                  }
-                  return null;
-                }}
+          {timeoutFields.map(({ label, name }) => (
+            <Form.Item label={label} required key={label}>
+              <Form.Item
+                name={name}
+                noStyle
+                rules={[
+                  {
+                    required: true,
+                    message: formatMessage({ id: 
`page.upstream.step.input.${name[1]}.timeout` }),
+                  },
+                ]}
+              >
+                <InputNumber disabled={readonly} />
               </Form.Item>
-            </div>
+              <TimeUnit />
+            </Form.Item>
           ))}
-        </PanelSection>
+
+          <PanelSection
+            title={formatMessage({ id: 
'page.upstream.step.healthyCheck.healthy.check' })}
+          >
+            {[
+              {
+                label: formatMessage({ id: 
'page.upstream.step.healthyCheck.active' }),
+                name: ['checks', 'active'],
+                component: <ActiveHealthCheck />,
+              },
+              {
+                label: formatMessage({ id: 
'page.upstream.step.healthyCheck.passive' }),
+                name: ['checks', 'passive'],
+                component: <InActiveHealthCheck />,
+              },
+            ].map(({ label, name, component }) => (
+              <div key={label}>
+                <Form.Item label={label} name={name} valuePropName="checked" 
key={label}>
+                  <Switch disabled={readonly} />
+                </Form.Item>
+                <Form.Item shouldUpdate noStyle>
+                  {() => {
+                    if (form.getFieldValue(name)) {
+                      return component;
+                    }
+                    return null;
+                  }}
+                </Form.Item>
+              </div>
+            ))}
+          </PanelSection>
+        </>)}
       </Form>
     );
   },
diff --git a/web/src/pages/Route/Create.tsx b/web/src/pages/Route/Create.tsx
index ab1257f..9155a08 100644
--- a/web/src/pages/Route/Create.tsx
+++ b/web/src/pages/Route/Create.tsx
@@ -135,7 +135,7 @@ const Page: React.FC<Props> = (props) => {
         );
       }
 
-      return <Step2 form={form2} upstreamRef={upstreamRef} />;
+      return <Step2 form={form2} upstreamRef={upstreamRef} 
hasServiceId={form1.getFieldValue('service_id') !== ''} />;
     }
 
     if (step === 3) {
@@ -256,11 +256,10 @@ const Page: React.FC<Props> = (props) => {
   return (
     <>
       <PageHeaderWrapper
-        title={`${
-          (props as any).match.params.rid
+        title={`${(props as any).match.params.rid
             ? formatMessage({ id: 'component.global.edit' })
             : formatMessage({ id: 'component.global.create' })
-        } ${formatMessage({ id: 'menu.routes' })}`}
+          } ${formatMessage({ id: 'menu.routes' })}`}
       >
         <Card bordered={false}>
           <Steps current={step - 1} className={styles.steps}>
diff --git a/web/src/pages/Route/components/CreateStep4/CreateStep4.tsx 
b/web/src/pages/Route/components/CreateStep4/CreateStep4.tsx
index 95fc6dc..9e6f49f 100644
--- a/web/src/pages/Route/components/CreateStep4/CreateStep4.tsx
+++ b/web/src/pages/Route/components/CreateStep4/CreateStep4.tsx
@@ -50,7 +50,7 @@ const CreateStep4: React.FC<Props> = ({ form1, form2, 
redirect, upstreamRef, ...
           <h2 style={style}>
             {formatMessage({ id: 
'page.route.steps.stepTitle.defineApiBackendServe' })}
           </h2>
-          <Step2 form={form2} upstreamRef={upstreamRef} disabled />
+          <Step2 form={form2} upstreamRef={upstreamRef} disabled 
hasServiceId={form1.getFieldValue('service_id') !== ''} />
           <h2 style={style}>
             {formatMessage({ id: 
'component.global.steps.stepTitle.pluginConfig' })}
           </h2>
diff --git a/web/src/pages/Route/components/Step2/RequestRewriteView.tsx 
b/web/src/pages/Route/components/Step2/RequestRewriteView.tsx
index a3bf8dc..023cac8 100644
--- a/web/src/pages/Route/components/Step2/RequestRewriteView.tsx
+++ b/web/src/pages/Route/components/Step2/RequestRewriteView.tsx
@@ -23,6 +23,7 @@ const RequestRewriteView: 
React.FC<RouteModule.Step2PassProps> = ({
   form,
   upstreamRef,
   disabled,
+  hasServiceId = false
 }) => {
   const [list, setList] = useState<UpstreamModule.RequestBody[]>([]);
   useEffect(() => {
@@ -35,6 +36,7 @@ const RequestRewriteView: 
React.FC<RouteModule.Step2PassProps> = ({
       disabled={disabled}
       list={list}
       showSelector
+      required={!hasServiceId}
       key={1}
     />
   );
diff --git a/web/src/pages/Route/transform.ts b/web/src/pages/Route/transform.ts
index bb794d2..3b9fa4f 100644
--- a/web/src/pages/Route/transform.ts
+++ b/web/src/pages/Route/transform.ts
@@ -107,6 +107,7 @@ export const transformStepData = ({
       'ret_code',
       'redirectOption',
       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' : '',
@@ -215,6 +216,10 @@ export const transformRouteData = (data: RouteModule.Body) 
=> {
 
   const advancedMatchingRules: RouteModule.MatchingRule[] = 
transformVarsToRules(vars);
 
+  if (upstream && Object.keys(upstream).length) {
+    upstream.upstream_id = '';
+  }
+
   const form2Data: RouteModule.Form2Data = upstream || { upstream_id };
 
   const { plugins, script } = data;
diff --git a/web/src/pages/Route/typing.d.ts b/web/src/pages/Route/typing.d.ts
index 992a845..342d7c1 100644
--- a/web/src/pages/Route/typing.d.ts
+++ b/web/src/pages/Route/typing.d.ts
@@ -80,6 +80,7 @@ declare namespace RouteModule {
     remote_addrs: string[];
     vars: [string, Operator, string][];
     upstream: {
+      upstream_id?: string;
       type: 'roundrobin' | 'chash' | 'ewma';
       hash_on?: string;
       key?: string;
@@ -163,6 +164,7 @@ declare namespace RouteModule {
     form: FormInstance;
     disabled?: boolean;
     upstreamRef: any;
+    hasServiceId: boolean;
   };
 
   type Form2Data = {
diff --git a/web/src/pages/Service/components/Step1.tsx 
b/web/src/pages/Service/components/Step1.tsx
index fd805ba..e904a57 100644
--- a/web/src/pages/Service/components/Step1.tsx
+++ b/web/src/pages/Service/components/Step1.tsx
@@ -54,6 +54,7 @@ const Step1: React.FC<ServiceModule.Step1PassProps> = ({
       </Form>
       <UpstreamForm
         ref={upstreamRef}
+        required
         form={upstreamForm}
         disabled={disabled}
         list={list}
diff --git a/web/src/pages/Upstream/locales/en-US.ts 
b/web/src/pages/Upstream/locales/en-US.ts
index caad2b4..f4063da 100644
--- a/web/src/pages/Upstream/locales/en-US.ts
+++ b/web/src/pages/Upstream/locales/en-US.ts
@@ -82,7 +82,7 @@ export default {
 
   'page.upstream.create.edit': 'Edit',
   'page.upstream.create.create': 'Create',
-  'page.upstream.create.upstream.successfully': 'upstream successfully',
+  'page.upstream.create.upstream.successfully': 'Upstream Successfully',
   'page.upstream.create.basic.info': 'Basic Information',
   'page.upstream.create.preview': 'Preview',
 
@@ -95,7 +95,7 @@ export default {
   'page.upstream.list.confirm.delete': 'Are you sure to delete ?',
   'page.upstream.list.confirm': 'Confirm',
   'page.upstream.list.cancel': 'Cancel',
-  'page.upstream.list.delete.successfully': 'Delete successfully',
+  'page.upstream.list.delete.successfully': 'Delete Upstream Successfully',
   'page.upstream.list.delete': 'Delete',
   'page.upstream.list': 'Upstream List',
   'page.upstream.list.input': 'Please input',

Reply via email to