From 0e21bedfc6c75644ba2d3bfc5c789e1113e5be4c Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Thu, 14 May 2020 16:20:53 -0700 Subject: [PATCH] [7.8] Fix pagination bugs in CCR and Remote Clusters (#65931) (#66642) --- .../auto_follow_pattern_list.test.js | 44 +++++++++ .../follower_indices_list.test.js | 56 +++++++++++ .../auto_follow_pattern_list.helpers.js | 5 + .../helpers/follower_index_list.helpers.js | 5 + .../auto_follow_pattern_table.js | 96 ++++++++++++------- .../follower_indices_table.container.js | 2 +- .../follower_indices_table.js | 86 ++++++++++++----- .../helpers/remote_clusters_list.helpers.js | 5 + .../remote_clusters_list.test.js | 47 +++++++++ .../remote_cluster_table.js | 70 +++++++++----- 10 files changed, 328 insertions(+), 88 deletions(-) diff --git a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_list.test.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_list.test.js index 190400e988634..0a7eaf647b020 100644 --- a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_list.test.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_list.test.js @@ -61,6 +61,50 @@ describe('', () => { }); }); + describe('when there are multiple pages of auto-follow patterns', () => { + let find; + let component; + let table; + let actions; + let form; + + const autoFollowPatterns = [ + getAutoFollowPatternMock({ name: 'unique', followPattern: '{{leader_index}}' }), + ]; + + for (let i = 0; i < 29; i++) { + autoFollowPatterns.push( + getAutoFollowPatternMock({ name: `${i}`, followPattern: '{{leader_index}}' }) + ); + } + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadAutoFollowPatternsResponse({ patterns: autoFollowPatterns }); + + // Mount the component + ({ find, component, table, actions, form } = setup()); + + await nextTick(); // Make sure that the http request is fulfilled + component.update(); + }); + + test('pagination works', () => { + actions.clickPaginationNextButton(); + const { tableCellsValues } = table.getMetaData('autoFollowPatternListTable'); + + // Pagination defaults to 20 auto-follow patterns per page. We loaded 30 auto-follow patterns, + // so the second page should have 10. + expect(tableCellsValues.length).toBe(10); + }); + + // Skipped until we can figure out how to get this test to work. + test.skip('search works', () => { + form.setInputValue(find('autoFollowPatternSearch'), 'unique'); + const { tableCellsValues } = table.getMetaData('autoFollowPatternListTable'); + expect(tableCellsValues.length).toBe(1); + }); + }); + describe('when there are auto-follow patterns', () => { let find; let exists; diff --git a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_indices_list.test.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_indices_list.test.js index f98a1dafbbcbf..ad9f2db2ce91c 100644 --- a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_indices_list.test.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_indices_list.test.js @@ -4,6 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +/** + * The below import is required to avoid a console error warn from brace package + * console.warn ../node_modules/brace/index.js:3999 + Could not load worker ReferenceError: Worker is not defined + at createWorker (//node_modules/brace/index.js:17992:5) + */ +import * as stubWebWorker from '../../../../../test_utils/stub_web_worker'; // eslint-disable-line no-unused-vars + import { getFollowerIndexMock } from './fixtures/follower_index'; import './mocks'; import { setupEnvironment, pageHelpers, nextTick, getRandomString } from './helpers'; @@ -59,6 +67,54 @@ describe('', () => { }); }); + describe('when there are multiple pages of follower indices', () => { + let find; + let component; + let table; + let actions; + let form; + + const followerIndices = [ + { + name: 'unique', + seeds: [], + }, + ]; + + for (let i = 0; i < 29; i++) { + followerIndices.push({ + name: `name${i}`, + seeds: [], + }); + } + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadFollowerIndicesResponse({ indices: followerIndices }); + + // Mount the component + ({ find, component, table, actions, form } = setup()); + + await nextTick(); // Make sure that the http request is fulfilled + component.update(); + }); + + test('pagination works', () => { + actions.clickPaginationNextButton(); + const { tableCellsValues } = table.getMetaData('followerIndexListTable'); + + // Pagination defaults to 20 follower indices per page. We loaded 30 follower indices, + // so the second page should have 10. + expect(tableCellsValues.length).toBe(10); + }); + + // Skipped until we can figure out how to get this test to work. + test.skip('search works', () => { + form.setInputValue(find('followerIndexSearch'), 'unique'); + const { tableCellsValues } = table.getMetaData('followerIndexListTable'); + expect(tableCellsValues.length).toBe(1); + }); + }); + describe('when there are follower indices', () => { let find; let exists; diff --git a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_list.helpers.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_list.helpers.js index 450feed49f9f2..2c2ab642e83c8 100644 --- a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_list.helpers.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_list.helpers.js @@ -84,6 +84,10 @@ export const setup = props => { autoFollowPatternLink.simulate('click'); }; + const clickPaginationNextButton = () => { + testBed.find('autoFollowPatternListTable.pagination-button-next').simulate('click'); + }; + return { ...testBed, actions: { @@ -94,6 +98,7 @@ export const setup = props => { clickAutoFollowPatternAt, getPatternsActionMenuItemText, clickPatternsActionMenuItem, + clickPaginationNextButton, }, }; }; diff --git a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_list.helpers.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_list.helpers.js index 52f4267594cc1..5e9f7d1263cf7 100644 --- a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_list.helpers.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_list.helpers.js @@ -64,6 +64,10 @@ export const setup = props => { followerIndexLink.simulate('click'); }; + const clickPaginationNextButton = () => { + testBed.find('followerIndexListTable.pagination-button-next').simulate('click'); + }; + return { ...testBed, actions: { @@ -72,6 +76,7 @@ export const setup = props => { clickContextMenuButtonAt, openTableRowContextMenuAt, clickFollowerIndexAt, + clickPaginationNextButton, }, }; }; diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js index eb90e59e99fee..d682fdaadf818 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js @@ -23,6 +23,30 @@ import { import { routing } from '../../../../../services/routing'; import { trackUiMetric } from '../../../../../services/track_ui_metric'; +const getFilteredPatterns = (autoFollowPatterns, queryText) => { + if (queryText) { + const normalizedSearchText = queryText.toLowerCase(); + + return autoFollowPatterns.filter(autoFollowPattern => { + const { + name, + remoteCluster, + followIndexPatternPrefix, + followIndexPatternSuffix, + } = autoFollowPattern; + + const inName = name.toLowerCase().includes(normalizedSearchText); + const inRemoteCluster = remoteCluster.toLowerCase().includes(normalizedSearchText); + const inPrefix = followIndexPatternPrefix.toLowerCase().includes(normalizedSearchText); + const inSuffix = followIndexPatternSuffix.toLowerCase().includes(normalizedSearchText); + + return inName || inRemoteCluster || inPrefix || inSuffix; + }); + } + + return autoFollowPatterns; +}; + export class AutoFollowPatternTable extends PureComponent { static propTypes = { autoFollowPatterns: PropTypes.array, @@ -31,41 +55,42 @@ export class AutoFollowPatternTable extends PureComponent { resumeAutoFollowPattern: PropTypes.func.isRequired, }; - state = { - selectedItems: [], - }; + static getDerivedStateFromProps(props, state) { + const { autoFollowPatterns } = props; + const { prevAutoFollowPatterns, queryText } = state; - onSearch = ({ query }) => { - const { text } = query; - const normalizedSearchText = text.toLowerCase(); - this.setState({ - queryText: normalizedSearchText, - }); - }; + // If an auto-follow pattern gets deleted, we need to recreate the cached filtered auto-follow patterns. + if (prevAutoFollowPatterns !== autoFollowPatterns) { + return { + prevAutoFollowPatterns: autoFollowPatterns, + filteredAutoFollowPatterns: getFilteredPatterns(autoFollowPatterns, queryText), + }; + } - getFilteredPatterns = () => { - const { autoFollowPatterns } = this.props; - const { queryText } = this.state; + return null; + } - if (queryText) { - return autoFollowPatterns.filter(autoFollowPattern => { - const { - name, - remoteCluster, - followIndexPatternPrefix, - followIndexPatternSuffix, - } = autoFollowPattern; + constructor(props) { + super(props); - const inName = name.toLowerCase().includes(queryText); - const inRemoteCluster = remoteCluster.toLowerCase().includes(queryText); - const inPrefix = followIndexPatternPrefix.toLowerCase().includes(queryText); - const inSuffix = followIndexPatternSuffix.toLowerCase().includes(queryText); + this.state = { + prevAutoFollowPatterns: props.autoFollowPatterns, + selectedItems: [], + filteredAutoFollowPatterns: props.autoFollowPatterns, + queryText: '', + }; + } - return inName || inRemoteCluster || inPrefix || inSuffix; - }); - } + onSearch = ({ query }) => { + const { autoFollowPatterns } = this.props; + const { text } = query; - return autoFollowPatterns.slice(0); + // We cache the filtered indices instead of calculating them inside render() because + // of https://github.com/elastic/eui/issues/3445. + this.setState({ + queryText: text, + filteredAutoFollowPatterns: getFilteredPatterns(autoFollowPatterns, text), + }); }; getTableColumns() { @@ -144,7 +169,7 @@ export class AutoFollowPatternTable extends PureComponent { defaultMessage: 'Leader patterns', } ), - render: leaderPatterns => leaderPatterns.join(', '), + render: leaderIndexPatterns => leaderIndexPatterns.join(', '), }, { field: 'followIndexPatternPrefix', @@ -278,7 +303,7 @@ export class AutoFollowPatternTable extends PureComponent { }; render() { - const { selectedItems } = this.state; + const { selectedItems, filteredAutoFollowPatterns } = this.state; const sorting = { sort: { @@ -297,13 +322,13 @@ export class AutoFollowPatternTable extends PureComponent { this.setState({ selectedItems: selectedItems.map(({ name }) => name) }), }; - const items = this.getFilteredPatterns(); - const search = { toolsLeft: selectedItems.length ? ( items.find(item => item.name === name))} + patterns={this.state.selectedItems.map(name => + filteredAutoFollowPatterns.find(item => item.name === name) + )} /> ) : ( undefined @@ -311,13 +336,14 @@ export class AutoFollowPatternTable extends PureComponent { onChange: this.onSearch, box: { incremental: true, + 'data-test-subj': 'autoFollowPatternSearch', }, }; return ( ({ apiStatusDelete: getApiStatus(`${scope}-delete`)(state), }); -// + const mapDispatchToProps = dispatch => ({ selectFollowerIndex: name => dispatch(selectDetailFollowerIndex(name)), }); diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js index ef4a511f276bd..e95b3b0356aba 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js @@ -26,21 +26,73 @@ import { routing } from '../../../../../services/routing'; import { trackUiMetric } from '../../../../../services/track_ui_metric'; import { ContextMenu } from '../context_menu'; +const getFilteredIndices = (followerIndices, queryText) => { + if (queryText) { + const normalizedSearchText = queryText.toLowerCase(); + + return followerIndices.filter(followerIndex => { + const { name, remoteCluster, leaderIndex } = followerIndex; + + if (name.toLowerCase().includes(normalizedSearchText)) { + return true; + } + + if (leaderIndex.toLowerCase().includes(normalizedSearchText)) { + return true; + } + + if (remoteCluster.toLowerCase().includes(normalizedSearchText)) { + return true; + } + + return false; + }); + } + + return followerIndices; +}; + export class FollowerIndicesTable extends PureComponent { static propTypes = { followerIndices: PropTypes.array, selectFollowerIndex: PropTypes.func.isRequired, }; - state = { - selectedItems: [], - }; + static getDerivedStateFromProps(props, state) { + const { followerIndices } = props; + const { prevFollowerIndices, queryText } = state; + + // If a follower index gets deleted, we need to recreate the cached filtered follower indices. + if (prevFollowerIndices !== followerIndices) { + return { + prevFollowerIndices: followerIndices, + filteredClusters: getFilteredIndices(followerIndices, queryText), + }; + } + + return null; + } + + constructor(props) { + super(props); + + this.state = { + prevFollowerIndices: props.followerIndices, + selectedItems: [], + filteredIndices: props.followerIndices, + queryText: '', + }; + } onSearch = ({ query }) => { + const { followerIndices } = this.props; const { text } = query; - const normalizedSearchText = text.toLowerCase(); + + // We cache the filtered indices instead of calculating them inside render() because + // of https://github.com/elastic/eui/issues/3445. this.setState({ - queryText: normalizedSearchText, + queryText: text, + filteredIndices: getFilteredIndices(followerIndices, text), }); }; @@ -49,25 +101,6 @@ export class FollowerIndicesTable extends PureComponent { routing.navigate(uri); }; - getFilteredIndices = () => { - const { followerIndices } = this.props; - const { queryText } = this.state; - - if (queryText) { - return followerIndices.filter(followerIndex => { - const { name, remoteCluster, leaderIndex } = followerIndex; - - const inName = name.toLowerCase().includes(queryText); - const inRemoteCluster = remoteCluster.toLowerCase().includes(queryText); - const inLeaderIndex = leaderIndex.toLowerCase().includes(queryText); - - return inName || inRemoteCluster || inLeaderIndex; - }); - } - - return followerIndices.slice(0); - }; - getTableColumns() { const { selectFollowerIndex } = this.props; @@ -258,7 +291,7 @@ export class FollowerIndicesTable extends PureComponent { }; render() { - const { selectedItems } = this.state; + const { selectedItems, filteredIndices } = this.state; const sorting = { sort: { @@ -285,13 +318,14 @@ export class FollowerIndicesTable extends PureComponent { onChange: this.onSearch, box: { incremental: true, + 'data-test-subj': 'followerIndexSearch', }, }; return ( { remoteClusterLink.simulate('click'); }; + const clickPaginationNextButton = () => { + testBed.find('remoteClusterListTable.pagination-button-next').simulate('click'); + }; + return { ...testBed, actions: { @@ -77,6 +81,7 @@ export const setup = props => { clickRowActionButtonAt, clickConfirmModalDeleteRemoteCluster, clickRemoteClusterAt, + clickPaginationNextButton, }, }; }; diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_list.test.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_list.test.js index 1dc6f7075e30e..187ce1ddc4ca5 100644 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_list.test.js +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_list.test.js @@ -70,6 +70,53 @@ describe.skip('', () => { }); }); + describe('when there are multiple pages of remote clusters', () => { + let find; + let table; + let actions; + let waitFor; + let form; + + const remoteClusters = [ + { + name: 'unique', + seeds: [], + }, + ]; + + for (let i = 0; i < 29; i++) { + remoteClusters.push({ + name: `name${i}`, + seeds: [], + }); + } + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadRemoteClustersResponse(remoteClusters); + + await act(async () => { + ({ find, table, actions, waitFor, form } = setup()); + await waitFor('remoteClusterListTable'); + }); + }); + + test('pagination works', () => { + actions.clickPaginationNextButton(); + const { tableCellsValues } = table.getMetaData('remoteClusterListTable'); + + // Pagination defaults to 20 remote clusters per page. We loaded 30 remote clusters, + // so the second page should have 10. + expect(tableCellsValues.length).toBe(10); + }); + + // Skipped until we can figure out how to get this test to work. + test.skip('search works', () => { + form.setInputValue(find('remoteClusterSearch'), 'unique'); + const { tableCellsValues } = table.getMetaData('remoteClusterListTable'); + expect(tableCellsValues.length).toBe(1); + }); + }); + describe('when there are remote clusters', () => { let find; let exists; diff --git a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_table/remote_cluster_table.js b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_table/remote_cluster_table.js index 73f32fe8bca5b..739c6e26784ef 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_table/remote_cluster_table.js +++ b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_table/remote_cluster_table.js @@ -25,6 +25,24 @@ import { PROXY_MODE } from '../../../../../common/constants'; import { getRouterLinkProps, trackUiMetric, METRIC_TYPE } from '../../../services'; import { ConnectionStatus, RemoveClusterButtonProvider } from '../components'; +const getFilteredClusters = (clusters, queryText) => { + if (queryText) { + const normalizedSearchText = queryText.toLowerCase(); + + return clusters.filter(cluster => { + const { name, seeds } = cluster; + const normalizedName = name.toLowerCase(); + if (normalizedName.toLowerCase().includes(normalizedSearchText)) { + return true; + } + + return seeds.some(seed => seed.includes(normalizedSearchText)); + }); + } else { + return clusters; + } +}; + export class RemoteClusterTable extends Component { static propTypes = { clusters: PropTypes.array, @@ -35,46 +53,47 @@ export class RemoteClusterTable extends Component { clusters: [], }; + static getDerivedStateFromProps(props, state) { + const { clusters } = props; + const { prevClusters, queryText } = state; + + // If a remote cluster gets deleted, we need to recreate the cached filtered clusters. + if (prevClusters !== clusters) { + return { + prevClusters: clusters, + filteredClusters: getFilteredClusters(clusters, queryText), + }; + } + + return null; + } + constructor(props) { super(props); this.state = { - queryText: undefined, + prevClusters: props.clusters, selectedItems: [], + filteredClusters: props.clusters, + queryText: '', }; } onSearch = ({ query }) => { + const { clusters } = this.props; const { text } = query; - const normalizedSearchText = text.toLowerCase(); + + // We cache the filtered indices instead of calculating them inside render() because + // of https://github.com/elastic/eui/issues/3445. this.setState({ - queryText: normalizedSearchText, + queryText: text, + filteredClusters: getFilteredClusters(clusters, text), }); }; - getFilteredClusters = () => { - const { clusters } = this.props; - const { queryText } = this.state; - - if (queryText) { - return clusters.filter(cluster => { - const { name, seeds } = cluster; - const normalizedName = name.toLowerCase(); - if (normalizedName.toLowerCase().includes(queryText)) { - return true; - } - - return seeds.some(seed => seed.includes(queryText)); - }); - } else { - return clusters.slice(0); - } - }; - render() { const { openDetailPanel } = this.props; - - const { selectedItems } = this.state; + const { selectedItems, filteredClusters } = this.state; const columns = [ { @@ -314,6 +333,7 @@ export class RemoteClusterTable extends Component { onChange: this.onSearch, box: { incremental: true, + 'data-test-subj': 'remoteClusterSearch', }, }; @@ -327,8 +347,6 @@ export class RemoteClusterTable extends Component { selectable: ({ isConfiguredByNode }) => !isConfiguredByNode, }; - const filteredClusters = this.getFilteredClusters(); - return (