Convert EuiTableOfRecords to simpler table component in scripted fields table. (#16901) (#17079)

* Convert EuiTableOfRecords to EuiBasicTable in scripted fields table.
* Convert from EuiBasicTable to EuiInMemoryTable.
* Fix bug with filtering and show 'no items' message when the filter doens't match any scripted fields.
This commit is contained in:
CJ Cenizal 2018-03-09 15:21:32 -08:00 committed by GitHub
parent 1def6ed78f
commit 5168d8fcdc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 316 additions and 566 deletions

View file

@ -2,7 +2,9 @@
exports[`ScriptedFieldsTable should filter based on the lang filter 1`] = `
<div>
<header />
<header
addScriptedFieldUrl="#"
/>
<call-outs
deprecatedLangsInUse={
Array [
@ -11,11 +13,6 @@ exports[`ScriptedFieldsTable should filter based on the lang filter 1`] = `
}
painlessDocLink="painlessDocs"
/>
<eui-button
data-test-subj="addScriptedFieldLink"
>
Add scripted field
</eui-button>
<eui-spacer
size="l"
/>
@ -27,52 +24,33 @@ exports[`ScriptedFieldsTable should filter based on the lang filter 1`] = `
"getScriptedFields": [Function],
}
}
model={
Object {
"criteria": Object {
"page": Object {
"index": 0,
"size": 10,
},
"sort": Object {
"direction": "asc",
"field": "name",
},
items={
Array [
Object {
"lang": "painless",
"name": "ScriptedField",
"script": "x++",
},
"data": Object {
"records": Array [
Object {
"lang": "painless",
"name": "ScriptedField",
"script": "x++",
},
Object {
"lang": "painless",
"name": "JustATest",
"script": "z++",
},
],
"totalRecordCount": 2,
Object {
"lang": "painless",
"name": "JustATest",
"script": "z++",
},
}
]
}
onDataCriteriaChange={[Function]}
/>
</div>
`;
exports[`ScriptedFieldsTable should filter based on the query bar 1`] = `
<div>
<header />
<header
addScriptedFieldUrl="#"
/>
<call-outs
deprecatedLangsInUse={Array []}
painlessDocLink="painlessDocs"
/>
<eui-button
data-test-subj="addScriptedFieldLink"
>
Add scripted field
</eui-button>
<eui-spacer
size="l"
/>
@ -84,65 +62,53 @@ exports[`ScriptedFieldsTable should filter based on the query bar 1`] = `
"getScriptedFields": [Function],
}
}
model={
Object {
"criteria": Object {
"page": Object {
"index": 0,
"size": 10,
},
"sort": Object {
"direction": "asc",
"field": "name",
},
items={
Array [
Object {
"lang": "painless",
"name": "JustATest",
"script": "z++",
},
"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 />
<header
addScriptedFieldUrl="#"
/>
<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],
}
}
items={Array []}
/>
</div>
`;
exports[`ScriptedFieldsTable should render normally 1`] = `
<div>
<header />
<header
addScriptedFieldUrl="#"
/>
<call-outs
deprecatedLangsInUse={Array []}
painlessDocLink="painlessDocs"
/>
<eui-button
data-test-subj="addScriptedFieldLink"
>
Add scripted field
</eui-button>
<eui-spacer
size="l"
/>
@ -154,52 +120,33 @@ exports[`ScriptedFieldsTable should render normally 1`] = `
"getScriptedFields": [Function],
}
}
model={
Object {
"criteria": Object {
"page": Object {
"index": 0,
"size": 10,
},
"sort": Object {
"direction": "asc",
"field": "name",
},
items={
Array [
Object {
"lang": "painless",
"name": "ScriptedField",
"script": "x++",
},
"data": Object {
"records": Array [
Object {
"lang": "painless",
"name": "ScriptedField",
"script": "x++",
},
Object {
"lang": "painless",
"name": "JustATest",
"script": "z++",
},
],
"totalRecordCount": 2,
Object {
"lang": "painless",
"name": "JustATest",
"script": "z++",
},
}
]
}
onDataCriteriaChange={[Function]}
/>
</div>
`;
exports[`ScriptedFieldsTable should show a delete modal 1`] = `
<div>
<header />
<header
addScriptedFieldUrl="#"
/>
<call-outs
deprecatedLangsInUse={Array []}
painlessDocLink="painlessDocs"
/>
<eui-button
data-test-subj="addScriptedFieldLink"
>
Add scripted field
</eui-button>
<eui-spacer
size="l"
/>
@ -211,36 +158,20 @@ exports[`ScriptedFieldsTable should show a delete modal 1`] = `
"getScriptedFields": [Function],
}
}
model={
Object {
"criteria": Object {
"page": Object {
"index": 0,
"size": 10,
},
"sort": Object {
"direction": "asc",
"field": "name",
},
items={
Array [
Object {
"lang": "painless",
"name": "ScriptedField",
"script": "x++",
},
"data": Object {
"records": Array [
Object {
"lang": "painless",
"name": "ScriptedField",
"script": "x++",
},
Object {
"lang": "painless",
"name": "JustATest",
"script": "z++",
},
],
"totalRecordCount": 2,
Object {
"lang": "painless",
"name": "JustATest",
"script": "z++",
},
}
]
}
onDataCriteriaChange={[Function]}
/>
<eui-overlay-mask>
<eui-confirm-modal

View file

@ -4,11 +4,8 @@ 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',
@ -42,7 +39,7 @@ jest.mock('ui/documentation_links', () => ({
const helpers = {
redirectToRoute: () => {},
getRouteHref: () => {},
getRouteHref: () => '#',
};
const indexPattern = {

View file

@ -1,21 +1,45 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Header should render normally 1`] = `
<div>
<EuiTitle
size="s"
<EuiFlexGroup
alignItems="center"
component="div"
gutterSize="l"
justifyContent="flexStart"
responsive={true}
wrap={false}
>
<EuiFlexItem
component="div"
grow={true}
>
<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>
<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>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={false}
>
<EuiButton
color="primary"
data-test-subj="addScriptedFieldLink"
fill={false}
href=""
iconSide="left"
type="button"
>
Add scripted field
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
`;

View file

@ -6,7 +6,7 @@ import { Header } from '../header';
describe('Header', () => {
it('should render normally', async () => {
const component = shallow(
<Header/>
<Header addScriptedFieldUrl="" />
);
expect(component).toMatchSnapshot();

View file

@ -1,22 +1,39 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
EuiTitle,
EuiButton,
EuiFlexGroup,
EuiFlexItem,
EuiText,
EuiSpacer,
EuiTitle,
} 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>
export const Header = ({ addScriptedFieldUrl }) => (
<EuiFlexGroup alignItems="center">
<EuiFlexItem>
<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>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="addScriptedFieldLink"
href={addScriptedFieldUrl}
>
Add scripted field
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
);
Header.propTypes = {
addScriptedFieldUrl: PropTypes.string.isRequired,
};

View file

@ -0,0 +1,3 @@
export { Table } from './table';
export { Header } from './header';
export { CallOuts } from './call_outs';

View file

@ -1,94 +1,77 @@
// 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": "displayName",
"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,
],
<EuiInMemoryTable
columns={
Array [
Object {
"dataType": "string",
"description": "Name of the field",
"field": "displayName",
"name": "Name",
"sortable": true,
},
"recordId": "name",
"selection": undefined,
}
}
model={
Object {
"criteria": Object {
"page": Object {
"index": 0,
"size": 10,
},
"sort": Object {
"direction": "asc",
"field": "name",
},
Object {
"dataType": "string",
"description": "Language used for the field",
"field": "lang",
"name": "Lang",
"render": [Function],
"sortable": true,
},
"data": Object {
"records": Array [
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 {
"id": 1,
"name": "Elastic",
"description": "Edit this field",
"icon": "pencil",
"name": "Edit",
"onClick": [Function],
},
Object {
"color": "danger",
"description": "Delete this field",
"icon": "trash",
"name": "Delete",
"onClick": [Function],
},
],
"totalRecordCount": 1,
"name": "",
},
]
}
items={
Array [
Object {
"id": 1,
"name": "Elastic",
},
]
}
pagination={
Object {
"pageSizeOptions": Array [
5,
10,
25,
50,
],
}
}
sorting={true}
/>
`;

View file

@ -13,32 +13,17 @@ const indexPattern = {
}
};
const model = {
data: {
records: [{ id: 1, name: 'Elastic' }],
totalRecordCount: 1,
},
criteria: {
page: {
index: 0,
size: 10,
},
sort: {
field: 'name',
direction: 'asc'
},
}
};
const items = [{ id: 1, name: 'Elastic' }];
describe('Table', () => {
it('should render normally', async () => {
const component = shallow(
<Table
indexPattern={indexPattern}
model={model}
items={items}
editField={() => {}}
deleteField={() => {}}
onDataCriteriaChange={() => {}}
onChange={() => {}}
/>
);
@ -49,14 +34,14 @@ describe('Table', () => {
const component = shallow(
<Table
indexPattern={indexPattern}
model={model}
items={items}
editField={() => {}}
deleteField={() => {}}
onDataCriteriaChange={() => {}}
onChange={() => {}}
/>
);
const formatTableCell = shallow(component.prop('config').columns[3].render('Elastic'));
const formatTableCell = shallow(component.prop('columns')[3].render('Elastic'));
expect(formatTableCell).toMatchSnapshot();
});
@ -66,15 +51,15 @@ describe('Table', () => {
const component = shallow(
<Table
indexPattern={indexPattern}
model={model}
items={items}
editField={editField}
deleteField={() => {}}
onDataCriteriaChange={() => {}}
onChange={() => {}}
/>
);
// Click the delete button
component.prop('config').columns[4].actions[0].onClick();
component.prop('columns')[4].actions[0].onClick();
expect(editField).toBeCalled();
});
@ -84,15 +69,15 @@ describe('Table', () => {
const component = shallow(
<Table
indexPattern={indexPattern}
model={model}
items={items}
editField={() => {}}
deleteField={deleteField}
onDataCriteriaChange={() => {}}
onChange={() => {}}
/>
);
// Click the delete button
component.prop('config').columns[4].actions[1].onClick();
component.prop('columns')[4].actions[1].onClick();
expect(deleteField).toBeCalled();
});

View file

@ -2,31 +2,15 @@ import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import {
EuiTableOfRecords
EuiInMemoryTable,
} 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,
}),
items: PropTypes.array.isRequired,
editField: PropTypes.func.isRequired,
deleteField: PropTypes.func.isRequired,
onDataCriteriaChange: PropTypes.func.isRequired,
}
renderFormatCell = (value) => {
@ -41,79 +25,71 @@ export class Table extends PureComponent {
);
}
getTableConfig() {
const { editField, deleteField, onDataCriteriaChange } = this.props;
return {
recordId: 'name',
columns: [
{
field: 'displayName',
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;
const {
items,
editField,
deleteField,
} = this.props;
const columns = [{
field: 'displayName',
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,
}],
}];
const pagination = {
pageSizeOptions: [5, 10, 25, 50],
};
return (
<EuiTableOfRecords config={this.getTableConfig()} model={model} />
<EuiInMemoryTable
items={items}
columns={columns}
pagination={pagination}
sorting={true}
/>
);
}
}

View file

@ -1,78 +0,0 @@
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'
},
}
});
});
});

