aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web
diff options
context:
space:
mode:
authorStas Vilchik <stas.vilchik@sonarsource.com>2018-08-10 10:48:27 +0200
committerSonarTech <sonartech@sonarsource.com>2018-08-21 20:21:02 +0200
commit20a8ceffbe12771dea8f9186a408aabbab32a8d7 (patch)
tree18c23ced96fe50c3730b214a1cdccf247a814b3d /server/sonar-web
parent15f3d9c2584cca304590ad68dea4d025ac356813 (diff)
downloadsonarqube-20a8ceffbe12771dea8f9186a408aabbab32a8d7.tar.gz
sonarqube-20a8ceffbe12771dea8f9186a408aabbab32a8d7.zip
SONAR-9369 Add search for module, directory, file and author facets (#606)
Diffstat (limited to 'server/sonar-web')
-rw-r--r--server/sonar-web/src/main/js/api/components.ts19
-rw-r--r--server/sonar-web/src/main/js/api/issues.ts9
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/Search.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.tsx116
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.tsx142
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.tsx145
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/ModuleFacet.tsx141
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.tsx26
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx6
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.tsx13
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/store/actions.js3
-rw-r--r--server/sonar-web/src/main/js/components/facet/ListStyleFacet.tsx53
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}
+ />
+ )}
</>
);
}