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 0000000000..af78885526 --- /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 b445f0c8e4..b6717414d1 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 3e7cff7d7b..36b8bf48d6 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 = 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 = 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 = forwardRef( { - 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; - }} > - {Object.entries(Type).map(([label, value]) => { - return ( - - {label} - - ); - })} - - - - {() => { - if (form.getFieldValue('type') === 'chash') { - return ; - } - return null; - }} - - {NodeList()} - - - - { - return prev.pass_host !== next.pass_host; - }} - > - {() => { - if (form.getFieldValue('pass_host') === 'rewrite') { - return ( - - - - ); - } - return null; - }} - - - {timeoutFields.map(({ label, name }) => ( - - - - - + {!hidenForm && (<> + + + + + {() => { + if (form.getFieldValue('type') === 'chash') { + return ; + } + return null; + }} + + {NodeList()} + + + + { + return prev.pass_host !== next.pass_host; + }} + > + {() => { + if (form.getFieldValue('pass_host') === 'rewrite') { + return ( + + + + ); + } + return null; + }} - ))} - - {[ - { - label: formatMessage({ id: 'page.upstream.step.healthyCheck.active' }), - name: ['checks', 'active'], - component: , - }, - { - label: formatMessage({ id: 'page.upstream.step.healthyCheck.passive' }), - name: ['checks', 'passive'], - component: , - }, - ].map(({ label, name, component }) => ( -
- - - - - {() => { - if (form.getFieldValue(name)) { - return component; - } - return null; - }} + {timeoutFields.map(({ label, name }) => ( + + + -
+ +
))} - + + + {[ + { + label: formatMessage({ id: 'page.upstream.step.healthyCheck.active' }), + name: ['checks', 'active'], + component: , + }, + { + label: formatMessage({ id: 'page.upstream.step.healthyCheck.passive' }), + name: ['checks', 'passive'], + component: , + }, + ].map(({ label, name, component }) => ( +
+ + + + + {() => { + if (form.getFieldValue(name)) { + return component; + } + return null; + }} + +
+ ))} +
+ )} ); }, diff --git a/web/src/pages/Route/Create.tsx b/web/src/pages/Route/Create.tsx index ab1257f130..9155a082ca 100644 --- a/web/src/pages/Route/Create.tsx +++ b/web/src/pages/Route/Create.tsx @@ -135,7 +135,7 @@ const Page: React.FC = (props) => { ); } - return ; + return ; } if (step === 3) { @@ -256,11 +256,10 @@ const Page: React.FC = (props) => { return ( <> diff --git a/web/src/pages/Route/components/CreateStep4/CreateStep4.tsx b/web/src/pages/Route/components/CreateStep4/CreateStep4.tsx index 95fc6dcf3f..9e6f49f657 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 = ({ form1, form2, redirect, upstreamRef, ...

{formatMessage({ id: 'page.route.steps.stepTitle.defineApiBackendServe' })}

- +

{formatMessage({ id: 'component.global.steps.stepTitle.pluginConfig' })}

diff --git a/web/src/pages/Route/components/Step2/RequestRewriteView.tsx b/web/src/pages/Route/components/Step2/RequestRewriteView.tsx index a3bf8dcb0f..023cac87f9 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 = ({ form, upstreamRef, disabled, + hasServiceId = false }) => { const [list, setList] = useState([]); useEffect(() => { @@ -35,6 +36,7 @@ const RequestRewriteView: React.FC = ({ 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 bb794d227f..3b9fa4f09f 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 992a845dcb..342d7c1700 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 fd805ba950..e904a577dd 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 = ({