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 3d20267b6a..4bfea0f4b6 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,6 +32,8 @@ context('Delete Plugin List with the Drawer', () => { empty: '.ant-empty-normal', tab: '.ant-tabs-tab', tabBtn: '.ant-tabs-tab-btn', + notification: '.ant-notification-notice', + notificationCloseIcon: '.ant-notification-notice-close', }; const data = { @@ -109,6 +111,57 @@ context('Delete Plugin List with the Drawer', () => { cy.contains('button', 'Confirm').click({ force: true, }); + cy.get(selector.notification).should('contain', 'Delete Plugin Successfully'); + cy.get(selector.notificationCloseIcon).click({ multiple: true }); + cy.get(selector.empty).should('be.visible'); + }); + + it('should delete the plugin with the drawer in the list of plugins', function () { + cy.visit('/plugin/list'); + cy.get(selector.refresh).click(); + cy.contains('Enable').click(); + + cy.contains(data.basicAuthPlugin) + .parents(selector.pluginCardBordered) + .within(() => { + cy.get('button').click({ + force: true, + }); + }); + cy.get(selector.drawer) + .should('be.visible') + .within(() => { + cy.get(selector.disabledSwitcher).click(); + cy.get(selector.checkedSwitcher).should('exist'); + }); + cy.contains('button', 'Submit').click(); + + cy.contains(data.basicAuthPlugin) + .parents(selector.pluginCardBordered) + .within(() => { + cy.get('button').click({ + force: true, + }); + }); + + cy.contains('button', 'Delete').click({ + force: true, + }); + cy.contains('button', 'Confirm').click({ + force: true, + }); + cy.get(selector.notification).should('contain', 'Delete Plugin Successfully'); + cy.get(selector.notificationCloseIcon).click({ multiple: true }); + + cy.contains(data.basicAuthPlugin) + .parents(selector.pluginCardBordered) + .within(() => { + cy.get('button').then(($el) => { + const text = $el.text(); + expect(text).to.eq('Enable'); + }); + }); + cy.visit('/plugin/list'); cy.get(selector.empty).should('be.visible'); }); diff --git a/web/cypress/integration/route/batch-delete-route.spec.js b/web/cypress/integration/route/batch-delete-route.spec.js new file mode 100644 index 0000000000..3a5c603a2a --- /dev/null +++ b/web/cypress/integration/route/batch-delete-route.spec.js @@ -0,0 +1,165 @@ +/* + * 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 */ + +context('Create and Batch Deletion Routes', () => { + const timeout = 5000; + + const selector = { + name: '#name', + description: '#desc', + hosts_0: '#hosts_0', + uris_0: '#uris_0', + labels_0_labelKey: '#labels_0_labelKey', + labels_0_labelValue: '#labels_0_labelValue', + nodes_0_host: '#submitNodes_0_host', + nodes_0_port: '#submitNodes_0_port', + nodes_0_weight: '#submitNodes_0_weight', + nameSearchInput: '#name', + pathSearchInput: '#uri', + drawerBody: '.ant-drawer-wrapper-body', + notification: '.ant-notification-notice-message', + notificationClose: '.anticon-close', + }; + + const data = { + host1: '11.11.11.11', + host2: '12.12.12.12', + port: '80', + weight: 1, + uris: '/get', + uris0: '/get0', + uris1: '/get1', + uris2: '/get2', + urisx: '/getx', + submitSuccess: 'Submit Successfully', + deleteRouteSuccess: 'Delete Route Successfully', + test: 'test', + test0: 'test0', + test1: 'test1', + test2: 'test2', + testx: 'testx', + desc0: 'desc0', + desc1: 'desc1', + desc2: 'desc2', + value0: 'value0', + label0_value0: 'label0:value0', + }; + + beforeEach(() => { + cy.login(); + }); + + it('should successfully create 3 routes', function () { + cy.visit('/'); + cy.contains('Route').click(); + for (let i = 0; i < 3; i += 1) { + cy.contains('Create').click(); + cy.contains('Next').click().click(); + cy.get(selector.name).type(`test${i}`); + cy.get(selector.description).type(`desc${i}`); + cy.get(selector.hosts_0).type(data.host1); + cy.get(selector.uris_0).clear().type(`/get${i}`); + + // config label + cy.contains('Manage').click(); + + // eslint-disable-next-line no-loop-func + cy.get(selector.drawerBody).within(() => { + cy.contains('button', 'Add') + .should('not.be.disabled') + .click() + .then(() => { + cy.get(selector.labels_0_labelKey).type(`label${i}`); + cy.get(selector.labels_0_labelValue).type(`value${i}`); + cy.contains('Confirm').click(); + }); + }); + + cy.contains('button', 'Next').should('not.be.disabled').click(); + cy.get(selector.nodes_0_host).type(data.host2, { + timeout, + }); + cy.get(selector.nodes_0_port).type(data.port); + cy.get(selector.nodes_0_weight).type(data.weight); + cy.contains('Next').click(); + cy.contains('Next').click(); + cy.contains('Submit').click(); + cy.contains(data.submitSuccess); + cy.contains('Goto List').click(); + cy.url().should('contains', 'routes/list'); + } + }); + + it('should delete the route', function () { + cy.visit('/routes/list'); + cy.contains(data.test0).get('[type="checkbox"]').check(); + cy.contains(data.test2).get('[type="checkbox"]').check(); + cy.contains('BatchDeletion Routes').should('be.visible').click({ timeout }); + cy.get(selector.notification).should('contain', data.deleteRouteSuccess); + cy.get(selector.notificationClose).should('be.visible').click({ + force: true, + multiple: true, + }); + }); + + it('should batch delete the name of the route', function () { + cy.visit('/'); + cy.contains('Route').click(); + // full match + cy.get(selector.nameSearchInput).type(data.test0); + cy.contains('Search').click(); + cy.contains(data.test1).should('not.exist'); + cy.contains(data.test0).should('not.exist'); + cy.contains(data.test2).should('not.exist'); + // partial match + cy.get(selector.nameSearchInput).clear().type(data.test2); + cy.contains('Search').click(); + cy.contains(data.test0).should('not.exist'); + cy.contains(data.test1).should('not.exist'); + cy.contains(data.test2).should('not.exist'); + // no match + cy.get(selector.nameSearchInput).clear().type(data.testx); + cy.contains('Search').click(); + cy.contains(data.test0).should('not.exist'); + cy.contains(data.test1).should('not.exist'); + cy.contains(data.test2).should('not.exist'); + }); + + it('should batch delete the path of the route', function () { + cy.visit('/'); + cy.contains('Route').click(); + // full match + cy.get(selector.pathSearchInput).type(data.uris0); + cy.contains('Search').click(); + cy.contains(data.uris1).should('not.exist'); + cy.contains(data.uris0).should('not.exist'); + cy.contains(data.uris2).should('not.exist'); + // partial match + cy.get(selector.pathSearchInput).clear().type(data.uris2); + cy.contains('Search').click(); + cy.contains(data.uris0).should('not.exist'); + cy.contains(data.uris1).should('not.exist'); + cy.contains(data.uris2).should('not.exist'); + // no match + cy.get(selector.pathSearchInput).clear().type(data.urisx); + cy.contains('Search').click(); + cy.contains(data.uris0).should('not.exist'); + cy.contains(data.uris1).should('not.exist'); + cy.contains(data.uris2).should('not.exist'); + }); +}); diff --git a/web/src/components/Plugin/PluginPage.tsx b/web/src/components/Plugin/PluginPage.tsx index 54bfc629a0..b2f842c83b 100644 --- a/web/src/components/Plugin/PluginPage.tsx +++ b/web/src/components/Plugin/PluginPage.tsx @@ -33,7 +33,11 @@ type Props = { schemaType?: PluginComponent.Schema; referPage?: PluginComponent.ReferPage; showSelector?: boolean; - onChange?: (plugins: PluginComponent.Data, plugin_config_id?: string) => void; + onChange?: ( + plugins: PluginComponent.Data, + plugin_config_id?: string, + handleType?: 'edit' | 'delete', + ) => void; }; const PanelSectionStyle = { @@ -100,11 +104,11 @@ const PluginPage: React.FC = ({ return index === self.indexOf(elem); }); - const [selectedTab, setSelectedTab] = useState('add'); + const [showEnablePlugin, setShowEnablePlugin] = useState(true); const tabsList = [ { title: formatMessage({ id: 'component.plugin.enable' }), - key: 'add', + key: 'enable', }, { title: formatMessage({ id: 'component.plugin.disable' }), @@ -113,9 +117,13 @@ const PluginPage: React.FC = ({ ]; const SwitchTab = () => ( { - setSelectedTab(val); + if (val === 'enable') { + setShowEnablePlugin(true); + } else { + setShowEnablePlugin(false); + } }} > {tabsList.map((tab) => ( @@ -223,7 +231,7 @@ const PluginPage: React.FC = ({ readonly ? (item) => item.type === typeItem && !item.hidden && initialData[item.name] : (item) => - selectedTab === 'add' + showEnablePlugin ? item.type === typeItem && !item.hidden && !initialData[item.name] : item.type === typeItem && !item.hidden && @@ -311,10 +319,12 @@ const PluginPage: React.FC = ({ ...initialData, [name]: { ...monacoData, disable: !formData.disable }, }; + let handleType = 'edit'; if (shouldDelete === true) { - newPlugins = omit(newPlugins, name); + newPlugins = omit(plugins, name); + handleType = 'delete'; } - onChange(newPlugins, form.getFieldValue('plugin_config_id')); + onChange(newPlugins, form.getFieldValue('plugin_config_id'), handleType); setPlugins(newPlugins); setName(NEVER_EXIST_PLUGIN_FLAG); }} diff --git a/web/src/locales/en-US/component.ts b/web/src/locales/en-US/component.ts index 10db5d53dd..0e3c263e46 100644 --- a/web/src/locales/en-US/component.ts +++ b/web/src/locales/en-US/component.ts @@ -53,6 +53,7 @@ export default { 'component.global.notification.success.message.deleteSuccess': 'Deleted Successfully', 'component.global.create.consumer.success': 'Create Consumer Successfully', 'component.global.delete.consumer.success': 'Delete Consumer Successfully', + 'component.global.delete.routes.success': 'Delete Route Successfully', 'component.global.edit.consumer.success': 'Edit Consumer Successfully', 'component.global.steps.stepTitle.basicInformation': 'Basic Information', diff --git a/web/src/locales/tr-TR/component.ts b/web/src/locales/tr-TR/component.ts index f4df04357b..10ba71e13c 100644 --- a/web/src/locales/tr-TR/component.ts +++ b/web/src/locales/tr-TR/component.ts @@ -53,6 +53,7 @@ export default { 'component.global.notification.success.message.deleteSuccess': 'Başarıyla silindi', 'component.global.create.consumer.success': 'Başarıyla oluşturuldu', 'component.global.delete.consumer.success': 'Başarıyla silindi', + 'component.global.delete.routes.success': 'Sil Yöneltmeler Başarılı', 'component.global.edit.consumer.success': 'Başarıyla güncellendi', 'component.global.steps.stepTitle.basicInformation': 'Temel Bilgiler', @@ -72,9 +73,9 @@ export default { 'component.global.name': 'Ad', 'component.global.id': 'ID', 'component.global.updateTime': 'Zamanı Güncelle', - 'component.global.form.itemExtraMessage.nameGloballyUnique': 'İsim global olarak benzersiz olmalıdır', - 'component.global.input.placeholder.description': - 'Açıklama girin, max 256 karakter', + 'component.global.form.itemExtraMessage.nameGloballyUnique': + 'İsim global olarak benzersiz olmalıdır', + 'component.global.input.placeholder.description': 'Açıklama girin, max 256 karakter', // User component 'component.user.loginByPassword': 'Kullanıcı adı ve şifre', 'component.user.login': 'Giriş', diff --git a/web/src/locales/zh-CN/component.ts b/web/src/locales/zh-CN/component.ts index 6131a42b5f..b24be33cfe 100644 --- a/web/src/locales/zh-CN/component.ts +++ b/web/src/locales/zh-CN/component.ts @@ -60,6 +60,7 @@ export default { 'component.global.updateTime': '更新时间', 'component.global.create.consumer.success': '创建消费者成功', 'component.global.delete.consumer.success': '删除消费者成功', + 'component.global.delete.routes.success': '删除路由成功', 'component.global.edit.consumer.success': '配置消费者成功', 'component.global.popconfirm.title.delete': '确定删除该条记录吗?', diff --git a/web/src/pages/Plugin/List.tsx b/web/src/pages/Plugin/List.tsx index 7d9be5db44..d4a4361e7f 100644 --- a/web/src/pages/Plugin/List.tsx +++ b/web/src/pages/Plugin/List.tsx @@ -81,9 +81,7 @@ const Page: React.FC = () => { const plugins = omit(initialData, [`${record.name}`]); createOrUpdate({ plugins }).then(() => { notification.success({ - message: `${formatMessage({ id: 'component.global.delete' })} ${formatMessage({ - id: 'menu.plugin', - })} ${formatMessage({ id: 'component.status.success' })}`, + message: formatMessage({ id: 'page.plugin.delete' }), }); checkPageList(ref); setInitialData(plugins); @@ -131,6 +129,11 @@ const Page: React.FC = () => { setVisible(false); setName(''); ref.current?.reload(); + notification.success({ + message: formatMessage({ + id: `page.plugin.${shouldDelete ? 'delete' : 'edit'}`, + }), + }); }); }} /> diff --git a/web/src/pages/Plugin/PluginMarket.tsx b/web/src/pages/Plugin/PluginMarket.tsx index b780afffae..aca5003b50 100644 --- a/web/src/pages/Plugin/PluginMarket.tsx +++ b/web/src/pages/Plugin/PluginMarket.tsx @@ -15,7 +15,7 @@ * limitations under the License. */ import React, { useEffect, useState } from 'react'; -import { Card } from 'antd'; +import { Card, notification } from 'antd'; import { PageHeaderWrapper } from '@ant-design/pro-layout'; import PluginPage from '@/components/Plugin'; @@ -48,11 +48,16 @@ const PluginMarket: React.FC = () => { initialData={initialData} type="global" schemaType="route" - onChange={(pluginsData) => { + onChange={(pluginsData, pluginId, handleType) => { createOrUpdate({ plugins: pluginsData, }).then(() => { initPageData(); + notification.success({ + message: formatMessage({ + id: `page.plugin.${handleType}`, + }), + }); }); }} /> diff --git a/web/src/pages/Plugin/locales/en-US.ts b/web/src/pages/Plugin/locales/en-US.ts index 16e45f824a..d294ee849d 100644 --- a/web/src/pages/Plugin/locales/en-US.ts +++ b/web/src/pages/Plugin/locales/en-US.ts @@ -19,4 +19,6 @@ export default { 'page.plugin.list': 'Plugin List', 'page.plugin.list.enabled': 'List of enabled plugins', 'page.plugin.market.config': 'Global Plugin List', + 'page.plugin.edit': 'Configure Plugin Successfully', + 'page.plugin.delete': 'Delete Plugin Successfully', }; diff --git a/web/src/pages/Plugin/locales/tr-TR.ts b/web/src/pages/Plugin/locales/tr-TR.ts index 3b38ff2b6d..627876ae62 100644 --- a/web/src/pages/Plugin/locales/tr-TR.ts +++ b/web/src/pages/Plugin/locales/tr-TR.ts @@ -19,4 +19,6 @@ export default { 'page.plugin.list': 'Eklenti Listesi', 'page.plugin.list.enabled': 'Aktif eklentilerin listesi', 'page.plugin.market.config': 'Genel Eklenti Listesi', + 'page.plugin.edit': 'Eklenti başarıyla yapılandırma', + 'page.plugin.delete': 'Eklentiyi başarıyla kaldırın', }; diff --git a/web/src/pages/Plugin/locales/zh-CN.ts b/web/src/pages/Plugin/locales/zh-CN.ts index 991532332d..48f0032b3b 100644 --- a/web/src/pages/Plugin/locales/zh-CN.ts +++ b/web/src/pages/Plugin/locales/zh-CN.ts @@ -19,4 +19,6 @@ export default { 'page.plugin.list': '插件列表', 'page.plugin.list.enabled': '已启用插件的列表', 'page.plugin.market.config': '全局插件列表', + 'page.plugin.edit': '配置插件成功', + 'page.plugin.delete': '删除插件成功', }; diff --git a/web/src/pages/Route/List.tsx b/web/src/pages/Route/List.tsx index f4f36bd544..3ea42b191e 100755 --- a/web/src/pages/Route/List.tsx +++ b/web/src/pages/Route/List.tsx @@ -31,6 +31,7 @@ import { Modal, Menu, Dropdown, + Table, Tooltip, } from 'antd'; import { history, useIntl } from 'umi'; @@ -96,6 +97,8 @@ const Page: React.FC = () => { setSelectedRowKeys(currentSelectKeys); }, preserveSelectedRowKeys: true, + selections: [Table.SELECTION_ALL, Table.SELECTION_INVERT, Table.SELECTION_NONE], + defaultSelectedRowKeys: [1], }; const handleTableActionSuccessResponse = (msgTip: string) => { @@ -306,7 +309,7 @@ const Page: React.FC = () => { { title: formatMessage({ id: 'component.global.name' }), dataIndex: 'name', - fixed: 'left', + width: 150, }, { title: formatMessage({ id: 'component.global.id' }), @@ -542,6 +545,33 @@ const Page: React.FC = () => { actionRef={ref} rowKey="id" columns={columns} + rowSelection={rowSelection} + tableAlertRender={() => ( + + + {formatMessage({ id: 'page.route.chosen' })} {selectedRowKeys.length}{' '} + {formatMessage({ id: 'page.route.item' })} + + + )} + tableAlertOptionRender={() => { + return ( + + + + ); + }} request={fetchList} pagination={{ onChange: (page, pageSize?) => savePageList(page, pageSize), @@ -559,9 +589,7 @@ const Page: React.FC = () => { , , ]} - rowSelection={rowSelection} footer={() => } - tableAlertRender={false} scroll={{ x: 1300 }} /> { }); }; -export const remove = (rid: string) => request(`/routes/${rid}`, { method: 'DELETE' }); +export const remove = (rid: string[]) => request(`/routes/${rid}`, { method: 'DELETE' }); export const checkUniqueName = (name = '', exclude = '') => request('/notexist/routes', {