You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by ju...@apache.org on 2020/11/28 01:04:48 UTC

[apisix-dashboard] branch juzhiyuan/feat-plugin created (now 09ad0c2)

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

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


      at 09ad0c2  feat(plugin): added code mirror

This branch includes the following new commits:

     new 09ad0c2  feat(plugin): added code mirror

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



[apisix-dashboard] 01/01: feat(plugin): added code mirror

Posted by ju...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 09ad0c28527739231da8e9ca284681ead162358e
Author: juzhiyuan <ju...@apache.org>
AuthorDate: Sat Nov 28 09:04:30 2020 +0800

    feat(plugin): added code mirror
---
 web/package.json                               |   1 +
 web/src/components/Plugin/CodeMirrorDrawer.tsx |  83 +++++++++++
 web/src/components/Plugin/IconFont.tsx         |  24 ++++
 web/src/components/Plugin/PluginPage.tsx       | 189 +++++++++++++++++++++++++
 web/src/components/Plugin/data.tsx             | 169 ++++++++++++++++++++++
 web/src/components/Plugin/index.ts             |  17 +++
 web/src/components/Plugin/service.ts           |  98 +++++++++++++
 web/src/components/Plugin/typing.d.ts          |  39 +++++
 web/src/pages/Route/Create.tsx                 |   7 +-
 web/src/pages/Route/components/Step3/index.tsx |  69 +++++----
 10 files changed, 658 insertions(+), 38 deletions(-)

diff --git a/web/package.json b/web/package.json
index da5799d..05ce18f 100644
--- a/web/package.json
+++ b/web/package.json
@@ -60,6 +60,7 @@
     "@api7-dashboard/ui": "^1.0.3",
     "@rjsf/antd": "2.2.0",
     "@rjsf/core": "2.2.0",
+    "@uiw/react-codemirror": "^3.0.1",
     "antd": "^4.4.0",
     "classnames": "^2.2.6",
     "dayjs": "1.8.28",
