feat(ui): filter history menu

This commit is contained in:
Łukasz Mierzwa
2018-07-07 22:41:29 +02:00
parent a8116dfcf5
commit e8fcaf1e27
8 changed files with 292 additions and 1 deletions

View File

@@ -0,0 +1,4 @@
.badge.components-label-history {
/* fix align after text-truncate */
vertical-align: middle;
}

View File

@@ -0,0 +1,28 @@
import React from "react";
import { observer } from "mobx-react";
import { BaseLabel } from "Components/Labels/BaseLabel";
import "./index.css";
const HistoryLabel = observer(
class HistoryLabel extends BaseLabel {
render() {
const { name, value } = this.props;
return (
<span
className={`components-label components-label-history text-nowrap text-truncate badge badge-${this.getColorClass(
name,
value
)} mw-100`}
style={this.getColorStyle(name, value)}
>
{name}: {value}
</span>
);
}
}
);
export { HistoryLabel };

View File

@@ -0,0 +1,197 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import { action, observable, toJS } from "mobx";
import { observer } from "mobx-react";
import { localStored } from "mobx-stored";
import { Manager, Reference, Popper } from "react-popper";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCaretDown } from "@fortawesome/free-solid-svg-icons/faCaretDown";
import { AlertStore } from "Stores/AlertStore";
import { HistoryLabel } from "Components/Labels/HistoryLabel";
const defaultHistory = {
filters: []
};
// takes a filter object out of alertStore.history.values and creates a new
// object with only those keys that will be stored in history
function reduceFilter(filter) {
return {
raw: filter.raw,
name: filter.name,
matcher: filter.matcher,
value: filter.value
};
}
const HistoryMenu = ({
popperPlacement,
popperRef,
popperStyle,
filters,
alertStore,
afterClick
}) => {
return (
<div
className="dropdown-menu d-block components-navbar-historymenu"
ref={popperRef}
style={popperStyle}
data-placement={popperPlacement}
>
{filters.length === 0 ? (
<h6 className="dropdown-header text-muted text-center">Empty</h6>
) : (
filters.map(historyFilters => (
<button
className="dropdown-item cursor-pointer px-3"
key={JSON.stringify(historyFilters.map(f => f.raw))}
onClick={() => {
alertStore.filters.setFilters(historyFilters.map(f => f.raw));
afterClick();
}}
>
<div className="components-navbar-historymenu-labels pl-2">
{historyFilters.map(f => (
<HistoryLabel
key={f.raw}
alertStore={alertStore}
name={f.name}
value={f.value}
/>
))}
</div>
</button>
))
)}
</div>
);
};
HistoryMenu.propTypes = {
popperPlacement: PropTypes.string,
popperRef: PropTypes.func,
popperStyle: PropTypes.object,
filters: PropTypes.array.isRequired,
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
afterClick: PropTypes.func.isRequired
};
const History = observer(
class History extends Component {
static propTypes = {
alertStore: PropTypes.instanceOf(AlertStore).isRequired
};
// how many filter sets do we store in local storage and render in the
// dropdown menu
maxSize = 8;
// this will be dumped to local storage via mbox-stored
history = localStored("history.filters", defaultHistory, { delay: 100 });
collapse = observable(
{
value: true,
toggle() {
this.value = !this.value;
},
hide() {
this.value = true;
}
},
{ toggle: action.bound, hide: action.bound },
{ name: "History menu toggle" }
);
appendToHistory = action(() => {
const { alertStore } = this.props;
// we don't store unapplied (we only have raw text for those, we need
// name & value for coloring) or invalid filters
const validAppliedFilters = alertStore.filters.values
.filter(f => f.applied === true && f.isValid === true)
.map(f => reduceFilter(f));
// don't store empty filters in history
if (validAppliedFilters.length === 0) return;
// make a JSON dump for comparing later with what's already stored
const filtersJSON = JSON.stringify(validAppliedFilters);
// dump observable array with stored filters to JS objects, without this
// we'll be passing around and comparing proxy objects that might mutate
// while we do so
const storedFilters = toJS(this.history.filters);
// rewrite history putting current filter set on top, this will move
// it up if user selects a filter set that was already in history
let newHistory = [
...[validAppliedFilters],
...storedFilters.filter(f => JSON.stringify(f) !== filtersJSON)
].slice(0, this.maxSize);
this.history.filters = newHistory;
});
componentDidUpdate() {
// every time this component updates we will rewrite history
// (if there are changes)
this.appendToHistory();
}
render() {
const { alertStore } = this.props;
return (
// data-filters is there to register filters for observation in mobx
// it needs to be using full filter object to notice changes to
// name & value but ignore hits
// using it this way will force re-render on every change, which is
// needed to keep track of all filter changes
<Manager
data-filters={alertStore.filters.values
.map(f => reduceFilter(f))
.join(" ")}
>
<Reference>
{({ ref }) => (
<button
ref={ref}
onClick={this.collapse.toggle}
className="btn btn-light dropdown-toggle rounded-right"
type="button"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="true"
>
<FontAwesomeIcon icon={faCaretDown} />
</button>
)}
</Reference>
<Popper
placement="top"
modifiers={{
arrow: { enabled: false }
}}
>
{this.collapse.value
? ({ placement, ref, style }) => <span />
: ({ placement, ref, style }) => (
<HistoryMenu
popperPlacement={placement}
popperRef={ref}
popperStyle={style}
filters={this.history.filters}
alertStore={alertStore}
afterClick={this.collapse.hide}
/>
)}
</Popper>
</Manager>
);
}
}
);
export { History };

View File

@@ -11,3 +11,15 @@
padding: 0;
vertical-align: middle;
}
.dropdown-menu.components-navbar-historymenu {
max-width: 400px;
}
.dropdown-menu.components-navbar-historymenu > .dropdown-item {
white-space: normal;
}
.components-navbar-historymenu-labels {
border-left: 3px solid;
}

View File

@@ -16,6 +16,7 @@ import { faSearch } from "@fortawesome/free-solid-svg-icons/faSearch";
import { AlertStore, FormatUnseeBackendURI } from "Stores/AlertStore";
import { FilterInputLabel } from "Components/Labels/FilterInputLabel";
import { AutosuggestTheme } from "./Constants";
import { History } from "./History";
import "./index.css";
@@ -124,6 +125,9 @@ const FilterInput = observer(
{...otherProps}
/>
</div>
<div className="input-group-append">
<History alertStore={alertStore} />
</div>
</div>
);
};

View File

@@ -9,7 +9,7 @@ import { App } from "./App";
if (process.env.NODE_ENV === "development") {
const { whyDidYouUpdate } = require("why-did-you-update");
whyDidYouUpdate(React, {
exclude: [/^Linkify$/, /^CSSTransition$/, /^inject-/]
exclude: [/^Linkify$/, /^CSSTransition$/, /^inject-/, /^InnerReference$/]
});
}