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 8f94c84  feat(FE): fetch category from manager-api (#1122)
8f94c84 is described below

commit 8f94c844218913fdf9e36cd7c62bbe94188ffed3
Author: litesun <[email protected]>
AuthorDate: Tue Dec 29 15:01:54 2020 +0800

    feat(FE): fetch category from manager-api (#1122)
    
    * feat: fetch category from manager-api
    
    * feat: add category sort
    
    * feat: use consumer_schema in consumer module
    
    * feat: use local PluginOrchestration components
    
    * feat: add  license
    
    * feat: pluginOrchestration fetch type from manager-api
    
    * feat: remove json-schema
    
    * feat: add pluginOrchestration i18n
    
    * feat: add showList sort
    
    * feat: clean code
    
    * feat: update name
    
    Co-authored-by: juzhiyuan <[email protected]>
---
 web/package.json                                   |   6 +-
 web/src/components/Plugin/PluginPage.tsx           | 211 ++++++++--------
 web/src/components/Plugin/service.ts               |  53 +---
 web/src/components/Plugin/typing.d.ts              |  18 +-
 .../PluginOrchestration/DrawPluginStyle.ts         |  82 ++++++
 .../components/Page.tsx}                           |  24 +-
 .../components/SidebarItem.tsx}                    |  42 ++--
 .../components/index.ts}                           |  24 +-
 .../PluginOrchestration/constants.ts}              |  63 +++--
 .../PluginOrchestration/customConfig.tsx           |  71 ++++++
 web/src/components/PluginOrchestration/index.tsx   | 277 +++++++++++++++++++++
 .../locales/en-US.ts}                              |  32 +--
 .../locales/zh-CN.ts}                              |  32 +--
 .../typing.d.ts => PluginOrchestration/service.ts} |  27 +-
 .../components/PluginOrchestration/transform.ts    | 122 +++++++++
 .../{Plugin => PluginOrchestration}/typing.d.ts    |  24 +-
 web/src/locales/en-US.ts                           |   2 +
 web/src/locales/zh-CN.ts                           |   2 +
 web/src/pages/Route/Create.tsx                     |   7 +-
 .../Route/components/CreateStep4/CreateStep4.tsx   |   4 +-
 web/src/pages/Route/components/Step3/index.tsx     |   2 +-
 web/yarn.lock                                      |  88 ++-----
 22 files changed, 805 insertions(+), 408 deletions(-)

diff --git a/web/package.json b/web/package.json
index 02a5bb8..9016a80 100644
--- a/web/package.json
+++ b/web/package.json
@@ -50,8 +50,8 @@
     "@ant-design/icons": "^4.0.0",
     "@ant-design/pro-layout": "^6.0.0",
     "@ant-design/pro-table": "2.6.3",
-    "@api7-dashboard/pluginchart": "^1.0.14",
     "@api7-dashboard/ui": "^1.0.3",
+    "@mrblenny/react-flow-chart": "^0.0.14",
     "@rjsf/antd": "2.2.0",
     "@rjsf/core": "2.2.0",
     "@uiw/react-codemirror": "^3.0.1",
@@ -73,6 +73,7 @@
     "react-dom": "^16.8.6",
     "react-helmet-async": "^1.0.4",
     "start-server-and-test": "^1.11.5",
+    "styled-components": "^5.2.1",
     "umi": "^3.1.2",
     "umi-request": "^1.0.8",
     "use-merge-value": "^1.0.1",
