diff --git a/x-pack/plugins/beats_management/common/domain_types.ts b/x-pack/plugins/beats_management/common/domain_types.ts index 3299457da019b..a553f987a233e 100644 --- a/x-pack/plugins/beats_management/common/domain_types.ts +++ b/x-pack/plugins/beats_management/common/domain_types.ts @@ -28,7 +28,7 @@ export interface CMBeat { } export interface CMPopulatedBeat extends CMBeat { - fullTags: BeatTag[]; + full_tags: BeatTag[]; } export interface BeatTag { diff --git a/x-pack/plugins/beats_management/public/components/__snapshots__/beats_table.test.tsx.snap b/x-pack/plugins/beats_management/public/components/__snapshots__/beats_table.test.tsx.snap deleted file mode 100644 index b4d041382b005..0000000000000 --- a/x-pack/plugins/beats_management/public/components/__snapshots__/beats_table.test.tsx.snap +++ /dev/null @@ -1,19 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`BeatsTable component matches snapshot 1`] = ` - -`; diff --git a/x-pack/plugins/beats_management/public/components/beats_table.tsx b/x-pack/plugins/beats_management/public/components/beats_table.tsx deleted file mode 100644 index 944a38003ea70..0000000000000 --- a/x-pack/plugins/beats_management/public/components/beats_table.tsx +++ /dev/null @@ -1,214 +0,0 @@ -/* - * 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 { - EuiBadge, - EuiButton, - EuiContextMenu, - EuiFlexGroup, - EuiIcon, - // @ts-ignore - EuiInMemoryTable, - EuiLink, - EuiPopover, -} from '@elastic/eui'; -import React from 'react'; -import styled from 'styled-components'; -import { CMPopulatedBeat } from '../../common/domain_types'; - -const columns = [ - { - field: 'id', - name: 'Beat name', - render: (id: string) => {id}, - sortable: true, - }, - { - field: 'type', - name: 'Type', - sortable: true, - }, - { - field: 'tags', - name: 'Tags', - render: (value: string, beat: CMPopulatedBeat) => ( - - {beat.fullTags.map(tag => ( - - {tag.id} - - ))} - - ), - sortable: true, - }, - { - // TODO: update to use actual metadata field - field: 'event_rate', - name: 'Event rate', - sortable: true, - }, - { - // TODO: update to use actual metadata field - field: 'last_updated', - name: 'Last config update', - sortable: true, - }, -]; - -interface BeatsTableProps { - items: CMPopulatedBeat[]; - onBulkEdit: any; - onBulkDelete: any; - onBulkAssignTags: any; -} - -interface BeatsTableState { - pageIndex: number; - pageSize: number; - selection: CMPopulatedBeat[]; - isBulkPopoverOpen: boolean; -} - -const TableContainer = styled.div` - padding: 16px; -`; - -export class BeatsTable extends React.Component { - constructor(props: BeatsTableProps) { - super(props); - - this.state = { - pageIndex: 0, - pageSize: 5, - selection: [], - isBulkPopoverOpen: false, - }; - } - - public render() { - const { items, onBulkEdit, onBulkDelete, onBulkAssignTags } = this.props; - const { pageIndex, pageSize, isBulkPopoverOpen, selection } = this.state; - - const bulkActionButton = ( - - Bulk Action - - ); - - const panels = [ - { - id: 0, - title: 'Bulk Action', - items: [ - { - name: 'Bulk Edit', - icon: , - onClick: () => onBulkEdit(selection), - }, - { - name: 'Bulk Delete', - icon: , - onClick: () => onBulkDelete(selection), - }, - { - name: 'Bulk Assign Tags', - icon: , - onClick: () => onBulkAssignTags(selection), - }, - ], - }, - ]; - - const toolsLeft = ( - - - - ); - - const search = { - box: { incremental: true }, - filters: [ - { - type: 'field_value_selection', - field: 'type', - name: 'Type', - multiSelect: true, - options: items.map(({ type }) => { - return { - value: type, - name: 'Type', - view: type, - }; - }), - }, - ], - toolsLeft, - }; - - const pagination = { - initialPageSize: 5, - pageIndex, - pageSize, - totalItemCount: items.length, - pageSizeOptions: [3, 5, 8], - }; - - const selectionOptions = { - onSelectionChange: this.setSelection, - selectable: () => true, - selectableMessage: () => null, - }; - - return ( - - { - return { - ...beat, - key: `beat${index}`, - }; - })} - itemId="id" - isSelectable={true} - pagination={pagination} - responsive={true} - search={search} - selection={selectionOptions} - sorting={true} - /> - - ); - } - - private showBulkPopover = () => { - const { isBulkPopoverOpen } = this.state; - this.setState({ - isBulkPopoverOpen: !isBulkPopoverOpen, - }); - }; - - private hideBulkPopover = () => { - this.setState({ - isBulkPopoverOpen: false, - }); - }; - - private setSelection = (selection: any) => { - this.setState({ - selection, - }); - }; -} diff --git a/x-pack/plugins/beats_management/public/components/table/__snapshots__/beats_table.test.tsx.snap b/x-pack/plugins/beats_management/public/components/table/__snapshots__/beats_table.test.tsx.snap new file mode 100644 index 0000000000000..fbf2e4fe03255 --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/table/__snapshots__/beats_table.test.tsx.snap @@ -0,0 +1,97 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`BeatsTable component matches snapshot 1`] = ` + + + + +`; diff --git a/x-pack/plugins/beats_management/public/components/beats_table.test.tsx b/x-pack/plugins/beats_management/public/components/table/beats_table.test.tsx similarity index 64% rename from x-pack/plugins/beats_management/public/components/beats_table.test.tsx rename to x-pack/plugins/beats_management/public/components/table/beats_table.test.tsx index 9e29fa87d7ad2..b241e04da1915 100644 --- a/x-pack/plugins/beats_management/public/components/beats_table.test.tsx +++ b/x-pack/plugins/beats_management/public/components/table/beats_table.test.tsx @@ -6,15 +6,13 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { CMPopulatedBeat } from '../../common/domain_types'; +import { CMPopulatedBeat } from '../../../common/domain_types'; import { BeatsTable } from './beats_table'; describe('BeatsTable component', () => { let items: CMPopulatedBeat[]; let beat; - let onBulkEdit: any; - let onBulkDelete: any; - let onBulkAssignTags: any; + let onBulkAction: any; beforeEach(() => { beat = { @@ -23,7 +21,7 @@ describe('BeatsTable component', () => { type: 'type', host_ip: 'ip', host_name: 'name', - fullTags: [ + full_tags: [ { id: 'Production', configuration_blocks: [], @@ -31,20 +29,11 @@ describe('BeatsTable component', () => { ], }; items = [beat]; - onBulkEdit = jest.fn(); - onBulkDelete = jest.fn(); - onBulkAssignTags = jest.fn(); + onBulkAction = jest.fn(); }); it('matches snapshot', () => { - const wrapper = shallow( - - ); + const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/plugins/beats_management/public/components/table/beats_table.tsx b/x-pack/plugins/beats_management/public/components/table/beats_table.tsx new file mode 100644 index 0000000000000..1ac8cbbea3171 --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/table/beats_table.tsx @@ -0,0 +1,185 @@ +/* + * 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 { + EuiBadge, + EuiFlexGroup, + // @ts-ignore + EuiInMemoryTable, + EuiLink, +} from '@elastic/eui'; +import { flatten, uniq } from 'lodash'; +import React from 'react'; +import styled from 'styled-components'; +import { CMPopulatedBeat } from '../../../common/domain_types'; +import { BulkActionControlBar } from './controls'; + +const columns = [ + { + field: 'id', + name: 'Beat name', + render: (id: string) => {id}, + sortable: true, + }, + { + field: 'type', + name: 'Type', + sortable: true, + }, + { + field: 'full_tags', + name: 'Tags', + render: (value: string, beat: CMPopulatedBeat) => ( + + {beat.full_tags.map(tag => ( + + {tag.id} + + ))} + + ), + sortable: false, + }, + { + // TODO: update to use actual metadata field + field: 'event_rate', + name: 'Event rate', + sortable: true, + }, + { + // TODO: update to use actual metadata field + field: 'last_updated', + name: 'Last config update', + sortable: true, + }, +]; + +interface BeatsTableProps { + items: CMPopulatedBeat[]; + onBulkAction: any; +} + +interface BeatsTableState { + itemsToRender: CMPopulatedBeat[]; + pageIndex: number; + pageSize: number; + search?: any; + selection: CMPopulatedBeat[]; +} + +const TableContainer = styled.div` + padding: 16px; +`; + +export class BeatsTable extends React.Component { + constructor(props: BeatsTableProps) { + super(props); + + this.state = { + itemsToRender: props.items, + pageIndex: 0, + pageSize: 5, + selection: [], + }; + } + + public render() { + const { onBulkAction } = this.props; + const { itemsToRender, pageIndex, pageSize } = this.state; + + const pagination = { + pageIndex, + pageSize, + totalItemCount: itemsToRender.length, + pageSizeOptions: [3, 5, 8], + }; + + const selectionOptions = { + onSelectionChange: this.setSelection, + selectable: () => true, + selectableMessage: () => null, + }; + + const tagOptions = this.getTagsOptions(); + const typeOptions = this.getTypeOptions(); + + return ( + + { + const { selection } = this.state; + onBulkAction(action, selection); + }} + onSearchQueryChange={this.onQueryChange} + tagOptions={tagOptions} + typeOptions={typeOptions} + /> + + + ); + } + + private getClauseValuesForField = (ast: any, fieldName: string) => { + const clauses = ast.getFieldClauses(fieldName); + return clauses ? clauses.map((clause: any) => clause.value) : []; + }; + + private onQueryChange = (search: any) => { + const { items } = this.props; + let itemsToRender = items; + + if (search && !search.error) { + const { query } = search; + const types = this.getClauseValuesForField(query.ast, 'type'); + const tags = this.getClauseValuesForField(query.ast, 'tag'); + const terms = query.ast.getTermClauses().map((clause: any) => clause.value); + if (types.length) { + itemsToRender = itemsToRender.filter(item => types.includes(item.type)); + } + if (tags.length) { + itemsToRender = itemsToRender.filter(item => + item.full_tags.some(({ id }) => tags.includes(id)) + ); + } + if (terms.length) { + itemsToRender = itemsToRender.filter(item => + terms.some((term: string) => item.id.includes(term)) + ); + } + } + + this.setState({ + itemsToRender, + }); + }; + + private getTagsOptions = () => { + const { items } = this.props; + const ids = flatten(items.map(({ full_tags }) => full_tags.map(({ id }) => id))); + return uniq(ids).map(id => ({ + value: id, + })); + }; + + private getTypeOptions = () => { + const { items } = this.props; + return uniq(items.map(({ type }) => type)).map(type => ({ value: type })); + }; + + private setSelection = (selection: any) => { + this.setState({ + selection, + }); + }; +} diff --git a/x-pack/plugins/beats_management/public/components/table/controls.tsx b/x-pack/plugins/beats_management/public/components/table/controls.tsx new file mode 100644 index 0000000000000..2602da4be6221 --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/table/controls.tsx @@ -0,0 +1,136 @@ +/* + * 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 { + EuiButton, + EuiContextMenu, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiPopover, + // @ts-ignore + EuiSearchBar, +} from '@elastic/eui'; +import React from 'react'; + +// TODO: move to constants +const BULK_ASSIGN_TAG = 'BULK_ASSIGN_TAG'; +const BULK_DELETE = 'BULK_DELETE'; +const BULK_EDIT = 'BULK_EDIT'; + +interface BulkActionControlBarState { + isPopoverVisible: boolean; +} + +interface FilterOption { + value: string; +} + +interface BulkActionControlBarProps { + onBulkAction: any; + onSearchQueryChange: any; + tagOptions: FilterOption[]; + typeOptions: FilterOption[]; +} + +export class BulkActionControlBar extends React.Component< + BulkActionControlBarProps, + BulkActionControlBarState +> { + constructor(props: BulkActionControlBarProps) { + super(props); + + this.state = { + isPopoverVisible: false, + }; + } + + public render() { + const { isPopoverVisible } = this.state; + + const bulkActionButton = ( + + Bulk Action + + ); + const { onBulkAction, onSearchQueryChange, tagOptions, typeOptions } = this.props; + const panels = [ + { + id: 0, + title: 'Bulk Action', + items: [ + { + name: 'Bulk Edit', + icon: , + onClick: () => onBulkAction(BULK_EDIT), + }, + { + name: 'Bulk Delete', + icon: , + onClick: () => onBulkAction(BULK_DELETE), + }, + { + name: 'Bulk Assign Tags', + icon: , + onClick: () => onBulkAction(BULK_ASSIGN_TAG), + }, + ], + }, + ]; + + return ( + + + + + + + + + + + ); + } + + private showPopover = () => { + this.setState({ + isPopoverVisible: true, + }); + }; + + private hidePopover = () => { + this.setState({ + isPopoverVisible: false, + }); + }; +} diff --git a/x-pack/plugins/beats_management/public/components/table/index.ts b/x-pack/plugins/beats_management/public/components/table/index.ts new file mode 100644 index 0000000000000..73facdbdb2ca0 --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/table/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { BeatsTable } from './beats_table'; diff --git a/x-pack/yarn.lock b/x-pack/yarn.lock index 61c89996ebfa3..5712838a9e070 100644 --- a/x-pack/yarn.lock +++ b/x-pack/yarn.lock @@ -2344,25 +2344,15 @@ ecc-jsbn@~0.1.1: dependencies: jsbn "~0.1.0" -ee-first@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" ecdsa-sig-formatter@1.0.10: version "1.0.10" resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.10.tgz#1c595000f04a8897dfb85000892a0f4c33af86c3" dependencies: safe-buffer "^5.0.1" -elasticsearch@13.0.1: - version "13.0.1" - resolved "https://registry.yarnpkg.com/elasticsearch/-/elasticsearch-13.0.1.tgz#fa58204233052c4cd221e8721e48f3906b385b32" - dependencies: - agentkeepalive "^2.2.0" - chalk "^1.0.0" - lodash "2.4.2" - lodash.get "^4.4.2" - lodash.isempty "^4.4.0" - lodash.trimend "^4.5.1" +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" elasticsearch@^14.1.0: version "14.2.2"