mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[ML] Adds page to ML Settings for viewing and editing filter lists (#20769)
* [ML] Add page to ML Settings for viewing and editing filter lists * [ML] Edits to Filter Lists Settings page following review
This commit is contained in:
parent
ac1a922124
commit
6939f5073c
36 changed files with 2132 additions and 20 deletions
8
x-pack/plugins/ml/public/components/items_grid/index.js
Normal file
8
x-pack/plugins/ml/public/components/items_grid/index.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
export { ItemsGrid } from './items_grid';
|
104
x-pack/plugins/ml/public/components/items_grid/items_grid.js
Normal file
104
x-pack/plugins/ml/public/components/items_grid/items_grid.js
Normal file
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
/*
|
||||
* React component for a paged grid of items.
|
||||
*/
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
EuiCheckbox,
|
||||
EuiFlexGrid,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiText
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { ItemsGridPagination } from './items_grid_pagination';
|
||||
|
||||
import './styles/main.less';
|
||||
|
||||
export function ItemsGrid({
|
||||
numberColumns,
|
||||
totalItemCount,
|
||||
items,
|
||||
selectedItems,
|
||||
itemsPerPage,
|
||||
itemsPerPageOptions,
|
||||
setItemsPerPage,
|
||||
setItemSelected,
|
||||
activePage,
|
||||
setActivePage }) {
|
||||
|
||||
if (items === undefined || items.length === 0) {
|
||||
return (
|
||||
<EuiFlexGroup justifyContent="spaceAround">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText>
|
||||
<h4>{(totalItemCount === 0) ? 'No items have been added' : 'No matching items'}</h4>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
const startIndex = activePage * itemsPerPage;
|
||||
const pageItems = items.slice(startIndex, startIndex + itemsPerPage);
|
||||
const gridItems = pageItems.map((item, index) => {
|
||||
return (
|
||||
<EuiFlexItem key={`ml_grid_item_${index}`}>
|
||||
<EuiCheckbox
|
||||
id={`ml_grid_item_${index}`}
|
||||
label={item}
|
||||
checked={(selectedItems.indexOf(item) >= 0)}
|
||||
onChange={(e) => { setItemSelected(item, e.target.checked); }}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<EuiFlexGrid
|
||||
columns={numberColumns}
|
||||
className="eui-textBreakWord"
|
||||
gutterSize="m"
|
||||
>
|
||||
{gridItems}
|
||||
</EuiFlexGrid>
|
||||
<ItemsGridPagination
|
||||
itemCount={items.length}
|
||||
itemsPerPage={itemsPerPage}
|
||||
itemsPerPageOptions={itemsPerPageOptions}
|
||||
setItemsPerPage={setItemsPerPage}
|
||||
activePage={activePage}
|
||||
setActivePage={setActivePage}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
ItemsGrid.propTypes = {
|
||||
numberColumns: PropTypes.oneOf([2, 3, 4]), // In line with EuiFlexGrid which supports 2, 3 or 4 columns.
|
||||
totalItemCount: PropTypes.number.isRequired,
|
||||
items: PropTypes.array,
|
||||
selectedItems: PropTypes.array,
|
||||
itemsPerPage: PropTypes.number,
|
||||
itemsPerPageOptions: PropTypes.arrayOf(PropTypes.number),
|
||||
setItemsPerPage: PropTypes.func.isRequired,
|
||||
setItemSelected: PropTypes.func.isRequired,
|
||||
activePage: PropTypes.number.isRequired,
|
||||
setActivePage: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
ItemsGrid.defaultProps = {
|
||||
numberColumns: 4,
|
||||
itemsPerPage: 50,
|
||||
itemsPerPageOptions: [50, 100, 500, 1000],
|
||||
};
|
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
/*
|
||||
* React component for the pagination controls of the items grid.
|
||||
*/
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React, {
|
||||
Component,
|
||||
} from 'react';
|
||||
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiContextMenuItem,
|
||||
EuiContextMenuPanel,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPagination,
|
||||
EuiPopover,
|
||||
} from '@elastic/eui';
|
||||
|
||||
|
||||
function getContextMenuItemIcon(menuItemSetting, itemsPerPage) {
|
||||
return (menuItemSetting === itemsPerPage) ? 'check' : 'empty';
|
||||
}
|
||||
|
||||
|
||||
export class ItemsGridPagination extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isPopoverOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
onButtonClick = () => {
|
||||
this.setState({
|
||||
isPopoverOpen: !this.state.isPopoverOpen,
|
||||
});
|
||||
}
|
||||
|
||||
closePopover = () => {
|
||||
this.setState({
|
||||
isPopoverOpen: false,
|
||||
});
|
||||
}
|
||||
|
||||
onPageClick = (pageNumber) => {
|
||||
this.props.setActivePage(pageNumber);
|
||||
}
|
||||
|
||||
onChangeItemsPerPage = (pageSize) => {
|
||||
this.closePopover();
|
||||
this.props.setItemsPerPage(pageSize);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
const {
|
||||
itemCount,
|
||||
itemsPerPage,
|
||||
itemsPerPageOptions,
|
||||
activePage } = this.props;
|
||||
|
||||
const button = (
|
||||
<EuiButtonEmpty
|
||||
size="s"
|
||||
color="text"
|
||||
iconType="arrowDown"
|
||||
iconSide="right"
|
||||
onClick={this.onButtonClick}
|
||||
>
|
||||
Items per page: {itemsPerPage}
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
|
||||
const pageCount = Math.ceil(itemCount / itemsPerPage);
|
||||
|
||||
const items = itemsPerPageOptions.map((pageSize) => {
|
||||
return (
|
||||
<EuiContextMenuItem
|
||||
key={`${pageSize} items`}
|
||||
icon={getContextMenuItemIcon(pageSize, itemsPerPage)}
|
||||
onClick={() => {this.onChangeItemsPerPage(pageSize);}}
|
||||
>
|
||||
{pageSize} items
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPopover
|
||||
id="customizablePagination"
|
||||
button={button}
|
||||
isOpen={this.state.isPopoverOpen}
|
||||
closePopover={this.closePopover}
|
||||
panelPaddingSize="none"
|
||||
>
|
||||
<EuiContextMenuPanel
|
||||
items={items}
|
||||
className="ml-items-grid-page-size-menu"
|
||||
/>
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPagination
|
||||
pageCount={pageCount}
|
||||
activePage={activePage}
|
||||
onPageClick={this.onPageClick}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
ItemsGridPagination.propTypes = {
|
||||
itemCount: PropTypes.number.isRequired,
|
||||
itemsPerPage: PropTypes.number.isRequired,
|
||||
itemsPerPageOptions: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
setItemsPerPage: PropTypes.func.isRequired,
|
||||
activePage: PropTypes.number.isRequired,
|
||||
setActivePage: PropTypes.func.isRequired,
|
||||
};
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
.ml-items-grid-page-size-menu {
|
||||
width: 140px;
|
||||
}
|
|
@ -50,6 +50,9 @@ module.directive('mlNavMenu', function () {
|
|||
calendars_list: { label: 'Calendar Management', url: '#/settings/calendars_list' },
|
||||
new_calendar: { label: 'New Calendar', url: '#/settings/calendars_list/new_calendar' },
|
||||
edit_calendar: { label: 'Edit Calendar', url: '#/settings/calendars_list/edit_calendar' },
|
||||
filter_lists: { label: 'Filter Lists', url: '#/settings/filter_lists' },
|
||||
new_filter_list: { label: 'New Filter List', url: '#/settings/filter_lists/new' },
|
||||
edit_filter_list: { label: 'Edit Filter List', url: '#/settings/filter_lists/edit' },
|
||||
};
|
||||
|
||||
const breadcrumbs = [{ label: 'Machine Learning', url: '#/' }];
|
||||
|
|
|
@ -22,6 +22,13 @@ export const filters = {
|
|||
});
|
||||
},
|
||||
|
||||
filtersStats() {
|
||||
return http({
|
||||
url: `${basePath}/filters/_stats`,
|
||||
method: 'GET'
|
||||
});
|
||||
},
|
||||
|
||||
addFilter(
|
||||
filterId,
|
||||
description,
|
||||
|
@ -41,7 +48,7 @@ export const filters = {
|
|||
filterId,
|
||||
description,
|
||||
addItems,
|
||||
deleteItems
|
||||
removeItems
|
||||
) {
|
||||
return http({
|
||||
url: `${basePath}/filters/${filterId}`,
|
||||
|
@ -49,7 +56,7 @@ export const filters = {
|
|||
data: {
|
||||
description,
|
||||
addItems,
|
||||
deleteItems
|
||||
removeItems
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
/*
|
||||
* React popover for adding items to a filter list.
|
||||
*/
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React, {
|
||||
Component,
|
||||
} from 'react';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiFlexItem,
|
||||
EuiFlexGroup,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiPopover,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTextArea
|
||||
} from '@elastic/eui';
|
||||
|
||||
|
||||
export class AddItemPopover extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isPopoverOpen: false,
|
||||
itemsText: ''
|
||||
};
|
||||
}
|
||||
|
||||
onItemsTextChange = (e) => {
|
||||
this.setState({
|
||||
itemsText: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
onButtonClick = () => {
|
||||
this.setState({
|
||||
isPopoverOpen: !this.state.isPopoverOpen,
|
||||
});
|
||||
}
|
||||
|
||||
closePopover = () => {
|
||||
this.setState({
|
||||
isPopoverOpen: false,
|
||||
});
|
||||
}
|
||||
|
||||
onAddButtonClick = () => {
|
||||
const items = this.state.itemsText.split('\n');
|
||||
const addItems = [];
|
||||
// Remove duplicates.
|
||||
items.forEach((item) => {
|
||||
if ((addItems.indexOf(item) === -1 && item.length > 0)) {
|
||||
addItems.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
this.props.addItems(addItems);
|
||||
this.setState({
|
||||
isPopoverOpen: false,
|
||||
itemsText: ''
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const button = (
|
||||
<EuiButton
|
||||
size="s"
|
||||
color="primary"
|
||||
iconType="arrowDown"
|
||||
iconSide="right"
|
||||
onClick={this.onButtonClick}
|
||||
>
|
||||
Add item
|
||||
</EuiButton>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<EuiPopover
|
||||
id="add_item_popover"
|
||||
panelClassName="ml-add-filter-item-popover"
|
||||
ownFocus
|
||||
button={button}
|
||||
isOpen={this.state.isPopoverOpen}
|
||||
closePopover={this.closePopover}
|
||||
>
|
||||
<EuiForm>
|
||||
<EuiFormRow
|
||||
label="Items"
|
||||
>
|
||||
<EuiTextArea
|
||||
value={this.state.itemsText}
|
||||
onChange={this.onItemsTextChange}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiForm>
|
||||
<EuiText size="xs">
|
||||
Enter one item per line
|
||||
</EuiText>
|
||||
<EuiSpacer size="s"/>
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
onClick={this.onAddButtonClick}
|
||||
disabled={(this.state.itemsText.length === 0)}
|
||||
>
|
||||
Add
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPopover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
AddItemPopover.propTypes = {
|
||||
addItems: PropTypes.func.isRequired
|
||||
};
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
export { AddItemPopover } from './add_item_popover';
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React, {
|
||||
Component,
|
||||
} from 'react';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiConfirmModal,
|
||||
EuiOverlayMask,
|
||||
EUI_MODAL_CONFIRM_BUTTON,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { deleteFilterLists } from './delete_filter_lists';
|
||||
|
||||
/*
|
||||
* React modal for confirming deletion of filter lists.
|
||||
*/
|
||||
export class DeleteFilterListModal extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isModalVisible: false
|
||||
};
|
||||
}
|
||||
|
||||
closeModal = () => {
|
||||
this.setState({ isModalVisible: false });
|
||||
}
|
||||
|
||||
showModal = () => {
|
||||
this.setState({ isModalVisible: true });
|
||||
}
|
||||
|
||||
onConfirmDelete = () => {
|
||||
this.doDelete();
|
||||
}
|
||||
|
||||
async doDelete() {
|
||||
const { selectedFilterLists, refreshFilterLists } = this.props;
|
||||
await deleteFilterLists(selectedFilterLists);
|
||||
|
||||
refreshFilterLists();
|
||||
this.closeModal();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { selectedFilterLists } = this.props;
|
||||
let modal;
|
||||
|
||||
if (this.state.isModalVisible) {
|
||||
const title = `Delete ${(selectedFilterLists.length > 1) ?
|
||||
`${selectedFilterLists.length} filter lists` : selectedFilterLists[0].filter_id}`;
|
||||
modal = (
|
||||
<EuiOverlayMask>
|
||||
<EuiConfirmModal
|
||||
title={title}
|
||||
className="eui-textBreakWord"
|
||||
onCancel={this.closeModal}
|
||||
onConfirm={this.onConfirmDelete}
|
||||
cancelButtonText="Cancel"
|
||||
confirmButtonText="Delete"
|
||||
buttonColor="danger"
|
||||
defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON}
|
||||
>
|
||||
<p>
|
||||
Are you sure you want to delete {(selectedFilterLists.length > 1) ?
|
||||
'these filter lists' : 'this filter list'}?
|
||||
</p>
|
||||
</EuiConfirmModal>
|
||||
</EuiOverlayMask>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<EuiButton
|
||||
key="delete_filter_list"
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
onClick={this.showModal}
|
||||
isDisabled={selectedFilterLists.length === 0}
|
||||
>
|
||||
Delete
|
||||
</EuiButton>
|
||||
|
||||
{modal}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
DeleteFilterListModal.propTypes = {
|
||||
selectedFilterLists: PropTypes.array.isRequired,
|
||||
};
|
||||
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import { ml } from 'plugins/ml/services/ml_api_service';
|
||||
|
||||
|
||||
export async function deleteFilterLists(filterListsToDelete) {
|
||||
if (filterListsToDelete === undefined || filterListsToDelete.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete each of the specified filter lists in turn, waiting for each response
|
||||
// before deleting the next to minimize load on the cluster.
|
||||
const messageId = `${(filterListsToDelete.length > 1) ?
|
||||
`${filterListsToDelete.length} filter lists` : filterListsToDelete[0].filter_id}`;
|
||||
toastNotifications.add(`Deleting ${messageId}`);
|
||||
|
||||
for(const filterList of filterListsToDelete) {
|
||||
const filterId = filterList.filter_id;
|
||||
try {
|
||||
await ml.filters.deleteFilter(filterId);
|
||||
} catch (resp) {
|
||||
console.log('Error deleting filter list:', resp);
|
||||
let errorMessage = `An error occurred deleting filter list ${filterList.filter_id}`;
|
||||
if (resp.message) {
|
||||
errorMessage += ` : ${resp.message}`;
|
||||
}
|
||||
toastNotifications.addDanger(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
toastNotifications.addSuccess(`${messageId} deleted`);
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
export { DeleteFilterListModal } from './delete_filter_list_modal';
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
/*
|
||||
* React popover for editing the description of a filter list.
|
||||
*/
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React, {
|
||||
Component,
|
||||
} from 'react';
|
||||
|
||||
import {
|
||||
EuiButtonIcon,
|
||||
EuiPopover,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiFieldText,
|
||||
} from '@elastic/eui';
|
||||
|
||||
|
||||
export class EditDescriptionPopover extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isPopoverOpen: false,
|
||||
value: props.description
|
||||
};
|
||||
}
|
||||
|
||||
onChange = (e) => {
|
||||
this.setState({
|
||||
value: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
onButtonClick = () => {
|
||||
if (this.state.isPopoverOpen === false) {
|
||||
this.setState({
|
||||
isPopoverOpen: !this.state.isPopoverOpen,
|
||||
value: this.props.description
|
||||
});
|
||||
} else {
|
||||
this.closePopover();
|
||||
}
|
||||
}
|
||||
|
||||
closePopover = () => {
|
||||
if (this.state.isPopoverOpen === true) {
|
||||
this.setState({
|
||||
isPopoverOpen: false,
|
||||
});
|
||||
this.props.updateDescription(this.state.value);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isPopoverOpen, value } = this.state;
|
||||
|
||||
const button = (
|
||||
<EuiButtonIcon
|
||||
size="s"
|
||||
color="primary"
|
||||
onClick={this.onButtonClick}
|
||||
iconType="pencil"
|
||||
aria-label="Edit description"
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<EuiPopover
|
||||
id="filter_list_description_popover"
|
||||
ownFocus
|
||||
button={button}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={this.closePopover}
|
||||
>
|
||||
<div style={{ width: '300px' }}>
|
||||
<EuiForm>
|
||||
<EuiFormRow
|
||||
label="Filter list description"
|
||||
>
|
||||
<EuiFieldText
|
||||
name="filter_list_description"
|
||||
value={value}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiForm>
|
||||
</div>
|
||||
</EuiPopover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
EditDescriptionPopover.propTypes = {
|
||||
description: PropTypes.string,
|
||||
updateDescription: PropTypes.func.isRequired
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
export { EditDescriptionPopover } from './edit_description_popover';
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
/*
|
||||
* React popover listing the jobs or detectors using a particular filter list in a custom rule.
|
||||
*/
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React, {
|
||||
Component,
|
||||
} from 'react';
|
||||
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiPopover,
|
||||
} from '@elastic/eui';
|
||||
|
||||
|
||||
export class FilterListUsagePopover extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isPopoverOpen: false,
|
||||
};
|
||||
}
|
||||
|
||||
onButtonClick = () => {
|
||||
this.setState({
|
||||
isPopoverOpen: !this.state.isPopoverOpen,
|
||||
});
|
||||
}
|
||||
|
||||
closePopover = () => {
|
||||
this.setState({
|
||||
isPopoverOpen: false,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
entityType,
|
||||
entityValues } = this.props;
|
||||
|
||||
const linkText = `${entityValues.length} ${entityType}${(entityValues.length !== 1) ? 's' : ''}`;
|
||||
|
||||
const listItems = entityValues.map(value => (<li key={value}>{value}</li>));
|
||||
|
||||
const button = (
|
||||
<EuiButtonEmpty
|
||||
size="s"
|
||||
color="primary"
|
||||
onClick={this.onButtonClick}
|
||||
>
|
||||
{linkText}
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<EuiPopover
|
||||
id={`${entityType}_filter_list_usage`}
|
||||
panelClassName="ml-filter-list-usage-popover"
|
||||
ownFocus
|
||||
button={button}
|
||||
isOpen={this.state.isPopoverOpen}
|
||||
closePopover={this.closePopover}
|
||||
>
|
||||
<ul>
|
||||
{listItems}
|
||||
</ul>
|
||||
</EuiPopover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
FilterListUsagePopover.propTypes = {
|
||||
entityType: PropTypes.oneOf(['job', 'detector']),
|
||||
entityValues: PropTypes.array.isRequired
|
||||
};
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
export { FilterListUsagePopover } from './filter_list_usage_popover';
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
import 'ngreact';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import { uiModules } from 'ui/modules';
|
||||
const module = uiModules.get('apps/ml', ['react']);
|
||||
|
||||
import { checkLicense } from 'plugins/ml/license/check_license';
|
||||
import { checkGetJobsPrivilege } from 'plugins/ml/privilege/check_privilege';
|
||||
import { getMlNodeCount } from 'plugins/ml/ml_nodes_check/check_ml_nodes';
|
||||
import { initPromise } from 'plugins/ml/util/promise';
|
||||
|
||||
import uiRoutes from 'ui/routes';
|
||||
|
||||
const template = `
|
||||
<ml-nav-menu name="settings"></ml-nav-menu>
|
||||
<div class="ml-filter-lists">
|
||||
<ml-edit-filter-list></ml-edit-filter-list>
|
||||
</div>
|
||||
`;
|
||||
|
||||
uiRoutes
|
||||
.when('/settings/filter_lists/new_filter_list', {
|
||||
template,
|
||||
resolve: {
|
||||
CheckLicense: checkLicense,
|
||||
privileges: checkGetJobsPrivilege,
|
||||
mlNodeCount: getMlNodeCount,
|
||||
initPromise: initPromise(false)
|
||||
}
|
||||
})
|
||||
.when('/settings/filter_lists/edit_filter_list/:filterId', {
|
||||
template,
|
||||
resolve: {
|
||||
CheckLicense: checkLicense,
|
||||
privileges: checkGetJobsPrivilege,
|
||||
mlNodeCount: getMlNodeCount,
|
||||
initPromise: initPromise(false)
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
import { EditFilterList } from './edit_filter_list';
|
||||
|
||||
module.directive('mlEditFilterList', function ($route) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
replace: false,
|
||||
scope: {},
|
||||
link: function (scope, element) {
|
||||
const props = {
|
||||
filterId: $route.current.params.filterId
|
||||
};
|
||||
|
||||
ReactDOM.render(
|
||||
React.createElement(EditFilterList, props),
|
||||
element[0]
|
||||
);
|
||||
}
|
||||
};
|
||||
});
|
|
@ -0,0 +1,337 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
/*
|
||||
* React component for viewing and editing a filter list, a list of items
|
||||
* used for example to safe list items via a job detector rule.
|
||||
*/
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React, {
|
||||
Component
|
||||
} from 'react';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPage,
|
||||
EuiPageContent,
|
||||
EuiSearchBar,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
|
||||
import { EditFilterListHeader } from './header';
|
||||
import { EditFilterListToolbar } from './toolbar';
|
||||
import { ItemsGrid } from 'plugins/ml/components/items_grid';
|
||||
import {
|
||||
isValidFilterListId,
|
||||
saveFilterList
|
||||
} from './utils';
|
||||
import { ml } from 'plugins/ml/services/ml_api_service';
|
||||
|
||||
const DEFAULT_ITEMS_PER_PAGE = 50;
|
||||
|
||||
// Returns the list of items that match the query entered in the EuiSearchBar.
|
||||
function getMatchingFilterItems(searchBarQuery, items) {
|
||||
if (searchBarQuery === undefined) {
|
||||
return [...items];
|
||||
}
|
||||
|
||||
// Convert the list of Strings into a list of Objects suitable for running through
|
||||
// the search bar query.
|
||||
const allItems = items.map(item => ({ value: item }));
|
||||
const matchingObjects =
|
||||
EuiSearchBar.Query.execute(searchBarQuery, allItems, { defaultFields: ['value'] });
|
||||
return matchingObjects.map(item => item.value);
|
||||
}
|
||||
|
||||
function getActivePage(activePageState, itemsPerPage, numMatchingItems) {
|
||||
// Checks if supplied active page number from state is applicable for the number
|
||||
// of matching items in the grid, and if not returns the last applicable page number.
|
||||
let activePage = activePageState;
|
||||
const activePageStartIndex = itemsPerPage * activePageState;
|
||||
if (activePageStartIndex > numMatchingItems) {
|
||||
activePage = Math.max((Math.ceil(numMatchingItems / itemsPerPage)) - 1, 0); // Sets to 0 for 0 matches.
|
||||
}
|
||||
return activePage;
|
||||
}
|
||||
|
||||
function returnToFiltersList() {
|
||||
window.location.href = `#/settings/filter_lists`;
|
||||
}
|
||||
|
||||
export class EditFilterList extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
description: '',
|
||||
items: [],
|
||||
matchingItems: [],
|
||||
selectedItems: [],
|
||||
loadedFilter: {},
|
||||
newFilterId: '',
|
||||
isNewFilterIdInvalid: true,
|
||||
activePage: 0,
|
||||
itemsPerPage: DEFAULT_ITEMS_PER_PAGE,
|
||||
saveInProgress: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const filterId = this.props.filterId;
|
||||
if (filterId !== undefined) {
|
||||
this.loadFilterList(filterId);
|
||||
} else {
|
||||
this.setState({ newFilterId: '' });
|
||||
}
|
||||
}
|
||||
|
||||
loadFilterList = (filterId) => {
|
||||
ml.filters.filters({ filterId })
|
||||
.then((filter) => {
|
||||
this.setLoadedFilterState(filter);
|
||||
})
|
||||
.catch((resp) => {
|
||||
console.log(`Error loading filter ${filterId}:`, resp);
|
||||
toastNotifications.addDanger(`An error occurred loading details of filter ${filterId}`);
|
||||
});
|
||||
}
|
||||
|
||||
setLoadedFilterState = (loadedFilter) => {
|
||||
// Store the loaded filter so we can diff changes to the items when saving updates.
|
||||
this.setState((prevState) => {
|
||||
const { itemsPerPage, searchQuery } = prevState;
|
||||
|
||||
const matchingItems = getMatchingFilterItems(searchQuery, loadedFilter.items);
|
||||
const activePage = getActivePage(prevState.activePage, itemsPerPage, matchingItems.length);
|
||||
|
||||
return {
|
||||
description: loadedFilter.description,
|
||||
items: [...loadedFilter.items],
|
||||
matchingItems,
|
||||
selectedItems: [],
|
||||
loadedFilter,
|
||||
isNewFilterIdInvalid: false,
|
||||
activePage,
|
||||
searchQuery,
|
||||
saveInProgress: false
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
updateNewFilterId = (newFilterId) => {
|
||||
this.setState({
|
||||
newFilterId,
|
||||
isNewFilterIdInvalid: !isValidFilterListId(newFilterId)
|
||||
});
|
||||
}
|
||||
|
||||
updateDescription = (description) => {
|
||||
this.setState({ description });
|
||||
}
|
||||
|
||||
addItems = (itemsToAdd) => {
|
||||
this.setState((prevState) => {
|
||||
const { itemsPerPage, searchQuery } = prevState;
|
||||
const items = [...prevState.items];
|
||||
const alreadyInFilter = [];
|
||||
itemsToAdd.forEach((item) => {
|
||||
if (items.indexOf(item) === -1) {
|
||||
items.push(item);
|
||||
} else {
|
||||
alreadyInFilter.push(item);
|
||||
}
|
||||
});
|
||||
items.sort((str1, str2) => {
|
||||
return str1.localeCompare(str2);
|
||||
});
|
||||
|
||||
if (alreadyInFilter.length > 0) {
|
||||
toastNotifications.addWarning(`The following items were already in the filter list: ${alreadyInFilter}`);
|
||||
}
|
||||
|
||||
const matchingItems = getMatchingFilterItems(searchQuery, items);
|
||||
const activePage = getActivePage(prevState.activePage, itemsPerPage, matchingItems.length);
|
||||
|
||||
return {
|
||||
items,
|
||||
matchingItems,
|
||||
activePage,
|
||||
searchQuery
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
deleteSelectedItems = () => {
|
||||
this.setState((prevState) => {
|
||||
const { selectedItems, itemsPerPage, searchQuery } = prevState;
|
||||
const items = [...prevState.items];
|
||||
selectedItems.forEach((item) => {
|
||||
const index = items.indexOf(item);
|
||||
if (index !== -1) {
|
||||
items.splice(index, 1);
|
||||
}
|
||||
});
|
||||
|
||||
const matchingItems = getMatchingFilterItems(searchQuery, items);
|
||||
const activePage = getActivePage(prevState.activePage, itemsPerPage, matchingItems.length);
|
||||
|
||||
return {
|
||||
items,
|
||||
matchingItems,
|
||||
selectedItems: [],
|
||||
activePage,
|
||||
searchQuery
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
onSearchChange = ({ query }) => {
|
||||
this.setState((prevState) => {
|
||||
const { items, itemsPerPage } = prevState;
|
||||
|
||||
const matchingItems = getMatchingFilterItems(query, items);
|
||||
const activePage = getActivePage(prevState.activePage, itemsPerPage, matchingItems.length);
|
||||
|
||||
return {
|
||||
matchingItems,
|
||||
activePage,
|
||||
searchQuery: query
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
setItemSelected = (item, isSelected) => {
|
||||
this.setState((prevState) => {
|
||||
const selectedItems = [...prevState.selectedItems];
|
||||
const index = selectedItems.indexOf(item);
|
||||
if (isSelected === true && index === -1) {
|
||||
selectedItems.push(item);
|
||||
} else if (isSelected === false && index !== -1) {
|
||||
selectedItems.splice(index, 1);
|
||||
}
|
||||
|
||||
return {
|
||||
selectedItems
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
setActivePage = (activePage) => {
|
||||
this.setState({ activePage });
|
||||
}
|
||||
|
||||
setItemsPerPage = (itemsPerPage) => {
|
||||
this.setState({
|
||||
itemsPerPage,
|
||||
activePage: 0
|
||||
});
|
||||
}
|
||||
|
||||
save = () => {
|
||||
this.setState({ saveInProgress: true });
|
||||
|
||||
const { loadedFilter, newFilterId, description, items } = this.state;
|
||||
const filterId = (this.props.filterId !== undefined) ? this.props.filterId : newFilterId;
|
||||
saveFilterList(
|
||||
filterId,
|
||||
description,
|
||||
items,
|
||||
loadedFilter
|
||||
)
|
||||
.then((savedFilter) => {
|
||||
this.setLoadedFilterState(savedFilter);
|
||||
returnToFiltersList();
|
||||
})
|
||||
.catch((resp) => {
|
||||
console.log(`Error saving filter ${filterId}:`, resp);
|
||||
toastNotifications.addDanger(`An error occurred saving filter ${filterId}`);
|
||||
this.setState({ saveInProgress: false });
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
loadedFilter,
|
||||
newFilterId,
|
||||
isNewFilterIdInvalid,
|
||||
description,
|
||||
items,
|
||||
matchingItems,
|
||||
selectedItems,
|
||||
itemsPerPage,
|
||||
activePage,
|
||||
saveInProgress } = this.state;
|
||||
|
||||
const totalItemCount = (items !== undefined) ? items.length : 0;
|
||||
|
||||
return (
|
||||
<EuiPage className="ml-edit-filter-lists">
|
||||
<EuiPageContent
|
||||
className="ml-edit-filter-lists-content"
|
||||
verticalPosition="center"
|
||||
horizontalPosition="center"
|
||||
>
|
||||
<EditFilterListHeader
|
||||
filterId={this.props.filterId}
|
||||
newFilterId={newFilterId}
|
||||
isNewFilterIdInvalid={isNewFilterIdInvalid}
|
||||
updateNewFilterId={this.updateNewFilterId}
|
||||
description={description}
|
||||
updateDescription={this.updateDescription}
|
||||
totalItemCount={totalItemCount}
|
||||
usedBy={loadedFilter.used_by}
|
||||
/>
|
||||
<EditFilterListToolbar
|
||||
onSearchChange={this.onSearchChange}
|
||||
addItems={this.addItems}
|
||||
deleteSelectedItems={this.deleteSelectedItems}
|
||||
selectedItemCount={selectedItems.length}
|
||||
/>
|
||||
<EuiSpacer size="xl" />
|
||||
<ItemsGrid
|
||||
totalItemCount={totalItemCount}
|
||||
items={matchingItems}
|
||||
selectedItems={selectedItems}
|
||||
itemsPerPage={itemsPerPage}
|
||||
setItemsPerPage={this.setItemsPerPage}
|
||||
setItemSelected={this.setItemSelected}
|
||||
activePage={activePage}
|
||||
setActivePage={this.setActivePage}
|
||||
/>
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
onClick={returnToFiltersList}
|
||||
>
|
||||
Cancel
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
onClick={this.save}
|
||||
disabled={(saveInProgress === true) || (isNewFilterIdInvalid === true)}
|
||||
fill
|
||||
>
|
||||
Save
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPageContent>
|
||||
</EuiPage>
|
||||
);
|
||||
}
|
||||
}
|
||||
EditFilterList.propTypes = {
|
||||
filterId: PropTypes.string
|
||||
};
|
||||
|
170
x-pack/plugins/ml/public/settings/filter_lists/edit/header.js
Normal file
170
x-pack/plugins/ml/public/settings/filter_lists/edit/header.js
Normal file
|
@ -0,0 +1,170 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
/*
|
||||
* React component for the header section of the edit filter list page, showing the
|
||||
* filter ID, description, number of items, and the jobs and detectors using the filter list.
|
||||
*/
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
EuiFieldText,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTextColor,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { EditDescriptionPopover } from '../components/edit_description_popover';
|
||||
import { FilterListUsagePopover } from '../components/filter_list_usage_popover';
|
||||
|
||||
export function EditFilterListHeader({
|
||||
filterId,
|
||||
totalItemCount,
|
||||
description,
|
||||
updateDescription,
|
||||
newFilterId,
|
||||
isNewFilterIdInvalid,
|
||||
updateNewFilterId,
|
||||
usedBy }) {
|
||||
|
||||
const title = (filterId !== undefined) ? `Filter list ${filterId}` : 'Create new filter list';
|
||||
|
||||
let idField;
|
||||
let descriptionField;
|
||||
let usedByElement;
|
||||
|
||||
if (filterId === undefined) {
|
||||
const msg = 'Use lowercase alphanumerics (a-z and 0-9), hyphens or underscores;' +
|
||||
' must start and end with an alphanumeric character';
|
||||
const helpText = (isNewFilterIdInvalid === false) ? msg : undefined;
|
||||
const error = (isNewFilterIdInvalid === true) ? [msg] : undefined;
|
||||
|
||||
idField = (
|
||||
<EuiFormRow
|
||||
label="Filter list ID"
|
||||
helpText={helpText}
|
||||
error={error}
|
||||
isInvalid={isNewFilterIdInvalid}
|
||||
>
|
||||
<EuiFieldText
|
||||
name="new_filter_id"
|
||||
value={newFilterId}
|
||||
isInvalid={isNewFilterIdInvalid}
|
||||
onChange={(e) => updateNewFilterId(e.target.value)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
||||
|
||||
if (description !== undefined && description.length > 0) {
|
||||
descriptionField = (
|
||||
<EuiText>
|
||||
<p>
|
||||
{description}
|
||||
</p>
|
||||
</EuiText>
|
||||
);
|
||||
} else {
|
||||
descriptionField = (
|
||||
<EuiText>
|
||||
<EuiTextColor color="subdued">
|
||||
Add a description
|
||||
</EuiTextColor>
|
||||
</EuiText>
|
||||
);
|
||||
}
|
||||
|
||||
if (filterId !== undefined) {
|
||||
if (usedBy !== undefined && usedBy.jobs.length > 0) {
|
||||
usedByElement = (
|
||||
<React.Fragment>
|
||||
<div className="ml-filter-list-usage">
|
||||
<EuiText>
|
||||
This filter list is used in
|
||||
</EuiText>
|
||||
<FilterListUsagePopover
|
||||
entityType="detector"
|
||||
entityValues={usedBy.detectors}
|
||||
/>
|
||||
<EuiText>
|
||||
across
|
||||
</EuiText>
|
||||
<FilterListUsagePopover
|
||||
entityType="job"
|
||||
entityValues={usedBy.jobs}
|
||||
/>
|
||||
</div>
|
||||
<EuiSpacer size="s"/>
|
||||
</React.Fragment>
|
||||
);
|
||||
} else {
|
||||
usedByElement = (
|
||||
<React.Fragment>
|
||||
<EuiText>
|
||||
<p>
|
||||
This filter list is not being used by any jobs.
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiSpacer size="s"/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="baseline">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup alignItems="baseline" gutterSize="m" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle>
|
||||
<h1>{title}</h1>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiTextColor color="subdued">
|
||||
<p>{totalItemCount} {(totalItemCount !== 1) ? 'items' : 'item'} in total</p>
|
||||
</EuiTextColor>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m"/>
|
||||
{idField}
|
||||
<EuiFlexGroup alignItems="baseline" gutterSize="s" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
{descriptionField}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EditDescriptionPopover
|
||||
description={description}
|
||||
updateDescription={updateDescription}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="s"/>
|
||||
{usedByElement}
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
}
|
||||
EditFilterListHeader.propTypes = {
|
||||
filterId: PropTypes.string,
|
||||
newFilterId: PropTypes.string,
|
||||
isNewFilterIdInvalid: PropTypes.bool,
|
||||
updateNewFilterId: PropTypes.func.isRequired,
|
||||
totalItemCount: PropTypes.number.isRequired,
|
||||
description: PropTypes.string,
|
||||
updateDescription: PropTypes.func.isRequired,
|
||||
usedBy: PropTypes.object
|
||||
};
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
import './directive';
|
||||
import './styles/main.less';
|
|
@ -0,0 +1,34 @@
|
|||
.ml-edit-filter-lists {
|
||||
.ml-edit-filter-lists-content {
|
||||
max-width: 1100px;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.ml-filter-list-usage > div {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.ml-filter-list-usage {
|
||||
.euiButtonEmpty.euiButtonEmpty--small {
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
.euiButtonEmpty .euiButtonEmpty__content {
|
||||
padding: 0px 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ml-add-filter-item-popover {
|
||||
.euiFormRow {
|
||||
width: 300px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.ml-filter-list-usage-popover {
|
||||
li {
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
/*
|
||||
* React component for the toolbar section of the edit filter list page,
|
||||
* holding a search bar,, and buttons for adding and deleting items from the list.
|
||||
*/
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSearchBar,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { AddItemPopover } from '../components/add_item_popover';
|
||||
|
||||
export function EditFilterListToolbar({
|
||||
onSearchChange,
|
||||
addItems,
|
||||
deleteSelectedItems,
|
||||
selectedItemCount }) {
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<AddItemPopover
|
||||
addItems={addItems}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="xl">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
color="danger"
|
||||
disabled={(selectedItemCount === 0)}
|
||||
onClick={deleteSelectedItems}
|
||||
>
|
||||
Delete item
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiSearchBar
|
||||
onChange={onSearchChange}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
</EuiFlexGroup>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
EditFilterListToolbar.propTypes = {
|
||||
onSearchChange: PropTypes.func.isRequired,
|
||||
addItems: PropTypes.func.isRequired,
|
||||
deleteSelectedItems: PropTypes.func.isRequired,
|
||||
selectedItemCount: PropTypes.number.isRequired
|
||||
};
|
105
x-pack/plugins/ml/public/settings/filter_lists/edit/utils.js
Normal file
105
x-pack/plugins/ml/public/settings/filter_lists/edit/utils.js
Normal file
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import { isJobIdValid } from 'plugins/ml/../common/util/job_utils';
|
||||
import { ml } from 'plugins/ml/services/ml_api_service';
|
||||
|
||||
export function isValidFilterListId(id) {
|
||||
// Filter List ID requires the same format as a Job ID, therefore isJobIdValid can be used
|
||||
return (id !== undefined) && (id.length > 0) && isJobIdValid(id);
|
||||
}
|
||||
|
||||
|
||||
// Saves a filter list, running an update if the supplied loadedFilterList, holding the
|
||||
// original filter list to which edits are being applied, is defined with a filter_id property.
|
||||
export function saveFilterList(filterId, description, items, loadedFilterList) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (loadedFilterList === undefined || loadedFilterList.filter_id === undefined) {
|
||||
// Create a new filter.
|
||||
addFilterList(filterId,
|
||||
description,
|
||||
items
|
||||
)
|
||||
.then((newFilter) => {
|
||||
resolve(newFilter);
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
} else {
|
||||
// Edit to existing filter.
|
||||
updateFilterList(
|
||||
loadedFilterList,
|
||||
description,
|
||||
items)
|
||||
.then((updatedFilter) => {
|
||||
resolve(updatedFilter);
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function addFilterList(filterId, description, items) {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
// First check the filterId isn't already in use by loading the current list of filters.
|
||||
ml.filters.filtersStats()
|
||||
.then((filterLists) => {
|
||||
const savedFilterIds = filterLists.map(filterList => filterList.filter_id);
|
||||
if (savedFilterIds.indexOf(filterId) === -1) {
|
||||
// Save the new filter.
|
||||
ml.filters.addFilter(
|
||||
filterId,
|
||||
description,
|
||||
items
|
||||
)
|
||||
.then((newFilter) => {
|
||||
resolve(newFilter);
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
} else {
|
||||
toastNotifications.addDanger(`A filter with id ${filterId} already exists`);
|
||||
reject(new Error(`A filter with id ${filterId} already exists`));
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
export function updateFilterList(loadedFilterList, description, items) {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
// Get items added and removed from loaded filter.
|
||||
const loadedItems = loadedFilterList.items;
|
||||
const addItems = items.filter(item => (loadedItems.includes(item) === false));
|
||||
const removeItems = loadedItems.filter(item => (items.includes(item) === false));
|
||||
|
||||
ml.filters.updateFilter(
|
||||
loadedFilterList.filter_id,
|
||||
description,
|
||||
addItems,
|
||||
removeItems
|
||||
)
|
||||
.then((updatedFilter) => {
|
||||
resolve(updatedFilter);
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
10
x-pack/plugins/ml/public/settings/filter_lists/index.js
Normal file
10
x-pack/plugins/ml/public/settings/filter_lists/index.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
import './edit';
|
||||
import './list';
|
||||
import './styles/main.less';
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
import 'ngreact';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import { uiModules } from 'ui/modules';
|
||||
const module = uiModules.get('apps/ml', ['react']);
|
||||
|
||||
import { checkLicense } from 'plugins/ml/license/check_license';
|
||||
import { checkGetJobsPrivilege } from 'plugins/ml/privilege/check_privilege';
|
||||
import { getMlNodeCount } from 'plugins/ml/ml_nodes_check/check_ml_nodes';
|
||||
import { initPromise } from 'plugins/ml/util/promise';
|
||||
|
||||
import uiRoutes from 'ui/routes';
|
||||
|
||||
const template = `
|
||||
<ml-nav-menu name="settings"></ml-nav-menu>
|
||||
<div class="ml-filter-lists">
|
||||
<ml-filter-lists></ml-filter-lists>
|
||||
</div>
|
||||
`;
|
||||
|
||||
uiRoutes
|
||||
.when('/settings/filter_lists', {
|
||||
template,
|
||||
resolve: {
|
||||
CheckLicense: checkLicense,
|
||||
privileges: checkGetJobsPrivilege,
|
||||
mlNodeCount: getMlNodeCount,
|
||||
initPromise: initPromise(false)
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
import { FilterLists } from './filter_lists';
|
||||
|
||||
module.directive('mlFilterLists', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
replace: false,
|
||||
scope: {},
|
||||
link: function (scope, element) {
|
||||
ReactDOM.render(
|
||||
React.createElement(FilterLists),
|
||||
element[0]
|
||||
);
|
||||
}
|
||||
};
|
||||
});
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
/*
|
||||
* React table for displaying a table of filter lists.
|
||||
*/
|
||||
|
||||
import React, {
|
||||
Component
|
||||
} from 'react';
|
||||
|
||||
import {
|
||||
EuiPage,
|
||||
EuiPageContent,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
|
||||
import { FilterListsHeader } from './header';
|
||||
import { FilterListsTable } from './table';
|
||||
import { ml } from 'plugins/ml/services/ml_api_service';
|
||||
|
||||
|
||||
export class FilterLists extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
filterLists: [],
|
||||
selectedFilterLists: []
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.refreshFilterLists();
|
||||
}
|
||||
|
||||
setSelectedFilterLists = (selectedFilterLists) => {
|
||||
this.setState({ selectedFilterLists });
|
||||
}
|
||||
|
||||
refreshFilterLists = () => {
|
||||
// Load the list of filters.
|
||||
ml.filters.filtersStats()
|
||||
.then((filterLists) => {
|
||||
// Check selected filter lists still exist.
|
||||
this.setState((prevState) => {
|
||||
const loadedFilterIds = filterLists.map(filterList => filterList.filter_id);
|
||||
const selectedFilterLists = prevState.selectedFilterLists.filter((filterList) => {
|
||||
return (loadedFilterIds.indexOf(filterList.filter_id) !== -1);
|
||||
});
|
||||
|
||||
return {
|
||||
filterLists,
|
||||
selectedFilterLists
|
||||
};
|
||||
});
|
||||
})
|
||||
.catch((resp) => {
|
||||
console.log('Error loading list of filters:', resp);
|
||||
toastNotifications.addDanger('An error occurred loading the filter lists');
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { filterLists, selectedFilterLists } = this.state;
|
||||
|
||||
return (
|
||||
<EuiPage className="ml-list-filter-lists">
|
||||
<EuiPageContent
|
||||
className="ml-list-filter-lists-content"
|
||||
verticalPosition="center"
|
||||
horizontalPosition="center"
|
||||
>
|
||||
<FilterListsHeader
|
||||
totalCount={filterLists.length}
|
||||
refreshFilterLists={this.refreshFilterLists}
|
||||
/>
|
||||
<FilterListsTable
|
||||
filterLists={filterLists}
|
||||
selectedFilterLists={selectedFilterLists}
|
||||
setSelectedFilterLists={this.setSelectedFilterLists}
|
||||
refreshFilterLists={this.refreshFilterLists}
|
||||
/>
|
||||
</EuiPageContent>
|
||||
</EuiPage>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
/*
|
||||
* React component for the header section of the filter lists page.
|
||||
*/
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiText,
|
||||
EuiTextColor,
|
||||
EuiButtonEmpty,
|
||||
} from '@elastic/eui';
|
||||
|
||||
export function FilterListsHeader({ totalCount, refreshFilterLists }) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="baseline">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup alignItems="baseline" gutterSize="m" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle>
|
||||
<h1>Filter Lists</h1>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiTextColor color="subdued">
|
||||
<p>{totalCount} in total</p>
|
||||
</EuiTextColor>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup alignItems="baseline" gutterSize="m" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
size="s"
|
||||
iconType="refresh"
|
||||
onClick={() => refreshFilterLists()}
|
||||
>
|
||||
Refresh
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m"/>
|
||||
<EuiText>
|
||||
<p>
|
||||
<EuiTextColor color="subdued">
|
||||
From here you can create and edit filter lists for use in detector rules for scoping whether the rule should
|
||||
apply to a known set of values.
|
||||
</EuiTextColor>
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiSpacer size="m"/>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
}
|
||||
FilterListsHeader.propTypes = {
|
||||
totalCount: PropTypes.number.isRequired,
|
||||
refreshFilterLists: PropTypes.func.isRequired
|
||||
};
|
10
x-pack/plugins/ml/public/settings/filter_lists/list/index.js
Normal file
10
x-pack/plugins/ml/public/settings/filter_lists/list/index.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import './directive';
|
||||
import './styles/main.less';
|
|
@ -0,0 +1,16 @@
|
|||
.ml-list-filter-lists {
|
||||
|
||||
.ml-list-filter-lists-content {
|
||||
max-width: 1100px;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.ml-filter-lists-table {
|
||||
th:last-child, td:last-child {
|
||||
width: 70px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
181
x-pack/plugins/ml/public/settings/filter_lists/list/table.js
Normal file
181
x-pack/plugins/ml/public/settings/filter_lists/list/table.js
Normal file
|
@ -0,0 +1,181 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
/*
|
||||
* React table for displaying a table of filter lists.
|
||||
*/
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
EuiInMemoryTable,
|
||||
EuiLink,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import chrome from 'ui/chrome';
|
||||
import { DeleteFilterListModal } from '../components/delete_filter_list_modal';
|
||||
|
||||
|
||||
function UsedByIcon({ usedBy }) {
|
||||
// Renders a tick or cross in the 'usedBy' column to indicate whether
|
||||
// the filter list is in use in a detectors in any jobs.
|
||||
let icon;
|
||||
if (usedBy !== undefined && usedBy.jobs.length > 0) {
|
||||
icon = <EuiIcon type="check" aria-label="In use"/>;
|
||||
} else {
|
||||
icon = <EuiIcon type="cross" aria-label="Not in use"/>;
|
||||
}
|
||||
|
||||
return icon;
|
||||
}
|
||||
UsedByIcon.propTypes = {
|
||||
usedBy: PropTypes.object
|
||||
};
|
||||
|
||||
function NewFilterButton() {
|
||||
return (
|
||||
<EuiButton
|
||||
key="new_filter_list"
|
||||
href={`${chrome.getBasePath()}/app/ml#/settings/filter_lists/new_filter_list`}
|
||||
>
|
||||
New
|
||||
</EuiButton>
|
||||
);
|
||||
}
|
||||
|
||||
function getColumns() {
|
||||
|
||||
const columns = [
|
||||
{
|
||||
field: 'filter_id',
|
||||
name: 'ID',
|
||||
render: (id) => (
|
||||
<EuiLink href={`${chrome.getBasePath()}/app/ml#/settings/filter_lists/edit_filter_list/${id}`} >
|
||||
{id}
|
||||
</EuiLink>
|
||||
),
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
name: 'Description',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
field: 'item_count',
|
||||
name: 'Item count',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
field: 'used_by',
|
||||
name: 'In use',
|
||||
render: (usedBy) => (
|
||||
<UsedByIcon
|
||||
usedBy={usedBy}
|
||||
/>
|
||||
),
|
||||
sortable: true
|
||||
}
|
||||
];
|
||||
|
||||
return columns;
|
||||
}
|
||||
|
||||
function renderToolsRight(selectedFilterLists, refreshFilterLists) {
|
||||
return [
|
||||
(
|
||||
<NewFilterButton
|
||||
key="new_filter_list"
|
||||
/>
|
||||
),
|
||||
(
|
||||
<DeleteFilterListModal
|
||||
selectedFilterLists={selectedFilterLists}
|
||||
refreshFilterLists={refreshFilterLists}
|
||||
/>
|
||||
)];
|
||||
}
|
||||
|
||||
|
||||
export function FilterListsTable({
|
||||
filterLists,
|
||||
selectedFilterLists,
|
||||
setSelectedFilterLists,
|
||||
refreshFilterLists
|
||||
}) {
|
||||
|
||||
const sorting = {
|
||||
sort: {
|
||||
field: 'filter_id',
|
||||
direction: 'asc',
|
||||
}
|
||||
};
|
||||
|
||||
const search = {
|
||||
toolsRight: renderToolsRight(selectedFilterLists, refreshFilterLists),
|
||||
box: {
|
||||
incremental: true,
|
||||
},
|
||||
filters: []
|
||||
};
|
||||
|
||||
const tableSelection = {
|
||||
selectable: (filterList) => (filterList.used_by === undefined || filterList.used_by.jobs.length === 0),
|
||||
selectableMessage: () => undefined,
|
||||
onSelectionChange: (selection) => setSelectedFilterLists(selection)
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{filterLists === undefined || filterLists.length === 0 ? (
|
||||
<React.Fragment>
|
||||
<EuiFlexGroup alignItems="flexEnd" justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<NewFilterButton />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiFlexGroup justifyContent="spaceAround">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText>
|
||||
<h4>No filters have been created</h4>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
<EuiInMemoryTable
|
||||
className="ml-filter-lists-table"
|
||||
items={filterLists}
|
||||
itemId="filter_id"
|
||||
columns={getColumns()}
|
||||
search={search}
|
||||
pagination={true}
|
||||
sorting={sorting}
|
||||
selection={tableSelection}
|
||||
isSelectable={true}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
}
|
||||
FilterListsTable.propTypes = {
|
||||
filterLists: PropTypes.array,
|
||||
selectedFilterLists: PropTypes.array,
|
||||
setSelectedFilterLists: PropTypes.func.isRequired,
|
||||
refreshFilterLists: PropTypes.func.isRequired
|
||||
};
|
|
@ -0,0 +1,4 @@
|
|||
.ml-filter-lists {
|
||||
background: #F5F5F5;
|
||||
min-height: 100vh;
|
||||
}
|
|
@ -9,3 +9,4 @@
|
|||
import './styles/main.less';
|
||||
import './settings_controller';
|
||||
import './scheduled_events';
|
||||
import './filter_lists';
|
||||
|
|
|
@ -28,9 +28,22 @@
|
|||
Calendar management
|
||||
</a>
|
||||
</li>
|
||||
<li class="col-xs-4 col-md-3 ng-scope">
|
||||
<a
|
||||
data-test-subj=""
|
||||
class="management-panel__link ng-binding"
|
||||
tooltip=""
|
||||
tooltip-placement="bottom"
|
||||
tooltip-popup-delay="400"
|
||||
tooltip-append-to-body="1"
|
||||
href="ml#/settings/filter_lists">
|
||||
Filter Lists
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</ml-settings>
|
||||
|
|
|
@ -5,3 +5,13 @@
|
|||
color: #999;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
ml-settings {
|
||||
.management-panel .management-panel__link {
|
||||
font-size: 17px;
|
||||
line-height: 32px;
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -537,7 +537,7 @@ export const elasticsearchJsPlugin = (Client, config, components) => {
|
|||
{
|
||||
fmt: '/_xpack/ml/filters/<%=filterId%>/_update',
|
||||
req: {
|
||||
jobId: {
|
||||
filterId: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,10 +15,21 @@ export class FilterManager {
|
|||
|
||||
async getFilter(filterId) {
|
||||
try {
|
||||
const resp = await this.callWithRequest('ml.filters', { filterId });
|
||||
const filters = resp.filters;
|
||||
if (filters.length) {
|
||||
return filters[0];
|
||||
const [ JOBS, FILTERS ] = [0, 1];
|
||||
const results = await Promise.all([
|
||||
this.callWithRequest('ml.jobs'),
|
||||
this.callWithRequest('ml.filters', { filterId })
|
||||
]);
|
||||
|
||||
if (results[FILTERS] && results[FILTERS].filters.length) {
|
||||
let filtersInUse = {};
|
||||
if (results[JOBS] && results[JOBS].jobs) {
|
||||
filtersInUse = this.buildFiltersInUse(results[JOBS].jobs);
|
||||
}
|
||||
|
||||
const filter = results[FILTERS].filters[0];
|
||||
filter.used_by = filtersInUse[filter.filter_id];
|
||||
return filter;
|
||||
} else {
|
||||
return Boom.notFound(`Filter with the id "${filterId}" not found`);
|
||||
}
|
||||
|
@ -36,14 +47,50 @@ export class FilterManager {
|
|||
}
|
||||
}
|
||||
|
||||
async getAllFilterStats() {
|
||||
try {
|
||||
const [ JOBS, FILTERS ] = [0, 1];
|
||||
const results = await Promise.all([
|
||||
this.callWithRequest('ml.jobs'),
|
||||
this.callWithRequest('ml.filters')
|
||||
]);
|
||||
|
||||
// Build a map of filter_ids against jobs and detectors using that filter.
|
||||
let filtersInUse = {};
|
||||
if (results[JOBS] && results[JOBS].jobs) {
|
||||
filtersInUse = this.buildFiltersInUse(results[JOBS].jobs);
|
||||
}
|
||||
|
||||
// For each filter, return just
|
||||
// filter_id
|
||||
// description
|
||||
// item_count
|
||||
// jobs using the filter
|
||||
const filterStats = [];
|
||||
if (results[FILTERS] && results[FILTERS].filters) {
|
||||
results[FILTERS].filters.forEach((filter) => {
|
||||
const stats = {
|
||||
filter_id: filter.filter_id,
|
||||
description: filter.description,
|
||||
item_count: filter.items.length,
|
||||
used_by: filtersInUse[filter.filter_id]
|
||||
};
|
||||
filterStats.push(stats);
|
||||
});
|
||||
}
|
||||
|
||||
return filterStats;
|
||||
} catch (error) {
|
||||
throw Boom.badRequest(error);
|
||||
}
|
||||
}
|
||||
|
||||
async newFilter(filter) {
|
||||
const filterId = filter.filterId;
|
||||
delete filter.filterId;
|
||||
try {
|
||||
await this.callWithRequest('ml.addFilter', { filterId, body: filter });
|
||||
|
||||
// Return the newly created filter.
|
||||
return await this.getFilter(filterId);
|
||||
// Returns the newly created filter.
|
||||
return await this.callWithRequest('ml.addFilter', { filterId, body: filter });
|
||||
} catch (error) {
|
||||
return Boom.badRequest(error);
|
||||
}
|
||||
|
@ -52,19 +99,17 @@ export class FilterManager {
|
|||
async updateFilter(filterId,
|
||||
description,
|
||||
addItems,
|
||||
deleteItems) {
|
||||
removeItems) {
|
||||
try {
|
||||
await this.callWithRequest('ml.updateFilter', {
|
||||
// Returns the newly updated filter.
|
||||
return await this.callWithRequest('ml.updateFilter', {
|
||||
filterId,
|
||||
body: {
|
||||
description,
|
||||
add_items: addItems,
|
||||
delete_items: deleteItems
|
||||
remove_items: removeItems
|
||||
}
|
||||
});
|
||||
|
||||
// Return the newly updated filter.
|
||||
return await this.getFilter(filterId);
|
||||
} catch (error) {
|
||||
return Boom.badRequest(error);
|
||||
}
|
||||
|
@ -75,4 +120,45 @@ export class FilterManager {
|
|||
return this.callWithRequest('ml.deleteFilter', { filterId });
|
||||
}
|
||||
|
||||
buildFiltersInUse(jobsList) {
|
||||
// Build a map of filter_ids against jobs and detectors using that filter.
|
||||
const filtersInUse = {};
|
||||
jobsList.forEach((job) => {
|
||||
const detectors = job.analysis_config.detectors;
|
||||
detectors.forEach((detector) => {
|
||||
if (detector.custom_rules) {
|
||||
const rules = detector.custom_rules;
|
||||
rules.forEach((rule) => {
|
||||
if (rule.scope) {
|
||||
const scopeFields = Object.keys(rule.scope);
|
||||
scopeFields.forEach((scopeField) => {
|
||||
const filter = rule.scope[scopeField];
|
||||
const filterId = filter.filter_id;
|
||||
if (filtersInUse[filterId] === undefined) {
|
||||
filtersInUse[filterId] = { jobs: [], detectors: [] };
|
||||
}
|
||||
|
||||
const jobs = filtersInUse[filterId].jobs;
|
||||
const dtrs = filtersInUse[filterId].detectors;
|
||||
const jobId = job.job_id;
|
||||
|
||||
// Label the detector with the job it comes from.
|
||||
const detectorLabel = `${detector.detector_description} (${jobId})`;
|
||||
if (jobs.indexOf(jobId) === -1) {
|
||||
jobs.push(jobId);
|
||||
}
|
||||
|
||||
if (dtrs.indexOf(detectorLabel) === -1) {
|
||||
dtrs.push(detectorLabel);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return filtersInUse;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -18,6 +18,11 @@ function getAllFilters(callWithRequest) {
|
|||
return mgr.getAllFilters();
|
||||
}
|
||||
|
||||
function getAllFilterStats(callWithRequest) {
|
||||
const mgr = new FilterManager(callWithRequest);
|
||||
return mgr.getAllFilterStats();
|
||||
}
|
||||
|
||||
function getFilter(callWithRequest, filterId) {
|
||||
const mgr = new FilterManager(callWithRequest);
|
||||
return mgr.getFilter(filterId);
|
||||
|
@ -33,9 +38,9 @@ function updateFilter(
|
|||
filterId,
|
||||
description,
|
||||
addItems,
|
||||
deleteItems) {
|
||||
removeItems) {
|
||||
const mgr = new FilterManager(callWithRequest);
|
||||
return mgr.updateFilter(filterId, description, addItems, deleteItems);
|
||||
return mgr.updateFilter(filterId, description, addItems, removeItems);
|
||||
}
|
||||
|
||||
function deleteFilter(callWithRequest, filterId) {
|
||||
|
@ -59,6 +64,20 @@ export function filtersRoutes(server, commonRouteConfig) {
|
|||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: '/api/ml/filters/_stats',
|
||||
handler(request, reply) {
|
||||
const callWithRequest = callWithRequestFactory(server, request);
|
||||
return getAllFilterStats(callWithRequest)
|
||||
.then(resp => reply(resp))
|
||||
.catch(resp => reply(wrapError(resp)));
|
||||
},
|
||||
config: {
|
||||
...commonRouteConfig
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: '/api/ml/filters/{filterId}',
|
||||
|
@ -101,7 +120,7 @@ export function filtersRoutes(server, commonRouteConfig) {
|
|||
filterId,
|
||||
payload.description,
|
||||
payload.addItems,
|
||||
payload.deleteItems)
|
||||
payload.removeItems)
|
||||
.then(resp => reply(resp))
|
||||
.catch(resp => reply(wrapError(resp)));
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue