diff options
author | Stas Vilchik <stas.vilchik@sonarsource.com> | 2018-08-14 16:12:56 +0200 |
---|---|---|
committer | SonarTech <sonartech@sonarsource.com> | 2018-08-21 20:21:02 +0200 |
commit | 63055cd49b4053c4f99949f505f6ce1214cb4135 (patch) | |
tree | 77ec3f0ef3410ce84155da6d5af27132cbde8b56 /server/sonar-web/src/main/js/apps/issues/sidebar | |
parent | c9d8fb12afc55512508c55f4026fbad3797c0439 (diff) | |
download | sonarqube-63055cd49b4053c4f99949f505f6ce1214cb4135.tar.gz sonarqube-63055cd49b4053c4f99949f505f6ce1214cb4135.zip |
SONAR-6961 Add issue counts to search in rule facet on issue page (#612)
Diffstat (limited to 'server/sonar-web/src/main/js/apps/issues/sidebar')
21 files changed, 310 insertions, 923 deletions
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.tsx index d7b83bf6228..656f74bc72c 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.tsx @@ -18,101 +18,32 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { sortBy, uniq, without } from 'lodash'; -import { - searchAssignees, - formatFacetStat, - Query, - ReferencedUser, - SearchedAssignee -} from '../utils'; -import { Component, Paging } from '../../../app/types'; -import FacetBox from '../../../components/facet/FacetBox'; -import FacetHeader from '../../../components/facet/FacetHeader'; -import FacetItem from '../../../components/facet/FacetItem'; -import FacetItemsList from '../../../components/facet/FacetItemsList'; +import { omit, sortBy, without } from 'lodash'; +import { searchAssignees, Query, ReferencedUser, SearchedAssignee } from '../utils'; +import { Component } from '../../../app/types'; import Avatar from '../../../components/ui/Avatar'; import { translate } from '../../../helpers/l10n'; -import DeferredSpinner from '../../../components/common/DeferredSpinner'; -import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHint'; -import SearchBox from '../../../components/controls/SearchBox'; -import ListFooter from '../../../components/controls/ListFooter'; import { highlightTerm } from '../../../helpers/search'; +import ListStyleFacet from '../../../components/facet/ListStyleFacet'; export interface Props { assigned: boolean; assignees: string[]; component: Component | undefined; fetching: boolean; - loading?: boolean; + loadSearchResultCount: (changes: Partial<Query>) => Promise<number>; onChange: (changes: Partial<Query>) => void; onToggle: (property: string) => void; open: boolean; organization: string | undefined; + query: Query; stats: { [x: string]: number } | undefined; referencedUsers: { [login: string]: ReferencedUser }; } -interface State { - query: string; - searching: boolean; - searchResults?: SearchedAssignee[]; - searchPaging?: Paging; -} - -export default class AssigneeFacet extends React.PureComponent<Props, State> { - mounted = false; - property = 'assignees'; - - state: State = { - query: '', - searching: false - }; - - componentDidMount() { - this.mounted = true; - } - - componentWillUnmount() { - this.mounted = false; - } - - stopSearching = () => { - if (this.mounted) { - this.setState({ searching: false }); - } - }; - - search = (query: string) => { - if (query.length >= 2) { - this.setState({ query, searching: true }); - searchAssignees(query, this.props.organization).then(({ paging, results }) => { - if (this.mounted) { - this.setState({ searching: false, searchResults: results, searchPaging: paging }); - } - }, this.stopSearching); - } else { - this.setState({ query, searching: false, searchResults: [] }); - } - }; - - searchMore = () => { - const { query, searchPaging, searchResults } = this.state; - if (query && searchResults && searchPaging) { - this.setState({ searching: true }); - searchAssignees(query, this.props.organization, searchPaging.pageIndex + 1).then( - ({ paging, results }) => { - if (this.mounted) { - this.setState({ - searching: false, - searchResults: [...searchResults, ...results], - searchPaging: paging - }); - } - }, - this.stopSearching - ); - } +export default class AssigneeFacet extends React.PureComponent<Props> { + handleSearch = (query: string, page?: number) => { + return searchAssignees(query, this.props.organization, page); }; handleItemClick = (itemValue: string, multiple: boolean) => { @@ -124,221 +55,105 @@ export default class AssigneeFacet extends React.PureComponent<Props, State> { const newValue = sortBy( assignees.includes(itemValue) ? without(assignees, itemValue) : [...assignees, itemValue] ); - this.props.onChange({ assigned: true, [this.property]: newValue }); + this.props.onChange({ assigned: true, assignees: newValue }); } else { this.props.onChange({ assigned: true, - [this.property]: assignees.includes(itemValue) && assignees.length < 2 ? [] : [itemValue] + assignees: assignees.includes(itemValue) && assignees.length < 2 ? [] : [itemValue] }); } }; - handleHeaderClick = () => { - this.props.onToggle(this.property); - }; - handleClear = () => { this.props.onChange({ assigned: true, assignees: [] }); }; - handleSelect = (option: { value: string }) => { - const { assignees } = this.props; - this.props.onChange({ assigned: true, [this.property]: uniq([...assignees, option.value]) }); - }; - - isAssigneeActive(assignee: string) { - return assignee === '' ? !this.props.assigned : this.props.assignees.includes(assignee); - } - - getAssigneeNameAndTooltip(assignee: string) { + getAssigneeName = (assignee: string) => { if (assignee === '') { - return { name: translate('unassigned'), tooltip: translate('unassigned') }; + return translate('unassigned'); } else { - const { referencedUsers } = this.props; - if (referencedUsers[assignee]) { - return { - name: ( - <span> - <Avatar - className="little-spacer-right" - hash={referencedUsers[assignee].avatar} - name={referencedUsers[assignee].name} - size={16} - /> - {referencedUsers[assignee].name} - </span> - ), - tooltip: referencedUsers[assignee].name - }; - } else { - return { name: assignee, tooltip: assignee }; - } - } - } - - getStat(assignee: string) { - const { stats } = this.props; - return stats ? stats[assignee] : undefined; - } - - getValues() { - const values = this.props.assignees.map(assignee => { const user = this.props.referencedUsers[assignee]; return user ? user.name : assignee; - }); - if (!this.props.assigned) { - values.push(translate('unassigned')); } - return values; - } - - renderOption = (option: { avatar: string; label: string }) => { - return this.renderAssignee(option.avatar, option.label); }; - renderAssignee = (avatar: string | undefined, name: string) => ( - <span> - {avatar !== undefined && ( - <Avatar className="little-spacer-right" hash={avatar} name={name} size={16} /> - )} - {name} - </span> - ); - - renderListItem(assignee: string) { - const { name, tooltip } = this.getAssigneeNameAndTooltip(assignee); - return ( - <FacetItem - active={this.isAssigneeActive(assignee)} - key={assignee} - loading={this.props.loading} - name={name} - onClick={this.handleItemClick} - stat={formatFacetStat(this.getStat(assignee))} - tooltip={tooltip} - value={assignee} - /> - ); - } - - renderList() { - const { stats } = this.props; - - if (!stats) { - return null; - } + loadSearchResultCount = (assignee: SearchedAssignee) => { + return this.props.loadSearchResultCount({ assigned: undefined, assignees: [assignee.login] }); + }; - const assignees = sortBy( + getSortedItems = () => { + const { stats = {} } = this.props; + return sortBy( Object.keys(stats), - // put unassigned first + // put "not assigned" first key => (key === '' ? 0 : 1), // the sort by number key => -stats[key] ); + }; - return ( - <FacetItemsList>{assignees.map(assignee => this.renderListItem(assignee))}</FacetItemsList> - ); - } - - renderSearch() { - if (!this.props.stats || !Object.keys(this.props.stats).length) { - return null; - } - - return ( - <SearchBox - autoFocus={true} - className="little-spacer-top spacer-bottom" - loading={this.state.searching} - minLength={2} - onChange={this.search} - placeholder={translate('search.search_for_users')} - value={this.state.query} - /> - ); - } - - renderSearchResults() { - const { searching, searchResults, searchPaging } = this.state; - - if (!searching && (!searchResults || !searchResults.length)) { - return <div className="note spacer-bottom">{translate('no_results')}</div>; - } - - if (!searchResults || !searchPaging) { - // initial search - return null; + renderFacetItem = (assignee: string) => { + if (assignee === '') { + return translate('unassigned'); } - return ( + const user = this.props.referencedUsers[assignee]; + return user ? ( <> - <FacetItemsList> - {searchResults.map(result => this.renderSearchResult(result))} - </FacetItemsList> - <ListFooter - count={searchResults.length} - loadMore={this.searchMore} - ready={!searching} - total={searchPaging.total} - /> + <Avatar className="little-spacer-right" hash={user.avatar} name={user.name} size={16} /> + {user.name} </> + ) : ( + assignee ); - } + }; - renderSearchResult(result: SearchedAssignee) { - const active = this.props.assignees.includes(result.login); - const stat = this.getStat(result.login); + renderSearchResult = (result: SearchedAssignee, query: string) => { return ( - <FacetItem - active={active} - disabled={!active && stat === 0} - key={result.login} - loading={this.props.loading} - name={ - <> - {result.avatar !== undefined && ( - <Avatar - className="little-spacer-right" - hash={result.avatar} - name={result.name} - size={16} - /> - )} - {highlightTerm(result.name, this.state.query)} - </> - } - onClick={this.handleItemClick} - stat={stat && formatFacetStat(stat)} - tooltip={result.name} - value={result.login} - /> + <> + {result.avatar !== undefined && ( + <Avatar + className="little-spacer-right" + hash={result.avatar} + name={result.name} + size={16} + /> + )} + {highlightTerm(result.name, query)} + </> ); - } + }; render() { - const { assignees, stats = {} } = this.props; - return ( - <FacetBox property={this.property}> - <FacetHeader - name={translate('issues.facet', this.property)} - onClear={this.handleClear} - onClick={this.handleHeaderClick} - open={this.props.open} - values={this.getValues()} - /> + const values = [...this.props.assignees]; + if (!this.props.assigned) { + values.push(''); + } - <DeferredSpinner loading={this.props.fetching} /> - {this.props.open && ( - <> - {this.renderSearch()} - {this.state.query && this.state.searchResults !== undefined - ? this.renderSearchResults() - : this.renderList()} - <MultipleSelectionHint options={Object.keys(stats).length} values={assignees.length} /> - </> - )} - </FacetBox> + return ( + <ListStyleFacet<SearchedAssignee> + facetHeader={translate('issues.facet.assignees')} + fetching={this.props.fetching} + getFacetItemText={this.getAssigneeName} + getSearchResultKey={user => user.login} + getSearchResultText={user => user.name} + // put "not assigned" item first + getSortedItems={this.getSortedItems} + loadSearchResultCount={this.loadSearchResultCount} + onChange={this.props.onChange} + onClear={this.handleClear} + onItemClick={this.handleItemClick} + onSearch={this.handleSearch} + onToggle={this.props.onToggle} + open={this.props.open} + property="assignees" + query={omit(this.props.query, 'assigned', 'assignees')} + renderFacetItem={this.renderFacetItem} + renderSearchResult={this.renderSearchResult} + searchPlaceholder={translate('search.search_for_users')} + stats={this.props.stats} + values={values} + /> ); } } diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.tsx index 301506b60a1..27d4fbd8355 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.tsx @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import { omit } from 'lodash'; import { Query } from '../utils'; import { translate } from '../../../helpers/l10n'; import ListStyleFacet from '../../../components/facet/ListStyleFacet'; @@ -27,11 +28,12 @@ import { highlightTerm } from '../../../helpers/search'; interface Props { componentKey: string | undefined; fetching: boolean; - loading?: boolean; + loadSearchResultCount: (changes: Partial<Query>) => Promise<number>; onChange: (changes: Partial<Query>) => void; onToggle: (property: string) => void; open: boolean; organization: string | undefined; + query: Query; stats: { [x: string]: number } | undefined; authors: string[]; } @@ -52,23 +54,29 @@ export default class AuthorFacet extends React.PureComponent<Props> { }).then(authors => ({ maxResults: authors.length === SEARCH_SIZE, results: authors })); }; + loadSearchResultCount = (author: string) => { + return this.props.loadSearchResultCount({ authors: [author] }); + }; + renderSearchResult = (author: string, term: string) => { return highlightTerm(author, term); }; render() { return ( - <ListStyleFacet + <ListStyleFacet<string> facetHeader={translate('issues.facet.authors')} fetching={this.props.fetching} getFacetItemText={this.identity} getSearchResultKey={this.identity} getSearchResultText={this.identity} + loadSearchResultCount={this.loadSearchResultCount} onChange={this.props.onChange} onSearch={this.handleSearch} onToggle={this.props.onToggle} open={this.props.open} property="authors" + query={omit(this.props.query, 'authors')} renderFacetItem={this.identity} renderSearchResult={this.renderSearchResult} searchPlaceholder={translate('search.search_for_authors')} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.tsx index 03fb868e97a..76d2b6314c8 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.tsx @@ -42,7 +42,6 @@ interface Props { createdBefore: Date | undefined; createdInLast: string; fetching: boolean; - loading?: boolean; onChange: (changes: Partial<Query>) => void; onToggle: (property: string) => void; open: boolean; @@ -173,7 +172,7 @@ export default class CreationDateFacet extends React.PureComponent<Props> { createdBefore: endDate, tooltip, x: index, - y: this.props.loading ? 0 : stats[start] + y: stats[start] }; }); @@ -226,7 +225,6 @@ export default class CreationDateFacet extends React.PureComponent<Props> { <div className="spacer-top issues-predefined-periods"> <FacetItem active={!this.hasValue()} - loading={this.props.loading} name={translate('issues.facet.createdAt.all')} onClick={this.handlePeriodClick} tooltip={translate('issues.facet.createdAt.all')} @@ -235,7 +233,6 @@ export default class CreationDateFacet extends React.PureComponent<Props> { {component ? ( <FacetItem active={sinceLeakPeriod} - loading={this.props.loading} name={translate('issues.new_code')} onClick={this.handleLeakPeriodClick} tooltip={translate('issues.leak_period')} @@ -245,7 +242,6 @@ export default class CreationDateFacet extends React.PureComponent<Props> { <> <FacetItem active={createdInLast === '1w'} - loading={this.props.loading} name={translate('issues.facet.createdAt.last_week')} onClick={this.handlePeriodClick} tooltip={translate('issues.facet.createdAt.last_week')} @@ -253,7 +249,6 @@ export default class CreationDateFacet extends React.PureComponent<Props> { /> <FacetItem active={createdInLast === '1m'} - loading={this.props.loading} name={translate('issues.facet.createdAt.last_month')} onClick={this.handlePeriodClick} tooltip={translate('issues.facet.createdAt.last_month')} @@ -261,7 +256,6 @@ export default class CreationDateFacet extends React.PureComponent<Props> { /> <FacetItem active={createdInLast === '1y'} - loading={this.props.loading} name={translate('issues.facet.createdAt.last_year')} onClick={this.handlePeriodClick} tooltip={translate('issues.facet.createdAt.last_year')} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.tsx index 9a776b636ff..56501ced226 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.tsx @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import { omit } from 'lodash'; import { Query } from '../utils'; import QualifierIcon from '../../../components/icons-components/QualifierIcon'; import { translate } from '../../../helpers/l10n'; @@ -30,10 +31,11 @@ interface Props { componentKey: string; fetching: boolean; directories: string[]; - loading?: boolean; + loadSearchResultCount: (changes: Partial<Query>) => Promise<number>; onChange: (changes: Partial<Query>) => void; onToggle: (property: string) => void; open: boolean; + query: Query; stats: { [x: string]: number } | undefined; } @@ -60,6 +62,10 @@ export default class DirectoryFacet extends React.PureComponent<Props> { }).then(({ components, paging }) => ({ paging, results: components })); }; + loadSearchResultCount = (directory: TreeComponent) => { + return this.props.loadSearchResultCount({ directories: [directory.name] }); + }; + renderDirectory = (directory: React.ReactNode) => ( <> <QualifierIcon className="little-spacer-right" qualifier="DIR" /> @@ -77,18 +83,20 @@ export default class DirectoryFacet extends React.PureComponent<Props> { render() { return ( - <ListStyleFacet + <ListStyleFacet<TreeComponent> facetHeader={translate('issues.facet.directories')} fetching={this.props.fetching} getFacetItemText={this.getFacetItemText} getSearchResultKey={this.getSearchResultKey} getSearchResultText={this.getSearchResultText} + loadSearchResultCount={this.loadSearchResultCount} minSearchLength={3} onChange={this.props.onChange} onSearch={this.handleSearch} onToggle={this.props.onToggle} open={this.props.open} property="directories" + query={omit(this.props.query, 'directories')} renderFacetItem={this.renderFacetItem} renderSearchResult={this.renderSearchResult} searchPlaceholder={translate('search.search_for_directories')} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.tsx index 45c71c36caa..1c1f2658e94 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.tsx @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import { omit } from 'lodash'; import { Query, ReferencedComponent } from '../utils'; import QualifierIcon from '../../../components/icons-components/QualifierIcon'; import { translate } from '../../../helpers/l10n'; @@ -30,10 +31,11 @@ interface Props { componentKey: string; fetching: boolean; files: string[]; - loading?: boolean; + loadSearchResultCount: (changes: Partial<Query>) => Promise<number>; onChange: (changes: Partial<Query>) => void; onToggle: (property: string) => void; open: boolean; + query: Query; referencedComponents: { [componentKey: string]: ReferencedComponent }; stats: { [x: string]: number } | undefined; } @@ -67,6 +69,10 @@ export default class FileFacet extends React.PureComponent<Props> { }).then(({ components, paging }) => ({ paging, results: components })); }; + loadSearchResultCount = (file: TreeComponent) => { + return this.props.loadSearchResultCount({ files: [file.id] }); + }; + renderFile = (file: React.ReactNode) => ( <> <QualifierIcon className="little-spacer-right" qualifier="FIL" /> @@ -85,18 +91,20 @@ export default class FileFacet extends React.PureComponent<Props> { render() { return ( - <ListStyleFacet + <ListStyleFacet<TreeComponent> facetHeader={translate('issues.facet.files')} fetching={this.props.fetching} getFacetItemText={this.getFacetItemText} getSearchResultKey={this.getSearchResultKey} getSearchResultText={this.getSearchResultText} + loadSearchResultCount={this.loadSearchResultCount} minSearchLength={3} onChange={this.props.onChange} onSearch={this.handleSearch} onToggle={this.props.onToggle} open={this.props.open} property="files" + query={omit(this.props.query, 'files')} renderFacetItem={this.renderFacetItem} renderSearchResult={this.renderSearchResult} searchPlaceholder={translate('search.search_for_files')} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.tsx index 65f2144844e..bfe9952e7bd 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.tsx @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { uniqBy } from 'lodash'; +import { uniqBy, omit } from 'lodash'; import { connect } from 'react-redux'; import ListStyleFacet from '../../../components/facet/ListStyleFacet'; import { Query, ReferencedLanguage } from '../utils'; @@ -35,10 +35,11 @@ interface Props { fetching: boolean; installedLanguages: InstalledLanguage[]; languages: string[]; - loading?: boolean; + loadSearchResultCount: (changes: Partial<Query>) => Promise<number>; onChange: (changes: Partial<Query>) => void; onToggle: (property: string) => void; open: boolean; + query: Query; referencedLanguages: { [languageKey: string]: ReferencedLanguage }; stats: { [x: string]: number } | undefined; } @@ -70,23 +71,29 @@ class LanguageFacet extends React.PureComponent<Props> { ); }; + loadSearchResultCount = (language: InstalledLanguage) => { + return this.props.loadSearchResultCount({ languages: [language.key] }); + }; + renderSearchResult = ({ name }: InstalledLanguage, term: string) => { return highlightTerm(name, term); }; render() { return ( - <ListStyleFacet + <ListStyleFacet<InstalledLanguage> facetHeader={translate('issues.facet.languages')} fetching={this.props.fetching} getFacetItemText={this.getLanguageName} - getSearchResultKey={(language: InstalledLanguage) => language.key} - getSearchResultText={(language: InstalledLanguage) => language.name} + getSearchResultKey={language => language.key} + getSearchResultText={language => language.name} + loadSearchResultCount={this.loadSearchResultCount} onChange={this.props.onChange} onSearch={this.handleSearch} onToggle={this.props.onToggle} open={this.props.open} property="languages" + query={omit(this.props.query, 'languages')} renderFacetItem={this.getLanguageName} renderSearchResult={this.renderSearchResult} searchPlaceholder={translate('search.search_for_languages')} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/ModuleFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/ModuleFacet.tsx index 82c767e2b4a..f20a6938ea5 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/ModuleFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/ModuleFacet.tsx @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import { omit } from 'lodash'; import { Query, ReferencedComponent } from '../utils'; import QualifierIcon from '../../../components/icons-components/QualifierIcon'; import { translate } from '../../../helpers/l10n'; @@ -28,11 +29,12 @@ import { highlightTerm } from '../../../helpers/search'; interface Props { componentKey: string; fetching: boolean; - loading?: boolean; + loadSearchResultCount: (changes: Partial<Query>) => Promise<number>; modules: string[]; onChange: (changes: Partial<Query>) => void; onToggle: (property: string) => void; open: boolean; + query: Query; referencedComponents: { [componentKey: string]: ReferencedComponent }; stats: { [x: string]: number } | undefined; } @@ -61,6 +63,10 @@ export default class ModuleFacet extends React.PureComponent<Props> { }).then(({ components, paging }) => ({ paging, results: components })); }; + loadSearchResultCount = (module: TreeComponent) => { + return this.props.loadSearchResultCount({ files: [module.id] }); + }; + renderModule = (module: React.ReactNode) => ( <> <QualifierIcon className="little-spacer-right" qualifier="BRC" /> @@ -79,18 +85,20 @@ export default class ModuleFacet extends React.PureComponent<Props> { render() { return ( - <ListStyleFacet + <ListStyleFacet<TreeComponent> facetHeader={translate('issues.facet.modules')} fetching={this.props.fetching} getFacetItemText={this.getModuleName} getSearchResultKey={this.getSearchResultKey} getSearchResultText={this.getSearchResultText} + loadSearchResultCount={this.loadSearchResultCount} minSearchLength={3} onChange={this.props.onChange} onSearch={this.handleSearch} onToggle={this.props.onToggle} open={this.props.open} property="modules" + query={omit(this.props.query, 'modules')} renderFacetItem={this.renderFacetItem} renderSearchResult={this.renderSearchResult} searchPlaceholder={translate('search.search_for_modules')} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.tsx index b0989c9e516..f54d8a94cd3 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.tsx @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import { omit } from 'lodash'; import ListStyleFacet from '../../../components/facet/ListStyleFacet'; import { Query, ReferencedComponent } from '../utils'; import { searchProjects, getTree } from '../../../api/components'; @@ -29,13 +30,14 @@ import { highlightTerm } from '../../../helpers/search'; interface Props { component: Component | undefined; - loading?: boolean; + loadSearchResultCount: (changes: Partial<Query>) => Promise<number>; fetching: boolean; onChange: (changes: Partial<Query>) => void; onToggle: (property: string) => void; open: boolean; organization: { key: string } | undefined; projects: string[]; + query: Query; referencedComponents: { [componentKey: string]: ReferencedComponent }; stats: { [x: string]: number } | undefined; } @@ -91,6 +93,10 @@ export default class ProjectFacet extends React.PureComponent<Props> { return referencedComponents[project] ? referencedComponents[project].name : project; }; + loadSearchResultCount = (project: SearchedProject) => { + return this.props.loadSearchResultCount({ projects: [project.id] }); + }; + renderFacetItem = (project: string) => { const { referencedComponents } = this.props; return referencedComponents[project] ? ( @@ -125,17 +131,19 @@ export default class ProjectFacet extends React.PureComponent<Props> { render() { return ( - <ListStyleFacet + <ListStyleFacet<SearchedProject> facetHeader={translate('issues.facet.projects')} fetching={this.props.fetching} getFacetItemText={this.getProjectName} - getSearchResultKey={(project: SearchedProject) => project.id} - getSearchResultText={(project: SearchedProject) => project.name} + getSearchResultKey={project => project.id} + getSearchResultText={project => project.name} + loadSearchResultCount={this.loadSearchResultCount} onChange={this.props.onChange} onSearch={this.handleSearch} onToggle={this.props.onToggle} open={this.props.open} property="projects" + query={omit(this.props.query, 'projects')} renderFacetItem={this.renderFacetItem} renderSearchResult={this.renderSearchResult} searchPlaceholder={translate('search.search_for_projects')} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.tsx index 500f1f6bf4a..eda62bd196a 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.tsx @@ -30,7 +30,6 @@ import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHi interface Props { fetching: boolean; - loading?: boolean; onChange: (changes: Partial<Query>) => void; onToggle: (property: string) => void; open: boolean; @@ -100,7 +99,6 @@ export default class ResolutionFacet extends React.PureComponent<Props> { disabled={stat === 0 && !active} halfWidth={true} key={resolution} - loading={this.props.loading} name={this.getFacetItemName(resolution)} onClick={this.handleItemClick} stat={formatFacetStat(stat)} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.tsx index be0c32bc774..64ef05a7d1c 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.tsx @@ -18,33 +18,28 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import { omit } from 'lodash'; import ListStyleFacet from '../../../components/facet/ListStyleFacet'; import { Query, ReferencedRule } from '../utils'; import { searchRules } from '../../../api/rules'; -import { Rule, Paging } from '../../../app/types'; +import { Rule } from '../../../app/types'; import { translate } from '../../../helpers/l10n'; interface Props { fetching: boolean; languages: string[]; - loading?: boolean; + loadSearchResultCount: (changes: Partial<Query>) => Promise<number>; onChange: (changes: Partial<Query>) => void; onToggle: (property: string) => void; open: boolean; organization: string | undefined; + query: Query; referencedRules: { [ruleKey: string]: ReferencedRule }; rules: string[]; stats: { [x: string]: number } | undefined; } -interface State { - query: string; - searching: boolean; - searchResults?: Rule[]; - searchPaging?: Paging; -} - -export default class RuleFacet extends React.PureComponent<Props, State> { +export default class RuleFacet extends React.PureComponent<Props> { handleSearch = (query: string, page = 1) => { const { languages, organization } = this.props; return searchRules({ @@ -63,6 +58,10 @@ export default class RuleFacet extends React.PureComponent<Props, State> { })); }; + loadSearchResultCount = (rule: Rule) => { + return this.props.loadSearchResultCount({ rules: [rule.key] }); + }; + getRuleName = (rule: string) => { const { referencedRules } = this.props; return referencedRules[rule] @@ -76,17 +75,19 @@ export default class RuleFacet extends React.PureComponent<Props, State> { render() { return ( - <ListStyleFacet + <ListStyleFacet<Rule> facetHeader={translate('issues.facet.rules')} fetching={this.props.fetching} getFacetItemText={this.getRuleName} - getSearchResultKey={result => result.key} - getSearchResultText={result => result.name} + getSearchResultKey={rule => rule.key} + getSearchResultText={rule => rule.name} + loadSearchResultCount={this.loadSearchResultCount} onChange={this.props.onChange} onSearch={this.handleSearch} onToggle={this.props.onToggle} open={this.props.open} property="rules" + query={omit(this.props.query, 'rules')} renderFacetItem={this.getRuleName} renderSearchResult={this.renderSearchResult} searchPlaceholder={translate('search.search_for_rules')} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.tsx index f8bab49546c..8f83047f9cc 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.tsx @@ -31,7 +31,6 @@ import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHi interface Props { fetching: boolean; - loading?: boolean; onChange: (changes: Partial<Query>) => void; onToggle: (property: string) => void; open: boolean; @@ -85,7 +84,6 @@ export default class SeverityFacet extends React.PureComponent<Props> { disabled={stat === 0 && !active} halfWidth={true} key={severity} - loading={this.props.loading} name={<SeverityHelper severity={severity} />} onClick={this.handleItemClick} stat={formatFacetStat(stat)} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx index 4a10412105f..fec7f81625a 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx @@ -48,7 +48,7 @@ export interface Props { component: Component | undefined; facets: { [facet: string]: Facet }; hideAuthorFacet?: boolean; - loading?: boolean; + loadSearchResultCount: (changes: Partial<Query>) => Promise<number>; loadingFacets: { [key: string]: boolean }; myIssues: boolean; onFacetToggle: (property: string) => void; @@ -81,7 +81,6 @@ export default class Sidebar extends React.PureComponent<Props> { <> <TypeFacet fetching={this.props.loadingFacets.types === true} - loading={this.props.loading} onChange={this.props.onFilterChange} onToggle={this.props.onFacetToggle} open={!!openFacets.types} @@ -90,7 +89,6 @@ export default class Sidebar extends React.PureComponent<Props> { /> <SeverityFacet fetching={this.props.loadingFacets.severities === true} - loading={this.props.loading} onChange={this.props.onFilterChange} onToggle={this.props.onFacetToggle} open={!!openFacets.severities} @@ -99,7 +97,6 @@ export default class Sidebar extends React.PureComponent<Props> { /> <ResolutionFacet fetching={this.props.loadingFacets.resolutions === true} - loading={this.props.loading} onChange={this.props.onFilterChange} onToggle={this.props.onFacetToggle} open={!!openFacets.resolutions} @@ -109,7 +106,6 @@ export default class Sidebar extends React.PureComponent<Props> { /> <StatusFacet fetching={this.props.loadingFacets.statuses === true} - loading={this.props.loading} onChange={this.props.onFilterChange} onToggle={this.props.onFacetToggle} open={!!openFacets.statuses} @@ -123,7 +119,6 @@ export default class Sidebar extends React.PureComponent<Props> { createdBefore={query.createdBefore} createdInLast={query.createdInLast} fetching={this.props.loadingFacets.createdAt === true} - loading={this.props.loading} onChange={this.props.onFilterChange} onToggle={this.props.onFacetToggle} open={!!openFacets.createdAt} @@ -133,21 +128,23 @@ export default class Sidebar extends React.PureComponent<Props> { <LanguageFacet fetching={this.props.loadingFacets.languages === true} languages={query.languages} - loading={this.props.loading} + loadSearchResultCount={this.props.loadSearchResultCount} onChange={this.props.onFilterChange} onToggle={this.props.onFacetToggle} open={!!openFacets.languages} + query={query} referencedLanguages={this.props.referencedLanguages} stats={facets.languages} /> <RuleFacet fetching={this.props.loadingFacets.rules === true} languages={query.languages} - loading={this.props.loading} + loadSearchResultCount={this.props.loadSearchResultCount} onChange={this.props.onFilterChange} onToggle={this.props.onFacetToggle} open={!!openFacets.rules} organization={organizationKey} + query={query} referencedRules={this.props.referencedRules} rules={query.rules} stats={facets.rules} @@ -159,13 +156,14 @@ export default class Sidebar extends React.PureComponent<Props> { fetchingCwe={this.props.loadingFacets.cwe === true} fetchingOwaspTop10={this.props.loadingFacets.owaspTop10 === true} fetchingSansTop25={this.props.loadingFacets.sansTop25 === true} - loading={this.props.loading} + loadSearchResultCount={this.props.loadSearchResultCount} onChange={this.props.onFilterChange} onToggle={this.props.onFacetToggle} open={!!openFacets[STANDARDS]} owaspTop10={query.owaspTop10} owaspTop10Open={!!openFacets.owaspTop10} owaspTop10Stats={facets.owaspTop10} + query={query} sansTop25={query.sansTop25} sansTop25Open={!!openFacets.sansTop25} sansTop25Stats={facets.sansTop25} @@ -173,11 +171,12 @@ export default class Sidebar extends React.PureComponent<Props> { <TagFacet component={component} fetching={this.props.loadingFacets.tags === true} - loading={this.props.loading} + loadSearchResultCount={this.props.loadSearchResultCount} onChange={this.props.onFilterChange} onToggle={this.props.onFacetToggle} open={!!openFacets.tags} organization={organizationKey} + query={query} stats={facets.tags} tags={query.tags} /> @@ -185,12 +184,13 @@ export default class Sidebar extends React.PureComponent<Props> { <ProjectFacet component={component} fetching={this.props.loadingFacets.projects === true} - loading={this.props.loading} + loadSearchResultCount={this.props.loadSearchResultCount} onChange={this.props.onFilterChange} onToggle={this.props.onFacetToggle} open={!!openFacets.projects} organization={this.props.organization} projects={query.projects} + query={query} referencedComponents={this.props.referencedComponents} stats={facets.projects} /> @@ -199,11 +199,12 @@ export default class Sidebar extends React.PureComponent<Props> { <ModuleFacet componentKey={this.props.component!.key} fetching={this.props.loadingFacets.modules === true} - loading={this.props.loading} - modules={query.modules} + loadSearchResultCount={this.props.loadSearchResultCount} + modules={query.files} onChange={this.props.onFilterChange} onToggle={this.props.onFacetToggle} open={!!openFacets.modules} + query={query} referencedComponents={this.props.referencedComponents} stats={facets.modules} /> @@ -213,10 +214,11 @@ export default class Sidebar extends React.PureComponent<Props> { componentKey={this.props.component!.key} directories={query.directories} fetching={this.props.loadingFacets.directories === true} - loading={this.props.loading} + loadSearchResultCount={this.props.loadSearchResultCount} onChange={this.props.onFilterChange} onToggle={this.props.onFacetToggle} open={!!openFacets.directories} + query={query} stats={facets.directories} /> )} @@ -225,10 +227,11 @@ export default class Sidebar extends React.PureComponent<Props> { componentKey={this.props.component!.key} fetching={this.props.loadingFacets.files === true} files={query.files} - loading={this.props.loading} + loadSearchResultCount={this.props.loadSearchResultCount} onChange={this.props.onFilterChange} onToggle={this.props.onFacetToggle} open={!!openFacets.files} + query={query} referencedComponents={this.props.referencedComponents} stats={facets.files} /> @@ -239,11 +242,12 @@ export default class Sidebar extends React.PureComponent<Props> { assignees={query.assignees} component={component} fetching={this.props.loadingFacets.assignees === true} - loading={this.props.loading} + loadSearchResultCount={this.props.loadSearchResultCount} onChange={this.props.onFilterChange} onToggle={this.props.onFacetToggle} open={!!openFacets.assignees} organization={organizationKey} + query={query} referencedUsers={this.props.referencedUsers} stats={facets.assignees} /> @@ -253,11 +257,12 @@ export default class Sidebar extends React.PureComponent<Props> { authors={query.authors} componentKey={this.props.component && this.props.component.key} fetching={this.props.loadingFacets.authors === true} - loading={this.props.loading} + loadSearchResultCount={this.props.loadSearchResultCount} onChange={this.props.onFilterChange} onToggle={this.props.onFacetToggle} open={!!openFacets.authors} organization={organizationKey} + query={query} stats={facets.authors} /> )} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx index e3cb518d74a..8c3005ae845 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { sortBy, without } from 'lodash'; +import { sortBy, without, omit } from 'lodash'; import { Query, STANDARDS, formatFacetStat } from '../utils'; import FacetBox from '../../../components/facet/FacetBox'; import FacetHeader from '../../../components/facet/FacetHeader'; @@ -33,8 +33,8 @@ import { } from '../../securityReports/utils'; import DeferredSpinner from '../../../components/common/DeferredSpinner'; import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHint'; -import SearchBox from '../../../components/controls/SearchBox'; import { highlightTerm } from '../../../helpers/search'; +import ListStyleFacet from '../../../components/facet/ListStyleFacet'; export interface Props { cwe: string[]; @@ -43,13 +43,14 @@ export interface Props { fetchingOwaspTop10: boolean; fetchingSansTop25: boolean; fetchingCwe: boolean; - loading?: boolean; + loadSearchResultCount: (changes: Partial<Query>) => Promise<number>; onChange: (changes: Partial<Query>) => void; onToggle: (property: string) => void; open: boolean; owaspTop10: string[]; owaspTop10Open: boolean; owaspTop10Stats: { [x: string]: number } | undefined; + query: Query; sansTop25: string[]; sansTop25Open: boolean; sansTop25Stats: { [x: string]: number } | undefined; @@ -132,10 +133,6 @@ export default class StandardFacet extends React.PureComponent<Props, State> { this.props.onToggle('sansTop25'); }; - handleCWEHeaderClick = () => { - this.props.onToggle('cwe'); - }; - handleClear = () => { this.props.onChange({ [this.property]: [], owaspTop10: [], sansTop25: [], cwe: [] }); }; @@ -158,20 +155,22 @@ export default class StandardFacet extends React.PureComponent<Props, State> { this.handleItemClick('owaspTop10', itemValue, multiple); }; - handleCWEItemClick = (itemValue: string, multiple: boolean) => { - this.handleItemClick('cwe', itemValue, multiple); - }; - handleSansTop25ItemClick = (itemValue: string, multiple: boolean) => { this.handleItemClick('sansTop25', itemValue, multiple); }; - handleCWESelect = ({ value }: { value: string }) => { - this.handleItemClick('cwe', value, true); + handleCWESearch = (query: string) => { + return Promise.resolve({ + results: Object.keys(this.state.standards.cwe).filter(cwe => + renderCWECategory(this.state.standards, cwe) + .toLowerCase() + .includes(query.toLowerCase()) + ) + }); }; - handleCWESearch = (query: string) => { - this.setState({ cweQuery: query }); + loadCWESearchResultCount = (category: string) => { + return this.props.loadSearchResultCount({ cwe: [category] }); }; renderList = ( @@ -216,7 +215,6 @@ export default class StandardFacet extends React.PureComponent<Props, State> { <FacetItem active={values.includes(category)} key={category} - loading={this.props.loading} name={renderName(this.state.standards, category)} onClick={onClick} stat={formatFacetStat(getStat(category))} @@ -247,45 +245,6 @@ export default class StandardFacet extends React.PureComponent<Props, State> { return this.renderHint('owaspTop10Stats', 'owaspTop10'); } - renderCWEList() { - const { cweQuery } = this.state; - if (cweQuery) { - const results = Object.keys(this.state.standards.cwe).filter(cwe => - renderCWECategory(this.state.standards, cwe) - .toLowerCase() - .includes(cweQuery.toLowerCase()) - ); - - return this.renderFacetItemsList( - this.props.cweStats, - this.props.cwe, - results, - (standards: Standards, category: string) => - highlightTerm(renderCWECategory(standards, category), cweQuery), - renderCWECategory, - this.handleCWEItemClick - ); - } else { - return this.renderList('cweStats', 'cwe', renderCWECategory, this.handleCWEItemClick); - } - } - - renderCWESearch() { - return ( - <SearchBox - autoFocus={true} - className="little-spacer-top spacer-bottom" - onChange={this.handleCWESearch} - placeholder={translate('search.search_for_cwe')} - value={this.state.cweQuery} - /> - ); - } - - renderCWEHint() { - return this.renderHint('cweStats', 'cwe'); - } - renderSansTop25List() { return this.renderList( 'sansTop25Stats', @@ -336,22 +295,28 @@ export default class StandardFacet extends React.PureComponent<Props, State> { </> )} </FacetBox> - <FacetBox className="is-inner" property="cwe"> - <FacetHeader - name={translate('issues.facet.cwe')} - onClick={this.handleCWEHeaderClick} - open={this.props.cweOpen} - values={this.props.cwe.map(item => renderCWECategory(this.state.standards, item))} - /> - <DeferredSpinner loading={this.props.fetchingCwe} /> - {this.props.cweOpen && ( - <> - {this.renderCWESearch()} - {this.renderCWEList()} - {this.renderCWEHint()} - </> - )} - </FacetBox> + <ListStyleFacet<string> + className="is-inner" + facetHeader={translate('issues.facet.cwe')} + fetching={this.props.fetchingCwe} + getFacetItemText={item => renderCWECategory(this.state.standards, item)} + getSearchResultKey={item => item} + getSearchResultText={item => renderCWECategory(this.state.standards, item)} + loadSearchResultCount={this.loadCWESearchResultCount} + onChange={this.props.onChange} + onSearch={this.handleCWESearch} + onToggle={this.props.onToggle} + open={this.props.cweOpen} + property="cwe" + query={omit(this.props.query, 'cwe')} + renderFacetItem={item => renderCWECategory(this.state.standards, item)} + renderSearchResult={(item, query) => + highlightTerm(renderCWECategory(this.state.standards, item), query) + } + searchPlaceholder={translate('search.search_for_cwe')} + stats={this.props.cweStats} + values={this.props.cwe} + /> </> ); } diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.tsx index f9cb69866c7..43291fe15a2 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.tsx @@ -31,7 +31,6 @@ import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHi interface Props { fetching: boolean; - loading?: boolean; onChange: (changes: Partial<Query>) => void; onToggle: (property: string) => void; open: boolean; @@ -85,7 +84,6 @@ export default class StatusFacet extends React.PureComponent<Props> { disabled={stat === 0 && !active} halfWidth={true} key={status} - loading={this.props.loading} name={<StatusHelper resolution={undefined} status={status} />} onClick={this.handleItemClick} stat={formatFacetStat(stat)} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.tsx index f68af7618f4..df2fcc7a898 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.tsx @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import { omit } from 'lodash'; import { Query } from '../utils'; import { searchIssueTags } from '../../../api/issues'; import * as theme from '../../../app/theme'; @@ -30,11 +31,12 @@ import { highlightTerm } from '../../../helpers/search'; interface Props { component: Component | undefined; fetching: boolean; - loading?: boolean; + loadSearchResultCount: (changes: Partial<Query>) => Promise<number>; onChange: (changes: Partial<Query>) => void; onToggle: (property: string) => void; open: boolean; organization: string | undefined; + query: Query; stats: { [x: string]: number } | undefined; tags: string[]; } @@ -54,6 +56,10 @@ export default class TagFacet extends React.PureComponent<Props> { return tag; }; + loadSearchResultCount = (tag: string) => { + return this.props.loadSearchResultCount({ tags: [tag] }); + }; + renderTag = (tag: string) => { return ( <> @@ -72,17 +78,19 @@ export default class TagFacet extends React.PureComponent<Props> { render() { return ( - <ListStyleFacet + <ListStyleFacet<string> facetHeader={translate('issues.facet.tags')} fetching={this.props.fetching} getFacetItemText={this.getTagName} getSearchResultKey={tag => tag} getSearchResultText={tag => tag} + loadSearchResultCount={this.loadSearchResultCount} onChange={this.props.onChange} onSearch={this.handleSearch} onToggle={this.props.onToggle} open={this.props.open} property="tags" + query={omit(this.props.query, 'tags')} renderFacetItem={this.renderTag} renderSearchResult={this.renderSearchResult} searchPlaceholder={translate('search.search_for_tags')} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.tsx index e3cb845070a..6dced6ed39c 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.tsx @@ -31,7 +31,6 @@ import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHi interface Props { fetching: boolean; - loading?: boolean; onChange: (changes: Partial<Query>) => void; onToggle: (property: string) => void; open: boolean; @@ -94,7 +93,6 @@ export default class TypeFacet extends React.PureComponent<Props> { active={active} disabled={stat === 0 && !active} key={type} - loading={this.props.loading} name={ <span> <IssueTypeIcon query={type} /> {translate('issue.type', type)} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.tsx index 0d797101882..1fa98d7098f 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.tsx @@ -20,56 +20,26 @@ import * as React from 'react'; import { shallow } from 'enzyme'; import AssigneeFacet, { Props } from '../AssigneeFacet'; +import { Query } from '../../utils'; jest.mock('../../../../store/rootReducer', () => ({})); -const renderAssigneeFacet = (props?: Partial<Props>) => - shallow( - <AssigneeFacet - assigned={true} - assignees={[]} - component={undefined} - fetching={false} - onChange={jest.fn()} - onToggle={jest.fn()} - open={true} - organization={undefined} - referencedUsers={{ foo: { avatar: 'avatart-foo', name: 'name-foo' } }} - stats={{ '': 5, foo: 13, bar: 7, baz: 6 }} - {...props} - /> - ); - it('should render', () => { - expect(renderAssigneeFacet()).toMatchSnapshot(); -}); - -it('should render without stats', () => { - expect(renderAssigneeFacet({ stats: undefined })).toMatchSnapshot(); -}); - -it('should select unassigned', () => { - expect(renderAssigneeFacet({ assigned: false })).toMatchSnapshot(); -}); - -it('should select user', () => { expect(renderAssigneeFacet({ assignees: ['foo'] })).toMatchSnapshot(); }); -it('should render footer select option', () => { - const wrapper = renderAssigneeFacet(); +it('should select unassigned', () => { expect( - (wrapper.instance() as AssigneeFacet).renderOption({ avatar: 'avatar-foo', label: 'name-foo' }) - ).toMatchSnapshot(); + renderAssigneeFacet({ assigned: false }) + .find('ListStyleFacet') + .prop('values') + ).toEqual(['']); }); it('should call onChange', () => { const onChange = jest.fn(); const wrapper = renderAssigneeFacet({ assignees: ['foo'], onChange }); - const itemOnClick = wrapper - .find('FacetItem') - .first() - .prop<Function>('onClick'); + const itemOnClick = wrapper.find('ListStyleFacet').prop<Function>('onItemClick'); itemOnClick(''); expect(onChange).lastCalledWith({ assigned: false, assignees: [] }); @@ -81,11 +51,22 @@ it('should call onChange', () => { expect(onChange).lastCalledWith({ assigned: true, assignees: ['baz', 'foo'] }); }); -it('should call onToggle', () => { - const onToggle = jest.fn(); - const wrapper = renderAssigneeFacet({ onToggle }); - const headerOnClick = wrapper.find('FacetHeader').prop<Function>('onClick'); - - headerOnClick(); - expect(onToggle).lastCalledWith('assignees'); -}); +function renderAssigneeFacet(props?: Partial<Props>) { + return shallow( + <AssigneeFacet + assigned={true} + assignees={[]} + component={undefined} + fetching={false} + loadSearchResultCount={jest.fn()} + onChange={jest.fn()} + onToggle={jest.fn()} + open={true} + organization={undefined} + query={{} as Query} + referencedUsers={{ foo: { avatar: 'avatart-foo', name: 'name-foo' } }} + stats={{ '': 5, foo: 13, bar: 7, baz: 6 }} + {...props} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-test.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-test.tsx index 7b658ae3db0..bf48d87f948 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-test.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-test.tsx @@ -29,6 +29,7 @@ const renderSidebar = (props?: Partial<Props>) => <Sidebar component={undefined} facets={{}} + loadSearchResultCount={jest.fn()} loadingFacets={{}} myIssues={false} onFacetToggle={jest.fn()} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/StandardFacet-test.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/StandardFacet-test.tsx index 41d59a42a92..982c62b04c3 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/StandardFacet-test.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/StandardFacet-test.tsx @@ -21,6 +21,7 @@ import * as React from 'react'; import { shallow } from 'enzyme'; import StandardFacet, { Props } from '../StandardFacet'; import { click } from '../../../../helpers/testUtils'; +import { Query } from '../../utils'; it('should render closed', () => { expect(shallowRender()).toMatchSnapshot(); @@ -84,7 +85,6 @@ it('should select items', () => { selectAndCheck('owaspTop10', 'a1'); selectAndCheck('owaspTop10', 'a1', true, ['a1', 'a3']); selectAndCheck('sansTop25', 'foo'); - selectAndCheck('cwe', '173'); function selectAndCheck(facet: string, value: string, multiple = false, expectedValue = [value]) { wrapper @@ -100,8 +100,6 @@ it('should toggle sub-facets', () => { const wrapper = shallowRender({ onToggle, open: true }); click(wrapper.find('FacetBox[property="owaspTop10"]').children('FacetHeader')); expect(onToggle).lastCalledWith('owaspTop10'); - click(wrapper.find('FacetBox[property="cwe"]').children('FacetHeader')); - expect(onToggle).lastCalledWith('cwe'); click(wrapper.find('FacetBox[property="sansTop25"]').children('FacetHeader')); expect(onToggle).lastCalledWith('sansTop25'); }); @@ -124,7 +122,6 @@ it('should display correct selection', () => { 'Unknown CWE' ]); checkValues('owaspTop10', ['A1 - a1 title', 'A3', 'Not OWAPS']); - checkValues('cwe', ['CWE-42 - cwe-42 title', 'CWE-1111', 'Unknown CWE']); checkValues('sansTop25', ['Risky Resource Management', 'foo']); function checkValues(property: string, values: string[]) { @@ -137,16 +134,6 @@ it('should display correct selection', () => { } }); -it('should search CWE', () => { - const wrapper = shallowRender({ open: true, cwe: ['42'], cweOpen: true }); - wrapper - .find('FacetBox[property="cwe"]') - .find('SearchBox') - .prop<Function>('onChange')('unkn'); - wrapper.update(); - expect(wrapper).toMatchSnapshot(); -}); - function shallowRender(props: Partial<Props> = {}) { const wrapper = shallow( <StandardFacet @@ -156,12 +143,14 @@ function shallowRender(props: Partial<Props> = {}) { fetchingCwe={false} fetchingOwaspTop10={false} fetchingSansTop25={false} + loadSearchResultCount={jest.fn()} onChange={jest.fn()} onToggle={jest.fn()} open={false} owaspTop10={[]} owaspTop10Open={false} owaspTop10Stats={{}} + query={{} as Query} sansTop25={[]} sansTop25Open={false} sansTop25Stats={{}} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.tsx.snap index 1ab0e42e2a6..5725fdffcb5 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.tsx.snap @@ -1,321 +1,39 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should render 1`] = ` -<FacetBox +<ListStyleFacet + facetHeader="issues.facet.assignees" + fetching={false} + getFacetItemText={[Function]} + getSearchResultKey={[Function]} + getSearchResultText={[Function]} + getSortedItems={[Function]} + loadSearchResultCount={[Function]} + maxInitialItems={15} + maxItems={100} + onChange={[MockFunction]} + onClear={[Function]} + onItemClick={[Function]} + onSearch={[Function]} + onToggle={[MockFunction]} + open={true} property="assignees" -> - <FacetHeader - name="issues.facet.assignees" - onClear={[Function]} - onClick={[Function]} - open={true} - values={Array []} - /> - <DeferredSpinner - loading={false} - timeout={100} - /> - <React.Fragment> - <SearchBox - autoFocus={true} - className="little-spacer-top spacer-bottom" - loading={false} - minLength={2} - onChange={[Function]} - placeholder="search.search_for_users" - value="" - /> - <FacetItemsList> - <FacetItem - active={false} - disabled={false} - halfWidth={false} - loading={false} - name="unassigned" - onClick={[Function]} - stat="5" - tooltip="unassigned" - value="" - /> - <FacetItem - active={false} - disabled={false} - halfWidth={false} - key="foo" - loading={false} - name={ - <span> - <Connect(Avatar) - className="little-spacer-right" - hash="avatart-foo" - name="name-foo" - size={16} - /> - name-foo - </span> - } - onClick={[Function]} - stat="13" - tooltip="name-foo" - value="foo" - /> - <FacetItem - active={false} - disabled={false} - halfWidth={false} - key="bar" - loading={false} - name="bar" - onClick={[Function]} - stat="7" - tooltip="bar" - value="bar" - /> - <FacetItem - active={false} - disabled={false} - halfWidth={false} - key="baz" - loading={false} - name="baz" - onClick={[Function]} - stat="6" - tooltip="baz" - value="baz" - /> - </FacetItemsList> - <MultipleSelectionHint - options={4} - values={0} - /> - </React.Fragment> -</FacetBox> -`; - -exports[`should render footer select option 1`] = ` -<span> - <Connect(Avatar) - className="little-spacer-right" - hash="avatar-foo" - name="name-foo" - size={16} - /> - name-foo -</span> -`; - -exports[`should render without stats 1`] = ` -<FacetBox - property="assignees" -> - <FacetHeader - name="issues.facet.assignees" - onClear={[Function]} - onClick={[Function]} - open={true} - values={Array []} - /> - <DeferredSpinner - loading={false} - timeout={100} - /> - <React.Fragment> - <MultipleSelectionHint - options={0} - values={0} - /> - </React.Fragment> -</FacetBox> -`; - -exports[`should select unassigned 1`] = ` -<FacetBox - property="assignees" -> - <FacetHeader - name="issues.facet.assignees" - onClear={[Function]} - onClick={[Function]} - open={true} - values={ - Array [ - "unassigned", - ] - } - /> - <DeferredSpinner - loading={false} - timeout={100} - /> - <React.Fragment> - <SearchBox - autoFocus={true} - className="little-spacer-top spacer-bottom" - loading={false} - minLength={2} - onChange={[Function]} - placeholder="search.search_for_users" - value="" - /> - <FacetItemsList> - <FacetItem - active={true} - disabled={false} - halfWidth={false} - loading={false} - name="unassigned" - onClick={[Function]} - stat="5" - tooltip="unassigned" - value="" - /> - <FacetItem - active={false} - disabled={false} - halfWidth={false} - key="foo" - loading={false} - name={ - <span> - <Connect(Avatar) - className="little-spacer-right" - hash="avatart-foo" - name="name-foo" - size={16} - /> - name-foo - </span> - } - onClick={[Function]} - stat="13" - tooltip="name-foo" - value="foo" - /> - <FacetItem - active={false} - disabled={false} - halfWidth={false} - key="bar" - loading={false} - name="bar" - onClick={[Function]} - stat="7" - tooltip="bar" - value="bar" - /> - <FacetItem - active={false} - disabled={false} - halfWidth={false} - key="baz" - loading={false} - name="baz" - onClick={[Function]} - stat="6" - tooltip="baz" - value="baz" - /> - </FacetItemsList> - <MultipleSelectionHint - options={4} - values={0} - /> - </React.Fragment> -</FacetBox> -`; - -exports[`should select user 1`] = ` -<FacetBox - property="assignees" -> - <FacetHeader - name="issues.facet.assignees" - onClear={[Function]} - onClick={[Function]} - open={true} - values={ - Array [ - "name-foo", - ] + query={Object {}} + renderFacetItem={[Function]} + renderSearchResult={[Function]} + searchPlaceholder="search.search_for_users" + stats={ + Object { + "": 5, + "bar": 7, + "baz": 6, + "foo": 13, } - /> - <DeferredSpinner - loading={false} - timeout={100} - /> - <React.Fragment> - <SearchBox - autoFocus={true} - className="little-spacer-top spacer-bottom" - loading={false} - minLength={2} - onChange={[Function]} - placeholder="search.search_for_users" - value="" - /> - <FacetItemsList> - <FacetItem - active={false} - disabled={false} - halfWidth={false} - loading={false} - name="unassigned" - onClick={[Function]} - stat="5" - tooltip="unassigned" - value="" - /> - <FacetItem - active={true} - disabled={false} - halfWidth={false} - key="foo" - loading={false} - name={ - <span> - <Connect(Avatar) - className="little-spacer-right" - hash="avatart-foo" - name="name-foo" - size={16} - /> - name-foo - </span> - } - onClick={[Function]} - stat="13" - tooltip="name-foo" - value="foo" - /> - <FacetItem - active={false} - disabled={false} - halfWidth={false} - key="bar" - loading={false} - name="bar" - onClick={[Function]} - stat="7" - tooltip="bar" - value="bar" - /> - <FacetItem - active={false} - disabled={false} - halfWidth={false} - key="baz" - loading={false} - name="baz" - onClick={[Function]} - stat="6" - tooltip="baz" - value="baz" - /> - </FacetItemsList> - <MultipleSelectionHint - options={4} - values={1} - /> - </React.Fragment> -</FacetBox> + } + values={ + Array [ + "foo", + ] + } +/> `; diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/StandardFacet-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/StandardFacet-test.tsx.snap index 939fb386fd6..363e37abfbe 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/StandardFacet-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/StandardFacet-test.tsx.snap @@ -163,166 +163,37 @@ exports[`should render sub-facets 1`] = ` /> </React.Fragment> </FacetBox> - <FacetBox - className="is-inner" - property="cwe" - > - <FacetHeader - name="issues.facet.cwe" - onClick={[Function]} - open={true} - values={ - Array [ - "CWE-42 - cwe-42 title", - ] - } - /> - <DeferredSpinner - loading={false} - timeout={100} - /> - <React.Fragment> - <SearchBox - autoFocus={true} - className="little-spacer-top spacer-bottom" - onChange={[Function]} - placeholder="search.search_for_cwe" - value="" - /> - <FacetItemsList> - <FacetItem - active={true} - disabled={false} - halfWidth={false} - key="42" - loading={false} - name="CWE-42 - cwe-42 title" - onClick={[Function]} - stat="5" - tooltip="CWE-42 - cwe-42 title" - value="42" - /> - <FacetItem - active={false} - disabled={false} - halfWidth={false} - key="173" - loading={false} - name="CWE-173" - onClick={[Function]} - stat="3" - tooltip="CWE-173" - value="173" - /> - </FacetItemsList> - <MultipleSelectionHint - options={2} - values={1} - /> - </React.Fragment> - </FacetBox> - </React.Fragment> -</FacetBox> -`; - -exports[`should search CWE 1`] = ` -<FacetBox - property="standards" -> - <FacetHeader - name="issues.facet.standards" - onClear={[Function]} - onClick={[Function]} - open={true} - values={ - Array [ - "CWE-42 - cwe-42 title", - ] - } - /> - <React.Fragment> - <FacetBox - className="is-inner" - property="owaspTop10" - > - <FacetHeader - name="issues.facet.owaspTop10" - onClick={[Function]} - open={false} - values={Array []} - /> - <DeferredSpinner - loading={false} - timeout={100} - /> - </FacetBox> - <FacetBox - className="is-inner" - property="sansTop25" - > - <FacetHeader - name="issues.facet.sansTop25" - onClick={[Function]} - open={false} - values={Array []} - /> - <DeferredSpinner - loading={false} - timeout={100} - /> - </FacetBox> - <FacetBox + <ListStyleFacet className="is-inner" + facetHeader="issues.facet.cwe" + fetching={false} + getFacetItemText={[Function]} + getSearchResultKey={[Function]} + getSearchResultText={[Function]} + loadSearchResultCount={[Function]} + maxInitialItems={15} + maxItems={100} + onChange={[MockFunction]} + onSearch={[Function]} + onToggle={[MockFunction]} + open={true} property="cwe" - > - <FacetHeader - name="issues.facet.cwe" - onClick={[Function]} - open={true} - values={ - Array [ - "CWE-42 - cwe-42 title", - ] + query={Object {}} + renderFacetItem={[Function]} + renderSearchResult={[Function]} + searchPlaceholder="search.search_for_cwe" + stats={ + Object { + "173": 3, + "42": 5, } - /> - <DeferredSpinner - loading={false} - timeout={100} - /> - <React.Fragment> - <SearchBox - autoFocus={true} - className="little-spacer-top spacer-bottom" - onChange={[Function]} - placeholder="search.search_for_cwe" - value="unkn" - /> - <FacetItemsList> - <FacetItem - active={false} - disabled={false} - halfWidth={false} - key="unknown" - loading={false} - name={ - <React.Fragment> - <mark> - Unkn - </mark> - own CWE - </React.Fragment> - } - onClick={[Function]} - tooltip="Unknown CWE" - value="unknown" - /> - </FacetItemsList> - <MultipleSelectionHint - options={0} - values={1} - /> - </React.Fragment> - </FacetBox> + } + values={ + Array [ + "42", + ] + } + /> </React.Fragment> </FacetBox> `; |