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

bzp2010 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 283b308b feat: support data loader in frontend (#2480)
283b308b is described below

commit 283b308b96f2e22c3264c1c55d071fe8979e171d
Author: Zeping Bai <[email protected]>
AuthorDate: Fri Jun 24 16:41:47 2022 +0800

    feat: support data loader in frontend (#2480)
---
 .../integration/route/data-loader-import.spec.js   | 184 +++++++++++++++
 web/src/locales/en-US/menu.ts                      |   1 +
 web/src/locales/tr-TR/menu.ts                      |   1 +
 web/src/locales/zh-CN/menu.ts                      |   1 +
 web/src/pages/Route/List.tsx                       |  79 +------
 .../pages/Route/components/DataLoader/Import.tsx   | 262 +++++++++++++++++++++
 .../components/DataLoader/loader/OpenAPI3.tsx      |  40 ++++
 web/src/pages/Route/locales/en-US.ts               |  14 ++
 web/src/pages/Route/locales/tr-TR.ts               |  41 +++-
 web/src/pages/Route/locales/zh-CN.ts               |  14 ++
 10 files changed, 557 insertions(+), 80 deletions(-)

diff --git a/web/cypress/integration/route/data-loader-import.spec.js 
b/web/cypress/integration/route/data-loader-import.spec.js
new file mode 100644
index 00000000..124daf4d
--- /dev/null
+++ b/web/cypress/integration/route/data-loader-import.spec.js
@@ -0,0 +1,184 @@
+/*
+ * 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.
+ */
+
+context('Data Loader import', () => {
+  const selector = {
+    drawer: '.ant-drawer-content',
+    selectDropdown: '.ant-select-dropdown',
+    listTbody: '.ant-table-tbody',
+    listRow: 'tr.ant-table-row-level-0',
+    refresh: '.anticon-reload',
+    notification: '.ant-notification-notice-message',
+    notificationCloseIcon: '.ant-notification-notice-close',
+    fileSelector: '[type=file]',
+    notificationDesc: '.ant-notification-notice-description',
+    task_name: '#task_name',
+    merge_method: '#merge_method',
+  };
+  const data = {
+    importRouteSuccess: 'Import Successfully',
+    deleteRouteSuccess: 'Delete Route Successfully',
+    deleteUpstreamSuccess: 'Delete Upstream Successfully',
+  };
+  const cases = {
+    API101: '../../../api/test/testdata/import/Postman-API101.yaml',
+  };
+
+  beforeEach(() => {
+    cy.login();
+
+    cy.fixture('selector.json').as('domSelector');
+    cy.fixture('data.json').as('data');
+    cy.fixture('export-route-dataset.json').as('exportFile');
+  });
+
+  it('should import API101 with merge mode', () => {
+    cy.visit('/');
+    cy.contains('Route').click();
+
+    cy.get(selector.refresh).click();
+    cy.contains('Advanced').click();
+    cy.contains('Import').should('be.visible').click();
+
+    // select Data Loader type
+    cy.contains('OpenAPI 3').should('be.visible').click();
+    cy.get(selector.selectDropdown).contains('OpenAPI 
3').should('be.visible').click();
+    cy.get(selector.drawer).get(selector.task_name).type('api101_mm');
+    cy.get(selector.fileSelector).attachFile(cases.API101);
+    cy.get(selector.drawer).contains('Submit').click();
+    cy.get(selector.drawer).contains('Import 
Successfully').should('be.visible');
+    cy.get(selector.drawer).contains('Total 3 route imported, 0 
failed').click();
+    cy.get(selector.drawer).contains('Close').click();
+    cy.get(selector.drawer).should('not.exist');
+
+    // check result
+    cy.get(selector.listTbody).get(selector.listRow).should('have.length', 3);
+    cy.contains('api101_mm_customer').should('be.visible');
+    cy.contains('api101_mm_customer/{customer_id}').should('be.visible');
+    cy.contains('api101_mm_customers').should('be.visible');
+
+    // remove route
+    for (let i = 0; i < 3; i += 1) {
+      
cy.get(selector.listTbody).get(selector.listRow).contains('More').click();
+      cy.contains('Delete').should('be.visible').click();
+      cy.contains('OK').should('be.visible').click();
+      cy.get(selector.notification).should('contain', data.deleteRouteSuccess);
+      cy.get(selector.notificationCloseIcon).click();
+    }
+  });
+
+  it('should import API101 with duplicate upstream', () => {
+    cy.visit('/');
+    cy.contains('Route').click();
+
+    cy.get(selector.refresh).click();
+    cy.contains('Advanced').click();
+    cy.contains('Import').should('be.visible').click();
+
+    // select Data Loader type
+    cy.contains('OpenAPI 3').should('be.visible').click();
+    cy.get(selector.selectDropdown).contains('OpenAPI 
3').should('be.visible').click();
+    cy.get(selector.drawer).get(selector.task_name).type('api101_mm');
+    cy.get(selector.fileSelector).attachFile(cases.API101);
+    cy.get(selector.drawer).contains('Submit').click();
+    cy.get(selector.drawer).contains('Import Failed').should('be.visible');
+    cy.get(selector.drawer).contains('Total 1 upstream imported, 1 
failed').click();
+    cy.get(selector.drawer).contains('key: api101_mm is 
conflicted').should('be.visible');
+    cy.get(selector.drawer).contains('Close').click();
+    cy.get(selector.drawer).should('not.exist');
+
+    // remove route
+    for (let i = 0; i < 3; i += 1) {
+      
cy.get(selector.listTbody).get(selector.listRow).contains('More').click();
+      cy.contains('Delete').should('be.visible').click();
+      cy.contains('OK').should('be.visible').click();
+      cy.get(selector.notification).should('contain', data.deleteRouteSuccess);
+      cy.get(selector.notificationCloseIcon).click();
+    }
+  });
+
+  it('should import API101 with non-merge mode', () => {
+    cy.visit('/');
+    cy.contains('Route').click();
+
+    cy.get(selector.refresh).click();
+    cy.contains('Advanced').click();
+    cy.contains('Import').should('be.visible').click();
+
+    // select Data Loader type
+    cy.contains('OpenAPI 3').should('be.visible').click();
+    cy.get(selector.selectDropdown).contains('OpenAPI 
3').should('be.visible').click();
+    cy.get(selector.drawer).get(selector.task_name).type('api101_nmm');
+    cy.get(selector.drawer).get(selector.merge_method).click();
+    cy.get(selector.fileSelector).attachFile(cases.API101);
+    cy.get(selector.drawer).contains('Submit').click();
+    cy.get(selector.drawer).contains('Import 
Successfully').should('be.visible');
+    cy.get(selector.drawer).contains('Total 5 route imported, 0 
failed').click();
+    cy.get(selector.drawer).contains('Close').click();
+    cy.get(selector.drawer).should('not.exist');
+
+    // check result
+    cy.get(selector.listTbody).get(selector.listRow).should('have.length', 5);
+  });
+
+  it('should import API101 with duplicate route', () => {
+    cy.visit('/');
+    cy.contains('Route').click();
+
+    cy.get(selector.refresh).click();
+    cy.contains('Advanced').click();
+    cy.contains('Import').should('be.visible').click();
+
+    // select Data Loader type
+    cy.contains('OpenAPI 3').should('be.visible').click();
+    cy.get(selector.selectDropdown).contains('OpenAPI 
3').should('be.visible').click();
+    cy.get(selector.drawer).get(selector.task_name).type('api101_nmm');
+    cy.get(selector.drawer).get(selector.merge_method).click();
+    cy.get(selector.fileSelector).attachFile(cases.API101);
+    cy.get(selector.drawer).contains('Submit').click();
+    cy.get(selector.drawer).contains('Import Failed').should('be.visible');
+    cy.get(selector.drawer).contains('Total 5 route imported, 1 
failed').click();
+    cy.get(selector.drawer).contains('is duplicated with route 
api101_nmm_').should('be.visible');
+    cy.get(selector.drawer).contains('Close').click();
+    cy.get(selector.drawer).should('not.exist');
+  });
+
+  it('should remove all routes and upstreams', function () {
+    cy.visit('/');
+    cy.contains('Route').click();
+    cy.get(selector.refresh).click();
+    // remove route
+    for (let i = 0; i < 5; i += 1) {
+      
cy.get(selector.listTbody).get(selector.listRow).contains('More').click();
+      cy.contains('Delete').should('be.visible').click();
+      cy.contains('OK').should('be.visible').click();
+      cy.get(selector.notification).should('contain', data.deleteRouteSuccess);
+      cy.get(selector.notificationCloseIcon).click();
+    }
+
+    cy.visit('/');
+    cy.contains('Upstream').click();
+    cy.get(selector.refresh).click();
+    // remove route
+    for (let i = 0; i < 2; i += 1) {
+      
cy.get(selector.listTbody).get(selector.listRow).contains('Delete').click();
+      cy.contains('Confirm').should('be.visible').click();
+      cy.get(selector.notification).should('contain', 
data.deleteUpstreamSuccess);
+      cy.get(selector.notificationCloseIcon).click();
+    }
+  });
+});
diff --git a/web/src/locales/en-US/menu.ts b/web/src/locales/en-US/menu.ts
index 2bfe6db4..8a28fd35 100644
--- a/web/src/locales/en-US/menu.ts
+++ b/web/src/locales/en-US/menu.ts
@@ -75,4 +75,5 @@ export default {
   'menu.serverinfo': 'System Info',
   'menu.advanced-feature': 'Advanced',
   'menu.more': 'More',
+  'menu.close': 'Close',
 };
diff --git a/web/src/locales/tr-TR/menu.ts b/web/src/locales/tr-TR/menu.ts
index a0bdfec2..9c140af2 100644
--- a/web/src/locales/tr-TR/menu.ts
+++ b/web/src/locales/tr-TR/menu.ts
@@ -75,4 +75,5 @@ export default {
   'menu.serverinfo': 'Sistem Bilgisi',
   'menu.advanced-feature': 'Gelişmiş Özellikler',
   'menu.more': 'Daha Fazla',
+  'menu.close': 'Kapat',
 };
diff --git a/web/src/locales/zh-CN/menu.ts b/web/src/locales/zh-CN/menu.ts
index b8c109a4..1976dad0 100644
--- a/web/src/locales/zh-CN/menu.ts
+++ b/web/src/locales/zh-CN/menu.ts
@@ -72,4 +72,5 @@ export default {
   'menu.serverinfo': '系统信息',
   'menu.advanced-feature': '高级特性',
   'menu.more': '更多',
+  'menu.close': '关闭',
 };
diff --git a/web/src/pages/Route/List.tsx b/web/src/pages/Route/List.tsx
index 4f516425..f4f36bd5 100755
--- a/web/src/pages/Route/List.tsx
+++ b/web/src/pages/Route/List.tsx
@@ -28,9 +28,7 @@ import {
   Select,
   Radio,
   Form,
-  Upload,
   Modal,
-  Divider,
   Menu,
   Dropdown,
   Tooltip,
@@ -46,7 +44,6 @@ import { omit } from 'lodash';
 
 import { DELETE_FIELDS } from '@/constants';
 import { timestampToLocaleString } from '@/helpers';
-import type { RcFile } from 'antd/lib/upload';
 
 import {
   update,
@@ -56,11 +53,11 @@ import {
   fetchLabelList,
   updateRouteStatus,
   exportRoutes,
-  importRoutes,
 } from './service';
 import { DebugDrawView } from './components/DebugViews';
 import { RawDataEditor } from '@/components/RawDataEditor';
 import { EXPORT_FILE_MIME_TYPE_SUPPORTED } from './constants';
+import DataLoaderImport from '@/pages/Route/components/DataLoader/Import';
 
 const { OptGroup, Option } = Select;
 
@@ -81,8 +78,7 @@ const Page: React.FC = () => {
 
   const [labelList, setLabelList] = useState<LabelList>({});
   const [selectedRowKeys, setSelectedRowKeys] = useState<string[]>([]);
-  const [uploadFileList, setUploadFileList] = useState<RcFile[]>([]);
-  const [showImportModal, setShowImportModal] = useState(false);
+  const [showImportDrawer, setShowImportDrawer] = useState(false);
   const [visible, setVisible] = useState(false);
   const [rawData, setRawData] = useState<Record<string, any>>({});
   const [id, setId] = useState('');
@@ -154,27 +150,6 @@ const Page: React.FC = () => {
     });
   };
 
-  const handleImport = () => {
-    const formData = new FormData();
-    if (!uploadFileList[0]) {
-      notification.warn({
-        message: formatMessage({ id: 'page.route.button.selectFile' }),
-      });
-      return;
-    }
-    formData.append('file', uploadFileList[0]);
-    formData.append('type', 'openapi3');
-
-    importRoutes(formData).then(() => {
-      handleTableActionSuccessResponse(
-        `${formatMessage({ id: 'page.route.button.importOpenApi' })} 
${formatMessage({
-          id: 'component.status.success',
-        })}`,
-      );
-      setShowImportModal(false);
-    });
-  };
-
   const ListToolbar = () => {
     const tools = [
       {
@@ -194,11 +169,10 @@ const Page: React.FC = () => {
         },
       },
       {
-        name: formatMessage({ id: 'page.route.button.importOpenApi' }),
+        name: formatMessage({ id: 'page.route.data_loader.import' }),
         icon: <ImportOutlined />,
         onClick: () => {
-          setUploadFileList([]);
-          setShowImportModal(true);
+          setShowImportDrawer(true);
         },
       },
     ];
@@ -613,45 +587,14 @@ const Page: React.FC = () => {
           );
         }}
       />
-      <Modal
-        title={formatMessage({ id: 'page.route.button.importOpenApi' })}
-        visible={showImportModal}
-        okText={formatMessage({ id: 'component.global.confirm' })}
-        onOk={handleImport}
-        onCancel={() => {
-          setShowImportModal(false);
-        }}
-      >
-        <Upload
-          fileList={uploadFileList as any}
-          beforeUpload={(file) => {
-            setUploadFileList([file]);
-            return false;
-          }}
-          onRemove={() => {
-            setUploadFileList([]);
+      {showImportDrawer && (
+        <DataLoaderImport
+          onClose={(finish) => {
+            if (finish) checkPageList(ref);
+            setShowImportDrawer(false);
           }}
-        >
-          <Button type="primary" icon={<ImportOutlined />}>
-            {formatMessage({ id: 'page.route.button.selectFile' })}
-          </Button>
-        </Upload>
-        <Divider />
-        <div>
-          <p>{formatMessage({ id: 'page.route.instructions' })}:</p>
-          <p>
-            <a
-              
href="https://apisix.apache.org/docs/dashboard/IMPORT_OPENAPI_USER_GUIDE";
-              target="_blank"
-            >
-              1.{' '}
-              {`${formatMessage({ id: 'page.route.import' })} ${formatMessage({
-                id: 'page.route.instructions',
-              })}`}
-            </a>
-          </p>
-        </div>
-      </Modal>
+        />
+      )}
     </PageHeaderWrapper>
   );
 };
diff --git a/web/src/pages/Route/components/DataLoader/Import.tsx 
b/web/src/pages/Route/components/DataLoader/Import.tsx
new file mode 100644
index 00000000..e85903cc
--- /dev/null
+++ b/web/src/pages/Route/components/DataLoader/Import.tsx
@@ -0,0 +1,262 @@
+/*
+ * 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, { memo, useState } from 'react';
+import {
+  Button,
+  Col,
+  Collapse,
+  Divider,
+  Drawer,
+  Form,
+  Input,
+  notification,
+  Result,
+  Row,
+  Select,
+  Space,
+  Upload,
+} from 'antd';
+import { UploadOutlined } from '@ant-design/icons';
+import { useIntl } from '@@/plugin-locale/localeExports';
+import OpenAPI3 from './loader/OpenAPI3';
+import type { RcFile } from 'antd/lib/upload';
+import { importRoutes } from '@/pages/Route/service';
+
+type Props = {
+  onClose: (finish: boolean) => void;
+};
+
+type ImportType = 'openapi3' | 'openapi_legacy';
+type ImportState = 'import' | 'result';
+type ImportResult = {
+  success: boolean;
+  data: Record<
+    string,
+    {
+      total: number;
+      failed: number;
+      errors: string[];
+    }
+  >;
+};
+
+const entityNames = [
+  'route',
+  'upstream',
+  'service',
+  'consumer',
+  'ssl',
+  'stream_route',
+  'global_rule',
+  'plugin_config',
+  'proto',
+];
+
+const Option: React.FC<{
+  type: ImportType;
+}> = ({ type }) => {
+  switch (type) {
+    case 'openapi_legacy':
+      return <></>;
+    case 'openapi3':
+    default:
+      return <OpenAPI3 />;
+  }
+};
+
+const DataLoaderImport: React.FC<Props> = (props) => {
+  const [form] = Form.useForm();
+  const { formatMessage } = useIntl();
+  const { onClose } = props;
+  const [importType, setImportType] = useState<ImportType>('openapi3');
+  const [uploadFileList, setUploadFileList] = useState<RcFile[]>([]);
+  const [state, setState] = useState<ImportState>('import');
+  const [importResult, setImportResult] = useState<ImportResult>({
+    success: true,
+    data: {},
+  });
+
+  const onFinish = (values: Record<string, string>) => {
+    const formData = new FormData();
+    if (!uploadFileList[0]) {
+      notification.warn({
+        message: formatMessage({ id: 'page.route.button.selectFile' }),
+      });
+      return;
+    }
+    Object.keys(values).forEach((key) => {
+      formData.append(key, values[key]);
+    });
+    formData.append('file', uploadFileList[0]);
+
+    importRoutes(formData)
+      .then((r) => {
+        let errorNumber = 0;
+        entityNames.forEach((v) => {
+          errorNumber += r.data[v].failed;
+        });
+
+        setImportResult({
+          success: errorNumber <= 0,
+          data: r.data,
+        });
+        setState('result');
+      })
+      .catch(() => {});
+  };
+
+  return (
+    <Drawer
+      title={formatMessage({ id: 'page.route.data_loader.import_panel' })}
+      width={480}
+      visible={true}
+      onClose={() => onClose(false)}
+      footer={
+        <div
+          style={{
+            display: state === 'result' ? 'none' : 'flex',
+            justifyContent: 'space-between',
+          }}
+        >
+          <Button onClick={() => onClose(false)}>
+            {formatMessage({ id: 'component.global.cancel' })}
+          </Button>
+          <Space>
+            <Button
+              type="primary"
+              onClick={() => {
+                form.submit();
+              }}
+            >
+              {formatMessage({ id: 'component.global.submit' })}
+            </Button>
+          </Space>
+        </div>
+      }
+    >
+      {state === 'import' && (
+        <Form layout="vertical" form={form} onFinish={onFinish} 
requiredMark={false}>
+          <Row gutter={16}>
+            <Col span={12}>
+              <Form.Item
+                name="type"
+                label={formatMessage({ id: 
'page.route.data_loader.labels.loader_type' })}
+                rules={[
+                  {
+                    required: true,
+                    message: formatMessage({ id: 
'page.route.data_loader.tips.select_type' }),
+                  },
+                ]}
+                initialValue={importType}
+              >
+                <Select onChange={(value: ImportType) => setImportType(value)}>
+                  <Select.Option value="openapi3">
+                    {formatMessage({ id: 
'page.route.data_loader.types.openapi3' })}
+                  </Select.Option>
+                  <Select.Option value="openapi_legacy" disabled>
+                    {formatMessage({ id: 
'page.route.data_loader.types.openapi_legacy' })}
+                  </Select.Option>
+                </Select>
+              </Form.Item>
+            </Col>
+            <Col span={12}>
+              <Form.Item
+                name="task_name"
+                label={formatMessage({ id: 
'page.route.data_loader.labels.task_name' })}
+                rules={[
+                  {
+                    required: true,
+                    message: formatMessage({ id: 
'page.route.data_loader.tips.input_task_name' }),
+                  },
+                ]}
+              >
+                <Input
+                  placeholder={formatMessage({
+                    id: 'page.route.data_loader.tips.input_task_name',
+                  })}
+                />
+              </Form.Item>
+            </Col>
+          </Row>
+          <Option type={importType}></Option>
+          <Divider />
+          <Row gutter={16}>
+            <Col span={24}>
+              <Form.Item label={formatMessage({ id: 
'page.route.data_loader.labels.upload' })}>
+                <Upload
+                  fileList={uploadFileList as any}
+                  beforeUpload={(file) => {
+                    setUploadFileList([file]);
+                    return false;
+                  }}
+                  onRemove={() => {
+                    setUploadFileList([]);
+                  }}
+                >
+                  <Button icon={<UploadOutlined />}>
+                    {formatMessage({ id: 
'page.route.data_loader.tips.click_upload' })}
+                  </Button>
+                </Upload>
+              </Form.Item>
+            </Col>
+          </Row>
+        </Form>
+      )}
+      {state === 'result' && (
+        <Result
+          status={importResult.success ? 'success' : 'error'}
+          title={`${formatMessage({ id: 'page.route.data_loader.import' })} ${
+            importResult.success
+              ? formatMessage({ id: 'component.status.success' })
+              : formatMessage({ id: 'component.status.fail' })
+          }`}
+          extra={[
+            <Button
+              type="primary"
+              onClick={() => {
+                setState('import');
+                onClose(true);
+              }}
+            >
+              {formatMessage({ id: 'menu.close' })}
+            </Button>,
+          ]}
+        >
+          <Collapse>
+            {entityNames.map((v) => {
+              if (importResult.data[v] && importResult.data[v].total > 0) {
+                return (
+                  <Collapse.Panel
+                    collapsible={importResult.data[v].failed > 0 ? 'header' : 
'disabled'}
+                    header={`Total ${importResult.data[v].total} ${v} 
imported, ${importResult.data[v].failed} failed`}
+                    key={v}
+                  >
+                    {importResult.data[v].errors &&
+                      importResult.data[v].errors.map((err) => <p>{err}</p>)}
+                  </Collapse.Panel>
+                );
+              }
+              return null;
+            })}
+          </Collapse>
+        </Result>
+      )}
+    </Drawer>
+  );
+};
+
+export default memo(DataLoaderImport);
diff --git a/web/src/pages/Route/components/DataLoader/loader/OpenAPI3.tsx 
b/web/src/pages/Route/components/DataLoader/loader/OpenAPI3.tsx
new file mode 100644
index 00000000..4eea08bb
--- /dev/null
+++ b/web/src/pages/Route/components/DataLoader/loader/OpenAPI3.tsx
@@ -0,0 +1,40 @@
+/*
+ * 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, { memo } from 'react';
+import { Col, Form, Row, Switch } from 'antd';
+import { useIntl } from '@@/plugin-locale/localeExports';
+
+const DataLoaderOpenAPI3: React.FC = () => {
+  const { formatMessage } = useIntl();
+
+  return (
+    <Row gutter={16}>
+      <Col span={12}>
+        <Form.Item
+          name="merge_method"
+          label={formatMessage({ id: 
'page.route.data_loader.labels.openapi3_merge_method' })}
+          tooltip={formatMessage({ id: 
'page.route.data_loader.tips.openapi3_merge_method' })}
+          initialValue={true}
+        >
+          <Switch defaultChecked />
+        </Form.Item>
+      </Col>
+    </Row>
+  );
+};
+
+export default memo(DataLoaderOpenAPI3);
diff --git a/web/src/pages/Route/locales/en-US.ts 
b/web/src/pages/Route/locales/en-US.ts
index 66136882..5fcfc284 100644
--- a/web/src/pages/Route/locales/en-US.ts
+++ b/web/src/pages/Route/locales/en-US.ts
@@ -198,4 +198,18 @@ export default {
   'page.route.fields.vars.invalid': 'Please check the advanced match condition 
configuration',
   'page.route.fields.vars.in.invalid':
     'When using the IN operator, enter the parameter values in array format.',
+
+  'page.route.data_loader.import': 'Import',
+  'page.route.data_loader.import_panel': 'Import data',
+  'page.route.data_loader.types.openapi3': 'OpenAPI 3',
+  'page.route.data_loader.types.openapi_legacy': 'OpenAPI 3 Legacy',
+  'page.route.data_loader.labels.loader_type': 'Data Loader Type',
+  'page.route.data_loader.labels.task_name': 'Task Name',
+  'page.route.data_loader.labels.upload': 'Upload',
+  'page.route.data_loader.labels.openapi3_merge_method': 'Merge HTTP Methods',
+  'page.route.data_loader.tips.select_type': 'Please select data loader',
+  'page.route.data_loader.tips.input_task_name': 'Please input import task 
name',
+  'page.route.data_loader.tips.click_upload': 'Click to Upload',
+  'page.route.data_loader.tips.openapi3_merge_method':
+    'Whether to merge multiple HTTP methods in the OpenAPI path into a single 
route. When you have multiple HTTP methods in your path with different details 
configuration (e.g. securitySchema), you can turn off this option to generate 
them into multiple routes.',
 };
diff --git a/web/src/pages/Route/locales/tr-TR.ts 
b/web/src/pages/Route/locales/tr-TR.ts
index e73f2e88..6078f474 100644
--- a/web/src/pages/Route/locales/tr-TR.ts
+++ b/web/src/pages/Route/locales/tr-TR.ts
@@ -76,12 +76,10 @@ export default {
   'page.route.form.itemLabel.token': 'Token(Jeton)',
   'page.route.form.itemLabel.apikey': 'Api Anahtarı',
 
-  'page.route.form.itemExtraMessage.domain':
-    'Domain adınızı girin. Örn: www.example.com',
+  'page.route.form.itemExtraMessage.domain': 'Domain adınızı girin. Örn: 
www.example.com',
   'page.route.form.itemRulesPatternMessage.domain':
     'Domain adı gerekli ve geçerli bir değer içermelidir.',
-  'page.route.form.itemExtraMessage1.path':
-    'Yönlendirme yolunu girin. Örn: /foo/index.html',
+  'page.route.form.itemExtraMessage1.path': 'Yönlendirme yolunu girin. Örn: 
/foo/index.html',
   'page.route.form.itemRulesPatternMessage.remoteAddrs':
     'Geçerli bir IP adresi girin. örn: 192.168.1.101, 192.168.1.0/24, ::1, 
fe80::1, fe80::1/64',
   'page.route.form.itemExtraMessage1.remoteAddrs':
@@ -118,7 +116,8 @@ export default {
   'page.route.steps.stepTitle.defineApiRequest': 'API Request tanımla',
   'page.route.steps.stepTitle.defineApiBackendServe': 'API Backend Server 
tanımla',
 
-  'page.route.popconfirm.title.offline': 'Bu yönlendirmeyi çevrimdışı yapmak 
istediğinize emin misiniz?',
+  'page.route.popconfirm.title.offline':
+    'Bu yönlendirmeyi çevrimdışı yapmak istediğinize emin misiniz?',
   'page.route.radio.static': 'Statik',
   'page.route.radio.regex': 'Regex',
   'page.route.form.itemLabel.from': 'Kimden',
@@ -145,7 +144,8 @@ export default {
   'page.route.list': 'Rotalar',
   'page.route.panelSection.title.requestOverride': 'Yönlendirme İsteği',
   'page.route.form.itemLabel.headerRewrite': 'Header Yönlendirme',
-  'page.route.tooltip.pluginOrchOnlySupportChrome': 'Eklenti düzenlemesi 
yalnızca Chromeu destekler.',
+  'page.route.tooltip.pluginOrchOnlySupportChrome':
+    'Eklenti düzenlemesi yalnızca Chromeu destekler.',
   'page.route.tooltip.pluginOrchWithoutProxyRewrite':
     'Adım 1de istek geçersiz kılma yapılandırıldığında eklenti düzenleme modu 
kullanılamaz.',
   'page.route.tooltip.pluginOrchWithoutRedirect':
@@ -155,14 +155,15 @@ export default {
   'page.route.tabs.orchestration': 'Orkestrasyon',
 
   'page.route.list.description':
-  'Rota, bir istemci isteği ile bir hizmet arasındaki eşleştirme kurallarını 
tanımlayan bir isteğin giriş noktasıdır. Bir yol bir hizmetle (Hizmet), bir 
yukarı akışla (Upstream) ilişkilendirilebilir, bir hizmet bir dizi rotaya 
karşılık gelebilir ve bir rota bir yukarı akış nesnesine (bir dizi arka uç 
hizmet düğümü) karşılık gelebilir, böylece her istek eşleşir. bir rotaya, ağ 
geçidi tarafından rotaya bağlı yukarı akış hizmetine vekalet edilecektir.',
+    'Rota, bir istemci isteği ile bir hizmet arasındaki eşleştirme kurallarını 
tanımlayan bir isteğin giriş noktasıdır. Bir yol bir hizmetle (Hizmet), bir 
yukarı akışla (Upstream) ilişkilendirilebilir, bir hizmet bir dizi rotaya 
karşılık gelebilir ve bir rota bir yukarı akış nesnesine (bir dizi arka uç 
hizmet düğümü) karşılık gelebilir, böylece her istek eşleşir. bir rotaya, ağ 
geçidi tarafından rotaya bağlı yukarı akış hizmetine vekalet edilecektir.',
   'page.route.configuration.name.rules.required.description': 'Rota adı 
zorunlu bir alandır.',
   'page.route.configuration.name.placeholder': 'Rota adı girin',
   'page.route.configuration.desc.tooltip': 'Rota açıklaması girin',
   'page.route.configuration.publish.tooltip':
     'Bir rotanın oluşturulduktan hemen sonra ağ geçidine yayınlanıp 
yayınlanmayacağını kontrol etmek için kullanılır',
   'page.route.configuration.version.placeholder': 'Rota sürümü girin',
-  'page.route.configuration.version.tooltip': 'Rota sürümü şu şekilde 
olabilir: 1.0.0, 1.0.1, 1.0.2',
+  'page.route.configuration.version.tooltip':
+    'Rota sürümü şu şekilde olabilir: 1.0.0, 1.0.1, 1.0.2',
   'page.route.configuration.normal-labels.tooltip':
     'Rota gruplaması için kullanılabilecek rotalara özel etiketler ekleyin.',
 
@@ -183,18 +184,34 @@ export default {
   'page.route.fields.service_id.without-upstream':
     'Hizmeti bağlamazsanız, Yukarı Akışı ayarlamanız gerekir (Adım 2)',
   'page.route.advanced-match.tooltip':
-  'İstek üstbilgileri, istek parametreleri ve tanımlama bilgileri aracılığıyla 
rota eşleştirmeyi destekler ve gri tonlamalı yayınlama ve mavi-yeşil test gibi 
senaryolara uygulanabilir.',
+    'İstek üstbilgileri, istek parametreleri ve tanımlama bilgileri 
aracılığıyla rota eşleştirmeyi destekler ve gri tonlamalı yayınlama ve 
mavi-yeşil test gibi senaryolara uygulanabilir.',
   'page.route.advanced-match.message': 'İpuçları',
-  'page.route.advanced-match.tips.requestParameter': 'İstek Parametresi:İstek 
URLsinin sorgulanması',
+  'page.route.advanced-match.tips.requestParameter':
+    'İstek Parametresi:İstek URLsinin sorgulanması',
   'page.route.advanced-match.tips.postRequestParameter':
-  'POST İstek Parametresi:Yalnızca x-www-form-urlencoding formunu destekler',
+    'POST İstek Parametresi:Yalnızca x-www-form-urlencoding formunu destekler',
   'page.route.advanced-match.tips.builtinParameter':
     'Yerleşik Parametre: Nginx dahili parametreleri destekler',
 
   'page.route.fields.custom.redirectOption.tooltip': 'Bu yönlendirme eklentisi 
ile ilgilidir',
-  'page.route.fields.service_id.tooltip': 'Yapılandırmalarını yeniden 
kullanmak için Hizmet nesnesini bağlayın.',
+  'page.route.fields.service_id.tooltip':
+    'Yapılandırmalarını yeniden kullanmak için Hizmet nesnesini bağlayın.',
 
   'page.route.fields.vars.invalid': 'Lütfen gelişmiş eşleşme koşulu 
yapılandırmasını kontrol edin',
   'page.route.fields.vars.in.invalid':
     'IN operatörünü kullanırken parametre değerlerini dizi formatında girin.',
+
+  'page.route.data_loader.import': 'Import',
+  'page.route.data_loader.import_panel': 'Import data',
+  'page.route.data_loader.types.openapi3': 'OpenAPI 3',
+  'page.route.data_loader.types.openapi_legacy': 'OpenAPI 3 Legacy',
+  'page.route.data_loader.labels.loader_type': 'Data Loader Type',
+  'page.route.data_loader.labels.task_name': 'Task Name',
+  'page.route.data_loader.labels.upload': 'Upload',
+  'page.route.data_loader.labels.openapi3_merge_method': 'Merge HTTP Methods',
+  'page.route.data_loader.tips.select_type': 'Please select data loader',
+  'page.route.data_loader.tips.input_task_name': 'Please input import task 
name',
+  'page.route.data_loader.tips.click_upload': 'Click to Upload',
+  'page.route.data_loader.tips.openapi3_merge_method':
+    'Whether to merge multiple HTTP methods in the OpenAPI path into a single 
route. When you have multiple HTTP methods in your path with different details 
configuration (e.g. securitySchema), you can turn off this option to generate 
them into multiple routes.',
 };
diff --git a/web/src/pages/Route/locales/zh-CN.ts 
b/web/src/pages/Route/locales/zh-CN.ts
index 4c0e7ffe..dce4a763 100644
--- a/web/src/pages/Route/locales/zh-CN.ts
+++ b/web/src/pages/Route/locales/zh-CN.ts
@@ -192,4 +192,18 @@ export default {
 
   'page.route.fields.vars.invalid': '请检查高级匹配条件配置',
   'page.route.fields.vars.in.invalid': '使用 IN 操作符时,请输入数组格式的参数值。',
+
+  'page.route.data_loader.import': '导入',
+  'page.route.data_loader.import_panel': '导入路由',
+  'page.route.data_loader.types.openapi3': 'OpenAPI 3',
+  'page.route.data_loader.types.openapi_legacy': 'OpenAPI 3 旧版',
+  'page.route.data_loader.labels.loader_type': '数据加载器类型',
+  'page.route.data_loader.labels.task_name': '导入任务名称',
+  'page.route.data_loader.labels.upload': '上传',
+  'page.route.data_loader.labels.openapi3_merge_method': '合并 HTTP 方法',
+  'page.route.data_loader.tips.select_type': '请选择数据加载器',
+  'page.route.data_loader.tips.input_task_name': '请输入导入任务名称',
+  'page.route.data_loader.tips.click_upload': '点击上传',
+  'page.route.data_loader.tips.openapi3_merge_method':
+    '是否将 OpenAPI 路径中的多个 HTTP 方法合并为单一路由。当你的路径中多个 HTTP 方法有不同的细节配置(如 
securitySchema),你可以关闭这个选项,将为不同的 HTTP 方法生成单独的路由。',
 };

Reply via email to