diff --git a/web/cypress/integration/consumer/create-with-fault-injection-form.spec.js b/web/cypress/integration/consumer/create-with-fault-injection-form.spec.js new file mode 100644 index 0000000000..899180ab84 --- /dev/null +++ b/web/cypress/integration/consumer/create-with-fault-injection-form.spec.js @@ -0,0 +1,101 @@ +/* + * 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. + */ +/* eslint-disable no-undef */ + +context('Create and Delete Consumer', () => { + beforeEach(() => { + cy.login(); + + cy.fixture('selector.json').as('domSelector'); + cy.fixture('data.json').as('data'); + }); + + const selector = { + abort_http_status: "#abort_http_status", + delay_duration: "#delay_duration", + } + + const data = { + time: 200, + } + + it('creates consumer with fault-injection form', function () { + cy.visit('/'); + cy.contains('Consumer').click(); + cy.get(this.domSelector.empty).should('be.visible'); + cy.contains('Create').click(); + // basic information + cy.get(this.domSelector.username).type(this.data.consumerName); + cy.get(this.domSelector.description).type(this.data.description); + cy.contains('Next').click(); + + // config auth plugin + cy.contains(this.domSelector.pluginCard, 'key-auth').within(() => { + cy.contains('Enable').click({ force: true }); + }); + cy.focused(this.domSelector.drawer).should('exist'); + cy.get(this.domSelector.disabledSwitcher).click().should('have.class', 'ant-switch-checked'); + // edit codemirror + cy.get(this.domSelector.codeMirror) + .first() + .then((editor) => { + editor[0].CodeMirror.setValue( + JSON.stringify({ + key: 'test', + }), + ); + cy.contains('button', 'Submit').click(); + }); + + cy.contains(this.domSelector.pluginCard, 'fault-injection').within(() => { + cy.contains('Enable').click({ + force: true, + }); + }); + + cy.focused(this.domSelector.drawer).should('exist'); + // config proxy-mirror form + cy.get(this.domSelector.drawer).within(() => { + cy.contains('Submit').click({ + force: true, + }); + }); + cy.get(this.domSelector.notification).should('contain', 'Invalid plugin data'); + cy.get(this.domSelector.notificationCloseIcon).click().should('not.exist'); + + cy.get(selector.abort_http_status).type(data.time); + cy.get(selector.delay_duration).type(data.time); + cy.get(this.domSelector.drawer).within(() => { + cy.contains('Submit').click({ + force: true, + }); + }); + cy.get(this.domSelector.drawer).should('not.exist'); + + cy.contains('button', 'Next').click(); + cy.contains('button', 'Submit').click(); + cy.get(this.domSelector.notification).should('contain', this.data.createConsumerSuccess); + cy.get(this.domSelector.notificationCloseIcon).click().should('not.exist'); + }); + + it('delete the consumer', function () { + cy.visit('/consumer/list'); + cy.contains(this.data.consumerName).should('be.visible').siblings().contains('Delete').click(); + cy.contains('button', 'Confirm').click(); + cy.get(this.domSelector.notification).should('contain', this.data.deleteConsumerSuccess); + }); +}); diff --git a/web/cypress/integration/route/create-route-with-fault-injection-form.spec.js b/web/cypress/integration/route/create-route-with-fault-injection-form.spec.js new file mode 100644 index 0000000000..dbfd07e5ee --- /dev/null +++ b/web/cypress/integration/route/create-route-with-fault-injection-form.spec.js @@ -0,0 +1,101 @@ +/* + * 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. + */ +/* eslint-disable no-undef */ + +context('Create and delete route with fault-injection form', () => { + const selector = { + abort_http_status: "#abort_http_status", + delay_duration: "#delay_duration", + } + + beforeEach(() => { + cy.login(); + + cy.fixture('selector.json').as('domSelector'); + cy.fixture('data.json').as('data'); + }); + + it('should create route with fault-injection form', function () { + cy.visit('/'); + cy.contains('Route').click(); + cy.get(this.domSelector.empty).should('be.visible'); + cy.contains('Create').click(); + cy.contains('Next').click().click(); + cy.get(this.domSelector.name).type('routeName'); + cy.get(this.domSelector.description).type('desc'); + cy.contains('Next').click(); + + cy.get(this.domSelector.nodes_0_host).type('127.0.0.1'); + cy.contains('Next').click(); + + // config fault-injection plugin + cy.contains('fault-injection').parents(this.domSelector.pluginCardBordered).within(() => { + cy.get('button').click({ + force: true + }); + }); + + cy.get(this.domSelector.drawer).should('be.visible').within(() => { + cy.get(this.domSelector.disabledSwitcher).click(); + cy.get(this.domSelector.checkedSwitcher).should('exist'); + }); + + // config fault-injection form + cy.get(this.domSelector.drawer).within(() => { + cy.contains('Submit').click({ + force: true, + }); + }); + cy.get(this.domSelector.notification).should('contain', 'Invalid plugin data'); + cy.get(this.domSelector.notificationCloseIcon).click().should('not.exist'); + + cy.get(selector.abort_http_status).type(200); + cy.get(selector.delay_duration).type(200); + cy.get(this.domSelector.drawer).within(() => { + cy.contains('Submit').click({ + force: true, + }); + }); + cy.get(this.domSelector.drawer).should('not.exist'); + + cy.contains('button', 'Next').click(); + cy.contains('button', 'Submit').click(); + cy.contains(this.data.submitSuccess); + + // back to route list page + cy.contains('Goto List').click(); + cy.url().should('contains', 'routes/list'); + }); + + it('should delete the route', function () { + cy.visit('/routes/list'); + const { + domSelector, + data + } = this; + + cy.get(domSelector.name).clear().type('routeName'); + cy.contains('Search').click(); + cy.contains('routeName').siblings().contains('More').click(); + cy.contains('Delete').click(); + cy.get(domSelector.deleteAlert).should('be.visible').within(() => { + cy.contains('OK').click(); + }); + cy.get(domSelector.notification).should('contain', data.deleteRouteSuccess); + cy.get(domSelector.notificationCloseIcon).click(); + }); +}); diff --git a/web/src/components/Plugin/UI/fault-injection.tsx b/web/src/components/Plugin/UI/fault-injection.tsx new file mode 100644 index 0000000000..ec6a7f2cdc --- /dev/null +++ b/web/src/components/Plugin/UI/fault-injection.tsx @@ -0,0 +1,201 @@ +/* + * 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 { Button, Form, Input, InputNumber, Select } from 'antd'; +import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; +import { useIntl } from 'umi'; + +type Props = { + form: FormInstance; +}; + +const FORM_ITEM_LAYOUT = { + labelCol: { + span: 6, + }, + wrapperCol: { + span: 7, + }, +}; + +const FORM_LIST_LAYOUT = { + labelCol: { + span: 6, + }, + wrapperCol: { + span: 10, + }, +}; + +const FORM_ITEM_WITHOUT_LABEL = { + wrapperCol: { + sm: { span: 10, offset: 6 }, + }, +}; + +const FaultInjection: React.FC = ({ form }) => { + const { formatMessage } = useIntl() + + return ( +
+ + + + + + + + + + + + {(fields, { add, remove }) => { + return ( +
+ {fields.map((field, index) => ( + + + + + {fields.length > 1 ? ( + { + remove(field.name); + }} + /> + ) : null} + + ))} + { + + + + } +
+ ); + }} +
+ + + + + + + + + + {(fields, { add, remove }) => { + return ( +
+ {fields.map((field, index) => ( + + + + + {fields.length > 1 ? ( + { + remove(field.name); + }} + /> + ) : null} + + ))} + { + + + + } +
+ ); + }} +
+
+ ); +} + +export default FaultInjection; diff --git a/web/src/components/Plugin/UI/plugin.tsx b/web/src/components/Plugin/UI/plugin.tsx index 79a2df5754..7cfa04a90f 100644 --- a/web/src/components/Plugin/UI/plugin.tsx +++ b/web/src/components/Plugin/UI/plugin.tsx @@ -21,6 +21,7 @@ import { useIntl } from 'umi'; import BasicAuth from './basic-auth'; import LimitConn from './limit-conn'; +import FaultInjection from './fault-injection'; type Props = { name: string, @@ -28,7 +29,7 @@ type Props = { renderForm: boolean } -export const PLUGIN_UI_LIST = ['basic-auth', 'limit-conn']; +export const PLUGIN_UI_LIST = ['basic-auth', 'limit-conn', 'fault-injection']; export const PluginForm: React.FC = ({ name, renderForm, form }) => { @@ -39,6 +40,8 @@ export const PluginForm: React.FC = ({ name, renderForm, form }) => { switch (name) { case 'basic-auth': return + case 'fault-injection': + return case 'limit-conn': return default: diff --git a/web/src/components/Plugin/locales/en-US.ts b/web/src/components/Plugin/locales/en-US.ts index 2b63c63573..75fbe3865c 100644 --- a/web/src/components/Plugin/locales/en-US.ts +++ b/web/src/components/Plugin/locales/en-US.ts @@ -22,6 +22,16 @@ export default { 'component.plugin.pluginTemplate.tip1': '1. When a route already have plugins field configured, the plugins in the plugin template will be merged into it.', 'component.plugin.pluginTemplate.tip2': '2. The same plugin in the plugin template will override one in the plugins', + // fault-injection + 'component.pluginForm.fault-injection.abort.http_status.rules': 'Please input abort.http_status!', + 'component.pluginForm.fault-injection.abort.http_status.tooltip': 'User-specified http code returned to the client.', + 'component.pluginForm.fault-injection.abort.body.tooltip': 'Response data returned to the client. Nginx variable can be used inside, like client addr: $remote_addr.', + 'component.pluginForm.fault-injection.abort.percentage.tooltip': 'Percentage of requests to be aborted.', + 'component.pluginForm.fault-injection.abort.vars.tooltip': 'The rules for executing fault injection will only be executed when the rules are matched. vars is a list of expressions, which is from the lua-resty-expr.', + 'component.pluginForm.fault-injection.delay.duration.rules': 'Please input delay.duration!', + 'component.pluginForm.fault-injection.delay.duration.tooltip': 'Delay time (can be decimal).', + 'component.pluginForm.fault-injection.delay.percentage.tooltip': 'Percentage of requests to be delayed.', + 'component.pluginForm.fault-injection.delay.vars.tooltip': 'Execute the request delay rule, and the request will be delayed only after the rule matches. vars is a list of expressions, which is from the lua-resty-expr.', // limit-conn 'component.pluginForm.limit-conn.conn.tooltip': 'the maximum number of concurrent requests allowed. Requests exceeding this ratio (and below conn + burst) will get delayed(the latency seconds is configured by default_conn_delay) to conform to this threshold.', 'component.pluginForm.limit-conn.burst.tooltip': 'the number of excessive concurrent requests (or connections) allowed to be delayed.', diff --git a/web/src/components/Plugin/locales/zh-CN.ts b/web/src/components/Plugin/locales/zh-CN.ts index c106b2e0dc..d3dc906426 100644 --- a/web/src/components/Plugin/locales/zh-CN.ts +++ b/web/src/components/Plugin/locales/zh-CN.ts @@ -22,6 +22,14 @@ export default { 'component.plugin.pluginTemplate.tip1': '1. 若路由已配置插件,则插件模板数据将与已配置的插件数据合并。', 'component.plugin.pluginTemplate.tip2': '2. 插件模板相同的插件会覆盖掉原有的插件。', + // fault-injection + 'component.pluginForm.fault-injection.abort.http_status.tooltip': '返回给客户端的 http 状态码。', + 'component.pluginForm.fault-injection.abort.body.tooltip': '返回给客户端的响应数据。支持使用 Nginx 变量,如 client addr: $remote_addr。', + 'component.pluginForm.fault-injection.abort.percentage.tooltip': '将被中断的请求占比。', + 'component.pluginForm.fault-injection.abort.vars.tooltip': '执行故障注入的规则,当规则匹配通过后才会执行故障注。vars 是一个表达式的列表,来自 lua-resty-expr。', + 'component.pluginForm.fault-injection.delay.duration.tooltip': '延迟时间,可以指定小数。', + 'component.pluginForm.fault-injection.delay.percentage.tooltip': '将被延迟的请求占比。', + 'component.pluginForm.fault-injection.delay.vars.tooltip': '执行请求延迟的规则,当规则匹配通过后才会延迟请求。vars 是一个表达式列表,来自 lua-resty-expr。', // limit-conn 'component.pluginForm.limit-conn.conn.tooltip': '允许的最大并发请求数。超过 conn 的限制、但是低于 conn + burst 的请求,将被延迟处理。', 'component.pluginForm.limit-conn.burst.tooltip': '允许被延迟处理的并发请求数。',