Skip to content

Commit

Permalink
Add SelectPopover component (Graylog2#4619)
Browse files Browse the repository at this point in the history
* Add first version of select popover

- Display list of elements in a popover
- Store selected element in state
- Add callbacks to know when the selected element changed

* Add element formatting to example

* Add optional data filter to SelectPopover

* Add clear selection option

* Make selectable list scroll

This will allow us to set many options in there.

* Add props documentation

* Revert change in local yarn cache

* Make @mariussturm happy
  • Loading branch information
edmundoa authored and Marius Sturm committed Feb 28, 2018
1 parent bdf7147 commit abc7346
Show file tree
Hide file tree
Showing 5 changed files with 312 additions and 0 deletions.
1 change: 1 addition & 0 deletions graylog2-web-interface/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"react-dnd": "^2.0.2",
"react-dnd-html5-backend": "^2.0.0",
"react-grid-layout": "^0.14.3",
"react-isolated-scroll": "^0.1.1",
"react-leaflet": "^1.6.0",
"react-overlays": "^0.6.5",
"react-resizable": "^1.7.5",
Expand Down
38 changes: 38 additions & 0 deletions graylog2-web-interface/src/components/common/SelectPopover.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
:local(.customPopover) {
padding-left: 0;
padding-right: 0;
}

:local(.customPopover) .popover-content {
min-width: 200px;
padding: 0;
}

:local(.customPopover) .list-group {
margin-bottom: 0;
}

:local(.customPopover) .list-group-item {
border-right: 0;
border-left: 0;
padding: 6px 15px;
}

:local(.customPopover) .list-group-item:first-child {
border-top-right-radius: 0;
border-top-left-radius: 0;
}

:local(.customPopover) .list-group-item:last-child {
border-bottom: 0;
}

:local(.scrollableList) {
max-height: 340px; /* 10 items */
overflow: auto;
}

:local(.dataFilterInput) {
margin-bottom: 0;
padding: 5px;
}
147 changes: 147 additions & 0 deletions graylog2-web-interface/src/components/common/SelectPopover.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import { FormControl, FormGroup, ListGroup, ListGroupItem, OverlayTrigger, Popover } from 'react-bootstrap';
import lodash from 'lodash';
import IsolatedScroll from 'react-isolated-scroll';

import style from './SelectPopover.css';

const SelectPopover = createReactClass({
propTypes: {
/** Provides an ID for this popover element. */
id: PropTypes.string.isRequired,
/** Indicates where the popover should appear. */
placement: PropTypes.oneOf(['top', 'right', 'bottom', 'left']),
/** Title to use in the popover header. */
title: PropTypes.string.isRequired,
/** React node that will be used as trigger to show/hide the popover. */
triggerNode: PropTypes.node.isRequired,
/** Event that will show/hide the popover. */
triggerAction: PropTypes.oneOf(['click', 'hover', 'focus']),
/**
* Array of strings that contain items to be displayed as options in the list.
* You can customize the items appearance by giving an `itemFormatter` prop.
*/
items: PropTypes.arrayOf(PropTypes.string),
/**
* Function that will be called for each item in the list. It receives the current item
* and must return a React node that will be displayed on screen.
*/
itemFormatter: PropTypes.func,
/** Indicates which is the selected item. This should be the same string that appears in the `items` list. */
selectedItem: PropTypes.string,
/**
* Function that will be called when the item selection changes.
* The function will receive the selected item as argument or `undefined` if the selection
* is cleared.
*/
onItemSelect: PropTypes.func.isRequired,
/** Indicates whether the component should display a text filter or not. */
displayDataFilter: PropTypes.bool,
/** Placeholder to display in the filter text input. */
filterPlaceholder: PropTypes.string,
},

getDefaultProps() {
return {
placement: 'bottom',
triggerAction: 'click',
items: [],
itemFormatter: item => item,
selectedItem: undefined,
onItemSelect: () => {},
displayDataFilter: true,
filterPlaceholder: 'Type to filter',
};
},

getInitialState() {
return {
filterText: '',
filteredItems: this.props.items,
selectedItem: this.props.selectedItem,
};
},

componentWillReceiveProps(nextProps) {
if (this.props.selectedItem !== nextProps.selectedItem) {
this.setState({ selectedItem: nextProps.selectedItem });
}
if (this.props.items !== nextProps.items) {
this.filterData(this.state.filterText, nextProps.items);
}
},

handleItemSelection(item) {
return () => {
this.setState({ selectedItem: item });
this.props.onItemSelect(item);
};
},

filterData(filterText, items) {
const newFilteredItems = items.filter(item => item.match(new RegExp(filterText, 'i')));
this.setState({ filterText: filterText, filteredItems: newFilteredItems });
},

handleFilterChange(items) {
return (event) => {
const filterText = event.target.value.trim();
this.filterData(filterText, items);
};
},

pickPopoverProps(props) {
const popoverPropKeys = Object.keys(Popover.propTypes);
return lodash.pick(props, popoverPropKeys);
},

renderDataFilter(items) {
return (
<FormGroup controlId="dataFilterInput" className={style.dataFilterInput}>
<FormControl type="text" placeholder={this.props.filterPlaceholder} onChange={this.handleFilterChange(items)} />
</FormGroup>
);
},

renderClearSelectionItem() {
return (
<ListGroupItem onClick={this.handleItemSelection()}><i className="fa fa-fw fa-times text-danger" /> Clear selection</ListGroupItem>
);
},

render() {
const { displayDataFilter, itemFormatter, items, placement, triggerAction, triggerNode, ...otherProps } = this.props;
const popoverProps = this.pickPopoverProps(otherProps);
const { filteredItems, selectedItem } = this.state;

const popover = (
<Popover {...popoverProps} className={style.customPopover}>
{displayDataFilter && this.renderDataFilter(items)}
{selectedItem && this.renderClearSelectionItem()}
<IsolatedScroll className={style.scrollableList}>
<ListGroup>
{filteredItems.map((item) => {
return (
<ListGroupItem key={item}
onClick={this.handleItemSelection(item)}
active={this.state.selectedItem === item}>
{itemFormatter(item)}
</ListGroupItem>
);
})}
</ListGroup>
</IsolatedScroll>
</Popover>
);

return (
<OverlayTrigger trigger={triggerAction} placement={placement} overlay={popover} rootClose>
{triggerNode}
</OverlayTrigger>
);
},
});

export default SelectPopover;
115 changes: 115 additions & 0 deletions graylog2-web-interface/src/components/common/SelectPopover.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
```js
const createReactClass = require('create-react-class');
const Button = require('react-bootstrap').Button;

const items = [
'Black',
'Blue',
'Green',
'Red',
'White',
'Yellow',
];

const SelectPopoverExample = createReactClass({
getInitialState() {
return {
selectedItem: undefined,
};
},

handleItemSelect(item) {
this.setState({ selectedItem: item });
},

render() {
const selectedItem = this.state.selectedItem;

return (
<div>
<div style={{ display: 'inline-block', marginRight: 20 }}>
<SelectPopover id="example-popover"
title="Filter by color"
triggerNode={<Button bsStyle="info" bsSize="small">Select color</Button>}
items={items}
selectedItem={selectedItem}
onItemSelect={this.handleItemSelect}
displayDataFilter={false} />
</div>

{selectedItem ? `You have selected ${selectedItem}` : 'Please select a color!'}
</div>
);
}
});

<SelectPopoverExample />
```

```js
const createReactClass = require('create-react-class');
const Badge = require('react-bootstrap').Badge;
const Button = require('react-bootstrap').Button;

const items = [
'AliceBlue',
'Aqua',
'Black',
'Blue',
'Brown',
'Cyan',
'DarkMagenta',
'Gold',
'Green',
'Magenta',
'Navy',
'Red',
'SeaGreen',
'Turquoise',
'White',
'Yellow',
];

const SelectPopoverFormattedExample = createReactClass({
getInitialState() {
return {
selectedItem: undefined,
};
},

handleItemSelect(item) {
this.setState({ selectedItem: item });
},

formatItem(item) {
return (
<span>
<i className="fa fa-fw fa-square" style={{ color: item }} /> {item}
</span>
)
},

render() {
const selectedItem = this.state.selectedItem;

return (
<div>
<div style={{ display: 'inline-block', marginRight: 20 }}>
<SelectPopover id="example-popover-formatted"
title="Filter by color"
triggerNode={<Button bsStyle="info" bsSize="small">Select color</Button>}
items={items}
itemFormatter={this.formatItem}
selectedItem={selectedItem}
onItemSelect={this.handleItemSelect}
filterPlaceholder="Filter by color" />
</div>

{selectedItem ? `You have selected ${selectedItem}` : 'Please select another color!'}
</div>
);
}
});

<SelectPopoverFormattedExample />
```
11 changes: 11 additions & 0 deletions graylog2-web-interface/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4115,6 +4115,10 @@ isobject@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"

isolated-scroll@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/isolated-scroll/-/isolated-scroll-0.1.0.tgz#463126c5adaabb5b759c721498ece090d8bef7a7"

isomorphic-fetch@^2.1.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9"
Expand Down Expand Up @@ -6515,6 +6519,13 @@ react-input-autosize@^2.0.1:
create-react-class "^15.5.2"
prop-types "^15.5.8"

react-isolated-scroll@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/react-isolated-scroll/-/react-isolated-scroll-0.1.1.tgz#781faea948ced62d07624d7e39a12bccb584a5c5"
dependencies:
isolated-scroll "^0.1.0"
prop-types "^15.5.8"

react-leaflet@^1.6.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/react-leaflet/-/react-leaflet-1.8.0.tgz#e33ba704910e2ad86dd29b5a4a52acb7030fe2c4"
Expand Down

0 comments on commit abc7346

Please sign in to comment.