From 7c1ab4d5d8b063ec562e60ad258d6035bebddd68 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Tue, 18 Jul 2017 11:09:52 -0700 Subject: [PATCH 1/2] Add a results count for issues list (custom searches) --- .../sentry/app/components/QueryCount.jsx | 42 +++++++++++ src/sentry/static/sentry/app/views/stream.jsx | 13 ++++ .../sentry/app/views/stream/filters.jsx | 44 ++++++++--- .../app/views/stream/savedSearchSelector.jsx | 17 ++++- src/sentry/static/sentry/less/QueryCount.less | 7 ++ src/sentry/static/sentry/less/sentry.less | 1 + tests/js/spec/components/QueryCount.spec.js | 30 ++++++++ .../__snapshots__/QueryCount.spec.js.snap | 75 +++++++++++++++++++ 8 files changed, 214 insertions(+), 15 deletions(-) create mode 100644 src/sentry/static/sentry/app/components/QueryCount.jsx create mode 100644 src/sentry/static/sentry/less/QueryCount.less create mode 100644 tests/js/spec/components/QueryCount.spec.js create mode 100644 tests/js/spec/components/__snapshots__/QueryCount.spec.js.snap 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`; From f401bf4e3e7fed662ef5151588fcc4abd8305788 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Tue, 18 Jul 2017 13:02:43 -0700 Subject: [PATCH 2/2] Refactor jest/test npm tasks so we can run jest for a single file easily --- package.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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": {