You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by GitBox <gi...@apache.org> on 2022/06/20 09:00:21 UTC

[GitHub] [apisix-dashboard] bzp2010 opened a new pull request, #2480: feat: support data loader in frontend

bzp2010 opened a new pull request, #2480:
URL: https://github.com/apache/apisix-dashboard/pull/2480

   Please answer these questions before submitting a pull request, **or your PR will get closed**.
   
   **Why submit this pull request?**
   
   - [ ] Bugfix
   - [x] New feature provided
   - [x] Improve performance
   - [ ] Backport patches
   
   **What changes will this PR take into?**
   
   Improved OpenAPI import form for a new import experience.
   
   In the current design, he will provide such an import form that can support multiple import data sources with their responsive import options.
   ![image](https://user-images.githubusercontent.com/8078418/174565160-e3fb2950-5749-461c-8dfb-f47a9b96ed4e.png)
   
   
   **Related issues**
   
   a part of #2465 
   
   **Checklist:**
   
   - [x] Did you explain what problem does this PR solve? Or what new features have been added?
   - [x] Have you added corresponding test cases?
   - [x] Have you modified the corresponding document?
   - [x] Is this PR backward compatible? If it is not backward compatible, please discuss on the mailing list first
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@apisix.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [apisix-dashboard] bzp2010 commented on a diff in pull request #2480: feat: support data loader in frontend

Posted by GitBox <gi...@apache.org>.
bzp2010 commented on code in PR #2480:
URL: https://github.com/apache/apisix-dashboard/pull/2480#discussion_r905640174


##########
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, { 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?: () => void;
+  onFinish?: () => 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) => {

Review Comment:
   Hi, @LiteSun.
   
   The implementation of this API does not use an HTTP status code as an error code, it will return 200 regardless of the input data, and the import error will be marked in the return `data`, while the underlying request library of the dashboard will automatically process for a 5xx error like a program error.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@apisix.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [apisix-dashboard] anldrms commented on a diff in pull request #2480: feat: support data loader in frontend

Posted by GitBox <gi...@apache.org>.
anldrms commented on code in PR #2480:
URL: https://github.com/apache/apisix-dashboard/pull/2480#discussion_r907273383


##########
web/src/pages/Route/locales/tr-TR.ts:
##########
@@ -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',

Review Comment:
   I'll glad to help for that.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@apisix.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [apisix-dashboard] Baoyuantop commented on a diff in pull request #2480: feat: support data loader in frontend

Posted by GitBox <gi...@apache.org>.
Baoyuantop commented on code in PR #2480:
URL: https://github.com/apache/apisix-dashboard/pull/2480#discussion_r905711534


##########
web/src/pages/Route/locales/tr-TR.ts:
##########
@@ -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',

Review Comment:
   Hi @anldrms, Can you help review these translations?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@apisix.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [apisix-dashboard] codecov-commenter commented on pull request #2480: feat: support data loader in frontend

Posted by GitBox <gi...@apache.org>.
codecov-commenter commented on PR #2480:
URL: https://github.com/apache/apisix-dashboard/pull/2480#issuecomment-1163977924

   # [Codecov](https://codecov.io/gh/apache/apisix-dashboard/pull/2480?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=The+Apache+Software+Foundation) Report
   > Merging [#2480](https://codecov.io/gh/apache/apisix-dashboard/pull/2480?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=The+Apache+Software+Foundation) (09b5350) into [master](https://codecov.io/gh/apache/apisix-dashboard/commit/13670d242a09bde9bb5382f4485c65fd95046e11?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=The+Apache+Software+Foundation) (13670d2) will **decrease** coverage by `4.04%`.
   > The diff coverage is `26.19%`.
   
   ```diff
   @@            Coverage Diff             @@
   ##           master    #2480      +/-   ##
   ==========================================
   - Coverage   71.55%   67.51%   -4.05%     
   ==========================================
     Files          60      133      +73     
     Lines        3966     3469     -497     
     Branches        0      846     +846     
   ==========================================
   - Hits         2838     2342     -496     
   - Misses        838     1127     +289     
   + Partials      290        0     -290     
   ```
   
   | Flag | Coverage Δ | |
   |---|---|---|
   | backend-e2e-test-ginkgo | `?` | |
   | backend-unit-test | `?` | |
   | frontend-e2e-test | `67.51% <26.19%> (?)` | |
   
   Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=The+Apache+Software+Foundation#carryforward-flags-in-the-pull-request-comment) to find out more.
   
   | [Impacted Files](https://codecov.io/gh/apache/apisix-dashboard/pull/2480?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=The+Apache+Software+Foundation) | Coverage Δ | |
   |---|---|---|
   | [web/src/pages/Route/List.tsx](https://codecov.io/gh/apache/apisix-dashboard/pull/2480/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=The+Apache+Software+Foundation#diff-d2ViL3NyYy9wYWdlcy9Sb3V0ZS9MaXN0LnRzeA==) | `66.88% <16.66%> (ø)` | |
   | [...b/src/pages/Route/components/DataLoader/Import.tsx](https://codecov.io/gh/apache/apisix-dashboard/pull/2480/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=The+Apache+Software+Foundation#diff-d2ViL3NyYy9wYWdlcy9Sb3V0ZS9jb21wb25lbnRzL0RhdGFMb2FkZXIvSW1wb3J0LnRzeA==) | `27.27% <27.27%> (ø)` | |
   | [...es/Route/components/DataLoader/loader/OpenAPI3.tsx](https://codecov.io/gh/apache/apisix-dashboard/pull/2480/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=The+Apache+Software+Foundation#diff-d2ViL3NyYy9wYWdlcy9Sb3V0ZS9jb21wb25lbnRzL0RhdGFMb2FkZXIvbG9hZGVyL09wZW5BUEkzLnRzeA==) | `33.33% <33.33%> (ø)` | |
   | [api/internal/route.go](https://codecov.io/gh/apache/apisix-dashboard/pull/2480/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=The+Apache+Software+Foundation#diff-YXBpL2ludGVybmFsL3JvdXRlLmdv) | | |
   | [api/internal/core/server/server.go](https://codecov.io/gh/apache/apisix-dashboard/pull/2480/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=The+Apache+Software+Foundation#diff-YXBpL2ludGVybmFsL2NvcmUvc2VydmVyL3NlcnZlci5nbw==) | | |
   | [api/internal/handler/migrate/migrate.go](https://codecov.io/gh/apache/apisix-dashboard/pull/2480/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=The+Apache+Software+Foundation#diff-YXBpL2ludGVybmFsL2hhbmRsZXIvbWlncmF0ZS9taWdyYXRlLmdv) | | |
   | [api/internal/handler/stream\_route/stream\_route.go](https://codecov.io/gh/apache/apisix-dashboard/pull/2480/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=The+Apache+Software+Foundation#diff-YXBpL2ludGVybmFsL2hhbmRsZXIvc3RyZWFtX3JvdXRlL3N0cmVhbV9yb3V0ZS5nbw==) | | |
   | [api/internal/handler/data\_loader/route\_export.go](https://codecov.io/gh/apache/apisix-dashboard/pull/2480/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=The+Apache+Software+Foundation#diff-YXBpL2ludGVybmFsL2hhbmRsZXIvZGF0YV9sb2FkZXIvcm91dGVfZXhwb3J0Lmdv) | | |
   | [api/main.go](https://codecov.io/gh/apache/apisix-dashboard/pull/2480/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=The+Apache+Software+Foundation#diff-YXBpL21haW4uZ28=) | | |
   | [...pi/internal/handler/plugin\_config/plugin\_config.go](https://codecov.io/gh/apache/apisix-dashboard/pull/2480/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=The+Apache+Software+Foundation#diff-YXBpL2ludGVybmFsL2hhbmRsZXIvcGx1Z2luX2NvbmZpZy9wbHVnaW5fY29uZmlnLmdv) | | |
   | ... and [184 more](https://codecov.io/gh/apache/apisix-dashboard/pull/2480/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=The+Apache+Software+Foundation) | |
   
   ------
   
   [Continue to review full report at Codecov](https://codecov.io/gh/apache/apisix-dashboard/pull/2480?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=The+Apache+Software+Foundation).
   > **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=The+Apache+Software+Foundation)
   > `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
   > Powered by [Codecov](https://codecov.io/gh/apache/apisix-dashboard/pull/2480?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=The+Apache+Software+Foundation). Last update [13670d2...09b5350](https://codecov.io/gh/apache/apisix-dashboard/pull/2480?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=The+Apache+Software+Foundation). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=The+Apache+Software+Foundation).
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@apisix.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [apisix-dashboard] bzp2010 commented on a diff in pull request #2480: feat: support data loader in frontend

Posted by GitBox <gi...@apache.org>.
bzp2010 commented on code in PR #2480:
URL: https://github.com/apache/apisix-dashboard/pull/2480#discussion_r905235342


##########
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 方法生成单独的路由。',

Review Comment:
   fixed



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@apisix.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [apisix-dashboard] anldrms commented on a diff in pull request #2480: feat: support data loader in frontend

Posted by GitBox <gi...@apache.org>.
anldrms commented on code in PR #2480:
URL: https://github.com/apache/apisix-dashboard/pull/2480#discussion_r907273383


##########
web/src/pages/Route/locales/tr-TR.ts:
##########
@@ -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',

Review Comment:
   Sorry for late but I'll glad to help for that. 



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@apisix.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [apisix-dashboard] bzp2010 commented on a diff in pull request #2480: feat: support data loader in frontend

Posted by GitBox <gi...@apache.org>.
bzp2010 commented on code in PR #2480:
URL: https://github.com/apache/apisix-dashboard/pull/2480#discussion_r905685130


##########
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, { 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?: () => void;
+  onFinish?: () => 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');
+    });
+  };
+
+  return (
+    <>
+      <Drawer
+        title={formatMessage({ id: 'page.route.data_loader.import_panel' })}
+        width={480}
+        visible={true}
+        onClose={onClose}
+        footer={
+          <div
+            style={{
+              display: state === 'result' ? 'none' : 'flex',
+              justifyContent: 'space-between',
+            }}
+          >
+            <Button onClick={onClose}>{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} hideRequiredMark>
+            <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>

Review Comment:
   The older version is provided by PR #2468, but it is still not merged, so it is disabled here. It is expected that it will be provided, but we will not provide any maintenance for it anymore. It may be removed in the future (after we have a new version of the export implementation), I can't be sure which will happen first.



##########
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, { 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?: () => void;
+  onFinish?: () => 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');
+    });
+  };
+
+  return (
+    <>
+      <Drawer
+        title={formatMessage({ id: 'page.route.data_loader.import_panel' })}
+        width={480}
+        visible={true}
+        onClose={onClose}
+        footer={
+          <div
+            style={{
+              display: state === 'result' ? 'none' : 'flex',
+              justifyContent: 'space-between',
+            }}
+          >
+            <Button onClick={onClose}>{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} hideRequiredMark>
+            <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>

Review Comment:
   @guoqqqi 
   
   The older version is provided by PR #2468, but it is still not merged, so it is disabled here. It is expected that it will be provided, but we will not provide any maintenance for it anymore. It may be removed in the future (after we have a new version of the export implementation), I can't be sure which will happen first.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@apisix.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [apisix-dashboard] bzp2010 commented on a diff in pull request #2480: feat: support data loader in frontend

Posted by GitBox <gi...@apache.org>.
bzp2010 commented on code in PR #2480:
URL: https://github.com/apache/apisix-dashboard/pull/2480#discussion_r905743040


##########
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, { 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?: () => void;
+  onFinish?: () => 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) => {

Review Comment:
   @Baoyuantop I've added an empty function as a catch for avoid the uncatch exception.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@apisix.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [apisix-dashboard] bzp2010 merged pull request #2480: feat: support data loader in frontend

Posted by GitBox <gi...@apache.org>.
bzp2010 merged PR #2480:
URL: https://github.com/apache/apisix-dashboard/pull/2480


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@apisix.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [apisix-dashboard] bzp2010 commented on a diff in pull request #2480: feat: support data loader in frontend

Posted by GitBox <gi...@apache.org>.
bzp2010 commented on code in PR #2480:
URL: https://github.com/apache/apisix-dashboard/pull/2480#discussion_r905743040


##########
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, { 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?: () => void;
+  onFinish?: () => 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) => {

Review Comment:
   @Baoyuantop I've added a console.error as a catch for it to help debug the error.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@apisix.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [apisix-dashboard] bzp2010 commented on pull request #2480: feat: support data loader in frontend

Posted by GitBox <gi...@apache.org>.
bzp2010 commented on PR #2480:
URL: https://github.com/apache/apisix-dashboard/pull/2480#issuecomment-1164173782

   ### About test
   
   Import using the new `Data Loader` interface. The imported target is the API101 OAS document, which contains three APIs, two of which have two HTTP methods each, thus generating three routes and one upstream in merged method mode and five routes and one upstream in non-merged mode.
   
   For routes a pre-check is performed before writing the data to make sure there are no duplicate routes with the same effect; for upstream, the detection is performed only at the time of writing. Therefore, if the route is duplicated, none of it will be written; if the upstream is duplicated, the route will be written and the upstream will fail to write.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@apisix.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [apisix-dashboard] SkyeYoung commented on a diff in pull request #2480: feat: support data loader in frontend

Posted by GitBox <gi...@apache.org>.
SkyeYoung commented on code in PR #2480:
URL: https://github.com/apache/apisix-dashboard/pull/2480#discussion_r904794983


##########
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 方法生成单独的路由。',

Review Comment:
   ```suggestion
       '是否将 OpenAPI 路径中的多个 HTTP 方法合并为单一路由。当你的路径中多个 HTTP 方法有不同的细节配置(如 securitySchema),你可以关闭这个选项,将为不同的 HTTP 方法生成单独的路由。',
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@apisix.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [apisix-dashboard] SkyeYoung commented on a diff in pull request #2480: feat: support data loader in frontend

Posted by GitBox <gi...@apache.org>.
SkyeYoung commented on code in PR #2480:
URL: https://github.com/apache/apisix-dashboard/pull/2480#discussion_r905641466


##########
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, { 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?: () => void;
+  onFinish?: () => 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');
+    });
+  };
+
+  return (
+    <>
+      <Drawer
+        title={formatMessage({ id: 'page.route.data_loader.import_panel' })}
+        width={480}
+        visible={true}
+        onClose={onClose}
+        footer={
+          <div
+            style={{
+              display: state === 'result' ? 'none' : 'flex',
+              justifyContent: 'space-between',
+            }}
+          >
+            <Button onClick={onClose}>{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} hideRequiredMark>
+            <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?.();
+                  if (props.onFinish) props.onFinish();
+                }}
+              >
+                {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 DataLoaderImport;

Review Comment:
   I think we need to validate rather than abuse "memo".



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@apisix.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [apisix-dashboard] guoqqqi commented on a diff in pull request #2480: feat: support data loader in frontend

Posted by GitBox <gi...@apache.org>.
guoqqqi commented on code in PR #2480:
URL: https://github.com/apache/apisix-dashboard/pull/2480#discussion_r905643042


##########
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, { 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?: () => void;
+  onFinish?: () => 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');
+    });
+  };
+
+  return (
+    <>
+      <Drawer
+        title={formatMessage({ id: 'page.route.data_loader.import_panel' })}
+        width={480}
+        visible={true}
+        onClose={onClose}
+        footer={
+          <div
+            style={{
+              display: state === 'result' ? 'none' : 'flex',
+              justifyContent: 'space-between',
+            }}
+          >
+            <Button onClick={onClose}>{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} hideRequiredMark>
+            <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>

Review Comment:
   I see that the old import method is disabled now, will the old code be removed uniformly, or will it be compatible with the new openAPI import?



##########
web/cypress/integration/route/data-loader-import.spec.js:
##########
@@ -0,0 +1,198 @@
+/*
+ * 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 = {
+    route_name_0: 'route_name_0',
+    route_name_1: 'route_name_1',
+    upstream_node0_host_0: '1.1.1.1',
+    upstream_node0_host_1: '2.2.2.2',
+    importErrorMsg: 'required file type is .yaml, .yml or .json but got: .txt',
+    uploadRouteFiles: [
+      '../../../api/test/testdata/import/default.json',
+      '../../../api/test/testdata/import/default.yaml',
+      'import-error.txt',
+    ],
+    // Note: export file's name will be end of a timestamp
+    jsonMask: 'cypress/downloads/*.json',
+    yamlMask: 'cypress/downloads/*.yaml',
+    port: '80',
+    weight: 1,
+    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();
+
+    // 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();
+
+    // 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();
+
+    // check result
+    cy.get(selector.listTbody).get(selector.listRow).should('have.length', 5);
+
+    // remove route
+    /**/

Review Comment:
   This section seems to be able to be deleted



##########
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, { 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?: () => void;
+  onFinish?: () => 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');
+    });
+  };
+
+  return (
+    <>
+      <Drawer
+        title={formatMessage({ id: 'page.route.data_loader.import_panel' })}
+        width={480}
+        visible={true}
+        onClose={onClose}
+        footer={
+          <div
+            style={{
+              display: state === 'result' ? 'none' : 'flex',
+              justifyContent: 'space-between',
+            }}
+          >
+            <Button onClick={onClose}>{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} hideRequiredMark>

Review Comment:
   ```suggestion
             <Form layout="vertical" form={form} onFinish={onFinish} hideRequiredMark>
   ```
   `hideRequiredMark` Why do we need to hide the required attributes?



##########
web/cypress/integration/route/data-loader-import.spec.js:
##########
@@ -0,0 +1,198 @@
+/*
+ * 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 = {
+    route_name_0: 'route_name_0',
+    route_name_1: 'route_name_1',
+    upstream_node0_host_0: '1.1.1.1',
+    upstream_node0_host_1: '2.2.2.2',
+    importErrorMsg: 'required file type is .yaml, .yml or .json but got: .txt',
+    uploadRouteFiles: [
+      '../../../api/test/testdata/import/default.json',
+      '../../../api/test/testdata/import/default.yaml',
+      'import-error.txt',
+    ],
+    // Note: export file's name will be end of a timestamp
+    jsonMask: 'cypress/downloads/*.json',
+    yamlMask: 'cypress/downloads/*.yaml',
+    port: '80',
+    weight: 1,
+    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();
+
+    // 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();
+
+    // 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();
+
+    // check result
+    cy.get(selector.listTbody).get(selector.listRow).should('have.length', 5);
+
+    // remove route
+    /**/
+  });
+
+  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();

Review Comment:
   It is best to add assertions to confirm that the drawer has disappeared



##########
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, { 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?: () => void;
+  onFinish?: () => 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');
+    });
+  };
+
+  return (
+    <>
+      <Drawer
+        title={formatMessage({ id: 'page.route.data_loader.import_panel' })}
+        width={480}
+        visible={true}
+        onClose={onClose}
+        footer={
+          <div
+            style={{
+              display: state === 'result' ? 'none' : 'flex',
+              justifyContent: 'space-between',
+            }}
+          >
+            <Button onClick={onClose}>{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} hideRequiredMark>
+            <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?.();

Review Comment:
   Would it be better to change the onClose method to a mandatory parameter?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@apisix.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [apisix-dashboard] bzp2010 commented on a diff in pull request #2480: feat: support data loader in frontend

Posted by GitBox <gi...@apache.org>.
bzp2010 commented on code in PR #2480:
URL: https://github.com/apache/apisix-dashboard/pull/2480#discussion_r905705680


##########
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, { 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?: () => void;
+  onFinish?: () => 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');
+    });
+  };
+
+  return (
+    <>

Review Comment:
   removed



##########
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, { 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?: () => void;
+  onFinish?: () => 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');
+    });
+  };
+
+  return (
+    <>
+      <Drawer
+        title={formatMessage({ id: 'page.route.data_loader.import_panel' })}
+        width={480}
+        visible={true}
+        onClose={onClose}
+        footer={
+          <div
+            style={{
+              display: state === 'result' ? 'none' : 'flex',
+              justifyContent: 'space-between',
+            }}
+          >
+            <Button onClick={onClose}>{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} hideRequiredMark>
+            <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?.();
+                  if (props.onFinish) props.onFinish();
+                }}
+              >
+                {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>
+    </>

Review Comment:
   removed



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@apisix.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [apisix-dashboard] bzp2010 commented on a diff in pull request #2480: feat: support data loader in frontend

Posted by GitBox <gi...@apache.org>.
bzp2010 commented on code in PR #2480:
URL: https://github.com/apache/apisix-dashboard/pull/2480#discussion_r905642692


##########
web/src/pages/Route/locales/tr-TR.ts:
##########
@@ -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',

Review Comment:
   @LiteSun 
   
   I don't know any Turkish and I can only translate using a translation tool that may not convey the meaning correctly (I had a lot of problems with the English=>Turkish=>English back-and-forth translation) and I think it's better to use English for now and wait for a later translator to do the translation.
   
   BTW, later I will try to import these into crowdin to simplify the internationalization process.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@apisix.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [apisix-dashboard] bzp2010 commented on a diff in pull request #2480: feat: support data loader in frontend

Posted by GitBox <gi...@apache.org>.
bzp2010 commented on code in PR #2480:
URL: https://github.com/apache/apisix-dashboard/pull/2480#discussion_r905743665


##########
web/src/pages/Route/locales/tr-TR.ts:
##########
@@ -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',

Review Comment:
   @Baoyuantop @anldrms  If it would help to add a translation of this section, a new PR could be created to do this. ☺️



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@apisix.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [apisix-dashboard] bzp2010 commented on a diff in pull request #2480: feat: support data loader in frontend

Posted by GitBox <gi...@apache.org>.
bzp2010 commented on code in PR #2480:
URL: https://github.com/apache/apisix-dashboard/pull/2480#discussion_r905748088


##########
web/src/pages/Route/components/DataLoader/Import.tsx:
##########
@@ -103,18 +103,20 @@ const DataLoaderImport: React.FC<Props> = (props) => {
     });
     formData.append('file', uploadFileList[0]);
 
-    importRoutes(formData).then((r) => {
-      let errorNumber = 0;
-      entityNames.forEach((v) => {
-        errorNumber += r.data[v].failed;
-      });
+    importRoutes(formData)
+      .then((r) => {
+        let errorNumber = 0;
+        entityNames.forEach((v) => {
+          errorNumber += r.data[v].failed;
+        });
 
-      setImportResult({
-        success: errorNumber <= 0,
-        data: r.data,
-      });
-      setState('result');
-    });
+        setImportResult({
+          success: errorNumber <= 0,
+          data: r.data,
+        });
+        setState('result');
+      })
+      .catch(console.error);

Review Comment:
   We don't actually need to use it to handle errors either, just to prevent JS errors where exceptions are not caught.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@apisix.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [apisix-dashboard] LiteSun commented on a diff in pull request #2480: feat: support data loader in frontend

Posted by GitBox <gi...@apache.org>.
LiteSun commented on code in PR #2480:
URL: https://github.com/apache/apisix-dashboard/pull/2480#discussion_r905629184


##########
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, { 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?: () => void;
+  onFinish?: () => 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');
+    });
+  };
+
+  return (
+    <>

Review Comment:
   ```suggestion
   ```



##########
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 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 DataLoaderOpenAPI3;

Review Comment:
   ditto.



##########
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, { 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?: () => void;
+  onFinish?: () => 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');
+    });
+  };
+
+  return (
+    <>
+      <Drawer
+        title={formatMessage({ id: 'page.route.data_loader.import_panel' })}
+        width={480}
+        visible={true}
+        onClose={onClose}
+        footer={
+          <div
+            style={{
+              display: state === 'result' ? 'none' : 'flex',
+              justifyContent: 'space-between',
+            }}
+          >
+            <Button onClick={onClose}>{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} hideRequiredMark>
+            <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?.();
+                  if (props.onFinish) props.onFinish();
+                }}
+              >
+                {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 DataLoaderImport;

Review Comment:
   ```suggestion
   export default memo(DataLoaderImport);
   ```
   This way could reduce component rerender times.



##########
web/src/pages/Route/locales/tr-TR.ts:
##########
@@ -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',

Review Comment:
   why use English here, will it update in the future?



##########
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, { 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?: () => void;
+  onFinish?: () => 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) => {

Review Comment:
   don't need to handle request error cases?



##########
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, { 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?: () => void;
+  onFinish?: () => 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');
+    });
+  };
+
+  return (
+    <>
+      <Drawer
+        title={formatMessage({ id: 'page.route.data_loader.import_panel' })}
+        width={480}
+        visible={true}
+        onClose={onClose}
+        footer={
+          <div
+            style={{
+              display: state === 'result' ? 'none' : 'flex',
+              justifyContent: 'space-between',
+            }}
+          >
+            <Button onClick={onClose}>{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} hideRequiredMark>
+            <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?.();
+                  if (props.onFinish) props.onFinish();
+                }}
+              >
+                {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>
+    </>

Review Comment:
   ```suggestion
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@apisix.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [apisix-dashboard] bzp2010 commented on a diff in pull request #2480: feat: support data loader in frontend

Posted by GitBox <gi...@apache.org>.
bzp2010 commented on code in PR #2480:
URL: https://github.com/apache/apisix-dashboard/pull/2480#discussion_r905696653


##########
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, { 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?: () => void;
+  onFinish?: () => 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');
+    });
+  };
+
+  return (
+    <>
+      <Drawer
+        title={formatMessage({ id: 'page.route.data_loader.import_panel' })}
+        width={480}
+        visible={true}
+        onClose={onClose}
+        footer={
+          <div
+            style={{
+              display: state === 'result' ? 'none' : 'flex',
+              justifyContent: 'space-between',
+            }}
+          >
+            <Button onClick={onClose}>{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} hideRequiredMark>
+            <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?.();

Review Comment:
   indeed



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@apisix.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [apisix-dashboard] bzp2010 commented on a diff in pull request #2480: feat: support data loader in frontend

Posted by GitBox <gi...@apache.org>.
bzp2010 commented on code in PR #2480:
URL: https://github.com/apache/apisix-dashboard/pull/2480#discussion_r905705458


##########
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, { 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?: () => void;
+  onFinish?: () => 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');
+    });
+  };
+
+  return (
+    <>
+      <Drawer
+        title={formatMessage({ id: 'page.route.data_loader.import_panel' })}
+        width={480}
+        visible={true}
+        onClose={onClose}
+        footer={
+          <div
+            style={{
+              display: state === 'result' ? 'none' : 'flex',
+              justifyContent: 'space-between',
+            }}
+          >
+            <Button onClick={onClose}>{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} hideRequiredMark>
+            <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?.();

Review Comment:
   improved



##########
web/cypress/integration/route/data-loader-import.spec.js:
##########
@@ -0,0 +1,198 @@
+/*
+ * 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 = {
+    route_name_0: 'route_name_0',
+    route_name_1: 'route_name_1',
+    upstream_node0_host_0: '1.1.1.1',
+    upstream_node0_host_1: '2.2.2.2',
+    importErrorMsg: 'required file type is .yaml, .yml or .json but got: .txt',
+    uploadRouteFiles: [
+      '../../../api/test/testdata/import/default.json',
+      '../../../api/test/testdata/import/default.yaml',
+      'import-error.txt',
+    ],
+    // Note: export file's name will be end of a timestamp
+    jsonMask: 'cypress/downloads/*.json',
+    yamlMask: 'cypress/downloads/*.yaml',
+    port: '80',
+    weight: 1,
+    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();
+
+    // 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();
+
+    // 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();
+
+    // check result
+    cy.get(selector.listTbody).get(selector.listRow).should('have.length', 5);
+
+    // remove route
+    /**/
+  });
+
+  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();

Review Comment:
   added



##########
web/cypress/integration/route/data-loader-import.spec.js:
##########
@@ -0,0 +1,198 @@
+/*
+ * 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 = {
+    route_name_0: 'route_name_0',
+    route_name_1: 'route_name_1',
+    upstream_node0_host_0: '1.1.1.1',
+    upstream_node0_host_1: '2.2.2.2',
+    importErrorMsg: 'required file type is .yaml, .yml or .json but got: .txt',
+    uploadRouteFiles: [
+      '../../../api/test/testdata/import/default.json',
+      '../../../api/test/testdata/import/default.yaml',
+      'import-error.txt',
+    ],
+    // Note: export file's name will be end of a timestamp
+    jsonMask: 'cypress/downloads/*.json',
+    yamlMask: 'cypress/downloads/*.yaml',
+    port: '80',
+    weight: 1,
+    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();
+
+    // 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();
+
+    // 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();
+
+    // check result
+    cy.get(selector.listTbody).get(selector.listRow).should('have.length', 5);
+
+    // remove route
+    /**/

Review Comment:
   removed



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@apisix.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [apisix-dashboard] Baoyuantop commented on a diff in pull request #2480: feat: support data loader in frontend

Posted by GitBox <gi...@apache.org>.
Baoyuantop commented on code in PR #2480:
URL: https://github.com/apache/apisix-dashboard/pull/2480#discussion_r905707355


##########
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, { 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?: () => void;
+  onFinish?: () => 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) => {

Review Comment:
   It feels better to add catch to prevent application exceptions🤔



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@apisix.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [apisix-dashboard] bzp2010 commented on a diff in pull request #2480: feat: support data loader in frontend

Posted by GitBox <gi...@apache.org>.
bzp2010 commented on code in PR #2480:
URL: https://github.com/apache/apisix-dashboard/pull/2480#discussion_r905747065


##########
web/src/pages/Route/components/DataLoader/Import.tsx:
##########
@@ -103,18 +103,20 @@ const DataLoaderImport: React.FC<Props> = (props) => {
     });
     formData.append('file', uploadFileList[0]);
 
-    importRoutes(formData).then((r) => {
-      let errorNumber = 0;
-      entityNames.forEach((v) => {
-        errorNumber += r.data[v].failed;
-      });
+    importRoutes(formData)
+      .then((r) => {
+        let errorNumber = 0;
+        entityNames.forEach((v) => {
+          errorNumber += r.data[v].failed;
+        });
 
-      setImportResult({
-        success: errorNumber <= 0,
-        data: r.data,
-      });
-      setState('result');
-    });
+        setImportResult({
+          success: errorNumber <= 0,
+          data: r.data,
+        });
+        setState('result');
+      })
+      .catch(console.error);

Review Comment:
   That probably means we don't need to catch and just keep an empty catch function.



##########
web/src/pages/Route/components/DataLoader/Import.tsx:
##########
@@ -103,18 +103,20 @@ const DataLoaderImport: React.FC<Props> = (props) => {
     });
     formData.append('file', uploadFileList[0]);
 
-    importRoutes(formData).then((r) => {
-      let errorNumber = 0;
-      entityNames.forEach((v) => {
-        errorNumber += r.data[v].failed;
-      });
+    importRoutes(formData)
+      .then((r) => {
+        let errorNumber = 0;
+        entityNames.forEach((v) => {
+          errorNumber += r.data[v].failed;
+        });
 
-      setImportResult({
-        success: errorNumber <= 0,
-        data: r.data,
-      });
-      setState('result');
-    });
+        setImportResult({
+          success: errorNumber <= 0,
+          data: r.data,
+        });
+        setState('result');
+      })
+      .catch(console.error);

Review Comment:
   That probably means we don't need to catch and just keep an empty catch function. 🤔



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@apisix.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [apisix-dashboard] SkyeYoung commented on a diff in pull request #2480: feat: support data loader in frontend

Posted by GitBox <gi...@apache.org>.
SkyeYoung commented on code in PR #2480:
URL: https://github.com/apache/apisix-dashboard/pull/2480#discussion_r905749966


##########
web/src/pages/Route/components/DataLoader/Import.tsx:
##########
@@ -103,18 +103,20 @@ const DataLoaderImport: React.FC<Props> = (props) => {
     });
     formData.append('file', uploadFileList[0]);
 
-    importRoutes(formData).then((r) => {
-      let errorNumber = 0;
-      entityNames.forEach((v) => {
-        errorNumber += r.data[v].failed;
-      });
+    importRoutes(formData)
+      .then((r) => {
+        let errorNumber = 0;
+        entityNames.forEach((v) => {
+          errorNumber += r.data[v].failed;
+        });
 
-      setImportResult({
-        success: errorNumber <= 0,
-        data: r.data,
-      });
-      setState('result');
-    });
+        setImportResult({
+          success: errorNumber <= 0,
+          data: r.data,
+        });
+        setState('result');
+      })
+      .catch(console.error);

Review Comment:
   Maybe we can try https://stackoverflow.com/a/41041580 (Not Now)



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@apisix.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [apisix-dashboard] bzp2010 commented on a diff in pull request #2480: feat: support data loader in frontend

Posted by GitBox <gi...@apache.org>.
bzp2010 commented on code in PR #2480:
URL: https://github.com/apache/apisix-dashboard/pull/2480#discussion_r905691143


##########
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, { 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?: () => void;
+  onFinish?: () => 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');
+    });
+  };
+
+  return (
+    <>
+      <Drawer
+        title={formatMessage({ id: 'page.route.data_loader.import_panel' })}
+        width={480}
+        visible={true}
+        onClose={onClose}
+        footer={
+          <div
+            style={{
+              display: state === 'result' ? 'none' : 'flex',
+              justifyContent: 'space-between',
+            }}
+          >
+            <Button onClick={onClose}>{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} hideRequiredMark>

Review Comment:
   ![image](https://user-images.githubusercontent.com/8078418/175457332-5d9d4456-b8a8-4e6e-a242-e4fcb29885d7.png)
   
   The require flag (red asterisk) does not cover all fields and is not elegant, we can hide it, but behind the scenes the check rules are still being enforced and if there is a problem with the data it cannot be submitted.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@apisix.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [apisix-dashboard] bzp2010 commented on pull request #2480: feat: support data loader in frontend

Posted by GitBox <gi...@apache.org>.
bzp2010 commented on PR #2480:
URL: https://github.com/apache/apisix-dashboard/pull/2480#issuecomment-1164163406

   ### Update
   
   A new import result UI was provided. It is like this, the imported entities total/failed/errors are shown.
   
   ![image](https://user-images.githubusercontent.com/8078418/175263708-0c51902c-d99f-4d2e-a986-f9f50ba648a2.png)
   
   ![image](https://user-images.githubusercontent.com/8078418/175263996-da343209-7f93-4d43-9ea5-b59bf11e9fa4.png)
   
   ![image](https://user-images.githubusercontent.com/8078418/175264281-01ac6ffd-9b41-435d-84d5-242771536786.png)
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@apisix.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [apisix-dashboard] SkyeYoung commented on a diff in pull request #2480: feat: support data loader in frontend

Posted by GitBox <gi...@apache.org>.
SkyeYoung commented on code in PR #2480:
URL: https://github.com/apache/apisix-dashboard/pull/2480#discussion_r905748112


##########
web/src/pages/Route/components/DataLoader/Import.tsx:
##########
@@ -103,18 +103,20 @@ const DataLoaderImport: React.FC<Props> = (props) => {
     });
     formData.append('file', uploadFileList[0]);
 
-    importRoutes(formData).then((r) => {
-      let errorNumber = 0;
-      entityNames.forEach((v) => {
-        errorNumber += r.data[v].failed;
-      });
+    importRoutes(formData)
+      .then((r) => {
+        let errorNumber = 0;
+        entityNames.forEach((v) => {
+          errorNumber += r.data[v].failed;
+        });
 
-      setImportResult({
-        success: errorNumber <= 0,
-        data: r.data,
-      });
-      setState('result');
-    });
+        setImportResult({
+          success: errorNumber <= 0,
+          data: r.data,
+        });
+        setState('result');
+      })
+      .catch(console.error);

Review Comment:
   > That probably means we don't need to catch and just keep an empty catch function. 🤔
   
   I think `console.error` can somehow be removed at compile time for the production environment



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@apisix.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [apisix-dashboard] SkyeYoung commented on a diff in pull request #2480: feat: support data loader in frontend

Posted by GitBox <gi...@apache.org>.
SkyeYoung commented on code in PR #2480:
URL: https://github.com/apache/apisix-dashboard/pull/2480#discussion_r905743831


##########
web/src/pages/Route/components/DataLoader/Import.tsx:
##########
@@ -103,18 +103,20 @@ const DataLoaderImport: React.FC<Props> = (props) => {
     });
     formData.append('file', uploadFileList[0]);
 
-    importRoutes(formData).then((r) => {
-      let errorNumber = 0;
-      entityNames.forEach((v) => {
-        errorNumber += r.data[v].failed;
-      });
+    importRoutes(formData)
+      .then((r) => {
+        let errorNumber = 0;
+        entityNames.forEach((v) => {
+          errorNumber += r.data[v].failed;
+        });
 
-      setImportResult({
-        success: errorNumber <= 0,
-        data: r.data,
-      });
-      setState('result');
-    });
+        setImportResult({
+          success: errorNumber <= 0,
+          data: r.data,
+        });
+        setState('result');
+      })
+      .catch(console.error);

Review Comment:
   should we disable `console.error` in production environment?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@apisix.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [apisix-dashboard] bzp2010 commented on a diff in pull request #2480: feat: support data loader in frontend

Posted by GitBox <gi...@apache.org>.
bzp2010 commented on code in PR #2480:
URL: https://github.com/apache/apisix-dashboard/pull/2480#discussion_r905705581


##########
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 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 DataLoaderOpenAPI3;

Review Comment:
   added



##########
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, { 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?: () => void;
+  onFinish?: () => 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');
+    });
+  };
+
+  return (
+    <>
+      <Drawer
+        title={formatMessage({ id: 'page.route.data_loader.import_panel' })}
+        width={480}
+        visible={true}
+        onClose={onClose}
+        footer={
+          <div
+            style={{
+              display: state === 'result' ? 'none' : 'flex',
+              justifyContent: 'space-between',
+            }}
+          >
+            <Button onClick={onClose}>{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} hideRequiredMark>
+            <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?.();
+                  if (props.onFinish) props.onFinish();
+                }}
+              >
+                {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 DataLoaderImport;

Review Comment:
   added



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@apisix.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [apisix-dashboard] bzp2010 commented on a diff in pull request #2480: feat: support data loader in frontend

Posted by GitBox <gi...@apache.org>.
bzp2010 commented on code in PR #2480:
URL: https://github.com/apache/apisix-dashboard/pull/2480#discussion_r905706851


##########
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, { 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?: () => void;
+  onFinish?: () => 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');
+    });
+  };
+
+  return (
+    <>
+      <Drawer
+        title={formatMessage({ id: 'page.route.data_loader.import_panel' })}
+        width={480}
+        visible={true}
+        onClose={onClose}
+        footer={
+          <div
+            style={{
+              display: state === 'result' ? 'none' : 'flex',
+              justifyContent: 'space-between',
+            }}
+          >
+            <Button onClick={onClose}>{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} hideRequiredMark>

Review Comment:
   I think it is enough to solve it by implicit check and default value.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@apisix.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [apisix-dashboard] anldrms commented on a diff in pull request #2480: feat: support data loader in frontend

Posted by GitBox <gi...@apache.org>.
anldrms commented on code in PR #2480:
URL: https://github.com/apache/apisix-dashboard/pull/2480#discussion_r907296529


##########
web/src/pages/Route/locales/tr-TR.ts:
##########
@@ -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',

Review Comment:
   https://github.com/apache/apisix-dashboard/pull/2487



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@apisix.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org