diff options
author | Stas Vilchik <stas.vilchik@sonarsource.com> | 2018-08-10 10:48:27 +0200 |
---|---|---|
committer | SonarTech <sonartech@sonarsource.com> | 2018-08-21 20:21:02 +0200 |
commit | 20a8ceffbe12771dea8f9186a408aabbab32a8d7 (patch) | |
tree | 18c23ced96fe50c3730b214a1cdccf247a814b3d /server/sonar-web | |
parent | 15f3d9c2584cca304590ad68dea4d025ac356813 (diff) | |
download | sonarqube-20a8ceffbe12771dea8f9186a408aabbab32a8d7.tar.gz sonarqube-20a8ceffbe12771dea8f9186a408aabbab32a8d7.zip |
SONAR-9369 Add search for module, directory, file and author facets (#606)
Diffstat (limited to 'server/sonar-web')
12 files changed, 283 insertions, 393 deletions
diff --git a/server/sonar-web/src/main/js/api/components.ts b/server/sonar-web/src/main/js/api/components.ts index de4f0cb7a4f..899e6a1c450 100644 --- a/server/sonar-web/src/main/js/api/components.ts +++ b/server/sonar-web/src/main/js/api/components.ts @@ -140,17 +140,26 @@ export function getComponent( export interface TreeComponent extends LightComponent { id: string; name: string; + path?: string; refId?: string; refKey?: string; tags?: string[]; visibility: Visibility; } -export function getTree( - component: string, - options: RequestData = {} -): Promise<{ baseComponent: TreeComponent; components: TreeComponent[]; paging: Paging }> { - return getJSON('/api/components/tree', { ...options, component }); +export function getTree(data: { + asc?: boolean; + branch?: string; + component: string; + p?: number; + ps?: number; + pullRequest?: string; + q?: string; + qualifiers?: string; + s?: string; + strategy?: 'all' | 'leaves' | 'children'; +}): Promise<{ baseComponent: TreeComponent; components: TreeComponent[]; paging: Paging }> { + return getJSON('/api/components/tree', data).catch(throwGlobalError); } export function getComponentShow(data: { component: string } & BranchParameters): Promise<any> { diff --git a/server/sonar-web/src/main/js/api/issues.ts b/server/sonar-web/src/main/js/api/issues.ts index 3fe3a9bb3ca..b49a6eddba9 100644 --- a/server/sonar-web/src/main/js/api/issues.ts +++ b/server/sonar-web/src/main/js/api/issues.ts @@ -165,3 +165,12 @@ export function setIssueType(data: { issue: string; type: string }): Promise<Iss export function bulkChangeIssues(issueKeys: string[], query: RequestData): Promise<void> { return post('/api/issues/bulk_change', { issues: issueKeys.join(), ...query }); } + +export function searchIssueAuthors(data: { + organization?: string; + project?: string; + ps?: number; + q?: string; +}): Promise<string[]> { + return getJSON('/api/issues/authors', data).then(r => r.authors, throwGlobalError); +} diff --git a/server/sonar-web/src/main/js/apps/code/components/Search.tsx b/server/sonar-web/src/main/js/apps/code/components/Search.tsx index 6315df5dcd3..7cdd3af90ac 100644 --- a/server/sonar-web/src/main/js/apps/code/components/Search.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/Search.tsx @@ -133,7 +133,8 @@ export default class Search extends React.PureComponent<Props, State> { const isPortfolio = ['VW', 'SVW', 'APP'].includes(component.qualifier); const qualifiers = isPortfolio ? 'SVW,TRK' : 'BRC,UTS,FIL'; - getTree(component.key, { + getTree({ + component: component.key, q: query, s: 'qualifier,name', qualifiers, 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 fb586f1b85c..301506b60a1 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,107 +18,63 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { sortBy, without } from 'lodash'; -import { formatFacetStat, Query } from '../utils'; -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 { Query } from '../utils'; import { translate } from '../../../helpers/l10n'; -import DeferredSpinner from '../../../components/common/DeferredSpinner'; -import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHint'; +import ListStyleFacet from '../../../components/facet/ListStyleFacet'; +import { searchIssueAuthors } from '../../../api/issues'; +import { highlightTerm } from '../../../helpers/search'; interface Props { + componentKey: string | undefined; fetching: boolean; loading?: boolean; onChange: (changes: Partial<Query>) => void; onToggle: (property: string) => void; open: boolean; + organization: string | undefined; stats: { [x: string]: number } | undefined; authors: string[]; } -export default class AuthorFacet extends React.PureComponent<Props> { - property = 'authors'; - - static defaultProps = { - open: true - }; +const SEARCH_SIZE = 100; - handleItemClick = (itemValue: string, multiple: boolean) => { - const { authors } = this.props; - if (multiple) { - const newValue = sortBy( - authors.includes(itemValue) ? without(authors, itemValue) : [...authors, itemValue] - ); - this.props.onChange({ [this.property]: newValue }); - } else { - this.props.onChange({ - [this.property]: authors.includes(itemValue) && authors.length < 2 ? [] : [itemValue] - }); - } +export default class AuthorFacet extends React.PureComponent<Props> { + identity = (author: string) => { + return author; }; - handleHeaderClick = () => { - this.props.onToggle(this.property); + handleSearch = (query: string, _page: number) => { + return searchIssueAuthors({ + organization: this.props.organization, + project: this.props.componentKey, + ps: SEARCH_SIZE, // maximum + q: query + }).then(authors => ({ maxResults: authors.length === SEARCH_SIZE, results: authors })); }; - handleClear = () => { - this.props.onChange({ [this.property]: [] }); + renderSearchResult = (author: string, term: string) => { + return highlightTerm(author, term); }; - getStat(author: string) { - const { stats } = this.props; - return stats ? stats[author] : undefined; - } - - renderList() { - const { stats } = this.props; - - if (!stats) { - return null; - } - - const authors = sortBy(Object.keys(stats), key => -stats[key]); - - return ( - <FacetItemsList> - {authors.map(author => ( - <FacetItem - active={this.props.authors.includes(author)} - key={author} - loading={this.props.loading} - name={author} - onClick={this.handleItemClick} - stat={formatFacetStat(this.getStat(author))} - tooltip={author} - value={author} - /> - ))} - </FacetItemsList> - ); - } - render() { - const { authors, 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.props.authors} - /> - - <DeferredSpinner loading={this.props.fetching} /> - {this.props.open && ( - <> - {this.renderList()} - <MultipleSelectionHint options={Object.keys(stats).length} values={authors.length} /> - </> - )} - </FacetBox> + <ListStyleFacet + facetHeader={translate('issues.facet.authors')} + fetching={this.props.fetching} + getFacetItemText={this.identity} + getSearchResultKey={this.identity} + getSearchResultText={this.identity} + onChange={this.props.onChange} + onSearch={this.handleSearch} + onToggle={this.props.onToggle} + open={this.props.open} + property="authors" + renderFacetItem={this.identity} + renderSearchResult={this.renderSearchResult} + searchPlaceholder={translate('search.search_for_authors')} + stats={this.props.stats} + values={this.props.authors} + /> ); } } 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 582917bf341..9a776b636ff 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,127 +18,83 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { sortBy, without } from 'lodash'; -import { formatFacetStat, Query, ReferencedComponent } from '../utils'; -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 { Query } from '../utils'; import QualifierIcon from '../../../components/icons-components/QualifierIcon'; import { translate } from '../../../helpers/l10n'; +import ListStyleFacet from '../../../components/facet/ListStyleFacet'; +import { highlightTerm } from '../../../helpers/search'; +import { getTree, TreeComponent } from '../../../api/components'; import { collapsePath } from '../../../helpers/path'; -import DeferredSpinner from '../../../components/common/DeferredSpinner'; -import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHint'; interface Props { + componentKey: string; fetching: boolean; directories: string[]; loading?: boolean; onChange: (changes: Partial<Query>) => void; onToggle: (property: string) => void; open: boolean; - referencedComponents: { [componentKey: string]: ReferencedComponent }; stats: { [x: string]: number } | undefined; } export default class DirectoryFacet extends React.PureComponent<Props> { - property = 'directories'; - - static defaultProps = { - open: true + getFacetItemText = (directory: string) => { + return collapsePath(directory, 15); }; - handleItemClick = (itemValue: string, multiple: boolean) => { - const { directories } = this.props; - if (multiple) { - const newValue = sortBy( - directories.includes(itemValue) - ? without(directories, itemValue) - : [...directories, itemValue] - ); - this.props.onChange({ [this.property]: newValue }); - } else { - this.props.onChange({ - [this.property]: - directories.includes(itemValue) && directories.length < 2 ? [] : [itemValue] - }); - } + getSearchResultKey = (directory: TreeComponent) => { + return directory.name; }; - handleHeaderClick = () => { - this.props.onToggle(this.property); + getSearchResultText = (directory: TreeComponent) => { + return directory.name; }; - handleClear = () => { - this.props.onChange({ [this.property]: [] }); + handleSearch = (query: string, page: number) => { + return getTree({ + component: this.props.componentKey, + q: query, + qualifiers: 'DIR', + p: page, + ps: 30 + }).then(({ components, paging }) => ({ paging, results: components })); }; - getStat(directory: string) { - const { stats } = this.props; - return stats ? stats[directory] : undefined; - } - - renderName(directory: string) { - return ( - <span> - <QualifierIcon className="little-spacer-right" qualifier="DIR" /> - {directory} - </span> - ); - } - - renderList() { - const { stats } = this.props; + renderDirectory = (directory: React.ReactNode) => ( + <> + <QualifierIcon className="little-spacer-right" qualifier="DIR" /> + {directory} + </> + ); - if (!stats) { - return null; - } - - // sort directories first by counts, then by path - const directories = sortBy(Object.keys(stats), key => -stats[key], d => d); + renderFacetItem = (directory: string) => { + return this.renderDirectory(collapsePath(directory, 15)); + }; - return ( - <FacetItemsList> - {directories.map(directory => ( - <FacetItem - active={this.props.directories.includes(directory)} - key={directory} - loading={this.props.loading} - name={this.renderName(directory)} - onClick={this.handleItemClick} - stat={formatFacetStat(this.getStat(directory))} - tooltip={directory} - value={directory} - /> - ))} - </FacetItemsList> - ); - } + renderSearchResult = (directory: TreeComponent, term: string) => { + return this.renderDirectory(highlightTerm(collapsePath(directory.name), term)); + }; render() { - const { directories, stats = {} } = this.props; - const values = directories.map(dir => collapsePath(dir)); return ( - <FacetBox property={this.property}> - <FacetHeader - name={translate('issues.facet', this.property)} - onClear={this.handleClear} - onClick={this.handleHeaderClick} - open={this.props.open} - values={values} - /> - - <DeferredSpinner loading={this.props.fetching} /> - {this.props.open && ( - <> - {this.renderList()} - <MultipleSelectionHint - options={Object.keys(stats).length} - values={directories.length} - /> - </> - )} - </FacetBox> + <ListStyleFacet + facetHeader={translate('issues.facet.directories')} + fetching={this.props.fetching} + getFacetItemText={this.getFacetItemText} + getSearchResultKey={this.getSearchResultKey} + getSearchResultText={this.getSearchResultText} + minSearchLength={3} + onChange={this.props.onChange} + onSearch={this.handleSearch} + onToggle={this.props.onToggle} + open={this.props.open} + property="directories" + renderFacetItem={this.renderFacetItem} + renderSearchResult={this.renderSearchResult} + searchPlaceholder={translate('search.search_for_directories')} + stats={this.props.stats} + values={this.props.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 77f529a6973..45c71c36caa 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,19 +18,16 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { sortBy, without } from 'lodash'; -import { formatFacetStat, Query, ReferencedComponent } from '../utils'; -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 { Query, ReferencedComponent } from '../utils'; import QualifierIcon from '../../../components/icons-components/QualifierIcon'; import { translate } from '../../../helpers/l10n'; import { collapsePath } from '../../../helpers/path'; -import DeferredSpinner from '../../../components/common/DeferredSpinner'; -import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHint'; +import { TreeComponent, getTree } from '../../../api/components'; +import ListStyleFacet from '../../../components/facet/ListStyleFacet'; +import { highlightTerm } from '../../../helpers/search'; interface Props { + componentKey: string; fetching: boolean; files: string[]; loading?: boolean; @@ -42,102 +39,70 @@ interface Props { } export default class FileFacet extends React.PureComponent<Props> { - property = 'files'; - - static defaultProps = { - open: true + getFile = (file: string) => { + const { referencedComponents } = this.props; + return referencedComponents[file] ? collapsePath(referencedComponents[file].path, 15) : file; }; - handleItemClick = (itemValue: string, multiple: boolean) => { - const { files } = this.props; - if (multiple) { - const newValue = sortBy( - files.includes(itemValue) ? without(files, itemValue) : [...files, itemValue] - ); - this.props.onChange({ [this.property]: newValue }); - } else { - this.props.onChange({ - [this.property]: files.includes(itemValue) && files.length < 2 ? [] : [itemValue] - }); - } + getFacetItemText = (file: string) => { + const { referencedComponents } = this.props; + return referencedComponents[file] ? referencedComponents[file].path : file; }; - handleHeaderClick = () => { - this.props.onToggle(this.property); + getSearchResultKey = (file: TreeComponent) => { + return file.id; }; - handleClear = () => { - this.props.onChange({ [this.property]: [] }); + getSearchResultText = (file: TreeComponent) => { + return file.path || file.name; }; - getStat(file: string) { - const { stats } = this.props; - return stats ? stats[file] : undefined; - } - - getFileName(file: string) { - const { referencedComponents } = this.props; - return referencedComponents[file] ? collapsePath(referencedComponents[file].path, 15) : file; - } - - renderName(file: string) { - const name = this.getFileName(file); - return ( - <span> - <QualifierIcon className="little-spacer-right" qualifier="FIL" /> - {name} - </span> - ); - } - - renderList() { - const { stats } = this.props; + handleSearch = (query: string, page: number) => { + return getTree({ + component: this.props.componentKey, + q: query, + qualifiers: 'FIL', + p: page, + ps: 30 + }).then(({ components, paging }) => ({ paging, results: components })); + }; - if (!stats) { - return null; - } + renderFile = (file: React.ReactNode) => ( + <> + <QualifierIcon className="little-spacer-right" qualifier="FIL" /> + {file} + </> + ); - const files = sortBy(Object.keys(stats), key => -stats[key]); + renderFacetItem = (file: string) => { + const name = this.getFile(file); + return this.renderFile(name); + }; - return ( - <FacetItemsList> - {files.map(file => ( - <FacetItem - active={this.props.files.includes(file)} - key={file} - loading={this.props.loading} - name={this.renderName(file)} - onClick={this.handleItemClick} - stat={formatFacetStat(this.getStat(file))} - tooltip={this.getFileName(file)} - value={file} - /> - ))} - </FacetItemsList> - ); - } + renderSearchResult = (file: TreeComponent, term: string) => { + return this.renderFile(highlightTerm(collapsePath(file.path || file.name, 15), term)); + }; render() { - const { files, stats = {} } = this.props; - const values = files.map(file => this.getFileName(file)); return ( - <FacetBox property={this.property}> - <FacetHeader - name={translate('issues.facet', this.property)} - onClear={this.handleClear} - onClick={this.handleHeaderClick} - open={this.props.open} - values={values} - /> - - <DeferredSpinner loading={this.props.fetching} /> - {this.props.open && ( - <> - {this.renderList()} - <MultipleSelectionHint options={Object.keys(stats).length} values={files.length} /> - </> - )} - </FacetBox> + <ListStyleFacet + facetHeader={translate('issues.facet.files')} + fetching={this.props.fetching} + getFacetItemText={this.getFacetItemText} + getSearchResultKey={this.getSearchResultKey} + getSearchResultText={this.getSearchResultText} + minSearchLength={3} + onChange={this.props.onChange} + onSearch={this.handleSearch} + onToggle={this.props.onToggle} + open={this.props.open} + property="files" + renderFacetItem={this.renderFacetItem} + renderSearchResult={this.renderSearchResult} + searchPlaceholder={translate('search.search_for_files')} + stats={this.props.stats} + values={this.props.files} + /> ); } } 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 171837435ce..82c767e2b4a 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,18 +18,15 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { sortBy, without } from 'lodash'; -import { formatFacetStat, Query, ReferencedComponent } from '../utils'; -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 { Query, ReferencedComponent } from '../utils'; import QualifierIcon from '../../../components/icons-components/QualifierIcon'; import { translate } from '../../../helpers/l10n'; -import DeferredSpinner from '../../../components/common/DeferredSpinner'; -import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHint'; +import ListStyleFacet from '../../../components/facet/ListStyleFacet'; +import { TreeComponent, getTree } from '../../../api/components'; +import { highlightTerm } from '../../../helpers/search'; interface Props { + componentKey: string; fetching: boolean; loading?: boolean; modules: string[]; @@ -41,101 +38,65 @@ interface Props { } export default class ModuleFacet extends React.PureComponent<Props> { - property = 'modules'; - - static defaultProps = { - open: true + getModuleName = (module: string) => { + const { referencedComponents } = this.props; + return referencedComponents[module] ? referencedComponents[module].name : module; }; - handleItemClick = (itemValue: string, multiple: boolean) => { - const { modules } = this.props; - if (multiple) { - const newValue = sortBy( - modules.includes(itemValue) ? without(modules, itemValue) : [...modules, itemValue] - ); - this.props.onChange({ [this.property]: newValue }); - } else { - this.props.onChange({ - [this.property]: modules.includes(itemValue) && modules.length < 2 ? [] : [itemValue] - }); - } + getSearchResultKey = (module: TreeComponent) => { + return module.id; }; - handleHeaderClick = () => { - this.props.onToggle(this.property); + getSearchResultText = (module: TreeComponent) => { + return module.name; }; - handleClear = () => { - this.props.onChange({ [this.property]: [] }); + handleSearch = (query: string, page: number) => { + return getTree({ + component: this.props.componentKey, + q: query, + qualifiers: 'BRC', + p: page, + ps: 30 + }).then(({ components, paging }) => ({ paging, results: components })); }; - getStat(module: string) { - const { stats } = this.props; - return stats ? stats[module] : undefined; - } - - getModuleName(module: string) { - const { referencedComponents } = this.props; - return referencedComponents[module] ? referencedComponents[module].name : module; - } - - renderName(module: string) { - return ( - <span> - <QualifierIcon className="little-spacer-right" qualifier="BRC" /> - {this.getModuleName(module)} - </span> - ); - } - - renderList() { - const { stats } = this.props; + renderModule = (module: React.ReactNode) => ( + <> + <QualifierIcon className="little-spacer-right" qualifier="BRC" /> + {module} + </> + ); - if (!stats) { - return null; - } - - const modules = sortBy(Object.keys(stats), key => -stats[key]); + renderFacetItem = (module: string) => { + const name = this.getModuleName(module); + return this.renderModule(name); + }; - return ( - <FacetItemsList> - {modules.map(module => ( - <FacetItem - active={this.props.modules.includes(module)} - key={module} - loading={this.props.loading} - name={this.renderName(module)} - onClick={this.handleItemClick} - stat={formatFacetStat(this.getStat(module))} - tooltip={this.getModuleName(module)} - value={module} - /> - ))} - </FacetItemsList> - ); - } + renderSearchResult = (module: TreeComponent, term: string) => { + return this.renderModule(highlightTerm(module.name, term)); + }; render() { - const { modules, stats = {} } = this.props; - const values = modules.map(module => this.getModuleName(module)); return ( - <FacetBox property={this.property}> - <FacetHeader - name={translate('issues.facet', this.property)} - onClear={this.handleClear} - onClick={this.handleHeaderClick} - open={this.props.open} - values={values} - /> - - <DeferredSpinner loading={this.props.fetching} /> - {this.props.open && ( - <> - {this.renderList()} - <MultipleSelectionHint options={Object.keys(stats).length} values={modules.length} /> - </> - )} - </FacetBox> + <ListStyleFacet + facetHeader={translate('issues.facet.modules')} + fetching={this.props.fetching} + getFacetItemText={this.getModuleName} + getSearchResultKey={this.getSearchResultKey} + getSearchResultText={this.getSearchResultText} + minSearchLength={3} + onChange={this.props.onChange} + onSearch={this.handleSearch} + onToggle={this.props.onToggle} + open={this.props.open} + property="modules" + renderFacetItem={this.renderFacetItem} + renderSearchResult={this.renderSearchResult} + searchPlaceholder={translate('search.search_for_modules')} + stats={this.props.stats} + values={this.props.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 24f4915fb09..b0989c9e516 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 @@ -53,17 +53,21 @@ export default class ProjectFacet extends React.PureComponent<Props> { ): Promise<{ results: SearchedProject[]; paging: Paging }> => { const { component, organization } = this.props; if (component && ['VW', 'SVW', 'APP'].includes(component.qualifier)) { - return getTree(component.key, { p: page, ps: 30, q: query, qualifiers: 'TRK' }).then( - ({ components, paging }) => ({ - paging, - results: components.map(component => ({ - id: component.refId || component.id, - key: component.key, - name: component.name, - organization: component.organization - })) - }) - ); + return getTree({ + component: component.key, + p: page, + ps: 30, + q: query, + qualifiers: 'TRK' + }).then(({ components, paging }) => ({ + paging, + results: components.map(component => ({ + id: component.refId || component.id, + key: component.key, + name: component.name, + organization: component.organization + })) + })); } return searchProjects({ 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 9b6c3217a94..4a10412105f 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 @@ -197,6 +197,7 @@ export default class Sidebar extends React.PureComponent<Props> { )} {displayModulesFacet && ( <ModuleFacet + componentKey={this.props.component!.key} fetching={this.props.loadingFacets.modules === true} loading={this.props.loading} modules={query.modules} @@ -209,18 +210,19 @@ export default class Sidebar extends React.PureComponent<Props> { )} {displayDirectoriesFacet && ( <DirectoryFacet + componentKey={this.props.component!.key} directories={query.directories} fetching={this.props.loadingFacets.directories === true} loading={this.props.loading} onChange={this.props.onFilterChange} onToggle={this.props.onFacetToggle} open={!!openFacets.directories} - referencedComponents={this.props.referencedComponents} stats={facets.directories} /> )} {displayFilesFacet && ( <FileFacet + componentKey={this.props.component!.key} fetching={this.props.loadingFacets.files === true} files={query.files} loading={this.props.loading} @@ -249,11 +251,13 @@ export default class Sidebar extends React.PureComponent<Props> { {displayAuthorFacet && ( <AuthorFacet authors={query.authors} + componentKey={this.props.component && this.props.component.key} fetching={this.props.loadingFacets.authors === true} loading={this.props.loading} onChange={this.props.onFilterChange} onToggle={this.props.onFacetToggle} open={!!openFacets.authors} + organization={organizationKey} stats={facets.authors} /> )} 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 75d251a4dfe..f68af7618f4 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 @@ -39,14 +39,15 @@ interface Props { tags: string[]; } +const SEARCH_SIZE = 100; + export default class TagFacet extends React.PureComponent<Props> { handleSearch = (query: string) => { - return searchIssueTags({ organization: this.props.organization, ps: 50, q: query }).then( - tags => ({ - paging: { pageIndex: 1, pageSize: tags.length, total: tags.length }, - results: tags - }) - ); + return searchIssueTags({ + organization: this.props.organization, + ps: SEARCH_SIZE, + q: query + }).then(tags => ({ maxResults: tags.length === SEARCH_SIZE, results: tags })); }; getTagName = (tag: string) => { diff --git a/server/sonar-web/src/main/js/apps/project-admin/store/actions.js b/server/sonar-web/src/main/js/apps/project-admin/store/actions.js index 189832011e1..36eee2cfd81 100644 --- a/server/sonar-web/src/main/js/apps/project-admin/store/actions.js +++ b/server/sonar-web/src/main/js/apps/project-admin/store/actions.js @@ -72,8 +72,7 @@ const receiveProjectModules = (projectKey, modules) => ({ }); export const fetchProjectModules = projectKey => dispatch => { - const options = { qualifiers: 'BRC', s: 'name', ps: 500 }; - getTree(projectKey, options).then( + getTree({ component: projectKey, qualifiers: 'BRC', s: 'name', ps: 500 }).then( r => { dispatch(receiveProjectModules(projectKey, r.components)); }, 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 4b0bc9c93ac..d941c3c17df 100644 --- a/server/sonar-web/src/main/js/components/facet/ListStyleFacet.tsx +++ b/server/sonar-web/src/main/js/components/facet/ListStyleFacet.tsx @@ -41,8 +41,12 @@ export interface Props<S> { loading?: boolean; maxInitialItems?: number; maxItems?: number; + minSearchLength?: number; onChange: (changes: { [x: string]: string | string[] }) => void; - onSearch: (query: string, page?: number) => Promise<{ results: S[]; paging: Paging }>; + onSearch: ( + query: string, + page?: number + ) => Promise<{ maxResults?: boolean; results: S[]; paging?: Paging }>; onToggle: (property: string) => void; open: boolean; property: string; @@ -57,6 +61,7 @@ interface State<S> { autoFocus: boolean; query: string; searching: boolean; + searchMaxResults?: boolean; searchPaging?: Paging; searchResults?: S[]; showFullList: boolean; @@ -87,7 +92,13 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S this.setState({ autoFocus: true }); } else if (prevProps.open && !this.props.open) { // reset state when closing the facet - this.setState({ query: '', searchResults: undefined, searching: false, showFullList: false }); + this.setState({ + query: '', + searchMaxResults: undefined, + searchResults: undefined, + searching: false, + showFullList: false + }); } else if ( prevProps.stats !== this.props.stats && Object.keys(this.props.stats || {}).length < this.props.maxInitialItems! @@ -132,9 +143,14 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S search = (query: string) => { if (query.length >= 2) { this.setState({ query, searching: true }); - this.props.onSearch(query).then(({ paging, results }) => { + this.props.onSearch(query).then(({ maxResults, paging, results }) => { if (this.mounted) { - this.setState({ searching: false, searchResults: results, searchPaging: paging }); + this.setState({ + searching: false, + searchMaxResults: maxResults, + searchResults: results, + searchPaging: paging + }); } }, this.stopSearching); } else { @@ -241,12 +257,14 @@ 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={2} + minLength={minSearchLength} onChange={this.search} placeholder={this.props.searchPlaceholder} value={this.state.query} @@ -255,13 +273,13 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S } renderSearchResults() { - const { searching, searchResults, searchPaging } = this.state; + const { searching, searchMaxResults, searchResults, searchPaging } = this.state; if (!searching && (!searchResults || !searchResults.length)) { return <div className="note spacer-bottom">{translate('no_results')}</div>; } - if (!searchResults || !searchPaging) { + if (!searchResults) { // initial search return null; } @@ -271,13 +289,20 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S <FacetItemsList> {searchResults.map(result => this.renderSearchResult(result))} </FacetItemsList> - <ListFooter - className="spacer-bottom" - count={searchResults.length} - loadMore={this.searchMore} - ready={!searching} - total={searchPaging.total} - /> + {searchMaxResults && ( + <div className="alert alert-warning spacer-top"> + {translate('facet_might_have_more_results')} + </div> + )} + {searchPaging && ( + <ListFooter + className="spacer-bottom" + count={searchResults.length} + loadMore={this.searchMore} + ready={!searching} + total={searchPaging.total} + /> + )} </> ); } |