mirror of
https://github.com/prymitive/karma
synced 2026-05-05 03:16:51 +00:00
feat(ui): filter history menu
This commit is contained in:
45
ui/package-lock.json
generated
45
ui/package-lock.json
generated
@@ -2332,6 +2332,15 @@
|
||||
"object-assign": "4.1.1"
|
||||
}
|
||||
},
|
||||
"create-react-context": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/create-react-context/-/create-react-context-0.2.2.tgz",
|
||||
"integrity": "sha512-KkpaLARMhsTsgp0d2NA/R94F/eDLbhXERdIq3LvX2biCAXcDvHYoOqHfWCHf1+OLj+HKBotLG3KqaOOf+C1C+A==",
|
||||
"requires": {
|
||||
"fbjs": "0.8.17",
|
||||
"gud": "1.0.0"
|
||||
}
|
||||
},
|
||||
"cross-spawn": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
|
||||
@@ -4789,6 +4798,11 @@
|
||||
"resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz",
|
||||
"integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE="
|
||||
},
|
||||
"gud": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz",
|
||||
"integrity": "sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw=="
|
||||
},
|
||||
"gzip-size": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-3.0.0.tgz",
|
||||
@@ -8106,6 +8120,11 @@
|
||||
"resolved": "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz",
|
||||
"integrity": "sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow=="
|
||||
},
|
||||
"popper.js": {
|
||||
"version": "1.14.3",
|
||||
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.14.3.tgz",
|
||||
"integrity": "sha1-FDj5jQRqz3tNeM1QK/QYrGTU8JU="
|
||||
},
|
||||
"portfinder": {
|
||||
"version": "1.0.13",
|
||||
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.13.tgz",
|
||||
@@ -9652,6 +9671,19 @@
|
||||
"resolved": "https://registry.npmjs.org/react-moment/-/react-moment-0.7.9.tgz",
|
||||
"integrity": "sha512-JpHQgpB+p3oqZv583xlJakPXgqdIjGBZvngHYcFhlYxTYs0NDZSdoM1FCpU9T2QmqKOEFBVuhhJBMvJzTkF/wQ=="
|
||||
},
|
||||
"react-popper": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-popper/-/react-popper-1.0.0.tgz",
|
||||
"integrity": "sha1-uZRSFE6P5KzHf6PZWajHngemUIQ=",
|
||||
"requires": {
|
||||
"babel-runtime": "6.26.0",
|
||||
"create-react-context": "0.2.2",
|
||||
"popper.js": "1.14.3",
|
||||
"prop-types": "15.6.2",
|
||||
"typed-styles": "0.0.5",
|
||||
"warning": "3.0.0"
|
||||
}
|
||||
},
|
||||
"react-resize-detector": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-resize-detector/-/react-resize-detector-3.1.0.tgz",
|
||||
@@ -11414,6 +11446,11 @@
|
||||
"mime-types": "2.1.18"
|
||||
}
|
||||
},
|
||||
"typed-styles": {
|
||||
"version": "0.0.5",
|
||||
"resolved": "https://registry.npmjs.org/typed-styles/-/typed-styles-0.0.5.tgz",
|
||||
"integrity": "sha512-ht+rEe5UsdEBAa3gr64+QjUOqjOLJfWLvl5HZR5Ev9uo/OnD3p43wPeFSB1hNFc13GXQF/JU1Bn0YHLUqBRIlw=="
|
||||
},
|
||||
"typedarray": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
|
||||
@@ -11764,6 +11801,14 @@
|
||||
"makeerror": "1.0.11"
|
||||
}
|
||||
},
|
||||
"warning": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz",
|
||||
"integrity": "sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w=",
|
||||
"requires": {
|
||||
"loose-envify": "1.3.1"
|
||||
}
|
||||
},
|
||||
"watch": {
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/watch/-/watch-0.10.0.tgz",
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"react-linkify": "^0.2.2",
|
||||
"react-masonry-infinite": "^1.2.2",
|
||||
"react-moment": "^0.7.9",
|
||||
"react-popper": "^1.0.0",
|
||||
"react-resize-detector": "^3.0.1",
|
||||
"react-scripts": "1.1.4",
|
||||
"react-transition-group": "^2.4.0",
|
||||
|
||||
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