@@ -80,6 +81,7 @@
   },
   "devDependencies": {
     "@ant-design/pro-cli": "^2.0.2",
+    "@types/base-64": "^0.1.3",
     "@types/classnames": "^2.2.7",
     "@types/express": "^4.17.0",
     "@types/history": "^4.7.2",
@@ -91,8 +93,8 @@
     "@types/react": "^16.9.17",
     "@types/react-dom": "^16.8.4",
     "@types/react-helmet": "^5.0.13",
+    "@types/styled-components": "^5.1.7",
     "@types/uuid": "7.0.4",
-    "@types/base-64": "^0.1.3",
     "@umijs/fabric": "^2.2.0",
     "@umijs/plugin-blocks": "^2.0.5",
     "@umijs/plugin-esbuild": "^1.0.0-beta.2",
diff --git a/web/src/components/Plugin/PluginPage.tsx 
b/web/src/components/Plugin/PluginPage.tsx
index e88f417..3a3f76f 100644
--- a/web/src/components/Plugin/PluginPage.tsx
+++ b/web/src/components/Plugin/PluginPage.tsx
@@ -15,12 +15,12 @@
  * limitations under the License.
  */
 import React, { useEffect, useState } from 'react';
-import { Anchor, Layout, Switch, Card, Tooltip, Button, notification, Avatar } 
from 'antd';
+import { Anchor, Layout, Switch, Card, Tooltip, Button, notification } from 
'antd';
 import { SettingFilled } from '@ant-design/icons';
 import { PanelSection } from '@api7-dashboard/ui';
 import Ajv, { DefinedError } from 'ajv';
 
-import { fetchSchema, getList } from './service';
+import { fetchList } from './service';
 import CodeMirrorDrawer from './CodeMirrorDrawer';
 
 type Props = {
@@ -51,11 +51,23 @@ const PluginPage: React.FC<Props> = ({
   schemaType = '',
   onChange = () => {},
 }) => {
-  const [pluginList, setPlugin] = useState<PluginComponent.Meta[][]>([]);
+  const [pluginList, setPluginList] = useState<PluginComponent.Meta[]>([]);
   const [name, setName] = useState<string>(NEVER_EXIST_PLUGIN_FLAG);
+  const [typeList, setTypeList] = useState<string[]>([]);
 
+  const firstUpperCase = ([first, ...rest]: string) => first.toUpperCase() + 
rest.join('');
   useEffect(() => {
-    getList().then(setPlugin);
+    fetchList().then((data) => {
+      setPluginList(data);
+
+      const categoryList: string[] = [];
+      data.forEach((item) => {
+        if (!categoryList.includes(firstUpperCase(item.type))) {
+          categoryList.push(firstUpperCase(item.type));
+        }
+      });
+      setTypeList(categoryList.sort());
+    });
   }, []);
 
   // NOTE: This function has side effect because it mutates the original 
schema data
@@ -73,48 +85,55 @@ const PluginPage: React.FC<Props> = ({
   };
 
   const validateData = (pluginName: string, value: PluginComponent.Data) => {
-    fetchSchema(pluginName, schemaType).then((schema) => {
-      if (schema.oneOf) {
-        (schema.oneOf || []).forEach((item: any) => {
-          injectDisableProperty(item);
-        });
-      } else {
-        injectDisableProperty(schema);
-      }
+    const plugin = pluginList.find((item) => item.name === pluginName);
+    let schema: any = {};
 
-      const validate = ajv.compile(schema);
+    if (schemaType === 'consumer' && plugin?.consumer_schema) {
+      schema = plugin.consumer_schema;
+    } else if (plugin?.schema) {
+      schema = plugin.schema;
+    }
 
-      if (validate(value)) {
-        setName(NEVER_EXIST_PLUGIN_FLAG);
-        onChange({ ...initialData, [pluginName]: value });
-        return;
-      }
+    if (schema.oneOf) {
+      (schema.oneOf || []).forEach((item: any) => {
+        injectDisableProperty(item);
+      });
+    } else {
+      injectDisableProperty(schema);
+    }
 
-      // eslint-disable-next-line
-      for (const err of validate.errors as DefinedError[]) {
-        let description = '';
-        switch (err.keyword) {
-          case 'enum':
-            description = `${err.dataPath} ${err.message}: 
${err.params.allowedValues.join(', ')}`;
-            break;
-          case 'minItems':
-          case 'type':
-            description = `${err.dataPath} ${err.message}`;
-            break;
-          case 'oneOf':
-          case 'required':
-            description = err.message || '';
-            break;
-          default:
-            description = `${err.schemaPath} ${err.message}`;
-        }
-        notification.error({
-          message: 'Invalid plugin data',
-          description,
-        });
+    const validate = ajv.compile(schema);
+
+    if (validate(value)) {
+      setName(NEVER_EXIST_PLUGIN_FLAG);
+      onChange({ ...initialData, [pluginName]: value });
+      return;
+    }
+
+    // eslint-disable-next-line
+    for (const err of validate.errors as DefinedError[]) {
+      let description = '';
+      switch (err.keyword) {
+        case 'enum':
+          description = `${err.dataPath} ${err.message}: 
${err.params.allowedValues.join(', ')}`;
+          break;
+        case 'minItems':
+        case 'type':
+          description = `${err.dataPath} ${err.message}`;
+          break;
+        case 'oneOf':
+        case 'required':
+          description = err.message || '';
+          break;
+        default:
+          description = `${err.schemaPath} ${err.message}`;
       }
-      setName(pluginName);
-    });
+      notification.error({
+        message: 'Invalid plugin data',
+        description,
+      });
+    }
+    setName(pluginName);
   };
 
   return (
@@ -133,78 +152,60 @@ const PluginPage: React.FC<Props> = ({
       <Layout>
         <Sider theme="light">
           <Anchor offsetTop={150}>
-            {pluginList.map((plugins) => {
-              const { category } = plugins[0];
-              return (
-                <Anchor.Link
-                  href={`#plugin-category-${category}`}
-                  title={category}
-                  key={category}
-                />
-              );
+            {typeList.map((type) => {
+              return <Anchor.Link href={`#plugin-category-${type}`} 
title={type} key={type} />;
             })}
           </Anchor>
         </Sider>
         <Content style={{ padding: '0 10px', backgroundColor: '#fff', 
minHeight: 1400 }}>
-          {pluginList.map((plugins) => {
-            const { category } = plugins[0];
+          {typeList.map((type) => {
             return (
               <PanelSection
-                title={category}
-                key={category}
+                title={type}
+                key={type}
                 style={PanelSectionStyle}
-                id={`plugin-category-${category}`}
+                id={`plugin-category-${type}`}
               >
-                {plugins.map((item) => (
-                  <Card
-                    key={item.name}
-                    title={[
-                      item.avatar && (
-                        <Avatar
-                          key={1}
-                          icon={item.avatar}
-                          className="plugin-avatar"
-                          style={{
-                            marginRight: 5,
-                          }}
-                        />
-                      ),
-                      <span key={2}>{item.name}</span>,
-                    ]}
-                    style={{ height: 66 }}
-                    extra={[
-                      <Tooltip title="Setting" 
key={`plugin-card-${item.name}-extra-tooltip-2`}>
-                        <Button
-                          shape="circle"
-                          icon={<SettingFilled />}
-                          style={{ marginRight: 10, marginLeft: 10 }}
-                          size="middle"
-                          onClick={() => {
-                            setName(item.name);
+                {pluginList
+                  .filter((item) => item.type === type.toLowerCase())
+                  .map((item) => (
+                    <Card
+                      key={item.name}
+                      title={[<span key={2}>{item.name}</span>]}
+                      style={{ height: 66 }}
+                      extra={[
+                        <Tooltip title="Setting" 
key={`plugin-card-${item.name}-extra-tooltip-2`}>
+                          <Button
+                            shape="circle"
+                            icon={<SettingFilled />}
+                            style={{ marginRight: 10, marginLeft: 10 }}
+                            size="middle"
+                            onClick={() => {
+                              setName(item.name);
+                            }}
+                          />
+                        </Tooltip>,
+                        <Switch
+                          defaultChecked={initialData[item.name] && 
!initialData[item.name].disable}
+                          disabled={readonly}
+                          onChange={(isChecked) => {
+                            if (isChecked) {
+                              validateData(item.name, {
+                                ...initialData[item.name],
+                                disable: false,
+                              });
+                            } else {
+                              onChange({
+                                ...initialData,
+                                [item.name]: { ...initialData[item.name], 
disable: true },
+                              });
+                            }
                           }}
-                        />
-                      </Tooltip>,
-                      <Switch
-                        defaultChecked={initialData[item.name] && 
!initialData[item.name].disable}
-                        disabled={readonly}
-                        onChange={(isChecked) => {
-                          if (isChecked) {
-                            validateData(item.name, {
-                              ...initialData[item.name],
-                              disable: false,
-                            });
-                          } else {
-                            onChange({
-                              ...initialData,
-                              [item.name]: { ...initialData[item.name], 
disable: true },
-                            });
-                          }
-                        }}
-                        key={Math.random().toString(36).substring(7)}
-                      />,
-                    ]}
-                  />
-                ))}
+                          key={Math.random().toString(36).substring(7)}
+                        />,
+                      ]}
+                    />
+                  ))}
               </PanelSection>
             );
           })}
diff --git a/web/src/components/Plugin/service.ts 
b/web/src/components/Plugin/service.ts
index 1f94ab6..7a15639 100644
--- a/web/src/components/Plugin/service.ts
+++ b/web/src/components/Plugin/service.ts
@@ -17,55 +17,10 @@
 import { omit } from 'lodash';
 import { request } from 'umi';
 
-import { PLUGIN_MAPPER_SOURCE } from './data';
-
-enum Category {
-  'Limit traffic',
-  'Observability',
-  'Security',
-  'Authentication',
-  'Log',
-  'Other',
-}
-
-export const fetchList = () => request<Res<string[]>>('/plugins');
-
-let cachedPluginNameList: string[] = [];
-export const getList = async () => {
-  if (!cachedPluginNameList.length) {
-    cachedPluginNameList = (await fetchList()).data;
-  }
-  const names = cachedPluginNameList;
-  const data: Record<string, PluginComponent.Meta[]> = {};
-
-  names.forEach((name) => {
-    const plugin = PLUGIN_MAPPER_SOURCE[name] || {};
-    const { category = 'Other', hidden = false } = plugin;
-
-    // NOTE: assign it to Authentication plugin
-    if (name.includes('auth')) {
-      plugin.category = 'Authentication';
-    }
-
-    if (!data[category]) {
-      data[category] = [];
-    }
-
-    if (!hidden) {
-      data[category] = data[category].concat({
-        ...plugin,
-        name,
-      });
-    }
-  });
-
-  return Object.keys(data)
-    .sort((a, b) => Category[a] - Category[b])
-    .map((category) => {
-      return data[category].sort((a, b) => {
-        return (a.priority || 9999) - (b.priority || 9999);
-      });
-    });
+export const fetchList = () => {
+  return request<Res<PluginComponent.Meta[]>>('/plugins?all=true').then(data 
=> {
+    return data.data;
+  })
 };
 
 /**
diff --git a/web/src/components/Plugin/typing.d.ts 
b/web/src/components/Plugin/typing.d.ts
index 1d8b7c8..5745bfd 100644
--- a/web/src/components/Plugin/typing.d.ts
+++ b/web/src/components/Plugin/typing.d.ts
@@ -19,20 +19,12 @@ declare namespace PluginComponent {
 
   type Schema = '' | 'route' | 'consumer' | 'service';
 
-  type Category =
-    | 'Security'
-    | 'Limit traffic'
-    | 'Log'
-    | 'Observability'
-    | 'Other'
-    | 'Authentication';
-
   type Meta = {
     name: string;
-    category: Category;
-    hidden?: boolean;
-    // Note: Plugins are sorted by priority under the same category in the 
frontend, the smaller the number, the higher the priority. The default value is 
9999.
-    priority?: number;
-    avatar?: React.ReactNode;
+    priority: number;
+    schema: object;
+    type: string;
+    version: number;
+    consumer_schema?: object;
   };
 }
diff --git a/web/src/components/PluginOrchestration/DrawPluginStyle.ts 
b/web/src/components/PluginOrchestration/DrawPluginStyle.ts
new file mode 100644
index 0000000..a1d40b2
--- /dev/null
+++ b/web/src/components/PluginOrchestration/DrawPluginStyle.ts
@@ -0,0 +1,82 @@
+/*
+ * 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/Plugin/typing.d.ts 
b/web/src/components/PluginOrchestration/components/Page.tsx
similarity index 59%
copy from web/src/components/Plugin/typing.d.ts
copy to web/src/components/PluginOrchestration/components/Page.tsx
index 1d8b7c8..2bdc3ff 100644
--- a/web/src/components/Plugin/typing.d.ts
+++ b/web/src/components/PluginOrchestration/components/Page.tsx
@@ -14,25 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-declare namespace PluginComponent {
-  type Data = object;
+import * as React from 'react';
+import { SPageContent } from '../DrawPluginStyle';
 
-  type Schema = '' | 'route' | 'consumer' | 'service';
-
-  type Category =
-    | 'Security'
-    | 'Limit traffic'
-    | 'Log'
-    | 'Observability'
-    | 'Other'
-    | 'Authentication';
-
-  type Meta = {
-    name: string;
-    category: Category;
-    hidden?: boolean;
-    // Note: Plugins are sorted by priority under the same category in the 
frontend, the smaller the number, the higher the priority. The default value is 
9999.
-    priority?: number;
-    avatar?: React.ReactNode;
-  };
-}
+export const Page: React.FC = (props) => 
<SPageContent>{props.children}</SPageContent>;
diff --git a/web/src/locales/en-US.ts 
b/web/src/components/PluginOrchestration/components/SidebarItem.tsx
similarity index 55%
copy from web/src/locales/en-US.ts
copy to web/src/components/PluginOrchestration/components/SidebarItem.tsx
index 6ea6069..90be87d 100644
--- a/web/src/locales/en-US.ts
+++ b/web/src/components/PluginOrchestration/components/SidebarItem.tsx
@@ -14,26 +14,28 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import { ActionBarEnUS } from '@/components/ActionBar';
+import * as React from 'react';
+import { INode, REACT_FLOW_CHART } from '@mrblenny/react-flow-chart';
+import { Button } from 'antd';
 
-import component from './en-US/component';
-import globalHeader from './en-US/globalHeader';
-import menu from './en-US/menu';
-import pwa from './en-US/pwa';
-import settingDrawer from './en-US/settingDrawer';
-import settings from './en-US/setting';
+import { SOuter } from '../DrawPluginStyle';
 
-export default {
-  'navBar.lang': 'Languages',
-  'layout.user.link.help': 'Help',
-  'layout.user.link.privacy': 'Privacy',
-  'layout.user.link.terms': 'Terms',
-  'app.preview.down.block': 'Download this page to your local project',
-  ...globalHeader,
-  ...menu,
-  ...settingDrawer,
-  ...settings,
-  ...pwa,
-  ...component,
-  ...ActionBarEnUS,
+export interface 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/Plugin/typing.d.ts 
b/web/src/components/PluginOrchestration/components/index.ts
similarity index 59%
copy from web/src/components/Plugin/typing.d.ts
copy to web/src/components/PluginOrchestration/components/index.ts
index 1d8b7c8..cb920f2 100644
--- a/web/src/components/Plugin/typing.d.ts
+++ b/web/src/components/PluginOrchestration/components/index.ts
@@ -14,25 +14,5 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-declare namespace PluginComponent {
-  type Data = object;
-
-  type Schema = '' | 'route' | 'consumer' | 'service';
-
-  type Category =
-    | 'Security'
-    | 'Limit traffic'
-    | 'Log'
-    | 'Observability'
-    | 'Other'
-    | 'Authentication';
-
-  type Meta = {
-    name: string;
-    category: Category;
-    hidden?: boolean;
-    // Note: Plugins are sorted by priority under the same category in the 
frontend, the smaller the number, the higher the priority. The default value is 
9999.
-    priority?: number;
-    avatar?: React.ReactNode;
-  };
-}
+export * from './Page';
+export * from './SidebarItem';
diff --git a/web/src/locales/en-US.ts 
b/web/src/components/PluginOrchestration/constants.ts
similarity index 55%
copy from web/src/locales/en-US.ts
copy to web/src/components/PluginOrchestration/constants.ts
index 6ea6069..2a8c97c 100644
--- a/web/src/locales/en-US.ts
+++ b/web/src/components/PluginOrchestration/constants.ts
@@ -14,26 +14,49 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import { ActionBarEnUS } from '@/components/ActionBar';
+export const INIT_CHART = {
+  offset: { x: 0, y: 0 },
+  scale: 0.577,
+  nodes: {},
+  links: {},
+  selected: {},
+  hovered: {},
+};
 
-import component from './en-US/component';
-import globalHeader from './en-US/globalHeader';
-import menu from './en-US/menu';
-import pwa from './en-US/pwa';
-import settingDrawer from './en-US/settingDrawer';
-import settings from './en-US/setting';
+export const PLUGINS_PORTS = {
+  port1: {
+    id: 'port1',
+    type: 'input',
+    properties: {
+      custom: 'property',
+    },
+  },
+  port2: {
+    id: 'port2',
+    type: 'output',
+    properties: {
+      custom: 'property',
+    },
+  },
+};
 
-export default {
-  'navBar.lang': 'Languages',
-  'layout.user.link.help': 'Help',
-  'layout.user.link.privacy': 'Privacy',
-  'layout.user.link.terms': 'Terms',
-  'app.preview.down.block': 'Download this page to your local project',
-  ...globalHeader,
-  ...menu,
-  ...settingDrawer,
-  ...settings,
-  ...pwa,
-  ...component,
-  ...ActionBarEnUS,
+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
new file mode 100644
index 0000000..1d09f0b
--- /dev/null
+++ b/web/src/components/PluginOrchestration/customConfig.tsx
@@ -0,0 +1,71 @@
+/*
+ * 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 { 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
new file mode 100644
index 0000000..37607e6
--- /dev/null
+++ b/web/src/components/PluginOrchestration/index.tsx
@@ -0,0 +1,277 @@
+/*
+ * 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, 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 { PluginOrchestrationModule } from './typing';
+
+export * from './transform';
+
+export enum PanelType {
+  Plugin,
+  Condition,
+  Default,
+}
+
+type Props = {
+  data: any;
+  onChange(data: object): 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(() => {
+    // eslint-disable-next-line no-shadow
+    fetchList().then((data) => {
+      const categoryList: string[] = [];
+      data.forEach(item => {
+        if (!categoryList.includes(firstUpperCase(item.type))) {
+          categoryList.push(firstUpperCase(item.type));
+        }
+      });
+      setTypeList(['All', ...categoryList.sort()]);
+      setPluginList(data);
+      setShowList(data.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/Plugin/typing.d.ts 
b/web/src/components/PluginOrchestration/locales/en-US.ts
similarity index 57%
copy from web/src/components/Plugin/typing.d.ts
copy to web/src/components/PluginOrchestration/locales/en-US.ts
index 1d8b7c8..08ba06a 100644
--- a/web/src/components/Plugin/typing.d.ts
+++ b/web/src/components/PluginOrchestration/locales/en-US.ts
@@ -14,25 +14,17 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-declare namespace PluginComponent {
-  type Data = object;
+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',
 
-  type Schema = '' | 'route' | 'consumer' | 'service';
+  'page.siderBar.button.submit': 'Save',
+  'page.siderBar.plugin': 'Plugin',
+  'page.siderBar.tips': 'Drag the required components to the panel',
 
-  type Category =
-    | 'Security'
-    | 'Limit traffic'
-    | 'Log'
-    | 'Observability'
-    | 'Other'
-    | 'Authentication';
-
-  type Meta = {
-    name: string;
-    category: Category;
-    hidden?: boolean;
-    // Note: Plugins are sorted by priority under the same category in the 
frontend, the smaller the number, the higher the priority. The default value is 
9999.
-    priority?: number;
-    avatar?: React.ReactNode;
-  };
-}
+  '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/Plugin/typing.d.ts 
b/web/src/components/PluginOrchestration/locales/zh-CN.ts
similarity index 59%
copy from web/src/components/Plugin/typing.d.ts
copy to web/src/components/PluginOrchestration/locales/zh-CN.ts
index 1d8b7c8..1f1cbe3 100644
--- a/web/src/components/Plugin/typing.d.ts
+++ b/web/src/components/PluginOrchestration/locales/zh-CN.ts
@@ -14,25 +14,17 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-declare namespace PluginComponent {
-  type Data = object;
+export default {
+  'page.siderBar.form.label.panelType.condition': '判断条件',
+  'page.siderBar.form.rule.panelType.condition': '请输入判断条件',
+  'page.siderBar.form.label.panelType.plugin': '插件分类',
 
-  type Schema = '' | 'route' | 'consumer' | 'service';
+  'page.siderBar.button.submit': '保存',
+  'page.siderBar.plugin': '插件',
+  'page.siderBar.tips': '拖动所需组件至面板',
 
-  type Category =
-    | 'Security'
-    | 'Limit traffic'
-    | 'Log'
-    | 'Observability'
-    | 'Other'
-    | 'Authentication';
-
-  type Meta = {
-    name: string;
-    category: Category;
-    hidden?: boolean;
-    // Note: Plugins are sorted by priority under the same category in the 
frontend, the smaller the number, the higher the priority. The default value is 
9999.
-    priority?: number;
-    avatar?: React.ReactNode;
-  };
-}
+  'page.panel.condition.tips': '点击配置判断条件',
+  'page.panel.condition.name': '判断条件',
+  'page.panel.plugin.tips': '点击配置插件',
+  'page.panel.plugin.name': '插件名称',
+};
diff --git a/web/src/components/Plugin/typing.d.ts 
b/web/src/components/PluginOrchestration/service.ts
similarity index 59%
copy from web/src/components/Plugin/typing.d.ts
copy to web/src/components/PluginOrchestration/service.ts
index 1d8b7c8..e7c62ad 100644
--- a/web/src/components/Plugin/typing.d.ts
+++ b/web/src/components/PluginOrchestration/service.ts
@@ -14,25 +14,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-declare namespace PluginComponent {
-  type Data = object;
+import { request } from 'umi';
 
-  type Schema = '' | 'route' | 'consumer' | 'service';
+import { PluginOrchestrationModule } from './typing';
 
-  type Category =
-    | 'Security'
-    | 'Limit traffic'
-    | 'Log'
-    | 'Observability'
-    | 'Other'
-    | 'Authentication';
-
-  type Meta = {
-    name: string;
-    category: Category;
-    hidden?: boolean;
-    // Note: Plugins are sorted by priority under the same category in the 
frontend, the smaller the number, the higher the priority. The default value is 
9999.
-    priority?: number;
-    avatar?: React.ReactNode;
-  };
-}
+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
new file mode 100644
index 0000000..658d009
--- /dev/null
+++ b/web/src/components/PluginOrchestration/transform.ts
@@ -0,0 +1,122 @@
+/*
+ * 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/components/Plugin/typing.d.ts 
b/web/src/components/PluginOrchestration/typing.d.ts
similarity index 62%
copy from web/src/components/Plugin/typing.d.ts
copy to web/src/components/PluginOrchestration/typing.d.ts
index 1d8b7c8..c9e909a 100644
--- a/web/src/components/Plugin/typing.d.ts
+++ b/web/src/components/PluginOrchestration/typing.d.ts
@@ -14,25 +14,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-declare namespace PluginComponent {
-  type Data = object;
-
-  type Schema = '' | 'route' | 'consumer' | 'service';
-
-  type Category =
-    | 'Security'
-    | 'Limit traffic'
-    | 'Log'
-    | 'Observability'
-    | 'Other'
-    | 'Authentication';
-
+export declare namespace PluginOrchestrationModule {
   type Meta = {
     name: string;
-    category: Category;
-    hidden?: boolean;
-    // Note: Plugins are sorted by priority under the same category in the 
frontend, the smaller the number, the higher the priority. The default value is 
9999.
-    priority?: number;
-    avatar?: React.ReactNode;
+    priority: number;
+    schema: object;
+    type: string;
+    version: number;
+    consumer_schema?: object;
   };
 }
diff --git a/web/src/locales/en-US.ts b/web/src/locales/en-US.ts
index 6ea6069..04c515f 100644
--- a/web/src/locales/en-US.ts
+++ b/web/src/locales/en-US.ts
@@ -22,6 +22,7 @@ import menu from './en-US/menu';
 import pwa from './en-US/pwa';
 import settingDrawer from './en-US/settingDrawer';
 import settings from './en-US/setting';
+import PluginOrchestration from 
'../components/PluginOrchestration/locales/en-US';
 
 export default {
   'navBar.lang': 'Languages',
@@ -36,4 +37,5 @@ export default {
   ...pwa,
   ...component,
   ...ActionBarEnUS,
+  ...PluginOrchestration
 };
diff --git a/web/src/locales/zh-CN.ts b/web/src/locales/zh-CN.ts
index 3eb93eb..e04343a 100644
--- a/web/src/locales/zh-CN.ts
+++ b/web/src/locales/zh-CN.ts
@@ -22,6 +22,7 @@ import menu from './zh-CN/menu';
 import pwa from './zh-CN/pwa';
 import settingDrawer from './zh-CN/settingDrawer';
 import settings from './zh-CN/setting';
+import PluginOrchestration from 
'../components/PluginOrchestration/locales/zh-CN';
 
 export default {
   'navBar.lang': '语言',
@@ -36,4 +37,5 @@ export default {
   ...pwa,
   ...component,
   ...ActionBarZhCN,
+  ...PluginOrchestration
 };
diff --git a/web/src/pages/Route/Create.tsx b/web/src/pages/Route/Create.tsx
index 41cadae..8bd2d69 100644
--- a/web/src/pages/Route/Create.tsx
+++ b/web/src/pages/Route/Create.tsx
@@ -18,11 +18,11 @@ import React, { useState, useEffect, useRef } from 'react';
 import { Card, Steps, Form } from 'antd';
 import { PageHeaderWrapper } from '@ant-design/pro-layout';
 import { history, useIntl } from 'umi';
-import { transformer as chartTransformer } from '@api7-dashboard/pluginchart';
 
 import ActionBar from '@/components/ActionBar';
 import { DEFAULT_UPSTREAM } from '@/components/Upstream';
 
+import { transformer as chartTransformer } from 
'@/components/PluginOrchestration';
 import { create, fetchItem, update, checkUniqueName, checkHostWithSSL } from 
'./service';
 import Step1 from './components/Step1';
 import Step2 from './components/Step2';
@@ -256,11 +256,10 @@ const Page: React.FC<Props> = (props) => {
   return (
     <>
       <PageHeaderWrapper
-        title={`${
-          (props as any).match.params.rid
+        title={`${(props as any).match.params.rid
             ? formatMessage({ id: 'component.global.edit' })
             : formatMessage({ id: 'component.global.create' })
-        } ${formatMessage({ id: 'menu.routes' })}`}
+          } ${formatMessage({ id: 'menu.routes' })}`}
       >
         <Card bordered={false}>
           <Steps current={step - 1} className={styles.steps}>
diff --git a/web/src/pages/Route/components/CreateStep4/CreateStep4.tsx 
b/web/src/pages/Route/components/CreateStep4/CreateStep4.tsx
index 16ba096..c2b28c9 100644
--- a/web/src/pages/Route/components/CreateStep4/CreateStep4.tsx
+++ b/web/src/pages/Route/components/CreateStep4/CreateStep4.tsx
@@ -17,8 +17,8 @@
 import React from 'react';
 import { FormInstance } from 'antd/lib/form';
 import { useIntl } from 'umi';
-import PluginOrchestration from '@api7-dashboard/pluginchart';
 
+import PluginOrchestration from '@/components/PluginOrchestration';
 import PluginPage from '@/components/Plugin';
 import Step1 from '../Step1';
 import Step2 from '../Step2';
@@ -58,7 +58,7 @@ const CreateStep4: React.FC<Props> = ({ form1, form2, 
redirect, upstreamRef, ...
             <PluginPage initialData={rest.step3Data.plugins} readonly />
           )}
           {Boolean(Object.keys(script).length !== 0) && (
-            <PluginOrchestration data={rest.step3Data.script.chart} readonly 
onChange={() => {}} />
+            <PluginOrchestration data={rest.step3Data.script.chart} readonly 
onChange={() => { }} />
           )}
         </>
       )}
diff --git a/web/src/pages/Route/components/Step3/index.tsx 
b/web/src/pages/Route/components/Step3/index.tsx
index 3eb30b8..df9a3e3 100644
--- a/web/src/pages/Route/components/Step3/index.tsx
+++ b/web/src/pages/Route/components/Step3/index.tsx
@@ -19,7 +19,7 @@ import { Radio, Tooltip } from 'antd';
 import { QuestionCircleOutlined } from '@ant-design/icons';
 import { isChrome } from 'react-device-detect';
 
-import PluginOrchestration from '@api7-dashboard/pluginchart';
+import PluginOrchestration from '@/components/PluginOrchestration';
 import PluginPage from '@/components/Plugin';
 
 type Props = {
diff --git a/web/yarn.lock b/web/yarn.lock
index f5727a2..93ed598 100644
--- a/web/yarn.lock
+++ b/web/yarn.lock
@@ -187,32 +187,6 @@
     lodash "^4.17.15"
     resize-observer-polyfill "^1.5.0"
 
-"@api7-dashboard/plugin@^1.0.6":
-  version "1.0.6"
-  resolved 
"https://registry.yarnpkg.com/@api7-dashboard/plugin/-/plugin-1.0.6.tgz#d17bd53da9151c0fee2d5591965b642bc7e1005d";
-  integrity 
sha512-xqrhPc5/HVIf9jBr5SNfwyfTFvXWfUn9rt2mHepDUCFRlE3WrMhorv2u+Zsk33LcgxG2uNvJrFA1T9Zhfl2hNA==
-  dependencies:
-    "@rjsf/antd" "^2.3.0"
-    "@rjsf/core" "^2.3.0"
-    "@uiw/react-codemirror" "^3.0.1"
-    ajv "^6.12.5"
-    json-schema "^0.2.5"
-    set-value "^3.0.2"
-
-"@api7-dashboard/pluginchart@^1.0.14":
-  version "1.0.14"
-  resolved 
"https://registry.yarnpkg.com/@api7-dashboard/pluginchart/-/pluginchart-1.0.14.tgz#3147b7e89cdcb541ed1559970b77cc443a3ef15d";
-  integrity 
sha512-LD5uhQgDwmsWkK7PfMl1XZulIU7klpFvBDHTrXIvJ23xwSPnT6tYlNS256O1QyVMuQaKRRH/lqbtPPop+Sgg1w==
-  dependencies:
-    "@ant-design/icons" "^4.2.2"
-    "@api7-dashboard/plugin" "^1.0.6"
-    "@mrblenny/react-flow-chart" "^0.0.14"
-    "@rjsf/antd" "^2.3.0"
-    "@rjsf/core" "^2.3.0"
-    json-schema "^0.2.5"
-    lodash "^4.17.20"
-    styled-components "^5.1.1"
-
 "@api7-dashboard/ui@^1.0.3":
   version "1.0.3"
   resolved 
"https://registry.yarnpkg.com/@api7-dashboard/ui/-/ui-1.0.3.tgz#77011750bebee7bb6f6966ea0596c5576951e3ff";
@@ -1920,8 +1894,8 @@
 
 "@mrblenny/react-flow-chart@^0.0.14":
   version "0.0.14"
-  resolved 
"https://registry.npm.taobao.org/@mrblenny/react-flow-chart/download/@mrblenny/react-flow-chart-0.0.14.tgz#be11d06345c7222b41f488b38011b109e48a04b3";
-  integrity sha1-vhHQY0XHIitB9IizgBGxCeSKBLM=
+  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"
@@ -1988,11 +1962,6 @@
   resolved 
"https://registry.npm.taobao.org/@rjsf/antd/download/@rjsf/antd-2.2.0.tgz#0a92d67c877bf080008c187ea714de640e58425d";
   integrity sha1-CpLWfId78IAAjBh+pxTeZA5YQl0=
 
-"@rjsf/antd@^2.3.0":
-  version "2.3.0"
-  resolved 
"https://registry.npm.taobao.org/@rjsf/antd/download/@rjsf/antd-2.3.0.tgz#c6df9ce0a5fd2cffe14d5ebcedafdaade4674272";
-  integrity sha1-xt+c4KX9LP/hTV687a/areRnQnI=
-
 "@rjsf/[email protected]":
   version "2.2.0"
   resolved 
"https://registry.npm.taobao.org/@rjsf/core/download/@rjsf/core-2.2.0.tgz#bedb20c51984769c0afe5c1932414a7f65f7cf31";
@@ -2010,23 +1979,6 @@
     react-is "^16.9.0"
     shortid "^2.2.14"
 
-"@rjsf/core@^2.3.0":
-  version "2.3.0"
-  resolved 
"https://registry.npm.taobao.org/@rjsf/core/download/@rjsf/core-2.3.0.tgz#334c73d2262ef1a8cda477e238067af7336c5599";
-  integrity sha1-M0xz0iYu8ajNpHfiOAZ69zNsVZk=
-  dependencies:
-    "@babel/runtime-corejs2" "^7.8.7"
-    "@types/json-schema" "^7.0.4"
-    ajv "^6.7.0"
-    core-js "^2.5.7"
-    json-schema-merge-allof "^0.6.0"
-    jsonpointer "^4.0.1"
-    lodash "^4.17.15"
-    prop-types "^15.7.2"
-    react-app-polyfill "^1.0.4"
-    react-is "^16.9.0"
-    shortid "^2.2.14"
-
 "@samverschueren/stream-to-observable@^0.3.0":
   version "0.3.1"
   resolved 
"https://registry.yarnpkg.com/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.1.tgz#a21117b19ee9be70c379ec1877537ef2e1c63301";
@@ -2364,7 +2316,7 @@
   resolved 
"https://registry.npm.taobao.org/@types/history/download/@types/history-4.7.7.tgz#613957d900fab9ff84c8dfb24fa3eef0c2a40896";
   integrity sha1-YTlX2QD6uf+EyN+yT6Pu8MKkCJY=
 
-"@types/hoist-non-react-statics@^3.3.0", 
"@types/hoist-non-react-statics@^3.3.1":
+"@types/hoist-non-react-statics@*", "@types/hoist-non-react-statics@^3.3.0", 
"@types/hoist-non-react-statics@^3.3.1":
   version "3.3.1"
   resolved 
"https://registry.npm.taobao.org/@types/hoist-non-react-statics/download/@types/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f";
   integrity sha1-ESSq/lEYy1kZd66xzqrtEHDrA58=
@@ -2728,6 +2680,15 @@
   resolved 
"https://registry.npm.taobao.org/@types/stack-utils/download/@types/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e";
   integrity sha1-CoUdO9lkmPolwzq3J47TvWXwbD4=
 
+"@types/styled-components@^5.1.7":
+  version "5.1.7"
+  resolved 
"https://registry.yarnpkg.com/@types/styled-components/-/styled-components-5.1.7.tgz#3cd10b088c1cb1acde2e4b166b3e8275a3083710";
+  integrity 
sha512-BJzPhFygYspyefAGFZTZ/8lCEY4Tk+Iqktvnko3xmJf9LrLqs3+grxPeU3O0zLl6yjbYBopD0/VikbHgXDbJtA==
+  dependencies:
+    "@types/hoist-non-react-statics" "*"
+    "@types/react" "*"
+    csstype "^3.0.2"
+
 "@types/tapable@*", "@types/[email protected]":
   version "1.0.6"
   resolved 
"https://registry.npm.taobao.org/@types/tapable/download/@types/tapable-1.0.6.tgz#a9ca4b70a18b270ccb2bc0aaafefd1d486b7ea74";
@@ -3887,16 +3848,6 @@ ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.2, 
ajv@^6.12.3, ajv@^6.7.0, ajv@
     json-schema-traverse "^0.4.1"
     uri-js "^4.2.2"
 
-ajv@^6.12.5:
-  version "6.12.5"
-  resolved 
"https://registry.yarnpkg.com/ajv/-/ajv-6.12.5.tgz#19b0e8bae8f476e5ba666300387775fb1a00a4da";
-  integrity 
sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag==
-  dependencies:
-    fast-deep-equal "^3.1.1"
-    fast-json-stable-stringify "^2.0.0"
-    json-schema-traverse "^0.4.1"
-    uri-js "^4.2.2"
-
 ajv@^7.0.0-rc.2:
   version "7.0.0-rc.2"
   resolved 
"https://registry.yarnpkg.com/ajv/-/ajv-7.0.0-rc.2.tgz#9c237b95072c1ee8c38e2df76422f37bacc9ae5e";
@@ -10504,11 +10455,6 @@ [email protected]:
   resolved 
"https://registry.npm.taobao.org/json-schema/download/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13";
   integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=
 
-json-schema@^0.2.5:
-  version "0.2.5"
-  resolved 
"https://registry.npm.taobao.org/json-schema/download/json-schema-0.2.5.tgz#97997f50972dd0500214e208c407efa4b5d7063b";
-  integrity sha1-l5l/UJct0FACFOIIxAfvpLXXBjs=
-
 json-stable-stringify-without-jsonify@^1.0.1:
   version "1.0.1"
   resolved 
"https://registry.npm.taobao.org/json-stable-stringify-without-jsonify/download/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651";
@@ -15729,7 +15675,7 @@ set-blocking@^2.0.0:
   resolved 
"https://registry.npm.taobao.org/set-blocking/download/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7";
   integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
 
[email protected], set-value@^3.0.2:
[email protected]:
   version "3.0.2"
   resolved 
"https://registry.npm.taobao.org/set-value/download/set-value-3.0.2.tgz#74e8ecd023c33d0f77199d415409a40f21e61b90";
   integrity sha1-dOjs0CPDPQ93GZ1BVAmkDyHmG5A=
@@ -16576,10 +16522,10 @@ styled-components@^4.4.0:
     stylis-rule-sheet "^0.0.10"
     supports-color "^5.5.0"
 
-styled-components@^5.1.1:
-  version "5.1.1"
-  resolved 
"https://registry.npm.taobao.org/styled-components/download/styled-components-5.1.1.tgz#96dfb02a8025794960863b9e8e365e3b6be5518d";
-  integrity sha1-lt+wKoAleUlghjuejjZeO2vlUY0=
+styled-components@^5.2.1:
+  version "5.2.1"
+  resolved 
"https://registry.yarnpkg.com/styled-components/-/styled-components-5.2.1.tgz#6ed7fad2dc233825f64c719ffbdedd84ad79101a";
+  integrity 
sha512-sBdgLWrCFTKtmZm/9x7jkIabjFNVzCUeKfoQsM6R3saImkUnjx0QYdLwJHBjY9ifEcmjDamJDVfknWm1yxZPxQ==
   dependencies:
     "@babel/helper-module-imports" "^7.0.0"
     "@babel/traverse" "^7.4.5"

Reply via email to