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 2df9f88abd760..8afc1837e6c7c 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 @@ -23,10 +23,7 @@

- This page lists every field in the {{::indexPattern.title}} - index and the field's associated core type as recorded by Elasticsearch. - While this list allows you to view the core type of each field, changing - field types must be done using Elasticsearch's + This page lists every field in the {{::indexPattern.title}} index and the field's associated core type as recorded by Elasticsearch. To change a field type, use the Elasticsearch Mapping API @@ -145,10 +142,7 @@ class="fields indexed-fields" > - +

{ + const node = document.getElementById(REACT_SCRIPTED_FIELDS_DOM_ELEMENT_ID); + if (!node) { + return; + } + + render( + { + $scope.kbnUrl.redirectToRoute(obj, route); + $scope.$apply(); + }, + getRouteHref: (obj, route) => $scope.kbnUrl.getRouteHref(obj, route), + }} + onRemoveField={() => { + $scope.editSections = $scope.editSectionsProvider($scope.indexPattern); + $scope.refreshFilters(); + }} + />, + node, + ); + }); + } else { + destroyScriptedFieldsTable(); + } +} + +function destroyScriptedFieldsTable() { + const node = document.getElementById(REACT_SCRIPTED_FIELDS_DOM_ELEMENT_ID); + node && unmountComponentAtNode(node); +} + uiRoutes .when('/management/kibana/indices/:indexPatternId', { template, @@ -45,6 +88,7 @@ uiModules.get('apps/management') const notify = new Notifier(); const $state = $scope.state = new AppState(); + $scope.editSectionsProvider = Private(IndicesEditSectionsProvider); $scope.kbnUrl = Private(KbnUrlProvider); $scope.indexPattern = $route.current.locals.indexPattern; docTitle.change($scope.indexPattern.title); @@ -54,7 +98,7 @@ uiModules.get('apps/management') }); $scope.$watch('indexPattern.fields', function () { - $scope.editSections = Private(IndicesEditSectionsProvider)($scope.indexPattern); + $scope.editSections = $scope.editSectionsProvider($scope.indexPattern); $scope.refreshFilters(); }); @@ -79,6 +123,7 @@ uiModules.get('apps/management') $scope.changeTab = function (obj) { $state.tab = obj.index; + updateScriptedFieldsTable($scope, $state); $state.save(); }; @@ -140,4 +185,22 @@ uiModules.get('apps/management') $scope.indexPattern.timeFieldName = field.name; return $scope.indexPattern.save(); }; + + $scope.$watch('fieldFilter', () => { + if ($scope.fieldFilter !== undefined && $state.tab === 'scriptedFields') { + updateScriptedFieldsTable($scope, $state); + } + }); + + $scope.$watch('scriptedFieldLanguageFilter', () => { + if ($scope.scriptedFieldLanguageFilter !== undefined && $state.tab === 'scriptedFields') { + updateScriptedFieldsTable($scope, $state); + } + }); + + $scope.$on('$destory', () => { + destroyScriptedFieldsTable(); + }); + + updateScriptedFieldsTable($scope, $state); }); diff --git a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/edit_sections.js b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/edit_sections.js index c83b7bbe2d4c2..c538a894ed287 100644 --- a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/edit_sections.js +++ b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/edit_sections.js @@ -15,17 +15,17 @@ export function IndicesEditSectionsProvider() { return [ { - title: 'fields', + title: 'Fields', index: 'indexedFields', count: fieldCount.indexed }, { - title: 'scripted fields', + title: 'Scripted fields', index: 'scriptedFields', count: fieldCount.scripted }, { - title: 'source filters', + title: 'Source filters', index: 'sourceFilters', count: fieldCount.sourceFilters } diff --git a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/__tests__/__snapshots__/scripted_field_table.test.js.snap b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/__tests__/__snapshots__/scripted_field_table.test.js.snap new file mode 100644 index 0000000000000..e80f3de86d401 --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/__tests__/__snapshots__/scripted_field_table.test.js.snap @@ -0,0 +1,255 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ScriptedFieldsTable should filter based on the lang filter 1`] = ` +
+
+ + + Add scripted field + + + + +`; + +exports[`ScriptedFieldsTable should filter based on the query bar 1`] = ` +
+
+ + + Add scripted field + + +
+ +`; + +exports[`ScriptedFieldsTable should hide the table if there are no scripted fields 1`] = ` +
+
+ + + Add scripted field + + +
+`; + +exports[`ScriptedFieldsTable should render normally 1`] = ` +
+
+ + + Add scripted field + + +
+ +`; + +exports[`ScriptedFieldsTable should show a delete modal 1`] = ` +
+
+ + + Add scripted field + + +
+ + + + +`; diff --git a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/__tests__/scripted_field_table.test.js b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/__tests__/scripted_field_table.test.js new file mode 100644 index 0000000000000..63a39ec8c08b7 --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/__tests__/scripted_field_table.test.js @@ -0,0 +1,169 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import { ScriptedFieldsTable } from '../scripted_fields_table'; + +jest.mock('@elastic/eui', () => ({ + EuiButton: 'eui-button', + EuiTableOfRecords: 'eui-table-of-records', + EuiTitle: 'eui-title', + EuiText: 'eui-text', + EuiButton: 'eui-button', + EuiHorizontalRule: 'eui-horizontal-rule', + EuiSpacer: 'eui-spacer', + EuiCallOut: 'eui-call-out', + EuiLink: 'eui-link', + EuiOverlayMask: 'eui-overlay-mask', + EuiConfirmModal: 'eui-confirm-modal', + Comparators: { + property: () => {}, + default: () => {}, + }, +})); +jest.mock('../components/header', () => ({ Header: 'header' })); +jest.mock('../components/call_outs', () => ({ CallOuts: 'call-outs' })); +jest.mock('../components/table', () => ({ + // Note: this seems to fix React complaining about non lowercase attributes + Table: () => { + return 'table'; + } +})); +jest.mock('ui/scripting_languages', () => ({ + getSupportedScriptingLanguages: () => ['painless'], + getDeprecatedScriptingLanguages: () => [], +})); +jest.mock('ui/documentation_links', () => ({ + documentationLinks: { + scriptedFields: { + painless: 'painlessDocs' + } + } +})); + +const helpers = { + redirectToRoute: () => {}, + getRouteHref: () => {}, +}; + +const indexPattern = { + getScriptedFields: () => ([ + { name: 'ScriptedField', lang: 'painless', script: 'x++' }, + { name: 'JustATest', lang: 'painless', script: 'z++' }, + ]) +}; + +describe('ScriptedFieldsTable', () => { + it('should render normally', async () => { + const component = shallow( + + ); + + // Allow the componentWillMount code to execute + // https://github.com/airbnb/enzyme/issues/450 + await component.update(); // Fire `componentWillMount()` + await component.update(); // Force update the component post async actions + + expect(component).toMatchSnapshot(); + }); + + it('should filter based on the query bar', async () => { + const component = shallow( + + ); + + // Allow the componentWillMount code to execute + // https://github.com/airbnb/enzyme/issues/450 + await component.update(); // Fire `componentWillMount()` + await component.update(); // Force update the component post async actions + + component.setProps({ fieldFilter: 'Just' }); + component.update(); + + expect(component).toMatchSnapshot(); + }); + + it('should filter based on the lang filter', async () => { + const component = shallow( + ([ + { name: 'ScriptedField', lang: 'painless', script: 'x++' }, + { name: 'JustATest', lang: 'painless', script: 'z++' }, + { name: 'Bad', lang: 'somethingElse', script: 'z++' }, + ]) + }} + helpers={helpers} + /> + ); + + // Allow the componentWillMount code to execute + // https://github.com/airbnb/enzyme/issues/450 + await component.update(); // Fire `componentWillMount()` + await component.update(); // Force update the component post async actions + + component.setProps({ scriptedFieldLanguageFilter: 'painless' }); + component.update(); + + expect(component).toMatchSnapshot(); + }); + + it('should hide the table if there are no scripted fields', async () => { + const component = shallow( + ([]) + }} + helpers={helpers} + /> + ); + + // Allow the componentWillMount code to execute + // https://github.com/airbnb/enzyme/issues/450 + await component.update(); // Fire `componentWillMount()` + await component.update(); // Force update the component post async actions + + expect(component).toMatchSnapshot(); + }); + + it('should show a delete modal', async () => { + const component = shallow( + + ); + + await component.update(); // Fire `componentWillMount()` + component.instance().startDeleteField({ name: 'ScriptedField' }); + await component.update(); + + // Ensure the modal is visible + expect(component).toMatchSnapshot(); + }); + + it('should delete a field', async () => { + const removeScriptedField = jest.fn(); + const component = shallow( + + ); + + await component.update(); // Fire `componentWillMount()` + component.instance().startDeleteField({ name: 'ScriptedField' }); + await component.update(); + await component.instance().deleteField(); + await component.update(); + expect(removeScriptedField).toBeCalled(); + }); +}); diff --git a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/components/call_outs/__tests__/__snapshots__/call_outs.test.js.snap b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/components/call_outs/__tests__/__snapshots__/call_outs.test.js.snap new file mode 100644 index 0000000000000..66b1cfd191a60 --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/components/call_outs/__tests__/__snapshots__/call_outs.test.js.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CallOuts should render normally 1`] = ` +
+ +

+ The following deprecated languages are in use: + php + . Support for these languages will be removed in the next major version of Kibana and Elasticsearch. Convert you scripted fields to + + Painless + + to avoid any problems. +

+
+ +
+`; + +exports[`CallOuts should render without any call outs 1`] = `""`; diff --git a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/components/call_outs/__tests__/call_outs.test.js b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/components/call_outs/__tests__/call_outs.test.js new file mode 100644 index 0000000000000..4395c830d6f4a --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/components/call_outs/__tests__/call_outs.test.js @@ -0,0 +1,28 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import { CallOuts } from '../call_outs'; + +describe('CallOuts', () => { + it('should render normally', async () => { + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + }); + + it('should render without any call outs', async () => { + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/components/call_outs/call_outs.js b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/components/call_outs/call_outs.js new file mode 100644 index 0000000000000..1210d5ade6f5a --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/components/call_outs/call_outs.js @@ -0,0 +1,33 @@ +import React from 'react'; + +import { + EuiCallOut, + EuiLink, + EuiSpacer, +} from '@elastic/eui'; + +export const CallOuts = ({ + deprecatedLangsInUse, + painlessDocLink, +}) => { + if (!deprecatedLangsInUse.length) { + return null; + } + + return ( +
+ +

+ The following deprecated languages are in use: {deprecatedLangsInUse.join(', ')}. + Support for these languages will be removed in the next major version of Kibana + and Elasticsearch. Convert you scripted fields to Painless to avoid any problems. +

+
+ +
+ ); +}; diff --git a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/components/call_outs/index.js b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/components/call_outs/index.js new file mode 100644 index 0000000000000..0df8edb404e9d --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/components/call_outs/index.js @@ -0,0 +1 @@ +export { CallOuts } from './call_outs'; diff --git a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/components/header/__tests__/__snapshots__/header.test.js.snap b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/components/header/__tests__/__snapshots__/header.test.js.snap new file mode 100644 index 0000000000000..546262f62f45a --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/components/header/__tests__/__snapshots__/header.test.js.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Header should render normally 1`] = ` +
+ +

+ Scripted fields +

+
+ +

+ You can use scripted fields in visualizations and display them in your documents. However, you cannot search scripted fields. +

+
+ +
+`; diff --git a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/components/header/__tests__/header.test.js b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/components/header/__tests__/header.test.js new file mode 100644 index 0000000000000..aa668eef621fa --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/components/header/__tests__/header.test.js @@ -0,0 +1,14 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import { Header } from '../header'; + +describe('Header', () => { + it('should render normally', async () => { + const component = shallow( +
+ ); + + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/components/header/header.js b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/components/header/header.js new file mode 100644 index 0000000000000..9227883a6a034 --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/components/header/header.js @@ -0,0 +1,22 @@ +import React from 'react'; + +import { + EuiTitle, + EuiText, + EuiSpacer, +} from '@elastic/eui'; + +export const Header = () => ( +
+ +

Scripted fields

+
+ +

+ You can use scripted fields in visualizations and display them in your documents. + However, you cannot search scripted fields. +

+
+ +
+); diff --git a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/components/header/index.js b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/components/header/index.js new file mode 100644 index 0000000000000..ddd9723152366 --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/components/header/index.js @@ -0,0 +1 @@ +export { Header } from './header'; 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 new file mode 100644 index 0000000000000..91a623f8cf899 --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/components/table/__tests__/__snapshots__/table.test.js.snap @@ -0,0 +1,99 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Table should render normally 1`] = ` + +`; + +exports[`Table should render the format 1`] = ` + + string + +`; diff --git a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/components/table/__tests__/table.test.js b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/components/table/__tests__/table.test.js new file mode 100644 index 0000000000000..02a2e26287868 --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/components/table/__tests__/table.test.js @@ -0,0 +1,99 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import { Table } from '../table'; + +const indexPattern = { + fieldFormatMap: { + Elastic: { + type: { + title: 'string' + } + } + } +}; + +const model = { + data: { + records: [{ id: 1, name: 'Elastic' }], + totalRecordCount: 1, + }, + criteria: { + page: { + index: 0, + size: 10, + }, + sort: { + field: 'name', + direction: 'asc' + }, + } +}; + +describe('Table', () => { + it('should render normally', async () => { + const component = shallow( +
{}} + deleteField={() => {}} + onDataCriteriaChange={() => {}} + /> + ); + + expect(component).toMatchSnapshot(); + }); + + it('should render the format', async () => { + const component = shallow( +
{}} + deleteField={() => {}} + onDataCriteriaChange={() => {}} + /> + ); + + const formatTableCell = shallow(component.prop('config').columns[3].render('Elastic')); + expect(formatTableCell).toMatchSnapshot(); + }); + + it('should allow edits', () => { + const editField = jest.fn(); + + const component = shallow( +
{}} + onDataCriteriaChange={() => {}} + /> + ); + + // Click the delete button + component.prop('config').columns[4].actions[0].onClick(); + expect(editField).toBeCalled(); + }); + + it('should allow deletes', () => { + const deleteField = jest.fn(); + + const component = shallow( +
{}} + deleteField={deleteField} + onDataCriteriaChange={() => {}} + /> + ); + + // Click the delete button + component.prop('config').columns[4].actions[1].onClick(); + expect(deleteField).toBeCalled(); + }); + +}); diff --git a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/components/table/index.js b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/components/table/index.js new file mode 100644 index 0000000000000..48232283cba67 --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/components/table/index.js @@ -0,0 +1 @@ +export { Table } from './table'; 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 new file mode 100644 index 0000000000000..9f75c6e7874a5 --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/components/table/table.js @@ -0,0 +1,119 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; + +import { + EuiTableOfRecords +} from '@elastic/eui'; + +export class Table extends PureComponent { + static propTypes = { + indexPattern: PropTypes.object.isRequired, + model: PropTypes.shape({ + data: PropTypes.shape({ + records: PropTypes.array.isRequired, + totalRecordCount: PropTypes.number.isRequired, + }).isRequired, + criteria: PropTypes.shape({ + page: PropTypes.shape({ + index: PropTypes.number.isRequired, + size: PropTypes.number.isRequired, + }).isRequired, + sort: PropTypes.shape({ + field: PropTypes.string.isRequired, + direction: PropTypes.string.isRequired, + }).isRequired, + }).isRequired, + }), + editField: PropTypes.func.isRequired, + deleteField: PropTypes.func.isRequired, + onDataCriteriaChange: PropTypes.func.isRequired, + } + + renderFormatCell = (value) => { + const { indexPattern } = this.props; + + const title = indexPattern.fieldFormatMap[value] && indexPattern.fieldFormatMap[value].type + ? indexPattern.fieldFormatMap[value].type.title + : ''; + + return ( + {title} + ); + } + + getTableConfig() { + const { editField, deleteField, onDataCriteriaChange } = this.props; + + return { + recordId: 'id', + columns: [ + { + field: 'name', + name: 'Name', + description: `Name of the field`, + dataType: 'string', + sortable: true, + }, + { + field: 'lang', + name: 'Lang', + description: `Language used for the field`, + dataType: 'string', + sortable: true, + render: value => { + return ( + + {value} + + ); + } + }, + { + field: 'script', + name: 'Script', + description: `Script for the field`, + dataType: 'string', + sortable: true, + }, + { + field: 'name', + name: 'Format', + description: `Format used for the field`, + render: this.renderFormatCell, + sortable: false, + }, + { + name: '', + actions: [ + { + name: 'Edit', + description: 'Edit this field', + icon: 'pencil', + onClick: editField, + }, + { + name: 'Delete', + description: 'Delete this field', + icon: 'trash', + color: 'danger', + onClick: deleteField, + }, + ] + } + ], + pagination: { + pageSizeOptions: [5, 10, 25, 50] + }, + selection: undefined, + onDataCriteriaChange, + }; + } + + render() { + const { model } = this.props; + + return ( + + ); + } +} diff --git a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/date_scripts.js b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/date_scripts.js deleted file mode 100644 index 559bb2a31a806..0000000000000 --- a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/date_scripts.js +++ /dev/null @@ -1,26 +0,0 @@ -import _ from 'lodash'; - -export function dateScripts(indexPattern) { - const dateScripts = {}; - const scripts = { - __dayOfMonth: 'dayOfMonth', - __dayOfWeek: 'dayOfWeek', - __dayOfYear: 'dayOfYear', - __hourOfDay: 'hourOfDay', - __minuteOfDay: 'minuteOfDay', - __minuteOfHour: 'minuteOfHour', - __monthOfYear: 'monthOfYear', - __weekOfYear: 'weekOfWeekyear', - __year: 'year' - }; - - _.each(indexPattern.fields.byType.date, function (field) { - if (field.indexed) { - _.each(scripts, function (value, key) { - dateScripts[field.name + '.' + key] = 'doc["' + field.name + '"].date.' + value; - }); - } - }); - - return dateScripts; -} diff --git a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/index.js b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/index.js index 547ccdebf2dd4..3083fd4de7539 100644 --- a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/index.js +++ b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/index.js @@ -1 +1 @@ -import './scripted_fields_table'; +export { ScriptedFieldsTable } from './scripted_fields_table'; diff --git a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/lib/__tests__/table.test.js b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/lib/__tests__/table.test.js new file mode 100644 index 0000000000000..69eaadae63432 --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/lib/__tests__/table.test.js @@ -0,0 +1,78 @@ +import { + getTableOfRecordsState, +} from '../table'; + +jest.mock('@elastic/eui', () => ({ + Comparators: { + property: () => {}, + default: () => {}, + }, +})); + +const items = [ + { name: 'Kibana' }, + { name: 'Elasticsearch' }, + { name: 'Logstash' }, +]; + +describe('getTableOfRecordsState', () => { + it('should return a TableOfRecords model', () => { + const model = getTableOfRecordsState(items, { + page: { + index: 0, + size: 10, + }, + sort: { + field: 'name', + direction: 'asc', + }, + }); + + expect(model).toEqual({ + data: { + records: items, + totalRecordCount: items.length, + }, + criteria: { + page: { + index: 0, + size: 10 + }, + sort: { + field: 'name', + direction: 'asc' + }, + } + }); + }); + + it('should paginate', () => { + const model = getTableOfRecordsState(items, { + page: { + index: 1, + size: 1, + }, + sort: { + field: 'name', + direction: 'asc', + }, + }); + + expect(model).toEqual({ + data: { + records: [{ name: 'Elasticsearch' }], + totalRecordCount: items.length, + }, + criteria: { + page: { + index: 1, + size: 1 + }, + sort: { + field: 'name', + direction: 'asc' + }, + } + }); + }); +}); diff --git a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/lib/index.js b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/lib/index.js new file mode 100644 index 0000000000000..01643f0f57fd8 --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/lib/index.js @@ -0,0 +1 @@ +export * from './table'; diff --git a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/lib/table.js b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/lib/table.js new file mode 100644 index 0000000000000..707187312103b --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/lib/table.js @@ -0,0 +1,61 @@ +import { + Comparators +} from '@elastic/eui'; + +export const getPage = (data, pageIndex, pageSize, sort) => { + let list = data; + if (sort) { + list = data.sort(Comparators.property(sort.field, Comparators.default(sort.direction))); + } + if (!pageIndex && !pageSize) { + return { + index: 0, + size: list.length, + items: list, + totalRecordCount: list.length + }; + } + const from = pageIndex * pageSize; + const items = list.slice(from, Math.min(from + pageSize, list.length)); + return { + index: pageIndex, + size: pageSize, + items, + totalRecordCount: list.length + }; +}; + +export const getTableOfRecordsState = (items, criteria) => { + const page = getPage(items, criteria.page.index, criteria.page.size, criteria.sort); + + return { + data: { + records: page.items, + totalRecordCount: page.totalRecordCount, + }, + criteria: { + page: { + index: page.index, + size: page.size + }, + sort: criteria.sort, + } + }; +}; + +export const DEFAULT_TABLE_OF_RECORDS_STATE = { + data: { + records: [], + totalRecordCount: 0, + }, + criteria: { + page: { + index: 0, + size: 10, + }, + sort: { + field: 'name', + direction: 'asc', + } + } +}; diff --git a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/scripted_fields_table.html b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/scripted_fields_table.html deleted file mode 100644 index 98b5eaa438c2c..0000000000000 --- a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/scripted_fields_table.html +++ /dev/null @@ -1,80 +0,0 @@ -

- Scripted fields -

- -

- These scripted fields are computed on the fly from your data. They can be used in visualizations and displayed in your documents, however they can not be searched. You can manage them here and add new ones as you see fit, but be careful, scripts can be tricky! -

- -
-
- - - Deprecation Warning - -
- -
-
- We've detected that the following deprecated languages are in use: {{ getDeprecatedLanguagesInUse().join(', ') }}. - Support for these languages will be removed in the next major version of Kibana and Elasticsearch. - We recommend converting your scripted fields to - Painless. -
-
-
- -
-
- - - Unsupported Languages - -
- -
-
- We've detected that the following unsupported languages are in use: {{ getUnsupportedLanguagesInUse().join(', ') }}. - All scripted fields must be converted to Painless. -
-
-
- - - - Add Scripted Field - - -
- -
- -
-
- - - No scripted fields found. - -
-
diff --git a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/scripted_fields_table.js b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/scripted_fields_table.js index 0a53d89a93da7..1104075f54c4c 100644 --- a/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/scripted_fields_table.js +++ b/src/core_plugins/kibana/public/management/sections/indices/edit_index_pattern/scripted_fields_table/scripted_fields_table.js @@ -1,138 +1,192 @@ - -import _ from 'lodash'; -import 'ui/paginated_table'; -import fieldControlsHtml from '../field_controls.html'; -import { dateScripts } from './date_scripts'; -import { uiModules } from 'ui/modules'; -import { toastNotifications } from 'ui/notify'; -import template from './scripted_fields_table.html'; +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; import { getSupportedScriptingLanguages, getDeprecatedScriptingLanguages } from 'ui/scripting_languages'; -import { documentationLinks } from 'ui/documentation_links/documentation_links'; - -uiModules.get('apps/management') - .directive('scriptedFieldsTable', function (kbnUrl, $filter, confirmModal) { - const rowScopes = []; // track row scopes, so they can be destroyed as needed - const filter = $filter('filter'); - - return { - restrict: 'E', - template, - scope: true, - link: function ($scope) { - - const fieldCreatorPath = '/management/kibana/indices/{{ indexPattern }}/scriptedField'; - const fieldEditorPath = fieldCreatorPath + '/{{ fieldName }}'; - - $scope.docLinks = documentationLinks.scriptedFields; - $scope.perPage = 25; - $scope.columns = [ - { title: 'name' }, - { title: 'lang' }, - { title: 'script' }, - { title: 'format' }, - { title: 'controls', sortable: false } - ]; - - $scope.$watchMulti(['[]indexPattern.fields', 'fieldFilter', 'scriptedFieldLanguageFilter'], refreshRows); - - function refreshRows() { - _.invoke(rowScopes, '$destroy'); - rowScopes.length = 0; - - const fields = filter($scope.indexPattern.getScriptedFields(), { - name: $scope.fieldFilter, - lang: $scope.scriptedFieldLanguageFilter - }); - _.find($scope.editSections, { index: 'scriptedFields' }).count = fields.length; // Update the tab count - - $scope.rows = fields.map(function (field) { - const rowScope = $scope.$new(); - rowScope.field = field; - rowScopes.push(rowScope); - - return [ - _.escape(field.name), - { - markup: field.lang, - attr: { - 'data-test-subj': 'scriptedFieldLang' - } - }, - _.escape(field.script), - _.get($scope.indexPattern, ['fieldFormatMap', field.name, 'type', 'title']), - { - markup: fieldControlsHtml, - scope: rowScope - } - ]; - }); - } +import { documentationLinks } from 'ui/documentation_links'; + +import { + EuiButton, + EuiSpacer, + EuiOverlayMask, + EuiConfirmModal, + EUI_MODAL_CONFIRM_BUTTON, +} from '@elastic/eui'; + +import { Table } from './components/table'; +import { Header } from './components/header'; +import { CallOuts } from './components/call_outs'; +import { getTableOfRecordsState, DEFAULT_TABLE_OF_RECORDS_STATE } from './lib'; + + +export class ScriptedFieldsTable extends Component { + static propTypes = { + indexPattern: PropTypes.object.isRequired, + fieldFilter: PropTypes.string, + scriptedFieldLanguageFilter: PropTypes.string, + helpers: PropTypes.shape({ + redirectToRoute: PropTypes.func.isRequired, + getRouteHref: PropTypes.func.isRequired, + }), + onRemoveField: PropTypes.func, + } + + constructor(props) { + super(props); + + this.state = { + deprecatedLangsInUse: [], + fieldToDelete: undefined, + isDeleteConfirmationModalVisible: false, + fields: [], + ...DEFAULT_TABLE_OF_RECORDS_STATE, + }; + } - $scope.addDateScripts = function () { - const conflictFields = []; - let fieldsAdded = 0; - _.each(dateScripts($scope.indexPattern), function (script, field) { - try { - $scope.indexPattern.addScriptedField(field, script, 'number'); - fieldsAdded++; - } catch (e) { - conflictFields.push(field); - } - }); - - if (fieldsAdded > 0) { - toastNotifications.addSuccess({ - title: 'Created script fields', - text: `Created ${fieldsAdded}`, - }); - } - - if (conflictFields.length > 0) { - toastNotifications.addWarning({ - title: `Didn't add duplicate fields`, - text: `${conflictFields.length} fields: ${conflictFields.join(', ')}`, - }); - } - }; - - $scope.create = function () { - const params = { - indexPattern: $scope.indexPattern.id - }; - - kbnUrl.change(fieldCreatorPath, params); - }; - - $scope.edit = function (field) { - const params = { - indexPattern: $scope.indexPattern.id, - fieldName: field.name - }; - - kbnUrl.change(fieldEditorPath, params); - }; - - $scope.remove = function (field) { - const confirmModalOptions = { - confirmButtonText: 'Delete', - onConfirm: () => { $scope.indexPattern.removeScriptedField(field.name); }, - title: `Delete scripted field '${field.name}'?` - }; - confirmModal(`You can't recover scripted fields.`, confirmModalOptions); - }; - - function getLanguagesInUse() { - const fields = $scope.indexPattern.getScriptedFields(); - return _.uniq(_.map(fields, 'lang')); - } + componentWillMount() { + this.fetchFields(); + } - $scope.getDeprecatedLanguagesInUse = function () { - return _.intersection(getLanguagesInUse(), getDeprecatedScriptingLanguages()); - }; + fetchFields = async () => { + const fields = await this.props.indexPattern.getScriptedFields(); - $scope.getUnsupportedLanguagesInUse = function () { - return _.difference(getLanguagesInUse(), _.union(getSupportedScriptingLanguages(), getDeprecatedScriptingLanguages())); - }; + const deprecatedLangsInUse = []; + const deprecatedLangs = getDeprecatedScriptingLanguages(); + const supportedLangs = getSupportedScriptingLanguages(); + + for (const { lang } of fields) { + if (deprecatedLangs.includes(lang) || !supportedLangs.includes(lang)) { + deprecatedLangsInUse.push(lang); } + } + + this.setState({ + fields, + deprecatedLangsInUse, + ...this.computeTableState(this.state.criteria, this.props, fields) + }); + } + + onDataCriteriaChange = criteria => { + this.setState(this.computeTableState(criteria)); + } + + componentWillReceiveProps(nextProps) { + if (this.props.fieldFilter !== nextProps.fieldFilter) { + this.setState(this.computeTableState(this.state.criteria, nextProps)); + } + if (this.props.scriptedFieldLanguageFilter !== nextProps.scriptedFieldLanguageFilter) { + this.setState(this.computeTableState(this.state.criteria, nextProps)); + } + } + + computeTableState(criteria, props = this.props, fields = this.state.fields) { + let items = fields; + if (props.fieldFilter) { + const fieldFilter = props.fieldFilter.toLowerCase(); + items = items.filter(field => field.name.toLowerCase().includes(fieldFilter)); + } + if (props.scriptedFieldLanguageFilter) { + items = items.filter(field => field.lang === props.scriptedFieldLanguageFilter); + } + + return getTableOfRecordsState(items, criteria); + } + + renderCallOuts() { + const { deprecatedLangsInUse } = this.state; + + return ( + + ); + } + + startDeleteField = field => { + this.setState({ fieldToDelete: field, isDeleteConfirmationModalVisible: true }); + } + + hideDeleteConfirmationModal = () => { + this.setState({ fieldToDelete: undefined, isDeleteConfirmationModalVisible: false }); + } + + deleteField = () => { + const { indexPattern, onRemoveField } = this.props; + const { fieldToDelete } = this.state; + + indexPattern.removeScriptedField(fieldToDelete.name); + onRemoveField && onRemoveField(); + this.fetchFields(); + this.hideDeleteConfirmationModal(); + } + + renderDeleteConfirmationModal() { + const { fieldToDelete } = this.state; + + if (!fieldToDelete) { + return null; + } + + return ( + + + + ); + } + + render() { + const { + helpers, + indexPattern, + } = this.props; + + const { + data, + criteria: { + page, + sort, + }, + fields, + } = this.state; + + const model = { + data, + criteria: { + page, + sort, + }, }; - }); + + return ( +
+
+ {this.renderCallOuts()} + + Add scripted field + + + { fields.length > 0 ? +
this.props.helpers.redirectToRoute(field, 'edit')} + deleteField={this.startDeleteField} + onDataCriteriaChange={this.onDataCriteriaChange} + /> + : null + } + {this.renderDeleteConfirmationModal()} + + ); + } +}