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 2020/09/01 01:43:11 UTC

[apisix-dashboard] branch master updated: feat: added chash (#429)

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 a0d7e0c  feat: added chash (#429)
a0d7e0c is described below

commit a0d7e0ce2269a366e56e5f0a9ca6ecacddd07070
Author: litesun <31...@users.noreply.github.com>
AuthorDate: Tue Sep 1 09:43:01 2020 +0800

    feat: added chash (#429)
    
    * merge master (#1)
    
    * add: Determine duplicate names api for route & upstream (#305)
    
    * fix: transaction in routes and upstreams (#306)
    
    * add transaction for ssl and consumer (#308)
    
    * update ci/cd for api (#307)
    
    * update github actions for api ci cd
    
    * fix: working-directory
    
    * fix error
    
    * fix: step name
    
    * fix: mysql config for github action
    
    * test
    
    * use default config
    
    * test: add e2e test for ssl and consumer (#309)
    
    * test: add e2e test for ssl and consumer
    
    * fix:  change assert to avoid  the mutual influence of route and service test
    
    * remove useless code
    
    * Feat: added Route Consumer and Upstream (#304)
    
    * feat: added routes
    
    * feat: added Consumer
    
    * feat: added upstream
    
    * feat: update SSL
    
    * fix: routes
    
    * feat: added commit command
    
    * feat(route): set empty array for upstreamHeaderList
    
    * fix: e2e test use the same function to set up router (#310)
    
    * fix: return all objects when search route & upstream (#311)
    
    * fix: route search
    
    * fix: upstream search
    
    * fix(deploy): added missing yarn.lock
    
    * fix: proxy-rewrite plugin in upstream (#312)
    
    * fix(SSL): search api
    
    * docs: added tips when deployment
    
    * feat(Deploy): use node alpine image
    
    * fix(Route): set required field for custom redirect
    
    * fix(Route): check if redirect is empty object
    
    * fix(Deploy): add Python installation in dockerfile (#316)
    
    Signed-off-by: imjoey <ma...@gmail.com>
    
    * fix(Route): update desc for status code
    
    * fix: proxy-path default type is static (#318)
    
    * add proxyRewrite test (#319)
    
    * feat: bump dependencies version (#320)
    
    * feat(Deploy): update Dockerfile
    
    * feat(Deploy): update Deploy Dockerfile
    
    * feat(Pages): update pages (#324)
    
    * feat(Pages): update pages
    
    * chore: update routes
    
    * fix(Route): omit upstream_id when not exist
    
    * i18n consumer (#325)
    
    * i18n ssl (#335)
    
    * nationalization PluginPage component (#323)
    
    * i18n upstream (#334)
    
    * feat(i18n): set module (#336)
    
    * i18n set
    
    * change set to setting
    
    * feat(i18n): metrics module (#326)
    
    * i18n metrics
    
    * combine import
    
    * feat(i18n): route module (#327)
    
    * i18n route
    
    * combine import
    
    * doc: sync config.yaml from the latest version of APISIX (#344)
    
    * i18n route (#342)
    
    * i18n actionbar (#343)
    
    * fix: transform vars error (#347)
    
    * feat(i18n): pluginpage component (#345)
    
    * i18n pluginpage
    
    * change pluginpage to PluginPage
    
    * feature: support run in mac system (#349)
    
    * combine import (#348)
    
    * i18n menu (#351)
    
    * i18n PluginPage (#350)
    
    * feat: prepare to release (#352)
    
    * feat(ManagerAPI): added ASF header
    
    * feat(FE): aded ASF Header
    
    * feat(FE): added ASF header
    
    * fix(FE): update PluginDrawer
    
    * feat: remove some images
    
    * feat: added LICENSE
    
    * feat: update Version
    
    * feat: added NOTICE & CODE_OF_CONDUCT
    
    * feat: added initial CHANGELOG
    
    * feat: rename CODE_OF_CONDUCT
    
    * feat: revert version
    
    * feat: update LICENSE
    
    * feat: update License
    
    * feat(conf): update default preview API (#353)
    
    * doc:  add install doc for manager-api (#355)
    
    * doc: add install doc for manager-api
    
    * doc: modify folder from build to run
    
    * doc: add ASF header
    
    * fix(ci): resolve lint failures (#354)
    
    * fix(deploy): failed to start manager_api (#363)
    
    Signed-off-by: imjoey <ma...@gmail.com>
    
    * feat(i18n): modify some i18n according to the proposal#331 (#366)
    
    * Create CONTRIBUTING.md (#368)
    
    * Create CONTRIBUTING.md
    
    * Create ISSUE_TEMPLATE
    
    * Create PULL_REQUEST_TEMPLATE
    
    * doc: remove all ‘incubator’ (#367)
    
    * feat(deploy): set gen-config-yaml.sh executable (#362)
    
    This also would simplify the docs.
    
    Signed-off-by: imjoey <ma...@gmail.com>
    
    * feat(i18n): Use auto load i18n (#332) (#371)
    
    * Create ci.yml (#372)
    
    * feat: release 1.5 (#364)
    
    * Feat release 1.5 (#358)
    
    * feat(doc): update README
    
    * feat: update CHANGELOG
    
    * doc: add usage of dashbaord
    
    * Revert "doc: add usage of dashbaord"
    
    This reverts commit 5a08c7f43539a44cd0cf0f6175574e59efbd0ab6.
    
    * feat(Doc): update deployment
    
    * feat(Doc): update the deployment
    
    * feat(Doc): update the deployment
    
    * feat: remove incubator text
    
    * doc: modify doc for manager-api runing in local
    
    * feat(Doc): update README
    
    * doc: check env variables and give run.sh power to execute
    
    * feat(Doc): update Deployment
    
    * feat(Doc): update deployment
    
    * doc: modify manager-api build
    
    * feat: update ignore file
    
    Co-authored-by: kv <gx...@163.com>
    Co-authored-by: 琚致远 <ju...@juzhiyuandeMini.lan>
    
    * feat: cherry-pick 4fd0ce79bb34dbe8c31b7a27884930e3b0e5437c
    
    * feat(compose): remove images
    
    * feat: added line
    
    Co-authored-by: kv <gx...@163.com>
    Co-authored-by: 琚致远 <ju...@juzhiyuandeMini.lan>
    
    * feat: Unified access entrance, only the dashboard port is exposed to … (#370)
    
    * feat: Unified access entrance, only the dashboard port is exposed to the outside
    
    * add EOL
    
    * docs: create I18N_USER_GUIDE.md (#373)
    
    * docs: create I18N_USER_GUIDE.md
    
    * docs: modify I18N_USER_GUIDE.md
    
    * feat(Doc): added deploy doc for docker (#376)
    
    * feat(Doc): added deploy doc for docker
    
    * feat: added CD
    
    * feat(Netlify): added proxy
    
    * feat: update API
    
    * feat: remove console
    
    * feat(Netlify): update redirect rule
    
    * feat: update README
    
    * feat: update README
    
    * update go module proxy (#378)
    
    * Update README.md (#379)
    
    * Update README.md
    
    * Update README.md
    
    * Create Preview.md
    
    * feat(Doc): added snapshots for Preview
    
    * feat(Doc): update images
    
    * feat(Doc): update images
    
    * Update README.md
    
    * Update netlify.toml
    
    * feat(route): route add params mapping feature (#375) (#377)
    
    * feat(doc): update deploy manually doc
    
    * fix: mv config.yml to config-default.yml in the latest version of apisix (#383)
    
    * fix: wget config-default.yaml the output file need to be named config.yaml (#384)
    
    * fix #386 wget special output file use -O (#387)
    
    * feat(authentication): create authentication module (#330)
    
    * feat(authentication): create module typing definition
    
    * feat(authentication): create Login page
    
    * feat(authentication): update typing definition
    
    * feat(authentication): add centent to Login page
    
    * feat(authentication): update typing definition
    
    * feat(authentication): update Login page to add Password and Test method
    
    * feat(authentication): update typing definition to add check and submit function
    
    * feat(authentication): move Test login method to Example
    
    * feat(authentication): add check and submit function
    
    * feat(authentication): add submit function in Login page
    
    * feat(authentication): add test to Password login method
    
    * feat(authentication): change example LoginMethod text
    
    * feat(authentication): add i18n content
    
    * feat(authentication): redirect to index when login success
    
    * feat(i18n): update i18n file import
    remove import i18n file of user module manually and try auto import by umi.js
    
    * feat(authentication): create authentication configure items
    
    * fix(authentication): fix logging filter
    write back request body for read by PostForm function
    
    * feat(authentication): create authentication controller
    
    * feat(authentication): update dependencies
    
    * fix(authentication): fix logging filter
    
    * feat(authentication): change to session for authentication
    
    * feat(authentication): create authentication filter
    use authentication filter to check every request
    
    * feat(authentication): create unit test case
    
    * fix(authentication): change HTTP code when authentication fail request
    
    * feat(authentication): add jwt dependency
    
    * feat(authentication): create session configures
    
    * feat(authentication): change cookie-based session to jwt
    
    * feat(authentication): change cors Access-Control-Allow-Headers header
    
    * feat(authentication): change login page path and error handler
    
    * feat(authentication): create request interceptor to add Authorization header
    
    * feat(authentication): connect to backend login API and i18n
    
    * feat(authentication): create logout page
    
    * feat(authentication): add redirect query to back previous page
    
    * feat(authentication): update LoginMethod definition for logout
    
    * feat(authentication): add logout button
    
    * feat(authentication): improve login page
    
    * fix: clean codes
    
    * fix(authentication): fix unit test crash
    
    * feat(authentication): remove API url setting
    
    * feat(authentication): improve session check
    
    * feat(authentication): redirect to login page when not exist token
    
    * fix: clean codes and add ASF header
    
    * feat(User): update prefix
    
    * fix(ci): fix preview environment (#388)
    
    * fix README typo (#389)
    
    * fix(ci): fix read configuration file path in docker (#390)
    
    * doc: Introducing manager-api (#391)
    
    * Update nginx.conf
    
    * Update Dockerfile
    
    * Revert "Update Dockerfile"
    
    This reverts commit ea827bfd2789c2d939a2517b279170cccdadf35b.
    
    * fix: preview mysql pwd was wrong (#393)
    
    * README in Chinese (#398)
    
    * feat(doc): added Chinese version of README
    
    * fix(README.zh-CN.md): fix wrong link
    
    * fix(README.zh-CN.md): add link to README.md
    
    * fix(README.zh-CN.md): sync with README.md
    
    * fix(README.zh-CN.md): Fix some translation errors
    
    * fix: dashboard /user/login get error code 405 (#397)
    
    * fix: fix dashboard /user/login get error code 405
    
    * fix: modify nginx according to giphoo proposal
    
    * fix(authentication): change Apache APISIX copyright (#401)
    
    * fix: configure only necessary items, such as etcd host (#405)
    
    * fix: configure only necessary items, such as etcd host
    
    * fix: configure only necessary items, such as etcd host
    
    * fix end of line
    
    * fix: using default admin key (#408)
    
    * fix:  we need conf.json when deploying manager-api in local  (#409)
    
    * fix: we need conf.json when deploying manager-api in loal
    
    * fix: log error when starting manager failed
    
    * fix: click create ssl prestep not response (#407)
    
    * fix: submit setting grafanaURl without validation (#413)
    
    * feat: support generate `script` for APISIX (#411)
    
    * feat: support generate `script` for APISIX
    
    * not run in `/root` dir
    
    * add  `config.yaml` for APISIX
    
    * fix path
    
    * fix(authentication): change login api url (#414)
    
    * fix(authentication): change manager API login path
    
    * fix(authentication): change authentication unit test
    
    * fix(authentication): clean nginx.conf codes
    
    * fix(authentication): change login URL of front end
    
    * fix(authentication): change authentication filter rule
    
    Co-authored-by: kv <gx...@163.com>
    Co-authored-by: nic-chen <33...@users.noreply.github.com>
    Co-authored-by: 琚致远 <ju...@juzhiyuandeMBP.lan>
    Co-authored-by: juzhiyuan <ju...@apache.org>
    Co-authored-by: Joey <ma...@gmail.com>
    Co-authored-by: bzp2010 <bz...@gmail.com>
    Co-authored-by: TikWind <65...@users.noreply.github.com>
    Co-authored-by: Lien <li...@apache.org>
    Co-authored-by: Rapiz <ra...@foxmail.com>
    Co-authored-by: liuxiran <be...@126.com>
    Co-authored-by: jie <ji...@163.com>
    Co-authored-by: Rapiz <co...@rapiz.me>
    Co-authored-by: 琚致远 <ju...@juzhiyuandeMini.lan>
    Co-authored-by: Tusdasa翼 <tu...@tusdasa.net>
    Co-authored-by: Shuyang Wu <wo...@gmail.com>
    Co-authored-by: Baoyuan <ba...@gmail.com>
    
    * feat: added chash
    
    * feat: update transform
    
    Co-authored-by: kv <gx...@163.com>
    Co-authored-by: nic-chen <33...@users.noreply.github.com>
    Co-authored-by: 琚致远 <ju...@juzhiyuandeMBP.lan>
    Co-authored-by: juzhiyuan <ju...@apache.org>
    Co-authored-by: Joey <ma...@gmail.com>
    Co-authored-by: bzp2010 <bz...@gmail.com>
    Co-authored-by: TikWind <65...@users.noreply.github.com>
    Co-authored-by: Lien <li...@apache.org>
    Co-authored-by: Rapiz <ra...@foxmail.com>
    Co-authored-by: liuxiran <be...@126.com>
    Co-authored-by: jie <ji...@163.com>
    Co-authored-by: Rapiz <co...@rapiz.me>
    Co-authored-by: 琚致远 <ju...@juzhiyuandeMini.lan>
    Co-authored-by: Tusdasa翼 <tu...@tusdasa.net>
    Co-authored-by: Shuyang Wu <wo...@gmail.com>
    Co-authored-by: Baoyuan <ba...@gmail.com>
---
 src/pages/Route/Create.tsx                         |   2 +
 .../Route/components/Step2/RequestRewriteView.tsx  | 263 ++++++++++++---------
 src/pages/Route/constants.ts                       |  16 ++
 src/pages/Route/transform.ts                       |  12 +-
 src/pages/Route/typing.d.ts                        |   5 +
 src/pages/Upstream/components/Step1.tsx            | 127 ++++++++--
 src/pages/Upstream/constants.ts                    |  18 +-
 7 files changed, 312 insertions(+), 131 deletions(-)

diff --git a/src/pages/Route/Create.tsx b/src/pages/Route/Create.tsx
index 326f667..51ee1c0 100644
--- a/src/pages/Route/Create.tsx
+++ b/src/pages/Route/Create.tsx
@@ -156,7 +156,9 @@ const Page: React.FC<Props> = (props) => {
                   ...form2.getFieldsValue(),
                   ...data,
                 });
+                setStep2Data({ ...form2.getFieldsValue(), ...params } as RouteModule.Step2Data);
               });
+              return;
             }
             setStep2Data({ ...form2.getFieldsValue(), ...params } as RouteModule.Step2Data);
           }}
diff --git a/src/pages/Route/components/Step2/RequestRewriteView.tsx b/src/pages/Route/components/Step2/RequestRewriteView.tsx
index 8bb4616..da56ef1 100644
--- a/src/pages/Route/components/Step2/RequestRewriteView.tsx
+++ b/src/pages/Route/components/Step2/RequestRewriteView.tsx
@@ -21,7 +21,12 @@ import { Input, Row, Col, InputNumber, Button, Select } from 'antd';
 import { PlusOutlined, MinusCircleOutlined } from '@ant-design/icons';
 import { useIntl } from 'umi';
 
-import { FORM_ITEM_LAYOUT, FORM_ITEM_WITHOUT_LABEL } from '@/pages/Route/constants';
+import {
+  FORM_ITEM_LAYOUT,
+  FORM_ITEM_WITHOUT_LABEL,
+  HASH_KEY_LIST,
+  HASH_ON_LIST,
+} from '@/pages/Route/constants';
 import PanelSection from '@/components/PanelSection';
 import styles from '../../Create.less';
 import { fetchUpstreamList } from '../../service';
@@ -48,117 +53,157 @@ const RequestRewriteView: React.FC<Props> = ({ data, form, disabled, onChange })
     });
   }, []);
   const renderUpstreamMeta = () => (
-    <Form.List name="upstreamHostList">
-      {(fields, { add, remove }) => (
+    <>
+      <Form.Item label="类型" name="type" rules={[{ required: true }]}>
+        <Select disabled={upstreamDisabled} onChange={(params) => onChange({ type: params })}>
+          <Select.Option value="roundrobin">roundrobin</Select.Option>
+          <Select.Option value="chash">chash</Select.Option>
+        </Select>
+      </Form.Item>
+      {step2Data.type === 'chash' && (
         <>
-          {fields.map((field, index) => (
-            <Form.Item
-              required
-              key={field.key}
-              {...(index === 0 ? FORM_ITEM_LAYOUT : FORM_ITEM_WITHOUT_LABEL)}
-              label={
-                index === 0 ? formatMessage({ id: 'route.request.override.domain.name.or.ip' }) : ''
-              }
-              extra={
-                index === 0
-                  ? formatMessage({ id: 'route.request.override.use.domain.name.default.analysis' })
-                  : ''
-              }
-            >
-              <Row style={{ marginBottom: '10px' }} gutter={16}>
-                <Col span={9}>
-                  <Form.Item
-                    style={{ marginBottom: 0 }}
-                    name={[field.name, 'host']}
-                    rules={[
-                      {
-                        required: true,
-                        message: formatMessage({ id: 'route.request.override.input.domain.or.ip' }),
-                      },
-                      {
-                        pattern: new RegExp(
-                          /(^([1-9]?\d|1\d{2}|2[0-4]\d|25[0-5])(\.(25[0-5]|1\d{2}|2[0-4]\d|[1-9]?\d)){3}$|^(?![0-9.]+$)([a-zA-Z0-9_-]+)(\.[a-zA-Z0-9_-]+){0,}$)/,
-                          'g',
-                        ),
-                        message: formatMessage({ id: 'route.request.override.domain.or.ip.rules' }),
-                      },
-                    ]}
-                  >
-                    <Input
-                      placeholder={formatMessage({
-                        id: 'route.request.override.domain.name.or.ip',
-                      })}
-                      disabled={upstreamDisabled}
-                    />
-                  </Form.Item>
-                </Col>
-                <Col span={4}>
-                  <Form.Item
-                    style={{ marginBottom: 0 }}
-                    name={[field.name, 'port']}
-                    rules={[
-                      {
-                        required: true,
-                        message: formatMessage({ id: 'route.request.override.input.port.number' }),
-                      },
-                    ]}
-                  >
-                    <InputNumber
-                      placeholder={formatMessage({ id: 'route.request.override.port.number' })}
-                      disabled={upstreamDisabled}
-                      min={1}
-                      max={65535}
-                    />
-                  </Form.Item>
-                </Col>
-                <Col span={4} offset={1}>
-                  <Form.Item
-                    style={{ marginBottom: 0 }}
-                    name={[field.name, 'weight']}
-                    rules={[
-                      {
-                        required: true,
-                        message: formatMessage({ id: 'route.request.override.input.weight' }),
-                      },
-                    ]}
-                  >
-                    <InputNumber
-                      placeholder={formatMessage({ id: 'route.request.override.weight' })}
-                      disabled={upstreamDisabled}
-                      min={0}
-                      max={1000}
-                    />
-                  </Form.Item>
-                </Col>
-                <Col>
-                  {!upstreamDisabled &&
-                    (fields.length > 1 ? (
-                      <MinusCircleOutlined
-                        style={{ margin: '0 8px' }}
-                        onClick={() => {
-                          remove(field.name);
-                        }}
-                      />
-                    ) : null)}
-                </Col>
-              </Row>
-            </Form.Item>
-          ))}
-          {!upstreamDisabled && (
-            <Form.Item {...FORM_ITEM_WITHOUT_LABEL}>
-              <Button
-                type="dashed"
-                onClick={() => {
-                  add();
-                }}
-              >
-                <PlusOutlined /> {formatMessage({ id: 'route.request.override.create' })}
-              </Button>
-            </Form.Item>
-          )}
+          <Form.Item label="Hash On" name="hash_on">
+            <Select disabled={upstreamDisabled}>
+              {HASH_ON_LIST.map((item) => (
+                <Select.Option value={item} key={item}>
+                  {item}
+                </Select.Option>
+              ))}
+            </Select>
+          </Form.Item>
+          <Form.Item label="Key" name="key">
+            <Select disabled={upstreamDisabled}>
+              {HASH_KEY_LIST.map((item) => (
+                <Select.Option value={item} key={item}>
+                  {item}
+                </Select.Option>
+              ))}
+            </Select>
+          </Form.Item>
         </>
       )}
-    </Form.List>
+      <Form.List name="upstreamHostList">
+        {(fields, { add, remove }) => (
+          <>
+            {fields.map((field, index) => (
+              <Form.Item
+                required
+                key={field.key}
+                {...(index === 0 ? FORM_ITEM_LAYOUT : FORM_ITEM_WITHOUT_LABEL)}
+                label={
+                  index === 0
+                    ? formatMessage({ id: 'route.request.override.domain.name.or.ip' })
+                    : ''
+                }
+                extra={
+                  index === 0
+                    ? formatMessage({
+                        id: 'route.request.override.use.domain.name.default.analysis',
+                      })
+                    : ''
+                }
+              >
+                <Row style={{ marginBottom: '10px' }} gutter={16}>
+                  <Col span={9}>
+                    <Form.Item
+                      style={{ marginBottom: 0 }}
+                      name={[field.name, 'host']}
+                      rules={[
+                        {
+                          required: true,
+                          message: formatMessage({
+                            id: 'route.request.override.input.domain.or.ip',
+                          }),
+                        },
+                        {
+                          pattern: new RegExp(
+                            /(^([1-9]?\d|1\d{2}|2[0-4]\d|25[0-5])(\.(25[0-5]|1\d{2}|2[0-4]\d|[1-9]?\d)){3}$|^(?![0-9.]+$)([a-zA-Z0-9_-]+)(\.[a-zA-Z0-9_-]+){0,}$)/,
+                            'g',
+                          ),
+                          message: formatMessage({
+                            id: 'route.request.override.domain.or.ip.rules',
+                          }),
+                        },
+                      ]}
+                    >
+                      <Input
+                        placeholder={formatMessage({
+                          id: 'route.request.override.domain.name.or.ip',
+                        })}
+                        disabled={upstreamDisabled}
+                      />
+                    </Form.Item>
+                  </Col>
+                  <Col span={4}>
+                    <Form.Item
+                      style={{ marginBottom: 0 }}
+                      name={[field.name, 'port']}
+                      rules={[
+                        {
+                          required: true,
+                          message: formatMessage({
+                            id: 'route.request.override.input.port.number',
+                          }),
+                        },
+                      ]}
+                    >
+                      <InputNumber
+                        placeholder={formatMessage({ id: 'route.request.override.port.number' })}
+                        disabled={upstreamDisabled}
+                        min={1}
+                        max={65535}
+                      />
+                    </Form.Item>
+                  </Col>
+                  <Col span={4} offset={1}>
+                    <Form.Item
+                      style={{ marginBottom: 0 }}
+                      name={[field.name, 'weight']}
+                      rules={[
+                        {
+                          required: true,
+                          message: formatMessage({ id: 'route.request.override.input.weight' }),
+                        },
+                      ]}
+                    >
+                      <InputNumber
+                        placeholder={formatMessage({ id: 'route.request.override.weight' })}
+                        disabled={upstreamDisabled}
+                        min={0}
+                        max={1000}
+                      />
+                    </Form.Item>
+                  </Col>
+                  <Col>
+                    {!upstreamDisabled &&
+                      (fields.length > 1 ? (
+                        <MinusCircleOutlined
+                          style={{ margin: '0 8px' }}
+                          onClick={() => {
+                            remove(field.name);
+                          }}
+                        />
+                      ) : null)}
+                  </Col>
+                </Row>
+              </Form.Item>
+            ))}
+            {!upstreamDisabled && (
+              <Form.Item {...FORM_ITEM_WITHOUT_LABEL}>
+                <Button
+                  type="dashed"
+                  onClick={() => {
+                    add();
+                  }}
+                >
+                  <PlusOutlined /> {formatMessage({ id: 'route.request.override.create' })}
+                </Button>
+              </Form.Item>
+            )}
+          </>
+        )}
+      </Form.List>
+    </>
   );
 
   const renderTimeUnit = () => <span style={{ margin: '0 8px' }}>ms</span>;
diff --git a/src/pages/Route/constants.ts b/src/pages/Route/constants.ts
index 435dbe9..0620bbd 100644
--- a/src/pages/Route/constants.ts
+++ b/src/pages/Route/constants.ts
@@ -60,6 +60,7 @@ export const DEFAULT_STEP_2_DATA: RouteModule.Step2Data = {
   upstreamHostList: [{} as RouteModule.UpstreamHost],
   upstreamHeaderList: [],
   upstreamPath: undefined,
+  type: 'roundrobin',
   mappingStrategy: undefined,
   rewriteType: 'keep',
   timeout: {
@@ -82,3 +83,18 @@ export const INIT_CHART = {
   selected: {},
   hovered: {},
 };
+
+export const HASH_KEY_LIST = [
+  'remote_addr',
+  'host',
+  'uri',
+  'server_name',
+  'server_addr',
+  'request_uri',
+  'query_string',
+  'remote_port',
+  'hostname',
+  'arg_id',
+];
+
+export const HASH_ON_LIST = ['vars', 'header', 'cookie', 'consumer'];
diff --git a/src/pages/Route/transform.ts b/src/pages/Route/transform.ts
index 02ce6de..dae7d91 100644
--- a/src/pages/Route/transform.ts
+++ b/src/pages/Route/transform.ts
@@ -30,6 +30,12 @@ export const transformStepData = ({
     upstream_header[header.header_name] = header.header_value || '';
   });
 
+  const chashData: any = {};
+  if (step2Data.type === 'chash') {
+    chashData.key = step2Data.key;
+    chashData.hash_on = step2Data.hash_on;
+  }
+
   let redirect: RouteModule.Redirect = {};
   if (step1Data.redirectOption === 'disabled') {
     redirect = {};
@@ -70,7 +76,8 @@ export const transformStepData = ({
       return [key, operator, value];
     }),
     upstream: {
-      type: 'roundrobin',
+      type: step2Data.type,
+      ...chashData,
       nodes,
       timeout: step2Data.timeout,
     },
@@ -199,6 +206,9 @@ export const transformRouteData = (data: RouteModule.Body) => {
   const step2Data: RouteModule.Step2Data = {
     upstream_protocol,
     upstreamHeaderList,
+    type: upstream ? upstream.type : 'roundrobin',
+    hash_on: upstream ? upstream.hash_on : undefined,
+    key: upstream ? upstream.key : undefined,
     upstreamHostList: transformUpstreamNodes(upstream?.nodes),
     upstream_id,
     upstreamPath: upstream_path?.to,
diff --git a/src/pages/Route/typing.d.ts b/src/pages/Route/typing.d.ts
index 56d4e7a..9fc25dc 100644
--- a/src/pages/Route/typing.d.ts
+++ b/src/pages/Route/typing.d.ts
@@ -86,6 +86,9 @@ declare namespace RouteModule {
 
   type Step2Data = {
     upstream_protocol: 'http' | 'https' | 'keep';
+    type: 'roundrobin' | 'chash';
+    hash_on?: string;
+    key?: string;
     upstreamHostList: UpstreamHost[];
     mappingStrategy: string | undefined;
     rewriteType: string | undefined;
@@ -121,6 +124,8 @@ declare namespace RouteModule {
     vars: [string, Operator, string][];
     upstream: {
       type: 'roundrobin' | 'chash';
+      hash_on?: string;
+      key?: string;
       nodes: {
         [key: string]: number;
       };
diff --git a/src/pages/Upstream/components/Step1.tsx b/src/pages/Upstream/components/Step1.tsx
index 9ac7f73..cfbf2b4 100644
--- a/src/pages/Upstream/components/Step1.tsx
+++ b/src/pages/Upstream/components/Step1.tsx
@@ -21,7 +21,12 @@ import { useIntl } from 'umi';
 
 import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
 import Button from 'antd/es/button';
-import { FORM_ITEM_WITHOUT_LABEL, FORM_ITEM_LAYOUT } from '@/pages/Upstream/constants';
+import {
+  FORM_ITEM_WITHOUT_LABEL,
+  FORM_ITEM_LAYOUT,
+  HASH_KEY_LIST,
+  HASH_ON_LIST,
+} from '@/pages/Upstream/constants';
 
 type Props = {
   form: FormInstance;
@@ -41,7 +46,6 @@ const initialValues = {
 };
 
 const Step1: React.FC<Props> = ({ form, disabled }) => {
-
   const { formatMessage } = useIntl();
 
   const renderUpstreamMeta = () => (
@@ -53,7 +57,11 @@ const Step1: React.FC<Props> = ({ form, disabled }) => {
               required
               key={field.key}
               {...(index === 0 ? FORM_ITEM_LAYOUT : FORM_ITEM_WITHOUT_LABEL)}
-              label={index === 0 ? formatMessage({ id: 'upstream.step.backend.server.domain.or.ip' }) : ''}
+              label={
+                index === 0
+                  ? formatMessage({ id: 'upstream.step.backend.server.domain.or.ip' })
+                  : ''
+              }
               extra={
                 index === 0
                   ? formatMessage({ id: 'upstream.step.domain.name.default.analysis' })
@@ -66,7 +74,10 @@ const Step1: React.FC<Props> = ({ form, disabled }) => {
                     style={{ marginBottom: 0 }}
                     name={[field.name, 'host']}
                     rules={[
-                      { required: true, message: formatMessage({ id: 'upstream.step.input.domain.name.or.ip' }) },
+                      {
+                        required: true,
+                        message: formatMessage({ id: 'upstream.step.input.domain.name.or.ip' }),
+                      },
                       {
                         pattern: new RegExp(
                           /(^([1-9]?\d|1\d{2}|2[0-4]\d|25[0-5])(\.(25[0-5]|1\d{2}|2[0-4]\d|[1-9]?\d)){3}$|^(?![0-9.]+$)([a-zA-Z0-9_-]+)(\.[a-zA-Z0-9_-]+){0,}$)/,
@@ -76,25 +87,48 @@ const Step1: React.FC<Props> = ({ form, disabled }) => {
                       },
                     ]}
                   >
-                    <Input placeholder={formatMessage({ id: 'upstream.step.domain.name.or.ip' })} disabled={disabled} />
+                    <Input
+                      placeholder={formatMessage({ id: 'upstream.step.domain.name.or.ip' })}
+                      disabled={disabled}
+                    />
                   </Form.Item>
                 </Col>
                 <Col span={3}>
                   <Form.Item
                     style={{ marginBottom: 0 }}
                     name={[field.name, 'port']}
-                    rules={[{ required: true, message: formatMessage({ id: 'upstream.step.input.port' }) }]}
+                    rules={[
+                      {
+                        required: true,
+                        message: formatMessage({ id: 'upstream.step.input.port' }),
+                      },
+                    ]}
                   >
-                    <InputNumber placeholder={formatMessage({ id: 'upstream.step.port' })} disabled={disabled} min={1} max={65535} />
+                    <InputNumber
+                      placeholder={formatMessage({ id: 'upstream.step.port' })}
+                      disabled={disabled}
+                      min={1}
+                      max={65535}
+                    />
                   </Form.Item>
                 </Col>
                 <Col span={3}>
                   <Form.Item
                     style={{ marginBottom: 0 }}
                     name={[field.name, 'weight']}
-                    rules={[{ required: true, message: formatMessage({ id: 'upstream.step.input.weight' }) }]}
+                    rules={[
+                      {
+                        required: true,
+                        message: formatMessage({ id: 'upstream.step.input.weight' }),
+                      },
+                    ]}
                   >
-                    <InputNumber placeholder={formatMessage({ id: 'upstream.step.weight' })} disabled={disabled} min={0} max={1000} />
+                    <InputNumber
+                      placeholder={formatMessage({ id: 'upstream.step.weight' })}
+                      disabled={disabled}
+                      min={0}
+                      max={1000}
+                    />
                   </Form.Item>
                 </Col>
                 <Col
@@ -125,7 +159,8 @@ const Step1: React.FC<Props> = ({ form, disabled }) => {
                   add();
                 }}
               >
-                <PlusOutlined />{formatMessage({ id: 'upstream.step.create' })}
+                <PlusOutlined />
+                {formatMessage({ id: 'upstream.step.create' })}
               </Button>
             </Form.Item>
           )}
@@ -138,24 +173,73 @@ const Step1: React.FC<Props> = ({ form, disabled }) => {
 
   return (
     <Form {...FORM_ITEM_LAYOUT} form={form} initialValues={initialValues}>
-      <Form.Item label={formatMessage({ id: 'upstream.step.name' })} name="name" rules={[{ required: true }]} extra={formatMessage({ id: 'upstream.step.name.should.unique' })}>
-        <Input placeholder={formatMessage({ id: 'upstream.step.input.upstream.name' })} disabled={disabled} />
+      <Form.Item
+        label={formatMessage({ id: 'upstream.step.name' })}
+        name="name"
+        rules={[{ required: true }]}
+        extra={formatMessage({ id: 'upstream.step.name.should.unique' })}
+      >
+        <Input
+          placeholder={formatMessage({ id: 'upstream.step.input.upstream.name' })}
+          disabled={disabled}
+        />
       </Form.Item>
       <Form.Item label={formatMessage({ id: 'upstream.step.description' })} name="description">
-        <Input.TextArea placeholder={formatMessage({ id: 'upstream.step.input.description' })} disabled={disabled} />
+        <Input.TextArea
+          placeholder={formatMessage({ id: 'upstream.step.input.description' })}
+          disabled={disabled}
+        />
       </Form.Item>
-      <Form.Item label={formatMessage({ id: 'upstream.step.type' })} name="type" rules={[{ required: true }]}>
+      <Form.Item
+        label={formatMessage({ id: 'upstream.step.type' })}
+        name="type"
+        rules={[{ required: true }]}
+      >
         <Select disabled={disabled}>
           <Select.Option value="roundrobin">roundrobin</Select.Option>
-          {/* TODO: chash */}
+          <Select.Option value="chash">chash</Select.Option>
         </Select>
       </Form.Item>
+      <Form.Item shouldUpdate>
+        {() => {
+          if (form.getFieldValue('type') === 'chash') {
+            return (
+              <>
+                <Form.Item label="Hash On" name="hash_on" labelCol={{ span: 6 }}>
+                  <Select disabled={disabled}>
+                    {HASH_ON_LIST.map((item) => (
+                      <Select.Option value={item} key={item}>
+                        {item}
+                      </Select.Option>
+                    ))}
+                  </Select>
+                </Form.Item>
+                <Form.Item label="Key" name="key" labelCol={{ span: 6 }}>
+                  <Select disabled={disabled}>
+                    {HASH_KEY_LIST.map((item) => (
+                      <Select.Option value={item} key={item}>
+                        {item}
+                      </Select.Option>
+                    ))}
+                  </Select>
+                </Form.Item>
+              </>
+            );
+          }
+          return null;
+        }}
+      </Form.Item>
       {renderUpstreamMeta()}
       <Form.Item label={formatMessage({ id: 'upstream.step.connect.timeout' })} required>
         <Form.Item
           name={['timeout', 'connect']}
           noStyle
-          rules={[{ required: true, message: formatMessage({ id: 'upstream.step.input.connect.timeout' }) }]}
+          rules={[
+            {
+              required: true,
+              message: formatMessage({ id: 'upstream.step.input.connect.timeout' }),
+            },
+          ]}
         >
           <InputNumber disabled={disabled} />
         </Form.Item>
@@ -165,7 +249,9 @@ const Step1: React.FC<Props> = ({ form, disabled }) => {
         <Form.Item
           name={['timeout', 'send']}
           noStyle
-          rules={[{ required: true, message: formatMessage({ id: 'upstream.step.input.send.timeout' }) }]}
+          rules={[
+            { required: true, message: formatMessage({ id: 'upstream.step.input.send.timeout' }) },
+          ]}
         >
           <InputNumber disabled={disabled} />
         </Form.Item>
@@ -175,7 +261,12 @@ const Step1: React.FC<Props> = ({ form, disabled }) => {
         <Form.Item
           name={['timeout', 'read']}
           noStyle
-          rules={[{ required: true, message: formatMessage({ id: 'upstream.step.input.receive.timeout' }) }]}
+          rules={[
+            {
+              required: true,
+              message: formatMessage({ id: 'upstream.step.input.receive.timeout' }),
+            },
+          ]}
         >
           <InputNumber disabled={disabled} />
         </Form.Item>
diff --git a/src/pages/Upstream/constants.ts b/src/pages/Upstream/constants.ts
index 25bb12f..c9e2306 100644
--- a/src/pages/Upstream/constants.ts
+++ b/src/pages/Upstream/constants.ts
@@ -18,9 +18,6 @@ export const FORM_ITEM_LAYOUT = {
   labelCol: {
     span: 6,
   },
-  wrapperCol: {
-    span: 18,
-  },
 };
 
 export const FORM_ITEM_WITHOUT_LABEL = {
@@ -29,3 +26,18 @@ export const FORM_ITEM_WITHOUT_LABEL = {
     sm: { span: 20, offset: 6 },
   },
 };
+
+export const HASH_KEY_LIST = [
+  'remote_addr',
+  'host',
+  'uri',
+  'server_name',
+  'server_addr',
+  'request_uri',
+  'query_string',
+  'remote_port',
+  'hostname',
+  'arg_id',
+];
+
+export const HASH_ON_LIST = ['vars', 'header', 'cookie', 'consumer'];