View file

@ -1,61 +0,0 @@
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',
}
}
};

View file

@ -4,18 +4,17 @@ import { getSupportedScriptingLanguages, getDeprecatedScriptingLanguages } from
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';
import {
Table,
Header,
CallOuts,
} from './components';
export class ScriptedFieldsTable extends Component {
static propTypes = {
@ -37,7 +36,6 @@ export class ScriptedFieldsTable extends Component {
fieldToDelete: undefined,
isDeleteConfirmationModalVisible: false,
fields: [],
...DEFAULT_TABLE_OF_RECORDS_STATE,
};
}
@ -61,34 +59,31 @@ export class ScriptedFieldsTable extends Component {
this.setState({
fields,
deprecatedLangsInUse,
...this.computeTableState(this.state.criteria, this.props, fields)
});
}
onDataCriteriaChange = criteria => {
this.setState(this.computeTableState(criteria));
}
getFilteredItems = () => {
const { fields } = this.state;
const { fieldFilter, scriptedFieldLanguageFilter } = this.props;
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));
}
}
let languageFilteredFields = fields;
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);
if (scriptedFieldLanguageFilter) {
languageFilteredFields = fields.filter(
field => field.lang === this.props.scriptedFieldLanguageFilter
);
}
return getTableOfRecordsState(items, criteria);
let filteredFields = languageFilteredFields;
if (fieldFilter) {
const normalizedFieldFilter = this.props.fieldFilter.toLowerCase();
filteredFields = languageFilteredFields.filter(
field => field.name.toLowerCase().includes(normalizedFieldFilter)
);
}
return filteredFields;
}
renderCallOuts() {
@ -147,44 +142,23 @@ export class ScriptedFieldsTable extends Component {
indexPattern,
} = this.props;
const {
data,
criteria: {
page,
sort,
},
fields,
} = this.state;
const model = {
data,
criteria: {
page,
sort,
},
};
const items = this.getFilteredItems();
return (
<div>
<Header/>
<Header addScriptedFieldUrl={helpers.getRouteHref(indexPattern, 'addField')} />
{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
}
<Table
indexPattern={indexPattern}
items={items}
editField={field => this.props.helpers.redirectToRoute(field, 'edit')}
deleteField={this.startDeleteField}
/>
{this.renderDeleteConfirmationModal()}
</div>
);