mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
* Source filters table * Updates * Handle no source filters * PR feedback * Fix merge issues * PR feedback * PR feedback * PR feedback * Upgrade to 3.3.0 which allows us to use Fragments in enzyme tests * Use EuiInMemoryTable instead * Remove dead code and simplify some tests * Dynamically update the matches as the user edits the filter * Apparently, this has been using the wrong parameter name * Restructure to stop storing computed data and add reselect helper * Reselect is tiny, just use that * PR feedback * Fix merge issues * PR feedback
This commit is contained in:
parent
c750011c2e
commit
64739f80e1
24 changed files with 1514 additions and 265 deletions
|
@ -80,9 +80,10 @@
|
|||
"@elastic/numeral": "2.3.1",
|
||||
"@elastic/ui-ace": "0.2.3",
|
||||
"@kbn/babel-preset": "link:packages/kbn-babel-preset",
|
||||
"@kbn/pm": "link:packages/kbn-pm",
|
||||
"@kbn/datemath": "link:packages/kbn-datemath",
|
||||
"@kbn/pm": "link:packages/kbn-pm",
|
||||
"@kbn/test-subj-selector": "link:packages/kbn-test-subj-selector",
|
||||
"JSONStream": "1.1.1",
|
||||
"accept-language-parser": "1.2.0",
|
||||
"angular": "1.6.5",
|
||||
"angular-aria": "1.6.6",
|
||||
|
@ -142,7 +143,6 @@
|
|||
"js-yaml": "3.4.1",
|
||||
"json-stringify-pretty-compact": "1.0.4",
|
||||
"json-stringify-safe": "5.0.1",
|
||||
"JSONStream": "1.1.1",
|
||||
"jstimezonedetect": "1.0.5",
|
||||
"leaflet": "1.0.3",
|
||||
"leaflet-draw": "0.4.10",
|
||||
|
|
|
@ -141,11 +141,7 @@
|
|||
|
||||
<div id="reactScriptedFieldsTable"></div>
|
||||
|
||||
<source-filters-table
|
||||
ng-show="state.tab == 'sourceFilters'"
|
||||
index-pattern="indexPattern"
|
||||
class="fields source-filters"
|
||||
></source-filters-table>
|
||||
<div id="reactSourceFiltersTable"></div>
|
||||
</div>
|
||||
</div>
|
||||
</kbn-management-indices>
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import _ from 'lodash';
|
||||
import './index_header';
|
||||
import './scripted_field_editor';
|
||||
import './source_filters_table';
|
||||
import { KbnUrlProvider } from 'ui/url';
|
||||
import { IndicesEditSectionsProvider } from './edit_sections';
|
||||
import { fatalError } from 'ui/notify';
|
||||
|
@ -9,15 +8,49 @@ import uiRoutes from 'ui/routes';
|
|||
import { uiModules } from 'ui/modules';
|
||||
import template from './edit_index_pattern.html';
|
||||
import { FieldWildcardProvider } from 'ui/field_wildcard';
|
||||
|
||||
import React from 'react';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
import { SourceFiltersTable } from './source_filters_table';
|
||||
import { IndexedFieldsTable } from './indexed_fields_table';
|
||||
import { ScriptedFieldsTable } from './scripted_fields_table';
|
||||
|
||||
const REACT_SOURCE_FILTERS_DOM_ELEMENT_ID = 'reactSourceFiltersTable';
|
||||
const REACT_INDEXED_FIELDS_DOM_ELEMENT_ID = 'reactIndexedFieldsTable';
|
||||
const REACT_SCRIPTED_FIELDS_DOM_ELEMENT_ID = 'reactScriptedFieldsTable';
|
||||
|
||||
function updateSourceFiltersTable($scope, $state) {
|
||||
if ($state.tab === 'sourceFilters') {
|
||||
$scope.$$postDigest(() => {
|
||||
const node = document.getElementById(REACT_SOURCE_FILTERS_DOM_ELEMENT_ID);
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
render(
|
||||
<SourceFiltersTable
|
||||
indexPattern={$scope.indexPattern}
|
||||
filterFilter={$scope.fieldFilter}
|
||||
fieldWildcardMatcher={$scope.fieldWildcardMatcher}
|
||||
onAddOrRemoveFilter={() => {
|
||||
$scope.editSections = $scope.editSectionsProvider($scope.indexPattern);
|
||||
$scope.refreshFilters();
|
||||
$scope.$apply();
|
||||
}}
|
||||
/>,
|
||||
node,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
destroySourceFiltersTable();
|
||||
}
|
||||
}
|
||||
|
||||
function destroySourceFiltersTable() {
|
||||
const node = document.getElementById(REACT_SOURCE_FILTERS_DOM_ELEMENT_ID);
|
||||
node && unmountComponentAtNode(node);
|
||||
}
|
||||
|
||||
|
||||
function updateScriptedFieldsTable($scope, $state) {
|
||||
if ($state.tab === 'scriptedFields') {
|
||||
$scope.$$postDigest(() => {
|
||||
|
@ -167,6 +200,7 @@ uiModules.get('apps/management')
|
|||
$state.tab = obj.index;
|
||||
updateIndexedFieldsTable($scope, $state);
|
||||
updateScriptedFieldsTable($scope, $state);
|
||||
updateSourceFiltersTable($scope, $state);
|
||||
$state.save();
|
||||
};
|
||||
|
||||
|
@ -249,6 +283,9 @@ uiModules.get('apps/management')
|
|||
if ($scope.indexedFieldTypeFilter !== undefined && $state.tab === 'indexedFields') {
|
||||
updateIndexedFieldsTable($scope, $state);
|
||||
}
|
||||
if ($scope.fieldFilter !== undefined && $state.tab === 'sourceFilters') {
|
||||
updateSourceFiltersTable($scope, $state);
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$watch('scriptedFieldLanguageFilter', () => {
|
||||
|
@ -261,4 +298,7 @@ uiModules.get('apps/management')
|
|||
destroyIndexedFieldsTable();
|
||||
destroyScriptedFieldsTable();
|
||||
});
|
||||
|
||||
updateScriptedFieldsTable($scope, $state);
|
||||
updateSourceFiltersTable($scope, $state);
|
||||
});
|
||||
|
|
|
@ -10,7 +10,7 @@ export function IndicesEditSectionsProvider() {
|
|||
_.defaults(fieldCount, {
|
||||
indexed: 0,
|
||||
scripted: 0,
|
||||
sourceFilters: 0
|
||||
sourceFilters: indexPattern.sourceFilters ? indexPattern.sourceFilters.length : 0,
|
||||
});
|
||||
|
||||
return [
|
||||
|
|
|
@ -0,0 +1,307 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SourceFiltersTable should add a filter 1`] = `
|
||||
<div>
|
||||
<header />
|
||||
<AddFilter
|
||||
onAddFilter={[Function]}
|
||||
/>
|
||||
<eui-spacer
|
||||
size="l"
|
||||
/>
|
||||
<Table
|
||||
deleteFilter={[Function]}
|
||||
fieldWildcardMatcher={[Function]}
|
||||
indexPattern={
|
||||
Object {
|
||||
"save": [MockFunction] {
|
||||
"calls": Array [
|
||||
Array [],
|
||||
],
|
||||
},
|
||||
"sourceFilters": Array [
|
||||
Object {
|
||||
"value": "tim*",
|
||||
},
|
||||
Object {
|
||||
"value": "na*",
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
isSaving={false}
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
"clientId": 2,
|
||||
"value": "tim*",
|
||||
},
|
||||
Object {
|
||||
"clientId": 3,
|
||||
"value": "na*",
|
||||
},
|
||||
]
|
||||
}
|
||||
saveFilter={[Function]}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`SourceFiltersTable should filter based on the query bar 1`] = `
|
||||
<div>
|
||||
<header />
|
||||
<AddFilter
|
||||
onAddFilter={[Function]}
|
||||
/>
|
||||
<eui-spacer
|
||||
size="l"
|
||||
/>
|
||||
<Table
|
||||
deleteFilter={[Function]}
|
||||
fieldWildcardMatcher={[Function]}
|
||||
indexPattern={
|
||||
Object {
|
||||
"sourceFilters": Array [
|
||||
Object {
|
||||
"value": "time*",
|
||||
},
|
||||
Object {
|
||||
"value": "nam*",
|
||||
},
|
||||
Object {
|
||||
"value": "age*",
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
isSaving={false}
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
"clientId": 1,
|
||||
"value": "time*",
|
||||
},
|
||||
]
|
||||
}
|
||||
saveFilter={[Function]}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`SourceFiltersTable should remove a filter 1`] = `
|
||||
<div>
|
||||
<header />
|
||||
<AddFilter
|
||||
onAddFilter={[Function]}
|
||||
/>
|
||||
<eui-spacer
|
||||
size="l"
|
||||
/>
|
||||
<Table
|
||||
deleteFilter={[Function]}
|
||||
fieldWildcardMatcher={[Function]}
|
||||
indexPattern={
|
||||
Object {
|
||||
"save": [MockFunction] {
|
||||
"calls": Array [
|
||||
Array [],
|
||||
],
|
||||
},
|
||||
"sourceFilters": Array [
|
||||
Object {
|
||||
"clientId": 1,
|
||||
"value": "tim*",
|
||||
},
|
||||
Object {
|
||||
"clientId": 2,
|
||||
"value": "na*",
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
isSaving={false}
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
"clientId": 3,
|
||||
"value": "tim*",
|
||||
},
|
||||
Object {
|
||||
"clientId": 4,
|
||||
"value": "na*",
|
||||
},
|
||||
]
|
||||
}
|
||||
saveFilter={[Function]}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`SourceFiltersTable should render normally 1`] = `
|
||||
<div>
|
||||
<header />
|
||||
<AddFilter
|
||||
onAddFilter={[Function]}
|
||||
/>
|
||||
<eui-spacer
|
||||
size="l"
|
||||
/>
|
||||
<Table
|
||||
deleteFilter={[Function]}
|
||||
fieldWildcardMatcher={[Function]}
|
||||
indexPattern={
|
||||
Object {
|
||||
"sourceFilters": Array [
|
||||
Object {
|
||||
"value": "time*",
|
||||
},
|
||||
Object {
|
||||
"value": "nam*",
|
||||
},
|
||||
Object {
|
||||
"value": "age*",
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
isSaving={false}
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
"clientId": 1,
|
||||
"value": "time*",
|
||||
},
|
||||
Object {
|
||||
"clientId": 2,
|
||||
"value": "nam*",
|
||||
},
|
||||
Object {
|
||||
"clientId": 3,
|
||||
"value": "age*",
|
||||
},
|
||||
]
|
||||
}
|
||||
saveFilter={[Function]}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`SourceFiltersTable should should a loading indicator when saving 1`] = `
|
||||
<div>
|
||||
<header />
|
||||
<AddFilter
|
||||
onAddFilter={[Function]}
|
||||
/>
|
||||
<eui-spacer
|
||||
size="l"
|
||||
/>
|
||||
<Table
|
||||
deleteFilter={[Function]}
|
||||
fieldWildcardMatcher={[Function]}
|
||||
indexPattern={
|
||||
Object {
|
||||
"sourceFilters": Array [
|
||||
Object {
|
||||
"value": "tim*",
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
isSaving={true}
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
"clientId": 1,
|
||||
"value": "tim*",
|
||||
},
|
||||
]
|
||||
}
|
||||
saveFilter={[Function]}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`SourceFiltersTable should show a delete modal 1`] = `
|
||||
<div>
|
||||
<header />
|
||||
<AddFilter
|
||||
onAddFilter={[Function]}
|
||||
/>
|
||||
<eui-spacer
|
||||
size="l"
|
||||
/>
|
||||
<Table
|
||||
deleteFilter={[Function]}
|
||||
fieldWildcardMatcher={[Function]}
|
||||
indexPattern={
|
||||
Object {
|
||||
"sourceFilters": Array [
|
||||
Object {
|
||||
"value": "tim*",
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
isSaving={false}
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
"clientId": 1,
|
||||
"value": "tim*",
|
||||
},
|
||||
]
|
||||
}
|
||||
saveFilter={[Function]}
|
||||
/>
|
||||
<eui-overlay-mask>
|
||||
<eui-confirm-modal
|
||||
cancelButtonText="Cancel"
|
||||
confirmButtonText="Delete"
|
||||
onCancel={[Function]}
|
||||
onConfirm={[Function]}
|
||||
title="Delete source filter 'tim*'?"
|
||||
/>
|
||||
</eui-overlay-mask>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`SourceFiltersTable should update a filter 1`] = `
|
||||
<div>
|
||||
<header />
|
||||
<AddFilter
|
||||
onAddFilter={[Function]}
|
||||
/>
|
||||
<eui-spacer
|
||||
size="l"
|
||||
/>
|
||||
<Table
|
||||
deleteFilter={[Function]}
|
||||
fieldWildcardMatcher={[Function]}
|
||||
indexPattern={
|
||||
Object {
|
||||
"save": [MockFunction] {
|
||||
"calls": Array [
|
||||
Array [],
|
||||
],
|
||||
},
|
||||
"sourceFilters": Array [
|
||||
Object {
|
||||
"clientId": 1,
|
||||
"value": "tim*",
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
isSaving={false}
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
"clientId": 2,
|
||||
"value": "tim*",
|
||||
},
|
||||
]
|
||||
}
|
||||
saveFilter={[Function]}
|
||||
/>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,152 @@
|
|||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { SourceFiltersTable } from '../source_filters_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',
|
||||
EuiLoadingSpinner: 'eui-loading-spinner',
|
||||
Comparators: {
|
||||
property: () => {},
|
||||
default: () => {},
|
||||
},
|
||||
}));
|
||||
jest.mock('../components/header', () => ({ Header: 'header' }));
|
||||
jest.mock('../components/table', () => ({
|
||||
// Note: this seems to fix React complaining about non lowercase attributes
|
||||
Table: () => {
|
||||
return 'table';
|
||||
}
|
||||
}));
|
||||
|
||||
const indexPattern = {
|
||||
sourceFilters: [
|
||||
{ value: 'time*' },
|
||||
{ value: 'nam*' },
|
||||
{ value: 'age*' },
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
describe('SourceFiltersTable', () => {
|
||||
it('should render normally', async () => {
|
||||
const component = shallow(
|
||||
<SourceFiltersTable
|
||||
indexPattern={indexPattern}
|
||||
fieldWildcardMatcher={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should filter based on the query bar', async () => {
|
||||
const component = shallow(
|
||||
<SourceFiltersTable
|
||||
indexPattern={indexPattern}
|
||||
fieldWildcardMatcher={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
component.setProps({ filterFilter: 'ti' });
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should should a loading indicator when saving', async () => {
|
||||
const component = shallow(
|
||||
<SourceFiltersTable
|
||||
indexPattern={{
|
||||
sourceFilters: [{ value: 'tim*' }]
|
||||
}}
|
||||
fieldWildcardMatcher={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
component.setState({ isSaving: true });
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should show a delete modal', async () => {
|
||||
const component = shallow(
|
||||
<SourceFiltersTable
|
||||
indexPattern={{
|
||||
sourceFilters: [{ value: 'tim*' }]
|
||||
}}
|
||||
fieldWildcardMatcher={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
component.instance().startDeleteFilter({ value: 'tim*' });
|
||||
component.update(); // We are not calling `.setState` directly so we need to re-render
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should remove a filter', async () => {
|
||||
const save = jest.fn();
|
||||
const component = shallow(
|
||||
<SourceFiltersTable
|
||||
indexPattern={{
|
||||
save,
|
||||
sourceFilters: [{ value: 'tim*' }, { value: 'na*' }]
|
||||
}}
|
||||
fieldWildcardMatcher={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
component.instance().startDeleteFilter({ value: 'tim*' });
|
||||
component.update(); // We are not calling `.setState` directly so we need to re-render
|
||||
await component.instance().deleteFilter();
|
||||
component.update(); // We are not calling `.setState` directly so we need to re-render
|
||||
|
||||
expect(save).toBeCalled();
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should add a filter', async () => {
|
||||
const save = jest.fn();
|
||||
const component = shallow(
|
||||
<SourceFiltersTable
|
||||
indexPattern={{
|
||||
save,
|
||||
sourceFilters: [{ value: 'tim*' }]
|
||||
}}
|
||||
fieldWildcardMatcher={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
await component.instance().onAddFilter('na*');
|
||||
component.update(); // We are not calling `.setState` directly so we need to re-render
|
||||
|
||||
expect(save).toBeCalled();
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should update a filter', async () => {
|
||||
const save = jest.fn();
|
||||
const component = shallow(
|
||||
<SourceFiltersTable
|
||||
indexPattern={{
|
||||
save,
|
||||
sourceFilters: [{ value: 'tim*' }]
|
||||
}}
|
||||
fieldWildcardMatcher={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
await component.instance().saveFilter({ oldFilterValue: 'tim*', newFilterValue: 'ti*' });
|
||||
component.update(); // We are not calling `.setState` directly so we need to re-render
|
||||
|
||||
expect(save).toBeCalled();
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,79 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AddFilter should ignore strings with just spaces 1`] = `
|
||||
<EuiFlexGroup
|
||||
alignItems="stretch"
|
||||
component="div"
|
||||
gutterSize="l"
|
||||
justifyContent="flexStart"
|
||||
responsive={true}
|
||||
wrap={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={10}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth={true}
|
||||
isLoading={false}
|
||||
onChange={[Function]}
|
||||
placeholder="source filter, accepts wildcards (e.g., \`user*\` to filter fields starting with 'user')"
|
||||
value=""
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={true}
|
||||
>
|
||||
<EuiButton
|
||||
color="primary"
|
||||
fill={false}
|
||||
iconSide="left"
|
||||
isDisabled={true}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Add
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
`;
|
||||
|
||||
exports[`AddFilter should render normally 1`] = `
|
||||
<EuiFlexGroup
|
||||
alignItems="stretch"
|
||||
component="div"
|
||||
gutterSize="l"
|
||||
justifyContent="flexStart"
|
||||
responsive={true}
|
||||
wrap={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={10}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth={true}
|
||||
isLoading={false}
|
||||
onChange={[Function]}
|
||||
placeholder="source filter, accepts wildcards (e.g., \`user*\` to filter fields starting with 'user')"
|
||||
value=""
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={true}
|
||||
>
|
||||
<EuiButton
|
||||
color="primary"
|
||||
fill={false}
|
||||
iconSide="left"
|
||||
isDisabled={true}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Add
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
`;
|
|
@ -0,0 +1,42 @@
|
|||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { AddFilter } from '../add_filter';
|
||||
|
||||
describe('AddFilter', () => {
|
||||
it('should render normally', async () => {
|
||||
const component = shallow(
|
||||
<AddFilter onAddFilter={() => {}}/>
|
||||
);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should allow adding a filter', async () => {
|
||||
const onAddFilter = jest.fn();
|
||||
const component = shallow(
|
||||
<AddFilter onAddFilter={onAddFilter}/>
|
||||
);
|
||||
|
||||
// Set a value in the input field
|
||||
component.setState({ filter: 'tim*' });
|
||||
|
||||
// Click the button
|
||||
component.find('EuiButton').simulate('click');
|
||||
component.update();
|
||||
|
||||
expect(onAddFilter).toBeCalledWith('tim*');
|
||||
});
|
||||
|
||||
it('should ignore strings with just spaces', async () => {
|
||||
const component = shallow(
|
||||
<AddFilter onAddFilter={() => {}}/>
|
||||
);
|
||||
|
||||
// Set a value in the input field
|
||||
component.find('EuiFieldText').simulate('keypress', ' ');
|
||||
component.update();
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,52 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFieldText,
|
||||
EuiButton,
|
||||
} from '@elastic/eui';
|
||||
|
||||
export class AddFilter extends Component {
|
||||
static propTypes = {
|
||||
onAddFilter: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
filter: '',
|
||||
};
|
||||
}
|
||||
|
||||
onAddFilter = () => {
|
||||
this.props.onAddFilter(this.state.filter);
|
||||
this.setState({ filter: '' });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { filter } = this.state;
|
||||
|
||||
return (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={10}>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
value={filter}
|
||||
onChange={e => this.setState({ filter: e.target.value.trim() })}
|
||||
placeholder="source filter, accepts wildcards (e.g., `user*` to filter fields starting with 'user')"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiButton
|
||||
isDisabled={filter.length === 0}
|
||||
onClick={this.onAddFilter}
|
||||
>
|
||||
Add
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { AddFilter } from './add_filter';
|
|
@ -0,0 +1,24 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Header should render normally 1`] = `
|
||||
<div>
|
||||
<EuiTitle
|
||||
size="s"
|
||||
>
|
||||
<h3>
|
||||
Source filters
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
<EuiText>
|
||||
<p>
|
||||
Source filters can be used to exclude one or more fields when fetching the document source. This happens when viewing a document in the Discover app, or with a table displaying results from a saved search in the Dashboard app. Each row is built using the source of a single document, and if you have documents with large or unimportant fields you may benefit from filtering those out at this lower level.
|
||||
</p>
|
||||
<p>
|
||||
Note that multi-fields will incorrectly appear as matches in the table below. These filters only actually apply to fields in the original source document, so matching multi-fields are not actually being filtered.
|
||||
</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,31 @@
|
|||
import React from 'react';
|
||||
|
||||
import {
|
||||
EuiTitle,
|
||||
EuiText,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
|
||||
export const Header = () => (
|
||||
<div>
|
||||
<EuiTitle size="s">
|
||||
<h3>Source filters</h3>
|
||||
</EuiTitle>
|
||||
<EuiText>
|
||||
<p>
|
||||
Source filters can be used to exclude one or more fields when fetching the document source.
|
||||
This happens when viewing a document in the Discover app, or with a table displaying results
|
||||
from a saved search in the Dashboard app. Each row is built using the source of a single
|
||||
document, and if you have documents with large or unimportant fields you may benefit from
|
||||
filtering those out at this lower level.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Note that multi-fields will incorrectly appear as matches in the table below.
|
||||
These filters only actually apply to fields in the original source document,
|
||||
so matching multi-fields are not actually being filtered.
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiSpacer size="s" />
|
||||
</div>
|
||||
);
|
|
@ -0,0 +1 @@
|
|||
export { Header } from './header';
|
|
@ -0,0 +1,107 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Table editing should show a save button 1`] = `
|
||||
<div>
|
||||
<React.Fragment>
|
||||
<EuiButtonIcon
|
||||
aria-label="Save"
|
||||
color="primary"
|
||||
iconType="checkInCircleFilled"
|
||||
onClick={[Function]}
|
||||
size="s"
|
||||
type="button"
|
||||
/>
|
||||
<EuiButtonIcon
|
||||
aria-label="Cancel"
|
||||
color="primary"
|
||||
iconType="cross"
|
||||
onClick={[Function]}
|
||||
size="s"
|
||||
type="button"
|
||||
/>
|
||||
</React.Fragment>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Table editing should show an input field 1`] = `
|
||||
<EuiFormControlLayout
|
||||
fullWidth={false}
|
||||
iconSide="left"
|
||||
isLoading={false}
|
||||
>
|
||||
<EuiValidatableControl>
|
||||
<input
|
||||
autoFocus={true}
|
||||
className="euiFieldText"
|
||||
onChange={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="text"
|
||||
value="tim*"
|
||||
/>
|
||||
</EuiValidatableControl>
|
||||
</EuiFormControlLayout>
|
||||
`;
|
||||
|
||||
exports[`Table editing should update the matches dynamically as input value is changed 1`] = `
|
||||
<div>
|
||||
<span>
|
||||
time, value
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Table should render filter matches 1`] = `
|
||||
<span>
|
||||
time
|
||||
</span>
|
||||
`;
|
||||
|
||||
exports[`Table should render normally 1`] = `
|
||||
<EuiInMemoryTable
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"dataType": "string",
|
||||
"description": "Filter name",
|
||||
"field": "value",
|
||||
"name": "Filter",
|
||||
"render": [Function],
|
||||
"sortable": true,
|
||||
},
|
||||
Object {
|
||||
"dataType": "string",
|
||||
"description": "Language used for the field",
|
||||
"field": "value",
|
||||
"name": "Matches",
|
||||
"render": [Function],
|
||||
"sortable": true,
|
||||
},
|
||||
Object {
|
||||
"align": "right",
|
||||
"name": "",
|
||||
"render": [Function],
|
||||
"width": "100",
|
||||
},
|
||||
]
|
||||
}
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
"value": "tim*",
|
||||
},
|
||||
]
|
||||
}
|
||||
loading={true}
|
||||
pagination={
|
||||
Object {
|
||||
"pageSizeOptions": Array [
|
||||
5,
|
||||
10,
|
||||
25,
|
||||
50,
|
||||
],
|
||||
}
|
||||
}
|
||||
sorting={true}
|
||||
/>
|
||||
`;
|
|
@ -0,0 +1,304 @@
|
|||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { Table } from '../table';
|
||||
import { keyCodes } from '@elastic/eui';
|
||||
|
||||
const indexPattern = {};
|
||||
const items = [{ value: 'tim*' }];
|
||||
|
||||
describe('Table', () => {
|
||||
it('should render normally', async () => {
|
||||
const component = shallow(
|
||||
<Table
|
||||
indexPattern={indexPattern}
|
||||
items={items}
|
||||
deleteFilter={() => {}}
|
||||
fieldWildcardMatcher={() => {}}
|
||||
saveFilter={() => {}}
|
||||
isSaving={true}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render filter matches', async () => {
|
||||
const component = shallow(
|
||||
<Table
|
||||
indexPattern={{
|
||||
getNonScriptedFields: () => [{ name: 'time' }, { name: 'value' }],
|
||||
}}
|
||||
items={items}
|
||||
deleteFilter={() => {}}
|
||||
fieldWildcardMatcher={filter => field => field.includes(filter[0])}
|
||||
saveFilter={() => {}}
|
||||
isSaving={false}
|
||||
/>
|
||||
);
|
||||
|
||||
const matchesTableCell = shallow(
|
||||
component.prop('columns')[1].render('tim', { clientId: 1 })
|
||||
);
|
||||
expect(matchesTableCell).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('editing', () => {
|
||||
const saveFilter = jest.fn();
|
||||
const clientId = 1;
|
||||
let component;
|
||||
|
||||
beforeEach(() => {
|
||||
component = shallow(
|
||||
<Table
|
||||
indexPattern={indexPattern}
|
||||
items={items}
|
||||
deleteFilter={() => {}}
|
||||
fieldWildcardMatcher={() => {}}
|
||||
saveFilter={saveFilter}
|
||||
isSaving={false}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
it('should show an input field', () => {
|
||||
// Start the editing process
|
||||
const editingComponent = shallow(
|
||||
// Wrap in a div because: https://github.com/airbnb/enzyme/issues/1213
|
||||
<div>
|
||||
{component.prop('columns')[2].render({ clientId, value: 'tim*' })}
|
||||
</div>
|
||||
);
|
||||
editingComponent
|
||||
.find('EuiButtonIcon')
|
||||
.at(1)
|
||||
.simulate('click');
|
||||
// Ensure the state change propagates
|
||||
component.update();
|
||||
|
||||
// Ensure the table cell switches to an input
|
||||
const filterNameTableCell = shallow(
|
||||
component.prop('columns')[0].render('tim*', { clientId })
|
||||
);
|
||||
expect(filterNameTableCell).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should show a save button', () => {
|
||||
// Start the editing process
|
||||
const editingComponent = shallow(
|
||||
// Fixes: Invariant Violation: ReactShallowRenderer render(): Shallow rendering works only with custom components, but the provided element type was `symbol`.
|
||||
<div>
|
||||
{component.prop('columns')[2].render({ clientId, value: 'tim*' })}
|
||||
</div>
|
||||
);
|
||||
editingComponent
|
||||
.find('EuiButtonIcon')
|
||||
.at(1)
|
||||
.simulate('click');
|
||||
|
||||
// Ensure the state change propagates
|
||||
component.update();
|
||||
|
||||
// Verify save button
|
||||
const saveTableCell = shallow(
|
||||
// Fixes Invariant Violation: ReactShallowRenderer render(): Shallow rendering works only with custom components, but the provided element type was `symbol`.
|
||||
<div>
|
||||
{component.prop('columns')[2].render({ clientId, value: 'tim*' })}
|
||||
</div>
|
||||
);
|
||||
expect(saveTableCell).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should update the matches dynamically as input value is changed', () => {
|
||||
const localComponent = shallow(
|
||||
<Table
|
||||
indexPattern={{
|
||||
getNonScriptedFields: () => [{ name: 'time' }, { name: 'value' }],
|
||||
}}
|
||||
items={items}
|
||||
deleteFilter={() => {}}
|
||||
fieldWildcardMatcher={query => () => {
|
||||
return query.includes('time*');
|
||||
}}
|
||||
saveFilter={saveFilter}
|
||||
isSaving={false}
|
||||
/>
|
||||
);
|
||||
// Start the editing process
|
||||
const editingComponent = shallow(
|
||||
// Fixes: Invariant Violation: ReactShallowRenderer render(): Shallow rendering works only with custom components, but the provided element type was `symbol`.
|
||||
<div>
|
||||
{localComponent
|
||||
.prop('columns')[2]
|
||||
.render({ clientId, value: 'tim*' })}
|
||||
</div>
|
||||
);
|
||||
editingComponent
|
||||
.find('EuiButtonIcon')
|
||||
.at(1)
|
||||
.simulate('click');
|
||||
|
||||
// Update the value
|
||||
localComponent.setState({ editingFilterValue: 'time*' });
|
||||
|
||||
// Ensure the state change propagates
|
||||
localComponent.update();
|
||||
|
||||
// Verify updated matches
|
||||
const matchesTableCell = shallow(
|
||||
// Fixes Invariant Violation: ReactShallowRenderer render(): Shallow rendering works only with custom components, but the provided element type was `symbol`.
|
||||
<div>
|
||||
{localComponent.prop('columns')[1].render('tim*', { clientId })}
|
||||
</div>
|
||||
);
|
||||
expect(matchesTableCell).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should exit on save', () => {
|
||||
// Change the value to something else
|
||||
component.setState({
|
||||
editingFilterId: clientId,
|
||||
editingFilterValue: 'ti*',
|
||||
});
|
||||
|
||||
// Click the save button
|
||||
const editingComponent = shallow(
|
||||
// Fixes Invariant Violation: ReactShallowRenderer render(): Shallow rendering works only with custom components, but the provided element type was `symbol`.
|
||||
<div>
|
||||
{component.prop('columns')[2].render({ clientId, value: 'tim*' })}
|
||||
</div>
|
||||
);
|
||||
editingComponent
|
||||
.find('EuiButtonIcon')
|
||||
.at(0)
|
||||
.simulate('click');
|
||||
|
||||
// Ensure we call saveFilter properly
|
||||
expect(saveFilter).toBeCalledWith({
|
||||
filterId: clientId,
|
||||
newFilterValue: 'ti*',
|
||||
});
|
||||
|
||||
// Ensure the state is properly reset
|
||||
expect(component.state('editingFilterId')).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow deletes', () => {
|
||||
const deleteFilter = jest.fn();
|
||||
|
||||
const component = shallow(
|
||||
<Table
|
||||
indexPattern={indexPattern}
|
||||
items={items}
|
||||
deleteFilter={deleteFilter}
|
||||
fieldWildcardMatcher={() => {}}
|
||||
saveFilter={() => {}}
|
||||
isSaving={false}
|
||||
/>
|
||||
);
|
||||
|
||||
// Click the delete button
|
||||
const deleteCellComponent = shallow(
|
||||
// Fixes Invariant Violation: ReactShallowRenderer render(): Shallow rendering works only with custom components, but the provided element type was `symbol`.
|
||||
<div>
|
||||
{component.prop('columns')[2].render({ clientId: 1, value: 'tim*' })}
|
||||
</div>
|
||||
);
|
||||
deleteCellComponent
|
||||
.find('EuiButtonIcon')
|
||||
.at(0)
|
||||
.simulate('click');
|
||||
expect(deleteFilter).toBeCalled();
|
||||
});
|
||||
|
||||
it('should save when in edit mode and the enter key is pressed', () => {
|
||||
const saveFilter = jest.fn();
|
||||
const clientId = 1;
|
||||
|
||||
const component = shallow(
|
||||
<Table
|
||||
indexPattern={indexPattern}
|
||||
items={items}
|
||||
deleteFilter={() => {}}
|
||||
fieldWildcardMatcher={() => {}}
|
||||
saveFilter={saveFilter}
|
||||
isSaving={false}
|
||||
/>
|
||||
);
|
||||
|
||||
// Start the editing process
|
||||
const editingComponent = shallow(
|
||||
// Fixes Invariant Violation: ReactShallowRenderer render(): Shallow rendering works only with custom components, but the provided element type was `symbol`.
|
||||
<div>
|
||||
{component.prop('columns')[2].render({ clientId, value: 'tim*' })}
|
||||
</div>
|
||||
);
|
||||
editingComponent
|
||||
.find('EuiButtonIcon')
|
||||
.at(1)
|
||||
.simulate('click');
|
||||
// Ensure the state change propagates
|
||||
component.update();
|
||||
|
||||
// Get the rendered input cell
|
||||
const filterNameTableCell = shallow(
|
||||
// Fixes Invariant Violation: ReactShallowRenderer render(): Shallow rendering works only with custom components, but the provided element type was `symbol`.
|
||||
<div>{component.prop('columns')[0].render('tim*', { clientId })}</div>
|
||||
);
|
||||
|
||||
// Press the enter key
|
||||
filterNameTableCell
|
||||
.find('EuiFieldText')
|
||||
.simulate('keydown', { keyCode: keyCodes.ENTER });
|
||||
expect(saveFilter).toBeCalled();
|
||||
|
||||
// It should reset
|
||||
expect(component.state('editingFilterId')).toBe(null);
|
||||
});
|
||||
|
||||
it('should cancel when in edit mode and the esc key is pressed', () => {
|
||||
const saveFilter = jest.fn();
|
||||
const clientId = 1;
|
||||
|
||||
const component = shallow(
|
||||
<Table
|
||||
indexPattern={indexPattern}
|
||||
items={items}
|
||||
deleteFilter={() => {}}
|
||||
fieldWildcardMatcher={() => {}}
|
||||
saveFilter={saveFilter}
|
||||
isSaving={false}
|
||||
/>
|
||||
);
|
||||
|
||||
// Start the editing process
|
||||
const editingComponent = shallow(
|
||||
// Fixes Invariant Violation: ReactShallowRenderer render(): Shallow rendering works only with custom components, but the provided element type was `symbol`.
|
||||
<div>
|
||||
{component.prop('columns')[2].render({ clientId, value: 'tim*' })}
|
||||
</div>
|
||||
);
|
||||
editingComponent
|
||||
.find('EuiButtonIcon')
|
||||
.at(1)
|
||||
.simulate('click');
|
||||
// Ensure the state change propagates
|
||||
component.update();
|
||||
|
||||
// Get the rendered input cell
|
||||
const filterNameTableCell = shallow(
|
||||
// Fixes Invariant Violation: ReactShallowRenderer render(): Shallow rendering works only with custom components, but the provided element type was `symbol`.
|
||||
<div>{component.prop('columns')[0].render('tim*', { clientId })}</div>
|
||||
);
|
||||
|
||||
// Press the enter key
|
||||
filterNameTableCell
|
||||
.find('EuiFieldText')
|
||||
.simulate('keydown', { keyCode: keyCodes.ESCAPE });
|
||||
expect(saveFilter).not.toBeCalled();
|
||||
|
||||
// It should reset
|
||||
expect(component.state('editingFilterId')).toBe(null);
|
||||
});
|
||||
});
|
|
@ -0,0 +1 @@
|
|||
export { Table } from './table';
|
|
@ -0,0 +1,178 @@
|
|||
import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {
|
||||
EuiInMemoryTable,
|
||||
EuiFieldText,
|
||||
EuiButtonIcon,
|
||||
keyCodes,
|
||||
RIGHT_ALIGNMENT,
|
||||
} from '@elastic/eui';
|
||||
|
||||
export class Table extends Component {
|
||||
static propTypes = {
|
||||
indexPattern: PropTypes.object.isRequired,
|
||||
items: PropTypes.array.isRequired,
|
||||
deleteFilter: PropTypes.func.isRequired,
|
||||
fieldWildcardMatcher: PropTypes.func.isRequired,
|
||||
saveFilter: PropTypes.func.isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
editingFilterId: null,
|
||||
editingFilterValue: null,
|
||||
};
|
||||
}
|
||||
|
||||
startEditingFilter = (id, value) =>
|
||||
this.setState({ editingFilterId: id, editingFilterValue: value });
|
||||
stopEditingFilter = () => this.setState({ editingFilterId: null });
|
||||
onEditingFilterChange = e =>
|
||||
this.setState({ editingFilterValue: e.target.value });
|
||||
|
||||
onEditFieldKeyDown = ({ keyCode }) => {
|
||||
if (keyCodes.ENTER === keyCode) {
|
||||
this.props.saveFilter({
|
||||
filterId: this.state.editingFilterId,
|
||||
newFilterValue: this.state.editingFilterValue,
|
||||
});
|
||||
this.stopEditingFilter();
|
||||
}
|
||||
if (keyCodes.ESCAPE === keyCode) {
|
||||
this.stopEditingFilter();
|
||||
}
|
||||
};
|
||||
|
||||
getColumns() {
|
||||
const {
|
||||
deleteFilter,
|
||||
fieldWildcardMatcher,
|
||||
indexPattern,
|
||||
saveFilter,
|
||||
} = this.props;
|
||||
|
||||
return [
|
||||
{
|
||||
field: 'value',
|
||||
name: 'Filter',
|
||||
description: `Filter name`,
|
||||
dataType: 'string',
|
||||
sortable: true,
|
||||
render: (value, filter) => {
|
||||
if (this.state.editingFilterId === filter.clientId) {
|
||||
return (
|
||||
<EuiFieldText
|
||||
autoFocus
|
||||
value={this.state.editingFilterValue}
|
||||
onChange={this.onEditingFilterChange}
|
||||
onKeyDown={this.onEditFieldKeyDown}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <span>{value}</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'value',
|
||||
name: 'Matches',
|
||||
description: `Language used for the field`,
|
||||
dataType: 'string',
|
||||
sortable: true,
|
||||
render: (value, filter) => {
|
||||
const realtimeValue =
|
||||
this.state.editingFilterId === filter.clientId
|
||||
? this.state.editingFilterValue
|
||||
: value;
|
||||
const matcher = fieldWildcardMatcher([realtimeValue]);
|
||||
const matches = indexPattern
|
||||
.getNonScriptedFields()
|
||||
.map(f => f.name)
|
||||
.filter(matcher)
|
||||
.sort();
|
||||
if (matches.length) {
|
||||
return <span>{matches.join(', ')}</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<em>The source filter doesn't match any known fields.</em>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '',
|
||||
align: RIGHT_ALIGNMENT,
|
||||
width: '100',
|
||||
render: filter => {
|
||||
if (this.state.editingFilterId === filter.clientId) {
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiButtonIcon
|
||||
size="s"
|
||||
onClick={() => {
|
||||
saveFilter({
|
||||
filterId: this.state.editingFilterId,
|
||||
newFilterValue: this.state.editingFilterValue,
|
||||
});
|
||||
this.stopEditingFilter();
|
||||
}}
|
||||
iconType="checkInCircleFilled"
|
||||
aria-label="Save"
|
||||
/>
|
||||
<EuiButtonIcon
|
||||
size="s"
|
||||
onClick={() => {
|
||||
this.stopEditingFilter();
|
||||
}}
|
||||
iconType="cross"
|
||||
aria-label="Cancel"
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiButtonIcon
|
||||
size="s"
|
||||
color="danger"
|
||||
onClick={() => deleteFilter(filter)}
|
||||
iconType="trash"
|
||||
aria-label="Delete"
|
||||
/>
|
||||
<EuiButtonIcon
|
||||
size="s"
|
||||
onClick={() =>
|
||||
this.startEditingFilter(filter.clientId, filter.value)
|
||||
}
|
||||
iconType="pencil"
|
||||
aria-label="Edit"
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
render() {
|
||||
const { items, isSaving } = this.props;
|
||||
const columns = this.getColumns();
|
||||
const pagination = {
|
||||
pageSizeOptions: [5, 10, 25, 50],
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiInMemoryTable
|
||||
loading={isSaving}
|
||||
items={items}
|
||||
columns={columns}
|
||||
pagination={pagination}
|
||||
sorting={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
<div class="actions">
|
||||
<button
|
||||
aria-label="Edit"
|
||||
ng-if="sourceFilters.editing !== filter"
|
||||
ng-click="sourceFilters.editing = filter"
|
||||
type="button"
|
||||
class="kuiButton kuiButton--basic kuiButton--small"
|
||||
>
|
||||
<span aria-hidden="true" class="kuiIcon fa-pencil"></span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
aria-label="Save"
|
||||
ng-if="sourceFilters.editing === filter"
|
||||
ng-click="sourceFilters.save()"
|
||||
ng-disabled="!filter.value"
|
||||
type="button"
|
||||
class="kuiButton kuiButton--primary kuiButton--small"
|
||||
>
|
||||
<span aria-hidden="true" class="kuiIcon fa-save"></span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
aria-label="Delete"
|
||||
ng-click="sourceFilters.delete(filter)"
|
||||
type="button"
|
||||
class="kuiButton kuiButton--danger kuiButton--small"
|
||||
>
|
||||
<span aria-hidden="true" class="kuiIcon fa-trash"></span>
|
||||
</button>
|
||||
</div>
|
|
@ -1,12 +0,0 @@
|
|||
<div class="value">
|
||||
<span ng-if="sourceFilters.editing !== filter">{{ filter.value }}</span>
|
||||
|
||||
<input
|
||||
ng-model="filter.value"
|
||||
input-focus
|
||||
ng-if="sourceFilters.editing === filter"
|
||||
placeholder="{{ sourceFilters.placeHolder }}"
|
||||
type="text"
|
||||
required
|
||||
class="form-control">
|
||||
</div>
|
|
@ -1 +1 @@
|
|||
import './source_filters_table';
|
||||
export { SourceFiltersTable } from './source_filters_table';
|
||||
|
|
|
@ -1,56 +0,0 @@
|
|||
<h3 class="kuiTextTitle kuiVerticalRhythm">
|
||||
Source Filters
|
||||
</h3>
|
||||
|
||||
<p class="kuiText kuiVerticalRhythm">
|
||||
Source filters can be used to exclude one or more fields when fetching the document source. This happens when viewing a document in the Discover app, or with a table displaying results from a saved search in the Dashboard app. Each row is built using the source of a single document, and if you have documents with large or unimportant fields you may benefit from filtering those out at this lower level.
|
||||
</p>
|
||||
|
||||
<p class="kuiText kuiVerticalRhythm">
|
||||
Note that multi-fields will incorrectly appear as matches in the table below. These filters only actually apply to fields in the original source document, so matching multi-fields are not actually being filtered.
|
||||
</p>
|
||||
|
||||
<div
|
||||
ng-class="{ saving: sourceFilters.saving }"
|
||||
class="source-filters-container kuiVerticalRhythm"
|
||||
>
|
||||
<form
|
||||
name="form"
|
||||
ng-submit="sourceFilters.create()"
|
||||
class="kuiVerticalRhythm"
|
||||
>
|
||||
<div class="kuiFieldGroup">
|
||||
<div class="kuiFieldGroupSection kuiFieldGroupSection--wide">
|
||||
<input
|
||||
ng-model="sourceFilters.newValue"
|
||||
placeholder="{{ sourceFilters.placeHolder }}"
|
||||
type="text"
|
||||
class="kuiTextInput"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="kuiFieldGroupSection"
|
||||
aria-label="Source Filter Editor Controls"
|
||||
>
|
||||
<button
|
||||
type="submit"
|
||||
class="kuiButton kuiButton--primary"
|
||||
ng-disabled="!sourceFilters.newValue"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="kuiVerticalRhythm">
|
||||
<paginated-table
|
||||
columns="columns"
|
||||
rows="rows"
|
||||
link-to-top="true"
|
||||
per-page="perPage"
|
||||
show-blank-rows="false"
|
||||
></paginated-table>
|
||||
</div>
|
||||
</div>
|
|
@ -1,128 +1,181 @@
|
|||
import { find, each, escape, invoke, size, without } from 'lodash';
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
import { uiModules } from 'ui/modules';
|
||||
import { Notifier } from 'ui/notify';
|
||||
import { FieldWildcardProvider } from 'ui/field_wildcard';
|
||||
import {
|
||||
EuiSpacer,
|
||||
EuiOverlayMask,
|
||||
EuiConfirmModal,
|
||||
EUI_MODAL_CONFIRM_BUTTON,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import controlsHtml from './controls.html';
|
||||
import filterHtml from './filter.html';
|
||||
import template from './source_filters_table.html';
|
||||
import './source_filters_table.less';
|
||||
import { Table } from './components/table';
|
||||
import { Header } from './components/header';
|
||||
import { AddFilter } from './components/add_filter';
|
||||
|
||||
const notify = new Notifier();
|
||||
export class SourceFiltersTable extends Component {
|
||||
static propTypes = {
|
||||
indexPattern: PropTypes.object.isRequired,
|
||||
filterFilter: PropTypes.string,
|
||||
fieldWildcardMatcher: PropTypes.func.isRequired,
|
||||
onAddOrRemoveFilter: PropTypes.func,
|
||||
};
|
||||
|
||||
uiModules.get('kibana')
|
||||
.directive('sourceFiltersTable', function (Private, $filter, confirmModal) {
|
||||
const angularFilter = $filter('filter');
|
||||
const { fieldWildcardMatcher } = Private(FieldWildcardProvider);
|
||||
const rowScopes = []; // track row scopes, so they can be destroyed as needed
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
indexPattern: '='
|
||||
},
|
||||
template,
|
||||
controllerAs: 'sourceFilters',
|
||||
controller: class FieldFiltersController {
|
||||
constructor($scope) {
|
||||
if (!$scope.indexPattern) {
|
||||
throw new Error('index pattern is required');
|
||||
}
|
||||
// Source filters do not have any unique ids, only the value is stored.
|
||||
// To ensure we can create a consistent and expected UX when managing
|
||||
// source filters, we are assigning a unique id to each filter on the
|
||||
// client side only
|
||||
this.clientSideId = 0;
|
||||
|
||||
$scope.perPage = 25;
|
||||
$scope.columns = [
|
||||
{
|
||||
title: 'filter'
|
||||
},
|
||||
{
|
||||
title: 'matches',
|
||||
sortable: false,
|
||||
info: 'The source fields that match the filter.'
|
||||
},
|
||||
{
|
||||
title: 'controls',
|
||||
sortable: false
|
||||
}
|
||||
];
|
||||
|
||||
this.$scope = $scope;
|
||||
this.saving = false;
|
||||
this.editing = null;
|
||||
this.newValue = null;
|
||||
this.placeHolder = 'source filter, accepts wildcards (e.g., `user*` to filter fields starting with \'user\')';
|
||||
|
||||
$scope.$watchMulti([ '[]indexPattern.sourceFilters', '$parent.fieldFilter' ], () => {
|
||||
invoke(rowScopes, '$destroy');
|
||||
rowScopes.length = 0;
|
||||
|
||||
if ($scope.indexPattern.sourceFilters) {
|
||||
$scope.rows = [];
|
||||
each($scope.indexPattern.sourceFilters, (filter) => {
|
||||
const matcher = fieldWildcardMatcher([ filter.value ]);
|
||||
// compute which fields match a filter
|
||||
const matches = $scope.indexPattern.getNonScriptedFields().map(f => f.name).filter(matcher).sort();
|
||||
if ($scope.$parent.fieldFilter && !angularFilter(matches, $scope.$parent.fieldFilter).length) {
|
||||
return;
|
||||
}
|
||||
// compute the rows
|
||||
const rowScope = $scope.$new();
|
||||
rowScope.filter = filter;
|
||||
rowScopes.push(rowScope);
|
||||
$scope.rows.push([
|
||||
{
|
||||
markup: filterHtml,
|
||||
scope: rowScope
|
||||
},
|
||||
size(matches) ? escape(matches.join(', ')) : '<em>The source filter doesn\'t match any known fields.</em>',
|
||||
{
|
||||
markup: controlsHtml,
|
||||
scope: rowScope
|
||||
}
|
||||
]);
|
||||
});
|
||||
// Update the tab count
|
||||
find($scope.$parent.editSections, { index: 'sourceFilters' }).count = $scope.rows.length;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
all() {
|
||||
return this.$scope.indexPattern.sourceFilters || [];
|
||||
}
|
||||
|
||||
delete(filter) {
|
||||
const doDelete = () => {
|
||||
if (this.editing === filter) {
|
||||
this.editing = null;
|
||||
}
|
||||
|
||||
this.$scope.indexPattern.sourceFilters = without(this.all(), filter);
|
||||
return this.save();
|
||||
};
|
||||
|
||||
const confirmModalOptions = {
|
||||
confirmButtonText: 'Delete',
|
||||
onConfirm: doDelete,
|
||||
title: 'Delete source filter?'
|
||||
};
|
||||
confirmModal('', confirmModalOptions);
|
||||
}
|
||||
|
||||
create() {
|
||||
const value = this.newValue;
|
||||
this.newValue = null;
|
||||
this.$scope.indexPattern.sourceFilters = [...this.all(), { value }];
|
||||
return this.save();
|
||||
}
|
||||
|
||||
save() {
|
||||
this.saving = true;
|
||||
this.$scope.indexPattern.save()
|
||||
.then(() => this.editing = null)
|
||||
.catch(notify.error)
|
||||
.finally(() => this.saving = false);
|
||||
}
|
||||
}
|
||||
this.state = {
|
||||
filterToDelete: undefined,
|
||||
isDeleteConfirmationModalVisible: false,
|
||||
isSaving: false,
|
||||
filters: [],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.updateFilters();
|
||||
}
|
||||
|
||||
updateFilters = () => {
|
||||
const sourceFilters = this.props.indexPattern.sourceFilters || [];
|
||||
const filters = sourceFilters.map(filter => ({
|
||||
...filter,
|
||||
clientId: ++this.clientSideId,
|
||||
}));
|
||||
|
||||
this.setState({ filters });
|
||||
};
|
||||
|
||||
getFilteredFilters = createSelector(
|
||||
state => state.filters,
|
||||
(state, props) => props.filterFilter,
|
||||
(filters, filterFilter) => {
|
||||
if (filterFilter) {
|
||||
const filterFilterToLowercase = filterFilter.toLowerCase();
|
||||
return filters.filter(filter =>
|
||||
filter.value.toLowerCase().includes(filterFilterToLowercase)
|
||||
);
|
||||
}
|
||||
|
||||
return filters;
|
||||
}
|
||||
);
|
||||
|
||||
startDeleteFilter = filter => {
|
||||
this.setState({
|
||||
filterToDelete: filter,
|
||||
isDeleteConfirmationModalVisible: true,
|
||||
});
|
||||
};
|
||||
|
||||
hideDeleteConfirmationModal = () => {
|
||||
this.setState({
|
||||
filterToDelete: undefined,
|
||||
isDeleteConfirmationModalVisible: false,
|
||||
});
|
||||
};
|
||||
|
||||
deleteFilter = async () => {
|
||||
const { indexPattern, onAddOrRemoveFilter } = this.props;
|
||||
const { filterToDelete, filters } = this.state;
|
||||
|
||||
indexPattern.sourceFilters = filters.filter(filter => {
|
||||
return filter.clientId !== filterToDelete.clientId;
|
||||
});
|
||||
|
||||
this.setState({ isSaving: true });
|
||||
await indexPattern.save();
|
||||
onAddOrRemoveFilter && onAddOrRemoveFilter();
|
||||
this.updateFilters();
|
||||
this.setState({ isSaving: false });
|
||||
this.hideDeleteConfirmationModal();
|
||||
};
|
||||
|
||||
onAddFilter = async value => {
|
||||
const { indexPattern, onAddOrRemoveFilter } = this.props;
|
||||
|
||||
indexPattern.sourceFilters = [
|
||||
...(indexPattern.sourceFilters || []),
|
||||
{ value },
|
||||
];
|
||||
|
||||
this.setState({ isSaving: true });
|
||||
await indexPattern.save();
|
||||
onAddOrRemoveFilter && onAddOrRemoveFilter();
|
||||
this.updateFilters();
|
||||
this.setState({ isSaving: false });
|
||||
};
|
||||
|
||||
saveFilter = async ({ filterId, newFilterValue }) => {
|
||||
const { indexPattern } = this.props;
|
||||
const { filters } = this.state;
|
||||
|
||||
indexPattern.sourceFilters = filters.map(filter => {
|
||||
if (filter.clientId === filterId) {
|
||||
return {
|
||||
value: newFilterValue,
|
||||
clientId: filter.clientId,
|
||||
};
|
||||
}
|
||||
return filter;
|
||||
});
|
||||
|
||||
this.setState({ isSaving: true });
|
||||
await indexPattern.save();
|
||||
this.updateFilters();
|
||||
this.setState({ isSaving: false });
|
||||
};
|
||||
|
||||
renderDeleteConfirmationModal() {
|
||||
const { filterToDelete } = this.state;
|
||||
|
||||
if (!filterToDelete) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiOverlayMask>
|
||||
<EuiConfirmModal
|
||||
title={`Delete source filter '${filterToDelete.value}'?`}
|
||||
onCancel={this.hideDeleteConfirmationModal}
|
||||
onConfirm={this.deleteFilter}
|
||||
cancelButtonText="Cancel"
|
||||
confirmButtonText="Delete"
|
||||
defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON}
|
||||
/>
|
||||
</EuiOverlayMask>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { indexPattern, fieldWildcardMatcher } = this.props;
|
||||
|
||||
const { isSaving } = this.state;
|
||||
|
||||
const filteredFilters = this.getFilteredFilters(this.state, this.props);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Header />
|
||||
<AddFilter onAddFilter={this.onAddFilter} />
|
||||
<EuiSpacer size="l" />
|
||||
<Table
|
||||
isSaving={isSaving}
|
||||
indexPattern={indexPattern}
|
||||
items={filteredFilters}
|
||||
fieldWildcardMatcher={fieldWildcardMatcher}
|
||||
deleteFilter={this.startDeleteFilter}
|
||||
saveFilter={this.saveFilter}
|
||||
/>
|
||||
|
||||
{this.renderDeleteConfirmationModal()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
@import (reference) "~ui/styles/variables";
|
||||
|
||||
source-filters {
|
||||
.header {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.source-filters-container {
|
||||
margin-top: 15px;
|
||||
|
||||
&.saving {
|
||||
pointer-events: none;
|
||||
opacity: .4;
|
||||
transition: opacity 0.75s;
|
||||
}
|
||||
|
||||
.source-filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 10px 0;
|
||||
|
||||
.value {
|
||||
text-align: left;
|
||||
flex: 1 1 auto;
|
||||
padding-right: 5px;
|
||||
font-family: @font-family-monospace;
|
||||
|
||||
:not(input) {
|
||||
padding-left: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue