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/05/11 06:36:24 UTC

[apisix-dashboard] branch master updated: feat: refactor Plugin Orchestration (#1813)

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 423c9e8  feat: refactor Plugin Orchestration (#1813)
423c9e8 is described below

commit 423c9e899681af166ee2e83038ddb07d62a2f38c
Author: 琚致远 <ju...@apache.org>
AuthorDate: Tue May 11 14:36:16 2021 +0800

    feat: refactor Plugin Orchestration (#1813)
---
 .actions/ASF-Release.cfg                           |   5 +-
 .github/workflows/frontend-plugin-e2e-test.yml     |   4 +
 LICENSE                                            |   1 +
 licenses/LICENSE-antvis-x6.txt                     |  21 ++
 .../create-route-with-plugin-orchestration.spec.js | 132 +++++++
 web/cypress/support/commands.js                    |   1 +
 web/package.json                                   |   4 +-
 web/src/components/Plugin/PluginDetail.tsx         | 299 ++++++++--------
 web/src/components/Plugin/service.ts               |  14 +
 web/src/components/PluginFlow/PluginFlow.tsx       | 203 +++++++++++
 .../PluginFlow/components/FlowGraph/FlowGraph.ts   | 394 +++++++++++++++++++++
 .../components/FlowGraph}/index.ts                 |   3 +-
 .../PluginFlow/components/FlowGraph/shapes.ts      | 161 +++++++++
 .../PluginFlow/components/Toolbar/index.tsx        | 195 ++++++++++
 web/src/components/PluginFlow/constants.ts         | 294 +++++++++++++++
 .../components/Page.tsx => PluginFlow/index.ts}    |   5 +-
 web/src/components/PluginFlow/locales/en-US.ts     |  41 +++
 web/src/components/PluginFlow/locales/zh-CN.ts     |  41 +++
 .../typing.d.ts => PluginFlow/style.less}          |  34 +-
 .../PluginOrchestration/DrawPluginStyle.ts         |  82 -----
 .../PluginOrchestration/components/SidebarItem.tsx |  42 ---
 .../components/PluginOrchestration/constants.ts    |  62 ----
 .../PluginOrchestration/customConfig.tsx           |  77 ----
 web/src/components/PluginOrchestration/index.tsx   | 289 ---------------
 .../PluginOrchestration/locales/en-US.ts           |  30 --
 .../PluginOrchestration/locales/zh-CN.ts           |  30 --
 web/src/components/PluginOrchestration/service.ts  |  25 --
 .../components/PluginOrchestration/transform.ts    | 122 -------
 web/src/locales/en-US.ts                           |   4 +-
 web/src/locales/zh-CN.ts                           |   4 +-
 web/src/locales/zh-CN/component.ts                 |   2 +-
 web/src/pages/Route/Create.tsx                     |  61 +++-
 .../Route/components/CreateStep4/CreateStep4.tsx   |  10 +-
 .../Route/components/DebugViews/DebugDrawView.tsx  |  24 +-
 web/src/pages/Route/components/Step3/index.tsx     |  42 ++-
 web/src/pages/Route/constants.ts                   |   9 -
 web/src/pages/Route/locales/en-US.ts               |   4 +-
 web/src/pages/Route/locales/zh-CN.ts               |   2 +-
 web/src/pages/Service/locales/en-US.ts             |   4 +-
 web/yarn.lock                                      | 167 ++++++---
 40 files changed, 1901 insertions(+), 1043 deletions(-)

diff --git a/.actions/ASF-Release.cfg b/.actions/ASF-Release.cfg
index 341e221..9bf3581 100644
--- a/.actions/ASF-Release.cfg
+++ b/.actions/ASF-Release.cfg
@@ -69,11 +69,12 @@ ASFLicenseHeaderLua.txt
 
 # Skip files containing MIT License
 web/scripts/verifyCommit.js
-web/src/components/HeaderDropdown/index.less
-web/src/components/HeaderDropdown/index.tsx
+web/src/components/HeaderDropdown
 web/src/components/NoticeIcon
 web/src/components/PageLoading/index.tsx
 web/src/components/RightContent
+web/src/components/PluginFlow/components/ConfigPanel
+web/src/components/PluginFlow/components/Toolbar
 web/src/e2e/__mocks__/antd-pro-merge-less.js
 web/src/e2e/baseLayout.e2e.js
 web/src/pages/404.tsx
diff --git a/.github/workflows/frontend-plugin-e2e-test.yml b/.github/workflows/frontend-plugin-e2e-test.yml
index d1d7e8f..1ce91d6 100644
--- a/.github/workflows/frontend-plugin-e2e-test.yml
+++ b/.github/workflows/frontend-plugin-e2e-test.yml
@@ -44,6 +44,10 @@ jobs:
         with:
           go-version: '1.13'
 
+      - name: Download dag-to-lua
+        working-directory: ./
+        run: make dag-lib
+
       - name: Start manager-api
         working-directory: ./api
         run: |
diff --git a/LICENSE b/LICENSE
index 112687a..36ca18c 100644
--- a/LICENSE
+++ b/LICENSE
@@ -216,6 +216,7 @@ The following components are provided under the MIT License. See project link fo
 The text of each license is also included at licenses/LICENSE-[project].txt.
 
    files from ant-design-pro: https://github.com/ant-design/ant-design-pro MIT
+   files from antvis-x6: https://github.com/antvis/X6 MIT
    files from json.lua: https://github.com/rxi/json.lua MIT
 
 ========================================================================
diff --git a/licenses/LICENSE-antvis-x6.txt b/licenses/LICENSE-antvis-x6.txt
new file mode 100644
index 0000000..c17ba2f
--- /dev/null
+++ b/licenses/LICENSE-antvis-x6.txt
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2019 Alipay.inc
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/web/cypress/integration/plugin/create-route-with-plugin-orchestration.spec.js b/web/cypress/integration/plugin/create-route-with-plugin-orchestration.spec.js
new file mode 100644
index 0000000..867baad
--- /dev/null
+++ b/web/cypress/integration/plugin/create-route-with-plugin-orchestration.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('Create and delete route with plugin orchestration', () => {
+  const selector = {
+    empty: '.ant-empty-normal',
+    name: '#name',
+    description: '#desc',
+    nodes_0_host: '#nodes_0_host',
+    nodes_0_port: '#nodes_0_port',
+    nodes_0_weight: '#nodes_0_weight',
+    groupButton: '.ant-radio-group',
+    canvas: '.x6-graph-svg',
+    startNode:
+      '#stencil > div > div.x6-widget-stencil-content > div:nth-child(1) > div > div > svg > g > g.x6-graph-svg-stage > g:nth-child(1) > g > circle',
+    notification: '.ant-notification-notice-message',
+    notificationClose: '.anticon-close',
+    nodeInput: '.x6-widget-stencil input[type="search"]',
+    hiddenGroup: '.x6-widget-stencil-group.unmatched',
+    canvasNode: '#container > svg > g > g.x6-graph-svg-stage > g:nth-child(2) > rect',
+    canvasContainer: '#container',
+    drawer: '.ant-drawer-content',
+    deleteAlert: '.ant-modal-body',
+    codemirrorScroll: '.CodeMirror-scroll',
+  };
+
+  beforeEach(() => {
+    cy.login();
+  });
+
+  it('should create route with plugin orchestration', function () {
+    cy.visit('/');
+    cy.contains('Route').click();
+    cy.get(selector.empty).should('be.visible');
+    cy.contains('Create').click();
+    cy.contains('Next').click().click();
+    cy.get(selector.name).type('routeName');
+    cy.get(selector.description).type('desc');
+    cy.contains('Next').click();
+
+    cy.get(selector.nodes_0_host).type('127.0.0.1');
+    cy.get(selector.nodes_0_port).clear().type('80');
+    cy.get(selector.nodes_0_weight).clear().type('1');
+    cy.contains('Next').click();
+
+    cy.get(selector.groupButton).contains('Orchestration').click();
+    cy.get(selector.canvas).should('be.visible');
+
+    // Plugin Orchestration
+    cy.get(selector.startNode).move({ x: 400, y: 0, force: true, position: 'center' });
+    cy.contains('Next').click();
+    cy.get(selector.notification).should('contain', 'Root node not found');
+    cy.get(selector.notificationClose).click().should('not.be.visible');
+
+    cy.get(selector.nodeInput).type('key-auth');
+    cy.get(selector.hiddenGroup).should('not.be.visible');
+    cy.contains('key-auth').move({ x: 300, y: 0, force: true, position: 'center' });
+    cy.contains('Next').click();
+    cy.get(selector.notification).should('contain', 'Root node not found');
+    cy.get(selector.notificationClose).click().should('not.be.visible');
+
+    // Linking nodes
+    cy.get(selector.canvasNode)
+      .click()
+      .then(() => {
+        const node2 = cy
+          .get('#container > svg > g > g.x6-graph-svg-stage > g:nth-child(2) > g > circle')
+          .eq(0);
+        const node1 = cy
+          .get('#container > svg > g > g.x6-graph-svg-stage > g:nth-child(1) > g > circle')
+          .eq(0);
+        node1
+          .trigger('mousedown')
+          .trigger('mousemove', { x: 0, y: 150, force: true })
+          .trigger('mouseup', { force: true });
+      });
+
+    cy.contains('Next').click();
+    cy.get(selector.notification).should('contain', 'Found node without configuration');
+    cy.get(selector.notificationClose).click().should('not.be.visible');
+
+    // Configuration plugins and submit
+    cy.get(selector.canvasContainer)
+      .click()
+      .within(() => {
+        cy.contains('key-auth').dblclick();
+      });
+    cy.contains('Submit').click();
+    cy.get(selector.drawer).should('not.exist');
+    cy.contains('Next').click();
+    cy.contains('Submit').click();
+
+    cy.contains('Submit Successfully');
+    cy.contains('Goto List').click();
+    cy.url().should('contains', 'routes/list');
+  });
+
+  it('should view and delete the route', function () {
+    cy.visit('/routes/list');
+    cy.contains('routeName').siblings().contains('More').click();
+    cy.contains('View').click();
+    cy.get(selector.codemirrorScroll).within(() => {
+      cy.contains('script').should('exist');
+    });
+    cy.contains('Cancel').click();
+
+    // Delete the route
+    cy.contains('routeName').siblings().contains('More').click();
+    cy.contains('Delete').click();
+    cy.get(selector.deleteAlert)
+      .should('be.visible')
+      .within(() => {
+        cy.contains('OK').click();
+      });
+    cy.get(selector.notification).should('contain', 'Delete Route Successfully');
+  });
+});
diff --git a/web/cypress/support/commands.js b/web/cypress/support/commands.js
index 4592efb..9a8a1f9 100644
--- a/web/cypress/support/commands.js
+++ b/web/cypress/support/commands.js
@@ -17,6 +17,7 @@
 /* eslint-disable no-undef */
 import defaultSettings from '../../config/defaultSettings';
 import 'cypress-file-upload';
+import '@4tw/cypress-drag-drop';
 
 Cypress.Commands.add('login', () => {
   const { SERVE_ENV = 'dev' } = Cypress.env();
diff --git a/web/package.json b/web/package.json
index 23337e4..8f425a4 100644
--- a/web/package.json
+++ b/web/package.json
@@ -52,7 +52,8 @@
     "@ant-design/icons": "^4.0.0",
     "@ant-design/pro-layout": "^6.0.0",
     "@ant-design/pro-table": "2.30.1",
-    "@mrblenny/react-flow-chart": "^0.0.14",
+    "@antv/x6": "^1.18.5",
+    "@antv/x6-react-components": "^1.1.7",
     "@rjsf/antd": "2.2.0",
     "@rjsf/core": "2.2.0",
     "@types/js-yaml": "^4.0.0",
@@ -88,6 +89,7 @@
     "yaml": "^1.10.0"
   },
   "devDependencies": {
+    "@4tw/cypress-drag-drop": "^1.6.0",
     "@ant-design/pro-cli": "^2.0.2",
     "@cypress/code-coverage": "^3.9.2",
     "@types/base-64": "^0.1.3",
diff --git a/web/src/components/Plugin/PluginDetail.tsx b/web/src/components/Plugin/PluginDetail.tsx
index addaf3c..0028116 100644
--- a/web/src/components/Plugin/PluginDetail.tsx
+++ b/web/src/components/Plugin/PluginDetail.tsx
@@ -53,6 +53,7 @@ type Props = {
   readonly?: boolean;
   visible: boolean;
   maskClosable?: boolean;
+  isEnabled?: boolean;
   onClose?: () => void;
   onChange?: (data: any) => void;
 };
@@ -91,6 +92,7 @@ const PluginDetail: React.FC<Props> = ({
   pluginList = [],
   readonly = false,
   maskClosable = true,
+  isEnabled = false,
   initialData = {},
   onClose = () => { },
   onChange = () => { },
@@ -144,7 +146,7 @@ const PluginDetail: React.FC<Props> = ({
 
   useEffect(() => {
     form.setFieldsValue({
-      disable: initialData[name] && !initialData[name].disable,
+      disable: isEnabled ? true : (initialData[name] && !initialData[name].disable),
       scope: 'global',
     });
     if (PLUGIN_UI_LIST.includes(name)) {
@@ -272,75 +274,77 @@ const PluginDetail: React.FC<Props> = ({
     }
   };
 
-  return (
-    <>
-      <Drawer
-        title={formatMessage({ id: 'component.plugin.editor' })}
-        visible={visible}
-        placement="right"
-        closable={false}
-        maskClosable={maskClosable}
-        onClose={onClose}
-        width={700}
-        footer={
-          <div style={{ display: 'flex', justifyContent: 'space-between' }}>
-            {' '}
-            <Button onClick={onClose} key={1}>
-              {formatMessage({ id: 'component.global.cancel' })}
-            </Button>
-            <Space>
-              <Popconfirm
-                title={formatMessage({ id: 'page.plugin.drawer.popconfirm.title.delete' })}
-                okText={formatMessage({ id: 'component.global.confirm' })}
-                cancelText={formatMessage({ id: 'component.global.cancel' })}
-                disabled={readonly}
-                onConfirm={() => {
-                  onChange({
-                    formData: form.getFieldsValue(),
-                    codemirrorData: {},
-                    shouldDelete: true,
-                  });
-                }}
-              >
-                {initialData[name] ? (
-                  <Button key={3} type="primary" danger disabled={readonly}>
-                    {formatMessage({ id: 'component.global.delete' })}
-                  </Button>
-                ) : null}
-              </Popconfirm>
-              <Button
-                key={2}
-                disabled={readonly}
-                type="primary"
-                onClick={() => {
-                  try {
-                    let editorData;
-                    if (codeMirrorMode === codeMirrorModeList.JSON) {
-                      editorData = JSON.parse(ref.current?.editor.getValue());
-                    } else if (codeMirrorMode === codeMirrorModeList.YAML) {
-                      editorData = yaml2json(ref.current?.editor.getValue(), false).data;
-                    } else {
-                      editorData = getUIFormData();
-                    }
+  const isNoConfigurationRequired = pluginType === PluginType.authentication && schemaType !== 'consumer' && (codeMirrorMode !== codeMirrorModeList.UIForm)
 
-                    validateData(name, editorData).then((value) => {
-                      onChange({ formData: form.getFieldsValue(), codemirrorData: value });
-                    });
-                  } catch (error) {
-                    notification.error({
-                      message: 'Invalid JSON data',
-                    });
+  return (
+    <Drawer
+      title={formatMessage({ id: 'component.plugin.editor' })}
+      visible={visible}
+      placement="right"
+      closable={false}
+      maskClosable={maskClosable}
+      destroyOnClose
+      onClose={onClose}
+      width={700}
+      footer={
+        <div style={{ display: 'flex', justifyContent: 'space-between' }}>
+          {' '}
+          <Button onClick={onClose} key={1}>
+            {formatMessage({ id: 'component.global.cancel' })}
+          </Button>
+          <Space>
+            <Popconfirm
+              title={formatMessage({ id: 'page.plugin.drawer.popconfirm.title.delete' })}
+              okText={formatMessage({ id: 'component.global.confirm' })}
+              cancelText={formatMessage({ id: 'component.global.cancel' })}
+              disabled={readonly}
+              onConfirm={() => {
+                onChange({
+                  formData: form.getFieldsValue(),
+                  codemirrorData: {},
+                  shouldDelete: true,
+                });
+              }}
+            >
+              {initialData[name] ? (
+                <Button key={3} type="primary" danger disabled={readonly}>
+                  {formatMessage({ id: 'component.global.delete' })}
+                </Button>
+              ) : null}
+            </Popconfirm>
+            <Button
+              key={2}
+              disabled={readonly}
+              type="primary"
+              onClick={() => {
+                try {
+                  let editorData;
+                  if (codeMirrorMode === codeMirrorModeList.JSON) {
+                    editorData = JSON.parse(ref.current?.editor.getValue());
+                  } else if (codeMirrorMode === codeMirrorModeList.YAML) {
+                    editorData = yaml2json(ref.current?.editor.getValue(), false).data;
+                  } else {
+                    editorData = getUIFormData();
                   }
-                }}
-              >
-                {formatMessage({ id: 'component.global.submit' })}
-              </Button>
-            </Space>
-          </div>
-        }
-      >
-        <style>
-          {`
+
+                  validateData(name, editorData).then((value) => {
+                    onChange({ formData: form.getFieldsValue(), codemirrorData: value });
+                  });
+                } catch (error) {
+                  notification.error({
+                    message: 'Invalid JSON data',
+                  });
+                }
+              }}
+            >
+              {formatMessage({ id: 'component.global.submit' })}
+            </Button>
+          </Space>
+        </div>
+      }
+    >
+      <style>
+        {`
         .site-page-header {
           border: 1px solid rgb(235, 237, 240);
           margin-top:10px;
@@ -349,89 +353,88 @@ const PluginDetail: React.FC<Props> = ({
           color: #000;
         }
       `}
-        </style>
+      </style>
 
-        <Form {...FORM_ITEM_LAYOUT} style={{ marginTop: '10px' }} form={form}>
-          <Form.Item label={formatMessage({ id: 'component.global.name' })}>
-            <Input value={name} bordered={false} disabled />
+      <Form {...FORM_ITEM_LAYOUT} style={{ marginTop: '10px' }} form={form}>
+        <Form.Item label={formatMessage({ id: 'component.global.name' })}>
+          <Input value={name} bordered={false} disabled />
+        </Form.Item>
+        <Form.Item label={formatMessage({ id: 'component.global.enable' })} valuePropName="checked" name="disable">
+          <Switch
+            defaultChecked={isEnabled ? true : initialData[name] && !initialData[name].disable}
+            disabled={readonly || isEnabled}
+          />
+        </Form.Item>
+        {type === 'global' && (
+          <Form.Item label={formatMessage({ id: 'component.global.scope' })} name="scope">
+            <Select disabled>
+              <Select.Option value="global">{formatMessage({ id: "other.global" })}</Select.Option>
+            </Select>
           </Form.Item>
-          <Form.Item label={formatMessage({ id: 'component.global.enable' })} valuePropName="checked" name="disable">
-            <Switch
-              defaultChecked={initialData[name] && !initialData[name].disable}
-              disabled={readonly}
-            />
-          </Form.Item>
-          {type === 'global' && (
-            <Form.Item label={formatMessage({ id: 'component.global.scope' })} name="scope">
-              <Select disabled>
-                <Select.Option value="global">{formatMessage({ id: "other.global" })}</Select.Option>
-              </Select>
-            </Form.Item>
-          )}
-        </Form>
-        <Divider orientation="left">{formatMessage({ id: 'component.global.data.editor' })}</Divider>
-        <PageHeader
-          title=""
-          subTitle={
-            pluginType === PluginType.authentication && schemaType !== 'consumer' && (codeMirrorMode !== codeMirrorModeList.UIForm) ? (
-              <Alert message={formatMessage({ id: 'component.plugin.noConfigurationRequired' })} type="warning" />
-            ) : null
-          }
-          ghost={false}
-          extra={[
-            <Select
-              defaultValue={codeMirrorModeList.JSON}
-              value={codeMirrorMode}
-              options={modeOptions}
-              onChange={(value: PluginComponent.CodeMirrorMode) => {
-                handleModeChange(value);
-              }}
-              data-cy='code-mirror-mode'
-              key={1}
-            ></Select>,
-            <Tooltip title={formatMessage({ id: "component.plugin.format-codes.disable" })} key={2}>
-              <Button type="primary" onClick={formatCodes} disabled={codeMirrorMode === codeMirrorModeList.UIForm}>
-                {formatMessage({ id: 'component.global.format' })}
-              </Button>
-            </Tooltip>,
-            <Button
-              type="default"
-              icon={<LinkOutlined />}
-              onClick={() => {
-                if (name.startsWith('serverless')) {
-                  window.open('https://apisix.apache.org/docs/apisix/plugins/serverless');
-                } else {
-                  window.open(`https://apisix.apache.org/docs/apisix/plugins/${name}`);
-                }
-              }}
-              key={3}
-            >
-              {formatMessage({ id: 'component.global.document' })}
+        )}
+      </Form>
+      <Divider orientation="left">{formatMessage({ id: 'component.global.data.editor' })}</Divider>
+      <PageHeader
+        title=""
+        subTitle={
+          isNoConfigurationRequired ? (
+            <Alert message={formatMessage({ id: 'component.plugin.noConfigurationRequired' })} type="warning" />
+          ) : null
+        }
+        ghost={false}
+        extra={[
+          <Select
+            defaultValue={codeMirrorModeList.JSON}
+            value={codeMirrorMode}
+            options={modeOptions}
+            onChange={(value: PluginComponent.CodeMirrorMode) => {
+              handleModeChange(value);
+            }}
+            data-cy='code-mirror-mode'
+            key={1}
+          ></Select>,
+          <Tooltip title={formatMessage({ id: "component.plugin.format-codes.disable" })} key={2}>
+            <Button type="primary" onClick={formatCodes} disabled={codeMirrorMode === codeMirrorModeList.UIForm}>
+              {formatMessage({ id: 'component.global.format' })}
             </Button>
-          ]}
-        />
-        {Boolean(codeMirrorMode === codeMirrorModeList.UIForm) && <PluginForm name={name} form={UIForm} renderForm={!(pluginType === PluginType.authentication && schemaType !== 'consumer')} />}
-        <div style={{ display: codeMirrorMode === codeMirrorModeList.UIForm ? 'none' : 'unset' }}><CodeMirror
-          ref={(codemirror) => {
-            ref.current = codemirror;
-            if (codemirror) {
-              // NOTE: for debug & test
-              // @ts-ignore
-              window.codemirror = codemirror.editor;
-            }
-          }}
-          value={JSON.stringify(data, null, 2)}
-          options={{
-            mode: codeMirrorMode,
-            readOnly: readonly ? 'nocursor' : '',
-            lineWrapping: true,
-            lineNumbers: true,
-            showCursorWhenSelecting: true,
-            autofocus: true,
-          }} />
-        </div>
-      </Drawer>
-    </>
+          </Tooltip>,
+          <Button
+            type="default"
+            icon={<LinkOutlined />}
+            onClick={() => {
+              if (name.startsWith('serverless')) {
+                window.open('https://apisix.apache.org/docs/apisix/plugins/serverless');
+              } else {
+                window.open(`https://apisix.apache.org/docs/apisix/plugins/${name}`);
+              }
+            }}
+            key={3}
+          >
+            {formatMessage({ id: 'component.global.document' })}
+          </Button>
+        ]}
+      />
+      {Boolean(codeMirrorMode === codeMirrorModeList.UIForm) && <PluginForm name={name} form={UIForm} renderForm={!(pluginType === PluginType.authentication && schemaType !== 'consumer')} />}
+      <div style={{ display: codeMirrorMode === codeMirrorModeList.UIForm ? 'none' : 'unset' }}><CodeMirror
+        ref={(codemirror) => {
+          ref.current = codemirror;
+          if (codemirror) {
+            // NOTE: for debug & test
+            // @ts-ignore
+            window.codemirror = codemirror.editor;
+          }
+        }}
+        value={JSON.stringify(data, null, 2)}
+        options={{
+          mode: codeMirrorMode,
+          readOnly: (readonly || isNoConfigurationRequired) ? 'nocursor' : '',
+          lineWrapping: true,
+          lineNumbers: true,
+          showCursorWhenSelecting: true,
+          autofocus: true,
+        }} />
+      </div>
+    </Drawer>
   );
 };
 
diff --git a/web/src/components/Plugin/service.ts b/web/src/components/Plugin/service.ts
index 89c7fee..98f04fc 100644
--- a/web/src/components/Plugin/service.ts
+++ b/web/src/components/Plugin/service.ts
@@ -19,7 +19,17 @@ import { request } from 'umi';
 
 import { PLUGIN_LIST, PluginType } from './data';
 
+const cached: {
+  list: PluginComponent.Meta[]
+} = {
+  list: []
+}
+
 export const fetchList = () => {
+  if (cached.list.length) {
+    return Promise.resolve(cached.list)
+  }
+
   return request<Res<PluginComponent.Meta[]>>('/plugins?all=true').then((data) => {
     const typedData = data.data.map(item => ({
       ...item,
@@ -33,6 +43,10 @@ export const fetchList = () => {
       finalList = finalList.concat(typedData.filter(item => item.type === type))
     })
 
+    if (cached.list.length === 0) {
+      cached.list = finalList
+    }
+
     return finalList
   });
 };
diff --git a/web/src/components/PluginFlow/PluginFlow.tsx b/web/src/components/PluginFlow/PluginFlow.tsx
new file mode 100644
index 0000000..f49130c
--- /dev/null
+++ b/web/src/components/PluginFlow/PluginFlow.tsx
@@ -0,0 +1,203 @@
+/*
+ * 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.
+ */
+import React, { useEffect, useState } from 'react'
+import { Modal, Form, Input, Alert } from 'antd'
+import { Cell } from '@antv/x6'
+import { useIntl } from 'umi'
+
+import FlowGraph from './components/FlowGraph'
+import Toolbar from './components/Toolbar'
+import { DEFAULT_CONDITION_PROPS, DEFAULT_PLUGIN_PROPS, DEFAULT_STENCIL_WIDTH, DEFAULT_TOOLBAR_HEIGHT, FlowGraphEvent } from './constants'
+import styles from './style.less'
+import PluginDetail from '../Plugin/PluginDetail'
+import { fetchList } from '../Plugin/service'
+
+type Props = {
+  chart: {
+    cells: Cell.Properties[];
+  };
+  readonly?: boolean;
+}
+
+type PluginProps = {
+  id: string;
+  name: string;
+  visible: boolean;
+  data: any;
+}
+
+type ConditionProps = {
+  id: string;
+  visible: boolean;
+  data: string;
+}
+
+const PluginFlow: React.FC<Props> = ({ chart, readonly = false }) => {
+  const { formatMessage } = useIntl()
+
+  // NOTE: To prevent from graph is not initialized
+  const [isReady, setIsReady] = useState(false)
+  const [plugins, setPlugins] = useState<PluginComponent.Meta[]>([])
+
+  const [pluginProps, setPluginProps] = useState<PluginProps>(DEFAULT_PLUGIN_PROPS)
+  const [conditionProps, setConditionProps] = useState<ConditionProps>(DEFAULT_CONDITION_PROPS)
+
+  const getContainerSize = () => {
+    const leftSidebar = document.querySelector('aside.ant-layout-sider')
+    const blankSpaceWidth = 24 * 4
+
+    const globalHeaderHeight = 48
+    const pageHeaderHeight = 72
+    const otherHeight = 191
+
+    const width = document.body.offsetWidth - (leftSidebar?.clientWidth || 0) - blankSpaceWidth - DEFAULT_STENCIL_WIDTH
+    const height = document.body.offsetHeight - globalHeaderHeight - pageHeaderHeight - otherHeight
+
+    return {
+      width,
+      height: height < 800 ? 800 : height
+    }
+  }
+
+  useEffect(() => {
+    if (!plugins.length) {
+      return
+    }
+
+    const container = document.getElementById("container")
+    if (!container) {
+      return
+    }
+
+    const siderbarCollapsedButton = document.querySelector('.ant-pro-sider-collapsed-button')
+
+    const graph = FlowGraph.init(container, plugins, chart);
+    (window as any).graph = FlowGraph
+    setIsReady(true)
+
+    const stencilContainer = document.querySelector('#stencil') as HTMLElement
+
+    const handleResize = () => {
+      const { width, height } = getContainerSize()
+      graph.resize(width, height)
+
+      stencilContainer.style.height = `${height + DEFAULT_TOOLBAR_HEIGHT}px`
+      stencilContainer.style.width = `${DEFAULT_STENCIL_WIDTH}px`
+    }
+
+    const handleLeftSidebarResize = () => {
+      setTimeout(() => {
+        handleResize()
+      }, 200)
+    }
+
+    handleResize()
+
+    graph.on(FlowGraphEvent.PLUGIN_CHANGE, setPluginProps)
+    graph.on(FlowGraphEvent.CONDITION_CHANGE, (props: ConditionProps) => {
+      setConditionProps(props)
+    })
+
+    if (readonly) {
+      graph.disableKeyboard()
+    }
+
+    window.addEventListener("resize", handleResize)
+    siderbarCollapsedButton?.addEventListener('click', handleLeftSidebarResize)
+    // eslint-disable-next-line
+    return () => {
+      window.removeEventListener("resize", handleResize)
+      siderbarCollapsedButton?.removeEventListener('click', handleLeftSidebarResize)
+    }
+  }, [plugins])
+
+  useEffect(() => {
+    fetchList().then(setPlugins)
+  }, [])
+
+  return (
+    <React.Fragment>
+      {readonly && <Alert type="warning" message={formatMessage({ id: 'component.plugin-flow.text.preview.readonly' })} showIcon style={{ marginBottom: 20 }} />}
+      <div className={styles.container}>
+        <div id="stencil" className={styles.stencil} style={readonly ? { width: 0, height: 0 } : {}} />
+        <div className={styles.panel}>
+          <div className={styles.toolbar}>{isReady && <Toolbar />}</div>
+          <div id="container" className={styles.flow}></div>
+        </div>
+      </div>
+      {
+        pluginProps.visible && (
+          <PluginDetail
+            readonly={readonly}
+            schemaType="route"
+            name={pluginProps.name}
+            visible={pluginProps.visible}
+            pluginList={plugins}
+            isEnabled
+            initialData={{
+              // NOTE: We use {PluginName: data} because initialData is all plugins' data
+              [pluginProps.name]: pluginProps.data
+            }}
+            onClose={() => {
+              setPluginProps(DEFAULT_PLUGIN_PROPS)
+            }}
+            onChange={({ formData, codemirrorData, shouldDelete }) => {
+              if (shouldDelete) {
+                FlowGraph.graph.removeCell(pluginProps.id)
+              } else {
+                const disable = !formData.disable
+                FlowGraph.setData(pluginProps.id, { ...codemirrorData, disable })
+              }
+              setPluginProps(DEFAULT_PLUGIN_PROPS)
+            }}
+          />
+        )
+      }
+
+      <Modal
+        visible={conditionProps.visible}
+        title={formatMessage({ id: 'component.plugin-flow.text.condition.required' })}
+        onOk={() => {
+          FlowGraph.setData(conditionProps.id, conditionProps.data);
+          setConditionProps(DEFAULT_CONDITION_PROPS)
+        }}
+        onCancel={() => setConditionProps(DEFAULT_CONDITION_PROPS)}
+        okText={formatMessage({ id: 'component.global.confirm' })}
+        cancelText={formatMessage({ id: 'component.global.cancel' })}
+        okButtonProps={{
+          disabled: readonly
+        }}
+      >
+        <Form.Item label={formatMessage({ id: 'component.plugin-flow.text.condition' })} style={{ marginBottom: 0 }} tooltip={formatMessage({ id: 'component.plugin-flow.text.condition-rule.tooltip' })}>
+          <Input
+            value={conditionProps.data}
+            disabled={readonly}
+            placeholder={formatMessage({ id: 'component.plugin-flow.text.condition.placeholder' })}
+            onChange={e => {
+              setConditionProps({
+                ...conditionProps,
+                data: e.target.value
+              })
+            }}
+          />
+        </Form.Item>
+      </Modal>
+    </React.Fragment>
+  )
+}
+
+export default PluginFlow
diff --git a/web/src/components/PluginFlow/components/FlowGraph/FlowGraph.ts b/web/src/components/PluginFlow/components/FlowGraph/FlowGraph.ts
new file mode 100644
index 0000000..af9fc32
--- /dev/null
+++ b/web/src/components/PluginFlow/components/FlowGraph/FlowGraph.ts
@@ -0,0 +1,394 @@
+/*
+ * 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.
+ */
+import { Graph, Addon, FunctionExt } from '@antv/x6'
+import type { Model, Cell } from '@antv/x6'
+import { formatMessage } from 'umi'
+import { notification } from 'antd'
+
+import './shapes'
+import { DEFAULT_OPINIONS, DEFAULT_PLUGIN_FLOW_DATA, DEFAULT_STENCIL_OPINIONS, FlowGraphEvent, FlowGraphShape } from '../../constants'
+
+class FlowGraph {
+  public static graph: Graph
+  private static stencil: Addon.Stencil
+  private static pluginTypeList: string[] = []
+  private static plugins: PluginComponent.Meta[] = []
+
+  public static init(container: HTMLElement, plugins: PluginComponent.Meta[] = [], chart: Model.FromJSONData) {
+    this.graph = new Graph({
+      container,
+      ...DEFAULT_OPINIONS
+    })
+
+    this.plugins = plugins
+    this.pluginTypeList = Array.from(new Set(plugins.map(item => item.type)))
+
+    this.initStencil()
+    this.initShape()
+    this.initGraphShape(chart)
+    this.initEvent()
+    return this.graph
+  }
+
+  // NOTE: set cell data according to Cell ID
+  public static setData(id: string, data: any): void {
+    const cell = this.graph.getCell(id)
+    if (cell) {
+      cell.setData(data, { overwrite: true })
+    }
+  }
+
+  // NOTE: Generate groups for stencil
+  private static generateGroups(): Addon.Stencil.Group[] {
+    const otherGroupList = [{
+      name: 'basic',
+      title: formatMessage({ id: 'component.plugin-flow.text.general' }),
+      graphHeight: 104,
+    }]
+
+    const pluginGroupList = this.pluginTypeList.map(item => {
+      const count = this.plugins.filter(plugin => plugin.type === item).length
+      return {
+        name: item,
+        title: formatMessage({ id: `component.plugin.${item}` }),
+        layoutOptions: {
+          columns: 1,
+          marginX: 60,
+        },
+        graphHeight: count * 82,
+      }
+    })
+
+    return otherGroupList.concat(pluginGroupList)
+  }
+
+  private static initStencil() {
+    this.stencil = new Addon.Stencil({
+      target: this.graph,
+      ...DEFAULT_STENCIL_OPINIONS,
+      groups: this.generateGroups()
+    })
+    const stencilContainer = document.querySelector('#stencil')
+    stencilContainer?.appendChild(this.stencil.container)
+  }
+
+  private static initShape() {
+    const { graph } = this
+    const r1 = graph.createNode({
+      shape: FlowGraphShape.start,
+      attrs: {
+        body: {
+          rx: 24,
+          ry: 24,
+        },
+        text: {
+          textWrap: {
+            text: formatMessage({ id: 'component.plugin-flow.text.start-node' }),
+          },
+        },
+      },
+    })
+
+    const r3 = graph.createNode({
+      shape: FlowGraphShape.condition,
+      width: 58,
+      height: 58,
+      angle: 45,
+      attrs: {
+        text: {
+          textWrap: {
+            text: formatMessage({ id: 'component.plugin-flow.text.condition2' }),
+          },
+          transform: 'rotate(-45deg)',
+        },
+      },
+      ports: {
+        groups: {
+          top: {
+            position: {
+              name: 'top',
+              args: {
+                dx: -26,
+              },
+            },
+          },
+          right: {
+            position: {
+              name: 'right',
+              args: {
+                dy: -26,
+              },
+            },
+          },
+          bottom: {
+            position: {
+              name: 'bottom',
+              args: {
+                dx: 26,
+              },
+            },
+          },
+          left: {
+            position: {
+              name: 'left',
+              args: {
+                dy: 26,
+              },
+            },
+          },
+        },
+      },
+    })
+
+    this.stencil.load([r1, r3], 'basic')
+    this.pluginTypeList.forEach(type => {
+      const plugins = this.plugins.filter(plugin => plugin.type === type).map(plugin => {
+        return graph.createNode({
+          shape: FlowGraphShape.plugin,
+          attrs: {
+            title: {
+              text: plugin.name
+            },
+            text: {
+              text: plugin.name
+            }
+          }
+        })
+      })
+
+      this.stencil.load(plugins, type)
+    })
+  }
+
+  private static initGraphShape(chart: Model.FromJSONData) {
+    if (!chart) {
+      return
+    }
+    this.graph.fromJSON(chart)
+  }
+
+  private static showPorts(ports: NodeListOf<SVGAElement>, show: boolean) {
+    // eslint-disable-next-line
+    for (let i = 0, len = ports.length; i < len; i = i + 1) {
+      // eslint-disable-next-line
+      ports[i].style.visibility = show ? 'visible' : 'hidden'
+    }
+  }
+
+  private static initEvent() {
+    const { graph } = this
+    const container = document.getElementById('container')!
+
+    graph.on(
+      'node:mouseenter',
+      FunctionExt.debounce(() => {
+        const ports = container.querySelectorAll(
+          '.x6-port-body',
+        ) as NodeListOf<SVGAElement>
+        this.showPorts(ports, true)
+      }),
+      500,
+    )
+
+    graph.on('node:mouseleave', () => {
+      const ports = container.querySelectorAll(
+        '.x6-port-body',
+      ) as NodeListOf<SVGAElement>
+      this.showPorts(ports, false)
+    })
+
+    graph.on('node:dblclick', ({ node }) => {
+      if (node.shape === FlowGraphShape.plugin) {
+        const name = node.getAttrByPath('text/text') as string
+        if (!name) {
+          return
+        }
+
+        this.graph.trigger(FlowGraphEvent.PLUGIN_CHANGE, {
+          visible: true,
+          id: node.id,
+          name,
+          data: node.getData()
+        })
+      }
+
+      if (node.shape === FlowGraphShape.condition) {
+        this.graph.trigger(FlowGraphEvent.CONDITION_CHANGE, {
+          id: node.id,
+          data: node.getData(),
+          visible: true
+        })
+      }
+    })
+
+    graph.bindKey('backspace', () => {
+      const cells = graph.getSelectedCells()
+      if (cells.length) {
+        graph.removeCells(cells)
+      }
+    })
+  }
+
+  private static getNextCell(id = '', position = ''): Cell.Properties | undefined {
+    const { cells = [] } = this.graph.toJSON()
+    const cell = cells.find(item => item.id === id)
+    if (!cell) {
+      return undefined
+    }
+
+    if (!cell.ports) {
+      return undefined
+    }
+
+    const port = cell.ports.items.find((item: { group: string }) => item.group === position)
+    if (!port) {
+      return undefined
+    }
+
+    const targetCellId = cells.find(item => item.source?.port === port.id && item.source?.cell === id)?.target.cell
+    const targetCell = cells.find(item => item.id === targetCellId)
+    return targetCell
+  }
+
+  private static getLeafList(currentId = '') {
+    let ids: string[] = []
+
+    const fn = (id: string) => {
+      const cell = this.getNextCell(id, "right")
+      if (!cell || !cell.id) {
+        return
+      }
+      ids = ids.concat(cell.id);
+      fn(cell.id)
+    }
+
+    fn(currentId)
+    return [currentId].concat(ids)
+  }
+
+  /**
+   * Convert Graph JSON Data to API Request Body Data
+  */
+  public static convertToData(chart: typeof DEFAULT_PLUGIN_FLOW_DATA.chart | undefined = undefined): {
+    chart: {
+      cells: Cell.Properties[];
+    };
+    conf: Record<string, any>;
+    rule: Record<string, any>;
+  } | undefined {
+    const data = {
+      ...DEFAULT_PLUGIN_FLOW_DATA,
+      chart: chart || this.graph.toJSON()
+    }
+
+    const { cells = [] } = data.chart
+
+    const edgeCells = cells.filter(cell => cell.shape === 'edge')
+
+    const startCell = cells.find(cell => cell.shape === FlowGraphShape.start)
+    if (!startCell) {
+      notification.warn({
+        message: formatMessage({ id: 'component.plugin-flow.text.no-start-node' })
+      })
+      return
+    }
+
+    const rootCell = cells.find(cell => cell.shape === 'edge' && cell.source.cell === startCell.id)
+    if (!rootCell) {
+      notification.warn({
+        message: formatMessage({ id: 'component.plugin-flow.text.no-root-node' })
+      })
+      return
+    }
+    data.rule.root = rootCell.target.cell
+
+    // Get the ID associated with each node, the relationship between nodes is in edgeCells.
+    edgeCells.forEach(edge => {
+      const sourceId = edge.source.cell
+      const targetId = edge.target.cell
+
+      data.rule[sourceId] = []
+
+      this.getLeafList(targetId).forEach(id => {
+        const cell = cells.find(item => item.id === id)
+        if (!cell) {
+          return
+        }
+
+        if (cell.shape === FlowGraphShape.condition) {
+          const nextCell = this.getNextCell(cell.id, "bottom");
+          if (!nextCell) {
+            return
+          }
+          data.rule[sourceId].push([cell.data, nextCell.id])
+        }
+
+        if (cell.shape === FlowGraphShape.plugin) {
+          data.rule[sourceId].push(['', cell.id])
+        }
+      })
+    })
+
+    // NOTE: Omit empty array, or API will throw error.
+    Object.entries(data.rule).forEach(([key, value]) => {
+      if (value.length === 0) {
+        delete data.rule[key]
+      }
+
+      if (key === 'root') {
+        return
+      }
+      const cell = cells.find(item => item.id === key)
+      if (cell?.shape !== FlowGraphShape.plugin) {
+        delete data.rule[key]
+      }
+    })
+
+    const invalidPluginCell = cells.find(item => item.shape === FlowGraphShape.plugin && !item.data)
+    if (invalidPluginCell) {
+      notification.warn({
+        message: formatMessage({ id: 'component.plugin-flow.text.without-data' }),
+        description: `${formatMessage({ id: 'component.plugin-flow.text.plugin-without-data.description' })}${invalidPluginCell.attrs?.text.text}`
+      })
+      return
+    }
+
+    const invalidConditionCell = cells.find(item => item.shape === FlowGraphShape.condition && !item.data)
+    if (invalidConditionCell) {
+      notification.warn({
+        message: formatMessage({ id: 'component.plugin-flow.text.without-data' }),
+        description: `${formatMessage({ id: 'component.plugin-flow.text.condition-without-configuration' })}`
+      })
+      return
+    }
+
+    data.conf = {}
+    cells.filter(item => item.shape === FlowGraphShape.plugin && item.id).forEach(item => {
+      if (item.id) {
+        data.conf[item.id] = {
+          name: item.attrs?.text.text,
+          conf: item.data
+        }
+      }
+    })
+
+    // eslint-disable-next-line
+    return data
+  }
+}
+
+export default FlowGraph
diff --git a/web/src/components/PluginOrchestration/components/index.ts b/web/src/components/PluginFlow/components/FlowGraph/index.ts
similarity index 93%
rename from web/src/components/PluginOrchestration/components/index.ts
rename to web/src/components/PluginFlow/components/FlowGraph/index.ts
index cb920f2..5dc41d5 100644
--- a/web/src/components/PluginOrchestration/components/index.ts
+++ b/web/src/components/PluginFlow/components/FlowGraph/index.ts
@@ -14,5 +14,4 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-export * from './Page';
-export * from './SidebarItem';
+export { default } from './FlowGraph'
diff --git a/web/src/components/PluginFlow/components/FlowGraph/shapes.ts b/web/src/components/PluginFlow/components/FlowGraph/shapes.ts
new file mode 100644
index 0000000..2e9756e
--- /dev/null
+++ b/web/src/components/PluginFlow/components/FlowGraph/shapes.ts
@@ -0,0 +1,161 @@
+/*
+ * 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.
+ */
+import { Graph } from '@antv/x6'
+
+import defaultPluginImg from '../../../../../public/static/default-plugin.png';
+import { DEFAULT_SHAPE_RECT_OPINIONS, FlowGraphShape } from '../../constants';
+
+export const FlowChartRect = Graph.registerNode(FlowGraphShape.base, DEFAULT_SHAPE_RECT_OPINIONS)
+
+export const FlowChartConditionRect = Graph.registerNode(FlowGraphShape.condition, {
+  ...DEFAULT_SHAPE_RECT_OPINIONS,
+  ports: {
+    ...DEFAULT_SHAPE_RECT_OPINIONS.ports,
+    items: [
+      {
+        group: 'top',
+      },
+      {
+        group: 'right',
+      },
+      {
+        group: 'bottom',
+      },
+    ],
+  }
+})
+
+export const FlowChartStartRect = Graph.registerNode(FlowGraphShape.start, {
+  ...DEFAULT_SHAPE_RECT_OPINIONS,
+  ports: {
+    ...DEFAULT_SHAPE_RECT_OPINIONS.ports,
+    items: [
+      {
+        group: 'bottom',
+      },
+    ],
+  }
+})
+
+export const FlowChartEndRect = Graph.registerNode(FlowGraphShape.end, {
+  ...DEFAULT_SHAPE_RECT_OPINIONS,
+  ports: {
+    ...DEFAULT_SHAPE_RECT_OPINIONS.ports,
+    items: [
+      {
+        group: 'top',
+      },
+    ],
+  }
+})
+
+export const FlowChartPluginRect = Graph.registerNode(FlowGraphShape.plugin, {
+  inherit: 'rect',
+  width: 200,
+  height: 60,
+  attrs: {
+    body: {
+      stroke: '#5F95FF',
+      strokeWidth: 1,
+      fill: 'rgba(95,149,255,0.05)',
+    },
+    image: {
+      'xlink:href':
+        defaultPluginImg,
+      width: 16,
+      height: 16,
+      x: 12,
+      y: 12,
+    },
+    title: {
+      text: 'Unknown Plugin',
+      refX: 40,
+      refY: 14,
+      fill: 'rgba(0,0,0,0.85)',
+      fontSize: 12,
+      'text-anchor': 'start',
+    },
+    text: {
+      text: '',
+      refX: 40,
+      refY: 38,
+      fontSize: 12,
+      fill: 'rgba(0,0,0,0.6)',
+      'text-anchor': 'start',
+    },
+  },
+  markup: [
+    {
+      tagName: 'rect',
+      selector: 'body',
+    },
+    {
+      tagName: 'image',
+      selector: 'image',
+    },
+    {
+      tagName: 'text',
+      selector: 'title',
+    },
+    {
+      tagName: 'text',
+      selector: 'text',
+    },
+  ],
+  ports: {
+    groups: {
+      top: {
+        position: 'top',
+        attrs: {
+          circle: {
+            r: 3,
+            magnet: true,
+            stroke: '#5F95FF',
+            strokeWidth: 1,
+            fill: '#fff',
+            style: {
+              visibility: 'hidden',
+            },
+          },
+        },
+      },
+      bottom: {
+        position: 'bottom',
+        attrs: {
+          circle: {
+            r: 3,
+            magnet: true,
+            stroke: '#5F95FF',
+            strokeWidth: 1,
+            fill: '#fff',
+            style: {
+              visibility: 'hidden',
+            },
+          },
+        },
+      },
+    },
+    items: [
+      {
+        group: 'top',
+      },
+      {
+        group: 'bottom',
+      }
+    ],
+  },
+})
diff --git a/web/src/components/PluginFlow/components/Toolbar/index.tsx b/web/src/components/PluginFlow/components/Toolbar/index.tsx
new file mode 100644
index 0000000..7931a26
--- /dev/null
+++ b/web/src/components/PluginFlow/components/Toolbar/index.tsx
@@ -0,0 +1,195 @@
+/*
+* MIT License
+
+* Copyright (c) 2019 Alipay.inc
+
+* Permission is hereby granted, free of charge, to any person obtaining a copy
+* of this software and associated documentation files (the "Software"), to deal
+* in the Software without restriction, including without limitation the rights
+* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+* copies of the Software, and to permit persons to whom the Software is
+* furnished to do so, subject to the following conditions:
+
+* The above copyright notice and this permission notice shall be included in all
+* copies or substantial portions of the Software.
+
+* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+* SOFTWARE.
+*/
+import React, { useEffect, useState } from 'react'
+import { Toolbar } from '@antv/x6-react-components'
+import FlowGraph from '../FlowGraph'
+import { DataUri } from '@antv/x6'
+import {
+  ClearOutlined,
+  SaveOutlined,
+  PrinterOutlined,
+  UndoOutlined,
+  RedoOutlined,
+  CopyOutlined,
+  ScissorOutlined,
+  SnippetsOutlined,
+} from '@ant-design/icons'
+import '@antv/x6-react-components/es/toolbar/style/index.css'
+
+const { Item, Group } = Toolbar
+
+const ToolbarComponent = () => {
+  const [canUndo, setCanUndo] = useState(false)
+  const [canRedo, setCanRedo] = useState(false)
+
+  const copy = () => {
+    const { graph } = FlowGraph
+    const cells = graph.getSelectedCells()
+    if (cells.length) {
+      graph.copy(cells)
+    }
+    return false
+  }
+
+  const cut = () => {
+    const { graph } = FlowGraph
+    const cells = graph.getSelectedCells()
+    if (cells.length) {
+      graph.cut(cells)
+    }
+    return false
+  }
+
+  const paste = () => {
+    const { graph } = FlowGraph
+    if (!graph.isClipboardEmpty()) {
+      const cells = graph.paste({ offset: 32 })
+      graph.cleanSelection()
+      graph.select(cells)
+    }
+    return false
+  }
+
+  useEffect(() => {
+    const { graph } = FlowGraph
+    const { history } = graph
+    setCanUndo(history.canUndo())
+    setCanRedo(history.canRedo())
+    history.on('change', () => {
+      setCanUndo(history.canUndo())
+      setCanRedo(history.canRedo())
+    })
+
+    graph.bindKey(['meta+z', 'ctrl+z'], () => {
+      if (history.canUndo()) {
+        history.undo()
+      }
+      return false
+    })
+    graph.bindKey(['meta+shift+z', 'ctrl+y'], () => {
+      if (history.canRedo()) {
+        history.redo()
+      }
+      return false
+    })
+    graph.bindKey(['meta+d', 'ctrl+d'], () => {
+      graph.clearCells()
+      return false
+    })
+    graph.bindKey(['meta+s', 'ctrl+s'], () => {
+      graph.toPNG((datauri: string) => {
+        DataUri.downloadDataUri(datauri, 'chart.png')
+      })
+      return false
+    })
+    graph.bindKey(['meta+p', 'ctrl+p'], () => {
+      graph.printPreview()
+      return false
+    })
+    graph.bindKey(['meta+c', 'ctrl+c'], copy)
+    graph.bindKey(['meta+v', 'ctrl+v'], paste)
+    graph.bindKey(['meta+x', 'ctrl+x'], cut)
+  }, [])
+
+  const handleClick = (name: string) => {
+    const { graph } = FlowGraph
+    switch (name) {
+      case 'undo':
+        graph.history.undo()
+        break
+      case 'redo':
+        graph.history.redo()
+        break
+      case 'delete':
+        graph.clearCells()
+        break
+      case 'save':
+        graph.toPNG((datauri: string) => {
+          DataUri.downloadDataUri(datauri, 'chart.png')
+        })
+        break
+      case 'print':
+        graph.printPreview()
+        break
+      case 'copy':
+        copy()
+        break
+      case 'cut':
+        cut()
+        break
+      case 'paste':
+        paste()
+        break
+      default:
+        break
+    }
+  }
+
+  return (
+    <div>
+      <Toolbar hoverEffect={true} size="small" onClick={handleClick}>
+        <Group>
+          <Item
+            name="delete"
+            icon={<ClearOutlined />}
+            tooltip="Clear (Cmd + D, Ctrl + D)"
+          />
+        </Group>
+        <Group>
+          <Item
+            name="undo"
+            tooltip="Undo (Cmd + Z, Ctrl + Z)"
+            icon={<UndoOutlined />}
+            disabled={!canUndo}
+          />
+          <Item
+            name="redo"
+            tooltip="Redo (Cmd + Shift + Z, Ctrl + Y)"
+            icon={<RedoOutlined />}
+            disabled={!canRedo}
+          />
+        </Group>
+        <Group>
+          <Item name="copy" tooltip="Copy (Cmd + C, Ctrl + C)" icon={<CopyOutlined />} />
+          <Item name="cut" tooltip="Cut (Cmd + X, Ctrl + X)" icon={<ScissorOutlined />} />
+          <Item
+            name="paste"
+            tooltip="Paste (Cmd + V, Ctrl + V)"
+            icon={<SnippetsOutlined />}
+          />
+        </Group>
+        <Group>
+          <Item name="save" icon={<SaveOutlined />} tooltip="Save (Cmd + S, Ctrl + S)" />
+          <Item
+            name="print"
+            icon={<PrinterOutlined />}
+            tooltip="Print (Cmd + P, Ctrl + P)"
+          />
+        </Group>
+      </Toolbar>
+    </div>
+  )
+}
+
+export default ToolbarComponent
diff --git a/web/src/components/PluginFlow/constants.ts b/web/src/components/PluginFlow/constants.ts
new file mode 100644
index 0000000..b81e60c
--- /dev/null
+++ b/web/src/components/PluginFlow/constants.ts
@@ -0,0 +1,294 @@
+/*
+ * 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.
+ */
+import { Shape, Dom } from '@antv/x6'
+import type { Addon, Graph, Cell } from '@antv/x6'
+import { formatMessage } from '@/.umi/plugin-locale/localeExports'
+
+export const DEFAULT_STENCIL_WIDTH = 280
+export const DEFAULT_TOOLBAR_HEIGHT = 38
+export const DEFAULT_SHAPE_TEXT_EDIT_CLASS_NAME = ".flow-graph-shape-text-editor"
+
+export const DEFAULT_OPINIONS: Partial<Graph.Options> = {
+  scroller: true,
+  width: 800,
+  height: 600,
+  grid: {
+    size: 10,
+    visible: true,
+  },
+  selecting: {
+    enabled: true,
+    multiple: true,
+    rubberband: true,
+    movable: true,
+    showNodeSelectionBox: true,
+    filter: ['groupNode'],
+  },
+  connecting: {
+    allowBlank: false,
+    highlight: true,
+    snap: true,
+    createEdge() {
+      return new Shape.Edge({
+        attrs: {
+          line: {
+            stroke: '#5F95FF',
+            strokeWidth: 1,
+            targetMarker: {
+              name: 'classic',
+              size: 8,
+            },
+          },
+        },
+        router: {
+          name: 'manhattan',
+        },
+        zIndex: 0,
+      })
+    },
+    validateConnection({
+      sourceView,
+      targetView,
+      sourceMagnet,
+      targetMagnet,
+    }) {
+      if (sourceView === targetView) {
+        return false
+      }
+      if (!sourceMagnet) {
+        return false
+      }
+      if (!targetMagnet) {
+        return false
+      }
+      return true
+    },
+  },
+  highlighting: {
+    magnetAvailable: {
+      name: 'stroke',
+      args: {
+        padding: 4,
+        attrs: {
+          strokeWidth: 4,
+          stroke: 'rgba(223,234,255)',
+        },
+      },
+    },
+  },
+  snapline: true,
+  history: true,
+  clipboard: {
+    enabled: true,
+  },
+  keyboard: {
+    enabled: true,
+  },
+  embedding: {
+    enabled: true,
+    findParent({ node }) {
+      const bbox = node.getBBox()
+      return this.getNodes().filter((item) => {
+        const data = item.getData<any>()
+        if (data && data.parent) {
+          const targetBBox = item.getBBox()
+          return bbox.isIntersectWithRect(targetBBox)
+        }
+        return false
+      })
+    },
+  },
+}
+
+export const DEFAULT_STENCIL_OPINIONS: Partial<Addon.Stencil.Options> = {
+  title: formatMessage({ id: 'component.plugin-flow.text.nodes-area' }),
+  stencilGraphWidth: DEFAULT_STENCIL_WIDTH,
+  search: (cell, keyword) => {
+    if (keyword) {
+      return (cell as any).label?.indexOf(keyword) !== -1
+    }
+    return true
+  },
+  notFoundText: formatMessage({ id: 'component.plugin-flow.text.nodes.not-found' }),
+  placeholder: formatMessage({ id: 'component.plugin-flow.text.search-nodes.placeholder' }),
+  collapsable: true,
+}
+
+export const DEFAULT_SHAPE_RECT_OPINIONS = {
+  inherit: 'rect',
+  width: 80,
+  height: 42,
+  attrs: {
+    body: {
+      stroke: '#5F95FF',
+      strokeWidth: 1,
+      fill: 'rgba(95,149,255,0.05)',
+    },
+    fo: {
+      refWidth: '100%',
+      refHeight: '100%',
+    },
+    foBody: {
+      xmlns: Dom.ns.xhtml,
+      style: {
+        width: '100%',
+        height: '100%',
+        display: 'flex',
+        justifyContent: 'center',
+        alignItems: 'center',
+      },
+    },
+    text: {
+      fontSize: 12,
+      fill: 'rgba(0,0,0,0.85)',
+      textWrap: {
+        text: '',
+        width: -10,
+      },
+    },
+  },
+  markup: [
+    {
+      tagName: 'rect',
+      selector: 'body',
+    },
+    {
+      tagName: 'text',
+      selector: 'text',
+    }
+  ],
+  ports: {
+    groups: {
+      top: {
+        position: 'top',
+        attrs: {
+          circle: {
+            r: 3,
+            magnet: true,
+            stroke: '#5F95FF',
+            strokeWidth: 1,
+            fill: '#fff',
+            style: {
+              visibility: 'hidden',
+            },
+          },
+        },
+      },
+      right: {
+        position: 'right',
+        attrs: {
+          circle: {
+            r: 3,
+            magnet: true,
+            stroke: '#5F95FF',
+            strokeWidth: 1,
+            fill: '#fff',
+            style: {
+              visibility: 'hidden',
+            },
+          },
+        },
+      },
+      bottom: {
+        position: 'bottom',
+        attrs: {
+          circle: {
+            r: 3,
+            magnet: true,
+            stroke: '#5F95FF',
+            strokeWidth: 1,
+            fill: '#fff',
+            style: {
+              visibility: 'hidden',
+            },
+          },
+        },
+      },
+      left: {
+        position: 'left',
+        attrs: {
+          circle: {
+            r: 3,
+            magnet: true,
+            stroke: '#5F95FF',
+            strokeWidth: 1,
+            fill: '#fff',
+            style: {
+              visibility: 'hidden',
+            },
+          },
+        },
+      },
+    },
+    items: [
+      {
+        group: 'top',
+      },
+      {
+        group: 'right',
+      },
+      {
+        group: 'bottom',
+      },
+      {
+        group: 'left',
+      },
+    ],
+  },
+}
+
+export enum FlowGraphShape {
+  base = 'flow-chart-rect',
+  condition = 'flow-chart-condition-rect',
+  start = 'flow-chart-start-rect',
+  end = 'flow-chart-end-rect',
+  plugin = 'flow-chart-plugin-rect'
+}
+
+export enum FlowGraphEvent {
+  PLUGIN_CHANGE = 'flowgraph:change:plugin',
+  CONDITION_CHANGE = 'flowgraph:change:condition',
+}
+
+export const DEFAULT_PLUGIN_PROPS = {
+  id: '',
+  name: '',
+  visible: false,
+  data: {}
+}
+
+export const DEFAULT_CONDITION_PROPS = {
+  visible: false,
+  id: '',
+  data: ''
+}
+
+export const DEFAULT_PLUGIN_FLOW_DATA: {
+  chart: {
+    cells: Cell.Properties[];
+  };
+  conf: Record<string, any>;
+  rule: Record<string, any>;
+} = {
+  chart: {
+    cells: []
+  },
+  conf: {},
+  rule: {
+    root: ""
+  }
+}
diff --git a/web/src/components/PluginOrchestration/components/Page.tsx b/web/src/components/PluginFlow/index.ts
similarity index 82%
rename from web/src/components/PluginOrchestration/components/Page.tsx
rename to web/src/components/PluginFlow/index.ts
index 2bdc3ff..fd368ef 100644
--- a/web/src/components/PluginOrchestration/components/Page.tsx
+++ b/web/src/components/PluginFlow/index.ts
@@ -14,7 +14,4 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import * as React from 'react';
-import { SPageContent } from '../DrawPluginStyle';
-
-export const Page: React.FC = (props) => <SPageContent>{props.children}</SPageContent>;
+export { default } from './PluginFlow'
diff --git a/web/src/components/PluginFlow/locales/en-US.ts b/web/src/components/PluginFlow/locales/en-US.ts
new file mode 100644
index 0000000..fdec09a
--- /dev/null
+++ b/web/src/components/PluginFlow/locales/en-US.ts
@@ -0,0 +1,41 @@
+/*
+ * 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 default {
+  'component.plugin-flow.text.condition.required': 'Configure Rule',
+  'component.plugin-flow.text.condition': 'Rule',
+  'component.plugin-flow.text.condition2': 'Condition',
+  'component.plugin-flow.text.condition.placeholder': 'Please enter the rule',
+  'component.plugin-flow.text.without-data': 'Found node without configuration',
+  'component.plugin-flow.text.plugin-without-data.description': 'Please condigure plugin: ',
+  'component.plugin-flow.text.no-start-node': 'Please connect the start node',
+  'component.plugin-flow.text.no-root-node': 'Root node not found',
+  'component.plugin-flow.text.start-node': 'Start',
+  'component.plugin-flow.text.general': 'General',
+  'component.plugin-flow.text.nodes-area': 'Available Nodes',
+  'component.plugin-flow.text.nodes.not-found': 'Not Found',
+  'component.plugin-flow.text.search-nodes.placeholder': 'Search plugin by name',
+  'component.plugin-flow.text.condition-rule.tooltip': 'The judgment condition of the node. e.g: code == 503',
+  'component.plugin-flow.text.line': 'Line',
+  'component.plugin-flow.text.grid': 'Grid',
+  'component.plugin-flow.text.background': 'Background',
+  'component.plugin-flow.text.node': 'Node',
+  'component.plugin-flow.text.text': 'Text',
+  'component.plugin-flow.text.condition-without-configuration': 'Please check all condition nodes\' data',
+  'component.plugin-flow.text.preview.readonly': 'NOTE: your actions on the following drawer will not be preserved.',
+  'component.plugin-flow.text.both-modes-exist': 'The orchestration mode configuration will override the normal mode configuration, are you sure to continue?',
+  'component.plugin-flow.text.both-modes-exist.title': 'Attention'
+}
diff --git a/web/src/components/PluginFlow/locales/zh-CN.ts b/web/src/components/PluginFlow/locales/zh-CN.ts
new file mode 100644
index 0000000..4486dff
--- /dev/null
+++ b/web/src/components/PluginFlow/locales/zh-CN.ts
@@ -0,0 +1,41 @@
+/*
+ * 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 default {
+  'component.plugin-flow.text.condition.required': '配置判断条件',
+  'component.plugin-flow.text.condition': '判断条件',
+  'component.plugin-flow.text.condition2': '条件判断',
+  'component.plugin-flow.text.condition.placeholder': '请输入判断条件',
+  'component.plugin-flow.text.without-data': '存在未配置的元件',
+  'component.plugin-flow.text.plugin-without-data.description': '请配置插件:',
+  'component.plugin-flow.text.no-start-node': '请关联开始节点',
+  'component.plugin-flow.text.no-root-node': '未找到根节点',
+  'component.plugin-flow.text.start-node': '开始',
+  'component.plugin-flow.text.general': '通用',
+  'component.plugin-flow.text.nodes-area': '元件选择区',
+  'component.plugin-flow.text.nodes.not-found': '无匹配元件',
+  'component.plugin-flow.text.search-nodes.placeholder': '请输入插件元件名称',
+  'component.plugin-flow.text.condition-rule.tooltip': '节点的判断条件。例如:code == 503',
+  'component.plugin-flow.text.line': '线条',
+  'component.plugin-flow.text.grid': '网格',
+  'component.plugin-flow.text.background': '背景',
+  'component.plugin-flow.text.node': '节点',
+  'component.plugin-flow.text.text': '文本',
+  'component.plugin-flow.text.condition-without-configuration': '请检查条件判断元件的配置',
+  'component.plugin-flow.text.preview.readonly': '请注意:在当前页面,您在画布上地操作不会被保留。',
+  'component.plugin-flow.text.both-modes-exist': '编排模式配置将覆盖普通模式配置,是否继续操作?',
+  'component.plugin-flow.text.both-modes-exist.title': '配置冲突'
+}
diff --git a/web/src/components/PluginOrchestration/typing.d.ts b/web/src/components/PluginFlow/style.less
similarity index 64%
rename from web/src/components/PluginOrchestration/typing.d.ts
rename to web/src/components/PluginFlow/style.less
index 3f4e30c..41ccddd 100644
--- a/web/src/components/PluginOrchestration/typing.d.ts
+++ b/web/src/components/PluginFlow/style.less
@@ -14,13 +14,29 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-export declare namespace PluginOrchestrationModule {
-  type Meta = {
-    name: string;
-    priority: number;
-    schema: Record<string, any>;
-    type: string;
-    version: number;
-    consumer_schema?: Record<string, any>;
-  };
+
+.container {
+  display: flex;
+  height: calc(100% - 48px);
+}
+
+.stencil {
+  position: relative;
+  // NOTE: constants.ts -> DEFAULT_STENCIL_WIDTH
+  width: 280px;
+  height: 400px;
+  border-right: 1px solid rgba(0, 0, 0, 0.08);
+}
+
+.panel {
+  height: 100%;
+}
+
+.toolbar {
+  display: flex;
+  align-items: center;
+  // NOTE: DEFAULT_TOOLBAR_HEIGHT
+  height: 38px;
+  background-color: #f7f9fb;
+  border-bottom: 1px solid rgba(0, 0, 0, 0.08);
 }
diff --git a/web/src/components/PluginOrchestration/DrawPluginStyle.ts b/web/src/components/PluginOrchestration/DrawPluginStyle.ts
deleted file mode 100644
index a1d40b2..0000000
--- a/web/src/components/PluginOrchestration/DrawPluginStyle.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * 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.
- */
-import styled from 'styled-components';
-
-export const SOuter = styled.div`
-  padding: 30px;
-`;
-
-export const SInput = styled.input`
-  padding: 10px;
-  border: 1px solid cornflowerblue;
-  width: 100%;
-`;
-
-export const SMessage = styled.div`
-  margin: 10px;
-  padding: 10px;
-  line-height: 1.4em;
-`;
-
-export const SButton = styled.div`
-  padding: 10px 15px;
-  background: cornflowerblue;
-  color: white;
-  border-radius: 3px;
-  text-align: center;
-  transition: 0.3s ease all;
-  cursor: pointer;
-  &:hover {
-    box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
-  }
-  &:active {
-    background: #5682d2;
-  }
-`;
-
-export const SPortDefaultOuter = styled.div`
-  width: 24px;
-  height: 24px;
-  background: cornflowerblue;
-  cursor: pointer;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-`;
-
-export const SContent = styled.div`
-  display: flex;
-  flex-direction: column;
-  flex: 1;
-  overflow: hidden;
-`;
-
-export const SSidebar = styled.div`
-  width: 300px;
-  background: white;
-  display: flex;
-  flex-direction: column;
-  flex-shrink: 0;
-`;
-
-export const SPageContent = styled.div`
-  display: flex;
-  flex-direction: row;
-  flex: 1;
-  max-width: 100vw;
-  max-height: 100vh;
-`;
diff --git a/web/src/components/PluginOrchestration/components/SidebarItem.tsx b/web/src/components/PluginOrchestration/components/SidebarItem.tsx
deleted file mode 100644
index 5a4b380..0000000
--- a/web/src/components/PluginOrchestration/components/SidebarItem.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * 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.
- */
-import * as React from 'react';
-import { REACT_FLOW_CHART } from '@mrblenny/react-flow-chart';
-import type { INode } from '@mrblenny/react-flow-chart';
-import { Button } from 'antd';
-
-import { SOuter } from '../DrawPluginStyle';
-
-export type ISidebarItemProps = {
-  type: string;
-  ports: INode['ports'];
-  properties?: any;
-};
-
-export const SidebarItem: React.FC<ISidebarItemProps> = ({ type, ports, properties }) => {
-  return (
-    <SOuter
-      draggable
-      onDragStart={(event: any) => {
-        event.dataTransfer.setData(REACT_FLOW_CHART, JSON.stringify({ type, ports, properties }));
-      }}
-      style={{ padding: '5px' }}
-    >
-      <Button type="dashed">{type}</Button>
-    </SOuter>
-  );
-};
diff --git a/web/src/components/PluginOrchestration/constants.ts b/web/src/components/PluginOrchestration/constants.ts
deleted file mode 100644
index 2a8c97c..0000000
--- a/web/src/components/PluginOrchestration/constants.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * 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 INIT_CHART = {
-  offset: { x: 0, y: 0 },
-  scale: 0.577,
-  nodes: {},
-  links: {},
-  selected: {},
-  hovered: {},
-};
-
-export const PLUGINS_PORTS = {
-  port1: {
-    id: 'port1',
-    type: 'input',
-    properties: {
-      custom: 'property',
-    },
-  },
-  port2: {
-    id: 'port2',
-    type: 'output',
-    properties: {
-      custom: 'property',
-    },
-  },
-};
-
-export const CONDITION_PORTS = {
-  port1: {
-    id: 'port1',
-    type: 'input',
-  },
-  port2: {
-    id: 'port2',
-    type: 'output',
-    properties: {
-      value: 'no',
-    },
-  },
-  port3: {
-    id: 'port3',
-    type: 'output',
-    properties: {
-      value: 'yes',
-    },
-  },
-};
diff --git a/web/src/components/PluginOrchestration/customConfig.tsx b/web/src/components/PluginOrchestration/customConfig.tsx
deleted file mode 100644
index b438aa5..0000000
--- a/web/src/components/PluginOrchestration/customConfig.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * 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.
- */
-import React from 'react';
-import { useIntl } from 'umi';
-import type { INodeInnerDefaultProps, IPortDefaultProps } from '@mrblenny/react-flow-chart';
-
-import { SOuter, SPortDefaultOuter } from './DrawPluginStyle';
-import { PanelType } from './index';
-
-export const NodeInnerCustom = ({ node }: INodeInnerDefaultProps) => {
-  const { formatMessage } = useIntl();
-  const { customData } = node.properties;
-  if (customData.type === PanelType.Condition) {
-    return (
-      <SOuter>
-        <p>
-          {formatMessage({ id: 'page.panel.condition.name' })}:
-          {customData.name || `(${formatMessage({ id: 'page.panel.condition.tips' })})`}
-        </p>
-      </SOuter>
-    );
-  }
-
-  if (customData.type === PanelType.Plugin) {
-    return (
-      <SOuter>
-        <p>
-          {formatMessage({ id: 'page.panel.plugin.name' })}:{' '}
-          {customData.name || `(${formatMessage({ id: 'page.panel.plugin.tips' })})`}
-        </p>
-      </SOuter>
-    );
-  }
-
-  return (
-    <SOuter>
-      <br />
-    </SOuter>
-  );
-};
-
-export const PortCustom = (props: IPortDefaultProps) => (
-  <SPortDefaultOuter>
-    {props.port.properties && props.port.properties.value === 'yes' && (
-      <svg style={{ width: '24px', height: '24px' }} viewBox="0 0 24 24">
-        <path fill="white" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z" />
-      </svg>
-    )}
-    {props.port.properties && props.port.properties.value === 'no' && (
-      <svg style={{ width: '24px', height: '24px' }} viewBox="0 0 24 24">
-        <path
-          fill="white"
-          d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z"
-        />
-      </svg>
-    )}
-    {!props.port.properties && (
-      <svg style={{ width: '24px', height: '24px' }} viewBox="0 0 24 24">
-        <path fill="white" d="M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z" />
-      </svg>
-    )}
-  </SPortDefaultOuter>
-);
diff --git a/web/src/components/PluginOrchestration/index.tsx b/web/src/components/PluginOrchestration/index.tsx
deleted file mode 100644
index 4cff7ba..0000000
--- a/web/src/components/PluginOrchestration/index.tsx
+++ /dev/null
@@ -1,289 +0,0 @@
-/*
- * 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.
- */
-import React, { Fragment, useState, useEffect } from 'react';
-import { cloneDeep } from 'lodash';
-import { FlowChart } from '@mrblenny/react-flow-chart';
-import type { IFlowChartCallbacks } from '@mrblenny/react-flow-chart';
-import * as actions from '@mrblenny/react-flow-chart/src/container/actions';
-import { Form, Input, Button, Divider, Card, Select } from 'antd';
-import { withTheme } from '@rjsf/core';
-import { useIntl } from 'umi';
-
-// @ts-ignore
-import { Theme as AntDTheme } from '@rjsf/antd';
-
-import { Page, SidebarItem } from './components';
-import { INIT_CHART, PLUGINS_PORTS, CONDITION_PORTS } from './constants';
-import { SMessage, SContent, SSidebar } from './DrawPluginStyle';
-import { PortCustom, NodeInnerCustom } from './customConfig';
-import { fetchList } from './service';
-import type { PluginOrchestrationModule } from './typing';
-
-export * from './transform';
-
-export enum PanelType {
-  Plugin,
-  Condition,
-  Default,
-}
-
-type Props = {
-  data: any;
-  onChange: (data: Record<string, unknown>) => void;
-  readonly: boolean;
-};
-
-const PluginForm = withTheme(AntDTheme);
-
-const LAYOUT = {
-  labelCol: { span: 8 },
-  wrapperCol: { span: 16 },
-};
-const TAIL_LAYOUT = {
-  wrapperCol: { offset: 8, span: 16 },
-};
-
-const SelectedSidebar: React.FC<Props> = ({ data = {}, onChange, readonly = false }) => {
-  const [form] = Form.useForm();
-  const [chart, setChart] = useState(cloneDeep(Object.keys(data).length ? data : INIT_CHART));
-  const [schema, setSchema] = useState<any>();
-  const [selectedType, setSelectedType] = useState<PanelType>(PanelType.Default);
-  const [pluginList, setPluginList] = useState<PluginOrchestrationModule.Meta[]>([]);
-  const [pluginCategory, setPluginCategory] = useState('All');
-  const [showList, setShowList] = useState<string[]>();
-  const [typeList, setTypeList] = useState<string[]>([]);
-
-  const { formatMessage } = useIntl();
-
-  const getCustomDataById = (id = chart.selected.id) => {
-    if (!id || !chart.nodes[id].properties) {
-      return {};
-    }
-    return chart.nodes[id].properties.customData;
-  };
-
-  const stateActionCallbacks = Object.keys(actions).reduce((obj, key) => {
-    const clonedObj = cloneDeep(obj);
-    clonedObj[key] = (...args: any) => {
-      const action = actions[key];
-      const newChartTransformer = action(...args);
-      const newChart = newChartTransformer(chart);
-      if (
-        ['onLinkMouseEnter', 'onLinkMouseLeave', 'onNodeMouseEnter', 'onNodeMouseLeave'].includes(
-          key,
-        )
-      ) {
-        return newChart;
-      }
-
-      if (key === 'onDragCanvasStop') {
-        setSelectedType(PanelType.Default);
-        return newChart;
-      }
-
-      setChart({ ...chart, ...newChart });
-      if (['onCanvasDrop', 'onNodeClick'].includes(key)) {
-        const { type, name } = getCustomDataById(args.nodeId);
-        setSelectedType(type);
-        if (type === PanelType.Plugin && name) {
-          const plugin = pluginList.find((item) => item.name === name);
-          if (plugin) {
-            setSchema(plugin.schema);
-          }
-        }
-      }
-      onChange(newChart);
-      return newChart;
-    };
-    return clonedObj;
-  }, {}) as IFlowChartCallbacks;
-
-  const firstUpperCase = ([first, ...rest]: string) => first.toUpperCase() + rest.join('');
-  useEffect(() => {
-    fetchList().then((list) => {
-      const categoryList: string[] = [];
-      list.forEach((item) => {
-        if (!categoryList.includes(firstUpperCase(item.type))) {
-          categoryList.push(firstUpperCase(item.type));
-        }
-      });
-      setTypeList(['All', ...categoryList.sort()]);
-      setPluginList(list);
-      setShowList(list.map((item) => item.name).sort());
-    });
-  }, []);
-
-  const renderSidebar = () => {
-    if (selectedType === PanelType.Condition) {
-      form.setFieldsValue({ condition: getCustomDataById().name });
-      return (
-        <SMessage>
-          <Form
-            {...LAYOUT}
-            name="basic"
-            form={form}
-            onFinish={(values) => {
-              const clonedChart = cloneDeep(chart);
-              clonedChart.nodes[chart.selected.id!].properties.customData.name = values.condition;
-              setChart(clonedChart);
-              onChange(clonedChart);
-              setSelectedType(PanelType.Default);
-            }}
-          >
-            <Form.Item
-              label={formatMessage({ id: 'page.siderBar.form.label.panelType.condition' })}
-              name="condition"
-              rules={[
-                {
-                  required: true,
-                  message: formatMessage({ id: 'page.siderBar.form.rule.panelType.condition' }),
-                },
-              ]}
-            >
-              <Input />
-            </Form.Item>
-            <Form.Item {...TAIL_LAYOUT}>
-              <Button type="primary" htmlType="submit">
-                {formatMessage({ id: 'page.siderBar.button.submit' })}
-              </Button>
-            </Form.Item>
-          </Form>
-        </SMessage>
-      );
-    }
-    if (selectedType === PanelType.Plugin && schema) {
-      return (
-        <SMessage style={{ overflow: 'scroll' }}>
-          <PluginForm
-            schema={schema}
-            liveValidate
-            formData={getCustomDataById().data || {}}
-            showErrorList={false}
-            onSubmit={({ formData }) => {
-              const clonedChart = cloneDeep(chart);
-              clonedChart.nodes[chart.selected.id!].properties.customData.data = formData;
-              setChart(clonedChart);
-              onChange(clonedChart);
-              setSelectedType(PanelType.Default);
-            }}
-          >
-            {/* NOTE: Leave blank to hide the Submit button */}
-            <Fragment />
-
-            <Button type="primary" htmlType="submit">
-              {formatMessage({ id: 'page.siderBar.button.submit' })}
-            </Button>
-          </PluginForm>
-        </SMessage>
-      );
-    }
-
-    return (
-      <SSidebar>
-        <SMessage style={{ fontSize: '16px', fontWeight: 'bold' }}>
-          {formatMessage({ id: 'page.siderBar.tips' })}
-        </SMessage>
-        <Divider style={{ margin: '0px' }} />
-        <SidebarItem
-          type={formatMessage({ id: 'page.siderBar.form.label.panelType.condition' })}
-          ports={CONDITION_PORTS}
-          properties={{
-            customData: {
-              type: PanelType.Condition,
-            },
-          }}
-        />
-        <Divider orientation="left">{formatMessage({ id: 'page.siderBar.plugin' })}</Divider>
-        <Select
-          showSearch
-          placeholder={formatMessage({ id: 'page.siderBar.form.label.panelType.plugin' })}
-          optionFilterProp="children"
-          defaultValue={pluginCategory}
-          onChange={(value) => {
-            setPluginCategory(value);
-            if (value === 'All') {
-              setShowList(pluginList.map((item) => item.name).sort());
-              return;
-            }
-            setShowList(
-              pluginList
-                .filter((item) => item.type === value.toLowerCase())
-                .map((item) => item.name)
-                .sort(),
-            );
-          }}
-          filterOption={(input, option) =>
-            option?.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
-          }
-        >
-          {typeList.map((item) => (
-            <Select.Option value={item}>{item}</Select.Option>
-          ))}
-        </Select>
-        <Card size="small" title={pluginCategory} style={{ height: 'unset' }}>
-          <div
-            style={{
-              overflowY: 'scroll',
-              height: '500px',
-              display: 'flex',
-              flexWrap: 'wrap',
-              alignContent: 'flex-start',
-            }}
-          >
-            {showList &&
-              showList.map((name) => {
-                return (
-                  <SidebarItem
-                    key={name}
-                    type={name}
-                    ports={PLUGINS_PORTS}
-                    properties={{
-                      customData: {
-                        type: PanelType.Plugin,
-                        name,
-                      },
-                    }}
-                  />
-                );
-              })}
-          </div>
-        </Card>
-      </SSidebar>
-    );
-  };
-  return (
-    <Page>
-      <SContent>
-        <FlowChart
-          chart={chart}
-          callbacks={stateActionCallbacks}
-          config={{
-            zoom: { wheel: { disabled: true } },
-            readonly,
-          }}
-          Components={{
-            Port: PortCustom,
-            NodeInner: NodeInnerCustom,
-          }}
-        />
-      </SContent>
-      {Boolean(!readonly) && <SSidebar>{renderSidebar()}</SSidebar>}
-    </Page>
-  );
-};
-
-export default SelectedSidebar;
diff --git a/web/src/components/PluginOrchestration/locales/en-US.ts b/web/src/components/PluginOrchestration/locales/en-US.ts
deleted file mode 100644
index 08ba06a..0000000
--- a/web/src/components/PluginOrchestration/locales/en-US.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * 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 default {
-  'page.siderBar.form.label.panelType.condition': 'Condition',
-  'page.siderBar.form.rule.panelType.condition': 'Please enter the condition of judgment',
-  'page.siderBar.form.label.panelType.plugin': 'Plugin Category',
-
-  'page.siderBar.button.submit': 'Save',
-  'page.siderBar.plugin': 'Plugin',
-  'page.siderBar.tips': 'Drag the required components to the panel',
-
-  'page.panel.condition.tips': 'Click here to configure',
-  'page.panel.condition.name': 'Condition',
-  'page.panel.plugin.tips': 'Click to configure the plugin',
-  'page.panel.plugin.name': 'Plugin Name',
-};
diff --git a/web/src/components/PluginOrchestration/locales/zh-CN.ts b/web/src/components/PluginOrchestration/locales/zh-CN.ts
deleted file mode 100644
index 1f1cbe3..0000000
--- a/web/src/components/PluginOrchestration/locales/zh-CN.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * 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 default {
-  'page.siderBar.form.label.panelType.condition': '判断条件',
-  'page.siderBar.form.rule.panelType.condition': '请输入判断条件',
-  'page.siderBar.form.label.panelType.plugin': '插件分类',
-
-  'page.siderBar.button.submit': '保存',
-  'page.siderBar.plugin': '插件',
-  'page.siderBar.tips': '拖动所需组件至面板',
-
-  'page.panel.condition.tips': '点击配置判断条件',
-  'page.panel.condition.name': '判断条件',
-  'page.panel.plugin.tips': '点击配置插件',
-  'page.panel.plugin.name': '插件名称',
-};
diff --git a/web/src/components/PluginOrchestration/service.ts b/web/src/components/PluginOrchestration/service.ts
deleted file mode 100644
index 7c008ff..0000000
--- a/web/src/components/PluginOrchestration/service.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * 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.
- */
-import { request } from 'umi';
-
-import type { PluginOrchestrationModule } from './typing';
-
-export const fetchList = () => {
-  return request<Res<PluginOrchestrationModule.Meta[]>>('/plugins?all=true').then((data) => {
-    return data.data;
-  });
-};
diff --git a/web/src/components/PluginOrchestration/transform.ts b/web/src/components/PluginOrchestration/transform.ts
deleted file mode 100644
index 658d009..0000000
--- a/web/src/components/PluginOrchestration/transform.ts
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- * 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.
- */
-import { PanelType } from '.';
-
-export const transformer = (chart: any) => {
-  const rule: any = {};
-  const conf: any = {};
-
-  const { links } = chart;
-
-  const findStartNode = () => {
-    const nodeIdFormArr: string[] = [];
-    const nodeIdToArr: string[] = [];
-    Object.keys(links).forEach((key) => {
-      const item = links[key];
-      nodeIdFormArr.push(item.from.nodeId);
-      nodeIdToArr.push(item.to.nodeId);
-    });
-    return nodeIdFormArr.filter((item) => !nodeIdToArr.includes(item))[0];
-  };
-
-  const findLinkId = (type: string, nodeId: string, port?: string) => {
-    let returnId;
-
-    Object.keys(links).forEach((key) => {
-      const item = links[key];
-      // condition
-      if (port) {
-        if (port === item[type].portId && item[type].nodeId === nodeId) {
-          returnId = key;
-        }
-        return;
-      }
-      // plugin
-      if (nodeId === item[type].nodeId) {
-        returnId = key;
-      }
-    });
-
-    return returnId;
-  };
-
-  const processRule = (id: string) => {
-    if (!chart.nodes[id]) return;
-
-    const link = findLinkId('from', id);
-    if (!link) return;
-
-    const nextNodeId = links[link].to.nodeId;
-
-    const nextNodeType = chart.nodes[nextNodeId].properties.customData.type;
-
-    if (nextNodeType === PanelType.Plugin) {
-      rule[id] = [['', nextNodeId]];
-      processRule(nextNodeId);
-    }
-
-    if (nextNodeType === PanelType.Condition) {
-      let truePortId;
-      let falsePortId;
-      const { ports } = chart.nodes[nextNodeId];
-      Object.keys(ports).forEach((key) => {
-        const item = ports[key];
-        if (item.properties) {
-          if (item.properties.value === 'yes') {
-            truePortId = item.id;
-          }
-          if (item.properties.value === 'no') {
-            falsePortId = item.id;
-          }
-        }
-      });
-      const trueLinkId = findLinkId('from', nextNodeId, truePortId);
-      const falseLinkId = findLinkId('from', nextNodeId, falsePortId);
-      const nextTrueNode = trueLinkId ? links[trueLinkId].to.nodeId : undefined;
-      const nextFalseNode = falseLinkId ? links[falseLinkId].to.nodeId : undefined;
-
-      rule[id] = [];
-      if (nextTrueNode) {
-        rule[id][0] = [chart.nodes[nextNodeId].properties.customData.name, nextTrueNode];
-        processRule(nextTrueNode);
-      }
-
-      if (nextFalseNode) {
-        rule[id][1] = ['', nextFalseNode];
-        processRule(nextFalseNode);
-      }
-    }
-  };
-
-  const startId = findStartNode();
-  rule.root = startId;
-
-  processRule(startId);
-
-  // handle conf
-  Object.keys(chart.nodes).forEach((key) => {
-    const item = chart.nodes[key];
-    if (item.properties.customData && item.properties.customData.type === 0) {
-      conf[key] = {
-        name: item.properties.customData.name,
-        conf: item.properties.customData.data,
-      };
-    }
-  });
-
-  return { rule, conf };
-};
diff --git a/web/src/locales/en-US.ts b/web/src/locales/en-US.ts
index e4773a6..049f03d 100644
--- a/web/src/locales/en-US.ts
+++ b/web/src/locales/en-US.ts
@@ -23,8 +23,8 @@ import pwa from './en-US/pwa';
 import settingDrawer from './en-US/settingDrawer';
 import settings from './en-US/setting';
 import other from './en-US/other'
-import PluginOrchestration from '../components/PluginOrchestration/locales/en-US';
 import Plugin from '../components/Plugin/locales/en-US';
+import PluginFlow from '../components/PluginFlow/locales/en-US';
 import RawDataEditor from '../components/RawDataEditor/locales/en-US';
 import UpstreamComponent from '../components/Upstream/locales/en-US'
 
@@ -42,8 +42,8 @@ export default {
   ...component,
   ...other,
   ...ActionBarEnUS,
-  ...PluginOrchestration,
   ...Plugin,
+  ...PluginFlow,
   ...RawDataEditor,
   ...UpstreamComponent
 };
diff --git a/web/src/locales/zh-CN.ts b/web/src/locales/zh-CN.ts
index 4d89580..1748751 100644
--- a/web/src/locales/zh-CN.ts
+++ b/web/src/locales/zh-CN.ts
@@ -23,8 +23,8 @@ import pwa from './zh-CN/pwa';
 import other from './zh-CN/other'
 import settingDrawer from './zh-CN/settingDrawer';
 import settings from './zh-CN/setting';
-import PluginOrchestration from '../components/PluginOrchestration/locales/zh-CN';
 import Plugin from '../components/Plugin/locales/zh-CN';
+import PluginFlow from '../components/PluginFlow/locales/zh-CN';
 import RawDataEditor from '../components/RawDataEditor/locales/zh-CN';
 import UpstreamComponent from '../components/Upstream/locales/zh-CN'
 
@@ -42,8 +42,8 @@ export default {
   ...component,
   ...other,
   ...ActionBarZhCN,
-  ...PluginOrchestration,
   ...Plugin,
+  ...PluginFlow,
   ...RawDataEditor,
   ...UpstreamComponent
 };
diff --git a/web/src/locales/zh-CN/component.ts b/web/src/locales/zh-CN/component.ts
index f3f813b..2acc640 100644
--- a/web/src/locales/zh-CN/component.ts
+++ b/web/src/locales/zh-CN/component.ts
@@ -76,5 +76,5 @@ export default {
   'component.global.noConfigurationRequired': '无需配置',
   'component.global.copy': '复制',
   'component.global.copySuccess': '复制成功',
-  'component.global.copyFail': '复制失败'
+  'component.global.copyFail': '复制失败',
 };
diff --git a/web/src/pages/Route/Create.tsx b/web/src/pages/Route/Create.tsx
index 295048e..770cc09 100644
--- a/web/src/pages/Route/Create.tsx
+++ b/web/src/pages/Route/Create.tsx
@@ -15,21 +15,21 @@
  * limitations under the License.
  */
 import React, { useState, useEffect, useRef } from 'react';
-import { Card, Steps, Form } from 'antd';
+import { Card, Steps, Form, Modal } from 'antd';
 import { PageHeaderWrapper } from '@ant-design/pro-layout';
 import { history, useIntl } from 'umi';
 import { isEmpty } from 'lodash';
 
 import ActionBar from '@/components/ActionBar';
+import FlowGraph from '@/components/PluginFlow/components/FlowGraph';
 
-import { transformer as chartTransformer } from '@/components/PluginOrchestration';
 import { create, fetchItem, update, checkUniqueName, checkHostWithSSL } from './service';
 import { transformProxyRewrite2Plugin } from './transform';
 import Step1 from './components/Step1';
 import Step2 from './components/Step2';
 import Step3 from './components/Step3';
 import CreateStep4 from './components/CreateStep4';
-import { DEFAULT_STEP_1_DATA, DEFAULT_STEP_3_DATA, INIT_CHART } from './constants';
+import { DEFAULT_STEP_1_DATA, DEFAULT_STEP_3_DATA } from './constants';
 import ResultView from './components/ResultView';
 import styles from './Create.less';
 
@@ -68,7 +68,6 @@ const Page: React.FC<Props> = (props) => {
 
   const [step, setStep] = useState(1);
   const [stepHeader, setStepHeader] = useState(STEP_HEADER_4);
-  const [chart, setChart] = useState(INIT_CHART);
 
   const setupRoute = (rid: number) =>
     fetchItem(rid).then((data) => {
@@ -159,9 +158,8 @@ const Page: React.FC<Props> = (props) => {
           data={step3Data}
           isForceHttps={form1.getFieldValue('redirectOption') === 'forceHttps'}
           isProxyEnable={getProxyRewriteEnable()}
-          onChange={({ plugins, script = INIT_CHART, plugin_config_id }) => {
+          onChange={({ plugins, script = {}, plugin_config_id }) => {
             setStep3Data({ plugins, script, plugin_config_id });
-            setChart(script);
           }}
         />
       );
@@ -195,6 +193,39 @@ const Page: React.FC<Props> = (props) => {
     return null;
   };
 
+  const savePlugins = (): boolean => {
+    const isScriptConfigured = FlowGraph.graph?.toJSON().cells.length
+    const isPluginsConfigured = Object.keys(step3Data.plugins || {}).length
+
+    if (step === 3 && isScriptConfigured && isPluginsConfigured) {
+      Modal.confirm({
+        title: formatMessage({ id: 'component.plugin-flow.text.both-modes-exist.title' }),
+        content: formatMessage({ id: 'component.plugin-flow.text.both-modes-exist' }),
+        onOk: () => {
+          const data = FlowGraph.convertToData()
+          if (data) {
+            setStep3Data({ script: data, plugins: {} });
+            setStep(4)
+          }
+        },
+        okText: formatMessage({ id: 'component.global.confirm' }),
+        cancelText: formatMessage({ id: 'component.global.cancel' }),
+      })
+      return false
+    }
+
+    if (isScriptConfigured) {
+      const data = FlowGraph.convertToData()
+      if (!data) {
+        return false
+      }
+      setStep3Data({ script: data, plugins: {} });
+    } else {
+      setStep3Data({ ...step3Data, script: {} });
+    }
+    return true
+  }
+
   const onStepChange = (nextStep: number) => {
     const onUpdateOrCreate = () => {
       const routeData = {
@@ -214,15 +245,6 @@ const Page: React.FC<Props> = (props) => {
       }
     };
 
-    const savePlugins = () => {
-      if (Object.keys(chart.nodes || {}).length) {
-        const transformChart = chartTransformer(chart);
-        setStep3Data({ script: { ...transformChart, chart }, plugins: {} });
-      } else {
-        setStep3Data({ ...step3Data, script: {} });
-      }
-    };
-
     if (nextStep === 1) {
       setStep(nextStep);
     }
@@ -242,8 +264,7 @@ const Page: React.FC<Props> = (props) => {
           });
         });
       } else {
-        savePlugins();
-        setStep(nextStep);
+        setStep(nextStep)
       }
       return;
     }
@@ -260,7 +281,10 @@ const Page: React.FC<Props> = (props) => {
     }
 
     if (nextStep === 4) {
-      savePlugins();
+      const result = savePlugins()
+      if (!result) {
+        return
+      }
       setStep(nextStep);
     }
 
@@ -282,7 +306,6 @@ const Page: React.FC<Props> = (props) => {
             ))}
           </Steps>
           {renderStepList()}
-          {/* NOTE: PluginOrchestration works unexpected when using <renderStepList/> */}
         </Card>
       </PageHeaderWrapper>
       <ActionBar step={step} lastStep={redirect ? 2 : 4} onChange={onStepChange} withResultView />
diff --git a/web/src/pages/Route/components/CreateStep4/CreateStep4.tsx b/web/src/pages/Route/components/CreateStep4/CreateStep4.tsx
index 26976eb..fb49fbd 100644
--- a/web/src/pages/Route/components/CreateStep4/CreateStep4.tsx
+++ b/web/src/pages/Route/components/CreateStep4/CreateStep4.tsx
@@ -18,8 +18,8 @@ import React from 'react';
 import type { FormInstance } from 'antd/lib/form';
 import { useIntl } from 'umi';
 
-import PluginOrchestration from '@/components/PluginOrchestration';
 import PluginPage from '@/components/Plugin';
+import PluginFlow from '@/components/PluginFlow';
 import Step1 from '../Step1';
 import Step2 from '../Step2';
 
@@ -39,7 +39,7 @@ const style = {
 
 const CreateStep4: React.FC<Props> = ({ form1, form2, redirect, upstreamRef, ...rest }) => {
   const { formatMessage } = useIntl();
-  const { plugins = {}, script = {}, plugin_config_id = '' } = rest.step3Data;
+  const { plugins = {}, plugin_config_id = '', script = {} } = rest.step3Data;
 
   return (
     <>
@@ -59,9 +59,9 @@ const CreateStep4: React.FC<Props> = ({ form1, form2, redirect, upstreamRef, ...
           <h2 style={style}>
             {formatMessage({ id: 'component.global.steps.stepTitle.pluginConfig' })}
           </h2>
-          {Boolean(Object.keys(plugins).length !== 0 || plugin_config_id !== '') && <PluginPage referPage = 'route' initialData={plugins} plugin_config_id={plugin_config_id} showSelector readonly />}
-          {Boolean(Object.keys(script).length !== 0) && (
-            <PluginOrchestration data={rest.step3Data.script.chart} readonly onChange={() => { }} />
+          {Boolean(Object.keys(plugins).length !== 0 || plugin_config_id !== '') && <PluginPage referPage='route' initialData={plugins} plugin_config_id={plugin_config_id} showSelector readonly />}
+          {Boolean(Object.keys(script || {}).length !== 0) && (
+            <PluginFlow chart={script.chart} readonly />
           )}
         </>
       )}
diff --git a/web/src/pages/Route/components/DebugViews/DebugDrawView.tsx b/web/src/pages/Route/components/DebugViews/DebugDrawView.tsx
index a152222..fa4b840 100644
--- a/web/src/pages/Route/components/DebugViews/DebugDrawView.tsx
+++ b/web/src/pages/Route/components/DebugViews/DebugDrawView.tsx
@@ -112,9 +112,9 @@ const DebugDrawView: React.FC<RouteModule.DebugDrawProps> = (props) => {
           }
         }
       }
-      case DEBUG_BODY_TYPE_SUPPORTED[DebugBodyType.RawInput]:{
+      case DEBUG_BODY_TYPE_SUPPORTED[DebugBodyType.RawInput]: {
         let contentType = [''];
-        switch (bodyCodeMirrorMode){
+        switch (bodyCodeMirrorMode) {
           case DEBUG_BODY_CODEMIRROR_MODE_SUPPORTED[0].mode:
             contentType = ['application/json;charset=UTF-8'];
             break;
@@ -209,7 +209,7 @@ const DebugDrawView: React.FC<RouteModule.DebugDrawProps> = (props) => {
 
   const handleDebug = (url: string) => {
     /* eslint-disable no-useless-escape */
-    if (!urlRegexSafe({exact: true, strict: false}).test(url)) {
+    if (!urlRegexSafe({ exact: true, strict: false }).test(url)) {
       notification.warning({
         message: formatMessage({ id: 'page.route.input.placeholder.requestUrl' }),
       });
@@ -217,7 +217,7 @@ const DebugDrawView: React.FC<RouteModule.DebugDrawProps> = (props) => {
     }
     const queryFormData = transformHeaderAndQueryParamsFormData(queryForm.getFieldsValue().params);
     const bodyFormRelateData = transformBodyParamsFormData();
-    const {bodyFormData, header: bodyFormHeader} = bodyFormRelateData;
+    const { bodyFormData, header: bodyFormHeader } = bodyFormRelateData;
     const pureHeaderFormData = transformHeaderAndQueryParamsFormData(
       headerForm.getFieldsValue().params,
     );
@@ -234,12 +234,12 @@ const DebugDrawView: React.FC<RouteModule.DebugDrawProps> = (props) => {
     }, bodyFormData)
       .then((req) => {
         setLoading(false);
-        const resp: RouteModule.debugResponse= req.data;
+        const resp: RouteModule.debugResponse = req.data;
         if (typeof (resp.data) !== 'string') {
           resp.data = JSON.stringify(resp.data, null, 2);
         }
         setResponse(resp);
-        const contentType=resp.header["Content-Type"];
+        const contentType = resp.header["Content-Type"];
         if (contentType == null || contentType.length !== 1) {
           setResponseBodyCodeMirrorMode("TEXT");
         } else if (contentType[0].toLowerCase().indexOf("json") !== -1) {
@@ -331,13 +331,13 @@ const DebugDrawView: React.FC<RouteModule.DebugDrawProps> = (props) => {
         >
           <Tabs>
             <TabPane data-cy='query' tab={formatMessage({ id: 'page.route.TabPane.queryParams' })} key="query">
-              <DebugParamsView form={queryForm} name='queryForm'/>
+              <DebugParamsView form={queryForm} name='queryForm' />
             </TabPane>
             <TabPane data-cy='auth' tab={formatMessage({ id: 'page.route.TabPane.authentication' })} key="auth">
               <AuthenticationView form={authForm} />
             </TabPane>
             <TabPane data-cy='header' tab={formatMessage({ id: 'page.route.TabPane.headerParams' })} key="header">
-              <DebugParamsView form={headerForm} name='headerForm' inputType="header"/>
+              <DebugParamsView form={headerForm} name='headerForm' inputType="header" />
             </TabPane>
             {showBodyTab && (
               <TabPane data-cy='body' tab={formatMessage({ id: 'page.route.TabPane.bodyParams' })} key="body">
@@ -371,7 +371,7 @@ const DebugDrawView: React.FC<RouteModule.DebugDrawProps> = (props) => {
                 )}
                 <div style={{ marginTop: 16 }}>
                   {bodyType === DEBUG_BODY_TYPE_SUPPORTED[DebugBodyType.FormUrlencoded] && (
-                    <DebugParamsView form={urlencodedForm} name='urlencodedForm'/>
+                    <DebugParamsView form={urlencodedForm} name='urlencodedForm' />
                   )}
 
                   {bodyType === DEBUG_BODY_TYPE_SUPPORTED[DebugBodyType.FormData] && (
@@ -420,7 +420,7 @@ const DebugDrawView: React.FC<RouteModule.DebugDrawProps> = (props) => {
                   onSelect={(mode) => setResponseBodyCodeMirrorMode(mode as string)}>
                   {
                     DEBUG_RESPONSE_BODY_CODEMIRROR_MODE_SUPPORTED.map(mode => {
-                      return <Option value={mode.mode}>{mode.name}</Option>
+                      return <Option value={mode.mode} key={mode.mode}>{mode.name}</Option>
                     })
                   }
                 </Select>
@@ -438,10 +438,10 @@ const DebugDrawView: React.FC<RouteModule.DebugDrawProps> = (props) => {
                     });
                   }}>
                   <Button type="text" disabled={!response}>
-                    <CopyOutlined/>
+                    <CopyOutlined />
                   </Button>
                 </CopyToClipboard>
-                <div id='codeMirror-response' style={{marginTop:16}}>
+                <div id='codeMirror-response' style={{ marginTop: 16 }}>
                   <CodeMirror
                     value={response ? response.data : ""}
                     height={codeMirrorHeight}
diff --git a/web/src/pages/Route/components/Step3/index.tsx b/web/src/pages/Route/components/Step3/index.tsx
index 614ae53..2977892 100644
--- a/web/src/pages/Route/components/Step3/index.tsx
+++ b/web/src/pages/Route/components/Step3/index.tsx
@@ -20,13 +20,19 @@ import { QuestionCircleOutlined } from '@ant-design/icons';
 import { isChrome, isChromium, isEdgeChromium } from 'react-device-detect';
 import { useIntl } from 'umi';
 
-import PluginOrchestration from '@/components/PluginOrchestration';
 import PluginPage from '@/components/Plugin';
+import PluginFlow from '@/components/PluginFlow';
+import { DEFAULT_PLUGIN_FLOW_DATA } from '@/components/PluginFlow/constants';
+import FlowGraph from '@/components/PluginFlow/components/FlowGraph';
 
 type Props = {
   data: {
     plugins: PluginComponent.Data;
-    script: Record<string, any>;
+    script: {
+      chart: Record<string, any>;
+      rule: Record<string, any>;
+      conf: Record<string, any>;
+    };
     plugin_config_id?: string;
   };
   onChange: (data: { plugins: PluginComponent.Data; script: any, plugin_config_id?: string; }) => void;
@@ -39,15 +45,13 @@ type Mode = 'NORMAL' | 'DRAW';
 
 const Page: React.FC<Props> = ({ data, onChange, readonly = false, isForceHttps = false, isProxyEnable = false }) => {
   const { formatMessage } = useIntl();
-  const { plugins = {}, script = {}, plugin_config_id = '' } = data;
+  const { plugins = {}, script = DEFAULT_PLUGIN_FLOW_DATA, plugin_config_id = '' } = data;
 
   // NOTE: Currently only compatible with chrome
   const useSupportBrowser = isChrome || isEdgeChromium || isChromium;
   const disableDraw = !useSupportBrowser || isForceHttps || isProxyEnable;
 
-  const type = Object.keys(script || {}).length === 0 || disableDraw ? 'NORMAL' : 'DRAW';
-
-  const [mode, setMode] = useState<Mode>(type);
+  const [mode, setMode] = useState<Mode>(Object.keys(script.chart?.cells || {}).length === 0 || disableDraw ? 'NORMAL' : 'DRAW');
 
   return (
     <>
@@ -55,15 +59,20 @@ const Page: React.FC<Props> = ({ data, onChange, readonly = false, isForceHttps
         <Radio.Group
           value={mode}
           onChange={(e) => {
+            if (e.target.value === 'NORMAL') {
+              // NOTE: current is DRAW
+              onChange({ ...data, script: { chart: FlowGraph.graph.toJSON() } })
+            }
+
             setMode(e.target.value);
           }}
           style={{ marginBottom: 10 }}
         >
           <Radio.Button value="NORMAL">
-            { formatMessage({ id: 'page.route.tabs.normalMode' }) }
+            {formatMessage({ id: 'page.route.tabs.normalMode' })}
           </Radio.Button>
           <Radio.Button value="DRAW" disabled={disableDraw}>
-            { formatMessage({ id: 'page.route.tabs.orchestration' }) }
+            {formatMessage({ id: 'page.route.tabs.orchestration' })}
           </Radio.Button>
         </Radio.Group>
         {Boolean(disableDraw) && (
@@ -74,13 +83,13 @@ const Page: React.FC<Props> = ({ data, onChange, readonly = false, isForceHttps
                 // NOTE: forceHttps do not support DRAW mode
                 const titleArr: string[] = [];
                 if (!useSupportBrowser) {
-                  titleArr.push(formatMessage({id: 'page.route.tooltip.pluginOrchOnlySuportChrome'}));
+                  titleArr.push(formatMessage({ id: 'page.route.tooltip.pluginOrchOnlySuportChrome' }));
                 }
                 if (isForceHttps) {
-                  titleArr.push(formatMessage({id: 'page.route.tooltip.pluginOrchWithoutRedirect'}));
+                  titleArr.push(formatMessage({ id: 'page.route.tooltip.pluginOrchWithoutRedirect' }));
                 }
                 if (isProxyEnable) {
-                  titleArr.push(formatMessage({id: 'page.route.tooltip.pluginOrchWithoutProxyRewrite'}));
+                  titleArr.push(formatMessage({ id: 'page.route.tooltip.pluginOrchWithoutProxyRewrite' }));
                 }
                 return titleArr.map((item, index) => `${index + 1}.${item}`).join('');
               }}
@@ -92,23 +101,18 @@ const Page: React.FC<Props> = ({ data, onChange, readonly = false, isForceHttps
       </div>
       {Boolean(mode === 'NORMAL') && (
         <PluginPage
+          readonly={readonly}
           initialData={plugins}
           plugin_config_id={plugin_config_id}
           schemaType="route"
           referPage="route"
           showSelector
           onChange={(pluginsData, id) => {
-            onChange({ plugins: pluginsData, script: {}, plugin_config_id: id })
+            onChange({ ...data, plugins: pluginsData, plugin_config_id: id })
           }}
         />
       )}
-      {Boolean(mode === 'DRAW') && (
-        <PluginOrchestration
-          data={script?.chart}
-          onChange={(scriptData) => onChange({ plugins: {}, script: scriptData })}
-          readonly={readonly}
-        />
-      )}
+      {Boolean(mode === 'DRAW') && (<PluginFlow chart={script.chart as any} />)}
     </>
   );
 };
diff --git a/web/src/pages/Route/constants.ts b/web/src/pages/Route/constants.ts
index f362736..f0aeea9 100644
--- a/web/src/pages/Route/constants.ts
+++ b/web/src/pages/Route/constants.ts
@@ -76,15 +76,6 @@ export const DEFAULT_STEP_3_DATA: RouteModule.Step3Data = {
   plugin_config_id: ""
 };
 
-export const INIT_CHART = {
-  offset: { x: 55.71, y: 21.69 },
-  scale: 0.329,
-  nodes: {},
-  links: {},
-  selected: {},
-  hovered: {},
-};
-
 export const AUTH_LIST = ['basic-auth', 'jwt-auth', 'key-auth'];
 
 export const HEADER_LIST = [
diff --git a/web/src/pages/Route/locales/en-US.ts b/web/src/pages/Route/locales/en-US.ts
index 2609800..60bc6b9 100644
--- a/web/src/pages/Route/locales/en-US.ts
+++ b/web/src/pages/Route/locales/en-US.ts
@@ -146,8 +146,8 @@ export default {
   'page.route.tooltip.pluginOrchWithoutProxyRewrite': 'Plugin orchestration mode cannot be used when request override is configured in Step 1.',
   'page.route.tooltip.pluginOrchWithoutRedirect': 'Plugin orchestration mode cannot be used when Redirect in Step 1 is selected to enable HTTPS.',
 
-  'page.route.tabs.normalMode': 'Normal mode',
-  'page.route.tabs.orchestration': 'Plugin orchestration',
+  'page.route.tabs.normalMode': 'Normal',
+  'page.route.tabs.orchestration': 'Orchestration',
 
   'page.route.list.description': 'Route is the entry point of a request, which defines the matching rules between a client request and a service. A route can be associated with a service (Service), an upstream (Upstream), a service can correspond to a set of routes, and a route can correspond to an upstream object (a set of backend service nodes), so each request matching to a route will be proxied by the gateway to the route-bound upstream service.',
 
diff --git a/web/src/pages/Route/locales/zh-CN.ts b/web/src/pages/Route/locales/zh-CN.ts
index 3c134bc..80a82bf 100644
--- a/web/src/pages/Route/locales/zh-CN.ts
+++ b/web/src/pages/Route/locales/zh-CN.ts
@@ -146,7 +146,7 @@ export default {
   'page.route.tooltip.pluginOrchWithoutRedirect': '当步骤一中 重定向 选择为 启用 HTTPS 时,不可使用插件编排模式。',
 
   'page.route.tabs.normalMode': '普通模式',
-  'page.route.tabs.orchestration': '插件编排',
+  'page.route.tabs.orchestration': '编排模式',
 
   'page.route.list.description': '路由(Route)是请求的入口点,它定义了客户端请求与服务之间的匹配规则。路由可以与服务(Service)、上游(Upstream)关联,一个服务可对应一组路由,一个路由可以对应一个上游对象(一组后端服务节点),因此,每个匹配到路由的请求将被网关代理到路由绑定的上游服务中。',
 
diff --git a/web/src/pages/Service/locales/en-US.ts b/web/src/pages/Service/locales/en-US.ts
index ca4952e..2c0f2e9 100644
--- a/web/src/pages/Service/locales/en-US.ts
+++ b/web/src/pages/Service/locales/en-US.ts
@@ -15,8 +15,8 @@
  * limitations under the License.
  */
 export default {
-  'page.service.steps.stepTitle.basicInformation': 'Basic Information',
-  'page.service.steps.stepTitle.pluginConfig': 'Plugin Config',
+  'page.service.steps.stepTitle.basicInformation': 'Basic',
+  'page.service.steps.stepTitle.pluginConfig': 'Plugin',
   'page.service.steps.stepTitle.preview': 'Preview',
   'page.service.list': 'Service List',
   'page.service.description': 'A service consists of a combination of public plugin configuration and upstream target information in a route. Services are associated with Routes and Upstreams, and a service can correspond to a set of upstream nodes and can be bound by multiple routes.',
diff --git a/web/yarn.lock b/web/yarn.lock
index b0abedb..446e9c2 100644
--- a/web/yarn.lock
+++ b/web/yarn.lock
@@ -2,6 +2,11 @@
 # yarn lockfile v1
 
 
+"@4tw/cypress-drag-drop@^1.6.0":
+  version "1.6.0"
+  resolved "https://registry.yarnpkg.com/@4tw/cypress-drag-drop/-/cypress-drag-drop-1.6.0.tgz#528cf837f16d16e059b2e3cae702d9de2c9e1088"
+  integrity sha512-B61iPspk2hZuuo3mjmlTqYZXJ9tusc8VyEk+5KMO/FTBrHKDWqYp8ANOJnIkRz6QfYZbx+qBoKBu7MTfvBCKew==
+
 "@ahooksjs/use-request@^2.0.0":
   version "2.8.3"
   resolved "https://registry.yarnpkg.com/@ahooksjs/use-request/-/use-request-2.8.3.tgz#9b7eff972658497473f61ceb89a268d665e28aeb"
@@ -185,6 +190,31 @@
     lodash "^4.17.15"
     resize-observer-polyfill "^1.5.0"
 
+"@antv/x6-react-components@^1.1.7":
+  version "1.1.7"
+  resolved "https://registry.yarnpkg.com/@antv/x6-react-components/-/x6-react-components-1.1.7.tgz#4f7d017b05dd3adb767a359bb0b9b4566b84ef8f"
+  integrity sha512-2G+J6GmCy6wfHJeDpopC7px6XbFc9zy0EFKAHkgOUS5XVj/n91VzvKViHd8miBRudkZ0ZTkf9NxtIR19hdOq6w==
+  dependencies:
+    clamp "^1.0.1"
+    classnames "^2.2.6"
+    rc-dropdown "^3.0.0-alpha.0"
+    rc-util "^4.15.7"
+    react-color "^2.17.3"
+    react-resize-detector "^6.6.4"
+    ua-parser-js "^0.7.20"
+
+"@antv/x6@^1.18.5":
+  version "1.18.5"
+  resolved "https://registry.yarnpkg.com/@antv/x6/-/x6-1.18.5.tgz#0215e8abbebd2ee508943aa58e62a88e2760799a"
+  integrity sha512-Bxn1pl5etaiDmO61Pc8EaqPqjld2vRz4+wF55VqFsGy2SpVYS1mPYZury1v5qYzAsU/BtQCW0j0RHppPR4U+UA==
+  dependencies:
+    csstype "^3.0.3"
+    jquery "^3.5.1"
+    jquery-mousewheel "^3.1.13"
+    lodash-es "^4.17.15"
+    mousetrap "^1.6.5"
+    utility-types "^3.10.0"
+
 "@babel/code-frame@7.0.0":
   version "7.0.0"
   resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0.tgz#06e2ab19bdb535385559aabb5ba59729482800f8"
@@ -1879,6 +1909,11 @@
   dependencies:
     "@hapi/hoek" "^9.0.0"
 
+"@icons/material@^0.2.4":
+  version "0.2.4"
+  resolved "https://registry.yarnpkg.com/@icons/material/-/material-0.2.4.tgz#e90c9f71768b3736e76d7dd6783fc6c2afa88bc8"
+  integrity sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==
+
 "@istanbuljs/load-nyc-config@^1.0.0":
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
@@ -2158,17 +2193,6 @@
   dependencies:
     extend "3.0.2"
 
-"@mrblenny/react-flow-chart@^0.0.14":
-  version "0.0.14"
-  resolved "https://registry.yarnpkg.com/@mrblenny/react-flow-chart/-/react-flow-chart-0.0.14.tgz#be11d06345c7222b41f488b38011b109e48a04b3"
-  integrity sha512-3bFjlmlYuqHpCRCPoA59jok2Vhe59ZKT5g9lb6U5IM+Zk2fIsKmXp8LEcliW0TrHtNMtZw5Gm3/rScrg/DwAFQ==
-  dependencies:
-    pathfinding "^0.4.18"
-    react-draggable "^4.4.3"
-    react-resize-observer "^1.1.1"
-    react-zoom-pan-pinch "^1.6.1"
-    uuid "^3.3.2"
-
 "@mrmlnc/readdir-enhanced@^2.2.1":
   version "2.2.1"
   resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde"
@@ -2899,6 +2923,11 @@
     "@types/scheduler" "*"
     csstype "^3.0.2"
 
+"@types/resize-observer-browser@^0.1.5":
+  version "0.1.5"
+  resolved "https://registry.yarnpkg.com/@types/resize-observer-browser/-/resize-observer-browser-0.1.5.tgz#36d897708172ac2380cd486da7a3daf1161c1e23"
+  integrity sha512-8k/67Z95Goa6Lznuykxkfhq9YU3l1Qe6LNZmwde1u7802a3x8v44oq0j91DICclxatTr0rNnhXx7+VTIetSrSQ==
+
 "@types/resolve@1.17.1":
   version "1.17.1"
   resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6"
@@ -5489,6 +5518,11 @@ cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3:
     inherits "^2.0.1"
     safe-buffer "^5.0.1"
 
+clamp@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/clamp/-/clamp-1.0.1.tgz#66a0e64011816e37196828fdc8c8c147312c8634"
+  integrity sha1-ZqDmQBGBbjcZaCj9yMjBRzEshjQ=
+
 class-utils@^0.3.5:
   version "0.3.6"
   resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463"
@@ -6352,6 +6386,11 @@ csstype@^3.0.2:
   resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.7.tgz#2a5fb75e1015e84dd15692f71e89a1450290950b"
   integrity sha512-KxnUB0ZMlnUWCsx2Z8MUsr6qV6ja1w9ArPErJaJaF8a5SOWoHLIszeCTKGRGRgtLgYrs1E8CHkNSP1VZTTPc9g==
 
+csstype@^3.0.3:
+  version "3.0.8"
+  resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.8.tgz#d2266a792729fb227cd216fb572f43728e1ad340"
+  integrity sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==
+
 currently-unhandled@^0.4.1:
   version "0.4.1"
   resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea"
@@ -8757,11 +8796,6 @@ hasha@^5.0.0:
     is-stream "^2.0.0"
     type-fest "^0.8.0"
 
-heap@0.2.5:
-  version "0.2.5"
-  resolved "https://registry.yarnpkg.com/heap/-/heap-0.2.5.tgz#713b65590ebcc40fcbeeaf55e851694092b39af1"
-  integrity sha1-cTtlWQ68xA/L7q9V6FFpQJKzmvE=
-
 history-with-query@4.10.4:
   version "4.10.4"
   resolved "https://registry.yarnpkg.com/history-with-query/-/history-with-query-4.10.4.tgz#8161ff3c5044e29dfaeb73e7587eb3d4c1a8090e"
@@ -10326,6 +10360,16 @@ joi@^17.3.0:
     "@sideway/formula" "^3.0.0"
     "@sideway/pinpoint" "^2.0.0"
 
+jquery-mousewheel@^3.1.13:
+  version "3.1.13"
+  resolved "https://registry.yarnpkg.com/jquery-mousewheel/-/jquery-mousewheel-3.1.13.tgz#06f0335f16e353a695e7206bf50503cb523a6ee5"
+  integrity sha1-BvAzXxbjU6aV5yBr9QUDy1I6buU=
+
+jquery@^3.5.1:
+  version "3.6.0"
+  resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.6.0.tgz#c72a09f15c1bdce142f49dbf1170bdf8adac2470"
+  integrity sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==
+
 js-beautify@^1.13.0:
   version "1.13.5"
   resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.13.5.tgz#a08a97890cae55daf1d758d3f6577bd4a64d7014"
@@ -10868,6 +10912,11 @@ locate-path@^5.0.0:
   dependencies:
     p-locate "^4.1.0"
 
+lodash-es@^4.17.15:
+  version "4.17.21"
+  resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
+  integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
+
 lodash._reinterpolate@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
@@ -10968,7 +11017,7 @@ lodash@4.17.20:
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
   integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
 
-lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4:
+lodash@^4.0.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4:
   version "4.17.21"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
   integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@@ -11168,6 +11217,11 @@ marked@1.2.7:
   resolved "https://registry.yarnpkg.com/marked/-/marked-1.2.7.tgz#6e14b595581d2319cdcf033a24caaf41455a01fb"
   integrity sha512-No11hFYcXr/zkBvL6qFmAp1z6BKY3zqLMHny/JN/ey+al7qwCM2+CMBL9BOgqMxZU36fz4cCWfn2poWIf7QRXA==
 
+material-colors@^1.2.1:
+  version "1.2.6"
+  resolved "https://registry.yarnpkg.com/material-colors/-/material-colors-1.2.6.tgz#6d1958871126992ceecc72f4bcc4d8f010865f46"
+  integrity sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==
+
 mathml-tag-names@^2.0.1, mathml-tag-names@^2.1.3:
   version "2.1.3"
   resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3"
@@ -11579,6 +11633,11 @@ moo@^0.5.0:
   resolved "https://registry.yarnpkg.com/moo/-/moo-0.5.1.tgz#7aae7f384b9b09f620b6abf6f74ebbcd1b65dbc4"
   integrity sha512-I1mnb5xn4fO80BH9BLcF0yLypy2UKl+Cb01Fu0hJRkJjlCRtxZMWkTdAtDd5ZqCOxtCkhmRwyI57vWT+1iZ67w==
 
+mousetrap@^1.6.5:
+  version "1.6.5"
+  resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.5.tgz#8a766d8c272b08393d5f56074e0b5ec183485bf9"
+  integrity sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA==
+
 ms@2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
@@ -12565,13 +12624,6 @@ path-type@^4.0.0:
   resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
   integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
 
-pathfinding@^0.4.18:
-  version "0.4.18"
-  resolved "https://registry.yarnpkg.com/pathfinding/-/pathfinding-0.4.18.tgz#a9990f6fa22b7ef196e5651b049165403a045fe8"
-  integrity sha1-qZkPb6IrfvGW5WUbBJFlQDoEX+g=
-  dependencies:
-    heap "0.2.5"
-
 pause-stream@0.0.11:
   version "0.0.11"
   resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445"
@@ -13348,7 +13400,7 @@ prompts@^2.0.1:
     kleur "^3.0.3"
     sisteransi "^1.0.5"
 
-prop-types@15.x, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.6, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2:
+prop-types@15.x, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.6, prop-types@^15.5.8, prop-types@^15.6.2, prop-types@^15.7.2:
   version "15.7.2"
   resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
   integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@@ -13691,7 +13743,7 @@ rc-drawer@~4.3.0:
     classnames "^2.2.6"
     rc-util "^5.7.0"
 
-rc-dropdown@^3.1.3, rc-dropdown@~3.2.0:
+rc-dropdown@^3.0.0-alpha.0, rc-dropdown@^3.1.3, rc-dropdown@~3.2.0:
   version "3.2.0"
   resolved "https://registry.yarnpkg.com/rc-dropdown/-/rc-dropdown-3.2.0.tgz#da6c2ada403842baee3a9e909a0b1a91ba3e1090"
   integrity sha512-j1HSw+/QqlhxyTEF6BArVZnTmezw2LnSmRk6I9W7BCqNCKaRwleRmMMs1PHbuaG8dKHVqP6e21RQ7vPBLVnnNw==
@@ -14018,7 +14070,7 @@ rc-upload@~4.2.0-alpha.0:
     classnames "^2.2.5"
     rc-util "^5.2.0"
 
-rc-util@4.x, rc-util@^4.0.4, rc-util@^4.15.3, rc-util@^4.4.0:
+rc-util@4.x, rc-util@^4.0.4, rc-util@^4.15.3, rc-util@^4.15.7, rc-util@^4.4.0:
   version "4.21.1"
   resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-4.21.1.tgz#88602d0c3185020aa1053d9a1e70eac161becb05"
   integrity sha512-Z+vlkSQVc1l8O2UjR3WQ+XdWlhj5q9BMQNLk2iOBch75CqPfrJyGtcWMcnhRlNuDu0Ndtt4kLVO8JI8BrABobg==
@@ -14068,6 +14120,19 @@ react-app-polyfill@^1.0.4:
     regenerator-runtime "^0.13.3"
     whatwg-fetch "^3.0.0"
 
+react-color@^2.17.3:
+  version "2.19.3"
+  resolved "https://registry.yarnpkg.com/react-color/-/react-color-2.19.3.tgz#ec6c6b4568312a3c6a18420ab0472e146aa5683d"
+  integrity sha512-LEeGE/ZzNLIsFWa1TMe8y5VYqr7bibneWmvJwm1pCn/eNmrabWDh659JSPn9BuaMpEfU83WTOJfnCcjDZwNQTA==
+  dependencies:
+    "@icons/material" "^0.2.4"
+    lodash "^4.17.15"
+    lodash-es "^4.17.15"
+    material-colors "^1.2.1"
+    prop-types "^15.5.10"
+    reactcss "^1.2.0"
+    tinycolor2 "^1.4.1"
+
 react-copy-to-clipboard@^5.0.3:
   version "5.0.3"
   resolved "https://registry.yarnpkg.com/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.3.tgz#2a0623b1115a1d8c84144e9434d3342b5af41ab4"
@@ -14124,14 +14189,6 @@ react-dom@16.x, react-dom@^16.8.6:
     prop-types "^15.6.2"
     scheduler "^0.19.1"
 
-react-draggable@^4.4.3:
-  version "4.4.3"
-  resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.4.3.tgz#0727f2cae5813e36b0e4962bf11b2f9ef2b406f3"
-  integrity sha512-jV4TE59MBuWm7gb6Ns3Q1mxX8Azffb7oTtDtBgFkxRvhDp38YAARmRplrj0+XGkhOJB5XziArX+4HUUABtyZ0w==
-  dependencies:
-    classnames "^2.2.5"
-    prop-types "^15.6.0"
-
 react-error-overlay@^5.1.6:
   version "5.1.6"
   resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-5.1.6.tgz#0cd73407c5d141f9638ae1e0c63e7b2bf7e9929d"
@@ -14221,10 +14278,15 @@ react-refresh@0.9.0:
   resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.9.0.tgz#71863337adc3e5c2f8a6bfddd12ae3bfe32aafbf"
   integrity sha512-Gvzk7OZpiqKSkxsQvO/mbTN1poglhmAV7gR/DdIrRrSMXraRQQlfikRJOr3Nb9GTMPC5kof948Zy6jJZIFtDvQ==
 
-react-resize-observer@^1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/react-resize-observer/-/react-resize-observer-1.1.1.tgz#641dfa2e0f4bd2549a8ab4bbbaf43b68f3dcaf76"
-  integrity sha512-3R+90Hou90Mr3wJYc+unsySC8Pn91V4nmjO32NKvUvjphRUbq9HisyLg7bDyGBE7xlMrrM6Fax7iNQaFdc/FYA==
+react-resize-detector@^6.6.4:
+  version "6.6.5"
+  resolved "https://registry.yarnpkg.com/react-resize-detector/-/react-resize-detector-6.6.5.tgz#adde70db9c76da09892134b8f6c4dfd351cdd93c"
+  integrity sha512-khKS1IpC2cfx5+6G9HkAU/9CGjDV8woE57pVeH8nP5Ji52yXz6MpQEHEzJZ2obGghWrewN4php8ArxB4yWNqZA==
+  dependencies:
+    "@types/resize-observer-browser" "^0.1.5"
+    lodash.debounce "^4.0.8"
+    lodash.throttle "^4.1.1"
+    resize-observer-polyfill "^1.5.1"
 
 react-router-config@5.1.1:
   version "5.1.1"
@@ -14275,11 +14337,6 @@ react-tween-state@^0.1.5:
     raf "^3.1.0"
     tween-functions "^1.0.1"
 
-react-zoom-pan-pinch@^1.6.1:
-  version "1.6.1"
-  resolved "https://registry.yarnpkg.com/react-zoom-pan-pinch/-/react-zoom-pan-pinch-1.6.1.tgz#da16267c258ab37e8ebcdc7c252794a9633e91ec"
-  integrity sha512-J2eM0gZ04XiUWvmKZrOhSAB2zjyoK7kw2POIeN1X0yTTlmp6HPGV0zYfjnlkhgt8nQwpvXAbsF/oAnkuiwk1kA==
-
 react@16.x, react@^16.8.6:
   version "16.14.0"
   resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d"
@@ -14289,6 +14346,13 @@ react@16.x, react@^16.8.6:
     object-assign "^4.1.1"
     prop-types "^15.6.2"
 
+reactcss@^1.2.0:
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/reactcss/-/reactcss-1.2.3.tgz#c00013875e557b1cf0dfd9a368a1c3dab3b548dd"
+  integrity sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==
+  dependencies:
+    lodash "^4.0.1"
+
 read-only-stream@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/read-only-stream/-/read-only-stream-2.0.0.tgz#2724fd6a8113d73764ac288d4386270c1dbf17f0"
@@ -16577,6 +16641,11 @@ tiny-warning@^1.0.0, tiny-warning@^1.0.3:
   resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
   integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
 
+tinycolor2@^1.4.1:
+  version "1.4.2"
+  resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.2.tgz#3f6a4d1071ad07676d7fa472e1fac40a719d8803"
+  integrity sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==
+
 tlds@^1.217.0:
   version "1.218.0"
   resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.218.0.tgz#f31804891c650c136f88cb8ec2f043577b5f5afd"
@@ -16860,6 +16929,11 @@ ua-parser-js@^0.7.18, ua-parser-js@^0.7.24:
   resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.24.tgz#8d3ecea46ed4f1f1d63ec25f17d8568105dc027c"
   integrity sha512-yo+miGzQx5gakzVK3QFfN0/L9uVhosXBBO7qmnk7c2iw1IhL212wfA3zbnI54B0obGwC/5NWub/iT9sReMx+Fw==
 
+ua-parser-js@^0.7.20:
+  version "0.7.28"
+  resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31"
+  integrity sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g==
+
 umd@^3.0.0:
   version "3.0.3"
   resolved "https://registry.yarnpkg.com/umd/-/umd-3.0.3.tgz#aa9fe653c42b9097678489c01000acb69f0b26cf"
@@ -17285,6 +17359,11 @@ util@~0.10.1:
   dependencies:
     inherits "2.0.3"
 
+utility-types@^3.10.0:
+  version "3.10.0"
+  resolved "https://registry.yarnpkg.com/utility-types/-/utility-types-3.10.0.tgz#ea4148f9a741015f05ed74fd615e1d20e6bed82b"
+  integrity sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==
+
 utils-merge@1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"