You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by su...@apache.org on 2021/04/09 04:33:07 UTC

[apisix-dashboard] branch master updated: feat: add basic-auth UI Form (#1718)

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

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


The following commit(s) were added to refs/heads/master by this push:
     new 82ab89e  feat: add basic-auth UI Form (#1718)
82ab89e is described below

commit 82ab89e2f208eeca1e6f03d9dae885ee7e4b19d3
Author: litesun <su...@apache.org>
AuthorDate: Fri Apr 9 12:31:36 2021 +0800

    feat: add basic-auth UI Form (#1718)
---
 web/cypress/fixtures/selector.json                 |  5 +-
 .../plugin/create-delete-in-drawer-plugin.spec.js  | 24 +++++-
 .../create-edit-delete-plugin-template.spec.js     | 13 +--
 .../create-edit-duplicate-delete-route.spec.js     |  4 +
 .../service/create-edit-delete-service.spec.js     |  3 +
 web/cypress/support/commands.js                    | 36 ++++++---
 web/src/components/Plugin/PluginDetail.tsx         | 93 +++++++++++++++-------
 .../Plugin/{typing.d.ts => UI/basic-auth.tsx}      | 53 ++++++++----
 .../components/Plugin/{typing.d.ts => UI/index.ts} | 19 +----
 .../Plugin/{typing.d.ts => UI/plugin.tsx}          | 36 ++++++---
 web/src/components/Plugin/typing.d.ts              |  2 +-
 web/src/locales/en-US/component.ts                 |  4 +-
 web/src/locales/zh-CN/component.ts                 |  4 +-
 13 files changed, 198 insertions(+), 98 deletions(-)

diff --git a/web/cypress/fixtures/selector.json b/web/cypress/fixtures/selector.json
index 71d4b58..0236d8e 100644
--- a/web/cypress/fixtures/selector.json
+++ b/web/cypress/fixtures/selector.json
@@ -82,5 +82,8 @@
   "twentyPerPage": "[title=\"20 / page\"]",
   "pageList": ".ant-table-pagination-right",
   "pageTwo": ".ant-pagination-item-2",
-  "pageTwoActived": ".ant-pagination-item-2.ant-pagination-item-active"
+  "pageTwoActived": ".ant-pagination-item-2.ant-pagination-item-active",
+  "selectDropdown": ".ant-select-dropdown",
+  "codeMirrorMode": "[data-cy='code-mirror-mode']",
+  "selectJSON":".ant-select-dropdown [label=JSON]"
 }
diff --git a/web/cypress/integration/plugin/create-delete-in-drawer-plugin.spec.js b/web/cypress/integration/plugin/create-delete-in-drawer-plugin.spec.js
index a6e7b4c..272b06b 100644
--- a/web/cypress/integration/plugin/create-delete-in-drawer-plugin.spec.js
+++ b/web/cypress/integration/plugin/create-delete-in-drawer-plugin.spec.js
@@ -32,7 +32,17 @@ context('Delete Plugin List with the Drawer', () => {
     cy.contains('Create').click();
 
     cy.contains(this.data.basicAuthPlugin).parents(this.domSelector.pluginCardBordered).within(() => {
-      cy.get('button').click({ force: true });
+      cy.get('button').click({
+        force: true
+      });
+    });
+
+    cy.get(this.domSelector.codeMirrorMode).invoke('text').then(text => {
+      if (text === 'Form') {
+        cy.get(this.domSelector.codeMirrorMode).click();
+        cy.get(this.domSelector.selectDropdown).should('be.visible');
+        cy.get(this.domSelector.selectJSON).click();
+      }
     });
 
     cy.get(this.domSelector.drawer).should('be.visible').within(() => {
@@ -41,15 +51,21 @@ context('Delete Plugin List with the Drawer', () => {
     });
 
     cy.contains('button', 'Submit').click();
-    cy.get(this.domSelector.drawer, { timeout }).should('not.exist');
+    cy.get(this.domSelector.drawer, {
+      timeout
+    }).should('not.exist');
   });
 
   it('should delete the plugin with the drawer', function () {
     cy.visit('/plugin/list');
     cy.get(this.domSelector.refresh).click();
     cy.contains('button', 'Configure').click();
-    cy.get(this.domSelector.drawerFooter).contains('button', 'Delete').click({ force: true });
-    cy.contains('button', 'Confirm').click({ force: true });
+    cy.get(this.domSelector.drawerFooter).contains('button', 'Delete').click({
+      force: true
+    });
+    cy.contains('button', 'Confirm').click({
+      force: true
+    });
     cy.get(this.domSelector.empty).should('be.visible');
   });
 });
diff --git a/web/cypress/integration/pluginTemplate/create-edit-delete-plugin-template.spec.js b/web/cypress/integration/pluginTemplate/create-edit-delete-plugin-template.spec.js
index d3ea3c4..f41f54a 100644
--- a/web/cypress/integration/pluginTemplate/create-edit-delete-plugin-template.spec.js
+++ b/web/cypress/integration/pluginTemplate/create-edit-delete-plugin-template.spec.js
@@ -39,13 +39,14 @@ context('Create Configure and Delete PluginTemplate', () => {
       force: true
     });
     cy.focused(this.domSelector.drawer).should('exist');
-    cy.get(this.domSelector.drawer, {
-      timeout
-    }).within(() => {
-      cy.get(this.domSelector.disabledSwitcher).click({
-        force: true,
-      });
+
+    cy.get(this.domSelector.codeMirrorMode).click();
+    cy.get(this.domSelector.selectDropdown).should('be.visible');
+    cy.get(this.domSelector.selectJSON).click();
+    cy.get(this.domSelector.disabledSwitcher).click({
+      force: true,
     });
+
     cy.contains('Submit').click();
     cy.contains('Next').click();
     cy.contains('Submit').click();
diff --git a/web/cypress/integration/route/create-edit-duplicate-delete-route.spec.js b/web/cypress/integration/route/create-edit-duplicate-delete-route.spec.js
index 85aad6f..048d489 100644
--- a/web/cypress/integration/route/create-edit-duplicate-delete-route.spec.js
+++ b/web/cypress/integration/route/create-edit-duplicate-delete-route.spec.js
@@ -84,6 +84,10 @@ context('Create and Delete Route', () => {
       cy.get(this.domSelector.checkedSwitcher).should('exist');
     });
 
+    cy.get(this.domSelector.codeMirrorMode).click();
+    cy.get(this.domSelector.selectDropdown).should('be.visible');
+    cy.get(this.domSelector.selectJSON).click();
+
     cy.contains('button', 'Submit').click();
     cy.get(this.domSelector.drawer, { timeout }).should('not.exist');
 
diff --git a/web/cypress/integration/service/create-edit-delete-service.spec.js b/web/cypress/integration/service/create-edit-delete-service.spec.js
index 52b63c3..8c7fa0a 100644
--- a/web/cypress/integration/service/create-edit-delete-service.spec.js
+++ b/web/cypress/integration/service/create-edit-delete-service.spec.js
@@ -47,6 +47,9 @@ context('Create and Delete Service ', () => {
       cy.get(this.domSelector.checkedSwitcher).should('exist');
     });
 
+    cy.get(this.domSelector.codeMirrorMode).click();
+    cy.get(this.domSelector.selectDropdown).should('be.visible');
+    cy.get(this.domSelector.selectJSON).click();
     cy.contains('button', 'Submit').click();
     cy.get(this.domSelector.drawer, { timeout }).should('not.exist');
 
diff --git a/web/cypress/support/commands.js b/web/cypress/support/commands.js
index 59e8ad5..d443339 100644
--- a/web/cypress/support/commands.js
+++ b/web/cypress/support/commands.js
@@ -34,16 +34,19 @@ Cypress.Commands.add('login', () => {
 
 Cypress.Commands.add('configurePlugins', (cases) => {
   const timeout = 300;
-  const domSelectors = {
+  const domSelector = {
     name: '[data-cy-plugin-name]',
     parents: '.ant-card-bordered',
     drawer_wrap: '.ant-drawer-content-wrapper',
     drawer: '.ant-drawer-content',
     switch: '#disable',
     close: '.anticon-close',
+    selectDropdown: '.ant-select-dropdown',
+    codeMirrorMode: '[data-cy="code-mirror-mode"]',
+    selectJSON: '.ant-select-dropdown [label=JSON]'
   };
 
-  cy.get(domSelectors.name, { timeout }).then(function (cards) {
+  cy.get(domSelector.name, { timeout }).then(function (cards) {
     [...cards].forEach((card) => {
       const name = card.innerText;
       const pluginCases = cases[name] || [];
@@ -54,7 +57,7 @@ Cypress.Commands.add('configurePlugins', (cases) => {
         }
 
         cy.contains(name)
-          .parents(domSelectors.parents)
+          .parents(domSelector.parents)
           .within(() => {
             cy.contains('Enable').click({
               force: true,
@@ -62,9 +65,9 @@ Cypress.Commands.add('configurePlugins', (cases) => {
           });
 
         // NOTE: wait for the Drawer to appear on the DOM
-        cy.focused(domSelectors.drawer).should('exist');
-        cy.get(domSelectors.drawer, { timeout }).within(() => {
-          cy.get(domSelectors.switch).click({
+        cy.focused(domSelector.drawer).should('exist');
+        cy.get(domSelector.drawer, { timeout }).within(() => {
+          cy.get(domSelector.switch).click({
             force: true,
           });
         });
@@ -73,26 +76,35 @@ Cypress.Commands.add('configurePlugins', (cases) => {
           if (codemirror) {
             codemirror.setValue(JSON.stringify(data));
           }
-          cy.get(domSelectors.drawer).should('exist');
-          cy.get(domSelectors.drawer, { timeout }).within(() => {
+          cy.get(domSelector.drawer).should('exist');
+
+          cy.get(domSelector.codeMirrorMode).invoke('text').then(text => {
+            if (text === 'Form') {
+              cy.get(domSelector.codeMirrorMode).click();
+              cy.get(domSelector.selectDropdown).should('be.visible');
+              cy.get(domSelector.selectJSON).click();
+            }
+          });
+
+          cy.get(domSelector.drawer, { timeout }).within(() => {
             cy.contains('Submit').click({
               force: true,
             });
-            cy.get(domSelectors.drawer).should('not.exist');
+            cy.get(domSelector.drawer).should('not.exist');
           });
         });
 
         if (shouldValid === true) {
-          cy.get(domSelectors.drawer).should('not.exist');
+          cy.get(domSelector.drawer).should('not.exist');
         } else if (shouldValid === false) {
           cy.get(this.domSelector.notification).should('contain', 'Invalid plugin data');
 
-          cy.get(domSelectors.close).should('be.visible').click({
+          cy.get(domSelector.close).should('be.visible').click({
             force: true,
             multiple: true,
           });
 
-          cy.get(domSelectors.drawer, { timeout })
+          cy.get(domSelector.drawer, { timeout })
             .invoke('show')
             .within(() => {
               cy.contains('Cancel').click({
diff --git a/web/src/components/Plugin/PluginDetail.tsx b/web/src/components/Plugin/PluginDetail.tsx
index 8c3eac7..8dbdfe8 100644
--- a/web/src/components/Plugin/PluginDetail.tsx
+++ b/web/src/components/Plugin/PluginDetail.tsx
@@ -38,6 +38,7 @@ import addFormats from 'ajv-formats';
 
 import { fetchSchema } from './service';
 import { json2yaml, yaml2json } from '../../helpers';
+import { PluginForm, PLUGIN_UI_LIST } from './UI';
 
 type Props = {
   name: string;
@@ -94,8 +95,10 @@ const PluginDetail: React.FC<Props> = ({
   enum codeMirrorModeList {
     JSON = 'JSON',
     YAML = 'YAML',
+    UIForm = 'Form'
   }
   const [form] = Form.useForm();
+  const [UIForm] = Form.useForm();
   const ref = useRef<any>(null);
   const data = initialData[name] || {};
   const pluginType = pluginList.find((item) => item.name === name)?.type;
@@ -107,11 +110,19 @@ const PluginDetail: React.FC<Props> = ({
     { label: codeMirrorModeList.YAML, value: codeMirrorModeList.YAML },
   ];
 
+  if (PLUGIN_UI_LIST.includes(name)) {
+    modeOptions.push({ label: codeMirrorModeList.UIForm, value: codeMirrorModeList.UIForm });
+  }
+
   useEffect(() => {
     form.setFieldsValue({
       disable: initialData[name] && !initialData[name].disable,
       scope: 'global',
     });
+    if (PLUGIN_UI_LIST.includes(name)) {
+      setCodeMirrorMode(codeMirrorModeList.UIForm);
+      UIForm.setFieldsValue(initialData[name]);
+    };
   }, []);
 
   const validateData = (pluginName: string, value: PluginComponent.Data) => {
@@ -161,23 +172,30 @@ const PluginDetail: React.FC<Props> = ({
   const handleModeChange = (value: PluginComponent.CodeMirrorMode) => {
     switch (value) {
       case codeMirrorModeList.JSON: {
-        const { data: yamlData, error } = yaml2json(ref.current.editor.getValue(), true);
-
-        if (error) {
-          notification.error({
-            message: 'Invalid Yaml data',
-          });
-          return;
+        if (codeMirrorMode === codeMirrorModeList.YAML) {
+          const { data: yamlData, error } = yaml2json(ref.current.editor.getValue(), true);
+          if (error) {
+            notification.error({
+              message: 'Invalid Yaml data',
+            });
+            return;
+          }
+          ref.current.editor.setValue(
+            js_beautify(yamlData, {
+              indent_size: 2,
+            }),
+          );
+        } else {
+          ref.current.editor.setValue(
+            js_beautify(JSON.stringify(UIForm.getFieldsValue()), {
+              indent_size: 2,
+            }),
+          );
         }
-        ref.current.editor.setValue(
-          js_beautify(yamlData, {
-            indent_size: 2,
-          }),
-        );
         break;
       }
       case codeMirrorModeList.YAML: {
-        const { data: jsonData, error } = json2yaml(ref.current.editor.getValue());
+        const { data: jsonData, error } = json2yaml(codeMirrorMode === codeMirrorModeList.JSON ? ref.current.editor.getValue() : JSON.stringify(UIForm.getFieldsValue()));
 
         if (error) {
           notification.error({
@@ -188,11 +206,28 @@ const PluginDetail: React.FC<Props> = ({
         ref.current.editor.setValue(jsonData);
         break;
       }
+
+      case codeMirrorModeList.UIForm: {
+        if (codeMirrorMode === codeMirrorModeList.JSON) {
+          UIForm.setFieldsValue(JSON.parse(ref.current.editor.getValue()));
+        } else {
+          const { data: yamlData, error } = yaml2json(ref.current.editor.getValue(), true);
+          if (error) {
+            notification.error({
+              message: 'Invalid Yaml data',
+            });
+            return;
+          }
+          UIForm.setFieldsValue(JSON.parse(yamlData));
+        }
+        break;
+      }
       default:
         break;
     }
     setCodeMirrorMode(value);
   };
+
   const formatCodes = () => {
     try {
       if (ref.current) {
@@ -249,10 +284,15 @@ const PluginDetail: React.FC<Props> = ({
                 type="primary"
                 onClick={() => {
                   try {
-                    const editorData =
-                      codeMirrorMode === codeMirrorModeList.JSON
-                        ? JSON.parse(ref.current?.editor.getValue())
-                        : yaml2json(ref.current?.editor.getValue(), false).data;
+                    let editorData;
+                    if (codeMirrorMode === codeMirrorModeList.JSON) {
+                      editorData = JSON.parse(ref.current?.editor.getValue());
+                    } else if (codeMirrorMode === codeMirrorModeList.YAML) {
+                      editorData = yaml2json(ref.current?.editor.getValue(), false).data;
+                    } else {
+                      editorData = UIForm.getFieldsValue();
+                    }
+
                     validateData(name, editorData).then((value) => {
                       onChange({ formData: form.getFieldsValue(), codemirrorData: value });
                     });
@@ -297,11 +337,9 @@ const PluginDetail: React.FC<Props> = ({
         <PageHeader
           title=""
           subTitle={
-            pluginType === 'auth' && schemaType !== 'consumer' ? (
-              <Alert message={`${name} does not require configuration`} type="warning" />
-            ) : (
-              <>Current plugin: {name}</>
-            )
+            pluginType === 'auth' && schemaType !== 'consumer' && (codeMirrorMode !== codeMirrorModeList.UIForm) ? (
+              <Alert message={formatMessage({ id: 'component.global.noConfigurationRequired' })} type="warning" />
+            ) : null
           }
           ghost={false}
           extra={[
@@ -328,12 +366,13 @@ const PluginDetail: React.FC<Props> = ({
               }}
               data-cy='code-mirror-mode'
             ></Select>,
-            <Button type="primary" onClick={formatCodes} key={3}>
+            <Button type="primary" onClick={formatCodes} key={3} disabled={codeMirrorMode === codeMirrorModeList.UIForm}>
               {formatMessage({ id: 'component.global.format' })}
-            </Button>,
+            </Button>
           ]}
         />
-        <CodeMirror
+        {Boolean(codeMirrorMode === codeMirrorModeList.UIForm) && <PluginForm name={name} form={UIForm} renderForm={!(pluginType === 'auth' && schemaType !== 'consumer')} />}
+        <div style={{ display: codeMirrorMode === codeMirrorModeList.UIForm ? 'none' : 'unset' }}><CodeMirror
           ref={(codemirror) => {
             ref.current = codemirror;
             if (codemirror) {
@@ -350,8 +389,8 @@ const PluginDetail: React.FC<Props> = ({
             lineNumbers: true,
             showCursorWhenSelecting: true,
             autofocus: true,
-          }}
-        />
+          }} />
+        </div>
       </Drawer>
     </>
   );
diff --git a/web/src/components/Plugin/typing.d.ts b/web/src/components/Plugin/UI/basic-auth.tsx
similarity index 53%
copy from web/src/components/Plugin/typing.d.ts
copy to web/src/components/Plugin/UI/basic-auth.tsx
index 79dda49..8d95aa0 100644
--- a/web/src/components/Plugin/typing.d.ts
+++ b/web/src/components/Plugin/UI/basic-auth.tsx
@@ -14,21 +14,46 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-declare namespace PluginComponent {
-  type Data = Record<string, any>;
+import React from 'react';
+import type { FormInstance } from 'antd/es/form';
+import { Form, Input } from 'antd';
 
-  type Schema = '' | 'route' | 'consumer';
+type Props = {
+  form: FormInstance;
+  ref?: any;
+};
 
-  type Meta = {
-    name: string;
-    priority: number;
-    schema: Record<string, any>;
-    type: string;
-    version: number;
-    consumer_schema?: Record<string, any>;
-  };
+export const FORM_ITEM_LAYOUT = {
+  labelCol: {
+    span: 4,
+  },
+  wrapperCol: {
+    span: 8
+  },
+};
 
-  type ReferPage = '' | 'route' | 'consumer' | 'service' | 'plugin';
-
-  type CodeMirrorMode = 'JSON' | 'YAML';
+const BasicAuth: React.FC<Props> = ({ form }) => {
+  return (
+    <Form
+      form={form}
+      {...FORM_ITEM_LAYOUT}
+    >
+      <Form.Item
+        label="username"
+        name="username"
+        required
+      >
+        <Input></Input>
+      </Form.Item>
+      <Form.Item
+        label="password"
+        name="password"
+        required
+      >
+        <Input></Input>
+      </Form.Item>
+    </Form>
+  );
 }
+
+export default BasicAuth;
diff --git a/web/src/components/Plugin/typing.d.ts b/web/src/components/Plugin/UI/index.ts
similarity index 66%
copy from web/src/components/Plugin/typing.d.ts
copy to web/src/components/Plugin/UI/index.ts
index 79dda49..8da266b 100644
--- a/web/src/components/Plugin/typing.d.ts
+++ b/web/src/components/Plugin/UI/index.ts
@@ -14,21 +14,4 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-declare namespace PluginComponent {
-  type Data = Record<string, any>;
-
-  type Schema = '' | 'route' | 'consumer';
-
-  type Meta = {
-    name: string;
-    priority: number;
-    schema: Record<string, any>;
-    type: string;
-    version: number;
-    consumer_schema?: Record<string, any>;
-  };
-
-  type ReferPage = '' | 'route' | 'consumer' | 'service' | 'plugin';
-
-  type CodeMirrorMode = 'JSON' | 'YAML';
-}
+export { PLUGIN_UI_LIST, PluginForm } from './plugin';
diff --git a/web/src/components/Plugin/typing.d.ts b/web/src/components/Plugin/UI/plugin.tsx
similarity index 55%
copy from web/src/components/Plugin/typing.d.ts
copy to web/src/components/Plugin/UI/plugin.tsx
index 79dda49..f13b31a 100644
--- a/web/src/components/Plugin/typing.d.ts
+++ b/web/src/components/Plugin/UI/plugin.tsx
@@ -14,21 +14,31 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-declare namespace PluginComponent {
-  type Data = Record<string, any>;
+import React from 'react';
+import { FormInstance } from 'antd/es/form';
+import { Empty } from 'antd';
+import { useIntl } from 'umi';
 
-  type Schema = '' | 'route' | 'consumer';
+import BasicAuth from './basic-auth'
 
-  type Meta = {
-    name: string;
-    priority: number;
-    schema: Record<string, any>;
-    type: string;
-    version: number;
-    consumer_schema?: Record<string, any>;
-  };
+type Props = {
+  name: string,
+  form: FormInstance,
+  renderForm: boolean
+}
+
+export const PLUGIN_UI_LIST = ['basic-auth',];
+
+export const PluginForm: React.FC<Props> = ({ name, renderForm, form }) => {
+
+  const { formatMessage } = useIntl();
 
-  type ReferPage = '' | 'route' | 'consumer' | 'service' | 'plugin';
+  if (!renderForm) { return <Empty description={formatMessage({ id: 'component.global.noConfigurationRequired' })} /> };
 
-  type CodeMirrorMode = 'JSON' | 'YAML';
+  switch (name) {
+    case 'basic-auth':
+      return <BasicAuth form={form} />
+    default:
+      return null;
+  }
 }
diff --git a/web/src/components/Plugin/typing.d.ts b/web/src/components/Plugin/typing.d.ts
index 79dda49..c92b882 100644
--- a/web/src/components/Plugin/typing.d.ts
+++ b/web/src/components/Plugin/typing.d.ts
@@ -30,5 +30,5 @@ declare namespace PluginComponent {
 
   type ReferPage = '' | 'route' | 'consumer' | 'service' | 'plugin';
 
-  type CodeMirrorMode = 'JSON' | 'YAML';
+  type CodeMirrorMode = 'JSON' | 'YAML'| 'Form';
 }
diff --git a/web/src/locales/en-US/component.ts b/web/src/locales/en-US/component.ts
index 55b6a8a..53e9496 100644
--- a/web/src/locales/en-US/component.ts
+++ b/web/src/locales/en-US/component.ts
@@ -72,5 +72,7 @@ export default {
   'component.user.loginByPassword': 'Username & Password',
   'component.user.login': 'Login',
 
-  'component.document': 'Document'
+  'component.document': 'Document',
+
+  'component.global.noConfigurationRequired': 'No configuration required',
 };
diff --git a/web/src/locales/zh-CN/component.ts b/web/src/locales/zh-CN/component.ts
index f4171ec..12b29e1 100644
--- a/web/src/locales/zh-CN/component.ts
+++ b/web/src/locales/zh-CN/component.ts
@@ -68,5 +68,7 @@ export default {
   'component.user.loginByPassword': '账号密码登录',
   'component.user.login': '登录',
 
-  'component.document': '操作手册'
+  'component.document': '操作手册',
+
+  'component.global.noConfigurationRequired': '无需配置',
 };