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} />
- +
+ onClick={onSidebarToggle}>
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`;