diff --git a/package.json b/package.json
index 5946041a2e6f4f..e0fd77ae3d4e56 100644
--- a/package.json
+++ b/package.json
@@ -72,9 +72,10 @@
"APIMethod": "stub",
"proxyURL": "http://localhost:8000",
"scripts": {
- "test": "NODE_ENV=test node_modules/.bin/jest tests/js/spec",
- "test-ci": "NODE_ENV=test node_modules/.bin/jest tests/js/spec --runInBand",
- "test-watch": "NODE_ENV=test node_modules/.bin/jest tests/js/spec --watch",
+ "test": "npm run jest -- tests/js/spec",
+ "test-ci": "npm run test -- --runInBand",
+ "test-watch": "npm run test -- --watch",
+ "jest": "NODE_ENV=test node_modules/.bin/jest",
"lint": "node_modules/.bin/eslint tests/js src/sentry/static/sentry/app --ext .jsx"
},
"jest": {
diff --git a/src/sentry/static/sentry/app/components/QueryCount.jsx b/src/sentry/static/sentry/app/components/QueryCount.jsx
new file mode 100644
index 00000000000000..cb1e3a23b87b71
--- /dev/null
+++ b/src/sentry/static/sentry/app/components/QueryCount.jsx
@@ -0,0 +1,42 @@
+import React, {PropTypes} from 'react';
+import classNames from 'classnames';
+
+/**
+ * Displays a number count. If `max` is specified, then give representation
+ * of count, i.e. "1000+"
+ *
+ * Render nothing by default if `count` is falsy.
+ */
+function QueryCount({className, count, max, hideIfEmpty, inline}) {
+ const countOrMax = typeof max !== 'undefined' && count >= max ? `${max}+` : count;
+ const cx = classNames('query-count', className, {
+ inline
+ });
+
+ if (hideIfEmpty && !count) {
+ return null;
+ }
+
+ return (
+
+ (
+
+ {countOrMax}
+
+ )
+
+ );
+}
+QueryCount.propTypes = {
+ className: PropTypes.string,
+ count: PropTypes.number,
+ max: PropTypes.number,
+ hideIfEmpty: PropTypes.bool,
+ inline: PropTypes.bool
+};
+QueryCount.defaultProps = {
+ hideIfEmpty: true,
+ inline: true
+};
+
+export default QueryCount;
diff --git a/src/sentry/static/sentry/app/views/stream.jsx b/src/sentry/static/sentry/app/views/stream.jsx
index e1bea71ac72f57..59dfe52e8dd732 100644
--- a/src/sentry/static/sentry/app/views/stream.jsx
+++ b/src/sentry/static/sentry/app/views/stream.jsx
@@ -72,6 +72,7 @@ const Stream = React.createClass({
statsPeriod: this.props.defaultStatsPeriod,
realtimeActive,
pageLinks: '',
+ queryCount: null,
dataLoading: true,
error: false,
query: '',
@@ -325,6 +326,7 @@ const Stream = React.createClass({
this.setState({
dataLoading: true,
+ queryCount: null,
error: false
});
@@ -372,9 +374,18 @@ const Stream = React.createClass({
this._streamManager.push(data);
+ let queryCount = jqXHR.getResponseHeader('X-Hits');
+ let queryMaxCount = jqXHR.getResponseHeader('X-Max-Hits');
+
return void this.setState({
error: false,
dataLoading: false,
+ queryCount: typeof queryCount !== 'undefined'
+ ? parseInt(queryCount, 10) || 0
+ : 0,
+ queryMaxCount: typeof queryMaxCount !== 'undefined'
+ ? parseInt(queryMaxCount, 10) || 0
+ : 0,
pageLinks: jqXHR.getResponseHeader('Link')
});
},
@@ -703,6 +714,8 @@ const Stream = React.createClass({
sort={this.state.sort}
tags={this.state.tags}
searchId={searchId}
+ queryCount={this.state.queryCount}
+ queryMaxCount={this.state.queryMaxCount}
defaultQuery={this.props.defaultQuery}
onSortChange={this.onSortChange}
onSearch={this.onSearch}
diff --git a/src/sentry/static/sentry/app/views/stream/filters.jsx b/src/sentry/static/sentry/app/views/stream/filters.jsx
index ee9b44d4330909..518ba42ee01be6 100644
--- a/src/sentry/static/sentry/app/views/stream/filters.jsx
+++ b/src/sentry/static/sentry/app/views/stream/filters.jsx
@@ -20,6 +20,8 @@ const StreamFilters = React.createClass({
filter: React.PropTypes.string,
query: React.PropTypes.string,
isSearchDisabled: React.PropTypes.bool,
+ queryCount: React.PropTypes.number,
+ queryMaxCount: React.PropTypes.number,
onSortChange: React.PropTypes.func,
onSearch: React.PropTypes.func,
@@ -44,7 +46,25 @@ const StreamFilters = React.createClass({
},
render() {
- let {access, orgId, projectId, searchId} = this.props;
+ let {
+ access,
+ orgId,
+ projectId,
+ searchId,
+ queryCount,
+ queryMaxCount,
+ query,
+ savedSearchList,
+ tags,
+ defaultQuery,
+ isSearchDisabled,
+ sort,
+
+ onSidebarToggle,
+ onSearch,
+ onSavedSearchCreate,
+ onSortChange
+ } = this.props;
return (
@@ -55,31 +75,33 @@ const StreamFilters = React.createClass({
orgId={orgId}
projectId={projectId}
searchId={searchId}
- query={this.props.query}
- onSavedSearchCreate={this.props.onSavedSearchCreate}
- savedSearchList={this.props.savedSearchList}
+ queryCount={queryCount}
+ queryMaxCount={queryMaxCount}
+ query={query}
+ onSavedSearchCreate={onSavedSearchCreate}
+ savedSearchList={savedSearchList}
/>
diff --git a/src/sentry/static/sentry/app/views/stream/savedSearchSelector.jsx b/src/sentry/static/sentry/app/views/stream/savedSearchSelector.jsx
index d22084e6216ec4..6fe4c572a0223d 100644
--- a/src/sentry/static/sentry/app/views/stream/savedSearchSelector.jsx
+++ b/src/sentry/static/sentry/app/views/stream/savedSearchSelector.jsx
@@ -2,11 +2,12 @@ import React from 'react';
import Modal from 'react-bootstrap/lib/Modal';
import {Link} from 'react-router';
+import {t} from '../../locale';
import ApiMixin from '../../mixins/apiMixin';
-import DropdownLink from '../../components/dropdownLink';
import IndicatorStore from '../../stores/indicatorStore';
+import DropdownLink from '../../components/dropdownLink';
+import QueryCount from '../../components/QueryCount';
import MenuItem from '../../components/menuItem';
-import {t} from '../../locale';
import {BooleanField, FormState, TextField} from '../../components/forms';
const SaveSearchButton = React.createClass({
@@ -186,6 +187,8 @@ const SavedSearchSelector = React.createClass({
searchId: React.PropTypes.string,
access: React.PropTypes.object.isRequired,
savedSearchList: React.PropTypes.array.isRequired,
+ queryCount: React.PropTypes.number,
+ queryMaxCount: React.PropTypes.number,
onSavedSearchCreate: React.PropTypes.func.isRequired
},
@@ -201,7 +204,7 @@ const SavedSearchSelector = React.createClass({
},
render() {
- let {access, orgId, projectId} = this.props;
+ let {access, orgId, projectId, queryCount, queryMaxCount} = this.props;
let children = this.props.savedSearchList.map(search => {
// TODO(dcramer): we want these to link directly to the saved
// search ID, and pass that into the backend (probably)
@@ -214,7 +217,13 @@ const SavedSearchSelector = React.createClass({
});
return (
-
+
+ {this.getTitle()}
+
+
+ }>
{children.length
? children
:
diff --git a/src/sentry/static/sentry/less/QueryCount.less b/src/sentry/static/sentry/less/QueryCount.less
new file mode 100644
index 00000000000000..deefbd75f11bb7
--- /dev/null
+++ b/src/sentry/static/sentry/less/QueryCount.less
@@ -0,0 +1,7 @@
+.query-count {
+ margin-left: 4px;
+
+ &.inline {
+ display: inline-block;
+ }
+}
diff --git a/src/sentry/static/sentry/less/sentry.less b/src/sentry/static/sentry/less/sentry.less
index a433391b1b7512..76eada45a501ae 100644
--- a/src/sentry/static/sentry/less/sentry.less
+++ b/src/sentry/static/sentry/less/sentry.less
@@ -45,3 +45,4 @@
@import url("./setup-wizard.less");
@import url("./spacing.less");
@import url("./stream.less");
+@import url("./QueryCount.less");
diff --git a/tests/js/spec/components/QueryCount.spec.js b/tests/js/spec/components/QueryCount.spec.js
new file mode 100644
index 00000000000000..1779bbb67b7657
--- /dev/null
+++ b/tests/js/spec/components/QueryCount.spec.js
@@ -0,0 +1,30 @@
+import React from 'react';
+import {shallow} from 'enzyme';
+import QueryCount from 'app/components/QueryCount';
+import toJson from 'enzyme-to-json';
+
+describe('QueryCount', function() {
+ it('displays count when no max', function() {
+ const wrapper = shallow();
+ expect(toJson(wrapper)).toMatchSnapshot();
+ });
+ it('displays count when count < max', function() {
+ const wrapper = shallow();
+ expect(toJson(wrapper)).toMatchSnapshot();
+ });
+
+ it('does not render if count is 0', function() {
+ const wrapper = shallow();
+ expect(toJson(wrapper)).toMatchSnapshot();
+ });
+
+ it('can render when count is 0 when `hideIfEmpty` is false', function() {
+ const wrapper = shallow();
+ expect(toJson(wrapper)).toMatchSnapshot();
+ });
+
+ it('displays max count if count >= max', function() {
+ const wrapper = shallow();
+ expect(toJson(wrapper)).toMatchSnapshot();
+ });
+});
diff --git a/tests/js/spec/components/__snapshots__/QueryCount.spec.js.snap b/tests/js/spec/components/__snapshots__/QueryCount.spec.js.snap
new file mode 100644
index 00000000000000..25f20352cc96a5
--- /dev/null
+++ b/tests/js/spec/components/__snapshots__/QueryCount.spec.js.snap
@@ -0,0 +1,75 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`QueryCount can render when count is 0 when \`hideIfEmpty\` is false 1`] = `
+
+
+ (
+
+
+ 0
+
+
+ )
+
+
+`;
+
+exports[`QueryCount displays count when count < max 1`] = `
+
+
+ (
+
+
+ 5
+
+
+ )
+
+
+`;
+
+exports[`QueryCount displays count when no max 1`] = `
+
+
+ (
+
+
+ 5
+
+
+ )
+
+
+`;
+
+exports[`QueryCount displays max count if count >= max 1`] = `
+
+
+ (
+
+
+ 500+
+
+
+ )
+
+
+`;
+
+exports[`QueryCount does not render if count is 0 1`] = `null`;