aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/components/facet
diff options
context:
space:
mode:
authorStas Vilchik <stas.vilchik@sonarsource.com>2018-08-16 14:43:06 +0200
committerSonarTech <sonartech@sonarsource.com>2018-08-21 20:21:03 +0200
commit6a2c038752b09413a9c749b3dbfdb408a72def20 (patch)
tree74f0f3174b434713246c221de4a167cf73c4f3ae /server/sonar-web/src/main/js/components/facet
parentf91cf3ea0050655c715dc20465118a0568d4ec83 (diff)
downloadsonarqube-6a2c038752b09413a9c749b3dbfdb408a72def20.tar.gz
sonarqube-6a2c038752b09413a9c749b3dbfdb408a72def20.zip
SONAR-6961 load counts for search results (#619)
Diffstat (limited to 'server/sonar-web/src/main/js/components/facet')
-rw-r--r--server/sonar-web/src/main/js/components/facet/FacetItem.tsx10
-rw-r--r--server/sonar-web/src/main/js/components/facet/ListStyleFacet.css39
-rw-r--r--server/sonar-web/src/main/js/components/facet/ListStyleFacet.tsx182
-rw-r--r--server/sonar-web/src/main/js/components/facet/__tests__/ListStyleFacet-test.tsx37
-rw-r--r--server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/ListStyleFacet-test.tsx.snap130
5 files changed, 123 insertions, 275 deletions
diff --git a/server/sonar-web/src/main/js/components/facet/FacetItem.tsx b/server/sonar-web/src/main/js/components/facet/FacetItem.tsx
index d3f9b6711c3..fb42dba239d 100644
--- a/server/sonar-web/src/main/js/components/facet/FacetItem.tsx
+++ b/server/sonar-web/src/main/js/components/facet/FacetItem.tsx
@@ -19,14 +19,12 @@
*/
import * as React from 'react';
import * as classNames from 'classnames';
-import DeferredSpinner from '../common/DeferredSpinner';
export interface Props {
active?: boolean;
className?: string;
disabled?: boolean;
halfWidth?: boolean;
- loading?: boolean;
name: React.ReactNode;
onClick: (x: string, multiple?: boolean) => void;
stat?: React.ReactNode;
@@ -49,14 +47,6 @@ export default class FacetItem extends React.PureComponent<Props> {
};
renderValue() {
- if (this.props.loading) {
- return (
- <span className="facet-stat">
- <DeferredSpinner />
- </span>
- );
- }
-
if (this.props.stat == null) {
return null;
}
diff --git a/server/sonar-web/src/main/js/components/facet/ListStyleFacet.css b/server/sonar-web/src/main/js/components/facet/ListStyleFacet.css
deleted file mode 100644
index 5dcb7327e36..00000000000
--- a/server/sonar-web/src/main/js/components/facet/ListStyleFacet.css
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-.list-style-facet-mouse-over-animation::after {
- content: '';
- position: absolute;
- z-index: 1;
- top: 0;
- bottom: 0;
- left: 0;
- width: 0;
- background-color: var(--lightBlue);
-}
-
-.list-style-facet-mouse-over-animation:hover::after {
- width: 100%;
- transition: width 0.5s linear;
-}
-
-.list-style-facet-mouse-over-animation .facet-name,
-.list-style-facet-mouse-over-animation .facet-stat {
- z-index: 2;
-}
diff --git a/server/sonar-web/src/main/js/components/facet/ListStyleFacet.tsx b/server/sonar-web/src/main/js/components/facet/ListStyleFacet.tsx
index e96394d6701..fc62405a778 100644
--- a/server/sonar-web/src/main/js/components/facet/ListStyleFacet.tsx
+++ b/server/sonar-web/src/main/js/components/facet/ListStyleFacet.tsx
@@ -19,7 +19,6 @@
*/
import * as React from 'react';
import { sortBy, without } from 'lodash';
-import * as classNames from 'classnames';
import FacetBox from './FacetBox';
import FacetHeader from './FacetHeader';
import FacetItem from './FacetItem';
@@ -32,9 +31,13 @@ import { Paging } from '../../app/types';
import SearchBox from '../controls/SearchBox';
import ListFooter from '../controls/ListFooter';
import { formatMeasure } from '../../helpers/measures';
-import MouseOverHandler from '../controls/MouseOverHandler';
import { queriesEqual, RawQuery } from '../../helpers/query';
-import './ListStyleFacet.css';
+
+interface SearchResponse<S> {
+ maxResults?: boolean;
+ results: S[];
+ paging?: Paging;
+}
export interface Props<S> {
className?: string;
@@ -43,17 +46,14 @@ export interface Props<S> {
getFacetItemText: (item: string) => string;
getSearchResultKey: (result: S) => string;
getSearchResultText: (result: S) => string;
- loadSearchResultCount?: (result: S) => Promise<number>;
- maxInitialItems?: number;
- maxItems?: number;
- minSearchLength?: number;
+ loadSearchResultCount?: (result: S[]) => Promise<{ [x: string]: number }>;
+ maxInitialItems: number;
+ maxItems: number;
+ minSearchLength: number;
onChange: (changes: { [x: string]: string | string[] }) => void;
onClear?: () => void;
onItemClick?: (itemValue: string, multiple: boolean) => void;
- onSearch: (
- query: string,
- page?: number
- ) => Promise<{ maxResults?: boolean; results: S[]; paging?: Paging }>;
+ onSearch: (query: string, page?: number) => Promise<SearchResponse<S>>;
onToggle: (property: string) => void;
open: boolean;
property: string;
@@ -74,7 +74,6 @@ interface State<S> {
searchPaging?: Paging;
searchResults?: S[];
searchResultsCounts: { [key: string]: number };
- searchResultsCountLoading: { [key: string]: boolean };
showFullList: boolean;
}
@@ -83,7 +82,8 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S
static defaultProps = {
maxInitialItems: 15,
- maxItems: 100
+ maxItems: 100,
+ minSearchLength: 2
};
state: State<S> = {
@@ -91,7 +91,6 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S
query: '',
searching: false,
searchResultsCounts: {},
- searchResultsCountLoading: {},
showFullList: false
};
@@ -100,16 +99,6 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S
}
componentDidUpdate(prevProps: Props<S>) {
- // always remember issue counts from `stats`
- if (prevProps.stats !== this.props.stats) {
- this.setState(state => ({
- searchResultsCounts: {
- ...state.searchResultsCounts,
- ...this.props.stats
- }
- }));
- }
-
if (!prevProps.open && this.props.open) {
// focus search field *only* if it was manually open
this.setState({ autoFocus: true });
@@ -124,12 +113,11 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S
searchResults: undefined,
searching: false,
searchResultsCounts: {},
- searchResultsCountLoading: {},
showFullList: false
});
} else if (
prevProps.stats !== this.props.stats &&
- Object.keys(this.props.stats || {}).length < this.props.maxInitialItems!
+ Object.keys(this.props.stats || {}).length < this.props.maxInitialItems
) {
// show limited list if `stats` changed and there are less than 15 items
this.setState({ showFullList: false });
@@ -175,18 +163,23 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S
};
search = (query: string) => {
- if (query.length >= 2) {
+ if (query.length >= this.props.minSearchLength) {
this.setState({ query, searching: true });
- this.props.onSearch(query).then(({ maxResults, paging, results }) => {
- if (this.mounted) {
- this.setState({
- searching: false,
- searchMaxResults: maxResults,
- searchResults: results,
- searchPaging: paging
- });
- }
- }, this.stopSearching);
+ this.props
+ .onSearch(query)
+ .then(this.loadCountsForSearchResults)
+ .then(({ maxResults, paging, results, stats }) => {
+ if (this.mounted) {
+ this.setState(state => ({
+ searching: false,
+ searchMaxResults: maxResults,
+ searchResults: results,
+ searchPaging: paging,
+ searchResultsCounts: { ...state.searchResultsCounts, ...stats }
+ }));
+ }
+ })
+ .catch(this.stopSearching);
} else {
this.setState({ query, searching: false, searchResults: [] });
}
@@ -196,56 +189,33 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S
const { query, searchPaging, searchResults } = this.state;
if (query && searchResults && searchPaging) {
this.setState({ searching: true });
- this.props.onSearch(query, searchPaging.pageIndex + 1).then(({ paging, results }) => {
- if (this.mounted) {
- this.setState({
- searching: false,
- searchResults: [...searchResults, ...results],
- searchPaging: paging
- });
- }
- }, this.stopSearching);
- }
- };
-
- handleSearchResultMouseOver = (result: S) => {
- if (
- this.props.loadSearchResultCount &&
- this.state.searchResultsCounts[this.props.getSearchResultKey(result)] === undefined
- ) {
- this.setState(state => ({
- searchResultsCountLoading: {
- ...state.searchResultsCountLoading,
- [this.props.getSearchResultKey(result)]: true
- }
- }));
-
- this.props.loadSearchResultCount(result).then(
- count => {
- if (this.mounted) {
- this.setState(state => ({
- searchResultsCounts: {
- ...state.searchResultsCounts,
- [this.props.getSearchResultKey(result)]: count
- },
- searchResultsCountLoading: {
- ...state.searchResultsCountLoading,
- [this.props.getSearchResultKey(result)]: false
- }
- }));
- }
- },
- () => {
+ this.props
+ .onSearch(query, searchPaging.pageIndex + 1)
+ .then(this.loadCountsForSearchResults)
+ .then(({ paging, results, stats }) => {
if (this.mounted) {
this.setState(state => ({
- searchResultsCountLoading: {
- ...state.searchResultsCountLoading,
- [this.props.getSearchResultKey(result)]: false
- }
+ searching: false,
+ searchResults: [...searchResults, ...results],
+ searchPaging: paging,
+ searchResultsCounts: { ...state.searchResultsCounts, ...stats }
}));
}
- }
- );
+ })
+ .catch(this.stopSearching);
+ }
+ };
+
+ loadCountsForSearchResults = (response: SearchResponse<S>) => {
+ const { loadSearchResultCount = () => Promise.resolve({}) } = this.props;
+ const resultsToLoad = response.results.filter(result => {
+ const key = this.props.getSearchResultKey(result);
+ return this.getStat(key) === undefined && this.state.searchResultsCounts[key] === undefined;
+ });
+ if (resultsToLoad.length > 0) {
+ return loadSearchResultCount(resultsToLoad).then(stats => ({ ...response, stats }));
+ } else {
+ return { ...response, stats: {} };
}
};
@@ -285,12 +255,12 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S
// limit the number of items to this.props.maxInitialItems,
// but make sure all (in other words, the last) selected items are displayed
const lastSelectedIndex = this.getLastActiveIndex(sortedItems);
- const countToDisplay = Math.max(this.props.maxInitialItems!, lastSelectedIndex + 1);
+ const countToDisplay = Math.max(this.props.maxInitialItems, lastSelectedIndex + 1);
const limitedList = this.state.showFullList
? sortedItems
: sortedItems.slice(0, countToDisplay);
- const mightHaveMoreResults = sortedItems.length >= this.props.maxItems!;
+ const mightHaveMoreResults = sortedItems.length >= this.props.maxItems;
return (
<>
@@ -328,14 +298,12 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S
return null;
}
- const { minSearchLength = 2 } = this.props;
-
return (
<SearchBox
autoFocus={this.state.autoFocus}
className="little-spacer-top spacer-bottom"
loading={this.state.searching}
- minLength={minSearchLength}
+ minLength={this.props.minSearchLength}
onChange={this.search}
placeholder={this.props.searchPlaceholder}
value={this.state.query}
@@ -381,31 +349,13 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S
renderSearchResult(result: S) {
const key = this.props.getSearchResultKey(result);
const active = this.props.values.includes(key);
-
- // default to 0 if we're sure there are not more results
- const isFacetExhaustive = Object.keys(this.props.stats || {}).length < this.props.maxItems!;
-
- let stat: number | undefined = this.getStat(key);
- let disabled = isFacetExhaustive && !active && stat === 0;
- if (stat === undefined) {
- stat = this.state.searchResultsCounts[key];
- disabled = false; // do not disable facet if the count was requested after mouse over
- }
- if (stat === undefined && isFacetExhaustive) {
- stat = 0;
- disabled = !active;
- }
-
- const loading = this.state.searchResultsCountLoading[key];
- const canBeLoaded =
- !loading && this.props.loadSearchResultCount !== undefined && stat === undefined;
-
- const facetItem = (
+ const stat = this.getStat(key) || this.state.searchResultsCounts[key];
+ const disabled = !active && stat === 0;
+ return (
<FacetItem
active={active}
- className={classNames({ 'list-style-facet-mouse-over-animation': canBeLoaded })}
disabled={disabled}
- loading={loading}
+ key={key}
name={this.props.renderSearchResult(result, this.state.query)}
onClick={this.handleItemClick}
stat={formatFacetStat(stat)}
@@ -413,18 +363,6 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S
value={key}
/>
);
-
- return (
- <React.Fragment key={key}>
- {canBeLoaded ? (
- <MouseOverHandler delay={500} onOver={() => this.handleSearchResultMouseOver(result)}>
- {facetItem}
- </MouseOverHandler>
- ) : (
- facetItem
- )}
- </React.Fragment>
- );
}
render() {
diff --git a/server/sonar-web/src/main/js/components/facet/__tests__/ListStyleFacet-test.tsx b/server/sonar-web/src/main/js/components/facet/__tests__/ListStyleFacet-test.tsx
index 553f1ca7796..d7290b74f44 100644
--- a/server/sonar-web/src/main/js/components/facet/__tests__/ListStyleFacet-test.tsx
+++ b/server/sonar-web/src/main/js/components/facet/__tests__/ListStyleFacet-test.tsx
@@ -76,19 +76,22 @@ it('should search', async () => {
results: ['d', 'e'],
paging: { pageIndex: 1, pageSize: 2, total: 3 }
});
- const wrapper = shallowRender({ onSearch });
+ const loadSearchResultCount = jest.fn().mockResolvedValue({ d: 7, e: 3 });
+ const wrapper = shallowRender({ loadSearchResultCount, onSearch });
// search
wrapper.find('SearchBox').prop<Function>('onChange')('query');
await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot();
expect(onSearch).lastCalledWith('query');
+ expect(loadSearchResultCount).lastCalledWith(['d', 'e']);
// load more results
onSearch.mockResolvedValue({
results: ['f'],
paging: { pageIndex: 2, pageSize: 2, total: 3 }
});
+ loadSearchResultCount.mockResolvedValue({ f: 5 });
wrapper.find('ListFooter').prop<Function>('loadMore')();
await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot();
@@ -96,10 +99,12 @@ it('should search', async () => {
// clear search
onSearch.mockClear();
+ loadSearchResultCount.mockClear();
wrapper.find('SearchBox').prop<Function>('onChange')('');
await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot();
expect(onSearch).not.toBeCalled();
+ expect(loadSearchResultCount).not.toBeCalled();
// search for no results
onSearch.mockResolvedValue({ results: [], paging: { pageIndex: 1, pageSize: 2, total: 0 } });
@@ -107,6 +112,7 @@ it('should search', async () => {
await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot();
expect(onSearch).lastCalledWith('blabla');
+ expect(loadSearchResultCount).not.toBeCalled();
// search fails
onSearch.mockRejectedValue(undefined);
@@ -114,6 +120,7 @@ it('should search', async () => {
await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot(); // should render previous results
expect(onSearch).lastCalledWith('blabla');
+ expect(loadSearchResultCount).not.toBeCalled();
});
it('should limit the number of items', () => {
@@ -164,33 +171,6 @@ it('should collapse list when new stats have few results', () => {
expect(wrapper.state('showFullList')).toBe(false);
});
-it('should load count on mouse over', async () => {
- const loadSearchResultCount = jest.fn().mockResolvedValue(5);
- const onSearch = jest.fn().mockResolvedValue({
- results: ['d', 'e'],
- paging: { pageIndex: 1, pageSize: 2, total: 3 }
- });
- const wrapper = shallowRender({ loadSearchResultCount, maxItems: 1, onSearch });
-
- // search
- wrapper.find('SearchBox').prop<Function>('onChange')('query');
- await waitAndUpdate(wrapper);
-
- expect(firstFacetItem().prop('stat')).toBeUndefined();
- wrapper
- .find('MouseOverHandler')
- .first()
- .prop<Function>('onOver')();
- expect(loadSearchResultCount).toBeCalledWith('d');
- await waitAndUpdate(wrapper);
-
- expect(firstFacetItem().prop('stat')).toBe('5');
-
- function firstFacetItem() {
- return wrapper.find('FacetItem').first();
- }
-});
-
function shallowRender(props: Partial<Props<string>> = {}) {
return shallow(
<ListStyleFacet
@@ -223,6 +203,5 @@ function checkInitialState(wrapper: ShallowWrapper) {
expect(wrapper.state('searchResults')).toBe(undefined);
expect(wrapper.state('searching')).toBe(false);
expect(wrapper.state('searchResultsCounts')).toEqual({});
- expect(wrapper.state('searchResultsCountLoading')).toEqual({});
expect(wrapper.state('showFullList')).toBe(false);
}
diff --git a/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/ListStyleFacet-test.tsx.snap b/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/ListStyleFacet-test.tsx.snap
index 0ec32826fd8..c5953ef7031 100644
--- a/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/ListStyleFacet-test.tsx.snap
+++ b/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/ListStyleFacet-test.tsx.snap
@@ -105,38 +105,30 @@ exports[`should search 1`] = `
/>
<React.Fragment>
<FacetItemsList>
- <React.Fragment
+ <FacetItem
+ active={false}
+ disabled={false}
+ halfWidth={false}
key="d"
- >
- <FacetItem
- active={false}
- className=""
- disabled={true}
- halfWidth={false}
- loading={false}
- name="d"
- onClick={[Function]}
- stat={0}
- tooltip="d"
- value="d"
- />
- </React.Fragment>
- <React.Fragment
+ loading={false}
+ name="d"
+ onClick={[Function]}
+ stat="7"
+ tooltip="d"
+ value="d"
+ />
+ <FacetItem
+ active={false}
+ disabled={false}
+ halfWidth={false}
key="e"
- >
- <FacetItem
- active={false}
- className=""
- disabled={true}
- halfWidth={false}
- loading={false}
- name="e"
- onClick={[Function]}
- stat={0}
- tooltip="e"
- value="e"
- />
- </React.Fragment>
+ loading={false}
+ name="e"
+ onClick={[Function]}
+ stat="3"
+ tooltip="e"
+ value="e"
+ />
</FacetItemsList>
<ListFooter
className="spacer-bottom"
@@ -181,54 +173,42 @@ exports[`should search 2`] = `
/>
<React.Fragment>
<FacetItemsList>
- <React.Fragment
+ <FacetItem
+ active={false}
+ disabled={false}
+ halfWidth={false}
key="d"
- >
- <FacetItem
- active={false}
- className=""
- disabled={true}
- halfWidth={false}
- loading={false}
- name="d"
- onClick={[Function]}
- stat={0}
- tooltip="d"
- value="d"
- />
- </React.Fragment>
- <React.Fragment
+ loading={false}
+ name="d"
+ onClick={[Function]}
+ stat="7"
+ tooltip="d"
+ value="d"
+ />
+ <FacetItem
+ active={false}
+ disabled={false}
+ halfWidth={false}
key="e"
- >
- <FacetItem
- active={false}
- className=""
- disabled={true}
- halfWidth={false}
- loading={false}
- name="e"
- onClick={[Function]}
- stat={0}
- tooltip="e"
- value="e"
- />
- </React.Fragment>
- <React.Fragment
+ loading={false}
+ name="e"
+ onClick={[Function]}
+ stat="3"
+ tooltip="e"
+ value="e"
+ />
+ <FacetItem
+ active={false}
+ disabled={false}
+ halfWidth={false}
key="f"
- >
- <FacetItem
- active={false}
- className=""
- disabled={true}
- halfWidth={false}
- loading={false}
- name="f"
- onClick={[Function]}
- stat={0}
- tooltip="f"
- value="f"
- />
- </React.Fragment>
+ loading={false}
+ name="f"
+ onClick={[Function]}
+ stat="5"
+ tooltip="f"
+ value="f"
+ />
</FacetItemsList>
<ListFooter
className="spacer-bottom"