Skip to content

Commit

Permalink
Merge pull request #75 from newrelic/filter-bar
Browse files Browse the repository at this point in the history
feat: filter bar component
  • Loading branch information
amit-y authored Aug 10, 2023
2 parents 1fe521e + fb8c938 commit f0949cc
Show file tree
Hide file tree
Showing 21 changed files with 1,000 additions and 0 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- FilterBar component by [@amit-y](https://github.com/amit-y)

## [1.15.0] - 2023-05-18

### Added
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ A layout component for `StatusIcon`
### [ProgressBar](src/components/progress-bar)
A component that renders a progress bar.

### [FilterBar](src/components/filter-bar)
Component that allows a user to filter options.

## Utilities

### [timeRangeToNrql](src/utils/time-range-to-nrql/)
Expand Down
93 changes: 93 additions & 0 deletions src/components/filter-bar/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# FilterBar

The FilterBar component allows a user to filter from a list of options. Based on the choices of the user, the component returns a NRQL query `WHERE` clause.

## Usage

To use the FilterBar component in your project, follow these steps:

Import the component:

```jsx
import { FilterBar } from '@newrelic/nr-labs-components';
```

Use the component in your code:

```jsx
<FilterBar
options={optionsArray}
onChange={(whereClause) => fnToHandleChange()}
getValues={fnToGetAdditionalValues}
/>
```

### Props

The FilterBar component accepts the following props:

- `options` (array) - an array of option objects. See below for object properties.
- `onChange` (function) - a callback function that receives a string formatted as WHERE clauses for a NRQL query.
- `getValues` (function) - an aysnc function that fetches values that match user input.

#### `options` object

- `option` (string) - the title for the option
- `type` (string) - option type - either `string` or `numeric`
- `values` (array) - array of values for the option

#### `getValues`

The async function passed to `getvalues` is called in two scenarios.

- When an empty array is passed for `values` for an option, and the user clicks on the option to expand the list of values for that option. The function is called with just one attribute - `option` and expects an array of values to be returned.
- When the user types out a value in the search field for the option. The attributes passed are the `option` and a `searchString` formatted as a NRQL WHERE clause.

## Example

Here's an example of how to use the FilterBar component:

```jsx
import React from 'react';
import { FilterBar } from '@newrelic/nr-labs-components';

const options = [
{
option: 'responseCode',
type: 'numeric',
values: ['200', '404'],
},
{
option: 'scheme',
type: 'string',
values: ['https', 'http'],
},
// ... more filter options
];

const getValues = async (option, searchString) => {
console.log(`getValues was called for option ${option}`);
if (searchString) console.log(`SELECT attribute FROM event ${searchString}`);
// query for values and return values as an array
return [];
};

function App() {
const changeHandler = (whereClause) => {
console.log(`SELECT * FROM Transaction WHERE ${whereClause}`);
};

return (
<div>
<h1>FilterBar</h1>
<FilterBar
options={options}
onChange={changeHandler}
getValues={getValues}
/>
</div>
);
}

export default App;
```
70 changes: 70 additions & 0 deletions src/components/filter-bar/components/conjunction/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React, { useEffect, useRef, useState } from 'react';
import PropTypes from 'prop-types';

import styles from './styles.scss';

const Conjunction = ({ operator, isHint, onChange }) => {
const [showPicker, setShowPicker] = useState(false);
const thisComponent = useRef();

useEffect(() => {
function handleClicksOutsideComponent(evt) {
if (
showPicker &&
thisComponent &&
!thisComponent.current.contains(evt.target)
)
setShowPicker(false);
}
document.addEventListener('mousedown', handleClicksOutsideComponent);

return function cleanup() {
document.removeEventListener('mousedown', handleClicksOutsideComponent);
};
});

const clickHandler = (evt) => {
evt.preventDefault();
evt.stopPropagation();
setShowPicker(!showPicker);
};

const changeHandler = (selection, evt) => {
evt.preventDefault();
evt.stopPropagation();
if (onChange && selection !== operator) onChange(selection);
};

const options = ['AND', 'OR'];

return (
<span
className={`${styles.conjunction} ${isHint ? styles.hint : ''}`}
onClick={clickHandler}
ref={thisComponent}
>
{operator}
{showPicker && (
<span className={styles['conjunction-picker']}>
{options.map((opt, i) => (
<span
key={i}
className={opt === operator ? styles.selected : ''}
onClick={(evt) => changeHandler(opt, evt)}
>
{opt}
</span>
))}
</span>
)}
</span>
);
};

Conjunction.propTypes = {
operator: PropTypes.string,
isHint: PropTypes.bool,
onChange: PropTypes.func,
};

export default Conjunction;
37 changes: 37 additions & 0 deletions src/components/filter-bar/components/conjunction/styles.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
.conjunction {
padding: 1px 4px;
background: rgb(231, 233, 233);
border-radius: 3px;
margin-right: 8px;
color: rgb(83, 94, 101);
cursor: pointer;
position: relative;
display: inline-block;

&.hint {
background: rgba(231, 233, 233, 0.5);
color: rgba(83, 94, 101, 0.5);
}

.conjunction-picker {
position: absolute;
background: #ffffff;
box-shadow: 0px 4px 4px 4px rgba(0, 0, 0, 0.02),
0px 8px 16px 8px rgba(2, 3, 3, 0.05);
border-radius: 4px;
display: flex;
gap: 3px;
padding: 5px;
z-index: 11;

span {
padding: 1px 4px;
background: #ffffff;
border-radius: 3px;

&.selected {
background: #e8e8e8;
}
}
}
}
5 changes: 5 additions & 0 deletions src/components/filter-bar/components/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Conjunction from './conjunction';
import Label from './label';
import Value from './value';

export { Conjunction, Label, Value };
29 changes: 29 additions & 0 deletions src/components/filter-bar/components/label/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from 'react';
import PropTypes from 'prop-types';

import { RemoveIcon } from '../../icons';

import styles from './styles.scss';

const Label = ({ value, onRemove }) => {
const removeClickHandler = (evt) => {
evt.stopPropagation();
if (onRemove) onRemove(evt);
};

return (
<span className={styles.label}>
<span className={styles['label-text']}>{value}</span>
<span className={styles['label-remove']} onClick={removeClickHandler}>
<img src={RemoveIcon} alt="remove" />
</span>
</span>
);
};

Label.propTypes = {
value: PropTypes.string,
onRemove: PropTypes.func,
};

export default Label;
23 changes: 23 additions & 0 deletions src/components/filter-bar/components/label/styles.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
.label {
display: inline-flex;
flex-direction: row;
justify-content: center;
align-items: center;
padding: 1px 4px;
background-color: #e1edff;
border-radius: 3px;
color: #0b6acb;
margin-right: 8px;
margin-bottom: 6px;

.label-text {
flex: none;
order: 0;
flex-grow: 0;
}

.label-remove {
margin-left: 4px;
cursor: pointer;
}
}
33 changes: 33 additions & 0 deletions src/components/filter-bar/components/value/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react';
import PropTypes from 'prop-types';

import styles from './styles.scss';

const Value = ({ value, width, optionIndex, valueIndex, onChange }) => {
const changeHandler = () =>
onChange ? onChange(optionIndex, valueIndex) : null;

return (
<div className={styles['option-value']} style={{ width }}>
<input
type="checkbox"
id={`nrlabs-filter-bar-checkbox-${value.id}-${valueIndex}`}
checked={value.isSelected}
onChange={changeHandler}
/>
<label htmlFor={`nrlabs-filter-bar-checkbox-${value.id}-${valueIndex}`}>
{value.display}
</label>
</div>
);
};

Value.propTypes = {
value: PropTypes.object,
width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
optionIndex: PropTypes.number,
valueIndex: PropTypes.number,
onChange: PropTypes.func,
};

export default Value;
47 changes: 47 additions & 0 deletions src/components/filter-bar/components/value/styles.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
.option-value {
align-items: center;
display: inline-flex;
position: relative;

label {
padding-left: 8px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}

.option-picker {
position: absolute;
background: #ffffff;
box-shadow: 0px 4px 4px 4px rgba(0, 0, 0, 0.02),
0px 8px 16px 8px rgba(2, 3, 3, 0.05);
border-radius: 4px;
display: flex;
gap: 3px;
padding: 5px;
top: 100%;
z-index: 1;

span {
padding: 1px 4px;
background: #ffffff;
border-radius: 3px;
width: 18px;
height: 18px;
background-position: center center;
background-repeat: no-repeat;

&.equal {
background-image: url("data:image/svg+xml,%3Csvg width='8' height='5' viewBox='0 0 8 5' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cline x1='0.5' y1='1' x2='7.5' y2='1' stroke='%23293338' stroke-linecap='round'/%3E%3Cline x1='0.5' y1='4' x2='7.5' y2='4' stroke='%23293338' stroke-linecap='round'/%3E%3C/svg%3E");
}

&.not-equal {
background-image: url("data:image/svg+xml,%3Csvg width='8' height='9' viewBox='0 0 8 9' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cline x1='0.5' y1='2.60288' x2='7.5' y2='2.60288' stroke='%23293338' stroke-linecap='round'/%3E%3Cline x1='0.5' y1='5.60288' x2='7.5' y2='5.60288' stroke='%23293338' stroke-linecap='round'/%3E%3Cline x1='6.18301' y1='0.785895' x2='2.18301' y2='7.7141' stroke='%23293338' stroke-linecap='round'/%3E%3C/svg%3E");
}

&.selected {
background-color: #e8e8e8;
}
}
}
}
3 changes: 3 additions & 0 deletions src/components/filter-bar/icons/close.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/components/filter-bar/icons/equal.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/components/filter-bar/icons/filter.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions src/components/filter-bar/icons/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import CloseIcon from './close.svg';
import FilterByIcon from './filter.svg';
import OpenIcon from './open.svg';
import RemoveIcon from './remove.svg';
import SearchIcon from './search.svg';

export { CloseIcon, FilterByIcon, OpenIcon, RemoveIcon, SearchIcon };
5 changes: 5 additions & 0 deletions src/components/filter-bar/icons/not-equal.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/components/filter-bar/icons/open.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit f0949cc

Please sign in to comment.