diff --git a/web/src/components/Plugin/CodeMirrorDrawer.tsx b/web/src/components/Plugin/CodeMirrorDrawer.tsx
new file mode 100644
index 0000000..b382d85
--- /dev/null
+++ b/web/src/components/Plugin/CodeMirrorDrawer.tsx
@@ -0,0 +1,83 @@
+/*
+ * 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, { useRef } from 'react';
+import { Drawer, Button, notification } from 'antd';
+import CodeMirror from '@uiw/react-codemirror';
+
+type Props = {
+  visible?: boolean;
+  data?: object;
+  readonly?: boolean;
+  onClose?: () => void;
+  onSubmit?: (data: object) => void;
+};
+
+const CodeMirrorDrawer: React.FC<Props> = ({
+  visible = false,
+  readonly = false,
+  data = {},
+  onClose,
+  onSubmit,
+}) => {
+  const ref = useRef<any>(null);
+  return (
+    <Drawer
+      visible={visible}
+      width={500}
+      onClose={onClose}
+      footer={
+        !readonly && (
+          <div style={{ display: 'flex', justifyContent: 'space-between' }}>
+            <Button onClick={onClose}>Cancel</Button>
+            <Button
+              type="primary"
+              style={{ marginRight: 8, marginLeft: 8 }}
+              onClick={() => {
+                try {
+                  if (onSubmit) {
+                    onSubmit(JSON.parse(ref.current?.editor.getValue()));
+                  }
+                } catch (error) {
+                  notification.error({
+                    message: 'Invalid JSON data',
+                  });
+                }
+              }}
+            >
+              Submit
+            </Button>
+          </div>
+        )
+      }
+    >
+      <CodeMirror
+        ref={ref}
+        value={JSON.stringify(data, null, 2)}
+        options={{
+          mode: 'json-ld',
+          readOnly: readonly ? 'nocursor' : '',
+          lineWrapping: true,
+          lineNumbers: true,
+          showCursorWhenSelecting: true,
+          autofocus: true,
+        }}
+      />
+    </Drawer>
+  );
+};
+
+export default CodeMirrorDrawer;
diff --git a/web/src/components/Plugin/IconFont.tsx b/web/src/components/Plugin/IconFont.tsx
new file mode 100644
index 0000000..4889720
--- /dev/null
+++ b/web/src/components/Plugin/IconFont.tsx
@@ -0,0 +1,24 @@
+/*
+ * 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 { createFromIconfontCN } from '@ant-design/icons';
+
+// NOTE: Icons from AliCDN https://www.iconfont.cn/manage/index
+const IconFont = createFromIconfontCN({
+  scriptUrl: '//at.alicdn.com/t/font_2088089_a3klmsocd15.js',
+});
+
+export default IconFont;
diff --git a/web/src/components/Plugin/PluginPage.tsx b/web/src/components/Plugin/PluginPage.tsx
new file mode 100644
index 0000000..d5bfabd
--- /dev/null
+++ b/web/src/components/Plugin/PluginPage.tsx
@@ -0,0 +1,189 @@
+/*
+ * 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 { Anchor, Layout, Switch, Card, Tooltip, Button, notification, Avatar } from 'antd';
+import { SettingFilled } from '@ant-design/icons';
+import { PanelSection } from '@api7-dashboard/ui';
+import { validate } from 'json-schema';
+
+import { fetchSchema, getList } from './service';
+import { PLUGIN_MAPPER_SOURCE } from './data';
+import CodeMirrorDrawer from './CodeMirrorDrawer';
+
+type Props = {
+  readonly?: boolean;
+  initialData?: PluginComponent.Data;
+  schemaType?: PluginComponent.Schema;
+  onChange?: (data: PluginComponent.Data) => void;
+};
+
+const PanelSectionStyle = {
+  display: 'grid',
+  gridTemplateColumns: 'repeat(3, 33.333333%)',
+  gridRowGap: 15,
+  gridColumnGap: 10,
+  width: 'calc(100% - 20px)',
+};
+
+const { Sider, Content } = Layout;
+
+// NOTE: use this flag as plugin's name to hide drawer
+const NEVER_EXIST_PLUGIN_FLAG = 'NEVER_EXIST_PLUGIN_FLAG';
+
+const PluginPage: React.FC<Props> = ({
+  readonly = false,
+  initialData = {},
+  schemaType = '',
+  onChange = () => {},
+}) => {
+  const [pluginList, setPlugin] = useState<PluginComponent.Meta[][]>([]);
+  const [name, setName] = useState<string>(NEVER_EXIST_PLUGIN_FLAG);
+
+  useEffect(() => {
+    getList().then(setPlugin);
+  }, []);
+
+  return (
+    <>
+      <style>{`
+        .ant-avatar > img {
+          object-fit: contain;
+        }
+        .ant-avatar {
+          background-color: transparent;
+        }
+        .ant-avatar.ant-avatar-icon {
+          font-size: 32px;
+        }
+      `}</style>
+      <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}
+                />
+              );
+            })}
+          </Anchor>
+        </Sider>
+        <Content style={{ padding: '0 10px', backgroundColor: '#fff', minHeight: 1400 }}>
+          {pluginList.map((plugins) => {
+            const { category } = plugins[0];
+            return (
+              <PanelSection
+                title={category}
+                key={category}
+                style={PanelSectionStyle}
+                id={`plugin-category-${category}`}
+              >
+                {plugins.map((item) => (
+                  <Card
+                    key={item.name}
+                    title={[
+                      item.avatar && (
+                        <Avatar
+                          icon={item.avatar}
+                          className="plugin-avatar"
+                          style={{
+                            marginRight: 5,
+                          }}
+                        />
+                      ),
+                      <a
+                        href={`https://github.com/apache/apisix/blob/master/doc/plugins/${item.name}.md`}
+                        style={{ color: 'inherit' }}
+                        target="_blank"
+                        rel="noreferrer"
+                      >
+                        {item.name}
+                      </a>,
+                    ]}
+                    style={{ height: 66 }}
+                    extra={[
+                      <Tooltip title="Setting" key={`plugin-card-${item.name}-extra-tooltip-2`}>
+                        <Button
+                          disabled={PLUGIN_MAPPER_SOURCE[item.name]?.noConfiguration}
+                          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) {
+                            setName(item.name);
+                            onChange({
+                              ...initialData,
+                              [item.name]: { ...initialData[item.name], disable: false },
+                            });
+                          } else {
+                            onChange({
+                              ...initialData,
+                              [item.name]: { ...initialData[item.name], disable: true },
+                            });
+                          }
+                        }}
+                        key={Math.random().toString(36).substring(7)}
+                      />,
+                    ]}
+                  />
+                ))}
+              </PanelSection>
+            );
+          })}
+        </Content>
+      </Layout>
+      <CodeMirrorDrawer
+        visible={name !== NEVER_EXIST_PLUGIN_FLAG}
+        data={initialData[name]}
+        readonly={readonly}
+        onClose={() => {
+          setName(NEVER_EXIST_PLUGIN_FLAG);
+        }}
+        onSubmit={(value) => {
+          fetchSchema(name, schemaType).then((schema) => {
+            const { valid, errors } = validate(value, schema);
+            if (valid) {
+              onChange({ ...initialData, [name]: { ...value, disable: false } });
+              setName(NEVER_EXIST_PLUGIN_FLAG);
+              return;
+            }
+            errors?.forEach((item) => {
+              notification.error({
+                message: 'Invalid plugin data',
+                description: item.message,
+              });
+            });
+          });
+        }}
+      />
+    </>
+  );
+};
+
+export default PluginPage;
diff --git a/web/src/components/Plugin/data.tsx b/web/src/components/Plugin/data.tsx
new file mode 100644
index 0000000..29762a0
--- /dev/null
+++ b/web/src/components/Plugin/data.tsx
@@ -0,0 +1,169 @@
+/*
+ * 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 IconFont from './IconFont';
+
+export const PLUGIN_MAPPER_SOURCE: Record<string, Omit<PluginComponent.Meta, 'name'>> = {
+  'limit-req': {
+    category: 'Limit traffic',
+    priority: 1,
+  },
+  'limit-count': {
+    category: 'Limit traffic',
+    priority: 2,
+  },
+  'limit-conn': {
+    category: 'Limit traffic',
+    priority: 3,
+  },
+  prometheus: {
+    category: 'Observability',
+    noConfiguration: true,
+    priority: 1,
+    avatar: <IconFont type="iconPrometheus_software_logo" />,
+  },
+  skywalking: {
+    category: 'Observability',
+    priority: 2,
+    avatar: <IconFont type="iconskywalking" />,
+  },
+  zipkin: {
+    category: 'Observability',
+    priority: 3,
+  },
+  'request-id': {
+    category: 'Observability',
+    priority: 4,
+  },
+  'key-auth': {
+    category: 'Authentication',
+    priority: 1,
+  },
+  'basic-auth': {
+    category: 'Authentication',
+    priority: 3,
+  },
+  'node-status': {
+    category: 'Other',
+    noConfiguration: true,
+  },
+  'jwt-auth': {
+    category: 'Authentication',
+    priority: 2,
+    avatar: <IconFont type="iconjwt-3" />,
+  },
+  'authz-keycloak': {
+    category: 'Authentication',
+    priority: 5,
+    avatar: <IconFont type="iconkeycloak_icon_32px" />,
+  },
+  'ip-restriction': {
+    category: 'Security',
+    priority: 1,
+  },
+  'grpc-transcode': {
+    category: 'Other',
+  },
+  'serverless-pre-function': {
+    category: 'Other',
+  },
+  'serverless-post-function': {
+    category: 'Other',
+  },
+  'openid-connect': {
+    category: 'Authentication',
+    priority: 4,
+    avatar: <IconFont type="iconicons8-openid" />,
+  },
+  'proxy-rewrite': {
+    category: 'Other',
+  },
+  redirect: {
+    category: 'Other',
+    hidden: true,
+  },
+  'response-rewrite': {
+    category: 'Other',
+  },
+  'fault-injection': {
+    category: 'Security',
+    priority: 4,
+  },
+  'udp-logger': {
+    category: 'Log',
+    priority: 4,
+  },
+  'wolf-rbac': {
+    category: 'Other',
+  },
+  'proxy-cache': {
+    category: 'Other',
+    priority: 1,
+  },
+  'tcp-logger': {
+    category: 'Log',
+    priority: 3,
+  },
+  'proxy-mirror': {
+    category: 'Other',
+    priority: 2,
+  },
+  'kafka-logger': {
+    category: 'Log',
+    priority: 1,
+    avatar: <IconFont type="iconApache_kafka" />,
+  },
+  cors: {
+    category: 'Security',
+    priority: 2,
+  },
+  'uri-blocker': {
+    category: 'Security',
+    priority: 3,
+  },
+  'request-validator': {
+    category: 'Security',
+    priority: 5,
+  },
+  heartbeat: {
+    category: 'Other',
+    hidden: true,
+  },
+  'batch-requests': {
+    category: 'Other',
+    noConfiguration: true,
+  },
+  'http-logger': {
+    category: 'Log',
+    priority: 2,
+  },
+  'mqtt-proxy': {
+    category: 'Other',
+  },
+  oauth: {
+    category: 'Security',
+  },
+  syslog: {
+    category: 'Log',
+    priority: 5,
+  },
+  echo: {
+    category: 'Other',
+    priority: 3,
+  },
+};
diff --git a/web/src/components/Plugin/index.ts b/web/src/components/Plugin/index.ts
new file mode 100644
index 0000000..21da022
--- /dev/null
+++ b/web/src/components/Plugin/index.ts
@@ -0,0 +1,17 @@
+/*
+ * 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 } from './PluginPage';
diff --git a/web/src/components/Plugin/service.ts b/web/src/components/Plugin/service.ts
new file mode 100644
index 0000000..01f171b
--- /dev/null
+++ b/web/src/components/Plugin/service.ts
@@ -0,0 +1,98 @@
+/*
+ * 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 { JSONSchema7 } from 'json-schema';
+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);
+      });
+    });
+};
+
+/**
+ * cache pulgin schema by schemaType
+ * default schema is route for plugins in route
+ * support schema: consumer for plugins in consumer
+ */
+const cachedPluginSchema: Record<string, object> = {
+  route: {},
+  consumer: {},
+};
+export const fetchSchema = async (
+  name: string,
+  schemaType: PluginComponent.Schema,
+): Promise<JSONSchema7> => {
+  if (!cachedPluginSchema[schemaType][name]) {
+    const queryString = schemaType !== 'route' ? `?schema_type=${schemaType}` : '';
+    cachedPluginSchema[schemaType][name] = (
+      await request(`/schema/plugins/${name}${queryString}`)
+    ).data;
+    // for plugins schema returned with properties: [], which will cause parse error
+    if (JSON.stringify(cachedPluginSchema[schemaType][name].properties) === '[]') {
+      cachedPluginSchema[schemaType][name] = omit(
+        cachedPluginSchema[schemaType][name],
+        'properties',
+      );
+    }
+  }
+  return cachedPluginSchema[schemaType][name];
+};
diff --git a/web/src/components/Plugin/typing.d.ts b/web/src/components/Plugin/typing.d.ts
new file mode 100644
index 0000000..c9233ea
--- /dev/null
+++ b/web/src/components/Plugin/typing.d.ts
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+declare namespace PluginComponent {
+  type Data = object;
+
+  type Schema = '' | 'route' | 'consumer';
+
+  type Category =
+    | 'Security'
+    | 'Limit traffic'
+    | 'Log'
+    | 'Observability'
+    | 'Other'
+    | 'Authentication';
+
+  type Meta = {
+    name: string;
+    category: Category;
+    hidden?: boolean;
+    noConfiguration?: 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;
+  };
+}
diff --git a/web/src/pages/Route/Create.tsx b/web/src/pages/Route/Create.tsx
index 396008c..379e83c 100644
--- a/web/src/pages/Route/Create.tsx
+++ b/web/src/pages/Route/Create.tsx
@@ -65,7 +65,7 @@ const Page: React.FC<Props> = (props) => {
   const [form2] = Form.useForm();
   const upstreamRef = useRef<any>();
 
-  const [step, setStep] = useState(1);
+  const [step, setStep] = useState(3);
   const [stepHeader, setStepHeader] = useState(STEP_HEADER_4);
   const [chart, setChart] = useState(INIT_CHART);
 
@@ -252,10 +252,11 @@ 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/Step3/index.tsx b/web/src/pages/Route/components/Step3/index.tsx
index fcc6b01..324b869 100644
--- a/web/src/pages/Route/components/Step3/index.tsx
+++ b/web/src/pages/Route/components/Step3/index.tsx
@@ -19,17 +19,18 @@ import { Radio, Tooltip } from 'antd';
 import { QuestionCircleOutlined } from '@ant-design/icons';
 import { isChrome } from 'react-device-detect';
 
-import { PluginPage, PluginPageType } from '@api7-dashboard/plugin';
+// import { PluginPage, PluginPageType } from '@api7-dashboard/plugin';
 import PluginOrchestration from '@api7-dashboard/pluginchart';
+import PluginPage from '@/components/Plugin';
 
 type Props = {
   data: {
-    plugins: PluginPageType.FinalData;
+    plugins: PluginComponent.Data;
     script: Record<string, any>;
   };
-  onChange(data: { plugins: PluginPageType.FinalData; script: any }): void;
+  onChange(data: { plugins: PluginComponent.Data; script: any }): void;
   readonly?: boolean;
-  isForceHttps: boolean
+  isForceHttps: boolean;
 };
 
 type Mode = 'NORMAL' | 'DRAW';
@@ -61,42 +62,40 @@ const Page: React.FC<Props> = ({ data, onChange, readonly = false, isForceHttps
         </Radio.Group>
         {Boolean(disableDraw) && (
           <div style={{ marginLeft: '10px' }}>
-            <Tooltip placement="right" title={() => {
-              // NOTE: forceHttps do not support DRAW mode
-              // TODO: i18n
-              const titleArr: string[] = [];
-              if (!isChrome) {
-                titleArr.push('插件编排仅支持 Chrome 浏览器。');
-              }
-              if (isForceHttps) {
-                titleArr.push('当步骤一中 重定向 选择为 启用 HTTPS 时,不可使用插件编排模式。');
-              }
-              return (
-                titleArr.map((item, index) => `${index + 1}.${item}`).join("")
-              )
-            }}>
+            <Tooltip
+              placement="right"
+              title={() => {
+                // NOTE: forceHttps do not support DRAW mode
+                // TODO: i18n
+                const titleArr: string[] = [];
+                if (!isChrome) {
+                  titleArr.push('插件编排仅支持 Chrome 浏览器。');
+                }
+                if (isForceHttps) {
+                  titleArr.push('当步骤一中 重定向 选择为 启用 HTTPS 时,不可使用插件编排模式。');
+                }
+                return titleArr.map((item, index) => `${index + 1}.${item}`).join('');
+              }}
+            >
               <QuestionCircleOutlined />
             </Tooltip>
           </div>
         )}
       </div>
-      {
-        Boolean(mode === 'NORMAL') && (
-          <PluginPage
-            initialData={plugins}
-            onChange={(pluginsData) => onChange({ plugins: pluginsData, script: {} })}
-          />
-        )
-      }
-      {
-        Boolean(mode === 'DRAW') && (
-          <PluginOrchestration
-            data={script?.chart}
-            onChange={(scriptData) => onChange({ plugins: {}, script: scriptData })}
-            readonly={readonly}
-          />
-        )
-      }
+      {Boolean(mode === 'NORMAL') && (
+        <PluginPage
+          initialData={plugins}
+          schemaType="route"
+          onChange={(pluginsData) => onChange({ plugins: pluginsData, script: {} })}
+        />
+      )}
+      {Boolean(mode === 'DRAW') && (
+        <PluginOrchestration
+          data={script?.chart}
+          onChange={(scriptData) => onChange({ plugins: {}, script: scriptData })}
+          readonly={readonly}
+        />
+      )}
     </>
   );
 };