aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src
diff options
context:
space:
mode:
authorstanislavh <stanislav.honcharov@sonarsource.com>2023-04-21 14:56:17 +0200
committersonartech <sonartech@sonarsource.com>2023-04-25 20:03:00 +0000
commit3aa961cd6a7d63edb7ac011c27ec695dfb94c253 (patch)
tree43a1cae5fd31224a76457ed81e0c5bacfaf1fa1d /server/sonar-web/src
parentfc5bd48d213b39a83a3828eaec1a9e89b5b88fbd (diff)
downloadsonarqube-3aa961cd6a7d63edb7ac011c27ec695dfb94c253.tar.gz
sonarqube-3aa961cd6a7d63edb7ac011c27ec695dfb94c253.zip
SONAR-19069 Add Show more filters button
Diffstat (limited to 'server/sonar-web/src')
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/FacetsList.tsx1
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/FacetsList-test.tsx.snap1
-rw-r--r--server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx8
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx24
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.tsx18
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.tsx17
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/CharacteristicFacet.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.tsx12
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.tsx22
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.tsx22
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/FiltersVisibilityButton.tsx44
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.tsx17
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.tsx22
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.tsx11
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.tsx7
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/ScopeFacet.tsx6
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx95
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx70
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.tsx10
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.tsx17
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.tsx7
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-it.tsx51
-rw-r--r--server/sonar-web/src/main/js/apps/issues/test-utils.tsx2
23 files changed, 368 insertions, 120 deletions
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/FacetsList.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/FacetsList.tsx
index a9b52ce93e8..1bb599037e1 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/components/FacetsList.tsx
+++ b/server/sonar-web/src/main/js/apps/coding-rules/components/FacetsList.tsx
@@ -125,6 +125,7 @@ export default function FacetsList(props: FacetsListProps) {
sonarsourceSecurity={props.query.sonarsourceSecurity}
sonarsourceSecurityOpen={!!props.openFacets.sonarsourceSecurity}
sonarsourceSecurityStats={props.facets && props.facets.sonarsourceSecurity}
+ forceShow={true}
/>
<AvailableSinceFacet
onChange={props.onFilterChange}
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/FacetsList-test.tsx.snap b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/FacetsList-test.tsx.snap
index ec82fc3872b..c27efaa2bf5 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/FacetsList-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/FacetsList-test.tsx.snap
@@ -40,6 +40,7 @@ exports[`should render correctly 1`] = `
fetchingOwaspTop10={false}
fetchingOwaspTop10-2021={false}
fetchingSonarSourceSecurity={false}
+ forceShow={true}
onChange={[MockFunction]}
onToggle={[MockFunction]}
open={false}
diff --git a/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx b/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx
index 113bd3addf5..78b6be118e1 100644
--- a/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx
@@ -61,6 +61,7 @@ describe('issues app', () => {
it('should support OWASP Top 10 version 2021', async () => {
const user = userEvent.setup();
renderIssueApp();
+ await user.click(ui.showFiltersButton().get());
await user.click(screen.getByRole('button', { name: 'issues.facet.standards' }));
const owaspTop102021 = screen.getByRole('button', { name: 'issues.facet.owaspTop10_2021' });
expect(owaspTop102021).toBeInTheDocument();
@@ -272,6 +273,7 @@ describe('issues app', () => {
const user = userEvent.setup();
renderIssueApp();
await waitOnDataLoaded();
+ await user.click(ui.showFiltersButton().get());
// Ensure issue type filter is unchecked
await user.click(ui.typeFacet.get());
@@ -327,6 +329,7 @@ describe('issues app', () => {
const user = userEvent.setup();
renderIssueApp();
await waitOnDataLoaded();
+ await user.click(ui.showFiltersButton().get());
// Select a characteristic
await user.click(ui.clearCharacteristicFilter.get());
@@ -435,6 +438,7 @@ describe('issues app', () => {
issuesHandler.setCurrentUser(currentUser);
renderIssueApp(currentUser);
await waitOnDataLoaded();
+ await user.click(ui.showFiltersButton().get());
// Select a specific date range such that only one issue matches
await user.click(ui.creationDateFacet.get());
@@ -480,6 +484,7 @@ describe('issues app', () => {
renderIssueApp();
+ await user.click(ui.showFiltersButton().get());
await user.click(await ui.ruleFacet.find());
await user.type(ui.ruleFacetSearch.get(), 'rule');
expect(within(ui.ruleFacetList.get()).getAllByRole('checkbox')).toHaveLength(2);
@@ -494,6 +499,7 @@ describe('issues app', () => {
})
).toBeInTheDocument();
+ await user.click(await ui.typeFacet.find());
await user.click(ui.vulnerabilityIssueTypeFilter.get());
// after changing the issue type filter, search field is reset, so we type again
await user.type(ui.ruleFacetSearch.get(), 'rule');
@@ -515,6 +521,7 @@ describe('issues app', () => {
renderIssueApp();
+ await user.click(ui.showFiltersButton().get());
await user.click(await ui.languageFacet.find());
expect(await ui.languageFacetList.find()).toBeInTheDocument();
expect(
@@ -526,6 +533,7 @@ describe('issues app', () => {
await user.click(ui.languageFacet.get());
expect(ui.languageFacetList.query()).not.toBeInTheDocument();
+ await user.click(await ui.typeFacet.find());
await user.click(ui.vulnerabilityIssueTypeFilter.get());
await user.click(ui.languageFacet.get());
expect(await ui.languageFacetList.find()).toBeInTheDocument();
diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx
index dc3e038eb77..4b482da7fea 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx
@@ -33,11 +33,11 @@ import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget';
import EmptySearch from '../../../components/common/EmptySearch';
import FiltersHeader from '../../../components/common/FiltersHeader';
import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
+import { Button } from '../../../components/controls/buttons';
import ButtonToggle from '../../../components/controls/ButtonToggle';
import Checkbox from '../../../components/controls/Checkbox';
import HelpTooltip from '../../../components/controls/HelpTooltip';
import ListFooter from '../../../components/controls/ListFooter';
-import { Button } from '../../../components/controls/buttons';
import Suggestions from '../../../components/embed-docs-modal/Suggestions';
import withIndexationGuard from '../../../components/hoc/withIndexationGuard';
import { Location, Router, withRouter } from '../../../components/hoc/withRouter';
@@ -80,23 +80,24 @@ import { CurrentUser, UserBase } from '../../../types/users';
import * as actions from '../actions';
import ConciseIssuesList from '../conciseIssuesList/ConciseIssuesList';
import ConciseIssuesListHeader from '../conciseIssuesList/ConciseIssuesListHeader';
+import FiltersVisibilityButton from '../sidebar/FiltersVisibilityButton';
import Sidebar from '../sidebar/Sidebar';
import '../styles.css';
import {
- OpenFacets,
- Query,
- STANDARDS,
areMyIssuesSelected,
areQueriesEqual,
getOpen,
getOpenIssue,
+ OpenFacets,
parseFacets,
parseQuery,
+ Query,
saveMyIssues,
serializeQuery,
shouldOpenSonarSourceSecurityFacet,
shouldOpenStandardsChildFacet,
shouldOpenStandardsFacet,
+ STANDARDS,
} from '../utils';
import BulkChangeModal, { MAX_PAGE_SIZE } from './BulkChangeModal';
import IssueHeader from './IssueHeader';
@@ -129,6 +130,7 @@ export interface State {
loadingMore: boolean;
locationsNavigator: boolean;
myIssues: boolean;
+ showAllFilters: boolean;
openFacets: OpenFacets;
openIssue?: Issue;
openPopup?: { issue: string; name: string };
@@ -168,6 +170,7 @@ export class App extends React.PureComponent<Props, State> {
loadingMore: false,
locationsNavigator: false,
myIssues: areMyIssuesSelected(props.location.query),
+ showAllFilters: false,
openFacets: {
characteristics: {
[IssueCharacteristicFitFor.Production]: true,
@@ -644,6 +647,12 @@ export class App extends React.PureComponent<Props, State> {
return translateWithParameters('issues.bulk_change_X_issues', count);
};
+ handleShowFiltersChange = (showAllFilters: boolean) => {
+ this.setState({
+ showAllFilters,
+ });
+ };
+
handleFilterChange = (changes: Partial<Query>) => {
this.props.router.push({
pathname: this.props.location.pathname,
@@ -905,7 +914,7 @@ export class App extends React.PureComponent<Props, State> {
renderFacets() {
const { component, currentUser, branchLike } = this.props;
- const { query } = this.state;
+ const { query, showAllFilters } = this.state;
return (
<div className="layout-page-filters">
@@ -934,12 +943,17 @@ export class App extends React.PureComponent<Props, State> {
onFilterChange={this.handleFilterChange}
openFacets={this.state.openFacets}
query={query}
+ showAllFilters={showAllFilters}
referencedComponentsById={this.state.referencedComponentsById}
referencedComponentsByKey={this.state.referencedComponentsByKey}
referencedLanguages={this.state.referencedLanguages}
referencedRules={this.state.referencedRules}
referencedUsers={this.state.referencedUsers}
/>
+ <FiltersVisibilityButton
+ showAllFilters={showAllFilters}
+ onClick={this.handleShowFiltersChange}
+ />
</div>
);
}
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 89d3d4f6f31..ff8ff15a035 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
@@ -39,6 +39,7 @@ interface Props {
query: Query;
stats: Dict<number> | undefined;
referencedUsers: Dict<UserBase>;
+ forceShow: boolean;
}
export default class AssigneeFacet extends React.PureComponent<Props> {
@@ -137,15 +138,20 @@ export default class AssigneeFacet extends React.PureComponent<Props> {
};
render() {
- const values = [...this.props.assignees];
- if (!this.props.assigned) {
+ const { forceShow, assignees, assigned, stats, open, fetching, query } = this.props;
+ const values = [...assignees];
+ if (!assigned) {
values.push('');
}
+ if (values.length < 1 && !forceShow) {
+ return null;
+ }
+
return (
<ListStyleFacet<UserBase>
facetHeader={translate('issues.facet.assignees')}
- fetching={this.props.fetching}
+ fetching={fetching}
getFacetItemText={this.getAssigneeName}
getSearchResultKey={(user) => user.login}
getSearchResultText={(user) => user.name || user.login}
@@ -157,13 +163,13 @@ export default class AssigneeFacet extends React.PureComponent<Props> {
onItemClick={this.handleItemClick}
onSearch={this.handleSearch}
onToggle={this.props.onToggle}
- open={this.props.open}
+ open={open}
property="assignees"
- query={omit(this.props.query, 'assigned', 'assignees')}
+ query={omit(query, 'assigned', 'assignees')}
renderFacetItem={this.renderFacetItem}
renderSearchResult={this.renderSearchResult}
searchPlaceholder={translate('search.search_for_users')}
- stats={this.props.stats}
+ stats={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 1647c689495..ada0eb6b2d7 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
@@ -37,6 +37,7 @@ interface Props {
query: Query;
stats: Dict<number> | undefined;
author: string[];
+ forceShow: boolean;
}
const SEARCH_SIZE = 100;
@@ -66,10 +67,16 @@ export default class AuthorFacet extends React.PureComponent<Props> {
};
render() {
+ const { forceShow, fetching, open, stats, author, query } = this.props;
+
+ if (author.length < 1 && !forceShow) {
+ return null;
+ }
+
return (
<ListStyleFacet<string>
facetHeader={translate('issues.facet.authors')}
- fetching={this.props.fetching}
+ fetching={fetching}
getFacetItemText={this.identity}
getSearchResultKey={this.identity}
getSearchResultText={this.identity}
@@ -77,14 +84,14 @@ export default class AuthorFacet extends React.PureComponent<Props> {
onChange={this.props.onChange}
onSearch={this.handleSearch}
onToggle={this.props.onToggle}
- open={this.props.open}
+ open={open}
property="author"
- query={omit(this.props.query, 'author')}
+ query={omit(query, 'author')}
renderFacetItem={this.identity}
renderSearchResult={this.renderSearchResult}
searchPlaceholder={translate('search.search_for_authors')}
- stats={this.props.stats}
- values={this.props.author}
+ stats={stats}
+ values={author}
/>
);
}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/CharacteristicFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/CharacteristicFacet.tsx
index 094f3e27c72..eb0b153027e 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/CharacteristicFacet.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/CharacteristicFacet.tsx
@@ -154,7 +154,9 @@ export default class CharacteristicFacet extends React.PureComponent<Props> {
{this.props.open && (
<>
- <FacetItemsList>{availableCharacteristics.map(this.renderItem)}</FacetItemsList>
+ <FacetItemsList label={this.property}>
+ {availableCharacteristics.map(this.renderItem)}
+ </FacetItemsList>
<MultipleSelectionHint
options={Object.keys(availableCharacteristics).length}
values={values.length}
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 5918ccc99dd..fd37e3fb43c 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
@@ -50,6 +50,7 @@ interface Props {
open: boolean;
inNewCodePeriod: boolean;
stats: Dict<number> | undefined;
+ forceShow: boolean;
}
export class CreationDateFacet extends React.PureComponent<Props & WrappedComponentProps> {
@@ -259,10 +260,10 @@ export class CreationDateFacet extends React.PureComponent<Props & WrappedCompon
if (createdAt) {
return (
<div className="search-navigator-facet-container">
- <DateTimeFormatter date={this.props.createdAt} />
+ <DateTimeFormatter date={createdAt} />
<br />
<span className="note">
- <DateFromNow date={this.props.createdAt} />
+ <DateFromNow date={createdAt} />
</span>
</div>
);
@@ -287,7 +288,12 @@ export class CreationDateFacet extends React.PureComponent<Props & WrappedCompon
}
render() {
- const { fetching, open } = this.props;
+ const { forceShow, open, fetching } = this.props;
+ const values = this.getValues();
+
+ if (values.length < 1 && !forceShow) {
+ return null;
+ }
return (
<FacetBox property={this.property}>
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 b4888017813..00b1ae90115 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
@@ -29,6 +29,7 @@ import { highlightTerm } from '../../../helpers/search';
import { BranchLike } from '../../../types/branch-like';
import { TreeComponentWithPath } from '../../../types/component';
import { Facet } from '../../../types/issues';
+import { MetricKey } from '../../../types/metrics';
import { Query } from '../utils';
interface Props {
@@ -42,6 +43,7 @@ interface Props {
open: boolean;
query: Query;
stats: Facet | undefined;
+ forceShow: boolean;
}
export default class DirectoryFacet extends React.PureComponent<Props> {
@@ -73,7 +75,7 @@ export default class DirectoryFacet extends React.PureComponent<Props> {
};
loadSearchResultCount = (directories: TreeComponentWithPath[]) => {
- return this.props.loadSearchResultCount('directories', {
+ return this.props.loadSearchResultCount(MetricKey.directories, {
directories: directories.map((directory) => directory.path),
});
};
@@ -94,10 +96,16 @@ export default class DirectoryFacet extends React.PureComponent<Props> {
};
render() {
+ const { forceShow, directories, stats, fetching, open, query } = this.props;
+
+ if (directories.length < 1 && !forceShow) {
+ return null;
+ }
+
return (
<ListStyleFacet<TreeComponentWithPath>
facetHeader={translate('issues.facet.directories')}
- fetching={this.props.fetching}
+ fetching={fetching}
getFacetItemText={this.getFacetItemText}
getSearchResultKey={this.getSearchResultKey}
getSearchResultText={this.getSearchResultText}
@@ -106,14 +114,14 @@ export default class DirectoryFacet extends React.PureComponent<Props> {
onChange={this.props.onChange}
onSearch={this.handleSearch}
onToggle={this.props.onToggle}
- open={this.props.open}
- property="directories"
- query={omit(this.props.query, 'directories')}
+ open={open}
+ property={MetricKey.directories}
+ query={omit(query, MetricKey.directories)}
renderFacetItem={this.renderFacetItem}
renderSearchResult={this.renderSearchResult}
searchPlaceholder={translate('search.search_for_directories')}
- stats={this.props.stats}
- values={this.props.directories}
+ stats={stats}
+ values={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 e315f8e0454..e27b23a5104 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
@@ -30,6 +30,7 @@ import { isDefined } from '../../../helpers/types';
import { BranchLike } from '../../../types/branch-like';
import { TreeComponentWithPath } from '../../../types/component';
import { Facet } from '../../../types/issues';
+import { MetricKey } from '../../../types/metrics';
import { Query } from '../utils';
interface Props {
@@ -43,6 +44,7 @@ interface Props {
open: boolean;
query: Query;
stats: Facet | undefined;
+ forceShow: boolean;
}
const MAX_PATH_LENGTH = 15;
@@ -75,7 +77,7 @@ export default class FileFacet extends React.PureComponent<Props> {
};
loadSearchResultCount = (files: TreeComponentWithPath[]) => {
- return this.props.loadSearchResultCount('files', {
+ return this.props.loadSearchResultCount(MetricKey.files, {
files: files
.map((file) => {
return file.path;
@@ -106,10 +108,16 @@ export default class FileFacet extends React.PureComponent<Props> {
};
render() {
+ const { forceShow, files, fetching, open, query, stats } = this.props;
+
+ if (files.length < 1 && !forceShow) {
+ return null;
+ }
+
return (
<ListStyleFacet<TreeComponentWithPath>
facetHeader={translate('issues.facet.files')}
- fetching={this.props.fetching}
+ fetching={fetching}
getFacetItemText={this.getFacetItemText}
getSearchResultKey={this.getSearchResultKey}
getSearchResultText={this.getSearchResultText}
@@ -118,14 +126,14 @@ export default class FileFacet extends React.PureComponent<Props> {
onChange={this.props.onChange}
onSearch={this.handleSearch}
onToggle={this.props.onToggle}
- open={this.props.open}
- property="files"
- query={omit(this.props.query, 'files')}
+ open={open}
+ property={MetricKey.files}
+ query={omit(query, MetricKey.files)}
renderFacetItem={this.renderFacetItem}
renderSearchResult={this.renderSearchResult}
searchPlaceholder={translate('search.search_for_files')}
- stats={this.props.stats}
- values={this.props.files}
+ stats={stats}
+ values={files}
/>
);
}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/FiltersVisibilityButton.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/FiltersVisibilityButton.tsx
new file mode 100644
index 00000000000..45da86c9112
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/FiltersVisibilityButton.tsx
@@ -0,0 +1,44 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+import classNames from 'classnames';
+import React from 'react';
+import { Button } from '../../../components/controls/buttons';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+ showAllFilters: boolean;
+ onClick: (val: boolean) => void;
+}
+
+export default function FiltersVisibilityButton(props: Props) {
+ const { showAllFilters } = props;
+
+ return (
+ <div className="display-flex-justify-center spacer-top">
+ <Button
+ onClick={() => props.onClick(!showAllFilters)}
+ className={classNames({ it__show_more_facets: !showAllFilters })}
+ >
+ {translate(showAllFilters ? 'issues.show_less_filters' : 'issues.show_more_filters')}
+ </Button>
+ </div>
+ );
+}
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 992f4a63b5c..a2b34e96573 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
@@ -39,6 +39,7 @@ interface Props {
query: Query;
referencedLanguages: Dict<ReferencedLanguage>;
stats: Dict<number> | undefined;
+ forceShow: boolean;
}
class LanguageFacet extends React.PureComponent<Props> {
@@ -79,10 +80,16 @@ class LanguageFacet extends React.PureComponent<Props> {
};
render() {
+ const { forceShow, stats, selectedLanguages, open, fetching, query } = this.props;
+
+ if (selectedLanguages.length < 1 && !forceShow) {
+ return null;
+ }
+
return (
<ListStyleFacet<Language>
facetHeader={translate('issues.facet.languages')}
- fetching={this.props.fetching}
+ fetching={fetching}
getFacetItemText={this.getLanguageName}
getSearchResultKey={(language) => language.key}
getSearchResultText={(language) => language.name}
@@ -91,14 +98,14 @@ class LanguageFacet extends React.PureComponent<Props> {
onChange={this.props.onChange}
onSearch={this.handleSearch}
onToggle={this.props.onToggle}
- open={this.props.open}
+ open={open}
property="languages"
- query={omit(this.props.query, 'languages')}
+ query={omit(query, 'languages')}
renderFacetItem={this.getLanguageName}
renderSearchResult={this.renderSearchResult}
searchPlaceholder={translate('search.search_for_languages')}
- stats={this.props.stats}
- values={this.props.selectedLanguages}
+ stats={stats}
+ values={selectedLanguages}
/>
);
}
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 4c738b2f4fa..93ebce99746 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
@@ -26,6 +26,7 @@ import { translate } from '../../../helpers/l10n';
import { highlightTerm } from '../../../helpers/search';
import { ComponentQualifier } from '../../../types/component';
import { Facet, ReferencedComponent } from '../../../types/issues';
+import { MetricKey } from '../../../types/metrics';
import { Component, Dict, Paging } from '../../../types/types';
import { Query } from '../utils';
@@ -40,6 +41,7 @@ interface Props {
query: Query;
referencedComponents: Dict<ReferencedComponent>;
stats: Dict<number> | undefined;
+ forceShow: boolean;
}
interface SearchedProject {
@@ -95,7 +97,7 @@ export default class ProjectFacet extends React.PureComponent<Props> {
};
loadSearchResultCount = (projects: SearchedProject[]) => {
- return this.props.loadSearchResultCount('projects', {
+ return this.props.loadSearchResultCount(MetricKey.projects, {
projects: projects.map((project) => project.key),
});
};
@@ -117,10 +119,16 @@ export default class ProjectFacet extends React.PureComponent<Props> {
);
render() {
+ const { forceShow, projects, stats, open, fetching, query } = this.props;
+
+ if (projects.length < 1 && !forceShow) {
+ return null;
+ }
+
return (
<ListStyleFacet<SearchedProject>
facetHeader={translate('issues.facet.projects')}
- fetching={this.props.fetching}
+ fetching={fetching}
getFacetItemText={this.getProjectName}
getSearchResultKey={(project) => project.key}
getSearchResultText={(project) => project.name}
@@ -128,14 +136,14 @@ export default class ProjectFacet extends React.PureComponent<Props> {
onChange={this.props.onChange}
onSearch={this.handleSearch}
onToggle={this.props.onToggle}
- open={this.props.open}
- property="projects"
- query={omit(this.props.query, 'projects')}
+ open={open}
+ property={MetricKey.projects}
+ query={omit(query, MetricKey.projects)}
renderFacetItem={this.renderFacetItem}
renderSearchResult={this.renderSearchResult}
searchPlaceholder={translate('search.search_for_projects')}
- stats={this.props.stats}
- values={this.props.projects}
+ stats={stats}
+ values={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 32578f5dc75..c437f10efc4 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
@@ -37,6 +37,7 @@ interface Props {
resolved: boolean;
resolutions: string[];
stats: Dict<number> | undefined;
+ forceShow: boolean;
}
const RESOLUTIONS = [
@@ -55,10 +56,10 @@ export default class ResolutionFacet extends React.PureComponent<Props> {
};
handleItemClick = (itemValue: string, multiple: boolean) => {
- const { resolutions } = this.props;
+ const { resolutions, resolved } = this.props;
if (itemValue === '') {
// unresolved
- this.props.onChange({ resolved: !this.props.resolved, resolutions: [] });
+ this.props.onChange({ resolved: !resolved, resolutions: [] });
} else if (multiple) {
const newValue = orderBy(
resolutions.includes(itemValue)
@@ -115,9 +116,13 @@ export default class ResolutionFacet extends React.PureComponent<Props> {
};
render() {
- const { fetching, open, resolutions, stats = {} } = this.props;
+ const { resolutions, stats = {}, forceShow, fetching, open } = this.props;
const values = resolutions.map((resolution) => this.getFacetItemName(resolution));
+ if (values.length < 1 && !forceShow) {
+ return null;
+ }
+
return (
<FacetBox property={this.property}>
<FacetHeader
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 1da206faff1..494d420b810 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
@@ -36,6 +36,7 @@ interface Props {
query: Query;
referencedRules: Dict<ReferencedRule>;
stats: Dict<number> | undefined;
+ forceShow: boolean;
}
export default class RuleFacet extends React.PureComponent<Props> {
@@ -78,7 +79,11 @@ export default class RuleFacet extends React.PureComponent<Props> {
};
render() {
- const { fetching, open, query, stats } = this.props;
+ const { forceShow, stats, query, open, fetching } = this.props;
+
+ if (query.rules.length < 1 && !forceShow) {
+ return null;
+ }
return (
<ListStyleFacet<Rule>
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/ScopeFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/ScopeFacet.tsx
index 329c046b92d..73320c77762 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/ScopeFacet.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/ScopeFacet.tsx
@@ -37,13 +37,17 @@ export interface ScopeFacetProps {
open: boolean;
scopes: string[];
stats: Dict<number> | undefined;
+ forceShow: boolean;
}
export default function ScopeFacet(props: ScopeFacetProps) {
- const { fetching, open, scopes = [], stats = {} } = props;
+ const { fetching, open, scopes = [], stats = {}, forceShow } = props;
const values = scopes.map((scope) => translate('issue.scope', scope));
const property = 'scopes';
+ if (values.length < 1 && !forceShow) {
+ return null;
+ }
return (
<FacetBox property={property}>
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 8f25db0785b..c677090a724 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
@@ -76,11 +76,21 @@ export interface Props {
referencedLanguages: Dict<ReferencedLanguage>;
referencedRules: Dict<ReferencedRule>;
referencedUsers: Dict<UserBase>;
+ showAllFilters: boolean;
}
export class Sidebar extends React.PureComponent<Props> {
renderComponentFacets() {
- const { component, facets, loadingFacets, openFacets, query, branchLike } = this.props;
+ const {
+ component,
+ facets,
+ loadingFacets,
+ openFacets,
+ query,
+ branchLike,
+ showAllFilters,
+ loadSearchResultCount,
+ } = this.props;
const hasFileOrDirectory =
!isApplication(component?.qualifier) && !isPortfolioLike(component?.qualifier);
if (!component || !hasFileOrDirectory) {
@@ -88,7 +98,7 @@ export class Sidebar extends React.PureComponent<Props> {
}
const commonProps = {
componentKey: component.key,
- loadSearchResultCount: this.props.loadSearchResultCount,
+ loadSearchResultCount,
onChange: this.props.onFilterChange,
onToggle: this.props.onFacetToggle,
query,
@@ -102,6 +112,7 @@ export class Sidebar extends React.PureComponent<Props> {
fetching={loadingFacets.directories === true}
open={!!openFacets.directories}
stats={facets.directories}
+ forceShow={showAllFilters}
{...commonProps}
/>
)}
@@ -111,6 +122,7 @@ export class Sidebar extends React.PureComponent<Props> {
files={query.files}
open={!!openFacets.files}
stats={facets.files}
+ forceShow={showAllFilters}
{...commonProps}
/>
</>
@@ -126,6 +138,13 @@ export class Sidebar extends React.PureComponent<Props> {
openFacets,
query,
branchLike,
+ showAllFilters,
+ loadingFacets,
+ loadSearchResultCount,
+ referencedRules,
+ referencedLanguages,
+ referencedComponentsByKey,
+ referencedUsers,
} = this.props;
const disableDeveloperAggregatedInfo =
@@ -144,14 +163,14 @@ export class Sidebar extends React.PureComponent<Props> {
<>
{displayPeriodFilter && (
<PeriodFilter
- fetching={this.props.loadingFacets.period === true}
+ fetching={loadingFacets.period === true}
onChange={this.props.onFilterChange}
stats={facets.period}
newCodeSelected={query.inNewCodePeriod}
/>
)}
<CharacteristicFacet
- fetching={this.props.loadingFacets.characteristics === true}
+ fetching={loadingFacets.characteristics === true}
onChange={this.props.onFilterChange}
onToggle={this.props.onFacetToggle}
open={openFacets.characteristics?.[IssueCharacteristicFitFor.Production]}
@@ -169,7 +188,7 @@ export class Sidebar extends React.PureComponent<Props> {
characteristics={query.characteristics as IssueCharacteristic[]}
/>
<SeverityFacet
- fetching={this.props.loadingFacets.severities === true}
+ fetching={loadingFacets.severities === true}
onChange={this.props.onFilterChange}
onToggle={this.props.onFacetToggle}
open={!!openFacets.severities}
@@ -177,47 +196,53 @@ export class Sidebar extends React.PureComponent<Props> {
stats={facets.severities}
/>
<TypeFacet
- fetching={this.props.loadingFacets.types === true}
+ fetching={loadingFacets.types === true}
onChange={this.props.onFilterChange}
onToggle={this.props.onFacetToggle}
open={!!openFacets.types}
stats={facets.types}
types={query.types}
+ forceShow={showAllFilters}
/>
+
<ScopeFacet
- fetching={this.props.loadingFacets.scopes === true}
+ fetching={loadingFacets.scopes === true}
onChange={this.props.onFilterChange}
onToggle={this.props.onFacetToggle}
open={!!openFacets.scopes}
stats={facets.scopes}
scopes={query.scopes}
+ forceShow={showAllFilters}
/>
+
<ResolutionFacet
- fetching={this.props.loadingFacets.resolutions === true}
+ fetching={loadingFacets.resolutions === true}
onChange={this.props.onFilterChange}
onToggle={this.props.onFacetToggle}
open={!!openFacets.resolutions}
resolutions={query.resolutions}
resolved={query.resolved}
stats={facets.resolutions}
+ forceShow={showAllFilters}
/>
<StatusFacet
- fetching={this.props.loadingFacets.statuses === true}
+ fetching={loadingFacets.statuses === true}
onChange={this.props.onFilterChange}
onToggle={this.props.onFacetToggle}
open={!!openFacets.statuses}
stats={facets.statuses}
statuses={query.statuses}
+ forceShow={showAllFilters}
/>
<StandardFacet
cwe={query.cwe}
cweOpen={!!openFacets.cwe}
cweStats={facets.cwe}
- fetchingCwe={this.props.loadingFacets.cwe === true}
- fetchingOwaspTop10={this.props.loadingFacets.owaspTop10 === true}
- fetchingOwaspTop10-2021={this.props.loadingFacets['owaspTop10-2021'] === true}
- fetchingSonarSourceSecurity={this.props.loadingFacets.sonarsourceSecurity === true}
- loadSearchResultCount={this.props.loadSearchResultCount}
+ fetchingCwe={loadingFacets.cwe === true}
+ fetchingOwaspTop10={loadingFacets.owaspTop10 === true}
+ fetchingOwaspTop10-2021={loadingFacets['owaspTop10-2021'] === true}
+ fetchingSonarSourceSecurity={loadingFacets.sonarsourceSecurity === true}
+ loadSearchResultCount={loadSearchResultCount}
onChange={this.props.onFilterChange}
onToggle={this.props.onFacetToggle}
open={!!openFacets.standards}
@@ -231,6 +256,7 @@ export class Sidebar extends React.PureComponent<Props> {
sonarsourceSecurity={query.sonarsourceSecurity}
sonarsourceSecurityOpen={!!openFacets.sonarsourceSecurity}
sonarsourceSecurityStats={facets.sonarsourceSecurity}
+ forceShow={showAllFilters}
/>
<CreationDateFacet
component={component}
@@ -239,58 +265,63 @@ export class Sidebar extends React.PureComponent<Props> {
createdAt={query.createdAt}
createdBefore={query.createdBefore}
createdInLast={query.createdInLast}
- fetching={this.props.loadingFacets.createdAt === true}
+ fetching={loadingFacets.createdAt === true}
onChange={this.props.onFilterChange}
onToggle={this.props.onFacetToggle}
open={!!openFacets.createdAt}
inNewCodePeriod={query.inNewCodePeriod}
stats={facets.createdAt}
+ forceShow={showAllFilters}
/>
<LanguageFacet
- fetching={this.props.loadingFacets.languages === true}
- loadSearchResultCount={this.props.loadSearchResultCount}
+ fetching={loadingFacets.languages === true}
+ loadSearchResultCount={loadSearchResultCount}
onChange={this.props.onFilterChange}
onToggle={this.props.onFacetToggle}
open={!!openFacets.languages}
query={query}
- referencedLanguages={this.props.referencedLanguages}
+ referencedLanguages={referencedLanguages}
selectedLanguages={query.languages}
stats={facets.languages}
+ forceShow={showAllFilters}
/>
<RuleFacet
- fetching={this.props.loadingFacets.rules === true}
- loadSearchResultCount={this.props.loadSearchResultCount}
+ fetching={loadingFacets.rules === true}
+ loadSearchResultCount={loadSearchResultCount}
onChange={this.props.onFilterChange}
onToggle={this.props.onFacetToggle}
open={!!openFacets.rules}
query={query}
- referencedRules={this.props.referencedRules}
+ referencedRules={referencedRules}
stats={facets.rules}
+ forceShow={showAllFilters}
/>
<TagFacet
component={component}
branch={branch}
- fetching={this.props.loadingFacets.tags === true}
- loadSearchResultCount={this.props.loadSearchResultCount}
+ fetching={loadingFacets.tags === true}
+ loadSearchResultCount={loadSearchResultCount}
onChange={this.props.onFilterChange}
onToggle={this.props.onFacetToggle}
open={!!openFacets.tags}
query={query}
stats={facets.tags}
tags={query.tags}
+ forceShow={showAllFilters}
/>
{displayProjectsFacet && (
<ProjectFacet
component={component}
- fetching={this.props.loadingFacets.projects === true}
- loadSearchResultCount={this.props.loadSearchResultCount}
+ fetching={loadingFacets.projects === true}
+ loadSearchResultCount={loadSearchResultCount}
onChange={this.props.onFilterChange}
onToggle={this.props.onFacetToggle}
open={!!openFacets.projects}
projects={query.projects}
query={query}
- referencedComponents={this.props.referencedComponentsByKey}
+ referencedComponents={referencedComponentsByKey}
stats={facets.projects}
+ forceShow={showAllFilters}
/>
)}
{this.renderComponentFacets()}
@@ -298,27 +329,29 @@ export class Sidebar extends React.PureComponent<Props> {
<AssigneeFacet
assigned={query.assigned}
assignees={query.assignees}
- fetching={this.props.loadingFacets.assignees === true}
- loadSearchResultCount={this.props.loadSearchResultCount}
+ fetching={loadingFacets.assignees === true}
+ loadSearchResultCount={loadSearchResultCount}
onChange={this.props.onFilterChange}
onToggle={this.props.onFacetToggle}
open={!!openFacets.assignees}
query={query}
- referencedUsers={this.props.referencedUsers}
+ referencedUsers={referencedUsers}
stats={facets.assignees}
+ forceShow={showAllFilters}
/>
)}
{displayAuthorFacet && !disableDeveloperAggregatedInfo && (
<AuthorFacet
author={query.author}
component={component}
- fetching={this.props.loadingFacets.author === true}
- loadSearchResultCount={this.props.loadSearchResultCount}
+ fetching={loadingFacets.author === true}
+ loadSearchResultCount={loadSearchResultCount}
onChange={this.props.onFilterChange}
onToggle={this.props.onFacetToggle}
open={!!openFacets.author}
query={query}
stats={facets.author}
+ forceShow={showAllFilters}
/>
)}
</>
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 acc5443f5a0..c5b9c80cdf0 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
@@ -64,6 +64,7 @@ interface Props {
sonarsourceSecurity: string[];
sonarsourceSecurityOpen: boolean;
sonarsourceSecurityStats: Dict<number> | undefined;
+ forceShow: boolean;
}
interface State {
@@ -99,19 +100,13 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
this.mounted = true;
// load standards.json only if the facet is open, or there is a selected value
- if (
- this.props.open ||
- this.props.owaspTop10.length > 0 ||
- this.props['owaspTop10-2021'].length > 0 ||
- this.props.cwe.length > 0 ||
- this.props.sonarsourceSecurity.length > 0
- ) {
+ if (this.isFacetVisible() || this.props.open) {
this.loadStandards();
}
}
componentDidUpdate(prevProps: Props) {
- if (!prevProps.open && this.props.open) {
+ if (!prevProps.open && this.props.open && this.isFacetVisible()) {
this.loadStandards();
}
}
@@ -150,17 +145,15 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
};
getValues = () => {
+ const { sonarsourceSecurity, owaspTop10, 'owaspTop10-2021': owaspTop2021, cwe } = this.props;
+ const { standards } = this.state;
return [
- ...this.props.sonarsourceSecurity.map((item) =>
- renderSonarSourceSecurityCategory(this.state.standards, item, true)
+ ...sonarsourceSecurity.map((item) =>
+ renderSonarSourceSecurityCategory(standards, item, true)
),
- ...this.props.owaspTop10.map((item) =>
- renderOwaspTop10Category(this.state.standards, item, true)
- ),
- ...this.props['owaspTop10-2021'].map((item) =>
- renderOwaspTop102021Category(this.state.standards, item, true)
- ),
- ...this.props.cwe.map((item) => renderCWECategory(this.state.standards, item)),
+ ...owaspTop10.map((item) => renderOwaspTop10Category(standards, item, true)),
+ ...owaspTop2021.map((item) => renderOwaspTop102021Category(standards, item, true)),
+ ...cwe.map((item) => renderCWECategory(standards, item)),
];
};
@@ -231,6 +224,19 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
: Promise.resolve({});
};
+ isFacetVisible = () => {
+ const {
+ forceShow,
+ cwe,
+ sonarsourceSecurity,
+ owaspTop10,
+ 'owaspTop10-2021': owaspTop2021,
+ } = this.props;
+ const values = [...cwe, ...sonarsourceSecurity, ...owaspTop10, ...owaspTop2021];
+
+ return !(values.length < 1 && !forceShow);
+ };
+
renderList = (
statsProp: StatsProp,
valuesProp: ValuesProp,
@@ -318,8 +324,8 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
}
renderSonarSourceSecurityList() {
- const stats = this.props.sonarsourceSecurityStats;
- const values = this.props.sonarsourceSecurity;
+ const { sonarsourceSecurityStats: stats, sonarsourceSecurity: values } = this.props;
+ const { standards, showFullSonarSourceList } = this.state;
if (!stats) {
return null;
@@ -328,15 +334,15 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
const sortedItems = sortBy(
Object.keys(stats),
(key) => -stats[key],
- (key) => renderSonarSourceSecurityCategory(this.state.standards, key)
+ (key) => renderSonarSourceSecurityCategory(standards, key)
);
- const limitedList = this.state.showFullSonarSourceList
+ const limitedList = showFullSonarSourceList
? sortedItems
: sortedItems.slice(0, INITIAL_FACET_COUNT);
// make sure all selected items are displayed
- const selectedBelowLimit = this.state.showFullSonarSourceList
+ const selectedBelowLimit = showFullSonarSourceList
? []
: sortedItems.slice(INITIAL_FACET_COUNT).filter((item) => values.includes(item));
@@ -415,6 +421,8 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
sonarsourceSecurity,
sonarsourceSecurityOpen,
} = this.props;
+ const { standards } = this.state;
+
return (
<>
<FacetBox className="is-inner" property={SecurityStandard.SONARSOURCE}>
@@ -424,7 +432,7 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
onClick={this.handleSonarSourceSecurityHeaderClick}
open={sonarsourceSecurityOpen}
values={sonarsourceSecurity.map((item) =>
- renderSonarSourceSecurityCategory(this.state.standards, item)
+ renderSonarSourceSecurityCategory(standards, item)
)}
/>
{sonarsourceSecurityOpen && (
@@ -440,9 +448,7 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
name={translate('issues.facet.owaspTop10_2021')}
onClick={this.handleOwaspTop102021HeaderClick}
open={owaspTop102021Open}
- values={owaspTop102021.map((item) =>
- renderOwaspTop102021Category(this.state.standards, item)
- )}
+ values={owaspTop102021.map((item) => renderOwaspTop102021Category(standards, item))}
/>
{owaspTop102021Open && (
<>
@@ -470,9 +476,9 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
className="is-inner"
facetHeader={translate('issues.facet.cwe')}
fetching={fetchingCwe}
- getFacetItemText={(item) => renderCWECategory(this.state.standards, item)}
+ getFacetItemText={(item) => renderCWECategory(standards, item)}
getSearchResultKey={(item) => item}
- getSearchResultText={(item) => renderCWECategory(this.state.standards, item)}
+ getSearchResultText={(item) => renderCWECategory(standards, item)}
loadSearchResultCount={this.loadCWESearchResultCount}
onChange={this.props.onChange}
onSearch={this.handleCWESearch}
@@ -480,9 +486,9 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
open={cweOpen}
property={SecurityStandard.CWE}
query={omit(query, 'cwe')}
- renderFacetItem={(item) => renderCWECategory(this.state.standards, item)}
+ renderFacetItem={(item) => renderCWECategory(standards, item)}
renderSearchResult={(item, query) =>
- highlightTerm(renderCWECategory(this.state.standards, item), query)
+ highlightTerm(renderCWECategory(standards, item), query)
}
searchPlaceholder={translate('search.search_for_cwe')}
stats={cweStats}
@@ -495,6 +501,10 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
render() {
const { open } = this.props;
+ if (!this.isFacetVisible()) {
+ return null;
+ }
+
return (
<FacetBox property={this.property}>
<FacetHeader
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 d9143084c86..c2130d2a157 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
@@ -36,6 +36,7 @@ interface Props {
open: boolean;
stats: Dict<number> | undefined;
statuses: string[];
+ forceShow: boolean;
}
const STATUSES = ['OPEN', 'CONFIRMED', 'REOPENED', 'RESOLVED', 'CLOSED'];
@@ -73,7 +74,8 @@ export default class StatusFacet extends React.PureComponent<Props> {
}
renderItem = (status: string) => {
- const active = this.props.statuses.includes(status);
+ const { statuses } = this.props;
+ const active = statuses.includes(status);
const stat = this.getStat(status);
return (
@@ -91,9 +93,13 @@ export default class StatusFacet extends React.PureComponent<Props> {
};
render() {
- const { fetching, open, statuses, stats = {} } = this.props;
+ const { statuses, stats = {}, forceShow, fetching, open } = this.props;
const values = statuses.map((status) => translate('issue.status', status));
+ if (values.length < 1 && !forceShow) {
+ return null;
+ }
+
return (
<FacetBox property={this.property}>
<FacetHeader
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 3744f609b40..46a8d52955a 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
@@ -40,6 +40,7 @@ interface Props {
query: Query;
stats: Dict<number> | undefined;
tags: string[];
+ forceShow: boolean;
}
const SEARCH_SIZE = 100;
@@ -82,10 +83,16 @@ export default class TagFacet extends React.PureComponent<Props> {
);
render() {
+ const { forceShow, tags, fetching, stats, open, query } = this.props;
+
+ if (tags.length < 1 && !forceShow) {
+ return null;
+ }
+
return (
<ListStyleFacet<string>
facetHeader={translate('issues.facet.tags')}
- fetching={this.props.fetching}
+ fetching={fetching}
getFacetItemText={this.getTagName}
getSearchResultKey={(tag) => tag}
getSearchResultText={(tag) => tag}
@@ -93,14 +100,14 @@ export default class TagFacet extends React.PureComponent<Props> {
onChange={this.props.onChange}
onSearch={this.handleSearch}
onToggle={this.props.onToggle}
- open={this.props.open}
+ open={open}
property="tags"
- query={omit(this.props.query, 'tags')}
+ query={omit(query, 'tags')}
renderFacetItem={this.renderTag}
renderSearchResult={this.renderSearchResult}
searchPlaceholder={translate('search.search_for_tags')}
- stats={this.props.stats}
- values={this.props.tags}
+ stats={stats}
+ values={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 28b1a8f8668..51ea8f58f4b 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
@@ -37,6 +37,7 @@ interface Props {
open: boolean;
stats: Dict<number> | undefined;
types: string[];
+ forceShow: boolean;
}
export default class TypeFacet extends React.PureComponent<Props> {
@@ -99,9 +100,13 @@ export default class TypeFacet extends React.PureComponent<Props> {
};
render() {
- const { fetching, open, types, stats = {} } = this.props;
+ const { types, stats = {}, forceShow, open, fetching } = this.props;
const values = types.map((type) => translate('issue.type', type));
+ if (values.length < 1 && !forceShow) {
+ return null;
+ }
+
return (
<FacetBox property={this.property}>
<FacetHeader
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-it.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-it.tsx
index a02ea0f3be6..b1d4d1ca4dd 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-it.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-it.tsx
@@ -24,6 +24,7 @@ import { mockQuery } from '../../../../helpers/mocks/issues';
import { mockAppState } from '../../../../helpers/testMocks';
import { renderComponent } from '../../../../helpers/testReactTestingUtils';
import { ComponentQualifier } from '../../../../types/component';
+import { IssueSeverity } from '../../../../types/issues';
import { GlobalSettingKeys } from '../../../../types/settings';
import { Sidebar } from '../Sidebar';
@@ -93,6 +94,55 @@ it('should render correct facets for SubPortfolio', () => {
]);
});
+it('should render only main visible facets: Characteristics & Severity', () => {
+ renderSidebar({
+ component: mockComponent(),
+ showAllFilters: false,
+ query: mockQuery({ assigned: true }),
+ });
+
+ expect(screen.getAllByRole('button').map((button) => button.textContent)).toStrictEqual([
+ 'issues.facet.characteristics.PRODUCTION',
+ 'issues.facet.characteristics.DEVELOPMENT',
+ 'issues.facet.severities',
+ ]);
+});
+
+it('should render secondary facets with filters applied eventhough "Show more filters" button isn`t toggled', () => {
+ renderSidebar({
+ component: mockComponent(),
+ showAllFilters: false,
+ query: mockQuery({
+ assigned: false,
+ tags: ['tag'],
+ rules: ['rule'],
+ directories: ['directory'],
+ cwe: ['security'],
+ languages: ['java'],
+ severities: [IssueSeverity.Blocker],
+ }),
+ });
+
+ expect(screen.getAllByRole('button').map((button) => button.textContent)).toStrictEqual([
+ 'issues.facet.characteristics.PRODUCTION',
+ 'issues.facet.characteristics.DEVELOPMENT',
+ 'issues.facet.severities',
+ 'clear',
+ 'issues.facet.standards',
+ 'clear',
+ 'issues.facet.languages',
+ 'clear',
+ 'issues.facet.rules',
+ 'clear',
+ 'issues.facet.tags',
+ 'clear',
+ 'issues.facet.directories',
+ 'clear',
+ 'issues.facet.assignees',
+ 'clear',
+ ]);
+});
+
it.each([
['week', '1w'],
['month', '1m'],
@@ -129,6 +179,7 @@ function renderSidebar(props: Partial<Sidebar['props']> = {}) {
referencedLanguages={{}}
referencedRules={{}}
referencedUsers={{}}
+ showAllFilters={true}
{...props}
/>
);
diff --git a/server/sonar-web/src/main/js/apps/issues/test-utils.tsx b/server/sonar-web/src/main/js/apps/issues/test-utils.tsx
index 24654152bb9..d52b7ac367c 100644
--- a/server/sonar-web/src/main/js/apps/issues/test-utils.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/test-utils.tsx
@@ -89,6 +89,8 @@ export const ui = {
dateInputYearSelect: byRole('combobox', { name: 'Year:' }),
clearAllFilters: byRole('button', { name: 'clear_all_filters' }),
+ showFiltersButton: (showMore = true) =>
+ byRole('button', { name: `issues.show_${showMore ? 'more' : 'less'}_filters` }),
ruleFacetList: byRole('list', { name: 'rules' }),
languageFacetList: byRole('list', { name: 'languages' }),