Skip to content

Commit

Permalink
[Beats Management] Add BeatsTable/Bulk Action Search Component (#21182)
Browse files Browse the repository at this point in the history
* Add BeatsTable and control bar components.

* Clean yarn.lock.

* Move raw numbers/strings to constants. Remove obsolete state/props.

* Update/add tests.

* Change prop name from "items" to "beats".

* Rename some variables.

* Move search bar filter definitions to table render.

* Update table to support assignment options.

* Update action control position.

* Refactor split render function into custom components.
  • Loading branch information
justinkambic authored Aug 3, 2018
1 parent 18d6c81 commit 9ec3d3b
Show file tree
Hide file tree
Showing 15 changed files with 1,363 additions and 115 deletions.
9 changes: 5 additions & 4 deletions x-pack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@kbn/test": "link:../packages/kbn-test",
"@types/boom": "^4.3.8",
"@types/chance": "^1.0.1",
"@types/enzyme": "^3.1.12",
"@types/history": "^4.6.2",
"@types/jest": "^22.2.3",
"@types/joi": "^10.4.0",
Expand All @@ -41,7 +42,7 @@
"aws-sdk": "2.2.33",
"axios": "^0.18.0",
"babel-jest": "^22.4.3",
"chalk": "^2.3.2",
"chalk": "^2.4.1",
"chance": "1.0.10",
"checksum": "0.1.1",
"commander": "2.12.2",
Expand Down Expand Up @@ -77,14 +78,14 @@
"supertest-as-promised": "4.0.2",
"tmp": "0.0.31",
"tree-kill": "^1.1.0",
"typescript": "^2.8.3",
"typescript": "^2.9.2",
"vinyl-fs": "^3.0.2",
"xml-crypto": "^0.10.1",
"xml2js": "^0.4.19",
"yargs": "4.7.1"
},
"dependencies": {
"@elastic/eui": "1.1.0",
"@elastic/eui": "3.0.0",
"@elastic/node-crypto": "0.1.2",
"@elastic/node-phantom-simple": "2.2.4",
"@elastic/numeral": "2.3.2",
Expand Down Expand Up @@ -113,7 +114,7 @@
"d3-scale": "1.0.6",
"dedent": "^0.7.0",
"dragselect": "1.7.17",
"elasticsearch": "13.0.1",
"elasticsearch": "^14.1.0",
"extract-zip": "1.5.0",
"font-awesome": "4.4.0",
"get-port": "2.1.0",
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/beats_management/common/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export { PLUGIN } from './plugin';
export { INDEX_NAMES } from './index_names';
export { UNIQUENESS_ENFORCING_TYPES, ConfigurationBlockTypes } from './configuration_blocks';
export const BASE_PATH = '/management/beats_management/';
export { TABLE_CONFIG } from './table';
10 changes: 10 additions & 0 deletions x-pack/plugins/beats_management/common/constants/table.ts
Original file line number Diff line number Diff line change
@@ -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.
*/

export const TABLE_CONFIG = {
INITIAL_ROW_SIZE: 5,
PAGE_SIZE_OPTIONS: [3, 5, 10, 20],
};
7 changes: 7 additions & 0 deletions x-pack/plugins/beats_management/common/domain_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,20 @@ export interface CMBeat {
host_ip: string;
host_name: string;
ephemeral_id?: string;
last_updated?: string;
event_rate?: string;
local_configuration_yml?: string;
tags?: string[];
central_configuration_yml?: string;
metadata?: {};
}

export interface CMPopulatedBeat extends CMBeat {
full_tags: BeatTag[];
}

export interface BeatTag {
id: string;
configuration_blocks: ConfigurationBlock[];
color?: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* 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, EuiPopover } from '@elastic/eui';
import React from 'react';
import { ActionDefinition } from './table_type_configs';

interface ActionButtonProps {
actions: ActionDefinition[];
isPopoverVisible: boolean;
actionHandler(action: string, payload?: any): void;
hidePopover(): void;
showPopover(): void;
}

export function ActionButton(props: ActionButtonProps) {
const { actions, actionHandler, hidePopover, isPopoverVisible, showPopover } = props;
if (actions.length === 0) {
return null;
} else if (actions.length === 1) {
const action = actions[0];
return (
<EuiButton
color={action.danger ? 'danger' : 'primary'}
onClick={() => actionHandler(action.action)}
>
{action.name}
</EuiButton>
);
}
return (
<EuiPopover
anchorPosition="downLeft"
button={
<EuiButton iconSide="right" iconType="arrowDown" onClick={showPopover}>
Bulk Action
</EuiButton>
}
closePopover={hidePopover}
id="contextMenu"
isOpen={isPopoverVisible}
panelPaddingSize="none"
withTitle
>
<EuiContextMenu
initialPanelId={0}
panels={[
{
id: 0,
title: 'Bulk Actions',
items: actions.map(action => ({
...action,
onClick: () => actionHandler(action.action),
})),
},
]}
/>
</EuiPopover>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* 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, EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiPopover } from '@elastic/eui';
import React from 'react';
import { ActionButton } from './action_button';
import { ControlDefinitions } from './table_type_configs';

interface AssignmentOptionsProps {
assignmentOptions: any[] | null;
assignmentTitle: string | null;
controlDefinitions: ControlDefinitions;
selectionCount: number;
actionHandler(action: string, payload?: any): void;
}

interface AssignmentOptionsState {
isAssignmentPopoverVisible: boolean;
isActionPopoverVisible: boolean;
}

export class AssignmentOptions extends React.Component<
AssignmentOptionsProps,
AssignmentOptionsState
> {
constructor(props: AssignmentOptionsProps) {
super(props);

this.state = {
isAssignmentPopoverVisible: false,
isActionPopoverVisible: false,
};
}

public render() {
const {
actionHandler,
assignmentOptions,
assignmentTitle,
controlDefinitions: { actions },
selectionCount,
} = this.props;
const { isActionPopoverVisible, isAssignmentPopoverVisible } = this.state;
return (
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
<EuiFlexItem>{selectionCount} selected</EuiFlexItem>
<EuiFlexItem grow={false}>
<ActionButton
actions={actions}
actionHandler={actionHandler}
hidePopover={() => {
this.setState({ isActionPopoverVisible: false });
}}
isPopoverVisible={isActionPopoverVisible}
showPopover={() => {
this.setState({ isActionPopoverVisible: true });
}}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiPopover
button={
<EuiButton
color="primary"
iconSide="right"
iconType="arrowDown"
onClick={() => {
this.setState({
isAssignmentPopoverVisible: true,
});
actionHandler('loadAssignmentOptions');
}}
>
{assignmentTitle}
</EuiButton>
}
closePopover={() => {
this.setState({ isAssignmentPopoverVisible: false });
}}
id="assignmentList"
isOpen={isAssignmentPopoverVisible}
panelPaddingSize="s"
withTitle
>
{assignmentOptions ? (
// @ts-ignore direction prop not available on current typing
<EuiFlexGroup direction="column" gutterSize="xs">
{assignmentOptions}
</EuiFlexGroup>
) : (
<div>
<EuiLoadingSpinner size="m" /> Loading
</div>
)}
</EuiPopover>
</EuiFlexItem>
</EuiFlexGroup>
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* 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 typings for EuiSearchar not included in EUI
EuiSearchBar,
} from '@elastic/eui';
import React from 'react';
import { AssignmentOptions } from './assignment_options';
import { ControlDefinitions } from './table_type_configs';

interface ControlBarProps {
assignmentOptions: any[] | null;
assignmentTitle: string | null;
showAssignmentOptions: boolean;
controlDefinitions: ControlDefinitions;
selectionCount: number;
actionHandler(actionType: string, payload?: any): void;
}

export function ControlBar(props: ControlBarProps) {
const {
actionHandler,
assignmentOptions,
assignmentTitle,
controlDefinitions,
selectionCount,
showAssignmentOptions,
} = props;
return selectionCount !== 0 && showAssignmentOptions ? (
<AssignmentOptions
actionHandler={actionHandler}
assignmentOptions={assignmentOptions}
assignmentTitle={assignmentTitle}
controlDefinitions={controlDefinitions}
selectionCount={selectionCount}
/>
) : (
<EuiSearchBar
box={{ incremental: true }}
filters={controlDefinitions.filters}
onChange={(query: any) => actionHandler('search', query)}
/>
);
}
Original file line number Diff line number Diff line change
@@ -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.
*/

export { Table } from './table';
export { ControlBar } from './controls';
export { BeatsTableType } from './table_type_configs';
88 changes: 88 additions & 0 deletions x-pack/plugins/beats_management/public/components/table/table.tsx
Original file line number Diff line number Diff line change
@@ -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 no typings for EuiInMemoryTable in EUI
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<BeatsTableProps, BeatsTableState> {
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 (
<TableContainer>
<ControlBar
actionHandler={(action: string, payload: any) => actionHandler(action, payload)}
assignmentOptions={assignmentOptions}
assignmentTitle={assignmentTitle}
controlDefinitions={type.controlDefinitions(items)}
selectionCount={this.state.selection.length}
showAssignmentOptions={true}
/>
<EuiSpacer size="m" />
<EuiInMemoryTable
columns={type.columnDefinitions}
items={items}
itemId="id"
isSelectable={true}
pagination={pagination}
selection={selectionOptions}
sorting={true}
/>
</TableContainer>
);
}

private setSelection = (selection: any) => {
this.setState({
selection,
});
};
}
Loading

0 comments on commit 9ec3d3b

Please sign in to comment.