mirror of
https://github.com/prymitive/karma
synced 2026-05-09 03:36:44 +00:00
feat(ui): filter history menu
This commit is contained in:
4
ui/src/Components/Labels/HistoryLabel/index.css
Normal file
4
ui/src/Components/Labels/HistoryLabel/index.css
Normal file
@@ -0,0 +1,4 @@
|
||||
.badge.components-label-history {
|
||||
/* fix align after text-truncate */
|
||||
vertical-align: middle;
|
||||
}
|
||||
28
ui/src/Components/Labels/HistoryLabel/index.js
Normal file
28
ui/src/Components/Labels/HistoryLabel/index.js
Normal 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 };
|
||||
197
ui/src/Components/NavBar/FilterInput/History.js
Normal file
197
ui/src/Components/NavBar/FilterInput/History.js
Normal 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 };
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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$/]
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user