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 6c3f35c feat: user can skip upstream when select service_id (#1302) 6c3f35c is described below commit 6c3f35ce7f0c6812004546dcf8483432e8bcb65b Author: litesun <su...@apache.org> AuthorDate: Fri Jan 22 23:51:59 2021 +0800 feat: user can skip upstream when select service_id (#1302) * feat: user can skip upstream when select service_id * feat: update code * feat: update upstream * fix: manual input * feat: update CreateStep4 * fix: Select Upstream show empty string * merge LiteSun/feat-route into test-route * feat: auto fill DEFAULT_UPSTREAM * test: create route can skip upstream * feat: update upstreamForm * feat: update testcase * feat: update upstream testcase Co-authored-by: guoqqqi <979918...@qq.com> --- .../route/create-route-can-skip-upstream.spec.js | 132 ++++++++++ .../upstream/create_and_delete_upstream.spec.js | 10 +- web/src/components/Upstream/UpstreamForm.tsx | 276 +++++++++++---------- web/src/pages/Route/Create.tsx | 7 +- .../Route/components/CreateStep4/CreateStep4.tsx | 2 +- .../Route/components/Step2/RequestRewriteView.tsx | 2 + web/src/pages/Route/transform.ts | 5 + web/src/pages/Route/typing.d.ts | 2 + web/src/pages/Service/components/Step1.tsx | 1 + web/src/pages/Upstream/locales/en-US.ts | 4 +- 10 files changed, 300 insertions(+), 141 deletions(-) diff --git a/web/cypress/integration/route/create-route-can-skip-upstream.spec.js b/web/cypress/integration/route/create-route-can-skip-upstream.spec.js new file mode 100644 index 0000000..af78885 --- /dev/null +++ b/web/cypress/integration/route/create-route-can-skip-upstream.spec.js @@ -0,0 +1,132 @@ +/* + * 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('Can select service_id skip upstream in route', () => { + const data = { + upstreamName: 'test_upstream', + serviceName: 'test_service', + routeName: 'test_route', + ip: '127.0.0.1', + } + const domSelector = { + name: '#name', + nodes_0_host: '#nodes_0_host', + upstream_id: '#upstream_id', + input: ':input', + customUpstream: '[title=Custom]', + titleName: '[title=Name]', + testService: '[title=test_service]', + notification: '.ant-notification-notice-message', + }; + + beforeEach(() => { + cy.login(); + }); + + it('should create test upstream and service', () => { + cy.visit('/'); + cy.contains('Upstream').click(); + cy.contains('Create').click(); + + cy.get(domSelector.name).type(data.upstreamName); + cy.get(domSelector.nodes_0_host).type(data.ip); + cy.contains('Next').click(); + cy.contains('Submit').click(); + cy.get(domSelector.notification).should('contain', 'Create Upstream Successfully'); + + cy.visit('/'); + cy.contains('Service').click(); + cy.contains('Create').click(); + cy.get(domSelector.name).type(data.serviceName); + cy.get(domSelector.customUpstream).click(); + cy.contains(data.upstreamName).click(); + cy.contains('Next').click(); + cy.contains('Next').click(); + cy.contains('Submit').click(); + cy.get(domSelector.notification).should('contain', 'Create Service Successfully'); + }); + + it('should skip upstream module after service is selected when creating route', () => { + cy.visit('/'); + cy.contains('Route').click(); + cy.contains('Create').click(); + + // The None option doesn't exist when service isn't selected + cy.get(domSelector.name).type(data.routeName); + cy.contains('Next').click(); + cy.get(domSelector.customUpstream).click(); + cy.contains('None').should('not.exist'); + + cy.contains('Previous').click(); + cy.contains('None').click(); + cy.contains(data.serviceName).click(); + cy.contains('Next').click(); + + // make sure upstream data can be saved + cy.get(domSelector.customUpstream).click(); + cy.contains(data.upstreamName).click(); + cy.get(domSelector.input).should('be.disabled'); + + cy.contains(data.upstreamName).click(); + cy.contains('None').click(); + cy.contains('Next').click(); + cy.contains('Next').click(); + cy.contains('Submit').click(); + cy.contains('Goto List').click(); + }); + + it('should skip Upstream module after service is selected when editing route', () => { + cy.visit('/'); + cy.contains('Route').click(); + + cy.get(domSelector.titleName).type(data.routeName); + cy.contains('Search').click(); + cy.contains(data.routeName).siblings().contains('Edit').click(); + cy.get(domSelector.testService).click(); + cy.contains('None').click(); + cy.contains('Next').click(); + cy.get(domSelector.upstream_id).click(); + cy.contains('None').should('not.exist'); + cy.contains(data.upstreamName).click(); + cy.contains('Next').click(); + cy.contains('Next').click(); + cy.contains('Submit').click(); + cy.contains('Submit Successfully'); + }); + + it('should delete upstream, service and route', () => { + cy.visit('/'); + cy.contains('Upstream').click(); + cy.contains(data.upstreamName).siblings().contains('Delete').click(); + cy.contains('button', 'Confirm').click(); + cy.get(domSelector.notification).should('contain', 'Delete Upstream Successfully'); + + cy.visit('/'); + cy.contains('Service').click(); + cy.contains(data.serviceName).siblings().contains('Delete').click(); + cy.contains('button', 'Confirm').click(); + cy.get(domSelector.notification).should('contain', 'Delete Service Successfully'); + + cy.visit('/'); + cy.contains('Route').click(); + cy.contains(data.routeName).siblings().contains('Delete').click(); + cy.contains('button', 'Confirm').click(); + cy.get(domSelector.notification).should('contain', 'Delete Route Successfully'); + }); +}); + diff --git a/web/cypress/integration/upstream/create_and_delete_upstream.spec.js b/web/cypress/integration/upstream/create_and_delete_upstream.spec.js index b445f0c..b671741 100644 --- a/web/cypress/integration/upstream/create_and_delete_upstream.spec.js +++ b/web/cypress/integration/upstream/create_and_delete_upstream.spec.js @@ -45,8 +45,8 @@ context('Create and Delete Upstream', () => { cy.get('#nodes_0_port').clear().type('7000'); cy.contains('Next').click(); cy.contains('Submit').click(); - cy.get(domSelectors.notification).should('contain', 'Create upstream successfully'); - cy.contains('Create upstream successfully'); + cy.get(domSelectors.notification).should('contain', 'Create Upstream Successfully'); + cy.contains('Create Upstream Successfully'); cy.wait(sleepTime * 5); cy.url().should('contains', 'upstream/list'); }); @@ -57,7 +57,7 @@ context('Create and Delete Upstream', () => { cy.wait(sleepTime * 5); cy.contains(name).siblings().contains('Delete').click(); cy.contains('button', 'Confirm').click(); - cy.get(domSelectors.notification).should('contain', 'Delete successfully'); + cy.get(domSelectors.notification).should('contain', 'Delete Upstream Successfully'); }); it('should create chash upstream', () => { @@ -101,7 +101,7 @@ context('Create and Delete Upstream', () => { // next to finish cy.contains('Next').click(); cy.contains('Submit').click(); - cy.get(domSelectors.notification).should('contain', 'Create upstream successfully'); + cy.get(domSelectors.notification).should('contain', 'Create Upstream Successfully'); cy.wait(sleepTime * 5); cy.url().should('contains', 'upstream/list'); }); @@ -112,6 +112,6 @@ context('Create and Delete Upstream', () => { cy.wait(sleepTime * 5); cy.contains(name).siblings().contains('Delete').click(); cy.contains('button', 'Confirm').click(); - cy.get(domSelectors.notification).should('contain', 'Delete successfully'); + cy.get(domSelectors.notification).should('contain', 'Delete Upstream Successfully'); }); }); diff --git a/web/src/components/Upstream/UpstreamForm.tsx b/web/src/components/Upstream/UpstreamForm.tsx index 3e7cff7..36b8bf4 100644 --- a/web/src/components/Upstream/UpstreamForm.tsx +++ b/web/src/components/Upstream/UpstreamForm.tsx @@ -18,10 +18,11 @@ import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; import { Button, Col, Divider, Form, Input, InputNumber, Row, Select, Switch } from 'antd'; import React, { useState, forwardRef, useImperativeHandle, useEffect } from 'react'; import { useIntl } from 'umi'; +import type { FormInstance } from 'antd/es/form'; import { PanelSection } from '@api7-dashboard/ui'; import { transformRequest } from '@/pages/Upstream/transform'; -import type { FormInstance } from 'antd/es/form'; +import { DEFAULT_UPSTREAM } from './constant' enum Type { roundrobin = 'roundrobin', @@ -61,6 +62,7 @@ type Props = { showSelector?: boolean; // FIXME: use proper typing ref?: any; + required: boolean, }; const removeBtnStyle = { @@ -70,11 +72,12 @@ const removeBtnStyle = { }; const UpstreamForm: React.FC<Props> = forwardRef( - ({ form, disabled, list = [], showSelector }, ref) => { + ({ form, disabled, list = [], showSelector, required = true }, ref) => { const { formatMessage } = useIntl(); const [readonly, setReadonly] = useState( Boolean(form.getFieldValue('upstream_id')) || disabled, ); + const [hidenForm, setHidenForm] = useState(false); const timeoutFields = [ { @@ -96,13 +99,32 @@ const UpstreamForm: React.FC<Props> = forwardRef( })); useEffect(() => { - const id = form.getFieldValue('upstream_id'); - if (id) { - setReadonly(true); - requestAnimationFrame(() => { - form.setFieldsValue(list.find((item) => item.id === id)); - }); + const formData = transformRequest(form.getFieldsValue()) || {}; + const { upstream_id } = form.getFieldsValue(); + + if (upstream_id === 'None') { + setHidenForm(true); + if (required) { + requestAnimationFrame(() => { + form.resetFields(); + form.setFieldsValue(DEFAULT_UPSTREAM); + setHidenForm(false); + }); + } + } else { + if (upstream_id) { + requestAnimationFrame(() => { + form.setFieldsValue(list.find((item) => item.id === upstream_id)); + }); + } + if (!required && !Object.keys(formData).length) { + requestAnimationFrame(() => { + form.setFieldsValue({ upstream_id: 'None' }); + setHidenForm(true); + }); + } } + setReadonly(Boolean(upstream_id) || disabled); }, [list]); const CHash = () => ( @@ -608,26 +630,20 @@ const UpstreamForm: React.FC<Props> = forwardRef( <Form.Item label={formatMessage({ id: 'page.upstream.step.select.upstream' })} name="upstream_id" - shouldUpdate={(prev, next) => { - setReadonly(Boolean(next.upstream_id)); - if (prev.upstream_id !== next.upstream_id) { - const id = next.upstream_id; - if (id) { - form.setFieldsValue(list.find((item) => item.id === id)); - form.setFieldsValue({ - upstream_id: id, - }); - } - } - return prev.upstream_id !== next.upstream_id; - }} > <Select disabled={disabled} - onChange={(id) => { - form.setFieldsValue(list.find((item) => item.id === id)); + onChange={(upstream_id) => { + setReadonly(Boolean(upstream_id)); + setHidenForm(Boolean(upstream_id === 'None')); + form.setFieldsValue(list.find((item) => item.id === upstream_id)); + if (upstream_id === '') { + form.resetFields(); + form.setFieldsValue(DEFAULT_UPSTREAM); + } }} > + {Boolean(!required) && <Select.Option value={'None'} >None</Select.Option>} {[ { name: formatMessage({ id: 'page.upstream.step.select.upstream.select.option' }), @@ -643,116 +659,118 @@ const UpstreamForm: React.FC<Props> = forwardRef( </Form.Item> )} - <Form.Item - label={formatMessage({ id: 'page.upstream.step.type' })} - name="type" - rules={[{ required: true }]} - > - <Select disabled={readonly}> - {Object.entries(Type).map(([label, value]) => { - return ( - <Select.Option value={value} key={value}> - {label} - </Select.Option> - ); - })} - </Select> - </Form.Item> - <Form.Item shouldUpdate noStyle> - {() => { - if (form.getFieldValue('type') === 'chash') { - return <CHash />; - } - return null; - }} - </Form.Item> - {NodeList()} - <Form.Item - label={formatMessage({ id: 'page.upstream.step.pass-host' })} - name="pass_host" - extra={formatMessage({ id: 'page.upstream.step.pass-host.tips' })} - > - <Select disabled={readonly}> - <Select.Option value="pass"> - {formatMessage({ id: 'page.upstream.step.pass-host.pass' })} - </Select.Option> - <Select.Option value="node"> - {formatMessage({ id: 'page.upstream.step.pass-host.node' })} - </Select.Option> - <Select.Option value="rewrite"> - {formatMessage({ id: 'page.upstream.step.pass-host.rewrite' })} - </Select.Option> - </Select> - </Form.Item> - <Form.Item - noStyle - shouldUpdate={(prev, next) => { - return prev.pass_host !== next.pass_host; - }} - > - {() => { - if (form.getFieldValue('pass_host') === 'rewrite') { - return ( - <Form.Item - label={formatMessage({ id: 'page.upstream.step.pass-host.upstream_host' })} - name="upstream_host" - > - <Input disabled={readonly} /> - </Form.Item> - ); - } - return null; - }} - </Form.Item> - - {timeoutFields.map(({ label, name }) => ( - <Form.Item label={label} required key={label}> - <Form.Item - name={name} - noStyle - rules={[ - { - required: true, - message: formatMessage({ id: `page.upstream.step.input.${name[1]}.timeout` }), - }, - ]} - > - <InputNumber disabled={readonly} /> - </Form.Item> - <TimeUnit /> + {!hidenForm && (<> + <Form.Item + label={formatMessage({ id: 'page.upstream.step.type' })} + name="type" + rules={[{ required: true }]} + > + <Select disabled={readonly}> + {Object.entries(Type).map(([label, value]) => { + return ( + <Select.Option value={value} key={value}> + {label} + </Select.Option> + ); + })} + </Select> + </Form.Item> + <Form.Item shouldUpdate noStyle> + {() => { + if (form.getFieldValue('type') === 'chash') { + return <CHash />; + } + return null; + }} + </Form.Item> + {NodeList()} + <Form.Item + label={formatMessage({ id: 'page.upstream.step.pass-host' })} + name="pass_host" + extra={formatMessage({ id: 'page.upstream.step.pass-host.tips' })} + > + <Select disabled={readonly}> + <Select.Option value="pass"> + {formatMessage({ id: 'page.upstream.step.pass-host.pass' })} + </Select.Option> + <Select.Option value="node"> + {formatMessage({ id: 'page.upstream.step.pass-host.node' })} + </Select.Option> + <Select.Option value="rewrite"> + {formatMessage({ id: 'page.upstream.step.pass-host.rewrite' })} + </Select.Option> + </Select> + </Form.Item> + <Form.Item + noStyle + shouldUpdate={(prev, next) => { + return prev.pass_host !== next.pass_host; + }} + > + {() => { + if (form.getFieldValue('pass_host') === 'rewrite') { + return ( + <Form.Item + label={formatMessage({ id: 'page.upstream.step.pass-host.upstream_host' })} + name="upstream_host" + > + <Input disabled={readonly} /> + </Form.Item> + ); + } + return null; + }} </Form.Item> - ))} - <PanelSection - title={formatMessage({ id: 'page.upstream.step.healthyCheck.healthy.check' })} - > - {[ - { - label: formatMessage({ id: 'page.upstream.step.healthyCheck.active' }), - name: ['checks', 'active'], - component: <ActiveHealthCheck />, - }, - { - label: formatMessage({ id: 'page.upstream.step.healthyCheck.passive' }), - name: ['checks', 'passive'], - component: <InActiveHealthCheck />, - }, - ].map(({ label, name, component }) => ( - <div key={label}> - <Form.Item label={label} name={name} valuePropName="checked" key={label}> - <Switch disabled={readonly} /> - </Form.Item> - <Form.Item shouldUpdate noStyle> - {() => { - if (form.getFieldValue(name)) { - return component; - } - return null; - }} + {timeoutFields.map(({ label, name }) => ( + <Form.Item label={label} required key={label}> + <Form.Item + name={name} + noStyle + rules={[ + { + required: true, + message: formatMessage({ id: `page.upstream.step.input.${name[1]}.timeout` }), + }, + ]} + > + <InputNumber disabled={readonly} /> </Form.Item> - </div> + <TimeUnit /> + </Form.Item> ))} - </PanelSection> + + <PanelSection + title={formatMessage({ id: 'page.upstream.step.healthyCheck.healthy.check' })} + > + {[ + { + label: formatMessage({ id: 'page.upstream.step.healthyCheck.active' }), + name: ['checks', 'active'], + component: <ActiveHealthCheck />, + }, + { + label: formatMessage({ id: 'page.upstream.step.healthyCheck.passive' }), + name: ['checks', 'passive'], + component: <InActiveHealthCheck />, + }, + ].map(({ label, name, component }) => ( + <div key={label}> + <Form.Item label={label} name={name} valuePropName="checked" key={label}> + <Switch disabled={readonly} /> + </Form.Item> + <Form.Item shouldUpdate noStyle> + {() => { + if (form.getFieldValue(name)) { + return component; + } + return null; + }} + </Form.Item> + </div> + ))} + </PanelSection> + </>)} </Form> ); }, diff --git a/web/src/pages/Route/Create.tsx b/web/src/pages/Route/Create.tsx index ab1257f..9155a08 100644 --- a/web/src/pages/Route/Create.tsx +++ b/web/src/pages/Route/Create.tsx @@ -135,7 +135,7 @@ const Page: React.FC<Props> = (props) => { ); } - return <Step2 form={form2} upstreamRef={upstreamRef} />; + return <Step2 form={form2} upstreamRef={upstreamRef} hasServiceId={form1.getFieldValue('service_id') !== ''} />; } if (step === 3) { @@ -256,11 +256,10 @@ const Page: React.FC<Props> = (props) => { return ( <> <PageHeaderWrapper - title={`${ - (props as any).match.params.rid + title={`${(props as any).match.params.rid ? formatMessage({ id: 'component.global.edit' }) : formatMessage({ id: 'component.global.create' }) - } ${formatMessage({ id: 'menu.routes' })}`} + } ${formatMessage({ id: 'menu.routes' })}`} > <Card bordered={false}> <Steps current={step - 1} className={styles.steps}> diff --git a/web/src/pages/Route/components/CreateStep4/CreateStep4.tsx b/web/src/pages/Route/components/CreateStep4/CreateStep4.tsx index 95fc6dc..9e6f49f 100644 --- a/web/src/pages/Route/components/CreateStep4/CreateStep4.tsx +++ b/web/src/pages/Route/components/CreateStep4/CreateStep4.tsx @@ -50,7 +50,7 @@ const CreateStep4: React.FC<Props> = ({ form1, form2, redirect, upstreamRef, ... <h2 style={style}> {formatMessage({ id: 'page.route.steps.stepTitle.defineApiBackendServe' })} </h2> - <Step2 form={form2} upstreamRef={upstreamRef} disabled /> + <Step2 form={form2} upstreamRef={upstreamRef} disabled hasServiceId={form1.getFieldValue('service_id') !== ''} /> <h2 style={style}> {formatMessage({ id: 'component.global.steps.stepTitle.pluginConfig' })} </h2> diff --git a/web/src/pages/Route/components/Step2/RequestRewriteView.tsx b/web/src/pages/Route/components/Step2/RequestRewriteView.tsx index a3bf8dc..023cac8 100644 --- a/web/src/pages/Route/components/Step2/RequestRewriteView.tsx +++ b/web/src/pages/Route/components/Step2/RequestRewriteView.tsx @@ -23,6 +23,7 @@ const RequestRewriteView: React.FC<RouteModule.Step2PassProps> = ({ form, upstreamRef, disabled, + hasServiceId = false }) => { const [list, setList] = useState<UpstreamModule.RequestBody[]>([]); useEffect(() => { @@ -35,6 +36,7 @@ const RequestRewriteView: React.FC<RouteModule.Step2PassProps> = ({ disabled={disabled} list={list} showSelector + required={!hasServiceId} key={1} /> ); diff --git a/web/src/pages/Route/transform.ts b/web/src/pages/Route/transform.ts index bb794d2..3b9fa4f 100644 --- a/web/src/pages/Route/transform.ts +++ b/web/src/pages/Route/transform.ts @@ -107,6 +107,7 @@ export const transformStepData = ({ 'ret_code', 'redirectOption', service_id.length === 0 ? 'service_id' : '', + form2Data.upstream_id === 'None' ? 'upstream_id' : '', !Object.keys(data.plugins || {}).length ? 'plugins' : '', !Object.keys(data.script || {}).length ? 'script' : '', form1Data.hosts.filter(Boolean).length === 0 ? 'hosts' : '', @@ -215,6 +216,10 @@ export const transformRouteData = (data: RouteModule.Body) => { const advancedMatchingRules: RouteModule.MatchingRule[] = transformVarsToRules(vars); + if (upstream && Object.keys(upstream).length) { + upstream.upstream_id = ''; + } + const form2Data: RouteModule.Form2Data = upstream || { upstream_id }; const { plugins, script } = data; diff --git a/web/src/pages/Route/typing.d.ts b/web/src/pages/Route/typing.d.ts index 992a845..342d7c1 100644 --- a/web/src/pages/Route/typing.d.ts +++ b/web/src/pages/Route/typing.d.ts @@ -80,6 +80,7 @@ declare namespace RouteModule { remote_addrs: string[]; vars: [string, Operator, string][]; upstream: { + upstream_id?: string; type: 'roundrobin' | 'chash' | 'ewma'; hash_on?: string; key?: string; @@ -163,6 +164,7 @@ declare namespace RouteModule { form: FormInstance; disabled?: boolean; upstreamRef: any; + hasServiceId: boolean; }; type Form2Data = { diff --git a/web/src/pages/Service/components/Step1.tsx b/web/src/pages/Service/components/Step1.tsx index fd805ba..e904a57 100644 --- a/web/src/pages/Service/components/Step1.tsx +++ b/web/src/pages/Service/components/Step1.tsx @@ -54,6 +54,7 @@ const Step1: React.FC<ServiceModule.Step1PassProps> = ({ </Form> <UpstreamForm ref={upstreamRef} + required form={upstreamForm} disabled={disabled} list={list} diff --git a/web/src/pages/Upstream/locales/en-US.ts b/web/src/pages/Upstream/locales/en-US.ts index caad2b4..f4063da 100644 --- a/web/src/pages/Upstream/locales/en-US.ts +++ b/web/src/pages/Upstream/locales/en-US.ts @@ -82,7 +82,7 @@ export default { 'page.upstream.create.edit': 'Edit', 'page.upstream.create.create': 'Create', - 'page.upstream.create.upstream.successfully': 'upstream successfully', + 'page.upstream.create.upstream.successfully': 'Upstream Successfully', 'page.upstream.create.basic.info': 'Basic Information', 'page.upstream.create.preview': 'Preview', @@ -95,7 +95,7 @@ export default { 'page.upstream.list.confirm.delete': 'Are you sure to delete ?', 'page.upstream.list.confirm': 'Confirm', 'page.upstream.list.cancel': 'Cancel', - 'page.upstream.list.delete.successfully': 'Delete successfully', + 'page.upstream.list.delete.successfully': 'Delete Upstream Successfully', 'page.upstream.list.delete': 'Delete', 'page.upstream.list': 'Upstream List', 'page.upstream.list.input': 'Please input',