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:49 UTC

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

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}
+        />
+      )}
     </>
   );
 };