[Management] Scripted fields table in React (#16604)
* First pass at new scripted fields table * Ensure we destroy when we switch tabs * PR feedback * Fix functional tests * Fix functional tests * PR feedback and more tests
This commit is contained in:
parent
b5bda1ee32
commit
b932467784
|
@ -23,10 +23,7 @@
|
|||
</p>
|
||||
|
||||
<p class="kuiText kuiVerticalRhythm">
|
||||
This page lists every field in the <strong>{{::indexPattern.title}}</strong>
|
||||
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 <strong>{{::indexPattern.title}}</strong> index and the field's associated core type as recorded by Elasticsearch. To change a field type, use the Elasticsearch
|
||||
<a target="_window" class="euiLink euiLink--primary" href="http://www.elastic.co/guide/en/elasticsearch/reference/current/mapping.html">
|
||||
Mapping API
|
||||
<i aria-hidden="true" class="fa-link fa"></i>
|
||||
|
@ -145,10 +142,7 @@
|
|||
class="fields indexed-fields"
|
||||
></indexed-fields-table>
|
||||
|
||||
<scripted-fields-table
|
||||
ng-show="state.tab == 'scriptedFields'"
|
||||
class="fields scripted-fields"
|
||||
></scripted-fields-table>
|
||||
<div id="reactScriptedFieldsTable"></div>
|
||||
|
||||
<source-filters-table
|
||||
ng-show="state.tab == 'sourceFilters'"
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import _ from 'lodash';
|
||||
import './index_header';
|
||||
import './indexed_fields_table';
|
||||
import './scripted_fields_table';
|
||||
import './scripted_field_editor';
|
||||
import './source_filters_table';
|
||||
import { KbnUrlProvider } from 'ui/url';
|
||||
|
@ -11,6 +10,50 @@ import uiRoutes from 'ui/routes';
|
|||
import { uiModules } from 'ui/modules';
|
||||
import template from './edit_index_pattern.html';
|
||||
|
||||
import React from 'react';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
import { ScriptedFieldsTable } from './scripted_fields_table';
|
||||
|
||||
const REACT_SCRIPTED_FIELDS_DOM_ELEMENT_ID = 'reactScriptedFieldsTable';
|
||||
|
||||
function updateScriptedFieldsTable($scope, $state) {
|
||||
if ($state.tab === 'scriptedFields') {
|
||||
$scope.$$postDigest(() => {
|
||||
const node = document.getElementById(REACT_SCRIPTED_FIELDS_DOM_ELEMENT_ID);
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
render(
|
||||
<ScriptedFieldsTable
|
||||
indexPattern={$scope.indexPattern}
|
||||
fieldFilter={$scope.fieldFilter}
|
||||
scriptedFieldLanguageFilter={$scope.scriptedFieldLanguageFilter}
|
||||
helpers={{
|
||||
redirectToRoute: (obj, route) => {
|
||||
$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);
|
||||
});
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -0,0 +1,255 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ScriptedFieldsTable should filter based on the lang filter 1`] = `
|
||||
<div>
|
||||
<header />
|
||||
<call-outs
|
||||
deprecatedLangsInUse={
|
||||
Array [
|
||||
"somethingElse",
|
||||
]
|
||||
}
|
||||
painlessDocLink="painlessDocs"
|
||||
/>
|
||||
<eui-button
|
||||
data-test-subj="addScriptedFieldLink"
|
||||
>
|
||||
Add scripted field
|
||||
</eui-button>
|
||||
<eui-spacer
|
||||
size="l"
|
||||
/>
|
||||
<Table
|
||||
deleteField={[Function]}
|
||||
editField={[Function]}
|
||||
indexPattern={
|
||||
Object {
|
||||
"getScriptedFields": [Function],
|
||||
}
|
||||
}
|
||||
model={
|
||||
Object {
|
||||
"criteria": Object {
|
||||
"page": Object {
|
||||
"index": 0,
|
||||
"size": 10,
|
||||
},
|
||||
"sort": Object {
|
||||
"direction": "asc",
|
||||
"field": "name",
|
||||
},
|
||||
},
|
||||
"data": Object {
|
||||
"records": Array [
|
||||
Object {
|
||||
"lang": "painless",
|
||||
"name": "ScriptedField",
|
||||
"script": "x++",
|
||||
},
|
||||
Object {
|
||||
"lang": "painless",
|
||||
"name": "JustATest",
|
||||
"script": "z++",
|
||||
},
|
||||
],
|
||||
"totalRecordCount": 2,
|
||||
},
|
||||
}
|
||||
}
|
||||
onDataCriteriaChange={[Function]}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ScriptedFieldsTable should filter based on the query bar 1`] = `
|
||||
<div>
|
||||
<header />
|
||||
<call-outs
|
||||
deprecatedLangsInUse={Array []}
|
||||
painlessDocLink="painlessDocs"
|
||||
/>
|
||||
<eui-button
|
||||
data-test-subj="addScriptedFieldLink"
|
||||
>
|
||||
Add scripted field
|
||||
</eui-button>
|
||||
<eui-spacer
|
||||
size="l"
|
||||
/>
|
||||
<Table
|
||||
deleteField={[Function]}
|
||||
editField={[Function]}
|
||||
indexPattern={
|
||||
Object {
|
||||
"getScriptedFields": [Function],
|
||||
}
|
||||
}
|
||||
model={
|
||||
Object {
|
||||
"criteria": Object {
|
||||
"page": Object {
|
||||
"index": 0,
|
||||
"size": 10,
|
||||
},
|
||||
"sort": Object {
|
||||
"direction": "asc",
|
||||
"field": "name",
|
||||
},
|
||||
},
|
||||
"data": Object {
|
||||
"records": Array [
|
||||
Object {
|
||||
"lang": "painless",
|
||||
"name": "JustATest",
|
||||
"script": "z++",
|
||||
},
|
||||
],
|
||||
"totalRecordCount": 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
onDataCriteriaChange={[Function]}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ScriptedFieldsTable should hide the table if there are no scripted fields 1`] = `
|
||||
<div>
|
||||
<header />
|
||||
<call-outs
|
||||
deprecatedLangsInUse={Array []}
|
||||
painlessDocLink="painlessDocs"
|
||||
/>
|
||||
<eui-button
|
||||
data-test-subj="addScriptedFieldLink"
|
||||
>
|
||||
Add scripted field
|
||||
</eui-button>
|
||||
<eui-spacer
|
||||
size="l"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ScriptedFieldsTable should render normally 1`] = `
|
||||
<div>
|
||||
<header />
|
||||
<call-outs
|
||||
deprecatedLangsInUse={Array []}
|
||||
painlessDocLink="painlessDocs"
|
||||
/>
|
||||
<eui-button
|
||||
data-test-subj="addScriptedFieldLink"
|
||||
>
|
||||
Add scripted field
|
||||
</eui-button>
|
||||
<eui-spacer
|
||||
size="l"
|
||||
/>
|
||||
<Table
|
||||
deleteField={[Function]}
|
||||
editField={[Function]}
|
||||
indexPattern={
|
||||
Object {
|
||||
"getScriptedFields": [Function],
|
||||
}
|
||||
}
|
||||
model={
|
||||
Object {
|
||||
"criteria": Object {
|
||||
"page": Object {
|
||||
"index": 0,
|
||||
"size": 10,
|
||||
},
|
||||
"sort": Object {
|
||||
"direction": "asc",
|
||||
"field": "name",
|
||||
},
|
||||
},
|
||||
"data": Object {
|
||||
"records": Array [
|
||||
Object {
|
||||
"lang": "painless",
|
||||
"name": "ScriptedField",
|
||||
"script": "x++",
|
||||
},
|
||||
Object {
|
||||
"lang": "painless",
|
||||
"name": "JustATest",
|
||||
"script": "z++",
|
||||
},
|
||||
],
|
||||
"totalRecordCount": 2,
|
||||
},
|
||||
}
|
||||
}
|
||||
onDataCriteriaChange={[Function]}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ScriptedFieldsTable should show a delete modal 1`] = `
|
||||
<div>
|
||||
<header />
|
||||
<call-outs
|
||||
deprecatedLangsInUse={Array []}
|
||||
painlessDocLink="painlessDocs"
|
||||
/>
|
||||
<eui-button
|
||||
data-test-subj="addScriptedFieldLink"
|
||||
>
|
||||
Add scripted field
|
||||
</eui-button>
|
||||
<eui-spacer
|
||||
size="l"
|
||||
/>
|
||||
<Table
|
||||
deleteField={[Function]}
|
||||
editField={[Function]}
|
||||
indexPattern={
|
||||
Object {
|
||||
"getScriptedFields": [Function],
|
||||
}
|
||||
}
|
||||
model={
|
||||
Object {
|
||||
"criteria": Object {
|
||||
"page": Object {
|
||||
"index": 0,
|
||||
"size": 10,
|
||||
},
|
||||
"sort": Object {
|
||||
"direction": "asc",
|
||||
"field": "name",
|
||||
},
|
||||
},
|
||||
"data": Object {
|
||||
"records": Array [
|
||||
Object {
|
||||
"lang": "painless",
|
||||
"name": "ScriptedField",
|
||||
"script": "x++",
|
||||
},
|
||||
Object {
|
||||
"lang": "painless",
|
||||
"name": "JustATest",
|
||||
"script": "z++",
|
||||
},
|
||||
],
|
||||
"totalRecordCount": 2,
|
||||
},
|
||||
}
|
||||
}
|
||||
onDataCriteriaChange={[Function]}
|
||||
/>
|
||||
<eui-overlay-mask>
|
||||
<eui-confirm-modal
|
||||
cancelButtonText="Cancel"
|
||||
confirmButtonText="Delete"
|
||||
onCancel={[Function]}
|
||||
onConfirm={[Function]}
|
||||
title="Delete scripted field 'ScriptedField'?"
|
||||
/>
|
||||
</eui-overlay-mask>
|
||||
</div>
|
||||
`;
|
|
@ -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(
|
||||
<ScriptedFieldsTable
|
||||
indexPattern={indexPattern}
|
||||
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 filter based on the query bar', async () => {
|
||||
const component = shallow(
|
||||
<ScriptedFieldsTable
|
||||
indexPattern={indexPattern}
|
||||
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({ fieldFilter: 'Just' });
|
||||
component.update();
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should filter based on the lang filter', async () => {
|
||||
const component = shallow(
|
||||
<ScriptedFieldsTable
|
||||
indexPattern={{
|
||||
getScriptedFields: () => ([
|
||||
{ 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(
|
||||
<ScriptedFieldsTable
|
||||
indexPattern={{
|
||||
getScriptedFields: () => ([])
|
||||
}}
|
||||
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(
|
||||
<ScriptedFieldsTable
|
||||
indexPattern={indexPattern}
|
||||
helpers={helpers}
|
||||
/>
|
||||
);
|
||||
|
||||
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(
|
||||
<ScriptedFieldsTable
|
||||
indexPattern={{
|
||||
...indexPattern,
|
||||
removeScriptedField,
|
||||
}}
|
||||
helpers={helpers}
|
||||
/>
|
||||
);
|
||||
|
||||
await component.update(); // Fire `componentWillMount()`
|
||||
component.instance().startDeleteField({ name: 'ScriptedField' });
|
||||
await component.update();
|
||||
await component.instance().deleteField();
|
||||
await component.update();
|
||||
expect(removeScriptedField).toBeCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,31 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CallOuts should render normally 1`] = `
|
||||
<div>
|
||||
<EuiCallOut
|
||||
color="danger"
|
||||
iconType="cross"
|
||||
size="m"
|
||||
title="Deprecation languages in use"
|
||||
>
|
||||
<p>
|
||||
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
|
||||
<EuiLink
|
||||
color="primary"
|
||||
href="http://www.elastic.co/painlessDocs"
|
||||
type="button"
|
||||
>
|
||||
Painless
|
||||
</EuiLink>
|
||||
to avoid any problems.
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer
|
||||
size="m"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CallOuts should render without any call outs 1`] = `""`;
|
|
@ -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(
|
||||
<CallOuts
|
||||
deprecatedLangsInUse={['php']}
|
||||
painlessDocLink="http://www.elastic.co/painlessDocs"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render without any call outs', async () => {
|
||||
const component = shallow(
|
||||
<CallOuts
|
||||
deprecatedLangsInUse={[]}
|
||||
painlessDocLink="http://www.elastic.co/painlessDocs"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -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 (
|
||||
<div>
|
||||
<EuiCallOut
|
||||
title="Deprecation languages in use"
|
||||
color="danger"
|
||||
iconType="cross"
|
||||
>
|
||||
<p>
|
||||
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 <EuiLink href={painlessDocLink}>Painless</EuiLink> to avoid any problems.
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="m"/>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
export { CallOuts } from './call_outs';
|
|
@ -0,0 +1,21 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Header should render normally 1`] = `
|
||||
<div>
|
||||
<EuiTitle
|
||||
size="s"
|
||||
>
|
||||
<h3>
|
||||
Scripted fields
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
<EuiText>
|
||||
<p>
|
||||
You can use scripted fields in visualizations and display them in your documents. However, you cannot search scripted fields.
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiSpacer
|
||||
size="s"
|
||||
/>
|
||||
</div>
|
||||
`;
|
|
@ -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(
|
||||
<Header/>
|
||||
);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,22 @@
|
|||
import React from 'react';
|
||||
|
||||
import {
|
||||
EuiTitle,
|
||||
EuiText,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
|
||||
export const Header = () => (
|
||||
<div>
|
||||
<EuiTitle size="s">
|
||||
<h3>Scripted fields</h3>
|
||||
</EuiTitle>
|
||||
<EuiText>
|
||||
<p>
|
||||
You can use scripted fields in visualizations and display them in your documents.
|
||||
However, you cannot search scripted fields.
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiSpacer size="s" />
|
||||
</div>
|
||||
);
|
|
@ -0,0 +1 @@
|
|||
export { Header } from './header';
|
|
@ -0,0 +1,99 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Table should render normally 1`] = `
|
||||
<EuiTableOfRecords
|
||||
config={
|
||||
Object {
|
||||
"columns": Array [
|
||||
Object {
|
||||
"dataType": "string",
|
||||
"description": "Name of the field",
|
||||
"field": "name",
|
||||
"name": "Name",
|
||||
"sortable": true,
|
||||
},
|
||||
Object {
|
||||
"dataType": "string",
|
||||
"description": "Language used for the field",
|
||||
"field": "lang",
|
||||
"name": "Lang",
|
||||
"render": [Function],
|
||||
"sortable": true,
|
||||
},
|
||||
Object {
|
||||
"dataType": "string",
|
||||
"description": "Script for the field",
|
||||
"field": "script",
|
||||
"name": "Script",
|
||||
"sortable": true,
|
||||
},
|
||||
Object {
|
||||
"description": "Format used for the field",
|
||||
"field": "name",
|
||||
"name": "Format",
|
||||
"render": [Function],
|
||||
"sortable": false,
|
||||
},
|
||||
Object {
|
||||
"actions": Array [
|
||||
Object {
|
||||
"description": "Edit this field",
|
||||
"icon": "pencil",
|
||||
"name": "Edit",
|
||||
"onClick": [Function],
|
||||
},
|
||||
Object {
|
||||
"color": "danger",
|
||||
"description": "Delete this field",
|
||||
"icon": "trash",
|
||||
"name": "Delete",
|
||||
"onClick": [Function],
|
||||
},
|
||||
],
|
||||
"name": "",
|
||||
},
|
||||
],
|
||||
"onDataCriteriaChange": [Function],
|
||||
"pagination": Object {
|
||||
"pageSizeOptions": Array [
|
||||
5,
|
||||
10,
|
||||
25,
|
||||
50,
|
||||
],
|
||||
},
|
||||
"recordId": "id",
|
||||
"selection": undefined,
|
||||
}
|
||||
}
|
||||
model={
|
||||
Object {
|
||||
"criteria": Object {
|
||||
"page": Object {
|
||||
"index": 0,
|
||||
"size": 10,
|
||||
},
|
||||
"sort": Object {
|
||||
"direction": "asc",
|
||||
"field": "name",
|
||||
},
|
||||
},
|
||||
"data": Object {
|
||||
"records": Array [
|
||||
Object {
|
||||
"id": 1,
|
||||
"name": "Elastic",
|
||||
},
|
||||
],
|
||||
"totalRecordCount": 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`Table should render the format 1`] = `
|
||||
<span>
|
||||
string
|
||||
</span>
|
||||
`;
|
|
@ -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(
|
||||
<Table
|
||||
indexPattern={indexPattern}
|
||||
model={model}
|
||||
editField={() => {}}
|
||||
deleteField={() => {}}
|
||||
onDataCriteriaChange={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render the format', async () => {
|
||||
const component = shallow(
|
||||
<Table
|
||||
indexPattern={indexPattern}
|
||||
model={model}
|
||||
editField={() => {}}
|
||||
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(
|
||||
<Table
|
||||
indexPattern={indexPattern}
|
||||
model={model}
|
||||
editField={editField}
|
||||
deleteField={() => {}}
|
||||
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(
|
||||
<Table
|
||||
indexPattern={indexPattern}
|
||||
model={model}
|
||||
editField={() => {}}
|
||||
deleteField={deleteField}
|
||||
onDataCriteriaChange={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
// Click the delete button
|
||||
component.prop('config').columns[4].actions[1].onClick();
|
||||
expect(deleteField).toBeCalled();
|
||||
});
|
||||
|
||||
});
|
|
@ -0,0 +1 @@
|
|||
export { Table } from './table';
|
|
@ -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 (
|
||||
<span>{title}</span>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<span data-test-subj="scriptedFieldLang">
|
||||
{value}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
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 (
|
||||
<EuiTableOfRecords config={this.getTableConfig()} model={model} />
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -1 +1 @@
|
|||
import './scripted_fields_table';
|
||||
export { ScriptedFieldsTable } from './scripted_fields_table';
|
||||
|
|
|
@ -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'
|
||||
},
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1 @@
|
|||
export * from './table';
|
|
@ -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',
|
||||
}
|
||||
}
|
||||
};
|
|
@ -1,80 +0,0 @@
|
|||
<h3 class="kuiTextTitle kuiVerticalRhythm">
|
||||
Scripted fields
|
||||
</h3>
|
||||
|
||||
<p class="kuiText kuiVerticalRhythm">
|
||||
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!
|
||||
</p>
|
||||
|
||||
<div class="kuiInfoPanel kuiInfoPanel--warning kuiVerticalRhythm" ng-if="getDeprecatedLanguagesInUse().length !== 0">
|
||||
<div class="kuiInfoPanelHeader">
|
||||
<span
|
||||
class="kuiInfoPanelHeader__icon kuiIcon kuiIcon--warning fa-bolt"
|
||||
aria-label="Warning"
|
||||
role="img"
|
||||
></span>
|
||||
<span class="kuiInfoPanelHeader__title">
|
||||
Deprecation Warning
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="kuiInfoPanelBody">
|
||||
<div class="kuiInfoPanelBody__message">
|
||||
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
|
||||
<a class="kuiLink" ng-href="{{docLinks.painless}}">Painless</a>.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kuiInfoPanel kuiInfoPanel--error kuiVerticalRhythm" ng-if="getUnsupportedLanguagesInUse().length !== 0">
|
||||
<div class="kuiInfoPanelHeader">
|
||||
<span
|
||||
class="kuiInfoPanelHeader__icon kuiIcon kuiIcon--error fa-warning"
|
||||
aria-label="Error"
|
||||
role="img"
|
||||
></span>
|
||||
<span class="kuiInfoPanelHeader__title">
|
||||
Unsupported Languages
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="kuiInfoPanelBody">
|
||||
<div class="kuiInfoPanelBody__message">
|
||||
We've detected that the following unsupported languages are in use: {{ getUnsupportedLanguagesInUse().join(', ') }}.
|
||||
All scripted fields must be converted to <a class="kuiLink" ng-href="{{docLinks.painless}}">Painless</a>.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a
|
||||
data-test-subj="addScriptedFieldLink"
|
||||
ng-href="{{ kbnUrl.getRouteHref(indexPattern, 'addField') }}"
|
||||
class="kuiButton kuiButton--primary kuiButton--iconText kuiVerticalRhythm"
|
||||
>
|
||||
<span aria-hidden="true" class="kuiButton__icon kuiIcon fa-plus"></span>
|
||||
<span>Add Scripted Field</span>
|
||||
</a>
|
||||
|
||||
<div class="kuiVerticalRhythm">
|
||||
<paginated-table
|
||||
columns="columns"
|
||||
rows="rows"
|
||||
per-page="perPage"
|
||||
link-to-top="true"
|
||||
show-blank-rows="false"
|
||||
></paginated-table>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="kuiInfoPanel kuiInfoPanel--info kuiVerticalRhythm"
|
||||
ng-if="rows.length === 0"
|
||||
>
|
||||
<div class="kuiInfoPanelHeader">
|
||||
<span class="kuiInfoPanelHeader__icon kuiIcon kuiIcon--info fa-info"></span>
|
||||
<span class="kuiInfoPanelHeader__title">
|
||||
No scripted fields found.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
|
@ -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';
|
||||
import { documentationLinks } from 'ui/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');
|
||||
import {
|
||||
EuiButton,
|
||||
EuiSpacer,
|
||||
EuiOverlayMask,
|
||||
EuiConfirmModal,
|
||||
EUI_MODAL_CONFIRM_BUTTON,
|
||||
} from '@elastic/eui';
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
template,
|
||||
scope: true,
|
||||
link: function ($scope) {
|
||||
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';
|
||||
|
||||
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 }
|
||||
];
|
||||
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,
|
||||
}
|
||||
|
||||
$scope.$watchMulti(['[]indexPattern.fields', 'fieldFilter', 'scriptedFieldLanguageFilter'], refreshRows);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
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
|
||||
}
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
$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'));
|
||||
}
|
||||
|
||||
$scope.getDeprecatedLanguagesInUse = function () {
|
||||
return _.intersection(getLanguagesInUse(), getDeprecatedScriptingLanguages());
|
||||
};
|
||||
|
||||
$scope.getUnsupportedLanguagesInUse = function () {
|
||||
return _.difference(getLanguagesInUse(), _.union(getSupportedScriptingLanguages(), getDeprecatedScriptingLanguages()));
|
||||
};
|
||||
}
|
||||
this.state = {
|
||||
deprecatedLangsInUse: [],
|
||||
fieldToDelete: undefined,
|
||||
isDeleteConfirmationModalVisible: false,
|
||||
fields: [],
|
||||
...DEFAULT_TABLE_OF_RECORDS_STATE,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.fetchFields();
|
||||
}
|
||||
|
||||
fetchFields = async () => {
|
||||
const fields = await this.props.indexPattern.getScriptedFields();
|
||||
|
||||
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 (
|
||||
<CallOuts
|
||||
deprecatedLangsInUse={deprecatedLangsInUse}
|
||||
painlessDocLink={documentationLinks.scriptedFields.painless}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<EuiOverlayMask>
|
||||
<EuiConfirmModal
|
||||
title={`Delete scripted field '${fieldToDelete.name}'?`}
|
||||
onCancel={this.hideDeleteConfirmationModal}
|
||||
onConfirm={this.deleteField}
|
||||
cancelButtonText="Cancel"
|
||||
confirmButtonText="Delete"
|
||||
defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON}
|
||||
/>
|
||||
</EuiOverlayMask>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
helpers,
|
||||
indexPattern,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
data,
|
||||
criteria: {
|
||||
page,
|
||||
sort,
|
||||
},
|
||||
fields,
|
||||
} = this.state;
|
||||
|
||||
const model = {
|
||||
data,
|
||||
criteria: {
|
||||
page,
|
||||
sort,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Header/>
|
||||
{this.renderCallOuts()}
|
||||
<EuiButton
|
||||
data-test-subj="addScriptedFieldLink"
|
||||
href={helpers.getRouteHref(indexPattern, 'addField')}
|
||||
>
|
||||
Add scripted field
|
||||
</EuiButton>
|
||||
<EuiSpacer size="l" />
|
||||
{ fields.length > 0 ?
|
||||
<Table
|
||||
indexPattern={indexPattern}
|
||||
model={model}
|
||||
editField={field => this.props.helpers.redirectToRoute(field, 'edit')}
|
||||
deleteField={this.startDeleteField}
|
||||
onDataCriteriaChange={this.onDataCriteriaChange}
|
||||
/>
|
||||
: null
|
||||
}
|
||||
{this.renderDeleteConfirmationModal()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue