diff --git a/web/cypress/fixtures/selector.json b/web/cypress/fixtures/selector.json index 71d4b58c51..0236d8e1e2 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 a6e7b4cbdf..272b06b5a9 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 d3ea3c4558..f41f54a24e 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 85aad6f2e7..048d489ff6 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 52b63c3f99..8c7fa0ac17 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 59e8ad5b0d..d4433390d3 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 8c3eac7220..8dbdfe8376 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 = ({ enum codeMirrorModeList { JSON = 'JSON', YAML = 'YAML', + UIForm = 'Form' } const [form] = Form.useForm(); + const [UIForm] = Form.useForm(); const ref = useRef(null); const data = initialData[name] || {}; const pluginType = pluginList.find((item) => item.name === name)?.type; @@ -107,11 +110,19 @@ const PluginDetail: React.FC = ({ { 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 = ({ 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 = ({ 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 = ({ 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 = ({ - ) : ( - <>Current plugin: {name} - ) + pluginType === 'auth' && schemaType !== 'consumer' && (codeMirrorMode !== codeMirrorModeList.UIForm) ? ( + + ) : null } ghost={false} extra={[ @@ -328,12 +366,13 @@ const PluginDetail: React.FC = ({ }} data-cy='code-mirror-mode' >, - , + ]} /> - } +
{ ref.current = codemirror; if (codemirror) { @@ -350,8 +389,8 @@ const PluginDetail: React.FC = ({ lineNumbers: true, showCursorWhenSelecting: true, autofocus: true, - }} - /> + }} /> +
); diff --git a/web/src/components/Plugin/UI/basic-auth.tsx b/web/src/components/Plugin/UI/basic-auth.tsx new file mode 100644 index 0000000000..8d95aa0bdd --- /dev/null +++ b/web/src/components/Plugin/UI/basic-auth.tsx @@ -0,0 +1,59 @@ +/* + * 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 type { FormInstance } from 'antd/es/form'; +import { Form, Input } from 'antd'; + +type Props = { + form: FormInstance; + ref?: any; +}; + +export const FORM_ITEM_LAYOUT = { + labelCol: { + span: 4, + }, + wrapperCol: { + span: 8 + }, +}; + +const BasicAuth: React.FC = ({ form }) => { + return ( +
+ + + + + + +
+ ); +} + +export default BasicAuth; diff --git a/web/src/components/Plugin/UI/index.ts b/web/src/components/Plugin/UI/index.ts new file mode 100644 index 0000000000..8da266b584 --- /dev/null +++ b/web/src/components/Plugin/UI/index.ts @@ -0,0 +1,17 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { PLUGIN_UI_LIST, PluginForm } from './plugin'; diff --git a/web/src/components/Plugin/UI/plugin.tsx b/web/src/components/Plugin/UI/plugin.tsx new file mode 100644 index 0000000000..f13b31ae16 --- /dev/null +++ b/web/src/components/Plugin/UI/plugin.tsx @@ -0,0 +1,44 @@ +/* + * 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 { FormInstance } from 'antd/es/form'; +import { Empty } from 'antd'; +import { useIntl } from 'umi'; + +import BasicAuth from './basic-auth' + +type Props = { + name: string, + form: FormInstance, + renderForm: boolean +} + +export const PLUGIN_UI_LIST = ['basic-auth',]; + +export const PluginForm: React.FC = ({ name, renderForm, form }) => { + + const { formatMessage } = useIntl(); + + if (!renderForm) { return }; + + switch (name) { + case 'basic-auth': + return + default: + return null; + } +} diff --git a/web/src/components/Plugin/typing.d.ts b/web/src/components/Plugin/typing.d.ts index 79dda497ce..c92b882f56 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 55b6a8a4ea..53e949695a 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 f4171ec256..12b29e1977 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': '无需配置', };