From 9e2a38e0de63d736e7ec901240670d1f51884833 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Thu, 2 Aug 2018 16:17:36 -0400 Subject: [PATCH] Update table to support assignment options. --- .../__snapshots__/beats_table.test.tsx.snap | 112 ---------- .../__snapshots__/controls.test.tsx.snap | 115 ---------- .../components/table/beats_table.test.tsx | 53 ----- .../public/components/table/beats_table.tsx | 194 ----------------- .../public/components/table/controls.test.tsx | 72 ------ .../public/components/table/controls.tsx | 205 ++++++++++++------ .../public/components/table/index.ts | 4 +- .../public/components/table/table.tsx | 88 ++++++++ .../components/table/table_type_configs.tsx | 121 +++++++++++ .../public/pages/main/beats.tsx | 105 ++++++++- 10 files changed, 448 insertions(+), 621 deletions(-) delete mode 100644 x-pack/plugins/beats_management/public/components/table/__snapshots__/beats_table.test.tsx.snap delete mode 100644 x-pack/plugins/beats_management/public/components/table/__snapshots__/controls.test.tsx.snap delete mode 100644 x-pack/plugins/beats_management/public/components/table/beats_table.test.tsx delete mode 100644 x-pack/plugins/beats_management/public/components/table/beats_table.tsx delete mode 100644 x-pack/plugins/beats_management/public/components/table/controls.test.tsx create mode 100644 x-pack/plugins/beats_management/public/components/table/table.tsx create mode 100644 x-pack/plugins/beats_management/public/components/table/table_type_configs.tsx 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 deleted file mode 100644 index 2a29a5e2d0224..0000000000000 --- a/x-pack/plugins/beats_management/public/components/table/__snapshots__/beats_table.test.tsx.snap +++ /dev/null @@ -1,112 +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/table/__snapshots__/controls.test.tsx.snap b/x-pack/plugins/beats_management/public/components/table/__snapshots__/controls.test.tsx.snap deleted file mode 100644 index 8f98e147e8392..0000000000000 --- a/x-pack/plugins/beats_management/public/components/table/__snapshots__/controls.test.tsx.snap +++ /dev/null @@ -1,115 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`BulkActionControlBar component matches snapshot 1`] = ` - - - - Bulk Action - - } - closePopover={[Function]} - id="contextMenu" - isOpen={false} - ownFocus={false} - panelPaddingSize="none" - withTitle={true} - > - , - "name": "Bulk Edit", - "onClick": [Function], - }, - Object { - "icon": , - "name": "Bulk Delete", - "onClick": [Function], - }, - Object { - "icon": , - "name": "Bulk Assign Tags", - "onClick": [Function], - }, - ], - "title": "Bulk Action", - }, - ] - } - /> - - - - - - -`; diff --git a/x-pack/plugins/beats_management/public/components/table/beats_table.test.tsx b/x-pack/plugins/beats_management/public/components/table/beats_table.test.tsx deleted file mode 100644 index 119ee5389dc74..0000000000000 --- a/x-pack/plugins/beats_management/public/components/table/beats_table.test.tsx +++ /dev/null @@ -1,53 +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 { shallow } from 'enzyme'; -import React from 'react'; -import { CMPopulatedBeat } from '../../../common/domain_types'; -import { BeatsTable } from './beats_table'; - -describe('BeatsTable component', () => { - let beats: CMPopulatedBeat[]; - let onBulkAction: any; - - beforeEach(() => { - beats = [ - { - id: 'beatid', - access_token: 'access', - type: 'type', - host_ip: 'ip', - host_name: 'name', - full_tags: [ - { - id: 'Production', - configuration_blocks: [], - }, - ], - }, - { - id: 'beatid2', - access_token: 'access', - type: 'Filebeat v6.3.2', - host_ip: '192.168.1.0', - host_name: 'name', - full_tags: [ - { - id: 'Production', - configuration_blocks: [], - }, - ], - }, - ]; - onBulkAction = jest.fn(); - }); - - it('matches snapshot', () => { - 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 deleted file mode 100644 index dcb7b843d079f..0000000000000 --- a/x-pack/plugins/beats_management/public/components/table/beats_table.tsx +++ /dev/null @@ -1,194 +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 { - // @ts-ignore - EuiBadge, - EuiFlexGroup, - // @ts-ignore - EuiInMemoryTable, - EuiLink, -} from '@elastic/eui'; -import { flatten, uniq } from 'lodash'; -import React from 'react'; -import styled from 'styled-components'; -import { TABLE_CONFIG } from '../../../common/constants'; -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 { - beats: CMPopulatedBeat[]; - onBulkAction: any; -} - -interface BeatsTableState { - beatsToRender: CMPopulatedBeat[]; - selection: CMPopulatedBeat[]; -} - -const TableContainer = styled.div` - padding: 16px; -`; - -export class BeatsTable extends React.Component { - constructor(props: BeatsTableProps) { - super(props); - - this.state = { - beatsToRender: props.beats, - selection: [], - }; - } - - public render() { - const { beatsToRender } = this.state; - - const pagination = { - initialPageSize: TABLE_CONFIG.INITIAL_ROW_SIZE, - pageSizeOptions: TABLE_CONFIG.PAGE_SIZE_OPTIONS, - }; - - const selectionOptions = { - onSelectionChange: this.setSelection, - selectable: () => true, - selectableMessage: () => 'Select this beat', - }; - - const tagOptions = this.getTagsOptions(); - const typeOptions = this.getTypeOptions(); - - const searchBarFilters = [ - { - type: 'field_value_selection', - field: 'type', - name: 'Type', - options: typeOptions, - }, - { - type: 'field_value_selection', - field: 'tag', - name: 'Tags', - options: tagOptions, - }, - ]; - - return ( - - - - - ); - } - - private getClauseValuesForField = (ast: any, fieldName: string) => { - const clauses = ast.getFieldClauses(fieldName); - return clauses ? clauses.map((clause: any) => clause.value) : []; - }; - - private handleBulkAction = (action: string) => { - const { onBulkAction } = this.props; - const { selection } = this.state; - onBulkAction(action, selection); - }; - - private onSearchQueryChange = (search: any) => { - const { beats } = this.props; - let beatsToRender = beats; - - if (search && !search.error) { - const { ast } = search.query; - const types = this.getClauseValuesForField(ast, 'type'); - const tags = this.getClauseValuesForField(ast, 'tag'); - const terms = ast.getTermClauses().map((clause: any) => clause.value); - if (types.length) { - beatsToRender = beatsToRender.filter(beat => types.includes(beat.type)); - } - if (tags.length) { - beatsToRender = beatsToRender.filter(beat => - beat.full_tags.some(({ id }) => tags.includes(id)) - ); - } - if (terms.length) { - beatsToRender = beatsToRender.filter(beat => - terms.some((term: string) => beat.id.includes(term)) - ); - } - } - - this.setState({ - beatsToRender, - }); - }; - - private getTagsOptions = () => { - const { beats } = this.props; - const fullTags = flatten(beats.map(item => item.full_tags)); - return uniq(fullTags.map(tag => ({ value: tag.id })), 'value'); - }; - - private getTypeOptions = () => { - const { beats } = this.props; - return uniq(beats.map(({ type }) => ({ value: type })), 'value'); - }; - - private setSelection = (selection: any) => { - this.setState({ - selection, - }); - }; -} diff --git a/x-pack/plugins/beats_management/public/components/table/controls.test.tsx b/x-pack/plugins/beats_management/public/components/table/controls.test.tsx deleted file mode 100644 index b16fdde8d1700..0000000000000 --- a/x-pack/plugins/beats_management/public/components/table/controls.test.tsx +++ /dev/null @@ -1,72 +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 { mount, shallow } from 'enzyme'; -import React from 'react'; -import { TABLE_CONFIG } from '../../../common/constants'; -import { BulkActionControlBar } from './controls'; - -describe('BulkActionControlBar component', () => { - let onBulkAction: any; - let onSearchQueryChange: any; - let tagOptions; - let typeOptions; - let props: any; - - beforeEach(() => { - onBulkAction = jest.fn(); - onSearchQueryChange = jest.fn(); - tagOptions = [{ value: 'Production' }]; - typeOptions = [{ value: 'Filebeat v6.3.2' }]; - - props = { - onBulkAction, - onSearchQueryChange, - tagOptions, - typeOptions, - }; - }); - - it('matches snapshot', () => { - const wrapper = shallow(); - - expect(wrapper).toMatchSnapshot(); - }); - - it('bulk action button exposes popover', () => { - const wrapper = mount(); - - wrapper.find('EuiButton').simulate('click'); - // @ts-ignore - expect(wrapper.instance().state.isPopoverVisible).toBe(true); - }); - - it('bulk action context item clicks trigger action handler', () => { - const wrapper = mount(); - - wrapper.find('EuiButton').simulate('click'); - wrapper - .find('EuiContextMenuItem') - .first() - .simulate('click'); - expect(onBulkAction).toHaveBeenCalledTimes(1); - expect(onBulkAction).toBeCalledWith(TABLE_CONFIG.ACTIONS.BULK_EDIT); - - wrapper - .find('EuiContextMenuItem') - .at(1) - .simulate('click'); - expect(onBulkAction).toHaveBeenCalledTimes(2); - expect(onBulkAction).toBeCalledWith(TABLE_CONFIG.ACTIONS.BULK_DELETE); - - wrapper - .find('EuiContextMenuItem') - .last() - .simulate('click'); - expect(onBulkAction).toHaveBeenCalledTimes(3); - expect(onBulkAction).toBeCalledWith(TABLE_CONFIG.ACTIONS.BULK_ASSIGN_TAG); - }); -}); diff --git a/x-pack/plugins/beats_management/public/components/table/controls.tsx b/x-pack/plugins/beats_management/public/components/table/controls.tsx index ed22553c1f741..db8528f27ef63 100644 --- a/x-pack/plugins/beats_management/public/components/table/controls.tsx +++ b/x-pack/plugins/beats_management/public/components/table/controls.tsx @@ -9,97 +9,162 @@ import { EuiContextMenu, EuiFlexGroup, EuiFlexItem, - EuiIcon, + EuiLoadingSpinner, EuiPopover, // @ts-ignore EuiSearchBar, } from '@elastic/eui'; import React from 'react'; -import { TABLE_CONFIG } from '../../../common/constants'; +import { ControlDefinitions } from './table_type_configs'; -interface BulkActionControlBarState { - isPopoverVisible: boolean; +interface ControlBarProps { + assignmentOptions: any[] | null; + assignmentTitle: string | null; + showAssignmentOptions: boolean; + controlDefinitions: ControlDefinitions; + selectionCount: number; + actionHandler(actionType: string, payload?: any): void; } -interface BulkActionControlBarProps { - onBulkAction: any; - onSearchQueryChange: any; - searchBarFilters: any[]; +interface ControlBarState { + isPopoverVisible: boolean; + isAssignmentPopoverVisible: boolean; } -export class BulkActionControlBar extends React.Component< - BulkActionControlBarProps, - BulkActionControlBarState -> { - constructor(props: BulkActionControlBarProps) { +export class ControlBar extends React.Component { + constructor(props: ControlBarProps) { super(props); this.state = { isPopoverVisible: false, + isAssignmentPopoverVisible: false, }; } public render() { - const { searchBarFilters } = this.props; - const { isPopoverVisible } = this.state; + const { selectionCount, showAssignmentOptions } = this.props; + return selectionCount !== 0 && showAssignmentOptions + ? this.renderAssignmentOptions() + : this.renderDefaultControls(); + } - const bulkActionButton = ( - - Bulk Action - - ); - const { onSearchQueryChange } = this.props; - const panels = [ - { - id: 0, - title: 'Bulk Action', - items: [ - { - name: 'Bulk Edit', - icon: , - onClick: this.getActionHandler(TABLE_CONFIG.ACTIONS.BULK_EDIT), - }, - { - name: 'Bulk Delete', - icon: , - onClick: this.getActionHandler(TABLE_CONFIG.ACTIONS.BULK_DELETE), - }, - { - name: 'Bulk Assign Tags', - icon: , - onClick: this.getActionHandler(TABLE_CONFIG.ACTIONS.BULK_ASSIGN_TAG), - }, - ], - }, - ]; + private renderAssignmentOptions = () => ( + + {this.renderActionButton()} + {this.props.selectionCount} selected + + Disenroll Selected + + + { + this.showAssignmentPopover(); + this.props.actionHandler('loadAssignmentOptions'); + }} + > + {this.props.assignmentTitle} + + } + closePopover={this.hideAssignmentPopover} + id="assignmentList" + isOpen={this.state.isAssignmentPopoverVisible} + panelPaddingSize="s" + withTitle + > + {this.props.assignmentOptions ? ( + // @ts-ignore direction prop not available on current typing + + {this.props.assignmentOptions} + + ) : ( +
+ Loading +
+ )} +
+
+
+ ); + + private renderDefaultControls = () => ( + + {this.renderActionButton()} + + this.props.actionHandler('search', query)} + /> + + + ); + private renderActionButton = () => { + const { + controlDefinitions: { actions }, + actionHandler, + } = this.props; + + if (actions.length === 0) { + return null; + } else if (actions.length === 1) { + const action = actions[0]; + return ( + actionHandler(action.action)} + > + {action.name} + + ); + } return ( - - - - - - - - - - + + Bulk Action + + } + closePopover={this.hidePopover} + id="contextMenu" + isOpen={this.state.isPopoverVisible} + panelPaddingSize="none" + withTitle + > + ({ + ...action, + onClick: () => actionHandler(action.action), + })), + }, + ]} + /> + ); - } + }; + + private hideAssignmentPopover = () => { + this.setState({ + isAssignmentPopoverVisible: false, + }); + }; + + private showAssignmentPopover = () => { + this.setState({ + isAssignmentPopoverVisible: true, + }); + }; private hidePopover = () => { this.setState({ @@ -107,8 +172,6 @@ export class BulkActionControlBar extends React.Component< }); }; - private getActionHandler = (action: string) => () => this.props.onBulkAction(action); - private showPopover = () => { this.setState({ isPopoverVisible: true, diff --git a/x-pack/plugins/beats_management/public/components/table/index.ts b/x-pack/plugins/beats_management/public/components/table/index.ts index 73facdbdb2ca0..0789cad5c3022 100644 --- a/x-pack/plugins/beats_management/public/components/table/index.ts +++ b/x-pack/plugins/beats_management/public/components/table/index.ts @@ -4,4 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export { BeatsTable } from './beats_table'; +export { Table } from './table'; +export { ControlBar } from './controls'; +export { BeatsTableType } from './table_type_configs'; diff --git a/x-pack/plugins/beats_management/public/components/table/table.tsx b/x-pack/plugins/beats_management/public/components/table/table.tsx new file mode 100644 index 0000000000000..f38feb7ca2ec4 --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/table/table.tsx @@ -0,0 +1,88 @@ +/* + * 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 { + // @ts-ignore + EuiInMemoryTable, + EuiSpacer, +} from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; +import { TABLE_CONFIG } from '../../../common/constants'; +import { CMPopulatedBeat } from '../../../common/domain_types'; +import { ControlBar } from './controls'; +import { TableType } from './table_type_configs'; + +interface BeatsTableProps { + assignmentOptions: any[] | null; + assignmentTitle: string | null; + items: any[]; + type: TableType; + actionHandler(action: string, payload?: any): void; +} + +interface BeatsTableState { + selection: CMPopulatedBeat[]; +} + +const TableContainer = styled.div` + padding: 16px; +`; + +export class Table extends React.Component { + constructor(props: BeatsTableProps) { + super(props); + + this.state = { + selection: [], + }; + } + + public render() { + const { actionHandler, assignmentOptions, assignmentTitle, items, type } = this.props; + + const pagination = { + initialPageSize: TABLE_CONFIG.INITIAL_ROW_SIZE, + pageSizeOptions: TABLE_CONFIG.PAGE_SIZE_OPTIONS, + }; + + const selectionOptions = { + onSelectionChange: this.setSelection, + selectable: () => true, + selectableMessage: () => 'Select this beat', + selection: this.state.selection, + }; + + return ( + + actionHandler(action, payload)} + assignmentOptions={assignmentOptions} + assignmentTitle={assignmentTitle} + controlDefinitions={type.controlDefinitions(items)} + selectionCount={this.state.selection.length} + showAssignmentOptions={true} + /> + + + + ); + } + + private setSelection = (selection: any) => { + this.setState({ + selection, + }); + }; +} diff --git a/x-pack/plugins/beats_management/public/components/table/table_type_configs.tsx b/x-pack/plugins/beats_management/public/components/table/table_type_configs.tsx new file mode 100644 index 0000000000000..484f067b2cb76 --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/table/table_type_configs.tsx @@ -0,0 +1,121 @@ +/* + * 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. + */ + +// @ts-ignore +import { EuiBadge, EuiFlexGroup, EuiIcon, EuiLink } from '@elastic/eui'; +import { flatten, uniq } from 'lodash'; +import moment from 'moment'; +import React from 'react'; +import { CMPopulatedBeat } from '../../../common/domain_types'; + +export interface ColumnDefinition { + field: string; + name: string; + sortable?: boolean; + render?(value: any, object?: any): any; +} + +export interface ActionDefinition { + action: string; + danger?: boolean; + icon?: any; + name: string; +} + +interface FilterOption { + value: string; +} + +export interface FilterDefinition { + field: string; + name: string; + options?: FilterOption[]; + type: string; +} + +export interface ControlDefinitions { + actions: ActionDefinition[]; + filters: FilterDefinition[]; +} + +export interface TableType { + columnDefinitions: ColumnDefinition[]; + controlDefinitions(items: any[]): ControlDefinitions; +} + +export const BeatsTableType: TableType = { + columnDefinitions: [ + { + 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', + render: (value: Date) =>
{moment(value).fromNow()}
, + sortable: true, + }, + ], + controlDefinitions: (data: any) => ({ + actions: [ + { + action: 'edit', + name: 'Bulk Edit', + icon: , + }, + { + action: 'delete', + name: 'Bulk Delete', + icon: , + }, + ], + filters: [ + { + type: 'field_value_selection', + field: 'type', + name: 'Type', + options: uniq(data.map(({ type }: { type: any }) => ({ value: type })), 'value'), + }, + { + type: 'field_value_selection', + field: 'full_tags', + name: 'Tags', + options: uniq( + flatten(data.map((item: any) => item.full_tags)).map((tag: any) => ({ value: tag.id })), + 'value' + ), + }, + ], + }), +}; diff --git a/x-pack/plugins/beats_management/public/pages/main/beats.tsx b/x-pack/plugins/beats_management/public/pages/main/beats.tsx index a8c7518a3e27c..1d8a9e6717605 100644 --- a/x-pack/plugins/beats_management/public/pages/main/beats.tsx +++ b/x-pack/plugins/beats_management/public/pages/main/beats.tsx @@ -5,8 +5,11 @@ */ import { + // @ts-ignore typings for EuiBadge not present in current version + EuiBadge, EuiButton, EuiButtonEmpty, + EuiFlexItem, EuiModal, EuiModalBody, EuiModalFooter, @@ -15,16 +18,20 @@ import { EuiOverlayMask, } from '@elastic/eui'; -import { CMBeat } from '../../../common/domain_types'; +import React from 'react'; +import { BeatTag, CMBeat, CMPopulatedBeat } from '../../../common/domain_types'; +import { BeatsTagAssignment } from '../../../server/lib/adapters/beats/adapter_types'; +import { BeatsTableType, Table } from '../../components/table'; import { FrontendLibs } from '../../lib/lib'; -import React from 'react'; interface BeatsPageProps { libs: FrontendLibs; } interface BeatsPageState { beats: CMBeat[]; + tags: any[] | null; + tableRef: any; } export class BeatsPage extends React.PureComponent { @@ -72,17 +79,109 @@ export class BeatsPage extends React.PureComponentbeats table and stuff - {this.state.beats.length}; + return ( + + ); } + + private handleBeatsActions = (action: string, payload: any) => { + switch (action) { + case 'edit': + // TODO: navigate to edit page + break; + case 'delete': + this.deleteSelected(); + break; + case 'search': + this.handleSearchQuery(payload); + break; + case 'loadAssignmentOptions': + this.loadTags(); + break; + } + + this.loadBeats(); + }; + + // TODO: call delete endpoint + private deleteSelected = async () => { + // const selected = this.getSelectedBeats(); + // await this.props.libs.beats.delete(selected); + }; + private async loadBeats() { const beats = await this.props.libs.beats.getAll(); this.setState({ beats, }); } + + // todo: add reference to ES filter endpoint + private handleSearchQuery = (query: any) => { + // await this.props.libs.beats.searach(query); + }; + + private loadTags = async () => { + const tags = await this.props.libs.tags.getAll(); + const selectedBeats = this.getSelectedBeats(); + + const renderedTags = tags.map((tag: BeatTag) => { + const hasMatches = selectedBeats.some((beat: any) => + beat.full_tags.some((t: any) => t.id === tag.id) + ); + + return ( + + this.removeTagsFromBeats(selectedBeats, tag) + : () => this.assignTagsToBeats(selectedBeats, tag) + } + onClickAriaLabel={tag.id} + > + {tag.id} + + + ); + }); + this.setState({ + tags: renderedTags, + }); + }; + + private createBeatTagAssignments = ( + beats: CMPopulatedBeat[], + tag: BeatTag + ): BeatsTagAssignment[] => beats.map(({ id }) => ({ beatId: id, tag: tag.id })); + + private removeTagsFromBeats = async (beats: CMPopulatedBeat[], tag: BeatTag) => { + await this.props.libs.beats.removeTagsFromBeats(this.createBeatTagAssignments(beats, tag)); + this.loadBeats(); + }; + + private assignTagsToBeats = async (beats: CMPopulatedBeat[], tag: BeatTag) => { + await this.props.libs.beats.assignTagsToBeats(this.createBeatTagAssignments(beats, tag)); + this.loadBeats(); + }; + + private getSelectedBeats = () => { + return this.state.tableRef.current.state.selection; + }; }