From e7555147d479ddf26f85bd6d9ba2faa1d24adafe Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Fri, 9 Mar 2018 08:09:54 -0800 Subject: [PATCH] React/EUI-ify indexed fields table (#16695) * React/EUI-ifying indexed fields table --- package.json | 1 + .../edit_index_pattern.html | 5 +- .../edit_index_pattern/edit_index_pattern.js | 70 ++++- .../indexed_fields_table.test.js.snap | 97 +++++++ .../__tests__/indexed_fields_table.test.js | 85 ++++++ .../__snapshots__/table.test.js.snap | 245 ++++++++++++++++++ .../components/table/__tests__/table.test.js | 122 +++++++++ .../components/table/index.js | 1 + .../components/table/table.js | 140 ++++++++++ .../indexed_fields_table/field_name.html | 8 - .../indexed_fields_table/field_type.html | 7 - .../indexed_fields_table/index.js | 2 +- .../indexed_fields_table.html | 9 - .../indexed_fields_table.js | 167 ++++++------ .../lib/__tests__/get_field_format.test.js | 34 +++ .../lib/get_field_format.js | 5 + .../indexed_fields_table/lib/index.js | 1 + .../__snapshots__/table.test.js.snap | 4 +- .../components/table/table.js | 4 +- .../_index_pattern_create_delete.js | 14 +- .../management/_index_pattern_popularity.js | 4 +- .../management/_index_pattern_results_sort.js | 8 +- test/functional/page_objects/settings_page.js | 72 ++--- yarn.lock | 4 + 24 files changed, 940 insertions(+), 169 deletions(-) create mode 100644 src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/__tests__/__snapshots__/indexed_fields_table.test.js.snap create mode 100644 src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/__tests__/indexed_fields_table.test.js create mode 100644 src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/components/table/__tests__/__snapshots__/table.test.js.snap create mode 100644 src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/components/table/__tests__/table.test.js create mode 100644 src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/components/table/index.js create mode 100644 src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/components/table/table.js delete mode 100644 src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/field_name.html delete mode 100644 src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/field_type.html delete mode 100644 src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/indexed_fields_table.html create mode 100644 src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/lib/__tests__/get_field_format.test.js create mode 100644 src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/lib/get_field_format.js create mode 100644 src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/lib/index.js diff --git a/package.json b/package.json index c25b1c8d733f3..ed07a0db7d1e4 100644 --- a/package.json +++ b/package.json @@ -190,6 +190,7 @@ "redux-thunk": "2.2.0", "regression": "2.0.0", "request": "2.61.0", + "reselect": "^3.0.1", "resize-observer-polyfill": "1.2.1", "rimraf": "2.4.3", "rison-node": "1.0.0", diff --git a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/edit_index_pattern.html b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/edit_index_pattern.html index 8afc1837e6c7c..0bf8f9b62d005 100644 --- a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/edit_index_pattern.html +++ b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/edit_index_pattern.html @@ -137,10 +137,7 @@
- +
diff --git a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/edit_index_pattern.js b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/edit_index_pattern.js index 8a8d1fb3c8e4f..ab15893609344 100644 --- a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/edit_index_pattern.js +++ b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/edit_index_pattern.js @@ -1,6 +1,5 @@ import _ from 'lodash'; import './index_header'; -import './indexed_fields_table'; import './scripted_field_editor'; import './source_filters_table'; import { KbnUrlProvider } from 'ui/url'; @@ -9,11 +8,14 @@ import { fatalError } from 'ui/notify'; import uiRoutes from 'ui/routes'; import { uiModules } from 'ui/modules'; import template from './edit_index_pattern.html'; +import { FieldWildcardProvider } from 'ui/field_wildcard'; import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; +import { IndexedFieldsTable } from './indexed_fields_table'; import { ScriptedFieldsTable } from './scripted_fields_table'; +const REACT_INDEXED_FIELDS_DOM_ELEMENT_ID = 'reactIndexedFieldsTable'; const REACT_SCRIPTED_FIELDS_DOM_ELEMENT_ID = 'reactScriptedFieldsTable'; function updateScriptedFieldsTable($scope, $state) { @@ -54,6 +56,41 @@ function destroyScriptedFieldsTable() { node && unmountComponentAtNode(node); } +function updateIndexedFieldsTable($scope, $state) { + if ($state.tab === 'indexedFields') { + $scope.$$postDigest(() => { + const node = document.getElementById(REACT_INDEXED_FIELDS_DOM_ELEMENT_ID); + if (!node) { + return; + } + + render( + { + $scope.kbnUrl.redirectToRoute(obj, route); + $scope.$apply(); + }, + }} + />, + node, + ); + }); + } else { + destroyIndexedFieldsTable(); + } +} + +function destroyIndexedFieldsTable() { + const node = document.getElementById(REACT_INDEXED_FIELDS_DOM_ELEMENT_ID); + node && unmountComponentAtNode(node); +} + uiRoutes .when('/management/kibana/indices/:indexPatternId', { template, @@ -87,7 +124,9 @@ uiModules.get('apps/management') $scope, $location, $route, config, courier, Notifier, Private, AppState, docTitle, confirmModal) { const notify = new Notifier(); const $state = $scope.state = new AppState(); + const { fieldWildcardMatcher } = Private(FieldWildcardProvider); + $scope.fieldWildcardMatcher = fieldWildcardMatcher; $scope.editSectionsProvider = Private(IndicesEditSectionsProvider); $scope.kbnUrl = Private(KbnUrlProvider); $scope.indexPattern = $route.current.locals.indexPattern; @@ -100,6 +139,9 @@ uiModules.get('apps/management') $scope.$watch('indexPattern.fields', function () { $scope.editSections = $scope.editSectionsProvider($scope.indexPattern); $scope.refreshFilters(); + $scope.fields = $scope.indexPattern.getNonScriptedFields(); + updateIndexedFieldsTable($scope, $state); + updateScriptedFieldsTable($scope, $state); }); $scope.refreshFilters = function () { @@ -123,6 +165,7 @@ uiModules.get('apps/management') $scope.changeTab = function (obj) { $state.tab = obj.index; + updateIndexedFieldsTable($scope, $state); updateScriptedFieldsTable($scope, $state); $state.save(); }; @@ -139,7 +182,10 @@ uiModules.get('apps/management') $scope.refreshFields = function () { const confirmModalOptions = { confirmButtonText: 'Refresh', - onConfirm: () => { $scope.indexPattern.refreshFields(); }, + onConfirm: async () => { + await $scope.indexPattern.refreshFields(); + $scope.fields = $scope.indexPattern.getNonScriptedFields(); + }, title: 'Refresh field list?' }; confirmModal( @@ -187,8 +233,21 @@ uiModules.get('apps/management') }; $scope.$watch('fieldFilter', () => { - if ($scope.fieldFilter !== undefined && $state.tab === 'scriptedFields') { - updateScriptedFieldsTable($scope, $state); + if ($scope.fieldFilter === undefined) { + return; + } + + switch($state.tab) { + case 'indexedFields': + updateIndexedFieldsTable($scope, $state); + case 'scriptedFields': + updateScriptedFieldsTable($scope, $state); + } + }); + + $scope.$watch('indexedFieldTypeFilter', () => { + if ($scope.indexedFieldTypeFilter !== undefined && $state.tab === 'indexedFields') { + updateIndexedFieldsTable($scope, $state); } }); @@ -199,8 +258,7 @@ uiModules.get('apps/management') }); $scope.$on('$destory', () => { + destroyIndexedFieldsTable(); destroyScriptedFieldsTable(); }); - - updateScriptedFieldsTable($scope, $state); }); diff --git a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/__tests__/__snapshots__/indexed_fields_table.test.js.snap b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/__tests__/__snapshots__/indexed_fields_table.test.js.snap new file mode 100644 index 0000000000000..f90041195be6f --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/__tests__/__snapshots__/indexed_fields_table.test.js.snap @@ -0,0 +1,97 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`IndexedFieldsTable should filter based on the query bar 1`] = ` +
+ + +`; + +exports[`IndexedFieldsTable should filter based on the type filter 1`] = ` +
+
+ +`; + +exports[`IndexedFieldsTable should render normally 1`] = ` +
+
+ +`; diff --git a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/__tests__/indexed_fields_table.test.js b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/__tests__/indexed_fields_table.test.js new file mode 100644 index 0000000000000..01d8f9dab7097 --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/__tests__/indexed_fields_table.test.js @@ -0,0 +1,85 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import { IndexedFieldsTable } from '../indexed_fields_table'; + +jest.mock('@elastic/eui', () => ({ + EuiFlexGroup: 'eui-flex-group', + EuiFlexItem: 'eui-flex-item', + EuiIcon: 'eui-icon', + EuiInMemoryTable: 'eui-in-memory-table', + TooltipTrigger: 'tooltip-trigger' +})); + +jest.mock('../components/table', () => ({ + // Note: this seems to fix React complaining about non lowercase attributes + Table: () => { + return 'table'; + } +})); + +const helpers = { + redirectToRoute: () => {}, +}; + +const fields = [ + { name: 'Elastic', displayName: 'Elastic', searchable: true }, + { name: 'timestamp', displayName: 'timestamp', type: 'date' }, + { name: 'conflictingField', displayName: 'conflictingField', type: 'conflict' }, +]; + +const indexPattern = { + getNonScriptedFields: () => fields, +}; + +describe('IndexedFieldsTable', () => { + it('should render normally', async () => { + const component = shallow( + {}} + /> + ); + + await new Promise(resolve => process.nextTick(resolve)); + component.update(); + + expect(component).toMatchSnapshot(); + }); + + it('should filter based on the query bar', async () => { + const component = shallow( + {}} + /> + ); + + await new Promise(resolve => process.nextTick(resolve)); + component.setProps({ fieldFilter: 'Elast' }); + component.update(); + + expect(component).toMatchSnapshot(); + }); + + it('should filter based on the type filter', async () => { + const component = shallow( + {}} + /> + ); + + await new Promise(resolve => process.nextTick(resolve)); + component.setProps({ indexedFieldTypeFilter: 'date' }); + component.update(); + + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/components/table/__tests__/__snapshots__/table.test.js.snap b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/components/table/__tests__/__snapshots__/table.test.js.snap new file mode 100644 index 0000000000000..980370d081a70 --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/components/table/__tests__/__snapshots__/table.test.js.snap @@ -0,0 +1,245 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Table should render conflicting type 1`] = ` +
+ + + conflict + + + + + + + +
+`; + +exports[`Table should render normal field name 1`] = ` +
+ + + Elastic + + +
+`; + +exports[`Table should render normal type 1`] = ` +
+ + + string + + +
+`; + +exports[`Table should render normally 1`] = ` + +`; + +exports[`Table should render the boolean template (false) 1`] = ``; + +exports[`Table should render the boolean template (true) 1`] = ` + +`; + +exports[`Table should render timestamp field name 1`] = ` +
+ + + timestamp + + + + + + + +
+`; diff --git a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/components/table/__tests__/table.test.js b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/components/table/__tests__/table.test.js new file mode 100644 index 0000000000000..a0096b7f38861 --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/components/table/__tests__/table.test.js @@ -0,0 +1,122 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import { Table } from '../table'; + +const indexPattern = { + timeFieldName: 'timestamp' +}; + +const items = [ + { name: 'Elastic', displayName: 'Elastic', searchable: true }, + { name: 'timestamp', displayName: 'timestamp', type: 'date' }, + { name: 'conflictingField', displayName: 'conflictingField', type: 'conflict' }, +]; + +describe('Table', () => { + it('should render normally', async () => { + const component = shallow( +
{}} + /> + ); + + expect(component).toMatchSnapshot(); + }); + + it('should render normal field name', async () => { + const component = shallow( +
{}} + /> + ); + + const tableCell = shallow(component.prop('columns')[0].render('Elastic')); + expect(tableCell).toMatchSnapshot(); + }); + + it('should render timestamp field name', async () => { + const component = shallow( +
{}} + /> + ); + + const tableCell = shallow(component.prop('columns')[0].render('timestamp', true)); + expect(tableCell).toMatchSnapshot(); + }); + + it('should render the boolean template (true)', async () => { + const component = shallow( +
{}} + /> + ); + + const tableCell = shallow(component.prop('columns')[3].render(true)); + expect(tableCell).toMatchSnapshot(); + }); + + it('should render the boolean template (false)', async () => { + const component = shallow( +
{}} + /> + ); + + const tableCell = shallow(component.prop('columns')[3].render(false)); + expect(tableCell).toMatchSnapshot(); + }); + + it('should render normal type', async () => { + const component = shallow( +
{}} + /> + ); + + const tableCell = shallow(component.prop('columns')[1].render('string')); + expect(tableCell).toMatchSnapshot(); + }); + + it('should render conflicting type', async () => { + const component = shallow( +
{}} + /> + ); + + const tableCell = shallow(component.prop('columns')[1].render('conflict', true)); + expect(tableCell).toMatchSnapshot(); + }); + + it('should allow edits', () => { + const editField = jest.fn(); + + const component = shallow( +
+ ); + + // Click the edit button + component.prop('columns')[6].actions[0].onClick(); + expect(editField).toBeCalled(); + }); +}); diff --git a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/components/table/index.js b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/components/table/index.js new file mode 100644 index 0000000000000..01643f0f57fd8 --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/components/table/index.js @@ -0,0 +1 @@ +export * from './table'; diff --git a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/components/table/table.js b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/components/table/table.js new file mode 100644 index 0000000000000..191e7668e67a4 --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/components/table/table.js @@ -0,0 +1,140 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; + +import { + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiInMemoryTable, + TooltipTrigger +} from '@elastic/eui'; + +export class Table extends PureComponent { + static propTypes = { + indexPattern: PropTypes.object.isRequired, + items: PropTypes.array.isRequired, + editField: PropTypes.func.isRequired + } + + renderBooleanTemplate(value) { + return value ? : ; + } + + renderFieldName(name, isTimeField) { + return ( +
+ + + {name} + + {isTimeField ? ( + + + + + + ) : ''} + +
+ ); + } + + renderFieldType(type, isConflict) { + return ( +
+ + + {type} + + {isConflict ? ( + + + + + + ) : ''} + +
+ ); + } + + render() { + const { indexPattern, items, editField } = this.props; + + const pagination = { + pageSizeOptions: [5, 10, 25, 50] + }; + + const columns = [ + { + field: 'displayName', + name: 'Name', + dataType: 'string', + sortable: true, + render: (value) => { + return this.renderFieldName(value, indexPattern.timeFieldName === value); + }, + }, + { + field: 'type', + name: 'Type', + dataType: 'string', + sortable: true, + render: (value) => { + return this.renderFieldType(value, value === 'conflict'); + }, + }, + { + field: 'format', + name: 'Format', + dataType: 'string', + sortable: true, + }, + { + field: 'searchable', + name: 'Searchable', + description: `These fields can be used in the filter bar`, + dataType: 'boolean', + sortable: true, + render: this.renderBooleanTemplate, + }, + { + field: 'aggregatable', + name: 'Aggregatable', + description: `These fields can be used in visualization aggregations`, + dataType: 'boolean', + sortable: true, + render: this.renderBooleanTemplate, + }, + { + field: 'excluded', + name: 'Excluded', + description: `Fields that are excluded from _source when it is fetched`, + dataType: 'boolean', + sortable: true, + render: this.renderBooleanTemplate, + }, + { + name: '', + actions: [ + { + name: 'Edit', + description: 'Edit', + icon: 'pencil', + onClick: editField, + type: 'icon', + }, + ], + } + ]; + + return ( + + ); + } +} diff --git a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/field_name.html b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/field_name.html deleted file mode 100644 index 7d14896a40fe1..0000000000000 --- a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/field_name.html +++ /dev/null @@ -1,8 +0,0 @@ -{{field.displayName}} -  - - - diff --git a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/field_type.html b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/field_type.html deleted file mode 100644 index 1a5cb6eb8e1d9..0000000000000 --- a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/field_type.html +++ /dev/null @@ -1,7 +0,0 @@ -{{field.type}} - - diff --git a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/index.js b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/index.js index b1f075fecd6d6..d5d6648bdd0ab 100644 --- a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/index.js +++ b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/index.js @@ -1 +1 @@ -import './indexed_fields_table'; +export { IndexedFieldsTable } from './indexed_fields_table'; diff --git a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/indexed_fields_table.html b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/indexed_fields_table.html deleted file mode 100644 index 1581786fc8e6f..0000000000000 --- a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/indexed_fields_table.html +++ /dev/null @@ -1,9 +0,0 @@ - - - -

No matching fields found.

diff --git a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/indexed_fields_table.js b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/indexed_fields_table.js index 24f01efc63857..f73b5224927d5 100644 --- a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/indexed_fields_table.js +++ b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/indexed_fields_table.js @@ -1,92 +1,91 @@ -import _ from 'lodash'; -import 'ui/paginated_table'; -import fieldNameHtml from './field_name.html'; -import fieldTypeHtml from './field_type.html'; -import fieldControlsHtml from '../field_controls.html'; -import { uiModules } from 'ui/modules'; -import { FieldWildcardProvider } from 'ui/field_wildcard'; -import template from './indexed_fields_table.html'; +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { createSelector } from 'reselect'; -uiModules.get('apps/management') - .directive('indexedFieldsTable', function (Private, $filter) { - const yesTemplate = ''; - const noTemplate = ''; - const filter = $filter('filter'); - const { fieldWildcardMatcher } = Private(FieldWildcardProvider); +import { Table } from './components/table'; +import { + getFieldFormat +} from './lib'; - return { - restrict: 'E', - template, - scope: true, - link: function ($scope) { - const rowScopes = []; // track row scopes, so they can be destroyed as needed - $scope.perPage = 25; - $scope.columns = [ - { title: 'name' }, - { title: 'type' }, - { title: 'format' }, - { title: 'searchable', info: 'These fields can be used in the filter bar' }, - { title: 'aggregatable', info: 'These fields can be used in visualization aggregations' }, - { title: 'excluded', info: 'Fields that are excluded from _source when it is fetched' }, - { title: 'controls', sortable: false } - ]; +export class IndexedFieldsTable extends Component { + static propTypes = { + fields: PropTypes.array.isRequired, + indexPattern: PropTypes.object.isRequired, + fieldFilter: PropTypes.string, + indexedFieldTypeFilter: PropTypes.string, + helpers: PropTypes.shape({ + redirectToRoute: PropTypes.func.isRequired, + }), + fieldWildcardMatcher: PropTypes.func.isRequired, + } - $scope.$watchMulti(['[]indexPattern.fields', 'fieldFilter', 'indexedFieldTypeFilter'], refreshRows); + constructor(props) { + super(props); - function refreshRows() { - // clear and destroy row scopes - _.invoke(rowScopes.splice(0), '$destroy'); - const fields = filter($scope.indexPattern.getNonScriptedFields(), { - name: $scope.fieldFilter, - type: $scope.indexedFieldTypeFilter - }); - const sourceFilters = $scope.indexPattern.sourceFilters && $scope.indexPattern.sourceFilters.map(f => f.value) || []; - const fieldWildcardMatch = fieldWildcardMatcher(sourceFilters); - _.find($scope.editSections, { index: 'indexedFields' }).count = fields.length; // Update the tab count + this.state = { + fields: this.mapFields(this.props.fields) + }; + } + + componentWillReceiveProps(nextProps) { + if (nextProps.fields !== this.props.fields) { + this.setState({ + fields: this.mapFields(nextProps.fields) + }); + } + } - $scope.rows = fields.map(function (field) { - const childScope = _.assign($scope.$new(), { field: field }); - rowScopes.push(childScope); + mapFields(fields) { + const { indexPattern, fieldWildcardMatcher } = this.props; + const sourceFilters = indexPattern.sourceFilters && indexPattern.sourceFilters.map(f => f.value); + const fieldWildcardMatch = fieldWildcardMatcher(sourceFilters || []); - const excluded = fieldWildcardMatch(field.name); + return fields && fields + .map((field) => { + return { + ...field, + displayName: field.displayName, + routes: field.routes, + indexPattern: field.indexPattern, + format: getFieldFormat(indexPattern, field.name), + excluded: fieldWildcardMatch ? fieldWildcardMatch(field.name) : false, + }; + }) || []; + } - return [ - { - markup: fieldNameHtml, - scope: childScope, - value: field.displayName, - attr: { - 'data-test-subj': 'indexedFieldName' - } - }, - { - markup: fieldTypeHtml, - scope: childScope, - value: field.type, - attr: { - 'data-test-subj': 'indexedFieldType' - } - }, - _.get($scope.indexPattern, ['fieldFormatMap', field.name, 'type', 'title']), - { - markup: field.searchable ? yesTemplate : noTemplate, - value: field.searchable - }, - { - markup: field.aggregatable ? yesTemplate : noTemplate, - value: field.aggregatable - }, - { - markup: excluded ? yesTemplate : noTemplate, - value: excluded - }, - { - markup: fieldControlsHtml, - scope: childScope - } - ]; - }); - } + getFilteredFields = createSelector( + (state) => state.fields, + (state, props) => props.fieldFilter, + (state, props) => props.indexedFieldTypeFilter, + (fields, fieldFilter, indexedFieldTypeFilter) => { + if (fieldFilter) { + const normalizedFieldFilter = fieldFilter.toLowerCase(); + fields = fields.filter(field => field.name.toLowerCase().includes(normalizedFieldFilter)); } - }; - }); + + if (indexedFieldTypeFilter) { + fields = fields.filter(field => field.type === indexedFieldTypeFilter); + } + + return fields; + } + ); + + render() { + const { + indexPattern, + } = this.props; + + const fields = this.getFilteredFields(this.state, this.props); + + return ( +
+
this.props.helpers.redirectToRoute(field, 'edit')} + /> + + ); + } +} diff --git a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/lib/__tests__/get_field_format.test.js b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/lib/__tests__/get_field_format.test.js new file mode 100644 index 0000000000000..a69f4d53d2a0c --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/lib/__tests__/get_field_format.test.js @@ -0,0 +1,34 @@ +import { getFieldFormat } from '../get_field_format'; + +const indexPattern = { + fieldFormatMap: { + Elastic: { + type: { + title: 'string' + } + } + } +}; + +describe('getFieldFormat', () => { + + it('should handle no arguments', () => { + expect(getFieldFormat()).toEqual(''); + }); + + it('should handle no field name', () => { + expect(getFieldFormat(indexPattern)).toEqual(''); + }); + + it('should handle empty name', () => { + expect(getFieldFormat(indexPattern, '')).toEqual(''); + }); + + it('should handle undefined field name', () => { + expect(getFieldFormat(indexPattern, 'none')).toEqual(undefined); + }); + + it('should retrieve field format', () => { + expect(getFieldFormat(indexPattern, 'Elastic')).toEqual('string'); + }); +}); diff --git a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/lib/get_field_format.js b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/lib/get_field_format.js new file mode 100644 index 0000000000000..c5660052cab15 --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/lib/get_field_format.js @@ -0,0 +1,5 @@ +import { get } from 'lodash'; + +export function getFieldFormat(indexPattern, fieldName) { + return indexPattern && fieldName ? get(indexPattern, ['fieldFormatMap', fieldName, 'type', 'title']) : ''; +} diff --git a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/lib/index.js b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/lib/index.js new file mode 100644 index 0000000000000..35ee050ca4849 --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/indexed_fields_table/lib/index.js @@ -0,0 +1 @@ +export { getFieldFormat } from './get_field_format'; diff --git a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/components/table/__tests__/__snapshots__/table.test.js.snap b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/components/table/__tests__/__snapshots__/table.test.js.snap index 91a623f8cf899..1d1485a56f7e4 100644 --- a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/components/table/__tests__/__snapshots__/table.test.js.snap +++ b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/components/table/__tests__/__snapshots__/table.test.js.snap @@ -8,7 +8,7 @@ exports[`Table should render normally 1`] = ` Object { "dataType": "string", "description": "Name of the field", - "field": "name", + "field": "displayName", "name": "Name", "sortable": true, }, @@ -62,7 +62,7 @@ exports[`Table should render normally 1`] = ` 50, ], }, - "recordId": "id", + "recordId": "name", "selection": undefined, } } diff --git a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/components/table/table.js b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/components/table/table.js index 9f75c6e7874a5..d7d0585724cf4 100644 --- a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/components/table/table.js +++ b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/components/table/table.js @@ -45,10 +45,10 @@ export class Table extends PureComponent { const { editField, deleteField, onDataCriteriaChange } = this.props; return { - recordId: 'id', + recordId: 'name', columns: [ { - field: 'name', + field: 'displayName', name: 'Name', description: `Name of the field`, dataType: 'string', diff --git a/test/functional/apps/management/_index_pattern_create_delete.js b/test/functional/apps/management/_index_pattern_create_delete.js index 3a2ab1137dba5..76a4ab9f64d7a 100644 --- a/test/functional/apps/management/_index_pattern_create_delete.js +++ b/test/functional/apps/management/_index_pattern_create_delete.js @@ -47,13 +47,13 @@ export default function ({ getService, getPageObjects }) { .then(function (headers) { log.debug('header.length = ' + headers.length); const expectedHeaders = [ - 'name', - 'type', - 'format', - 'searchable', - 'aggregatable', - 'excluded', - 'controls' + 'Name', + 'Type', + 'Format', + 'Searchable', + 'Aggregatable', + 'Excluded', + '' ]; expect(headers.length).to.be(expectedHeaders.length); diff --git a/test/functional/apps/management/_index_pattern_popularity.js b/test/functional/apps/management/_index_pattern_popularity.js index 5830399327211..48d6fee60dda3 100644 --- a/test/functional/apps/management/_index_pattern_popularity.js +++ b/test/functional/apps/management/_index_pattern_popularity.js @@ -28,13 +28,13 @@ export default function ({ getService, getPageObjects }) { // set the page size to All again, https://github.com/elastic/kibana/issues/5030 // TODO: remove this after issue #5030 is closed async function fix5030() { - await PageObjects.settings.setPageSize('All'); + await PageObjects.settings.setPageSize(50); await PageObjects.common.sleep(1000); } beforeEach(async function () { // increase Popularity of geo.coordinates - await PageObjects.settings.setPageSize('All'); + await PageObjects.settings.setPageSize(50); await PageObjects.common.sleep(1000); log.debug('Starting openControlsByName (' + fieldName + ')'); await PageObjects.settings.openControlsByName(fieldName); diff --git a/test/functional/apps/management/_index_pattern_results_sort.js b/test/functional/apps/management/_index_pattern_results_sort.js index 827db54655edc..d2d5670f8ed11 100644 --- a/test/functional/apps/management/_index_pattern_results_sort.js +++ b/test/functional/apps/management/_index_pattern_results_sort.js @@ -12,14 +12,14 @@ export default function ({ getService, getPageObjects }) { }); const columns = [{ - heading: 'name', + heading: 'Name', first: '@message', last: 'xss.raw', selector: function () { return PageObjects.settings.getTableRow(0, 0).getVisibleText(); } }, { - heading: 'type', + heading: 'Type', first: '_source', last: 'string', selector: function () { @@ -70,7 +70,7 @@ export default function ({ getService, getPageObjects }) { }); describe('field list pagination', function () { - const EXPECTED_DEFAULT_PAGE_SIZE = 25; + const EXPECTED_DEFAULT_PAGE_SIZE = 5; const EXPECTED_FIELD_COUNT = 86; const EXPECTED_LAST_PAGE_COUNT = EXPECTED_FIELD_COUNT % EXPECTED_DEFAULT_PAGE_SIZE; const LAST_PAGE_NUMBER = Math.ceil(EXPECTED_FIELD_COUNT / EXPECTED_DEFAULT_PAGE_SIZE); @@ -98,7 +98,7 @@ export default function ({ getService, getPageObjects }) { it('should have correct default page size selected', function () { return PageObjects.settings.getPageSize() .then(function (pageSize) { - expect(pageSize).to.be('' + EXPECTED_DEFAULT_PAGE_SIZE); + expect(pageSize).to.be('Rows per page: ' + EXPECTED_DEFAULT_PAGE_SIZE); }); }); diff --git a/test/functional/page_objects/settings_page.js b/test/functional/page_objects/settings_page.js index 3430a9d0cfe82..8baa1395e087b 100644 --- a/test/functional/page_objects/settings_page.js +++ b/test/functional/page_objects/settings_page.js @@ -140,12 +140,12 @@ export function SettingsPageProvider({ getService, getPageObjects }) { getTableHeader() { return remote.setFindTimeout(defaultFindTimeout) - .findAllByCssSelector('table.table.table-condensed thead tr th'); + .findAllByCssSelector('table.euiTable thead tr th'); } sortBy(columnName) { return remote.setFindTimeout(defaultFindTimeout) - .findAllByCssSelector('table.table.table-condensed thead tr th span') + .findAllByCssSelector('table.euiTable thead tr th button') .then(function (chartTypes) { function getChartType(chart) { return chart.getVisibleText() @@ -167,9 +167,9 @@ export function SettingsPageProvider({ getService, getPageObjects }) { getTableRow(rowNumber, colNumber) { return remote.setFindTimeout(defaultFindTimeout) // passing in zero-based index, but adding 1 for css 1-based indexes - .findByCssSelector('div.agg-table-paginated table.table.table-condensed tbody tr:nth-child(' + - (rowNumber + 1) + ') td.ng-scope:nth-child(' + - (colNumber + 1) + ') span.ng-binding' + .findByCssSelector('table.euiTable tbody tr:nth-child(' + + (rowNumber + 1) + ') td.euiTableRowCell:nth-child(' + + (colNumber + 1) + ')' ); } @@ -190,29 +190,9 @@ export function SettingsPageProvider({ getService, getPageObjects }) { } getPageSize() { - let selectedItemLabel = ''; return remote.setFindTimeout(defaultFindTimeout) - .findAllByCssSelector('select.ng-pristine.ng-valid.ng-untouched option') - .then(function (chartTypes) { - function getChartType(chart) { - const thisChart = chart; - return chart.isSelected() - .then(function (isSelected) { - if (isSelected === true) { - return thisChart.getProperty('label') - .then(function (theLabel) { - selectedItemLabel = theLabel; - }); - } - }); - } - - const getChartTypesPromises = chartTypes.map(getChartType); - return Promise.all(getChartTypesPromises); - }) - .then(() => { - return selectedItemLabel; - }); + .findByCssSelector('div.euiPopover button.euiButtonEmpty span.euiButtonEmpty__content span') + .getVisibleText(); } async getFieldNames() { @@ -249,9 +229,14 @@ export function SettingsPageProvider({ getService, getPageObjects }) { } async goToPage(pageNum) { + const pageButtons = await remote.setFindTimeout(defaultFindTimeout) + .findAllByCssSelector('.euiPagination button.euiPaginationButton') + .getVisibleText(); + await remote.setFindTimeout(defaultFindTimeout) - .findByCssSelector(`[data-test-subj="paginationControls"] li:nth-child(${pageNum + 1}) button`) + .findByCssSelector(`.euiPagination button.euiPaginationButton:nth-child(${pageButtons.indexOf(pageNum + '') + 2})`) .click(); + await PageObjects.header.waitUntilLoadingHasFinished(); } @@ -262,8 +247,13 @@ export function SettingsPageProvider({ getService, getPageObjects }) { } async openControlsByName(name) { + const tableFields = await remote.setFindTimeout(defaultFindTimeout) + .findAllByCssSelector('table.euiTable tbody tr.euiTableRow td.euiTableRowCell:first-child') + .getVisibleText(); + await remote.setFindTimeout(defaultFindTimeout) - .findByCssSelector('[data-test-subj="indexPatternFieldEditButton"][href$="/' + name + '"]') + .findAllByCssSelector(`table.euiTable tbody tr.euiTableRow:nth-child(${tableFields.indexOf(name) + 1}) + td:last-child button`) .click(); } @@ -289,10 +279,26 @@ export function SettingsPageProvider({ getService, getPageObjects }) { } async setPageSize(size) { - await remote.setFindTimeout(defaultFindTimeout) - .findByCssSelector(`[data-test-subj="paginateControlsPageSizeSelect"] option[label="${size}"]`) - .click(); - await PageObjects.header.waitUntilLoadingHasFinished(); + try { + await remote.setFindTimeout(defaultFindTimeout) + .findByCssSelector('div.euiPopover button.euiButtonEmpty') + .click(); + + const sizeButtons = await remote.setFindTimeout(defaultFindTimeout) + .findAllByCssSelector('div.euiPopover .euiContextMenuPanel button.euiContextMenuItem') + .getVisibleText(); + + await remote.setFindTimeout(defaultFindTimeout) + .findAllByCssSelector(`div.euiPopover .euiContextMenuPanel + button.euiContextMenuItem:nth-child(${sizeButtons.indexOf(size + ' rows') + 1})`) + .click(); + } catch(e) { + await remote.setFindTimeout(defaultFindTimeout) + .findByCssSelector(`[data-test-subj="paginateControlsPageSizeSelect"] option[label="${size}"]`) + .click(); + } finally { + await PageObjects.header.waitUntilLoadingHasFinished(); + } } async createIndexPattern(indexPatternName, timefield = '@timestamp') { diff --git a/yarn.lock b/yarn.lock index b0a8be6e54e0d..6e31465a2049b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10718,6 +10718,10 @@ requires-port@1.0.x, requires-port@1.x.x, requires-port@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" +reselect@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-3.0.1.tgz#efdaa98ea7451324d092b2b2163a6a1d7a9a2147" + resize-observer-polyfill@1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.2.1.tgz#55a4ff3e4f212a76470835fb7590dbb62a3e6542"